From 88eda8390b0df718724ede962a9377a762e52805 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Wed, 16 Sep 2026 10:21:09 +0200 Subject: [PATCH] 0.1.0-0-pre.10 --- Android/game-reflex-poc/build.gradle | 4 +- Android/game-snake-poc/build.gradle | 4 +- Cargo.toml | 4 +- ROADMAP.md | 4 +- .../engines/engine-v1-common/src/game_loop.rs | 7 +- crates/engines/engine-v1-common/src/lib.rs | 13 +- crates/engines/engine-v1-common/src/render.rs | 167 ++++++++++++++++++ .../engine-v1-common/unit_tests/render.rs | 18 ++ crates/engines/engine-v1-sdl/src/runtime.rs | 67 +++++-- crates/games/game-reflex-poc/src/state.rs | 56 +++++- .../games/game-reflex-poc/unit_tests/state.rs | 41 ++++- deltas/0.1.0/0-pre.10.md | 122 +++++++++++++ docs/000-README.md | 4 +- docs/architecture/004-INPUT_AND_CONTROLS.md | 13 +- .../009-ENGINE_V1_RENDER_SCENE.md | 53 ++++++ history/0.1.0/0-pre.9.fix.1.md | 33 ++++ 16 files changed, 573 insertions(+), 37 deletions(-) create mode 100644 crates/engines/engine-v1-common/src/render.rs create mode 100644 crates/engines/engine-v1-common/unit_tests/render.rs create mode 100644 deltas/0.1.0/0-pre.10.md create mode 100644 docs/architecture/009-ENGINE_V1_RENDER_SCENE.md create mode 100644 history/0.1.0/0-pre.9.fix.1.md diff --git a/Android/game-reflex-poc/build.gradle b/Android/game-reflex-poc/build.gradle index 582b2b3..68a92bd 100644 --- a/Android/game-reflex-poc/build.gradle +++ b/Android/game-reflex-poc/build.gradle @@ -1,5 +1,5 @@ // file: Android/game-reflex-poc/build.gradle -// version: 7 +// version: 8 plugins { id 'com.android.application' @@ -16,7 +16,7 @@ android { minSdk 21 targetSdk 36 versionCode 1 - versionName '0.1.0-0-pre.9' + versionName '0.1.0-0-pre.10' } compileOptions { diff --git a/Android/game-snake-poc/build.gradle b/Android/game-snake-poc/build.gradle index 9e25e3b..14113e6 100644 --- a/Android/game-snake-poc/build.gradle +++ b/Android/game-snake-poc/build.gradle @@ -1,5 +1,5 @@ // file: Android/game-snake-poc/build.gradle -// version: 7 +// version: 8 plugins { id 'com.android.application' @@ -16,7 +16,7 @@ android { minSdk 21 targetSdk 36 versionCode 1 - versionName '0.1.0-0-pre.9' + versionName '0.1.0-0-pre.10' } compileOptions { diff --git a/Cargo.toml b/Cargo.toml index 08abeab..5ebe03d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ # file: Cargo.toml -# version: 18 +# version: 19 [workspace] resolver = "3" @@ -17,7 +17,7 @@ members = [ ] [workspace.package] -version = "0.1.0-0-pre.9.fix.1" +version = "0.1.0-0-pre.10" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/games" diff --git a/ROADMAP.md b/ROADMAP.md index 28f82eb..50e075f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,5 +1,5 @@ - + # Roadmap @@ -13,7 +13,7 @@ - [x] `0-pre.6` — fondation Gradle Android, SDL3 AAR, Activity Java commune et politique de fenêtre Desktop redimensionnable. - [x] `0-pre.7` — compilation Rust Android `cdylib`, point d'entrée SDL Android et premier lancement APK sur appareil/émulateur. - [x] `0-pre.8` — bridge Java/JNI minimal et input tactile Android. -- [ ] `0-pre.9` — assets communs + spécifiques empaquetés sans copie dans les crates. +- [x] `0-pre.9` — assets communs + spécifiques empaquetés sans copie dans les crates. - [ ] `0-pre.10` — POC Reflex jouable Desktop + Android. - [ ] `0-pre.11` — POC Snake jouable et validation de la réutilisation du moteur. - [ ] `1-alpha.1` — première API moteur V1 volontairement stabilisée. diff --git a/crates/engines/engine-v1-common/src/game_loop.rs b/crates/engines/engine-v1-common/src/game_loop.rs index 0cfff35..469a8ae 100644 --- a/crates/engines/engine-v1-common/src/game_loop.rs +++ b/crates/engines/engine-v1-common/src/game_loop.rs @@ -1,10 +1,15 @@ // file: crates/engines/engine-v1-common/src/game_loop.rs -// version: 2 +// version: 3 /// 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); + + /// Produces the platform-independent scene rendered after the update. + fn scene(&self) -> crate::EngineScene { + return crate::EngineScene::empty(crate::RenderColor::rgb(18, 18, 24)); + } } /// Deterministic fixed-step driver used before and underneath platform event loops. diff --git a/crates/engines/engine-v1-common/src/lib.rs b/crates/engines/engine-v1-common/src/lib.rs index 1702fbc..21671ab 100644 --- a/crates/engines/engine-v1-common/src/lib.rs +++ b/crates/engines/engine-v1-common/src/lib.rs @@ -1,5 +1,5 @@ // file: crates/engines/engine-v1-common/src/lib.rs -// version: 3 +// version: 4 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -12,6 +12,7 @@ mod frame; mod game_loop; mod input; mod pointer; +mod render; /// Re-export of the canonical logical input action used by engine V1 games. pub use self::action::GameAction; @@ -25,3 +26,13 @@ pub use self::game_loop::FixedStepRunner; pub use self::input::InputState; /// Re-export of the normalized primary pointer snapshot. pub use self::pointer::PointerState; +/// Re-export of the maximum engine scene rectangle capacity. +pub use self::render::ENGINE_SCENE_RECT_CAPACITY; +/// Re-export of the platform-independent scene snapshot. +pub use self::render::EngineScene; +/// Re-export of normalized rectangle geometry. +pub use self::render::NormalizedRect; +/// Re-export of platform-independent render color. +pub use self::render::RenderColor; +/// Re-export of one colored render rectangle. +pub use self::render::RenderRect; diff --git a/crates/engines/engine-v1-common/src/render.rs b/crates/engines/engine-v1-common/src/render.rs new file mode 100644 index 0000000..58df410 --- /dev/null +++ b/crates/engines/engine-v1-common/src/render.rs @@ -0,0 +1,167 @@ +// file: crates/engines/engine-v1-common/src/render.rs +// version: 1 + +/// Maximum rectangle count carried by one engine V1 scene snapshot. +pub const ENGINE_SCENE_RECT_CAPACITY: usize = 64; + +/// Platform-independent RGBA color. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct RenderColor { + red: u8, + green: u8, + blue: u8, + alpha: u8, +} + +impl RenderColor { + /// Creates an opaque RGB color. + #[must_use] + pub const fn rgb(red: u8, green: u8, blue: u8) -> Self { + return Self { red, green, blue, alpha: 255 }; + } + + /// Returns the red channel. + #[must_use] + pub const fn red(self) -> u8 { + return self.red; + } + + /// Returns the green channel. + #[must_use] + pub const fn green(self) -> u8 { + return self.green; + } + + /// Returns the blue channel. + #[must_use] + pub const fn blue(self) -> u8 { + return self.blue; + } + + /// Returns the alpha channel. + #[must_use] + pub const fn alpha(self) -> u8 { + return self.alpha; + } +} + +/// Rectangle expressed in normalized viewport coordinates. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct NormalizedRect { + x: f32, + y: f32, + width: f32, + height: f32, +} + +impl NormalizedRect { + /// Creates a normalized rectangle while clamping all coordinates and dimensions. + #[must_use] + pub fn new(x: f32, y: f32, width: f32, height: f32) -> Self { + let x = x.clamp(0.0, 1.0); + let y = y.clamp(0.0, 1.0); + let width = width.clamp(0.0, 1.0 - x); + let height = height.clamp(0.0, 1.0 - y); + return Self { x, y, width, height }; + } + + /// Returns the normalized left coordinate. + #[must_use] + pub const fn x(self) -> f32 { + return self.x; + } + + /// Returns the normalized top coordinate. + #[must_use] + pub const fn y(self) -> f32 { + return self.y; + } + + /// Returns the normalized width. + #[must_use] + pub const fn width(self) -> f32 { + return self.width; + } + + /// Returns the normalized height. + #[must_use] + pub const fn height(self) -> f32 { + return self.height; + } + + /// Reports whether a normalized pointer coordinate lies inside this rectangle. + #[must_use] + pub fn contains(self, x: f32, y: f32) -> bool { + return x >= self.x && y >= self.y && x <= self.x + self.width && y <= self.y + self.height; + } +} + +/// One colored rectangle in a platform-independent scene. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct RenderRect { + rect: crate::NormalizedRect, + color: crate::RenderColor, +} + +impl RenderRect { + /// Creates one colored scene rectangle. + #[must_use] + pub const fn new(rect: crate::NormalizedRect, color: crate::RenderColor) -> Self { + return Self { rect, color }; + } + + /// Returns the rectangle geometry. + #[must_use] + pub const fn rect(self) -> crate::NormalizedRect { + return self.rect; + } + + /// Returns the rectangle color. + #[must_use] + pub const fn color(self) -> crate::RenderColor { + return self.color; + } +} + +/// Immutable platform-independent scene snapshot. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct EngineScene { + background: crate::RenderColor, + rectangles: [Option; crate::ENGINE_SCENE_RECT_CAPACITY], +} + +impl EngineScene { + /// Creates an empty scene with the supplied background. + #[must_use] + pub const fn empty(background: crate::RenderColor) -> Self { + return Self { background, rectangles: [None; crate::ENGINE_SCENE_RECT_CAPACITY] }; + } + + /// Returns the background color. + #[must_use] + pub const fn background(self) -> crate::RenderColor { + return self.background; + } + + /// Returns the rectangle slots. + #[must_use] + pub const fn rectangles(&self) -> &[Option; crate::ENGINE_SCENE_RECT_CAPACITY] { + return &self.rectangles; + } + + /// Returns a copy with one rectangle inserted into the first free slot. + #[must_use] + pub fn with_rect(mut self, rectangle: crate::RenderRect) -> Self { + for slot in &mut self.rectangles { + if slot.is_none() { + *slot = Some(rectangle); + return self; + } + } + return self; + } +} + +#[cfg(test)] +#[path = "../unit_tests/render.rs"] +mod tests; diff --git a/crates/engines/engine-v1-common/unit_tests/render.rs b/crates/engines/engine-v1-common/unit_tests/render.rs new file mode 100644 index 0000000..5cf21b1 --- /dev/null +++ b/crates/engines/engine-v1-common/unit_tests/render.rs @@ -0,0 +1,18 @@ +// file: crates/engines/engine-v1-common/unit_tests/render.rs +// version: 1 + +#[test] +fn normalized_rect_clamps_and_contains_points() { + let rect = crate::NormalizedRect::new(0.8, 0.8, 0.5, 0.5); + assert_eq!(rect.width(), 0.2); + assert_eq!(rect.height(), 0.2); + assert!(rect.contains(0.9, 0.9)); + assert!(!rect.contains(0.5, 0.5)); +} + +#[test] +fn scene_accepts_render_rectangles() { + let target = crate::RenderRect::new(crate::NormalizedRect::new(0.2, 0.3, 0.4, 0.2), crate::RenderColor::rgb(255, 0, 0)); + let scene = crate::EngineScene::empty(crate::RenderColor::rgb(0, 0, 0)).with_rect(target); + assert_eq!(scene.rectangles()[0], Some(target)); +} diff --git a/crates/engines/engine-v1-sdl/src/runtime.rs b/crates/engines/engine-v1-sdl/src/runtime.rs index bb76da4..165f026 100644 --- a/crates/engines/engine-v1-sdl/src/runtime.rs +++ b/crates/engines/engine-v1-sdl/src/runtime.rs @@ -1,7 +1,7 @@ // file: crates/engines/engine-v1-sdl/src/runtime.rs -// version: 5 +// version: 6 -/// Minimal SDL3 runtime used by Desktop POC runners. +/// Minimal SDL3 runtime used by Desktop and Android POC runners. pub struct SdlRuntime { title: std::string::String, width: u32, @@ -47,13 +47,26 @@ impl SdlRuntime { match event { sdl3::event::Event::Quit { .. } => break 'running, sdl3::event::Event::KeyDown { keycode: Some(sdl3::keyboard::Keycode::Escape), .. } => break 'running, - sdl3::event::Event::KeyDown { keycode: Some(sdl3::keyboard::Keycode::Space), .. } => { - input = input.with_action(engine_v1_common::GameAction::Primary, true); + sdl3::event::Event::MouseButtonDown { mouse_btn: sdl3::mouse::MouseButton::Left, x, y, .. } => { + pointer = normalize_mouse_pointer(&canvas, true, x, y); + input = input.with_pointer(pointer).with_action(engine_v1_common::GameAction::Primary, true); }, - sdl3::event::Event::FingerDown { x, y, .. } | sdl3::event::Event::FingerMotion { x, y, .. } => { + sdl3::event::Event::MouseButtonUp { mouse_btn: sdl3::mouse::MouseButton::Left, x, y, .. } => { + pointer = normalize_mouse_pointer(&canvas, false, x, y); + input = input.with_pointer(pointer); + }, + sdl3::event::Event::MouseMotion { x, y, .. } if pointer.active() => { + pointer = normalize_mouse_pointer(&canvas, true, x, y); + input = input.with_pointer(pointer); + }, + sdl3::event::Event::FingerDown { x, y, .. } => { pointer = engine_v1_common::PointerState::normalized(true, x, y); input = input.with_pointer(pointer).with_action(engine_v1_common::GameAction::Primary, true); }, + sdl3::event::Event::FingerMotion { x, y, .. } => { + pointer = engine_v1_common::PointerState::normalized(true, x, y); + input = input.with_pointer(pointer); + }, sdl3::event::Event::FingerUp { x, y, .. } | sdl3::event::Event::FingerCanceled { x, y, .. } => { pointer = engine_v1_common::PointerState::normalized(false, x, y); input = input.with_pointer(pointer); @@ -61,16 +74,48 @@ impl SdlRuntime { _ => {}, } } - if pointer.active() { - input = input.with_pointer(pointer).with_action(engine_v1_common::GameAction::Primary, true); - } runner.tick(game, input); - canvas.set_draw_color(sdl3::pixels::Color::RGB(18, 18, 24)); - canvas.clear(); - canvas.present(); + match render_scene(&mut canvas, game.scene()) { + std::result::Result::Ok(()) => {}, + std::result::Result::Err(error) => return std::result::Result::Err(error), + } std::thread::sleep(self.frame_duration); } tracing::info!(frames = runner.next_frame_index(), "SDL3 runtime stopped"); return std::result::Result::Ok(()); } } + +fn normalize_mouse_pointer(canvas: &sdl3::render::WindowCanvas, active: bool, x: f32, y: f32) -> engine_v1_common::PointerState { + let (width, height) = canvas.window().size(); + if width == 0 || height == 0 { + return engine_v1_common::PointerState::normalized(active, 0.0, 0.0); + } + return engine_v1_common::PointerState::normalized(active, x / width as f32, y / height as f32); +} + +fn render_scene(canvas: &mut sdl3::render::WindowCanvas, scene: engine_v1_common::EngineScene) -> std::result::Result<(), std::string::String> { + let background = scene.background(); + canvas.set_draw_color(sdl3::pixels::Color::RGBA(background.red(), background.green(), background.blue(), background.alpha())); + canvas.clear(); + let (width, height) = match canvas.output_size() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), + }; + for rectangle in scene.rectangles() { + let rectangle = match rectangle { + Some(value) => *value, + None => continue, + }; + let color = rectangle.color(); + let rect = rectangle.rect(); + canvas.set_draw_color(sdl3::pixels::Color::RGBA(color.red(), color.green(), color.blue(), color.alpha())); + let physical = sdl3::rect::FRect::new(rect.x() * width as f32, rect.y() * height as f32, rect.width() * width as f32, rect.height() * height as f32); + match canvas.fill_rect(physical) { + std::result::Result::Ok(()) => {}, + std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), + } + } + canvas.present(); + return std::result::Result::Ok(()); +} diff --git a/crates/games/game-reflex-poc/src/state.rs b/crates/games/game-reflex-poc/src/state.rs index c35cbba..bee084e 100644 --- a/crates/games/game-reflex-poc/src/state.rs +++ b/crates/games/game-reflex-poc/src/state.rs @@ -1,17 +1,18 @@ // file: crates/games/game-reflex-poc/src/state.rs -// version: 3 +// version: 4 -/// Minimal deterministic state used to validate the first game boundary. -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +/// Deterministic playable state for the Reflex POC. +#[derive(Clone, Copy, Debug, PartialEq)] pub struct ReflexState { score: u64, + target: engine_v1_common::NormalizedRect, } impl ReflexState { /// Creates a fresh Reflex state. #[must_use] - pub const fn new() -> Self { - return Self { score: 0 }; + pub fn new() -> Self { + return Self { score: 0, target: target_for_index(0) }; } /// Returns the current score. @@ -20,19 +21,58 @@ impl ReflexState { return self.score; } - /// Registers one successful reflex action. - pub const fn register_success(&mut self) { + /// Returns the current normalized target rectangle. + #[must_use] + pub const fn target(self) -> engine_v1_common::NormalizedRect { + return self.target; + } + + /// Registers one successful reflex action and relocates the target. + pub fn register_success(&mut self) { self.score = self.score.saturating_add(1); + self.target = target_for_index(self.score); + return; + } +} + +impl Default for ReflexState { + fn default() -> Self { + return Self::new(); } } 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) { + if !input.is_active(engine_v1_common::GameAction::Primary) { + return; + } + let pointer = input.pointer(); + if !pointer.active() { + return; + } + if self.target.contains(pointer.x(), pointer.y()) { self.register_success(); } return; } + + fn scene(&self) -> engine_v1_common::EngineScene { + let target = engine_v1_common::RenderRect::new(self.target, engine_v1_common::RenderColor::rgb(255, 196, 64)); + return engine_v1_common::EngineScene::empty(engine_v1_common::RenderColor::rgb(18, 18, 24)).with_rect(target); + } +} + +fn target_for_index(index: u64) -> engine_v1_common::NormalizedRect { + return match index % 8 { + 0 => engine_v1_common::NormalizedRect::new(0.12, 0.12, 0.24, 0.14), + 1 => engine_v1_common::NormalizedRect::new(0.62, 0.16, 0.24, 0.14), + 2 => engine_v1_common::NormalizedRect::new(0.34, 0.34, 0.24, 0.14), + 3 => engine_v1_common::NormalizedRect::new(0.08, 0.52, 0.24, 0.14), + 4 => engine_v1_common::NormalizedRect::new(0.66, 0.54, 0.24, 0.14), + 5 => engine_v1_common::NormalizedRect::new(0.38, 0.72, 0.24, 0.14), + 6 => engine_v1_common::NormalizedRect::new(0.14, 0.78, 0.24, 0.14), + _ => engine_v1_common::NormalizedRect::new(0.62, 0.76, 0.24, 0.14), + }; } #[cfg(test)] diff --git a/crates/games/game-reflex-poc/unit_tests/state.rs b/crates/games/game-reflex-poc/unit_tests/state.rs index 5c47fbc..07bd157 100644 --- a/crates/games/game-reflex-poc/unit_tests/state.rs +++ b/crates/games/game-reflex-poc/unit_tests/state.rs @@ -1,20 +1,24 @@ // file: crates/games/game-reflex-poc/unit_tests/state.rs -// version: 1 +// version: 2 #[test] -fn success_increments_score() { - game_logging_lib::with_test_tracing("success_increments_score", || { +fn success_increments_score_and_moves_target() { + game_logging_lib::with_test_tracing("success_increments_score_and_moves_target", || { let mut state = crate::ReflexState::new(); + let initial_target = state.target(); state.register_success(); assert_eq!(state.score(), 1); + assert_ne!(state.target(), initial_target); }); } #[test] -fn engine_update_maps_primary_action_to_success() { - game_logging_lib::with_test_tracing("engine_update_maps_primary_action_to_success", || { +fn primary_pointer_inside_target_scores_once() { + game_logging_lib::with_test_tracing("primary_pointer_inside_target_scores_once", || { let mut state = crate::ReflexState::new(); - let input = engine_v1_common::InputState::none().with_action(engine_v1_common::GameAction::Primary, true); + let target = state.target(); + let pointer = engine_v1_common::PointerState::normalized(true, target.x() + target.width() / 2.0, target.y() + target.height() / 2.0); + let input = engine_v1_common::InputState::none().with_pointer(pointer).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), @@ -23,3 +27,28 @@ fn engine_update_maps_primary_action_to_success() { assert_eq!(state.score(), 1); }); } + +#[test] +fn primary_pointer_outside_target_does_not_score() { + game_logging_lib::with_test_tracing("primary_pointer_outside_target_does_not_score", || { + let mut state = crate::ReflexState::new(); + let pointer = engine_v1_common::PointerState::normalized(true, 0.95, 0.95); + let input = engine_v1_common::InputState::none().with_pointer(pointer).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(), 0); + }); +} + +#[test] +fn scene_contains_current_target() { + game_logging_lib::with_test_tracing("scene_contains_current_target", || { + let state = crate::ReflexState::new(); + let scene = engine_v1_common::EngineGame::scene(&state); + let first = scene.rectangles()[0]; + assert_eq!(first, Some(engine_v1_common::RenderRect::new(state.target(), engine_v1_common::RenderColor::rgb(255, 196, 64)))); + }); +} diff --git a/deltas/0.1.0/0-pre.10.md b/deltas/0.1.0/0-pre.10.md new file mode 100644 index 0000000..8955ef4 --- /dev/null +++ b/deltas/0.1.0/0-pre.10.md @@ -0,0 +1,122 @@ + + + +# Delta 0.1.0-0-pre.10 + +## Base + +Base validée : `0.1.0-0-pre.9.fix.1`. + +## Objectif + +Rendre le POC Reflex effectivement jouable avec le même gameplay Rust sur Desktop et Android. + +## Scène portable + +`engine-v1-common` introduit : + +```text +RenderColor +NormalizedRect +RenderRect +EngineScene +``` + +`EngineGame::scene()` fournit un snapshot portable après chaque update. + +Le backend SDL3 convertit les coordonnées normalisées vers la taille de sortie courante, donc le redimensionnement Desktop reste indépendant des coordonnées gameplay. + +## Gameplay Reflex + +La cible est visible sous forme de rectangle jaune. + +Le score augmente uniquement lorsqu'une impulsion `Primary` porte un pointeur situé à l'intérieur de la cible. + +Après chaque succès, la cible se déplace selon une séquence déterministe de huit positions. + +## Input + +Desktop : + +- clic gauche = impulsion `Primary` ; +- déplacement souris avec bouton maintenu = mise à jour du pointeur ; +- relâchement = pointeur inactif. + +Android : + +- `FingerDown` = impulsion `Primary` ; +- `FingerMotion` = mise à jour sans répétition du déclenchement ; +- `FingerUp`/`FingerCanceled` = pointeur inactif. + +Un contact maintenu ne peut donc plus compter comme un succès à chaque frame. + +## Validation Rust + +```bash +cargo fmt --all +cargo fmt --all -- --check + +python3 scripts/audit_rust_workspace_rules.py +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates Android deltas history + +cargo check --workspace +cargo clippy --workspace --all-targets --all-features -- -D warnings + +cargo test -p engine-v1-common --all-targets --all-features +cargo test -p engine-v1-sdl --all-targets --all-features +cargo test -p game-reflex-poc --all-targets --all-features +``` + +## Smoke Desktop + +```bash +cargo run -p game-reflex-poc-desktop +``` + +Critères : + +- cible jaune visible ; +- clic hors cible : aucun déplacement ; +- clic dans la cible : déplacement immédiat vers la position suivante ; +- plusieurs succès consécutifs possibles ; +- resize de fenêtre : la cible conserve ses coordonnées relatives ; +- `Escape` et fermeture système restent propres. + +## Build Android + +Le code natif a changé : reconstruire impérativement le `cdylib` Reflex. + +```bash +python3 scripts/build_android_rust.py reflex +``` + +Puis : + +```bash +cd Android +gradle :game-reflex-poc:assembleDebug +cd .. +``` + +Si un appareil ou émulateur est disponible : + +```bash +adb install -r Android/game-reflex-poc/build/outputs/apk/debug/game-reflex-poc-debug.apk +adb shell am start -n com.sasedev.games.reflex/.ReflexActivity +``` + +Critères appareil : + +- surface SDL visible ; +- cible jaune visible ; +- tap hors cible sans succès ; +- tap dans la cible déplace la cible ; +- pas de crash JNI ou natif. + +Si aucun device n'est disponible, le build natif + APK et le smoke Desktop suffisent pour valider techniquement le delta, avec smoke Android explicitement reporté. + +## Transition + +Si les gates sont propres, passer automatiquement à `0.1.0-0-pre.11` pour rendre Snake jouable et vérifier la réutilisation du modèle de scène. + +En cas d'échec imputable au projet, produire `0.1.0-0-pre.10.fix.1`. diff --git a/docs/000-README.md b/docs/000-README.md index 9310092..456cfdf 100644 --- a/docs/000-README.md +++ b/docs/000-README.md @@ -1,5 +1,5 @@ - + # Documentation games.sasedev @@ -56,3 +56,5 @@ Voir [`../RULES.md`](../RULES.md), notamment [`rules/RULES_COMMANDS.md`](rules/R ## Historique validé - [`../history/README.md`](../history/README.md) — convention et navigation de l'historique transitoire immuable des jalons validés. + +- [`architecture/009-ENGINE_V1_RENDER_SCENE.md`](architecture/009-ENGINE_V1_RENDER_SCENE.md) — scène 2D portable, rectangles normalisés et backend SDL3. diff --git a/docs/architecture/004-INPUT_AND_CONTROLS.md b/docs/architecture/004-INPUT_AND_CONTROLS.md index f19fd4c..ac9204e 100644 --- a/docs/architecture/004-INPUT_AND_CONTROLS.md +++ b/docs/architecture/004-INPUT_AND_CONTROLS.md @@ -1,5 +1,5 @@ - + # Abstraction des entrées et contrôles @@ -67,3 +67,14 @@ SDL3 fournit cette abstraction sur Android avec les événements `FingerDown`, ` La baseline `0.1.0-0-pre.8` mappe également un doigt actif vers `GameAction::Primary`. Cette association est volontairement minimale et pourra être remplacée par des zones tactiles ou boutons virtuels par jeu. Le gameplay ne dépend d'aucun type Android, Java ou SDL3. + +## Impulsion Primary et état maintenu + +À partir de `0.1.0-0-pre.10`, `GameAction::Primary` représente une impulsion de déclenchement pour souris/tactile : + +- `MouseButtonDown` gauche produit une impulsion `Primary` ; +- `FingerDown` produit une impulsion `Primary` ; +- les mouvements mettent à jour `PointerState` sans répéter `Primary` ; +- `MouseButtonUp`, `FingerUp` et `FingerCanceled` désactivent le pointeur. + +Cette séparation empêche un contact maintenu de produire un succès par frame. diff --git a/docs/architecture/009-ENGINE_V1_RENDER_SCENE.md b/docs/architecture/009-ENGINE_V1_RENDER_SCENE.md new file mode 100644 index 0000000..6bd4c1d --- /dev/null +++ b/docs/architecture/009-ENGINE_V1_RENDER_SCENE.md @@ -0,0 +1,53 @@ + + + +# Scène de rendu Engine V1 + +## Principe + +Le gameplay ne dépend pas de SDL3. + +`engine-v1-common` expose un modèle de scène 2D volontairement minimal : + +- `RenderColor` ; +- `NormalizedRect` ; +- `RenderRect` ; +- `EngineScene`. + +Toutes les géométries sont normalisées dans `[0.0, 1.0]`. + +`EngineGame::scene()` produit un snapshot après chaque update. L'implémentation par défaut fournit uniquement un fond sombre, ce qui permet aux jeux non migrés de rester compatibles. + +## Backend SDL3 + +`engine-v1-sdl` : + +1. reçoit la scène portable ; +2. récupère la taille de sortie courante du renderer ; +3. convertit les rectangles normalisés en `FRect` physiques ; +4. dessine le fond et les rectangles ; +5. présente la frame. + +Le resize Desktop ne modifie donc pas les coordonnées logiques du jeu. + +## Baseline Reflex + +Le POC Reflex expose une cible rectangulaire jaune. + +Un clic/tap ne marque un point que si le pointeur se trouve dans la cible au moment de l'impulsion `Primary`. + +Après chaque succès, la cible se déplace selon une séquence déterministe afin que les tests restent reproductibles. + +## Limites V1 actuelles + +Le modèle ne fournit pas encore : + +- texte de production ; +- textures ; +- sprites ; +- audio ; +- transformations ; +- clipping complexe ; +- batching spécialisé. + +Ces capacités seront ajoutées uniquement lorsqu'un besoin concret du POC le justifiera. diff --git a/history/0.1.0/0-pre.9.fix.1.md b/history/0.1.0/0-pre.9.fix.1.md new file mode 100644 index 0000000..361d5f0 --- /dev/null +++ b/history/0.1.0/0-pre.9.fix.1.md @@ -0,0 +1,33 @@ + + + +# Historique 0.1.0-0-pre.9.fix.1 + +## Statut + +Validé par l'utilisateur le 2026-09-16. + +## Gates validées + +- formatage Rust ; +- audits Rust/workspace ; +- audit Markdown : `1 table(s), 58 file(s)` ; +- `cargo check --workspace` ; +- Clippy global strict ; +- tests `game-assets-lib` : 2/2 ; +- builds APK Reflex et Snake. + +## Packaging Android vérifié + +Les deux APK contiennent : + +```text +assets/common/data/runtime.json +assets/game/data/game.json +``` + +Les vérifications négatives confirment que Reflex n'embarque pas de namespace `game-snake-poc` et que Snake n'embarque pas de namespace `game-reflex-poc`. + +## Portée durable + +Ce jalon valide la source unique `/assets`, les namespaces `common://` et `game://`, le staging Desktop et le packaging Android par Variant API AGP.