4 Commits

Author SHA1 Message Date
3aec1d3f6e AUTOPLAY une nouvellle fois 2026-01-30 09:32:17 +01:00
a7d3e9d138 implémentation de l'algo fonctionnelle. Reste à faire un code qui évalue une position 2026-01-21 17:20:06 +01:00
f207da0e2b Merge pull request 'Algo Victoire + Console Player + Main + Javadoc' (#12) from riad-kara-mostefa into master
Reviewed-on: #12
Reviewed-by: Alistair VAISSE <alistair.vaisse@etu.u-pec.fr>
Reviewed-by: Clemence DUCREUX <clemence.ducreux@etu.u-pec.fr>
2026-01-14 16:16:49 +01:00
22891ae2b6 Algo Victoire + Console Player + Main + Javadoc 2026-01-14 11:23:18 +01:00
18 changed files with 434 additions and 113 deletions

View File

@@ -112,60 +112,4 @@ Voici un récapitulatif des commandes Git que vous utiliserez fréquemment :
## 5. Supprimer une branche ## 5. Supprimer une branche
git branch -d <nom_de_la_branche> git branch -d <nom_de_la_branche>
## Comment fonctionne lalgorithme de victoire (idée générale)
Dans Hex, un joueur gagne sil 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 lalgo (principe)
Lalgo fait un parcours de graphe (DFS avec une pile, ou BFS avec une file cest 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 lexploration on atteint lautre bord, alors il existe un chemin → victoire.
Pourquoi ça marche ?
Parce que ça revient à demander :
“Est-ce quil 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 quune 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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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;
} }
/** /**

View File

@@ -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;
} }
@@ -180,8 +308,4 @@ public class HexBoard extends AbstractBoard {
sb.append("Current player: ").append(getCurrentPlayer()).append("\n"); sb.append("Current player: ").append(getCurrentPlayer()).append("\n");
return sb.toString(); return sb.toString();
} }
public int getSize() {
return size;
}
} }

View 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();
}
}

View File

@@ -0,0 +1,68 @@
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;
}
@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.");
}
}
}
}

View File

@@ -0,0 +1,145 @@
package fr.iut_fbleau.HexGame;
import fr.iut_fbleau.GameAPI.*;
import java.util.EnumMap;
import java.util.LinkedList;
public class Simulation extends AbstractGame {
//ATTRIBUTS
private HexPly bestmove;
private float bestoutcome;
private int MAXDEPTH = 6;
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
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 0f;
} 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 0f;
} 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 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 = explMAX(hb, 0);
} else {
bestcase = explMIN(hb, 0);
}
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();
}
}