From 0e17334f456208e680057edfd594e1df9fc0c4e5 Mon Sep 17 00:00:00 2001 From: Pierre MARTIN Date: Wed, 11 Nov 2026 23:12:22 +0100 Subject: [PATCH 1/8] Plan : retours des premiers joueurs Co-Authored-By: Claude Opus 5.5 --- docs/plans/2026-11-11-retours-joueurs.md | 50 ++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 docs/plans/2026-11-11-retours-joueurs.md diff --git a/docs/plans/2026-11-11-retours-joueurs.md b/docs/plans/2026-11-11-retours-joueurs.md new file mode 100644 index 0000000..b1706b7 --- /dev/null +++ b/docs/plans/2026-11-11-retours-joueurs.md @@ -0,0 +1,50 @@ +# Retours des premiers joueurs + +- **Date** : 2026-11-11 +- **Étape** : 1 (retours des premiers joueurs, après 1f) +- **Statut** : en cours +- **Exécutant** : sous-agent general-purpose + +## Objectif +Mettre en œuvre dans l'application les règles tirées des retours des premiers +joueurs par la session design (MR !54 et !55) : pions blancs et noirs avec +contour, pions de l'ordinateur lisibles sur la barre, numéros des flèches sur +le cadre en portrait, flèches circulaires au tutoriel, vocabulaire des jans. + +## Consignes +1. **Pions blancs et noirs, avec contour** (ADR 0010, amendement du + 2026-11-11) : reporter dans `web/app.css`, pour les deux thèmes, les + valeurs de `docs/identite/charte/tokens.json` pour `checker-*`, + `checker-*-edge`, `count-*`, `die-*`, `pip-*`, `cube` et `cube-text`. Le + logo et le cadre ne changent pas. Aucune couleur en dur dans le TypeScript. +2. **Pions de l'ordinateur lisibles sur la barre** : contour noir sur la barre + à environ 6,8:1 (clair) et 8,5:1 (sombre). `tests/browser/recette.mjs` + contrôle le contraste du contour des pions sur le fond qui les porte + (3:1 au moins, règle de `recette-ux.md`). +3. **Numéros des flèches sur le cadre en portrait** : bandes haute et basse du + cadre agrandies pour loger des numéros de 12 px au moins ; `geometry.ts`, + ses tests et la recette adaptés. +4. **Tutoriel, étape 2** : deux flèches circulaires (claire pour le joueur, + sombre pour l'ordinateur, chacune dans le sens de son joueur, avec une + pointe), dessinées sous les pions, avec un liseré de la couleur opposée ; + jamais en brique. +5. **Les jans** : « jan intérieur » (flèches 1 à 6) et « jan extérieur » + (7 à 12) remplacent « camp » dans le tutoriel, l'aide, les phrases du maître + et les prompts ; fiches `jan-interieur.md` (Home board) et + `jan-exterieur.md` (Outer board) reliées au code ; fiche « camp » retirée + sans lien cassé ; dans `regles.md`, pions « blancs » et « noirs ». + +Limites : CSP stricte (aucun `.style.`), aucune nouvelle dépendance, `-j2` au +plus, pas de recompilation du moteur. Vérifier avec `docker ps` qu'aucun autre +test navigateur ne tourne avant `make test-browser`. + +## Critères de réussite +- `make lint`, `make test` et `make test-browser` passent. +- Captures dans `build/browser-shots/retours-joueurs/` (412×915 et 915×412, + thèmes clair et sombre) : pions des deux camps sur la barre, étape 2 du + tutoriel, portrait avec les numéros sur le cadre. +- Aucune occurrence de « camp » au sens de jan dans `docs/tutoriel/`, + `docs/scenarios/`, `docs/maitre/` et `docs/prompts/`. + +## Résultat +Rempli à la clôture. -- 2.54.0 From cfdbc1cedc4add5056a1798fae0d3fc028366c66 Mon Sep 17 00:00:00 2001 From: Pierre MARTIN Date: Thu, 12 Nov 2026 02:50:10 +0100 Subject: [PATCH 2/8] Pions blancs et noirs, avec contour : valeurs de tokens.json dans app.css Co-Authored-By: Claude Opus 5.5 --- docs/glossaire/camp.md | 12 ----- web/app.css | 116 ++++++++++++++++++++--------------------- 2 files changed, 56 insertions(+), 72 deletions(-) delete mode 100644 docs/glossaire/camp.md diff --git a/docs/glossaire/camp.md b/docs/glossaire/camp.md deleted file mode 100644 index 3b9b8c3..0000000 --- a/docs/glossaire/camp.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -term: Camp -english: Home board ---- -# Camp - -**En bref** : les six dernières flèches d'un joueur, avant sa sortie : il y rassemble ses pions pour les sortir, et un pion adverse frappé doit y rentrer. - -Votre camp est en bas à droite (vos flèches 1 à 6) ; celui de l'ordinateur, -en haut à droite (vos flèches 19 à 24), où rentrent vos pions frappés. - -Voir aussi : [Sortie](sortie.md), [Barre](barre.md), [Point](point.md) diff --git a/web/app.css b/web/app.css index 5d658f0..e01ab88 100644 --- a/web/app.css +++ b/web/app.css @@ -81,29 +81,23 @@ --point-a: #6b4430; --point-b: #c9ae88; --point-number: #f3ead8; - --checker-player: #f7f0e2; - --checker-player-edge: #8c7a62; - --checker-computer: #2e1c12; - --checker-computer-edge: #120a05; - --count-player: #2b1d15; - --count-computer: #f3ead8; - --die-player: #f7f0e2; - --pip-player: #2b1d15; - --die-computer: #2e1c12; - --pip-computer: #f3ead8; - --cube: #f7f0e2; - --cube-text: #2b1d15; + --checker-player: #fafaf7; + --checker-player-edge: #6e6e69; + --checker-computer: #1c1c1c; + --checker-computer-edge: #b5b5b0; + --count-player: #1c1c1c; + --count-computer: #fafaf7; + --die-player: #fafaf7; + --pip-player: #1c1c1c; + --die-computer: #1c1c1c; + --pip-computer: #fafaf7; + --cube: #fafaf7; + --cube-text: #1c1c1c; --highlight: #a8432b; --target: rgba(168, 67, 43, 0.32); /* Dernier coup joué (marques et traces) : le laiton, rien d'autre. */ --last-move: var(--brand-laiton); - /* Numéros des flèches. La charte les pose sur le cadre (point-number, - * ivoire) ; notre plateau les pose sur le fond, où l'ivoire ne se lirait - * pas : texte secondaire en thème clair (4,7:1 sur bed), point-number en - * thème sombre (6,3:1). */ - --point-label: var(--muted); - --shadow-raised: 0 1px 2px rgba(43, 29, 21, 0.1); --font-sans: "Alegreya Sans", "Gill Sans", "Segoe UI", system-ui, sans-serif; @@ -167,23 +161,21 @@ --point-a: #3a2519; --point-b: #7e6142; --point-number: #cdbfa8; - --checker-player: #ede3cf; - --checker-player-edge: #8e7c64; - --checker-computer: #140d09; - --checker-computer-edge: #a8957d; - --count-player: #2b1d15; - --count-computer: #f3ead8; - --die-player: #ede3cf; - --pip-player: #2b1d15; - --die-computer: #140d09; - --pip-computer: #ede3cf; - --cube: #ede3cf; - --cube-text: #2b1d15; + --checker-player: #f2f2ef; + --checker-player-edge: #6e6e69; + --checker-computer: #141414; + --checker-computer-edge: #b5b5b0; + --count-player: #1c1c1c; + --count-computer: #fafaf7; + --die-player: #f2f2ef; + --pip-player: #1c1c1c; + --die-computer: #141414; + --pip-computer: #f2f2ef; + --cube: #f2f2ef; + --cube-text: #1c1c1c; --highlight: #e07a5a; --target: rgba(224, 122, 90, 0.38); - --point-label: var(--point-number); - --shadow-raised: 0 1px 2px rgba(0, 0, 0, 0.4); --scrim: rgba(0, 0, 0, 0.6); color-scheme: dark; @@ -215,23 +207,21 @@ --point-a: #3a2519; --point-b: #7e6142; --point-number: #cdbfa8; - --checker-player: #ede3cf; - --checker-player-edge: #8e7c64; - --checker-computer: #140d09; - --checker-computer-edge: #a8957d; - --count-player: #2b1d15; - --count-computer: #f3ead8; - --die-player: #ede3cf; - --pip-player: #2b1d15; - --die-computer: #140d09; - --pip-computer: #ede3cf; - --cube: #ede3cf; - --cube-text: #2b1d15; + --checker-player: #f2f2ef; + --checker-player-edge: #6e6e69; + --checker-computer: #141414; + --checker-computer-edge: #b5b5b0; + --count-player: #1c1c1c; + --count-computer: #fafaf7; + --die-player: #f2f2ef; + --pip-player: #1c1c1c; + --die-computer: #141414; + --pip-computer: #f2f2ef; + --cube: #f2f2ef; + --cube-text: #1c1c1c; --highlight: #e07a5a; --target: rgba(224, 122, 90, 0.38); - --point-label: var(--point-number); - --shadow-raised: 0 1px 2px rgba(0, 0, 0, 0.4); --scrim: rgba(0, 0, 0, 0.6); color-scheme: dark; @@ -984,9 +974,10 @@ a.notation { .point-b { fill: var(--point-b); } /* Numéros des flèches : leur taille est une mesure du dessin (attribut - * font-size), calculée pour faire au moins 12 px à l'écran (geometry.ts). */ + * font-size), calculée pour faire au moins 12 px à l'écran (geometry.ts). + * Posés sur le cadre, en portrait comme en paysage. */ .point-number { - fill: var(--point-label); + fill: var(--point-number); font-weight: 500; text-anchor: middle; } @@ -1118,10 +1109,8 @@ a.notation { cursor: pointer; } -/* Paysage : numéros sur le cadre (ivoire), comptes de points en pastilles - * aux bouts de la barre, consigne dans la bande, commandes à gauche. */ -.point-number.on-frame { fill: var(--point-number); } - +/* Paysage : comptes de points en pastilles aux bouts de la barre, consigne + * dans la bande, commandes à gauche. */ .pip-chip { stroke-width: 1.5; } .chip-player { fill: var(--checker-player); stroke: var(--checker-player-edge); } .chip-computer { fill: var(--checker-computer); stroke: var(--checker-computer-edge); } @@ -2046,9 +2035,10 @@ html.bare .topbar { min-width: 0; } -/* Le sens du jeu : pointillés text sur un liseré ivoire, jamais en brique - * (la brique veut dire « à faire »). Couleurs de marque, les mêmes dans les - * deux thèmes : le liseré reste lisible sur le bois. */ +/* Le sens du jeu : deux flèches circulaires, claire pour le joueur, sombre + * pour l'ordinateur, aux couleurs de leurs pions, chacune avec un liseré de + * la couleur opposée pour rester lisible sur le bois ; jamais en brique (la + * brique veut dire « à faire »). */ .tutorial-path path { fill: none; stroke-linecap: round; @@ -2056,13 +2046,19 @@ html.bare .topbar { pointer-events: none; } -.tutorial-path .path-edge { - stroke: var(--brand-ivoire); +.tutorial-path .direction-player.direction-line { stroke: var(--checker-player); } +.tutorial-path .direction-player.direction-edge { stroke: var(--checker-computer); } +.tutorial-path .direction-computer.direction-line { stroke: var(--checker-computer); } +.tutorial-path .direction-computer.direction-edge { stroke: var(--checker-player); } + +.tutorial-path .direction-head.direction-player { + fill: var(--checker-player); + stroke: var(--checker-computer); } -.tutorial-path .path-dots, -.tutorial-path .path-head { - stroke: var(--brand-noyer); +.tutorial-path .direction-head.direction-computer { + fill: var(--checker-computer); + stroke: var(--checker-player); } /* En paysage, comme la position partagée : le plateau à gauche sur toute -- 2.54.0 From 09a2ddf56627d4ca44cb094e7597439eeca6e7a3 Mon Sep 17 00:00:00 2001 From: Pierre MARTIN Date: Thu, 12 Nov 2026 02:50:10 +0100 Subject: [PATCH 3/8] =?UTF-8?q?Num=C3=A9ros=20des=20fl=C3=A8ches=20sur=20l?= =?UTF-8?q?e=20cadre=20en=20portrait=20aussi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- app/src/ui/board-view.ts | 14 ++++-------- app/src/ui/geometry.ts | 15 ++++++++----- app/tests/layout.test.ts | 34 +++++++++++++++++++++++++----- docs/identite/charte/recette-ux.md | 4 ++-- 4 files changed, 45 insertions(+), 22 deletions(-) diff --git a/app/src/ui/board-view.ts b/app/src/ui/board-view.ts index 35bf99a..f2e9dd6 100644 --- a/app/src/ui/board-view.ts +++ b/app/src/ui/board-view.ts @@ -303,23 +303,17 @@ function drawPoint( "data-point": number, points: `${left},${base} ${left + g.pointWidth},${base} ${center},${tip}`, }); - // Numéros : sur le cadre en paysage, dans la bande centrale en - // portrait. Leur taille (12 px au moins à l'écran) dépend de l'échelle + // Numéros : sur le cadre, bandes haute et basse, en portrait comme en + // paysage. Leur taille (12 px au moins à l'écran) dépend de l'échelle // du dessin : c'est une mesure du dessin, comme x et y. const size = g.numberSize; - let y: number; - if (g.layout === "landscape") { - y = top ? g.edge / 2 + size * 0.36 : g.height - g.edge / 2 + size * 0.36; - } else { - y = top ? middle - g.die / 2 - 4 : middle + g.die / 2 + 4 + size * 0.72; - } add( svg, "text", { - class: g.layout === "landscape" ? "point-number on-frame" : "point-number", + class: "point-number", x: center, - y, + y: top ? g.edge / 2 + size * 0.36 : g.height - g.edge / 2 + size * 0.36, "font-size": size, }, String(number), diff --git a/app/src/ui/geometry.ts b/app/src/ui/geometry.ts index bcacc61..6620af0 100644 --- a/app/src/ui/geometry.ts +++ b/app/src/ui/geometry.ts @@ -25,7 +25,7 @@ export interface Geometry { readonly height: number; /** Épaisseur du cadre sur les côtés. */ readonly frame: number; - /** Hauteur du cadre en haut et en bas : la bande des numéros en paysage. */ + /** Hauteur du cadre en haut et en bas : la bande des numéros des flèches. */ readonly edge: number; /** Colonne de commandes à gauche (0 en portrait). */ readonly left: number; @@ -95,14 +95,19 @@ export function boardGeometry(layout: Layout, aspect: number, pxPerUnit: number) arrowWidth: 2.5 / pxPerUnit, }; if (layout === "portrait") { - // Les mesures du plateau d'origine : flèches de 44, pions de 20. + // Les mesures du plateau d'origine : flèches de 44. Les bandes haute + // et basse du cadre portent les numéros des flèches, comme en + // paysage (12 px au moins à l'écran) : pions de 19 au plus. La bande ne + // dépasse pas 36 unités : sur une miniature (moins de 275 px de + // large), les numéros rapetissent plutôt que d'écraser les flèches. const frame = 8; + const edge = Math.min(36, Math.max(24, numberSize + 8)); const tray = 48; const bar = 40; const width = 640; const pointWidth = (width - 3 * frame - tray - bar) / 12; const band = 84; - const radius = Math.min(pointWidth / 2 - 2, (height / 2 - frame - band / 2) / 10); + const radius = Math.min(pointWidth / 2 - 2, (height / 2 - edge - band / 2) / 10); const x0 = frame; const barLeft = x0 + 6 * pointWidth; return { @@ -110,7 +115,7 @@ export function boardGeometry(layout: Layout, aspect: number, pxPerUnit: number) width, height, frame, - edge: frame, + edge, left: 0, tray, bar, @@ -123,7 +128,7 @@ export function boardGeometry(layout: Layout, aspect: number, pxPerUnit: number) rightHalf: barLeft + bar, trayLeft: barLeft + bar + 6 * pointWidth + frame, target, - numberSize, + numberSize: Math.min(numberSize, edge - 8), bandFont: atLeast(13.5, pxPerUnit, 17), chipFont: atLeast(13.5, pxPerUnit, 17), ...sizes, diff --git a/app/tests/layout.test.ts b/app/tests/layout.test.ts index a0eaa20..b2bfdb8 100644 --- a/app/tests/layout.test.ts +++ b/app/tests/layout.test.ts @@ -9,7 +9,7 @@ import assert from "node:assert/strict"; import { readdirSync, readFileSync, statSync } from "node:fs"; import { test } from "node:test"; import { createSettingsStore } from "../src/storage/settings-store.js"; -import { OPENING_BOARDS, pipLead } from "../src/ui/board-model.js"; +import { homeBoard, OPENING_BOARDS, outerBoard, pipLead } from "../src/ui/board-model.js"; import { BOARD_HEIGHT, boardGeometry, MAX_STACK, stackY } from "../src/ui/geometry.js"; import { createHaptics, HAPTIC_PATTERNS } from "../src/ui/haptics.js"; import { ICON_PATHS } from "../src/ui/icons.js"; @@ -37,15 +37,32 @@ const LANDSCAPES: [number, number][] = [ [800, 308], ]; -test("géométrie en portrait : les mesures du plateau d'origine", () => { +test("géométrie en portrait : les mesures du plateau d'origine, numéros sur le cadre", () => { const g = boardGeometry("portrait", 640 / 520, 396 / 640); assert.equal(g.width, 640); assert.equal(g.pointWidth, 44); - assert.equal(g.radius, 20); - assert.equal(g.step, 42); + // Pions d'un peu moins de 19 unités (20 avant les numéros sur le cadre). + assert.ok(g.radius >= 18.5 && g.radius <= 19, `pions de ${g.radius}`); + assert.equal(g.step, 2 * g.radius + 2); assert.equal(g.trayLeft + g.tray + g.frame, 640); - // Numéros d'au moins 12 px à l'écran, à 412 px de large. + // Numéros d'au moins 12 px à l'écran, à 412 px de large, logés dans les + // bandes haute et basse du cadre (retours des premiers joueurs). assert.ok(g.numberSize * (396 / 640) >= 12, `numéros de ${g.numberSize} unités`); + assert.ok(g.numberSize + 8 <= g.edge, `bande de ${g.edge} pour des numéros de ${g.numberSize}`); +}); + +test("géométrie en portrait : numéros d'au moins 12 px sur le cadre dès 275 px de large", () => { + for (const width of [275, 320, 358, 380, 396, 600]) { + const px = width / 640; + const g = boardGeometry("portrait", 640 / 520, px); + assert.ok(g.numberSize * px >= 12, `${width} px : numéros de ${g.numberSize * px} px`); + assert.ok(g.numberSize < g.edge, `${width} px : numéros hors du cadre`); + assert.ok(g.edge <= 36, `${width} px : bande de ${g.edge}`); + } + // Une miniature : la bande reste de 36 unités, les numéros y tiennent. + const thumbnail = boardGeometry("portrait", 640 / 520, 200 / 640); + assert.equal(thumbnail.edge, 36); + assert.ok(thumbnail.numberSize < thumbnail.edge); }); test("géométrie en paysage : le plateau remplit l'écran, flèches de 44 px et pions de 22 px au moins", () => { @@ -93,6 +110,13 @@ test("en paysage, les pions de la barre s'arrêtent avant les pastilles des comp } }); +test("les jans de chaque camp, numérotés pour le joueur, dans le sens de marche", () => { + assert.deepEqual(homeBoard("player").points, [6, 5, 4, 3, 2, 1]); + assert.deepEqual(outerBoard("player").points, [12, 11, 10, 9, 8, 7]); + assert.deepEqual(homeBoard("computer").points, [19, 20, 21, 22, 23, 24]); + assert.deepEqual(outerBoard("computer").points, [13, 14, 15, 16, 17, 18]); +}); + // -------------------------------------------------------------- Vibrations test("vibrations : les motifs de la charte, et rien quand elles sont coupées", () => { diff --git a/docs/identite/charte/recette-ux.md b/docs/identite/charte/recette-ux.md index 791590f..f58ff60 100644 --- a/docs/identite/charte/recette-ux.md +++ b/docs/identite/charte/recette-ux.md @@ -8,8 +8,8 @@ Référence : `interaction.md`, maquettes « Révision UX » du canevas. ## Toutes tailles - [ ] Flèches impaires en `point-a`, paires en `point-b` ; 12 et 13 de couleurs différentes. -- [ ] Numéros des flèches à l'extérieur du plateau, sur le cadre (bande haute et bande basse), en portrait comme en paysage ; jamais dans le fond du plateau entre les deux rangées. -- [ ] Pions de l'ordinateur lisibles partout : contour et filet `checker-computer-edge` (gris clair) visibles sur la barre, sur les flèches sombres et en thème sombre. +- [ ] Numéros des flèches à l'extérieur du plateau, sur le cadre (bande haute et bande basse), en portrait comme en paysage ; jamais dans le fond du plateau entre les deux rangées. (Vérifié par `tests/browser/retours-joueurs.mjs`.) +- [ ] Pions de l'ordinateur lisibles partout : contour et filet `checker-computer-edge` (gris clair) visibles sur la barre, sur les flèches sombres et en thème sombre. Règle automatique : là où sa couleur ne se détache pas de son fond (moins de 3:1), son contour s'en détache à 3:1 au moins (`checkerContrasts` de `tests/browser/recette.mjs`). - [ ] Une seule action principale (brique) visible à la fois. - [ ] Aucun `window.confirm` ni `alert` dans le code de l'app (`grep -rn "confirm(\|alert(" app/src` vide). - [ ] Aucune cible tactile sous 44 × 44 px (boutons, pictogrammes, dés, videau). -- 2.54.0 From 966822084a138cc26dd7c30c187607f05bbe3ece Mon Sep 17 00:00:00 2001 From: Pierre MARTIN Date: Thu, 12 Nov 2026 02:50:10 +0100 Subject: [PATCH 4/8] =?UTF-8?q?Tutoriel=20:=20deux=20fl=C3=A8ches=20circul?= =?UTF-8?q?aires=20;=20les=20jans=20remplacent=20le=20camp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- app/src/ui/board-model.ts | 37 ++++++++ app/src/ui/home-texts.ts | 5 +- app/src/ui/texts.ts | 2 +- app/src/ui/tutorial-text.ts | 2 +- app/src/ui/tutorial-view.ts | 107 ++++++++++++++---------- app/tests/tutorial.test.ts | 4 +- app/tests/ui.test.ts | 6 +- docs/glossaire/README.md | 3 +- docs/glossaire/jan-exterieur.md | 15 ++++ docs/glossaire/jan-interieur.md | 16 ++++ docs/scenarios/jouer-une-partie.feature | 2 +- docs/tutoriel/README.md | 3 +- docs/tutoriel/regles.md | 20 +++-- 13 files changed, 156 insertions(+), 66 deletions(-) create mode 100644 docs/glossaire/jan-exterieur.md create mode 100644 docs/glossaire/jan-interieur.md diff --git a/app/src/ui/board-model.ts b/app/src/ui/board-model.ts index bc3277a..c67ae35 100644 --- a/app/src/ui/board-model.ts +++ b/app/src/ui/board-model.ts @@ -156,6 +156,43 @@ export function pointSlot(number: number): { top: boolean; column: number } { return { top: true, column: number - 13 }; } +/** + * Le jan intérieur d'un camp : ses six dernières flèches avant la sortie, + * numérotées du point de vue du joueur (1 à 6 pour lui, 19 à 24 pour + * l'ordinateur), dans le sens de marche du camp. Pur dessin : le moteur + * reste seul juge des règles qui en dépendent (sortie, rentrée). + * Glossaire : docs/glossaire/jan-interieur.md + */ +export interface HomeBoard { + readonly side: Side; + readonly points: readonly number[]; +} + +/** + * Le jan extérieur d'un camp : les six flèches qui précèdent son jan + * intérieur (7 à 12 pour le joueur, 13 à 18 pour l'ordinateur), dans le sens + * de marche du camp. + * Glossaire : docs/glossaire/jan-exterieur.md + */ +export interface OuterBoard { + readonly side: Side; + readonly points: readonly number[]; +} + +/** Six flèches d'un camp, de sa flèche first à first - 5 dans sa propre + * numérotation, renumérotées pour le joueur. */ +function sixPoints(side: Side, first: number): number[] { + return Array.from({ length: 6 }, (_, i) => (side === "player" ? first - i : 25 - (first - i))); +} + +export function homeBoard(side: Side): HomeBoard { + return { side, points: sixPoints(side, 6) }; +} + +export function outerBoard(side: Side): OuterBoard { + return { side, points: sixPoints(side, 12) }; +} + /** Le plateau de la position de départ, pour le dessiner avant que le * moteur soit chargé (première ouverture) : chaque camp a 2 pions sur sa case * 24, 5 sur la 13, 3 sur la 8 et 5 sur la 6 ; 167 points chacun. Une donnée, diff --git a/app/src/ui/home-texts.ts b/app/src/ui/home-texts.ts index b662d27..2c31ac1 100644 --- a/app/src/ui/home-texts.ts +++ b/app/src/ui/home-texts.ts @@ -94,14 +94,15 @@ export const HOME_TEXTS = { drop: "Passer", tutorialLoading: "Chargement du tutoriel…", tutorialFailed: "Le tutoriel n'a pas pu se charger.", - pathLabel: "Le sens du jeu de vos pions, en pointillés", + pathLabel: + "Le sens du jeu : la flèche claire pour vos pions, la sombre pour ceux de l'ordinateur", /** Le résultat d'une partie finie par le coup illustré (étape 7). */ illustratedEnd: (kind: ResultKind) => kind === "single" ? "Partie finie : l'ordinateur avait sorti des pions, c'est une partie simple." : kind === "gammon" ? "Partie finie : l'ordinateur n'a sorti aucun pion, c'est un gammon." - : "Partie finie : il reste un pion de l'ordinateur dans votre camp, c'est un backgammon.", + : "Partie finie : il reste un pion de l'ordinateur dans votre jan intérieur, c'est un backgammon.", computerDoubles: "L'ordinateur double.", // Aide diff --git a/app/src/ui/texts.ts b/app/src/ui/texts.ts index aab99c1..b5ce55f 100644 --- a/app/src/ui/texts.ts +++ b/app/src/ui/texts.ts @@ -188,7 +188,7 @@ export const TEXTS = { ) => [ winner === null - ? "Vous avez la main : vos pions ivoire, en bas." + ? "Vous avez la main : vos pions blancs, en bas." : `La partie est finie dans cette position : ${ winner === "player" ? "vous gagnez" : "l'ordinateur gagne" }.`, diff --git a/app/src/ui/tutorial-text.ts b/app/src/ui/tutorial-text.ts index e8df0a0..04260a0 100644 --- a/app/src/ui/tutorial-text.ts +++ b/app/src/ui/tutorial-text.ts @@ -24,7 +24,7 @@ export interface TutorialScreen { readonly roll: readonly [number, number] | null; /** Explication : le coup illustré, en notation de gnubg. */ readonly move: string | null; - /** Explication : le sens du jeu dessiné en pointillés. */ + /** Explication : le sens du jeu, deux flèches circulaires. */ readonly path: boolean; /** « À vous » : les coups justes, en notation de gnubg. */ readonly expected: readonly string[]; diff --git a/app/src/ui/tutorial-view.ts b/app/src/ui/tutorial-view.ts index 7c6b2c1..425ec3a 100644 --- a/app/src/ui/tutorial-view.ts +++ b/app/src/ui/tutorial-view.ts @@ -18,11 +18,14 @@ import type { Side } from "../domain/game/side.js"; import type { PracticeVerdict, TutorialBoard } from "../domain/learning/tutorial.js"; import { boardModel, + homeBoard, moveTargets, moveTrace, onlyHalfMove, + outerBoard, type Place, placeOf as placeOfChecker, + pointSlot, } from "./board-model.js"; import { type BoardDecor, @@ -149,57 +152,71 @@ export function fillParagraph(target: HTMLElement, paragraph: Paragraph): void { const SVG_NS = "http://www.w3.org/2000/svg"; -/** Le sens du jeu des pions du joueur, en pointillés : du haut à droite - * (flèche 24) vers la gauche, puis en bas jusqu'à la flèche 1 et la sortie. - * Tracé sous les pions et sous les numéros des flèches, qu'il ne cache - * jamais. pxPerUnit : pixels à l'écran par unité du dessin (le liseré - * ivoire fait 8 px, les points 4 px, quelle que soit la taille). */ -function drawPath(svg: SVGSVGElement, g: Geometry, pxPerUnit: number): void { +/** Le sens du jeu : deux flèches circulaires, une claire pour les pions du + * joueur (du haut à droite vers la gauche, puis en bas jusqu'à son jan + * intérieur), une sombre pour ceux de l'ordinateur, en sens inverse et à + * l'intérieur de la première. Chacune a un liseré de la couleur opposée et + * finit par une pointe ; jamais en brique (la brique veut dire « à faire »). + * Dessinées sous les pions, qu'elles ne cachent jamais. pxPerUnit : pixels + * à l'écran par unité du dessin (trait de 5 px sur un liseré de 9 px, + * quelle que soit la taille du plateau). */ +function drawDirections(svg: SVGSVGElement, g: Geometry, pxPerUnit: number): void { + const px = (n: number) => n / pxPerUnit; const center = (column: number) => columnLeft(g, column) + g.pointWidth / 2; - const top = g.height * 0.3; - const bottom = g.height * 0.7; - const right = center(11); - const left = center(0) - g.pointWidth * 0.25; - const turn = g.pointWidth * 0.6; - const end = g.trayLeft + g.tray * 0.25; - const d = - `M ${right} ${top} L ${left + turn} ${top} Q ${left} ${top} ${left} ${top + turn} ` + - `L ${left} ${bottom - turn} Q ${left} ${bottom} ${left + turn} ${bottom} L ${end} ${bottom}`; const group = document.createElementNS(SVG_NS, "g"); group.setAttribute("class", "tutorial-path"); group.setAttribute("role", "img"); group.setAttribute("aria-label", HOME_TEXTS.pathLabel); - const px = (n: number) => n / pxPerUnit; - const width = px(4); - for (const [className, stroke] of [ - ["path-edge", px(8)], - ["path-dots", width], - ] as const) { - const path = document.createElementNS(SVG_NS, "path"); - path.setAttribute("class", className); - path.setAttribute("d", d); - path.setAttribute("stroke-width", String(stroke)); - if (className === "path-dots") { - path.setAttribute("stroke-dasharray", `0.1 ${px(10)}`); + // Chaque flèche part du jan intérieur adverse (sa flèche 24), longe les + // deux jans extérieurs, tourne au bout du sien et finit dans son jan + // intérieur. Celle de l'ordinateur passe à l'intérieur de la claire. + const column = (point: number) => pointSlot(point).column; + const loops = [ + { + side: "player", + from: g.height * 0.29, + to: g.height * 0.71, + left: center(column(outerBoard("player").points[0] ?? 12)) - g.pointWidth * 0.1, + }, + { + side: "computer", + from: g.height * 0.62, + to: g.height * 0.38, + left: center(column(outerBoard("computer").points[0] ?? 13) + 1) + g.pointWidth * 0.5, + }, + ] as const; + const head = px(18); + const half = px(10); + for (const { side, from, to, left } of loops) { + const other = side === "player" ? "computer" : "player"; + const start = center(column(homeBoard(other).points[5] ?? 1)); + const end = center(column(homeBoard(side).points[4] ?? 2)) + g.pointWidth * 0.3; + const turn = Math.min(Math.abs(to - from) / 2, g.pointWidth * 0.9); + const down = to > from ? 1 : -1; + const d = + `M ${start} ${from} L ${left + turn} ${from} Q ${left} ${from} ${left} ${from + down * turn} ` + + `L ${left} ${to - down * turn} Q ${left} ${to} ${left + turn} ${to} L ${end - head / 2} ${to}`; + for (const [part, stroke] of [ + ["edge", px(9)], + ["line", px(5)], + ] as const) { + const path = document.createElementNS(SVG_NS, "path"); + path.setAttribute("class", `direction-${part} direction-${side}`); + path.setAttribute("d", d); + path.setAttribute("stroke-width", String(stroke)); + group.append(path); } - group.append(path); - } - const head = document.createElementNS(SVG_NS, "path"); - head.setAttribute("class", "path-head"); - const size = px(7); - head.setAttribute( - "d", - `M ${end - size} ${bottom - size} L ${end} ${bottom} L ${end - size} ${bottom + size}`, - ); - head.setAttribute("stroke-width", String(px(3))); - group.append(head); - // Sous les pions, et sous les numéros des flèches, remontés au-dessus - // du tracé ; les zones à toucher restent devant tout. - const pieces = svg.querySelector(":scope > g.pieces"); - svg.insertBefore(group, pieces); - for (const number of svg.querySelectorAll(":scope > .point-number")) { - svg.insertBefore(number, pieces); + const tip = document.createElementNS(SVG_NS, "path"); + tip.setAttribute("class", `direction-head direction-${side}`); + tip.setAttribute( + "d", + `M ${end - head} ${to - half} L ${end} ${to} L ${end - head} ${to + half} Z`, + ); + tip.setAttribute("stroke-width", String(px(2))); + group.append(tip); } + // Sous les pions ; les zones à toucher restent devant tout. + svg.insertBefore(group, svg.querySelector(":scope > g.pieces")); } /** Crée l'écran du tutoriel dans section (vide dans index.html). */ @@ -359,7 +376,7 @@ export function createTutorialView(options: TutorialViewOptions): TutorialView { }; renderBoard(svg, g, boardModel(current), decor); if (screen?.path === true) { - drawPath(svg, g, (svg.getBoundingClientRect().width || g.width) / g.width); + drawDirections(svg, g, (svg.getBoundingClientRect().width || g.width) / g.width); } } diff --git a/app/tests/tutorial.test.ts b/app/tests/tutorial.test.ts index d4cc81f..3abbc01 100644 --- a/app/tests/tutorial.test.ts +++ b/app/tests/tutorial.test.ts @@ -101,8 +101,8 @@ test("chaque position du tutoriel est valide pour le moteur", async () => { const s = board.sides[side]; return s.points.reduce((a, b) => a + b, 0) + s.bar + s.off; }; - assert.equal(count("player"), 15, `${name} : pions ivoire`); - assert.equal(count("computer"), 15, `${name} : pions noyer`); + assert.equal(count("player"), 15, `${name} : pions blancs`); + assert.equal(count("computer"), 15, `${name} : pions noirs`); } }); diff --git a/app/tests/ui.test.ts b/app/tests/ui.test.ts index dc7a5e8..caae8e5 100644 --- a/app/tests/ui.test.ts +++ b/app/tests/ui.test.ts @@ -209,14 +209,14 @@ test("aide contextuelle : chaque question ouverte depuis la partie existe dans l }); test("glossaire : nom anglais, renvois, recherche sans accents", () => { - const entry = parseGlossaryEntry(read("docs/glossaire/camp.md")); + const entry = parseGlossaryEntry(read("docs/glossaire/jan-interieur.md")); assert.ok(entry !== null); assert.equal(entry.english, "Home board"); assert.deepEqual( entry.seeAlso.map((s) => s.name), - ["sortie", "barre", "point"], + ["jan-exterieur", "sortie", "barre", "point"], ); - assert.ok(matchesSearch(entry, "camp")); + assert.ok(matchesSearch(entry, "jan interieur")); assert.ok(matchesSearch(entry, "HOME")); assert.ok(matchesSearch(entry, "fleches sortir")); assert.ok(!matchesSearch(entry, "videau")); diff --git a/docs/glossaire/README.md b/docs/glossaire/README.md index b49b1c8..c2a3342 100644 --- a/docs/glossaire/README.md +++ b/docs/glossaire/README.md @@ -40,7 +40,6 @@ exportée. `make test` vérifie le lien dans les deux sens : chaque fiche avec | [Barre](barre.md) | Bar | | [Blot](blot.md) | Blot | | [Branche](branche.md) | Branch | -| [Camp](camp.md) | Home board | | [Chance](chance.md) | Luck | | [Compte de points](compte-de-points.md) | Pip count | | [Conseil du maître](conseil-du-maitre.md) | Advice | @@ -54,6 +53,8 @@ exportée. `make test` vérifie le lien dans les deux sens : chaque fiche avec | [Gammon](gammon.md) | Gammon | | [Identifiant de match](identifiant-de-match.md) | Match ID | | [Identifiant de position](identifiant-de-position.md) | Position ID | +| [Jan extérieur](jan-exterieur.md) | Outer board | +| [Jan intérieur](jan-interieur.md) | Home board | | [Jet](jet.md) | Roll | | [Lien de position](lien-de-position.md) | Position link | | [Match](match.md) | Match | diff --git a/docs/glossaire/jan-exterieur.md b/docs/glossaire/jan-exterieur.md new file mode 100644 index 0000000..29153eb --- /dev/null +++ b/docs/glossaire/jan-exterieur.md @@ -0,0 +1,15 @@ +--- +term: Jan extérieur +english: Outer board +code: OuterBoard +--- +# Jan extérieur + +**En bref** : les six flèches qui précèdent le jan intérieur d'un joueur : ses pions y passent avant d'y entrer. + +Votre jan extérieur est en bas à gauche (vos flèches 7 à 12) ; celui de +l'ordinateur, en haut à gauche (vos flèches 13 à 18). Vos pions partent du jan +intérieur de l'ordinateur, traversent les deux jans extérieurs, puis entrent +dans votre jan intérieur. + +Voir aussi : [Jan intérieur](jan-interieur.md), [Point](point.md) diff --git a/docs/glossaire/jan-interieur.md b/docs/glossaire/jan-interieur.md new file mode 100644 index 0000000..6284842 --- /dev/null +++ b/docs/glossaire/jan-interieur.md @@ -0,0 +1,16 @@ +--- +term: Jan intérieur +english: Home board +code: HomeBoard +--- +# Jan intérieur + +**En bref** : les six dernières flèches d'un joueur avant la sortie : il y rassemble ses quinze pions pour les sortir, et les pions adverses frappés y rentrent. + +Le plateau se partage en quatre jans de six flèches. Votre jan intérieur est +en bas à droite (vos flèches 1 à 6) ; celui de l'ordinateur, en haut à droite +(vos flèches 19 à 24) : c'est là que rentrent vos pions frappés. Un pion +adverse resté dans votre jan intérieur à la fin de la partie en fait un +backgammon. + +Voir aussi : [Jan extérieur](jan-exterieur.md), [Sortie](sortie.md), [Barre](barre.md), [Point](point.md) diff --git a/docs/scenarios/jouer-une-partie.feature b/docs/scenarios/jouer-une-partie.feature index 5de42e5..5fd8be1 100644 --- a/docs/scenarios/jouer-une-partie.feature +++ b/docs/scenarios/jouer-une-partie.feature @@ -144,7 +144,7 @@ Fonctionnalité: Jouer une partie contre l'ordinateur @position:BQAACAIAAAAAAA Scénario: Sortir ses pions dans l'ordre de son choix - Une fois tous vos pions dans les six dernières flèches, vous les + Une fois tous vos pions dans votre jan intérieur (flèches 1 à 6), vous les sortez dans l'ordre que vous voulez. Avec 6-1, un pion sur la flèche 6 et un sur la flèche 1 : sortez l'un puis l'autre, ou l'inverse. Si vous ne sortez que le pion de la 6, « Jouer ce coup » apparaît déjà, car ce diff --git a/docs/tutoriel/README.md b/docs/tutoriel/README.md index 95bc379..ca9e684 100644 --- a/docs/tutoriel/README.md +++ b/docs/tutoriel/README.md @@ -51,7 +51,8 @@ Le texte de la fin du tutoriel. position (par défaut, vous) ; - `jet` et `coup` : sur un écran d'explication, le coup illustré, qui se joue tout seul (notation de gnubg) ; - - `chemin: oui` : le sens du jeu, dessiné en pointillés sur le plateau ; + - `chemin: oui` : le sens du jeu, deux flèches circulaires sur le plateau + (claire pour vos pions, sombre pour ceux de l'ordinateur) ; - `attendu` (une ligne par coup accepté) : sur un écran « à vous », les coups justes, en notation de gnubg, tels que le moteur les écrit ; - `réponse` : `prendre` ou `passer`, pour un écran « à vous » où diff --git a/docs/tutoriel/regles.md b/docs/tutoriel/regles.md index df1dcd8..2470cf8 100644 --- a/docs/tutoriel/regles.md +++ b/docs/tutoriel/regles.md @@ -1,13 +1,15 @@ # Les règles du backgammon Tutoriel d'Ustagammon, en huit étapes. Texte sous licence CC BY-SA 4.0, -validé par Pierre le 9 novembre 2026. Format : voir README.md. +validé par Pierre le 9 novembre 2026 ; pions blancs et noirs, flèches +circulaires et jans repris de la charte le 11 novembre 2026 +(docs/identite/charte/accueil-tutoriel-aide.md). Format : voir README.md. ## 1. Le but du jeu - position: 4HPwATDgc/ABMA -Vous avez quinze [pions](pion.md) ivoire, l'ordinateur quinze pions noyer. Chacun fait faire le tour du [plateau](plateau.md) à ses pions, puis les [sort](sortie.md). +Vous avez quinze [pions](pion.md) blancs, l'ordinateur quinze pions noirs. Chacun fait faire le tour du [plateau](plateau.md) à ses pions, puis les [sort](sortie.md). Le premier qui a sorti tous les siens gagne la [partie](partie.md). @@ -16,9 +18,9 @@ Le premier qui a sorti tous les siens gagne la [partie](partie.md). - position: 4HPwATDgc/ABMA - chemin: oui -Vos pions ivoire font le tour du plateau dans le sens des pointillés : ils partent d'en haut à droite, passent à gauche, puis reviennent en bas jusqu'aux **six dernières flèches**, en bas à droite. C'est là que vous les rassemblerez avant de les sortir. +Vos pions suivent la **flèche claire** : ils partent d'en haut à droite, passent à gauche, puis reviennent en bas jusqu'à votre [jan intérieur](jan-interieur.md), les flèches 1 à 6. C'est là que vous les rassemblez avant de les sortir. -Les pions noyer de l'ordinateur font le même chemin, en sens inverse. +Les pions de l'ordinateur suivent la **flèche sombre**, en sens inverse, jusqu'à son jan intérieur, les flèches 19 à 24. ## 3. Les dés et les doubles @@ -60,10 +62,10 @@ Une flèche où l'adversaire a deux pions ou plus est fermée : vous ne pouvez p - attendu: 24/23 13/7* - attendu: 24/18 8/7* - attendu: 8/7* 8/2 -- juste: Touché : le pion noyer part sur la barre, il devra refaire tout le chemin. +- juste: Touché : le pion noir part sur la barre, il devra refaire tout le chemin. - refus: Ce coup ne frappe pas le pion seul de la flèche 7. Un pion qui s'y pose le frappe : celui de la flèche 13 avec le 6, ou celui de la flèche 8 avec le 1. -Vous avez 6 et 1. Le pion noyer de la flèche 7 est seul : frappez-le. +Vous avez 6 et 1. Le pion noir de la flèche 7 est seul : frappez-le. ## 5. Rentrer depuis la barre @@ -71,7 +73,7 @@ Vous avez 6 et 1. Le pion noyer de la flèche 7 est seul : frappez-le. - jet: 4-3 - coup: bar/22 13/9 -Votre pion frappé attend sur la barre, au milieu. Avant de jouer quoi que ce soit d'autre, il doit rentrer dans le [camp](camp.md) adverse, en haut à droite. +Votre pion frappé attend sur la barre, au milieu. Avant de jouer quoi que ce soit d'autre, il doit rentrer dans le jan intérieur de l'ordinateur, en haut à droite. Chaque dé indique une flèche d'entrée : avec 3 et 4, la 22 ou la 21. Si les deux flèches sont fermées, votre tour passe. @@ -81,7 +83,7 @@ Chaque dé indique une flèche d'entrée : avec 3 et 4, la 22 ou la 21. Si les d - jet: 5-2 - coup: 5/off 2/off -Quand vos quinze pions sont dans les six dernières flèches, vous pouvez commencer à les sortir : un 6 sort un pion de la flèche 6, un 1 un pion de la flèche 1. +Quand vos quinze pions sont dans votre jan intérieur (flèches 1 à 6), vous pouvez commencer à les sortir : un 6 sort un pion de la flèche 6, un 1 un pion de la flèche 1. Touchez le pion, puis la sortie à droite. @@ -101,7 +103,7 @@ Vous avez 6 et 3 : sortez deux pions. - jet: 2-1 - coup: 2/off 1/off -Une partie gagnée vaut 1 point. Si l'adversaire n'a sorti aucun pion, c'est un [gammon](gammon.md) : 2 points ; s'il lui reste en plus un pion sur la barre ou dans votre camp, c'est un [backgammon](backgammon.md) : 3 points. En partie libre, gammon et backgammon ne comptent que si le videau a été doublé : c'est la [règle de Jacoby](regle-de-jacoby.md). +Une partie gagnée vaut 1 point. Si l'adversaire n'a sorti aucun pion, c'est un [gammon](gammon.md) : 2 points ; s'il lui reste en plus un pion sur la barre ou dans votre jan intérieur, c'est un [backgammon](backgammon.md) : 3 points. En partie libre, gammon et backgammon ne comptent que si le videau a été doublé : c'est la [règle de Jacoby](regle-de-jacoby.md). ## 8. Le videau -- 2.54.0 From 8c2043aca5de2f8f77c09d01147c64b0659c26c1 Mon Sep 17 00:00:00 2001 From: Pierre MARTIN Date: Thu, 12 Nov 2026 02:50:10 +0100 Subject: [PATCH 5/8] Recette : contraste du contour des pions, retours des joueurs dans le navigateur Co-Authored-By: Claude Opus 5.5 --- tests/browser.sh | 19 ++- tests/browser/accueil.mjs | 13 +- tests/browser/recette.mjs | 93 +++++++++++ tests/browser/retours-joueurs.mjs | 258 ++++++++++++++++++++++++++++++ 4 files changed, 372 insertions(+), 11 deletions(-) create mode 100644 tests/browser/retours-joueurs.mjs diff --git a/tests/browser.sh b/tests/browser.sh index 8ca14e9..f232419 100755 --- a/tests/browser.sh +++ b/tests/browser.sh @@ -15,10 +15,13 @@ # captures dans build/browser-shots/prompts/. « Nouvelle partie » après une # partie finie et son bilan, par chaque entrée # (tests/browser/nouvelle-partie.mjs), captures dans -# build/browser-shots/nouvelle-partie/. +# build/browser-shots/nouvelle-partie/. Les retours des premiers joueurs +# (tests/browser/retours-joueurs.mjs : pions sur la barre, numéros sur le +# cadre, flèches du tutoriel, en thème clair et sombre), captures dans +# build/browser-shots/retours-joueurs/. # # BROWSERS=chromium et SCRIPTS=accueil (ou play, match, maitre, bilan, -# prompts, nouvelle-partie) restreignent le test. +# prompts, nouvelle-partie, retours-joueurs) restreignent le test. # # Outils : l'image de Playwright (navigateurs) et le paquet playwright, # installé une fois par npm dans build/browser-tools/ (réseau nécessaire la @@ -31,7 +34,7 @@ cd "$(dirname "$0")/.." image="${PLAYWRIGHT_IMAGE:?lancer via make test-browser}" browsers="${BROWSERS:-chromium firefox}" # Les scripts à lancer (tests/browser/.mjs), tous par défaut. -scripts="${SCRIPTS:-play accueil match maitre bilan prompts nouvelle-partie}" +scripts="${SCRIPTS:-play accueil match maitre bilan prompts nouvelle-partie retours-joueurs}" unset MAKEFLAGS MFLAGS MAKELEVEL make -s package >/dev/null @@ -70,7 +73,7 @@ rm -rf build/browser-shots mkdir -p build/browser-shots cp tests/browser/play.mjs tests/browser/accueil.mjs tests/browser/match.mjs tests/browser/recette.mjs \ tests/browser/maitre.mjs tests/browser/bilan.mjs tests/browser/prompts.mjs tests/browser/nouvelle-partie.mjs \ - "$tools/" + tests/browser/retours-joueurs.mjs "$tools/" status=0 for browser in $browsers; do # Réseau de l'hôte : le site est sur 127.0.0.1. Le service worker @@ -125,5 +128,13 @@ for browser in $browsers; do -v "$PWD/$tools:/tools" -v "$PWD/build/browser-shots:/shots" -w /tools "$image" \ node nouvelle-partie.mjs "http://localhost:$port/" "$browser" /shots || status=1 fi + if [[ " $scripts " == *" retours-joueurs "* ]]; then + # Retours des premiers joueurs : pions sur la barre, numéros sur le + # cadre, flèches du tutoriel ; captures dans + # build/browser-shots/retours-joueurs/. + docker run --rm --init --ipc=host --network host --user "$(id -u):$(id -g)" -e HOME=/tmp \ + -v "$PWD/$tools:/tools" -v "$PWD/build/browser-shots:/shots" -w /tools "$image" \ + node retours-joueurs.mjs "http://localhost:$port/" "$browser" /shots || status=1 + fi done exit "$status" diff --git a/tests/browser/accueil.mjs b/tests/browser/accueil.mjs index 1e305dd..361b881 100644 --- a/tests/browser/accueil.mjs +++ b/tests/browser/accueil.mjs @@ -274,17 +274,16 @@ async function playTouches(page, touches) { await tutorialReady(page, hash); } if (step === 2) { - check(await page.locator("#tutorial-board .tutorial-path").count() === 1, - "étape 2 : pas de chemin en pointillés"); - // Sous les pions et sous les numéros des flèches. + check(await page.locator("#tutorial-board .tutorial-path .direction-line").count() === 2, + "étape 2 : pas les deux flèches du sens du jeu"); + // Sous les pions (détail : retours-joueurs.mjs). const order = await page.evaluate(() => { const children = [...document.getElementById("tutorial-board").children]; const at = (selector) => children.findIndex((c) => c.matches(selector)); - const numbers = children.filter((c) => c.matches(".point-number")).map((c) => children.indexOf(c)); - return { path: at(".tutorial-path"), pieces: at("g.pieces"), firstNumber: Math.min(...numbers) }; + return { path: at(".tutorial-path"), pieces: at("g.pieces") }; }); - check(order.path >= 0 && order.path < order.pieces && order.path < order.firstNumber, - `étape 2 : le chemin passe au-dessus des pions ou des numéros ${JSON.stringify(order)}`); + check(order.path >= 0 && order.path < order.pieces, + `étape 2 : les flèches passent au-dessus des pions ${JSON.stringify(order)}`); } await tutorialCapture(page, `tutoriel-${step}`); screens++; diff --git a/tests/browser/recette.mjs b/tests/browser/recette.mjs index ddc2658..c860809 100644 --- a/tests/browser/recette.mjs +++ b/tests/browser/recette.mjs @@ -167,3 +167,96 @@ export function tutorialWidthProblems(views) { } return problems; } + +/** + * Les contrastes des pions dessinés sur les plateaux visibles de la page + * (svg.board), mesurés sur le fond qui porte chacun : la barre, une flèche, + * le fond du plateau ou la sortie, trouvé au centre du pion (le dernier + * dessiné qui le contient). Pour chaque pion : son camp, ce fond, le + * contraste de son contour (stroke) et celui de sa couleur (fill) avec ce + * fond. Les pions entourés de brique (jouable, sélectionné) sont laissés : + * leur contour est celui du pion jouable, pas celui de son camp. + */ +export async function checkerContrasts(target) { + return target.evaluate(() => { + const rgb = (color) => (color.match(/[\d.]+/g) ?? []).slice(0, 3).map(Number); + const luminance = (color) => { + const [r, g, b] = rgb(color).map((v) => { + const c = v / 255; + return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; + }); + return 0.2126 * r + 0.7152 * g + 0.0722 * b; + }; + const ratio = (a, b) => { + const [dark, light] = [luminance(a), luminance(b)].sort((x, y) => x - y); + return (light + 0.05) / (dark + 0.05); + }; + const BACKGROUNDS = [ + ["board-bar", "barre"], + ["board-tray", "sortie"], + ["point-a", "flèche impaire"], + ["point-b", "flèche paire"], + ["board-bed", "fond"], + ]; + const selector = BACKGROUNDS.map(([c]) => `.${c}`).join(", "); + const found = []; + for (const svg of document.querySelectorAll("svg.board")) { + if (svg.closest("[hidden]") !== null || svg.getClientRects().length === 0) { + continue; + } + const shapes = [...svg.querySelectorAll(selector)].reverse(); + for (const checker of svg.querySelectorAll("circle.checker")) { + if (checker.matches(".movable, .selected")) { + continue; + } + const point = svg.createSVGPoint(); + point.x = Number(checker.getAttribute("cx")); + point.y = Number(checker.getAttribute("cy")); + const under = shapes.find((s) => s.isPointInFill(point)); + if (under === undefined) { + continue; + } + const ground = getComputedStyle(under).fill; + const style = getComputedStyle(checker); + found.push({ + board: svg.id || "plateau", + side: checker.matches(".checker-computer") ? "computer" : "player", + on: BACKGROUNDS.find(([c]) => under.classList.contains(c))[1], + edge: ratio(style.stroke, ground), + fill: ratio(style.fill, ground), + }); + } + } + return found; + }); +} + +/** + * Règle de la recette (recette-ux.md, « Pions de l'ordinateur lisibles + * partout ») : là où un pion de l'ordinateur ne se détache pas de son fond + * par sa couleur (moins de 3:1 : sur la barre, les flèches sombres, le fond + * sombre), son contour s'en détache à 3:1 au moins (guide de marque : + * contours porteurs de sens à 3:1). contrasts : le résultat de + * checkerContrasts. Renvoie la liste des écarts, décrits (vide si tout va + * bien). + */ +export function checkerContrastProblems(contrasts) { + const problems = new Set(); + for (const c of contrasts) { + if (c.side === "computer" && c.fill < 3 && c.edge < 3) { + problems.add(`pion de l'ordinateur sur ${c.on} (${c.board}) : contour à ${c.edge.toFixed(2)}:1`); + } + } + return [...problems]; +} + +/** Le plus faible contraste du contour, par camp et par fond, pour le + * journal du test (« ordinateur sur barre 6.79:1, … »). */ +export function checkerContrastSummary(contrasts) { + const lowest = new Map(); + for (const c of contrasts) { + const key = `${c.side === "computer" ? "ordinateur" : "joueur"} sur ${c.on}`; + lowest.set(key, Math.min(lowest.get(key) ?? Number.POSITIVE_INFINITY, c.edge)); + } + return [...lowest].map(([key, edge]) => `${key} ${edge.toFixed(2)}:1`).join(", "); +} diff --git a/tests/browser/retours-joueurs.mjs b/tests/browser/retours-joueurs.mjs new file mode 100644 index 0000000..4934dd7 --- /dev/null +++ b/tests/browser/retours-joueurs.mjs @@ -0,0 +1,258 @@ +// retours-joueurs.mjs -- les règles tirées des retours des premiers joueurs +// (ADR 0010, amendement du 2026-11-11 ; recette-ux.md), dans un vrai +// navigateur sans écran (Playwright), sur le site servi par l'image de +// production, en thème clair et en thème sombre : +// +// - pions de l'ordinateur lisibles sur la barre : un pion de chaque camp sur +// la barre, contraste du contour de chaque pion sur son fond +// (recette.mjs, checkerContrasts) ; +// - numéros des flèches sur le cadre, en portrait comme en paysage, de +// 12 px au moins ; +// - étape 2 du tutoriel : deux flèches circulaires, claire et sombre, sous +// les pions, jamais en brique. +// +// Copyright (C) 2026 les auteurs d'Ustagammon. +// Licence : GPLv3 ou ultérieure (voir LICENSE). +// +// Usage : node retours-joueurs.mjs +// Lancé par tests/browser.sh. Captures (Chromium) dans +// /retours-joueurs/, en 412×915 et 915×412. + +import { mkdirSync, writeFileSync } from "node:fs"; +import { chromium, firefox } from "playwright"; +import { + checkerContrastProblems, + checkerContrastSummary, + checkerContrasts, + hiddenUnderActionBar, +} from "./recette.mjs"; + +const [base, browserName, shots] = process.argv.slice(2); +const browserType = { chromium, firefox }[browserName]; +if (!base || !browserType || !shots) { + console.error("usage : node retours-joueurs.mjs "); + process.exit(2); +} +const full = browserName === "chromium"; +const dir = `${shots}/retours-joueurs`; +mkdirSync(dir, { recursive: true }); +const SIZES = [ + { width: 412, height: 915 }, + { width: 915, height: 412 }, +]; +// Positions de gnubg, chacun vu de lui-même : la position de départ, et la +// même avec un pion de chaque camp de sa flèche 24 sur la barre. +const OPENING = "4HPwATDgc/ABMA"; +const BOTH_ON_BAR = "4HPwAVDgc/ABUA"; + +const problems = []; +const log = (text) => console.log(`[${browserName}] ${text}`); +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); +const contrasts = { light: [], dark: [] }; +let captures = 0; + +function check(condition, message) { + if (!condition) { + throw new Error(message); + } +} + +const browser = await browserType.launch(); + +/** Un contexte neuf dans un thème, tutoriel déjà vu (l'accueil ne propose + * plus le premier lancement). */ +async function newPage(theme) { + const context = await browser.newContext({ + viewport: SIZES[0], + deviceScaleFactor: 1, + locale: "fr-FR", + reducedMotion: "reduce", + }); + await context.addInitScript((saved) => { + window.__csp = []; + document.addEventListener("securitypolicyviolation", (e) => + window.__csp.push(`${e.effectiveDirective} ${e.blockedURI}`), + ); + if (localStorage.getItem("ustagammon.settings") === null) { + localStorage.setItem("ustagammon.settings", saved); + } + }, JSON.stringify({ theme, tutorialSeen: 1 })); + const page = await context.newPage(); + page.on("pageerror", (error) => problems.push(`erreur de la page : ${error.message}`)); + page.on("console", (message) => { + if (message.type() === "error") { + problems.push(`console : ${message.text()}`); + } + }); + return { context, page }; +} + +/** Attend une condition dans la page (sans page.waitForFunction, qui passe + * par eval, interdit par la politique de sécurité du site). */ +async function until(page, what, fn, arg, ms = 60_000) { + for (let waited = 0; waited < ms; waited += 100) { + if (await page.evaluate(fn, arg).catch(() => false)) { + return; + } + await sleep(100); + } + await page.screenshot({ path: `${dir}/${browserName}-bloque.png` }).catch(() => {}); + throw new Error(`attente trop longue : ${what}`); +} + +/** Une partie à partir d'une position, joueur au trait, dés à lancer. */ +async function startFrom(page, position) { + await page.goto(`${base}#/position/${encodeURIComponent(position)}`); + await until(page, "position lue", () => { + const play = document.getElementById("position-play"); + return !play.hidden && !play.disabled; + }); + await page.click("#position-play"); + await until( + page, + "partie commencée", + () => + location.hash === "#/" && + !document.body.classList.contains("busy") && + !document.getElementById("roll").hidden, + ); +} + +/** Les numéros des flèches du plateau visible : tous sur le cadre (hors du + * fond du plateau) et de 12 px au moins à l'écran. Renvoie les écarts. */ +async function numberProblems(page, selector) { + return page.evaluate((selector) => { + const svg = document.querySelector(selector); + const box = svg.getBoundingClientRect(); + const vb = svg.viewBox.baseVal; + const scale = Math.min(box.width / vb.width, box.height / vb.height); + const bed = svg.querySelector(".board-bed").getBBox(); + const found = []; + const numbers = [...svg.querySelectorAll(".point-number")]; + if (numbers.length !== 24) { + found.push(`${numbers.length} numéros`); + } + for (const text of numbers) { + const b = text.getBBox(); + const px = Number(text.getAttribute("font-size")) * scale; + if (b.y + b.height > bed.y + 0.5 && b.y < bed.y + bed.height - 0.5) { + found.push(`flèche ${text.textContent} dans le fond du plateau`); + } + if (px < 12) { + found.push(`flèche ${text.textContent} en ${px.toFixed(1)} px`); + } + } + return found; + }, selector); +} + +/** Les règles sur l'écran courant, dans chaque taille, et sa capture + * (Chromium). */ +async function checkScreen(page, theme, name, selector, settle = async () => {}) { + for (const size of SIZES) { + await page.setViewportSize(size); + await sleep(300); + await settle(); + const where = `${name} ${theme} ${size.width}x${size.height}`; + const numbers = await numberProblems(page, selector); + check(numbers.length === 0, `${where} : numéros : ${numbers.join(", ")}`); + const measured = await checkerContrasts(page); + check(measured.length > 0, `${where} : aucun pion mesuré`); + const low = checkerContrastProblems(measured); + check(low.length === 0, `${where} : ${low.join(", ")}`); + contrasts[theme].push(...measured); + if (full) { + const hidden = await hiddenUnderActionBar(page); + check(hidden.length === 0, `${where} : caché sous la barre d'action : ${hidden.join(", ")}`); + await page.screenshot({ path: `${dir}/${name}-${theme}-${size.width}x${size.height}.png` }); + captures++; + } + } + await page.setViewportSize(SIZES[0]); + await sleep(200); +} + +for (const theme of ["light", "dark"]) { + const { context, page } = await newPage(theme); + + // Un pion de chaque camp sur la barre. + await startFrom(page, BOTH_ON_BAR); + check( + (await page.evaluate(() => document.documentElement.dataset.theme)) === theme, + `thème ${theme} non appliqué`, + ); + const onBar = await page.evaluate(() => + ["player", "computer"].map( + (side) => document.querySelectorAll(`#board [data-checker="bar-${side}"]`).length, + ), + ); + check(onBar[0] === 1 && onBar[1] === 1, `pions sur la barre : ${onBar}`); + await checkScreen(page, theme, "barre", "#board"); + const bar = contrasts[theme].filter((c) => c.on === "barre" && c.side === "computer"); + check(bar.length > 0, `${theme} : pion de l'ordinateur sur la barre non mesuré`); + + // La position de départ : les numéros sur le cadre, en portrait aussi. + await startFrom(page, OPENING); + await checkScreen(page, theme, "numeros-sur-le-cadre", "#board"); + + // L'étape 2 du tutoriel : deux flèches circulaires sous les pions. + await page.goto(`${base}#/tutoriel/2`); + const ready = () => + until(page, "tutoriel, étape 2", () => { + const section = document.getElementById("tutorial"); + return ( + location.hash === "#/tutoriel/2" && + section.dataset.ready !== undefined && + document.querySelectorAll("#tutorial-board .tutorial-path").length === 1 + ); + }); + await ready(); + const arrows = async () => { + const drawn = await page.evaluate(() => { + const svg = document.getElementById("tutorial-board"); + const children = [...svg.children]; + const path = svg.querySelector(".tutorial-path"); + const colors = (selector) => + [...path.querySelectorAll(selector)].map((el) => getComputedStyle(el).stroke); + return { + path: children.indexOf(path), + pieces: children.findIndex((c) => c.matches("g.pieces")), + player: colors(".direction-line.direction-player"), + computer: colors(".direction-line.direction-computer"), + heads: path.querySelectorAll(".direction-head").length, + pieceColors: ["player", "computer"].map( + (side) => getComputedStyle(svg.querySelector(`.checker-${side}`)).fill, + ), + }; + }); + check(drawn.path >= 0 && drawn.path < drawn.pieces, `étape 2 : flèches au-dessus des pions ${JSON.stringify(drawn)}`); + check(drawn.player.length === 1 && drawn.computer.length === 1 && drawn.heads === 2, + `étape 2 : flèches ${JSON.stringify(drawn)}`); + // Aux couleurs des pions de chaque camp, donc jamais en brique. + check(drawn.player[0] === drawn.pieceColors[0] && drawn.computer[0] === drawn.pieceColors[1], + `étape 2 : flèches pas aux couleurs des pions ${JSON.stringify(drawn)}`); + }; + await checkScreen(page, theme, "tutoriel-2", "#tutorial-board", async () => { + await ready(); + await arrows(); + }); + + const csp = await page.evaluate(() => window.__csp); + check(csp.length === 0, `violations de la CSP : ${csp.join(", ")}`); + await context.close(); + log(`thème ${theme} : contours ${checkerContrastSummary(contrasts[theme])}`); +} + +await browser.close(); +if (full) { + // Les mesures, à côté des captures. + writeFileSync( + `${dir}/contrastes.txt`, + ["light", "dark"].map((theme) => `${theme}\t${checkerContrastSummary(contrasts[theme])}`).join("\n") + "\n", + ); +} +check(problems.length === 0, `problèmes relevés :\n ${problems.join("\n ")}`); +log( + "retours des joueurs : pions des deux camps sur la barre, numéros sur le cadre, " + + `flèches circulaires du tutoriel, thèmes clair et sombre${full ? ` ; ${captures} captures` : ""}`, +); -- 2.54.0 From 5a13c40bbb6a9dfd443b413796c9a10a1695b9cf Mon Sep 17 00:00:00 2001 From: Pierre MARTIN Date: Thu, 12 Nov 2026 02:54:27 +0100 Subject: [PATCH 6/8] =?UTF-8?q?Recette=20des=20retours=20:=20capture=20apr?= =?UTF-8?q?=C3=A8s=20le=20bandeau,=20capture=20si=20le=20bouton=20reste=20?= =?UTF-8?q?cach=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- tests/browser/retours-joueurs.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/browser/retours-joueurs.mjs b/tests/browser/retours-joueurs.mjs index 4934dd7..f241696 100644 --- a/tests/browser/retours-joueurs.mjs +++ b/tests/browser/retours-joueurs.mjs @@ -107,7 +107,10 @@ async function startFrom(page, position) { const play = document.getElementById("position-play"); return !play.hidden && !play.disabled; }); - await page.click("#position-play"); + await page.click("#position-play", { timeout: 15_000 }).catch(async (error) => { + await page.screenshot({ path: `${dir}/${browserName}-bloque.png` }).catch(() => {}); + throw error; + }); await until( page, "partie commencée", @@ -149,6 +152,8 @@ async function numberProblems(page, selector) { /** Les règles sur l'écran courant, dans chaque taille, et sa capture * (Chromium). */ async function checkScreen(page, theme, name, selector, settle = async () => {}) { + // Le bandeau « Reprendre l'ancienne » (10 s) n'est pas le sujet. + await until(page, "bandeau parti", () => document.getElementById("banner").hidden, null, 15_000); for (const size of SIZES) { await page.setViewportSize(size); await sleep(300); -- 2.54.0 From 57eeb19630536dd33a91fc7e1e28be2f898dbcaf Mon Sep 17 00:00:00 2001 From: Pierre MARTIN Date: Thu, 12 Nov 2026 03:34:40 +0100 Subject: [PATCH 7/8] =?UTF-8?q?Plan=20des=20retours=20des=20premiers=20jou?= =?UTF-8?q?eurs=20clos=20:=20r=C3=A9sultat=20dans=20la=20description=20de?= =?UTF-8?q?=20la=20MR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/plans/2026-11-11-retours-joueurs.md | 50 ------------------------ 1 file changed, 50 deletions(-) delete mode 100644 docs/plans/2026-11-11-retours-joueurs.md diff --git a/docs/plans/2026-11-11-retours-joueurs.md b/docs/plans/2026-11-11-retours-joueurs.md deleted file mode 100644 index b1706b7..0000000 --- a/docs/plans/2026-11-11-retours-joueurs.md +++ /dev/null @@ -1,50 +0,0 @@ -# Retours des premiers joueurs - -- **Date** : 2026-11-11 -- **Étape** : 1 (retours des premiers joueurs, après 1f) -- **Statut** : en cours -- **Exécutant** : sous-agent general-purpose - -## Objectif -Mettre en œuvre dans l'application les règles tirées des retours des premiers -joueurs par la session design (MR !54 et !55) : pions blancs et noirs avec -contour, pions de l'ordinateur lisibles sur la barre, numéros des flèches sur -le cadre en portrait, flèches circulaires au tutoriel, vocabulaire des jans. - -## Consignes -1. **Pions blancs et noirs, avec contour** (ADR 0010, amendement du - 2026-11-11) : reporter dans `web/app.css`, pour les deux thèmes, les - valeurs de `docs/identite/charte/tokens.json` pour `checker-*`, - `checker-*-edge`, `count-*`, `die-*`, `pip-*`, `cube` et `cube-text`. Le - logo et le cadre ne changent pas. Aucune couleur en dur dans le TypeScript. -2. **Pions de l'ordinateur lisibles sur la barre** : contour noir sur la barre - à environ 6,8:1 (clair) et 8,5:1 (sombre). `tests/browser/recette.mjs` - contrôle le contraste du contour des pions sur le fond qui les porte - (3:1 au moins, règle de `recette-ux.md`). -3. **Numéros des flèches sur le cadre en portrait** : bandes haute et basse du - cadre agrandies pour loger des numéros de 12 px au moins ; `geometry.ts`, - ses tests et la recette adaptés. -4. **Tutoriel, étape 2** : deux flèches circulaires (claire pour le joueur, - sombre pour l'ordinateur, chacune dans le sens de son joueur, avec une - pointe), dessinées sous les pions, avec un liseré de la couleur opposée ; - jamais en brique. -5. **Les jans** : « jan intérieur » (flèches 1 à 6) et « jan extérieur » - (7 à 12) remplacent « camp » dans le tutoriel, l'aide, les phrases du maître - et les prompts ; fiches `jan-interieur.md` (Home board) et - `jan-exterieur.md` (Outer board) reliées au code ; fiche « camp » retirée - sans lien cassé ; dans `regles.md`, pions « blancs » et « noirs ». - -Limites : CSP stricte (aucun `.style.`), aucune nouvelle dépendance, `-j2` au -plus, pas de recompilation du moteur. Vérifier avec `docker ps` qu'aucun autre -test navigateur ne tourne avant `make test-browser`. - -## Critères de réussite -- `make lint`, `make test` et `make test-browser` passent. -- Captures dans `build/browser-shots/retours-joueurs/` (412×915 et 915×412, - thèmes clair et sombre) : pions des deux camps sur la barre, étape 2 du - tutoriel, portrait avec les numéros sur le cadre. -- Aucune occurrence de « camp » au sens de jan dans `docs/tutoriel/`, - `docs/scenarios/`, `docs/maitre/` et `docs/prompts/`. - -## Résultat -Rempli à la clôture. -- 2.54.0 From a238da77ec9d1f19eb49908dad7f427be5faaa12 Mon Sep 17 00:00:00 2001 From: Pierre MARTIN Date: Thu, 12 Nov 2026 09:05:45 +0100 Subject: [PATCH 8/8] =?UTF-8?q?Contour=20des=20pions=20blancs=20plus=20som?= =?UTF-8?q?bre=20en=20th=C3=A8me=20clair=20;=20recette=20des=20pions=20du?= =?UTF-8?q?=20joueur?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- tests/browser/recette.mjs | 18 ++++++++++++++---- tests/browser/retours-joueurs.mjs | 2 +- web/app.css | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/tests/browser/recette.mjs b/tests/browser/recette.mjs index c860809..0d60a23 100644 --- a/tests/browser/recette.mjs +++ b/tests/browser/recette.mjs @@ -236,16 +236,26 @@ export async function checkerContrasts(target) { * partout ») : là où un pion de l'ordinateur ne se détache pas de son fond * par sa couleur (moins de 3:1 : sur la barre, les flèches sombres, le fond * sombre), son contour s'en détache à 3:1 au moins (guide de marque : - * contours porteurs de sens à 3:1). contrasts : le résultat de - * checkerContrasts. Renvoie la liste des écarts, décrits (vide si tout va - * bien). + * contours porteurs de sens à 3:1). Et (« Pions du joueur lisibles + * partout ») : en thème clair, le contour d'un pion du joueur atteint 3:1 sur + * les flèches en chêne et sur le fond. contrasts : le résultat de + * checkerContrasts ; theme : « light » ou « dark ». Renvoie la liste des + * écarts, décrits (vide si tout va bien). */ -export function checkerContrastProblems(contrasts) { +export function checkerContrastProblems(contrasts, theme) { const problems = new Set(); for (const c of contrasts) { if (c.side === "computer" && c.fill < 3 && c.edge < 3) { problems.add(`pion de l'ordinateur sur ${c.on} (${c.board}) : contour à ${c.edge.toFixed(2)}:1`); } + if ( + c.side === "player" && + theme === "light" && + (c.on === "flèche paire" || c.on === "fond") && + c.edge < 3 + ) { + problems.add(`pion du joueur sur ${c.on} (${c.board}) : contour à ${c.edge.toFixed(2)}:1`); + } } return [...problems]; } diff --git a/tests/browser/retours-joueurs.mjs b/tests/browser/retours-joueurs.mjs index f241696..5595715 100644 --- a/tests/browser/retours-joueurs.mjs +++ b/tests/browser/retours-joueurs.mjs @@ -163,7 +163,7 @@ async function checkScreen(page, theme, name, selector, settle = async () => {}) check(numbers.length === 0, `${where} : numéros : ${numbers.join(", ")}`); const measured = await checkerContrasts(page); check(measured.length > 0, `${where} : aucun pion mesuré`); - const low = checkerContrastProblems(measured); + const low = checkerContrastProblems(measured, theme); check(low.length === 0, `${where} : ${low.join(", ")}`); contrasts[theme].push(...measured); if (full) { diff --git a/web/app.css b/web/app.css index e01ab88..7ccfe51 100644 --- a/web/app.css +++ b/web/app.css @@ -82,7 +82,7 @@ --point-b: #c9ae88; --point-number: #f3ead8; --checker-player: #fafaf7; - --checker-player-edge: #6e6e69; + --checker-player-edge: #55554f; --checker-computer: #1c1c1c; --checker-computer-edge: #b5b5b0; --count-player: #1c1c1c; -- 2.54.0