Le classement du conseil vient de l'analyse du coup joué (conseils en version 2) : mêmes équités que la feuille du maître et l'analyse. Le titre dit le niveau du meilleur coup ; une ligne plus basse le dit aussi. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
520 lines
23 KiB
TypeScript
520 lines
23 KiB
TypeScript
// Les prompts pour une IA (étape 1f) : gabarits lus dans leur source
|
||
// (docs/prompts/), remplis avec de vraies analyses du moteur. Aucun repère
|
||
// vide ni inconnu, et les chiffres sont ceux que l'application affiche.
|
||
//
|
||
// Copyright (C) 2026 les auteurs d'Ustagammon.
|
||
// Licence : GPLv3 ou ultérieure (voir LICENSE).
|
||
|
||
import assert from "node:assert/strict";
|
||
import { readFileSync } from "node:fs";
|
||
import { test } from "node:test";
|
||
import {
|
||
missingAnalyses,
|
||
runAnalysis,
|
||
summarizeAnalysis,
|
||
} from "../src/domain/analysis/game-analysis.js";
|
||
import { formatEquity, rankingRows } from "../src/domain/analysis/numbers.js";
|
||
import { diceWords, masterWords } from "../src/domain/analysis/phrases.js";
|
||
import type { Advice } from "../src/domain/game/advice.js";
|
||
import type { Engine } from "../src/domain/game/engine.js";
|
||
import type { GameJournal } from "../src/domain/game/events.js";
|
||
import { Game } from "../src/domain/game/game.js";
|
||
import { COMPUTER_LEVELS } from "../src/domain/game/level.js";
|
||
import { START_POSITION } from "../src/domain/game/position.js";
|
||
import {
|
||
boardDiagram,
|
||
PUBLIC_SITE,
|
||
positionAddress,
|
||
positionBefore,
|
||
positionNow,
|
||
} from "../src/domain/prompts/position.js";
|
||
import {
|
||
CUBE_ACTION_WORDS,
|
||
CUBE_ROW_WORDS,
|
||
conceptExample,
|
||
conceptPrompt,
|
||
decisionPrompt,
|
||
exercisesPosition,
|
||
exercisesPrompt,
|
||
LEVEL_MIN_DECISIONS,
|
||
LEVEL_NAMES,
|
||
playerSentence,
|
||
summaryPosition,
|
||
summaryPrompt,
|
||
} from "../src/domain/prompts/prompts.js";
|
||
import {
|
||
fillPrompt,
|
||
PROMPT_FILES,
|
||
PROMPT_KINDS,
|
||
type PromptTemplates,
|
||
parsePromptTemplate,
|
||
placeholders,
|
||
} from "../src/domain/prompts/templates.js";
|
||
import { ADVICE_TEXTS } from "../src/ui/advice-texts.js";
|
||
import { MATCH_TEXTS } from "../src/ui/match-texts.js";
|
||
import { REVIEW_TEXTS, signed } from "../src/ui/review-texts.js";
|
||
import { parseRoute, routeHash } from "../src/ui/router.js";
|
||
import {
|
||
adviceOn,
|
||
loadPhrases,
|
||
playerAnswers,
|
||
playerDoubles,
|
||
playerRollsWithoutDoubling,
|
||
playOpening,
|
||
} from "./support/advice.js";
|
||
import { loadNodeEngine } from "./support/node-engine.js";
|
||
import { playToTheEnd, seededDice } from "./support/play.js";
|
||
|
||
const root = new URL("../../../", import.meta.url);
|
||
const phrases = loadPhrases();
|
||
|
||
/** Les gabarits, lus dans leur source. */
|
||
function loadTemplates(): PromptTemplates {
|
||
const read = (file: string) =>
|
||
parsePromptTemplate(readFileSync(new URL(`docs/prompts/${file}`, root), "utf8"));
|
||
return {
|
||
move: read(PROMPT_FILES.move),
|
||
cube: read(PROMPT_FILES.cube),
|
||
summary: read(PROMPT_FILES.summary),
|
||
concept: read(PROMPT_FILES.concept),
|
||
exercises: read(PROMPT_FILES.exercises),
|
||
};
|
||
}
|
||
const templates = loadTemplates();
|
||
|
||
/** Ce que chaque prompt doit contenir (cadrage, « IA générative en V1 »). */
|
||
function assertComplete(prompt: string, position: { positionId: string; matchId: string }): void {
|
||
for (const hole of ["{", "}", "inconnu", "undefined", "NaN", "null", " :"]) {
|
||
assert.ok(!prompt.includes(hole), `« ${hole} » dans le prompt :\n${prompt}`);
|
||
}
|
||
assert.ok(prompt.startsWith("Tu es un coach de backgammon"), "rôle absent");
|
||
assert.match(
|
||
prompt,
|
||
/^Tu es un coach de backgammon, bienveillant et précis\. (Je débute au backgammon\.|Je suis un joueur de niveau [A-ZÉ]\S+\.)\n/,
|
||
);
|
||
assert.ok(prompt.includes(`Position ID : ${position.positionId}`), "Position ID absent");
|
||
assert.ok(prompt.includes(`Match ID : ${position.matchId}`), "Match ID absent");
|
||
assert.ok(prompt.includes(`${PUBLIC_SITE}#/position/`), "lien absent");
|
||
assert.ok(prompt.includes("X : mes pions"), "diagramme absent");
|
||
assert.match(prompt, /GNU Backgammon \(analyse à \d plis?\)/);
|
||
assert.ok(prompt.includes("ne les contredis pas"), "consigne absente");
|
||
assert.ok(prompt.includes("Réponds en français"), "langue absente");
|
||
}
|
||
|
||
test("les cinq gabarits se lisent dans leur source, avec les repères du cadrage", () => {
|
||
assert.equal(PROMPT_KINDS.length, 5);
|
||
for (const kind of PROMPT_KINDS) {
|
||
const names = placeholders(templates[kind]);
|
||
for (const name of [
|
||
"joueur",
|
||
"position_id",
|
||
"match_id",
|
||
"au_trait",
|
||
"lien",
|
||
"diagramme",
|
||
"score",
|
||
"videau",
|
||
"niveau_eval",
|
||
"langue",
|
||
]) {
|
||
assert.ok(names.includes(name), `{${name}} absent du gabarit ${kind}`);
|
||
}
|
||
assert.ok(templates[kind].includes("ne les contredis pas"), `consigne absente : ${kind}`);
|
||
}
|
||
assert.throws(() => parsePromptTemplate("# Sans gabarit\n"), /Gabarit/);
|
||
});
|
||
|
||
test("un repère inconnu ou vide est une erreur", () => {
|
||
assert.equal(fillPrompt("a {b} c", { b: "x" }), "a x c");
|
||
assert.throws(() => fillPrompt("a {b}", {}), /inconnu/);
|
||
assert.throws(() => fillPrompt("a {b}", { b: " " }), /vide/);
|
||
});
|
||
|
||
test("les mots du prompt sont ceux de l'écran", () => {
|
||
for (const level of COMPUTER_LEVELS) {
|
||
assert.equal(LEVEL_NAMES[level], MATCH_TEXTS.levels[level].name);
|
||
}
|
||
assert.deepEqual(CUBE_ACTION_WORDS, REVIEW_TEXTS.cubeActions);
|
||
assert.deepEqual(CUBE_ROW_WORDS, ADVICE_TEXTS.cubeRows);
|
||
});
|
||
|
||
test("le niveau du joueur ne se dit qu'à partir de 20 décisions, sans son taux", () => {
|
||
assert.equal(LEVEL_MIN_DECISIONS, 20);
|
||
assert.equal(playerSentence(null), "Je débute au backgammon.");
|
||
for (const level of COMPUTER_LEVELS) {
|
||
assert.equal(playerSentence({ decisions: 19, level }), "Je débute au backgammon.");
|
||
assert.equal(
|
||
playerSentence({ decisions: 20, level }),
|
||
`Je suis un joueur de niveau ${MATCH_TEXTS.levels[level].name}.`,
|
||
);
|
||
}
|
||
assert.equal(
|
||
playerSentence({ decisions: 120, level: "advanced" }),
|
||
"Je suis un joueur de niveau Avancé.",
|
||
);
|
||
});
|
||
|
||
test("le diagramme recopie le plateau décodé par le moteur", async () => {
|
||
const engine = await loadNodeEngine();
|
||
const start = await positionNow(engine, null);
|
||
assert.equal(start.positionId, START_POSITION);
|
||
const diagram = boardDiagram(start.sides);
|
||
assert.ok(diagram.includes("Mes pions (X), case (nombre) : 24 (2), 13 (5), 8 (3), 6 (5)."));
|
||
assert.ok(
|
||
diagram.includes(
|
||
"Pions de l'ordinateur (O), dans ma numérotation : 19 (5), 17 (3), 12 (5), 1 (2).",
|
||
),
|
||
);
|
||
assert.ok(diagram.includes("Compte de points : moi 167, l'ordinateur 167."));
|
||
// La rangée du haut : 13 (X, 5 pions) à gauche, 24 (X, 2 pions) à
|
||
// droite ; 19 (O) juste après la barre.
|
||
const lines = diagram.split("\n");
|
||
assert.equal(lines[1], " 13 14 15 16 17 18 | 19 20 21 22 23 24");
|
||
assert.equal(lines[2], " X . . . O . | O . . . . X");
|
||
assert.equal(lines[3], " X O | O X");
|
||
assert.equal(lines[0], "```");
|
||
assert.equal(lines[lines.indexOf("```", 1) - 1], " 12 11 10 9 8 7 | 6 5 4 3 2 1");
|
||
});
|
||
|
||
test("le lien de la position est l'adresse publique de l'écran « position »", async () => {
|
||
const engine = await loadNodeEngine();
|
||
const played = await playOpening(engine, [3, 1], "24/23 13/10");
|
||
const position = await positionBefore(engine, played.game.journal, played.eventId);
|
||
const address = positionAddress(position);
|
||
const hash = routeHash({
|
||
screen: "position",
|
||
position: position.positionId,
|
||
match: position.matchId,
|
||
});
|
||
assert.equal(address, `${PUBLIC_SITE}${hash}`);
|
||
assert.deepEqual(parseRoute(new URL(address).hash), {
|
||
screen: "position",
|
||
position: position.positionId,
|
||
match: position.matchId,
|
||
});
|
||
});
|
||
|
||
/** Les lignes du classement telles que la feuille du maître les affiche :
|
||
* rang, coup, écart. */
|
||
function shownRanking(advice: Advice): string[] {
|
||
if (advice.kind !== "move") {
|
||
return [];
|
||
}
|
||
return rankingRows(advice).map((row) => `${row.move.notation}|${formatEquity(row.loss)}`);
|
||
}
|
||
|
||
test("prompt d'un coup : la position, les identifiants et le top 3 du moteur, mêmes chiffres", async () => {
|
||
const engine = await loadNodeEngine();
|
||
for (const [dice, move] of [
|
||
[[3, 1], "24/23 13/10"],
|
||
[[3, 1], "8/5 6/5"],
|
||
[[2, 1], "13/11 6/5"],
|
||
[[6, 5], "24/18 13/8"],
|
||
] as const) {
|
||
const played = await playOpening(engine, dice, move);
|
||
const advice = await adviceOn(engine, played);
|
||
assert.equal(advice.kind, "move");
|
||
if (advice.kind !== "move") {
|
||
continue;
|
||
}
|
||
// Le conseil, rattaché au journal comme dans l'application.
|
||
played.game.recordAdvice(advice);
|
||
const position = await positionBefore(engine, played.game.journal, played.eventId);
|
||
assert.equal(position.positionId, advice.position);
|
||
const prompt = decisionPrompt(templates, phrases, {
|
||
journal: played.game.journal,
|
||
advice,
|
||
position,
|
||
});
|
||
assertComplete(prompt, position);
|
||
assert.ok(prompt.includes(`Dés : ${dice[0]}-${dice[1]}.`), prompt);
|
||
assert.ok(prompt.includes(`J'ai joué ${advice.played.notation} : `), prompt);
|
||
assert.ok(prompt.includes(`« ${masterWords(phrases, advice).word} »`), prompt);
|
||
// Les trois premiers coups du moteur, et le vôtre, avec les écarts
|
||
// de « Voir les chiffres du moteur ».
|
||
const shown = shownRanking(advice);
|
||
for (const row of rankingRows(advice).filter((r) => r.rank <= 3 || r.played)) {
|
||
const line = `${row.rank}. ${row.move.notation}${row.played ? " (mon coup)" : ""} : équité ${formatEquity(row.move.equity)}, écart ${formatEquity(row.loss)}`;
|
||
assert.ok(prompt.includes(line), `ligne absente : ${line}\n${prompt}`);
|
||
assert.ok(shown.includes(`${row.move.notation}|${formatEquity(row.loss)}`));
|
||
}
|
||
assert.ok(prompt.includes(`1. ${advice.best.notation}`), prompt);
|
||
assert.match(prompt, /\(analyse à [12] plis?\)/);
|
||
}
|
||
});
|
||
|
||
/** Une équité écrite (« −0,212 ») proche d'une équité de gnubg de bureau :
|
||
* à l'arrondi près, plus la tolérance des positions de référence (0,002 ;
|
||
* tests/reference/README.md), le moteur WebAssembly ne calculant pas au
|
||
* bit près comme gnubg de bureau (ADR 0005). */
|
||
function assertNear(text: string, value: number, message: string): void {
|
||
const written = Number(text.replace("−", "-").replace("+", "").replace(",", "."));
|
||
assert.ok(Math.abs(written - value) <= 0.0005 + 0.002, `${text} ≠ ${value}\n${message}`);
|
||
}
|
||
|
||
/** Le niveau du titre du classement, et chaque ligne du classement : coup,
|
||
* équité, écart, niveau propre (null : celui du titre). */
|
||
function rankingOfPrompt(prompt: string) {
|
||
const title = /^Classement de GNU Backgammon \(analyse à (\d) plis?\)/m.exec(prompt);
|
||
assert.ok(title, prompt);
|
||
const lines = [
|
||
...prompt.matchAll(
|
||
/^(\d+)\. (.+?)( \(mon coup\))? : équité (\S+), écart (\S+?)(?:, évalué à (\d) plis?)?$/gm,
|
||
),
|
||
].map((m) => ({
|
||
rank: Number(m[1]),
|
||
notation: m[2],
|
||
mine: m[3] !== undefined,
|
||
equity: m[4],
|
||
loss: m[5],
|
||
ply: m[6] === undefined ? null : Number(m[6]),
|
||
}));
|
||
return { ply: Number(title[1]), lines };
|
||
}
|
||
|
||
test("prompt d'un coup : le niveau du titre est celui des lignes, les équités celles de l'analyse", async () => {
|
||
const engine = await loadNodeEngine();
|
||
// gnubg de bureau, analyse de partie à 2 plis (tests/reference/analysis/).
|
||
const reference = (id: string) =>
|
||
JSON.parse(readFileSync(new URL(`tests/reference/analysis/${id}.json`, root), "utf8"))
|
||
.plies["2"];
|
||
for (const [dice, move, id] of [
|
||
// Erreur : gnubg réévalue à 2 plis le meilleur coup et le coup joué ;
|
||
// les autres restent au niveau où ses filtres les ont laissés.
|
||
[[3, 1], "24/23 13/10", "move-opening-31-error"],
|
||
// Meilleur coup : un seul coup passe le filtre de 1 pli, gnubg
|
||
// s'arrête là, tout le classement est à 1 pli.
|
||
[[3, 1], "8/5 6/5", "move-opening-31-best"],
|
||
[[6, 5], "13/8 13/7", "move-opening-65-error"],
|
||
] as const) {
|
||
const played = await playOpening(engine, dice, move);
|
||
const advice = await adviceOn(engine, played);
|
||
assert.ok(advice.kind === "move");
|
||
played.game.recordAdvice(advice);
|
||
const position = await positionBefore(engine, played.game.journal, played.eventId);
|
||
const prompt = decisionPrompt(templates, phrases, {
|
||
journal: played.game.journal,
|
||
advice,
|
||
position,
|
||
});
|
||
const { ply, lines } = rankingOfPrompt(prompt);
|
||
const expected = reference(id);
|
||
|
||
// Le titre dit le niveau du meilleur coup, celui que gnubg de bureau
|
||
// donne ; une ligne sans niveau propre est à ce niveau, une ligne
|
||
// avec un niveau propre, à un niveau inférieur.
|
||
assert.equal(ply, expected.ranking[0].ply, prompt);
|
||
assert.equal(ply, advice.best.ply);
|
||
assert.ok(lines.length >= 3, prompt);
|
||
for (const line of lines) {
|
||
const reference = expected.ranking.find(
|
||
(m: { move: string }) => m.move === line.notation,
|
||
);
|
||
assert.ok(reference, `${line.notation} absent du classement de gnubg`);
|
||
assert.equal(line.ply ?? ply, reference.ply, `${line.notation}\n${prompt}`);
|
||
assert.ok(line.ply === null || line.ply < ply, prompt);
|
||
assertNear(line.equity, reference.equity, `${line.notation}\n${prompt}`);
|
||
}
|
||
|
||
// Les équités et l'écart du meilleur coup et du vôtre sont ceux de
|
||
// l'analyse, que disent la feuille du maître et l'écran d'analyse.
|
||
const best = lines[0];
|
||
const mine = lines.find((line) => line.mine);
|
||
assert.ok(best && mine, prompt);
|
||
assert.equal(best.notation, advice.best.notation);
|
||
assert.equal(best.equity, formatEquity(advice.best.equity));
|
||
assert.equal(best.loss, formatEquity(0));
|
||
assert.equal(mine.notation, advice.played.notation);
|
||
assert.equal(mine.rank, advice.rank);
|
||
assert.equal(mine.equity, formatEquity(advice.played.equity));
|
||
assert.equal(mine.loss, formatEquity(advice.equityLoss));
|
||
assertNear(mine.loss, expected.equity_loss, prompt);
|
||
assert.ok(shownRanking(advice).includes(`${mine.notation}|${mine.loss}`));
|
||
}
|
||
});
|
||
|
||
test("prompt d'une décision de videau : les équités de la feuille du maître", async () => {
|
||
const engine = await loadNodeEngine();
|
||
const cases = [
|
||
{ play: playerDoubles, who: "moi", question: "doubler ou lancer" },
|
||
{ play: playerRollsWithoutDoubling, who: "moi", question: "doubler ou lancer" },
|
||
{ play: (e: Engine) => playerAnswers(e, true), who: "l'ordinateur", question: "prendre" },
|
||
{ play: (e: Engine) => playerAnswers(e, false), who: "l'ordinateur", question: "prendre" },
|
||
];
|
||
for (const { play, who, question } of cases) {
|
||
const played = await play(engine);
|
||
const advice = await adviceOn(engine, played);
|
||
assert.equal(advice.kind, "cube");
|
||
if (advice.kind !== "cube") {
|
||
continue;
|
||
}
|
||
played.game.recordAdvice(advice);
|
||
const position = await positionBefore(engine, played.game.journal, played.eventId);
|
||
assert.equal(position.positionId, advice.position);
|
||
const prompt = decisionPrompt(templates, phrases, {
|
||
journal: played.game.journal,
|
||
advice,
|
||
position,
|
||
});
|
||
assertComplete(prompt, position);
|
||
assert.ok(prompt.includes(`Il fallait choisir : ${question}`), prompt);
|
||
assert.ok(prompt.includes(`(${who}), pour un videau de 1`), prompt);
|
||
assert.ok(prompt.includes(`J'ai choisi de ${CUBE_ACTION_WORDS[advice.action]},`), prompt);
|
||
for (const key of ["noDouble", "doubleTake", "doublePass"] as const) {
|
||
const value = formatEquity(advice.equities[key]);
|
||
assert.match(
|
||
prompt,
|
||
new RegExp(
|
||
`- ${ADVICE_TEXTS.cubeRows[key]}( \\(ma décision\\))? : ${value.replace("+", "\\+")}\\n`,
|
||
),
|
||
);
|
||
}
|
||
assert.ok(prompt.includes(`Écart de ma décision : ${formatEquity(advice.equityLoss)}.`));
|
||
}
|
||
});
|
||
|
||
/** Toutes les analyses qui manquent, une à une, comme l'application en
|
||
* arrière-plan. */
|
||
async function analyseAll(engine: Engine, journal: GameJournal): Promise<GameJournal> {
|
||
let current = journal;
|
||
for (let i = 0; i < 500; i++) {
|
||
const next = missingAnalyses(current)[0];
|
||
if (next === undefined) {
|
||
return current;
|
||
}
|
||
current = await runAnalysis(engine, current, next);
|
||
}
|
||
throw new Error("l'analyse ne finit pas");
|
||
}
|
||
|
||
/** Une course courte jouée jusqu'au bout et analysée (le joueur prend le
|
||
* choice-ième coup légal). */
|
||
async function analysedRace(engine: Engine, seed: number, choice: number): Promise<GameJournal> {
|
||
const game = Game.start(engine, {
|
||
setup: { position: "trsXAADs7mwAAA", firstTurn: "player" },
|
||
level: "intermediate",
|
||
dice: seededDice(seed),
|
||
});
|
||
await playToTheEnd(game, engine, choice);
|
||
return analyseAll(engine, game.journal);
|
||
}
|
||
|
||
test("prompt du bilan : résultat, décomptes, taux et chance de l'écran, erreurs", async () => {
|
||
const engine = await loadNodeEngine();
|
||
const journal = await analysedRace(engine, 3, 3);
|
||
const summary = summarizeAnalysis(journal);
|
||
assert.ok(summary.toReview !== null, "la partie devrait avoir une erreur");
|
||
const position = await summaryPosition(engine, journal);
|
||
const prompt = summaryPrompt(templates, phrases, { journal, position });
|
||
assertComplete(prompt, position);
|
||
const { player, computer, counts } = summary;
|
||
// Les chiffres de « Voir les chiffres du moteur » de l'analyse, et les
|
||
// trois cases du bilan.
|
||
assert.ok(
|
||
prompt.includes(
|
||
`Mon erreur par décision : ${signed(-player.errorRate.perDecision)} sur ${player.errorRate.decisions} décision`,
|
||
),
|
||
);
|
||
assert.ok(
|
||
prompt.includes(`soit le niveau ${MATCH_TEXTS.levels[player.errorRate.level].name} ;`),
|
||
);
|
||
assert.ok(
|
||
prompt.includes(`celle de l'ordinateur : ${signed(-computer.errorRate.perDecision)} sur`),
|
||
);
|
||
assert.ok(
|
||
prompt.includes(
|
||
`La chance de mes dés par tour : ${signed(player.luck.perRoll)} (${diceWords(phrases, player.luck.rating)})`,
|
||
),
|
||
);
|
||
assert.ok(prompt.includes(`celle de l'ordinateur : ${signed(computer.luck.perRoll)}.`));
|
||
assert.ok(prompt.includes(`Mes décisions : ${counts.good} bon`), prompt);
|
||
assert.ok(prompt.includes(`, ${counts.bad + counts.veryBad} erreur`), prompt);
|
||
assert.ok(prompt.includes("Résultat : "), prompt);
|
||
assert.ok(prompt.includes("contre l'ordinateur (niveau Intermédiaire)"), prompt);
|
||
// Le coup à revoir : sa position, et sa ligne avec son écart.
|
||
const worst = summary.toReview.advice;
|
||
assert.equal(position.positionId, worst.position);
|
||
assert.ok(
|
||
prompt.includes(`Le coup à revoir, ma plus grosse erreur (coup ${summary.toReview.turn})`),
|
||
);
|
||
assert.ok(
|
||
prompt.includes(
|
||
`écart ${formatEquity(worst.equityLoss)} (${masterWords(phrases, worst).word})`,
|
||
),
|
||
prompt,
|
||
);
|
||
});
|
||
|
||
test("prompt du bilan sans erreur : la position finale", async () => {
|
||
const engine = await loadNodeEngine();
|
||
// Le joueur au trait : un pion sur 6, un sur 1 ; il joue 6/off 1/off et
|
||
// gagne (scénario « Revoir ma partie »).
|
||
const game = Game.start(engine, {
|
||
setup: { position: "BQAACAIAAAAAAA", firstTurn: "player" },
|
||
dice: () => 6,
|
||
});
|
||
await game.roll();
|
||
const moves = await engine.legalMoves(game.state.position, game.state.roll ?? [6, 6]);
|
||
for (const halfMove of moves[0]?.halfMoves ?? []) {
|
||
await game.playHalfMove(halfMove);
|
||
}
|
||
await game.confirmMove();
|
||
assert.equal(game.state.phase, "over");
|
||
const journal = await analyseAll(engine, game.journal);
|
||
const position = await summaryPosition(engine, journal);
|
||
const prompt = summaryPrompt(templates, phrases, { journal, position });
|
||
assertComplete(prompt, position);
|
||
assert.ok(prompt.includes("- aucune : j'ai joué comme GNU Backgammon."), prompt);
|
||
assert.ok(prompt.includes("Voici la position finale"), prompt);
|
||
assert.ok(prompt.includes("j'ai gagné une partie simple, 1 point"), prompt);
|
||
});
|
||
|
||
test("prompt d'un concept : la position de départ, ou celle de la partie en cours", async () => {
|
||
const engine = await loadNodeEngine();
|
||
const alone = await conceptExample(engine, null, 2);
|
||
const prompt = conceptPrompt(templates, {
|
||
term: "Videau",
|
||
english: "Cube",
|
||
brief: "le dé des enchères.",
|
||
journal: null,
|
||
example: alone,
|
||
});
|
||
assertComplete(prompt, alone.position);
|
||
assert.ok(prompt.includes("le concept « Videau » (en anglais : Cube)."), prompt);
|
||
assert.ok(prompt.includes("voici la position de départ"), prompt);
|
||
assert.ok(prompt.includes("précis. Je débute au backgammon.\n"), prompt);
|
||
assert.match(prompt, /chances de gagner \d+ %, équité [+−]?\d,\d{3}/);
|
||
|
||
const played = await playOpening(engine, [3, 1], "24/23 13/10");
|
||
played.game.recordAdvice(await adviceOn(engine, played));
|
||
const during = await conceptExample(engine, played.game.journal, 2);
|
||
assert.ok(during.fromGame);
|
||
const second = conceptPrompt(templates, {
|
||
term: "Blot",
|
||
english: "Blot",
|
||
brief: "un pion seul sur une case.",
|
||
journal: played.game.journal,
|
||
example: during,
|
||
});
|
||
assertComplete(second, during.position);
|
||
assert.ok(second.includes("le concept « Blot »."), second);
|
||
assert.ok(second.includes("une position de ma partie en cours"), second);
|
||
// Une seule décision analysée : pas de niveau, pas de taux d'erreur.
|
||
assert.ok(second.includes("Je débute au backgammon."), second);
|
||
assert.ok(!second.includes("erreur par décision"), second);
|
||
});
|
||
|
||
test("prompt des exercices : les erreurs de plusieurs parties, la plus coûteuse en position", async () => {
|
||
const engine = await loadNodeEngine();
|
||
const journals = [await analysedRace(engine, 3, 3), await analysedRace(engine, 7, 2)];
|
||
const position = await exercisesPosition(engine, journals);
|
||
assert.ok(position !== null);
|
||
const prompt = exercisesPrompt(templates, phrases, { journals, position });
|
||
assertComplete(prompt, position);
|
||
assert.ok(prompt.includes("Voici mes 2 dernières parties"), prompt);
|
||
assert.match(prompt, /- Partie [12], coup \d+/);
|
||
assert.ok(prompt.includes("La plus coûteuse (partie"), prompt);
|
||
assert.ok(prompt.includes("cinq exercices"), prompt);
|
||
assert.equal(await exercisesPosition(engine, []), null);
|
||
});
|