diff --git a/.gitignore b/.gitignore index 070e0fe..4b26c77 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,7 @@ # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff: -**/.idea/**/workspace.xml -**/.idea/**/tasks.xml -**/.idea/dictionaries +**/.idea # Sensitive or high-churn files: **/.idea/**/dataSources/ diff --git a/Tic-tac-toe/build.gradle b/Tic-tac-toe/build.gradle new file mode 100644 index 0000000..c892ef2 --- /dev/null +++ b/Tic-tac-toe/build.gradle @@ -0,0 +1,30 @@ +group 'ru.spbau.mit.kazakov.Tic-tac-toe' +version '1.0-SNAPSHOT' + +apply plugin: 'java' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + testCompile group: 'junit', name: 'junit', version: '4.12' +} +group 'ru.spbau.mit.kazakov.Tic-tac-toe' +version '1.0-SNAPSHOT' + +apply plugin: 'java' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + testCompile group: 'junit', name: 'junit', version: '4.12' + compile group: 'org.jetbrains', name: 'annotations', version: '13.0' + +} diff --git a/Tic-tac-toe/gradle/wrapper/gradle-wrapper.jar b/Tic-tac-toe/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..9254496 Binary files /dev/null and b/Tic-tac-toe/gradle/wrapper/gradle-wrapper.jar differ diff --git a/Tic-tac-toe/gradle/wrapper/gradle-wrapper.properties b/Tic-tac-toe/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..1197c59 --- /dev/null +++ b/Tic-tac-toe/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Mar 02 17:12:44 MSK 2018 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.0-bin.zip diff --git a/Tic-tac-toe/gradlew b/Tic-tac-toe/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/Tic-tac-toe/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/Tic-tac-toe/gradlew.bat b/Tic-tac-toe/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/Tic-tac-toe/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/Tic-tac-toe/settings.gradle b/Tic-tac-toe/settings.gradle new file mode 100644 index 0000000..861864f --- /dev/null +++ b/Tic-tac-toe/settings.gradle @@ -0,0 +1,3 @@ +rootProject.name = 'Tic-tac-toe' +rootProject.name = 'Tic-tac-toe' + diff --git a/Tic-tac-toe/src/main/java/ru/spbau/mit/kazakov/TicTacToe/AI.java b/Tic-tac-toe/src/main/java/ru/spbau/mit/kazakov/TicTacToe/AI.java new file mode 100644 index 0000000..a5f6b9a --- /dev/null +++ b/Tic-tac-toe/src/main/java/ru/spbau/mit/kazakov/TicTacToe/AI.java @@ -0,0 +1,11 @@ +package ru.spbau.mit.kazakov.TicTacToe; + +/** + * An interface for representation AI in {@link AIBoard} + */ +public interface AI { + /** + * Analyzes the board and makes turn. + */ + void makeTurn(); +} diff --git a/Tic-tac-toe/src/main/java/ru/spbau/mit/kazakov/TicTacToe/AIBoard.java b/Tic-tac-toe/src/main/java/ru/spbau/mit/kazakov/TicTacToe/AIBoard.java new file mode 100644 index 0000000..c6527d8 --- /dev/null +++ b/Tic-tac-toe/src/main/java/ru/spbau/mit/kazakov/TicTacToe/AIBoard.java @@ -0,0 +1,178 @@ +package ru.spbau.mit.kazakov.TicTacToe; + +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.ThreadLocalRandom; + +/** + * Implements logic of game with AI. + */ +public class AIBoard extends AbstractBoard { + private final AI ai; + + /** + * Calls parent's constructor and initializes AI. + * + * @param size of board + * @param level of AI + */ + public AIBoard(int size, @NotNull AILevel level) { + super(size); + switch (level) { + case EASY: + ai = new EasyAI(); + break; + case MEDIUM: + ai = new MediumAI(); + break; + default: + throw new IllegalArgumentException(); + } + } + + /** + * @see Board#move(int, int) + */ + @NotNull + @Override + public CellContent[][] move(int row, int col) { + board[row][col] = player.toCellContent(); + updateState(row, col); + if (state != State.RUNNING) { + return board; + } + player = player.next(); + ai.makeTurn(); + player = player.next(); + return board; + } + + /** + * AI witch makes random moves. + */ + private class EasyAI implements AI { + /** + * Chooses random free cell and makes turn. + */ + @Override + public void makeTurn() { + int freeCellsNumber = 0; + for (CellContent[] row : board) { + for (CellContent cell : row) { + if (cell.equals(CellContent.EMPTY)) { + freeCellsNumber++; + } + } + } + int turn = ThreadLocalRandom.current().nextInt(0, freeCellsNumber); + for (int i = 0; i < board.length; i++) { + for (int j = 0; j < board.length; j++) { + if (board[i][j].equals(CellContent.EMPTY) && turn-- == 0) { + board[i][j] = player.toCellContent(); + updateState(i, j); + return; + } + } + } + } + } + + /** + * AI witch prevents losing on the next turn and wins this turn if it's possible. + */ + private class MediumAI implements AI { + /** + * Checks if specified line from the board leads to lose in one turn. + * + * @param turns specified line from the board + */ + private boolean canLose(String turns) { + return turns.matches(player.next().toCellContent() + "*" + CellContent.EMPTY + player.next().toCellContent() + "*"); + } + + /** + * Checks if specified line from the board leads to victory in one turn. + * + * @param turns specified line from the board + */ + private boolean canWin(String turns) { + return turns.matches(player.toCellContent() + "*" + CellContent.EMPTY + player.toCellContent() + "*"); + } + + /** + * Tries to win or not to lose this turn. + */ + @Override + public void makeTurn() { + int defendRow = -1; + int defendCol = -1; + + for (int i = 0; i < board.length; i++) { + StringBuilder row = new StringBuilder(); + int rowFreeCell = 0, colFreeCell = 0; + StringBuilder col = new StringBuilder(); + for (int j = 0; j < board.length; j++) { + if (board[i][j].equals(CellContent.EMPTY)) { + rowFreeCell = j; + } + row.append(board[i][j]); + if (board[j][i].equals(CellContent.EMPTY)) { + colFreeCell = j; + } + col.append(board[j][i]); + } + if (canWin(row.toString())) { + board[i][rowFreeCell] = player.toCellContent(); + updateState(i, rowFreeCell); + return; + } else if (canWin(col.toString())) { + board[colFreeCell][i] = player.toCellContent(); + updateState(colFreeCell, i); + return; + } else if (canLose(row.toString())) { + defendRow = i; + defendCol = rowFreeCell; + } else if (canLose(col.toString())) { + defendRow = colFreeCell; + defendCol = i; + } + } + + int firstDiagonalFreeCell = 0, secondDiagonalFreeCell = 0; + StringBuilder firstDiagonal = new StringBuilder(); + StringBuilder secondDiagonal = new StringBuilder(); + for (int i = 0; i < board.length; i++) { + if (board[i][i].equals(CellContent.EMPTY)) { + firstDiagonalFreeCell = i; + } + if (board[i][board.length - 1 - i].equals(CellContent.EMPTY)) { + secondDiagonalFreeCell = i; + } + firstDiagonal.append(board[i][i]); + secondDiagonal.append(board[i][board.length - 1 - i]); + } + if (canWin(firstDiagonal.toString())) { + board[firstDiagonalFreeCell][firstDiagonalFreeCell] = player.toCellContent(); + updateState(firstDiagonalFreeCell, firstDiagonalFreeCell); + return; + } else if (canWin(secondDiagonal.toString())) { + board[secondDiagonalFreeCell][board.length - 1 - secondDiagonalFreeCell] = player.toCellContent(); + updateState(secondDiagonalFreeCell, board.length - 1 - secondDiagonalFreeCell); + return; + } else if (canLose(firstDiagonal.toString())) { + defendCol = firstDiagonalFreeCell; + defendRow = firstDiagonalFreeCell; + } else if (canLose(secondDiagonal.toString())) { + defendCol = board.length - 1 - secondDiagonalFreeCell; + defendRow = secondDiagonalFreeCell; + } + + if (defendCol != -1) { + board[defendRow][defendCol] = player.toCellContent(); + updateState(defendRow, defendCol); + return; + } + new EasyAI().makeTurn(); + } + } +} diff --git a/Tic-tac-toe/src/main/java/ru/spbau/mit/kazakov/TicTacToe/AILevel.java b/Tic-tac-toe/src/main/java/ru/spbau/mit/kazakov/TicTacToe/AILevel.java new file mode 100644 index 0000000..1a21dd7 --- /dev/null +++ b/Tic-tac-toe/src/main/java/ru/spbau/mit/kazakov/TicTacToe/AILevel.java @@ -0,0 +1,8 @@ +package ru.spbau.mit.kazakov.TicTacToe; + +/** + * Enum for available difficulty levels. + */ +public enum AILevel { + EASY, MEDIUM +} diff --git a/Tic-tac-toe/src/main/java/ru/spbau/mit/kazakov/TicTacToe/AbstractBoard.java b/Tic-tac-toe/src/main/java/ru/spbau/mit/kazakov/TicTacToe/AbstractBoard.java new file mode 100644 index 0000000..1bd7e90 --- /dev/null +++ b/Tic-tac-toe/src/main/java/ru/spbau/mit/kazakov/TicTacToe/AbstractBoard.java @@ -0,0 +1,73 @@ +package ru.spbau.mit.kazakov.TicTacToe; + +import org.jetbrains.annotations.NotNull; + +/** + * An abstract class with base functionality of game logic. + */ +public abstract class AbstractBoard implements Board { + protected CellContent[][] board; + protected State state; + protected Player player; + private int size; + private int turnNumber; + + /** + * Sets board size and creates two-dimensional array for board. + */ + public AbstractBoard(int size) { + this.size = size; + reset(); + } + + /** + * @see Board#getState() + */ + @Override + @NotNull + public State getState() { + return state; + } + + /** + * Checks last turn changes and updates game state. + * + * @param row of last turn + * @param col of last turn + */ + public void updateState(int row, int col) { + turnNumber++; + boolean horizontalWin = true; + boolean verticalWin = true; + boolean firstDiagonalWin = true; + boolean secondDiagonalWin = true; + for (int i = 0; i < size; i++) { + verticalWin &= board[i][col].equals(player.toCellContent()); + horizontalWin &= board[row][i].equals(player.toCellContent()); + firstDiagonalWin &= board[i][i].equals(player.toCellContent()); + secondDiagonalWin &= board[i][size - 1 - i].equals(player.toCellContent()); + } + if (horizontalWin || verticalWin || firstDiagonalWin || secondDiagonalWin) { + state = player.getWinState(); + } else if (turnNumber == size * size) { + state = State.DRAW; + } + } + + /** + * @see Board#reset() + */ + @Override + public CellContent[][] reset() { + board = new CellContent[size][size]; + for (CellContent[] row : board) { + for (int i = 0; i < size; i++) { + row[i] = CellContent.EMPTY; + } + } + state = State.RUNNING; + player = Player.X; + turnNumber = 0; + return board; + } +} diff --git a/Tic-tac-toe/src/main/java/ru/spbau/mit/kazakov/TicTacToe/Board.java b/Tic-tac-toe/src/main/java/ru/spbau/mit/kazakov/TicTacToe/Board.java new file mode 100644 index 0000000..188a833 --- /dev/null +++ b/Tic-tac-toe/src/main/java/ru/spbau/mit/kazakov/TicTacToe/Board.java @@ -0,0 +1,27 @@ +package ru.spbau.mit.kazakov.TicTacToe; + +import org.jetbrains.annotations.NotNull; + +public interface Board { + /** + * Makes specified turn on the board and updates state of game. + * + * @param row of turn + * @param col of turn + * @return new state of board + */ + @NotNull + CellContent[][] move(int row, int col); + + /** + * Returns current state of the game. + */ + @NotNull + State getState(); + + /** + * Sets board to starting state. + */ + @NotNull + CellContent[][] reset(); +} diff --git a/Tic-tac-toe/src/main/java/ru/spbau/mit/kazakov/TicTacToe/CellContent.java b/Tic-tac-toe/src/main/java/ru/spbau/mit/kazakov/TicTacToe/CellContent.java new file mode 100644 index 0000000..95b0223 --- /dev/null +++ b/Tic-tac-toe/src/main/java/ru/spbau/mit/kazakov/TicTacToe/CellContent.java @@ -0,0 +1,17 @@ +package ru.spbau.mit.kazakov.TicTacToe; + +import org.jetbrains.annotations.NotNull; + +/** + * Enum for describing content of a cell. + */ +public enum CellContent { + X, O, + EMPTY { + @Override + @NotNull + public String toString() { + return " "; + } + } +} diff --git a/Tic-tac-toe/src/main/java/ru/spbau/mit/kazakov/TicTacToe/HotSeatBoard.java b/Tic-tac-toe/src/main/java/ru/spbau/mit/kazakov/TicTacToe/HotSeatBoard.java new file mode 100644 index 0000000..6db59f2 --- /dev/null +++ b/Tic-tac-toe/src/main/java/ru/spbau/mit/kazakov/TicTacToe/HotSeatBoard.java @@ -0,0 +1,27 @@ +package ru.spbau.mit.kazakov.TicTacToe; + +import org.jetbrains.annotations.NotNull; + +/** + * Implements logic of game for two players. + */ +public class HotSeatBoard extends AbstractBoard { + /** + * @see AbstractBoard#AbstractBoard(int) + */ + public HotSeatBoard(int size) { + super(size); + } + + /** + * @see Board#move(int, int) + */ + @NotNull + @Override + public CellContent[][] move(int row, int col) { + board[row][col] = player.toCellContent(); + updateState(row, col); + player = player.next(); + return board; + } +} diff --git a/Tic-tac-toe/src/main/java/ru/spbau/mit/kazakov/TicTacToe/Player.java b/Tic-tac-toe/src/main/java/ru/spbau/mit/kazakov/TicTacToe/Player.java new file mode 100644 index 0000000..99bf0e5 --- /dev/null +++ b/Tic-tac-toe/src/main/java/ru/spbau/mit/kazakov/TicTacToe/Player.java @@ -0,0 +1,34 @@ +package ru.spbau.mit.kazakov.TicTacToe; + +import org.jetbrains.annotations.NotNull; + +/** + * Enum for describing current turn's owner. + */ +public enum Player { + X, O; + + /** + * Returns owner of the next turn. + */ + @NotNull + public Player next() { + return this == X ? O : X; + } + + /** + * Returns content of cell corresponding to current turn's owner. + */ + @NotNull + public CellContent toCellContent() { + return this == X ? CellContent.X : CellContent.O; + } + + /** + * Returns state of game corresponding to victory of current turn's owner. + */ + @NotNull + public State getWinState() { + return this == X ? State.X_WINS : State.O_WINS; + } +} diff --git a/Tic-tac-toe/src/main/java/ru/spbau/mit/kazakov/TicTacToe/State.java b/Tic-tac-toe/src/main/java/ru/spbau/mit/kazakov/TicTacToe/State.java new file mode 100644 index 0000000..7d4a36f --- /dev/null +++ b/Tic-tac-toe/src/main/java/ru/spbau/mit/kazakov/TicTacToe/State.java @@ -0,0 +1,8 @@ +package ru.spbau.mit.kazakov.TicTacToe; + +/** + * Enum for states of game. + */ +public enum State { + RUNNING, X_WINS, O_WINS, DRAW +} diff --git a/Tic-tac-toe/src/main/java/ru/spbau/mit/kazakov/TicTacToe/TicTacToe.java b/Tic-tac-toe/src/main/java/ru/spbau/mit/kazakov/TicTacToe/TicTacToe.java new file mode 100644 index 0000000..94db018 --- /dev/null +++ b/Tic-tac-toe/src/main/java/ru/spbau/mit/kazakov/TicTacToe/TicTacToe.java @@ -0,0 +1,288 @@ +package ru.spbau.mit.kazakov.TicTacToe; + +import javafx.application.Application; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.ListView; +import javafx.scene.layout.*; +import javafx.scene.paint.Color; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; +import javafx.scene.text.Text; +import javafx.scene.text.TextAlignment; +import javafx.stage.Stage; + + +/** + * Tic Tac Toe application with single and multi player. + */ +public class TicTacToe extends Application { + private static final int BOARD_SIZE = 3; + private static final String EASY_AI = "Easy"; + private static final String MEDIUM_AI = "Medium"; + private static final String SINGLE_PLAYER_LABEL = "SinglePlayer"; + private static final String MULTI_PLAYER_LABEL = "MultiPlayer"; + private static final String STATS_LABEL = "Stats"; + private static final String EXIT_LABEL = "Exit"; + private static final String RESTART_LABEL = "Restart"; + private static final String MAIN_MENU_LABEL = "Main menu"; + private static final String APP_TITLE = "Nevermind"; + private static final String MAIN_MENU_TITLE = "Welcome"; + + private Stage primaryStage; + private Scene mainMenuScene; + private Scene boardScene; + private Button[][] cells; + private Text gameResult; + private Scene statsScene; + private final ObservableList stats = FXCollections.observableArrayList(); + private Board board; + + /** + * @see Application#start(Stage) + */ + @Override + public void start(Stage primaryStage) { + this.primaryStage = primaryStage; + initializeMainMenuScene(); + initializeBoardScene(); + initializeStatsScene(); + primaryStage.setScene(mainMenuScene); + primaryStage.setTitle(APP_TITLE); + primaryStage.show(); + primaryStage.requestFocus(); + } + + /** + * Creates and initializes main menu scene. + */ + private void initializeMainMenuScene() { + Text sceneTitle = new Text(MAIN_MENU_TITLE); + sceneTitle.setFont(Font.font("Tahoma", FontWeight.NORMAL, 20)); + sceneTitle.setFill(Color.WHITE); + + HBox singlePlayerMenu = new HBox(); + singlePlayerMenu.setPadding(new Insets(0, 0, 0, 0)); + singlePlayerMenu.setSpacing(0); + + ComboBox AILevelComboBox = new ComboBox<>(); + AILevelComboBox.setFocusTraversable(false); + AILevelComboBox.setMaxWidth(Double.MAX_VALUE); + AILevelComboBox.getItems().addAll(MEDIUM_AI, EASY_AI); + AILevelComboBox.setValue(MEDIUM_AI); + + Button singlePlayerButton = new Button(SINGLE_PLAYER_LABEL); + singlePlayerButton.setFocusTraversable(false); + singlePlayerButton.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); + singlePlayerButton.setOnAction(value -> { + switch (AILevelComboBox.getValue()) { + case MEDIUM_AI: + board = new AIBoard(BOARD_SIZE, AILevel.MEDIUM); + break; + case EASY_AI: + board = new AIBoard(BOARD_SIZE, AILevel.EASY); + break; + } + gameResult.setText(""); + updateBoardScene(board.reset()); + this.primaryStage.setScene(boardScene); + }); + + HBox.setHgrow(AILevelComboBox, Priority.ALWAYS); + HBox.setHgrow(singlePlayerButton, Priority.ALWAYS); + singlePlayerMenu.getChildren().addAll(singlePlayerButton, AILevelComboBox); + + Button multiPlayerButton = new Button(MULTI_PLAYER_LABEL); + multiPlayerButton.setFocusTraversable(false); + multiPlayerButton.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); + multiPlayerButton.setOnAction(value -> { + board = new HotSeatBoard(BOARD_SIZE); + gameResult.setText(""); + updateBoardScene(board.reset()); + primaryStage.setScene(boardScene); + }); + + Button statsButton = new Button(STATS_LABEL); + statsButton.setFocusTraversable(false); + statsButton.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); + statsButton.setOnAction(value -> primaryStage.setScene(statsScene)); + + Button exitButton = new Button(EXIT_LABEL); + exitButton.setFocusTraversable(false); + exitButton.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); + exitButton.setOnAction(value -> Platform.exit()); + + VBox menu = new VBox(); + menu.setPadding(new Insets(25, 25, 25, 25)); + menu.setSpacing(10); + menu.setAlignment(Pos.CENTER); + menu.setStyle("-fx-background-color: #ffad33;"); + menu.getChildren().addAll(sceneTitle, singlePlayerMenu, multiPlayerButton, statsButton, exitButton); + mainMenuScene = new Scene(menu, 300, 275); + } + + /** + * Creates and initializes board scene. + */ + private void initializeBoardScene() { + Button mainMenuButton = new Button(MAIN_MENU_LABEL); + mainMenuButton.setFocusTraversable(false); + mainMenuButton.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); + mainMenuButton.setOnAction(value -> primaryStage.setScene(mainMenuScene)); + + Button restartButton = new Button(RESTART_LABEL); + restartButton.setFocusTraversable(false); + restartButton.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); + restartButton.setOnAction(value -> { + gameResult.setText(""); + updateBoardScene(board.reset()); + }); + + Text gameResult = new Text(); + gameResult.setTextAlignment(TextAlignment.CENTER); + gameResult.setFont(Font.font("Tahoma", FontWeight.NORMAL, 20)); + gameResult.setFill(Color.WHITE); + this.gameResult = gameResult; + + HBox navigationBar = new HBox(); + navigationBar.setPadding(new Insets(0, 0, 0, 0)); + navigationBar.setSpacing(0); + navigationBar.setAlignment(Pos.CENTER); + navigationBar.setStyle("-fx-background-color: #ffad33;"); + navigationBar.getChildren().addAll(mainMenuButton, restartButton); + + Button[][] cells = new Button[BOARD_SIZE][BOARD_SIZE]; + for (int i = 0; i < cells.length; i++) { + for (int j = 0; j < cells.length; j++) { + int row = i; + int col = j; + cells[i][j] = new Button(" "); + cells[i][j].setStyle("-fx-text-fill: black; -fx-font-size: 35;"); + cells[i][j].setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); + cells[i][j].setFocusTraversable(false); + cells[i][j].setOnAction(value -> move(row, col)); + } + } + this.cells = cells; + + ColumnConstraints[] cols = new ColumnConstraints[BOARD_SIZE]; + for (int i = 0; i < cols.length; i++) { + cols[i] = new ColumnConstraints(); + cols[i].setPercentWidth(50); + } + + RowConstraints[] rows = new RowConstraints[BOARD_SIZE]; + for (int i = 0; i < cols.length; i++) { + rows[i] = new RowConstraints(); + rows[i].setPercentHeight(100); + rows[i].setFillHeight(true); + } + + GridPane grid = new GridPane(); + grid.getColumnConstraints().addAll(cols); + grid.getRowConstraints().addAll(rows); + for (int i = 0; i < cells.length; i++) { + for (int j = 0; j < cells.length; j++) { + grid.add(cells[i][j], j, i); + } + } + + BorderPane border = new BorderPane(); + BorderPane.setAlignment(gameResult, Pos.CENTER); + border.setStyle("-fx-background-color: #ffad33;"); + border.setBottom(navigationBar); + border.setCenter(grid); + border.setTop(gameResult); + boardScene = new Scene(border, 300, 275); + } + + /** + * Creates and initializes stats scene. + */ + private void initializeStatsScene() { + VBox statsPane = new VBox(); + statsPane.setAlignment(Pos.CENTER); + statsPane.setStyle("-fx-background-color: #ffad33;"); + statsPane.setPadding(new Insets(10, 25, 10, 25)); + statsPane.setSpacing(10); + + Text sceneTitle = new Text(STATS_LABEL); + sceneTitle.setFont(Font.font("Tahoma", FontWeight.NORMAL, 20)); + sceneTitle.setFill(Color.WHITE); + + ListView listView = new ListView<>(stats); + listView.setFocusTraversable(false); + + Button mainMenuButton = new Button(MAIN_MENU_LABEL); + mainMenuButton.setFocusTraversable(false); + mainMenuButton.setOnAction(value -> primaryStage.setScene(mainMenuScene)); + + statsPane.getChildren().addAll(sceneTitle, listView, mainMenuButton); + statsScene = new Scene(statsPane, 300, 275); + } + + /** + * Handles player's turn. + * + * @param row witch player've chose + * @param col witch player've chose + */ + private void move(int row, int col) { + updateBoardScene(board.move(row, col)); + switch (board.getState()) { + case O_WINS: + gameResult.setText("O WINS!"); + stats.add("O's victory"); + break; + case X_WINS: + gameResult.setText("X WINS!"); + stats.add("X's victory"); + break; + case DRAW: + gameResult.setText("DRAW!"); + stats.add("Draw"); + break; + } + if (!board.getState().equals(State.RUNNING)) { + disableBoard(); + } + } + + /** + * Prevents all new turns. + */ + private void disableBoard() { + for (Button[] cell : cells) { + for (int j = 0; j < cells.length; j++) { + cell[j].setDisable(true); + } + } + } + + /** + * Reflects specified state of the board for player. + * + * @param boardState specified state of the board + */ + private void updateBoardScene(CellContent[][] boardState) { + for (int i = 0; i < cells.length; i++) { + for (int j = 0; j < cells.length; j++) { + cells[i][j].setDisable(!boardState[i][j].equals(CellContent.EMPTY)); + cells[i][j].setText(boardState[i][j].toString()); + } + } + } + + /** + * Runs application. + */ + public static void main(String[] args) { + Application.launch(args); + } +} diff --git a/Tic-tac-toe/src/test/java/ru/spbau/mit/kazakov/TicTacToe/AIBoardTest.java b/Tic-tac-toe/src/test/java/ru/spbau/mit/kazakov/TicTacToe/AIBoardTest.java new file mode 100644 index 0000000..03acec4 --- /dev/null +++ b/Tic-tac-toe/src/test/java/ru/spbau/mit/kazakov/TicTacToe/AIBoardTest.java @@ -0,0 +1,197 @@ +package ru.spbau.mit.kazakov.TicTacToe; + +import org.junit.Before; +import org.junit.Test; + +import java.lang.reflect.Field; + +import static org.junit.Assert.*; + +public class AIBoardTest { + private Field boardField; + + @Before + public void initializeFiled() throws NoSuchFieldException { + boardField = AbstractBoard.class.getDeclaredField("board"); + boardField.setAccessible(true); + } + + @Test + public void testEasyLevelConstructor() { + new AIBoard(10, AILevel.EASY); + } + + @Test + public void testMediumLevelConstructor() { + new AIBoard(10, AILevel.MEDIUM); + } + + @Test + public void testMakeTurnEasyAI() { + Board board = new AIBoard(3, AILevel.EASY); + CellContent[][] boardState = board.move(2, 1); + checkEnemyFirstTurn(boardState); + assertEquals(CellContent.X, boardState[2][1]); + } + + @Test + public void testMakeTurnMediumAISimple() { + Board board = new AIBoard(3, AILevel.EASY); + CellContent[][] boardState = board.move(2, 1); + checkEnemyFirstTurn(boardState); + assertEquals(CellContent.X, boardState[2][1]); + } + + @Test + public void testMakeTurnMediumAIPreventHorizontalLose() throws IllegalAccessException { + CellContent[][] startingState = new CellContent[][]{{CellContent.X, CellContent.EMPTY, CellContent.EMPTY}, + {CellContent.O, CellContent.EMPTY, CellContent.EMPTY}, + {CellContent.EMPTY, CellContent.EMPTY, CellContent.EMPTY}}; + CellContent[][] expectedState = new CellContent[][]{{CellContent.X, CellContent.X, CellContent.O}, + {CellContent.O, CellContent.EMPTY, CellContent.EMPTY}, + {CellContent.EMPTY, CellContent.EMPTY, CellContent.EMPTY}}; + checkStateAfterPlayerTurn(startingState, expectedState, 0, 1); + } + + @Test + public void testMakeTurnMediumAIPreventVerticalLose() throws IllegalAccessException { + CellContent[][] startingState = new CellContent[][]{{CellContent.X, CellContent.EMPTY, CellContent.O}, + {CellContent.EMPTY, CellContent.EMPTY, CellContent.EMPTY}, + {CellContent.EMPTY, CellContent.EMPTY, CellContent.EMPTY}}; + CellContent[][] expectedState = new CellContent[][]{{CellContent.X, CellContent.EMPTY, CellContent.O}, + {CellContent.X, CellContent.EMPTY, CellContent.EMPTY}, + {CellContent.O, CellContent.EMPTY, CellContent.EMPTY}}; + checkStateAfterPlayerTurn(startingState, expectedState, 1, 0); + } + + @Test + public void testMakeTurnMediumAIPreventFirstDiagonalLose() throws IllegalAccessException { + CellContent[][] startingState = new CellContent[][]{{CellContent.X, CellContent.EMPTY, CellContent.O}, + {CellContent.EMPTY, CellContent.EMPTY, CellContent.EMPTY}, + {CellContent.EMPTY, CellContent.EMPTY, CellContent.EMPTY}}; + CellContent[][] expectedState = new CellContent[][]{{CellContent.X, CellContent.EMPTY, CellContent.O}, + {CellContent.EMPTY, CellContent.X, CellContent.EMPTY}, + {CellContent.EMPTY, CellContent.EMPTY, CellContent.O}}; + checkStateAfterPlayerTurn(startingState, expectedState, 1, 1); + } + + @Test + public void testMakeTurnMediumAIPreventSecondDiagonalLose() throws IllegalAccessException { + CellContent[][] startingState = new CellContent[][]{{CellContent.O, CellContent.EMPTY, CellContent.X}, + {CellContent.EMPTY, CellContent.EMPTY, CellContent.EMPTY}, + {CellContent.EMPTY, CellContent.EMPTY, CellContent.EMPTY}}; + CellContent[][] expectedState = new CellContent[][]{{CellContent.O, CellContent.EMPTY, CellContent.X}, + {CellContent.EMPTY, CellContent.X, CellContent.EMPTY}, + {CellContent.O, CellContent.EMPTY, CellContent.EMPTY}}; + checkStateAfterPlayerTurn(startingState, expectedState, 1, 1); + } + + @Test + public void testMakeTurnMediumAIHorizontalWin() throws IllegalAccessException { + CellContent[][] startingState = new CellContent[][]{{CellContent.O, CellContent.EMPTY, CellContent.O}, + {CellContent.X, CellContent.EMPTY, CellContent.EMPTY}, + {CellContent.X, CellContent.EMPTY, CellContent.EMPTY}}; + CellContent[][] expectedState = new CellContent[][]{{CellContent.O, CellContent.O, CellContent.O}, + {CellContent.X, CellContent.X, CellContent.EMPTY}, + {CellContent.X, CellContent.EMPTY, CellContent.EMPTY}}; + checkStateAfterPlayerTurn(startingState, expectedState, 1, 1); + } + + @Test + public void testMakeTurnMediumAIVerticalWin() throws IllegalAccessException { + CellContent[][] startingState = new CellContent[][]{{CellContent.O, CellContent.X, CellContent.X}, + {CellContent.O, CellContent.EMPTY, CellContent.EMPTY}, + {CellContent.EMPTY, CellContent.EMPTY, CellContent.EMPTY}}; + CellContent[][] expectedState = new CellContent[][]{{CellContent.O, CellContent.X, CellContent.X}, + {CellContent.O, CellContent.X, CellContent.EMPTY}, + {CellContent.O, CellContent.EMPTY, CellContent.EMPTY}}; + checkStateAfterPlayerTurn(startingState, expectedState, 1, 1); + } + + @Test + public void testMakeTurnMediumAIFirstDiagonalWin() throws IllegalAccessException { + CellContent[][] startingState = new CellContent[][]{{CellContent.O, CellContent.X, CellContent.X}, + {CellContent.EMPTY, CellContent.EMPTY, CellContent.EMPTY}, + {CellContent.EMPTY, CellContent.EMPTY, CellContent.O}}; + CellContent[][] expectedState = new CellContent[][]{{CellContent.O, CellContent.X, CellContent.X}, + {CellContent.EMPTY, CellContent.O, CellContent.EMPTY}, + {CellContent.X, CellContent.EMPTY, CellContent.O}}; + checkStateAfterPlayerTurn(startingState, expectedState, 2, 0); + } + + @Test + public void testMakeTurnMediumAISecondDiagonalWin() throws IllegalAccessException { + CellContent[][] startingState = new CellContent[][]{{CellContent.X, CellContent.X, CellContent.O}, + {CellContent.EMPTY, CellContent.EMPTY, CellContent.EMPTY}, + {CellContent.O, CellContent.EMPTY, CellContent.EMPTY}}; + CellContent[][] expectedState = new CellContent[][]{{CellContent.X, CellContent.X, CellContent.O}, + {CellContent.X, CellContent.O, CellContent.EMPTY}, + {CellContent.O, CellContent.EMPTY, CellContent.EMPTY}}; + checkStateAfterPlayerTurn(startingState, expectedState, 1, 0); + } + + @Test + public void testMakeTurnPlayerHorizontalWin() throws IllegalAccessException { + CellContent[][] startingState = new CellContent[][]{{CellContent.X, CellContent.EMPTY, CellContent.X}, + {CellContent.O, CellContent.EMPTY, CellContent.O}, + {CellContent.EMPTY, CellContent.EMPTY, CellContent.EMPTY}}; + CellContent[][] expectedState = new CellContent[][]{{CellContent.X, CellContent.X, CellContent.X}, + {CellContent.O, CellContent.EMPTY, CellContent.O}, + {CellContent.EMPTY, CellContent.EMPTY, CellContent.EMPTY}}; + checkStateAfterPlayerTurn(startingState, expectedState, 0, 1); + } + + @Test + public void testMakeTurnPlayerVerticalWin() throws IllegalAccessException { + CellContent[][] startingState = new CellContent[][]{{CellContent.X, CellContent.EMPTY, CellContent.O}, + {CellContent.X, CellContent.EMPTY, CellContent.O}, + {CellContent.EMPTY, CellContent.EMPTY, CellContent.EMPTY}}; + CellContent[][] expectedState = new CellContent[][]{{CellContent.X, CellContent.EMPTY, CellContent.O}, + {CellContent.X, CellContent.EMPTY, CellContent.O}, + {CellContent.X, CellContent.EMPTY, CellContent.EMPTY}}; + checkStateAfterPlayerTurn(startingState, expectedState, 2, 0); + } + + @Test + public void testMakeTurnPlayerFirstDiagonalWin() throws IllegalAccessException { + CellContent[][] startingState = new CellContent[][]{{CellContent.X, CellContent.EMPTY, CellContent.O}, + {CellContent.EMPTY, CellContent.X, CellContent.O}, + {CellContent.EMPTY, CellContent.EMPTY, CellContent.EMPTY}}; + CellContent[][] expectedState = new CellContent[][]{{CellContent.X, CellContent.EMPTY, CellContent.O}, + {CellContent.EMPTY, CellContent.X, CellContent.O}, + {CellContent.EMPTY, CellContent.EMPTY, CellContent.X}}; + checkStateAfterPlayerTurn(startingState, expectedState, 2, 2); + } + + @Test + public void testMakeTurnPlayerSecondDiagonalWin() throws IllegalAccessException { + CellContent[][] startingState = new CellContent[][]{{CellContent.O, CellContent.EMPTY, CellContent.X}, + {CellContent.EMPTY, CellContent.X, CellContent.O}, + {CellContent.EMPTY, CellContent.EMPTY, CellContent.EMPTY}}; + CellContent[][] expectedState = new CellContent[][]{{CellContent.O, CellContent.EMPTY, CellContent.X}, + {CellContent.EMPTY, CellContent.X, CellContent.O}, + {CellContent.X, CellContent.EMPTY, CellContent.EMPTY}}; + checkStateAfterPlayerTurn(startingState, expectedState, 2, 0); + } + + private void checkEnemyFirstTurn(CellContent[][] boardState) { + int diff = 0; + for (int i = 0; i < boardState.length; i++) { + for (int j = 0; j < boardState.length; j++) { + if (!boardState[i][j].equals(CellContent.EMPTY) && !(i == 2 && j == 1)) { + assertEquals(CellContent.O, boardState[i][j]); + diff++; + } + } + } + + assertEquals(1, diff); + } + + private void checkStateAfterPlayerTurn(CellContent[][] staringState, CellContent[][] expectedState, int row, int col) throws IllegalAccessException { + Board board = new AIBoard(3, AILevel.MEDIUM); + boardField.set(board, staringState); + CellContent[][] actualState = board.move(row, col); + assertArrayEquals(expectedState, actualState); + } +} \ No newline at end of file diff --git a/Tic-tac-toe/src/test/java/ru/spbau/mit/kazakov/TicTacToe/AbstractBoardTest.java b/Tic-tac-toe/src/test/java/ru/spbau/mit/kazakov/TicTacToe/AbstractBoardTest.java new file mode 100644 index 0000000..03e1ba9 --- /dev/null +++ b/Tic-tac-toe/src/test/java/ru/spbau/mit/kazakov/TicTacToe/AbstractBoardTest.java @@ -0,0 +1,103 @@ +package ru.spbau.mit.kazakov.TicTacToe; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class AbstractBoardTest { + @Test + public void testGetStateInitial() { + AbstractBoard board = new HotSeatBoard(3); + assertEquals(State.RUNNING, board.getState()); + } + + @Test + public void testGetStateRunning() { + AbstractBoard board = new HotSeatBoard(3); + board.move(1, 1); + board.move(2, 0); + assertEquals(State.RUNNING, board.getState()); + } + + @Test + public void testGetStateHorizontalWin() { + AbstractBoard board = new HotSeatBoard(3); + board.move(0, 0); + board.move(1, 1); + board.move(2, 2); + board.move(0, 2); + board.move(2, 0); + board.move(1, 0); + board.move(2, 1); + assertEquals(State.X_WINS, board.getState()); + } + + @Test + public void testGetStateVerticalWin() { + AbstractBoard board = new HotSeatBoard(3); + board.move(0, 1); + board.move(0, 0); + board.move(1, 2); + board.move(2, 2); + board.move(0, 2); + board.move(2, 0); + board.move(2, 1); + board.move(1, 0); + assertEquals(State.O_WINS, board.getState()); + } + + @Test + public void testGetStateFirstDiagonalWin() { + AbstractBoard board = new HotSeatBoard(3); + board.move(0, 0); + board.move(0, 1); + board.move(1, 1); + board.move(0, 2); + board.move(2, 2); + assertEquals(State.X_WINS, board.getState()); + } + + @Test + public void testGetStateSecondDiagonalWin() { + AbstractBoard board = new HotSeatBoard(3); + board.move(0, 0); + board.move(0, 2); + board.move(0, 1); + board.move(1, 1); + board.move(2, 2); + board.move(2, 0); + assertEquals(State.O_WINS, board.getState()); + } + + @Test + public void testGetStateDraw() { + AbstractBoard board = new HotSeatBoard(3); + board.move(0, 0); + board.move(0, 1); + board.move(0, 2); + board.move(1, 1); + board.move(1, 0); + board.move(2, 0); + board.move(1, 2); + board.move(2, 2); + board.move(2, 1); + assertEquals(State.DRAW, board.getState()); + } + + @Test + public void testReset() { + AbstractBoard board = new HotSeatBoard(3); + board.move(0, 0); + board.move(0, 2); + board.move(0, 1); + board.move(1, 1); + board.move(2, 2); + board.move(2, 0); + board.reset(); + assertArrayEquals(new CellContent[][]{{CellContent.X, CellContent.EMPTY, CellContent.EMPTY}, + {CellContent.EMPTY, CellContent.EMPTY, CellContent.EMPTY}, + {CellContent.EMPTY, CellContent.EMPTY, CellContent.EMPTY}}, + board.move(0, 0)); + assertEquals(State.RUNNING, board.getState()); + } +} \ No newline at end of file diff --git a/Tic-tac-toe/src/test/java/ru/spbau/mit/kazakov/TicTacToe/HotSeatBoardTest.java b/Tic-tac-toe/src/test/java/ru/spbau/mit/kazakov/TicTacToe/HotSeatBoardTest.java new file mode 100644 index 0000000..79b8a6f --- /dev/null +++ b/Tic-tac-toe/src/test/java/ru/spbau/mit/kazakov/TicTacToe/HotSeatBoardTest.java @@ -0,0 +1,40 @@ +package ru.spbau.mit.kazakov.TicTacToe; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class HotSeatBoardTest { + @Test + public void testMakeTurnSimple() { + Board board = new HotSeatBoard(3); + CellContent[][] boardState = board.move(1, 0); + assertArrayEquals(new CellContent[][]{{CellContent.EMPTY, CellContent.EMPTY, CellContent.EMPTY}, {CellContent.X, CellContent.EMPTY, CellContent.EMPTY}, + {CellContent.EMPTY, CellContent.EMPTY, CellContent.EMPTY}}, boardState); + } + + @Test + public void testMakeTurnTwice() { + Board board = new HotSeatBoard(3); + board.move(1, 1); + CellContent[][] boardState = board.move(2, 0); + assertArrayEquals(new CellContent[][]{{CellContent.EMPTY, CellContent.EMPTY, CellContent.EMPTY}, {CellContent.EMPTY, CellContent.X, CellContent.EMPTY}, + {CellContent.O, CellContent.EMPTY, CellContent.EMPTY}}, boardState); + } + + @Test + public void testMakeTurn() { + Board board = new HotSeatBoard(3); + board.move(0, 0); + board.move(1, 1); + board.move(2, 2); + board.move(0, 2); + board.move(2, 0); + board.move(1, 0); + CellContent[][] boardState = board.move(2, 1); + + assertArrayEquals(new CellContent[][]{{CellContent.X, CellContent.EMPTY, CellContent.O}, + {CellContent.O, CellContent.O, CellContent.EMPTY}, + {CellContent.X, CellContent.X, CellContent.X}}, boardState); + } +} \ No newline at end of file