Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2dcfcf2baf | ||
|
|
f3555d0f11 | ||
|
|
b27ca76191 | ||
|
|
2d787b2c0b | ||
|
|
368d3c9e8f | ||
|
|
ac27625e67 | ||
|
|
73f4cfce51 | ||
|
|
351482fa9e | ||
|
|
5d0f7a9616 | ||
|
|
ed6d0a7140 | ||
|
|
217c40a245 |
@@ -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
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -245,14 +255,28 @@ Maquette « Progression ».
|
||||
|
||||
- Une phrase de synthèse, factuelle (« Sur vos 8 dernières parties, vous
|
||||
faites moins d'erreurs : votre taux d'erreur est passé de 14 à 7. »). Si la
|
||||
tendance monte, la phrase reste encourageante et sans reproche (« Votre
|
||||
taux d'erreur remonte un peu ces derniers jours. Les leçons sont là
|
||||
pour reprendre un point. »).
|
||||
tendance monte, la phrase reste encourageante et sans reproche, sans
|
||||
minimiser ni dramatiser (« Sur vos 8 dernières parties, votre taux
|
||||
d'erreur remonte, de 7 à 12. Les leçons sont là pour reprendre un
|
||||
point. »).
|
||||
- La courbe du taux d'erreur par partie terminée, avec le rappel « plus bas,
|
||||
c'est mieux » dans son titre, le dernier point en `verdict-good`, et un lien
|
||||
vers la fiche « taux d'erreur ».
|
||||
- La liste des parties (résultat, date, niveau de l'ordinateur, taux d'erreur
|
||||
en chiffre gras aligné à droite), chacune ouvrant son bilan.
|
||||
c'est mieux » dans son titre et un lien vers la fiche « taux d'erreur ».
|
||||
Le dernier point est plus gros, à l'encre du texte ; il ne passe en
|
||||
`verdict-good` que si la tendance baisse (le vert dit « mieux », jamais
|
||||
« dernier »). Les valeurs s'écrivent au-dessus du premier et du dernier
|
||||
point seulement ; l'axe ne porte que la ligne du 0, sans chiffres qui
|
||||
doublent ceux des points.
|
||||
- La liste des parties : chaque ligne est un seul lien vers son bilan
|
||||
(chevron à droite), sans bouton par ligne ; l'analyse s'ouvre depuis le
|
||||
bilan. Sur la ligne : le résultat court (« Gagnée, 1 point », « Perdue,
|
||||
gammon, 2 points », jamais « partie simple »), puis en `caption`
|
||||
`text-muted` la date et le niveau (« 12 nov. 2026 · contre
|
||||
Intermédiaire », le niveau sans couleur), et à droite le taux d'erreur en
|
||||
chiffre gras.
|
||||
- Sans partie terminée : une phrase et « Nouvelle partie » en action
|
||||
principale, pleine largeur en portrait.
|
||||
- Pas de bouton « Retour » dans l'en-tête : la barre du haut et le retour du
|
||||
téléphone suffisent, comme sur la liste des leçons.
|
||||
- Moins de trois parties analysées : pas de courbe, une phrase « Votre
|
||||
courbe apparaîtra après trois parties. ».
|
||||
|
||||
@@ -271,3 +295,214 @@ Maquette « Progression ».
|
||||
qu'une case d'arrivée).
|
||||
- Les trois gestes coexistent ; aucun n'est expliqué par une fenêtre : le
|
||||
tutoriel et l'aide en parlent.
|
||||
|
||||
## Retours des joueurs, 2e lot : Crawford sur la barre, abandon
|
||||
|
||||
Maquettes : rangée « Retours des joueurs, 2e lot » du canevas, et les
|
||||
artboards « Match en 5 points » et « Match en paysage » de la rangée 1c,
|
||||
mis à jour.
|
||||
|
||||
### Crawford sur la barre
|
||||
|
||||
Pendant la partie Crawford, le videau ne peut pas être proposé : c'est ce
|
||||
que le joueur doit voir, là où il regarde, sur le plateau.
|
||||
|
||||
- Le mot **« Crawford »** est écrit à la verticale sur la barre (lecture de
|
||||
bas en haut), en capitales, `font-weight` 800, espacement 0,12 em, dans
|
||||
une capsule au contour de 1,5 px. Texte et contour prennent la couleur
|
||||
des numéros du cadre (`#F3EAD8` en clair, `#CDBFA8` en sombre), soit plus
|
||||
de 9:1 sur le bois de la barre. Pas de brique : la brique veut dire « à
|
||||
faire », et Crawford est un état.
|
||||
- **Taille** : 11 px au moins à l'écran, en portrait comme en paysage. La
|
||||
capsule fait environ 15 px de large en portrait, dans une barre d'environ
|
||||
22 px.
|
||||
- **Place** : dans la moitié haute de la barre, entre le bord du cadre et le
|
||||
premier pion frappé. En paysage, elle se place sous la pastille du compte
|
||||
de points de l'ordinateur, sans la toucher (8 px d'écart au moins).
|
||||
- **Pions frappés** : la capsule est dessinée sous eux. Si la moitié haute
|
||||
porte plus de pions frappés que la moitié basse, elle passe dans la
|
||||
moitié basse, symétriquement (au-dessus de la pastille de votre compte en
|
||||
paysage). Les pions ne bougent jamais pour elle.
|
||||
- **Le videau** reste à sa place, à 40 % d'opacité pendant cette partie.
|
||||
Le bouton « Doubler » disparaît de la barre d'actions.
|
||||
- **La ligne du match** en portrait ne dit plus « partie Crawford » :
|
||||
« Match en 5 points » suffit, la barre porte l'information. En paysage,
|
||||
le préfixe de la consigne disparaît aussi.
|
||||
- **Accessibilité** : `role="note"`, avec le libellé « Partie Crawford : le
|
||||
videau ne peut pas être proposé pendant cette partie. ». Ce n'est pas une
|
||||
cible tactile : la capsule ne doit pas gêner les touchers sur les flèches
|
||||
7 et 18. L'explication vient de l'aide et de la fiche du glossaire.
|
||||
- **Apparition** : un fondu en `duration-base` (160 ms) au début de la partie
|
||||
Crawford, et rien d'autre. Elle disparaît à la partie suivante.
|
||||
|
||||
### Abandonner une partie
|
||||
|
||||
Comme dans gnubg : le joueur propose d'abandonner en concédant une partie
|
||||
simple, un gammon ou un backgammon, et l'ordinateur accepte ou refuse selon
|
||||
le moteur.
|
||||
|
||||
- **Quand** : à votre tour, avant de lancer les dés, comme pour doubler.
|
||||
C'est le seul moment où le drapeau est présent ; ailleurs, il n'apparaît
|
||||
pas. Jamais dans le tutoriel ni dans les leçons.
|
||||
- **Le drapeau** : une icône de drapeau au trait (20 px, couleur du texte,
|
||||
jamais en brique), dans un bouton rond de 52 px.
|
||||
- En portrait, il est à gauche dans la barre d'actions, avant
|
||||
« Doubler » et « Lancer les dés ».
|
||||
- En paysage, il est dans la colonne de commandes, au-dessus du bouton
|
||||
d'annulation.
|
||||
- Son libellé accessible est « Abandonner la partie ». Le menu propose
|
||||
aussi la ligne « Abandonner la partie », active au même moment
|
||||
seulement.
|
||||
- **La feuille de choix**, qui monte depuis le bas (artboard « Abandon :
|
||||
feuille de choix ») :
|
||||
- le titre « Abandonner la partie » ;
|
||||
- une phrase : « Vous proposez de perdre tout de suite. L'ordinateur
|
||||
accepte ou refuse, selon le moteur. », complétée par « Le videau est
|
||||
à 2. » quand il a été doublé ;
|
||||
- un choix unique en cartes de 56 px : « Une partie simple »,
|
||||
« Un gammon », « Un backgammon », chacune avec les points en jeu à
|
||||
droite, videau compris ;
|
||||
- en match, les points se lisent comme le score qui en résulterait :
|
||||
« vous 2, l'ordinateur 5 · fin du match » ;
|
||||
- seuls les choix qui peuvent changer le résultat sont proposés. Ce sont
|
||||
ceux que le moteur juge possibles ; en partie libre, sans videau
|
||||
doublé, la règle de Jacoby fait que gammon et backgammon ne comptent
|
||||
pas, donc seule « Une partie simple » reste, sans liste de choix ;
|
||||
- le choix par défaut est le plus petit ;
|
||||
- les actions sont « Continuer la partie » (secondaire) et « Proposer »
|
||||
(principale) ;
|
||||
- la feuille elle-même tient lieu de confirmation : pas de deuxième
|
||||
question.
|
||||
- **La réponse** arrive après un temps court. Pendant le calcul,
|
||||
« Proposer » devient « L'ordinateur réfléchit… », désactivé.
|
||||
- **Il accepte** : la feuille se ferme et la partie se termine comme une
|
||||
défaite ordinaire, avec la fin de partie puis le bilan. Le résultat
|
||||
s'écrit « Perdue par abandon, gammon, 2 points » dans la fin de partie,
|
||||
le bilan et la progression. L'abandon accepté ne s'annule pas : c'est
|
||||
la seule action du jeu sans retour, et la feuille l'a dit.
|
||||
- **Il refuse** : la feuille se ferme, la partie continue et le drapeau
|
||||
reste disponible. Un message du maître s'affiche au-dessus de la barre
|
||||
d'actions (artboard « Abandon : l'ordinateur refuse ») : le drapeau,
|
||||
puis « L'ordinateur refuse. » en gras, puis la raison donnée par le
|
||||
moteur. Le message reste jusqu'au lancer des dés.
|
||||
- **Ton** : des faits, ni reproche ni encouragement à abandonner. Phrases
|
||||
à ajouter à `docs/maitre/phrases.md`, puis à valider par Pierre :
|
||||
- `abandon.refus` : « L'ordinateur refuse. D'après le moteur, il peut
|
||||
espérer plus que {concession} en continuant. La partie continue. »
|
||||
({concession} : « une partie simple », « un gammon ») ;
|
||||
- `abandon.accepte` : « L'ordinateur accepte votre abandon : vous perdez
|
||||
{concession}, {points}. ».
|
||||
- **Glossaire** : nouvelle fiche `abandon.md` (anglais : resign). En bref :
|
||||
« Abandonner, c'est concéder la partie avant la fin, en proposant de
|
||||
perdre une partie simple, un gammon ou un backgammon. L'adversaire
|
||||
accepte ou refuse ; s'il refuse, la partie continue. »
|
||||
- **Hors périmètre** : l'ordinateur qui propose lui-même d'abandonner
|
||||
(gnubg sait le faire). C'est à décider plus tard, avec Pierre.
|
||||
|
||||
## Bilan du match (`#/matchs/<id>/bilan`)
|
||||
|
||||
Demande de Pierre (2026-11-12) : une analyse du match entier, surtout pour
|
||||
le videau. Maquettes : rangée « Analyse du match entier » du canevas
|
||||
(« Bilan du match », page entière, et « Vos parties : un match = une
|
||||
ligne »). L'exemple suit le match de Pierre : mené 0–4, gagné 5–4.
|
||||
|
||||
Le bilan d'une partie (1e) reste tel quel. Le bilan du match a la même
|
||||
forme et les mêmes composants, de haut en bas :
|
||||
|
||||
1. **L'en-tête** : en `caption`, « Fin du match · 5 points · <date> » ; en
|
||||
`display`, « Vous gagnez le match » ou « L'ordinateur gagne le match » ;
|
||||
puis une phrase factuelle avec le score final et le plus grand retard
|
||||
remonté, s'il y en a un (« 5 à 4, en cinq parties, après avoir été mené 0
|
||||
à 4. »). Pour un match en cours : « Match en cours · 1–2 » et « Reprendre
|
||||
le match » en action principale.
|
||||
2. **La courbe des chances de gagner le match**, la pièce maîtresse :
|
||||
- une seule ligne continue, de 0 à 100 %, de la première décision à la
|
||||
dernière : la fin d'une partie est le début de la suivante ;
|
||||
- les parties sont séparées par un trait vertical `border` ; au-dessus de
|
||||
chaque partie, le score **au début** de celle-ci (« 0–2 ») ; en
|
||||
dessous, son nom court (« P2 ») ;
|
||||
- la partie Crawford a un fond `surface-sunken` ;
|
||||
- la ligne des 50 % est en pointillés, avec son libellé ;
|
||||
- trois marques, sans autre couleur sur la courbe : le moment décisif
|
||||
(anneau `last`, or), la décision à revoir (point `verdict-blunder`) et
|
||||
la fin (point `verdict-good` si le match est gagné, `text` sinon) ;
|
||||
- une légende de trois éléments sous la courbe ;
|
||||
- **lecture au doigt** : glisser sur la courbe affiche une bulle (partie,
|
||||
coup, score, chances en %), avec un trait vertical qui suit le doigt et
|
||||
une vibration légère à chaque changement de partie ; lâcher garde la
|
||||
bulle ; toucher la bulle ouvre la décision dans l'analyse. Au clavier :
|
||||
← → d'une décision à l'autre, Entrée pour ouvrir.
|
||||
C'est la seule cible de la courbe : tout son cadre (au moins 150 px de
|
||||
haut) sert de zone tactile.
|
||||
- le libellé accessible résume la courbe en une phrase (départ, plus bas,
|
||||
fin, moment décisif).
|
||||
3. **« Partie par partie »** : une liste, une ligne de 52 px par partie (son
|
||||
numéro, son résultat court comme dans la progression, qui a marqué
|
||||
combien, et le score **après** la partie en chiffres gras à droite) ;
|
||||
chaque ligne ouvre le bilan de la partie. Le score se lit toujours
|
||||
« vous – l'ordinateur », et une phrase `caption` le rappelle sous la
|
||||
liste.
|
||||
4. **« Votre jeu sur le match »** : trois tuiles, le taux d'erreur de tout
|
||||
le match, celui des pions et celui du videau, en millièmes comme ailleurs.
|
||||
En dessous, les jauges « Votre jeu » et « Les dés » du bilan de partie,
|
||||
calculées sur tout le match.
|
||||
5. **« Le videau »**, le cœur de la demande :
|
||||
- une phrase : « 8 décisions de videau ; pour 2 d'entre elles, j'aurais
|
||||
fait autrement. » ;
|
||||
- une ligne par décision qui diffère de celle du moteur, regroupée par
|
||||
sorte, dans l'ordre : mauvais double, double manqué, prise fautive,
|
||||
refus fautif. Sur la ligne : la sorte, « Partie 2, coup 6, à 0–2 » (le
|
||||
score compte beaucoup au videau), et la perte de chances de gagner le
|
||||
match en %, colorée comme le verdict ;
|
||||
- les sortes sans écart tiennent en une phrase (« Prises et refus :
|
||||
aucune différence avec mes choix. ») ;
|
||||
- chaque ligne ouvre la décision dans l'analyse.
|
||||
6. **« Le moment décisif »** : la décision ou le jet qui a fait le plus
|
||||
varier les chances de gagner le match. Une phrase factuelle (« Avec 5-5,
|
||||
vos chances de gagner le match passent de 24 % à 46 %. C'est le plus
|
||||
grand écart du match. ») et le lien « Voir ce coup ». Pas de jugement :
|
||||
ce peut être un coup de dés.
|
||||
7. **« La décision à revoir »** : la plus grande perte de chances de gagner
|
||||
le match, pions ou videau, présentée comme « le coup à revoir » du bilan
|
||||
de partie (miniature, verdict, phrase du maître, « Revoir » et
|
||||
« Demander à une IA »). Le prompt est le gabarit « bilan de mon match ».
|
||||
8. **« Exporter le match (SGF) »**, un lien avec une phrase : « pour l'ouvrir
|
||||
dans GNU Backgammon ».
|
||||
|
||||
**Barre d'actions** : « Revoir le match » (secondaire, ouvre l'analyse à
|
||||
la première décision de la partie 1) et « Nouveau match » (principale).
|
||||
|
||||
**Unités** : en match, les pertes s'expriment en **chances de gagner le
|
||||
match** (%), à une décimale, et jamais en équité. Le taux d'erreur reste en
|
||||
millièmes, comme dans la progression.
|
||||
|
||||
### Accès
|
||||
|
||||
- **Fin d'un match** : la fin de partie de la dernière partie propose « Bilan
|
||||
du match » en action principale, à la place de « Nouvelle partie » ; le
|
||||
bilan de cette partie garde un lien « Voir le bilan du match ».
|
||||
- **Bilan d'une partie de match** : sous l'en-tête, une ligne `caption`
|
||||
« Partie 2 du match en 5 points · <lien>Bilan du match</lien> ».
|
||||
- **Analyse d'une partie de match** : en tête, « Partie 2 sur 5 · 0–2 »,
|
||||
avec « ‹ » et « › » (44 px) pour passer à la partie précédente ou
|
||||
suivante. La courbe de l'analyse reste celle de la partie.
|
||||
- **Listes (« Vos parties », progression)** : un match tient en **une
|
||||
ligne** :
|
||||
- le titre « Match en 5 points · gagné 5–4 » (« perdu », « en cours,
|
||||
1–2 ») ;
|
||||
- un trait par partie, `verdict-good` si vous l'avez gagnée,
|
||||
`border-strong` sinon, et un trait en pointillés pour la partie à
|
||||
venir d'un match en cours ;
|
||||
- en `caption`, la date, le nombre de parties et le niveau ;
|
||||
- à droite, le taux d'erreur du match ; pour un match en cours,
|
||||
« Reprendre » en brique ;
|
||||
- la ligne ouvre le bilan du match (le match en cours, s'il n'est pas
|
||||
fini). Les parties libres gardent leur ligne.
|
||||
- **Courbe de la progression** : inchangée, un point par partie, y compris
|
||||
les parties de match.
|
||||
|
||||
### Paysage
|
||||
|
||||
Deux colonnes : à gauche, l'en-tête, la courbe et « Partie par partie » ; à
|
||||
droite, le jeu, le videau, le moment décisif et la décision à revoir. La
|
||||
barre d'actions reste en bas de la colonne de droite.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user