diff --git a/build/fr/iut_fbleau/GameAPI/AbstractBoard.class b/build/fr/iut_fbleau/GameAPI/AbstractBoard.class new file mode 100644 index 0000000..7c1f157 Binary files /dev/null and b/build/fr/iut_fbleau/GameAPI/AbstractBoard.class differ diff --git a/build/fr/iut_fbleau/GameAPI/AbstractGame.class b/build/fr/iut_fbleau/GameAPI/AbstractGame.class new file mode 100644 index 0000000..b2e2856 Binary files /dev/null and b/build/fr/iut_fbleau/GameAPI/AbstractGame.class differ diff --git a/build/fr/iut_fbleau/GameAPI/AbstractGamePlayer.class b/build/fr/iut_fbleau/GameAPI/AbstractGamePlayer.class new file mode 100644 index 0000000..deaa1ac Binary files /dev/null and b/build/fr/iut_fbleau/GameAPI/AbstractGamePlayer.class differ diff --git a/build/fr/iut_fbleau/GameAPI/AbstractPly.class b/build/fr/iut_fbleau/GameAPI/AbstractPly.class new file mode 100644 index 0000000..79018e6 Binary files /dev/null and b/build/fr/iut_fbleau/GameAPI/AbstractPly.class differ diff --git a/build/fr/iut_fbleau/GameAPI/IBoard.class b/build/fr/iut_fbleau/GameAPI/IBoard.class new file mode 100644 index 0000000..35a6e98 Binary files /dev/null and b/build/fr/iut_fbleau/GameAPI/IBoard.class differ diff --git a/build/fr/iut_fbleau/GameAPI/Player.class b/build/fr/iut_fbleau/GameAPI/Player.class new file mode 100644 index 0000000..6be1739 Binary files /dev/null and b/build/fr/iut_fbleau/GameAPI/Player.class differ diff --git a/build/fr/iut_fbleau/GameAPI/Result.class b/build/fr/iut_fbleau/GameAPI/Result.class new file mode 100644 index 0000000..73c27d2 Binary files /dev/null and b/build/fr/iut_fbleau/GameAPI/Result.class differ diff --git a/build/fr/iut_fbleau/HexGame/HexBoard.class b/build/fr/iut_fbleau/HexGame/HexBoard.class new file mode 100644 index 0000000..8c58a16 Binary files /dev/null and b/build/fr/iut_fbleau/HexGame/HexBoard.class differ diff --git a/build/fr/iut_fbleau/HexGame/HexMain$1.class b/build/fr/iut_fbleau/HexGame/HexMain$1.class new file mode 100644 index 0000000..f572f6f Binary files /dev/null and b/build/fr/iut_fbleau/HexGame/HexMain$1.class differ diff --git a/build/fr/iut_fbleau/HexGame/HexMain.class b/build/fr/iut_fbleau/HexGame/HexMain.class new file mode 100644 index 0000000..10e072e Binary files /dev/null and b/build/fr/iut_fbleau/HexGame/HexMain.class differ diff --git a/build/fr/iut_fbleau/HexGame/HexPly.class b/build/fr/iut_fbleau/HexGame/HexPly.class new file mode 100644 index 0000000..51bf46d Binary files /dev/null and b/build/fr/iut_fbleau/HexGame/HexPly.class differ diff --git a/build/fr/iut_fbleau/HexGame/HumanConsolePlayer.class b/build/fr/iut_fbleau/HexGame/HumanConsolePlayer.class new file mode 100644 index 0000000..ab4c1f6 Binary files /dev/null and b/build/fr/iut_fbleau/HexGame/HumanConsolePlayer.class differ diff --git a/javaAPI/fr/iut_fbleau/HexGame/HexBoard.java b/javaAPI/fr/iut_fbleau/HexGame/HexBoard.java index 9dfd892..3d5c7ca 100644 --- a/javaAPI/fr/iut_fbleau/HexGame/HexBoard.java +++ b/javaAPI/fr/iut_fbleau/HexGame/HexBoard.java @@ -1,63 +1,154 @@ package fr.iut_fbleau.HexGame; import fr.iut_fbleau.GameAPI.*; -import java.util.*; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.Iterator; +import java.util.List; /** - * Plateau du jeu de Hex. + * Représente le plateau du jeu de Hex. * - * Joueur 1 relie la gauche et la droite. - * Joueur 2 relie le haut et le bas. + *

Rappel des conditions de victoire

+ * + * + *

Idée de l'algorithme de détection de victoire

+ * On modélise le plateau comme un graphe : + * + * + * Pour tester la victoire d'un joueur, on lance un parcours (DFS/BFS) : + *
    + *
  1. On part de toutes les cases du bord de départ qui contiennent un pion du joueur.
  2. + *
  3. On explore tous les pions du joueur connectés à ces cases.
  4. + *
  5. Si on atteint le bord opposé, il existe un chemin : le joueur gagne.
  6. + *
+ * + * Complexité : O(N²) au pire (on visite chaque case au plus une fois). */ public class HexBoard extends AbstractBoard { + /** Taille du plateau : size x size. */ private final int size; - private Player[][] cells; - private Deque historyLocal; + /** + * Grille des cases. + * Une case vaut : + * + */ + private final Player[][] cells; + + /** + * Offsets des 6 voisins d'une case dans une grille hexagonale. + * + * Pour une case (r,c), les voisins potentiels sont : + * (r-1,c), (r+1,c), (r,c-1), (r,c+1), (r-1,c+1), (r+1,c-1). + */ private static final int[][] NEIGHBORS = { - {-1, 0}, {+1, 0}, - { 0, -1}, { 0, +1}, - {-1, +1}, {+1, -1} + {-1, 0}, {+1, 0}, + { 0, -1}, { 0, +1}, + {-1, +1}, {+1, -1} }; + /** Crée un plateau vide avec {@link Player#PLAYER1} qui commence. */ public HexBoard(int size) { - super(); - this.size = size; - this.cells = new Player[size][size]; - this.historyLocal = new ArrayDeque<>(); - this.currentPlayer = Player.PLAYER1; + this(size, Player.PLAYER1); } + /** + * Constructeur interne, utile pour {@link #safeCopy()}. + * @param size taille du plateau + * @param current joueur courant + */ + private HexBoard(int size, Player current) { + super(current, new ArrayDeque<>()); + if (size <= 0) throw new IllegalArgumentException("size must be > 0"); + this.size = size; + this.cells = new Player[size][size]; + } + + /** @return la taille du plateau. */ + public int getSize() { + return size; + } + + /** + * Vérifie si (r,c) est dans le plateau. + * @param r ligne (0..size-1) + * @param c colonne (0..size-1) + */ private boolean inBounds(int r, int c) { return r >= 0 && r < size && c >= 0 && c < size; } + /** @return le contenu d'une case (null si vide). */ private Player getCell(int r, int c) { return cells[r][c]; } + /** Modifie une case (utilisé par doPly/undoPly). */ private void setCell(int r, int c, Player p) { cells[r][c] = p; } + /** + * Teste la victoire de PLAYER1 (gauche -> droite). + * + *

Détails de l'algorithme

+ *
    + *
  1. On initialise une structure "visited" pour ne pas revisiter les cases.
  2. + *
  3. On met dans une pile toutes les cases du bord gauche (colonne 0) + * qui contiennent un pion PLAYER1.
  4. + *
  5. On effectue un DFS : + *
      + *
    • on dépile une case
    • + *
    • si elle est sur la colonne size-1 : on a touché le bord droit -> victoire
    • + *
    • sinon, on empile tous ses voisins qui sont des pions PLAYER1 et pas encore visités
    • + *
    + *
  6. + *
+ * + * @return true si PLAYER1 a un chemin gauche->droite, false sinon + */ private boolean hasPlayer1Won() { boolean[][] visited = new boolean[size][size]; Deque stack = new ArrayDeque<>(); + + // 1) points de départ : bord gauche for (int r = 0; r < size; r++) { if (getCell(r, 0) == Player.PLAYER1) { visited[r][0] = true; stack.push(new int[]{r, 0}); } } + + // 2) DFS while (!stack.isEmpty()) { int[] cur = stack.pop(); - int cr = cur[0]; - int cc = cur[1]; + int cr = cur[0], cc = cur[1]; + + // condition d'arrivée : bord droit if (cc == size - 1) return true; + + // explore les 6 voisins for (int[] d : NEIGHBORS) { int nr = cr + d[0], nc = cc + d[1]; - if (inBounds(nr, nc) && !visited[nr][nc] && getCell(nr, nc) == Player.PLAYER1) { + if (inBounds(nr, nc) + && !visited[nr][nc] + && getCell(nr, nc) == Player.PLAYER1) { visited[nr][nc] = true; stack.push(new int[]{nr, nc}); } @@ -66,23 +157,42 @@ public class HexBoard extends AbstractBoard { return false; } + /** + * Teste la victoire de PLAYER2 (haut -> bas). + * + * Même principe que {@link #hasPlayer1Won()} mais : + * + * + * @return true si PLAYER2 a un chemin haut->bas, false sinon + */ private boolean hasPlayer2Won() { boolean[][] visited = new boolean[size][size]; Deque stack = new ArrayDeque<>(); + + // points de départ : bord haut for (int c = 0; c < size; c++) { if (getCell(0, c) == Player.PLAYER2) { visited[0][c] = true; stack.push(new int[]{0, c}); } } + + // DFS while (!stack.isEmpty()) { int[] cur = stack.pop(); - int cr = cur[0]; - int cc = cur[1]; + int cr = cur[0], cc = cur[1]; + + // condition d'arrivée : bord bas if (cr == size - 1) return true; + for (int[] d : NEIGHBORS) { int nr = cr + d[0], nc = cc + d[1]; - if (inBounds(nr, nc) && !visited[nr][nc] && getCell(nr, nc) == Player.PLAYER2) { + if (inBounds(nr, nc) + && !visited[nr][nc] + && getCell(nr, nc) == Player.PLAYER2) { visited[nr][nc] = true; stack.push(new int[]{nr, nc}); } @@ -98,18 +208,50 @@ public class HexBoard extends AbstractBoard { int r = hp.getRow(), c = hp.getCol(); return inBounds(r, c) && getCell(r, c) == null - && hp.getPlayer() == this.getCurrentPlayer(); + && hp.getPlayer() == getCurrentPlayer(); + } + + /** + * Teste si un coup est immédiatement gagnant. + * + * On joue le coup, on teste la victoire, puis on annule le coup. + * Cela permet d'évaluer un coup sans modifier définitivement l'état du plateau. + * + * @param move coup à tester + * @return true si après ce coup le joueur a gagné, false sinon + */ + public boolean isWinningMove(AbstractPly move) { + if (!isLegal(move)) return false; + Player p = move.getPlayer(); + + doPly(move); + boolean winNow = (p == Player.PLAYER1) ? hasPlayer1Won() : hasPlayer2Won(); + undoPly(); + + return winNow; } @Override public void doPly(AbstractPly move) { - if (!(move instanceof HexPly)) + if (!(move instanceof HexPly)) { throw new IllegalArgumentException("Coup invalide: " + move); + } + if (!isLegal(move)) { + throw new IllegalStateException("Coup illégal: " + move); + } + HexPly hp = (HexPly) move; - if (!isLegal(hp)) - throw new IllegalStateException("Coup illégal: " + hp); setCell(hp.getRow(), hp.getCol(), hp.getPlayer()); - historyLocal.push(hp); + addPlyToHistory(move); + setNextPlayer(); + } + + @Override + public void undoPly() { + AbstractPly last = removePlyFromHistory(); + HexPly hp = (HexPly) last; + + setCell(hp.getRow(), hp.getCol(), null); setNextPlayer(); } @@ -120,46 +262,32 @@ public class HexBoard extends AbstractBoard { @Override public Result getResult() { - if (hasPlayer1Won()) return Result.WIN; - if (hasPlayer2Won()) return Result.LOSS; - return Result.DRAW; + if (!isGameOver()) return null; + if (hasPlayer1Won()) return Result.WIN; // du point de vue PLAYER1 + return Result.LOSS; } @Override - public Iterator getPlies() { - Player me = this.getCurrentPlayer(); + public Iterator iterator() { + Player me = getCurrentPlayer(); List moves = new ArrayList<>(); + for (int r = 0; r < size; r++) { for (int c = 0; c < size; c++) { - if (getCell(r, c) == null) moves.add(new HexPly(me, r, c)); + if (getCell(r, c) == null) { + moves.add(new HexPly(me, r, c)); + } } } return moves.iterator(); } - @Override - public Iterator getHistory() { - return historyLocal.iterator(); - } - - @Override - public void undoLastPly() { - if (historyLocal.isEmpty()) return; - HexPly last = (HexPly) historyLocal.pop(); - setCell(last.getRow(), last.getCol(), null); - this.currentPlayer = last.getPlayer(); - } - @Override public IBoard safeCopy() { - HexBoard copy = new HexBoard(this.size); - copy.currentPlayer = this.currentPlayer; + HexBoard copy = new HexBoard(this.size, this.getCurrentPlayer()); for (int r = 0; r < size; r++) { - for (int c = 0; c < size; c++) { - copy.cells[r][c] = this.cells[r][c]; - } + System.arraycopy(this.cells[r], 0, copy.cells[r], 0, size); } - copy.historyLocal = new ArrayDeque<>(this.historyLocal); return copy; } @@ -180,8 +308,4 @@ public class HexBoard extends AbstractBoard { sb.append("Current player: ").append(getCurrentPlayer()).append("\n"); return sb.toString(); } - - public int getSize() { - return size; - } } diff --git a/javaAPI/fr/iut_fbleau/HexGame/HexMain.java b/javaAPI/fr/iut_fbleau/HexGame/HexMain.java new file mode 100644 index 0000000..e9d3885 --- /dev/null +++ b/javaAPI/fr/iut_fbleau/HexGame/HexMain.java @@ -0,0 +1,34 @@ +package fr.iut_fbleau.HexGame; + +import fr.iut_fbleau.GameAPI.*; + +import java.util.EnumMap; +import java.util.Scanner; + +/** + * Lancement d'une partie de Hex en console. + */ +public class HexMain { + + public static void main(String[] args) { + int size = 7; + if (args.length >= 1) { + try { size = Integer.parseInt(args[0]); } catch (NumberFormatException ignored) {} + } + + HexBoard board = new HexBoard(size); + + Scanner sc = new Scanner(System.in); + EnumMap players = new EnumMap<>(Player.class); + players.put(Player.PLAYER1, new HumanConsolePlayer(Player.PLAYER1, sc)); + players.put(Player.PLAYER2, new HumanConsolePlayer(Player.PLAYER2, sc)); + + AbstractGame game = new AbstractGame(board, players) {}; + Result res = game.run(); + + System.out.println(board); + System.out.println("Résultat (du point de vue de PLAYER1) : " + res); + + sc.close(); + } +} diff --git a/javaAPI/fr/iut_fbleau/HexGame/HumanConsolePlayer.java b/javaAPI/fr/iut_fbleau/HexGame/HumanConsolePlayer.java new file mode 100644 index 0000000..63068fc --- /dev/null +++ b/javaAPI/fr/iut_fbleau/HexGame/HumanConsolePlayer.java @@ -0,0 +1,68 @@ +package fr.iut_fbleau.HexGame; + +import fr.iut_fbleau.GameAPI.*; + +import java.util.Scanner; + +/** + * Joueur humain en console. + * + * Format attendu : "row col" (indices à partir de 0). + */ +public class HumanConsolePlayer extends AbstractGamePlayer { + + private final Scanner in; + + public HumanConsolePlayer(Player me, Scanner in) { + super(me); + this.in = in; + } + + @Override + public AbstractPly giveYourMove(IBoard board) { + if (!(board instanceof HexBoard)) { + throw new IllegalArgumentException("Ce joueur attend un HexBoard."); + } + HexBoard hb = (HexBoard) board; + + while (true) { + System.out.println(hb); + System.out.print("Joueur " + board.getCurrentPlayer() + " - entrez un coup (row col) : "); + + String line = in.nextLine().trim(); + if (line.equalsIgnoreCase("quit") || line.equalsIgnoreCase("exit")) { + throw new IllegalStateException("Partie interrompue par l'utilisateur."); + } + if (line.equalsIgnoreCase("help")) { + System.out.println("Entrez deux entiers : row col (0 <= row,col < " + hb.getSize() + ")"); + System.out.println("Commandes: help, quit"); + continue; + } + + String[] parts = line.split("\\s+"); + if (parts.length != 2) { + System.out.println("Format invalide. Exemple: 3 4"); + continue; + } + + try { + int r = Integer.parseInt(parts[0]); + int c = Integer.parseInt(parts[1]); + HexPly ply = new HexPly(board.getCurrentPlayer(), r, c); + + if (!hb.isLegal(ply)) { + System.out.println("Coup illégal (case occupée / hors plateau / mauvais joueur). Réessayez."); + continue; + } + + if (hb.isWinningMove(ply)) { + System.out.println("Coup gagnant !"); + } + + return ply; + } catch (NumberFormatException e) { + System.out.println("Veuillez entrer deux entiers."); + } + } + } +}