Skip to content

Architecture historique

github-actions[bot] edited this page Nov 8, 2026 · 1 revision

Architecture : notes d'août 2026

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é.

Vue d'ensemble : deux dépôts, deux rôles

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.

Le moteur (index.html)

  • 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_W utilisé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).

Labo local vs Labo distribué

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.

Détection de plateau — 100 % hors-ligne

Remplace un ancien appel à une API externe (qui ne fonctionnait que dans l'environnement claude.ai) par de la géométrie pure :

  1. L'utilisateur tape les 4 coins connus du plateau sur sa photo (comme un scanner de documents)
  2. 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
  3. Échantillonnage de la couleur à l'emplacement calculé de chacune des 61 cases
  4. 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.

Position de référence (posRef)

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.

Rendu du plateau : un contrat unique

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é.

Conventions de développement (pour qui contribue)

  • 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.

Mode AR : le pont entre la détection photo et le flux vidéo live

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) et rcToAxial (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 de board sû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.

10 nouveaux thèmes de plateau, un seul mécanisme (v1.48)

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.

Audit de sécurité externe : fact-checké avant d'agir, pas appliqué tel quel

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 les onclick= 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.

monCamp() / estMonTour() : un point de vérité pour « qui je suis » (v1.49)

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/humanColor par comparaison directe aux 3 anciennes versions), mais dans l'oubli d'un site d'appel lors du remplacement — un grep du nom supprimé après coup, pas seulement une relecture, est ce qui a permis de confirmer qu'aucun ne subsistait.

Export de position AR, modération du chat, consolidation monCamp() (v1.50)

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).

Clone this wiki locally