diff --git a/fr/iut_fbleau/Avalam/ArenaGame.java b/fr/iut_fbleau/Avalam/ArenaGame.java new file mode 100644 index 0000000..aa27144 --- /dev/null +++ b/fr/iut_fbleau/Avalam/ArenaGame.java @@ -0,0 +1,44 @@ +package fr.iut_fbleau.Avalam; + +import fr.iut_fbleau.GameAPI.AbstractGame; +import fr.iut_fbleau.GameAPI.AbstractGamePlayer; +import fr.iut_fbleau.GameAPI.IBoard; +import fr.iut_fbleau.GameAPI.Player; + +import java.util.EnumMap; + +/** + * Classe pour jouer une partie entre deux bots sans interface graphique. + * Utilisée dans le mode Arène. + * + * @version 1.0 + */ +public class ArenaGame extends AbstractGame { + + /** + * Construit une partie Arène entre deux bots. + * + * @param board plateau initial + * @param bot1 bot pour PLAYER1 + * @param bot2 bot pour PLAYER2 + */ + public ArenaGame(IBoard board, AbstractGamePlayer bot1, AbstractGamePlayer bot2) { + super(board, createPlayerMap(bot1, bot2)); + } + + /** + * Crée la map des joueurs pour AbstractGame. + * + * @param bot1 bot pour PLAYER1 + * @param bot2 bot pour PLAYER2 + * @return une EnumMap associant chaque Player à son bot + */ + private static EnumMap createPlayerMap( + AbstractGamePlayer bot1, AbstractGamePlayer bot2) { + EnumMap map = new EnumMap<>(Player.class); + map.put(Player.PLAYER1, bot1); + map.put(Player.PLAYER2, bot2); + return map; + } +} + diff --git a/fr/iut_fbleau/Avalam/ArenaWindow.java b/fr/iut_fbleau/Avalam/ArenaWindow.java new file mode 100644 index 0000000..09982df --- /dev/null +++ b/fr/iut_fbleau/Avalam/ArenaWindow.java @@ -0,0 +1,280 @@ +package fr.iut_fbleau.Avalam; + +import fr.iut_fbleau.Bot.AlphaBetaBot; +import fr.iut_fbleau.Bot.IdiotBot; +import fr.iut_fbleau.GameAPI.AbstractGamePlayer; +import fr.iut_fbleau.GameAPI.Player; +import fr.iut_fbleau.GameAPI.Result; + +import javax.swing.*; +import javax.swing.table.DefaultTableModel; +import java.awt.*; +import java.util.ArrayList; +import java.util.List; + +/** + * Fenêtre pour le mode Arène. + * Permet de sélectionner deux bots, le nombre de parties, et affiche les résultats. + * + * @version 1.0 + */ +public class ArenaWindow extends JFrame { + + /** Tableau affichant les résultats des parties. */ + private JTable resultsTable; + + /** Modèle de données pour le tableau des résultats. */ + private DefaultTableModel tableModel; + + /** Liste des résultats des parties (non utilisée actuellement). */ + private List results; + + /** + * Construit la fenêtre Arène. + */ + public ArenaWindow() { + super("Arène - Bot vs Bot"); + setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + setLayout(new BorderLayout()); + + results = new ArrayList<>(); + + // Panneau de configuration + JPanel configPanel = createConfigPanel(); + add(configPanel, BorderLayout.NORTH); + + // Tableau des résultats + createResultsTable(); + JScrollPane scrollPane = new JScrollPane(resultsTable); + add(scrollPane, BorderLayout.CENTER); + + // Panneau des boutons + JPanel buttonPanel = new JPanel(new FlowLayout()); + JButton startButton = new JButton("Lancer les parties"); + startButton.addActionListener(e -> showConfigDialog()); + buttonPanel.add(startButton); + + JButton backButton = new JButton("Retour au menu"); + backButton.addActionListener(e -> { + dispose(); // Ferme la fenêtre Arène + Main.showModeSelection(); // Affiche le menu principal + }); + buttonPanel.add(backButton); + + add(buttonPanel, BorderLayout.SOUTH); + + pack(); + setSize(600, 400); + setLocationRelativeTo(null); + setVisible(true); + } + + /** + * Crée le panneau de configuration (pour l'instant vide, sera rempli par le dialogue). + * + * @return un JPanel contenant les informations de configuration + */ + private JPanel createConfigPanel() { + JPanel panel = new JPanel(); + panel.setBorder(BorderFactory.createTitledBorder("Configuration")); + panel.add(new JLabel("Utilisez le bouton 'Lancer les parties' pour configurer et démarrer.")); + return panel; + } + + /** + * Crée le tableau des résultats avec les colonnes : Partie, Bot 1, Bot 2, Gagnant. + */ + private void createResultsTable() { + String[] columnNames = {"Partie", "Bot 1", "Bot 2", "Gagnant"}; + tableModel = new DefaultTableModel(columnNames, 0) { + @Override + public boolean isCellEditable(int row, int column) { + return false; + } + }; + resultsTable = new JTable(tableModel); + resultsTable.setFillsViewportHeight(true); + } + + /** + * Affiche le dialogue de configuration et lance les parties. + */ + private void showConfigDialog() { + // Choix du bot 1 + String[] botTypes = {"Bot Idiot", "Bot Alpha-Beta", "Bot Divin"}; + String bot1Choice = (String) JOptionPane.showInputDialog( + this, + "Choisissez le Bot 1 (Joueur 1) :", + "Configuration Arène - Bot 1", + JOptionPane.QUESTION_MESSAGE, + null, + botTypes, + botTypes[0] + ); + + if (bot1Choice == null) return; + + // Choix du bot 2 + String bot2Choice = (String) JOptionPane.showInputDialog( + this, + "Choisissez le Bot 2 (Joueur 2) :", + "Configuration Arène - Bot 2", + JOptionPane.QUESTION_MESSAGE, + null, + botTypes, + botTypes[0] + ); + + if (bot2Choice == null) return; + + // Profondeur pour Alpha-Beta et Divin + int depth = 4; + if (bot1Choice.contains("Alpha") || bot1Choice.contains("Divin") || + bot2Choice.contains("Alpha") || bot2Choice.contains("Divin")) { + String depthStr = JOptionPane.showInputDialog( + this, + "Profondeur de recherche pour les bots Alpha-Beta/Divin ?\n(Conseil: 4)", + "Profondeur", + JOptionPane.QUESTION_MESSAGE + ); + if (depthStr != null) { + try { + depth = Integer.parseInt(depthStr.trim()); + if (depth < 1) depth = 1; + } catch (Exception e) { + depth = 4; + } + } + } + + // Nombre de parties + String nbPartiesStr = JOptionPane.showInputDialog( + this, + "Combien de parties voulez-vous jouer ?", + "Nombre de parties", + JOptionPane.QUESTION_MESSAGE + ); + + if (nbPartiesStr == null) return; + + int nbParties; + try { + nbParties = Integer.parseInt(nbPartiesStr.trim()); + if (nbParties < 1) { + JOptionPane.showMessageDialog(this, "Le nombre de parties doit être au moins 1."); + return; + } + } catch (Exception e) { + JOptionPane.showMessageDialog(this, "Nombre invalide."); + return; + } + + // Lancer les parties + runArena(bot1Choice, bot2Choice, depth, nbParties); + } + + /** + * Lance les parties entre les deux bots. + * + * @param bot1Type type du bot 1 (Joueur 1) + * @param bot2Type type du bot 2 (Joueur 2) + * @param depth profondeur de recherche pour les bots Alpha-Beta/Divin + * @param nbParties nombre de parties à jouer + */ + private void runArena(String bot1Type, String bot2Type, int depth, int nbParties) { + // Créer les bots + AbstractGamePlayer bot1 = createBot(bot1Type, Player.PLAYER1, depth); + AbstractGamePlayer bot2 = createBot(bot2Type, Player.PLAYER2, depth); + + if (bot1 == null || bot2 == null) { + JOptionPane.showMessageDialog(this, "Erreur lors de la création des bots."); + return; + } + + // Vider le tableau + tableModel.setRowCount(0); + results.clear(); + + // Charger le plateau initial + Tower[][] initialGrid = BoardLoader.loadFromFile("fr/iut_fbleau/Res/Plateau.txt"); + + // Lancer les parties + for (int i = 1; i <= nbParties; i++) { + AvalamBoard board = new AvalamBoard(initialGrid, Player.PLAYER1); + ArenaGame game = new ArenaGame(board, bot1, bot2); + + try { + Result result = game.run(); + String winner = getWinnerName(result, bot1Type, bot2Type); + + // Ajouter au tableau + tableModel.addRow(new Object[]{ + "Partie " + i, + bot1Type, + bot2Type, + winner + }); + } catch (Exception e) { + tableModel.addRow(new Object[]{ + "Partie " + i, + bot1Type, + bot2Type, + "Erreur: " + e.getMessage() + }); + } + } + + // Afficher un message de fin + JOptionPane.showMessageDialog( + this, + "Toutes les parties sont terminées !", + "Arène terminée", + JOptionPane.INFORMATION_MESSAGE + ); + } + + /** + * Crée un bot selon son type. + * + * @param botType type de bot ("Bot Idiot", "Bot Alpha-Beta", "Bot Divin") + * @param player joueur contrôlé par ce bot (PLAYER1 ou PLAYER2) + * @param depth profondeur de recherche pour Alpha-Beta/Divin + * @return une instance de AbstractGamePlayer correspondant au type, ou null si le type est invalide + */ + private AbstractGamePlayer createBot(String botType, Player player, int depth) { + if (botType.equals("Bot Idiot")) { + return new IdiotBot(player); + } else if (botType.equals("Bot Alpha-Beta")) { + return new AlphaBetaBot(player, depth); + } else if (botType.equals("Bot Divin")) { + // Pour l'instant, le Bot Divin n'est pas implémenté, on utilise IdiotBot + JOptionPane.showMessageDialog( + this, + "Le Bot Divin n'est pas encore implémenté. Utilisation du Bot Idiot à la place.", + "Avertissement", + JOptionPane.WARNING_MESSAGE + ); + return new IdiotBot(player); + } + return null; + } + + /** + * Détermine le nom du gagnant selon le résultat. + * + * @param result résultat de la partie (WIN, LOSS, ou DRAW du point de vue de PLAYER1) + * @param bot1Type nom du bot 1 + * @param bot2Type nom du bot 2 + * @return une chaîne indiquant le gagnant ou "Match nul" + */ + private String getWinnerName(Result result, String bot1Type, String bot2Type) { + if (result == Result.WIN) { + return bot1Type + " (Joueur 1)"; + } else if (result == Result.LOSS) { + return bot2Type + " (Joueur 2)"; + } else { + return "Match nul"; + } + } +} + diff --git a/fr/iut_fbleau/Avalam/AvalamWindow.java b/fr/iut_fbleau/Avalam/AvalamWindow.java index ddec50e..1497478 100644 --- a/fr/iut_fbleau/Avalam/AvalamWindow.java +++ b/fr/iut_fbleau/Avalam/AvalamWindow.java @@ -2,7 +2,7 @@ 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.DivineBot; import fr.iut_fbleau.Bot.IdiotBot; import fr.iut_fbleau.GameAPI.AbstractPly; import fr.iut_fbleau.GameAPI.Player; @@ -59,6 +59,8 @@ public class AvalamWindow extends JFrame { /** Bot Alpha-Beta (utilisé si mode PVALPHA). */ private final AlphaBetaBot alphaBot; + private final DivineBot divineBot; + // A FAIRE PLUS TARD (PVGOD) // /** Bot Divin (utilisé si mode PVGOD). */ // private final DivineBot divineBot; @@ -68,7 +70,6 @@ public class AvalamWindow extends JFrame { * 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; @@ -110,7 +111,7 @@ public class AvalamWindow extends JFrame { 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; + this.divineBot = (mode == GameMode.PVGOD) ? new DivineBot(botPlayer, depth) : null; setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLayout(new BorderLayout()); @@ -220,11 +221,10 @@ public class AvalamWindow extends JFrame { if (mode == GameMode.PVALPHA && alphaBot == null) return; // A FAIRE PLUS TARD (PVGOD) - // if (mode == GameMode.PVGOD && divineBot == null) return; + 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; @@ -239,8 +239,7 @@ public class AvalamWindow extends JFrame { botMove = alphaBot.giveYourMove(board.safeCopy()); } else { // A FAIRE PLUS TARD (PVGOD) - // botMove = divineBot.giveYourMove(board.safeCopy()); - botMove = null; + botMove = divineBot.giveYourMove(board.safeCopy()); } if (botMove == null) { diff --git a/fr/iut_fbleau/Avalam/GameMode.java b/fr/iut_fbleau/Avalam/GameMode.java index 09c306a..b5a83d9 100644 --- a/fr/iut_fbleau/Avalam/GameMode.java +++ b/fr/iut_fbleau/Avalam/GameMode.java @@ -7,5 +7,6 @@ public enum GameMode { PVP, // joueur vs joueur PVBOT, // joueur vs bot idiot PVALPHA, // joueur vs bot alpha - PVGOD // joueur vs bot stratégique + PVGOD, // joueur vs bot stratégique + ARENA // bot vs bot (mode arène) } diff --git a/fr/iut_fbleau/Avalam/Main.java b/fr/iut_fbleau/Avalam/Main.java index 6cd72de..598e445 100644 --- a/fr/iut_fbleau/Avalam/Main.java +++ b/fr/iut_fbleau/Avalam/Main.java @@ -8,62 +8,75 @@ import javax.swing.*; public class Main { public static void main(String[] args) { - SwingUtilities.invokeLater(() -> { + showModeSelection(); + }); + } - String[] options = { - "joueur vs joueur", - "joueur vs botidiot", - "joueur vs bot alpha", - "joueur vs bot divin (NON IMPLEMENTE)" - }; + /** + * Affiche le menu de sélection du mode de jeu. + * Peut être appelé depuis d'autres fenêtres pour revenir au menu. + */ + public static void showModeSelection() { + String[] options = { + "joueur vs joueur", + "joueur vs botidiot", + "joueur vs bot alpha", + "joueur vs bot divin (NON IMPLEMENTE)", + "Arène" + }; - int choice = JOptionPane.showOptionDialog( + int choice = JOptionPane.showOptionDialog( + null, + "Choisissez un mode de jeu :", + "Avalam - Mode de jeu", + JOptionPane.DEFAULT_OPTION, + JOptionPane.QUESTION_MESSAGE, + null, + options, + options[0] + ); + + if (choice == -1) System.exit(0); + + // Mode Arène + if (choice == 4) { + new ArenaWindow(); + return; + } + + 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; + + // Pour ALPHA et GOD : demander une profondeur + if (mode == GameMode.PVALPHA || mode == GameMode.PVGOD) { + + String s = JOptionPane.showInputDialog( null, - "Choisissez un mode de jeu :", - "Avalam - Mode de jeu", - JOptionPane.DEFAULT_OPTION, - JOptionPane.QUESTION_MESSAGE, - null, - options, - options[0] + "Profondeur de recherche ?\n(Conseil 4)", + (mode == GameMode.PVGOD ? "Bot Divin (PVGOD)" : "Bot Alpha-Beta"), + JOptionPane.QUESTION_MESSAGE ); - 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; - - // Pour ALPHA et GOD : demander une profondeur - if (mode == GameMode.PVALPHA || mode == GameMode.PVGOD) { - - String s = JOptionPane.showInputDialog( - null, - "Profondeur de recherche ?\n(Conseil 4)", - (mode == GameMode.PVGOD ? "Bot Divin (PVGOD)" : "Bot Alpha-Beta"), - JOptionPane.QUESTION_MESSAGE - ); - - 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); + 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; } - new AvalamWindow(mode); - }); + if (depth < 1) depth = 1; + + new AvalamWindow(mode, depth); + return; + } + + new AvalamWindow(mode); } } diff --git a/fr/iut_fbleau/Bot/DivineBot.java b/fr/iut_fbleau/Bot/DivineBot.java index cbc6fec..0361e98 100644 --- a/fr/iut_fbleau/Bot/DivineBot.java +++ b/fr/iut_fbleau/Bot/DivineBot.java @@ -1,22 +1,185 @@ 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 fr.iut_fbleau.Avalam.*; +import fr.iut_fbleau.GameAPI.*; import java.util.*; /** - * Bot "Divin" (fort) pour Avalam. - * - * - * Objectif : trop fort. */ -public class DivineBot{ + * Bot "Divin" (alpha-beta + évaluateur pondéré). + * * Idée : + * - Utilise l'algorithme Alpha-Beta pour anticiper les coups. + * - Évalue les plateaux non terminaux en accordant plus d'importance aux tours hautes. + */ +public class DivineBot extends AbstractGamePlayer { -} + // Attributs + + /** Joueur contrôlé par ce bot (PLAYER1 ou PLAYER2). */ + private final Player me; + + /** Profondeur maximale de recherche avant évaluation. */ + private final int maxDepth; + + /** Générateur aléatoire pour choisir parmi les meilleurs coups équivalents. */ + private final Random rng = new Random(); + + // Constructeur + + /** + * Construit le bot Divine. + * + * @param p joueur contrôlé par ce bot + * @param maxDepth profondeur de l'arbre de recherche + */ + public DivineBot(Player p, int maxDepth) { + super(p); + this.me = p; + this.maxDepth = Math.max(1, maxDepth); + } + + // Méthodes + + /** + * Méthode principale de décision du bot. + * Explore le premier niveau de l'arbre et lance les appels Alpha-Beta. + * * @param board état actuel du jeu + * @return le meilleur coup calculé (AbstractPly) + */ + @Override + public AbstractPly giveYourMove(IBoard board) { + + 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); + + // Appel récursif pour évaluer la suite du coup + 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); + } + } + + // Retourne un coup au hasard parmi les meilleurs ex-aequo + return bestMoves.get(rng.nextInt(bestMoves.size())); + } + + /** + * Algorithme récursif de recherche avec élagage Alpha-Beta. + */ + private int alphaBeta(IBoard board, int depth, int alpha, int beta) { + + // Cas de base : fin de partie ou limite de profondeur atteinte + if (board.isGameOver()) return terminalValue(board); + if (depth == 0) return evaluate(board); + + boolean isMax = board.getCurrentPlayer() == me; + + for (AbstractPly m : listMoves(board)) { + IBoard next = board.safeCopy(); + next.doPly(m); + + int val = alphaBeta(next, depth - 1, alpha, beta); + + if (isMax) { + alpha = Math.max(alpha, val); + if (alpha >= beta) break; // Coupure Beta + } else { + beta = Math.min(beta, val); + if (alpha >= beta) break; // Coupure Alpha + } + } + + return isMax ? alpha : beta; + } + + /** + * Calcule la valeur de l'état final (Victoire / Défaite). + */ + private int terminalValue(IBoard board) { + Result r = board.getResult(); + if (r == null) return 0; + + if (r == Result.DRAW) return 0; + + boolean botIsP1 = (me == Player.PLAYER1); + // Si le bot gagne, valeur positive élevée, sinon valeur négative + return ((r == Result.WIN) == botIsP1) ? 100000 : -100000; + } + + /** + * Heuristique évoluée pour Avalam : + * Calcule un score basé sur le contrôle des tours et leur hauteur. + * Les tours de hauteur 5 sont prioritaires car elles sont bloquées. + */ + private int evaluate(IBoard board) { + + if (!(board instanceof AvalamBoard)) return 0; + AvalamBoard b = (AvalamBoard) board; + + Color myColor = (me == Player.PLAYER1) ? Color.YELLOW : Color.RED; + Color oppColor = (myColor == Color.YELLOW) ? Color.RED : Color.YELLOW; + + int score = 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; + + int h = t.getHeight(); + + // Pondération selon la hauteur (heuristique "Divine") + int value = + (h == 5) ? 1000 : + (h == 4) ? 300 : + (h == 3) ? 120 : + (h == 2) ? 40 : 10; + + if (t.getColor() == myColor) score += value; + else score -= value; + } + } + return score; + } + + /** + * Génère la liste de tous les coups possibles sur le plateau donné. + */ + private List listMoves(IBoard board) { + List moves = new ArrayList<>(); + board.iterator().forEachRemaining(moves::add); + return moves; + } +} \ No newline at end of file