Compare commits
24 Commits
kara-mosr-
...
Arbre
| Author | SHA1 | Date | |
|---|---|---|---|
| e8f083424b | |||
| 942462994b | |||
| 55f6492655 | |||
| fafebb2648 | |||
|
|
fe13b946b1 | ||
|
|
c278b18872 | ||
|
|
e0a2c2642a | ||
|
|
c9e559fe12 | ||
|
|
98c6b4678e | ||
| 05871232bd | |||
| 912675f897 | |||
| 0c8f3b0dd6 | |||
| fc6e6b9fa6 | |||
| 8cad839e4d | |||
| d2f34577e2 | |||
| ebfc2df29b | |||
| 9a1ae37130 | |||
| 9e843fe646 | |||
| fa96aae6e6 | |||
| 2dfc6014e0 | |||
| 3aec1d3f6e | |||
| a7d3e9d138 | |||
| f207da0e2b | |||
| 22891ae2b6 |
187
README.md
187
README.md
@@ -1,171 +1,56 @@
|
|||||||
# Instructions de Travail sur les Tickets
|
# BUT3 – Projet Jeu : Hex
|
||||||
|
|
||||||
Ce document présente la procédure à suivre lors de la création et de la gestion des tickets de développement. Veuillez suivre chaque étape avec attention.
|
|
||||||
## 1. Création du Ticket
|
|
||||||
|
|
||||||
### Titre du Ticket
|
|
||||||
|
|
||||||
Le titre doit décrire de manière générale la tâche à réaliser. Soyez précis, mais sans entrer dans les détails techniques. Par exemple :
|
|
||||||
|
|
||||||
Ajout d'une nouvelle fonctionnalité de recherche dans l'application
|
|
||||||
|
|
||||||
Correction du bug d'affichage sur la page d'accueil
|
|
||||||
|
|
||||||
### Description du Ticket
|
|
||||||
|
|
||||||
La description doit fournir une explication légèrement détaillée des tâches à réaliser. Elle doit inclure les éléments suivants :
|
|
||||||
|
|
||||||
Objectif global de la tâche
|
|
||||||
|
|
||||||
Étapes spécifiques ou parties du projet concernées
|
|
||||||
|
|
||||||
Comportement attendu une fois la tâche accomplie
|
|
||||||
|
|
||||||
|
|
||||||
## 2. Création de la Branche
|
Il s’agit d’une implémentation du jeu **Hex** en Java, développée à partir de l’API fournie par le Monsieur Madelaine.
|
||||||
|
Le projet comprend un moteur de jeu fonctionnel, un affichage console pour le debug, ainsi que des bots permettant de jouer automatiquement.
|
||||||
|
|
||||||
Lorsque vous commencez à travailler sur un ticket, créez une nouvelle branche avec un nom particulier qui reflète le ticket en cours. Le format de la branche doit être :
|
|
||||||
|
|
||||||
nom-de-la-feature-#numeroduticket
|
## Compilation
|
||||||
|
|
||||||
### Pour créer une branche :
|
Depuis la racine du projet, compiler l’ensemble des fichiers Java avec la commande suivante :
|
||||||
|
|
||||||
git checkout -b feature-recherche-#123
|
```bash
|
||||||
|
javac -d build $(find javaAPI -name "*.java")
|
||||||
## 3. Commit des Changements
|
|
||||||
|
|
||||||
Les commits doivent suivre la convention suivante :
|
|
||||||
|
|
||||||
- Le message de commit doit décrire brièvement le changement effectué.
|
|
||||||
|
|
||||||
- À la fin du message de commit, vous devez toujours ajouter le numéro du ticket pour faciliter le suivi des tâches.
|
|
||||||
|
|
||||||
Exemple de message de commit :
|
|
||||||
|
|
||||||
Ajout du champ de recherche sur la page d'accueil #123
|
|
||||||
|
|
||||||
## 4. Push de la Branche
|
|
||||||
|
|
||||||
Après avoir effectué vos changements et effectué vos commits, vous devrez pousser la branche sur le dépôt distant. Lors de votre premier git push, vous recevrez un message pour définir l'upstream de la branche.
|
|
||||||
|
|
||||||
Exemple de message affiché :
|
|
||||||
|
|
||||||
```
|
|
||||||
fatal: The upstream branch 'origin/feature-recherche-#123' does not exist
|
|
||||||
To push the branch and set the upstream, use the following command:
|
|
||||||
git push --set-upstream origin nom-de-la-feature-#numero
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Les fichiers compilés (`.class`) sont générés dans le dossier `bin`.
|
||||||
Vous devez copier et coller la commande dans votre terminal pour effectuer le push. Une fois cette commande exécutée, votre branche sera poussée vers le dépôt distant.
|
|
||||||
|
|
||||||
## 5. Création d'une Pull Request (PR)
|
|
||||||
|
|
||||||
Une fois que vous avez poussé votre branche sur Gitea, vous devez ouvrir une pull request pour demander la révision de votre code.
|
|
||||||
|
|
||||||
Voici les étapes pour créer une pull request correctement :
|
|
||||||
|
|
||||||
- Allez sur Gitea et naviguez vers le projet concerné.
|
|
||||||
|
|
||||||
- Cliquez sur "Branches" et vous devriez voir la branche que vous venez de pousser.
|
|
||||||
|
|
||||||
- Cliquez sur le bouton "Create Pull Request" à côté de votre branche.
|
|
||||||
|
|
||||||
Remplissez les informations nécessaires :
|
|
||||||
|
|
||||||
- Titre de la PR : Utilisez le même titre que celui du ticket.
|
|
||||||
|
|
||||||
- Description de la PR : Décrivez brièvement ce que votre PR accomplit. Vous pouvez vous baser sur la description du ticket.
|
|
||||||
|
|
||||||
- Revues : Assurez-vous de demander une révision par deux membres de l’équipe.
|
|
||||||
|
|
||||||
- Cliquez sur "Create Pull Request" pour soumettre.
|
|
||||||
|
|
||||||
Une fois la PR ouverte, vous devrez attendre la révision et l’approbation de l’équipe avant de pouvoir fusionner la branche dans main ou develop selon le flux de travail de votre projet.
|
|
||||||
|
|
||||||
|
|
||||||
# Résumé des Commandes Git :
|
## Lancer une démonstration
|
||||||
|
|
||||||
Voici un récapitulatif des commandes Git que vous utiliserez fréquemment :
|
### Partie automatique (bot)
|
||||||
|
|
||||||
## 1. Créer une branche
|
```bash
|
||||||
|
java -cp build fr.iut_fbleau.HexGame.HexMain 3 autoplay
|
||||||
|
```
|
||||||
|
|
||||||
git checkout -b feature-recherche-#123
|
Ce mode permet de lancer une partie entièrement automatique en utilisant le bot implémenté dans la classe `Simulation`.
|
||||||
|
|
||||||
|
### Partie interactive (joueur humain)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -cp build fr.iut_fbleau.HexGame.HexMain
|
||||||
|
```
|
||||||
|
|
||||||
|
Le plateau s’affiche dans le terminal et les coups sont entrés sous forme de coordonnées.
|
||||||
|
|
||||||
|
|
||||||
## 2. Ajouter les fichiers modifiés :
|
## Tests et validation
|
||||||
|
|
||||||
git add .
|
Les tests sont réalisés sous forme de **tests fonctionnels** via des méthodes `main` et des modes de démonstration :
|
||||||
git add *
|
- vérification de la validité des coups,
|
||||||
git add <nom_du_fichier>
|
- alternance correcte des joueurs,
|
||||||
|
- détection des conditions de fin de partie,
|
||||||
|
- exécution de parties complètes en mode automatique.
|
||||||
|
|
||||||
|
L’affichage console du plateau, fourni par la méthode `HexBoard.toString()`, est utilisé comme outil de debug pour visualiser l’état du jeu à chaque tour.
|
||||||
|
|
||||||
|
|
||||||
## 3. Commit des changements :
|
## Organisation du projet
|
||||||
|
|
||||||
git commit -m "Ajout de [...] #numeroticket"
|
- `HexBoard` : représentation du plateau et gestion des règles du jeu
|
||||||
|
- `HexPly` : représentation d’un coup
|
||||||
|
- `Simulation` : bot basé sur une recherche Minimax à profondeur limitée
|
||||||
|
- `HexMain` : point d’entrée du programme
|
||||||
|
|
||||||
|
Les classes principales sont documentées à l’aide de **Javadoc**.
|
||||||
## 4. Pousser la branche
|
|
||||||
|
|
||||||
git push -set-upstream origin <nom-de-la-branche-#numeroticket>
|
|
||||||
|
|
||||||
|
|
||||||
## 5. Supprimer une branche
|
|
||||||
|
|
||||||
git branch -d <nom_de_la_branche>
|
|
||||||
|
|
||||||
|
|
||||||
## Comment fonctionne l’algorithme de victoire (idée générale)
|
|
||||||
|
|
||||||
Dans Hex, un joueur gagne s’il existe un chemin continu de ses pions connectant ses deux bords :
|
|
||||||
|
|
||||||
PLAYER1 : relier gauche → droite
|
|
||||||
|
|
||||||
PLAYER2 : relier haut → bas
|
|
||||||
|
|
||||||
Un “chemin” = une suite de cases adjacentes sur la grille hexagonale (6 voisins possibles) appartenant au joueur.
|
|
||||||
|
|
||||||
Ce que fait l’algo (principe)
|
|
||||||
|
|
||||||
L’algo fait un parcours de graphe (DFS avec une pile, ou BFS avec une file c’est pareil pour le résultat) :
|
|
||||||
|
|
||||||
On prend toutes les cases du bord de départ du joueur (ex: bord gauche pour PLAYER1).
|
|
||||||
|
|
||||||
On ne garde que celles qui contiennent un pion du joueur.
|
|
||||||
|
|
||||||
À partir de ces cases, on explore toutes les cases voisines contenant aussi un pion du joueur, et ainsi de suite.
|
|
||||||
|
|
||||||
Si pendant l’exploration on atteint l’autre bord, alors il existe un chemin → victoire.
|
|
||||||
|
|
||||||
Pourquoi ça marche ?
|
|
||||||
|
|
||||||
Parce que ça revient à demander :
|
|
||||||
|
|
||||||
“Est-ce qu’il existe une composante connexe de pions du joueur qui touche les deux bords ?”
|
|
||||||
|
|
||||||
Le DFS/BFS explore exactement la composante connexe.
|
|
||||||
|
|
||||||
Les 6 voisins en Hex (grille hexagonale)
|
|
||||||
|
|
||||||
Dans ton code, tu as :
|
|
||||||
|
|
||||||
private static final int[][] NEIGHBORS = {
|
|
||||||
{-1, 0}, {+1, 0},
|
|
||||||
{ 0, -1}, { 0, +1},
|
|
||||||
{-1, +1}, {+1, -1}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
Ça signifie qu’une case (r,c) a jusqu’à 6 voisins :
|
|
||||||
'''
|
|
||||||
(r-1,c), (r+1,c) : “haut/bas”
|
|
||||||
|
|
||||||
(r,c-1), (r,c+1) : “gauche/droite”
|
|
||||||
|
|
||||||
(r-1,c+1) et (r+1,c-1) : les 2 diagonales propres au pavage hexagonal
|
|
||||||
'''
|
|
||||||
Complexité
|
|
||||||
|
|
||||||
Au pire, on visite chaque case une seule fois → O(N²) pour un plateau N×N.
|
|
||||||
|
|
||||||
Très correct pour Hex.
|
|
||||||
9784
Rapport Hex.pdf
Normal file
9784
Rapport Hex.pdf
Normal file
File diff suppressed because it is too large
Load Diff
1
javaAPI/.~lock.arena_results.csv#
Normal file
1
javaAPI/.~lock.arena_results.csv#
Normal file
@@ -0,0 +1 @@
|
|||||||
|
,vaisse,salle235-12,06.02.2026 11:29,file:///export/home/an23/vaisse/.config/libreoffice/4;
|
||||||
@@ -34,8 +34,8 @@ public abstract class AbstractGame {
|
|||||||
|
|
||||||
// constructeur à appeler dans le constructeur d'un fils concret avec super.
|
// constructeur à appeler dans le constructeur d'un fils concret avec super.
|
||||||
public AbstractGame(IBoard b, EnumMap<Player,AbstractGamePlayer> m){
|
public AbstractGame(IBoard b, EnumMap<Player,AbstractGamePlayer> m){
|
||||||
this.currentBoard=b;
|
this.currentBoard=b;
|
||||||
this.mapPlayers=m;
|
this.mapPlayers=m;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -26,5 +26,6 @@ public abstract class AbstractGamePlayer {
|
|||||||
* @throws IllegalStateException if the Situation is already in the bookmarks
|
* @throws IllegalStateException if the Situation is already in the bookmarks
|
||||||
*/
|
*/
|
||||||
public abstract AbstractPly giveYourMove(IBoard p);
|
public abstract AbstractPly giveYourMove(IBoard p);
|
||||||
|
public abstract Boolean jesuisMinimax();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
64
javaAPI/fr/iut_fbleau/HexGame/Arena.java
Normal file
64
javaAPI/fr/iut_fbleau/HexGame/Arena.java
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package fr.iut_fbleau.HexGame;
|
||||||
|
|
||||||
|
import fr.iut_fbleau.GameAPI.*;
|
||||||
|
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class Arena {
|
||||||
|
|
||||||
|
private List<AbstractGamePlayer> bots = new ArrayList<>();
|
||||||
|
private FileWriter csvWriter;
|
||||||
|
private int board_size;
|
||||||
|
|
||||||
|
public Arena(int size) {
|
||||||
|
try {
|
||||||
|
csvWriter = new FileWriter("arena_results.csv");
|
||||||
|
csvWriter.append("Bot 1, Bot 2, Winner\n");
|
||||||
|
this.board_size = size;
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addBot(AbstractGamePlayer bot) {
|
||||||
|
bots.add(bot);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run() {
|
||||||
|
for (int i = 0; i < bots.size(); i++) {
|
||||||
|
for (int j = i + 1; j < bots.size(); j++) {
|
||||||
|
AbstractGamePlayer bot1 = bots.get(i);
|
||||||
|
AbstractGamePlayer bot2 = bots.get(j);
|
||||||
|
|
||||||
|
System.out.println("Running match: " + bot1.getClass().getSimpleName() + " vs " + bot2.getClass().getSimpleName());
|
||||||
|
Result result = playMatch(bot1, bot2);
|
||||||
|
|
||||||
|
try {
|
||||||
|
csvWriter.append(bot1.getClass().getSimpleName() + "," + bot2.getClass().getSimpleName() + "," + result + "\n");
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
csvWriter.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result playMatch(AbstractGamePlayer bot1, AbstractGamePlayer bot2) {
|
||||||
|
IBoard board = new HexBoard(this.board_size);
|
||||||
|
EnumMap<Player, AbstractGamePlayer> players = new EnumMap<>(Player.class);
|
||||||
|
players.put(Player.PLAYER1, bot1);
|
||||||
|
players.put(Player.PLAYER2, bot2);
|
||||||
|
|
||||||
|
Simulation simulation = new Simulation(board, players);
|
||||||
|
return simulation.runForArena();
|
||||||
|
}
|
||||||
|
}
|
||||||
19
javaAPI/fr/iut_fbleau/HexGame/ArenaMain.java
Normal file
19
javaAPI/fr/iut_fbleau/HexGame/ArenaMain.java
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package fr.iut_fbleau.HexGame;
|
||||||
|
|
||||||
|
import fr.iut_fbleau.GameAPI.Player;
|
||||||
|
|
||||||
|
public class ArenaMain {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
int size = 7;
|
||||||
|
if (args.length >= 1) {
|
||||||
|
try { size = Integer.parseInt(args[0]); } catch (NumberFormatException ignored) {}
|
||||||
|
}
|
||||||
|
Arena arena = new Arena(size);
|
||||||
|
arena.addBot(new RandomBot(Player.PLAYER1, 24015L)); // Correct constructor usage
|
||||||
|
arena.addBot(new MiniMaxBot(Player.PLAYER2));
|
||||||
|
arena.addBot(new HeuristicBot(Player.PLAYER1));
|
||||||
|
arena.addBot(new MonteCarloBot(Player.PLAYER2)); // Correct constructor usage
|
||||||
|
|
||||||
|
arena.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
53
javaAPI/fr/iut_fbleau/HexGame/HeuristicBot.java
Normal file
53
javaAPI/fr/iut_fbleau/HexGame/HeuristicBot.java
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package fr.iut_fbleau.HexGame;
|
||||||
|
|
||||||
|
import fr.iut_fbleau.GameAPI.*;
|
||||||
|
|
||||||
|
public class HeuristicBot extends AbstractGamePlayer {
|
||||||
|
|
||||||
|
public HeuristicBot(Player me) {
|
||||||
|
super(me); // Correct constructor usage
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean jesuisMinimax(){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AbstractPly giveYourMove(IBoard board) {
|
||||||
|
HexBoard hb = (HexBoard) board;
|
||||||
|
float bestScore = -Float.MAX_VALUE;
|
||||||
|
HexPly bestMove = null;
|
||||||
|
|
||||||
|
for (int i = 0; i < hb.getSize(); i++) {
|
||||||
|
for (int j = 0; j < hb.getSize(); j++) {
|
||||||
|
HexPly move = new HexPly(hb.getCurrentPlayer(), i, j);
|
||||||
|
if (hb.isLegal(move)) {
|
||||||
|
hb.doPly(move);
|
||||||
|
float score = evaluateBoard(hb);
|
||||||
|
if (score > bestScore) {
|
||||||
|
bestScore = score;
|
||||||
|
bestMove = move;
|
||||||
|
}
|
||||||
|
hb.undoPly();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestMove;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float evaluateBoard(HexBoard board) {
|
||||||
|
int size = board.getSize();
|
||||||
|
int center = size / 2;
|
||||||
|
float score = 0;
|
||||||
|
//HexBoard simBoard = (HexBoard) board.safeCopy();
|
||||||
|
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
for (int j = 0; j < size; j++) {
|
||||||
|
if (board.getCellPlayer(i, j) == Player.PLAYER1) {
|
||||||
|
score += Math.abs(i - center) + Math.abs(j - center); // Distance from center
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,63 +1,154 @@
|
|||||||
package fr.iut_fbleau.HexGame;
|
package fr.iut_fbleau.HexGame;
|
||||||
|
|
||||||
import fr.iut_fbleau.GameAPI.*;
|
import fr.iut_fbleau.GameAPI.*;
|
||||||
import java.util.*;
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plateau du jeu de Hex.
|
* Représente le plateau du jeu de Hex.
|
||||||
*
|
*
|
||||||
* Joueur 1 relie la gauche et la droite.
|
* <h2>Rappel des conditions de victoire</h2>
|
||||||
* Joueur 2 relie le haut et le bas.
|
* <ul>
|
||||||
|
* <li>{@link Player#PLAYER1} gagne s'il existe un chemin de pions connectés
|
||||||
|
* reliant le bord gauche au bord droit.</li>
|
||||||
|
* <li>{@link Player#PLAYER2} gagne s'il existe un chemin de pions connectés
|
||||||
|
* reliant le bord haut au bord bas.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Idée de l'algorithme de détection de victoire</h2>
|
||||||
|
* On modélise le plateau comme un graphe :
|
||||||
|
* <ul>
|
||||||
|
* <li>Chaque case est un sommet</li>
|
||||||
|
* <li>Deux cases sont connectées si elles sont voisines sur la grille hexagonale (6 voisins)</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* Pour tester la victoire d'un joueur, on lance un parcours (DFS/BFS) :
|
||||||
|
* <ol>
|
||||||
|
* <li>On part de toutes les cases du bord de départ qui contiennent un pion du joueur.</li>
|
||||||
|
* <li>On explore tous les pions du joueur connectés à ces cases.</li>
|
||||||
|
* <li>Si on atteint le bord opposé, il existe un chemin : le joueur gagne.</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* Complexité : O(N²) au pire (on visite chaque case au plus une fois).
|
||||||
*/
|
*/
|
||||||
public class HexBoard extends AbstractBoard {
|
public class HexBoard extends AbstractBoard {
|
||||||
|
|
||||||
|
/** Taille du plateau : size x size. */
|
||||||
private final int size;
|
private final int size;
|
||||||
private Player[][] cells;
|
|
||||||
private Deque<AbstractPly> historyLocal;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grille des cases.
|
||||||
|
* Une case vaut :
|
||||||
|
* <ul>
|
||||||
|
* <li>null : case vide</li>
|
||||||
|
* <li>PLAYER1 : pion du joueur 1</li>
|
||||||
|
* <li>PLAYER2 : pion du joueur 2</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
private final Player[][] cells;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offsets des 6 voisins d'une case dans une grille hexagonale.
|
||||||
|
*
|
||||||
|
* Pour une case (r,c), les voisins potentiels sont :
|
||||||
|
* (r-1,c), (r+1,c), (r,c-1), (r,c+1), (r-1,c+1), (r+1,c-1).
|
||||||
|
*/
|
||||||
private static final int[][] NEIGHBORS = {
|
private static final int[][] NEIGHBORS = {
|
||||||
{-1, 0}, {+1, 0},
|
{-1, 0}, {+1, 0},
|
||||||
{ 0, -1}, { 0, +1},
|
{ 0, -1}, { 0, +1},
|
||||||
{-1, +1}, {+1, -1}
|
{-1, +1}, {+1, -1}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Crée un plateau vide avec {@link Player#PLAYER1} qui commence. */
|
||||||
public HexBoard(int size) {
|
public HexBoard(int size) {
|
||||||
super();
|
this(size, Player.PLAYER1);
|
||||||
this.size = size;
|
|
||||||
this.cells = new Player[size][size];
|
|
||||||
this.historyLocal = new ArrayDeque<>();
|
|
||||||
this.currentPlayer = Player.PLAYER1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructeur interne, utile pour {@link #safeCopy()}.
|
||||||
|
* @param size taille du plateau
|
||||||
|
* @param current joueur courant
|
||||||
|
*/
|
||||||
|
private HexBoard(int size, Player current) {
|
||||||
|
super(current, new ArrayDeque<>());
|
||||||
|
if (size <= 0) throw new IllegalArgumentException("size must be > 0");
|
||||||
|
this.size = size;
|
||||||
|
this.cells = new Player[size][size];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return la taille du plateau. */
|
||||||
|
public int getSize() {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si (r,c) est dans le plateau.
|
||||||
|
* @param r ligne (0..size-1)
|
||||||
|
* @param c colonne (0..size-1)
|
||||||
|
*/
|
||||||
private boolean inBounds(int r, int c) {
|
private boolean inBounds(int r, int c) {
|
||||||
return r >= 0 && r < size && c >= 0 && c < size;
|
return r >= 0 && r < size && c >= 0 && c < size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return le contenu d'une case (null si vide). */
|
||||||
private Player getCell(int r, int c) {
|
private Player getCell(int r, int c) {
|
||||||
return cells[r][c];
|
return cells[r][c];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Modifie une case (utilisé par doPly/undoPly). */
|
||||||
private void setCell(int r, int c, Player p) {
|
private void setCell(int r, int c, Player p) {
|
||||||
cells[r][c] = p;
|
cells[r][c] = p;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Teste la victoire de PLAYER1 (gauche -> droite).
|
||||||
|
*
|
||||||
|
* <h3>Détails de l'algorithme</h3>
|
||||||
|
* <ol>
|
||||||
|
* <li>On initialise une structure "visited" pour ne pas revisiter les cases.</li>
|
||||||
|
* <li>On met dans une pile toutes les cases du bord gauche (colonne 0)
|
||||||
|
* qui contiennent un pion PLAYER1.</li>
|
||||||
|
* <li>On effectue un DFS :
|
||||||
|
* <ul>
|
||||||
|
* <li>on dépile une case</li>
|
||||||
|
* <li>si elle est sur la colonne size-1 : on a touché le bord droit -> victoire</li>
|
||||||
|
* <li>sinon, on empile tous ses voisins qui sont des pions PLAYER1 et pas encore visités</li>
|
||||||
|
* </ul>
|
||||||
|
* </li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @return true si PLAYER1 a un chemin gauche->droite, false sinon
|
||||||
|
*/
|
||||||
private boolean hasPlayer1Won() {
|
private boolean hasPlayer1Won() {
|
||||||
boolean[][] visited = new boolean[size][size];
|
boolean[][] visited = new boolean[size][size];
|
||||||
Deque<int[]> stack = new ArrayDeque<>();
|
Deque<int[]> stack = new ArrayDeque<>();
|
||||||
|
|
||||||
|
// 1) points de départ : bord gauche
|
||||||
for (int r = 0; r < size; r++) {
|
for (int r = 0; r < size; r++) {
|
||||||
if (getCell(r, 0) == Player.PLAYER1) {
|
if (getCell(r, 0) == Player.PLAYER1) {
|
||||||
visited[r][0] = true;
|
visited[r][0] = true;
|
||||||
stack.push(new int[]{r, 0});
|
stack.push(new int[]{r, 0});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2) DFS
|
||||||
while (!stack.isEmpty()) {
|
while (!stack.isEmpty()) {
|
||||||
int[] cur = stack.pop();
|
int[] cur = stack.pop();
|
||||||
int cr = cur[0];
|
int cr = cur[0], cc = cur[1];
|
||||||
int cc = cur[1];
|
|
||||||
|
// condition d'arrivée : bord droit
|
||||||
if (cc == size - 1) return true;
|
if (cc == size - 1) return true;
|
||||||
|
|
||||||
|
// explore les 6 voisins
|
||||||
for (int[] d : NEIGHBORS) {
|
for (int[] d : NEIGHBORS) {
|
||||||
int nr = cr + d[0], nc = cc + d[1];
|
int nr = cr + d[0], nc = cc + d[1];
|
||||||
if (inBounds(nr, nc) && !visited[nr][nc] && getCell(nr, nc) == Player.PLAYER1) {
|
if (inBounds(nr, nc)
|
||||||
|
&& !visited[nr][nc]
|
||||||
|
&& getCell(nr, nc) == Player.PLAYER1) {
|
||||||
visited[nr][nc] = true;
|
visited[nr][nc] = true;
|
||||||
stack.push(new int[]{nr, nc});
|
stack.push(new int[]{nr, nc});
|
||||||
}
|
}
|
||||||
@@ -66,23 +157,42 @@ public class HexBoard extends AbstractBoard {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Teste la victoire de PLAYER2 (haut -> bas).
|
||||||
|
*
|
||||||
|
* Même principe que {@link #hasPlayer1Won()} mais :
|
||||||
|
* <ul>
|
||||||
|
* <li>Départ : bord haut (ligne 0)</li>
|
||||||
|
* <li>Arrivée : bord bas (ligne size-1)</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @return true si PLAYER2 a un chemin haut->bas, false sinon
|
||||||
|
*/
|
||||||
private boolean hasPlayer2Won() {
|
private boolean hasPlayer2Won() {
|
||||||
boolean[][] visited = new boolean[size][size];
|
boolean[][] visited = new boolean[size][size];
|
||||||
Deque<int[]> stack = new ArrayDeque<>();
|
Deque<int[]> stack = new ArrayDeque<>();
|
||||||
|
|
||||||
|
// points de départ : bord haut
|
||||||
for (int c = 0; c < size; c++) {
|
for (int c = 0; c < size; c++) {
|
||||||
if (getCell(0, c) == Player.PLAYER2) {
|
if (getCell(0, c) == Player.PLAYER2) {
|
||||||
visited[0][c] = true;
|
visited[0][c] = true;
|
||||||
stack.push(new int[]{0, c});
|
stack.push(new int[]{0, c});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DFS
|
||||||
while (!stack.isEmpty()) {
|
while (!stack.isEmpty()) {
|
||||||
int[] cur = stack.pop();
|
int[] cur = stack.pop();
|
||||||
int cr = cur[0];
|
int cr = cur[0], cc = cur[1];
|
||||||
int cc = cur[1];
|
|
||||||
|
// condition d'arrivée : bord bas
|
||||||
if (cr == size - 1) return true;
|
if (cr == size - 1) return true;
|
||||||
|
|
||||||
for (int[] d : NEIGHBORS) {
|
for (int[] d : NEIGHBORS) {
|
||||||
int nr = cr + d[0], nc = cc + d[1];
|
int nr = cr + d[0], nc = cc + d[1];
|
||||||
if (inBounds(nr, nc) && !visited[nr][nc] && getCell(nr, nc) == Player.PLAYER2) {
|
if (inBounds(nr, nc)
|
||||||
|
&& !visited[nr][nc]
|
||||||
|
&& getCell(nr, nc) == Player.PLAYER2) {
|
||||||
visited[nr][nc] = true;
|
visited[nr][nc] = true;
|
||||||
stack.push(new int[]{nr, nc});
|
stack.push(new int[]{nr, nc});
|
||||||
}
|
}
|
||||||
@@ -98,18 +208,50 @@ public class HexBoard extends AbstractBoard {
|
|||||||
int r = hp.getRow(), c = hp.getCol();
|
int r = hp.getRow(), c = hp.getCol();
|
||||||
return inBounds(r, c)
|
return inBounds(r, c)
|
||||||
&& getCell(r, c) == null
|
&& getCell(r, c) == null
|
||||||
&& hp.getPlayer() == this.getCurrentPlayer();
|
&& hp.getPlayer() == getCurrentPlayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Teste si un coup est immédiatement gagnant.
|
||||||
|
*
|
||||||
|
* On joue le coup, on teste la victoire, puis on annule le coup.
|
||||||
|
* Cela permet d'évaluer un coup sans modifier définitivement l'état du plateau.
|
||||||
|
*
|
||||||
|
* @param move coup à tester
|
||||||
|
* @return true si après ce coup le joueur a gagné, false sinon
|
||||||
|
*/
|
||||||
|
public boolean isWinningMove(AbstractPly move) {
|
||||||
|
if (!isLegal(move)) return false;
|
||||||
|
Player p = move.getPlayer();
|
||||||
|
|
||||||
|
doPly(move);
|
||||||
|
boolean winNow = (p == Player.PLAYER1) ? hasPlayer1Won() : hasPlayer2Won();
|
||||||
|
undoPly();
|
||||||
|
|
||||||
|
return winNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void doPly(AbstractPly move) {
|
public void doPly(AbstractPly move) {
|
||||||
if (!(move instanceof HexPly))
|
if (!(move instanceof HexPly)) {
|
||||||
throw new IllegalArgumentException("Coup invalide: " + move);
|
throw new IllegalArgumentException("Coup invalide: " + move);
|
||||||
|
}
|
||||||
|
if (!isLegal(move)) {
|
||||||
|
throw new IllegalStateException("Coup illégal: " + move);
|
||||||
|
}
|
||||||
|
|
||||||
HexPly hp = (HexPly) move;
|
HexPly hp = (HexPly) move;
|
||||||
if (!isLegal(hp))
|
|
||||||
throw new IllegalStateException("Coup illégal: " + hp);
|
|
||||||
setCell(hp.getRow(), hp.getCol(), hp.getPlayer());
|
setCell(hp.getRow(), hp.getCol(), hp.getPlayer());
|
||||||
historyLocal.push(hp);
|
addPlyToHistory(move);
|
||||||
|
setNextPlayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void undoPly() {
|
||||||
|
AbstractPly last = removePlyFromHistory();
|
||||||
|
HexPly hp = (HexPly) last;
|
||||||
|
|
||||||
|
setCell(hp.getRow(), hp.getCol(), null);
|
||||||
setNextPlayer();
|
setNextPlayer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,46 +262,32 @@ public class HexBoard extends AbstractBoard {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result getResult() {
|
public Result getResult() {
|
||||||
if (hasPlayer1Won()) return Result.WIN;
|
if (!isGameOver()) return null;
|
||||||
if (hasPlayer2Won()) return Result.LOSS;
|
if (hasPlayer1Won()) return Result.WIN; // du point de vue PLAYER1
|
||||||
return Result.DRAW;
|
return Result.LOSS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Iterator<AbstractPly> getPlies() {
|
public Iterator<AbstractPly> iterator() {
|
||||||
Player me = this.getCurrentPlayer();
|
Player me = getCurrentPlayer();
|
||||||
List<AbstractPly> moves = new ArrayList<>();
|
List<AbstractPly> moves = new ArrayList<>();
|
||||||
|
|
||||||
for (int r = 0; r < size; r++) {
|
for (int r = 0; r < size; r++) {
|
||||||
for (int c = 0; c < size; c++) {
|
for (int c = 0; c < size; c++) {
|
||||||
if (getCell(r, c) == null) moves.add(new HexPly(me, r, c));
|
if (getCell(r, c) == null) {
|
||||||
|
moves.add(new HexPly(me, r, c));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return moves.iterator();
|
return moves.iterator();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public Iterator<AbstractPly> getHistory() {
|
|
||||||
return historyLocal.iterator();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void undoLastPly() {
|
|
||||||
if (historyLocal.isEmpty()) return;
|
|
||||||
HexPly last = (HexPly) historyLocal.pop();
|
|
||||||
setCell(last.getRow(), last.getCol(), null);
|
|
||||||
this.currentPlayer = last.getPlayer();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IBoard safeCopy() {
|
public IBoard safeCopy() {
|
||||||
HexBoard copy = new HexBoard(this.size);
|
HexBoard copy = new HexBoard(this.size, this.getCurrentPlayer());
|
||||||
copy.currentPlayer = this.currentPlayer;
|
|
||||||
for (int r = 0; r < size; r++) {
|
for (int r = 0; r < size; r++) {
|
||||||
for (int c = 0; c < size; c++) {
|
System.arraycopy(this.cells[r], 0, copy.cells[r], 0, size);
|
||||||
copy.cells[r][c] = this.cells[r][c];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
copy.historyLocal = new ArrayDeque<>(this.historyLocal);
|
|
||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +309,9 @@ public class HexBoard extends AbstractBoard {
|
|||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getSize() {
|
|
||||||
return size;
|
public Player getCellPlayer(int r, int c) {
|
||||||
|
return cells[r][c];
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
52
javaAPI/fr/iut_fbleau/HexGame/HexFrame.java
Normal file
52
javaAPI/fr/iut_fbleau/HexGame/HexFrame.java
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package fr.iut_fbleau.HexGame;
|
||||||
|
|
||||||
|
import fr.iut_fbleau.GameAPI.Player;
|
||||||
|
import fr.iut_fbleau.GameAPI.Result;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.*;
|
||||||
|
|
||||||
|
public class HexFrame {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
int size = 11;
|
||||||
|
if (args.length >= 1) {
|
||||||
|
try { size = Integer.parseInt(args[0]); } catch (NumberFormatException ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
HexBoard board = new HexBoard(size);
|
||||||
|
|
||||||
|
JFrame frame = new JFrame("Hex - " + size + "x" + size);
|
||||||
|
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||||
|
frame.setLayout(new BorderLayout());
|
||||||
|
|
||||||
|
JLabel statusLabel = new JLabel("", SwingConstants.CENTER);
|
||||||
|
statusLabel.setFont(statusLabel.getFont().deriveFont(Font.BOLD, 18f));
|
||||||
|
statusLabel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
|
||||||
|
|
||||||
|
HexPanel panel = new HexPanel(board, statusLabel);
|
||||||
|
|
||||||
|
frame.add(statusLabel, BorderLayout.NORTH);
|
||||||
|
frame.add(panel, BorderLayout.CENTER);
|
||||||
|
|
||||||
|
// Taille confortable
|
||||||
|
frame.pack();
|
||||||
|
frame.setLocationRelativeTo(null);
|
||||||
|
frame.setVisible(true);
|
||||||
|
|
||||||
|
// Message initial
|
||||||
|
updateStatus(board, statusLabel);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static void updateStatus(HexBoard board, JLabel statusLabel) {
|
||||||
|
if (board.isGameOver()) {
|
||||||
|
Result r = board.getResult(); // résultat du point de vue PLAYER1
|
||||||
|
Player winner = (r == Result.WIN) ? Player.PLAYER1 : Player.PLAYER2;
|
||||||
|
statusLabel.setText("" + winner + " a gagné !");
|
||||||
|
} else {
|
||||||
|
statusLabel.setText("C'est à " + board.getCurrentPlayer() + " de jouer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
javaAPI/fr/iut_fbleau/HexGame/HexMain.java
Normal file
40
javaAPI/fr/iut_fbleau/HexGame/HexMain.java
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package fr.iut_fbleau.HexGame;
|
||||||
|
|
||||||
|
import fr.iut_fbleau.GameAPI.*;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.Scanner;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lancement d'une partie de Hex en console.
|
||||||
|
*/
|
||||||
|
public class HexMain {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
int size = 7;
|
||||||
|
if (args.length >= 1) {
|
||||||
|
try { size = Integer.parseInt(args[0]); } catch (NumberFormatException ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
HexBoard board = new HexBoard(size);
|
||||||
|
|
||||||
|
Scanner sc = new Scanner(System.in);
|
||||||
|
Result res;
|
||||||
|
EnumMap<Player, AbstractGamePlayer> players = new EnumMap<>(Player.class);
|
||||||
|
players.put(Player.PLAYER1, new HumanConsolePlayer(Player.PLAYER1, sc));
|
||||||
|
players.put(Player.PLAYER2, new HumanConsolePlayer(Player.PLAYER2, sc));
|
||||||
|
|
||||||
|
|
||||||
|
if (args.length>=2 && args[1].equals("autoplay")) {
|
||||||
|
Simulation sim = new Simulation(board, players);
|
||||||
|
res = sim.run();
|
||||||
|
} else {
|
||||||
|
AbstractGame game = new AbstractGame(board, players) {};
|
||||||
|
res = game.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println(board);
|
||||||
|
System.out.println("Résultat (du point de vue de PLAYER1) : " + res);
|
||||||
|
|
||||||
|
sc.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
166
javaAPI/fr/iut_fbleau/HexGame/HexPanel.java
Normal file
166
javaAPI/fr/iut_fbleau/HexGame/HexPanel.java
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
package fr.iut_fbleau.HexGame;
|
||||||
|
|
||||||
|
import fr.iut_fbleau.GameAPI.Player;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.event.MouseAdapter;
|
||||||
|
import java.awt.event.MouseEvent;
|
||||||
|
import java.awt.geom.Path2D;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Panel Swing qui dessine un plateau Hex en hexagones et gère les clics.
|
||||||
|
*
|
||||||
|
* Grille "flat-top" (hexagones à sommet plat en haut),
|
||||||
|
* avec décalage vertical d'une demi-hauteur une colonne sur deux.
|
||||||
|
*/
|
||||||
|
public class HexPanel extends JPanel {
|
||||||
|
|
||||||
|
private final HexBoard board;
|
||||||
|
private final JLabel statusLabel;
|
||||||
|
|
||||||
|
// Rayon (distance centre -> sommet)
|
||||||
|
private final int s = 26;
|
||||||
|
private final int margin = 40;
|
||||||
|
// pointy-top : largeur = sqrt(3)*s, hauteur = 2*s
|
||||||
|
private final double hexW = Math.sqrt(3) * s;
|
||||||
|
private final double hexVStep = 1.5 * s; // distance verticale entre centres
|
||||||
|
|
||||||
|
|
||||||
|
private Shape[][] hexShapes;
|
||||||
|
|
||||||
|
public HexPanel(HexBoard board, JLabel statusLabel) {
|
||||||
|
this.board = board;
|
||||||
|
this.statusLabel = statusLabel;
|
||||||
|
this.hexShapes = new Shape[board.getSize()][board.getSize()];
|
||||||
|
|
||||||
|
setBackground(Color.WHITE);
|
||||||
|
|
||||||
|
addMouseListener(new MouseAdapter() {
|
||||||
|
@Override
|
||||||
|
public void mouseClicked(MouseEvent e) {
|
||||||
|
handleClick(e.getX(), e.getY());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dimension getPreferredSize() {
|
||||||
|
int n = board.getSize();
|
||||||
|
|
||||||
|
// largeur : n * hexW + décalage max (hexW/2) + marges
|
||||||
|
int w = margin * 2 + (int) (n * hexW + hexW / 2);
|
||||||
|
|
||||||
|
// hauteur : (n-1)*1.5*s + 2*s + marges
|
||||||
|
int h = margin * 2 + (int) ((n - 1) * hexVStep + 2 * s);
|
||||||
|
|
||||||
|
return new Dimension(w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void handleClick(int x, int y) {
|
||||||
|
if (board.isGameOver()) return;
|
||||||
|
|
||||||
|
int n = board.getSize();
|
||||||
|
for (int row = 0; row < n; row++) {
|
||||||
|
for (int col = 0; col < n; col++) {
|
||||||
|
Shape sh = hexShapes[row][col];
|
||||||
|
if (sh != null && sh.contains(x, y)) {
|
||||||
|
HexPly ply = new HexPly(board.getCurrentPlayer(), row, col);
|
||||||
|
if (board.isLegal(ply)) {
|
||||||
|
board.doPly(ply);
|
||||||
|
HexFrame.updateStatus(board, statusLabel);
|
||||||
|
repaint();
|
||||||
|
} else {
|
||||||
|
Toolkit.getDefaultToolkit().beep();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void paintComponent(Graphics g) {
|
||||||
|
super.paintComponent(g);
|
||||||
|
|
||||||
|
Graphics2D g2 = (Graphics2D) g.create();
|
||||||
|
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
|
||||||
|
// Bordures objectifs (bleu gauche/droite, rouge haut/bas)
|
||||||
|
drawGoalBorders(g2);
|
||||||
|
|
||||||
|
int n = board.getSize();
|
||||||
|
|
||||||
|
// IMPORTANT : boucles cohérentes -> row puis col
|
||||||
|
for (int row = 0; row < n; row++) {
|
||||||
|
for (int col = 0; col < n; col++) {
|
||||||
|
|
||||||
|
Shape hex = createHexShape(row, col);
|
||||||
|
hexShapes[row][col] = hex;
|
||||||
|
|
||||||
|
Player p = board.getCellPlayer(row, col);
|
||||||
|
g2.setColor(colorForCell(p));
|
||||||
|
g2.fill(hex);
|
||||||
|
|
||||||
|
g2.setColor(new Color(120, 120, 120));
|
||||||
|
g2.setStroke(new BasicStroke(1.2f));
|
||||||
|
g2.draw(hex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g2.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Color colorForCell(Player p) {
|
||||||
|
if (p == Player.PLAYER1) return new Color(30, 90, 160); // bleu
|
||||||
|
if (p == Player.PLAYER2) return new Color(220, 50, 50); // rouge
|
||||||
|
return new Color(190, 190, 190); // gris
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pointy-top + décalage par ligne :
|
||||||
|
*
|
||||||
|
* centreX = margin + hexW/2 + col*hexW + (row%2)*(hexW/2)
|
||||||
|
* centreY = margin + s + row*(1.5*s)
|
||||||
|
*/
|
||||||
|
private Shape createHexShape(int row, int col) {
|
||||||
|
double cx = margin + (hexW / 2.0) + col * hexW + ((row % 2) * (hexW / 2.0));
|
||||||
|
double cy = margin + s + row * hexVStep;
|
||||||
|
|
||||||
|
Path2D.Double path = new Path2D.Double();
|
||||||
|
for (int i = 0; i < 6; i++) {
|
||||||
|
double angle = Math.toRadians(i * 60); // pointy-top
|
||||||
|
double x = cx + s * Math.cos(angle);
|
||||||
|
double y = cy + s * Math.sin(angle);
|
||||||
|
if (i == 0) path.moveTo(x, y);
|
||||||
|
else path.lineTo(x, y);
|
||||||
|
}
|
||||||
|
path.closePath();
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void drawGoalBorders(Graphics2D g2) {
|
||||||
|
int n = board.getSize();
|
||||||
|
|
||||||
|
double leftX = margin - 12;
|
||||||
|
double rightX = margin + (hexW / 2.0) + (n - 1) * hexW + (hexW / 2.0) + (hexW / 2.0) + 12;
|
||||||
|
// explication: largeur n colonnes + potentiel décalage d'une demi-largeur
|
||||||
|
|
||||||
|
double topY = margin - 12;
|
||||||
|
double bottomY = margin + s + (n - 1) * hexVStep + s + 12;
|
||||||
|
|
||||||
|
g2.setStroke(new BasicStroke(6f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
|
||||||
|
|
||||||
|
// Bleu: gauche / droite (objectif PLAYER1)
|
||||||
|
g2.setColor(new Color(30, 90, 160));
|
||||||
|
g2.drawLine((int) leftX, (int) topY, (int) leftX, (int) bottomY);
|
||||||
|
g2.drawLine((int) rightX, (int) topY, (int) rightX, (int) bottomY);
|
||||||
|
|
||||||
|
// Rouge: haut / bas (objectif PLAYER2)
|
||||||
|
g2.setColor(new Color(220, 50, 50));
|
||||||
|
g2.drawLine((int) leftX, (int) topY, (int) rightX, (int) topY);
|
||||||
|
g2.drawLine((int) leftX, (int) bottomY, (int) rightX, (int) bottomY);
|
||||||
|
}
|
||||||
|
}
|
||||||
217
javaAPI/fr/iut_fbleau/HexGame/HexSimMain.java
Normal file
217
javaAPI/fr/iut_fbleau/HexGame/HexSimMain.java
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
package fr.iut_fbleau.HexGame;
|
||||||
|
|
||||||
|
import fr.iut_fbleau.GameAPI.*;
|
||||||
|
|
||||||
|
import java.io.BufferedWriter;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lance un grand nombre de parties Hex entre 2 bots aléatoires et affiche des stats.
|
||||||
|
*
|
||||||
|
* Exemples :
|
||||||
|
* java fr.iut_fbleau.HexGame.HexSimMain
|
||||||
|
* java fr.iut_fbleau.HexGame.HexSimMain --games 10000 --size 7 --seed 123
|
||||||
|
* java fr.iut_fbleau.HexGame.HexSimMain --games 5000 --size 11 --csv results.csv
|
||||||
|
*
|
||||||
|
* À seed identique, la suite de nombres
|
||||||
|
* pseudo-aléatoires générée est identique, donc les bots "aléatoires" joueront les mêmes coups
|
||||||
|
* dans le même ordre (tant que le code et l'ordre des appels à Random ne changent pas).</p>
|
||||||
|
*
|
||||||
|
* Intérêt :
|
||||||
|
*
|
||||||
|
* Reproductibilité</b> : relancer exactement la même simulation pour déboguer / analyser.</li>
|
||||||
|
* Comparaison équitable</b> : comparer 2 bots sur les mêmes tirages aléatoires.</li>
|
||||||
|
* Si aucun seed n'est fourni, on utilise généralement l'heure courante, ce qui rend chaque exécution différente.</p>
|
||||||
|
*
|
||||||
|
* long seed;
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class HexSimMain {
|
||||||
|
|
||||||
|
private static class Stats {
|
||||||
|
long win = 0;
|
||||||
|
long draw = 0;
|
||||||
|
long loss = 0;
|
||||||
|
|
||||||
|
long totalMoves = 0;
|
||||||
|
long minMoves = Long.MAX_VALUE;
|
||||||
|
long maxMoves = Long.MIN_VALUE;
|
||||||
|
|
||||||
|
void record(Result r, int moves) {
|
||||||
|
if (r == Result.WIN) win++;
|
||||||
|
else if (r == Result.DRAW) draw++;
|
||||||
|
else if (r == Result.LOSS) loss++;
|
||||||
|
totalMoves += moves;
|
||||||
|
minMoves = Math.min(minMoves, moves);
|
||||||
|
maxMoves = Math.max(maxMoves, moves);
|
||||||
|
}
|
||||||
|
|
||||||
|
long games() { return win + draw + loss; }
|
||||||
|
|
||||||
|
double winRate() { return games() == 0 ? 0.0 : (double) win / games(); }
|
||||||
|
double drawRate() { return games() == 0 ? 0.0 : (double) draw / games(); }
|
||||||
|
double lossRate() { return games() == 0 ? 0.0 : (double) loss / games(); }
|
||||||
|
double avgMoves() { return games() == 0 ? 0.0 : (double) totalMoves / games(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("Games: ").append(games()).append("\n");
|
||||||
|
sb.append("WIN: ").append(win).append(String.format(" (%.2f%%)\n", 100.0 * winRate()));
|
||||||
|
sb.append("DRAW: ").append(draw).append(String.format(" (%.2f%%)\n", 100.0 * drawRate()));
|
||||||
|
sb.append("LOSS: ").append(loss).append(String.format(" (%.2f%%)\n", 100.0 * lossRate()));
|
||||||
|
sb.append(String.format("Moves: avg=%.2f, min=%d, max=%d\n", avgMoves(), minMoves, maxMoves));
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Args {
|
||||||
|
int size = 7;
|
||||||
|
int games = 1000;
|
||||||
|
long seed = System.currentTimeMillis();
|
||||||
|
int progressEvery = 0; // 0 = pas de progress
|
||||||
|
String csvPath = null; // si non null, export par partie
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
Args a = parseArgs(args);
|
||||||
|
|
||||||
|
System.out.println("Hex random-vs-random simulation");
|
||||||
|
System.out.println(" size=" + a.size + " games=" + a.games + " seed=" + a.seed +
|
||||||
|
(a.csvPath != null ? " csv=" + a.csvPath : ""));
|
||||||
|
|
||||||
|
Random master = new Random(a.seed);
|
||||||
|
Stats stats = new Stats();
|
||||||
|
|
||||||
|
BufferedWriter csv = null;
|
||||||
|
try {
|
||||||
|
if (a.csvPath != null) {
|
||||||
|
csv = new BufferedWriter(new FileWriter(a.csvPath));
|
||||||
|
csv.write("game_index,result_p1,moves\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < a.games; i++) {
|
||||||
|
// Nouveau plateau, nouveaux bots (seeds dérivés du seed principal)
|
||||||
|
HexBoard board = new HexBoard(a.size);
|
||||||
|
|
||||||
|
EnumMap<Player, AbstractGamePlayer> players = new EnumMap<>(Player.class);
|
||||||
|
players.put(Player.PLAYER1, new RandomBot(Player.PLAYER1, new Random(master.nextLong())));
|
||||||
|
players.put(Player.PLAYER2, new RandomBot(Player.PLAYER2, new Random(master.nextLong())));
|
||||||
|
|
||||||
|
int moves = runOneGame(board, players);
|
||||||
|
|
||||||
|
Result res = board.getResult();
|
||||||
|
stats.record(res, moves);
|
||||||
|
|
||||||
|
if (csv != null) {
|
||||||
|
csv.write(i + "," + res + "," + moves + "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.progressEvery > 0 && (i + 1) % a.progressEvery == 0) {
|
||||||
|
System.out.println("Progress: " + (i + 1) + "/" + a.games);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("\n=== SUMMARY (Result is from PLAYER1 perspective) ===");
|
||||||
|
System.out.println(stats);
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
System.err.println("I/O error: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
if (csv != null) {
|
||||||
|
try { csv.close(); } catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boucle de jeu (même logique que AbstractGame.run, mais on compte les coups).
|
||||||
|
* On ne modifie pas GameAPI.
|
||||||
|
*/
|
||||||
|
private static int runOneGame(IBoard board, EnumMap<Player, AbstractGamePlayer> players) {
|
||||||
|
int moves = 0;
|
||||||
|
int guardMaxMoves = ((HexBoard) board).getSize() * ((HexBoard) board).getSize(); // au pire : plateau rempli
|
||||||
|
|
||||||
|
while (!board.isGameOver()) {
|
||||||
|
AbstractGamePlayer p = players.get(board.getCurrentPlayer());
|
||||||
|
IBoard safe = board.safeCopy();
|
||||||
|
AbstractPly ply = p.giveYourMove(safe);
|
||||||
|
|
||||||
|
if (!board.isLegal(ply)) {
|
||||||
|
throw new IllegalStateException("Illegal move: " + ply + " by " + board.getCurrentPlayer());
|
||||||
|
}
|
||||||
|
board.doPly(ply);
|
||||||
|
moves++;
|
||||||
|
|
||||||
|
if (moves > guardMaxMoves) {
|
||||||
|
throw new IllegalStateException("Too many moves (" + moves + "), something is wrong.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return moves;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Args parseArgs(String[] args) {
|
||||||
|
Args a = new Args();
|
||||||
|
for (int i = 0; i < args.length; i++) {
|
||||||
|
String s = args[i];
|
||||||
|
switch (s) {
|
||||||
|
case "--size":
|
||||||
|
a.size = Integer.parseInt(nextArg(args, ++i, "--size requires a value"));
|
||||||
|
break;
|
||||||
|
case "--games":
|
||||||
|
a.games = Integer.parseInt(nextArg(args, ++i, "--games requires a value"));
|
||||||
|
break;
|
||||||
|
case "--seed":
|
||||||
|
a.seed = Long.parseLong(nextArg(args, ++i, "--seed requires a value"));
|
||||||
|
break;
|
||||||
|
case "--progress":
|
||||||
|
a.progressEvery = Integer.parseInt(nextArg(args, ++i, "--progress requires a value"));
|
||||||
|
break;
|
||||||
|
case "--csv":
|
||||||
|
a.csvPath = nextArg(args, ++i, "--csv requires a value");
|
||||||
|
break;
|
||||||
|
case "--help":
|
||||||
|
case "-h":
|
||||||
|
printHelpAndExit();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// compat: si l'utilisateur donne juste un nombre, on l'interprète comme size ou games
|
||||||
|
// ex: "7 10000"
|
||||||
|
if (isInt(s)) {
|
||||||
|
int v = Integer.parseInt(s);
|
||||||
|
if (a.size == 11) a.size = v;
|
||||||
|
else a.games = v;
|
||||||
|
} else {
|
||||||
|
System.err.println("Unknown arg: " + s);
|
||||||
|
printHelpAndExit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String nextArg(String[] args, int idx, String errMsg) {
|
||||||
|
if (idx < 0 || idx >= args.length) throw new IllegalArgumentException(errMsg);
|
||||||
|
return args[idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isInt(String s) {
|
||||||
|
try { Integer.parseInt(s); return true; } catch (NumberFormatException e) { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void printHelpAndExit() {
|
||||||
|
System.out.println("Usage: java fr.iut_fbleau.HexGame.HexSimMain [options]\n" +
|
||||||
|
"Options:\n" +
|
||||||
|
" --size N Board size (default 7)\n" +
|
||||||
|
" --games N Number of games (default 1000)\n" +
|
||||||
|
" --seed N Random seed (default current time)\n" +
|
||||||
|
" --progress N Print progress every N games (default 0)\n" +
|
||||||
|
" --csv FILE Write per-game results to CSV\n" +
|
||||||
|
" -h, --help Show this help\n");
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
72
javaAPI/fr/iut_fbleau/HexGame/HumanConsolePlayer.java
Normal file
72
javaAPI/fr/iut_fbleau/HexGame/HumanConsolePlayer.java
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package fr.iut_fbleau.HexGame;
|
||||||
|
|
||||||
|
import fr.iut_fbleau.GameAPI.*;
|
||||||
|
|
||||||
|
import java.util.Scanner;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Joueur humain en console.
|
||||||
|
*
|
||||||
|
* Format attendu : "row col" (indices à partir de 0).
|
||||||
|
*/
|
||||||
|
public class HumanConsolePlayer extends AbstractGamePlayer {
|
||||||
|
|
||||||
|
private final Scanner in;
|
||||||
|
|
||||||
|
public HumanConsolePlayer(Player me, Scanner in) {
|
||||||
|
super(me);
|
||||||
|
this.in = in;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean jesuisMinimax(){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AbstractPly giveYourMove(IBoard board) {
|
||||||
|
if (!(board instanceof HexBoard)) {
|
||||||
|
throw new IllegalArgumentException("Ce joueur attend un HexBoard.");
|
||||||
|
}
|
||||||
|
HexBoard hb = (HexBoard) board;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
System.out.println(hb);
|
||||||
|
System.out.print("Joueur " + board.getCurrentPlayer() + " - entrez un coup (row col) : ");
|
||||||
|
|
||||||
|
String line = in.nextLine().trim();
|
||||||
|
if (line.equalsIgnoreCase("quit") || line.equalsIgnoreCase("exit")) {
|
||||||
|
throw new IllegalStateException("Partie interrompue par l'utilisateur.");
|
||||||
|
}
|
||||||
|
if (line.equalsIgnoreCase("help")) {
|
||||||
|
System.out.println("Entrez deux entiers : row col (0 <= row,col < " + hb.getSize() + ")");
|
||||||
|
System.out.println("Commandes: help, quit");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] parts = line.split("\\s+");
|
||||||
|
if (parts.length != 2) {
|
||||||
|
System.out.println("Format invalide. Exemple: 3 4");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
int r = Integer.parseInt(parts[0]);
|
||||||
|
int c = Integer.parseInt(parts[1]);
|
||||||
|
HexPly ply = new HexPly(board.getCurrentPlayer(), r, c);
|
||||||
|
|
||||||
|
if (!hb.isLegal(ply)) {
|
||||||
|
System.out.println("Coup illégal (case occupée / hors plateau / mauvais joueur). Réessayez.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hb.isWinningMove(ply)) {
|
||||||
|
System.out.println("Coup gagnant !");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ply;
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
System.out.println("Veuillez entrer deux entiers.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
165
javaAPI/fr/iut_fbleau/HexGame/MiniMaxBot.java
Normal file
165
javaAPI/fr/iut_fbleau/HexGame/MiniMaxBot.java
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
package fr.iut_fbleau.HexGame;
|
||||||
|
|
||||||
|
import fr.iut_fbleau.GameAPI.*;
|
||||||
|
|
||||||
|
public class MiniMaxBot extends AbstractGamePlayer {
|
||||||
|
|
||||||
|
private int MAXDEPTH = 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* En dessous (ou égal) à ce nombre de coups restants (cases vides),
|
||||||
|
* on considère que l'arbre est "petit" et on fait un alpha-bêta simple :
|
||||||
|
* pas de cut-off, on explore jusqu'aux positions terminales (isGameOver()).
|
||||||
|
*/
|
||||||
|
private int SMALL_TREE_MOVE_LIMIT = 6;
|
||||||
|
|
||||||
|
public MiniMaxBot(Player me) {
|
||||||
|
super(me); // Correct constructor usage
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean jesuisMinimax(){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AbstractPly giveYourMove(IBoard board) {
|
||||||
|
HexBoard hb = (HexBoard) board;
|
||||||
|
float bestScore = -Float.MAX_VALUE;
|
||||||
|
HexPly bestMove = null;
|
||||||
|
|
||||||
|
// Détermine si l'arbre est petit ou grand
|
||||||
|
int movesLeft = countLegalMoves(hb);
|
||||||
|
boolean useCutoff = (movesLeft > SMALL_TREE_MOVE_LIMIT);
|
||||||
|
int depthToUse = useCutoff ? MAXDEPTH : 0; // 0 car en "simple" on ne coupe pas sur depth
|
||||||
|
|
||||||
|
for (int i = 0; i < hb.getSize(); i++) {
|
||||||
|
for (int j = 0; j < hb.getSize(); j++) {
|
||||||
|
HexPly move = new HexPly(hb.getCurrentPlayer(), i, j);
|
||||||
|
if (hb.isLegal(move)) {
|
||||||
|
hb.doPly(move);
|
||||||
|
|
||||||
|
float score = minimax(hb, depthToUse, -Float.MAX_VALUE, Float.MAX_VALUE, true, useCutoff);
|
||||||
|
|
||||||
|
if (score > bestScore) {
|
||||||
|
bestScore = score;
|
||||||
|
bestMove = move;
|
||||||
|
}
|
||||||
|
hb.undoPly();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestMove;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimax + alpha-bêta
|
||||||
|
* - useCutoff = false : alpha-bêta "simple" -> on s'arrête uniquement sur isGameOver()
|
||||||
|
* - useCutoff = true : alpha-bêta avec cut-off -> arrêt si depth == 0 (heuristique)
|
||||||
|
*/
|
||||||
|
private float minimax(HexBoard board, int depth, float alpha, float beta, boolean isMaximizing, boolean useCutoff) {
|
||||||
|
// Toujours prioritaire : position terminale
|
||||||
|
if (board.isGameOver()) {
|
||||||
|
return terminalScore(board);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cut-off uniquement si demandé
|
||||||
|
if (useCutoff && depth == 0) {
|
||||||
|
return evaluateBoard(board);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMaximizing) {
|
||||||
|
float bestScore = -Float.MAX_VALUE;
|
||||||
|
for (int i = 0; i < board.getSize(); i++) {
|
||||||
|
for (int j = 0; j < board.getSize(); j++) {
|
||||||
|
HexPly move = new HexPly(board.getCurrentPlayer(), i, j);
|
||||||
|
if (board.isLegal(move)) {
|
||||||
|
board.doPly(move);
|
||||||
|
|
||||||
|
int nextDepth = useCutoff ? (depth - 1) : depth;
|
||||||
|
float score = minimax(board, nextDepth, alpha, beta, false, useCutoff);
|
||||||
|
|
||||||
|
bestScore = Math.max(bestScore, score);
|
||||||
|
alpha = Math.max(alpha, bestScore);
|
||||||
|
|
||||||
|
if (beta <= alpha) {
|
||||||
|
board.undoPly();
|
||||||
|
break; // Pruning
|
||||||
|
}
|
||||||
|
|
||||||
|
board.undoPly();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore;
|
||||||
|
} else {
|
||||||
|
float bestScore = Float.MAX_VALUE;
|
||||||
|
for (int i = 0; i < board.getSize(); i++) {
|
||||||
|
for (int j = 0; j < board.getSize(); j++) {
|
||||||
|
HexPly move = new HexPly(board.getCurrentPlayer(), i, j);
|
||||||
|
if (board.isLegal(move)) {
|
||||||
|
board.doPly(move);
|
||||||
|
|
||||||
|
int nextDepth = useCutoff ? (depth - 1) : depth;
|
||||||
|
float score = minimax(board, nextDepth, alpha, beta, true, useCutoff);
|
||||||
|
|
||||||
|
bestScore = Math.min(bestScore, score);
|
||||||
|
beta = Math.min(beta, bestScore);
|
||||||
|
|
||||||
|
if (beta <= alpha) {
|
||||||
|
board.undoPly();
|
||||||
|
break; // Pruning
|
||||||
|
}
|
||||||
|
|
||||||
|
board.undoPly();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Score terminal (feuille) du point de vue de PLAYER1 :
|
||||||
|
* - WIN : PLAYER1 gagne
|
||||||
|
* - LOSS : PLAYER1 perd
|
||||||
|
*/
|
||||||
|
private float terminalScore(HexBoard board) {
|
||||||
|
Result r = board.getResult();
|
||||||
|
if (r == null) return 0f;
|
||||||
|
if (r == Result.WIN) return 1000000f;
|
||||||
|
return -1000000f;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Heuristique actuelle (inchangée) : distance au centre des pions PLAYER1.
|
||||||
|
* (Je ne la modifie pas pour ne pas toucher à la logique existante.)
|
||||||
|
*/
|
||||||
|
private float evaluateBoard(HexBoard board) {
|
||||||
|
int size = board.getSize();
|
||||||
|
int center = size / 2;
|
||||||
|
int score = 0;
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
for (int j = 0; j < size; j++) {
|
||||||
|
if (board.getCellPlayer(i, j) == Player.PLAYER1) {
|
||||||
|
score += Math.abs(i - center) + Math.abs(j - center); // Distance from center
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compte les coups légaux (cases vides) sur le plateau courant.
|
||||||
|
*/
|
||||||
|
private int countLegalMoves(HexBoard board) {
|
||||||
|
int count = 0;
|
||||||
|
for (int i = 0; i < board.getSize(); i++) {
|
||||||
|
for (int j = 0; j < board.getSize(); j++) {
|
||||||
|
if (board.getCellPlayer(i, j) == null) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
}
|
||||||
63
javaAPI/fr/iut_fbleau/HexGame/MonteCarloBot.java
Normal file
63
javaAPI/fr/iut_fbleau/HexGame/MonteCarloBot.java
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package fr.iut_fbleau.HexGame;
|
||||||
|
|
||||||
|
import fr.iut_fbleau.GameAPI.*;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
public class MonteCarloBot extends AbstractGamePlayer {
|
||||||
|
|
||||||
|
private static final int SIMULATION_COUNT = 1000;
|
||||||
|
|
||||||
|
public MonteCarloBot(Player me) {
|
||||||
|
super(me); // Correct constructor usage
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean jesuisMinimax(){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AbstractPly giveYourMove(IBoard board) {
|
||||||
|
HexBoard hb = (HexBoard) board;
|
||||||
|
float bestScore = -Float.MAX_VALUE;
|
||||||
|
HexPly bestMove = null;
|
||||||
|
|
||||||
|
for (int i = 0; i < hb.getSize(); i++) {
|
||||||
|
for (int j = 0; j < hb.getSize(); j++) {
|
||||||
|
HexPly move = new HexPly(hb.getCurrentPlayer(), i, j);
|
||||||
|
if (hb.isLegal(move)) {
|
||||||
|
hb.doPly(move);
|
||||||
|
float score = monteCarloSimulation(hb);
|
||||||
|
if (score > bestScore) {
|
||||||
|
bestScore = score;
|
||||||
|
bestMove = move;
|
||||||
|
}
|
||||||
|
hb.undoPly();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestMove;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float monteCarloSimulation(HexBoard board) {
|
||||||
|
RandomBot simBot = new RandomBot(Player.PLAYER1, new Random().nextLong());
|
||||||
|
HexBoard simBoard = (HexBoard) board.safeCopy();
|
||||||
|
int wins = 0;
|
||||||
|
int simulations = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < SIMULATION_COUNT; i++) {
|
||||||
|
while (!simBoard.isGameOver()) {
|
||||||
|
AbstractPly move = simBot.giveYourMove(simBoard);
|
||||||
|
simBoard.doPly(move);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (simBoard.getResult() == Result.WIN) {
|
||||||
|
wins++;
|
||||||
|
}
|
||||||
|
simulations++;
|
||||||
|
simBoard = (HexBoard) board.safeCopy(); // Reset the board for the next simulation
|
||||||
|
}
|
||||||
|
|
||||||
|
return (float) wins / simulations;
|
||||||
|
}
|
||||||
|
}
|
||||||
44
javaAPI/fr/iut_fbleau/HexGame/RandomBot.java
Normal file
44
javaAPI/fr/iut_fbleau/HexGame/RandomBot.java
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package fr.iut_fbleau.HexGame;
|
||||||
|
|
||||||
|
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 java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
public class RandomBot extends AbstractGamePlayer {
|
||||||
|
|
||||||
|
private final Random rng;
|
||||||
|
|
||||||
|
public RandomBot(Player me, Random rng) {
|
||||||
|
super(me);
|
||||||
|
this.rng = rng;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean jesuisMinimax(){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RandomBot(Player me, long seed) {
|
||||||
|
this(me, new Random(seed));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AbstractPly giveYourMove(IBoard board) {
|
||||||
|
List<AbstractPly> legal = new ArrayList<>();
|
||||||
|
Iterator<AbstractPly> it = board.iterator();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
legal.add(it.next());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (legal.isEmpty()) {
|
||||||
|
throw new IllegalStateException("No legal move available (board is full?)");
|
||||||
|
}
|
||||||
|
|
||||||
|
return legal.get(rng.nextInt(legal.size()));
|
||||||
|
}
|
||||||
|
}
|
||||||
293
javaAPI/fr/iut_fbleau/HexGame/Simulation.java
Normal file
293
javaAPI/fr/iut_fbleau/HexGame/Simulation.java
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
package fr.iut_fbleau.HexGame;
|
||||||
|
|
||||||
|
import fr.iut_fbleau.GameAPI.*;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
|
||||||
|
public class Simulation extends AbstractGame {
|
||||||
|
|
||||||
|
//ATTRIBUTS
|
||||||
|
private HexPly bestmove;
|
||||||
|
private float bestoutcome;
|
||||||
|
private int MAXDEPTH = 9;
|
||||||
|
private int EVALDEPTH = 10;
|
||||||
|
private LinkedList<Integer[]> taken = new LinkedList<Integer[]>();
|
||||||
|
|
||||||
|
//ATTRIBUTS QUE JE NE VOUDRAIS PAS CRÉER IDÉALEMENT
|
||||||
|
private IBoard simCurrentBoard;
|
||||||
|
private EnumMap<Player, AbstractGamePlayer> simmapPlayers;
|
||||||
|
|
||||||
|
//CONSTRUCTEUR
|
||||||
|
public Simulation(IBoard b, EnumMap<Player,AbstractGamePlayer> m){
|
||||||
|
super(b, m);
|
||||||
|
simCurrentBoard = b;
|
||||||
|
simmapPlayers = m;
|
||||||
|
}
|
||||||
|
|
||||||
|
//METHODES
|
||||||
|
/*Le jeu de Hex ne peut jamais finir avec le résultat null. En utilisant cette propriété, on peut avoir cet algorithme simplifié du monte-carlo*/
|
||||||
|
private float MonteCarlo(HexBoard position, Player current){
|
||||||
|
RandomBot simplay = new RandomBot(current, new Random().nextLong());
|
||||||
|
HexBoard simpos = position;
|
||||||
|
LinkedList<Integer[]> ctaken = taken;
|
||||||
|
HexPly testmove;
|
||||||
|
float wins = 0;
|
||||||
|
float losses = 0;
|
||||||
|
int count = 0;
|
||||||
|
for(int i=0; i<EVALDEPTH; i++){
|
||||||
|
while(!simpos.isGameOver()){
|
||||||
|
count++;
|
||||||
|
testmove = (HexPly) simplay.giveYourMove(simpos);
|
||||||
|
if(!ctaken.contains(new Integer[]{testmove.getRow(), testmove.getCol()}) && simpos.isLegal(testmove)){
|
||||||
|
ctaken.add(new Integer[]{testmove.getRow(), testmove.getCol()});
|
||||||
|
simpos.doPly(testmove);
|
||||||
|
if(simpos.getResult()==Result.LOSS){
|
||||||
|
losses++;
|
||||||
|
} else if(simpos.getResult()==Result.WIN){
|
||||||
|
wins++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//System.out.println("count:"+count);
|
||||||
|
for (int j=0; j<count; j++) {
|
||||||
|
simpos.undoPly();
|
||||||
|
}
|
||||||
|
ctaken = taken;
|
||||||
|
count = 0;
|
||||||
|
}
|
||||||
|
//System.out.println(" wins : "+wins+"/losses : "+losses);
|
||||||
|
//System.out.println(" eval : "+(wins-losses)/EVALDEPTH);
|
||||||
|
return (wins-losses)/EVALDEPTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float explMAX(HexBoard position, int depth){
|
||||||
|
if (position.getResult()==Result.LOSS) {
|
||||||
|
return -1.0f;
|
||||||
|
} else if (position.getResult()==Result.WIN){
|
||||||
|
return 1.0f;
|
||||||
|
} else if (depth==MAXDEPTH) {
|
||||||
|
return MonteCarlo(position, Player.PLAYER1);
|
||||||
|
} else {
|
||||||
|
float bestcase = -1.0f;
|
||||||
|
HexPly bestcasemove;
|
||||||
|
HexPly testmove;
|
||||||
|
for (int i=0; i<position.getSize(); i++) {
|
||||||
|
for (int j=0; j<position.getSize(); j++) {
|
||||||
|
if(depth==0){
|
||||||
|
//System.out.println("MAX New Line :");
|
||||||
|
}
|
||||||
|
Integer[] t = new Integer[]{i, j};
|
||||||
|
testmove = new HexPly(Player.PLAYER1, i, j);
|
||||||
|
if(!taken.contains(t) && position.isLegal(testmove)){
|
||||||
|
//System.out.println(" MAX test move : "+Integer.toString(i)+","+Integer.toString(j));
|
||||||
|
taken.add(t);
|
||||||
|
position.doPly(testmove);
|
||||||
|
float val = explMIN(position, depth+1);
|
||||||
|
if (val >= bestcase) {
|
||||||
|
//System.out.println(" MAX new best case");
|
||||||
|
bestcase = val;
|
||||||
|
bestcasemove = testmove;
|
||||||
|
if (depth==0) {
|
||||||
|
this.bestoutcome = bestcase;
|
||||||
|
this.bestmove = bestcasemove;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
position.undoPly();
|
||||||
|
taken.remove(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestcase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private float explMIN(HexBoard position, int depth){
|
||||||
|
if (position.getResult()==Result.LOSS) {
|
||||||
|
return -1.0f;
|
||||||
|
} else if (position.getResult()==Result.WIN){
|
||||||
|
return 1.0f;
|
||||||
|
} else if (depth==MAXDEPTH) {
|
||||||
|
return MonteCarlo(position, Player.PLAYER2);
|
||||||
|
} else {
|
||||||
|
float bestcase = 1.0f;
|
||||||
|
HexPly bestcasemove;
|
||||||
|
HexPly testmove;
|
||||||
|
for (int i=0; i<position.getSize(); i++) {
|
||||||
|
for (int j=0; j<position.getSize(); j++) {
|
||||||
|
if(depth==0){
|
||||||
|
//System.out.println("MIN New Line :");
|
||||||
|
}
|
||||||
|
Integer[] t = new Integer[]{i, j};
|
||||||
|
testmove = new HexPly(Player.PLAYER2, i, j);
|
||||||
|
if(!taken.contains(t) && position.isLegal(testmove)){
|
||||||
|
//System.out.println(" MIN test move : "+Integer.toString(i)+","+Integer.toString(j));
|
||||||
|
taken.add(t);
|
||||||
|
position.doPly(testmove);
|
||||||
|
float val = explMAX(position, depth+1);
|
||||||
|
if (val <= bestcase) {
|
||||||
|
//System.out.println(" MIN new best case");
|
||||||
|
bestcase = val;
|
||||||
|
bestcasemove = testmove;
|
||||||
|
if (depth==0) {
|
||||||
|
this.bestoutcome = bestcase;
|
||||||
|
this.bestmove = bestcasemove;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
position.undoPly();
|
||||||
|
taken.remove(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestcase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private float explMAXAB(HexBoard position, int depth, float A, float B){
|
||||||
|
if (position.getResult()==Result.LOSS) {
|
||||||
|
return -1.0f;
|
||||||
|
} else if (position.getResult()==Result.WIN){
|
||||||
|
return 1.0f;
|
||||||
|
} else if (depth==MAXDEPTH) {
|
||||||
|
return MonteCarlo(position, Player.PLAYER1);
|
||||||
|
} else {
|
||||||
|
float bestcase = A;
|
||||||
|
HexPly bestcasemove;
|
||||||
|
HexPly testmove;
|
||||||
|
for (int i=0; i<position.getSize(); i++) {
|
||||||
|
for (int j=0; j<position.getSize(); j++) {
|
||||||
|
if(depth==0){
|
||||||
|
//System.out.println("MAX New Line :");
|
||||||
|
}
|
||||||
|
Integer[] t = new Integer[]{i, j};
|
||||||
|
testmove = new HexPly(Player.PLAYER1, i, j);
|
||||||
|
if(!taken.contains(t) && position.isLegal(testmove)){
|
||||||
|
//System.out.println(" MAX test move : "+Integer.toString(i)+","+Integer.toString(j));
|
||||||
|
taken.add(t);
|
||||||
|
position.doPly(testmove);
|
||||||
|
float val = explMINAB(position, depth+1, bestcase, B);
|
||||||
|
if (val >= bestcase) {
|
||||||
|
//System.out.println(" MAX new best case");
|
||||||
|
bestcase = val;
|
||||||
|
bestcasemove = testmove;
|
||||||
|
if (depth==0) {
|
||||||
|
this.bestoutcome = bestcase;
|
||||||
|
this.bestmove = bestcasemove;
|
||||||
|
}
|
||||||
|
if(bestcase>=B){
|
||||||
|
return bestcase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
position.undoPly();
|
||||||
|
taken.remove(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestcase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private float explMINAB(HexBoard position, int depth, float A, float B){
|
||||||
|
if (position.getResult()==Result.LOSS) {
|
||||||
|
return -1.0f;
|
||||||
|
} else if (position.getResult()==Result.WIN){
|
||||||
|
return 1.0f;
|
||||||
|
} else if (depth==MAXDEPTH) {
|
||||||
|
return MonteCarlo(position, Player.PLAYER2);
|
||||||
|
} else {
|
||||||
|
float bestcase = B;
|
||||||
|
HexPly bestcasemove;
|
||||||
|
HexPly testmove;
|
||||||
|
for (int i=0; i<position.getSize(); i++) {
|
||||||
|
for (int j=0; j<position.getSize(); j++) {
|
||||||
|
if(depth==0){
|
||||||
|
//System.out.println("MIN New Line :");
|
||||||
|
}
|
||||||
|
Integer[] t = new Integer[]{i, j};
|
||||||
|
testmove = new HexPly(Player.PLAYER2, i, j);
|
||||||
|
if(!taken.contains(t) && position.isLegal(testmove)){
|
||||||
|
//System.out.println(" MIN test move : "+Integer.toString(i)+","+Integer.toString(j));
|
||||||
|
taken.add(t);
|
||||||
|
position.doPly(testmove);
|
||||||
|
float val = explMAXAB(position, depth+1, A, bestcase);
|
||||||
|
if (val <= bestcase) {
|
||||||
|
//System.out.println(" MIN new best case");
|
||||||
|
bestcase = val;
|
||||||
|
bestcasemove = testmove;
|
||||||
|
if (depth==0) {
|
||||||
|
this.bestoutcome = bestcase;
|
||||||
|
this.bestmove = bestcasemove;
|
||||||
|
}
|
||||||
|
if(bestcase<=A){
|
||||||
|
return bestcase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
position.undoPly();
|
||||||
|
taken.remove(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestcase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private AbstractPly GiveBestMove(IBoard board) {
|
||||||
|
if (!(board instanceof HexBoard)) {
|
||||||
|
throw new IllegalArgumentException("Ce joueur attend un HexBoard.");
|
||||||
|
}
|
||||||
|
HexBoard hb = (HexBoard) board;
|
||||||
|
float bestcase;
|
||||||
|
if(hb.getCurrentPlayer()==Player.PLAYER1){
|
||||||
|
bestcase = explMAXAB(hb, 0, -1.0f, 1.0f);
|
||||||
|
} else {
|
||||||
|
bestcase = explMINAB(hb, 0, -1.0f, 1.0f);
|
||||||
|
}
|
||||||
|
return this.bestmove;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result run(){
|
||||||
|
while(!simCurrentBoard.isGameOver()) {
|
||||||
|
AbstractGamePlayer player = simmapPlayers.get(simCurrentBoard.getCurrentPlayer());
|
||||||
|
IBoard board = simCurrentBoard.safeCopy();
|
||||||
|
AbstractPly ply = GiveBestMove(board);
|
||||||
|
HexPly concretePly = (HexPly) ply;
|
||||||
|
|
||||||
|
if (simCurrentBoard.isLegal(ply)) {
|
||||||
|
simCurrentBoard.doPly(ply);
|
||||||
|
taken.add(new Integer[]{concretePly.getRow(), concretePly.getCol()});
|
||||||
|
System.out.println("Player "+player+" goes ("+concretePly.getRow()+","+concretePly.getCol()+")");
|
||||||
|
}
|
||||||
|
else throw new IllegalStateException("Player "+ player + " is a bloody cheat. He tried playing : "+concretePly.getRow()+","+concretePly.getCol()+" I give up.");
|
||||||
|
}
|
||||||
|
return simCurrentBoard.getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result runForArena(){
|
||||||
|
while(!simCurrentBoard.isGameOver()){
|
||||||
|
AbstractGamePlayer player = simmapPlayers.get(simCurrentBoard.getCurrentPlayer());
|
||||||
|
IBoard board = simCurrentBoard.safeCopy();
|
||||||
|
AbstractPly ply;
|
||||||
|
if(player.jesuisMinimax()){
|
||||||
|
ply = GiveBestMove(board);
|
||||||
|
} else {
|
||||||
|
ply = player.giveYourMove(board);
|
||||||
|
}
|
||||||
|
HexPly concretePly = (HexPly) ply;
|
||||||
|
|
||||||
|
if (simCurrentBoard.isLegal(ply)) {
|
||||||
|
simCurrentBoard.doPly(ply);
|
||||||
|
taken.add(new Integer[]{concretePly.getRow(), concretePly.getCol()});
|
||||||
|
System.out.println("Player "+player+" goes ("+concretePly.getRow()+","+concretePly.getCol()+")");
|
||||||
|
}
|
||||||
|
else throw new IllegalStateException("Player "+ player + " is a bloody cheat. He tried playing : "+concretePly.getRow()+","+concretePly.getCol()+" I give up.");
|
||||||
|
}
|
||||||
|
return simCurrentBoard.getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user