diff --git a/src/main/java/de/tims/tictactoe/GameLogic.java b/src/main/java/de/tims/tictactoe/GameLogic.java new file mode 100644 index 0000000..befbb06 --- /dev/null +++ b/src/main/java/de/tims/tictactoe/GameLogic.java @@ -0,0 +1,183 @@ +package de.tims.tictactoe; + +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JButton; +import javax.swing.JOptionPane; +import javax.swing.JPanel; + +public class GameLogic implements ActionListener { + + private static final char EMPTY_FIELD = '-'; + private static final char PLAYER_1 = 'x'; + private static final char PLAYER_2 = 'o'; + private char[][] board; + private final char[] occupiedFields = { PLAYER_1, PLAYER_2 }; + private char currentPlayer = PLAYER_1; + private boolean gui = false; + + private JButton[][] fields; + private JPanel contentPanel; + + public GameLogic(int size) { + if (size < 3) { + size = 3; + } + this.board = new char[size][size]; + this.resetBoard(); + } + + public GameLogic(char[][] board) { + this.board = board; + } + + public char[][] getBoard() { + return this.board; + } + + public int countFields() { + return this.board[0].length * this.board.length; + } + + public void setField(int column, int row, char player) { + if (this.fieldIsEmpty(column, row)) { + this.board[column][row] = player; + if (gui) { + this.fields[column][row].setText("" + this.getCurrentPlayer()); + this.updateGUI(); + } + } + } + + public boolean fieldIsEmpty(int column, int row) { + for (char field : this.occupiedFields) { + if (this.board[column][row] == field) + return false; + } + return true; + } + + public boolean checkForWin(char player) { + boolean won = false; + int countFields = 0; + + // check columns + for (int i = 0; i < this.board.length; i++) { + for (int j = 0; j < this.board[0].length; j++) { + if (this.board[j][i] == player) + countFields++; + } + if (countFields == this.board.length) + return won = true; + countFields = 0; + } + // check rows + for (int i = 0; i < this.board[0].length; i++) { + for (int j = 0; j < this.board.length; j++) { + if (this.board[i][j] == player) + countFields++; + } + if (countFields == this.board.length) + return won = true; + countFields = 0; + } + + // check diagonal left + for (int i = this.board.length - 1, j = this.board.length - 1; i >= 0; i--, j--) { + if (this.board[i][j] == player) + countFields++; + } + if (countFields == this.board.length) + return won = true; + countFields = 0; + + // check diagonal right + for (int i = this.board.length - 1, j = 0; i >= 0; i--, j++) { + if (this.board[i][j] == player) + countFields++; + } + if (countFields == this.board.length) + return won = true; + + return won; + } + + public boolean checkEndOfGame() { + return this.checkForWin(PLAYER_1) || this.checkForWin(PLAYER_2); + } + + public char getCurrentPlayer() { + return this.currentPlayer; + } + + public void switchPlayer() { + this.currentPlayer = this.currentPlayer == PLAYER_1 ? PLAYER_2 : PLAYER_1; + } + + public void resetBoard() { + for (int i = 0; i < this.board.length; i++) { + for (int j = 0; j < this.board.length; j++) { + this.board[i][j] = EMPTY_FIELD; + } + } + } + + public JPanel generateGUI() { + this.fields = new JButton[this.board.length][this.board.length]; + this.contentPanel = new JPanel(); + this.contentPanel.setLayout(new GridLayout(this.board.length, this.board.length)); + + for (int i = 0; i < this.fields.length; i++) { + for (int j = 0; j < this.fields.length; j++) { + this.fields[i][j] = new JButton(); + this.fields[i][j].addActionListener(this); + this.contentPanel.add(this.fields[i][j]); + } + } + this.gui = true; + return this.contentPanel; + } + + public JButton getGUIField(int column, int row) { + return this.fields[column][row]; + } + + private void updateGUI() { + if (this.checkEndOfGame()) { + for (int i = 0; i < this.fields.length; i++) { + for (int j = 0; j < this.fields.length; j++) { + this.fields[i][j].setEnabled(false); + } + } + JOptionPane.showMessageDialog(contentPanel, "Spieler " + this.currentPlayer + " hat gewonnen."); + this.resetGUI(); + } + this.switchPlayer(); + } + + private void resetGUI() { + for (int i = 0; i < this.fields.length; i++) { + for (int j = 0; j < this.fields.length; j++) { + this.resetBoard(); + this.fields[i][j].setText(""); + this.fields[i][j].setEnabled(true); + } + } + this.switchPlayer(); + } + + @Override + public void actionPerformed(ActionEvent e) { + for (int i = 0; i < this.fields.length; i++) { + for (int j = 0; j < this.fields[0].length; j++) { + if (e.getSource() == this.fields[i][j]) { + this.setField(i, j, currentPlayer); + this.fields[i][j].getText(); + } + } + } + } + +} \ No newline at end of file diff --git a/src/main/java/de/tims/tictactoe/ShowGUI.java b/src/main/java/de/tims/tictactoe/ShowGUI.java new file mode 100644 index 0000000..47a7f2c --- /dev/null +++ b/src/main/java/de/tims/tictactoe/ShowGUI.java @@ -0,0 +1,22 @@ +package de.tims.tictactoe; + +import javax.swing.JFrame; + +public class ShowGUI { + + private JFrame frame; + + public ShowGUI() { + this.frame = new JFrame("TicTacToe"); + this.frame.setSize(600, 600); + + GameLogic game = new GameLogic(3); + this.frame.add(game.generateGUI()); + this.frame.setVisible(true); + } + + public static void main(String[] args) { + new ShowGUI(); + } + +} diff --git a/src/main/java/de/tims/tictactoe/ai/AIEasy.java b/src/main/java/de/tims/tictactoe/ai/AIEasy.java new file mode 100644 index 0000000..6b61ffa --- /dev/null +++ b/src/main/java/de/tims/tictactoe/ai/AIEasy.java @@ -0,0 +1,34 @@ +package de.tims.tictactoe.ai; + +import de.tims.tictactoe.GameLogic; + +import java.util.Random; + +public class AIEasy implements TicTacToeAI { + private static final char AI_CHAR = 'o'; + private static final char EMPTY_CHAR = '-'; + + private Random rand; + private GameLogic gl; + private int boardSize; + + public AIEasy(GameLogic gl) { + this.gl = gl; + boardSize = gl.getBoard().length; + rand = new Random(); + } + + @Override + public void calculateNextMove() { + char[][] board = gl.getBoard(); + int row; + int col; + do { + row = rand.nextInt(boardSize); + col = rand.nextInt(boardSize); + } while (board[row][col] != EMPTY_CHAR); + + gl.setField(row, col, AI_CHAR); + } + +} diff --git a/src/main/java/de/tims/tictactoe/ai/AIHard.java b/src/main/java/de/tims/tictactoe/ai/AIHard.java new file mode 100644 index 0000000..d3cd174 --- /dev/null +++ b/src/main/java/de/tims/tictactoe/ai/AIHard.java @@ -0,0 +1,190 @@ +package de.tims.tictactoe.ai; + +import de.tims.tictactoe.GameLogic; + +public class AIHard implements TicTacToeAI { + private static final char AI_CHAR = 'o'; + private static final char EMPTY_CHAR = '-'; + private static final char PLAYER_CHAR = 'x'; + private static final int BOARD_SIZE = 3; + + private GameLogic gl; + + public AIHard(GameLogic gl) throws IllegalArgumentException { + if (gl.getBoard().length != BOARD_SIZE) { + throw new IllegalArgumentException("Hard AI only supports 3x3 boards!"); + } + this.gl = gl; + } + + @Override + public void calculateNextMove() { + char[][] board = gl.getBoard(); + int row; + int col; + + int charsInRow = 0; + int charsInCol = 0; + int charsInDiag = 0; + char actualChar; + + for (int i = 0; i < 2; i++) { + actualChar = (i == 0) ? AI_CHAR : PLAYER_CHAR; + + for (int j = 0; j < BOARD_SIZE; j++) { + charsInRow = countCharsInRow(j, actualChar); + charsInCol = countCharsInCol(j, actualChar); + + if (j < 2) { + charsInDiag = countCharsInDiag(j, actualChar); + if (charsInDiag == BOARD_SIZE - 1) { + for (int k = 0; k < BOARD_SIZE; k++) { + row = k; + col = (j == 0) ? k : BOARD_SIZE - 1 - k; + + if (board[row][col] == EMPTY_CHAR) { + gl.setField(row, col, AI_CHAR); + return; + } + } + } + } + + if (charsInRow == BOARD_SIZE - 1 || charsInCol == BOARD_SIZE - 1) { + for (int k = 0; k < BOARD_SIZE; k++) { + if (charsInRow == BOARD_SIZE - 1) { + row = j; + col = k; + + if (board[row][col] == EMPTY_CHAR) { + gl.setField(row, col, AI_CHAR); + return; + } + } + + if (charsInCol == BOARD_SIZE - 1) { + row = k; + col = j; + + if (board[row][col] == EMPTY_CHAR) { + gl.setField(row, col, AI_CHAR); + return; + } + } + } + } + } + } + + if (board[BOARD_SIZE / 2][BOARD_SIZE / 2] == EMPTY_CHAR) { + gl.setField(BOARD_SIZE / 2, BOARD_SIZE / 2, AI_CHAR); + return; + } else if (board[BOARD_SIZE / 2][BOARD_SIZE / 2] == AI_CHAR && (board[0][0] == AI_CHAR || board[0][BOARD_SIZE - 1] == AI_CHAR || board[BOARD_SIZE - 1][0] == AI_CHAR || board[BOARD_SIZE - 1][BOARD_SIZE - 1] == AI_CHAR)) { + int onwCharsInRow = 0; + int ownCharsInCol = 0; + int emptyCharsInRow = 0; + int emptyCharsInCol = 0; + + for (int i = BOARD_SIZE - 2; i > 0; i--) { + for (int j = 0; j < BOARD_SIZE; j += BOARD_SIZE - 1) { + for (int k = 1; k < BOARD_SIZE - 1; k++) { + for (int l = 0; l < 2; l++) { + row = (l == 0) ? j : k; + col = (l == 0) ? k : j; + + onwCharsInRow = countCharsInRow(row, AI_CHAR); + ownCharsInCol = countCharsInCol(col, AI_CHAR); + emptyCharsInRow = countCharsInRow(row, EMPTY_CHAR); + emptyCharsInCol = countCharsInCol(col, EMPTY_CHAR); + + if (onwCharsInRow >= i && ownCharsInCol >= i && emptyCharsInRow >= BOARD_SIZE - onwCharsInRow && emptyCharsInCol == BOARD_SIZE - ownCharsInCol) { + gl.setField(row, col, AI_CHAR); + return; + } + } + } + } + } + } else { + boolean emptyEdgeFound = false; + row = -1; + col = -1; + int prioRow = -1; + int prioCol = -1; + + for (int i = 0; i < BOARD_SIZE; i = i + BOARD_SIZE - 1) { + for (int j = 0; j < BOARD_SIZE; j = j + BOARD_SIZE - 1) { + if (board[i][j] == EMPTY_CHAR) { + row = (row == -1) ? i : row; + col = (col == -1) ? j : col; + + if (countCharsInRow(i, PLAYER_CHAR) != 0 || countCharsInCol(j, PLAYER_CHAR) != 0) { + prioRow = i; + prioCol = j; + emptyEdgeFound = true; + break; + } + } + } + + if (emptyEdgeFound) { + break; + } + } + + if (row != -1 && col != -1) { + row = (prioRow != -1) ? prioRow : row; + col = (prioCol != -1) ? prioCol : col; + + gl.setField(row, col, AI_CHAR); + return; + } + } + + for (row = 0; row < BOARD_SIZE; row++) { + for (col = 0; col < BOARD_SIZE; col++) { + if (board[row][col] == EMPTY_CHAR) { + gl.setField(row, col, AI_CHAR); + return; + } + } + } + } + + public int countCharsInRow(int index, char charToCount) { + int count = 0; + char[][] board = gl.getBoard(); + + for (int i = 0; i < BOARD_SIZE; i++) { + count += (board[index][i] == charToCount) ? 1 : 0; + } + + return count; + } + + public int countCharsInCol(int index, char charToCount) { + int count = 0; + char[][] board = gl.getBoard(); + + for (int i = 0; i < BOARD_SIZE; i++) { + count += (board[i][index] == charToCount) ? 1 : 0; + } + + return count; + } + + public int countCharsInDiag(int index, char charToCount) throws IndexOutOfBoundsException { + if (index < 0 || index > 1) { + throw new IndexOutOfBoundsException("Only 0 and 1 are allowed values for index!"); + } + + int count = 0; + char[][] board = gl.getBoard(); + + for (int i = 0; i < BOARD_SIZE; i++) { + count += (board[i][(index == 0) ? i : BOARD_SIZE - 1 - i] == charToCount) ? 1 : 0; + } + + return count; + } +} diff --git a/src/main/java/de/tims/tictactoe/ai/TicTacToeAI.java b/src/main/java/de/tims/tictactoe/ai/TicTacToeAI.java new file mode 100644 index 0000000..f0f357a --- /dev/null +++ b/src/main/java/de/tims/tictactoe/ai/TicTacToeAI.java @@ -0,0 +1,5 @@ +package de.tims.tictactoe.ai; + +public interface TicTacToeAI { + void calculateNextMove(); +} diff --git a/src/test/java/de/tims/tictactoe/GameLogicTest.java b/src/test/java/de/tims/tictactoe/GameLogicTest.java new file mode 100644 index 0000000..857be01 --- /dev/null +++ b/src/test/java/de/tims/tictactoe/GameLogicTest.java @@ -0,0 +1,337 @@ +package de.tims.tictactoe; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.awt.Component; +import java.util.stream.Stream; + +import javax.swing.JButton; +import javax.swing.JPanel; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +@TestInstance(Lifecycle.PER_CLASS) +class GameLogicTest { + + private final int SIZE = 3; + private GameLogic game; + + @BeforeAll + void setUpBeforeClass() throws Exception { + this.game = new GameLogic(SIZE); + } + + @Test + void createGameLogicTest() { + GameLogic expectedResult = this.game; + GameLogic realResult = new GameLogic(SIZE); + + assertEquals(expectedResult.getClass(), realResult.getClass()); + } + + @Test + void getBoardTest() { + // @formatter:off + char[][] expectedResult = new char[][]{{'-', '-', '-'}, + {'-', '-', '-'}, + {'-', '-', '-'}}; + // @formatter:on + char[][] realResult = this.game.getBoard(); + + assertArrayEquals(expectedResult, realResult); + } + + @Test + void createGameLogicWithGivenBoardTest() { + // @formatter:off + char[][] expectedResult = new char[][]{{'x', '-', '-'}, + {'-', 'o', '-'}, + {'x', '-', '-'}}; + // @formatter:on + char[][] givenBoard = expectedResult; + char[][] realResult = new GameLogic(givenBoard).getBoard(); + + assertArrayEquals(expectedResult, realResult); + } + + @Test + void generateGUITest() { + JPanel expectedResult = new JPanel(); + JPanel realResult = this.game.generateGUI(); + + assertEquals(expectedResult.getClass(), realResult.getClass()); + } + + @Test + void numberOfGUIFieldsTest() { + int expectedResult = (int) Math.pow(SIZE, 2); + int realResult = 0; + + JPanel gui = this.game.generateGUI(); + Component[] components = gui.getComponents(); + + for (Component component : components) { + if (component instanceof JButton) + realResult++; + } + + assertEquals(expectedResult, realResult); + } + + @Test + void getCurrentPlayerTest() { + GameLogic game = new GameLogic(SIZE); + char expectedResult = 'x'; + char realResult = game.getCurrentPlayer(); + + assertEquals(expectedResult, realResult); + } + + @Test + void switchPlayerTest() { + GameLogic game = new GameLogic(SIZE); + game.switchPlayer(); + + char expectedResult = 'o'; + char realResult = game.getCurrentPlayer(); + + assertEquals(expectedResult, realResult); + } + + @Test + void resetBoardTest() { + GameLogic game = new GameLogic(SIZE); + game.setField(1, 2, 'x'); + // @formatter:off + char[][] expectedResult = new char[][]{{'-', '-', '-'}, + {'-', '-', '-'}, + {'-', '-', '-'}}; + // @formatter:on + game.resetBoard(); + char[][] realResult = game.getBoard(); + + assertArrayEquals(expectedResult, realResult); + } + + @ParameterizedTest(name = "[{index}] {0} -> {2} fields") + @MethodSource("testCasesForCountPlayfields") + void fieldCountTest(String testName, int size, int expectedResult) { + GameLogic game = new GameLogic(size); + int realResult = game.countFields(); + + assertEquals(expectedResult, realResult); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("testCasesForSetField") + void setFieldTest(String testName, int column, int row, char player, char[][] expectedResult) { + this.game.setField(column, row, player); + char[][] realResult = this.game.getBoard(); + + assertArrayEquals(expectedResult, realResult); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("testCasesForCheckEmptyField") + void fieldIsEmptyTest(String testName, int columnToCheck, int rowToCheck, boolean expectedResult, char[][] board) { + GameLogic game = new GameLogic(board); + boolean realResult = game.fieldIsEmpty(columnToCheck, rowToCheck); + + assertEquals(expectedResult, realResult); + } + + @ParameterizedTest(name = "[{index}] {0}: should be {2}") + @MethodSource("testCasesForCheckForWin") + void checkForWinTest(String testName, char player, boolean expectedResult, char[][] boardToCheck) { + boolean realResult = new GameLogic(boardToCheck).checkForWin(player); + + assertEquals(expectedResult, realResult); + } + + @ParameterizedTest(name = "[{index}] {0}: should be {1}") + @MethodSource("testCasesForCheckEndOfGame") + void checkEndOfGameTest(String testName, boolean expectedResult, char[][] boardToCheck) { + boolean realResult = new GameLogic(boardToCheck).checkEndOfGame(); + + assertEquals(expectedResult, realResult); + } + + @ParameterizedTest(name = "[{index}] {0}: should be {1}") + @MethodSource("testCasesForCheckButtonState") + void buttonStateTest(String testName, boolean expectedResult, boolean doClick, int column, int row) throws InterruptedException { + GameLogic game = new GameLogic(SIZE); + game.generateGUI(); + JButton currentField = game.getGUIField(0, 0); + + if (doClick) + currentField.doClick(); + boolean realResult = !currentField.getText().isEmpty(); + + assertEquals(expectedResult, realResult); + } + + // @formatter:off + private static Stream testCasesForCountPlayfields() { + return Stream.of(Arguments.of("1x1 board with too few fields", 1, 9), + Arguments.of("2x2 board with too few fields", 2, 9), + Arguments.of("3x3 board with 9 playfields", 3, 9), + Arguments.of("4x4 board with 16 playfields", 4, 16), + Arguments.of("5x5 board with 25 playfields", 5, 25)); + } + + private static Stream testCasesForSetField() { + return Stream.of( + Arguments.of("set field [0][0] for player 1", 0, 0, 'x', new char[][] + {{'x', '-', '-'}, + {'-', '-', '-'}, + {'-', '-', '-'}}), + Arguments.of("set field [1][0] for player 2", 1, 0, 'o', new char[][] + {{'x', '-', '-'}, + {'o', '-', '-'}, + {'-', '-', '-'}}), + Arguments.of("try to set occupied field [1][0] for player 1", 1, 0, 'x', new char[][] + {{'x', '-', '-'}, + {'o', '-', '-'}, + {'-', '-', '-'}}) + ); + } + + private static Stream testCasesForCheckEmptyField() { + return Stream.of( + Arguments.of("check an empty field", 0, 0, true, new char[][] + {{'-', '-', '-'}, + {'-', '-', '-'}, + {'-', '-', '-'}}), + Arguments.of("check a field set by player 1", 0, 0, false, new char[][] + {{'x', '-', '-'}, + {'-', '-', '-'}, + {'-', '-', '-'}}), + Arguments.of("check a field set by player 2", 0, 0, false, new char[][] + {{'o', '-', '-'}, + {'-', '-', '-'}, + {'-', '-', '-'}}) + ); + } + + private static Stream testCasesForCheckForWin() { + return Stream.of( + Arguments.of("check win in column 0 for player 1", 'x', true, new char[][] + {{'x', '-', '-'}, + {'x', '-', '-'}, + {'x', '-', '-'}}), + Arguments.of("check win in column 1 for player 1", 'x', true, new char[][] + {{'-', 'x', '-'}, + {'-', 'x', '-'}, + {'-', 'x', '-'}}), + Arguments.of("check win in column 2 for player 1", 'x', true, new char[][] + {{'-', '-', 'x'}, + {'-', '-', 'x'}, + {'-', '-', 'x'}}), + Arguments.of("check win in column 0 for player 2", 'o', true, new char[][] + {{'o', '-', '-'}, + {'o', '-', '-'}, + {'o', '-', '-'}}), + Arguments.of("check win in row 0 for player 1", 'x', true, new char[][] + {{'x', 'x', 'x'}, + {'-', '-', '-'}, + {'-', '-', '-'}}), + Arguments.of("check win in row 0 for player 2", 'o', true, new char[][] + {{'o', 'o', 'o'}, + {'-', '-', '-'}, + {'-', '-', '-'}}), + Arguments.of("check win in row 1 for player 2", 'o', true, new char[][] + {{'-', '-', '-'}, + {'o', 'o', 'o'}, + {'-', '-', '-'}}), + Arguments.of("check win in row 2 for player 2", 'o', true, new char[][] + {{'-', '-', '-'}, + {'-', '-', '-'}, + {'o', 'o', 'o'}}), + Arguments.of("check win in column 0 for player 1 with full board", 'x', true, new char[][] + {{'x', 'o', 'o'}, + {'x', 'o', 'x'}, + {'x', 'x', 'o'}}), + Arguments.of("check win in column 1 for player 2 with full board", 'o', true, new char[][] + {{'x', 'o', 'o'}, + {'x', 'o', 'x'}, + {'o', 'o', 'x'}}), + Arguments.of("check win in column 2 for player 2 with full board", 'o', true, new char[][] + {{'x', 'o', 'o'}, + {'x', 'x', 'o'}, + {'o', 'o', 'o'}}), + Arguments.of("check win in row 0 for player 1 with full board", 'x', true, new char[][] + {{'x', 'x', 'x'}, + {'x', 'o', 'o'}, + {'o', 'o', 'x'}}), + Arguments.of("check win in row 1 for player 1 with full board", 'x', true, new char[][] + {{'x', 'x', 'o'}, + {'x', 'x', 'x'}, + {'o', 'o', 'x'}}), + Arguments.of("check win in row 2 for player 2 with full board", 'o', true, new char[][] + {{'x', 'x', 'o'}, + {'o', 'x', 'x'}, + {'o', 'o', 'o'}}), + Arguments.of("check win in column 0 for player 2", 'o', false, new char[][] + {{'o', '-', '-'}, + {'o', '-', '-'}, + {'-', '-', '-'}}), + Arguments.of("check a draw for player 2", 'o', false, new char[][] + {{'o', 'o', 'x'}, + {'o', 'x', 'x'}, + {'x', 'x', 'o'}}), + Arguments.of("check a draw for player 1", 'x', false, new char[][] + {{'o', 'o', 'x'}, + {'o', 'o', 'x'}, + {'x', 'x', 'o'}}), + Arguments.of("check diagonal left win for player 1", 'x', true, new char[][] + {{'x', 'o', 'x'}, + {'x', 'x', 'o'}, + {'o', 'o', 'x'}}), + Arguments.of("check diagonal right win for player 2", 'o', true, new char[][] + {{'x', 'x', 'o'}, + {'x', 'o', 'o'}, + {'o', 'x', 'x'}}) + ); + } + + private static Stream testCasesForCheckEndOfGame() { + return Stream.of( + Arguments.of("check empty board", false, new char[][] + {{'-', '-', '-'}, + {'-', '-', '-'}, + {'-', '-', '-'}}), + Arguments.of("end of game with win for player 1", true, new char[][] + {{'x', 'o', 'x'}, + {'x', 'x', 'o'}, + {'x', 'o', 'o'}}), + Arguments.of("end of game with win for player 2", true, new char[][] + {{'x', 'x', 'o'}, + {'o', 'o', 'o'}, + {'x', 'o', 'x'}}), + Arguments.of("check tied game", true, new char[][] + {{'x', 'x', 'o'}, + {'o', 'x', 'o'}, + {'x', 'o', 'x'}}), + Arguments.of("check not yet finished game", false, new char[][] + {{'x', 'x', '-'}, + {'o', 'o', '-'}, + {'x', 'o', 'x'}}) + ); + } + + private static Stream testCasesForCheckButtonState() { + return Stream.of( + Arguments.of("trigger gui field [0][0]", true, true, 0, 0), + Arguments.of("dont't trigger gui field [1][1]", false, false, 1, 1) + ); + } + // @formatter:on + +} diff --git a/src/test/java/de/tims/tictactoe/ai/AIEasyTest.java b/src/test/java/de/tims/tictactoe/ai/AIEasyTest.java new file mode 100644 index 0000000..181577e --- /dev/null +++ b/src/test/java/de/tims/tictactoe/ai/AIEasyTest.java @@ -0,0 +1,62 @@ +package de.tims.tictactoe.ai; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentMatcher; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import de.tims.tictactoe.GameLogic; + +@ExtendWith(MockitoExtension.class) +class AIEasyTest { + static int size = 3; + + @Mock + private GameLogic gl; + + @Test + void emptyBoardChooseRandomField() { + char realChar = 'o'; + doReturn(new char[][] { {'-', '-', '-'}, {'-', '-', '-'}, {'-', '-', '-'} }).when(gl).getBoard(); + + TicTacToeAI ai = new AIEasy(gl); + + //run method 100 times, because of random generator + for (int i = 0; i < 100; i++) { + ai.calculateNextMove(); + } + + verify(gl, times(100)).setField(intThat(new ChooseRandomFieldMatcher()), intThat(new ChooseRandomFieldMatcher()), eq(realChar)); + } + + @Test + void notEmptyBoardChooseRandomFreeField() { + char realChar = 'o'; + doReturn(new char[][] { {'x', '-', 'o'}, {'-', 'o', '-'}, {'-', 'x', 'x'} }).when(gl).getBoard(); + + TicTacToeAI ai = new AIEasy(gl); + + //run method 100 times, because of random generator + for (int i = 0; i < 100; i++) { + ai.calculateNextMove(); + } + + verify(gl, times(100)).setField(intThat(new ChooseRandomFieldMatcher()), intThat(new ChooseRandomFieldMatcher()), eq(realChar)); + //verify that the method is never called with a field which was already set + verify(gl, never()).setField(0, 0, realChar); + verify(gl, never()).setField(0, 2, realChar); + verify(gl, never()).setField(1, 1, realChar); + verify(gl, never()).setField(2, 1, realChar); + verify(gl, never()).setField(2, 2, realChar); + } + + private static class ChooseRandomFieldMatcher implements ArgumentMatcher { + @Override + public boolean matches(Integer argument) { + return argument.intValue() >= 0 && argument.intValue() < size; + } + } +} diff --git a/src/test/java/de/tims/tictactoe/ai/AIHardTest.java b/src/test/java/de/tims/tictactoe/ai/AIHardTest.java new file mode 100644 index 0000000..57fc4f5 --- /dev/null +++ b/src/test/java/de/tims/tictactoe/ai/AIHardTest.java @@ -0,0 +1,262 @@ +package de.tims.tictactoe.ai; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import de.tims.tictactoe.GameLogic; + +@ExtendWith(MockitoExtension.class) +class AIHardTest { + static int size = 3; + + @Mock + private GameLogic gl; + + @Test + void emptyBoardChooseMiddleField() { + char realChar = 'o'; + doReturn(new char[][] { {'-', '-', '-'}, {'-', '-', '-'}, {'-', '-', '-'} }).when(gl).getBoard(); + + TicTacToeAI ai = new AIHard(gl); + ai.calculateNextMove(); + + verify(gl, times(1)).setField(1, 1, realChar); + } + + @Test + void middleFieldAlreadySetChooseEdgeField() { + char realChar = 'o'; + doReturn(new char[][] { {'-', '-', '-'}, {'-', 'x', '-'}, {'-', '-', '-'} }).when(gl).getBoard(); + + TicTacToeAI ai = new AIHard(gl); + ai.calculateNextMove(); + + verify(gl, times(1)).setField(0, 0, realChar); + } + + @Test + void opponentDidntChooseMiddleFieldSoAIDoes() { + char realChar = 'o'; + doReturn(new char[][] { {'-', '-', 'x'}, {'-', '-', '-'}, {'-', '-', '-'} }).when(gl).getBoard(); + + TicTacToeAI ai = new AIHard(gl); + ai.calculateNextMove(); + + verify(gl, times(1)).setField(1, 1, realChar); + } + + @Test + void setEdgeFieldInSecondMove() { + char realChar = 'o'; + doReturn(new char[][] { {'x', '-', '-'}, {'-', 'o', '-'}, {'-', '-', '-'} }).when(gl).getBoard(); + + TicTacToeAI ai = new AIHard(gl); + ai.calculateNextMove(); + + verify(gl, times(1)).setField(0, 2, realChar); + } + + @Test + void preventOpponentsWinInRow() { + char realChar = 'o'; + doReturn(new char[][] { {'o', '-', '-'}, {'x', 'x', '-'}, {'-', '-', '-'} }).when(gl).getBoard(); + + TicTacToeAI ai = new AIHard(gl); + ai.calculateNextMove(); + + verify(gl, times(1)).setField(1, 2, realChar); + } + + @Test + void preventOpponentsWinInCol() { + char realChar = 'o'; + doReturn(new char[][] { {'o', 'x', '-'}, {'-', 'x', '-'}, {'-', '-', '-'} }).when(gl).getBoard(); + + TicTacToeAI ai = new AIHard(gl); + ai.calculateNextMove(); + + verify(gl, times(1)).setField(2, 1, realChar); + } + + @Test + void preventOpponentsWinInDiag() { + char realChar = 'o'; + doReturn(new char[][] { {'x', '-', 'o'}, {'-', 'x', '-'}, {'-', '-', '-'} }).when(gl).getBoard(); + + TicTacToeAI ai = new AIHard(gl); + ai.calculateNextMove(); + + verify(gl, times(1)).setField(2, 2, realChar); + } + + @Test + void when2InRowSetThird() { + char realChar = 'o'; + doReturn(new char[][] { {'o', '-', 'o'}, {'-', 'x', '-'}, {'x', 'x', '-'} }).when(gl).getBoard(); + + TicTacToeAI ai = new AIHard(gl); + ai.calculateNextMove(); + + verify(gl, times(1)).setField(0, 1, realChar); + } + + @Test + void when2InColSetThird() { + char realChar = 'o'; + doReturn(new char[][] { {'o', '-', 'x'}, {'-', 'x', '-'}, {'o', '-', 'x'} }).when(gl).getBoard(); + + TicTacToeAI ai = new AIHard(gl); + ai.calculateNextMove(); + + verify(gl, times(1)).setField(1, 0, realChar); + } + + @Test + void when2inDiagSetThird() { + char realChar = 'o'; + doReturn(new char[][] { {'o', '-', 'x'}, {'-', 'o', 'x'}, {'-', '-', '-'} }).when(gl).getBoard(); + + TicTacToeAI ai = new AIHard(gl); + ai.calculateNextMove(); + + verify(gl, times(1)).setField(2, 2, realChar); + } + + @Test + void opportunityToWinIsMoreImportantThanPreventingOpponentsWinInNextRound() { + char realChar = 'o'; + doReturn(new char[][] { {'x', '-', 'o'}, {'-', 'o', '-'}, {'x', 'x', 'o'} }).when(gl).getBoard(); + + TicTacToeAI ai = new AIHard(gl); + ai.calculateNextMove(); + + verify(gl, times(1)).setField(1, 2, realChar); + } + + @Test + void alwaysSetInTheNearestEdgeToOpponentsX() { + char realChar = 'o'; + doReturn(new char[][] { {'-', '-', '-'}, {'-', 'o', 'x'}, {'-', '-', '-'} }).when(gl).getBoard(); + + TicTacToeAI ai = new AIHard(gl); + ai.calculateNextMove(); + + verify(gl, times(1)).setField(0, 2, realChar); + } + + @Test + void chooseFieldWhichCreatesMostOpportunitiesToWin() { + char realChar = 'o'; + doReturn(new char[][] { {'-', 'x', 'o'}, {'-', 'o', '-'}, {'x', '-', '-'} }).when(gl).getBoard(); + + TicTacToeAI ai = new AIHard(gl); + ai.calculateNextMove(); + + verify(gl, times(1)).setField(1, 2, realChar); + } + + @Test + void setFirstFreeFieldIfNoBetterOpportunities() { + char realChar = 'o'; + doReturn(new char[][] { {'o', 'x', 'x'}, {'x', 'o', 'o'}, {'o', '-', 'x'} }).when(gl).getBoard(); + + TicTacToeAI ai = new AIHard(gl); + ai.calculateNextMove(); + + verify(gl, times(1)).setField(2, 1, realChar); + } + + @Test + void ifRowAndColWithSameNumberContainEachContainTwoCharsDontIgnoreCol() { + char realChar = 'o'; + doReturn(new char[][] { {'x', 'o', 'x'}, {'-', 'o', '-'}, {'o', 'x', 'x'} }).when(gl).getBoard(); + + TicTacToeAI ai = new AIHard(gl); + ai.calculateNextMove(); + + verify(gl, times(1)).setField(1, 2, realChar); + } + + @ParameterizedTest + @MethodSource("testCasesForCountCharsInRow") + void countCharsInRowTest(String testName, char[][] board, int rowNum, char charToCount, int expectedResult) { + doReturn(board).when(gl).getBoard(); + + AIHard ai = new AIHard(gl); + int realResult = ai.countCharsInRow(rowNum, charToCount); + + assertThat(realResult).describedAs(testName).isEqualTo(expectedResult); + } + + private static Stream testCasesForCountCharsInRow() { + return Stream.of(Arguments.of("EmptyFieldReturns0", + new char[][] { {'-', '-', '-'}, {'-', '-', '-'}, {'-', '-', '-'} }, + 0, 'o', 0), + Arguments.of("TwoCharsInRowReturnsTwo", + new char[][] { {'-', '-', '-'}, {'o', 'o', '-'}, {'-', '-', '-'} }, + 1, 'o', 2)); + } + + @ParameterizedTest + @MethodSource("testCasesForCountCharsInCol") + void countCharsInColTest(String testName, char[][] board, int colNum, char charToCount, int expectedResult) { + doReturn(board).when(gl).getBoard(); + + AIHard ai = new AIHard(gl); + int realResult = ai.countCharsInCol(colNum, charToCount); + + assertThat(realResult).describedAs(testName).isEqualTo(expectedResult); + } + + private static Stream testCasesForCountCharsInCol() { + return Stream.of(Arguments.of("EmptyFieldReturns0", + new char[][] { {'-', '-', '-'}, {'-', '-', '-'}, {'-', '-', '-'} }, + 0, 'o', 0), + Arguments.of("TwoCharsInRowReturnsTwo", + new char[][] { {'-', '-', '-'}, {'o', 'o', '-'}, {'-', '-', '-'} }, + 1, 'o', 1)); + } + + @ParameterizedTest + @MethodSource("testCasesForCountCharsInDiag") + void countCharsInDiagTest(String testName, char[][] board, int diagNum, char charToCount, int expectedResult) { + doReturn(board).when(gl).getBoard(); + + AIHard ai = new AIHard(gl); + int realResult = ai.countCharsInDiag(diagNum, charToCount); + + assertThat(realResult).describedAs(testName).isEqualTo(expectedResult); + } + + private static Stream testCasesForCountCharsInDiag() { + return Stream.of(Arguments.of("EmptyFieldReturns0", + new char[][] { {'-', '-', '-'}, {'-', '-', '-'}, {'-', '-', '-'} }, + 0, 'o', 0), + Arguments.of("TwoCharsInRowReturnsTwo", + new char[][] { {'-', '-', 'o'}, {'o', 'o', '-'}, {'-', '-', '-'} }, + 1, 'o', 2)); + } + + @Test + void invalidIndexCausesIndexOutOfBoundsException() { + int index = 2; + char charToCount = 'o'; + String msg = "Only 0 and 1 are allowed values for index!"; + doReturn(new char[][] { {'-', '-', '-'}, {'-', '-', '-'}, {'-', '-', '-'} }).when(gl).getBoard(); + + AIHard ai = new AIHard(gl); + + assertThatThrownBy(() -> {ai.countCharsInDiag(index, charToCount);}).isInstanceOf(IndexOutOfBoundsException.class).hasMessage(msg); + } + +}