diff --git a/config.json b/config.json index 2d3e824e..50652d13 100644 --- a/config.json +++ b/config.json @@ -258,6 +258,14 @@ ], "prerequisites": [], "difficulty": 3 + }, + { + "slug": "dominoes", + "name": "Dominoes", + "uuid": "4bf12182-491e-4da7-a51a-3d96a2649d8a", + "practices": [], + "prerequisites": [], + "difficulty": 10 } ], "foregone": [ diff --git a/exercises/practice/dominoes/.docs/instructions.md b/exercises/practice/dominoes/.docs/instructions.md new file mode 100644 index 00000000..1ced9f64 --- /dev/null +++ b/exercises/practice/dominoes/.docs/instructions.md @@ -0,0 +1,13 @@ +# Instructions + +Make a chain of dominoes. + +Compute a way to order a given set of dominoes in such a way that they form a correct domino chain (the dots on one half of a stone match the dots on the neighboring half of an adjacent stone) and that dots on the halves of the stones which don't have a neighbor (the first and last stone) match each other. + +For example given the stones `[2|1]`, `[2|3]` and `[1|3]` you should compute something +like `[1|2] [2|3] [3|1]` or `[3|2] [2|1] [1|3]` or `[1|3] [3|2] [2|1]` etc, where the first and last numbers are the same. + +For stones `[1|2]`, `[4|1]` and `[2|3]` the resulting chain is not valid: `[4|1] [1|2] [2|3]`'s first and last numbers are not the same. +4 != 3 + +Some test cases may use duplicate stones in a chain solution, assume that multiple Domino sets are being used. diff --git a/exercises/practice/dominoes/.meta/config.json b/exercises/practice/dominoes/.meta/config.json new file mode 100644 index 00000000..9c731c16 --- /dev/null +++ b/exercises/practice/dominoes/.meta/config.json @@ -0,0 +1,18 @@ +{ + "authors": [ + "misicnenad" + ], + "files": { + "solution": [ + "src/lib.cairo", + "Scarb.toml" + ], + "test": [ + "src/tests.cairo" + ], + "example": [ + ".meta/example.cairo" + ] + }, + "blurb": "Make a chain of dominoes." +} diff --git a/exercises/practice/dominoes/.meta/example.cairo b/exercises/practice/dominoes/.meta/example.cairo new file mode 100644 index 00000000..a709d46c --- /dev/null +++ b/exercises/practice/dominoes/.meta/example.cairo @@ -0,0 +1,145 @@ +use core::dict::Felt252DictTrait; + +type Domino = (u8, u8); + +/// A table keeping track of available dominoes. +/// +/// Effectively a 6x6 matrix represented as a dynamic array. Each position denotes whether a domino +/// is available with that column dots and row dots. Positions are mirrored ((3,4) == (4,3)), except +/// for positions with equal row and column numbers. +#[derive(Destruct)] +struct AvailabilityTable { + d: Felt252Dict, + len: usize +} + +fn index(x: u8, y: u8) -> u8 { + (x - 1) * 6 + (y - 1) +} + +#[generate_trait] +impl AvailabilityTableImpl of AvailabilityTableTrait { + fn new() -> AvailabilityTable { + AvailabilityTable { d: Default::default(), len: 36 } + } + + fn get(ref self: AvailabilityTable, x: u8, y: u8) -> u8 { + let i = index(x, y); + assert!(i.into() < self.len, "Index out of bounds"); + self.d.get(i.into()) + } + + fn set(ref self: AvailabilityTable, x: u8, y: u8, c: u8) { + self.d.insert(index(x, y).into(), c); + } + + fn add(ref self: AvailabilityTable, x: u8, y: u8) { + let c = self.get(x, y); + self.set(x, y, c + 1); + if x != y { // Not the diagonal + let c = self.get(y, x); + self.set(y, x, c + 1); + } + } + + fn remove(ref self: AvailabilityTable, x: u8, y: u8) { + // For this toy code hard explicit fail is best + assert!(self.get(x, y) > 0, "no stones to remove: ({:?}, {:?})", x, y); + + let c = self.get(x, y); + self.set(x, y, c - 1); + if x != y { // Not the diagonal + let c = self.get(y, x); + self.set(y, x, c - 1); + } + } + + fn pop_first(ref self: AvailabilityTable, x: u8) -> Option { + // the "double" has precedence, otherwise an invalid chain might occur + if self.get(x, x) > 0 { + self.remove(x, x); + return Option::Some(x); + } + + let mut y = 1; + loop { + if y == 7 { + break Option::None; + } + if self.get(x, y) > 0 { + self.remove(x, y); + break Option::Some(y); + } + y += 1; + } + } +} + +fn chain(dominoes: @Array) -> Option> { + match dominoes.len() { + 0 => Option::Some(array![]), + 1 => { + let domino: Domino = *dominoes[0]; + let (x, y) = domino; + if x == y { + Option::Some(array![domino]) + } else { + Option::None + } + }, + _ => { + // First check if the total number of each amount of dots is even, if not it's not + // possible to complete a cycle. This follows from that it's an Eulerian path. + let mut d: Felt252Dict = Default::default(); + let mut i = 0; + while i < dominoes + .len() { + let (x, y): Domino = *dominoes[i]; + d.insert(x.into(), d.get(x.into()) + 1); + d.insert(y.into(), d.get(y.into()) + 1); + i += 1; + }; + let mut i = 0; + let even_dot_types = loop { + if i == 6 { + break true; + } + if d.get(i.into()) % 2 != 0 { + break false; + } + i += 1; + }; + if !even_dot_types { + return Option::None; + } + let chain = chain_worker(dominoes); + if chain.len() == dominoes.len() { + Option::Some(chain) + } else { + Option::None + } + } + } +} + +fn chain_worker(dominoes: @Array) -> Array { + let mut t = AvailabilityTableTrait::new(); + let mut i = dominoes.len() - 1; + let first = *dominoes[i]; + while i != 0 { + i -= 1; + let (x, y): Domino = *dominoes[i]; + t.add(x, y); + }; + let mut chain: Array = array![]; + chain.append(first); + let (_, mut x) = first; + while let Option::Some(y) = t.pop_first(x) { + chain.append((x, y)); + x = y; + }; + chain +} + +#[cfg(test)] +mod tests; diff --git a/exercises/practice/dominoes/.meta/tests.toml b/exercises/practice/dominoes/.meta/tests.toml new file mode 100644 index 00000000..08c8e08d --- /dev/null +++ b/exercises/practice/dominoes/.meta/tests.toml @@ -0,0 +1,49 @@ +# 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. + +[31a673f2-5e54-49fe-bd79-1c1dae476c9c] +description = "empty input = empty output" + +[4f99b933-367b-404b-8c6d-36d5923ee476] +description = "singleton input = singleton output" + +[91122d10-5ec7-47cb-b759-033756375869] +description = "singleton that can't be chained" + +[be8bc26b-fd3d-440b-8e9f-d698a0623be3] +description = "three elements" + +[99e615c6-c059-401c-9e87-ad7af11fea5c] +description = "can reverse dominoes" + +[51f0c291-5d43-40c5-b316-0429069528c9] +description = "can't be chained" + +[9a75e078-a025-4c23-8c3a-238553657f39] +description = "disconnected - simple" + +[0da0c7fe-d492-445d-b9ef-1f111f07a301] +description = "disconnected - double loop" + +[b6087ff0-f555-4ea0-a71c-f9d707c5994a] +description = "disconnected - single isolated" + +[2174fbdc-8b48-4bac-9914-8090d06ef978] +description = "need backtrack" + +[167bb480-dfd1-4318-a20d-4f90adb4a09f] +description = "separate loops" + +[cd061538-6046-45a7-ace9-6708fe8f6504] +description = "nine elements" + +[44704c7c-3adb-4d98-bd30-f45527cf8b49] +description = "separate three-domino loops" diff --git a/exercises/practice/dominoes/Scarb.toml b/exercises/practice/dominoes/Scarb.toml new file mode 100644 index 00000000..7b9a6adc --- /dev/null +++ b/exercises/practice/dominoes/Scarb.toml @@ -0,0 +1,9 @@ +[package] +name = "dominoes" +version = "0.1.0" +edition = "2023_11" + +# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html + +[dependencies] +alexandria_sorting = { git = "https://github.com/keep-starknet-strange/alexandria.git" } diff --git a/exercises/practice/dominoes/src/lib.cairo b/exercises/practice/dominoes/src/lib.cairo new file mode 100644 index 00000000..70323702 --- /dev/null +++ b/exercises/practice/dominoes/src/lib.cairo @@ -0,0 +1,5 @@ +fn chain(dominoes: @Array<(u8, u8)>) -> Option> { + panic!( + "From the given dominoes '{dominoes:?}' construct a proper dominoes chain or return None if it is not possible." + ) +} diff --git a/exercises/practice/dominoes/src/tests.cairo b/exercises/practice/dominoes/src/tests.cairo new file mode 100644 index 00000000..ce85f295 --- /dev/null +++ b/exercises/practice/dominoes/src/tests.cairo @@ -0,0 +1,206 @@ +use dominoes::{Domino, chain}; +use alexandria_sorting::MergeSort; + +#[derive(Debug)] +enum CheckResult { + GotInvalid, // chain returned None + Correct, + ChainingFailure: Array< + Domino + >, // failure to match the dots at the right side of one domino with + // the one on the left side of the next + LengthMismatch: Array, + DominoMismatch: Array, // different dominoes are used in input and output +} + +fn normalize(d: Domino) -> Domino { + let (m, n) = d; + if m > n { + (n, m) + } else { + (m, n) + } +} + +fn check(input: @Array) -> CheckResult { + match chain(input) { + Option::None => CheckResult::GotInvalid, + Option::Some(output) => { + if input.len() != output.len() { + return CheckResult::LengthMismatch(output); + } else if input.is_empty() { + // and thus output.is_empty() + return CheckResult::Correct; + } + + let mut output_sorted = sort(@output); + let mut input_sorted = sort(input); + if input_sorted != output_sorted { + return CheckResult::DominoMismatch(output); + } + + // both input and output have at least 1 element + // This essentially puts the first element after the last one, thereby making it + // easy to check whether the domino chains "wraps around". + let (_, mut n): Domino = *output.at(0); + let mut i = 1; + loop { + if i == output.len() { + break CheckResult::Correct; + } + let (first, second): Domino = *output.at(i); + if n != first { + break CheckResult::ChainingFailure(output); + } + n = second; + i += 1; + } + } + } +} + +impl DominoPartialOrd of PartialOrd { + fn le(lhs: Domino, rhs: Domino) -> bool { + let (l_x, l_y) = lhs; + let (r_x, r_y) = rhs; + l_x < r_x || (l_x == r_x && l_y <= r_y) + } + + fn ge(lhs: Domino, rhs: Domino) -> bool { + let (l_x, l_y) = lhs; + let (r_x, r_y) = rhs; + l_x > r_x || (l_x == r_x && l_y >= r_y) + } + + fn lt(lhs: Domino, rhs: Domino) -> bool { + let (l_x, l_y) = lhs; + let (r_x, r_y) = rhs; + l_x < r_x || (l_x == r_x && l_y < r_y) + } + + fn gt(lhs: Domino, rhs: Domino) -> bool { + let (l_x, l_y) = lhs; + let (r_x, r_y) = rhs; + l_x > r_x || (l_x == r_x && l_y > r_y) + } +} + +fn sort(arr: @Array) -> Array { + let mut normalized: Array = array![]; + let mut i = 0; + while i < arr + .len() { + let domino: Domino = *arr[i]; + normalized.append(normalize(domino)); + i += 1; + }; + MergeSort::sort(normalized.span()) +} + +fn assert_correct(input: @Array) { + match check(input) { + CheckResult::Correct => (), + CheckResult::GotInvalid => panic!("Unexpectedly got invalid on input {input:?}"), + CheckResult::ChainingFailure(output) => { + panic!("Chaining failure for input {input:?}, output {output:?}") + }, + CheckResult::LengthMismatch(output) => { + panic!("Length mismatch for input {input:?}, output {output:?}") + }, + CheckResult::DominoMismatch(output) => { + panic!("Domino mismatch for input {input:?}, output {output:?}") + } + } +} + +#[test] +fn empty_input_empty_output() { + let input: Array = array![]; + assert_eq!(chain(@input), Option::Some(array![])); +} + +#[test] +fn singleton_input_singleton_output() { + let input: Array = array![(1, 1)]; + assert_correct(@input); +} + +#[test] +fn singleton_that_cant_be_chained() { + let input: Array = array![(1, 2)]; + let none: Option> = Option::None; + assert_eq!(chain(@input), none); +} + +#[test] +fn three_elements() { + let input: Array = array![(1, 2), (3, 1), (2, 3)]; + assert_correct(@input); +} + +#[test] +fn can_reverse_dominoes() { + let input: Array = array![(1, 2), (1, 3), (2, 3)]; + assert_correct(@input); +} + +#[test] +fn cant_be_chained() { + let input: Array = array![(1, 2), (4, 1), (2, 3)]; + let none: Option> = Option::None; + assert_eq!(chain(@input), none); +} + +#[test] +fn disconnected_simple() { + let input: Array = array![(1, 1), (2, 2)]; + let none: Option> = Option::None; + assert_eq!(chain(@input), none); +} + +#[test] +fn disconnected_double_loop() { + let input: Array = array![(1, 2), (2, 1), (3, 4), (4, 3)]; + let none: Option> = Option::None; + assert_eq!(chain(@input), none); +} + +#[test] +fn disconnected_single_isolated() { + let input: Array = array![(1, 2), (2, 3), (3, 1), (4, 4)]; + let none: Option> = Option::None; + assert_eq!(chain(@input), none); +} + +#[test] +fn need_backtrack() { + let input: Array = array![(1, 2), (2, 3), (3, 1), (2, 4), (2, 4)]; + assert_correct(@input); +} + +#[test] +fn separate_loops() { + let input: Array = array![(1, 2), (2, 3), (3, 1), (1, 1), (2, 2), (3, 3)]; + assert_correct(@input); +} + +#[test] +fn pop_same_value_first() { + let input: Array = array![(2, 3), (3, 1), (1, 1), (2, 2), (3, 3), (2, 1)]; + assert_correct(@input); +} + +#[test] +fn nine_elements() { + let input: Array = array![ + (1, 2), (5, 3), (3, 1), (1, 2), (2, 4), (1, 6), (2, 3), (3, 4), (5, 6), + ]; + assert_correct(@input); +} + +#[test] +fn separate_three_domino_loops() { + let input: Array = array![(1, 2), (2, 3), (3, 1), (4, 5), (5, 6), (6, 4)]; + let none: Option> = Option::None; + assert_eq!(chain(@input), none); +}