diff --git a/.travis.yml b/.travis.yml index aac6a7a..da630d3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,4 +3,7 @@ language: java sudo: false notifications: - email: false \ No newline at end of file + email: false +env: + - TEST_DIR=TicTacToe +script: cd $TEST_DIR && ./gradlew clean check \ No newline at end of file diff --git a/TicTacToe/build.gradle b/TicTacToe/build.gradle new file mode 100644 index 0000000..c73fa91 --- /dev/null +++ b/TicTacToe/build.gradle @@ -0,0 +1,14 @@ +group 'ru.spbau.dkaznacheev' +version '1.0-SNAPSHOT' + +apply plugin: 'java' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + testCompile group: 'junit', name: 'junit', version: '4.12' +} diff --git a/TicTacToe/gradle/wrapper/gradle-wrapper.jar b/TicTacToe/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..01b8bf6 Binary files /dev/null and b/TicTacToe/gradle/wrapper/gradle-wrapper.jar differ diff --git a/TicTacToe/gradle/wrapper/gradle-wrapper.properties b/TicTacToe/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..933b647 --- /dev/null +++ b/TicTacToe/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-bin.zip diff --git a/TicTacToe/gradlew b/TicTacToe/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/TicTacToe/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/TicTacToe/gradlew.bat b/TicTacToe/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/TicTacToe/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/TicTacToe/settings.gradle b/TicTacToe/settings.gradle new file mode 100644 index 0000000..eabddd0 --- /dev/null +++ b/TicTacToe/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'TicTacToe' + diff --git a/TicTacToe/src/main/java/ru/spbau/dkaznacheev/Board.java b/TicTacToe/src/main/java/ru/spbau/dkaznacheev/Board.java new file mode 100644 index 0000000..a4e84e8 --- /dev/null +++ b/TicTacToe/src/main/java/ru/spbau/dkaznacheev/Board.java @@ -0,0 +1,20 @@ +package ru.spbau.dkaznacheev; + +public class Board { + private BoardState[][] board; + + public Board(int size) { + this.board = new BoardState[size][size]; + for (int i = 0; i < size; i++) + for (int j = 0; j < size; j++) + board[i][j] = BoardState.STATE_NONE; + } + + public BoardState get(int row, int column) { + return board[row][column]; + } + + public void set(int row, int column, BoardState state) { + board[row][column] = state; + } +} diff --git a/TicTacToe/src/main/java/ru/spbau/dkaznacheev/BoardState.java b/TicTacToe/src/main/java/ru/spbau/dkaznacheev/BoardState.java new file mode 100644 index 0000000..54c8ace --- /dev/null +++ b/TicTacToe/src/main/java/ru/spbau/dkaznacheev/BoardState.java @@ -0,0 +1,23 @@ +package ru.spbau.dkaznacheev; + +/** + * Enum for board states. + */ +public enum BoardState { + + /** + * Board cell has X on it + */ + STATE_X, + + + /** + * Board cell has Y on it + */ + STATE_O, + + /** + * Board cell has nothing on it + */ + STATE_NONE +} diff --git a/TicTacToe/src/main/java/ru/spbau/dkaznacheev/Bot.java b/TicTacToe/src/main/java/ru/spbau/dkaznacheev/Bot.java new file mode 100644 index 0000000..7685faf --- /dev/null +++ b/TicTacToe/src/main/java/ru/spbau/dkaznacheev/Bot.java @@ -0,0 +1,14 @@ +package ru.spbau.dkaznacheev; + +/** + * A bot interface capable of making turns. + * Currently bot only plays as O. + */ +public interface Bot { + /** + * Make a turn on a board. + * @param board current board + * @return point where the turn is made + */ + Point makeTurn(Board board); +} diff --git a/TicTacToe/src/main/java/ru/spbau/dkaznacheev/EasyBot.java b/TicTacToe/src/main/java/ru/spbau/dkaznacheev/EasyBot.java new file mode 100644 index 0000000..b24ad7d --- /dev/null +++ b/TicTacToe/src/main/java/ru/spbau/dkaznacheev/EasyBot.java @@ -0,0 +1,22 @@ +package ru.spbau.dkaznacheev; + +import java.util.Random; + +import static ru.spbau.dkaznacheev.Model.BOARD_SIZE; + +/** + * Easy bot that makes its turns randomly. + */ +public class EasyBot implements Bot { + private final Random random = new Random(); + + @Override + public Point makeTurn(Board board) { + int row, column; + do { + row = Math.abs(random.nextInt()) % BOARD_SIZE; + column = Math.abs(random.nextInt()) % BOARD_SIZE; + } while (board.get(row, column) != BoardState.STATE_NONE); + return new Point(row, column); + } +} diff --git a/TicTacToe/src/main/java/ru/spbau/dkaznacheev/GameController.java b/TicTacToe/src/main/java/ru/spbau/dkaznacheev/GameController.java new file mode 100644 index 0000000..b659465 --- /dev/null +++ b/TicTacToe/src/main/java/ru/spbau/dkaznacheev/GameController.java @@ -0,0 +1,181 @@ +package ru.spbau.dkaznacheev; + +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Node; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.text.Text; +import javafx.stage.Stage; + +import java.io.IOException; + +import static ru.spbau.dkaznacheev.Log.logGame; +import static ru.spbau.dkaznacheev.Model.BOARD_SIZE; + +/** + * Game controller class. + */ +public class GameController { + + /** + * Game logic model object. + */ + private Model model; + + /** + * Type of the game. + */ + private GameType gameType; + + @FXML + private Text label; + @FXML + private Button button00; + @FXML + private Button button01; + @FXML + private Button button02; + @FXML + private Button button10; + @FXML + private Button button11; + @FXML + private Button button12; + @FXML + private Button button20; + @FXML + private Button button21; + @FXML + private Button button22; + + private Button[][] buttons; + + public GameController(GameType gameType) { + this.gameType = gameType; + } + + @FXML + public void initialize() { + model = new Model(gameType); + buttons = new Button[BOARD_SIZE][BOARD_SIZE]; + buttons[0][0] = button00; + buttons[0][1] = button01; + buttons[0][2] = button02; + buttons[1][0] = button10; + buttons[1][1] = button11; + buttons[1][2] = button12; + buttons[2][0] = button20; + buttons[2][1] = button21; + buttons[2][2] = button22; + + } + + /** + * Processes a click on a button. + * @param row row of buttons + * @param column column of buttons + */ + public void processTurn(int row, int column) { + PlayerType winner = model.makeTurn(row, column); + if (winner == null) + return; + if (winner != PlayerType.NOBODY) { + logGame(winner, gameType); + } + if (winner == PlayerType.PLAYER_X) { + label.setText("X won!"); + } else if (winner == PlayerType.PLAYER_O) { + label.setText("O won!"); + } else if (winner == PlayerType.DRAW) { + label.setText("Draw!"); + } else if (model.getCurrentPlayer() == PlayerType.PLAYER_X) { + label.setText("X turn"); + } else if (model.getCurrentPlayer() == PlayerType.PLAYER_O) { + label.setText("O turn"); + } + updateBoard(); + } + + /** + * Updates the board buttons. + */ + private void updateBoard() { + Board board = model.getBoard(); + for (int i = 0; i < BOARD_SIZE; i++) + for (int j = 0; j < BOARD_SIZE; j++) { + if (board.get(i, j) == BoardState.STATE_X) { + buttons[i][j].setText("X"); + } else if (board.get(i, j) == BoardState.STATE_O) { + buttons[i][j].setText("O"); + } else { + buttons[i][j].setText(""); + } + } + } + + @FXML + public void handle00Action(ActionEvent actionEvent) { + processTurn(0, 0); + } + + @FXML + public void handle01Action(ActionEvent actionEvent) { + processTurn(0, 1); + } + + @FXML + public void handle02Action(ActionEvent actionEvent) { + processTurn(0, 2); + } + + @FXML + public void handle10Action(ActionEvent actionEvent) { + processTurn(1, 0); + } + + @FXML + public void handle11Action(ActionEvent actionEvent) { + processTurn(1, 1); + } + + @FXML + public void handle12Action(ActionEvent actionEvent) { + processTurn(1, 2); + } + + @FXML + public void handle20Action(ActionEvent actionEvent) { + processTurn(2, 0); + } + + @FXML + public void handle21Action(ActionEvent actionEvent) { + processTurn(2, 1); + } + + @FXML + public void handle22Action(ActionEvent actionEvent) { + processTurn(2, 2); + } + + @FXML + public void handleNewGameAction(ActionEvent actionEvent) { + model.reset(); + updateBoard(); + } + + @FXML + public void handleBackAction(ActionEvent actionEvent) { + Stage stage = (Stage) ((Node)actionEvent.getSource()).getScene().getWindow(); + try { + FXMLLoader loader = new FXMLLoader(getClass().getResource("start.fxml")); + Parent root = loader.load(); + stage.setScene(new Scene(root, 650, 650)); + } catch (IOException e) { + System.err.println("IO error!"); + } + } +} diff --git a/TicTacToe/src/main/java/ru/spbau/dkaznacheev/GameInfo.java b/TicTacToe/src/main/java/ru/spbau/dkaznacheev/GameInfo.java new file mode 100644 index 0000000..6cf4e4a --- /dev/null +++ b/TicTacToe/src/main/java/ru/spbau/dkaznacheev/GameInfo.java @@ -0,0 +1,22 @@ +package ru.spbau.dkaznacheev; + +/** + * An info storage for the logger. + */ +public class GameInfo { + + /** + * Type of game. + */ + public final GameType gameType; + + /** + * Who won the game. + */ + public final PlayerType result; + + public GameInfo(GameType gameType, PlayerType result) { + this.gameType = gameType; + this.result = result; + } +} diff --git a/TicTacToe/src/main/java/ru/spbau/dkaznacheev/GameType.java b/TicTacToe/src/main/java/ru/spbau/dkaznacheev/GameType.java new file mode 100644 index 0000000..f0b1b7e --- /dev/null +++ b/TicTacToe/src/main/java/ru/spbau/dkaznacheev/GameType.java @@ -0,0 +1,22 @@ +package ru.spbau.dkaznacheev; + +/** + * Enum for game types. + */ +public enum GameType { + + /** + * Game with another player. + */ + MULTIPLAYER, + + /** + * Game with easy bot. + */ + BOT_EASY, + + /** + * Game with hard bot. + */ + BOT_HARD +} diff --git a/TicTacToe/src/main/java/ru/spbau/dkaznacheev/HardBot.java b/TicTacToe/src/main/java/ru/spbau/dkaznacheev/HardBot.java new file mode 100644 index 0000000..da0fc91 --- /dev/null +++ b/TicTacToe/src/main/java/ru/spbau/dkaznacheev/HardBot.java @@ -0,0 +1,109 @@ +package ru.spbau.dkaznacheev; + +import java.util.Random; + +import static ru.spbau.dkaznacheev.Model.BOARD_SIZE; + +/** + * An implementation of a bot that almost never loses. + */ +public class HardBot implements Bot { + + /** + * If it is the first turn. + */ + private boolean firstTurn = true; + + private Random random = new Random(); + + /** + * Finds a point on the board that ends the game. + * @param board current board + * @return point that we have to put an O to, null if there is none + */ + private Point winning(Board board) { + Point point; + for (int i = 0; i < BOARD_SIZE; i++) { + point = twoSameAndEmpty( + board, + new Point(i, 0), + new Point(i, 1), + new Point(i, 2)); + if (point != null) { + return point; + } + point = twoSameAndEmpty( + board, + new Point(0, i), + new Point(1, i), + new Point(2, i)); + if (point != null) { + return point; + } + } + point = twoSameAndEmpty( + board, + new Point(0, 0), + new Point(1, 1), + new Point(2, 2)); + if (point != null) { + return point; + } + point = twoSameAndEmpty( + board, + new Point(0, 2), + new Point(1, 1), + new Point(2, 0)); + return point; + } + + /** + * Checks if two of the three points are of the same non-empty type and the third is empty. + * @param board current board + * @param point0 first point + * @param point1 second point + * @param point2 third point + * @return empty point, null if conditions are not met + */ + private Point twoSameAndEmpty(Board board, Point point0, Point point1, Point point2) { + if ( board.get(point0.row, point0.column) == BoardState.STATE_NONE + && board.get(point1.row, point1.column) == board.get(point2.row, point2.column) + && board.get(point1.row, point1.column) != BoardState.STATE_NONE ) { + return point0; + } + if ( board.get(point1.row, point1.column) == BoardState.STATE_NONE + && board.get(point0.row, point0.column) == board.get(point2.row, point2.column) + && board.get(point0.row, point0.column) != BoardState.STATE_NONE ) { + return point1; + } + if ( board.get(point2.row, point2.column) == BoardState.STATE_NONE + && board.get(point1.row, point1.column) == board.get(point0.row, point0.column) + && board.get(point1.row, point1.column) != BoardState.STATE_NONE ) { + return point2; + } + return null; + } + + @Override + public Point makeTurn(Board board) { + if (firstTurn) { + firstTurn = false; + if (board.get(1, 1) != BoardState.STATE_NONE) { + return new Point(0, 0); + } else { + return new Point(1, 1); + } + } + Point winning = winning(board); + if (winning != null) { + return winning; + } else { + int row, column; + do { + row = Math.abs(random.nextInt()) % BOARD_SIZE; + column = Math.abs(random.nextInt()) % BOARD_SIZE; + } while (board.get(row, column) != BoardState.STATE_NONE); + return new Point(row, column); + } + } +} diff --git a/TicTacToe/src/main/java/ru/spbau/dkaznacheev/Log.java b/TicTacToe/src/main/java/ru/spbau/dkaznacheev/Log.java new file mode 100644 index 0000000..d076361 --- /dev/null +++ b/TicTacToe/src/main/java/ru/spbau/dkaznacheev/Log.java @@ -0,0 +1,65 @@ +package ru.spbau.dkaznacheev; + +import java.util.LinkedList; +import java.util.List; +import java.util.StringJoiner; + +/** + * Static class for logging. + */ +public class Log { + + /** + * A log where GameInfos are stored. + */ + private static List log = new LinkedList<>(); + + /** + * Template string for formatting. + */ + private static final String template = "%TYPE% ended with %RESULT%"; + + /** + * Adds a game to the log. + * @param result result of the game + * @param gameType type of the game + */ + public static void logGame(PlayerType result, GameType gameType) { + log.add(new GameInfo(gameType, result)); + } + + /** + * Pretty prints the log. + * @return log as a string + */ + public static String getLog() { + if (log.isEmpty()) { + return "No games found"; + } + StringJoiner joiner = new StringJoiner("\n"); + String gameTypeString = ""; + String resultString = ""; + for (GameInfo info : log) { + if (info.gameType == GameType.MULTIPLAYER) { + gameTypeString = "Multiplayer game"; + } else if (info.gameType == GameType.BOT_EASY) { + gameTypeString = "Game versus easy bot"; + } else { + gameTypeString = "Game versus hard bot"; + } + + if (info.result == PlayerType.DRAW) { + resultString = "a draw"; + } else if (info.result == PlayerType.PLAYER_X) { + resultString = "X victory"; + } else { + resultString = "O victory"; + } + joiner.add( + template.replace("%TYPE%", gameTypeString) + .replace("%RESULT%", resultString)); + } + return joiner.toString(); + } + +} diff --git a/TicTacToe/src/main/java/ru/spbau/dkaznacheev/Main.java b/TicTacToe/src/main/java/ru/spbau/dkaznacheev/Main.java new file mode 100644 index 0000000..dabc1c6 --- /dev/null +++ b/TicTacToe/src/main/java/ru/spbau/dkaznacheev/Main.java @@ -0,0 +1,22 @@ +package ru.spbau.dkaznacheev; + +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Stage; + +public class Main extends Application { + + @Override + public void start(Stage primaryStage) throws Exception{ + Parent root = FXMLLoader.load(getClass().getResource("start.fxml")); + primaryStage.setTitle("TicTacToe"); + primaryStage.setScene(new Scene(root, 650, 650)); + primaryStage.show(); + } + + public static void main(String[] args) { + launch(args); + } +} diff --git a/TicTacToe/src/main/java/ru/spbau/dkaznacheev/Model.java b/TicTacToe/src/main/java/ru/spbau/dkaznacheev/Model.java new file mode 100644 index 0000000..7d3bfcf --- /dev/null +++ b/TicTacToe/src/main/java/ru/spbau/dkaznacheev/Model.java @@ -0,0 +1,159 @@ +package ru.spbau.dkaznacheev; + +/** + * Game logic model. + */ +public class Model { + + /** + * Size of the board. + */ + public static final int BOARD_SIZE = 3; + + /** + * Game board. + */ + private Board board; + + /** + * Type of the game. + */ + private final GameType gameType; + + /** + * A bot that plays the game. + */ + private Bot bot; + + /** + * Whether the game has ended. + */ + private boolean gameWon; + + /** + * Returns current player. + * @return current player + */ + public PlayerType getCurrentPlayer() { + return currentPlayer; + } + + /** + * Current player. + */ + private PlayerType currentPlayer = PlayerType.PLAYER_X; + + /** + * Returns board. + * @return board + */ + public Board getBoard() { + return board; + } + + public Model(GameType gameType) { + this.gameType = gameType; + reset(); + } + + /** + * Resets the game. + */ + public void reset() { + currentPlayer = PlayerType.PLAYER_X; + gameWon = false; + this.board = new Board(BOARD_SIZE); + + if (gameType == GameType.BOT_EASY) { + bot = new EasyBot(); + } + if (gameType == GameType.BOT_HARD) { + bot = new HardBot(); + } + } + + /** + * Processes a turn. + * @param row row of a turn + * @param column column of a turn + * @return winning player + */ + public PlayerType makeTurn(int row, int column) { + if (gameWon) { + return null; + } + if (board.get(row, column) != BoardState.STATE_NONE) { + return null; + } + PlayerType winner; + if (gameType == GameType.MULTIPLAYER) { + if (currentPlayer == PlayerType.PLAYER_X) { + board.set(row, column, BoardState.STATE_X); + currentPlayer = PlayerType.PLAYER_O; + } else { + board.set(row, column, BoardState.STATE_O); + currentPlayer = PlayerType.PLAYER_X; + } + } else { + board.set(row, column, BoardState.STATE_X); + winner = checkWinner(); + if (winner != PlayerType.NOBODY) { + gameWon = true; + return winner; + } + Point turn = bot.makeTurn(board); + board.set(turn.row, turn.column, BoardState.STATE_O); + } + winner = checkWinner(); + if (winner != PlayerType.NOBODY) { + gameWon = true; + } + return winner; + } + + /** + * Checks who won. + * @return winning player + */ + private PlayerType checkWinner() { + for (int i = 0; i < BOARD_SIZE; i++) { + if (board.get(i, 0) == board.get(i, 1) + && board.get(i, 1) == board.get(i, 2)) { + if (board.get(i, 0) == BoardState.STATE_X) + return PlayerType.PLAYER_X; + if (board.get(i, 0) == BoardState.STATE_O) + return PlayerType.PLAYER_O; + } + if (board.get(0, i) == board.get(1, i) + && board.get(1, i) == board.get(2, i)) { + if (board.get(0, i) == BoardState.STATE_X) + return PlayerType.PLAYER_X; + if (board.get(0, i) == BoardState.STATE_O) + return PlayerType.PLAYER_O; + } + } + if (board.get(0, 0) == board.get(1, 1) + && board.get(1, 1) == board.get(2, 2)) { + if (board.get(0, 0) == BoardState.STATE_X) + return PlayerType.PLAYER_X; + if (board.get(0, 0) == BoardState.STATE_O) + return PlayerType.PLAYER_O; + } + if (board.get(0, 2) == board.get(1, 1) + && board.get(1, 1) == board.get(2, 0)) { + if (board.get(0, 2) == BoardState.STATE_X) + return PlayerType.PLAYER_X; + if (board.get(0, 2) == BoardState.STATE_O) + return PlayerType.PLAYER_O; + } + for (int i = 0; i < BOARD_SIZE; i++) { + for (int j = 0; j < BOARD_SIZE; j++) { + if (board.get(i, j) == BoardState.STATE_NONE) { + return PlayerType.NOBODY; + } + } + } + + return PlayerType.DRAW; + } +} diff --git a/TicTacToe/src/main/java/ru/spbau/dkaznacheev/PlayerType.java b/TicTacToe/src/main/java/ru/spbau/dkaznacheev/PlayerType.java new file mode 100644 index 0000000..2f13b1d --- /dev/null +++ b/TicTacToe/src/main/java/ru/spbau/dkaznacheev/PlayerType.java @@ -0,0 +1,27 @@ +package ru.spbau.dkaznacheev; + +/** + * Enum for player types and game result. + */ +public enum PlayerType { + + /** + * X player. + */ + PLAYER_X, + + /** + * O player. + */ + PLAYER_O, + + /** + * Nobody won: the game is not over yet. + */ + NOBODY, + + /** + * Game ended in a draw. + */ + DRAW +} diff --git a/TicTacToe/src/main/java/ru/spbau/dkaznacheev/Point.java b/TicTacToe/src/main/java/ru/spbau/dkaznacheev/Point.java new file mode 100644 index 0000000..4b0f201 --- /dev/null +++ b/TicTacToe/src/main/java/ru/spbau/dkaznacheev/Point.java @@ -0,0 +1,21 @@ +package ru.spbau.dkaznacheev; + +/** + * Bidimensional point on the board. + */ +public class Point { + /** + * Row. + */ + public final int row; + + /** + * Column. + */ + public final int column; + + public Point(int row, int column) { + this.row = row; + this.column = column; + } +} diff --git a/TicTacToe/src/main/java/ru/spbau/dkaznacheev/StartController.java b/TicTacToe/src/main/java/ru/spbau/dkaznacheev/StartController.java new file mode 100644 index 0000000..cc7c780 --- /dev/null +++ b/TicTacToe/src/main/java/ru/spbau/dkaznacheev/StartController.java @@ -0,0 +1,52 @@ +package ru.spbau.dkaznacheev; + +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Node; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.control.Alert; +import javafx.stage.Stage; + +import java.io.IOException; + +import static ru.spbau.dkaznacheev.Log.getLog; + +public class StartController { + @FXML + private void handleMultiplayerButtonAction(ActionEvent actionEvent) { + launchGame(GameType.MULTIPLAYER, actionEvent); + } + + @FXML + public void handleHardBotButtonAction(ActionEvent actionEvent) { + launchGame(GameType.BOT_HARD, actionEvent); + } + + @FXML + public void handleEasyBotButtonAction(ActionEvent actionEvent) { + launchGame(GameType.BOT_EASY, actionEvent); + } + + private void launchGame(GameType gameType, ActionEvent actionEvent) { + Stage stage = (Stage) ((Node)actionEvent.getSource()).getScene().getWindow(); + try { + FXMLLoader loader = new FXMLLoader(getClass().getResource("game.fxml")); + GameController controller = new GameController(gameType); + loader.setController(controller); + Parent root = loader.load(); + stage.setScene(new Scene(root, 500, 500)); + } catch (IOException e) { + System.err.println("IO error!"); + } + } + + public void handleLogButtonAction(ActionEvent actionEvent) { + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setTitle("Log"); + alert.setHeaderText(null); + alert.setContentText(getLog()); + alert.showAndWait(); + } +} diff --git a/TicTacToe/src/main/java/ru/spbau/dkaznacheev/game.fxml b/TicTacToe/src/main/java/ru/spbau/dkaznacheev/game.fxml new file mode 100644 index 0000000..c9a51f6 --- /dev/null +++ b/TicTacToe/src/main/java/ru/spbau/dkaznacheev/game.fxml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + diff --git a/TicTacToe/src/test/java/ru/spbau/dkaznacheev/ModelTest.java b/TicTacToe/src/test/java/ru/spbau/dkaznacheev/ModelTest.java new file mode 100644 index 0000000..cc0f3d8 --- /dev/null +++ b/TicTacToe/src/test/java/ru/spbau/dkaznacheev/ModelTest.java @@ -0,0 +1,62 @@ +package ru.spbau.dkaznacheev; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class ModelTest { + + @Test + public void simpleMultiplayerGameWorks() { + Model model = new Model(GameType.MULTIPLAYER); + model.makeTurn(1,1); + model.makeTurn(1,2); + model.makeTurn(0,1); + model.makeTurn(1,0); + PlayerType result = model.makeTurn(2,1); + assertEquals(PlayerType.PLAYER_X, result); + } + + @Test + public void turnOnNonEmptyFails() { + Model model = new Model(GameType.MULTIPLAYER); + model.makeTurn(1,1); + model.makeTurn(1,1); + assertEquals(BoardState.STATE_X, model.getBoard().get(1,1 )); + assertEquals(PlayerType.PLAYER_O, model.getCurrentPlayer()); + } + + @Test + public void gameEndsInADraw() { + Model model = new Model(GameType.MULTIPLAYER); + assertEquals(PlayerType.NOBODY, model.makeTurn(1,1)); + model.makeTurn(2,2); + model.makeTurn(2,1); + model.makeTurn(0,1); + model.makeTurn(0,2); + model.makeTurn(2, 0); + model.makeTurn(1, 0); + assertEquals(PlayerType.NOBODY, model.makeTurn(1,2)); + PlayerType result = model.makeTurn(0,0); + assertEquals(PlayerType.DRAW, result); + } + + @Test + public void hardBotDraws() { + Model model = new Model(GameType.BOT_HARD); + model.makeTurn(1,1); + model.makeTurn(2,0); + model.makeTurn(0,1); + model.makeTurn(1,0); + PlayerType result = model.makeTurn(2,2); + assertEquals(PlayerType.DRAW, result); + } + + @Test + public void hardBotPreventsLoss() { + Model model = new Model(GameType.BOT_HARD); + model.makeTurn(1, 1); + model.makeTurn(1, 2); + assertEquals(BoardState.STATE_O, model.getBoard().get(1, 0)); + } +}