0.1.0-0-pre.4

This commit is contained in:
2026-09-16 01:01:09 +02:00
parent 2c79a05dd6
commit dc22011997
38 changed files with 540 additions and 131 deletions

View File

@@ -1,5 +1,5 @@
# file: crates/apps/game-reflex-poc-desktop/Cargo.toml
# version: 2
# version: 3
[package]
name = "game-reflex-poc-desktop"
@@ -11,6 +11,8 @@ authors.workspace = true
publish.workspace = true
[dependencies]
game-logging-lib = { path = "../../common/game-logging-lib" }
tracing.workspace = true
engine-v1-common = { path = "../../engines/engine-v1-common" }
engine-v1-platform-api = { path = "../../engines/engine-v1-platform-api" }
game-reflex-poc = { path = "../../games/game-reflex-poc" }

View File

@@ -1,5 +1,5 @@
// file: crates/apps/game-reflex-poc-desktop/src/main.rs
// version: 2
// version: 3
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -10,6 +10,14 @@
//! This binary exercises the platform-independent fixed-step loop. SDL3 windowing and physical input mapping are introduced in the next dedicated delta.
fn main() {
let _logging_guard = match game_logging_lib::init_console_tracing() {
std::result::Result::Ok(guard) => guard,
std::result::Result::Err(error) => {
eprintln!("failed to initialize tracing: {error}");
return;
},
};
tracing::info!(target: "games::runner", game = "reflex", "desktop POC runner started");
let _monetization = engine_v1_platform_api::MonetizationCapabilities::disabled();
let input = engine_v1_common::InputState::none().with_action(engine_v1_common::GameAction::Primary, true);
let mut runner = engine_v1_common::FixedStepRunner::new(std::time::Duration::from_millis(16));
@@ -17,6 +25,7 @@ fn main() {
for _ in 0..3 {
runner.tick(&mut state, input);
}
tracing::info!(target: "games::runner", game = "reflex", frames = runner.next_frame_index(), "desktop POC runner completed");
println!("Reflex POC desktop runner: score={} frames={}", state.score(), runner.next_frame_index());
return;
}

View File

@@ -1,5 +1,5 @@
# file: crates/apps/game-snake-poc-desktop/Cargo.toml
# version: 2
# version: 3
[package]
name = "game-snake-poc-desktop"
@@ -11,6 +11,8 @@ authors.workspace = true
publish.workspace = true
[dependencies]
game-logging-lib = { path = "../../common/game-logging-lib" }
tracing.workspace = true
engine-v1-common = { path = "../../engines/engine-v1-common" }
engine-v1-platform-api = { path = "../../engines/engine-v1-platform-api" }
game-snake-poc = { path = "../../games/game-snake-poc" }

View File

@@ -1,5 +1,5 @@
// file: crates/apps/game-snake-poc-desktop/src/main.rs
// version: 2
// version: 3
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -10,6 +10,14 @@
//! This binary exercises the platform-independent fixed-step loop. SDL3 windowing and physical input mapping are introduced in the next dedicated delta.
fn main() {
let _logging_guard = match game_logging_lib::init_console_tracing() {
std::result::Result::Ok(guard) => guard,
std::result::Result::Err(error) => {
eprintln!("failed to initialize tracing: {error}");
return;
},
};
tracing::info!(target: "games::runner", game = "snake", "desktop POC runner started");
let _monetization = engine_v1_platform_api::MonetizationCapabilities::disabled();
let input = engine_v1_common::InputState::none().with_action(engine_v1_common::GameAction::Primary, true);
let mut runner = engine_v1_common::FixedStepRunner::new(std::time::Duration::from_millis(16));
@@ -17,6 +25,7 @@ fn main() {
for _ in 0..3 {
runner.tick(&mut state, input);
}
tracing::info!(target: "games::runner", game = "snake", frames = runner.next_frame_index(), "desktop POC runner completed");
println!("Snake POC desktop runner: length={} frames={}", state.length(), runner.next_frame_index());
return;
}

View File

@@ -0,0 +1,19 @@
# file: crates/common/game-logging-lib/Cargo.toml
# version: 1
[package]
name = "game-logging-lib"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
authors.workspace = true
publish.workspace = true
[dependencies]
tracing.workspace = true
tracing-appender.workspace = true
tracing-subscriber.workspace = true
[lints]
workspace = true

View File

@@ -0,0 +1,20 @@
// file: crates/common/game-logging-lib/src/lib.rs
// version: 1
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
//! Shared tracing initialization and test diagnostics for games.sasedev applications and crates.
mod runtime;
mod test_support;
/// Re-export of the console tracing initialization error.
pub use self::runtime::LoggingInitError;
/// Re-export of the guard keeping the non-blocking tracing writer alive.
pub use self::runtime::LoggingWorkerGuard;
/// Re-export of the standard console tracing initializer.
pub use self::runtime::init_console_tracing;
/// Re-export of the scoped tracing helper intended for tests.
pub use self::test_support::with_test_tracing;

View File

@@ -0,0 +1,31 @@
// file: crates/common/game-logging-lib/src/runtime.rs
// version: 1
/// Error returned when the process-global tracing subscriber is already configured.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct LoggingInitError;
impl std::fmt::Display for LoggingInitError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.write_str("the process-global tracing subscriber is already configured");
}
}
impl std::error::Error for LoggingInitError {}
/// Guard keeping the non-blocking tracing writer alive for the application lifetime.
pub struct LoggingWorkerGuard {
_worker_guard: tracing_appender::non_blocking::WorkerGuard,
}
/// Initializes a process-global non-blocking tracing subscriber writing formatted events to standard error.
///
/// The returned guard must remain alive for as long as events may still be emitted.
pub fn init_console_tracing() -> std::result::Result<LoggingWorkerGuard, LoggingInitError> {
let (writer, worker_guard) = tracing_appender::non_blocking(std::io::stderr());
let subscriber = tracing_subscriber::fmt().with_target(true).with_writer(writer).finish();
if tracing::subscriber::set_global_default(subscriber).is_err() {
return std::result::Result::Err(LoggingInitError);
}
return std::result::Result::Ok(LoggingWorkerGuard { _worker_guard: worker_guard });
}

View File

@@ -0,0 +1,13 @@
// file: crates/common/game-logging-lib/src/test_support.rs
// version: 1
/// Executes one test body with a thread-local tracing subscriber configured for the Rust test writer.
pub fn with_test_tracing<T>(test_name: &str, operation: impl FnOnce() -> T) -> T {
let subscriber = tracing_subscriber::fmt().with_target(true).with_test_writer().without_time().finish();
return tracing::subscriber::with_default(subscriber, || {
tracing::debug!(test_name = test_name, "test started");
let result = operation();
tracing::debug!(test_name = test_name, "test completed");
return result;
});
}

View File

@@ -0,0 +1,11 @@
// file: crates/common/game-logging-lib/tests/test_support.rs
// version: 1
#[test]
fn scoped_test_tracing_returns_test_result() {
let result = game_logging_lib::with_test_tracing("scoped_test_tracing_returns_test_result", || {
tracing::info!(value = 41_u32, "integration test event");
return 42_u32;
});
assert_eq!(result, 42_u32);
}

View File

@@ -1,5 +1,5 @@
# file: crates/engines/engine-v1-common/Cargo.toml
# version: 1
# version: 2
[package]
name = "engine-v1-common"
@@ -10,5 +10,8 @@ repository.workspace = true
authors.workspace = true
publish.workspace = true
[dev-dependencies]
game-logging-lib = { path = "../../common/game-logging-lib" }
[lints]
workspace = true

View File

@@ -1,5 +1,5 @@
// file: crates/engines/engine-v1-common/src/game_loop.rs
// version: 1
// version: 2
/// Minimal update contract implemented by a game state consumed by engine V1.
pub trait EngineGame {
@@ -54,30 +54,5 @@ impl FixedStepRunner {
}
#[cfg(test)]
mod unit_tests {
#[derive(Default)]
struct CountingGame {
updates: u64,
last_frame: Option<crate::EngineFrame>,
}
impl crate::EngineGame for CountingGame {
fn update(&mut self, frame: crate::EngineFrame, _input: crate::InputState) {
self.updates = self.updates.saturating_add(1);
self.last_frame = Some(frame);
return;
}
}
#[test]
fn fixed_step_runner_advances_index_and_elapsed_time() {
let mut game = CountingGame::default();
let mut runner = crate::FixedStepRunner::new(std::time::Duration::from_millis(16));
runner.tick(&mut game, crate::InputState::none());
runner.tick(&mut game, crate::InputState::none());
assert_eq!(game.updates, 2);
assert_eq!(runner.next_frame_index(), 2);
assert_eq!(runner.elapsed(), std::time::Duration::from_millis(32));
assert_eq!(game.last_frame.map(crate::EngineFrame::index), Some(1));
}
}
#[path = "../unit_tests/game_loop.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/engines/engine-v1-common/src/input.rs
// version: 1
// version: 2
/// Platform-independent snapshot of logical input actions for one update.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
@@ -51,11 +51,5 @@ impl InputState {
}
#[cfg(test)]
mod unit_tests {
#[test]
fn action_state_is_independent() {
let state = crate::InputState::none().with_action(crate::GameAction::Primary, true);
assert!(state.is_active(crate::GameAction::Primary));
assert!(!state.is_active(crate::GameAction::Secondary));
}
}
#[path = "../unit_tests/input.rs"]
mod tests;

View File

@@ -0,0 +1,30 @@
// file: crates/engines/engine-v1-common/unit_tests/game_loop.rs
// version: 1
#[derive(Default)]
struct CountingGame {
updates: u64,
last_frame: std::option::Option<crate::EngineFrame>,
}
impl crate::EngineGame for CountingGame {
fn update(&mut self, frame: crate::EngineFrame, _input: crate::InputState) {
self.updates = self.updates.saturating_add(1);
self.last_frame = std::option::Option::Some(frame);
return;
}
}
#[test]
fn fixed_step_runner_advances_index_and_elapsed_time() {
game_logging_lib::with_test_tracing("fixed_step_runner_advances_index_and_elapsed_time", || {
let mut game = CountingGame::default();
let mut runner = crate::FixedStepRunner::new(std::time::Duration::from_millis(16));
runner.tick(&mut game, crate::InputState::none());
runner.tick(&mut game, crate::InputState::none());
assert_eq!(game.updates, 2);
assert_eq!(runner.next_frame_index(), 2);
assert_eq!(runner.elapsed(), std::time::Duration::from_millis(32));
assert_eq!(game.last_frame.map(crate::EngineFrame::index), std::option::Option::Some(1));
});
}

View File

@@ -0,0 +1,12 @@
// file: crates/engines/engine-v1-common/unit_tests/input.rs
// version: 1
#[test]
fn action_state_is_independent() {
game_logging_lib::with_test_tracing("action_state_is_independent", || {
let input = crate::InputState::none().with_action(crate::GameAction::Primary, true);
assert!(input.is_active(crate::GameAction::Primary));
assert!(!input.is_active(crate::GameAction::Secondary));
assert!(!input.is_active(crate::GameAction::Left));
});
}

View File

@@ -1,5 +1,5 @@
# file: crates/engines/engine-v1-platform-api/Cargo.toml
# version: 1
# version: 2
[package]
name = "engine-v1-platform-api"
@@ -10,5 +10,8 @@ repository.workspace = true
authors.workspace = true
publish.workspace = true
[dev-dependencies]
game-logging-lib = { path = "../../common/game-logging-lib" }
[lints]
workspace = true

View File

@@ -1,5 +1,5 @@
// file: crates/engines/engine-v1-platform-api/src/service.rs
// version: 2
// version: 3
/// Platform capabilities that may have different implementations per target.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -61,11 +61,5 @@ impl MonetizationCapabilities {
}
#[cfg(test)]
mod unit_tests {
#[test]
fn disabled_monetization_disables_both_services() {
let capabilities = crate::MonetizationCapabilities::disabled();
assert_eq!(capabilities.advertising(), crate::PlatformServiceAvailability::Disabled);
assert_eq!(capabilities.billing(), crate::PlatformServiceAvailability::Disabled);
}
}
#[path = "../unit_tests/service.rs"]
mod tests;

View File

@@ -0,0 +1,11 @@
// file: crates/engines/engine-v1-platform-api/unit_tests/service.rs
// version: 1
#[test]
fn disabled_monetization_disables_both_services() {
game_logging_lib::with_test_tracing("disabled_monetization_disables_both_services", || {
let capabilities = crate::MonetizationCapabilities::disabled();
assert_eq!(capabilities.advertising(), crate::PlatformServiceAvailability::Disabled);
assert_eq!(capabilities.billing(), crate::PlatformServiceAvailability::Disabled);
});
}

View File

@@ -1,5 +1,5 @@
# file: crates/games/game-reflex-poc/Cargo.toml
# version: 1
# version: 2
[package]
name = "game-reflex-poc"
@@ -15,5 +15,8 @@ engine-v1-common = { path = "../../engines/engine-v1-common" }
engine-v1-platform-api = { path = "../../engines/engine-v1-platform-api" }
engine-v1-sdl = { path = "../../engines/engine-v1-sdl" }
[dev-dependencies]
game-logging-lib = { path = "../../common/game-logging-lib" }
[lints]
workspace = true

View File

@@ -1,5 +1,5 @@
// file: crates/games/game-reflex-poc/src/state.rs
// version: 2
// version: 3
/// Minimal deterministic state used to validate the first game boundary.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
@@ -36,23 +36,5 @@ impl engine_v1_common::EngineGame for ReflexState {
}
#[cfg(test)]
mod unit_tests {
#[test]
fn success_increments_score() {
let mut state = crate::ReflexState::new();
state.register_success();
assert_eq!(state.score(), 1);
}
#[test]
fn engine_update_maps_primary_action_to_success() {
let mut state = crate::ReflexState::new();
let input = engine_v1_common::InputState::none().with_action(engine_v1_common::GameAction::Primary, true);
engine_v1_common::EngineGame::update(
&mut state,
engine_v1_common::EngineFrame::new(0, std::time::Duration::from_millis(16), std::time::Duration::ZERO),
input,
);
assert_eq!(state.score(), 1);
}
}
#[path = "../unit_tests/state.rs"]
mod tests;

View File

@@ -0,0 +1,25 @@
// file: crates/games/game-reflex-poc/unit_tests/state.rs
// version: 1
#[test]
fn success_increments_score() {
game_logging_lib::with_test_tracing("success_increments_score", || {
let mut state = crate::ReflexState::new();
state.register_success();
assert_eq!(state.score(), 1);
});
}
#[test]
fn engine_update_maps_primary_action_to_success() {
game_logging_lib::with_test_tracing("engine_update_maps_primary_action_to_success", || {
let mut state = crate::ReflexState::new();
let input = engine_v1_common::InputState::none().with_action(engine_v1_common::GameAction::Primary, true);
engine_v1_common::EngineGame::update(
&mut state,
engine_v1_common::EngineFrame::new(0, std::time::Duration::from_millis(16), std::time::Duration::ZERO),
input,
);
assert_eq!(state.score(), 1);
});
}

View File

@@ -1,5 +1,5 @@
# file: crates/games/game-snake-poc/Cargo.toml
# version: 1
# version: 2
[package]
name = "game-snake-poc"
@@ -15,5 +15,8 @@ engine-v1-common = { path = "../../engines/engine-v1-common" }
engine-v1-platform-api = { path = "../../engines/engine-v1-platform-api" }
engine-v1-sdl = { path = "../../engines/engine-v1-sdl" }
[dev-dependencies]
game-logging-lib = { path = "../../common/game-logging-lib" }
[lints]
workspace = true

View File

@@ -1,5 +1,5 @@
// file: crates/games/game-snake-poc/src/state.rs
// version: 2
// version: 3
/// Minimal Snake state used to validate reusable engine dependencies.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -42,23 +42,5 @@ impl engine_v1_common::EngineGame for SnakeState {
}
#[cfg(test)]
mod unit_tests {
#[test]
fn growth_increments_length() {
let mut state = crate::SnakeState::new();
state.grow();
assert_eq!(state.length(), 2);
}
#[test]
fn engine_update_maps_primary_action_to_growth() {
let mut state = crate::SnakeState::new();
let input = engine_v1_common::InputState::none().with_action(engine_v1_common::GameAction::Primary, true);
engine_v1_common::EngineGame::update(
&mut state,
engine_v1_common::EngineFrame::new(0, std::time::Duration::from_millis(16), std::time::Duration::ZERO),
input,
);
assert_eq!(state.length(), 2);
}
}
#[path = "../unit_tests/state.rs"]
mod tests;

View File

@@ -0,0 +1,25 @@
// file: crates/games/game-snake-poc/unit_tests/state.rs
// version: 1
#[test]
fn growth_increments_length() {
game_logging_lib::with_test_tracing("growth_increments_length", || {
let mut state = crate::SnakeState::new();
state.grow();
assert_eq!(state.length(), 2);
});
}
#[test]
fn engine_update_maps_primary_action_to_growth() {
game_logging_lib::with_test_tracing("engine_update_maps_primary_action_to_growth", || {
let mut state = crate::SnakeState::new();
let input = engine_v1_common::InputState::none().with_action(engine_v1_common::GameAction::Primary, true);
engine_v1_common::EngineGame::update(
&mut state,
engine_v1_common::EngineFrame::new(0, std::time::Duration::from_millis(16), std::time::Duration::ZERO),
input,
);
assert_eq!(state.length(), 2);
});
}