diff --git a/.idea/misc.xml b/.idea/misc.xml index d0e8c51..67f7df6 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -13,7 +13,7 @@ - + \ No newline at end of file diff --git a/src/main/java/org/toop/game/GameBase.java b/src/main/java/org/toop/game/GameBase.java index f4a0d0f..d1dc9d0 100644 --- a/src/main/java/org/toop/game/GameBase.java +++ b/src/main/java/org/toop/game/GameBase.java @@ -2,10 +2,10 @@ package org.toop.game; public abstract class GameBase { protected Player[] players; - protected int currentPlayer; + public int currentPlayer; protected int size; - protected char[] grid; + public char[] grid; public GameBase(int size) { currentPlayer = 0; @@ -18,22 +18,22 @@ public abstract class GameBase { } } - public Player[] Players() { + public Player[] getPlayers() { return players; } - public Player CurrentPlayer() { + public Player getCurrentPlayer() { return players[currentPlayer]; } - public int Size() { + public int getSize() { return size; } - public char[] Grid() { + public char[] getGrid() { return grid; } - public abstract boolean ValidateMove(int index); - public abstract State PlayMove(int index); + public abstract boolean validateMove(int index); + public abstract State playMove(int index); } diff --git a/src/main/java/org/toop/game/MinMaxTTT.java b/src/main/java/org/toop/game/MinMaxTTT.java new file mode 100644 index 0000000..24231de --- /dev/null +++ b/src/main/java/org/toop/game/MinMaxTTT.java @@ -0,0 +1,100 @@ +package org.toop.game; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.toop.Main; + +/* + * TTT = TIC TAC TOE FOR THE LESS EDUCATED POPULATION ON THIS CODE + */ + +public class MinMaxTTT { + + private static final Logger logger = LogManager.getLogger(Main.class); + + public int findBestMove(TTT game) { + /** + * This method tries to find the best move by seeing if it can set a winning move, if not, it will do a minimax. + */ + int bestVal = -100; // set bestval to something impossible + int bestMove = 10; // set bestmove to something impossible + + // simulate all possible moves on the field + for (int i = 0; i < game.grid.length; i++) { + if (game.validateMove(i)) { // check if the move is legal here + TTT copyGame = game.copyBoard(); // make a copy of the game + State result = copyGame.playMove(i); // play a move on the copy board + + int thisMoveValue; + + if (result == State.WIN) { + return i; // just return right away if you can win on the next move + } + else { + thisMoveValue = doMinimax(copyGame, 8, false); // else look at other moves + } + if (thisMoveValue > bestVal) { // if better move than the current best, change the move + bestVal = thisMoveValue; + bestMove = i; + } + } + } + return bestMove; // return the best move when we've done everything + } + + public int doMinimax(TTT game, int depth, boolean maximizing) { + /** + * This method simulates all the possible future moves in the game through a copy in search of the best move. + */ + boolean state = game.checkWin(); // check for a win (base case stuff) + + if (state) { + if (maximizing) { + // its the maximizing players turn and someone has won. this is not good, so return a negative value + return -10 + depth; + } else { + // it is the turn of the ai and it has won! this is good for us, so return a positive value above 0 + return 10 - depth; + } + } + + else { + boolean empty = false; + for (char cell : game.grid) { // else, look at draw conditions. we check per cell if its empty or not + if (cell == ' ') { + empty = true; // if a thing is empty, set to true + break; // break the loop + } + } + if (!empty || depth == 0) { // if the grid is full or the depth is 0 (both meaning game is over) return 0 for draw + return 0; + } + } + + if (maximizing) { // its the maximizing players turn, the AI + int bestVal = -100; // set the value to lowest as possible + for (int i = 0; i < game.grid.length; i++) { // loop through the grid + if (game.validateMove(i)) { + TTT copyGame = game.copyBoard(); + copyGame.playMove(i); // play the move on a copy board + int value = doMinimax(copyGame, depth - 1, false); // keep going with the minimax + bestVal = Math.max(bestVal, value); // select the best value for the maximizing player (the AI) + } + } + return bestVal; + } + + else { // it's the minimizing players turn, the player + int bestVal = 100; // set the value to the highest possible + for (int i = 0; i < game.grid.length; i++) { // loop through the grid + if (game.validateMove(i)) { + TTT copyGame = game.copyBoard(); + copyGame.playMove(i); // play the move on a copy board + int value = doMinimax(copyGame, depth - 1, true); // keep minimaxing + bestVal = Math.min(bestVal, value); // select the lowest score for the minimizing player, they want to make it hard for us + } + } + return bestVal; + } + } +} diff --git a/src/main/java/org/toop/game/TTT.java b/src/main/java/org/toop/game/TTT.java index 972f4e3..1d448e6 100644 --- a/src/main/java/org/toop/game/TTT.java +++ b/src/main/java/org/toop/game/TTT.java @@ -1,10 +1,15 @@ package org.toop.game; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.toop.Main; + public class TTT extends GameBase { - private int moveCount; + public int moveCount; + private static final Logger logger = LogManager.getLogger(Main.class); public TTT(String player1, String player2) { - super(9); + super(3); // 3x3 Grid players = new Player[2]; players[0] = new Player(player1, 'X'); players[1] = new Player(player2, 'O'); @@ -13,7 +18,7 @@ public class TTT extends GameBase { } @Override - public boolean ValidateMove(int index) { + public boolean validateMove(int index) { if (index < 0 || index > (size * size - 1)) { return false; } @@ -22,15 +27,15 @@ public class TTT extends GameBase { } @Override - public State PlayMove(int index) { - if (!ValidateMove(index)) { + public State playMove(int index) { + if (!validateMove(index)) { return State.INVALID; } grid[index] = players[currentPlayer].Move(); moveCount += 1; - if (CheckWin()) { + if (checkWin()) { return State.WIN; } @@ -42,7 +47,7 @@ public class TTT extends GameBase { return State.NORMAL; } - private boolean CheckWin() { + public boolean checkWin() { // Horizontal for (int i = 0; i < 3; i++) { int index = i * 3; @@ -73,4 +78,15 @@ public class TTT extends GameBase { return false; } + + public TTT copyBoard() { + /** + * This method copies the board, mainly for AI use. + */ + TTT clone = new TTT(players[0].Name(), players[1].Name()); + System.arraycopy(this.grid, 0, clone.grid, 0, this.grid.length); + clone.moveCount = this.moveCount; + clone.currentPlayer = this.currentPlayer; + return clone; + } } diff --git a/src/test/java/MinMaxTTTTest.java b/src/test/java/MinMaxTTTTest.java new file mode 100644 index 0000000..8080285 --- /dev/null +++ b/src/test/java/MinMaxTTTTest.java @@ -0,0 +1,78 @@ + +import org.junit.jupiter.api.Test; +import org.toop.game.MinMaxTTT; +import org.toop.game.TTT; + +import static org.junit.jupiter.api.Assertions.*; + +class MinMaxTTTTest { + + // makegame makes a board situation so we can test the ai. thats it really, the rest is easy to follow id say + private TTT makeGame(String board, int currentPlayer) { + TTT game = new TTT("AI", "Human"); + // Fill the board + for (int i = 0; i < board.length(); i++) { + char c = board.charAt(i); + game.grid[i] = c; + if (c != ' ') game.moveCount++; + } + game.currentPlayer = currentPlayer; + return game; + } + + @Test + void testFindBestMove_AIImmediateWin() { + TTT game = makeGame("XX OO ", 0); + MinMaxTTT ai = new MinMaxTTT(); + int bestMove = ai.findBestMove(game); + assertEquals(2, bestMove, "AI has to take winning move at 2"); + } + + @Test + void testFindBestMove_BlockOpponentWin() { + TTT game = makeGame("OO X ", 0); // 0 = AI's turn + MinMaxTTT ai = new MinMaxTTT(); + int bestMove = ai.findBestMove(game); + assertEquals(2, bestMove, "AI should block opponent win at 2"); + } + + @Test + void testFindBestMove_ChooseDrawIfNoWin() { + TTT game = makeGame("XOXOX O ", 0); + MinMaxTTT ai = new MinMaxTTT(); + int bestMove = ai.findBestMove(game); + assertTrue(bestMove == 6 || bestMove == 8, "AI should draw"); + } + + @Test + void testMinimax_ScoreWin() { + TTT game = makeGame("XXX ", 0); + MinMaxTTT ai = new MinMaxTTT(); + int score = ai.doMinimax(game, 5, false); + assertTrue(score > 0, "AI win scored positively"); + } + + @Test + void testMinimax_ScoreLoss() { + TTT game = makeGame("OOO ", 1); + MinMaxTTT ai = new MinMaxTTT(); + int score = ai.doMinimax(game, 5, true); + assertTrue(score < 0, "AI loss is negative"); + } + + @Test + void testMinimax_ScoreDraw() { + TTT game = makeGame("XOXOXOOXO", 0); + MinMaxTTT ai = new MinMaxTTT(); + int score = ai.doMinimax(game, 5, true); + assertEquals(0, score, "Draw should be zero!"); + } + + @Test + void testMiniMax_MultipleMoves() { + TTT game = makeGame(" X OX O ", 0); + MinMaxTTT ai = new MinMaxTTT(); + int bestMove = ai.findBestMove(game); + assertTrue(bestMove == 0 || bestMove == 2, "Can look at multiple moves!"); + } +} \ No newline at end of file