diff --git a/config.json b/config.json index 11141ab..a8cec80 100644 --- a/config.json +++ b/config.json @@ -768,6 +768,14 @@ "prerequisites": [], "difficulty": 5 }, + { + "slug": "prism", + "name": "Prism", + "uuid": "0067850a-7008-4b67-b3f2-e795d00dea89", + "practices": [], + "prerequisites": [], + "difficulty": 5 + }, { "slug": "run-length-encoding", "name": "Run-Length Encoding", diff --git a/exercises/practice/prism/.docs/instructions.md b/exercises/practice/prism/.docs/instructions.md new file mode 100644 index 0000000..a68c80d --- /dev/null +++ b/exercises/practice/prism/.docs/instructions.md @@ -0,0 +1,36 @@ +# 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": 3, "x": 30, "y": 10, "angle": 45 }, + { "id": 1, "x": 10, "y": 10, "angle": -90 }, + { "id": 2, "x": 10, "y": 0, "angle": 90 }, + { "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. diff --git a/exercises/practice/prism/.docs/introduction.md b/exercises/practice/prism/.docs/introduction.md new file mode 100644 index 0000000..bfa7ed7 --- /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 0000000..b1cc141 --- /dev/null +++ b/exercises/practice/prism/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "BNAndras" + ], + "files": { + "solution": [ + "prism.coffee" + ], + "test": [ + "prism.spec.coffee" + ], + "example": [ + ".meta/example.coffee" + ] + }, + "blurb": "Calculate the path of a laser through reflective prisms.", + "source": "FraSanga", + "source_url": "https://github.com/exercism/problem-specifications/pull/2625" +} \ No newline at end of file diff --git a/exercises/practice/prism/.meta/example.coffee b/exercises/practice/prism/.meta/example.coffee new file mode 100644 index 0000000..40a4ad1 --- /dev/null +++ b/exercises/practice/prism/.meta/example.coffee @@ -0,0 +1,43 @@ +class Prism + @findSequence: (start, prisms) -> + {x, y, angle} = start + sequence = [] + + loop + rad = angle * Math.PI / 180 + dirX = Math.cos rad + dirY = Math.sin rad + + # Find the nearest prism along the ray + nearest = null + nearestDist = Infinity + + for prism in prisms + dx = prism.x - x + dy = prism.y - y + + # how far along the ray is the prism? + dist = dx * dirX + dy * dirY + # ignore prisms behind us + continue unless dist > 1e-6 + + # how far off center is the prism? + crossSq = (dx - dist * dirX) ** 2 + (dy - dist * dirY) ** 2 + + # Bail if outside relative tolerance (more wiggle room for further prisms) + continue unless crossSq < 1e-6 * (Math.max 1, dist * dist) + + if dist < nearestDist + nearestDist = dist + nearest = prism + + break unless nearest + + sequence.push nearest.id + x = nearest.x + y = nearest.y + angle = (angle + nearest.angle) % 360 + + sequence: sequence + +module.exports = Prism diff --git a/exercises/practice/prism/.meta/tests.toml b/exercises/practice/prism/.meta/tests.toml new file mode 100644 index 0000000..b002223 --- /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/prism.coffee b/exercises/practice/prism/prism.coffee new file mode 100644 index 0000000..8e7ac93 --- /dev/null +++ b/exercises/practice/prism/prism.coffee @@ -0,0 +1,4 @@ +class Prism + @findSequence: (start, prisms) -> + +module.exports = Prism diff --git a/exercises/practice/prism/prism.spec.coffee b/exercises/practice/prism/prism.spec.coffee new file mode 100644 index 0000000..b395a42 --- /dev/null +++ b/exercises/practice/prism/prism.spec.coffee @@ -0,0 +1,242 @@ +Prism = require './prism' + +describe 'Prism', -> + it 'zero prisms', -> + start = { x: 0, y: 0, angle: 0 } + prisms = [] + result = Prism.findSequence(start, prisms) + expect(result.sequence).toEqual [] + + xit 'one prism one hit', -> + start = { x: 0, y: 0, angle: 0 } + prisms = [{ id: 1, x: 10, y: 0, angle: 0 }] + result = Prism.findSequence(start, prisms) + expect(result.sequence).toEqual [1] + + xit 'one prism zero hits', -> + start = { x: 0, y: 0, angle: 0 } + prisms = [{ id: 1, x: -10, y: 0, angle: 0 }] + result = Prism.findSequence(start, prisms) + expect(result.sequence).toEqual [] + + xit 'going up zero hits', -> + start = { x: 0, y: 0, angle: 90 } + prisms = [ + { id: 3, x: 0, y: -10, angle: 0 } + { id: 1, x: -10, y: 0, angle: 0 } + { id: 2, x: 10, y: 0, angle: 0 } + ] + result = Prism.findSequence(start, prisms) + expect(result.sequence).toEqual [] + + xit 'going down zero hits', -> + start = { x: 0, y: 0, angle: -90 } + prisms = [ + { id: 1, x: 10, y: 0, angle: 0 } + { id: 2, x: 0, y: 10, angle: 0 } + { id: 3, x: -10, y: 0, angle: 0 } + ] + result = Prism.findSequence(start, prisms) + expect(result.sequence).toEqual [] + + xit 'going left zero hits', -> + start = { x: 0, y: 0, angle: 180 } + prisms = [ + { id: 2, x: 0, y: 10, angle: 0 } + { id: 3, x: 10, y: 0, angle: 0 } + { id: 1, x: 0, y: -10, angle: 0 } + ] + result = Prism.findSequence(start, prisms) + expect(result.sequence).toEqual [] + + xit 'negative angle', -> + start = { x: 0, y: 0, angle: -180 } + prisms = [ + { id: 1, x: 0, y: -10, angle: 0 } + { id: 2, x: 0, y: 10, angle: 0 } + { id: 3, x: 10, y: 0, angle: 0 } + ] + result = Prism.findSequence(start, prisms) + expect(result.sequence).toEqual [] + + xit 'large angle', -> + start = { x: 0, y: 0, angle: 2340 } + prisms = [{ id: 1, x: 10, y: 0, angle: 0 }] + result = Prism.findSequence(start, prisms) + expect(result.sequence).toEqual [] + + xit 'upward refraction two hits', -> + start = { x: 0, y: 0, angle: 0 } + prisms = [ + { id: 1, x: 10, y: 10, angle: 0 } + { id: 2, x: 10, y: 0, angle: 90 } + ] + result = Prism.findSequence(start, prisms) + expect(result.sequence).toEqual [2, 1] + + xit 'downward refraction two hits', -> + start = { x: 0, y: 0, angle: 0 } + prisms = [ + { id: 1, x: 10, y: 0, angle: -90 } + { id: 2, x: 10, y: -10, angle: 0 } + ] + result = Prism.findSequence(start, prisms) + expect(result.sequence).toEqual [1, 2] + + xit 'same prism twice', -> + start = { x: 0, y: 0, angle: 0 } + prisms = [ + { id: 2, x: 10, y: 0, angle: 0 } + { id: 1, x: 20, y: 0, angle: -180 } + ] + result = Prism.findSequence(start, prisms) + expect(result.sequence).toEqual [2, 1, 2] + + xit 'simple path', -> + start = { x: 0, y: 0, angle: 0 } + prisms = [ + { id: 3, x: 30, y: 10, angle: 45 } + { id: 1, x: 10, y: 10, angle: -90 } + { id: 2, x: 10, y: 0, angle: 90 } + { id: 4, x: 20, y: 0, angle: 0 } + ] + result = Prism.findSequence(start, prisms) + expect(result.sequence).toEqual [2, 1, 3] + + xit 'multiple prisms floating point precision', -> + start = { x: 0, y: 0, angle: -6.429 } + prisms = [ + { id: 26, x: 5.8, y: 73.4, angle: 6.555 } + { id: 24, x: 36.2, y: 65.2, angle: -0.304 } + { id: 20, x: 20.4, y: 82.8, angle: 45.17 } + { id: 31, x: -20.2, y: 48.8, angle: 30.615 } + { id: 30, x: 24.0, y: 0.6, angle: 28.771 } + { id: 29, x: 31.4, y: 79.4, angle: 61.327 } + { id: 28, x: 36.4, y: 31.4, angle: -18.157 } + { id: 22, x: 47.0, y: 57.8, angle: 54.745 } + { id: 38, x: 36.4, y: 79.2, angle: 49.05 } + { id: 10, x: 37.8, y: 55.2, angle: 11.978 } + { id: 18, x: -26.0, y: 42.6, angle: 22.661 } + { id: 25, x: 38.8, y: 76.2, angle: 51.958 } + { id: 2, x: 0.0, y: 42.4, angle: -21.817 } + { id: 35, x: 21.4, y: 44.8, angle: -171.579 } + { id: 7, x: 14.2, y: -1.6, angle: 19.081 } + { id: 33, x: 11.2, y: 44.4, angle: -165.941 } + { id: 11, x: 15.4, y: 82.6, angle: 66.262 } + { id: 16, x: 30.8, y: 6.6, angle: 35.852 } + { id: 15, x: -3.0, y: 79.2, angle: 53.782 } + { id: 4, x: 29.0, y: 75.4, angle: 17.016 } + { id: 23, x: 41.6, y: 59.8, angle: 70.763 } + { id: 8, x: -10.0, y: 15.8, angle: -9.24 } + { id: 13, x: 48.6, y: 51.8, angle: 45.812 } + { id: 1, x: 13.2, y: 77.0, angle: 17.937 } + { id: 34, x: -8.8, y: 36.8, angle: -4.199 } + { id: 21, x: 24.4, y: 75.8, angle: 20.783 } + { id: 17, x: -4.4, y: 74.6, angle: 24.709 } + { id: 9, x: 30.8, y: 41.8, angle: -165.413 } + { id: 32, x: 4.2, y: 78.6, angle: 40.892 } + { id: 37, x: -15.8, y: 47.0, angle: 33.29 } + { id: 6, x: 1.0, y: 80.6, angle: 51.295 } + { id: 36, x: -27.0, y: 47.8, angle: 92.52 } + { id: 14, x: -2.0, y: 34.4, angle: -52.001 } + { id: 5, x: 23.2, y: 80.2, angle: 31.866 } + { id: 27, x: -5.6, y: 32.8, angle: -75.303 } + { id: 12, x: -1.0, y: 0.2, angle: 0.0 } + { id: 3, x: -6.6, y: 3.2, angle: 46.72 } + { id: 19, x: -13.8, y: 24.2, angle: -9.205 } + ] + result = Prism.findSequence(start, prisms) + expect(result.sequence).toEqual [ + 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 + ] + + xit 'complex path with multiple prisms floating point precision', -> + start = { x: 0, y: 0, angle: 0.0 } + prisms = [ + { id: 46, x: 37.4, y: 20.6, angle: -88.332 } + { id: 72, x: -24.2, y: 23.4, angle: -90.774 } + { id: 25, x: 78.6, y: 7.8, angle: 98.562 } + { id: 60, x: -58.8, y: 31.6, angle: 115.56 } + { id: 22, x: 75.2, y: 28.0, angle: 63.515 } + { id: 2, x: 89.8, y: 27.8, angle: 91.176 } + { id: 23, x: 9.8, y: 30.8, angle: 30.829 } + { id: 69, x: 22.8, y: 20.6, angle: -88.315 } + { id: 44, x: -0.8, y: 15.6, angle: -116.565 } + { id: 36, x: -24.2, y: 8.2, angle: -90.0 } + { id: 53, x: -1.2, y: 0.0, angle: 0.0 } + { id: 52, x: 14.2, y: 24.0, angle: -143.896 } + { id: 5, x: -65.2, y: 21.6, angle: 93.128 } + { id: 66, x: 5.4, y: 15.6, angle: 31.608 } + { id: 51, x: -72.6, y: 21.0, angle: -100.976 } + { id: 65, x: 48.0, y: 10.2, angle: 87.455 } + { id: 21, x: -41.8, y: 0.0, angle: 68.352 } + { id: 18, x: -46.2, y: 19.2, angle: -128.362 } + { id: 10, x: 74.4, y: 0.4, angle: 90.939 } + { id: 15, x: 67.6, y: 0.4, angle: 84.958 } + { id: 35, x: 14.8, y: -0.4, angle: 89.176 } + { id: 1, x: 83.0, y: 0.2, angle: 89.105 } + { id: 68, x: 14.6, y: 28.0, angle: -29.867 } + { id: 67, x: 79.8, y: 18.6, angle: -136.643 } + { id: 38, x: 53.0, y: 14.6, angle: -90.848 } + { id: 31, x: -58.0, y: 6.6, angle: -61.837 } + { id: 74, x: -30.8, y: 0.4, angle: 85.966 } + { id: 48, x: -4.6, y: 10.0, angle: -161.222 } + { id: 12, x: 59.0, y: 5.0, angle: -91.164 } + { id: 33, x: -16.4, y: 18.4, angle: 90.734 } + { id: 4, x: 82.6, y: 27.6, angle: 71.127 } + { id: 75, x: -10.2, y: 30.6, angle: -1.108 } + { id: 28, x: 38.0, y: 0.0, angle: 86.863 } + { id: 11, x: 64.4, y: -0.2, angle: 92.353 } + { id: 9, x: -51.4, y: 31.6, angle: 67.249 } + { id: 26, x: -39.8, y: 30.8, angle: 61.113 } + { id: 30, x: -34.2, y: 0.6, angle: 111.33 } + { id: 56, x: -51.0, y: 0.2, angle: 70.445 } + { id: 41, x: -12.0, y: 0.0, angle: 91.219 } + { id: 24, x: 63.8, y: 14.4, angle: 86.586 } + { id: 70, x: -72.8, y: 13.4, angle: -87.238 } + { id: 3, x: 22.4, y: 7.0, angle: -91.685 } + { id: 13, x: 34.4, y: 7.0, angle: 90.0 } + { id: 16, x: -47.4, y: 11.4, angle: -136.02 } + { id: 6, x: 90.0, y: 0.2, angle: 90.415 } + { id: 54, x: 44.0, y: 27.8, angle: 85.969 } + { id: 32, x: -9.0, y: 0.0, angle: 91.615 } + { id: 8, x: -31.6, y: 30.8, angle: 0.535 } + { id: 39, x: -12.0, y: 8.2, angle: 90.0 } + { id: 14, x: -79.6, y: 32.4, angle: 92.342 } + { id: 42, x: 65.8, y: 20.8, angle: -85.867 } + { id: 40, x: -65.0, y: 14.0, angle: 87.109 } + { id: 45, x: 10.6, y: 18.8, angle: 23.697 } + { id: 71, x: -24.2, y: 18.6, angle: -88.531 } + { id: 7, x: -72.6, y: 6.4, angle: -89.148 } + { id: 62, x: -32.0, y: 24.8, angle: -140.8 } + { id: 49, x: 34.4, y: -0.2, angle: 89.415 } + { id: 63, x: 74.2, y: 12.6, angle: -138.429 } + { id: 59, x: 82.8, y: 13.0, angle: -140.177 } + { id: 34, x: -9.4, y: 23.2, angle: -88.238 } + { id: 76, x: -57.6, y: 0.0, angle: 1.2 } + { id: 43, x: 7.0, y: 0.0, angle: 116.565 } + { id: 20, x: 45.8, y: -0.2, angle: 1.469 } + { id: 37, x: -16.6, y: 13.2, angle: 84.785 } + { id: 58, x: -79.0, y: -0.2, angle: 89.481 } + { id: 50, x: -24.2, y: 12.8, angle: -86.987 } + { id: 64, x: 59.2, y: 10.2, angle: -92.203 } + { id: 61, x: -72.0, y: 26.4, angle: -83.66 } + { id: 47, x: 45.4, y: 5.8, angle: -82.992 } + { id: 17, x: -52.2, y: 17.8, angle: -52.938 } + { id: 57, x: -61.8, y: 32.0, angle: 84.627 } + { id: 29, x: 47.2, y: 28.2, angle: 92.954 } + { id: 27, x: -4.6, y: 0.2, angle: 87.397 } + { id: 55, x: -61.4, y: 26.4, angle: 94.086 } + { id: 73, x: -40.4, y: 13.4, angle: -62.229 } + { id: 19, x: 53.2, y: 20.6, angle: -87.181 } + ] + result = Prism.findSequence(start, prisms) + expect(result.sequence).toEqual [ + 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 + ]