Files
BUT3ProjetJeuGroupe/fr/iut_fbleau/Avalam/AvalamBoard.java
2025-11-23 17:29:28 +01:00

526 lines
18 KiB
Java

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.
*
* <p>Cette classe gère :
* <ul>
* <li>L'état du plateau de jeu (grille 9x9)</li>
* <li>La validation des règles d'Avalam</li>
* <li>L'application et l'annulation des coups</li>
* <li>La détection de fin de partie et le calcul du résultat</li>
* </ul>
*
* <p>Le plateau est représenté par une grille où chaque case contient :
* <ul>
* <li>null : case vide (trou)</li>
* <li>ArrayList&lt;Integer&gt; : tour de pions (chaque Integer = 1 pour PLAYER1, 2 pour PLAYER2)</li>
* </ul>
*
* @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&lt;Integer&gt; représente une tour, où chaque Integer est un joueur
* (1=PLAYER1, 2=PLAYER2). null représente une case vide (trou).
*/
private ArrayList<Integer>[][] 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<Integer>
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.
*
* <p>Un coup est légal si :
* <ul>
* <li>Le jeu n'est pas terminé</li>
* <li>Le coup est un AvalamPly</li>
* <li>C'est le tour du bon joueur</li>
* <li>Les coordonnées sont dans les limites du plateau</li>
* <li>La case source n'est pas vide</li>
* <li>La case destination n'est pas vide (pas de trou)</li>
* <li>La destination est voisine de la source</li>
* <li>La hauteur totale après déplacement ne dépasse pas max_height (5)</li>
* </ul>
*
* @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.
*
* <p>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<Integer> sourceTower = grid[xFrom][yFrom];
ArrayList<Integer> 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é.
*
* <p>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<Integer> destTower = grid[xTo][yTo];
ArrayList<Integer> 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).
*
* <p>Vérifie s'il reste des coups possibles. Si aucun coup n'est possible :
* <ul>
* <li>Marque la partie comme terminée (gameOver = true)</li>
* <li>Calcule le score de chaque joueur (nombre de tours possédées)</li>
* <li>Détermine le résultat (WIN/LOSS/DRAW du point de vue de PLAYER1)</li>
* </ul>
*/
private void updateGameState() {
// Vérifier s'il y a encore des coups possibles
boolean hasLegalMove = false;
Iterator<AbstractPly> 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.
*
* <p>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<AbstractPly> iterator() {
return new LegalMovesIterator();
}
/**
* Itérateur sur tous les coups légaux possibles.
*
* <p>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<AbstractPly> {
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.
*
* <p>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;
}
}