// Point d'entrée de l'application dans le navigateur (index.html) : le seul // endroit qui assemble le domaine, le moteur, le stockage et l'interface // (ADR 0007, vérifié par dependency-cruiser). // // Copyright (C) 2026 les auteurs d'Ustagammon. // Licence : GPLv3 ou ultérieure (voir LICENSE). import { analyseDecision } from "./domain/analysis/analyse.js"; import { Game } from "./domain/game/game.js"; import { linkOf, readPosition } from "./domain/game/position-link.js"; import { exportSgf } from "./domain/game/sgf.js"; import { illustratedMove, MovePractice, shouldTake, tutorialBoard, } from "./domain/learning/tutorial.js"; import { createWasmEngine } from "./engine/wasm-engine.js"; import { type EngineWorker, startEngineWorker } from "./engine/worker-run.js"; import { createGameSession } from "./storage/game-session.js"; import { memoryStorage, openGameLibrary } from "./storage/open-library.js"; import { createSettingsStore } from "./storage/settings-store.js"; import { startApp } from "./ui/app.js"; import { startUpdates } from "./ui/updates.js"; // Ce fichier est servi sous app/ ; le Worker et le service worker sont à la // racine du site. const worker = startEngineWorker(new URL("../engine-worker.mjs", import.meta.url)); const engine = createWasmEngine(worker.run); // Le conseil du maître (étape 1d) : les analyses à 2 plis tournent sur un // second moteur, dans son propre Web Worker. Le premier ne traite qu'une // commande à la fois : une analyse y retarderait le coup de l'ordinateur. // Le second démarre à la première analyse demandée, une fois le premier // chargé et le service worker installé (au plus 20 s d'attente) : si deux // téléchargements des données du moteur se croisent, Chromium fait échouer // l'installation hors ligne (voir startUpdates plus bas). Ensuite, les // données viennent du cache. let analysisWorker: Promise | null = null; function startAnalysisWorker(): Promise { analysisWorker ??= worker.ready .then(() => { const installed = "serviceWorker" in navigator ? navigator.serviceWorker.ready : Promise.resolve(); return Promise.race([installed, new Promise((resolve) => setTimeout(resolve, 20_000))]); }) .then(() => { const started = startEngineWorker(new URL("../engine-worker.mjs", import.meta.url)); return started.ready.then(() => started); }); return analysisWorker; } const analysisEngine = createWasmEngine(async (args) => (await startAnalysisWorker()).run(args)); // Chaque accès au stockage du navigateur peut lever une exception (stockage // interdit) : on fait alors sans. const localStorage = attempt(() => window.localStorage); const settings = createSettingsStore(localStorage ?? memoryStorage()); // Les parties enregistrées (IndexedDB, ou localStorage en repli), ouvertes // pendant que le moteur se charge : le plateau est dessiné sans les // attendre. L'écran de jeu ne s'en sert qu'une fois engineReady résolue. const opening = (async () => { const { library } = await openGameLibrary({ indexedDB: attempt(() => window.indexedDB), localStorage, }); // Le niveau et le match de chaque partie viennent de l'écran « Nouvelle // partie » (ou de la partie précédente du match). const session = createGameSession(library, { start: (newGame) => Game.start(engine, newGame ?? {}), resume: (journal) => Game.resume(engine, journal), }); const saved = await session.restore(); return { library, session, saved }; })(); let stored: Awaited | null = null; /** Le stockage, ouvert (toujours le cas une fois engineReady résolue). */ function storage(): Awaited { if (stored === null) { throw new Error("stockage pas encore ouvert"); } return stored; } startApp({ engineReady: Promise.all([ worker.ready, opening.then((opened) => { stored = opened; }), ]), newGame: (newGame) => storage().session.newGame(newGame), positions: { read: (link) => readPosition(engine, link), linkOf: (game) => linkOf(engine, game.state), }, savedGame: () => storage().saved, resumePrevious: () => storage().session.resumePrevious(), saveGame: (journal) => storage().session.save(journal), games: { list: () => storage().library.list(), journal: async (id) => (await storage().library.load(id))?.journal ?? null, shownId: () => storage().session.shownId, open: (id, branchHead) => storage().session.open(id, branchHead), remove: (id) => storage().library.remove(id), exportSgf: async (id) => { const game = await storage().library.load(id); if (game === null) { throw new Error("partie introuvable"); } const started = new Date(game.summary.startedAt); const two = (n: number) => String(n).padStart(2, "0"); const date = `${started.getFullYear()}-${two(started.getMonth() + 1)}-${two(started.getDate())}`; const text = await exportSgf(engine, game.journal, { playerName: "Vous", computerName: "Ustagammon", date, }); const time = `${two(started.getHours())}${two(started.getMinutes())}`; return { fileName: `ustagammon-${date}-${time}.sgf`, text }; }, }, cachedBoard: () => settings.loadBoard(), saveBoard: (board) => settings.saveBoard(board), loadSettings: () => settings.loadSettings(), saveSettings: (json) => settings.saveSettings(json), // Le tutoriel : positions, coups illustrés et gestes jugés par le // domaine, sur le moteur. tutorial: { board: (position, onRoll) => tutorialBoard(engine, position, onRoll), illustrate: (position, roll, move) => illustratedMove(engine, position, roll, move), practice: (position, roll, expected) => MovePractice.start(engine, position, roll, expected), shouldTake: (position) => shouldTake(engine, position), }, // Hors ligne : le service worker met tout le site en cache. Le navigateur // le refuse hors https (sauf localhost) : l'application marche alors sans. // Il est enregistré une fois le moteur chargé : si le Worker et le service // worker téléchargent les données du moteur (8,7 Mo) en même temps, // Chromium fait échouer l'installation (« Cache.add() encountered a // network error »), et l'application n'est pas disponible hors ligne. // Cela évite aussi de télécharger deux fois ces données à la première // visite. analyse: (journal, eventId) => analyseDecision(analysisEngine, journal, eventId), startUpdates: (onAvailable) => startUpdates(new URL("../sw.js", import.meta.url), worker.ready, onAvailable), }); /** La valeur, ou undefined si l'obtenir lève une exception. */ function attempt(get: () => T): T | undefined { try { return get(); } catch { return undefined; } }