0.3.0-0-pre.5
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# file: Cargo.toml
|
||||
# version: 66
|
||||
# version: 67
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
@@ -20,7 +20,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.3.0-0-pre.4.fix.3"
|
||||
version = "0.3.0-0-pre.5"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/games"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: README.md -->
|
||||
<!-- version: 34 -->
|
||||
<!-- version: 35 -->
|
||||
|
||||
# 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 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.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: Web/game-snake-poc/frontend/main.html -->
|
||||
<!-- version: 3 -->
|
||||
<!-- version: 4 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
@@ -81,6 +81,16 @@
|
||||
<i class="fa-solid fa-arrow-right" aria-hidden="true"></i>
|
||||
</button>
|
||||
</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">
|
||||
Les entrées ne font pas avancer la simulation : elles sont consommées au prochain <code>tick()</code> WASM.
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: Web/game-snake-poc/frontend/sass/_app.scss
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
$app-header-height: 72px;
|
||||
$app-footer-height: 42px;
|
||||
@@ -101,6 +101,11 @@ body {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.runtime-details dd {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.direction-pad {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(64px, 88px));
|
||||
|
||||
54
Web/game-snake-poc/frontend/ts/assets.ts
Normal file
54
Web/game-snake-poc/frontend/ts/assets.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
// file: Web/game-snake-poc/frontend/ts/game.ts
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
import init, { SnakeWasmGame } from "@snake-wasm";
|
||||
import { webDebug, webInfo } from "./logging";
|
||||
import { BrowserRuntimeProvenance, type SnakeInputSource } from "./provenance";
|
||||
|
||||
const FRAME_MILLIS = 16;
|
||||
const MAX_FRAME_ELAPSED_MILLIS = 250;
|
||||
@@ -9,7 +11,12 @@ const MAX_FRAME_ELAPSED_MILLIS = 250;
|
||||
export type SnakeDirection = "left" | "right" | "up" | "down";
|
||||
|
||||
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 {
|
||||
@@ -71,18 +78,42 @@ export async function startGame(
|
||||
canvas: HTMLCanvasElement,
|
||||
score: HTMLOutputElement,
|
||||
length: HTMLOutputElement,
|
||||
provenanceOutput: HTMLOutputElement,
|
||||
): Promise<SnakeWebSession> {
|
||||
await init();
|
||||
webInfo("snake-web-runtime", "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 BrowserRuntimeProvenance(game, provenanceOutput);
|
||||
let previous = performance.now();
|
||||
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 {
|
||||
animationFrame = null;
|
||||
if (!running || disposed) {
|
||||
return;
|
||||
}
|
||||
const elapsed = Math.min(Math.max(0, now - previous), MAX_FRAME_ELAPSED_MILLIS);
|
||||
previous = now;
|
||||
accumulator += elapsed;
|
||||
@@ -90,15 +121,59 @@ export async function startGame(
|
||||
game.tick();
|
||||
accumulator -= FRAME_MILLIS;
|
||||
}
|
||||
render(game, canvas, renderingContext, score, length);
|
||||
window.requestAnimationFrame(frame);
|
||||
renderNow();
|
||||
schedule();
|
||||
}
|
||||
|
||||
render(game, canvas, renderingContext, score, length);
|
||||
window.requestAnimationFrame(frame);
|
||||
function pause(): void {
|
||||
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 {
|
||||
queueDirection(direction: SnakeDirection): void {
|
||||
queueDirection(direction: SnakeDirection, source: SnakeInputSource): void {
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
provenance.recordInput(source);
|
||||
queueDirection(game, direction);
|
||||
},
|
||||
pause,
|
||||
resume,
|
||||
renderNow,
|
||||
refreshDeviceClass(): void {
|
||||
if (!disposed) {
|
||||
provenance.refreshDeviceClass();
|
||||
}
|
||||
},
|
||||
dispose(): void {
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
pause();
|
||||
disposed = true;
|
||||
game.free();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// file: Web/game-snake-poc/frontend/ts/input.ts
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
import type { SnakeDirection, SnakeWebSession } from "./game";
|
||||
import { webDebug } from "./logging";
|
||||
|
||||
const KEY_DIRECTIONS: Readonly<Record<string, SnakeDirection>> = {
|
||||
arrowleft: "left",
|
||||
@@ -34,7 +35,8 @@ export function bindInputs(session: SnakeWebSession, buttons: readonly HTMLButto
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
session.queueDirection(direction);
|
||||
session.queueDirection(direction, "keyboard-mouse");
|
||||
webDebug("snake-web-input", "direction", { direction, source: "keyboard-mouse" });
|
||||
});
|
||||
|
||||
for (const button of buttons) {
|
||||
@@ -44,13 +46,16 @@ export function bindInputs(session: SnakeWebSession, buttons: readonly HTMLButto
|
||||
}
|
||||
button.addEventListener("pointerdown", event => {
|
||||
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 => {
|
||||
if (event.detail !== 0) {
|
||||
return;
|
||||
}
|
||||
session.queueDirection(direction);
|
||||
session.queueDirection(direction, "keyboard-mouse");
|
||||
webDebug("snake-web-input", "direction", { direction, source: "keyboard-mouse" });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
58
Web/game-snake-poc/frontend/ts/lifecycle.ts
Normal file
58
Web/game-snake-poc/frontend/ts/lifecycle.ts
Normal 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();
|
||||
},
|
||||
};
|
||||
}
|
||||
45
Web/game-snake-poc/frontend/ts/logging.ts
Normal file
45
Web/game-snake-poc/frontend/ts/logging.ts
Normal 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);
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
// file: Web/game-snake-poc/frontend/ts/main.ts
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
import "simplebar";
|
||||
import { loadSnakeWebAssets } from "./assets";
|
||||
import { startGame } from "./game";
|
||||
import { bindInputs } from "./input";
|
||||
import { bindBrowserLifecycle } from "./lifecycle";
|
||||
import { webError, webInfo } from "./logging";
|
||||
|
||||
(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> {
|
||||
const canvas = requireElement<HTMLCanvasElement>("#game");
|
||||
const stage = requireElement<HTMLElement>(".game-stage");
|
||||
const score = requireElement<HTMLOutputElement>("#score");
|
||||
const length = requireElement<HTMLOutputElement>("#length");
|
||||
const provenance = requireElement<HTMLOutputElement>("#runtime-provenance");
|
||||
const assets = requireElement<HTMLOutputElement>("#asset-status");
|
||||
const status = requireElement<HTMLSpanElement>("#runtime-status");
|
||||
const error = requireElement<HTMLParagraphElement>("#startup-error");
|
||||
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…";
|
||||
const session = await startGame(canvas, score, length);
|
||||
const session = await startGame(canvas, score, length, provenance);
|
||||
bindInputs(session, buttons);
|
||||
bindBrowserLifecycle(session, stage);
|
||||
status.className = "badge text-bg-success";
|
||||
status.textContent = "Prêt";
|
||||
error.hidden = true;
|
||||
canvas.focus();
|
||||
webInfo("snake-web-main", "runtime_ready");
|
||||
}
|
||||
|
||||
void main().catch(caughtError => {
|
||||
const status = document.querySelector<HTMLSpanElement>("#runtime-status");
|
||||
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) {
|
||||
status.className = "badge text-bg-danger";
|
||||
status.textContent = "Erreur";
|
||||
@@ -45,4 +62,5 @@ void main().catch(caughtError => {
|
||||
error.textContent = `Impossible de démarrer Snake : ${message}`;
|
||||
error.hidden = false;
|
||||
}
|
||||
webError("snake-web-main", "runtime_start_failed", { message });
|
||||
});
|
||||
|
||||
98
Web/game-snake-poc/frontend/ts/provenance.ts
Normal file
98
Web/game-snake-poc/frontend/ts/provenance.ts
Normal 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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
"@types/node": "^26.1",
|
||||
"sass-embedded": "^1.102",
|
||||
"typescript": "^7.0",
|
||||
"vite": "^8.2"
|
||||
"vite": "^8.2",
|
||||
"vite-plugin-static-copy": "^4.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// file: Web/game-snake-poc/vite.config.ts
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
import { NodePackageImporter } from "sass-embedded";
|
||||
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, "../.."));
|
||||
@@ -14,8 +15,18 @@ 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"));
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
viteStaticCopy({
|
||||
targets: [
|
||||
{ src: commonRuntimeAsset, dest: "common/data" },
|
||||
{ src: gameRuntimeAsset, dest: "game/data" },
|
||||
],
|
||||
}),
|
||||
],
|
||||
base: "./",
|
||||
cacheDir: viteCacheDir,
|
||||
clearScreen: false,
|
||||
@@ -74,7 +85,7 @@ export default defineConfig({
|
||||
port: 1434,
|
||||
strictPort: true,
|
||||
fs: {
|
||||
allow: [appRoot, externalBuildRoot],
|
||||
allow: [appRoot, repositoryRoot, externalBuildRoot],
|
||||
},
|
||||
},
|
||||
preview: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# file: crates/apps/game-snake-poc-wasm/Cargo.toml
|
||||
# version: 1
|
||||
# version: 2
|
||||
|
||||
[package]
|
||||
name = "game-snake-poc-wasm"
|
||||
@@ -15,6 +15,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
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" }
|
||||
wasm-bindgen.workspace = true
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/apps/game-snake-poc-wasm/src/runtime.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
const FIXED_STEP_MILLIS: u64 = 16;
|
||||
|
||||
@@ -7,6 +7,7 @@ const FIXED_STEP_MILLIS: u64 = 16;
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub struct SnakeWasmGame {
|
||||
pending_input: engine_v1_common::InputState,
|
||||
provenance: engine_v1_platform_api::RuntimeProvenance,
|
||||
runner: engine_v1_common::FixedStepRunner,
|
||||
state: game_snake_poc::SnakeState,
|
||||
}
|
||||
@@ -18,11 +19,63 @@ impl SnakeWasmGame {
|
||||
pub fn new() -> Self {
|
||||
return Self {
|
||||
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)),
|
||||
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.
|
||||
pub fn left(&mut self) {
|
||||
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)]
|
||||
#[path = "../unit_tests/runtime.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/apps/game-snake-poc-wasm/unit_tests/runtime.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
#[test]
|
||||
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_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
118
deltas/0.3.0/0-pre.5.md
Normal 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.
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/000-README.md -->
|
||||
<!-- version: 24 -->
|
||||
<!-- version: 25 -->
|
||||
|
||||
# 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/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/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é
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/architecture/005-ASSET_ARCHITECTURE.md -->
|
||||
<!-- version: 3 -->
|
||||
<!-- version: 4 -->
|
||||
|
||||
# Architecture des assets
|
||||
|
||||
@@ -92,3 +92,16 @@ assets/game/...
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/architecture/010-RUNTIME_PROVENANCE.md -->
|
||||
<!-- version: 1 -->
|
||||
<!-- version: 2 -->
|
||||
|
||||
# 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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/development/003-TRACING_AND_DIAGNOSTICS.md -->
|
||||
<!-- version: 1 -->
|
||||
<!-- version: 2 -->
|
||||
|
||||
# Tracing et diagnostics
|
||||
|
||||
@@ -21,3 +21,9 @@ Les runners Desktop initialisent ce socle. Les futures intégrations Android pou
|
||||
## Évolution
|
||||
|
||||
Les filtres par target/niveau, fichiers rotatifs, configuration utilisateur, traces Android et éventuelle télémétrie distante seront ajoutés uniquement lorsqu’un 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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/development/010-SNAKE_WEB_FRONTEND.md -->
|
||||
<!-- version: 3 -->
|
||||
<!-- version: 4 -->
|
||||
|
||||
# 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 ;
|
||||
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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- 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
|
||||
|
||||
@@ -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.
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/rules/RULES_PROJECT.md -->
|
||||
<!-- version: 15 -->
|
||||
<!-- version: 16 -->
|
||||
|
||||
# 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-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-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
|
||||
|
||||
@@ -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-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-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-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-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-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
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/validation/001-VALIDATION_GATES.md -->
|
||||
<!-- version: 6 -->
|
||||
<!-- version: 7 -->
|
||||
|
||||
# Gates de validation
|
||||
|
||||
@@ -71,4 +71,6 @@ npm install
|
||||
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.
|
||||
|
||||
29
history/0.3.0/0-pre.4.fix.3.md
Normal file
29
history/0.3.0/0-pre.4.fix.3.md
Normal 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.
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# file: scripts/audit_distribution_layout.py
|
||||
# version: 5
|
||||
# version: 6
|
||||
|
||||
"""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/main.scss",
|
||||
"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-snake-poc/build.gradle",
|
||||
"scripts/build_android_rust.py",
|
||||
@@ -88,6 +92,50 @@ def audit_snake_web_shell(root: pathlib.Path) -> list[str]:
|
||||
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:
|
||||
"""Validate that every static distribution boundary exists."""
|
||||
|
||||
@@ -105,6 +153,7 @@ def main() -> int:
|
||||
if (root / relative).exists():
|
||||
forbidden_present.append(relative)
|
||||
contract_violations.extend(audit_snake_web_shell(root))
|
||||
contract_violations.extend(audit_snake_web_integration(root))
|
||||
if missing:
|
||||
for relative in missing:
|
||||
print(f"DIST-LAYOUT-001: missing required path: {relative}", file=sys.stderr)
|
||||
|
||||
Reference in New Issue
Block a user