Compare commits

...
Author SHA1 Message Date
Pierre MARTINandClaude Opus 5.5 b4d79576a7 Fin de l'étape 0 : guides temporaires supprimés, paysage dès la rotation (ADR 0012)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 07:02:02 +01:00
Pierre MARTINandClaude Opus 5.5 3b94475f15 Fusion de la MR !20 : retours du test sur téléphone, feuille de route et plans
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 06:56:13 +01:00
Pierre MARTINandClaude Opus 5.5 aa453b53e1 Retours du test sur téléphone : feuille de route, plans, Playwright validé
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 06:55:52 +01:00
pierre 55f383dc10 Fusion de la MR !19 : coup de l'ordinateur, aucun coup possible, dés et videau touchables 2026-11-03 04:06:50 +00:00
Pierre MARTINandClaude Opus 5.5 9bfc7efb9e 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>
2026-11-03 05:06:09 +01:00
Pierre MARTINandClaude Opus 5.5 a08c997ac1 Plan coup adverse : résultat des deux ajouts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 04:57:34 +01:00
pierre 99e87a3f50 Fusion de la MR !18 : charte graphique appliquée 2026-11-03 03:56:56 +00:00
Pierre MARTINandClaude Opus 5.5 02050eff01 Charte appliquée : plan clos ; fluidité ajoutée à la feuille de route
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 04:56:53 +01:00
Pierre MARTIN 15caf4da6e Merge remote-tracking branch 'origin/main' into charte-ui 2026-11-03 04:51:18 +01:00
pierre 27ca5bce1e Fusion de la MR !17 : ADR 0012, mode immersif 2026-11-03 01:07:20 +00:00
Pierre MARTINandClaude Opus 5.5 d61efbc9ae Plan : résultat de l'application de la charte
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 02:07:18 +01:00
Pierre MARTINandClaude Opus 5.5 62dfc1922a Chiffres alignés sur le plateau et dans le compte de points
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 02:07:18 +01:00
Pierre MARTINandClaude Opus 5.5 7ecb2da39f ADR 0012 : mode immersif en paysage, sur demande
Bouton « Plein écran » sur l'écran de jeu : plein écran et verrouillage
de l'orientation en paysage, avec repli par rotation CSS si le
verrouillage est refusé. Choix de Pierre ; feuille de route mise à jour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 02:07:13 +01:00
Pierre MARTINandClaude Opus 5.5 797c1724ab Lint : la licence de la police reste identique à l'original
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 01:59:31 +01:00
Pierre MARTINandClaude Opus 5.5 27ae0285a0 Charte appliquée à l'interface : couleurs, composants, mises en page
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 01:58:16 +01:00
Pierre MARTINandClaude Opus 5.5 5d3dedcc90 Icônes et logo de la charte, copiés depuis docs/identite/logo/
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 01:58:16 +01:00
Pierre MARTINandClaude Opus 5.5 f1c52941e9 Police Alegreya Sans embarquée (woff2, sous-ensemble latin)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 01:58:16 +01:00
pierre b08c4ffd54 Fusion de la MR !16 : ADR 0011 2026-11-03 00:54:14 +00:00
Pierre MARTINandClaude Opus 5.5 a490e72409 ADR 0011 : leçons numérotées, conseil du maître, bilan de fin de partie
Choix de Pierre sur les maquettes : parcours de leçons numérotées,
conseil du maître donné après le coup (jamais avant), bilan de fin de
partie avec le coup à revoir. Feuille de route mise à jour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 01:53:48 +01:00
Pierre MARTINandClaude Opus 5.5 e0f811d2b8 Plan : appliquer la charte graphique à l'application
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 01:47:42 +01:00
31 changed files with 875 additions and 652 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
# CLAUDE.md — Règles du projet
Ce fichier est lu par Claude Code au début de chaque session. Il fixe le cadre du projet.
Le cadrage complet est dans `docs/cadrage.md`, l'étape en cours dans `docs/etape-0.md`.
Le cadrage complet est dans `docs/cadrage.md`, le travail en cours dans `docs/feuille-de-route.md`.
**En début de session**, lire `docs/organisation.md`, `docs/feuille-de-route.md`, la fin
de `docs/journal.md`, l'index des ADR (`docs/adr/README.md`) et les plans en cours dans `docs/plans/`, puis reprendre là où en
+8 -1
View File
@@ -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
+12 -2
View File
@@ -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/
@@ -52,3 +52,9 @@ construire ou tester ; rien n'est embarqué dans l'application.
Validés par Pierre avec l'ADR 0009, dans Docker, jamais embarqués :
TypeScript (`tsc`, Apache-2.0), Biome (MIT ou Apache-2.0),
`@cucumber/cucumber` (MIT), `dependency-cruiser` (MIT).
### 2026-11-03 — Playwright pour les tests dans le navigateur
Validé par Pierre : Playwright 1.48.2 (Apache-2.0), image
`mcr.microsoft.com/playwright:v1.48.2-noble`, utilisé seulement par
`make test-browser` (Chromium et Firefox sans écran). Rien n'est embarqué.
+59
View File
@@ -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.
+79
View File
@@ -0,0 +1,79 @@
# 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.
## Amendements
### 2026-11-03 — Vue paysage dès la rotation, plateau = écran
Décidé par Pierre après son premier essai sur le téléphone, où la vue paysage
à deux colonnes (plateau limité à 70 % de la largeur, texte à droite) lui a
paru « vraiment pas optimale ».
- Dès que le téléphone est en paysage, même sans plein écran, l'app applique
la mise en page « plateau = écran » sur la hauteur réelle (`100dvh`, barre
d'adresse comprise). La mise en page à deux colonnes disparaît.
- Dans cette vue, les contrôles sont dans le plateau : comptes de points sur la
barre, videau au milieu de la barre, dés et consigne dans la bande centrale,
pictogrammes (menu, annuler, plein écran) dans la colonne des sorties
(maquette « Immersif : plein écran, contrôles dans le plateau » du canevas
ci-dessus). La géométrie du plateau suit les proportions de l'écran ; les
numéros des flèches font au moins 12 px.
- Le bouton « Plein écran » garde son rôle : plein écran, verrouillage en
paysage, repli par rotation CSS.
La bascule automatique écartée plus haut était celle qui forçait
l'orientation ; ici, seule la mise en page suit l'orientation choisie par le
joueur.
+2
View File
@@ -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é |
-73
View File
@@ -1,73 +0,0 @@
# Étape 0 — Prototype technique
**But** : prouver que la chaîne technique fonctionne avant d'investir dans le design, et trancher entre deux socles : PWA (moteur en WebAssembly) ou Android natif.
**Règle** : chaque sous-étape se termine par une démonstration que le développeur peut lancer lui-même avec une commande `make`.
---
## 0a. Le moteur en ligne de commande
**Objectif** : interroger le moteur GNU Backgammon sur une position, depuis le PC.
À faire :
1. Récupérer les sources du moteur (choisir entre gnubg, gnubg-nn ou le moteur extrait par CBG ; justifier le choix dans un ADR, `docs/adr/`).
2. Définir une **interface C minimale** du moteur (`engine/include/engine.h`, identifiants en anglais : ADR 0007), indépendante de toute interface graphique :
- initialiser le moteur (charger réseaux, base de fin de partie, table d'équité de match) ;
- lister les coups légaux pour une position et des dés ;
- donner le meilleur coup et le classement des coups avec leur équité, pour un niveau d'évaluation donné ;
- donner la décision de videau (pas de double, double/prend, double/passe, avec équités) ;
- évaluer une position (probabilités de gain, gammon, backgammon).
Les positions entrent et sortent au format **GNU Position ID + Match ID**.
3. Écrire `engine-cli`, un petit programme qui expose ces fonctions en ligne de commande, avec une sortie JSON.
4. Générer un jeu de **positions de référence** (au moins 30 : ouvertures, contact, course, sortie, décisions de videau) avec le GNU Backgammon de bureau dans Docker, et stocker les résultats attendus dans `tests/reference/`.
5. `make test` compare `engine-cli` aux références (même meilleur coup, équités à une tolérance près, à fixer).
**Démo** : `make engine && ./build/engine-cli bestmove <position_id> <match_id> 31` affiche le meilleur coup en JSON ; `make test` passe.
---
## 0b. Le test WebAssembly (décisif)
**Objectif** : savoir si le moteur tourne assez vite et léger dans le navigateur du téléphone.
À faire :
1. Compiler le même moteur et la même interface C en WebAssembly avec Emscripten (dans Docker).
2. Charger le moteur dans un **Web Worker** (jamais sur le fil d'affichage).
3. Page HTML minimale : un champ Position ID, des dés, un bouton « meilleur coup », l'affichage du résultat et du temps de calcul.
4. Rejouer les positions de référence dans le navigateur : les résultats doivent concorder avec 0a.
5. `make deploy-web` sert la page sur le réseau local pour l'ouvrir depuis le Fairphone 4.
6. `make bench` (ou une page de mesure) produit un petit rapport : temps par coup selon le niveau d'évaluation, taille téléchargée, mémoire utilisée.
**Critères de réussite (à ajuster après mesure)** :
| Mesure sur le Fairphone 4 | Seuil |
| --- | --- |
| Meilleur coup, niveau rapide (0-ply GNU) | moins de 0,3 s |
| Meilleur coup, niveau fort (2-ply GNU) | moins de 3 s |
| Décision de videau, niveau fort | moins de 3 s |
| Taille totale à télécharger (wasm + données, compressé) | moins de 40 Mo |
| Résultats identiques aux références de 0a | 100 % |
**Démo** : la page ouverte sur le Fairphone donne le meilleur coup d'une position de référence, avec le temps de calcul affiché.
---
## 0c. Android natif (seulement si 0b échoue)
**Objectif** : la même démonstration en application Android.
À faire :
1. Compiler le moteur pour arm64 avec le NDK (dans Docker), en s'appuyant sur la configuration de CBG.
2. Application Android minimale, construite en ligne de commande (Gradle sans Android Studio, `-j2`, démon Gradle limité en mémoire).
3. Même écran de test et mêmes mesures qu'en 0b.
4. `make deploy` installe l'APK sur le Fairphone via adb.
---
## 0d. Plateau minimal et décision
1. Sur le socle retenu, un **plateau minimal** : une partie complète jouable contre le moteur, coups et videau décidés par le moteur, dés tirés par l'app.
2. Rédiger un ADR (`docs/adr/`) : socle retenu (PWA ou Android natif), mesures obtenues, raisons, risques restants.
**Critère de fin de l'étape 0** : 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.
+19 -16
View File
@@ -3,34 +3,37 @@
Le travail restant, dans l'ordre. Une ligne réalisée est retirée : ce qui a été
fait se lit dans les merge requests et le journal, ce qui a été décidé dans les
ADR (`docs/adr/`). Les versions et leur contenu sont définis dans
`docs/cadrage.md` ; le détail de l'étape en cours est dans `docs/etape-0.md`.
`docs/cadrage.md`.
Légende : 🔄 en cours · ⏳ à venir · ⏸ conditionnel
## Étape 0 — Prototype technique
## Étape 0 — Prototype technique : terminée
Acquis : socle du dépôt, organisation (ADR 0001, 0003, 0006), source du moteur
(ADR 0005 : gnubg officiel, commit 14b6798), gnubg de référence et 32 positions
de référence (0a.2), moteur embarqué et `engine-cli` (0a.3), comparaison
automatique aux références, tolérance 0,002 (0a.4), moteur en WebAssembly avec
page de test, page de mesure et guide de test (0b), mesures sur le Fairphone 4 et
socle PWA en TypeScript (ADR 0009 ; 0c Android natif abandonnée), domaine
« partie » en journal d'événements avec scénario exécuté sur le vrai moteur et
garde-fous d'architecture (0d.1), déploiement sur https://ustagammon.sans.pub.
Close le 2026-11-03 : partie complète jouée par Pierre sur le Fairphone 4, coups
de l'ordinateur rapides, tous les tests au vert. Acquis : moteur gnubg officiel
(ADR 0005) comparé à 32 positions de référence, moteur en WebAssembly, socle
PWA en TypeScript (ADR 0009 ; Android natif abandonné), domaine « partie » en
journal d'événements, plateau jouable hors ligne avec la charte graphique,
publié sur https://ustagammon.sans.pub. Le détail est dans le journal et les
merge requests.
## Retours du premier test sur téléphone (avant l'étape 1)
Demandes de Pierre du 2026-11-03, traitées avant d'ouvrir l'étape 1.
| 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` |
**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.
| 0e.1 | Saisie du coup : pion à destination unique joué en un toucher, coup à valider et annulable demi-coup par demi-coup, dés joués marqués, dés des deux côtés touchables, videau confirmé ; pages de test du moteur retirées | 🔄 | `plans/2026-11-03-saisie-du-coup.md` |
| 0e.2 | Revue UX par un designer UI : compte de points, fluidité (bouton « Nouvelle partie », interruptions), paysage ; propositions à valider par Pierre | 🔄 | `plans/2026-11-03-revue-ux.md` |
| 0e.3 | Paysage sur téléphone : plateau sur presque tout l'écran, contrôles en pictogrammes (ADR 0012 à amender), selon la revue validée | ⏳ | |
| 0e.4 | Position dans l'adresse : chaque position du plateau a un lien direct (nouvel ADR pour le format) | ⏳ | |
| 0e.5 | Page d'accueil, installation en un toucher (« Ajouter à l'écran d'accueil », proposée par l'app), tutoriel des règles très didactique avec plateaux illustrés, aide réécrite sur un ton plus humain ; textes à valider par Pierre | ⏳ | |
## Étapes suivantes
| É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 | ⏳ |
-171
View File
@@ -1,171 +0,0 @@
# Guide de test : Ustagammon sur le Fairphone 4 (étape 0)
Document temporaire de l'étape 0 : il sera supprimé avec `docs/etape-0.md`.
Deux tests sur le téléphone :
- **Étape 0d : jouer une partie** (section suivante), sur le site publié ;
- **Étape 0b : mesurer le moteur** (fait le 2026-11-02), sur une page
servie par le PC.
## Étape 0d : jouer une partie sur le téléphone
Le but : installer l'application sur le Fairphone 4, jouer une partie
complète contre l'ordinateur, vérifier qu'elle marche sans réseau, et
rapporter ce que vous avez constaté. Comptez une demi-heure.
### Ouvrir et installer l'application
1. Sur le Fairphone, ouvrez Chrome et allez sur https://ustagammon.sans.pub.
2. Attendez que le plateau s'affiche et que le message « Chargement du
moteur… » laisse place à une consigne (par exemple « À vous : lancez les
dés. » ou « L'ordinateur réfléchit… »). La première fois, le moteur se
télécharge (environ 7 Mo) : quelques secondes en Wi-Fi.
3. Installez l'application : menu de Chrome (les trois points), puis
« Installer l'application » ou « Ajouter à l'écran d'accueil ». Une icône
(deux pions sur du bois) apparaît sur l'écran d'accueil.
4. Fermez Chrome et ouvrez Ustagammon depuis cette icône : l'application
s'ouvre en plein écran, sans barre d'adresse.
### Jouer une partie complète
Suivez le scénario « Jouer une partie contre l'ordinateur »
(`docs/scenarios/jouer-une-partie.feature`), qui est aussi l'aide de
l'application : bouton « Aide », en haut.
1. Une partie commence d'elle-même au premier lancement, par le jet
d'ouverture (un dé chacun) : le message dit qui commence. « Nouvelle
partie », en haut, en recommence une.
2. Quand c'est à vous : touchez « Lancer les dés » (ou « Doubler », pour
proposer de doubler avant de lancer).
3. Pour jouer : touchez un pion entouré de vert, puis l'une des cases
d'arrivée en surbrillance ; recommencez pour chaque dé. Pour sortir un
pion, touchez le bac à droite, en bas. « Annuler » remet les pions du coup
en cours à leur place.
4. Le coup est envoyé dès qu'il est complet ; l'ordinateur joue ensuite, et
le message dit son coup et son temps de calcul, par exemple
« L'ordinateur a joué 6-4 : 24/18 13/9 (0,3 s) ». Les petits points
orange marquent ses cases de départ et d'arrivée (dans sa numérotation :
sa case 24 est votre case 1).
5. Si l'ordinateur vous propose de doubler : « Prendre » ou « Passer ».
Essayez au moins une fois de doubler vous-même.
6. En fin de partie, le message donne le gagnant, la sorte de victoire
(simple, gammon ou backgammon) et les points.
En cours de partie, fermez l'application (balayez-la dans la liste des
applications ouvertes), puis rouvrez-la : la partie doit reprendre là où vous
en étiez.
### Vérifier le hors ligne
1. Après avoir ouvert l'application au moins une fois (attendez une
vingtaine de secondes après le premier chargement, le temps que tout soit
mis de côté), fermez-la.
2. Passez le téléphone en mode avion.
3. Rouvrez Ustagammon depuis son icône : le plateau, l'ordinateur et l'aide
doivent fonctionner comme avant.
4. Repassez en mode normal.
### Ce qu'il faut rapporter
Dans la conversation avec Claude Code :
- les **temps des coups de l'ordinateur** : ceux affichés dans les messages
(le plus long que vous avez vu), et votre ressenti (attente gênante ou
non) ;
- si la partie est allée **jusqu'au bout**, et son résultat ;
- si la **reprise** après fermeture et le **mode avion** ont marché ;
- tout ce qui gêne : plateau trop petit, case difficile à toucher, texte
incompréhensible, bouton qui ne répond pas, couleurs peu lisibles (thème
clair ou sombre du téléphone) ;
- une **capture d'écran** (bouton marche + volume bas) de tout problème.
Si vous le pouvez, refaites l'essai dans Firefox sur le téléphone (sans
l'installer) et dites si les temps de l'ordinateur sont nettement plus longs.
## Étape 0b : mesurer le moteur
Le but : ouvrir sur le téléphone une page web où le moteur de backgammon
calcule, puis lancer une mesure et coller son rapport dans la conversation.
Comptez un quart d'heure.
### 1. Préparer le PC
1. Ouvrez un terminal dans le dossier du projet (`~/Code/ustagammon`).
2. Si ce n'est pas déjà fait, tapez `make setup` puis Entrée. Cela prépare les
outils. La première fois, c'est long ; ensuite, c'est immédiat.
3. Le PC et le téléphone doivent être sur **le même Wi-Fi** (la même box).
### 2. Lancer la page
1. Tapez `make deploy` puis Entrée.
2. La première fois, le moteur se compile : quelques minutes.
3. Le terminal affiche deux adresses, par exemple :
```
Application : http://192.168.1.21:8080/
Test du moteur : http://192.168.1.21:8080/moteur/
Page de mesure : http://192.168.1.21:8080/moteur/bench.html
```
Vos chiffres peuvent être différents : notez ceux de votre écran.
4. Laissez ce terminal ouvert pendant tout le test. Pour arrêter la page à la
fin : Ctrl+C dans le terminal.
### 3. Essayer un meilleur coup
1. Sur le Fairphone, ouvrez le navigateur (Chrome ou Firefox).
2. Tapez l'adresse du **test du moteur** dans la barre d'adresse, exactement
comme affichée (avec `http://` et `:8080`). Les pages de test sont aussi
publiées : https://ustagammon.sans.pub/moteur/ et
https://ustagammon.sans.pub/moteur/bench.html.
3. Attendez le message « Moteur prêt, chargé en … ms ».
4. Dans « Exemple », choisissez une position, par exemple « middlegame-1 ».
5. Dans « Niveau », choisissez « fort (2 plis) ».
6. Touchez « Meilleur coup ».
7. La page affiche le coup conseillé et le **temps de calcul**. Notez ce temps
si vous voulez, il sera aussi dans le rapport.
### 4. Lancer la mesure
1. En bas de la page, touchez « Page de mesure » (ou tapez l'adresse de la
page de mesure).
2. Touchez « Lancer la mesure ».
3. Attendez « Mesure terminée » : de une à quelques minutes. Pendant ce temps,
gardez l'écran allumé et la page ouverte, sans changer d'application.
4. Un tableau résume les résultats : « OK » si le seuil est respecté,
« dépassé » sinon.
### 5. Envoyer le rapport
1. Touchez « Copier les résultats ».
2. Si la page dit « Rapport copié », c'est bon.
3. Sinon, le texte du rapport est sélectionné : faites un appui long dessus,
puis « Copier ».
4. Collez le rapport dans la conversation avec Claude Code (sur le téléphone,
ou envoyez-le vous sur le PC).
Si vous en avez le temps, refaites la mesure une deuxième fois (rechargez la
page de mesure) et collez aussi ce second rapport : cela montre si les temps
sont stables. Si les deux navigateurs sont installés, faites la mesure dans
Chrome **et** dans Firefox : sur le PC, Firefox s'est montré nettement plus
lent, et le rapport indique le navigateur utilisé.
### Si la page ne s'ouvre pas
- **Vérifiez l'adresse** : `http://` (et non `https://`), les chiffres exacts,
et `:8080` à la fin.
- **Vérifiez le Wi-Fi** : le téléphone doit être sur le même réseau que le PC,
pas sur la 4G ni sur un Wi-Fi invité.
- **Vérifiez que la page tourne** : sur le PC, ouvrez
`http://localhost:8080/` dans un navigateur. Si elle ne s'ouvre pas non
plus, relancez `make deploy` et lisez le message d'erreur.
- **Le pare-feu du PC** peut bloquer le téléphone. S'il est actif
(`sudo ufw status` affiche « Status: active »), autorisez le port le temps
du test : `sudo ufw allow 8080/tcp`, puis à la fin `sudo ufw delete allow
8080/tcp`.
- **« Le moteur n'a pas pu se charger »** : notez le message exact et
collez-le dans la conversation. Essayez aussi l'autre navigateur (Chrome ou
Firefox), à jour.
- **Le port 8080 est déjà pris** : un autre programme l'utilise. Lancez
`make deploy WEB_PORT=8081` et utilisez `:8081` dans l'adresse.
+18
View File
@@ -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
+25
View File
@@ -110,3 +110,28 @@ Une entrée datée par session : ce qui a été fait, décidé, et la suite.
- Les maquettes proposent des éléments de produit qui ne sont pas encore
décidés (leçons, « Conseil du maître », bilan de fin de partie) : à valider
par Pierre avant toute mise en œuvre.
## 2026-11-03 (matin) : fin de l'étape 0, retours du test sur téléphone
**Fait**
- Pierre a joué une partie complète sur le Fairphone 4 : « très rapide de la
part de l'ordinateur ». Avec les tests au vert, le critère de fin de l'étape 0
est atteint. Le guide de test et `docs/etape-0.md`, temporaires, sont
supprimés ; leurs décisions sont dans les ADR 0005 et 0009.
- Playwright validé par Pierre (amendement de l'ADR 0004, MR !20).
- Douze retours de Pierre après l'essai, plus l'installation en un toucher et
les retours haptiques, rangés en sous-étapes 0e dans la feuille de route
(MR !20). Lancés : la saisie du coup (0e.1) et une revue UX par un designer
UI (0e.2).
- Coordination avec la session « identité » : elle propose et maquette le
design (mode immersif, compte de points, accueil, tutoriel), Pierre tranche,
la session technique implémente. Numéros des flèches à 12 px minimum.
- Décision de Pierre : la vue paysage « plateau = écran » s'applique dès la
rotation du téléphone, sans plein écran (amendement de l'ADR 0012).
- Nouvel objectif fixé par Pierre : traiter les retours 0e (les siens et ceux
du designer), puis réaliser l'étape 1, livrée avec un plan de test.
**Reste à faire**
- Le test en mode avion sur le téléphone n'a pas été rapporté ; le hors ligne
est vérifié dans Chromium par `make test-browser`.
- Firefox sur le téléphone reste à mesurer.
-181
View File
@@ -1,181 +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.
- Résultats : `make lint` sans problème ; `make test` 10/10 ; `make
test-browser` vert sous Chromium (coup de l'ordinateur montré 26 fois) et
Firefox (43 fois), sans violation CSP ni erreur dans la page.
**Limites** :
- Aucune des deux parties du navigateur n'a vu l'ordinateur frapper (0 pion
frappé signalé). L'animation vers la barre n'est donc vérifiée que par le
scénario et les tests unitaires (plateaux et traces), pas dans un
navigateur.
- Firefox a affiché un tour de l'ordinateur à 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.
- Sur une case de plus de cinq pions, c'est le cinquième pion dessiné qui
glisse (le nombre reste affiché sur la pile).
+37
View File
@@ -0,0 +1,37 @@
# Revue UX : compte de points, fluidité, paysage
- **Date** : 2026-11-03
- **Étape** : 0e (retours du premier test de Pierre sur le téléphone)
- **Statut** : en cours
- **Exécutant** : sous-agent general-purpose, rôle de designer UI, en lecture
seule sur le code
## Objectif
Proposer à Pierre, avec maquettes, une meilleure présentation du compte de
points et une revue de toute l'expérience de jeu (point 10 de son message du
2026-11-03), en couvrant la fluidité (ligne 0d.2+ de la feuille de route) et le
paysage sur téléphone (point 5).
## Consignes
- Partir de la charte (`docs/identite/`, ADR 0010), des ADR 0011 et 0012, et de
l'application publiée (https://ustagammon.sans.pub, ou `make site` puis les
captures de `make test-browser`).
- Ne pas modifier le dépôt. Livrer un rapport en Markdown (chemin indiqué par
la session principale) et, si utile, une page de maquettes.
- Pour chaque proposition : le problème observé, la solution, sa maquette, son
coût technique estimé, et ce qui relève d'un choix de Pierre.
- Sujets imposés : compte de points ; bouton « Nouvelle partie » et tout ce qui
interrompt la partie ; paysage sur téléphone, plateau sur presque tout
l'écran, contrôles en pictogrammes.
- Les changements de saisie du coup en cours (plan
`2026-11-03-saisie-du-coup.md`) sont acquis : ne pas les reproposer.
## Critères de réussite
Un rapport priorisé que Pierre peut valider point par point.
## Résultat
À remplir à la clôture.
+65
View File
@@ -0,0 +1,65 @@
# Saisie du coup, videau confirmé, pages de test du moteur retirées
- **Date** : 2026-11-03
- **Étape** : 0e (retours du premier test de Pierre sur le téléphone)
- **Statut** : en cours
- **Exécutant** : sous-agent general-purpose, dans un worktree
## Objectif
Rendre la saisie du coup aussi confortable que dans CBG, d'après les retours de
Pierre du 2026-11-03 (points 1, 6, 7, 8, 9 et 11 de son message), et retirer du
site les pages de test du moteur.
## Consignes
Règles de code : ADR 0007 (domaine pur, aucune règle de backgammon
réimplémentée : tout vient du moteur via le domaine). Glossaire et scénarios :
ADR 0008 (chaque comportement visible a son scénario Gherkin dans
`docs/scenarios/`, chaque nouveau terme sa fiche). Aucune nouvelle dépendance.
1. **Pages de test du moteur retirées** (« supprimer la partie test du moteur
totalement ») : plus de `/moteur/` sur le site ni dans `make deploy`.
`make bench` (mesure sous Node) reste : déplacer ce dont il a besoin hors de
`web/`. Mettre à jour `tests/test_site.sh`, le Makefile, `deploy/`, le
service worker, `docs/guide-test-etape-0.md` et ajouter un amendement daté à
l'ADR 0009.
2. **Un seul coup possible, le pion avance** : quand le joueur touche un pion
(ou une case) et qu'une seule case d'arrivée est possible pour ce pion, le
demi-coup est joué aussitôt, sans second toucher. S'il y en a plusieurs, la
sélection actuelle reste.
3. **Coup à valider, annulable** : quand tous les demi-coups sont joués (ou
qu'aucun autre n'est possible), le coup n'est pas envoyé tout de suite. Le
joueur le valide en touchant ses dés (ou un bouton « Valider »). Tant qu'il
ne l'a pas validé, « Annuler » défait le dernier demi-coup, un par un. Le
journal d'événements ne reçoit `MovePlayed` qu'à la validation.
4. **Dés joués marqués** : chaque dé déjà utilisé par un demi-coup est
visiblement estompé. Pour un double, quatre dés, marqués un par un.
5. **Dés de l'adversaire touchables** : toucher la zone des dés de l'un ou
l'autre côté du plateau lance les dés quand le joueur peut lancer (et valide
le coup en attente de validation).
6. **Confirmation du videau** : toucher le videau ou « Doubler » ouvre une
confirmation courte (« Doubler à 2 ? L'ordinateur pourra prendre ou
passer. » : Doubler / Annuler) avant d'envoyer le double.
Tests : scénarios Gherkin mis à jour ou ajoutés (validation du coup, annulation
demi-coup par demi-coup, confirmation du videau), tests unitaires, et
`tests/browser/play.mjs` adapté (Playwright est validé par Pierre). Lancer
`make lint`, `make test` et `make test-browser`.
Rapporter : ce qui a été fait, les écarts, les résultats des trois commandes,
et un brouillon de description de MR (7 sections, voir `docs/organisation.md`).
Commits fréquents sur la branche du worktree ; ne pas pousser, ne pas ouvrir de
MR : la session principale s'en charge.
## Critères de réussite
- `make lint`, `make test` et `make test-browser` passent.
- Dans le navigateur : un pion à destination unique avance en un toucher ; un
coup complet attend la validation ; « Annuler » défait un demi-coup à la
fois ; les dés joués sont estompés ; le videau demande confirmation.
- `https://…/moteur/` n'existe plus dans le site construit (`make site`).
## Résultat
À remplir à la clôture.
-73
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+93
View File
@@ -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

-7
View File
@@ -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
View File
@@ -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 -6
View File
@@ -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" }
]
}
+1 -1
View File
@@ -7,7 +7,7 @@
// Sur les positions de référence (tests/reference/positions.json) : temps du
// meilleur coup à 0 et 2 plis pour les positions avec dés, temps de la
// décision de videau à 2 plis pour les autres. Le rapport compare aux seuils
// de l'étape 0b (docs/etape-0.md).
// de l'étape 0b (ADR 0009).
// Seuils de l'étape 0b. Un temps respecte son seuil si son maximum, sur
// toutes les positions, est en dessous (la médiane est donnée en plus).