You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
479 lines
15 KiB
479 lines
15 KiB
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<Option<Tile>>,
|
|
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<Tile> {
|
|
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<Tile> {
|
|
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<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
|
|
}
|
|
|
|
/// Tests whether the given positions form a unique chain.
|
|
pub fn is_unique_chain(&self, positions: &Vec<Position2d>) -> 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<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)
|
|
}
|
|
}
|
|
|
|
/// 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<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::*;
|
|
use crate::tile::Digit;
|
|
|
|
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::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::<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);
|
|
}
|
|
|
|
#[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
|
|
);
|
|
}
|
|
}
|