0.3.0-0-pre.5

This commit is contained in:
2026-09-20 13:58:13 +02:00
parent 86b8af3b61
commit 7f0635ec9d
27 changed files with 827 additions and 39 deletions

View File

@@ -1,5 +1,5 @@
# file: Cargo.toml # file: Cargo.toml
# version: 66 # version: 67
[workspace] [workspace]
resolver = "3" resolver = "3"
@@ -20,7 +20,7 @@ members = [
] ]
[workspace.package] [workspace.package]
version = "0.3.0-0-pre.4.fix.3" version = "0.3.0-0-pre.5"
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: README.md --> <!-- file: README.md -->
<!-- version: 34 --> <!-- version: 35 -->
# games.sasedev # games.sasedev
@@ -26,7 +26,7 @@ Workspace expérimental puis productif pour des jeux multiplateformes principale
Version stable de référence : `0.2.0`. Version stable de référence : `0.2.0`.
Version de développement : `0.3.0-0-pre.4.fix.3`. Version de développement : `0.3.0-0-pre.5`.
Les deux premiers jeux sont des POC structurels : `game-reflex-poc` et `game-snake-poc`. Ils existent d'abord pour valider les frontières du workspace, le moteur, les assets et le packaging multiplateforme. Les deux premiers jeux sont des POC structurels : `game-reflex-poc` et `game-snake-poc`. Ils existent d'abord pour valider les frontières du workspace, le moteur, les assets et le packaging multiplateforme.

View File

@@ -1,5 +1,5 @@
<!-- file: Web/game-snake-poc/frontend/main.html --> <!-- file: Web/game-snake-poc/frontend/main.html -->
<!-- version: 3 --> <!-- version: 4 -->
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fr"> <html lang="fr">
@@ -81,6 +81,16 @@
<i class="fa-solid fa-arrow-right" aria-hidden="true"></i> <i class="fa-solid fa-arrow-right" aria-hidden="true"></i>
</button> </button>
</div> </div>
<dl class="runtime-details small border-top pt-3 mt-3 mb-0">
<div class="d-flex justify-content-between gap-3">
<dt class="text-body-secondary fw-normal">Provenance</dt>
<dd class="mb-2 text-end"><output id="runtime-provenance">web / browser / wasm / unknown / unknown</output></dd>
</div>
<div class="d-flex justify-content-between gap-3">
<dt class="text-body-secondary fw-normal">Assets</dt>
<dd class="mb-0 text-end"><output id="asset-status">chargement…</output></dd>
</div>
</dl>
<div class="alert alert-info small mt-3 mb-0" role="note"> <div class="alert alert-info small mt-3 mb-0" role="note">
Les entrées ne font pas avancer la simulation : elles sont consommées au prochain <code>tick()</code> WASM. Les entrées ne font pas avancer la simulation : elles sont consommées au prochain <code>tick()</code> WASM.
</div> </div>

View File

@@ -1,5 +1,5 @@
// file: Web/game-snake-poc/frontend/sass/_app.scss // file: Web/game-snake-poc/frontend/sass/_app.scss
// version: 3 // version: 4
$app-header-height: 72px; $app-header-height: 72px;
$app-footer-height: 42px; $app-footer-height: 42px;
@@ -101,6 +101,11 @@ body {
min-width: 0; min-width: 0;
} }
.runtime-details dd {
min-width: 0;
overflow-wrap: anywhere;
}
.direction-pad { .direction-pad {
display: grid; display: grid;
grid-template-columns: repeat(3, minmax(64px, 88px)); grid-template-columns: repeat(3, minmax(64px, 88px));

View File

@@ -0,0 +1,54 @@
// file: Web/game-snake-poc/frontend/ts/assets.ts
// version: 1
export interface SnakeWebAssets {
engineGeneration: number;
game: string;
gameKind: string;
}
interface CommonRuntimeAsset {
schema: number;
scope: string;
engine_generation: number;
}
interface GameRuntimeAsset {
schema: number;
game: string;
kind: string;
}
async function fetchJson<T>(relativePath: string): Promise<T> {
const response = await fetch(new URL(relativePath, document.baseURI));
if (!response.ok) {
throw new Error(`Asset runtime indisponible (${response.status}) : ${relativePath}`);
}
return (await response.json()) as T;
}
function validateCommonRuntime(asset: CommonRuntimeAsset): void {
if (asset.schema !== 1 || asset.scope !== "common" || asset.engine_generation !== 1) {
throw new Error("Asset common://data/runtime.json invalide pour le POC Web Snake.");
}
}
function validateGameRuntime(asset: GameRuntimeAsset): void {
if (asset.schema !== 1 || asset.game !== "game-snake-poc" || asset.kind !== "snake") {
throw new Error("Asset game://data/game.json invalide pour le POC Web Snake.");
}
}
export async function loadSnakeWebAssets(): Promise<SnakeWebAssets> {
const [commonRuntime, gameRuntime] = await Promise.all([
fetchJson<CommonRuntimeAsset>("./common/data/runtime.json"),
fetchJson<GameRuntimeAsset>("./game/data/game.json"),
]);
validateCommonRuntime(commonRuntime);
validateGameRuntime(gameRuntime);
return {
engineGeneration: commonRuntime.engine_generation,
game: gameRuntime.game,
gameKind: gameRuntime.kind,
};
}

View File

@@ -1,7 +1,9 @@
// file: Web/game-snake-poc/frontend/ts/game.ts // file: Web/game-snake-poc/frontend/ts/game.ts
// version: 2 // version: 3
import init, { SnakeWasmGame } from "@snake-wasm"; import init, { SnakeWasmGame } from "@snake-wasm";
import { webDebug, webInfo } from "./logging";
import { BrowserRuntimeProvenance, type SnakeInputSource } from "./provenance";
const FRAME_MILLIS = 16; const FRAME_MILLIS = 16;
const MAX_FRAME_ELAPSED_MILLIS = 250; const MAX_FRAME_ELAPSED_MILLIS = 250;
@@ -9,7 +11,12 @@ const MAX_FRAME_ELAPSED_MILLIS = 250;
export type SnakeDirection = "left" | "right" | "up" | "down"; export type SnakeDirection = "left" | "right" | "up" | "down";
export interface SnakeWebSession { export interface SnakeWebSession {
queueDirection(direction: SnakeDirection): void; queueDirection(direction: SnakeDirection, source: SnakeInputSource): void;
pause(): void;
resume(): void;
renderNow(): void;
refreshDeviceClass(): void;
dispose(): void;
} }
function color(red: number, green: number, blue: number): string { function color(red: number, green: number, blue: number): string {
@@ -71,18 +78,42 @@ export async function startGame(
canvas: HTMLCanvasElement, canvas: HTMLCanvasElement,
score: HTMLOutputElement, score: HTMLOutputElement,
length: HTMLOutputElement, length: HTMLOutputElement,
provenanceOutput: HTMLOutputElement,
): Promise<SnakeWebSession> { ): Promise<SnakeWebSession> {
await init(); await init();
webInfo("snake-web-runtime", "wasm_initialized");
const context = canvas.getContext("2d"); const context = canvas.getContext("2d");
if (context === null) { if (context === null) {
throw new Error("Le contexte Canvas 2D est indisponible."); throw new Error("Le contexte Canvas 2D est indisponible.");
} }
const renderingContext: CanvasRenderingContext2D = context; const renderingContext: CanvasRenderingContext2D = context;
const game = new SnakeWasmGame(); const game = new SnakeWasmGame();
const provenance = new BrowserRuntimeProvenance(game, provenanceOutput);
let previous = performance.now(); let previous = performance.now();
let accumulator = 0; let accumulator = 0;
let animationFrame: number | null = null;
let running = false;
let disposed = false;
function renderNow(): void {
if (disposed) {
return;
}
render(game, canvas, renderingContext, score, length);
}
function schedule(): void {
if (!running || disposed || animationFrame !== null) {
return;
}
animationFrame = window.requestAnimationFrame(frame);
}
function frame(now: number): void { function frame(now: number): void {
animationFrame = null;
if (!running || disposed) {
return;
}
const elapsed = Math.min(Math.max(0, now - previous), MAX_FRAME_ELAPSED_MILLIS); const elapsed = Math.min(Math.max(0, now - previous), MAX_FRAME_ELAPSED_MILLIS);
previous = now; previous = now;
accumulator += elapsed; accumulator += elapsed;
@@ -90,15 +121,59 @@ export async function startGame(
game.tick(); game.tick();
accumulator -= FRAME_MILLIS; accumulator -= FRAME_MILLIS;
} }
render(game, canvas, renderingContext, score, length); renderNow();
window.requestAnimationFrame(frame); schedule();
} }
render(game, canvas, renderingContext, score, length); function pause(): void {
window.requestAnimationFrame(frame); if (!running || disposed) {
return;
}
running = false;
accumulator = 0;
if (animationFrame !== null) {
window.cancelAnimationFrame(animationFrame);
animationFrame = null;
}
webDebug("snake-web-runtime", "runtime_paused");
}
function resume(): void {
if (running || disposed) {
return;
}
previous = performance.now();
accumulator = 0;
running = true;
schedule();
webDebug("snake-web-runtime", "runtime_resumed");
}
renderNow();
resume();
return { return {
queueDirection(direction: SnakeDirection): void { queueDirection(direction: SnakeDirection, source: SnakeInputSource): void {
if (disposed) {
return;
}
provenance.recordInput(source);
queueDirection(game, direction); queueDirection(game, direction);
}, },
pause,
resume,
renderNow,
refreshDeviceClass(): void {
if (!disposed) {
provenance.refreshDeviceClass();
}
},
dispose(): void {
if (disposed) {
return;
}
pause();
disposed = true;
game.free();
},
}; };
} }

View File

@@ -1,7 +1,8 @@
// file: Web/game-snake-poc/frontend/ts/input.ts // file: Web/game-snake-poc/frontend/ts/input.ts
// version: 1 // version: 2
import type { SnakeDirection, SnakeWebSession } from "./game"; import type { SnakeDirection, SnakeWebSession } from "./game";
import { webDebug } from "./logging";
const KEY_DIRECTIONS: Readonly<Record<string, SnakeDirection>> = { const KEY_DIRECTIONS: Readonly<Record<string, SnakeDirection>> = {
arrowleft: "left", arrowleft: "left",
@@ -34,7 +35,8 @@ export function bindInputs(session: SnakeWebSession, buttons: readonly HTMLButto
return; return;
} }
event.preventDefault(); event.preventDefault();
session.queueDirection(direction); session.queueDirection(direction, "keyboard-mouse");
webDebug("snake-web-input", "direction", { direction, source: "keyboard-mouse" });
}); });
for (const button of buttons) { for (const button of buttons) {
@@ -44,13 +46,16 @@ export function bindInputs(session: SnakeWebSession, buttons: readonly HTMLButto
} }
button.addEventListener("pointerdown", event => { button.addEventListener("pointerdown", event => {
event.preventDefault(); event.preventDefault();
session.queueDirection(direction); const source = event.pointerType === "touch" ? "touch" : "keyboard-mouse";
session.queueDirection(direction, source);
webDebug("snake-web-input", "direction", { direction, source });
}); });
button.addEventListener("click", event => { button.addEventListener("click", event => {
if (event.detail !== 0) { if (event.detail !== 0) {
return; return;
} }
session.queueDirection(direction); session.queueDirection(direction, "keyboard-mouse");
webDebug("snake-web-input", "direction", { direction, source: "keyboard-mouse" });
}); });
} }
} }

View File

@@ -0,0 +1,58 @@
// file: Web/game-snake-poc/frontend/ts/lifecycle.ts
// version: 1
import type { SnakeWebSession } from "./game";
import { webDebug, webInfo } from "./logging";
export interface SnakeLifecycleBinding {
dispose(): void;
}
export function bindBrowserLifecycle(session: SnakeWebSession, stage: HTMLElement): SnakeLifecycleBinding {
const resizeObserver = new ResizeObserver(() => {
session.refreshDeviceClass();
session.renderNow();
});
function synchronizeVisibility(): void {
if (document.visibilityState === "hidden") {
session.pause();
webDebug("snake-web-lifecycle", "visibility_hidden");
return;
}
session.resume();
session.renderNow();
webDebug("snake-web-lifecycle", "visibility_visible");
}
function pageHide(event: PageTransitionEvent): void {
if (event.persisted) {
session.pause();
} else {
session.dispose();
}
webInfo("snake-web-lifecycle", "page_hide", { persisted: event.persisted });
}
function pageShow(event: PageTransitionEvent): void {
session.resume();
session.renderNow();
webInfo("snake-web-lifecycle", "page_show", { persisted: event.persisted });
}
resizeObserver.observe(stage);
document.addEventListener("visibilitychange", synchronizeVisibility);
window.addEventListener("pagehide", pageHide);
window.addEventListener("pageshow", pageShow);
synchronizeVisibility();
return {
dispose(): void {
resizeObserver.disconnect();
document.removeEventListener("visibilitychange", synchronizeVisibility);
window.removeEventListener("pagehide", pageHide);
window.removeEventListener("pageshow", pageShow);
session.dispose();
},
};
}

View File

@@ -0,0 +1,45 @@
// file: Web/game-snake-poc/frontend/ts/logging.ts
// version: 1
export type WebLogLevel = "debug" | "info" | "warn" | "error";
export type WebLogFields = Readonly<Record<string, boolean | number | string | null>>;
type ConsoleMethod = (...items: unknown[]) => void;
function consoleMethod(level: WebLogLevel): ConsoleMethod {
if (level === "debug") {
return console.debug.bind(console);
}
if (level === "info") {
return console.info.bind(console);
}
if (level === "warn") {
return console.warn.bind(console);
}
return console.error.bind(console);
}
function emit(level: WebLogLevel, target: string, action: string, fields: WebLogFields = {}): void {
const event = {
target,
action,
...fields,
};
consoleMethod(level)(`[games.sasedev][${target}] ${action}`, event);
}
export function webDebug(target: string, action: string, fields: WebLogFields = {}): void {
emit("debug", target, action, fields);
}
export function webInfo(target: string, action: string, fields: WebLogFields = {}): void {
emit("info", target, action, fields);
}
export function webWarn(target: string, action: string, fields: WebLogFields = {}): void {
emit("warn", target, action, fields);
}
export function webError(target: string, action: string, fields: WebLogFields = {}): void {
emit("error", target, action, fields);
}

View File

@@ -1,10 +1,13 @@
// file: Web/game-snake-poc/frontend/ts/main.ts // file: Web/game-snake-poc/frontend/ts/main.ts
// version: 2 // version: 3
import ResizeObserver from "resize-observer-polyfill"; import ResizeObserver from "resize-observer-polyfill";
import "simplebar"; import "simplebar";
import { loadSnakeWebAssets } from "./assets";
import { startGame } from "./game"; import { startGame } from "./game";
import { bindInputs } from "./input"; import { bindInputs } from "./input";
import { bindBrowserLifecycle } from "./lifecycle";
import { webError, webInfo } from "./logging";
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver; (window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
@@ -18,25 +21,39 @@ function requireElement<T extends Element>(selector: string): T {
async function main(): Promise<void> { async function main(): Promise<void> {
const canvas = requireElement<HTMLCanvasElement>("#game"); const canvas = requireElement<HTMLCanvasElement>("#game");
const stage = requireElement<HTMLElement>(".game-stage");
const score = requireElement<HTMLOutputElement>("#score"); const score = requireElement<HTMLOutputElement>("#score");
const length = requireElement<HTMLOutputElement>("#length"); const length = requireElement<HTMLOutputElement>("#length");
const provenance = requireElement<HTMLOutputElement>("#runtime-provenance");
const assets = requireElement<HTMLOutputElement>("#asset-status");
const status = requireElement<HTMLSpanElement>("#runtime-status"); const status = requireElement<HTMLSpanElement>("#runtime-status");
const error = requireElement<HTMLParagraphElement>("#startup-error"); const error = requireElement<HTMLParagraphElement>("#startup-error");
const buttons = Array.from(document.querySelectorAll<HTMLButtonElement>("[data-direction]")); const buttons = Array.from(document.querySelectorAll<HTMLButtonElement>("[data-direction]"));
status.textContent = "Chargement assets…";
const assetMetadata = await loadSnakeWebAssets();
assets.value = `${assetMetadata.gameKind} / engine-v${assetMetadata.engineGeneration}`;
webInfo("snake-web-assets", "runtime_assets_loaded", {
engineGeneration: assetMetadata.engineGeneration,
game: assetMetadata.game,
gameKind: assetMetadata.gameKind,
});
status.textContent = "Chargement WASM…"; status.textContent = "Chargement WASM…";
const session = await startGame(canvas, score, length); const session = await startGame(canvas, score, length, provenance);
bindInputs(session, buttons); bindInputs(session, buttons);
bindBrowserLifecycle(session, stage);
status.className = "badge text-bg-success"; status.className = "badge text-bg-success";
status.textContent = "Prêt"; status.textContent = "Prêt";
error.hidden = true; error.hidden = true;
canvas.focus(); canvas.focus();
webInfo("snake-web-main", "runtime_ready");
} }
void main().catch(caughtError => { void main().catch(caughtError => {
const status = document.querySelector<HTMLSpanElement>("#runtime-status"); const status = document.querySelector<HTMLSpanElement>("#runtime-status");
const error = document.querySelector<HTMLParagraphElement>("#startup-error"); const error = document.querySelector<HTMLParagraphElement>("#startup-error");
const message = caughtError instanceof Error ? caughtError.message : String(caughtError); const message = caughtError instanceof Error ? caughtError.stack ?? caughtError.message : String(caughtError);
if (status !== null) { if (status !== null) {
status.className = "badge text-bg-danger"; status.className = "badge text-bg-danger";
status.textContent = "Erreur"; status.textContent = "Erreur";
@@ -45,4 +62,5 @@ void main().catch(caughtError => {
error.textContent = `Impossible de démarrer Snake : ${message}`; error.textContent = `Impossible de démarrer Snake : ${message}`;
error.hidden = false; error.hidden = false;
} }
webError("snake-web-main", "runtime_start_failed", { message });
}); });

View File

@@ -0,0 +1,98 @@
// file: Web/game-snake-poc/frontend/ts/provenance.ts
// version: 1
import type { SnakeWasmGame } from "@snake-wasm";
import { webInfo } from "./logging";
export type SnakeInputSource = "keyboard-mouse" | "touch";
type DeviceClass = "desktop" | "phone" | "tablet" | "unknown";
type InputProfile = "keyboard-mouse" | "mixed" | "touch" | "unknown";
function detectedDeviceClass(): DeviceClass {
const coarsePointer = window.matchMedia("(pointer: coarse)").matches;
const finePointer = window.matchMedia("(pointer: fine)").matches;
if (!coarsePointer || finePointer) {
return "desktop";
}
const shortEdge = Math.min(window.screen.width, window.screen.height);
if (!Number.isFinite(shortEdge) || shortEdge <= 0) {
return "unknown";
}
return shortEdge < 768 ? "phone" : "tablet";
}
function inputProfile(keyboardMouseObserved: boolean, touchObserved: boolean): InputProfile {
if (keyboardMouseObserved && touchObserved) {
return "mixed";
}
if (keyboardMouseObserved) {
return "keyboard-mouse";
}
if (touchObserved) {
return "touch";
}
return "unknown";
}
function displayLabel(game: SnakeWasmGame): string {
return [
game.provenance_platform_family(),
game.provenance_runtime_host(),
game.provenance_execution_model(),
game.provenance_device_class(),
game.provenance_input_profile(),
].join(" / ");
}
export class BrowserRuntimeProvenance {
private keyboardMouseObserved = false;
private touchObserved = false;
private deviceClass: DeviceClass = detectedDeviceClass();
public constructor(
private readonly game: SnakeWasmGame,
private readonly output: HTMLOutputElement,
) {
this.synchronize();
}
public recordInput(source: SnakeInputSource): void {
if (source === "touch") {
if (this.touchObserved) {
return;
}
this.touchObserved = true;
} else {
if (this.keyboardMouseObserved) {
return;
}
this.keyboardMouseObserved = true;
}
this.synchronize();
}
public refreshDeviceClass(): void {
const nextDeviceClass = detectedDeviceClass();
if (nextDeviceClass === this.deviceClass) {
return;
}
this.deviceClass = nextDeviceClass;
this.synchronize();
}
private synchronize(): void {
const profile = inputProfile(this.keyboardMouseObserved, this.touchObserved);
if (!this.game.configure_runtime_provenance(this.deviceClass, profile)) {
throw new Error("La provenance navigateur n'a pas pu être configurée dans le bridge WASM.");
}
this.output.value = displayLabel(this.game);
webInfo("snake-web-provenance", "runtime_provenance", {
deviceClass: this.game.provenance_device_class(),
executionModel: this.game.provenance_execution_model(),
inputProfile: this.game.provenance_input_profile(),
platformFamily: this.game.provenance_platform_family(),
runtimeHost: this.game.provenance_runtime_host(),
});
}
}

View File

@@ -19,6 +19,7 @@
"@types/node": "^26.1", "@types/node": "^26.1",
"sass-embedded": "^1.102", "sass-embedded": "^1.102",
"typescript": "^7.0", "typescript": "^7.0",
"vite": "^8.2" "vite": "^8.2",
"vite-plugin-static-copy": "^4.1"
} }
} }

View File

@@ -1,10 +1,11 @@
// file: Web/game-snake-poc/vite.config.ts // file: Web/game-snake-poc/vite.config.ts
// version: 1 // version: 2
import { NodePackageImporter } from "sass-embedded"; import { NodePackageImporter } from "sass-embedded";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { resolve } from "node:path"; import { resolve } from "node:path";
import { defineConfig, normalizePath } from "vite"; import { defineConfig, normalizePath } from "vite";
import { viteStaticCopy } from "vite-plugin-static-copy";
const appRoot = fileURLToPath(new URL(".", import.meta.url)); const appRoot = fileURLToPath(new URL(".", import.meta.url));
const repositoryRoot = normalizePath(resolve(appRoot, "../..")); const repositoryRoot = normalizePath(resolve(appRoot, "../.."));
@@ -14,8 +15,18 @@ const wasmRoot = normalizePath(resolve(externalBuildRoot, "wasm"));
const wasmModule = normalizePath(resolve(wasmRoot, "game_snake_poc_wasm.js")); const wasmModule = normalizePath(resolve(wasmRoot, "game_snake_poc_wasm.js"));
const frontendDist = normalizePath(resolve(externalBuildRoot, "dist")); const frontendDist = normalizePath(resolve(externalBuildRoot, "dist"));
const viteCacheDir = normalizePath(resolve(externalBuildRoot, "vite-cache")); const viteCacheDir = normalizePath(resolve(externalBuildRoot, "vite-cache"));
const commonRuntimeAsset = normalizePath(resolve(repositoryRoot, "assets/common/data/runtime.json"));
const gameRuntimeAsset = normalizePath(resolve(repositoryRoot, "assets/game-snake-poc/data/game.json"));
export default defineConfig({ export default defineConfig({
plugins: [
viteStaticCopy({
targets: [
{ src: commonRuntimeAsset, dest: "common/data" },
{ src: gameRuntimeAsset, dest: "game/data" },
],
}),
],
base: "./", base: "./",
cacheDir: viteCacheDir, cacheDir: viteCacheDir,
clearScreen: false, clearScreen: false,
@@ -74,7 +85,7 @@ export default defineConfig({
port: 1434, port: 1434,
strictPort: true, strictPort: true,
fs: { fs: {
allow: [appRoot, externalBuildRoot], allow: [appRoot, repositoryRoot, externalBuildRoot],
}, },
}, },
preview: { preview: {

View File

@@ -1,5 +1,5 @@
# file: crates/apps/game-snake-poc-wasm/Cargo.toml # file: crates/apps/game-snake-poc-wasm/Cargo.toml
# version: 1 # version: 2
[package] [package]
name = "game-snake-poc-wasm" name = "game-snake-poc-wasm"
@@ -15,6 +15,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies] [dependencies]
engine-v1-common = { path = "../../engines/engine-v1-common" } engine-v1-common = { path = "../../engines/engine-v1-common" }
engine-v1-platform-api = { path = "../../engines/engine-v1-platform-api" }
game-snake-poc = { path = "../../games/game-snake-poc" } game-snake-poc = { path = "../../games/game-snake-poc" }
wasm-bindgen.workspace = true wasm-bindgen.workspace = true

View File

@@ -1,5 +1,5 @@
// file: crates/apps/game-snake-poc-wasm/src/runtime.rs // file: crates/apps/game-snake-poc-wasm/src/runtime.rs
// version: 1 // version: 2
const FIXED_STEP_MILLIS: u64 = 16; const FIXED_STEP_MILLIS: u64 = 16;
@@ -7,6 +7,7 @@ const FIXED_STEP_MILLIS: u64 = 16;
#[wasm_bindgen::prelude::wasm_bindgen] #[wasm_bindgen::prelude::wasm_bindgen]
pub struct SnakeWasmGame { pub struct SnakeWasmGame {
pending_input: engine_v1_common::InputState, pending_input: engine_v1_common::InputState,
provenance: engine_v1_platform_api::RuntimeProvenance,
runner: engine_v1_common::FixedStepRunner, runner: engine_v1_common::FixedStepRunner,
state: game_snake_poc::SnakeState, state: game_snake_poc::SnakeState,
} }
@@ -18,11 +19,63 @@ impl SnakeWasmGame {
pub fn new() -> Self { pub fn new() -> Self {
return Self { return Self {
pending_input: engine_v1_common::InputState::none(), pending_input: engine_v1_common::InputState::none(),
provenance: engine_v1_platform_api::RuntimeProvenance::new(
engine_v1_platform_api::DeviceClass::Unknown,
engine_v1_platform_api::ExecutionModel::Wasm,
engine_v1_platform_api::InputProfile::Unknown,
engine_v1_platform_api::PlatformFamily::Web,
engine_v1_platform_api::RuntimeHost::Browser,
),
runner: engine_v1_common::FixedStepRunner::new(std::time::Duration::from_millis(FIXED_STEP_MILLIS)), runner: engine_v1_common::FixedStepRunner::new(std::time::Duration::from_millis(FIXED_STEP_MILLIS)),
state: game_snake_poc::SnakeState::new(), state: game_snake_poc::SnakeState::new(),
}; };
} }
/// Updates browser-observed device and input dimensions while preserving Web/WASM/Browser provenance.
pub fn configure_runtime_provenance(&mut self, device_class: &str, input_profile: &str) -> bool {
let device_class = match parse_device_class(device_class) {
Some(value) => value,
None => return false,
};
let input_profile = match parse_input_profile(input_profile) {
Some(value) => value,
None => return false,
};
self.provenance = engine_v1_platform_api::RuntimeProvenance::new(
device_class,
engine_v1_platform_api::ExecutionModel::Wasm,
input_profile,
engine_v1_platform_api::PlatformFamily::Web,
engine_v1_platform_api::RuntimeHost::Browser,
);
return true;
}
/// Returns the current physical device class label.
pub fn provenance_device_class(&self) -> String {
return device_class_label(self.provenance.device_class()).to_string();
}
/// Returns the current execution model label.
pub fn provenance_execution_model(&self) -> String {
return execution_model_label(self.provenance.execution_model()).to_string();
}
/// Returns the current primary input profile label.
pub fn provenance_input_profile(&self) -> String {
return input_profile_label(self.provenance.input_profile()).to_string();
}
/// Returns the current platform family label.
pub fn provenance_platform_family(&self) -> String {
return platform_family_label(self.provenance.platform_family()).to_string();
}
/// Returns the current runtime host label.
pub fn provenance_runtime_host(&self) -> String {
return runtime_host_label(self.provenance.runtime_host()).to_string();
}
/// Queues a logical left direction for the next deterministic update. /// Queues a logical left direction for the next deterministic update.
pub fn left(&mut self) { pub fn left(&mut self) {
self.queue_direction(engine_v1_common::GameAction::Left); self.queue_direction(engine_v1_common::GameAction::Left);
@@ -183,6 +236,69 @@ impl SnakeWasmGame {
} }
} }
fn parse_device_class(label: &str) -> Option<engine_v1_platform_api::DeviceClass> {
return match label {
"desktop" => Some(engine_v1_platform_api::DeviceClass::Desktop),
"phone" => Some(engine_v1_platform_api::DeviceClass::Phone),
"tablet" => Some(engine_v1_platform_api::DeviceClass::Tablet),
"unknown" => Some(engine_v1_platform_api::DeviceClass::Unknown),
_ => None,
};
}
fn parse_input_profile(label: &str) -> Option<engine_v1_platform_api::InputProfile> {
return match label {
"gamepad" => Some(engine_v1_platform_api::InputProfile::Gamepad),
"keyboard-mouse" => Some(engine_v1_platform_api::InputProfile::KeyboardMouse),
"mixed" => Some(engine_v1_platform_api::InputProfile::Mixed),
"touch" => Some(engine_v1_platform_api::InputProfile::Touch),
"unknown" => Some(engine_v1_platform_api::InputProfile::Unknown),
_ => None,
};
}
fn device_class_label(value: engine_v1_platform_api::DeviceClass) -> &'static str {
return match value {
engine_v1_platform_api::DeviceClass::Desktop => "desktop",
engine_v1_platform_api::DeviceClass::Phone => "phone",
engine_v1_platform_api::DeviceClass::Tablet => "tablet",
engine_v1_platform_api::DeviceClass::Unknown => "unknown",
};
}
fn execution_model_label(value: engine_v1_platform_api::ExecutionModel) -> &'static str {
return match value {
engine_v1_platform_api::ExecutionModel::Native => "native",
engine_v1_platform_api::ExecutionModel::Wasm => "wasm",
};
}
fn input_profile_label(value: engine_v1_platform_api::InputProfile) -> &'static str {
return match value {
engine_v1_platform_api::InputProfile::Gamepad => "gamepad",
engine_v1_platform_api::InputProfile::KeyboardMouse => "keyboard-mouse",
engine_v1_platform_api::InputProfile::Mixed => "mixed",
engine_v1_platform_api::InputProfile::Touch => "touch",
engine_v1_platform_api::InputProfile::Unknown => "unknown",
};
}
fn platform_family_label(value: engine_v1_platform_api::PlatformFamily) -> &'static str {
return match value {
engine_v1_platform_api::PlatformFamily::Android => "android",
engine_v1_platform_api::PlatformFamily::Desktop => "desktop",
engine_v1_platform_api::PlatformFamily::Web => "web",
};
}
fn runtime_host_label(value: engine_v1_platform_api::RuntimeHost) -> &'static str {
return match value {
engine_v1_platform_api::RuntimeHost::Browser => "browser",
engine_v1_platform_api::RuntimeHost::Native => "native",
engine_v1_platform_api::RuntimeHost::TauriWebView => "tauri-webview",
};
}
#[cfg(test)] #[cfg(test)]
#[path = "../unit_tests/runtime.rs"] #[path = "../unit_tests/runtime.rs"]
mod tests; mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/apps/game-snake-poc-wasm/unit_tests/runtime.rs // file: crates/apps/game-snake-poc-wasm/unit_tests/runtime.rs
// version: 1 // version: 2
#[test] #[test]
fn direction_is_queued_without_advancing_simulation() { fn direction_is_queued_without_advancing_simulation() {
@@ -25,3 +25,28 @@ fn scene_and_status_are_exposed_for_canvas_host() {
assert_eq!(game.rectangle_x(99), -1.0); assert_eq!(game.rectangle_x(99), -1.0);
assert_eq!(game.rectangle_y(99), -1.0); assert_eq!(game.rectangle_y(99), -1.0);
} }
#[test]
fn browser_runtime_provenance_preserves_static_and_observed_dimensions() {
let mut game = crate::SnakeWasmGame::new();
assert_eq!(game.provenance_platform_family(), "web");
assert_eq!(game.provenance_execution_model(), "wasm");
assert_eq!(game.provenance_runtime_host(), "browser");
assert_eq!(game.provenance_device_class(), "unknown");
assert_eq!(game.provenance_input_profile(), "unknown");
assert!(game.configure_runtime_provenance("phone", "touch"));
assert_eq!(game.provenance_device_class(), "phone");
assert_eq!(game.provenance_input_profile(), "touch");
assert_eq!(game.provenance_platform_family(), "web");
assert_eq!(game.provenance_execution_model(), "wasm");
assert_eq!(game.provenance_runtime_host(), "browser");
}
#[test]
fn invalid_runtime_provenance_labels_are_rejected_without_mutation() {
let mut game = crate::SnakeWasmGame::new();
assert!(game.configure_runtime_provenance("desktop", "keyboard-mouse"));
assert!(!game.configure_runtime_provenance("console", "telepathy"));
assert_eq!(game.provenance_device_class(), "desktop");
assert_eq!(game.provenance_input_profile(), "keyboard-mouse");
}

118
deltas/0.3.0/0-pre.5.md Normal file
View File

@@ -0,0 +1,118 @@
<!-- file: deltas/0.3.0/0-pre.5.md -->
<!-- version: 1 -->
# Delta 0.3.0-0-pre.5
## Objectif
Fermer l'intégration plateforme du premier POC Snake Web direct après validation de `0-pre.4.fix.3` : lifecycle/resize navigateur, assets runtime, logging frontend structuré, provenance `Web/Wasm/Browser` et non-régression du runner Desktop SDL3.
## État précédent validé
`0-pre.4.fix.3` a été accepté par l'utilisateur le 2026-09-20 : build Vite propre et smoke visuel/fonctionnel conforme, y compris Canvas portrait, contrôles et scroll SimpleBar du shell KSP-derived. Le résultat est consigné sous `history/0.3.0/0-pre.4.fix.3.md`.
## Lifecycle et resize
Le host Web ne laisse plus une boucle `requestAnimationFrame` implicite posséder tout le lifecycle :
- `SnakeWebSession` expose `pause`, `resume`, `renderNow`, `refreshDeviceClass` et `dispose` ;
- `visibilitychange` et `pagehide` suspendent les ticks ;
- la reprise réinitialise l'origine temporelle et l'accumulateur afin d'éviter un rattrapage massif après un onglet caché ;
- `pageshow` reprend et redessine ;
- `ResizeObserver` redessine le Canvas et rafraîchit la classe d'appareil sans faire avancer la simulation ;
- la destruction de page libère l'instance WASM.
Le fixed-step gameplay reste dans Rust/WASM et aucun événement lifecycle n'est ajouté à `game-snake-poc`.
## Assets Web
Le package Vite déclare `vite-plugin-static-copy` et package explicitement les deux assets-sondes depuis les sources canoniques :
```text
assets/common/data/runtime.json -> common/data/runtime.json
assets/game-snake-poc/data/game.json -> game/data/game.json
```
`frontend/ts/assets.ts` charge et valide les deux JSON avant le démarrage WASM. Le shell affiche ensuite un statut d'assets afin que le smoke confirme le chargement réel. Aucune copie source durable n'est créée sous `Web/`.
## Logging navigateur
`frontend/ts/logging.ts` centralise les diagnostics Web sous forme d'événements `level / target / action / fields` vers la console du navigateur.
Ce logging est spécifique au frontend navigateur direct :
- il ne dépend pas de Tauri ;
- il ne remplace pas `tracing` dans le code Rust ;
- les runners natifs continuent d'utiliser `game-logging-lib`.
## Provenance runtime
`game-snake-poc-wasm` dépend désormais de `engine-v1-platform-api` et conserve une `RuntimeProvenance` réelle.
Dimensions fixes :
```text
PlatformFamily = Web
ExecutionModel = Wasm
RuntimeHost = Browser
```
Le host renseigne ensuite la classe `Desktop/Phone/Tablet/Unknown` et le profil d'entrée `Unknown/KeyboardMouse/Touch/Mixed` selon les capacités et inputs observés. Ces dimensions sont exposées par le bridge WASM et affichées dans le shell.
La crate de gameplay `game-snake-poc` reste sans dépendance plateforme.
## Règles et documentation
- `GAME-ASSET-007` fixe le packaging Web `common/` + `game/` sans copie source ;
- `GAME-TRACE-005` distingue logging frontend navigateur et tracing Rust ;
- `GAME-PLATFORM-024` fixe pause/reprise sans catch-up massif ;
- `GAME-PLATFORM-025` fixe la provenance navigateur via `engine-v1-platform-api` ;
- l'architecture assets/provenance, la documentation tracing/frontend et les gates sont réconciliées ;
- `audit_distribution_layout.py` vérifie désormais les fichiers et branchements statiques de cette intégration.
## Non modifié
- règles de gameplay Snake ;
- runner Snake Desktop SDL3 ;
- moteur SDL3 ;
- Android/Tauri ;
- ROADMAP et CHANGELOG, réservés à la consolidation prévue par le plan.
## Validation demandée
Depuis la racine :
```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 Web deltas history
python3 scripts/audit_distribution_layout.py
cargo check --workspace
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test -p engine-v1-platform-api --all-targets --all-features
cargo test -p game-snake-poc-wasm --all-targets --all-features
cargo build -p game-snake-poc-wasm --target wasm32-unknown-unknown
wasm-bindgen ../builds/sasedev-games/target/wasm32-unknown-unknown/debug/game_snake_poc_wasm.wasm --target web --out-dir ../builds/sasedev-games/game-snake-poc-web/wasm --out-name game_snake_poc_wasm
cargo tree -p game-snake-poc-wasm --edges normal
(cd Web/game-snake-poc && npm install && npm run build && npm run dev)
```
Smoke navigateur sur `http://127.0.0.1:1434/main.html` :
1. vérifier que `Assets` passe à `snake / engine-v1` avant le statut `Prêt` ;
2. vérifier que la provenance affiche `web / browser / wasm / ... / ...` ;
3. utiliser clavier/souris puis tactile si disponible et vérifier l'évolution du profil d'entrée (`keyboard-mouse`, `touch` ou `mixed`) ;
4. masquer l'onglet quelques secondes puis revenir : Snake ne doit pas accélérer pour rattraper le temps caché ;
5. redimensionner la fenêtre : Canvas et provenance device doivent rester cohérents sans saut de gameplay ;
6. revalider SimpleBar, clavier et contrôles pointer/tactiles.
Puis vérifier la non-régression Desktop :
```bash
cargo run -p game-snake-poc-desktop
```
## Suite
Si cette gate ne révèle ni duplication structurante ni défaut de frontière, la tranche conditionnelle `0-pre.6` est omise et la version peut entrer directement en `2-beta.1`. Sinon, `0-pre.6` ne traite que la correction/extraction prouvée par cette validation.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/000-README.md --> <!-- file: docs/000-README.md -->
<!-- version: 24 --> <!-- version: 25 -->
# Documentation games.sasedev # Documentation games.sasedev
@@ -75,7 +75,7 @@ Voir [`../RULES.md`](../RULES.md), notamment [`rules/RULES_DOCUMENTATION.md`](ru
- [`development/007-ANDROID_JNI_BRIDGE.md`](development/007-ANDROID_JNI_BRIDGE.md) — frontière Java/JNI minimale, version de contrat et séparation avec l'input SDL3. - [`development/007-ANDROID_JNI_BRIDGE.md`](development/007-ANDROID_JNI_BRIDGE.md) — frontière Java/JNI minimale, version de contrat et séparation avec l'input SDL3.
- [`development/008-WASM_TAURI_POC.md`](development/008-WASM_TAURI_POC.md) — baseline historique Reflex Tauri/WebAssembly et génération WASM associée. - [`development/008-WASM_TAURI_POC.md`](development/008-WASM_TAURI_POC.md) — baseline historique Reflex Tauri/WebAssembly et génération WASM associée.
- [`development/009-SNAKE_WEB_WASM_BUILD.md`](development/009-SNAKE_WEB_WASM_BUILD.md) — build Cargo + `wasm-bindgen` natif de l'adapter Snake pour le navigateur direct. - [`development/009-SNAKE_WEB_WASM_BUILD.md`](development/009-SNAKE_WEB_WASM_BUILD.md) — build Cargo + `wasm-bindgen` natif de l'adapter Snake pour le navigateur direct.
- [`development/010-SNAKE_WEB_FRONTEND.md`](development/010-SNAKE_WEB_FRONTEND.md) — host Web direct Snake sous Vite/TypeScript, shell Bootstrap 5, Canvas et contrôles clavier/tactiles. - [`development/010-SNAKE_WEB_FRONTEND.md`](development/010-SNAKE_WEB_FRONTEND.md) — host Web direct Snake sous Vite/TypeScript, shell Bootstrap 5, Canvas, lifecycle, assets, provenance et contrôles clavier/tactiles.
## Historique validé ## Historique validé

View File

@@ -1,5 +1,5 @@
<!-- file: docs/architecture/005-ASSET_ARCHITECTURE.md --> <!-- file: docs/architecture/005-ASSET_ARCHITECTURE.md -->
<!-- version: 3 --> <!-- version: 4 -->
# Architecture des assets # Architecture des assets
@@ -92,3 +92,16 @@ assets/game/...
``` ```
Les répertoires `build/generated/` restent des artefacts jetables. Les répertoires `build/generated/` restent des artefacts jetables.
## Web navigateur direct
Le POC Snake Web valide le même espace logique sans copier les sources dans `Web/`. Vite package explicitement les assets nécessaires depuis `assets/` vers :
```text
dist/common/data/runtime.json
dist/game/data/game.json
```
Le serveur Vite de développement expose les mêmes URL runtime. Le frontend charge donc `common://data/runtime.json` et `game://data/game.json` sous leurs chemins de distribution `./common/data/runtime.json` et `./game/data/game.json`, puis valide leur schéma avant de démarrer la session.
Cette première intégration ne transforme pas encore `game-assets-lib` en loader navigateur : la librairie Rust reste responsable de la validation/résolution de chemins physiques pour les hosts qui disposent d'un filesystem, tandis que Vite possède le packaging Web. Une abstraction commune ne sera extraite que si plusieurs hosts en ont réellement besoin.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/architecture/010-RUNTIME_PROVENANCE.md --> <!-- file: docs/architecture/010-RUNTIME_PROVENANCE.md -->
<!-- version: 1 --> <!-- version: 2 -->
# Provenance d'environnement d'exécution # Provenance d'environnement d'exécution
@@ -72,3 +72,17 @@ L'identité joueur reste orthogonale. Un score pourra appartenir à un utilisate
La provenance V1 décrit des catégories techniques générales. Elle ne requiert ni modèle précis de téléphone, ni identifiant matériel, ni adresse réseau. La provenance V1 décrit des catégories techniques générales. Elle ne requiert ni modèle précis de téléphone, ni identifiant matériel, ni adresse réseau.
Un besoin futur de diagnostic plus fin devra être ajouté explicitement plutôt que d'élargir silencieusement cette structure. Un besoin futur de diagnostic plus fin devra être ajouté explicitement plutôt que d'élargir silencieusement cette structure.
## POC Snake Web direct
À partir de `0.3.0-0-pre.5`, `game-snake-poc-wasm` consomme réellement `engine-v1-platform-api` et conserve une `RuntimeProvenance` pour la session navigateur. Les dimensions fixes du host sont :
```text
PlatformFamily = Web
ExecutionModel = Wasm
RuntimeHost = Browser
```
Le frontend observe la classe d'appareil à partir des capacités de pointeur et de la taille logique de l'écran sans collecter de modèle matériel. Le profil d'entrée commence à `Unknown`, devient `KeyboardMouse` ou `Touch` lors de l'utilisation réelle et passe à `Mixed` si les deux familles sont observées pendant la même session.
Cette provenance est affichée dans le shell et émise dans les diagnostics frontend afin de rendre le smoke vérifiable. Elle reste descriptive et ne modifie ni le gameplay ni le score.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/development/003-TRACING_AND_DIAGNOSTICS.md --> <!-- file: docs/development/003-TRACING_AND_DIAGNOSTICS.md -->
<!-- version: 1 --> <!-- version: 2 -->
# Tracing et diagnostics # Tracing et diagnostics
@@ -21,3 +21,9 @@ Les runners Desktop initialisent ce socle. Les futures intégrations Android pou
## Évolution ## Évolution
Les filtres par target/niveau, fichiers rotatifs, configuration utilisateur, traces Android et éventuelle télémétrie distante seront ajoutés uniquement lorsquun besoin concret les justifie. Les filtres par target/niveau, fichiers rotatifs, configuration utilisateur, traces Android et éventuelle télémétrie distante seront ajoutés uniquement lorsquun besoin concret les justifie.
## Host navigateur direct
Le navigateur direct ne possède ni backend Rust Tauri ni plugin de relayage. Son frontend utilise donc un module TypeScript unique de diagnostics structurés qui écrit dans la console du navigateur avec `target`, `action` et champs associés. Les modules applicatifs n'émettent pas leurs propres formats parallèles.
Cette sortie console est un mécanisme frontend. Elle ne remplace pas `tracing` dans le code Rust : les runners natifs continuent d'initialiser `game-logging-lib`, et un futur besoin de tracing Rust directement dans WebAssembly devra être introduit explicitement plutôt que d'être simulé par le frontend.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/development/010-SNAKE_WEB_FRONTEND.md --> <!-- file: docs/development/010-SNAKE_WEB_FRONTEND.md -->
<!-- version: 3 --> <!-- version: 4 -->
# Frontend du POC Snake Web direct # Frontend du POC Snake Web direct
@@ -133,4 +133,31 @@ Ouvrir explicitement `http://127.0.0.1:1434/main.html` — le host utilise volon
8. le shell KSP-derived et le Canvas restent utilisables sur une largeur mobile et Desktop ; 8. le shell KSP-derived et le Canvas restent utilisables sur une largeur mobile et Desktop ;
9. score et longueur restent visibles et évoluent avec l'état WASM. 9. score et longueur restent visibles et évoluent avec l'état WASM.
Le lifecycle complet, les assets, le logging structuré et la provenance runtime appartiennent à `0-pre.5`. ## Lifecycle navigateur
`0-pre.5` ferme le lifecycle du POC. La boucle `requestAnimationFrame` est suspendue lorsque le document devient caché ou reçoit `pagehide`. La reprise sur `visibilitychange`/`pageshow` réinitialise l'origine temporelle et l'accumulateur afin de ne pas rattraper artificiellement les ticks perdus en arrière-plan.
Un `ResizeObserver` attaché à la zone de jeu redessine le Canvas et rafraîchit la classe d'appareil observée sans faire avancer la simulation. La destruction de page libère l'instance WASM.
## Assets runtime
Le package Vite utilise `vite-plugin-static-copy` pour exposer les deux assets de sonde depuis les sources canoniques :
```text
assets/common/data/runtime.json -> dist/common/data/runtime.json
assets/game-snake-poc/data/game.json -> dist/game/data/game.json
```
Le serveur Vite de développement expose les mêmes routes. Le frontend charge et valide les deux JSON avant le démarrage WASM. Le statut visible `Assets` permet de confirmer le chargement pendant le smoke. Aucune copie source n'est ajoutée sous `Web/`.
## Logging frontend
`frontend/ts/logging.ts` centralise les diagnostics navigateur. Les événements sont structurés par niveau, target, action et champs, puis écrits dans la console. Ce module est volontairement distinct du bridge tracing Tauri : le host Web direct ne possède pas de backend Tauri à qui relayer ses événements.
Le Rust natif continue d'utiliser `tracing`/`game-logging-lib`; le frontend console ne remplace pas cette façade Rust.
## Provenance runtime
`game-snake-poc-wasm` dépend de `engine-v1-platform-api` uniquement au niveau adapter et conserve une `RuntimeProvenance`. Les dimensions `Web / Wasm / Browser` sont fixes ; le host renseigne la classe `Desktop`/`Phone`/`Tablet`/`Unknown` et le profil `Unknown`/`KeyboardMouse`/`Touch`/`Mixed` à partir des capacités et entrées réellement observées.
Le shell affiche ces cinq dimensions afin que le smoke puisse confirmer que la provenance évolue lorsqu'un clavier/souris ou un contrôle tactile est utilisé. Le gameplay `game-snake-poc` reste sans dépendance plateforme.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/001-V0_3_0_WEB_SNAKE_POC_PLAN.md --> <!-- file: docs/plans/001-V0_3_0_WEB_SNAKE_POC_PLAN.md -->
<!-- version: 9 --> <!-- version: 10 -->
# Plan v0.3.0 — baseline Snake et premier POC Web direct # Plan v0.3.0 — baseline Snake et premier POC Web direct
@@ -79,11 +79,15 @@ Suivi `0-pre.4.fix.2` : la validation de `fix.1` a confirmé les gates Rust/WASM
Suivi `0-pre.4.fix.3` : la validation de `fix.2` confirme la gate Rust/WASM, le build Vite production et le ratio Canvas corrigé. Le smoke révèle toutefois que le scroll SimpleBar ne reproduit pas encore le comportement du shell KSP. La comparaison avec `ksp-app-*-desk` montre que `data-simplebar` doit porter sur la zone centrale `app-content app-scrollable h-100`, enfant d'un `app-main` borné, et non sur `app-main` lui-même. Le fix aligne la structure HTML/Sass sur ce contrat sans initialisation manuelle spécifique. Suivi `0-pre.4.fix.3` : la validation de `fix.2` confirme la gate Rust/WASM, le build Vite production et le ratio Canvas corrigé. Le smoke révèle toutefois que le scroll SimpleBar ne reproduit pas encore le comportement du shell KSP. La comparaison avec `ksp-app-*-desk` montre que `data-simplebar` doit porter sur la zone centrale `app-content app-scrollable h-100`, enfant d'un `app-main` borné, et non sur `app-main` lui-même. Le fix aligne la structure HTML/Sass sur ce contrat sans initialisation manuelle spécifique.
Validation `0-pre.4.fix.3` : le build Vite reste propre et le smoke utilisateur confirme le comportement attendu du shell, y compris le scroll SimpleBar, le Canvas portrait et les contrôles. `0-pre.4` est donc fermé et `0-pre.5` peut porter l'intégration plateforme complète.
### `0-pre.5` — intégration plateforme complète ### `0-pre.5` — intégration plateforme complète
Fermer resize/lifecycle, assets, logging et provenance runtime du POC ; vérifier que Desktop SDL reste intact. Fermer resize/lifecycle, assets, logging et provenance runtime du POC ; vérifier que Desktop SDL reste intact.
Gate : tests ciblés, build Web et smoke navigateur couvrant les requirements de `0.3.0`. Livraison candidate : le host suspend la simulation sur `visibilitychange`/`pagehide` et reprend sans rattrapage massif ; un `ResizeObserver` redessine sans tick artificiel. Vite package `assets/common/data/runtime.json` vers `common/data/runtime.json` et `assets/game-snake-poc/data/game.json` vers `game/data/game.json`, puis le frontend charge/valide ces deux sondes avant le démarrage. Les diagnostics navigateur passent par un module TypeScript structuré local. `game-snake-poc-wasm` consomme `engine-v1-platform-api` pour une provenance `Web/Wasm/Browser`, complétée par la classe d'appareil et le profil d'entrée réellement observés. Aucun de ces contrats ne fuit dans `game-snake-poc`.
Gate : audits statiques, `cargo check --workspace`, Clippy workspace strict, tests `engine-v1-platform-api` et `game-snake-poc-wasm`, build WASM + `wasm-bindgen`, build Vite, smoke navigateur couvrant assets/provenance/lifecycle/resize/inputs/SimpleBar, puis smoke `game-snake-poc-desktop` pour la non-régression SDL3.
### `0-pre.6` — correction/extraction seulement si prouvée ### `0-pre.6` — correction/extraction seulement si prouvée

View File

@@ -1,5 +1,5 @@
<!-- file: docs/rules/RULES_PROJECT.md --> <!-- file: docs/rules/RULES_PROJECT.md -->
<!-- version: 15 --> <!-- version: 16 -->
# Règles spécifiques games.sasedev # Règles spécifiques games.sasedev
@@ -31,6 +31,7 @@
- **GAME-ASSET-004** — Chaque jeu peut posséder `assets/<game>/` pour ses ressources spécifiques. - **GAME-ASSET-004** — Chaque jeu peut posséder `assets/<game>/` pour ses ressources spécifiques.
- **GAME-ASSET-005** — Le packaging de chaque plateforme assemble les assets communs et spécifiques sans créer de copie source durable dans une crate. - **GAME-ASSET-005** — Le packaging de chaque plateforme assemble les assets communs et spécifiques sans créer de copie source durable dans une crate.
- **GAME-ASSET-006** — Les chemins logiques d'assets doivent éviter les collisions entre espace commun et espace jeu. - **GAME-ASSET-006** — Les chemins logiques d'assets doivent éviter les collisions entre espace commun et espace jeu.
- **GAME-ASSET-007** — Un host Web direct package les assets runtime communs et spécifiques depuis `assets/` vers les namespaces de distribution `common/` et `game/` sans créer de copie source durable sous `Web/`; le smoke doit charger au moins un asset de chaque namespace lorsqu'une tranche déclare l'intégration assets complète.
## Android ## Android
@@ -64,6 +65,7 @@
- **GAME-TRACE-002** — `tracing-subscriber` compose les subscribers applicatifs et de test ; une librairie métier ne configure pas silencieusement le subscriber global. - **GAME-TRACE-002** — `tracing-subscriber` compose les subscribers applicatifs et de test ; une librairie métier ne configure pas silencieusement le subscriber global.
- **GAME-TRACE-003** — `tracing-appender` est utilisé lorsque lécriture non bloquante ou les fichiers de logs deviennent nécessaires ; le guard associé reste vivant pendant toute la durée utile. - **GAME-TRACE-003** — `tracing-appender` est utilisé lorsque lécriture non bloquante ou les fichiers de logs deviennent nécessaires ; le guard associé reste vivant pendant toute la durée utile.
- **GAME-TRACE-004** — La configuration de logging commune réside dans une crate transverse et ne doit pas être dupliquée par jeu. - **GAME-TRACE-004** — La configuration de logging commune réside dans une crate transverse et ne doit pas être dupliquée par jeu.
- **GAME-TRACE-005** — Un host navigateur direct peut émettre ses diagnostics frontend dans la console du navigateur via un module TypeScript structuré dédié ; ce mécanisme ne remplace pas `tracing` dans le code Rust et ne doit pas importer le bridge tracing Tauri lorsqu'aucun host Tauri n'est présent.
- **GAME-PLATFORM-009** — Une adaptation WebAssembly réutilisable est une crate dédiée distincte de la crate Tauri. - **GAME-PLATFORM-009** — Une adaptation WebAssembly réutilisable est une crate dédiée distincte de la crate Tauri.
- **GAME-PLATFORM-010** — Dans une app Tauri, `lib.rs` reste une façade/reexport ; `tauri.rs` assemble Tauri et expose les commandes qui délèguent aux modules propriétaires. - **GAME-PLATFORM-010** — Dans une app Tauri, `lib.rs` reste une façade/reexport ; `tauri.rs` assemble Tauri et expose les commandes qui délèguent aux modules propriétaires.
@@ -87,6 +89,8 @@
- **GAME-PLATFORM-021** — Les dépendances de présentation nécessaires au runtime Web versionné sont déclarées dans le package frontend et intégrées au build Vite ; un CDN externe n'est pas requis pour exécuter le POC local et ne devient une dépendance de distribution qu'après décision explicite. - **GAME-PLATFORM-021** — Les dépendances de présentation nécessaires au runtime Web versionné sont déclarées dans le package frontend et intégrées au build Vite ; un CDN externe n'est pas requis pour exécuter le POC local et ne devient une dépendance de distribution qu'après décision explicite.
- **GAME-PLATFORM-022** — SimpleBar et `resize-observer-polyfill` sont des dépendances de shell Web, pas des dépendances Tauri. Lorsqu'un host conserve header et footer fixes, la zone centrale bornée à la hauteur disponible peut les utiliser pour fournir un scroll interne personnalisé sans déplacer les éléments fixes. - **GAME-PLATFORM-022** — SimpleBar et `resize-observer-polyfill` sont des dépendances de shell Web, pas des dépendances Tauri. Lorsqu'un host conserve header et footer fixes, la zone centrale bornée à la hauteur disponible peut les utiliser pour fournir un scroll interne personnalisé sans déplacer les éléments fixes.
- **GAME-PLATFORM-023** — Lorsqu'un host Web reprend le shell KSP avec SimpleBar, `app-main` borne la hauteur disponible et reste non scrollable ; `data-simplebar` appartient à l'enfant central `app-content app-scrollable h-100`, dont `height` et `max-height` valent `100%`. Le polyfill `ResizeObserver` est installé avant l'initialisation applicative et le package `simplebar` est importé par le point d'entrée TypeScript. - **GAME-PLATFORM-023** — Lorsqu'un host Web reprend le shell KSP avec SimpleBar, `app-main` borne la hauteur disponible et reste non scrollable ; `data-simplebar` appartient à l'enfant central `app-content app-scrollable h-100`, dont `height` et `max-height` valent `100%`. Le polyfill `ResizeObserver` est installé avant l'initialisation applicative et le package `simplebar` est importé par le point d'entrée TypeScript.
- **GAME-PLATFORM-024** — Un host navigateur à simulation fixed-step suspend ses ticks lorsque le document devient caché ou passe en `pagehide`, puis reprend avec une nouvelle origine temporelle sans rattrapage massif au retour ; le resize peut redessiner la scène sans avancer la simulation.
- **GAME-PLATFORM-025** — La provenance d'un host navigateur est construite via `engine-v1-platform-api` avec `PlatformFamily::Web`, `ExecutionModel::Wasm` et `RuntimeHost::Browser`; la classe d'appareil et le profil d'entrée restent des dimensions observées par le host, sans identification matérielle fine.
## Version d'en-tête des fichiers ## Version d'en-tête des fichiers

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/001-VALIDATION_GATES.md --> <!-- file: docs/validation/001-VALIDATION_GATES.md -->
<!-- version: 6 --> <!-- version: 7 -->
# Gates de validation # Gates de validation
@@ -71,4 +71,6 @@ npm install
npm run build npm run build
``` ```
Le smoke direct utilise ensuite `npm run dev` et `http://127.0.0.1:1434/main.html`. Les frontends Tauri restent pilotés par leurs hooks Tauri et ne reprennent pas cette gate manuelle. Le smoke direct utilise ensuite `npm run dev` et `http://127.0.0.1:1434/main.html`. Pour le POC Snake feature-complete, vérifier aussi : assets `common` + `game` chargés avant le statut `Prêt`, provenance `Web/Wasm/Browser` visible et mise à jour par l'input, pause sans rattrapage lors d'un masquage d'onglet, reprise propre, resize sans tick artificiel, SimpleBar, clavier et contrôles pointer/tactiles.
Lorsque `0-pre.5` touche la frontière plateforme du bridge WASM, exécuter également `cargo test -p engine-v1-platform-api`, `cargo test -p game-snake-poc-wasm`, puis un smoke `cargo run -p game-snake-poc-desktop` pour confirmer la non-régression SDL3. Les frontends Tauri restent pilotés par leurs hooks Tauri et ne reprennent pas cette gate manuelle.

View File

@@ -0,0 +1,29 @@
<!-- file: history/0.3.0/0-pre.4.fix.3.md -->
<!-- version: 1 -->
# Historique 0.3.0-0-pre.4.fix.3
## Statut
Correctif SimpleBar validé par l'utilisateur le 2026-09-20. La tranche `0-pre.4` est fermée et `0-pre.5` peut ouvrir l'intégration plateforme complète.
## Validation utilisateur
La sortie fournie sur `0.3.0-0-pre.4.fix.3` confirme au minimum :
```text
npm install: 49 packages audités, 0 vulnérabilité
npm run build: clean
npm run dev: Vite démarré sur 127.0.0.1:1434
cargo tree -p game-snake-poc-wasm --edges normal: dépendances attendues
manual smoke: page et Canvas corrects
manual smoke: comportement SimpleBar conforme au shell KSP-derived
```
Le build Vite production a généré `main.html`, fonts, WASM, CSS et JavaScript sous `../builds/sasedev-games/game-snake-poc-web/dist/`.
Le warning npm non bloquant relatif au script d'installation de `@parcel/watcher@2.6.0` reste présent et n'a pas empêché le build.
## Suite
`0-pre.5` ferme lifecycle/resize, assets, logging et provenance runtime du host Web, puis vérifie la non-régression du runner Snake Desktop SDL3.

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# file: scripts/audit_distribution_layout.py # file: scripts/audit_distribution_layout.py
# version: 5 # version: 6
"""Audit the static distribution layout expected by supported POCs.""" """Audit the static distribution layout expected by supported POCs."""
@@ -37,6 +37,10 @@ REQUIRED_PATHS = (
"Web/game-snake-poc/frontend/sass/_simplebar.scss", "Web/game-snake-poc/frontend/sass/_simplebar.scss",
"Web/game-snake-poc/frontend/sass/main.scss", "Web/game-snake-poc/frontend/sass/main.scss",
"Web/game-snake-poc/frontend/ts/main.ts", "Web/game-snake-poc/frontend/ts/main.ts",
"Web/game-snake-poc/frontend/ts/assets.ts",
"Web/game-snake-poc/frontend/ts/lifecycle.ts",
"Web/game-snake-poc/frontend/ts/logging.ts",
"Web/game-snake-poc/frontend/ts/provenance.ts",
"Android/game-reflex-poc/build.gradle", "Android/game-reflex-poc/build.gradle",
"Android/game-snake-poc/build.gradle", "Android/game-snake-poc/build.gradle",
"scripts/build_android_rust.py", "scripts/build_android_rust.py",
@@ -88,6 +92,50 @@ def audit_snake_web_shell(root: pathlib.Path) -> list[str]:
return violations return violations
def audit_snake_web_integration(root: pathlib.Path) -> list[str]:
"""Validate assets, lifecycle, logging and provenance wiring for the Snake Web POC."""
violations: list[str] = []
package_path = root / "Web/game-snake-poc/package.json"
vite_path = root / "Web/game-snake-poc/vite.config.ts"
main_path = root / "Web/game-snake-poc/frontend/ts/main.ts"
lifecycle_path = root / "Web/game-snake-poc/frontend/ts/lifecycle.ts"
provenance_path = root / "Web/game-snake-poc/frontend/ts/provenance.ts"
wasm_manifest_path = root / "crates/apps/game-snake-poc-wasm/Cargo.toml"
wasm_runtime_path = root / "crates/apps/game-snake-poc-wasm/src/runtime.rs"
if not all(path.exists() for path in (package_path, vite_path, main_path, lifecycle_path, provenance_path, wasm_manifest_path, wasm_runtime_path)):
return violations
package = package_path.read_text(encoding="utf-8")
vite = vite_path.read_text(encoding="utf-8")
main = main_path.read_text(encoding="utf-8")
lifecycle = lifecycle_path.read_text(encoding="utf-8")
provenance = provenance_path.read_text(encoding="utf-8")
wasm_manifest = wasm_manifest_path.read_text(encoding="utf-8")
wasm_runtime = wasm_runtime_path.read_text(encoding="utf-8")
required_fragments = (
(package, '"vite-plugin-static-copy"', "DIST-LAYOUT-007: Snake Web package must declare static asset packaging"),
(vite, 'dest: "common/data"', "DIST-LAYOUT-008: common Web assets must retain the common/data namespace"),
(vite, 'dest: "game/data"', "DIST-LAYOUT-009: game Web assets must retain the game/data namespace"),
(main, 'loadSnakeWebAssets()', "DIST-LAYOUT-010: Snake Web startup must load runtime asset probes"),
(main, 'bindBrowserLifecycle(session, stage)', "DIST-LAYOUT-011: Snake Web startup must bind browser lifecycle"),
(lifecycle, '"visibilitychange"', "DIST-LAYOUT-012: Snake Web lifecycle must observe visibility changes"),
(lifecycle, '"pagehide"', "DIST-LAYOUT-013: Snake Web lifecycle must observe pagehide"),
(lifecycle, 'ResizeObserver', "DIST-LAYOUT-014: Snake Web lifecycle must observe resize"),
(provenance, '"keyboard-mouse"', "DIST-LAYOUT-015: Snake Web provenance must distinguish keyboard/mouse input"),
(provenance, '"touch"', "DIST-LAYOUT-016: Snake Web provenance must distinguish touch input"),
(wasm_manifest, 'engine-v1-platform-api = { path = "../../engines/engine-v1-platform-api" }', "DIST-LAYOUT-017: Snake WASM adapter must consume the platform provenance API"),
(wasm_runtime, 'PlatformFamily::Web', "DIST-LAYOUT-018: Snake WASM provenance must declare Web platform family"),
(wasm_runtime, 'ExecutionModel::Wasm', "DIST-LAYOUT-019: Snake WASM provenance must declare WASM execution"),
(wasm_runtime, 'RuntimeHost::Browser', "DIST-LAYOUT-020: Snake WASM provenance must declare browser host"),
)
for content, fragment, message in required_fragments:
if fragment not in content:
violations.append(message)
return violations
def main() -> int: def main() -> int:
"""Validate that every static distribution boundary exists.""" """Validate that every static distribution boundary exists."""
@@ -105,6 +153,7 @@ def main() -> int:
if (root / relative).exists(): if (root / relative).exists():
forbidden_present.append(relative) forbidden_present.append(relative)
contract_violations.extend(audit_snake_web_shell(root)) contract_violations.extend(audit_snake_web_shell(root))
contract_violations.extend(audit_snake_web_integration(root))
if missing: if missing:
for relative in missing: for relative in missing:
print(f"DIST-LAYOUT-001: missing required path: {relative}", file=sys.stderr) print(f"DIST-LAYOUT-001: missing required path: {relative}", file=sys.stderr)