Plateau jouable en PWA : interface, Web Worker, stockage, service worker

L'application est à la racine du site : plateau en SVG, saisie au toucher
(pion puis case d'arrivée, proposées à partir du moteur), boutons du jet,
du videau et de nouvelle partie, coup de l'ordinateur affiché avec son
temps, fin de partie, reprise depuis localStorage, aide lue dans les
copies de docs/. Le moteur tourne dans le Web Worker existant ; main.ts
est le seul assembleur (nouvelles règles dependency-cruiser). Le service
worker met tout le site en cache, avec une empreinte du contenu comme
version. Les pages de test du moteur passent sous /moteur/. Politique de
sécurité du contenu dans le Caddyfile ; make test-browser joue une partie
dans Chromium et Firefox sans écran (Playwright, hors de make test).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Pierre MARTIN
2026-11-02 21:47:12 +01:00
co-authored by Claude Opus 5.5
parent 613ff38254
commit a8636b3f83
37 changed files with 2548 additions and 106 deletions
+81
View File
@@ -0,0 +1,81 @@
// Les commandes du moteur exécutées dans un Web Worker (web/engine-worker.mjs),
// pour que le calcul ne bloque jamais l'affichage.
//
// Copyright (C) 2026 les auteurs d'Ustagammon.
// Licence : GPLv3 ou ultérieure (voir LICENSE).
//
// Messages du Worker (voir engine-worker.mjs) : une fois au chargement
// { type: "ready" } ou { type: "failed", message } ; puis, pour chaque
// commande { id, args } envoyée, { id, result } ou { id, error }. Le Worker
// traite les commandes une à une, dans l'ordre d'arrivée.
import type { RunCommand } from "./wasm-engine.js";
export interface EngineWorker {
/** Résolue quand le moteur est chargé ; rejetée s'il ne peut pas l'être. */
readonly ready: Promise<void>;
readonly run: RunCommand;
}
interface Pending {
resolve(result: unknown): void;
reject(error: Error): void;
}
export function startEngineWorker(url: URL): EngineWorker {
const worker = new Worker(url, { type: "module" });
const pending = new Map<number, Pending>();
let nextId = 1;
let failure: Error | null = null;
let markReady: () => void = () => {};
let markFailed: (error: Error) => void = () => {};
const ready = new Promise<void>((resolve, reject) => {
markReady = resolve;
markFailed = reject;
});
// Le moteur est inutilisable : toutes les commandes échouent.
function fail(error: Error): void {
failure = error;
markFailed(error);
for (const { reject } of pending.values()) {
reject(error);
}
pending.clear();
}
worker.onmessage = (event: MessageEvent) => {
const message = event.data;
if (message.type === "ready") {
markReady();
} else if (message.type === "failed") {
fail(new Error(message.message));
} else {
const request = pending.get(message.id);
pending.delete(message.id);
if (message.error !== undefined) {
request?.reject(new Error(message.error));
} else {
request?.resolve(message.result);
}
}
};
// Le Worker lui-même n'a pas démarré (fichier absent, navigateur trop
// ancien pour les Workers en module ES…).
worker.onerror = (event: ErrorEvent) => {
fail(new Error(event.message || "le Web Worker du moteur n'a pas démarré"));
};
function run(args: string[]): Promise<unknown> {
if (failure !== null) {
return Promise.reject(failure);
}
const id = nextId++;
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject });
worker.postMessage({ id, args });
});
}
return { ready, run };
}