diff --git a/src/game/board.rs b/src/game/board.rs index cc903fa..a533415 100644 --- a/src/game/board.rs +++ b/src/game/board.rs @@ -1,3 +1,7 @@ +#[cfg(test)] +#[path = "tests/board.rs"] +mod tests; + use crate::game::{ color::Color, piece::{Piece}, @@ -10,6 +14,7 @@ pub struct Board { } impl Board { + /// Creates a new empty board with all cells set to None. pub fn new() -> Self { Board { board: [[None; BOARD_SIZE]; BOARD_SIZE], diff --git a/src/game/coordinate.rs b/src/game/coordinate.rs index be94287..fc1a5cc 100644 --- a/src/game/coordinate.rs +++ b/src/game/coordinate.rs @@ -1,3 +1,7 @@ +#[cfg(test)] +#[path = "tests/coordinate.rs"] +mod tests; + use crate::game::rotation::Rotation; #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] diff --git a/src/game/gamerulelogic.rs b/src/game/gamerulelogic.rs index 4a9e695..7484e53 100644 --- a/src/game/gamerulelogic.rs +++ b/src/game/gamerulelogic.rs @@ -1,3 +1,8 @@ +#[cfg(test)] +#[path = "tests/gamerulelogic.rs"] +mod tests; + + use std::collections::HashSet; use crate::{game::{ board::Board, @@ -305,118 +310,4 @@ pub fn is_valid_move(gamestate: &GameState, m: &Move) -> bool { } true -} - -#[cfg(test)] -mod tests { - use crate::game::{ - board::Board, - color::Color, - gamestate::GameState, - piece::PieceType, - r#move::Move, - rotation::Rotation - }; - use crate::game::coordinate::Coordinate; - - use super::{get_possible_moves_for_piece, is_valid_move}; - - fn blue_turn_state_with_board(board: Board) -> GameState { - GameState::new( - PieceType::Mono, - true, - board, - 5, - 2, - Color::Blue, - vec![PieceType::Mono], - vec![], - vec![], - vec![], - ) - } - - #[test] - fn invalid_when_directly_adjacent_to_own_piece() { - let mut board = Board::new(); - board.set_cell(5, 5, Color::Blue); - - let state = blue_turn_state_with_board(board); - let m = Move { - color: Color::Blue, - piece: PieceType::Mono, - x: 6, - y: 5, - is_flipped: false, - rotation: Rotation::None, - skip: false, - }; - - assert!(!is_valid_move(&state, &m)); - } - - #[test] - fn valid_when_only_corner_contact_exists() { - let mut board = Board::new(); - board.set_cell(5, 5, Color::Blue); - - let state = blue_turn_state_with_board(board); - let m = Move { - color: Color::Blue, - piece: PieceType::Mono, - x: 6, - y: 6, - is_flipped: false, - rotation: Rotation::None, - skip: false, - }; - - assert!(is_valid_move(&state, &m)); - } - - #[test] - fn invalid_when_no_corner_contact_after_first_move() { - let mut board = Board::new(); - board.set_cell(5, 5, Color::Blue); - - let state = blue_turn_state_with_board(board); - let m = Move { - color: Color::Blue, - piece: PieceType::Mono, - x: 10, - y: 10, - is_flipped: false, - rotation: Rotation::None, - skip: false, - }; - - assert!(!is_valid_move(&state, &m)); - } - - #[test] - fn calculates_moves_where_corner_is_not_piece_origin() { - let mut board = Board::new(); - board.set_cell(5, 5, Color::Blue); - - let state = GameState::new( - PieceType::Mono, - true, - board, - 5, - 2, - Color::Blue, - vec![PieceType::PentoX], - vec![], - vec![], - vec![], - ); - - let valid_fields = vec![Coordinate { x: 6, y: 6 }]; - let moves = get_possible_moves_for_piece(&state, &PieceType::PentoX, &valid_fields); - - assert!( - moves.iter().any(|m| m.x == 6 && m.y == 5), - "expected a placement that aligns a non-origin PENTO_X tile to the corner" - ); - } -} +} \ No newline at end of file diff --git a/src/game/gamestate.rs b/src/game/gamestate.rs index 580b3a2..f3519a7 100644 --- a/src/game/gamestate.rs +++ b/src/game/gamestate.rs @@ -1,3 +1,7 @@ +#[cfg(test)] +#[path = "tests/gamestate.rs"] +mod tests; + use crate::game::{ board::Board, color::Color, diff --git a/src/game/piece.rs b/src/game/piece.rs index 394ee68..44f9394 100644 --- a/src/game/piece.rs +++ b/src/game/piece.rs @@ -1,3 +1,7 @@ +#[cfg(test)] +#[path = "tests/piece.rs"] +mod tests; + use std::{fmt, str::FromStr}; use crate::game::{ @@ -282,83 +286,4 @@ impl FromStr for PieceType { _ => Err(ParsePieceTypeError), } } -} - -#[cfg(test)] -mod tests { - use super::{Piece, PieceType}; - use crate::{game::rotation::Rotation, game::coordinate::Coordinate}; - - fn sorted_xy(coordinates: Vec) -> Vec<(isize, isize)> { - let mut xy: Vec<(isize, isize)> = coordinates - .into_iter() - .map(|coord| (coord.x, coord.y)) - .collect(); - xy.sort_unstable(); - xy - } - - #[test] - fn pento_r_get_coordinates_all_8_variants() { - let cases: Vec<(Rotation, bool, Vec<(isize, isize)>)> = vec![ - ( - Rotation::None, - false, - vec![(2, 0), (0, 1), (1, 1), (2, 1), (1, 2)], - ), - ( - Rotation::Right, - false, - vec![(2, 2), (1, 0), (1, 1), (1, 2), (0, 1)], - ), - ( - Rotation::Mirror, - false, - vec![(0, 2), (2, 1), (1, 1), (0, 1), (1, 0)], - ), - ( - Rotation::Left, - false, - vec![(0, 0), (1, 2), (1, 1), (1, 0), (2, 1)], - ), - ( - Rotation::None, - true, - vec![(0, 0), (2, 1), (1, 1), (0, 1), (1, 2)], - ), - ( - Rotation::Right, - true, - vec![(0, 2), (1, 0), (1, 1), (1, 2), (2, 1)], - ), - ( - Rotation::Mirror, - true, - vec![(2, 2), (0, 1), (1, 1), (2, 1), (1, 0)], - ), - ( - Rotation::Left, - true, - vec![(2, 0), (1, 2), (1, 1), (1, 0), (0, 1)], - ), - ]; - - for (rotation, is_flipped, expected) in cases { - let piece = Piece::new(PieceType::PentoR, rotation, is_flipped); - let actual = sorted_xy(piece.get_coordinates()); - - assert_eq!( - actual, - sorted_xy( - expected - .into_iter() - .map(|(x, y)| Coordinate::new(x, y)) - .collect() - ), - "unexpected coordinates for rotation={:?}, is_flipped={}", - rotation, - is_flipped - ); - } - } } \ No newline at end of file diff --git a/src/game/tests/board.rs b/src/game/tests/board.rs new file mode 100644 index 0000000..2dadfc6 --- /dev/null +++ b/src/game/tests/board.rs @@ -0,0 +1,100 @@ +use super::Board; +use crate::game::{color::Color, constants::BOARD_SIZE, piece::{Piece, PieceType}, rotation::Rotation}; + +#[test] +fn test_board_initialization() { + let board = Board::new(); + for row in 0..BOARD_SIZE { + for col in 0..BOARD_SIZE { + assert_eq!(board.get_cell(col, row), None); + } + } +} + +#[test] +fn test_set_and_get_cell() { + let mut board = Board::new(); + assert!(board.set_cell(3, 4, Color::Red)); + assert_eq!(board.get_cell(3, 4), Some(Color::Red)); + assert_eq!(board.set_cell(BOARD_SIZE, BOARD_SIZE, Color::Blue), false); // Out of bounds, should return false + assert_eq!(board.get_cell(BOARD_SIZE, BOARD_SIZE), None); // Out of bounds, should return None +} + +#[test] +fn test_get_colored_tiles() { + let mut board = Board::new(); + assert_eq!(board.get_colored_tiles(&Color::Red), 0); + board.set_cell(0, 0, Color::Red); + board.set_cell(1, 1, Color::Red); + board.set_cell(2, 2, Color::Blue); + assert_eq!(board.get_colored_tiles(&Color::Red), 2); + assert_eq!(board.get_colored_tiles(&Color::Blue), 1); +} + +#[test] +fn test_place_piece_unchecked_no_rotation_no_flipping() { + let mut board = Board::new(); + + let piece = Piece::new(PieceType::TetroI, Rotation::None, false); + board.place_piece_unchecked(0, 0, Color::Green, piece); + assert_eq!(board.get_cell(0, 0), Some(Color::Green)); + assert_eq!(board.get_cell(0, 1), Some(Color::Green)); + assert_eq!(board.get_cell(0, 2), Some(Color::Green)); + assert_eq!(board.get_cell(0, 3), Some(Color::Green)); +} + +#[test] +fn test_place_piece_unchecked_rotation_no_flipping() { + let mut board = Board::new(); + + let piece = Piece::new(PieceType::TetroI, Rotation::Right, false); + board.place_piece_unchecked(0, 0, Color::Green, piece); + assert_eq!(board.get_cell(0, 0), Some(Color::Green)); + assert_eq!(board.get_cell(1, 0), Some(Color::Green)); + assert_eq!(board.get_cell(2, 0), Some(Color::Green)); + assert_eq!(board.get_cell(3, 0), Some(Color::Green)); +} + +#[test] +fn test_place_piece_unchecked_no_rotation_flipping() { + let mut board = Board::new(); + + let piece = Piece::new(PieceType::TetroL, Rotation::None, true); + board.place_piece_unchecked(0, 0, Color::Green, piece); + assert_eq!(board.get_cell(1, 0), Some(Color::Green)); + assert_eq!(board.get_cell(1, 1), Some(Color::Green)); + assert_eq!(board.get_cell(1, 2), Some(Color::Green)); + assert_eq!(board.get_cell(0, 2), Some(Color::Green)); +} + +#[test] +fn test_place_piece_unchecked_rotation_flipping() { + let mut board = Board::new(); + + let piece = Piece::new(PieceType::TetroL, Rotation::Left, true); + board.place_piece_unchecked(0, 0, Color::Green, piece); + assert_eq!(board.get_cell(0, 0), Some(Color::Green)); + assert_eq!(board.get_cell(0, 1), Some(Color::Green)); + assert_eq!(board.get_cell(1, 1), Some(Color::Green)); + assert_eq!(board.get_cell(2, 1), Some(Color::Green)); +} + +#[test] +fn test_place_piece_success() { + let mut board = Board::new(); + + let piece = Piece::new(PieceType::TetroI, Rotation::None, false); + assert!(board.place_piece(0, 0, Color::Green, piece)); + assert_eq!(board.get_cell(0, 0), Some(Color::Green)); + assert_eq!(board.get_cell(0, 1), Some(Color::Green)); + assert_eq!(board.get_cell(0, 2), Some(Color::Green)); + assert_eq!(board.get_cell(0, 3), Some(Color::Green)); +} + +#[test] +fn test_place_piece_failure_out_of_bounds() { + let mut board = Board::new(); + + let piece = Piece::new(PieceType::TetroI, Rotation::None, false); + assert!(!board.place_piece(BOARD_SIZE - 1, BOARD_SIZE - 1, Color::Green, piece)); // This should fail as it goes out of bounds +} \ No newline at end of file diff --git a/src/game/tests/coordinate.rs b/src/game/tests/coordinate.rs new file mode 100644 index 0000000..18f9f87 --- /dev/null +++ b/src/game/tests/coordinate.rs @@ -0,0 +1,98 @@ +use super::{Coordinate, normalize_coordinates, rotate_coordinates, flip_coordinates}; +use crate::game::rotation::Rotation; + +#[test] +fn test_coordinate_equality() { + let coord1 = Coordinate::new(3, 4); + let coord2 = Coordinate::new(3, 4); + let coord3 = Coordinate::new(5, 6); + + assert_eq!(coord1, coord2); + assert_ne!(coord1, coord3); +} + +#[test] +fn test_coordinate_addition() { + let mut coord1 = Coordinate::new(2, 3); + let coord2 = Coordinate::new(4, 5); + coord1.add(&coord2); + assert_eq!(coord1, Coordinate::new(6, 8)); +} + +#[test] +fn test_coordinate_subtraction() { + let mut coord1 = Coordinate::new(5, 7); + let coord2 = Coordinate::new(2, 3); + coord1.subtract(&coord2); + assert_eq!(coord1, Coordinate::new(3, 4)); +} + +#[test] +fn test_coordinate_multiplication() { + let mut coord = Coordinate::new(3, 4); + coord.multiply(2); + assert_eq!(coord, Coordinate::new(6, 8)); +} + +#[test] +fn test_coordinate_division() { + let mut coord = Coordinate::new(8, 12); + coord.divide(4); + assert_eq!(coord, Coordinate::new(2, 3)); +} + +#[test] +fn test_coordinate_rotation() { + let coord = Coordinate::new(1, 2); + assert_eq!(coord.rotate(&Rotation::Right), Coordinate::new(-2, 1)); + assert_eq!(coord.rotate(&Rotation::Mirror), Coordinate::new(-1, -2)); + assert_eq!(coord.rotate(&Rotation::Left), Coordinate::new(2, -1)); + assert_eq!(coord.rotate(&Rotation::None), coord); +} + +#[test] +fn test_coordinate_flip_on_vertical() { + let coord = Coordinate::new(1, 2); + assert_eq!(coord.flip_on_vertical(), Coordinate::new(-1, 2)); +} + +#[test] +fn test_normalize_coordinates() { + let coords = vec![ + Coordinate::new(3, 4), + Coordinate::new(1, 2), + Coordinate::new(5, 6), + ]; + let normalized = normalize_coordinates(&coords); + assert_eq!(normalized, vec![ + Coordinate::new(2, 2), + Coordinate::new(0, 0), + Coordinate::new(4, 4), + ]); +} + +#[test] +fn test_rotate_coordinates() { + let coords = vec![ + Coordinate::new(1, 2), + Coordinate::new(3, 4), + ]; + let rotated = rotate_coordinates(coords, &Rotation::Right); + assert_eq!(rotated, vec![ + Coordinate::new(-2, 1), + Coordinate::new(-4, 3), + ]); +} + +#[test] +fn test_flip_coordinates() { + let coords = vec![ + Coordinate::new(1, 2), + Coordinate::new(3, 4), + ]; + let flipped = flip_coordinates(coords); + assert_eq!(flipped, vec![ + Coordinate::new(-1, 2), + Coordinate::new(-3, 4), + ]); +} \ No newline at end of file diff --git a/src/game/tests/gamerulelogic.rs b/src/game/tests/gamerulelogic.rs new file mode 100644 index 0000000..9bd46eb --- /dev/null +++ b/src/game/tests/gamerulelogic.rs @@ -0,0 +1,205 @@ +use crate::game::{ + board::Board, + color::Color, + coordinate::Coordinate, + gamestate::GameState, + piece::PieceType, + r#move::Move, + rotation::Rotation, +}; + +use super::{ + get_colored_fields, get_possible_moves, get_possible_moves_for_piece, get_valid_fields, + is_valid_move, +}; + +fn blue_turn_state_with_board(board: Board, pieces: Vec) -> GameState { + GameState::new( + PieceType::Mono, + true, + board, + 5, + 2, + Color::Blue, + pieces, + vec![], + vec![], + vec![], + ) +} + +#[test] +fn invalid_when_directly_adjacent_to_own_piece() { + let mut board = Board::new(); + board.set_cell(5, 5, Color::Blue); + + let state = blue_turn_state_with_board(board, vec![PieceType::Mono]); + let m = Move { + color: Color::Blue, + piece: PieceType::Mono, + x: 6, + y: 5, + is_flipped: false, + rotation: Rotation::None, + skip: false, + }; + + assert!(!is_valid_move(&state, &m)); +} + +#[test] +fn valid_when_only_corner_contact_exists() { + let mut board = Board::new(); + board.set_cell(5, 5, Color::Blue); + + let state = blue_turn_state_with_board(board, vec![PieceType::Mono]); + let m = Move { + color: Color::Blue, + piece: PieceType::Mono, + x: 6, + y: 6, + is_flipped: false, + rotation: Rotation::None, + skip: false, + }; + + assert!(is_valid_move(&state, &m)); +} + +#[test] +fn invalid_when_no_corner_contact_after_first_move() { + let mut board = Board::new(); + board.set_cell(5, 5, Color::Blue); + + let state = blue_turn_state_with_board(board, vec![PieceType::Mono]); + let m = Move { + color: Color::Blue, + piece: PieceType::Mono, + x: 10, + y: 10, + is_flipped: false, + rotation: Rotation::None, + skip: false, + }; + + assert!(!is_valid_move(&state, &m)); +} + +#[test] +fn calculates_moves_where_corner_is_not_piece_origin() { + let mut board = Board::new(); + board.set_cell(5, 5, Color::Blue); + + let state = GameState::new( + PieceType::Mono, + true, + board, + 5, + 2, + Color::Blue, + vec![PieceType::PentoX], + vec![], + vec![], + vec![], + ); + + let valid_fields = vec![Coordinate { x: 6, y: 6 }]; + let moves = get_possible_moves_for_piece(&state, &PieceType::PentoX, &valid_fields); + + assert!( + moves.iter().any(|m| m.x == 6 && m.y == 5), + "expected a placement that aligns a non-origin PENTO_X tile to the corner" + ); +} + +fn mono_move(color: Color, x: usize, y: usize) -> Move { + Move { + color, + piece: PieceType::Mono, + x, + y, + is_flipped: false, + rotation: Rotation::None, + skip: false, + } +} + +#[test] +fn invalid_when_piece_is_not_available() { + let state = blue_turn_state_with_board(Board::new(), vec![]); + + assert!(!is_valid_move(&state, &mono_move(Color::Blue, 0, 0))); +} + +#[test] +fn invalid_when_placement_is_out_of_bounds() { + let state = blue_turn_state_with_board(Board::new(), vec![PieceType::Mono]); + + assert!(!is_valid_move(&state, &mono_move(Color::Blue, 20, 0))); +} + +#[test] +fn invalid_when_placement_overlaps_an_occupied_cell() { + let mut board = Board::new(); + board.set_cell(0, 0, Color::Yellow); + let state = blue_turn_state_with_board(board, vec![PieceType::Mono]); + + assert!(!is_valid_move(&state, &mono_move(Color::Blue, 0, 0))); +} + +#[test] +fn skip_move_is_always_valid() { + let mut move_to_skip = mono_move(Color::Blue, 20, 20); + move_to_skip.skip = true; + let state = blue_turn_state_with_board(Board::new(), vec![]); + + assert!(is_valid_move(&state, &move_to_skip)); +} + +#[test] +fn valid_fields_contain_only_unoccupied_diagonal_corners() { + let mut board = Board::new(); + board.set_cell(5, 5, Color::Blue); + board.set_cell(7, 7, Color::Blue); + board.set_cell(4, 6, Color::Blue); + + let fields = get_valid_fields(&board, &Color::Blue); + + assert!(fields.contains(&Coordinate { x: 6, y: 6 })); + assert!(!fields.contains(&Coordinate { x: 6, y: 5 })); + assert!(!fields.contains(&Coordinate { x: 7, y: 7 })); + assert!(!fields.contains(&Coordinate { x: 5, y: 6 })); +} + +#[test] +fn colored_fields_returns_only_matching_cells() { + let mut board = Board::new(); + board.set_cell(1, 2, Color::Blue); + board.set_cell(3, 4, Color::Yellow); + + assert_eq!( + get_colored_fields(&board, &Color::Blue), + vec![Coordinate { x: 1, y: 2 }] + ); +} + +#[test] +fn first_round_generates_starting_piece_moves() { + let state = GameState::new( + PieceType::Mono, + true, + Board::new(), + 0, + 1, + Color::Blue, + vec![PieceType::Mono], + vec![], + vec![], + vec![], + ); + + let moves = get_possible_moves(&state); + + assert!(!moves.is_empty()); + assert!(moves.iter().all(|m| !m.skip)); +} \ No newline at end of file diff --git a/src/game/tests/gamestate.rs b/src/game/tests/gamestate.rs new file mode 100644 index 0000000..73a3d76 --- /dev/null +++ b/src/game/tests/gamestate.rs @@ -0,0 +1,100 @@ +use crate::game::{ + board::Board, + color::Color, + piece::PieceType, + rotation::Rotation, + r#move::Move, + team::Team, +}; + +use super::GameState; + +#[test] +fn test_initialize_game_state() { + let game_state = GameState::new( + PieceType::PentoR, + Default::default(), + Board::new(), + 0, + 0, + Color::Blue, + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + ); + + // Check that the initial board is empty + for row in 0..20 { + for col in 0..20 { + assert_eq!(game_state.get_board().get_cell(row, col), None); + } + } + + // Check that the current team is Team One + assert_eq!(*game_state.get_current_turn_color(), Color::Blue); + + // Check that the initial scores are zero + assert_eq!(game_state.get_points_for_team(&Team::One), 0); + assert_eq!(game_state.get_points_for_team(&Team::Two), 0); +} + +#[test] +fn test_apply_move_unchecked() { + let mut game_state = GameState::new( + PieceType::Mono, + true, + Board::new(), + 0, + 0, + Color::Blue, + vec![PieceType::Mono], + Vec::new(), + Vec::new(), + Vec::new(), + ); + let m = Move::new(Color::Blue, PieceType::Mono, 0, 0, false, Rotation::None, false); + + game_state.apply_move_unchecked(&m, 4); + + assert_eq!(game_state.get_board().get_cell(0, 0), Some(Color::Blue)); + assert!(game_state.get_color_pieces(&Color::Blue).is_empty()); + assert_eq!(game_state.get_last_move(&Color::Blue), &Some(m)); + assert_eq!(game_state.get_points_for_color(&Color::Blue), 16); + assert_eq!(game_state.get_points_for_team(&Team::One), 16); + assert_eq!(*game_state.get_turn(), 4); + assert_eq!(*game_state.get_round(), 1); + assert_eq!(*game_state.get_current_turn_color(), Color::Blue); +} + +#[test] +fn test_apply_move() { + let mut game_state = GameState::new( + PieceType::Mono, + true, + Board::new(), + 0, + 0, + Color::Blue, + vec![PieceType::Mono], + Vec::new(), + Vec::new(), + Vec::new(), + ); + let m = Move::new(Color::Blue, PieceType::Mono, 0, 0, false, Rotation::None, false); + + assert!(game_state.apply_move(&m, 1)); + assert_eq!(game_state.get_board().get_cell(0, 0), Some(Color::Blue)); + assert_eq!(game_state.get_points_for_color(&Color::Blue), 16); + assert_eq!(*game_state.get_turn(), 1); + assert_eq!(*game_state.get_round(), 0); + assert_eq!(*game_state.get_current_turn_color(), Color::Yellow); + + let state_after_success = game_state.clone(); + let invalid_move = Move::new(Color::Blue, PieceType::Mono, 0, 0, false, Rotation::None, false); + + assert!(!game_state.apply_move(&invalid_move, 2)); + assert_eq!(game_state, state_after_success); + +} + diff --git a/src/game/tests/piece.rs b/src/game/tests/piece.rs new file mode 100644 index 0000000..d6b1e4a --- /dev/null +++ b/src/game/tests/piece.rs @@ -0,0 +1,155 @@ +use super::{Piece, PieceType}; +use crate::{game::rotation::Rotation, game::coordinate::Coordinate}; + +fn sorted_xy(coordinates: Vec) -> Vec<(isize, isize)> { + let mut xy: Vec<(isize, isize)> = coordinates + .into_iter() + .map(|coord| (coord.x, coord.y)) + .collect(); + xy.sort_unstable(); + xy +} + +#[test] +fn pento_r_get_coordinates_all_8_variants() { + let cases: Vec<(Rotation, bool, Vec<(isize, isize)>)> = vec![ + ( + Rotation::None, + false, + vec![(2, 0), (0, 1), (1, 1), (2, 1), (1, 2)], + ), + ( + Rotation::Right, + false, + vec![(2, 2), (1, 0), (1, 1), (1, 2), (0, 1)], + ), + ( + Rotation::Mirror, + false, + vec![(0, 2), (2, 1), (1, 1), (0, 1), (1, 0)], + ), + ( + Rotation::Left, + false, + vec![(0, 0), (1, 2), (1, 1), (1, 0), (2, 1)], + ), + ( + Rotation::None, + true, + vec![(0, 0), (2, 1), (1, 1), (0, 1), (1, 2)], + ), + ( + Rotation::Right, + true, + vec![(0, 2), (1, 0), (1, 1), (1, 2), (2, 1)], + ), + ( + Rotation::Mirror, + true, + vec![(2, 2), (0, 1), (1, 1), (2, 1), (1, 0)], + ), + ( + Rotation::Left, + true, + vec![(2, 0), (1, 2), (1, 1), (1, 0), (0, 1)], + ), + ]; + + for (rotation, is_flipped, expected) in cases { + let piece = Piece::new(PieceType::PentoR, rotation, is_flipped); + let actual = sorted_xy(piece.get_coordinates()); + + assert_eq!( + actual, + sorted_xy( + expected + .into_iter() + .map(|(x, y)| Coordinate::new(x, y)) + .collect() + ), + "unexpected coordinates for rotation={:?}, is_flipped={}", + rotation, + is_flipped + ); + } +} + +#[test] +fn pento_r_all_variants_generates_all_unique_orientations() { + let variants = PieceType::PentoR.all_variants(true); + let unfiltered = PieceType::PentoR.all_variants(false); + + assert_eq!(variants.len(), 8, "filtered variants should still include all unique PentoR states"); + assert_eq!(unfiltered.len(), 8, "every rotation/flip combination should be represented"); + + let expected_rotations = vec![ + Rotation::None, + Rotation::Right, + Rotation::Mirror, + Rotation::Left, + ]; + let mut seen = Vec::new(); + for (_, (rotation, is_flipped)) in variants { + seen.push((rotation, is_flipped)); + } + for rotation in expected_rotations { + for is_flipped in [false, true] { + assert!( + seen.contains(&(rotation, is_flipped)), + "missing rotation={:?}, is_flipped={}", + rotation, + is_flipped + ); + } + } +} + +#[test] +fn piece_mutators_keep_state_in_sync() { + let mut piece = Piece::new(PieceType::Mono, Rotation::None, false); + + assert_eq!(piece.get_piece_type(), &PieceType::Mono); + assert_eq!(piece.get_rotation(), &Rotation::None); + assert!(piece.is_flipped() == &false); + + piece.set_piece_type(PieceType::PentoR); + piece.set_rotation(Rotation::Right); + piece.set_flipped(true); + + assert_eq!(piece.get_piece_type(), &PieceType::PentoR); + assert_eq!(piece.get_rotation(), &Rotation::Right); + assert!(piece.is_flipped() == &true); + assert_eq!(piece.get_coordinates().len(), 5); +} + +#[test] +fn piece_type_parse_round_trip_for_all_variants() { + let values = [ + PieceType::Mono, + PieceType::Domino, + PieceType::TrioL, + PieceType::TrioI, + PieceType::TetroO, + PieceType::TetroT, + PieceType::TetroI, + PieceType::TetroL, + PieceType::TetroZ, + PieceType::PentoL, + PieceType::PentoT, + PieceType::PentoV, + PieceType::PentoS, + PieceType::PentoZ, + PieceType::PentoI, + PieceType::PentoP, + PieceType::PentoW, + PieceType::PentoU, + PieceType::PentoR, + PieceType::PentoX, + PieceType::PentoY, + ]; + + for piece_type in values { + let parsed = piece_type.to_string().parse::(); + assert_eq!(parsed, Ok(piece_type), "round-trip parse failed for {}", piece_type); + } +} \ No newline at end of file