Coup de l'ordinateur montré, aucun coup possible, dés et videau touchables
Fusion du travail du sous-agent (branche coup-adverse-travail) avec la charte graphique de main ; plan supprimé à la clôture, son résultat est repris dans la merge request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+8
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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é |
|
||||
|
||||
@@ -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 | ⏳ |
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <noreply@anthropic.com>`.
|
||||
|
||||
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).
|
||||
@@ -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)))
|
||||
+4
-3
@@ -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, -))"
|
||||
|
||||
+25
-3
@@ -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")"
|
||||
|
||||
+396
-110
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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.
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.1 KiB |
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<!-- Icône provisoire (pas un logo) : deux pions sur du bois. Les PNG sont
|
||||
produits à partir des mêmes formes par scripts/make-icons.py. -->
|
||||
<rect width="512" height="512" fill="#6b4426"/>
|
||||
<circle cx="300" cy="300" r="120" fill="#2b221c" stroke="#0d0a08" stroke-width="10"/>
|
||||
<circle cx="212" cy="212" r="120" fill="#fbf6ea" stroke="#7d6a52" stroke-width="10"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 449 B |
+18
-4
@@ -4,17 +4,31 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="theme-color" content="#6b4426">
|
||||
<meta name="theme-color" content="#4A2E1F">
|
||||
<title>Ustagammon</title>
|
||||
<link rel="manifest" href="manifest.webmanifest">
|
||||
<link rel="icon" href="icons/icon.svg" type="image/svg+xml">
|
||||
<link rel="icon" href="icons/favicon.svg" type="image/svg+xml">
|
||||
<link rel="icon" href="icons/favicon-32.png" sizes="32x32" type="image/png">
|
||||
<link rel="apple-touch-icon" href="icons/apple-touch-icon-180.png">
|
||||
<link rel="preload" href="fonts/AlegreyaSans-Regular.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="fonts/AlegreyaSans-Bold.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="stylesheet" href="app.css">
|
||||
<script type="module" src="app/main.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<body class="busy">
|
||||
<!-- Les textes des boutons et des messages viennent de app/src/ui/texts.ts. -->
|
||||
<header class="topbar">
|
||||
<h1>Ustagammon</h1>
|
||||
<!-- Logo de la charte (docs/identite/logo/, copié dans icons/ par make
|
||||
site) : horizontal, ou symbole seul quand la place manque (téléphone
|
||||
étroit, colonne du mode paysage) ; version sombre en thème sombre. -->
|
||||
<h1 class="brand">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark) and (max-width: 24.5rem), (prefers-color-scheme: dark) and (orientation: landscape) and (max-height: 40rem)" srcset="icons/symbole-sombre.svg">
|
||||
<source media="(max-width: 24.5rem), (orientation: landscape) and (max-height: 40rem)" srcset="icons/symbole-couleur.svg">
|
||||
<source media="(prefers-color-scheme: dark)" srcset="icons/logo-horizontal-sombre.svg">
|
||||
<img src="icons/logo-horizontal-couleur.svg" alt="Ustagammon">
|
||||
</picture>
|
||||
</h1>
|
||||
<button id="help-button" type="button" class="secondary"></button>
|
||||
<button id="new-game" type="button" class="secondary"></button>
|
||||
</header>
|
||||
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user