Compare commits

..
14 Commits
Author SHA1 Message Date
Pierre MARTINandClaude Opus 5.5 1f69635c49 Fusion de la MR !57 : nouvelle partie directe après un bilan, bouton Commencer, fiche Notation des coups
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 23:02:48 +01:00
Pierre MARTINandClaude Opus 5.5 fb161789bf Liens de notation : pointillé discret, zone d'appui agrandie
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 22:41:42 +01:00
Pierre MARTINandClaude Opus 5.5 e20f215030 Fusion de main (1f, prompts à copier) dans nouvelle-partie-directe
Conflits résolus en gardant les deux apports : la feuille du prompt et la
correction de reviewFinishedGame dans app.ts ; prompts et nouvelle-partie
dans tests/browser.sh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 22:41:42 +01:00
Pierre MARTINandClaude Opus 5.5 abff843519 Plan de la nouvelle partie directe clos : résultat dans la description de la MR
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 22:39:47 +01:00
Pierre MARTINandClaude Opus 5.5 05bd06c946 Fusion de la MR !56 : demander à une IA, prompts à copier (1f)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 22:28:40 +01:00
Pierre MARTINandClaude Opus 5.5 4050517904 Glossaire : fiche « Notation des coups », touchable depuis la partie
La fiche explique 13/7, 24/22*, 6/off, bar/22 et 8/4(2), dans la notation
de gnubg. Le type MoveNotation la relie au code. Le coup de la ligne
« L'ordinateur a joué » et celui de la feuille du maître mènent à la fiche.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 22:15:09 +01:00
Pierre MARTINandClaude Opus 5.5 05d2479bd3 Écran « Nouvelle partie » : « Commencer la partie » ou « Commencer le match »
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 22:15:09 +01:00
Pierre MARTINandClaude Opus 5.5 5da3baecde Nouvelle partie : plus de retour au bilan de l'ancienne partie
Le passage différé au bilan (reviewFinishedGame) ne vérifiait pas que la
partie finie était encore à l'écran : « Commencer » redessinait d'abord
l'ancienne partie finie (adresse #/), puis lançait la nouvelle, et la
microtâche ouvrait le bilan de l'ancienne par-dessus. Le bilan ne s'ouvre
plus que pour la partie finie encore affichée, et « Commencer » lance la
partie avant de changer d'adresse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 22:15:09 +01:00
pierre 65ee563794 Fusion de la MR !55 : pions blancs et noirs 2026-11-11 21:14:56 +00:00
Pierre MARTINandClaude Opus 5.5 145c19ac77 Identité : pions blancs et noirs avec contour (ADR 0010 amendé, choix de Pierre)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 22:14:54 +01:00
Pierre MARTINandClaude Opus 5.5 0bb41524bf Test navigateur : nouvelle partie après une partie finie et son bilan
Reproduit le défaut signalé : après une partie finie, l'application
rouverte, « Commencer » de l'écran « Nouvelle partie » ramenait au bilan
de l'ancienne partie. Vérifie l'accueil, le menu et « Changer les réglages ».

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 22:14:46 +01:00
pierre 6a1ee6f65a Fusion de la MR !54 : retours des premiers joueurs 2026-11-11 21:00:44 +00:00
Pierre MARTINandClaude Opus 5.5 22e415bb81 Charte : retours des premiers joueurs (pions sombres lisibles, numéros sur le cadre, jans, flèches circulaires)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 22:00:42 +01:00
Pierre MARTINandClaude Opus 5.5 62ecff245f Plan : nouvelle partie directe, libellé « Commencer », notation des coups
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 21:54:18 +01:00
22 changed files with 591 additions and 83 deletions
+2 -2
View File
@@ -10,7 +10,7 @@
// stockage, dans le journal (events.ts, champ advice). Le format est
// durable (ADR 0007) : chaque conseil porte sa version (ADVICE_VERSION).
import type { CubeAction, CubeEquities, Ply, Skill } from "./engine.js";
import type { CubeAction, CubeEquities, MoveNotation, Ply, Skill } from "./engine.js";
import type { HalfMove, PositionId } from "./position.js";
import type { Roll } from "./roll.js";
@@ -29,7 +29,7 @@ export type Advice = MoveAdvice | CubeAdvice;
/** Un coup, tel que le conseil le retient. */
export interface AdvisedMove {
/** Notation de gnubg, par exemple « 8/5 6/5 ». */
readonly notation: string;
readonly notation: MoveNotation;
readonly halfMoves: readonly HalfMove[];
/** Équité (avec videau) du point de vue du joueur, et le niveau auquel
* le moteur l'a évaluée. */
+10 -1
View File
@@ -281,6 +281,15 @@ export interface EngineMatchAfter {
readonly crawford: boolean;
}
/**
* Un coup écrit dans la notation de gnubg : « 8/5 6/5 », « 24/22* »,
* « 6/off », « bar/22 », « 8/4(2) ». Le moteur l'écrit ; l'application ne
* fait que l'afficher.
*
* Glossaire : docs/glossaire/notation-des-coups.md
*/
export type MoveNotation = string;
/**
* Un coup légal complet.
*
@@ -288,7 +297,7 @@ export interface EngineMatchAfter {
*/
export interface Move {
/** Notation de gnubg, par exemple « 8/5 6/5 ». */
readonly notation: string;
readonly notation: MoveNotation;
/** Ses demi-coups, dans un ordre jouable. */
readonly halfMoves: readonly HalfMove[];
/** La position après le coup, vue du joueur qui vient de jouer. */
+10 -2
View File
@@ -12,7 +12,15 @@
import type { Advice } from "./advice.js";
import { type Branch, branches, switchToBranch } from "./branches.js";
import { CENTERED_CUBE, type Cube } from "./cube.js";
import type { BoardSide, Engine, EngineCube, Level, Ply, ResultKind } from "./engine.js";
import type {
BoardSide,
Engine,
EngineCube,
Level,
MoveNotation,
Ply,
ResultKind,
} from "./engine.js";
import {
appendEvent,
currentLine,
@@ -290,7 +298,7 @@ export class Game {
* À défaut (aucun coup légal ne correspond), les demi-coups ; chaîne
* vide si l'événement n'est pas un coup ou si aucun pion n'a bougé.
*/
async moveNotation(eventId: string): Promise<string> {
async moveNotation(eventId: string): Promise<MoveNotation> {
const line = currentLine({ ...this.#journal, head: eventId });
const event = line.at(-1);
if (event?.type !== "MovePlayed" || event.halfMoves.length === 0) {
+5 -3
View File
@@ -21,6 +21,7 @@ import { ADVICE_TEXTS, CUBE_GLOSSARY, formatEquity } from "./advice-texts.js";
import { askAiLink, clearSheet } from "./ask-ai.js";
import { type IconName, icon } from "./icons.js";
import { boardSvg, createMovePlayer, drawSteps, type MovePlayer } from "./move-board.js";
import { showWithNotation } from "./notation.js";
import { routeHash } from "./router.js";
/** Préfixe des identifiants du plateau de la feuille. */
@@ -203,8 +204,9 @@ export function createAdviceView(options: AdviceViewOptions): AdviceView {
const svg = sheetBoard(boardBox);
if (best === null) {
svg.dataset.move = "mine";
svg.setAttribute("aria-label", ADVICE_TEXTS.legendSame(advice.played.notation));
legend.textContent = ADVICE_TEXTS.legendSame(advice.played.notation);
const text = ADVICE_TEXTS.legendSame(advice.played.notation);
svg.setAttribute("aria-label", text);
showWithNotation(legend, text, advice.played.notation);
void player?.play(mine);
return;
}
@@ -233,7 +235,7 @@ export function createAdviceView(options: AdviceViewOptions): AdviceView {
shown === "mine"
? ADVICE_TEXTS.legendMine(notation)
: ADVICE_TEXTS.legendBest(notation);
legend.textContent = text;
showWithNotation(legend, text, notation);
svg.setAttribute("aria-label", text);
svg.dataset.move = shown;
void player?.play(shown === "mine" ? mine : best);
+47 -14
View File
@@ -36,6 +36,7 @@ import {
} from "../domain/analysis/phrases.js";
import type { Advice } from "../domain/game/advice.js";
import type { Cube } from "../domain/game/cube.js";
import type { MoveNotation } from "../domain/game/engine.js";
import type { GameJournal } from "../domain/game/events.js";
import type { Game, MoveSteps, NewGame, SideBoards } from "../domain/game/game.js";
import type { HalfMove } from "../domain/game/position.js";
@@ -84,6 +85,7 @@ import { installPlatform, startInstaller } from "./install.js";
import { MATCH_TEXTS } from "./match-texts.js";
import { replacingLink, startNavigation } from "./navigation.js";
import { createNewGameView } from "./new-game-view.js";
import { showWithNotation } from "./notation.js";
import { createPromptSource, type PromptsAccess } from "./prompt-source.js";
import { createReviewRunner, type RunAnalysis } from "./review-runner.js";
import {
@@ -443,6 +445,10 @@ export function startApp(options: AppOptions): void {
let stuck = false;
let busy = true;
let info: string = TEXTS.loading;
// La dernière ligne « L'ordinateur a joué … » et le coup qu'elle cite :
// tant qu'elle est la ligne d'information, ce coup mène à la fiche
// « Notation des coups ».
let computerLine: ComputerTurnLine | null = null;
let engineFailed = false;
// Le dernier coup de l'ordinateur, montré (traces, « Revoir ») jusqu'à
// la prochaine action du joueur ; null : rien à montrer.
@@ -646,7 +652,11 @@ export function startApp(options: AppOptions): void {
} else if (computerCouldNotMove(game)) {
ui.info.textContent = TEXTS.computerCannotMove(rollBefore(game));
} else {
ui.info.textContent = info;
showWithNotation(
ui.info,
info,
computerLine?.text === info ? computerLine.notation : null,
);
}
ui.prompt.textContent = panelShown
? ""
@@ -1037,7 +1047,8 @@ export function startApp(options: AppOptions): void {
while (game !== null && game.state.turn === "computer" && isPlaying(game)) {
await refresh();
await game.playComputerTurn();
info = await describeComputerTurn(game);
computerLine = await describeComputerTurn(game);
info = computerLine.text;
save();
played = true;
}
@@ -1529,25 +1540,37 @@ export function startApp(options: AppOptions): void {
* La partie à l'écran vient de finir : son analyse part en
* arrière-plan, et le bilan remplace le panneau de fin de partie dès
* qu'il est prêt (charte 1e), si le panneau est toujours là.
*
* Le bilan s'ouvre un peu plus tard (microtâche) : seulement si cette
* partie finie est encore celle de l'écran. Sinon, « Commencer » de
* l'écran « Nouvelle partie » ouvrait le bilan de l'ancienne partie
* par-dessus la nouvelle, qui venait de commencer entre-temps (défaut
* signalé le 2026-11-11).
*/
function reviewFinishedGame(): void {
const id = options.games.shownId();
if (game === null || id === null || busy || game.state.phase !== "over") {
const finished = game;
if (finished === null || id === null || busy || finished.state.phase !== "over") {
return;
}
// L'analyse d'une autre partie, regardée, passe d'abord.
const running = reviewRunner.running;
if (running === null || running === id) {
review(id, game.journal);
review(id, finished.journal);
}
if (
navigation.route.screen === "game" &&
!panelDismissed &&
phrases !== null &&
readyForSummary(game.journal)
readyForSummary(finished.journal)
) {
queueMicrotask(() => {
if (navigation.route.screen === "game" && !panelDismissed) {
if (
game === finished &&
options.games.shownId() === id &&
navigation.route.screen === "game" &&
!panelDismissed
) {
navigation.go(routeHash({ screen: "summary", game: id }));
}
});
@@ -2015,8 +2038,10 @@ export function startApp(options: AppOptions): void {
welcomed: () => saveSettings(welcomed()),
});
// L'écran « Nouvelle partie » : « Commencer » remplace son adresse par
// celle de la partie (le retour du téléphone ne le rouvre pas).
// L'écran « Nouvelle partie » : « Commencer » lance la partie, puis
// remplace son adresse par celle de la partie (le retour du téléphone
// ne le rouvre pas). Dans cet ordre : l'écran de jeu ne se redessine
// jamais avec l'ancienne partie, finie, et son panneau de fin.
const newGameView = createNewGameView({
section: ui.newGameSection,
settings: () => settings,
@@ -2025,8 +2050,8 @@ export function startApp(options: AppOptions): void {
if (busy || game === null) {
return;
}
navigation.replace("#/");
newGame(chosenGame(settings));
navigation.replace("#/");
},
});
leavingLink(newGameView.closeLink, (route) => route.screen === "newGame");
@@ -2661,22 +2686,30 @@ async function computerMoveToShow(game: Game): Promise<MoveSteps | null> {
return game.lastMoveSteps();
}
/** La ligne d'information après le tour de l'ordinateur, et le coup
* qu'elle cite (null s'il n'a pas joué de coup). */
interface ComputerTurnLine {
readonly text: string;
readonly notation: MoveNotation | null;
}
/** Ce qu'a fait l'ordinateur pendant son tour (sans durée de calcul). Son
* coup s'écrit dans la notation du moteur, comme dans l'analyse. */
async function describeComputerTurn(game: Game): Promise<string> {
async function describeComputerTurn(game: Game): Promise<ComputerTurnLine> {
const last = game.events.at(-1);
if (last?.type === "CubeOffered") {
return "";
return { text: "", notation: null };
}
const played = game.events.findLast((e) => e.type === "MovePlayed");
if (played?.type !== "MovePlayed") {
return "";
return { text: "", notation: null };
}
const dice = rollBefore(game);
if (played.halfMoves.length === 0) {
return TEXTS.computerCannotMove(dice);
return { text: TEXTS.computerCannotMove(dice), notation: null };
}
return TEXTS.computerMoved(dice, await game.moveNotation(played.id));
const notation = await game.moveNotation(played.id);
return { text: TEXTS.computerMoved(dice, notation), notation };
}
/** Le résultat d'une partie finie, en deux parties : le titre du panneau
+1 -1
View File
@@ -57,7 +57,7 @@ export const MATCH_TEXTS = {
advice: "Conseil du maître",
adviceDetail: "Une ligne après chacun de vos coups",
startGame: "Commencer la partie",
startMatch: (n: number) => `Commencer le match en ${n} points`,
startMatch: "Commencer le match",
kept: "Vos choix sont gardés pour la prochaine fois.",
// Score du match sur l'écran de jeu
+1 -3
View File
@@ -200,9 +200,7 @@ export function createNewGameView(options: NewGameViewOptions): NewGameView {
levelName.textContent = MATCH_TEXTS.levels[s.level].name;
levelText.textContent = MATCH_TEXTS.levels[s.level].text;
advice.setAttribute("aria-checked", String(s.masterAdvice));
start.textContent = isMatch
? MATCH_TEXTS.startMatch(s.matchLength)
: MATCH_TEXTS.startGame;
start.textContent = isMatch ? MATCH_TEXTS.startMatch : MATCH_TEXTS.startGame;
start.disabled = busy;
},
focus() {
+34
View File
@@ -0,0 +1,34 @@
// Un coup écrit dans la notation du moteur (« 24/22* 6/1 », « 8/4(2) »),
// touchable vers sa fiche du glossaire : un débutant y apprend ce que
// veulent dire l'étoile, « off », « bar » et « (2) ».
//
// Copyright (C) 2026 les auteurs d'Ustagammon.
// Licence : GPLv3 ou ultérieure (voir LICENSE).
import type { MoveNotation } from "../domain/game/engine.js";
import { routeHash } from "./router.js";
/** La fiche du glossaire de la notation (docs/glossaire/, sans .md). */
export const NOTATION_TERM = "notation-des-coups";
/**
* Remplace le contenu de parent par text, où la première occurrence de
* notation devient un lien vers la fiche « Notation des coups ». Sans
* notation dans le texte, le texte seul.
*/
export function showWithNotation(
parent: HTMLElement,
text: string,
notation: MoveNotation | null,
): void {
const at = notation === null || notation === "" ? -1 : text.indexOf(notation);
if (notation === null || at < 0) {
parent.textContent = text;
return;
}
const link = document.createElement("a");
link.className = "notation";
link.href = routeHash({ screen: "glossary", term: NOTATION_TERM });
link.textContent = notation;
parent.replaceChildren(text.slice(0, at), link, text.slice(at + notation.length));
}
+18
View File
@@ -60,3 +60,21 @@ couleur, en monochrome et en taille réelle d'icône (48 et 24 px).
en dessous de 32 px.
- Les icônes Android (premier plan, arrière-plan, monochrome) sont fournies en
SVG ; leur conversion en `VectorDrawable` se fera avec l'application Android.
## Amendements
### 2026-11-11 — Pions blancs et noirs, avec un contour
Retour des premiers joueurs, relayé par Pierre : les pions sombres se voyaient
mal sur la barre, et les joueurs préféraient le blanc et le noir à l'ivoire et
au noyer. Après comparaison sur le canevas (rangée « Retours des premiers
joueurs »), Pierre retient le blanc et le noir, avec un contour.
- Pions du joueur blancs (`#FAFAF7`), contour et filet gris foncé
(`#6E6E69`) ; pions de l'ordinateur noirs (`#1C1C1C`), contour et filet gris
clair (`#B5B5B0`), dans les deux thèmes. Dés et videau suivent les pions.
- Le contour assure la lisibilité partout : pions noirs sur la barre (6,8:1
en thème clair, 8,5:1 en sombre), pions blancs sur le fond clair (3,8:1).
- Le logo ne change pas : ses pions restent ivoire et noyer, qui sont la
matière de la marque, distincte des pièces du jeu.
- Valeurs dans `docs/identite/charte/tokens.json`.
+1
View File
@@ -59,6 +59,7 @@ exportée. `make test` vérifie le lien dans les deux sens : chaque fiche avec
| [Match](match.md) | Match |
| [Moteur](moteur.md) | Engine |
| [Niveau de jeu](niveau-de-jeu.md) | Level |
| [Notation des coups](notation-des-coups.md) | Move notation |
| [Partie](partie.md) | Game |
| [Partie enregistrée](partie-enregistree.md) | Saved game |
| [Passer](passer.md) | Drop |
+27
View File
@@ -0,0 +1,27 @@
---
term: Notation des coups
english: Move notation
code: MoveNotation
---
# Notation des coups
**En bref** : un coup s'écrit case de départ/case d'arrivée pour chaque pion, comme 13/7 ; une étoile signale un pion adverse frappé (24/22*), « off » un pion sorti (6/off), « bar » un pion qui rentre depuis la barre (bar/22), et (2) un déplacement fait deux fois avec un double (8/4(2)).
Ustagammon écrit les coups comme GNU Backgammon, le moteur qui les calcule.
Chaque joueur numérote les cases de son propre point de vue, de 1 (la case la
plus proche de sa sortie) à 24 :
- `13/7` : un pion part de la case 13 et arrive sur la case 7. Un coup de deux
dés compte en général deux déplacements, séparés par une espace :
`13/7 8/7`.
- `24/22*` : l'étoile dit que le pion arrivé sur la 22 y a frappé un pion
seul de l'adversaire, qui part sur la barre.
- `6/off` : le pion de la case 6 sort du plateau.
- `bar/22` : un pion qui était sur la barre rentre sur la case 22.
- `8/4(2)` : le même déplacement, fait deux fois. Avec un double, on joue
quatre fois le dé : `8/4(2) 6/2(2)` déplace deux pions de 8 à 4 et deux de
6 à 2.
- `13/1*` : un seul pion qui avance avec ses deux dés s'écrit d'un trait,
de sa case de départ à sa case d'arrivée.
Voir aussi : [Coup](coup.md), [Demi-coup](demi-coup.md), [Barre](barre.md), [Sortie](sortie.md), [Blot](blot.md)
+34 -15
View File
@@ -76,9 +76,11 @@ interrompre.
demande au moteur si le coup du joueur fait partie des coups permis attendus.
- **Les pions bougent** : sur un écran d'explication, le coup illustré se joue
tout seul après une seconde, pion par pion (`duration-fast`), puis reste
affiché ; « Revoir » le rejoue. Sur les écrans à flèches (sens du jeu), un
tracé en pointillés `text` sur un liseré ivoire, jamais en brique (la brique
veut dire « à faire »).
affiché ; « Revoir » le rejoue. Sur l'écran du sens du jeu, **deux flèches
circulaires** : une claire pour vos pions, une sombre pour ceux de
l'ordinateur, chacune dans le sens de son joueur et finissant par une pointe,
dessinées sous les pions, avec un liseré de la couleur opposée pour rester
lisibles sur le plateau ; jamais en brique (la brique veut dire « à faire »).
- Un geste juste : coche `verdict-good`, vibration « pion posé », phrase
courte, « Continuer » devient l'action principale. Un geste faux : le pion
revient, et une phrase dit pourquoi, sans le mot « erreur ».
@@ -87,14 +89,14 @@ interrompre.
### Texte, première version
1. **Le but du jeu.** « Vous avez quinze pions ivoire, l'ordinateur quinze
pions noyer. Chacun fait faire le tour du plateau à ses pions, puis les
1. **Le but du jeu.** « Vous avez quinze pions blancs, l'ordinateur quinze
pions noirs. Chacun fait faire le tour du plateau à ses pions, puis les
sort. Le premier qui a sorti tous les siens gagne la partie. »
2. **Dans quel sens avancer.** « 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. Les pions noyer de
l'ordinateur font le même chemin, en sens inverse. »
2. **Dans quel sens avancer.** « Vos pions suivent la flèche claire : ils
partent d'en haut à droite, passent à gauche, puis reviennent en bas jusqu'à
votre jan intérieur, les flèches 1 à 6. C'est là que vous les rassemblez
avant de les sortir. 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.** « Chaque dé déplace un pion d'autant de
flèches. Avec 5 et 2, vous jouez un pion de 5 et un pion de 2, ou le même
pion de 5 puis de 2. Un double se joue quatre fois : avec 4-4, vous avancez
@@ -106,16 +108,16 @@ interrompre.
point 7 avec 6-1.
5. **Rentrer depuis la barre.** « Votre pion frappé attend sur la barre, au
milieu. Avant de jouer quoi que ce soit d'autre, il doit rentrer dans le
camp adverse, en haut à droite. Chaque dé indique une flèche d'entrée :
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. »
6. **Sortir ses pions.** « Quand vos quinze pions sont dans les six dernières
flèches, vous pouvez commencer à les sortir : un 6 sort un pion de la
6. **Sortir ses pions.** « 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. » *À vous* : sortir deux pions avec 6-3.
7. **Gammon et backgammon.** « Une partie gagnée vaut 1 point. Si
l'adversaire n'a sorti aucun pion, c'est un gammon : 2 points ; s'il lui
reste en plus un pion sur la barre ou dans votre camp, c'est un
reste en plus un pion sur la barre ou dans votre jan intérieur, c'est un
backgammon : 3 points. En partie libre, gammon et backgammon ne comptent
que si le videau a été doublé : c'est la règle de Jacoby. »
8. **Le videau.** « Le cube aux faces 2, 4, 8, 16, 32 et 64 sert à miser.
@@ -164,7 +166,7 @@ ouvrir, partager et retrouver avec le bouton « retour » du téléphone.
| « Je découvre », « Toutes les leçons » | `#/tutoriel/1` |
| Étape *n* du tutoriel (« Continuer », « Retour », segments de la barre de progression) | `#/tutoriel/<n>` |
| « Je connais les règles », « Reprendre la partie » | l'écran de jeu |
| Un terme du jeu dans un texte (blot, barre, camp…) | `#/glossaire/<terme>` |
| Un terme du jeu dans un texte (blot, barre, jan intérieur…) | `#/glossaire/<terme>` |
| Une position illustrée, « Voir sur le plateau » | `#/position/<Position ID>/<Match ID>` |
| Aide, onglet « Jouer » | `#/aide` ; une question ouverte : `#/aide/<scénario>` |
| Menu | `#/menu` |
@@ -174,3 +176,20 @@ ouvrir, partager et retrouver avec le bouton « retour » du téléphone.
- Le bouton « retour » du téléphone ferme d'abord la feuille ou la question
ouverte, puis revient à l'écran précédent ; il ne quitte jamais une partie
sans retour possible.
## Vocabulaire : les jans
Le plateau se partage en quatre jans de six flèches. Les nommer simplifie les
explications (retour des premiers joueurs, 2026-11-11) :
| Flèches | Nom |
| --- | --- |
| 1 à 6 | votre jan intérieur |
| 7 à 12 | votre jan extérieur |
| 13 à 18 | le jan extérieur de l'ordinateur |
| 19 à 24 | le jan intérieur de l'ordinateur |
Le tutoriel, l'aide, le maître et les leçons emploient ces mots, avec un lien
vers leur fiche du glossaire à la première apparition. Fiches à créer :
`jan-interieur.md` (anglais : home board) et `jan-exterieur.md` (anglais :
outer board). « Camp » n'est plus employé dans les textes.
+2 -2
View File
@@ -38,8 +38,8 @@ Ouvert depuis le menu, l'accueil et la fin de partie. Une seule page, sans
3. **Pendant la partie** : interrupteur « Conseil du maître », sous-titre
« Une ligne après chacun de vos coups ».
4. **Barre d'action collée en bas** : un seul bouton principal, qui dit ce
qu'il lance (« Commencer la partie » ou « Commencer le match en 5
points »), et la mention « Vos choix sont gardés pour la prochaine fois. ».
qu'il lance (« Commencer la partie » ou « Commencer le match » ; choix
de Pierre du 2026-11-11, la longueur se lit déjà sur le bouton choisi), et la mention « Vos choix sont gardés pour la prochaine fois. ».
Par défaut : partie libre, Intermédiaire, conseil activé. Fermer (croix en
haut à gauche, ou retour du téléphone) ne change rien à la partie en cours.
+1 -1
View File
@@ -22,7 +22,7 @@ La palette, c'est le bois, les pions et le café, avec **un seul accent : le rou
- `accent-soft` : fond léger pour signaler « c'est à vous », avec du texte `text` dessus.
- Les appréciations de coups ont leurs propres couleurs, distinctes de la brique : `verdict-good` (vert), `verdict-doubtful` (ocre), `verdict-bad` (lie-de-vin). **Toujours avec le mot et l'icône**, jamais la couleur seule.
- `brand-laiton` (via `last-move`) marque le dernier coup joué, comme les charnières en laiton d'un plateau de tavla. Rien d'autre.
- Le plateau : cadre `frame` en noyer, fond `bed`, flèches impaires en `point-a` (noyer clair) et paires en `point-b` (chêne), barre `bar`, sorties `tray`. Le joueur a les pions ivoire (`checker-player`, l'apprenti), l'ordinateur les pions noyer (`checker-computer`, le maître), comme dans le logo.
- Le plateau : cadre `frame` en noyer, fond `bed`, flèches impaires en `point-a` (noyer clair) et paires en `point-b` (chêne), barre `bar`, sorties `tray`. Le joueur a les pions blancs (`checker-player`), l'ordinateur les pions noirs (`checker-computer`), chacun avec un contour et un filet (`checker-player-edge`, `checker-computer-edge`) qui le détachent de tous les fonds ; les pions du logo restent ivoire et noyer (ADR 0010, amendement du 2026-11-11).
- **À éviter** : le vert feutre de casino, le noir et or, les dégradés, les néons.
Les deux thèmes, clair et sombre, sont complets. Tous les textes atteignent 4,5:1 sur leurs fonds dans les deux thèmes ; les contours et marques porteuses de sens, 3:1.
+2
View File
@@ -8,6 +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.
- [ ] 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).
+32 -32
View File
@@ -216,96 +216,96 @@
{
"name": "checker-player",
"value": {
"light": "#F7F0E2",
"dark": "#EDE3CF"
"light": "#FAFAF7",
"dark": "#F2F2EF"
},
"usage": "Pions du joueur : ivoire, l'apprenti."
"usage": "Pions du joueur : blancs."
},
{
"name": "checker-player-edge",
"value": {
"light": "#8C7A62",
"dark": "#8E7C64"
"light": "#6E6E69",
"dark": "#6E6E69"
},
"usage": "Contour et filet des pions du joueur."
"usage": "Contour et filet des pions blancs : gris foncé, pour qu'ils se détachent du fond clair du plateau."
},
{
"name": "checker-computer",
"value": {
"light": "#2E1C12",
"dark": "#140D09"
"light": "#1C1C1C",
"dark": "#141414"
},
"usage": "Pions de l'ordinateur : noyer sombre, le maître."
"usage": "Pions de l'ordinateur : noirs."
},
{
"name": "checker-computer-edge",
"value": {
"light": "#120A05",
"dark": "#A8957D"
"light": "#B5B5B0",
"dark": "#B5B5B0"
},
"usage": "Contour et filet des pions de l'ordinateur ; clair en thème sombre pour rester visible."
"usage": "Contour et filet des pions noirs : gris clair dans les deux thèmes, pour qu'ils se détachent de la barre, des flèches sombres et du fond sombre."
},
{
"name": "count-player",
"value": {
"light": "#2B1D15",
"dark": "#2B1D15"
"light": "#1C1C1C",
"dark": "#1C1C1C"
},
"usage": "Nombre écrit sur une pile de pions du joueur."
"usage": "Nombre écrit sur une pile de pions blancs."
},
{
"name": "count-computer",
"value": {
"light": "#F3EAD8",
"dark": "#F3EAD8"
"light": "#FAFAF7",
"dark": "#FAFAF7"
},
"usage": "Nombre écrit sur une pile de pions de l'ordinateur."
"usage": "Nombre écrit sur une pile de pions noirs."
},
{
"name": "die-player",
"value": {
"light": "#F7F0E2",
"dark": "#EDE3CF"
"light": "#FAFAF7",
"dark": "#F2F2EF"
},
"usage": "Dés du joueur."
"usage": "Dés du joueur, blancs comme ses pions."
},
{
"name": "pip-player",
"value": {
"light": "#2B1D15",
"dark": "#2B1D15"
"light": "#1C1C1C",
"dark": "#1C1C1C"
},
"usage": "Points des dés du joueur."
},
{
"name": "die-computer",
"value": {
"light": "#2E1C12",
"dark": "#140D09"
"light": "#1C1C1C",
"dark": "#141414"
},
"usage": "Dés de l'ordinateur."
"usage": "Dés de l'ordinateur, noirs comme ses pions."
},
{
"name": "pip-computer",
"value": {
"light": "#F3EAD8",
"dark": "#EDE3CF"
"light": "#FAFAF7",
"dark": "#F2F2EF"
},
"usage": "Points des dés de l'ordinateur."
},
{
"name": "cube",
"value": {
"light": "#F7F0E2",
"dark": "#EDE3CF"
"light": "#FAFAF7",
"dark": "#F2F2EF"
},
"usage": "Face du videau."
},
{
"name": "cube-text",
"value": {
"light": "#2B1D15",
"dark": "#2B1D15"
"light": "#1C1C1C",
"dark": "#1C1C1C"
},
"usage": "Valeur du videau (2, 4, 8…)."
},
+3 -1
View File
@@ -4,7 +4,9 @@ Fonctionnalité: Choisir sa partie et jouer un match
L'écran « Nouvelle partie » s'ouvre depuis le menu, l'accueil et, à la
fin d'une partie, par « Changer les réglages ». Vous y choisissez une partie libre ou un match en 3, 5, 7
ou 11 points, le niveau de l'ordinateur, et si le maître commente vos
coups. Vos choix sont gardés pour la prochaine fois.
coups. Vos choix sont gardés pour la prochaine fois. « Commencer la
partie » (ou « Commencer le match ») lance aussitôt la partie, sur le
plateau, même juste après le bilan d'une partie finie.
Scénario: Comment choisir le niveau de l'ordinateur ?
Six niveaux, du Débutant, qui laisse passer des occasions, au Champion,
+16 -4
View File
@@ -12,10 +12,13 @@
# build/browser-shots/maitre/. Le bilan de fin de partie et l'écran
# d'analyse (tests/browser/bilan.mjs), captures dans
# build/browser-shots/bilan/. « Demander à une IA » (tests/browser/prompts.mjs),
# captures dans build/browser-shots/prompts/.
# 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/.
#
# BROWSERS=chromium et SCRIPTS=accueil (ou play, match, maitre, bilan,
# prompts) restreignent le test.
# prompts, nouvelle-partie) 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
@@ -28,7 +31,7 @@ cd "$(dirname "$0")/.."
image="${PLAYWRIGHT_IMAGE:?lancer via make test-browser}"
browsers="${BROWSERS:-chromium firefox}"
# Les scripts à lancer (tests/browser/<nom>.mjs), tous par défaut.
scripts="${SCRIPTS:-play accueil match maitre bilan prompts}"
scripts="${SCRIPTS:-play accueil match maitre bilan prompts nouvelle-partie}"
unset MAKEFLAGS MFLAGS MAKELEVEL
make -s package >/dev/null
@@ -66,7 +69,8 @@ update_port="$(docker port "$update_container" 80/tcp | head -n1 | sed 's/.*://'
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 "$tools/"
tests/browser/maitre.mjs tests/browser/bilan.mjs tests/browser/prompts.mjs tests/browser/nouvelle-partie.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
@@ -113,5 +117,13 @@ for browser in $browsers; do
-v "$PWD/$tools:/tools" -v "$PWD/build/browser-shots:/shots" -w /tools "$image" \
node prompts.mjs "http://localhost:$port/" "$browser" /shots || status=1
fi
if [[ " $scripts " == *" nouvelle-partie "* ]]; then
# « Nouvelle partie » après une partie finie et son bilan : la
# nouvelle partie tout de suite ; captures dans
# build/browser-shots/nouvelle-partie/.
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 nouvelle-partie.mjs "http://localhost:$port/" "$browser" /shots || status=1
fi
done
exit "$status"
+9
View File
@@ -184,6 +184,15 @@ await until("feuille du maître", () => document.getElementById("explain-mine")
await page.click("#explain-mine");
const sheetLegend = await page.textContent("#explain-legend");
check(sheetLegend.includes(" 6/off,"), `feuille du maître : ${sheetLegend}`);
// Le coup y est un lien vers la fiche « Notation des coups ».
const notationLink = await page.evaluate(() => {
const a = document.querySelector("#explain-legend a.notation");
return a === null ? null : { text: a.textContent, href: a.getAttribute("href") };
});
check(
notationLink?.text === "6/off" && notationLink.href === "#/glossaire/notation-des-coups",
`feuille du maître, lien de la notation : ${JSON.stringify(notationLink)}`,
);
await page.click("#explain-done");
await until("feuille fermée", () => document.getElementById("explain-veil").hidden);
// Le joueur passe.
+2 -2
View File
@@ -194,7 +194,7 @@ await page.click("#new-game-match");
await page.click("#new-game-length-3");
await page.click("#new-game-level-beginner");
s = await screen();
check(s.match === "true" && s.lengthsShown && !s.jacobyShown && s.start === "Commencer le match en 3 points",
check(s.match === "true" && s.lengthsShown && !s.jacobyShown && s.start === "Commencer le match",
`match en 3 points : ${JSON.stringify(s)}`);
check(s.level === "beginner" && s.reached === 1 && s.name === "Débutant", `Débutant : ${JSON.stringify(s)}`);
check((await page.getAttribute("#new-game-crawford a", "href")) === "#/glossaire/regle-de-crawford",
@@ -254,7 +254,7 @@ await page.click("#menu-new-game");
await until("écran « Nouvelle partie »", () => location.hash === "#/nouvelle-partie" &&
!document.getElementById("new-game").hidden);
s = await screen();
check(s.match === "true" && s.level === "beginner" && s.start === "Commencer le match en 3 points",
check(s.match === "true" && s.level === "beginner" && s.start === "Commencer le match",
`choix retrouvés : ${JSON.stringify(s)}`);
await page.click("#new-game-start");
await until("match commencé", () => location.hash === "#/" && !document.body.classList.contains("busy"));
+321
View File
@@ -0,0 +1,321 @@
// nouvelle-partie.mjs -- après une partie finie et son bilan, « Nouvelle
// partie » mène tout droit à la nouvelle partie, sur le plateau, avec les
// réglages choisis, par chaque entrée vers #/nouvelle-partie : l'accueil
// (l'application rouverte), le menu, « Changer les réglages » du bilan.
// Dans un vrai navigateur sans écran (Playwright), sur le site servi par
// l'image de production.
//
// Copyright (C) 2026 les auteurs d'Ustagammon.
// Licence : GPLv3 ou ultérieure (voir LICENSE).
//
// Usage : node nouvelle-partie.mjs <adresse du site> <chromium|firefox> <dossier des captures>
// Lancé par tests/browser.sh. Défaut signalé par un joueur : « tu fais
// nouvelle partie […] tu te retrouves sur le résultat du match précédent et
// tu dois cliquer à nouveau sur nouvelle partie ». Chaque partie finie est
// une fin de course courte, partie d'un lien de position avec des dés
// imposés, comme dans bilan.mjs. Vérifie aussi le libellé du bouton
// (« Commencer la partie », « Commencer le match ») et que le coup de
// l'ordinateur, sur la ligne d'information, mène à la fiche « Notation des
// coups ». Captures (Chromium) dans <dossier des captures>/nouvelle-partie/,
// en 412×915 et 915×412.
import { mkdirSync } from "node:fs";
import { chromium, firefox } from "playwright";
import { 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 nouvelle-partie.mjs <adresse> <chromium|firefox> <dossier des captures>");
process.exit(2);
}
const full = browserName === "chromium";
const dir = `${shots}/nouvelle-partie`;
mkdirSync(dir, { recursive: true });
const PORTRAIT = { width: 412, height: 915 };
const LANDSCAPE = { width: 915, height: 412 };
// Le joueur au trait : un pion sur 6, un sur 1 ; l'ordinateur : un sur 1,
// un sur 2. Le joueur joue 6/5/off, l'ordinateur double, le joueur passe.
const BEAROFF = "BQAACAIAAAAAAA";
const problems = [];
const log = (text) => console.log(`[${browserName}] ${text}`);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
let captures = 0;
function check(condition, message) {
if (!condition) {
throw new Error(message);
}
}
const browser = await browserType.launch();
const context = await browser.newContext({
viewport: PORTRAIT,
deviceScaleFactor: 1,
locale: "fr-FR",
reducedMotion: "reduce",
});
await context.addInitScript(() => {
// Les dés imposés d'abord (un dé par valeur), puis une graine fixe.
window.__dice = [];
let state = 7;
Math.random = () => {
const die = window.__dice.shift();
if (die !== undefined) {
return (die - 0.5) / 6;
}
state = (state * 1103515245 + 12345) % 2147483648;
return state / 2147483648;
};
window.__csp = [];
document.addEventListener("securitypolicyviolation", (e) =>
window.__csp.push(`${e.effectiveDirective} ${e.blockedURI}`),
);
});
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()}`);
}
});
/** 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(what, fn, arg, ms = 120_000) {
for (let waited = 0; waited < ms; waited += 100) {
if (await page.evaluate(fn, arg)) {
return;
}
await sleep(100);
}
await page.screenshot({ path: `${dir}/${browserName}-bloque.png` }).catch(() => {});
throw new Error(`attente trop longue : ${what} (adresse ${await page.evaluate(() => location.hash)})`);
}
const idle = () => until("écran libre", () => !document.body.classList.contains("busy"));
/** Une capture en portrait et en paysage (Chromium seulement), après la
* règle de la recette : rien d'interactif sous une barre d'action. */
async function capture(name) {
if (!full) {
return;
}
for (const size of [PORTRAIT, LANDSCAPE]) {
await page.setViewportSize(size);
await sleep(300);
const hidden = await hiddenUnderActionBar(page);
check(
hidden.length === 0,
`${name} ${size.width}x${size.height} : caché sous la barre d'action : ${hidden.join(", ")}`,
);
await page.screenshot({ path: `${dir}/${name}-${size.width}x${size.height}.png` });
captures++;
}
await page.setViewportSize(PORTRAIT);
await sleep(300);
}
/** Une partie finie et son bilan à l'écran : une fin de course depuis un
* lien de position, 6/5/off, puis le double de l'ordinateur passé. Renvoie
* l'identifiant de la partie. */
async function finishedGameWithSummary() {
await page.goto(`${base}#/position/${encodeURIComponent(BEAROFF)}`);
await until("position lue", () => {
const play = document.getElementById("position-play");
return !play.hidden && !play.disabled;
});
await page.click("#position-play");
await until(
"partie commencée",
() =>
location.hash === "#/" &&
!document.body.classList.contains("busy") &&
!document.getElementById("roll").hidden,
);
await page.evaluate(() => window.__dice.push(6, 1));
await page.click("#roll");
await idle();
for (const [from, to] of [
["p6", "p5"],
["p5", "off-player"],
]) {
await page.click(`#board .hit[data-place="${from}"]`);
await idle();
const land = page.locator(`#board .hit.can-land[data-place="${to}"]`);
if ((await land.count()) > 0) {
await land.click();
await idle();
}
}
await page.click("#confirm");
await until("double de l'ordinateur", () => !document.getElementById("drop").hidden);
await page.click("#drop");
await until(
"bilan de la partie finie",
() =>
/^#\/parties\/[a-z0-9-]+\/bilan$/.test(location.hash) &&
document.getElementById("summary")?.dataset.ready === "true",
);
return page.evaluate(() => location.hash.split("/")[2]);
}
/** Choisit les réglages sur l'écran « Nouvelle partie », vérifie le libellé
* du bouton, puis commence. */
async function chooseAndStart(choice, shot) {
await until(
"écran « Nouvelle partie »",
() =>
location.hash === "#/nouvelle-partie" &&
!document.getElementById("new-game").hidden &&
!document.getElementById("new-game-start").disabled,
);
await page.click(choice.match ? "#new-game-match" : "#new-game-free");
if (choice.match) {
await page.click(`#new-game-length-${choice.length}`);
}
await page.click(`#new-game-level-${choice.level}`);
const label = await page.textContent("#new-game-start");
const expected = choice.match ? "Commencer le match" : "Commencer la partie";
check(label === expected, `bouton : « ${label} », attendu « ${expected} »`);
if (shot) {
await capture("1-nouvelle-partie");
}
await page.click("#new-game-start");
}
/** La nouvelle partie est à l'écran, sur le plateau, avec les réglages
* choisis ; ni le bilan ni le panneau de fin de l'ancienne partie, même un
* peu plus tard. */
async function checkNewGame(entry, oldId, choice) {
await until(
`${entry} : partie commencée`,
() => location.hash === "#/" && !document.body.classList.contains("busy"),
);
// Le défaut apparaissait juste après : l'adresse passait au bilan de
// l'ancienne partie. On regarde pendant deux secondes.
for (let waited = 0; waited < 2000; waited += 100) {
const s = await page.evaluate(() => ({
hash: location.hash,
summary: !document.getElementById("summary-veil").hidden,
panel: !document.getElementById("panel-veil").hidden,
board: !document.getElementById("game").hidden,
newGame: !document.getElementById("new-game").hidden,
}));
check(
s.hash === "#/" && !s.summary && !s.panel && s.board && !s.newGame,
`${entry} : la nouvelle partie n'est pas à l'écran (ancienne partie ${oldId}) : ${JSON.stringify(s)}`,
);
await sleep(100);
}
await idle();
const shown = await page.evaluate(() => ({
line: document.getElementById("match-line").textContent,
lineShown: !document.getElementById("match-line").hidden,
level: JSON.parse(localStorage.getItem("ustagammon.settings") ?? "{}").level,
over: document.getElementById("again").hidden === false,
}));
check(!shown.over, `${entry} : la partie à l'écran est finie`);
check(shown.level === choice.level, `${entry} : niveau ${shown.level}`);
if (choice.match) {
check(
shown.lineShown && shown.line === `Match en ${choice.length} points`,
`${entry} : ligne du match « ${shown.line} »`,
);
} else {
check(!shown.lineShown, `${entry} : un match au lieu d'une partie libre`);
}
log(`${entry} : nouvelle partie directe (${choice.match ? `match en ${choice.length}` : "partie libre"}, ${choice.level})`);
}
// --------------------------- 1. L'application rouverte, puis l'accueil
let oldId = await finishedGameWithSummary();
// L'application rouverte : sans adresse, elle ouvre l'accueil.
await page.goto(base);
await until(
"accueil",
() =>
location.hash === "#/accueil" &&
!document.getElementById("home-new").hidden &&
!document.getElementById("home-new").disabled,
);
await page.click("#home-new");
let choice = { match: true, length: 3, level: "beginner" };
await chooseAndStart(choice, true);
await checkNewGame("accueil", oldId, choice);
await capture("2-partie-demarree");
// Le coup de l'ordinateur, sur la ligne d'information, mène à la fiche
// « Notation des coups ». On joue jusqu'à ce qu'il ait joué un coup.
for (let actions = 0; ; actions++) {
check(actions < 200, "l'ordinateur ne joue pas de coup");
const link = await page.evaluate(() => document.querySelector("#info a.notation")?.href ?? null);
if (link !== null) {
check(link.endsWith("#/glossaire/notation-des-coups"), `lien de la notation : ${link}`);
break;
}
const what = await page.evaluate(() => {
const usable = (id) => {
const b = document.getElementById(id);
return !b.hidden && !b.disabled;
};
for (const id of ["take", "skip", "confirm", "roll"]) {
if (usable(id)) {
return `#${id}`;
}
}
if (document.querySelector("#board .hit.can-land")) {
return "#board .hit.can-land";
}
return document.querySelector("#board .hit.can-move") ? "#board .hit.can-move" : null;
});
if (what === null) {
await sleep(100);
continue;
}
await page.locator(what).first().click();
await idle();
}
await page.click("#info a.notation");
await until(
"fiche « Notation des coups »",
() =>
location.hash === "#/glossaire/notation-des-coups" &&
document.getElementById("glossaire-notation-des-coups") !== null,
);
await page.goBack();
await until("retour à la partie", () => location.hash === "#/");
log("ligne de l'ordinateur : la notation mène à sa fiche");
// --------------------------------- 2. L'application rouverte sur le menu
oldId = await finishedGameWithSummary();
await page.goto(`${base}#/menu`);
await until("menu", () => !document.getElementById("menu-veil").hidden);
await page.click("#menu-new-game");
choice = { match: false, level: "amateur" };
await chooseAndStart(choice, false);
await checkNewGame("menu", oldId, choice);
// ------------------------------- 3. « Changer les réglages » du bilan
oldId = await finishedGameWithSummary();
await page.click("#summary-settings");
choice = { match: true, length: 5, level: "intermediate" };
await chooseAndStart(choice, false);
await checkNewGame("bilan, « Changer les réglages »", oldId, choice);
const csp = await page.evaluate(() => window.__csp);
for (const violation of csp) {
problems.push(`politique de sécurité : ${violation}`);
}
await browser.close();
if (problems.length > 0) {
for (const problem of problems) {
console.error(`[${browserName}] ${problem}`);
}
process.exit(1);
}
log(`nouvelle partie : tout est conforme (${captures} captures)`);
+13
View File
@@ -586,6 +586,19 @@ button:disabled {
line-clamp: 2;
}
/* Un coup en notation du moteur, lien vers sa fiche du glossaire (ligne de
* l'ordinateur, feuille du maître) : couleur du texte, soulignement en
* pointillé discret. Le remplissage vertical d'un élément en ligne agrandit
* la zone d'appui sans changer la hauteur des lignes. */
a.notation {
padding-block: 8px;
color: inherit;
text-decoration-line: underline;
text-decoration-style: dotted;
text-decoration-color: var(--muted);
text-decoration-thickness: 1px;
}
.infoline .replay {
color: var(--accent);
}