From a5d018ac68e1e04b135fe0c7f85f341b95ec590b Mon Sep 17 00:00:00 2001 From: Eric Willigers Date: Sat, 4 Apr 2026 23:18:59 +1100 Subject: [PATCH 1/3] Add `zebra-puzzle` exercise --- bin/add-practice-exercise | 2 +- config.json | 8 ++ exercises/practice/zebra-puzzle/.busted | 5 + .../zebra-puzzle/.docs/instructions.md | 32 +++++ .../zebra-puzzle/.docs/introduction.md | 15 +++ .../practice/zebra-puzzle/.meta/config.json | 19 +++ .../practice/zebra-puzzle/.meta/example.lua | 120 ++++++++++++++++++ .../zebra-puzzle/.meta/spec_generator.lua | 9 ++ .../practice/zebra-puzzle/.meta/tests.toml | 16 +++ .../practice/zebra-puzzle/zebra-puzzle.lua | 9 ++ .../zebra-puzzle/zebra-puzzle_spec.lua | 11 ++ 11 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 exercises/practice/zebra-puzzle/.busted create mode 100644 exercises/practice/zebra-puzzle/.docs/instructions.md create mode 100644 exercises/practice/zebra-puzzle/.docs/introduction.md create mode 100644 exercises/practice/zebra-puzzle/.meta/config.json create mode 100644 exercises/practice/zebra-puzzle/.meta/example.lua create mode 100644 exercises/practice/zebra-puzzle/.meta/spec_generator.lua create mode 100644 exercises/practice/zebra-puzzle/.meta/tests.toml create mode 100644 exercises/practice/zebra-puzzle/zebra-puzzle.lua create mode 100644 exercises/practice/zebra-puzzle/zebra-puzzle_spec.lua diff --git a/bin/add-practice-exercise b/bin/add-practice-exercise index 114f3f23..c12a0d7a 100755 --- a/bin/add-practice-exercise +++ b/bin/add-practice-exercise @@ -67,7 +67,7 @@ fi ./bin/fetch-configlet ./bin/configlet create --practice-exercise "${slug}" --author "${author}" --difficulty "${difficulty}" -filter='.exercises.practice = (.exercises.practice | sort_by(.difficulty, (.name|ascii_upcase))' +filter='.exercises.practice = (.exercises.practice | sort_by(.difficulty, (.name|ascii_upcase)))' jq "${filter}" config.json > config.sorted && mv config.sorted config.json exercise_dir="exercises/practice/${slug}" diff --git a/config.json b/config.json index 11ec49db..4a0afe0a 100644 --- a/config.json +++ b/config.json @@ -1459,6 +1459,14 @@ "strings" ] }, + { + "slug": "zebra-puzzle", + "name": "Zebra Puzzle", + "uuid": "87be2159-daf5-4a6f-b7e1-61dc93edb1e7", + "practices": [], + "prerequisites": [], + "difficulty": 8 + }, { "slug": "pov", "name": "POV", diff --git a/exercises/practice/zebra-puzzle/.busted b/exercises/practice/zebra-puzzle/.busted new file mode 100644 index 00000000..86b84e7c --- /dev/null +++ b/exercises/practice/zebra-puzzle/.busted @@ -0,0 +1,5 @@ +return { + default = { + ROOT = { '.' } + } +} diff --git a/exercises/practice/zebra-puzzle/.docs/instructions.md b/exercises/practice/zebra-puzzle/.docs/instructions.md new file mode 100644 index 00000000..aedce9b2 --- /dev/null +++ b/exercises/practice/zebra-puzzle/.docs/instructions.md @@ -0,0 +1,32 @@ +# Instructions + +Your task is to solve the Zebra Puzzle to find the answer to these two questions: + +- Which of the residents drinks water? +- Who owns the zebra? + +## Puzzle + +The following 15 statements are all known to be true: + +1. There are five houses. +2. The Englishman lives in the red house. +3. The Spaniard owns the dog. +4. The person in the green house drinks coffee. +5. The Ukrainian drinks tea. +6. The green house is immediately to the right of the ivory house. +7. The snail owner likes to go dancing. +8. The person in the yellow house is a painter. +9. The person in the middle house drinks milk. +10. The Norwegian lives in the first house. +11. The person who enjoys reading lives in the house next to the person with the fox. +12. The painter's house is next to the house with the horse. +13. The person who plays football drinks orange juice. +14. The Japanese person plays chess. +15. The Norwegian lives next to the blue house. + +Additionally, each of the five houses is painted a different color, and their inhabitants are of different national extractions, own different pets, drink different beverages and engage in different hobbies. + +~~~~exercism/note +There are 24 billion (5!⁵ = 24,883,200,000) possible solutions, so try ruling out as many solutions as possible. +~~~~ diff --git a/exercises/practice/zebra-puzzle/.docs/introduction.md b/exercises/practice/zebra-puzzle/.docs/introduction.md new file mode 100644 index 00000000..bbcaa6fd --- /dev/null +++ b/exercises/practice/zebra-puzzle/.docs/introduction.md @@ -0,0 +1,15 @@ +# Introduction + +The Zebra Puzzle is a famous logic puzzle in which there are five houses, each painted a different color. +The houses have different inhabitants, who have different nationalities, own different pets, drink different beverages and enjoy different hobbies. + +To help you solve the puzzle, you're given 15 statements describing the solution. +However, only by combining the information in _all_ statements will you be able to find the solution to the puzzle. + +~~~~exercism/note +The Zebra Puzzle is a [Constraint satisfaction problem (CSP)][constraint-satisfaction-problem]. +In such a problem, you have a set of possible values and a set of constraints that limit which values are valid. +Another well-known CSP is Sudoku. + +[constraint-satisfaction-problem]: https://en.wikipedia.org/wiki/Constraint_satisfaction_problem +~~~~ diff --git a/exercises/practice/zebra-puzzle/.meta/config.json b/exercises/practice/zebra-puzzle/.meta/config.json new file mode 100644 index 00000000..017d98d4 --- /dev/null +++ b/exercises/practice/zebra-puzzle/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "keiravillekode" + ], + "files": { + "solution": [ + "zebra-puzzle.lua" + ], + "test": [ + "zebra-puzzle_spec.lua" + ], + "example": [ + ".meta/example.lua" + ] + }, + "blurb": "Solve the zebra puzzle.", + "source": "Wikipedia", + "source_url": "https://en.wikipedia.org/wiki/Zebra_Puzzle" +} diff --git a/exercises/practice/zebra-puzzle/.meta/example.lua b/exercises/practice/zebra-puzzle/.meta/example.lua new file mode 100644 index 00000000..c7a912a3 --- /dev/null +++ b/exercises/practice/zebra-puzzle/.meta/example.lua @@ -0,0 +1,120 @@ +local zebra_puzzle = {} + +local function permutations(a) + local n + + return function() + if not n then + n = #a + return a + end + + -- Step 1. Find largest j such that a[j] > a[j + 1]. + local j = n - 1 + while j >= 1 and a[j] <= a[j + 1] do + j = j - 1 + end + if j < 1 then return nil end + + -- Step 2. Find largest l such that a[j] > a[l], then swap. + local l = n + while a[j] <= a[l] do + l = l - 1 + end + a[j], a[l] = a[l], a[j] + + -- Step 3. Reverse a[j+1] ... a[n]. + local lo, hi = j + 1, n + while lo < hi do + a[lo], a[hi] = a[hi], a[lo] + lo = lo + 1 + hi = hi - 1 + end + + return a + end +end + +local function index_of(t, val) + for i, v in ipairs(t) do + if v == val then return i end + end + return nil +end + +local function next_to(a, b) + return math.abs(a - b) == 1 +end + +local water_drinker, zebra_owner + +local function solve() + for colors in permutations( + { 'yellow', 'red', 'ivory', 'green', 'blue' }) do + -- 6. The green house is immediately to the right of the ivory house. + if index_of(colors, 'green') == index_of(colors, 'ivory') + 1 then + for drinks in permutations( + { 'water', 'tea', 'orange juice', 'milk', 'coffee' }) do + -- 4. Coffee is drunk in the green house. + -- 9. Milk is drunk in the middle house. + if index_of(drinks, 'coffee') == index_of(colors, 'green') + and drinks[3] == 'milk' then + for hobbies in permutations( + { 'reading', 'painting', 'football', 'dancing', 'chess' }) do + -- 8. The person in the yellow house is a painter. + -- 13. The person who plays football drinks orange juice. + if index_of(hobbies, 'painting') == index_of(colors, 'yellow') + and index_of(hobbies, 'football') == index_of(drinks, 'orange juice') then + for nationalities in permutations( + { 'Ukrainian', 'Spaniard', 'Norwegian', 'Japanese', 'Englishman' }) do + -- 10. The Norwegian lives in the first house. + -- 2. The Englishman lives in the red house. + -- 15. The Norwegian lives next to the blue house. + -- 5. The Ukrainian drinks tea. + -- 14. The Japanese person plays chess. + if nationalities[1] == 'Norwegian' + and index_of(colors, 'red') == index_of(nationalities, 'Englishman') + and next_to(index_of(nationalities, 'Norwegian'), index_of(colors, 'blue')) + and index_of(drinks, 'tea') == index_of(nationalities, 'Ukrainian') + and index_of(hobbies, 'chess') == index_of(nationalities, 'Japanese') then + for pets in permutations( + { 'zebra', 'snail', 'horse', 'fox', 'dog' }) do + -- 3. The Spaniard owns the dog. + -- 7. The snail owner likes to go dancing. + -- 11. The person who enjoys reading lives in the house next to the person with the fox. + -- 12. The painter's house is next to the house with the horse. + if index_of(pets, 'dog') == index_of(nationalities, 'Spaniard') + and index_of(pets, 'snail') == index_of(hobbies, 'dancing') + and next_to(index_of(hobbies, 'reading'), index_of(pets, 'fox')) + and next_to(index_of(hobbies, 'painting'), index_of(pets, 'horse')) then + return nationalities[index_of(drinks, 'water')], + nationalities[index_of(pets, 'zebra')] + end + end + end + end + end + end + end + end + end + end +end + +local function ensure_solved() + if not water_drinker then + water_drinker, zebra_owner = solve() + end +end + +function zebra_puzzle.drinksWater() + ensure_solved() + return water_drinker +end + +function zebra_puzzle.ownsZebra() + ensure_solved() + return zebra_owner +end + +return zebra_puzzle diff --git a/exercises/practice/zebra-puzzle/.meta/spec_generator.lua b/exercises/practice/zebra-puzzle/.meta/spec_generator.lua new file mode 100644 index 00000000..fcaf8781 --- /dev/null +++ b/exercises/practice/zebra-puzzle/.meta/spec_generator.lua @@ -0,0 +1,9 @@ +return { + module_name = 'zebra_puzzle', + + generate_test = function(case) + local template = [[ + assert.equal('%s', zebra_puzzle.%s())]] + return template:format(case.expected, case.property) + end +} diff --git a/exercises/practice/zebra-puzzle/.meta/tests.toml b/exercises/practice/zebra-puzzle/.meta/tests.toml new file mode 100644 index 00000000..56c21c7a --- /dev/null +++ b/exercises/practice/zebra-puzzle/.meta/tests.toml @@ -0,0 +1,16 @@ +# 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. + +[16efb4e4-8ad7-4d5e-ba96-e5537b66fd42] +description = "resident who drinks water" + +[084d5b8b-24e2-40e6-b008-c800da8cd257] +description = "resident who owns zebra" diff --git a/exercises/practice/zebra-puzzle/zebra-puzzle.lua b/exercises/practice/zebra-puzzle/zebra-puzzle.lua new file mode 100644 index 00000000..f507c7a5 --- /dev/null +++ b/exercises/practice/zebra-puzzle/zebra-puzzle.lua @@ -0,0 +1,9 @@ +local zebra_puzzle = {} + +function zebra_puzzle.drinksWater() +end + +function zebra_puzzle.ownsZebra() +end + +return zebra_puzzle diff --git a/exercises/practice/zebra-puzzle/zebra-puzzle_spec.lua b/exercises/practice/zebra-puzzle/zebra-puzzle_spec.lua new file mode 100644 index 00000000..c9be403b --- /dev/null +++ b/exercises/practice/zebra-puzzle/zebra-puzzle_spec.lua @@ -0,0 +1,11 @@ +local zebra_puzzle = require('zebra-puzzle') + +describe('zebra-puzzle', function() + it('resident who drinks water', function() + assert.equal('Norwegian', zebra_puzzle.drinksWater()) + end) + + it('resident who owns zebra', function() + assert.equal('Japanese', zebra_puzzle.ownsZebra()) + end) +end) From f198304e4e3298e909ce2dd39aa8b0411e525d3f Mon Sep 17 00:00:00 2001 From: Eric Willigers Date: Sun, 5 Apr 2026 12:52:39 +1000 Subject: [PATCH 2/3] snake_case --- exercises/practice/zebra-puzzle/.meta/example.lua | 4 ++-- .../practice/zebra-puzzle/.meta/spec_generator.lua | 12 +++++++++++- exercises/practice/zebra-puzzle/zebra-puzzle.lua | 4 ++-- .../practice/zebra-puzzle/zebra-puzzle_spec.lua | 4 ++-- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/exercises/practice/zebra-puzzle/.meta/example.lua b/exercises/practice/zebra-puzzle/.meta/example.lua index c7a912a3..31229a22 100644 --- a/exercises/practice/zebra-puzzle/.meta/example.lua +++ b/exercises/practice/zebra-puzzle/.meta/example.lua @@ -107,12 +107,12 @@ local function ensure_solved() end end -function zebra_puzzle.drinksWater() +function zebra_puzzle.drinks_water() ensure_solved() return water_drinker end -function zebra_puzzle.ownsZebra() +function zebra_puzzle.owns_zebra() ensure_solved() return zebra_owner end diff --git a/exercises/practice/zebra-puzzle/.meta/spec_generator.lua b/exercises/practice/zebra-puzzle/.meta/spec_generator.lua index fcaf8781..e3e3997a 100644 --- a/exercises/practice/zebra-puzzle/.meta/spec_generator.lua +++ b/exercises/practice/zebra-puzzle/.meta/spec_generator.lua @@ -1,9 +1,19 @@ +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 + return { module_name = 'zebra_puzzle', generate_test = function(case) local template = [[ assert.equal('%s', zebra_puzzle.%s())]] - return template:format(case.expected, case.property) + return template:format(case.expected, snake_case(case.property)) end } diff --git a/exercises/practice/zebra-puzzle/zebra-puzzle.lua b/exercises/practice/zebra-puzzle/zebra-puzzle.lua index f507c7a5..723094e8 100644 --- a/exercises/practice/zebra-puzzle/zebra-puzzle.lua +++ b/exercises/practice/zebra-puzzle/zebra-puzzle.lua @@ -1,9 +1,9 @@ local zebra_puzzle = {} -function zebra_puzzle.drinksWater() +function zebra_puzzle.drinks_water() end -function zebra_puzzle.ownsZebra() +function zebra_puzzle.owns_zebra() end return zebra_puzzle diff --git a/exercises/practice/zebra-puzzle/zebra-puzzle_spec.lua b/exercises/practice/zebra-puzzle/zebra-puzzle_spec.lua index c9be403b..bf18f74f 100644 --- a/exercises/practice/zebra-puzzle/zebra-puzzle_spec.lua +++ b/exercises/practice/zebra-puzzle/zebra-puzzle_spec.lua @@ -2,10 +2,10 @@ local zebra_puzzle = require('zebra-puzzle') describe('zebra-puzzle', function() it('resident who drinks water', function() - assert.equal('Norwegian', zebra_puzzle.drinksWater()) + assert.equal('Norwegian', zebra_puzzle.drinks_water()) end) it('resident who owns zebra', function() - assert.equal('Japanese', zebra_puzzle.ownsZebra()) + assert.equal('Japanese', zebra_puzzle.owns_zebra()) end) end) From 534ab210550192ba7723ff3748f2c3ebd6e1f87d Mon Sep 17 00:00:00 2001 From: Eric Willigers Date: Sun, 5 Apr 2026 12:59:53 +1000 Subject: [PATCH 3/3] lua-format --- .../practice/zebra-puzzle/.meta/example.lua | 49 +++++++++---------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/exercises/practice/zebra-puzzle/.meta/example.lua b/exercises/practice/zebra-puzzle/.meta/example.lua index 31229a22..4747f25a 100644 --- a/exercises/practice/zebra-puzzle/.meta/example.lua +++ b/exercises/practice/zebra-puzzle/.meta/example.lua @@ -14,7 +14,9 @@ local function permutations(a) while j >= 1 and a[j] <= a[j + 1] do j = j - 1 end - if j < 1 then return nil end + if j < 1 then + return nil + end -- Step 2. Find largest l such that a[j] > a[l], then swap. local l = n @@ -37,7 +39,9 @@ end local function index_of(t, val) for i, v in ipairs(t) do - if v == val then return i end + if v == val then + return i + end end return nil end @@ -49,46 +53,37 @@ end local water_drinker, zebra_owner local function solve() - for colors in permutations( - { 'yellow', 'red', 'ivory', 'green', 'blue' }) do + for colors in permutations({ 'yellow', 'red', 'ivory', 'green', 'blue' }) do -- 6. The green house is immediately to the right of the ivory house. if index_of(colors, 'green') == index_of(colors, 'ivory') + 1 then - for drinks in permutations( - { 'water', 'tea', 'orange juice', 'milk', 'coffee' }) do + for drinks in permutations({ 'water', 'tea', 'orange juice', 'milk', 'coffee' }) do -- 4. Coffee is drunk in the green house. -- 9. Milk is drunk in the middle house. - if index_of(drinks, 'coffee') == index_of(colors, 'green') - and drinks[3] == 'milk' then - for hobbies in permutations( - { 'reading', 'painting', 'football', 'dancing', 'chess' }) do + if index_of(drinks, 'coffee') == index_of(colors, 'green') and drinks[3] == 'milk' then + for hobbies in permutations({ 'reading', 'painting', 'football', 'dancing', 'chess' }) do -- 8. The person in the yellow house is a painter. -- 13. The person who plays football drinks orange juice. - if index_of(hobbies, 'painting') == index_of(colors, 'yellow') - and index_of(hobbies, 'football') == index_of(drinks, 'orange juice') then - for nationalities in permutations( - { 'Ukrainian', 'Spaniard', 'Norwegian', 'Japanese', 'Englishman' }) do + if index_of(hobbies, 'painting') == index_of(colors, 'yellow') and index_of(hobbies, 'football') == + index_of(drinks, 'orange juice') then + for nationalities in permutations({ 'Ukrainian', 'Spaniard', 'Norwegian', 'Japanese', 'Englishman' }) do -- 10. The Norwegian lives in the first house. -- 2. The Englishman lives in the red house. -- 15. The Norwegian lives next to the blue house. -- 5. The Ukrainian drinks tea. -- 14. The Japanese person plays chess. - if nationalities[1] == 'Norwegian' - and index_of(colors, 'red') == index_of(nationalities, 'Englishman') - and next_to(index_of(nationalities, 'Norwegian'), index_of(colors, 'blue')) - and index_of(drinks, 'tea') == index_of(nationalities, 'Ukrainian') - and index_of(hobbies, 'chess') == index_of(nationalities, 'Japanese') then - for pets in permutations( - { 'zebra', 'snail', 'horse', 'fox', 'dog' }) do + if nationalities[1] == 'Norwegian' and index_of(colors, 'red') == index_of(nationalities, 'Englishman') and + next_to(index_of(nationalities, 'Norwegian'), index_of(colors, 'blue')) and index_of(drinks, 'tea') == + index_of(nationalities, 'Ukrainian') and index_of(hobbies, 'chess') == + index_of(nationalities, 'Japanese') then + for pets in permutations({ 'zebra', 'snail', 'horse', 'fox', 'dog' }) do -- 3. The Spaniard owns the dog. -- 7. The snail owner likes to go dancing. -- 11. The person who enjoys reading lives in the house next to the person with the fox. -- 12. The painter's house is next to the house with the horse. - if index_of(pets, 'dog') == index_of(nationalities, 'Spaniard') - and index_of(pets, 'snail') == index_of(hobbies, 'dancing') - and next_to(index_of(hobbies, 'reading'), index_of(pets, 'fox')) - and next_to(index_of(hobbies, 'painting'), index_of(pets, 'horse')) then - return nationalities[index_of(drinks, 'water')], - nationalities[index_of(pets, 'zebra')] + if index_of(pets, 'dog') == index_of(nationalities, 'Spaniard') and index_of(pets, 'snail') == + index_of(hobbies, 'dancing') and next_to(index_of(hobbies, 'reading'), index_of(pets, 'fox')) and + next_to(index_of(hobbies, 'painting'), index_of(pets, 'horse')) then + return nationalities[index_of(drinks, 'water')], nationalities[index_of(pets, 'zebra')] end end end