526 lines
18 KiB
Java
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<Integer> : 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<Integer> 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;
|
|
}
|
|
}
|