diff --git a/FTP/build.gradle b/FTP/build.gradle
new file mode 100644
index 0000000..b1a9381
--- /dev/null
+++ b/FTP/build.gradle
@@ -0,0 +1,26 @@
+plugins {
+ id 'java-library'
+}
+
+dependencies {
+ api 'org.apache.commons:commons-math3:3.6.1'
+
+ implementation 'com.google.guava:guava:23.0'
+
+ compile group: 'org.jetbrains', name: 'annotations', version: '15.0'
+ compile group: 'commons-io', name: 'commons-io', version: '2.6'
+
+ testCompile group: 'org.hamcrest', name: 'hamcrest-all', version: '1.3'
+ testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.1.0'
+ testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: '5.0.0-M4'
+
+ testImplementation 'junit:junit:4.12'
+}
+
+repositories {
+ jcenter()
+}
+task wrapper(type: Wrapper) {
+ description = 'Generates gradlew[.bat] scripts'
+ gradleVersion = '4.6'
+}
diff --git a/FTP/gradle/wrapper/gradle-wrapper.jar b/FTP/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..01b8bf6
Binary files /dev/null and b/FTP/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/FTP/gradle/wrapper/gradle-wrapper.properties b/FTP/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..88f8762
--- /dev/null
+++ b/FTP/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sun Apr 22 23:21:38 MSK 2018
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
diff --git a/FTP/gradlew b/FTP/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/FTP/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/FTP/gradlew.bat b/FTP/gradlew.bat
new file mode 100644
index 0000000..f955316
--- /dev/null
+++ b/FTP/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/FTP/settings.gradle b/FTP/settings.gradle
new file mode 100644
index 0000000..8af3acb
--- /dev/null
+++ b/FTP/settings.gradle
@@ -0,0 +1,2 @@
+rootProject.name = 'FTP'
+
diff --git a/FTP/src/main/java/FTPClient.java b/FTP/src/main/java/FTPClient.java
new file mode 100644
index 0000000..d704998
--- /dev/null
+++ b/FTP/src/main/java/FTPClient.java
@@ -0,0 +1,101 @@
+import org.jetbrains.annotations.NotNull;
+
+import java.io.*;
+import java.net.Socket;
+
+/**
+ * This class provides methods to work with FTPServer. These are:
+ *
+ * {@link FTPClient#list(String)} -- prints all files in the target directory.
+ *
+ * {@link FTPClient#get(String, String)} -- downloads and saves file from the
+ * server to the target file.
+ */
+@SuppressWarnings({"unused", "WeakerAccess"})
+public class FTPClient {
+
+ private final static int BUF_SIZE = 2048;
+
+ @NotNull
+ final private DataOutputStream os;
+ @NotNull
+ final private DataInputStream is;
+
+ /**
+ * Creates new client, that connected to the FTPServer.
+ *
+ * @param url servers url.
+ * @param port port to connect.
+ * @throws IOException if any IOException occurred.
+ */
+ public FTPClient(final String url, final int port) throws IOException {
+ final Socket socket = new Socket(url, port);
+ os = new DataOutputStream(socket.getOutputStream());
+ is = new DataInputStream(socket.getInputStream());
+ }
+
+ /**
+ * Creates new client, that connected to the FTPServer.
+ *
+ * @param url servers url.
+ * @throws IOException if any IOException occurred.
+ */
+ public FTPClient(final String url) throws IOException {
+ this(url, FTPServer.DEFAULT_PORT);
+ }
+
+ /**
+ * Prints list of all files (including folders), inside the source folder.
+ *
+ * Also for every file prints whether the file is a directory or not.
+ *
+ * @param source target folder.
+ * @throws IOException if any IOException occurred.
+ */
+ public void list(@NotNull final String source) throws IOException {
+ os.writeInt(1);
+ os.writeUTF(source);
+ os.flush();
+
+ final int number = is.readInt();
+ System.out.println("Total number of files: " + number);
+ for (int i = 0; i < number; i++) {
+ final String name = is.readUTF();
+ final boolean isFolder = is.readBoolean();
+ final String isFolderString = isFolder ? "is a folder." : "is not a folder.";
+ System.out.println("\"" + name + "\" " + isFolderString);
+ }
+ }
+
+ /**
+ * Downloads file from the server to the file with specified name. If file with
+ * the given name already exists, FileAlreadyExistsException will be thrown.
+ *
+ * @param source target file.
+ * @param destination path, to save file.
+ * @throws IOException if any IOException occurred.
+ */
+ public void get(@NotNull final String source, @NotNull final String destination) throws IOException {
+ os.writeInt(2);
+ os.writeUTF(source);
+ os.flush();
+
+ final File file = new File(destination);
+ if (!file.createNewFile()) {
+ throw new FileAlreadyExistsException();
+ }
+ final byte[] buf = new byte[BUF_SIZE];
+ try (final FileOutputStream os = new FileOutputStream(file)) {
+ final long size = is.readLong();
+ int readSize = 0;
+ int bytesRead;
+ while (readSize != size) {
+ bytesRead = is.read(buf);
+ readSize += bytesRead;
+ os.write(buf, 0, bytesRead);
+ }
+ }
+ }
+
+ private class FileAlreadyExistsException extends RuntimeException {}
+}
diff --git a/FTP/src/main/java/FTPServer.java b/FTP/src/main/java/FTPServer.java
new file mode 100644
index 0000000..ca54544
--- /dev/null
+++ b/FTP/src/main/java/FTPServer.java
@@ -0,0 +1,62 @@
+import abstractServer.AbstractBlockingServer;
+import abstractServer.Session;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.net.Socket;
+
+/**
+ * FTPServer class. Has factory init method and shutDown method. Also it is possible
+ * to track mistakes, that happened during server work.
+ *
+ * Server may accept a lot of clients and work with them in distinct thread by {@link FTPSession}
+ * class. In fact all possibilities of server are described in {@link FTPSession}.
+ *
+ * Server will shut down if any error occurred during it's work (not just in one created session).
+ **/
+@SuppressWarnings("unused")
+public class FTPServer extends AbstractBlockingServer {
+
+ @SuppressWarnings("WeakerAccess")
+ public final static int DEFAULT_PORT = 9995;
+
+ private FTPServer(final int port) throws IOException {
+ super(port);
+ }
+
+ private FTPServer() throws IOException {
+ this(DEFAULT_PORT);
+ }
+
+ @NotNull
+ @Override
+ protected Session newSession(final Socket socket, final int number) {
+ return new FTPSession(socket, number, this);
+ }
+
+ /**
+ * Creates new FTPServer on {@link FTPServer#DEFAULT_PORT} port.
+ *
+ * @return new FTPServer on default port.
+ * @throws IOException if IOException happened while creating new server.
+ */
+ @SuppressWarnings({"WeakerAccess", "UnusedReturnValue"})
+ public static FTPServer init() throws IOException {
+ return new FTPServer();
+ }
+
+ /**
+ * Creates new FTPServer on target port.
+ *
+ * @param port port to create server on.
+ * @return new FTPServer on default port.
+ * @throws IOException if IOException happened while creating new server.
+ */
+ public static FTPServer init(final int port) throws IOException {
+ return new FTPServer(port);
+ }
+
+ public static void main(final String[] args) throws IOException {
+ init();
+ }
+}
diff --git a/FTP/src/main/java/FTPSession.java b/FTP/src/main/java/FTPSession.java
new file mode 100644
index 0000000..08c7e3d
--- /dev/null
+++ b/FTP/src/main/java/FTPSession.java
@@ -0,0 +1,82 @@
+import abstractServer.AbstractBlockingSession;
+import abstractServer.Server;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.*;
+import java.net.Socket;
+import java.util.Objects;
+
+class FTPSession extends AbstractBlockingSession {
+
+ private static final int BUF_SIZE = 2048;
+
+ FTPSession(@NotNull final Socket socket, final int id, final Server server) {
+ super(socket, id, server);
+ }
+
+ @Override
+ protected void processInput(@NotNull final DataInputStream is) throws IOException {
+ while (true) {
+ final int query = is.readInt();
+ final String path = is.readUTF();
+ switch (query) {
+ case 1:
+ sendList(path);
+ break;
+ case 2:
+ sendFile(path);
+ break;
+ case 3:
+ closeSession(null);
+ return;
+ default:
+ // do nothing, unknown command
+ }
+ }
+ }
+
+ private void sendList(@NotNull final String path) {
+ try (final ByteArrayOutputStream bos = new ByteArrayOutputStream(BUF_SIZE);
+ final DataOutputStream os = new DataOutputStream(bos)) {
+
+ final File target = new File(path);
+ if (!target.isDirectory()) {
+ os.writeInt(0);
+ } else {
+ os.writeInt(Objects.requireNonNull(target.listFiles()).length);
+ for (final File file : Objects.requireNonNull(target.listFiles())) {
+ os.writeUTF(file.getName());
+ os.writeBoolean(file.isDirectory());
+ }
+ }
+ sendToClient(bos.toByteArray());
+
+ } catch (@NotNull final IOException e) {
+ closeSession(e);
+ }
+ }
+
+ private void sendFile(@NotNull final String path) {
+ final File target = new File(path);
+ if (!target.isFile() || !target.canRead()) {
+ sendToClient(0);
+ return;
+ }
+ sendToClient(target.length());
+ final byte[] buf = new byte[BUF_SIZE];
+ try (final InputStream is = new FileInputStream(target)) {
+
+ int bytesRead;
+ while (true) {
+ bytesRead = is.read(buf);
+ if (bytesRead == -1) {
+ break;
+ }
+ sendToClient(buf, 0, bytesRead);
+ }
+
+ } catch (@NotNull final IOException e) {
+ closeSession(e);
+ }
+ }
+}
diff --git a/FTP/src/main/java/abstractServer/AbstractBlockingServer.java b/FTP/src/main/java/abstractServer/AbstractBlockingServer.java
new file mode 100644
index 0000000..b57392a
--- /dev/null
+++ b/FTP/src/main/java/abstractServer/AbstractBlockingServer.java
@@ -0,0 +1,142 @@
+package abstractServer;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.util.*;
+
+@SuppressWarnings("unused")
+public abstract class AbstractBlockingServer implements Server {
+
+ @NotNull
+ private final ServerSocket server;
+ private final int port;
+
+ @NotNull
+ private final Thread mainThread;
+
+ private volatile boolean ceaseWorking;
+ private volatile boolean serverClosed;
+
+ private final Set sessions = new TreeSet<>();
+ private int sessionsProcessed = 0;
+ private final List errors = new ArrayList<>();
+
+ protected AbstractBlockingServer(final int port) throws IOException {
+ server = new ServerSocket(port);
+ this.port = port;
+ mainThread = new Thread(this::work);
+ mainThread.start();
+ }
+
+ /**
+ * The method shuts down server.
+ *
+ * Server will not accept connections anymore after this method was executed.
+ * Every present session will be closed.
+ *
+ * This is blocking method. You may interrupt this method for your own risk.
+ * In that case, it is not guaranteed that every port will be released by the time
+ * of the method end.
+ */
+ @Override
+ public synchronized void shutDown() {
+ if (ceaseWorking || serverClosed) {
+ return;
+ }
+ ceaseWorking = true;
+ try {
+ server.close();
+ mainThread.join();
+ } catch (@NotNull final IOException e) {
+ errors.add(e);
+ } catch (@NotNull final InterruptedException e) { // If we do not want to wait for shutdown
+ // do nothing
+ }
+
+ }
+
+ /**
+ * This method checks if any error occurred.
+ *
+ * @return true if any error occurred during work and false otherwise.
+ */
+ public boolean anyErrorOccurred() {
+ return errors.size() != 0;
+ }
+
+ /**
+ * Method to track all errors, occurred during work.
+ *
+ * @return list of occurred errors.
+ */
+ @NotNull
+ public List getErrors() {
+ return errors;
+ }
+
+ private void work() {
+ System.out.println("Server was initialized on port " + port);
+
+ // working until error or shutdown
+ while (!ceaseWorking) {
+ final Socket socket;
+ try {
+ socket = server.accept(); // must be closed by session
+ final Session session = newSession(socket, sessionsProcessed++);
+ handleSession(session);
+ } catch (@NotNull final IOException e) {
+ if (e instanceof SocketException && ceaseWorking) {
+ break; // server was turned off
+ }
+ errors.add(e); // Shutdown if any error occurred
+ break;
+ }
+ }
+
+
+ // shutting down server if error happened
+ if (!server.isClosed()) {
+ try {
+ server.close();
+ } catch (@NotNull final IOException e) {
+ errors.add(e); // store all exceptions
+ }
+ }
+
+ // closing current connections
+ closeSessions();
+
+ serverClosed = true;
+ System.out.println("Server was shut down");
+ }
+
+ @Override
+ public void closeSession(@NotNull final Session session) {
+ sessions.remove(session);
+ session.close();
+ }
+
+ protected abstract Session newSession(final Socket socket, int number);
+
+ private void closeSessions() {
+ for (final Session session : sessions) {
+ session.close();
+ }
+ }
+
+ private void handleSession(@NotNull final Session session) {
+ sessions.add(session);
+ session.run(); // starts session
+ }
+}
+
+
+
+
+
+
+
diff --git a/FTP/src/main/java/abstractServer/AbstractBlockingSession.java b/FTP/src/main/java/abstractServer/AbstractBlockingSession.java
new file mode 100644
index 0000000..96245e6
--- /dev/null
+++ b/FTP/src/main/java/abstractServer/AbstractBlockingSession.java
@@ -0,0 +1,140 @@
+package abstractServer;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+/**
+ * Abstract class to process one client connection.
+ *
+ * By default creates threads to process reading and writing. Every task from the client will
+ * be added to the ThreadPool.
+ *
+ * Also this class takes socket to work with as an input parameter, therefore this class
+ * must close the socket when it is not needed anymore (on {@link Session#close()} method call).
+ */
+public abstract class AbstractBlockingSession implements Session {
+
+ private final int id;
+ @NotNull
+ private final Socket socket;
+ private final Server server;
+
+ @NotNull
+ private final Executor executor;
+ private DataOutputStream os;
+
+ private final List errors = new ArrayList<>();
+
+ protected AbstractBlockingSession(final Socket socket, final int id, final Server server) {
+ this.socket = socket;
+ this.id = id;
+ this.server = server;
+ executor = Executors.newSingleThreadExecutor();
+
+ try {
+ os = new DataOutputStream(socket.getOutputStream());
+ } catch (@NotNull final IOException e) {
+ closeSession(e);
+ }
+ }
+
+ protected void closeSession(@Nullable final Exception e) {
+ if (e != null) {
+ errors.add(e);
+ }
+ server.closeSession(this);
+ }
+
+ // all docs inherited from Session interface
+ @Override
+ public void run() {
+ new Thread(this::processRead).start();
+ }
+
+ @Override
+ public int id() {
+ return id;
+ }
+
+ @Override
+ public void close() {
+ // we need two block in case of error happened in one of them
+ try {
+ os.close();
+ } catch (@NotNull final IOException e) {
+ errors.add(e);
+ e.printStackTrace();
+ }
+
+ try {
+ socket.close();
+ } catch (@NotNull final IOException e) {
+ errors.add(e);
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public boolean anyErrorOccurred() {
+ return errors.size() != 0;
+ }
+
+ @NotNull
+ @Override
+ public List getErrors() {
+ return errors;
+ }
+
+ protected void sendToClient(@NotNull final byte[] buf) {
+ executor.execute(() -> {
+ try {
+ os.write(buf);
+ os.flush();
+ } catch (@NotNull final IOException e) {
+ closeSession(e);
+ }
+ });
+ }
+
+ protected void sendToClient(final long x) {
+ executor.execute(() -> {
+ try {
+ os.writeLong(x);
+ os.flush();
+ } catch (@NotNull final IOException e) {
+ closeSession(e);
+ }
+ });
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ protected void sendToClient(@NotNull final byte[] buf, final int offset, final int length) {
+ executor.execute(() -> {
+ try {
+ os.write(buf, offset, length);
+ os.flush();
+ } catch (@NotNull final IOException e) {
+ closeSession(e);
+ }
+ });
+ }
+
+ private void processRead() {
+ try (final DataInputStream is = new DataInputStream(socket.getInputStream())) {
+ processInput(is);
+ } catch (@NotNull final IOException e) {
+ closeSession(e);
+ }
+ }
+
+ protected abstract void processInput(final DataInputStream is) throws IOException;
+}
diff --git a/FTP/src/main/java/abstractServer/Server.java b/FTP/src/main/java/abstractServer/Server.java
new file mode 100644
index 0000000..76216bd
--- /dev/null
+++ b/FTP/src/main/java/abstractServer/Server.java
@@ -0,0 +1,19 @@
+package abstractServer;
+
+/**
+ * Server interface. Might have different architectures.
+ */
+public interface Server {
+
+ /**
+ * Turns off the server. It is required, that any implementation releases
+
+ * every port, used by server.
+ */
+ void shutDown();
+
+ /**
+ * Call this method to close one exact session.
+ */
+ void closeSession(Session session);
+}
diff --git a/FTP/src/main/java/abstractServer/Session.java b/FTP/src/main/java/abstractServer/Session.java
new file mode 100644
index 0000000..b999f14
--- /dev/null
+++ b/FTP/src/main/java/abstractServer/Session.java
@@ -0,0 +1,56 @@
+package abstractServer;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.Closeable;
+import java.util.List;
+
+/**
+ * This interface provides session methods to work with one exact client.
+ */
+@SuppressWarnings("unused")
+public interface Session extends Comparable, Closeable {
+
+ /**
+ * Id is required to implement comparable method, therefore server might
+ * be able to store sessions in set.
+ *
+ * @return session id
+ */
+ int id();
+
+ /**
+ * The method must be used to close all resources, such as sockets, Readers e.t.c.
+ */
+ @Override
+ void close();
+
+ @Override
+ default int compareTo(@NotNull final Session o) {
+ return id() - o.id();
+ }
+
+ /**
+ * Starts to process the session. For example, this method might start reading
+ * and writing threads.
+ */
+ void run();
+
+ /**
+ * This method checks if any error occurred.
+ *
+ * @return true if any error occurred during work and false otherwise.
+ */
+ default boolean anyErrorOccurred() {
+ return false;
+ }
+
+ /**
+ * Method to track all errors, occurred during work.
+ *
+ * @return list of occurred errors.
+ */
+ default List getErrors() {
+ return List.of();
+ }
+}
diff --git a/FTP/src/test/java/FTPClientTest.java b/FTP/src/test/java/FTPClientTest.java
new file mode 100644
index 0000000..8a616d1
--- /dev/null
+++ b/FTP/src/test/java/FTPClientTest.java
@@ -0,0 +1,88 @@
+import org.apache.commons.io.FileUtils;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.Arrays;
+import java.util.Map;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.*;
+
+
+class FTPClientTest {
+
+ @Test
+ void testListEmpty() throws IOException {
+ Utils.clear();
+ testListWithContent(Map.of());
+ testListWithContent(Map.of("file1", false));
+ testListWithContent(Map.of("file1", true));
+ testListWithContent(Map.of("file1", true, "file2", false));
+ }
+
+ private void testListWithContent(@NotNull final Map files) throws IOException {
+ final FTPServer server = FTPServer.init();
+ final FTPClient client = new FTPClient("localhost", FTPServer.DEFAULT_PORT);
+ try {
+ Thread.sleep(100);
+ } catch (@NotNull final InterruptedException e) {
+ // do nothing
+ }
+
+ for (final Map.Entry entry : files.entrySet()) {
+ if (entry.getValue()) {
+ Utils.createFile(entry.getKey() + File.separator + "dummy");
+ } else {
+ Utils.createFile(entry.getKey());
+ }
+ }
+
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+
+ final PrintStream old = System.out;
+ System.setOut(new PrintStream(os));
+ client.list(Utils.sourceDir);
+
+ final StringBuilder answer = new StringBuilder("Total number of files: " + files.size() + "\n");
+ for (final Map.Entry entry : files.entrySet()) {
+ final String isFolderString = entry.getValue() ? "is a folder." : "is not a folder.";
+ answer.append("\"").append(entry.getKey())
+ .append("\" ").append(isFolderString).append('\n');
+ }
+ assertThat(Arrays.asList(os.toString().split("\n")),
+ containsInAnyOrder(answer.toString().split("\n")));
+
+ System.setOut(old);
+ Utils.clear();
+ server.shutDown();
+ }
+
+ @Test
+ void testGet() throws IOException {
+ Utils.clear();
+ testGetWithContent();
+ testGetWithContent("hello");
+ testGetWithContent("hello", "one");
+ testGetWithContent("hello", "hello");
+ }
+
+ private void testGetWithContent(final String... content) throws IOException {
+ final FTPServer server = FTPServer.init();
+ final FTPClient client = new FTPClient("localhost", FTPServer.DEFAULT_PORT);
+
+ Utils.createFile("file", content);
+
+ client.get(Utils.getFileName("file"), "tmp.txt");
+
+ FileUtils.contentEquals(new File(Utils.getFileName("file")), new File("tmp.txt"));
+
+ Utils.deleteFile("tmp.txt");
+ Utils.clear();
+ server.shutDown();
+ }
+
+}
\ No newline at end of file
diff --git a/FTP/src/test/java/Utils.java b/FTP/src/test/java/Utils.java
new file mode 100644
index 0000000..b215a33
--- /dev/null
+++ b/FTP/src/test/java/Utils.java
@@ -0,0 +1,55 @@
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+@SuppressWarnings({"WeakerAccess", "unused"})
+public class Utils {
+
+ public static final String sourceDir = "tempDir";
+
+ @NotNull
+ @SuppressWarnings("ResultOfMethodCallIgnored")
+ public static File createFile(final String name, @NotNull final String... lines) throws IOException {
+ final File file = new File(getFileName(name));
+ new File(file.getParent()).mkdirs();
+ file.createNewFile();
+
+ try (final PrintWriter writer = new PrintWriter(file)) {
+ for (final String line : lines) {
+ writer.append(line).append('\n');
+ }
+ }
+ return file;
+ }
+
+ @SuppressWarnings("ResultOfMethodCallIgnored")
+ public static void deleteFile(@NotNull final String name) {
+ final File file = new File(name);
+ if (!file.isDirectory()) {
+ file.delete();
+ } else {
+ //noinspection ConstantConditions
+ for (final File inner : file.listFiles()) {
+ deleteFile(inner.getAbsolutePath());
+ }
+ file.delete();
+ }
+ }
+
+ public static void clear() {
+ deleteFile(sourceDir);
+ }
+
+ public static String getFileName(final String name) {
+ return sourceDir + File.separator + name;
+ }
+
+ @NotNull
+ public static File createTemporaryFile(final String proxy, final String... lines) throws IOException {
+ final File file = createFile(proxy, lines);
+ file.deleteOnExit();
+ return file;
+ }
+}
\ No newline at end of file
diff --git a/FTP/src/test/java/abstractServer/AbstractBlockingServerTest.java b/FTP/src/test/java/abstractServer/AbstractBlockingServerTest.java
new file mode 100644
index 0000000..c01f929
--- /dev/null
+++ b/FTP/src/test/java/abstractServer/AbstractBlockingServerTest.java
@@ -0,0 +1,97 @@
+package abstractServer;
+
+import abstractServer.dummies.DummyClient;
+import abstractServer.dummies.DummyException;
+import abstractServer.dummies.DummyServer;
+import abstractServer.dummies.DummySession;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.*;
+import static org.hamcrest.core.Every.everyItem;
+import static org.hamcrest.core.Is.is;
+
+class AbstractBlockingServerTest {
+
+ @Test
+ void initAndShutDown() throws IOException {
+ DummyServer server = DummyServer.init();
+
+ for (int i = 0; i < 5; i++) {
+ server.shutDown();
+ server = DummyServer.init();
+ assertThat(server.anyErrorOccurred(), is(false));
+ }
+
+ server.shutDown();
+ }
+
+ @Test
+ void testMessages() throws IOException {
+ final DummyServer server = DummyServer.init();
+
+ for (int i = 0; i < 5; i++) {
+ new DummyClient().sendCommands(42, 17, 46, 14, 22, 134);
+ }
+
+ try {
+ Thread.sleep(1000);
+ } catch (@NotNull final InterruptedException e) {
+ // just waiting...
+ }
+
+ assertThat(server.anyErrorOccurred(), is(false));
+ assertThat(server.getErrors(), is(empty()));
+ assertThat(server.session().stream()
+ .map(DummySession::lines).collect(Collectors.toList()),
+ everyItem(is(List.of("42", "17", "46", "14", "22", "134"))));
+
+ assertThat(server.session().stream()
+ .map(DummySession::getErrors).collect(Collectors.toList()),
+ everyItem(is(empty())));
+
+
+ server.shutDown();
+ }
+
+ @Test
+ void testCloseSession() throws IOException {
+ final DummyServer server = DummyServer.init();
+
+ for (int i = 0; i < 3; i++) {
+ new DummyClient().sendCommands(-2);
+ }
+
+ try {
+ Thread.sleep(1000);
+ } catch (@NotNull final InterruptedException e) {
+ // just waiting...
+ }
+
+ assertThat(server.anyErrorOccurred(), is(false));
+ assertThat(server.getErrors(), is(empty()));
+ assertThat(server.session().stream().map(DummySession::getErrors)
+ .collect(Collectors.toList()),
+ everyItem(everyItem(is(instanceOf(DummyException.class)))));
+ assertThat(server.session().stream().map(DummySession::getErrors)
+ .collect(Collectors.toList()),
+ everyItem(iterableWithSize(1)));
+
+ server.shutDown();
+ }
+
+ @Test
+ void testAnswer() throws IOException {
+ final DummyServer server = DummyServer.init();
+
+ assertThat(new DummyClient().sendAnswerIntCommand(), is(42));
+
+ server.shutDown();
+ }
+
+}
\ No newline at end of file
diff --git a/FTP/src/test/java/abstractServer/dummies/DummyClient.java b/FTP/src/test/java/abstractServer/dummies/DummyClient.java
new file mode 100644
index 0000000..1337d69
--- /dev/null
+++ b/FTP/src/test/java/abstractServer/dummies/DummyClient.java
@@ -0,0 +1,37 @@
+package abstractServer.dummies;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.net.Socket;
+
+public class DummyClient {
+
+ @NotNull
+ private final DataOutputStream os;
+ @NotNull
+ private final DataInputStream is;
+
+ public DummyClient() throws IOException {
+ final @NotNull Socket socket = new Socket("localhost", DummyServer.DEFAULT_PORT);
+ os = new DataOutputStream(socket.getOutputStream());
+ is = new DataInputStream(socket.getInputStream());
+ }
+
+ public void sendCommands(@NotNull final int... commands) throws IOException {
+ for (final int cmd : commands) {
+ os.writeInt(cmd);
+ os.flush();
+ }
+ }
+
+ public int sendAnswerIntCommand() throws IOException {
+ os.writeInt(1);
+ os.flush();
+
+ return is.readInt();
+ }
+
+}
diff --git a/FTP/src/test/java/abstractServer/dummies/DummyException.java b/FTP/src/test/java/abstractServer/dummies/DummyException.java
new file mode 100644
index 0000000..dd0b1ce
--- /dev/null
+++ b/FTP/src/test/java/abstractServer/dummies/DummyException.java
@@ -0,0 +1,5 @@
+package abstractServer.dummies;
+
+public class DummyException extends Exception {
+
+}
diff --git a/FTP/src/test/java/abstractServer/dummies/DummyServer.java b/FTP/src/test/java/abstractServer/dummies/DummyServer.java
new file mode 100644
index 0000000..323ce1e
--- /dev/null
+++ b/FTP/src/test/java/abstractServer/dummies/DummyServer.java
@@ -0,0 +1,42 @@
+package abstractServer.dummies;
+
+import abstractServer.AbstractBlockingServer;
+import abstractServer.Session;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.List;
+
+public class DummyServer extends AbstractBlockingServer {
+
+ @SuppressWarnings("WeakerAccess")
+ public final static int DEFAULT_PORT = 9925;
+
+ @NotNull
+ private final List sessionList = new ArrayList<>();
+
+ private DummyServer(final int port) throws IOException {
+ super(port);
+ }
+
+ @NotNull
+ @Override
+ protected Session newSession(final Socket socket, final int number) {
+ final DummySession session = new DummySession(socket, number, this);
+ sessionList.add(session);
+ return session;
+ }
+
+ @NotNull
+ public List session() {
+ return sessionList;
+ }
+
+ public static DummyServer init() throws IOException {
+ return new DummyServer(DEFAULT_PORT);
+ }
+
+
+}
diff --git a/FTP/src/test/java/abstractServer/dummies/DummySession.java b/FTP/src/test/java/abstractServer/dummies/DummySession.java
new file mode 100644
index 0000000..88458c3
--- /dev/null
+++ b/FTP/src/test/java/abstractServer/dummies/DummySession.java
@@ -0,0 +1,58 @@
+package abstractServer.dummies;
+
+import abstractServer.AbstractBlockingSession;
+import abstractServer.Server;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.List;
+
+public class DummySession extends AbstractBlockingSession {
+
+ private final List lines = new ArrayList<>();
+
+ DummySession(@NotNull final Socket socket, final int id, final Server server) {
+ super(socket, id, server);
+ }
+
+ @Override
+ protected void processInput(@NotNull final DataInputStream is) throws IOException {
+ while (true) {
+ final int cmd = is.readInt();
+ switch (cmd) {
+ case -1:
+ closeSession(null);
+ return;
+ case -2:
+ closeSession(new DummyException());
+ return;
+ case 1:
+ sendNumber(42);
+ break;
+ default:
+ lines.add(String.valueOf(cmd));
+ break;
+ }
+ }
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private void sendNumber(final int x) throws IOException {
+ final ByteArrayOutputStream bos = new ByteArrayOutputStream(1000);
+ final DataOutputStream os = new DataOutputStream(bos);
+
+ os.writeInt(x);
+
+ sendToClient(bos.toByteArray());
+ }
+
+ @NotNull
+ public List lines() {
+ return lines;
+ }
+}
diff --git a/HW2-Tic-Tac-Toe/build.gradle b/HW2-Tic-Tac-Toe/build.gradle
new file mode 100644
index 0000000..6ff4d22
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/build.gradle
@@ -0,0 +1,67 @@
+buildscript {
+ repositories {
+ mavenCentral()
+ }
+ dependencies {
+ classpath 'org.junit.platform:junit-platform-gradle-plugin:1.0.2'
+ classpath 'com.google.guava:guava:23.5-jre'
+ classpath 'org.mockito:mockito-all:2.0.2-beta'
+ }
+}
+
+repositories {
+ mavenCentral()
+}
+
+ext.junit4Version = '4.12'
+ext.junitVintageVersion = '4.12.2'
+ext.junitPlatformVersion = '1.0.2'
+ext.junitJupiterVersion = '5.0.2'
+ext.log4jVersion = '2.9.0'
+
+apply plugin: 'java'
+apply plugin: 'eclipse'
+apply plugin: 'idea'
+apply plugin: 'org.junit.platform.gradle.plugin'
+
+jar {
+ baseName = 'junit5-gradle-consumer'
+ version = '1.0.0-SNAPSHOT'
+}
+
+compileTestJava {
+ sourceCompatibility = 1.9
+ targetCompatibility = 1.9
+ options.compilerArgs += '-parameters'
+}
+
+dependencies {
+ // JUnit Jupiter API and TestEngine implementation
+ testCompile("org.junit.jupiter:junit-jupiter-api:${junitJupiterVersion}")
+ testRuntime("org.junit.jupiter:junit-jupiter-engine:${junitJupiterVersion}")
+
+ // If you also want to support JUnit 3 and JUnit 4 tests
+ testCompile("junit:junit:${junit4Version}")
+ testRuntime("org.junit.vintage:junit-vintage-engine:${junitVintageVersion}")
+
+ // To avoid compiler warnings about @API annotations in JUnit code
+ testCompileOnly('org.apiguardian:apiguardian-api:1.0.0')
+
+ // To use Log4J's LogManager
+ testRuntime("org.apache.logging.log4j:log4j-core:${log4jVersion}")
+ testRuntime("org.apache.logging.log4j:log4j-jul:${log4jVersion}")
+
+ // Only needed to run tests in an (IntelliJ) IDE(A) that bundles an older version
+ testRuntime("org.junit.platform:junit-platform-launcher:${junitPlatformVersion}")
+
+ testCompile group: 'org.hamcrest', name: 'hamcrest-junit', version: '2.0.0.0'
+ compile group: 'org.jetbrains', name: 'annotations', version: '15.0'
+
+ compile group: 'com.google.guava', name: 'guava', version: '23.5-jre'
+ testCompile group: 'org.mockito', name: 'mockito-all', version: '2.0.2-beta'
+}
+
+task wrapper(type: Wrapper) {
+ description = 'Generates gradlew[.bat] scripts'
+ gradleVersion = '4.6'
+}
\ No newline at end of file
diff --git a/HW2-Tic-Tac-Toe/gradle/wrapper/gradle-wrapper.jar b/HW2-Tic-Tac-Toe/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..f6b961f
Binary files /dev/null and b/HW2-Tic-Tac-Toe/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/HW2-Tic-Tac-Toe/gradle/wrapper/gradle-wrapper.properties b/HW2-Tic-Tac-Toe/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..bf3de21
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/HW2-Tic-Tac-Toe/gradlew b/HW2-Tic-Tac-Toe/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/HW2-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/HW2-Tic-Tac-Toe/gradlew.bat b/HW2-Tic-Tac-Toe/gradlew.bat
new file mode 100644
index 0000000..f955316
--- /dev/null
+++ b/HW2-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/HW2-Tic-Tac-Toe/settings.gradle b/HW2-Tic-Tac-Toe/settings.gradle
new file mode 100644
index 0000000..a4c0c1b
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/settings.gradle
@@ -0,0 +1,2 @@
+rootProject.name = 'Tic-Tac-Toe'
+
diff --git a/HW2-Tic-Tac-Toe/src/main/java/Main.java b/HW2-Tic-Tac-Toe/src/main/java/Main.java
new file mode 100644
index 0000000..c50dc8d
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/src/main/java/Main.java
@@ -0,0 +1,28 @@
+import handlers.SceneManager;
+import javafx.application.Application;
+import javafx.stage.Stage;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Main class of application. Starts a tic-tac-toe application.
+ */
+public class Main extends Application {
+
+ /**
+ * The main entry point of tic-tac-toe application.
+ */
+ @Override
+ public void start(@NotNull final Stage primaryStage) {
+ primaryStage.setMinWidth(300);
+ primaryStage.setMinHeight(400);
+ primaryStage.setTitle("Tic-tac-toe");
+
+ SceneManager.initialize(primaryStage);
+ SceneManager.changeScene(SceneManager.SceneEnum.MAIN_MENU);
+ primaryStage.show();
+ }
+
+ public static void main(final String[] args) {
+ launch(args);
+ }
+}
diff --git a/HW2-Tic-Tac-Toe/src/main/java/game/GameConfig.java b/HW2-Tic-Tac-Toe/src/main/java/game/GameConfig.java
new file mode 100644
index 0000000..fcf07ec
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/src/main/java/game/GameConfig.java
@@ -0,0 +1,48 @@
+package game;
+
+import game.gameTypes.GameOptions;
+import game.gameTypes.MultiPlayerGameType;
+import game.gameTypes.SinglePlayerGameType;
+
+/**
+ * In this class options for the new game a stored. Also, this class helps to
+ * create different game types such as multi-player/single-player game.
+ */
+@SuppressWarnings("WeakerAccess")
+public class GameConfig {
+
+ private static GameOptions options = MultiPlayerGameType.multiPlayerGame();
+
+ /**
+ * The method returns current game options.
+ */
+ public static GameOptions getOptions() {
+ return options;
+ }
+
+ public static void setOptions(final GameOptions options) {
+ GameConfig.options = options;
+ }
+
+ /**
+ * Options for default multi-player game.
+ */
+ public static void setMultiPlayerGame() {
+ setOptions(MultiPlayerGameType.multiPlayerGame());
+ }
+
+ /**
+ * Options for default single-player game with easy bot. Player will have the first turn.
+ */
+ public static void setSinglePlayerEasyGame() {
+ setOptions(SinglePlayerGameType.singlePlayerGame(true, true));
+ }
+
+ /**
+ * Options for default single-player game with hard bot. Player will have the first turn.
+ */
+ public static void setSinglePlayerHardGame() {
+ setOptions(SinglePlayerGameType.singlePlayerGame(false, true));
+ }
+
+}
diff --git a/HW2-Tic-Tac-Toe/src/main/java/game/GameController.java b/HW2-Tic-Tac-Toe/src/main/java/game/GameController.java
new file mode 100644
index 0000000..23b8033
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/src/main/java/game/GameController.java
@@ -0,0 +1,77 @@
+package game;
+
+import game.gameTypes.GameOptions;
+import game.model.Cell;
+import game.model.GameState;
+import game.model.Model;
+import org.jetbrains.annotations.NotNull;
+import view.View;
+
+public class GameController {
+
+ private final View view;
+
+ private Statistics statistics;
+ private Model model;
+
+ private boolean isActiveSession;
+ private boolean isGameInProgress;
+
+ public GameController(final View view) {
+ this.view = view;
+ }
+
+ public void startNewSession(@NotNull final GameOptions options) {
+ statistics = new Statistics();
+ isActiveSession = true;
+ startNewGame(options);
+ }
+
+ public void startNewGame(@NotNull final GameOptions options) {
+ model = new Model(this, options);
+ view.clear();
+ isGameInProgress = true;
+ }
+
+ public void turn(final int x, final int y) {
+ model.turn(x, y);
+ }
+
+ public void set(final int x, final int y, final Cell cell) {
+ view.set(x, y, cell);
+ }
+
+ public void onGameEnd(@NotNull final GameState state) {
+ switch (state) {
+ case IN_PROGRESS:
+ throw new RuntimeException(); // should never happen
+ case DRAW:
+ statistics.onDraw();
+ break;
+ case X_WIN:
+ statistics.onXWin();
+ break;
+ case O_WIN:
+ statistics.onOWin();
+ break;
+ }
+ isGameInProgress = false;
+ }
+
+ public boolean isGameInProgress() {
+ return isGameInProgress;
+ }
+
+ public Statistics getStatistics() {
+ return statistics;
+ }
+
+ public boolean isActiveSession() {
+ return isActiveSession;
+ }
+
+ public void closeSession() {
+ isActiveSession = false;
+ isGameInProgress = false;
+ }
+}
diff --git a/HW2-Tic-Tac-Toe/src/main/java/game/Statistics.java b/HW2-Tic-Tac-Toe/src/main/java/game/Statistics.java
new file mode 100644
index 0000000..62f8da2
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/src/main/java/game/Statistics.java
@@ -0,0 +1,42 @@
+package game;
+
+@SuppressWarnings("unused")
+public class Statistics {
+
+ private int gamesNumber;
+
+ private int xWins;
+ private int oWins;
+ private int draws;
+
+ void onXWin() {
+ gamesNumber++;
+ xWins++;
+ }
+
+ void onOWin() {
+ gamesNumber++;
+ oWins++;
+ }
+
+ void onDraw() {
+ gamesNumber++;
+ draws++;
+ }
+
+ public int getXWins() {
+ return xWins;
+ }
+
+ public int getOWins() {
+ return oWins;
+ }
+
+ public int getDraws() {
+ return draws;
+ }
+
+ public int getGamesNumber() {
+ return gamesNumber;
+ }
+}
diff --git a/HW2-Tic-Tac-Toe/src/main/java/game/gameTypes/AbstractGameType.java b/HW2-Tic-Tac-Toe/src/main/java/game/gameTypes/AbstractGameType.java
new file mode 100644
index 0000000..005af8d
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/src/main/java/game/gameTypes/AbstractGameType.java
@@ -0,0 +1,23 @@
+package game.gameTypes;
+
+import game.model.players.Player;
+import utils.CycleCollection;
+
+/**
+ * This class provides simple constructor for game type.
+ */
+@SuppressWarnings("WeakerAccess")
+public abstract class AbstractGameType implements GameOptions {
+
+ protected final CycleCollection players;
+
+ protected AbstractGameType(final CycleCollection players) {
+ this.players = players;
+ }
+
+ @Override
+ public final CycleCollection getPlayers() {
+ return players;
+ }
+
+}
diff --git a/HW2-Tic-Tac-Toe/src/main/java/game/gameTypes/GameOptions.java b/HW2-Tic-Tac-Toe/src/main/java/game/gameTypes/GameOptions.java
new file mode 100644
index 0000000..4341150
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/src/main/java/game/gameTypes/GameOptions.java
@@ -0,0 +1,27 @@
+package game.gameTypes;
+
+import game.model.Model;
+import game.model.players.Player;
+import org.jetbrains.annotations.NotNull;
+import utils.CycleCollection;
+
+/**
+ * Interface for game options. In case of tic-tac-toe returns list of players and sets
+ * first turn to X by default.
+ */
+public interface GameOptions {
+
+ /**
+ * The methods returns cycle list of all players in the game.
+ */
+ CycleCollection getPlayers();
+
+ /**
+ * Returns first-turn symbol.
+ */
+ @SuppressWarnings("SameReturnValue")
+ @NotNull
+ default Model.Turn getFirstTurn() {
+ return Model.Turn.X;
+ }
+}
diff --git a/HW2-Tic-Tac-Toe/src/main/java/game/gameTypes/MultiPlayerGameType.java b/HW2-Tic-Tac-Toe/src/main/java/game/gameTypes/MultiPlayerGameType.java
new file mode 100644
index 0000000..3bc202c
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/src/main/java/game/gameTypes/MultiPlayerGameType.java
@@ -0,0 +1,27 @@
+package game.gameTypes;
+
+import game.model.players.HumanPlayer;
+import org.jetbrains.annotations.NotNull;
+import utils.CycleCollection;
+
+import java.util.Arrays;
+
+/**
+ * Default class for multi-player game type. Overrides constructor with fabric method.
+ */
+public class MultiPlayerGameType extends AbstractGameType {
+
+ private final static GameOptions MULTI_PLAYER_GAME_TYPE = new MultiPlayerGameType();
+
+ /**
+ * The method returns game options to describe simple Multi-player game.
+ */
+ @NotNull
+ public static GameOptions multiPlayerGame() {
+ return MULTI_PLAYER_GAME_TYPE;
+ }
+
+ private MultiPlayerGameType() {
+ super(new CycleCollection<>(Arrays.asList(new HumanPlayer(), new HumanPlayer())));
+ }
+}
diff --git a/HW2-Tic-Tac-Toe/src/main/java/game/gameTypes/SinglePlayerGameType.java b/HW2-Tic-Tac-Toe/src/main/java/game/gameTypes/SinglePlayerGameType.java
new file mode 100644
index 0000000..8661e8c
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/src/main/java/game/gameTypes/SinglePlayerGameType.java
@@ -0,0 +1,78 @@
+package game.gameTypes;
+
+import game.model.players.*;
+import org.jetbrains.annotations.NotNull;
+import utils.CycleCollection;
+
+import java.util.Arrays;
+
+/**
+ * Default class for single-player game type construction.
+ *
+ * Overrides constructor with number of fabric methods.
+ */
+public class SinglePlayerGameType extends AbstractGameType {
+
+ private final static GameOptions SINGLE_PLAYER_EASY_FIRST =
+ new SinglePlayerGameType(BotLevel.EASY, true);
+
+ private final static GameOptions SINGLE_PLAYER_EASY_SECOND =
+ new SinglePlayerGameType(BotLevel.EASY, false);
+
+ private final static GameOptions SINGLE_PLAYER_HARD_FIRST =
+ new SinglePlayerGameType(BotLevel.HARD, true);
+
+ private final static GameOptions SINGLE_PLAYER_HARD_SECOND =
+ new SinglePlayerGameType(BotLevel.HARD, false);
+
+ /**
+ * Fabric method to create single-player game.
+ *
+ * @param isEasy sets bot to easy if true, and to hard otherwise.
+ * @param first describes, whether human player has first turn or second one.
+ * @return new single-player game options described by the given parameters.
+ */
+ @NotNull
+ public static GameOptions singlePlayerGame(final boolean isEasy, final boolean first) {
+ if (isEasy) {
+ return first ? SINGLE_PLAYER_EASY_FIRST : SINGLE_PLAYER_EASY_SECOND;
+ }
+ return first ? SINGLE_PLAYER_HARD_FIRST : SINGLE_PLAYER_HARD_SECOND;
+ }
+
+ private SinglePlayerGameType(@NotNull final BotLevel level, final boolean firstTurn) {
+ super(getList(level, firstTurn));
+ }
+
+ /**
+ * The method returns bot from the target level.
+ *
+ * @param level target bot level.
+ */
+ private static Bot getBot(final BotLevel level) {
+ switch (level) {
+ case HARD:
+ return new HardBot();
+ default:
+ return new EasyBot();
+ }
+ }
+
+ /**
+ * The method to create circle list of players with given options.
+ *
+ * @param level target bot level.
+ * @param firstTurn set true, if player has first turn and false otherwise.
+ * @return new list
+ */
+ private static CycleCollection getList(@NotNull final BotLevel level, final boolean firstTurn) {
+ final Bot bot = getBot(level);
+ return firstTurn ?
+ new CycleCollection<>(Arrays.asList(new HumanPlayer(), bot)) :
+ new CycleCollection<>(Arrays.asList(bot, new HumanPlayer()));
+ }
+
+ private enum BotLevel {
+ EASY, HARD
+ }
+}
diff --git a/HW2-Tic-Tac-Toe/src/main/java/game/model/Board.java b/HW2-Tic-Tac-Toe/src/main/java/game/model/Board.java
new file mode 100644
index 0000000..f47da25
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/src/main/java/game/model/Board.java
@@ -0,0 +1,103 @@
+package game.model;
+
+import org.jetbrains.annotations.NotNull;
+import utils.Direction;
+
+@SuppressWarnings("WeakerAccess")
+public class Board {
+
+ private final static int SIZE = 3;
+ private final static int targetLength = 3;
+
+ private final Cell[][] board = new Cell[SIZE][SIZE];
+
+ Board() {
+ clear();
+ }
+
+ public boolean isEmpty(final int x, final int y) {
+ return board[x][y] == Cell.EMPTY;
+ }
+
+ public int size() {
+ return SIZE;
+ }
+
+ public Cell get(final int x, final int y) {
+ return board[x][y];
+ }
+
+ // this method is public for bot only.
+ public boolean set(final int x, final int y, final Cell cell) {
+ if (isEmpty(x, y)) {
+ board[x][y] = cell;
+ return true;
+ }
+ return false;
+ }
+
+ public void override(final int x, final int y, final Cell cell) {
+ board[x][y] = cell;
+ }
+
+ // this method is public for ot only.
+ public void clear() {
+ for (int i = 0; i < SIZE; i++) {
+ for (int j = 0; j < SIZE; j++) {
+ board[i][j] = Cell.EMPTY;
+ }
+ }
+ }
+
+ @NotNull
+ public GameState getState() {
+ for (int i = 0; i < SIZE; i++) {
+ for (int j = 0; j < SIZE; j++) {
+ for (final Direction direction : Direction.ANGLE_DIRECTIONS) {
+ final LineState state = getLineState(i, j, direction);
+ switch (state) {
+ case X:
+ return GameState.X_WIN;
+ case O:
+ return GameState.O_WIN;
+ }
+ }
+ }
+ }
+ for (int i = 0; i < SIZE; i++) {
+ for (int j = 0; j < SIZE; j++) {
+ if (board[i][j] == Cell.EMPTY) {
+ return GameState.IN_PROGRESS;
+ }
+ }
+ }
+ return GameState.DRAW;
+ }
+
+ @NotNull
+ private LineState getLineState(final int x, final int y, @NotNull final Direction direction) {
+ final Cell init = board[x][y];
+ for (int i = 1; i < targetLength; i++) {
+ final int newX = x + i * direction.dx();
+ final int newY = y + i * direction.dy();
+ if (newX >= SIZE || newY >= SIZE || newX < 0 || newY < 0) {
+ return LineState.NONE;
+ }
+ final Cell cell = board[newX][newY];
+ if (cell != init) {
+ return LineState.NONE;
+ }
+ }
+ switch (init) {
+ case X:
+ return LineState.X;
+ case O:
+ return LineState.O;
+ }
+ return LineState.NONE;
+ }
+
+ private enum LineState {
+ X, O, NONE
+ }
+}
diff --git a/HW2-Tic-Tac-Toe/src/main/java/game/model/Cell.java b/HW2-Tic-Tac-Toe/src/main/java/game/model/Cell.java
new file mode 100644
index 0000000..feff6c5
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/src/main/java/game/model/Cell.java
@@ -0,0 +1,8 @@
+package game.model;
+
+/**
+ * Enum for board cell state.
+ */
+public enum Cell {
+ EMPTY, X, O
+}
diff --git a/HW2-Tic-Tac-Toe/src/main/java/game/model/GameState.java b/HW2-Tic-Tac-Toe/src/main/java/game/model/GameState.java
new file mode 100644
index 0000000..7b8d03c
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/src/main/java/game/model/GameState.java
@@ -0,0 +1,8 @@
+package game.model;
+
+/**
+ * Enum class to store game state.
+ */
+public enum GameState {
+ X_WIN, O_WIN, DRAW, IN_PROGRESS
+}
diff --git a/HW2-Tic-Tac-Toe/src/main/java/game/model/Model.java b/HW2-Tic-Tac-Toe/src/main/java/game/model/Model.java
new file mode 100644
index 0000000..081c91c
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/src/main/java/game/model/Model.java
@@ -0,0 +1,79 @@
+package game.model;
+
+import game.GameController;
+import game.gameTypes.GameOptions;
+import game.model.players.Bot;
+import game.model.players.Player;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Iterator;
+
+public class Model {
+
+ private final GameController gameController;
+
+ private final Board board = new Board();
+ private final Iterator iterator;
+
+ private Turn turn;
+ private Player player;
+
+ private boolean gameInProgress = true;
+
+ public Model(final GameController gameController, final GameOptions gameOptions) {
+ this.gameController = gameController;
+
+ iterator = gameOptions.getPlayers().cycleIterator();
+ player = iterator.next();
+ turn = gameOptions.getFirstTurn();
+
+ // Init bots
+ Turn tmpTurn = gameOptions.getFirstTurn();
+ for (final Player player : gameOptions.getPlayers()) {
+ if (player instanceof Bot) {
+ ((Bot) player).initBot(this, tmpTurn);
+ }
+ tmpTurn = tmpTurn == Turn.X ? Turn.O : Turn.X;
+ }
+ }
+
+ @NotNull
+ public Board getBoard() {
+ return board;
+ }
+
+ public void turn(final int x, final int y) {
+ if (!gameInProgress) {
+ return;
+ }
+ final Cell cell = turn == Turn.X ? Cell.X : Cell.O;
+ if (board.set(x, y, cell)) {
+ gameController.set(x, y, cell);
+ if (checkGameEnd()) {
+ gameInProgress = false;
+ return;
+ }
+ nextPlayer();
+ }
+ }
+
+ private boolean checkGameEnd() {
+ final GameState state = board.getState();
+ if (state != GameState.IN_PROGRESS) {
+ gameController.onGameEnd(state);
+ return true;
+ }
+ return false;
+ }
+
+ private void nextPlayer() {
+ player = iterator.next();
+ turn = turn == Turn.X ? Turn.O : Turn.X;
+ player.getTurn();
+ }
+
+ public enum Turn {
+ X, O
+ }
+
+}
diff --git a/HW2-Tic-Tac-Toe/src/main/java/game/model/players/Bot.java b/HW2-Tic-Tac-Toe/src/main/java/game/model/players/Bot.java
new file mode 100644
index 0000000..ac2137e
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/src/main/java/game/model/players/Bot.java
@@ -0,0 +1,16 @@
+package game.model.players;
+
+import game.model.Model;
+
+public interface Bot extends Player {
+
+ @Override
+ default boolean isBot() {
+ return true;
+ }
+
+ void initBot(Model model, final Model.Turn turn);
+
+ class NoTurnException extends RuntimeException {
+ }
+}
diff --git a/HW2-Tic-Tac-Toe/src/main/java/game/model/players/EasyBot.java b/HW2-Tic-Tac-Toe/src/main/java/game/model/players/EasyBot.java
new file mode 100644
index 0000000..1a937e6
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/src/main/java/game/model/players/EasyBot.java
@@ -0,0 +1,31 @@
+package game.model.players;
+
+import game.model.Board;
+import org.jetbrains.annotations.NotNull;
+
+public class EasyBot extends OptimizationBot {
+
+ @Override
+ protected int getScore(@NotNull final Board board) {
+ int score = 0;
+
+ if (isGameWon()) {
+ return Integer.MAX_VALUE;
+ }
+
+ if (board.get(board.size() / 2, board.size() / 2) == ourCell) {
+ score += 50;
+ }
+
+ final int[] xs = {0, board.size() - 1};
+ for (final int i : xs) {
+ for (final int j : xs) {
+ if (board.get(i, j) == ourCell) {
+ score += 10;
+ }
+ }
+ }
+ return score;
+ }
+
+}
diff --git a/HW2-Tic-Tac-Toe/src/main/java/game/model/players/HardBot.java b/HW2-Tic-Tac-Toe/src/main/java/game/model/players/HardBot.java
new file mode 100644
index 0000000..75521f0
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/src/main/java/game/model/players/HardBot.java
@@ -0,0 +1,78 @@
+package game.model.players;
+
+import game.model.Board;
+import game.model.Cell;
+import org.jetbrains.annotations.NotNull;
+import utils.Direction;
+
+/**
+ * Hard bot for tic-tac-toe 3x3 game.
+ *
+ * This bot compares different turns by optimality function. Therefore, it is easy to
+ * modify this bot by changing {@link HardBot#getScore(Board)} function.
+ */
+public class HardBot extends OptimizationBot {
+
+ @Override
+ protected int getScore(@NotNull final Board board) {
+ int score = 10;
+ final int size = board.size();
+ final int target = 3;
+
+ if (isGameWon()) {
+ return Integer.MAX_VALUE;
+ }
+
+ // check if opponent can end the game
+ for (int i = 0; i < size; i++) {
+ for (int j = 0; j < size; j++) {
+ for (final Direction direction : Direction.ANGLE_DIRECTIONS) {
+ final int mx = i + direction.dx() * (target - 1);
+ final int my = j + direction.dy() * (target - 1);
+ if (mx >= size || my >= size || mx < 0 || my < 0) {
+ continue;
+ }
+ int theirCnt = 0;
+ int ourCnt = 0;
+ for (int k = 0; k < target; k++) {
+ final int nx = i + direction.dx() * k;
+ final int ny = j + direction.dy() * k;
+ if (nx < size && ny < size) {
+ final Cell cell = board.get(nx, ny);
+ if (cell == opponentCell) {
+ theirCnt++;
+ }
+ if (cell == ourCell) {
+ ourCnt++;
+ }
+ }
+ }
+ if (ourCnt == 0 && theirCnt >= target - 1) {
+ score -= 10000;
+ }
+ if (theirCnt == 0 && ourCnt >= target - 1) {
+ score += 1000;
+ }
+ if (theirCnt == 0 && ourCnt >= 1) {
+ score += 100;
+ }
+ }
+ }
+ }
+
+ if (board.get(size / 2, size / 2) == ourCell) {
+ score += 20;
+ }
+
+ final int[] xs = {0, board.size() - 1};
+ for (final int i : xs) {
+ for (final int j : xs) {
+ if (board.get(i, j) == ourCell) {
+ score += 10;
+ }
+ }
+ }
+
+ return Math.max(score, 0);
+ }
+}
diff --git a/HW2-Tic-Tac-Toe/src/main/java/game/model/players/HumanPlayer.java b/HW2-Tic-Tac-Toe/src/main/java/game/model/players/HumanPlayer.java
new file mode 100644
index 0000000..4255c4b
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/src/main/java/game/model/players/HumanPlayer.java
@@ -0,0 +1,14 @@
+package game.model.players;
+
+public class HumanPlayer implements Player {
+
+ @Override
+ public boolean isBot() {
+ return false;
+ }
+
+ @Override
+ public void getTurn() {
+ // do nothing
+ }
+}
diff --git a/HW2-Tic-Tac-Toe/src/main/java/game/model/players/OptimizationBot.java b/HW2-Tic-Tac-Toe/src/main/java/game/model/players/OptimizationBot.java
new file mode 100644
index 0000000..70f611e
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/src/main/java/game/model/players/OptimizationBot.java
@@ -0,0 +1,60 @@
+package game.model.players;
+
+import game.model.Board;
+import game.model.Cell;
+import game.model.GameState;
+import game.model.Model;
+
+@SuppressWarnings("WeakerAccess")
+abstract class OptimizationBot implements Bot {
+
+ private Model model;
+ protected Board board;
+
+ protected Cell ourCell;
+ protected Cell opponentCell;
+
+ @Override
+ public final void initBot(final Model model, final Model.Turn turn) {
+ this.model = model;
+ board = model.getBoard();
+ ourCell = turn == Model.Turn.X ? Cell.X : Cell.O;
+ opponentCell = turn == Model.Turn.X ? Cell.O : Cell.X;
+ }
+
+ @Override
+ public void getTurn() {
+ int x = 0;
+ int y = 0;
+ int score = -1;
+ for (int i = 0; i < board.size(); i++) {
+ for (int j = 0; j < board.size(); j++) {
+ if (board.get(i, j) != Cell.EMPTY) {
+ continue;
+ }
+ board.override(i, j, ourCell);
+ final int newScore = getScore(board);
+ board.override(i, j, Cell.EMPTY);
+ if (newScore > score) {
+ x = i;
+ y = j;
+ score = newScore;
+ }
+ }
+ }
+
+ if (score == -1) {
+ throw new NoTurnException();
+ }
+
+ model.turn(x, y);
+ }
+
+ protected abstract int getScore(final Board board);
+
+ protected boolean isGameWon() {
+ final GameState state = board.getState();
+ return state == GameState.X_WIN && ourCell == Cell.X ||
+ state == GameState.O_WIN && ourCell == Cell.O;
+ }
+}
diff --git a/HW2-Tic-Tac-Toe/src/main/java/game/model/players/Player.java b/HW2-Tic-Tac-Toe/src/main/java/game/model/players/Player.java
new file mode 100644
index 0000000..6a7d56e
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/src/main/java/game/model/players/Player.java
@@ -0,0 +1,11 @@
+package game.model.players;
+
+public interface Player {
+
+ @SuppressWarnings("unused")
+ default boolean isBot() {
+ return false;
+ }
+
+ void getTurn();
+}
diff --git a/HW2-Tic-Tac-Toe/src/main/java/handlers/BotMenuHandler.java b/HW2-Tic-Tac-Toe/src/main/java/handlers/BotMenuHandler.java
new file mode 100644
index 0000000..7480018
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/src/main/java/handlers/BotMenuHandler.java
@@ -0,0 +1,56 @@
+package handlers;
+
+import game.GameConfig;
+import javafx.fxml.FXML;
+import javafx.scene.input.KeyCode;
+import javafx.scene.layout.StackPane;
+
+import java.net.URL;
+import java.util.ResourceBundle;
+
+/**
+ * Handler to work with MainMenu buttons.
+ */
+public class BotMenuHandler implements Handler {
+
+ @FXML
+ private StackPane botModeMenu;
+
+ /**
+ * The method sets exit listener for escape button.
+ */
+ @Override
+ public void initialize(final URL location, final ResourceBundle resources) {
+ botModeMenu.setOnKeyReleased(event -> {
+ if (event.getCode() == KeyCode.ESCAPE) {
+ onBackToModeMenu();
+ }
+ });
+ }
+
+ /**
+ * The method initializes new single-player game with an easy bot.
+ */
+ @FXML
+ private void onEasyBot() {
+ GameConfig.setSinglePlayerEasyGame();
+ SceneManager.changeScene(SceneManager.SceneEnum.GAME);
+ }
+
+ /**
+ * The method initializes new single-player game with a hard bot.
+ */
+ @FXML
+ private void onHardBot() {
+ GameConfig.setSinglePlayerHardGame();
+ SceneManager.changeScene(SceneManager.SceneEnum.GAME);
+ }
+
+ /**
+ * The method changes scene to the ModeMenu scene.
+ */
+ @FXML
+ private void onBackToModeMenu() {
+ SceneManager.changeScene(SceneManager.SceneEnum.MODE_MENU);
+ }
+}
diff --git a/HW2-Tic-Tac-Toe/src/main/java/handlers/GameHandler.java b/HW2-Tic-Tac-Toe/src/main/java/handlers/GameHandler.java
new file mode 100644
index 0000000..b8aec56
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/src/main/java/handlers/GameHandler.java
@@ -0,0 +1,91 @@
+package handlers;
+
+import game.GameConfig;
+import game.GameController;
+import javafx.fxml.FXML;
+import javafx.scene.Node;
+import javafx.scene.input.KeyCode;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.StackPane;
+import view.View;
+
+import java.net.URL;
+import java.util.ResourceBundle;
+
+/**
+ * Handles game actions.
+ */
+public class GameHandler implements Handler {
+
+ @FXML private StackPane gameScreen;
+ @FXML private GridPane table;
+
+ private GameController gameController;
+
+ @Override
+ public void initialize(final URL location, final ResourceBundle resources) {
+ setFieldListeners();
+ gameController = new GameController(initView());
+
+ gameScreen.setOnKeyReleased(event -> {
+ if (event.getCode() == KeyCode.ESCAPE) {
+ onExit();
+ }
+ });
+ }
+
+ @Override
+ public void onShow() {
+ if (!gameController.isActiveSession()) {
+ gameController.startNewSession(GameConfig.getOptions());
+ }
+ }
+
+ private View initView() {
+ final StackPane[][] grid = new StackPane[3][3];
+ for (final Node node : table.getChildren()) {
+ if (node instanceof StackPane) {
+ final StackPane stackPane = (StackPane) node;
+ final int x = GridPane.getColumnIndex(stackPane);
+ final int y = GridPane.getRowIndex(stackPane);
+ grid[x][y] = stackPane;
+ }
+ }
+ return new View(grid);
+ }
+
+ private void setFieldListeners() {
+ for (final Node node : table.getChildren()) {
+ if (node instanceof StackPane) {
+ final StackPane stackPane = (StackPane) node;
+ final int x = GridPane.getColumnIndex(stackPane);
+ final int y = GridPane.getRowIndex(stackPane);
+ stackPane.setOnMouseReleased(event -> {
+ if (gameController.isGameInProgress()) {
+ gameController.turn(x, y);
+ } else {
+ onRestart();
+ }
+ });
+ }
+ }
+ }
+
+ @FXML
+ private void onRestart() {
+ gameController.startNewGame(GameConfig.getOptions());
+ }
+
+ @FXML
+ private void onExit() {
+ gameController.closeSession();
+ SceneManager.changeScene(SceneManager.SceneEnum.MAIN_MENU);
+ }
+
+ @FXML
+ private void onStats() {
+ SceneManager.changeScene(SceneManager.SceneEnum.STATS);
+ ((StatsHandler) SceneManager.getScene(SceneManager.SceneEnum.STATS)).
+ setStatistics(gameController.getStatistics());
+ }
+}
diff --git a/HW2-Tic-Tac-Toe/src/main/java/handlers/Handler.java b/HW2-Tic-Tac-Toe/src/main/java/handlers/Handler.java
new file mode 100644
index 0000000..6e4be64
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/src/main/java/handlers/Handler.java
@@ -0,0 +1,35 @@
+package handlers;
+
+import javafx.fxml.Initializable;
+
+import java.net.URL;
+import java.util.ResourceBundle;
+
+/**
+ * Interface to handle different scenes.
+ *
+ * Initialize method suspected to be called only one time for every scene.
+ *
+ * If you want to do action on every scene call use onShow method.
+ */
+public interface Handler extends Initializable {
+
+ /**
+ * Method to initialize scene. Derived from Initializable interface.
+ *
+ * The method is called, when attached to the handler scene was shown
+ * on the first time.
+ *
+ * The method does nothing by default.
+ */
+ @Override
+ default void initialize(final URL location, final ResourceBundle resources) {}
+
+ /**
+ * This method is suspected to be called when scene shows up. That means, that
+ * method might be used if you want to do some initialization on every scene show up.
+ *
+ * The method does nothing by default.
+ */
+ default void onShow() {}
+}
diff --git a/HW2-Tic-Tac-Toe/src/main/java/handlers/MainMenuHandler.java b/HW2-Tic-Tac-Toe/src/main/java/handlers/MainMenuHandler.java
new file mode 100644
index 0000000..1d53ef5
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/src/main/java/handlers/MainMenuHandler.java
@@ -0,0 +1,45 @@
+package handlers;
+
+import javafx.fxml.FXML;
+import javafx.scene.input.KeyCode;
+import javafx.scene.layout.StackPane;
+
+import java.net.URL;
+import java.util.ResourceBundle;
+
+/**
+ * Handler to work with MainMenu buttons.
+ */
+public class MainMenuHandler implements Handler {
+
+ @FXML
+ private StackPane mainMenu;
+
+ /**
+ * The method sets exit listener for escape button.
+ */
+ @Override
+ public void initialize(final URL location, final ResourceBundle resources) {
+ mainMenu.setOnKeyReleased(event -> {
+ if (event.getCode() == KeyCode.ESCAPE) {
+ onExit();
+ }
+ });
+ }
+
+ /**
+ * The method changes scene to the ModeMenu scene.
+ */
+ @FXML
+ private void onNewGame() {
+ SceneManager.changeScene(SceneManager.SceneEnum.MODE_MENU);
+ }
+
+ /**
+ * The method closes an application.
+ */
+ @FXML
+ private void onExit() {
+ System.exit(0);
+ }
+}
diff --git a/HW2-Tic-Tac-Toe/src/main/java/handlers/ModeMenuHandler.java b/HW2-Tic-Tac-Toe/src/main/java/handlers/ModeMenuHandler.java
new file mode 100644
index 0000000..00e3c70
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/src/main/java/handlers/ModeMenuHandler.java
@@ -0,0 +1,55 @@
+package handlers;
+
+import game.GameConfig;
+import javafx.fxml.FXML;
+import javafx.scene.input.KeyCode;
+import javafx.scene.layout.StackPane;
+
+import java.net.URL;
+import java.util.ResourceBundle;
+
+/**
+ * Handler to work with ModeMenu buttons.
+ */
+public class ModeMenuHandler implements Handler {
+
+ @FXML
+ private StackPane gameModeMenu;
+
+ /**
+ * The method sets exit listener for escape button.
+ */
+ @Override
+ public void initialize(final URL location, final ResourceBundle resources) {
+ gameModeMenu.setOnKeyReleased(event -> {
+ if (event.getCode() == KeyCode.ESCAPE) {
+ onBackToMainMenu();
+ }
+ });
+ }
+
+ /**
+ * The method initializes new multi-player game.
+ */
+ @FXML
+ private void onNewMultiGame() {
+ GameConfig.setMultiPlayerGame();
+ SceneManager.changeScene(SceneManager.SceneEnum.GAME);
+ }
+
+ /**
+ * The method changes scene to the BotLevelMenu scene.
+ */
+ @FXML
+ private void onNewSingleGame() {
+ SceneManager.changeScene(SceneManager.SceneEnum.BOT_MENU);
+ }
+
+ /**
+ * The method changes scene to the MainMenu scene.
+ */
+ @FXML
+ private void onBackToMainMenu() {
+ SceneManager.changeScene(SceneManager.SceneEnum.MAIN_MENU);
+ }
+}
diff --git a/HW2-Tic-Tac-Toe/src/main/java/handlers/SceneManager.java b/HW2-Tic-Tac-Toe/src/main/java/handlers/SceneManager.java
new file mode 100644
index 0000000..5336273
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/src/main/java/handlers/SceneManager.java
@@ -0,0 +1,158 @@
+package handlers;
+
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Parent;
+import javafx.scene.Scene;
+import javafx.stage.Stage;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+
+/**
+ * This class provides simple interface to change scenes in the application.
+ *
+ * As a small bonus of this class, every scene will be initialized not more then one time.
+ */
+public class SceneManager {
+
+ public enum SceneEnum {
+ GAME, MAIN_MENU, MODE_MENU, BOT_MENU, STATS
+
+ }
+ private static Stage stage;
+
+ private static final LazyScene gameScene = new LazyScene("../game.fxml");
+
+ private static final LazyScene mainMenuScene = new LazyScene("../mainMenu.fxml");
+ private static final LazyScene modeMenuScene = new LazyScene("../modeMenu.fxml");
+ private static final LazyScene botMenuScene = new LazyScene("../botMenu.fxml");
+ private static final LazyScene statsScene = new LazyScene("../stats.fxml");
+ /**
+ * Initializes SceneManager with main stage.
+ *
+ * @param primaryStage stage to work with.
+ */
+ public static void initialize(final Stage primaryStage) {
+ stage = primaryStage;
+ }
+
+ /**
+ * The method changes a scene.
+ *
+ * @param sceneEnum describes the scene to call.
+ */
+ public static void changeScene(final SceneEnum sceneEnum) {
+ Scene newScene = null;
+ Handler newHandler = null;
+ switch (sceneEnum) {
+ case GAME:
+ newScene = gameScene.getScene();
+ newHandler = gameScene.getHandler();
+ break;
+ case MAIN_MENU:
+ newScene = mainMenuScene.getScene();
+ newHandler = mainMenuScene.getHandler();
+ break;
+ case MODE_MENU:
+ newScene = modeMenuScene.getScene();
+ newHandler = modeMenuScene.getHandler();
+ break;
+ case BOT_MENU:
+ newScene = botMenuScene.getScene();
+ newHandler = botMenuScene.getHandler();
+ break;
+ case STATS:
+ newScene = statsScene.getScene();
+ newHandler = statsScene.getHandler();
+ }
+ final double width = stage.getWidth();
+ final double height = stage.getHeight();
+
+ stage.setScene(newScene);
+ newHandler.onShow();
+
+ stage.setWidth(width);
+ stage.setHeight(height);
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ public static Handler getScene(final SceneEnum sceneEnum) {
+ switch (sceneEnum) {
+ case GAME:
+ return gameScene.getHandler();
+ case MAIN_MENU:
+ return mainMenuScene.getHandler();
+ case MODE_MENU:
+ return modeMenuScene.getHandler();
+ case BOT_MENU:
+ return botMenuScene.getHandler();
+ case STATS:
+ return statsScene.getHandler();
+ }
+ throw new NoSuchSceneException();
+ }
+
+
+ /**
+ * This class provides lazy initialization of scenes.
+ *
+ * We do not have a lot of overhead without this class in that game,
+ * but in general it might be a good pattern not to load every single scene
+ * on the game start, and to load them when they are required instead.
+ */
+ @SuppressWarnings("WeakerAccess")
+ private static class LazyScene {
+
+
+ private Scene scene;
+ private Handler handler;
+ private final String name;
+
+ public LazyScene(final String name) {
+ this.name = name;
+ }
+
+ public Scene getScene() {
+ if (scene == null) {
+ init();
+ }
+ return scene;
+ }
+
+ public Handler getHandler() {
+ if (handler == null) {
+ init();
+ }
+ return handler;
+ }
+
+ private void init() {
+ final FXMLLoader loader = new FXMLLoader();
+ final Parent root;
+ try {
+ root = loader.load(SceneManager.class.getResourceAsStream(name));
+ } catch (@NotNull final IOException e) {
+ throw new ErrorWhileLoadingScene(e);
+ }
+ handler = loader.getController();
+ scene = new Scene(root, 300, 400);
+ }
+ }
+
+ /**
+ * This wrapper is required, because most of IOExceptions here are caused by
+ * mistake in program, such as wrong files names and so on.
+ *
+ * Also, we might have no file cause user deleted it or something like that,
+ * but right now I am not able to fix it (download files from server for example),
+ * therefore it is more logical to throw a RuntimeException here.
+ */
+ private static class ErrorWhileLoadingScene extends RuntimeException {
+ @SuppressWarnings("unused")
+ ErrorWhileLoadingScene(final IOException e) {
+ }
+ }
+
+ private static class NoSuchSceneException extends RuntimeException {
+ }
+}
diff --git a/HW2-Tic-Tac-Toe/src/main/java/handlers/StatsHandler.java b/HW2-Tic-Tac-Toe/src/main/java/handlers/StatsHandler.java
new file mode 100644
index 0000000..a68cad4
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/src/main/java/handlers/StatsHandler.java
@@ -0,0 +1,43 @@
+package handlers;
+
+import game.Statistics;
+import javafx.fxml.FXML;
+import javafx.scene.control.Button;
+import javafx.scene.input.KeyCode;
+import javafx.scene.layout.StackPane;
+import org.jetbrains.annotations.NotNull;
+
+import java.net.URL;
+import java.util.ResourceBundle;
+
+public class StatsHandler implements Handler {
+
+ @FXML
+ private StackPane stats;
+ @FXML
+ private Button XWins;
+ @FXML
+ private Button OWins;
+ @FXML
+ private Button Draws;
+
+ @Override
+ public void initialize(final URL location, final ResourceBundle resources) {
+ stats.setOnKeyReleased(event -> {
+ if (event.getCode() == KeyCode.ESCAPE) {
+ onBackToGame();
+ }
+ });
+ }
+
+ void setStatistics(@NotNull final Statistics statistics) {
+ XWins.setText("X won " + statistics.getXWins() + " times.");
+ OWins.setText("O won " + statistics.getOWins() + " times.");
+ Draws.setText("Draw was " + statistics.getDraws() + " times.");
+ }
+
+ @FXML
+ private void onBackToGame() {
+ SceneManager.changeScene(SceneManager.SceneEnum.GAME);
+ }
+}
\ No newline at end of file
diff --git a/HW2-Tic-Tac-Toe/src/main/java/utils/CycleCollection.java b/HW2-Tic-Tac-Toe/src/main/java/utils/CycleCollection.java
new file mode 100644
index 0000000..91e8be5
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/src/main/java/utils/CycleCollection.java
@@ -0,0 +1,66 @@
+package utils;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.AbstractCollection;
+import java.util.Collection;
+import java.util.Iterator;
+
+/**
+ * This class provides cycle iterator for any collection.
+ *
+ * @param type of the elements.
+ */
+public class CycleCollection extends AbstractCollection {
+
+ private final Collection collection;
+
+ /**
+ * Wraps the given collection to the CycleCollection.
+ */
+ public CycleCollection(final Collection collection) {
+ this.collection = collection;
+ }
+
+ @Override
+ public int size() {
+ return collection.size();
+ }
+
+ @NotNull
+ @Override
+ public Iterator iterator() {
+ return collection.iterator();
+ }
+
+ /**
+ * The method returns new Iterator, which provides collection elements in cycle order.
+ *
+ * The iterator will always have next element, if collection is not empty.
+ *
+ * At the beginning iterator will point at the first element of the collection. If
+ * iterator points in the last element, next method will move it to the first one again.
+ */
+ @NotNull
+ public Iterator cycleIterator() {
+ return new Iterator<>() {
+ @NotNull
+ private Iterator iterator = collection.iterator();
+
+ @Override
+ public boolean hasNext() {
+ return collection.size() != 0;
+ }
+
+ @Override
+ public T next() {
+ final T result = iterator.next();
+ if (!iterator.hasNext()) {
+ iterator = collection.iterator();
+ }
+ return result;
+ }
+ };
+ }
+
+}
diff --git a/HW2-Tic-Tac-Toe/src/main/java/utils/Direction.java b/HW2-Tic-Tac-Toe/src/main/java/utils/Direction.java
new file mode 100644
index 0000000..fa5ce3b
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/src/main/java/utils/Direction.java
@@ -0,0 +1,25 @@
+package utils;
+
+public class Direction {
+
+ public final static Direction[] ANGLE_DIRECTIONS = {
+ new Direction(0, 1), new Direction(1, 0), new Direction(1, 1), new Direction(-1, 1)
+ };
+
+ private final int dx;
+ private final int dy;
+
+ @SuppressWarnings("WeakerAccess")
+ public Direction(final int dx, final int dy) {
+ this.dx = dx;
+ this.dy = dy;
+ }
+
+ public int dx() {
+ return dx;
+ }
+
+ public int dy() {
+ return dy;
+ }
+}
diff --git a/HW2-Tic-Tac-Toe/src/main/java/view/View.java b/HW2-Tic-Tac-Toe/src/main/java/view/View.java
new file mode 100644
index 0000000..d296929
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/src/main/java/view/View.java
@@ -0,0 +1,64 @@
+package view;
+
+import game.model.Cell;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.StackPane;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * View class to control game board.
+ *
+ * The main idea of separating this class from GameHandler class is to separate
+ * class, which listens to actions (Handler) from class that actually draws (View).
+ *
+ * Unfortunately, in that case I have to pass all mutable objects to this class.
+ */
+public class View {
+
+ private final static Image X = new Image("X.png");
+ private final static Image O = new Image("O.png");
+
+ private final StackPane[][] grid;
+
+ /**
+ * Creates view from the grid. Created view class will be able t control inner images
+ * of grid.
+ */
+ public View(final StackPane[][] grid) {
+ this.grid = grid;
+ }
+
+ /**
+ * The method sets image of the cell to the target one.
+ *
+ * @param x x-coordinate.
+ * @param y y-coordinate.
+ * @param cell cell state to put into target cell.
+ */
+ public void set(final int x, final int y, @NotNull final Cell cell) {
+ final ImageView imageView = (ImageView) grid[x][y].getChildren().iterator().next();
+
+ switch (cell) {
+ case X:
+ imageView.setImage(X);
+ break;
+ case O:
+ imageView.setImage(O);
+ break;
+ default:
+ imageView.setImage(null);
+ }
+ }
+
+ /**
+ * Clears the table.
+ */
+ public void clear() {
+ for (int i = 0; i < grid.length; i++) {
+ for (int j = 0; j < grid[i].length; j++) {
+ set(i, j, Cell.EMPTY);
+ }
+ }
+ }
+}
diff --git a/HW2-Tic-Tac-Toe/src/main/resources/O.png b/HW2-Tic-Tac-Toe/src/main/resources/O.png
new file mode 100644
index 0000000..d5a1ce9
Binary files /dev/null and b/HW2-Tic-Tac-Toe/src/main/resources/O.png differ
diff --git a/HW2-Tic-Tac-Toe/src/main/resources/X.png b/HW2-Tic-Tac-Toe/src/main/resources/X.png
new file mode 100644
index 0000000..8257859
Binary files /dev/null and b/HW2-Tic-Tac-Toe/src/main/resources/X.png differ
diff --git a/HW2-Tic-Tac-Toe/src/main/resources/background.jpg b/HW2-Tic-Tac-Toe/src/main/resources/background.jpg
new file mode 100644
index 0000000..c2e2152
Binary files /dev/null and b/HW2-Tic-Tac-Toe/src/main/resources/background.jpg differ
diff --git a/HW2-Tic-Tac-Toe/src/main/resources/botMenu.fxml b/HW2-Tic-Tac-Toe/src/main/resources/botMenu.fxml
new file mode 100644
index 0000000..72bff78
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/src/main/resources/botMenu.fxml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/HW2-Tic-Tac-Toe/src/main/resources/game.fxml b/HW2-Tic-Tac-Toe/src/main/resources/game.fxml
new file mode 100644
index 0000000..d717be5
--- /dev/null
+++ b/HW2-Tic-Tac-Toe/src/main/resources/game.fxml
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+