diff --git a/fr/iut_fbleau/Avalam/AvalamBoard.java b/fr/iut_fbleau/Avalam/AvalamBoard.java index 6c23677..a91a8a9 100644 --- a/fr/iut_fbleau/Avalam/AvalamBoard.java +++ b/fr/iut_fbleau/Avalam/AvalamBoard.java @@ -280,12 +280,23 @@ public class AvalamBoard extends AbstractBoard { */ @Override public IBoard safeCopy() { + Tower[][] newGrid = new Tower[SIZE][SIZE]; - for (int r = 0; r < SIZE; r++) - for (int c = 0; c < SIZE; c++) - newGrid[r][c] = grid[r][c]; + for (int r = 0; r < SIZE; r++) { + for (int c = 0; c < SIZE; c++) { - return new AvalamBoard(newGrid, getCurrentPlayer(), gameOver, result); + Tower t = grid[r][c]; + if (t == null) { + newGrid[r][c] = null; + } else { + // Copie profonde : on recrée une nouvelle Tower indépendante + newGrid[r][c] = new Tower(t.getHeight(), t.getColor()); + } + } + } + + // On conserve le joueur courant + return new AvalamBoard(newGrid, getCurrentPlayer()); } } diff --git a/fr/iut_fbleau/Avalam/AvalamWindow.java b/fr/iut_fbleau/Avalam/AvalamWindow.java index d0e6304..ddec50e 100644 --- a/fr/iut_fbleau/Avalam/AvalamWindow.java +++ b/fr/iut_fbleau/Avalam/AvalamWindow.java @@ -1,5 +1,8 @@ package fr.iut_fbleau.Avalam; +import fr.iut_fbleau.Bot.AlphaBetaBot; +// A FAIRE PLUS TARD (PVGOD) +// import fr.iut_fbleau.Bot.DivineBot; import fr.iut_fbleau.Bot.IdiotBot; import fr.iut_fbleau.GameAPI.AbstractPly; import fr.iut_fbleau.GameAPI.Player; @@ -9,24 +12,25 @@ import javax.swing.*; import java.awt.*; /** -* La classe AvalamWindow -* -* Fenêtre principale (interface graphique) du jeu Avalam. -* Elle contient : -* - le plateau (BoardView) -* - l’affichage du score (ScoreView) -* - l’affichage du joueur courant (TurnView) -* -* Elle pilote un objet AvalamBoard (moteur du jeu). -* Elle peut fonctionner en mode : -* - joueur vs joueur -* - joueur vs bot idiot (aléatoire) -* - joueur vs bot alpha (préparé) -* -* @version 1.0 -* Date : -* Licence : -*/ + * La classe AvalamWindow + * + * Fenêtre principale (interface graphique) du jeu Avalam. + * Elle contient : + * - le plateau (BoardView) + * - l’affichage du score (ScoreView) + * - l’affichage du joueur courant (TurnView) + * + * Elle pilote un objet AvalamBoard (moteur du jeu). + * Elle peut fonctionner en mode : + * - joueur vs joueur + * - joueur vs bot idiot (aléatoire) + * - joueur vs bot alpha (cut-off) + * - joueur vs bot divin (PVGOD) + * + * @version 1.0 + * Date : + * Licence : + */ public class AvalamWindow extends JFrame { //Attributs @@ -52,29 +56,62 @@ public class AvalamWindow extends JFrame { /** Bot idiot (utilisé si mode PVBOT). */ private final IdiotBot idiotBot; + /** Bot Alpha-Beta (utilisé si mode PVALPHA). */ + private final AlphaBetaBot alphaBot; + + // A FAIRE PLUS TARD (PVGOD) + // /** Bot Divin (utilisé si mode PVGOD). */ + // private final DivineBot divineBot; + + /** + * A FAIRE PLUS TARD (PVGOD) + * On garde l'attribut à null pour ne pas casser la compilation, + * mais toute la logique PVGOD est désactivée/commentée. + */ + private final Object divineBot = null; + /** Indique si une animation de tour de bot est en cours. */ private boolean botAnimating = false; //Constructeur /** - * Construit la fenêtre en mode joueur vs joueur. - */ + * Construit la fenêtre en mode joueur vs joueur. + */ public AvalamWindow() { - this(GameMode.PVP); + this(GameMode.PVP, 4); } /** - * Construit la fenêtre en fonction du mode choisi. - * - * @param mode mode de jeu - */ + * Construit la fenêtre en fonction du mode choisi. + * Pour PVALPHA/PVGOD, la profondeur par défaut est 4. + * + * @param mode mode de jeu + */ public AvalamWindow(GameMode mode) { + this(mode, 4); + } + + /** + * Construit la fenêtre en fonction du mode choisi. + * Si le mode est PVALPHA ou PVGOD, la profondeur est utilisée comme cut-off. + * + * @param mode mode de jeu + * @param alphaDepth profondeur de recherche pour Alpha-Beta / Bot Divin + */ + public AvalamWindow(GameMode mode, int alphaDepth) { super("Avalam"); this.mode = mode; + this.idiotBot = (mode == GameMode.PVBOT) ? new IdiotBot(botPlayer) : null; + int depth = Math.max(1, alphaDepth); + this.alphaBot = (mode == GameMode.PVALPHA) ? new AlphaBetaBot(botPlayer, depth) : null; + + // A FAIRE PLUS TARD (PVGOD) + // this.divineBot = (mode == GameMode.PVGOD) ? new DivineBot(botPlayer, depth) : null; + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLayout(new BorderLayout()); @@ -109,16 +146,16 @@ public class AvalamWindow extends JFrame { setLocationRelativeTo(null); setVisible(true); - // Si un jour le bot doit commencer, on peut déclencher ici. + // Si un jour le bot devait commencer (pas le cas ici), on le ferait jouer ici. maybePlayBotTurn(); } //Méthodes /** - * Appelée après chaque coup (humain ou bot). - * Met à jour score, tour, et affiche la fin de partie. - */ + * Appelée après chaque coup (humain ou bot). + * Met à jour score, tour, et affiche la fin de partie. + */ public void onBoardUpdated() { scoreView.updateScores( @@ -161,27 +198,51 @@ public class AvalamWindow extends JFrame { } /** - * Fait jouer le bot en deux étapes visibles : - * 1) sélection de la tour (affiche les coups légaux) - * 2) attente 1 seconde - * 3) déplacement vers la destination - * - * Le tout sans bloquer l'interface (Timer Swing). - */ + * Fait jouer le bot (idiot / alpha / divin) en deux étapes visibles : + * 1) sélection de la tour (affiche les coups légaux) + * 2) attente 1 seconde + * 3) déplacement vers la destination + * + * Le tout sans bloquer l'interface (Timer Swing). + */ private void maybePlayBotTurn() { - if (mode != GameMode.PVBOT) return; + // Mode joueur vs joueur : aucun bot + if (mode == GameMode.PVP) return; + + // Sécurité if (board.isGameOver()) return; if (board.getCurrentPlayer() != botPlayer) return; if (botAnimating) return; + // Vérifier qu'on a bien le bot correspondant + if (mode == GameMode.PVBOT && idiotBot == null) return; + if (mode == GameMode.PVALPHA && alphaBot == null) return; + + // A FAIRE PLUS TARD (PVGOD) + // if (mode == GameMode.PVGOD && divineBot == null) return; + + // A FAIRE PLUS TARD (PVGOD) + // Pour l'instant, si PVGOD est sélectionné, on ne joue pas de coup bot. + if (mode == GameMode.PVGOD) return; + botAnimating = true; // Désactiver les interactions du joueur pendant le tour du bot. boardView.setInteractionEnabled(false); // Choix d’un coup sur une copie sûre - AbstractPly botMove = idiotBot.giveYourMove(board.safeCopy()); + AbstractPly botMove; + if (mode == GameMode.PVBOT) { + botMove = idiotBot.giveYourMove(board.safeCopy()); + } else if (mode == GameMode.PVALPHA) { + botMove = alphaBot.giveYourMove(board.safeCopy()); + } else { + // A FAIRE PLUS TARD (PVGOD) + // botMove = divineBot.giveYourMove(board.safeCopy()); + botMove = null; + } + if (botMove == null) { botAnimating = false; boardView.setInteractionEnabled(true); @@ -226,11 +287,11 @@ public class AvalamWindow extends JFrame { } /** - * Calcule le score d'une couleur : nombre de tours contrôlées. - * - * @param c couleur à compter - * @return nombre de tours appartenant à la couleur c - */ + * Calcule le score d'une couleur : nombre de tours contrôlées. + * + * @param c couleur à compter + * @return nombre de tours appartenant à la couleur c + */ private int computeScore(Color c) { int score = 0; for (int r = 0; r < AvalamBoard.SIZE; r++) { @@ -245,10 +306,10 @@ public class AvalamWindow extends JFrame { } /** - * Retourne le message affiché pour le joueur courant. - * - * @return message du tour - */ + * Retourne le message affiché pour le joueur courant. + * + * @return message du tour + */ private String turnMessage() { return "Tour du joueur : " + (board.getCurrentPlayer() == Player.PLAYER1 ? "Jaune" : "Rouge"); diff --git a/fr/iut_fbleau/Avalam/GameMode.java b/fr/iut_fbleau/Avalam/GameMode.java index 8d5ac1b..09c306a 100644 --- a/fr/iut_fbleau/Avalam/GameMode.java +++ b/fr/iut_fbleau/Avalam/GameMode.java @@ -6,5 +6,6 @@ package fr.iut_fbleau.Avalam; public enum GameMode { PVP, // joueur vs joueur PVBOT, // joueur vs bot idiot - PVALPHA // joueur vs bot alpha (préparé) + PVALPHA, // joueur vs bot alpha + PVGOD // joueur vs bot stratégique } diff --git a/fr/iut_fbleau/Avalam/Main.java b/fr/iut_fbleau/Avalam/Main.java index 91bb0aa..6cd72de 100644 --- a/fr/iut_fbleau/Avalam/Main.java +++ b/fr/iut_fbleau/Avalam/Main.java @@ -14,7 +14,8 @@ public class Main { String[] options = { "joueur vs joueur", "joueur vs botidiot", - "joueur vs bot alpha" + "joueur vs bot alpha", + "joueur vs bot divin (NON IMPLEMENTE)" }; int choice = JOptionPane.showOptionDialog( @@ -28,20 +29,38 @@ public class Main { options[0] ); + if (choice == -1) System.exit(0); + GameMode mode; if (choice == 1) mode = GameMode.PVBOT; else if (choice == 2) mode = GameMode.PVALPHA; + else if (choice == 3) mode = GameMode.PVGOD; else mode = GameMode.PVP; - // Si alpha choisi : non implémenté, on prévient et on lance en PVP (préparation). - if (mode == GameMode.PVALPHA) { - JOptionPane.showMessageDialog( + // Pour ALPHA et GOD : demander une profondeur + if (mode == GameMode.PVALPHA || mode == GameMode.PVGOD) { + + String s = JOptionPane.showInputDialog( null, - "Bot Alpha-Beta non implémenté pour l'instant.\nLancement en joueur vs joueur.", - "Information", - JOptionPane.INFORMATION_MESSAGE + "Profondeur de recherche ?\n(Conseil 4)", + (mode == GameMode.PVGOD ? "Bot Divin (PVGOD)" : "Bot Alpha-Beta"), + JOptionPane.QUESTION_MESSAGE ); - mode = GameMode.PVP; + + int depth = 4; // défaut + if (s != null) { + try { depth = Integer.parseInt(s.trim()); } + catch (Exception ignored) { depth = 4; } + } else { + // Annulation : on revient en PVP + new AvalamWindow(GameMode.PVP); + return; + } + + if (depth < 1) depth = 1; + + new AvalamWindow(mode, depth); + return; } new AvalamWindow(mode); diff --git a/fr/iut_fbleau/Bot/AlphaBetaBot.java b/fr/iut_fbleau/Bot/AlphaBetaBot.java index 459b99f..8694e42 100644 --- a/fr/iut_fbleau/Bot/AlphaBetaBot.java +++ b/fr/iut_fbleau/Bot/AlphaBetaBot.java @@ -1,22 +1,217 @@ package fr.iut_fbleau.Bot; +import fr.iut_fbleau.Avalam.AvalamBoard; +import fr.iut_fbleau.Avalam.Color; +import fr.iut_fbleau.Avalam.Tower; import fr.iut_fbleau.GameAPI.AbstractGamePlayer; import fr.iut_fbleau.GameAPI.AbstractPly; import fr.iut_fbleau.GameAPI.IBoard; import fr.iut_fbleau.GameAPI.Player; +import fr.iut_fbleau.GameAPI.Result; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Random; /** - * Bot Alpha-Beta (préparé). - * Pour l'instant non implémenté. + * Bot Alpha-Beta avec cut-off (profondeur maximale). + * + * Idée : + * - si on atteint la profondeur limite, on évalue la position (heuristique). + * - sinon, on explore les coups avec Alpha-Beta (minimax optimisé). + * + * */ public class AlphaBetaBot extends AbstractGamePlayer { - public AlphaBetaBot(Player p) { + //Attributs + + /** Joueur contrôlé par ce bot (PLAYER1 ou PLAYER2). */ + private final Player me; + + /** Profondeur maximale de recherche (cut-off). */ + private final int maxDepth; + + /** Générateur aléatoire pour départager des coups de même valeur. */ + private final Random rng = new Random(); + + //Constructeur + + /** + * Construit un bot Alpha-Beta. + * + * @param p joueur contrôlé par ce bot + * @param maxDepth profondeur maximale de recherche + */ + public AlphaBetaBot(Player p, int maxDepth) { super(p); + this.me = p; + this.maxDepth = Math.max(1, maxDepth); } + //Méthodes + + /** + * Méthode appelée par GameAPI : le bot doit choisir un coup. + * + * @param board copie sûre de l'état de jeu (IBoard) + * @return un coup (AbstractPly) ou null si aucun coup possible + */ @Override public AbstractPly giveYourMove(IBoard board) { - throw new UnsupportedOperationException("AlphaBetaBot non implémenté pour l'instant."); + + if (board == null || board.isGameOver()) return null; + + List moves = listMoves(board); + if (moves.isEmpty()) return null; + + boolean isMax = (board.getCurrentPlayer() == me); + int bestValue = isMax ? Integer.MIN_VALUE : Integer.MAX_VALUE; + + List bestMoves = new ArrayList<>(); + + int alpha = Integer.MIN_VALUE; + int beta = Integer.MAX_VALUE; + + for (AbstractPly m : moves) { + IBoard next = board.safeCopy(); + next.doPly(m); + + int value = alphaBeta(next, maxDepth - 1, alpha, beta); + + if (isMax) { + if (value > bestValue) { + bestValue = value; + bestMoves.clear(); + bestMoves.add(m); + } else if (value == bestValue) { + bestMoves.add(m); + } + alpha = Math.max(alpha, bestValue); + } else { + if (value < bestValue) { + bestValue = value; + bestMoves.clear(); + bestMoves.add(m); + } else if (value == bestValue) { + bestMoves.add(m); + } + beta = Math.min(beta, bestValue); + } + } + + return bestMoves.get(rng.nextInt(bestMoves.size())); } + + /** + * Fonction récursive Alpha-Beta. + */ + private int alphaBeta(IBoard board, int depth, int alpha, int beta) { + + if (board.isGameOver()) { + return terminalValue(board); + } + + if (depth == 0) { + return evaluate(board); + } + + boolean isMax = (board.getCurrentPlayer() == me); + + List moves = listMoves(board); + if (moves.isEmpty()) { + return evaluate(board); + } + + if (isMax) { + int best = Integer.MIN_VALUE; + + for (AbstractPly m : moves) { + IBoard next = board.safeCopy(); + next.doPly(m); + + int val = alphaBeta(next, depth - 1, alpha, beta); + best = Math.max(best, val); + alpha = Math.max(alpha, best); + + if (alpha >= beta) break; + } + return best; + + } else { + int best = Integer.MAX_VALUE; + + for (AbstractPly m : moves) { + IBoard next = board.safeCopy(); + next.doPly(m); + + int val = alphaBeta(next, depth - 1, alpha, beta); + best = Math.min(best, val); + beta = Math.min(beta, best); + + if (alpha >= beta) break; + } + return best; + } + } + + /** + * Convertit le résultat final en valeur très grande (victoire/défaite). + * Result est du point de vue de PLAYER1. + */ + private int terminalValue(IBoard board) { + Result r = board.getResult(); + if (r == null) return 0; + + boolean botIsP1 = (me == Player.PLAYER1); + + if (r == Result.DRAW) return 0; + + if (botIsP1) { + return (r == Result.WIN) ? 100000 : -100000; + } else { + return (r == Result.LOSS) ? 100000 : -100000; + } + } + + /** + * Heuristique simple Avalam : + * score = (tours du bot) - (tours adverses) + */ + private int evaluate(IBoard board) { + + if (!(board instanceof AvalamBoard)) return 0; + AvalamBoard b = (AvalamBoard) board; + + Color botColor = (me == Player.PLAYER1) ? Color.YELLOW : Color.RED; + Color oppColor = (me == Player.PLAYER1) ? Color.RED : Color.YELLOW; + + int botTowers = 0; + int oppTowers = 0; + + for (int r = 0; r < AvalamBoard.SIZE; r++) { + for (int c = 0; c < AvalamBoard.SIZE; c++) { + Tower t = b.getTowerAt(r, c); + if (t == null) continue; + + if (t.getColor() == botColor) botTowers++; + else if (t.getColor() == oppColor) oppTowers++; + } + } + + return botTowers - oppTowers; + } + + /** + * Récupère tous les coups légaux via iterator(). + */ + private List listMoves(IBoard board) { + List moves = new ArrayList<>(); + Iterator it = board.iterator(); + while (it.hasNext()) moves.add(it.next()); + return moves; + } + + //Affichage } diff --git a/fr/iut_fbleau/Bot/DivineBot.java b/fr/iut_fbleau/Bot/DivineBot.java new file mode 100644 index 0000000..cbc6fec --- /dev/null +++ b/fr/iut_fbleau/Bot/DivineBot.java @@ -0,0 +1,22 @@ +package fr.iut_fbleau.Bot; + +import fr.iut_fbleau.Avalam.AvalamBoard; +import fr.iut_fbleau.Avalam.AvalamPly; +import fr.iut_fbleau.Avalam.Color; +import fr.iut_fbleau.Avalam.Tower; +import fr.iut_fbleau.GameAPI.AbstractGamePlayer; +import fr.iut_fbleau.GameAPI.AbstractPly; +import fr.iut_fbleau.GameAPI.IBoard; +import fr.iut_fbleau.GameAPI.Player; +import fr.iut_fbleau.GameAPI.Result; + +import java.util.*; + +/** + * Bot "Divin" (fort) pour Avalam. + * + * + * Objectif : trop fort. */ +public class DivineBot{ + +}