package fr.iut_fbleau.Avalam; import fr.iut_fbleau.GameAPI.AbstractBoard; import fr.iut_fbleau.GameAPI.AbstractPly; import fr.iut_fbleau.GameAPI.Player; import fr.iut_fbleau.GameAPI.Result; import fr.iut_fbleau.GameAPI.IBoard; import java.util.Iterator; import java.util.ArrayList; import java.util.Deque; import java.util.ArrayDeque; import java.util.NoSuchElementException; /** * Implémentation du plateau de jeu Avalam conforme à l'API AbstractBoard. * *

Cette classe gère : *

* *

Le plateau est représenté par une grille où chaque case contient : *

* * @author AMARY Aurelien, DICK Adrien, FELIX-VIMALARATNAM Patrick, RABAN Hugo * @version 1.0 */ public class AvalamBoard extends AbstractBoard { /** Hauteur maximale d'une tour (règle d'Avalam : 5 pions maximum) */ private int max_height = 5; /** Résultat de la partie (null si la partie n'est pas terminée) */ private Result result; /** Indique si la partie est terminée */ private boolean gameOver; /** Taille du plateau (9x9) */ private int array_length = 9; /** * Grille du plateau de jeu. * Chaque ArrayList<Integer> représente une tour, où chaque Integer est un joueur * (1=PLAYER1, 2=PLAYER2). null représente une case vide (trou). */ private ArrayList[][] grid = new ArrayList[this.array_length][this.array_length]; /** * Constructeur par défaut. * Charge le plateau depuis le fichier par défaut (fr/iut_fbleau/Res/Plateau.txt). */ public AvalamBoard(){ // Charger depuis le fichier par défaut this("fr/iut_fbleau/Res/Plateau.txt"); } /** * Constructeur avec chargement depuis un fichier. * * @param filePath Chemin vers le fichier contenant la configuration du plateau */ public AvalamBoard(String filePath) { super(Player.PLAYER1, new ArrayDeque<>()); // Initialisation de la grille for (int i = 0; i < array_length; i++) { for (int j = 0; j < array_length; j++) { grid[i][j] = null; } } // Charger depuis le fichier Tower[][] towerGrid = fr.iut_fbleau.Avalam.logic.BoardLoader.loadFromFile(filePath); for (int i = 0; i < array_length; i++) { for (int j = 0; j < array_length; j++) { if (towerGrid[i][j] != null) { grid[i][j] = new ArrayList<>(); // Convertir Tower en ArrayList Color color = towerGrid[i][j].getColor(); int playerValue = (color == Color.COLOR1) ? 1 : 2; for (int k = 0; k < towerGrid[i][j].getHeight(); k++) { grid[i][j].add(playerValue); } } } } this.gameOver = false; this.result = null; updateGameState(); } /** * Vérifie si la partie est terminée. * * @return true si la partie est terminée (plus aucun coup possible) */ @Override public boolean isGameOver() { return this.gameOver; } /** * Retourne le résultat de la partie. * * @return Le résultat (WIN/LOSS/DRAW du point de vue de PLAYER1), ou null si la partie n'est pas terminée */ @Override public Result getResult() { return this.result; } /** * Vérifie si les coordonnées sont dans les limites du plateau. * * @param x Coordonnée X * @param y Coordonnée Y * @return true si les coordonnées sont valides (dans [0, 8]) */ public boolean inRange(int x, int y) { return x >= 0 && x < array_length && y >= 0 && y < array_length; } /** * Vérifie si deux cases sont voisines (horizontal, vertical ou diagonal). * * @param x1 Coordonnée X de la première case * @param y1 Coordonnée Y de la première case * @param x2 Coordonnée X de la deuxième case * @param y2 Coordonnée Y de la deuxième case * @return true si les deux cases sont voisines (distance de 1 case maximum) */ private boolean isNeighbor(int x1, int y1, int x2, int y2) { int dx = Math.abs(x1 - x2); int dy = Math.abs(y1 - y2); return (dx <= 1 && dy <= 1) && !(dx == 0 && dy == 0); } /** * Retourne la hauteur d'une tour (nombre de pions). * * @param x Coordonnée X de la case * @param y Coordonnée Y de la case * @return La hauteur de la tour (0 si la case est vide) */ private int getTowerHeight(int x, int y) { if (grid[x][y] == null) { return 0; } return grid[x][y].size(); } /** * Retourne le joueur propriétaire d'une tour (celui du sommet). * * @param x Coordonnée X de la case * @param y Coordonnée Y de la case * @return Le joueur propriétaire (PLAYER1 ou PLAYER2), ou null si la case est vide */ private Player getTowerOwner(int x, int y) { if (grid[x][y] == null || grid[x][y].isEmpty()) { return null; } int topPlayer = grid[x][y].get(grid[x][y].size() - 1); return (topPlayer == 1) ? Player.PLAYER1 : Player.PLAYER2; } /** * Vérifie si un coup est légal selon les règles d'Avalam. * *

Un coup est légal si : *

* * @param c Le coup à vérifier * @return true si le coup est légal, false sinon */ @Override public boolean isLegal(AbstractPly c) { if (this.gameOver) { return false; } if (!(c instanceof AvalamPly)) { return false; } AvalamPly coup = (AvalamPly) c; // Vérifier que c'est le bon joueur if (coup.getPlayer() != getCurrentPlayer()) { return false; } int xFrom = coup.getXFrom(); int yFrom = coup.getYFrom(); int xTo = coup.getXTo(); int yTo = coup.getYTo(); // Vérifier que les coordonnées sont dans les limites if (!inRange(xFrom, yFrom) || !inRange(xTo, yTo)) { return false; } // Règle : On ne peut pas déplacer depuis une case vide if (grid[xFrom][yFrom] == null || grid[xFrom][yFrom].isEmpty()) { return false; } // Règle : On ne peut pas poser sur une case vide (trou) if (grid[xTo][yTo] == null) { return false; } // Règle : La destination doit être voisine if (!isNeighbor(xFrom, yFrom, xTo, yTo)) { return false; } // Règle : On déplace toute la pile int heightFrom = getTowerHeight(xFrom, yFrom); int heightTo = getTowerHeight(xTo, yTo); // Règle : La hauteur totale après déplacement ne doit pas dépasser max_height if (heightFrom + heightTo > max_height) { return false; } return true; } /** * Applique un coup sur le plateau. * *

Le coup déplace toute la tour de la case source vers la case destination. * La méthode met également à jour l'historique et change de joueur via l'appel à super.doPly(). * * @param c Le coup à appliquer * @throws IllegalStateException si le coup n'est pas légal */ @Override public void doPly(AbstractPly c) { if (!isLegal(c)) { throw new IllegalStateException("Coup illégal : " + c); } AvalamPly coup = (AvalamPly) c; int xFrom = coup.getXFrom(); int yFrom = coup.getYFrom(); int xTo = coup.getXTo(); int yTo = coup.getYTo(); // Stocker la hauteur de la source avant le déplacement (pour undo) int sourceHeight = getTowerHeight(xFrom, yFrom); coup.setSourceHeight(sourceHeight); // Déplacer toute la pile de la source vers la destination // On empile les pions de la source sur ceux de la destination ArrayList sourceTower = grid[xFrom][yFrom]; ArrayList destTower = grid[xTo][yTo]; // Ajouter tous les pions de la source à la destination for (Integer pion : sourceTower) { destTower.add(pion); } // Vider la case source grid[xFrom][yFrom] = null; // Appeler la méthode parente pour gérer l'historique et changer de joueur super.doPly(c); // Mettre à jour l'état du jeu updateGameState(); } /** * Annule le dernier coup joué. * *

Restaure l'état du plateau avant le dernier coup. * La méthode met également à jour l'historique et change de joueur via l'appel à super.undoPly(). * * @throws IllegalStateException si aucun coup n'a été joué ou si la hauteur source n'est pas définie */ @Override public void undoPly() { AbstractPly lastPly = getLastPlyFromHistory(); if (lastPly == null) { throw new IllegalStateException("Aucun coup à annuler"); } AvalamPly coup = (AvalamPly) lastPly; int xFrom = coup.getXFrom(); int yFrom = coup.getYFrom(); int xTo = coup.getXTo(); int yTo = coup.getYTo(); int sourceHeight = coup.getSourceHeight(); if (sourceHeight < 0) { throw new IllegalStateException("Impossible d'annuler : hauteur source non définie"); } // La destination contient maintenant : destination_avant + source // On doit retirer les derniers sourceHeight pions pour reconstruire la source ArrayList destTower = grid[xTo][yTo]; ArrayList sourceTower = new ArrayList<>(); // Retirer les derniers pions de la destination (ceux qui venaient de la source) for (int i = 0; i < sourceHeight; i++) { if (destTower.isEmpty()) { throw new IllegalStateException("Erreur lors de l'annulation : tour destination vide"); } sourceTower.add(0, destTower.remove(destTower.size() - 1)); } // Remettre la tour source à sa place grid[xFrom][yFrom] = sourceTower; // Si la destination est vide après annulation, la mettre à null if (destTower.isEmpty()) { grid[xTo][yTo] = null; } // Appeler la méthode parente super.undoPly(); // Mettre à jour l'état du jeu updateGameState(); } /** * Met à jour l'état du jeu (gameOver et result). * *

Vérifie s'il reste des coups possibles. Si aucun coup n'est possible : *

*/ private void updateGameState() { // Vérifier s'il y a encore des coups possibles boolean hasLegalMove = false; Iterator it = iterator(); if (it.hasNext()) { hasLegalMove = true; } if (!hasLegalMove) { this.gameOver = true; // Calculer le résultat int scorePlayer1 = 0; int scorePlayer2 = 0; for (int i = 0; i < array_length; i++) { for (int j = 0; j < array_length; j++) { Player owner = getTowerOwner(i, j); if (owner == Player.PLAYER1) { scorePlayer1++; } else if (owner == Player.PLAYER2) { scorePlayer2++; } } } // Result est du point de vue de PLAYER1 if (scorePlayer1 > scorePlayer2) { this.result = Result.WIN; } else if (scorePlayer1 < scorePlayer2) { this.result = Result.LOSS; } else { this.result = Result.DRAW; } } else { this.gameOver = false; this.result = null; } } /** * Retourne un itérateur sur tous les coups légaux possibles pour le joueur actuel. * *

Cet itérateur est utilisé par les bots pour explorer les coups possibles. * * @return Un itérateur sur tous les coups légaux */ @Override public Iterator iterator() { return new LegalMovesIterator(); } /** * Itérateur sur tous les coups légaux possibles. * *

Parcourt toutes les cases source possibles et trouve toutes les destinations valides * pour chaque source, en respectant les règles d'Avalam. */ private class LegalMovesIterator implements Iterator { private int currentX = 0; private int currentY = 0; private int currentDestX = -1; private int currentDestY = -1; private AbstractPly nextMove = null; public LegalMovesIterator() { findNextMove(); } @Override public boolean hasNext() { return nextMove != null; } @Override public AbstractPly next() { if (nextMove == null) { throw new NoSuchElementException(); } AbstractPly move = nextMove; findNextMove(); return move; } private void findNextMove() { nextMove = null; // Parcourir toutes les cases source possibles while (currentX < array_length) { // Si on a une case source valide if (grid[currentX][currentY] != null && !grid[currentX][currentY].isEmpty()) { // Chercher une destination valide while (currentDestX < array_length) { currentDestY++; if (currentDestY >= array_length) { currentDestY = 0; currentDestX++; } if (currentDestX >= array_length) { break; } // Vérifier si ce mouvement est légal if (isNeighbor(currentX, currentY, currentDestX, currentDestY) && grid[currentDestX][currentDestY] != null && getTowerHeight(currentX, currentY) + getTowerHeight(currentDestX, currentDestY) <= max_height) { nextMove = new AvalamPly(getCurrentPlayer(), currentX, currentY, currentDestX, currentDestY); return; } } } // Passer à la case suivante currentY++; if (currentY >= array_length) { currentY = 0; currentX++; } currentDestX = -1; currentDestY = -1; } } } /** * Crée une copie indépendante du plateau. * *

Cette méthode est utilisée pour empêcher la triche : les bots reçoivent une copie * du plateau et ne peuvent pas modifier l'état original. * * @return Une copie indépendante du plateau */ @Override public IBoard safeCopy() { AvalamBoard copy = new AvalamBoard(true); // Réinitialiser l'état sans charger depuis le fichier copy.gameOver = this.gameOver; copy.result = this.result; copy.max_height = this.max_height; // Copier la grille for (int i = 0; i < array_length; i++) { for (int j = 0; j < array_length; j++) { if (grid[i][j] != null) { copy.grid[i][j] = new ArrayList<>(grid[i][j]); } else { copy.grid[i][j] = null; } } } return copy; } /** * Constructeur privé pour safeCopy(). * Crée un plateau vide sans charger depuis un fichier. * * @param empty Paramètre inutilisé, présent uniquement pour différencier ce constructeur */ private AvalamBoard(boolean empty) { super(Player.PLAYER1, new ArrayDeque<>()); // Initialisation de la grille vide for (int i = 0; i < array_length; i++) { for (int j = 0; j < array_length; j++) { grid[i][j] = null; } } this.gameOver = false; this.result = null; } }