diff --git a/config.json b/config.json index d85ddef5f..a5b4ecfa3 100644 --- a/config.json +++ b/config.json @@ -799,6 +799,16 @@ ], "difficulty": 4 }, + { + "slug": "prism", + "name": "Prism", + "uuid": "1fca8759-0236-493c-a4ef-2807cb33fd2b", + "practices": [], + "prerequisites": [ + "lists" + ], + "difficulty": 4 + }, { "slug": "proverb", "name": "Proverb", diff --git a/exercises/practice/prism/.docs/instructions.md b/exercises/practice/prism/.docs/instructions.md new file mode 100644 index 000000000..13cefae8c --- /dev/null +++ b/exercises/practice/prism/.docs/instructions.md @@ -0,0 +1,38 @@ +# Instructions + +Before activating the laser array, you must predict the exact order in which crystals will be hit, identified by their sample IDs. + +## Example Test Case + +Consider this crystal array configuration: + +```json +{ + "start": { "x": 0, "y": 0, "angle": 0 }, + "prisms": [ + { "id": 1, "x": 10, "y": 10, "angle": -90 }, + { "id": 2, "x": 10, "y": 0, "angle": 90 }, + { "id": 3, "x": 30, "y": 10, "angle": 45 }, + { "id": 4, "x": 20, "y": 0, "angle": 0 } + ] +} +``` + +## What's Happening + +The laser starts at the origin `(0, 0)` and fires horizontally to the right at angle 0°. +Here's the step-by-step beam path: + +**Step 1**: The beam travels along the x-axis (y = 0) and first encounters **Crystal #2** at position `(10, 0)`. +This crystal has a refraction angle of 90°, which means it bends the beam perpendicular to its current path. +The beam, originally traveling at 0°, is now redirected to 90° (straight up). + +**Step 2**: The beam now travels vertically upward from position `(10, 0)` and strikes **Crystal #1** at position `(10, 10)`. +This crystal has a refraction angle of -90°, bending the beam by -90° relative to its current direction. +The beam was traveling at 90°, so after refraction it's now at 0° (90° + (-90°) = 0°), traveling horizontally to the right again. + +**Step 3**: From position `(10, 10)`, the beam travels horizontally and encounters **Crystal #3** at position `(30, 10)`. +This crystal refracts the beam by 45°, changing its direction to 45°. +The beam continues into empty space beyond the array. + +!["A graph showing the path of a laser beam refracted through three prisms."](https://assets.exercism.org/images/exercises/prism/laser_path-light.svg) diff --git a/exercises/practice/prism/.docs/introduction.md b/exercises/practice/prism/.docs/introduction.md new file mode 100644 index 000000000..bfa7ed72e --- /dev/null +++ b/exercises/practice/prism/.docs/introduction.md @@ -0,0 +1,5 @@ +# Introduction + +You're a researcher at **PRISM** (Precariously Redirected Illumination Safety Management), working with a precision laser calibration system that tests experimental crystal prisms. +These crystals are being developed for next-generation optical computers, and each one has unique refractive properties based on its molecular structure. +The lab's laser system can damage crystals if they receive unexpected illumination, so precise path prediction is critical. diff --git a/exercises/practice/prism/.meta/config.json b/exercises/practice/prism/.meta/config.json new file mode 100644 index 000000000..80bee3ce1 --- /dev/null +++ b/exercises/practice/prism/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "kahgoh" + ], + "files": { + "solution": [ + "src/main/java/Prism.java" + ], + "test": [ + "src/test/java/PrismTest.java" + ], + "example": [ + ".meta/src/reference/java/Prism.java" + ] + }, + "blurb": "Calculate the path of a laser through refractive prisms.", + "source": "FraSanga", + "source_url": "https://github.com/exercism/problem-specifications/pull/2625" +} diff --git a/exercises/practice/prism/.meta/src/reference/java/Prism.java b/exercises/practice/prism/.meta/src/reference/java/Prism.java new file mode 100644 index 000000000..5be2d192e --- /dev/null +++ b/exercises/practice/prism/.meta/src/reference/java/Prism.java @@ -0,0 +1,88 @@ +import java.util.*; +import java.util.function.Predicate; + +public class Prism { + + public record LaserInfo(double x, double y, double angle, Integer prismId) { + public LaserInfo(double x, double y, double angle) { + this(x, y, angle, null); + } + } + + public record PrismInfo(int id, double x, double y, double angle) { + } + + private static final int DECIMAL_PLACES = 3; + + private static final double ROUND_FACTOR = Math.pow(10, DECIMAL_PLACES); + + public static List findSequence(LaserInfo laser, List prisms) { + LaserInfo last = laser; + Optional lastPrism = Optional.empty(); + List sequence = new ArrayList<>(); + + do { + lastPrism = prisms.stream().filter(new TouchesPrism(last)).min(new CompareDistance(last)); + if (lastPrism.isPresent()) { + PrismInfo nextPrism = lastPrism.get(); + sequence.add(nextPrism.id); + last = new LaserInfo(nextPrism.x, nextPrism.y, + normalizeDegrees(nextPrism.angle + last.angle), nextPrism.id); + } + } while (lastPrism.isPresent()); + return sequence; + } + + private static double normalizeDegrees(double degrees) { + if (degrees < 0) { + return (degrees % 360) + 360; + } + return degrees % 360; + } + + private static class CompareDistance implements Comparator { + private final LaserInfo laser; + + public CompareDistance(LaserInfo laser) { + this.laser = laser; + } + + @Override + public int compare(PrismInfo o1, PrismInfo o2) { + final double d1 = Math.hypot(o1.x - laser.x, o1.y - laser.y); + final double d2 = Math.hypot(o2.x - laser.x, o2.y - laser.y); + return Double.compare(d1, d2); + } + } + + private static class TouchesPrism implements Predicate { + private final LaserInfo laser; + private final double sinAngle; + private final double cosAngle; + + public TouchesPrism(LaserInfo laser) { + this.laser = laser; + + double angleRadians = Math.toRadians(laser.angle); + this.sinAngle = Math.sin(angleRadians); + this.cosAngle = Math.cos(angleRadians); + } + + @Override + public boolean test(PrismInfo prism) { + if (laser.prismId != null && laser.prismId == prism.id) { + return false; + } + + double dx = prism.x - laser.x; + double dy = prism.y - laser.y; + double hyp = Math.hypot(dx, dy); + + return isClose(hyp * cosAngle, dx) && isClose(hyp * sinAngle, dy); + } + } + + private static boolean isClose(double a, double b) { + return Math.abs(Math.round(a * ROUND_FACTOR - b * ROUND_FACTOR)) <= 1; + } +} diff --git a/exercises/practice/prism/.meta/tests.toml b/exercises/practice/prism/.meta/tests.toml new file mode 100644 index 000000000..b00222383 --- /dev/null +++ b/exercises/practice/prism/.meta/tests.toml @@ -0,0 +1,52 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[ec65d3b3-f7bf-4015-8156-0609c141c4c4] +description = "zero prisms" + +[ec0ca17c-0c5f-44fb-89ba-b76395bdaf1c] +description = "one prism one hit" + +[0db955f2-0a27-4c82-ba67-197bd6202069] +description = "one prism zero hits" + +[8d92485b-ebc0-4ee9-9b88-cdddb16b52da] +description = "going up zero hits" + +[78295b3c-7438-492d-8010-9c63f5c223d7] +description = "going down zero hits" + +[acc723ea-597b-4a50-8d1b-b980fe867d4c] +description = "going left zero hits" + +[3f19b9df-9eaa-4f18-a2db-76132f466d17] +description = "negative angle" + +[96dacffb-d821-4cdf-aed8-f152ce063195] +description = "large angle" + +[513a7caa-957f-4c5d-9820-076842de113c] +description = "upward refraction two hits" + +[d452b7c7-9761-4ea9-81a9-2de1d73eb9ef] +description = "downward refraction two hits" + +[be1a2167-bf4c-4834-acc9-e4d68e1a0203] +description = "same prism twice" + +[df5a60dd-7c7d-4937-ac4f-c832dae79e2e] +description = "simple path" + +[8d9a3cc8-e846-4a3b-a137-4bfc4aa70bd1] +description = "multiple prisms floating point precision" + +[e077fc91-4e4a-46b3-a0f5-0ba00321da56] +description = "complex path with multiple prisms floating point precision" diff --git a/exercises/practice/prism/build.gradle b/exercises/practice/prism/build.gradle new file mode 100644 index 000000000..d28f35dee --- /dev/null +++ b/exercises/practice/prism/build.gradle @@ -0,0 +1,25 @@ +plugins { + id "java" +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation platform("org.junit:junit-bom:5.10.0") + testImplementation "org.junit.jupiter:junit-jupiter" + testImplementation "org.assertj:assertj-core:3.25.1" + + testRuntimeOnly "org.junit.platform:junit-platform-launcher" +} + +test { + useJUnitPlatform() + + testLogging { + exceptionFormat = "full" + showStandardStreams = true + events = ["passed", "failed", "skipped"] + } +} diff --git a/exercises/practice/prism/gradle/wrapper/gradle-wrapper.jar b/exercises/practice/prism/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..61285a659 Binary files /dev/null and b/exercises/practice/prism/gradle/wrapper/gradle-wrapper.jar differ diff --git a/exercises/practice/prism/gradle/wrapper/gradle-wrapper.properties b/exercises/practice/prism/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..8d9046d01 --- /dev/null +++ b/exercises/practice/prism/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/exercises/practice/prism/gradlew b/exercises/practice/prism/gradlew new file mode 100755 index 000000000..adff685a0 --- /dev/null +++ b/exercises/practice/prism/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/exercises/practice/prism/gradlew.bat b/exercises/practice/prism/gradlew.bat new file mode 100644 index 000000000..c4bdd3ab8 --- /dev/null +++ b/exercises/practice/prism/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/exercises/practice/prism/src/main/java/Prism.java b/exercises/practice/prism/src/main/java/Prism.java new file mode 100644 index 000000000..b02fe3fae --- /dev/null +++ b/exercises/practice/prism/src/main/java/Prism.java @@ -0,0 +1,15 @@ +import java.util.*; +import java.util.function.Predicate; + +public class Prism { + + public record LaserInfo(double x, double y, double angle) { + } + + public record PrismInfo(int id, double x, double y, double angle) { + } + + public static List findSequence(LaserInfo laser, List prisms) { + throw new UnsupportedOperationException("Delete this statement and write your own implementation."); + } +} \ No newline at end of file diff --git a/exercises/practice/prism/src/test/java/PrismTest.java b/exercises/practice/prism/src/test/java/PrismTest.java new file mode 100644 index 000000000..a4637c1bc --- /dev/null +++ b/exercises/practice/prism/src/test/java/PrismTest.java @@ -0,0 +1,303 @@ +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PrismTest { + @Test + @DisplayName("zero prisms") + public void testZeroPrisms() { + final Prism.LaserInfo laser = new Prism.LaserInfo(0, 0, 0); + + assertThat(Prism.findSequence(laser, List.of())).isEmpty(); + } + + @Disabled("Remove to run test") + @Test + @DisplayName("one prism one hit") + public void testOnePrismOneHit() { + final Prism.LaserInfo laser = new Prism.LaserInfo(0, 0, 0); + final List prisms = List.of(new Prism.PrismInfo(1, 10, 0, 0)); + + assertThat(Prism.findSequence(laser, prisms)).containsExactly(1); + } + + @Disabled("Remove to run test") + @Test + @DisplayName("one prism hits zero") + public void testOnePrismHitsZero() { + final Prism.LaserInfo laser = new Prism.LaserInfo(0, 0, 0); + final List prisms = List.of(new Prism.PrismInfo(1, -10, 0, 0)); + + assertThat(Prism.findSequence(laser, prisms)).isEmpty(); + } + + @Disabled("Remove to run test") + @Test + @DisplayName("going up zero hits") + public void testGoingUpZeroHits() { + final Prism.LaserInfo laser = new Prism.LaserInfo(0, 0, 90); + final List prisms = List.of( + new Prism.PrismInfo(3, 0, -10, 0), + new Prism.PrismInfo(1, -10, 0, 0), + new Prism.PrismInfo(2, 10, 0, 0) + ); + + assertThat(Prism.findSequence(laser, prisms)).isEmpty(); + } + + @Disabled("Remove to run test") + @Test + @DisplayName("going down zero hits") + public void testGoingDownZeroHits() { + final Prism.LaserInfo laser = new Prism.LaserInfo(0, 0, -90); + final List prisms = List.of( + new Prism.PrismInfo(1, 10, 0, 0), + new Prism.PrismInfo(2, 0, 10, 0), + new Prism.PrismInfo(3, -10, 0, 0) + ); + + assertThat(Prism.findSequence(laser, prisms)).isEmpty(); + } + + @Disabled("Remove to run test") + @Test + @DisplayName("going left zero hits") + public void testGoingLeftZeroHits() { + final Prism.LaserInfo laser = new Prism.LaserInfo(0, 0, 180); + final List prisms = List.of( + new Prism.PrismInfo(2, 0, 10, 0), + new Prism.PrismInfo(3, 10, 0, 0), + new Prism.PrismInfo(1, 0, -10, 0) + ); + + assertThat(Prism.findSequence(laser, prisms)).isEmpty(); + } + + @Disabled("Remove to run test") + @Test + @DisplayName("negative angle") + public void testNegativeAngle() { + final Prism.LaserInfo laser = new Prism.LaserInfo(0, 0, -180); + final List prisms = List.of( + new Prism.PrismInfo(1, 0, -10, 0), + new Prism.PrismInfo(2, 0, 10, 0), + new Prism.PrismInfo(3, 10, 0, 0) + ); + + assertThat(Prism.findSequence(laser, prisms)).isEmpty(); + } + + @Disabled("Remove to run test") + @Test + @DisplayName("large angle") + public void testLargeAngle() { + final Prism.LaserInfo laser = new Prism.LaserInfo(0, 0, 2340); + final List prisms = List.of( + new Prism.PrismInfo(1, 10, 0, 0) + ); + + assertThat(Prism.findSequence(laser, prisms)).isEmpty(); + } + + @Disabled("Remove to run test") + @Test + @DisplayName("upward reflection two hits") + public void testUpwardReflectionTwoHits() { + final Prism.LaserInfo laser = new Prism.LaserInfo(0, 0, 0); + final List prisms = List.of( + new Prism.PrismInfo(1, 10, 10, 0), + new Prism.PrismInfo(2, 10, 0, 90) + ); + + assertThat(Prism.findSequence(laser, prisms)).containsExactly(2, 1); + } + + @Disabled("Remove to run test") + @Test + @DisplayName("downward reflection two hits") + public void testDownwardReflectionTwoHits() { + final Prism.LaserInfo laser = new Prism.LaserInfo(0, 0, 0); + final List prisms = List.of( + new Prism.PrismInfo(1, 10, 0, -90), + new Prism.PrismInfo(2, 10, -10, 0) + ); + + assertThat(Prism.findSequence(laser, prisms)).containsExactly(1, 2); + } + + @Disabled("Remove to run test") + @Test + @DisplayName("same prism twice") + public void testSamePrismTwice() { + final Prism.LaserInfo laser = new Prism.LaserInfo(0, 0, 0); + final List prisms = List.of( + new Prism.PrismInfo(2, 10, 0, 0), + new Prism.PrismInfo(1, 20, 0, -180) + ); + + assertThat(Prism.findSequence(laser, prisms)).containsExactly(2, 1, 2); + } + + @Disabled("Remove to run test") + @Test + @DisplayName("simple path") + public void testSimplePath() { + final Prism.LaserInfo laser = new Prism.LaserInfo(0, 0, 0); + final List prisms = List.of( + new Prism.PrismInfo(3, 30, 10, 45), + new Prism.PrismInfo(1, 10, 10, -90), + new Prism.PrismInfo(2, 10, 0, 90), + new Prism.PrismInfo(4, 20, 0, 0) + ); + + assertThat(Prism.findSequence(laser, prisms)).containsExactly(2, 1, 3); + } + + @Disabled("Remove to run test") + @Test + @DisplayName("multiple prisms floating point precision") + public void testMultiplePrismFloatingPointPrecision() { + final Prism.LaserInfo laser = new Prism.LaserInfo(0, 0, -6.429); + final List prisms = List.of( + new Prism.PrismInfo(26, 5.8, 73.4, 6.555), + new Prism.PrismInfo(24, 36.2, 65.2, -0.304), + new Prism.PrismInfo(20, 20.4, 82.8, 45.17), + new Prism.PrismInfo(31, -20.2, 48.8, 30.615), + new Prism.PrismInfo(30, 24.0, 0.6, 28.771), + new Prism.PrismInfo(29, 31.4, 79.4, 61.327), + new Prism.PrismInfo(28, 36.4, 31.4, -18.157), + new Prism.PrismInfo(22, 47.0, 57.8, 54.745), + new Prism.PrismInfo(38, 36.4, 79.2, 49.05), + new Prism.PrismInfo(10, 37.8, 55.2, 11.978), + new Prism.PrismInfo(18, -26.0, 42.6, 22.661), + new Prism.PrismInfo(25, 38.8, 76.2, 51.958), + new Prism.PrismInfo(2, 0.0, 42.4, -21.817), + new Prism.PrismInfo(35, 21.4, 44.8, -171.579), + new Prism.PrismInfo(7, 14.2, -1.6, 19.081), + new Prism.PrismInfo(33, 11.2, 44.4, -165.941), + new Prism.PrismInfo(11, 15.4, 82.6, 66.262), + new Prism.PrismInfo(16, 30.8, 6.6, 35.852), + new Prism.PrismInfo(15, -3.0, 79.2, 53.782), + new Prism.PrismInfo(4, 29.0, 75.4, 17.016), + new Prism.PrismInfo(23, 41.6, 59.8, 70.763), + new Prism.PrismInfo(8, -10.0, 15.8, -9.24), + new Prism.PrismInfo(13, 48.6, 51.8, 45.812), + new Prism.PrismInfo(1, 13.2, 77.0, 17.937), + new Prism.PrismInfo(34, -8.8, 36.8, -4.199), + new Prism.PrismInfo(21, 24.4, 75.8, 20.783), + new Prism.PrismInfo(17, -4.4, 74.6, 24.709), + new Prism.PrismInfo(9, 30.8, 41.8, -165.413), + new Prism.PrismInfo(32, 4.2, 78.6, 40.892), + new Prism.PrismInfo(37, -15.8, 47.0, 33.29), + new Prism.PrismInfo(6, 1.0, 80.6, 51.295), + new Prism.PrismInfo(36, -27.0, 47.8, 92.52), + new Prism.PrismInfo(14, -2.0, 34.4, -52.001), + new Prism.PrismInfo(5, 23.2, 80.2, 31.866), + new Prism.PrismInfo(27, -5.6, 32.8, -75.303), + new Prism.PrismInfo(12, -1.0, 0.2, 0.0), + new Prism.PrismInfo(3, -6.6, 3.2, 46.72), + new Prism.PrismInfo(19, -13.8, 24.2, -9.205) + ); + + assertThat(Prism.findSequence(laser, prisms)).containsExactly( + 7, 30, 16, 28, 13, 22, 23, 10, 9, 24, 25, 38, 29, 4, 35, 21, 5, 20, + 11, 1, 33, 26, 32, 6, 15, 17, 2, 14, 27, 34, 37, 31, 36, 18, 19, 8, 3, 12); + } + + @Disabled("Remove to run test") + @Test + @DisplayName("complex path with multiple prisms floating point precision") + public void testComplexPathWithMultiplePrismFloatingPointPrecision() { + final Prism.LaserInfo laser = new Prism.LaserInfo(0, 0, 0); + final List prisms = List.of( + new Prism.PrismInfo(46, 37.4, 20.6, -88.332), + new Prism.PrismInfo(72, -24.2, 23.4, -90.774), + new Prism.PrismInfo(25, 78.6, 7.8, 98.562), + new Prism.PrismInfo(60, -58.8, 31.6, 115.56), + new Prism.PrismInfo(22, 75.2, 28.0, 63.515), + new Prism.PrismInfo(2, 89.8, 27.8, 91.176), + new Prism.PrismInfo(23, 9.8, 30.8, 30.829), + new Prism.PrismInfo(69, 22.8, 20.6, -88.315), + new Prism.PrismInfo(44, -0.8, 15.6, -116.565), + new Prism.PrismInfo(36, -24.2, 8.2, -90.0), + new Prism.PrismInfo(53, -1.2, 0.0, 0.0), + new Prism.PrismInfo(52, 14.2, 24.0, -143.896), + new Prism.PrismInfo(5, -65.2, 21.6, 93.128), + new Prism.PrismInfo(66, 5.4, 15.6, 31.608), + new Prism.PrismInfo(51, -72.6, 21.0, -100.976), + new Prism.PrismInfo(65, 48.0, 10.2, 87.455), + new Prism.PrismInfo(21, -41.8, 0.0, 68.352), + new Prism.PrismInfo(18, -46.2, 19.2, -128.362), + new Prism.PrismInfo(10, 74.4, 0.4, 90.939), + new Prism.PrismInfo(15, 67.6, 0.4, 84.958), + new Prism.PrismInfo(35, 14.8, -0.4, 89.176), + new Prism.PrismInfo(1, 83.0, 0.2, 89.105), + new Prism.PrismInfo(68, 14.6, 28.0, -29.867), + new Prism.PrismInfo(67, 79.8, 18.6, -136.643), + new Prism.PrismInfo(38, 53.0, 14.6, -90.848), + new Prism.PrismInfo(31, -58.0, 6.6, -61.837), + new Prism.PrismInfo(74, -30.8, 0.4, 85.966), + new Prism.PrismInfo(48, -4.6, 10.0, -161.222), + new Prism.PrismInfo(12, 59.0, 5.0, -91.164), + new Prism.PrismInfo(33, -16.4, 18.4, 90.734), + new Prism.PrismInfo(4, 82.6, 27.6, 71.127), + new Prism.PrismInfo(75, -10.2, 30.6, -1.108), + new Prism.PrismInfo(28, 38.0, 0.0, 86.863), + new Prism.PrismInfo(11, 64.4, -0.2, 92.353), + new Prism.PrismInfo(9, -51.4, 31.6, 67.249), + new Prism.PrismInfo(26, -39.8, 30.8, 61.113), + new Prism.PrismInfo(30, -34.2, 0.6, 111.33), + new Prism.PrismInfo(56, -51.0, 0.2, 70.445), + new Prism.PrismInfo(41, -12.0, 0.0, 91.219), + new Prism.PrismInfo(24, 63.8, 14.4, 86.586), + new Prism.PrismInfo(70, -72.8, 13.4, -87.238), + new Prism.PrismInfo(3, 22.4, 7.0, -91.685), + new Prism.PrismInfo(13, 34.4, 7.0, 90.0), + new Prism.PrismInfo(16, -47.4, 11.4, -136.02), + new Prism.PrismInfo(6, 90.0, 0.2, 90.415), + new Prism.PrismInfo(54, 44.0, 27.8, 85.969), + new Prism.PrismInfo(32, -9.0, 0.0, 91.615), + new Prism.PrismInfo(8, -31.6, 30.8, 0.535), + new Prism.PrismInfo(39, -12.0, 8.2, 90.0), + new Prism.PrismInfo(14, -79.6, 32.4, 92.342), + new Prism.PrismInfo(42, 65.8, 20.8, -85.867), + new Prism.PrismInfo(40, -65.0, 14.0, 87.109), + new Prism.PrismInfo(45, 10.6, 18.8, 23.697), + new Prism.PrismInfo(71, -24.2, 18.6, -88.531), + new Prism.PrismInfo(7, -72.6, 6.4, -89.148), + new Prism.PrismInfo(62, -32.0, 24.8, -140.8), + new Prism.PrismInfo(49, 34.4, -0.2, 89.415), + new Prism.PrismInfo(63, 74.2, 12.6, -138.429), + new Prism.PrismInfo(59, 82.8, 13.0, -140.177), + new Prism.PrismInfo(34, -9.4, 23.2, -88.238), + new Prism.PrismInfo(76, -57.6, 0.0, 1.2), + new Prism.PrismInfo(43, 7.0, 0.0, 116.565), + new Prism.PrismInfo(20, 45.8, -0.2, 1.469), + new Prism.PrismInfo(37, -16.6, 13.2, 84.785), + new Prism.PrismInfo(58, -79.0, -0.2, 89.481), + new Prism.PrismInfo(50, -24.2, 12.8, -86.987), + new Prism.PrismInfo(64, 59.2, 10.2, -92.203), + new Prism.PrismInfo(61, -72.0, 26.4, -83.66), + new Prism.PrismInfo(47, 45.4, 5.8, -82.992), + new Prism.PrismInfo(17, -52.2, 17.8, -52.938), + new Prism.PrismInfo(57, -61.8, 32.0, 84.627), + new Prism.PrismInfo(29, 47.2, 28.2, 92.954), + new Prism.PrismInfo(27, -4.6, 0.2, 87.397), + new Prism.PrismInfo(55, -61.4, 26.4, 94.086), + new Prism.PrismInfo(73, -40.4, 13.4, -62.229), + new Prism.PrismInfo(19, 53.2, 20.6, -87.181) + ); + + assertThat(Prism.findSequence(laser, prisms)).containsExactly( + 43, 44, 66, 45, 52, 35, 49, 13, 3, 69, 46, 28, 20, 11, 24, 38, 19, 42, + 15, 10, 63, 25, 59, 1, 6, 2, 4, 67, 22, 29, 65, 64, 12, 47, 54, 68, + 23, 75, 8, 26, 18, 9, 60, 17, 31, 7, 70, 40, 5, 51, 61, 55, 57, 14, + 58, 76, 56, 16, 21, 30, 73, 62, 74, 41, 39, 36, 50, 37, 33, 71, 72, + 34, 32, 27, 48, 53 + ); + } +} diff --git a/exercises/settings.gradle b/exercises/settings.gradle index 30b009906..28f6eca2d 100644 --- a/exercises/settings.gradle +++ b/exercises/settings.gradle @@ -111,6 +111,7 @@ include 'practice:phone-number' include 'practice:piecing-it-together' include 'practice:pig-latin' include 'practice:poker' +include 'practice:prism' include 'practice:eliuds-eggs' include 'practice:pov' include 'practice:prime-factors'