diff --git a/Task_2_3_1/.gitignore b/Task_2_3_1/.gitignore new file mode 100644 index 0000000..b63da45 --- /dev/null +++ b/Task_2_3_1/.gitignore @@ -0,0 +1,42 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/Task_2_3_1/.idea/.gitignore b/Task_2_3_1/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/Task_2_3_1/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/Task_2_3_1/.idea/gradle.xml b/Task_2_3_1/.idea/gradle.xml new file mode 100644 index 0000000..ce1c62c --- /dev/null +++ b/Task_2_3_1/.idea/gradle.xml @@ -0,0 +1,16 @@ + + + + + + + \ No newline at end of file diff --git a/Task_2_3_1/.idea/misc.xml b/Task_2_3_1/.idea/misc.xml new file mode 100644 index 0000000..b35a12a --- /dev/null +++ b/Task_2_3_1/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Task_2_3_1/.idea/vcs.xml b/Task_2_3_1/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/Task_2_3_1/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Task_2_3_1/build.gradle.kts b/Task_2_3_1/build.gradle.kts new file mode 100644 index 0000000..2316530 --- /dev/null +++ b/Task_2_3_1/build.gradle.kts @@ -0,0 +1,63 @@ +plugins { + java + jacoco + application + id("org.javamodularity.moduleplugin") version "1.8.15" + id("org.openjfx.javafxplugin") version "0.0.13" + id("org.beryx.jlink") version "2.25.0" +} + +group = "ru.nsu.nmashkin.task231" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +val junitVersion = "5.12.1" + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +tasks.withType { + options.encoding = "UTF-8" +} + +application { + mainModule.set("ru.nsu.nmashkin.task231") + mainClass.set("ru.nsu.nmashkin.task231.View") +} + +javafx { + version = "21.0.6" + modules = listOf("javafx.controls", "javafx.fxml") +} + +dependencies { + testImplementation("org.junit.jupiter:junit-jupiter-api:5.12.1") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.12.1") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + implementation("javax.annotation:javax.annotation-api:1.3.2") +} + +tasks.withType { + useJUnitPlatform() +} + +jlink { + imageZip.set(layout.buildDirectory.file("/distributions/app-${javafx.platform.classifier}.zip")) + options.set(listOf("--strip-debug", "--compress", "2", "--no-header-files", "--no-man-pages")) + launcher { + name = "app" + } +} + +tasks.jacocoTestReport { + reports { + xml.required.set(true) + html.required.set(true) // Optional: for readable browser view + } +} \ No newline at end of file diff --git a/Task_2_3_1/gradle/wrapper/gradle-wrapper.jar b/Task_2_3_1/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..1b33c55 Binary files /dev/null and b/Task_2_3_1/gradle/wrapper/gradle-wrapper.jar differ diff --git a/Task_2_3_1/gradle/wrapper/gradle-wrapper.properties b/Task_2_3_1/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ca025c8 --- /dev/null +++ b/Task_2_3_1/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/Task_2_3_1/gradlew b/Task_2_3_1/gradlew new file mode 100644 index 0000000..23d15a9 --- /dev/null +++ b/Task_2_3_1/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# 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 ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# 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 + if ! command -v java >/dev/null 2>&1 + then + 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 +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/Task_2_3_1/gradlew.bat b/Task_2_3_1/gradlew.bat new file mode 100644 index 0000000..db3a6ac --- /dev/null +++ b/Task_2_3_1/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@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=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@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="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 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! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/Task_2_3_1/settings.gradle.kts b/Task_2_3_1/settings.gradle.kts new file mode 100644 index 0000000..ab17e85 --- /dev/null +++ b/Task_2_3_1/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "Task_2_3_1" diff --git a/Task_2_3_1/src/main/java/module-info.java b/Task_2_3_1/src/main/java/module-info.java new file mode 100644 index 0000000..fefdd90 --- /dev/null +++ b/Task_2_3_1/src/main/java/module-info.java @@ -0,0 +1,9 @@ +module ru.nsu.nmashkin.task231 { + requires javafx.controls; + requires javafx.fxml; + requires java.compiler; + requires javafx.graphics; + + opens ru.nsu.nmashkin.task231 to javafx.fxml; + exports ru.nsu.nmashkin.task231; +} \ No newline at end of file diff --git a/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/BotLogic.java b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/BotLogic.java new file mode 100644 index 0000000..3d89ea9 --- /dev/null +++ b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/BotLogic.java @@ -0,0 +1,18 @@ +package ru.nsu.nmashkin.task231; + +/** + * Bot logic interface. + */ +public interface BotLogic { + /** + * Return direction to move next. + * + * @return . + */ + Direction nextMove(); + + /** + * Sets a bot related with logic. + */ + void setBot(Snake bot); +} diff --git a/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/Controller.java b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/Controller.java new file mode 100644 index 0000000..5e889b6 --- /dev/null +++ b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/Controller.java @@ -0,0 +1,172 @@ +package ru.nsu.nmashkin.task231; + +import java.util.HashMap; +import javafx.animation.AnimationTimer; +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; +import javafx.scene.input.KeyEvent; +import javafx.scene.paint.Color; +import javax.annotation.processing.Generated; + +/** + * . + */ +public class Controller { + @FXML + private Canvas canvas; + + private Model model; + private AnimationTimer gameLoop; + private long lastTick = 0L; + private final long tickDuration = 250_000_000L; + private GraphicsContext gc; + + @FXML + private void initialize() { + HashMap snakeColoring = new HashMap<>(); + snakeColoring.put(SnakePartType.HEAD, Color.DARKGREEN); + snakeColoring.put(SnakePartType.BODY, Color.GREEN); + snakeColoring.put(SnakePartType.TAIL, Color.LIGHTGREEN); + + HashMap bot1Coloring = new HashMap<>(); + bot1Coloring.put(SnakePartType.HEAD, Color.DARKBLUE); + bot1Coloring.put(SnakePartType.BODY, Color.BLUE); + bot1Coloring.put(SnakePartType.TAIL, Color.LIGHTBLUE); + + HashMap bot2Coloring = new HashMap<>(); + bot2Coloring.put(SnakePartType.HEAD, Color.DARKRED); + bot2Coloring.put(SnakePartType.BODY, Color.RED); + bot2Coloring.put(SnakePartType.TAIL, Color.LIGHTCORAL); + + model = new Model(10, 10, 50, + Direction.UP, 5, 20, + snakeColoring, bot1Coloring, bot2Coloring); + gameLoop = new AnimationTimer() { + @Override + public void handle(long now) { + if (now - lastTick >= tickDuration) { + MoveResult result = model.move(); + if (result != MoveResult.NEUTRAL) { + stop(); + gameOver(result); + } + + render(); + lastTick = now; + } + } + }; + + gc = canvas.getGraphicsContext2D(); + render(); + canvas.requestFocus(); + gameLoop.start(); + } + + private void gameOver(MoveResult result) { + if (gameLoop != null) { + gameLoop.stop(); + } + + Platform.runLater(() -> { + Alert alert = new Alert(Alert.AlertType.NONE); + alert.setTitle("Game Over"); + alert.setHeaderText("Game Over! You " + result); + alert.setContentText("Final Score: " + model.getScore()); + + ButtonType quitBtn = new ButtonType("Quit"); + alert.getButtonTypes().setAll(quitBtn); + + alert.showAndWait(); + Platform.exit(); + }); + } + + private void render() { + gc.clearRect(0, 0, canvas.getWidth(), canvas.getHeight()); + + drawGrid(); + drawFoods(); + + Snake player = model.getSnake(); + Snake bot1 = model.getBot1(); + Snake bot2 = model.getBot2(); + if (bot1.isDead()) { + drawBot(bot1); + } + if (bot2.isDead()) { + drawBot(bot2); + } + if (player.isDead()) { + drawSnake(); + } + if (!player.isDead()) { + drawSnake(); + } + if (!bot1.isDead()) { + drawBot(bot1); + } + if (!bot2.isDead()) { + drawBot(bot2); + } + } + + private void drawGrid() { + gc.setStroke(Color.LIGHTGRAY); + for (int i = 0; i <= model.getGridHeight(); i++) { + gc.strokeLine(0, i * model.getCellSize(), canvas.getWidth(), i * model.getCellSize()); + } + for (int i = 0; i <= model.getGridWidth(); i++) { + gc.strokeLine(i * model.getCellSize(), 0, i * model.getCellSize(), canvas.getHeight()); + } + } + + private void drawFoods() { + for (Food food : model.getFoods()) { + gc.setFill(Color.RED); + gc.fillRect((food.coords().x() + 0.4) * model.getCellSize(), + (food.coords().y() + 0.4) * model.getCellSize(), + 0.2 * model.getCellSize(), 0.2 * model.getCellSize()); + } + } + + private void drawSnake() { + for (SnakePart part : model.getSnake().getParts()) { + gc.setFill(model.getSnake().getColoring().get(part.type())); + + gc.fillRect((part.coords().x() + 0.15) * model.getCellSize(), + (part.coords().y() + 0.15) * model.getCellSize(), + 0.7 * model.getCellSize(), 0.7 * model.getCellSize()); + } + } + + private void drawBot(Snake bot) { + for (SnakePart part : bot.getParts()) { + gc.setFill(bot.getColoring().get(part.type())); + + gc.fillRect((part.coords().x() + 0.25) * model.getCellSize(), + (part.coords().y() + 0.25) * model.getCellSize(), + 0.5 * model.getCellSize(), 0.5 * model.getCellSize()); + } + } + + /** + * . + * + * @param e event. + */ + @Generated("Nothing to test") + public void handleKeyPress(KeyEvent e) { + switch (e.getCode()) { + case W, UP -> model.setDirection(Direction.UP); + case S, DOWN -> model.setDirection(Direction.DOWN); + case A, LEFT -> model.setDirection(Direction.LEFT); + case D, RIGHT -> model.setDirection(Direction.RIGHT); + default -> { } + } + } +} diff --git a/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/Direction.java b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/Direction.java new file mode 100644 index 0000000..632fe7b --- /dev/null +++ b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/Direction.java @@ -0,0 +1,11 @@ +package ru.nsu.nmashkin.task231; + +/** + * . + */ +public enum Direction { + UP, + DOWN, + LEFT, + RIGHT, +} diff --git a/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/EvilBot.java b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/EvilBot.java new file mode 100644 index 0000000..0443f49 --- /dev/null +++ b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/EvilBot.java @@ -0,0 +1,156 @@ +package ru.nsu.nmashkin.task231; + +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Set; +import javafx.scene.paint.Color; +import javax.annotation.processing.Generated; + +/** + * Bot that really wants to steal player's food. + */ +public class EvilBot implements Snake { + private final Deque snake = new LinkedList<>(); + private final HashMap coloring; + private final Set obstacles; + private final BotLogic botLogic; + private boolean dead = false; + + /** + * . + * + * @param position . + * @param coloring . + * @param obstacles . + */ + public EvilBot(Point position, HashMap coloring, + Set obstacles, BotLogic botLogic) { + this.coloring = coloring; + this.obstacles = obstacles; + this.botLogic = botLogic; + + snake.push(new SnakePart(position, SnakePartType.HEAD)); + + botLogic.setBot(this); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean move(Direction direction) { + assert snake.peek() != null; + + if (dead) { + return false; + } + + if (direction == null) { + direction = botLogic.nextMove(); + } + + SnakePart currHead = snake.pop(); + Point newHeadCoords = currHead.coords().applyDirection(direction); + if (hitObstacle(newHeadCoords)) { + snake.push(currHead); + return false; + } + + snake.push(new SnakePart(currHead.coords(), + snake.isEmpty() ? SnakePartType.TAIL : SnakePartType.BODY)); + snake.push(new SnakePart(newHeadCoords, SnakePartType.HEAD)); + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean hitObstacle(Point point) { + Set newObstacles = new HashSet<>(Set.copyOf(snake)); + newObstacles.addAll(obstacles); + return newObstacles.stream().anyMatch(obstacle -> obstacle.isHit(point)); + } + + /** + * {@inheritDoc} + */ + @Override + public Point eatFood(Set foods) { + Food foodToEat = foods.stream() + .filter(food -> food.coords().equals(head().coords())) + .findFirst().orElse(null); + + if (foodToEat == null) { + SnakePart freedUp = snake.removeLast(); + if (snake.size() > 1) { + SnakePart tail = snake.removeLast(); + snake.addLast(new SnakePart(tail.coords(), SnakePartType.TAIL)); + } + return freedUp.coords(); + } + + foods.remove(foodToEat); + return null; + } + + /** + * {@inheritDoc} + */ + @Override + @Generated("Untested because getter") + public SnakePart head() { + assert snake.peek() != null; + + return snake.peek(); + } + + /** + * {@inheritDoc} + */ + @Override + @Generated("Untested because getter") + public SnakePart tail() { + assert snake.peek() != null; + + return snake.peekLast(); + } + + /** + * {@inheritDoc} + */ + @Override + @Generated("Untested because getter") + public Deque getParts() { + return snake; + } + + /** + * {@inheritDoc} + */ + @Override + @Generated("Untested because getter") + public HashMap getColoring() { + return coloring; + } + + /** + * {@inheritDoc} + */ + @Override + public void kill() { + dead = true; + coloring.replaceAll((partType, color) -> Color.GRAY); + } + + /** + * {@inheritDoc} + */ + @Override + @Generated("Untested because getter") + public boolean isDead() { + return dead; + } +} diff --git a/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/Food.java b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/Food.java new file mode 100644 index 0000000..62013b1 --- /dev/null +++ b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/Food.java @@ -0,0 +1,6 @@ +package ru.nsu.nmashkin.task231; + +/** + * . + */ +public record Food(Point coords) { } diff --git a/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/GeneralDirectionLogic.java b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/GeneralDirectionLogic.java new file mode 100644 index 0000000..993e46e --- /dev/null +++ b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/GeneralDirectionLogic.java @@ -0,0 +1,78 @@ +package ru.nsu.nmashkin.task231; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Set; +import java.util.stream.Collectors; +import javafx.util.Pair; + +/** + * . + */ +public class GeneralDirectionLogic implements BotLogic { + private final Set foods; + private final HashMap variants = new HashMap<>(); + private Snake bot; + + /** + * . + * + * @param foods . + */ + public GeneralDirectionLogic(Set foods) { + this.foods = foods; + } + + /** + * If far from foods, try to go in the general direction towards them. + * If close, go to the closest. + * + * @return . + */ + @Override + public Direction nextMove() { + assembleVariants(); + if (variants.isEmpty()) { + return Direction.UP; // Loser + } + + return variants.keySet().stream() + .map(direction -> new Pair<>(foodDistanceSum(direction), direction)) + .min(Comparator.comparing(Pair::getKey)).get().getValue(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setBot(Snake bot) { + this.bot = bot; + } + + private void assembleVariants() { + Point headCell = bot.head().coords(); + + variants.clear(); + variants.putAll(Arrays.stream(Direction.values()) + .filter(direction -> !bot.hitObstacle(headCell.applyDirection(direction))) + .collect(Collectors.toMap( + direction -> direction, + headCell::applyDirection + ))); + } + + private double foodDistanceSum(Direction direction) { + double result = 0; + for (Food food : foods) { + double distance = variants.get(direction).distance(food.coords()); + if (distance <= 3) { + return foods.stream().map(fd -> variants.get(direction).distance(fd.coords())) + .min(Comparator.comparingDouble(Double::doubleValue)).get(); + } + + result += distance; + } + return result; + } +} diff --git a/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/GridBorder.java b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/GridBorder.java new file mode 100644 index 0000000..c0a1ee2 --- /dev/null +++ b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/GridBorder.java @@ -0,0 +1,18 @@ +package ru.nsu.nmashkin.task231; + +/** + * Grid border. + * + * @param gridWidth . + * @param gridHeight . + */ +public record GridBorder(int gridWidth, int gridHeight) implements Obstacle { + /** + * {@inheritDoc} + */ + @Override + public boolean isHit(Point point) { + return point.x() < 0 || point.x() >= gridWidth + || point.y() < 0 || point.y() >= gridHeight; + } +} diff --git a/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/Launcher.java b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/Launcher.java new file mode 100644 index 0000000..13b4d27 --- /dev/null +++ b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/Launcher.java @@ -0,0 +1,12 @@ +package ru.nsu.nmashkin.task231; + +import javafx.application.Application; + +/** + * . + */ +public class Launcher { + public static void main(String[] args) { + Application.launch(View.class, args); + } +} diff --git a/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/Model.java b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/Model.java new file mode 100644 index 0000000..10494c0 --- /dev/null +++ b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/Model.java @@ -0,0 +1,251 @@ +package ru.nsu.nmashkin.task231; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Random; +import java.util.Set; +import javafx.scene.paint.Color; + +/** + * . + */ +public class Model { + private final Random random = new Random(); + private final int gridWidth; + private final int gridHeight; + private final int cellSize; + private final int maxScore; + private final Snake player; + private final Snake bot1; + private final Snake bot2; + private final Set foods = new HashSet<>(); + private final Set freeCells = new HashSet<>(); + private final Set obstacles = new HashSet<>(); + private Direction direction; + private Direction requestedDirection; + private int score = 0; + private int bot1Score = 0; + private int bot2Score = 0; + + /** + * . + * + * @param gridWidth . + * @param gridHeight . + * @param cellSize . + * @param direction . + * @param foodCount . + * @param maxScore . + */ + public Model(int gridWidth, int gridHeight, int cellSize, + Direction direction, int foodCount, int maxScore, + HashMap snakeColoring, + HashMap bot1Coloring, + HashMap bot2Coloring) { + for (int x = 0; x < gridWidth; x++) { + for (int y = 0; y < gridHeight; y++) { + Point point = new Point(x, y); + freeCells.add(point); + } + } + + obstacles.add(new GridBorder(gridWidth, gridHeight)); + + this.gridWidth = gridWidth; + this.gridHeight = gridHeight; + this.cellSize = cellSize; + this.requestedDirection = direction; + this.maxScore = maxScore; + + Point snakeStart = new Point(gridWidth / 4, gridHeight / 2); + player = new Player(snakeStart, snakeColoring, obstacles); + freeCells.remove(snakeStart); + + Point bot1Start = new Point(3 * gridWidth / 4, gridHeight / 2); + bot1 = new PeacefulBot(bot1Start, bot1Coloring, + obstacles, new GeneralDirectionLogic(foods)); + freeCells.remove(bot1Start); + + Point bot2Start = new Point(3 * gridWidth / 4, gridHeight / 4); + bot2 = new EvilBot(bot2Start, bot2Coloring, + obstacles, new PlayerRacerLogic(foods, player)); + freeCells.remove(bot2Start); + + for (int i = 0; i < foodCount; i++) { + generateNewFood(); + } + } + + /** + * Move the snake and the bot. + * + * @return move result. + */ + public MoveResult move() { + direction = requestedDirection; + + if (player.move(direction)) { + freeCells.remove(player.head().coords()); + } else { + player.kill(); + } + + if (bot1.move(null)) { + freeCells.remove(bot1.head().coords()); + } else { + bot1.kill(); + } + + if (bot2.move(null)) { + freeCells.remove(bot2.head().coords()); + } else { + bot2.kill(); + } + + if (bot1.isDead() && bot2.isDead() && player.isDead()) { + return MoveResult.TIE; + } + + Point freedUp = player.eatFood(foods); + if (freedUp != null) { + freeCells.remove(freedUp); + } else if (!player.isDead()) { + score++; + if (score >= maxScore) { + return MoveResult.WIN; + } + generateNewFood(); + } + + freedUp = bot1.eatFood(foods); + if (freedUp != null) { + freeCells.remove(freedUp); + } else if (!bot1.isDead()) { + bot1Score++; + if (bot1Score >= maxScore) { + return MoveResult.LOSE; + } + generateNewFood(); + } + + freedUp = bot2.eatFood(foods); + if (freedUp != null) { + freeCells.remove(freedUp); + } else if (!bot2.isDead()) { + bot2Score++; + if (bot2Score >= maxScore) { + return MoveResult.LOSE; + } + generateNewFood(); + } + + return MoveResult.NEUTRAL; + } + + /** + * . + * + * @param direction . + */ + public void setDirection(Direction direction) { + if (direction == Direction.UP && this.direction == Direction.DOWN + || direction == Direction.DOWN && this.direction == Direction.UP + || direction == Direction.LEFT && this.direction == Direction.RIGHT + || direction == Direction.RIGHT && this.direction == Direction.LEFT) { + return; + } + + this.requestedDirection = direction; + } + + private void generateNewFood() { + if (freeCells.isEmpty()) { + return; + } + + Point newFoodCell = freeCells.toArray(Point[]::new)[random.nextInt(freeCells.size())]; + foods.add(new Food(newFoodCell)); + freeCells.remove(newFoodCell); + } + + /** + * . + * + * @return . + */ + public Snake getSnake() { + return player; + } + + /** + * . + * + * @return . + */ + public Snake getBot1() { + return bot1; + } + + /** + * . + * + * @return . + */ + public Snake getBot2() { + return bot2; + } + + /** + * . + * + * @return . + */ + public int getGridHeight() { + return gridHeight; + } + + /** + * . + * + * @return . + */ + public int getGridWidth() { + return gridWidth; + } + + /** + * . + * + * @return . + */ + public int getCellSize() { + return cellSize; + } + + /** + * . + * + * @return . + */ + public Set getFoods() { + return foods; + } + + /** + * . + * + * @return . + */ + public int getScore() { + return score; + } + + /** + * . + * + * @return . + */ + public Direction getDirection() { + return direction; + } +} diff --git a/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/MoveResult.java b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/MoveResult.java new file mode 100644 index 0000000..6d3682e --- /dev/null +++ b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/MoveResult.java @@ -0,0 +1,14 @@ +package ru.nsu.nmashkin.task231; + +import javax.annotation.processing.Generated; + +/** + * . + */ +@Generated("Nothing to test") +public enum MoveResult { + LOSE, + WIN, + TIE, + NEUTRAL, +} diff --git a/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/Obstacle.java b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/Obstacle.java new file mode 100644 index 0000000..963b1bd --- /dev/null +++ b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/Obstacle.java @@ -0,0 +1,14 @@ +package ru.nsu.nmashkin.task231; + +/** + * Obstacle interface. + */ +public interface Obstacle { + /** + * Is the obstacle hit at the point. + * + * @param point . + * @return true if yes. + */ + boolean isHit(Point point); +} diff --git a/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/PeacefulBot.java b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/PeacefulBot.java new file mode 100644 index 0000000..e365d11 --- /dev/null +++ b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/PeacefulBot.java @@ -0,0 +1,156 @@ +package ru.nsu.nmashkin.task231; + +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Set; +import javafx.scene.paint.Color; +import javax.annotation.processing.Generated; + +/** + * Bot that just minds its own business. + */ +public class PeacefulBot implements Snake { + private final Deque snake = new LinkedList<>(); + private final HashMap coloring; + private final Set obstacles; + private final BotLogic botLogic; + private boolean dead = false; + + /** + * . + * + * @param position . + * @param coloring . + * @param obstacles . + */ + public PeacefulBot(Point position, HashMap coloring, + Set obstacles, BotLogic botLogic) { + this.coloring = coloring; + this.obstacles = obstacles; + this.botLogic = botLogic; + + snake.push(new SnakePart(position, SnakePartType.HEAD)); + + botLogic.setBot(this); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean move(Direction direction) { + assert snake.peek() != null; + + if (dead) { + return false; + } + + if (direction == null) { + direction = botLogic.nextMove(); + } + + SnakePart currHead = snake.pop(); + Point newHeadCoords = currHead.coords().applyDirection(direction); + if (hitObstacle(newHeadCoords)) { + snake.push(currHead); + return false; + } + + snake.push(new SnakePart(currHead.coords(), + snake.isEmpty() ? SnakePartType.TAIL : SnakePartType.BODY)); + snake.push(new SnakePart(newHeadCoords, SnakePartType.HEAD)); + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean hitObstacle(Point point) { + Set newObstacles = new HashSet<>(Set.copyOf(snake)); + newObstacles.addAll(obstacles); + return newObstacles.stream().anyMatch(obstacle -> obstacle.isHit(point)); + } + + /** + * {@inheritDoc} + */ + @Override + public Point eatFood(Set foods) { + Food foodToEat = foods.stream() + .filter(food -> food.coords().equals(head().coords())) + .findFirst().orElse(null); + + if (foodToEat == null) { + SnakePart freedUp = snake.removeLast(); + if (snake.size() > 1) { + SnakePart tail = snake.removeLast(); + snake.addLast(new SnakePart(tail.coords(), SnakePartType.TAIL)); + } + return freedUp.coords(); + } + + foods.remove(foodToEat); + return null; + } + + /** + * {@inheritDoc} + */ + @Override + @Generated("Untested because getter") + public SnakePart head() { + assert snake.peek() != null; + + return snake.peek(); + } + + /** + * {@inheritDoc} + */ + @Override + @Generated("Untested because getter") + public SnakePart tail() { + assert snake.peek() != null; + + return snake.peekLast(); + } + + /** + * {@inheritDoc} + */ + @Override + @Generated("Untested because getter") + public Deque getParts() { + return snake; + } + + /** + * {@inheritDoc} + */ + @Override + @Generated("Untested because getter") + public HashMap getColoring() { + return coloring; + } + + /** + * {@inheritDoc} + */ + @Override + public void kill() { + dead = true; + coloring.replaceAll((partType, color) -> Color.GRAY); + } + + /** + * {@inheritDoc} + */ + @Override + @Generated("Untested because getter") + public boolean isDead() { + return dead; + } +} diff --git a/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/Player.java b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/Player.java new file mode 100644 index 0000000..c36a182 --- /dev/null +++ b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/Player.java @@ -0,0 +1,152 @@ +package ru.nsu.nmashkin.task231; + +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Set; +import javafx.scene.paint.Color; +import javax.annotation.processing.Generated; + +/** + * . + */ +public class Player implements Snake { + private final Deque snake = new LinkedList<>(); + private final HashMap coloring; + private final Set obstacles; + private boolean dead = false; + + /** + * . + * + * @param position . + * @param coloring . + * @param obstacles . + */ + public Player(Point position, HashMap coloring, + Set obstacles) { + this.coloring = coloring; + this.obstacles = obstacles; + + snake.push(new SnakePart(position, SnakePartType.HEAD)); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean move(Direction direction) { + assert snake.peek() != null; + + if (dead) { + return false; + } + + SnakePart currHead = snake.pop(); + Point newHeadCoords = currHead.coords().applyDirection(direction); + if (hitObstacle(newHeadCoords)) { + snake.push(currHead); + return false; + } + + snake.push(new SnakePart(currHead.coords(), + snake.isEmpty() ? SnakePartType.TAIL : SnakePartType.BODY)); + snake.push(new SnakePart(newHeadCoords, SnakePartType.HEAD)); + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean hitObstacle(Point point) { + Set newObstacles = new HashSet<>(Set.copyOf(snake)); + newObstacles.addAll(obstacles); + return newObstacles.stream().anyMatch(obstacle -> obstacle.isHit(point)); + } + + /** + * {@inheritDoc} + */ + @Override + public Point eatFood(Set foods) { + if (dead) { + return null; + } + + Food foodToEat = foods.stream() + .filter(food -> food.coords().equals(head().coords())) + .findFirst().orElse(null); + + if (foodToEat == null) { + SnakePart freedUp = snake.removeLast(); + if (snake.size() > 1) { + SnakePart tail = snake.removeLast(); + snake.addLast(new SnakePart(tail.coords(), SnakePartType.TAIL)); + } + return freedUp.coords(); + } + + foods.remove(foodToEat); + return null; + } + + /** + * {@inheritDoc} + */ + @Override + @Generated("Untested because getter") + public SnakePart head() { + assert snake.peek() != null; + + return snake.peek(); + } + + /** + * {@inheritDoc} + */ + @Override + @Generated("Untested because getter") + public SnakePart tail() { + assert snake.peek() != null; + + return snake.peekLast(); + } + + /** + * {@inheritDoc} + */ + @Override + @Generated("Untested because getter") + public Deque getParts() { + return snake; + } + + /** + * {@inheritDoc} + */ + @Override + @Generated("Untested because getter") + public HashMap getColoring() { + return coloring; + } + + /** + * {@inheritDoc} + */ + @Override + public void kill() { + dead = true; + coloring.replaceAll((partType, color) -> Color.GRAY); + } + + /** + * {@inheritDoc} + */ + @Override + @Generated("Untested because getter") + public boolean isDead() { + return dead; + } +} diff --git a/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/PlayerRacerLogic.java b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/PlayerRacerLogic.java new file mode 100644 index 0000000..4fe9a22 --- /dev/null +++ b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/PlayerRacerLogic.java @@ -0,0 +1,72 @@ +package ru.nsu.nmashkin.task231; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Set; +import java.util.stream.Collectors; +import javafx.util.Pair; + +/** + * . + */ +public class PlayerRacerLogic implements BotLogic { + private final Set foods; + private final HashMap variants = new HashMap<>(); + private final Snake player; + private Snake bot; + + /** + * . + * + * @param foods . + * @param player . + */ + public PlayerRacerLogic(Set foods, Snake player) { + this.foods = foods; + this.player = player; + } + + /** + * Finds closest food to player and goes there. + * + * @return . + */ + @Override + public Direction nextMove() { + assembleVariants(); + if (variants.isEmpty()) { + return Direction.UP; // Loser + } + + Point playerHead = player.head().coords(); + Point playerGoal = foods.stream().map(Food::coords) + .min(Comparator.comparingDouble(playerHead::distance)).orElse(playerHead); + + return variants.keySet().stream() + .map(dir -> new Pair<>(playerGoal.distance(variants.get(dir)), dir)) + .min(Comparator.comparing(Pair::getKey)).get().getValue(); + } + + private void assembleVariants() { + Point headCell = bot.head().coords(); + + variants.clear(); + variants.putAll(Arrays.stream(Direction.values()) + .filter(direction -> !bot.hitObstacle(headCell.applyDirection(direction))) + .collect(Collectors.toMap( + direction -> direction, + headCell::applyDirection + ))); + } + + /** + * . + * + * @param bot . + */ + @Override + public void setBot(Snake bot) { + this.bot = bot; + } +} diff --git a/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/Point.java b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/Point.java new file mode 100644 index 0000000..61cf1dc --- /dev/null +++ b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/Point.java @@ -0,0 +1,31 @@ +package ru.nsu.nmashkin.task231; + +/** + * . + */ +public record Point(int x, int y) { + /** + * Needed for Model::move(). + * + * @param direction . + * @return new Point. + */ + public Point applyDirection(Direction direction) { + return switch (direction) { + case UP -> new Point(x, y - 1); + case DOWN -> new Point(x, y + 1); + case LEFT -> new Point(x - 1, y); + case RIGHT -> new Point(x + 1, y); + }; + } + + /** + * . + * + * @param other . + * @return . + */ + public double distance(Point other) { + return Math.sqrt((other.x - x) * (other.x - x) + (other.y - y) * (other.y - y)); + } +} diff --git a/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/Snake.java b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/Snake.java new file mode 100644 index 0000000..d265c79 --- /dev/null +++ b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/Snake.java @@ -0,0 +1,75 @@ +package ru.nsu.nmashkin.task231; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import javafx.scene.paint.Color; + +/** + * Snake interface. + */ +public interface Snake { + /** + * Move the Snake in the direction. + * + * @param direction . + * @return true if successful. + */ + boolean move(Direction direction); + + /** + * Try to eat the food in head position. + * + * @return freed up position if the Snake shrunk, + * null else. + */ + Point eatFood(Set foods); + + /** + * Get collection of snake parts. + * + * @return . + */ + Collection getParts(); + + /** + * Get snake head. + * + * @return . + */ + SnakePart head(); + + /** + * Get snake tail. + * + * @return . + */ + SnakePart tail(); + + /** + * Get snake coloring. + * + * @return . + */ + Map getColoring(); + + /** + * Does the point hit any obstacles. + * + * @param point . + * @return true is yes. + */ + boolean hitObstacle(Point point); + + /** + * Disables the Snake. + */ + void kill(); + + /** + * Is the Snake dead. + * + * @return true if yes. + */ + boolean isDead(); +} diff --git a/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/SnakePart.java b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/SnakePart.java new file mode 100644 index 0000000..e464ff2 --- /dev/null +++ b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/SnakePart.java @@ -0,0 +1,14 @@ +package ru.nsu.nmashkin.task231; + +import javax.annotation.processing.Generated; + +/** + * . + */ +@Generated("Nothing to test") +public record SnakePart(Point coords, SnakePartType type) implements Obstacle { + @Override + public boolean isHit(Point point) { + return point.equals(coords); + } +} diff --git a/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/SnakePartType.java b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/SnakePartType.java new file mode 100644 index 0000000..7fcde78 --- /dev/null +++ b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/SnakePartType.java @@ -0,0 +1,13 @@ +package ru.nsu.nmashkin.task231; + +import javax.annotation.processing.Generated; + +/** + * . + */ +@Generated("Nothing to test") +public enum SnakePartType { + BODY, + HEAD, + TAIL, +} diff --git a/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/View.java b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/View.java new file mode 100644 index 0000000..e6e1c0a --- /dev/null +++ b/Task_2_3_1/src/main/java/ru/nsu/nmashkin/task231/View.java @@ -0,0 +1,37 @@ +package ru.nsu.nmashkin.task231; + +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Stage; +import javax.annotation.processing.Generated; + +/** + * . + */ +public class View extends Application { + /** + * . + * + * @param stage the primary stage for this application, onto which + * the application scene can be set. + * Applications may create other stages, if needed, but they will not be + * primary stages. + * @throws Exception it does do so. + */ + @Generated("Nothing to test") + @Override + public void start(Stage stage) throws Exception { + FXMLLoader loader = new FXMLLoader(getClass().getResource("view.fxml")); + Parent root = loader.load(); + + Controller controller = loader.getController(); + Scene scene = new Scene(root); + scene.setOnKeyPressed(controller::handleKeyPress); + stage.setTitle("Snake race"); + stage.setScene(scene); + stage.setResizable(false); + stage.show(); + } +} diff --git a/Task_2_3_1/src/main/resources/ru/nsu/nmashkin/task231/view.fxml b/Task_2_3_1/src/main/resources/ru/nsu/nmashkin/task231/view.fxml new file mode 100644 index 0000000..c2f7b52 --- /dev/null +++ b/Task_2_3_1/src/main/resources/ru/nsu/nmashkin/task231/view.fxml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/Task_2_3_1/src/test/java/ru/nsu/nmashkin/task231/EvilBotTest.java b/Task_2_3_1/src/test/java/ru/nsu/nmashkin/task231/EvilBotTest.java new file mode 100644 index 0000000..9dd0d5a --- /dev/null +++ b/Task_2_3_1/src/test/java/ru/nsu/nmashkin/task231/EvilBotTest.java @@ -0,0 +1,111 @@ +package ru.nsu.nmashkin.task231; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; +import javafx.scene.paint.Color; +import org.junit.jupiter.api.Test; + +class EvilBotTest { + + @Test + void move() { + Set foods = new HashSet<>(); + foods.add(new Food(new Point(0, 0))); + foods.add(new Food(new Point(2, 0))); + foods.add(new Food(new Point(4, 0))); + Set obstacles = new HashSet<>(); + obstacles.add(new GridBorder(5, 5)); + Snake player = new Player(new Point(4, 4), null, obstacles); + PlayerRacerLogic playerRacerLogic = new PlayerRacerLogic(foods, player); + Snake bot = new PeacefulBot(new Point(2, 4), null, + obstacles, playerRacerLogic); + + bot.move(null); + + assertEquals(new Point(2, 3), bot.head().coords()); + } + + @Test + void move_loser() { + Set foods = new HashSet<>(); + Set obstacles = new HashSet<>(); + obstacles.add(new GridBorder(1, 1)); + Snake player = new Player(new Point(4, 4), null, obstacles); + PlayerRacerLogic playerRacerLogic = new PlayerRacerLogic(foods, player); + Snake bot = new EvilBot(new Point(0, 0), null, + obstacles, playerRacerLogic); + + assertFalse(bot.move(null)); + } + + @Test + void hitObstacle() { + Set foods = new HashSet<>(); + Set obstacles = new HashSet<>(); + obstacles.add(new GridBorder(1, 1)); + Snake player = new Player(new Point(4, 4), null, obstacles); + PlayerRacerLogic playerRacerLogic = new PlayerRacerLogic(foods, player); + Snake bot = new EvilBot(new Point(0, 0), null, + obstacles, playerRacerLogic); + + assertTrue(bot.hitObstacle(new Point(1, 0))); + } + + @Test + void eatFood() { + Set foods = new HashSet<>(); + foods.add(new Food(new Point(2, 1))); + Set obstacles = new HashSet<>(); + obstacles.add(new GridBorder(5, 5)); + Snake player = new Player(new Point(4, 4), null, obstacles); + PlayerRacerLogic playerRacerLogic = new PlayerRacerLogic(foods, player); + Snake bot = new EvilBot(new Point(2, 2), null, + obstacles, playerRacerLogic); + + bot.move(null); + + assertNull(bot.eatFood(foods)); + } + + @Test + void eatFood_shrink() { + Set foods = new HashSet<>(); + Set obstacles = new HashSet<>(); + obstacles.add(new GridBorder(5, 5)); + Snake player = new Player(new Point(4, 4), null, obstacles); + PlayerRacerLogic playerRacerLogic = new PlayerRacerLogic(foods, player); + Snake bot = new EvilBot(new Point(2, 2), null, + obstacles, playerRacerLogic); + + bot.move(null); + + assertEquals(new Point(2, 2), bot.eatFood(foods)); + } + + @Test + void kill() { + HashMap coloring = new HashMap<>(); + coloring.put(SnakePartType.HEAD, Color.DARKGREEN); + coloring.put(SnakePartType.BODY, Color.GREEN); + coloring.put(SnakePartType.TAIL, Color.LIGHTGREEN); + + Set foods = new HashSet<>(); + Set obstacles = new HashSet<>(); + obstacles.add(new GridBorder(5, 5)); + Snake player = new Player(new Point(4, 4), null, obstacles); + PlayerRacerLogic playerRacerLogic = new PlayerRacerLogic(foods, player); + Snake bot = new EvilBot(new Point(2, 2), coloring, + obstacles, playerRacerLogic); + + bot.kill(); + + assertTrue(bot.isDead()); + assertTrue(coloring.values().stream().allMatch(color -> color == Color.GRAY)); + } +} \ No newline at end of file diff --git a/Task_2_3_1/src/test/java/ru/nsu/nmashkin/task231/ForCoverageTest.java b/Task_2_3_1/src/test/java/ru/nsu/nmashkin/task231/ForCoverageTest.java new file mode 100644 index 0000000..81a496d --- /dev/null +++ b/Task_2_3_1/src/test/java/ru/nsu/nmashkin/task231/ForCoverageTest.java @@ -0,0 +1,40 @@ +package ru.nsu.nmashkin.task231; + +import org.junit.jupiter.api.Test; + +class ForCoverageTest { + + @Test + void forCoverage() { + Model model = new Model(5, 5, 50, Direction.UP, + 5, 20, null, null, null); + model.getScore(); + model.getSnake(); + model.getFoods(); + model.getCellSize(); + model.getGridHeight(); + model.getGridWidth(); + model.getBot1(); + model.getDirection(); + + Snake player = new Player(new Point(2, 2), null, null); + player.head(); + player.tail(); + player.getParts(); + player.getColoring(); + player.isDead(); + + Snake bot = new PeacefulBot(new Point(2, 2), null, null, new GeneralDirectionLogic(null)); + bot.head(); + bot.tail(); + bot.getParts(); + bot.getColoring(); + bot.isDead(); + bot = new EvilBot(new Point(2, 2), null, null, new GeneralDirectionLogic(null)); + bot.head(); + bot.tail(); + bot.getParts(); + bot.getColoring(); + bot.isDead(); + } +} diff --git a/Task_2_3_1/src/test/java/ru/nsu/nmashkin/task231/GeneralDirectionLogicTest.java b/Task_2_3_1/src/test/java/ru/nsu/nmashkin/task231/GeneralDirectionLogicTest.java new file mode 100644 index 0000000..2b6c479 --- /dev/null +++ b/Task_2_3_1/src/test/java/ru/nsu/nmashkin/task231/GeneralDirectionLogicTest.java @@ -0,0 +1,53 @@ +package ru.nsu.nmashkin.task231; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.Test; + +class GeneralDirectionLogicTest { + + @Test + void nextMove_far() { + Set foods = new HashSet<>(); + foods.add(new Food(new Point(0, 0))); + foods.add(new Food(new Point(2, 0))); + foods.add(new Food(new Point(4, 0))); + GeneralDirectionLogic generalDirectionLogic = new GeneralDirectionLogic(foods); + Set obstacles = new HashSet<>(); + obstacles.add(new GridBorder(5, 5)); + Snake bot = new PeacefulBot(new Point(2, 4), null, + obstacles, generalDirectionLogic); + + assertEquals(Direction.UP, generalDirectionLogic.nextMove()); + } + + @Test + void nextMove_close() { + Set foods = new HashSet<>(); + foods.add(new Food(new Point(2, 1))); + foods.add(new Food(new Point(2, 4))); + foods.add(new Food(new Point(2, 4))); + foods.add(new Food(new Point(2, 4))); + GeneralDirectionLogic generalDirectionLogic = new GeneralDirectionLogic(foods); + Set obstacles = new HashSet<>(); + obstacles.add(new GridBorder(5, 5)); + Snake bot = new PeacefulBot(new Point(2, 2), null, + obstacles, generalDirectionLogic); + + assertEquals(Direction.UP, generalDirectionLogic.nextMove()); + } + + @Test + void nextMove_loser() { + Set foods = new HashSet<>(); + GeneralDirectionLogic generalDirectionLogic = new GeneralDirectionLogic(foods); + Set obstacles = new HashSet<>(); + obstacles.add(new GridBorder(1, 1)); + Snake bot = new PeacefulBot(new Point(0, 0), null, + obstacles, generalDirectionLogic); + + assertEquals(Direction.UP, generalDirectionLogic.nextMove()); + } +} \ No newline at end of file diff --git a/Task_2_3_1/src/test/java/ru/nsu/nmashkin/task231/ModelTest.java b/Task_2_3_1/src/test/java/ru/nsu/nmashkin/task231/ModelTest.java new file mode 100644 index 0000000..b35de9a --- /dev/null +++ b/Task_2_3_1/src/test/java/ru/nsu/nmashkin/task231/ModelTest.java @@ -0,0 +1,39 @@ +package ru.nsu.nmashkin.task231; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; +import org.junit.jupiter.api.Test; + +class ModelTest { + + @Test + void move() { + Model model = new Model(10, 10, 50, Direction.UP, + 25, 20, new HashMap<>(), new HashMap<>(), new HashMap<>()); + + for (int i = 0; i < 25; i++) { + MoveResult res = model.move(); + + if (res != MoveResult.NEUTRAL) { + assertNotEquals(MoveResult.WIN, res); + break; + } + } + + assertTrue(model.getScore() < 20); + } + + @Test + void setDirection() { + Model model = new Model(10, 10, 50, Direction.UP, + 5, 20, new HashMap<>(), new HashMap<>(), new HashMap<>()); + model.move(); + model.setDirection(Direction.RIGHT); + model.setDirection(Direction.DOWN); + model.move(); + assertEquals(Direction.RIGHT, model.getDirection()); + } +} diff --git a/Task_2_3_1/src/test/java/ru/nsu/nmashkin/task231/PeacefulBotTest.java b/Task_2_3_1/src/test/java/ru/nsu/nmashkin/task231/PeacefulBotTest.java new file mode 100644 index 0000000..19665d4 --- /dev/null +++ b/Task_2_3_1/src/test/java/ru/nsu/nmashkin/task231/PeacefulBotTest.java @@ -0,0 +1,123 @@ +package ru.nsu.nmashkin.task231; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; +import javafx.scene.paint.Color; +import org.junit.jupiter.api.Test; + +class PeacefulBotTest { + + @Test + void move_far() { + Set foods = new HashSet<>(); + foods.add(new Food(new Point(0, 0))); + foods.add(new Food(new Point(2, 0))); + foods.add(new Food(new Point(4, 0))); + GeneralDirectionLogic generalDirectionLogic = new GeneralDirectionLogic(foods); + Set obstacles = new HashSet<>(); + obstacles.add(new GridBorder(5, 5)); + Snake bot = new PeacefulBot(new Point(2, 4), null, + obstacles, generalDirectionLogic); + + bot.move(null); + + assertEquals(new Point(2, 3), bot.head().coords()); + } + + @Test + void move_close() { + Set foods = new HashSet<>(); + foods.add(new Food(new Point(2, 1))); + foods.add(new Food(new Point(2, 4))); + foods.add(new Food(new Point(2, 4))); + foods.add(new Food(new Point(2, 4))); + GeneralDirectionLogic generalDirectionLogic = new GeneralDirectionLogic(foods); + Set obstacles = new HashSet<>(); + obstacles.add(new GridBorder(5, 5)); + Snake bot = new PeacefulBot(new Point(2, 2), null, + obstacles, generalDirectionLogic); + + bot.move(null); + + assertEquals(new Point(2, 1), bot.head().coords()); + } + + @Test + void move_loser() { + Set foods = new HashSet<>(); + GeneralDirectionLogic generalDirectionLogic = new GeneralDirectionLogic(foods); + Set obstacles = new HashSet<>(); + obstacles.add(new GridBorder(1, 1)); + Snake bot = new PeacefulBot(new Point(0, 0), null, + obstacles, generalDirectionLogic); + + assertFalse(bot.move(null)); + } + + @Test + void hitObstacle() { + Set foods = new HashSet<>(); + GeneralDirectionLogic generalDirectionLogic = new GeneralDirectionLogic(foods); + Set obstacles = new HashSet<>(); + obstacles.add(new GridBorder(1, 1)); + Snake bot = new PeacefulBot(new Point(0, 0), null, + obstacles, generalDirectionLogic); + + assertTrue(bot.hitObstacle(new Point(1, 0))); + } + + @Test + void eatFood() { + Set foods = new HashSet<>(); + foods.add(new Food(new Point(2, 1))); + GeneralDirectionLogic generalDirectionLogic = new GeneralDirectionLogic(foods); + Set obstacles = new HashSet<>(); + obstacles.add(new GridBorder(5, 5)); + Snake bot = new PeacefulBot(new Point(2, 2), null, + obstacles, generalDirectionLogic); + + bot.move(null); + + assertNull(bot.eatFood(foods)); + } + + @Test + void eatFood_shrink() { + Set foods = new HashSet<>(); + GeneralDirectionLogic generalDirectionLogic = new GeneralDirectionLogic(foods); + Set obstacles = new HashSet<>(); + obstacles.add(new GridBorder(5, 5)); + Snake bot = new PeacefulBot(new Point(2, 2), null, + obstacles, generalDirectionLogic); + + bot.move(null); + + assertEquals(new Point(2, 2), bot.eatFood(foods)); + } + + @Test + void kill() { + HashMap coloring = new HashMap<>(); + coloring.put(SnakePartType.HEAD, Color.DARKGREEN); + coloring.put(SnakePartType.BODY, Color.GREEN); + coloring.put(SnakePartType.TAIL, Color.LIGHTGREEN); + + Set foods = new HashSet<>(); + GeneralDirectionLogic generalDirectionLogic = new GeneralDirectionLogic(foods); + Set obstacles = new HashSet<>(); + obstacles.add(new GridBorder(5, 5)); + Snake bot = new PeacefulBot(new Point(2, 2), coloring, + obstacles, generalDirectionLogic); + + bot.kill(); + + assertTrue(bot.isDead()); + assertTrue(coloring.values().stream().allMatch(color -> color == Color.GRAY)); + } +} \ No newline at end of file diff --git a/Task_2_3_1/src/test/java/ru/nsu/nmashkin/task231/PlayerRacerLogicTest.java b/Task_2_3_1/src/test/java/ru/nsu/nmashkin/task231/PlayerRacerLogicTest.java new file mode 100644 index 0000000..e03fa69 --- /dev/null +++ b/Task_2_3_1/src/test/java/ru/nsu/nmashkin/task231/PlayerRacerLogicTest.java @@ -0,0 +1,39 @@ +package ru.nsu.nmashkin.task231; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.Test; + +class PlayerRacerLogicTest { + + @Test + void nextMove() { + Set foods = new HashSet<>(); + foods.add(new Food(new Point(0, 0))); + foods.add(new Food(new Point(2, 0))); + foods.add(new Food(new Point(4, 0))); + Set obstacles = new HashSet<>(); + obstacles.add(new GridBorder(5, 5)); + Snake player = new Player(new Point(4, 4), null, obstacles); + PlayerRacerLogic playerRacerLogic = new PlayerRacerLogic(foods, player); + Snake bot = new PeacefulBot(new Point(2, 4), null, + obstacles, playerRacerLogic); + + assertEquals(Direction.UP, playerRacerLogic.nextMove()); + } + + @Test + void nextMove_loser() { + Set foods = new HashSet<>(); + Set obstacles = new HashSet<>(); + obstacles.add(new GridBorder(1, 1)); + Snake player = new Player(new Point(4, 4), null, obstacles); + PlayerRacerLogic playerRacerLogic = new PlayerRacerLogic(foods, player); + Snake bot = new EvilBot(new Point(0, 0), null, + obstacles, playerRacerLogic); + + assertEquals(Direction.UP, playerRacerLogic.nextMove()); + } +} \ No newline at end of file diff --git a/Task_2_3_1/src/test/java/ru/nsu/nmashkin/task231/PlayerTest.java b/Task_2_3_1/src/test/java/ru/nsu/nmashkin/task231/PlayerTest.java new file mode 100644 index 0000000..a1a3ce4 --- /dev/null +++ b/Task_2_3_1/src/test/java/ru/nsu/nmashkin/task231/PlayerTest.java @@ -0,0 +1,81 @@ +package ru.nsu.nmashkin.task231; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; +import javafx.scene.paint.Color; +import org.junit.jupiter.api.Test; + +class PlayerTest { + + @Test + void move() { + Set obstacles = new HashSet<>(); + obstacles.add(new GridBorder(5, 5)); + Snake player = new Player(new Point(2, 2), null, obstacles); + + boolean result = true; + result &= player.move(Direction.UP); + result &= player.move(Direction.RIGHT); + result &= player.move(Direction.DOWN); + + assertTrue(result); + assertFalse(player.move(Direction.LEFT)); + } + + @Test + void hitObstacle() { + Set obstacles = new HashSet<>(); + obstacles.add(new GridBorder(5, 5)); + Snake player = new Player(new Point(2, 2), null, obstacles); + + assertTrue(player.hitObstacle(new Point(5, 5))); + } + + @Test + void eatFood() { + Set obstacles = new HashSet<>(); + obstacles.add(new GridBorder(5, 5)); + Snake player = new Player(new Point(2, 2), null, obstacles); + Set foods = new HashSet<>(); + foods.add(new Food(new Point(2, 1))); + + player.move(Direction.UP); + + assertNull(player.eatFood(foods)); + } + + @Test + void eatFood_shrink() { + Set obstacles = new HashSet<>(); + obstacles.add(new GridBorder(5, 5)); + Snake player = new Player(new Point(2, 2), null, obstacles); + Set foods = new HashSet<>(); + + player.move(Direction.UP); + + assertEquals(new Point(2, 2), player.eatFood(foods)); + } + + @Test + void kill() { + HashMap coloring = new HashMap<>(); + coloring.put(SnakePartType.HEAD, Color.DARKGREEN); + coloring.put(SnakePartType.BODY, Color.GREEN); + coloring.put(SnakePartType.TAIL, Color.LIGHTGREEN); + + Set obstacles = new HashSet<>(); + obstacles.add(new GridBorder(5, 5)); + Snake player = new Player(new Point(2, 2), coloring, obstacles); + + player.kill(); + + assertTrue(player.isDead()); + assertTrue(coloring.values().stream().allMatch(color -> color == Color.GRAY)); + } +} \ No newline at end of file diff --git a/Task_2_3_1/src/test/java/ru/nsu/nmashkin/task231/PointTest.java b/Task_2_3_1/src/test/java/ru/nsu/nmashkin/task231/PointTest.java new file mode 100644 index 0000000..ada8e3e --- /dev/null +++ b/Task_2_3_1/src/test/java/ru/nsu/nmashkin/task231/PointTest.java @@ -0,0 +1,26 @@ +package ru.nsu.nmashkin.task231; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class PointTest { + + @Test + void applyDirection() { + Point p = new Point(6, 7); + p.applyDirection(Direction.UP); + p.applyDirection(Direction.RIGHT); + p.applyDirection(Direction.LEFT); + p.applyDirection(Direction.DOWN); + assertEquals(new Point(6, 7), p); + } + + @Test + void distance() { + Point p1 = new Point(6, 7); + Point p2 = new Point(6, 9); + + assertEquals(2, p1.distance(p2)); + } +}