Scénario « Jouer une partie » exécuté ; garde-fous d'architecture
Les étapes de docs/scenarios/jouer-une-partie.feature pilotent le domaine sur le vrai moteur WebAssembly, avec des dés imposés (Cucumber, sous Node). Deux retouches du scénario : 8/4 avec 3-1 est un coup permis (8/5/4), remplacé par 13/12 (case tenue par l'ordinateur) ; « c'est ensuite au tour de l'ordinateur » distingue le constat de la situation de départ du même nom. dependency-cruiser vérifie que le domaine n'importe que le domaine, que l'interface n'appelle pas le moteur et qu'il n'y a pas de cycle ; tests/test_web.sh prouve aussi que des violations volontaires sont détectées. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
602d0eafed
commit
1f9981dc63
@@ -0,0 +1,242 @@
|
||||
// Étapes du scénario docs/scenarios/jouer-une-partie.feature (ADR 0008) :
|
||||
// elles pilotent le domaine, branché sur le vrai moteur WebAssembly, avec des
|
||||
// dés imposés pour que chaque exécution soit identique.
|
||||
//
|
||||
// Copyright (C) 2026 les auteurs d'Ustagammon.
|
||||
// Licence : GPLv3 ou ultérieure (voir LICENSE).
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { Before, BeforeAll, Given, setDefaultTimeout, Then, When } from "@cucumber/cucumber";
|
||||
import type { Engine, Move } from "../../src/domain/game/engine.js";
|
||||
import type { GameEvent } from "../../src/domain/game/events.js";
|
||||
import { Game, STANDARD_PLY } from "../../src/domain/game/game.js";
|
||||
import { type PositionId, parseHalfMoves, START_POSITION } from "../../src/domain/game/position.js";
|
||||
import { fixedDice } from "../../src/domain/game/roll.js";
|
||||
import type { GameState } from "../../src/domain/game/state.js";
|
||||
import { loadNodeEngine } from "../support/node-engine.js";
|
||||
|
||||
// Le chargement du moteur prend une à deux secondes sur le PC de 2011.
|
||||
setDefaultTimeout(30_000);
|
||||
|
||||
// Course où l'ordinateur, au trait, mène nettement : le moteur lui recommande
|
||||
// de doubler, et au joueur de prendre (position de référence
|
||||
// « cube-double-take », tests/reference/positions.json).
|
||||
const COMPUTER_DOUBLES = "6O5uAAC4uzsAAA";
|
||||
|
||||
// Le joueur, au trait, n'a plus que deux pions, sur ses cases 6 et 5 ;
|
||||
// l'ordinateur n'en a sorti aucun.
|
||||
const PLAYER_ALMOST_DONE = "/38AAABQAAAAAA";
|
||||
|
||||
let engine: Engine;
|
||||
let game: Game;
|
||||
let accepted: boolean;
|
||||
let positionBefore: PositionId;
|
||||
let computerTurn: { before: GameState; ms: number };
|
||||
|
||||
BeforeAll(async () => {
|
||||
engine = await loadNodeEngine();
|
||||
});
|
||||
|
||||
Before(() => {
|
||||
accepted = false;
|
||||
});
|
||||
|
||||
/** Joue les demi-coups un par un ; au premier refus, le coup est abandonné. */
|
||||
async function play(text: string): Promise<boolean> {
|
||||
positionBefore = game.displayedPosition;
|
||||
for (const halfMove of parseHalfMoves(text)) {
|
||||
if (!(await game.playHalfMove(halfMove))) {
|
||||
game.cancelHalfMoves();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function lastEvent(): GameEvent {
|
||||
const event = game.events.at(-1);
|
||||
assert.ok(event !== undefined, "aucun événement");
|
||||
return event;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------- Commencer une partie
|
||||
|
||||
When("vous commencez une nouvelle partie", () => {
|
||||
// Égalité au premier jet d'ouverture, puis 5 contre 2.
|
||||
game = Game.start(engine, { dice: fixedDice([4, 4, 5, 2]) });
|
||||
});
|
||||
|
||||
Then("les pions sont en position de départ", async () => {
|
||||
assert.equal(game.state.position, START_POSITION);
|
||||
const board = await engine.board(game.state.position, { value: 1, owner: "centered" });
|
||||
const start = Array.from({ length: 24 }, (_, i) => ({ 5: 5, 7: 3, 12: 5, 23: 2 })[i] ?? 0);
|
||||
for (const side of [board.onRoll, board.opponent]) {
|
||||
assert.deepEqual(side.points, start);
|
||||
assert.equal(side.bar, 0);
|
||||
assert.equal(side.off, 0);
|
||||
}
|
||||
});
|
||||
|
||||
Then("le premier jet de dés désigne qui commence", () => {
|
||||
const opening = game.events.filter((e) => e.type === "DiceRolled" && e.side === null);
|
||||
const last = opening.at(-1);
|
||||
assert.ok(last?.type === "DiceRolled");
|
||||
// Les jets d'ouverture égaux ont été relancés.
|
||||
for (const tie of opening.slice(0, -1)) {
|
||||
assert.ok(tie.type === "DiceRolled" && tie.roll[0] === tie.roll[1]);
|
||||
}
|
||||
const [playerDie, computerDie] = last.roll;
|
||||
assert.notEqual(playerDie, computerDie);
|
||||
assert.equal(game.state.turn, playerDie > computerDie ? "player" : "computer");
|
||||
assert.equal(game.state.phase, "moving");
|
||||
assert.deepEqual(game.state.roll, last.roll);
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------ Jouer un coup
|
||||
|
||||
Given("c'est à vous de jouer avec le jet {int}-{int}", (first: number, second: number) => {
|
||||
// Le jet d'ouverture : votre dé, puis celui de l'ordinateur.
|
||||
game = Game.start(engine, { dice: fixedDice([first, second]) });
|
||||
assert.equal(game.state.turn, "player");
|
||||
assert.deepEqual(game.state.roll, [first, second]);
|
||||
});
|
||||
|
||||
When(/^vous jouez (.+)$/, async (text: string) => {
|
||||
accepted = await play(text);
|
||||
});
|
||||
|
||||
When(/^vous essayez de jouer (.+)$/, async (text: string) => {
|
||||
accepted = await play(text);
|
||||
});
|
||||
|
||||
Then("votre coup est accepté", () => {
|
||||
assert.equal(accepted, true);
|
||||
const event = lastEvent();
|
||||
assert.ok(event.type === "MovePlayed");
|
||||
assert.equal(event.side, "player");
|
||||
});
|
||||
|
||||
Then("c'est ensuite au tour de l'ordinateur", () => {
|
||||
assert.equal(game.state.turn, "computer");
|
||||
assert.equal(game.state.phase, "rolling");
|
||||
});
|
||||
|
||||
Then("votre coup est refusé", () => {
|
||||
assert.equal(accepted, false);
|
||||
});
|
||||
|
||||
Then("la position n'a pas changé", () => {
|
||||
assert.equal(game.displayedPosition, positionBefore);
|
||||
assert.equal(game.state.position, positionBefore);
|
||||
assert.equal(game.state.turn, "player");
|
||||
assert.equal(game.state.phase, "moving");
|
||||
assert.equal(lastEvent().type, "DiceRolled");
|
||||
});
|
||||
|
||||
// ------------------------------------------------------- L'ordinateur joue
|
||||
|
||||
Given("c'est au tour de l'ordinateur", async () => {
|
||||
// Vous commencez avec 3-1, puis l'ordinateur tirera 6-4.
|
||||
game = Game.start(engine, { dice: fixedDice([3, 1, 6, 4]) });
|
||||
assert.equal(await play("8/5 6/5"), true);
|
||||
assert.equal(game.state.turn, "computer");
|
||||
});
|
||||
|
||||
When("ses dés sont tirés", async () => {
|
||||
const before = game.state;
|
||||
const start = performance.now();
|
||||
await game.playComputerTurn();
|
||||
computerTurn = { before, ms: performance.now() - start };
|
||||
});
|
||||
|
||||
Then(
|
||||
"il joue le coup recommandé par le moteur en moins de {int} secondes",
|
||||
async (seconds: number) => {
|
||||
const rolled = game.events.findLast((e) => e.type === "DiceRolled");
|
||||
const played = lastEvent();
|
||||
assert.ok(rolled?.type === "DiceRolled" && rolled.side === "computer");
|
||||
assert.ok(played.type === "MovePlayed" && played.side === "computer");
|
||||
const best: Move | null = await engine.bestMove(
|
||||
computerTurn.before.position,
|
||||
{ value: 1, owner: "centered" },
|
||||
rolled.roll,
|
||||
STANDARD_PLY,
|
||||
);
|
||||
assert.ok(best !== null);
|
||||
assert.deepEqual(played.halfMoves, best.halfMoves);
|
||||
assert.ok(
|
||||
computerTurn.ms < seconds * 1000,
|
||||
`tour de l'ordinateur en ${Math.round(computerTurn.ms)} ms`,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// ------------------------------------------------------------------ Videau
|
||||
|
||||
Given("l'ordinateur vous propose de doubler", async () => {
|
||||
game = Game.start(engine, { setup: { position: COMPUTER_DOUBLES, firstTurn: "computer" } });
|
||||
await game.playComputerTurn();
|
||||
const event = lastEvent();
|
||||
assert.ok(event.type === "CubeOffered" && event.side === "computer");
|
||||
assert.equal(game.state.phase, "doubled");
|
||||
});
|
||||
|
||||
When("vous prenez", () => {
|
||||
game.take();
|
||||
});
|
||||
|
||||
When("vous passez", () => {
|
||||
game.drop();
|
||||
});
|
||||
|
||||
Then("la partie continue à valeur doublée", () => {
|
||||
assert.equal(game.state.cube.value, 2);
|
||||
assert.equal(game.state.result, null);
|
||||
assert.equal(game.state.phase, "rolling");
|
||||
});
|
||||
|
||||
Then("le videau est de votre côté : vous seul pourrez redoubler", async () => {
|
||||
const { cube, position } = game.state;
|
||||
assert.equal(cube.owner, "player");
|
||||
// Le moteur le confirme : l'ordinateur, au trait, ne peut plus doubler…
|
||||
const computer = await engine.cubeDecision(
|
||||
position,
|
||||
{ value: 2, owner: "opponent" },
|
||||
STANDARD_PLY,
|
||||
);
|
||||
assert.equal(computer.available, false);
|
||||
// … alors que vous, à votre tour, le pourrez.
|
||||
const board = await engine.board(position, { value: 2, owner: "opponent" });
|
||||
const player = await engine.cubeDecision(
|
||||
board.swappedPosition,
|
||||
{ value: 2, owner: "onRoll" },
|
||||
STANDARD_PLY,
|
||||
);
|
||||
assert.equal(player.available, true);
|
||||
});
|
||||
|
||||
Then("la partie s'arrête", () => {
|
||||
assert.equal(game.state.phase, "over");
|
||||
assert.equal(lastEvent().type, "GameEnded");
|
||||
});
|
||||
|
||||
Then("vous perdez la partie à sa valeur d'avant le double", () => {
|
||||
assert.deepEqual(game.state.result, { winner: "computer", kind: "single", points: 1 });
|
||||
});
|
||||
|
||||
// --------------------------------------------------------- Fin de partie
|
||||
|
||||
Given("un joueur a sorti tous ses pions", async () => {
|
||||
game = Game.start(engine, {
|
||||
setup: { position: PLAYER_ALMOST_DONE, firstTurn: "player" },
|
||||
dice: fixedDice([6, 5]),
|
||||
});
|
||||
await game.roll();
|
||||
assert.equal(await play("6/off 5/off"), true);
|
||||
});
|
||||
|
||||
Then("le résultat indique le gagnant et les points gagnés", () => {
|
||||
// Gammon (l'ordinateur n'a sorti aucun pion), mais 1 point seulement :
|
||||
// règle Jacoby, le videau n'a pas bougé.
|
||||
assert.deepEqual(game.state.result, { winner: "player", kind: "gammon", points: 1 });
|
||||
});
|
||||
Reference in New Issue
Block a user