142 lines
5.2 KiB
TypeScript
142 lines
5.2 KiB
TypeScript
// Le conseil du maître pendant la partie (app/src/ui/advice-tracker.ts) :
|
|
// les analyses ne retardent pas le tour de l'ordinateur, la ligne
|
|
// n'apparaît qu'après la décision et pas si le joueur a déjà agi, et
|
|
// chaque conseil est rattaché au journal.
|
|
//
|
|
// 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 { analyseDecision } from "../src/domain/analysis/analyse.js";
|
|
import type { Advice } from "../src/domain/game/advice.js";
|
|
import type { GameJournal } from "../src/domain/game/events.js";
|
|
import { Game } from "../src/domain/game/game.js";
|
|
import { parseHalfMoves, START_POSITION } from "../src/domain/game/position.js";
|
|
import { fixedDice } from "../src/domain/game/roll.js";
|
|
import { createAdviceTracker } from "../src/ui/advice-tracker.js";
|
|
import { DOUBLE_TAKE_POSITION } from "./support/advice.js";
|
|
import { loadNodeEngine } from "./support/node-engine.js";
|
|
|
|
/** Une analyse qu'on libère à la main. */
|
|
function heldAnalyses() {
|
|
const held: { id: string; release: () => Promise<void> }[] = [];
|
|
const analyse = (journal: GameJournal, eventId: string) =>
|
|
new Promise<Advice | null>((resolve) => {
|
|
held.push({
|
|
id: eventId,
|
|
release: async () => {
|
|
resolve(await analyseDecision(engine, journal, eventId));
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
},
|
|
});
|
|
});
|
|
return { held, analyse };
|
|
}
|
|
|
|
let engine: Awaited<ReturnType<typeof loadNodeEngine>>;
|
|
|
|
async function opening(dice: number[]): Promise<Game> {
|
|
engine = await loadNodeEngine();
|
|
return Game.start(engine, {
|
|
setup: { position: START_POSITION, firstTurn: "player" },
|
|
dice: fixedDice(dice),
|
|
});
|
|
}
|
|
|
|
async function play(game: Game, move: string): Promise<void> {
|
|
for (const halfMove of parseHalfMoves(move)) {
|
|
assert.ok(await game.playHalfMove(halfMove));
|
|
}
|
|
await game.confirmMove();
|
|
}
|
|
|
|
test("le tour de l'ordinateur n'attend pas l'analyse ; la ligne vient ensuite", async () => {
|
|
const game = await opening([3, 1, 6, 5, 4, 2]);
|
|
const { held, analyse } = heldAnalyses();
|
|
let saved = 0;
|
|
let changes = 0;
|
|
const tracker = createAdviceTracker({
|
|
analyse,
|
|
recorded: () => saved++,
|
|
changed: () => changes++,
|
|
});
|
|
let before = game.events;
|
|
await game.roll();
|
|
tracker.track(game, before);
|
|
// Un jet seul ne montre rien : le joueur réfléchit.
|
|
assert.equal(held.length, 1, "le jet est analysé (aurait-il fallu doubler ?)");
|
|
before = game.events;
|
|
await play(game, "24/23 13/10");
|
|
tracker.track(game, before);
|
|
// Les analyses sont en attente : l'ordinateur joue quand même.
|
|
await game.playComputerTurn();
|
|
assert.equal(game.state.turn, "player");
|
|
assert.equal(tracker.shown, null);
|
|
for (const h of held) {
|
|
await h.release();
|
|
}
|
|
const shown = tracker.shown as Advice | null;
|
|
assert.equal(shown?.kind, "move");
|
|
assert.equal(shown?.skill, "veryBad");
|
|
assert.equal(changes, 1);
|
|
// Le coup est rattaché au journal (le jet aussi s'il y avait à dire).
|
|
assert.ok(saved >= 1);
|
|
assert.ok(game.advice[shown?.eventId ?? ""] !== undefined);
|
|
// L'action suivante du joueur efface la ligne.
|
|
tracker.clear();
|
|
assert.equal(tracker.shown, null);
|
|
});
|
|
|
|
test("si le joueur a déjà agi, la ligne n'apparaît pas, mais le conseil reste au journal", async () => {
|
|
const game = await opening([3, 1, 6, 5]);
|
|
const { held, analyse } = heldAnalyses();
|
|
const tracker = createAdviceTracker({ analyse, recorded: () => {}, changed: () => {} });
|
|
await game.roll();
|
|
const before = game.events;
|
|
await play(game, "8/5 6/5");
|
|
tracker.track(game, before);
|
|
tracker.clear(); // le joueur a relancé avant l'analyse
|
|
for (const h of held) {
|
|
await h.release();
|
|
}
|
|
assert.equal(tracker.shown, null);
|
|
const move = game.events.findLast((e) => e.type === "MovePlayed");
|
|
assert.equal(game.advice[move?.id ?? ""]?.skill, "none");
|
|
});
|
|
|
|
test("un double manqué avant le jet passe avant le coup dans la ligne", async () => {
|
|
engine = await loadNodeEngine();
|
|
const game = Game.start(engine, {
|
|
setup: { position: DOUBLE_TAKE_POSITION, firstTurn: "player" },
|
|
dice: fixedDice([6, 5]),
|
|
});
|
|
const tracker = createAdviceTracker({
|
|
analyse: (journal, id) => analyseDecision(engine, journal, id),
|
|
recorded: () => {},
|
|
changed: () => {},
|
|
});
|
|
let before = game.events;
|
|
await game.roll();
|
|
tracker.track(game, before);
|
|
const best = await engine.bestMove(
|
|
game.state.position,
|
|
{ value: 1, owner: "centered" },
|
|
[6, 5],
|
|
0,
|
|
);
|
|
assert.ok(best !== null);
|
|
before = game.events;
|
|
for (const halfMove of best.halfMoves) {
|
|
assert.ok(await game.playHalfMove(halfMove));
|
|
}
|
|
await game.confirmMove();
|
|
tracker.track(game, before);
|
|
for (let i = 0; i < 50 && tracker.shown === null; i++) {
|
|
await new Promise((r) => setTimeout(r, 20));
|
|
}
|
|
const shown = tracker.shown as Advice | null;
|
|
assert.equal(shown?.kind, "cube");
|
|
assert.equal(shown?.kind === "cube" ? shown.action : null, "noDouble");
|
|
});
|