Domaine : le coup évident du double toucher, jugé par le moteur
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
cfb2e485c3
commit
6dd0e93d3d
@@ -365,6 +365,69 @@ export class Game {
|
||||
return moves.filter((_, i) => checks[i]?.valid === true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Le coup évident qui commence par le pion de from (une case du joueur,
|
||||
* ou la barre : BAR), après les demi-coups déjà saisis : les demi-coups
|
||||
* qui restent à jouer pour le compléter, ou null s'il n'y en a pas
|
||||
* (charte, ecrans-etape-1.md, 1i : le double toucher). Il existe quand
|
||||
* un seul coup légal complet commence par ce pion, ou quand le moteur ne
|
||||
* propose qu'un coup. Deux coups sont différents s'ils mènent à des
|
||||
* positions différentes, comme dans la liste des coups de gnubg.
|
||||
*
|
||||
* Aucune règle n'est jugée ici : chaque demi-coup essayé est soumis au
|
||||
* moteur (checkHalfMoves), qui dit s'il commence un coup légal et si le
|
||||
* coup est complet. On parcourt ses réponses et on s'arrête dès que deux
|
||||
* coups complets différents apparaissent.
|
||||
*/
|
||||
async obviousMove(from: number): Promise<HalfMove[] | null> {
|
||||
// Lus avant toute attente : la réponse vaut pour l'état de l'appel.
|
||||
const state = this.state;
|
||||
const pending = this.pendingHalfMoves;
|
||||
const displayed = this.displayedPosition;
|
||||
if (state.turn !== "player" || state.phase !== "moving" || !this.#diceLeft(state)) {
|
||||
return null;
|
||||
}
|
||||
const roll = rollToPlay(state);
|
||||
const limit = (roll[0] === roll[1] ? 4 : 2) - pending.length;
|
||||
const legal = await this.#engine.legalMoves(state.position, roll);
|
||||
// Un seul coup légal : il est évident, quel que soit le pion.
|
||||
const anyChecker = legal.length === 1;
|
||||
const found = new Map<PositionId, HalfMove[]>();
|
||||
const explore = async (sequence: HalfMove[], position: PositionId): Promise<void> => {
|
||||
if (found.size > 1 || sequence.length >= limit) {
|
||||
return;
|
||||
}
|
||||
const { player } = await this.#sideBoards(position, "player", state);
|
||||
const starts = sequence.length === 0 && !anyChecker ? [from] : occupiedPoints(player);
|
||||
const candidates = new Map<string, HalfMove>();
|
||||
for (const start of starts) {
|
||||
for (const die of roll) {
|
||||
const to = Math.max(start - die, OFF);
|
||||
candidates.set(`${start}/${to}`, { from: start, to });
|
||||
}
|
||||
}
|
||||
for (const halfMove of candidates.values()) {
|
||||
if (found.size > 1) {
|
||||
return;
|
||||
}
|
||||
const next = [...sequence, halfMove];
|
||||
const check = await this.#engine.checkHalfMoves(state.position, roll, [
|
||||
...pending,
|
||||
...next,
|
||||
]);
|
||||
if (!check.valid || check.position === null) {
|
||||
continue;
|
||||
}
|
||||
if (check.complete && !found.has(check.position)) {
|
||||
found.set(check.position, next);
|
||||
}
|
||||
await explore(next, check.position);
|
||||
}
|
||||
};
|
||||
await explore([], displayed);
|
||||
return found.size === 1 ? ([...found.values()][0] ?? null) : null;
|
||||
}
|
||||
|
||||
/** Le joueur peut-il doubler maintenant ? (Le moteur dit si le videau le
|
||||
* permet.) */
|
||||
async canDouble(): Promise<boolean> {
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// Le coup évident du double toucher (charte, ecrans-etape-1.md, 1i), sur le
|
||||
// vrai moteur WebAssembly : un seul coup légal complet commence par le pion
|
||||
// touché, ou le moteur ne propose qu'un coup.
|
||||
//
|
||||
// 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 { Engine } from "../src/domain/game/engine.js";
|
||||
import { Game } from "../src/domain/game/game.js";
|
||||
import { formatHalfMoves, type HalfMove } from "../src/domain/game/position.js";
|
||||
import { fixedDice } from "../src/domain/game/roll.js";
|
||||
import { loadNodeEngine } from "./support/node-engine.js";
|
||||
|
||||
// Le joueur, au trait : un pion sur sa case 6, un sur sa case 1.
|
||||
const SIX_AND_ONE = "BQAACAIAAAAAAA";
|
||||
// Le joueur, au trait : deux pions, sur ses cases 6 et 5.
|
||||
const PLAYER_ALMOST_DONE = "/38AAABQAAAAAA";
|
||||
|
||||
let engine: Engine | null = null;
|
||||
async function loaded(): Promise<Engine> {
|
||||
engine ??= await loadNodeEngine();
|
||||
return engine;
|
||||
}
|
||||
|
||||
/** Une partie à partir d'une position, joueur au trait, dés lancés. */
|
||||
async function from(position: string, dice: [number, number]): Promise<Game> {
|
||||
const game = Game.start(await loaded(), {
|
||||
setup: { position, firstTurn: "player" },
|
||||
dice: fixedDice(dice),
|
||||
});
|
||||
await game.roll();
|
||||
return game;
|
||||
}
|
||||
|
||||
function text(halfMoves: readonly HalfMove[] | null): string | null {
|
||||
return halfMoves === null ? null : formatHalfMoves(halfMoves);
|
||||
}
|
||||
|
||||
test("au départ avec 3-1 : plusieurs coups commencent par chaque pion, rien d'évident", async () => {
|
||||
const game = Game.start(await loaded(), { dice: fixedDice([3, 1]) });
|
||||
for (const point of [24, 13, 8, 6]) {
|
||||
assert.equal(await game.obviousMove(point), null, `pion de la ${point}`);
|
||||
}
|
||||
// Rien n'a été saisi.
|
||||
assert.deepEqual(game.pendingHalfMoves, []);
|
||||
});
|
||||
|
||||
test("6-1 avec des pions sur 6 et 1 : seul le pion de la 1 a un coup évident", async () => {
|
||||
const game = await from(SIX_AND_ONE, [6, 1]);
|
||||
// 1/off, puis le 6 sort l'autre pion : un seul coup.
|
||||
assert.equal(text(await game.obviousMove(1)), "1/off 6/off");
|
||||
// Depuis la 6 : 6/off 1/off, ou 6/5/off (le pion de la 1 reste).
|
||||
assert.equal(await game.obviousMove(6), null);
|
||||
// Une case sans pion n'a pas de coup.
|
||||
assert.equal(await game.obviousMove(3), null);
|
||||
});
|
||||
|
||||
test("un seul coup légal : il est évident depuis chaque pion, même à moitié saisi", async () => {
|
||||
const game = await from(PLAYER_ALMOST_DONE, [6, 6]);
|
||||
assert.equal((await (await loaded()).legalMoves(game.state.position, [6, 6])).length, 1);
|
||||
const fromFive = await game.obviousMove(5);
|
||||
const fromSix = await game.obviousMove(6);
|
||||
assert.ok(fromFive !== null && fromSix !== null);
|
||||
assert.equal(fromFive.length, 2);
|
||||
assert.equal(fromSix.length, 2);
|
||||
// Une fois 6/off saisi, il ne reste que 5/off.
|
||||
assert.equal(await game.playHalfMove({ from: 6, to: 0 }), true);
|
||||
assert.equal(text(await game.obviousMove(5)), "5/off");
|
||||
// Le coup évident se joue tel quel, demi-coup par demi-coup.
|
||||
for (const halfMove of (await game.obviousMove(5)) ?? []) {
|
||||
assert.equal(await game.playHalfMove(halfMove), true);
|
||||
}
|
||||
assert.equal(game.moveComplete, true);
|
||||
});
|
||||
|
||||
test("hors du tour de jeu du joueur, aucun coup évident", async () => {
|
||||
const game = Game.start(await loaded(), { dice: fixedDice([1, 3]) });
|
||||
assert.equal(game.state.turn, "computer");
|
||||
assert.equal(await game.obviousMove(13), null);
|
||||
});
|
||||
@@ -511,3 +511,20 @@ Then("le résultat indique le gagnant et les points gagnés", () => {
|
||||
// règle Jacoby, le videau n'a pas bougé.
|
||||
assert.deepEqual(game.state.result, { winner: "player", kind: "gammon", points: 1 });
|
||||
});
|
||||
|
||||
// ------------------------------------------------- Glisser, double toucher
|
||||
|
||||
When("vous demandez le coup évident de votre pion de la flèche {int}", async (point: number) => {
|
||||
positionBefore = game.displayedPosition;
|
||||
const obvious = await game.obviousMove(point);
|
||||
accepted = obvious !== null;
|
||||
for (const halfMove of obvious ?? []) {
|
||||
accepted &&= await game.playHalfMove(halfMove);
|
||||
}
|
||||
});
|
||||
|
||||
Then("aucun pion n'a bougé", () => {
|
||||
assert.equal(accepted, false);
|
||||
assert.deepEqual(game.pendingHalfMoves, []);
|
||||
assert.equal(game.displayedPosition, positionBefore);
|
||||
});
|
||||
|
||||
@@ -30,6 +30,24 @@ Fonctionnalité: Jouer une partie contre l'ordinateur
|
||||
Alors votre coup est accepté
|
||||
Et c'est ensuite au tour de l'ordinateur
|
||||
|
||||
@position:BQAACAIAAAAAAA
|
||||
Scénario: Jouer un pion en le faisant glisser, ou d'un double toucher
|
||||
Vous pouvez aussi faire glisser un pion jusqu'à sa case d'arrivée : il
|
||||
suit votre doigt, juste au-dessus pour rester visible, et un pion en
|
||||
pointillés marque sa place de départ. Lâché ailleurs que sur une case
|
||||
permise, il revient à sa place. Touchez deux fois de suite un pion pour
|
||||
jouer le coup évident : quand un seul coup commence par ce pion, ou que
|
||||
vous n'avez qu'un coup possible. Sinon, rien de plus ne se passe. Votre
|
||||
coup attend ensuite votre validation, comme d'habitude. Au clavier, les
|
||||
flèches gauche et droite choisissent un pion, les flèches haut et bas sa
|
||||
case d'arrivée, et Entrée le pose.
|
||||
|
||||
Étant donné que vous avez un pion sur la flèche 6 et un sur la flèche 1, avec le jet 6-1
|
||||
Quand vous demandez le coup évident de votre pion de la flèche 6
|
||||
Alors aucun pion n'a bougé
|
||||
Quand vous demandez le coup évident de votre pion de la flèche 1
|
||||
Alors vos deux pions sont sortis
|
||||
|
||||
Scénario: Pourquoi mon pion part-il dès que je le touche ?
|
||||
Un pion qui n'a qu'une case d'arrivée possible y va dès que vous le
|
||||
touchez, sans attendre un second toucher. Par exemple, avec 3-1, une fois
|
||||
|
||||
Reference in New Issue
Block a user