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>
332 lines
15 KiB
Markdown
332 lines
15 KiB
Markdown
# 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.
|