// 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 }[] = []; const analyse = (journal: GameJournal, eventId: string) => new Promise((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>; async function opening(dice: number[]): Promise { engine = await loadNodeEngine(); return Game.start(engine, { setup: { position: START_POSITION, firstTurn: "player" }, dice: fixedDice(dice), }); } async function play(game: Game, move: string): Promise { 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"); });