Files
isospace/src/card.rs
2025-08-27 07:51:13 -07:00

901 lines
22 KiB
Rust

//! 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<Self>) {
let mut new_card = Self::default();
let mut doors: Option<Self> = 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::<Vec<Cell>>()
.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);
}
}