Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06cc20fb1b | ||
|
|
c2c5d13651 | ||
|
|
2664486541 | ||
|
|
2d787b2c0b | ||
|
|
368d3c9e8f | ||
|
|
0d9622eff9 | ||
|
|
d31ab0e30a | ||
|
|
ac27625e67 | ||
|
|
73f4cfce51 |
@@ -12,8 +12,8 @@
|
||||
// n'a pas encore de taux ; l'application l'analyse en arrière-plan.
|
||||
|
||||
import type { Level } from "../game/engine.js";
|
||||
import type { GameJournal } from "../game/events.js";
|
||||
import type { GameResult } from "../game/state.js";
|
||||
import { currentLine, type GameJournal } from "../game/events.js";
|
||||
import { type GameResult, replay } from "../game/state.js";
|
||||
import { type ErrorRate, readyForSummary, summarizeAnalysis } from "./game-analysis.js";
|
||||
|
||||
/** Les parties prises en compte au plus : les plus récentes. Au-delà, la
|
||||
@@ -43,6 +43,8 @@ export interface ProgressionEntry {
|
||||
readonly startedAt: number;
|
||||
readonly level: Level;
|
||||
readonly result: GameResult | null;
|
||||
/** La valeur du videau à la fin de la partie (1 : jamais doublé). */
|
||||
readonly cube: number;
|
||||
readonly errorRate: ErrorRate | null;
|
||||
}
|
||||
|
||||
@@ -56,6 +58,7 @@ export function progressionEntries(games: readonly FinishedGame[]): ProgressionE
|
||||
startedAt: game.startedAt,
|
||||
level: game.level,
|
||||
result: game.result,
|
||||
cube: replay(currentLine(game.journal)).cube.value,
|
||||
errorRate: readyForSummary(game.journal)
|
||||
? summarizeAnalysis(game.journal).player.errorRate
|
||||
: null,
|
||||
|
||||
+6
-5
@@ -1553,10 +1553,7 @@ export function startApp(options: AppOptions): void {
|
||||
// relues à chaque visite, avec leurs analyses rangées. Celles qui n'en
|
||||
// ont pas encore sont analysées en arrière-plan, une à une, la plus
|
||||
// récente d'abord ; rien n'est recalculé pour les autres.
|
||||
const progressionView = createProgressionView({
|
||||
section: ui.progression,
|
||||
back: (link) => leavingLink(link, (route) => route.screen === "progression"),
|
||||
});
|
||||
const progressionView = createProgressionView({ section: ui.progression });
|
||||
let progressionGames: FinishedGame[] | null = null;
|
||||
// Chaque lecture a son numéro : une lecture dépassée n'est pas dessinée.
|
||||
let progressionRequest = 0;
|
||||
@@ -1589,10 +1586,14 @@ export function startApp(options: AppOptions): void {
|
||||
return games.filter((g): g is FinishedGame => g !== null);
|
||||
}
|
||||
|
||||
/** Ouvre l'écran : ce qui est déjà lu, puis les parties relues. */
|
||||
/** Ouvre l'écran : les parties relues à chaque visite (une partie a pu
|
||||
* finir entre-temps) ; d'ici là, l'écran dit qu'il les lit, sans
|
||||
* montrer la liste de la visite précédente comme si elle était à
|
||||
* jour. */
|
||||
function showProgression(): void {
|
||||
const request = ++progressionRequest;
|
||||
progressionTried.clear();
|
||||
progressionGames = null;
|
||||
renderProgression();
|
||||
options.engineReady
|
||||
.then(() => loadProgression())
|
||||
|
||||
@@ -30,6 +30,11 @@ export interface HomeModel {
|
||||
/** La carte de la partie : « Reprendre la partie » si une partie est en
|
||||
* cours, sinon « Nouvelle partie » en action principale. */
|
||||
readonly resume: boolean;
|
||||
/** La miniature du plateau et le compte de points : seulement pour une
|
||||
* partie à reprendre. Sans partie en cours, la carte ne montre que son
|
||||
* titre et « Nouvelle partie », jamais la position de la partie finie
|
||||
* (recette 1h). */
|
||||
readonly board: boolean;
|
||||
/** L'invitation à installer. */
|
||||
readonly installCard: boolean;
|
||||
}
|
||||
@@ -45,6 +50,7 @@ export function homeModel(
|
||||
return {
|
||||
welcome,
|
||||
resume: playing !== false,
|
||||
board: playing !== false,
|
||||
installCard: !welcome && installCardShown(settings, installed),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ export const HOME_TEXTS = {
|
||||
progress: (played: number, won: number) =>
|
||||
played === 0
|
||||
? "aucune partie jouée"
|
||||
: `${plural(played, "partie jouée", "parties jouées")} · ${plural(won, "gagnée", "gagnées")}`,
|
||||
: `${plural(played, "partie jouée", "parties jouées")} · ${won === 0 ? "aucune gagnée" : plural(won, "gagnée", "gagnées")}`,
|
||||
/** La ligne de progression, lien vers l'écran de progression (1h). */
|
||||
progressLink: (line: string) => `Votre progression : ${line}`,
|
||||
footer: "Né à l'Atelier du Huit, à Cugnaux. Logiciel libre, sans publicité ni mise.",
|
||||
|
||||
@@ -161,7 +161,7 @@ export function createHomeView(options: HomeViewOptions): HomeView {
|
||||
const mini = boardSvg(miniBox, "home-board", "home-board");
|
||||
const facts = element(cardTop, "div", { className: "home-facts" });
|
||||
const gameOverline = element(facts, "span", { className: "overline" });
|
||||
element(facts, "strong", { text: HOME_TEXTS.againstComputer });
|
||||
const againstLine = element(facts, "strong", { text: HOME_TEXTS.againstComputer });
|
||||
const pips = element(facts, "span", { id: "home-pips", className: "home-pips" });
|
||||
const actions = element(card, "div", { className: "home-actions" });
|
||||
const resume = element(actions, "a", {
|
||||
@@ -243,7 +243,12 @@ export function createHomeView(options: HomeViewOptions): HomeView {
|
||||
return;
|
||||
}
|
||||
const sides = state.sides ?? OPENING_BOARDS;
|
||||
drawBoard(mini, sides, miniBox.clientWidth || 132);
|
||||
miniBox.hidden = !model.board;
|
||||
pips.hidden = !model.board;
|
||||
againstLine.hidden = !model.board;
|
||||
if (model.board) {
|
||||
drawBoard(mini, sides, miniBox.clientWidth || 132);
|
||||
}
|
||||
gameOverline.textContent = model.resume
|
||||
? HOME_TEXTS.gameOverline
|
||||
: HOME_TEXTS.noGameOverline;
|
||||
|
||||
@@ -11,14 +11,13 @@ import type { GameResult } from "../domain/game/state.js";
|
||||
const plural = (n: number, one: string, many: string) => `${n} ${n > 1 ? many : one}`;
|
||||
|
||||
const RESULT_KIND = {
|
||||
single: "partie simple",
|
||||
single: "",
|
||||
gammon: "gammon",
|
||||
backgammon: "backgammon",
|
||||
} as const;
|
||||
|
||||
export const PROGRESSION_TEXTS = {
|
||||
title: "Votre progression",
|
||||
back: "Retour",
|
||||
loading: "Lecture de vos parties…",
|
||||
failed: "Vos parties n'ont pas pu être lues.",
|
||||
/** Aucune partie terminée. */
|
||||
@@ -30,22 +29,31 @@ export const PROGRESSION_TEXTS = {
|
||||
whatIsIt: "Qu'est-ce que le taux d'erreur ?",
|
||||
analysing: (n: number) =>
|
||||
`Le maître analyse ${plural(n, "partie", "parties")} : ${n > 1 ? "elles apparaîtront" : "elle apparaîtra"} ici une fois ${n > 1 ? "analysées" : "analysée"}.`,
|
||||
exercises: "Créer des exercices",
|
||||
exercises: "Demander des exercices à une IA",
|
||||
listTitle: "Vos parties",
|
||||
/** « Gagnée, gammon (2 points) ». */
|
||||
result: (result: GameResult | null) => {
|
||||
/** Le résultat court, qui dit d'où viennent les points : « Gagnée,
|
||||
* 1 point », « Perdue, gammon, 2 points », « Perdue, videau à 2,
|
||||
* 2 points », « Perdue, gammon, videau à 2, 4 points » (jamais
|
||||
* « partie simple »). cube : la valeur du videau à la fin de la
|
||||
* partie. */
|
||||
result: (result: GameResult | null, cube: number) => {
|
||||
if (result === null) {
|
||||
return "Partie terminée";
|
||||
}
|
||||
const who = result.winner === "player" ? "Gagnée" : "Perdue";
|
||||
return `${who}, ${RESULT_KIND[result.kind]} (${plural(result.points, "point", "points")})`;
|
||||
const parts = [result.winner === "player" ? "Gagnée" : "Perdue"];
|
||||
if (result.kind !== "single") {
|
||||
parts.push(RESULT_KIND[result.kind]);
|
||||
}
|
||||
if (cube > 1) {
|
||||
parts.push(`videau à ${cube}`);
|
||||
}
|
||||
parts.push(plural(result.points, "point", "points"));
|
||||
return parts.join(", ");
|
||||
},
|
||||
level: (name: string) => `contre ${name}`,
|
||||
rateLabel: "taux d'erreur",
|
||||
rateLoading: "en analyse",
|
||||
openSummary: (result: string, date: string) => `Bilan de la partie du ${date} : ${result}`,
|
||||
analysis: "Analyse",
|
||||
analysisLabel: (date: string) => `Analyse de la partie du ${date}`,
|
||||
/** La date d'une partie : « 3 nov. 2026 ». */
|
||||
date: (ms: number) => new Date(ms).toLocaleDateString("fr-FR", { dateStyle: "medium" }),
|
||||
} as const;
|
||||
|
||||
@@ -37,8 +37,6 @@ export interface ProgressionData {
|
||||
|
||||
export interface ProgressionViewOptions {
|
||||
readonly section: HTMLElement;
|
||||
/** Branche le lien « Retour » (il revient dans l'historique). */
|
||||
readonly back: (link: HTMLAnchorElement) => void;
|
||||
}
|
||||
|
||||
export interface ProgressionView {
|
||||
@@ -47,12 +45,10 @@ export interface ProgressionView {
|
||||
}
|
||||
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
/** Hauteur de la courbe, et ses marges intérieures (étiquettes de l'axe à
|
||||
* gauche, valeurs au-dessus des points). */
|
||||
/** Hauteur de la courbe, et ses marges intérieures (les valeurs au-dessus
|
||||
* des points, le dernier point plus gros). */
|
||||
const CURVE_HEIGHT = 168;
|
||||
const PAD = { left: 34, right: 16, top: 24, bottom: 12 };
|
||||
/** Au-delà, les valeurs ne s'écrivent plus au-dessus des points. */
|
||||
const LABELLED_POINTS = 10;
|
||||
const PAD = { left: 12, right: 12, top: 30, bottom: 12 };
|
||||
|
||||
function element<K extends keyof HTMLElementTagNameMap>(
|
||||
parent: HTMLElement,
|
||||
@@ -95,17 +91,11 @@ function levelName(entry: ProgressionEntry): string {
|
||||
export function createProgressionView(options: ProgressionViewOptions): ProgressionView {
|
||||
const { section } = options;
|
||||
section.replaceChildren();
|
||||
const head = element(section, "div", { className: "analysis-head" });
|
||||
const back = element(head, "a", {
|
||||
id: "progression-back",
|
||||
className: "button secondary",
|
||||
text: PROGRESSION_TEXTS.back,
|
||||
});
|
||||
back.href = "#/accueil";
|
||||
options.back(back);
|
||||
element(head, "h2", {
|
||||
// Pas de « Retour » (charte 1h) : la barre du haut et le retour du
|
||||
// téléphone suffisent, comme sur la liste des leçons.
|
||||
element(section, "h1", {
|
||||
id: "progression-title",
|
||||
className: "analysis-title",
|
||||
className: "progression-title",
|
||||
text: PROGRESSION_TEXTS.title,
|
||||
});
|
||||
const summary = element(section, "p", {
|
||||
@@ -135,6 +125,7 @@ export function createProgressionView(options: ProgressionViewOptions): Progress
|
||||
entries: data.entries.map((e) => [
|
||||
e.id,
|
||||
e.result,
|
||||
e.cube,
|
||||
e.level,
|
||||
e.errorRate?.perDecision ?? null,
|
||||
]),
|
||||
@@ -153,7 +144,9 @@ export function createProgressionView(options: ProgressionViewOptions): Progress
|
||||
}
|
||||
if (data.entries.length === 0) {
|
||||
summary.textContent = PROGRESSION_TEXTS.empty;
|
||||
const actions = element(body, "div", { className: "progression-actions" });
|
||||
const actions = element(body, "div", {
|
||||
className: "progression-actions progression-empty",
|
||||
});
|
||||
const start = element(actions, "a", {
|
||||
id: "progression-new-game",
|
||||
className: "button",
|
||||
@@ -164,10 +157,8 @@ export function createProgressionView(options: ProgressionViewOptions): Progress
|
||||
return;
|
||||
}
|
||||
const points = progressionCurve(data.entries);
|
||||
summary.textContent =
|
||||
data.phrases === null
|
||||
? ""
|
||||
: progressionSentence(data.phrases, progressionTrend(points));
|
||||
const trend = progressionTrend(points);
|
||||
summary.textContent = data.phrases === null ? "" : progressionSentence(data.phrases, trend);
|
||||
|
||||
if (points.length >= CURVE_MIN_GAMES) {
|
||||
const card = element(body, "section", {
|
||||
@@ -190,7 +181,10 @@ export function createProgressionView(options: ProgressionViewOptions): Progress
|
||||
text: PROGRESSION_TEXTS.whatIsIt,
|
||||
});
|
||||
link.href = routeHash({ screen: "glossary", term: "taux-d-erreur" });
|
||||
drawnCurve = () => drawCurve(svg, points);
|
||||
// Le vert dit « mieux », jamais « dernier » : seulement si le
|
||||
// taux baisse.
|
||||
const better = trend.kind === "down";
|
||||
drawnCurve = () => drawCurve(svg, points, better);
|
||||
drawnCurve();
|
||||
}
|
||||
|
||||
@@ -203,7 +197,8 @@ export function createProgressionView(options: ProgressionViewOptions): Progress
|
||||
}).setAttribute("role", "status");
|
||||
}
|
||||
|
||||
// « Créer des exercices » : la feuille du prompt, étape 1f ; bouton secondaire.
|
||||
// « Demander des exercices à une IA » : la feuille du prompt, étape
|
||||
// 1f ; bouton secondaire avec l'icône de copie.
|
||||
if (points.length > 0) {
|
||||
const actions = element(body, "div", { className: "progression-actions" });
|
||||
const exercises = element(actions, "a", {
|
||||
@@ -228,14 +223,16 @@ export function createProgressionView(options: ProgressionViewOptions): Progress
|
||||
li.className = "progression-item";
|
||||
li.dataset.id = entry.id;
|
||||
const date = PROGRESSION_TEXTS.date(entry.startedAt);
|
||||
const result = PROGRESSION_TEXTS.result(entry.result);
|
||||
const result = PROGRESSION_TEXTS.result(entry.result, entry.cube);
|
||||
// Une ligne, un seul lien : le bilan (l'analyse s'ouvre depuis
|
||||
// lui), chevron à droite.
|
||||
const game = element(li, "a", { className: "progression-game" });
|
||||
game.href = routeHash({ screen: "summary", game: entry.id });
|
||||
game.setAttribute("aria-label", PROGRESSION_TEXTS.openSummary(result, date));
|
||||
const text = element(game, "span", { className: "progression-text" });
|
||||
element(text, "strong", { text: result });
|
||||
element(text, "span", {
|
||||
className: "muted",
|
||||
className: "caption",
|
||||
text: `${date} · ${PROGRESSION_TEXTS.level(levelName(entry))}`,
|
||||
});
|
||||
const rate = element(game, "span", { className: "progression-rate" });
|
||||
@@ -247,12 +244,7 @@ export function createProgressionView(options: ProgressionViewOptions): Progress
|
||||
element(rate, "strong", { text: String(errorRateMillis(entry.errorRate.perDecision)) });
|
||||
element(rate, "span", { className: "caption", text: PROGRESSION_TEXTS.rateLabel });
|
||||
}
|
||||
const analysis = element(li, "a", {
|
||||
className: "button secondary progression-analysis",
|
||||
text: PROGRESSION_TEXTS.analysis,
|
||||
});
|
||||
analysis.href = routeHash({ screen: "analysis", game: entry.id, turn: null });
|
||||
analysis.setAttribute("aria-label", PROGRESSION_TEXTS.analysisLabel(date));
|
||||
game.append(icon("chevron"));
|
||||
return li;
|
||||
}
|
||||
|
||||
@@ -260,58 +252,55 @@ export function createProgressionView(options: ProgressionViewOptions): Progress
|
||||
}
|
||||
|
||||
/** La courbe : un point par partie analysée, de gauche (la plus ancienne)
|
||||
* à droite ; l'axe va de 0 au plus haut taux, en millièmes ; le dernier
|
||||
* point en verdict-good (charte). */
|
||||
* à droite, de 0 (la ligne du bas, seule ligne d'axe) au plus haut taux. Le
|
||||
* dernier point est plus gros, à l'encre du texte, en verdict-good si le
|
||||
* taux baisse (better) ; les valeurs, en millièmes, s'écrivent au-dessus du
|
||||
* premier et du dernier point seulement (charte 1h). */
|
||||
function drawCurve(
|
||||
svg: SVGSVGElement,
|
||||
points: readonly { readonly id: string; readonly perDecision: number }[],
|
||||
better: boolean,
|
||||
): void {
|
||||
const width = Math.round(svg.getBoundingClientRect().width || 360);
|
||||
const height = CURVE_HEIGHT;
|
||||
svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
||||
svg.replaceChildren();
|
||||
const millis = points.map((p) => errorRateMillis(p.perDecision));
|
||||
// Le haut de l'axe : le plus haut taux, arrondi à la dizaine au-dessus.
|
||||
const top = Math.max(10, Math.ceil(Math.max(...millis) / 10) * 10);
|
||||
const top = Math.max(1, ...millis);
|
||||
const inner = width - PAD.left - PAD.right;
|
||||
const step = points.length > 1 ? inner / (points.length - 1) : 0;
|
||||
const x = (i: number) => PAD.left + i * step;
|
||||
const y = (value: number) => PAD.top + (1 - value / top) * (height - PAD.top - PAD.bottom);
|
||||
for (const level of [top, 0]) {
|
||||
svgElement(svg, "line", {
|
||||
x1: PAD.left,
|
||||
x2: width - PAD.right,
|
||||
y1: y(level),
|
||||
y2: y(level),
|
||||
class: "curve-axis",
|
||||
});
|
||||
const label = svgElement(svg, "text", {
|
||||
x: PAD.left - 6,
|
||||
y: y(level) + 4,
|
||||
class: "curve-label",
|
||||
"text-anchor": "end",
|
||||
});
|
||||
label.textContent = String(level);
|
||||
}
|
||||
svgElement(svg, "line", {
|
||||
x1: PAD.left,
|
||||
x2: width - PAD.right,
|
||||
y1: y(0),
|
||||
y2: y(0),
|
||||
class: "curve-axis",
|
||||
});
|
||||
const line = points.map((p, i) => `${x(i)},${y(p.perDecision * 1000)}`).join(" ");
|
||||
svgElement(svg, "polyline", { points: line, class: "curve-line" });
|
||||
const lastIndex = points.length - 1;
|
||||
points.forEach((point, i) => {
|
||||
const last = i === points.length - 1;
|
||||
const last = i === lastIndex;
|
||||
const cy = y(point.perDecision * 1000);
|
||||
const dot = svgElement(svg, "circle", {
|
||||
cx: x(i),
|
||||
cy: y(point.perDecision * 1000),
|
||||
r: last ? 6 : 4,
|
||||
class: last ? "progression-dot last" : "progression-dot",
|
||||
cy,
|
||||
r: last ? 7 : 4,
|
||||
class: last ? `progression-dot last${better ? " better" : ""}` : "progression-dot",
|
||||
"data-id": point.id,
|
||||
"data-rate": String(point.perDecision),
|
||||
});
|
||||
dot.setAttribute("aria-hidden", "true");
|
||||
if (points.length <= LABELLED_POINTS) {
|
||||
if (i === 0 || last) {
|
||||
const value = svgElement(svg, "text", {
|
||||
x: x(i),
|
||||
y: y(point.perDecision * 1000) - 10,
|
||||
class: last ? "progression-value last" : "progression-value",
|
||||
"text-anchor": "middle",
|
||||
y: cy - (last ? 14 : 11),
|
||||
class: last
|
||||
? `progression-value last${better ? " better" : ""}`
|
||||
: "progression-value",
|
||||
"text-anchor": i === 0 && !last ? "start" : last && i > 0 ? "end" : "middle",
|
||||
});
|
||||
value.textContent = String(millis[i]);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -38,6 +38,8 @@ import {
|
||||
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";
|
||||
@@ -86,6 +88,25 @@ test("l'adresse #/progression et celle de « Créer des exercices »", () => {
|
||||
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);
|
||||
|
||||
@@ -226,16 +226,17 @@ Ajoutées avec l'écran de progression (étape 1h), dans le même schéma :
|
||||
| Écran | Adresse |
|
||||
| --- | --- |
|
||||
| Progression : courbe du taux d'erreur et parties terminées | `#/progression` |
|
||||
| Prompt « Créer des exercices », depuis la progression | `#/progression/ia` |
|
||||
| Prompt « Demander des exercices à une IA », depuis la progression | `#/progression/ia` |
|
||||
|
||||
- La ligne de progression de l'accueil est un lien vers `#/progression`.
|
||||
L'écran est une page, comme l'analyse : « Retour » (et Échap, et le
|
||||
retour du téléphone) revient à l'écran d'avant, l'accueil s'il a été
|
||||
ouvert par son adresse.
|
||||
L'écran est une page, comme la liste des leçons : pas de bouton
|
||||
« Retour », la barre du haut, Échap et le retour du téléphone suffisent.
|
||||
Échap revient à l'écran d'avant, l'accueil s'il a été ouvert par son
|
||||
adresse.
|
||||
- Chaque partie de la liste est un lien vers son bilan
|
||||
(`#/parties/<id>/bilan`) ; « Analyse » vers son analyse
|
||||
(`#/parties/<id>/analyse`). Fermer le bilan ramène à la progression.
|
||||
- « Créer des exercices » suit la règle des prompts (amendement du
|
||||
(`#/parties/<id>/bilan`), d'où s'ouvre son analyse. Fermer le bilan
|
||||
ramène à la progression.
|
||||
- « Demander des exercices à une IA » suit la règle des prompts (amendement du
|
||||
2026-11-10) : le bouton ouvre la feuille du prompt, dans sa propre
|
||||
feuille au-dessus de la progression (« ‹ Progression »), où le texte se
|
||||
relit avant d'être copié. Rechargée, l'adresse recalcule le prompt à
|
||||
|
||||
@@ -255,14 +255,28 @@ Maquette « Progression ».
|
||||
|
||||
- Une phrase de synthèse, factuelle (« Sur vos 8 dernières parties, vous
|
||||
faites moins d'erreurs : votre taux d'erreur est passé de 14 à 7. »). Si la
|
||||
tendance monte, la phrase reste encourageante et sans reproche (« Votre
|
||||
taux d'erreur remonte un peu ces derniers jours. Les leçons sont là
|
||||
pour reprendre un point. »).
|
||||
tendance monte, la phrase reste encourageante et sans reproche, sans
|
||||
minimiser ni dramatiser (« Sur vos 8 dernières parties, votre taux
|
||||
d'erreur remonte, de 7 à 12. Les leçons sont là pour reprendre un
|
||||
point. »).
|
||||
- La courbe du taux d'erreur par partie terminée, avec le rappel « plus bas,
|
||||
c'est mieux » dans son titre, le dernier point en `verdict-good`, et un lien
|
||||
vers la fiche « taux d'erreur ».
|
||||
- La liste des parties (résultat, date, niveau de l'ordinateur, taux d'erreur
|
||||
en chiffre gras aligné à droite), chacune ouvrant son bilan.
|
||||
c'est mieux » dans son titre et un lien vers la fiche « taux d'erreur ».
|
||||
Le dernier point est plus gros, à l'encre du texte ; il ne passe en
|
||||
`verdict-good` que si la tendance baisse (le vert dit « mieux », jamais
|
||||
« dernier »). Les valeurs s'écrivent au-dessus du premier et du dernier
|
||||
point seulement ; l'axe ne porte que la ligne du 0, sans chiffres qui
|
||||
doublent ceux des points.
|
||||
- La liste des parties : chaque ligne est un seul lien vers son bilan
|
||||
(chevron à droite), sans bouton par ligne ; l'analyse s'ouvre depuis le
|
||||
bilan. Sur la ligne : le résultat court (« Gagnée, 1 point », « Perdue,
|
||||
gammon, 2 points », jamais « partie simple »), puis en `caption`
|
||||
`text-muted` la date et le niveau (« 12 nov. 2026 · contre
|
||||
Intermédiaire », le niveau sans couleur), et à droite le taux d'erreur en
|
||||
chiffre gras.
|
||||
- Sans partie terminée : une phrase et « Nouvelle partie » en action
|
||||
principale, pleine largeur en portrait.
|
||||
- Pas de bouton « Retour » dans l'en-tête : la barre du haut et le retour du
|
||||
téléphone suffisent, comme sur la liste des leçons.
|
||||
- Moins de trois parties analysées : pas de courbe, une phrase « Votre
|
||||
courbe apparaîtra après trois parties. ».
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ Vous avez 5 et 2.
|
||||
|
||||
Ici, les pions de l'ordinateur ont 81 flèches à parcourir, les vôtres 110 : il a 29 points d'avance. Son dernier pion est seul sur votre flèche 9.
|
||||
|
||||
Frappé, ce pion part sur la barre, et le compte de l'ordinateur remonte : pour l'ordinateur, ce pion compte 16 ; sur la barre, il compte 25 : 9 de plus.
|
||||
Frappé, ce pion part sur la barre : pour l'ordinateur, il comptait 16 et compte maintenant 25, soit 9 de plus.
|
||||
|
||||
### À vous
|
||||
|
||||
|
||||
@@ -296,9 +296,10 @@ milieu, quand leur nombre est impair, ne compte dans aucune).
|
||||
|
||||
## progression.hausse
|
||||
|
||||
Le taux moyen remonte : la phrase reste encourageante, sans reproche.
|
||||
Le taux moyen remonte : la phrase reste encourageante, sans reproche, sans
|
||||
minimiser ni dramatiser.
|
||||
|
||||
- Sur vos {parties} dernières parties, votre taux d'erreur remonte un peu, de {debut} à {fin}. Les leçons sont là pour reprendre un point.
|
||||
- Sur vos {parties} dernières parties, votre taux d'erreur remonte, de {debut} à {fin}. Les leçons sont là pour reprendre un point.
|
||||
|
||||
## progression.stable
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ d'après le cadrage, **à relire** par Pierre.
|
||||
| [videau.md](videau.md) | Expliquer cette décision de videau | Feuille du maître, bilan, analyse |
|
||||
| [bilan.md](bilan.md) | Bilan de ma partie | Bilan de fin de partie |
|
||||
| [concept.md](concept.md) | Expliquer ce concept | Fiche d'un terme du glossaire |
|
||||
| [exercices.md](exercices.md) | Créer des exercices | Écran de progression (étape 1h) |
|
||||
| [exercices.md](exercices.md) | Demander des exercices à une IA | Écran de progression (étape 1h) |
|
||||
|
||||
Chaque prompt contient :
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Créer des exercices
|
||||
|
||||
Le bouton « Créer des exercices » de l'écran de progression (étape 1h,
|
||||
Le bouton « Demander des exercices à une IA » de l'écran de progression (étape 1h,
|
||||
`#/progression/ia`). Il rassemble les erreurs de vos dernières parties
|
||||
analysées (les 20 plus récentes au plus), et ce qui y revient. Texte sous licence CC BY-SA 4.0 (voir
|
||||
[README.md](README.md)).
|
||||
|
||||
@@ -5,7 +5,7 @@ Fonctionnalité: Suivre ma progression
|
||||
votre taux d'erreur partie après partie : plus il est bas, mieux vous avez
|
||||
joué. Chaque partie terminée y figure avec son résultat, sa date, le niveau
|
||||
de l'ordinateur et son taux d'erreur, celui de son bilan ; touchez-la pour
|
||||
rouvrir son bilan, ou son analyse. Une phrase du maître dit, en chiffres,
|
||||
rouvrir son bilan, et de là son analyse. Une phrase du maître dit, en chiffres,
|
||||
comment évolue votre taux d'erreur.
|
||||
|
||||
Scénario: Comment voir ma progression ?
|
||||
@@ -44,7 +44,7 @@ Fonctionnalité: Suivre ma progression
|
||||
Et les deux premières parties n'ont pas été analysées de nouveau
|
||||
|
||||
Scénario: Comment créer des exercices sur mes erreurs ?
|
||||
« Créer des exercices » prépare une question pour une IA : vos erreurs
|
||||
« Demander des exercices à une IA » prépare une question : vos erreurs
|
||||
les plus coûteuses sur vos dernières parties, avec leur coût selon le
|
||||
maître, ce qui y revient (pions frappés, points faits par le coup du
|
||||
maître et pas par le vôtre), et la position de la plus coûteuse. Votre
|
||||
|
||||
+105
-54
@@ -7,14 +7,17 @@
|
||||
//
|
||||
// Usage : node progression.mjs <adresse du site> <chromium|firefox> <dossier des captures>
|
||||
// Lancé par tests/browser.sh. L'écran sans partie terminée, puis trois
|
||||
// fins de course courtes jouées par l'interface, depuis un lien de
|
||||
// position, avec des dés imposés (comme bilan.mjs) : le joueur a 6-1 et
|
||||
// deux pions, sur 6 et 1 ; il joue 6/5/off (grosse erreur, l'ordinateur
|
||||
// double et le joueur passe), puis 6/off 1/off (gagne), puis de nouveau
|
||||
// 6/5/off. Vérifie que la ligne de l'accueil mène à #/progression, que la
|
||||
// courbe a trois points égaux, dans l'ordre, aux taux d'erreur des trois
|
||||
// bilans, la phrase du maître, la liste reliée aux bilans et aux analyses,
|
||||
// et « Créer des exercices » (feuille du prompt). Rien d'interactif sous
|
||||
// courses courtes jouées jusqu'au bout par l'interface, depuis un lien de
|
||||
// position (celle des tests du domaine), dés d'une graine fixe : le joueur
|
||||
// joue toujours le premier pion jouable et la première case d'arrivée, et
|
||||
// prend les doubles, ce qui donne plusieurs décisions par partie et des
|
||||
// taux d'erreur réalistes. Vérifie que la ligne de l'accueil (44 px au
|
||||
// moins) mène à #/progression, que la carte de la partie ne montre plus la
|
||||
// partie finie, que la courbe a trois points égaux, dans l'ordre, aux taux
|
||||
// d'erreur des trois bilans, le dernier point en vert seulement si le taux
|
||||
// baisse, la phrase du maître, la liste (un lien par partie, vers son
|
||||
// bilan) et « Demander des exercices à une IA » (feuille du prompt). Rien
|
||||
// d'interactif sous
|
||||
// une barre d'action, aucune erreur ni violation de la politique de
|
||||
// sécurité. Captures (Chromium) dans <dossier des captures>/progression/,
|
||||
// en 412×915 et 915×412.
|
||||
@@ -35,9 +38,8 @@ mkdirSync(dir, { recursive: true });
|
||||
const PORTRAIT = { width: 412, height: 915 };
|
||||
const LANDSCAPE = { width: 915, height: 412 };
|
||||
const SIZES = [PORTRAIT, LANDSCAPE];
|
||||
// Le joueur au trait : un pion sur 6, un sur 1 ; l'ordinateur : un sur 1,
|
||||
// un sur 2 (positions de gnubg, chacun vu de lui-même).
|
||||
const BEAROFF = "BQAACAIAAAAAAA";
|
||||
// Une course courte (Position ID de gnubg, app/tests/progression.test.ts).
|
||||
const RACE = "trsXAADs7mwAAA";
|
||||
|
||||
const problems = [];
|
||||
const log = (text) => console.log(`[${browserName}] ${text}`);
|
||||
@@ -95,7 +97,6 @@ async function until(what, fn, arg, ms = 120_000) {
|
||||
throw new Error(`attente trop longue : ${what}`);
|
||||
}
|
||||
|
||||
const idle = () => until("écran libre", () => !document.body.classList.contains("busy"));
|
||||
|
||||
/** Une capture de la recette dans les deux tailles (Chromium seulement),
|
||||
* après la règle commune ; settle replace l'écran après le changement de
|
||||
@@ -104,6 +105,8 @@ async function capture(name, settle = async () => {}) {
|
||||
if (!full) {
|
||||
return;
|
||||
}
|
||||
// Le bandeau « Reprendre l'ancienne » (10 s) d'une partie commencée.
|
||||
await until("bandeau parti", () => document.getElementById("banner").hidden, null, 15_000);
|
||||
for (const size of SIZES) {
|
||||
await page.setViewportSize(size);
|
||||
await sleep(300);
|
||||
@@ -139,26 +142,49 @@ async function startFrom(position) {
|
||||
);
|
||||
}
|
||||
|
||||
const place = (point) => (point === 0 ? "off-player" : `p${point}`);
|
||||
|
||||
/** Lance les dés imposés et joue un coup, demi-coup par demi-coup. */
|
||||
async function play(dice, halfMoves) {
|
||||
await page.evaluate((d) => {
|
||||
window.__dice.push(...d);
|
||||
}, dice);
|
||||
await page.click("#roll");
|
||||
await idle();
|
||||
for (const [from, to] of halfMoves) {
|
||||
await page.click(`#board .hit[data-place="${place(from)}"]`);
|
||||
await idle();
|
||||
const land = page.locator(`#board .hit.can-land[data-place="${place(to)}"]`);
|
||||
if ((await land.count()) > 0) {
|
||||
await land.click();
|
||||
await idle();
|
||||
/** Joue la partie jusqu'au bout : lancer, le premier pion jouable et sa
|
||||
* première case d'arrivée, valider ; prendre les doubles ; passer quand
|
||||
* aucun coup n'est possible. */
|
||||
async function playOut() {
|
||||
for (let step = 0; step < 2000; step++) {
|
||||
const s = await page.evaluate(() => {
|
||||
const shown = (id) => {
|
||||
const el = document.getElementById(id);
|
||||
return el !== null && !el.hidden && el.closest("[hidden]") === null && !el.disabled;
|
||||
};
|
||||
return {
|
||||
over: !document.getElementById("panel-veil").hidden || /\/bilan$/.test(location.hash),
|
||||
busy: document.body.classList.contains("busy"),
|
||||
take: shown("take"),
|
||||
confirm: shown("confirm"),
|
||||
skip: shown("skip"),
|
||||
roll: shown("roll"),
|
||||
land: document.querySelector("#board .hit.can-land") !== null,
|
||||
move: document.querySelector("#board .hit.can-move") !== null,
|
||||
};
|
||||
});
|
||||
if (s.over) {
|
||||
return;
|
||||
}
|
||||
if (s.busy) {
|
||||
await sleep(100);
|
||||
} else if (s.take) {
|
||||
await page.click("#take");
|
||||
} else if (s.confirm) {
|
||||
await page.click("#confirm");
|
||||
} else if (s.skip) {
|
||||
await page.click("#skip");
|
||||
} else if (s.land) {
|
||||
await page.locator("#board .hit.can-land").first().click();
|
||||
} else if (s.move) {
|
||||
await page.locator("#board .hit.can-move").first().click();
|
||||
} else if (s.roll) {
|
||||
await page.click("#roll");
|
||||
} else {
|
||||
await sleep(100);
|
||||
}
|
||||
}
|
||||
check(!(await page.locator("#confirm").isHidden()), `coup incomplet : ${JSON.stringify(halfMoves)}`);
|
||||
await page.click("#confirm");
|
||||
throw new Error("la partie ne finit pas");
|
||||
}
|
||||
|
||||
/** Le bilan de la partie qui vient de finir : son identifiant et son taux
|
||||
@@ -176,17 +202,10 @@ async function summaryRate() {
|
||||
}));
|
||||
}
|
||||
|
||||
/** Une partie : le joueur joue 6/5/off (puis passe le double) ou 6/off
|
||||
* 1/off. */
|
||||
async function oneGame(mistake) {
|
||||
await startFrom(BEAROFF);
|
||||
if (mistake) {
|
||||
await play([6, 1], [[6, 5], [5, 0]]);
|
||||
await until("double de l'ordinateur", () => !document.getElementById("drop").hidden);
|
||||
await page.click("#drop");
|
||||
} else {
|
||||
await play([6, 1], [[6, 0], [1, 0]]);
|
||||
}
|
||||
/** Une course jouée jusqu'au bout, et le taux de son bilan. */
|
||||
async function oneGame() {
|
||||
await startFrom(RACE);
|
||||
await playOut();
|
||||
return summaryRate();
|
||||
}
|
||||
|
||||
@@ -226,14 +245,13 @@ log("progression sans partie terminée : phrase et « Nouvelle partie »");
|
||||
// ----------------------------------------------------- Trois parties jouées
|
||||
|
||||
const games = [];
|
||||
for (const mistake of [true, false, true]) {
|
||||
games.push(await oneGame(mistake));
|
||||
for (let i = 0; i < 3; i++) {
|
||||
games.push(await oneGame());
|
||||
}
|
||||
check(
|
||||
games.every((g) => g.id && g.rate !== undefined && !Number.isNaN(Number(g.rate))),
|
||||
`bilans : ${JSON.stringify(games)}`,
|
||||
);
|
||||
check(Number(games[0].rate) > 0 && Number(games[1].rate) === 0, `taux des bilans : ${JSON.stringify(games)}`);
|
||||
log(`trois parties jouées, taux des bilans : ${games.map((g) => g.rate).join(", ")}`);
|
||||
|
||||
// La ligne de l'accueil mène à la progression.
|
||||
@@ -249,9 +267,23 @@ await until("accueil", () => {
|
||||
});
|
||||
const line = await page.evaluate(() => {
|
||||
const a = document.getElementById("home-progress");
|
||||
return { tag: a.tagName, href: a.getAttribute("href"), text: a.textContent };
|
||||
return {
|
||||
tag: a.tagName,
|
||||
href: a.getAttribute("href"),
|
||||
text: a.textContent,
|
||||
height: a.getBoundingClientRect().height,
|
||||
// La carte de la partie, sans partie en cours : ni miniature ni
|
||||
// compte de la partie finie (recette 1h).
|
||||
mini: !document.querySelector("#home-game .home-mini").hidden,
|
||||
pips: !document.getElementById("home-pips").hidden,
|
||||
resume: !document.getElementById("home-resume").hidden,
|
||||
};
|
||||
});
|
||||
check(line.tag === "A" && line.href === "#/progression", `ligne de l'accueil : ${JSON.stringify(line)}`);
|
||||
check(
|
||||
line.tag === "A" && line.href === "#/progression" && line.height >= 44,
|
||||
`ligne de l'accueil : ${JSON.stringify(line)}`,
|
||||
);
|
||||
check(!line.mini && !line.pips && !line.resume, `carte de la partie finie : ${JSON.stringify(line)}`);
|
||||
await capture("3-accueil-ligne", async () => {
|
||||
await page.locator("#home-progress").scrollIntoViewIfNeeded();
|
||||
});
|
||||
@@ -267,22 +299,37 @@ const shown = await page.evaluate(() => ({
|
||||
id: c.getAttribute("data-id"),
|
||||
rate: c.getAttribute("data-rate"),
|
||||
last: c.classList.contains("last"),
|
||||
better: c.classList.contains("better"),
|
||||
r: Number(c.getAttribute("r")),
|
||||
})),
|
||||
values: [...document.querySelectorAll("#progression-curve .progression-value")].length,
|
||||
axisLabels: [...document.querySelectorAll("#progression-curve .curve-label")].length,
|
||||
items: [...document.querySelectorAll("#progression-list .progression-item")].map((li) => ({
|
||||
id: li.dataset.id,
|
||||
summary: li.querySelector(".progression-game").getAttribute("href"),
|
||||
analysis: li.querySelector(".progression-analysis").getAttribute("href"),
|
||||
links: li.querySelectorAll("a, button").length,
|
||||
result: li.querySelector(".progression-text strong").textContent,
|
||||
chevron: li.querySelector(".progression-game > svg.icon") !== null,
|
||||
rate: li.querySelector(".progression-rate")?.dataset.rate,
|
||||
})),
|
||||
glossary: document.getElementById("progression-glossary")?.getAttribute("href"),
|
||||
exercises: document.getElementById("progression-exercises")?.getAttribute("href"),
|
||||
exercisesText: document.getElementById("progression-exercises")?.textContent,
|
||||
back: document.getElementById("progression-back") !== null,
|
||||
title: document.getElementById("progression-title")?.tagName,
|
||||
}));
|
||||
check(shown.dots.length === 3, `courbe : ${shown.dots.length} points`);
|
||||
for (const [i, game] of games.entries()) {
|
||||
const dot = shown.dots[i];
|
||||
check(dot.id === game.id && dot.rate === game.rate, `point ${i + 1} : ${JSON.stringify(dot)} contre ${JSON.stringify(game)}`);
|
||||
}
|
||||
check(shown.dots[2].last && !shown.dots[0].last, "le dernier point n'est pas mis en avant");
|
||||
check(shown.dots[2].last && !shown.dots[0].last && shown.dots[2].r > shown.dots[0].r,
|
||||
"le dernier point n'est pas plus gros");
|
||||
// Vert seulement si le taux baisse (progression.baisse).
|
||||
const down = shown.summary.includes("vous faites moins d'erreurs");
|
||||
check(shown.dots[2].better === down && !shown.dots[0].better, `vert du dernier point : ${shown.summary}`);
|
||||
check(shown.values === 2 && shown.axisLabels === 0, `valeurs de la courbe : ${shown.values}, axe : ${shown.axisLabels}`);
|
||||
check(!shown.back && shown.title === "H1", `en-tête : ${JSON.stringify({ back: shown.back, title: shown.title })}`);
|
||||
check(shown.summary.startsWith("Sur vos 3 dernières parties"), `phrase : ${shown.summary}`);
|
||||
check(!shown.summary.includes("!"), `phrase : ${shown.summary}`);
|
||||
check(shown.items.length === 3 && shown.items[0].id === games[2].id, `liste : ${JSON.stringify(shown.items)}`);
|
||||
@@ -290,15 +337,19 @@ for (const item of shown.items) {
|
||||
const game = games.find((g) => g.id === item.id);
|
||||
check(
|
||||
item.summary === `#/parties/${item.id}/bilan` &&
|
||||
item.analysis === `#/parties/${item.id}/analyse` &&
|
||||
item.links === 1 &&
|
||||
item.chevron &&
|
||||
item.rate === game.rate,
|
||||
`ligne : ${JSON.stringify(item)}`,
|
||||
);
|
||||
}
|
||||
check(shown.glossary === "#/glossaire/taux-d-erreur", `lien du glossaire : ${shown.glossary}`);
|
||||
check(shown.exercises === "#/progression/ia", `exercices : ${shown.exercises}`);
|
||||
check(
|
||||
shown.exercises === "#/progression/ia" && shown.exercisesText === "Demander des exercices à une IA",
|
||||
`exercices : ${shown.exercises} « ${shown.exercisesText} »`,
|
||||
);
|
||||
await capture("2-trois-parties");
|
||||
log(`courbe : ${shown.dots.map((d) => d.rate).join(", ")} ; « ${shown.summary} »`);
|
||||
log(`courbe : ${shown.dots.map((d) => d.rate).join(", ")} ; « ${shown.summary} » ; résultats : ${shown.items.map((i) => i.result).join(" | ")}`);
|
||||
|
||||
// Une ligne ouvre le bilan ; le fermer ramène à la progression.
|
||||
await page.click(`#progression-list .progression-item[data-id="${games[0].id}"] .progression-game`);
|
||||
@@ -313,7 +364,7 @@ await page.click("#summary-close");
|
||||
await progressionReady();
|
||||
log("une ligne ouvre le bilan, la croix ramène à la progression");
|
||||
|
||||
// « Créer des exercices » : la feuille du prompt.
|
||||
// « Demander des exercices à une IA » : la feuille du prompt.
|
||||
await page.click("#progression-exercises");
|
||||
await until(
|
||||
"prompt des exercices",
|
||||
@@ -324,11 +375,11 @@ await until(
|
||||
const prompt = await page.textContent("#prompt-text");
|
||||
check(prompt.includes("Voici mes 3 dernières parties"), `prompt : ${prompt}`);
|
||||
check(prompt.includes("Ce qui revient dans ces erreurs"), `prompt : ${prompt}`);
|
||||
check(prompt.includes("Je débute au backgammon."), `prompt : ${prompt}`);
|
||||
check(/Je débute au backgammon\.|Je suis un joueur de niveau /.test(prompt), `prompt : ${prompt}`);
|
||||
check(!/[{}]/.test(prompt), `repère non rempli : ${prompt}`);
|
||||
await page.keyboard.press("Escape");
|
||||
await until("retour à la progression", () => location.hash === "#/progression");
|
||||
log("« Créer des exercices » : prompt avec les erreurs et leurs thèmes");
|
||||
log("« Demander des exercices à une IA » : prompt avec les erreurs et leurs thèmes");
|
||||
|
||||
const csp = await page.evaluate(() => window.__csp);
|
||||
for (const violation of csp) {
|
||||
|
||||
+35
-23
@@ -3503,9 +3503,17 @@ html.landscape .summary-panel > .summary-end {
|
||||
.curve-band { fill: transparent; cursor: pointer; }
|
||||
.curve-band.selected { fill: var(--accent); fill-opacity: 0.08; }
|
||||
|
||||
/* La progression (#/progression, 1h) : la phrase du maître, la courbe du
|
||||
* taux d'erreur (le dernier point en verdict-good), puis les parties. La
|
||||
* page reprend l'en-tête et les cartes de l'analyse. */
|
||||
/* La progression (#/progression, 1h) : le titre, la phrase du maître, la
|
||||
* courbe du taux d'erreur, puis les parties (charte, ecrans-etape-1.md,
|
||||
* « 1h »). Les cartes reprennent celles de l'analyse. */
|
||||
.progression-title {
|
||||
margin: var(--space-3) 0 var(--space-2);
|
||||
font-size: 32px;
|
||||
line-height: 36px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.progression-summary {
|
||||
margin: 0 0 var(--space-4);
|
||||
font-size: 17px;
|
||||
@@ -3523,10 +3531,13 @@ html.landscape .summary-panel > .summary-end {
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
/* Le dernier point, plus gros, à l'encre du texte ; en verdict-good
|
||||
* seulement si le taux baisse (le vert dit « mieux », jamais « dernier »). */
|
||||
.progression-dot { fill: var(--text); }
|
||||
.progression-dot.last { fill: var(--verdict-good); }
|
||||
.progression-value { fill: var(--muted); font-size: 12px; }
|
||||
.progression-value.last { fill: var(--verdict-good); font-weight: 700; }
|
||||
.progression-dot.better { fill: var(--verdict-good); }
|
||||
.progression-value { fill: var(--muted); font-size: 13px; }
|
||||
.progression-value.last { fill: var(--text); font-weight: 700; }
|
||||
.progression-value.better { fill: var(--verdict-good); }
|
||||
|
||||
.progression .link-button {
|
||||
display: inline-flex;
|
||||
@@ -3539,6 +3550,13 @@ html.landscape .summary-panel > .summary-end {
|
||||
margin: 0 0 var(--space-4);
|
||||
}
|
||||
|
||||
/* Sans partie terminée : « Nouvelle partie », pleine largeur en portrait. */
|
||||
@media (orientation: portrait) {
|
||||
.progression-empty .button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.progression-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -3549,27 +3567,30 @@ html.landscape .summary-panel > .summary-end {
|
||||
}
|
||||
|
||||
.progression-item {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
/* Toute la ligne ouvre le bilan de la partie. */
|
||||
/* Une ligne, un seul lien vers le bilan : résultat et date à gauche, taux
|
||||
* en gras, puis le chevron. */
|
||||
.progression-game {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: var(--space-3);
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
min-height: 44px;
|
||||
min-height: 56px;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.progression-game > .icon {
|
||||
flex: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.progression-text {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
@@ -3577,11 +3598,6 @@ html.landscape .summary-panel > .summary-end {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.progression-text .muted {
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.progression-rate {
|
||||
display: flex;
|
||||
flex: none;
|
||||
@@ -3601,10 +3617,6 @@ html.landscape .summary-panel > .summary-end {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.progression-analysis {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
/* Le visualiseur : « ‹ Coup 7 sur 24 · 6-1 › », le plateau d'avant le
|
||||
* coup, la phrase du maître, votre coup contre celui du maître. */
|
||||
.viewer-nav {
|
||||
|
||||
Reference in New Issue
Block a user