Écran #/nouvelle-partie (type de partie, niveau, conseil du maître, choix gardés), score du match en portrait et en paysage, videau caché pendant la partie Crawford, « Partie suivante » ; test navigateur d'un match en 3 points jusqu'au bout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
132 lines
5.6 KiB
TypeScript
132 lines
5.6 KiB
TypeScript
// 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 { 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 { 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);
|
|
|
|
// 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<typeof opening> | null = null;
|
|
/** Le stockage, ouvert (toujours le cas une fois engineReady résolue). */
|
|
function storage(): Awaited<typeof opening> {
|
|
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.
|
|
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<T>(get: () => T): T | undefined {
|
|
try {
|
|
return get();
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|