diff --git a/solution/H02/.editorconfig b/solution/H02/.editorconfig new file mode 100644 index 0000000..38866d3 --- /dev/null +++ b/solution/H02/.editorconfig @@ -0,0 +1,12 @@ +# Editor configuration, see https://editorconfig.org + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true + +[{*.yml,*.json}] +indent_size = 2 diff --git a/solution/H02/.gitignore b/solution/H02/.gitignore new file mode 100644 index 0000000..7b957c2 --- /dev/null +++ b/solution/H02/.gitignore @@ -0,0 +1,87 @@ +### Intellij ### +.idea/ +*.iws +/out/ +*.iml +.idea_modules/ +atlassian-ide-plugin.xml + +### VS-Code ### +.vscode/ +.VSCodeCounter/ + +### Eclipse ### +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders +.externalToolBuilders/ +*.launch +.factorypath +.recommenders/ +.apt_generated/ +.project +.classpath + +### Linux ### +*~ +.fuse_hidden* +.directory +.Trash-* +.nfs* + +### macOS ### +.DS_Store +.AppleDouble +.LSOverride +Icon +._* +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### NetBeans ### +nbproject/private/ +build/ +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db +*.stackdump +[Dd]esktop.ini +$RECYCLE.BIN/ +*.lnk + +### Gradle ### +.gradle +/build/ +out/ +gradle-app.setting +!gradle-wrapper.jar +.gradletasknamecache + +*.hprof +screenshots/ +jagr.conf diff --git a/solution/H02/README.md b/solution/H02/README.md new file mode 100644 index 0000000..15491c9 --- /dev/null +++ b/solution/H02/README.md @@ -0,0 +1,4 @@ +# Musterlösung zu Hausübung 02 + +Beachten Sie die Hinweise zum Herunterladen, Importieren, Bearbeitern, Exportieren und Hochladen in unserem +[Studierenden-Guide](https://wiki.tudalgo.org/) diff --git a/solution/H02/build.gradle.kts b/solution/H02/build.gradle.kts new file mode 100644 index 0000000..237c4e8 --- /dev/null +++ b/solution/H02/build.gradle.kts @@ -0,0 +1,41 @@ +plugins { + alias(libs.plugins.algomate) + alias(libs.plugins.style) +} + +version = file("version").readLines().first() + +exercise { + assignmentId.set("h02") +} + +submission { + // ACHTUNG! + // Setzen Sie im folgenden Bereich Ihre TU-ID (NICHT Ihre Matrikelnummer!), Ihren Nachnamen und Ihren Vornamen + // in Anführungszeichen (z.B. "ab12cdef" für Ihre TU-ID) ein! + // BEISPIEL: + // studentId = "ab12cdef" + // firstName = "sol_first" + // lastName = "sol_last" + studentId = "ab12cdef" + firstName = "sol_first" + lastName = "sol_last" + + // Optionally require own tests for mainBuildSubmission task. Default is false + requireTests = false +} + +dependencies { + implementation(libs.fopbot) +} + +jagr { + graders { + val graderPublic by getting + val graderPrivate by creating { + parent(graderPublic) + graderName.set("FOP-2425-H02-Private") + rubricProviderName.set("h02.H02_RubricProvider") + } + } +} diff --git a/solution/H02/gradle/libs.versions.toml b/solution/H02/gradle/libs.versions.toml new file mode 100644 index 0000000..c23692d --- /dev/null +++ b/solution/H02/gradle/libs.versions.toml @@ -0,0 +1,6 @@ +[plugins] +algomate = { id = "org.tudalgo.algomate", version = "0.7.1" } +style = { id = "org.sourcegrade.style", version = "3.0.0" } + +[libraries] +fopbot = { module = "org.tudalgo:fopbot", version = "0.8.1" } diff --git a/solution/H02/gradle/wrapper/gradle-wrapper.jar b/solution/H02/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..c1962a7 Binary files /dev/null and b/solution/H02/gradle/wrapper/gradle-wrapper.jar differ diff --git a/solution/H02/gradle/wrapper/gradle-wrapper.properties b/solution/H02/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..36074ad --- /dev/null +++ b/solution/H02/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/solution/H02/gradlew b/solution/H02/gradlew new file mode 100755 index 0000000..aeb74cb --- /dev/null +++ b/solution/H02/gradlew @@ -0,0 +1,245 @@ +#!/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. +# + +############################################################################## +# +# 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/subprojects/plugins/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##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || 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=$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" && ! "$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=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=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 $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# 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/solution/H02/gradlew.bat b/solution/H02/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/solution/H02/gradlew.bat @@ -0,0 +1,92 @@ +@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 + +@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. +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 execute + +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 + +: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 %* + +: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/solution/H02/settings.gradle.kts b/solution/H02/settings.gradle.kts new file mode 100644 index 0000000..b15e0b9 --- /dev/null +++ b/solution/H02/settings.gradle.kts @@ -0,0 +1,11 @@ +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { +// mavenLocal() + maven("https://s01.oss.sonatype.org/content/repositories/snapshots") + maven("https://jitpack.io") + mavenCentral() + } +} + +rootProject.name = "H02-Root" diff --git a/solution/H02/src/graderPrivate/java/h02/FourWinsTest.java b/solution/H02/src/graderPrivate/java/h02/FourWinsTest.java new file mode 100644 index 0000000..7df3635 --- /dev/null +++ b/solution/H02/src/graderPrivate/java/h02/FourWinsTest.java @@ -0,0 +1,748 @@ +package h02; + +import fopbot.Direction; +import fopbot.Robot; +import fopbot.RobotFamily; +import fopbot.RobotTrace; +import fopbot.Transition; +import fopbot.World; +import h02.template.InputHandler; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; +import org.sourcegrade.jagr.api.rubric.TestForSubmission; +import org.tudalgo.algoutils.tutor.general.annotation.SkipAfterFirstFailedTest; +import org.tudalgo.algoutils.tutor.general.assertions.Assertions2; +import org.tudalgo.algoutils.tutor.general.assertions.Context; +import org.tudalgo.algoutils.tutor.general.assertions.PreCommentSupplier; +import org.tudalgo.algoutils.tutor.general.assertions.ResultOfObject; +import org.tudalgo.algoutils.tutor.general.json.JsonParameterSet; +import org.tudalgo.algoutils.tutor.general.json.JsonParameterSetTest; +import spoon.reflect.code.CtInvocation; +import spoon.reflect.code.CtLoop; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.declaration.CtMethod; +import spoon.reflect.reference.CtExecutableReference; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import static h02.TestUtils.getCtMethod; +import static h02.TestUtils.iterateMethodStatements; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.assertCallEquals; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.assertCallFalse; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.assertEquals; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.assertSame; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.assertTrue; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.call; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.callObject; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.contextBuilder; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.emptyContext; + +@TestForSubmission +@Timeout( + value = TestConstants.TEST_TIMEOUT_IN_SECONDS, + unit = TimeUnit.SECONDS, + threadMode = Timeout.ThreadMode.SEPARATE_THREAD +) +@SkipAfterFirstFailedTest(TestConstants.SKIP_AFTER_FIRST_FAILED_TEST) +public class FourWinsTest { + + private static final Consumer> SINGLE_LOOP_VA = iterator -> { + int loopStatements = 0; + + while (iterator.hasNext()) { + if (iterator.next() instanceof CtLoop) { + loopStatements++; + } + } + assertEquals(1, loopStatements, emptyContext(), result -> "Method does not use exactly one loop"); + }; + + private int worldHeight; + private int worldWidth; + private RobotFamily[][] stones; + private RobotFamily currentPlayer; + private Context.Builder baseContextBuilder; + + public void setup(JsonParameterSet params) { + worldHeight = params.getInt("worldHeight"); + worldWidth = params.getInt("worldWidth"); + List> paramStones = params.get("gameBoard"); + stones = new RobotFamily[worldHeight][worldWidth]; + for (int row = 0; row < worldHeight; row++) { + for (int col = 0; col < worldWidth; col++) { + stones[row][col] = robotFamilyLookup(paramStones.get(row).get(col)); + } + } + baseContextBuilder = contextBuilder() + .add("world height", worldHeight) + .add("world width", worldWidth) + .add("stones", stones); + if (params.availableKeys().contains("currentPlayer")) { + currentPlayer = robotFamilyLookup(params.get("currentPlayer")); + baseContextBuilder.add("currentPlayer", currentPlayer); + } + + World.setSize(worldWidth, worldHeight); + World.setDelay(0); + } + + @ParameterizedTest + @JsonParameterSetTest(value = "FourWinsTestValidateInput.json") + public void testValidateInputEdgeCases(final JsonParameterSet params) { + testValidateInput(params); + } + + @ParameterizedTest + @JsonParameterSetTest(value = "FourWinsTestValidateInputRandomCases.generated.json") + public void testValidateInputRandomCases(final JsonParameterSet params) { + testValidateInput(params); + } + + public void testValidateInput(final JsonParameterSet params) { + // get params + final int paramColumn = params.getInt("column"); + final int paramWidth = params.getInt("width"); + final int paramHeight = params.getInt("height"); + final List> paramStones = params.get("stones"); + final boolean expectedResult = params.getBoolean("expected result"); + + // write params to context + final var ParamsContext = params.toContext("expected result"); + final var cb = contextBuilder() + .add(ParamsContext) + .add("Method", "validateInput"); + + // init the world size + World.setSize(paramWidth, paramHeight); + + + // parse array and calculate result + RobotFamily[][] paramStonesArray = paramStones.stream() + .map(innerList -> innerList.stream() + .map(str -> "EMPTY".equals(str) ? null : "SQUARE_RED".equals(str) ? RobotFamily.SQUARE_RED : + RobotFamily.SQUARE_BLUE) + .toArray(RobotFamily[]::new)) + .toArray(RobotFamily[][]::new); + + + + final boolean actualResult = Assertions2.callObject( + () -> FourWins.validateInput( + paramColumn, + paramStonesArray + ), + cb.build(), + r -> "An error occurred during execution." + ); + + // validate result + Assertions2.assertEquals( + expectedResult, + actualResult, + cb.build(), + r -> "Invalid result." + ); + } + + @ParameterizedTest + @JsonParameterSetTest("FourWinsTestGameBoard.generated.json") + public void testGetDestinationRowFreeSlot(JsonParameterSet params) { + testGetDestinationRow(params, true); + } + + @ParameterizedTest + @JsonParameterSetTest("FourWinsTestGameBoard.generated.json") + public void testGetDestinationRowBlockedSlot(JsonParameterSet params) { + testGetDestinationRow(params, false); + } + + @Test + public void testGetDestinationRowVAnforderung() { + iterateMethodStatements(FourWins.class, + "getDestinationRow", + new Class[] {int.class, RobotFamily[][].class}, + SINGLE_LOOP_VA); + } + + @ParameterizedTest + @JsonParameterSetTest("FourWinsTestGameBoard.generated.json") + public void testDropStoneRobotCorrect(JsonParameterSet params) { + setup(params); + List firstFreeIndex = params.get("firstFreeIndex"); + + for (int col = 0; col < worldWidth; col++) { + if (firstFreeIndex.get(col) >= worldHeight) { + continue; + } + + World.getGlobalWorld().reset(); // clear entities + RobotFamily currentPlayer = col % 2 == 0 ? RobotFamily.SQUARE_RED : RobotFamily.SQUARE_BLUE; + Context context = baseContextBuilder + .add("column", col) + .add("currentPlayer", currentPlayer) + .build(); + + try { + final int finalCol = col; + call(() -> FourWins.dropStone(finalCol, stones, currentPlayer), context, result -> + "An exception occurred while invoking method dropStone. Result may be salvageable, continuing..."); + } catch (Throwable t) { + t.printStackTrace(System.err); + } + + List robots = World.getGlobalWorld() + .getAllFieldEntities() + .stream() + .filter(fieldEntity -> fieldEntity instanceof Robot) + .map(fieldEntity -> (Robot) fieldEntity) + .toList(); + assertEquals(1, robots.size(), context, result -> + "Unexpected number of robots in world"); + + RobotTrace trace = World.getGlobalWorld().getTrace(robots.get(0)); + Robot robot = trace.getTransitions().get(0).robot; + assertEquals(col, robot.getX(), context, result -> + "Robot was initialized with incorrect x coordinate"); + assertEquals(worldHeight - 1, robot.getY(), context, result -> + "Robot was initialized with incorrect y coordinate"); + assertEquals(Direction.DOWN, robot.getDirection(), context, result -> + "Robot was initialized with incorrect direction"); + assertEquals(0, robot.getNumberOfCoins(), context, result -> + "Robot was initialized with incorrect number of coins"); + assertEquals(currentPlayer, robot.getRobotFamily(), context, result -> + "Robot was initialized with incorrect robot family"); + } + } + + @Test + public void testDropStoneCallsGetDestinationRow() { + CtMethod dropStoneCtMethod = getCtMethod(FourWins.class, "dropStone", int.class, RobotFamily[][].class, RobotFamily.class); + CtExecutableReference getDestinationRowCtExecRef = getCtMethod(FourWins.class, "getDestinationRow", int.class, RobotFamily[][].class) + .getReference(); + Iterator iterator = dropStoneCtMethod.descendantIterator(); + + boolean getDestinationRowCalled = false; + while (!getDestinationRowCalled && iterator.hasNext()) { + CtElement ctElement = iterator.next(); + if (ctElement instanceof CtInvocation ctInvocation) { + getDestinationRowCalled = ctInvocation.getExecutable().equals(getDestinationRowCtExecRef); + } + } + assertTrue(getDestinationRowCalled, emptyContext(), result -> + "Method dropStone does not call method getDestinationRow"); + } + + @ParameterizedTest + @JsonParameterSetTest("FourWinsTestGameBoard.generated.json") + public void testDropStoneMovementCorrect(JsonParameterSet params) { + setup(params); + List firstFreeIndex = params.get("firstFreeIndex"); + + for (int col = 0; col < worldWidth; col++) { + if (firstFreeIndex.get(col) >= worldHeight) { + continue; + } + + World.getGlobalWorld().reset(); // clear entities + RobotFamily currentPlayer = RobotFamily.SQUARE_RED; + Context context = baseContextBuilder + .add("column", col) + .add("currentPlayer", currentPlayer) + .build(); + + try { + final int finalCol = col; + call(() -> FourWins.dropStone(finalCol, stones, currentPlayer), context, result -> + "An exception occurred while invoking method dropStone. Result may be salvageable, continuing..."); + } catch (Throwable t) { + t.printStackTrace(System.err); + } + + List robots = World.getGlobalWorld() + .getAllFieldEntities() + .stream() + .filter(Robot.class::isInstance) + .map(Robot.class::cast) + .toList(); + assertEquals(1, robots.size(), context, result -> + "Unexpected number of robots in world"); + + List transitions = World.getGlobalWorld().getTrace(robots.get(0)).getTransitions(); + int expectedTransitionsSize = worldHeight - 1 - firstFreeIndex.get(col) + 3; + for (int i = 0; i < expectedTransitionsSize; i++) { + Transition transition = transitions.get(i); + final int finalI = i; + PreCommentSupplier> preCommentSupplier = result -> + "Robot did not perform the expected action (action number %d)".formatted(finalI); + if (i < expectedTransitionsSize - 3) { // moving + assertEquals(Transition.RobotAction.MOVE, transition.action, context, preCommentSupplier); + } else if (i < expectedTransitionsSize - 1) { // left turns + assertEquals(Transition.RobotAction.TURN_LEFT, transition.action, context, preCommentSupplier); + } else { // last action (none) + assertEquals(Transition.RobotAction.NONE, transition.action, context, preCommentSupplier); + } + } + } + } + + @Test + public void testDropStoneVAnforderung() { + iterateMethodStatements(FourWins.class, + "dropStone", + new Class[] {int.class, RobotFamily[][].class, RobotFamily.class}, + SINGLE_LOOP_VA); + } + + @ParameterizedTest + @JsonParameterSetTest("FourWinsTestGameBoardHorizontalWin.generated.json") + public void testTestWinHorizontal(JsonParameterSet params) { + setup(params); + List> winningRowCoordinates = params.get("winningRowCoordinates"); + Context context = baseContextBuilder.build(); + boolean expected = !winningRowCoordinates.isEmpty(); + boolean actual = callObject(() -> FourWins.testWinHorizontal(stones, currentPlayer), context, result -> + "An exception occurred while invoking method testWinHorizontal"); + assertEquals(expected, actual, context, result -> + "Method testWinHorizontal did not return the correct value"); + } + + @Test + public void testTestWinHorizontalVAnforderung1() { + testWinVAnforderung("testWinHorizontal"); + } + + @Test + public void testTestWinHorizontalVAnforderung2() { + int worldHeight = 5; + int worldWidth = 5; + RobotFamily[][] stones = new RobotFamily[worldHeight][worldWidth]; + for (int row = 0; row < 4; row++) { + stones[row][0] = RobotFamily.SQUARE_RED; + } + RobotFamily currentPlayer = RobotFamily.SQUARE_RED; + Context context = contextBuilder() + .add("world height", worldHeight) + .add("world width", worldWidth) + .add("stones", stones) + .add("currentPlayer", currentPlayer) + .build(); + + World.setSize(worldWidth, worldHeight); + assertCallFalse(() -> FourWins.testWinHorizontal(stones, currentPlayer), context, result -> + "Method testWinHorizontal returned an incorrect value"); + } + + @ParameterizedTest + @JsonParameterSetTest("FourWinsTestGameBoardVerticalWin.generated.json") + public void testTestWinVertical(JsonParameterSet params) { + setup(params); + List> winningColCoordinates = params.get("winningColCoordinates"); + Context context = baseContextBuilder.build(); + + boolean expected = !winningColCoordinates.isEmpty(); + boolean actual = callObject(() -> FourWins.testWinVertical(stones, currentPlayer), context, result -> + "An exception occurred while invoking method testWinVertical"); + assertEquals(expected, actual, context, result -> + "Method testWinVertical did not return the correct value"); + } + + @Test + public void testTestWinVerticalVAnforderung1() { + testWinVAnforderung("testWinVertical"); + } + + @Test + public void testTestWinVerticalVAnforderung2() { + int worldHeight = 5; + int worldWidth = 5; + RobotFamily[][] stones = new RobotFamily[worldHeight][worldWidth]; + for (int col = 0; col < 4; col++) { + stones[0][col] = RobotFamily.SQUARE_RED; + } + RobotFamily currentPlayer = RobotFamily.SQUARE_RED; + Context context = contextBuilder() + .add("world height", worldHeight) + .add("world width", worldWidth) + .add("stones", stones) + .add("currentPlayer", currentPlayer) + .build(); + + World.setSize(worldWidth, worldHeight); + assertCallFalse(() -> FourWins.testWinVertical(stones, currentPlayer), context, result -> + "Method testWinVertical returned an incorrect value"); + } + + /** + * Tests {@link FourWins#testWinConditions(RobotFamily[][], RobotFamily)}. + * The parameter {@code flags} is used to determine the return value of the win condition methods, + * where a set bit is interpreted as {@code true} and an unset bit as {@code false}. + * Only the first three bits are evaluated and correspond to the methods as follows: + *
    + *
  • Bit 0: {@link FourWins#testWinHorizontal(RobotFamily[][], RobotFamily)}
  • + *
  • Bit 1: {@link FourWins#testWinVertical(RobotFamily[][], RobotFamily)}
  • + *
  • Bit 2: {@link FourWins#testWinDiagonal(RobotFamily[][], RobotFamily)}
  • + *
+ * + * @param flags the flags (2:0, 31:3 are unused) + * @throws ReflectiveOperationException if methods testWinHorizontal, testWinVertical or testWinDiagonal are not found + */ + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 3, 4, 5, 6, 7}) + public void testTestWinConditions(int flags) throws ReflectiveOperationException { + Method testWinHorizontalMethod = FourWins.class.getDeclaredMethod("testWinHorizontal", RobotFamily[][].class, RobotFamily.class); + Method testWinVerticalMethod = FourWins.class.getDeclaredMethod("testWinVertical", RobotFamily[][].class, RobotFamily.class); + Method testWinDiagonalMethod = FourWins.class.getDeclaredMethod("testWinDiagonal", RobotFamily[][].class, RobotFamily.class); + Answer answer = invocation -> { + if (invocation.getMethod().equals(testWinHorizontalMethod)) { + return (flags & 1) == 1; + } else if (invocation.getMethod().equals(testWinVerticalMethod)) { + return (flags >> 1 & 1) == 1; + } else if (invocation.getMethod().equals(testWinDiagonalMethod)) { + return (flags >> 2 & 1) == 1; + } else { + return invocation.callRealMethod(); + } + }; + int worldHeight = 5; + int worldWidth = 5; + RobotFamily[][] stones = new RobotFamily[worldHeight][worldWidth]; + RobotFamily currentPlayer = RobotFamily.SQUARE_RED; + Context context = contextBuilder() + .add("world height", worldHeight) + .add("world width", worldWidth) + .add("stones (ignored)", stones) + .add("currentPlayer (ignored)", currentPlayer.getName()) + .add("testWinHorizontal (mocked) return value", (flags & 1) == 1) + .add("testWinVertical (mocked) return value", (flags >> 1 & 1) == 1) + .add("testWinDiagonal (mocked) return value", (flags >> 2 & 1) == 1) + .build(); + + World.setSize(worldWidth, worldHeight); + try (MockedStatic mock = Mockito.mockStatic(FourWins.class, answer)) { + assertCallEquals(flags != 0, () -> FourWins.testWinConditions(stones, currentPlayer), context, result -> + "Method testWinConditions did not return the correct value"); + } + } + + @Test + public void testTestWinConditionsVAnforderung() { + iterateMethodStatements(FourWins.class, "testWinConditions", new Class[] {RobotFamily[][].class, RobotFamily.class}, iterator -> { + boolean callsTestWinHorizontal = false; + boolean callsTestWinVertical = false; + boolean callsTestWinDiagonal = false; + + while (iterator.hasNext()) { + if (iterator.next() instanceof CtInvocation ctInvocation) { + if (ctInvocation.getExecutable().getSimpleName().equals("testWinHorizontal")) { + callsTestWinHorizontal = true; + } else if (ctInvocation.getExecutable().getSimpleName().equals("testWinVertical")) { + callsTestWinVertical = true; + } else if (ctInvocation.getExecutable().getSimpleName().equals("testWinDiagonal")) { + callsTestWinDiagonal = true; + } + } + } + + assertTrue(callsTestWinHorizontal, emptyContext(), result -> "Method testWinConditions did not call testWinHorizontal"); + assertTrue(callsTestWinVertical, emptyContext(), result -> "Method testWinConditions did not call testWinVertical"); + assertTrue(callsTestWinDiagonal, emptyContext(), result -> "Method testWinConditions did not call testWinDiagonal"); + }); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testNextPlayer(boolean isRedPlayer) { + RobotFamily currentPlayer = isRedPlayer ? RobotFamily.SQUARE_RED : RobotFamily.SQUARE_BLUE; + Context context = contextBuilder() + .add("currentPlayer", currentPlayer) + .build(); + + RobotFamily expected = isRedPlayer ? RobotFamily.SQUARE_BLUE : RobotFamily.SQUARE_RED; + assertCallEquals(expected, () -> FourWins.nextPlayer(currentPlayer), context, result -> + "Method nextPlayer did not return the correct value"); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testColorFieldBackground(boolean isRedPlayer) { + int worldHeight = 5; + int worldWidth = 5; + RobotFamily winner = isRedPlayer ? RobotFamily.SQUARE_RED : RobotFamily.SQUARE_BLUE; + Context context = contextBuilder() + .add("world height", worldHeight) + .add("world width", worldWidth) + .add("winner", winner) + .build(); + + World.setSize(worldWidth, worldHeight); + World.setDelay(0); + call(() -> FourWins.colorFieldBackground(winner), context, result -> + "An exception occurred while invoking colorFieldBackground"); + for (int row = 0; row < worldHeight; row++) { + for (int col = 0; col < worldWidth; col++) { + int finalRow = row; + int finalCol = col; + assertEquals(winner.getColor(), World.getGlobalWorld().getFieldColor(col, row), context, result -> + "Color of field at row %d, column %d is incorrect".formatted(finalRow, finalCol)); + } + } + } + + @Test + public void testWriteMessages() { + PrintStream originalOut = System.out; + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + PrintStream printStream = new PrintStream(outputStream, true); + System.setOut(printStream); + + int worldHeight = 5; + int worldWidth = 5; + FourWins fourWinsInstance = new FourWins(worldWidth, worldHeight); + Context.Builder contextBuilder = contextBuilder() + .add("world height", worldHeight) + .add("world width", worldWidth); + + World.setSize(worldWidth, worldHeight); + try { + contextBuilder.add("method", "writeDrawMessage"); + fourWinsInstance.writeDrawMessage(); + assertEquals( + "No valid columns found. Hence, game ends with a draw.", + outputStream.toString().strip(), + contextBuilder.build(), + result -> "Method did not print the correct string" + ); + + contextBuilder.add("method", "writeWinnerMessage"); + for (RobotFamily winner : new RobotFamily[] {RobotFamily.SQUARE_RED, RobotFamily.SQUARE_BLUE}) { + contextBuilder.add("winner", winner); + outputStream.reset(); + fourWinsInstance.writeWinnerMessage(winner); + assertEquals( + "Player %s wins the game!".formatted(winner), + outputStream.toString().strip(), + contextBuilder.build(), + result -> "Method did not print the correct string" + ); + } + } finally { + System.setOut(originalOut); + } + } + + @Test + public void testGameLoopCallsNextPlayer() { + List nextPlayerArgs = new ArrayList<>(); + Answer answer = invocation -> { + Method method = invocation.getMethod(); + if (method.getName().equals("nextPlayer") && Arrays.equals(method.getParameterTypes(), new Class[] {RobotFamily.class})) { + RobotFamily currentPlayer = invocation.getArgument(0); + nextPlayerArgs.add(currentPlayer); + return currentPlayer == RobotFamily.SQUARE_RED ? RobotFamily.SQUARE_BLUE : RobotFamily.SQUARE_RED; + } else if (method.getName().equals("dropStone") && Arrays.equals(method.getParameterTypes(), new Class[] {int.class, RobotFamily[][].class, RobotFamily.class})) { + return null; + } else if (method.getName().equals("testWinConditions") && Arrays.equals(method.getParameterTypes(), new Class[] {RobotFamily[][].class, RobotFamily.class})) { + return nextPlayerArgs.size() >= 5; + } else { + return invocation.callRealMethod(); + } + }; + try (MockedStatic mockedStatic = Mockito.mockStatic(FourWins.class, answer)) { + int worldHeight = 5; + int worldWidth = 5; + FourWins fourWins = new FourWins(worldWidth, worldHeight); + InputHandler inputHandler = fourWins.getInputHandler(); + for (int i = 0; i < 10; i++) { + inputHandler.addInput(0); + } + + fourWins.startGame(); + for (int i = 0; i < nextPlayerArgs.size(); i++) { + Context context = contextBuilder() + .add("world height", worldHeight) + .add("world width", worldWidth) + .add("game loop iteration", i + 1) + .build(); + + if (i % 2 == 0) { + assertEquals(RobotFamily.SQUARE_BLUE, nextPlayerArgs.get(i), context, result -> + "Method nextPlayer was not called with correct parameters"); + } else { + assertEquals(RobotFamily.SQUARE_RED, nextPlayerArgs.get(i), context, result -> + "Method nextPlayer was not called with correct parameters"); + } + } + } + } + + @Test + public void testGameLoopCallsDropStone() { + List columnArgs = new ArrayList<>(); + List stonesArgs = new ArrayList<>(); + List currentPlayerArgs = new ArrayList<>(); + Answer answer = invocation -> { + Method method = invocation.getMethod(); + if (method.getName().equals("nextPlayer") && Arrays.equals(method.getParameterTypes(), new Class[] {RobotFamily.class})) { + RobotFamily currentPlayer = invocation.getArgument(0); + return currentPlayer == RobotFamily.SQUARE_RED ? RobotFamily.SQUARE_BLUE : RobotFamily.SQUARE_RED; + } else if (method.getName().equals("dropStone") && Arrays.equals(method.getParameterTypes(), new Class[] {int.class, RobotFamily[][].class, RobotFamily.class})) { + columnArgs.add(invocation.getArgument(0)); + stonesArgs.add(invocation.getArgument(1)); + currentPlayerArgs.add(invocation.getArgument(2)); + return null; + } else if (method.getName().equals("testWinConditions") && Arrays.equals(method.getParameterTypes(), new Class[] {RobotFamily[][].class, RobotFamily.class})) { + return currentPlayerArgs.size() >= 5; + } else { + return invocation.callRealMethod(); + } + }; + int worldHeight = 5; + int worldWidth = 5; + try (MockedStatic mockedStatic = Mockito.mockStatic(FourWins.class, answer)) { + FourWins fourWins = new FourWins(worldWidth, worldHeight); + InputHandler inputHandler = fourWins.getInputHandler(); + for (int i = 0; i < 10; i++) { + inputHandler.addInput(i % worldWidth); + } + + fourWins.startGame(); + } + + RobotFamily[][] stones = null; + for (int i = 0; i < currentPlayerArgs.size(); i++) { + if (stones == null) { + stones = stonesArgs.get(i); + } + Context context = contextBuilder() + .add("world height", worldHeight) + .add("world width", worldWidth) + .add("game loop iteration", i + 1) + .build(); + + assertEquals(i % worldWidth, columnArgs.get(i), context, result -> + "Method dropStone was not called with correct parameter column"); + assertSame(stones, stonesArgs.get(i), context, result -> + "Method dropStone was not called with correct parameter stones"); + if (i % 2 == 0) { + assertEquals(RobotFamily.SQUARE_RED, currentPlayerArgs.get(i), context, result -> + "Method dropStone was not called with correct parameter currentPlayer"); + } else { + assertEquals(RobotFamily.SQUARE_BLUE, currentPlayerArgs.get(i), context, result -> + "Method dropStone was not called with correct parameter currentPlayer"); + } + } + } + + @Test + public void testGameLoopCallsGetWinConditions() { + List stonesArgs = new ArrayList<>(); + List currentPlayerArgs = new ArrayList<>(); + Answer answer = invocation -> { + Method method = invocation.getMethod(); + if (method.getName().equals("nextPlayer") && Arrays.equals(method.getParameterTypes(), new Class[] {RobotFamily.class})) { + RobotFamily currentPlayer = invocation.getArgument(0); + return currentPlayer == RobotFamily.SQUARE_RED ? RobotFamily.SQUARE_BLUE : RobotFamily.SQUARE_RED; + } else if (method.getName().equals("dropStone") && Arrays.equals(method.getParameterTypes(), new Class[] {int.class, RobotFamily[][].class, RobotFamily.class})) { + return null; + } else if (method.getName().equals("testWinConditions") && Arrays.equals(method.getParameterTypes(), new Class[] {RobotFamily[][].class, RobotFamily.class})) { + stonesArgs.add(invocation.getArgument(0)); + currentPlayerArgs.add(invocation.getArgument(1)); + return currentPlayerArgs.size() >= 5; + } else { + return invocation.callRealMethod(); + } + }; + int worldHeight = 5; + int worldWidth = 5; + try (MockedStatic mockedStatic = Mockito.mockStatic(FourWins.class, answer)) { + FourWins fourWins = new FourWins(worldWidth, worldHeight); + InputHandler inputHandler = fourWins.getInputHandler(); + for (int i = 0; i < 10; i++) { + inputHandler.addInput(i % worldWidth); + } + + fourWins.startGame(); + } + + RobotFamily[][] stones = null; + for (int i = 0; i < currentPlayerArgs.size(); i++) { + if (stones == null) { + stones = stonesArgs.get(i); + } + Context context = contextBuilder() + .add("world height", worldHeight) + .add("world width", worldWidth) + .add("game loop iteration", i + 1) + .build(); + + assertSame(stones, stonesArgs.get(i), context, result -> + "Method getWinConditions was not called with correct parameter stones"); + if (i % 2 == 0) { + assertEquals(RobotFamily.SQUARE_RED, currentPlayerArgs.get(i), context, result -> + "Method getWinConditions was not called with correct parameter currentPlayer"); + } else { + assertEquals(RobotFamily.SQUARE_BLUE, currentPlayerArgs.get(i), context, result -> + "Method getWinConditions was not called with correct parameter currentPlayer"); + } + } + } + + private void testGetDestinationRow(JsonParameterSet params, boolean testFreeSlots) { + setup(params); + List firstFreeIndex = params.get("firstFreeIndex"); + + for (int i = 0; i < firstFreeIndex.size(); i++) { + int index = firstFreeIndex.get(i); + if ((testFreeSlots && index >= worldHeight) || (!testFreeSlots && index < worldHeight)) { + continue; + } + + final int column = i; + int expected = testFreeSlots ? index : -1; + Context context = baseContextBuilder + .add("column", column) + .build(); + int actual = callObject(() -> FourWins.getDestinationRow(column, stones), context, result -> + "An exception occurred while invoking method getDestinationRow"); + assertEquals(expected, actual, context, result -> + "Method getDestinationRow returned an incorrect value"); + } + } + + private void testWinVAnforderung(String methodName) { + List loops = getCtMethod(FourWins.class, methodName, RobotFamily[][].class, RobotFamily.class) + .filterChildren(CtLoop.class::isInstance) + .list(); + + assertEquals(2, loops.size(), emptyContext(), result -> + "Method %s does not use exactly two loops".formatted(methodName)); + assertTrue(loops.get(0).getBody().equals(loops.get(1).getParent()), emptyContext(), result -> + "Method %s does not use exactly two nested loops".formatted(methodName)); + } + + private static RobotFamily robotFamilyLookup(String robotFamilyName) { + if (robotFamilyName == null) { + return null; + } + + return switch (robotFamilyName) { + case "SQUARE_RED" -> RobotFamily.SQUARE_RED; + case "SQUARE_BLUE" -> RobotFamily.SQUARE_BLUE; + default -> null; + }; + } +} diff --git a/solution/H02/src/graderPrivate/java/h02/H02_RubricProvider.java b/solution/H02/src/graderPrivate/java/h02/H02_RubricProvider.java new file mode 100644 index 0000000..075a9de --- /dev/null +++ b/solution/H02/src/graderPrivate/java/h02/H02_RubricProvider.java @@ -0,0 +1,367 @@ +package h02; + +import org.sourcegrade.jagr.api.rubric.Criterion; +import org.sourcegrade.jagr.api.rubric.JUnitTestRef; +import org.sourcegrade.jagr.api.rubric.Rubric; +import org.sourcegrade.jagr.api.rubric.RubricProvider; +import org.sourcegrade.jagr.api.testing.RubricConfiguration; +import org.tudalgo.algoutils.tutor.general.json.JsonParameterSet; + +import static org.tudalgo.algoutils.tutor.general.jagr.RubricUtils.criterion; + +public class H02_RubricProvider implements RubricProvider { + + public static final Rubric RUBRIC = Rubric.builder() + .title("H02 | Vier Gewinnt") + .addChildCriteria( + Criterion.builder() + .shortDescription("H2.1 | Grundlagen-Training") + .minPoints(0) + .addChildCriteria( + Criterion.builder() + .shortDescription("H2.1.1 | Fibonacci mit 1D Array") + .minPoints(0) + .addChildCriteria( + criterion( + "Methode push: Das letzte Element des Ergebnis-Arrays ist das übergebene Element.", + JUnitTestRef.ofMethod( + () -> OneDimensionalArrayStuffTest.class.getDeclaredMethod( + "testPushLastElementCorrect", + JsonParameterSet.class + ) + ) + ), + criterion( + "Methode push: Die Elemente des Ergebnis-Arrays sind korrekt.", + JUnitTestRef.ofMethod( + () -> OneDimensionalArrayStuffTest.class.getDeclaredMethod( + "testPushAllElementsCorrect", + JsonParameterSet.class + ) + ) + ), + criterion( + "Methode push: Das Eingabe-Array wird nicht verändert.", + JUnitTestRef.ofMethod( + () -> OneDimensionalArrayStuffTest.class.getDeclaredMethod( + "testPushOriginalArrayUnchanged", + JsonParameterSet.class + ) + ) + ), + criterion( + "Methode calculateNextFibonacci: Das Ergebnis ist korrekt mit zwei positiven Zahlen.", + JUnitTestRef.ofMethod( + () -> OneDimensionalArrayStuffTest.class.getDeclaredMethod( + "testCalculateNextFibonacciPositiveOnly", JsonParameterSet.class) + ) + ), + criterion( + "Methode calculateNextFibonacci: Das Ergebnis ist korrekt mit beliebigen Eingaben.", + JUnitTestRef.ofMethod( + () -> OneDimensionalArrayStuffTest.class.getDeclaredMethod( + "testCalculateNextFibonacciAllNumbers", JsonParameterSet.class) + ) + ), + criterion( + "Methode calculateNextFibonacci: Eine verbindliche Anforderung wurde verletzt.", + JUnitTestRef.ofMethod( + () -> OneDimensionalArrayStuffTest.class.getDeclaredMethod( + "testCalculateNextFibonacciVanforderungen", JsonParameterSet.class) + ), + -1 + ), + criterion( + "Methode fibonacci: Das Ergebnis ist korrekt für n < 2.", + JUnitTestRef.ofMethod( + () -> OneDimensionalArrayStuffTest.class.getDeclaredMethod( + "testFibonacciSmallerThanTwo", JsonParameterSet.class) + ) + ), + criterion( + "Methode fibonacci: Das Ergebnis ist korrekt für n >= 2.", + JUnitTestRef.ofMethod( + () -> OneDimensionalArrayStuffTest.class.getDeclaredMethod( + "testFibonacciBigNumbers", JsonParameterSet.class) + ) + ), + criterion( + "Methode fibonacci: Eine Verbindliche Anforderung wurde verletzt.", + JUnitTestRef.and( + JUnitTestRef.ofMethod( + () -> OneDimensionalArrayStuffTest.class.getDeclaredMethod( + "testFibonacciVanforderungen", JsonParameterSet.class) + ), + JUnitTestRef.ofMethod( + () -> OneDimensionalArrayStuffTest.class.getDeclaredMethod( + "testFibonacciNonIterativeVanforderungen") + ) + ), + -1 + ) + ) + .build(), + Criterion.builder() + .shortDescription("H2.1.2 | Textsuche mit 2D Arrays") + .minPoints(0) + .addChildCriteria( + criterion( + "Methode occurrences: Die Methode funktioniert korrekt mit einem leeren Array.", + JUnitTestRef.ofMethod( + () -> TwoDimensionalArrayStuffTest.class.getDeclaredMethod( + "testOccurrencesEmptyArray", JsonParameterSet.class) + ) + ), + criterion( + "Methode occurrences: Die Methode funktioniert mit einem Satz.", + JUnitTestRef.ofMethod( + () -> TwoDimensionalArrayStuffTest.class.getDeclaredMethod( + "testOccurrencesSingleSentence", JsonParameterSet.class) + ) + ), + criterion( + "Methode occurrences: Die Methode funktioniert mit mehreren Sätzen.", + JUnitTestRef.ofMethod( + () -> TwoDimensionalArrayStuffTest.class.getDeclaredMethod( + "testOccurrencesMultipleSentences", JsonParameterSet.class) + ) + ), + criterion( + "Methode mean: Methode funktioniert mit ganzzahligen Rechenwerten.", + JUnitTestRef.ofMethod( + () -> TwoDimensionalArrayStuffTest.class.getDeclaredMethod( + "testMeanInteger", JsonParameterSet.class) + ) + ), + criterion( + "Methode mean: Die Methode funktioniert auch dann korrekt, wenn das Ergebnis eine fließkommazahl ist.", + JUnitTestRef.ofMethod( + () -> TwoDimensionalArrayStuffTest.class.getDeclaredMethod( + "testMeanFloat", JsonParameterSet.class) + ) + ) + ) + .build() + ) + .build() + ) + .addChildCriteria( + Criterion.builder() + .shortDescription("H2.2 | Vier Gewinnt") + .minPoints(0) + .addChildCriteria( + Criterion.builder() + .shortDescription("H2.2.1 | Slot Prüfen") + .addChildCriteria( + criterion( + "Methode validateInput: Methode ist vollständig korrekt implementiert.", + JUnitTestRef.and( + JUnitTestRef.ofMethod( + () -> FourWinsTest.class.getDeclaredMethod( + "testValidateInputEdgeCases", JsonParameterSet.class) + ), + JUnitTestRef.ofMethod( + () -> FourWinsTest.class.getDeclaredMethod( + "testValidateInputRandomCases", JsonParameterSet.class) + ) + ) + ) + ) + .build(), + Criterion.builder() + .shortDescription("H2.2.2 | Münzen fallen lassen") + .minPoints(0) + .addChildCriteria( + criterion( + "Methode getDestinationRow: Die Rückgabe ist korrekt, wenn ein freier Slot existiert.", + JUnitTestRef.ofMethod( + () -> FourWinsTest.class.getDeclaredMethod( + "testGetDestinationRowFreeSlot", JsonParameterSet.class) + ) + ), + criterion( + "Methode getDestinationRow: Die Rückgabe ist korrekt, wenn KEIN freier Slot existiert.", + JUnitTestRef.ofMethod( + () -> FourWinsTest.class.getDeclaredMethod( + "testGetDestinationRowBlockedSlot", JsonParameterSet.class) + ) + ), + criterion( + "Methode getDestinationRow: Verbindliche Anforderung 'genau eine Schleife' wurde verletzt.", + JUnitTestRef.ofMethod( + () -> FourWinsTest.class.getDeclaredMethod( + "testGetDestinationRowVAnforderung") + ), + -1 + ), + criterion( + "Methode dropStone: Robot wird mit korrekten Parametern erstellt.", + JUnitTestRef.ofMethod( + () -> FourWinsTest.class.getDeclaredMethod( + "testDropStoneRobotCorrect", JsonParameterSet.class) + ) + ), + criterion( + "Methode dropStone: getDestinationRow wird korrekt aufgerufen.", + JUnitTestRef.ofMethod( + () -> FourWinsTest.class.getDeclaredMethod( + "testDropStoneCallsGetDestinationRow") + ) + ), + criterion( + "Methode dropStone: Robot führt die korrekte Bewegung aus.", + JUnitTestRef.ofMethod( + () -> FourWinsTest.class.getDeclaredMethod( + "testDropStoneMovementCorrect", JsonParameterSet.class) + ) + ), + criterion( + "Methode dropStone: Verbindliche Anforderung 'genau eine Schleife' wurde verletzt.", + JUnitTestRef.ofMethod( + () -> FourWinsTest.class.getDeclaredMethod("testDropStoneVAnforderung") + ), + -1 + ) + ) + .build(), + Criterion.builder() + .shortDescription("H2.2.3 | Gewinnbedingung prüfen") + .addChildCriteria( + Criterion.builder() + .shortDescription("Methode testWinHorizontal: ") + .addChildCriteria( + criterion( + "Methode erkennt richtige horizontale Steinfolgen.", + JUnitTestRef.ofMethod( + () -> FourWinsTest.class.getDeclaredMethod( + "testTestWinHorizontal", JsonParameterSet.class) + ), + 2 + ), + criterion( + "Methode nutzt genau zwei verschachtelte Schleifen.", + JUnitTestRef.ofMethod( + () -> FourWinsTest.class.getDeclaredMethod( + "testTestWinHorizontalVAnforderung1") + ), + 1 + ), + criterion( + "Methode erkennt keine falschen Steinfolgen.", + JUnitTestRef.ofMethod( + () -> FourWinsTest.class.getDeclaredMethod("testTestWinHorizontalVAnforderung2") + ), + -2 + ) + ).minPoints(0).build(), + Criterion.builder() + .shortDescription("Methode testWinVertical: ") + .addChildCriteria( + criterion( + "Methode erkennt richtige vertikale Steinfolgen.", + JUnitTestRef.ofMethod( + () -> FourWinsTest.class.getDeclaredMethod( + "testTestWinVertical", JsonParameterSet.class) + ), + 2 + ), + criterion( + "Methode nutzt genau zwei verschachtelte Schleifen.", + JUnitTestRef.ofMethod( + () -> FourWinsTest.class.getDeclaredMethod( + "testTestWinVerticalVAnforderung1") + ), + 1 + ), + criterion( + "Methode erkennt keine falschen Steinfolgen.", + JUnitTestRef.ofMethod( + () -> FourWinsTest.class.getDeclaredMethod( + "testTestWinVerticalVAnforderung2") + ), + -2 + ) + ).minPoints(0).build(), + Criterion.builder() + .shortDescription("Methode testWinConditions: ") + .addChildCriteria( + criterion( + "Die Rückgabe ist in allen Fällen korrekt.", + JUnitTestRef.ofMethod( + () -> FourWinsTest.class.getDeclaredMethod( + "testTestWinConditions", int.class) + ) + ), + criterion( + "testWinHorizontal, testWinVertical und testWinDiagonal werden korrekt aufgerufen.", + JUnitTestRef.ofMethod( + () -> FourWinsTest.class.getDeclaredMethod( + "testTestWinConditionsVAnforderung") + ) + ) + ).build() + ) + .build(), + Criterion.builder() + .shortDescription("H2.2.4 | Game Loop") + .minPoints(0) + .addChildCriteria( + criterion( + "Methode nextPlayer: Die Rückgabe für beide RobotFamily.SQUARE_BLUE und SQUARE_RED korrekt.", + JUnitTestRef.ofMethod( + () -> FourWinsTest.class.getDeclaredMethod( + "testNextPlayer", boolean.class) + ) + ), + criterion( + "Methode colorFieldBackground: Das Spielfeld wird korrekt eingefärbt.", + JUnitTestRef.ofMethod( + () -> FourWinsTest.class.getDeclaredMethod( + "testColorFieldBackground", boolean.class) + ) + ), + criterion( + "Methoden writeDrawMessage, writeWinnerMessage: Die Ausgabe in die Konsole ist korrekt.", + JUnitTestRef.ofMethod( + () -> FourWinsTest.class.getDeclaredMethod( + "testWriteMessages") + ) + ), + criterion( + "Methode gameLoop: nextPlayer wird mit korrektem Parameter aufgerufen.", + JUnitTestRef.ofMethod( + () -> FourWinsTest.class.getDeclaredMethod( + "testGameLoopCallsNextPlayer") + ) + ), + criterion( + "Methode gameLoop: dropStone wird mit korrekten Parametern aufgerufen.", + JUnitTestRef.ofMethod( + () -> FourWinsTest.class.getDeclaredMethod( + "testGameLoopCallsDropStone") + ) + ), + criterion( + "Methode gameLoop: testWinConditions wird mit korrekten Parametern aufgerufen.", + JUnitTestRef.ofMethod( + () -> FourWinsTest.class.getDeclaredMethod( + "testGameLoopCallsGetWinConditions") + ) + ) + ) + .build() + ) + .build() + ) + .build(); + + @Override + public Rubric getRubric() { + return RUBRIC; + } + + @Override + public void configure(RubricConfiguration configuration) { + configuration.addTransformer(new SubmissionTransformer()); + } +} diff --git a/solution/H02/src/graderPrivate/java/h02/OneDimensionalArrayStuffTest.java b/solution/H02/src/graderPrivate/java/h02/OneDimensionalArrayStuffTest.java new file mode 100644 index 0000000..a4417bf --- /dev/null +++ b/solution/H02/src/graderPrivate/java/h02/OneDimensionalArrayStuffTest.java @@ -0,0 +1,327 @@ +package h02; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.mockito.Answers; +import org.mockito.Mockito; +import org.mockito.exceptions.base.MockitoException; +import org.sourcegrade.jagr.api.rubric.TestForSubmission; +import org.tudalgo.algoutils.tutor.general.annotation.SkipAfterFirstFailedTest; +import org.tudalgo.algoutils.tutor.general.assertions.Assertions2; +import org.tudalgo.algoutils.tutor.general.assertions.Assertions4; +import org.tudalgo.algoutils.tutor.general.json.JsonParameterSet; +import org.tudalgo.algoutils.tutor.general.json.JsonParameterSetTest; +import org.tudalgo.algoutils.tutor.general.reflections.BasicMethodLink; +import spoon.reflect.code.CtInvocation; +import spoon.reflect.code.CtLocalVariable; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.contextBuilder; + +@TestForSubmission +@Timeout( + value = TestConstants.TEST_TIMEOUT_IN_SECONDS, + unit = TimeUnit.SECONDS, + threadMode = Timeout.ThreadMode.SEPARATE_THREAD +) +@SkipAfterFirstFailedTest(TestConstants.SKIP_AFTER_FIRST_FAILED_TEST) +public class OneDimensionalArrayStuffTest { + /** + * Tests the {@link OneDimensionalArrayStuff#push(int[], int)} method. + * + * @param params The {@link JsonParameterSet} to use for the test. + * @param lastOnly Whether to only test the last element. + * @param unchangedOnly Whether to only test that the input array is unchanged. Will not test the result. + */ + private static void testPush(final JsonParameterSet params, final boolean lastOnly, final boolean unchangedOnly) { + final List input = params.get("array"); + final int value = params.getInt("value"); + final List expectedArray = params.get("expected result"); + final var ParamsContext = params.toContext(); + final var cb = contextBuilder() + .add(ParamsContext) + .add("Method", "push"); + final int[] inputArray = input.stream().mapToInt(i -> i).toArray(); + final int[] result = Assertions2.callObject( + () -> OneDimensionalArrayStuff.push( + inputArray, + value + ), + cb.build(), + r -> "An error occurred during execution." + ); + final var actualArray = Arrays.stream(result).boxed().toList(); + cb.add("actual result", actualArray); + if (unchangedOnly) { + Assertions2.assertIterableEquals( + input, + Arrays.stream(inputArray).boxed().toList(), + cb.build(), + r -> "The input array was changed." + ); + return; + } + if (lastOnly) { + Assertions2.assertEquals( + expectedArray.get(expectedArray.size() - 1), + actualArray.get(actualArray.size() - 1), + cb.build(), + r -> "Invalid result." + ); + } else { + Assertions2.assertIterableEquals( + expectedArray, + actualArray, + cb.build(), + r -> "Invalid result." + ); + } + } + + private static void testCalculateNextFibonacci(final JsonParameterSet params, final boolean vanforderung) { + try (final var odasMock = Mockito.mockStatic(OneDimensionalArrayStuff.class, Answers.CALLS_REAL_METHODS)) { + final List input = params.get("array"); + final List expectedArray = params.get("expected result"); + final var ParamsContext = params.toContext(); + final var cb = contextBuilder() + .add(ParamsContext) + .add("Method", "calculateNextFibonacci"); + + odasMock.when(() -> OneDimensionalArrayStuff.push(Mockito.any(), Mockito.anyInt())).thenAnswer(invocation -> { + final int[] array = invocation.getArgument(0); + final int value = invocation.getArgument(1); + final int[] newArray = Arrays.copyOf(array, array.length + 1); + newArray[array.length] = value; + return newArray; + }); + + final int[] inputArray = input.stream().mapToInt(i -> i).toArray(); + final int[] result = Assertions2.callObject( + () -> OneDimensionalArrayStuff.calculateNextFibonacci( + inputArray + ), + cb.build(), + r -> "An error occurred during execution." + ); + final var actualArray = Arrays.stream(result).boxed().toList(); + cb.add("actual result", actualArray); + if (vanforderung) { + Assertions2.assertIterableEquals( + input, + Arrays.stream(inputArray).boxed().toList(), + cb.build(), + r -> "The input array was changed." + ); + odasMock.verify( + () -> OneDimensionalArrayStuff.push( + inputArray, + expectedArray.get(expectedArray.size() - 1) + ), + Mockito.atLeastOnce() + ); + return; + } + Assertions2.assertIterableEquals( + expectedArray, + actualArray, + cb.build(), + r -> "Invalid result." + ); + + } + } + + private static void testFibonacci(final JsonParameterSet params, final boolean vanforderung) { + try (final var odasMock = Mockito.mockStatic(OneDimensionalArrayStuff.class, Answers.CALLS_REAL_METHODS)) { + final Integer input = params.get("n"); + final int expectedresult = params.get("expected result"); + final List referenceArray = params.get("reference array"); + final var ParamsContext = params.toContext("reference array"); + final var cb = contextBuilder() + .add(ParamsContext) + .add("Method", "fibonacci"); + + odasMock.when(() -> OneDimensionalArrayStuff.push(Mockito.any(), Mockito.anyInt())).thenAnswer(invocation -> { + final int[] array = invocation.getArgument(0); + final int value = invocation.getArgument(1); + final int[] newArray = Arrays.copyOf(array, array.length + 1); + newArray[array.length] = value; + return newArray; + }); + + odasMock.when(() -> OneDimensionalArrayStuff.calculateNextFibonacci(Mockito.any())).thenAnswer(invocation -> { + final int[] array = invocation.getArgument(0); + return OneDimensionalArrayStuff.push(array, array[array.length - 1] + array[array.length - 2]); + }); + + final int result = Assertions2.callObject( + () -> OneDimensionalArrayStuff.fibonacci( + input + ), + cb.build(), + r -> "An error occurred during execution." + ); + + cb.add("actual result", result); + if (vanforderung) { + // test calculateFib was used + for (int i = 2; i < referenceArray.size(); i++) { + final int finalI = i; + try { + // test for entire array + odasMock.verify( + () -> OneDimensionalArrayStuff.calculateNextFibonacci( + referenceArray.subList(0, finalI).stream().mapToInt(j -> j).toArray() + ), + Mockito.atLeastOnce() + ); + } catch (final MockitoException e) { + // test for last two elements + odasMock.verify( + () -> OneDimensionalArrayStuff.calculateNextFibonacci( + referenceArray.subList(finalI - 2, finalI).stream().mapToInt(j -> j).toArray() + ), + Mockito.atLeastOnce() + ); + } + } + return; + } + Assertions2.assertEquals( + expectedresult, + result, + cb.build(), + r -> "Invalid result." + ); + + } + } + + @ParameterizedTest + @JsonParameterSetTest(value = "OneDimensionalArrayStuffTestPushRandomNumbers.generated.json") + public void testPushLastElementCorrect(final JsonParameterSet params) { + testPush(params, true, false); + } + + @ParameterizedTest + @JsonParameterSetTest(value = "OneDimensionalArrayStuffTestPushRandomNumbers.generated.json") + public void testPushAllElementsCorrect(final JsonParameterSet params) { + testPush(params, false, false); + } + + @ParameterizedTest + @JsonParameterSetTest(value = "OneDimensionalArrayStuffTestPushRandomNumbers.generated.json") + public void testPushOriginalArrayUnchanged(final JsonParameterSet params) { + testPush(params, false, true); + } + + @ParameterizedTest + @JsonParameterSetTest(value = "OneDimensionalArrayStuffTestCalculateNextFibonacciRandomNumbersTwoPositiveNumbersOnly.generated.json") + public void testCalculateNextFibonacciPositiveOnly(final JsonParameterSet params) { + testCalculateNextFibonacci(params, false); + } + + @ParameterizedTest + @JsonParameterSetTest(value = "OneDimensionalArrayStuffTestCalculateNextFibonacciRandomNumbers.generated.json") + public void testCalculateNextFibonacciAllNumbers(final JsonParameterSet params) { + testCalculateNextFibonacci(params, false); + } + + @ParameterizedTest + @JsonParameterSetTest(value = "OneDimensionalArrayStuffTestCalculateNextFibonacciRandomNumbers.generated.json") + public void testCalculateNextFibonacciVanforderungen(final JsonParameterSet params) { + testCalculateNextFibonacci(params, true); + } + + @ParameterizedTest + @JsonParameterSetTest(value = "OneDimensionalArrayStuffTestFibonacciRandomNumbers.generated.json") + public void testFibonacciVanforderungen(final JsonParameterSet params) { + testFibonacci(params, true); + } + + @Test + public void testFibonacciNonIterativeVanforderungen() throws NoSuchMethodException { + // test exactly one for loop + final var ml = BasicMethodLink.of(OneDimensionalArrayStuff.class.getDeclaredMethod("fibonacci", int.class)); + final var ctElement = ml.getCtElement(); + final var cb = contextBuilder() + .add("Method", "fibonacci"); + Assertions4.assertIsNotRecursively( + ctElement, + cb.build(), + r -> "The method should not have any recursive calls." + ); + final var forLoops = ctElement.filterChildren( + c -> c instanceof spoon.reflect.code.CtFor + || c instanceof spoon.reflect.code.CtForEach + || c instanceof spoon.reflect.code.CtWhile + || c instanceof spoon.reflect.code.CtDo + ).list(); + Assertions2.assertEquals( + 1, + forLoops.size(), + cb.build(), + r -> "The method should contain exactly one for loop." + ); + // test exactly one variable declaration + final var variableDeclarations = ctElement.filterChildren( + c -> c instanceof CtLocalVariable + ).list(CtLocalVariable.class); + cb.add("variable declarations", variableDeclarations); + Assertions2.assertEquals( + 2, + variableDeclarations.size(), + cb.build(), + r -> "The method should contain exactly two variable declarations." + ); + // one of type int[] and one of type int + Assertions2.assertTrue( + variableDeclarations.stream().anyMatch( + c -> c.getType().toString().equals("int[]") + ), + cb.build(), + r -> "The method should declare exactly one variable of type int[]." + ); + Assertions2.assertTrue( + variableDeclarations.stream().anyMatch( + c -> c.getType().toString().equals("int") + ), + cb.build(), + r -> "The method should contain exactly one variable of type int." + ); + // test no Method calls except for calculateNextFibonacci + final var methodCalls = ctElement.filterChildren( + c -> c instanceof spoon.reflect.code.CtInvocation + ).list(CtInvocation.class); + cb.add("method calls", methodCalls); + Assertions2.assertEquals( + 1, + methodCalls.size(), + cb.build(), + r -> "The method should contain exactly one method call." + ); + final var methodCall = methodCalls.get(0); + Assertions2.assertEquals( + "calculateNextFibonacci", + methodCall.getExecutable().getSimpleName(), + cb.build(), + r -> "The method should only call calculateNextFibonacci." + ); + } + + @ParameterizedTest + @JsonParameterSetTest(value = "OneDimensionalArrayStuffTestFibonacciRandomNumbersSmallerThanTwo.generated.json") + public void testFibonacciSmallerThanTwo(final JsonParameterSet params) { + testFibonacci(params, false); + } + + @ParameterizedTest + @JsonParameterSetTest(value = "OneDimensionalArrayStuffTestFibonacciRandomNumbers.generated.json") + public void testFibonacciBigNumbers(final JsonParameterSet params) { + testFibonacci(params, false); + } +} diff --git a/solution/H02/src/graderPrivate/java/h02/SubmissionTransformer.java b/solution/H02/src/graderPrivate/java/h02/SubmissionTransformer.java new file mode 100644 index 0000000..804fccd --- /dev/null +++ b/solution/H02/src/graderPrivate/java/h02/SubmissionTransformer.java @@ -0,0 +1,64 @@ +package h02; + +import org.objectweb.asm.*; +import org.sourcegrade.jagr.api.testing.ClassTransformer; + +import static org.objectweb.asm.Opcodes.*; + +public class SubmissionTransformer implements ClassTransformer { + + @Override + public String getName() { + return "MethodInjectingTransformer"; + } + + @Override + public void transform(ClassReader reader, ClassWriter writer) { + if (reader.getClassName().equals("h02/FourWins")) { + reader.accept(new FourWinsClassVisitor(writer), 0); + } else { + reader.accept(writer, 0); + } + } + + private static class FourWinsClassVisitor extends ClassVisitor { + + private FourWinsClassVisitor(ClassWriter classWriter) { + super(Opcodes.ASM9, classWriter); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + return new MethodVisitor(api, super.visitMethod(access, name, descriptor, signature, exceptions)) { + @Override + public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { + if (owner.equals("h02/template/InputHandler") && name.equals("install") && descriptor.equals("()V")) { + super.visitInsn(POP); + } else { + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + } + } + }; + } + + @Override + public void visitEnd() { + Label startLabel = new Label(); + Label endLabel = new Label(); + MethodVisitor mv = super.visitMethod(ACC_PUBLIC, + "getInputHandler", + "()Lh02/template/InputHandler;", + null, + null); + mv.visitLabel(startLabel); + mv.visitVarInsn(ALOAD, 0); + mv.visitFieldInsn(GETFIELD, "h02/FourWins", "inputHandler", "Lh02/template/InputHandler;"); + mv.visitInsn(ARETURN); + mv.visitLabel(endLabel); + mv.visitLocalVariable("this", "Lh02/FourWins;", null, startLabel, endLabel, 0); + mv.visitMaxs(1, 1); + + super.visitEnd(); + } + } +} diff --git a/solution/H02/src/graderPrivate/java/h02/TemplateAnnotationProcessor.java b/solution/H02/src/graderPrivate/java/h02/TemplateAnnotationProcessor.java new file mode 100644 index 0000000..31ba2ab --- /dev/null +++ b/solution/H02/src/graderPrivate/java/h02/TemplateAnnotationProcessor.java @@ -0,0 +1,154 @@ +package h02; + +import org.junit.jupiter.api.Test; +import org.tudalgo.algoutils.student.annotation.SolutionOnly; +import org.tudalgo.algoutils.student.annotation.StudentCreationRequired; +import org.tudalgo.algoutils.student.annotation.StudentImplementationRequired; +import org.tudalgo.algoutils.tutor.general.reflections.BasicPackageLink; +import org.tudalgo.algoutils.tutor.general.reflections.BasicTypeLink; +import org.tudalgo.algoutils.tutor.general.reflections.TypeLink; +import spoon.Launcher; +import spoon.reflect.CtModel; +import spoon.reflect.code.CtCodeSnippetStatement; +import spoon.reflect.declaration.CtAnnotation; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.declaration.CtMethod; +import spoon.reflect.declaration.CtRecord; +import spoon.reflect.factory.Factory; +import spoon.support.sniper.SniperJavaPrettyPrinter; + +import java.io.File; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +import static org.tudalgo.algoutils.tutor.general.ResourceUtils.toPathString; + +public class TemplateAnnotationProcessor { + private static Launcher createSpoonLauncher(final boolean sniper) { + final Launcher spoon = new Launcher(); + spoon.getEnvironment().setAutoImports(true); + spoon.getEnvironment().setComplianceLevel(17); + spoon.getEnvironment().setNoClasspath(true); + spoon.getEnvironment().setCommentEnabled(true); + spoon.getEnvironment().setIgnoreSyntaxErrors(true); + if (sniper) { + spoon.getEnvironment().setPrettyPrinterCreator( + () -> new SniperJavaPrettyPrinter(spoon.getEnvironment()) + ); + } + return spoon; + } + + @Test + public void execute() { + // final Collection types = List.of(BasicTypeLink.of(TestClass.class)); + final Collection types = BasicPackageLink.of("h02", true).getTypes(); + Path tmpDir = null; + try { + tmpDir = Files.createTempDirectory("spoon_output"); + System.out.println("tmpdir: " + tmpDir.toString()); + for (final BasicTypeLink type : types) { + if (type.reflection().getName().contains("TemplateAnnotationProcessor") || type.reflection().getName().contains("TestClass")) { + continue; + } + System.out.println("processing " + type.name()); + final AtomicBoolean modified = new AtomicBoolean(false); + Launcher spoon = createSpoonLauncher(true); + spoon.addInputResource(getFilePath(type).toString()); + CtModel model = spoon.buildModel(); + var spoonClass = model.getAllTypes().stream().filter(it -> it.getQualifiedName().equals(type.reflection().getName())).findFirst().orElse(null); + if (spoonClass == null || spoonClass instanceof CtRecord) { + // workaround for records because spoon hates them + spoon = createSpoonLauncher(false); + spoon.addInputResource(getFilePath(type).toString()); + model = spoon.buildModel(); + spoonClass = model.getAllTypes().stream().filter(it -> it.getQualifiedName().equals(type.reflection().getName())).findFirst().orElseThrow(); + } + final Factory factory = spoon.getFactory(); + processAnnotation(spoonClass, StudentImplementationRequired.class, element -> { + // get annotation text + final var exercise = element.getAnnotation(StudentImplementationRequired.class).value(); + if (element instanceof final CtMethod method) { + modified.set(true); + // Create a new code snippet statement with the desired code + String optionalReturn = ""; + if (!method.getType().getSimpleName().equals("void")) { + optionalReturn = "return "; + } + final CtCodeSnippetStatement snippet = method.getFactory().Code().createCodeSnippetStatement("// TODO: " + exercise + "\n" + optionalReturn + "org.tudalgo.algoutils.student.Student.crash(\"" + exercise + " - Remove if implemented\")"); + // Set the code snippet as the body of the method + method.setBody(snippet); + } + }); + processAnnotation(spoonClass, Set.of(SolutionOnly.class, StudentCreationRequired.class), element -> { + if (element instanceof final CtMethod method) { + modified.set(true); + // remove annotations + method.getAnnotations().forEach(CtAnnotation::delete); + // remove method + method.delete(); + } + }); + if (!modified.get()) { + continue; + } + // Save the modified class + spoon.getEnvironment().setSourceOutputDirectory(new File(tmpDir.toString())); + // overwrite the file if it already exists +// spoon.getEnvironment().setAutoImports(true); + + spoon.prettyprint(); + final var path = getFilePath(type); + final var outputPath = Path.of(tmpDir.toString(), toPathString(type.reflection().getName())); + Files.deleteIfExists(path); + Files.move(outputPath, path); + } + } catch (final IOException e) { + throw new RuntimeException(e); + } finally { + try (final var walk = Files.walk(tmpDir)) { + walk.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); + } catch (final IOException e) { + throw new RuntimeException("Could not delete tmp dir", e); + } + } + } + + public static Path getFilePath(final TypeLink type) { + final String pathString = toPathString(type.reflection().getName()); + // find the file + try (final var walk = Files.walk(Path.of("."))) { + return walk.filter(p -> p.endsWith(pathString)).findFirst().orElseThrow(); + } catch (final IOException e) { + throw new RuntimeException("an error occurred while reading a source files ", e); + } + } + + public static void processAnnotation( + final CtElement root, final Set> annotations, + final Consumer consumer + ) { + CtElement element = root.filterChildren(c -> annotations.stream().anyMatch(c::hasAnnotation)).first(); + final Set processed = new HashSet<>(); + while (element != null && !processed.contains(element)) { + consumer.accept(element); + processed.add(element); + element = root.filterChildren(c -> !processed.contains(c) && annotations.stream().anyMatch(c::hasAnnotation)).first(); + } + } + + public static void processAnnotation( + final CtElement root, final Class annotation, + final Consumer consumer + ) { + processAnnotation(root, Set.of(annotation), consumer); + } +} diff --git a/solution/H02/src/graderPrivate/java/h02/TestConstants.java b/solution/H02/src/graderPrivate/java/h02/TestConstants.java new file mode 100644 index 0000000..ad78769 --- /dev/null +++ b/solution/H02/src/graderPrivate/java/h02/TestConstants.java @@ -0,0 +1,19 @@ +package h02; + +import java.util.concurrent.ThreadLocalRandom; + +public class TestConstants { + public static long RANDOM_SEED = ThreadLocalRandom.current().nextLong(); + public static final boolean SHOW_WORLD = java.lang.management.ManagementFactory + .getRuntimeMXBean() + .getInputArguments() + .toString() + .contains("-agentlib:jdwp"); // true if debugger is attached + public static final int WORLD_DELAY = 500; + + public static final int TEST_TIMEOUT_IN_SECONDS = 10; + + public static final int TEST_ITERATIONS = 30; + + public static final boolean SKIP_AFTER_FIRST_FAILED_TEST = true; +} diff --git a/solution/H02/src/graderPrivate/java/h02/TestJsonGenerators.java b/solution/H02/src/graderPrivate/java/h02/TestJsonGenerators.java new file mode 100644 index 0000000..f15bf6e --- /dev/null +++ b/solution/H02/src/graderPrivate/java/h02/TestJsonGenerators.java @@ -0,0 +1,267 @@ +package h02; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import fopbot.RobotFamily; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIf; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static h02.TestConstants.TEST_ITERATIONS; + +@DisabledIf("org.tudalgo.algoutils.tutor.general.Utils#isJagrRun()") +public class TestJsonGenerators { + @Test + public void generateOneDimensionalArrayStuffTestPushRandomNumbers() throws IOException { + TestUtils.generateJsonTestData( + (mapper, index, rnd) -> { + final ObjectNode objectNode = mapper.createObjectNode(); + objectNode.put("value", rnd.nextInt((int) -2e5, (int) 2e5)); + final List input = new ArrayList<>(); + if (index < 99) { + for (int i = 0; i < rnd.nextInt(5, 10); i++) { + input.add(rnd.nextInt((int) -2e5, (int) 2e5)); + } + } + final ArrayNode inputArrayNode = mapper.createArrayNode(); + input.forEach(inputArrayNode::add); + objectNode.set("array", inputArrayNode); + input.add(objectNode.get("value").asInt()); + final ArrayNode expectedArrayNode = mapper.createArrayNode(); + input.forEach(expectedArrayNode::add); + objectNode.set("expected result", expectedArrayNode); + return objectNode; + }, + TEST_ITERATIONS, + "OneDimensionalArrayStuffTestPushRandomNumbers.generated.json" + ); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void generateOneDimensionalArrayStuffTestCalculateNextFibonacciRandomNumbers( + final boolean twoPositivesOnly + ) throws IOException { + TestUtils.generateJsonTestData( + (mapper, index, rnd) -> { + final ObjectNode objectNode = mapper.createObjectNode(); + final List input = new ArrayList<>(); + for (int i = 0; i < (twoPositivesOnly ? 2 : rnd.nextInt(5, 10)); i++) { + input.add(rnd.nextInt( + twoPositivesOnly ? 0 : (int) -2e5, + (int) 2e5 + )); + } + final ArrayNode inputArrayNode = mapper.createArrayNode(); + input.forEach(inputArrayNode::add); + objectNode.set("array", inputArrayNode); + System.out.println(input.size()); + final int nextFibonacci = input.get(input.size() - 1) + input.get(input.size() - 2); + input.add(nextFibonacci); + final ArrayNode expectedArrayNode = mapper.createArrayNode(); + input.forEach(expectedArrayNode::add); + objectNode.set("expected result", expectedArrayNode); + return objectNode; + }, + TEST_ITERATIONS, + "OneDimensionalArrayStuffTestCalculateNextFibonacciRandomNumbers" + (twoPositivesOnly ? "TwoPositiveNumbersOnly" : "") + ".generated.json" + ); + } + + /** + * Reference Fibonacci implementation using the closed-form formula. + * + * @param n The number to calculate the Fibonacci number for. + * @return The Fibonacci number. + * @see Fibonacci Closed-form expression on Wikipedia + */ + public static long fib(final int n) { + final double sqrt5 = Math.sqrt(5); + final double phi = (1 + sqrt5) / 2; + final double psi = (1 - sqrt5) / 2; + + return Math.round((Math.pow(phi, n) - Math.pow(psi, n)) / sqrt5); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void generateOneDimensionalArrayStuffTestFibonacciRandomNumbers( + final boolean smallerThanTwo + ) throws IOException { + TestUtils.generateJsonTestData( + (mapper, index, rnd) -> { + final int startIdx = smallerThanTwo ? index : index + 2; + final ObjectNode objectNode = mapper.createObjectNode(); + objectNode.put("n", startIdx); + objectNode.put("expected result", fib(startIdx)); + final ArrayNode refArrayNode = mapper.createArrayNode(); + for (int i = 0; i <= startIdx; i++) { + refArrayNode.add(fib(i)); + } + objectNode.set("reference array", refArrayNode); + return objectNode; + }, + smallerThanTwo ? 2 : TEST_ITERATIONS, + "OneDimensionalArrayStuffTestFibonacciRandomNumbers" + (smallerThanTwo ? "SmallerThanTwo" : "") + ".generated.json" + ); + } + + + @Test + public void generateValidateInputRandomCases() throws IOException { + TestUtils.generateJsonTestData( + (mapper, index, rnd) -> { + final ObjectNode objectNode = mapper.createObjectNode(); + + // Spielfeldgröße zufällig wählen + final int width = rnd.nextInt(3, 10); // Breite zwischen 3 und 10 + final int height = rnd.nextInt(3, 10); // Höhe zwischen 3 und 10 + objectNode.put("width", width); + objectNode.put("height", height); + + // Zufälliges Spielfeld generieren + RobotFamily[][] stones = new RobotFamily[height][width]; + for (int row = 0; row < height; row++) { + for (int col = 0; col < width; col++) { + if (rnd.nextBoolean()) { + stones[row][col] = rnd.nextBoolean() ? RobotFamily.SQUARE_RED : RobotFamily.SQUARE_BLUE; + } + } + } + + // Spaltenindex zufällig wählen (auch ungültige Indizes) + final int column = rnd.nextInt(-1, width + 1); + objectNode.put("column", column); + + // Erwartetes Ergebnis berechnen + boolean expectedResult = column >= 0 && column < width && stones[height - 1][column] == null; + objectNode.put("expected result", expectedResult); + + // Spielfeld in JSON-Format umwandeln + ArrayNode stonesArray = mapper.createArrayNode(); + for (int row = 0; row < height; row++) { + ArrayNode rowArray = mapper.createArrayNode(); + for (int col = 0; col < width; col++) { + if (stones[row][col] == null) { + rowArray.add("EMPTY"); + } else if (stones[row][col] == RobotFamily.SQUARE_RED) { + rowArray.add("SQUARE_RED"); + } else { + rowArray.add("SQUARE_BLUE"); + } + } + stonesArray.add(rowArray); + } + objectNode.set("stones", stonesArray); + + return objectNode; + }, + TEST_ITERATIONS, + "FourWinsTestValidateInputRandomCases.generated.json" + ); + } + + @Test + public void generateFourWinsTestGameBoard() throws IOException { + TestUtils.generateJsonTestData( + (mapper, index, rnd) -> { + int worldHeight = rnd.nextInt(5, 10); + int worldWidth = rnd.nextInt(5, 10); + // SQUARE_RED <-> true, SQUARE_BLUE <-> false, null <-> null + RobotFamily[][] gameBoard = new RobotFamily[worldHeight][worldWidth]; + List firstFreeIndex = new ArrayList<>(worldWidth); // values may exceed array index range + for (int col = 0; col < worldWidth; col++) { + int rowsToFill = rnd.nextInt(worldHeight); + for (int row = 0; row <= rowsToFill; row++) { + gameBoard[row][col] = RobotFamily.SQUARE_RED; + } + firstFreeIndex.add(rowsToFill + 1); + } + + ArrayNode firstFreeIndexNode = mapper.createArrayNode(); + firstFreeIndex.stream() + .map(mapper.getNodeFactory()::numberNode) + .forEach(firstFreeIndexNode::add); + + ArrayNode gameBoardNode = mapper.createArrayNode(); + Arrays.stream(gameBoard) + .map(gameBoardRow -> Arrays.stream(gameBoardRow) + .map(rf -> mapper.getNodeFactory().textNode(rf != null ? rf.getName() : null)) + .toList()) + .forEach(gameBoardRow -> gameBoardNode.add(mapper.createArrayNode().addAll(gameBoardRow))); + + ObjectNode objectNode = mapper.createObjectNode() + .put("worldHeight", worldHeight) + .put("worldWidth", worldWidth); + objectNode.set("firstFreeIndex", firstFreeIndexNode); + objectNode.set("gameBoard", gameBoardNode); + + return objectNode; + }, + TEST_ITERATIONS, + "FourWinsTestGameBoard.generated.json" + ); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void generateFourWinsTestGameBoardWin(boolean horizontal) throws IOException { + TestUtils.generateJsonTestData( + (mapper, index, rnd) -> { + int worldHeight = rnd.nextInt(5, 10); + int worldWidth = rnd.nextInt(5, 10); + RobotFamily currentPlayer = rnd.nextBoolean() ? RobotFamily.SQUARE_RED : RobotFamily.SQUARE_BLUE; + ObjectNode objectNode = mapper.createObjectNode() + .put("worldHeight", worldHeight) + .put("worldWidth", worldWidth) + .put("currentPlayer", currentPlayer.getName()); + + ArrayNode winningCoordinates = mapper.createArrayNode(); + rnd.ints(rnd.nextInt(3), 0, horizontal ? worldHeight : worldWidth) + .distinct() + .forEach(i -> { + if (horizontal) { + winningCoordinates.add(mapper.createObjectNode() + .put("x", rnd.nextInt(worldWidth - 4 + 1)) + .put("y", i)); + } else { + winningCoordinates.add(mapper.createObjectNode() + .put("x", i) + .put("y", rnd.nextInt(worldHeight - 4 + 1))); + } + }); + objectNode.set(horizontal ? "winningRowCoordinates" : "winningColCoordinates", winningCoordinates); + + RobotFamily[][] gameBoard = new RobotFamily[worldHeight][worldWidth]; + for (JsonNode node : winningCoordinates) { + for (int offset = 0; offset < 4; offset++) { + if (horizontal) { + gameBoard[node.get("y").intValue()][node.get("x").intValue() + offset] = currentPlayer; + } else { + gameBoard[node.get("y").intValue() + offset][node.get("x").intValue()] = currentPlayer; + } + } + } + + ArrayNode gameBoardNode = mapper.createArrayNode(); + Arrays.stream(gameBoard) + .map(gameBoardRow -> Arrays.stream(gameBoardRow) + .map(rf -> mapper.getNodeFactory().textNode(rf != null ? rf.getName() : null)) + .toList()) + .forEach(gameBoardRow -> gameBoardNode.add(mapper.createArrayNode().addAll(gameBoardRow))); + objectNode.set("gameBoard", gameBoardNode); + + return objectNode; + }, + TEST_ITERATIONS, + "FourWinsTestGameBoard" + (horizontal ? "Horizontal" : "Vertical") + "Win.generated.json" + ); + } +} diff --git a/solution/H02/src/graderPrivate/java/h02/TestUtils.java b/solution/H02/src/graderPrivate/java/h02/TestUtils.java new file mode 100644 index 0000000..8c8ee0b --- /dev/null +++ b/solution/H02/src/graderPrivate/java/h02/TestUtils.java @@ -0,0 +1,110 @@ +package h02; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.tudalgo.algoutils.tutor.general.SpoonUtils; +import spoon.reflect.declaration.CtElement; +import spoon.reflect.declaration.CtMethod; +import spoon.reflect.declaration.CtParameter; + +import java.io.IOException; +import java.nio.file.Paths; +import java.util.Iterator; +import java.util.List; +import java.util.Random; +import java.util.function.Consumer; + +import static h02.TestConstants.RANDOM_SEED; + +public abstract class TestUtils { + + /** + * A generator for JSON test data. + */ + public interface JsonGenerator { + /** + * Generates a JSON object node. + * + * @param mapper The object mapper to use. + * @param index The index of the object node. + * @param rnd The random number generator to use. + * @return The generated JSON object node. + */ + ObjectNode generateJson(ObjectMapper mapper, int index, Random rnd); + } + + /** + * Generates and saves JSON test data. + * + * @param generator The generator to use. + * @param amount The amount of test data to generate. + * @param fileName The file name to save the test data to. + * @throws IOException If an I/O error occurs. + */ + public static void generateJsonTestData(final JsonGenerator generator, final int amount, final String fileName) throws IOException { + final var seed = RANDOM_SEED; + final var random = new java.util.Random(seed); + final ObjectMapper mapper = new ObjectMapper(); + final ArrayNode arrayNode = mapper.createArrayNode(); + System.out.println("Generating test data with seed: " + seed); + for (int i = 0; i < amount; i++) { + arrayNode.add(generator.generateJson(mapper, i, random)); + } + + // convert `ObjectNode` to pretty-print JSON +// System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(arrayNode)); + + final var path = Paths.get( + "src", + "graderPrivate", + "resources", + "h02", + fileName + ).toAbsolutePath(); + System.out.printf("Saving to file: %s%n", path); + final var file = path.toFile(); + file.createNewFile(); + mapper.writerWithDefaultPrettyPrinter().writeValue(file, arrayNode); + } + + /** + * Returns the Spoon representation of the given method. + * + * @param clazz the method's owner + * @param methodName the method name + * @param paramTypes the method's formal parameter types, if any + * @return the Spoon representation of the given method + */ + public static CtMethod getCtMethod(Class clazz, String methodName, Class... paramTypes) { + return SpoonUtils.getType(clazz.getName()) + .getMethodsByName(methodName) + .stream() + .filter(ctMethod -> { + List> parameters = ctMethod.getParameters(); + boolean result = parameters.size() == paramTypes.length; + for (int i = 0; result && i < parameters.size(); i++) { + result = parameters.get(i).getType().getQualifiedName().equals(paramTypes[i].getTypeName()); + } + return result; + }) + .findAny() + .orElseThrow(); + } + + /** + * Applies the given consumer to the body and its descendants of the given method. + * See also: {@link #getCtMethod(Class, String, Class[])}. + * + * @param clazz the method's owner + * @param methodName the method name + * @param paramTypes the method's formal parameter types, if any + * @param consumer the consumer to apply + */ + public static void iterateMethodStatements(Class clazz, String methodName, Class[] paramTypes, Consumer> consumer) { + Iterator iterator = getCtMethod(clazz, methodName, paramTypes) + .getBody() + .descendantIterator(); + consumer.accept(iterator); + } +} diff --git a/solution/H02/src/graderPrivate/java/h02/TwoDimensionalArrayStuffTest.java b/solution/H02/src/graderPrivate/java/h02/TwoDimensionalArrayStuffTest.java new file mode 100644 index 0000000..8a14401 --- /dev/null +++ b/solution/H02/src/graderPrivate/java/h02/TwoDimensionalArrayStuffTest.java @@ -0,0 +1,104 @@ +package h02; + +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.sourcegrade.jagr.api.rubric.TestForSubmission; +import org.tudalgo.algoutils.tutor.general.annotation.SkipAfterFirstFailedTest; +import org.tudalgo.algoutils.tutor.general.assertions.Context; +import org.tudalgo.algoutils.tutor.general.json.JsonParameterSet; +import org.tudalgo.algoutils.tutor.general.json.JsonParameterSetTest; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.assertEquals; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.assertNotNull; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.callObject; +import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.contextBuilder; + +@TestForSubmission +@Timeout( + value = TestConstants.TEST_TIMEOUT_IN_SECONDS, + unit = TimeUnit.SECONDS, + threadMode = Timeout.ThreadMode.SEPARATE_THREAD +) +@SkipAfterFirstFailedTest(TestConstants.SKIP_AFTER_FIRST_FAILED_TEST) +public class TwoDimensionalArrayStuffTest { + + @ParameterizedTest + @JsonParameterSetTest("TwoDimensionalArrayStuffTestEmptySentence.json") + public void testOccurrencesEmptyArray(JsonParameterSet params) { + testOccurrences(params); + } + + @ParameterizedTest + @JsonParameterSetTest("TwoDimensionalArrayStuffTestSingleSentence.json") + public void testOccurrencesSingleSentence(JsonParameterSet params) { + testOccurrences(params); + } + + @ParameterizedTest + @JsonParameterSetTest("TwoDimensionalArrayStuffTestMultipleSentences.json") + public void testOccurrencesMultipleSentences(JsonParameterSet params) { + testOccurrences(params); + } + + @ParameterizedTest + @JsonParameterSetTest("TwoDimensionalArrayStuffTestIntegerMean.json") + public void testMeanInteger(JsonParameterSet params) { + testMean(params); + } + + @ParameterizedTest + @JsonParameterSetTest("TwoDimensionalArrayStuffTestFloatMean.json") + public void testMeanFloat(JsonParameterSet params) { + testMean(params); + } + + private static void testOccurrences(JsonParameterSet params) { + List sentences = params.get("sentences"); + String[][] input = sentences.stream() + .map(s -> s.split(" ")) + .toArray(String[][]::new); + String query = params.getString("query"); + Context context = contextBuilder() + .add("input", input) + .add("query", query) + .build(); + + List expectedList = params.get("expectedResult"); + AtomicInteger counter = new AtomicInteger(0); + int[] expected = new int[expectedList.size()]; + expectedList.forEach(i -> expected[counter.getAndIncrement()] = i); + int[] actual = callObject(() -> TwoDimensionalArrayStuff.occurrences(input, query), context, result -> + "An exception occurred while invoking method occurrences"); + + assertNotNull(actual, context, result -> + "Array returned by method occurrences is null"); + assertEquals(input.length, actual.length, context, result -> + "Array returned by method occurrences does not have correct length"); + for (int i = 0; i < sentences.size(); i++) { + final int finalI = i; + assertEquals(expected[i], actual[i], context, result -> + "Array returned by method occurrences does not have correct value at index " + finalI); + } + } + + private static void testMean(JsonParameterSet params) { + List inputList = params.get("input"); + AtomicInteger counter = new AtomicInteger(0); + int[] input = new int[inputList.size()]; + inputList.forEach(i -> input[counter.getAndIncrement()] = i); + Context context = contextBuilder() + .add("input", input) + .build(); + + float expected = params.getFloat("expected"); + float actual = callObject(() -> TwoDimensionalArrayStuff.mean(input), context, result -> + "An exception occurred while invoking method mean"); + + // susceptible to rounding errors? + assertEquals(expected, actual, context, result -> "Value returned by method mean is incorrect"); + } +} diff --git a/solution/H02/src/graderPrivate/resources/h02/.gitignore b/solution/H02/src/graderPrivate/resources/h02/.gitignore new file mode 100644 index 0000000..f5d73e8 --- /dev/null +++ b/solution/H02/src/graderPrivate/resources/h02/.gitignore @@ -0,0 +1 @@ +*.generated.json diff --git a/solution/H02/src/graderPrivate/resources/h02/FourWinsTestValidateInput.json b/solution/H02/src/graderPrivate/resources/h02/FourWinsTestValidateInput.json new file mode 100644 index 0000000..eda9fa4 --- /dev/null +++ b/solution/H02/src/graderPrivate/resources/h02/FourWinsTestValidateInput.json @@ -0,0 +1,175 @@ +[ + { + "column": -1, + "width": 7, + "height": 6, + "stones": [ + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"] + ], + "expected result": false + }, + { + "column": 7, + "width": 7, + "height": 6, + "stones": [ + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"] + ], + "expected result": false + }, + { + "column": 3, + "width": 7, + "height": 6, + "stones": [ + ["EMPTY", "EMPTY", "EMPTY", "SQUARE_RED", "EMPTY", "EMPTY", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "SQUARE_BLUE", "EMPTY", "EMPTY", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"] + ], + "expected result": true + }, + { + "column": 5, + "width": 7, + "height": 6, + "stones": [ + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "SQUARE_RED", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "SQUARE_BLUE", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "SQUARE_RED", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "SQUARE_BLUE", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "SQUARE_RED", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "SQUARE_BLUE", "EMPTY"] + ], + "expected result": false + }, + { + "column": 2, + "width": 7, + "height": 6, + "stones": [ + ["EMPTY", "EMPTY", "SQUARE_RED", "EMPTY", "EMPTY", "EMPTY", "EMPTY"], + ["EMPTY", "EMPTY", "SQUARE_BLUE", "EMPTY", "EMPTY", "EMPTY", "EMPTY"], + ["EMPTY", "EMPTY", "SQUARE_RED", "EMPTY", "EMPTY", "EMPTY", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"] + ], + "expected result": true + }, + { + "column": 6, + "width": 7, + "height": 6, + "stones": [ + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "SQUARE_BLUE"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "SQUARE_RED"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "SQUARE_BLUE"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "SQUARE_RED"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "SQUARE_BLUE"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "SQUARE_RED"] + ], + "expected result": false + }, + { + "column": 4, + "width": 7, + "height": 6, + "stones": [ + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"] + ], + "expected result": true + }, + { + "column": 2, + "width": 4, + "height": 4, + "stones": [ + ["EMPTY", "EMPTY", "SQUARE_RED", "EMPTY"], + ["SQUARE_BLUE", "SQUARE_RED", "SQUARE_RED", "EMPTY"], + ["SQUARE_RED", "SQUARE_BLUE", "SQUARE_RED", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY"] + ], + "expected result": true + }, + { + "column": 3, + "width": 4, + "height": 5, + "stones": [ + ["EMPTY", "EMPTY", "SQUARE_RED", "SQUARE_BLUE"], + ["SQUARE_RED", "SQUARE_BLUE", "SQUARE_RED", "SQUARE_RED"], + ["SQUARE_BLUE", "SQUARE_RED", "SQUARE_RED", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY"] + ], + "expected result": true + }, + { + "column": 1, + "width": 5, + "height": 4, + "stones": [ + ["EMPTY", "EMPTY", "SQUARE_BLUE", "EMPTY", "EMPTY"], + ["SQUARE_RED", "SQUARE_RED", "SQUARE_RED", "EMPTY", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"] + ], + "expected result": true + }, + { + "column": 0, + "width": 3, + "height": 3, + "stones": [ + ["SQUARE_RED", "EMPTY", "EMPTY"], + ["SQUARE_BLUE", "EMPTY", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY"] + ], + "expected result": true + }, + { + "column": 6, + "width": 8, + "height": 5, + "stones": [ + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "SQUARE_BLUE", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "SQUARE_RED", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "SQUARE_BLUE", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "SQUARE_RED", "EMPTY"], + ["EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "SQUARE_BLUE", "EMPTY"] + ], + "expected result": false + }, + { + "column": 2, + "width": 8, + "height": 6, + "stones": [ + ["EMPTY", "EMPTY", "SQUARE_RED", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"], + ["SQUARE_BLUE", "SQUARE_RED", "SQUARE_BLUE", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"], + ["SQUARE_RED", "SQUARE_BLUE", "SQUARE_RED", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"], + ["SQUARE_BLUE", "SQUARE_RED", "SQUARE_BLUE", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"], + ["SQUARE_RED", "SQUARE_BLUE", "SQUARE_RED", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"], + ["SQUARE_BLUE", "SQUARE_RED", "SQUARE_BLUE", "EMPTY", "EMPTY", "EMPTY", "EMPTY", "EMPTY"] + ], + "expected result": false + } +] diff --git a/solution/H02/src/graderPrivate/resources/h02/TwoDimensionalArrayStuffTestEmptySentence.json b/solution/H02/src/graderPrivate/resources/h02/TwoDimensionalArrayStuffTestEmptySentence.json new file mode 100644 index 0000000..0bd8735 --- /dev/null +++ b/solution/H02/src/graderPrivate/resources/h02/TwoDimensionalArrayStuffTestEmptySentence.json @@ -0,0 +1,7 @@ +[ + { + "sentences": [], + "query": "", + "expectedResult": [] + } +] diff --git a/solution/H02/src/graderPrivate/resources/h02/TwoDimensionalArrayStuffTestFloatMean.json b/solution/H02/src/graderPrivate/resources/h02/TwoDimensionalArrayStuffTestFloatMean.json new file mode 100644 index 0000000..1e59e57 --- /dev/null +++ b/solution/H02/src/graderPrivate/resources/h02/TwoDimensionalArrayStuffTestFloatMean.json @@ -0,0 +1,22 @@ +[ + { + "input": [1, 1, 2, 3, 5, 8], + "expected": 3.3333333 + }, + { + "input": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "expected": 5.5 + }, + { + "input": [1, 10, 100, 1000, 10000], + "expected": 2222.2 + }, + { + "input": [11, 22, 33, 44], + "expected": 27.5 + }, + { + "input": [5, 10, 15, 20, 25, 30], + "expected": 17.5 + } +] diff --git a/solution/H02/src/graderPrivate/resources/h02/TwoDimensionalArrayStuffTestIntegerMean.json b/solution/H02/src/graderPrivate/resources/h02/TwoDimensionalArrayStuffTestIntegerMean.json new file mode 100644 index 0000000..5e93b7b --- /dev/null +++ b/solution/H02/src/graderPrivate/resources/h02/TwoDimensionalArrayStuffTestIntegerMean.json @@ -0,0 +1,26 @@ +[ + { + "input": [5, 5], + "expected": 5 + }, + { + "input": [1, 2, 3, 4, 5], + "expected": 3 + }, + { + "input": [5, 4, 3, 2, 1], + "expected": 3 + }, + { + "input": [1, 2, 1, 3, 1, 4], + "expected": 2 + }, + { + "input": [1, 11, 11, 1], + "expected": 6 + }, + { + "input": [2, 0, 2, 4], + "expected": 2 + } +] diff --git a/solution/H02/src/graderPrivate/resources/h02/TwoDimensionalArrayStuffTestMultipleSentences.json b/solution/H02/src/graderPrivate/resources/h02/TwoDimensionalArrayStuffTestMultipleSentences.json new file mode 100644 index 0000000..d73c1d3 --- /dev/null +++ b/solution/H02/src/graderPrivate/resources/h02/TwoDimensionalArrayStuffTestMultipleSentences.json @@ -0,0 +1,39 @@ +[ + { + "sentences": [ + "lorem ipsum dolor sit amet", + "consectetur adipisici elit", + "sed eiusmod tempor incidunt ut labore et dolore magna aliqua" + ], + "query": "lorem", + "expectedResult": [1, 0, 0] + }, + { + "sentences": [ + "lorem ipsum dolor sit amet", + "amet sit dolor ipsum lorem" + ], + "query": "amet", + "expectedResult": [1, 1] + }, + { + "sentences": [ + "lorem lorem lorem", + "lorem lorem", + "lorem", + "lorem lorem", + "lorem lorem lorem" + ], + "query": "lorem", + "expectedResult": [3, 2, 1, 2, 3] + }, + { + "sentences": [ + "Wenn hinter fliegenden Fliegen Fliegen fliegen dann fliegen Fliegen Fliegen nach", + "Fliegers Fritze fängt fliegende Fliegen fliegende Fliegen fängt Flieger Fritze", + "Blaukraut bleibt Blaukraut und Brautkleid bleibt Brautkleid" + ], + "query": "Fliegen", + "expectedResult": [4, 2, 0] + } +] diff --git a/solution/H02/src/graderPrivate/resources/h02/TwoDimensionalArrayStuffTestSingleSentence.json b/solution/H02/src/graderPrivate/resources/h02/TwoDimensionalArrayStuffTestSingleSentence.json new file mode 100644 index 0000000..2894a60 --- /dev/null +++ b/solution/H02/src/graderPrivate/resources/h02/TwoDimensionalArrayStuffTestSingleSentence.json @@ -0,0 +1,30 @@ +[ + { + "sentences": [ + "lorem ipsum dolor sit amet" + ], + "query": "lorem", + "expectedResult": [1] + }, + { + "sentences": [ + "lorem ipsum dolor sit amet" + ], + "query": "amet", + "expectedResult": [1] + }, + { + "sentences": [ + "lorem lorem lorem" + ], + "query": "lorem", + "expectedResult": [3] + }, + { + "sentences": [ + "ipsum ipsum ipsum" + ], + "query": "lorem", + "expectedResult": [0] + } +] diff --git a/solution/H02/src/main/java/h02/FourWins.java b/solution/H02/src/main/java/h02/FourWins.java new file mode 100644 index 0000000..19cfc05 --- /dev/null +++ b/solution/H02/src/main/java/h02/FourWins.java @@ -0,0 +1,388 @@ +package h02; + +import fopbot.Direction; +import fopbot.Robot; +import fopbot.RobotFamily; +import fopbot.World; +import h02.template.InputHandler; +import org.tudalgo.algoutils.student.annotation.DoNotTouch; +import org.tudalgo.algoutils.student.annotation.StudentImplementationRequired; + +import java.util.Optional; + +/** + * The {@link FourWins} class represents the main class of the FourWins game. + */ +public class FourWins { + private final InputHandler inputHandler = new InputHandler(this); + /** + * The width of the game board. + */ + private final int width; + /** + * The height of the game board. + */ + private final int height; + /** + * Indicates whether the game has finished. + */ + @SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"}) + private boolean finished = false; + + /** + * Creates a new {@link FourWins} instance with the given width and height. + * + * @param width the width of the game board + * @param height the height of the game board + */ + FourWins(final int width, final int height) { + this.width = width; + this.height = height; + } + + /** + * Starts the game by setting up the world and executing the game loop. + */ + void startGame() { + setupWorld(); + gameLoop(); + } + + /** + * Sets up the world and installs the {@link InputHandler}. + */ + void setupWorld() { + World.setSize(width, height); + World.setDelay(10); + World.setVisible(true); + inputHandler.install(); + } + + + /** + * Validates if a given column index is within the bounds of the game board and not fully occupied. + * + * @param column The column index to validate. + * @param stones 2D array representing the game board, where each cell contains a RobotFamily color indicating the + * player that has placed a stone in that position. + * @return true if the column is within bounds and has at least one unoccupied cell; false otherwise. + */ + @StudentImplementationRequired("H2.2.1") + public static boolean validateInput(final int column, final RobotFamily[][] stones) { + return column >= 0 && column < World.getWidth() && stones[World.getHeight() - 1][column] == null; + } + + + /** + * Calculates the next unoccupied row index in the specified column. This row index is the next destination for a + * falling stone. + * + * @param column The column index where the stone is to be dropped. + * @param stones 2D array representing the game board, where each cell contains a RobotFamily object indicating the + * player that has placed a stone in that position. + * @return Index of the next unoccupied row index in the specified column. + */ + @StudentImplementationRequired("H2.2.2") + public static int getDestinationRow(final int column, final RobotFamily[][] stones) { + for (int row = 0; row < stones.length; row++) { + if (stones[row][column] == null) { + return row; + } + } + return -1; + } + + /** + * Drops a stone into the specified column of the game board, simulating a falling animation. This method gets the + * destination row for the stone in the specified column with the `getDestinationRow` method. It creates a new Robot + * instance to represent the stone with the currentPlayer's RobotFamily in the given column and the destination row. + * After that it simulates the stone's fall by decrementing its position until it reaches the destination row. Once + * the stone reaches its destination, the method updates the stones array (a 2D array of RobotFamily colors) to + * mark the slot as occupied by the currentPlayer. + * + * @param column The column index where the stone is to be dropped. + * @param stones 2D array representing the game board, where each cell contains a RobotFamily object + * indicating the player that has placed a stone in that position. + * @param currentPlayer The RobotFamily object representing the current player dropping the stone. + */ + @StudentImplementationRequired("H2.2.2") + public static void dropStone(final int column, final RobotFamily[][] stones, final RobotFamily currentPlayer) { + // spawn stone + final Robot stone = new Robot(column, World.getHeight() - 1, Direction.DOWN, 0, currentPlayer); + + // let stone fall + final int row = getDestinationRow(column, stones); + for (int currentRow = World.getHeight() - 1; currentRow > row; currentRow--) { + stone.move(); + } + + // turn stone up + stone.turnLeft(); + stone.turnLeft(); + + // set slot as occupied + stones[row][column] = currentPlayer; + } + + + /** + * Checks if the current player has won by any condition. The conditions can be a horizontal, vertical, diagonal, or + * anti-diagonal line of at least four stones. + * + * @param stones 2D array representing the game board, where each cell contains a RobotFamily color + * indicating the player that has placed a stone in that position. + * @param currentPlayer The RobotFamily color representing the current player to check for a win. + * @return true if the current player has formed a horizontal line of at least four stones; false otherwise. + */ + @StudentImplementationRequired("H2.2.3") + public static boolean testWinConditions(final RobotFamily[][] stones, final RobotFamily currentPlayer) { + return testWinVertical(stones, currentPlayer) + || testWinHorizontal(stones, currentPlayer) + || testWinDiagonal(stones, currentPlayer); + } + + /** + * Checks if the current player has won by forming a horizontal line of at least consecutive four stones. + * + * @param stones 2D array representing the game board, where each cell contains a RobotFamily color + * indicating the player that has placed a stone in that position. + * @param currentPlayer The RobotFamily color representing the current player to check for a win. + * @return true if the current player has formed a horizontal line of at least four stones; false otherwise. + */ + @StudentImplementationRequired("H2.2.3") + public static boolean testWinHorizontal(final RobotFamily[][] stones, final RobotFamily currentPlayer) { + for (int row = 0; row < World.getHeight(); row++) { + int stoneCount = 0; + for (int column = 0; column < World.getWidth(); column++) { + stoneCount = stones[row][column] == currentPlayer ? stoneCount + 1 : 0; + if (stoneCount >= 4) { + return true; + } + } + } + return false; + } + + /** + * Checks if the current player has won by forming a vertical line of at least consecutive four stones. + * + * @param stones 2D array representing the game board, where each cell contains a RobotFamily color + * indicating the player that has placed a stone in that position. + * @param currentPlayer The RobotFamily color representing the current player to check for a win. + * @return true if the current player has formed a vertical line of at least four stones; false otherwise. + */ + @StudentImplementationRequired("H2.2.3") + public static boolean testWinVertical(final RobotFamily[][] stones, final RobotFamily currentPlayer) { + for (int column = 0; column < World.getWidth(); column++) { + int stoneCount = 0; + for (int row = 0; row < World.getHeight(); row++) { + stoneCount = stones[row][column] == currentPlayer ? stoneCount + 1 : 0; + if (stoneCount >= 4) { + return true; + } + } + } + return false; + } + + /** + * Checks if the current player has won by forming a diagonal line of at least consecutive four stones. + * + * @param stones 2D array representing the game board, where each cell contains a RobotFamily color + * indicating the player that has placed a stone in that position. + * @param currentPlayer The RobotFamily color representing the current player to check for a win. + * @return true if the current player has formed a diagonal line of at least four stones; false otherwise. + */ + @DoNotTouch + public static boolean testWinDiagonal(final RobotFamily[][] stones, final RobotFamily currentPlayer) { + @SuppressWarnings("CheckStyle") final int MAX_STONES = 4; + + @SuppressWarnings("CheckStyle") final int WIDTH = World.getWidth(); + @SuppressWarnings("CheckStyle") final int HEIGHT = World.getHeight(); + int[] direction = new int[]{1, 1}; + + // for every field + for (int y = 0; y < HEIGHT; y++) { + for (int x = 0; x < WIDTH; x++) { + + // for every direction + for (int nthDirection = 0; nthDirection < 4; nthDirection++) { + final int[] pos = {x, y}; + + // test for consecutive coins + int coinCount = 0; // start counting at 0 + while (pos[0] >= 0 && pos[0] < WIDTH + && pos[1] >= 0 && pos[1] < HEIGHT + && stones[pos[1]][pos[0]] == currentPlayer) { + coinCount++; // count every stone that has currentPlayer's color + if (coinCount >= MAX_STONES) { + return true; + } + pos[0] += direction[0]; + pos[1] += direction[1]; + } + + direction = new int[]{direction[1], -direction[0]}; // next direction (rotate by 90 deg) + } + } + } + + return false; + } + + + /** + * Switches the player for each turn. If the current player is SQUARE_BLUE, SQUARE_RED is returned as the next + * player. If the current player is SQUARE_RED, SQUARE_BLUE is returned as the next player. + * + * @param currentPlayer The player color of the current player. + * @return The player color of the next player. + */ + @StudentImplementationRequired("H2.2.4") + public static RobotFamily nextPlayer(final RobotFamily currentPlayer) { + return currentPlayer == RobotFamily.SQUARE_BLUE ? RobotFamily.SQUARE_RED : RobotFamily.SQUARE_BLUE; + } + + /** + * Displays a Message in the console and on the game board indicating the game is drawn. + */ + @StudentImplementationRequired("H2.2.4") + public void writeDrawMessage() { + inputHandler.displayDrawStatus(); + + // student implementation here: + System.out.println("No valid columns found. Hence, game ends with a draw."); + } + + /** + * Displays a Message in the console and on the game board indicating the game is won by a player. + * + * @param winner {@link RobotFamily} of the winning player + */ + @StudentImplementationRequired("H2.2.4") + public void writeWinnerMessage(final RobotFamily winner) { + inputHandler.displayWinnerStatus(winner); + + // student implementation here: + System.out.println("Player " + winner + " wins the game!"); + } + + /** + * Displays the winner of the game by printing the winning color in the console and filling the whole field with + * Robots of the winning color. + * + * @param winner The RobotFamily color of the winner. + */ + @StudentImplementationRequired("H2.2.4") + public static void colorFieldBackground(final RobotFamily winner) { + for (int x = 0; x < World.getWidth(); x++) { + for (int y = 0; y < World.getHeight(); y++) { + setFieldColor(x, y, winner); + } + } + } + + /** + * Executes the main game loop, handling player turns, stone drops, and win condition checks. This method + * initializes the game board as a 2D array of RobotFamily colors, representing the slots that can be filled with + * players' stones. It starts with a predefined currentPlayer and continues in a loop until a win condition is met. + * Each iteration of the loop waits for player input to select a column to drop a stone into, switches the current + * player, drops the stone in the selected column, and checks for win conditions. If a win condition is met, the + * loop ends, and the winner is displayed. + */ + @StudentImplementationRequired("H2.2.4") + void gameLoop() { + final RobotFamily[][] stones = new RobotFamily[World.getHeight()][World.getWidth()]; + RobotFamily currentPlayer = RobotFamily.SQUARE_BLUE; + + boolean draw = false; + finished = false; + + while (!finished) { + // student implementation here: + currentPlayer = nextPlayer(currentPlayer); + + // wait for click in column (DO NOT TOUCH) + finished = draw = isGameBoardFull(stones); + if (draw) { + break; + } + final int column = inputHandler.getNextInput(currentPlayer, stones); + + // student implementation here: + dropStone(column, stones, currentPlayer); + finished = testWinConditions(stones, currentPlayer); + } + + // displaying either draw or winner (DO NOT TOUCH) + if (draw) { + writeDrawMessage(); + colorFieldBackground(getDrawnRobotFamily()); + } else { + writeWinnerMessage(currentPlayer); + colorFieldBackground(currentPlayer); + } + } + + + /** + * Executes the main game loop, handling player turns, stone drops, and win condition checks. Sets the background + * color of a field at the specified coordinates. The color is derived from the {@link RobotFamily} SQUARE_BLUE or + * SQUARE_RED. + * + * @param x the x coordinate of the field + * @param y the y coordinate of the field + * @param color the {@link RobotFamily} corresponding to the field color to set + */ + @DoNotTouch + public static void setFieldColor(final int x, final int y, final RobotFamily color) { + World.getGlobalWorld().setFieldColor(x, y, color.getColor()); + } + + /** + * Returns the {@link RobotFamily} which represents a drawn game. + * + * @return the {@link RobotFamily} which represents a drawn game. + */ + @DoNotTouch + @SuppressWarnings("UnstableApiUsage") + protected static RobotFamily getDrawnRobotFamily() { + return Optional.ofNullable(World.getGlobalWorld().getGuiPanel()).filter(guiPanel -> !guiPanel.isDarkMode()).map(guiPanel -> RobotFamily.SQUARE_ORANGE).orElse(RobotFamily.SQUARE_YELLOW); + } + + /** + * Checks if all columns of the game board are fully occupied. + * + * @param stones 2D array representing the game board, where each cell contains a RobotFamily + * @return true if all columns of the game board are fully occupied; false otherwise. + */ + @DoNotTouch + public static boolean isGameBoardFull(final RobotFamily[][] stones) { + for (int x = 0; x < World.getWidth(); x++) { + if (FourWins.validateInput(x, stones)) { + return false; + } + } + return true; + } + + /** + * Returns this instance's {@link InputHandler}. + * + * @return the input handler + */ + @DoNotTouch + public InputHandler getInputHandler() { + return inputHandler; + } + + /** + * Returns {@code true} when the game is finished, {@code false} otherwise. + * + * @return whether the game is finished. + */ + public boolean isFinished() { + return finished; + } + +} diff --git a/solution/H02/src/main/java/h02/Main.java b/solution/H02/src/main/java/h02/Main.java new file mode 100644 index 0000000..452d68f --- /dev/null +++ b/solution/H02/src/main/java/h02/Main.java @@ -0,0 +1,218 @@ +package h02; + +import fopbot.RobotFamily; +import fopbot.World; +import org.tudalgo.algoutils.student.annotation.SolutionOnly; +import org.tudalgo.algoutils.student.annotation.StudentImplementationRequired; + +import static org.tudalgo.algoutils.student.io.PropertyUtils.getIntProperty; +import static org.tudalgo.algoutils.student.test.StudentTestUtils.printTestResults; +import static org.tudalgo.algoutils.student.test.StudentTestUtils.testEquals; + +/** + * Main entry point in executing the program. + */ +public class Main { + /** + * Main entry point in executing the program. + * + * @param args program arguments, currently ignored + */ + public static void main(final String[] args) { + // H1 + sanityChecksH211(); + sanityChecksH212(); + printTestResults(); + + // H2 + sanityChecksH22(); + printTestResults(); + + // starting game (comment out if you just want to run the tests) + final var propFile = "h02.properties"; + new FourWins( + getIntProperty(propFile, "FW_WORLD_WIDTH"), + getIntProperty(propFile, "FW_WORLD_HEIGHT") + ).startGame(); + } + + /** + * Perform sanity checks for exercise H2.1.1. + */ + @StudentImplementationRequired("H2.3") + public static void sanityChecksH211() { + // push test + final int[] newArray = OneDimensionalArrayStuff.push(new int[]{0, 1}, 2); + final int[] expectedArray = {0, 1, 2}; + testEquals(expectedArray.length, newArray.length); + for (int i = 0; i < newArray.length; i++) { + testEquals(expectedArray[i], newArray[i]); + } + + // calculateNextFibonacci test + int[] fibonacciArray = {0, 1}; + for (int i = 0; i < 20; i++) { + fibonacciArray = OneDimensionalArrayStuff.calculateNextFibonacci(fibonacciArray); + } + testEquals(22, fibonacciArray.length); + testEquals(0, fibonacciArray[0]); + testEquals(1, fibonacciArray[1]); + for (int i = 2; i < fibonacciArray.length; i++) { + testEquals(fibonacciArray[i - 1] + fibonacciArray[i - 2], fibonacciArray[i]); + } + + // fibonacci test + final int[] reference = {0, 1, 1, 2, 3, 5, 8, 13, 21, 34}; + for (int i = 0; i < 10; i++) { + testEquals(reference[i], OneDimensionalArrayStuff.fibonacci(i)); + } + } + + /** + * Perform sanity checks for exercise H2.1.2. + */ + @StudentImplementationRequired("H2.3") + public static void sanityChecksH212() { + // predefined simple test + final String[][] simpleTest = new String[][]{ + "a b c d e f".split(" "), + "a b c d e f".split(" "), + "a b c d e f".split(" "), + }; + // predefined complex test + final String[][] complexTest = new String[][]{ + "a a b b c c".split(" "), + "a b c d e f".split(" "), + "a a a b b b c c c".split(" "), + }; + + + // student implementation here: + + sanityChecksH212Helper( + simpleTest, + "b", + new int[]{1, 1, 1}, + 1 + ); + + sanityChecksH212Helper( + complexTest, + "b", + new int[]{2, 1, 3}, + 2 + ); + } + + /** + * Helper method for sanity checks for exercise H2.1.2. + * + * @param input the input array + * @param query the query string + * @param refOcc the reference occurrences + * @param refMean the reference mean + */ + @SolutionOnly + public static void sanityChecksH212Helper( + final String[][] input, + final String query, + final int[] refOcc, + final float refMean + ) { + final int[] occ = TwoDimensionalArrayStuff.occurrences(input, query); + testEquals(refOcc.length, occ.length); + for (int i = 0; i < occ.length; i++) { + testEquals(refOcc[i], occ[i]); + } + testEquals(refMean, TwoDimensionalArrayStuff.meanOccurrencesPerLine(input, query)); + } + + /** + * Perform sanity checks for exercise H2.2 + */ + @StudentImplementationRequired("H2.4") + public static void sanityChecksH22() { + // setting world size + World.setSize(4, 5); + + // predefined stones1 array + final RobotFamily[][] stones1 = { + {null, RobotFamily.SQUARE_BLUE, null, RobotFamily.SQUARE_RED}, + {null, null, null, RobotFamily.SQUARE_BLUE}, + {null, null, null, RobotFamily.SQUARE_RED}, + {null, null, null, RobotFamily.SQUARE_BLUE}, + {null, null, null, RobotFamily.SQUARE_RED}, + }; + + // predefined stones2 array + final RobotFamily[][] stones2 = { + {RobotFamily.SQUARE_BLUE, RobotFamily.SQUARE_BLUE, RobotFamily.SQUARE_BLUE, RobotFamily.SQUARE_BLUE}, + {RobotFamily.SQUARE_RED, RobotFamily.SQUARE_RED, RobotFamily.SQUARE_BLUE, RobotFamily.SQUARE_RED}, + {RobotFamily.SQUARE_BLUE, RobotFamily.SQUARE_RED, RobotFamily.SQUARE_BLUE, RobotFamily.SQUARE_BLUE}, + {RobotFamily.SQUARE_BLUE, RobotFamily.SQUARE_RED, RobotFamily.SQUARE_BLUE, RobotFamily.SQUARE_RED}, + {RobotFamily.SQUARE_BLUE, RobotFamily.SQUARE_BLUE, RobotFamily.SQUARE_BLUE, RobotFamily.SQUARE_RED}, + }; + + + // student implementation here: + + // H2.2.1 validateInput + final boolean isInCol1 = FourWins.validateInput(1, stones1); + final boolean isInCol3 = FourWins.validateInput(3, stones1); + + testEquals(true, isInCol1); + testEquals(false, isInCol3); + + + // H2.2.2 getDestinationRow + final int rowCol1 = FourWins.getDestinationRow(1, stones1); + final int rowCol3 = FourWins.getDestinationRow(3, stones1); + + testEquals(1, rowCol1); + testEquals(-1, rowCol3); + + + // H2.2.2 dropStone + FourWins.dropStone(1, stones1, RobotFamily.SQUARE_RED); + // System.out.println(Arrays.deepToString(stones1)); + // System.out.println(stones1); + testEquals(RobotFamily.SQUARE_RED, stones1[1][1]); + + + // H2.2.3 testWinHorizontal + final boolean winRowBlue = FourWins.testWinHorizontal(stones2, RobotFamily.SQUARE_BLUE); + final boolean winRowRed = FourWins.testWinHorizontal(stones2, RobotFamily.SQUARE_RED); + + testEquals(true, winRowBlue); + testEquals(false, winRowRed); + + + // H2.2.3 testWinVertical + final boolean winColStones2 = FourWins.testWinVertical(stones2, RobotFamily.SQUARE_BLUE); + final boolean winColStones1 = FourWins.testWinVertical(stones1, RobotFamily.SQUARE_BLUE); + + testEquals(true, winColStones2); + testEquals(false, winColStones1); + + + // H2.2.3 testWinConditions + final boolean winStones2 = FourWins.testWinConditions(stones2, RobotFamily.SQUARE_BLUE); + final boolean winStones1 = FourWins.testWinConditions(stones1, RobotFamily.SQUARE_BLUE); + + testEquals(true, winStones2); + testEquals(false, winStones1); + + + // H2.2.4 switchPlayer + final RobotFamily nextPlayer1 = FourWins.nextPlayer(RobotFamily.SQUARE_BLUE); + final RobotFamily nextPlayer2 = FourWins.nextPlayer(RobotFamily.SQUARE_RED); + + testEquals(RobotFamily.SQUARE_RED, nextPlayer1); + testEquals(RobotFamily.SQUARE_BLUE, nextPlayer2); + + + // H2.2.4 colorFieldBackground, writeDrawMessage, writeWinnerMessage, gameLoop + // Test by playing + } + +} diff --git a/solution/H02/src/main/java/h02/OneDimensionalArrayStuff.java b/solution/H02/src/main/java/h02/OneDimensionalArrayStuff.java new file mode 100644 index 0000000..4fa4b5d --- /dev/null +++ b/solution/H02/src/main/java/h02/OneDimensionalArrayStuff.java @@ -0,0 +1,64 @@ +package h02; + +import org.tudalgo.algoutils.student.annotation.StudentImplementationRequired; + +/** + * This class serves as a container for the methods that are to be implemented by the students for exercise H2.1.1. + */ +public class OneDimensionalArrayStuff { + + /** + * Prevent instantiation of this utility class. + */ + private OneDimensionalArrayStuff() { + throw new IllegalStateException("This class is not meant to be instantiated."); + } + + /** + * Returns a new array that is a copy of the input array with the given value appended at the end. + * + * @param array the input array + * @param value the value to append + * @return a new array that is a copy of the input array with the given value appended at the end + */ + @StudentImplementationRequired("H2.1.1") + public static int[] push(final int[] array, final int value) { + final int[] newArray = new int[array.length + 1]; + //noinspection ManualArrayCopy + for (int i = 0; i < array.length; i++) { + newArray[i] = array[i]; + } + newArray[array.length] = value; + return newArray; + } + + /** + * Calculates the next Fibonacci number based on the given array and returns a new array with the next Fibonacci + * number appended at the end. + * + * @param array the input array containing the last two Fibonacci numbers up to the current point + * @return a new array with the next Fibonacci number appended at the end + */ + @StudentImplementationRequired("H2.1.1") + public static int[] calculateNextFibonacci(final int[] array) { + return push(array, array[array.length - 1] + array[array.length - 2]); + } + + /** + * Returns the n-th Fibonacci number. + * + * @param n the index of the Fibonacci number to return + * @return the n-th Fibonacci number + */ + @StudentImplementationRequired("H2.1.1") + public static int fibonacci(final int n) { + if (n < 2) { + return n; // base case (n=0 or n=1) + } + int[] array = {0, 1}; + for (int i = 2; i <= n; i++) { + array = calculateNextFibonacci(array); + } + return array[array.length - 1]; + } +} diff --git a/solution/H02/src/main/java/h02/TwoDimensionalArrayStuff.java b/solution/H02/src/main/java/h02/TwoDimensionalArrayStuff.java new file mode 100644 index 0000000..2db8863 --- /dev/null +++ b/solution/H02/src/main/java/h02/TwoDimensionalArrayStuff.java @@ -0,0 +1,89 @@ +package h02; + +import org.tudalgo.algoutils.student.annotation.DoNotTouch; +import org.tudalgo.algoutils.student.annotation.StudentImplementationRequired; + +import java.util.Arrays; + +/** + * This class serves as a container for the methods that are to be implemented by the students for exercise H2.1.2. + */ +public class TwoDimensionalArrayStuff { + + /** + * Prevent instantiation of this utility class. + */ + private TwoDimensionalArrayStuff() { + throw new IllegalStateException("This class is not meant to be instantiated."); + } + + /** + * Returns an array containing the number of occurrences of the query {@link String} in each line of the input array. + * + * @param input the input array + * @param query the query {@link String} + * @return an array containing the number of occurrences of the query {@link String} in each line of the input array + */ + @StudentImplementationRequired("H2.1.2") + public static int[] occurrences(final String[][] input, final String query) { + final int[] result = new int[input.length]; + for (int row = 0; row < input.length; row++) { + for (int col = 0; col < input[row].length; col++) { + if (input[row][col].equals(query)) { + result[row]++; + } + } + } + return result; + } + + /** + * Returns the mean of the input array. + * + * @param input the input array + * @return the mean of the input array + */ + @StudentImplementationRequired("H2.1.2") + public static float mean(final int[] input) { + int sum = 0; + for (final int j : input) { + sum += j; + } + return (float) sum / input.length; + } + + /** + * Returns the mean number of occurrences of the query {@link String} in each line of the input array. + * + * @param input the input array + * @param query the query {@link String} + * @return the mean number of occurrences of the query {@link String} in each line of the input array + */ + @DoNotTouch + public static float meanOccurrencesPerLine(final String[][] input, final String query) { + return mean(occurrences(input, query)); + } + + /** + * Overload that splits the input string by lines and spaces, then calls regular meanOccurrencesPerLine. + * + * @param input the input string to split by lines and spaces + * @param query the query {@link String} + * @return the mean number of occurrences of the query {@link String} in each line of the input array + */ + @DoNotTouch + public static float meanOccurrencesPerLine(final String input, final String query) { + // filter out unwanted symbols + final String filteredInput = input.replaceAll("[^\\w\\s]", ""); + // split by lines + final var processedInput = Arrays.stream(filteredInput.split("(\\r\\n|\\r|\\n)")) + // split by spaces + .map(line -> line.split("\\s")) + // collect to 2D array + .toArray(String[][]::new); + /// uncomment the following line to log processed input + // System.out.printf("Processed input: %s%n", Arrays.deepToString(processedInput)); + // call regular meanOccurrencesPerLine + return meanOccurrencesPerLine(processedInput, query); + } +} diff --git a/solution/H02/src/main/java/h02/template/InputHandler.java b/solution/H02/src/main/java/h02/template/InputHandler.java new file mode 100644 index 0000000..cb65bba --- /dev/null +++ b/solution/H02/src/main/java/h02/template/InputHandler.java @@ -0,0 +1,201 @@ +package h02.template; + +import fopbot.RobotFamily; +import fopbot.World; +import h02.FourWins; +import org.tudalgo.algoutils.student.annotation.DoNotTouch; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import java.beans.PropertyChangeEvent; +import java.util.concurrent.BlockingDeque; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; + +/** + * The {@link InputHandler} handles the input of the users. + */ +@DoNotTouch +public class InputHandler { + /** + * The input queue. + */ + private final BlockingDeque inputQueue = new LinkedBlockingDeque<>(); + + /** + * The {@link FourWins} instance. + */ + private final FourWins fourWins; + + /** + * Whether the row select mode is active. + */ + private final AtomicBoolean rowSelectMode = new AtomicBoolean(false); + + /** + * The status label. + */ + private final JLabel statusLabel = new JLabel("", SwingConstants.CENTER); + + /** + * Creates a new {@link InputHandler} instance. + * + * @param fourWins the {@link FourWins} instance + */ + public InputHandler(final FourWins fourWins) { + this.fourWins = fourWins; + final int padding = 4; // Padding in pixels + statusLabel.setBorder(new EmptyBorder(padding, padding, padding, padding)); + } + + /** + * Sets the color of the given column to the given color. + * + * @param column the column to set the color of + * @param colorSupplier the color to set + */ + private void setColumnColor(final int column, final Supplier colorSupplier) { + for (int i = 0; i < World.getHeight(); i++) { + final int finalI = i; + SwingUtilities.invokeLater(() -> World.getGlobalWorld().getField(column, finalI).setFieldColor(colorSupplier)); + } + } + + /** + * Executes the given action only if the game is running. + * + * @param action the action to execute + */ + private void whenGameIsRunning(final Runnable action) { + if (!fourWins.isFinished()) { + action.run(); + } + } + + /** + * Installs the input handler to the fopbot world. + */ + @SuppressWarnings("UnstableApiUsage") + public void install() { + final var guiPanel = World.getGlobalWorld().getGuiPanel(); + final var guiFrame = World.getGlobalWorld().getGuiFrame(); + World.getGlobalWorld().getInputHandler().addFieldClickListener( + e -> whenGameIsRunning(() -> addInput(e.getField().getX())) + ); + World.getGlobalWorld().getInputHandler().addFieldHoverListener(e -> whenGameIsRunning(() -> { + // deselect last hovered field, if any + if (e.getPreviousField() != null) { + setColumnColor(e.getPreviousField().getX(), () -> null); + } + if (rowSelectMode.get()) { + // select current hovered field + if (e.getField() != null) { + setColumnColor( + e.getField().getX(), + () -> guiPanel.isDarkMode() + ? Color.yellow + : Color.orange + ); + } + } + })); + statusLabel.setFont(statusLabel.getFont().deriveFont(guiPanel.scale(20.0f))); + guiFrame.add(statusLabel, BorderLayout.NORTH); + guiFrame.pack(); + guiPanel.addDarkModeChangeListener(this::onDarkModeChange); + guiPanel.addComponentListener(new ComponentAdapter() { + @Override + public void componentResized(final ComponentEvent e) { + statusLabel.setFont( + statusLabel.getFont().deriveFont( + Math.max(20f, 0.04f * Math.min(guiPanel.getWidth(), guiPanel.getHeight())) + ) + ); + } + }); + // trigger dark mode change to set the correct color + guiPanel.setDarkMode(World.getGlobalWorld().getGuiPanel().isDarkMode()); + } + + /** + * Called when the dark mode changes. + * + * @param e the property change event + */ + @SuppressWarnings("UnstableApiUsage") + public void onDarkModeChange(final PropertyChangeEvent e) { + final var darkMode = (boolean) e.getNewValue(); + statusLabel.setForeground(darkMode ? Color.white : Color.black); + World.getGlobalWorld().getGuiFrame().getContentPane().setBackground(darkMode ? Color.black : Color.white); + } + + /** + * Adds an input to the input queue. When {@link #getNextInput(RobotFamily, RobotFamily[][])} is called, the program + * will wait until this method is called. + * + * @param input the input to add + */ + public void addInput(final int input) { + inputQueue.add(input); + } + + /** + * Returns the next input from the input queue. If the input is invalid, the user will be prompted to enter a new + * input. The program will halt until a valid input is entered. + * + * @param currentPlayer the current player + * @param stones the current state of the game board + * @return the next input from the input queue + */ + public int getNextInput(final RobotFamily currentPlayer, final RobotFamily[][] stones) { + rowSelectMode.set(true); + statusLabel.setText( + "Click on a column to insert a disc.
Current Player: %s".formatted(currentPlayer.getName()) + ); + try { + final int input = inputQueue.take(); + System.out.println("Received column input: " + input); + if (!FourWins.validateInput(input, stones)) { + System.out.println("Invalid column input, please try again."); + return getNextInput(currentPlayer, stones); + } + rowSelectMode.set(false); + return input; + } catch (final InterruptedException e) { + rowSelectMode.set(false); + throw new RuntimeException(e); + } + } + + /** + * Sets a status message, saying that the game has ended in a draw. + */ + public void displayDrawStatus() { + statusLabel.setText("No valid columns found.
Hence, game ends with a draw."); + } + + /** + * Sets a status message, saying that the game has ended with a winner. + * + * @param winner the winner of the game + */ + public void displayWinnerStatus(final RobotFamily winner) { + statusLabel.setText("Player %s has won the game!".formatted(winner.getName())); + } + + /** + * Returns the {@link #statusLabel} of this {@link InputHandler}. + * + *

Use the {@link JLabel#getText()} method to get the current text of the label, and the + * {@link JLabel#setText(String)} method to update the text. + * + * @return the {@link #statusLabel} of this {@link InputHandler} + */ + public JLabel getStatusLabel() { + return statusLabel; + } +} diff --git a/solution/H02/src/main/resources/h02.properties b/solution/H02/src/main/resources/h02.properties new file mode 100644 index 0000000..58ef558 --- /dev/null +++ b/solution/H02/src/main/resources/h02.properties @@ -0,0 +1,2 @@ +FW_WORLD_WIDTH = 7 +FW_WORLD_HEIGHT = 6 diff --git a/solution/H02/src/test/java/h02/ExampleJUnitTest.java b/solution/H02/src/test/java/h02/ExampleJUnitTest.java new file mode 100644 index 0000000..6e4a903 --- /dev/null +++ b/solution/H02/src/test/java/h02/ExampleJUnitTest.java @@ -0,0 +1,16 @@ +package h02; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * An example JUnit test class. + */ +public class ExampleJUnitTest { + + @Test + public void testAddition() { + assertEquals(2, 1 + 1); + } +} diff --git a/solution/H02/version b/solution/H02/version new file mode 100644 index 0000000..b694fe3 --- /dev/null +++ b/solution/H02/version @@ -0,0 +1 @@ +0.1.0-SNAPSHOT