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> {
|
async fn get_highscores(&self) -> Result<Vec<LeaderboardEntry>, RedisError> {
|
||||||
let mut con = self.client.get_async_connection().await?;
|
let mut con = self.client.get_async_connection().await?;
|
||||||
let count: isize = con.zcard(LEADERBOARD).await?;
|
let count: isize = con.zcard(LEADERBOARD).await?;
|
||||||
let leaderboard: Vec<LeaderboardEntry> =
|
let leaderboard: Vec<LeaderboardEntry> = con
|
||||||
con.zrange_withscores(LEADERBOARD, 0, (count - 1).min(LEADERBOARD_SIZE)).await?;
|
.zrange_withscores(LEADERBOARD, 0, (count - 1).min(LEADERBOARD_SIZE))
|
||||||
|
.await?;
|
||||||
Ok(leaderboard)
|
Ok(leaderboard)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -210,7 +210,7 @@ impl Room {
|
|||||||
self.successive_skipped_turns += 1;
|
self.successive_skipped_turns += 1;
|
||||||
self.next_player();
|
self.next_player();
|
||||||
}
|
}
|
||||||
if !Board::has_alignment(&diff) {
|
if !Board::has_alignment(&diff) || !self.board.is_unique_chain(&diff) {
|
||||||
self.reset_player_moves();
|
self.reset_player_moves();
|
||||||
self.send(
|
self.send(
|
||||||
self.active_player,
|
self.active_player,
|
||||||
|
@@ -1,8 +1,16 @@
|
|||||||
use crate::position::{Alignment, Grid2d, Position2d};
|
use crate::position::{Alignment, Direction, Grid2d, Position2d};
|
||||||
use crate::tile::Tile;
|
use crate::tile::Tile;
|
||||||
|
|
||||||
const DEFAULT_BOARD_SIZE: usize = 19;
|
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)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Board {
|
pub struct Board {
|
||||||
tiles: Vec<Option<Tile>>,
|
tiles: Vec<Option<Tile>>,
|
||||||
@@ -11,19 +19,47 @@ pub struct Board {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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> {
|
pub fn get(&self, x: usize, y: usize) -> Option<Tile> {
|
||||||
self.tiles[y * self.width + x]
|
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) {
|
pub fn set(&mut self, x: usize, y: usize, tile: Tile) {
|
||||||
self.tiles[y * self.width + x] = Some(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> {
|
pub fn take(&mut self, x: usize, y: usize) -> Option<Tile> {
|
||||||
self.tiles[y * self.width + x].take()
|
self.tiles[y * self.width + x].take()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the difference between this board and another.
|
/// 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> {
|
pub fn difference(&self, other: &Board) -> Vec<Position2d> {
|
||||||
let mut diff = Vec::new();
|
let mut diff = Vec::new();
|
||||||
for y in 0..self.height {
|
for y in 0..self.height {
|
||||||
@@ -53,6 +89,14 @@ impl Board {
|
|||||||
chains
|
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.
|
/// Finds the starting tile of a chain in the given direction.
|
||||||
fn find_starting_tile(&self, pos: Position2d, alignment: Alignment) -> Option<Position2d> {
|
fn find_starting_tile(&self, pos: Position2d, alignment: Alignment) -> Option<Position2d> {
|
||||||
self.get(pos.x, pos.y)?;
|
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.
|
/// Determines whether the given positions are contiguous.
|
||||||
///
|
///
|
||||||
/// Contiguous means that the positions are adjacent in a straight line, either
|
/// 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> {
|
pub fn is_contiguous(positions: &[Position2d]) -> Option<bool> {
|
||||||
let mut it = positions.iter();
|
let mut it = positions.iter();
|
||||||
let first = *it.next()?;
|
let first = *it.next()?;
|
||||||
@@ -180,6 +247,7 @@ impl Default for Board {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::tile::Digit;
|
||||||
|
|
||||||
fn positions(input: &[(usize, usize)]) -> Vec<Position2d> {
|
fn positions(input: &[(usize, usize)]) -> Vec<Position2d> {
|
||||||
input
|
input
|
||||||
@@ -224,28 +292,28 @@ mod tests {
|
|||||||
assert!(Board::is_aligned(&[], Alignment::Vertical));
|
assert!(Board::is_aligned(&[], Alignment::Vertical));
|
||||||
assert!(Board::is_aligned(
|
assert!(Board::is_aligned(
|
||||||
&positions(&[(0, 0)]),
|
&positions(&[(0, 0)]),
|
||||||
Alignment::Horizontal
|
Alignment::Vertical
|
||||||
));
|
));
|
||||||
assert!(Board::is_aligned(
|
assert!(Board::is_aligned(
|
||||||
&positions(&[(0, 0)]),
|
&positions(&[(0, 0)]),
|
||||||
Alignment::Vertical
|
|
||||||
));
|
|
||||||
assert!(Board::is_aligned(
|
|
||||||
&positions(&[(0, 0), (0, 1)]),
|
|
||||||
Alignment::Horizontal
|
Alignment::Horizontal
|
||||||
));
|
));
|
||||||
assert!(Board::is_aligned(
|
assert!(Board::is_aligned(
|
||||||
&positions(&[(0, 0), (1, 0)]),
|
&positions(&[(0, 0), (0, 1)]),
|
||||||
Alignment::Vertical
|
Alignment::Vertical
|
||||||
));
|
));
|
||||||
assert!(!Board::is_aligned(
|
assert!(Board::is_aligned(
|
||||||
&positions(&[(0, 0), (1, 0)]),
|
&positions(&[(0, 0), (1, 0)]),
|
||||||
Alignment::Horizontal
|
Alignment::Horizontal
|
||||||
));
|
));
|
||||||
assert!(!Board::is_aligned(
|
assert!(!Board::is_aligned(
|
||||||
&positions(&[(0, 0), (0, 1)]),
|
&positions(&[(0, 0), (1, 0)]),
|
||||||
Alignment::Vertical
|
Alignment::Vertical
|
||||||
));
|
));
|
||||||
|
assert!(!Board::is_aligned(
|
||||||
|
&positions(&[(0, 0), (0, 1)]),
|
||||||
|
Alignment::Horizontal
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -268,4 +336,37 @@ mod tests {
|
|||||||
assert_eq!(board.find_chains(&[Position2d::new(2, 2)]), expected);
|
assert_eq!(board.find_chains(&[Position2d::new(2, 2)]), expected);
|
||||||
assert_eq!(board.find_chains(&[Position2d::new(4, 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")]
|
#[cfg(feature = "ai")]
|
||||||
pub mod ai;
|
pub mod ai;
|
||||||
pub mod board;
|
pub mod board;
|
||||||
|
@@ -41,8 +41,8 @@ impl Alignment {
|
|||||||
/// Test if two positions are aligned in this direction.
|
/// Test if two positions are aligned in this direction.
|
||||||
pub fn is_aligned(self, a: Position2d, b: Position2d) -> bool {
|
pub fn is_aligned(self, a: Position2d, b: Position2d) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Alignment::Horizontal => a.x == b.x,
|
Alignment::Horizontal => a.y == b.y,
|
||||||
Alignment::Vertical => a.y == b.y,
|
Alignment::Vertical => a.x == b.x,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,6 +67,53 @@ impl Position2d {
|
|||||||
None
|
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 {
|
impl From<(usize, usize)> for Position2d {
|
||||||
|
Reference in New Issue
Block a user