Joueur IA Escampe (Puyaubreau/Russac) — version finale

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

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

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

Binary file not shown.

3
dist/Puyaubreau_Russac/mainClass vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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