Browse Source

Merge commit 'c556fe867d31adc7c9cd0c21f395f06a29a83dc8' into HEAD

develop
Jenkins 2 years ago
parent
commit
6669a2c78c
  1. 183
      src/main/java/de/tims/tictactoe/GameLogic.java
  2. 22
      src/main/java/de/tims/tictactoe/ShowGUI.java
  3. 34
      src/main/java/de/tims/tictactoe/ai/AIEasy.java
  4. 190
      src/main/java/de/tims/tictactoe/ai/AIHard.java
  5. 5
      src/main/java/de/tims/tictactoe/ai/TicTacToeAI.java
  6. 337
      src/test/java/de/tims/tictactoe/GameLogicTest.java
  7. 62
      src/test/java/de/tims/tictactoe/ai/AIEasyTest.java
  8. 262
      src/test/java/de/tims/tictactoe/ai/AIHardTest.java

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

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

34
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);
}
}

190
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;
}
}

5
src/main/java/de/tims/tictactoe/ai/TicTacToeAI.java

@ -0,0 +1,5 @@
package de.tims.tictactoe.ai;
public interface TicTacToeAI {
void calculateNextMove();
}

337
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<Arguments> 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<Arguments> 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<Arguments> 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<Arguments> 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<Arguments> 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<Arguments> 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
}

62
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<Integer> {
@Override
public boolean matches(Integer argument) {
return argument.intValue() >= 0 && argument.intValue() < size;
}
}
}

262
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<Arguments> 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<Arguments> 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<Arguments> 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);
}
}
Loading…
Cancel
Save