Files
scrabble-with-numbers/board-shared/src/board.rs

268 lines
7.8 KiB
Rust

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<Option<Tile>>,
width: usize,
height: usize,
}
impl Board {
pub fn get(&self, x: usize, y: usize) -> Option<Tile> {
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<Position2d> {
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<Vec<Position2d>> {
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<Position2d> {
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<Vec<Position2d>> {
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<bool> {
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<Item = Option<Tile>> + '_ {
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<Position2d> {
input
.iter()
.map(|(x, y)| Position2d::new(*x, *y))
.collect::<Vec<_>>()
}
#[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::<Vec<Position2d>>::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);
}
}