Compare commits

..
Author SHA1 Message Date
Pierre MARTINandClaude Opus 5.5 8d1eba9e35 Plan de test de l'étape 1 (1j)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 19:04:47 +01:00
Pierre MARTINandClaude Opus 5.5 ac27625e67 Fusion de la MR !61 : cinq premières leçons, progression et carte de l'accueil (1g)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 14:23:51 +01:00
Pierre MARTINandClaude Opus 5.5 73f4cfce51 Leçon 4 : une seule phrase pour le pion frappé (recette)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 14:23:48 +01:00
Pierre MARTINandClaude Opus 5.5 351482fa9e Recette 1g : textes corrigés, boutons en bas, barre haute en paysage
Leçon 1 §2 (flèche 7 vide, 5 déjà bloqué), leçon 2 « Faire des points »,
flèches 7 à 12, reformulations des leçons 3 et 4. Écran des leçons et du
tutoriel : « Retour » et « Continuer » en bas en portrait, « Revoir » sur
la ligne du titre, ligne « Leçon n · … » gardée en paysage. Barre haute
visible hors de l'écran de jeu en paysage (défaut antérieur à 1g), avec
un contrôle dans la recette automatique. Rappel des règles haut de
44 px ; « Aucune leçon terminée sur 5 · … » sur l'accueil.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 13:55:17 +01:00
Pierre MARTIN 5d0f7a9616 Merge remote-tracking branch 'origin/main' into lecons 2026-11-12 13:41:31 +01:00
pierre ed6d0a7140 Merge pull request 'Charte des leçons : écarts de la recette 1g' (#62) from charte-lecons-quitter into main 2026-11-12 12:40:48 +00:00
Pierre MARTINandClaude Opus 5.5 217c40a245 Charte des leçons : « Quitter », leçon terminée, actions en bas, « Revoir » sur la ligne du titre
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 13:34:18 +01:00
18 changed files with 314 additions and 52 deletions
+3 -1
View File
@@ -41,7 +41,9 @@ export const HOME_TEXTS = {
rulesContinue: (step: number) => `Reprendre à l'étape ${step}`,
rulesAgain: "Revoir",
progress: (played: number, won: number) =>
`${plural(played, "partie jouée", "parties jouées")} · ${plural(won, "gagnée", "gagnées")}`,
played === 0
? "aucune partie jouée"
: `${plural(played, "partie jouée", "parties jouées")} · ${plural(won, "gagnée", "gagnées")}`,
footer: "Né à l'Atelier du Huit, à Cugnaux. Logiciel libre, sans publicité ni mise.",
// Installation
+3 -2
View File
@@ -290,10 +290,11 @@ export function createHomeView(options: HomeViewOptions): HomeView {
if (state.lessons !== null) {
parts.push(LESSON_TEXTS.homeSummary(state.lessons.finished, state.lessons.total));
}
if (state.progress !== null && state.progress.played > 0) {
if (state.progress !== null) {
parts.push(HOME_TEXTS.progress(state.progress.played, state.progress.won));
}
progress.textContent = parts.join(" · ");
const line = parts.join(" · ");
progress.textContent = line.charAt(0).toUpperCase() + line.slice(1);
},
};
}
+3 -1
View File
@@ -53,5 +53,7 @@ export const LESSON_TEXTS = {
homeAllDoneDetail: "Vous pouvez les revoir quand vous voulez.",
allLessons: "Toutes les leçons",
homeSummary: (finished: number, total: number) =>
`${plural(finished, "leçon terminée", "leçons terminées")} sur ${total}`,
finished === 0
? `Aucune leçon terminée sur ${total}`
: `${plural(finished, "leçon terminée", "leçons terminées")} sur ${total}`,
} as const;
+4 -3
View File
@@ -86,9 +86,10 @@ export function createLessonsView(section: HTMLElement): LessonsView {
const head = element(section, "div", { className: "lessons-intro" });
element(head, "h2", { text: LESSON_TEXTS.listTitle });
element(head, "p", { className: "muted", text: LESSON_TEXTS.listLead });
const rules = element(section, "p", { className: "lessons-rules caption" });
rules.append(`${LESSON_TEXTS.rulesReminder} · `);
const review = element(rules, "a", { id: "lessons-rules", text: LESSON_TEXTS.rulesReview });
// Le rappel des règles : toute la ligne est le lien, haute de 44 px.
const review = element(section, "a", { id: "lessons-rules", className: "lessons-rules" });
review.append(`${LESSON_TEXTS.rulesReminder} · `);
element(review, "span", { text: LESSON_TEXTS.rulesReview });
review.href = "#/tutoriel/1";
review.setAttribute("aria-label", LESSON_TEXTS.rulesReviewLabel);
const status = element(section, "p", { id: "lessons-status", className: "muted" });
+4 -2
View File
@@ -287,14 +287,16 @@ export function createTutorialView(options: TutorialViewOptions): TutorialView {
skip.textContent = course.texts.quit;
skip.setAttribute("aria-label", course.texts.quitLabel);
const overline = element(section, "p", { id: idOf("overline"), className: "overline" });
const title = element(section, "h2", { id: idOf("title"), className: "tutorial-title" });
// Le titre, et « Revoir » à sa droite (en portrait comme en paysage).
const titleRow = element(section, "div", { className: "tutorial-title-row" });
const title = element(titleRow, "h2", { id: idOf("title"), className: "tutorial-title" });
const boardBox = element(section, "div", { className: "tutorial-board-box" });
const svg = document.createElementNS(SVG_NS, "svg");
svg.id = idOf("board");
svg.setAttribute("class", "board tutorial-board");
svg.setAttribute("role", "img");
boardBox.append(svg);
const replay = element(boardBox, "button", {
const replay = element(titleRow, "button", {
id: idOf("replay"),
className: "icon-button tutorial-replay",
});
+4 -2
View File
@@ -106,7 +106,7 @@ test("cinq leçons, de deux à quatre exercices chacune, avec un titre et une fi
lessons.map((l) => l.title),
[
"Les ouvertures",
"Construire ses cases",
"Faire des points",
"Frapper ou ne pas frapper",
"La course et le compte de points",
"Sortir sans se faire frapper",
@@ -217,7 +217,9 @@ test("chaque coup attendu est le meilleur coup du moteur à 2 plis, avec plus de
assert.equal(report.length, 16);
});
test("chaque coup illustré est le meilleur coup du moteur à 2 plis", async () => {
// Couvre aussi le « nettement devant tous les autres » de la leçon 1 (étape
// 1) : chaque coup illustré a plus de MIN_GAP d'avance sur le deuxième.
test("chaque coup illustré est le meilleur coup du moteur à 2 plis, avec plus de 0,04 d'avance", async () => {
const engine = await loadNodeEngine();
let illustrated = 0;
for (const [name, screen] of allScreens()) {
+1 -1
View File
@@ -26,7 +26,7 @@ Repris du cadrage (modules, écrans clés, IA générative en V1) et des ADR 001
| 1g. Accueil, tutoriel et leçons (fait) | Accueil (reprendre, nouvelle partie, prochaine leçon, progression résumée), tutoriel des règles interactif et illustré (0e.5), cinq premières leçons numérotées (ADR 0011) avec positions et « à vous de jouer », progression locale ; contenus CC BY-SA 4.0 livrés sans relecture préalable, sous les garde-fous du moteur (décision du 2026-11-09) | 0e.5, 1a |
| 1h. Progression | Historique des parties et courbe du taux d'erreur | 1b, 1e |
| 1i. Plateau soigné | Dessin du plateau aligné sur la charte (`docs/identite/charte/board.js`), animations des pions, glisser-déposer en plus du toucher, double toucher pour le seul coup évident | 0e.3 |
| 1j. Plan de test | `docs/guide-test-etape-1.md` : parcours du débutant et du joueur de club sur le téléphone, avec ce qu'il faut rapporter | toutes |
| 1j. Plan de test (fait) | `docs/guide-test-etape-1.md` : parcours du débutant et du joueur de club sur le téléphone, avec ce qu'il faut rapporter | toutes |
## Orientation donnée par Pierre
+154
View File
@@ -0,0 +1,154 @@
# Guide de test de l'étape 1 : « Jouer et comprendre »
Document temporaire : il sert à la vérification de l'étape 1 avec Pierre, sur
le Fairphone 4, et sera supprimé à la clôture de l'étape (ce qui en reste de
durable va dans le journal).
**Critère de fin de l'étape** (cadrage) : un débutant apprend et joue seul ; un
joueur de club comprend ses erreurs.
Le guide suit deux parcours : celui du **débutant** (parties A à C) et celui du
**joueur de club** (parties D à F). Une partie G regroupe ce qui est commun
(installation, hors ligne, mise à jour, affichage). Comptez environ une heure
pour l'ensemble, en plusieurs fois si besoin : l'application reprend là où
vous en étiez.
Pour chaque point : cochez ce qui marche, et notez ce qui coince avec le
numéro du point (par exemple « C4 : le bilan s'ouvre deux fois »). Une capture
d'écran aide beaucoup.
## Avant de commencer
- Adresse : https://ustagammon.sans.pub, dans Chrome sur le Fairphone 4.
- Version : le menu, puis « Aide et glossaire », onglet « À propos », donne le
numéro de version (un commit). Notez-le en tête de vos retours.
- Pour repartir de zéro (comme un nouveau joueur) : dans Chrome, Paramètres du
site pour ustagammon.sans.pub, puis « Effacer et réinitialiser ». Toutes les
parties et la progression sont alors perdues.
## A. Premier contact (débutant)
1. Ouvrez l'application après l'avoir réinitialisée. L'écran de premier
lancement propose « Je découvre » et « Je connais les règles ».
2. Touchez « Je découvre » : le tutoriel des règles s'ouvre à l'étape 1 sur 8.
3. Suivez les huit étapes jusqu'au bout. À chaque écran « à vous » : jouez
d'abord un mauvais coup (le pion revient, une phrase explique sans
reproche), puis le bon (coche, « Continuer »).
4. Étape 2 : deux flèches circulaires montrent le sens de chaque camp, sous
les pions.
5. Le texte est-il clair pour quelqu'un qui n'a jamais joué ? Notez les mots
ou phrases qui bloquent. Les mots soulignés ouvrent leur fiche du glossaire.
6. À la fin, l'accueil propose la suite (partie, leçons).
**À rapporter** : une étape incompréhensible, un coup juste refusé ou un coup
faux accepté, un texte coupé ou qui déborde.
## B. Première partie (débutant)
1. Depuis l'accueil, « Nouvelle partie » : choisissez « Partie libre » et le
niveau « Débutant », conseil du maître activé, puis « Commencer la partie ».
2. Touchez les dés pour les lancer. Touchez un pion : s'il n'a qu'une case
d'arrivée, il y va tout seul ; sinon, ses cases d'arrivée s'allument.
3. Essayez aussi de glisser un pion avec le doigt jusqu'à sa case ; lâché
ailleurs, il revient à sa place.
4. Un double toucher sur un pion joue le coup évident s'il n'y en a qu'un.
5. Avant de valider, « Annuler » reprend le dernier pion, un à la fois. Puis
validez en touchant les dés ou « Jouer ce coup ».
6. Après votre coup, une ligne discrète du maître apparaît (« Bon coup. »,
« Erreur. J'aurais joué… »). Touchez-la : la feuille montre votre coup et
celui du maître, joués pion par pion.
7. Regardez le coup de l'ordinateur : ses pions glissent, des traces restent,
« Revoir » le rejoue. Touchez la notation du coup : la fiche « Notation des
coups » s'ouvre.
8. Quand aucun coup n'est possible, les dés sont barrés : touchez-les pour
passer.
9. Jouez la partie jusqu'au bout.
**À rapporter** : un pion qui ne bouge pas comme prévu, un geste raté, une
phrase du maître fausse ou blessante, la durée d'un tour de l'ordinateur si
elle dépasse 2 s.
## C. Bilan, leçons et retour (débutant)
1. En fin de partie, le bilan s'affiche : l'issue, ce qui a fait la
différence, vos décisions comptées, votre jeu et vos dés, le coup à revoir.
2. « Rejouer » relance une partie avec les mêmes réglages ; « Changer les
réglages » ouvre l'écran « Nouvelle partie ».
3. Depuis l'accueil, ouvrez « Toutes les leçons » puis la leçon 1. Suivez-la,
en faisant au moins un exercice faux puis juste.
4. Revenez à la liste : la leçon affiche « k exercices sur n ». Fermez
l'application, rouvrez-la : la progression est gardée.
5. Faites les cinq leçons si vous avez le temps : chaque réponse attendue est
le meilleur coup du moteur.
**À rapporter** : une explication de leçon qui vous semble fausse ou trop
difficile pour un débutant, un exercice où le refus ne met pas sur la voie.
## D. Match et niveaux (joueur de club)
1. « Nouvelle partie » : choisissez « Match », 3 points, niveau « Avancé »,
puis « Commencer le match ».
2. Le score du match s'affiche (« Match en 3 points ») ; à 2 points d'un
joueur, la partie Crawford est signalée et le videau disparaît.
3. Doublez au moins une fois (le videau demande confirmation). Répondez au
moins une fois à un double de l'ordinateur (Prendre ou Passer).
4. Entre deux parties, « Partie suivante » ; à la fin du match, le score final.
5. Le niveau de l'ordinateur paraît-il juste ? « Champion » est le plus fort.
**À rapporter** : une décision de videau de l'ordinateur qui vous surprend
(notez la position avec « Copier le lien de la position » dans le menu), un
score de match faux.
## E. Comprendre ses erreurs (joueur de club)
1. Après une partie, depuis le bilan, « Voir l'analyse ».
2. La courbe de vos chances de gagner se parcourt du doigt ; toucher un point
ouvre le coup.
3. Le visualiseur montre « Votre coup » et « Le coup du maître ». Les chiffres
du moteur sont repliés en dessous.
4. Filtrez la liste des coups par appréciation (erreurs, grosses erreurs).
5. Sur un coup, « Rejouer d'ici » : une branche part de ce coup, la partie
d'origine reste dans « Mes parties ».
6. Sur une erreur, « Demander à une IA » : le prompt s'affiche, « Copier le
prompt » confirme par « ✓ Copié ». Collez-le dans l'IA de votre choix et
jugez la réponse.
7. Ouvrez la progression (la ligne de l'accueil) après trois parties : la
courbe du taux d'erreur, la liste des parties, « Créer des exercices ».
8. « Mes parties » (menu) : reprenez une ancienne partie, exportez-en une en
SGF et ouvrez-la dans GNU Backgammon de bureau (`load game fichier.sgf`).
**À rapporter** : une analyse qui contredit ce que vous pensiez (c'est peut-être
juste, mais notez-la), un prompt incomplet, une partie absente de la liste.
## F. Liens et positions (joueur de club)
1. Dans le menu, « Copier le lien de la position ». Ouvrez ce lien dans un
nouvel onglet : la même position s'affiche, avec « Jouer à partir d'ici ».
2. L'aide, une question de l'aide, un terme du glossaire, la progression : chaque
écran a son adresse ; le bouton retour du téléphone ferme d'abord la feuille
ouverte, puis revient à l'écran précédent.
## G. Commun : installation, hors ligne, affichage
1. Après une partie terminée, l'accueil propose « Installer l'application » :
installez-la, puis ouvrez-la depuis l'écran d'accueil du téléphone.
2. Mode avion : l'application s'ouvre et se joue normalement.
3. Tournez le téléphone : en paysage, le plateau prend l'écran, la partie
continue sans perte. Essayez « Plein écran » dans le menu.
4. Menu, thème clair puis sombre : les pions blancs et noirs se distinguent
bien partout, y compris sur la barre.
5. Vibrations : pion posé, pion frappé, double de l'ordinateur, fin de partie.
Coupez-les dans le menu.
6. Mise à jour : à la prochaine publication, le bandeau « Nouvelle version
prête » apparaît en bas ; « Mettre à jour » vous ramène à votre partie.
7. Performances : notez si l'application ralentit, chauffe ou se recharge
seule (elle fait tourner deux moteurs : un pour jouer, un pour analyser).
## Ce qu'il faut rapporter
- Le numéro de version (onglet « À propos »).
- Pour chaque problème : le numéro du point, ce que vous avez fait, ce que vous
attendiez, ce qui s'est passé, et si possible une capture ou le lien de la
position.
- Votre avis sur les deux critères : un débutant peut-il apprendre et jouer
seul ? Un joueur de club comprend-il ses erreurs ?
+12 -2
View File
@@ -236,8 +236,18 @@ l'ordre, chacune avec une pastille ronde à son numéro :
Toutes les leçons restent ouvertes : l'ordre est conseillé, jamais imposé.
Les écrans d'une leçon suivent la forme du tutoriel (barre de progression,
« Passer », « Retour », « Continuer », écrans « à vous » contrôlés par le
moteur).
« Quitter », « Retour », « Continuer », écrans « à vous » contrôlés par le
moteur), aux adresses `#/lecons/<n>/<écran>`, `…/a-vous` et `…/fin`.
- Une leçon est **terminée** quand tous ses exercices sont réussis.
- « Retour » et « Continuer » restent **en bas de l'écran**, sous le pouce,
comme dans le tutoriel, quelle que soit la longueur du texte.
- « Revoir » (rejouer le coup illustré) se place sur la ligne du titre, à
droite, en portrait comme en paysage : le texte garde toute la largeur.
- En paysage, la colonne de droite garde la ligne `caption` « Leçon 1 · 1 sur
4 · À vous » au-dessus du titre.
- Les titres et les textes n'emploient que les mots du glossaire : « point »
et « flèche », jamais « case ».
## 1h. Progression (`#/progression`)
+2 -2
View File
@@ -27,9 +27,9 @@ Vous avez 4 et 2. Faites un point dans votre jan intérieur.
- position: 4HPwATDgc/ABMA
La flèche 7 ouvre votre [jan extérieur](jan-exterieur.md), juste à côté de la barre. Avec la flèche 6, déjà fermée par vos cinq pions, elle forme deux points côte à côte.
La flèche 7 ouvre votre [jan extérieur](jan-exterieur.md), juste à côté de la barre. Fermée, elle formerait avec la flèche 6, où sont déjà vos cinq pions, deux points côte à côte.
Les deux pions noirs de votre flèche 1 doivent passer par là : depuis la flèche 1, un 5 les mène sur la flèche 6 et un 6 sur la flèche 7.
Les deux pions noirs de votre flèche 1 doivent passer par là : depuis la flèche 1, un 6 les mène sur la flèche 7, et un 5 est déjà bloqué par votre point 6.
### À vous
+3 -3
View File
@@ -1,6 +1,6 @@
# Construire ses cases
# Faire des points
Leçon 2 d'Ustagammon : construire ses cases et faire des points. Texte sous
Leçon 2 d'Ustagammon : construire son jeu en faisant des points. Texte sous
licence CC BY-SA 4.0. Format : voir README.md.
## 1. Deux pions font un point
@@ -27,7 +27,7 @@ Même position, vous avez 6 et 2. Votre pion de la flèche 5 est seul : couvrez-
- position: 4HPwATDQZ+EBMA
Un pion posé sur les flèches 7 à 11, dans votre [jan extérieur](jan-exterieur.md), peut entrer dans votre jan intérieur avec un seul dé.
Un pion posé sur les flèches 7 à 12, dans votre [jan extérieur](jan-exterieur.md), peut entrer dans votre jan intérieur avec un seul dé.
Ici, un pion blanc est seul sur la flèche 5, un autre sur la flèche 9.
+1 -1
View File
@@ -11,7 +11,7 @@ sous licence CC BY-SA 4.0. Format : voir README.md.
Un pion noir est seul sur votre flèche 7 : c'est un [blot](blot.md). Avec 6 et 1, je joue 13/7* 8/7. L'astérisque marque le pion frappé : il part sur la [barre](barre.md).
Mes deux pions restent ensemble sur la flèche 7 : le point est fait, aucun pion noir ne peut plus s'y poser.
Les deux pions blancs restent ensemble sur la flèche 7 : le point est fait, aucun pion noir ne peut plus s'y poser.
### À vous
+1 -1
View File
@@ -57,7 +57,7 @@ Vous avez 5 et 2.
Ici, les pions de l'ordinateur ont 81 flèches à parcourir, les vôtres 110 : il a 29 points d'avance. Son dernier pion est seul sur votre flèche 9.
Frappé, ce pion part sur la barre, et le compte de l'ordinateur remonte : de la flèche 16 de son côté jusqu'à la barre, 9 de plus.
Frappé, ce pion part sur la barre : pour l'ordinateur, il comptait 16 et compte maintenant 25, soit 9 de plus.
### À vous
+2 -2
View File
@@ -1,8 +1,8 @@
# language: fr
Fonctionnalité: Progresser avec les leçons
Cinq leçons courtes, sur le vrai plateau : les ouvertures, construire ses
cases, frapper ou non, la course et le compte de points, sortir sans se
Cinq leçons courtes, sur le vrai plateau : les ouvertures, faire des
points, frapper ou non, la course et le compte de points, sortir sans se
faire frapper. Chacune explique une idée en quelques écrans, puis vous
fait jouer le coup du maître. GNU Backgammon vérifie chaque coup.
+3 -1
View File
@@ -16,7 +16,7 @@
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { chromium, firefox } from "playwright";
import { hiddenUnderActionBar, tutorialWidthProblems, tutorialWidths } from "./recette.mjs";
import { hiddenUnderActionBar, missingTopbar, tutorialWidthProblems, tutorialWidths } from "./recette.mjs";
const [base, browserName, shots, updateBase, updateDir] = process.argv.slice(2);
const browserType = { chromium, firefox }[browserName];
@@ -117,6 +117,8 @@ async function capture(page, name, settle = async () => {}, sizes = SIZES) {
await settle();
const hidden = await hiddenUnderActionBar(page);
check(hidden.length === 0, `${name} ${width}x${height} : caché sous la barre d'action : ${hidden.join(", ")}`);
const topbar = await missingTopbar(page);
check(topbar === null, `${name} : ${topbar}`);
await page.screenshot({ path: `${dir}/${name}-${width}x${height}.png`, fullPage: true });
captures++;
}
+20 -3
View File
@@ -13,7 +13,7 @@
import { mkdirSync } from "node:fs";
import { chromium, firefox } from "playwright";
import { hiddenUnderActionBar, tallButtons } from "./recette.mjs";
import { hiddenUnderActionBar, missingTopbar, tallButtons } from "./recette.mjs";
const [base, browserName, shots] = process.argv.slice(2);
const browserType = { chromium, firefox }[browserName];
@@ -68,6 +68,8 @@ async function capture(page, name, settle = async () => {}) {
await settle();
const hidden = await hiddenUnderActionBar(page);
check(hidden.length === 0, `${name} ${width}x${height} : caché sous la barre d'action : ${hidden.join(", ")}`);
const topbar = await missingTopbar(page);
check(topbar === null, `${name} : ${topbar}`);
const tall = await tallButtons(page);
check(tall.length === 0, `${name} ${width}x${height} : boutons étirés : ${tall.join(", ")}`);
await page.screenshot({ path: `${dir}/${name}-${width}x${height}.png`, fullPage: true });
@@ -155,7 +157,8 @@ try {
await until(page, "carte de la leçon", () => {
const card = document.getElementById("home-lesson");
return location.hash === "#/accueil" && !card.hidden && !card.closest("[hidden]") &&
card.textContent.includes("Leçon 1") && !document.getElementById("home-new").disabled;
card.textContent.includes("Leçon 1") && !document.getElementById("home-new").disabled &&
document.getElementById("home-progress").textContent.includes(" · ");
});
const home = await page.evaluate(() => ({
go: document.getElementById("home-lesson-go").getAttribute("href"),
@@ -170,7 +173,7 @@ try {
}));
check(home.go === "#/lecons/1/1" && home.goText === "Commencer" && home.goClass.includes("secondary") &&
home.all === "#/lecons" && home.title === "Les ouvertures" && home.state === "À commencer · 4 exercices" &&
home.summary.startsWith("0 leçon terminée sur 5") && home.primaries === 1,
home.summary.startsWith("Aucune leçon terminée sur 5 · ") && home.primaries === 1,
`carte de l'accueil : ${JSON.stringify(home)}`);
await capture(page, "01-accueil-carte", async () => {
await page.locator("#home-lesson").scrollIntoViewIfNeeded();
@@ -188,6 +191,8 @@ try {
[...document.querySelectorAll("#lessons a.button:not(.secondary)")].filter((el) => !el.closest("[hidden]")).length);
check(primaries === 1, `liste : ${primaries} actions principales`);
check(await page.getAttribute("#lessons-rules", "href") === "#/tutoriel/1", "liste : rappel des règles");
const rulesHeight = await page.evaluate(() => document.getElementById("lessons-rules").getBoundingClientRect().height);
check(rulesHeight >= 44, `liste : rappel des règles haut de ${rulesHeight} px`);
await capture(page, "02-liste");
log("liste : cinq leçons, la première proposée, une seule action principale, rappel des règles");
@@ -208,6 +213,18 @@ try {
explain.segments === 4 && explain.skip === "#/lecons" && explain.back === "#/lecons" &&
explain.next === "#/lecons/1/1/a-vous" && explain.topbar === "none" &&
explain.glossary.includes("#/glossaire/jan-interieur"), `explication : ${JSON.stringify(explain)}`);
// « Revoir » sur la ligne du titre ; « Retour » et « Continuer » en bas
// de l'écran, sous le pouce (recette 1g).
const layout = await page.evaluate(() => {
const title = document.getElementById("lesson-title").getBoundingClientRect();
const replay = document.getElementById("lesson-replay").getBoundingClientRect();
const next = document.getElementById("lesson-next").getBoundingClientRect();
return { titleTop: title.top, titleBottom: title.bottom, replayTop: replay.top,
replayBottom: replay.bottom, nextBottom: next.bottom, height: innerHeight };
});
check(layout.replayTop < layout.titleBottom && layout.replayBottom > layout.titleTop,
`« Revoir » hors de la ligne du titre : ${JSON.stringify(layout)}`);
check(layout.nextBottom > layout.height - 64, `« Continuer » loin du bas : ${JSON.stringify(layout)}`);
check(await page.locator("#lesson-board .trace-arrow").count() > 0, "le coup illustré n'a pas laissé de traces");
await capture(page, "03-explication");
log("explication : coup illustré 8/5 6/5 joué tout seul, barre de quatre étapes, liens du glossaire");
+21
View File
@@ -99,6 +99,27 @@ export async function hiddenUnderActionBar(target) {
});
}
/**
* La barre haute (Aide, Plein écran, Menu) hors de l'écran de jeu : sur une
* page (accueil, leçons, aide…) qui n'a pas son propre en-tête, elle doit
* rester visible, en paysage comme en portrait ; sans elle, le menu est
* inaccessible (recette 1g). Renvoie un problème décrit, ou null.
*/
export async function missingTopbar(target) {
return target.evaluate(() => {
const root = document.documentElement;
if (!root.classList.contains("page-open") || root.classList.contains("bare")) {
return null;
}
const bar = document.querySelector(".topbar");
const r = bar?.getBoundingClientRect();
const shown = bar !== null && getComputedStyle(bar).display !== "none" && r.height > 0;
return shown
? null
: `barre haute absente sur ${root.dataset.screen} (${innerWidth}x${innerHeight})`;
});
}
/**
* Les largeurs d'un écran du tutoriel, en px CSS : le plateau, la colonne
* de texte, et la largeur de la page (scrollWidth) face à celle de l'écran.
+73 -25
View File
@@ -1201,6 +1201,13 @@ html.landscape.page-open body {
background: var(--background);
}
/* Hors de l'écran de jeu, la barre haute (Aide, Plein écran, Menu) reste
* en paysage : seul le plateau en paysage s'en passe. Les écrans qui ont
* leur propre en-tête (bare) la masquent toujours. */
html.landscape.page-open:not(.bare) .topbar {
display: flex;
}
/* Repli du plein écran : le paysage ne peut pas être verrouillé, l'écran
* entier tourne d'un quart de tour dans le portrait. Les touchers suivent
* la rotation ; le plateau et les fenêtres sont posés dans #screen. */
@@ -1876,17 +1883,20 @@ html.bare .topbar {
.lessons-rules {
display: flex;
align-items: center;
width: fit-content;
min-height: 44px;
margin-top: var(--space-2);
font-size: 15px;
line-height: 20px;
color: var(--muted);
text-decoration: none;
}
.lessons-rules a {
display: inline-flex;
align-items: center;
min-height: 44px;
.lessons-rules span {
margin-left: var(--space-1);
font-weight: 700;
color: var(--accent);
text-decoration: underline;
}
#lessons-status:empty {
@@ -2136,8 +2146,28 @@ html.bare .topbar {
color: var(--muted);
}
.tutorial-title {
/* En portrait, l'écran tient au moins la hauteur de l'écran : « Retour »
* et « Continuer » restent en bas, sous le pouce. */
.tutorial {
display: flex;
flex-direction: column;
box-sizing: border-box;
min-height: 100vh;
min-height: 100dvh;
}
/* Le titre, et « Revoir » sur sa ligne, à droite. */
.tutorial-title-row {
display: flex;
gap: var(--space-2);
align-items: center;
margin: var(--space-1) 0 var(--space-3);
}
.tutorial-title {
flex: 1;
min-width: 0;
margin: 0;
font-size: 24px;
line-height: 30px;
}
@@ -2152,16 +2182,13 @@ html.bare .topbar {
}
}
.tutorial-board-box .tutorial-replay {
position: absolute;
right: var(--space-1);
bottom: calc(-44px - var(--space-1));
.tutorial-replay {
flex: none;
color: var(--accent);
}
.tutorial-text {
margin-top: var(--space-3);
padding-right: 52px;
}
.tutorial-text p {
@@ -2214,7 +2241,8 @@ html.bare .topbar {
display: grid;
grid-template-columns: 1fr 1.4fr;
gap: var(--space-2);
margin-top: var(--space-6);
margin-top: auto;
padding-top: var(--space-6);
}
.tutorial-nav .button {
@@ -2281,16 +2309,46 @@ html.landscape .tutorial-head {
padding-top: 0;
}
html.landscape .tutorial-title {
html.landscape .tutorial-title-row {
grid-row: 2;
margin: 0 0 var(--space-1);
padding-right: calc(44px + var(--space-4));
padding-right: calc(var(--space-2) + env(safe-area-inset-right));
}
html.landscape .tutorial-title {
font-size: 20px;
line-height: 26px;
}
/* La boîte du plateau s'efface : le plateau et « Revoir » se placent
* eux-mêmes dans la grille. */
/* Une leçon garde en paysage sa ligne « Leçon 1 · 1 sur 4 · à vous »
* au-dessus du titre (recette 1g) : une rangée de plus dans la grille. */
html.landscape #lesson {
grid-template-rows: auto auto auto minmax(0, 1fr) auto auto;
}
html.landscape #lesson .overline {
display: block;
grid-row: 2;
}
html.landscape #lesson .tutorial-title-row {
grid-row: 3;
}
html.landscape #lesson .tutorial-body {
grid-row: 4;
}
html.landscape #lesson .tutorial-cube {
grid-row: 5;
}
html.landscape #lesson .tutorial-nav {
grid-row: 6;
}
/* La boîte du plateau s'efface : le plateau se place lui-même dans la
* grille. */
html.landscape .tutorial-board-box {
display: contents;
}
@@ -2305,16 +2363,6 @@ html.landscape .tutorial-board {
background: var(--frame);
}
html.landscape .tutorial-board-box .tutorial-replay {
position: static;
grid-row: 2;
grid-column: 2;
justify-self: end;
align-self: start;
margin: -9px calc(var(--space-2) + env(safe-area-inset-right)) 0 0;
padding: 0;
}
html.landscape .tutorial-body {
grid-row: 3;
min-height: 0;