Fusion de main (progression, 1h) dans plateau-soigne
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { test } from "node:test";
|
||||
import { homeModel } from "../src/ui/home-model.js";
|
||||
import { installPlatform } from "../src/ui/install.js";
|
||||
import {
|
||||
DEFAULT_SETTINGS,
|
||||
@@ -45,6 +46,20 @@ test("réglages d'une version précédente : les nouveaux champs prennent leur v
|
||||
assert.equal(parseSettings('{"tutorialSeen": -3, "welcomed": "oui"}').tutorialSeen, 0);
|
||||
});
|
||||
|
||||
test("carte de la partie : sans partie en cours, ni miniature ni compte de la partie finie", () => {
|
||||
const settings = { ...DEFAULT_SETTINGS, welcomed: true, gamesFinished: 1 };
|
||||
// Partie en cours, ou moteur qui se charge : la miniature et le compte.
|
||||
assert.deepEqual(
|
||||
[homeModel(settings, true, false).board, homeModel(settings, true, false).resume],
|
||||
[true, true],
|
||||
);
|
||||
assert.equal(homeModel(settings, null, false).board, true);
|
||||
// Partie finie : le titre et « Nouvelle partie » seulement (recette 1h).
|
||||
const over = homeModel(settings, false, false);
|
||||
assert.equal(over.board, false);
|
||||
assert.equal(over.resume, false);
|
||||
});
|
||||
|
||||
test("premier lancement : tant que rien n'a été fait, une seule fois", () => {
|
||||
assert.ok(isFirstLaunch(DEFAULT_SETTINGS));
|
||||
// La partie commencée d'office au chargement ne compte pas.
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
// Tests de la progression (étape 1h) : le taux d'erreur de chaque partie
|
||||
// terminée est celui du bilan, lu dans les analyses rangées sans rien
|
||||
// recalculer ; la courbe, sa tendance et la phrase du maître ; le prompt
|
||||
// « Créer des exercices » sur plusieurs parties (thèmes des erreurs, niveau
|
||||
// du joueur à partir de 20 décisions cumulées).
|
||||
//
|
||||
// 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,
|
||||
readyForSummary,
|
||||
runAnalysis,
|
||||
summarizeAnalysis,
|
||||
} from "../src/domain/analysis/game-analysis.js";
|
||||
import { PHRASE_KEYS, progressionSentence } from "../src/domain/analysis/phrases.js";
|
||||
import {
|
||||
CURVE_MIN_GAMES,
|
||||
errorRateMillis,
|
||||
type FinishedGame,
|
||||
progressionCurve,
|
||||
progressionEntries,
|
||||
progressionTrend,
|
||||
} from "../src/domain/analysis/progression.js";
|
||||
import type { Engine } from "../src/domain/game/engine.js";
|
||||
import type { GameJournal } from "../src/domain/game/events.js";
|
||||
import { currentLine } from "../src/domain/game/events.js";
|
||||
import { Game } from "../src/domain/game/game.js";
|
||||
import { replay } from "../src/domain/game/state.js";
|
||||
import {
|
||||
exercisesPosition,
|
||||
exercisesPrompt,
|
||||
LEVEL_MIN_DECISIONS,
|
||||
LEVEL_NAMES,
|
||||
mistakeThemes,
|
||||
} from "../src/domain/prompts/prompts.js";
|
||||
import { PROMPT_FILES, parsePromptTemplate } from "../src/domain/prompts/templates.js";
|
||||
import { HOME_TEXTS } from "../src/ui/home-texts.js";
|
||||
import { PROGRESSION_TEXTS } from "../src/ui/progression-texts.js";
|
||||
import { parseRoute, routeHash } from "../src/ui/router.js";
|
||||
import { loadPhrases } 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();
|
||||
const point = (id: string, perDecision: number) => ({ id, perDecision });
|
||||
|
||||
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 (le joueur prend le choice-ième
|
||||
* coup légal), non analysée. */
|
||||
async function race(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 game.journal;
|
||||
}
|
||||
|
||||
function finished(id: string, startedAt: number, journal: GameJournal): FinishedGame {
|
||||
return {
|
||||
id,
|
||||
startedAt,
|
||||
level: "intermediate",
|
||||
result: replay(currentLine(journal)).result,
|
||||
journal,
|
||||
};
|
||||
}
|
||||
|
||||
test("l'adresse #/progression et celle de « Créer des exercices »", () => {
|
||||
assert.deepEqual(parseRoute("#/progression"), { screen: "progression" });
|
||||
assert.equal(routeHash({ screen: "progression", ai: true }), "#/progression/ia");
|
||||
});
|
||||
|
||||
test("le résultat d'une partie dit d'où viennent les points : gammon, videau", () => {
|
||||
const result = PROGRESSION_TEXTS.result;
|
||||
const lost = (kind: "single" | "gammon" | "backgammon", points: number) =>
|
||||
({ winner: "computer", kind, points }) as const;
|
||||
assert.equal(result({ winner: "player", kind: "single", points: 1 }, 1), "Gagnée, 1 point");
|
||||
assert.equal(result(lost("single", 1), 1), "Perdue, 1 point");
|
||||
assert.equal(result(lost("gammon", 2), 1), "Perdue, gammon, 2 points");
|
||||
assert.equal(result(lost("single", 2), 2), "Perdue, videau à 2, 2 points");
|
||||
assert.equal(result(lost("gammon", 4), 2), "Perdue, gammon, videau à 2, 4 points");
|
||||
assert.equal(result(lost("backgammon", 12), 4), "Perdue, backgammon, videau à 4, 12 points");
|
||||
assert.equal(result(null, 1), "Partie terminée");
|
||||
});
|
||||
|
||||
test("la ligne de l'accueil : « aucune gagnée », comme « aucune leçon terminée »", () => {
|
||||
assert.equal(HOME_TEXTS.progress(3, 0), "3 parties jouées · aucune gagnée");
|
||||
assert.equal(HOME_TEXTS.progress(3, 1), "3 parties jouées · 1 gagnée");
|
||||
assert.equal(HOME_TEXTS.progress(0, 0), "aucune partie jouée");
|
||||
});
|
||||
|
||||
test("le taux d'erreur s'écrit en millièmes, comme les mEMG de GNU Backgammon", () => {
|
||||
assert.equal(errorRateMillis(0.0142), 14);
|
||||
assert.equal(errorRateMillis(0.0075), 8);
|
||||
assert.equal(errorRateMillis(0), 0);
|
||||
});
|
||||
|
||||
test("la tendance : moyennes des deux moitiés, arrondies en millièmes", () => {
|
||||
assert.deepEqual(progressionTrend([]), { kind: "waiting" });
|
||||
assert.deepEqual(progressionTrend([point("a", 0.02), point("b", 0.01)]), { kind: "waiting" });
|
||||
assert.equal(CURVE_MIN_GAMES, 3);
|
||||
// Trois parties : la première contre la dernière, celle du milieu ne
|
||||
// compte pas.
|
||||
assert.deepEqual(progressionTrend([point("a", 0.014), point("b", 0.05), point("c", 0.007)]), {
|
||||
kind: "down",
|
||||
games: 3,
|
||||
from: 14,
|
||||
to: 7,
|
||||
});
|
||||
assert.deepEqual(
|
||||
progressionTrend([
|
||||
point("a", 0.01),
|
||||
point("b", 0.012),
|
||||
point("c", 0.02),
|
||||
point("d", 0.016),
|
||||
]),
|
||||
{ kind: "up", games: 4, from: 11, to: 18 },
|
||||
);
|
||||
// 0,0101 et 0,0104 s'écrivent tous deux 10 : stable.
|
||||
assert.equal(
|
||||
progressionTrend([point("a", 0.0101), point("b", 0.03), point("c", 0.0104)]).kind,
|
||||
"steady",
|
||||
);
|
||||
});
|
||||
|
||||
test("la phrase du maître : des faits, sans point d'exclamation, sous 140 caractères", () => {
|
||||
for (const key of ["progression.baisse", "progression.hausse", "progression.stable"]) {
|
||||
assert.ok(PHRASE_KEYS.includes(key), key);
|
||||
}
|
||||
assert.equal(
|
||||
progressionSentence(phrases, { kind: "down", games: 8, from: 14, to: 7 }),
|
||||
"Sur vos 8 dernières parties, vous faites moins d'erreurs : votre taux d'erreur est passé de 14 à 7.",
|
||||
);
|
||||
assert.equal(
|
||||
progressionSentence(phrases, { kind: "waiting" }),
|
||||
"Votre courbe apparaîtra après trois parties.",
|
||||
);
|
||||
for (const trend of [
|
||||
{ kind: "down", games: 20, from: 140, to: 70 },
|
||||
{ kind: "up", games: 20, from: 70, to: 140 },
|
||||
{ kind: "steady", games: 20, from: 12, to: 12 },
|
||||
] as const) {
|
||||
const sentence = progressionSentence(phrases, trend);
|
||||
assert.ok(sentence.length < 140, sentence);
|
||||
assert.ok(!sentence.includes("!"), sentence);
|
||||
assert.ok(sentence.includes(String(trend.to)), sentence);
|
||||
}
|
||||
assert.ok(
|
||||
progressionSentence(phrases, { kind: "up", games: 5, from: 7, to: 9 }).includes(
|
||||
"Les leçons sont là",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("la progression lit le taux du bilan, sans rien recalculer ; une partie non analysée attend", async () => {
|
||||
const engine = await loadNodeEngine();
|
||||
const raw = [await race(engine, 3, 3), await race(engine, 7, 2), await race(engine, 11, 1)];
|
||||
const analysed = await Promise.all(raw.map((j) => analyseAll(engine, j)));
|
||||
// Données dans le désordre : la progression les range par date.
|
||||
const games = [
|
||||
finished("c", 3000, analysed[2] as GameJournal),
|
||||
finished("a", 1000, analysed[0] as GameJournal),
|
||||
finished("b", 2000, analysed[1] as GameJournal),
|
||||
finished("d", 4000, raw[0] as GameJournal),
|
||||
];
|
||||
const entries = progressionEntries(games);
|
||||
assert.deepEqual(
|
||||
entries.map((e) => e.id),
|
||||
["a", "b", "c", "d"],
|
||||
);
|
||||
for (const [i, entry] of entries.slice(0, 3).entries()) {
|
||||
assert.deepEqual(
|
||||
entry.errorRate,
|
||||
summarizeAnalysis(analysed[i] as GameJournal).player.errorRate,
|
||||
);
|
||||
}
|
||||
assert.equal(readyForSummary(raw[0] as GameJournal), false);
|
||||
assert.equal(entries[3]?.errorRate, null, "partie non analysée : pas de taux");
|
||||
const curve = progressionCurve(entries);
|
||||
assert.deepEqual(
|
||||
curve.map((p) => p.id),
|
||||
["a", "b", "c"],
|
||||
);
|
||||
assert.equal(progressionTrend(curve).kind === "waiting", false);
|
||||
});
|
||||
|
||||
test("« Créer des exercices » : erreurs, thèmes comptés sur les faits du moteur, niveau à 20 décisions cumulées", async () => {
|
||||
const engine = await loadNodeEngine();
|
||||
const template = parsePromptTemplate(
|
||||
readFileSync(new URL(`docs/prompts/${PROMPT_FILES.exercises}`, root), "utf8"),
|
||||
);
|
||||
const templates = { move: "", cube: "", summary: "", concept: "", exercises: template };
|
||||
const journals: GameJournal[] = [];
|
||||
for (const [seed, choice] of [
|
||||
[3, 3],
|
||||
[7, 2],
|
||||
[11, 1],
|
||||
[5, 4],
|
||||
[9, 3],
|
||||
] as const) {
|
||||
journals.push(await analyseAll(engine, await race(engine, seed, choice)));
|
||||
}
|
||||
const decisions = journals.map((j) => summarizeAnalysis(j).player.errorRate.decisions);
|
||||
assert.ok((decisions[0] ?? 0) < LEVEL_MIN_DECISIONS, `décisions : ${decisions.join()}`);
|
||||
let cumulated = 0;
|
||||
for (let n = 1; n <= journals.length; n++) {
|
||||
const some = journals.slice(0, n);
|
||||
cumulated += decisions[n - 1] ?? 0;
|
||||
const position = await exercisesPosition(engine, some);
|
||||
if (position === null) {
|
||||
continue;
|
||||
}
|
||||
const prompt = exercisesPrompt(templates, phrases, { journals: some, position });
|
||||
assert.ok(!/[{}]/.test(prompt), prompt);
|
||||
assert.ok(prompt.includes("Ce qui revient dans ces erreurs"), prompt);
|
||||
assert.match(prompt, /- \d+ coups? et \d+ décisions? de videau/);
|
||||
if (cumulated >= LEVEL_MIN_DECISIONS) {
|
||||
assert.match(prompt, /Je suis un joueur de niveau /);
|
||||
assert.ok(Object.values(LEVEL_NAMES).some((name) => prompt.includes(name)));
|
||||
} else {
|
||||
assert.ok(prompt.includes("Je débute au backgammon."), prompt);
|
||||
}
|
||||
}
|
||||
assert.ok(cumulated >= LEVEL_MIN_DECISIONS, `décisions cumulées : ${cumulated}`);
|
||||
});
|
||||
|
||||
test("les thèmes des erreurs : faits du moteur seulement, une ligne par fait présent", () => {
|
||||
const facts = (hits: number, madePoints: number[]) => ({ hits, madePoints });
|
||||
const move = (best: ReturnType<typeof facts>, played: ReturnType<typeof facts>) => ({
|
||||
advice: { kind: "move", bestFacts: best, playedFacts: played } as never,
|
||||
});
|
||||
const cube = { advice: { kind: "cube" } as never };
|
||||
assert.deepEqual(
|
||||
mistakeThemes([
|
||||
move(facts(1, []), facts(0, [])),
|
||||
move(facts(0, [5]), facts(0, [])),
|
||||
move(facts(0, []), facts(1, [])),
|
||||
cube,
|
||||
]),
|
||||
[
|
||||
"- 3 coups et 1 décision de videau",
|
||||
"- 1 fois, le coup de GNU Backgammon frappait un pion et le mien non",
|
||||
"- 1 fois, le coup de GNU Backgammon faisait un point que le mien ne faisait pas",
|
||||
"- 1 fois, mon coup frappait un pion et celui de GNU Backgammon non",
|
||||
],
|
||||
);
|
||||
assert.deepEqual(mistakeThemes([cube]), ["- 0 coup et 1 décision de videau"]);
|
||||
});
|
||||
@@ -46,6 +46,9 @@ const TABLE: [string, Route][] = [
|
||||
["#/parties/mh2k3x-a1b2c3/bilan", { screen: "summary", game: "mh2k3x-a1b2c3" }],
|
||||
["#/parties/mh2k3x-a1b2c3/analyse", { screen: "analysis", game: "mh2k3x-a1b2c3", turn: null }],
|
||||
["#/parties/mh2k3x-a1b2c3/analyse/7", { screen: "analysis", game: "mh2k3x-a1b2c3", turn: 7 }],
|
||||
// La progression (étape 1h) et son prompt « Créer des exercices ».
|
||||
["#/progression", { screen: "progression" }],
|
||||
["#/progression/ia", { screen: "progression", ai: true }],
|
||||
["#/menu", { screen: "menu" }],
|
||||
["#/nouvelle-partie", { screen: "newGame" }],
|
||||
["#/explication/12", { screen: "explanation", event: "12" }],
|
||||
@@ -127,6 +130,8 @@ test("une adresse inconnue est reconnue comme telle", () => {
|
||||
"#/menu/ia",
|
||||
"#/parties/ab/ia",
|
||||
"#/explication/12/ia/ia",
|
||||
"#/progression/x",
|
||||
"#/progression/ia/ia",
|
||||
]) {
|
||||
assert.equal(parseRoute(hash).screen, "unknown", hash);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
// Étapes du scénario docs/scenarios/suivre-ma-progression.feature (ADR
|
||||
// 0008) : des courses courtes jouées par le domaine sur le vrai moteur, avec
|
||||
// des dés reproductibles, enregistrées dans le stockage des parties (sur un
|
||||
// localStorage simulé), analysées par l'analyse en arrière-plan de
|
||||
// l'interface (review-runner.ts, sans DOM) ; l'écran de progression lit
|
||||
// ensuite le stockage comme app.ts (domain/analysis/progression.ts).
|
||||
//
|
||||
// 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 { Before, BeforeAll, Given, setDefaultTimeout, Then, When } from "@cucumber/cucumber";
|
||||
import {
|
||||
readyForSummary,
|
||||
runAnalysis,
|
||||
summarizeAnalysis,
|
||||
} from "../../src/domain/analysis/game-analysis.js";
|
||||
import { progressionSentence } from "../../src/domain/analysis/phrases.js";
|
||||
import {
|
||||
CURVE_MIN_GAMES,
|
||||
type FinishedGame,
|
||||
type ProgressionEntry,
|
||||
progressionCurve,
|
||||
progressionEntries,
|
||||
progressionTrend,
|
||||
} from "../../src/domain/analysis/progression.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 { levelOf } from "../../src/domain/game/saved-game.js";
|
||||
import {
|
||||
exercisesPosition,
|
||||
exercisesPrompt,
|
||||
LEVEL_MIN_DECISIONS,
|
||||
} from "../../src/domain/prompts/prompts.js";
|
||||
import { PROMPT_FILES, parsePromptTemplate } from "../../src/domain/prompts/templates.js";
|
||||
import type { GameLibrary } from "../../src/storage/game-library.js";
|
||||
import { memoryStorage, openGameLibrary } from "../../src/storage/open-library.js";
|
||||
import { createReviewRunner } from "../../src/ui/review-runner.js";
|
||||
import { loadPhrases } from "../support/advice.js";
|
||||
import { loadNodeEngine } from "../support/node-engine.js";
|
||||
import { playToTheEnd, seededDice } from "../support/play.js";
|
||||
|
||||
setDefaultTimeout(120_000);
|
||||
|
||||
const root = new URL("../../../../", import.meta.url);
|
||||
const phrases = loadPhrases();
|
||||
/** Les courses jouées : graine des dés et choix du joueur. */
|
||||
const RACES = [
|
||||
[3, 3],
|
||||
[7, 2],
|
||||
[11, 1],
|
||||
] as const;
|
||||
|
||||
let engine: Engine;
|
||||
let library: GameLibrary;
|
||||
/** Les identifiants des parties enregistrées, dans l'ordre où elles ont
|
||||
* été jouées. */
|
||||
let ids: string[];
|
||||
let entries: ProgressionEntry[];
|
||||
/** Les analyses demandées au moteur, par partie. */
|
||||
let analyses: Map<string, number>;
|
||||
let prompt: string;
|
||||
let clock: number;
|
||||
|
||||
BeforeAll(async () => {
|
||||
engine = await loadNodeEngine();
|
||||
});
|
||||
|
||||
Before(async () => {
|
||||
clock = 1_000_000;
|
||||
library = (await openGameLibrary({ localStorage: memoryStorage() }, { now: () => clock }))
|
||||
.library;
|
||||
ids = [];
|
||||
analyses = new Map();
|
||||
});
|
||||
|
||||
/** Analyse en arrière-plan ce qui manque à la partie id, comme
|
||||
* l'application, et l'enregistre. */
|
||||
async function analyse(id: string): Promise<void> {
|
||||
const saved = await library.load(id);
|
||||
assert.ok(saved !== null);
|
||||
const runner = createReviewRunner((journal, task) => {
|
||||
analyses.set(id, (analyses.get(id) ?? 0) + 1);
|
||||
return runAnalysis(engine, journal, task);
|
||||
});
|
||||
let latest = saved.journal;
|
||||
await new Promise<void>((resolve) => {
|
||||
runner.start(id, saved.journal, (j) => (latest = j), resolve);
|
||||
});
|
||||
await library.saveAnalyses(id, latest, { level: levelOf(latest) });
|
||||
}
|
||||
|
||||
/** Ce que lit l'écran de progression : les parties terminées et leur
|
||||
* journal enregistré. */
|
||||
async function openProgression(): Promise<void> {
|
||||
const games: FinishedGame[] = [];
|
||||
for (const summary of await library.list()) {
|
||||
const saved = await library.load(summary.id);
|
||||
if (summary.status === "over" && saved !== null) {
|
||||
games.push({ ...summary, journal: saved.journal });
|
||||
}
|
||||
}
|
||||
entries = progressionEntries(games);
|
||||
}
|
||||
|
||||
Given("vous avez terminé {word} parties contre l'ordinateur", async (count: string) => {
|
||||
const n = { deux: 2, trois: 3 }[count] ?? Number(count);
|
||||
for (const [seed, choice] of RACES.slice(0, n)) {
|
||||
const game = Game.start(engine, {
|
||||
setup: { position: "trsXAADs7mwAAA", firstTurn: "player" },
|
||||
level: "intermediate",
|
||||
dice: seededDice(seed),
|
||||
});
|
||||
await playToTheEnd(game, engine, choice);
|
||||
clock += 60_000;
|
||||
const summary = await library.save(`partie-${seed}`, game.journal, {
|
||||
level: "intermediate",
|
||||
});
|
||||
assert.equal(summary.status, "over");
|
||||
ids.push(summary.id);
|
||||
}
|
||||
});
|
||||
|
||||
Given("le maître les a analysées", async () => {
|
||||
for (const id of ids) {
|
||||
await analyse(id);
|
||||
}
|
||||
});
|
||||
|
||||
Given("le maître n'a analysé que les deux premières", async () => {
|
||||
for (const id of ids.slice(0, 2)) {
|
||||
await analyse(id);
|
||||
}
|
||||
analyses.clear();
|
||||
});
|
||||
|
||||
When("vous ouvrez votre progression", async () => {
|
||||
await openProgression();
|
||||
});
|
||||
|
||||
Then("la courbe a trois points, chacun égal au taux d'erreur du bilan de sa partie", async () => {
|
||||
const curve = progressionCurve(entries);
|
||||
assert.equal(curve.length, 3);
|
||||
// De la plus ancienne partie à la plus récente.
|
||||
assert.deepEqual(
|
||||
curve.map((p) => p.id),
|
||||
ids,
|
||||
);
|
||||
for (const point of curve) {
|
||||
const saved = await library.load(point.id);
|
||||
assert.ok(saved !== null && readyForSummary(saved.journal));
|
||||
assert.equal(
|
||||
point.perDecision,
|
||||
summarizeAnalysis(saved.journal).player.errorRate.perDecision,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Then("la phrase du maître compare vos premières parties aux dernières", () => {
|
||||
const trend = progressionTrend(progressionCurve(entries));
|
||||
assert.notEqual(trend.kind, "waiting");
|
||||
const sentence = progressionSentence(phrases, trend);
|
||||
assert.ok(sentence.startsWith("Sur vos 3 dernières parties"), sentence);
|
||||
assert.ok(!sentence.includes("!"), sentence);
|
||||
});
|
||||
|
||||
Then("il n'y a pas encore de courbe", () => {
|
||||
assert.equal(entries.length, 2);
|
||||
assert.ok(
|
||||
entries.every((e) => e.errorRate !== null),
|
||||
"les deux parties ont leur taux",
|
||||
);
|
||||
assert.ok(progressionCurve(entries).length < CURVE_MIN_GAMES);
|
||||
});
|
||||
|
||||
Then(/^la phrase de votre progression est « (.+) »$/, (sentence: string) => {
|
||||
assert.equal(
|
||||
progressionSentence(phrases, progressionTrend(progressionCurve(entries))),
|
||||
sentence,
|
||||
);
|
||||
});
|
||||
|
||||
Then("la troisième partie attend son analyse, sans taux d'erreur", () => {
|
||||
assert.equal(entries.length, 3);
|
||||
assert.equal(entries[2]?.id, ids[2]);
|
||||
assert.equal(entries[2]?.errorRate, null);
|
||||
assert.equal(progressionCurve(entries).length, 2);
|
||||
});
|
||||
|
||||
When("son analyse en arrière-plan est finie", async () => {
|
||||
// Comme l'écran : les parties qui en ont besoin, une à une.
|
||||
for (const entry of entries) {
|
||||
if (entry.errorRate === null) {
|
||||
await analyse(entry.id);
|
||||
}
|
||||
}
|
||||
await openProgression();
|
||||
});
|
||||
|
||||
Then("les deux premières parties n'ont pas été analysées de nouveau", () => {
|
||||
assert.deepEqual([...analyses.keys()], [ids[2]]);
|
||||
});
|
||||
|
||||
When("vous demandez des exercices", async () => {
|
||||
await openProgression();
|
||||
const journals: GameJournal[] = [];
|
||||
for (const entry of entries) {
|
||||
const saved = await library.load(entry.id);
|
||||
assert.ok(saved !== null);
|
||||
journals.push(saved.journal);
|
||||
}
|
||||
const position = await exercisesPosition(engine, journals);
|
||||
assert.ok(position !== null, "aucune erreur dans ces parties");
|
||||
const exercises = parsePromptTemplate(
|
||||
readFileSync(new URL(`docs/prompts/${PROMPT_FILES.exercises}`, root), "utf8"),
|
||||
);
|
||||
prompt = exercisesPrompt({ move: "", cube: "", summary: "", concept: "", exercises }, phrases, {
|
||||
journals,
|
||||
position,
|
||||
});
|
||||
});
|
||||
|
||||
Then("la question cite vos erreurs avec leur coût et ce qui y revient", () => {
|
||||
assert.match(prompt, /- Partie \d, coup \d+.*écart −\d,\d{3}/);
|
||||
assert.ok(prompt.includes("Ce qui revient dans ces erreurs"), prompt);
|
||||
assert.match(prompt, /- \d+ coups? et \d+ décisions? de videau/);
|
||||
assert.ok(prompt.includes("Voici mes 3 dernières parties"), prompt);
|
||||
});
|
||||
|
||||
Then(
|
||||
"elle ne donne votre niveau que si vos parties comptent au moins {int} décisions",
|
||||
(min: number) => {
|
||||
assert.equal(min, LEVEL_MIN_DECISIONS);
|
||||
const decisions = entries.reduce((sum, e) => sum + (e.errorRate?.decisions ?? 0), 0);
|
||||
if (decisions >= min) {
|
||||
assert.match(prompt, /Je suis un joueur de niveau /);
|
||||
} else {
|
||||
assert.ok(prompt.includes("Je débute au backgammon."), prompt);
|
||||
}
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user