diff --git a/CREDITS.md b/CREDITS.md index cba0206..9a3b244 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -27,8 +27,15 @@ Les contenus pédagogiques (leçons, textes) sont sous licence CC BY-SA 4.0. (Juan Pablo del Peral, Huerta Tipográfica), licence SIL Open Font License 1.1 — https://github.com/google/fonts/tree/main/ofl/alegreyasans Ses tracés (graisse Bold) composent le logotype, dans `docs/identite/logo/`. + Cinq graisses (Regular, Italic, Medium, Bold, ExtraBold) sont embarquées + dans l'application (`web/fonts/`, avec la licence `OFL.txt`), réduites aux + caractères latins et converties en woff2 par fontTools (143 Ko en tout). +- **Logo et icônes d'Ustagammon** (`docs/identite/logo/`), licence + CC BY-SA 4.0 ; publiés dans `icons/` du site (favicon, icônes de la PWA, + logo de la barre haute). - **fontTools** (MIT), utilisé par `docs/identite/generer-logo.py` pour - convertir le texte du logotype en tracés. + convertir le texte du logotype en tracés, et pour produire les fichiers + woff2 de la police ; avec **Brotli** (MIT), qui les compresse. ## Outils de développement diff --git a/Makefile b/Makefile index 7746b09..3883141 100644 --- a/Makefile +++ b/Makefile @@ -115,9 +115,17 @@ deploy: site ## Sert l'application sur le réseau local (Ctrl+C pour arrêter) deploy-web: deploy ## Comme make deploy (ancien nom, étape 0b) +# Fichiers de docs/identite/logo/ publiés dans icons/ (manifeste, favicon, +# barre haute). +SITE_ICONS := icone.svg icone-192.png icone-512.png icone-maskable-192.png icone-maskable-512.png \ + apple-touch-icon-180.png favicon.svg favicon-32.png \ + logo-horizontal-couleur.svg logo-horizontal-sombre.svg symbole-couleur.svg symbole-sombre.svg + # Le site tel qu'il sera publié, reconstruit entièrement à chaque appel : -# - web/ à la racine : la page de l'application, son style, le manifeste, les -# icônes, le Web Worker du moteur, et les pages de test sous moteur/ ; +# - web/ à la racine : la page de l'application, son style, sa police, le +# manifeste, le Web Worker du moteur, et les pages de test sous moteur/ ; +# - sous icons/, le logo et les icônes de la charte, copiés depuis +# docs/identite/logo/ (une seule source, ADR 0010) ; # - l'application compilée (build/web/src/) sous app/ ; # - le moteur sous wasm/ ; # - les copies du glossaire et des scénarios sous docs/, lues par l'aide de @@ -128,6 +136,8 @@ site: engine-wasm web ## Assemble le site publié dans build/site/ rm -rf $(BUILD_DIR)/site mkdir -p $(BUILD_DIR)/site/wasm $(BUILD_DIR)/site/docs cp -r web/. $(BUILD_DIR)/site/ + mkdir -p $(BUILD_DIR)/site/icons + cp $(addprefix docs/identite/logo/,$(SITE_ICONS)) $(BUILD_DIR)/site/icons/ cp -r $(BUILD_DIR)/web/src $(BUILD_DIR)/site/app cp $(BUILD_DIR)/wasm/engine.mjs $(BUILD_DIR)/wasm/engine.wasm $(BUILD_DIR)/wasm/engine.data \ $(BUILD_DIR)/wasm/positions.json $(BUILD_DIR)/site/wasm/ diff --git a/docs/adr/0011-lecons-conseil-et-bilan.md b/docs/adr/0011-lecons-conseil-et-bilan.md new file mode 100644 index 0000000..83d2410 --- /dev/null +++ b/docs/adr/0011-lecons-conseil-et-bilan.md @@ -0,0 +1,59 @@ +# 0011. Leçons numérotées, conseil du maître après le coup, bilan de fin de partie + +- **Statut** : accepté +- **Date** : 2026-11-03 +- **Décideurs** : Pierre + +## Contexte + +Le cadrage (`docs/cadrage.md`) prévoit pour la V1 un coach en direct, un écran +de fin de partie et des leçons par concept, sans en fixer la forme. Les +maquettes des écrans (https://claude.ai/artifact/CQw6vbDpi6XQqVWQa4sANe), +faites avec la charte graphique (ADR 0010), en ont proposé une. Pierre en a +retenu trois éléments le 2026-11-03. + +Un point des maquettes contredisait le cadrage : le conseil apparaissait avant +que le joueur ne joue, en donnant la réponse. Pierre a tranché pour un +commentaire après le coup. + +## Décision + +- **Leçons numérotées** : un parcours de leçons dans un ordre fixe + (« Leçon 1 », « Leçon 2 »…). Chaque leçon affiche son état (à commencer, + « 3 exercices sur 5 », terminée) et sa progression. L'accueil propose de + continuer la leçon en cours. +- **Conseil du maître** : c'est la forme du coach en direct du cadrage. Il + commente le coup **après** que le joueur l'a joué, jamais avant. Il donne + l'appréciation du moteur avec un mot et une icône (bon coup, imprécision, + erreur), puis une phrase du maître qui dit quel coup il aurait joué et + pourquoi, avec un lien pour en savoir plus. L'écart d'équité n'apparaît que + sur demande. Il reste désactivable, comme le prévoit le cadrage. +- **Bilan de fin de partie** : le résultat, le décompte des coups par + appréciation (bons coups, imprécisions, erreurs), « le coup à revoir » + (la plus grosse erreur, avec sa position, le coup joué et le meilleur coup) + et le prompt prêt à copier pour une IA. + +Les maquettes de référence sont les écrans « Plateau de jeu », « Leçon et +aide » et « Fin de partie » du canevas ci-dessus. + +## Alternatives écartées + +- **Conseil avant le coup, avec la réponse** : le joueur n'essaie plus par + lui-même, et on n'apprend pas en recopiant. +- **Indice avant le coup, sans la réponse, en plus du commentaire après** : + écarté pour l'instant au profit d'un seul mécanisme simple. L'« indice » de + la liste des fonctions du cadrage reste à concevoir séparément. + +## Conséquences + +- Le commentaire après le coup demande une analyse du coup joué par le moteur + (classement des coups et écart avec le meilleur). Elle doit tenir dans le + budget de temps du téléphone, en arrière-plan, sans retarder le tour + suivant. +- Les seuils entre « bon coup », « imprécision » et « erreur » sont à fixer, + en s'appuyant sur ceux de GNU Backgammon (doubtful, bad, very bad). +- Les phrases du maître doivent rester exactes : elles s'appuient sur les + coups et les chiffres du moteur, jamais sur une explication inventée. +- Les leçons sont des contenus sous CC BY-SA 4.0, à écrire et à faire relire + par un joueur fort (cadrage, risque « contenus longs à produire ») ; leur + progression est stockée localement. diff --git a/docs/adr/0012-mode-immersif.md b/docs/adr/0012-mode-immersif.md new file mode 100644 index 0000000..706cdb4 --- /dev/null +++ b/docs/adr/0012-mode-immersif.md @@ -0,0 +1,55 @@ +# 0012. Mode immersif : le plateau en paysage, sur demande + +- **Statut** : accepté +- **Date** : 2026-11-03 +- **Décideurs** : Pierre + +## Contexte + +Sur l'écran de jeu en portrait, le plateau n'occupe qu'environ 35 % de l'écran +d'un téléphone (pions de 24 px sur un écran de 390 px de large). Pierre le +trouve peu immersif et a demandé un mode où le plateau prend le maximum de +l'écran. + +Deux pistes ont été maquettées +(https://claude.ai/artifact/CQw6vbDpi6XQqVWQa4sANe, rangée « Mode immersif ») : +le téléphone en paysage, plateau sur toute la hauteur ; ou le portrait avec le +plateau tourné d'un quart de tour. Toutes deux donnent environ 57 % de l'écran +au plateau et des pions de 30 px : le petit côté de l'écran limite la taille +des pions dans les deux cas. + +## Décision + +- Un **mode immersif en paysage** : le plateau occupe toute la hauteur, dans + son orientation habituelle. À gauche : le menu, les comptes de points et la + consigne. À droite : l'aide et les actions. Le fond est en noyer jusqu'aux + bords de l'écran. +- On y entre **par un geste explicite** : un bouton « Plein écran » sur l'écran + de jeu en portrait. On en sort par le menu ou par le retour du téléphone. +- Le bouton force la vue paysage même si le téléphone est tenu droit ou si sa + rotation automatique est verrouillée : passage en plein écran + (`requestFullscreen`), puis verrouillage de l'orientation + (`screen.orientation.lock("landscape")`). +- Si le verrouillage est refusé (navigateur ou système qui ne le permet pas, + iPhone), l'app affiche la vue paysage tournée d'un quart de tour dans l'écran + portrait. Le résultat est le même en main. +- L'écran de jeu en portrait reste la vue par défaut, pour jouer d'une main. + +## Alternatives écartées + +- **Plateau tourné en portrait** : il change la lecture du plateau (numéros de + côté, camps à gauche et à droite) et risque de dérouter les débutants habitués + à une vraie table. +- **Bascule automatique quand on tourne le téléphone** : elle ne marche pas si + la rotation est verrouillée, et elle surprend en cours de partie. Elle reste + possible en plus du bouton, plus tard. + +## Conséquences + +- Le rendu du plateau doit s'adapter à deux mises en page (portrait et paysage) + sans changer le domaine ni le moteur : seule la vue change (ADR 0007). +- `screen.orientation.lock` n'est accepté qu'en plein écran, et pas partout : + la solution de repli par rotation CSS est obligatoire, et elle doit être + testée (`make test-browser`). +- Le plein écran masque la barre d'état du téléphone, ce qui libère encore un + peu de place. diff --git a/docs/adr/README.md b/docs/adr/README.md index 0bc531c..ef00008 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -17,3 +17,5 @@ l'ancien passe au statut « remplacé par NNNN ». Voir l'ADR 0001. | [0008](0008-glossaire-et-scenarios-source-unique.md) | Glossaire et scénarios : une source unique pour le code, l'aide et les tests | accepté | | [0009](0009-socle-pwa-typescript.md) | Socle : PWA en TypeScript, moteur en WebAssembly | accepté | | [0010](0010-identite-visuelle.md) | Identité visuelle : symbole, logotype, palette | accepté | +| [0011](0011-lecons-conseil-et-bilan.md) | Leçons numérotées, conseil du maître après le coup, bilan de fin de partie | accepté | +| [0012](0012-mode-immersif.md) | Mode immersif : le plateau en paysage, sur demande | accepté | diff --git a/docs/feuille-de-route.md b/docs/feuille-de-route.md index 67637ba..1504fd8 100644 --- a/docs/feuille-de-route.md +++ b/docs/feuille-de-route.md @@ -21,7 +21,7 @@ garde-fous d'architecture (0d.1), déploiement sur https://ustagammon.sans.pub. | Sous-étape | Contenu | État | Plan | | --- | --- | --- | --- | | 0d.2 | Plateau minimal jouable (PWA hors ligne), publié ; reste la vérification avec Pierre sur le Fairphone 4 (`docs/guide-test-etape-0.md`, section « Étape 0d ») | 🔄 | | -| 0d.2+ | Montrer le coup de l'ordinateur (animation, traces, « Revoir ») : retour de Pierre | 🔄 | `plans/2026-11-03-coup-adverse.md` | +| 0d.2+ | Fluidité de la partie : supprimer ce qui interrompt le jeu sans nécessité (bouton « Nouvelle partie » toujours visible, confirmations, étapes inutiles) : demande de Pierre | ⏳ | | **Critère de fin** : une partie complète jouée sur le Fairphone 4, moins de 2 s par coup de l'IA en niveau standard, tous les tests au vert. @@ -30,7 +30,7 @@ par coup de l'IA en niveau standard, tous les tests au vert. | Étape | Contenu | État | | --- | --- | --- | -| 1. V1 Jouer et comprendre | Plateau soigné, niveaux, coach, analyse, branches, tutoriel, prompts, sauvegarde | ⏳ | +| 1. V1 Jouer et comprendre | Charte graphique appliquée à la PWA (ADR 0010, `docs/identite/charte/`), plateau soigné, niveaux, conseil du maître après le coup, bilan de fin de partie, premières leçons numérotées (ADR 0011), mode immersif en paysage (ADR 0012), analyse, branches, tutoriel, prompts, sauvegarde | ⏳ | | 2. V1.5 Apprendre | Parcours 2, exercices, progression | ⏳ | | 3. V2 Perfectionnement et IA | Parcours 3 et 4, rollouts, révision espacée | ⏳ | diff --git a/docs/identite/README.md b/docs/identite/README.md index 06f6fe3..377f704 100644 --- a/docs/identite/README.md +++ b/docs/identite/README.md @@ -47,6 +47,24 @@ python3 -I docs/identite/generer-logo.py build/police/AlegreyaSans-Bold.ttf docs Les PNG sont des rendus des SVG par Chrome sans interface (`google-chrome --headless --screenshot`), à la taille indiquée dans leur nom. +## Police de l'application + +`web/fonts/` contient Alegreya Sans en cinq graisses, réduite aux caractères +latins (français compris) et convertie en woff2, soit 143 Ko au lieu de +1,3 Mo en TTF. Pour la refaire, depuis les TTF de +https://github.com/google/fonts/tree/main/ofl/alegreyasans (Python 3, +fontTools et brotli, par exemple dans l'image `emscripten/emsdk`) : + +```sh +pip install fonttools brotli +for f in Regular Italic Medium Bold ExtraBold; do + python3 -m fontTools.subset AlegreyaSans-$f.ttf \ + --output-file=web/fonts/AlegreyaSans-$f.woff2 --flavor=woff2 \ + --unicodes="U+0020-007E,U+00A0-00FF,U+0152-0153,U+0178,U+02BC,U+02C6,U+02DC,U+2009,U+2011,U+2013-2014,U+2018-201E,U+2022,U+2026,U+202F,U+2039-203A,U+20AC,U+2192,U+2212" \ + --layout-features='*' --name-IDs='*' --name-languages='*' +done +``` + ## Charte graphique La charte complète (couleurs des deux thèmes, typographie, composants) est dans diff --git a/docs/plans/2026-11-03-coup-adverse.md b/docs/plans/2026-11-03-coup-adverse.md deleted file mode 100644 index 395997b..0000000 --- a/docs/plans/2026-11-03-coup-adverse.md +++ /dev/null @@ -1,220 +0,0 @@ -# Montrer le coup que l'ordinateur vient de jouer - -- **Date** : 2026-11-03 -- **Étape** : 0d (retour de Pierre sur le plateau publié) -- **Statut** : terminé -- **Exécutant** : sous-agent general-purpose, dans un worktree isolé - -## Objectif - -Après le coup de l'ordinateur, le joueur voit ce qui a bougé sans lire la -notation. Choix de Pierre : **animation puis traces**. - -## Consignes - -1. **Animation** : quand l'ordinateur joue, ses pions glissent un par un, un - demi-coup après l'autre (environ 0,4 s chacun), de la case de départ à la - case d'arrivée. Un pion frappé part sur la barre de façon visible. Si le - système demande moins d'animations (`prefers-reduced-motion`), pas - d'animation : on passe directement aux traces. -2. **Traces** : une fois l'animation finie, et jusqu'à la prochaine action du - joueur (lancer les dés, doubler, prendre, passer, toucher un pion) : - - une flèche de chaque case de départ vers sa case d'arrivée ; - - les pions déplacés entourés ; - - à la case de départ d'un pion parti, un pion « fantôme » (contour - seul) ; - - un pion frappé signalé sur la barre. - Les petits points orange actuels sont remplacés par ces traces. -3. **Bouton « Revoir »** à côté des boutons d'action : il rejoue l'animation du - dernier coup de l'ordinateur, puis remet les traces. Il disparaît dès que - le joueur agit. -4. **Où vit la logique** : la liste des demi-coups du dernier coup de - l'ordinateur et les positions intermédiaires viennent du domaine (le journal - contient déjà `MovePlayed` avec ses demi-coups ; ajouter si besoin une - fonction qui donne la suite des plateaux intermédiaires, à partir du - moteur, sans réimplémenter de règle : par exemple `checkHalfMoves` ou le - décodage de position à chaque étape). L'interface ne fait que dessiner. - Les garde-fous d'architecture existants doivent passer. -5. **Textes** dans `app/src/ui/texts.ts` ; mots du glossaire. -6. **Scénario** (ADR 0008) : ajouter à `docs/scenarios/jouer-une-partie.feature` - un scénario « Voir le coup de l'ordinateur », écrit en intentions (par - exemple : quand l'ordinateur a joué, alors son coup vous est montré case par - case, et vous pouvez le revoir), avec ses étapes Cucumber sur le domaine. -7. **Tests** : - - tests unitaires de ce qui est calculable hors navigateur (traces et - plateaux intermédiaires d'un coup avec et sans pion frappé, coup depuis la - barre, sortie de pions) ; - - `make test-browser` (`tests/browser/play.mjs`) : après un coup de - l'ordinateur, les flèches et les pions entourés sont présents ; « Revoir » - rejoue puis les remet ; tout disparaît à l'action suivante ; capture - d'écran `coup-adverse` ; - - `make lint`, `make test` et `make test-browser` verts. - -Contraintes : -- `CLAUDE.md`, ADR 0007, 0008, 0009 : simplicité, pas de framework ni de - bibliothèque d'animation (animations CSS ou `requestAnimationFrame`), - identifiants en anglais, commentaires et textes en français ; -- aucune règle du jeu réimplémentée ; -- l'animation ne doit pas retarder le moteur ni bloquer la saisie au-delà de - sa durée ; le temps de calcul affiché reste celui du moteur ; -- ne pas publier, ne pas pousser, ne pas créer de merge request ; -- commits petits, en français, avec la ligne - `Co-Authored-By: Claude Opus 5.5 `. - -Rapport attendu : ce qui a été fait, captures d'écran (chemins), résultat des -tests, difficultés, écarts au plan. - -## Ajout : aucun coup possible - -Demandé par Pierre en cours de tâche (inspiré de l'application CBG). - -1. Quand le jet du joueur ne permet aucun coup, le tour ne passe plus tout - seul à l'ordinateur. Les dés restent affichés, estompés ou barrés, avec un - message clair (« Aucun coup possible avec 6-6. ») et un bouton - « Continuer ». Le tour passe à l'ordinateur seulement quand le joueur le - touche. -2. Quand l'ordinateur ne peut pas jouer son jet, ses dés et un message - (« L'ordinateur ne peut pas jouer 5-5. ») restent visibles jusqu'à la - prochaine action du joueur, comme les traces d'un coup. -3. Le domaine reste la seule source, sans règle réimplémentée ; textes dans - `texts.ts`. -4. Scénario « Aucun coup possible avec votre jet » avec ses étapes sur le - domaine et des dés imposés. Dans `play.mjs`, si le cas survient, vérifier - que « Continuer » est demandé, sans forcer de cas artificiel. - -Les changements CSS se limitent aux nouvelles classes : un autre agent -travaille sur la charte. - -## Ajout : toucher les dés et le videau - -Demandé par Pierre en cours de tâche. - -1. Toucher les dés lance les dés quand c'est au joueur de lancer (même effet - que « Lancer les dés »). Sans dés à lancer affichés, la zone des dés du - joueur doit être touchable (dés « en attente »). -2. Toucher le videau propose de doubler quand le joueur en a le droit (mêmes - conditions, venues du domaine). Le reste du temps, toucher dés ou videau ne - fait rien. -3. Les boutons restent. Dés et videau touchables : cible d'au moins 44 px, - retour visuel discret, `role="button"` et `aria-label`. -4. Tests : dans `play.mjs`, lancer au moins une fois en touchant les dés et - doubler au moins une fois en touchant le videau. - -## Critères de réussite - -- Dans Chromium et Firefox sans interface, après chaque coup de l'ordinateur : - animation (ou traces directes en mouvement réduit), traces visibles, « Revoir » - fonctionnel, traces effacées à l'action suivante. -- Le scénario « Voir le coup de l'ordinateur » passe dans `make test`. -- `make lint`, `make test` et `make test-browser` passent. - -## Résultat - -Terminé le 2026-11-03, branche `coup-adverse-travail`. - -**Domaine** (`app/src/domain/game/game.ts`) : `Game.lastMoveSteps()` donne le -dernier coup joué demi-coup par demi-coup (`MoveSteps` : camp, demi-coups, -plateaux successifs, pions frappés). Les plateaux intermédiaires viennent du -moteur : `checkHalfMoves` sur les premiers demi-coups depuis la position -d'avant le coup, puis `board`. Un pion frappé se lit sur le plateau du moteur -(la barre adverse grossit), aucune règle n'est rejouée. Le format des -événements ne change pas. - -**Interface** : -- `board-model.ts` : `moveTrace()` calcule les traces (une flèche par - demi-coup, pions arrivés, pions partis, pions frappés). Un pion qui fait - étape (13/7 7/4) ne laisse rien sur la case d'étape. Testé sous Node. -- `board-view.ts` : traces dessinées en laiton (`--last-move`, comme le - demande la charte) : flèches avec pointe, pions arrivés entourés, pion - fantôme en pointillés à la case de départ, pion frappé entouré en - pointillés sur la barre. Les petits points orange ont disparu. La - géométrie des piles est regroupée dans `slotCenter()`. Le pion du dessus de - chaque endroit porte `data-checker`. `slideChecker()` le fait glisser avec - `Element.animate` (API du navigateur, sans bibliothèque). -- `app.ts` : après le tour de l'ordinateur, le plateau est redessiné à - chaque étape et le pion arrivé glisse en 0,4 s depuis sa case de départ. Un - pion frappé glisse en même temps vers la barre. Ensuite viennent les traces - et le bouton « Revoir ». Tout s'efface à l'action suivante (lancer, - doubler, prendre, passer, nouvelle partie, toucher un pion). Avec - `prefers-reduced-motion: reduce`, les traces s'affichent directement. - L'animation commence une fois le moteur fini : le temps affiché reste celui - du moteur. Les touches sont ignorées pendant l'animation (au plus 0,4 s par - demi-coup). Une partie reprise juste après le coup de l'ordinateur montre - ses traces sans l'animer. -- Textes `replay` et `replayTitle` dans `texts.ts`, bouton `#replay` dans - `web/index.html`. - -**Scénario** « Voir le coup de l'ordinateur » dans -`docs/scenarios/jouer-une-partie.feature`. Il part d'une position où le seul -coup permis de l'ordinateur avec 3-1 frappe votre blot (13/10* 10/9), avec le -videau chez vous pour qu'il ne double pas. Ses étapes vérifient les plateaux -case par case, le pion frappé qui part sur la barre et un second appel -identique (« revoir »). - -**Tests** : -- `app/tests/move-steps.test.ts` (vrai moteur) : coup simple, pion frappé, - coup depuis la barre qui frappe en rentrant, sortie jusqu'à la fin de - partie, coup de l'ordinateur, aucun coup. -- `app/tests/ui.test.ts` : traces d'un coup simple, d'un pion qui fait - étape, d'une rentrée avec un pion frappé, d'une sortie. -- `tests/browser/play.mjs` : après chaque action, les traces sont là si le - dernier coup est celui de l'ordinateur (autant de flèches que de - demi-coups, au moins un pion entouré et un fantôme, « Revoir » visible), et - absentes sinon. « Revoir » est essayé deux fois. La première fois, des - animations tournent et les flèches disparaissent pendant l'animation, puis - les mêmes traces reviennent ; une capture `coup-adverse` est prise. La - seconde fois, en mouvement réduit, aucune animation ne tourne et les traces - reviennent. -**Ajout : aucun coup possible** : -- Domaine : `Game.roll()` ne fait plus passer le tour. `Game.cannotMove()` - dit, d'après `legalMoves` du moteur, que le jet ne permet aucun coup. - `Game.skipTurn()` enregistre alors le coup vide (même événement - `MovePlayed` sans demi-coup qu'avant, format inchangé) ; il est refusé s'il - existe un coup. Le nom `skipTurn` évite la confusion avec « passer » (le - double), qui est `drop` dans le glossaire. -- Interface : bouton « Continuer » (`#skip`), message « Aucun coup possible - avec 6-5. », consigne « Touchez « Continuer » : le tour passe à - l'ordinateur. », dés estompés et barrés (`.die-blocked`, `.die-strike`). - Quand l'ordinateur ne peut pas jouer, ses dés restent barrés avec « L'ordinateur - ne peut pas jouer 5-5. » jusqu'à l'action suivante. Ces deux messages se - déduisent du journal : ils survivent à un rechargement. -- Tests : scénario « Aucun coup possible avec votre jet » (pion sur la barre - face aux six cases tenues, 6-5), tests unitaires `cannotMove`/`skipTurn` - (faux moteur), partie complète adaptée. Dans le navigateur, « Continuer » - a été demandé 1 fois (Chromium) et 3 fois (Firefox) : message et deux dés - barrés vérifiés à chaque fois. Le jet sans coup de l'ordinateur est vérifié - quand il survient (pas survenu dans ces deux parties). - -**Ajout : toucher les dés et le videau** : -- Quand vous pouvez lancer, deux dés « en attente » (faces vides, contour en - pointillés) apparaissent au milieu de la moitié gauche. La moitié droite - garde le dernier jet de l'ordinateur, qui doit rester visible. Quand vous - pouvez doubler (`Game.canDouble()`, comme le bouton), le videau est souligné. -- Les toucher clique le bouton correspondant (`#roll`, `#double`) : mêmes - conditions, même effet. Hors de ces moments, aucune zone n'est posée. -- Zones à toucher : `role="button"`, `aria-label` (« Lancer les dés », - « Doubler »), au moins 88 unités, soit 47 px sur le plateau de 344 px d'un - téléphone de 360 px. Les boutons restent pour le clavier ; les zones n'ont - pas de `tabindex`, pour ne pas doubler l'ordre de tabulation. -- Navigateur : dés touchés 10 fois (Chromium) et 14 fois (Firefox), videau - touché pour doubler, taille et rôle vérifiés ; aucune zone pendant la - saisie d'un coup. - -**Résultats finaux** (dernier commit vérifié) : `make lint` sans problème ; -`make test` 10/10 ; `make test-browser` vert sous Chromium et Firefox, sans -violation CSP ni erreur dans la page. Le coup de l'ordinateur a été montré 21 -et 30 fois. Dans Firefox, un coup revu a frappé deux pions, tous deux signalés sur la -barre. - -**Limites** : -- Un premier essai de `make test-browser` a échoué dans `docker build` - (« parent snapshot … does not exist »), sans lien avec le code - (probablement une autre construction Docker en parallèle). L'essai suivant - est passé. -- Firefox affiche des tours de l'ordinateur de 3 à 4,4 s au plus. Ce temps - est mesuré avant l'animation, qui n'y est pour rien : c'est le moteur sous - Firefox sur le PC de 2011. Chromium : 0,6 s au plus. -- Les dés en attente sont à gauche alors que le jet lancé s'affiche à - droite : c'est un choix d'interface, à revoir avec la charte si besoin. -- Sur une case de plus de cinq pions, c'est le cinquième pion dessiné qui - glisse (le nombre reste affiché sur la pile). diff --git a/scripts/make-icons.py b/scripts/make-icons.py deleted file mode 100644 index bd42c57..0000000 --- a/scripts/make-icons.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env python3 -# Produit les icônes PNG de l'application (web/icons/icon-192.png et -# icon-512.png) à partir des formes de web/icons/icon.svg : un fond de bois -# et deux pions. Icônes provisoires, en attendant l'identité définitive. -# -# Python seul, sans bibliothèque : chaque pixel est échantillonné 4 × 4 fois -# pour adoucir les bords. À relancer seulement si les formes changent, dans -# la chaîne C (Python 3) : -# docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/src" -w /src \ -# ustagammon/build-c:2 python3 scripts/make-icons.py -# -# Copyright (C) 2026 les auteurs d'Ustagammon. -# Licence : GPLv3 ou ultérieure (voir LICENSE). - -import struct -import zlib - -BACKGROUND = (0x6B, 0x44, 0x26) -# Pions dessinés dans l'ordre : (centre x, centre y, rayon, remplissage, -# bord), en unités d'une icône de 512 pixels ; bord de 10 unités. -CHECKERS = [ - (300, 300, 120, (0x2B, 0x22, 0x1C), (0x0D, 0x0A, 0x08)), - (212, 212, 120, (0xFB, 0xF6, 0xEA), (0x7D, 0x6A, 0x52)), -] -EDGE = 10 -SAMPLES = 4 - - -def color_at(x, y): - """Couleur d'un point de l'icône, en unités de 512.""" - color = BACKGROUND - for cx, cy, r, fill, edge in CHECKERS: - d2 = (x - cx) ** 2 + (y - cy) ** 2 - if d2 <= (r + EDGE / 2) ** 2: - color = fill if d2 <= (r - EDGE / 2) ** 2 else edge - return color - - -def render(size): - scale = 512 / size - rows = [] - for py in range(size): - row = bytearray([0]) # filtre PNG « aucun » - for px in range(size): - total = [0, 0, 0] - for sy in range(SAMPLES): - for sx in range(SAMPLES): - x = (px + (sx + 0.5) / SAMPLES) * scale - y = (py + (sy + 0.5) / SAMPLES) * scale - for i, c in enumerate(color_at(x, y)): - total[i] += c - row += bytes(round(t / SAMPLES**2) for t in total) - rows.append(bytes(row)) - return b"".join(rows) - - -def png(size, pixels): - def chunk(kind, data): - body = kind + data - return struct.pack(">I", len(data)) + body + struct.pack(">I", zlib.crc32(body)) - - header = struct.pack(">IIBBBBB", size, size, 8, 2, 0, 0, 0) # RVB, 8 bits - return ( - b"\x89PNG\r\n\x1a\n" - + chunk(b"IHDR", header) - + chunk(b"IDAT", zlib.compress(pixels, 9)) - + chunk(b"IEND", b"") - ) - - -for size in (192, 512): - with open(f"web/icons/icon-{size}.png", "wb") as f: - f.write(png(size, render(size))) diff --git a/tests/lint.sh b/tests/lint.sh index 7406a01..18f705f 100755 --- a/tests/lint.sh +++ b/tests/lint.sh @@ -29,10 +29,11 @@ for f in "${fichiers[@]}"; do if grep -q $'\r' "$f"; then signaler "$f : fins de ligne Windows (CRLF)" fi - # Copies fidèles de GNU Backgammon (sources et données) : on ne - # reformate pas leurs espaces, pour rester identique à l'original. + # Copies fidèles de GNU Backgammon (sources et données) et de la licence + # de la police : on ne reformate pas leurs espaces, pour rester + # identique à l'original. case "$f" in - engine/gnubg/* | engine/data/*) continue ;; + engine/gnubg/* | engine/data/* | web/fonts/OFL.txt) continue ;; esac if grep -nE '[[:blank:]]+$' "$f" >/dev/null; then signaler "$f : espaces en fin de ligne (lignes $(grep -nE '[[:blank:]]+$' "$f" | cut -d: -f1 | paste -sd, -))" diff --git a/tests/test_site.sh b/tests/test_site.sh index e394931..ff3b846 100755 --- a/tests/test_site.sh +++ b/tests/test_site.sh @@ -43,8 +43,28 @@ check "app/main.js" "text/javascript" check "app/ui/app.js" "text/javascript" check "app/domain/game/game.js" "text/javascript" check "manifest.webmanifest" "application/manifest\+json" -check "icons/icon-192.png" "image/png" -check "icons/icon-512.png" "image/png" +# Charte graphique (ADR 0010) : icônes et logo copiés de docs/identite/logo/, +# police Alegreya Sans et sa licence. +check "icons/favicon.svg" "image/svg\+xml" +check "icons/favicon-32.png" "image/png" +check "icons/apple-touch-icon-180.png" "image/png" +check "icons/logo-horizontal-couleur.svg" "image/svg\+xml" +check "icons/logo-horizontal-sombre.svg" "image/svg\+xml" +check "icons/symbole-couleur.svg" "image/svg\+xml" +check "icons/symbole-sombre.svg" "image/svg\+xml" +cmp -s <(curl -s "$base/icons/icone-512.png") docs/identite/logo/icone-512.png \ + || report "/icons/icone-512.png diffère de docs/identite/logo/" +for weight in Regular Italic Medium Bold ExtraBold; do + check "fonts/AlegreyaSans-$weight.woff2" "font/woff2" +done +check "fonts/OFL.txt" "text/plain" +# Chaque fichier désigné par la page (icônes, police, style) est servi. +page="$(curl -s "$base/")" +while read -r ref; do + curl -sf -o /dev/null "$base/$ref" || report "index.html : $ref introuvable" +done < <(grep -oE '(href|src|srcset)="[^"]*"' <<<"$page" | sed -E 's/^[a-z]+="(.*)"$/\1/') +grep -qF "url(\"fonts/AlegreyaSans-Regular.woff2\")" <(curl -s "$base/app.css") \ + || report "app.css : police Alegreya Sans non déclarée" check "sw.js" "text/javascript" check "engine-worker.mjs" "text/javascript" check "wasm/engine.mjs" "text/javascript" @@ -65,6 +85,8 @@ cmp -s <(curl -s "$base/docs/glossaire/videau.md") docs/glossaire/videau.md \ # Le manifeste désigne des icônes servies, et l'application à la racine. manifest="$(curl -s "$base/manifest.webmanifest")" grep -q '"start_url": "./"' <<<"$manifest" || report "manifeste : start_url absent" +grep -q '"theme_color": "#4A2E1F"' <<<"$manifest" || report "manifeste : theme_color autre que le noyer" +grep -q '"purpose": "maskable"' <<<"$manifest" || report "manifeste : pas d'icône maskable" while read -r icon; do curl -sf -o /dev/null "$base/$icon" || report "manifeste : icône $icon introuvable" done < <(grep -o '"src": "[^"]*"' <<<"$manifest" | sed 's/"src": "\(.*\)"/\1/') @@ -73,7 +95,7 @@ done < <(grep -o '"src": "[^"]*"' <<<"$manifest" | sed 's/"src": "\(.*\)"/\1/') sw="$(curl -s "$base/sw.js")" grep -qE '^const VERSION = "[0-9a-f]{16}";' <<<"$sw" || report "sw.js : version absente" for file in ./ index.html app/main.js wasm/engine.data wasm/engine.wasm moteur/ \ - docs/glossaire/videau.md version.json; do + docs/glossaire/videau.md version.json fonts/AlegreyaSans-Bold.woff2 icons/favicon.svg; do grep -qF " \"$file\"," <<<"$sw" || report "sw.js : $file absent de la liste à mettre en cache" done listed="$(grep -c '^ "' <<<"$sw")" diff --git a/web/app.css b/web/app.css index dc8ad38..372535e 100644 --- a/web/app.css +++ b/web/app.css @@ -1,81 +1,191 @@ -/* Style de l'application. Palette provisoire et sobre (bois, ivoire, un - * accent vert), en attendant l'identité définitive : toutes les couleurs et - * tailles sont des variables, redéfinies pour le thème sombre. */ +/* Style de l'application, selon la charte graphique (ADR 0010, + * docs/identite/charte/) : les couleurs, tailles et rayons sont les + * variables de tokens.json, sous le même nom, redéfinies pour le thème + * sombre ; les composants reprennent bundle.css. */ + +/* ------------------------------------------------------------ Police */ + +/* Alegreya Sans (SIL Open Font License 1.1, voir fonts/OFL.txt), réduite + * aux caractères latins et convertie en woff2 (voir docs/identite/README.md). + * Embarquée dans le site, donc dans le cache du service worker : elle + * fonctionne hors ligne. */ +@font-face { + font-family: "Alegreya Sans"; + src: url("fonts/AlegreyaSans-Regular.woff2") format("woff2"); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "Alegreya Sans"; + src: url("fonts/AlegreyaSans-Italic.woff2") format("woff2"); + font-weight: 400; + font-style: italic; + font-display: swap; +} + +@font-face { + font-family: "Alegreya Sans"; + src: url("fonts/AlegreyaSans-Medium.woff2") format("woff2"); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "Alegreya Sans"; + src: url("fonts/AlegreyaSans-Bold.woff2") format("woff2"); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "Alegreya Sans"; + src: url("fonts/AlegreyaSans-ExtraBold.woff2") format("woff2"); + font-weight: 800; + font-style: normal; + font-display: swap; +} + +/* ------------------------------------------------------------ Couleurs */ :root { - --text: #2b211a; - --muted: #6b5d50; - --background: #f7f2e8; - --accent: #2e6b4f; + --background: #f5efe5; + --surface: #fffcf6; + --surface-sunken: #ece3d4; + --text: #2b1d15; + --muted: #6b5c50; + --border: #ded2c0; + --border-strong: #8e7d6a; + --accent: #a8432b; --on-accent: #ffffff; - --border: #cbbfae; - --error: #a3261b; + --accent-soft: #f1d9cf; + --focus: #a8432b; + --error: #8c1c3a; + --verdict-good: #2f6b4f; + --verdict-doubtful: #7e5a0f; + --verdict-bad: #8c1c3a; + --brand-noyer: #4a2e1f; + --brand-ivoire: #f3ead8; + --brand-laiton: #8f6a1c; - --frame: #6b4426; - --bed: #e6cfa6; - --bar: #5c3a20; - --tray: #57371e; - --point-a: #9a6439; - --point-b: #c79b69; - --point-number: #f3e7d0; - --checker-player: #fbf6ea; - --checker-player-edge: #7d6a52; - --checker-computer: #2b221c; - --checker-computer-edge: #0d0a08; - --count-player: #2b211a; - --count-computer: #fbf6ea; - --die-player: #fbf6ea; - --pip-player: #2b211a; - --die-computer: #2b221c; - --pip-computer: #fbf6ea; - --cube: #fbf6ea; - --cube-text: #2b211a; - --highlight: #2e6b4f; - --target: rgba(46, 107, 79, 0.35); - --last-move: #d9822b; + --frame: #4a2e1f; + --bed: #e9dcc4; + --bar: #3f2619; + --tray: #3a2418; + --point-a: #6b4430; + --point-b: #c9ae88; + --point-number: #f3ead8; + --checker-player: #f7f0e2; + --checker-player-edge: #8c7a62; + --checker-computer: #2e1c12; + --checker-computer-edge: #120a05; + --count-player: #2b1d15; + --count-computer: #f3ead8; + --die-player: #f7f0e2; + --pip-player: #2b1d15; + --die-computer: #2e1c12; + --pip-computer: #f3ead8; + --cube: #f7f0e2; + --cube-text: #2b1d15; + --highlight: #a8432b; + --target: rgba(168, 67, 43, 0.32); + /* Dernier coup joué (marques et traces) : le laiton, rien d'autre. */ + --last-move: var(--brand-laiton); - --font-size: 17px; + /* Numéros des flèches. La charte les pose sur le cadre (point-number, + * ivoire) ; notre plateau les pose sur le fond, où l'ivoire ne se lirait + * pas : texte secondaire en thème clair (4,7:1 sur bed), point-number en + * thème sombre (6,3:1). */ + --point-label: var(--muted); + + --shadow-raised: 0 1px 2px rgba(43, 29, 21, 0.1); + + --font-sans: "Alegreya Sans", "Gill Sans", "Segoe UI", system-ui, sans-serif; + + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-6: 24px; + --space-8: 32px; --button-height: 48px; - --radius: 8px; --page-width: 40rem; + --radius-sm: 6px; + --radius-md: 12px; + --radius-pill: 999px; + + /* Hauteur de la barre haute : boutons de 44 px et marges. */ + --topbar-height: 60px; } @media (prefers-color-scheme: dark) { :root { - --text: #ece4d6; - --muted: #b3a796; - --background: #1b1714; - --accent: #6fc3a0; - --on-accent: #10221a; - --border: #4d443b; - --error: #f08a7e; + --background: #17110d; + --surface: #221913; + --surface-sunken: #2c211a; + --text: #efe5d5; + --muted: #a99886; + --border: #3a2d24; + --border-strong: #7d6b5b; + --accent: #d0694b; + --on-accent: #1a110c; + --accent-soft: #4a2519; + --focus: #e58a6e; + --error: #f08fa6; + --verdict-good: #7ccba4; + --verdict-doubtful: #e0b04f; + --verdict-bad: #f08fa6; + --brand-laiton: #d6a64a; - --frame: #3a2616; - --bed: #6d5136; - --bar: #2f1f12; - --tray: #2c1d11; - --point-a: #4a3120; - --point-b: #8b6943; - --point-number: #d9cbb4; - --checker-player: #efe6d2; + --frame: #2a1a10; + --bed: #4a3626; + --bar: #24170e; + --tray: #22150d; + --point-a: #3a2519; + --point-b: #7e6142; + --point-number: #cdbfa8; + --checker-player: #ede3cf; --checker-player-edge: #8e7c64; - --checker-computer: #17110d; + --checker-computer: #140d09; --checker-computer-edge: #a8957d; - --highlight: #7fd6b0; - --target: rgba(127, 214, 176, 0.35); - --last-move: #f0a050; + --count-player: #2b1d15; + --count-computer: #f3ead8; + --die-player: #ede3cf; + --pip-player: #2b1d15; + --die-computer: #140d09; + --pip-computer: #ede3cf; + --cube: #ede3cf; + --cube-text: #2b1d15; + --highlight: #e07a5a; + --target: rgba(224, 122, 90, 0.38); + + --point-label: var(--point-number); + + --shadow-raised: 0 1px 2px rgba(0, 0, 0, 0.4); } } +/* -------------------------------------------------------------- Page */ + * { box-sizing: border-box; } +html { + -webkit-text-size-adjust: 100%; + text-size-adjust: 100%; +} + body { margin: 0 auto; max-width: var(--page-width); - padding: 0 0.5rem 1rem; - font: var(--font-size) / 1.45 system-ui, sans-serif; + padding: 0 var(--space-4) var(--space-6); + font-family: var(--font-sans); + font-size: 17px; + line-height: 25px; color: var(--text); background: var(--background); } @@ -84,78 +194,148 @@ body { display: none !important; } +a { + color: var(--accent); + text-underline-offset: 3px; +} + +/* Anneau de focus clavier, sur tout élément interactif. */ +:focus-visible { + outline: 3px solid var(--focus); + outline-offset: 2px; +} + +/* -------------------------------------------------------- Barre haute */ + .topbar { display: flex; - gap: 0.5rem; + gap: var(--space-2); align-items: center; - padding: 0.5rem 0; + min-height: var(--topbar-height); + padding-block: var(--space-2); } -.topbar h1 { +.topbar .brand { flex: 1; + display: flex; + align-items: center; + min-width: 0; margin: 0; - font-size: 1.25rem; } +.topbar .brand img { + display: block; + width: auto; + height: 32px; +} + +/* ------------------------------------------------------------ Boutons */ + +/* Ronds comme des pions ; un seul bouton principal (brique) par écran, + * les autres en contour. */ button { min-height: var(--button-height); - padding: 0 1rem; + padding: 0 20px; font: inherit; - font-weight: 600; + font-weight: 700; color: var(--on-accent); background: var(--accent); - border: 1px solid var(--accent); - border-radius: var(--radius); + border: 2px solid var(--accent); + border-radius: var(--radius-pill); + cursor: pointer; + /* Pas de délai ni de zoom sur un double toucher. */ + touch-action: manipulation; + -webkit-tap-highlight-color: transparent; } button.secondary { color: var(--text); background: transparent; - border-color: var(--border); + border-color: var(--border-strong); } -.topbar button { - min-height: 40px; - padding: 0 0.75rem; - font-size: 0.9rem; +@media (hover: hover) { + button.secondary:hover:not(:disabled) { + background: var(--surface-sunken); + } +} + +/* Retour visuel au toucher. */ +button:active:not(:disabled) { + transform: translateY(1px); } button:disabled { opacity: 0.45; + cursor: default; +} + +.topbar button { + min-height: 44px; + padding: 0 14px; + font-size: 15px; + line-height: 20px; } .actions { display: flex; flex-wrap: wrap; - gap: 0.5rem; + gap: var(--space-2); } .actions button { - flex: 1; + flex: 1 1 auto; } +/* --------------------------------------------- Messages sous le plateau */ + +/* Compte de points : information secondaire, discrète. */ .pips { - margin: 0.25rem 0 0; - font-size: 0.85rem; + margin: var(--space-2) 0 0; + font-size: 15px; + line-height: 20px; color: var(--muted); text-align: center; + font-variant-numeric: tabular-nums lining-nums; } -.info, -.prompt { - min-height: 1.45em; - margin: 0.4rem 0; -} - +/* Ce qui vient de se passer (coup de l'ordinateur, jet d'ouverture) : + * secondaire, mais lisible. */ .info { + min-height: 25px; + margin: var(--space-3) 0 0; color: var(--muted); } -.prompt { - font-weight: 600; +.info:empty { + margin-top: 0; } -/* L'ordinateur réfléchit : trois points qui apparaissent l'un après l'autre. */ +/* La consigne du moment : ce qui doit ressortir de l'écran. Fond d'accent + * léger quand c'est au joueur, neutre pendant que l'ordinateur réfléchit. */ +.prompt { + min-height: calc(25px + 2 * var(--space-3)); + margin: var(--space-3) 0; + padding: var(--space-3) var(--space-4); + font-weight: 700; + background: var(--accent-soft); + border-radius: var(--radius-md); +} + +.prompt.thinking, +body.busy .prompt { + font-weight: 400; + background: var(--surface-sunken); +} + +/* Fin de partie : le résultat en titre. */ +#game:has(#again:not([hidden])) .prompt { + font-size: 24px; + line-height: 30px; +} + +/* L'ordinateur réfléchit : trois points qui apparaissent l'un après + * l'autre (la seule animation en boucle). */ .prompt.thinking::after { content: ""; animation: thinking 1.2s steps(4, end) infinite; @@ -168,6 +348,17 @@ button:disabled { 75% { content: "..."; } } +@media (prefers-reduced-motion: reduce) { + .prompt.thinking::after { + animation: none; + content: "…"; + } + + button:active:not(:disabled) { + transform: none; + } +} + /* ------------------------------------------------------------ Plateau */ .board { @@ -176,7 +367,23 @@ button:disabled { height: auto; aspect-ratio: 640 / 520; user-select: none; + -webkit-user-select: none; + touch-action: manipulation; -webkit-tap-highlight-color: transparent; + font-family: var(--font-sans); + /* Alegreya Sans dessine par défaut des chiffres elzéviriens (de hauteurs + * variables) : sur le plateau, des chiffres alignés se lisent mieux. */ + font-variant-numeric: tabular-nums lining-nums; +} + +/* Téléphone en portrait : le plateau, héros de l'écran, prend la largeur + * presque entière (8 px de marge au lieu de 16) ; chaque pixel compte pour + * toucher un pion. */ +@media (max-width: 40rem) and (orientation: portrait) { + .board { + width: calc(100% + 2 * (var(--space-4) - var(--space-2))); + margin-inline: calc(var(--space-2) - var(--space-4)); + } } .board-frame { fill: var(--frame); } @@ -187,8 +394,9 @@ button:disabled { .point-b { fill: var(--point-b); } .point-number { - fill: var(--muted); + fill: var(--point-label); font-size: 13px; + font-weight: 500; text-anchor: middle; } @@ -206,14 +414,24 @@ button:disabled { stroke: var(--checker-computer-edge); } +/* Filet intérieur des pions (bundle.css), prêt pour le jour où le plateau + * le dessine. */ +.checker-ring { + fill: none; + stroke-width: 1.5; +} + +.ring-player { stroke: var(--checker-player-edge); } +.ring-computer { stroke: var(--checker-computer-edge); } + .checker.movable { stroke: var(--highlight); - stroke-width: 4; + stroke-width: 3; } .checker.selected { stroke: var(--highlight); - stroke-width: 7; + stroke-width: 6; } .checker-count { @@ -254,8 +472,8 @@ button:disabled { stroke-dasharray: 5 4; } -.die-player { fill: var(--die-player); stroke: var(--checker-player-edge); } -.die-computer { fill: var(--die-computer); stroke: var(--checker-computer-edge); } +.die-player { fill: var(--die-player); stroke: var(--checker-player-edge); stroke-width: 1.5; } +.die-computer { fill: var(--die-computer); stroke: var(--checker-computer-edge); stroke-width: 1.5; } .pip-player { fill: var(--pip-player); } .pip-computer { fill: var(--pip-computer); } @@ -286,7 +504,7 @@ button:disabled { .cube-value { fill: var(--cube-text); font-size: 17px; - font-weight: 700; + font-weight: 800; text-anchor: middle; } @@ -297,70 +515,138 @@ button:disabled { /* --------------------------------------------------------------- Aide */ +.help { + max-width: var(--page-width); + margin: 0 auto; +} + .help h2 { - margin-top: 1.5rem; - font-size: 1.2rem; + margin: var(--space-8) 0 var(--space-2); + font-size: 24px; + line-height: 30px; + font-weight: 700; } .help h3 { - margin-bottom: 0.25rem; - font-size: 1rem; + margin: var(--space-6) 0 var(--space-1); + font-size: 20px; + line-height: 26px; + font-weight: 700; +} + +.help p { + margin: var(--space-2) 0; } .steps { - padding-left: 1.25rem; + margin: var(--space-2) 0; + padding-left: var(--space-6); +} + +.steps li + li { + margin-top: var(--space-1); +} + +.steps li::marker { + color: var(--muted); +} + +/* Glossaire : une liste de définitions, sur une carte. */ +.glossary { + margin: var(--space-3) 0 0; + padding: var(--space-1) var(--space-4); + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-md); + box-shadow: var(--shadow-raised); } .glossary dt { - margin-top: 0.6rem; - font-weight: 600; + padding-top: var(--space-3); + font-weight: 700; } .glossary dd { margin: 0; + padding-bottom: var(--space-3); +} + +.glossary dd + dt { + border-top: 1px solid var(--border); } .about { - margin-top: 2rem; - font-size: 0.85rem; + margin-top: var(--space-8); + font-size: 15px; + line-height: 20px; color: var(--muted); } +/* Une cible tactile confortable pour le lien. */ .about a { - color: var(--accent); + display: inline-block; + padding-block: 12px; } /* ------------------------------------------------------- Mode paysage */ -/* Téléphone tourné : le plateau prend toute la hauteur disponible, et les - * messages et boutons passent dans une colonne à sa droite. */ +/* Téléphone tourné : le plateau prend toute la hauteur de l'écran, à + * gauche ; la barre haute, les messages et les boutons passent dans une + * colonne à sa droite. */ @media (orientation: landscape) and (max-height: 40rem) { body { max-width: none; + padding: 0 var(--space-4); } - .topbar { - padding: 0.25rem 0; - } - - #game { + /* Le jeu en deux colonnes : #game laisse ses enfants dans la grille de + * la page. Quand l'aide est affichée, #game est caché, la première + * colonne n'a plus de largeur et l'aide prend toute la place. */ + body { display: grid; - grid-template-columns: auto minmax(12rem, 1fr); - grid-template-rows: auto auto auto 1fr; - column-gap: 1rem; + grid-template-columns: auto minmax(13rem, 1fr); + grid-template-rows: auto auto auto auto auto 1fr; + column-gap: var(--space-4); align-items: start; } + #game { + display: contents; + } + + .topbar { + grid-column: 2; + grid-row: 1; + min-height: 0; + padding-block: var(--space-2) 0; + } + .board { - grid-row: 1 / 5; + grid-column: 1; + grid-row: 1 / -1; width: auto; - /* Hauteur de l'écran moins la barre du haut (boutons de 40 px). */ - height: calc(100vh - 3.5rem); - height: calc(100dvh - 3.5rem); + height: calc(100vh - 2 * var(--space-2)); + height: calc(100dvh - 2 * var(--space-2)); max-width: 70vw; + margin-block: var(--space-2); + } + + .pips, + .info, + .prompt, + .actions { + grid-column: 2; } .pips { text-align: left; } + + .actions { + margin-bottom: var(--space-2); + } + + .help { + grid-column: 1 / -1; + } } diff --git a/web/fonts/AlegreyaSans-Bold.woff2 b/web/fonts/AlegreyaSans-Bold.woff2 new file mode 100644 index 0000000..dfae42e Binary files /dev/null and b/web/fonts/AlegreyaSans-Bold.woff2 differ diff --git a/web/fonts/AlegreyaSans-ExtraBold.woff2 b/web/fonts/AlegreyaSans-ExtraBold.woff2 new file mode 100644 index 0000000..bdbc2d2 Binary files /dev/null and b/web/fonts/AlegreyaSans-ExtraBold.woff2 differ diff --git a/web/fonts/AlegreyaSans-Italic.woff2 b/web/fonts/AlegreyaSans-Italic.woff2 new file mode 100644 index 0000000..bbf7480 Binary files /dev/null and b/web/fonts/AlegreyaSans-Italic.woff2 differ diff --git a/web/fonts/AlegreyaSans-Medium.woff2 b/web/fonts/AlegreyaSans-Medium.woff2 new file mode 100644 index 0000000..2cd3dac Binary files /dev/null and b/web/fonts/AlegreyaSans-Medium.woff2 differ diff --git a/web/fonts/AlegreyaSans-Regular.woff2 b/web/fonts/AlegreyaSans-Regular.woff2 new file mode 100644 index 0000000..5a022fb Binary files /dev/null and b/web/fonts/AlegreyaSans-Regular.woff2 differ diff --git a/web/fonts/OFL.txt b/web/fonts/OFL.txt new file mode 100644 index 0000000..93a573f --- /dev/null +++ b/web/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2013 The Alegreya Sans Project Authors (https://github.com/huertatipografica/Alegreya-Sans) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/web/icons/icon-192.png b/web/icons/icon-192.png deleted file mode 100644 index 82f3f1b..0000000 Binary files a/web/icons/icon-192.png and /dev/null differ diff --git a/web/icons/icon-512.png b/web/icons/icon-512.png deleted file mode 100644 index febd9a4..0000000 Binary files a/web/icons/icon-512.png and /dev/null differ diff --git a/web/icons/icon.svg b/web/icons/icon.svg deleted file mode 100644 index d4902fc..0000000 --- a/web/icons/icon.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/web/index.html b/web/index.html index 70734e6..6b3d0aa 100644 --- a/web/index.html +++ b/web/index.html @@ -4,17 +4,31 @@ - + Ustagammon - + + + + + - +
-

Ustagammon

+ +

+ + + + + Ustagammon + +

diff --git a/web/manifest.webmanifest b/web/manifest.webmanifest index 18eb96d..95e0ad3 100644 --- a/web/manifest.webmanifest +++ b/web/manifest.webmanifest @@ -7,12 +7,13 @@ "scope": "./", "display": "standalone", "orientation": "any", - "background_color": "#f7f2e8", - "theme_color": "#6b4426", + "background_color": "#F5EFE5", + "theme_color": "#4A2E1F", "icons": [ - { "src": "icons/icon-192.png", "sizes": "192x192", "type": "image/png" }, - { "src": "icons/icon-512.png", "sizes": "512x512", "type": "image/png" }, - { "src": "icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }, - { "src": "icons/icon.svg", "sizes": "any", "type": "image/svg+xml" } + { "src": "icons/icone-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" }, + { "src": "icons/icone-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" }, + { "src": "icons/icone-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, + { "src": "icons/icone-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }, + { "src": "icons/icone.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any" } ] }