Files
escampe-ia/RAPPORT.md
Ethan Puyaubreau e508efa14f Joueur IA Escampe (Puyaubreau/Russac) — version finale
Joueur alpha-bêta + iterative deepening pour le tournoi APP5 « IA et contraintes ».

- src/escampe/ : joueur (IJoueur), moteur (alpha-bêta + DFS bitmask, make/unmake
  sans allocation), modèle EscampeBoard (Partie1), utilitaires de test.
- Protocole arbitre vérifié (pass="E", carte des liserés identique au serveur,
  machine à états placement/jeu) ; 7/7 victoires vs joueur aléatoire, 0 illégal.
- Vérifications : VerifMoves (int≡String, 0 divergence/142k positions),
  RulesTest (21/21), Branching (facteur de branchement mesuré).
- Rapport : report/rapport.html + tools/make_report_pdf.py (PyMuPDF) → PDF, RAPPORT.md.
- Livrables buildés inclus (dist/ : jar, mainClass, tgz, rapport PDF) + lib/escampeobf.jar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 16:00:29 +02:00

15 KiB
Raw Blame History

Escampe — Rapport (version finale)

Université Paris-Saclay — Polytech APP5 — Année 2025-2026 — « IA et contraintes » Binôme : Ethan Puyaubreau & Antonin Russac — 30 mai 2026 Joueur : escampe.JoueurPuyaubreauRussac

Ce fichier est le miroir Markdown du rapport. La version PDF mise en page (dist/Puyaubreau_Russac_rapport.pdf) est générée depuis report/rapport.html par python tools/make_report_pdf.py (PyMuPDF, sans dépendance externe).


1. Présentation et règles

Escampe se joue sur un plateau de 36 cases (6×6). Chaque case porte un liseré simple, double ou triple. Chaque joueur dispose d'une licorne et de cinq paladins (noirs ou blancs). Lignes numérotées de 1 à 6, colonnes de A à F. Le but est de prendre la licorne adverse.

Règle caractéristique — la contrainte de liseré : la pièce jouée doit partir d'une case dont le liseré est identique à celui de la case d'arrivée du coup adverse précédent. Le liseré de départ fixe le nombre de pas (1, 2 ou 3), orthogonaux, sans traverser ni revisiter de case. On ne capture qu'en se posant, au dernier pas, sur la licorne adverse (paladins imprenables). Sans coup possible, on passe son tour.

Déroulement : Noir place ses six pièces sur les deux lignes d'un bord ; Blanc sur le bord opposé ; Blanc joue le premier coup.


2. Analyse des caractéristiques du jeu (Q1Q7)

Q1 — Modélisation d'un état

Plateau int[6][6] (board[ligne][colonne], ligne 0 = ligne 1 en bas, colonne 0 = A). Chaque case : EMPTY, WHITE_LICORNE, WHITE_PALADIN, BLACK_LICORNE, BLACK_PALADIN. État hors-plateau : lastTileType (liseré imposé, -1 = libre), currentPlayer, blackPlaced/whitePlaced, blackRows (bord de Noir).

  • Avantages : accès O(1), copie immédiate pour l'arbre de recherche, sérialisation triviale, et surtout make/unmake sans allocation (clé de la vitesse, §6).
  • Inconvénient : la contrainte de liseré est un état séparé à maintenir (encapsulé dans play).

Carte des liserés TILE_MAP (figure 4, ligne 1 en bas) :

       A  B  C  D  E  F
 6     3  2  2  1  3  2
 5     1  3  1  3  1  2
 4     2  1  3  2  3  1
 3     2  3  1  2  1  3
 2     3  1  3  1  3  2
 1     1  2  2  3  1  2

Vérifié : cette carte est identique, case pour case, à celle utilisée en interne par l'arbitre (extraite par réflexion de la classe de jeu du serveur), et cohérente avec l'exemple tactique de la figure 6. Point critique : une carte divergente aurait produit des coups jugés illégaux.

Q2 — Détection de fin de partie

Partie finie dès qu'une licorne disparaît (seul cas, pas de nul). Balayage O(1) (gameOver) ; le moteur détecte la capture au moment où elle est jouée.

Q3 — Sources de difficulté et facteur de branchement

Difficultés : contrainte de liseré (mobilité variable), dépendance entre tours (la case d'arrivée détermine les options adverses), asymétrie du plateau, risque de blocage / pass forcé.

Facteur de branchement. Borne théorique lâche estimée en partie 1 : ~120. La mesure réelle (utilitaire escampe.Branching, 30 000 parties aléatoires) est bien plus basse car la contrainte de liseré ne laisse jouables que les pièces du bon liseré :

Situation Branchement max observé
Coup contraint (un liseré imposé) 45
Coup libre (1er coup ou après pass) 49
Branchement moyen (toutes positions) ≈ 8,9

Le branchement effectif modeste explique les profondeurs élevées atteintes par l'alpha-bêta (§6).

Q4 — Coups imparables

Pas d'« imparable » universel garanti dès le départ (la contrainte de liseré peut toujours bloquer une menace). Mais des configurations créent un zugzwang partiel (exemple figure 6 : C2 prend C1 dès que Noir est forcé d'imposer un liseré double). Notre recherche les exploite quand ils sont à portée d'horizon.

Q5 — Critères pour l'heuristique

Cinq critères identifiés : distance à la licorne adverse, mobilité différentielle, contrôle du liseré imposé, protection de sa licorne, avancée. Retenu en pratique (§7) : proximité des paladins à la licorne adverse (attaque) et éloignement des paladins adverses de notre licorne (défense) — le reste est largement pris en charge par la recherche.

Q6 — Stratégie selon la phase

  • Début (placement) : irréversible ; protéger la licorne, garantir de toujours pouvoir jouer (§5).
  • Milieu : manœuvrer pour menacer la licorne adverse en contrôlant le liseré imposé ; chercher le zugzwang partiel.
  • Fin : dès qu'une capture est à portée, le calcul tactique prime.

Q7 — Majorant du nombre de coups et gestion du temps

Aucune pièce ne disparaît avant la capture finale : borne raisonnable ~400600 demi-coups. Pour tenir les 300 s/joueur : approfondissement itératif, alpha-bêta, budget par coup dérivé du temps restant (§8).


3. Modélisation : la classe EscampeBoard

EscampeBoard (~860 lignes) implémente Partie1 (setFromFile/saveToFile, isValidMove, possiblesMoves, play, gameOver). Conventions de l'arbitre : coup "B1-D1", placement "C6/A6/B5/D5/E6/F5" (licorne en tête), pass "E".

Format fichier : 6 lignes de plateau (bas→haut), N/n B/b -, encadrées d'un numéro ; lignes % = commentaires (où l'on stocke l'état hors-plateau pour un rechargement fidèle).

Génération des coups : DFS avec retour arrière (exactement N pas, intermédiaires vides, dernière case vide ou licorne adverse). possiblesMoves filtre le bon liseré et renvoie ["E"] si bloqué. Une méthode main illustre placements, liseré, pass, round-trip fichier, capture.

Bug latent corrigé en partie 3 : un placement légal sur une seule ligne faisait planter le calcul du bord de Noir (supposait deux lignes). Le bord est désormais déduit de la ligne de la licorne.


4. Intégration au tournoi : protocole de l'arbitre

JoueurPuyaubreauRussac implements IJoueur enveloppe un EscampeBoard tenu à jour à chaque coup (le nôtre via play, l'adverse via mouvementEnnemi). Trois adaptations, dont deux vérifiées par analyse du jar obfusqué :

  • Couleurs : IJoueur en entiers (NOIR=1, BLANC=-1), EscampeBoard en "noir"/"blanc".
  • Pass = "E", pas "PASSE" : le Javadoc d'IJoueur dit "PASSE", mais la classe serveur teste move.equals("E") (et "PASSE" est absent du jar). Envoyer "PASSE" = défaite sur coup illégal.
  • Carte des liserés identique au serveur (cf. Q1).

Machine à états : placement et coups passent par le même canal. Premier choixMouvement = placement, suivants = coups ; phase détectée via blackPlaced/whitePlaced. Séquence (déduite de Solo) :

Noir  : choixMouvement(placement) -> mvtEnnemi(placement Blanc)
        -> mvtEnnemi(1er coup Blanc) -> choixMouvement(coup) -> ...
Blanc : mvtEnnemi(placement Noir) -> choixMouvement(placement)
        -> choixMouvement(1er coup, Blanc rejoue) -> mvtEnnemi(coup Noir) -> ...

Exécution (3 processus) :

java -cp escampeobf.jar        escampe.ServeurJeu 1234 1
java -cp Puyaubreau_Russac.jar escampe.ClientJeu escampe.JoueurPuyaubreauRussac localhost 1234
java -cp escampeobf.jar        escampe.ClientJeu escampe.JoueurAleatoire        localhost 1234

5. Placement d'ouverture

Constat issu de l'auto-jeu : une licorne mal placée peut se retrouver seule pièce jouable et bloquée sur le liseré imposé → passes successifs → perte d'initiative. Trois principes :

  1. Licorne dans un coin — un coin n'a que 2 voisines, donc 2 cases d'attaque.
  2. Murs — on occupe ces 2 voisines par des paladins : licorne incapturable tant que les murs tiennent.
  3. Couverture des liserés — les 3 paladins restants sur des liserés 1, 2 et 3 distincts : jamais de pass forcé, jamais besoin de bouger un mur ou la licorne.

Dispositions retenues (Blanc joue le bord complémentaire de Noir) :

Bord bas  A1/A2/B1/E1/F1/C2          Bord haut  A6/A5/B6/C5/F5/E6
  licorne A1, murs A2/B1,              licorne A6, murs A5/B6,
  mobiles E1·F1·C2 = liserés 1·2·3     mobiles C5·F5·E6 = liserés 1·2·3

6. Moteur de décision

Negamax + élagage alpha-bêta + approfondissement itératif (Moteur), sur une copie du plateau. Capture de licorne = nœud terminal WIN - ply (gagner vite).

Astuces de performance :

  • Coups en entier (case = ligne*6+colonne, coup = départ*36+arrivée) : pas de chaîne dans la boucle chaude.
  • DFS sur masque de bits long (36 cases ⊂ 64 bits) : ensembles visité/ atteignable en masques, sans allocation par appel.
  • make/unmake sans allocation : un petit jeton d'annulation → millions de nœuds sans pression GC.
  • Buffers de coups pré-alloués, un par profondeur.
  • Ordonnancement : capture de licorne essayée en premier ; meilleur coup d'une itération replacé en tête de la suivante.

Cohérence : le chemin « entier » du moteur double le chemin « chaîne » vérifié. VerifMoves (§9) prouve qu'ils produisent les mêmes coups et états — optimiser n'a pas changé les règles.

Performance mesurée : ~45 M nœuds/s ; profondeur 1215 demi-coups en 6 s (plus dans les positions étroites). Les gains forcés annoncés se concrétisent par une capture.


7. Heuristique d'évaluation

Matériel constant → évaluation purement positionnelle, du point de vue du joueur au trait, à partir des distances de Manhattan :

  • Attaque : proximité de nos paladins à la licorne adverse — terme somme (pression globale) + terme minimum (l'attaquant le plus proche pèse plus) ;
  • Défense : éloignement des paladins adverses de notre licorne — mêmes termes, signe opposé.

Avec les poids retenus (somme = 2, minimum = 8) :

eval = 2·Σ(10d_attaque)  2·Σ(10d_défense)
       + 8·(10min d_attaque)  8·(10min d_défense)

Heuristiques testées et choix (réglage par auto-jeu déterministe + matchs vs aléatoire) : (a) somme seule → jeu trop diffus ; (b) somme + minimum (retenue) → le terme minimum fortement pondéré oriente les paladins vers la licorne adverse et améliore le taux de capture ; (c) terme défensif symétrique conservé (évite d'exposer notre licorne). Le fort poids du minimum reflète que c'est l'attaquant le plus avancé qui décide d'une prise.

Limite assumée : poids validés contre l'aléatoire et en auto-jeu, faute d'adversaires IA tiers. Les tactiques à court terme sont gérées par la recherche, ce qui rend le joueur robuste malgré une évaluation simple.


8. Gestion du temps réel

Limite arbitre 300 s/joueur/partie → enveloppe interne 280 s (~20 s de marge). Budget par coup :

tranche = clamp( temps_restant / 12 , 120 ms , 6000 ms )

La division par le temps restant décroît géométriquement : budget jamais épuisable. Plafond 6 s (pas de surinvestissement en ouverture), plancher 120 ms, mode « panique » pour les dernières secondes. L'approfondissement itératif rend le meilleur coup déjà trouvé dès l'expiration de la tranche (temps contrôlé toutes les 2048 explorations de nœuds).

Mesures (auto-jeu équilibré, plein budget) : max ≈ 6,0 s/coup (le plafond), cumul max ≈ 36 s/joueur sur une partie complète — très loin des 300 s. Réglage conservateur, augmentable sans risque.


9. Performances et tests

Test Garantit Résultat
VerifMoves chemin entier ≡ chemin chaîne (coups + make/unmake) 3 000 parties · 142 165 positions · 1 281 985 contrôles · 0 divergence
RulesTest règles directes (pas/liseré, capture, imprenabilité, non-traversée, pass, fin, placement) 21 / 21
Matchs arbitrés vs JoueurAleatoire protocole de bout en bout, légalité 7 / 7 victoires, 0 illégal, 0 exception (2 couleurs)
Démo IA vs IA (serveur réel) partie complète moteur vs moteur, pass 21 coups, fin propre par capture
Bench / Branching vitesse, profondeur, branchement ~45 M nœuds/s ; prof. 1215 ; branchement max 49 / moyen ≈ 8,9

Séparation des rôles : VerifMoves (moteur ≡ EscampeBoard), RulesTest (EscampeBoard ≡ règles), parties arbitrées (dialogue correct avec l'arbitre réel). Aucun coup illégal sur l'ensemble des parties jouées.


10. Compilation, exécution et livrables

build.sh produit dans dist/ les trois livrables de la version finale :

Puyaubreau_Russac.jar    jar exécutable (Main-Class : escampe.ClientJeu)
mainClass                jar:Puyaubreau_Russac.jar
                         clientClass:escampe.ClientJeu
                         mainClass:escampe.JoueurPuyaubreauRussac
Puyaubreau_Russac.tgz    archive : Puyaubreau_Russac/ { src/escampe/*.java, mainClass, jar }

Seules les classes de production entrent dans le jar ; les utilitaires de test (VerifMoves, RulesTest, Bench, Branching) en sont exclus. Le multijoueur (humain vs humain, humain vs IA, local ou distant) est documenté dans MULTIJOUEUR.md.


11. Sources et bibliographie

  • Énoncé du cours (Université Paris-Saclay, Polytech APP5, 2025-2026) : règles, carte des liserés (figure 4), interface Partie1, classes fournies (IJoueur, ClientJeu, Solo, Applet, serveur).
  • Algorithmes classiques, pour inspiration sans copie de code : alpha-bêta (Knuth & Moore, 1975) ; minimax/negamax/approfondissement itératif (Russell & Norvig, AIMA) ; masques de bits et ordonnancement de coups (Chess Programming Wiki).
  • Déclaration : aucun programme d'Escampe externe recopié. La seule rétro-ingénierie porte sur le jar d'arbitre fourni avec le sujet, pour confirmer le protocole (pass "E") et la carte des liserés (documentation ambiguë).

12. Conclusion et difficultés rencontrées

Le joueur conduit une partie de façon autonome, dialogue correctement avec l'arbitre, ne produit jamais de coup illégal et respecte très confortablement la contrainte de temps. Difficultés principales :

  • Obfuscation du serveur : lever l'ambiguïté du pass ("E" vs "PASSE") et confirmer la carte des liserés a nécessité l'analyse du jar — décisif pour ne pas perdre sur coup illégal.
  • Interface obfusquée vs nos sources : le joueur aléatoire du jar n'implémente pas notre IJoueur ; les tests contre lui passent par le réseau.
  • Avantage du trait : en miroir, Blanc garde l'initiative via la contrainte de liseré — propriété du jeu.
  • Réglage de l'heuristique sans adversaires : validé contre l'aléatoire et en auto-jeu.

Pistes d'amélioration : table de transposition (Zobrist), bibliothèque d'ouvertures de placement, terme de mobilité différentielle, recherche de quiescence sur les menaces de capture.