.damier-mini { display: grid; grid-template-columns: repeat(10, 1fr); width: min(100%, 340px); aspect-ratio: 1; border: 4px solid #4a2e17; border-radius: 4px; margin: 0 auto; } .damier-mini span { display: grid; place-items: center; font-family: var(--font-mono); font-size: 10px; } .damier-mini .c { background: #efdcb8; } .damier-mini .f { background: #8b5a33; color: #fff3df; } .damier-mini .f.ray { background: #5f8f3e; } .damier-mini .f.dame { background: #a87a06; font-weight: 700; }
Comment la page applique les règles des dames internationales, comment l'IA choisit ses coups et apprend à mieux juger une position, et comment deux téléphones jouent ensemble à travers le serveur.
Ce sont les dames internationales, celles des clubs français : un damier de 10 × 10, 20 pions chacun sur les cases foncées, les blancs commencent.
Seules les 50 cases foncées comptent. Le damier est donc un simple tableau de 50 nombres : 0 case vide, 1 pion blanc, 2 dame blanche, -1 pion noir, -2 dame noire. Les cases sont numérotées comme sur un vrai damier de compétition (1 à 50), ce qui donne directement la notation officielle des coups : 32-28, 28x19.
Pour aller vite, on calcule une fois pour toutes les rayons : pour chaque case et chacune des 4 diagonales, la liste des cases rencontrées jusqu'au bord. Un pion n'en regarde que la première case, 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;
}));
Le plus délicat est la rafle. Pour chaque pièce, une recherche en profondeur essaie toutes les suites de prises : depuis la case courante, dans chaque direction, peut-on sauter une pièce adverse pas encore prise et atterrir sur une case libre ? Si oui, on recommence depuis la case d'arrivée. Quand plus aucune prise n'est possible, la rafle est terminée et on la note avec le nombre de pièces prises.
Deux détails comptent. La case de départ est vidée pendant la recherche, car la pièce peut repasser par là. Les pièces déjà prises restent sur le damier jusqu'à la fin : une dame ne peut donc pas les traverser.
let i = 0;
while (i < ray.length && b[ray[i]] === 0) i++; // on glisse sur les cases vides
const a = ray[i];
if (b[a] * camp >= 0 || prises.includes(a)) continue; // pas une pièce 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]]); // la rafle continue depuis chacune
i++;
}
On garde ensuite seulement les rafles qui prennent le plus de pièces, toutes pièces confondues : c'est la règle majoritaire. S'il n'y a aucune prise, on liste les déplacements simples. La page montre alors seulement les pièces jouables, et sélectionne d'office la pièce quand il n'y en a qu'une.
Pour comparer deux positions, l'IA compte 7 critères pour chaque camp et fait la différence, chaque critère multiplié par un poids. C'est ce jeu de 7 poids qui fait « le caractère » de l'IA, et c'est lui qu'elle apprend.
| Critère | Ce qu'on compte | Poids de départ |
|---|---|---|
| Pion | Nombre de pions | 1 |
| Dame | Nombre de dames | 3 |
| Avancée des pions | Rangées parcourues par chaque pion (0 à 1) | 0,1 |
| Contrôle du centre | Pièces sur les 10 cases centrales | 0,1 |
| Rangée du fond gardée | Pions restés sur sa première rangée (ils empêchent l'adversaire d'aller à dame) | 0,1 |
| Pion protégé | Pions avec une pièce amie ou le bord juste derrière | 0,05 |
| Pion bientôt dame | Pions sur les 3 dernières rangées | 0,3 |
L'IA imagine tous ses coups, toutes les réponses, ses réponses aux réponses, et ainsi de suite jusqu'à une certaine profondeur. Au bout de chaque branche, elle juge la position. Elle suppose que chaque camp choisit le meilleur coup pour lui (minimax), et coupe les branches qui ne peuvent plus changer le résultat (alpha-bêta), ce qui permet de voir bien plus loin pour le même calcul.
Une précaution propre aux dames : si la position au bout d'une branche contient une prise obligatoire, la juger maintenant serait trompeur (une pièce est sur le point de tomber). La recherche continue donc tant qu'il y a des prises, jusqu'à 6 demi-coups de plus.
const nega = (q, prof, alpha, beta, ext) => {
const liste = coups(q);
if (!liste.length) return -1000 - prof; // plus de coup : perdu
const forcee = liste[0].prises.length > 0;
if (prof <= 0 && (!forcee || ext <= 0)) return evaluer(q, w);
let best = -Infinity;
for (const m of liste) {
const v = -nega(jouer(q, m), ..., -beta, -alpha, ...);
if (v > best) best = v;
if (v > alpha) alpha = v;
if (alpha >= beta) break; // coupure alpha-bêta
}
return best;
};
| Force | Profondeur | Remarque |
|---|---|---|
| Débutant | 1 demi-coup | Avec une bonne part de hasard dans le choix. |
| Moyen | 3 | Bat le jeu au hasard 10 fois sur 10. |
| Fort | 5 | Environ 8 500 positions examinées au premier coup. |
| Expert | 7 | Environ 140 000 positions, 0,4 s au premier coup, plus en milieu de partie. |
La recherche tourne dans un Web Worker : pendant que l'IA réfléchit, la page reste fluide.
Ici, pas de réseau de neurones comme pour les pandas : ce qui s'apprend, ce sont les 7 poids. On utilise un algorithme évolutionnaire, le même principe que NEAT mais sur une liste de nombres au lieu d'un réseau.
Les poids sont toujours remis à l'échelle pour qu'un pion vaille 1 : on lit directement « une dame vaut 3,6 pions ». Chaque génération est gardée, et vous pouvez choisir l'IA de n'importe laquelle comme adversaire.
Résultat mesuré sur 30 générations : l'IA entraînée marque 69 % des points contre l'IA de départ, au contrôle sur 40 parties. Elle a appris seule à garder sa rangée du fond (poids de 0,1 à 0,4-0,6) et à valoriser fortement les pions proches de la promotion (de 0,3 à plus de 1). La courbe génération par génération est bruitée : 8 parties, c'est peu pour départager deux IA proches.
Pour jouer à deux sur deux téléphones, un petit serveur Node (serveur/dames-serveur.js, sans aucune dépendance) tient les parties. Chaque partie est une salle avec un code de 5 caractères, sans 0, O, 1 ni I pour pouvoir le dicter.
dames.html?salle=CODE et reçoit l'autre place et son propre jeton.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);
Le serveur charge le même fichier de règles que la page (dames-moteur.js) : une page trafiquée ne peut pas faire accepter un coup interdit. Comme la partie est stockée sous forme de liste de coups, un joueur qui revient reçoit la liste et reconstruit la position.
Le flux passe par nginx avec la mise en tampon désactivée, sinon les coups arriveraient par paquets. Les salles vivent en mémoire et disparaissent après 48 heures sans activité.
| Requête | Rôle |
|---|---|
POST /dames-api/salles | Créer une partie (couleur au choix) |
POST …/CODE/rejoindre | Prendre la place libre, ou retrouver la sienne avec son jeton |
GET …/CODE/flux | Recevoir l'état de la partie en continu |
POST …/CODE/coup | Jouer un coup (vérifié) |
POST …/CODE/abandon, …/revanche | Abandonner ; rejouer en échangeant les couleurs |
| Fichier | Contenu |
|---|---|
web/dames-moteur.js | Règles, jugement d'une position, recherche alpha-bêta, partie entre deux IA |
web/dames.html | Damier, modes de jeu, Web Worker (réflexion et entraînement), mode en ligne |
serveur/dames-serveur.js | Serveur des parties en ligne (service pm2 dames-reseau, port 3077) |
pm2 start serveur/dames-serveur.js --name dames-reseau # lancer le serveur des parties en ligne
pm2 restart dames-reseau # après une modification (les parties en cours sont perdues)
./deploy.sh # publier les pages sur rdv-app.fr