//! TODO: module doc :v use bevy::ecs::component::Component; /// Value for the "sub tiles" inside a room tile #[derive(Clone, Copy, Debug, Default, PartialEq)] pub enum Cell { #[default] Empty, NW, NE, SE, SW, Filled, } #[derive(Clone, Copy, Debug)] pub struct Walls { pub north: bool, pub east: bool, pub south: bool, pub west: bool, } impl Walls { pub fn new(north: bool, east: bool, south: bool, west: bool) -> Self { Self { north, east, south, west, } } } #[derive(Clone, Copy, Debug)] pub enum CutLine { VertLeft, VertRight, HorizUpper, HorizLower, } #[derive(Clone, Copy, Debug)] pub enum FlipDir { Vertical, Horizontal, } #[derive(Clone, Copy, Debug)] pub enum TransposeIndex { First, Second, Third, } #[derive(Clone, Copy, Debug)] pub enum TransposeSelection { Column(TransposeIndex), Row(TransposeIndex), } #[derive(Clone, Copy, Debug)] pub enum RotationDir { Clockwise, CounterClockwise, } /// An invidiual room, or "card" in the player's hand. The room may /// *or may not* be valid, yet. #[derive(Clone, Copy, Component, Debug, Default, PartialEq)] pub struct Card { pub cells: [Cell; 9], pub nw: bool, pub n: bool, pub ne: bool, pub w: bool, pub e: bool, pub sw: bool, pub s: bool, pub se: bool, } impl Card { /// Produces a new card by stacking another on top of this one. pub fn merge(self, top: Self) -> (Self, Option) { let mut new_card = Self::default(); let mut doors: Option = None; new_card.cells = std::iter::zip(&self.cells, &top.cells) .map(|(lower, upper)| { if (lower == &Cell::Filled || upper == &Cell::Filled) || (lower == &Cell::SW && upper == &Cell::NE) || (lower == &Cell::SE && upper == &Cell::NW) || (lower == &Cell::NE && upper == &Cell::SW) || (lower == &Cell::NW && upper == &Cell::SE) { Cell::Filled } else if upper != &Cell::Empty { *upper } else { *lower } }) .collect::>() .try_into() .unwrap(); // Check for doors macro_rules! check_door { ( $dir:ident, $index:literal ) => { if self.$dir { if top.cells[$index] == Cell::Empty || top.$dir { new_card.$dir = true; } else { doors.get_or_insert_default().$dir = true; } } else if top.$dir && new_card.cells[$index] != Cell::Empty { new_card.$dir = true; } }; } check_door!(nw, 0); check_door!(n, 1); check_door!(ne, 2); check_door!(w, 3); check_door!(e, 5); check_door!(sw, 6); check_door!(s, 7); check_door!(se, 8); (new_card, doors) } /// Cuts this Card on the given line. Returns two cards pub fn cut(self, line: CutLine) -> (Self, Self) { let is_cut: fn(usize, usize) -> bool = match line { CutLine::VertLeft => |x, _| x > 0, CutLine::VertRight => |x, _| x > 1, CutLine::HorizUpper => |_, y| y > 0, CutLine::HorizLower => |_, y| y > 1, }; let mut first_card = Self::default(); let mut second_card = Self::default(); macro_rules! perform_cut { ( $dir:ident, $index:literal ) => { if is_cut($index % 3, $index / 3) { second_card.cells[$index] = self.cells[$index]; second_card.$dir = self.$dir; } else { first_card.cells[$index] = self.cells[$index]; first_card.$dir = self.$dir; } }; ( $index:literal ) => { if is_cut($index % 3, $index / 3) { second_card.cells[$index] = self.cells[$index]; } else { first_card.cells[$index] = self.cells[$index]; } }; } perform_cut!(nw, 0); perform_cut!(n, 1); perform_cut!(ne, 2); perform_cut!(w, 3); perform_cut!(4); perform_cut!(e, 5); perform_cut!(sw, 6); perform_cut!(s, 7); perform_cut!(se, 8); (first_card, second_card) } pub fn flip(self, flip: FlipDir) -> Self { let mut new_card = Self::default(); let transform = match flip { FlipDir::Horizontal => |cell: Cell| match cell { Cell::NW => Cell::NE, Cell::NE => Cell::NW, Cell::SW => Cell::SE, Cell::SE => Cell::SW, other => other, }, FlipDir::Vertical => |cell: Cell| match cell { Cell::NW => Cell::SW, Cell::NE => Cell::SE, Cell::SW => Cell::NW, Cell::SE => Cell::NE, other => other, }, }; // Check for doors macro_rules! assign_cell { ( ($source_dir:ident, $source_index:literal), ($dest_dir:ident, $dest_index:literal), $repeat:expr ) => { new_card.cells[$dest_index] = transform(self.cells[$source_index]); new_card.$dest_dir = self.$source_dir; if $repeat == true { new_card.cells[$source_index] = transform(self.cells[$dest_index]); new_card.$source_dir = self.$dest_dir; } }; } match flip { FlipDir::Horizontal => { assign_cell!((nw, 0), (ne, 2), true); assign_cell!((w, 3), (e, 5), true); assign_cell!((sw, 6), (se, 8), true); assign_cell!((n, 1), (n, 1), false); assign_cell!((s, 7), (s, 7), false); } FlipDir::Vertical => { assign_cell!((nw, 0), (sw, 6), true); assign_cell!((n, 1), (s, 7), true); assign_cell!((ne, 2), (se, 8), true); assign_cell!((w, 3), (w, 3), false); assign_cell!((e, 5), (e, 5), false); } } new_card.cells[4] = transform(self.cells[4]); new_card } pub fn transpose(self, other: Self, selection: TransposeSelection) -> (Self, Self) { let is_transposed: fn(usize, usize) -> bool = match selection { TransposeSelection::Column(TransposeIndex::First) => |x, _| x == 0, TransposeSelection::Column(TransposeIndex::Second) => |x, _| x == 1, TransposeSelection::Column(TransposeIndex::Third) => |x, _| x == 2, TransposeSelection::Row(TransposeIndex::First) => |_, y| y == 0, TransposeSelection::Row(TransposeIndex::Second) => |_, y| y == 1, TransposeSelection::Row(TransposeIndex::Third) => |_, y| y == 2, }; let mut first_card = Self::default(); let mut second_card = Self::default(); macro_rules! perform_transpose { ( $dir:ident, $index:literal ) => { if is_transposed($index % 3, $index / 3) { first_card.cells[$index] = other.cells[$index]; second_card.cells[$index] = self.cells[$index]; first_card.$dir = other.$dir; second_card.$dir = self.$dir; } else { first_card.cells[$index] = self.cells[$index]; second_card.cells[$index] = other.cells[$index]; first_card.$dir = self.$dir; second_card.$dir = other.$dir; } }; ( $index:literal ) => { if is_transposed($index % 3, $index / 3) { first_card.cells[$index] = other.cells[$index]; second_card.cells[$index] = self.cells[$index]; } else { first_card.cells[$index] = self.cells[$index]; second_card.cells[$index] = other.cells[$index]; } }; } perform_transpose!(nw, 0); perform_transpose!(n, 1); perform_transpose!(ne, 2); perform_transpose!(w, 3); perform_transpose!(4); perform_transpose!(e, 5); perform_transpose!(sw, 6); perform_transpose!(s, 7); perform_transpose!(se, 8); (first_card, second_card) } pub fn rotate(self, dir: RotationDir) -> Self { let mut new_card = Self::default(); let transform = match dir { RotationDir::Clockwise => |cell: Cell| match cell { Cell::NW => Cell::NE, Cell::NE => Cell::SE, Cell::SE => Cell::SW, Cell::SW => Cell::NW, other => other, }, RotationDir::CounterClockwise => |cell: Cell| match cell { Cell::NW => Cell::SW, Cell::SW => Cell::SE, Cell::SE => Cell::NW, Cell::NE => Cell::NW, other => other, }, }; // Check for doors macro_rules! assign_cell { ( ($source_dir:ident, $source_index:literal), ($dest_dir:ident, $dest_index:literal) ) => { new_card.cells[$dest_index] = transform(self.cells[$source_index]); new_card.$dest_dir = self.$source_dir; }; } match dir { RotationDir::Clockwise => { assign_cell!((nw, 0), (ne, 2)); assign_cell!((n, 1), (e, 5)); assign_cell!((ne, 2), (se, 8)); assign_cell!((e, 5), (s, 7)); assign_cell!((se, 8), (sw, 6)); assign_cell!((s, 7), (w, 3)); assign_cell!((sw, 6), (nw, 0)); assign_cell!((w, 3), (n, 1)); } RotationDir::CounterClockwise => { assign_cell!((nw, 0), (sw, 6)); assign_cell!((w, 3), (s, 7)); assign_cell!((sw, 6), (se, 8)); assign_cell!((s, 7), (e, 5)); assign_cell!((se, 8), (ne, 2)); assign_cell!((e, 5), (n, 1)); assign_cell!((ne, 2), (nw, 0)); assign_cell!((n, 1), (w, 3)); } } new_card.cells[4] = transform(self.cells[4]); new_card } } #[rustfmt::skip] pub const FULL_SQUARE: [Cell; 9] = [ Cell::Filled, Cell::Filled, Cell::Filled, Cell::Filled, Cell::Filled, Cell::Filled, Cell::Filled, Cell::Filled, Cell::Filled, ]; #[rustfmt::skip] pub const NW_TRIANGLE: [Cell; 9] = [ Cell::Empty, Cell::Empty, Cell::NW, Cell::Empty, Cell::NW, Cell::Filled, Cell::NW, Cell::Filled, Cell::Filled, ]; #[rustfmt::skip] pub const SW_TRIANGLE: [Cell; 9] = [ Cell::SW, Cell::Filled, Cell::Filled, Cell::Empty, Cell::SW, Cell::Filled, Cell::Empty, Cell::Empty, Cell::SW, ]; #[rustfmt::skip] pub const NE_TRIANGLE: [Cell; 9] = [ Cell::NE, Cell::Empty, Cell::Empty, Cell::Filled, Cell::NE, Cell::Empty, Cell::Filled, Cell::Filled, Cell::NE ]; #[rustfmt::skip] pub const OCTAGON: [Cell; 9] = [ Cell::NW, Cell::Filled, Cell::NE, Cell::Filled, Cell::Filled, Cell::Filled, Cell::SW, Cell::Filled, Cell::SE, ]; #[cfg(test)] mod test { use super::*; #[test] fn check_merge() { let bottom = Card { cells: NW_TRIANGLE, ..Default::default() // no doors }; let top = Card { cells: OCTAGON, ..Default::default() }; // For now, triangular cells from the top-most Card survive the merge. let expected = Card { cells: [ Cell::NW, Cell::Filled, Cell::NE, Cell::Filled, Cell::Filled, Cell::Filled, Cell::SW, Cell::Filled, Cell::Filled, ], ..Default::default() }; // Run the test let (stacked, extra_doors) = bottom.merge(top); assert_eq!(stacked, expected); assert!(extra_doors.is_none()); } #[test] fn check_merge_doors() { let bottom = Card { cells: NW_TRIANGLE, s: true, // Test that this is not overwritten by an empty tile se: true, // Test that this is subsumed by another door e: true, // Test that this is popped off into the second card ..Default::default() }; let top = Card { cells: [ Cell::Empty, Cell::Empty, Cell::Empty, Cell::Empty, Cell::Empty, Cell::Filled, Cell::Empty, Cell::Empty, Cell::Filled, ], se: true, // Test that this is subsumed by another door n: true, // Test that is is dropped because no floor ..Default::default() }; let expected = Card { cells: [ Cell::Empty, Cell::Empty, Cell::NW, Cell::Empty, Cell::NW, Cell::Filled, Cell::NW, Cell::Filled, Cell::Filled, ], s: true, se: true, ..Default::default() }; let expected_doors = Card { e: true, ..Default::default() }; let (stacked, extra_doors) = bottom.merge(top); assert_eq!(stacked, expected); assert_eq!(extra_doors, Some(expected_doors)); } /// Merging two triangular [`Cell`]s should prefer the top-most one, *unless* /// they are opposites. See test [`merge_opposite_triangles()`]. #[test] fn merge_triangle_and_triangle() { let mut top = Card::default(); top.cells[0] = Cell::NW; let mut bottom = Card::default(); bottom.cells[0] = Cell::NE; let expected = top.clone(); let (stacked, doors) = bottom.merge(top); assert_eq!(stacked, expected); assert!(doors.is_none()); } /// Merging a filled cell with anything should result in [`Cell::Filled`]. #[test] fn merge_triangle_and_filled() { let filled = Card { cells: FULL_SQUARE, ..Default::default() }; let triangle = Card { cells: NW_TRIANGLE, ..Default::default() }; let expected = filled.clone(); // Check the merge in both directions. let result_fill_over_tri = triangle.merge(filled); let result_tri_over_fill = filled.merge(triangle); assert_eq!(expected, result_fill_over_tri.0); assert_eq!(expected, result_tri_over_fill.0); assert!(result_fill_over_tri.1.is_none()); assert!(result_tri_over_fill.1.is_none()); } /// Merging a NW and SE cell should result in a single [`Cell::Filled`]. #[test] fn merge_opposite_triangles() { let mut top = Card::default(); top.cells[0] = Cell::NW; let mut bottom = Card::default(); bottom.cells[0] = Cell::SE; let mut expected = Card::default(); expected.cells[0] = Cell::Filled; let (stacked, doors) = bottom.merge(top); assert_eq!(stacked, expected); assert!(doors.is_none()); } #[test] fn cut_octagon() { // Original octagon let octagon = Card { cells: OCTAGON, ..Default::default() }; // Pairs of each different slice option let vert_left_pair = ( Card { #[rustfmt::skip] cells: [ Cell::NW, Cell::Empty, Cell::Empty, Cell::Filled, Cell::Empty, Cell::Empty, Cell::SW, Cell::Empty, Cell::Empty, ], ..Default::default() }, Card { #[rustfmt::skip] cells: [ Cell::Empty, Cell::Filled, Cell::NE, Cell::Empty, Cell::Filled, Cell::Filled, Cell::Empty, Cell::Filled, Cell::SE, ], ..Default::default() }, ); let vert_right_pair = ( Card { #[rustfmt::skip] cells: [ Cell::NW, Cell::Filled, Cell::Empty, Cell::Filled, Cell::Filled, Cell::Empty, Cell::SW, Cell::Filled, Cell::Empty, ], ..Default::default() }, Card { #[rustfmt::skip] cells: [ Cell::Empty, Cell::Empty, Cell::NE, Cell::Empty, Cell::Empty, Cell::Filled, Cell::Empty, Cell::Empty, Cell::SE, ], ..Default::default() }, ); let horiz_top_pair = ( Card { #[rustfmt::skip] cells: [ Cell::NW, Cell::Filled, Cell::NE, Cell::Empty, Cell::Empty, Cell::Empty, Cell::Empty, Cell::Empty, Cell::Empty, ], ..Default::default() }, Card { #[rustfmt::skip] cells: [ Cell::Empty, Cell::Empty, Cell::Empty, Cell::Filled, Cell::Filled, Cell::Filled, Cell::SW, Cell::Filled, Cell::SE, ], ..Default::default() }, ); let horiz_bottom_pair = ( Card { #[rustfmt::skip] cells: [ Cell::NW, Cell::Filled, Cell::NE, Cell::Filled, Cell::Filled, Cell::Filled, Cell::Empty, Cell::Empty, Cell::Empty, ], ..Default::default() }, Card { #[rustfmt::skip] cells: [ Cell::Empty, Cell::Empty, Cell::Empty, Cell::Empty, Cell::Empty, Cell::Empty, Cell::SW, Cell::Filled, Cell::SE, ], ..Default::default() }, ); // Run tests let result_vleft = octagon.clone().cut(CutLine::VertLeft); assert_eq!(result_vleft, vert_left_pair); let result_vright = octagon.clone().cut(CutLine::VertRight); assert_eq!(result_vright, vert_right_pair); let result_hupper = octagon.clone().cut(CutLine::HorizUpper); assert_eq!(result_hupper, horiz_top_pair); let result_hlower = octagon.cut(CutLine::HorizLower); assert_eq!(result_hlower, horiz_bottom_pair); } #[test] fn cut_triangle() { let tri = Card { cells: NW_TRIANGLE, ..Default::default() }; // Pairs of each different slice option let vert_left_pair = ( Card { #[rustfmt::skip] cells: [ Cell::Empty, Cell::Empty, Cell::Empty, Cell::Empty, Cell::Empty, Cell::Empty, Cell::NW, Cell::Empty, Cell::Empty, ], ..Default::default() }, Card { #[rustfmt::skip] cells: [ Cell::Empty, Cell::Empty, Cell::NW, Cell::Empty, Cell::NW, Cell::Filled, Cell::Empty, Cell::Filled, Cell::Filled, ], ..Default::default() }, ); let vert_right_pair = ( Card { #[rustfmt::skip] cells: [ Cell::Empty, Cell::Empty, Cell::Empty, Cell::Empty, Cell::NW, Cell::Empty, Cell::NW, Cell::Filled, Cell::Empty, ], ..Default::default() }, Card { #[rustfmt::skip] cells: [ Cell::Empty, Cell::Empty, Cell::NW, Cell::Empty, Cell::Empty, Cell::Filled, Cell::Empty, Cell::Empty, Cell::Filled, ], ..Default::default() }, ); let horiz_top_pair = ( Card { #[rustfmt::skip] cells: [ Cell::Empty, Cell::Empty, Cell::NW, Cell::Empty, Cell::Empty, Cell::Empty, Cell::Empty, Cell::Empty, Cell::Empty, ], ..Default::default() }, Card { #[rustfmt::skip] cells: [ Cell::Empty, Cell::Empty, Cell::Empty, Cell::Empty, Cell::NW, Cell::Filled, Cell::NW, Cell::Filled, Cell::Filled, ], ..Default::default() }, ); let horiz_bottom_pair = ( Card { #[rustfmt::skip] cells: [ Cell::Empty, Cell::Empty, Cell::NW, Cell::Empty, Cell::NW, Cell::Filled, Cell::Empty, Cell::Empty, Cell::Empty, ], ..Default::default() }, Card { #[rustfmt::skip] cells: [ Cell::Empty, Cell::Empty, Cell::Empty, Cell::Empty, Cell::Empty, Cell::Empty, Cell::NW, Cell::Filled, Cell::Filled, ], ..Default::default() }, ); // Run tests let result_vleft = tri.clone().cut(CutLine::VertLeft); assert_eq!(result_vleft, vert_left_pair); let result_vright = tri.clone().cut(CutLine::VertRight); assert_eq!(result_vright, vert_right_pair); let result_hupper = tri.clone().cut(CutLine::HorizUpper); assert_eq!(result_hupper, horiz_top_pair); let result_hlower = tri.cut(CutLine::HorizLower); assert_eq!(result_hlower, horiz_bottom_pair); } #[test] fn flip_triangle_vertical() { let tri = Card { cells: NW_TRIANGLE, ..Default::default() }; let expected = Card { cells: SW_TRIANGLE, ..Default::default() }; let result = tri.flip(FlipDir::Vertical); assert_eq!(result, expected); } #[test] fn flip_triangle_horizontal() { let tri = Card { cells: NW_TRIANGLE, ..Default::default() }; let expected = Card { cells: NE_TRIANGLE, ..Default::default() }; let result = tri.flip(FlipDir::Horizontal); assert_eq!(result, expected); } /// Test transposition on column 1 #[test] fn transpose_vertical() { let left_shape = Card { cells: OCTAGON, ..Default::default() }; // It's a small NW triangle. let right_shape = Card { #[rustfmt::skip] cells: [ Cell::Filled, Cell::Empty, Cell::Empty, Cell::SE, Cell::Empty, Cell::NW, Cell::Empty, Cell::NW, Cell::Filled, ], ..Default::default() }; let expected_left = Card { #[rustfmt::skip] cells: [ Cell::Filled, Cell::Filled, Cell::NE, Cell::SE, Cell::Filled, Cell::Filled, Cell::Empty, Cell::Filled, Cell::SE, ], ..Default::default() }; let expected_right = Card { #[rustfmt::skip] cells: [ Cell::NW, Cell::Empty, Cell::Empty, Cell::Filled, Cell::Empty, Cell::NW, Cell::SW, Cell::NW, Cell::Filled, ], ..Default::default() }; let (res_left, res_right) = left_shape.transpose( right_shape, TransposeSelection::Column(TransposeIndex::First), ); assert_eq!(res_left, expected_left); assert_eq!(res_right, expected_right); } /// Test transposition on row 3 #[test] fn transpose_horizontal() { let left_shape = Card { cells: OCTAGON, ..Default::default() }; let right_shape = Card { #[rustfmt::skip] cells: [ Cell::Filled, Cell::Empty, Cell::Empty, Cell::SE, Cell::Empty, Cell::NW, Cell::Empty, Cell::NW, Cell::Filled, ], ..Default::default() }; let expected_left = Card { #[rustfmt::skip] cells: [ Cell::NW, Cell::Filled, Cell::NE, Cell::Filled, Cell::Filled, Cell::Filled, Cell::Empty, Cell::NW, Cell::Filled, ], ..Default::default() }; let expected_right = Card { #[rustfmt::skip] cells: [ Cell::Filled, Cell::Empty, Cell::Empty, Cell::SE, Cell::Empty, Cell::NW, Cell::SW, Cell::Filled, Cell::SE, ], ..Default::default() }; let (res_left, res_right) = left_shape.transpose(right_shape, TransposeSelection::Row(TransposeIndex::Third)); assert_eq!(res_left, expected_left); assert_eq!(res_right, expected_right); } #[test] fn rotate_clockwise() { let shape = Card { cells: NW_TRIANGLE, ..Default::default() }; let expected = Card { cells: NE_TRIANGLE, ..Default::default() }; let result = shape.rotate(RotationDir::Clockwise); assert_eq!(result, expected); } #[test] fn rotate_counter_clockwise() { let shape = Card { cells: NE_TRIANGLE, ..Default::default() }; let expected = Card { cells: NW_TRIANGLE, ..Default::default() }; let result = shape.rotate(RotationDir::CounterClockwise); assert_eq!(result, expected); } }