Test if an expression is placeable in an existing board

main
Clément FRÉVILLE 2 years ago
parent ac7c9b30de
commit 0288254f75

@ -1,9 +1,42 @@
use crate::board::Board;
use crate::expr::is_valid_guess_of_tokens;
use crate::game::Hand;
use crate::lexer::{lexer_reuse, Token};
use crate::position::{Direction, Grid2d};
use crate::tile::{Digit, Operator, Tile};
use itertools::Itertools;
/// Configuration for the combination generator.
#[derive(Debug, Clone)]
pub struct CombinationConfig {
/// The minimum number of digits to use in the expression.
min_digits: usize,
/// The maximum number of digits to use in the expression.
max_digits: Option<usize>,
}
impl Default for CombinationConfig {
fn default() -> Self {
CombinationConfig {
min_digits: 2,
max_digits: None,
}
}
}
/// Context for the combination generator.
///
/// To limit the number of combinations generated, the generator can be configured
/// to only generate combinations that are valid in the current context.
pub trait GenerationContext {
/// The tiles that are available to the player.
fn tiles(&self) -> &Hand;
/// Verify that a move is playable, i.e. that the expression can be placed on the board.
fn is_playable(&self, tile: &[Tile]) -> bool;
}
fn merge_expression(
numbers: &[&Digit],
operators: &[&Operator],
@ -21,7 +54,21 @@ fn merge_expression(
}
}
pub fn generate_valid_combinations(hand: &Hand) -> Vec<Vec<Tile>> {
/// Generate all possible valid combinations of tiles in a hand.
///
/// To further limit the number of combinations generated, configuration options
/// can directly be passed to the [`generate_valid_combinations`] function.
pub fn generate_valid_all_combinations(hand: &Hand) -> Vec<Vec<Tile>> {
generate_valid_combinations(
CombinationConfig::default(),
SimpleGenerationContext::new(hand),
)
}
pub fn generate_valid_combinations(
config: CombinationConfig,
context: impl GenerationContext,
) -> Vec<Vec<Tile>> {
let mut combinations = Vec::new();
// Separate numbers and operators
@ -38,8 +85,14 @@ pub fn generate_valid_combinations(hand: &Hand) -> Vec<Vec<Tile>> {
let mut trial: Vec<Tile> = Vec::with_capacity(numbers.len() + operators.len() + 1);
let mut tokens: Vec<Token> = Vec::with_capacity(numbers.len() + operators.len() + 1);
let digits_range = {
let min_digits = config.min_digits;
let max_digits = config.max_digits.unwrap_or(numbers.len());
min_digits..=max_digits
};
// Generate all possible permutations, with an increasing number of tiles
for nb_digits in 2..=numbers.len() {
for nb_digits in digits_range {
for digits in numbers.iter().permutations(nb_digits) {
// Then try to place the equals sign at each possible position
// Since equality is commutative, we only need to try half of the positions
@ -48,7 +101,7 @@ pub fn generate_valid_combinations(hand: &Hand) -> Vec<Vec<Tile>> {
for operators in operators.iter().permutations(nb_operators) {
merge_expression(&digits, &operators, equals_idx, &mut trial);
lexer_reuse(&trial, &mut tokens);
if is_valid_guess_of_tokens(&tokens) {
if is_valid_guess_of_tokens(&tokens) && context.is_playable(&trial) {
combinations.push(trial.clone());
}
trial.clear();
@ -61,6 +114,58 @@ pub fn generate_valid_combinations(hand: &Hand) -> Vec<Vec<Tile>> {
combinations
}
/// A convenient implementation of [`GenerationContext`] that can be used when
/// the playability is not relevant.
struct SimpleGenerationContext<'a> {
tiles: &'a Hand,
}
impl<'a> SimpleGenerationContext<'a> {
fn new(tiles: &'a Hand) -> Self {
SimpleGenerationContext { tiles }
}
}
impl<'a> GenerationContext for SimpleGenerationContext<'a> {
fn tiles(&self) -> &Hand {
self.tiles
}
fn is_playable(&self, _tile: &[Tile]) -> bool {
true
}
}
/// A [`GenerationContext`] that can be used to check if a move is playable on
/// a board.
pub struct BoardGenerationContext<'a> {
tiles: &'a Hand,
board: &'a Board,
}
impl<'a> BoardGenerationContext<'a> {
fn new(tiles: &'a Hand, board: &'a Board) -> Self {
BoardGenerationContext { tiles, board }
}
}
impl<'a> GenerationContext for BoardGenerationContext<'a> {
fn tiles(&self) -> &Hand {
self.tiles
}
fn is_playable(&self, tile: &[Tile]) -> bool {
for pos in self.board.row_iter() {
for &direction in &[Direction::Down, Direction::Right] {
if self.board.is_playable(tile, pos, direction) {
return true;
}
}
}
false
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -75,7 +180,34 @@ mod tests {
Tile::Operator(Operator::Add),
Tile::Operator(Operator::Subtract),
]);
let combinations = generate_valid_combinations(&hand);
let combinations = generate_valid_all_combinations(&hand);
assert_eq!(combinations.len(), 4);
}
#[test]
fn generate_combinations_with_board() {
let hand = Hand::new(vec![
Tile::Digit(Digit::new(-5)),
Tile::Digit(Digit::new(-5)),
Tile::Digit(Digit::new(1)),
Tile::Digit(Digit::new(-6)),
Tile::Operator(Operator::Add),
]);
let board = Board::new(3, 3);
board.set_tile(1, 0, Tile::Digit(Digit::new(9)));
board.set_tile(1, 1, Tile::Equals);
board.set_tile(1, 2, Tile::Digit(Digit::new(9)));
let combinations = generate_valid_combinations(
CombinationConfig::default(),
BoardGenerationContext::new(&hand, &board),
);
assert_eq!(
combinations,
vec![vec![
Tile::Digit(Digit::new(-5)),
Tile::Equals,
Tile::Digit(Digit::new(-5)),
]]
);
}
}

@ -97,6 +97,55 @@ impl Board {
&& 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;
for tile in tiles {
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 false;
}
}
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)?;
@ -142,7 +191,7 @@ 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 {
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() {

@ -129,6 +129,38 @@ pub trait Grid2d {
/// Returns the grid height.
fn height(&self) -> usize;
/// Returns an iterator over all positions in the grid.
fn row_iter(&self) -> RowByRowIterator {
RowByRowIterator {
pos: Position2d::new(0, 0),
end: Position2d::new(self.width(), self.height()),
}
}
}
/// Iterator over all positions in a grid.
pub struct RowByRowIterator {
pos: Position2d,
end: Position2d,
}
impl Iterator for RowByRowIterator {
type Item = Position2d;
fn next(&mut self) -> Option<Self::Item> {
if self.pos == self.end {
None
} else {
let pos = self.pos;
self.pos = if self.pos.x == self.end.x - 1 {
Position2d::new(0, self.pos.y + 1)
} else {
Position2d::new(self.pos.x + 1, self.pos.y)
};
Some(pos)
}
}
}
#[cfg(test)]

Loading…
Cancel
Save