From 0288254f75e62d4161607c1298dbe4a702433704 Mon Sep 17 00:00:00 2001 From: clfreville2 Date: Sat, 25 Mar 2023 12:58:08 +0100 Subject: [PATCH] Test if an expression is placeable in an existing board --- board-shared/src/ai.rs | 140 ++++++++++++++++++++++++++++++++++- board-shared/src/board.rs | 51 ++++++++++++- board-shared/src/position.rs | 32 ++++++++ 3 files changed, 218 insertions(+), 5 deletions(-) diff --git a/board-shared/src/ai.rs b/board-shared/src/ai.rs index 3009c4e..f5e968a 100644 --- a/board-shared/src/ai.rs +++ b/board-shared/src/ai.rs @@ -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, +} + +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> { +/// 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> { + generate_valid_combinations( + CombinationConfig::default(), + SimpleGenerationContext::new(hand), + ) +} + +pub fn generate_valid_combinations( + config: CombinationConfig, + context: impl GenerationContext, +) -> Vec> { let mut combinations = Vec::new(); // Separate numbers and operators @@ -38,8 +85,14 @@ pub fn generate_valid_combinations(hand: &Hand) -> Vec> { let mut trial: Vec = Vec::with_capacity(numbers.len() + operators.len() + 1); let mut tokens: Vec = 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> { 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> { 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)), + ]] + ); + } } diff --git a/board-shared/src/board.rs b/board-shared/src/board.rs index 6dd534f..53cfdff 100644 --- a/board-shared/src/board.rs +++ b/board-shared/src/board.rs @@ -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 { 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, direction: Direction) -> bool { + fn belong_to_same_chain(&self, positions: &Vec, direction: Direction) -> bool { let mut it = positions.iter().copied().peekable(); while let Some(mut pos) = it.next() { if let Some(&next) = it.peek() { diff --git a/board-shared/src/position.rs b/board-shared/src/position.rs index 241ce9b..8836f41 100644 --- a/board-shared/src/position.rs +++ b/board-shared/src/position.rs @@ -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 { + 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)]