Fusion de main (révision UX) ; Mes parties dans le menu, reprise sans rechargement
« Reprendre l'ancienne » relit l'ancienne partie dans le stockage par son identifiant et supprime la nouvelle (GameSession). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+46
-93
@@ -5,120 +5,73 @@
|
||||
// Copyright (C) 2026 les auteurs d'Ustagammon.
|
||||
// Licence : GPLv3 ou ultérieure (voir LICENSE).
|
||||
|
||||
import { switchToBranch } from "./domain/game/branches.js";
|
||||
import type { Level } from "./domain/game/engine.js";
|
||||
import { Game } from "./domain/game/game.js";
|
||||
import { exportSgf } from "./domain/game/sgf.js";
|
||||
import { createWasmEngine } from "./engine/wasm-engine.js";
|
||||
import { startEngineWorker } from "./engine/worker-run.js";
|
||||
import { newGameId } from "./storage/game-library.js";
|
||||
import { openGameLibrary } from "./storage/open-library.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 { GAMES_TEXTS } from "./ui/games-texts.js";
|
||||
import { createGamesView } from "./ui/games-view.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);
|
||||
|
||||
// Les parties enregistrées (IndexedDB, ou localStorage en repli). Chaque
|
||||
// accès au stockage du navigateur peut lever une exception (stockage
|
||||
// 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).
|
||||
const { library } = await openGameLibrary({
|
||||
indexedDB: attempt(() => window.indexedDB),
|
||||
localStorage: attempt(() => window.localStorage),
|
||||
localStorage,
|
||||
});
|
||||
/** Le niveau de l'ordinateur (celui de Game.start, STANDARD_PLY). */
|
||||
const LEVEL: Level = "2ply";
|
||||
const saved = await library.current().catch(() => null);
|
||||
// La partie à l'écran ; un nouvel identifiant à chaque nouvelle partie.
|
||||
let shownId = saved?.summary.id ?? newGameId();
|
||||
// Vrai une fois qu'on recharge la page pour reprendre une autre partie :
|
||||
// plus rien n'est enregistré sous l'identifiant de celle qu'on quitte.
|
||||
let leaving = false;
|
||||
// Le niveau de l'ordinateur : celui de Game.start (STANDARD_PLY).
|
||||
const session = createGameSession(
|
||||
library,
|
||||
{ start: () => Game.start(engine), resume: (journal) => Game.resume(engine, journal) },
|
||||
"2ply",
|
||||
);
|
||||
const saved = await session.restore();
|
||||
|
||||
startApp({
|
||||
engineReady: worker.ready,
|
||||
newGame: () => {
|
||||
shownId = newGameId();
|
||||
return Game.start(engine);
|
||||
newGame: () => session.newGame(),
|
||||
savedGame: () => saved,
|
||||
resumePrevious: () => session.resumePrevious(),
|
||||
saveGame: (journal) => session.save(journal),
|
||||
games: {
|
||||
list: () => library.list(),
|
||||
journal: async (id) => (await library.load(id))?.journal ?? null,
|
||||
shownId: () => session.shownId,
|
||||
open: (id, branchHead) => session.open(id, branchHead),
|
||||
remove: (id) => library.remove(id),
|
||||
exportSgf: async (id) => {
|
||||
const game = await 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 };
|
||||
},
|
||||
},
|
||||
savedGame: () => {
|
||||
if (saved === null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Game.resume(engine, saved.journal);
|
||||
} catch {
|
||||
return null; // partie illisible : on en commence une nouvelle
|
||||
}
|
||||
},
|
||||
saveGame: (journal) => {
|
||||
if (!leaving) {
|
||||
// Stockage plein ou interdit : la partie continue, elle ne
|
||||
// pourra seulement pas être reprise.
|
||||
library.save(shownId, journal, { level: LEVEL }).catch(() => {});
|
||||
}
|
||||
},
|
||||
confirm: (question) => window.confirm(question),
|
||||
cachedBoard: () => settings.loadBoard(),
|
||||
saveBoard: (board) => settings.saveBoard(board),
|
||||
loadSettings: () => settings.loadSettings(),
|
||||
saveSettings: (json) => settings.saveSettings(json),
|
||||
});
|
||||
|
||||
// « Mes parties ». Point d'accroche provisoire : un bouton ajouté devant
|
||||
// celui de l'aide, en attendant le menu de l'application (révision UX).
|
||||
// Reprendre une partie la désigne comme partie en cours, puis recharge la
|
||||
// page, qui l'ouvre.
|
||||
const gamesView = createGamesView({
|
||||
list: () => library.list(),
|
||||
journal: async (id) => (await library.load(id))?.journal ?? null,
|
||||
shownId: () => shownId,
|
||||
resume: async (id, branchHead) => {
|
||||
const game = await library.load(id);
|
||||
if (game === null) {
|
||||
throw new Error("partie introuvable");
|
||||
}
|
||||
leaving = true;
|
||||
if (branchHead === undefined) {
|
||||
await library.setCurrent(id);
|
||||
} else {
|
||||
const journal = switchToBranch(game.journal, branchHead);
|
||||
await library.save(id, journal, { level: game.summary.level });
|
||||
}
|
||||
window.location.reload();
|
||||
},
|
||||
remove: (id) => library.remove(id),
|
||||
exportSgf: async (id) => {
|
||||
const game = await library.load(id);
|
||||
if (game === null) {
|
||||
throw new Error("partie introuvable");
|
||||
}
|
||||
const started = new Date(game.summary.startedAt);
|
||||
const date = [started.getFullYear(), started.getMonth() + 1, started.getDate()]
|
||||
.map((n) => String(n).padStart(2, "0"))
|
||||
.join("-");
|
||||
const time = [started.getHours(), started.getMinutes()]
|
||||
.map((n) => String(n).padStart(2, "0"))
|
||||
.join("");
|
||||
const text = await exportSgf(engine, game.journal, {
|
||||
playerName: "Vous",
|
||||
computerName: "Ustagammon",
|
||||
date,
|
||||
});
|
||||
return { fileName: `ustagammon-${date}-${time}.sgf`, text };
|
||||
},
|
||||
confirm: (question) => window.confirm(question),
|
||||
});
|
||||
const helpButton = document.getElementById("help-button");
|
||||
if (helpButton !== null) {
|
||||
const gamesButton = document.createElement("button");
|
||||
gamesButton.id = "games-button";
|
||||
gamesButton.type = "button";
|
||||
gamesButton.className = "secondary";
|
||||
gamesButton.textContent = GAMES_TEXTS.button;
|
||||
gamesButton.onclick = () => void gamesView.open();
|
||||
helpButton.before(gamesButton);
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user