Domaine : match, niveau de l'ordinateur, événements en version 2
Partie de match (score et Crawford dits par le moteur), niveau choisi porté par GameStarted, ordinateur joué comme ComputerTurn de gnubg ; migration des journaux de la version 1 ; scénarios, glossaire, ADR 0007 et 0013 amendés. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c0f827c5ba
commit
339d699488
+198
-16
@@ -10,14 +10,24 @@ import type {
|
||||
Board,
|
||||
CubeDecision,
|
||||
Engine,
|
||||
EngineCube,
|
||||
HalfMovesCheck,
|
||||
Level,
|
||||
Move,
|
||||
} from "../src/domain/game/engine.js";
|
||||
import { appendEvent, currentLine, EMPTY_JOURNAL, goBackTo } from "../src/domain/game/events.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 { replay } from "../src/domain/game/state.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. */
|
||||
@@ -51,6 +61,17 @@ function fakeEngine(overrides: Partial<Engine> = {}): Engine {
|
||||
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,
|
||||
analyseCube: async () => ({
|
||||
analysed: false,
|
||||
@@ -237,10 +258,8 @@ test("le joueur ne peut pas laisser passer son tour s'il a un coup", async () =>
|
||||
assert.equal(game.state.phase, "moving");
|
||||
});
|
||||
|
||||
test("l'ordinateur double si le moteur le recommande ; le joueur prend", async () => {
|
||||
const engine = fakeEngine({
|
||||
cubeDecision: async () => ({ available: true, double: true, take: true }),
|
||||
});
|
||||
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");
|
||||
@@ -251,22 +270,18 @@ test("l'ordinateur double si le moteur le recommande ; le joueur prend", async (
|
||||
});
|
||||
|
||||
test("le joueur passe : il perd la partie à la valeur d'avant le double", async () => {
|
||||
const engine = fakeEngine({
|
||||
cubeDecision: async () => ({ available: true, double: true, take: true }),
|
||||
});
|
||||
const engine = fakeEngine({ levelDouble: async () => true });
|
||||
const game = Game.start(engine, {
|
||||
setup: { position: "X", firstTurn: "computer", cube: { value: 2, owner: "computer" } },
|
||||
});
|
||||
await game.playComputerTurn();
|
||||
game.drop();
|
||||
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", async () => {
|
||||
const engine = fakeEngine({
|
||||
cubeDecision: async () => ({ available: true, double: true, take: false }),
|
||||
});
|
||||
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(
|
||||
@@ -283,7 +298,7 @@ test("le joueur ne peut pas doubler si le videau est chez l'ordinateur", async (
|
||||
const game = Game.start(engine, {
|
||||
setup: { position: "X", firstTurn: "player", cube: { value: 2, owner: "computer" } },
|
||||
});
|
||||
await assert.rejects(game.double(), /videau/);
|
||||
await assert.rejects(game.double(), /pas doubler/);
|
||||
assert.equal(game.events.length, 1);
|
||||
});
|
||||
|
||||
@@ -339,7 +354,7 @@ test("les événements sont immuables et versionnés ; le rejeu est déterminist
|
||||
});
|
||||
journal = appendEvent(journal, { type: "DiceRolled", side: null, roll: [5, 2] });
|
||||
const [started, rolled] = currentLine(journal);
|
||||
assert.equal(started?.version, 1);
|
||||
assert.equal(started?.version, 2);
|
||||
assert.equal(started?.parent, null);
|
||||
assert.equal(rolled?.parent, started?.id);
|
||||
assert.throws(() => {
|
||||
@@ -413,3 +428,170 @@ test("le plateau affiché est donné du point de vue de chaque camp", async () =
|
||||
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));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user