Implement Nim game logic and heuristics, including board, moves, roles, and AI algorithms
This commit is contained in:
18
Makefile
Normal file
18
Makefile
Normal file
@@ -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
|
||||||
94
src/main/java/games/nim/NimBoard.java
Normal file
94
src/main/java/games/nim/NimBoard.java
Normal file
@@ -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<NimMove, NimRole, NimBoard> {
|
||||||
|
|
||||||
|
/** 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<NimMove> possibleMoves(NimRole playerRole) {
|
||||||
|
ArrayList<NimMove> 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<Score<NimRole>> getScores() {
|
||||||
|
ArrayList<Score<NimRole>> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
65
src/main/java/games/nim/NimGame.java
Normal file
65
src/main/java/games/nim/NimGame.java
Normal file
@@ -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<NimMove, NimRole, NimBoard> {
|
||||||
|
|
||||||
|
NimGame(ArrayList<AIPlayer<NimMove, NimRole, NimBoard>> 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<NimMove, NimRole, NimBoard> algAmi =
|
||||||
|
new MiniMax<>(NimRole.AMI, NimRole.ENNEMI, NimHeuristics.hAmi, 2);
|
||||||
|
|
||||||
|
GameAlgorithm<NimMove, NimRole, NimBoard> algEnnemi =
|
||||||
|
new MiniMax<>(NimRole.ENNEMI, NimRole.AMI, NimHeuristics.hEnnemi, 4);
|
||||||
|
|
||||||
|
AIPlayer<NimMove, NimRole, NimBoard> playerAmi =
|
||||||
|
new AIPlayer<>(NimRole.AMI, algAmi);
|
||||||
|
|
||||||
|
AIPlayer<NimMove, NimRole, NimBoard> playerEnnemi =
|
||||||
|
new AIPlayer<>(NimRole.ENNEMI, algEnnemi);
|
||||||
|
|
||||||
|
ArrayList<AIPlayer<NimMove, NimRole, NimBoard>> 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<NimMove, NimRole, NimBoard> algAbAmi =
|
||||||
|
new AlphaBeta<>(NimRole.AMI, NimRole.ENNEMI, NimHeuristics.hAmi, 4);
|
||||||
|
|
||||||
|
GameAlgorithm<NimMove, NimRole, NimBoard> algMmEnnemi =
|
||||||
|
new MiniMax<>(NimRole.ENNEMI, NimRole.AMI, NimHeuristics.hEnnemi, 2);
|
||||||
|
|
||||||
|
AIPlayer<NimMove, NimRole, NimBoard> playerAbAmi =
|
||||||
|
new AIPlayer<>(NimRole.AMI, algAbAmi);
|
||||||
|
|
||||||
|
AIPlayer<NimMove, NimRole, NimBoard> playerMmEnnemi =
|
||||||
|
new AIPlayer<>(NimRole.ENNEMI, algMmEnnemi);
|
||||||
|
|
||||||
|
ArrayList<AIPlayer<NimMove, NimRole, NimBoard>> players2 = new ArrayList<>();
|
||||||
|
players2.add(playerAbAmi);
|
||||||
|
players2.add(playerMmEnnemi);
|
||||||
|
|
||||||
|
NimBoard initialBoard2 = new NimBoard(N);
|
||||||
|
NimGame game2 = new NimGame(players2, initialBoard2);
|
||||||
|
game2.runGame();
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/main/java/games/nim/NimHeuristics.java
Normal file
39
src/main/java/games/nim/NimHeuristics.java
Normal file
@@ -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<NimBoard, NimRole> 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<NimBoard, NimRole> 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;
|
||||||
|
};
|
||||||
|
}
|
||||||
18
src/main/java/games/nim/NimMove.java
Normal file
18
src/main/java/games/nim/NimMove.java
Normal file
@@ -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 + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/main/java/games/nim/NimRole.java
Normal file
11
src/main/java/games/nim/NimRole.java
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
102
src/main/java/iialib/games/algs/algorithms/AlphaBeta.java
Normal file
102
src/main/java/iialib/games/algs/algorithms/AlphaBeta.java
Normal file
@@ -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<Move extends IMove, Role extends IRole, Board extends IBoard<Move, Role, Board>>
|
||||||
|
implements GameAlgorithm<Move, Role, Board> {
|
||||||
|
|
||||||
|
// 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<Board, Role> 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<Board, Role> h) {
|
||||||
|
this.playerMaxRole = playerMaxRole;
|
||||||
|
this.playerMinRole = playerMinRole;
|
||||||
|
this.h = h;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AlphaBeta(Role playerMaxRole, Role playerMinRole, IHeuristic<Board, Role> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -71,7 +71,7 @@ public class MiniMax<Move extends IMove,Role extends IRole,Board extends IBoard<
|
|||||||
for (Move move : board.possibleMoves(playerMaxRole)) {
|
for (Move move : board.possibleMoves(playerMaxRole)) {
|
||||||
Board successor = board.play(move, playerMaxRole);
|
Board successor = board.play(move, playerMaxRole);
|
||||||
int value = minValue(successor, 1);
|
int value = minValue(successor, 1);
|
||||||
if (value > bestValue) {
|
if (bestMove == null || value > bestValue) {
|
||||||
bestValue = value;
|
bestValue = value;
|
||||||
bestMove = move;
|
bestMove = move;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user