// Un coup demi-coup par demi-coup (Game.lastMoveSteps), sur le vrai moteur // WebAssembly : les plateaux intermédiaires que l'interface anime, et les // pions frappés qu'elle signale. // // 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 type { BoardSide } from "../src/domain/game/engine.js"; import { Game } from "../src/domain/game/game.js"; import { BAR, formatHalfMoves, type PositionId, parseHalfMoves, } from "../src/domain/game/position.js"; import { fixedDice } from "../src/domain/game/roll.js"; import { loadNodeEngine } from "./support/node-engine.js"; // Le joueur, au trait : un pion sur sa case 13, quatorze sur sa case 6. // L'ordinateur : un blot sur la case 10 du joueur (sa case 15), quatorze sur // sa case 6. const PLAYER_CAN_HIT = "4P8HEADg/wcEAA"; // Le joueur, au trait : un pion sur la barre, quatorze sur sa case 6. // L'ordinateur : un blot sur la case 21 du joueur (sa case 4), quatorze sur // sa case 6. const PLAYER_ON_BAR = "yP8PAADg/wcAQA"; // Le joueur, au trait, n'a plus que deux pions, sur ses cases 6 et 5. const PLAYER_ALMOST_DONE = "/38AAABQAAAAAA"; /** Une partie où le joueur, au trait, joue le jet dice à la position. */ async function playerMove(position: PositionId, dice: [number, number], move: string) { const engine = await loadNodeEngine(); const game = Game.start(engine, { dice: fixedDice(dice), setup: { position, firstTurn: "player" }, }); await game.roll(); for (const halfMove of parseHalfMoves(move)) { assert.equal(await game.playHalfMove(halfMove), true, formatHalfMoves([halfMove])); } return game; } /** Le nombre de pions d'un camp sur une case (1 à 24), sur la barre ou sortis. */ function at(side: BoardSide, point: number | "bar" | "off"): number { if (point === "bar") { return side.bar; } return point === "off" ? side.off : (side.points[point - 1] ?? 0); } test("un coup simple : un plateau par demi-coup, sans pion frappé", async () => { const engine = await loadNodeEngine(); const game = Game.start(engine, { dice: fixedDice([3, 1]) }); for (const halfMove of parseHalfMoves("8/5 6/5")) { await game.playHalfMove(halfMove); } const steps = await game.lastMoveSteps(); assert.ok(steps !== null); assert.equal(steps.side, "player"); assert.equal(formatHalfMoves(steps.halfMoves), "8/5 6/5"); assert.deepEqual(steps.hits, [false, false]); const counts = steps.boards.map((b) => [at(b.player, 8), at(b.player, 6), at(b.player, 5)]); assert.deepEqual(counts, [ [3, 5, 0], [2, 5, 1], [2, 4, 2], ]); // Le dernier plateau est celui de la partie après le coup. assert.deepEqual(steps.boards[2], await game.displayedBoard()); }); test("un pion frappé part sur la barre au demi-coup qui le frappe", async () => { const game = await playerMove(PLAYER_CAN_HIT, [3, 1], "13/10 10/9"); const steps = await game.lastMoveSteps(); assert.ok(steps !== null); assert.deepEqual(steps.hits, [true, false]); const bar = steps.boards.map((b) => at(b.computer, "bar")); assert.deepEqual(bar, [0, 1, 1]); // Le blot était sur la case 10 du joueur, la case 15 de l'ordinateur. assert.equal(at(steps.boards[0]?.computer as BoardSide, 15), 1); assert.equal(at(steps.boards[1]?.computer as BoardSide, 15), 0); assert.equal(at(steps.boards[2]?.player as BoardSide, 9), 1); }); test("un coup depuis la barre, qui frappe en rentrant", async () => { const game = await playerMove(PLAYER_ON_BAR, [4, 2], "bar/21 6/4"); const steps = await game.lastMoveSteps(); assert.ok(steps !== null); assert.deepEqual(steps.hits, [true, false]); assert.deepEqual( steps.boards.map((b) => [at(b.player, "bar"), at(b.player, 21), at(b.computer, "bar")]), [ [1, 0, 0], [0, 1, 1], [0, 1, 1], ], ); }); test("une sortie de pions, jusqu'à la fin de la partie", async () => { const game = await playerMove(PLAYER_ALMOST_DONE, [6, 5], "6/off 5/off"); assert.equal(game.state.phase, "over"); const steps = await game.lastMoveSteps(); assert.ok(steps !== null); assert.deepEqual(steps.hits, [false, false]); assert.deepEqual( steps.boards.map((b) => at(b.player, "off")), [13, 14, 15], ); }); test("le coup de l'ordinateur, vu dans sa propre numérotation", async () => { const engine = await loadNodeEngine(); // Vous commencez avec 3-1, puis l'ordinateur tire 6-4. const game = Game.start(engine, { dice: fixedDice([3, 1, 6, 4]) }); for (const halfMove of parseHalfMoves("8/5 6/5")) { await game.playHalfMove(halfMove); } await game.playComputerTurn(); const steps = await game.lastMoveSteps(); assert.ok(steps !== null); assert.equal(steps.side, "computer"); assert.equal(steps.boards.length, steps.halfMoves.length + 1); // Chaque demi-coup déplace un pion de l'ordinateur de sa case de départ. for (const [i, h] of steps.halfMoves.entries()) { const from = h.from === BAR ? "bar" : h.from; const before = steps.boards[i]?.computer as BoardSide; const after = steps.boards[i + 1]?.computer as BoardSide; assert.equal(at(after, from), at(before, from) - 1); } assert.deepEqual(steps.boards.at(-1), await game.displayedBoard()); }); test("avant le premier coup, il n'y a rien à montrer", async () => { const engine = await loadNodeEngine(); const game = Game.start(engine, { dice: fixedDice([3, 1]) }); assert.equal(await game.lastMoveSteps(), null); });