Repository navigation
Architecture historique
Notes écrites entre les versions 1.48 et 1.50 (août 2026), gardées pour l'historique et non mises à jour : le Labo, le serveur et le moteur ont beaucoup changé depuis. La page à jour est Architecture ; le réseau de calcul actuel est décrit dans Calcul distribué.
Abalassembly |
abalassembly-api |
|
|---|---|---|
| Rôle | Le jeu, en ligne, actif | Backend RGPD-ready, dormant |
| Stack | HTML/CSS/JS, un seul fichier, zéro dépendance | Node + Fastify + PostgreSQL + Redis |
| État | https://ocj94.github.io/Abalassembly/ | Construit, testé (62 tests, CI verte), non déployé |
Le jeu n'appelle le backend que si BACKEND.enabled = true dans son code — ce n'est pas le cas. Tant que ce drapeau reste à false, le site ne collecte rien et ne contacte aucun serveur.
-
Génération de coups : en ligne (pousées, sumito 2v1/3v1/3v2), latéraux (broadside), sur un plateau hexagonal à 61 cases. Coordonnées internes en
(r,c), converties en axiales(q,r)pour les calculs géométriques (rotation, voisinage). - Recherche IA : alpha-bêta avec PVS/NegaScout (~5-6× plus rapide que la version naïve), table de transposition, killer moves, quiescence search. Tourne dans un Web Worker dédié pour ne jamais geler l'interface.
-
Évaluation : pondération de 6 facteurs (centre, cohésion, bord, mobilité, isolement, danger) — les mêmes clés
EVAL_Wutilisées côté client ET côté serveur (Labo distribué). -
5 dispositions officielles : standard, belge, allemande, hollandaise, suisse — définies une fois (
LAYOUTS), réutilisées partout (moteur, Labo, importateur).
Les deux partagent la même méthodologie (SPRT — test séquentiel de Wald, façon Fishtest/OpenBench), mais à des échelles différentes :
-
Local : tourne dans le navigateur,
localStorage, un seul joueur. Le champion validé réécrit en direct le style « équilibré » de l'IA — l'IA que vous affrontez évolue réellement d'une session à l'autre. -
Distribué (
abalassembly-api,/lab/*) : plusieurs joueurs contribuent au même test collectif. Chaque soumission est vérifiée par rejeu du vrai moteur (src/engine.js, extrait du code du jeu, isolé par requête pour rester sûr en concurrence) — légalité des coups, authenticité de la position de départ, cohérence du résultat annoncé. Diversité des 5 ouvertures et équilibrage de la couleur jouée par le candidat, gérés par équilibrage glouton.
Ce qui n'est PAS vérifié côté serveur : la réflexion de l'IA elle-même (recherche/évaluation) n'est jamais rejouée pour chaque soumission — seulement la légalité et le résultat de la partie témoin. Limite assumée, documentée dans le README du backend.
Remplace un ancien appel à une API externe (qui ne fonctionnait que dans l'environnement claude.ai) par de la géométrie pure :
- L'utilisateur tape les 4 coins connus du plateau sur sa photo (comme un scanner de documents)
- Calcul d'une homographie (transformation projective) reliant la géométrie idéale du plateau à la photo, quel que soit l'angle de prise de vue
- Échantillonnage de la couleur à l'emplacement calculé de chacune des 61 cases
- Classification en 2 étapes, auto-calibrée sur la photo (pas de seuil fixe) : distance chromatique au bois (bille vs case vide), puis luminosité (noir vs blanc) — robuste même si une couleur est totalement absente du plateau
Validé sur 4 scénarios synthétiques (position complète, milieu de partie éparse, une seule couleur présente, plateau vide) : 100 % de précision sur chacun.
Forme canonique d'une position parmi ses 12 symétries du plateau hexagonal (6 rotations à 60° × 2 avec/sans miroir). Les formules de rotation et de miroir sont vérifiées par calcul de clôture sur les 61 vraies cases — jamais recopiées à la main. Appliqué concrètement : confirme qu'aucun des 80 puzzles du jeu n'est le doublon géométrique d'un autre.
drawBoard(opts) dessine tous les plateaux de l'application — partie, Éditeur, Puzzles, Analyse, Tutoriel — et les vues 1D et 3D se raccrochent au même cycle de rafraîchissement. Deux règles en découlent, détaillées dans Vues du plateau :
-
Une vue ne connaît aucune règle du jeu. Elle désigne une case et appelle
handleClick(r, c), point d'entrée unique du moteur. C'est ce qui rend les vues 1D et 2D interchangeables en cours de partie sans une ligne de code de synchronisation. -
Toute couche qui lit un état global de la partie est conditionnée par une option (
hideGutter,hideGameOverlays,gutterCounts), activée par défaut sur les plateaux qui ne sont pas la partie. Faute de quoi la flèche du dernier coup ou les billes éjectées de la partie précédente réapparaissent sur l'Éditeur — le défaut s'est produit deux fois, pour deux couches différentes, avant que le principe ne soit généralisé.
- Jamais de constante géométrique recopiée à la main — toujours dérivée par calcul et vérifiée par un test de clôture ou de cohérence. Deux bugs réels de la session sont nés de constantes mal recopiées (une direction opposée, une fonction de tri de ligne) — repérés uniquement grâce à des tests qui comparaient au vrai moteur plutôt que de faire confiance à la logique écrite.
- Toute nouvelle fonctionnalité touchant au moteur est testée contre le vrai moteur, jamais une réimplémentation isolée jugée sur sa seule cohérence interne.
-
Mono-fichier préservé côté jeu : pas de build, pas de
node_modules, distribution par simple copie du HTML.
Ajouté en v1.48, mais sans ajouter de logique de jeu nouvelle : le mode AR
(Détection → AR Live) réutilise telles quelles les fonctions déjà
validées de la détection photo (detComputeHomography, detApplyHomography,
detSamplePatch, detClassify) et du moteur de jeu (validateMove,
applyMove, undoMove, evaluateBoard) — aucune n'a été réimplémentée pour
l'occasion, contrairement à un prototype externe proposé qui refaisait sa
propre géométrie de coups légaux et sa propre heuristique de « meilleur
coup » en React, incompatible avec le principe mono-fichier vanilla du
projet.
Le point technique délicat : les coups légaux et le meilleur coup
dépendent de validateMove/applyMove/evaluateBoard, qui lisent tous le
board global de la partie réelle — pas le plateau détecté par la caméra.
La solution retenue : échanger temporairement board (et les compteurs de
capture) pour le plateau AR, calculer, puis restaurer — toujours de façon
synchrone, jamais de await entre l'échange et la restauration, ce qui
garantit qu'aucune autre partie de l'application ne peut lire un état
incohérent pendant le calcul. Vérifié explicitement : après tout calcul AR
(coups légaux ou meilleur coup), le board et les compteurs de capture
réels sont comparés bit à bit à leur valeur d'avant l'appel.
detRcToAx(détection) etrcToAxial(moteur de jeu) se sont révélées être exactement la même formule, vérifié avant d'écrire une seule ligne du mode AR — c'est ce qui rend l'échange deboardsûr : les indices(r,c)du plateau détecté par caméra sont directement compatibles avec ceux qu'attend le vrai moteur, sans table de conversion.
Métal, Bronze, Cuivre, Ardoise, Granite, Verre, Béton, Corail, Glace,
Carbone — chacun avec sa propre gouttière colorée (gutterOuter/gutGrad/
trim/surf) et sa propre recette de couches (THEME_RECIPES), construits
exclusivement à partir des 11 motifs déjà catalogués par le moteur générique
de la v1.46 (voir Vues du plateau) — aucun nouveau type
de couche créé. La 3D hérite des 10 nouveaux thèmes sans une seule ligne
de code 3D modifiée : peindreTextureTheme (3D) et peindreTextureTheme2D
consomment le même window.THEME_RECIPES — la preuve que le refactor en
moteur de couches de la v1.46 tient sa promesse à sa première vraie mise à
l'épreuve à grande échelle.
Vérifié par comparaison de pixels réels (pas seulement « le code s'exécute ») que les 10 nouveaux thèmes sont visuellement distincts entre eux et des thèmes existants — deux recettes qui ne diffèrent que par une teinte proche peuvent sembler différentes dans le code et se révéler quasi identiques une fois rendues.
Un audit de sécurité produit par un outil externe a signalé 5 points sur le fichier déployé. Vérifiés un par un contre le code réel avant toute correction :
| Signalement | Verdict |
|---|---|
| Pas de CSP | Faux — CSP complète déjà présente |
Clé AES stockée en localStorage
|
Faux — dérivée par PBKDF2, gardée en mémoire uniquement, jamais persistée |
XSS via innerHTML sur le pseudo |
Faux — déjà échappé via escapeHtml()
|
| Upload photo sans limite de taille | Vrai |
| Sortie Mode Enfant par calcul trop simple | Vrai, mais déjà assumé comme suffisant pour un enfant |
Les 124 usages d'innerHTML du fichier ont été audités un par un (en
excluant les blobs base64 qui faussent un grep naïf) : sept seulement
injectent une variable, toutes numériques ou déjà passées par
escapeHtml(). Aucune faille XSS trouvée. Le chargement de Three.js sans
attribut integrity (SRI), également signalé, s'est avéré non pertinent :
integrity ne s'applique pas à un import() dynamique, et le fichier
charge déjà en priorité une copie locale (./vendor/three.module.js) avant
tout repli CDN — une parade plus robuste que le SRI proposé.
Seuls les deux signalements réels ont été corrigés : garde de 10 Mo avant lecture d'une photo, et rate-limit de 3 essais / 5 secondes sur la sortie du Mode Enfant.
Le diff correctif fourni par l'audit n'a pas été appliqué. Il aurait remplacé la CSP existante par une version sans
'unsafe-inline', cassant tous lesonclick=inline du fichier — structurellement nécessaires dans un mono-fichier sans build. Un correctif généré sans lire le code réel peut casser exactement ce qu'il prétend sécuriser.
Le bug « noir = moi » — supposer en dur que le joueur humain tient toujours
les noirs, faux dès qu'il joue blanc — s'était produit indépendamment à
trois endroits : l'horloge (_clockMine()), l'attribution de la heatmap
(un _mine local), et les métriques de style (recordPlayerMove, un
_myCol local). Chacun avait déjà été corrigé individuellement, avec
exactement la même logique recopiée trois fois plutôt que centralisée.
monCamp() est désormais l'unique source : la couleur que l'humain tient
réellement (humanColor en mode IA, convention « noir = joueur local » en
mode 2 joueurs locaux — les deux joueurs partagent le même profil sur
l'appareil). estMonTour() l'accompagne pour les appelants qui posent la
question « est-ce mon tour ? » plutôt que « quelle couleur je tiens ? ».
Ceci n'était pas la correction d'un bug — les trois anciennes implémentations étaient déjà correctes individuellement, vérifié avant de toucher au code. C'était une consolidation pure : le risque n'est donc pas dans le comportement (identique, vérifié sur 6 scénarios
gameMode/humanColorpar comparaison directe aux 3 anciennes versions), mais dans l'oubli d'un site d'appel lors du remplacement — ungrepdu nom supprimé après coup, pas seulement une relecture, est ce qui a permis de confirmer qu'aucun ne subsistait.
Export de position depuis le mode AR : réutilise coordToABAPRO(), la
même fonction que l'éditeur de position — pas une notation propre au mode
AR. Copie dans le presse-papier avec repli toast si l'API est indisponible.
Modération du chat (insultes / propos haineux / menaces) : filtre local, hors ligne, sur les deux chats existants (partie et communauté). Trois catégories avec priorité (menace > haine > insulte quand plusieurs motifs coexistent dans un message), normalisation contre les contournements par répétition de lettres (« coonnnnard » → détecté), escalade après 3 messages bloqués d'affilée (chat coupé 60s). Le texte n'est jamais effacé au blocage — laissé en place pour correction, pas de punition sur un faux positif.
Le vrai risque d'un filtre de modération n'est pas de laisser passer un message limite, c'est de bloquer des joueurs qui n'ont rien fait de mal. Vérifié explicitement : zéro faux positif sur des messages de jeu normaux contenant des sous-chaînes proches d'un motif interdit (« la constitution du plateau », « connaissais-tu cette ouverture » — le mot « con » ne se déclenche que seul, jamais à l'intérieur d'un autre mot).
monCamp() / estMonTour() : voir la section dédiée plus haut — une
fonction unique remplace 3 implémentations locales identiques du même
correctif historique (« noir = moi » codé en dur).