Jenkins
3 years ago
8 changed files with 1095 additions and 0 deletions
-
183src/main/java/de/tims/tictactoe/GameLogic.java
-
22src/main/java/de/tims/tictactoe/ShowGUI.java
-
34src/main/java/de/tims/tictactoe/ai/AIEasy.java
-
190src/main/java/de/tims/tictactoe/ai/AIHard.java
-
5src/main/java/de/tims/tictactoe/ai/TicTacToeAI.java
-
337src/test/java/de/tims/tictactoe/GameLogicTest.java
-
62src/test/java/de/tims/tictactoe/ai/AIEasyTest.java
-
262src/test/java/de/tims/tictactoe/ai/AIHardTest.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(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
@ -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(); |
|||
} |
|||
|
|||
} |
@ -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); |
|||
} |
|||
|
|||
} |
@ -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; |
|||
} |
|||
} |
@ -0,0 +1,5 @@ |
|||
package de.tims.tictactoe.ai; |
|||
|
|||
public interface TicTacToeAI { |
|||
void calculateNextMove(); |
|||
} |
@ -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 |
|||
|
|||
} |
@ -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; |
|||
} |
|||
} |
|||
} |
@ -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); |
|||
} |
|||
|
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue