diff --git a/RAPPORT.md b/RAPPORT.md index 8494eea..a743d2c 100644 --- a/RAPPORT.md +++ b/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. diff --git a/dist/Puyaubreau_Russac.jar b/dist/Puyaubreau_Russac.jar index ecce872..b7e98fc 100644 Binary files a/dist/Puyaubreau_Russac.jar and b/dist/Puyaubreau_Russac.jar differ diff --git a/dist/Puyaubreau_Russac.tgz b/dist/Puyaubreau_Russac.tgz index 8fcef60..94cc359 100644 Binary files a/dist/Puyaubreau_Russac.tgz and b/dist/Puyaubreau_Russac.tgz differ diff --git a/dist/Puyaubreau_Russac/Puyaubreau_Russac.jar b/dist/Puyaubreau_Russac/Puyaubreau_Russac.jar index ecce872..b7e98fc 100644 Binary files a/dist/Puyaubreau_Russac/Puyaubreau_Russac.jar and b/dist/Puyaubreau_Russac/Puyaubreau_Russac.jar differ diff --git a/dist/Puyaubreau_Russac_rapport.pdf b/dist/Puyaubreau_Russac_rapport.pdf index 7b5a9f7..ed58e85 100644 Binary files a/dist/Puyaubreau_Russac_rapport.pdf and b/dist/Puyaubreau_Russac_rapport.pdf differ diff --git a/report/rapport.html b/report/rapport.html index c3ecbfa..8924251 100644 --- a/report/rapport.html +++ b/report/rapport.html @@ -23,56 +23,75 @@ + +

Sommaire

+
    +
  1. Présentation et règles
  2. +
  3. Analyse des caractéristiques du jeu (Q1 à Q7)
  4. +
  5. Modélisation : la classe EscampeBoard
  6. +
  7. Intégration au tournoi : le protocole de l'arbitre
  8. +
  9. Placement d'ouverture
  10. +
  11. Moteur de décision
  12. +
  13. Heuristique d'évaluation
  14. +
  15. Gestion du temps réel
  16. +
  17. Performances et tests
  18. +
  19. Compilation, exécution et livrables
  20. +
  21. Sources et bibliographie
  22. +
  23. Conclusion et difficultés rencontrées
  24. +
+

1. Présentation et règles

Escampe se joue sur un plateau de 36 cases (6×6). Chaque case porte un liseré simple, double ou triple. Chaque joueur dispose -d'une licorne et de cinq paladins (couleur noire -ou blanche). Les lignes sont numérotées de 1 à 6, les colonnes de A à F. Le but -est de prendre la licorne adverse.

+d'une licorne et de cinq paladins (noirs ou +blancs). Les lignes vont de 1 à 6, les colonnes de A à F, et le but est de +prendre la licorne adverse.

-

La règle caractéristique du jeu est une contrainte de liseré : -la pièce que l'on joue 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 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.

+

Ce qui fait l'originalité du jeu, c'est la contrainte de liseré : +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.

-

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é ; Blanc joue le -premier coup. 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.

+

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. 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.

2. Analyse des caractéristiques du jeu

-

Nous reprenons ici les sept questions de la première partie, en les étayant -par l'implémentation finalement réalisée.

+

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

-

Le plateau est un tableau int[6][6] : board[ligne][colonne] -avec ligne 0 = ligne 1 (bas) et colonne 0 = A. -Chaque case contient une constante de pièce (EMPTY, +

Le plateau est un tableau int[6][6] : board[ligne][colonne], +avec ligne 0 = ligne 1 (en bas) et colonne 0 = A. +Chaque case vaut une constante de pièce (EMPTY, WHITE_LICORNE, WHITE_PALADIN, BLACK_LICORNE, -BLACK_PALADIN). L'état complémentaire, indispensable à la règle, est -maintenu hors du plateau :

+BLACK_PALADIN). Quatre informations que le tableau ne porte pas, mais +dont la règle a besoin, sont gardées à côté :

-

Avantages. Accès O(1) à toute case ; copie immédiate de l'état -pour l'arbre de recherche ; sérialisation triviale ; surtout, un schéma -make/unmake sans aucune allocation (essentiel pour la vitesse, §6). -Inconvénient. La contrainte de liseré est un état séparé qu'il -faut maintenir explicitement à chaque coup ; nous l'encapsulons 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.

-

La carte des liserés est une constante TILE_MAP reproduisant la -figure 4 de l'énoncé (ligne 1 en bas) :

+

La carte des liserés est figée dans la constante TILE_MAP, recopie +de la figure 4 de l'énoncé (ligne 1 en bas) :

       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) :

3 2 3 1 2 1 3 2 3 1 3 1 3 2 1 1 2 2 3 1 2
-

Fait vérifié : cette carte est identique, 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.

+

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

-

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 -(gameOver) ; le moteur, lui, détecte la capture directement au moment -où elle est jouée (§6).

+

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 (§6).

Q3 — Sources de difficulté et facteur de branchement

-

Les principales sources de difficulté sont :

- -

Facteur de branchement. 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 escampe.Branching) :

+

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.

+

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 :

- +
SituationBranchement maximal observé
Coup contraint (un liseré imposé)45
Coup libre (1er coup ou après un pass, aucune contrainte)49
Coup libre (1er coup ou après un pass)49
Branchement moyen (toutes positions)≈ 8,9
-

Le branchement effectif modeste (moyenne < 10) explique qu'une recherche -alpha-bêta atteigne des profondeurs élevées en quelques secondes (§6).

+

Avec une moyenne sous 10, l'alpha-bêta descend profond en quelques secondes (§6).

Q4 — Coups imparables

-

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 zugzwang -partiel 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.

+

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.

+

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.

+

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

-

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 -proximité des paladins à la licorne adverse (pression d'attaque) -et l'éloignement des paladins adverses de notre licorne -(sécurité) — les autres critères sont, en pratique, largement pris en charge par -la recherche elle-même.

+

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

Q7 — Majorant du nombre de coups et gestion du temps

-

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 -approfondissement itératif, élagage alpha-bêta et -un 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 (≈ 860 lignes) implémente l'interface fournie -Partie1 : setFromFile / saveToFile, -isValidMove, possiblesMoves, play, -gameOver. Les conventions de l'arbitre sont respectées à la lettre :

- +

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 de fichier. Six lignes de plateau (bas vers haut), -caractères N/n (licorne/paladin noir), B/b (blanc), -- (vide), chaque ligne encadrée d'un numéro ; toute autre ligne -commence par % (commentaire). Nous y ajoutons en commentaires l'état -hors-plateau (liseré courant, joueur, bord de Noir) afin que la sauvegarde soit -fidèlement rechargeable.

+

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. 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). possiblesMoves filtre les -pièces sur le bon liseré et renvoie ["E"] si aucun coup n'est possible. -Une méthode main illustre placements, contrainte de liseré, pass, -round-trip fichier et 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 mais -disposé sur une seule 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.

+

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 : le protocole de l'arbitre

Le joueur escampe.JoueurPuyaubreauRussac implémente l'interface -fournie IJoueur et enveloppe un EscampeBoard tenu à jour -à chaque coup (le nôtre comme celui de l'adversaire, via mouvementEnnemi). -Trois points d'adaptation, dont deux vérifiés par analyse du jar de -l'arbitre car l'infrastructure fournie est obfusquée :

+fournie IJoueur et garde à jour un EscampeBoard à chaque +coup, le nôtre comme 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é :

-

Machine à états. Le placement et les coups transitent par le -même canal choixMouvement/mouvementEnnemi. Le premier -appel à choixMouvement renvoie donc un placement, les suivants -des coups ; la phase est détectée via blackPlaced/whitePlaced. -La séquence (déduite de la classe Solo fournie) est :

+

Placement et coups passent par le même canal +(choixMouvement/mouvementEnnemi) : 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)
         → mvtEnnemi(1er coup Blanc) → choixMouvement(coup) → ...
 Blanc : mvtEnnemi(placement Noir) → choixMouvement(placement)
         → choixMouvement(1er coup, Blanc rejoue) → mvtEnnemi(coup Noir) → ...
-

En appliquant chaque coup à l'EscampeBoard interne dans cet ordre, -le joueur au trait reste naturellement synchronisé avec l'arbitre.

+

Comme on rejoue chaque coup sur l'EscampeBoard interne dans cet ordre, +le joueur au trait reste synchronisé avec l'arbitre sans traitement particulier.

-

Exécution. Trois processus (serveur + deux clients) :

+

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
 java -cp escampeobf.jar        escampe.ClientJeu escampe.JoueurAleatoire        localhost 1234
@@ -224,24 +243,24 @@ java -cp escampeobf.jar escampe.ClientJeu escampe.JoueurAleatoire

5. Placement d'ouverture

-

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 seule pièce jouable et -bloquée sur le liseré imposé, forçant des passes successifs qui livrent -l'initiative à l'adversaire. Trois principes y répondent :

+

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 deux cases voisines : - seulement deux cases d'où l'adversaire peut l'atteindre.
  2. -
  3. Murs. On occupe ces deux voisines par des paladins. La - licorne devient incapturable tant que les murs tiennent (impossible de - franchir le dernier pas sur une case occupée).
  4. -
  5. Couverture des liserés. Les trois paladins restants sont - placés sur des cases de liserés 1, 2 et 3 distincts : 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.
  6. +
  7. La licorne dans un coin. Un coin n'a que deux voisines, donc + seulement deux cases d'où l'adversaire peut venir la prendre.
  8. +
  9. 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.
  10. +
  11. 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 (légalité et propriétés vérifiées) ; pour Blanc, on joue -le bord complémentaire de celui 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
    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 :

(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)
+

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

-

La décision repose sur un negamax avec élagage +

Le choix du coup repose sur un negamax avec élagage alpha-bêta et approfondissement itératif (classe -Moteur). La recherche s'effectue sur une copie du plateau, -jamais sur l'état réel. Capturer la licorne adverse est traité comme un nœud -terminal de valeur WIN - ply (gagner vite plutôt que tard).

+Moteur). La recherche travaille sur une copie du plateau, jamais sur +l'état réel. Une capture de licorne compte comme une feuille de valeur +WIN - ply, ce qui pousse à gagner tôt plutôt que tard.

-

Astuces de performance.

+

Plusieurs choix tirent la vitesse vers le haut :

-

Cohérence des deux chemins. Le chemin « entier » du moteur double -le chemin « chaîne » vérifié de EscampeBoard. Pour exclure toute -divergence silencieuse entre ces deux implémentations des règles, un test croisé -(VerifMoves, §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.

+

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.

-

Performance mesurée. Environ 4 à 5 millions de -nœuds par seconde. En milieu de partie, l'approfondissement itératif -atteint une profondeur de 12 à 15 demi-coups 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.

+

En pratique, le moteur explore de l'ordre de 4 à 5 millions de nœuds +par seconde. 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

-

Le matériel étant constant (paladins imprenables, licornes présentes jusqu'à la -capture), l'évaluation d'une position non terminale est purement positionnelle, -exprimée du point de vue du joueur au trait. Elle somme, à 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 :

-

Concrètement, avec les poids retenus (somme = 2, minimum = 8) :

+

Avec les poids retenus (2 pour les sommes, 8 pour les minimums) :

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. 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) somme seule — jeu trop diffus, le moteur tarde à -concentrer une menace ; (b) somme + minimum (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 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 (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.

-

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.

+

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

-

La limite de l'arbitre est de 300 s par joueur et par partie. Nous nous -fixons une enveloppe interne de 280 s (≈ 20 s de marge). -Le budget alloué à un coup est une fraction du temps restant, bornée :

+

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 : le budget ne peut -jamais ê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).

+

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.

-

Mesures (auto-jeu équilibré, plein budget) : temps -maximal par coup ≈ 6,0 s (le plafond), cumul maximal -≈ 36 s 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.

+

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

-

Notre démarche de validation est empirique et redondante : chaque maillon est -contrôlé contre une référence indépendante.

+

Chaque maillon de la chaîne est contrôlé contre une référence indépendante.

- + - + - + @@ -373,75 +394,74 @@ contrôlé contre une référence indépendante.

- +
TestCe qu'il garantitRésultat
VerifMovesChemin entier (moteur) ≡ chemin chaîne (vérifié) : mêmes coups, même - make/unmakeGénération en entiers (moteur) identique à la génération en chaînes (vérifiée) : + mêmes coups, même make/unmake 3 000 parties · 142 165 positions · 1 281 985 contrôles · 0 divergence
RulesTestRègles directes : pas = liseré, capture au dernier pas, paladins - imprenables, non-traversée, contrainte de liseré, pass forcé, fin, zones de placementRè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 21 / 21
Matchs arbitrés vs JoueurAleatoireProtocole de bout en bout (placement, liseré, pass, couleurs), légalitéProtocole de bout en bout (placement, liseré, pass, couleurs) et légalité 7 / 7 victoires, 0 coup illégal, 0 exception (les deux couleurs)
Bench / Branching Vitesse, profondeur, facteur de branchement≈ 4–5 M nœuds/s ; profondeur 12–15 ; branchement max 49 / moyen ≈ 8,9≈ 4–5 M nœuds/s ; profondeur 12–15 ; branchement max 49, moyen ≈ 8,9
-

La séparation des rôles est délibérée : VerifMoves prouve que le -moteur ≡ EscampeBoard ; RulesTest prouve que -EscampeBoard 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.

+

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

-

Le script build.sh produit dans dist/ les trois -livrables de la version finale :

+

Le script build.sh fabrique dans dist/ les trois livrables +de la version finale :

Puyaubreau_Russac.jar    jar exécutable (Main-Class : escampe.ClientJeu)
 mainClass                jar:Puyaubreau_Russac.jar
                          clientClass:escampe.ClientJeu
                          mainClass:escampe.JoueurPuyaubreauRussac
 Puyaubreau_Russac.tgz    archive de rendu : répertoire Puyaubreau_Russac/
                          contenant src/escampe/*.java + mainClass + le jar
-

Seules les classes de production entrent dans le jar (le joueur, le moteur, le -plateau et les classes fournies) ; les utilitaires de test (VerifMoves, -RulesTest, Bench, Branching) en sont exclus. -Le jeu en multijoueur (humain contre humain, ou humain contre notre IA, en local -ou à distance) est documenté à part dans MULTIJOUEUR.md.

+

Le jar ne contient que les classes de production (le joueur, le moteur, le plateau et +les classes fournies) ; les utilitaires de test (VerifMoves, +RulesTest, Bench, Branching) restent dehors. Le +jeu en multijoueur, humain contre humain ou humain contre notre IA, en local comme à +distance, est décrit dans MULTIJOUEUR.md.

11. Sources et bibliographie

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. Les principales difficultés ont été :

+

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 :

-

Pistes d'amélioration : 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.

+

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.

diff --git a/tools/make_report_pdf.py b/tools/make_report_pdf.py index 3effa53..4d470a1 100644 --- a/tools/make_report_pdf.py +++ b/tools/make_report_pdf.py @@ -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)