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:
co-authored by
Claude Opus 5.5
parent
54883f4ca3
commit
602d0eafed
@@ -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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user