Rapport : intègre les ajouts du binôme + passe de relecture

- Sommaire, exemple tactique Q4 (figure 6) et note « pas de bibliothèque
  d'ouvertures » repris du commit d'Antonin et portés dans report/rapport.html
  (source du PDF), jusque-là seulement dans RAPPORT.md.
- Exemple Q4 vérifié contre TILE_MAP : liserés D4/F6=2, E5=1, A2/C2=3 et chemin
  de capture C2→D2→D1→C1 (3 pas = liseré de C2) tous corrects.
- Relecture du style sur tout le rapport ; correction de deux coquilles
  (« énnoncé », ancre de sommaire). HTML et RAPPORT.md tenus en miroir.
- PDF régénéré (9 pages, sommaire inclus) ; chiffres mesurés inchangés.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 20:39:15 +02:00
parent cfc1ff0b72
commit 052a3bf978
7 changed files with 534 additions and 448 deletions

View File

@@ -13,8 +13,8 @@ Joueur : `escampe.JoueurPuyaubreauRussac`
## Sommaire ## Sommaire
1. [Présentation et règles](#1-présentation-et-règles) 1. [Présentation et règles](#1-présentation-et-règles)
2. [Analyse des caractéristiques du jeu (Q1Q7)](#2-analyse-des-caractéristiques-du-jeu-q1q7) 2. [Analyse des caractéristiques du jeu (Q1 à Q7)](#2-analyse-des-caractéristiques-du-jeu)
3. [Modélisation : la classe `EscampeBoard`](#3-modélisation--la-classe-escapeboard) 3. [Modélisation : la classe `EscampeBoard`](#3-modélisation--la-classe-escampeboard)
4. [Intégration au tournoi : protocole de l'arbitre](#4-intégration-au-tournoi--protocole-de-larbitre) 4. [Intégration au tournoi : protocole de l'arbitre](#4-intégration-au-tournoi--protocole-de-larbitre)
5. [Placement d'ouverture](#5-placement-douverture) 5. [Placement d'ouverture](#5-placement-douverture)
6. [Moteur de décision](#6-moteur-de-décision) 6. [Moteur de décision](#6-moteur-de-décision)
@@ -31,33 +31,43 @@ Joueur : `escampe.JoueurPuyaubreauRussac`
Escampe se joue sur un plateau de 36 cases (6×6). Chaque case porte un liseré 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 *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 **paladins** (noirs ou blancs). Les lignes vont de 1 à 6, les colonnes de A à F, et
but est de **prendre la licorne adverse**. le but est de **prendre la licorne adverse**.
Règle caractéristique — la **contrainte de liseré** : la pièce jouée doit partir Ce qui fait l'originalité du jeu, c'est la **contrainte de liseré** : la pièce qu'on
d'une case dont le liseré est *identique* à celui de la case d'arrivée du coup joue doit partir d'une case dont le liseré est le même que celui de la case
adverse précédent. Le liseré de départ fixe le nombre de pas (1, 2 ou 3), l'adversaire vient de poser sa pièce. Ce liseré de départ fixe aussi le nombre de pas
orthogonaux, sans traverser ni revisiter de case. On ne capture qu'en se posant, (1, 2 ou 3), orthogonaux, sans traverser ni repasser sur une case déjà visitée. On ne
au dernier pas, sur la licorne adverse (paladins imprenables). Sans coup possible, capture qu'en s'arrêtant, au dernier pas, sur la licorne adverse ; les paladins, eux,
on passe son tour. Toute la difficulté consiste donc à "coincer" son adversaire. sont imprenables. Un joueur qui ne peut rien jouer passe son tour. Toute la difficulté
revient donc à coincer l'adversaire en lui imposant des liserés qui le bloquent.
Déroulement : Noir place ses six pièces sur les deux lignes d'un bord (spécifié dans l'énnoncé : haut ou bas) ; Blanc sur le bord opposé ; **Blanc joue le premier coup**. Pour le déroulement, Noir place d'abord ses six pièces sur les deux lignes d'un bord
de son choix (haut ou bas), puis Blanc fait de même sur le bord opposé, et c'est
**Blanc qui joue le premier coup**.
--- ---
## 2. Analyse des caractéristiques du jeu (Q1Q7) ## 2. Analyse des caractéristiques du jeu
Nous reprenons les sept questions de la première partie, cette fois à la lumière du
code que nous avons réellement écrit.
### Q1 — Modélisation d'un état ### Q1 — Modélisation d'un état
Plateau `int[6][6]` (`board[ligne][colonne]`, ligne 0 = ligne 1 en bas, colonne Le plateau est un tableau `int[6][6]` (`board[ligne][colonne]`, ligne 0 = ligne 1 en
0 = A). Chaque case : `EMPTY`, `WHITE_LICORNE`, `WHITE_PALADIN`, `BLACK_LICORNE`, bas, colonne 0 = A). Chaque case vaut `EMPTY`, `WHITE_LICORNE`, `WHITE_PALADIN`,
`BLACK_PALADIN`. État hors-plateau : `lastTileType` (liseré imposé, `-1` = libre), `BLACK_LICORNE` ou `BLACK_PALADIN`. Quatre informations que le tableau ne porte pas,
`currentPlayer`, `blackPlaced`/`whitePlaced`, `blackRows` (bord de Noir). mais dont la règle a besoin, sont gardées à côté : `lastTileType` (le liseré imposé,
`-1` quand il n'y a pas de contrainte), `currentPlayer`, les drapeaux
`blackPlaced`/`whitePlaced`, et `blackRows` (le bord de Noir, qui détermine celui de
Blanc).
- **Avantages** : accès O(1), copie immédiate pour l'arbre de recherche, Le tableau d'entiers donne un accès en O(1) à n'importe quelle case et se copie sans
sérialisation triviale, et surtout `make/unmake` sans allocation (clé de la effort, ce qui compte pour l'arbre de recherche ; il se sérialise aussi directement
vitesse, §6). vers le format de fichier. Surtout, il autorise un schéma `make`/`unmake` qui n'alloue
- **Inconvénient** : la contrainte de liseré est un état séparé à maintenir rien (voir §6). Le seul point gênant est que la contrainte de liseré vit en dehors du
(encapsulé dans `play`). tableau : il faut penser à la mettre à jour à chaque coup, ce que nous centralisons
dans `play`.
Carte des liserés `TILE_MAP` (figure 4, ligne 1 en bas) : Carte des liserés `TILE_MAP` (figure 4, ligne 1 en bas) :
@@ -71,113 +81,126 @@ Carte des liserés `TILE_MAP` (figure 4, ligne 1 en bas) :
1 1 2 2 3 1 2 1 1 2 2 3 1 2
``` ```
> **Vérifié** : cette carte est identique, case pour case, à celle utilisée en > Nous avons extrait par réflexion la carte qu'utilise l'arbitre dans sa propre classe
> interne par l'arbitre (extraite par réflexion de la classe de jeu du serveur), > de jeu, et elle coïncide case pour case avec la nôtre (elle colle aussi à l'exemple
> et cohérente avec l'exemple tactique de la figure 6. Point critique : une carte > de la figure 6). La vérification valait le coup : une carte fausse aurait fait
> divergente aurait produit des coups jugés illégaux. > rejeter nos coups par l'arbitre.
### Q2 — Détection de fin de partie ### Q2 — Détection de fin de partie
Partie finie dès qu'une licorne disparaît (seul cas, pas de nul). Balayage O(1) La partie s'arrête dès qu'une des deux licornes quitte le plateau ; il n'y a pas
(`gameOver`) ; le moteur détecte la capture au moment où elle est jouée. d'autre cas, donc pas de nul. Le test (`gameOver`) est un simple balayage en O(1). En
recherche, le moteur n'attend même pas ce balayage : il repère la capture à l'instant
où le coup la produit.
### Q3 — Sources de difficulté et facteur de branchement ### Q3 — Sources de difficulté et facteur de branchement
Difficultés : contrainte de liseré (mobilité variable), dépendance entre tours Quatre choses rendent le jeu retors : la contrainte de liseré, qui fait varier
(la case d'arrivée détermine les options adverses), asymétrie du plateau, risque fortement la mobilité ; la dépendance entre tours, puisque la case d'arrivée qu'on
de blocage / pass forcé. choisit dicte les pièces que l'adversaire pourra bouger ; l'asymétrie du plateau, avec
des zones riches en liserés triples (mobiles) et d'autres en liserés simples ; et le
risque qu'une pièce, voire un joueur entier, se retrouve bloqué et doive passer.
**Facteur de branchement.** Borne théorique lâche estimée en partie 1 : ~120. La Côté **facteur de branchement**, nous avions avancé en première partie une borne
mesure réelle (utilitaire `escampe.Branching`, 30 000 parties aléatoires) est bien théorique de l'ordre de 120 (six pièces, jusqu'à une vingtaine de destinations sur un
plus basse car la contrainte de liseré ne laisse jouables que les pièces du bon liseré triple). En pratique c'est beaucoup moins, parce que la contrainte de liseré ne
liseré : laisse jouables que les pièces du bon type. Une simulation de 30 000 parties aléatoires
(utilitaire `escampe.Branching`) donne :
| Situation | Branchement max observé | | Situation | Branchement max observé |
|---|---| |---|---|
| Coup contraint (un liseré imposé) | **45** | | Coup contraint (un liseré imposé) | **45** |
| Coup libre (1er coup ou après pass) | **49** | | Coup libre (1er coup ou après un pass) | **49** |
| Branchement moyen (toutes positions) | **≈ 8,9** | | Branchement moyen (toutes positions) | **≈ 8,9** |
Le branchement effectif modeste explique les profondeurs élevées atteintes par Avec une moyenne sous 10, l'alpha-bêta descend profond en quelques secondes (§6).
l'alpha-bêta (§6).
### Q4 — Coups imparables ### Q4 — Coups imparables
Il n'y a pas de coup gagnant à coup sûr dès le départ : comme l'adversaire choisit sa
case d'arrivée, donc le liseré qu'il nous impose, il peut toujours désamorcer une
menace au mauvais moment. Ce qui existe, en revanche, ce sont des positions de
**zugzwang partiel**, où il est forcé d'imposer précisément le liseré qui ouvre la
capture.
Pas d'« imparable » universel garanti dès le départ : la contrainte de liseré peut L'énoncé en donne un cas net (figure 6). Noir vient de jouer en D4, une case à liseré
toujours bloquer une menace, car l'adversaire choisit sa case d'arrivée et donc le double, donc Blanc doit partir d'une case double : il choisit **F6 E5** (F6 est
liseré qu'il impose au tour suivant. En revanche, certaines configurations créent double). Noir est alors contraint de jouer depuis un liseré simple comme E5, et son
un **zugzwang partiel** — l'adversaire est forcé d'imposer précisément le liseré seul coup raisonnable est **A1 A2**. Or A2 est à liseré triple : Blanc enchaîne
qui autorise la capture. **C2 × C1**, son paladin en C2 parcourant les trois pas C2 → D2 → D1 → C1 pour prendre
la licorne noire. La séquence est imparable localement : une fois Noir poussé en A2,
il ne peut plus empêcher la prise.
**Exemple concret (figure 6 de l'énoncé).** Noir vient de poser sa pièce en D4 (liseré double, Ce genre de combinaison ne se construit pas mécaniquement depuis l'ouverture, il y a
TILE\_MAP = 2). Blanc doit donc jouer depuis une case à liseré double. Il choisit trop de degrés de liberté ; mais notre alpha-bêta la trouve et la joue dès qu'elle
**F6E5** (F6 est à liseré double). Noir doit maintenant jouer depuis une case à entre dans son horizon de recherche.
liseré simple (E5 = 1) : son seul coup raisonnable est **A1A2**. A2 est à liseré
triple (TILE\_MAP = 3). Blanc joue alors **C2×C1** : le paladin en C2 (liseré 3)
effectue les trois pas C2→D2→D1→C1 et capture la licorne noire. La séquence
F6E5 / A1A2 / C2×C1 est donc un « imparable local » : dès que Noir est forcé
d'atterrir en A2, la capture est inévitable.
Ce type de combinaison est inexploitable de façon générale depuis le début de la
partie (trop de degrés de liberté), mais notre alpha-bêta le détecte et le joue
dès qu'il est à portée d'horizon.
### Q5 — Critères pour l'heuristique ### Q5 — Critères pour l'heuristique
Cinq critères identifiés : distance à la licorne adverse, mobilité différentielle, Cinq critères nous semblaient pertinents : la distance à la licorne adverse, la
contrôle du liseré imposé, protection de sa licorne, avancée. Retenu en pratique mobilité différentielle, le contrôle du liseré qu'on impose, la protection de sa
(§7) : proximité des paladins à la licorne adverse (attaque) et éloignement des propre licorne et l'avancée des pièces. Au final (§7), l'évaluation retenue tient
paladins adverses de notre licorne (défense) — le reste est largement pris en surtout à deux d'entre eux, la proximité de nos paladins à la licorne adverse (attaque)
charge par la recherche. et l'éloignement des paladins adverses de la nôtre (défense) ; le reste, la recherche
s'en charge assez bien toute seule.
### Q6 — Stratégie selon la phase ### Q6 — Stratégie selon la phase
- **Début (placement)** : irréversible ; protéger la licorne, garantir de toujours - **Ouverture (placement)** : c'est irréversible, donc on sécurise d'emblée la licorne
pouvoir jouer (§5). et on s'arrange pour pouvoir toujours jouer (§5).
- **Milieu** : manœuvrer pour menacer la licorne adverse en contrôlant le liseré - **Milieu** : on manœuvre pour menacer la licorne adverse tout en gardant la main sur
imposé ; chercher le zugzwang partiel. le liseré qu'on impose, en visant le zugzwang partiel.
- **Fin** : dès qu'une capture est à portée, le calcul tactique prime. - **Finale** : dès qu'une capture est en vue, c'est le calcul tactique qui décide.
### Q7 — Majorant du nombre de coups et gestion du temps ### Q7 — Majorant du nombre de coups et gestion du temps
Aucune pièce ne disparaît avant la capture finale : borne raisonnable ~400600 Aucune pièce ne disparaît avant la prise finale, donc une partie peut traîner. En
demi-coups. Pour tenir les 300 s/joueur : approfondissement itératif, alpha-bêta, bornant le branchement par tour sur quelques dizaines de tours, on arrive à un ordre de
budget par coup dérivé du temps restant (§8). grandeur de 400 à 600 demi-coups. Pour rester dans les 300 s par joueur, on s'appuie sur
l'approfondissement itératif, l'élagage alpha-bêta et un budget par coup calculé à
partir du temps restant (§8).
--- ---
## 3. Modélisation : la classe `EscampeBoard` ## 3. Modélisation : la classe `EscampeBoard`
`EscampeBoard` implémente `Partie1` (`setFromFile`/`saveToFile`, `EscampeBoard` implémente l'interface fournie `Partie1` (`setFromFile`/`saveToFile`,
`isValidMove`, `possiblesMoves`, `play`, `gameOver`). Conventions de l'arbitre : `isValidMove`, `possiblesMoves`, `play`, `gameOver`) et suit les conventions de
coup `"B1-D1"`, placement `"C6/A6/B5/D5/E6/F5"` (licorne en tête), pass `"E"`. l'arbitre : coup régulier `"B1-D1"`, placement `"C6/A6/B5/D5/E6/F5"` avec la licorne
en tête puis les cinq paladins, et pass `"E"`.
**Format fichier** : 6 lignes de plateau (bas→haut), `N/n` `B/b` `-`, encadrées Le format de fichier reprend celui de l'énoncé : six lignes de plateau du bas vers le
d'un numéro ; lignes `%` = commentaires (où l'on stocke l'état hors-plateau pour haut, avec `N/n` pour le noir, `B/b` pour le blanc et `-` pour le vide, chaque ligne
un rechargement fidèle). encadrée de son numéro ; les lignes en `%` sont des commentaires. Nous y rangeons
justement l'état hors-plateau (liseré courant, joueur, bord de Noir), de sorte qu'une
sauvegarde se recharge à l'identique.
**Génération des coups** : DFS avec retour arrière (exactement N pas, intermédiaires Pour générer les coups, on part d'une case et on énumère les arrivées par un parcours
vides, dernière case vide ou licorne adverse). `possiblesMoves` filtre le bon en profondeur avec retour arrière : exactement N pas (N = liseré de départ), cases
liseré et renvoie `["E"]` si bloqué. Une méthode `main` illustre placements, intermédiaires vides, et case finale soit vide soit occupée par la licorne adverse,
liseré, pass, round-trip fichier, capture. auquel cas c'est une capture. `possiblesMoves` ne garde que les pièces sur le bon
liseré et renvoie `["E"]` quand plus rien n'est jouable. Une méthode `main` fait la
démonstration sur des exemples : placements, contrainte de liseré, pass, aller-retour
fichier et capture.
> Bug latent corrigé en partie 3 : un placement légal sur une seule ligne faisait > Un bug s'était glissé là et nous l'avons corrigé en partie 3 : un placement légal
> planter le calcul du bord de Noir (supposait deux lignes). Le bord est désormais > mais aligné sur une seule ligne faisait planter le calcul du bord de Noir, qui
> déduit de la ligne de la licorne. > supposait toujours deux lignes. Le bord se déduit maintenant de la ligne de la
> licorne.
--- ---
## 4. Intégration au tournoi : protocole de l'arbitre ## 4. Intégration au tournoi : protocole de l'arbitre
`JoueurPuyaubreauRussac implements IJoueur` enveloppe un `EscampeBoard` tenu à jour `JoueurPuyaubreauRussac implements IJoueur` garde à jour un `EscampeBoard` à chaque
à chaque coup (le nôtre via `play`, l'adverse via `mouvementEnnemi`). Trois coup, le nôtre via `play`, celui de l'adversaire reçu par `mouvementEnnemi`. Trois
adaptations, dont deux **vérifiées par analyse du jar obfusqué** : détails ont demandé une adaptation, et deux d'entre eux ont dû être confirmés en
regardant dans le jar de l'arbitre, qui est obfusqué :
- **Couleurs** : `IJoueur` en entiers (`NOIR=1`, `BLANC=-1`), `EscampeBoard` en - **Les couleurs** : `IJoueur` raisonne en entiers (`NOIR=1`, `BLANC=-1`) alors que
`"noir"`/`"blanc"`. `EscampeBoard` utilise les chaînes `"noir"` et `"blanc"`.
- **Pass = `"E"`, pas `"PASSE"`** : le Javadoc d'`IJoueur` dit `"PASSE"`, mais la - **Le pass se note `"E"`, pas `"PASSE"`** : le Javadoc d'`IJoueur` annonce `"PASSE"`,
classe serveur teste `move.equals("E")` (et `"PASSE"` est absent du jar). mais le serveur teste bel et bien `move.equals("E")`, et `"PASSE"` n'apparaît nulle
Envoyer `"PASSE"` = défaite sur coup illégal. part dans le jar. Suivre le Javadoc nous aurait coûté la partie sur coup illégal.
- **Carte des liserés** identique au serveur (cf. Q1). - **La carte des liserés** doit être celle du serveur (cf. Q1).
**Machine à états** : placement et coups passent par le même canal. Premier Placement et coups passent par le même canal : le premier `choixMouvement` renvoie un
`choixMouvement` = placement, suivants = coups ; phase détectée via placement, les suivants des coups, la phase se lisant sur `blackPlaced`/`whitePlaced`.
`blackPlaced`/`whitePlaced`. Séquence (déduite de `Solo`) : En lisant la classe `Solo` fournie, on reconstitue l'ordre des appels :
``` ```
Noir : choixMouvement(placement) -> mvtEnnemi(placement Blanc) Noir : choixMouvement(placement) -> mvtEnnemi(placement Blanc)
@@ -186,7 +209,11 @@ Blanc : mvtEnnemi(placement Noir) -> choixMouvement(placement)
-> choixMouvement(1er coup, Blanc rejoue) -> mvtEnnemi(coup Noir) -> ... -> choixMouvement(1er coup, Blanc rejoue) -> mvtEnnemi(coup Noir) -> ...
``` ```
**Exécution** (3 processus) : Comme on rejoue chaque coup sur l'`EscampeBoard` interne dans cet ordre, le joueur au
trait reste synchronisé avec l'arbitre sans traitement particulier.
Côté lancement, il faut trois processus, le serveur et deux clients :
``` ```
java -cp escampeobf.jar escampe.ServeurJeu 1234 1 java -cp escampeobf.jar escampe.ServeurJeu 1234 1
java -cp Puyaubreau_Russac.jar escampe.ClientJeu escampe.JoueurPuyaubreauRussac localhost 1234 java -cp Puyaubreau_Russac.jar escampe.ClientJeu escampe.JoueurPuyaubreauRussac localhost 1234
@@ -197,17 +224,22 @@ java -cp escampeobf.jar escampe.ClientJeu escampe.JoueurAleatoire
## 5. Placement d'ouverture ## 5. Placement d'ouverture
Constat issu de l'auto-jeu : une licorne mal placée peut se retrouver seule pièce Le placement ne se rejoue pas, donc autant le soigner. Le constat est venu de
jouable et bloquée sur le liseré imposé → passes successifs → perte d'initiative. l'auto-jeu : une licorne mal posée peut devenir la seule pièce jouable sur le liseré
Trois principes : imposé, et se retrouver bloquée, ce qui force des passes à répétition et abandonne
l'initiative. Trois principes répondent à ça :
1. **Licorne dans un coin** — un coin n'a que 2 voisines, donc 2 cases d'attaque. 1. **La licorne dans un coin.** Un coin n'a que deux voisines, donc seulement deux
2. **Murs** — on occupe ces 2 voisines par des paladins : licorne incapturable cases d'où l'adversaire peut venir la prendre.
tant que les murs tiennent. 2. **Deux murs.** On occupe ces deux voisines avec des paladins, et la licorne devient
3. **Couverture des liserés** — les 3 paladins restants sur des liserés 1, 2 et 3 imprenable tant que les murs tiennent, puisqu'on ne peut pas finir son dernier pas
distincts : jamais de pass forcé, jamais besoin de bouger un mur ou la licorne. sur une case occupée.
3. **Trois liserés couverts.** Les trois paladins restants se posent sur des cases de
liserés 1, 2 et 3 différents. Quel que soit le liseré imposé, il reste une pièce
mobile, et on n'a jamais à passer ni à déranger un mur ou la licorne.
Dispositions retenues (Blanc joue le bord complémentaire de Noir) : Voici les deux dispositions retenues (Blanc prend le bord opposé à Noir) ; nous en
avons vérifié la légalité et les trois propriétés ci-dessus :
``` ```
Bord bas A1/A2/B1/E1/F1/C2 Bord haut A6/A5/B6/C5/F5/E6 Bord bas A1/A2/B1/E1/F1/C2 Bord haut A6/A5/B6/C5/F5/E6
@@ -215,103 +247,127 @@ Bord bas A1/A2/B1/E1/F1/C2 Bord haut A6/A5/B6/C5/F5/E6
mobiles E1·F1·C2 = liserés 1·2·3 mobiles C5·F5·E6 = liserés 1·2·3 mobiles E1·F1·C2 = liserés 1·2·3 mobiles C5·F5·E6 = liserés 1·2·3
``` ```
Nous n'utilisons pas de bibliothèque d'ouvertures de coups. La raison principale est que la contrainte de liseré rend l'arbre d'ouverture très sensible au placement adverse : une séquence pré-calculée contre un placement différent du nôtre perdrait toute pertinence dès le deuxième coup. De plus, le branchement moyen (~8,9) est suffisamment faible pour que l'alpha-bêta atteigne la profondeur 1215 dès les premiers coups, rendant une bibliothèque peu utile. Le placement fixe ci-dessus suffit à garantir une position solide et reproductible dès le début. Nous n'avons pas de bibliothèque d'ouvertures. Elle aurait peu de valeur ici : la
contrainte de liseré rend toute séquence pré-calculée caduque dès que le placement
adverse diffère du cas prévu, souvent au deuxième coup. Et comme le branchement moyen
(~8,9) laisse l'alpha-bêta atteindre la profondeur 12 à 15 d'entrée, le placement fixe
ci-dessus suffit à démarrer sur une position saine et reproductible.
--- ---
## 6. Moteur de décision ## 6. Moteur de décision
Negamax + élagage alpha-bêta + approfondissement itératif (`Moteur`), sur une Le choix du coup repose sur un negamax avec élagage alpha-bêta et approfondissement
**copie** du plateau. Capture de licorne = nœud terminal `WIN - ply` (gagner vite). itératif (classe `Moteur`). La recherche travaille sur une copie du plateau, jamais sur
l'état réel. Une capture de licorne compte comme une feuille de valeur `WIN - ply`, ce
qui pousse à gagner tôt plutôt que tard.
**Astuces de performance :** Plusieurs choix tirent la vitesse vers le haut :
- **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é. - **Coups codés sur un entier** (case = `ligne*6+colonne`, coup = `départ*36+arrivée`),
> `VerifMoves` (§9) prouve qu'ils produisent les mêmes coups et états — optimiser pour ne manipuler aucune chaîne dans la boucle chaude.
> n'a pas changé les règles. - **DFS sur masque de bits** : les 36 cases tiennent dans un `long`, et les ensembles
« visité » et « atteignable » sont de simples masques, sans tableau alloué à chaque
appel.
- **`make`/`unmake` sans allocation** : un petit jeton suffit à défaire un coup, donc
on explore des millions de nœuds sans solliciter le ramasse-miettes.
- **Buffers de coups réservés** à l'avance, un par profondeur.
- **Ordre des coups** : on essaie d'abord toute prise de licorne (coupure immédiate),
et on remet en tête le meilleur coup de l'itération précédente.
**Performance mesurée** : ~45 M nœuds/s ; profondeur 1215 demi-coups en 6 s > Le moteur a sa propre génération de coups en entiers, en parallèle de celle, vérifiée,
(plus dans les positions étroites). Les gains forcés annoncés se concrétisent par > d'`EscampeBoard` en chaînes. Pour être sûr qu'elles ne divergent pas en silence, le
une capture. > test `VerifMoves` (§9) confronte les deux et exige les mêmes coups et les mêmes états :
> c'est ce qui nous garantit qu'optimiser n'a pas modifié les règles au passage.
En pratique, le moteur explore de l'ordre de **4 à 5 M nœuds/s**. En milieu de partie,
l'approfondissement itératif atteint 12 à 15 demi-coups en 6 s, davantage dans les
positions étroites. Quand il annonce un gain forcé, la capture a bien lieu dans les
parties de contrôle.
--- ---
## 7. Heuristique d'évaluation ## 7. Heuristique d'évaluation
Matériel constant → évaluation purement positionnelle, du point de vue du joueur Le matériel ne bouge pas (paladins imprenables, licornes en place jusqu'à la prise),
au trait, à partir des distances de Manhattan : donc évaluer une position non terminale revient à juger un placement. L'évaluation se
fait du point de vue du joueur au trait, à partir de distances de Manhattan, et combine
deux idées :
- **Attaque** : proximité de nos paladins à la licorne adverse — terme *somme* - la **pression d'attaque**, c'est-à-dire la proximité de nos paladins à la licorne
(pression globale) + terme *minimum* (l'attaquant le plus proche pèse plus) ; adverse, avec un terme de somme (pression d'ensemble) et un terme de minimum (le
- **Défense** : éloignement des paladins adverses de notre licorne — mêmes termes, paladin le plus proche pèse plus lourd) ;
signe opposé. - la **sécurité**, soit l'éloignement des paladins adverses de notre licorne, avec les
deux mêmes termes mais de signe opposé.
Avec les poids retenus (2 pour les sommes, 8 pour les minimums) :
Avec les poids retenus (somme = 2, minimum = 8) :
``` ```
eval = 2·Σ(10d_attaque) 2·Σ(10d_défense) eval = 2·Σ(10d_attaque) 2·Σ(10d_défense)
+ 8·(10min d_attaque) 8·(10min d_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 Pour régler ces poids, nous avons fait jouer le moteur contre lui-même et contre le
aléatoire) : (a) somme seule → jeu trop diffus ; (b) **somme + minimum (retenue)** joueur aléatoire fourni, en comparant trois variantes. Avec la somme seule, le jeu
→ le terme minimum fortement pondéré oriente les paladins vers la licorne adverse restait trop diffus et le moteur tardait à concentrer une menace. La somme plus le
et améliore le taux de capture ; (c) terme défensif symétrique conservé (évite minimum, que nous avons gardée, recentre les paladins vers la licorne adverse grâce au
d'exposer notre licorne). Le fort poids du minimum reflète que c'est l'attaquant fort poids du minimum et fait monter le taux de capture. L'ajout d'un terme défensif
le plus avancé qui décide d'une prise. symétrique a été conservé aussi : il évite d'exposer notre licorne sans pénaliser
l'attaque. Ce poids élevé sur le minimum traduit une réalité du jeu, où c'est le paladin
le plus avancé qui conclut une prise.
> Limite assumée : poids validés contre l'aléatoire et en auto-jeu, faute > Une limite que nous assumons : faute d'autres IA disponibles avant le tournoi, ces
> d'adversaires IA tiers. Les tactiques à court terme sont gérées par la recherche, > poids sont calés contre l'aléatoire et en auto-jeu, pas contre des joueurs forts. Cela
> ce qui rend le joueur robuste malgré une évaluation simple. > dit, les prises à courte échéance relèvent de la recherche, ce qui rend le joueur
> solide même avec une évaluation aussi simple.
--- ---
## 8. Gestion du temps réel ## 8. Gestion du temps réel
Limite arbitre 300 s/joueur/partie → **enveloppe interne 280 s** (~20 s de marge). L'arbitre laisse 300 s par joueur et par partie. Nous travaillons sous une enveloppe
Budget par coup : interne de **280 s**, soit une vingtaine de secondes de marge. Le budget d'un coup est
une fraction du temps restant, bornée des deux côtés :
``` ```
tranche = clamp( temps_restant / 12 , 120 ms , 6000 ms ) 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), Diviser le temps restant le fait décroître géométriquement, si bien que le budget ne
cumul max ≈ 36 s/joueur sur une partie complète — très loin des 300 s. Réglage peut pas s'épuiser, même sur une partie qui s'éternise. Le plafond de 6 s évite de
conservateur, augmentable sans risque. gaspiller du temps en ouverture, le plancher de 120 ms garantit un minimum de réflexion,
et un mode « panique » couvre les toutes dernières secondes. Comme la recherche est
itérative, le meilleur coup déjà trouvé est disponible dès que la tranche expire, le
temps étant relu toutes les 2048 explorations de nœuds.
En mesure (auto-jeu équilibré, plein budget), le coup le plus long approche le plafond,
environ 6 s, et le cumul sur une partie entière plafonne vers 36 s par joueur, loin des
300 s. Le réglage est prudent et on pourrait l'ouvrir davantage sans risque.
--- ---
## 9. Performances et tests ## 9. Performances et tests
Chaque maillon de la chaîne est contrôlé contre une référence indépendante.
| Test | Garantit | Résultat | | 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** | | `VerifMoves` | génération en entiers (moteur) identique à la génération en chaînes (vérifiée), 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** | | `RulesTest` | règles vérifiées directement (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) | | 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 | | Démo IA vs IA (serveur réel) | partie complète moteur contre 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 | | `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` Les rôles ne se recouvrent pas : `VerifMoves` montre que le moteur colle à
(`EscampeBoard` ≡ règles), parties arbitrées (dialogue correct avec l'arbitre `EscampeBoard`, `RulesTest` que `EscampeBoard` respecte les règles, et les parties
réel). Aucun coup illégal sur l'ensemble des parties jouées. arbitrées que l'ensemble dialogue correctement avec le vrai arbitre. Sur toutes les
parties jouées, aucun coup illégal n'a été produit.
--- ---
## 10. Compilation, exécution et livrables ## 10. Compilation, exécution et livrables
`build.sh` produit dans `dist/` les trois livrables de la version finale : `build.sh` fabrique dans `dist/` les trois livrables de la version finale :
``` ```
Puyaubreau_Russac.jar jar exécutable (Main-Class : escampe.ClientJeu) Puyaubreau_Russac.jar jar exécutable (Main-Class : escampe.ClientJeu)
@@ -321,44 +377,48 @@ mainClass jar:Puyaubreau_Russac.jar
Puyaubreau_Russac.tgz archive : Puyaubreau_Russac/ { src/escampe/*.java, mainClass, jar } Puyaubreau_Russac.tgz archive : Puyaubreau_Russac/ { src/escampe/*.java, mainClass, jar }
``` ```
Seules les classes de production entrent dans le jar ; les utilitaires de test Le jar ne contient que les classes de production ; les utilitaires de test
(`VerifMoves`, `RulesTest`, `Bench`, `Branching`) en sont exclus. Le multijoueur (`VerifMoves`, `RulesTest`, `Bench`, `Branching`) restent dehors. Le jeu en
(humain vs humain, humain vs IA, local ou distant) est documenté dans multijoueur (humain contre humain, humain contre notre IA, en local comme à distance)
`MULTIJOUEUR.md`. est décrit dans `MULTIJOUEUR.md`.
--- ---
## 11. Sources et bibliographie ## 11. Sources et bibliographie
- **Énoncé du cours** (Université Paris-Saclay, Polytech APP5, 2025-2026) : règles, - L'**énoncé du cours** (Université Paris-Saclay, Polytech APP5, 2025-2026) pour les
carte des liserés (figure 4), interface `Partie1`, classes fournies (`IJoueur`, règles, la carte des liserés (figure 4), l'interface `Partie1` et les classes fournies
`ClientJeu`, `Solo`, `Applet`, serveur). (`IJoueur`, `ClientJeu`, `Solo`, `Applet`, serveur).
- **Algorithmes classiques**, pour inspiration sans copie de code : alpha-bêta - Des **algorithmes classiques**, comme inspiration et sans copie de code : l'élagage
(Knuth & Moore, 1975) ; minimax/negamax/approfondissement itératif (Russell & alpha-bêta (Knuth et Moore, 1975), le minimax, le negamax et l'approfondissement
Norvig, *AIMA*) ; masques de bits et ordonnancement de coups (*Chess Programming itératif (Russell et Norvig, *AIMA*), ainsi que les masques de bits et l'ordonnancement
Wiki*). de coups (*Chess Programming Wiki*).
- **Déclaration** : aucun programme d'Escampe externe recopié. La seule - Pour être clairs : nous n'avons recopié aucun programme d'Escampe existant. La seule
rétro-ingénierie porte sur le jar d'arbitre *fourni avec le sujet*, pour confirmer rétro-ingénierie a porté sur le jar d'arbitre fourni avec le sujet, et uniquement pour
le protocole (pass `"E"`) et la carte des liserés (documentation ambiguë). confirmer le protocole (le pass `"E"`) et la carte des liserés, deux points que la
documentation laissait dans le flou.
--- ---
## 12. Conclusion et difficultés rencontrées ## 12. Conclusion et difficultés rencontrées
Le joueur conduit une partie de façon autonome, dialogue correctement avec Le joueur mène une partie tout seul, dialogue correctement avec l'arbitre, ne joue
l'arbitre, ne produit jamais de coup illégal et respecte très confortablement la jamais de coup illégal et tient le temps très largement. Les principaux obstacles ont
contrainte de temps. Difficultés principales : été les suivants :
- **Obfuscation du serveur** : lever l'ambiguïté du pass (`"E"` vs `"PASSE"`) et - **L'obfuscation du serveur.** Trancher l'ambiguïté du pass (`"E"` et non `"PASSE"`)
confirmer la carte des liserés a nécessité l'analyse du jar — décisif pour ne pas et confirmer la carte des liserés a demandé de fouiller le jar, sans quoi on perdait
perdre sur coup illégal. sur coup illégal.
- **Interface obfusquée vs nos sources** : le joueur aléatoire du jar n'implémente - **L'interface obfusquée face à nos sources.** Le joueur aléatoire du jar n'implémente
pas notre `IJoueur` ; les tests contre lui passent par le réseau. pas notre `IJoueur`, donc les tests contre lui passent par le réseau, où seules des
- **Avantage du trait** : en miroir, Blanc garde l'initiative via la contrainte de chaînes circulent.
liseré — propriété du jeu. - **L'avantage du trait.** En miroir, Blanc, qui joue le premier, garde l'initiative via
- **Réglage de l'heuristique sans adversaires** : validé contre l'aléatoire et en la contrainte de liseré ; c'est une propriété du jeu, pas une question de force du
auto-jeu. moteur.
- **Le réglage de l'heuristique sans sparring-partner**, calé faute de mieux contre
l'aléatoire et en auto-jeu.
**Pistes d'amélioration** : table de transposition (Zobrist), bibliothèque Si nous devions continuer, plusieurs pistes se présentent : une table de transposition
d'ouvertures de placement, terme de mobilité différentielle, recherche de (hachage de Zobrist), une bibliothèque d'ouvertures de placement, un terme de mobilité
quiescence sur les menaces de capture. différentielle dans l'évaluation et une recherche de quiescence sur les menaces de
capture.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -23,56 +23,75 @@
</div> </div>
</div> </div>
<!-- ====================== SOMMAIRE ====================== -->
<h2 class="toc-title">Sommaire</h2>
<ol class="toc">
<li>Présentation et règles</li>
<li>Analyse des caractéristiques du jeu (Q1 à Q7)</li>
<li>Modélisation : la classe <code>EscampeBoard</code></li>
<li>Intégration au tournoi : le protocole de l'arbitre</li>
<li>Placement d'ouverture</li>
<li>Moteur de décision</li>
<li>Heuristique d'évaluation</li>
<li>Gestion du temps réel</li>
<li>Performances et tests</li>
<li>Compilation, exécution et livrables</li>
<li>Sources et bibliographie</li>
<li>Conclusion et difficultés rencontrées</li>
</ol>
<!-- ====================== 1. INTRODUCTION ====================== --> <!-- ====================== 1. INTRODUCTION ====================== -->
<h2>1. Présentation et règles</h2> <h2>1. Présentation et règles</h2>
<p>Escampe se joue sur un plateau de 36&nbsp;cases (6×6). Chaque case porte un <p>Escampe se joue sur un plateau de 36&nbsp;cases (6×6). Chaque case porte un
liseré <em>simple</em>, <em>double</em> ou <em>triple</em>. Chaque joueur dispose liseré <em>simple</em>, <em>double</em> ou <em>triple</em>. Chaque joueur dispose
d'une <strong>licorne</strong> et de cinq <strong>paladins</strong> (couleur noire d'une <strong>licorne</strong> et de cinq <strong>paladins</strong> (noirs ou
ou blanche). Les lignes sont numérotées de 1 à 6, les colonnes de A à F. Le but blancs). Les lignes vont de 1 à 6, les colonnes de A à F, et le but est de
est de <strong>prendre la licorne adverse</strong>.</p> <strong>prendre la licorne adverse</strong>.</p>
<p>La règle caractéristique du jeu est une <strong>contrainte de liseré</strong> : <p>Ce qui fait l'originalité du jeu, c'est la <strong>contrainte de liseré</strong> :
la pièce que l'on joue doit partir d'une case dont le liseré est <em>identique</em> la pièce que l'on joue doit partir d'une case dont le liseré est le même que celui
à celui de la case d'arrivée du coup adverse précédent. Le liseré de la case de de la case où l'adversaire vient de poser sa pièce. Ce liseré de départ fixe aussi
départ fixe en outre le nombre de pas (1, 2 ou 3), orthogonaux, sans traverser ni le nombre de pas (1, 2 ou 3), orthogonaux, sans traverser ni repasser sur une case
revisiter de case. On ne capture qu'en se posant, au dernier pas, sur la licorne déjà visitée. On ne capture qu'en s'arrêtant, au dernier pas, sur la licorne
adverse les paladins sont imprenables. Si un joueur ne peut rien jouer, il passe adverse&nbsp;; les paladins, eux, sont imprenables. Un joueur qui ne peut rien jouer
son tour.</p> passe son tour. Toute la difficulté revient donc à coincer l'adversaire en lui
imposant des liserés qui le bloquent.</p>
<p>Le déroulement : Noir place ses six pièces sur les deux lignes d'un bord <p>Pour le déroulement, Noir place d'abord ses six pièces sur les deux lignes d'un
(haut ou bas) ; Blanc fait de même sur le bord opposé ; <strong>Blanc joue le bord de son choix (haut ou bas), puis Blanc fait de même sur le bord opposé, et
premier coup</strong>. Ce rapport décrit nos choix de modélisation (parties&nbsp;1 c'est <strong>Blanc qui joue le premier coup</strong>. Le rapport reprend nos choix
et&nbsp;2) puis la conception du joueur artificiel pour le tournoi (partie&nbsp;3), de modélisation (parties&nbsp;1 et&nbsp;2) puis détaille la conception du joueur pour
avec les mesures qui justifient nos choix.</p> le tournoi (partie&nbsp;3), chiffres à l'appui.</p>
<!-- ====================== 2. ANALYSE Q1-Q7 ====================== --> <!-- ====================== 2. ANALYSE Q1-Q7 ====================== -->
<h2>2. Analyse des caractéristiques du jeu</h2> <h2>2. Analyse des caractéristiques du jeu</h2>
<p>Nous reprenons ici les sept questions de la première partie, en les étayant <p>Nous reprenons les sept questions de la première partie, cette fois à la lumière
par l'implémentation finalement réalisée.</p> du code que nous avons réellement écrit.</p>
<h3>Q1 — Modélisation d'un état</h3> <h3>Q1 — Modélisation d'un état</h3>
<p>Le plateau est un tableau <code>int[6][6]</code> : <code>board[ligne][colonne]</code> <p>Le plateau est un tableau <code>int[6][6]</code> : <code>board[ligne][colonne]</code>,
avec <code>ligne&nbsp;0</code> = ligne&nbsp;1 (bas) et <code>colonne&nbsp;0</code> = A. avec <code>ligne&nbsp;0</code> = ligne&nbsp;1 (en bas) et <code>colonne&nbsp;0</code> = A.
Chaque case contient une constante de pièce (<code>EMPTY</code>, Chaque case vaut une constante de pièce (<code>EMPTY</code>,
<code>WHITE_LICORNE</code>, <code>WHITE_PALADIN</code>, <code>BLACK_LICORNE</code>, <code>WHITE_LICORNE</code>, <code>WHITE_PALADIN</code>, <code>BLACK_LICORNE</code>,
<code>BLACK_PALADIN</code>). L'état complémentaire, indispensable à la règle, est <code>BLACK_PALADIN</code>). Quatre informations que le tableau ne porte pas, mais
maintenu hors du plateau :</p> dont la règle a besoin, sont gardées à côté :</p>
<ul> <ul>
<li><code>lastTileType</code> : liseré imposé au coup suivant (<code>-1</code> = aucune contrainte) ;</li> <li><code>lastTileType</code> : le liseré imposé au coup suivant (<code>-1</code> quand il n'y a pas de contrainte) ;</li>
<li><code>currentPlayer</code> : joueur au trait ;</li> <li><code>currentPlayer</code> : le joueur au trait ;</li>
<li><code>blackPlaced</code>, <code>whitePlaced</code> : fin des phases de placement ;</li> <li><code>blackPlaced</code>, <code>whitePlaced</code> : la fin des phases de placement ;</li>
<li><code>blackRows</code> : le bord choisi par Noir (en déduit celui de Blanc).</li> <li><code>blackRows</code> : le bord choisi par Noir, qui détermine celui de Blanc.</li>
</ul> </ul>
<p><strong>Avantages.</strong> Accès O(1) à toute case ; copie immédiate de l'état <p>Le tableau d'entiers donne un accès en O(1) à n'importe quelle case et se copie
pour l'arbre de recherche ; sérialisation triviale ; surtout, un schéma sans effort, ce qui compte pour l'arbre de recherche ; il se sérialise aussi
<code>make/unmake</code> sans aucune allocation (essentiel pour la vitesse, §6). directement vers le format de fichier. Surtout, il autorise un schéma
<strong>Inconvénient.</strong> La contrainte de liseré est un état séparé qu'il <code>make</code>/<code>unmake</code> qui n'alloue rien (voir §6). Le seul point
faut maintenir explicitement à chaque coup ; nous l'encapsulons dans <code>play</code>.</p> gênant est que la contrainte de liseré vit en dehors du tableau : il faut penser à
la mettre à jour à chaque coup, ce que nous centralisons dans <code>play</code>.</p>
<p>La carte des liserés est une constante <code>TILE_MAP</code> reproduisant la <p>La carte des liserés est figée dans la constante <code>TILE_MAP</code>, recopie
figure&nbsp;4 de l'énoncé (ligne&nbsp;1 en bas) :</p> de la figure&nbsp;4 de l'énoncé (ligne&nbsp;1 en bas) :</p>
<pre class="grid"> A B C D E F <pre class="grid"> A B C D E F
6 3 2 2 1 3 2 6 3 2 2 1 3 2
5 1 3 1 3 1 2 5 1 3 1 3 1 2
@@ -80,143 +99,143 @@ figure&nbsp;4 de l'énoncé (ligne&nbsp;1 en bas) :</p>
3 2 3 1 2 1 3 3 2 3 1 2 1 3
2 3 1 3 1 3 2 2 3 1 3 1 3 2
1 1 2 2 3 1 2</pre> 1 1 2 2 3 1 2</pre>
<p class="note">Fait vérifié : cette carte est <em>identique</em>, case pour case, <p class="note">Nous avons extrait par réflexion la carte qu'utilise l'arbitre dans
à celle utilisée en interne par l'arbitre du tournoi — nous l'avons extraite par sa propre classe de jeu, et elle coïncide case pour case avec la nôtre (elle colle
réflexion de la classe de jeu du serveur fourni. Elle est aussi cohérente avec aussi à l'exemple de la figure&nbsp;6). La vérification valait le coup : une carte
l'exemple tactique de la figure&nbsp;6 de l'énoncé. Une carte divergente aurait fausse aurait fait rejeter nos coups par l'arbitre.</p>
produit des coups jugés illégaux : ce point était critique.</p>
<h3>Q2 — Détection de fin de partie</h3> <h3>Q2 — Détection de fin de partie</h3>
<p>La partie est finie dès qu'une des deux licornes a disparu du plateau (seul cas <p>La partie s'arrête dès qu'une des deux licornes quitte le plateau ; il n'y a pas
de fin, pas de match nul). La vérification est un simple balayage O(1) du plateau d'autre cas, donc pas de nul. Le test (<code>gameOver</code>) est un simple balayage
(<code>gameOver</code>) ; le moteur, lui, détecte la capture directement au moment en O(1). En recherche, le moteur n'attend même pas ce balayage : il repère la
elle est jouée (§6).</p> capture à l'instant où le coup la produit (§6).</p>
<h3>Q3 — Sources de difficulté et facteur de branchement</h3> <h3>Q3 — Sources de difficulté et facteur de branchement</h3>
<p>Les principales sources de difficulté sont :</p> <p>Quatre choses rendent le jeu retors : la contrainte de liseré, qui fait varier
<ul> fortement la mobilité ; la dépendance entre tours, puisque la case d'arrivée qu'on
<li>la <strong>contrainte de liseré</strong>, qui limite fortement et variablement la mobilité ;</li> choisit dicte les pièces que l'adversaire pourra bouger ; l'asymétrie du plateau,
<li>la <strong>dépendance entre tours</strong> : la case d'arrivée choisie détermine les pièces que l'adversaire pourra jouer ;</li> avec des zones riches en liserés triples (mobiles) et d'autres en liserés simples ;
<li>l'<strong>asymétrie</strong> du plateau (zones riches en liserés triples, donc mobiles, vs zones simples) ;</li> et le risque qu'une pièce, voire un joueur entier, se retrouve bloqué et doive
<li>le risque de <strong>blocage</strong> d'une pièce, voire d'un joueur (pass forcé).</li> passer.</p>
</ul> <p>Côté <strong>facteur de branchement</strong>, nous avions avancé en première
<p><strong>Facteur de branchement.</strong> En première partie nous avions estimé partie une borne théorique de l'ordre de 120 (six pièces, jusqu'à une vingtaine de
une borne théorique de l'ordre de 120 (6&nbsp;pièces × jusqu'à ~20 destinations destinations sur un liseré triple). En pratique c'est beaucoup moins, parce que la
sur liseré triple). La mesure réelle est bien plus basse, car la contrainte de contrainte de liseré ne laisse jouables que les pièces du bon type. Une simulation
liseré ne laisse jouables que les pièces du bon liseré. Sur 30&nbsp;000 parties de 30&nbsp;000 parties aléatoires (utilitaire <code>escampe.Branching</code>) donne :</p>
aléatoires simulées (utilitaire <code>escampe.Branching</code>) :</p>
<table> <table>
<tr><th>Situation</th><th>Branchement maximal observé</th></tr> <tr><th>Situation</th><th>Branchement maximal observé</th></tr>
<tr><td>Coup contraint (un liseré imposé)</td><td>45</td></tr> <tr><td>Coup contraint (un liseré imposé)</td><td>45</td></tr>
<tr><td>Coup libre (1<sup>er</sup> coup ou après un pass, aucune contrainte)</td><td>49</td></tr> <tr><td>Coup libre (1<sup>er</sup> coup ou après un pass)</td><td>49</td></tr>
<tr><td>Branchement moyen (toutes positions)</td><td>≈ 8,9</td></tr> <tr><td>Branchement moyen (toutes positions)</td><td>≈ 8,9</td></tr>
</table> </table>
<p>Le branchement effectif modeste (moyenne &lt;&nbsp;10) explique qu'une recherche <p>Avec une moyenne sous 10, l'alpha-bêta descend profond en quelques secondes (§6).</p>
alpha-bêta atteigne des profondeurs élevées en quelques secondes (§6).</p>
<h3>Q4 — Coups imparables</h3> <h3>Q4 — Coups imparables</h3>
<p>Il n'existe pas de coup « imparable » universel garanti dès le départ : la <p>Il n'y a pas de coup gagnant à coup sûr dès le départ : comme l'adversaire choisit
contrainte de liseré peut toujours empêcher l'exécution d'une menace au mauvais sa case d'arrivée, donc le liseré qu'il nous impose, il peut toujours désamorcer une
moment. En revanche, certaines configurations créent un <strong>zugzwang menace au mauvais moment. Ce qui existe, en revanche, ce sont des positions de
partiel</strong>l'adversaire ne peut éviter d'imposer le liseré qui nous <strong>zugzwang partiel</strong>,il est forcé d'imposer précisément le liseré
arrange — l'énoncé en donne l'exemple (figure&nbsp;6 : le paladin blanc en C2 prend qui ouvre la capture.</p>
la licorne en C1 dès que Noir est forcé d'imposer un liseré double). Construire de <p>L'énoncé en donne un cas net (figure&nbsp;6). Noir vient de jouer en D4, une case
tels pièges est un axe stratégique ; notre recherche les exploite implicitement à liseré double, donc Blanc doit partir d'une case double : il choisit
quand ils sont à portée d'horizon.</p> <strong>F6&nbsp;&nbsp;E5</strong> (F6 est double). Noir est alors contraint de jouer
depuis un liseré simple comme E5, et son seul coup raisonnable est
<strong>A1&nbsp;&nbsp;A2</strong>. Or A2 est à liseré triple : Blanc enchaîne
<strong>C2&nbsp;×&nbsp;C1</strong>, son paladin en C2 parcourant les trois pas
C2&nbsp;&nbsp;D2&nbsp;&nbsp;D1&nbsp;&nbsp;C1 pour prendre la licorne noire. La
séquence est imparable localement : une fois Noir poussé en A2, il ne peut plus
empêcher la prise.</p>
<p>Ce genre de combinaison ne se construit pas mécaniquement depuis l'ouverture, il y
a trop de degrés de liberté&nbsp;; mais notre alpha-bêta la trouve et la joue dès
qu'elle entre dans son horizon de recherche.</p>
<h3>Q5 — Critères pour l'heuristique</h3> <h3>Q5 — Critères pour l'heuristique</h3>
<p>Nous avions identifié cinq critères : distance à la licorne adverse, mobilité <p>Cinq critères nous semblaient pertinents : la distance à la licorne adverse, la
différentielle, contrôle du liseré imposé, protection de sa propre licorne, et mobilité différentielle, le contrôle du liseré qu'on impose, la protection de sa
avancée sur le plateau. L'heuristique finalement retenue (§7) s'appuie sur la propre licorne et l'avancée des pièces. Au final (§7), l'évaluation retenue tient
<strong>proximité des paladins à la licorne adverse</strong> (pression d'attaque) surtout à deux d'entre eux, la proximité de nos paladins à la licorne adverse
et l'<strong>éloignement des paladins adverses de notre licorne</strong> (attaque) et l'éloignement des paladins adverses de la nôtre (défense)&nbsp;; le
(sécurité) — les autres critères sont, en pratique, largement pris en charge par reste, la recherche s'en charge assez bien toute seule.</p>
la recherche elle-même.</p>
<h3>Q6 — Stratégie selon la phase</h3> <h3>Q6 — Stratégie selon la phase</h3>
<ul> <ul>
<li><strong>Début (placement)</strong> : irréversible et déterminant. On protège <li><strong>Ouverture (placement)</strong> : c'est irréversible, donc on sécurise
la licorne et on garantit de toujours pouvoir jouer (§5).</li> d'emblée la licorne et on s'arrange pour pouvoir toujours jouer (§5).</li>
<li><strong>Milieu</strong> : manœuvre pour construire des menaces sur la licorne <li><strong>Milieu</strong> : on manœuvre pour menacer la licorne adverse tout en
adverse tout en contrôlant le liseré imposé ; recherche de zugzwang partiel.</li> gardant la main sur le liseré qu'on impose, en visant le zugzwang partiel.</li>
<li><strong>Fin</strong> : dès qu'une capture est à portée, le calcul tactique <li><strong>Finale</strong> : dès qu'une capture est en vue, c'est le calcul
(recherche profonde) prime.</li> tactique qui décide.</li>
</ul> </ul>
<h3>Q7 — Majorant du nombre de coups et gestion du temps</h3> <h3>Q7 — Majorant du nombre de coups et gestion du temps</h3>
<p>Aucune pièce ne disparaît avant la capture finale ; une partie peut donc <p>Aucune pièce ne disparaît avant la prise finale, donc une partie peut traîner.
théoriquement s'étirer. En bornant le branchement par tour et en comptant quelques En bornant le branchement par tour sur quelques dizaines de tours, on arrive à un
dizaines de tours, une borne raisonnable se situe vers 400600 demi-coups. Pour ordre de grandeur de 400 à 600 demi-coups. Pour rester dans les 300&nbsp;s par
tenir la contrainte de temps (300&nbsp;s par joueur et par partie), nous combinons joueur, on s'appuie sur l'approfondissement itératif, l'élagage alpha-bêta et un
<strong>approfondissement itératif</strong>, <strong>élagage alpha-bêta</strong> et budget par coup calculé à partir du temps restant (§8).</p>
un <strong>budget par coup</strong> dérivé du temps restant (§8).</p>
<!-- ====================== 3. MODELISATION (PARTIE 2) ====================== --> <!-- ====================== 3. MODELISATION (PARTIE 2) ====================== -->
<h2>3. Modélisation : la classe <code>EscampeBoard</code></h2> <h2>3. Modélisation : la classe <code>EscampeBoard</code></h2>
<p><code>EscampeBoard</code> (≈ 860 lignes) implémente l'interface fournie <p><code>EscampeBoard</code> implémente l'interface fournie <code>Partie1</code>
<code>Partie1</code> : <code>setFromFile</code> / <code>saveToFile</code>, (<code>setFromFile</code>/<code>saveToFile</code>, <code>isValidMove</code>,
<code>isValidMove</code>, <code>possiblesMoves</code>, <code>play</code>, <code>possiblesMoves</code>, <code>play</code>, <code>gameOver</code>) et suit les
<code>gameOver</code>. Les conventions de l'arbitre sont respectées à la lettre :</p> conventions de l'arbitre : coup régulier <code>"B1-D1"</code>, placement
<ul> <code>"C6/A6/B5/D5/E6/F5"</code> avec la licorne en tête puis les cinq paladins, et
<li>coup régulier <code>"B1-D1"</code> ;</li> pass <code>"E"</code>.</p>
<li>placement <code>"C6/A6/B5/D5/E6/F5"</code> (licorne en tête, puis les 5 paladins) ;</li>
<li>pass <code>"E"</code>.</li>
</ul>
<p><strong>Format de fichier.</strong> Six lignes de plateau (bas vers haut), <p>Le format de fichier reprend celui de l'énoncé : six lignes de plateau du bas vers
caractères <code>N/n</code> (licorne/paladin noir), <code>B/b</code> (blanc), le haut, avec <code>N/n</code> pour le noir, <code>B/b</code> pour le blanc et
<code>-</code> (vide), chaque ligne encadrée d'un numéro ; toute autre ligne <code>-</code> pour le vide, chaque ligne encadrée de son numéro&nbsp;; les lignes en
commence par <code>%</code> (commentaire). Nous y ajoutons en commentaires l'état <code>%</code> sont des commentaires. Nous y rangeons justement l'état hors-plateau
hors-plateau (liseré courant, joueur, bord de Noir) afin que la sauvegarde soit (liseré courant, joueur, bord de Noir), de sorte qu'une sauvegarde se recharge à
fidèlement rechargeable.</p> l'identique.</p>
<p><strong>Génération des coups.</strong> Depuis une case, on énumère les <p>Pour générer les coups, on part d'une case et on énumère les arrivées par un
destinations par un parcours en profondeur (DFS) avec retour arrière : exactement parcours en profondeur avec retour arrière : exactement N&nbsp;pas (N = liseré de
N&nbsp;pas (N = liseré de départ), cases intermédiaires vides, dernière case vide ou départ), cases intermédiaires vides, et case finale soit vide soit occupée par la
occupée par la licorne adverse (capture). <code>possiblesMoves</code> filtre les licorne adverse, auquel cas c'est une capture. <code>possiblesMoves</code> ne garde
pièces sur le bon liseré et renvoie <code>["E"]</code> si aucun coup n'est possible. que les pièces sur le bon liseré et renvoie <code>["E"]</code> quand plus rien n'est
Une méthode <code>main</code> illustre placements, contrainte de liseré, pass, jouable. Une méthode <code>main</code> fait la démonstration sur des exemples :
round-trip fichier et capture.</p> placements, contrainte de liseré, pass, aller-retour fichier et capture.</p>
<p class="note">Bug latent corrigé en partie&nbsp;3 : un placement légal mais <p class="note">Un bug s'était glissé là et nous l'avons corrigé en partie&nbsp;3 :
disposé sur une <em>seule</em> ligne faisait planter le calcul du bord de Noir un placement légal mais aligné sur une seule ligne faisait planter le calcul du bord
(il supposait deux lignes distinctes). Le bord est désormais déduit de façon de Noir, qui supposait toujours deux lignes. Le bord se déduit maintenant de la ligne
robuste à partir de la ligne de la licorne.</p> de la licorne.</p>
<!-- ====================== 4. PROTOCOLE ====================== --> <!-- ====================== 4. PROTOCOLE ====================== -->
<h2>4. Intégration au tournoi : le protocole de l'arbitre</h2> <h2>4. Intégration au tournoi : le protocole de l'arbitre</h2>
<p>Le joueur <code>escampe.JoueurPuyaubreauRussac</code> implémente l'interface <p>Le joueur <code>escampe.JoueurPuyaubreauRussac</code> implémente l'interface
fournie <code>IJoueur</code> et enveloppe un <code>EscampeBoard</code> tenu à jour fournie <code>IJoueur</code> et garde à jour un <code>EscampeBoard</code> à chaque
à chaque coup (le nôtre comme celui de l'adversaire, via <code>mouvementEnnemi</code>). coup, le nôtre comme celui de l'adversaire reçu par <code>mouvementEnnemi</code>.
Trois points d'adaptation, dont deux <strong>vérifiés par analyse du jar de Trois détails ont demandé une adaptation, et deux d'entre eux ont dû être
l'arbitre</strong> car l'infrastructure fournie est obfusquée :</p> <strong>confirmés en regardant dans le jar de l'arbitre</strong>, qui est obfusqué :</p>
<ul> <ul>
<li><strong>Couleurs.</strong> <code>IJoueur</code> parle en entiers <li><strong>Les couleurs</strong> : <code>IJoueur</code> raisonne en entiers
(<code>NOIR&nbsp;=&nbsp;1</code>, <code>BLANC&nbsp;=&nbsp;-1</code>) ; (<code>NOIR&nbsp;=&nbsp;1</code>, <code>BLANC&nbsp;=&nbsp;-1</code>) alors que
<code>EscampeBoard</code> en chaînes <code>"noir"</code>/<code>"blanc"</code>.</li> <code>EscampeBoard</code> utilise les chaînes <code>"noir"</code> et <code>"blanc"</code>.</li>
<li><strong>Pass = <code>"E"</code>, et non <code>"PASSE"</code>.</strong> Le <li><strong>Le pass se note <code>"E"</code>, pas <code>"PASSE"</code></strong> :
Javadoc d'<code>IJoueur</code> indique <code>"PASSE"</code>, mais la classe de le Javadoc d'<code>IJoueur</code> annonce <code>"PASSE"</code>, mais le serveur teste
jeu du serveur teste <code>move.equals("E")</code> (et <code>"PASSE"</code> bel et bien <code>move.equals("E")</code>, et <code>"PASSE"</code> n'apparaît nulle
n'apparaît nulle part dans le jar). Envoyer <code>"PASSE"</code> aurait valu une part dans le jar. Suivre le Javadoc nous aurait coûté la partie sur coup illégal.</li>
défaite sur coup illégal.</li> <li><strong>La carte des liserés</strong> doit être celle du serveur (cf. Q1).</li>
<li><strong>Carte des liserés</strong> identique à celle du serveur (cf. Q1).</li>
</ul> </ul>
<p><strong>Machine à états.</strong> Le placement et les coups transitent par le <p>Placement et coups passent par le même canal
même canal <code>choixMouvement</code>/<code>mouvementEnnemi</code>. Le premier (<code>choixMouvement</code>/<code>mouvementEnnemi</code>) : le premier
appel à <code>choixMouvement</code> renvoie donc un <em>placement</em>, les suivants <code>choixMouvement</code> renvoie un placement, les suivants des coups, la phase
des coups ; la phase est détectée via <code>blackPlaced</code>/<code>whitePlaced</code>. se lisant sur <code>blackPlaced</code>/<code>whitePlaced</code>. En lisant la classe
La séquence (déduite de la classe <code>Solo</code> fournie) est :</p> <code>Solo</code> fournie, on reconstitue l'ordre des appels :</p>
<pre class="grid">Noir : choixMouvement(placement) → mvtEnnemi(placement Blanc) <pre class="grid">Noir : choixMouvement(placement) → mvtEnnemi(placement Blanc)
→ mvtEnnemi(1er coup Blanc) → choixMouvement(coup) → ... → mvtEnnemi(1er coup Blanc) → choixMouvement(coup) → ...
Blanc : mvtEnnemi(placement Noir) → choixMouvement(placement) Blanc : mvtEnnemi(placement Noir) → choixMouvement(placement)
→ choixMouvement(1er coup, Blanc rejoue) → mvtEnnemi(coup Noir) → ...</pre> → choixMouvement(1er coup, Blanc rejoue) → mvtEnnemi(coup Noir) → ...</pre>
<p>En appliquant chaque coup à l'<code>EscampeBoard</code> interne dans cet ordre, <p>Comme on rejoue chaque coup sur l'<code>EscampeBoard</code> interne dans cet ordre,
le joueur au trait reste naturellement synchronisé avec l'arbitre.</p> le joueur au trait reste synchronisé avec l'arbitre sans traitement particulier.</p>
<p><strong>Exécution.</strong> Trois processus (serveur + deux clients) :</p> <p>Côté lancement, il faut trois processus, le serveur et deux clients :</p>
<pre class="grid">java -cp escampeobf.jar escampe.ServeurJeu 1234 1 <pre class="grid">java -cp escampeobf.jar escampe.ServeurJeu 1234 1
java -cp Puyaubreau_Russac.jar escampe.ClientJeu escampe.JoueurPuyaubreauRussac localhost 1234 java -cp Puyaubreau_Russac.jar escampe.ClientJeu escampe.JoueurPuyaubreauRussac localhost 1234
java -cp escampeobf.jar escampe.ClientJeu escampe.JoueurAleatoire localhost 1234</pre> java -cp escampeobf.jar escampe.ClientJeu escampe.JoueurAleatoire localhost 1234</pre>
@@ -224,24 +243,24 @@ java -cp escampeobf.jar escampe.ClientJeu escampe.JoueurAleatoire
<!-- ====================== 5. PLACEMENT ====================== --> <!-- ====================== 5. PLACEMENT ====================== -->
<h2>5. Placement d'ouverture</h2> <h2>5. Placement d'ouverture</h2>
<p>Le placement est irréversible : nous l'avons conçu à partir d'un constat issu de <p>Le placement ne se rejoue pas, donc autant le soigner. Le constat est venu de
l'auto-jeu une licorne mal placée peut se retrouver <em>seule pièce jouable et l'auto-jeu : une licorne mal posée peut devenir la seule pièce jouable sur le liseré
bloquée</em> sur le liseré imposé, forçant des passes successifs qui livrent imposé, et se retrouver bloquée, ce qui force des passes à répétition et abandonne
l'initiative à l'adversaire. Trois principes y répondent :</p> l'initiative. Trois principes répondent à ça :</p>
<ol> <ol>
<li><strong>Licorne dans un coin.</strong> Un coin n'a que deux cases voisines : <li><strong>La licorne dans un coin.</strong> Un coin n'a que deux voisines, donc
seulement deux cases d'où l'adversaire peut l'atteindre.</li> seulement deux cases d'où l'adversaire peut venir la prendre.</li>
<li><strong>Murs.</strong> On occupe ces deux voisines par des paladins. La <li><strong>Deux murs.</strong> On occupe ces deux voisines avec des paladins, et
licorne devient <em>incapturable</em> tant que les murs tiennent (impossible de la licorne devient imprenable tant que les murs tiennent, puisqu'on ne peut pas
franchir le dernier pas sur une case occupée).</li> finir son dernier pas sur une case occupée.</li>
<li><strong>Couverture des liserés.</strong> Les trois paladins restants sont <li><strong>Trois liserés couverts.</strong> Les trois paladins restants se posent
placés sur des cases de liserés <strong>1, 2 et 3 distincts</strong> : quel que sur des cases de liserés 1, 2 et 3 différents. Quel que soit le liseré imposé, il
soit le liseré imposé, on dispose toujours d'une pièce mobile jamais de pass reste une pièce mobile, et on n'a jamais à passer ni à déranger un mur ou la
forcé, jamais besoin de déplacer un mur ou la licorne.</li> licorne.</li>
</ol> </ol>
<p>Dispositions retenues (légalité et propriétés vérifiées) ; pour Blanc, on joue <p>Voici les deux dispositions retenues (Blanc prend le bord opposé à Noir) ; nous en
le bord complémentaire de celui de Noir :</p> avons vérifié la légalité et les trois propriétés ci-dessus :</p>
<pre class="grid">Bord bas A1/A2/B1/E1/F1/C2 Bord haut A6/A5/B6/C5/F5/E6 <pre class="grid">Bord bas A1/A2/B1/E1/F1/C2 Bord haut A6/A5/B6/C5/F5/E6
A B C D E F A B C D E F A B C D E F A B C D E F
2 n . . . . . 6 N b . . b . 2 n . . . . . 6 N b . . b .
@@ -249,120 +268,122 @@ le bord complémentaire de celui de Noir :</p>
(licorne A1, murs A2/B1, (licorne A6, murs A5/B6, (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)</pre> mobiles E1·F1·C2 = liserés 1·2·3) mobiles C5·F5·E6 = liserés 1·2·3)</pre>
<p>Nous n'avons pas de bibliothèque d'ouvertures. Elle aurait peu de valeur ici : la
contrainte de liseré rend toute séquence pré-calculée caduque dès que le placement
adverse diffère du cas prévu, souvent au deuxième coup. Et comme le branchement moyen
(~8,9) laisse l'alpha-bêta atteindre la profondeur 12 à 15 d'entrée, le placement fixe
ci-dessus suffit à démarrer sur une position saine et reproductible.</p>
<!-- ====================== 6. MOTEUR ====================== --> <!-- ====================== 6. MOTEUR ====================== -->
<h2>6. Moteur de décision</h2> <h2>6. Moteur de décision</h2>
<p>La décision repose sur un <strong>negamax</strong> avec <strong>élagage <p>Le choix du coup repose sur un <strong>negamax</strong> avec <strong>élagage
alpha-bêta</strong> et <strong>approfondissement itératif</strong> (classe alpha-bêta</strong> et <strong>approfondissement itératif</strong> (classe
<code>Moteur</code>). La recherche s'effectue sur une <em>copie</em> du plateau, <code>Moteur</code>). La recherche travaille sur une copie du plateau, jamais sur
jamais sur l'état réel. Capturer la licorne adverse est traité comme un nœud l'état réel. Une capture de licorne compte comme une feuille de valeur
terminal de valeur <code>WIN&nbsp;-&nbsp;ply</code> (gagner vite plutôt que tard).</p> <code>WIN&nbsp;-&nbsp;ply</code>, ce qui pousse à gagner tôt plutôt que tard.</p>
<p><strong>Astuces de performance.</strong></p> <p>Plusieurs choix tirent la vitesse vers le haut :</p>
<ul> <ul>
<li><strong>Coups encodés en entier</strong> (case = <code>ligne×6+colonne</code>, <li><strong>Coups codés sur un entier</strong> (case = <code>ligne×6+colonne</code>,
coup = <code>départ×36+arrivée</code>) : aucune chaîne manipulée dans la boucle coup = <code>départ×36+arrivée</code>), pour ne manipuler aucune chaîne dans la
chaude.</li> boucle chaude.</li>
<li><strong>DFS sur masque de bits <code>long</code></strong> : les 36 cases <li><strong>DFS sur masque de bits</strong> : les 36 cases tiennent dans un
tiennent dans un <code>long</code> ; les ensembles « visité » et « atteignable » <code>long</code>, et les ensembles « visité » et « atteignable » sont de simples
sont des masques — pas d'allocation de tableau par appel.</li> masques, sans tableau alloué à chaque appel.</li>
<li><strong><code>make</code>/<code>unmake</code> sans allocation</strong> : un <li><strong><code>make</code>/<code>unmake</code> sans allocation</strong> : un petit
petit jeton d'annulation suffit à défaire un coup, ce qui permet d'explorer des jeton suffit à défaire un coup, donc on explore des millions de nœuds sans solliciter
millions de nœuds sans pression sur le ramasse-miettes.</li> le ramasse-miettes.</li>
<li><strong>Buffers de coups p-alloués</strong>, un par profondeur.</li> <li><strong>Buffers de coups réservés</strong> à l'avance, un par profondeur.</li>
<li><strong>Ordonnancement</strong> : tout coup capturant la licorne est essayé <li><strong>Ordre des coups</strong> : on essaie d'abord toute prise de licorne (coupure
en premier (coupure immédiate) ; le meilleur coup d'une itération est repla en immédiate), et on remet en tête le meilleur coup de l'itération prédente.</li>
tête à l'itération suivante.</li>
</ul> </ul>
<p class="note">Cohérence des deux chemins. Le chemin « entier » du moteur double <p class="note">Le moteur a sa propre génération de coups en entiers, en parallèle de
le chemin « chaîne » vérifié de <code>EscampeBoard</code>. Pour exclure toute celle, vérifiée, d'<code>EscampeBoard</code> en chaînes. Pour être sûr qu'elles ne
divergence silencieuse entre ces deux implémentations des règles, un test croisé divergent pas en silence, le test <code>VerifMoves</code> (§9) confronte les deux et
(<code>VerifMoves</code>, §9) vérifie qu'ils produisent exactement les mêmes coups exige les mêmes coups et les mêmes états : c'est ce qui nous garantit qu'optimiser
et les mêmes états — c'est la garantie qu'optimiser n'a pas changé les règles.</p> n'a pas modifié les règles au passage.</p>
<p><strong>Performance mesurée.</strong> Environ <strong>4 à 5&nbsp;millions de <p>En pratique, le moteur explore de l'ordre de <strong>4 à 5&nbsp;millions de nœuds
nœuds par seconde</strong>. En milieu de partie, l'approfondissement itératif par seconde</strong>. En milieu de partie, l'approfondissement itératif atteint
atteint une profondeur de <strong>12 à 15 demi-coups</strong> en 6&nbsp;s (davantage <strong>12 à 15 demi-coups</strong> en 6&nbsp;s, davantage dans les positions
dans les positions étroites). Les annonces de gain forcé du moteur se matérialisent étroites. Quand il annonce un gain forcé, la capture a bien lieu dans les parties de
bien par une capture effective lors des parties de contrôle.</p> contrôle.</p>
<!-- ====================== 7. HEURISTIQUE ====================== --> <!-- ====================== 7. HEURISTIQUE ====================== -->
<h2>7. Heuristique d'évaluation</h2> <h2>7. Heuristique d'évaluation</h2>
<p>Le matériel étant constant (paladins imprenables, licornes présentes jusqu'à la <p>Le matériel ne bouge pas (paladins imprenables, licornes en place jusqu'à la
capture), l'évaluation d'une position non terminale est purement <em>positionnelle</em>, prise), donc évaluer une position non terminale revient à juger un placement.
exprimée du point de vue du joueur au trait. Elle somme, à partir des distances de L'évaluation se fait du point de vue du joueur au trait, à partir de distances de
Manhattan :</p> Manhattan, et combine deux idées :</p>
<ul> <ul>
<li><strong>Pression d'attaque</strong> : proximité de nos paladins à la licorne <li>la <strong>pression d'attaque</strong>, c'est-à-dire la proximité de nos paladins
adverse un terme de <em>somme</em> (pression globale) et un terme de à la licorne adverse, avec un terme de somme (pression d'ensemble) et un terme de
<em>minimum</em> (l'attaquant le plus proche pèse davantage) ;</li> minimum (le paladin le plus proche pèse plus lourd) ;</li>
<li><strong>Sécurité</strong> : éloignement des paladins adverses de notre <li>la <strong>sécurité</strong>, soit l'éloignement des paladins adverses de notre
licorne — mêmes deux termes, de signe opposé.</li> licorne, avec les deux mêmes termes mais de signe opposé.</li>
</ul> </ul>
<p>Concrètement, avec les poids retenus (somme&nbsp;=&nbsp;2, minimum&nbsp;=&nbsp;8) :</p> <p>Avec les poids retenus (2 pour les sommes, 8 pour les minimums) :</p>
<pre class="grid">eval = 2·Σ(10d_attaque) 2·Σ(10d_défense) <pre class="grid">eval = 2·Σ(10d_attaque) 2·Σ(10d_défense)
+ 8·(10min d_attaque) 8·(10min d_défense)</pre> + 8·(10min d_attaque) 8·(10min d_défense)</pre>
<p><strong>Heuristiques testées et choix.</strong> Le réglage s'est fait par <p>Pour régler ces poids, nous avons fait jouer le moteur contre lui-même et contre le
auto-jeu déterministe et matchs arbitrés contre le joueur aléatoire fourni. Nous joueur aléatoire fourni, en comparant trois variantes. Avec la somme seule (a), le jeu
avons comparé : (a) <em>somme seule</em> — jeu trop diffus, le moteur tarde à restait trop diffus et le moteur tardait à concentrer une menace. La somme plus le
concentrer une menace ; (b) <em>somme + minimum</em> (retenue) — le terme minimum, mininum (b), que nous avons gardée, recentre les paladins vers la licorne adverse grâce
fortement pondéré, oriente nettement les paladins vers la licorne adverse et au fort poids du minimum et fait monter le taux de capture. L'ajout d'un terme défensif
améliore le taux de capture ; (c) ajout d'un terme défensif symétrique — conservé, symétrique (c) a été conservé aussi : il évite d'exposer notre licorne sans pénaliser
il évite d'exposer notre licorne sans nuire à l'attaque. Le fort poids du terme l'attaque. Ce poids élevé sur le minimum traduit une réalité du jeu, où c'est le paladin
minimum reflète que, dans ce jeu, c'est l'attaquant <em>le plus avancé</em> qui le plus avancé qui conclut une prise.</p>
décide d'une prise.</p>
<p class="note">Limite assumée. Faute d'adversaires IA tiers disponibles avant le <p class="note">Une limite que nous assumons : faute d'autres IA disponibles avant le
tournoi, ces poids sont validés contre l'aléatoire et en auto-jeu, non contre tournoi, ces poids sont calés contre l'aléatoire et en auto-jeu, pas contre des joueurs
d'autres joueurs forts. Les tactiques de capture à court terme sont, elles, forts. Cela dit, les prises à courte échéance relèvent de la recherche, ce qui rend le
gérées par la recherche, ce qui rend le joueur robuste même avec une évaluation joueur solide même avec une évaluation aussi simple.</p>
positionnelle simple.</p>
<!-- ====================== 8. TEMPS ====================== --> <!-- ====================== 8. TEMPS ====================== -->
<h2>8. Gestion du temps réel</h2> <h2>8. Gestion du temps réel</h2>
<p>La limite de l'arbitre est de 300&nbsp;s par joueur et par partie. Nous nous <p>L'arbitre laisse 300&nbsp;s par joueur et par partie. Nous travaillons sous une
fixons une <strong>enveloppe interne de 280&nbsp;s</strong> (≈ 20&nbsp;s de marge). enveloppe interne de <strong>280&nbsp;s</strong>, soit une vingtaine de secondes de
Le budget alloué à un coup est une fraction du temps restant, bornée :</p> marge. Le budget d'un coup est une fraction du temps restant, bornée des deux côtés :</p>
<pre class="grid">tranche = clamp( temps_restant / 12 , 120 ms , 6000 ms )</pre> <pre class="grid">tranche = clamp( temps_restant / 12 , 120 ms , 6000 ms )</pre>
<p>La division par le temps restant décroît géométriquement : le budget ne peut <p>Diviser le temps restant le fait décroître géométriquement, si bien que le budget
<strong>jamais</strong> être épuisé, même sur une partie très longue. Le plafond de ne peut pas s'épuiser, même sur une partie qui s'éternise. Le plafond de 6&nbsp;s évite
6&nbsp;s évite de surinvestir en ouverture ; un plancher de 120&nbsp;ms garantit un de gaspiller du temps en ouverture, le plancher de 120&nbsp;ms garantit un minimum de
minimum de réflexion ; un mode « panique » sécurise les toutes dernières secondes. réflexion, et un mode « panique » couvre les toutes dernières secondes. Comme la
L'approfondissement itératif rend le meilleur coup déjà trouvé dès que la tranche recherche est itérative, le meilleur coup déjà trouvé est disponible dès que la tranche
expire (le temps est contrôlé toutes les 2048 explorations de nœuds).</p> expire, le temps étant relu toutes les 2048 explorations de nœuds.</p>
<p><strong>Mesures</strong> (auto-jeu équilibré, plein budget) : temps <p>En mesure (auto-jeu équilibré, plein budget), le coup le plus long approche le
<strong>maximal par coup ≈ 6,0&nbsp;s</strong> (le plafond), <strong>cumul maximal plafond, environ <strong>6&nbsp;s</strong>, et le cumul sur une partie entière plafonne
36&nbsp;s</strong> par joueur sur une partie complète — très loin des 300&nbsp;s. vers <strong>36&nbsp;s</strong> par joueur, loin des 300&nbsp;s. Le réglage est prudent
Le réglage est volontairement conservateur et pourrait être augmenté sans risque.</p> et on pourrait l'ouvrir davantage sans risque.</p>
<!-- ====================== 9. PERFS & TESTS ====================== --> <!-- ====================== 9. PERFS & TESTS ====================== -->
<h2>9. Performances et tests</h2> <h2>9. Performances et tests</h2>
<p>Notre démarche de validation est empirique et redondante : chaque maillon est <p>Chaque maillon de la chaîne est contrôlé contre une référence indépendante.</p>
contrôlé contre une référence indépendante.</p>
<table> <table>
<tr><th>Test</th><th>Ce qu'il garantit</th><th>Résultat</th></tr> <tr><th>Test</th><th>Ce qu'il garantit</th><th>Résultat</th></tr>
<tr> <tr>
<td><code>VerifMoves</code></td> <td><code>VerifMoves</code></td>
<td>Chemin entier (moteur) ≡ chemin chaîne (vérifié) : mêmes coups, même <td>Génération en entiers (moteur) identique à la génération en chaînes (vérifiée) :
<code>make</code>/<code>unmake</code></td> mêmes coups, même <code>make</code>/<code>unmake</code></td>
<td>3 000 parties · 142 165 positions · 1 281 985 contrôles · <strong>0 divergence</strong></td> <td>3 000 parties · 142 165 positions · 1 281 985 contrôles · <strong>0 divergence</strong></td>
</tr> </tr>
<tr> <tr>
<td><code>RulesTest</code></td> <td><code>RulesTest</code></td>
<td>Règles directes : pas = liseré, capture au dernier pas, paladins <td>Règles vérifiées directement : nombre de pas = liseré, capture au dernier pas,
imprenables, non-traversée, contrainte de liseré, pass forcé, fin, zones de placement</td> paladins imprenables, non-traversée, contrainte de liseré, pass forcé, fin, zones de placement</td>
<td><strong>21 / 21</strong></td> <td><strong>21 / 21</strong></td>
</tr> </tr>
<tr> <tr>
<td>Matchs arbitrés vs <code>JoueurAleatoire</code></td> <td>Matchs arbitrés vs <code>JoueurAleatoire</code></td>
<td>Protocole de bout en bout (placement, liseré, pass, couleurs), légalité</td> <td>Protocole de bout en bout (placement, liseré, pass, couleurs) et légalité</td>
<td><strong>7 / 7 victoires</strong>, 0 coup illégal, 0 exception (les deux couleurs)</td> <td><strong>7 / 7 victoires</strong>, 0 coup illégal, 0 exception (les deux couleurs)</td>
</tr> </tr>
<tr> <tr>
@@ -373,75 +394,74 @@ contrôlé contre une référence indépendante.</p>
<tr> <tr>
<td><code>Bench</code> / <code>Branching</code></td> <td><code>Bench</code> / <code>Branching</code></td>
<td>Vitesse, profondeur, facteur de branchement</td> <td>Vitesse, profondeur, facteur de branchement</td>
<td>≈ 45 M nœuds/s ; profondeur 1215 ; branchement max 49 / moyen ≈ 8,9</td> <td>≈ 45 M nœuds/s ; profondeur 1215 ; branchement max 49, moyen ≈ 8,9</td>
</tr> </tr>
</table> </table>
<p>La séparation des rôles est délibérée : <code>VerifMoves</code> prouve que le <p>Les rôles ne se recouvrent pas : <code>VerifMoves</code> montre que le moteur colle
moteur ≡ <code>EscampeBoard</code> ; <code>RulesTest</code> prouve que à <code>EscampeBoard</code>, <code>RulesTest</code> que <code>EscampeBoard</code>
<code>EscampeBoard</code> respecte les règles ; les parties arbitrées prouvent que respecte les règles, et les parties arbitrées que l'ensemble dialogue correctement avec
le tout dialogue correctement avec l'arbitre réel. Aucun coup illégal n'a été le vrai arbitre. Sur toutes les parties jouées, aucun coup illégal n'a été produit.</p>
produit sur l'ensemble des parties jouées.</p>
<!-- ====================== 10. BUILD ====================== --> <!-- ====================== 10. BUILD ====================== -->
<h2>10. Compilation, exécution et livrables</h2> <h2>10. Compilation, exécution et livrables</h2>
<p>Le script <code>build.sh</code> produit dans <code>dist/</code> les trois <p>Le script <code>build.sh</code> fabrique dans <code>dist/</code> les trois livrables
livrables de la version finale :</p> de la version finale :</p>
<pre class="grid">Puyaubreau_Russac.jar jar exécutable (Main-Class : escampe.ClientJeu) <pre class="grid">Puyaubreau_Russac.jar jar exécutable (Main-Class : escampe.ClientJeu)
mainClass jar:Puyaubreau_Russac.jar mainClass jar:Puyaubreau_Russac.jar
clientClass:escampe.ClientJeu clientClass:escampe.ClientJeu
mainClass:escampe.JoueurPuyaubreauRussac mainClass:escampe.JoueurPuyaubreauRussac
Puyaubreau_Russac.tgz archive de rendu : répertoire Puyaubreau_Russac/ Puyaubreau_Russac.tgz archive de rendu : répertoire Puyaubreau_Russac/
contenant src/escampe/*.java + mainClass + le jar</pre> contenant src/escampe/*.java + mainClass + le jar</pre>
<p>Seules les classes de production entrent dans le jar (le joueur, le moteur, le <p>Le jar ne contient que les classes de production (le joueur, le moteur, le plateau et
plateau et les classes fournies) ; les utilitaires de test (<code>VerifMoves</code>, les classes fournies)&nbsp;; les utilitaires de test (<code>VerifMoves</code>,
<code>RulesTest</code>, <code>Bench</code>, <code>Branching</code>) en sont exclus. <code>RulesTest</code>, <code>Bench</code>, <code>Branching</code>) restent dehors. Le
Le jeu en multijoueur (humain contre humain, ou humain contre notre IA, en local jeu en multijoueur, humain contre humain ou humain contre notre IA, en local comme à
ou à distance) est documenté à part dans <code>MULTIJOUEUR.md</code>.</p> distance, est décrit dans <code>MULTIJOUEUR.md</code>.</p>
<!-- ====================== 11. SOURCES ====================== --> <!-- ====================== 11. SOURCES ====================== -->
<h2>11. Sources et bibliographie</h2> <h2>11. Sources et bibliographie</h2>
<ul> <ul>
<li><strong>Énoncé du cours</strong> (Université Paris-Saclay, Polytech APP5, <li>L'<strong>énoncé du cours</strong> (Université Paris-Saclay, Polytech APP5,
2025-2026) : règles d'Escampe, carte des liserés (figure&nbsp;4), interface 2025-2026) pour les règles d'Escampe, la carte des liserés (figure&nbsp;4),
<code>Partie1</code>, et classes d'infrastructure fournies l'interface <code>Partie1</code> et les classes fournies
(<code>IJoueur</code>, <code>ClientJeu</code>, <code>Solo</code>, (<code>IJoueur</code>, <code>ClientJeu</code>, <code>Solo</code>,
<code>Applet</code>, serveur).</li> <code>Applet</code>, serveur).</li>
<li><strong>Algorithmes classiques</strong>, à titre d'inspiration et sans copie <li>Des <strong>algorithmes classiques</strong>, comme inspiration et sans copie de
de code : élagage alpha-bêta (Knuth &amp; Moore, <em>An Analysis of Alpha-Beta code : l'élagage alpha-bêta (Knuth et Moore, <em>An Analysis of Alpha-Beta
Pruning</em>, 1975) ; minimax, negamax et approfondissement itératif Pruning</em>, 1975), le minimax, le negamax et l'approfondissement itératif
(Russell &amp; Norvig, <em>Artificial Intelligence: A Modern Approach</em>) ; (Russell et Norvig, <em>Artificial Intelligence: A Modern Approach</em>), ainsi que
techniques de représentation par masques de bits et d'ordonnancement de coups les représentations par masques de bits et l'ordonnancement de coups
(<em>Chess Programming Wiki</em>).</li> (<em>Chess Programming Wiki</em>).</li>
<li><strong>Déclaration.</strong> Aucun programme d'Escampe externe n'a été <li>Pour être clairs : nous n'avons recopié aucun programme d'Escampe existant. La
recopié. La seule rétro-ingénierie effectuée porte sur le jar d'arbitre seule rétro-ingénierie a porté sur le jar d'arbitre fourni avec le sujet, et
<em>fourni avec le sujet</em>, dans le seul but de confirmer le protocole (pass uniquement pour confirmer le protocole (le pass <code>"E"</code>) et la carte des
<code>"E"</code>) et la carte des liserés points sur lesquels la documentation liserés, deux points que la documentation laissait dans le flou.</li>
était ambiguë.</li>
</ul> </ul>
<!-- ====================== 12. CONCLUSION ====================== --> <!-- ====================== 12. CONCLUSION ====================== -->
<h2>12. Conclusion et difficultés rencontrées</h2> <h2>12. Conclusion et difficultés rencontrées</h2>
<p>Le joueur conduit une partie de façon autonome, dialogue correctement avec <p>Le joueur mène une partie tout seul, dialogue correctement avec l'arbitre, ne joue
l'arbitre, ne produit jamais de coup illégal et respecte très confortablement la jamais de coup illégal et tient le temps très largement. Les principaux obstacles ont
contrainte de temps. Les principales difficultés ont été :</p> été les suivants :</p>
<ul> <ul>
<li><strong>L'obfuscation du serveur</strong> : lever l'ambiguïté du pass <li><strong>L'obfuscation du serveur.</strong> Trancher l'ambiguïté du pass
(<code>"E"</code> vs <code>"PASSE"</code>) et confirmer la carte des liserés a (<code>"E"</code> et non <code>"PASSE"</code>) et confirmer la carte des liserés a
demandé une analyse du jar — étape décisive pour ne pas perdre sur coup illégal.</li> demandé de fouiller le jar, sans quoi on perdait sur coup illégal.</li>
<li><strong>L'interface obfusquée vs nos sources</strong> : le joueur aléatoire du <li><strong>L'interface obfusquée face à nos sources.</strong> Le joueur aléatoire du
jar n'implémente pas notre <code>IJoueur</code> ; les tests contre lui passent jar n'implémente pas notre <code>IJoueur</code>, donc les tests contre lui passent par
donc par le réseau (seules des chaînes circulent).</li> le réseau, où seules des chaînes circulent.</li>
<li><strong>L'avantage du trait</strong> : en miroir, Blanc (premier à jouer) <li><strong>L'avantage du trait.</strong> En miroir, Blanc, qui joue le premier, garde
conserve l'initiative via la contrainte de liseré propriété du jeu, indépendante l'initiative via la contrainte de liseré&nbsp;; c'est une propriété du jeu, pas une
de la force du moteur.</li> question de force du moteur.</li>
<li><strong>Le réglage de l'heuristique sans adversaires</strong> : validé contre <li><strong>Le réglage de l'heuristique sans sparring-partner</strong>, calé faute de
l'aléatoire et en auto-jeu.</li> mieux contre l'aléatoire et en auto-jeu.</li>
</ul> </ul>
<p><strong>Pistes d'amélioration</strong> : table de transposition (hachage de <p>Si nous devions continuer, plusieurs pistes se présentent : une table de transposition
Zobrist), bibliothèque d'ouvertures de placement, terme de mobilité différentielle (hachage de Zobrist), une bibliothèque d'ouvertures de placement, un terme de mobilité
dans l'évaluation, et recherche de quiescence sur les menaces de capture.</p> différentielle dans l'évaluation et une recherche de quiescence sur les menaces de
capture.</p>
</body> </body>
</html> </html>

View File

@@ -44,10 +44,16 @@ tr:nth-child(even) td { background: #f4f6f8; }
p.note { background: #fff8e6; border-left: 3px solid #e0a526; p.note { background: #fff8e6; border-left: 3px solid #e0a526;
padding: 5pt 8pt; margin: 7pt 0; font-size: 9.8pt; } padding: 5pt 8pt; margin: 7pt 0; font-size: 9.8pt; }
/* Sommaire */
.toc-title { border: none; color: #1c3d5a; margin-bottom: 4pt; }
ol.toc { font-size: 11pt; line-height: 1.7; color: #1a1a1a; }
ol.toc li { margin: 1pt 0; }
/* Éviter qu'un bloc préformaté ou une table soit coupé entre deux pages. */ /* Éviter qu'un bloc préformaté ou une table soit coupé entre deux pages. */
pre.grid, table, tr { page-break-inside: avoid; } pre.grid, table, tr { page-break-inside: avoid; }
h2, h3 { page-break-after: avoid; } h2, h3 { page-break-after: avoid; }
.cover { page-break-after: always; } .cover { page-break-after: always; }
ol.toc { page-break-after: always; }
/* Page de titre */ /* Page de titre */
.cover { text-align: center; padding-top: 40pt; } .cover { text-align: center; padding-top: 40pt; }
@@ -107,9 +113,9 @@ def verify():
doc = fitz.open(OUT) doc = fitz.open(OUT)
full = "".join(p.get_text() for p in doc) full = "".join(p.get_text() for p in doc)
doc.close() doc.close()
# Mots accentués présents tels quels dans report/rapport.html. # Mots accentués stables, présents dans report/rapport.html (titres de section).
probes = ["liseré", "Présentation", "élagage", "Modélisation", probes = ["liseré", "Présentation", "Modélisation", "Intégration",
"stratégique", "approfondissement", "Puyaubreau"] "Heuristique", "Puyaubreau"]
missing = [s for s in probes if s not in full] missing = [s for s in probes if s not in full]
return missing, len(full) return missing, len(full)