Initial commit

This commit is contained in:
2023-01-26 17:21:14 +01:00
commit fe671b8682
17 changed files with 1011 additions and 0 deletions

86
board-shared/src/board.rs Normal file
View File

@@ -0,0 +1,86 @@
use crate::tile::Tile;
const BOARD_SIZE: usize = 25;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Board {
tiles: [Option<Tile>; BOARD_SIZE * BOARD_SIZE],
}
impl Board {
pub fn get(&self, x: usize, y: usize) -> Option<Tile> {
self.tiles[y * BOARD_SIZE + x]
}
pub fn set(&mut self, x: usize, y: usize, tile: Tile) {
self.tiles[y * BOARD_SIZE + x] = Some(tile);
}
pub fn difference(&self, other: &Board) -> Vec<(usize, usize)> {
let mut diff = Vec::new();
for x in 0..BOARD_SIZE {
for y in 0..BOARD_SIZE {
if self.get(x, y) != other.get(x, y) {
diff.push((x, y));
}
}
}
diff
}
pub fn is_contiguous(positions: &[(usize, usize)]) -> Option<bool> {
let mut it = positions.iter();
let first = *it.next()?;
let mut second = *it.next()?;
if first.0 == second.0 {
// Vertical
for &(x, y) in it {
if x != first.0 || y != second.1 + 1 {
return Some(false);
}
second = (x, y);
}
Some(true)
} else if first.1 == second.1 {
// Horizontal
for &(x, y) in it {
if y != first.1 || x != second.0 + 1 {
return Some(false);
}
second = (x, y);
}
Some(true)
} else {
Some(false)
}
}
}
impl Default for Board {
fn default() -> Self {
Self {
tiles: [None; BOARD_SIZE * BOARD_SIZE],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_contiguous() {
assert_eq!(Board::is_contiguous(&[]), None);
assert_eq!(Board::is_contiguous(&[(0, 0)]), None);
assert_eq!(Board::is_contiguous(&[(0, 0), (0, 1), (0, 2)]), Some(true));
assert_eq!(
Board::is_contiguous(&[(1, 0), (2, 0), (3, 0), (4, 0)]),
Some(true)
);
assert_eq!(Board::is_contiguous(&[(0, 0), (0, 1), (1, 3)]), Some(false));
assert_eq!(
Board::is_contiguous(&[(0, 0), (0, 1), (0, 2), (1, 2)]),
Some(false)
);
}
}

91
board-shared/src/expr.rs Normal file
View File

@@ -0,0 +1,91 @@
use crate::board::Board;
use crate::lexer::lexer;
use crate::parser;
use crate::parser::{Expression, Expressions};
use crate::tile::{Operator, Tile};
pub fn calculate(expr: &Expression) -> f64 {
match expr {
Expression::Digit(value) => *value as f64,
Expression::Parentheses(expr) => calculate(expr),
Expression::Binary(operator, left, right) => {
let left = calculate(left);
let right = calculate(right);
match operator {
Operator::Add => left + right,
Operator::Subtract => left - right,
Operator::Multiply => left * right,
Operator::Divide => left / right,
}
}
}
}
pub fn are_valid_expressions(expr: &Expressions) -> bool {
let mut res: Option<f64> = None;
for expr in expr {
let value = calculate(expr);
if let Some(res) = res {
if res != value {
return false;
}
} else {
res = Some(value);
}
}
res.is_some()
}
pub fn is_valid_guess(board: &Board, positions: &[(usize, usize)]) -> Result<bool, ()> {
let tiles = positions
.iter()
.map(|&(x, y)| board.get(x, y))
.collect::<Option<Vec<Tile>>>()
.ok_or(())?;
let tokens = lexer(&tiles)?;
let expressions = parser::parse(&tokens)?;
Ok(are_valid_expressions(&expressions))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_calculate() {
let expr = Expression::Binary(
Operator::Add,
Box::new(Expression::Digit(1)),
Box::new(Expression::Digit(2)),
);
assert_eq!(calculate(&expr), 3.0);
}
#[test]
fn test_are_valid_expressions() {
let expr = vec![
Expression::Binary(
Operator::Add,
Box::new(Expression::Digit(3)),
Box::new(Expression::Digit(4)),
),
Expression::Binary(
Operator::Add,
Box::new(Expression::Digit(6)),
Box::new(Expression::Digit(1)),
),
];
assert!(are_valid_expressions(&expr));
let expr = vec![
Expression::Digit(9),
Expression::Binary(
Operator::Add,
Box::new(Expression::Digit(7)),
Box::new(Expression::Digit(1)),
),
];
assert!(!are_valid_expressions(&expr));
}
}

42
board-shared/src/game.rs Normal file
View File

@@ -0,0 +1,42 @@
use crate::board::Board;
use crate::tile::{Digit, Operator, Tile};
#[derive(Default, Debug, Clone, PartialEq)]
pub struct Game {
pub board: Board,
pub in_hand: Hand,
}
#[derive(Default, Debug, Clone, PartialEq)]
pub struct Hand {
pub tiles: Vec<Tile>,
}
impl Hand {
pub fn count_missing_operators(&self) -> usize {
4usize.saturating_sub(
self.tiles
.iter()
.filter(|tile| matches!(tile, Tile::Operator(_)))
.count(),
)
}
pub fn count_missing_numbers(&self) -> usize {
8usize.saturating_sub(
self.tiles
.iter()
.filter(|tile| matches!(tile, Tile::Digit(_)))
.count(),
)
}
pub fn complete(&mut self) {
for _ in 0..self.count_missing_operators() {
self.tiles.push(Tile::Operator(Operator::Add));
}
for n in 0..self.count_missing_numbers() {
self.tiles.push(Tile::Digit(Digit::new((n % 10) as u8)));
}
}
}

145
board-shared/src/lexer.rs Normal file
View File

@@ -0,0 +1,145 @@
use crate::tile::{Operator, Tile};
use std::fmt;
#[derive(Debug, PartialEq, Clone)]
pub enum Token {
NumberLiteral(u64),
Operator(Operator),
LeftParen,
RightParen,
Equals,
}
impl fmt::Display for Token {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Token::NumberLiteral(value) => write!(f, "{value}"),
Token::Operator(operator) => write!(f, "{operator}"),
Token::LeftParen => write!(f, "("),
Token::RightParen => write!(f, ")"),
Token::Equals => write!(f, "="),
}
}
}
/// Tokenize a sequence of tiles into tokens.
pub fn lexer(input: &[Tile]) -> Result<Vec<Token>, ()> {
let mut result = Vec::new();
let mut it = input.iter().peekable();
while let Some(&c) = it.peek() {
match c {
Tile::Digit(digit) => {
let mut has_right_parenthesis = digit.has_right_parenthesis;
if digit.has_left_parenthesis {
result.push(Token::LeftParen);
}
let mut value = digit.value as u64;
it.next();
while let Some(&Tile::Digit(digit)) = it.peek() {
if digit.has_left_parenthesis {
break;
}
value = value * 10 + digit.value as u64;
it.next();
if digit.has_right_parenthesis {
has_right_parenthesis = true;
break;
}
}
result.push(Token::NumberLiteral(value));
if has_right_parenthesis {
result.push(Token::RightParen);
}
}
Tile::Operator(operator) => {
result.push(Token::Operator(*operator));
it.next();
}
Tile::Equals => {
result.push(Token::Equals);
it.next();
}
}
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lex() {
let input = vec![
Tile::Digit(Digit::new(1)),
Tile::Digit(Digit::new(2)),
Tile::Digit(Digit::new(3)),
Tile::Operator(Operator::Add),
Tile::Digit(Digit::new(4)),
Tile::Digit(Digit::new(5)),
Tile::Digit(Digit::new(6)),
Tile::Equals,
];
let expected = vec![
Token::NumberLiteral(123),
Token::Operator(Operator::Add),
Token::NumberLiteral(456),
Token::Equals,
];
assert_eq!(lexer(&input).unwrap(), expected);
}
#[test]
fn test_lex_parentheses() {
let input = vec![
Tile::Digit(Digit::new(1)),
Tile::Operator(Operator::Subtract),
Tile::Digit(Digit {
value: 2,
has_left_parenthesis: true,
has_right_parenthesis: false,
}),
Tile::Operator(Operator::Add),
Tile::Digit(Digit {
value: 3,
has_left_parenthesis: false,
has_right_parenthesis: true,
}),
];
let expected = vec![
Token::NumberLiteral(1),
Token::Operator(Operator::Subtract),
Token::LeftParen,
Token::NumberLiteral(2),
Token::Operator(Operator::Add),
Token::NumberLiteral(3),
Token::RightParen,
];
assert_eq!(lexer(&input).unwrap(), expected);
}
#[test]
fn test_lex_parentheses_long() {
let input = vec![
Tile::Digit(Digit {
value: 1,
has_left_parenthesis: true,
has_right_parenthesis: false,
}),
Tile::Digit(Digit::new(2)),
Tile::Digit(Digit::new(3)),
Tile::Digit(Digit {
value: 4,
has_left_parenthesis: false,
has_right_parenthesis: true,
}),
];
let expected = vec![
Token::LeftParen,
Token::NumberLiteral(1234),
Token::RightParen,
];
assert_eq!(lexer(&input).unwrap(), expected);
}
}

6
board-shared/src/lib.rs Normal file
View File

@@ -0,0 +1,6 @@
pub mod board;
pub mod expr;
pub mod game;
mod lexer;
mod parser;
pub mod tile;

140
board-shared/src/parser.rs Normal file
View File

@@ -0,0 +1,140 @@
use crate::lexer::Token;
use crate::tile::Operator;
use std::iter::Peekable;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Expression {
Digit(u64),
Parentheses(Box<Expression>),
Binary(Operator, Box<Expression>, Box<Expression>),
}
pub type Expressions = Vec<Expression>;
pub fn parse(tokens: &[Token]) -> Result<Expressions, ()> {
let mut tokens = tokens.iter().peekable();
let mut expressions = Vec::new();
while tokens.peek().is_some() {
expressions.push(parse_expression(&mut tokens)?);
tokens.next();
}
Ok(expressions)
}
fn parse_expression<'a>(
tokens: &mut Peekable<impl Iterator<Item = &'a Token>>,
) -> Result<Expression, ()> {
let mut left = parse_term(tokens)?;
while let Some(Token::Operator(operator)) = tokens.peek() {
let operator = *operator;
tokens.next();
let right = parse_term(tokens)?;
left = Expression::Binary(operator, Box::new(left), Box::new(right));
}
Ok(left)
}
fn parse_term<'a>(
tokens: &mut Peekable<impl Iterator<Item = &'a Token>>,
) -> Result<Expression, ()> {
let mut left = parse_factor(tokens)?;
while let Some(Token::Operator(operator)) = tokens.peek() {
let operator = *operator;
tokens.next();
let right = parse_factor(tokens)?;
left = Expression::Binary(operator, Box::new(left), Box::new(right));
}
Ok(left)
}
fn parse_factor<'a>(
tokens: &mut Peekable<impl Iterator<Item = &'a Token>>,
) -> Result<Expression, ()> {
match tokens.next() {
Some(Token::NumberLiteral(value)) => Ok(Expression::Digit(*value)),
Some(Token::LeftParen) => {
let expression = parse_expression(tokens)?;
if let Some(Token::RightParen) = tokens.next() {
Ok(Expression::Parentheses(Box::new(expression)))
} else {
Err(())
}
}
_ => Err(()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse() {
let tokens = vec![
Token::NumberLiteral(1),
Token::Operator(Operator::Add),
Token::NumberLiteral(2),
Token::Operator(Operator::Multiply),
Token::NumberLiteral(3),
];
let expression = parse(&tokens).unwrap();
assert_eq!(
expression,
vec![Expression::Binary(
Operator::Multiply,
Box::new(Expression::Binary(
Operator::Add,
Box::new(Expression::Digit(1)),
Box::new(Expression::Digit(2)),
)),
Box::new(Expression::Digit(3)),
)],
);
}
#[test]
fn test_parse_parentheses() {
let tokens = vec![
Token::LeftParen,
Token::NumberLiteral(1),
Token::Operator(Operator::Add),
Token::NumberLiteral(2),
Token::RightParen,
Token::Operator(Operator::Multiply),
Token::NumberLiteral(3),
];
let expression = parse(&tokens).unwrap();
assert_eq!(
expression,
vec![Expression::Binary(
Operator::Multiply,
Box::new(Expression::Parentheses(Box::new(Expression::Binary(
Operator::Add,
Box::new(Expression::Digit(1)),
Box::new(Expression::Digit(2)),
)))),
Box::new(Expression::Digit(3)),
)],
);
}
#[test]
fn test_parse_equals() {
let tokens = vec![
Token::NumberLiteral(1),
Token::Equals,
Token::NumberLiteral(2),
Token::Equals,
Token::NumberLiteral(3),
];
let expression = parse(&tokens).unwrap();
assert_eq!(
expression,
vec![
Expression::Digit(1),
Expression::Digit(2),
Expression::Digit(3),
],
);
}
}

176
board-shared/src/tile.rs Normal file
View File

@@ -0,0 +1,176 @@
use std::fmt;
/// A single digit that can be wrapped in parentheses.
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub struct Digit {
pub value: u8,
pub has_left_parenthesis: bool,
pub has_right_parenthesis: bool,
}
impl Digit {
pub fn new(value: u8) -> Self {
Self {
value,
has_left_parenthesis: false,
has_right_parenthesis: false,
}
}
}
impl fmt::Display for Digit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.has_left_parenthesis {
write!(f, "(")?;
}
write!(f, "{}", self.value)?;
if self.has_right_parenthesis {
write!(f, ")")?;
}
Ok(())
}
}
impl TryFrom<&str> for Digit {
type Error = ();
fn try_from(value: &str) -> Result<Self, Self::Error> {
let mut res = Digit {
value: 0,
has_left_parenthesis: false,
has_right_parenthesis: false,
};
let mut it = value.chars();
let c = it.next().ok_or(())?;
if c == '(' {
res.has_left_parenthesis = true;
res.value = it.next().ok_or(())?.to_digit(10).ok_or(())? as u8;
} else {
res.value = c.to_digit(10).ok_or(())? as u8;
}
if let Some(c) = it.next() {
if c != ')' {
return Err(());
}
res.has_right_parenthesis = true;
}
Ok(res)
}
}
/// An operator that can be applied between two terms.
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum Operator {
Add,
Subtract,
Multiply,
Divide,
}
impl fmt::Display for Operator {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Operator::Add => write!(f, "+"),
Operator::Subtract => write!(f, "-"),
Operator::Multiply => write!(f, "*"),
Operator::Divide => write!(f, "/"),
}
}
}
impl TryFrom<char> for Operator {
type Error = ();
fn try_from(value: char) -> Result<Self, Self::Error> {
match value {
'+' => Ok(Operator::Add),
'-' => Ok(Operator::Subtract),
'*' => Ok(Operator::Multiply),
'/' => Ok(Operator::Divide),
_ => Err(()),
}
}
}
/// A single piece of a mathematical expression.
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum Tile {
Digit(Digit),
Operator(Operator),
Equals,
}
impl TryFrom<&str> for Tile {
type Error = ();
fn try_from(value: &str) -> Result<Self, Self::Error> {
if let Ok(digit) = Digit::try_from(value) {
return Ok(Tile::Digit(digit));
}
match value {
"=" => Ok(Tile::Equals),
_ => Ok(Tile::Operator(value.chars().next().ok_or(())?.try_into()?)),
}
}
}
impl fmt::Display for Tile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Tile::Digit(digit) => write!(f, "{digit}"),
Tile::Operator(operator) => write!(f, "{operator}"),
Tile::Equals => write!(f, "="),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn digit_from_str() {
assert_eq!(Digit::try_from("1"), Ok(Digit::new(1)));
assert_eq!(
Digit::try_from("(5"),
Ok(Digit {
value: 5,
has_left_parenthesis: true,
has_right_parenthesis: false,
})
);
assert_eq!(
Digit::try_from("8)"),
Ok(Digit {
value: 8,
has_left_parenthesis: false,
has_right_parenthesis: true,
})
);
assert_eq!(Digit::try_from("+"), Err(()));
assert_eq!(Digit::try_from("1("), Err(()));
assert_eq!(Digit::try_from(""), Err(()));
}
#[test]
fn operator_from_str() {
assert_eq!(Operator::try_from('+'), Ok(Operator::Add));
assert_eq!(Operator::try_from('-'), Ok(Operator::Subtract));
assert_eq!(Operator::try_from('²'), Err(()));
}
#[test]
fn piece_from_str() {
assert_eq!(Tile::try_from("+"), Ok(Tile::Operator(Operator::Add)));
assert_eq!(
Tile::try_from("(7)"),
Ok(Tile::Digit(Digit {
value: 7,
has_left_parenthesis: true,
has_right_parenthesis: true,
}))
);
assert_eq!(Tile::try_from("="), Ok(Tile::Equals));
assert_eq!(Tile::try_from(""), Err(()));
}
}