0.1.0-0-pre.11
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: Android/README.md -->
|
<!-- file: Android/README.md -->
|
||||||
<!-- version: 7 -->
|
<!-- version: 8 -->
|
||||||
|
|
||||||
# Android
|
# Android
|
||||||
|
|
||||||
@@ -91,3 +91,11 @@ assets/<game>/ -> assets/game/
|
|||||||
dans un répertoire généré sous `Android/<module>/build/generated/sasedevAssets/<variant>/`.
|
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.
|
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
|
// file: Android/common/build.gradle
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.android.library'
|
id 'com.android.library'
|
||||||
}
|
}
|
||||||
|
|
||||||
def sdl3AarName = providers.gradleProperty("sdl3AarName").get()
|
def sdl3AarName = providers.gradleProperty("sdl3AarName").get()
|
||||||
|
def androidNdkVersion = providers.gradleProperty("androidNdkVersion").get()
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'com.sasedev.games.common'
|
namespace 'com.sasedev.games.common'
|
||||||
compileSdk 36
|
compileSdk 36
|
||||||
|
ndkVersion androidNdkVersion
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk 21
|
minSdk 21
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
// file: Android/game-reflex-poc/build.gradle
|
// file: Android/game-reflex-poc/build.gradle
|
||||||
// version: 8
|
// version: 9
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
}
|
}
|
||||||
|
|
||||||
def sdl3AarName = providers.gradleProperty("sdl3AarName").get()
|
def sdl3AarName = providers.gradleProperty("sdl3AarName").get()
|
||||||
|
def androidNdkVersion = providers.gradleProperty("androidNdkVersion").get()
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'com.sasedev.games.reflex'
|
namespace 'com.sasedev.games.reflex'
|
||||||
compileSdk 36
|
compileSdk 36
|
||||||
|
ndkVersion androidNdkVersion
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId 'com.sasedev.games.reflex'
|
applicationId 'com.sasedev.games.reflex'
|
||||||
minSdk 21
|
minSdk 21
|
||||||
targetSdk 36
|
targetSdk 36
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName '0.1.0-0-pre.10'
|
versionName '0.1.0-0-pre.11'
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
// file: Android/game-snake-poc/build.gradle
|
// file: Android/game-snake-poc/build.gradle
|
||||||
// version: 8
|
// version: 9
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
}
|
}
|
||||||
|
|
||||||
def sdl3AarName = providers.gradleProperty("sdl3AarName").get()
|
def sdl3AarName = providers.gradleProperty("sdl3AarName").get()
|
||||||
|
def androidNdkVersion = providers.gradleProperty("androidNdkVersion").get()
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'com.sasedev.games.snake'
|
namespace 'com.sasedev.games.snake'
|
||||||
compileSdk 36
|
compileSdk 36
|
||||||
|
ndkVersion androidNdkVersion
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId 'com.sasedev.games.snake'
|
applicationId 'com.sasedev.games.snake'
|
||||||
minSdk 21
|
minSdk 21
|
||||||
targetSdk 36
|
targetSdk 36
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName '0.1.0-0-pre.10'
|
versionName '0.1.0-0-pre.11'
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# file: Android/gradle.properties
|
# file: Android/gradle.properties
|
||||||
# version: 1
|
# version: 2
|
||||||
|
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
sdl3AarName=SDL3-3.4.16.aar
|
sdl3AarName=SDL3-3.4.16.aar
|
||||||
|
androidNdkVersion=28.2.13676358
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 20
|
# version: 21
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
@@ -17,7 +17,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.0-0-pre.10.fix.1"
|
version = "0.1.0-0-pre.11"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/games"
|
repository = "https://git.sasedev.com/Sasedev/games"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: ROADMAP.md -->
|
<!-- file: ROADMAP.md -->
|
||||||
<!-- version: 8 -->
|
<!-- version: 9 -->
|
||||||
|
|
||||||
# Roadmap
|
# 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.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.
|
||||||
- [x] `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.
|
- [x] `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.
|
||||||
- [ ] `2-beta.1` — stabilisation, packaging, tests multi-appareils.
|
- [ ] `2-beta.1` — stabilisation, packaging, tests multi-appareils.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/engines/engine-v1-sdl/src/runtime.rs
|
// file: crates/engines/engine-v1-sdl/src/runtime.rs
|
||||||
// version: 7
|
// version: 8
|
||||||
|
|
||||||
/// Minimal SDL3 runtime used by Desktop and Android POC runners.
|
/// Minimal SDL3 runtime used by Desktop and Android POC runners.
|
||||||
pub struct SdlRuntime {
|
pub struct SdlRuntime {
|
||||||
@@ -47,9 +47,22 @@ 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::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, .. } => {
|
sdl3::event::Event::MouseButtonDown { mouse_btn: sdl3::mouse::MouseButton::Left, x, y, .. } => {
|
||||||
pointer = normalize_mouse_pointer(&canvas, true, x, y);
|
pointer = normalize_mouse_pointer(&canvas, 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);
|
||||||
|
input = apply_pointer_direction(input, pointer);
|
||||||
},
|
},
|
||||||
sdl3::event::Event::MouseButtonUp { mouse_btn: sdl3::mouse::MouseButton::Left, x, y, .. } => {
|
sdl3::event::Event::MouseButtonUp { mouse_btn: sdl3::mouse::MouseButton::Left, x, y, .. } => {
|
||||||
pointer = normalize_mouse_pointer(&canvas, false, x, y);
|
pointer = normalize_mouse_pointer(&canvas, false, x, y);
|
||||||
@@ -62,6 +75,7 @@ impl SdlRuntime {
|
|||||||
sdl3::event::Event::FingerDown { x, y, .. } => {
|
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);
|
||||||
|
input = apply_pointer_direction(input, pointer);
|
||||||
},
|
},
|
||||||
sdl3::event::Event::FingerMotion { x, y, .. } => {
|
sdl3::event::Event::FingerMotion { x, y, .. } => {
|
||||||
pointer = engine_v1_common::PointerState::normalized(true, 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();
|
canvas.present();
|
||||||
return std::result::Result::Ok(());
|
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
|
// 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)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub struct SnakeState {
|
pub struct SnakeState {
|
||||||
length: u32,
|
segments: [SnakeCell; MAX_SEGMENTS],
|
||||||
|
length: usize,
|
||||||
|
direction: SnakeDirection,
|
||||||
|
food: SnakeCell,
|
||||||
|
food_index: u64,
|
||||||
|
score: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SnakeState {
|
impl Default for SnakeState {
|
||||||
@@ -17,28 +62,161 @@ impl SnakeState {
|
|||||||
/// Creates a fresh Snake state.
|
/// Creates a fresh Snake state.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn new() -> Self {
|
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.
|
/// Returns the current snake length.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn length(self) -> u32 {
|
pub const fn length(self) -> usize {
|
||||||
return self.length;
|
return self.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Registers one consumed growth item.
|
/// Returns the current score.
|
||||||
pub const fn grow(&mut self) {
|
#[must_use]
|
||||||
self.length = self.length.saturating_add(1);
|
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 {
|
impl engine_v1_common::EngineGame for SnakeState {
|
||||||
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) {
|
self.apply_direction_input(input);
|
||||||
self.grow();
|
if frame.index() % MOVE_EVERY_FRAMES == 0 {
|
||||||
|
self.move_once();
|
||||||
}
|
}
|
||||||
return;
|
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)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -1,25 +1,62 @@
|
|||||||
// file: crates/games/game-snake-poc/unit_tests/state.rs
|
// 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]
|
#[test]
|
||||||
fn growth_increments_length() {
|
fn eating_food_grows_and_scores() {
|
||||||
game_logging_lib::with_test_tracing("growth_increments_length", || {
|
game_logging_lib::with_test_tracing("eating_food_grows_and_scores", || {
|
||||||
let mut state = crate::SnakeState::new();
|
let mut state = crate::SnakeState::new();
|
||||||
state.grow();
|
let initial_length = state.length();
|
||||||
assert_eq!(state.length(), 2);
|
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]
|
#[test]
|
||||||
fn engine_update_maps_primary_action_to_growth() {
|
fn opposite_direction_is_rejected() {
|
||||||
game_logging_lib::with_test_tracing("engine_update_maps_primary_action_to_growth", || {
|
game_logging_lib::with_test_tracing("opposite_direction_is_rejected", || {
|
||||||
let mut state = crate::SnakeState::new();
|
let mut state = crate::SnakeState::new();
|
||||||
let input = engine_v1_common::InputState::none().with_action(engine_v1_common::GameAction::Primary, true);
|
let input = engine_v1_common::InputState::none().with_action(engine_v1_common::GameAction::Left, true);
|
||||||
engine_v1_common::EngineGame::update(
|
engine_v1_common::EngineGame::update(&mut state, frame(0), input);
|
||||||
&mut state,
|
assert_eq!(state.head(), crate::SnakeCell::new(6, 10));
|
||||||
engine_v1_common::EngineFrame::new(0, std::time::Duration::from_millis(16), std::time::Duration::ZERO),
|
});
|
||||||
input,
|
}
|
||||||
);
|
|
||||||
assert_eq!(state.length(), 2);
|
#[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 -->
|
<!-- file: docs/architecture/004-INPUT_AND_CONTROLS.md -->
|
||||||
<!-- version: 3 -->
|
<!-- version: 4 -->
|
||||||
|
|
||||||
# Abstraction des entrées et contrôles
|
# 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.
|
- `MouseButtonUp`, `FingerUp` et `FingerCanceled` désactivent le pointeur.
|
||||||
|
|
||||||
Cette séparation empêche un contact maintenu de produire un succès par frame.
|
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
|
#!/usr/bin/env python3
|
||||||
# file: scripts/build_android_rust.py
|
# file: scripts/build_android_rust.py
|
||||||
# version: 2
|
# version: 3
|
||||||
|
|
||||||
"""Build one Android Rust game entrypoint and stage SDL3 and Rust jniLibs."""
|
"""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"}
|
GAME_TO_MODULE = {"reflex": "game-reflex-poc", "snake": "game-snake-poc"}
|
||||||
|
|
||||||
def read_sdl3_aar_name(properties_path: pathlib.Path) -> str:
|
def read_gradle_property(properties_path: pathlib.Path, property_name: str) -> str:
|
||||||
"""Read the configured SDL3 AAR filename."""
|
"""Read one required Android Gradle property."""
|
||||||
|
prefix = f"{property_name}="
|
||||||
for raw_line in properties_path.read_text(encoding="utf-8").splitlines():
|
for raw_line in properties_path.read_text(encoding="utf-8").splitlines():
|
||||||
line = raw_line.strip()
|
line = raw_line.strip()
|
||||||
if line.startswith("sdl3AarName="):
|
if line.startswith(prefix):
|
||||||
return line.split("=", 1)[1].strip()
|
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:
|
def find_sdl3_member(archive: zipfile.ZipFile, abi: str) -> str:
|
||||||
"""Return the SDL3 shared-library member for one Android ABI."""
|
"""Return the SDL3 shared-library member for one Android ABI."""
|
||||||
@@ -75,7 +80,9 @@ def main() -> int:
|
|||||||
parser.add_argument("--release", action="store_true")
|
parser.add_argument("--release", action="store_true")
|
||||||
arguments = parser.parse_args()
|
arguments = parser.parse_args()
|
||||||
root = pathlib.Path(__file__).resolve().parent.parent
|
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
|
aar_path = root / "Android" / "libs" / aar_name
|
||||||
if not aar_path.is_file():
|
if not aar_path.is_file():
|
||||||
print(f"missing SDL3 AAR: {aar_path}", file=sys.stderr)
|
print(f"missing SDL3 AAR: {aar_path}", file=sys.stderr)
|
||||||
@@ -90,6 +97,15 @@ def main() -> int:
|
|||||||
print(str(error), file=sys.stderr)
|
print(str(error), file=sys.stderr)
|
||||||
return 2
|
return 2
|
||||||
environment = os.environ.copy()
|
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()
|
rustflags = environment.get("RUSTFLAGS", "").strip()
|
||||||
link_flag = f"-L native={link_dir}"
|
link_flag = f"-L native={link_dir}"
|
||||||
environment["RUSTFLAGS"] = f"{rustflags} {link_flag}".strip()
|
environment["RUSTFLAGS"] = f"{rustflags} {link_flag}".strip()
|
||||||
|
|||||||
Reference in New Issue
Block a user