0.1.0-0-pre.10

This commit is contained in:
2026-09-16 10:21:09 +02:00
parent a3d5572c37
commit 88eda8390b
16 changed files with 573 additions and 37 deletions

View File

@@ -1,5 +1,5 @@
// file: Android/game-reflex-poc/build.gradle // file: Android/game-reflex-poc/build.gradle
// version: 7 // version: 8
plugins { plugins {
id 'com.android.application' id 'com.android.application'
@@ -16,7 +16,7 @@ android {
minSdk 21 minSdk 21
targetSdk 36 targetSdk 36
versionCode 1 versionCode 1
versionName '0.1.0-0-pre.9' versionName '0.1.0-0-pre.10'
} }
compileOptions { compileOptions {

View File

@@ -1,5 +1,5 @@
// file: Android/game-snake-poc/build.gradle // file: Android/game-snake-poc/build.gradle
// version: 7 // version: 8
plugins { plugins {
id 'com.android.application' id 'com.android.application'
@@ -16,7 +16,7 @@ android {
minSdk 21 minSdk 21
targetSdk 36 targetSdk 36
versionCode 1 versionCode 1
versionName '0.1.0-0-pre.9' versionName '0.1.0-0-pre.10'
} }
compileOptions { compileOptions {

View File

@@ -1,5 +1,5 @@
# file: Cargo.toml # file: Cargo.toml
# version: 18 # version: 19
[workspace] [workspace]
resolver = "3" resolver = "3"
@@ -17,7 +17,7 @@ members = [
] ]
[workspace.package] [workspace.package]
version = "0.1.0-0-pre.9.fix.1" version = "0.1.0-0-pre.10"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://git.sasedev.com/Sasedev/games" repository = "https://git.sasedev.com/Sasedev/games"

View File

@@ -1,5 +1,5 @@
<!-- file: ROADMAP.md --> <!-- file: ROADMAP.md -->
<!-- version: 7 --> <!-- version: 8 -->
# Roadmap # 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.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.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. - [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.10` — POC Reflex jouable Desktop + Android.
- [ ] `0-pre.11` — POC Snake jouable et validation de la réutilisation du moteur. - [ ] `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. - [ ] `1-alpha.1` — première API moteur V1 volontairement stabilisée.

View File

@@ -1,10 +1,15 @@
// file: crates/engines/engine-v1-common/src/game_loop.rs // 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. /// Minimal update contract implemented by a game state consumed by engine V1.
pub trait EngineGame { pub trait EngineGame {
/// Advances the game by one engine update using platform-independent input. /// Advances the game by one engine update using platform-independent input.
fn update(&mut self, frame: crate::EngineFrame, input: crate::InputState); 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. /// Deterministic fixed-step driver used before and underneath platform event loops.

View File

@@ -1,5 +1,5 @@
// file: crates/engines/engine-v1-common/src/lib.rs // file: crates/engines/engine-v1-common/src/lib.rs
// version: 3 // version: 4
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -12,6 +12,7 @@ mod frame;
mod game_loop; mod game_loop;
mod input; mod input;
mod pointer; mod pointer;
mod render;
/// Re-export of the canonical logical input action used by engine V1 games. /// Re-export of the canonical logical input action used by engine V1 games.
pub use self::action::GameAction; pub use self::action::GameAction;
@@ -25,3 +26,13 @@ pub use self::game_loop::FixedStepRunner;
pub use self::input::InputState; pub use self::input::InputState;
/// Re-export of the normalized primary pointer snapshot. /// Re-export of the normalized primary pointer snapshot.
pub use self::pointer::PointerState; 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;

View File

@@ -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::RenderRect>; 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::RenderRect>; 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;

View File

@@ -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));
}

View File

@@ -1,7 +1,7 @@
// file: crates/engines/engine-v1-sdl/src/runtime.rs // 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 { pub struct SdlRuntime {
title: std::string::String, title: std::string::String,
width: u32, width: u32,
@@ -47,13 +47,26 @@ impl SdlRuntime {
match event { match event {
sdl3::event::Event::Quit { .. } => break 'running, 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::Escape), .. } => break 'running,
sdl3::event::Event::KeyDown { keycode: Some(sdl3::keyboard::Keycode::Space), .. } => { sdl3::event::Event::MouseButtonDown { mouse_btn: sdl3::mouse::MouseButton::Left, x, y, .. } => {
input = input.with_action(engine_v1_common::GameAction::Primary, true); 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); pointer = engine_v1_common::PointerState::normalized(true, x, y);
input = input.with_pointer(pointer).with_action(engine_v1_common::GameAction::Primary, true); 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, .. } => { sdl3::event::Event::FingerUp { x, y, .. } | sdl3::event::Event::FingerCanceled { x, y, .. } => {
pointer = engine_v1_common::PointerState::normalized(false, x, y); pointer = engine_v1_common::PointerState::normalized(false, x, y);
input = input.with_pointer(pointer); 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); runner.tick(game, input);
canvas.set_draw_color(sdl3::pixels::Color::RGB(18, 18, 24)); match render_scene(&mut canvas, game.scene()) {
canvas.clear(); std::result::Result::Ok(()) => {},
canvas.present(); std::result::Result::Err(error) => return std::result::Result::Err(error),
}
std::thread::sleep(self.frame_duration); std::thread::sleep(self.frame_duration);
} }
tracing::info!(frames = runner.next_frame_index(), "SDL3 runtime stopped"); tracing::info!(frames = runner.next_frame_index(), "SDL3 runtime stopped");
return std::result::Result::Ok(()); 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(());
}

View File

@@ -1,17 +1,18 @@
// file: crates/games/game-reflex-poc/src/state.rs // file: crates/games/game-reflex-poc/src/state.rs
// version: 3 // version: 4
/// Minimal deterministic state used to validate the first game boundary. /// Deterministic playable state for the Reflex POC.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub struct ReflexState { pub struct ReflexState {
score: u64, score: u64,
target: engine_v1_common::NormalizedRect,
} }
impl ReflexState { impl ReflexState {
/// Creates a fresh Reflex state. /// Creates a fresh Reflex state.
#[must_use] #[must_use]
pub const fn new() -> Self { pub fn new() -> Self {
return Self { score: 0 }; return Self { score: 0, target: target_for_index(0) };
} }
/// Returns the current score. /// Returns the current score.
@@ -20,19 +21,58 @@ impl ReflexState {
return self.score; return self.score;
} }
/// Registers one successful reflex action. /// Returns the current normalized target rectangle.
pub const fn register_success(&mut self) { #[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.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 { impl engine_v1_common::EngineGame for ReflexState {
fn update(&mut self, _frame: engine_v1_common::EngineFrame, input: engine_v1_common::InputState) { 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(); self.register_success();
} }
return; 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)] #[cfg(test)]

View File

@@ -1,20 +1,24 @@
// file: crates/games/game-reflex-poc/unit_tests/state.rs // file: crates/games/game-reflex-poc/unit_tests/state.rs
// version: 1 // version: 2
#[test] #[test]
fn success_increments_score() { fn success_increments_score_and_moves_target() {
game_logging_lib::with_test_tracing("success_increments_score", || { game_logging_lib::with_test_tracing("success_increments_score_and_moves_target", || {
let mut state = crate::ReflexState::new(); let mut state = crate::ReflexState::new();
let initial_target = state.target();
state.register_success(); state.register_success();
assert_eq!(state.score(), 1); assert_eq!(state.score(), 1);
assert_ne!(state.target(), initial_target);
}); });
} }
#[test] #[test]
fn engine_update_maps_primary_action_to_success() { fn primary_pointer_inside_target_scores_once() {
game_logging_lib::with_test_tracing("engine_update_maps_primary_action_to_success", || { game_logging_lib::with_test_tracing("primary_pointer_inside_target_scores_once", || {
let mut state = crate::ReflexState::new(); 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( engine_v1_common::EngineGame::update(
&mut state, &mut state,
engine_v1_common::EngineFrame::new(0, std::time::Duration::from_millis(16), std::time::Duration::ZERO), 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); 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))));
});
}

122
deltas/0.1.0/0-pre.10.md Normal file
View File

@@ -0,0 +1,122 @@
<!-- file: deltas/0.1.0/0-pre.10.md -->
<!-- version: 1 -->
# 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`.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/000-README.md --> <!-- file: docs/000-README.md -->
<!-- version: 11 --> <!-- version: 12 -->
# Documentation games.sasedev # Documentation games.sasedev
@@ -56,3 +56,5 @@ Voir [`../RULES.md`](../RULES.md), notamment [`rules/RULES_COMMANDS.md`](rules/R
## Historique validé ## Historique validé
- [`../history/README.md`](../history/README.md) — convention et navigation de l'historique transitoire immuable des jalons validés. - [`../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.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/architecture/004-INPUT_AND_CONTROLS.md --> <!-- file: docs/architecture/004-INPUT_AND_CONTROLS.md -->
<!-- version: 2 --> <!-- version: 3 -->
# Abstraction des entrées et contrôles # 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. 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. 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.

View File

@@ -0,0 +1,53 @@
<!-- file: docs/architecture/009-ENGINE_V1_RENDER_SCENE.md -->
<!-- version: 1 -->
# 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.

View File

@@ -0,0 +1,33 @@
<!-- file: history/0.1.0/0-pre.9.fix.1.md -->
<!-- version: 1 -->
# 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.