Files
Ustagammon/app/tests/move-steps.test.ts
Pierre MARTINandClaude Opus 5.5 7a9b376417 Coup de l'ordinateur : animation, traces et bouton Revoir
Le domaine donne le dernier coup demi-coup par demi-coup (plateaux
intermédiaires du moteur, pions frappés lus sur la barre) ; l'interface
fait glisser les pions puis laisse flèches, pions entourés et fantômes
jusqu'à l'action suivante. Scénario « Voir le coup de l'ordinateur »,
tests unitaires et test dans le navigateur.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 01:53:26 +01:00

144 lines
5.6 KiB
TypeScript

// 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);
});