use crate::sim::{Board, Direction, Handedness, Orientation, TileKind, mirror_dir, mirrored_tile};
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum Symmetry {
#[default]
Off,
LeftRight,
TopBottom,
Both,
HalfTurn,
}
impl Symmetry {
pub(super) const ALL: [Symmetry; 5] = [
Symmetry::Off,
Symmetry::LeftRight,
Symmetry::TopBottom,
Symmetry::Both,
Symmetry::HalfTurn,
];
pub(super) fn flips(self) -> &'static [(bool, bool)] {
match self {
Symmetry::Off => &[],
Symmetry::LeftRight => &[(true, false)],
Symmetry::TopBottom => &[(false, true)],
Symmetry::Both => &[(true, false), (false, true), (true, true)],
Symmetry::HalfTurn => &[(true, true)],
}
}
pub(super) fn moves_off_centre(self, odd_x: bool, odd_y: bool) -> bool {
self.flips()
.iter()
.any(|&(flip_x, flip_y)| (flip_x && odd_x) || (flip_y && odd_y))
}
pub(super) fn index(self) -> usize {
match self {
Symmetry::Off => 0,
Symmetry::LeftRight => 1,
Symmetry::TopBottom => 2,
Symmetry::Both => 3,
Symmetry::HalfTurn => 4,
}
}
pub(super) fn seat(self, (flip_x, flip_y): (bool, bool), owner: u8) -> u8 {
let pair = |owner: u8| match usize::from(owner) < crate::sim::MAX_PLAYERS {
true => owner ^ 1,
false => owner,
};
match self {
Symmetry::Off => owner,
Symmetry::LeftRight | Symmetry::TopBottom | Symmetry::HalfTurn => pair(owner),
Symmetry::Both => match owner {
0..=3 => owner ^ ((u8::from(flip_x) * 2) ^ (u8::from(flip_y) * 3)),
_ if flip_y => pair(owner),
_ => owner,
},
}
}
fn images(self, board: &Board, at: (u8, u8)) -> Vec<((u8, u8), (bool, bool))> {
let mut out: Vec<((u8, u8), (bool, bool))> = Vec::new();
for &flips in self.flips() {
let image = place(board, flips, at);
if image != at && out.iter().all(|(seen, _)| *seen != image) {
out.push((image, flips));
}
}
out
}
}
fn place(board: &Board, (flip_x, flip_y): (bool, bool), at: (u8, u8)) -> (u8, u8) {
Orientation {
flip_x,
flip_y,
roll: 0,
}
.place(at, board.width(), board.height())
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(super) enum Unfair {
Line,
Across,
SeatTwice,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(super) enum Layer {
Both,
Ground,
Creatures,
}
pub(super) fn unfair(
board: &Board,
symmetry: Symmetry,
at: (u8, u8),
layer: Layer,
) -> Option<Unfair> {
let ground = || unfair_ground(board, symmetry, at);
let creatures = || unfair_creatures(board, symmetry, at);
match layer {
Layer::Both => ground().or_else(creatures),
Layer::Ground => ground(),
Layer::Creatures => creatures(),
}
}
fn copies(
board: &Board,
symmetry: Symmetry,
at: (u8, u8),
) -> impl Iterator<Item = ((bool, bool), (u8, u8))> + '_ {
std::iter::once((false, false))
.chain(symmetry.flips().iter().copied())
.map(move |flips| (flips, place(board, flips, at)))
}
fn unfair_ground(board: &Board, symmetry: Symmetry, at: (u8, u8)) -> Option<Unfair> {
let (x, y) = at;
let ground = board.tile_at(x, y);
let turned_on_line = || {
copies(board, symmetry, at)
.any(|((fx, fy), image)| image == at && mirrored_tile(ground, fx, fy) != ground)
};
match ground {
TileKind::Castle(owner) => {
let seated: Vec<((u8, u8), u8)> = copies(board, symmetry, at)
.map(|(flips, image)| {
let seat = match flips {
(false, false) => owner,
flips => symmetry.seat(flips, owner),
};
(image, seat)
})
.collect();
let line = seated
.iter()
.any(|&(image, seat)| image == at && seat != owner);
let twice = seated.iter().enumerate().any(|(i, &(a, seat))| {
seated[..i]
.iter()
.any(|&(b, other)| a != b && seat == other)
});
match (line, twice) {
(true, _) => Some(Unfair::Line),
(false, true) => Some(Unfair::SeatTwice),
(false, false) => None,
}
}
TileKind::Spawner(_) => turned_on_line().then_some(Unfair::Line),
TileKind::Turnstile { .. } => turned_on_line().then_some(Unfair::Across),
TileKind::Empty | TileKind::Rock | TileKind::Kelp | TileKind::Pool => None,
}
}
fn unfair_creatures(board: &Board, symmetry: Symmetry, at: (u8, u8)) -> Option<Unfair> {
let tile = board.index_of(at.0, at.1);
let crabs = board
.crabs()
.iter()
.filter(|c| c.tile == tile)
.map(|c| c.dir);
let gulls = board
.gulls()
.iter()
.filter(|g| g.tile == tile)
.map(|g| g.dir);
let mut facings = crabs.chain(gulls);
facings
.any(|dir| {
copies(board, symmetry, at)
.any(|((fx, fy), image)| image == at && mirror_dir(dir, fx, fy) != dir)
})
.then_some(Unfair::Across)
}
pub(super) fn mirror_tile(
board: &mut Board,
symmetry: Symmetry,
seats: bool,
at: (u8, u8),
) -> Vec<(u8, u8)> {
let (x, y) = at;
let tile = board.index_of(x, y);
let ground = board.tile_at(x, y);
let crabs: Vec<_> = board
.crabs()
.iter()
.filter(|c| c.tile == tile)
.copied()
.collect();
let gulls: Vec<Direction> = board
.gulls()
.iter()
.filter(|g| g.tile == tile)
.map(|g| g.dir)
.collect();
let images = symmetry.images(board, at);
for &((ix, iy), (flip_x, flip_y)) in &images {
let kind = match mirrored_tile(ground, flip_x, flip_y) {
TileKind::Castle(owner) if seats => {
TileKind::Castle(symmetry.seat((flip_x, flip_y), owner))
}
kind @ (TileKind::Castle(_)
| TileKind::Empty
| TileKind::Rock
| TileKind::Spawner(_)
| TileKind::Turnstile { .. }
| TileKind::Kelp
| TileKind::Pool) => kind,
};
board.remove_crabs_at(ix, iy);
board.remove_gulls_at(ix, iy);
board.set_tile(ix, iy, kind);
for crab in &crabs {
let handed = match (flip_x ^ flip_y, crab.handed) {
(false, handed) => handed,
(true, Handedness::Left) => Handedness::Right,
(true, Handedness::Right) => Handedness::Left,
};
let dir = mirror_dir(crab.dir, flip_x, flip_y);
board.spawn_crab(ix, iy, dir, handed, crab.kind);
}
for &dir in &gulls {
if board.gull_may_stand(ix, iy) {
board.spawn_gull(ix, iy, mirror_dir(dir, flip_x, flip_y));
}
}
}
images.into_iter().map(|(image, _)| image).collect()
}
pub(super) fn mirror_wall(board: &mut Board, symmetry: Symmetry, at: (u8, u8), dir: Direction) {
let present = board.wall_at(at.0, at.1, dir);
for &(flip_x, flip_y) in symmetry.flips() {
let (ix, iy) = place(board, (flip_x, flip_y), at);
let side = mirror_dir(dir, flip_x, flip_y);
if ((ix, iy), side) != (at, dir) {
board.set_wall(ix, iy, side, present);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sim::{CrabKind, Spawner};
#[test]
fn the_seat_maps_compose_like_the_mirrors() {
for symmetry in Symmetry::ALL {
for &a in symmetry.flips() {
for owner in 0..crate::sim::MAX_PLAYERS as u8 {
let back = symmetry.seat(a, symmetry.seat(a, owner));
assert_eq!(back, owner, "{symmetry:?} {a:?} undoes itself");
for &b in symmetry.flips() {
let both = (a.0 ^ b.0, a.1 ^ b.1);
let composed = match both {
(false, false) => owner,
flips => symmetry.seat(flips, owner),
};
assert_eq!(
symmetry.seat(b, symmetry.seat(a, owner)),
composed,
"{symmetry:?} {a:?} then {b:?} on {owner}"
);
}
}
}
}
let mut seats: Vec<u8> = Symmetry::Both
.flips()
.iter()
.map(|&flips| Symmetry::Both.seat(flips, 0))
.collect();
seats.sort_unstable();
assert_eq!(seats, [1, 2, 3]);
}
#[test]
fn a_tile_lands_on_every_copy_mirrored() {
let mut board = Board::new(6, 5, 1);
board.set_tile(0, 0, TileKind::Castle(0));
mirror_tile(&mut board, Symmetry::Both, true, (0, 0));
assert_eq!(board.tile_at(5, 0), TileKind::Castle(2));
assert_eq!(board.tile_at(0, 4), TileKind::Castle(3));
assert_eq!(board.tile_at(5, 4), TileKind::Castle(1));
let hole = |dir| TileKind::Spawner(Spawner { dir, period: 60 });
board.set_tile(1, 1, hole(Direction::Right));
mirror_tile(&mut board, Symmetry::LeftRight, true, (1, 1));
assert_eq!(board.tile_at(4, 1), hole(Direction::Left));
board.spawn_crab(2, 3, Direction::Up, Handedness::Left, CrabKind::Giant);
mirror_tile(&mut board, Symmetry::TopBottom, true, (2, 3));
let copy = board
.crabs()
.iter()
.find(|c| c.tile == board.index_of(2, 1))
.expect("the crab's copy");
assert_eq!(
(copy.dir, copy.handed, copy.kind),
(Direction::Down, Handedness::Right, CrabKind::Giant)
);
let mut stage = Board::new(6, 5, 1);
stage.set_tile(0, 2, TileKind::Castle(0));
mirror_tile(&mut stage, Symmetry::HalfTurn, false, (0, 2));
assert_eq!(stage.tile_at(5, 2), TileKind::Castle(0));
}
#[test]
fn the_line_mirrors_walls_and_refuses_unfair_tiles() {
let mut board = Board::new(9, 7, 1);
board.set_wall(4, 2, Direction::Right, true);
mirror_wall(&mut board, Symmetry::LeftRight, (4, 2), Direction::Right);
assert!(board.wall_at(4, 2, Direction::Left), "the other side");
board.set_wall(4, 3, Direction::Up, true);
mirror_wall(&mut board, Symmetry::HalfTurn, (4, 3), Direction::Up);
assert!(board.wall_at(4, 3, Direction::Down), "round the centre");
board.set_tile(4, 1, TileKind::Castle(0));
assert_eq!(
unfair(&board, Symmetry::LeftRight, (4, 1), Layer::Both),
Some(Unfair::Line)
);
assert_eq!(unfair(&board, Symmetry::Off, (4, 1), Layer::Both), None);
board.set_tile(3, 1, TileKind::Castle(0));
assert_eq!(
unfair(&board, Symmetry::LeftRight, (3, 1), Layer::Both),
None
);
board.set_tile(4, 0, TileKind::Castle(4));
assert_eq!(unfair(&board, Symmetry::Both, (4, 0), Layer::Both), None);
board.set_tile(2, 0, TileKind::Castle(4));
assert_eq!(
unfair(&board, Symmetry::Both, (2, 0), Layer::Both),
Some(Unfair::SeatTwice)
);
let mut even = Board::new(12, 9, 1);
for x in 0..12 {
even.set_tile(x, 0, TileKind::Castle(5));
assert!(
unfair(&even, Symmetry::Both, (x, 0), Layer::Both).is_some(),
"x {x}"
);
}
let hole = |dir| TileKind::Spawner(Spawner { dir, period: 60 });
board.set_tile(4, 5, hole(Direction::Right));
assert_eq!(
unfair(&board, Symmetry::LeftRight, (4, 5), Layer::Both),
Some(Unfair::Line)
);
board.set_tile(4, 5, hole(Direction::Down));
assert_eq!(
unfair(&board, Symmetry::LeftRight, (4, 5), Layer::Both),
None
);
assert_eq!(
unfair(&board, Symmetry::TopBottom, (4, 5), Layer::Both),
None,
"off its line"
);
}
#[test]
fn the_line_refuses_logs_and_creatures_facing_across() {
let mut board = Board::new(9, 7, 1);
board.set_tile(4, 1, TileKind::Turnstile { next_right: true });
assert_eq!(
unfair(&board, Symmetry::LeftRight, (4, 1), Layer::Both),
Some(Unfair::Across)
);
assert_eq!(
unfair(&board, Symmetry::HalfTurn, (4, 3), Layer::Both),
None
);
board.set_tile(4, 3, TileKind::Turnstile { next_right: true });
assert_eq!(
unfair(&board, Symmetry::HalfTurn, (4, 3), Layer::Both),
None
);
assert_eq!(
unfair(&board, Symmetry::LeftRight, (3, 1), Layer::Both),
None
);
board.spawn_crab(4, 4, Direction::Right, Handedness::Left, CrabKind::Common);
assert_eq!(
unfair(&board, Symmetry::LeftRight, (4, 4), Layer::Both),
Some(Unfair::Across)
);
assert_eq!(
unfair(&board, Symmetry::TopBottom, (4, 4), Layer::Both),
None
);
board.remove_crabs_at(4, 4);
board.spawn_crab(4, 4, Direction::Down, Handedness::Left, CrabKind::Common);
assert_eq!(
unfair(&board, Symmetry::LeftRight, (4, 4), Layer::Both),
None
);
board.spawn_gull(4, 5, Direction::Left);
assert_eq!(
unfair(&board, Symmetry::LeftRight, (4, 5), Layer::Both),
Some(Unfair::Across)
);
board.set_tile(
4,
6,
TileKind::Spawner(Spawner {
dir: Direction::Right,
period: 60,
}),
);
board.spawn_crab(4, 6, Direction::Up, Handedness::Left, CrabKind::Common);
assert_eq!(
unfair(&board, Symmetry::LeftRight, (4, 6), Layer::Both),
Some(Unfair::Line)
);
assert_eq!(
unfair(&board, Symmetry::LeftRight, (4, 6), Layer::Creatures),
None
);
}
#[test]
fn the_line_keeps_its_tile_and_walls_follow() {
let mut board = Board::new(5, 5, 1);
board.set_tile(2, 0, TileKind::Castle(4));
mirror_tile(&mut board, Symmetry::Both, true, (2, 0));
assert_eq!(board.tile_at(2, 0), TileKind::Castle(4), "its own copy");
assert_eq!(board.tile_at(2, 4), TileKind::Castle(5));
board.set_wall(0, 0, Direction::Right, true);
mirror_wall(&mut board, Symmetry::Both, (0, 0), Direction::Right);
assert!(board.wall_at(4, 0, Direction::Left));
assert!(board.wall_at(0, 4, Direction::Right));
assert!(board.wall_at(4, 4, Direction::Left));
board.set_wall(0, 0, Direction::Right, false);
mirror_wall(&mut board, Symmetry::Both, (0, 0), Direction::Right);
assert!(!board.wall_at(4, 4, Direction::Left));
}
}