0.1.0-0-pre.3
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# file: crates/apps/game-reflex-poc-desktop/Cargo.toml
|
||||
# version: 1
|
||||
# version: 2
|
||||
|
||||
[package]
|
||||
name = "game-reflex-poc-desktop"
|
||||
@@ -11,6 +11,8 @@ authors.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
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" }
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/apps/game-reflex-poc-desktop/src/main.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -7,10 +7,16 @@
|
||||
|
||||
//! Desktop development runner for the Reflex POC game library.
|
||||
//!
|
||||
//! This binary is intentionally minimal until the executable SDL3 runtime is introduced.
|
||||
//! 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 state = game_reflex_poc::ReflexState::new();
|
||||
println!("Reflex POC desktop runner: score={}", state.score());
|
||||
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));
|
||||
let mut state = game_reflex_poc::ReflexState::new();
|
||||
for _ in 0..3 {
|
||||
runner.tick(&mut state, input);
|
||||
}
|
||||
println!("Reflex POC desktop runner: score={} frames={}", state.score(), runner.next_frame_index());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# file: crates/apps/game-snake-poc-desktop/Cargo.toml
|
||||
# version: 1
|
||||
# version: 2
|
||||
|
||||
[package]
|
||||
name = "game-snake-poc-desktop"
|
||||
@@ -11,6 +11,8 @@ authors.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
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" }
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/apps/game-snake-poc-desktop/src/main.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -7,10 +7,16 @@
|
||||
|
||||
//! Desktop development runner for the Snake POC game library.
|
||||
//!
|
||||
//! This binary is intentionally minimal until the executable SDL3 runtime is introduced.
|
||||
//! 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 state = game_snake_poc::SnakeState::new();
|
||||
println!("Snake POC desktop runner: length={}", state.length());
|
||||
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));
|
||||
let mut state = game_snake_poc::SnakeState::new();
|
||||
for _ in 0..3 {
|
||||
runner.tick(&mut state, input);
|
||||
}
|
||||
println!("Snake POC desktop runner: length={} frames={}", state.length(), runner.next_frame_index());
|
||||
return;
|
||||
}
|
||||
|
||||
36
crates/engines/engine-v1-common/src/frame.rs
Normal file
36
crates/engines/engine-v1-common/src/frame.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
// file: crates/engines/engine-v1-common/src/frame.rs
|
||||
// version: 1
|
||||
|
||||
/// Immutable timing information associated with one engine update.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct EngineFrame {
|
||||
index: u64,
|
||||
delta: std::time::Duration,
|
||||
elapsed: std::time::Duration,
|
||||
}
|
||||
|
||||
impl EngineFrame {
|
||||
/// Creates timing information for one update.
|
||||
#[must_use]
|
||||
pub const fn new(index: u64, delta: std::time::Duration, elapsed: std::time::Duration) -> Self {
|
||||
return Self { index, delta, elapsed };
|
||||
}
|
||||
|
||||
/// Returns the zero-based update index.
|
||||
#[must_use]
|
||||
pub const fn index(self) -> u64 {
|
||||
return self.index;
|
||||
}
|
||||
|
||||
/// Returns the duration represented by this update.
|
||||
#[must_use]
|
||||
pub const fn delta(self) -> std::time::Duration {
|
||||
return self.delta;
|
||||
}
|
||||
|
||||
/// Returns the accumulated simulated duration before this update completes.
|
||||
#[must_use]
|
||||
pub const fn elapsed(self) -> std::time::Duration {
|
||||
return self.elapsed;
|
||||
}
|
||||
}
|
||||
83
crates/engines/engine-v1-common/src/game_loop.rs
Normal file
83
crates/engines/engine-v1-common/src/game_loop.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
// file: crates/engines/engine-v1-common/src/game_loop.rs
|
||||
// version: 1
|
||||
|
||||
/// Minimal update contract implemented by a game state consumed by engine V1.
|
||||
pub trait EngineGame {
|
||||
/// Advances the game by one engine update using platform-independent input.
|
||||
fn update(&mut self, frame: crate::EngineFrame, input: crate::InputState);
|
||||
}
|
||||
|
||||
/// Deterministic fixed-step driver used before and underneath platform event loops.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct FixedStepRunner {
|
||||
delta: std::time::Duration,
|
||||
next_frame_index: u64,
|
||||
elapsed: std::time::Duration,
|
||||
}
|
||||
|
||||
impl FixedStepRunner {
|
||||
/// Creates a fixed-step runner using the supplied update duration.
|
||||
#[must_use]
|
||||
pub const fn new(delta: std::time::Duration) -> Self {
|
||||
return Self { delta, next_frame_index: 0, elapsed: std::time::Duration::ZERO };
|
||||
}
|
||||
|
||||
/// Returns the configured fixed update duration.
|
||||
#[must_use]
|
||||
pub const fn delta(self) -> std::time::Duration {
|
||||
return self.delta;
|
||||
}
|
||||
|
||||
/// Returns the index that will be assigned to the next update.
|
||||
#[must_use]
|
||||
pub const fn next_frame_index(self) -> u64 {
|
||||
return self.next_frame_index;
|
||||
}
|
||||
|
||||
/// Returns the accumulated simulated time.
|
||||
#[must_use]
|
||||
pub const fn elapsed(self) -> std::time::Duration {
|
||||
return self.elapsed;
|
||||
}
|
||||
|
||||
/// Advances one game state by one deterministic update.
|
||||
pub fn tick<G>(&mut self, game: &mut G, input: crate::InputState)
|
||||
where
|
||||
G: crate::EngineGame,
|
||||
{
|
||||
let frame = crate::EngineFrame::new(self.next_frame_index, self.delta, self.elapsed);
|
||||
game.update(frame, input);
|
||||
self.next_frame_index = self.next_frame_index.saturating_add(1);
|
||||
self.elapsed = self.elapsed.saturating_add(self.delta);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
#[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));
|
||||
}
|
||||
}
|
||||
61
crates/engines/engine-v1-common/src/input.rs
Normal file
61
crates/engines/engine-v1-common/src/input.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
// file: crates/engines/engine-v1-common/src/input.rs
|
||||
// version: 1
|
||||
|
||||
/// Platform-independent snapshot of logical input actions for one update.
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub struct InputState {
|
||||
left: bool,
|
||||
right: bool,
|
||||
up: bool,
|
||||
down: bool,
|
||||
primary: bool,
|
||||
secondary: bool,
|
||||
pause: bool,
|
||||
}
|
||||
|
||||
impl InputState {
|
||||
/// Creates a snapshot with no active action.
|
||||
#[must_use]
|
||||
pub const fn none() -> Self {
|
||||
return Self { left: false, right: false, up: false, down: false, primary: false, secondary: false, pause: false };
|
||||
}
|
||||
|
||||
/// Returns a copy with the selected logical action set to the requested state.
|
||||
#[must_use]
|
||||
pub const fn with_action(mut self, action: crate::GameAction, active: bool) -> Self {
|
||||
match action {
|
||||
crate::GameAction::Left => self.left = active,
|
||||
crate::GameAction::Right => self.right = active,
|
||||
crate::GameAction::Up => self.up = active,
|
||||
crate::GameAction::Down => self.down = active,
|
||||
crate::GameAction::Primary => self.primary = active,
|
||||
crate::GameAction::Secondary => self.secondary = active,
|
||||
crate::GameAction::Pause => self.pause = active,
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
/// Reports whether the selected logical action is active.
|
||||
#[must_use]
|
||||
pub const fn is_active(self, action: crate::GameAction) -> bool {
|
||||
return match action {
|
||||
crate::GameAction::Left => self.left,
|
||||
crate::GameAction::Right => self.right,
|
||||
crate::GameAction::Up => self.up,
|
||||
crate::GameAction::Down => self.down,
|
||||
crate::GameAction::Primary => self.primary,
|
||||
crate::GameAction::Secondary => self.secondary,
|
||||
crate::GameAction::Pause => self.pause,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[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));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/engines/engine-v1-common/src/lib.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -8,6 +8,17 @@
|
||||
//! Common, platform-independent primitives for engine generation V1.
|
||||
|
||||
mod action;
|
||||
mod frame;
|
||||
mod game_loop;
|
||||
mod input;
|
||||
|
||||
/// Re-export of the canonical logical input action used by engine V1 games.
|
||||
pub use self::action::GameAction;
|
||||
/// Re-export of immutable timing information for one engine update.
|
||||
pub use self::frame::EngineFrame;
|
||||
/// Re-export of the minimal game update contract consumed by engine V1.
|
||||
pub use self::game_loop::EngineGame;
|
||||
/// Re-export of the deterministic fixed-step driver.
|
||||
pub use self::game_loop::FixedStepRunner;
|
||||
/// Re-export of the platform-independent logical input snapshot.
|
||||
pub use self::input::InputState;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/engines/engine-v1-platform-api/src/lib.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -9,5 +9,9 @@
|
||||
|
||||
mod service;
|
||||
|
||||
/// Re-export of optional monetization capability declarations.
|
||||
pub use self::service::MonetizationCapabilities;
|
||||
/// Re-export of the minimal platform service capability enumeration.
|
||||
pub use self::service::PlatformCapability;
|
||||
/// Re-export of optional platform service availability states.
|
||||
pub use self::service::PlatformServiceAvailability;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/engines/engine-v1-platform-api/src/service.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
/// Platform capabilities that may have different implementations per target.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
@@ -15,3 +15,57 @@ pub enum PlatformCapability {
|
||||
/// Online leaderboard integration.
|
||||
Leaderboard,
|
||||
}
|
||||
|
||||
/// Availability state of one optional platform service.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum PlatformServiceAvailability {
|
||||
/// The platform implementation is present and may be used.
|
||||
Available,
|
||||
/// The platform could support the service, but this build intentionally disables it.
|
||||
Disabled,
|
||||
/// The current target or distribution does not provide this service implementation.
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
/// Declarative availability of optional monetization services for one build.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct MonetizationCapabilities {
|
||||
advertising: PlatformServiceAvailability,
|
||||
billing: PlatformServiceAvailability,
|
||||
}
|
||||
|
||||
impl MonetizationCapabilities {
|
||||
/// Creates a monetization capability declaration.
|
||||
#[must_use]
|
||||
pub const fn new(advertising: PlatformServiceAvailability, billing: PlatformServiceAvailability) -> Self {
|
||||
return Self { advertising, billing };
|
||||
}
|
||||
|
||||
/// Creates a declaration with monetization intentionally disabled.
|
||||
#[must_use]
|
||||
pub const fn disabled() -> Self {
|
||||
return Self::new(PlatformServiceAvailability::Disabled, PlatformServiceAvailability::Disabled);
|
||||
}
|
||||
|
||||
/// Returns advertising availability.
|
||||
#[must_use]
|
||||
pub const fn advertising(self) -> PlatformServiceAvailability {
|
||||
return self.advertising;
|
||||
}
|
||||
|
||||
/// Returns billing availability.
|
||||
#[must_use]
|
||||
pub const fn billing(self) -> PlatformServiceAvailability {
|
||||
return self.billing;
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/games/game-reflex-poc/src/state.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
/// Minimal deterministic state used to validate the first game boundary.
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
@@ -26,6 +26,15 @@ impl ReflexState {
|
||||
}
|
||||
}
|
||||
|
||||
impl engine_v1_common::EngineGame for ReflexState {
|
||||
fn update(&mut self, _frame: engine_v1_common::EngineFrame, input: engine_v1_common::InputState) {
|
||||
if input.is_active(engine_v1_common::GameAction::Primary) {
|
||||
self.register_success();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_tests {
|
||||
#[test]
|
||||
@@ -34,4 +43,16 @@ mod unit_tests {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/games/game-snake-poc/src/state.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
/// Minimal Snake state used to validate reusable engine dependencies.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
@@ -32,6 +32,15 @@ impl SnakeState {
|
||||
}
|
||||
}
|
||||
|
||||
impl engine_v1_common::EngineGame for SnakeState {
|
||||
fn update(&mut self, _frame: engine_v1_common::EngineFrame, input: engine_v1_common::InputState) {
|
||||
if input.is_active(engine_v1_common::GameAction::Primary) {
|
||||
self.grow();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_tests {
|
||||
#[test]
|
||||
@@ -40,4 +49,16 @@ mod unit_tests {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user