use crate::position::{Alignment, Direction, Grid2d, Position2d}; use crate::tile::Tile; const DEFAULT_BOARD_SIZE: usize = 19; /// A board of tiles. /// /// This is a fixed-size 2D grid of tiles, where each tile is either empty or /// contains a single tile. /// /// This struct is implement `Default` so that you can use [`Board::default()`] /// to create a new board with a default size. To create a board with a custom /// size, use [`Board::new()`]. #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Board { tiles: Vec>, width: usize, height: usize, } impl Board { /// Creates a new board with the given size. pub fn new(width: usize, height: usize) -> Self { Self { tiles: vec![None; width * height], width, height, } } /// Gets the tile at the given position, or `None` if there is no tile. /// /// # Panics /// Panics if the position is out of bounds. pub fn get(&self, x: usize, y: usize) -> Option { self.tiles[y * self.width + x] } /// Sets the tile at the given position. /// /// If there was already a tile at the position, it is replaced. /// /// # Panics /// Panics if the position is out of bounds. pub fn set(&mut self, x: usize, y: usize, tile: Tile) { self.tiles[y * self.width + x] = Some(tile); } /// Removes and returns the tile at the given position. /// /// If there was no tile at the position, `None` is returned. /// /// # Panics /// Panics if the position is out of bounds. pub fn take(&mut self, x: usize, y: usize) -> Option { self.tiles[y * self.width + x].take() } /// Gets the difference between this board and another. /// /// This returns a vector of positions at which the tiles differ. /// The order is not guaranteed. pub fn difference(&self, other: &Board) -> Vec { let mut diff = Vec::new(); for y in 0..self.height { for x in 0..self.width { if self.get(x, y) != other.get(x, y) { diff.push(Position2d::new(x, y)); } } } diff } /// Gets all chains of tiles that are adjacent to the given positions. pub fn find_chains(&self, positions: &[Position2d]) -> Vec> { let mut chains = Vec::new(); for &pos in positions { for &alignment in &[Alignment::Horizontal, Alignment::Vertical] { if let Some(start) = self.find_starting_tile(pos, alignment) { if let Some(chain) = self.find_chain_in_direction(start, alignment) { if chain.len() > 1 { chains.push(chain); } } } } } chains } /// Tests whether the given positions form a unique chain. pub fn is_unique_chain(&self, positions: &Vec) -> bool { Self::is_aligned(positions, Alignment::Horizontal) && self.belong_to_same_chain(positions, Direction::Right) || Self::is_aligned(positions, Alignment::Vertical) && self.belong_to_same_chain(positions, Direction::Down) } /// Tests whether all the tiles can be placed contiguously. /// /// Such tiles can be placed in a single move and can reuse existing tiles /// on the board if the exact same tile present at the correct position in /// the `tiles` slice. /// /// # Examples /// ``` /// use board_shared::board::Board; /// use board_shared::tile::{Digit, Tile}; /// /// let mut board = Board::default(); /// board.set(2, 0, Tile::Equals); /// let guess = vec![ /// Tile::Digit(Digit::new(7)), /// Tile::Equals, /// Tile::Digit(Digit::new(7)), /// ]; /// /// // The equals sign is already on the board at the correct position, /// // so it can be reused. /// assert!(board.is_playable(&guess, (1, 0).into(), board_shared::position::Direction::Right)); /// /// // The equals sign exist on the board but at the wrong position, /// // so it cannot be reused. /// assert!(!board.is_playable(&guess, (2, 0).into(), board_shared::position::Direction::Right)); /// ``` pub fn is_playable( &self, tiles: &[Tile], starting_from: Position2d, direction: Direction, ) -> bool { let mut pos = starting_from; let mut it = tiles.iter(); while let Some(tile) = it.next() { if let Some(existing) = self.get(pos.x, pos.y) { if existing != *tile { return false; } } if let Some(relative) = pos.relative(direction, self) { pos = relative; } else { return it.next().is_none(); } } true } /// Finds the starting tile of a chain in the given direction. fn find_starting_tile(&self, pos: Position2d, alignment: Alignment) -> Option { self.get(pos.x, pos.y)?; let mut pos = pos; let direction = alignment.start(); loop { if let Some(relative) = pos.relative(direction, self) { if self.get(relative.x, relative.y).is_some() { pos = relative; } else { return Some(pos); } } else { return Some(pos); } } } /// Finds a chain of tiles in the given direction. fn find_chain_in_direction( &self, pos: Position2d, direction: Alignment, ) -> Option> { let mut chain = Vec::new(); let mut pos = pos; loop { chain.push(pos); if let Some(relative) = pos.relative(direction.end(), self) { if self.get(relative.x, relative.y).is_none() { break; } pos = relative; } else { break; } } if chain.is_empty() { None } else { Some(chain) } } /// Tests whether the given positions are part of the same chain. fn belong_to_same_chain(&self, positions: &[Position2d], direction: Direction) -> bool { let mut it = positions.iter().copied().peekable(); while let Some(mut pos) = it.next() { if let Some(&next) = it.peek() { while !pos.is_contiguous(next) { if let Some(relative) = pos.relative(direction, self) { pos = relative; if self.get(pos.x, pos.y).is_none() { return false; } } else { return false; } } } } true } /// Determines whether the given positions are contiguous. /// /// Contiguous means that the positions are adjacent in a straight line, either /// horizontally or vertically. This does not check the tiles at the positions. /// /// You may want to use [`Board::is_unique_chain`] to check if the positions are /// contiguous based on the current board state. pub fn is_contiguous(positions: &[Position2d]) -> Option { let mut it = positions.iter(); let first = *it.next()?; let mut second = *it.next()?; let orientation = match (second.x.checked_sub(first.x), second.y.checked_sub(first.y)) { (Some(0), Some(1)) => (0, 1), (Some(1), Some(0)) => (1, 0), (_, _) => return Some(false), }; for &pos in it { if pos.x != second.x + orientation.0 || pos.y != second.y + orientation.1 { return Some(false); } second = pos; } Some(true) } /// Determines whether the given positions are aligned. pub fn is_aligned(positions: &[Position2d], alignement: Alignment) -> bool { if let Some(&first) = positions.first() { positions .iter() .all(|&pos| alignement.is_aligned(first, pos)) } else { true } } /// Determines whether the given positions have any alignment. pub fn has_alignment(positions: &[Position2d]) -> bool { Self::is_aligned(positions, Alignment::Horizontal) || Self::is_aligned(positions, Alignment::Vertical) } /// Gets a linear iterator over the tiles, row by row. /// /// # Example: /// ``` /// use board_shared::board::Board; /// /// let board = Board::default(); /// let placed_tiles = board.iter().filter(Option::is_some).count(); /// assert_eq!(placed_tiles, 0); /// ``` pub fn iter(&self) -> impl Iterator> + '_ { self.tiles.iter().copied() } } impl Grid2d for Board { fn width(&self) -> usize { self.width } fn height(&self) -> usize { self.height } } impl Default for Board { fn default() -> Self { let size = DEFAULT_BOARD_SIZE * DEFAULT_BOARD_SIZE; let mut tiles = Vec::with_capacity(size); tiles.resize_with(size, || None); Self { tiles, width: DEFAULT_BOARD_SIZE, height: DEFAULT_BOARD_SIZE, } } } #[cfg(test)] mod tests { use super::*; use crate::tile::Digit; fn positions(input: &[(usize, usize)]) -> Vec { input .iter() .map(|(x, y)| Position2d::new(*x, *y)) .collect::>() } #[test] fn test_is_contiguous() { assert_eq!(Board::is_contiguous(&[]), None); assert_eq!(Board::is_contiguous(&positions(&[(0, 0)])), None); assert_eq!( Board::is_contiguous(&positions(&[(0, 0), (0, 2)])), Some(false) ); assert_eq!( Board::is_contiguous(&positions(&[(0, 0), (2, 0)])), Some(false) ); assert_eq!( Board::is_contiguous(&positions(&[(0, 0), (0, 1), (0, 2)])), Some(true) ); assert_eq!( Board::is_contiguous(&positions(&[(1, 0), (2, 0), (3, 0), (4, 0)])), Some(true) ); assert_eq!( Board::is_contiguous(&positions(&[(0, 0), (0, 1), (1, 3)])), Some(false) ); assert_eq!( Board::is_contiguous(&positions(&[(0, 0), (0, 1), (0, 2), (1, 2)])), Some(false) ); } #[test] fn test_is_aligned() { assert!(Board::is_aligned(&[], Alignment::Horizontal)); assert!(Board::is_aligned(&[], Alignment::Vertical)); assert!(Board::is_aligned( &positions(&[(0, 0)]), Alignment::Vertical )); assert!(Board::is_aligned( &positions(&[(0, 0)]), Alignment::Horizontal )); assert!(Board::is_aligned( &positions(&[(0, 0), (0, 1)]), Alignment::Vertical )); assert!(Board::is_aligned( &positions(&[(0, 0), (1, 0)]), Alignment::Horizontal )); assert!(!Board::is_aligned( &positions(&[(0, 0), (1, 0)]), Alignment::Vertical )); assert!(!Board::is_aligned( &positions(&[(0, 0), (0, 1)]), Alignment::Horizontal )); } #[test] fn test_find_chains() { let mut board = Board::default(); for x in 1..5 { board.set(x, 2, Tile::Equals); } assert_eq!( board.find_chains(&[Position2d::new(0, 0)]), Vec::>::new() ); let expected = vec![vec![ Position2d::new(1, 2), Position2d::new(2, 2), Position2d::new(3, 2), Position2d::new(4, 2), ]]; assert_eq!(board.find_chains(&[Position2d::new(1, 2)]), expected); assert_eq!(board.find_chains(&[Position2d::new(2, 2)]), expected); assert_eq!(board.find_chains(&[Position2d::new(4, 2)]), expected); } #[test] fn test_is_unique_chain_empty_board() { let board = Board::default(); assert_eq!( board.is_unique_chain(&vec![Position2d::new(0, 0), Position2d::new(0, 1),]), true ); assert_eq!( board.is_unique_chain(&vec![ Position2d::new(0, 0), Position2d::new(0, 1), Position2d::new(1, 1), ]), false ); assert_eq!(board.is_unique_chain(&vec![]), true); } #[test] fn test_is_unique_chain_existing_board() { let mut board = Board::default(); board.set(0, 1, Tile::Digit(Digit::new(2))); board.set(0, 2, Tile::Equals); board.set(1, 1, Tile::Equals); assert_eq!(board.is_unique_chain(&positions(&[(0, 0), (0, 3)])), true); assert_eq!( board.is_unique_chain(&positions(&[(0, 0), (0, 3), (0, 4)])), true ); assert_eq!(board.is_unique_chain(&positions(&[(1, 1), (2, 1)])), true); assert_eq!(board.is_unique_chain(&positions(&[(1, 1), (3, 1)])), false); } #[test] fn test_is_playable() { let board = Board::new(5, 5); let tiles = vec![ Tile::Digit(Digit::new(5)), Tile::Digit(Digit::new(6)), Tile::Digit(Digit::new(9)), ]; assert_eq!( board.is_playable(&tiles, (1, 1).into(), Direction::Right), true ); assert_eq!( board.is_playable(&tiles, (4, 0).into(), Direction::Down), true ); } #[test] fn test_is_playable_constrained() { let board = Board::new(2, 2); let tiles = vec![Tile::Digit(Digit::new(7)), Tile::Digit(Digit::new(8))]; assert_eq!( board.is_playable(&tiles, (0, 0).into(), Direction::Right), true ); assert_eq!( board.is_playable(&tiles, (0, 0).into(), Direction::Down), true ); assert_eq!( board.is_playable(&tiles, (1, 0).into(), Direction::Right), false ); } #[test] fn test_is_playable_blocking_pieces() { let mut board = Board::new(3, 3); board.set(1, 1, Tile::Digit(Digit::new(1))); let tiles = vec![Tile::Digit(Digit::new(0)), Tile::Digit(Digit::new(0))]; assert_eq!( board.is_playable(&tiles, (0, 0).into(), Direction::Right), true ); assert_eq!( board.is_playable(&tiles, (0, 1).into(), Direction::Right), false ); assert_eq!( board.is_playable(&tiles, (1, 0).into(), Direction::Down), false ); } }