diff --git a/Cargo.toml b/Cargo.toml index 924d778..4dd1fe8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ # file: Cargo.toml -# version: 74 +# version: 75 [workspace] resolver = "3" @@ -21,7 +21,7 @@ members = [ ] [workspace.package] -version = "0.3.1-0-pre.2.fix.3" +version = "0.3.1-0-pre.3" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/games" diff --git a/README.md b/README.md index 893f194..7569ca4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ - + # games.sasedev @@ -14,6 +14,7 @@ Workspace expérimental puis productif pour des jeux multiplateformes principale - assets hors des crates sous `assets/` ; - `assets/common/` pour les ressources mutualisées et un répertoire par jeu pour les ressources spécifiques ; - frontend Android sous `Android/`, en Java, avec une partie commune et une partie spécifique par jeu ; +- Android de production orienté SDL3 natif/Java/JNI ; Tauri Android reste un POC de référence et non un template de jeu ; - hosts navigateur directs sous `Web/`, avec frontend Vite/TypeScript séparé des adapters Rust/WASM ; - monétisation optionnelle et spécifique à chaque plateforme/distribution ; - runner Desktop natif SDL3 par défaut, avec variante Tauri uniquement si un besoin futur la justifie ; @@ -26,7 +27,7 @@ Workspace expérimental puis productif pour des jeux multiplateformes principale Version stable de référence : `0.3.0`. -Version de développement courante : `0.3.1-0-pre.2.fix.3`. +Version de développement courante : `0.3.1-0-pre.3`. 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. diff --git a/ROADMAP.md b/ROADMAP.md index 601c7a9..28a1540 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,5 +1,5 @@ - + # Roadmap @@ -46,8 +46,8 @@ Le détail historique des prereleases `0.1.0-*` reste dans `deltas/0.1.0/` et `h ## Série 0.3.x — trajectoire actuelle après 0.3.0 -- ( ) `0.3.1` — second host Snake : Tauri Android, en réutilisant la baseline Web/WASM validée et en remplaçant toute orchestration Python du chemin touché. -- ( ) `0.3.2` — Tauri Desktop + Snake et factorisation WebView uniquement lorsque deux consommateurs réels justifient l'extraction. +- ( ) `0.3.1` — terminer Snake Tauri Android comme POC de référence réutilisant la baseline Web/WASM ; la production Android reste orientée SDL3 natif. +- ( ) `0.3.2` — évaluer Tauri Desktop + Snake seulement si la WebView apporte un avantage explicite, notamment une monétisation publicitaire viable ; factoriser uniquement entre consommateurs réellement retenus. - ( ) `0.3.3` — Android SDL natif multi-ABI avec Cargo/Gradle natifs, sans script Python de build. - ( ) `0.3.4` — API de transport realtime + WebSocket/tokio-tungstenite baseline, sans serveur Uroburas Mode 3. - ( ) `0.3.5` — POC WebTransport/QUIC sur le même protocole, avec comparaison mesurée et fallback WebSocket. diff --git a/crates/apps/game-snake-poc-tauri/README.md b/crates/apps/game-snake-poc-tauri/README.md index 177ba93..5c13876 100644 --- a/crates/apps/game-snake-poc-tauri/README.md +++ b/crates/apps/game-snake-poc-tauri/README.md @@ -1,9 +1,9 @@ - + # game-snake-poc-tauri -Host Tauri Android du POC Snake. Cette application réutilise le gameplay Rust de `game-snake-poc` et l'unique adapter WebAssembly `game-snake-poc-wasm` ; elle ne possède aucune règle de jeu. +Host Tauri Android de référence du POC Snake. Cette application réutilise le gameplay Rust de `game-snake-poc` et l'unique adapter WebAssembly `game-snake-poc-wasm` ; elle ne possède aucune règle de jeu. ## Responsabilités @@ -16,7 +16,13 @@ L'organisation Rust suit le modèle des applications Desk KSP : - `src/runtime.rs` possède le petit contrat runtime/provenance ; - `frontend/` contient le host WebView et consomme les bindings générés de `game-snake-poc-wasm`. -Le frontend minimal de `0.3.1-0-pre.2` valide uniquement le démarrage Tauri, le bridge Rust, le tracing et le chargement du même adapter WASM que le Web direct. Canvas, contrôles, assets et lifecycle complet sont prévus dans les tranches suivantes. +À partir de `0.3.1-0-pre.3`, le frontend fournit aussi le Canvas, la boucle fixed-step, les contrôles clavier/touch et le chargement des assets canoniques. Le lifecycle mobile complet et la politique Back restent consolidés dans la tranche suivante. + +## Statut du POC + +Ce host est conservé comme **POC de référence** et non comme template de production Android. La voie de jeu Android privilégiée reste SDL3 natif/Java/JNI, qui couvre déjà les besoins principaux avec une chaîne de build plus directe. + +Tauri Android reste utile pour comparer une WebView/WASM à la voie native et pour tester ultérieurement des besoins spécifiques, par exemple la saisie texte. Une future distribution Tauri Desktop est une décision séparée, conditionnée à un bénéfice produit concret tel qu’une monétisation WebView exploitable. ## Build diff --git a/crates/apps/game-snake-poc-tauri/USAGE.md b/crates/apps/game-snake-poc-tauri/USAGE.md index 9db7321..127745b 100644 --- a/crates/apps/game-snake-poc-tauri/USAGE.md +++ b/crates/apps/game-snake-poc-tauri/USAGE.md @@ -1,5 +1,5 @@ - + # Utilisation — Snake Tauri Android @@ -57,6 +57,10 @@ Lancer ensuite : (cd crates/apps/game-snake-poc-tauri && cargo tauri android dev) ``` -Choisir l'appareil réel ou l'AVD voulu lorsque la CLI le demande. Le frontend doit afficher une provenance `android / tauri-webview / wasm / unknown / unknown` et les événements de démarrage doivent apparaître dans le tracing/logcat. +Choisir l'appareil réel ou l'AVD voulu lorsque la CLI le demande. Le frontend doit afficher le Canvas Snake, les compteurs score/longueur, les contrôles directionnels et les assets `snake / engine-v1`. La provenance doit évoluer depuis `android / tauri-webview / wasm / / unknown` vers un profil d'entrée observé (`touch`, `keyboard-mouse` ou `mixed`) après utilisation des contrôles. Les événements de démarrage doivent apparaître dans le tracing/logcat. Ne pas utiliser `npm run dev` ou `npm run build` comme gate manuelle : ces scripts sont des hooks internes possédés par Tauri. `cargo tauri android build` reste réservé au jalon de packaging prévu par le plan, sauf diagnostic explicite. + +## Positionnement + +`game-snake-poc-tauri` est un POC de référence. Il ne doit pas être utilisé comme base automatique des futurs jeux Android ; la cible Android productive reste SDL3 natif tant qu’un besoin concret ne justifie pas la WebView Tauri. diff --git a/crates/apps/game-snake-poc-tauri/frontend/css/main.css b/crates/apps/game-snake-poc-tauri/frontend/css/main.css index b021e82..de4b785 100644 --- a/crates/apps/game-snake-poc-tauri/frontend/css/main.css +++ b/crates/apps/game-snake-poc-tauri/frontend/css/main.css @@ -1,5 +1,5 @@ /* file: crates/apps/game-snake-poc-tauri/frontend/css/main.css */ -/* version: 1 */ +/* version: 2 */ :root { font-family: system-ui, sans-serif; @@ -14,45 +14,222 @@ html, body { + width: 100%; + min-width: 320px; min-height: 100%; margin: 0; } body { min-height: 100vh; - display: grid; - place-items: center; - padding: max(1.5rem, env(safe-area-inset-top)) max(1.5rem, env(safe-area-inset-right)) max(1.5rem, env(safe-area-inset-bottom)) - max(1.5rem, env(safe-area-inset-left)); + padding: max(0.75rem, env(safe-area-inset-top)) max(0.75rem, env(safe-area-inset-right)) max(0.75rem, env(safe-area-inset-bottom)) + max(0.75rem, env(safe-area-inset-left)); + background: #101418; + overscroll-behavior: none; } -.bootstrap-panel { - width: min(100%, 34rem); - padding: 1.5rem; - border: 1px solid currentColor; +.app-header, +.app-footer, +.game-panel, +.control-panel { + border: 1px solid #50606b; border-radius: 0.75rem; + background: #161c21; +} + +.app-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.75rem 1rem; } .eyebrow { - margin: 0 0 0.5rem; - opacity: 0.7; + margin: 0 0 0.15rem; + color: #9fb0ba; + font-size: 0.75rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +h1, +h2, +p { + margin-top: 0; } h1 { - margin: 0 0 1rem; - font-size: clamp(1.6rem, 7vw, 2.4rem); + margin-bottom: 0; + font-size: clamp(1.25rem, 6vw, 1.85rem); } -#runtime-status { - margin: 0 0 0.75rem; +h2 { + margin-bottom: 0.35rem; + font-size: 1.1rem; } -#runtime-provenance { +.status-badge, +.game-summary output { + display: inline-flex; + align-items: center; + min-height: 2rem; + padding: 0.35rem 0.65rem; + border-radius: 999px; + background: #29343c; + color: #f4f7f8; + font-size: 0.85rem; + white-space: nowrap; +} + +body[data-runtime-state="ready"] .status-badge { + background: #1d5f3d; +} + +body[data-runtime-state="error"] .status-badge { + background: #7a2630; +} + +.app-main { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 0.75rem; + margin-block: 0.75rem; +} + +.game-panel, +.control-panel { + min-width: 0; + padding: 0.75rem; +} + +.game-summary { + display: flex; + justify-content: center; + gap: 0.5rem; + margin-bottom: 0.6rem; +} + +.game-stage { + width: min(100%, 21rem); + max-height: 58vh; + margin-inline: auto; + aspect-ratio: 3 / 5; + overflow: hidden; + border: 1px solid #6d7f8a; + border-radius: 0.45rem; + background: #000; +} + +#game { display: block; - overflow-wrap: anywhere; - font-family: ui-monospace, monospace; + width: 100%; + height: 100%; + touch-action: none; + outline: none; } -body[data-runtime-state="error"] #runtime-status { - font-weight: 700; +#game:focus-visible { + box-shadow: inset 0 0 0 3px #8fbfff; +} + +.control-panel > div:first-child p { + margin-bottom: 0.75rem; + color: #b9c4ca; + font-size: 0.9rem; +} + +.direction-pad { + display: grid; + grid-template-columns: repeat(3, minmax(3.75rem, 5rem)); + grid-template-rows: repeat(2, minmax(3.3rem, 4.2rem)); + gap: 0.45rem; + justify-content: center; + margin-inline: auto; + user-select: none; +} + +.direction-pad button { + border: 1px solid #7892a1; + border-radius: 0.6rem; + background: #202b32; + color: #f4f7f8; + font-size: 1.45rem; + touch-action: manipulation; +} + +.direction-pad button:active { + background: #355365; +} + +.direction-up { + grid-column: 2; + grid-row: 1; +} + +.direction-left { + grid-column: 1; + grid-row: 2; +} + +.direction-down { + grid-column: 2; + grid-row: 2; +} + +.direction-right { + grid-column: 3; + grid-row: 2; +} + +.runtime-details { + margin: 0.9rem 0 0; + padding-top: 0.8rem; + border-top: 1px solid #3a474f; + font-size: 0.78rem; +} + +.runtime-details div { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: 0.75rem; + margin-top: 0.45rem; +} + +.runtime-details dt { + color: #a9b6bd; +} + +.runtime-details dd { + min-width: 0; + margin: 0; + overflow-wrap: anywhere; + text-align: right; +} + +.startup-error { + margin: 0.9rem 0 0; + padding: 0.65rem; + border: 1px solid #b34b57; + border-radius: 0.5rem; + background: #51232a; +} + +.app-footer { + padding: 0.55rem 0.75rem; + color: #a9b6bd; + font-size: 0.75rem; + text-align: center; +} + +@media (min-width: 760px) { + .app-main { + grid-template-columns: minmax(18rem, 1fr) minmax(17rem, 22rem); + align-items: start; + } + + .game-stage { + width: min(100%, 24rem); + max-height: 70vh; + } } diff --git a/crates/apps/game-snake-poc-tauri/frontend/main.html b/crates/apps/game-snake-poc-tauri/frontend/main.html index fb46119..3798a59 100644 --- a/crates/apps/game-snake-poc-tauri/frontend/main.html +++ b/crates/apps/game-snake-poc-tauri/frontend/main.html @@ -1,17 +1,59 @@ + + - Snake POC Tauri + + Snake POC — Tauri Android -
-

games.sasedev

-

Snake — Tauri Android

-

Initialisation du runtime…

- provenance en attente +
+
+

games.sasedev

+

Snake — Tauri Android

+
+ Initialisation… +
+ +
+
+
+ Score : 0 + Longueur : 0 +
+
+ +
+
+ +
+ +
POC de référence Tauri Android — gameplay Rust partagé
diff --git a/crates/apps/game-snake-poc-tauri/frontend/ts/assets.ts b/crates/apps/game-snake-poc-tauri/frontend/ts/assets.ts new file mode 100644 index 0000000..f119d10 --- /dev/null +++ b/crates/apps/game-snake-poc-tauri/frontend/ts/assets.ts @@ -0,0 +1,54 @@ +// file: crates/apps/game-snake-poc-tauri/frontend/ts/assets.ts +// version: 1 + +export interface SnakeTauriAssets { + 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(relativePath: string): Promise { + 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 Tauri 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 Tauri Snake."); + } +} + +export async function loadSnakeTauriAssets(): Promise { + const [commonRuntime, gameRuntime] = await Promise.all([ + fetchJson("./common/data/runtime.json"), + fetchJson("./game/data/game.json"), + ]); + validateCommonRuntime(commonRuntime); + validateGameRuntime(gameRuntime); + return { + engineGeneration: commonRuntime.engine_generation, + game: gameRuntime.game, + gameKind: gameRuntime.kind, + }; +} diff --git a/crates/apps/game-snake-poc-tauri/frontend/ts/game.ts b/crates/apps/game-snake-poc-tauri/frontend/ts/game.ts new file mode 100644 index 0000000..5f13b67 --- /dev/null +++ b/crates/apps/game-snake-poc-tauri/frontend/ts/game.ts @@ -0,0 +1,147 @@ +// file: crates/apps/game-snake-poc-tauri/frontend/ts/game.ts +// version: 1 + +import init, { SnakeWasmGame } from "@snake-wasm"; +import type { RuntimeDescriptor } from "./bridge"; +import { frontendInfo } from "./logging"; +import { TauriRuntimeProvenance, type SnakeInputSource } from "./provenance"; + +const FRAME_MILLIS = 16; +const MAX_FRAME_ELAPSED_MILLIS = 250; + +export type SnakeDirection = "left" | "right" | "up" | "down"; + +export interface SnakeTauriSession { + queueDirection(direction: SnakeDirection, source: SnakeInputSource): void; + renderNow(): void; + refreshDeviceClass(): void; + dispose(): void; +} + +function color(red: number, green: number, blue: number): string { + return `rgb(${red} ${green} ${blue})`; +} + +function resizeCanvas(canvas: HTMLCanvasElement): void { + const ratio = window.devicePixelRatio || 1; + const width = Math.max(1, Math.floor(canvas.clientWidth * ratio)); + const height = Math.max(1, Math.floor(canvas.clientHeight * ratio)); + if (canvas.width !== width || canvas.height !== height) { + canvas.width = width; + canvas.height = height; + } +} + +function render( + game: SnakeWasmGame, + canvas: HTMLCanvasElement, + context: CanvasRenderingContext2D, + score: HTMLOutputElement, + length: HTMLOutputElement, +): void { + resizeCanvas(canvas); + context.fillStyle = color(game.background_red(), game.background_green(), game.background_blue()); + context.fillRect(0, 0, canvas.width, canvas.height); + const count = game.rectangle_count(); + for (let index = 0; index < count; index += 1) { + context.fillStyle = color(game.rectangle_red(index), game.rectangle_green(index), game.rectangle_blue(index)); + context.fillRect( + game.rectangle_x(index) * canvas.width, + game.rectangle_y(index) * canvas.height, + game.rectangle_width(index) * canvas.width, + game.rectangle_height(index) * canvas.height, + ); + } + score.value = `Score : ${game.score()}`; + length.value = `Longueur : ${game.length()}`; +} + +function queueDirection(game: SnakeWasmGame, direction: SnakeDirection): void { + switch (direction) { + case "left": + game.left(); + return; + case "right": + game.right(); + return; + case "up": + game.up(); + return; + case "down": + game.down(); + return; + } +} + +export async function startGame( + canvas: HTMLCanvasElement, + score: HTMLOutputElement, + length: HTMLOutputElement, + provenanceOutput: HTMLOutputElement, + descriptor: RuntimeDescriptor, +): Promise { + await init(); + frontendInfo("Snake Tauri WASM initialized"); + const context = canvas.getContext("2d"); + if (context === null) { + throw new Error("Le contexte Canvas 2D est indisponible."); + } + const renderingContext: CanvasRenderingContext2D = context; + const game = new SnakeWasmGame(); + const provenance = new TauriRuntimeProvenance(game, provenanceOutput, descriptor); + let previous = performance.now(); + let accumulator = 0; + let animationFrame: number | null = null; + let disposed = false; + + function renderNow(): void { + if (disposed) { + return; + } + render(game, canvas, renderingContext, score, length); + } + + function frame(now: number): void { + if (disposed) { + return; + } + const elapsed = Math.min(Math.max(0, now - previous), MAX_FRAME_ELAPSED_MILLIS); + previous = now; + accumulator += elapsed; + while (accumulator >= FRAME_MILLIS) { + game.tick(); + accumulator -= FRAME_MILLIS; + } + renderNow(); + animationFrame = window.requestAnimationFrame(frame); + } + + renderNow(); + animationFrame = window.requestAnimationFrame(frame); + return { + queueDirection(direction: SnakeDirection, source: SnakeInputSource): void { + if (disposed) { + return; + } + provenance.recordInput(source); + queueDirection(game, direction); + }, + renderNow, + refreshDeviceClass(): void { + if (!disposed) { + provenance.refreshDeviceClass(); + } + }, + dispose(): void { + if (disposed) { + return; + } + disposed = true; + if (animationFrame !== null) { + window.cancelAnimationFrame(animationFrame); + animationFrame = null; + } + game.free(); + }, + }; +} diff --git a/crates/apps/game-snake-poc-tauri/frontend/ts/input.ts b/crates/apps/game-snake-poc-tauri/frontend/ts/input.ts new file mode 100644 index 0000000..099a427 --- /dev/null +++ b/crates/apps/game-snake-poc-tauri/frontend/ts/input.ts @@ -0,0 +1,57 @@ +// file: crates/apps/game-snake-poc-tauri/frontend/ts/input.ts +// version: 1 + +import type { SnakeDirection, SnakeTauriSession } from "./game"; + +const KEY_DIRECTIONS: Readonly> = { + arrowleft: "left", + a: "left", + q: "left", + arrowright: "right", + d: "right", + arrowup: "up", + w: "up", + z: "up", + arrowdown: "down", + s: "down", +}; + +function buttonDirection(button: HTMLButtonElement): SnakeDirection | null { + const direction = button.dataset.direction; + if (direction === "left" || direction === "right" || direction === "up" || direction === "down") { + return direction; + } + return null; +} + +export function bindInputs(session: SnakeTauriSession, buttons: readonly HTMLButtonElement[]): void { + window.addEventListener("keydown", event => { + if (event.repeat) { + return; + } + const direction = KEY_DIRECTIONS[event.key.toLowerCase()]; + if (direction === undefined) { + return; + } + event.preventDefault(); + session.queueDirection(direction, "keyboard-mouse"); + }); + + for (const button of buttons) { + const direction = buttonDirection(button); + if (direction === null) { + continue; + } + button.addEventListener("pointerdown", event => { + event.preventDefault(); + const source = event.pointerType === "touch" ? "touch" : "keyboard-mouse"; + session.queueDirection(direction, source); + }); + button.addEventListener("click", event => { + if (event.detail !== 0) { + return; + } + session.queueDirection(direction, "keyboard-mouse"); + }); + } +} diff --git a/crates/apps/game-snake-poc-tauri/frontend/ts/main.ts b/crates/apps/game-snake-poc-tauri/frontend/ts/main.ts index 2322078..3cae07c 100644 --- a/crates/apps/game-snake-poc-tauri/frontend/ts/main.ts +++ b/crates/apps/game-snake-poc-tauri/frontend/ts/main.ts @@ -1,9 +1,11 @@ // file: crates/apps/game-snake-poc-tauri/frontend/ts/main.ts -// version: 1 +// version: 2 -import init, { SnakeWasmGame } from "@snake-wasm"; import "../css/main.css"; +import { loadSnakeTauriAssets } from "./assets"; import { getRuntimeDescriptor, reportFrontendReady } from "./bridge"; +import { startGame } from "./game"; +import { bindInputs } from "./input"; import { frontendError, frontendInfo, initializeFrontendTracing } from "./logging"; function requiredElement(identifier: string): T { @@ -18,39 +20,48 @@ async function bootstrap(): Promise { await initializeFrontendTracing(); frontendInfo("Snake Tauri frontend bootstrap started"); const descriptor = await getRuntimeDescriptor(); - await init(); - const game = new SnakeWasmGame(); - const configured = game.configure_runtime_provenance( - descriptor.platformFamily, - descriptor.runtimeHost, - descriptor.deviceClass, - descriptor.inputProfile, - ); - if (!configured) { - game.free(); - throw new Error("La provenance Tauri Android n'a pas été acceptée par l'adapter Snake WASM."); - } - const status = requiredElement("runtime-status"); + const canvas = requiredElement("game"); + const score = requiredElement("score"); + const length = requiredElement("length"); const provenance = requiredElement("runtime-provenance"); - status.textContent = "Runtime minimal initialisé. Le Canvas et les contrôles arrivent dans 0-pre.3."; - provenance.value = [ - game.provenance_platform_family(), - game.provenance_runtime_host(), - game.provenance_execution_model(), - game.provenance_device_class(), - game.provenance_input_profile(), - ].join(" / "); - window.addEventListener("pagehide", () => game.free(), { once: true }); + const assets = requiredElement("asset-status"); + const status = requiredElement("runtime-status"); + const error = requiredElement("startup-error"); + const buttons = Array.from(document.querySelectorAll("[data-direction]")); + + status.textContent = "Chargement assets…"; + const assetMetadata = await loadSnakeTauriAssets(); + assets.value = `${assetMetadata.gameKind} / engine-v${assetMetadata.engineGeneration}`; + frontendInfo(`Snake Tauri assets ready: ${assetMetadata.game} / ${assets.value}`); + + status.textContent = "Chargement WASM…"; + const session = await startGame(canvas, score, length, provenance, descriptor); + bindInputs(session, buttons); + window.addEventListener("resize", () => { + session.refreshDeviceClass(); + session.renderNow(); + }); + window.addEventListener("pagehide", () => session.dispose(), { once: true }); + + document.body.dataset.runtimeState = "ready"; + status.textContent = "Prêt"; + error.hidden = true; + canvas.focus(); await reportFrontendReady(); frontendInfo(`Snake Tauri frontend ready: ${provenance.value}`); } void bootstrap().catch(reason => { - const message = reason instanceof Error ? reason.message : String(reason); + const message = reason instanceof Error ? reason.stack ?? reason.message : String(reason); document.body.dataset.runtimeState = "error"; const status = document.getElementById("runtime-status"); + const error = document.getElementById("startup-error"); if (status !== null) { - status.textContent = `Échec d'initialisation : ${message}`; + status.textContent = "Erreur"; + } + if (error !== null) { + error.textContent = `Impossible de démarrer Snake : ${message}`; + error.hidden = false; } frontendError(`Snake Tauri frontend bootstrap failed: ${message}`); }); diff --git a/crates/apps/game-snake-poc-tauri/frontend/ts/provenance.ts b/crates/apps/game-snake-poc-tauri/frontend/ts/provenance.ts new file mode 100644 index 0000000..a1dcd0f --- /dev/null +++ b/crates/apps/game-snake-poc-tauri/frontend/ts/provenance.ts @@ -0,0 +1,97 @@ +// file: crates/apps/game-snake-poc-tauri/frontend/ts/provenance.ts +// version: 1 + +import type { SnakeWasmGame } from "@snake-wasm"; +import type { RuntimeDescriptor } from "./bridge"; +import { frontendInfo } 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 TauriRuntimeProvenance { + private keyboardMouseObserved = false; + private touchObserved = false; + private deviceClass: DeviceClass = detectedDeviceClass(); + + public constructor( + private readonly game: SnakeWasmGame, + private readonly output: HTMLOutputElement, + private readonly descriptor: RuntimeDescriptor, + ) { + if (descriptor.executionModel !== "wasm") { + throw new Error(`Modèle d'exécution Tauri inattendu : ${descriptor.executionModel}`); + } + 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.descriptor.platformFamily, this.descriptor.runtimeHost, this.deviceClass, profile)) { + throw new Error("La provenance Tauri Android n'a pas pu être configurée dans le bridge WASM."); + } + this.output.value = displayLabel(this.game); + frontendInfo(`Snake Tauri provenance: ${this.output.value}`); + } +} diff --git a/crates/apps/game-snake-poc-tauri/package.json b/crates/apps/game-snake-poc-tauri/package.json index 85e5282..4a1ea36 100644 --- a/crates/apps/game-snake-poc-tauri/package.json +++ b/crates/apps/game-snake-poc-tauri/package.json @@ -16,6 +16,7 @@ "devDependencies": { "@types/node": "^26.1", "typescript": "^7.0", - "vite": "^8.2" + "vite": "^8.2", + "vite-plugin-static-copy": "^4.1" } } diff --git a/crates/apps/game-snake-poc-tauri/vite.config.ts b/crates/apps/game-snake-poc-tauri/vite.config.ts index 5e2892b..ea53658 100644 --- a/crates/apps/game-snake-poc-tauri/vite.config.ts +++ b/crates/apps/game-snake-poc-tauri/vite.config.ts @@ -1,9 +1,10 @@ // file: crates/apps/game-snake-poc-tauri/vite.config.ts -// version: 1 +// version: 2 import { fileURLToPath } from "node:url"; import { resolve } from "node:path"; import { defineConfig, normalizePath } from "vite"; +import { viteStaticCopy } from "vite-plugin-static-copy"; const appRoot = fileURLToPath(new URL(".", import.meta.url)); const repositoryRoot = normalizePath(resolve(appRoot, "../../..")); @@ -13,9 +14,19 @@ const wasmRoot = normalizePath(resolve(externalBuildRoot, "wasm")); const wasmModule = normalizePath(resolve(wasmRoot, "game_snake_poc_wasm.js")); const frontendDist = normalizePath(resolve(externalBuildRoot, "dist")); 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")); const devHost = process.env.TAURI_DEV_HOST; export default defineConfig({ + plugins: [ + viteStaticCopy({ + targets: [ + { src: commonRuntimeAsset, dest: "common/data", rename: { stripBase: true } }, + { src: gameRuntimeAsset, dest: "game/data", rename: { stripBase: true } }, + ], + }), + ], base: "./", cacheDir: viteCacheDir, clearScreen: false, @@ -60,11 +71,15 @@ export default defineConfig({ fs: { allow: [appRoot, repositoryRoot, externalBuildRoot], }, - ws: { - protocol: "ws", - host: devHost || "localhost", - port: 1437, - }, + ws: devHost + ? { + protocol: "ws", + host: devHost, + port: 1437, + } + : { + clientPort: 1436, + }, watch: { ignored: ["**/src/**"], }, diff --git a/deltas/0.3.1/0-pre.3.md b/deltas/0.3.1/0-pre.3.md new file mode 100644 index 0000000..e0a92b8 --- /dev/null +++ b/deltas/0.3.1/0-pre.3.md @@ -0,0 +1,152 @@ + + + +# Delta 0.3.1-0-pre.3 + +## Base + +Base déclarée : `0.3.1-0-pre.2.fix.3`, validée par l'utilisateur le 2026-09-20. + +## Objet + +Cette tranche transforme le host Tauri Android minimal en POC Snake visuellement jouable : Canvas, boucle de simulation fixed-step, contrôles clavier/touch, assets canoniques et shell responsive. + +Elle enregistre aussi la décision issue du smoke précédent : Tauri Android est conservé comme POC de référence, mais la voie Android de production reste SDL3 natif tant qu'un besoin fonctionnel explicite ne justifie pas la WebView. + +## Frontend Tauri Snake + +Le host ajoute des modules propriétaires séparés : + +```text +frontend/ts/assets.ts validation des assets common/game +frontend/ts/game.ts projection Canvas + fixed-step WASM +frontend/ts/input.ts clavier + pointer/touch +frontend/ts/provenance.ts device/input observés pour le host Tauri +``` + +`main.ts` reste l'orchestrateur frontend et ne réimplémente aucune règle Snake. Le gameplay continue de résider dans `game-snake-poc` et le même `game-snake-poc-wasm` reste consommé par le navigateur direct et la WebView Tauri. + +Le shell reste volontairement léger : CSS local, aucun Bootstrap/SimpleBar/ResizeObserver ajouté. Le Canvas conserve le ratio logique `3 / 5` du Snake et les boutons directionnels sont utilisables au tactile/pointer. + +## Assets + +`vite-plugin-static-copy` est ajouté au package Tauri pour exposer exactement les deux assets canoniques : + +```text +assets/common/data/runtime.json -> common/data/runtime.json +assets/game-snake-poc/data/game.json -> game/data/game.json +``` + +Les assets restent hors des crates Rust et aucun duplicat runtime n'est créé sous `frontend/` ou `gen/android`. + +## Provenance + +Le backend Rust fournit toujours les dimensions statiques `Android / TauriWebView / Wasm`. Le frontend observe désormais la classe de device et les sources d'input pour configurer l'adapter partagé avec `phone/tablet/desktop/unknown` et `touch/keyboard-mouse/mixed/unknown`. + +La stabilisation lifecycle/background/foreground appartient encore à `0-pre.4`. + +## Vite dev + +La configuration WebSocket garde `server.ws`, mécanisme Vite 8 courant. Sur l'émulateur sans `TAURI_DEV_HOST`, le client HMR utilise le même port `1436` que le serveur dev/forwarding Tauri ; avec un host réseau explicite, le port HMR séparé `1437` reste utilisé. + +Cette correction vise le warning HMR observé au smoke précédent mais n'est pas un critère fonctionnel du gameplay. + +## Positionnement Tauri après le POC minimal + +La documentation durable et la roadmap sont réconciliées : + +- Tauri Android reste un POC de référence ; +- SDL3 natif reste la cible Android productive par défaut ; +- les futurs jeux ne reçoivent pas automatiquement une app Tauri Android ; +- la saisie texte/WebView reste une hypothèse à comparer aux capacités SDL réelles ; +- Tauri Desktop devient une évaluation conditionnelle, notamment selon la viabilité d'une monétisation publicitaire WebView. + +Aucune extraction Web/Tauri commune n'est créée dans cette tranche. Le statut de POC de référence réduit au contraire la valeur d'une abstraction qui n'aurait pas au moins deux consommateurs durablement retenus. + +## Historique créé + +Conformément à `DOC-HIST-002/003`, la tranche crée : + +```text +history/0.3.1/0-pre.2.fix.3.md +``` + +qui enregistre uniquement les validations réellement fournies pour le jalon précédent. + +## Fichiers principaux modifiés + +- `Cargo.toml` ; +- `README.md` ; +- `ROADMAP.md` ; +- `docs/architecture/015-PLATFORM_POC_ARCHITECTURE.md` ; +- `docs/plans/002-V0_3_1_TAURI_ANDROID_SNAKE_PLAN.md` ; +- `crates/apps/game-snake-poc-tauri/README.md` ; +- `crates/apps/game-snake-poc-tauri/USAGE.md` ; +- `crates/apps/game-snake-poc-tauri/frontend/main.html` ; +- `crates/apps/game-snake-poc-tauri/frontend/css/main.css` ; +- `crates/apps/game-snake-poc-tauri/frontend/ts/main.ts` ; +- `crates/apps/game-snake-poc-tauri/package.json` ; +- `crates/apps/game-snake-poc-tauri/vite.config.ts` ; +- `scripts/audit_distribution_layout.py`. + +## Fichiers ajoutés + +- `crates/apps/game-snake-poc-tauri/frontend/ts/assets.ts` ; +- `crates/apps/game-snake-poc-tauri/frontend/ts/game.ts` ; +- `crates/apps/game-snake-poc-tauri/frontend/ts/input.ts` ; +- `crates/apps/game-snake-poc-tauri/frontend/ts/provenance.ts` ; +- `history/0.3.1/0-pre.2.fix.3.md` ; +- `deltas/0.3.1/0-pre.3.md`. + +## Gate utilisateur + +Conserver JDK 17 pour les commandes Tauri Android : + +```bash +export JAVA_HOME=/usr/lib/jvm/temurin-17-jdk-amd64 +"$JAVA_HOME/bin/java" -version +``` + +Le package Tauri possède une nouvelle dépendance frontend ; mettre à jour ses dépendances sans lancer manuellement son build : + +```bash +(cd crates/apps/game-snake-poc-tauri && npm install) +``` + +Puis exécuter : + +```bash +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 fmt --all +cargo fmt --all -- --check +cargo check --workspace +cargo clippy --workspace --all-targets --all-features -- -D warnings +cargo test -p game-snake-poc-wasm --all-targets --all-features +``` + +Le smoke frontend reste possédé par Tauri : + +```bash +(cd crates/apps/game-snake-poc-tauri && cargo tauri android dev) +``` + +Ne pas exécuter `npm run dev` ou `npm run build` manuellement. + +## Critère d'acceptation + +Sur l'AVD Android : + +- le Canvas Snake est visible avec un ratio `3 / 5` ; +- le Snake avance automatiquement ; +- score et longueur sont visibles ; +- les quatre boutons directionnels répondent au pointer/tactile ; +- le clavier fonctionne lorsqu'il est disponible ; +- `Assets` affiche `snake / engine-v1` ; +- la provenance est `android / tauri-webview / wasm / / ` et évolue après une entrée ; +- aucune règle gameplay n'est dupliquée dans TypeScript ; +- aucun fichier sous `gen/android` n'est modifié par le projet. + +Le background/foreground, la reprise sans catch-up, la rotation consolidée et Back restent les objectifs explicites de `0-pre.4`. diff --git a/docs/architecture/015-PLATFORM_POC_ARCHITECTURE.md b/docs/architecture/015-PLATFORM_POC_ARCHITECTURE.md index 489f1bc..4758906 100644 --- a/docs/architecture/015-PLATFORM_POC_ARCHITECTURE.md +++ b/docs/architecture/015-PLATFORM_POC_ARCHITECTURE.md @@ -1,5 +1,5 @@ - + # Architecture des POC plateforme et réseau @@ -14,12 +14,20 @@ Snake est le jeu-sonde principal. Première vague, réordonnée à partir du résultat réel de `0.3.0` : - Web navigateur direct + Snake — validé par `0.3.0` ; -- Tauri Android + Snake — prochain host prévu par `0.3.1` ; -- Tauri Desktop + Snake — prévu ensuite pour éprouver la factorisation WebView ; +- Tauri Android + Snake — POC de référence terminé par `0.3.1`, conservé pour comparaison mais non retenu comme voie de production Android par défaut ; +- Tauri Desktop + Snake — évaluation conditionnelle seulement si la WebView apporte un bénéfice produit explicite, notamment une voie de monétisation publicitaire viable ; - build Android multi-ABI avec outils natifs — tranche dédiée après les POC WebView. Plateformes supplémentaires lorsque l'environnement existe : Windows SDL natif, macOS SDL natif et iOS SDL natif. +## Positionnement Tauri Android après `0.3.1` + +Le host Tauri Android reste pertinent comme preuve de portabilité du gameplay Rust/WASM et comme banc d’essai WebView. Il n’est pas retenu comme distribution Android de jeu par défaut. + +La baseline Android productive reste SDL3 natif avec wrapper Java/JNI : elle fournit déjà le rendu et les contrôles nécessaires avec moins de couches d’outillage. Tauri Android ne doit être rouvert pour un produit que lorsqu’un besoin concret apporte une valeur supérieure à ce coût, par exemple si les tests futurs démontrent un avantage net de la WebView pour une saisie texte ou une capacité UI difficile à obtenir proprement via SDL. + +Cette décision n’interdit pas Tauri Desktop. Le Desktop reste une distribution distincte et optionnelle ; sa valeur doit être démontrée séparément, notamment au regard des possibilités de monétisation dans une WebView Desktop. + ## POC réseau Avant le Mode 3, comparer au minimum WebSocket/tokio-tungstenite, WebTransport/QUIC, fallback automatique, charge, reconnect/resync, backpressure, snapshots/deltas et mobilité réseau. @@ -38,7 +46,7 @@ Un POC sert à identifier duplication, adapters manquants, capability réellemen Une abstraction n'est pas extraite avant observation d'un besoin concret. -Le POC Web direct `0.3.0` n'a pas justifié d'extraire prématurément un adapter WASM commun entre Reflex et Snake : les deux adapters spécifiques restent acceptés. Le second host Snake doit d'abord fournir un second besoin réel avant toute factorisation. +Le POC Web direct `0.3.0` n'a pas justifié d'extraire prématurément un adapter WASM commun entre Reflex et Snake : les deux adapters spécifiques restent acceptés. Le second host Snake fournit un second consommateur technique, mais son statut de POC Tauri Android de référence réduit encore l'intérêt d'une factorisation durable : une extraction n'est retenue que si elle reste utile à au moins deux hosts réellement conservés. ## Relation avec Uroburas diff --git a/docs/plans/002-V0_3_1_TAURI_ANDROID_SNAKE_PLAN.md b/docs/plans/002-V0_3_1_TAURI_ANDROID_SNAKE_PLAN.md index e0a490a..5e3fda5 100644 --- a/docs/plans/002-V0_3_1_TAURI_ANDROID_SNAKE_PLAN.md +++ b/docs/plans/002-V0_3_1_TAURI_ANDROID_SNAKE_PLAN.md @@ -1,11 +1,11 @@ - + # Plan v0.3.1 — second host Snake Tauri Android ## But de la version -`0.3.1` doit ajouter un second host Snake réellement exécutable sous Tauri Android en réutilisant la baseline Web/WASM validée par `0.3.0`. +`0.3.1` doit terminer un second host Snake réellement exécutable sous Tauri Android en réutilisant la baseline Web/WASM validée par `0.3.0`, puis conserver ce host comme POC de référence plutôt que comme voie de production des jeux Android. La version ne doit ni déplacer le gameplay hors de `game-snake-poc`, ni créer un second adapter Snake, ni transformer immédiatement les similitudes Web/Tauri en framework générique. @@ -16,7 +16,7 @@ Cadrage : [`../studies/024-V0_3_1_TAURI_ANDROID_SNAKE_AUDIT.md`](../studies/024- ## Décisions acquises - la nouvelle crate sera `crates/apps/game-snake-poc-tauri` ; -- `0.3.1` valide uniquement sa cible Android ; +- `0.3.1` valide uniquement sa cible Android et en fait un POC de référence, pas une distribution jeu Android de production ; - `game-snake-poc` reste inchangé sauf défaut réellement découvert ; - `game-snake-poc-wasm` reste l'unique adapter Snake WASM et recevra seulement la généralisation de provenance nécessaire au second host ; - le frontend Tauri part du comportement réellement validé dans `Web/game-snake-poc`, avec adaptation de lifecycle et logging ; @@ -26,7 +26,10 @@ Cadrage : [`../studies/024-V0_3_1_TAURI_ANDROID_SNAKE_AUDIT.md`](../studies/024- - aucun nouveau chemin de build n'est piloté par Python ; - `README.md` et `USAGE.md` seront créés avec l'app : architecture pour le premier, prérequis/commandes/smokes pour le second ; - `cargo tauri android init/dev/build` sont les commandes mobiles de référence ; -- SimpleBar et `resize-observer-polyfill` ne sont pas ajoutés au host Tauri sans besoin démontré. +- SimpleBar et `resize-observer-polyfill` ne sont pas ajoutés au host Tauri sans besoin démontré ; +- la voie Android de production reste le host natif SDL3/Java/JNI tant qu’un besoin fonctionnel démontré ne justifie pas Tauri ; +- Tauri Android peut rester utile comme référence comparative, notamment pour évaluer plus tard les saisies texte/WebView, mais ce besoin n’est pas considéré comme acquis ; +- une future variante Tauri Desktop reste conditionnée à un avantage explicite, notamment une monétisation WebView/publicitaire techniquement et contractuellement viable. ## Scope @@ -45,7 +48,22 @@ La version couvre : - build/package et smoke sur AVD/appareil disponible aux jalons prévus ; - comparaison du code Web direct/Tauri après fonctionnement réel afin de décider s'il existe une extraction commune justifiée. -Hors scope : Tauri Desktop Snake comme cible validée, iOS, Android SDL3 multi-ABI additionnel, réseau realtime, Uroburas, ads, billing, auth et leaderboard. +Hors scope : Tauri Desktop Snake comme cible validée, iOS, Android SDL3 multi-ABI additionnel, réseau realtime, Uroburas, intégration ads effective, billing, auth et leaderboard. + +## Positionnement produit après validation du host minimal + +Le smoke `0-pre.2.fix.3` a confirmé que Tauri Android fonctionne techniquement, mais aussi que son outillage ajoute une couche de complexité propre au host : combinaison JDK/Gradle imposée par la version Tauri, projet Android généré et chaîne WebView/WASM supplémentaire alors que le POC Android natif SDL3 fonctionne déjà. + +Décision de `0-pre.3` : + +- terminer `0.3.1` afin d’obtenir un POC Tauri Android jouable et documenté ; +- conserver `game-snake-poc-tauri` comme POC de référence et banc de comparaison ; +- ne pas utiliser Tauri Android comme template de nouvelles applications de jeu ; +- privilégier SDL3 natif pour Android ; +- ne rouvrir la question Tauri Android que pour un besoin concret que SDL3 ne couvre pas proprement, par exemple une saisie texte complexe après test réel des possibilités SDL ; +- évaluer séparément Tauri Desktop seulement si la WebView apporte un avantage produit mesurable, notamment une voie publicitaire exploitable. + +Cette décision ne réduit pas le critère de qualité de `0.3.1` : le POC de référence doit rester jouable, reproductible et suffisamment complet pour permettre une comparaison technique utile. ## Prévision souple @@ -103,11 +121,11 @@ Stabiliser : - sorties applicatives conformes à `EngineGame::quit_requested` sans détourner le Back système Android ; - smokes AVD et, si disponible, appareil réel. -Cette tranche doit rendre le second host fonctionnel de bout en bout. +Cette tranche doit rendre le second host fonctionnel de bout en bout et suffisamment stable pour être conservé comme POC de référence. ### `0-pre.5` — extraction conditionnelle -Créer uniquement si l'état `0-pre.4` montre une duplication réellement durable entre `Web/game-snake-poc` et le frontend Tauri. +Créer uniquement si l'état `0-pre.4` montre une duplication réellement durable entre `Web/game-snake-poc` et le frontend Tauri **et** si l'extraction garde une valeur au-delà du seul POC Android de référence. Une extraction possible doit rester bornée à une responsabilité clairement partagée, par exemple projection/rendu Canvas, mapping input Web ou helpers d'assets. Lifecycle et logging restent host-specific s'ils divergent réellement. @@ -124,7 +142,8 @@ Prévoir : - suite workspace complète à ce jalon planifié ; - build WASM release et bindings ; - `cargo tauri android build` ; -- APK/AAB selon le livrable de validation ; +- APK de validation du POC de référence ; +- AAB uniquement si une valeur de validation supplémentaire est démontrée, pas par automatisme de distribution ; - install/smoke AVD ; - install/smoke appareil ARM64 réel si disponible ; - vérification que les outputs restent hors racine du dépôt lorsqu'ils sont configurables par le projet. @@ -168,7 +187,7 @@ La gate `0-pre.1` validée le 2026-09-20 a confirmé Rust `1.94.1`, Tauri CLI `2 La version peut entrer en RC lorsque : -- Snake est jouable dans le WebView Tauri Android sans gameplay dupliqué ; +- Snake est jouable dans le WebView Tauri Android sans gameplay dupliqué et le host est explicitement positionné comme POC de référence ; - le même adapter WASM sert Web direct et Tauri Android avec provenance exacte ; - lifecycle/input/assets/tracing/Back ont un comportement explicite et validé ; - le chemin de build ne dépend d'aucun orchestrateur Python ; @@ -179,4 +198,4 @@ La version peut entrer en RC lorsque : ## Version suivante -`0.3.2` reste réservée à Tauri Desktop + Snake et à une éventuelle factorisation WebView seulement après deux consommateurs réels. +`0.3.2` devient une évaluation conditionnelle de Tauri Desktop + Snake. Elle n’est ouverte que si la WebView Desktop apporte un bénéfice explicite, notamment une piste de monétisation publicitaire viable ; la factorisation WebView reste interdite sans deux consommateurs réellement retenus. diff --git a/history/0.3.1/0-pre.2.fix.3.md b/history/0.3.1/0-pre.2.fix.3.md new file mode 100644 index 0000000..74bc5e4 --- /dev/null +++ b/history/0.3.1/0-pre.2.fix.3.md @@ -0,0 +1,64 @@ + + + +# Historique 0.3.1-0-pre.2.fix.3 + +## Statut + +Correctif validé par l'utilisateur le 2026-09-20. Le host Tauri Android minimal est assemblé, installé et lancé sur l'AVD x86_64 avec le template Android officiel Tauri 2.11.x sous JDK 17. + +Le frontend atteint son état `ready` avec la provenance `Android / TauriWebView / Wasm`. L'écran reste volontairement minimal à ce jalon : Canvas, contrôles et assets appartiennent à `0-pre.3`. + +## Toolchain confirmée + +La machine possède notamment Temurin 17, 21 et 25. La gate Android a sélectionné explicitement : + +```text +JAVA_HOME=/usr/lib/jvm/temurin-17-jdk-amd64 +OpenJDK 17.0.20.1 +NDK sélectionné par Tauri : 30.0.14904198 +Gradle wrapper généré : 8.14.3 +``` + +Le projet `gen/android` pollué par la tentative Gradle 9 de `0-pre.2.fix.1` a été supprimé puis régénéré uniquement avec `cargo tauri android init`. L'audit de distribution l'a accepté. + +## Gate workspace + +Les validations suivantes ont passé : + +```text +General Rust rule audit: clean +Rust export completeness audit: 0 candidate(s) +games.sasedev workspace audit: clean +Markdown table audit: clean (5 table(s), 217 file(s)) +Distribution layout audit: clean (42 required path(s), 1 forbidden path(s) absent) +cargo fmt --all: clean +cargo fmt --all -- --check: clean +cargo check --workspace: clean +cargo clippy --workspace --all-targets --all-features -- -D warnings: clean +cargo test -p game-snake-poc-wasm --all-targets --all-features: 5 passed, 0 failed +``` + +## Smoke Tauri Android + +`cargo tauri android dev` a : + +- construit l'adapter Snake WASM et le frontend Vite ; +- construit le `cdylib` Android x86_64 ; +- assemblé l'APK avec Gradle 8.14.3 ; +- installé l'application par streamed install ; +- lancé `com.sasedev.games.snake.tauri/.MainActivity` ; +- démarré le runtime Rust avec `platform_family="android"`, `runtime_host="tauri-webview"`, `execution_model="wasm"` ; +- atteint `Snake Tauri frontend ready`. + +Les warnings Gradle/Kotlin/Tauri observés sont des dépréciations upstream non bloquantes. + +Vite a aussi journalisé un échec de connexion WebSocket HMR. Ce défaut n'empêche ni le chargement du frontend ni l'état `ready` et n'est pas bloquant pour la fermeture du host minimal ; la configuration dev peut être ajustée dans la tranche frontend suivante. + +## Décision produit issue du smoke + +Le POC démontre la faisabilité de Tauri Android, mais aussi un coût d'outillage supplémentaire par rapport au host Android natif SDL3 déjà fonctionnel. La suite de `0.3.1` termine donc un POC jouable de référence, sans faire de Tauri Android la voie de production des futurs jeux. + +## Suite + +`0.3.1-0-pre.3` ajoute Canvas, boucle fixed-step, contrôles clavier/touch, assets et shell responsive afin de rendre le POC réellement comparable au host Web direct et au natif SDL3. diff --git a/scripts/audit_distribution_layout.py b/scripts/audit_distribution_layout.py index 1917644..bf622cf 100644 --- a/scripts/audit_distribution_layout.py +++ b/scripts/audit_distribution_layout.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # file: scripts/audit_distribution_layout.py -# version: 11 +# version: 12 """Audit the static distribution layout expected by supported POCs.""" @@ -40,7 +40,12 @@ REQUIRED_PATHS = ( "crates/apps/game-snake-poc-tauri/capabilities/default.json", "crates/apps/game-snake-poc-tauri/icons/icon.png", "crates/apps/game-snake-poc-tauri/frontend/main.html", + "crates/apps/game-snake-poc-tauri/frontend/css/main.css", + "crates/apps/game-snake-poc-tauri/frontend/ts/assets.ts", + "crates/apps/game-snake-poc-tauri/frontend/ts/game.ts", + "crates/apps/game-snake-poc-tauri/frontend/ts/input.ts", "crates/apps/game-snake-poc-tauri/frontend/ts/main.ts", + "crates/apps/game-snake-poc-tauri/frontend/ts/provenance.ts", "Web/game-snake-poc/package.json", "Web/game-snake-poc/tsconfig.json", "Web/game-snake-poc/vite.config.ts", @@ -148,18 +153,40 @@ def audit_snake_web_integration(root: pathlib.Path) -> list[str]: return violations def audit_snake_tauri_android_integration(root: pathlib.Path) -> list[str]: - """Validate the minimal Snake Tauri Android host boundary.""" + """Validate the playable Snake Tauri Android reference POC boundary.""" violations: list[str] = [] - cargo_path = root / "crates/apps/game-snake-poc-tauri/Cargo.toml" - package_path = root / "crates/apps/game-snake-poc-tauri/package.json" - tauri_path = root / "crates/apps/game-snake-poc-tauri/tauri.conf.json" - tauri_rust_path = root / "crates/apps/game-snake-poc-tauri/src/tauri.rs" - build_script_path = root / "crates/apps/game-snake-poc-tauri/build.rs" - frontend_path = root / "crates/apps/game-snake-poc-tauri/frontend/ts/main.ts" - logging_path = root / "crates/apps/game-snake-poc-tauri/frontend/ts/logging.ts" + app_root = root / "crates/apps/game-snake-poc-tauri" + cargo_path = app_root / "Cargo.toml" + package_path = app_root / "package.json" + tauri_path = app_root / "tauri.conf.json" + tauri_rust_path = app_root / "src/tauri.rs" + build_script_path = app_root / "build.rs" + html_path = app_root / "frontend/main.html" + main_path = app_root / "frontend/ts/main.ts" + assets_path = app_root / "frontend/ts/assets.ts" + game_path = app_root / "frontend/ts/game.ts" + input_path = app_root / "frontend/ts/input.ts" + provenance_path = app_root / "frontend/ts/provenance.ts" + logging_path = app_root / "frontend/ts/logging.ts" + vite_path = app_root / "vite.config.ts" wasm_runtime_path = root / "crates/apps/game-snake-poc-wasm/src/runtime.rs" - paths = (cargo_path, package_path, tauri_path, tauri_rust_path, build_script_path, frontend_path, logging_path, wasm_runtime_path) + paths = ( + cargo_path, + package_path, + tauri_path, + tauri_rust_path, + build_script_path, + html_path, + main_path, + assets_path, + game_path, + input_path, + provenance_path, + logging_path, + vite_path, + wasm_runtime_path, + ) if not all(path.exists() for path in paths): return violations @@ -168,8 +195,14 @@ def audit_snake_tauri_android_integration(root: pathlib.Path) -> list[str]: tauri = tauri_path.read_text(encoding="utf-8") tauri_rust = tauri_rust_path.read_text(encoding="utf-8") build_script = build_script_path.read_text(encoding="utf-8") - frontend = frontend_path.read_text(encoding="utf-8") + html = html_path.read_text(encoding="utf-8") + main = main_path.read_text(encoding="utf-8") + assets = assets_path.read_text(encoding="utf-8") + game = game_path.read_text(encoding="utf-8") + input_module = input_path.read_text(encoding="utf-8") + provenance = provenance_path.read_text(encoding="utf-8") logging = logging_path.read_text(encoding="utf-8") + vite = vite_path.read_text(encoding="utf-8") wasm_runtime = wasm_runtime_path.read_text(encoding="utf-8") required_fragments = ( (cargo, 'game-logging-lib = { path = "../../common/game-logging-lib" }', "DIST-LAYOUT-021: Snake Tauri host must use shared Rust logging"), @@ -178,12 +211,27 @@ def audit_snake_tauri_android_integration(root: pathlib.Path) -> list[str]: (package, 'wasm-bindgen ../../../../builds/sasedev-games/', "DIST-LAYOUT-024: Snake Tauri bindings must be generated outside the repository"), (package, '"dev": "npm run wasm:dev && vite"', "DIST-LAYOUT-036: Snake Tauri dev hook must leave Android generation to Tauri"), (package, '"build": "npm run wasm:build && tsc && vite build"', "DIST-LAYOUT-037: Snake Tauri build hook must leave Android generation to Tauri"), + (package, '"vite-plugin-static-copy"', "DIST-LAYOUT-043: Snake Tauri package must declare static asset packaging"), (build_script, "tauri_build::build();", "DIST-LAYOUT-038: Snake Tauri build script must delegate Android integration to tauri-build"), (tauri, '"script": "npm run dev"', "DIST-LAYOUT-025: Tauri must own the frontend development hook"), (tauri, '"script": "npm run build"', "DIST-LAYOUT-026: Tauri must own the frontend production hook"), (tauri_rust, '#[tauri::mobile_entry_point]', "DIST-LAYOUT-027: Snake Tauri run entry point must support Tauri mobile"), (tauri_rust, 'tauri_plugin_tracing::Builder::new()', "DIST-LAYOUT-028: Snake Tauri runtime must install the tracing plugin"), - (frontend, 'configure_runtime_provenance(', "DIST-LAYOUT-029: Snake Tauri frontend must configure provenance through the shared adapter"), + (html, ' Some(engine_v1_platform_api::RuntimeHost::TauriWebView)', "DIST-LAYOUT-031: Snake WASM adapter must accept the Tauri WebView host"), (wasm_runtime, '"android" => Some(engine_v1_platform_api::PlatformFamily::Android)', "DIST-LAYOUT-032: Snake WASM adapter must accept Android provenance"), @@ -196,7 +244,7 @@ def audit_snake_tauri_android_integration(root: pathlib.Path) -> list[str]: (package, "android:prepare", "DIST-LAYOUT-039: Snake Tauri build hooks must not patch generated Android Gradle files"), (build_script, "gen/android", "DIST-LAYOUT-040: Snake Tauri build script must not mutate generated Android Gradle files"), (tauri, "python", "DIST-LAYOUT-034: Snake Tauri configuration must not use Python build orchestration"), - (frontend, "console.", "DIST-LAYOUT-035: Snake Tauri application frontend must not log directly through console.*"), + (main, "console.", "DIST-LAYOUT-035: Snake Tauri application frontend must not log directly through console.*"), ) for content, fragment, message in forbidden_fragments: if fragment in content.lower():