Match, niveaux et réglages : écran Nouvelle partie, Crawford, Jacoby (1c) #49
+12
-2
@@ -1652,6 +1652,7 @@ export function startApp(options: AppOptions): void {
|
||||
? TEXTS.unreadablePosition
|
||||
: "";
|
||||
ui.positionBoard.toggleAttribute("hidden", !shown);
|
||||
ui.position.classList.toggle("with-board", shown);
|
||||
ui.positionDetail.hidden = !shown;
|
||||
ui.positionCopy.hidden = !shown;
|
||||
ui.positionPlay.hidden = state.kind !== "shown" || state.shared.result !== null;
|
||||
@@ -1659,11 +1660,20 @@ export function startApp(options: AppOptions): void {
|
||||
return;
|
||||
}
|
||||
const shared = state.shared;
|
||||
const width = ui.positionBoard.clientWidth || Math.min(window.innerWidth, 640);
|
||||
// En paysage, le plateau occupe toute la hauteur, à gauche de la
|
||||
// colonne du titre et des actions (app.css), avec la géométrie du
|
||||
// jeu : la colonne gauche du cadre garde le videau, sans menu ni
|
||||
// bouton annuler (il ne se joue pas).
|
||||
const landscape = layout() === "landscape";
|
||||
const box = ui.positionBoard.getBoundingClientRect();
|
||||
const width = box.width || Math.min(window.innerWidth, 640);
|
||||
const height = box.height || window.innerHeight;
|
||||
const model = boardModel(shared.sides);
|
||||
renderBoard(
|
||||
ui.positionBoard,
|
||||
boardGeometry("portrait", PORTRAIT_ASPECT, width / 640),
|
||||
landscape
|
||||
? boardGeometry("landscape", width / height, height / 520)
|
||||
: boardGeometry("portrait", PORTRAIT_ASPECT, width / 640),
|
||||
model,
|
||||
{
|
||||
dice: [],
|
||||
|
||||
@@ -323,6 +323,8 @@ export function createHelpView(container: HTMLElement, tabs: HTMLElement): HelpV
|
||||
const opened = questions.find((q) => q.id === question) ?? null;
|
||||
const closed = route.screen === "help" && opened === null ? lastOpen : null;
|
||||
lastOpen = opened;
|
||||
// La barre haute de l'aide reste en vue (app.css) : ce qu'on
|
||||
// amène en haut s'arrête 12 px sous elle (scroll-margin-top).
|
||||
const target = opened?.section ?? (term === null ? undefined : terms.get(term)?.card);
|
||||
if (target !== undefined) {
|
||||
target.scrollIntoView({ block: "start" });
|
||||
|
||||
+21
-12
@@ -20,7 +20,7 @@ Repris du cadrage (modules, écrans clés, IA générative en V1) et des ADR 001
|
||||
| 1a. Moteur pour l'analyse (fait) | Niveaux de jeu (plis et bruit de GNU Backgammon : débutant à expert), classement des coups d'un jet avec équités, analyse d'un coup joué et d'une décision de videau, chance d'un jet ; exposés par `engine.h`, `engine-cli` et le Worker, avec positions de référence comparées à gnubg de bureau | — |
|
||||
| 1b. Parties enregistrées (fait) | Stockage IndexedDB derrière l'interface de stockage existante : plusieurs parties, reprise automatique, branches (« Rejouer d'ici » crée une branche, l'originale reste intacte et rechargeable), export SGF avec variantes | — |
|
||||
| 1c. Match et réglages | Partie libre ou match en N points, règle Crawford, règle Jacoby, choix du niveau de l'ordinateur, conseil du maître activable ou non ; écran de nouvelle partie | 1a, 1b |
|
||||
| 1d. Conseil du maître | Après chaque coup du joueur, jamais avant (ADR 0011) : appréciation du moteur (bon coup, imprécision, erreur, gaffe) avec mot, icône et couleur, phrase du maître fondée sur les chiffres du moteur, coup recommandé montré sur demande ; aussi pour les décisions de videau ; calcul en arrière-plan sans retarder le jeu | 1a |
|
||||
| 1d. Conseil du maître | Après chaque coup du joueur, jamais avant (ADR 0011) : appréciation du moteur (bon coup, imprécision, erreur, grosse erreur) avec mot, icône et couleur, phrase du maître fondée sur les chiffres du moteur, coup recommandé montré sur demande ; aussi pour les décisions de videau ; calcul en arrière-plan sans retarder le jeu | 1a |
|
||||
| 1e. Fin de partie et analyse | Bilan (ADR 0011) : résultat, coups par appréciation, taux d'erreur comparé à la chance, « le coup à revoir » ; écran d'analyse : graphe d'équité (toucher un point ouvre la position), liste des erreurs filtrable, visualiseur pas à pas coup joué contre meilleur coup, « Rejouer d'ici » | 1a, 1b, 1d |
|
||||
| 1f. Prompts à copier | Gabarits versionnés et testés (`docs/prompts/` ou équivalent, source unique), bouton « Demander à une IA » sur le plateau, le conseil du maître, l'analyse et la fin de partie ; le moteur décide, l'IA explique | 1d, 1e |
|
||||
| 1g. Accueil, tutoriel et leçons | 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 en brouillon, à relire par un joueur fort | 0e.5, 1a |
|
||||
@@ -36,21 +36,30 @@ Repris du cadrage (modules, écrans clés, IA générative en V1) et des ADR 001
|
||||
ton des phrases du maître est encourageant ; l'espace qu'il occupe est
|
||||
réservé dans la mise en page (revue UX 0e.2), pas en fenêtre surgissante.
|
||||
|
||||
## Questions pour Pierre (ne bloquent pas le démarrage)
|
||||
## Décisions de Pierre (2026-11-09)
|
||||
|
||||
- **Indice** : le cadrage le liste dans le module Jeu, l'ADR 0011 l'a laissé
|
||||
« à concevoir séparément ». Il n'est pas dans l'étape 1 sans décision.
|
||||
- **Questionnaire de premier lancement** (choix du parcours) : le parcours 1
|
||||
seul existe en V1 ; proposé : un simple choix « Je découvre » / « Je connais
|
||||
les règles » qui oriente vers le tutoriel ou la partie.
|
||||
- **Indice avant le coup** : pas en V1 (« l'idée est bonne »), à reprendre en
|
||||
V1.5 ou V2. Le conseil du maître après le coup reste le seul mécanisme de
|
||||
l'étape 1.
|
||||
- **Premier lancement** : un choix « Je découvre » / « Je connais les règles »
|
||||
(fait en 0e.5).
|
||||
- **Seuils des appréciations** : ceux de GNU Backgammon (douteux 0,03,
|
||||
mauvais 0,06, très mauvais 0,12 d'équité, `arSkillLevel`), exposés par le
|
||||
moteur depuis 1a ; à calibrer sur de vraies parties.
|
||||
- **Relecture des leçons** par un joueur fort de l'Atelier du Huit.
|
||||
mauvais 0,06, très mauvais 0,12, `arSkillLevel`), exposés par le moteur
|
||||
depuis 1a ; à recalibrer après les retours de Pierre sur de vraies parties.
|
||||
Le niveau le plus grave se dit « Grosse erreur », pas « gaffe ».
|
||||
- **Leçons** : pas de relecture préalable par un joueur fort ; elles sont
|
||||
livrées telles quelles et améliorées sur retours d'experts. Garde-fous
|
||||
maintenus : chaque réponse attendue est le meilleur coup du moteur, vérifié
|
||||
par un test, et le texte n'affirme rien que le moteur ne confirme.
|
||||
- **Plein écran par défaut dans l'app installée** : non (pas de
|
||||
`display_override` fullscreen) ; la barre d'état reste visible en portrait.
|
||||
- **Validés** : le texte du tutoriel et toutes les maquettes du canevas
|
||||
(accueil, installation, tutoriel, aide, écrans 1c à 1i), ainsi que le cadre
|
||||
des phrases du maître (section « Les phrases du maître » de
|
||||
`docs/identite/charte/ecrans-etape-1.md`).
|
||||
|
||||
## Ordre de réalisation
|
||||
|
||||
1a et 1b d'abord (fondations, sans conflit avec l'interface), en parallèle des
|
||||
sous-étapes 0e. Puis 1c, 1d, 1e, 1f dans cet ordre ; 1g après la validation des
|
||||
maquettes d'accueil et de tutoriel par Pierre ; 1h et 1i ; 1j en dernier. Chaque
|
||||
sous-étapes 0e. Puis 1c, 1d, 1e, 1f dans cet ordre ; 1g (maquettes validées par Pierre) ; 1h et 1i ; 1j en dernier. Chaque
|
||||
sous-étape a son plan dans `docs/plans/` et sa merge request.
|
||||
|
||||
@@ -29,7 +29,7 @@ Demandes de Pierre du 2026-11-03, traitées avant d'ouvrir l'étape 1.
|
||||
| Étape | Contenu | État |
|
||||
| --- | --- | --- |
|
||||
| 1. V1 Jouer et comprendre | Détail et ordre dans `docs/etape-1.md` : moteur pour l'analyse, parties enregistrées et branches, match et réglages, conseil du maître, fin de partie et analyse, prompts, accueil, tutoriel et leçons, progression, plateau soigné, plan de test | 🔄 |
|
||||
| 2. V1.5 Apprendre | Parcours 2, exercices, progression | ⏳ |
|
||||
| 2. V1.5 Apprendre | Parcours 2, exercices, progression ; idée retenue par Pierre : un indice avant le coup (sans la réponse) | ⏳ |
|
||||
| 3. V2 Perfectionnement et IA | Parcours 3 et 4, rollouts, révision espacée | ⏳ |
|
||||
|
||||
Chaque étape reçoit son fichier `docs/etape-N.md` avant de démarrer.
|
||||
|
||||
@@ -67,7 +67,8 @@ interrompre.
|
||||
deux ou trois phrases. Une étape sur deux finit par un écran **« à vous »**
|
||||
où le joueur fait le geste sur le vrai plateau, contrôlé par le moteur.
|
||||
- En haut : une barre de progression en huit segments (`accent` pour les
|
||||
étapes faites) et « Passer » à droite. En bas : « Retour » et
|
||||
étapes faites) et « Quitter » à droite (pas « Passer » : ce mot appartient au
|
||||
videau, et le tutoriel enseigne les mots exacts du jeu). En bas : « Retour » et
|
||||
« Continuer ».
|
||||
- Les positions sont données par leurs identifiants gnubg (ADR 0013) ; le
|
||||
plateau est le composant de l'app, non jouable sauf sur les écrans
|
||||
@@ -113,9 +114,10 @@ interrompre.
|
||||
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 quand vous finissez, c'est un gammon :
|
||||
2 points. S'il lui reste en plus un pion sur la barre ou dans votre camp,
|
||||
c'est un backgammon : 3 points. »
|
||||
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
|
||||
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.
|
||||
Avant de lancer vos dés, vous pouvez proposer de doubler la valeur de la
|
||||
partie. L'ordinateur prend, et la partie continue en valant le double, ou il
|
||||
|
||||
@@ -163,3 +163,38 @@ Une entrée datée par session : ce qui a été fait, décidé, et la suite.
|
||||
Playwright (412×915, 915×412, 844×320, clair et sombre) avant l'essai de
|
||||
Pierre sur le téléphone.
|
||||
- Réglage en main des vibrations sur le Fairphone 4.
|
||||
|
||||
## 2026-11-03 → 2026-11-09 : retours 0e et début de l'étape 1
|
||||
|
||||
**Fait**
|
||||
- Retours du premier test sur téléphone, traités et publiés :
|
||||
- saisie du coup (MR !28) ;
|
||||
- révision UX arbitrée par la session « identité » (MR !35) ;
|
||||
- parties enregistrées avec branches et export SGF, sous-étape 1b (MR !37).
|
||||
- Fusionnés :
|
||||
- adresses par écran et liens de position, sous-étape 0e.4 (MR !43). L'ADR 0013 est amendé : une adresse par écran, de vrais liens ;
|
||||
- moteur pour l'analyse et les niveaux, sous-étape 1a (MR !32) : 9 niveaux de gnubg, analyse de coup et de videau, chance d'un jet, concordance avec gnubg de bureau à 1,64e-3 près.
|
||||
- Décisions techniques :
|
||||
- le cache d'évaluation de gnubg est vidé à chaque calcul, pour que les résultats ne dépendent que de leurs arguments ;
|
||||
- le schéma IndexedDB est un format durable (amendement de l'ADR 0007) ;
|
||||
- l'image Docker de test est nommée par worktree, ce qui corrige l'instabilité des tests concurrents.
|
||||
- Plans écrits pour toute l'étape 1, de 1c à 1i. La session « identité » a maquetté tous les écrans (MR !40 à !42).
|
||||
- Décisions et validations de Pierre (2026-11-09), consignées dans `docs/etape-1.md` :
|
||||
- « Grosse erreur » plutôt que « gaffe » ;
|
||||
- pas d'indice en V1 ;
|
||||
- pas de relecture préalable des leçons ;
|
||||
- pas de plein écran par défaut ;
|
||||
- seuils de gnubg ;
|
||||
- validés : le texte du tutoriel, les maquettes et le cadre des phrases du maître.
|
||||
- Nouvelles demandes de Pierre, intégrées aux plans :
|
||||
- une adresse pour chaque écran ;
|
||||
- la mise à jour forcée de l'app (0e.5) ;
|
||||
- un coach bienveillant, mis en avant mais discret.
|
||||
|
||||
**Reste à faire**
|
||||
- 0e.5 (MR !44) : appliquer les corrections de recette T1 à T5, puis fusionner et publier.
|
||||
- Publication bloquée : `RESTART.sh`, un fichier local non suivi par git, fait échouer `make lint`, et donc `make deploy-prod`.
|
||||
- Étape 1 :
|
||||
- 1c en cours ;
|
||||
- puis 1d, 1e, 1f, 1g, 1h et 1i ;
|
||||
- enfin le plan de test 1j.
|
||||
|
||||
@@ -29,17 +29,21 @@ du moteur.
|
||||
(événement d'analyse rétrocompatible, ou cache séparé justifié) pour servir
|
||||
au bilan (1e) sans recalcul.
|
||||
2. **Ligne du maître** : icône d'appréciation, mot en couleur (« Bon coup. »,
|
||||
« Imprécision. », « Erreur. », « Gaffe. » : correspondance avec les seuils
|
||||
« Imprécision. », « Erreur. », « Grosse erreur. » (choix de Pierre du 2026-11-09, plutôt que « gaffe ») : correspondance avec les seuils
|
||||
de gnubg documentée), phrase courte en italique, chevron. Toucher ouvre la
|
||||
feuille d'explication : coup joué et meilleur coup montrés sur le plateau
|
||||
(pions animés, ADR 0011 « comparaison visuelle »), écart d'équité seulement
|
||||
sur demande, lien vers la fiche du glossaire du concept en jeu si le moteur
|
||||
permet de le déterminer, sinon rien.
|
||||
3. **Phrases du maître** : gabarits versionnés en français dans une source
|
||||
unique sous `docs/` (CC BY-SA 4.0), ton encourageant, jamais culpabilisant ;
|
||||
elles ne citent que des faits du moteur (coup recommandé, rang, pions
|
||||
frappés, écart) : aucune explication inventée. Première version à faire
|
||||
valider par Pierre (ton).
|
||||
unique sous `docs/` (CC BY-SA 4.0). Référence de ton, approuvée par Pierre
|
||||
le 2026-11-09 : la section « Les phrases du maître » de
|
||||
`docs/identite/charte/ecrans-etape-1.md` (1re personne et vouvoiement,
|
||||
faits du moteur uniquement, moins de 140 caractères, ton sobre sans
|
||||
exclamation, formules qui tournent pour les bons coups, exemples). Un test
|
||||
vérifie la longueur et l'absence de « ! » sur chaque gabarit rendu. Les
|
||||
phrases livrées passent par la session « identité » puis par Pierre pour
|
||||
la relecture finale.
|
||||
4. **Discret** : rien n'apparaît pendant que le joueur réfléchit ; la ligne
|
||||
s'efface à l'action suivante ; désactivable (réglage de 1c et menu).
|
||||
5. **Videau** : même traitement pour doubler, ne pas doubler, prendre, passer.
|
||||
|
||||
@@ -18,7 +18,7 @@ Mettre en place les cinq premières leçons numérotées de l'ADR 0011. Chaque l
|
||||
4. La course et le compte de points.
|
||||
5. Sortir ses pions sans se faire frapper.
|
||||
|
||||
Chaque leçon compte deux à quatre exercices « à vous ». Leurs positions sont données par identifiants gnubg. La réponse attendue est le meilleur coup du moteur à 2 plis, vérifié par un test. Le texte n'affirme rien que le moteur ne confirme. Les contenus sont livrés en brouillon, marqués « à relire par un joueur fort » (cadrage, risque « contenus »), et Pierre les valide.
|
||||
Chaque leçon compte deux à quatre exercices « à vous ». Leurs positions sont données par identifiants gnubg. La réponse attendue est le meilleur coup du moteur à 2 plis, vérifié par un test. Le texte n'affirme rien que le moteur ne confirme. Décision de Pierre du 2026-11-09 : pas de relecture préalable par un joueur fort ; les leçons sont livrées telles quelles et améliorées sur retours d'experts. Les garde-fous restent : chaque réponse attendue est le meilleur coup du moteur, vérifié par un test, et le texte n'affirme rien que le moteur ne confirme.
|
||||
2. **Progression.** Elle est enregistrée dans le stockage de 1b, dans le magasin `settings` ou dans un nouveau magasin avec montée de version (ADR 0007 amendé). Chaque leçon a son état : à commencer, « 2 exercices sur 4 », terminée.
|
||||
3. **Adresses** : `#/lecons` et `#/lecons/<n>/<écran>`, à ajouter au tableau de l'ADR 0013.
|
||||
4. **Accueil.** Il reçoit la carte « prochaine leçon » prévue par la charte.
|
||||
|
||||
@@ -300,10 +300,49 @@ async function checkStable(when) {
|
||||
stableChecks++;
|
||||
}
|
||||
|
||||
/** Les boutons visibles d'une page dont le texte passe à la ligne : un
|
||||
* bouton tient sur une ligne et fait 48 px de haut (charte). Un bouton plus
|
||||
* haut que 48 px n'est refusé que si son texte occupe plusieurs lignes ;
|
||||
* un texte coupé par le bord (white-space: nowrap) est refusé aussi. */
|
||||
async function wrappedButtons(target) {
|
||||
return target.evaluate(() => {
|
||||
const wrapped = [];
|
||||
for (const el of document.querySelectorAll("button, a.button")) {
|
||||
const r = el.getBoundingClientRect();
|
||||
if (el.closest("[hidden]") || r.width === 0 || r.height === 0) {
|
||||
continue;
|
||||
}
|
||||
const name = el.id ? `#${el.id}` : el.textContent.trim();
|
||||
const tops = new Set();
|
||||
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
||||
for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
|
||||
if (node.textContent.trim() === "") {
|
||||
continue;
|
||||
}
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(node);
|
||||
for (const line of range.getClientRects()) {
|
||||
if (line.width > 0 && ![...tops].some((top) => Math.abs(top - line.top) < 4)) {
|
||||
tops.add(line.top);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (r.height > 48.5 && tops.size > 1) {
|
||||
wrapped.push(`${name} sur ${tops.size} lignes (${Math.round(r.height)} px)`);
|
||||
} else if (el.scrollWidth > el.clientWidth + 1) {
|
||||
wrapped.push(`${name} coupé (${el.scrollWidth} px pour ${el.clientWidth})`);
|
||||
}
|
||||
}
|
||||
return wrapped;
|
||||
});
|
||||
}
|
||||
|
||||
/** Les critères de la recette vérifiables sur la vue courante. Renvoie des
|
||||
* mesures pour le compte rendu. */
|
||||
async function checkView(width, height, when) {
|
||||
const where = `${width}x${height} (${when})`;
|
||||
const wrapped = await wrappedButtons(page);
|
||||
check(wrapped.length === 0, `${where} : boutons sur plusieurs lignes : ${wrapped.join(", ")}`);
|
||||
const seen = await page.evaluate(() => {
|
||||
const visible = (el) => {
|
||||
if (el.closest("[hidden]")) {
|
||||
@@ -767,6 +806,65 @@ async function checkAddresses() {
|
||||
await expectScreen(tab, `capture ${name}`, expected);
|
||||
await sleep(300);
|
||||
await tab.screenshot({ path: `${dir}/${browserName}-${name}-${width}x${height}.png` });
|
||||
const where = `${name} ${width}x${height}`;
|
||||
const wrapped = await wrappedButtons(tab);
|
||||
check(wrapped.length === 0, `${where} : boutons sur plusieurs lignes : ${wrapped.join(", ")}`);
|
||||
if (name === "question" || name === "terme") {
|
||||
// La barre haute de l'aide reste en vue, et ce qu'on a
|
||||
// ouvert s'arrête juste sous elle.
|
||||
const seen = await tab.evaluate(() => {
|
||||
const bar = document.getElementById("help-bar").getBoundingClientRect();
|
||||
const target = document.querySelector(
|
||||
"#help-play .question.open, #help-glossary dt.current",
|
||||
).getBoundingClientRect();
|
||||
return { scrolled: window.scrollY, barTop: bar.top, barBottom: bar.bottom, top: target.top };
|
||||
});
|
||||
check(seen.scrolled > 0 && Math.abs(seen.barTop) <= 1,
|
||||
`${where} : barre de l'aide hors de la vue (${JSON.stringify(seen)})`);
|
||||
check(Math.abs(seen.top - (seen.barBottom + 12)) <= 2,
|
||||
`${where} : élément à ${seen.top} px, barre jusqu'à ${seen.barBottom} px`);
|
||||
}
|
||||
if (name === "position") {
|
||||
// Les trois actions, dans l'ordre, visibles sans défiler.
|
||||
const seen = await tab.evaluate(() => ({
|
||||
scrolled: window.scrollY,
|
||||
order: [...document.querySelectorAll(".position-actions > *")].map((el) => el.id),
|
||||
boxes: ["position-play", "position-copy", "position-back"].map((id) => {
|
||||
const r = document.getElementById(id).getBoundingClientRect();
|
||||
return [r.left, r.top, r.right, r.bottom];
|
||||
}),
|
||||
board: (() => {
|
||||
const r = document.getElementById("position-board").getBoundingClientRect();
|
||||
return [r.left, r.top, r.width, r.height];
|
||||
})(),
|
||||
cube: (() => {
|
||||
const r = document.querySelector("#position-board .cube-face")?.getBoundingClientRect();
|
||||
return r === undefined ? null : [r.left, r.top, r.right, r.bottom];
|
||||
})(),
|
||||
controls: document.querySelectorAll("#position-board [data-action]").length,
|
||||
}));
|
||||
check(seen.order.join() === "position-play,position-copy,position-back",
|
||||
`${where} : actions dans l'ordre ${seen.order}`);
|
||||
const inView = seen.boxes.every(([l, t, r, b]) => l >= 0 && t >= 0 && r <= width && b <= height);
|
||||
if (width > height) {
|
||||
check(seen.scrolled === 0 && inView,
|
||||
`${where} : actions hors de la vue sans défiler : ${JSON.stringify(seen.boxes)}`);
|
||||
check(seen.board[0] === 0 && seen.board[1] === 0 && Math.abs(seen.board[3] - height) <= 1,
|
||||
`${where} : plateau pas sur toute la hauteur à gauche : ${seen.board}`);
|
||||
// Le videau, dans la colonne gauche du cadre, entier à
|
||||
// l'écran ; ni menu ni annuler : la position ne se joue pas.
|
||||
const [cl, ct, cr, cb] = seen.cube ?? [-1, -1, -1, -1];
|
||||
check(seen.cube !== null && cl >= 0 && ct >= 0 && cr <= seen.board[2] && cb <= height,
|
||||
`${where} : videau coupé : ${JSON.stringify(seen.cube)}`);
|
||||
check(seen.controls === 0, `${where} : ${seen.controls} commandes sur le plateau`);
|
||||
} else {
|
||||
// Empilées sur toute la largeur.
|
||||
const [first] = seen.boxes;
|
||||
check(seen.boxes.every(([l, , r], i) => Math.abs(l - first[0]) <= 1 &&
|
||||
Math.abs(r - first[2]) <= 1 && (i === 0 || seen.boxes[i - 1][3] <= seen.boxes[i][1])),
|
||||
`${where} : actions pas empilées : ${JSON.stringify(seen.boxes)}`);
|
||||
}
|
||||
}
|
||||
await tab.close();
|
||||
}
|
||||
}
|
||||
|
||||
+85
-20
@@ -363,6 +363,8 @@ button {
|
||||
border: 2px solid var(--accent);
|
||||
border-radius: var(--radius-pill);
|
||||
cursor: pointer;
|
||||
/* Un bouton tient sur une ligne (charte). */
|
||||
white-space: nowrap;
|
||||
/* Pas de délai ni de zoom sur un double toucher. */
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
@@ -1317,16 +1319,36 @@ html.rotated .banner {
|
||||
-webkit-user-select: text;
|
||||
}
|
||||
|
||||
#help-back {
|
||||
margin-top: var(--space-4);
|
||||
/* La barre haute de l'aide (« Retour » et onglets) reste en vue quand la
|
||||
* page défile. Sa hauteur est fixe (12 + 48 + 8 + 44 + 1 px de filet) : une
|
||||
* question ou un terme ouvert par son adresse s'arrête 12 px sous elle. */
|
||||
.help {
|
||||
--help-bar-height: 113px;
|
||||
}
|
||||
|
||||
/* Les onglets de l'aide : des liens, l'onglet courant souligné en brique. */
|
||||
.help-bar {
|
||||
position: sticky;
|
||||
top: env(safe-area-inset-top);
|
||||
z-index: 3;
|
||||
height: var(--help-bar-height);
|
||||
margin-inline: calc(-1 * var(--space-4));
|
||||
padding: var(--space-3) var(--space-4) 0;
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.help .question,
|
||||
.glossary dt {
|
||||
scroll-margin-top: calc(var(--help-bar-height) + 12px + env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
/* Les onglets de l'aide : des liens, l'onglet courant souligné en brique ;
|
||||
* sur une ligne, qui défile si l'écran est trop étroit. */
|
||||
.help-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-1);
|
||||
margin-top: var(--space-4);
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
margin-top: var(--space-2);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
@@ -1402,6 +1424,12 @@ html.rotated .banner {
|
||||
margin-inline: calc(-1 * var(--space-4));
|
||||
padding-inline: var(--space-4);
|
||||
background: var(--accent-soft);
|
||||
/* Le surlignage montre où l'on arrive, puis s'efface après 2 s. */
|
||||
animation: term-fade var(--duration-moderate) var(--easing-exit) 2s forwards;
|
||||
}
|
||||
|
||||
@keyframes term-fade {
|
||||
to { background-color: transparent; }
|
||||
}
|
||||
|
||||
/* -------------------------------------------------- Liens et boutons */
|
||||
@@ -1420,6 +1448,7 @@ a.button {
|
||||
background: var(--accent);
|
||||
border: 2px solid var(--accent);
|
||||
border-radius: var(--radius-pill);
|
||||
white-space: nowrap;
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
@@ -1483,14 +1512,6 @@ a.link-button {
|
||||
margin: var(--space-2) 0;
|
||||
}
|
||||
|
||||
/* En paysage, le plateau tient dans la hauteur de l'écran. */
|
||||
html.landscape .position-board {
|
||||
width: min(100%, calc(80vh * 640 / 520));
|
||||
height: auto;
|
||||
margin-inline: auto;
|
||||
aspect-ratio: 640 / 520;
|
||||
}
|
||||
|
||||
.position-detail {
|
||||
margin: var(--space-2) 0 var(--space-4);
|
||||
font-size: 15px;
|
||||
@@ -1498,26 +1519,25 @@ html.landscape .position-board {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* Les actions empilées sur toute la largeur, la principale en premier. */
|
||||
.position-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.6fr;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.position-actions > * {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.position-copy {
|
||||
#position-copy {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
margin-top: var(--space-3);
|
||||
}
|
||||
|
||||
.position-copy .icon {
|
||||
#position-copy .icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
@@ -2367,3 +2387,48 @@ html.landscape .welcome {
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* En paysage, comme l'écran de jeu : le plateau à gauche sur toute la
|
||||
* hauteur (colonne gauche du cadre avec le videau seul), le titre, le contexte et les actions
|
||||
* dans une colonne à droite. */
|
||||
html.landscape .position-view.with-board {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 288px;
|
||||
grid-template-rows: auto auto auto auto 1fr;
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
html.landscape .position-view.with-board > * {
|
||||
grid-column: 2;
|
||||
margin-inline: 0;
|
||||
padding-inline: var(--space-4) calc(var(--space-4) + env(safe-area-inset-right));
|
||||
}
|
||||
|
||||
html.landscape .position-view.with-board .position-title {
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
|
||||
html.landscape .position-view.with-board .position-board {
|
||||
grid-row: 1 / -1;
|
||||
grid-column: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
aspect-ratio: auto;
|
||||
background: var(--frame);
|
||||
}
|
||||
|
||||
html.landscape .position-view.with-board .position-actions {
|
||||
padding-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
html.landscape .position-view.with-board .position-actions > * {
|
||||
padding-inline: 16px;
|
||||
}
|
||||
|
||||
+13
-9
@@ -99,27 +99,31 @@
|
||||
|
||||
<!-- L'aide : un onglet par adresse (#/aide, #/glossaire, #/a-propos). -->
|
||||
<section id="help" class="help page" hidden>
|
||||
<a id="help-back" href="#/" class="button secondary"></a>
|
||||
<nav id="help-tabs" class="help-tabs">
|
||||
<a href="#/aide" data-screen="help"></a>
|
||||
<a href="#/glossaire" data-screen="glossary"></a>
|
||||
<a href="#/a-propos" data-screen="about"></a>
|
||||
</nav>
|
||||
<!-- La barre haute de l'aide reste en vue quand la page défile. -->
|
||||
<div id="help-bar" class="help-bar">
|
||||
<a id="help-back" href="#/" class="button secondary"></a>
|
||||
<nav id="help-tabs" class="help-tabs">
|
||||
<a href="#/aide" data-screen="help"></a>
|
||||
<a href="#/glossaire" data-screen="glossary"></a>
|
||||
<a href="#/a-propos" data-screen="about"></a>
|
||||
</nav>
|
||||
</div>
|
||||
<div id="help-content"></div>
|
||||
</section>
|
||||
|
||||
<!-- Une position partagée (#/position/<Position ID>/<Match ID>), non
|
||||
jouable. -->
|
||||
jouable. En portrait, de haut en bas ; en paysage, le plateau à gauche
|
||||
sur toute la hauteur, le reste dans une colonne à droite. -->
|
||||
<section id="position-view" class="position-view page" aria-labelledby="position-title" hidden>
|
||||
<h2 id="position-title" class="position-title"></h2>
|
||||
<p id="position-message" class="position-message" role="status"></p>
|
||||
<svg id="position-board" class="board position-board" role="img" xmlns="http://www.w3.org/2000/svg"></svg>
|
||||
<p id="position-detail" class="position-detail"></p>
|
||||
<div class="position-actions">
|
||||
<a id="position-back" href="#/" class="button secondary"></a>
|
||||
<button id="position-play" type="button"></button>
|
||||
<button id="position-copy" type="button" class="secondary"></button>
|
||||
<a id="position-back" href="#/" class="button secondary"></a>
|
||||
</div>
|
||||
<button id="position-copy" type="button" class="secondary position-copy"></button>
|
||||
</section>
|
||||
|
||||
<!-- Menu : une feuille qui monte du bas. -->
|
||||
|
||||
Reference in New Issue
Block a user