package escampe; import java.io.*; import java.util.*; /** * Représentation d'un état du jeu Escampe. * *

Le plateau est un tableau {@code int[6][6]} : *

* *

Chaque case stocke l'une des constantes pièce : * {@code EMPTY}, {@code WHITE_LICORNE}, {@code WHITE_PALADIN}, * {@code BLACK_LICORNE}, {@code BLACK_PALADIN}. * *

L'état complémentaire mémorisé : *

* *

Règles de déplacement : *

*/ 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 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 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 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 getReachableSquares(int fromRow, int fromCol, String player) { Set 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. * *

À 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). * *

Règles : *

    *
  • Pas intermédiaires (stepsLeft > 1) : la case suivante doit être vide.
  • *
  • Dernier pas (stepsLeft == 1) : la case peut être vide ou contenir * la licorne adverse (capture).
  • *
*/ private void dfs(int row, int col, int stepsLeft, String player, boolean[][] visited, Set 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 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; } }