diff --git a/.drone.yml b/.drone.yml index 077531d..24b7311 100644 --- a/.drone.yml +++ b/.drone.yml @@ -16,7 +16,7 @@ steps: - name: test image: rust:1.67 commands: - - cargo test + - cargo test --all-features volumes: - name: cargo-cache path: /cache/cargo diff --git a/board-shared/Cargo.toml b/board-shared/Cargo.toml index 667928c..2a3721d 100644 --- a/board-shared/Cargo.toml +++ b/board-shared/Cargo.toml @@ -7,4 +7,8 @@ edition = "2021" [dependencies] enum-map = "2.4.2" +itertools = { version = "0.10.5", optional = true } rand = "0.8.5" + +[features] +ai = ["itertools"] diff --git a/board-shared/src/ai.rs b/board-shared/src/ai.rs new file mode 100644 index 0000000..de2553c --- /dev/null +++ b/board-shared/src/ai.rs @@ -0,0 +1,81 @@ +use crate::expr::are_valid_expressions; +use crate::game::Hand; +use crate::lexer::lexer; +use crate::parser::parse; +use crate::tile::{Digit, Operator, Tile}; +use itertools::Itertools; + +fn merge_expression( + numbers: &[&Digit], + operators: &[&Operator], + equals_idx: usize, + buf: &mut Vec, +) { + let mut op_it = operators.iter(); + for (i, &number) in numbers.iter().enumerate() { + buf.push(Tile::Digit(*number)); + if i == equals_idx { + buf.push(Tile::Equals); + } else if let Some(&&operator) = op_it.next() { + buf.push(Tile::Operator(operator)); + } + } +} + +pub fn generate_valid_combinations(hand: &Hand) -> Vec> { + let mut combinations = Vec::new(); + + // Separate numbers and operators + let mut numbers = Vec::new(); + let mut operators = Vec::new(); + for &tile in &hand.tiles { + match tile { + Tile::Digit(digit) => numbers.push(digit), + Tile::Operator(operator) => operators.push(operator), + _ => (), + } + } + + let mut trial: Vec = Vec::with_capacity(numbers.len() + operators.len() + 1); + + // Generate all possible permutations, with an increasing number of tiles + for nb_digits in 2..=numbers.len() { + 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 + for equals_idx in 0..(nb_digits / 2) { + for operators in operators.iter().permutations(nb_digits - 2) { + merge_expression(&digits, &operators, equals_idx, &mut trial); + if let Ok(tokens) = lexer(&trial) { + if let Ok(expressions) = parse(&tokens) { + if are_valid_expressions(&expressions) { + combinations.push(trial.clone()); + } + } + } + trial.clear(); + } + } + } + } + combinations +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tile::{Digit, Operator}; + + #[test] + fn generate_combinations() { + let hand = Hand::new(vec![ + Tile::Digit(Digit::new(1)), + Tile::Digit(Digit::new(3)), + Tile::Digit(Digit::new(4)), + Tile::Operator(Operator::Add), + Tile::Operator(Operator::Subtract), + ]); + let combinations = generate_valid_combinations(&hand); + assert_eq!(combinations.len(), 4); + } +} diff --git a/board-shared/src/deck.rs b/board-shared/src/deck.rs index b120e65..d4f9dfe 100644 --- a/board-shared/src/deck.rs +++ b/board-shared/src/deck.rs @@ -1,7 +1,7 @@ -use std::error::Error; use crate::tile::Operator; use enum_map::EnumMap; use rand::{thread_rng, Rng}; +use std::error::Error; type DeckSize = u16; type DigitDeck = [DeckSize; 19]; diff --git a/board-shared/src/game.rs b/board-shared/src/game.rs index 7f6517e..2e92f6a 100644 --- a/board-shared/src/game.rs +++ b/board-shared/src/game.rs @@ -14,6 +14,10 @@ pub struct Hand { } impl Hand { + pub fn new(tiles: Vec) -> Self { + Self { tiles } + } + pub fn count_missing_operators(&self) -> usize { 4usize.saturating_sub( self.tiles @@ -38,8 +42,9 @@ impl Hand { .push(Tile::Operator(deck.rand_operator().ok_or(EmptyDeckError)?)); } for _ in 0..self.count_missing_numbers() { - self.tiles - .push(Tile::Digit(Digit::new(deck.rand_digit().ok_or(EmptyDeckError)?))); + self.tiles.push(Tile::Digit(Digit::new( + deck.rand_digit().ok_or(EmptyDeckError)?, + ))); } Ok(()) } @@ -51,4 +56,8 @@ impl Hand { None } } + + pub fn iter(&self) -> impl Iterator { + self.tiles.iter() + } } diff --git a/board-shared/src/lib.rs b/board-shared/src/lib.rs index 4bbf163..41efeea 100644 --- a/board-shared/src/lib.rs +++ b/board-shared/src/lib.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "ai")] +pub mod ai; pub mod board; pub mod deck; pub mod expr; diff --git a/board-shared/src/parser.rs b/board-shared/src/parser.rs index b76395a..46f4f25 100644 --- a/board-shared/src/parser.rs +++ b/board-shared/src/parser.rs @@ -1,5 +1,6 @@ use crate::lexer::Token; use crate::tile::Operator; +use std::fmt; use std::iter::Peekable; #[derive(Debug, Clone, PartialEq, Eq)] @@ -9,6 +10,18 @@ pub enum Expression { Binary(Operator, Box, Box), } +impl fmt::Display for Expression { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Expression::Digit(value) => write!(f, "{}", value), + Expression::Parentheses(expr) => write!(f, "({})", expr), + Expression::Binary(operator, left, right) => { + write!(f, "{} {} {}", left, operator, right) + } + } + } +} + pub type Expressions = Vec; pub fn parse(tokens: &[Token]) -> Result {