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
@@ -83,7 +83,7 @@ Given("vous avez choisi de jouer tout de suite", () => {
|
||||
});
|
||||
|
||||
Given("une partie est en cours", () => {
|
||||
game = Game.start(engine, { dice: seededDice(11), ply: 0 });
|
||||
game = Game.start(engine, { dice: seededDice(11), level: "0ply" });
|
||||
settings = { ...settings, gamesStarted: settings.gamesStarted + 1 };
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
// Étapes du scénario docs/scenarios/jouer-un-match.feature (ADR 0008) : le
|
||||
// choix de la partie passe par les réglages de l'écran « Nouvelle partie »
|
||||
// (settings.ts) et la session d'enregistrement ; les parties et le match,
|
||||
// par le domaine branché sur le vrai moteur, avec des dés imposés.
|
||||
//
|
||||
// Copyright (C) 2026 les auteurs d'Ustagammon.
|
||||
// Licence : GPLv3 ou ultérieure (voir LICENSE).
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { BeforeAll, Given, setDefaultTimeout, Then, When } from "@cucumber/cucumber";
|
||||
import type { Engine } from "../../src/domain/game/engine.js";
|
||||
import { engineCube, Game, type NewGame } from "../../src/domain/game/game.js";
|
||||
import { firstGameOf, type Match } from "../../src/domain/game/match.js";
|
||||
import { START_POSITION } from "../../src/domain/game/position.js";
|
||||
import { fixedDice } from "../../src/domain/game/roll.js";
|
||||
import { createGameLibrary } from "../../src/storage/game-library.js";
|
||||
import { createGameSession, type GameSession } from "../../src/storage/game-session.js";
|
||||
import { createKeyValueBackend } from "../../src/storage/key-value-backend.js";
|
||||
import { memoryStorage } from "../../src/storage/open-library.js";
|
||||
import { chosenGame, parseSettings, type Settings } from "../../src/ui/settings.js";
|
||||
import { loadNodeEngine } from "../support/node-engine.js";
|
||||
import { playToTheEnd, seededDice } from "../support/play.js";
|
||||
|
||||
setDefaultTimeout(120_000);
|
||||
|
||||
// Course : le joueur au trait n'a plus que deux dames, sur ses points 6 et
|
||||
// 5 ; son adversaire a ses quinze dames sur son point 1, aucune sortie. Avec
|
||||
// 6-5, le joueur au trait gagne un gammon.
|
||||
const GAMMON_NEXT_ROLL = "/38AAABQAAAAAA";
|
||||
// La même, mais l'adversaire a déjà sorti quatre dames : le joueur au trait
|
||||
// gagne une partie simple avec 6-5.
|
||||
const SINGLE_NEXT_ROLL = "/wcAAAAFAAAAAA";
|
||||
|
||||
let engine: Engine;
|
||||
let settings: Settings;
|
||||
let session: GameSession;
|
||||
let game: Game;
|
||||
|
||||
BeforeAll(async () => {
|
||||
engine = await loadNodeEngine();
|
||||
});
|
||||
|
||||
/** Joue le jet du joueur : le premier coup légal, demi-coup par demi-coup. */
|
||||
async function playFirstMove(g: Game): Promise<void> {
|
||||
await g.roll();
|
||||
const state = g.state;
|
||||
assert.ok(state.roll !== null);
|
||||
const [move] = await engine.legalMoves(state.position, state.roll);
|
||||
assert.ok(move !== undefined);
|
||||
for (const halfMove of move.halfMoves) {
|
||||
assert.ok(await g.playHalfMove(halfMove));
|
||||
}
|
||||
await g.confirmMove();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ Le niveau
|
||||
|
||||
Given("vous ouvrez l'écran « Nouvelle partie » pour la première fois", () => {
|
||||
settings = parseSettings(null);
|
||||
session = createGameSession(createGameLibrary(createKeyValueBackend(memoryStorage())), {
|
||||
start: (newGame) => Game.start(engine, { dice: fixedDice([3, 1, 4, 2]), ...newGame }),
|
||||
resume: (journal) => Game.resume(engine, journal),
|
||||
});
|
||||
});
|
||||
|
||||
Then(
|
||||
"une partie libre contre un ordinateur Intermédiaire, avec le conseil du maître, est proposée",
|
||||
() => {
|
||||
assert.equal(settings.gameKind, "free");
|
||||
assert.equal(settings.level, "intermediate");
|
||||
assert.equal(settings.masterAdvice, true);
|
||||
assert.deepEqual(chosenGame(settings), { level: "intermediate" });
|
||||
},
|
||||
);
|
||||
|
||||
When("vous choisissez le niveau Champion et commencez la partie", () => {
|
||||
settings = { ...settings, level: "champion" };
|
||||
game = session.newGame(chosenGame(settings));
|
||||
session.save(game.journal);
|
||||
});
|
||||
|
||||
Then("l'ordinateur joue au niveau {int} plis de GNU Backgammon", async (plies: number) => {
|
||||
assert.equal(game.state.level, `${plies}ply`);
|
||||
// Le joueur commence avec 3-1 ; l'ordinateur répond avec 4-2, et joue
|
||||
// le coup que le moteur donne pour ce niveau.
|
||||
await game.playHalfMove({ from: 8, to: 5 });
|
||||
await game.playHalfMove({ from: 6, to: 5 });
|
||||
await game.confirmMove();
|
||||
const before = game.state;
|
||||
await game.playComputerTurn();
|
||||
const played = game.events.findLast((e) => e.type === "MovePlayed");
|
||||
assert.ok(played?.type === "MovePlayed" && played.side === "computer");
|
||||
const roll = game.events.findLast((e) => e.type === "DiceRolled");
|
||||
assert.ok(roll?.type === "DiceRolled");
|
||||
const expected = await engine.levelMove(
|
||||
before.position,
|
||||
engineCube(before, "computer"),
|
||||
roll.roll,
|
||||
"2ply",
|
||||
);
|
||||
assert.deepEqual(played.halfMoves, expected?.halfMoves ?? []);
|
||||
session.save(game.journal);
|
||||
});
|
||||
|
||||
Then("la partie reprise garde ce niveau", async () => {
|
||||
const resumed = await session.restore();
|
||||
assert.ok(resumed !== null);
|
||||
assert.equal(resumed.state.level, "2ply");
|
||||
});
|
||||
|
||||
Then("vos choix sont proposés à la prochaine nouvelle partie", () => {
|
||||
const again = parseSettings(JSON.stringify(settings));
|
||||
assert.equal(again.level, "champion");
|
||||
assert.equal(chosenGame(again).level, "2ply");
|
||||
});
|
||||
|
||||
// ------------------------------------------------------- Un match entier
|
||||
|
||||
// Les parties du match, dans l'ordre.
|
||||
let games: Game[] = [];
|
||||
|
||||
Given("vous commencez un match en {int} points contre un ordinateur Débutant", (n: number) => {
|
||||
settings = { ...parseSettings(null), gameKind: "match", matchLength: n, level: "beginner" };
|
||||
games = [];
|
||||
});
|
||||
|
||||
When("le match se joue jusqu'au bout, partie après partie", async () => {
|
||||
// Des dés reproductibles, partagés par toutes les parties du match.
|
||||
const dice = seededDice(3);
|
||||
let next: NewGame | null = chosenGame(settings);
|
||||
while (next !== null) {
|
||||
assert.ok(games.length < 20, "le match ne finit pas");
|
||||
const g = Game.start(engine, { dice, ...next });
|
||||
await playToTheEnd(g, engine);
|
||||
games.push(g);
|
||||
next = g.nextGame;
|
||||
}
|
||||
});
|
||||
|
||||
Then("le match se termine dès qu'un joueur atteint {int} points", (n: number) => {
|
||||
const last = games.at(-1)?.state;
|
||||
assert.ok(last?.matchAfter);
|
||||
assert.equal(last.matchAfter.over, true);
|
||||
const { player, computer } = last.matchAfter.score;
|
||||
assert.ok(Math.max(player, computer) >= n);
|
||||
// Avant la dernière partie, personne n'avait encore les points.
|
||||
for (const g of games.slice(0, -1)) {
|
||||
assert.equal(g.state.matchAfter?.over, false);
|
||||
const score = g.state.matchAfter?.score;
|
||||
assert.ok(score !== undefined && Math.max(score.player, score.computer) < n);
|
||||
}
|
||||
});
|
||||
|
||||
Then("chaque partie commence avec le score laissé par la précédente", () => {
|
||||
assert.deepEqual(games[0]?.state.match?.score, { player: 0, computer: 0 });
|
||||
for (let i = 1; i < games.length; i++) {
|
||||
assert.deepEqual(games[i]?.state.match?.score, games[i - 1]?.state.matchAfter?.score);
|
||||
}
|
||||
});
|
||||
|
||||
Then("la partie Crawford s'est jouée sans videau", () => {
|
||||
const crawford = games.filter((g) => g.state.match?.crawford === true);
|
||||
assert.equal(crawford.length, 1, "une partie Crawford, et une seule");
|
||||
for (const g of crawford) {
|
||||
assert.ok(!g.events.some((e) => e.type === "CubeOffered"));
|
||||
assert.equal(g.state.cube.value, 1);
|
||||
}
|
||||
});
|
||||
|
||||
// ------------------------------------------------------- La partie Crawford
|
||||
|
||||
let crawfordGame: Game;
|
||||
|
||||
Given("un match en 3 points commence et que l'ordinateur va gagner un gammon", () => {
|
||||
game = Game.start(engine, {
|
||||
setup: { position: GAMMON_NEXT_ROLL, firstTurn: "computer" },
|
||||
level: "beginner",
|
||||
match: firstGameOf(3),
|
||||
dice: fixedDice([6, 5]),
|
||||
});
|
||||
});
|
||||
|
||||
When("l'ordinateur gagne la partie", async () => {
|
||||
await game.playComputerTurn();
|
||||
assert.deepEqual(game.state.result, { winner: "computer", kind: "gammon", points: 2 });
|
||||
});
|
||||
|
||||
Then("l'ordinateur mène le match {int} à {int}", (computer: number, player: number) => {
|
||||
assert.deepEqual(game.state.matchAfter?.score, { player, computer });
|
||||
});
|
||||
|
||||
Then("la partie suivante est la partie Crawford", () => {
|
||||
assert.equal(game.nextGame?.match?.crawford, true);
|
||||
});
|
||||
|
||||
When("la partie Crawford commence", () => {
|
||||
const next = game.nextGame;
|
||||
assert.ok(next?.match !== undefined);
|
||||
crawfordGame = Game.start(engine, {
|
||||
...next,
|
||||
setup: { position: SINGLE_NEXT_ROLL, firstTurn: "player" },
|
||||
dice: fixedDice([6, 5]),
|
||||
});
|
||||
});
|
||||
|
||||
Then("personne ne peut doubler pendant cette partie", async () => {
|
||||
assert.equal(await crawfordGame.canDouble(), false);
|
||||
// L'ordinateur non plus, dans la même position vue de lui.
|
||||
const state = crawfordGame.state;
|
||||
const board = await engine.board(state.position, engineCube(state, "player"));
|
||||
assert.equal(
|
||||
await engine.levelDouble(board.swappedPosition, engineCube(state, "computer"), state.level),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
When("vous gagnez la partie Crawford", async () => {
|
||||
await playFirstMove(crawfordGame);
|
||||
assert.deepEqual(crawfordGame.state.matchAfter, {
|
||||
score: { player: 1, computer: 2 },
|
||||
over: false,
|
||||
crawford: false,
|
||||
});
|
||||
});
|
||||
|
||||
Then("vous pouvez de nouveau doubler dans la partie suivante", async () => {
|
||||
const next = crawfordGame.nextGame;
|
||||
assert.ok(next?.match !== undefined);
|
||||
const match: Match = next.match;
|
||||
assert.equal(match.crawford, false);
|
||||
const after = Game.start(engine, {
|
||||
...next,
|
||||
setup: { position: START_POSITION, firstTurn: "player" },
|
||||
});
|
||||
assert.equal(await after.canDouble(), true);
|
||||
});
|
||||
|
||||
// ------------------------------------------------------- La règle Jacoby
|
||||
|
||||
Given("vous allez gagner un gammon sans que personne ait doublé", () => {
|
||||
// La position est choisie à chaque partie, ci-dessous.
|
||||
});
|
||||
|
||||
When("vous gagnez une partie libre", async () => {
|
||||
game = Game.start(engine, {
|
||||
setup: { position: GAMMON_NEXT_ROLL, firstTurn: "player" },
|
||||
dice: fixedDice([6, 5]),
|
||||
});
|
||||
await playFirstMove(game);
|
||||
});
|
||||
|
||||
When("vous gagnez la même partie dans un match", async () => {
|
||||
game = Game.start(engine, {
|
||||
setup: { position: GAMMON_NEXT_ROLL, firstTurn: "player" },
|
||||
match: firstGameOf(5),
|
||||
dice: fixedDice([6, 5]),
|
||||
});
|
||||
await playFirstMove(game);
|
||||
});
|
||||
|
||||
Then("vous marquez {int} point(s)", (points: number) => {
|
||||
assert.equal(game.state.result?.winner, "player");
|
||||
assert.equal(game.state.result?.kind, "gammon");
|
||||
assert.equal(game.state.result?.points, points);
|
||||
});
|
||||
@@ -426,8 +426,8 @@ When("vous prenez", () => {
|
||||
game.take();
|
||||
});
|
||||
|
||||
When("vous passez", () => {
|
||||
game.drop();
|
||||
When("vous passez", async () => {
|
||||
await game.drop();
|
||||
});
|
||||
|
||||
Then("la partie continue à valeur doublée", () => {
|
||||
|
||||
@@ -42,7 +42,7 @@ Before(async () => {
|
||||
|
||||
/** Commence une partie, comme l'application : un nouvel identifiant. */
|
||||
function startGame(): void {
|
||||
game = Game.start(engine, { dice: seededDice(nextSeed), ply: 0 });
|
||||
game = Game.start(engine, { dice: seededDice(nextSeed), level: "0ply" });
|
||||
shownId = `partie-${nextSeed++}`;
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ async function reopen(): Promise<void> {
|
||||
library = (await openGameLibrary({ localStorage: storage })).library;
|
||||
const saved = await library.current();
|
||||
assert.ok(saved !== null, "aucune partie en cours");
|
||||
game = Game.resume(engine, saved.journal, { ply: 0 });
|
||||
game = Game.resume(engine, saved.journal, { level: "0ply" });
|
||||
shownId = saved.summary.id;
|
||||
}
|
||||
|
||||
|
||||
@@ -45,15 +45,10 @@ BeforeAll(async () => {
|
||||
|
||||
Before(() => {
|
||||
library = createGameLibrary(createKeyValueBackend(memoryStorage()));
|
||||
session = createGameSession(
|
||||
library,
|
||||
{
|
||||
start: (setup) =>
|
||||
Game.start(engine, { dice: seededDice(7), ply: 0, ...(setup ? { setup } : {}) }),
|
||||
resume: (journal) => Game.resume(engine, journal, { ply: 0 }),
|
||||
},
|
||||
"0ply",
|
||||
);
|
||||
session = createGameSession(library, {
|
||||
start: (newGame) => Game.start(engine, { dice: seededDice(7), level: "0ply", ...newGame }),
|
||||
resume: (journal) => Game.resume(engine, journal, { level: "0ply" }),
|
||||
});
|
||||
shared = null;
|
||||
failure = null;
|
||||
});
|
||||
@@ -131,7 +126,7 @@ Then("votre partie n'a pas changé", () => {
|
||||
|
||||
When("vous choisissez « Jouer à partir d'ici »", () => {
|
||||
assert.ok(shared !== null);
|
||||
game = session.newGame(setupOf(shared));
|
||||
game = session.newGame({ setup: setupOf(shared) });
|
||||
session.save(game.journal);
|
||||
});
|
||||
|
||||
|
||||
@@ -48,14 +48,10 @@ let oldId: string | null;
|
||||
Given("une partie est en cours depuis quelques coups", async () => {
|
||||
library = createGameLibrary(createKeyValueBackend(memoryStorage()));
|
||||
let dice = fixedDice([3, 1, 6, 4, 5, 2]);
|
||||
session = createGameSession(
|
||||
library,
|
||||
{
|
||||
start: () => Game.start(engine, { dice }),
|
||||
resume: (journal) => Game.resume(engine, journal, { dice }),
|
||||
},
|
||||
"2ply",
|
||||
);
|
||||
session = createGameSession(library, {
|
||||
start: () => Game.start(engine, { dice }),
|
||||
resume: (journal) => Game.resume(engine, journal, { dice }),
|
||||
});
|
||||
game = session.newGame();
|
||||
dice = () => 1 + Math.floor(Math.random() * 6);
|
||||
for (const halfMove of parseHalfMoves("8/5 6/5")) {
|
||||
@@ -178,7 +174,7 @@ Given("une nouvelle version d'Ustagammon est disponible", () => {
|
||||
|
||||
Given("vous avez déjà déplacé un pion de votre coup", async () => {
|
||||
// Jet d'ouverture 3-1 : vous commencez.
|
||||
game = Game.start(engine, { dice: fixedDice([3, 1, 6, 4, 5, 2]), ply: 0 });
|
||||
game = Game.start(engine, { dice: fixedDice([3, 1, 6, 4, 5, 2]), level: "0ply" });
|
||||
assert.equal(game.state.turn, "player");
|
||||
assert.ok(await game.playHalfMove({ from: 8, to: 5 }));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user