commit
fe671b8682
@ -0,0 +1,6 @@
|
|||||||
|
target/
|
||||||
|
Cargo.lock
|
||||||
|
dist/
|
||||||
|
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
@ -0,0 +1,5 @@
|
|||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
"board-shared",
|
||||||
|
"board-frontend",
|
||||||
|
]
|
@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "board-frontend"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "A web app built with Yew"
|
||||||
|
keywords = ["yew", "trunk"]
|
||||||
|
categories = ["gui", "wasm", "web-programming"]
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
[dependencies]
|
||||||
|
yew = { version="0.20", features=["csr"] }
|
||||||
|
board-shared = { path = "../board-shared" }
|
||||||
|
gloo-dialogs = "0.1.1"
|
@ -0,0 +1,9 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Scrabble des chiffres</title>
|
||||||
|
<link data-trunk rel="sass" href="index.scss" />
|
||||||
|
</head>
|
||||||
|
<body></body>
|
||||||
|
</html>
|
@ -0,0 +1,71 @@
|
|||||||
|
:root {
|
||||||
|
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #242424;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-row {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell, .tile {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell {
|
||||||
|
background-color: #888888;
|
||||||
|
margin: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button, .tile {
|
||||||
|
background-color: #535bf2;
|
||||||
|
}
|
||||||
|
.button:hover, .tile:hover {
|
||||||
|
background-color: #646cff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
padding: 0.25rem 1rem;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hand {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
@ -0,0 +1,123 @@
|
|||||||
|
use crate::hand_view::HandView;
|
||||||
|
use crate::tile_view::PlacedTileView;
|
||||||
|
use board_shared::board::Board;
|
||||||
|
use board_shared::expr::is_valid_guess;
|
||||||
|
use board_shared::game::Game;
|
||||||
|
use board_shared::tile::Tile;
|
||||||
|
use gloo_dialogs::alert;
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
enum SelectedTile {
|
||||||
|
InHand(usize),
|
||||||
|
Equals,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
struct BoardViewProps {
|
||||||
|
board: Board,
|
||||||
|
on_click: Callback<(usize, usize)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(BoardView)]
|
||||||
|
fn board_view(BoardViewProps { board, on_click }: &BoardViewProps) -> Html {
|
||||||
|
html! {
|
||||||
|
<table class="board">
|
||||||
|
{ (0..25).map(|x| html! {
|
||||||
|
<tr class="board-row">
|
||||||
|
{ (0..25).map(|y| html! {
|
||||||
|
<PlacedTileView x={x} y={y} key={x} tile={board.get(x, y)} on_click={on_click.clone()} />
|
||||||
|
}).collect::<Html>() }
|
||||||
|
</tr>
|
||||||
|
}).collect::<Html>() }
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(App)]
|
||||||
|
pub fn app() -> Html {
|
||||||
|
let game = use_state(Game::default);
|
||||||
|
let current_game = use_state(Game::default);
|
||||||
|
|
||||||
|
let selected_tile = use_state(|| SelectedTile::None);
|
||||||
|
let on_tile_click = {
|
||||||
|
let game = current_game.clone();
|
||||||
|
let selected_tile = selected_tile.clone();
|
||||||
|
Callback::from(move |(x, y)| {
|
||||||
|
if let SelectedTile::InHand(idx) = *selected_tile {
|
||||||
|
let mut in_hand = game.in_hand.clone();
|
||||||
|
let tile = in_hand.tiles.remove(idx);
|
||||||
|
let mut board = game.board.clone();
|
||||||
|
board.set(x, y, tile);
|
||||||
|
game.set(Game { board, in_hand });
|
||||||
|
selected_tile.set(SelectedTile::None);
|
||||||
|
} else if let SelectedTile::Equals = *selected_tile {
|
||||||
|
let mut board = game.board.clone();
|
||||||
|
board.set(x, y, Tile::Equals);
|
||||||
|
game.set(Game {
|
||||||
|
board,
|
||||||
|
in_hand: game.in_hand.clone(),
|
||||||
|
});
|
||||||
|
selected_tile.set(SelectedTile::None);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let on_tile_select = {
|
||||||
|
let selected_tile = selected_tile.clone();
|
||||||
|
Callback::from(move |idx| {
|
||||||
|
selected_tile.set(SelectedTile::InHand(idx));
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let on_equals_select = {
|
||||||
|
Callback::from(move |_| {
|
||||||
|
selected_tile.set(SelectedTile::Equals);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let on_continue_click = {
|
||||||
|
let current_game = current_game.clone();
|
||||||
|
Callback::from(move |_| {
|
||||||
|
let diff = game.board.difference(¤t_game.board);
|
||||||
|
if let Some(true) = Board::is_contiguous(&diff) {
|
||||||
|
if let Ok(true) = is_valid_guess(¤t_game.board, &diff) {
|
||||||
|
alert("Valid move!");
|
||||||
|
let mut in_hand = current_game.in_hand.clone();
|
||||||
|
in_hand.complete();
|
||||||
|
game.set(Game {
|
||||||
|
board: current_game.board.clone(),
|
||||||
|
in_hand: in_hand.clone()
|
||||||
|
});
|
||||||
|
current_game.set(Game {
|
||||||
|
board: current_game.board.clone(),
|
||||||
|
in_hand,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
alert("Invalid move! (invalid expressions)");
|
||||||
|
current_game.set(Game {
|
||||||
|
board: game.board.clone(),
|
||||||
|
in_hand: game.in_hand.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !diff.is_empty() {
|
||||||
|
alert("Invalid move! (not contiguous)");
|
||||||
|
}
|
||||||
|
let mut in_hand = game.in_hand.clone();
|
||||||
|
in_hand.complete();
|
||||||
|
current_game.set(Game {
|
||||||
|
board: game.board.clone(),
|
||||||
|
in_hand,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
html! {
|
||||||
|
<main>
|
||||||
|
<BoardView board={current_game.board.clone()} on_click={on_tile_click} />
|
||||||
|
<HandView hand={current_game.in_hand.clone()} on_select={on_tile_select} />
|
||||||
|
<div class="row">
|
||||||
|
<button onclick={on_equals_select} class="button">{"="}</button>
|
||||||
|
<button onclick={on_continue_click} class="button">{if current_game.in_hand.tiles.is_empty() { "Start" } else { "Continue" }}</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
use crate::tile_view::TileView;
|
||||||
|
use board_shared::game::Hand;
|
||||||
|
use yew::prelude::*;
|
||||||
|
use yew::{html, Callback, Html};
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
pub struct HandViewProps {
|
||||||
|
pub hand: Hand,
|
||||||
|
pub on_select: Callback<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(HandView)]
|
||||||
|
pub fn hand_view(HandViewProps { hand, on_select }: &HandViewProps) -> Html {
|
||||||
|
let on_select = on_select.clone();
|
||||||
|
html! {
|
||||||
|
<div class="hand">
|
||||||
|
{ hand.tiles.iter().enumerate().map(|(i, tile)| html! {
|
||||||
|
<TileView tile={*tile} key={i} idx={i} on_select={on_select.clone()} />
|
||||||
|
}).collect::<Html>() }
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
mod app;
|
||||||
|
mod hand_view;
|
||||||
|
mod tile_view;
|
||||||
|
|
||||||
|
use app::App;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
yew::Renderer::<App>::new().render();
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
use board_shared::tile::Tile;
|
||||||
|
use yew::prelude::*;
|
||||||
|
use yew::{html, Callback, Html};
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
pub struct PlacedTileViewProps {
|
||||||
|
pub x: usize,
|
||||||
|
pub y: usize,
|
||||||
|
pub tile: Option<Tile>,
|
||||||
|
pub on_click: Callback<(usize, usize)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(PlacedTileView)]
|
||||||
|
pub fn placed_tile_view(
|
||||||
|
PlacedTileViewProps {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
tile,
|
||||||
|
on_click,
|
||||||
|
}: &PlacedTileViewProps,
|
||||||
|
) -> Html {
|
||||||
|
let x = *x;
|
||||||
|
let y = *y;
|
||||||
|
let on_select = {
|
||||||
|
let on_click = on_click.clone();
|
||||||
|
Callback::from(move |_| on_click.emit((x, y)))
|
||||||
|
};
|
||||||
|
html! {
|
||||||
|
<td class="cell" key={y} onclick={on_select}>{ tile.map(|tile| {
|
||||||
|
html! { tile }
|
||||||
|
}).unwrap_or_else(|| {
|
||||||
|
html! { "" }
|
||||||
|
})}</td>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
pub struct TileViewProps {
|
||||||
|
pub tile: Tile,
|
||||||
|
pub on_select: Callback<usize>,
|
||||||
|
pub idx: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(TileView)]
|
||||||
|
pub fn tile_view(
|
||||||
|
TileViewProps {
|
||||||
|
tile,
|
||||||
|
on_select,
|
||||||
|
idx,
|
||||||
|
}: &TileViewProps,
|
||||||
|
) -> Html {
|
||||||
|
let on_select = on_select.clone();
|
||||||
|
let idx = *idx;
|
||||||
|
html! {
|
||||||
|
<div class="tile" onclick={Callback::from(move |_| {
|
||||||
|
on_select.emit(idx)
|
||||||
|
})}>{ tile }</div>
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "board-shared"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
@ -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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
pub mod board;
|
||||||
|
pub mod expr;
|
||||||
|
pub mod game;
|
||||||
|
mod lexer;
|
||||||
|
mod parser;
|
||||||
|
pub mod tile;
|
@ -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),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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(()));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue