0.1.0-0-pre.11
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<!-- file: Android/README.md -->
|
||||
<!-- version: 7 -->
|
||||
<!-- version: 8 -->
|
||||
|
||||
# Android
|
||||
|
||||
@@ -91,3 +91,11 @@ assets/<game>/ -> assets/game/
|
||||
dans un répertoire généré sous `Android/<module>/build/generated/sasedevAssets/<variant>/`.
|
||||
|
||||
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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: ROADMAP.md -->
|
||||
<!-- version: 8 -->
|
||||
<!-- version: 9 -->
|
||||
|
||||
# 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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
106
deltas/0.1.0/0-pre.11.md
Normal file
106
deltas/0.1.0/0-pre.11.md
Normal file
@@ -0,0 +1,106 @@
|
||||
<!-- file: deltas/0.1.0/0-pre.11.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# 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`.
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/architecture/004-INPUT_AND_CONTROLS.md -->
|
||||
<!-- version: 3 -->
|
||||
<!-- version: 4 -->
|
||||
|
||||
# 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.
|
||||
|
||||
32
history/0.1.0/0-pre.10.fix.1.md
Normal file
32
history/0.1.0/0-pre.10.fix.1.md
Normal file
@@ -0,0 +1,32 @@
|
||||
<!-- file: history/0.1.0/0-pre.10.fix.1.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# 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.
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user