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:
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal 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
93
MULTIJOUEUR.md
Normal 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
BIN
Puyaubreau_Russac.pdf
Normal file
Binary file not shown.
BIN
Puyaubreau_Russac.tgz
Normal file
BIN
Puyaubreau_Russac.tgz
Normal file
Binary file not shown.
331
RAPPORT.md
Normal file
331
RAPPORT.md
Normal 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 (Q1–Q7)
|
||||||
|
|
||||||
|
### 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 ~400–600
|
||||||
|
demi-coups. Pour tenir les 300 s/joueur : approfondissement itératif, alpha-bêta,
|
||||||
|
budget par coup dérivé du temps restant (§8).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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** : ~4–5 M nœuds/s ; profondeur 12–15 demi-coups en 6 s
|
||||||
|
(plus dans les positions étroites). Les gains forcés annoncés se concrétisent par
|
||||||
|
une capture.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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·Σ(10−d_attaque) − 2·Σ(10−d_défense)
|
||||||
|
+ 8·(10−min d_attaque) − 8·(10−min d_défense)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Heuristiques testées et choix** (réglage par auto-jeu déterministe + matchs vs
|
||||||
|
aléatoire) : (a) somme seule → jeu trop diffus ; (b) **somme + minimum (retenue)**
|
||||||
|
→ le terme minimum fortement pondéré oriente les paladins vers la licorne adverse
|
||||||
|
et améliore le taux de capture ; (c) terme défensif symétrique conservé (évite
|
||||||
|
d'exposer notre licorne). Le fort poids du minimum reflète que c'est l'attaquant
|
||||||
|
le plus avancé qui décide d'une prise.
|
||||||
|
|
||||||
|
> 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 | ~4–5 M nœuds/s ; prof. 12–15 ; branchement max 49 / moyen ≈ 8,9 |
|
||||||
|
|
||||||
|
Séparation des rôles : `VerifMoves` (moteur ≡ `EscampeBoard`), `RulesTest`
|
||||||
|
(`EscampeBoard` ≡ règles), parties arbitrées (dialogue correct avec l'arbitre
|
||||||
|
réel). Aucun coup illégal sur l'ensemble des parties jouées.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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
58
README.md
Normal 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
58
build.sh
Normal 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
BIN
dist/Puyaubreau_Russac.jar
vendored
Normal file
Binary file not shown.
BIN
dist/Puyaubreau_Russac.tgz
vendored
Normal file
BIN
dist/Puyaubreau_Russac.tgz
vendored
Normal file
Binary file not shown.
BIN
dist/Puyaubreau_Russac/Puyaubreau_Russac.jar
vendored
Normal file
BIN
dist/Puyaubreau_Russac/Puyaubreau_Russac.jar
vendored
Normal file
Binary file not shown.
3
dist/Puyaubreau_Russac/mainClass
vendored
Normal file
3
dist/Puyaubreau_Russac/mainClass
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
jar:Puyaubreau_Russac.jar
|
||||||
|
clientClass:escampe.ClientJeu
|
||||||
|
mainClass:escampe.JoueurPuyaubreauRussac
|
||||||
298
dist/Puyaubreau_Russac/src/escampe/Applet.java
vendored
Normal file
298
dist/Puyaubreau_Russac/src/escampe/Applet.java
vendored
Normal 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
dist/Puyaubreau_Russac/src/escampe/Bench.java
vendored
Normal file
30
dist/Puyaubreau_Russac/src/escampe/Bench.java
vendored
Normal 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
58
dist/Puyaubreau_Russac/src/escampe/Branching.java
vendored
Normal file
58
dist/Puyaubreau_Russac/src/escampe/Branching.java
vendored
Normal 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
dist/Puyaubreau_Russac/src/escampe/ClientJeu.java
vendored
Normal file
151
dist/Puyaubreau_Russac/src/escampe/ClientJeu.java
vendored
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
862
dist/Puyaubreau_Russac/src/escampe/EscampeBoard.java
vendored
Normal file
862
dist/Puyaubreau_Russac/src/escampe/EscampeBoard.java
vendored
Normal 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
dist/Puyaubreau_Russac/src/escampe/IJoueur.java
vendored
Normal file
65
dist/Puyaubreau_Russac/src/escampe/IJoueur.java
vendored
Normal 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();
|
||||||
|
|
||||||
|
}
|
||||||
117
dist/Puyaubreau_Russac/src/escampe/JoueurPuyaubreauRussac.java
vendored
Normal file
117
dist/Puyaubreau_Russac/src/escampe/JoueurPuyaubreauRussac.java
vendored
Normal 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
dist/Puyaubreau_Russac/src/escampe/Moteur.java
vendored
Normal file
137
dist/Puyaubreau_Russac/src/escampe/Moteur.java
vendored
Normal 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
dist/Puyaubreau_Russac/src/escampe/Partie1.java
vendored
Normal file
45
dist/Puyaubreau_Russac/src/escampe/Partie1.java
vendored
Normal 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
dist/Puyaubreau_Russac/src/escampe/RulesTest.java
vendored
Normal file
143
dist/Puyaubreau_Russac/src/escampe/RulesTest.java
vendored
Normal 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
dist/Puyaubreau_Russac/src/escampe/Solo.java
vendored
Normal file
183
dist/Puyaubreau_Russac/src/escampe/Solo.java
vendored
Normal 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
dist/Puyaubreau_Russac/src/escampe/VerifMoves.java
vendored
Normal file
121
dist/Puyaubreau_Russac/src/escampe/VerifMoves.java
vendored
Normal 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
BIN
dist/Puyaubreau_Russac_rapport.pdf
vendored
Normal file
Binary file not shown.
3
dist/mainClass
vendored
Normal file
3
dist/mainClass
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
jar:Puyaubreau_Russac.jar
|
||||||
|
clientClass:escampe.ClientJeu
|
||||||
|
mainClass:escampe.JoueurPuyaubreauRussac
|
||||||
12
escampe_save.txt
Normal file
12
escampe_save.txt
Normal 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
29
jouer-vs-IA.bat
Normal 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
26
jouer-vs-pote.bat
Normal 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
BIN
lib/escampeobf.jar
Normal file
Binary file not shown.
BIN
main-polytech.pdf
Normal file
BIN
main-polytech.pdf
Normal file
Binary file not shown.
9
partie1.md
Normal file
9
partie1.md
Normal 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
447
report/rapport.html
Normal 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 Puyaubreau & Antonin Russac
|
||||||
|
</div>
|
||||||
|
<div class="cover-date">30 mai 2026</div>
|
||||||
|
<div class="cover-meta">
|
||||||
|
Joueur : <code>escampe.JoueurPuyaubreauRussac</code><br>
|
||||||
|
Encadrement : Yue 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 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 1
|
||||||
|
et 2) puis la conception du joueur artificiel pour le tournoi (partie 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 0</code> = ligne 1 (bas) et <code>colonne 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 4 de l'énoncé (ligne 1 en bas) :</p>
|
||||||
|
<pre class="grid"> A B C D E F
|
||||||
|
6 3 2 2 1 3 2
|
||||||
|
5 1 3 1 3 1 2
|
||||||
|
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 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 pièces × jusqu'à ~20 destinations
|
||||||
|
sur liseré triple). La mesure réelle est bien plus basse, car la contrainte de
|
||||||
|
liseré ne laisse jouables que les pièces du bon liseré. Sur 30 000 parties
|
||||||
|
aléatoires simulées (utilitaire <code>escampe.Branching</code>) :</p>
|
||||||
|
<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 < 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 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 400–600 demi-coups. Pour
|
||||||
|
tenir la contrainte de temps (300 s par joueur et par partie), nous combinons
|
||||||
|
<strong>approfondissement itératif</strong>, <strong>élagage alpha-bêta</strong> et
|
||||||
|
un <strong>budget par coup</strong> dérivé du temps restant (§8).</p>
|
||||||
|
|
||||||
|
<!-- ====================== 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 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 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 = 1</code>, <code>BLANC = -1</code>) ;
|
||||||
|
<code>EscampeBoard</code> en chaînes <code>"noir"</code>/<code>"blanc"</code>.</li>
|
||||||
|
<li><strong>Pass = <code>"E"</code>, et non <code>"PASSE"</code>.</strong> Le
|
||||||
|
Javadoc d'<code>IJoueur</code> indique <code>"PASSE"</code>, mais la classe de
|
||||||
|
jeu du serveur teste <code>move.equals("E")</code> (et <code>"PASSE"</code>
|
||||||
|
n'apparaît nulle part dans le jar). Envoyer <code>"PASSE"</code> aurait valu une
|
||||||
|
défaite sur coup illégal.</li>
|
||||||
|
<li><strong>Carte des liserés</strong> identique à celle du serveur (cf. Q1).</li>
|
||||||
|
</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 - 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 millions de
|
||||||
|
nœuds par seconde</strong>. En milieu de partie, l'approfondissement itératif
|
||||||
|
atteint une profondeur de <strong>12 à 15 demi-coups</strong> en 6 s (davantage
|
||||||
|
dans les positions étroites). Les annonces de gain forcé du moteur se matérialisent
|
||||||
|
bien par une capture effective lors des parties de contrôle.</p>
|
||||||
|
|
||||||
|
<!-- ====================== 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 = 2, minimum = 8) :</p>
|
||||||
|
<pre class="grid">eval = 2·Σ(10−d_attaque) − 2·Σ(10−d_défense)
|
||||||
|
+ 8·(10−min d_attaque) − 8·(10−min d_défense)</pre>
|
||||||
|
|
||||||
|
<p><strong>Heuristiques testées et choix.</strong> Le réglage s'est fait par
|
||||||
|
auto-jeu déterministe et matchs arbitrés contre le joueur aléatoire fourni. Nous
|
||||||
|
avons comparé : (a) <em>somme seule</em> — jeu trop diffus, le moteur tarde à
|
||||||
|
concentrer une menace ; (b) <em>somme + minimum</em> (retenue) — le terme minimum,
|
||||||
|
fortement pondéré, oriente nettement les paladins vers la licorne adverse et
|
||||||
|
améliore le taux de capture ; (c) ajout d'un terme défensif symétrique — conservé,
|
||||||
|
il évite d'exposer notre licorne sans nuire à l'attaque. Le fort poids du terme
|
||||||
|
minimum reflète que, dans ce jeu, c'est l'attaquant <em>le plus avancé</em> qui
|
||||||
|
décide d'une prise.</p>
|
||||||
|
|
||||||
|
<p 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 s par joueur et par partie. Nous nous
|
||||||
|
fixons une <strong>enveloppe interne de 280 s</strong> (≈ 20 s de marge).
|
||||||
|
Le budget alloué à un coup est une fraction du temps restant, bornée :</p>
|
||||||
|
<pre class="grid">tranche = clamp( temps_restant / 12 , 120 ms , 6000 ms )</pre>
|
||||||
|
<p>La division par le temps restant décroît géométriquement : le budget ne peut
|
||||||
|
<strong>jamais</strong> être épuisé, même sur une partie très longue. Le plafond de
|
||||||
|
6 s évite de surinvestir en ouverture ; un plancher de 120 ms garantit un
|
||||||
|
minimum de réflexion ; un mode « panique » sécurise les toutes dernières secondes.
|
||||||
|
L'approfondissement itératif rend le meilleur coup déjà trouvé dès que la tranche
|
||||||
|
expire (le temps est contrôlé toutes les 2048 explorations de nœuds).</p>
|
||||||
|
|
||||||
|
<p><strong>Mesures</strong> (auto-jeu équilibré, plein budget) : temps
|
||||||
|
<strong>maximal par coup ≈ 6,0 s</strong> (le plafond), <strong>cumul maximal
|
||||||
|
≈ 36 s</strong> par joueur sur une partie complète — très loin des 300 s.
|
||||||
|
Le réglage est volontairement conservateur et pourrait être augmenté sans risque.</p>
|
||||||
|
|
||||||
|
<!-- ====================== 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>≈ 4–5 M nœuds/s ; profondeur 12–15 ; branchement max 49 / moyen ≈ 8,9</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p>La séparation des rôles est délibérée : <code>VerifMoves</code> prouve que le
|
||||||
|
moteur ≡ <code>EscampeBoard</code> ; <code>RulesTest</code> prouve que
|
||||||
|
<code>EscampeBoard</code> respecte les règles ; les parties arbitrées prouvent que
|
||||||
|
le tout dialogue correctement avec l'arbitre réel. Aucun coup illégal n'a été
|
||||||
|
produit sur l'ensemble des parties jouées.</p>
|
||||||
|
|
||||||
|
<!-- ====================== 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 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 & Moore, <em>An Analysis of Alpha-Beta
|
||||||
|
Pruning</em>, 1975) ; minimax, negamax et approfondissement itératif
|
||||||
|
(Russell & Norvig, <em>Artificial Intelligence: A Modern Approach</em>) ;
|
||||||
|
techniques de représentation par masques de bits et d'ordonnancement de coups
|
||||||
|
(<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>
|
||||||
41
scripts/bench_vs_random.sh
Normal file
41
scripts/bench_vs_random.sh
Normal 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
51
scripts/match.sh
Normal 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
743
src/EscampeBoard.java
Normal 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
43
src/Partie1.java
Normal 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
298
src/escampe/Applet.java
Normal 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
30
src/escampe/Bench.java
Normal 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/escampe/Branching.java
Normal file
58
src/escampe/Branching.java
Normal 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
151
src/escampe/ClientJeu.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
862
src/escampe/EscampeBoard.java
Normal file
862
src/escampe/EscampeBoard.java
Normal 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
65
src/escampe/IJoueur.java
Normal 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();
|
||||||
|
|
||||||
|
}
|
||||||
117
src/escampe/JoueurPuyaubreauRussac.java
Normal file
117
src/escampe/JoueurPuyaubreauRussac.java
Normal 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
137
src/escampe/Moteur.java
Normal 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
45
src/escampe/Partie1.java
Normal 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
143
src/escampe/RulesTest.java
Normal 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
183
src/escampe/Solo.java
Normal 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
121
src/escampe/VerifMoves.java
Normal 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
12
src/escampe_save.txt
Normal 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
125
tools/make_report_pdf.py
Normal 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).")
|
||||||
Reference in New Issue
Block a user