Detect unique chains

This commit is contained in:
2023-03-23 17:18:59 +01:00
parent 915949fb98
commit ac7c9b30de
5 changed files with 177 additions and 15 deletions

View File

@@ -37,8 +37,9 @@ impl Leaderboard for RedisLeaderboard {
async fn get_highscores(&self) -> Result<Vec<LeaderboardEntry>, RedisError> {
let mut con = self.client.get_async_connection().await?;
let count: isize = con.zcard(LEADERBOARD).await?;
let leaderboard: Vec<LeaderboardEntry> =
con.zrange_withscores(LEADERBOARD, 0, (count - 1).min(LEADERBOARD_SIZE)).await?;
let leaderboard: Vec<LeaderboardEntry> = con
.zrange_withscores(LEADERBOARD, 0, (count - 1).min(LEADERBOARD_SIZE))
.await?;
Ok(leaderboard)
}

View File

@@ -210,7 +210,7 @@ impl Room {
self.successive_skipped_turns += 1;
self.next_player();
}
if !Board::has_alignment(&diff) {
if !Board::has_alignment(&diff) || !self.board.is_unique_chain(&diff) {
self.reset_player_moves();
self.send(
self.active_player,

View File

@@ -1,8 +1,16 @@
use crate::position::{Alignment, Grid2d, Position2d};
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)]
pub struct Board {
tiles: Vec<Option<Tile>>,
@@ -11,19 +19,47 @@ pub struct Board {
}
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 {
@@ -53,6 +89,14 @@ impl Board {
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)
}
/// 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)?;
@@ -97,10 +141,33 @@ impl Board {
}
}
/// Tests whether the given positions are part of the same chain.
pub fn belong_to_same_chain(&self, positions: &Vec<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.
/// 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()?;
@@ -180,6 +247,7 @@ impl Default for Board {
#[cfg(test)]
mod tests {
use super::*;
use crate::tile::Digit;
fn positions(input: &[(usize, usize)]) -> Vec<Position2d> {
input
@@ -224,28 +292,28 @@ mod tests {
assert!(Board::is_aligned(&[], Alignment::Vertical));
assert!(Board::is_aligned(
&positions(&[(0, 0)]),
Alignment::Horizontal
Alignment::Vertical
));
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)]),
&positions(&[(0, 0), (0, 1)]),
Alignment::Vertical
));
assert!(!Board::is_aligned(
assert!(Board::is_aligned(
&positions(&[(0, 0), (1, 0)]),
Alignment::Horizontal
));
assert!(!Board::is_aligned(
&positions(&[(0, 0), (0, 1)]),
&positions(&[(0, 0), (1, 0)]),
Alignment::Vertical
));
assert!(!Board::is_aligned(
&positions(&[(0, 0), (0, 1)]),
Alignment::Horizontal
));
}
#[test]
@@ -268,4 +336,37 @@ mod tests {
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);
}
}

View File

@@ -1,3 +1,16 @@
///! # Scrabble with Numbers
///!
///! This crate provides the core game logic for the Scrabble with Numbers game.
///! It can be used standalone, or as a library for other projects.
///!
///! ## Features
///! - Create and verify game expressions, such as `2*3+4*5`.
///! - Generate valid automatic moves for a given board state, with the `ai` feature.
///! - Check if a player move valid.
///! - Calculate the score of an expression.
///!
///! If you are looking for a server implementation, see the `board-server` crate.
#[cfg(feature = "ai")]
pub mod ai;
pub mod board;

View File

@@ -41,8 +41,8 @@ impl Alignment {
/// Test if two positions are aligned in this direction.
pub fn is_aligned(self, a: Position2d, b: Position2d) -> bool {
match self {
Alignment::Horizontal => a.x == b.x,
Alignment::Vertical => a.y == b.y,
Alignment::Horizontal => a.y == b.y,
Alignment::Vertical => a.x == b.x,
}
}
}
@@ -67,6 +67,53 @@ impl Position2d {
None
}
}
/// Returns an iterator over all positions relative to this position in the given direction.
///
/// If the positions are not aligned in any direction, the iterator will be empty.
/// If the end position is before the start position, the iterator will be empty.
pub fn iterate_to(self, end: Position2d, offset: (usize, usize)) -> RelativeIterator {
if (self.x != end.x && self.y != end.y) || self.x > end.x || self.y > end.y {
return RelativeIterator {
pos: self,
end: self,
offset: (0, 0),
};
}
RelativeIterator {
pos: self,
end,
offset,
}
}
pub fn manhattan_distance(self, other: Position2d) -> usize {
self.x.abs_diff(other.x) + self.y.abs_diff(other.y)
}
pub fn is_contiguous(self, other: Position2d) -> bool {
self.manhattan_distance(other) == 1
}
}
pub struct RelativeIterator {
pos: Position2d,
end: Position2d,
offset: (usize, usize),
}
impl Iterator for RelativeIterator {
type Item = Position2d;
fn next(&mut self) -> Option<Self::Item> {
if self.pos == self.end {
None
} else {
let pos = self.pos;
self.pos = Position2d::new(self.pos.x + self.offset.0, self.pos.y + self.offset.1);
Some(pos)
}
}
}
impl From<(usize, usize)> for Position2d {