diff --git a/config.json b/config.json index 5743640..3a32a3c 100644 --- a/config.json +++ b/config.json @@ -902,6 +902,14 @@ "math" ] }, + { + "slug": "prism", + "name": "Prism", + "uuid": "bd71935a-a63a-4be3-b0ec-982240515b59", + "practices": [], + "prerequisites": [], + "difficulty": 4 + }, { "slug": "robot-name", "name": "Robot Name", 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..cd34901 --- /dev/null +++ b/exercises/practice/prism/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "BNAndras" + ], + "files": { + "solution": [ + "prism.lua" + ], + "test": [ + "prism_spec.lua" + ], + "example": [ + ".meta/example.lua" + ] + }, + "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.lua b/exercises/practice/prism/.meta/example.lua new file mode 100644 index 0000000..ab3312f --- /dev/null +++ b/exercises/practice/prism/.meta/example.lua @@ -0,0 +1,45 @@ +local function find_sequence(start, prisms) + local x, y, angle = start.x, start.y, start.angle + local sequence = {} + + while true do + local rad = angle * math.pi / 180 + local dirX = math.cos(rad) + local dirY = math.sin(rad) + + local nearest = nil + local nearestDist = math.huge + + for _, prism in ipairs(prisms) do + local dx = prism.x - x + local dy = prism.y - y + + local dist = dx * dirX + dy * dirY + -- ignore prisms behind or at the start + if dist > 1e-6 then + local 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) then + if dist < nearestDist then + nearestDist = dist + nearest = prism + end + end + end + end + + if not nearest then + break + end + + table.insert(sequence, nearest.id) + x = nearest.x + y = nearest.y + angle = (angle + nearest.angle) % 360 + end + + return sequence +end + +return { find_sequence = find_sequence } diff --git a/exercises/practice/prism/.meta/spec_generator.lua b/exercises/practice/prism/.meta/spec_generator.lua new file mode 100644 index 0000000..7aec349 --- /dev/null +++ b/exercises/practice/prism/.meta/spec_generator.lua @@ -0,0 +1,45 @@ +return { + module_name = 'prism', + + generate_test = function(case) + local lines = {} + + local function snake_case(str) + local s = str:gsub('%u', function(c) + return '_' .. c:lower() + end) + if s:sub(1, 1) == '_' then + s = s:sub(2) + end + return s + end + + table.insert(lines, + string.format("local start = { x = %s, y = %s, angle = %s }", case.input.start.x, case.input.start.y, + case.input.start.angle)) + + if #case.input.prisms == 0 then + table.insert(lines, "local prisms = {}") + else + table.insert(lines, "local prisms = {") + for _, prism in ipairs(case.input.prisms) do + table.insert(lines, string.format(" { id = %s, x = %s, y = %s, angle = %s },", prism.id, prism.x, prism.y, + prism.angle)) + end + table.insert(lines, "}") + end + + local expected = "" + if #case.expected.sequence == 0 then + expected = "{}" + else + expected = string.format("{ %s }", table.concat(case.expected.sequence, ", ")) + end + + table.insert(lines, string.format("local expected = %s", expected)) + table.insert(lines, string.format("local result = prism.%s(start, prisms)", snake_case(case.property))) + table.insert(lines, "assert.are.same(expected, result)") + + return table.concat(lines, "\n") + end +} 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.lua b/exercises/practice/prism/prism.lua new file mode 100644 index 0000000..ad5810e --- /dev/null +++ b/exercises/practice/prism/prism.lua @@ -0,0 +1,5 @@ +local function find_sequence(start, prisms) + +end + +return { find_sequence = find_sequence } diff --git a/exercises/practice/prism/prism_spec.lua b/exercises/practice/prism/prism_spec.lua new file mode 100644 index 0000000..90b7d20 --- /dev/null +++ b/exercises/practice/prism/prism_spec.lua @@ -0,0 +1,368 @@ +local prism = require('prism') + +describe('prism', function() + it('zero prisms', function() + local start = { x = 0, y = 0, angle = 0 } + local prisms = {} + local expected = {} + local result = prism.find_sequence(start, prisms) + assert.are.same(expected, result) + end) + + it('one prism one hit', function() + local start = { x = 0, y = 0, angle = 0 } + local prisms = { { id = 1, x = 10, y = 0, angle = 0 } } + local expected = { 1 } + local result = prism.find_sequence(start, prisms) + assert.are.same(expected, result) + end) + + it('one prism zero hits', function() + local start = { x = 0, y = 0, angle = 0 } + local prisms = { { id = 1, x = -10, y = 0, angle = 0 } } + local expected = {} + local result = prism.find_sequence(start, prisms) + assert.are.same(expected, result) + end) + + it('going up zero hits', function() + local start = { x = 0, y = 0, angle = 90 } + local 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 } + } + local expected = {} + local result = prism.find_sequence(start, prisms) + assert.are.same(expected, result) + end) + + it('going down zero hits', function() + local start = { x = 0, y = 0, angle = -90 } + local 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 } + } + local expected = {} + local result = prism.find_sequence(start, prisms) + assert.are.same(expected, result) + end) + + it('going left zero hits', function() + local start = { x = 0, y = 0, angle = 180 } + local 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 } + } + local expected = {} + local result = prism.find_sequence(start, prisms) + assert.are.same(expected, result) + end) + + it('negative angle', function() + local start = { x = 0, y = 0, angle = -180 } + local 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 } + } + local expected = {} + local result = prism.find_sequence(start, prisms) + assert.are.same(expected, result) + end) + + it('large angle', function() + local start = { x = 0, y = 0, angle = 2340 } + local prisms = { { id = 1, x = 10, y = 0, angle = 0 } } + local expected = {} + local result = prism.find_sequence(start, prisms) + assert.are.same(expected, result) + end) + + it('upward refraction two hits', function() + local start = { x = 0, y = 0, angle = 0 } + local prisms = { { id = 1, x = 10, y = 10, angle = 0 }, { id = 2, x = 10, y = 0, angle = 90 } } + local expected = { 2, 1 } + local result = prism.find_sequence(start, prisms) + assert.are.same(expected, result) + end) + + it('downward refraction two hits', function() + local start = { x = 0, y = 0, angle = 0 } + local prisms = { { id = 1, x = 10, y = 0, angle = -90 }, { id = 2, x = 10, y = -10, angle = 0 } } + local expected = { 1, 2 } + local result = prism.find_sequence(start, prisms) + assert.are.same(expected, result) + end) + + it('same prism twice', function() + local start = { x = 0, y = 0, angle = 0 } + local prisms = { { id = 2, x = 10, y = 0, angle = 0 }, { id = 1, x = 20, y = 0, angle = -180 } } + local expected = { 2, 1, 2 } + local result = prism.find_sequence(start, prisms) + assert.are.same(expected, result) + end) + + it('simple path', function() + local start = { x = 0, y = 0, angle = 0 } + local 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 } + } + local expected = { 2, 1, 3 } + local result = prism.find_sequence(start, prisms) + assert.are.same(expected, result) + end) + + it('multiple prisms floating point precision', function() + local start = { x = 0, y = 0, angle = -6.429 } + local 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 } + } + local 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 + } + local result = prism.find_sequence(start, prisms) + assert.are.same(expected, result) + end) + + it('complex path with multiple prisms floating point precision', function() + local start = { x = 0, y = 0, angle = 0.0 } + local 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 } + } + local 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 + } + local result = prism.find_sequence(start, prisms) + assert.are.same(expected, result) + end) +end)