use crate::position::{Alignment, Grid2d, Position2d}; use crate::tile::Tile; const DEFAULT_BOARD_SIZE: usize = 25; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Board { tiles: Vec>, width: usize, height: usize, } impl Board { pub fn get(&self, x: usize, y: usize) -> Option { self.tiles[y * self.width + x] } pub fn set(&mut self, x: usize, y: usize, tile: Tile) { self.tiles[y * self.width + x] = Some(tile); } /// Gets the difference between this board and another. 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 } /// 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) } } /// Determines whether the given positions are contiguous. /// /// Contiguous means that the positions are adjacent in a straight line, either /// horizontally or vertically. 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::*; 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::Horizontal )); assert!(Board::is_aligned( &positions(&[(0, 0)]), Alignment::Vertical )); assert!(Board::is_aligned( &positions(&[(0, 0), (0, 1)]), Alignment::Horizontal )); assert!(Board::is_aligned( &positions(&[(0, 0), (1, 0)]), Alignment::Vertical )); assert!(!Board::is_aligned( &positions(&[(0, 0), (1, 0)]), Alignment::Horizontal )); assert!(!Board::is_aligned( &positions(&[(0, 0), (0, 1)]), Alignment::Vertical )); } #[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); } }