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.
|
||||
|
||||
BIN
dist/Puyaubreau_Russac.jar
vendored
BIN
dist/Puyaubreau_Russac.jar
vendored
Binary file not shown.
BIN
dist/Puyaubreau_Russac.tgz
vendored
BIN
dist/Puyaubreau_Russac.tgz
vendored
Binary file not shown.
BIN
dist/Puyaubreau_Russac/Puyaubreau_Russac.jar
vendored
BIN
dist/Puyaubreau_Russac/Puyaubreau_Russac.jar
vendored
Binary file not shown.
BIN
dist/Puyaubreau_Russac_rapport.pdf
vendored
BIN
dist/Puyaubreau_Russac_rapport.pdf
vendored
Binary file not shown.
@@ -23,56 +23,75 @@
|
||||
</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 ====================== -->
|
||||
<h2>1. Présentation et règles</h2>
|
||||
|
||||
<p>Escampe se joue sur un plateau de 36 cases (6×6). Chaque case porte un
|
||||
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
|
||||
ou blanche). Les lignes sont numérotées de 1 à 6, les colonnes de A à F. Le but
|
||||
est de <strong>prendre la licorne adverse</strong>.</p>
|
||||
d'une <strong>licorne</strong> et de cinq <strong>paladins</strong> (noirs ou
|
||||
blancs). Les lignes vont de 1 à 6, les colonnes de A à F, et le but est de
|
||||
<strong>prendre la licorne adverse</strong>.</p>
|
||||
|
||||
<p>La règle caractéristique du jeu est une <strong>contrainte de liseré</strong> :
|
||||
la pièce que l'on joue doit partir d'une case dont le liseré est <em>identique</em>
|
||||
à celui de la case d'arrivée du coup adverse précédent. Le liseré de la case de
|
||||
départ fixe en outre 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 — les paladins sont imprenables. Si un joueur ne peut rien jouer, il passe
|
||||
son tour.</p>
|
||||
<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 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.</p>
|
||||
|
||||
<p>Le déroulement : Noir place ses six pièces sur les deux lignes d'un bord
|
||||
(haut ou bas) ; Blanc fait de même sur le bord opposé ; <strong>Blanc joue le
|
||||
premier coup</strong>. Ce rapport décrit nos choix de modélisation (parties 1
|
||||
et 2) puis la conception du joueur artificiel pour le tournoi (partie 3),
|
||||
avec les mesures qui justifient nos choix.</p>
|
||||
<p>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 <strong>Blanc qui joue le premier coup</strong>. Le rapport reprend nos choix
|
||||
de modélisation (parties 1 et 2) puis détaille la conception du joueur pour
|
||||
le tournoi (partie 3), chiffres à l'appui.</p>
|
||||
|
||||
<!-- ====================== 2. ANALYSE Q1-Q7 ====================== -->
|
||||
<h2>2. Analyse des caractéristiques du jeu</h2>
|
||||
|
||||
<p>Nous reprenons ici les sept questions de la première partie, en les étayant
|
||||
par l'implémentation finalement réalisée.</p>
|
||||
<p>Nous reprenons les sept questions de la première partie, cette fois à la lumière
|
||||
du code que nous avons réellement écrit.</p>
|
||||
|
||||
<h3>Q1 — Modélisation d'un état</h3>
|
||||
<p>Le plateau est un tableau <code>int[6][6]</code> : <code>board[ligne][colonne]</code>
|
||||
avec <code>ligne 0</code> = ligne 1 (bas) et <code>colonne 0</code> = A.
|
||||
Chaque case contient une constante de pièce (<code>EMPTY</code>,
|
||||
<p>Le plateau est un tableau <code>int[6][6]</code> : <code>board[ligne][colonne]</code>,
|
||||
avec <code>ligne 0</code> = ligne 1 (en bas) et <code>colonne 0</code> = A.
|
||||
Chaque case vaut une constante de pièce (<code>EMPTY</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
|
||||
maintenu hors du plateau :</p>
|
||||
<code>BLACK_PALADIN</code>). Quatre informations que le tableau ne porte pas, mais
|
||||
dont la règle a besoin, sont gardées à côté :</p>
|
||||
<ul>
|
||||
<li><code>lastTileType</code> : liseré imposé au coup suivant (<code>-1</code> = aucune contrainte) ;</li>
|
||||
<li><code>currentPlayer</code> : joueur au trait ;</li>
|
||||
<li><code>blackPlaced</code>, <code>whitePlaced</code> : fin des phases de placement ;</li>
|
||||
<li><code>blackRows</code> : le bord choisi par Noir (en déduit celui de Blanc).</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> : le joueur au trait ;</li>
|
||||
<li><code>blackPlaced</code>, <code>whitePlaced</code> : la fin des phases de placement ;</li>
|
||||
<li><code>blackRows</code> : le bord choisi par Noir, qui détermine celui de Blanc.</li>
|
||||
</ul>
|
||||
<p><strong>Avantages.</strong> Accès O(1) à toute case ; copie immédiate de l'état
|
||||
pour l'arbre de recherche ; sérialisation triviale ; surtout, un schéma
|
||||
<code>make/unmake</code> sans aucune allocation (essentiel pour la vitesse, §6).
|
||||
<strong>Inconvénient.</strong> La contrainte de liseré est un état séparé qu'il
|
||||
faut maintenir explicitement à chaque coup ; nous l'encapsulons dans <code>play</code>.</p>
|
||||
<p>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
|
||||
<code>make</code>/<code>unmake</code> 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 <code>play</code>.</p>
|
||||
|
||||
<p>La carte des liserés est une constante <code>TILE_MAP</code> reproduisant la
|
||||
figure 4 de l'énoncé (ligne 1 en bas) :</p>
|
||||
<p>La carte des liserés est figée dans la constante <code>TILE_MAP</code>, recopie
|
||||
de la figure 4 de l'énoncé (ligne 1 en bas) :</p>
|
||||
<pre class="grid"> A B C D E F
|
||||
6 3 2 2 1 3 2
|
||||
5 1 3 1 3 1 2
|
||||
@@ -80,143 +99,143 @@ figure 4 de l'énoncé (ligne 1 en bas) :</p>
|
||||
3 2 3 1 2 1 3
|
||||
2 3 1 3 1 3 2
|
||||
1 1 2 2 3 1 2</pre>
|
||||
<p class="note">Fait vérifié : cette carte est <em>identique</em>, case pour case,
|
||||
à celle utilisée en interne par l'arbitre du tournoi — nous l'avons extraite par
|
||||
réflexion de la classe de jeu du serveur fourni. Elle est aussi cohérente avec
|
||||
l'exemple tactique de la figure 6 de l'énoncé. Une carte divergente aurait
|
||||
produit des coups jugés illégaux : ce point était critique.</p>
|
||||
<p class="note">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.</p>
|
||||
|
||||
<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
|
||||
de fin, pas de match nul). La vérification est un simple balayage O(1) du plateau
|
||||
(<code>gameOver</code>) ; le moteur, lui, détecte la capture directement au moment
|
||||
où elle est jouée (§6).</p>
|
||||
<p>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 (<code>gameOver</code>) 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 (§6).</p>
|
||||
|
||||
<h3>Q3 — Sources de difficulté et facteur de branchement</h3>
|
||||
<p>Les principales sources de difficulté sont :</p>
|
||||
<ul>
|
||||
<li>la <strong>contrainte de liseré</strong>, qui limite fortement et variablement la mobilité ;</li>
|
||||
<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>
|
||||
<li>l'<strong>asymétrie</strong> du plateau (zones riches en liserés triples, donc mobiles, vs zones simples) ;</li>
|
||||
<li>le risque de <strong>blocage</strong> d'une pièce, voire d'un joueur (pass forcé).</li>
|
||||
</ul>
|
||||
<p><strong>Facteur de branchement.</strong> En première partie nous avions estimé
|
||||
une borne théorique de l'ordre de 120 (6 pièces × jusqu'à ~20 destinations
|
||||
sur liseré triple). La mesure réelle est bien plus basse, car la contrainte de
|
||||
liseré ne laisse jouables que les pièces du bon liseré. Sur 30 000 parties
|
||||
aléatoires simulées (utilitaire <code>escampe.Branching</code>) :</p>
|
||||
<p>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.</p>
|
||||
<p>Côté <strong>facteur de branchement</strong>, 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 <code>escampe.Branching</code>) donne :</p>
|
||||
<table>
|
||||
<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 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>
|
||||
</table>
|
||||
<p>Le branchement effectif modeste (moyenne < 10) explique qu'une recherche
|
||||
alpha-bêta atteigne des profondeurs élevées en quelques secondes (§6).</p>
|
||||
<p>Avec une moyenne sous 10, l'alpha-bêta descend profond en quelques secondes (§6).</p>
|
||||
|
||||
<h3>Q4 — Coups imparables</h3>
|
||||
<p>Il n'existe pas de coup « imparable » universel garanti dès le départ : la
|
||||
contrainte de liseré peut toujours empêcher l'exécution d'une menace au mauvais
|
||||
moment. En revanche, certaines configurations créent un <strong>zugzwang
|
||||
partiel</strong> où l'adversaire ne peut éviter d'imposer le liseré qui nous
|
||||
arrange — l'énoncé en donne l'exemple (figure 6 : le paladin blanc en C2 prend
|
||||
la licorne en C1 dès que Noir est forcé d'imposer un liseré double). Construire de
|
||||
tels pièges est un axe stratégique ; notre recherche les exploite implicitement
|
||||
quand ils sont à portée d'horizon.</p>
|
||||
<p>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
|
||||
<strong>zugzwang partiel</strong>, où il est forcé d'imposer précisément le liseré
|
||||
qui ouvre la capture.</p>
|
||||
<p>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
|
||||
<strong>F6 – 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 – A2</strong>. Or A2 est à liseré triple : Blanc enchaîne
|
||||
<strong>C2 × C1</strong>, 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.</p>
|
||||
<p>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.</p>
|
||||
|
||||
<h3>Q5 — Critères pour l'heuristique</h3>
|
||||
<p>Nous avions identifié cinq critères : distance à la licorne adverse, mobilité
|
||||
différentielle, contrôle du liseré imposé, protection de sa propre licorne, et
|
||||
avancée sur le plateau. L'heuristique finalement retenue (§7) s'appuie sur la
|
||||
<strong>proximité des paladins à la licorne adverse</strong> (pression d'attaque)
|
||||
et l'<strong>éloignement des paladins adverses de notre licorne</strong>
|
||||
(sécurité) — les autres critères sont, en pratique, largement pris en charge par
|
||||
la recherche elle-même.</p>
|
||||
<p>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.</p>
|
||||
|
||||
<h3>Q6 — Stratégie selon la phase</h3>
|
||||
<ul>
|
||||
<li><strong>Début (placement)</strong> : irréversible et déterminant. On protège
|
||||
la licorne et on garantit de toujours pouvoir jouer (§5).</li>
|
||||
<li><strong>Milieu</strong> : manœuvre pour construire des menaces sur la licorne
|
||||
adverse tout en contrôlant le liseré imposé ; recherche de zugzwang partiel.</li>
|
||||
<li><strong>Fin</strong> : dès qu'une capture est à portée, le calcul tactique
|
||||
(recherche profonde) prime.</li>
|
||||
<li><strong>Ouverture (placement)</strong> : c'est irréversible, donc on sécurise
|
||||
d'emblée la licorne et on s'arrange pour pouvoir toujours jouer (§5).</li>
|
||||
<li><strong>Milieu</strong> : on manœuvre pour menacer la licorne adverse tout en
|
||||
gardant la main sur le liseré qu'on impose, en visant le zugzwang partiel.</li>
|
||||
<li><strong>Finale</strong> : dès qu'une capture est en vue, c'est le calcul
|
||||
tactique qui décide.</li>
|
||||
</ul>
|
||||
|
||||
<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
|
||||
théoriquement s'étirer. En bornant le branchement par tour et en comptant quelques
|
||||
dizaines de tours, une borne raisonnable se situe vers 400–600 demi-coups. Pour
|
||||
tenir la contrainte de temps (300 s par joueur et par partie), nous combinons
|
||||
<strong>approfondissement itératif</strong>, <strong>élagage alpha-bêta</strong> et
|
||||
un <strong>budget par coup</strong> dérivé du temps restant (§8).</p>
|
||||
<p>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).</p>
|
||||
|
||||
<!-- ====================== 3. MODELISATION (PARTIE 2) ====================== -->
|
||||
<h2>3. Modélisation : la classe <code>EscampeBoard</code></h2>
|
||||
|
||||
<p><code>EscampeBoard</code> (≈ 860 lignes) implémente l'interface fournie
|
||||
<code>Partie1</code> : <code>setFromFile</code> / <code>saveToFile</code>,
|
||||
<code>isValidMove</code>, <code>possiblesMoves</code>, <code>play</code>,
|
||||
<code>gameOver</code>. Les conventions de l'arbitre sont respectées à la lettre :</p>
|
||||
<ul>
|
||||
<li>coup régulier <code>"B1-D1"</code> ;</li>
|
||||
<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><code>EscampeBoard</code> implémente l'interface fournie <code>Partie1</code>
|
||||
(<code>setFromFile</code>/<code>saveToFile</code>, <code>isValidMove</code>,
|
||||
<code>possiblesMoves</code>, <code>play</code>, <code>gameOver</code>) et suit les
|
||||
conventions de l'arbitre : coup régulier <code>"B1-D1"</code>, placement
|
||||
<code>"C6/A6/B5/D5/E6/F5"</code> avec la licorne en tête puis les cinq paladins, et
|
||||
pass <code>"E"</code>.</p>
|
||||
|
||||
<p><strong>Format de fichier.</strong> Six lignes de plateau (bas vers haut),
|
||||
caractères <code>N/n</code> (licorne/paladin noir), <code>B/b</code> (blanc),
|
||||
<code>-</code> (vide), chaque ligne encadrée d'un numéro ; toute autre ligne
|
||||
commence par <code>%</code> (commentaire). Nous y ajoutons en commentaires l'état
|
||||
hors-plateau (liseré courant, joueur, bord de Noir) afin que la sauvegarde soit
|
||||
fidèlement rechargeable.</p>
|
||||
<p>Le format de fichier reprend celui de l'énoncé : six lignes de plateau du bas vers
|
||||
le haut, avec <code>N/n</code> pour le noir, <code>B/b</code> pour le blanc et
|
||||
<code>-</code> pour le vide, chaque ligne encadrée de son numéro ; les lignes en
|
||||
<code>%</code> 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.</p>
|
||||
|
||||
<p><strong>Génération des coups.</strong> Depuis une case, on énumère les
|
||||
destinations par un parcours en profondeur (DFS) avec retour arrière : exactement
|
||||
N pas (N = liseré de départ), cases intermédiaires vides, dernière case vide ou
|
||||
occupée par la licorne adverse (capture). <code>possiblesMoves</code> filtre les
|
||||
pièces sur le bon liseré et renvoie <code>["E"]</code> si aucun coup n'est possible.
|
||||
Une méthode <code>main</code> illustre placements, contrainte de liseré, pass,
|
||||
round-trip fichier et capture.</p>
|
||||
<p>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. <code>possiblesMoves</code> ne garde
|
||||
que les pièces sur le bon liseré et renvoie <code>["E"]</code> quand plus rien n'est
|
||||
jouable. Une méthode <code>main</code> fait la démonstration sur des exemples :
|
||||
placements, contrainte de liseré, pass, aller-retour fichier et capture.</p>
|
||||
|
||||
<p class="note">Bug latent corrigé en partie 3 : un placement légal mais
|
||||
disposé sur une <em>seule</em> ligne faisait planter le calcul du bord de Noir
|
||||
(il supposait deux lignes distinctes). Le bord est désormais déduit de façon
|
||||
robuste à partir de la ligne de la licorne.</p>
|
||||
<p class="note">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.</p>
|
||||
|
||||
<!-- ====================== 4. PROTOCOLE ====================== -->
|
||||
<h2>4. Intégration au tournoi : le protocole de l'arbitre</h2>
|
||||
|
||||
<p>Le joueur <code>escampe.JoueurPuyaubreauRussac</code> implémente l'interface
|
||||
fournie <code>IJoueur</code> et enveloppe un <code>EscampeBoard</code> tenu à jour
|
||||
à chaque coup (le nôtre comme celui de l'adversaire, via <code>mouvementEnnemi</code>).
|
||||
Trois points d'adaptation, dont deux <strong>vérifiés par analyse du jar de
|
||||
l'arbitre</strong> car l'infrastructure fournie est obfusquée :</p>
|
||||
fournie <code>IJoueur</code> et garde à jour un <code>EscampeBoard</code> à chaque
|
||||
coup, le nôtre comme celui de l'adversaire reçu par <code>mouvementEnnemi</code>.
|
||||
Trois détails ont demandé une adaptation, et deux d'entre eux ont dû être
|
||||
<strong>confirmés en regardant dans le jar de l'arbitre</strong>, qui est obfusqué :</p>
|
||||
<ul>
|
||||
<li><strong>Couleurs.</strong> <code>IJoueur</code> parle en entiers
|
||||
(<code>NOIR = 1</code>, <code>BLANC = -1</code>) ;
|
||||
<code>EscampeBoard</code> en chaînes <code>"noir"</code>/<code>"blanc"</code>.</li>
|
||||
<li><strong>Pass = <code>"E"</code>, et non <code>"PASSE"</code>.</strong> Le
|
||||
Javadoc d'<code>IJoueur</code> indique <code>"PASSE"</code>, mais la classe de
|
||||
jeu du serveur teste <code>move.equals("E")</code> (et <code>"PASSE"</code>
|
||||
n'apparaît nulle part dans le jar). Envoyer <code>"PASSE"</code> aurait valu une
|
||||
défaite sur coup illégal.</li>
|
||||
<li><strong>Carte des liserés</strong> identique à celle du serveur (cf. Q1).</li>
|
||||
<li><strong>Les couleurs</strong> : <code>IJoueur</code> raisonne en entiers
|
||||
(<code>NOIR = 1</code>, <code>BLANC = -1</code>) alors que
|
||||
<code>EscampeBoard</code> utilise les chaînes <code>"noir"</code> et <code>"blanc"</code>.</li>
|
||||
<li><strong>Le pass se note <code>"E"</code>, pas <code>"PASSE"</code></strong> :
|
||||
le Javadoc d'<code>IJoueur</code> annonce <code>"PASSE"</code>, mais le serveur teste
|
||||
bel et bien <code>move.equals("E")</code>, et <code>"PASSE"</code> n'apparaît nulle
|
||||
part dans le jar. Suivre le Javadoc nous aurait coûté la partie sur coup illégal.</li>
|
||||
<li><strong>La carte des liserés</strong> doit être celle du serveur (cf. Q1).</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Machine à états.</strong> Le placement et les coups transitent par le
|
||||
même canal <code>choixMouvement</code>/<code>mouvementEnnemi</code>. Le premier
|
||||
appel à <code>choixMouvement</code> renvoie donc un <em>placement</em>, les suivants
|
||||
des coups ; la phase est détectée via <code>blackPlaced</code>/<code>whitePlaced</code>.
|
||||
La séquence (déduite de la classe <code>Solo</code> fournie) est :</p>
|
||||
<p>Placement et coups passent par le même canal
|
||||
(<code>choixMouvement</code>/<code>mouvementEnnemi</code>) : le premier
|
||||
<code>choixMouvement</code> renvoie un placement, les suivants des coups, la phase
|
||||
se lisant sur <code>blackPlaced</code>/<code>whitePlaced</code>. En lisant la classe
|
||||
<code>Solo</code> fournie, on reconstitue l'ordre des appels :</p>
|
||||
<pre class="grid">Noir : choixMouvement(placement) → mvtEnnemi(placement Blanc)
|
||||
→ mvtEnnemi(1er coup Blanc) → choixMouvement(coup) → ...
|
||||
Blanc : mvtEnnemi(placement Noir) → choixMouvement(placement)
|
||||
→ choixMouvement(1er coup, Blanc rejoue) → mvtEnnemi(coup Noir) → ...</pre>
|
||||
<p>En appliquant chaque coup à l'<code>EscampeBoard</code> interne dans cet ordre,
|
||||
le joueur au trait reste naturellement synchronisé avec l'arbitre.</p>
|
||||
<p>Comme on rejoue chaque coup sur l'<code>EscampeBoard</code> interne dans cet ordre,
|
||||
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
|
||||
java -cp Puyaubreau_Russac.jar escampe.ClientJeu escampe.JoueurPuyaubreauRussac localhost 1234
|
||||
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 ====================== -->
|
||||
<h2>5. Placement d'ouverture</h2>
|
||||
|
||||
<p>Le placement est irréversible : nous l'avons conçu à partir d'un constat issu de
|
||||
l'auto-jeu — une licorne mal placée peut se retrouver <em>seule pièce jouable et
|
||||
bloquée</em> sur le liseré imposé, forçant des passes successifs qui livrent
|
||||
l'initiative à l'adversaire. Trois principes y répondent :</p>
|
||||
<p>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 :</p>
|
||||
<ol>
|
||||
<li><strong>Licorne dans un coin.</strong> Un coin n'a que deux cases voisines :
|
||||
seulement deux cases d'où l'adversaire peut l'atteindre.</li>
|
||||
<li><strong>Murs.</strong> On occupe ces deux voisines par des paladins. La
|
||||
licorne devient <em>incapturable</em> tant que les murs tiennent (impossible de
|
||||
franchir le dernier pas sur une case occupée).</li>
|
||||
<li><strong>Couverture des liserés.</strong> Les trois paladins restants sont
|
||||
placés sur des cases de liserés <strong>1, 2 et 3 distincts</strong> : quel que
|
||||
soit le liseré imposé, on dispose toujours d'une pièce mobile — jamais de pass
|
||||
forcé, jamais besoin de déplacer un mur ou la licorne.</li>
|
||||
<li><strong>La licorne dans un coin.</strong> Un coin n'a que deux voisines, donc
|
||||
seulement deux cases d'où l'adversaire peut venir la prendre.</li>
|
||||
<li><strong>Deux murs.</strong> 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.</li>
|
||||
<li><strong>Trois liserés couverts.</strong> 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.</li>
|
||||
</ol>
|
||||
|
||||
<p>Dispositions retenues (légalité et propriétés vérifiées) ; pour Blanc, on joue
|
||||
le bord complémentaire de celui de Noir :</p>
|
||||
<p>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 :</p>
|
||||
<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
|
||||
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,
|
||||
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 ====================== -->
|
||||
<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
|
||||
<code>Moteur</code>). La recherche s'effectue sur une <em>copie</em> du plateau,
|
||||
jamais sur l'état réel. Capturer la licorne adverse est traité comme un nœud
|
||||
terminal de valeur <code>WIN - ply</code> (gagner vite plutôt que tard).</p>
|
||||
<code>Moteur</code>). La recherche travaille sur une copie du plateau, jamais sur
|
||||
l'état réel. Une capture de licorne compte comme une feuille de valeur
|
||||
<code>WIN - 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>
|
||||
<li><strong>Coups encodés en entier</strong> (case = <code>ligne×6+colonne</code>,
|
||||
coup = <code>départ×36+arrivée</code>) : aucune chaîne manipulée dans la boucle
|
||||
chaude.</li>
|
||||
<li><strong>DFS sur masque de bits <code>long</code></strong> : les 36 cases
|
||||
tiennent dans un <code>long</code> ; les ensembles « visité » et « atteignable »
|
||||
sont des masques — pas d'allocation de tableau par appel.</li>
|
||||
<li><strong><code>make</code>/<code>unmake</code> sans allocation</strong> : un
|
||||
petit jeton d'annulation suffit à défaire un coup, ce qui permet d'explorer des
|
||||
millions de nœuds sans pression sur le ramasse-miettes.</li>
|
||||
<li><strong>Buffers de coups pré-alloués</strong>, un par profondeur.</li>
|
||||
<li><strong>Ordonnancement</strong> : tout coup capturant la licorne est essayé
|
||||
en premier (coupure immédiate) ; le meilleur coup d'une itération est replacé en
|
||||
tête à l'itération suivante.</li>
|
||||
<li><strong>Coups codés sur un entier</strong> (case = <code>ligne×6+colonne</code>,
|
||||
coup = <code>départ×36+arrivée</code>), pour ne manipuler aucune chaîne dans la
|
||||
boucle chaude.</li>
|
||||
<li><strong>DFS sur masque de bits</strong> : les 36 cases tiennent dans un
|
||||
<code>long</code>, et les ensembles « visité » et « atteignable » sont de simples
|
||||
masques, sans tableau alloué à chaque appel.</li>
|
||||
<li><strong><code>make</code>/<code>unmake</code> sans allocation</strong> : un petit
|
||||
jeton suffit à défaire un coup, donc on explore des millions de nœuds sans solliciter
|
||||
le ramasse-miettes.</li>
|
||||
<li><strong>Buffers de coups réservés</strong> à l'avance, un par profondeur.</li>
|
||||
<li><strong>Ordre des coups</strong> : 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.</li>
|
||||
</ul>
|
||||
|
||||
<p class="note">Cohérence des deux chemins. Le chemin « entier » du moteur double
|
||||
le chemin « chaîne » vérifié de <code>EscampeBoard</code>. Pour exclure toute
|
||||
divergence silencieuse entre ces deux implémentations des règles, un test croisé
|
||||
(<code>VerifMoves</code>, §9) vérifie qu'ils produisent exactement les mêmes coups
|
||||
et les mêmes états — c'est la garantie qu'optimiser n'a pas changé les règles.</p>
|
||||
<p class="note">Le moteur a sa propre génération de coups en entiers, en parallèle de
|
||||
celle, vérifiée, d'<code>EscampeBoard</code> en chaînes. Pour être sûr qu'elles ne
|
||||
divergent pas en silence, le test <code>VerifMoves</code> (§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.</p>
|
||||
|
||||
<p><strong>Performance mesurée.</strong> Environ <strong>4 à 5 millions de
|
||||
nœuds par seconde</strong>. En milieu de partie, l'approfondissement itératif
|
||||
atteint une profondeur de <strong>12 à 15 demi-coups</strong> en 6 s (davantage
|
||||
dans les positions étroites). Les annonces de gain forcé du moteur se matérialisent
|
||||
bien par une capture effective lors des parties de contrôle.</p>
|
||||
<p>En pratique, le moteur explore de l'ordre de <strong>4 à 5 millions de nœuds
|
||||
par seconde</strong>. En milieu de partie, l'approfondissement itératif atteint
|
||||
<strong>12 à 15 demi-coups</strong> 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.</p>
|
||||
|
||||
<!-- ====================== 7. HEURISTIQUE ====================== -->
|
||||
<h2>7. Heuristique d'évaluation</h2>
|
||||
|
||||
<p>Le matériel étant constant (paladins imprenables, licornes présentes jusqu'à la
|
||||
capture), l'évaluation d'une position non terminale est purement <em>positionnelle</em>,
|
||||
exprimée du point de vue du joueur au trait. Elle somme, à partir des distances de
|
||||
Manhattan :</p>
|
||||
<p>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 :</p>
|
||||
<ul>
|
||||
<li><strong>Pression d'attaque</strong> : proximité de nos paladins à la licorne
|
||||
adverse — un terme de <em>somme</em> (pression globale) et un terme de
|
||||
<em>minimum</em> (l'attaquant le plus proche pèse davantage) ;</li>
|
||||
<li><strong>Sécurité</strong> : éloignement des paladins adverses de notre
|
||||
licorne — mêmes deux termes, de signe opposé.</li>
|
||||
<li>la <strong>pression d'attaque</strong>, 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) ;</li>
|
||||
<li>la <strong>sécurité</strong>, soit l'éloignement des paladins adverses de notre
|
||||
licorne, avec les deux mêmes termes mais de signe opposé.</li>
|
||||
</ul>
|
||||
<p>Concrètement, avec les poids retenus (somme = 2, minimum = 8) :</p>
|
||||
<p>Avec les poids retenus (2 pour les sommes, 8 pour les minimums) :</p>
|
||||
<pre class="grid">eval = 2·Σ(10−d_attaque) − 2·Σ(10−d_défense)
|
||||
+ 8·(10−min d_attaque) − 8·(10−min d_défense)</pre>
|
||||
|
||||
<p><strong>Heuristiques testées et choix.</strong> Le réglage s'est fait par
|
||||
auto-jeu déterministe et matchs arbitrés contre le joueur aléatoire fourni. Nous
|
||||
avons comparé : (a) <em>somme seule</em> — jeu trop diffus, le moteur tarde à
|
||||
concentrer une menace ; (b) <em>somme + minimum</em> (retenue) — le terme minimum,
|
||||
fortement pondéré, oriente nettement les paladins vers la licorne adverse et
|
||||
améliore le taux de capture ; (c) ajout d'un terme défensif symétrique — conservé,
|
||||
il évite d'exposer notre licorne sans nuire à l'attaque. Le fort poids du terme
|
||||
minimum reflète que, dans ce jeu, c'est l'attaquant <em>le plus avancé</em> qui
|
||||
décide d'une prise.</p>
|
||||
<p>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 (a), le jeu
|
||||
restait trop diffus et le moteur tardait à concentrer une menace. La somme plus le
|
||||
mininum (b), 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 (c) 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.</p>
|
||||
|
||||
<p class="note">Limite assumée. Faute d'adversaires IA tiers disponibles avant le
|
||||
tournoi, ces poids sont validés contre l'aléatoire et en auto-jeu, non contre
|
||||
d'autres joueurs forts. Les tactiques de capture à court terme sont, elles,
|
||||
gérées par la recherche, ce qui rend le joueur robuste même avec une évaluation
|
||||
positionnelle simple.</p>
|
||||
<p class="note">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.</p>
|
||||
|
||||
<!-- ====================== 8. TEMPS ====================== -->
|
||||
<h2>8. Gestion du temps réel</h2>
|
||||
|
||||
<p>La limite de l'arbitre est de 300 s par joueur et par partie. Nous nous
|
||||
fixons une <strong>enveloppe interne de 280 s</strong> (≈ 20 s de marge).
|
||||
Le budget alloué à un coup est une fraction du temps restant, bornée :</p>
|
||||
<p>L'arbitre laisse 300 s par joueur et par partie. Nous travaillons sous une
|
||||
enveloppe interne de <strong>280 s</strong>, 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 :</p>
|
||||
<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
|
||||
<strong>jamais</strong> être épuisé, même sur une partie très longue. Le plafond de
|
||||
6 s évite de surinvestir en ouverture ; un plancher de 120 ms garantit un
|
||||
minimum de réflexion ; un mode « panique » sécurise les toutes dernières secondes.
|
||||
L'approfondissement itératif rend le meilleur coup déjà trouvé dès que la tranche
|
||||
expire (le temps est contrôlé toutes les 2048 explorations de nœuds).</p>
|
||||
<p>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.</p>
|
||||
|
||||
<p><strong>Mesures</strong> (auto-jeu équilibré, plein budget) : temps
|
||||
<strong>maximal par coup ≈ 6,0 s</strong> (le plafond), <strong>cumul maximal
|
||||
≈ 36 s</strong> par joueur sur une partie complète — très loin des 300 s.
|
||||
Le réglage est volontairement conservateur et pourrait être augmenté sans risque.</p>
|
||||
<p>En mesure (auto-jeu équilibré, plein budget), le coup le plus long approche le
|
||||
plafond, environ <strong>6 s</strong>, et le cumul sur une partie entière plafonne
|
||||
vers <strong>36 s</strong> par joueur, loin des 300 s. Le réglage est prudent
|
||||
et on pourrait l'ouvrir davantage sans risque.</p>
|
||||
|
||||
<!-- ====================== 9. PERFS & TESTS ====================== -->
|
||||
<h2>9. Performances et tests</h2>
|
||||
|
||||
<p>Notre démarche de validation est empirique et redondante : chaque maillon est
|
||||
contrôlé contre une référence indépendante.</p>
|
||||
<p>Chaque maillon de la chaîne est contrôlé contre une référence indépendante.</p>
|
||||
|
||||
<table>
|
||||
<tr><th>Test</th><th>Ce qu'il garantit</th><th>Résultat</th></tr>
|
||||
<tr>
|
||||
<td><code>VerifMoves</code></td>
|
||||
<td>Chemin entier (moteur) ≡ chemin chaîne (vérifié) : mêmes coups, même
|
||||
<code>make</code>/<code>unmake</code></td>
|
||||
<td>Génération en entiers (moteur) identique à la génération en chaînes (vérifiée) :
|
||||
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>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>RulesTest</code></td>
|
||||
<td>Règles directes : pas = liseré, capture au dernier pas, paladins
|
||||
imprenables, non-traversée, contrainte de liseré, pass forcé, fin, zones de placement</td>
|
||||
<td>Règles vérifiées directement : nombre de pas = liseré, capture au dernier pas,
|
||||
paladins imprenables, non-traversée, contrainte de liseré, pass forcé, fin, zones de placement</td>
|
||||
<td><strong>21 / 21</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -373,75 +394,74 @@ contrôlé contre une référence indépendante.</p>
|
||||
<tr>
|
||||
<td><code>Bench</code> / <code>Branching</code></td>
|
||||
<td>Vitesse, profondeur, facteur de branchement</td>
|
||||
<td>≈ 4–5 M nœuds/s ; profondeur 12–15 ; branchement max 49 / moyen ≈ 8,9</td>
|
||||
<td>≈ 4–5 M nœuds/s ; profondeur 12–15 ; branchement max 49, moyen ≈ 8,9</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p>La séparation des rôles est délibérée : <code>VerifMoves</code> prouve que le
|
||||
moteur ≡ <code>EscampeBoard</code> ; <code>RulesTest</code> prouve que
|
||||
<code>EscampeBoard</code> respecte les règles ; les parties arbitrées prouvent que
|
||||
le tout dialogue correctement avec l'arbitre réel. Aucun coup illégal n'a été
|
||||
produit sur l'ensemble des parties jouées.</p>
|
||||
<p>Les rôles ne se recouvrent pas : <code>VerifMoves</code> montre que le moteur colle
|
||||
à <code>EscampeBoard</code>, <code>RulesTest</code> que <code>EscampeBoard</code>
|
||||
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.</p>
|
||||
|
||||
<!-- ====================== 10. BUILD ====================== -->
|
||||
<h2>10. Compilation, exécution et livrables</h2>
|
||||
|
||||
<p>Le script <code>build.sh</code> produit dans <code>dist/</code> les trois
|
||||
livrables de la version finale :</p>
|
||||
<p>Le script <code>build.sh</code> fabrique dans <code>dist/</code> les trois livrables
|
||||
de la version finale :</p>
|
||||
<pre class="grid">Puyaubreau_Russac.jar jar exécutable (Main-Class : escampe.ClientJeu)
|
||||
mainClass jar:Puyaubreau_Russac.jar
|
||||
clientClass:escampe.ClientJeu
|
||||
mainClass:escampe.JoueurPuyaubreauRussac
|
||||
Puyaubreau_Russac.tgz archive de rendu : répertoire Puyaubreau_Russac/
|
||||
contenant src/escampe/*.java + mainClass + le jar</pre>
|
||||
<p>Seules les classes de production entrent dans le jar (le joueur, le moteur, le
|
||||
plateau et les classes fournies) ; les utilitaires de test (<code>VerifMoves</code>,
|
||||
<code>RulesTest</code>, <code>Bench</code>, <code>Branching</code>) en sont exclus.
|
||||
Le jeu en multijoueur (humain contre humain, ou humain contre notre IA, en local
|
||||
ou à distance) est documenté à part dans <code>MULTIJOUEUR.md</code>.</p>
|
||||
<p>Le jar ne contient que les classes de production (le joueur, le moteur, le plateau et
|
||||
les classes fournies) ; les utilitaires de test (<code>VerifMoves</code>,
|
||||
<code>RulesTest</code>, <code>Bench</code>, <code>Branching</code>) restent dehors. Le
|
||||
jeu en multijoueur, humain contre humain ou humain contre notre IA, en local comme à
|
||||
distance, est décrit dans <code>MULTIJOUEUR.md</code>.</p>
|
||||
|
||||
<!-- ====================== 11. SOURCES ====================== -->
|
||||
<h2>11. Sources et bibliographie</h2>
|
||||
<ul>
|
||||
<li><strong>Énoncé du cours</strong> (Université Paris-Saclay, Polytech APP5,
|
||||
2025-2026) : règles d'Escampe, carte des liserés (figure 4), interface
|
||||
<code>Partie1</code>, et classes d'infrastructure fournies
|
||||
<li>L'<strong>énoncé du cours</strong> (Université Paris-Saclay, Polytech APP5,
|
||||
2025-2026) pour les règles d'Escampe, la carte des liserés (figure 4),
|
||||
l'interface <code>Partie1</code> et les classes fournies
|
||||
(<code>IJoueur</code>, <code>ClientJeu</code>, <code>Solo</code>,
|
||||
<code>Applet</code>, serveur).</li>
|
||||
<li><strong>Algorithmes classiques</strong>, à titre d'inspiration et sans copie
|
||||
de code : élagage alpha-bêta (Knuth & Moore, <em>An Analysis of Alpha-Beta
|
||||
Pruning</em>, 1975) ; minimax, negamax et approfondissement itératif
|
||||
(Russell & Norvig, <em>Artificial Intelligence: A Modern Approach</em>) ;
|
||||
techniques de représentation par masques de bits et d'ordonnancement de coups
|
||||
<li>Des <strong>algorithmes classiques</strong>, comme inspiration et sans copie de
|
||||
code : l'élagage alpha-bêta (Knuth et Moore, <em>An Analysis of Alpha-Beta
|
||||
Pruning</em>, 1975), le minimax, le negamax et l'approfondissement itératif
|
||||
(Russell et Norvig, <em>Artificial Intelligence: A Modern Approach</em>), ainsi que
|
||||
les représentations par masques de bits et l'ordonnancement de coups
|
||||
(<em>Chess Programming Wiki</em>).</li>
|
||||
<li><strong>Déclaration.</strong> Aucun programme d'Escampe externe n'a été
|
||||
recopié. La seule rétro-ingénierie effectuée porte sur le jar d'arbitre
|
||||
<em>fourni avec le sujet</em>, dans le seul but de confirmer le protocole (pass
|
||||
<code>"E"</code>) et la carte des liserés — points sur lesquels la documentation
|
||||
était ambiguë.</li>
|
||||
<li>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 <code>"E"</code>) et la carte des
|
||||
liserés, deux points que la documentation laissait dans le flou.</li>
|
||||
</ul>
|
||||
|
||||
<!-- ====================== 12. CONCLUSION ====================== -->
|
||||
<h2>12. Conclusion et difficultés rencontrées</h2>
|
||||
<p>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. Les principales difficultés ont été :</p>
|
||||
<p>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 :</p>
|
||||
<ul>
|
||||
<li><strong>L'obfuscation du serveur</strong> : lever l'ambiguïté du pass
|
||||
(<code>"E"</code> vs <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>
|
||||
<li><strong>L'interface obfusquée vs nos sources</strong> : le joueur aléatoire du
|
||||
jar n'implémente pas notre <code>IJoueur</code> ; les tests contre lui passent
|
||||
donc par le réseau (seules des chaînes circulent).</li>
|
||||
<li><strong>L'avantage du trait</strong> : en miroir, Blanc (premier à jouer)
|
||||
conserve l'initiative via la contrainte de liseré — propriété du jeu, indépendante
|
||||
de la force du moteur.</li>
|
||||
<li><strong>Le réglage de l'heuristique sans adversaires</strong> : validé contre
|
||||
l'aléatoire et en auto-jeu.</li>
|
||||
<li><strong>L'obfuscation du serveur.</strong> Trancher l'ambiguïté du pass
|
||||
(<code>"E"</code> et non <code>"PASSE"</code>) et confirmer la carte des liserés a
|
||||
demandé de fouiller le jar, sans quoi on perdait sur coup illégal.</li>
|
||||
<li><strong>L'interface obfusquée face à nos sources.</strong> Le joueur aléatoire du
|
||||
jar n'implémente pas notre <code>IJoueur</code>, donc les tests contre lui passent par
|
||||
le réseau, où seules des chaînes circulent.</li>
|
||||
<li><strong>L'avantage du trait.</strong> 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.</li>
|
||||
<li><strong>Le réglage de l'heuristique sans sparring-partner</strong>, calé faute de
|
||||
mieux contre l'aléatoire et en auto-jeu.</li>
|
||||
</ul>
|
||||
<p><strong>Pistes d'amélioration</strong> : table de transposition (hachage de
|
||||
Zobrist), bibliothèque d'ouvertures de placement, terme de mobilité différentielle
|
||||
dans l'évaluation, et recherche de quiescence sur les menaces de capture.</p>
|
||||
<p>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.</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -44,10 +44,16 @@ tr:nth-child(even) td { background: #f4f6f8; }
|
||||
p.note { background: #fff8e6; border-left: 3px solid #e0a526;
|
||||
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. */
|
||||
pre.grid, table, tr { page-break-inside: avoid; }
|
||||
h2, h3 { page-break-after: avoid; }
|
||||
.cover { page-break-after: always; }
|
||||
ol.toc { page-break-after: always; }
|
||||
|
||||
/* Page de titre */
|
||||
.cover { text-align: center; padding-top: 40pt; }
|
||||
@@ -107,9 +113,9 @@ def verify():
|
||||
doc = fitz.open(OUT)
|
||||
full = "".join(p.get_text() for p in doc)
|
||||
doc.close()
|
||||
# Mots accentués présents tels quels dans report/rapport.html.
|
||||
probes = ["liseré", "Présentation", "élagage", "Modélisation",
|
||||
"stratégique", "approfondissement", "Puyaubreau"]
|
||||
# Mots accentués stables, présents dans report/rapport.html (titres de section).
|
||||
probes = ["liseré", "Présentation", "Modélisation", "Intégration",
|
||||
"Heuristique", "Puyaubreau"]
|
||||
missing = [s for s in probes if s not in full]
|
||||
return missing, len(full)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user