Skip to content

Commit f355e2c

Browse files
committed
Add Prism exercise
1 parent 552bda8 commit f355e2c

14 files changed

Lines changed: 903 additions & 0 deletions

File tree

config.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,16 @@
799799
],
800800
"difficulty": 4
801801
},
802+
{
803+
"slug": "prism",
804+
"name": "Prism",
805+
"uuid": "1fca8759-0236-493c-a4ef-2807cb33fd2b",
806+
"practices": [],
807+
"prerequisites": [
808+
"lists"
809+
],
810+
"difficulty": 4
811+
},
802812
{
803813
"slug": "proverb",
804814
"name": "Proverb",
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Instructions
2+
3+
Before activating the laser array, you must predict the exact order in which crystals will be hit, identified by their sample IDs.
4+
5+
## Example Test Case
6+
7+
Consider this crystal array configuration:
8+
9+
```json
10+
{
11+
"start": { "x": 0, "y": 0, "angle": 0 },
12+
"prisms": [
13+
{ "id": 1, "x": 10, "y": 10, "angle": -90 },
14+
{ "id": 2, "x": 10, "y": 0, "angle": 90 },
15+
{ "id": 3, "x": 30, "y": 10, "angle": 45 },
16+
{ "id": 4, "x": 20, "y": 0, "angle": 0 }
17+
]
18+
}
19+
```
20+
21+
## What's Happening
22+
23+
The laser starts at the origin `(0, 0)` and fires horizontally to the right at angle 0°.
24+
Here's the step-by-step beam path:
25+
26+
**Step 1**: The beam travels along the x-axis (y = 0) and first encounters **Crystal #2** at position `(10, 0)`.
27+
This crystal has a refraction angle of 90°, which means it bends the beam perpendicular to its current path.
28+
The beam, originally traveling at 0°, is now redirected to 90° (straight up).
29+
30+
**Step 2**: The beam now travels vertically upward from position `(10, 0)` and strikes **Crystal #1** at position `(10, 10)`.
31+
This crystal has a refraction angle of -90°, bending the beam by -90° relative to its current direction.
32+
The beam was traveling at 90°, so after refraction it's now at 0° (90° + (-90°) = 0°), traveling horizontally to the right again.
33+
34+
**Step 3**: From position `(10, 10)`, the beam travels horizontally and encounters **Crystal #3** at position `(30, 10)`.
35+
This crystal refracts the beam by 45°, changing its direction to 45°.
36+
The beam continues into empty space beyond the array.
37+
38+
!["A graph showing the path of a laser beam refracted through three prisms."](https://assets.exercism.org/images/exercises/prism/laser_path-light.svg)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Introduction
2+
3+
You're a researcher at **PRISM** (Precariously Redirected Illumination Safety Management), working with a precision laser calibration system that tests experimental crystal prisms.
4+
These crystals are being developed for next-generation optical computers, and each one has unique refractive properties based on its molecular structure.
5+
The lab's laser system can damage crystals if they receive unexpected illumination, so precise path prediction is critical.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"authors": [
3+
"kahgoh"
4+
],
5+
"files": {
6+
"solution": [
7+
"src/main/java/Prism.java"
8+
],
9+
"test": [
10+
"src/test/java/PrismTest.java"
11+
],
12+
"example": [
13+
".meta/src/reference/java/Prism.java"
14+
]
15+
},
16+
"blurb": "Calculate the path of a laser through refractive prisms.",
17+
"source": "FraSanga",
18+
"source_url": "https://github.com/exercism/problem-specifications/pull/2625"
19+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import java.util.*;
2+
import java.util.function.Predicate;
3+
4+
public class Prism {
5+
6+
public record LaserInfo(double x, double y, double angle, Integer prismId) {
7+
public LaserInfo(double x, double y, double angle) {
8+
this(x, y, angle, null);
9+
}
10+
}
11+
12+
public record PrismInfo(int id, double x, double y, double angle) {
13+
}
14+
15+
private static final int DECIMAL_PLACES = 3;
16+
17+
private static final double ROUND_FACTOR = Math.pow(10, DECIMAL_PLACES);
18+
19+
public static List<Integer> findSequence(LaserInfo laser, List<PrismInfo> prisms) {
20+
LaserInfo last = laser;
21+
Optional<PrismInfo> lastPrism = Optional.empty();
22+
List<Integer> sequence = new ArrayList<>();
23+
24+
do {
25+
lastPrism = prisms.stream().filter(new TouchesPrism(last)).min(new CompareDistance(last));
26+
if (lastPrism.isPresent()) {
27+
PrismInfo nextPrism = lastPrism.get();
28+
sequence.add(nextPrism.id);
29+
last = new LaserInfo(nextPrism.x, nextPrism.y,
30+
normalizeDegrees(nextPrism.angle + last.angle), nextPrism.id);
31+
}
32+
} while (lastPrism.isPresent());
33+
return sequence;
34+
}
35+
36+
private static double normalizeDegrees(double degrees) {
37+
if (degrees < 0) {
38+
return (degrees % 360) + 360;
39+
}
40+
return degrees % 360;
41+
}
42+
43+
private static class CompareDistance implements Comparator<PrismInfo> {
44+
private final LaserInfo laser;
45+
46+
public CompareDistance(LaserInfo laser) {
47+
this.laser = laser;
48+
}
49+
50+
@Override
51+
public int compare(PrismInfo o1, PrismInfo o2) {
52+
final double d1 = Math.hypot(o1.x - laser.x, o1.y - laser.y);
53+
final double d2 = Math.hypot(o2.x - laser.x, o2.y - laser.y);
54+
return Double.compare(d1, d2);
55+
}
56+
}
57+
58+
private static class TouchesPrism implements Predicate<PrismInfo> {
59+
private final LaserInfo laser;
60+
private final double sinAngle;
61+
private final double cosAngle;
62+
63+
public TouchesPrism(LaserInfo laser) {
64+
this.laser = laser;
65+
66+
double angleRadians = Math.toRadians(laser.angle);
67+
this.sinAngle = Math.sin(angleRadians);
68+
this.cosAngle = Math.cos(angleRadians);
69+
}
70+
71+
@Override
72+
public boolean test(PrismInfo prism) {
73+
if (laser.prismId != null && laser.prismId == prism.id) {
74+
return false;
75+
}
76+
77+
double dx = prism.x - laser.x;
78+
double dy = prism.y - laser.y;
79+
double hyp = Math.hypot(dx, dy);
80+
81+
return isClose(hyp * cosAngle, dx) && isClose(hyp * sinAngle, dy);
82+
}
83+
}
84+
85+
private static boolean isClose(double a, double b) {
86+
return Math.abs(Math.round(a * ROUND_FACTOR - b * ROUND_FACTOR)) <= 1;
87+
}
88+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# This is an auto-generated file.
2+
#
3+
# Regenerating this file via `configlet sync` will:
4+
# - Recreate every `description` key/value pair
5+
# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications
6+
# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion)
7+
# - Preserve any other key/value pair
8+
#
9+
# As user-added comments (using the # character) will be removed when this file
10+
# is regenerated, comments can be added via a `comment` key.
11+
12+
[ec65d3b3-f7bf-4015-8156-0609c141c4c4]
13+
description = "zero prisms"
14+
15+
[ec0ca17c-0c5f-44fb-89ba-b76395bdaf1c]
16+
description = "one prism one hit"
17+
18+
[0db955f2-0a27-4c82-ba67-197bd6202069]
19+
description = "one prism zero hits"
20+
21+
[8d92485b-ebc0-4ee9-9b88-cdddb16b52da]
22+
description = "going up zero hits"
23+
24+
[78295b3c-7438-492d-8010-9c63f5c223d7]
25+
description = "going down zero hits"
26+
27+
[acc723ea-597b-4a50-8d1b-b980fe867d4c]
28+
description = "going left zero hits"
29+
30+
[3f19b9df-9eaa-4f18-a2db-76132f466d17]
31+
description = "negative angle"
32+
33+
[96dacffb-d821-4cdf-aed8-f152ce063195]
34+
description = "large angle"
35+
36+
[513a7caa-957f-4c5d-9820-076842de113c]
37+
description = "upward refraction two hits"
38+
39+
[d452b7c7-9761-4ea9-81a9-2de1d73eb9ef]
40+
description = "downward refraction two hits"
41+
42+
[be1a2167-bf4c-4834-acc9-e4d68e1a0203]
43+
description = "same prism twice"
44+
45+
[df5a60dd-7c7d-4937-ac4f-c832dae79e2e]
46+
description = "simple path"
47+
48+
[8d9a3cc8-e846-4a3b-a137-4bfc4aa70bd1]
49+
description = "multiple prisms floating point precision"
50+
51+
[e077fc91-4e4a-46b3-a0f5-0ba00321da56]
52+
description = "complex path with multiple prisms floating point precision"
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
plugins {
2+
id "java"
3+
}
4+
5+
repositories {
6+
mavenCentral()
7+
}
8+
9+
dependencies {
10+
testImplementation platform("org.junit:junit-bom:5.10.0")
11+
testImplementation "org.junit.jupiter:junit-jupiter"
12+
testImplementation "org.assertj:assertj-core:3.25.1"
13+
14+
testRuntimeOnly "org.junit.platform:junit-platform-launcher"
15+
}
16+
17+
test {
18+
useJUnitPlatform()
19+
20+
testLogging {
21+
exceptionFormat = "full"
22+
showStandardStreams = true
23+
events = ["passed", "failed", "skipped"]
24+
}
25+
}
45.1 KB
Binary file not shown.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
distributionBase=GRADLE_USER_HOME
2+
distributionPath=wrapper/dists
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
4+
validateDistributionUrl=true
5+
zipStoreBase=GRADLE_USER_HOME
6+
zipStorePath=wrapper/dists

0 commit comments

Comments
 (0)