Les 5 exemples de Tuto-IA-3D reposent sur la même idée : une population de réseaux de neurones apprend seule par évolution (NEAT), sans règle programmée à la main. Ce tutoriel suit le code de chaque jeu, puis les problèmes rencontrés et la façon dont ils ont été réglés.
Le dépôt part du tutoriel vidéo Tuto-IA-3D-Youtube de neocodeytb (le parcours 3D). Quatre jeux ont été ajoutés ensuite. Tous sont publiés sur rdv-app.fr/jeux.html, chacun avec une page « jouer » et une page « code expliqué ».
| # | Jeu | Langage | Ce que l'IA apprend | Jouer | Code expliqué |
|---|---|---|---|---|---|
| 1 | Le parcours 3D | Python (Panda3D) + démo Three.js | Franchir échelle, plateforme et bombe | pandas.html | pandas2.html |
| 2 | Le labyrinthe | Python + page web | Trouver le chemin le plus court | labyrinthe.html | labyrinthe2.html |
| 3 | Panda ninja | JavaScript (navigateur) | Finir un niveau à défilement | minigame.html | minigame2.html |
| 4 | Les dames | JavaScript + serveur Node | Jouer aux dames internationales | dames.html | dames2.html |
| 5 | L'IA et l'or | JavaScript + scripts Python | Trader l'or sur 25 ans de cours réels | trading-or.html | trading-or2.html |
| Fichier ou dossier | Rôle |
|---|---|
main.py, config.txt |
Parcours 3D en Python (Panda3D + NEAT) |
labyrinthe.py, config_labyrinthe*.txt |
Labyrinthe en Python |
web/ |
Pages des jeux et pages de code expliqué |
serveur/ |
Serveurs Node : dames en ligne, cours de l'or en direct |
scripts/ |
Mise à jour des données de l'or et de l'économie |
deploy.sh |
Copie des pages et des données vers /var/www/rdv-app |
Point commun à respecter partout : neat-python doit rester en version 0.92. La version 2.0 refuse les fichiers config*.txt du projet.
Vingt pandas apprennent à traverser un parcours 3D. Le résultat est atteint en 30 à 100 générations en mode rapide, soit une à quatre minutes sur un processeur ordinaire.
Deux boucles cohabitent : celle de Panda3D (taskMgr, une tâche par image) et celle de NEAT (p.run, une évaluation par génération). C'est NEAT qui commande.
initNeat lit config.txt et crée 20 génomes.p.run(self.playGame, 10000) appelle playGame à chaque génération.playGame transforme chaque génome en réseau et en panda, puis lance taskMgr.run().updatePlayerNeat (décider, bouger), updateCollisionsNeat (murs, sols, zones), verifFinPartie.taskMgr.stop() rend la main à playGame, qui note chaque génome.def playGame(self, genomes, config):
self.players = []
for _, g in genomes:
net = neat.nn.FeedForwardNetwork.create(g, config)
self.players.append(self.generatePlayerNeat(idx, net))
g.fitness = 0
self.taskMgr.run() # la partie se joue ici
for _, g in genomes:
g.fitness = ... # voir la fitness
La carte map_tuto.glb n'est qu'un décor. Le jeu ne connaît que des formes de collision invisibles placées dans initColliders, chacune avec une étiquette : Wall (rebond), Floor, Ladder, Score1 à Score9 (zones de progression), Reward (victoire), Bomb (mort).
La physique est volontairement simple : frottement ×0,93 par image, gravité −0,30 par image en l'air, mort sous z = −100.
Un panda ne voit rien. Il porte trois boîtes de collision devant lui, qui valent 1 si elles touchent le sol. Un capteur à 0 veut dire « du vide devant ».
Les 9 cases de zone permettent au même réseau d'agir différemment selon l'endroit du parcours, alors qu'il ne voit presque rien.
Dans le tuto d'origine, la note est le numéro de la dernière zone franchie (0 à 10). Beaucoup de pandas ont la même note, et NEAT ne sait pas lequel était le plus proche de réussir. La version corrigée ajoute un bonus inférieur à 1 :
bonus = 0 if player["score"] == 10 else 0.99 / (1 + player["distMin"] / 50)
g.fitness = player["score"] + bonus
Il départage les ex æquo sans jamais faire passer une zone 4 devant une zone 5.
| Problème dans le code d'origine | Conséquence | Solution |
|---|---|---|
fitness_threshold = 11 |
L'entraînement ne s'arrête jamais, best_model.pkl n'est jamais écrit |
Seuil à 10 |
Variable self.config pour NEAT |
Écrase la config interne de Panda3D : plantage à la fermeture | Renommée self.neatConfig |
| Panda tué par la bombe, autres collisions traitées dans la même image | Plantage aléatoire (setPos sur un objet supprimé) |
Collisions d'un panda mort ignorées |
| Caméra qui suit toujours le panda n°0 | Plantage s'il meurt | Elle suit le meilleur panda en vie |
Échelle : abs(vx) compté deux fois |
La montée dépend de la direction | abs(vx) + abs(vy) |
| Boîtes de collision jamais retirées | Fuite mémoire au fil des générations | removeCollider en fin de partie |
| Physique dépendante des images par seconde | Un modèle ne rejoue pas pareil à 60 et à 144 images/s | Pas de temps fixe de 1/60 s (ClockObject.MNonRealTime) |
| Dépendances non précisées | .glb illisibles, neat-python 2.0 refuse la config |
requirements.txt avec panda3d-gltf et neat-python==0.92 |
| Notes ex æquo | NEAT ne distingue pas les meilleurs | Bonus de distance < 1 |
Le pas de temps fixe a un second effet : chaque partie devient reproductible. C'est ce qui permet à la démo web (Three.js) de rejouer exactement les parties de l'entraînement exportées par python main.py --rapide --export-web.
Des pandas qui ne voient presque rien apprennent à sortir d'un labyrinthe en un minimum d'actions. Le champion final sort de 17 labyrinthes jamais vus sur 20, en 1,00 fois le minimum d'actions en moyenne.
Grille de 10 × 10 cases, départ en haut à gauche tourné vers l'est, sortie dans le mur est de la case en bas à droite. À chaque pas, une seule action parmi trois : avancer, tourner à gauche, tourner à droite. Tourner coûte autant qu'avancer.
La partie s'arrête à la sortie, après 4 × taille² actions (400 pour un 10 × 10), ou si le panda repasse plus de 4 fois sur la même case avec la même orientation (il tourne en rond).
Les labyrinthes sont « parfaits » (un seul chemin entre deux cases). Ils sont creusés par exploration en profondeur : on perce un mur vers un voisin pas encore visité, et on revient en arrière dans un cul-de-sac.
while pile:
x, y = pile[-1]
voisins = [(d, x + DX[d], y + DY[d]) for d in range(4)
if ... and (x + DX[d], y + DY[d]) not in vues]
if not voisins:
pile.pop() # cul-de-sac : on revient en arrière
continue
d, nx, ny = rng.choice(voisins)
self.murs[y][x][d] = False # on perce le mur des deux côtés
self.murs[ny][nx][(d + 2) % 4] = False
vues.add((nx, ny))
pile.append((nx, ny))
Le hasard dépend d'une graine : la même graine donne le même labyrinthe. On entraîne sur les graines 1 à 20 et on teste sur d'autres. Deux parcours en largeur calculent ensuite la distance de chaque case à la sortie et le nombre minimal d'actions, virages compris.
Le réseau rend 3 valeurs ; on joue la plus forte (argmax). Contrairement au parcours 3D, une seule action à la fois.
| Résultat | Python (par labyrinthe) | Navigateur |
|---|---|---|
| Sorti en n actions | 1 + (1 − n / 400) | 1 + minimum / n |
| Pas sorti | 0,99 × (1 − plus courte distance atteinte / distance au départ) | idem |
Sortir rapporte toujours plus que ne pas sortir. Entre deux pandas bloqués, celui qui s'est le plus approché gagne : l'évolution progresse avant même la première sortie. En Python, on additionne les 20 labyrinthes d'entraînement (maximum 40).
--export-web, chaque partie est enregistrée en lettres (A avancer, G gauche, D droite) : 150 générations × 60 pandas × 3 labyrinthes tiennent en 2 Mo.FeedForwardNetwork de neat-python (activation tanh(2,5 × z)) : le champion Python fait les mêmes actions dans le navigateur.| Difficulté | Solution |
|---|---|
| Avec 5 labyrinthes d'entraînement, le panda les réussissait tous mais échouait sur les inconnus (apprentissage par cœur) | 20 labyrinthes d'entraînement, bilan sur 20 jamais vus |
| Sans vraie mémoire, suivre la boussole est rapide mais peut boucler ; suivre un mur est sûr mais lent | Fitness qui récompense la vitesse ; essai d'un réseau récurrent avec --memoire |
| Pandas qui tournent en rond sans fin | Arrêt après 4 passages au même endroit avec la même orientation |
| L'évolution s'arrêtait au premier chemin optimal | Option pour continuer, bouton « Continuer » |
| Perte de diversité | Espèces maintenues entre 5 et 10, relance de diversité après 40 générations sans progrès |
| « Arrêter » puis « Continuer » ne relançait rien | Bug corrigé dans le Web Worker |
| Calcul différent entre Python et JavaScript | Copie exacte des couches et de l'activation de neat-python, vérifiée sur le champion |
Pour lancer : python labyrinthe.py (300 générations et bilan), python labyrinthe.py --generations 150 --graine 1 --export-web pour régénérer les données de la démo.
Un ninja apprend à traverser un niveau à défilement plein de trous, de caisses, de gardes et d'archers. Tout tourne dans le navigateur : avec 80 ninjas, le premier arrivé apparaît entre la génération 18 et 65 selon le niveau.
| Obstacle | Danger | Parade |
|---|---|---|
| Trou | Tomber : fin de partie | Sauter (un trou fait au plus 114 px, un saut en couvre 115) |
| Caisse | Bloque le passage | Sauter par-dessus ou dessus |
| Garde | Le toucher est mortel, il fait des allers-retours | Shuriken ou saut bien placé |
| Archer | Une flèche toutes les ~2 s quand le ninja approche | Sauter la flèche, la casser au shuriken, ou abattre l'archer |
Une partie s'arrête aussi au bout de 60 secondes, ou si le ninja n'a pas avancé depuis 5 secondes.
Un niveau est une liste de morceaux de sol (les trous sont les espaces entre eux), de caisses et d'ennemis, tirée d'une graine. La difficulté augmente le nombre et la taille des obstacles, sans jamais dépasser ce qu'un saut permet.
const nb = 12 + 4 * difficulte;
for (let i = 0; i < nb; i++) {
const t = r();
if (t < 0.32) { // un trou
const w = Math.round(48 + r() * (24 + 14 * difficulte));
sols.push([debut, x]); x += w; debut = x; x += 130 + r() * 80;
} else if (t < 0.52) { // une caisse
caisses.push([x, 40, Math.round(24 + r() * (14 + 10 * difficulte))]);
} else if (t < 0.8) { // un garde
ennemis.push({ type: "garde", x1: x, x2: x + l });
} else { // un archer
ennemis.push({ type: "archer", x: x + 60 });
}
}
pas(niveau, état, actions) avance le jeu d'une image (1/60 s). Les actions tiennent sur 4 bits : 1 droite, 2 gauche, 4 sauter, 8 lancer. Pas de hasard, pas d'horloge : la même suite d'actions donne toujours la même partie.
Le même code sert à trois choses : faire jouer l'humain, faire jouer l'IA dans le Web Worker, et rejouer une partie enregistrée.
if (a & 1) vx += VIT; // droite : 3 px par image
if (a & 2) vx -= VIT; // gauche
if ((a & 4) && p.sol) { p.vy = SAUT; } // sauter : -11,5 px par image
if ((a & 8) && p.cd <= 0) { // lancer, si le shuriken est prêt
s.shurikens.push({ x: p.x + p.dir * 12, y: p.y - 20, v: 8 * p.dir, reste: 40 });
p.cd = 30;
}
p.vy += GRAV; // gravité : 0,6 px par image²
Le ninja ne voit pas l'écran. Il reçoit des mesures de ce qui arrive devant lui :
Les distances sont divisées par une portée (200 ou 300 px) puis bornées à 1 : « 1 » veut dire « rien en vue ». Chaque sortie positive appuie sur sa touche : le ninja peut courir, sauter et lancer en même temps.
| Résultat | Fitness |
|---|---|
| Arrivé en t images | 1 + (1 − t / 3600) |
| Tombé, touché, bloqué ou temps écoulé | 0,99 × distance la plus loin / longueur du niveau |
| Difficulté | Solution |
|---|---|
| Un niveau tiré au hasard pouvait être infranchissable | Trous limités à 114 px pour un saut de 115 px, caisses bornées |
| Des ninjas immobiles faisaient perdre du temps de calcul | Arrêt après 5 secondes sans avancer |
| Sur téléphone, la vitesse du jeu dépendait de l'écran | La page accumule le temps et appelle pas exactement 60 fois par seconde : l'humain joue avec les mêmes règles que l'IA |
| Stocker toutes les parties pour les revoir | Une lettre hexadécimale par image : 60 s de jeu = 3 600 lettres, rejouées par le même moteur |
| Afficher 12 parties à la fois alors que chacune a ses propres ennemis abattus | Le meilleur est rejoué avec tout son monde, les 11 autres en « fantômes » transparents |
| Contrôles tactiles | Boutons maintenus (on garde le doigt appuyé pour courir) |
| Avec l'option position, le ninja apprend le niveau par cœur | Option laissée au choix : sans elle, il apprend une vraie stratégie (première arrivée à la génération 65 au lieu de 18 sur le niveau 2) |
| Stagnation | Espèces gardées entre 5 et 10 ; après 40 générations sans progrès, la moitié la moins bonne est remplacée |
Dames internationales contre une IA, à deux sur le même écran ou en ligne. Ici, pas de réseau de neurones : l'IA cherche ses coups par alpha-bêta et apprend les 7 poids qui servent à juger une position. Entraînée 30 générations, elle marque 69 % des points contre l'IA de départ (contrôle sur 40 parties).
Seules les 50 cases foncées comptent : un tableau de 50 nombres (0 vide, 1 pion blanc, 2 dame blanche, −1 pion noir, −2 dame noire). La numérotation 1 à 50 est celle des compétitions, ce qui donne directement la notation officielle (32-28, 28x19).
Les « rayons » sont calculés une fois pour toutes : pour chaque case et chaque diagonale, la liste des cases jusqu'au bord. Un pion n'en regarde que la première, une dame les parcourt toutes.
const RAYONS = Array.from({ length: 50 }, (_, s) => DIRS.map(([dr, dc]) => {
const r = [];
let l = LIGNE(s) + dr, c = COL(s) + dc;
while (CASE(l, c) >= 0) { r.push(CASE(l, c)); l += dr; c += dc; }
return r;
}));
Une recherche en profondeur essaie toutes les suites de prises depuis chaque pièce. On ne garde ensuite que les rafles qui prennent le plus de pièces (prise majoritaire).
let i = 0;
while (i < ray.length && b[ray[i]] === 0) i++; // la dame glisse sur les cases vides
const a = ray[i];
if (b[a] * camp >= 0 || prises.includes(a)) continue; // pas adverse, ou déjà prise
i++;
while (i < ray.length && b[ray[i]] === 0) { // toutes les cases libres derrière
dfs(ray[i], [...prises, a], [...chemin, ray[i]]);
i++;
}
L'IA compte 7 critères par camp (pions, dames, avancée, centre, rangée du fond, pions protégés, pions bientôt dame), multipliés chacun par un poids. Elle explore les coups jusqu'à une profondeur donnée (minimax), en coupant les branches inutiles (alpha-bêta), dans un Web Worker.
| Force | Profondeur | Remarque |
|---|---|---|
| Débutant | 1 demi-coup | Avec une part de hasard |
| Moyen | 3 | Bat le jeu au hasard 10 fois sur 10 |
| Fort | 5 | Environ 8 500 positions au premier coup |
| Expert | 7 | Environ 140 000 positions, 0,4 s au premier coup |
Un algorithme évolutionnaire fait évoluer 12 jeux de 7 poids. Chacun joue 4 parties à profondeur 2 ; les 3 meilleurs passent tels quels, les autres places vont à des enfants de deux bons parents, légèrement modifiés. L'IA a appris seule à garder sa rangée du fond (poids de 0,1 à 0,4-0,6) et à valoriser les pions proches de la promotion (de 0,3 à plus de 1).
Un serveur Node sans dépendance tient les parties. Chaque salle a un code de 5 caractères. Chaque navigateur reçoit l'état par un flux Server-Sent Events et envoie ses coups par POST. Le serveur vérifie chaque coup avec le même fichier de règles que la page :
if (p.trait !== c) return repondre(res, 409, { erreur: "Ce n'est pas à vous de jouer." });
if (m.index !== salle.coups.length) return repondre(res, 409, { erreur: "Coup déjà joué ou décalé" });
const legal = liste.find((x) => cle(x) === cle({ de: m.de, a: m.a, prises: m.prises || [] }));
if (!legal) return repondre(res, 422, { erreur: "Coup illégal." });
salle.coups.push(legal);
diffuser(salle);
| Difficulté | Solution |
|---|---|
| La rafle : la pièce peut repasser par sa case de départ, et les pièces prises ne disparaissent qu'à la fin | Case de départ vidée pendant la recherche ; pièces prises gardées sur le damier (une dame ne peut pas les traverser) |
| Juger une position au milieu d'un échange de prises est trompeur | La recherche continue tant qu'il y a des prises, jusqu'à 6 demi-coups de plus |
| Voir loin coûte très cher | Coupures alpha-bêta et recherche dans un Web Worker (la page reste fluide) |
| Parties d'entraînement toutes identiques | Les 2 premiers coups sont joués au hasard |
| Comparer des poids de tailles différentes | Poids remis à l'échelle pour qu'un pion vaille 1 (« une dame vaut 3,6 pions ») |
| Courbe d'entraînement bruitée | Limite connue : 8 parties par génération, c'est peu ; contrôle final sur 40 parties |
| Une page trafiquée pourrait envoyer un coup interdit | Moteur extrait dans dames-moteur.js, partagé par la page, le Worker et le serveur, qui revérifie tout |
| Joueur qui recharge la page ou revient plus tard | Jeton secret gardé dans le navigateur ; la partie est une liste de coups rejouée à la reconnexion |
| Coups qui arrivaient par paquets | Mise en tampon désactivée dans nginx pour le flux |
| Code de salle difficile à dicter | Ni 0, O, 1 ni I dans les codes |
| Salles abandonnées en mémoire | Ménage après 48 heures sans activité |
Le serveur tourne sous pm2 (dames-reseau, port 3077), derrière nginx sur /dames-api/.
Des IA apprennent à trader l'or sur 25 ans de cours réels (simulation, sans argent réel). Résultat principal : sur une période jamais vue (2019-2026), la meilleure IA fait 7 à 14 % par an, quand acheter et garder rapportait 16,4 %.
Les cours viennent de Yahoo Finance (contrat à terme COMEX, symbole GC=F) : 6 550 clôtures quotidiennes depuis 2000 pour l'entraînement, et le cours minute par minute pour le temps réel. Un relais Node sur rdv-app.fr interroge Yahoo à la place du navigateur et garde les réponses en mémoire (6 heures pour l'historique, 30 secondes pour le direct).
Chaque jour, au moins 13 entrées : variations sur 1, 5, 20 et 60 jours, écarts aux moyennes 20, 50 et 200 jours, volatilité, RSI, recul depuis le plus haut d'un an, et sa position actuelle (vendeuse, neutre, acheteuse). Chaque indicateur est divisé par une valeur typique puis borné entre −3 et 3.
const borne = (v) => Math.max(-3, Math.min(3, v));
f[o] = borne(lr(i, 1) / 0.015); // variation sur 1 jour
f[o + 4] = borne((c[i] / moy(20) - 1) / 0.04); // écart à la moyenne 20 jours
f[o + 8] = borne((rsi - 50) / 25); // RSI centré
3 sorties : vendre, rester neutre, acheter ; la plus forte l'emporte. La décision prise à la clôture du jour i s'applique jusqu'à la clôture du lendemain, frais de 0,1 % par changement.
g.fitness = Math.log(r.eq) - 0.5 * r.dd; // gain en logarithme, moins la moitié de la pire chute
La sélection n'utilise que la période d'entraînement. Après chaque génération, la meilleure IA est rejouée sur la période de test, pour information seulement : c'est ce qui fait du test un vrai examen.
| Difficulté | Solution |
|---|---|
| Le navigateur ne peut pas interroger Yahoo (règle CORS) | Relais or-serveur.js sur rdv-app.fr, avec cache ; fichier or.json de secours |
Avec range=max, Yahoo renvoyait 269 points mensuels au lieu de 6 550 quotidiens, sans erreur |
Dates demandées explicitement (period1, period2, interval=1d) |
| Risque d'utiliser un cours du lendemain (IA « miraculeuse » sur le papier) | Indicateurs calculés uniquement avec les cours jusqu'au jour J ; position appliquée au lendemain |
| Un jour de krach faisait « exploser » les calculs | Entrées divisées par une valeur typique et bornées à ±3 |
| Chiffres économiques connus seulement après publication (l'inflation de mars sort vers le 15 avril) | Chaque valeur n'est disponible qu'à sa date de publication (chômage vu le 20 mars 2020 : encore 3,5 %) |
| FRED refuse les requêtes de serveurs | Données prises au BLS et chez Yahoo |
| BLS : 25 requêtes par jour sans clé, 10 ans par requête, « - » pour les mois sans chiffre | Cache dans scripts/cache/, seules les 2 dernières années redemandées, dernière valeur gardée |
Variable m qui masquait les séries économiques dans une boucle |
Paramètre renommé macro |
| Surapprentissage : 21 %/an sur l'entraînement, 7 à 14 % sur le test | Montré sur la page ; l'économie (15 critères) l'aggrave : −7,0 points/an contre le marché, décochée par défaut |
| Les figures de bougies classiques ne prédisent rien (le marteau change de sens entre 2010-2018 et 2019-2026) | 5 mesures brutes de la bougie données à l'IA : écart au marché de −1,7 à −0,4 point/an, gain modeste |
| Un réseau ne dit pas pourquoi il décide | On neutralise un critère à la fois et on compte les jours où la décision change |
| Déplacer la coupure entraînement/test rend les anciens résultats incomparables | Les IA déjà entraînées sont effacées |
La page liste aussi ses simplifications (exécution au cours de clôture, frais fixes, pas de taux de change, chiffres économiques non révisés).
Un sixième essai, en cours : l'IA apprend un vrai jeu Sega dans l'émulateur stable-retro, à partir de l'image de l'écran. C'est là que les difficultés ont été les plus nombreuses. Page : shinobi.html.
| Difficulté | Solution |
|---|---|
| Les états de stable-retro plantaient l'émulateur du navigateur (EmulatorJS) | Conversion des états (etat_navigateur.py) pour rejouer chaque partie à l'image près dans la page |
| Une mort suivie de la réapparition était prise pour une fin de niveau : faux niveaux 3 à 6, tous des copies du 1-2 | Fin de niveau détectée par le compteur de stage du jeu (adresse 0xC092) ; faux niveaux mis de côté |
| La vie n'est pas remplie entre deux niveaux : le 1-2 commençait avec 1 barre | Bonus pour la vie restante et passage au niveau suivant seulement avec au moins 4 barres |
| Pas de bonus shuriken : il faut sauver certains otages, l'IA n'en sauvait que 2 | Otages repérés par le signal 0xC079 accompagné de +1 000 points, et récompensés ; l'IA garde la manette pendant la transition |
| L'IA se faisait toucher sans se baisser | Pénalité par barre de vie perdue |
| En 8 × 8 pixels par case, les ennemis et leurs tirs disparaissaient dans le décor | Image plus fine (4 × 4 pixels par case) et population neuve |
Le réseau qui avait fini le 1-1 a été écrasé (meilleur_n1.pkl réécrit à chaque génération) |
Chaque réseau qui finit un niveau est gardé à part (fini_nN_gG.pkl) |
| Le script de relance croyait l'entraînement déjà lancé et ne démarrait rien | Relance depuis une commande qui ne contient pas le nom du script |
Les mêmes problèmes reviennent d'un jeu à l'autre. Les régler une fois a servi pour tous les suivants.
| Leçon | Où elle est apparue | Ce qu'on fait maintenant |
|---|---|---|
| Un jeu déterministe permet de rejouer exactement chaque partie | Parcours 3D (pas de temps fixe), ninja (pas() à 1/60 s), Shinobi (émulateur) |
On n'enregistre que les actions, jamais les positions |
| La note doit départager les perdants | Parcours 3D (bonus < 1), labyrinthe et ninja (distance atteinte) | Toujours une récompense partielle pour qui s'approche du but |
| La note fait le comportement : ce qui n'est pas récompensé n'est pas appris | Shinobi (otages ignorés, coups encaissés) | Chaque objectif voulu a sa ligne dans la note |
| Bien réussir sur l'entraînement ne prouve rien | Labyrinthe (5 labyrinthes appris par cœur), or (surapprentissage) | Toujours un test sur des cas jamais vus |
| Plus d'informations n'est pas toujours mieux | Or (économie : −7 points/an) | Ajouter une entrée seulement si elle améliore le test |
| L'IA doit voir ce qui compte | Shinobi (tirs invisibles en 8 × 8) | Vérifier à l'œil ce que reçoit le réseau |
| Arrêter tôt les parties inutiles | Ninja (5 s sans avancer), labyrinthe (tourne en rond), Shinobi (bloqué) | Gain de temps de calcul important |
| Le calcul lourd ne doit pas figer la page | Labyrinthe, ninja, dames, or | NEAT ou alpha-bêta dans un Web Worker |
| Un seul moteur partagé évite les écarts | Dames (page, Worker, serveur), labyrinthe (réseau identique à neat-python) | Le même code partout, vérifié sur un champion |
| Garder ce qui a marché avant de changer | Shinobi (réseau du 1-1 perdu) | Sauvegarde à part de chaque réseau gagnant, déplacer plutôt que supprimer |
| Fixer les versions | neat-python 2.0 refuse les configs | neat-python==0.92 partout |