Detect unique chains
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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 {
|
||||
|
Reference in New Issue
Block a user