318 lines
9.7 KiB
Java
318 lines
9.7 KiB
Java
package fr.iut_fbleau.HexGame;
|
|
|
|
import fr.iut_fbleau.GameAPI.*;
|
|
|
|
import java.util.ArrayDeque;
|
|
import java.util.ArrayList;
|
|
import java.util.Deque;
|
|
import java.util.Iterator;
|
|
import java.util.List;
|
|
|
|
/**
|
|
* Représente le plateau du jeu de Hex.
|
|
*
|
|
* <h2>Rappel des conditions de victoire</h2>
|
|
* <ul>
|
|
* <li>{@link Player#PLAYER1} gagne s'il existe un chemin de pions connectés
|
|
* reliant le bord gauche au bord droit.</li>
|
|
* <li>{@link Player#PLAYER2} gagne s'il existe un chemin de pions connectés
|
|
* reliant le bord haut au bord bas.</li>
|
|
* </ul>
|
|
*
|
|
* <h2>Idée de l'algorithme de détection de victoire</h2>
|
|
* On modélise le plateau comme un graphe :
|
|
* <ul>
|
|
* <li>Chaque case est un sommet</li>
|
|
* <li>Deux cases sont connectées si elles sont voisines sur la grille hexagonale (6 voisins)</li>
|
|
* </ul>
|
|
*
|
|
* Pour tester la victoire d'un joueur, on lance un parcours (DFS/BFS) :
|
|
* <ol>
|
|
* <li>On part de toutes les cases du bord de départ qui contiennent un pion du joueur.</li>
|
|
* <li>On explore tous les pions du joueur connectés à ces cases.</li>
|
|
* <li>Si on atteint le bord opposé, il existe un chemin : le joueur gagne.</li>
|
|
* </ol>
|
|
*
|
|
* 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;
|
|
|
|
/**
|
|
* Grille des cases.
|
|
* Une case vaut :
|
|
* <ul>
|
|
* <li>null : case vide</li>
|
|
* <li>PLAYER1 : pion du joueur 1</li>
|
|
* <li>PLAYER2 : pion du joueur 2</li>
|
|
* </ul>
|
|
*/
|
|
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}
|
|
};
|
|
|
|
/** Crée un plateau vide avec {@link Player#PLAYER1} qui commence. */
|
|
public HexBoard(int size) {
|
|
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).
|
|
*
|
|
* <h3>Détails de l'algorithme</h3>
|
|
* <ol>
|
|
* <li>On initialise une structure "visited" pour ne pas revisiter les cases.</li>
|
|
* <li>On met dans une pile toutes les cases du bord gauche (colonne 0)
|
|
* qui contiennent un pion PLAYER1.</li>
|
|
* <li>On effectue un DFS :
|
|
* <ul>
|
|
* <li>on dépile une case</li>
|
|
* <li>si elle est sur la colonne size-1 : on a touché le bord droit -> victoire</li>
|
|
* <li>sinon, on empile tous ses voisins qui sont des pions PLAYER1 et pas encore visités</li>
|
|
* </ul>
|
|
* </li>
|
|
* </ol>
|
|
*
|
|
* @return true si PLAYER1 a un chemin gauche->droite, false sinon
|
|
*/
|
|
private boolean hasPlayer1Won() {
|
|
boolean[][] visited = new boolean[size][size];
|
|
Deque<int[]> 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], 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) {
|
|
visited[nr][nc] = true;
|
|
stack.push(new int[]{nr, nc});
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Teste la victoire de PLAYER2 (haut -> bas).
|
|
*
|
|
* Même principe que {@link #hasPlayer1Won()} mais :
|
|
* <ul>
|
|
* <li>Départ : bord haut (ligne 0)</li>
|
|
* <li>Arrivée : bord bas (ligne size-1)</li>
|
|
* </ul>
|
|
*
|
|
* @return true si PLAYER2 a un chemin haut->bas, false sinon
|
|
*/
|
|
private boolean hasPlayer2Won() {
|
|
boolean[][] visited = new boolean[size][size];
|
|
Deque<int[]> 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], 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) {
|
|
visited[nr][nc] = true;
|
|
stack.push(new int[]{nr, nc});
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean isLegal(AbstractPly move) {
|
|
if (!(move instanceof HexPly)) return false;
|
|
HexPly hp = (HexPly) move;
|
|
int r = hp.getRow(), c = hp.getCol();
|
|
return inBounds(r, c)
|
|
&& getCell(r, c) == null
|
|
&& 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)) {
|
|
throw new IllegalArgumentException("Coup invalide: " + move);
|
|
}
|
|
if (!isLegal(move)) {
|
|
throw new IllegalStateException("Coup illégal: " + move);
|
|
}
|
|
|
|
HexPly hp = (HexPly) move;
|
|
setCell(hp.getRow(), hp.getCol(), hp.getPlayer());
|
|
addPlyToHistory(move);
|
|
setNextPlayer();
|
|
}
|
|
|
|
@Override
|
|
public void undoPly() {
|
|
AbstractPly last = removePlyFromHistory();
|
|
HexPly hp = (HexPly) last;
|
|
|
|
setCell(hp.getRow(), hp.getCol(), null);
|
|
setNextPlayer();
|
|
}
|
|
|
|
@Override
|
|
public boolean isGameOver() {
|
|
return hasPlayer1Won() || hasPlayer2Won();
|
|
}
|
|
|
|
@Override
|
|
public Result getResult() {
|
|
if (!isGameOver()) return null;
|
|
if (hasPlayer1Won()) return Result.WIN; // du point de vue PLAYER1
|
|
return Result.LOSS;
|
|
}
|
|
|
|
@Override
|
|
public Iterator<AbstractPly> iterator() {
|
|
Player me = getCurrentPlayer();
|
|
List<AbstractPly> 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));
|
|
}
|
|
}
|
|
}
|
|
return moves.iterator();
|
|
}
|
|
|
|
@Override
|
|
public IBoard safeCopy() {
|
|
HexBoard copy = new HexBoard(this.size, this.getCurrentPlayer());
|
|
for (int r = 0; r < size; r++) {
|
|
System.arraycopy(this.cells[r], 0, copy.cells[r], 0, size);
|
|
}
|
|
return copy;
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
StringBuilder sb = new StringBuilder();
|
|
for (int r = 0; r < size; r++) {
|
|
for (int k = 0; k < r; k++) sb.append(" ");
|
|
for (int c = 0; c < size; c++) {
|
|
Player p = getCell(r, c);
|
|
char ch = '.';
|
|
if (p == Player.PLAYER1) ch = '1';
|
|
else if (p == Player.PLAYER2) ch = '2';
|
|
sb.append(ch).append(" ");
|
|
}
|
|
sb.append("\n");
|
|
}
|
|
sb.append("Current player: ").append(getCurrentPlayer()).append("\n");
|
|
return sb.toString();
|
|
}
|
|
|
|
|
|
public Player getCellPlayer(int r, int c) {
|
|
return cells[r][c];
|
|
}
|
|
|
|
}
|