From ab9403384f79f33d1c8844866c9d12c7edb3e01f Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Wed, 16 Sep 2026 11:32:07 +0200 Subject: [PATCH] 0.1.0-0-pre.11 --- Android/README.md | 10 +- Android/common/build.gradle | 4 +- Android/game-reflex-poc/build.gradle | 6 +- Android/game-snake-poc/build.gradle | 6 +- Android/gradle.properties | 3 +- Cargo.toml | 4 +- ROADMAP.md | 4 +- crates/engines/engine-v1-sdl/src/runtime.rs | 33 ++- crates/games/game-snake-poc/src/state.rs | 200 +++++++++++++++++- .../games/game-snake-poc/unit_tests/state.rs | 65 ++++-- deltas/0.1.0/0-pre.11.md | 106 ++++++++++ docs/architecture/004-INPUT_AND_CONTROLS.md | 8 +- history/0.1.0/0-pre.10.fix.1.md | 32 +++ scripts/build_android_rust.py | 28 ++- 14 files changed, 465 insertions(+), 44 deletions(-) create mode 100644 deltas/0.1.0/0-pre.11.md create mode 100644 history/0.1.0/0-pre.10.fix.1.md diff --git a/Android/README.md b/Android/README.md index b38d484..e51c46a 100644 --- a/Android/README.md +++ b/Android/README.md @@ -1,5 +1,5 @@ - + # Android @@ -91,3 +91,11 @@ assets// -> assets/game/ dans un répertoire généré sous `Android//build/generated/sasedevAssets//`. Aucune ressource source n'est copiée durablement dans un module Android ou une crate Rust. + +## Version NDK du projet + +`Android/gradle.properties` définit `androidNdkVersion=28.2.13676358`. + +Gradle utilise cette valeur comme `ndkVersion`. Le script Rust Android résout la même version sous `${ANDROID_HOME}/ndk/` et définit `ANDROID_NDK_HOME` uniquement pour le sous-processus `cargo ndk`. + +Aucun `ANDROID_NDK_HOME` global n'est requis ; plusieurs NDK peuvent rester installés côte à côte. diff --git a/Android/common/build.gradle b/Android/common/build.gradle index 57deaf4..221ecbe 100644 --- a/Android/common/build.gradle +++ b/Android/common/build.gradle @@ -1,15 +1,17 @@ // file: Android/common/build.gradle -// version: 3 +// version: 4 plugins { id 'com.android.library' } def sdl3AarName = providers.gradleProperty("sdl3AarName").get() +def androidNdkVersion = providers.gradleProperty("androidNdkVersion").get() android { namespace 'com.sasedev.games.common' compileSdk 36 + ndkVersion androidNdkVersion defaultConfig { minSdk 21 diff --git a/Android/game-reflex-poc/build.gradle b/Android/game-reflex-poc/build.gradle index 68a92bd..6803f1a 100644 --- a/Android/game-reflex-poc/build.gradle +++ b/Android/game-reflex-poc/build.gradle @@ -1,22 +1,24 @@ // file: Android/game-reflex-poc/build.gradle -// version: 8 +// version: 9 plugins { id 'com.android.application' } def sdl3AarName = providers.gradleProperty("sdl3AarName").get() +def androidNdkVersion = providers.gradleProperty("androidNdkVersion").get() android { namespace 'com.sasedev.games.reflex' compileSdk 36 + ndkVersion androidNdkVersion defaultConfig { applicationId 'com.sasedev.games.reflex' minSdk 21 targetSdk 36 versionCode 1 - versionName '0.1.0-0-pre.10' + versionName '0.1.0-0-pre.11' } compileOptions { diff --git a/Android/game-snake-poc/build.gradle b/Android/game-snake-poc/build.gradle index 14113e6..0eb10c8 100644 --- a/Android/game-snake-poc/build.gradle +++ b/Android/game-snake-poc/build.gradle @@ -1,22 +1,24 @@ // file: Android/game-snake-poc/build.gradle -// version: 8 +// version: 9 plugins { id 'com.android.application' } def sdl3AarName = providers.gradleProperty("sdl3AarName").get() +def androidNdkVersion = providers.gradleProperty("androidNdkVersion").get() android { namespace 'com.sasedev.games.snake' compileSdk 36 + ndkVersion androidNdkVersion defaultConfig { applicationId 'com.sasedev.games.snake' minSdk 21 targetSdk 36 versionCode 1 - versionName '0.1.0-0-pre.10' + versionName '0.1.0-0-pre.11' } compileOptions { diff --git a/Android/gradle.properties b/Android/gradle.properties index bf6534f..7eb966d 100644 --- a/Android/gradle.properties +++ b/Android/gradle.properties @@ -1,6 +1,7 @@ # file: Android/gradle.properties -# version: 1 +# version: 2 android.useAndroidX=true org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 sdl3AarName=SDL3-3.4.16.aar +androidNdkVersion=28.2.13676358 diff --git a/Cargo.toml b/Cargo.toml index 19350cb..f75df55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ # file: Cargo.toml -# version: 20 +# version: 21 [workspace] resolver = "3" @@ -17,7 +17,7 @@ members = [ ] [workspace.package] -version = "0.1.0-0-pre.10.fix.1" +version = "0.1.0-0-pre.11" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/games" diff --git a/ROADMAP.md b/ROADMAP.md index 50e075f..5d4be5e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,5 +1,5 @@ - + # Roadmap @@ -14,7 +14,7 @@ - [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.9` — assets communs + spécifiques empaquetés sans copie dans les crates. -- [ ] `0-pre.10` — POC Reflex jouable Desktop + Android. +- [x] `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. - [ ] `2-beta.1` — stabilisation, packaging, tests multi-appareils. diff --git a/crates/engines/engine-v1-sdl/src/runtime.rs b/crates/engines/engine-v1-sdl/src/runtime.rs index 96e391a..5c3131e 100644 --- a/crates/engines/engine-v1-sdl/src/runtime.rs +++ b/crates/engines/engine-v1-sdl/src/runtime.rs @@ -1,5 +1,5 @@ // file: crates/engines/engine-v1-sdl/src/runtime.rs -// version: 7 +// version: 8 /// Minimal SDL3 runtime used by Desktop and Android POC runners. pub struct SdlRuntime { @@ -47,9 +47,22 @@ 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::Left), repeat: false, .. } => { + input = input.with_action(engine_v1_common::GameAction::Left, true); + }, + sdl3::event::Event::KeyDown { keycode: Some(sdl3::keyboard::Keycode::Right), repeat: false, .. } => { + input = input.with_action(engine_v1_common::GameAction::Right, true); + }, + sdl3::event::Event::KeyDown { keycode: Some(sdl3::keyboard::Keycode::Up), repeat: false, .. } => { + input = input.with_action(engine_v1_common::GameAction::Up, true); + }, + sdl3::event::Event::KeyDown { keycode: Some(sdl3::keyboard::Keycode::Down), repeat: false, .. } => { + input = input.with_action(engine_v1_common::GameAction::Down, 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); + input = apply_pointer_direction(input, pointer); }, sdl3::event::Event::MouseButtonUp { mouse_btn: sdl3::mouse::MouseButton::Left, x, y, .. } => { pointer = normalize_mouse_pointer(&canvas, false, x, y); @@ -62,6 +75,7 @@ impl SdlRuntime { 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); + input = apply_pointer_direction(input, pointer); }, sdl3::event::Event::FingerMotion { x, y, .. } => { pointer = engine_v1_common::PointerState::normalized(true, x, y); @@ -119,3 +133,20 @@ fn render_scene(canvas: &mut sdl3::render::WindowCanvas, scene: engine_v1_common canvas.present(); return std::result::Result::Ok(()); } + +fn apply_pointer_direction(mut input: engine_v1_common::InputState, pointer: engine_v1_common::PointerState) -> engine_v1_common::InputState { + let horizontal = pointer.x() - 0.5; + let vertical = pointer.y() - 0.5; + if horizontal.abs() >= vertical.abs() { + if horizontal < 0.0 { + input = input.with_action(engine_v1_common::GameAction::Left, true); + } else { + input = input.with_action(engine_v1_common::GameAction::Right, true); + } + } else if vertical < 0.0 { + input = input.with_action(engine_v1_common::GameAction::Up, true); + } else { + input = input.with_action(engine_v1_common::GameAction::Down, true); + } + return input; +} diff --git a/crates/games/game-snake-poc/src/state.rs b/crates/games/game-snake-poc/src/state.rs index 0d87d57..54a29f7 100644 --- a/crates/games/game-snake-poc/src/state.rs +++ b/crates/games/game-snake-poc/src/state.rs @@ -1,10 +1,55 @@ // file: crates/games/game-snake-poc/src/state.rs -// version: 3 +// version: 4 -/// Minimal Snake state used to validate reusable engine dependencies. +const GRID_HEIGHT: i16 = 20; +const GRID_WIDTH: i16 = 12; +const MAX_SEGMENTS: usize = 48; +const MOVE_EVERY_FRAMES: u64 = 8; + +/// Grid cell used by the Snake POC. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct SnakeCell { + x: i16, + y: i16, +} + +impl SnakeCell { + /// Creates one grid cell. + #[must_use] + pub const fn new(x: i16, y: i16) -> Self { + return Self { x, y }; + } + + /// Returns the horizontal grid coordinate. + #[must_use] + pub const fn x(self) -> i16 { + return self.x; + } + + /// Returns the vertical grid coordinate. + #[must_use] + pub const fn y(self) -> i16 { + return self.y; + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum SnakeDirection { + Left, + Right, + Up, + Down, +} + +/// Playable deterministic Snake POC state. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct SnakeState { - length: u32, + segments: [SnakeCell; MAX_SEGMENTS], + length: usize, + direction: SnakeDirection, + food: SnakeCell, + food_index: u64, + score: u64, } impl Default for SnakeState { @@ -17,28 +62,161 @@ impl SnakeState { /// Creates a fresh Snake state. #[must_use] pub const fn new() -> Self { - return Self { length: 1 }; + let mut segments = [SnakeCell::new(0, 0); MAX_SEGMENTS]; + segments[0] = SnakeCell::new(5, 10); + segments[1] = SnakeCell::new(4, 10); + segments[2] = SnakeCell::new(3, 10); + return Self { + segments, + length: 3, + direction: SnakeDirection::Right, + food: SnakeCell::new(8, 10), + food_index: 0, + score: 0, + }; } /// Returns the current snake length. #[must_use] - pub const fn length(self) -> u32 { + pub const fn length(self) -> usize { return self.length; } - /// Registers one consumed growth item. - pub const fn grow(&mut self) { - self.length = self.length.saturating_add(1); + /// Returns the current score. + #[must_use] + pub const fn score(self) -> u64 { + return self.score; + } + + /// Returns the current food cell. + #[must_use] + pub const fn food(self) -> SnakeCell { + return self.food; + } + + /// Returns the current head cell. + #[must_use] + pub const fn head(self) -> SnakeCell { + return self.segments[0]; + } + + fn apply_direction_input(&mut self, input: engine_v1_common::InputState) { + if input.is_active(engine_v1_common::GameAction::Left) && self.direction != SnakeDirection::Right { + self.direction = SnakeDirection::Left; + } else if input.is_active(engine_v1_common::GameAction::Right) && self.direction != SnakeDirection::Left { + self.direction = SnakeDirection::Right; + } else if input.is_active(engine_v1_common::GameAction::Up) && self.direction != SnakeDirection::Down { + self.direction = SnakeDirection::Up; + } else if input.is_active(engine_v1_common::GameAction::Down) && self.direction != SnakeDirection::Up { + self.direction = SnakeDirection::Down; + } + return; + } + + fn move_once(&mut self) { + let head = self.head(); + let next = match self.direction { + SnakeDirection::Left => SnakeCell::new(head.x - 1, head.y), + SnakeDirection::Right => SnakeCell::new(head.x + 1, head.y), + SnakeDirection::Up => SnakeCell::new(head.x, head.y - 1), + SnakeDirection::Down => SnakeCell::new(head.x, head.y + 1), + }; + if self.hits_wall(next) || self.hits_body(next) { + *self = Self::new(); + return; + } + let ate = next == self.food; + let new_length = if ate { self.length.saturating_add(1).min(MAX_SEGMENTS) } else { self.length }; + let mut index = new_length.saturating_sub(1); + while index > 0 { + self.segments[index] = self.segments[index - 1]; + index -= 1; + } + self.segments[0] = next; + self.length = new_length; + if ate { + self.score = self.score.saturating_add(1); + self.food_index = self.food_index.saturating_add(1); + self.food = food_for_index(self.food_index); + if self.food_overlaps_snake() { + self.food_index = self.food_index.saturating_add(1); + self.food = food_for_index(self.food_index); + } + } + return; + } + + fn hits_body(self, cell: SnakeCell) -> bool { + let mut index = 0; + while index < self.length { + if self.segments[index] == cell { + return true; + } + index += 1; + } + return false; + } + + fn hits_wall(self, cell: SnakeCell) -> bool { + return cell.x < 0 || cell.y < 0 || cell.x >= GRID_WIDTH || cell.y >= GRID_HEIGHT; + } + + fn food_overlaps_snake(self) -> bool { + return self.hits_body(self.food); } } 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(); + fn update(&mut self, frame: engine_v1_common::EngineFrame, input: engine_v1_common::InputState) { + self.apply_direction_input(input); + if frame.index() % MOVE_EVERY_FRAMES == 0 { + self.move_once(); } return; } + + fn scene(&self) -> engine_v1_common::EngineScene { + let mut scene = engine_v1_common::EngineScene::empty(engine_v1_common::RenderColor::rgb(14, 20, 18)); + scene = scene.with_rect(engine_v1_common::RenderRect::new(cell_rect(self.food), engine_v1_common::RenderColor::rgb(255, 96, 72))); + let mut index = 0; + while index < self.length { + let color = if index == 0 { + engine_v1_common::RenderColor::rgb(120, 255, 160) + } else { + engine_v1_common::RenderColor::rgb(64, 192, 112) + }; + scene = scene.with_rect(engine_v1_common::RenderRect::new(cell_rect(self.segments[index]), color)); + index += 1; + } + return scene; + } +} + +fn cell_rect(cell: SnakeCell) -> engine_v1_common::NormalizedRect { + let cell_height = 1.0 / GRID_HEIGHT as f32; + let cell_width = 1.0 / GRID_WIDTH as f32; + let inset_x = cell_width * 0.08; + let inset_y = cell_height * 0.08; + return engine_v1_common::NormalizedRect::new( + cell.x as f32 * cell_width + inset_x, + cell.y as f32 * cell_height + inset_y, + cell_width - inset_x * 2.0, + cell_height - inset_y * 2.0, + ); +} + +fn food_for_index(index: u64) -> SnakeCell { + const FOODS: [SnakeCell; 8] = [ + SnakeCell::new(8, 10), + SnakeCell::new(8, 5), + SnakeCell::new(2, 5), + SnakeCell::new(2, 15), + SnakeCell::new(9, 15), + SnakeCell::new(9, 3), + SnakeCell::new(1, 3), + SnakeCell::new(6, 17), + ]; + return FOODS[index as usize % FOODS.len()]; } #[cfg(test)] diff --git a/crates/games/game-snake-poc/unit_tests/state.rs b/crates/games/game-snake-poc/unit_tests/state.rs index e7d82b2..cf0c3cc 100644 --- a/crates/games/game-snake-poc/unit_tests/state.rs +++ b/crates/games/game-snake-poc/unit_tests/state.rs @@ -1,25 +1,62 @@ // file: crates/games/game-snake-poc/unit_tests/state.rs -// version: 1 +// version: 2 + +fn frame(index: u64) -> engine_v1_common::EngineFrame { + return engine_v1_common::EngineFrame::new(index, std::time::Duration::from_millis(16), std::time::Duration::ZERO); +} #[test] -fn growth_increments_length() { - game_logging_lib::with_test_tracing("growth_increments_length", || { +fn eating_food_grows_and_scores() { + game_logging_lib::with_test_tracing("eating_food_grows_and_scores", || { let mut state = crate::SnakeState::new(); - state.grow(); - assert_eq!(state.length(), 2); + let initial_length = state.length(); + let mut step = 0; + while step < 3 { + engine_v1_common::EngineGame::update(&mut state, frame(step * 8), engine_v1_common::InputState::none()); + step += 1; + } + assert_eq!(state.length(), initial_length + 1); + assert_eq!(state.score(), 1); + assert_ne!(state.food(), crate::SnakeCell::new(8, 10)); }); } #[test] -fn engine_update_maps_primary_action_to_growth() { - game_logging_lib::with_test_tracing("engine_update_maps_primary_action_to_growth", || { +fn opposite_direction_is_rejected() { + game_logging_lib::with_test_tracing("opposite_direction_is_rejected", || { 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); + let input = engine_v1_common::InputState::none().with_action(engine_v1_common::GameAction::Left, true); + engine_v1_common::EngineGame::update(&mut state, frame(0), input); + assert_eq!(state.head(), crate::SnakeCell::new(6, 10)); + }); +} + +#[test] +fn scene_contains_food_and_snake_segments() { + game_logging_lib::with_test_tracing("scene_contains_food_and_snake_segments", || { + let state = crate::SnakeState::new(); + let scene = engine_v1_common::EngineGame::scene(&state); + let occupied = scene.rectangles().iter().filter(|slot| slot.is_some()).count(); + assert_eq!(occupied, state.length() + 1); + }); +} + +#[test] +fn snake_moves_on_fixed_cadence() { + game_logging_lib::with_test_tracing("snake_moves_on_fixed_cadence", || { + let mut state = crate::SnakeState::new(); + let start = state.head(); + engine_v1_common::EngineGame::update(&mut state, frame(0), engine_v1_common::InputState::none()); + assert_eq!(state.head(), crate::SnakeCell::new(start.x() + 1, start.y())); + }); +} + +#[test] +fn up_direction_changes_movement() { + game_logging_lib::with_test_tracing("up_direction_changes_movement", || { + let mut state = crate::SnakeState::new(); + let input = engine_v1_common::InputState::none().with_action(engine_v1_common::GameAction::Up, true); + engine_v1_common::EngineGame::update(&mut state, frame(0), input); + assert_eq!(state.head(), crate::SnakeCell::new(5, 9)); }); } diff --git a/deltas/0.1.0/0-pre.11.md b/deltas/0.1.0/0-pre.11.md new file mode 100644 index 0000000..6890942 --- /dev/null +++ b/deltas/0.1.0/0-pre.11.md @@ -0,0 +1,106 @@ + + + +# Delta 0.1.0-0-pre.11 + +## Base + +Base validée : `0.1.0-0-pre.10.fix.1`. + +## Objectifs + +- rendre Snake réellement jouable ; +- valider la réutilisation de `EngineScene` et du backend input SDL3 ; +- figer le NDK au niveau projet sans variable globale ; +- permettre un smoke natif x86_64 sur l'AVD Linux. + +## Snake + +Le POC utilise une grille 12 × 20, un serpent initial de trois segments, un déplacement automatique, quatre directions, une nourriture déterministe, croissance/score et reset sur collision mur/corps. + +Le rendu réutilise exclusivement `EngineScene` et `RenderRect`. + +## Input + +Desktop : flèches clavier. + +Souris/tactile : le clic/tap produit aussi une direction selon l'axe dominant depuis le centre de l'écran. Reflex ignore ces directions ; Snake les consomme. + +## NDK + +La propriété unique : + +```text +androidNdkVersion=28.2.13676358 +``` + +est lue par Gradle et `scripts/build_android_rust.py`. + +Le script définit `ANDROID_NDK_HOME` uniquement dans l'environnement du sous-processus `cargo ndk`. + +## Validation + +```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-sdl --all-targets --all-features +cargo test -p game-snake-poc --all-targets --all-features +``` + +Smoke Desktop : + +```bash +cargo run -p game-snake-poc-desktop +``` + +Critères : serpent/nourriture visibles, déplacement automatique, flèches fonctionnelles, demi-tour refusé, croissance sur nourriture, reset sur collision, fermeture propre. + +Android ARM64 : + +```bash +python3 scripts/build_android_rust.py snake --abi arm64-v8a +``` + +Android x86_64 pour l'AVD : + +```bash +rustup target add x86_64-linux-android +python3 scripts/build_android_rust.py snake --abi x86_64 +``` + +Puis : + +```bash +cd Android +gradle :game-snake-poc:assembleDebug +cd .. +``` + +Vérifier : + +```bash +unzip -l Android/game-snake-poc/build/outputs/apk/debug/game-snake-poc-debug.apk | grep 'lib/arm64-v8a/libgame_android_entrypoint.so' +unzip -l Android/game-snake-poc/build/outputs/apk/debug/game-snake-poc-debug.apk | grep 'lib/x86_64/libgame_android_entrypoint.so' +``` + +Smoke AVD : + +```bash +adb devices +adb shell getprop ro.product.cpu.abi +adb install -r Android/game-snake-poc/build/outputs/apk/debug/game-snake-poc-debug.apk +adb shell am start -n com.sasedev.games.snake/.SnakeActivity +``` + +## Transition + +Si ces gates sont propres, `0.1.0-0-pre.11` termine la phase `0-pre.*` prévue et permet de passer à `0.1.0-1-alpha.1`. + +En cas d'échec imputable au projet, produire `0.1.0-0-pre.11.fix.1`. diff --git a/docs/architecture/004-INPUT_AND_CONTROLS.md b/docs/architecture/004-INPUT_AND_CONTROLS.md index ac9204e..af0fc0e 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 @@ -78,3 +78,9 @@ Le gameplay ne dépend d'aucun type Android, Java ou SDL3. - `MouseButtonUp`, `FingerUp` et `FingerCanceled` désactivent le pointeur. Cette séparation empêche un contact maintenu de produire un succès par frame. + +## Directions clavier et tactile + +Les flèches clavier produisent `Left`, `Right`, `Up` et `Down`. + +Lors d'un clic/tap, la position normalisée produit également une direction selon l'axe dominant depuis le centre. Reflex ignore ces directions ; Snake les consomme. Le backend SDL3 reste ainsi commun aux jeux. diff --git a/history/0.1.0/0-pre.10.fix.1.md b/history/0.1.0/0-pre.10.fix.1.md new file mode 100644 index 0000000..d71a733 --- /dev/null +++ b/history/0.1.0/0-pre.10.fix.1.md @@ -0,0 +1,32 @@ + + + +# Historique 0.1.0-0-pre.10.fix.1 + +## Statut + +Validé techniquement par l'utilisateur le 2026-09-16. + +## Gates validées + +- formatage Rust ; +- audits Rust/workspace ; +- audit Markdown : `1 table(s), 70 file(s)` ; +- `cargo check --workspace` ; +- Clippy global strict ; +- tests `engine-v1-common` : 6/6 ; +- test `engine-v1-sdl` : 1/1 ; +- tests `game-reflex-poc` : 4/4 ; +- smoke Desktop Reflex propre ; +- build Rust Android Reflex ; +- assemblage APK Reflex. + +## Runtime Android + +Un AVD API 36 est disponible sous `emulator-5554`. L'APK Reflex s'installe avec succès et l'Activity démarre via `adb shell am start`. + +Le contrôle interactif tactile complet n'a pas encore été consigné comme gate observée. + +## Portée durable + +Ce jalon valide le modèle `EngineScene`, le rendu SDL3 par rectangles normalisés et le gameplay Reflex Desktop, avec chaîne Android exécutable sur émulateur. diff --git a/scripts/build_android_rust.py b/scripts/build_android_rust.py index f75ba03..075b07c 100644 --- a/scripts/build_android_rust.py +++ b/scripts/build_android_rust.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # file: scripts/build_android_rust.py -# version: 2 +# version: 3 """Build one Android Rust game entrypoint and stage SDL3 and Rust jniLibs.""" @@ -22,13 +22,18 @@ ABI_TO_TRIPLE = { } GAME_TO_MODULE = {"reflex": "game-reflex-poc", "snake": "game-snake-poc"} -def read_sdl3_aar_name(properties_path: pathlib.Path) -> str: - """Read the configured SDL3 AAR filename.""" +def read_gradle_property(properties_path: pathlib.Path, property_name: str) -> str: + """Read one required Android Gradle property.""" + prefix = f"{property_name}=" for raw_line in properties_path.read_text(encoding="utf-8").splitlines(): line = raw_line.strip() - if line.startswith("sdl3AarName="): + if line.startswith(prefix): return line.split("=", 1)[1].strip() - raise RuntimeError("sdl3AarName is missing from Android/gradle.properties") + raise RuntimeError(f"{property_name} is missing from Android/gradle.properties") + +def read_sdl3_aar_name(properties_path: pathlib.Path) -> str: + """Read the configured SDL3 AAR filename.""" + return read_gradle_property(properties_path, "sdl3AarName") def find_sdl3_member(archive: zipfile.ZipFile, abi: str) -> str: """Return the SDL3 shared-library member for one Android ABI.""" @@ -75,7 +80,9 @@ def main() -> int: parser.add_argument("--release", action="store_true") arguments = parser.parse_args() root = pathlib.Path(__file__).resolve().parent.parent - aar_name = read_sdl3_aar_name(root / "Android" / "gradle.properties") + properties_path = root / "Android" / "gradle.properties" + aar_name = read_sdl3_aar_name(properties_path) + ndk_version = read_gradle_property(properties_path, "androidNdkVersion") aar_path = root / "Android" / "libs" / aar_name if not aar_path.is_file(): print(f"missing SDL3 AAR: {aar_path}", file=sys.stderr) @@ -90,6 +97,15 @@ def main() -> int: print(str(error), file=sys.stderr) return 2 environment = os.environ.copy() + android_home = environment.get("ANDROID_HOME") + if not android_home: + print("ANDROID_HOME is required to resolve the project NDK", file=sys.stderr) + return 2 + ndk_home = pathlib.Path(android_home) / "ndk" / ndk_version + if not ndk_home.is_dir(): + print(f"configured Android NDK is missing: {ndk_home}", file=sys.stderr) + return 2 + environment["ANDROID_NDK_HOME"] = str(ndk_home) rustflags = environment.get("RUSTFLAGS", "").strip() link_flag = f"-L native={link_dir}" environment["RUSTFLAGS"] = f"{rustflags} {link_flag}".strip()