Prompts : le niveau du joueur seulement à partir de 20 décisions, son taux d'erreur dans le seul bilan

Recette de 1f (F3). En dessous de 20 décisions analysées, le prompt dit
« Je débute au backgammon. ». Test du seuil ; outil prompt-sample pour
relire des prompts remplis par le moteur. Plan de 1f clos.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Pierre MARTIN
2026-11-11 21:52:23 +01:00
co-authored by Claude Opus 5.5
parent f509f8746e
commit f2df1b2e7f
10 changed files with 143 additions and 71 deletions
+27 -4
View File
@@ -36,7 +36,9 @@ import {
decisionPrompt,
exercisesPosition,
exercisesPrompt,
LEVEL_MIN_DECISIONS,
LEVEL_NAMES,
playerSentence,
summaryPosition,
summaryPrompt,
} from "../src/domain/prompts/prompts.js";
@@ -86,7 +88,10 @@ function assertComplete(prompt: string, position: { positionId: string; matchId:
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, /Je suis un joueur de niveau \S/);
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");
@@ -101,7 +106,7 @@ test("les cinq gabarits se lisent dans leur source, avec les repères du cadrage
for (const kind of PROMPT_KINDS) {
const names = placeholders(templates[kind]);
for (const name of [
"niveau",
"joueur",
"position_id",
"match_id",
"au_trait",
@@ -133,6 +138,22 @@ test("les mots du prompt sont ceux de l'écran", () => {
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);
@@ -371,7 +392,7 @@ test("prompt d'un concept : la position de départ, ou celle de la partie en cou
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("niveau pas encore mesuré"), 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");
@@ -388,7 +409,9 @@ test("prompt d'un concept : la position de départ, ou celle de la partie en cou
assertComplete(second, during.position);
assert.ok(second.includes("le concept « Blot »."), second);
assert.ok(second.includes("une position de ma partie en cours"), second);
assert.ok(second.includes("Je suis un joueur de niveau Débutant, d'après mon erreur"), 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 () => {
+82
View File
@@ -0,0 +1,82 @@
// Affiche des prompts remplis sur de vraies analyses du moteur, pour les
// relire (étape 1f) : node build/web/tests/tools/prompt-sample.js
//
// Copyright (C) 2026 les auteurs d'Ustagammon.
// Licence : GPLv3 ou ultérieure (voir LICENSE).
import { readFileSync } from "node:fs";
import {
missingAnalyses,
runAnalysis,
summarizeAnalysis,
} from "../../src/domain/analysis/game-analysis.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 { positionBefore } from "../../src/domain/prompts/position.js";
import {
decisionPrompt,
summaryPosition,
summaryPrompt,
} from "../../src/domain/prompts/prompts.js";
import {
PROMPT_FILES,
type PromptTemplates,
parsePromptTemplate,
} from "../../src/domain/prompts/templates.js";
import { adviceOn, loadPhrases, 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 read = (file: string) =>
parsePromptTemplate(readFileSync(new URL(`docs/prompts/${file}`, root), "utf8"));
const templates: PromptTemplates = {
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 phrases = loadPhrases();
const engine: Engine = await loadNodeEngine();
const played = await playOpening(engine, [3, 1], "24/23 13/10");
const advice = await adviceOn(engine, played);
played.game.recordAdvice(advice);
const position = await positionBefore(engine, played.game.journal, played.eventId);
console.log(decisionPrompt(templates, phrases, { journal: played.game.journal, advice, position }));
async function analyseAll(journal: GameJournal): Promise<GameJournal> {
let current = journal;
for (;;) {
const next = missingAnalyses(current)[0];
if (next === undefined) {
return current;
}
current = await runAnalysis(engine, current, next);
}
}
for (const [seed, choice] of [
[7, 1],
[7, 2],
[11, 2],
[3, 3],
]) {
const game = Game.start(engine, {
setup: { position: "trsXAADs7mwAAA", firstTurn: "player" },
level: "intermediate",
dice: seededDice(seed ?? 0),
});
await playToTheEnd(game, engine, choice);
const journal = await analyseAll(game.journal);
const summary = summarizeAnalysis(journal);
console.log(
`\n=== graine ${seed}, choix ${choice} : coup à revoir ${summary.toReview?.turn ?? "aucun"}`,
);
if (seed === 3) {
const where = await summaryPosition(engine, journal);
console.log(summaryPrompt(templates, phrases, { journal, position: where }));
}
}