# 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 (Q1–Q7) ### 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 ~400–600 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** : ~4–5 M nœuds/s ; profondeur 12–15 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·Σ(10−d_attaque) − 2·Σ(10−d_défense) + 8·(10−min d_attaque) − 8·(10−min 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 | ~4–5 M nœuds/s ; prof. 12–15 ; 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.