599 lines
23 KiB
TypeScript
599 lines
23 KiB
TypeScript
// Tests unitaires du domaine « partie », avec un faux moteur (ADR 0007,
|
|
// point 7) : journal, rejeu, branches, déroulement des tours et du videau.
|
|
//
|
|
// 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 {
|
|
Board,
|
|
CubeDecision,
|
|
Engine,
|
|
EngineCube,
|
|
HalfMovesCheck,
|
|
Level,
|
|
Move,
|
|
} from "../src/domain/game/engine.js";
|
|
import {
|
|
appendEvent,
|
|
currentLine,
|
|
EMPTY_JOURNAL,
|
|
EVENT_VERSION,
|
|
type GameJournal,
|
|
goBackTo,
|
|
migrateJournal,
|
|
} from "../src/domain/game/events.js";
|
|
import { Game } from "../src/domain/game/game.js";
|
|
import { formatHalfMoves, parseHalfMoves, START_POSITION } from "../src/domain/game/position.js";
|
|
import { fixedDice } from "../src/domain/game/roll.js";
|
|
import { DEFAULT_LEVEL, replay } from "../src/domain/game/state.js";
|
|
|
|
/** Un faux moteur : chaque réponse est réglable, et les positions sont des
|
|
* textes sans signification. */
|
|
function fakeEngine(overrides: Partial<Engine> = {}): Engine {
|
|
const move: Move = {
|
|
notation: "8/5 6/5",
|
|
halfMoves: parseHalfMoves("8/5 6/5"),
|
|
position: "AFTER",
|
|
};
|
|
const notOver: Board = {
|
|
onRoll: { points: [], bar: 0, off: 0, pipCount: 0 },
|
|
opponent: { points: [], bar: 0, off: 0, pipCount: 0 },
|
|
swappedPosition: "SWAPPED",
|
|
result: null,
|
|
};
|
|
const noDouble: CubeDecision = { available: true, double: false, take: true };
|
|
return {
|
|
legalMoves: async () => [move],
|
|
bestMove: async () => move,
|
|
cubeDecision: async () => noDouble,
|
|
checkHalfMoves: async (_position, _roll, halfMoves): Promise<HalfMovesCheck> => {
|
|
const text = formatHalfMoves(halfMoves);
|
|
if (text === "8/5") {
|
|
return { valid: true, complete: false, position: "HALF" };
|
|
}
|
|
if (text === "8/5 6/5") {
|
|
return { valid: true, complete: true, position: "AFTER" };
|
|
}
|
|
return { valid: false, complete: false, position: null };
|
|
},
|
|
board: async () => notOver,
|
|
levelMove: async () => move,
|
|
levelCubeDecision: async () => noDouble,
|
|
levelDouble: async () => false,
|
|
levelTake: async () => true,
|
|
matchAfterGame: async (match, winner, points) => {
|
|
const score = { ...match.score };
|
|
score[winner] += points;
|
|
return {
|
|
score,
|
|
over: score[winner] >= match.length,
|
|
crawford: score[winner] === match.length - 1,
|
|
};
|
|
},
|
|
analyseMove: async () => null,
|
|
rankMoves: async () => [],
|
|
analyseCube: async () => ({
|
|
analysed: false,
|
|
equities: null,
|
|
equityLoss: 0,
|
|
skill: "none",
|
|
}),
|
|
luck: async () => ({ value: 0, rating: "none" }),
|
|
encodeMatch: async () => "MATCH",
|
|
decodeMatch: async () => ({
|
|
matchLength: 0,
|
|
score: [0, 0],
|
|
cubeValue: 1,
|
|
cubeOwner: -1,
|
|
playerOnRoll: 0,
|
|
crawford: false,
|
|
jacoby: true,
|
|
}),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
test("le jet d'ouverture se relance en cas d'égalité, puis le plus fort dé commence", () => {
|
|
const game = Game.start(fakeEngine(), { dice: fixedDice([4, 4, 2, 5]) });
|
|
const rolls = game.events.filter((e) => e.type === "DiceRolled");
|
|
assert.deepEqual(
|
|
rolls.map((e) => e.roll),
|
|
[
|
|
[4, 4],
|
|
[2, 5],
|
|
],
|
|
);
|
|
assert.equal(game.state.turn, "computer");
|
|
assert.equal(game.state.phase, "moving");
|
|
assert.deepEqual(game.state.roll, [2, 5]);
|
|
assert.equal(game.state.position, START_POSITION);
|
|
});
|
|
|
|
test("un coup se saisit demi-coup par demi-coup ; un demi-coup refusé ne change rien", async () => {
|
|
const game = Game.start(fakeEngine(), { dice: fixedDice([3, 1]) });
|
|
assert.equal(await game.playHalfMove({ from: 13, to: 12 }), false);
|
|
assert.equal(game.events.length, 2);
|
|
assert.equal(await game.playHalfMove({ from: 8, to: 5 }), true);
|
|
assert.equal(game.displayedPosition, "HALF");
|
|
assert.deepEqual(game.pendingHalfMoves, [{ from: 8, to: 5 }]);
|
|
assert.equal(await game.playHalfMove({ from: 6, to: 5 }), true);
|
|
|
|
// Coup complet : il attend la validation, hors du journal.
|
|
assert.equal(game.moveComplete, true);
|
|
assert.equal(game.displayedPosition, "AFTER");
|
|
assert.equal(game.events.length, 2);
|
|
assert.equal(game.state.turn, "player");
|
|
assert.deepEqual(await game.nextHalfMoves(), []);
|
|
assert.equal(await game.playHalfMove({ from: 8, to: 7 }), false);
|
|
|
|
await game.confirmMove();
|
|
const last = game.events.at(-1);
|
|
assert.equal(last?.type, "MovePlayed");
|
|
assert.deepEqual(game.pendingHalfMoves, []);
|
|
assert.equal(game.moveComplete, false);
|
|
assert.equal(game.state.turn, "computer");
|
|
assert.equal(game.state.phase, "rolling");
|
|
assert.equal(game.state.position, "SWAPPED");
|
|
});
|
|
|
|
test("un coup incomplet ne se valide pas", async () => {
|
|
const game = Game.start(fakeEngine(), { dice: fixedDice([3, 1]) });
|
|
await assert.rejects(game.confirmMove(), /pas complet/);
|
|
await game.playHalfMove({ from: 8, to: 5 });
|
|
await assert.rejects(game.confirmMove(), /pas complet/);
|
|
assert.equal(game.events.length, 2);
|
|
});
|
|
|
|
test("Annuler défait les demi-coups un par un, sans toucher au journal", async () => {
|
|
const game = Game.start(fakeEngine(), { dice: fixedDice([3, 1]) });
|
|
await game.playHalfMove({ from: 8, to: 5 });
|
|
await game.playHalfMove({ from: 6, to: 5 });
|
|
const journal = game.journal;
|
|
|
|
game.undoLastHalfMove();
|
|
assert.deepEqual(game.pendingHalfMoves, [{ from: 8, to: 5 }]);
|
|
assert.equal(game.moveComplete, false);
|
|
assert.equal(game.displayedPosition, "HALF");
|
|
|
|
game.undoLastHalfMove();
|
|
assert.deepEqual(game.pendingHalfMoves, []);
|
|
assert.equal(game.displayedPosition, START_POSITION);
|
|
game.undoLastHalfMove(); // rien à défaire : sans effet
|
|
assert.equal(game.journal, journal);
|
|
|
|
// Le coup se ressaisit ensuite normalement.
|
|
assert.equal(await game.playHalfMove({ from: 8, to: 5 }), true);
|
|
assert.equal(await game.playHalfMove({ from: 6, to: 5 }), true);
|
|
assert.equal(game.moveComplete, true);
|
|
});
|
|
|
|
test("les dés utilisés par les demi-coups saisis sont marqués", async () => {
|
|
const game = Game.start(fakeEngine(), { dice: fixedDice([3, 1]) });
|
|
assert.deepEqual(game.diceToPlay, [
|
|
{ value: 3, used: false },
|
|
{ value: 1, used: false },
|
|
]);
|
|
await game.playHalfMove({ from: 8, to: 5 });
|
|
assert.deepEqual(game.diceToPlay, [
|
|
{ value: 3, used: true },
|
|
{ value: 1, used: false },
|
|
]);
|
|
await game.playHalfMove({ from: 6, to: 5 });
|
|
assert.deepEqual(
|
|
game.diceToPlay.map((d) => d.used),
|
|
[true, true],
|
|
);
|
|
await game.confirmMove();
|
|
assert.deepEqual(game.diceToPlay, []);
|
|
});
|
|
|
|
test("un double donne quatre dés, marqués un par un ; une sortie prend le plus petit dé suffisant", async () => {
|
|
const engine = fakeEngine({
|
|
checkHalfMoves: async () => ({ valid: true, complete: false, position: "P" }),
|
|
});
|
|
const double = Game.start(engine, {
|
|
dice: fixedDice([2, 2]),
|
|
setup: { position: "X", firstTurn: "player" },
|
|
});
|
|
await double.roll();
|
|
assert.equal(double.diceToPlay.length, 4);
|
|
await double.playHalfMove({ from: 13, to: 11 });
|
|
await double.playHalfMove({ from: 11, to: 9 });
|
|
assert.deepEqual(
|
|
double.diceToPlay.map((d) => d.used),
|
|
[true, true, false, false],
|
|
);
|
|
|
|
const bearOff = Game.start(engine, {
|
|
dice: fixedDice([6, 4]),
|
|
setup: { position: "X", firstTurn: "player" },
|
|
});
|
|
await bearOff.roll();
|
|
// 3/off : le 4 suffit, le 6 reste.
|
|
await bearOff.playHalfMove({ from: 3, to: 0 });
|
|
assert.deepEqual(bearOff.diceToPlay, [
|
|
{ value: 6, used: false },
|
|
{ value: 4, used: true },
|
|
]);
|
|
// 6/2 demande le 4 : la sortie a donc pris le 6.
|
|
await bearOff.playHalfMove({ from: 6, to: 2 });
|
|
assert.deepEqual(
|
|
bearOff.diceToPlay.map((d) => d.used),
|
|
[true, true],
|
|
);
|
|
});
|
|
|
|
test("sans coup possible, le tour passe quand le joueur le laisse passer", async () => {
|
|
const engine = fakeEngine({ legalMoves: async () => [] });
|
|
const game = Game.start(engine, {
|
|
dice: fixedDice([6, 6]),
|
|
setup: { position: "BLOCKED", firstTurn: "player" },
|
|
});
|
|
await game.roll();
|
|
// Le tour ne passe pas tout seul : le jet reste à « jouer ».
|
|
assert.equal(game.state.phase, "moving");
|
|
assert.equal(game.state.turn, "player");
|
|
assert.equal(await game.cannotMove(), true);
|
|
await game.skipTurn();
|
|
const last = game.events.at(-1);
|
|
assert.ok(last?.type === "MovePlayed");
|
|
assert.deepEqual(last.halfMoves, []);
|
|
assert.equal(game.state.turn, "computer");
|
|
});
|
|
|
|
test("le joueur ne peut pas laisser passer son tour s'il a un coup", async () => {
|
|
const engine = fakeEngine({
|
|
legalMoves: async () => [
|
|
{ notation: "8/5", halfMoves: [{ from: 8, to: 5 }], position: "P" },
|
|
],
|
|
});
|
|
const game = Game.start(engine, {
|
|
dice: fixedDice([3, 1]),
|
|
setup: { position: "X", firstTurn: "player" },
|
|
});
|
|
await game.roll();
|
|
assert.equal(await game.cannotMove(), false);
|
|
await assert.rejects(game.skipTurn(), /coup possible/);
|
|
assert.equal(game.state.phase, "moving");
|
|
});
|
|
|
|
test("l'ordinateur double si le moteur le dit à son niveau ; le joueur prend", async () => {
|
|
const engine = fakeEngine({ levelDouble: async () => true });
|
|
const game = Game.start(engine, { setup: { position: "X", firstTurn: "computer" } });
|
|
await game.playComputerTurn();
|
|
assert.equal(game.state.phase, "doubled");
|
|
game.take();
|
|
assert.deepEqual(game.state.cube, { value: 2, owner: "player" });
|
|
assert.equal(game.state.phase, "rolling");
|
|
assert.equal(game.state.turn, "computer");
|
|
});
|
|
|
|
test("le joueur passe : il perd la partie à la valeur d'avant le double", async () => {
|
|
const engine = fakeEngine({ levelDouble: async () => true });
|
|
const game = Game.start(engine, {
|
|
setup: { position: "X", firstTurn: "computer", cube: { value: 2, owner: "computer" } },
|
|
});
|
|
await game.playComputerTurn();
|
|
await game.drop();
|
|
assert.equal(game.state.phase, "over");
|
|
assert.deepEqual(game.state.result, { winner: "computer", kind: "single", points: 2 });
|
|
});
|
|
|
|
test("le joueur double : l'ordinateur passe si le moteur le dit à son niveau", async () => {
|
|
const engine = fakeEngine({ levelTake: async () => false });
|
|
const game = Game.start(engine, { setup: { position: "X", firstTurn: "player" } });
|
|
await game.double();
|
|
assert.deepEqual(
|
|
game.events.slice(-3).map((e) => e.type),
|
|
["CubeOffered", "CubeDropped", "GameEnded"],
|
|
);
|
|
assert.deepEqual(game.state.result, { winner: "player", kind: "single", points: 1 });
|
|
});
|
|
|
|
test("le joueur ne peut pas doubler si le videau est chez l'ordinateur", async () => {
|
|
const engine = fakeEngine({
|
|
cubeDecision: async () => ({ available: false, double: false, take: true }),
|
|
});
|
|
const game = Game.start(engine, {
|
|
setup: { position: "X", firstTurn: "player", cube: { value: 2, owner: "computer" } },
|
|
});
|
|
await assert.rejects(game.double(), /pas doubler/);
|
|
assert.equal(game.events.length, 1);
|
|
});
|
|
|
|
test("la fin de partie vient du moteur : gagnant, sorte et points", async () => {
|
|
const engine = fakeEngine({
|
|
board: async () => ({
|
|
onRoll: { points: [], bar: 0, off: 15, pipCount: 0 },
|
|
opponent: { points: [], bar: 0, off: 0, pipCount: 100 },
|
|
swappedPosition: "SWAPPED",
|
|
result: { winner: "onRoll", kind: "gammon", points: 2 },
|
|
}),
|
|
});
|
|
const game = Game.start(engine, {
|
|
setup: { position: "X", firstTurn: "computer" },
|
|
dice: fixedDice([2, 1]),
|
|
});
|
|
await game.playComputerTurn();
|
|
assert.deepEqual(game.state.result, { winner: "computer", kind: "gammon", points: 2 });
|
|
assert.throws(() => game.take(), /action impossible/);
|
|
});
|
|
|
|
test("annuler repart du parent ; l'action suivante ouvre une branche", async () => {
|
|
const game = Game.start(fakeEngine(), { dice: fixedDice([3, 1, 6, 5, 4, 2]) });
|
|
await game.playHalfMove({ from: 8, to: 5 });
|
|
await game.playHalfMove({ from: 6, to: 5 });
|
|
await game.confirmMove();
|
|
const played = game.events.at(-1);
|
|
assert.equal(played?.type, "MovePlayed");
|
|
|
|
game.undo();
|
|
assert.equal(game.state.phase, "moving");
|
|
assert.equal(game.state.turn, "player");
|
|
await game.playHalfMove({ from: 8, to: 5 });
|
|
await game.playHalfMove({ from: 6, to: 5 });
|
|
await game.confirmMove();
|
|
|
|
// Deux coups accrochés au même jet : l'ancien reste dans le journal.
|
|
const moves = game.journal.events.filter((e) => e.type === "MovePlayed");
|
|
assert.equal(moves.length, 2);
|
|
assert.equal(moves[0]?.parent, moves[1]?.parent);
|
|
assert.notEqual(game.events.at(-1)?.id, played?.id);
|
|
|
|
game.goBackTo(played?.id ?? "");
|
|
assert.equal(game.events.at(-1)?.id, played?.id);
|
|
});
|
|
|
|
test("les événements sont immuables et versionnés ; le rejeu est déterministe", () => {
|
|
let journal = appendEvent(EMPTY_JOURNAL, {
|
|
type: "GameStarted",
|
|
position: START_POSITION,
|
|
cube: { value: 1, owner: null },
|
|
firstTurn: null,
|
|
});
|
|
journal = appendEvent(journal, { type: "DiceRolled", side: null, roll: [5, 2] });
|
|
const [started, rolled] = currentLine(journal);
|
|
assert.equal(started?.version, 2);
|
|
assert.equal(started?.parent, null);
|
|
assert.equal(rolled?.parent, started?.id);
|
|
assert.throws(() => {
|
|
(rolled as { parent: string | null }).parent = null;
|
|
}, TypeError);
|
|
assert.deepEqual(replay(currentLine(journal)), replay(currentLine(journal)));
|
|
assert.equal(replay(currentLine(goBackTo(journal, "1"))).phase, "opening");
|
|
});
|
|
|
|
test("lecture et écriture des demi-coups", () => {
|
|
assert.deepEqual(parseHalfMoves("bar/22 6/off"), [
|
|
{ from: 25, to: 22 },
|
|
{ from: 6, to: 0 },
|
|
]);
|
|
assert.equal(formatHalfMoves(parseHalfMoves(" 8/5 6/5 ")), "8/5 6/5");
|
|
assert.throws(() => parseHalfMoves("8-5"), /mal formé/);
|
|
assert.throws(() => parseHalfMoves("30/5"), /case invalide/);
|
|
});
|
|
|
|
test("une partie se reprend depuis son journal ; un journal d'une autre version est refusé", async () => {
|
|
const game = Game.start(fakeEngine(), { dice: fixedDice([3, 1]) });
|
|
await game.playHalfMove({ from: 8, to: 5 });
|
|
await game.playHalfMove({ from: 6, to: 5 });
|
|
await game.confirmMove();
|
|
const saved = JSON.parse(JSON.stringify(game.journal));
|
|
|
|
const resumed = Game.resume(fakeEngine(), saved);
|
|
assert.deepEqual(resumed.state, game.state);
|
|
assert.deepEqual(resumed.events, game.events);
|
|
|
|
const other = { ...saved, events: saved.events.map((e: object) => ({ ...e, version: 99 })) };
|
|
assert.throws(() => Game.resume(fakeEngine(), other), /version/);
|
|
assert.throws(() => Game.resume(fakeEngine(), EMPTY_JOURNAL), /mal formé/);
|
|
assert.throws(() => Game.resume(fakeEngine(), { ...saved, head: "inconnu" }), /introuvable/);
|
|
});
|
|
|
|
test("le droit de doubler vient du moteur, seulement avant de lancer les dés", async () => {
|
|
let available = true;
|
|
const engine = fakeEngine({
|
|
cubeDecision: async () => ({ available, double: false, take: true }),
|
|
});
|
|
const game = Game.start(engine, {
|
|
dice: fixedDice([3, 1]),
|
|
setup: { position: "X", firstTurn: "player" },
|
|
});
|
|
assert.equal(await game.canDouble(), true);
|
|
available = false;
|
|
assert.equal(await game.canDouble(), false);
|
|
available = true;
|
|
await game.roll();
|
|
assert.equal(game.state.phase, "moving");
|
|
assert.equal(await game.canDouble(), false);
|
|
});
|
|
|
|
test("le plateau affiché est donné du point de vue de chaque camp", async () => {
|
|
const side = (pipCount: number) => ({ points: [], bar: 0, off: 0, pipCount });
|
|
const engine = fakeEngine({
|
|
board: async () => ({
|
|
onRoll: side(1),
|
|
opponent: side(2),
|
|
swappedPosition: "SWAPPED",
|
|
result: null,
|
|
}),
|
|
});
|
|
const playerTurn = Game.start(engine, { setup: { position: "X", firstTurn: "player" } });
|
|
const sides = await playerTurn.displayedBoard();
|
|
assert.equal(sides.player.pipCount, 1);
|
|
assert.equal(sides.computer.pipCount, 2);
|
|
const computerTurn = Game.start(engine, { setup: { position: "X", firstTurn: "computer" } });
|
|
const swapped = await computerTurn.displayedBoard();
|
|
assert.equal(swapped.player.pipCount, 2);
|
|
assert.equal(swapped.computer.pipCount, 1);
|
|
});
|
|
|
|
// ------------------------------------------------------------- Match (1c)
|
|
|
|
test("une partie de match garde son niveau et son match, et les donne au moteur vus du camp au trait", async () => {
|
|
const cubes: EngineCube[] = [];
|
|
const levels: Level[] = [];
|
|
const engine = fakeEngine({
|
|
levelDouble: async (_position, cube, level) => {
|
|
cubes.push(cube);
|
|
levels.push(level);
|
|
return false;
|
|
},
|
|
});
|
|
const match = { length: 5, score: { player: 1, computer: 3 }, crawford: false };
|
|
const game = Game.start(engine, {
|
|
dice: fixedDice([2, 5, 3, 1, 4, 2]),
|
|
level: "intermediate",
|
|
match,
|
|
});
|
|
const start = game.events[0];
|
|
assert.ok(start?.type === "GameStarted");
|
|
assert.equal(start.level, "intermediate");
|
|
assert.deepEqual(start.match, match);
|
|
assert.equal(game.state.level, "intermediate");
|
|
// L'ordinateur commence (2-5) et joue, puis le joueur ; ensuite
|
|
// l'ordinateur pense à doubler, et le match lui est donné vu de lui.
|
|
await game.playComputerTurn();
|
|
await game.roll();
|
|
await game.playHalfMove({ from: 8, to: 5 });
|
|
await game.playHalfMove({ from: 6, to: 5 });
|
|
await game.confirmMove();
|
|
await game.playComputerTurn();
|
|
assert.deepEqual(levels, ["intermediate"]);
|
|
assert.deepEqual(cubes[0]?.match, {
|
|
length: 5,
|
|
score: { onRoll: 3, opponent: 1 },
|
|
crawford: false,
|
|
});
|
|
});
|
|
|
|
test("une partie de match finie : le moteur dit le score, la fin du match et la partie Crawford suivante", async () => {
|
|
const winning: Board = {
|
|
onRoll: { points: [], bar: 0, off: 15, pipCount: 0 },
|
|
opponent: { points: [], bar: 0, off: 0, pipCount: 100 },
|
|
swappedPosition: "SWAPPED",
|
|
result: { winner: "onRoll", kind: "gammon", points: 2 },
|
|
};
|
|
const engine = fakeEngine({ board: async () => winning });
|
|
const game = Game.start(engine, {
|
|
dice: fixedDice([5, 2]),
|
|
level: "advanced",
|
|
match: { length: 3, score: { player: 0, computer: 0 }, crawford: false },
|
|
});
|
|
assert.equal(game.nextGame, null);
|
|
await game.playHalfMove({ from: 8, to: 5 });
|
|
await game.playHalfMove({ from: 6, to: 5 });
|
|
await game.confirmMove();
|
|
const end = game.events.at(-1);
|
|
assert.ok(end?.type === "GameEnded");
|
|
assert.deepEqual(end.match, { score: { player: 2, computer: 0 }, over: false, crawford: true });
|
|
const next = game.nextGame;
|
|
assert.deepEqual(next, {
|
|
level: "advanced",
|
|
match: { length: 3, score: { player: 2, computer: 0 }, crawford: true },
|
|
});
|
|
|
|
// La partie Crawford, gagnée : le match est fini, plus de partie suivante.
|
|
const crawford = Game.start(engine, { dice: fixedDice([5, 2]), ...(game.nextGame ?? {}) });
|
|
await crawford.playHalfMove({ from: 8, to: 5 });
|
|
await crawford.playHalfMove({ from: 6, to: 5 });
|
|
await crawford.confirmMove();
|
|
assert.deepEqual(crawford.state.matchAfter, {
|
|
score: { player: 4, computer: 0 },
|
|
over: true,
|
|
crawford: false,
|
|
});
|
|
assert.equal(crawford.nextGame, null);
|
|
});
|
|
|
|
test("passer un double dans un match : le moteur compte le match", async () => {
|
|
const engine = fakeEngine({ levelDouble: async () => true });
|
|
const game = Game.start(engine, {
|
|
dice: fixedDice([2, 5, 3, 1]),
|
|
match: { length: 3, score: { player: 0, computer: 1 }, crawford: false },
|
|
});
|
|
// L'ordinateur joue son jet d'ouverture, le joueur son coup, puis
|
|
// l'ordinateur double.
|
|
await game.playComputerTurn();
|
|
await game.roll();
|
|
await game.playHalfMove({ from: 8, to: 5 });
|
|
await game.playHalfMove({ from: 6, to: 5 });
|
|
await game.confirmMove();
|
|
await game.playComputerTurn();
|
|
assert.equal(game.state.phase, "doubled");
|
|
await game.drop();
|
|
assert.deepEqual(game.state.matchAfter, {
|
|
score: { player: 0, computer: 2 },
|
|
over: false,
|
|
crawford: true,
|
|
});
|
|
});
|
|
|
|
test("une partie libre n'a ni match ni partie suivante", () => {
|
|
const game = Game.start(fakeEngine(), { dice: fixedDice([3, 1]) });
|
|
assert.equal(game.state.match, null);
|
|
assert.equal(game.state.level, DEFAULT_LEVEL);
|
|
assert.equal(game.nextGame, null);
|
|
});
|
|
|
|
test("migration : un journal de la version 1 se reprend en version 2, au niveau 2 plis, en partie libre", async () => {
|
|
// Un journal tel que l'étape 1b l'enregistrait : version 1, GameStarted
|
|
// sans level ni match, un coup joué, une branche.
|
|
const v1 = {
|
|
events: [
|
|
{
|
|
id: "1",
|
|
parent: null,
|
|
version: 1,
|
|
type: "GameStarted",
|
|
position: START_POSITION,
|
|
cube: { value: 1, owner: null },
|
|
firstTurn: null,
|
|
},
|
|
{ id: "2", parent: "1", version: 1, type: "DiceRolled", side: null, roll: [3, 1] },
|
|
{
|
|
id: "3",
|
|
parent: "2",
|
|
version: 1,
|
|
type: "MovePlayed",
|
|
side: "player",
|
|
halfMoves: [
|
|
{ from: 8, to: 5 },
|
|
{ from: 6, to: 5 },
|
|
],
|
|
position: "SWAPPED",
|
|
},
|
|
{ id: "4", parent: "1", version: 1, type: "DiceRolled", side: null, roll: [5, 2] },
|
|
],
|
|
head: "3",
|
|
} as unknown as GameJournal;
|
|
const before = replay(currentLine(v1));
|
|
const migrated = migrateJournal(v1);
|
|
assert.ok(migrated.events.every((event) => event.version === EVENT_VERSION));
|
|
assert.equal(migrated.head, "3");
|
|
const start = migrated.events[0];
|
|
assert.ok(start?.type === "GameStarted");
|
|
assert.equal(start.level, "2ply");
|
|
assert.equal(start.match, undefined);
|
|
// Rien d'autre ne change : mêmes événements, même arbre, même état.
|
|
const strip = (journal: GameJournal) =>
|
|
journal.events.map(({ version: _, ...event }) => {
|
|
const { level: _level, ...rest } = event as typeof event & { level?: unknown };
|
|
return rest;
|
|
});
|
|
assert.deepEqual(strip(migrated), strip(v1));
|
|
assert.deepEqual(replay(currentLine(migrated)), { ...before, level: "2ply" });
|
|
// Une version courante passe telle quelle.
|
|
assert.equal(migrateJournal(migrated), migrated);
|
|
|
|
const resumed = Game.resume(fakeEngine(), JSON.parse(JSON.stringify(v1)));
|
|
assert.equal(resumed.state.level, "2ply");
|
|
assert.equal(resumed.state.match, null);
|
|
assert.ok(resumed.journal.events.every((event) => event.version === EVENT_VERSION));
|
|
// La partie continue en version 2.
|
|
await resumed.playComputerTurn();
|
|
assert.ok(resumed.journal.events.every((event) => event.version === EVENT_VERSION));
|
|
});
|