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>
This commit is contained in:
2026-05-30 16:00:29 +02:00
commit e508efa14f
50 changed files with 6521 additions and 0 deletions

331
RAPPORT.md Normal file
View File

@@ -0,0 +1,331 @@
# 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.