use super::{Tessellation, TessellationError, validate_and_apply_delta};
use crate::game_board::BoardTransform;
use crate::{Direction, TileShape};
pub static KNOWN_DIRECTIONS: [Direction; 4] = [
Direction::Left,
Direction::Up,
Direction::Right,
Direction::Down,
];
pub static TILE_SHAPES: [TileShape; 1] = [TileShape::Square];
pub const ROTATION_STEP_DEGREES: u16 = 90;
pub const fn tile_shape_from_pos() -> TileShape { TileShape::Square }
pub const fn is_rigid_translation(_dx: i64, _dy: i64) -> bool { true }
pub fn transformed_raw(transform: BoardTransform, x: i64, y: i64) -> (i64, i64) {
match transform {
BoardTransform::MirrorHorizontal => (x, -y),
BoardTransform::MirrorVertical => (-x, y),
BoardTransform::RotateClockwise => (-y, x),
BoardTransform::RotateCounterClockwise => (y, -x),
}
}
pub fn neighbor_checked(
x: usize, y: usize, to: Direction, width: usize, height: usize,
) -> Result<(usize, usize), TessellationError> {
let dx: i8;
let dy: i8;
match to {
Direction::Left => {
dx = -1;
dy = 0;
}
Direction::Up => {
dx = 0;
dy = -1;
}
Direction::Right => {
dx = 1;
dy = 0;
}
Direction::Down => {
dx = 0;
dy = 1;
}
_ => {
return Err(TessellationError::IllegalDirection(Tessellation::Sokoban, to));
}
}
validate_and_apply_delta(x, y, to, dx, dy, width, height)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Size2D;
mod test_transforms {
use super::*;
type Expectation = ((i64, i64), [(i64, i64); 4]);
const TRANSFORMS: [BoardTransform; 4] = [
BoardTransform::MirrorHorizontal,
BoardTransform::MirrorVertical,
BoardTransform::RotateClockwise,
BoardTransform::RotateCounterClockwise,
];
#[rustfmt::skip]
#[test]
fn it_maps_the_first_two_rows_under_every_transform() {
let expectations: [Expectation; 8] = [
((0, 0), [(0, 0), ( 0, 0), ( 0, 0), ( 0, 0)]),
((1, 0), [(1, 0), (-1, 0), ( 0, 1), ( 0, -1)]),
((2, 0), [(2, 0), (-2, 0), ( 0, 2), ( 0, -2)]),
((3, 0), [(3, 0), (-3, 0), ( 0, 3), ( 0, -3)]),
((0, 1), [(0, -1), ( 0, 1), (-1, 0), ( 1, 0)]),
((1, 1), [(1, -1), (-1, 1), (-1, 1), ( 1, -1)]),
((2, 1), [(2, -1), (-2, 1), (-1, 2), ( 1, -2)]),
((3, 1), [(3, -1), (-3, 1), (-1, 3), ( 1, -3)]),
];
for ((x, y), wanted) in expectations {
for (transform, want) in TRANSFORMS.iter().zip(wanted) {
assert_eq!(
transformed_raw(*transform, x, y), want,
"{transform:?} at ({x}, {y})"
);
}
}
}
#[rustfmt::skip]
#[test]
fn it_maps_negative_coordinates() {
let expectations: [Expectation; 8] = [
(( 0, -1), [( 0, 1), ( 0, -1), ( 1, 0), (-1, 0)]),
(( 1, -1), [( 1, 1), (-1, -1), ( 1, 1), (-1, -1)]),
((-1, 0), [(-1, 0), ( 1, 0), ( 0, -1), ( 0, 1)]),
((-1, -1), [(-1, 1), ( 1, -1), ( 1, -1), (-1, 1)]),
((-2, -1), [(-2, 1), ( 2, -1), ( 1, -2), (-1, 2)]),
((-1, -2), [(-1, 2), ( 1, -2), ( 2, -1), (-2, 1)]),
((-2, -2), [(-2, 2), ( 2, -2), ( 2, -2), (-2, 2)]),
((-3, -2), [(-3, 2), ( 3, -2), ( 2, -3), (-2, 3)]),
];
for ((x, y), wanted) in expectations {
for (transform, want) in TRANSFORMS.iter().zip(wanted) {
assert_eq!(
transformed_raw(*transform, x, y), want,
"{transform:?} at ({x}, {y})"
);
}
}
}
#[test]
fn it_returns_to_the_start_only_after_a_full_turn() {
let steps = 360 / usize::from(ROTATION_STEP_DEGREES);
assert_eq!(steps, 4);
let witness = (2_i64, 1_i64);
let mut point = witness;
for step in 1..steps {
point = transformed_raw(BoardTransform::RotateClockwise, point.0, point.1);
assert_ne!(point, witness, "home again after {step} of {steps} steps");
}
for y in -6_i64..=6 {
for x in -6_i64..=6 {
let mut point = (x, y);
for _ in 0..steps {
point = transformed_raw(BoardTransform::RotateClockwise, point.0, point.1);
}
assert_eq!(point, (x, y), "({x}, {y}) after a full turn");
}
}
}
#[test]
fn counter_clockwise_inverts_clockwise_and_mirrors_are_involutions() {
for y in -6_i64..=6 {
for x in -6_i64..=6 {
let clockwise = transformed_raw(BoardTransform::RotateClockwise, x, y);
assert_eq!(
transformed_raw(
BoardTransform::RotateCounterClockwise,
clockwise.0,
clockwise.1
),
(x, y),
"CCW . CW at ({x}, {y})"
);
for mirror in [
BoardTransform::MirrorHorizontal,
BoardTransform::MirrorVertical,
] {
let once = transformed_raw(mirror, x, y);
assert_eq!(
transformed_raw(mirror, once.0, once.1),
(x, y),
"{mirror:?} twice at ({x}, {y})"
);
}
}
}
}
#[test]
fn it_reports_every_translation_as_rigid() {
let rigid = (-3_i64..=3)
.flat_map(|dy| (-3_i64..=3).map(move |dx| (dx, dy)))
.filter(|(dx, dy)| is_rigid_translation(*dx, *dy))
.count();
assert_eq!(rigid, 49);
}
#[test]
fn it_rotates_every_direction_by_one_step() {
let relabelled = [
(Direction::Up, Direction::Right),
(Direction::Right, Direction::Down),
(Direction::Down, Direction::Left),
(Direction::Left, Direction::Up),
];
for y in 0_usize..4 {
for x in 0_usize..4 {
for (direction, rotated) in relabelled {
assert_rotated_direction(x, y, direction, rotated);
}
}
}
}
}
#[cfg(test)]
fn assert_rotated_direction(x: usize, y: usize, direction: Direction, rotated: Direction) {
const ORIGIN: usize = 64;
const SHIFT: i64 = 128;
const SIZE: usize = 256;
let turned = |(cx, cy): (usize, usize)| {
let (tx, ty) = transformed_raw(
BoardTransform::RotateClockwise,
i64::try_from(cx).unwrap(),
i64::try_from(cy).unwrap(),
);
(
usize::try_from(tx + SHIFT).unwrap(),
usize::try_from(ty + SHIFT).unwrap(),
)
};
let (column, row) = (x + ORIGIN, y + ORIGIN);
let neighbor = neighbor_checked(column, row, direction, SIZE, SIZE)
.expect("the source neighbour is on board");
let source = turned((column, row));
assert_eq!(
neighbor_checked(source.0, source.1, rotated, SIZE, SIZE),
Ok(turned(neighbor)),
"a clockwise turn should carry {direction:?} onto {rotated:?}, at ({column}, {row})"
);
}
#[rustfmt::skip]
#[test]
fn test_top_left() {
let size = Size2D::new(10, 10);
let width = size.width;
let height = size.height;
let row: usize = 0;
let column: usize = 0;
let expectations = [
(Direction::Left , Err(TessellationError::OffBoard(column, row, Direction::Left))),
(Direction::Right , Ok((1, 0))),
(Direction::Up , Err(TessellationError::OffBoard(column, row, Direction::Up))),
(Direction::Down , Ok((0, 1))),
(Direction::NorthWest, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::NorthWest))),
(Direction::NorthEast, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::NorthEast))),
(Direction::SouthWest, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::SouthWest))),
(Direction::SouthEast, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::SouthEast))),
];
for (direction, result) in &expectations {
assert_eq!(
neighbor_checked(column, row, *direction, width, height),
*result
);
}
}
#[rustfmt::skip]
#[test]
fn test_top_right() {
let size = Size2D::new(10, 10);
let width = size.width;
let height = size.height;
let row: usize = 0;
let column: usize = 9;
let expectations = [
(Direction::Left , Ok((8, 0))),
(Direction::Right , Err(TessellationError::OffBoard(column, row, Direction::Right))),
(Direction::Up , Err(TessellationError::OffBoard(column, row, Direction::Up))),
(Direction::Down , Ok((9, 1))),
(Direction::NorthWest, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::NorthWest))),
(Direction::NorthEast, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::NorthEast))),
(Direction::SouthWest, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::SouthWest))),
(Direction::SouthEast, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::SouthEast))),
];
for (direction, result) in &expectations {
assert_eq!(
neighbor_checked(column, row, *direction, width, height),
*result
);
}
}
#[rustfmt::skip]
#[test]
fn test_bottom_left() {
let size = Size2D::new(10, 10);
let width = size.width;
let height = size.height;
let row: usize = 9;
let column: usize = 0;
let expectations = [
(Direction::Left , Err(TessellationError::OffBoard(column, row, Direction::Left))),
(Direction::Right , Ok((1, 9))),
(Direction::Up , Ok((0, 8))),
(Direction::Down , Err(TessellationError::OffBoard(column, row, Direction::Down))),
(Direction::NorthWest, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::NorthWest))),
(Direction::NorthEast, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::NorthEast))),
(Direction::SouthWest, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::SouthWest))),
(Direction::SouthEast, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::SouthEast))),
];
for (direction, result) in &expectations {
assert_eq!(
neighbor_checked(column, row, *direction, width, height),
*result
);
}
}
#[rustfmt::skip]
#[test]
fn test_bottom_right() {
let size = Size2D::new(10, 10);
let width = size.width;
let height = size.height;
let row: usize = 9;
let column: usize = 9;
let expectations = [
(Direction::Left , Ok((8, 9))),
(Direction::Right , Err(TessellationError::OffBoard(column, row, Direction::Right))),
(Direction::Up , Ok((9, 8))),
(Direction::Down , Err(TessellationError::OffBoard(column, row, Direction::Down))),
(Direction::NorthWest, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::NorthWest))),
(Direction::NorthEast, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::NorthEast))),
(Direction::SouthWest, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::SouthWest))),
(Direction::SouthEast, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::SouthEast))),
];
for (direction, result) in &expectations {
assert_eq!(
neighbor_checked(column, row, *direction, width, height),
*result
);
}
}
#[rustfmt::skip]
#[test]
fn test_middle_top() {
let size = Size2D::new(10, 10);
let width = size.width;
let height = size.height;
let row: usize = 0;
let column: usize = 5;
let expectations = [
(Direction::Left , Ok((4, 0))),
(Direction::Right , Ok((6, 0))),
(Direction::Up , Err(TessellationError::OffBoard(column, row, Direction::Up))),
(Direction::Down , Ok((5, 1))),
(Direction::NorthWest, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::NorthWest))),
(Direction::NorthEast, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::NorthEast))),
(Direction::SouthWest, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::SouthWest))),
(Direction::SouthEast, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::SouthEast))),
];
for (direction, result) in &expectations {
assert_eq!(
neighbor_checked(column, row, *direction, width, height),
*result
);
}
}
#[rustfmt::skip]
#[test]
fn test_middle_bottom() {
let size = Size2D::new(10, 10);
let width = size.width;
let height = size.height;
let row: usize = 9;
let column: usize = 5;
let expectations = [
(Direction::Left , Ok((4, 9))),
(Direction::Right , Ok((6, 9))),
(Direction::Up , Ok((5, 8))),
(Direction::Down , Err(TessellationError::OffBoard(column, row, Direction::Down))),
(Direction::NorthWest, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::NorthWest))),
(Direction::NorthEast, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::NorthEast))),
(Direction::SouthWest, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::SouthWest))),
(Direction::SouthEast, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::SouthEast))),
];
for (direction, result) in &expectations {
assert_eq!(
neighbor_checked(column, row, *direction, width, height),
*result
);
}
}
#[rustfmt::skip]
#[test]
fn test_middle_left() {
let size = Size2D::new(10, 10);
let width = size.width;
let height = size.height;
let row: usize = 5;
let column: usize = 0;
let expectations = [
(Direction::Left , Err(TessellationError::OffBoard(column, row, Direction::Left))),
(Direction::Right , Ok((1, 5))),
(Direction::Up , Ok((0, 4))),
(Direction::Down , Ok((0, 6))),
(Direction::NorthWest, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::NorthWest))),
(Direction::NorthEast, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::NorthEast))),
(Direction::SouthWest, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::SouthWest))),
(Direction::SouthEast, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::SouthEast))),
];
for (direction, result) in &expectations {
assert_eq!(
neighbor_checked(column, row, *direction, width, height),
*result
);
}
}
#[rustfmt::skip]
#[test]
fn test_middle_right() {
let size = Size2D::new(10, 10);
let width = size.width;
let height = size.height;
let row: usize = 5;
let column: usize = 9;
let expectations = [
(Direction::Left , Ok((8, 5))),
(Direction::Right , Err(TessellationError::OffBoard(column, row, Direction::Right))),
(Direction::Up , Ok((9, 4))),
(Direction::Down , Ok((9, 6))),
(Direction::NorthWest, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::NorthWest))),
(Direction::NorthEast, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::NorthEast))),
(Direction::SouthWest, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::SouthWest))),
(Direction::SouthEast, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::SouthEast))),
];
for (direction, result) in &expectations {
assert_eq!(
neighbor_checked(column, row, *direction, width, height),
*result
);
}
}
#[rustfmt::skip]
#[test]
fn test_board1x1() {
let size = Size2D::new(1, 1);
let width = size.width;
let height = size.height;
let row: usize = 0;
let column: usize = 0;
let expectations = [
(Direction::Left , Err(TessellationError::OffBoard(column, row, Direction::Left))),
(Direction::Right , Err(TessellationError::OffBoard(column, row, Direction::Right))),
(Direction::Up , Err(TessellationError::OffBoard(column, row, Direction::Up))),
(Direction::Down , Err(TessellationError::OffBoard(column, row, Direction::Down))),
(Direction::NorthWest, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::NorthWest))),
(Direction::NorthEast, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::NorthEast))),
(Direction::SouthWest, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::SouthWest))),
(Direction::SouthEast, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::SouthEast))),
];
for (direction, result) in &expectations {
assert_eq!(
neighbor_checked(column, row, *direction, width, height),
*result
);
}
}
#[rustfmt::skip]
#[test]
fn test_board2x1_left() {
let size = Size2D::new(2, 1);
let width = size.width;
let height = size.height;
let row: usize = 0;
let column: usize = 0;
let expectations = [
(Direction::Left , Err(TessellationError::OffBoard(column, row, Direction::Left))),
(Direction::Right , Ok((1, 0))),
(Direction::Up , Err(TessellationError::OffBoard(column, row, Direction::Up))),
(Direction::Down , Err(TessellationError::OffBoard(column, row, Direction::Down))),
(Direction::NorthWest, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::NorthWest))),
(Direction::NorthEast, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::NorthEast))),
(Direction::SouthWest, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::SouthWest))),
(Direction::SouthEast, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::SouthEast))),
];
for (direction, result) in &expectations {
assert_eq!(
neighbor_checked(column, row, *direction, width, height),
*result
);
}
}
#[rustfmt::skip]
#[test]
fn test_boar_d2x1_right() {
let size = Size2D::new(2, 1);
let width = size.width;
let height = size.height;
let row: usize = 0;
let column: usize = 1;
let expectations = [
(Direction::Left , Ok((0, 0))),
(Direction::Right , Err(TessellationError::OffBoard(column, row, Direction::Right))),
(Direction::Up , Err(TessellationError::OffBoard(column, row, Direction::Up))),
(Direction::Down , Err(TessellationError::OffBoard(column, row, Direction::Down))),
(Direction::NorthWest, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::NorthWest))),
(Direction::NorthEast, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::NorthEast))),
(Direction::SouthWest, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::SouthWest))),
(Direction::SouthEast, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::SouthEast))),
];
for (direction, result) in &expectations {
assert_eq!(
neighbor_checked(column, row, *direction, width, height),
*result
);
}
}
#[rustfmt::skip]
#[test]
fn test_1x2_top() {
let size = Size2D::new(1, 2);
let width = size.width;
let height = size.height;
let row: usize = 0;
let column: usize = 0;
let expectations = [
(Direction::Left , Err(TessellationError::OffBoard(column, row, Direction::Left))),
(Direction::Right , Err(TessellationError::OffBoard(column, row, Direction::Right))),
(Direction::Up , Err(TessellationError::OffBoard(column, row, Direction::Up))),
(Direction::Down , Ok((0, 1))),
(Direction::NorthWest, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::NorthWest))),
(Direction::NorthEast, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::NorthEast))),
(Direction::SouthWest, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::SouthWest))),
(Direction::SouthEast, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::SouthEast))),
];
for (direction, result) in &expectations {
assert_eq!(
neighbor_checked(column, row, *direction, width, height),
*result
);
}
}
#[rustfmt::skip]
#[test]
fn test_1x2_bottom() {
let size = Size2D::new(1, 2);
let width = size.width;
let height = size.height;
let row: usize = 1;
let column: usize = 0;
let expectations = [
(Direction::Left , Err(TessellationError::OffBoard(column, row, Direction::Left))),
(Direction::Right , Err(TessellationError::OffBoard(column, row, Direction::Right))),
(Direction::Up , Ok((0, 0))),
(Direction::Down , Err(TessellationError::OffBoard(column, row, Direction::Down))),
(Direction::NorthWest, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::NorthWest))),
(Direction::NorthEast, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::NorthEast))),
(Direction::SouthWest, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::SouthWest))),
(Direction::SouthEast, Err(TessellationError::IllegalDirection(Tessellation::Sokoban, Direction::SouthEast))),
];
for (direction, result) in &expectations {
assert_eq!(
neighbor_checked(column, row, *direction, width, height),
*result
);
}
}
}