From c7a54a8c8118a6eb775320c2efd1707520271f4d Mon Sep 17 00:00:00 2001 From: Pierre MARTIN Date: Tue, 10 Nov 2026 05:36:11 +0100 Subject: [PATCH] =?UTF-8?q?Conseil=20du=20ma=C3=AEtre=20:=20rang=20cit?= =?UTF-8?q?=C3=A9=20seulement=20pour=20un=20coup=20proche=20ou=20une=20imp?= =?UTF-8?q?r=C3=A9cision=20au=202e=20ou=203e=20choix=20;=201d=20faite,=20p?= =?UTF-8?q?lan=20clos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- app/src/domain/analysis/phrases.ts | 14 +-- app/src/ui/advice-view.ts | 16 +-- app/tests/advice.test.ts | 24 ++--- docs/etape-1.md | 2 +- docs/maitre/phrases.md | 29 ++++-- docs/plans/2026-11-03-conseil-du-maitre.md | 115 --------------------- tests/browser/maitre.mjs | 2 +- 7 files changed, 49 insertions(+), 153 deletions(-) delete mode 100644 docs/plans/2026-11-03-conseil-du-maitre.md diff --git a/app/src/domain/analysis/phrases.ts b/app/src/domain/analysis/phrases.ts index 276ee0f..49b77d7 100644 --- a/app/src/domain/analysis/phrases.ts +++ b/app/src/domain/analysis/phrases.ts @@ -73,7 +73,7 @@ export const PHRASE_KEYS: readonly string[] = [ "coup.douteux", "coup.mauvais", "coup.tres-mauvais", - "coup.tres-mauvais.loin", + "coup.loin", "fait.frappe", "fait.frappe-plusieurs", "fait.point", @@ -145,13 +145,15 @@ function moveWords(phrases: MasterPhrases, advice: MoveAdvice): MasterWords { base = fill("coup.meilleur"); fact = selfFact(phrases, advice.eventId, advice.played.notation, advice.playedFacts); } else { + // Le rang n'est cité que pour un bon coup non meilleur ou une + // imprécision, au 2e ou 3e choix : il mesure mal la gravité d'une + // erreur, et le classement est dans les chiffres du moteur. + const near = advice.rank <= 3; const key = { - good: "coup.proche", - doubtful: "coup.douteux", + good: near ? "coup.proche" : "coup.loin", + doubtful: near ? "coup.douteux" : "coup.loin", bad: "coup.mauvais", - // Une grosse erreur ne cite son rang que s'il est proche (2e ou - // 3e choix) ; plus loin, le classement est dans les chiffres. - veryBad: advice.rank <= 3 ? "coup.tres-mauvais" : "coup.tres-mauvais.loin", + veryBad: "coup.tres-mauvais", }[verdict]; base = fill(key); fact = differenceFact(phrases, advice); diff --git a/app/src/ui/advice-view.ts b/app/src/ui/advice-view.ts index 50b7e1a..3f43fbf 100644 --- a/app/src/ui/advice-view.ts +++ b/app/src/ui/advice-view.ts @@ -121,14 +121,14 @@ export function createAdviceView(options: AdviceViewOptions): AdviceView { verdictLine.append(icon(VERDICT_ICONS[words.verdict])); element(verdictLine, "span", { text: `${words.word} · ${words.context}` }); title.textContent = words.title; - element(sheet, "p", { - id: "explain-phrase", - className: "explain-phrase", - // Le titre dit déjà le coup du maître : la phrase ne le répète pas. - text: words.phrase.startsWith(`${words.title}. `) - ? words.phrase.slice(words.title.length + 2) - : words.phrase, - }); + // Le titre dit déjà le coup du maître : la phrase ne le répète pas + // (et disparaît si elle ne disait que lui). + const rest = words.phrase.startsWith(`${words.title}.`) + ? words.phrase.slice(words.title.length + 1).trim() + : words.phrase; + if (rest !== "") { + element(sheet, "p", { id: "explain-phrase", className: "explain-phrase", text: rest }); + } const body = element(sheet, "div", { className: "explain-body" }); const boardBox = element(body, "div", { className: "explain-board-box" }); diff --git a/app/tests/advice.test.ts b/app/tests/advice.test.ts index aae7517..75b06e1 100644 --- a/app/tests/advice.test.ts +++ b/app/tests/advice.test.ts @@ -163,25 +163,25 @@ test("le coup du maître est nommé, et le fait qui distingue les coups vient du assert.deepEqual(worst.bestFacts.madePoints, [5]); const said = masterWords(phrases, worst); assert.equal(said.title, "J'aurais joué 8/5 6/5"); - assert.equal( - said.phrase, - "J'aurais joué 8/5 6/5. Votre 24/23 13/10 était le 2e choix. Mon coup fait votre point 5.", - ); + assert.equal(said.phrase, "J'aurais joué 8/5 6/5. Mon coup fait votre point 5."); }); -test("grosse erreur : le rang n'est cité que pour un 2e ou 3e choix", async () => { +test("le rang n'est cité que pour un bon coup non meilleur ou une imprécision, au 2e ou 3e choix", async () => { const engine = await loadNodeEngine(); const worst = (await adviceOn( engine, await playOpening(engine, [3, 1], "24/23 13/10"), )) as MoveAdvice; - for (const rank of [2, 3, 4, 7, 15]) { - const { phrase } = masterWords(phrases, { ...worst, rank }); - if (rank <= 3) { - assert.ok(phrase.includes(`était le ${rank}e choix`), phrase); - } else { - assert.ok(!phrase.includes("choix"), phrase); - assert.equal(phrase, "J'aurais joué 8/5 6/5. Mon coup fait votre point 5."); + for (const skill of ["none", "doubtful", "bad", "veryBad"] as const) { + for (const rank of [2, 3, 4, 7, 15]) { + const { phrase } = masterWords(phrases, { ...worst, skill, rank }); + const cited = (skill === "none" || skill === "doubtful") && rank <= 3; + if (cited) { + assert.ok(phrase.includes(`le ${rank}e choix`), `${skill} ${rank} : ${phrase}`); + } else { + assert.ok(!phrase.includes("choix"), `${skill} ${rank} : ${phrase}`); + assert.equal(phrase, "J'aurais joué 8/5 6/5. Mon coup fait votre point 5."); + } } } }); diff --git a/docs/etape-1.md b/docs/etape-1.md index 7dd86f3..f506d6b 100644 --- a/docs/etape-1.md +++ b/docs/etape-1.md @@ -20,7 +20,7 @@ Repris du cadrage (modules, écrans clés, IA générative en V1) et des ADR 001 | 1a. Moteur pour l'analyse (fait) | Niveaux de jeu (plis et bruit de GNU Backgammon : débutant à expert), classement des coups d'un jet avec équités, analyse d'un coup joué et d'une décision de videau, chance d'un jet ; exposés par `engine.h`, `engine-cli` et le Worker, avec positions de référence comparées à gnubg de bureau | — | | 1b. Parties enregistrées (fait) | Stockage IndexedDB derrière l'interface de stockage existante : plusieurs parties, reprise automatique, branches (« Rejouer d'ici » crée une branche, l'originale reste intacte et rechargeable), export SGF avec variantes | — | | 1c. Match et réglages (fait) | Partie libre ou match en N points, règle Crawford, règle Jacoby, choix du niveau de l'ordinateur, conseil du maître activable ou non ; écran de nouvelle partie | 1a, 1b | -| 1d. Conseil du maître | Après chaque coup du joueur, jamais avant (ADR 0011) : appréciation du moteur (bon coup, imprécision, erreur, grosse erreur) avec mot, icône et couleur, phrase du maître fondée sur les chiffres du moteur, coup recommandé montré sur demande ; aussi pour les décisions de videau ; calcul en arrière-plan sans retarder le jeu | 1a | +| 1d. Conseil du maître (fait) | Après chaque coup du joueur, jamais avant (ADR 0011) : appréciation du moteur (bon coup, imprécision, erreur, grosse erreur) avec mot, icône et couleur, phrase du maître fondée sur les chiffres du moteur, coup recommandé montré sur demande ; aussi pour les décisions de videau ; calcul en arrière-plan sans retarder le jeu | 1a | | 1e. Fin de partie et analyse | Bilan (ADR 0011) : résultat, coups par appréciation, taux d'erreur comparé à la chance, « le coup à revoir » ; écran d'analyse : graphe d'équité (toucher un point ouvre la position), liste des erreurs filtrable, visualiseur pas à pas coup joué contre meilleur coup, « Rejouer d'ici » | 1a, 1b, 1d | | 1f. Prompts à copier | Gabarits versionnés et testés (`docs/prompts/` ou équivalent, source unique), bouton « Demander à une IA » sur le plateau, le conseil du maître, l'analyse et la fin de partie ; le moteur décide, l'IA explique | 1d, 1e | | 1g. Accueil, tutoriel et leçons | Accueil (reprendre, nouvelle partie, prochaine leçon, progression résumée), tutoriel des règles interactif et illustré (0e.5), cinq premières leçons numérotées (ADR 0011) avec positions et « à vous de jouer », progression locale ; contenus CC BY-SA 4.0 en brouillon, à relire par un joueur fort | 0e.5, 1a | diff --git a/docs/maitre/phrases.md b/docs/maitre/phrases.md index a8499f0..d1f2658 100644 --- a/docs/maitre/phrases.md +++ b/docs/maitre/phrases.md @@ -33,7 +33,7 @@ mots entre accolades sont remplis avec les données du moteur : | `{best}` | le coup du maître, en notation de gnubg (« 8/5 6/5 ») | | `{played}` | votre coup, même notation | | `{move}` | le coup dont parle le fait (le vôtre ou celui du maître) | -| `{rank}` | le rang de votre coup dans le classement du moteur (2, 3…) | +| `{rank}` | le rang de votre coup dans le classement du moteur (2 ou 3 seulement) | | `{points}` | « votre point 5 » ou « vos points 5 et 7 » | | `{roll}` | le jet, « 3-1 » | | `{hits}` | le nombre de pions frappés, en chiffres | @@ -76,26 +76,35 @@ Tout ce qui précède la première section `## mot.…` n'est pas lu. ## coup.proche +« Bon coup » sans être le meilleur, votre coup étant le 2e ou le 3e choix du +moteur. Le rang n'est cité qu'ici et dans coup.douteux : il mesure mal la +gravité d'une erreur (un 2e choix peut être loin du premier), et le +classement complet est dans « Voir les chiffres du moteur ». + - Tout près : j'aurais joué {best}, votre coup était le {rank}e choix. ## coup.douteux +Imprécision, votre coup étant le 2e ou le 3e choix. + - J'aurais joué {best}. Votre coup était le {rank}e choix, tout près. +## coup.loin + +« Bon coup » non meilleur ou imprécision, votre coup étant au-delà du 3e +choix : le rang n'est pas cité. + +- J'aurais joué {best}. + ## coup.mauvais -- J'aurais joué {best}. Votre {played} était le {rank}e choix. +Erreur : le rang n'est jamais cité. + +- J'aurais joué {best}. ## coup.tres-mauvais -Votre coup est le 2e ou le 3e choix du moteur. - -- J'aurais joué {best}. Votre {played} était le {rank}e choix. - -## coup.tres-mauvais.loin - -Votre coup est plus loin dans le classement : le rang n'est pas dit ici, il -se lit dans « Voir les chiffres du moteur ». +Grosse erreur : le rang n'est jamais cité. - J'aurais joué {best}. diff --git a/docs/plans/2026-11-03-conseil-du-maitre.md b/docs/plans/2026-11-03-conseil-du-maitre.md deleted file mode 100644 index bc76462..0000000 --- a/docs/plans/2026-11-03-conseil-du-maitre.md +++ /dev/null @@ -1,115 +0,0 @@ -# Conseil du maître (1d) - -- **Date** : 2026-11-03 -- **Étape** : 1d (`docs/etape-1.md`) -- **Statut** : prévu (après 1c) -- **Exécutant** : sous-agent general-purpose, dans un worktree - -## Objectif - -Le coach bienveillant, « mis en avant mais discret » (Pierre, 2026-11-03) : -après chaque coup et chaque décision de videau du joueur, jamais avant -(ADR 0011), une appréciation et une phrase du maître fondées sur les chiffres -du moteur. - -## Références - -- ADR 0011 ; maquette « conseil du maître après le coup » du canevas - https://claude.ai/artifact/CQw6vbDpi6XQqVWQa4sANe ; règles de la charte - (`docs/identite/charte/`, dont interaction.md) ; la ligne réservée de 44 px - de la révision UX. -- Moteur : `analyseMove`, `analyseCube`, `luck`, seuils `SKILL_THRESHOLDS` - (gnubg : 0,03 / 0,06 / 0,12). - -## Consignes - -1. **Analyse en arrière-plan** : après la validation d'un coup (ou une - décision de videau), l'analyse à 2 plis part dans le Worker sans retarder le - tour de l'ordinateur ; son résultat est rattaché au coup dans le journal - (événement d'analyse rétrocompatible, ou cache séparé justifié) pour servir - au bilan (1e) sans recalcul. -2. **Ligne du maître** : icône d'appréciation, mot en couleur (« Bon coup. », - « Imprécision. », « Erreur. », « Grosse erreur. » (choix de Pierre du 2026-11-09, plutôt que « gaffe ») : correspondance avec les seuils - de gnubg documentée), phrase courte en italique, chevron. Toucher ouvre la - feuille d'explication : coup joué et meilleur coup montrés sur le plateau - (pions animés, ADR 0011 « comparaison visuelle »), écart d'équité seulement - sur demande, lien vers la fiche du glossaire du concept en jeu si le moteur - permet de le déterminer, sinon rien. -3. **Phrases du maître** : gabarits versionnés en français dans une source - unique sous `docs/` (CC BY-SA 4.0). Référence de ton, approuvée par Pierre - le 2026-11-09 : la section « Les phrases du maître » de - `docs/identite/charte/ecrans-etape-1.md` (1re personne et vouvoiement, - faits du moteur uniquement, moins de 140 caractères, ton sobre sans - exclamation, formules qui tournent pour les bons coups, exemples). Un test - vérifie la longueur et l'absence de « ! » sur chaque gabarit rendu. Les - phrases livrées passent par la session « identité » puis par Pierre pour - la relecture finale. -4. **Discret** : rien n'apparaît pendant que le joueur réfléchit ; la ligne - s'efface à l'action suivante ; désactivable (réglage de 1c et menu). -5. **Videau** : même traitement pour doubler, ne pas doubler, prendre, passer. -6. Scénarios Gherkin (bon coup, erreur avec meilleur coup montré, conseil - désactivé, décision de videau commentée) avec positions et dés imposés ; - tests unitaires des gabarits (chaque gabarit rendu sur des analyses réelles - du moteur). `make lint`, `make test`, `make test-browser`, captures pour la - recette design. - -## Critères de réussite - -Après une erreur volontaire dans une position connue, la ligne du maître dit -« Erreur. », nomme le coup recommandé par le moteur et le montre sur demande ; -le tour de l'ordinateur n'est pas retardé ; les trois commandes passent. - -## Résultat - -Réalisé le 2026-11-10 sur la branche `conseil-du-maitre` (partie de 1c -fusionnée, corrections de recette C1 à C3 comprises). - -- **Analyse sans retard** : un second moteur, dans son propre Web Worker - (`app/src/main.ts`), fait les analyses à 2 plis ; le premier, qui ne - traite qu'une commande à la fois, reste à la partie. Le second démarre à - la première analyse, une fois le premier chargé et le service worker - installé (au plus 20 s), pour ne pas croiser deux téléchargements des - données du moteur. Les décisions analysées : coup, jet sans doubler - (aurait-il fallu doubler ?), double, prise, refus - (`app/src/domain/analysis/analyse.ts`). Le domaine a une méthode de plus - dans `Engine` : `rankMoves` (commande `moves` d'engine-cli, déjà - existante : le moteur n'est pas recompilé). -- **Persistance** : le conseil est rattaché à l'événement de la décision, - dans un champ facultatif `advice` du journal, versionné - (`ADVICE_VERSION`) ; migration testée (journal sans conseil, conseil - d'une autre version ou orphelin). ADR 0007 amendé. -- **Ligne du maître** (portrait) : à la place du texte du dernier coup, - dans la ligne de 44 px ; pictogramme et mot en couleur, phrase en - italique tronquée, chevron ; lien vers la feuille. Rien avant le coup ; - effacée à l'action suivante ; analyse trop tardive : pas de ligne, mais - le conseil reste au journal. Un double manqué avant le jet passe avant le - coup dans la ligne (le jet seul ne montre rien, le joueur réfléchit). -- **Paysage** : la charte ne place pas la ligne ; choix proposé : une - pastille (pictogramme et mot) au début de la bande centrale, avant la - consigne, lien vers la même feuille. À valider en recette design. -- **Feuille d'explication** : `#/explication/<événement>` (ADR 0013 amendé), - rechargeable ; mot et contexte, titre à la première personne, phrase, - « Votre coup » / « Le coup du maître » joués pion par pion (le maître en - `verdict-good`), « Voir les chiffres du moteur » replié, lien vers la - fiche du glossaire pour le videau seulement (le moteur ne nomme pas de - concept pour un coup). « Demander à une IA » attend 1f. -- **Phrases** : `docs/maitre/phrases.md` (CC BY-SA 4.0), lues à - l'exécution ; test de longueur, d'absence de « ! » et de repère non - rempli sur de vraies analyses, et sur tous les gabarits remplis au plus - long. Vérifiées par la session design ; retouches P1 (grosse erreur : - plus de « sur N », rang cité seulement pour un 2e ou 3e choix) et P2 - (« Mon coup frappe un pion. ») appliquées et testées. À soumettre à - Pierre. -- **Réglage** : interrupteur « Conseil du maître » dans le menu, en plus de - l'écran « Nouvelle partie » ; coupé, la ligne reste vide et les coups - restent analysés pour le bilan. -- **Tests** : `app/tests/advice.test.ts`, `app/tests/advice-tracker.test.ts` - (dont le tour de l'ordinateur joué pendant qu'une analyse attend), - scénario `docs/scenarios/comprendre-mes-coups.feature`, et - `tests/browser/maitre.mjs` (captures dans `build/browser-shots/maitre/`). - -Écarts et points à trancher : en portrait, la ligne du maître remplace le -texte « L'ordinateur a joué … » tant qu'elle est là (les traces et -« Revoir » restent) ; la pastille du paysage ne montre pas la phrase ; le -mot du videau est « Bonne décision » dans tous les cas (la charte cite -aussi « Bon choix »). diff --git a/tests/browser/maitre.mjs b/tests/browser/maitre.mjs index 74aa0ab..827ae00 100644 --- a/tests/browser/maitre.mjs +++ b/tests/browser/maitre.mjs @@ -1,6 +1,6 @@ // maitre.mjs -- le conseil du maître (étape 1d) joué par l'interface, dans // un vrai navigateur sans écran (Playwright), sur le site servi par l'image -// de production (docs/plans/2026-11-03-conseil-du-maitre.md). +// de production (étape 1d, docs/etape-1.md ; charte, ecrans-etape-1.md). // // Copyright (C) 2026 les auteurs d'Ustagammon. // Licence : GPLv3 ou ultérieure (voir LICENSE).