From ce36d738fb49bc227d67ee2993b64945cf48106c Mon Sep 17 00:00:00 2001 From: Pierre MARTIN Date: Tue, 10 Nov 2026 20:05:14 +0100 Subject: [PATCH] =?UTF-8?q?Saisie=20:=20plus=20de=20demi-coup=20demand?= =?UTF-8?q?=C3=A9=20une=20fois=20tous=20les=20d=C3=A9s=20jou=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Après un double complet, la saisie restée ouverte demandait au moteur un cinquième demi-coup, que la ligne de commande refuse (erreur dans la page, relevée par make test-browser). Test : un moteur qui refuse plus de demi-coups que de dés. Coup de l'ordinateur : la recette du navigateur vérifie la ligne « L'ordinateur a joué … » au premier coup montré. Co-Authored-By: Claude Opus 5.5 --- app/src/domain/game/game.ts | 16 +++++++++++++-- app/tests/bear-off-input.test.ts | 34 ++++++++++++++++++++++++++++++++ tests/browser/play.mjs | 6 ++++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/app/src/domain/game/game.ts b/app/src/domain/game/game.ts index 6b66e7f..70963d5 100644 --- a/app/src/domain/game/game.ts +++ b/app/src/domain/game/game.ts @@ -330,11 +330,13 @@ export class Game { * déplace un de ses pions de la valeur d'un dé (vers la sortie si le dé * dépasse la case 1) ; seuls restent ceux que le moteur déclare menant à * un coup légal. Aucune règle n'est jugée ici : même après un coup - * complet, c'est le moteur qui dit si un dé reste à jouer. + * complet, c'est le moteur qui dit si un dé reste à jouer. Seule + * exception : une fois tous les dés utilisés (diceLeft), rien n'est + * demandé, le moteur n'acceptant pas plus de demi-coups que de dés. */ async nextHalfMoves(): Promise { const state = this.state; - if (state.turn !== "player" || state.phase !== "moving") { + if (state.turn !== "player" || state.phase !== "moving" || !this.#diceLeft(state)) { return []; } const roll = rollToPlay(state); @@ -410,6 +412,9 @@ export class Game { */ async playHalfMove(halfMove: HalfMove): Promise { const state = this.#expect("player", "moving"); + if (!this.#diceLeft(state)) { + return false; + } const check = await this.#engine.checkHalfMoves(state.position, rollToPlay(state), [ ...this.pendingHalfMoves, halfMove, @@ -424,6 +429,13 @@ export class Game { return true; } + /** Il reste un dé à jouer : moins de demi-coups saisis que de dés + * (quatre pour un double). */ + #diceLeft(state: GameState): boolean { + const roll = rollToPlay(state); + return this.#pending.length < (roll[0] === roll[1] ? 4 : 2); + } + /** Défait le dernier demi-coup saisi du coup en cours (rien s'il n'y en a * pas) : il n'a jamais été dans le journal. */ undoLastHalfMove(): void { diff --git a/app/tests/bear-off-input.test.ts b/app/tests/bear-off-input.test.ts index 802c33e..c2bca54 100644 --- a/app/tests/bear-off-input.test.ts +++ b/app/tests/bear-off-input.test.ts @@ -126,6 +126,40 @@ test("6-1 en sortie : 6/off puis 1/off, comme 1/off puis 6/off", async () => { assert.equal(alone.state.turn, "computer"); }); +test("tous les dés joués, le moteur n'est plus interrogé", async () => { + // Le moteur de la page refuse plus de quatre demi-coups (erreur de la + // ligne de commande) : la partie ne doit pas en demander un cinquième + // après un double, ni un troisième après un jet simple. + const real = await loadNodeEngine(); + const engine: Engine = { + ...real, + checkHalfMoves: (position, roll, halfMoves) => { + const dice = roll[0] === roll[1] ? 4 : 2; + if (halfMoves.length > dice) { + throw new Error(`${halfMoves.length} demi-coups pour ${dice} dés`); + } + return real.checkHalfMoves(position, roll, halfMoves); + }, + }; + const double = await rolled(engine, bearOffPosition([0, 0, 2, 2, 0, 0]), [2, 2]); + for (const h of [ + { from: 4, to: 2 }, + { from: 4, to: 2 }, + { from: 3, to: 1 }, + { from: 3, to: 1 }, + ]) { + assert.equal(await double.playHalfMove(h), true); + } + assert.equal(double.moveComplete, true); + assert.deepEqual(await double.nextHalfMoves(), []); + assert.equal(await double.playHalfMove({ from: 2, to: 0 }), false); + + const simple = await rolled(engine, SIX_AND_ONE, [6, 1]); + assert.equal(await simple.playHalfMove({ from: 1, to: 0 }), true); + assert.equal(await simple.playHalfMove({ from: 6, to: 0 }), true); + assert.deepEqual(await simple.nextHalfMoves(), []); +}); + test("en sortie, tout ordre accepté par le moteur l'est par la partie", async () => { const engine = await loadNodeEngine(); // Toutes les fins de partie d'un à trois pions dans le jan intérieur, diff --git a/tests/browser/play.mjs b/tests/browser/play.mjs index 94f1cd8..2b47443 100644 --- a/tests/browser/play.mjs +++ b/tests/browser/play.mjs @@ -1193,6 +1193,12 @@ for (let actions = 0; ; actions++) { tracesSeen++; if (!moments.has("coup-ordinateur")) { moments.add("coup-ordinateur"); + // La ligne du dernier coup (portrait seulement : en paysage, le + // plateau occupe tout l'écran), dans la notation du moteur. + const played = (await page.textContent("#info")) ?? ""; + check(/^L'ordinateur a joué \d-\d : \S.*\.$/.test(played), + `coup de l'ordinateur mal écrit : « ${played} »`); + log(`coup de l'ordinateur : « ${played} »`); await review("4-coup-ordinateur"); } if (replays < 2) {