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:
428
RAPPORT.md
428
RAPPORT.md
@@ -13,8 +13,8 @@ Joueur : `escampe.JoueurPuyaubreauRussac`
|
||||
## Sommaire
|
||||
|
||||
1. [Présentation et règles](#1-présentation-et-règles)
|
||||
2. [Analyse des caractéristiques du jeu (Q1–Q7)](#2-analyse-des-caractéristiques-du-jeu-q1q7)
|
||||
3. [Modélisation : la classe `EscampeBoard`](#3-modélisation--la-classe-escapeboard)
|
||||
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-escampeboard)
|
||||
4. [Intégration au tournoi : protocole de l'arbitre](#4-intégration-au-tournoi--protocole-de-larbitre)
|
||||
5. [Placement d'ouverture](#5-placement-douverture)
|
||||
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é
|
||||
*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**.
|
||||
**paladins** (noirs ou blancs). Les lignes vont de 1 à 6, les colonnes de A à F, et
|
||||
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. Toute la difficulté consiste donc à "coincer" son adversaire.
|
||||
Ce qui fait l'originalité du jeu, c'est la **contrainte de liseré** : la pièce qu'on
|
||||
joue doit partir d'une case dont le liseré est le même que celui de la case où
|
||||
l'adversaire vient de poser sa pièce. Ce liseré de départ fixe aussi le nombre de pas
|
||||
(1, 2 ou 3), orthogonaux, sans traverser ni repasser sur une case déjà visitée. On ne
|
||||
capture qu'en s'arrêtant, au dernier pas, sur la licorne adverse ; les paladins, eux,
|
||||
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 (Q1–Q7)
|
||||
## 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
|
||||
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).
|
||||
Le plateau est un tableau `int[6][6]` (`board[ligne][colonne]`, ligne 0 = ligne 1 en
|
||||
bas, colonne 0 = A). Chaque case vaut `EMPTY`, `WHITE_LICORNE`, `WHITE_PALADIN`,
|
||||
`BLACK_LICORNE` ou `BLACK_PALADIN`. Quatre informations que le tableau ne porte pas,
|
||||
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,
|
||||
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`).
|
||||
Le tableau d'entiers donne un accès en O(1) à n'importe quelle case et se copie sans
|
||||
effort, ce qui compte pour l'arbre de recherche ; il se sérialise aussi directement
|
||||
vers le format de fichier. Surtout, il autorise un schéma `make`/`unmake` qui n'alloue
|
||||
rien (voir §6). Le seul point 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 `play`.
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
> **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.
|
||||
> Nous avons extrait par réflexion la carte qu'utilise l'arbitre dans sa propre classe
|
||||
> de jeu, et elle coïncide case pour case avec la nôtre (elle colle aussi à l'exemple
|
||||
> de la figure 6). La vérification valait le coup : une carte fausse aurait fait
|
||||
> rejeter nos coups par l'arbitre.
|
||||
|
||||
### 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.
|
||||
La partie s'arrête dès qu'une des deux licornes quitte le plateau ; il n'y a pas
|
||||
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
|
||||
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é.
|
||||
Quatre choses rendent le jeu retors : la contrainte de liseré, qui fait varier
|
||||
fortement la mobilité ; la dépendance entre tours, puisque la case d'arrivée qu'on
|
||||
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
|
||||
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é :
|
||||
Côté **facteur de branchement**, nous avions avancé en première partie une borne
|
||||
théorique de l'ordre de 120 (six pièces, jusqu'à une vingtaine de destinations sur un
|
||||
liseré triple). En pratique c'est beaucoup moins, parce que la contrainte de liseré ne
|
||||
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é |
|
||||
|---|---|
|
||||
| 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** |
|
||||
|
||||
Le branchement effectif modeste explique les profondeurs élevées atteintes par
|
||||
l'alpha-bêta (§6).
|
||||
Avec une moyenne sous 10, l'alpha-bêta descend profond en quelques secondes (§6).
|
||||
|
||||
### 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
|
||||
toujours bloquer une menace, car l'adversaire choisit sa case d'arrivée et donc le
|
||||
liseré qu'il impose au tour suivant. En revanche, certaines configurations créent
|
||||
un **zugzwang partiel** — l'adversaire est forcé d'imposer précisément le liseré
|
||||
qui autorise la capture.
|
||||
L'énoncé en donne un cas net (figure 6). Noir vient de jouer en D4, une case à liseré
|
||||
double, donc Blanc doit partir d'une case double : il choisit **F6 – E5** (F6 est
|
||||
double). Noir est alors contraint de jouer depuis un liseré simple comme E5, et son
|
||||
seul coup raisonnable est **A1 – A2**. Or A2 est à liseré triple : Blanc enchaîne
|
||||
**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,
|
||||
TILE\_MAP = 2). Blanc doit donc jouer depuis une case à liseré double. Il choisit
|
||||
**F6–E5** (F6 est à liseré double). Noir doit maintenant jouer depuis une case à
|
||||
liseré simple (E5 = 1) : son seul coup raisonnable est **A1–A2**. 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
|
||||
F6–E5 / A1–A2 / 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.
|
||||
Ce genre de combinaison ne se construit pas mécaniquement depuis l'ouverture, il y a
|
||||
trop de degrés de liberté ; mais notre alpha-bêta la trouve et la joue dès qu'elle
|
||||
entre dans son horizon de recherche.
|
||||
|
||||
### 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.
|
||||
Cinq critères nous semblaient pertinents : la distance à la licorne adverse, la
|
||||
mobilité différentielle, le contrôle du liseré qu'on impose, la protection de sa
|
||||
propre licorne et l'avancée des pièces. Au final (§7), l'évaluation retenue tient
|
||||
surtout à deux d'entre eux, la proximité de nos paladins à la licorne adverse (attaque)
|
||||
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
|
||||
- **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.
|
||||
- **Ouverture (placement)** : c'est irréversible, donc on sécurise d'emblée la licorne
|
||||
et on s'arrange pour pouvoir toujours jouer (§5).
|
||||
- **Milieu** : on manœuvre pour menacer la licorne adverse tout en gardant la main sur
|
||||
le liseré qu'on impose, en visant le zugzwang partiel.
|
||||
- **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
|
||||
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).
|
||||
Aucune pièce ne disparaît avant la prise finale, donc une partie peut traîner. En
|
||||
bornant le branchement par tour sur quelques dizaines de tours, on arrive à un ordre de
|
||||
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`
|
||||
|
||||
`EscampeBoard` 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"`.
|
||||
`EscampeBoard` implémente l'interface fournie `Partie1` (`setFromFile`/`saveToFile`,
|
||||
`isValidMove`, `possiblesMoves`, `play`, `gameOver`) et suit les conventions de
|
||||
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
|
||||
d'un numéro ; lignes `%` = commentaires (où l'on stocke l'état hors-plateau pour
|
||||
un rechargement fidèle).
|
||||
Le format de fichier reprend celui de l'énoncé : six lignes de plateau du bas vers le
|
||||
haut, avec `N/n` pour le noir, `B/b` pour le blanc et `-` pour le vide, chaque ligne
|
||||
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
|
||||
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.
|
||||
Pour générer les coups, on part d'une case et on énumère les arrivées par un parcours
|
||||
en profondeur avec retour arrière : exactement N pas (N = liseré de départ), cases
|
||||
intermédiaires vides, et case finale soit vide soit occupée par la licorne adverse,
|
||||
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
|
||||
> planter le calcul du bord de Noir (supposait deux lignes). Le bord est désormais
|
||||
> déduit de la ligne de la licorne.
|
||||
> Un bug s'était glissé là et nous l'avons corrigé en partie 3 : un placement légal
|
||||
> mais aligné sur une seule ligne faisait planter le calcul du bord de Noir, qui
|
||||
> 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
|
||||
|
||||
`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é** :
|
||||
`JoueurPuyaubreauRussac implements IJoueur` garde à jour un `EscampeBoard` à chaque
|
||||
coup, le nôtre via `play`, celui de l'adversaire reçu par `mouvementEnnemi`. Trois
|
||||
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
|
||||
`"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).
|
||||
- **Les couleurs** : `IJoueur` raisonne en entiers (`NOIR=1`, `BLANC=-1`) alors que
|
||||
`EscampeBoard` utilise les chaînes `"noir"` et `"blanc"`.
|
||||
- **Le pass se note `"E"`, pas `"PASSE"`** : le Javadoc d'`IJoueur` annonce `"PASSE"`,
|
||||
mais le serveur teste bel et bien `move.equals("E")`, et `"PASSE"` n'apparaît nulle
|
||||
part dans le jar. Suivre le Javadoc nous aurait coûté la partie sur coup illégal.
|
||||
- **La carte des liserés** doit être celle du 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`) :
|
||||
Placement et coups passent par le même canal : le premier `choixMouvement` renvoie un
|
||||
placement, les suivants des coups, la phase se lisant sur `blackPlaced`/`whitePlaced`.
|
||||
En lisant la classe `Solo` fournie, on reconstitue l'ordre des appels :
|
||||
|
||||
```
|
||||
Noir : choixMouvement(placement) -> mvtEnnemi(placement Blanc)
|
||||
@@ -186,7 +209,11 @@ Blanc : mvtEnnemi(placement Noir) -> choixMouvement(placement)
|
||||
-> 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 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
|
||||
|
||||
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 :
|
||||
Le placement ne se rejoue pas, donc autant le soigner. Le constat est venu de
|
||||
l'auto-jeu : une licorne mal posée peut devenir la seule pièce jouable sur le liseré
|
||||
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.
|
||||
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.
|
||||
1. **La licorne dans un coin.** Un coin n'a que deux voisines, donc seulement deux
|
||||
cases d'où l'adversaire peut venir la prendre.
|
||||
2. **Deux murs.** On occupe ces deux voisines avec des paladins, et la licorne devient
|
||||
imprenable tant que les murs tiennent, puisqu'on ne peut pas finir son dernier pas
|
||||
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
|
||||
@@ -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
|
||||
```
|
||||
|
||||
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 12–15 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
|
||||
|
||||
Negamax + élagage alpha-bêta + approfondissement itératif (`Moteur`), sur une
|
||||
**copie** du plateau. Capture de licorne = nœud terminal `WIN - ply` (gagner vite).
|
||||
Le choix du coup repose sur un negamax avec élagage alpha-bêta et approfondissement
|
||||
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 :**
|
||||
- **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.
|
||||
Plusieurs choix tirent la vitesse vers le haut :
|
||||
|
||||
> 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.
|
||||
- **Coups codés sur un entier** (case = `ligne*6+colonne`, coup = `départ*36+arrivée`),
|
||||
pour ne manipuler aucune chaîne dans la boucle chaude.
|
||||
- **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** : ~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.
|
||||
> Le moteur a sa propre génération de coups en entiers, en parallèle de celle, vérifiée,
|
||||
> d'`EscampeBoard` en chaînes. Pour être sûr qu'elles ne divergent pas en silence, le
|
||||
> 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
|
||||
|
||||
Matériel constant → évaluation purement positionnelle, du point de vue du joueur
|
||||
au trait, à partir des distances de Manhattan :
|
||||
Le matériel ne bouge pas (paladins imprenables, licornes en place jusqu'à la prise),
|
||||
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*
|
||||
(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é.
|
||||
- la **pression d'attaque**, c'est-à-dire la proximité de nos paladins à la licorne
|
||||
adverse, avec un terme de somme (pression d'ensemble) et un terme de minimum (le
|
||||
paladin le plus proche pèse plus lourd) ;
|
||||
- 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·Σ(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.
|
||||
Pour régler ces poids, nous avons fait jouer le moteur contre lui-même et contre le
|
||||
joueur aléatoire fourni, en comparant trois variantes. Avec la somme seule, le jeu
|
||||
restait trop diffus et le moteur tardait à concentrer une menace. La somme plus le
|
||||
minimum, que nous avons gardée, recentre les paladins vers la licorne adverse grâce au
|
||||
fort poids du minimum et fait monter le taux de capture. L'ajout d'un terme défensif
|
||||
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
|
||||
> 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.
|
||||
> Une limite que nous assumons : faute d'autres IA disponibles avant le tournoi, ces
|
||||
> poids sont calés contre l'aléatoire et en auto-jeu, pas contre des joueurs forts. Cela
|
||||
> 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
|
||||
|
||||
Limite arbitre 300 s/joueur/partie → **enveloppe interne 280 s** (~20 s de marge).
|
||||
Budget par coup :
|
||||
L'arbitre laisse 300 s par joueur et par partie. Nous travaillons sous une enveloppe
|
||||
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 )
|
||||
```
|
||||
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.
|
||||
Diviser le temps restant le fait décroître géométriquement, si bien que le budget ne
|
||||
peut pas s'épuiser, même sur une partie qui s'éternise. Le plafond de 6 s évite de
|
||||
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
|
||||
|
||||
Chaque maillon de la chaîne est contrôlé contre une référence indépendante.
|
||||
|
||||
| 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** |
|
||||
| `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 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) |
|
||||
| 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 |
|
||||
| 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 | ~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.
|
||||
Les rôles ne se recouvrent pas : `VerifMoves` montre que le moteur colle à
|
||||
`EscampeBoard`, `RulesTest` que `EscampeBoard` respecte les règles, et les parties
|
||||
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
|
||||
|
||||
`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)
|
||||
@@ -321,44 +377,48 @@ mainClass jar:Puyaubreau_Russac.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
|
||||
(`VerifMoves`, `RulesTest`, `Bench`, `Branching`) en sont exclus. Le multijoueur
|
||||
(humain vs humain, humain vs IA, local ou distant) est documenté dans
|
||||
`MULTIJOUEUR.md`.
|
||||
Le jar ne contient que les classes de production ; les utilitaires de test
|
||||
(`VerifMoves`, `RulesTest`, `Bench`, `Branching`) restent dehors. Le jeu en
|
||||
multijoueur (humain contre humain, humain contre notre IA, en local comme à distance)
|
||||
est décrit 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ë).
|
||||
- L'**énoncé du cours** (Université Paris-Saclay, Polytech APP5, 2025-2026) pour les
|
||||
règles, la carte des liserés (figure 4), l'interface `Partie1` et les classes fournies
|
||||
(`IJoueur`, `ClientJeu`, `Solo`, `Applet`, serveur).
|
||||
- Des **algorithmes classiques**, comme inspiration et sans copie de code : l'élagage
|
||||
alpha-bêta (Knuth et Moore, 1975), le minimax, le negamax et l'approfondissement
|
||||
itératif (Russell et Norvig, *AIMA*), ainsi que les masques de bits et l'ordonnancement
|
||||
de coups (*Chess Programming Wiki*).
|
||||
- Pour être clairs : nous n'avons recopié aucun programme d'Escampe existant. La seule
|
||||
rétro-ingénierie a porté sur le jar d'arbitre fourni avec le sujet, et uniquement pour
|
||||
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
|
||||
|
||||
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 :
|
||||
Le joueur mène une partie tout seul, dialogue correctement avec l'arbitre, ne joue
|
||||
jamais de coup illégal et tient le temps très largement. Les principaux obstacles ont
|
||||
été les suivants :
|
||||
|
||||
- **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.
|
||||
- **L'obfuscation du serveur.** Trancher l'ambiguïté du pass (`"E"` et non `"PASSE"`)
|
||||
et confirmer la carte des liserés a demandé de fouiller le jar, sans quoi on perdait
|
||||
sur coup illégal.
|
||||
- **L'interface obfusquée face à nos sources.** Le joueur aléatoire du jar n'implémente
|
||||
pas notre `IJoueur`, donc les tests contre lui passent par le réseau, où seules des
|
||||
chaînes circulent.
|
||||
- **L'avantage du trait.** En miroir, Blanc, qui joue le premier, garde l'initiative via
|
||||
la contrainte de liseré ; c'est une propriété du jeu, pas une question de force du
|
||||
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
|
||||
d'ouvertures de placement, terme de mobilité différentielle, recherche de
|
||||
quiescence sur les menaces de capture.
|
||||
Si nous devions continuer, plusieurs pistes se présentent : une table de transposition
|
||||
(hachage de Zobrist), une bibliothèque d'ouvertures de placement, un terme de mobilité
|
||||
différentielle dans l'évaluation et une recherche de quiescence sur les menaces de
|
||||
capture.
|
||||
|
||||
Reference in New Issue
Block a user