From 966822084a138cc26dd7c30c187607f05bbe3ece Mon Sep 17 00:00:00 2001 From: Pierre MARTIN Date: Thu, 12 Nov 2026 02:50:10 +0100 Subject: [PATCH] =?UTF-8?q?Tutoriel=20:=20deux=20fl=C3=A8ches=20circulaire?= =?UTF-8?q?s=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