Écran de jeu : le coup de l'ordinateur dans la notation du moteur

« L'ordinateur a joué 6-4 : 6/off » au lieu de « 6/2 2/off » : la
notation vient du coup légal de gnubg qui mène à la même position
(Game.moveNotation), comme dans l'analyse et le bilan. Testé : sur une
partie analysée, chaque coup s'écrit pareil des deux côtés.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Pierre MARTIN
2026-11-10 19:37:46 +01:00
co-authored by Claude Opus 5.5
parent ab8dc512ba
commit 4e74bab3ac
3 changed files with 64 additions and 7 deletions
+31 -1
View File
@@ -27,7 +27,14 @@ import {
withAnalysesOf,
} from "./events.js";
import { firstGameOf, type Match, nextGameOf } from "./match.js";
import { BAR, type HalfMove, OFF, type PositionId, START_POSITION } from "./position.js";
import {
BAR,
formatHalfMoves,
type HalfMove,
OFF,
type PositionId,
START_POSITION,
} from "./position.js";
import { type DiceSource, type Roll, randomDice, rollDice } from "./roll.js";
import { otherSide, type Side } from "./side.js";
import { type GameState, replay } from "./state.js";
@@ -276,6 +283,29 @@ export class Game {
return { side, halfMoves, boards, hits };
}
/**
* Le coup joué à l'événement eventId (un MovePlayed), dans la notation
* du moteur : celle du coup légal de gnubg qui mène à la même position
* (« 6/off » et non « 6/5 5/off »), comme dans l'analyse et le bilan.
* À défaut (aucun coup légal ne correspond), les demi-coups ; chaîne
* vide si l'événement n'est pas un coup ou si aucun pion n'a bougé.
*/
async moveNotation(eventId: string): Promise<string> {
const line = currentLine({ ...this.#journal, head: eventId });
const event = line.at(-1);
if (event?.type !== "MovePlayed" || event.halfMoves.length === 0) {
return "";
}
const before = replay(line.slice(0, -1));
const roll = rollToPlay(before);
const [check, legal] = await Promise.all([
this.#engine.checkHalfMoves(before.position, roll, event.halfMoves),
this.#engine.legalMoves(before.position, roll),
]);
const move = legal.find((m) => m.position === check.position);
return move?.notation ?? formatHalfMoves(event.halfMoves);
}
/** Les conseils du maître déjà rattachés aux décisions du joueur, par
* identifiant d'événement. */
get advice(): Readonly<Record<string, Advice>> {
+6 -5
View File
@@ -32,7 +32,7 @@ import type { Advice } from "../domain/game/advice.js";
import type { Cube } from "../domain/game/cube.js";
import type { GameJournal } from "../domain/game/events.js";
import type { Game, MoveSteps, NewGame, SideBoards } from "../domain/game/game.js";
import { formatHalfMoves, type HalfMove } from "../domain/game/position.js";
import type { HalfMove } from "../domain/game/position.js";
import { type PositionLink, type SharedPosition, setupOf } from "../domain/game/position-link.js";
import type { SavedGameSummary } from "../domain/game/saved-game.js";
import { otherSide, type Side } from "../domain/game/side.js";
@@ -1018,7 +1018,7 @@ export function startApp(options: AppOptions): void {
while (game !== null && game.state.turn === "computer" && isPlaying(game)) {
await refresh();
await game.playComputerTurn();
info = describeComputerTurn(game);
info = await describeComputerTurn(game);
save();
played = true;
}
@@ -2500,8 +2500,9 @@ async function computerMoveToShow(game: Game): Promise<MoveSteps | null> {
return game.lastMoveSteps();
}
/** Ce qu'a fait l'ordinateur pendant son tour (sans durée de calcul). */
function describeComputerTurn(game: Game): string {
/** Ce qu'a fait l'ordinateur pendant son tour (sans durée de calcul). Son
* coup s'écrit dans la notation du moteur, comme dans l'analyse. */
async function describeComputerTurn(game: Game): Promise<string> {
const last = game.events.at(-1);
if (last?.type === "CubeOffered") {
return "";
@@ -2514,7 +2515,7 @@ function describeComputerTurn(game: Game): string {
if (played.halfMoves.length === 0) {
return TEXTS.computerCannotMove(dice);
}
return TEXTS.computerMoved(dice, formatHalfMoves(played.halfMoves));
return TEXTS.computerMoved(dice, await game.moveNotation(played.id));
}
/** Le résultat d'une partie finie, en deux parties : le titre du panneau
+27 -1
View File
@@ -30,7 +30,7 @@ import {
withAnalysesOf,
} from "../src/domain/game/events.js";
import { Game } from "../src/domain/game/game.js";
import { START_POSITION } from "../src/domain/game/position.js";
import { formatHalfMoves, START_POSITION } from "../src/domain/game/position.js";
import { emptyReview, REVIEW_VERSION } from "../src/domain/game/review.js";
import { loadNodeEngine } from "./support/node-engine.js";
import { playToTheEnd, seededDice } from "./support/play.js";
@@ -315,4 +315,30 @@ test("une partie complète analysée par le moteur, comme une partie d'avant 1d"
// Le journal analysé se reprend : l'analyse fait partie du format.
const resumed = Game.resume(engine, JSON.parse(JSON.stringify(analysed)));
assert.deepEqual(resumed.journal.review, analysed.review);
// Chaque coup s'écrit pareil sur l'écran de jeu (« L'ordinateur a
// joué … », moveNotation) et dans l'analyse : la notation du moteur.
let compared = 0;
let rewritten = 0;
for (const move of moves) {
if (move.type !== "MovePlayed" || move.halfMoves.length === 0) {
continue;
}
const advice =
move.side === "computer"
? analysed.review?.computer[move.id]
: analysed.advice?.[move.id];
if (advice?.kind !== "move") {
continue;
}
const notation = await resumed.moveNotation(move.id);
assert.equal(notation, advice.played.notation, `coup ${move.id}`);
compared++;
if (notation !== formatHalfMoves(move.halfMoves)) {
rewritten++;
}
}
assert.ok(compared > 5, `${compared} coups comparés`);
// La course finit en sortie : au moins un coup s'écrit autrement que
// ses demi-coups (« 6/off » pour « 6/2 2/off »…).
assert.ok(rewritten > 0, "aucun coup réécrit par le moteur");
});