Joueur IA Escampe (Puyaubreau/Russac) — version finale

Joueur alpha-bêta + iterative deepening pour le tournoi APP5 « IA et contraintes ».

- src/escampe/ : joueur (IJoueur), moteur (alpha-bêta + DFS bitmask, make/unmake
  sans allocation), modèle EscampeBoard (Partie1), utilitaires de test.
- Protocole arbitre vérifié (pass="E", carte des liserés identique au serveur,
  machine à états placement/jeu) ; 7/7 victoires vs joueur aléatoire, 0 illégal.
- Vérifications : VerifMoves (int≡String, 0 divergence/142k positions),
  RulesTest (21/21), Branching (facteur de branchement mesuré).
- Rapport : report/rapport.html + tools/make_report_pdf.py (PyMuPDF) → PDF, RAPPORT.md.
- Livrables buildés inclus (dist/ : jar, mainClass, tgz, rapport PDF) + lib/escampeobf.jar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 16:00:29 +02:00
commit e508efa14f
50 changed files with 6521 additions and 0 deletions

17
.gitignore vendored Normal file
View File

@@ -0,0 +1,17 @@
# Artefacts de compilation (régénérables)
out/
build/
**/*.class
# Logs de tests
scripts/logs/
# Brouillons / notes hors-sujet
tempo.md
# OS / éditeurs
.DS_Store
Thumbs.db
*.swp
# NB : dist/ est VOLONTAIREMENT versionné (livrables buildés demandés).

93
MULTIJOUEUR.md Normal file
View File

@@ -0,0 +1,93 @@
# Jouer à Escampe en multijoueur
Le jeu est **réseau** : un **serveur** (l'arbitre) + **deux clients** qui s'y
connectent. Chaque client charge soit un humain (`escampe.JoueurHumain`, en
console), soit une IA (`escampe.JoueurPuyaubreauRussac`, la nôtre, ou
`escampe.JoueurAleatoire`). Les clients peuvent être sur la **même machine** ou
sur **deux machines différentes** : seules des chaînes de caractères circulent.
Prérequis sur chaque machine : **Java** et le fichier **`escampeobf.jar`**
(le serveur + les joueurs de référence). Pour jouer contre notre IA, il faut
aussi **`Puyaubreau_Russac.jar`** (produit par `build.sh`, dans `dist/`).
---
## 1. Sur le même PC — le plus simple
Double-cliquez sur :
- **`jouer-vs-pote.bat`** → deux humains (3 fenêtres : serveur + 2 joueurs).
- **`jouer-vs-IA.bat`** → vous (humain) contre notre IA.
(Ou, à la main, dans 3 terminaux PowerShell :)
```powershell
java -cp escampeobf.jar escampe.ServeurJeu 1234 1
java -cp escampeobf.jar escampe.ClientJeu escampe.JoueurHumain localhost 1234
java -cp escampeobf.jar escampe.ClientJeu escampe.JoueurHumain localhost 1234
```
---
## 2. À distance, avec un pote (deux PC sur le même réseau / Wi-Fi)
**Vous (l'hôte)** lancez le serveur et trouvez votre adresse IP locale :
```powershell
java -cp escampeobf.jar escampe.ServeurJeu 1234 1
ipconfig # repérez « Adresse IPv4 », ex. 192.168.1.42
```
Puis lancez votre propre client (sur l'hôte, `localhost` suffit) :
```powershell
java -cp escampeobf.jar escampe.ClientJeu escampe.JoueurHumain localhost 1234
```
**Votre pote**, sur son PC, se connecte à **votre IP** (remplacez `localhost`) :
```powershell
java -cp escampeobf.jar escampe.ClientJeu escampe.JoueurHumain 192.168.1.42 1234
```
Notes :
- Le **pare-feu Windows** de l'hôte doit autoriser Java sur le port 1234 (une
fenêtre de demande apparaît au 1ᵉʳ lancement — cliquez « Autoriser »).
- Il faut être sur le **même réseau local** (même box/Wi-Fi).
- **Par Internet** (réseaux différents) : il faut une redirection de port sur la
box de l'hôte (port 1234 → IP de l'hôte) **ou** un VPN type Tailscale/Hamachi
(plus simple et sûr). Sinon le pote ne peut pas atteindre votre machine.
---
## 3. Comment on joue (client console `JoueurHumain`)
À votre tour, le client affiche le plateau et vous demande de taper :
- au **placement** : le bord `H`/`B` (Noir choisit ; Blanc est forcé au bord
opposé), puis la case de la **licorne**, puis les **5 paladins** (ex. `A1`,
`B2`, …) ;
- en **jeu** : la case de **départ** puis la case d'**arrivée** (ex. `C2`, `D2`).
Rappel des règles : la pièce doit partir d'une case du **même liseré** que la
case où l'adversaire vient d'arriver, et avance d'autant de cases que le liseré
(1, 2 ou 3), sans traverser ni revenir sur une case. On gagne en se posant sur
la **licorne** adverse. Si vous ne pouvez rien jouer, le tour est passé
automatiquement.
> Le serveur ouvre aussi une **fenêtre graphique** du plateau (attention : d'après
> l'énoncé, le tout dernier coup n'y est pas affiché). Le client humain en console
> reste un peu rustique, mais fonctionne.
---
## 4. Variantes utiles
```powershell
# Vous (humain) contre l'IA :
java -cp Puyaubreau_Russac.jar escampe.ClientJeu escampe.JoueurPuyaubreauRussac localhost 1234
java -cp escampeobf.jar escampe.ClientJeu escampe.JoueurHumain localhost 1234
# Joueur aléatoire de référence (pour tester) :
java -cp escampeobf.jar escampe.ClientJeu escampe.JoueurAleatoire localhost 1234
```

BIN
Puyaubreau_Russac.pdf Normal file

Binary file not shown.

BIN
Puyaubreau_Russac.tgz Normal file

Binary file not shown.

331
RAPPORT.md Normal file
View File

@@ -0,0 +1,331 @@
# Escampe — Rapport (version finale)
**Université Paris-Saclay — Polytech APP5 — Année 2025-2026 — « IA et contraintes »**
**Binôme : Ethan Puyaubreau & Antonin Russac — 30 mai 2026**
Joueur : `escampe.JoueurPuyaubreauRussac`
> Ce fichier est le miroir Markdown du rapport. La version PDF mise en page
> (`dist/Puyaubreau_Russac_rapport.pdf`) est générée depuis `report/rapport.html`
> par `python tools/make_report_pdf.py` (PyMuPDF, sans dépendance externe).
---
## 1. Présentation et règles
Escampe se joue sur un plateau de 36 cases (6×6). Chaque case porte un liseré
*simple*, *double* ou *triple*. Chaque joueur dispose d'une **licorne** et de cinq
**paladins** (noirs ou blancs). Lignes numérotées de 1 à 6, colonnes de A à F. Le
but est de **prendre la licorne adverse**.
Règle caractéristique — la **contrainte de liseré** : la pièce jouée doit partir
d'une case dont le liseré est *identique* à celui de la case d'arrivée du coup
adverse précédent. Le liseré de départ fixe le nombre de pas (1, 2 ou 3),
orthogonaux, sans traverser ni revisiter de case. On ne capture qu'en se posant,
au dernier pas, sur la licorne adverse (paladins imprenables). Sans coup possible,
on passe son tour.
Déroulement : Noir place ses six pièces sur les deux lignes d'un bord ; Blanc sur
le bord opposé ; **Blanc joue le premier coup**.
---
## 2. Analyse des caractéristiques du jeu (Q1Q7)
### Q1 — Modélisation d'un état
Plateau `int[6][6]` (`board[ligne][colonne]`, ligne 0 = ligne 1 en bas, colonne
0 = A). Chaque case : `EMPTY`, `WHITE_LICORNE`, `WHITE_PALADIN`, `BLACK_LICORNE`,
`BLACK_PALADIN`. État hors-plateau : `lastTileType` (liseré imposé, `-1` = libre),
`currentPlayer`, `blackPlaced`/`whitePlaced`, `blackRows` (bord de Noir).
- **Avantages** : accès O(1), copie immédiate pour l'arbre de recherche,
sérialisation triviale, et surtout `make/unmake` sans allocation (clé de la
vitesse, §6).
- **Inconvénient** : la contrainte de liseré est un état séparé à maintenir
(encapsulé dans `play`).
Carte des liserés `TILE_MAP` (figure 4, ligne 1 en bas) :
```
A B C D E F
6 3 2 2 1 3 2
5 1 3 1 3 1 2
4 2 1 3 2 3 1
3 2 3 1 2 1 3
2 3 1 3 1 3 2
1 1 2 2 3 1 2
```
> **Vérifié** : cette carte est identique, case pour case, à celle utilisée en
> interne par l'arbitre (extraite par réflexion de la classe de jeu du serveur),
> et cohérente avec l'exemple tactique de la figure 6. Point critique : une carte
> divergente aurait produit des coups jugés illégaux.
### Q2 — Détection de fin de partie
Partie finie dès qu'une licorne disparaît (seul cas, pas de nul). Balayage O(1)
(`gameOver`) ; le moteur détecte la capture au moment où elle est jouée.
### Q3 — Sources de difficulté et facteur de branchement
Difficultés : contrainte de liseré (mobilité variable), dépendance entre tours
(la case d'arrivée détermine les options adverses), asymétrie du plateau, risque
de blocage / pass forcé.
**Facteur de branchement.** Borne théorique lâche estimée en partie 1 : ~120. La
mesure réelle (utilitaire `escampe.Branching`, 30 000 parties aléatoires) est bien
plus basse car la contrainte de liseré ne laisse jouables que les pièces du bon
liseré :
| Situation | Branchement max observé |
|---|---|
| Coup contraint (un liseré imposé) | **45** |
| Coup libre (1er coup ou après pass) | **49** |
| Branchement moyen (toutes positions) | **≈ 8,9** |
Le branchement effectif modeste explique les profondeurs élevées atteintes par
l'alpha-bêta (§6).
### Q4 — Coups imparables
Pas d'« imparable » universel garanti dès le départ (la contrainte de liseré peut
toujours bloquer une menace). Mais des configurations créent un **zugzwang
partiel** (exemple figure 6 : C2 prend C1 dès que Noir est forcé d'imposer un
liseré double). Notre recherche les exploite quand ils sont à portée d'horizon.
### Q5 — Critères pour l'heuristique
Cinq critères identifiés : distance à la licorne adverse, mobilité différentielle,
contrôle du liseré imposé, protection de sa licorne, avancée. Retenu en pratique
(§7) : proximité des paladins à la licorne adverse (attaque) et éloignement des
paladins adverses de notre licorne (défense) — le reste est largement pris en
charge par la recherche.
### Q6 — Stratégie selon la phase
- **Début (placement)** : irréversible ; protéger la licorne, garantir de toujours
pouvoir jouer (§5).
- **Milieu** : manœuvrer pour menacer la licorne adverse en contrôlant le liseré
imposé ; chercher le zugzwang partiel.
- **Fin** : dès qu'une capture est à portée, le calcul tactique prime.
### Q7 — Majorant du nombre de coups et gestion du temps
Aucune pièce ne disparaît avant la capture finale : borne raisonnable ~400600
demi-coups. Pour tenir les 300 s/joueur : approfondissement itératif, alpha-bêta,
budget par coup dérivé du temps restant (§8).
---
## 3. Modélisation : la classe `EscampeBoard`
`EscampeBoard` (~860 lignes) implémente `Partie1` (`setFromFile`/`saveToFile`,
`isValidMove`, `possiblesMoves`, `play`, `gameOver`). Conventions de l'arbitre :
coup `"B1-D1"`, placement `"C6/A6/B5/D5/E6/F5"` (licorne en tête), pass `"E"`.
**Format fichier** : 6 lignes de plateau (bas→haut), `N/n` `B/b` `-`, encadrées
d'un numéro ; lignes `%` = commentaires (où l'on stocke l'état hors-plateau pour
un rechargement fidèle).
**Génération des coups** : DFS avec retour arrière (exactement N pas, intermédiaires
vides, dernière case vide ou licorne adverse). `possiblesMoves` filtre le bon
liseré et renvoie `["E"]` si bloqué. Une méthode `main` illustre placements,
liseré, pass, round-trip fichier, capture.
> Bug latent corrigé en partie 3 : un placement légal sur une seule ligne faisait
> planter le calcul du bord de Noir (supposait deux lignes). Le bord est désormais
> déduit de la ligne de la licorne.
---
## 4. Intégration au tournoi : protocole de l'arbitre
`JoueurPuyaubreauRussac implements IJoueur` enveloppe un `EscampeBoard` tenu à jour
à chaque coup (le nôtre via `play`, l'adverse via `mouvementEnnemi`). Trois
adaptations, dont deux **vérifiées par analyse du jar obfusqué** :
- **Couleurs** : `IJoueur` en entiers (`NOIR=1`, `BLANC=-1`), `EscampeBoard` en
`"noir"`/`"blanc"`.
- **Pass = `"E"`, pas `"PASSE"`** : le Javadoc d'`IJoueur` dit `"PASSE"`, mais la
classe serveur teste `move.equals("E")` (et `"PASSE"` est absent du jar).
Envoyer `"PASSE"` = défaite sur coup illégal.
- **Carte des liserés** identique au serveur (cf. Q1).
**Machine à états** : placement et coups passent par le même canal. Premier
`choixMouvement` = placement, suivants = coups ; phase détectée via
`blackPlaced`/`whitePlaced`. Séquence (déduite de `Solo`) :
```
Noir : choixMouvement(placement) -> mvtEnnemi(placement Blanc)
-> mvtEnnemi(1er coup Blanc) -> choixMouvement(coup) -> ...
Blanc : mvtEnnemi(placement Noir) -> choixMouvement(placement)
-> choixMouvement(1er coup, Blanc rejoue) -> mvtEnnemi(coup Noir) -> ...
```
**Exécution** (3 processus) :
```
java -cp escampeobf.jar escampe.ServeurJeu 1234 1
java -cp Puyaubreau_Russac.jar escampe.ClientJeu escampe.JoueurPuyaubreauRussac localhost 1234
java -cp escampeobf.jar escampe.ClientJeu escampe.JoueurAleatoire localhost 1234
```
---
## 5. Placement d'ouverture
Constat issu de l'auto-jeu : une licorne mal placée peut se retrouver seule pièce
jouable et bloquée sur le liseré imposé → passes successifs → perte d'initiative.
Trois principes :
1. **Licorne dans un coin** — un coin n'a que 2 voisines, donc 2 cases d'attaque.
2. **Murs** — on occupe ces 2 voisines par des paladins : licorne incapturable
tant que les murs tiennent.
3. **Couverture des liserés** — les 3 paladins restants sur des liserés 1, 2 et 3
distincts : jamais de pass forcé, jamais besoin de bouger un mur ou la licorne.
Dispositions retenues (Blanc joue le bord complémentaire de Noir) :
```
Bord bas A1/A2/B1/E1/F1/C2 Bord haut A6/A5/B6/C5/F5/E6
licorne A1, murs A2/B1, licorne A6, murs A5/B6,
mobiles E1·F1·C2 = liserés 1·2·3 mobiles C5·F5·E6 = liserés 1·2·3
```
---
## 6. Moteur de décision
Negamax + élagage alpha-bêta + approfondissement itératif (`Moteur`), sur une
**copie** du plateau. Capture de licorne = nœud terminal `WIN - ply` (gagner vite).
**Astuces de performance :**
- **Coups en entier** (case = `ligne*6+colonne`, coup = `départ*36+arrivée`) : pas
de chaîne dans la boucle chaude.
- **DFS sur masque de bits `long`** (36 cases ⊂ 64 bits) : ensembles visité/
atteignable en masques, sans allocation par appel.
- **`make`/`unmake` sans allocation** : un petit jeton d'annulation → millions de
nœuds sans pression GC.
- **Buffers de coups pré-alloués**, un par profondeur.
- **Ordonnancement** : capture de licorne essayée en premier ; meilleur coup
d'une itération replacé en tête de la suivante.
> Cohérence : le chemin « entier » du moteur double le chemin « chaîne » vérifié.
> `VerifMoves` (§9) prouve qu'ils produisent les mêmes coups et états — optimiser
> n'a pas changé les règles.
**Performance mesurée** : ~45 M nœuds/s ; profondeur 1215 demi-coups en 6 s
(plus dans les positions étroites). Les gains forcés annoncés se concrétisent par
une capture.
---
## 7. Heuristique d'évaluation
Matériel constant → évaluation purement positionnelle, du point de vue du joueur
au trait, à partir des distances de Manhattan :
- **Attaque** : proximité de nos paladins à la licorne adverse — terme *somme*
(pression globale) + terme *minimum* (l'attaquant le plus proche pèse plus) ;
- **Défense** : éloignement des paladins adverses de notre licorne — mêmes termes,
signe opposé.
Avec les poids retenus (somme = 2, minimum = 8) :
```
eval = 2·Σ(10d_attaque) 2·Σ(10d_défense)
+ 8·(10min d_attaque) 8·(10min d_défense)
```
**Heuristiques testées et choix** (réglage par auto-jeu déterministe + matchs vs
aléatoire) : (a) somme seule → jeu trop diffus ; (b) **somme + minimum (retenue)**
→ le terme minimum fortement pondéré oriente les paladins vers la licorne adverse
et améliore le taux de capture ; (c) terme défensif symétrique conservé (évite
d'exposer notre licorne). Le fort poids du minimum reflète que c'est l'attaquant
le plus avancé qui décide d'une prise.
> Limite assumée : poids validés contre l'aléatoire et en auto-jeu, faute
> d'adversaires IA tiers. Les tactiques à court terme sont gérées par la recherche,
> ce qui rend le joueur robuste malgré une évaluation simple.
---
## 8. Gestion du temps réel
Limite arbitre 300 s/joueur/partie → **enveloppe interne 280 s** (~20 s de marge).
Budget par coup :
```
tranche = clamp( temps_restant / 12 , 120 ms , 6000 ms )
```
La division par le temps restant décroît géométriquement : budget **jamais
épuisable**. Plafond 6 s (pas de surinvestissement en ouverture), plancher 120 ms,
mode « panique » pour les dernières secondes. L'approfondissement itératif rend le
meilleur coup déjà trouvé dès l'expiration de la tranche (temps contrôlé toutes les
2048 explorations de nœuds).
**Mesures** (auto-jeu équilibré, plein budget) : max ≈ 6,0 s/coup (le plafond),
cumul max ≈ 36 s/joueur sur une partie complète — très loin des 300 s. Réglage
conservateur, augmentable sans risque.
---
## 9. Performances et tests
| Test | Garantit | Résultat |
|---|---|---|
| `VerifMoves` | chemin entier ≡ chemin chaîne (coups + make/unmake) | 3 000 parties · 142 165 positions · 1 281 985 contrôles · **0 divergence** |
| `RulesTest` | règles directes (pas/liseré, capture, imprenabilité, non-traversée, pass, fin, placement) | **21 / 21** |
| Matchs arbitrés vs `JoueurAleatoire` | protocole de bout en bout, légalité | **7 / 7 victoires**, 0 illégal, 0 exception (2 couleurs) |
| Démo IA vs IA (serveur réel) | partie complète moteur vs moteur, pass | 21 coups, fin propre par capture |
| `Bench` / `Branching` | vitesse, profondeur, branchement | ~45 M nœuds/s ; prof. 1215 ; branchement max 49 / moyen ≈ 8,9 |
Séparation des rôles : `VerifMoves` (moteur ≡ `EscampeBoard`), `RulesTest`
(`EscampeBoard` ≡ règles), parties arbitrées (dialogue correct avec l'arbitre
réel). Aucun coup illégal sur l'ensemble des parties jouées.
---
## 10. Compilation, exécution et livrables
`build.sh` produit dans `dist/` les trois livrables de la version finale :
```
Puyaubreau_Russac.jar jar exécutable (Main-Class : escampe.ClientJeu)
mainClass jar:Puyaubreau_Russac.jar
clientClass:escampe.ClientJeu
mainClass:escampe.JoueurPuyaubreauRussac
Puyaubreau_Russac.tgz archive : Puyaubreau_Russac/ { src/escampe/*.java, mainClass, jar }
```
Seules les classes de production entrent dans le jar ; les utilitaires de test
(`VerifMoves`, `RulesTest`, `Bench`, `Branching`) en sont exclus. Le multijoueur
(humain vs humain, humain vs IA, local ou distant) est documenté dans
`MULTIJOUEUR.md`.
---
## 11. Sources et bibliographie
- **Énoncé du cours** (Université Paris-Saclay, Polytech APP5, 2025-2026) : règles,
carte des liserés (figure 4), interface `Partie1`, classes fournies (`IJoueur`,
`ClientJeu`, `Solo`, `Applet`, serveur).
- **Algorithmes classiques**, pour inspiration sans copie de code : alpha-bêta
(Knuth & Moore, 1975) ; minimax/negamax/approfondissement itératif (Russell &
Norvig, *AIMA*) ; masques de bits et ordonnancement de coups (*Chess Programming
Wiki*).
- **Déclaration** : aucun programme d'Escampe externe recopié. La seule
rétro-ingénierie porte sur le jar d'arbitre *fourni avec le sujet*, pour confirmer
le protocole (pass `"E"`) et la carte des liserés (documentation ambiguë).
---
## 12. Conclusion et difficultés rencontrées
Le joueur conduit une partie de façon autonome, dialogue correctement avec
l'arbitre, ne produit jamais de coup illégal et respecte très confortablement la
contrainte de temps. Difficultés principales :
- **Obfuscation du serveur** : lever l'ambiguïté du pass (`"E"` vs `"PASSE"`) et
confirmer la carte des liserés a nécessité l'analyse du jar — décisif pour ne pas
perdre sur coup illégal.
- **Interface obfusquée vs nos sources** : le joueur aléatoire du jar n'implémente
pas notre `IJoueur` ; les tests contre lui passent par le réseau.
- **Avantage du trait** : en miroir, Blanc garde l'initiative via la contrainte de
liseré — propriété du jeu.
- **Réglage de l'heuristique sans adversaires** : validé contre l'aléatoire et en
auto-jeu.
**Pistes d'amélioration** : table de transposition (Zobrist), bibliothèque
d'ouvertures de placement, terme de mobilité différentielle, recherche de
quiescence sur les menaces de capture.

58
README.md Normal file
View File

@@ -0,0 +1,58 @@
# Escampe — Joueur IA (Puyaubreau / Russac)
Joueur artificiel pour le jeu **Escampe**, devoir « IA et contraintes »
(Polytech Paris-Saclay, APP5, 2025-2026). Le joueur dialogue avec l'arbitre du
tournoi via une interface réseau et choisit ses coups par recherche **alpha-bêta
+ approfondissement itératif**.
## Démarrage rapide
```bash
bash build.sh # compile, produit le jar, mainClass, l'archive et le rapport PDF
```
Tout est produit dans `dist/`. Pour jouer ou tester :
```bash
# Une partie arbitrée : notre IA contre le joueur aléatoire fourni
bash scripts/match.sh
# Sous Windows, en local (double-clic) :
jouer-vs-IA.bat # vous (humain) contre notre IA
jouer-vs-pote.bat # deux humains
```
Le serveur de jeu et les joueurs de référence sont dans `lib/escampeobf.jar`
(fourni avec le sujet). Voir [MULTIJOUEUR.md](MULTIJOUEUR.md) pour le jeu à distance.
## Structure
| Chemin | Rôle |
|--------|------|
| `src/escampe/` | Sources Java (paquetage `escampe`) |
| `src/escampe/JoueurPuyaubreauRussac.java` | Le joueur (interface `IJoueur`) |
| `src/escampe/Moteur.java` | Recherche alpha-bêta + heuristique |
| `src/escampe/EscampeBoard.java` | Modèle de jeu (interface `Partie1`) |
| `src/escampe/{VerifMoves,RulesTest,Bench,Branching}.java` | Utilitaires de test (hors jar de production) |
| `report/rapport.html` · `tools/make_report_pdf.py` | Source du rapport et générateur PDF |
| `RAPPORT.md` | Rapport (version Markdown) |
| `build.sh` | Build reproductible |
| `lib/escampeobf.jar` | Serveur d'arbitre + joueurs de référence (fournis) |
| `dist/` | Livrables buildés (jar, `mainClass`, archive, rapport PDF) |
## Livrables de la version finale (dans `dist/`)
- `Puyaubreau_Russac.jar` — jar exécutable (point d'entrée `escampe.ClientJeu`)
- `mainClass` — descripteur du tournoi (jar / clientClass / mainClass)
- `Puyaubreau_Russac.tgz` — archive de rendu (`src/` + `mainClass` + jar)
- `Puyaubreau_Russac_rapport.pdf` — rapport
## Tests
```bash
javac -d out src/escampe/*.java
java -cp out escampe.VerifMoves # chemin de recherche ≡ règles vérifiées (0 divergence)
java -cp out escampe.RulesTest # tests de règles (21/21)
java -cp out escampe.Bench 3000 8 # profondeur / vitesse du moteur
java -cp out escampe.Branching # facteur de branchement
```

58
build.sh Normal file
View File

@@ -0,0 +1,58 @@
#!/bin/bash
# Build reproductible du joueur Escampe (partie 3).
# Produit dans dist/ :
# - Puyaubreau_Russac.jar : jar exécutable (point d'entrée escampe.ClientJeu)
# - mainClass : descripteur attendu par le tournoi
# - Puyaubreau_Russac.tgz : archive de rendu (Puyaubreau_Russac/src + mainClass + jar)
# - Puyaubreau_Russac_rapport.pdf : rapport (via PyMuPDF ; sauter avec --no-report)
set -e
ROOT="$(cd "$(dirname "$0")" && pwd)"; cd "$ROOT"
WITH_REPORT=1; [ "${1:-}" = "--no-report" ] && WITH_REPORT=0
NAME="Puyaubreau_Russac"
JAR="$NAME.jar"
PLAYER="escampe.JoueurPuyaubreauRussac"
CLIENT="escampe.ClientJeu"
# Classes de PRODUCTION (on exclut les utilitaires de test : VerifMoves, RulesTest, Bench).
RUNTIME="IJoueur ClientJeu Solo Applet Partie1 EscampeBoard Moteur JoueurPuyaubreauRussac"
echo "[1/4] Compilation des classes de production…"
rm -rf build dist
mkdir -p build dist
SRCS=""
for c in $RUNTIME; do SRCS="$SRCS src/escampe/$c.java"; done
javac -d build $SRCS
echo "[2/4] Création du jar exécutable ($JAR)…"
jar --create --file "dist/$JAR" --main-class "$CLIENT" -C build escampe
echo "[3/4] Écriture du fichier mainClass…"
printf 'jar:%s\nclientClass:%s\nmainClass:%s\n' "$JAR" "$CLIENT" "$PLAYER" > dist/mainClass
echo "[4/4] Assemblage de l'archive de rendu…"
rm -rf "dist/$NAME"
mkdir -p "dist/$NAME/src/escampe"
cp src/escampe/*.java "dist/$NAME/src/escampe/"
cp dist/mainClass "dist/$NAME/mainClass"
cp "dist/$JAR" "dist/$NAME/$JAR"
( cd dist && tar czf "$NAME.tgz" "$NAME" )
REPORT=""
if [ "$WITH_REPORT" = "1" ]; then
echo "[5/5] Génération du rapport PDF…"
if python tools/make_report_pdf.py >/dev/null 2>&1; then
REPORT=" dist/${NAME}_rapport.pdf"
else
echo " (PDF non généré : PyMuPDF indisponible — relancer 'python tools/make_report_pdf.py')"
fi
fi
echo "----------------------------------------------------"
echo "OK :"
echo " dist/$JAR"
echo " dist/mainClass"
echo " dist/$NAME.tgz"
[ -n "$REPORT" ] && echo "$REPORT"
echo "Lancement tournoi (rappel) :"
echo " java -cp $JAR $CLIENT $PLAYER <hôte> <port>"

BIN
dist/Puyaubreau_Russac.jar vendored Normal file

Binary file not shown.

BIN
dist/Puyaubreau_Russac.tgz vendored Normal file

Binary file not shown.

Binary file not shown.

3
dist/Puyaubreau_Russac/mainClass vendored Normal file
View File

@@ -0,0 +1,3 @@
jar:Puyaubreau_Russac.jar
clientClass:escampe.ClientJeu
mainClass:escampe.JoueurPuyaubreauRussac

View File

@@ -0,0 +1,298 @@
package escampe;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import javax.swing.DefaultListModel;
import javax.swing.JApplet;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ListSelectionModel;
public class Applet extends JApplet {
// Constantes pour les pièces
final private static int LICORNEBLANCHE = -2;
final private static int PALADINBLANC = -1;
final private static int LICORNENOIRE = 2;
final private static int PALADINNOIR = 1;
final private static int VIDE = 0;
// Constantes pour le plateau
final private static int LARGEUR = 6;
final private static int HAUTEUR = 6;
final private static int[][] lisereCase = {
{1, 2, 2, 3, 1, 2},
{3, 1, 3, 1, 3, 2},
{2, 3, 1, 2, 1, 3},
{2, 1, 3, 2, 3, 1},
{1, 3, 1, 3, 1, 2},
{3, 2, 2, 1, 3, 2}
};
// Constantes pour les couleurs
Color DARK = new Color(155, 102, 95);
Color LIGHT = new Color(239, 210, 158);
Color BLACK = new Color(255, 255, 255);
Color WHITE = new Color(0, 0, 0);
Color HIGHLIGHT = new Color(255, 0, 0);
// Constantes pour l'affichage
final private static int TAILLECASE = 100;
final private static int TAILLEPION = 60;
final private static Dimension FRAMEDIMENSION = new Dimension(TAILLECASE*6 + 260,TAILLECASE*6 + 60);
private static final long serialVersionUID = 1L;
private JList brdList;
private Board displayBoard;
private JScrollPane scrollPane;
private DefaultListModel listModel;
private Frame myFrame;
static int cpt = 0;
// Autres constantes utiles pour l'affichage du plateau d'Escampe
int mpiece = (int) (TAILLECASE - TAILLEPION)/2;
int epaisseurCercle = (int) (TAILLECASE*0.1);
int epaisseurInterCercle = (int) (TAILLECASE*0.05);
int diametre1e = TAILLECASE; // extérieur 1er cercle
int diametre1i = diametre1e - epaisseurCercle; // intérieur 1er cercle
int diametre2e = diametre1i - epaisseurInterCercle; // extérieur 2eme cercle
int diametre2i = diametre2e - epaisseurCercle; // intérieur 2eme cercle
int diametre3e = diametre2i - epaisseurInterCercle; // extérieur 3eme cercle
int diametre3i = diametre3e - epaisseurCercle; // intérieur 3eme cercle
int m1e = 0;
int m1i = (int) (TAILLECASE - diametre1i)/2;
int m2e = (int) (TAILLECASE - diametre2e)/2;
int m2i = (int) (TAILLECASE - diametre2i)/2;
int m3e = (int) (TAILLECASE - diametre3e)/2;
int m3i = (int) (TAILLECASE - diametre3i)/2;
public void init() {
System.out.println("Initialisation BoardApplet" + cpt++);
buildUI(getContentPane());
}
public void buildUI(Container container) {
setBackground(Color.white);
int[][] temp = new int[HAUTEUR][LARGEUR];
for (int i = 0; i < HAUTEUR; i++)
for (int j = 0; j < LARGEUR; j++)
temp[i][j] = VIDE;
displayBoard = new Board("Coups :", temp);
listModel = new DefaultListModel();
listModel.addElement(displayBoard);
brdList = new JList(listModel);
brdList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
brdList.setSelectedIndex(0);
scrollPane = new JScrollPane(brdList);
Dimension d = scrollPane.getSize();
scrollPane.setPreferredSize(new Dimension(200, d.height));
brdList.addKeyListener(new java.awt.event.KeyAdapter() {
public void keyPressed(KeyEvent e) {
brdList_keyPressed(e);
}
});
brdList.addMouseListener(new java.awt.event.MouseAdapter() {
public void mouseClicked(MouseEvent e) {
brdList_mouseClicked(e);
}
});
container.add(displayBoard, BorderLayout.CENTER);
container.add(scrollPane, BorderLayout.EAST);
}
public void update(Graphics g, Insets in) {
Insets tempIn = in;
g.translate(tempIn.left, tempIn.top);
paint(g);
}
public void paint(Graphics g) {
displayBoard.paint(g);
}
public void addBoard(String move, int[][] board) {
Board tempEntrop = new Board(move, board);
listModel.addElement(new Board(move, board));
brdList.setSelectedIndex(listModel.getSize() - 1);
brdList.ensureIndexIsVisible(listModel.getSize() - 1);
displayBoard = tempEntrop;
update(myFrame.getGraphics(), myFrame.getInsets());
}
public void setMyFrame(Frame f) {
myFrame = f;
}
void brdList_keyPressed(KeyEvent e) {
int index = brdList.getSelectedIndex();
if (e.getKeyCode() == KeyEvent.VK_UP && index > 0)
displayBoard = (Board) listModel.getElementAt(index - 1);
if (e.getKeyCode() == KeyEvent.VK_DOWN && index < (listModel.getSize() - 1))
displayBoard = (Board) listModel.getElementAt(index + 1);
update(myFrame.getGraphics(), myFrame.getInsets());
}
void brdList_mouseClicked(MouseEvent e) {
displayBoard = (Board) listModel.getElementAt(brdList.getSelectedIndex());
update(myFrame.getGraphics(), myFrame.getInsets());
}
public Dimension getDimension() {
return FRAMEDIMENSION;
}
// Sous classe qui dessine le plateau de jeu
class Board extends JPanel {
private static final long serialVersionUID = 1L;
private int[][] boardState;
String move;
int depCol = -1;
int depLin = -1;
int arvCol = -1;
int arvLin = -1;
// The string will be the move details
// and the array the details of the board after the move has been applied.
public Board(String mv, int[][] bs) {
boardState = bs;
move = mv;
if (mv.length() == 5) {
String[] positions = mv.split("-");
depCol = (int) positions[0].charAt(0) - (int) 'A';
depLin = Integer.parseInt(positions[0].substring(1)) - 1;
arvCol = (int) positions[1].charAt(0) - (int) 'A';
arvLin = Integer.parseInt(positions[1].substring(1)) - 1;
}
}
public void drawBoard(Graphics g) {
// First draw the lines
// Board
int bx = 30;
int by = 30;
// axis labels
g.setColor(new Color(0, 0, 0));
for (int i = 1; i <= LARGEUR; i++) {
g.drawString("" + (char) ('A' + i - 1), bx + (int) ((i - 0.5)*TAILLECASE), 20);
}
for (int i = 1; i <= HAUTEUR; i++) {
g.drawString("" + i, 10, by + (int) ((i - 0.5)*TAILLECASE));
}
// Draw the circles
Color c1 = DARK;
Color c2 = LIGHT;
int casex;
int casey;
int lisere;
// fond des cases
g.setColor(c1);
g.fillRect(bx, by, LARGEUR*TAILLECASE, HAUTEUR*TAILLECASE);
for (int j = 0; j < LARGEUR; j++) {
for (int i = 0; i < HAUTEUR; i++) {
casex = bx + j*TAILLECASE;
casey = by + i*TAILLECASE;
lisere = lisereCase[i][j];
c2 = (i == depLin && j == depCol) ? HIGHLIGHT : LIGHT;
// 1er cercle
g.setColor(c2);
g.fillOval(casex + m1e, casey + m1e , diametre1e, diametre1e);
g.setColor(c1);
g.fillOval(casex + m1i, casey + m1i, diametre1i, diametre1i);
if (lisere > 1) {
// 2eme cercle
g.setColor(c2);
g.fillOval(casex + m2e, casey + m2e, diametre2e, diametre2e);
g.setColor(c1);
g.fillOval(casex + m2i, casey + m2i, diametre2i, diametre2i);
if (lisere > 2) {
// 3eme cercle
g.setColor(c2);
g.fillOval(casex + m3e, casey + m3e, diametre3e, diametre3e);
g.setColor(c1);
g.fillOval(casex + m3i, casey + m3i, diametre3i, diametre3i);
}
}
}
}
// Draw the pieces by referencing boardState array
c1 = BLACK;
c2 = WHITE;
for (int j = 0; j < LARGEUR; j++) {
for (int i = 0; i < HAUTEUR; i++) {
casex = mpiece + bx + j*TAILLECASE;
casey = mpiece + by + i*TAILLECASE;
switch (boardState[i][j]) {
case (LICORNEBLANCHE):
g.setColor(c1);
g.fillRect(casex, casey, TAILLEPION, TAILLEPION);
break;
case (PALADINBLANC):
g.setColor(c1);
g.fillOval(casex, casey, TAILLEPION, TAILLEPION);
break;
case (LICORNENOIRE):
g.setColor(c2);
g.fillRect(casex, casey, TAILLEPION, TAILLEPION);
break;
case (PALADINNOIR):
g.setColor(c2);
g.fillOval(casex, casey, TAILLEPION, TAILLEPION);
break;
}
if (i == arvLin && j == arvCol) {
g.setColor(HIGHLIGHT);
g.fillOval(casex + 20, casey + 20, TAILLEPION - 40, TAILLEPION - 40);
}
}
}
}
public void paint(Graphics g) {
drawBoard(g);
}
public void update(Graphics g) {
drawBoard(g);
}
public String toString() {
return move;
}
}
}

View File

@@ -0,0 +1,30 @@
package escampe;
/**
* Banc d'essai du moteur : joue quelques coups depuis l'ouverture et affiche
* profondeur, score, nœuds et vitesse. java -cp out escampe.Bench [msParCoup] [nbCoups]
*/
public class Bench {
public static void main(String[] args) {
long budget = args.length > 0 ? Long.parseLong(args[0]) : 3000;
int coups = args.length > 1 ? Integer.parseInt(args[1]) : 8;
EscampeBoard b = new EscampeBoard();
b.play("C1/A1/E1/B2/C2/D2", "noir");
b.play("C6/A6/E6/B5/C5/D5", "blanc");
Moteur mo = new Moteur();
boolean black = false; // Blanc joue en premier après les placements
for (int i = 0; i < coups && !b.gameOver(); i++) {
long t0 = System.currentTimeMillis();
int m = mo.bestMove(b, black, budget);
long dt = System.currentTimeMillis() - t0;
System.out.printf("coup %d (%s) : %-6s prof=%2d score=%7d noeuds=%9d %5dms %6.0f kN/s%n",
i, black ? "noir" : "blanc", b.moveToString(m),
mo.reachedDepth, mo.lastScore, mo.nodes, dt, mo.nodes / (dt + 1.0));
b.play(b.moveToString(m), black ? "noir" : "blanc");
black = !black;
}
System.out.println(b.gameOver() ? "Partie terminée (capture)." : "Fin du banc.");
}
}

View File

@@ -0,0 +1,58 @@
package escampe;
import java.util.*;
/**
* Mesure empirique du facteur de branchement (question Q3 du rapport) : explore
* des parties aléatoires et relève le nombre maximal de coups légaux rencontré,
* en distinguant le cas contraint (un liseré imposé) du cas libre (1er coup ou
* après un pass, lastTileType = -1). java -cp out escampe.Branching [parties]
*/
public class Branching {
public static void main(String[] args) {
int games = args.length > 0 ? Integer.parseInt(args[0]) : 20000;
Random rng = new Random(1L);
int maxConstrained = 0, maxFree = 0;
long sum = 0, count = 0;
for (int g = 0; g < games; g++) {
EscampeBoard b = new EscampeBoard();
int[] nr = rng.nextBoolean() ? new int[]{0, 1} : new int[]{4, 5};
b.play(rndPlace(b, "noir", nr, rng), "noir");
int[] wr = nr[0] == 0 ? new int[]{4, 5} : new int[]{0, 1};
b.play(rndPlace(b, "blanc", wr, rng), "blanc");
for (int ply = 0; ply < 120 && !b.gameOver(); ply++) {
String side = b.currentPlayer;
String[] mv = b.possiblesMoves(side);
int n = (mv.length == 1 && mv[0].equals("E")) ? 0 : mv.length;
if (b.lastTileType == -1) maxFree = Math.max(maxFree, n);
else maxConstrained = Math.max(maxConstrained, n);
sum += n; count++;
if (n == 0) { b.play("E", side); }
else { b.play(mv[rng.nextInt(mv.length)], side); }
}
}
System.out.println("Parties simulées : " + games);
System.out.println("Branchement max CONTRAINT : " + maxConstrained + " (un liseré imposé)");
System.out.println("Branchement max LIBRE : " + maxFree + " (1er coup / après pass)");
System.out.printf ("Branchement moyen : %.1f%n", (double) sum / count);
}
static String rndPlace(EscampeBoard b, String pl, int[] rows, Random rng) {
List<int[]> cells = new ArrayList<>();
for (int r : rows) for (int c = 0; c < 6; c++) cells.add(new int[]{r, c});
for (int t = 0; t < 50; t++) {
Collections.shuffle(cells, rng);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 6; i++) {
if (i > 0) sb.append('/');
sb.append((char) ('A' + cells.get(i)[1])).append((char) ('1' + cells.get(i)[0]));
}
if (b.isValidMove(sb.toString(), pl)) return sb.toString();
}
throw new IllegalStateException("placement");
}
}

View File

@@ -0,0 +1,151 @@
package escampe;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.StringTokenizer;
/**
* Cette classe permet de charger dynamiquement une classe de joueur, qui doit obligatoirement
* implanter l'interface IJoueur. Vous lui donnez aussi en argument le nom de la machine distante
* (ou "localhost") sur laquelle le serveur de jeu est lancé, ainsi que le port sur lequel la
* machine écoute.
*
* Exemple: >java -cp . frontieres.ClientJeu frontieres.joueurProf localhost 1234
*
* Le client s'occupe alors de tout en lançant les méthodes implantées de l'interface IJoueur. Toute
* la gestion réseau est donc cachée.
*
* @author L. Simon (Univ. Paris-Sud)- 2006-2008
* @see IJoueur
*/
public class ClientJeu {
// Mais pas lors de la conversation avec l'arbitre
// Vous pouvez changer cela en interne si vous le souhaitez
static final int BLANC = -1;
static final int NOIR = 1;
static final int VIDE = 0;
/**
* @param args
* Dans l'ordre : NomClasseJoueur MachineServeur PortEcoute
*/
public static void main(String[] args) {
if (args.length < 3) {
System.err.println("ClientJeu Usage: NomClasseJoueur MachineServeur PortEcoute");
System.exit(1);
}
// Le nom de la classe joueur à charger dynamiquement
String classeJoueur = args[0];
// Le nom de la machine serveur a été donné en ligne de commande
String serverMachine = args[1];
// Le numéro du port sur lequel on se connecte a aussi été donné
int portNum = Integer.parseInt(args[2]);
System.out.println("Le client se connectera sur " + serverMachine + ":" + portNum);
Socket clientSocket = null;
IJoueur joueur;
String msg, firstToken;
// permet d'analyser les chaînes de caractères lues
StringTokenizer msgTokenizer;
// C'est la couleur qui doit jouer le prochain coup
int couleurAJouer;
// C'est ma couleur (quand je joue)
int maCouleur;
boolean jeuTermine = false;
try {
// initialise la socket
clientSocket = new Socket(serverMachine, portNum);
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
// *****************************************************
System.out.print("Chargement de la classe joueur " + classeJoueur + "... ");
Class<?> cjoueur = Class.forName(classeJoueur);
joueur = (IJoueur) cjoueur.newInstance();
System.out.println("Ok");
// ****************************************************
// Envoie de l'identifiant de votre quadrinome.
out.println(joueur.binoName());
System.out.println("Mon nom de quadrinome envoyé est " + joueur.binoName());
// Récupère le message sous forme de chaine de caractères
msg = in.readLine();
System.out.println(msg);
// Lit le contenu du message, toutes les infos du message
msgTokenizer = new StringTokenizer(msg, " \n\0");
if ((msgTokenizer.nextToken()).equals("Blanc")) {
System.out.println("Je suis Blanc, j'attends le mouvement de Noir.");
maCouleur = BLANC;
}
else { // doit etre égal à "Noir"
System.out.println("Je suis Noir, c'est à moi de jouer.");
maCouleur = NOIR;
}
// permet d'initialiser votre joueur avec sa couleur
joueur.initJoueur(maCouleur);
// boucle générale de jeu
do {
// Lire le msg à partir du serveur
msg = in.readLine();
msgTokenizer = new StringTokenizer(msg, " \n\0");
firstToken = msgTokenizer.nextToken();
if (firstToken.equals("FIN!")) {
jeuTermine = true;
String theWinnerIs = msgTokenizer.nextToken();
if (theWinnerIs.equals("Blanc")) {
couleurAJouer = BLANC;
}
else {
if (theWinnerIs.equals("Noir"))
couleurAJouer = NOIR;
else
couleurAJouer = VIDE;
}
if (couleurAJouer == maCouleur)
System.out.println("J'ai gagné!");
joueur.declareLeVainqueur(couleurAJouer);
}
else if (firstToken.equals("JOUEUR")) {
// On demande au joueur de jouer
if ((msgTokenizer.nextToken()).equals("Blanc")) {
couleurAJouer = BLANC;
}
else {
couleurAJouer = NOIR;
}
if (couleurAJouer == maCouleur) {
// On appelle la classe du joueur pour choisir un mouvement
msg = joueur.choixMouvement();
out.println(msg);
}
}
else if (firstToken.equals("MOUVEMENT")) {
// On lit ce que joue le joueur et on l'envoie à l'autre
joueur.mouvementEnnemi(msgTokenizer.nextToken());
}
} while (!jeuTermine);
}
catch (Exception e) {
System.out.println(e);
}
}
}

View File

@@ -0,0 +1,862 @@
package escampe;
import java.io.*;
import java.util.*;
/**
* Représentation d'un état du jeu Escampe.
*
* <p>Le plateau est un tableau {@code int[6][6]} :
* <ul>
* <li>{@code board[row][col]} avec row 0 = ligne 1 (bas), row 5 = ligne 6 (haut).</li>
* <li>col 0 = colonne A, col 5 = colonne F.</li>
* </ul>
*
* <p>Chaque case stocke l'une des constantes pièce :
* {@code EMPTY}, {@code WHITE_LICORNE}, {@code WHITE_PALADIN},
* {@code BLACK_LICORNE}, {@code BLACK_PALADIN}.
*
* <p>L'état complémentaire mémorisé :
* <ul>
* <li>{@code lastTileType} : type de liseré (1, 2 ou 3) de la case d'arrivée du dernier coup ;
* -1 = pas de contrainte (premier coup ou après un pass).</li>
* <li>{@code currentPlayer} : "noir" ou "blanc", joueur dont c'est le tour.</li>
* <li>{@code blackPlaced}, {@code whitePlaced} : phases de placement terminées.</li>
* <li>{@code blackRows} : les deux lignes (index 0-5) choisies par noir lors du placement.</li>
* </ul>
*
* <p>Règles de déplacement :
* <ul>
* <li>Une pièce avance exactement N pas orthogonaux (N = liseré de la case de départ).</li>
* <li>Elle peut changer de direction à chaque pas.</li>
* <li>Elle ne peut pas passer par une case occupée ni repasser deux fois par la même case.</li>
* <li>Au dernier pas uniquement, elle peut se poser sur la licorne adverse (capture).</li>
* </ul>
*/
public class EscampeBoard implements Partie1 {
// ── Constantes pièces ────────────────────────────────────────────────────
static final int EMPTY = 0;
static final int WHITE_LICORNE = 1;
static final int WHITE_PALADIN = 2;
static final int BLACK_LICORNE = 3;
static final int BLACK_PALADIN = 4;
/**
* Carte des liserés : {@code TILE_MAP[row][col]}.
* row 0 = ligne 1 (bas), row 5 = ligne 6 (haut). col 0 = A, col 5 = F.
*/
static final int[][] TILE_MAP = {
{1, 2, 2, 3, 1, 2}, // ligne 1
{3, 1, 3, 1, 3, 2}, // ligne 2
{2, 3, 1, 2, 1, 3}, // ligne 3
{2, 1, 3, 2, 3, 1}, // ligne 4
{1, 3, 1, 3, 1, 2}, // ligne 5
{3, 2, 2, 1, 3, 2}, // ligne 6
};
// ── État ─────────────────────────────────────────────────────────────────
int[][] board;
int lastTileType; // -1 = pas de contrainte
String currentPlayer; // "noir" ou "blanc"
boolean blackPlaced;
boolean whitePlaced;
int[] blackRows; // les 2 lignes (0-indexé) choisies par noir
// ── Constructeur ─────────────────────────────────────────────────────────
public EscampeBoard() {
board = new int[6][6];
lastTileType = -1;
currentPlayer = "noir";
blackPlaced = false;
whitePlaced = false;
blackRows = null;
}
// =========================================================================
// Fichier I/O
// =========================================================================
@Override
public void setFromFile(String fileName) {
board = new int[6][6];
lastTileType = -1;
currentPlayer = "noir";
blackPlaced = false;
whitePlaced = false;
blackRows = null;
try (BufferedReader br = new BufferedReader(new FileReader(fileName))) {
String line;
while ((line = br.readLine()) != null) {
line = line.trim();
if (line.isEmpty()) continue;
char first = line.charAt(0);
// Commentaire / méta-donnée
if (first == '%') {
parseMeta(line);
continue;
}
// Ligne de plateau : "1 XXXX 1" ou "01 XXXX 01"
int rowNum = -1;
int pos = 0;
if (first >= '1' && first <= '6') {
rowNum = first - '0';
pos = 1;
} else if (first == '0' && line.length() > 1) {
char second = line.charAt(1);
if (second >= '1' && second <= '6') {
rowNum = second - '0';
pos = 2;
}
}
if (rowNum != -1) {
int rowIdx = rowNum - 1;
while (pos < line.length() && line.charAt(pos) == ' ') pos++;
for (int c = 0; c < 6 && pos + c < line.length(); c++) {
board[rowIdx][c] = charToPiece(line.charAt(pos + c));
}
}
}
} catch (IOException e) {
throw new RuntimeException("Erreur de lecture du fichier : " + fileName, e);
}
// Si pas de méta-commentaires, on infère l'état à partir des pièces
inferState();
}
/** Parse une ligne de méta-commentaire "% clé: valeur". */
private void parseMeta(String line) {
if (line.startsWith("% lastTileType:")) {
lastTileType = Integer.parseInt(line.substring(15).trim());
} else if (line.startsWith("% currentPlayer:")) {
currentPlayer = line.substring(16).trim();
} else if (line.startsWith("% blackPlaced:")) {
blackPlaced = Boolean.parseBoolean(line.substring(14).trim());
} else if (line.startsWith("% whitePlaced:")) {
whitePlaced = Boolean.parseBoolean(line.substring(14).trim());
} else if (line.startsWith("% blackRows:")) {
String s = line.substring(12).trim();
String[] parts = s.split(",");
int r0 = Integer.parseInt(parts[0].trim());
int r1 = Integer.parseInt(parts[1].trim());
if (r0 >= 0) blackRows = new int[]{r0, r1};
}
}
/**
* Infère {@code blackPlaced}, {@code whitePlaced} et {@code blackRows}
* à partir des pièces présentes sur le plateau
* (utilisé quand le fichier ne contient pas de méta-commentaires).
*/
private void inferState() {
if (blackPlaced && whitePlaced) return; // méta déjà chargée
int bc = 0, wc = 0;
Set<Integer> bRowSet = new TreeSet<>();
for (int r = 0; r < 6; r++) {
for (int c = 0; c < 6; c++) {
int p = board[r][c];
if (p == BLACK_LICORNE || p == BLACK_PALADIN) { bc++; bRowSet.add(r); }
if (p == WHITE_LICORNE || p == WHITE_PALADIN) { wc++; }
}
}
if (!blackPlaced && bc == 6) {
blackPlaced = true;
// Bord de noir déduit d'une ligne occupée (robuste à 1 seule ligne).
int anyRow = bRowSet.iterator().next();
blackRows = (anyRow <= 1) ? new int[]{0, 1} : new int[]{4, 5};
}
if (!whitePlaced && wc == 6) {
whitePlaced = true;
}
}
@Override
public void saveToFile(String fileName) {
try (PrintWriter pw = new PrintWriter(new FileWriter(fileName))) {
pw.println("% Escampe - sauvegarde du plateau");
pw.println("% lastTileType: " + lastTileType);
pw.println("% currentPlayer: " + currentPlayer);
pw.println("% blackPlaced: " + blackPlaced);
pw.println("% whitePlaced: " + whitePlaced);
if (blackRows != null) {
pw.println("% blackRows: " + blackRows[0] + "," + blackRows[1]);
} else {
pw.println("% blackRows: -1,-1");
}
// Lignes 6 à 1 (haut vers bas dans le fichier)
for (int rowIdx = 5; rowIdx >= 0; rowIdx--) {
int rowNum = rowIdx + 1;
StringBuilder sb = new StringBuilder();
String rowLabel = String.format("%02d", rowNum);
sb.append(rowLabel).append(' ');
for (int c = 0; c < 6; c++) sb.append(pieceToChar(board[rowIdx][c]));
sb.append(' ').append(rowLabel);
pw.println(sb.toString());
}
} catch (IOException e) {
throw new RuntimeException("Erreur d'écriture du fichier : " + fileName, e);
}
}
// =========================================================================
// Fin de partie
// =========================================================================
@Override
public boolean gameOver() {
if (!blackPlaced || !whitePlaced) return false;
boolean wl = false, bl = false;
for (int r = 0; r < 6; r++)
for (int c = 0; c < 6; c++) {
if (board[r][c] == WHITE_LICORNE) wl = true;
if (board[r][c] == BLACK_LICORNE) bl = true;
}
return !wl || !bl;
}
// =========================================================================
// Validation d'un coup
// =========================================================================
@Override
public boolean isValidMove(String move, String player) {
if (move == null || move.isEmpty()) return false;
if (!"noir".equals(player) && !"blanc".equals(player)) return false;
if (move.contains("/")) return isValidPlacement(move, player);
if ("E".equals(move)) return isValidPass(player);
return isValidRegularMove(move, player);
}
/**
* Valide un coup de placement "P1/P2/P3/P4/P5/P6"
* (P1 = licorne, P2-P6 = paladins).
*/
private boolean isValidPlacement(String move, String player) {
if ("noir".equals(player) && blackPlaced) return false;
if ("blanc".equals(player) && whitePlaced) return false;
if (!player.equals(currentPlayer)) return false;
if ("blanc".equals(player) && !blackPlaced) return false;
String[] parts = move.split("/");
if (parts.length != 6) return false;
int[][] pos = new int[6][2];
for (int i = 0; i < 6; i++) {
int[] cell = cellFromString(parts[i]);
if (cell == null) return false;
pos[i] = cell;
}
// Zone autorisée
if ("noir".equals(player)) {
boolean allLow = true, allHigh = true;
for (int[] p : pos) {
if (p[0] != 0 && p[0] != 1) allLow = false;
if (p[0] != 4 && p[0] != 5) allHigh = false;
}
if (!allLow && !allHigh) return false;
} else {
if (blackRows == null) return false;
int[] wr = complementaryRows(blackRows);
for (int[] p : pos) {
if (p[0] != wr[0] && p[0] != wr[1]) return false;
}
}
// Pas de doublons, cases vides
Set<String> seen = new HashSet<>();
for (int[] p : pos) {
if (!seen.add(p[0] + "," + p[1])) return false;
if (board[p[0]][p[1]] != EMPTY) return false;
}
return true;
}
/** Valide un pass "E" : uniquement si aucun coup régulier n'est disponible. */
private boolean isValidPass(String player) {
if (!player.equals(currentPlayer)) return false;
if (!blackPlaced || !whitePlaced) return false;
if (gameOver()) return false;
String[] m = possiblesMoves(player);
return m.length == 1 && "E".equals(m[0]);
}
/** Valide un coup régulier "XX-YY". */
private boolean isValidRegularMove(String move, String player) {
if (!blackPlaced || !whitePlaced) return false;
if (gameOver()) return false;
if (!player.equals(currentPlayer)) return false;
int dash = move.indexOf('-');
if (dash < 1 || dash >= move.length() - 1) return false;
int[] from = cellFromString(move.substring(0, dash));
int[] to = cellFromString(move.substring(dash + 1));
if (from == null || to == null) return false;
if (!belongsToPlayer(board[from[0]][from[1]], player)) return false;
if (lastTileType != -1 && TILE_MAP[from[0]][from[1]] != lastTileType) return false;
return getReachableSquares(from[0], from[1], player).contains(to[0] + "," + to[1]);
}
// =========================================================================
// Génération de coups
// =========================================================================
@Override
public String[] possiblesMoves(String player) {
// Pendant le placement le nombre de combinaisons est trop grand pour être énuméré
if (!blackPlaced || !whitePlaced) return new String[0];
if (gameOver()) return new String[0];
List<String> moves = new ArrayList<>();
for (int r = 0; r < 6; r++) {
for (int c = 0; c < 6; c++) {
if (!belongsToPlayer(board[r][c], player)) continue;
if (lastTileType != -1 && TILE_MAP[r][c] != lastTileType) continue;
for (String dest : getReachableSquares(r, c, player)) {
String[] d = dest.split(",");
moves.add(stringFromCell(r, c) + "-"
+ stringFromCell(Integer.parseInt(d[0]), Integer.parseInt(d[1])));
}
}
}
if (moves.isEmpty()) return new String[]{"E"};
return moves.toArray(new String[0]);
}
// =========================================================================
// Jouer un coup
// =========================================================================
@Override
public void play(String move, String player) {
if (!isValidMove(move, player))
throw new IllegalArgumentException("Coup invalide : '" + move + "' pour " + player);
if (move.contains("/")) {
playPlacement(move, player);
} else if ("E".equals(move)) {
// Pass : supprime la contrainte de liseré (règle officielle)
lastTileType = -1;
currentPlayer = opponent(currentPlayer);
} else {
playRegular(move, player);
}
}
private void playPlacement(String move, String player) {
String[] parts = move.split("/");
int[][] pos = new int[6][2];
for (int i = 0; i < 6; i++) pos[i] = cellFromString(parts[i]);
int licorne = "noir".equals(player) ? BLACK_LICORNE : WHITE_LICORNE;
int paladin = "noir".equals(player) ? BLACK_PALADIN : WHITE_PALADIN;
board[pos[0][0]][pos[0][1]] = licorne;
for (int i = 1; i < 6; i++) board[pos[i][0]][pos[i][1]] = paladin;
if ("noir".equals(player)) {
blackPlaced = true;
// Bord de noir (bas {0,1} ou haut {4,5}), déduit de la ligne de la licorne.
blackRows = (pos[0][0] <= 1) ? new int[]{0, 1} : new int[]{4, 5};
currentPlayer = "blanc";
} else {
whitePlaced = true;
lastTileType = -1; // pas de contrainte pour le premier coup régulier
currentPlayer = "blanc"; // blanc joue en premier
}
}
private void playRegular(String move, String player) {
int dash = move.indexOf('-');
int[] from = cellFromString(move.substring(0, dash));
int[] to = cellFromString(move.substring(dash + 1));
board[to[0]][to[1]] = board[from[0]][from[1]]; // capture si case adverse
board[from[0]][from[1]] = EMPTY;
lastTileType = TILE_MAP[to[0]][to[1]];
currentPlayer = opponent(currentPlayer);
}
// =========================================================================
// Algorithme de déplacement (DFS)
// =========================================================================
/**
* Calcule l'ensemble des cases atteignables depuis (fromRow, fromCol).
* Résultats encodés sous forme "row,col".
*/
Set<String> getReachableSquares(int fromRow, int fromCol, String player) {
Set<String> result = new HashSet<>();
boolean[][] visited = new boolean[6][6];
visited[fromRow][fromCol] = true;
dfs(fromRow, fromCol, TILE_MAP[fromRow][fromCol], player, visited, result);
return result;
}
/**
* DFS récursif pour le calcul des destinations.
*
* <p>À chaque appel, la pièce se trouve en (row, col) et doit encore effectuer
* {@code stepsLeft} pas. Les cases déjà visitées dans le chemin courant sont
* marquées dans {@code visited} (réinitialisation après backtrack).
*
* <p>Règles :
* <ul>
* <li>Pas intermédiaires (stepsLeft > 1) : la case suivante doit être vide.</li>
* <li>Dernier pas (stepsLeft == 1) : la case peut être vide ou contenir
* la licorne adverse (capture).</li>
* </ul>
*/
private void dfs(int row, int col, int stepsLeft,
String player, boolean[][] visited, Set<String> result) {
if (stepsLeft == 0) {
result.add(row + "," + col);
return;
}
// Directions orthogonales : haut, bas, gauche, droite
int[] dr = {-1, 1, 0, 0};
int[] dc = { 0, 0, -1, 1};
for (int d = 0; d < 4; d++) {
int nr = row + dr[d];
int nc = col + dc[d];
if (nr < 0 || nr >= 6 || nc < 0 || nc >= 6) continue;
if (visited[nr][nc]) continue;
int occ = board[nr][nc];
boolean canStep;
if (stepsLeft > 1) {
// Pas intermédiaire : case obligatoirement vide
canStep = (occ == EMPTY);
} else {
// Dernier pas : vide OU capture de la licorne adverse
canStep = (occ == EMPTY)
|| ("blanc".equals(player) && occ == BLACK_LICORNE)
|| ("noir".equals(player) && occ == WHITE_LICORNE);
}
if (!canStep) continue;
visited[nr][nc] = true;
dfs(nr, nc, stepsLeft - 1, player, visited, result);
visited[nr][nc] = false; // backtrack
}
}
// Chemin de génération « int » pour le moteur, sans allocation de String.
// Case = row*6+col (0..35) ; coup = from*36+to ; pass = MOVE_PASS ; black = noir.
// Équivalent au chemin String vérifié (contrôlé par VerifMoves).
static final int MOVE_PASS = -1;
record Undo(int move, int captured, int savedLastTile, String savedPlayer) {}
/** Copie profonde de l'état (le moteur cherche sur une copie, jamais sur le live). */
EscampeBoard copy() {
EscampeBoard b = new EscampeBoard();
for (int r = 0; r < 6; r++) b.board[r] = board[r].clone();
b.lastTileType = lastTileType;
b.currentPlayer = currentPlayer;
b.blackPlaced = blackPlaced;
b.whitePlaced = whitePlaced;
b.blackRows = (blackRows == null) ? null : blackRows.clone();
return b;
}
private boolean isSide(int piece, boolean black) {
return black ? (piece == BLACK_LICORNE || piece == BLACK_PALADIN)
: (piece == WHITE_LICORNE || piece == WHITE_PALADIN);
}
/** Version allouante de {@link #genMovesIntInto}, pour les tests. */
int[] genMovesInt(boolean black) {
int[] buf = new int[256];
int n = genMovesIntInto(black, buf);
if (n == 0) return new int[0];
return java.util.Arrays.copyOf(buf, n);
}
/**
* Écrit les coups de la phase régulière de {@code black} dans {@code buf} et
* renvoie leur nombre : 0 hors phase régulière, ou {@code {MOVE_PASS}} si bloqué.
*/
int genMovesIntInto(boolean black, int[] buf) {
if (!blackPlaced || !whitePlaced) return 0;
if (gameOver()) return 0;
int n = 0;
for (int r = 0; r < 6; r++) {
for (int c = 0; c < 6; c++) {
if (!isSide(board[r][c], black)) continue;
if (lastTileType != -1 && TILE_MAP[r][c] != lastTileType) continue;
int from = r * 6 + c;
long reach = dfsMask(r, c, TILE_MAP[r][c], black, 1L << from, 0L);
while (reach != 0L) {
int t = Long.numberOfTrailingZeros(reach);
reach &= reach - 1;
buf[n++] = from * 36 + t;
}
}
}
if (n == 0) { buf[0] = MOVE_PASS; return 1; }
return n;
}
/** DFS sur masque de bits (équivalent de {@link #dfs}) : {@code visited}/{@code reach} = ensembles de cases. */
private long dfsMask(int row, int col, int steps, boolean black, long visited, long reach) {
if (steps == 0) return reach | (1L << (row * 6 + col));
final int[] dr = {-1, 1, 0, 0};
final int[] dc = { 0, 0, -1, 1};
for (int d = 0; d < 4; d++) {
int nr = row + dr[d], nc = col + dc[d];
if (nr < 0 || nr >= 6 || nc < 0 || nc >= 6) continue;
int ncell = nr * 6 + nc;
if ((visited & (1L << ncell)) != 0) continue;
int occ = board[nr][nc];
boolean canStep;
if (steps > 1) {
canStep = (occ == EMPTY);
} else {
canStep = (occ == EMPTY)
|| (black && occ == WHITE_LICORNE)
|| (!black && occ == BLACK_LICORNE);
}
if (!canStep) continue;
reach = dfsMask(nr, nc, steps - 1, black, visited | (1L << ncell), reach);
}
return reach;
}
/** Applique un coup int (régulier ou {@code MOVE_PASS}) et renvoie le jeton d'annulation. */
Undo makeInt(int move) {
int savedLast = lastTileType;
String savedPlayer = currentPlayer;
if (move == MOVE_PASS) {
lastTileType = -1;
currentPlayer = opponent(currentPlayer);
return new Undo(move, EMPTY, savedLast, savedPlayer);
}
int from = move / 36, to = move % 36;
int fr = from / 6, fc = from % 6, tr = to / 6, tc = to % 6;
int captured = board[tr][tc];
board[tr][tc] = board[fr][fc];
board[fr][fc] = EMPTY;
lastTileType = TILE_MAP[tr][tc];
currentPlayer = opponent(currentPlayer);
return new Undo(move, captured, savedLast, savedPlayer);
}
/** Annule l'effet de {@link #makeInt}. */
void unmakeInt(Undo u) {
if (u.move() != MOVE_PASS) {
int from = u.move() / 36, to = u.move() % 36;
int fr = from / 6, fc = from % 6, tr = to / 6, tc = to % 6;
board[fr][fc] = board[tr][tc];
board[tr][tc] = u.captured();
}
lastTileType = u.savedLastTile();
currentPlayer = u.savedPlayer();
}
/** Code int → notation "A1-B2" (ou "E" pour le pass). */
String moveToString(int move) {
if (move == MOVE_PASS) return "E";
int from = move / 36, to = move % 36;
return stringFromCell(from / 6, from % 6) + "-" + stringFromCell(to / 6, to % 6);
}
// =========================================================================
// Méthodes utilitaires
// =========================================================================
private int charToPiece(char c) {
switch (c) {
case 'B': return WHITE_LICORNE;
case 'b': return WHITE_PALADIN;
case 'N': return BLACK_LICORNE;
case 'n': return BLACK_PALADIN;
default: return EMPTY;
}
}
private char pieceToChar(int piece) {
switch (piece) {
case WHITE_LICORNE: return 'B';
case WHITE_PALADIN: return 'b';
case BLACK_LICORNE: return 'N';
case BLACK_PALADIN: return 'n';
default: return '-';
}
}
/**
* Convertit une chaîne "A1"-"F6" en coordonnées {row, col} (0-indexé).
* Retourne null si le format est invalide.
*/
int[] cellFromString(String s) {
if (s == null || s.length() < 2) return null;
s = s.trim();
char colC = Character.toUpperCase(s.charAt(0));
char rowC = s.charAt(1);
if (colC < 'A' || colC > 'F') return null;
if (rowC < '1' || rowC > '6') return null;
return new int[]{rowC - '1', colC - 'A'};
}
/** Convertit des coordonnées internes en notation "A1"-"F6". */
String stringFromCell(int row, int col) {
return "" + (char)('A' + col) + (char)('1' + row);
}
private boolean belongsToPlayer(int piece, String player) {
if ("blanc".equals(player)) return piece == WHITE_LICORNE || piece == WHITE_PALADIN;
if ("noir".equals(player)) return piece == BLACK_LICORNE || piece == BLACK_PALADIN;
return false;
}
private String opponent(String player) {
return "blanc".equals(player) ? "noir" : "blanc";
}
/**
* Retourne les deux lignes (0-indexé) que doit utiliser blanc,
* sachant que noir a choisi {@code bRows}.
* Noir sur {0,1} → blanc sur {4,5} ; noir sur {4,5} → blanc sur {0,1}.
*/
private int[] complementaryRows(int[] bRows) {
return (bRows[0] == 0) ? new int[]{4, 5} : new int[]{0, 1};
}
// =========================================================================
// Affichage
// =========================================================================
/** Affiche le plateau en console (ligne 6 en haut). */
public void printBoard() {
System.out.println(" A B C D E F liseré");
for (int r = 5; r >= 0; r--) {
System.out.print((r + 1) + " [ ");
for (int c = 0; c < 6; c++) System.out.print(pieceToChar(board[r][c]) + " ");
System.out.print("] " + (r + 1) + " |");
for (int c = 0; c < 6; c++) System.out.print(" " + TILE_MAP[r][c]);
System.out.println();
}
System.out.println("lastTileType=" + lastTileType
+ " currentPlayer=" + currentPlayer + "\n");
}
// =========================================================================
// Main de démonstration
// =========================================================================
public static void main(String[] args) throws IOException {
System.out.println("=========================================");
System.out.println(" Demo EscampeBoard ");
System.out.println("=========================================\n");
// ── Placements utilisés dans plusieurs scenarios ──────────────────
// Noir : lignes 5-6 (rows 4-5) — licorne en A6, paladins en B6 C6 D5 E5 F5
final String NOIR_PL = "A6/B6/C6/D5/E5/F5";
// Blanc : lignes 1-2 (rows 0-1) — licorne en D2, paladins en A1 B1 C1 E1 F2
final String BLANC_PL = "D2/A1/B1/C1/E1/F2";
// ─────────────────────────────────────────────────────────────────
// 1. PHASE DE PLACEMENT
// ─────────────────────────────────────────────────────────────────
System.out.println("=== 1. PHASE DE PLACEMENT ===");
EscampeBoard b = new EscampeBoard();
// Tentatives invalides avant le placement normal
System.out.println("Blanc tente de placer avant noir : "
+ b.isValidMove(BLANC_PL, "blanc") + " (attendu: false)");
System.out.println("Noir placement au milieu du plateau : "
+ b.isValidMove("A3/B3/C3/D3/E3/F3", "noir") + " (attendu: false)");
System.out.println("Noir placement sur deux bords diff. : "
+ b.isValidMove("A1/B1/C1/D5/E5/F5", "noir") + " (attendu: false)");
// Placement valide de noir
System.out.println("\nNoir place : " + NOIR_PL
+ " valid=" + b.isValidMove(NOIR_PL, "noir"));
b.play(NOIR_PL, "noir");
System.out.println(" blackPlaced=" + b.blackPlaced
+ " blackRows=[" + b.blackRows[0] + "," + b.blackRows[1] + "]"
+ " currentPlayer=" + b.currentPlayer);
// Placement valide de blanc
System.out.println("Blanc place : " + BLANC_PL
+ " valid=" + b.isValidMove(BLANC_PL, "blanc"));
b.play(BLANC_PL, "blanc");
System.out.println(" whitePlaced=" + b.whitePlaced
+ " currentPlayer=" + b.currentPlayer);
b.printBoard();
// ─────────────────────────────────────────────────────────────────
// 2. PHASE REGULIERE — contrainte de liseré
// ─────────────────────────────────────────────────────────────────
System.out.println("=== 2. PHASE REGULIERE ===");
System.out.println("lastTileType=" + b.lastTileType
+ " (pas de contrainte pour le premier coup)\n");
// Blanc joue en premier, pas de contrainte
String[] bMoves = b.possiblesMoves("blanc");
System.out.println("Coups possibles pour blanc : " + bMoves.length + " coups");
System.out.printf("Exemples : %s %s %s%n",
bMoves[0],
bMoves.length > 1 ? bMoves[1] : "",
bMoves.length > 2 ? bMoves[2] : "");
String m1 = bMoves[0];
System.out.println("\nBlanc joue : " + m1 + " valid=" + b.isValidMove(m1, "blanc"));
b.play(m1, "blanc");
System.out.println(" lastTileType=" + b.lastTileType
+ " (liseré de la case d'arrivée = contrainte pour noir)"
+ " currentPlayer=" + b.currentPlayer);
// Tentative invalide : blanc rejoue hors de son tour
System.out.println("\nBlanc rejoue hors tour : "
+ b.isValidMove(m1, "blanc") + " (attendu: false)");
// Tentative invalide : noir joue depuis un mauvais liseré
String badNoirMove = findMoveFromWrongTile(b, "noir");
if (badNoirMove != null) {
System.out.println("Noir depuis mauvais liseré (" + badNoirMove + ") : "
+ b.isValidMove(badNoirMove, "noir") + " (attendu: false)");
}
// Coup valide de noir
String[] nMoves = b.possiblesMoves("noir");
System.out.println("\nCoups possibles pour noir (liseré " + b.lastTileType + ") : "
+ nMoves.length + " coups");
String m2 = nMoves[0];
System.out.println("Noir joue : " + m2 + " valid=" + b.isValidMove(m2, "noir"));
b.play(m2, "noir");
System.out.println(" lastTileType=" + b.lastTileType
+ " currentPlayer=" + b.currentPlayer);
// ─────────────────────────────────────────────────────────────────
// 3. ROUND-TRIP FICHIER
// ─────────────────────────────────────────────────────────────────
System.out.println("\n=== 3. ROUND-TRIP FICHIER ===");
b.saveToFile("escampe_save.txt");
System.out.println("Sauvegardé dans escampe_save.txt");
EscampeBoard b2 = new EscampeBoard();
b2.setFromFile("escampe_save.txt");
System.out.println("Rechargé : lastTileType=" + b2.lastTileType
+ " currentPlayer=" + b2.currentPlayer);
System.out.println("Plateaux identiques : " + Arrays.deepEquals(b.board, b2.board));
System.out.println("lastTileType identique : " + (b.lastTileType == b2.lastTileType));
System.out.println("currentPlayer identique : " + b.currentPlayer.equals(b2.currentPlayer));
// ─────────────────────────────────────────────────────────────────
// 4. SCENARIO DE PASS (E)
// ─────────────────────────────────────────────────────────────────
System.out.println("\n=== 4. SCENARIO DE PASS ===");
EscampeBoard bPass = new EscampeBoard();
bPass.play(NOIR_PL, "noir");
bPass.play(BLANC_PL, "blanc");
// Forcer une situation où noir n'a aucun coup :
// lastTileType=2, mais toutes les pièces noires sont sur liseré 1 ou 3.
for (int r = 0; r < 6; r++) Arrays.fill(bPass.board[r], EMPTY);
bPass.board[0][3] = WHITE_LICORNE; // D1 liseré=3
bPass.board[0][0] = WHITE_PALADIN; // A1 liseré=1
bPass.board[0][4] = WHITE_PALADIN; // E1 liseré=1
bPass.board[5][0] = BLACK_LICORNE; // A6 liseré=3
bPass.board[4][4] = BLACK_PALADIN; // E5 liseré=1
bPass.board[4][2] = BLACK_PALADIN; // C5 liseré=1
bPass.lastTileType = 2; // blanc vient de poser sur liseré 2
bPass.currentPlayer = "noir";
System.out.println("Pièces noires sur liserés 1 et 3, contrainte = 2");
System.out.println("possiblesMoves(noir) = "
+ Arrays.toString(bPass.possiblesMoves("noir")) + " (attendu: [E])");
System.out.println("isValidMove(E, noir) = "
+ bPass.isValidMove("E", "noir") + " (attendu: true)");
System.out.println("isValidMove(E, blanc) = "
+ bPass.isValidMove("E", "blanc") + " (attendu: false, pas son tour)");
bPass.play("E", "noir");
System.out.println("Après pass : lastTileType=" + bPass.lastTileType
+ " (attendu: -1) currentPlayer=" + bPass.currentPlayer);
// ─────────────────────────────────────────────────────────────────
// 5. CAPTURE ET FIN DE PARTIE
// ─────────────────────────────────────────────────────────────────
System.out.println("\n=== 5. CAPTURE ET FIN DE PARTIE ===");
EscampeBoard bCap = new EscampeBoard();
bCap.play(NOIR_PL, "noir");
bCap.play(BLANC_PL, "blanc");
// Mise en scène :
// - Blanc paladin en B1 (row=0,col=1 ; liseré=2)
// → 2 pas orthogonaux : B1 -> B2 -> B3
// - Licorne noire en B3 (row=2,col=1) ; case B2 vide
// - lastTileType=2 → blanc peut jouer depuis B1
for (int r = 0; r < 6; r++) Arrays.fill(bCap.board[r], EMPTY);
bCap.board[0][1] = WHITE_PALADIN; // B1 liseré=2
bCap.board[0][3] = WHITE_LICORNE; // D1 (garde-fou : licorne blanche présente)
bCap.board[2][1] = BLACK_LICORNE; // B3
bCap.board[5][5] = BLACK_PALADIN; // F6 (présence de pièce noire restante)
bCap.lastTileType = 2;
bCap.currentPlayer = "blanc";
System.out.println("Avant capture :");
bCap.printBoard();
System.out.println("gameOver = " + bCap.gameOver() + " (attendu: false)");
// Coup invalide : un pas seulement (B1->B2), pas assez de cases
System.out.println("Coup B1-B2 (1 pas, manque 1) : "
+ bCap.isValidMove("B1-B2", "blanc") + " (attendu: false)");
// Coup valide : deux pas (B1->B2->B3), B2 vide, B3 = licorne noire
System.out.println("Coup B1-B3 (2 pas, capture) : "
+ bCap.isValidMove("B1-B3", "blanc") + " (attendu: true)");
bCap.play("B1-B3", "blanc");
System.out.println("Après capture :");
bCap.printBoard();
System.out.println("gameOver = " + bCap.gameOver() + " (attendu: true)");
System.out.println("Blanc gagne !");
System.out.println("\n=========================================");
System.out.println(" Demo terminee ");
System.out.println("=========================================");
}
/**
* Utilitaire pour la démo : trouve un coup depuis une pièce
* de {@code player} dont le liseré est différent de {@code lastTileType}.
* Retourne null si aucune telle pièce n'a de destinations.
*/
private static String findMoveFromWrongTile(EscampeBoard b, String player) {
for (int r = 0; r < 6; r++) {
for (int c = 0; c < 6; c++) {
if (!b.belongsToPlayer(b.board[r][c], player)) continue;
if (TILE_MAP[r][c] == b.lastTileType) continue;
Set<String> reach = b.getReachableSquares(r, c, player);
if (!reach.isEmpty()) {
String dest = reach.iterator().next();
String[] parts = dest.split(",");
return b.stringFromCell(r, c) + "-"
+ b.stringFromCell(Integer.parseInt(parts[0]),
Integer.parseInt(parts[1]));
}
}
}
return null;
}
}

View File

@@ -0,0 +1,65 @@
package escampe;
/**
* Voici l'interface abstraite qu'il suffit d'implanter pour jouer. Ensuite, vous devez utiliser
* ClientJeu en lui donnant le nom de votre classe pour qu'il la charge et se connecte au serveur.
*
* @author L. Simon (Univ. Paris-Sud)- 2006-2013
*
*/
public interface IJoueur {
// Mais pas lors de la conversation avec l'arbitre (méthodes initJoueur et getNumJoueur)
// Vous pouvez changer cela en interne si vous le souhaitez
static final int BLANC = -1;
static final int NOIR = 1;
/**
* L'arbitre vient de lancer votre joueur. Il lui informe par cette méthode que vous devez jouer
* dans cette couleur. Vous pouvez utiliser cette m?thode abstraite, ou la méthode constructeur
* de votre classe, pour initialiser vos structures.
*
* @param mycolour
* La couleur dans laquelle vous allez jouer (-1=BLANC, 1=NOIR)
*/
public void initJoueur(int mycolour);
// Doit retourner l'argument passé par la fonction ci-dessus (constantes BLANC ou NOIR)
public int getNumJoueur();
/**
* C'est ici que vous devez faire appel à votre IA pour trouver le meilleur coup à jouer sur le
* plateau courant.
*
* @return une chaine décrivant le mouvement. Cette chaine doit être décrite exactement comme
* sur l'exemple : String msg = "" + positionInitiale + "-" +positionFinale + ""; ou "PASSE";
* Chaque position contient une lettre et un num?ro, par exemple:A1,B2 (coup "A1-B2")
*/
public String choixMouvement();
/**
* Méthode appelée par l'arbitre pour désigner le vainqueur. Vous pouvez en profiter pour
* imprimer une bannière de joie... Si vous gagnez...
*
* @param colour
* La couleur du gagnant (BLANC=-1, NOIR=1).
*/
public void declareLeVainqueur(int colour);
/**
* On suppose que l'arbitre a vérifié que le mouvement ennemi était bien légal. Il vous informe
* du mouvement ennemi. A vous de répercuter ce mouvement dans vos structures. Comme par exemple
* éliminer les pions que ennemi vient de vous prendre par ce mouvement. Il n'est pas nécessaire
* de réfléchir déjà à votre prochain coup à jouer : pour cela l'arbitre appelera ensuite
* choixMouvement().
*
* @param coup
* une chaine décrivant le mouvement: par exemple: "A1-B2"
*/
public void mouvementEnnemi(String coup);
public String binoName();
}

View File

@@ -0,0 +1,117 @@
package escampe;
/**
* Joueur du tournoi (Puyaubreau / Russac). Enveloppe un {@link EscampeBoard}
* tenu à jour à chaque coup et délègue la décision à {@link Moteur}.
*
* L'interface {@code IJoueur} parle en entiers ({@code NOIR=1}, {@code BLANC=-1})
* et place les pièces via le même canal que les coups : le premier
* {@code choixMouvement} renvoie un placement, les suivants des coups. Le pass
* se note {@code "E"} (et non {@code "PASSE"}, contrairement au Javadoc d'IJoueur).
*/
public class JoueurPuyaubreauRussac implements IJoueur {
private int couleur = NOIR;
private EscampeBoard board;
private final Moteur moteur = new Moteur();
// Budget de temps : enveloppe sous la limite arbitre de 300 s, fraction du
// temps restant par coup. Surchargeable par -Descampe.* pour les tests.
private static final long BUDGET_MS = Long.getLong("escampe.budgetMs", 280_000);
private static final long MAX_SLICE_MS = Long.getLong("escampe.maxSliceMs", 6_000);
private static final long MIN_SLICE_MS = 120;
private static final int TIME_DIVISOR = 12;
private static final boolean DEBUG = Boolean.getBoolean("escampe.debug");
private long usedMs = 0;
@Override
public void initJoueur(int mycolour) {
couleur = mycolour;
board = new EscampeBoard();
}
@Override
public int getNumJoueur() {
return couleur;
}
@Override
public String binoName() {
return "Puyaubreau_Russac";
}
private String myStr() { return couleur == NOIR ? "noir" : "blanc"; }
private String oppStr() { return couleur == NOIR ? "blanc" : "noir"; }
@Override
public String choixMouvement() {
if (board.gameOver()) return "xxxxx"; // fin de partie sous Solo ; l'arbitre, lui, n'appelle plus
if (couleur == NOIR && !board.blackPlaced) {
String pl = placement(new int[]{0, 1});
board.play(pl, "noir");
return pl;
}
if (couleur == BLANC && !board.whitePlaced) {
String pl = placement(complementaryRows(board.blackRows));
board.play(pl, "blanc");
return pl;
}
String move = chooseMove();
board.play(move, myStr());
return move;
}
@Override
public void mouvementEnnemi(String coup) {
if (coup == null) return;
coup = coup.trim();
if (coup.isEmpty() || coup.equals("xxxxx")) return;
try {
board.play(coup, oppStr());
} catch (RuntimeException e) {
// L'arbitre garantit la légalité ; on ne plante pas sur une désync.
System.err.println("[" + binoName() + "] coup ennemi rejeté : " + coup);
}
}
@Override
public void declareLeVainqueur(int colour) {
if (colour == couleur) System.out.println("[" + binoName() + "] Victoire !");
else if (colour == -couleur) System.out.println("[" + binoName() + "] Défaite.");
}
/** Temps alloué au moteur pour ce coup, puis appel de la recherche. */
private String chooseMove() {
long remaining = BUDGET_MS - usedMs;
long slice = Math.max(MIN_SLICE_MS, Math.min(remaining / TIME_DIVISOR, MAX_SLICE_MS));
if (remaining < 1500) slice = Math.max(40, remaining - 300);
long t0 = System.currentTimeMillis();
int m = moteur.bestMove(board, couleur == NOIR, slice);
usedMs += System.currentTimeMillis() - t0;
if (DEBUG) {
System.err.printf("[%s] %s prof=%d score=%d noeuds=%d cumul=%ds%n",
binoName(), board.moveToString(m), moteur.reachedDepth, moteur.lastScore,
moteur.nodes, usedMs / 1000);
}
return board.moveToString(m);
}
private int[] complementaryRows(int[] blackRows) {
return blackRows[0] == 0 ? new int[]{4, 5} : new int[]{0, 1};
}
/**
* Placement : licorne dans un coin, ses deux voisines occupées par des
* paladins (la licorne devient incapturable), les trois autres paladins sur
* des liserés 1/2/3 distincts pour ne jamais être contraint de passer.
*/
private String placement(int[] rows) {
boolean bottom = Math.min(rows[0], rows[1]) == 0;
return bottom ? "A1/A2/B1/E1/F1/C2" // coin A1, murs A2/B1, mobiles E1(1)/F1(2)/C2(3)
: "A6/A5/B6/C5/F5/E6"; // coin A6, murs A5/B6, mobiles C5(1)/F5(2)/E6(3)
}
}

View File

@@ -0,0 +1,137 @@
package escampe;
/**
* Recherche du meilleur coup : negamax + élagage alpha-bêta + approfondissement
* itératif sous limite de temps. La recherche se fait sur une copie du plateau,
* via makeInt/unmakeInt (sans allocation). Capturer la licorne adverse vaut
* {@code WIN - ply} (gagner vite plutôt que tard).
*/
final class Moteur {
static final int WIN = 1_000_000;
static final int INF = 2_000_000;
static final int MAX_DEPTH = 40;
private static final int MAX_PLY = MAX_DEPTH + 8;
// Poids de l'évaluation (proximité paladins/licornes : attaque vs défense).
int wAtkSum = 2, wDefSum = 2, wAtkMin = 8, wDefMin = 8;
private long deadline;
private boolean timedOut;
long nodes;
int reachedDepth;
int lastScore;
private final int[][] buf = new int[MAX_PLY][256]; // un buffer de coups par profondeur
int bestMove(EscampeBoard root, boolean black, long budgetMs) {
EscampeBoard pos = root.copy();
deadline = System.currentTimeMillis() + Math.max(1, budgetMs);
nodes = 0; timedOut = false; reachedDepth = 0; lastScore = 0;
int[] moves = new int[256];
int n = pos.genMovesIntInto(black, moves);
if (n == 0 || moves[0] == EscampeBoard.MOVE_PASS) return EscampeBoard.MOVE_PASS;
orderCapturesFirst(pos, moves, n, black);
int best = moves[0];
for (int depth = 1; depth <= MAX_DEPTH; depth++) {
int alpha = -INF, bestScore = -INF, bestThis = moves[0];
boolean complete = true;
for (int i = 0; i < n; i++) {
EscampeBoard.Undo u = pos.makeInt(moves[i]);
int sc = isCapture(u, black) ? WIN - 1 : -negamax(pos, depth - 1, -INF, -alpha, !black, 1);
pos.unmakeInt(u);
if (timedOut) { complete = false; break; }
if (sc > bestScore) { bestScore = sc; bestThis = moves[i]; }
if (sc > alpha) alpha = sc;
}
if (!complete) break; // profondeur interrompue : on garde la précédente
best = bestThis;
reachedDepth = depth;
lastScore = bestScore;
moveToFront(moves, n, best); // ordonne l'itération suivante
if (bestScore >= WIN - 64) break;
}
return best;
}
private int negamax(EscampeBoard pos, int depth, int alpha, int beta, boolean black, int ply) {
if ((++nodes & 2047) == 0 && System.currentTimeMillis() >= deadline) { timedOut = true; return 0; }
if (depth <= 0) return eval(pos, black);
int[] moves = buf[ply];
int n = pos.genMovesIntInto(black, moves);
if (n == 0) return eval(pos, black);
orderCapturesFirst(pos, moves, n, black);
int bestScore = -INF;
for (int i = 0; i < n; i++) {
EscampeBoard.Undo u = pos.makeInt(moves[i]);
int sc = isCapture(u, black) ? WIN - ply : -negamax(pos, depth - 1, -beta, -alpha, !black, ply + 1);
pos.unmakeInt(u);
if (timedOut) return 0;
if (sc > bestScore) bestScore = sc;
if (bestScore > alpha) alpha = bestScore;
if (alpha >= beta) break;
}
return bestScore;
}
private boolean isCapture(EscampeBoard.Undo u, boolean black) {
return u.captured() == (black ? EscampeBoard.WHITE_LICORNE : EscampeBoard.BLACK_LICORNE);
}
/** Place en tête un coup capturant la licorne adverse, pour une coupure immédiate. */
private void orderCapturesFirst(EscampeBoard pos, int[] moves, int n, boolean black) {
int enemy = black ? EscampeBoard.WHITE_LICORNE : EscampeBoard.BLACK_LICORNE;
for (int i = 0; i < n; i++) {
int to = moves[i] % 36;
if (moves[i] != EscampeBoard.MOVE_PASS && pos.board[to / 6][to % 6] == enemy) {
int t = moves[0]; moves[0] = moves[i]; moves[i] = t;
return;
}
}
}
private void moveToFront(int[] moves, int n, int target) {
for (int i = 0; i < n; i++) {
if (moves[i] == target) { int t = moves[0]; moves[0] = moves[i]; moves[i] = t; return; }
}
}
private int eval(EscampeBoard pos, boolean black) {
int adv = evalBlackAdvantage(pos);
return black ? adv : -adv;
}
/** Avantage de Noir : nos paladins proches de la licorne adverse, les leurs loin de la nôtre. */
private int evalBlackAdvantage(EscampeBoard pos) {
int[][] b = pos.board;
int blr = -1, blc = -1, wlr = -1, wlc = -1;
for (int r = 0; r < 6; r++)
for (int c = 0; c < 6; c++) {
int p = b[r][c];
if (p == EscampeBoard.BLACK_LICORNE) { blr = r; blc = c; }
else if (p == EscampeBoard.WHITE_LICORNE) { wlr = r; wlc = c; }
}
if (wlr < 0) return WIN;
if (blr < 0) return -WIN;
int atkSum = 0, defSum = 0, atkMin = 99, defMin = 99;
for (int r = 0; r < 6; r++)
for (int c = 0; c < 6; c++) {
int p = b[r][c];
if (p == EscampeBoard.BLACK_PALADIN) {
int d = Math.abs(r - wlr) + Math.abs(c - wlc);
atkSum += 10 - d;
if (d < atkMin) atkMin = d;
} else if (p == EscampeBoard.WHITE_PALADIN) {
int d = Math.abs(r - blr) + Math.abs(c - blc);
defSum += 10 - d;
if (d < defMin) defMin = d;
}
}
return wAtkSum * atkSum - wDefSum * defSum + wAtkMin * (10 - atkMin) - wDefMin * (10 - defMin);
}
}

View File

@@ -0,0 +1,45 @@
package escampe;
public interface Partie1 {
/**
* Initialise un plateau à partir d'un fichier texte.
* @param fileName le nom du fichier à lire
*/
public void setFromFile(String fileName);
/**
* Sauve la configuration de l'état courant (plateau et pièces restantes) dans un fichier.
* @param fileName le nom du fichier à sauvegarder
* Le format doit être compatible avec celui utilisé pour la lecture.
*/
public void saveToFile(String fileName);
/**
* Indique si le coup {@code move} est valide pour le joueur {@code player} sur le plateau courant.
* @param move le coup à jouer,
* sous la forme "B1-D1" en général,
* sous la forme "C6/A6/B5/D5/E6/F5" pour le coup qui place les pièces,
* ou "E" pour passer son tour.
* @param player le joueur qui joue, représenté par "noir" ou "blanc"
*/
public boolean isValidMove(String move, String player);
/**
* Calcule les coups possibles pour le joueur {@code player} sur le plateau courant.
* @param player le joueur qui joue, représenté par "noir" ou "blanc"
*/
public String[] possiblesMoves(String player);
/**
* Modifie le plateau en jouant le coup {@code move} pour le joueur {@code player}.
* @param move le coup à jouer, sous la forme "C1-D1" ou "C6/A6/B5/D5/E6/F5"
* @param player le joueur qui joue, représenté par "noir" ou "blanc"
*/
public void play(String move, String player);
/**
* Retourne vrai lorsque le plateau correspond à une fin de partie.
*/
public boolean gameOver();
}

View File

@@ -0,0 +1,143 @@
package escampe;
import java.util.*;
/**
* Tests directs des règles du jeu : compte de pas selon le liseré, capture au
* dernier pas uniquement, paladins imprenables, interdiction de traverser une
* case occupée, contrainte de liseré, pass forcé, fin de partie, zones de placement.
*/
public class RulesTest {
static int pass = 0, fail = 0;
static void check(boolean cond, String name) {
if (cond) pass++;
else { fail++; System.out.println(" ÉCHEC : " + name); }
}
static boolean has(Set<String> s, int r, int c) { return s.contains(r + "," + c); }
public static void main(String[] args) {
stepCount();
captureAndBlocking();
lisereConstraint();
forcedPass();
gameOver();
placementZones();
System.out.println("\nRulesTest : " + pass + " OK, " + fail + " échec(s).");
if (fail > 0) System.exit(1);
}
/** Le nombre de pas est exactement le liseré de la case de départ. */
static void stepCount() {
EscampeBoard b = new EscampeBoard();
b.board[2][2] = EscampeBoard.WHITE_PALADIN; // C3, liseré 1
Set<String> r = b.getReachableSquares(2, 2, "blanc");
check(r.size() == 4 && has(r,1,2) && has(r,3,2) && has(r,2,1) && has(r,2,3),
"liseré 1 (centre) → exactement les 4 voisins orthogonaux");
b = new EscampeBoard();
b.board[2][3] = EscampeBoard.WHITE_PALADIN; // D3, liseré 2
r = b.getReachableSquares(2, 3, "blanc");
check(r.size() == 8
&& has(r,0,3) && has(r,4,3) && has(r,2,1) && has(r,2,5)
&& has(r,1,2) && has(r,1,4) && has(r,3,2) && has(r,3,4),
"liseré 2 (centre) → les 8 cases à distance 2");
b = new EscampeBoard();
b.board[3][2] = EscampeBoard.WHITE_PALADIN; // C4, liseré 3
r = b.getReachableSquares(3, 2, "blanc");
check(has(r,0,2), "liseré 3 atteint (0,2) à 3 pas en ligne droite");
check(!has(r,1,2), "liseré 3 n'atteint PAS (1,2) (mauvaise parité : 3 pas)");
check(has(r,2,2) && has(r,3,3), "liseré 3 atteint des cases à distance 1 (zigzag)");
}
/** Capture au dernier pas uniquement ; paladins imprenables ; pas de traversée. */
static void captureAndBlocking() {
EscampeBoard b = new EscampeBoard();
b.board[3][2] = EscampeBoard.WHITE_PALADIN; // C4 liseré 3
b.board[0][2] = EscampeBoard.BLACK_LICORNE; // cible à 3 pas (droit)
Set<String> r = b.getReachableSquares(3, 2, "blanc");
check(has(r,0,2), "capture de la licorne adverse au dernier pas : autorisée");
b = new EscampeBoard();
b.board[3][2] = EscampeBoard.WHITE_PALADIN;
b.board[0][2] = EscampeBoard.BLACK_PALADIN; // paladin sur la case finale
r = b.getReachableSquares(3, 2, "blanc");
check(!has(r,0,2), "paladin imprenable : pas d'arrivée dessus");
b = new EscampeBoard();
b.board[3][2] = EscampeBoard.WHITE_PALADIN;
b.board[1][2] = EscampeBoard.BLACK_PALADIN; // bloque l'unique chemin vers (0,2)
r = b.getReachableSquares(3, 2, "blanc");
check(!has(r,0,2), "interdit de traverser une case occupée");
b = new EscampeBoard();
b.board[3][2] = EscampeBoard.WHITE_PALADIN;
b.board[1][2] = EscampeBoard.BLACK_LICORNE; // licorne à distance 2 (parité ≠)
r = b.getReachableSquares(3, 2, "blanc");
check(!has(r,1,2), "licorne à mauvaise distance : non capturable (compte de pas exact)");
}
/** On ne peut jouer que depuis une case du liseré imposé. */
static void lisereConstraint() {
EscampeBoard b = inPlay();
b.board[2][2] = EscampeBoard.WHITE_LICORNE; // C3 liseré 1
b.board[5][5] = EscampeBoard.BLACK_LICORNE;
b.board[2][3] = EscampeBoard.WHITE_PALADIN; // D3 liseré 2
b.board[0][0] = EscampeBoard.WHITE_PALADIN; // A1 liseré 1
b.lastTileType = 2; // seules les pièces liseré 2 bougent
boolean allLis2 = true;
for (String m : b.possiblesMoves("blanc")) {
int[] from = b.cellFromString(m.substring(0, m.indexOf('-')));
if (EscampeBoard.TILE_MAP[from[0]][from[1]] != 2) allLis2 = false;
}
check(allLis2, "contrainte de liseré : tous les coups partent d'une case liseré 2");
}
/** Pass autorisé seulement si aucune pièce ne peut jouer le liseré imposé. */
static void forcedPass() {
EscampeBoard b = inPlay();
b.board[0][0] = EscampeBoard.WHITE_LICORNE; // A1 liseré 1
b.board[5][5] = EscampeBoard.BLACK_LICORNE;
b.lastTileType = 3; // blanc n'a aucune pièce liseré 3
String[] mv = b.possiblesMoves("blanc");
check(mv.length == 1 && mv[0].equals("E"), "aucune pièce sur le liseré → pass forcé");
check(b.isValidMove("E", "blanc"), "E valide quand bloqué");
b.lastTileType = 1; // la licorne A1 (liseré 1) peut bouger
String[] mv2 = b.possiblesMoves("blanc");
check(mv2.length >= 1 && !mv2[0].equals("E"), "des coups existent → pas de pass");
check(!b.isValidMove("E", "blanc"), "E invalide si des coups existent");
}
static void gameOver() {
EscampeBoard b = inPlay();
b.board[0][0] = EscampeBoard.WHITE_LICORNE;
b.board[5][5] = EscampeBoard.BLACK_LICORNE;
check(!b.gameOver(), "deux licornes présentes → partie en cours");
b.board[5][5] = EscampeBoard.EMPTY;
check(b.gameOver(), "une licorne manquante → fin de partie");
check(!new EscampeBoard().gameOver(), "avant placement → jamais fini");
}
/** Placement : zones autorisées et complémentarité noir/blanc. */
static void placementZones() {
EscampeBoard b = new EscampeBoard();
check(!b.isValidMove("A3/B3/C3/D3/E3/F3", "noir"), "placement noir au centre : refusé");
check(b.isValidMove("A1/A2/B1/E1/F1/C2", "noir"), "placement noir sur 2 lignes du bord : accepté");
b.play("A1/A2/B1/E1/F1/C2", "noir");
check(b.isValidMove("A6/A5/B6/C5/F5/E6", "blanc"), "placement blanc complémentaire (haut) : accepté");
check(!b.isValidMove("A1/A2/B1/E1/F1/D1", "blanc"), "placement blanc du même côté que noir : refusé");
}
/** Plateau vide « en jeu » (les deux placements faits), à remplir à la main. */
static EscampeBoard inPlay() {
EscampeBoard b = new EscampeBoard();
b.blackPlaced = true;
b.whitePlaced = true;
b.currentPlayer = "blanc";
b.lastTileType = -1;
return b;
}
}

View File

@@ -0,0 +1,183 @@
package escampe;
import java.util.Date;
import javax.swing.JFrame;
/**
* Petite Classe toute simple qui vous montre comment on peut lancer une partie sur deux IJoueurs...
* Cela vous servira a debugger facilement votre projet en conditions presque reelles de tournoi
*
* Attention, l'arbitre n'est pas lancé dessus, mais comme il s'agit de deux IJoueur à vous il n'est
* pas nécessaire de vérifier la validité des coups (bien entendu)
*
* Par contre, comme rien ne vérifie la fin de partie (pas d'arbitre), vos IJoueur devront renvoyer
* la chaine "xxxxx" pour dire que la partie est finie.
*
* Cette classe n'affiche rien : elle se contente de donner la main alternativement aux deux
* joueurs.
*
* 2008-2012
*/
public class Solo {
private static IJoueur joueurBlanc;
private static IJoueur joueurNoir;
// Ne pas modifier ces constantes, elles seront utilisees par l'arbitre
private final static int BLANC = -1;
private final static int NOIR = 1;
private static int nbCoups = 0;
/*// Par défaut, on a une applet graphique
static boolean APPLETGRAPHIQUE = true;
// applet game viewer
static private Applet vueDuJeu;
static private JFrame f = null;*/
/**
* Pour éviter de toujours envoyer des lignes de commandes, vous pouvez renvoyer automatiquement
* dans cette méthode votre joueur par défaut. Attention, il faut bien remplir le return new
* VOTREJOUEUR() pour que cela fonctionne la classe implantee renvoyee doit implanter
* l'interface IJoueur...
*
* @param s
* @return Ijoueur un joueur demande
*/
private static IJoueur getDefaultPlayer(String s) {
System.out.println(s + " : defaultPlayer");
// vous devez faire qq chose comme return new MonMeilleurJoueur();
// JoueurAleatoire vit dans escampeobf.jar (interface obfusquée) : on ne peut
// pas le référencer ici à la compilation. On renvoie donc notre propre joueur.
return new JoueurPuyaubreauRussac();
}
/**
* Juste pour rendre le tout plus generique, et vous donner une idee de comment le tournoi sera
* lance automatiquement, voici une methode permettant de charger une certaine classe implantant
* un IJoueur
*
* @param classeJoueur
* @param s
* @return la classe chargee dynamiquement
*/
private static IJoueur loadNamedPlayer(String classeJoueur, String s) {
IJoueur joueur;
System.out.print(s + " : Chargement de la classe joueur " + classeJoueur + "... ");
try {
Class<?> cjoueur = Class.forName(classeJoueur);
joueur = (IJoueur) cjoueur.newInstance();
}
catch (Exception e) {
System.out.println("Erreur de chargement");
System.out.println(e);
return null;
}
System.out.println("Ok");
return joueur;
}
/**
* Boucle principale du jeu, en utilisant une version de l'arbitre identique a celle du tournoi
* L'arbitre sera le garant de la validite des coups, et de leur affichage standard pour la
* publication via le site web.
*
* @param joueurBlanc
* @param joueurNoir
*/
public static void gameLoop(IJoueur joueurBlanc, IJoueur joueurNoir) {
String coup;
boolean partieFinie = false;
IJoueur joueurCourant = joueurNoir; // Dans Escampe le joueur Noir commence
while (!partieFinie) {
nbCoups++;
System.out.println("\n*********\nOn demande à " + joueurCourant.binoName() + " de jouer...");
long waitingTime1 = new Date().getTime();
coup = joueurCourant.choixMouvement();
long waitingTime2 = new Date().getTime();
// On rajoute 1 pour eliminer les temps infinis
long waitingTime = waitingTime2 - waitingTime1 + 1;
System.out.println("Le joueur " + joueurCourant.binoName() + " a joué le coup " + coup + " en " + waitingTime + "s.");
try {
Thread.sleep(1); // Juste pour attendre un peu
}
catch (InterruptedException e) {
}
if (coup.compareTo("xxxxx") == 0)
partieFinie = true;
else if (nbCoups == 2) { // Dans Escampe le joueur Blanc rejoue après avoir posé ses pièces
// On avertit le joueur Noir du placement des pièces
joueurNoir.mouvementEnnemi(coup);
}
else {
if (joueurCourant.getNumJoueur() == BLANC)
joueurCourant = joueurNoir;
else
joueurCourant = joueurBlanc;
// On avertit le second joueur du coup calcule par le precedent
joueurCourant.mouvementEnnemi(coup);
// Ce sera ensuite à lui de jouer de nouveau en haut de la boucle
}
}
System.out.println("Partie finie en " + nbCoups + " coups.\n");
}
/**
* On charge eventuellement les classes demandee pour les joueurs, et on lance la boucle
* principale
*
* @param args
*/
public static void main(String args[]) {
/*// S'il le faut, on initialise l'applet graphique
if (APPLETGRAPHIQUE) {
f = new JFrame("Vue du jeu");
vueDuJeu = new Applet();
vueDuJeu.buildUI(f.getContentPane());
f.setSize(vueDuJeu.getDimension());
vueDuJeu.setMyFrame(f);
f.setVisible(true);
vueDuJeu.addBoard("Départ ", plateau);
vueDuJeu.update(f.getGraphics(), f.getInsets());
}*/
System.out.println("Partie solo ...");
if (args.length == 0) { // On a deux classes à charger
joueurBlanc = getDefaultPlayer("Blanc");
joueurNoir = getDefaultPlayer("Noir");
}
else if (args.length == 2) { // On a deux classes à charger
joueurBlanc = getDefaultPlayer("Blanc");
joueurNoir = getDefaultPlayer("Noir");
}
else if (args.length == 3) {
joueurBlanc = loadNamedPlayer(args[0], "Blanc");
joueurNoir = loadNamedPlayer(args[0], "Noir");
}
else if (args.length == 4) {
joueurBlanc = loadNamedPlayer(args[0], "Blanc");
joueurNoir = loadNamedPlayer(args[1], "Noir");
}
joueurBlanc.initJoueur(BLANC);
System.out.println("Joueur Blanc : " + joueurBlanc.binoName());
joueurNoir.initJoueur(NOIR);
System.out.println("Joueur Noir : " + joueurNoir.binoName());
System.out.println("Initialisation des deux joueurs ok.");
gameLoop(joueurBlanc, joueurNoir);
}
}

View File

@@ -0,0 +1,121 @@
package escampe;
import java.util.*;
/**
* Cross-vérifie le chemin « int » du moteur contre le chemin « String » vérifié,
* sur des milliers de parties aléatoires : mêmes coups que possiblesMoves, makeInt
* équivalent à play, unmakeInt qui restaure l'état. Échoue à la moindre divergence.
*/
public class VerifMoves {
static int mismatches = 0;
public static void main(String[] args) {
int games = args.length > 0 ? Integer.parseInt(args[0]) : 3000;
Random rng = new Random(20260530L);
long positions = 0, makeChecks = 0;
for (int g = 0; g < games; g++) {
EscampeBoard b = new EscampeBoard();
// Placements aléatoires légaux.
int[] noirRows = rng.nextBoolean() ? new int[]{0, 1} : new int[]{4, 5};
b.play(randomPlacement(b, "noir", noirRows, rng), "noir");
int[] blancRows = (noirRows[0] == 0) ? new int[]{4, 5} : new int[]{0, 1};
b.play(randomPlacement(b, "blanc", blancRows, rng), "blanc");
for (int ply = 0; ply < 200 && !b.gameOver(); ply++) {
positions++;
// (1) égalité des ensembles de coups, pour les deux couleurs.
checkMoveSets(b, true);
checkMoveSets(b, false);
// Côté au trait : (2) make==play et (3) unmake, sur chaque coup.
boolean black = "noir".equals(b.currentPlayer);
String side = b.currentPlayer;
int[] moves = b.genMovesInt(black);
for (int m : moves) {
makeChecks++;
EscampeBoard after = b.copy();
EscampeBoard.Undo u = after.makeInt(m);
EscampeBoard ref = b.copy();
ref.play(b.moveToString(m), side);
if (!sameState(after, ref)) {
report(b, "make!=play pour " + b.moveToString(m) + " (" + side + ")");
}
after.unmakeInt(u);
if (!sameState(after, b)) {
report(b, "unmake ne restaure pas pour " + b.moveToString(m));
}
}
if (mismatches > 0) { dumpAndExit(); }
// Avance la partie d'un coup aléatoire (chemin String vérifié).
if (moves.length == 1 && moves[0] == EscampeBoard.MOVE_PASS) {
b.play("E", side);
} else {
int m = moves[rng.nextInt(moves.length)];
b.play(b.moveToString(m), side);
}
}
}
System.out.println("Parties : " + games);
System.out.println("Positions testées : " + positions);
System.out.println("make/unmake testés: " + makeChecks);
System.out.println(mismatches == 0
? "RÉSULTAT : OK — chemin int ≡ chemin String vérifié (0 divergence)."
: "RÉSULTAT : " + mismatches + " DIVERGENCES !");
if (mismatches != 0) System.exit(1);
}
/** Compare genMovesInt(black) et possiblesMoves(player) comme ensembles. */
static void checkMoveSets(EscampeBoard b, boolean black) {
String player = black ? "noir" : "blanc";
Set<String> fromInt = new TreeSet<>();
for (int m : b.genMovesInt(black)) fromInt.add(b.moveToString(m));
Set<String> fromStr = new TreeSet<>(Arrays.asList(b.possiblesMoves(player)));
if (!fromInt.equals(fromStr)) {
report(b, "ensembles différents pour " + player
+ "\n int = " + fromInt + "\n str = " + fromStr);
}
}
static boolean sameState(EscampeBoard a, EscampeBoard c) {
if (a.lastTileType != c.lastTileType) return false;
if (!a.currentPlayer.equals(c.currentPlayer)) return false;
for (int r = 0; r < 6; r++)
for (int col = 0; col < 6; col++)
if (a.board[r][col] != c.board[r][col]) return false;
return true;
}
static void report(EscampeBoard b, String msg) {
if (mismatches < 5) {
System.out.println("DIVERGENCE : " + msg);
System.out.println(" lastTileType=" + b.lastTileType + " currentPlayer=" + b.currentPlayer);
}
mismatches++;
}
static void dumpAndExit() {
System.out.println(">>> arrêt sur première divergence.");
System.exit(1);
}
/** Placement aléatoire légal : 6 cases distinctes sur les 2 lignes, licorne en tête. */
static String randomPlacement(EscampeBoard b, String player, int[] rows, Random rng) {
List<int[]> cells = new ArrayList<>();
for (int r : rows) for (int c = 0; c < 6; c++) cells.add(new int[]{r, c});
for (int tries = 0; tries < 100; tries++) {
Collections.shuffle(cells, rng);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 6; i++) {
if (i > 0) sb.append('/');
sb.append((char) ('A' + cells.get(i)[1])).append((char) ('1' + cells.get(i)[0]));
}
String pl = sb.toString();
if (b.isValidMove(pl, player)) return pl;
}
throw new IllegalStateException("aucun placement légal trouvé");
}
}

BIN
dist/Puyaubreau_Russac_rapport.pdf vendored Normal file

Binary file not shown.

3
dist/mainClass vendored Normal file
View File

@@ -0,0 +1,3 @@
jar:Puyaubreau_Russac.jar
clientClass:escampe.ClientJeu
mainClass:escampe.JoueurPuyaubreauRussac

12
escampe_save.txt Normal file
View File

@@ -0,0 +1,12 @@
% Escampe - sauvegarde du plateau
% lastTileType: 1
% currentPlayer: blanc
% blackPlaced: true
% whitePlaced: true
% blackRows: 4,5
06 Nnn--- 06
05 ----nn 05
04 ------ 04
03 ------ 03
02 b--n-b 02
01 -bb-b- 01

29
jouer-vs-IA.bat Normal file
View File

@@ -0,0 +1,29 @@
@echo off
REM ==========================================================================
REM Escampe — jouer (humain) contre notre IA, en local, sur cette machine.
REM Ouvre 3 fenetres : serveur, IA, et VOUS. Jouez dans la fenetre "VOUS".
REM ==========================================================================
REM Jar du serveur : dans le repo (lib\) en priorite, sinon dans Downloads.
set "SERVEUR=%~dp0lib\escampeobf.jar"
if not exist "%SERVEUR%" set "SERVEUR=C:\Users\Kerboul\Downloads\escampeobf.jar"
REM Jar de notre IA (genere par build.sh, chemin relatif a ce .bat) :
set "IA=%~dp0dist\Puyaubreau_Russac.jar"
if not exist "%SERVEUR%" echo [ERREUR] Introuvable : %SERVEUR% & pause & exit /b 1
if not exist "%IA%" echo [ERREUR] Introuvable : %IA% (lancez d'abord build.sh) & pause & exit /b 1
echo Lancement du serveur...
start "Escampe - Serveur" cmd /k java -cp "%SERVEUR%" escampe.ServeurJeu 1234 1
timeout /t 2 >nul
echo Lancement de l'IA (Puyaubreau_Russac)...
start "Escampe - IA" cmd /k java -cp "%IA%" escampe.ClientJeu escampe.JoueurPuyaubreauRussac localhost 1234
timeout /t 1 >nul
echo Lancement de votre client humain...
start "Escampe - VOUS" cmd /k java -cp "%SERVEUR%" escampe.ClientJeu escampe.JoueurHumain localhost 1234
echo.
echo C'est parti ! Jouez dans la fenetre "Escampe - VOUS".
echo (Le serveur ouvre aussi une fenetre graphique du plateau.)

26
jouer-vs-pote.bat Normal file
View File

@@ -0,0 +1,26 @@
@echo off
REM ==========================================================================
REM Escampe — deux HUMAINS sur la MEME machine (3 fenetres).
REM Chaque joueur joue dans sa fenetre "Joueur 1" / "Joueur 2".
REM
REM Pour jouer a DISTANCE avec un pote (2 PC), voir MULTIJOUEUR.md :
REM l'hote lance le serveur, le pote se connecte sur l'IP de l'hote.
REM ==========================================================================
set "SERVEUR=%~dp0lib\escampeobf.jar"
if not exist "%SERVEUR%" set "SERVEUR=C:\Users\Kerboul\Downloads\escampeobf.jar"
if not exist "%SERVEUR%" echo [ERREUR] Introuvable : %SERVEUR% & pause & exit /b 1
echo Lancement du serveur...
start "Escampe - Serveur" cmd /k java -cp "%SERVEUR%" escampe.ServeurJeu 1234 1
timeout /t 2 >nul
echo Lancement du Joueur 1...
start "Escampe - Joueur 1" cmd /k java -cp "%SERVEUR%" escampe.ClientJeu escampe.JoueurHumain localhost 1234
timeout /t 1 >nul
echo Lancement du Joueur 2...
start "Escampe - Joueur 2" cmd /k java -cp "%SERVEUR%" escampe.ClientJeu escampe.JoueurHumain localhost 1234
echo.
echo A vous deux ! Chacun joue dans sa fenetre.

BIN
lib/escampeobf.jar Normal file

Binary file not shown.

BIN
main-polytech.pdf Normal file

Binary file not shown.

9
partie1.md Normal file
View File

@@ -0,0 +1,9 @@
# Analyse des caract eristiques du jeu
## Ethan PUYAUBREAU & Antonin RUSSAC
1.
2. Une configuration peut être considérée comme une fin de partie si :
- Il ne reste qu'un licorne sur le terrain
3.

447
report/rapport.html Normal file
View File

@@ -0,0 +1,447 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Escampe — Rapport (version finale)</title>
</head>
<body>
<!-- ====================== PAGE DE TITRE ====================== -->
<div class="cover">
<div class="cover-univ">Université Paris-Saclay — Polytech APP5 — Année 2025-2026</div>
<div class="cover-course">IA et contraintes</div>
<h1 class="cover-title">Devoir Escampe</h1>
<div class="cover-sub">Conception et réalisation d'un joueur artificiel</div>
<div class="cover-sub">Rapport — version finale</div>
<div class="cover-authors">
Ethan&nbsp;Puyaubreau &nbsp;&amp;&nbsp; Antonin&nbsp;Russac
</div>
<div class="cover-date">30&nbsp;mai&nbsp;2026</div>
<div class="cover-meta">
Joueur : <code>escampe.JoueurPuyaubreauRussac</code><br>
Encadrement : Yue&nbsp;Ma (yue.ma@universite-paris-saclay.fr)
</div>
</div>
<!-- ====================== 1. INTRODUCTION ====================== -->
<h2>1. Présentation et règles</h2>
<p>Escampe se joue sur un plateau de 36&nbsp;cases (6×6). Chaque case porte un
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>
<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>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&nbsp;1
et&nbsp;2) puis la conception du joueur artificiel pour le tournoi (partie&nbsp;3),
avec les mesures qui justifient nos choix.</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>
<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&nbsp;0</code> = ligne&nbsp;1 (bas) et <code>colonne&nbsp;0</code> = A.
Chaque case contient 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>
<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>
</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>La carte des liserés est une constante <code>TILE_MAP</code> reproduisant la
figure&nbsp;4 de l'énoncé (ligne&nbsp;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
4 2 1 3 2 3 1
3 2 3 1 2 1 3
2 3 1 3 1 3 2
1 1 2 2 3 1 2</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&nbsp;6 de l'énoncé. Une carte divergente aurait
produit des coups jugés illégaux : ce point était critique.</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>
<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&nbsp;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&nbsp;000 parties
aléatoires simulées (utilitaire <code>escampe.Branching</code>) :</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>Branchement moyen (toutes positions)</td><td>≈ 8,9</td></tr>
</table>
<p>Le branchement effectif modeste (moyenne &lt;&nbsp;10) explique qu'une recherche
alpha-bêta atteigne des profondeurs élevées 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&nbsp;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>
<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>
<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>
</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 400600 demi-coups. Pour
tenir la contrainte de temps (300&nbsp;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>
<!-- ====================== 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><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><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&nbsp;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 class="note">Bug latent corrigé en partie&nbsp;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>
<!-- ====================== 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>
<ul>
<li><strong>Couleurs.</strong> <code>IJoueur</code> parle en entiers
(<code>NOIR&nbsp;=&nbsp;1</code>, <code>BLANC&nbsp;=&nbsp;-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>
</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>
<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><strong>Exécution.</strong> Trois processus (serveur + 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>
<!-- ====================== 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>
<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>
</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>
<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 .
1 N n . n n n 5 b . b . . b
(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>
<!-- ====================== 6. MOTEUR ====================== -->
<h2>6. Moteur de décision</h2>
<p>La décision 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&nbsp;-&nbsp;ply</code> (gagner vite plutôt que tard).</p>
<p><strong>Astuces de performance.</strong></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>
</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><strong>Performance mesurée.</strong> Environ <strong>4 à 5&nbsp;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&nbsp;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>
<!-- ====================== 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>
<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>
</ul>
<p>Concrètement, avec les poids retenus (somme&nbsp;=&nbsp;2, minimum&nbsp;=&nbsp;8) :</p>
<pre class="grid">eval = 2·Σ(10d_attaque) 2·Σ(10d_défense)
+ 8·(10min d_attaque) 8·(10min d_défense)</pre>
<p><strong>Heuristiques testées et choix.</strong> Le réglage s'est fait par
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 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>
<!-- ====================== 8. TEMPS ====================== -->
<h2>8. Gestion du temps réel</h2>
<p>La limite de l'arbitre est de 300&nbsp;s par joueur et par partie. Nous nous
fixons une <strong>enveloppe interne de 280&nbsp;s</strong> (≈ 20&nbsp;s de marge).
Le budget alloué à un coup est une fraction du temps restant, bornée :</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&nbsp;s évite de surinvestir en ouverture ; un plancher de 120&nbsp;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><strong>Mesures</strong> (auto-jeu équilibré, plein budget) : temps
<strong>maximal par coup ≈ 6,0&nbsp;s</strong> (le plafond), <strong>cumul maximal
≈ 36&nbsp;s</strong> par joueur sur une partie complète — très loin des 300&nbsp;s.
Le réglage est volontairement conservateur et pourrait être augmenté 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>
<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>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><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><strong>7 / 7 victoires</strong>, 0 coup illégal, 0 exception (les deux couleurs)</td>
</tr>
<tr>
<td>Démo IA vs IA (serveur réel)</td>
<td>Partie complète moteur contre moteur, gestion des pass</td>
<td>21 coups, fin propre par capture</td>
</tr>
<tr>
<td><code>Bench</code> / <code>Branching</code></td>
<td>Vitesse, profondeur, facteur de branchement</td>
<td>≈ 45 M nœuds/s ; profondeur 1215 ; 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>
<!-- ====================== 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>
<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>
<!-- ====================== 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&nbsp;4), interface
<code>Partie1</code>, et classes d'infrastructure 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 &amp; Moore, <em>An Analysis of Alpha-Beta
Pruning</em>, 1975) ; minimax, negamax et approfondissement itératif
(Russell &amp; Norvig, <em>Artificial Intelligence: A Modern Approach</em>) ;
techniques de représentation par masques de bits et d'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>
</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>
<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>
</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>
</body>
</html>

View File

@@ -0,0 +1,41 @@
#!/bin/bash
# Lot de parties arbitrées contre JoueurAleatoire, alternant les couleurs.
# Convention observée : le 1er connecté = JOUEUR 1 = Blanc, le 2e = JOUEUR 2 = Noir.
# Donc : moi en joueur A => je suis Blanc ; moi en joueur B => je suis Noir.
#
# usage: bench_vs_random.sh [N_par_couleur] [sliceMs]
set -u
ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
N="${1:-3}"; SLICE="${2:-300}"
JAR="$ROOT/lib/escampeobf.jar"; [ -f "$JAR" ] || JAR="C:/Users/Kerboul/Downloads/escampeobf.jar"
ME="escampe.JoueurPuyaubreauRussac"; RND="escampe.JoueurAleatoire"
LOG="$ROOT/scripts/logs"
port=1300; wins=0; losses=0; illegal=0; exc=0; games=0
play() { # $1 = ma couleur attendue (Blanc|Noir)
port=$((port+1)); games=$((games+1))
if [ "$1" = "Blanc" ]; then
OPTS_A="-Descampe.maxSliceMs=$SLICE" OPTS_B="" \
bash scripts/match.sh "$ME" out "$RND" "$JAR" "$port" 45 >/dev/null 2>&1
else
OPTS_A="" OPTS_B="-Descampe.maxSliceMs=$SLICE" \
bash scripts/match.sh "$RND" "$JAR" "$ME" out "$port" 45 >/dev/null 2>&1
fi
local winner; winner=$(grep -aoE "FIN! (Blanc|Noir)" "$LOG/server.log" | tail -1 | awk '{print $2}')
local il; il=$(grep -ac "illegal" "$LOG/server.log"); il=${il//[^0-9]/}; il=${il:-0}
illegal=$((illegal + il))
# exception côté MON client (A si Blanc, B si Noir)
local mylog; [ "$1" = "Blanc" ] && mylog="$LOG/playerA.log" || mylog="$LOG/playerB.log"
if grep -aqiE "exception|\bat escampe\." "$mylog" 2>/dev/null; then exc=$((exc+1)); fi
if [ "$winner" = "$1" ]; then wins=$((wins+1)); R=GAGNE; else losses=$((losses+1)); R=perdu; fi
echo " partie $games : moi=$1 vainqueur=$winner -> $R"
}
echo "=== $N parties en Blanc, $N en Noir (slice ${SLICE}ms) ==="
for i in $(seq 1 "$N"); do play Blanc; done
for i in $(seq 1 "$N"); do play Noir; done
echo "-------------------------------------------"
echo "Victoires : $wins / $games"
echo "Défaites : $losses"
echo "Coups illégaux (arbitre) : $illegal"
echo "Exceptions dans mon client : $exc"

51
scripts/match.sh Normal file
View File

@@ -0,0 +1,51 @@
#!/bin/bash
# Lance une partie ARBITRÉE entre deux IJoueur via le serveur réseau fourni.
#
# usage: match.sh [CLASS_A] [CP_A] [CLASS_B] [CP_B] [PORT] [TIMEOUT_S]
#
# Par défaut : notre joueur (depuis out/) contre escampe.JoueurAleatoire (jar).
# Le serveur (escampe.ServeurJeu) et les adversaires de référence vivent dans
# escampeobf.jar, fourni séparément (hors livrable). Seules des chaînes de
# caractères circulent sur le réseau : la divergence d'interface obfusquée
# entre le jar et nos sources est donc sans effet.
set -u
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
# Jar arbitre : dans le repo (lib/) en priorité, sinon dans Downloads.
JAR="$ROOT/lib/escampeobf.jar"
[ -f "$JAR" ] || JAR="C:/Users/Kerboul/Downloads/escampeobf.jar"
CLA="${1:-escampe.JoueurPuyaubreauRussac}"; CPA="${2:-out}"
CLB="${3:-escampe.JoueurAleatoire}"; CPB="${4:-$JAR}"
PORT="${5:-1234}"; TMO="${6:-60}"
OPTS_A="${OPTS_A:-}" # options JVM pour le joueur A (ex: -Descampe.debug=true)
OPTS_B="${OPTS_B:-}"
LOG="$ROOT/scripts/logs"; mkdir -p "$LOG"; rm -f "$LOG"/*.log
echo "Serveur : ServeurJeu $PORT 1"
echo "Joueur A : $CLA (cp=$CPA)"
echo "Joueur B : $CLB (cp=$CPB)"
echo "----------------------------------------"
java -cp "$JAR" escampe.ServeurJeu "$PORT" 1 > "$LOG/server.log" 2>&1 &
SRV=$!
sleep 2
java $OPTS_A -cp "$CPA" escampe.ClientJeu "$CLA" localhost "$PORT" > "$LOG/playerA.log" 2>&1 &
A=$!
sleep 1
java $OPTS_B -cp "$CPB" escampe.ClientJeu "$CLB" localhost "$PORT" > "$LOG/playerB.log" 2>&1 &
B=$!
# Chien de garde : tue tout après TMO secondes si la partie ne se termine pas.
( sleep "$TMO"; kill $A $B $SRV 2>/dev/null ) & WATCH=$!
wait $A 2>/dev/null
wait $B 2>/dev/null
kill $SRV 2>/dev/null
kill $WATCH 2>/dev/null
echo "=== SERVER ==="; cat "$LOG/server.log"
echo; echo "=== PLAYER A ($CLA) ==="; cat "$LOG/playerA.log"
echo; echo "=== PLAYER B ($CLB) ==="; cat "$LOG/playerB.log"

743
src/EscampeBoard.java Normal file
View File

@@ -0,0 +1,743 @@
import java.io.*;
import java.util.*;
/**
* Représentation d'un état du jeu Escampe.
*
* <p>Le plateau est un tableau {@code int[6][6]} :
* <ul>
* <li>{@code board[row][col]} avec row 0 = ligne 1 (bas), row 5 = ligne 6 (haut).</li>
* <li>col 0 = colonne A, col 5 = colonne F.</li>
* </ul>
*
* <p>Chaque case stocke l'une des constantes pièce :
* {@code EMPTY}, {@code WHITE_LICORNE}, {@code WHITE_PALADIN},
* {@code BLACK_LICORNE}, {@code BLACK_PALADIN}.
*
* <p>L'état complémentaire mémorisé :
* <ul>
* <li>{@code lastTileType} : type de liseré (1, 2 ou 3) de la case d'arrivée du dernier coup ;
* -1 = pas de contrainte (premier coup ou après un pass).</li>
* <li>{@code currentPlayer} : "noir" ou "blanc", joueur dont c'est le tour.</li>
* <li>{@code blackPlaced}, {@code whitePlaced} : phases de placement terminées.</li>
* <li>{@code blackRows} : les deux lignes (index 0-5) choisies par noir lors du placement.</li>
* </ul>
*
* <p>Règles de déplacement :
* <ul>
* <li>Une pièce avance exactement N pas orthogonaux (N = liseré de la case de départ).</li>
* <li>Elle peut changer de direction à chaque pas.</li>
* <li>Elle ne peut pas passer par une case occupée ni repasser deux fois par la même case.</li>
* <li>Au dernier pas uniquement, elle peut se poser sur la licorne adverse (capture).</li>
* </ul>
*/
public class EscampeBoard implements Partie1 {
// ── Constantes pièces ────────────────────────────────────────────────────
static final int EMPTY = 0;
static final int WHITE_LICORNE = 1;
static final int WHITE_PALADIN = 2;
static final int BLACK_LICORNE = 3;
static final int BLACK_PALADIN = 4;
/**
* Carte des liserés : {@code TILE_MAP[row][col]}.
* row 0 = ligne 1 (bas), row 5 = ligne 6 (haut). col 0 = A, col 5 = F.
*/
static final int[][] TILE_MAP = {
{1, 2, 2, 3, 1, 2}, // ligne 1
{3, 1, 3, 1, 3, 2}, // ligne 2
{2, 3, 1, 2, 1, 3}, // ligne 3
{2, 1, 3, 2, 3, 1}, // ligne 4
{1, 3, 1, 3, 1, 2}, // ligne 5
{3, 2, 2, 1, 3, 2}, // ligne 6
};
// ── État ─────────────────────────────────────────────────────────────────
int[][] board;
int lastTileType; // -1 = pas de contrainte
String currentPlayer; // "noir" ou "blanc"
boolean blackPlaced;
boolean whitePlaced;
int[] blackRows; // les 2 lignes (0-indexé) choisies par noir
// ── Constructeur ─────────────────────────────────────────────────────────
public EscampeBoard() {
board = new int[6][6];
lastTileType = -1;
currentPlayer = "noir";
blackPlaced = false;
whitePlaced = false;
blackRows = null;
}
// =========================================================================
// Fichier I/O
// =========================================================================
@Override
public void setFromFile(String fileName) {
board = new int[6][6];
lastTileType = -1;
currentPlayer = "noir";
blackPlaced = false;
whitePlaced = false;
blackRows = null;
try (BufferedReader br = new BufferedReader(new FileReader(fileName))) {
String line;
while ((line = br.readLine()) != null) {
line = line.trim();
if (line.isEmpty()) continue;
char first = line.charAt(0);
// Commentaire / méta-donnée
if (first == '%') {
parseMeta(line);
continue;
}
// Ligne de plateau : "1 XXXX 1" ou "01 XXXX 01"
int rowNum = -1;
int pos = 0;
if (first >= '1' && first <= '6') {
rowNum = first - '0';
pos = 1;
} else if (first == '0' && line.length() > 1) {
char second = line.charAt(1);
if (second >= '1' && second <= '6') {
rowNum = second - '0';
pos = 2;
}
}
if (rowNum != -1) {
int rowIdx = rowNum - 1;
while (pos < line.length() && line.charAt(pos) == ' ') pos++;
for (int c = 0; c < 6 && pos + c < line.length(); c++) {
board[rowIdx][c] = charToPiece(line.charAt(pos + c));
}
}
}
} catch (IOException e) {
throw new RuntimeException("Erreur de lecture du fichier : " + fileName, e);
}
// Si pas de méta-commentaires, on infère l'état à partir des pièces
inferState();
}
/** Parse une ligne de méta-commentaire "% clé: valeur". */
private void parseMeta(String line) {
if (line.startsWith("% lastTileType:")) {
lastTileType = Integer.parseInt(line.substring(15).trim());
} else if (line.startsWith("% currentPlayer:")) {
currentPlayer = line.substring(16).trim();
} else if (line.startsWith("% blackPlaced:")) {
blackPlaced = Boolean.parseBoolean(line.substring(14).trim());
} else if (line.startsWith("% whitePlaced:")) {
whitePlaced = Boolean.parseBoolean(line.substring(14).trim());
} else if (line.startsWith("% blackRows:")) {
String s = line.substring(12).trim();
String[] parts = s.split(",");
int r0 = Integer.parseInt(parts[0].trim());
int r1 = Integer.parseInt(parts[1].trim());
if (r0 >= 0) blackRows = new int[]{r0, r1};
}
}
/**
* Infère {@code blackPlaced}, {@code whitePlaced} et {@code blackRows}
* à partir des pièces présentes sur le plateau
* (utilisé quand le fichier ne contient pas de méta-commentaires).
*/
private void inferState() {
if (blackPlaced && whitePlaced) return; // méta déjà chargée
int bc = 0, wc = 0;
Set<Integer> bRowSet = new TreeSet<>();
for (int r = 0; r < 6; r++) {
for (int c = 0; c < 6; c++) {
int p = board[r][c];
if (p == BLACK_LICORNE || p == BLACK_PALADIN) { bc++; bRowSet.add(r); }
if (p == WHITE_LICORNE || p == WHITE_PALADIN) { wc++; }
}
}
if (!blackPlaced && bc == 6) {
blackPlaced = true;
if (bRowSet.size() == 2) {
Iterator<Integer> it = bRowSet.iterator();
blackRows = new int[]{it.next(), it.next()};
}
}
if (!whitePlaced && wc == 6) {
whitePlaced = true;
}
}
@Override
public void saveToFile(String fileName) {
try (PrintWriter pw = new PrintWriter(new FileWriter(fileName))) {
pw.println("% Escampe - sauvegarde du plateau");
pw.println("% lastTileType: " + lastTileType);
pw.println("% currentPlayer: " + currentPlayer);
pw.println("% blackPlaced: " + blackPlaced);
pw.println("% whitePlaced: " + whitePlaced);
if (blackRows != null) {
pw.println("% blackRows: " + blackRows[0] + "," + blackRows[1]);
} else {
pw.println("% blackRows: -1,-1");
}
// Lignes 6 à 1 (haut vers bas dans le fichier)
for (int rowIdx = 5; rowIdx >= 0; rowIdx--) {
int rowNum = rowIdx + 1;
StringBuilder sb = new StringBuilder();
String rowLabel = String.format("%02d", rowNum);
sb.append(rowLabel).append(' ');
for (int c = 0; c < 6; c++) sb.append(pieceToChar(board[rowIdx][c]));
sb.append(' ').append(rowLabel);
pw.println(sb.toString());
}
} catch (IOException e) {
throw new RuntimeException("Erreur d'écriture du fichier : " + fileName, e);
}
}
// =========================================================================
// Fin de partie
// =========================================================================
@Override
public boolean gameOver() {
if (!blackPlaced || !whitePlaced) return false;
boolean wl = false, bl = false;
for (int r = 0; r < 6; r++)
for (int c = 0; c < 6; c++) {
if (board[r][c] == WHITE_LICORNE) wl = true;
if (board[r][c] == BLACK_LICORNE) bl = true;
}
return !wl || !bl;
}
// =========================================================================
// Validation d'un coup
// =========================================================================
@Override
public boolean isValidMove(String move, String player) {
if (move == null || move.isEmpty()) return false;
if (!"noir".equals(player) && !"blanc".equals(player)) return false;
if (move.contains("/")) return isValidPlacement(move, player);
if ("E".equals(move)) return isValidPass(player);
return isValidRegularMove(move, player);
}
/**
* Valide un coup de placement "P1/P2/P3/P4/P5/P6"
* (P1 = licorne, P2-P6 = paladins).
*/
private boolean isValidPlacement(String move, String player) {
if ("noir".equals(player) && blackPlaced) return false;
if ("blanc".equals(player) && whitePlaced) return false;
if (!player.equals(currentPlayer)) return false;
if ("blanc".equals(player) && !blackPlaced) return false;
String[] parts = move.split("/");
if (parts.length != 6) return false;
int[][] pos = new int[6][2];
for (int i = 0; i < 6; i++) {
int[] cell = cellFromString(parts[i]);
if (cell == null) return false;
pos[i] = cell;
}
// Zone autorisée
if ("noir".equals(player)) {
boolean allLow = true, allHigh = true;
for (int[] p : pos) {
if (p[0] != 0 && p[0] != 1) allLow = false;
if (p[0] != 4 && p[0] != 5) allHigh = false;
}
if (!allLow && !allHigh) return false;
} else {
if (blackRows == null) return false;
int[] wr = complementaryRows(blackRows);
for (int[] p : pos) {
if (p[0] != wr[0] && p[0] != wr[1]) return false;
}
}
// Pas de doublons, cases vides
Set<String> seen = new HashSet<>();
for (int[] p : pos) {
if (!seen.add(p[0] + "," + p[1])) return false;
if (board[p[0]][p[1]] != EMPTY) return false;
}
return true;
}
/** Valide un pass "E" : uniquement si aucun coup régulier n'est disponible. */
private boolean isValidPass(String player) {
if (!player.equals(currentPlayer)) return false;
if (!blackPlaced || !whitePlaced) return false;
if (gameOver()) return false;
String[] m = possiblesMoves(player);
return m.length == 1 && "E".equals(m[0]);
}
/** Valide un coup régulier "XX-YY". */
private boolean isValidRegularMove(String move, String player) {
if (!blackPlaced || !whitePlaced) return false;
if (gameOver()) return false;
if (!player.equals(currentPlayer)) return false;
int dash = move.indexOf('-');
if (dash < 1 || dash >= move.length() - 1) return false;
int[] from = cellFromString(move.substring(0, dash));
int[] to = cellFromString(move.substring(dash + 1));
if (from == null || to == null) return false;
if (!belongsToPlayer(board[from[0]][from[1]], player)) return false;
if (lastTileType != -1 && TILE_MAP[from[0]][from[1]] != lastTileType) return false;
return getReachableSquares(from[0], from[1], player).contains(to[0] + "," + to[1]);
}
// =========================================================================
// Génération de coups
// =========================================================================
@Override
public String[] possiblesMoves(String player) {
// Pendant le placement le nombre de combinaisons est trop grand pour être énuméré
if (!blackPlaced || !whitePlaced) return new String[0];
if (gameOver()) return new String[0];
List<String> moves = new ArrayList<>();
for (int r = 0; r < 6; r++) {
for (int c = 0; c < 6; c++) {
if (!belongsToPlayer(board[r][c], player)) continue;
if (lastTileType != -1 && TILE_MAP[r][c] != lastTileType) continue;
for (String dest : getReachableSquares(r, c, player)) {
String[] d = dest.split(",");
moves.add(stringFromCell(r, c) + "-"
+ stringFromCell(Integer.parseInt(d[0]), Integer.parseInt(d[1])));
}
}
}
if (moves.isEmpty()) return new String[]{"E"};
return moves.toArray(new String[0]);
}
// =========================================================================
// Jouer un coup
// =========================================================================
@Override
public void play(String move, String player) {
if (!isValidMove(move, player))
throw new IllegalArgumentException("Coup invalide : '" + move + "' pour " + player);
if (move.contains("/")) {
playPlacement(move, player);
} else if ("E".equals(move)) {
// Pass : supprime la contrainte de liseré (règle officielle)
lastTileType = -1;
currentPlayer = opponent(currentPlayer);
} else {
playRegular(move, player);
}
}
private void playPlacement(String move, String player) {
String[] parts = move.split("/");
int[][] pos = new int[6][2];
for (int i = 0; i < 6; i++) pos[i] = cellFromString(parts[i]);
int licorne = "noir".equals(player) ? BLACK_LICORNE : WHITE_LICORNE;
int paladin = "noir".equals(player) ? BLACK_PALADIN : WHITE_PALADIN;
board[pos[0][0]][pos[0][1]] = licorne;
for (int i = 1; i < 6; i++) board[pos[i][0]][pos[i][1]] = paladin;
if ("noir".equals(player)) {
blackPlaced = true;
// Enregistrer les deux lignes choisies par noir
Set<Integer> rows = new TreeSet<>();
for (int[] p : pos) rows.add(p[0]);
Iterator<Integer> it = rows.iterator();
blackRows = new int[]{it.next(), it.next()};
currentPlayer = "blanc";
} else {
whitePlaced = true;
lastTileType = -1; // pas de contrainte pour le premier coup régulier
currentPlayer = "blanc"; // blanc joue en premier
}
}
private void playRegular(String move, String player) {
int dash = move.indexOf('-');
int[] from = cellFromString(move.substring(0, dash));
int[] to = cellFromString(move.substring(dash + 1));
board[to[0]][to[1]] = board[from[0]][from[1]]; // capture si case adverse
board[from[0]][from[1]] = EMPTY;
lastTileType = TILE_MAP[to[0]][to[1]];
currentPlayer = opponent(currentPlayer);
}
// =========================================================================
// Algorithme de déplacement (DFS)
// =========================================================================
/**
* Calcule l'ensemble des cases atteignables depuis (fromRow, fromCol).
* Résultats encodés sous forme "row,col".
*/
Set<String> getReachableSquares(int fromRow, int fromCol, String player) {
Set<String> result = new HashSet<>();
boolean[][] visited = new boolean[6][6];
visited[fromRow][fromCol] = true;
dfs(fromRow, fromCol, TILE_MAP[fromRow][fromCol], player, visited, result);
return result;
}
/**
* DFS récursif pour le calcul des destinations.
*
* <p>À chaque appel, la pièce se trouve en (row, col) et doit encore effectuer
* {@code stepsLeft} pas. Les cases déjà visitées dans le chemin courant sont
* marquées dans {@code visited} (réinitialisation après backtrack).
*
* <p>Règles :
* <ul>
* <li>Pas intermédiaires (stepsLeft > 1) : la case suivante doit être vide.</li>
* <li>Dernier pas (stepsLeft == 1) : la case peut être vide ou contenir
* la licorne adverse (capture).</li>
* </ul>
*/
private void dfs(int row, int col, int stepsLeft,
String player, boolean[][] visited, Set<String> result) {
if (stepsLeft == 0) {
result.add(row + "," + col);
return;
}
// Directions orthogonales : haut, bas, gauche, droite
int[] dr = {-1, 1, 0, 0};
int[] dc = { 0, 0, -1, 1};
for (int d = 0; d < 4; d++) {
int nr = row + dr[d];
int nc = col + dc[d];
if (nr < 0 || nr >= 6 || nc < 0 || nc >= 6) continue;
if (visited[nr][nc]) continue;
int occ = board[nr][nc];
boolean canStep;
if (stepsLeft > 1) {
// Pas intermédiaire : case obligatoirement vide
canStep = (occ == EMPTY);
} else {
// Dernier pas : vide OU capture de la licorne adverse
canStep = (occ == EMPTY)
|| ("blanc".equals(player) && occ == BLACK_LICORNE)
|| ("noir".equals(player) && occ == WHITE_LICORNE);
}
if (!canStep) continue;
visited[nr][nc] = true;
dfs(nr, nc, stepsLeft - 1, player, visited, result);
visited[nr][nc] = false; // backtrack
}
}
// =========================================================================
// Méthodes utilitaires
// =========================================================================
private int charToPiece(char c) {
switch (c) {
case 'B': return WHITE_LICORNE;
case 'b': return WHITE_PALADIN;
case 'N': return BLACK_LICORNE;
case 'n': return BLACK_PALADIN;
default: return EMPTY;
}
}
private char pieceToChar(int piece) {
switch (piece) {
case WHITE_LICORNE: return 'B';
case WHITE_PALADIN: return 'b';
case BLACK_LICORNE: return 'N';
case BLACK_PALADIN: return 'n';
default: return '-';
}
}
/**
* Convertit une chaîne "A1"-"F6" en coordonnées {row, col} (0-indexé).
* Retourne null si le format est invalide.
*/
int[] cellFromString(String s) {
if (s == null || s.length() < 2) return null;
s = s.trim();
char colC = Character.toUpperCase(s.charAt(0));
char rowC = s.charAt(1);
if (colC < 'A' || colC > 'F') return null;
if (rowC < '1' || rowC > '6') return null;
return new int[]{rowC - '1', colC - 'A'};
}
/** Convertit des coordonnées internes en notation "A1"-"F6". */
String stringFromCell(int row, int col) {
return "" + (char)('A' + col) + (char)('1' + row);
}
private boolean belongsToPlayer(int piece, String player) {
if ("blanc".equals(player)) return piece == WHITE_LICORNE || piece == WHITE_PALADIN;
if ("noir".equals(player)) return piece == BLACK_LICORNE || piece == BLACK_PALADIN;
return false;
}
private String opponent(String player) {
return "blanc".equals(player) ? "noir" : "blanc";
}
/**
* Retourne les deux lignes (0-indexé) que doit utiliser blanc,
* sachant que noir a choisi {@code bRows}.
* Noir sur {0,1} → blanc sur {4,5} ; noir sur {4,5} → blanc sur {0,1}.
*/
private int[] complementaryRows(int[] bRows) {
return (bRows[0] == 0) ? new int[]{4, 5} : new int[]{0, 1};
}
// =========================================================================
// Affichage
// =========================================================================
/** Affiche le plateau en console (ligne 6 en haut). */
public void printBoard() {
System.out.println(" A B C D E F liseré");
for (int r = 5; r >= 0; r--) {
System.out.print((r + 1) + " [ ");
for (int c = 0; c < 6; c++) System.out.print(pieceToChar(board[r][c]) + " ");
System.out.print("] " + (r + 1) + " |");
for (int c = 0; c < 6; c++) System.out.print(" " + TILE_MAP[r][c]);
System.out.println();
}
System.out.println("lastTileType=" + lastTileType
+ " currentPlayer=" + currentPlayer + "\n");
}
// =========================================================================
// Main de démonstration
// =========================================================================
public static void main(String[] args) throws IOException {
System.out.println("=========================================");
System.out.println(" Demo EscampeBoard ");
System.out.println("=========================================\n");
// ── Placements utilisés dans plusieurs scenarios ──────────────────
// Noir : lignes 5-6 (rows 4-5) — licorne en A6, paladins en B6 C6 D5 E5 F5
final String NOIR_PL = "A6/B6/C6/D5/E5/F5";
// Blanc : lignes 1-2 (rows 0-1) — licorne en D2, paladins en A1 B1 C1 E1 F2
final String BLANC_PL = "D2/A1/B1/C1/E1/F2";
// ─────────────────────────────────────────────────────────────────
// 1. PHASE DE PLACEMENT
// ─────────────────────────────────────────────────────────────────
System.out.println("=== 1. PHASE DE PLACEMENT ===");
EscampeBoard b = new EscampeBoard();
// Tentatives invalides avant le placement normal
System.out.println("Blanc tente de placer avant noir : "
+ b.isValidMove(BLANC_PL, "blanc") + " (attendu: false)");
System.out.println("Noir placement au milieu du plateau : "
+ b.isValidMove("A3/B3/C3/D3/E3/F3", "noir") + " (attendu: false)");
System.out.println("Noir placement sur deux bords diff. : "
+ b.isValidMove("A1/B1/C1/D5/E5/F5", "noir") + " (attendu: false)");
// Placement valide de noir
System.out.println("\nNoir place : " + NOIR_PL
+ " valid=" + b.isValidMove(NOIR_PL, "noir"));
b.play(NOIR_PL, "noir");
System.out.println(" blackPlaced=" + b.blackPlaced
+ " blackRows=[" + b.blackRows[0] + "," + b.blackRows[1] + "]"
+ " currentPlayer=" + b.currentPlayer);
// Placement valide de blanc
System.out.println("Blanc place : " + BLANC_PL
+ " valid=" + b.isValidMove(BLANC_PL, "blanc"));
b.play(BLANC_PL, "blanc");
System.out.println(" whitePlaced=" + b.whitePlaced
+ " currentPlayer=" + b.currentPlayer);
b.printBoard();
// ─────────────────────────────────────────────────────────────────
// 2. PHASE REGULIERE — contrainte de liseré
// ─────────────────────────────────────────────────────────────────
System.out.println("=== 2. PHASE REGULIERE ===");
System.out.println("lastTileType=" + b.lastTileType
+ " (pas de contrainte pour le premier coup)\n");
// Blanc joue en premier, pas de contrainte
String[] bMoves = b.possiblesMoves("blanc");
System.out.println("Coups possibles pour blanc : " + bMoves.length + " coups");
System.out.printf("Exemples : %s %s %s%n",
bMoves[0],
bMoves.length > 1 ? bMoves[1] : "",
bMoves.length > 2 ? bMoves[2] : "");
String m1 = bMoves[0];
System.out.println("\nBlanc joue : " + m1 + " valid=" + b.isValidMove(m1, "blanc"));
b.play(m1, "blanc");
System.out.println(" lastTileType=" + b.lastTileType
+ " (liseré de la case d'arrivée = contrainte pour noir)"
+ " currentPlayer=" + b.currentPlayer);
// Tentative invalide : blanc rejoue hors de son tour
System.out.println("\nBlanc rejoue hors tour : "
+ b.isValidMove(m1, "blanc") + " (attendu: false)");
// Tentative invalide : noir joue depuis un mauvais liseré
String badNoirMove = findMoveFromWrongTile(b, "noir");
if (badNoirMove != null) {
System.out.println("Noir depuis mauvais liseré (" + badNoirMove + ") : "
+ b.isValidMove(badNoirMove, "noir") + " (attendu: false)");
}
// Coup valide de noir
String[] nMoves = b.possiblesMoves("noir");
System.out.println("\nCoups possibles pour noir (liseré " + b.lastTileType + ") : "
+ nMoves.length + " coups");
String m2 = nMoves[0];
System.out.println("Noir joue : " + m2 + " valid=" + b.isValidMove(m2, "noir"));
b.play(m2, "noir");
System.out.println(" lastTileType=" + b.lastTileType
+ " currentPlayer=" + b.currentPlayer);
// ─────────────────────────────────────────────────────────────────
// 3. ROUND-TRIP FICHIER
// ─────────────────────────────────────────────────────────────────
System.out.println("\n=== 3. ROUND-TRIP FICHIER ===");
b.saveToFile("escampe_save.txt");
System.out.println("Sauvegardé dans escampe_save.txt");
EscampeBoard b2 = new EscampeBoard();
b2.setFromFile("escampe_save.txt");
System.out.println("Rechargé : lastTileType=" + b2.lastTileType
+ " currentPlayer=" + b2.currentPlayer);
System.out.println("Plateaux identiques : " + Arrays.deepEquals(b.board, b2.board));
System.out.println("lastTileType identique : " + (b.lastTileType == b2.lastTileType));
System.out.println("currentPlayer identique : " + b.currentPlayer.equals(b2.currentPlayer));
// ─────────────────────────────────────────────────────────────────
// 4. SCENARIO DE PASS (E)
// ─────────────────────────────────────────────────────────────────
System.out.println("\n=== 4. SCENARIO DE PASS ===");
EscampeBoard bPass = new EscampeBoard();
bPass.play(NOIR_PL, "noir");
bPass.play(BLANC_PL, "blanc");
// Forcer une situation où noir n'a aucun coup :
// lastTileType=2, mais toutes les pièces noires sont sur liseré 1 ou 3.
for (int r = 0; r < 6; r++) Arrays.fill(bPass.board[r], EMPTY);
bPass.board[0][3] = WHITE_LICORNE; // D1 liseré=3
bPass.board[0][0] = WHITE_PALADIN; // A1 liseré=1
bPass.board[0][4] = WHITE_PALADIN; // E1 liseré=1
bPass.board[5][0] = BLACK_LICORNE; // A6 liseré=3
bPass.board[4][4] = BLACK_PALADIN; // E5 liseré=1
bPass.board[4][2] = BLACK_PALADIN; // C5 liseré=1
bPass.lastTileType = 2; // blanc vient de poser sur liseré 2
bPass.currentPlayer = "noir";
System.out.println("Pièces noires sur liserés 1 et 3, contrainte = 2");
System.out.println("possiblesMoves(noir) = "
+ Arrays.toString(bPass.possiblesMoves("noir")) + " (attendu: [E])");
System.out.println("isValidMove(E, noir) = "
+ bPass.isValidMove("E", "noir") + " (attendu: true)");
System.out.println("isValidMove(E, blanc) = "
+ bPass.isValidMove("E", "blanc") + " (attendu: false, pas son tour)");
bPass.play("E", "noir");
System.out.println("Après pass : lastTileType=" + bPass.lastTileType
+ " (attendu: -1) currentPlayer=" + bPass.currentPlayer);
// ─────────────────────────────────────────────────────────────────
// 5. CAPTURE ET FIN DE PARTIE
// ─────────────────────────────────────────────────────────────────
System.out.println("\n=== 5. CAPTURE ET FIN DE PARTIE ===");
EscampeBoard bCap = new EscampeBoard();
bCap.play(NOIR_PL, "noir");
bCap.play(BLANC_PL, "blanc");
// Mise en scène :
// - Blanc paladin en B1 (row=0,col=1 ; liseré=2)
// → 2 pas orthogonaux : B1 -> B2 -> B3
// - Licorne noire en B3 (row=2,col=1) ; case B2 vide
// - lastTileType=2 → blanc peut jouer depuis B1
for (int r = 0; r < 6; r++) Arrays.fill(bCap.board[r], EMPTY);
bCap.board[0][1] = WHITE_PALADIN; // B1 liseré=2
bCap.board[0][3] = WHITE_LICORNE; // D1 (garde-fou : licorne blanche présente)
bCap.board[2][1] = BLACK_LICORNE; // B3
bCap.board[5][5] = BLACK_PALADIN; // F6 (présence de pièce noire restante)
bCap.lastTileType = 2;
bCap.currentPlayer = "blanc";
System.out.println("Avant capture :");
bCap.printBoard();
System.out.println("gameOver = " + bCap.gameOver() + " (attendu: false)");
// Coup invalide : un pas seulement (B1->B2), pas assez de cases
System.out.println("Coup B1-B2 (1 pas, manque 1) : "
+ bCap.isValidMove("B1-B2", "blanc") + " (attendu: false)");
// Coup valide : deux pas (B1->B2->B3), B2 vide, B3 = licorne noire
System.out.println("Coup B1-B3 (2 pas, capture) : "
+ bCap.isValidMove("B1-B3", "blanc") + " (attendu: true)");
bCap.play("B1-B3", "blanc");
System.out.println("Après capture :");
bCap.printBoard();
System.out.println("gameOver = " + bCap.gameOver() + " (attendu: true)");
System.out.println("Blanc gagne !");
System.out.println("\n=========================================");
System.out.println(" Demo terminee ");
System.out.println("=========================================");
}
/**
* Utilitaire pour la démo : trouve un coup depuis une pièce
* de {@code player} dont le liseré est différent de {@code lastTileType}.
* Retourne null si aucune telle pièce n'a de destinations.
*/
private static String findMoveFromWrongTile(EscampeBoard b, String player) {
for (int r = 0; r < 6; r++) {
for (int c = 0; c < 6; c++) {
if (!b.belongsToPlayer(b.board[r][c], player)) continue;
if (TILE_MAP[r][c] == b.lastTileType) continue;
Set<String> reach = b.getReachableSquares(r, c, player);
if (!reach.isEmpty()) {
String dest = reach.iterator().next();
String[] parts = dest.split(",");
return b.stringFromCell(r, c) + "-"
+ b.stringFromCell(Integer.parseInt(parts[0]),
Integer.parseInt(parts[1]));
}
}
}
return null;
}
}

43
src/Partie1.java Normal file
View File

@@ -0,0 +1,43 @@
public interface Partie1 {
/**
* Initialise un plateau à partir d'un fichier texte.
* @param fileName le nom du fichier à lire
*/
public void setFromFile(String fileName);
/**
* Sauve la configuration de l'état courant (plateau et pièces restantes) dans un fichier.
* @param fileName le nom du fichier à sauvegarder
* Le format doit être compatible avec celui utilisé pour la lecture.
*/
public void saveToFile(String fileName);
/**
* Indique si le coup {@code move} est valide pour le joueur {@code player} sur le plateau courant.
* @param move le coup à jouer,
* sous la forme "B1-D1" en général,
* sous la forme "C6/A6/B5/D5/E6/F5" pour le coup qui place les pièces,
* ou "E" pour passer son tour.
* @param player le joueur qui joue, représenté par "noir" ou "blanc"
*/
public boolean isValidMove(String move, String player);
/**
* Calcule les coups possibles pour le joueur {@code player} sur le plateau courant.
* @param player le joueur qui joue, représenté par "noir" ou "blanc"
*/
public String[] possiblesMoves(String player);
/**
* Modifie le plateau en jouant le coup {@code move} pour le joueur {@code player}.
* @param move le coup à jouer, sous la forme "C1-D1" ou "C6/A6/B5/D5/E6/F5"
* @param player le joueur qui joue, représenté par "noir" ou "blanc"
*/
public void play(String move, String player);
/**
* Retourne vrai lorsque le plateau correspond à une fin de partie.
*/
public boolean gameOver();
}

298
src/escampe/Applet.java Normal file
View File

@@ -0,0 +1,298 @@
package escampe;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import javax.swing.DefaultListModel;
import javax.swing.JApplet;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ListSelectionModel;
public class Applet extends JApplet {
// Constantes pour les pièces
final private static int LICORNEBLANCHE = -2;
final private static int PALADINBLANC = -1;
final private static int LICORNENOIRE = 2;
final private static int PALADINNOIR = 1;
final private static int VIDE = 0;
// Constantes pour le plateau
final private static int LARGEUR = 6;
final private static int HAUTEUR = 6;
final private static int[][] lisereCase = {
{1, 2, 2, 3, 1, 2},
{3, 1, 3, 1, 3, 2},
{2, 3, 1, 2, 1, 3},
{2, 1, 3, 2, 3, 1},
{1, 3, 1, 3, 1, 2},
{3, 2, 2, 1, 3, 2}
};
// Constantes pour les couleurs
Color DARK = new Color(155, 102, 95);
Color LIGHT = new Color(239, 210, 158);
Color BLACK = new Color(255, 255, 255);
Color WHITE = new Color(0, 0, 0);
Color HIGHLIGHT = new Color(255, 0, 0);
// Constantes pour l'affichage
final private static int TAILLECASE = 100;
final private static int TAILLEPION = 60;
final private static Dimension FRAMEDIMENSION = new Dimension(TAILLECASE*6 + 260,TAILLECASE*6 + 60);
private static final long serialVersionUID = 1L;
private JList brdList;
private Board displayBoard;
private JScrollPane scrollPane;
private DefaultListModel listModel;
private Frame myFrame;
static int cpt = 0;
// Autres constantes utiles pour l'affichage du plateau d'Escampe
int mpiece = (int) (TAILLECASE - TAILLEPION)/2;
int epaisseurCercle = (int) (TAILLECASE*0.1);
int epaisseurInterCercle = (int) (TAILLECASE*0.05);
int diametre1e = TAILLECASE; // extérieur 1er cercle
int diametre1i = diametre1e - epaisseurCercle; // intérieur 1er cercle
int diametre2e = diametre1i - epaisseurInterCercle; // extérieur 2eme cercle
int diametre2i = diametre2e - epaisseurCercle; // intérieur 2eme cercle
int diametre3e = diametre2i - epaisseurInterCercle; // extérieur 3eme cercle
int diametre3i = diametre3e - epaisseurCercle; // intérieur 3eme cercle
int m1e = 0;
int m1i = (int) (TAILLECASE - diametre1i)/2;
int m2e = (int) (TAILLECASE - diametre2e)/2;
int m2i = (int) (TAILLECASE - diametre2i)/2;
int m3e = (int) (TAILLECASE - diametre3e)/2;
int m3i = (int) (TAILLECASE - diametre3i)/2;
public void init() {
System.out.println("Initialisation BoardApplet" + cpt++);
buildUI(getContentPane());
}
public void buildUI(Container container) {
setBackground(Color.white);
int[][] temp = new int[HAUTEUR][LARGEUR];
for (int i = 0; i < HAUTEUR; i++)
for (int j = 0; j < LARGEUR; j++)
temp[i][j] = VIDE;
displayBoard = new Board("Coups :", temp);
listModel = new DefaultListModel();
listModel.addElement(displayBoard);
brdList = new JList(listModel);
brdList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
brdList.setSelectedIndex(0);
scrollPane = new JScrollPane(brdList);
Dimension d = scrollPane.getSize();
scrollPane.setPreferredSize(new Dimension(200, d.height));
brdList.addKeyListener(new java.awt.event.KeyAdapter() {
public void keyPressed(KeyEvent e) {
brdList_keyPressed(e);
}
});
brdList.addMouseListener(new java.awt.event.MouseAdapter() {
public void mouseClicked(MouseEvent e) {
brdList_mouseClicked(e);
}
});
container.add(displayBoard, BorderLayout.CENTER);
container.add(scrollPane, BorderLayout.EAST);
}
public void update(Graphics g, Insets in) {
Insets tempIn = in;
g.translate(tempIn.left, tempIn.top);
paint(g);
}
public void paint(Graphics g) {
displayBoard.paint(g);
}
public void addBoard(String move, int[][] board) {
Board tempEntrop = new Board(move, board);
listModel.addElement(new Board(move, board));
brdList.setSelectedIndex(listModel.getSize() - 1);
brdList.ensureIndexIsVisible(listModel.getSize() - 1);
displayBoard = tempEntrop;
update(myFrame.getGraphics(), myFrame.getInsets());
}
public void setMyFrame(Frame f) {
myFrame = f;
}
void brdList_keyPressed(KeyEvent e) {
int index = brdList.getSelectedIndex();
if (e.getKeyCode() == KeyEvent.VK_UP && index > 0)
displayBoard = (Board) listModel.getElementAt(index - 1);
if (e.getKeyCode() == KeyEvent.VK_DOWN && index < (listModel.getSize() - 1))
displayBoard = (Board) listModel.getElementAt(index + 1);
update(myFrame.getGraphics(), myFrame.getInsets());
}
void brdList_mouseClicked(MouseEvent e) {
displayBoard = (Board) listModel.getElementAt(brdList.getSelectedIndex());
update(myFrame.getGraphics(), myFrame.getInsets());
}
public Dimension getDimension() {
return FRAMEDIMENSION;
}
// Sous classe qui dessine le plateau de jeu
class Board extends JPanel {
private static final long serialVersionUID = 1L;
private int[][] boardState;
String move;
int depCol = -1;
int depLin = -1;
int arvCol = -1;
int arvLin = -1;
// The string will be the move details
// and the array the details of the board after the move has been applied.
public Board(String mv, int[][] bs) {
boardState = bs;
move = mv;
if (mv.length() == 5) {
String[] positions = mv.split("-");
depCol = (int) positions[0].charAt(0) - (int) 'A';
depLin = Integer.parseInt(positions[0].substring(1)) - 1;
arvCol = (int) positions[1].charAt(0) - (int) 'A';
arvLin = Integer.parseInt(positions[1].substring(1)) - 1;
}
}
public void drawBoard(Graphics g) {
// First draw the lines
// Board
int bx = 30;
int by = 30;
// axis labels
g.setColor(new Color(0, 0, 0));
for (int i = 1; i <= LARGEUR; i++) {
g.drawString("" + (char) ('A' + i - 1), bx + (int) ((i - 0.5)*TAILLECASE), 20);
}
for (int i = 1; i <= HAUTEUR; i++) {
g.drawString("" + i, 10, by + (int) ((i - 0.5)*TAILLECASE));
}
// Draw the circles
Color c1 = DARK;
Color c2 = LIGHT;
int casex;
int casey;
int lisere;
// fond des cases
g.setColor(c1);
g.fillRect(bx, by, LARGEUR*TAILLECASE, HAUTEUR*TAILLECASE);
for (int j = 0; j < LARGEUR; j++) {
for (int i = 0; i < HAUTEUR; i++) {
casex = bx + j*TAILLECASE;
casey = by + i*TAILLECASE;
lisere = lisereCase[i][j];
c2 = (i == depLin && j == depCol) ? HIGHLIGHT : LIGHT;
// 1er cercle
g.setColor(c2);
g.fillOval(casex + m1e, casey + m1e , diametre1e, diametre1e);
g.setColor(c1);
g.fillOval(casex + m1i, casey + m1i, diametre1i, diametre1i);
if (lisere > 1) {
// 2eme cercle
g.setColor(c2);
g.fillOval(casex + m2e, casey + m2e, diametre2e, diametre2e);
g.setColor(c1);
g.fillOval(casex + m2i, casey + m2i, diametre2i, diametre2i);
if (lisere > 2) {
// 3eme cercle
g.setColor(c2);
g.fillOval(casex + m3e, casey + m3e, diametre3e, diametre3e);
g.setColor(c1);
g.fillOval(casex + m3i, casey + m3i, diametre3i, diametre3i);
}
}
}
}
// Draw the pieces by referencing boardState array
c1 = BLACK;
c2 = WHITE;
for (int j = 0; j < LARGEUR; j++) {
for (int i = 0; i < HAUTEUR; i++) {
casex = mpiece + bx + j*TAILLECASE;
casey = mpiece + by + i*TAILLECASE;
switch (boardState[i][j]) {
case (LICORNEBLANCHE):
g.setColor(c1);
g.fillRect(casex, casey, TAILLEPION, TAILLEPION);
break;
case (PALADINBLANC):
g.setColor(c1);
g.fillOval(casex, casey, TAILLEPION, TAILLEPION);
break;
case (LICORNENOIRE):
g.setColor(c2);
g.fillRect(casex, casey, TAILLEPION, TAILLEPION);
break;
case (PALADINNOIR):
g.setColor(c2);
g.fillOval(casex, casey, TAILLEPION, TAILLEPION);
break;
}
if (i == arvLin && j == arvCol) {
g.setColor(HIGHLIGHT);
g.fillOval(casex + 20, casey + 20, TAILLEPION - 40, TAILLEPION - 40);
}
}
}
}
public void paint(Graphics g) {
drawBoard(g);
}
public void update(Graphics g) {
drawBoard(g);
}
public String toString() {
return move;
}
}
}

30
src/escampe/Bench.java Normal file
View File

@@ -0,0 +1,30 @@
package escampe;
/**
* Banc d'essai du moteur : joue quelques coups depuis l'ouverture et affiche
* profondeur, score, nœuds et vitesse. java -cp out escampe.Bench [msParCoup] [nbCoups]
*/
public class Bench {
public static void main(String[] args) {
long budget = args.length > 0 ? Long.parseLong(args[0]) : 3000;
int coups = args.length > 1 ? Integer.parseInt(args[1]) : 8;
EscampeBoard b = new EscampeBoard();
b.play("C1/A1/E1/B2/C2/D2", "noir");
b.play("C6/A6/E6/B5/C5/D5", "blanc");
Moteur mo = new Moteur();
boolean black = false; // Blanc joue en premier après les placements
for (int i = 0; i < coups && !b.gameOver(); i++) {
long t0 = System.currentTimeMillis();
int m = mo.bestMove(b, black, budget);
long dt = System.currentTimeMillis() - t0;
System.out.printf("coup %d (%s) : %-6s prof=%2d score=%7d noeuds=%9d %5dms %6.0f kN/s%n",
i, black ? "noir" : "blanc", b.moveToString(m),
mo.reachedDepth, mo.lastScore, mo.nodes, dt, mo.nodes / (dt + 1.0));
b.play(b.moveToString(m), black ? "noir" : "blanc");
black = !black;
}
System.out.println(b.gameOver() ? "Partie terminée (capture)." : "Fin du banc.");
}
}

View File

@@ -0,0 +1,58 @@
package escampe;
import java.util.*;
/**
* Mesure empirique du facteur de branchement (question Q3 du rapport) : explore
* des parties aléatoires et relève le nombre maximal de coups légaux rencontré,
* en distinguant le cas contraint (un liseré imposé) du cas libre (1er coup ou
* après un pass, lastTileType = -1). java -cp out escampe.Branching [parties]
*/
public class Branching {
public static void main(String[] args) {
int games = args.length > 0 ? Integer.parseInt(args[0]) : 20000;
Random rng = new Random(1L);
int maxConstrained = 0, maxFree = 0;
long sum = 0, count = 0;
for (int g = 0; g < games; g++) {
EscampeBoard b = new EscampeBoard();
int[] nr = rng.nextBoolean() ? new int[]{0, 1} : new int[]{4, 5};
b.play(rndPlace(b, "noir", nr, rng), "noir");
int[] wr = nr[0] == 0 ? new int[]{4, 5} : new int[]{0, 1};
b.play(rndPlace(b, "blanc", wr, rng), "blanc");
for (int ply = 0; ply < 120 && !b.gameOver(); ply++) {
String side = b.currentPlayer;
String[] mv = b.possiblesMoves(side);
int n = (mv.length == 1 && mv[0].equals("E")) ? 0 : mv.length;
if (b.lastTileType == -1) maxFree = Math.max(maxFree, n);
else maxConstrained = Math.max(maxConstrained, n);
sum += n; count++;
if (n == 0) { b.play("E", side); }
else { b.play(mv[rng.nextInt(mv.length)], side); }
}
}
System.out.println("Parties simulées : " + games);
System.out.println("Branchement max CONTRAINT : " + maxConstrained + " (un liseré imposé)");
System.out.println("Branchement max LIBRE : " + maxFree + " (1er coup / après pass)");
System.out.printf ("Branchement moyen : %.1f%n", (double) sum / count);
}
static String rndPlace(EscampeBoard b, String pl, int[] rows, Random rng) {
List<int[]> cells = new ArrayList<>();
for (int r : rows) for (int c = 0; c < 6; c++) cells.add(new int[]{r, c});
for (int t = 0; t < 50; t++) {
Collections.shuffle(cells, rng);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 6; i++) {
if (i > 0) sb.append('/');
sb.append((char) ('A' + cells.get(i)[1])).append((char) ('1' + cells.get(i)[0]));
}
if (b.isValidMove(sb.toString(), pl)) return sb.toString();
}
throw new IllegalStateException("placement");
}
}

151
src/escampe/ClientJeu.java Normal file
View File

@@ -0,0 +1,151 @@
package escampe;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.StringTokenizer;
/**
* Cette classe permet de charger dynamiquement une classe de joueur, qui doit obligatoirement
* implanter l'interface IJoueur. Vous lui donnez aussi en argument le nom de la machine distante
* (ou "localhost") sur laquelle le serveur de jeu est lancé, ainsi que le port sur lequel la
* machine écoute.
*
* Exemple: >java -cp . frontieres.ClientJeu frontieres.joueurProf localhost 1234
*
* Le client s'occupe alors de tout en lançant les méthodes implantées de l'interface IJoueur. Toute
* la gestion réseau est donc cachée.
*
* @author L. Simon (Univ. Paris-Sud)- 2006-2008
* @see IJoueur
*/
public class ClientJeu {
// Mais pas lors de la conversation avec l'arbitre
// Vous pouvez changer cela en interne si vous le souhaitez
static final int BLANC = -1;
static final int NOIR = 1;
static final int VIDE = 0;
/**
* @param args
* Dans l'ordre : NomClasseJoueur MachineServeur PortEcoute
*/
public static void main(String[] args) {
if (args.length < 3) {
System.err.println("ClientJeu Usage: NomClasseJoueur MachineServeur PortEcoute");
System.exit(1);
}
// Le nom de la classe joueur à charger dynamiquement
String classeJoueur = args[0];
// Le nom de la machine serveur a été donné en ligne de commande
String serverMachine = args[1];
// Le numéro du port sur lequel on se connecte a aussi été donné
int portNum = Integer.parseInt(args[2]);
System.out.println("Le client se connectera sur " + serverMachine + ":" + portNum);
Socket clientSocket = null;
IJoueur joueur;
String msg, firstToken;
// permet d'analyser les chaînes de caractères lues
StringTokenizer msgTokenizer;
// C'est la couleur qui doit jouer le prochain coup
int couleurAJouer;
// C'est ma couleur (quand je joue)
int maCouleur;
boolean jeuTermine = false;
try {
// initialise la socket
clientSocket = new Socket(serverMachine, portNum);
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
// *****************************************************
System.out.print("Chargement de la classe joueur " + classeJoueur + "... ");
Class<?> cjoueur = Class.forName(classeJoueur);
joueur = (IJoueur) cjoueur.newInstance();
System.out.println("Ok");
// ****************************************************
// Envoie de l'identifiant de votre quadrinome.
out.println(joueur.binoName());
System.out.println("Mon nom de quadrinome envoyé est " + joueur.binoName());
// Récupère le message sous forme de chaine de caractères
msg = in.readLine();
System.out.println(msg);
// Lit le contenu du message, toutes les infos du message
msgTokenizer = new StringTokenizer(msg, " \n\0");
if ((msgTokenizer.nextToken()).equals("Blanc")) {
System.out.println("Je suis Blanc, j'attends le mouvement de Noir.");
maCouleur = BLANC;
}
else { // doit etre égal à "Noir"
System.out.println("Je suis Noir, c'est à moi de jouer.");
maCouleur = NOIR;
}
// permet d'initialiser votre joueur avec sa couleur
joueur.initJoueur(maCouleur);
// boucle générale de jeu
do {
// Lire le msg à partir du serveur
msg = in.readLine();
msgTokenizer = new StringTokenizer(msg, " \n\0");
firstToken = msgTokenizer.nextToken();
if (firstToken.equals("FIN!")) {
jeuTermine = true;
String theWinnerIs = msgTokenizer.nextToken();
if (theWinnerIs.equals("Blanc")) {
couleurAJouer = BLANC;
}
else {
if (theWinnerIs.equals("Noir"))
couleurAJouer = NOIR;
else
couleurAJouer = VIDE;
}
if (couleurAJouer == maCouleur)
System.out.println("J'ai gagné!");
joueur.declareLeVainqueur(couleurAJouer);
}
else if (firstToken.equals("JOUEUR")) {
// On demande au joueur de jouer
if ((msgTokenizer.nextToken()).equals("Blanc")) {
couleurAJouer = BLANC;
}
else {
couleurAJouer = NOIR;
}
if (couleurAJouer == maCouleur) {
// On appelle la classe du joueur pour choisir un mouvement
msg = joueur.choixMouvement();
out.println(msg);
}
}
else if (firstToken.equals("MOUVEMENT")) {
// On lit ce que joue le joueur et on l'envoie à l'autre
joueur.mouvementEnnemi(msgTokenizer.nextToken());
}
} while (!jeuTermine);
}
catch (Exception e) {
System.out.println(e);
}
}
}

View File

@@ -0,0 +1,862 @@
package escampe;
import java.io.*;
import java.util.*;
/**
* Représentation d'un état du jeu Escampe.
*
* <p>Le plateau est un tableau {@code int[6][6]} :
* <ul>
* <li>{@code board[row][col]} avec row 0 = ligne 1 (bas), row 5 = ligne 6 (haut).</li>
* <li>col 0 = colonne A, col 5 = colonne F.</li>
* </ul>
*
* <p>Chaque case stocke l'une des constantes pièce :
* {@code EMPTY}, {@code WHITE_LICORNE}, {@code WHITE_PALADIN},
* {@code BLACK_LICORNE}, {@code BLACK_PALADIN}.
*
* <p>L'état complémentaire mémorisé :
* <ul>
* <li>{@code lastTileType} : type de liseré (1, 2 ou 3) de la case d'arrivée du dernier coup ;
* -1 = pas de contrainte (premier coup ou après un pass).</li>
* <li>{@code currentPlayer} : "noir" ou "blanc", joueur dont c'est le tour.</li>
* <li>{@code blackPlaced}, {@code whitePlaced} : phases de placement terminées.</li>
* <li>{@code blackRows} : les deux lignes (index 0-5) choisies par noir lors du placement.</li>
* </ul>
*
* <p>Règles de déplacement :
* <ul>
* <li>Une pièce avance exactement N pas orthogonaux (N = liseré de la case de départ).</li>
* <li>Elle peut changer de direction à chaque pas.</li>
* <li>Elle ne peut pas passer par une case occupée ni repasser deux fois par la même case.</li>
* <li>Au dernier pas uniquement, elle peut se poser sur la licorne adverse (capture).</li>
* </ul>
*/
public class EscampeBoard implements Partie1 {
// ── Constantes pièces ────────────────────────────────────────────────────
static final int EMPTY = 0;
static final int WHITE_LICORNE = 1;
static final int WHITE_PALADIN = 2;
static final int BLACK_LICORNE = 3;
static final int BLACK_PALADIN = 4;
/**
* Carte des liserés : {@code TILE_MAP[row][col]}.
* row 0 = ligne 1 (bas), row 5 = ligne 6 (haut). col 0 = A, col 5 = F.
*/
static final int[][] TILE_MAP = {
{1, 2, 2, 3, 1, 2}, // ligne 1
{3, 1, 3, 1, 3, 2}, // ligne 2
{2, 3, 1, 2, 1, 3}, // ligne 3
{2, 1, 3, 2, 3, 1}, // ligne 4
{1, 3, 1, 3, 1, 2}, // ligne 5
{3, 2, 2, 1, 3, 2}, // ligne 6
};
// ── État ─────────────────────────────────────────────────────────────────
int[][] board;
int lastTileType; // -1 = pas de contrainte
String currentPlayer; // "noir" ou "blanc"
boolean blackPlaced;
boolean whitePlaced;
int[] blackRows; // les 2 lignes (0-indexé) choisies par noir
// ── Constructeur ─────────────────────────────────────────────────────────
public EscampeBoard() {
board = new int[6][6];
lastTileType = -1;
currentPlayer = "noir";
blackPlaced = false;
whitePlaced = false;
blackRows = null;
}
// =========================================================================
// Fichier I/O
// =========================================================================
@Override
public void setFromFile(String fileName) {
board = new int[6][6];
lastTileType = -1;
currentPlayer = "noir";
blackPlaced = false;
whitePlaced = false;
blackRows = null;
try (BufferedReader br = new BufferedReader(new FileReader(fileName))) {
String line;
while ((line = br.readLine()) != null) {
line = line.trim();
if (line.isEmpty()) continue;
char first = line.charAt(0);
// Commentaire / méta-donnée
if (first == '%') {
parseMeta(line);
continue;
}
// Ligne de plateau : "1 XXXX 1" ou "01 XXXX 01"
int rowNum = -1;
int pos = 0;
if (first >= '1' && first <= '6') {
rowNum = first - '0';
pos = 1;
} else if (first == '0' && line.length() > 1) {
char second = line.charAt(1);
if (second >= '1' && second <= '6') {
rowNum = second - '0';
pos = 2;
}
}
if (rowNum != -1) {
int rowIdx = rowNum - 1;
while (pos < line.length() && line.charAt(pos) == ' ') pos++;
for (int c = 0; c < 6 && pos + c < line.length(); c++) {
board[rowIdx][c] = charToPiece(line.charAt(pos + c));
}
}
}
} catch (IOException e) {
throw new RuntimeException("Erreur de lecture du fichier : " + fileName, e);
}
// Si pas de méta-commentaires, on infère l'état à partir des pièces
inferState();
}
/** Parse une ligne de méta-commentaire "% clé: valeur". */
private void parseMeta(String line) {
if (line.startsWith("% lastTileType:")) {
lastTileType = Integer.parseInt(line.substring(15).trim());
} else if (line.startsWith("% currentPlayer:")) {
currentPlayer = line.substring(16).trim();
} else if (line.startsWith("% blackPlaced:")) {
blackPlaced = Boolean.parseBoolean(line.substring(14).trim());
} else if (line.startsWith("% whitePlaced:")) {
whitePlaced = Boolean.parseBoolean(line.substring(14).trim());
} else if (line.startsWith("% blackRows:")) {
String s = line.substring(12).trim();
String[] parts = s.split(",");
int r0 = Integer.parseInt(parts[0].trim());
int r1 = Integer.parseInt(parts[1].trim());
if (r0 >= 0) blackRows = new int[]{r0, r1};
}
}
/**
* Infère {@code blackPlaced}, {@code whitePlaced} et {@code blackRows}
* à partir des pièces présentes sur le plateau
* (utilisé quand le fichier ne contient pas de méta-commentaires).
*/
private void inferState() {
if (blackPlaced && whitePlaced) return; // méta déjà chargée
int bc = 0, wc = 0;
Set<Integer> bRowSet = new TreeSet<>();
for (int r = 0; r < 6; r++) {
for (int c = 0; c < 6; c++) {
int p = board[r][c];
if (p == BLACK_LICORNE || p == BLACK_PALADIN) { bc++; bRowSet.add(r); }
if (p == WHITE_LICORNE || p == WHITE_PALADIN) { wc++; }
}
}
if (!blackPlaced && bc == 6) {
blackPlaced = true;
// Bord de noir déduit d'une ligne occupée (robuste à 1 seule ligne).
int anyRow = bRowSet.iterator().next();
blackRows = (anyRow <= 1) ? new int[]{0, 1} : new int[]{4, 5};
}
if (!whitePlaced && wc == 6) {
whitePlaced = true;
}
}
@Override
public void saveToFile(String fileName) {
try (PrintWriter pw = new PrintWriter(new FileWriter(fileName))) {
pw.println("% Escampe - sauvegarde du plateau");
pw.println("% lastTileType: " + lastTileType);
pw.println("% currentPlayer: " + currentPlayer);
pw.println("% blackPlaced: " + blackPlaced);
pw.println("% whitePlaced: " + whitePlaced);
if (blackRows != null) {
pw.println("% blackRows: " + blackRows[0] + "," + blackRows[1]);
} else {
pw.println("% blackRows: -1,-1");
}
// Lignes 6 à 1 (haut vers bas dans le fichier)
for (int rowIdx = 5; rowIdx >= 0; rowIdx--) {
int rowNum = rowIdx + 1;
StringBuilder sb = new StringBuilder();
String rowLabel = String.format("%02d", rowNum);
sb.append(rowLabel).append(' ');
for (int c = 0; c < 6; c++) sb.append(pieceToChar(board[rowIdx][c]));
sb.append(' ').append(rowLabel);
pw.println(sb.toString());
}
} catch (IOException e) {
throw new RuntimeException("Erreur d'écriture du fichier : " + fileName, e);
}
}
// =========================================================================
// Fin de partie
// =========================================================================
@Override
public boolean gameOver() {
if (!blackPlaced || !whitePlaced) return false;
boolean wl = false, bl = false;
for (int r = 0; r < 6; r++)
for (int c = 0; c < 6; c++) {
if (board[r][c] == WHITE_LICORNE) wl = true;
if (board[r][c] == BLACK_LICORNE) bl = true;
}
return !wl || !bl;
}
// =========================================================================
// Validation d'un coup
// =========================================================================
@Override
public boolean isValidMove(String move, String player) {
if (move == null || move.isEmpty()) return false;
if (!"noir".equals(player) && !"blanc".equals(player)) return false;
if (move.contains("/")) return isValidPlacement(move, player);
if ("E".equals(move)) return isValidPass(player);
return isValidRegularMove(move, player);
}
/**
* Valide un coup de placement "P1/P2/P3/P4/P5/P6"
* (P1 = licorne, P2-P6 = paladins).
*/
private boolean isValidPlacement(String move, String player) {
if ("noir".equals(player) && blackPlaced) return false;
if ("blanc".equals(player) && whitePlaced) return false;
if (!player.equals(currentPlayer)) return false;
if ("blanc".equals(player) && !blackPlaced) return false;
String[] parts = move.split("/");
if (parts.length != 6) return false;
int[][] pos = new int[6][2];
for (int i = 0; i < 6; i++) {
int[] cell = cellFromString(parts[i]);
if (cell == null) return false;
pos[i] = cell;
}
// Zone autorisée
if ("noir".equals(player)) {
boolean allLow = true, allHigh = true;
for (int[] p : pos) {
if (p[0] != 0 && p[0] != 1) allLow = false;
if (p[0] != 4 && p[0] != 5) allHigh = false;
}
if (!allLow && !allHigh) return false;
} else {
if (blackRows == null) return false;
int[] wr = complementaryRows(blackRows);
for (int[] p : pos) {
if (p[0] != wr[0] && p[0] != wr[1]) return false;
}
}
// Pas de doublons, cases vides
Set<String> seen = new HashSet<>();
for (int[] p : pos) {
if (!seen.add(p[0] + "," + p[1])) return false;
if (board[p[0]][p[1]] != EMPTY) return false;
}
return true;
}
/** Valide un pass "E" : uniquement si aucun coup régulier n'est disponible. */
private boolean isValidPass(String player) {
if (!player.equals(currentPlayer)) return false;
if (!blackPlaced || !whitePlaced) return false;
if (gameOver()) return false;
String[] m = possiblesMoves(player);
return m.length == 1 && "E".equals(m[0]);
}
/** Valide un coup régulier "XX-YY". */
private boolean isValidRegularMove(String move, String player) {
if (!blackPlaced || !whitePlaced) return false;
if (gameOver()) return false;
if (!player.equals(currentPlayer)) return false;
int dash = move.indexOf('-');
if (dash < 1 || dash >= move.length() - 1) return false;
int[] from = cellFromString(move.substring(0, dash));
int[] to = cellFromString(move.substring(dash + 1));
if (from == null || to == null) return false;
if (!belongsToPlayer(board[from[0]][from[1]], player)) return false;
if (lastTileType != -1 && TILE_MAP[from[0]][from[1]] != lastTileType) return false;
return getReachableSquares(from[0], from[1], player).contains(to[0] + "," + to[1]);
}
// =========================================================================
// Génération de coups
// =========================================================================
@Override
public String[] possiblesMoves(String player) {
// Pendant le placement le nombre de combinaisons est trop grand pour être énuméré
if (!blackPlaced || !whitePlaced) return new String[0];
if (gameOver()) return new String[0];
List<String> moves = new ArrayList<>();
for (int r = 0; r < 6; r++) {
for (int c = 0; c < 6; c++) {
if (!belongsToPlayer(board[r][c], player)) continue;
if (lastTileType != -1 && TILE_MAP[r][c] != lastTileType) continue;
for (String dest : getReachableSquares(r, c, player)) {
String[] d = dest.split(",");
moves.add(stringFromCell(r, c) + "-"
+ stringFromCell(Integer.parseInt(d[0]), Integer.parseInt(d[1])));
}
}
}
if (moves.isEmpty()) return new String[]{"E"};
return moves.toArray(new String[0]);
}
// =========================================================================
// Jouer un coup
// =========================================================================
@Override
public void play(String move, String player) {
if (!isValidMove(move, player))
throw new IllegalArgumentException("Coup invalide : '" + move + "' pour " + player);
if (move.contains("/")) {
playPlacement(move, player);
} else if ("E".equals(move)) {
// Pass : supprime la contrainte de liseré (règle officielle)
lastTileType = -1;
currentPlayer = opponent(currentPlayer);
} else {
playRegular(move, player);
}
}
private void playPlacement(String move, String player) {
String[] parts = move.split("/");
int[][] pos = new int[6][2];
for (int i = 0; i < 6; i++) pos[i] = cellFromString(parts[i]);
int licorne = "noir".equals(player) ? BLACK_LICORNE : WHITE_LICORNE;
int paladin = "noir".equals(player) ? BLACK_PALADIN : WHITE_PALADIN;
board[pos[0][0]][pos[0][1]] = licorne;
for (int i = 1; i < 6; i++) board[pos[i][0]][pos[i][1]] = paladin;
if ("noir".equals(player)) {
blackPlaced = true;
// Bord de noir (bas {0,1} ou haut {4,5}), déduit de la ligne de la licorne.
blackRows = (pos[0][0] <= 1) ? new int[]{0, 1} : new int[]{4, 5};
currentPlayer = "blanc";
} else {
whitePlaced = true;
lastTileType = -1; // pas de contrainte pour le premier coup régulier
currentPlayer = "blanc"; // blanc joue en premier
}
}
private void playRegular(String move, String player) {
int dash = move.indexOf('-');
int[] from = cellFromString(move.substring(0, dash));
int[] to = cellFromString(move.substring(dash + 1));
board[to[0]][to[1]] = board[from[0]][from[1]]; // capture si case adverse
board[from[0]][from[1]] = EMPTY;
lastTileType = TILE_MAP[to[0]][to[1]];
currentPlayer = opponent(currentPlayer);
}
// =========================================================================
// Algorithme de déplacement (DFS)
// =========================================================================
/**
* Calcule l'ensemble des cases atteignables depuis (fromRow, fromCol).
* Résultats encodés sous forme "row,col".
*/
Set<String> getReachableSquares(int fromRow, int fromCol, String player) {
Set<String> result = new HashSet<>();
boolean[][] visited = new boolean[6][6];
visited[fromRow][fromCol] = true;
dfs(fromRow, fromCol, TILE_MAP[fromRow][fromCol], player, visited, result);
return result;
}
/**
* DFS récursif pour le calcul des destinations.
*
* <p>À chaque appel, la pièce se trouve en (row, col) et doit encore effectuer
* {@code stepsLeft} pas. Les cases déjà visitées dans le chemin courant sont
* marquées dans {@code visited} (réinitialisation après backtrack).
*
* <p>Règles :
* <ul>
* <li>Pas intermédiaires (stepsLeft > 1) : la case suivante doit être vide.</li>
* <li>Dernier pas (stepsLeft == 1) : la case peut être vide ou contenir
* la licorne adverse (capture).</li>
* </ul>
*/
private void dfs(int row, int col, int stepsLeft,
String player, boolean[][] visited, Set<String> result) {
if (stepsLeft == 0) {
result.add(row + "," + col);
return;
}
// Directions orthogonales : haut, bas, gauche, droite
int[] dr = {-1, 1, 0, 0};
int[] dc = { 0, 0, -1, 1};
for (int d = 0; d < 4; d++) {
int nr = row + dr[d];
int nc = col + dc[d];
if (nr < 0 || nr >= 6 || nc < 0 || nc >= 6) continue;
if (visited[nr][nc]) continue;
int occ = board[nr][nc];
boolean canStep;
if (stepsLeft > 1) {
// Pas intermédiaire : case obligatoirement vide
canStep = (occ == EMPTY);
} else {
// Dernier pas : vide OU capture de la licorne adverse
canStep = (occ == EMPTY)
|| ("blanc".equals(player) && occ == BLACK_LICORNE)
|| ("noir".equals(player) && occ == WHITE_LICORNE);
}
if (!canStep) continue;
visited[nr][nc] = true;
dfs(nr, nc, stepsLeft - 1, player, visited, result);
visited[nr][nc] = false; // backtrack
}
}
// Chemin de génération « int » pour le moteur, sans allocation de String.
// Case = row*6+col (0..35) ; coup = from*36+to ; pass = MOVE_PASS ; black = noir.
// Équivalent au chemin String vérifié (contrôlé par VerifMoves).
static final int MOVE_PASS = -1;
record Undo(int move, int captured, int savedLastTile, String savedPlayer) {}
/** Copie profonde de l'état (le moteur cherche sur une copie, jamais sur le live). */
EscampeBoard copy() {
EscampeBoard b = new EscampeBoard();
for (int r = 0; r < 6; r++) b.board[r] = board[r].clone();
b.lastTileType = lastTileType;
b.currentPlayer = currentPlayer;
b.blackPlaced = blackPlaced;
b.whitePlaced = whitePlaced;
b.blackRows = (blackRows == null) ? null : blackRows.clone();
return b;
}
private boolean isSide(int piece, boolean black) {
return black ? (piece == BLACK_LICORNE || piece == BLACK_PALADIN)
: (piece == WHITE_LICORNE || piece == WHITE_PALADIN);
}
/** Version allouante de {@link #genMovesIntInto}, pour les tests. */
int[] genMovesInt(boolean black) {
int[] buf = new int[256];
int n = genMovesIntInto(black, buf);
if (n == 0) return new int[0];
return java.util.Arrays.copyOf(buf, n);
}
/**
* Écrit les coups de la phase régulière de {@code black} dans {@code buf} et
* renvoie leur nombre : 0 hors phase régulière, ou {@code {MOVE_PASS}} si bloqué.
*/
int genMovesIntInto(boolean black, int[] buf) {
if (!blackPlaced || !whitePlaced) return 0;
if (gameOver()) return 0;
int n = 0;
for (int r = 0; r < 6; r++) {
for (int c = 0; c < 6; c++) {
if (!isSide(board[r][c], black)) continue;
if (lastTileType != -1 && TILE_MAP[r][c] != lastTileType) continue;
int from = r * 6 + c;
long reach = dfsMask(r, c, TILE_MAP[r][c], black, 1L << from, 0L);
while (reach != 0L) {
int t = Long.numberOfTrailingZeros(reach);
reach &= reach - 1;
buf[n++] = from * 36 + t;
}
}
}
if (n == 0) { buf[0] = MOVE_PASS; return 1; }
return n;
}
/** DFS sur masque de bits (équivalent de {@link #dfs}) : {@code visited}/{@code reach} = ensembles de cases. */
private long dfsMask(int row, int col, int steps, boolean black, long visited, long reach) {
if (steps == 0) return reach | (1L << (row * 6 + col));
final int[] dr = {-1, 1, 0, 0};
final int[] dc = { 0, 0, -1, 1};
for (int d = 0; d < 4; d++) {
int nr = row + dr[d], nc = col + dc[d];
if (nr < 0 || nr >= 6 || nc < 0 || nc >= 6) continue;
int ncell = nr * 6 + nc;
if ((visited & (1L << ncell)) != 0) continue;
int occ = board[nr][nc];
boolean canStep;
if (steps > 1) {
canStep = (occ == EMPTY);
} else {
canStep = (occ == EMPTY)
|| (black && occ == WHITE_LICORNE)
|| (!black && occ == BLACK_LICORNE);
}
if (!canStep) continue;
reach = dfsMask(nr, nc, steps - 1, black, visited | (1L << ncell), reach);
}
return reach;
}
/** Applique un coup int (régulier ou {@code MOVE_PASS}) et renvoie le jeton d'annulation. */
Undo makeInt(int move) {
int savedLast = lastTileType;
String savedPlayer = currentPlayer;
if (move == MOVE_PASS) {
lastTileType = -1;
currentPlayer = opponent(currentPlayer);
return new Undo(move, EMPTY, savedLast, savedPlayer);
}
int from = move / 36, to = move % 36;
int fr = from / 6, fc = from % 6, tr = to / 6, tc = to % 6;
int captured = board[tr][tc];
board[tr][tc] = board[fr][fc];
board[fr][fc] = EMPTY;
lastTileType = TILE_MAP[tr][tc];
currentPlayer = opponent(currentPlayer);
return new Undo(move, captured, savedLast, savedPlayer);
}
/** Annule l'effet de {@link #makeInt}. */
void unmakeInt(Undo u) {
if (u.move() != MOVE_PASS) {
int from = u.move() / 36, to = u.move() % 36;
int fr = from / 6, fc = from % 6, tr = to / 6, tc = to % 6;
board[fr][fc] = board[tr][tc];
board[tr][tc] = u.captured();
}
lastTileType = u.savedLastTile();
currentPlayer = u.savedPlayer();
}
/** Code int → notation "A1-B2" (ou "E" pour le pass). */
String moveToString(int move) {
if (move == MOVE_PASS) return "E";
int from = move / 36, to = move % 36;
return stringFromCell(from / 6, from % 6) + "-" + stringFromCell(to / 6, to % 6);
}
// =========================================================================
// Méthodes utilitaires
// =========================================================================
private int charToPiece(char c) {
switch (c) {
case 'B': return WHITE_LICORNE;
case 'b': return WHITE_PALADIN;
case 'N': return BLACK_LICORNE;
case 'n': return BLACK_PALADIN;
default: return EMPTY;
}
}
private char pieceToChar(int piece) {
switch (piece) {
case WHITE_LICORNE: return 'B';
case WHITE_PALADIN: return 'b';
case BLACK_LICORNE: return 'N';
case BLACK_PALADIN: return 'n';
default: return '-';
}
}
/**
* Convertit une chaîne "A1"-"F6" en coordonnées {row, col} (0-indexé).
* Retourne null si le format est invalide.
*/
int[] cellFromString(String s) {
if (s == null || s.length() < 2) return null;
s = s.trim();
char colC = Character.toUpperCase(s.charAt(0));
char rowC = s.charAt(1);
if (colC < 'A' || colC > 'F') return null;
if (rowC < '1' || rowC > '6') return null;
return new int[]{rowC - '1', colC - 'A'};
}
/** Convertit des coordonnées internes en notation "A1"-"F6". */
String stringFromCell(int row, int col) {
return "" + (char)('A' + col) + (char)('1' + row);
}
private boolean belongsToPlayer(int piece, String player) {
if ("blanc".equals(player)) return piece == WHITE_LICORNE || piece == WHITE_PALADIN;
if ("noir".equals(player)) return piece == BLACK_LICORNE || piece == BLACK_PALADIN;
return false;
}
private String opponent(String player) {
return "blanc".equals(player) ? "noir" : "blanc";
}
/**
* Retourne les deux lignes (0-indexé) que doit utiliser blanc,
* sachant que noir a choisi {@code bRows}.
* Noir sur {0,1} → blanc sur {4,5} ; noir sur {4,5} → blanc sur {0,1}.
*/
private int[] complementaryRows(int[] bRows) {
return (bRows[0] == 0) ? new int[]{4, 5} : new int[]{0, 1};
}
// =========================================================================
// Affichage
// =========================================================================
/** Affiche le plateau en console (ligne 6 en haut). */
public void printBoard() {
System.out.println(" A B C D E F liseré");
for (int r = 5; r >= 0; r--) {
System.out.print((r + 1) + " [ ");
for (int c = 0; c < 6; c++) System.out.print(pieceToChar(board[r][c]) + " ");
System.out.print("] " + (r + 1) + " |");
for (int c = 0; c < 6; c++) System.out.print(" " + TILE_MAP[r][c]);
System.out.println();
}
System.out.println("lastTileType=" + lastTileType
+ " currentPlayer=" + currentPlayer + "\n");
}
// =========================================================================
// Main de démonstration
// =========================================================================
public static void main(String[] args) throws IOException {
System.out.println("=========================================");
System.out.println(" Demo EscampeBoard ");
System.out.println("=========================================\n");
// ── Placements utilisés dans plusieurs scenarios ──────────────────
// Noir : lignes 5-6 (rows 4-5) — licorne en A6, paladins en B6 C6 D5 E5 F5
final String NOIR_PL = "A6/B6/C6/D5/E5/F5";
// Blanc : lignes 1-2 (rows 0-1) — licorne en D2, paladins en A1 B1 C1 E1 F2
final String BLANC_PL = "D2/A1/B1/C1/E1/F2";
// ─────────────────────────────────────────────────────────────────
// 1. PHASE DE PLACEMENT
// ─────────────────────────────────────────────────────────────────
System.out.println("=== 1. PHASE DE PLACEMENT ===");
EscampeBoard b = new EscampeBoard();
// Tentatives invalides avant le placement normal
System.out.println("Blanc tente de placer avant noir : "
+ b.isValidMove(BLANC_PL, "blanc") + " (attendu: false)");
System.out.println("Noir placement au milieu du plateau : "
+ b.isValidMove("A3/B3/C3/D3/E3/F3", "noir") + " (attendu: false)");
System.out.println("Noir placement sur deux bords diff. : "
+ b.isValidMove("A1/B1/C1/D5/E5/F5", "noir") + " (attendu: false)");
// Placement valide de noir
System.out.println("\nNoir place : " + NOIR_PL
+ " valid=" + b.isValidMove(NOIR_PL, "noir"));
b.play(NOIR_PL, "noir");
System.out.println(" blackPlaced=" + b.blackPlaced
+ " blackRows=[" + b.blackRows[0] + "," + b.blackRows[1] + "]"
+ " currentPlayer=" + b.currentPlayer);
// Placement valide de blanc
System.out.println("Blanc place : " + BLANC_PL
+ " valid=" + b.isValidMove(BLANC_PL, "blanc"));
b.play(BLANC_PL, "blanc");
System.out.println(" whitePlaced=" + b.whitePlaced
+ " currentPlayer=" + b.currentPlayer);
b.printBoard();
// ─────────────────────────────────────────────────────────────────
// 2. PHASE REGULIERE — contrainte de liseré
// ─────────────────────────────────────────────────────────────────
System.out.println("=== 2. PHASE REGULIERE ===");
System.out.println("lastTileType=" + b.lastTileType
+ " (pas de contrainte pour le premier coup)\n");
// Blanc joue en premier, pas de contrainte
String[] bMoves = b.possiblesMoves("blanc");
System.out.println("Coups possibles pour blanc : " + bMoves.length + " coups");
System.out.printf("Exemples : %s %s %s%n",
bMoves[0],
bMoves.length > 1 ? bMoves[1] : "",
bMoves.length > 2 ? bMoves[2] : "");
String m1 = bMoves[0];
System.out.println("\nBlanc joue : " + m1 + " valid=" + b.isValidMove(m1, "blanc"));
b.play(m1, "blanc");
System.out.println(" lastTileType=" + b.lastTileType
+ " (liseré de la case d'arrivée = contrainte pour noir)"
+ " currentPlayer=" + b.currentPlayer);
// Tentative invalide : blanc rejoue hors de son tour
System.out.println("\nBlanc rejoue hors tour : "
+ b.isValidMove(m1, "blanc") + " (attendu: false)");
// Tentative invalide : noir joue depuis un mauvais liseré
String badNoirMove = findMoveFromWrongTile(b, "noir");
if (badNoirMove != null) {
System.out.println("Noir depuis mauvais liseré (" + badNoirMove + ") : "
+ b.isValidMove(badNoirMove, "noir") + " (attendu: false)");
}
// Coup valide de noir
String[] nMoves = b.possiblesMoves("noir");
System.out.println("\nCoups possibles pour noir (liseré " + b.lastTileType + ") : "
+ nMoves.length + " coups");
String m2 = nMoves[0];
System.out.println("Noir joue : " + m2 + " valid=" + b.isValidMove(m2, "noir"));
b.play(m2, "noir");
System.out.println(" lastTileType=" + b.lastTileType
+ " currentPlayer=" + b.currentPlayer);
// ─────────────────────────────────────────────────────────────────
// 3. ROUND-TRIP FICHIER
// ─────────────────────────────────────────────────────────────────
System.out.println("\n=== 3. ROUND-TRIP FICHIER ===");
b.saveToFile("escampe_save.txt");
System.out.println("Sauvegardé dans escampe_save.txt");
EscampeBoard b2 = new EscampeBoard();
b2.setFromFile("escampe_save.txt");
System.out.println("Rechargé : lastTileType=" + b2.lastTileType
+ " currentPlayer=" + b2.currentPlayer);
System.out.println("Plateaux identiques : " + Arrays.deepEquals(b.board, b2.board));
System.out.println("lastTileType identique : " + (b.lastTileType == b2.lastTileType));
System.out.println("currentPlayer identique : " + b.currentPlayer.equals(b2.currentPlayer));
// ─────────────────────────────────────────────────────────────────
// 4. SCENARIO DE PASS (E)
// ─────────────────────────────────────────────────────────────────
System.out.println("\n=== 4. SCENARIO DE PASS ===");
EscampeBoard bPass = new EscampeBoard();
bPass.play(NOIR_PL, "noir");
bPass.play(BLANC_PL, "blanc");
// Forcer une situation où noir n'a aucun coup :
// lastTileType=2, mais toutes les pièces noires sont sur liseré 1 ou 3.
for (int r = 0; r < 6; r++) Arrays.fill(bPass.board[r], EMPTY);
bPass.board[0][3] = WHITE_LICORNE; // D1 liseré=3
bPass.board[0][0] = WHITE_PALADIN; // A1 liseré=1
bPass.board[0][4] = WHITE_PALADIN; // E1 liseré=1
bPass.board[5][0] = BLACK_LICORNE; // A6 liseré=3
bPass.board[4][4] = BLACK_PALADIN; // E5 liseré=1
bPass.board[4][2] = BLACK_PALADIN; // C5 liseré=1
bPass.lastTileType = 2; // blanc vient de poser sur liseré 2
bPass.currentPlayer = "noir";
System.out.println("Pièces noires sur liserés 1 et 3, contrainte = 2");
System.out.println("possiblesMoves(noir) = "
+ Arrays.toString(bPass.possiblesMoves("noir")) + " (attendu: [E])");
System.out.println("isValidMove(E, noir) = "
+ bPass.isValidMove("E", "noir") + " (attendu: true)");
System.out.println("isValidMove(E, blanc) = "
+ bPass.isValidMove("E", "blanc") + " (attendu: false, pas son tour)");
bPass.play("E", "noir");
System.out.println("Après pass : lastTileType=" + bPass.lastTileType
+ " (attendu: -1) currentPlayer=" + bPass.currentPlayer);
// ─────────────────────────────────────────────────────────────────
// 5. CAPTURE ET FIN DE PARTIE
// ─────────────────────────────────────────────────────────────────
System.out.println("\n=== 5. CAPTURE ET FIN DE PARTIE ===");
EscampeBoard bCap = new EscampeBoard();
bCap.play(NOIR_PL, "noir");
bCap.play(BLANC_PL, "blanc");
// Mise en scène :
// - Blanc paladin en B1 (row=0,col=1 ; liseré=2)
// → 2 pas orthogonaux : B1 -> B2 -> B3
// - Licorne noire en B3 (row=2,col=1) ; case B2 vide
// - lastTileType=2 → blanc peut jouer depuis B1
for (int r = 0; r < 6; r++) Arrays.fill(bCap.board[r], EMPTY);
bCap.board[0][1] = WHITE_PALADIN; // B1 liseré=2
bCap.board[0][3] = WHITE_LICORNE; // D1 (garde-fou : licorne blanche présente)
bCap.board[2][1] = BLACK_LICORNE; // B3
bCap.board[5][5] = BLACK_PALADIN; // F6 (présence de pièce noire restante)
bCap.lastTileType = 2;
bCap.currentPlayer = "blanc";
System.out.println("Avant capture :");
bCap.printBoard();
System.out.println("gameOver = " + bCap.gameOver() + " (attendu: false)");
// Coup invalide : un pas seulement (B1->B2), pas assez de cases
System.out.println("Coup B1-B2 (1 pas, manque 1) : "
+ bCap.isValidMove("B1-B2", "blanc") + " (attendu: false)");
// Coup valide : deux pas (B1->B2->B3), B2 vide, B3 = licorne noire
System.out.println("Coup B1-B3 (2 pas, capture) : "
+ bCap.isValidMove("B1-B3", "blanc") + " (attendu: true)");
bCap.play("B1-B3", "blanc");
System.out.println("Après capture :");
bCap.printBoard();
System.out.println("gameOver = " + bCap.gameOver() + " (attendu: true)");
System.out.println("Blanc gagne !");
System.out.println("\n=========================================");
System.out.println(" Demo terminee ");
System.out.println("=========================================");
}
/**
* Utilitaire pour la démo : trouve un coup depuis une pièce
* de {@code player} dont le liseré est différent de {@code lastTileType}.
* Retourne null si aucune telle pièce n'a de destinations.
*/
private static String findMoveFromWrongTile(EscampeBoard b, String player) {
for (int r = 0; r < 6; r++) {
for (int c = 0; c < 6; c++) {
if (!b.belongsToPlayer(b.board[r][c], player)) continue;
if (TILE_MAP[r][c] == b.lastTileType) continue;
Set<String> reach = b.getReachableSquares(r, c, player);
if (!reach.isEmpty()) {
String dest = reach.iterator().next();
String[] parts = dest.split(",");
return b.stringFromCell(r, c) + "-"
+ b.stringFromCell(Integer.parseInt(parts[0]),
Integer.parseInt(parts[1]));
}
}
}
return null;
}
}

65
src/escampe/IJoueur.java Normal file
View File

@@ -0,0 +1,65 @@
package escampe;
/**
* Voici l'interface abstraite qu'il suffit d'implanter pour jouer. Ensuite, vous devez utiliser
* ClientJeu en lui donnant le nom de votre classe pour qu'il la charge et se connecte au serveur.
*
* @author L. Simon (Univ. Paris-Sud)- 2006-2013
*
*/
public interface IJoueur {
// Mais pas lors de la conversation avec l'arbitre (méthodes initJoueur et getNumJoueur)
// Vous pouvez changer cela en interne si vous le souhaitez
static final int BLANC = -1;
static final int NOIR = 1;
/**
* L'arbitre vient de lancer votre joueur. Il lui informe par cette méthode que vous devez jouer
* dans cette couleur. Vous pouvez utiliser cette m?thode abstraite, ou la méthode constructeur
* de votre classe, pour initialiser vos structures.
*
* @param mycolour
* La couleur dans laquelle vous allez jouer (-1=BLANC, 1=NOIR)
*/
public void initJoueur(int mycolour);
// Doit retourner l'argument passé par la fonction ci-dessus (constantes BLANC ou NOIR)
public int getNumJoueur();
/**
* C'est ici que vous devez faire appel à votre IA pour trouver le meilleur coup à jouer sur le
* plateau courant.
*
* @return une chaine décrivant le mouvement. Cette chaine doit être décrite exactement comme
* sur l'exemple : String msg = "" + positionInitiale + "-" +positionFinale + ""; ou "PASSE";
* Chaque position contient une lettre et un num?ro, par exemple:A1,B2 (coup "A1-B2")
*/
public String choixMouvement();
/**
* Méthode appelée par l'arbitre pour désigner le vainqueur. Vous pouvez en profiter pour
* imprimer une bannière de joie... Si vous gagnez...
*
* @param colour
* La couleur du gagnant (BLANC=-1, NOIR=1).
*/
public void declareLeVainqueur(int colour);
/**
* On suppose que l'arbitre a vérifié que le mouvement ennemi était bien légal. Il vous informe
* du mouvement ennemi. A vous de répercuter ce mouvement dans vos structures. Comme par exemple
* éliminer les pions que ennemi vient de vous prendre par ce mouvement. Il n'est pas nécessaire
* de réfléchir déjà à votre prochain coup à jouer : pour cela l'arbitre appelera ensuite
* choixMouvement().
*
* @param coup
* une chaine décrivant le mouvement: par exemple: "A1-B2"
*/
public void mouvementEnnemi(String coup);
public String binoName();
}

View File

@@ -0,0 +1,117 @@
package escampe;
/**
* Joueur du tournoi (Puyaubreau / Russac). Enveloppe un {@link EscampeBoard}
* tenu à jour à chaque coup et délègue la décision à {@link Moteur}.
*
* L'interface {@code IJoueur} parle en entiers ({@code NOIR=1}, {@code BLANC=-1})
* et place les pièces via le même canal que les coups : le premier
* {@code choixMouvement} renvoie un placement, les suivants des coups. Le pass
* se note {@code "E"} (et non {@code "PASSE"}, contrairement au Javadoc d'IJoueur).
*/
public class JoueurPuyaubreauRussac implements IJoueur {
private int couleur = NOIR;
private EscampeBoard board;
private final Moteur moteur = new Moteur();
// Budget de temps : enveloppe sous la limite arbitre de 300 s, fraction du
// temps restant par coup. Surchargeable par -Descampe.* pour les tests.
private static final long BUDGET_MS = Long.getLong("escampe.budgetMs", 280_000);
private static final long MAX_SLICE_MS = Long.getLong("escampe.maxSliceMs", 6_000);
private static final long MIN_SLICE_MS = 120;
private static final int TIME_DIVISOR = 12;
private static final boolean DEBUG = Boolean.getBoolean("escampe.debug");
private long usedMs = 0;
@Override
public void initJoueur(int mycolour) {
couleur = mycolour;
board = new EscampeBoard();
}
@Override
public int getNumJoueur() {
return couleur;
}
@Override
public String binoName() {
return "Puyaubreau_Russac";
}
private String myStr() { return couleur == NOIR ? "noir" : "blanc"; }
private String oppStr() { return couleur == NOIR ? "blanc" : "noir"; }
@Override
public String choixMouvement() {
if (board.gameOver()) return "xxxxx"; // fin de partie sous Solo ; l'arbitre, lui, n'appelle plus
if (couleur == NOIR && !board.blackPlaced) {
String pl = placement(new int[]{0, 1});
board.play(pl, "noir");
return pl;
}
if (couleur == BLANC && !board.whitePlaced) {
String pl = placement(complementaryRows(board.blackRows));
board.play(pl, "blanc");
return pl;
}
String move = chooseMove();
board.play(move, myStr());
return move;
}
@Override
public void mouvementEnnemi(String coup) {
if (coup == null) return;
coup = coup.trim();
if (coup.isEmpty() || coup.equals("xxxxx")) return;
try {
board.play(coup, oppStr());
} catch (RuntimeException e) {
// L'arbitre garantit la légalité ; on ne plante pas sur une désync.
System.err.println("[" + binoName() + "] coup ennemi rejeté : " + coup);
}
}
@Override
public void declareLeVainqueur(int colour) {
if (colour == couleur) System.out.println("[" + binoName() + "] Victoire !");
else if (colour == -couleur) System.out.println("[" + binoName() + "] Défaite.");
}
/** Temps alloué au moteur pour ce coup, puis appel de la recherche. */
private String chooseMove() {
long remaining = BUDGET_MS - usedMs;
long slice = Math.max(MIN_SLICE_MS, Math.min(remaining / TIME_DIVISOR, MAX_SLICE_MS));
if (remaining < 1500) slice = Math.max(40, remaining - 300);
long t0 = System.currentTimeMillis();
int m = moteur.bestMove(board, couleur == NOIR, slice);
usedMs += System.currentTimeMillis() - t0;
if (DEBUG) {
System.err.printf("[%s] %s prof=%d score=%d noeuds=%d cumul=%ds%n",
binoName(), board.moveToString(m), moteur.reachedDepth, moteur.lastScore,
moteur.nodes, usedMs / 1000);
}
return board.moveToString(m);
}
private int[] complementaryRows(int[] blackRows) {
return blackRows[0] == 0 ? new int[]{4, 5} : new int[]{0, 1};
}
/**
* Placement : licorne dans un coin, ses deux voisines occupées par des
* paladins (la licorne devient incapturable), les trois autres paladins sur
* des liserés 1/2/3 distincts pour ne jamais être contraint de passer.
*/
private String placement(int[] rows) {
boolean bottom = Math.min(rows[0], rows[1]) == 0;
return bottom ? "A1/A2/B1/E1/F1/C2" // coin A1, murs A2/B1, mobiles E1(1)/F1(2)/C2(3)
: "A6/A5/B6/C5/F5/E6"; // coin A6, murs A5/B6, mobiles C5(1)/F5(2)/E6(3)
}
}

137
src/escampe/Moteur.java Normal file
View File

@@ -0,0 +1,137 @@
package escampe;
/**
* Recherche du meilleur coup : negamax + élagage alpha-bêta + approfondissement
* itératif sous limite de temps. La recherche se fait sur une copie du plateau,
* via makeInt/unmakeInt (sans allocation). Capturer la licorne adverse vaut
* {@code WIN - ply} (gagner vite plutôt que tard).
*/
final class Moteur {
static final int WIN = 1_000_000;
static final int INF = 2_000_000;
static final int MAX_DEPTH = 40;
private static final int MAX_PLY = MAX_DEPTH + 8;
// Poids de l'évaluation (proximité paladins/licornes : attaque vs défense).
int wAtkSum = 2, wDefSum = 2, wAtkMin = 8, wDefMin = 8;
private long deadline;
private boolean timedOut;
long nodes;
int reachedDepth;
int lastScore;
private final int[][] buf = new int[MAX_PLY][256]; // un buffer de coups par profondeur
int bestMove(EscampeBoard root, boolean black, long budgetMs) {
EscampeBoard pos = root.copy();
deadline = System.currentTimeMillis() + Math.max(1, budgetMs);
nodes = 0; timedOut = false; reachedDepth = 0; lastScore = 0;
int[] moves = new int[256];
int n = pos.genMovesIntInto(black, moves);
if (n == 0 || moves[0] == EscampeBoard.MOVE_PASS) return EscampeBoard.MOVE_PASS;
orderCapturesFirst(pos, moves, n, black);
int best = moves[0];
for (int depth = 1; depth <= MAX_DEPTH; depth++) {
int alpha = -INF, bestScore = -INF, bestThis = moves[0];
boolean complete = true;
for (int i = 0; i < n; i++) {
EscampeBoard.Undo u = pos.makeInt(moves[i]);
int sc = isCapture(u, black) ? WIN - 1 : -negamax(pos, depth - 1, -INF, -alpha, !black, 1);
pos.unmakeInt(u);
if (timedOut) { complete = false; break; }
if (sc > bestScore) { bestScore = sc; bestThis = moves[i]; }
if (sc > alpha) alpha = sc;
}
if (!complete) break; // profondeur interrompue : on garde la précédente
best = bestThis;
reachedDepth = depth;
lastScore = bestScore;
moveToFront(moves, n, best); // ordonne l'itération suivante
if (bestScore >= WIN - 64) break;
}
return best;
}
private int negamax(EscampeBoard pos, int depth, int alpha, int beta, boolean black, int ply) {
if ((++nodes & 2047) == 0 && System.currentTimeMillis() >= deadline) { timedOut = true; return 0; }
if (depth <= 0) return eval(pos, black);
int[] moves = buf[ply];
int n = pos.genMovesIntInto(black, moves);
if (n == 0) return eval(pos, black);
orderCapturesFirst(pos, moves, n, black);
int bestScore = -INF;
for (int i = 0; i < n; i++) {
EscampeBoard.Undo u = pos.makeInt(moves[i]);
int sc = isCapture(u, black) ? WIN - ply : -negamax(pos, depth - 1, -beta, -alpha, !black, ply + 1);
pos.unmakeInt(u);
if (timedOut) return 0;
if (sc > bestScore) bestScore = sc;
if (bestScore > alpha) alpha = bestScore;
if (alpha >= beta) break;
}
return bestScore;
}
private boolean isCapture(EscampeBoard.Undo u, boolean black) {
return u.captured() == (black ? EscampeBoard.WHITE_LICORNE : EscampeBoard.BLACK_LICORNE);
}
/** Place en tête un coup capturant la licorne adverse, pour une coupure immédiate. */
private void orderCapturesFirst(EscampeBoard pos, int[] moves, int n, boolean black) {
int enemy = black ? EscampeBoard.WHITE_LICORNE : EscampeBoard.BLACK_LICORNE;
for (int i = 0; i < n; i++) {
int to = moves[i] % 36;
if (moves[i] != EscampeBoard.MOVE_PASS && pos.board[to / 6][to % 6] == enemy) {
int t = moves[0]; moves[0] = moves[i]; moves[i] = t;
return;
}
}
}
private void moveToFront(int[] moves, int n, int target) {
for (int i = 0; i < n; i++) {
if (moves[i] == target) { int t = moves[0]; moves[0] = moves[i]; moves[i] = t; return; }
}
}
private int eval(EscampeBoard pos, boolean black) {
int adv = evalBlackAdvantage(pos);
return black ? adv : -adv;
}
/** Avantage de Noir : nos paladins proches de la licorne adverse, les leurs loin de la nôtre. */
private int evalBlackAdvantage(EscampeBoard pos) {
int[][] b = pos.board;
int blr = -1, blc = -1, wlr = -1, wlc = -1;
for (int r = 0; r < 6; r++)
for (int c = 0; c < 6; c++) {
int p = b[r][c];
if (p == EscampeBoard.BLACK_LICORNE) { blr = r; blc = c; }
else if (p == EscampeBoard.WHITE_LICORNE) { wlr = r; wlc = c; }
}
if (wlr < 0) return WIN;
if (blr < 0) return -WIN;
int atkSum = 0, defSum = 0, atkMin = 99, defMin = 99;
for (int r = 0; r < 6; r++)
for (int c = 0; c < 6; c++) {
int p = b[r][c];
if (p == EscampeBoard.BLACK_PALADIN) {
int d = Math.abs(r - wlr) + Math.abs(c - wlc);
atkSum += 10 - d;
if (d < atkMin) atkMin = d;
} else if (p == EscampeBoard.WHITE_PALADIN) {
int d = Math.abs(r - blr) + Math.abs(c - blc);
defSum += 10 - d;
if (d < defMin) defMin = d;
}
}
return wAtkSum * atkSum - wDefSum * defSum + wAtkMin * (10 - atkMin) - wDefMin * (10 - defMin);
}
}

45
src/escampe/Partie1.java Normal file
View File

@@ -0,0 +1,45 @@
package escampe;
public interface Partie1 {
/**
* Initialise un plateau à partir d'un fichier texte.
* @param fileName le nom du fichier à lire
*/
public void setFromFile(String fileName);
/**
* Sauve la configuration de l'état courant (plateau et pièces restantes) dans un fichier.
* @param fileName le nom du fichier à sauvegarder
* Le format doit être compatible avec celui utilisé pour la lecture.
*/
public void saveToFile(String fileName);
/**
* Indique si le coup {@code move} est valide pour le joueur {@code player} sur le plateau courant.
* @param move le coup à jouer,
* sous la forme "B1-D1" en général,
* sous la forme "C6/A6/B5/D5/E6/F5" pour le coup qui place les pièces,
* ou "E" pour passer son tour.
* @param player le joueur qui joue, représenté par "noir" ou "blanc"
*/
public boolean isValidMove(String move, String player);
/**
* Calcule les coups possibles pour le joueur {@code player} sur le plateau courant.
* @param player le joueur qui joue, représenté par "noir" ou "blanc"
*/
public String[] possiblesMoves(String player);
/**
* Modifie le plateau en jouant le coup {@code move} pour le joueur {@code player}.
* @param move le coup à jouer, sous la forme "C1-D1" ou "C6/A6/B5/D5/E6/F5"
* @param player le joueur qui joue, représenté par "noir" ou "blanc"
*/
public void play(String move, String player);
/**
* Retourne vrai lorsque le plateau correspond à une fin de partie.
*/
public boolean gameOver();
}

143
src/escampe/RulesTest.java Normal file
View File

@@ -0,0 +1,143 @@
package escampe;
import java.util.*;
/**
* Tests directs des règles du jeu : compte de pas selon le liseré, capture au
* dernier pas uniquement, paladins imprenables, interdiction de traverser une
* case occupée, contrainte de liseré, pass forcé, fin de partie, zones de placement.
*/
public class RulesTest {
static int pass = 0, fail = 0;
static void check(boolean cond, String name) {
if (cond) pass++;
else { fail++; System.out.println(" ÉCHEC : " + name); }
}
static boolean has(Set<String> s, int r, int c) { return s.contains(r + "," + c); }
public static void main(String[] args) {
stepCount();
captureAndBlocking();
lisereConstraint();
forcedPass();
gameOver();
placementZones();
System.out.println("\nRulesTest : " + pass + " OK, " + fail + " échec(s).");
if (fail > 0) System.exit(1);
}
/** Le nombre de pas est exactement le liseré de la case de départ. */
static void stepCount() {
EscampeBoard b = new EscampeBoard();
b.board[2][2] = EscampeBoard.WHITE_PALADIN; // C3, liseré 1
Set<String> r = b.getReachableSquares(2, 2, "blanc");
check(r.size() == 4 && has(r,1,2) && has(r,3,2) && has(r,2,1) && has(r,2,3),
"liseré 1 (centre) → exactement les 4 voisins orthogonaux");
b = new EscampeBoard();
b.board[2][3] = EscampeBoard.WHITE_PALADIN; // D3, liseré 2
r = b.getReachableSquares(2, 3, "blanc");
check(r.size() == 8
&& has(r,0,3) && has(r,4,3) && has(r,2,1) && has(r,2,5)
&& has(r,1,2) && has(r,1,4) && has(r,3,2) && has(r,3,4),
"liseré 2 (centre) → les 8 cases à distance 2");
b = new EscampeBoard();
b.board[3][2] = EscampeBoard.WHITE_PALADIN; // C4, liseré 3
r = b.getReachableSquares(3, 2, "blanc");
check(has(r,0,2), "liseré 3 atteint (0,2) à 3 pas en ligne droite");
check(!has(r,1,2), "liseré 3 n'atteint PAS (1,2) (mauvaise parité : 3 pas)");
check(has(r,2,2) && has(r,3,3), "liseré 3 atteint des cases à distance 1 (zigzag)");
}
/** Capture au dernier pas uniquement ; paladins imprenables ; pas de traversée. */
static void captureAndBlocking() {
EscampeBoard b = new EscampeBoard();
b.board[3][2] = EscampeBoard.WHITE_PALADIN; // C4 liseré 3
b.board[0][2] = EscampeBoard.BLACK_LICORNE; // cible à 3 pas (droit)
Set<String> r = b.getReachableSquares(3, 2, "blanc");
check(has(r,0,2), "capture de la licorne adverse au dernier pas : autorisée");
b = new EscampeBoard();
b.board[3][2] = EscampeBoard.WHITE_PALADIN;
b.board[0][2] = EscampeBoard.BLACK_PALADIN; // paladin sur la case finale
r = b.getReachableSquares(3, 2, "blanc");
check(!has(r,0,2), "paladin imprenable : pas d'arrivée dessus");
b = new EscampeBoard();
b.board[3][2] = EscampeBoard.WHITE_PALADIN;
b.board[1][2] = EscampeBoard.BLACK_PALADIN; // bloque l'unique chemin vers (0,2)
r = b.getReachableSquares(3, 2, "blanc");
check(!has(r,0,2), "interdit de traverser une case occupée");
b = new EscampeBoard();
b.board[3][2] = EscampeBoard.WHITE_PALADIN;
b.board[1][2] = EscampeBoard.BLACK_LICORNE; // licorne à distance 2 (parité ≠)
r = b.getReachableSquares(3, 2, "blanc");
check(!has(r,1,2), "licorne à mauvaise distance : non capturable (compte de pas exact)");
}
/** On ne peut jouer que depuis une case du liseré imposé. */
static void lisereConstraint() {
EscampeBoard b = inPlay();
b.board[2][2] = EscampeBoard.WHITE_LICORNE; // C3 liseré 1
b.board[5][5] = EscampeBoard.BLACK_LICORNE;
b.board[2][3] = EscampeBoard.WHITE_PALADIN; // D3 liseré 2
b.board[0][0] = EscampeBoard.WHITE_PALADIN; // A1 liseré 1
b.lastTileType = 2; // seules les pièces liseré 2 bougent
boolean allLis2 = true;
for (String m : b.possiblesMoves("blanc")) {
int[] from = b.cellFromString(m.substring(0, m.indexOf('-')));
if (EscampeBoard.TILE_MAP[from[0]][from[1]] != 2) allLis2 = false;
}
check(allLis2, "contrainte de liseré : tous les coups partent d'une case liseré 2");
}
/** Pass autorisé seulement si aucune pièce ne peut jouer le liseré imposé. */
static void forcedPass() {
EscampeBoard b = inPlay();
b.board[0][0] = EscampeBoard.WHITE_LICORNE; // A1 liseré 1
b.board[5][5] = EscampeBoard.BLACK_LICORNE;
b.lastTileType = 3; // blanc n'a aucune pièce liseré 3
String[] mv = b.possiblesMoves("blanc");
check(mv.length == 1 && mv[0].equals("E"), "aucune pièce sur le liseré → pass forcé");
check(b.isValidMove("E", "blanc"), "E valide quand bloqué");
b.lastTileType = 1; // la licorne A1 (liseré 1) peut bouger
String[] mv2 = b.possiblesMoves("blanc");
check(mv2.length >= 1 && !mv2[0].equals("E"), "des coups existent → pas de pass");
check(!b.isValidMove("E", "blanc"), "E invalide si des coups existent");
}
static void gameOver() {
EscampeBoard b = inPlay();
b.board[0][0] = EscampeBoard.WHITE_LICORNE;
b.board[5][5] = EscampeBoard.BLACK_LICORNE;
check(!b.gameOver(), "deux licornes présentes → partie en cours");
b.board[5][5] = EscampeBoard.EMPTY;
check(b.gameOver(), "une licorne manquante → fin de partie");
check(!new EscampeBoard().gameOver(), "avant placement → jamais fini");
}
/** Placement : zones autorisées et complémentarité noir/blanc. */
static void placementZones() {
EscampeBoard b = new EscampeBoard();
check(!b.isValidMove("A3/B3/C3/D3/E3/F3", "noir"), "placement noir au centre : refusé");
check(b.isValidMove("A1/A2/B1/E1/F1/C2", "noir"), "placement noir sur 2 lignes du bord : accepté");
b.play("A1/A2/B1/E1/F1/C2", "noir");
check(b.isValidMove("A6/A5/B6/C5/F5/E6", "blanc"), "placement blanc complémentaire (haut) : accepté");
check(!b.isValidMove("A1/A2/B1/E1/F1/D1", "blanc"), "placement blanc du même côté que noir : refusé");
}
/** Plateau vide « en jeu » (les deux placements faits), à remplir à la main. */
static EscampeBoard inPlay() {
EscampeBoard b = new EscampeBoard();
b.blackPlaced = true;
b.whitePlaced = true;
b.currentPlayer = "blanc";
b.lastTileType = -1;
return b;
}
}

183
src/escampe/Solo.java Normal file
View File

@@ -0,0 +1,183 @@
package escampe;
import java.util.Date;
import javax.swing.JFrame;
/**
* Petite Classe toute simple qui vous montre comment on peut lancer une partie sur deux IJoueurs...
* Cela vous servira a debugger facilement votre projet en conditions presque reelles de tournoi
*
* Attention, l'arbitre n'est pas lancé dessus, mais comme il s'agit de deux IJoueur à vous il n'est
* pas nécessaire de vérifier la validité des coups (bien entendu)
*
* Par contre, comme rien ne vérifie la fin de partie (pas d'arbitre), vos IJoueur devront renvoyer
* la chaine "xxxxx" pour dire que la partie est finie.
*
* Cette classe n'affiche rien : elle se contente de donner la main alternativement aux deux
* joueurs.
*
* 2008-2012
*/
public class Solo {
private static IJoueur joueurBlanc;
private static IJoueur joueurNoir;
// Ne pas modifier ces constantes, elles seront utilisees par l'arbitre
private final static int BLANC = -1;
private final static int NOIR = 1;
private static int nbCoups = 0;
/*// Par défaut, on a une applet graphique
static boolean APPLETGRAPHIQUE = true;
// applet game viewer
static private Applet vueDuJeu;
static private JFrame f = null;*/
/**
* Pour éviter de toujours envoyer des lignes de commandes, vous pouvez renvoyer automatiquement
* dans cette méthode votre joueur par défaut. Attention, il faut bien remplir le return new
* VOTREJOUEUR() pour que cela fonctionne la classe implantee renvoyee doit implanter
* l'interface IJoueur...
*
* @param s
* @return Ijoueur un joueur demande
*/
private static IJoueur getDefaultPlayer(String s) {
System.out.println(s + " : defaultPlayer");
// vous devez faire qq chose comme return new MonMeilleurJoueur();
// JoueurAleatoire vit dans escampeobf.jar (interface obfusquée) : on ne peut
// pas le référencer ici à la compilation. On renvoie donc notre propre joueur.
return new JoueurPuyaubreauRussac();
}
/**
* Juste pour rendre le tout plus generique, et vous donner une idee de comment le tournoi sera
* lance automatiquement, voici une methode permettant de charger une certaine classe implantant
* un IJoueur
*
* @param classeJoueur
* @param s
* @return la classe chargee dynamiquement
*/
private static IJoueur loadNamedPlayer(String classeJoueur, String s) {
IJoueur joueur;
System.out.print(s + " : Chargement de la classe joueur " + classeJoueur + "... ");
try {
Class<?> cjoueur = Class.forName(classeJoueur);
joueur = (IJoueur) cjoueur.newInstance();
}
catch (Exception e) {
System.out.println("Erreur de chargement");
System.out.println(e);
return null;
}
System.out.println("Ok");
return joueur;
}
/**
* Boucle principale du jeu, en utilisant une version de l'arbitre identique a celle du tournoi
* L'arbitre sera le garant de la validite des coups, et de leur affichage standard pour la
* publication via le site web.
*
* @param joueurBlanc
* @param joueurNoir
*/
public static void gameLoop(IJoueur joueurBlanc, IJoueur joueurNoir) {
String coup;
boolean partieFinie = false;
IJoueur joueurCourant = joueurNoir; // Dans Escampe le joueur Noir commence
while (!partieFinie) {
nbCoups++;
System.out.println("\n*********\nOn demande à " + joueurCourant.binoName() + " de jouer...");
long waitingTime1 = new Date().getTime();
coup = joueurCourant.choixMouvement();
long waitingTime2 = new Date().getTime();
// On rajoute 1 pour eliminer les temps infinis
long waitingTime = waitingTime2 - waitingTime1 + 1;
System.out.println("Le joueur " + joueurCourant.binoName() + " a joué le coup " + coup + " en " + waitingTime + "s.");
try {
Thread.sleep(1); // Juste pour attendre un peu
}
catch (InterruptedException e) {
}
if (coup.compareTo("xxxxx") == 0)
partieFinie = true;
else if (nbCoups == 2) { // Dans Escampe le joueur Blanc rejoue après avoir posé ses pièces
// On avertit le joueur Noir du placement des pièces
joueurNoir.mouvementEnnemi(coup);
}
else {
if (joueurCourant.getNumJoueur() == BLANC)
joueurCourant = joueurNoir;
else
joueurCourant = joueurBlanc;
// On avertit le second joueur du coup calcule par le precedent
joueurCourant.mouvementEnnemi(coup);
// Ce sera ensuite à lui de jouer de nouveau en haut de la boucle
}
}
System.out.println("Partie finie en " + nbCoups + " coups.\n");
}
/**
* On charge eventuellement les classes demandee pour les joueurs, et on lance la boucle
* principale
*
* @param args
*/
public static void main(String args[]) {
/*// S'il le faut, on initialise l'applet graphique
if (APPLETGRAPHIQUE) {
f = new JFrame("Vue du jeu");
vueDuJeu = new Applet();
vueDuJeu.buildUI(f.getContentPane());
f.setSize(vueDuJeu.getDimension());
vueDuJeu.setMyFrame(f);
f.setVisible(true);
vueDuJeu.addBoard("Départ ", plateau);
vueDuJeu.update(f.getGraphics(), f.getInsets());
}*/
System.out.println("Partie solo ...");
if (args.length == 0) { // On a deux classes à charger
joueurBlanc = getDefaultPlayer("Blanc");
joueurNoir = getDefaultPlayer("Noir");
}
else if (args.length == 2) { // On a deux classes à charger
joueurBlanc = getDefaultPlayer("Blanc");
joueurNoir = getDefaultPlayer("Noir");
}
else if (args.length == 3) {
joueurBlanc = loadNamedPlayer(args[0], "Blanc");
joueurNoir = loadNamedPlayer(args[0], "Noir");
}
else if (args.length == 4) {
joueurBlanc = loadNamedPlayer(args[0], "Blanc");
joueurNoir = loadNamedPlayer(args[1], "Noir");
}
joueurBlanc.initJoueur(BLANC);
System.out.println("Joueur Blanc : " + joueurBlanc.binoName());
joueurNoir.initJoueur(NOIR);
System.out.println("Joueur Noir : " + joueurNoir.binoName());
System.out.println("Initialisation des deux joueurs ok.");
gameLoop(joueurBlanc, joueurNoir);
}
}

121
src/escampe/VerifMoves.java Normal file
View File

@@ -0,0 +1,121 @@
package escampe;
import java.util.*;
/**
* Cross-vérifie le chemin « int » du moteur contre le chemin « String » vérifié,
* sur des milliers de parties aléatoires : mêmes coups que possiblesMoves, makeInt
* équivalent à play, unmakeInt qui restaure l'état. Échoue à la moindre divergence.
*/
public class VerifMoves {
static int mismatches = 0;
public static void main(String[] args) {
int games = args.length > 0 ? Integer.parseInt(args[0]) : 3000;
Random rng = new Random(20260530L);
long positions = 0, makeChecks = 0;
for (int g = 0; g < games; g++) {
EscampeBoard b = new EscampeBoard();
// Placements aléatoires légaux.
int[] noirRows = rng.nextBoolean() ? new int[]{0, 1} : new int[]{4, 5};
b.play(randomPlacement(b, "noir", noirRows, rng), "noir");
int[] blancRows = (noirRows[0] == 0) ? new int[]{4, 5} : new int[]{0, 1};
b.play(randomPlacement(b, "blanc", blancRows, rng), "blanc");
for (int ply = 0; ply < 200 && !b.gameOver(); ply++) {
positions++;
// (1) égalité des ensembles de coups, pour les deux couleurs.
checkMoveSets(b, true);
checkMoveSets(b, false);
// Côté au trait : (2) make==play et (3) unmake, sur chaque coup.
boolean black = "noir".equals(b.currentPlayer);
String side = b.currentPlayer;
int[] moves = b.genMovesInt(black);
for (int m : moves) {
makeChecks++;
EscampeBoard after = b.copy();
EscampeBoard.Undo u = after.makeInt(m);
EscampeBoard ref = b.copy();
ref.play(b.moveToString(m), side);
if (!sameState(after, ref)) {
report(b, "make!=play pour " + b.moveToString(m) + " (" + side + ")");
}
after.unmakeInt(u);
if (!sameState(after, b)) {
report(b, "unmake ne restaure pas pour " + b.moveToString(m));
}
}
if (mismatches > 0) { dumpAndExit(); }
// Avance la partie d'un coup aléatoire (chemin String vérifié).
if (moves.length == 1 && moves[0] == EscampeBoard.MOVE_PASS) {
b.play("E", side);
} else {
int m = moves[rng.nextInt(moves.length)];
b.play(b.moveToString(m), side);
}
}
}
System.out.println("Parties : " + games);
System.out.println("Positions testées : " + positions);
System.out.println("make/unmake testés: " + makeChecks);
System.out.println(mismatches == 0
? "RÉSULTAT : OK — chemin int ≡ chemin String vérifié (0 divergence)."
: "RÉSULTAT : " + mismatches + " DIVERGENCES !");
if (mismatches != 0) System.exit(1);
}
/** Compare genMovesInt(black) et possiblesMoves(player) comme ensembles. */
static void checkMoveSets(EscampeBoard b, boolean black) {
String player = black ? "noir" : "blanc";
Set<String> fromInt = new TreeSet<>();
for (int m : b.genMovesInt(black)) fromInt.add(b.moveToString(m));
Set<String> fromStr = new TreeSet<>(Arrays.asList(b.possiblesMoves(player)));
if (!fromInt.equals(fromStr)) {
report(b, "ensembles différents pour " + player
+ "\n int = " + fromInt + "\n str = " + fromStr);
}
}
static boolean sameState(EscampeBoard a, EscampeBoard c) {
if (a.lastTileType != c.lastTileType) return false;
if (!a.currentPlayer.equals(c.currentPlayer)) return false;
for (int r = 0; r < 6; r++)
for (int col = 0; col < 6; col++)
if (a.board[r][col] != c.board[r][col]) return false;
return true;
}
static void report(EscampeBoard b, String msg) {
if (mismatches < 5) {
System.out.println("DIVERGENCE : " + msg);
System.out.println(" lastTileType=" + b.lastTileType + " currentPlayer=" + b.currentPlayer);
}
mismatches++;
}
static void dumpAndExit() {
System.out.println(">>> arrêt sur première divergence.");
System.exit(1);
}
/** Placement aléatoire légal : 6 cases distinctes sur les 2 lignes, licorne en tête. */
static String randomPlacement(EscampeBoard b, String player, int[] rows, Random rng) {
List<int[]> cells = new ArrayList<>();
for (int r : rows) for (int c = 0; c < 6; c++) cells.add(new int[]{r, c});
for (int tries = 0; tries < 100; tries++) {
Collections.shuffle(cells, rng);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 6; i++) {
if (i > 0) sb.append('/');
sb.append((char) ('A' + cells.get(i)[1])).append((char) ('1' + cells.get(i)[0]));
}
String pl = sb.toString();
if (b.isValidMove(pl, player)) return pl;
}
throw new IllegalStateException("aucun placement légal trouvé");
}
}

12
src/escampe_save.txt Normal file
View File

@@ -0,0 +1,12 @@
% Escampe - sauvegarde du plateau
% lastTileType: 1
% currentPlayer: blanc
% blackPlaced: true
% whitePlaced: true
% blackRows: 4,5
06 Nnn--- 06
05 ----nn 05
04 ------ 04
03 ------ 03
02 b--n-b 02
01 -bb-b- 01

125
tools/make_report_pdf.py Normal file
View File

@@ -0,0 +1,125 @@
#!/usr/bin/env python3
"""Génère le rapport PDF à partir de report/rapport.html, avec PyMuPDF (fitz).
Aucune dépendance externe : ni pandoc, ni LaTeX, ni navigateur. On utilise
l'API fitz.Story (rendu HTML/CSS -> PDF multi-pages) puis une seconde passe
pour le pied de page et les numéros de page.
python tools/make_report_pdf.py
"""
import os
import sys
import fitz # PyMuPDF
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
HTML = os.path.join(ROOT, "report", "rapport.html")
OUT = os.path.join(ROOT, "dist", "Puyaubreau_Russac_rapport.pdf")
# Feuille de style : mise en page A4 sobre, titres colorés, tables et blocs <pre>
# en Courier pour aligner diagrammes et carte des liserés.
CSS = """
* { font-family: serif; }
body { font-size: 10.5pt; line-height: 1.45; color: #1a1a1a; }
h1, h2, h3, .cover-title, .cover-course, .cover-sub, th { font-family: sans-serif; }
h2 { font-size: 15pt; color: #1c3d5a; margin: 18pt 0 6pt 0;
border-bottom: 1.5px solid #1c3d5a; padding-bottom: 2pt; }
h3 { font-size: 12pt; color: #2a6f97; margin: 12pt 0 3pt 0; }
p { margin: 5pt 0; text-align: justify; }
ul, ol { margin: 4pt 0 4pt 0; }
li { margin: 2pt 0; }
code { font-family: monospace; font-size: 9.5pt; color: #8a2846; }
pre.grid { font-family: monospace; font-size: 9pt; line-height: 1.25;
background: #f4f6f8; border: 1px solid #d3dae0; border-radius: 3px;
padding: 7pt; margin: 6pt 0; color: #14213d; white-space: pre; }
table { border-collapse: collapse; width: 100%; margin: 7pt 0; font-size: 9.5pt; }
th { background: #1c3d5a; color: #ffffff; text-align: left; padding: 4pt 6pt; }
td { border: 1px solid #c5ccd3; padding: 4pt 6pt; vertical-align: top; }
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; }
/* É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; }
/* Page de titre */
.cover { text-align: center; padding-top: 40pt; }
.cover-univ { font-size: 10pt; color: #555; margin-bottom: 30pt; }
.cover-course { font-size: 13pt; color: #2a6f97; letter-spacing: 1pt; }
.cover-title { font-size: 30pt; color: #1c3d5a; margin: 8pt 0 0 0; }
.cover-sub { font-size: 12pt; color: #333; margin: 4pt 0; }
.cover-authors{ font-size: 15pt; color: #14213d; margin-top: 34pt; font-family: sans-serif; }
.cover-date { font-size: 11pt; color: #555; margin-top: 6pt; }
.cover-meta { font-size: 9.5pt; color: #555; margin-top: 34pt; line-height: 1.6; }
"""
MARGIN = 48 # marge en points (1pt = 1/72")
FOOTER = "Escampe — Puyaubreau / Russac — version finale"
def build():
with open(HTML, "r", encoding="utf-8") as f:
html = f.read()
os.makedirs(os.path.dirname(OUT), exist_ok=True)
mediabox = fitz.paper_rect("a4")
where = mediabox + (MARGIN, MARGIN, -MARGIN, -MARGIN)
# 1) Rendu du flux HTML en pages PDF via Story.
writer = fitz.DocumentWriter(OUT)
story = fitz.Story(html=html, user_css=CSS)
more = 1
pages = 0
while more:
dev = writer.begin_page(mediabox)
more, _ = story.place(where)
story.draw(dev)
writer.end_page()
pages += 1
writer.close()
# 2) Seconde passe : pied de page + numéros « page X / N ».
doc = fitz.open(OUT)
n = doc.page_count
for i, page in enumerate(doc, start=1):
y = page.rect.height - 26
page.insert_text((MARGIN, y), FOOTER, fontname="helv",
fontsize=7.5, color=(0.45, 0.45, 0.45))
label = f"page {i} / {n}"
w = fitz.get_text_length(label, fontname="helv", fontsize=7.5)
page.insert_text((page.rect.width - MARGIN - w, y), label,
fontname="helv", fontsize=7.5, color=(0.45, 0.45, 0.45))
doc.saveIncr()
doc.close()
return pages, n
def verify():
"""Contrôle que les accents survivent au rendu (round-trip texte)."""
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"]
missing = [s for s in probes if s not in full]
return missing, len(full)
if __name__ == "__main__":
pages, n = build()
missing, chars = verify()
print(f"PDF écrit : {OUT}")
print(f"Pages : {n} | texte extrait : {chars} caractères")
if missing:
print("ATTENTION, chaînes accentuées introuvables après rendu :", missing)
sys.exit(1)
print("Accents vérifiés (round-trip OK).")