diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..eb09b43 --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +.PHONY: build dominos nim clean + +JAR = build/libs/iialib.jar + +build: + gradle build + +dominos: $(JAR) + java -cp $(JAR) games.dominos.DominosGame + +nim: $(JAR) + java -cp $(JAR) games.nim.NimGame + +$(JAR): + gradle build + +clean: + gradle clean diff --git a/src/main/java/games/nim/NimBoard.java b/src/main/java/games/nim/NimBoard.java new file mode 100644 index 0000000..5294432 --- /dev/null +++ b/src/main/java/games/nim/NimBoard.java @@ -0,0 +1,94 @@ +package games.nim; + +import iialib.games.model.IBoard; +import iialib.games.model.Score; + +import java.util.ArrayList; + +public class NimBoard implements IBoard { + + /** Initial number of matchsticks (for heuristic) */ + private final int initialN; + + /** Current number of remaining matchsticks */ + private final int remaining; + + /** The player who plays next */ + private final NimRole currentPlayer; + + // --------- Constructors --------- + + public NimBoard(int n) { + this.initialN = n; + this.remaining = n; + this.currentPlayer = NimRole.AMI; + } + + private NimBoard(int initialN, int remaining, NimRole currentPlayer) { + this.initialN = initialN; + this.remaining = remaining; + this.currentPlayer = currentPlayer; + } + + // --------- Getters --------- + + public int getRemaining() { + return remaining; + } + + public int getInitialN() { + return initialN; + } + + public NimRole getCurrentPlayer() { + return currentPlayer; + } + + // --------- IBoard Methods --------- + + @Override + public ArrayList possibleMoves(NimRole playerRole) { + ArrayList moves = new ArrayList<>(); + if (remaining == 0) return moves; + for (int i = 1; i <= 3 && i <= remaining; i++) { + moves.add(new NimMove(i)); + } + return moves; + } + + @Override + public NimBoard play(NimMove move, NimRole playerRole) { + return new NimBoard(initialN, remaining - move.matchsticks, playerRole.other()); + } + + @Override + public boolean isValidMove(NimMove move, NimRole playerRole) { + return move.matchsticks >= 1 && move.matchsticks <= 3 && move.matchsticks <= remaining; + } + + @Override + public boolean isGameOver() { + return remaining == 0; + } + + @Override + public ArrayList> getScores() { + ArrayList> scores = new ArrayList<>(); + if (isGameOver()) { + // currentPlayer is the one who plays next — they WIN + // (the previous player took the last matchstick and lost) + scores.add(new Score<>(currentPlayer, Score.Status.WIN, 1)); + scores.add(new Score<>(currentPlayer.other(), Score.Status.LOOSE, 0)); + } + return scores; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < remaining; i++) sb.append("|"); + sb.append(" (").append(remaining).append(" matchstick").append(remaining > 1 ? "s" : "").append(")"); + sb.append(" — tour de : ").append(currentPlayer); + return sb.toString(); + } +} diff --git a/src/main/java/games/nim/NimGame.java b/src/main/java/games/nim/NimGame.java new file mode 100644 index 0000000..2bc3068 --- /dev/null +++ b/src/main/java/games/nim/NimGame.java @@ -0,0 +1,65 @@ +package games.nim; + +import java.util.ArrayList; + +import iialib.games.algs.AIPlayer; +import iialib.games.algs.AbstractGame; +import iialib.games.algs.GameAlgorithm; +import iialib.games.algs.algorithms.AlphaBeta; +import iialib.games.algs.algorithms.MiniMax; + +public class NimGame extends AbstractGame { + + NimGame(ArrayList> players, NimBoard board) { + super(players, board); + } + + public static void main(String[] args) { + + // N=10 : AMI voit à 2 demi-coups, ENNEMI voit à 4 demi-coups (TD1 Ex1 Q3) + int N = 10; + + GameAlgorithm algAmi = + new MiniMax<>(NimRole.AMI, NimRole.ENNEMI, NimHeuristics.hAmi, 2); + + GameAlgorithm algEnnemi = + new MiniMax<>(NimRole.ENNEMI, NimRole.AMI, NimHeuristics.hEnnemi, 4); + + AIPlayer playerAmi = + new AIPlayer<>(NimRole.AMI, algAmi); + + AIPlayer playerEnnemi = + new AIPlayer<>(NimRole.ENNEMI, algEnnemi); + + ArrayList> players = new ArrayList<>(); + players.add(playerAmi); + players.add(playerEnnemi); + + NimBoard initialBoard = new NimBoard(N); + NimGame game = new NimGame(players, initialBoard); + game.runGame(); + + // --- Variante : AlphaBeta vs MiniMax --- + System.out.println("\n=== AlphaBeta (AMI, depth 4) vs MiniMax (ENNEMI, depth 2) ==="); + + GameAlgorithm algAbAmi = + new AlphaBeta<>(NimRole.AMI, NimRole.ENNEMI, NimHeuristics.hAmi, 4); + + GameAlgorithm algMmEnnemi = + new MiniMax<>(NimRole.ENNEMI, NimRole.AMI, NimHeuristics.hEnnemi, 2); + + AIPlayer playerAbAmi = + new AIPlayer<>(NimRole.AMI, algAbAmi); + + AIPlayer playerMmEnnemi = + new AIPlayer<>(NimRole.ENNEMI, algMmEnnemi); + + ArrayList> players2 = new ArrayList<>(); + players2.add(playerAbAmi); + players2.add(playerMmEnnemi); + + NimBoard initialBoard2 = new NimBoard(N); + NimGame game2 = new NimGame(players2, initialBoard2); + game2.runGame(); + } +} diff --git a/src/main/java/games/nim/NimHeuristics.java b/src/main/java/games/nim/NimHeuristics.java new file mode 100644 index 0000000..6af5c17 --- /dev/null +++ b/src/main/java/games/nim/NimHeuristics.java @@ -0,0 +1,39 @@ +package games.nim; + +import iialib.games.algs.IHeuristic; + +public class NimHeuristics { + + /** + * Heuristique du point de vue d'AMI (joueur MAX) : + * - h(0) = +inf : jeu terminé, le joueur courant (AMI) gagne (ennemi a pris la dernière) + * - h(1) = -inf si c'est le tour d'AMI (AMI doit prendre la dernière, AMI perd) + * - h(1) = +inf si c'est le tour d'ENNEMI (ENNEMI doit prendre la dernière, AMI gagne) + * - h(n) = N - n pour n > 1 + */ + public static IHeuristic hAmi = (board, role) -> { + int n = board.getRemaining(); + if (n == 0) return IHeuristic.MAX_VALUE; + if (n == 1) { + return (board.getCurrentPlayer() == NimRole.AMI) + ? IHeuristic.MIN_VALUE + : IHeuristic.MAX_VALUE; + } + return board.getInitialN() - n; + }; + + /** + * Heuristique du point de vue d'ENNEMI (joueur MAX dans une partie où ENNEMI maximise) : + * - symétrique à hAmi + */ + public static IHeuristic hEnnemi = (board, role) -> { + int n = board.getRemaining(); + if (n == 0) return IHeuristic.MAX_VALUE; + if (n == 1) { + return (board.getCurrentPlayer() == NimRole.ENNEMI) + ? IHeuristic.MIN_VALUE + : IHeuristic.MAX_VALUE; + } + return board.getInitialN() - n; + }; +} diff --git a/src/main/java/games/nim/NimMove.java b/src/main/java/games/nim/NimMove.java new file mode 100644 index 0000000..109b433 --- /dev/null +++ b/src/main/java/games/nim/NimMove.java @@ -0,0 +1,18 @@ +package games.nim; + +import iialib.games.model.IMove; + +public class NimMove implements IMove { + + /** Number of matchsticks to remove (1, 2 or 3) */ + public final int matchsticks; + + public NimMove(int matchsticks) { + this.matchsticks = matchsticks; + } + + @Override + public String toString() { + return "Remove(" + matchsticks + ")"; + } +} diff --git a/src/main/java/games/nim/NimRole.java b/src/main/java/games/nim/NimRole.java new file mode 100644 index 0000000..683cc6c --- /dev/null +++ b/src/main/java/games/nim/NimRole.java @@ -0,0 +1,11 @@ +package games.nim; + +import iialib.games.model.IRole; + +public enum NimRole implements IRole { + AMI, ENNEMI; + + public NimRole other() { + return (this == AMI) ? ENNEMI : AMI; + } +} diff --git a/src/main/java/iialib/games/algs/algorithms/AlphaBeta.java b/src/main/java/iialib/games/algs/algorithms/AlphaBeta.java new file mode 100644 index 0000000..28950b0 --- /dev/null +++ b/src/main/java/iialib/games/algs/algorithms/AlphaBeta.java @@ -0,0 +1,102 @@ +package iialib.games.algs.algorithms; + +import iialib.games.algs.GameAlgorithm; +import iialib.games.algs.IHeuristic; +import iialib.games.model.IBoard; +import iialib.games.model.IMove; +import iialib.games.model.IRole; + +public class AlphaBeta> + implements GameAlgorithm { + + // Constants + private final static int DEPTH_MAX_DEFAULT = 4; + + // Attributes + private final Role playerMaxRole; + private final Role playerMinRole; + private int depthMax = DEPTH_MAX_DEFAULT; + private IHeuristic h; + + /** number of internal visited (developed) nodes (for stats) */ + private int nbNodes; + + /** number of leaf nodes (for stats) */ + private int nbLeaves; + + // --------- Constructors --------- + + public AlphaBeta(Role playerMaxRole, Role playerMinRole, IHeuristic h) { + this.playerMaxRole = playerMaxRole; + this.playerMinRole = playerMinRole; + this.h = h; + } + + public AlphaBeta(Role playerMaxRole, Role playerMinRole, IHeuristic h, int depthMax) { + this(playerMaxRole, playerMinRole, h); + this.depthMax = depthMax; + } + + // --------- IAlgo Methods --------- + + @Override + public Move bestMove(Board board, Role playerRole) { + System.out.println("[AlphaBeta]"); + nbNodes = 0; + nbLeaves = 0; + + Move bestMove = null; + int alpha = IHeuristic.MIN_VALUE; + int beta = IHeuristic.MAX_VALUE; + + for (Move move : board.possibleMoves(playerMaxRole)) { + Board successor = board.play(move, playerMaxRole); + int value = minValue(successor, 1, alpha, beta); + if (bestMove == null || value > alpha) { + alpha = value; + bestMove = move; + } + } + + System.out.println("Nodes=" + nbNodes + " Leaves=" + nbLeaves); + return bestMove; + } + + // --------- Public Methods --------- + + public String toString() { + return "AlphaBeta(ProfMax=" + depthMax + ")"; + } + + // --------- Private Methods --------- + + private int maxValue(Board board, int depth, int alpha, int beta) { + nbNodes++; + if (board.isGameOver() || depth >= depthMax) { + nbLeaves++; + return h.eval(board, playerMaxRole); + } + for (Move move : board.possibleMoves(playerMaxRole)) { + Board successor = board.play(move, playerMaxRole); + int value = minValue(successor, depth + 1, alpha, beta); + if (value >= beta) return value; // coupe beta + if (value > alpha) alpha = value; + } + return alpha; + } + + private int minValue(Board board, int depth, int alpha, int beta) { + nbNodes++; + if (board.isGameOver() || depth >= depthMax) { + nbLeaves++; + return h.eval(board, playerMaxRole); + } + for (Move move : board.possibleMoves(playerMinRole)) { + Board successor = board.play(move, playerMinRole); + int value = maxValue(successor, depth + 1, alpha, beta); + if (value <= alpha) return value; // coupe alpha + if (value < beta) beta = value; + } + return beta; + } +} diff --git a/src/main/java/iialib/games/algs/algorithms/MiniMax.java b/src/main/java/iialib/games/algs/algorithms/MiniMax.java index 97c5876..80090ea 100644 --- a/src/main/java/iialib/games/algs/algorithms/MiniMax.java +++ b/src/main/java/iialib/games/algs/algorithms/MiniMax.java @@ -71,7 +71,7 @@ public class MiniMax bestValue) { + if (bestMove == null || value > bestValue) { bestValue = value; bestMove = move; }