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:
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user