diff --git a/config.json b/config.json index 8c29591..5275cc3 100644 --- a/config.json +++ b/config.json @@ -378,6 +378,14 @@ "prerequisites": [], "difficulty": 4 }, + { + "slug": "prism", + "name": "Prism", + "uuid": "a7b1dec7-9fd6-4c1b-8a55-4c3619d944d6", + "practices": [], + "prerequisites": [], + "difficulty": 4 + }, { "slug": "robot-simulator", "name": "Robot Simulator", diff --git a/exercises/practice/prism/.busted b/exercises/practice/prism/.busted new file mode 100644 index 0000000..86b84e7 --- /dev/null +++ b/exercises/practice/prism/.busted @@ -0,0 +1,5 @@ +return { + default = { + ROOT = { '.' } + } +} 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..c4057c9 --- /dev/null +++ b/exercises/practice/prism/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "BNAndras" + ], + "files": { + "solution": [ + "prism.moon" + ], + "test": [ + "prism_spec.moon" + ], + "example": [ + ".meta/example.moon" + ] + }, + "blurb": "Calculate the path of a laser through reflective prisms.", + "source": "FraSanga", + "source_url": "https://github.com/exercism/problem-specifications/pull/2625" +} diff --git a/exercises/practice/prism/.meta/example.moon b/exercises/practice/prism/.meta/example.moon new file mode 100644 index 0000000..0a4221a --- /dev/null +++ b/exercises/practice/prism/.meta/example.moon @@ -0,0 +1,40 @@ +math = require "math" + +{ + findSequence: (start, prisms) -> + { :x, :y, :angle } = start + sequence = {} + + while true + rad = angle * math.pi / 180 + dirX = math.cos rad + dirY = math.sin rad + + nearest = nil + nearestDist = math.huge + + for prism in *prisms + dx = prism.x - x + dy = prism.y - y + + dist = dx * dirX + dy * dirY + -- ignore prisms behind or at the start + if dist > 1e-6 + crossSq = (dx - dist * dirX) ^ 2 + (dy - dist * dirY) ^ 2 + + -- Bail if outside relative tolerance (more wiggle room for further prisms) + if crossSq < 1e-6 * math.max(1, dist * dist) + if dist < nearestDist + nearestDist = dist + nearest = prism + + if not nearest + break + + table.insert sequence, nearest.id + x = nearest.x + y = nearest.y + angle = (angle + nearest.angle) % 360 + + sequence +} diff --git a/exercises/practice/prism/.meta/spec_generator.moon b/exercises/practice/prism/.meta/spec_generator.moon new file mode 100644 index 0000000..7da422f --- /dev/null +++ b/exercises/practice/prism/.meta/spec_generator.moon @@ -0,0 +1,22 @@ +{ + module_name: 'Prism' + + generate_test: (case, level) -> + lines = { + "start = {x: #{case.input.start.x}, y: #{case.input.start.y}, angle: #{case.input.start.angle}}" + } + + if #case.input.prisms == 0 + table.insert lines, "prisms = {}" + else + table.insert(lines, "prisms = {") + for p in *case.input.prisms + table.insert(lines, " {id: #{p.id}, x: #{p.x}, y: #{p.y}, angle: #{p.angle}}") + table.insert(lines, "}") + + table.insert(lines, "expected = {#{table.concat(case.expected.sequence, ', ')}}") + table.insert(lines, "result = Prism.#{case.property} start, prisms") + table.insert(lines, "assert.are.same expected, result") + + table.concat [indent line, level for line in *lines], '\n' +} 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.moon b/exercises/practice/prism/prism.moon new file mode 100644 index 0000000..235ff9a --- /dev/null +++ b/exercises/practice/prism/prism.moon @@ -0,0 +1,4 @@ +{ + findSequence: (start, prisms) -> + error 'Implement me' +} diff --git a/exercises/practice/prism/prism_spec.moon b/exercises/practice/prism/prism_spec.moon new file mode 100644 index 0000000..1b5ed51 --- /dev/null +++ b/exercises/practice/prism/prism_spec.moon @@ -0,0 +1,252 @@ +Prism = require 'prism' + +describe 'prism', -> + it 'zero prisms', -> + start = {x: 0, y: 0, angle: 0} + prisms = {} + expected = {} + result = Prism.findSequence start, prisms + assert.are.same expected, result + + pending 'one prism one hit', -> + start = {x: 0, y: 0, angle: 0} + prisms = { + {id: 1, x: 10, y: 0, angle: 0} + } + expected = {1} + result = Prism.findSequence start, prisms + assert.are.same expected, result + + pending 'one prism zero hits', -> + start = {x: 0, y: 0, angle: 0} + prisms = { + {id: 1, x: -10, y: 0, angle: 0} + } + expected = {} + result = Prism.findSequence start, prisms + assert.are.same expected, result + + pending '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} + } + expected = {} + result = Prism.findSequence start, prisms + assert.are.same expected, result + + pending '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} + } + expected = {} + result = Prism.findSequence start, prisms + assert.are.same expected, result + + pending '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} + } + expected = {} + result = Prism.findSequence start, prisms + assert.are.same expected, result + + pending '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} + } + expected = {} + result = Prism.findSequence start, prisms + assert.are.same expected, result + + pending 'large angle', -> + start = {x: 0, y: 0, angle: 2340} + prisms = { + {id: 1, x: 10, y: 0, angle: 0} + } + expected = {} + result = Prism.findSequence start, prisms + assert.are.same expected, result + + pending '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} + } + expected = {2, 1} + result = Prism.findSequence start, prisms + assert.are.same expected, result + + pending '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} + } + expected = {1, 2} + result = Prism.findSequence start, prisms + assert.are.same expected, result + + pending '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} + } + expected = {2, 1, 2} + result = Prism.findSequence start, prisms + assert.are.same expected, result + + pending '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} + } + expected = {2, 1, 3} + result = Prism.findSequence start, prisms + assert.are.same expected, result + + pending '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, 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, 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, y: 42.6, angle: 22.661} + {id: 25, x: 38.8, y: 76.2, angle: 51.958} + {id: 2, x: 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, y: 79.2, angle: 53.782} + {id: 4, x: 29, y: 75.4, angle: 17.016} + {id: 23, x: 41.6, y: 59.8, angle: 70.763} + {id: 8, x: -10, y: 15.8, angle: -9.24} + {id: 13, x: 48.6, y: 51.8, angle: 45.812} + {id: 1, x: 13.2, y: 77, 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, angle: 33.29} + {id: 6, x: 1, y: 80.6, angle: 51.295} + {id: 36, x: -27, y: 47.8, angle: 92.52} + {id: 14, x: -2, 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, y: 0.2, angle: 0} + {id: 3, x: -6.6, y: 3.2, angle: 46.72} + {id: 19, x: -13.8, y: 24.2, angle: -9.205} + } + expected = {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} + result = Prism.findSequence start, prisms + assert.are.same expected, result + + pending 'complex path with multiple prisms floating point precision', -> + start = {x: 0, y: 0, angle: 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, 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} + {id: 53, x: -1.2, y: 0, angle: 0} + {id: 52, x: 14.2, y: 24, 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, angle: -100.976} + {id: 65, x: 48, y: 10.2, angle: 87.455} + {id: 21, x: -41.8, y: 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, y: 0.2, angle: 89.105} + {id: 68, x: 14.6, y: 28, angle: -29.867} + {id: 67, x: 79.8, y: 18.6, angle: -136.643} + {id: 38, x: 53, y: 14.6, angle: -90.848} + {id: 31, x: -58, y: 6.6, angle: -61.837} + {id: 74, x: -30.8, y: 0.4, angle: 85.966} + {id: 48, x: -4.6, y: 10, angle: -161.222} + {id: 12, x: 59, y: 5, 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, y: 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, y: 0.2, angle: 70.445} + {id: 41, x: -12, y: 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, angle: -91.685} + {id: 13, x: 34.4, y: 7, angle: 90} + {id: 16, x: -47.4, y: 11.4, angle: -136.02} + {id: 6, x: 90, y: 0.2, angle: 90.415} + {id: 54, x: 44, y: 27.8, angle: 85.969} + {id: 32, x: -9, y: 0, angle: 91.615} + {id: 8, x: -31.6, y: 30.8, angle: 0.535} + {id: 39, x: -12, y: 8.2, angle: 90} + {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, y: 14, 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, 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, angle: -140.177} + {id: 34, x: -9.4, y: 23.2, angle: -88.238} + {id: 76, x: -57.6, y: 0, angle: 1.2} + {id: 43, x: 7, y: 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, 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, 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, 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} + } + expected = {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} + result = Prism.findSequence start, prisms + assert.are.same expected, result