Domaine « partie » en journal d'événements, branché au moteur WebAssembly

app/src/domain/game/ : une partie libre contre l'ordinateur, enregistrée
comme un arbre d'événements immuables et versionnés (GameStarted,
DiceRolled, MovePlayed, CubeOffered, CubeTaken, CubeDropped, GameEnded) ;
l'état se recalcule en les rejouant, annuler repart du parent, une action
après un retour ouvre une branche. Dés injectés. Le joueur saisit son coup
demi-coup par demi-coup ; l'ordinateur joue et décide du videau à 2 plis.
Toutes les règles viennent du moteur, derrière l'interface Engine que le
domaine définit.

app/src/engine/wasm-engine.ts : l'adaptateur Engine sur les commandes
d'engine-cli du moteur WebAssembly.

Tests : unitaires avec un faux moteur, et deux parties complètes jusqu'à
GameEnded sur le vrai moteur, avec des dés imposés.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Pierre MARTIN
2026-11-02 20:42:08 +01:00
co-authored by Claude Opus 5.5
parent 54883f4ca3
commit 602d0eafed
14 changed files with 1314 additions and 0 deletions
+68
View File
@@ -0,0 +1,68 @@
// Une partie complète sur le vrai moteur WebAssembly, avec des dés imposés :
// l'ordinateur contre une stratégie simple du joueur (toujours le premier
// coup légal, saisi demi-coup par demi-coup, et toujours prendre).
//
// Copyright (C) 2026 les auteurs d'Ustagammon.
// Licence : GPLv3 ou ultérieure (voir LICENSE).
import assert from "node:assert/strict";
import { test } from "node:test";
import { Game } from "../src/domain/game/game.js";
import type { DiceSource } from "../src/domain/game/roll.js";
import { replay } from "../src/domain/game/state.js";
import { loadNodeEngine } from "./support/node-engine.js";
/** Des dés pseudo-aléatoires reproductibles (générateur congruentiel). */
function seededDice(seed: number): DiceSource {
let state = seed;
return () => {
state = (state * 1103515245 + 12345) % 2147483648;
return 1 + (Math.floor(state / 65536) % 6);
};
}
/** Joue jusqu'à la fin ; renvoie le nombre d'actions. */
async function playToTheEnd(game: Game, engine: Awaited<ReturnType<typeof loadNodeEngine>>) {
let actions = 0;
while (game.state.phase !== "over") {
assert.ok(++actions < 2000, "la partie ne finit pas");
const state = game.state;
if (state.turn === "computer" && state.phase === "doubled") {
game.take();
} else if (state.turn === "computer") {
await game.playComputerTurn();
} else if (state.phase === "rolling") {
await game.roll();
} else {
assert.ok(state.roll !== null);
const [first] = await engine.legalMoves(state.position, state.roll);
assert.ok(first !== undefined, "le joueur devrait avoir un coup");
for (const halfMove of first.halfMoves) {
assert.equal(await game.playHalfMove(halfMove), true);
}
}
}
return actions;
}
for (const [seed, ply] of [
[1, 0],
[7, 2],
] as const) {
test(`partie complète, dés ${seed}, ordinateur à ${ply} pli(s)`, async () => {
const engine = await loadNodeEngine();
const game = Game.start(engine, { dice: seededDice(seed), ply });
await playToTheEnd(game, engine);
const events = game.events;
const last = events.at(-1);
assert.equal(last?.type, "GameEnded");
const result = game.state.result;
assert.ok(result !== null);
assert.ok(result.points >= 1);
assert.ok(events.some((e) => e.type === "MovePlayed" && e.side === "player"));
assert.ok(events.some((e) => e.type === "MovePlayed" && e.side === "computer"));
// Rejouer le journal redonne le même état.
assert.deepEqual(replay(events), game.state);
});
}