diff --git a/.drone.yml b/.drone.yml index c20a29e..2e2d263 100644 --- a/.drone.yml +++ b/.drone.yml @@ -4,26 +4,21 @@ name: default steps: - name: build - image: rust:1.68 + image: rust:1.77 commands: - cargo build - volumes: + volumes: &caches - name: cargo-cache path: /cache/cargo - environment: + environment: &env CARGO_HOME: /cache/cargo - CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse - name: test - image: rust:1.68 + image: rust:1.77 commands: - cargo test --all-features - volumes: - - name: cargo-cache - path: /cache/cargo - environment: - CARGO_HOME: /cache/cargo - CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse + volumes: *caches + environment: *env depends_on: - build diff --git a/Cargo.toml b/Cargo.toml index d660e5d..b23e17b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ strip = true panic = "abort" [workspace] +resolver = "2" members = [ "board-shared", "board-frontend", diff --git a/board-frontend/Cargo.toml b/board-frontend/Cargo.toml index edca37f..6fe961d 100644 --- a/board-frontend/Cargo.toml +++ b/board-frontend/Cargo.toml @@ -8,13 +8,13 @@ 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"] } +yew = { version = "0.21.0", features=["csr"] } board-shared = { path = "../board-shared" } board-network = { path = "../board-network" } -gloo-dialogs = "0.1.1" +gloo-dialogs = "0.2.0" getrandom = { version = "0.2.8", features = ["js"] } -gloo-net = "0.2.6" +gloo-net = "0.5.0" futures = "0.3.26" serde_json = "1.0.93" -gloo-utils = "0.1.6" +gloo-utils = "0.2.0" web-sys = "0.3.61" diff --git a/board-frontend/src/hand_view.rs b/board-frontend/src/hand_view.rs index d071835..a4042f5 100644 --- a/board-frontend/src/hand_view.rs +++ b/board-frontend/src/hand_view.rs @@ -1,7 +1,7 @@ use crate::tile_view::TileView; use board_shared::game::Hand; +use yew::html; use yew::prelude::*; -use yew::{html, Callback, Html}; #[derive(Properties, PartialEq)] pub struct HandViewProps { diff --git a/board-frontend/src/remote_view.rs b/board-frontend/src/remote_view.rs index ee01749..06ae6a5 100644 --- a/board-frontend/src/remote_view.rs +++ b/board-frontend/src/remote_view.rs @@ -43,7 +43,7 @@ pub fn remote_game_view( }: &RemoteGameViewProps, ) -> Html { macro_rules! send_client_message { - ($write:expr, $message:expr) => {{ + ($write:ident, $message:expr) => {{ let write = $write.clone(); spawn_local(async move { write @@ -68,61 +68,54 @@ pub fn remote_game_view( let player_name = player_name.clone(); let room_name = room_name.clone(); let write = write.clone(); - use_effect_with_deps( - move |_| { - send_client_message!( - write, - if let Some(room_name) = room_name { - ClientMessage::JoinRoom(room_name, player_name) - } else { - ClientMessage::CreateRoom(player_name) - } - ); - }, - (), - ); + use_effect(move || { + send_client_message!( + write, + if let Some(room_name) = room_name { + ClientMessage::JoinRoom(room_name, player_name) + } else { + ClientMessage::CreateRoom(player_name) + } + ); + }); let is_started = is_started.clone(); let current_player_turn = current_player_turn.clone(); let read = read.clone(); - use_effect_with_deps( - move |_| { - spawn_local(async move { - while let Some(event) = read.borrow_mut().next().await { - if let Message::Text(msg) = event.unwrap() { - match serde_json::from_str::(&msg) { - Ok(ServerMessage::JoinedRoom { - room_name, - has_started, - .. - }) => { - alert(&format!("Joined room {room_name}")); - is_started.set(has_started); - } - Ok(ServerMessage::PlayerTurn(player_id)) => { - current_player_turn.set(player_id); - is_started.set(true); - } - Ok(ServerMessage::SyncHand(hand)) => { - in_hand - .set(Hand::new(hand.iter().map(|&x| x.into()).collect())); - } - Ok(ServerMessage::TilePlaced(pos, tile)) => { - let mut changed = board.deref().clone(); - changed.set(pos.x, pos.y, tile.into()); - board.set(changed); - } - r => { - alert(&format!("{r:?}")); - } - }; - } + use_effect(move || { + spawn_local(async move { + while let Some(event) = read.borrow_mut().next().await { + if let Message::Text(msg) = event.unwrap() { + match serde_json::from_str::(&msg) { + Ok(ServerMessage::JoinedRoom { + room_name, + has_started, + .. + }) => { + alert(&format!("Joined room {room_name}")); + is_started.set(has_started); + } + Ok(ServerMessage::PlayerTurn(player_id)) => { + current_player_turn.set(player_id); + is_started.set(true); + } + Ok(ServerMessage::SyncHand(hand)) => { + in_hand.set(Hand::new(hand.iter().map(|&x| x.into()).collect())); + } + Ok(ServerMessage::TilePlaced(pos, tile)) => { + let mut changed = board.deref().clone(); + changed.set(pos.x, pos.y, tile.into()); + board.set(changed); + } + r => { + alert(&format!("{r:?}")); + } + }; } - }); - || {} - }, - (), - ); + } + }); + || {} + }); } let on_tile_select = { diff --git a/board-frontend/src/tile_view.rs b/board-frontend/src/tile_view.rs index 1992c5e..5e6e9f8 100644 --- a/board-frontend/src/tile_view.rs +++ b/board-frontend/src/tile_view.rs @@ -1,6 +1,6 @@ use board_shared::tile::Tile; +use yew::html; use yew::prelude::*; -use yew::{html, Callback, Html}; #[derive(Properties, PartialEq)] pub struct PlacedTileViewProps { @@ -54,6 +54,6 @@ pub fn tile_view( html! {
{ tile }
+ })}>{ tile.to_string() } } } diff --git a/board-network/src/lib.rs b/board-network/src/lib.rs index c087a5c..1b800ec 100644 --- a/board-network/src/lib.rs +++ b/board-network/src/lib.rs @@ -1,2 +1 @@ pub mod protocol; -pub mod types; diff --git a/board-network/src/protocol.rs b/board-network/src/protocol.rs index 4cf239b..5186f67 100644 --- a/board-network/src/protocol.rs +++ b/board-network/src/protocol.rs @@ -1,4 +1,6 @@ -use crate::types::{BoardRef, Position2dRef, TileRef}; +use board_shared::board::Board; +use board_shared::position::Position2d; +use board_shared::tile::Tile; use serde::{Deserialize, Serialize}; /// A message sent by the client to the server. @@ -19,11 +21,11 @@ pub enum ClientMessage { /// Try to place a tile from the hand on the board. /// /// The server will validate the move and answer with a TilePlaced if the message is valid. - TileUse(Position2dRef, usize), + TileUse(Position2d, usize), /// Try to place an equal sign on the board. - TilePlaceEqual(Position2dRef), + TilePlaceEqual(Position2d), /// Try to remove a tile from the board to add it to the hand. - TileTake(Position2dRef), + TileTake(Position2d), /// Get the server to validate the current player moves. Validate, } @@ -35,7 +37,7 @@ pub enum ServerMessage { JoinedRoom { room_name: String, players: Vec<(String, u32, bool)>, - board: BoardRef, + board: Board, active_player: usize, has_started: bool, }, @@ -50,13 +52,13 @@ pub enum ServerMessage { /// Change the current player PlayerTurn(usize), /// Update the current hand of the player - SyncHand(Vec), + SyncHand(Vec), /// Update the score of the n-th player. SyncScore(usize, u32), /// Informs that a tile has been placed - TilePlaced(Position2dRef, TileRef), + TilePlaced(Position2d, Tile), /// Informs that a tile has been removed - TileRemoved(Position2dRef), + TileRemoved(Position2d), TurnRejected(String), /// Notify that the game has ended. GameOver, diff --git a/board-network/src/types.rs b/board-network/src/types.rs deleted file mode 100644 index 8ca4ffc..0000000 --- a/board-network/src/types.rs +++ /dev/null @@ -1,134 +0,0 @@ -use board_shared::{ - board::Board, - position::{Grid2d, Position2d}, - tile::{Digit, Operator, Tile}, -}; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct BoardRef { - pub tiles: Vec>, - pub width: usize, - pub height: usize, -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize)] -pub enum TileRef { - Digit(DigitRef), - Operator(OperatorRef), - Equals, -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize)] -pub struct DigitRef { - pub value: i8, - pub has_left_parenthesis: bool, - pub has_right_parenthesis: bool, -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize)] -pub enum OperatorRef { - Add, - Subtract, - Multiply, - Divide, -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize)] -pub struct Position2dRef { - pub x: usize, - pub y: usize, -} - -impl From<&Board> for BoardRef { - fn from(value: &Board) -> Self { - Self { - tiles: value - .iter() - .map(|tile| tile.map(Into::into)) - .collect::>(), - width: value.width(), - height: value.height(), - } - } -} - -impl From for TileRef { - fn from(value: Tile) -> Self { - match value { - Tile::Digit(digit) => TileRef::Digit(digit.into()), - Tile::Operator(operator) => TileRef::Operator(operator.into()), - Tile::Equals => TileRef::Equals, - } - } -} - -impl From for Tile { - fn from(value: TileRef) -> Self { - match value { - TileRef::Digit(digit) => Tile::Digit(digit.into()), - TileRef::Operator(operator) => Tile::Operator(operator.into()), - TileRef::Equals => Tile::Equals, - } - } -} - -impl From for DigitRef { - fn from(value: Digit) -> Self { - Self { - value: value.value, - has_left_parenthesis: value.has_left_parenthesis, - has_right_parenthesis: value.has_right_parenthesis, - } - } -} - -impl From for Digit { - fn from(value: DigitRef) -> Self { - Self { - value: value.value, - has_left_parenthesis: value.has_left_parenthesis, - has_right_parenthesis: value.has_right_parenthesis, - } - } -} - -impl From for OperatorRef { - fn from(value: Operator) -> Self { - match value { - Operator::Add => OperatorRef::Add, - Operator::Subtract => OperatorRef::Subtract, - Operator::Multiply => OperatorRef::Multiply, - Operator::Divide => OperatorRef::Divide, - } - } -} - -impl From for Operator { - fn from(value: OperatorRef) -> Self { - match value { - OperatorRef::Add => Operator::Add, - OperatorRef::Subtract => Operator::Subtract, - OperatorRef::Multiply => Operator::Multiply, - OperatorRef::Divide => Operator::Divide, - } - } -} - -impl From for Position2dRef { - fn from(value: Position2d) -> Self { - Self { - x: value.x, - y: value.y, - } - } -} - -impl From for Position2d { - fn from(value: Position2dRef) -> Self { - Self { - x: value.x, - y: value.y, - } - } -} diff --git a/board-server/Cargo.toml b/board-server/Cargo.toml index f14cc12..11c55d8 100644 --- a/board-server/Cargo.toml +++ b/board-server/Cargo.toml @@ -7,13 +7,13 @@ edition = "2021" [dependencies] futures = { version = "0.3" } -board-shared = { path = "../board-shared" } +board-shared = { path = "../board-shared", features = ["serde"] } board-network = { path = "../board-network" } -smol = "1.3.0" -async-tungstenite = "0.20.0" -tungstenite = "0.18.0" +smol = "2.0.0" +async-tungstenite = "0.25.0" +tungstenite = "0.21.0" anyhow = "1.0.69" rand = "0.8.5" serde_json = "1.0.93" -redis = { version = "0.22.3", features = ["aio", "async-std-comp"] } +redis = { version = "0.25.2", features = ["aio", "async-std-comp"] } async-trait = "0.1.66" diff --git a/board-server/Dockerfile b/board-server/Dockerfile index 7b8bc11..4044bf7 100644 --- a/board-server/Dockerfile +++ b/board-server/Dockerfile @@ -1,6 +1,5 @@ -FROM rust:1.68.0-slim as builder +FROM rust:1.77.0-slim as builder -ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse WORKDIR /usr/src/board # Build with musl to statically link diff --git a/board-server/src/leaderboard.rs b/board-server/src/leaderboard.rs index 21814e8..7621568 100644 --- a/board-server/src/leaderboard.rs +++ b/board-server/src/leaderboard.rs @@ -29,13 +29,13 @@ impl RedisLeaderboard { #[async_trait] impl Leaderboard for RedisLeaderboard { async fn add_score(&self, player_name: &str, score: u32) -> Result<(), RedisError> { - let mut con = self.client.get_async_connection().await?; + let mut con = self.client.get_multiplexed_async_connection().await?; con.zadd(LEADERBOARD, player_name, score).await?; Ok(()) } async fn get_highscores(&self) -> Result, RedisError> { - let mut con = self.client.get_async_connection().await?; + let mut con = self.client.get_multiplexed_async_connection().await?; let count: isize = con.zcard(LEADERBOARD).await?; let leaderboard: Vec = con .zrange_withscores(LEADERBOARD, 0, (count - 1).min(LEADERBOARD_SIZE)) diff --git a/board-server/src/room.rs b/board-server/src/room.rs index f368df2..383d924 100644 --- a/board-server/src/room.rs +++ b/board-server/src/room.rs @@ -1,7 +1,6 @@ use crate::leaderboard::{InMemoryLeaderboard, Leaderboard}; use crate::player::Player; use board_network::protocol::{ClientMessage, ServerMessage}; -use board_network::types::TileRef; use board_shared::board::Board; use board_shared::deck::RngDeck; use board_shared::expr::is_valid_guess; @@ -87,7 +86,7 @@ impl Room { .iter() .map(|p| (p.name.clone(), p.score, p.ws.is_some())) .collect(), - board: (&self.board).into(), + board: self.board.clone(), active_player: self.active_player, has_started: self.has_started, })?; @@ -328,13 +327,7 @@ impl Room { fn sync_hand(&mut self, player_id: usize) { self.send( player_id, - ServerMessage::SyncHand( - self.players[player_id] - .hand - .iter() - .map(|t| >::into(*t)) - .collect::>(), - ), + ServerMessage::SyncHand(self.players[player_id].hand.tiles.clone()), ); } } diff --git a/board-shared/Cargo.toml b/board-shared/Cargo.toml index 2a3721d..5657e3b 100644 --- a/board-shared/Cargo.toml +++ b/board-shared/Cargo.toml @@ -7,8 +7,10 @@ edition = "2021" [dependencies] enum-map = "2.4.2" -itertools = { version = "0.10.5", optional = true } +itertools = { version = "0.12.1", optional = true } rand = "0.8.5" +serde = { version = "1.0.197", features = ["derive"], optional = true } [features] -ai = ["itertools"] +ai = ["dep:itertools"] +serde = ["dep:serde"] diff --git a/board-shared/src/board.rs b/board-shared/src/board.rs index f2d980a..cb9d723 100644 --- a/board-shared/src/board.rs +++ b/board-shared/src/board.rs @@ -12,6 +12,7 @@ const DEFAULT_BOARD_SIZE: usize = 19; /// to create a new board with a default size. To create a board with a custom /// size, use [`Board::new()`]. #[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Board { tiles: Vec>, width: usize, @@ -192,7 +193,7 @@ impl Board { } /// Tests whether the given positions are part of the same chain. - fn belong_to_same_chain(&self, positions: &Vec, direction: Direction) -> bool { + fn belong_to_same_chain(&self, positions: &[Position2d], 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/game.rs b/board-shared/src/game.rs index 40504d1..7528935 100644 --- a/board-shared/src/game.rs +++ b/board-shared/src/game.rs @@ -54,11 +54,7 @@ impl Hand { } pub fn remove(&mut self, idx: usize) -> Option { - if idx < self.tiles.len() { - Some(self.tiles.remove(idx)) - } else { - None - } + (idx < self.tiles.len()).then(|| self.tiles.remove(idx)) } pub fn iter(&self) -> impl Iterator { diff --git a/board-shared/src/lexer.rs b/board-shared/src/lexer.rs index 777cc1d..f0a2ed1 100644 --- a/board-shared/src/lexer.rs +++ b/board-shared/src/lexer.rs @@ -83,7 +83,7 @@ mod tests { #[test] fn test_lex() { - let input = vec![ + let input = [ Tile::Digit(Digit::new(1)), Tile::Digit(Digit::new(2)), Tile::Digit(Digit::new(3)), @@ -104,7 +104,7 @@ mod tests { #[test] fn test_lex_parentheses() { - let input = vec![ + let input = [ Tile::Digit(Digit::new(1)), Tile::Operator(Operator::Subtract), Tile::Digit(Digit { @@ -133,7 +133,7 @@ mod tests { #[test] fn test_lex_parentheses_long() { - let input = vec![ + let input = [ Tile::Digit(Digit { value: 1, has_left_parenthesis: true, diff --git a/board-shared/src/lib.rs b/board-shared/src/lib.rs index 7ca103b..4146849 100644 --- a/board-shared/src/lib.rs +++ b/board-shared/src/lib.rs @@ -1,15 +1,15 @@ -///! # Scrabble with Numbers -///! -///! This crate provides the core game logic for the Scrabble with Numbers game. -///! It can be used standalone, or as a library for other projects. -///! -///! ## Features -///! - Create and verify game expressions, such as `2*3+4*5`. -///! - Generate valid automatic moves for a given board state, with the `ai` feature. -///! - Check if a player move valid. -///! - Calculate the score of an expression. -///! -///! If you are looking for a server implementation, see the `board-server` crate. +//! # Scrabble with Numbers +//! +//! This crate provides the core game logic for the Scrabble with Numbers game. +//! It can be used standalone, or as a library for other projects. +//! +//! ## Features +//! - Create and verify game expressions, such as `2*3+4*5`. +//! - Generate valid automatic moves for a given board state, with the `ai` feature. +//! - Check if a player move valid. +//! - Calculate the score of an expression. +//! +//! If you are looking for a server implementation, see the `board-server` crate. #[cfg(feature = "ai")] pub mod ai; diff --git a/board-shared/src/parser.rs b/board-shared/src/parser.rs index 9c75f77..aee4915 100644 --- a/board-shared/src/parser.rs +++ b/board-shared/src/parser.rs @@ -39,20 +39,17 @@ pub fn parse(tokens: &[Token]) -> Result { fn parse_primary<'a>( tokens: &mut Peekable>, ) -> Result { - if let Some(Token::NumberLiteral(value)) = tokens.peek() { - tokens.next(); - Ok(Expression::Digit(*value)) - } else if let Some(Token::LeftParen) = tokens.peek() { - tokens.next(); - let expr = parse_term(tokens)?; - if let Some(Token::RightParen) = tokens.peek() { - tokens.next(); - Ok(Expression::Parentheses(Box::new(expr))) - } else { - Err(()) + match tokens.next() { + Some(Token::NumberLiteral(value)) => Ok(Expression::Digit(*value)), + Some(Token::LeftParen) => { + let expr = parse_term(tokens)?; + if let Some(Token::RightParen) = tokens.next() { + Ok(Expression::Parentheses(Box::new(expr))) + } else { + Err(()) + } } - } else { - Err(()) + _ => Err(()), } } @@ -104,7 +101,7 @@ mod tests { #[test] fn test_parse() { - let tokens = vec![ + let tokens = [ Token::NumberLiteral(1), Token::Operator(Operator::Add), Token::NumberLiteral(2), @@ -128,7 +125,7 @@ mod tests { #[test] fn test_parse_reverse() { - let tokens = vec![ + let tokens = [ Token::NumberLiteral(2), Token::Operator(Operator::Multiply), Token::NumberLiteral(3), @@ -152,7 +149,7 @@ mod tests { #[test] fn test_parse_parentheses() { - let tokens = vec![ + let tokens = [ Token::LeftParen, Token::NumberLiteral(1), Token::Operator(Operator::Add), @@ -178,7 +175,7 @@ mod tests { #[test] fn test_parse_equals() { - let tokens = vec![ + let tokens = [ Token::NumberLiteral(1), Token::Equals, Token::NumberLiteral(2), @@ -198,7 +195,7 @@ mod tests { #[test] fn test_parse_unary_and_binary_minus() { - let tokens = vec![ + let tokens = [ Token::Operator(Operator::Subtract), Token::NumberLiteral(1), Token::Operator(Operator::Subtract), @@ -220,7 +217,7 @@ mod tests { #[test] fn test_parse_unary_before_parenthesis() { - let tokens = vec![ + let tokens = [ Token::Operator(Operator::Subtract), Token::LeftParen, Token::NumberLiteral(9), @@ -244,7 +241,7 @@ mod tests { #[test] fn test_double_unary() { - let tokens = vec![ + let tokens = [ Token::Operator(Operator::Subtract), Token::Operator(Operator::Subtract), Token::NumberLiteral(7), diff --git a/board-shared/src/position.rs b/board-shared/src/position.rs index 8aa53ea..c591d0e 100644 --- a/board-shared/src/position.rs +++ b/board-shared/src/position.rs @@ -1,5 +1,6 @@ /// A position in a 2d grid. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Position2d { pub x: usize, pub y: usize, diff --git a/board-shared/src/score.rs b/board-shared/src/score.rs index 9a4313b..98ef569 100644 --- a/board-shared/src/score.rs +++ b/board-shared/src/score.rs @@ -14,14 +14,10 @@ fn calc_expression_score(tiles: &[Tile]) -> u32 { for token in tiles { match token { Tile::Digit(_) => digit_score += 1, - Tile::Operator(op) => { - match op { - Operator::Add | Operator::Subtract => multiplier = 2, - Operator::Multiply => multiplier = 3, - Operator::Divide => digit_score += 10, - }; - } - _ => unreachable!(), + Tile::Operator(Operator::Add | Operator::Subtract) => multiplier = 2, + Tile::Operator(Operator::Multiply) => multiplier = 3, + Tile::Operator(Operator::Divide) => digit_score += 10, + Tile::Equals => unreachable!(), } } digit_score * multiplier diff --git a/board-shared/src/tile.rs b/board-shared/src/tile.rs index 8f88bb1..7e5f612 100644 --- a/board-shared/src/tile.rs +++ b/board-shared/src/tile.rs @@ -1,8 +1,10 @@ use enum_map::Enum; use std::fmt; +use std::str::FromStr; /// A single digit that can be wrapped in parentheses. #[derive(Debug, PartialEq, Eq, Copy, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Digit { pub value: i8, pub has_left_parenthesis: bool, @@ -61,6 +63,7 @@ impl TryFrom<&str> for Digit { /// An operator that can be applied between two terms. #[derive(Debug, PartialEq, Eq, Copy, Clone, Enum)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Operator { Add, Subtract, @@ -104,6 +107,7 @@ impl TryFrom for Operator { /// A single piece of a mathematical expression. #[derive(Debug, PartialEq, Eq, Copy, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Tile { Digit(Digit), Operator(Operator), @@ -114,6 +118,14 @@ impl TryFrom<&str> for Tile { type Error = (); fn try_from(value: &str) -> Result { + Self::from_str(value) + } +} + +impl FromStr for Tile { + type Err = (); + + fn from_str(value: &str) -> Result { if let Ok(digit) = Digit::try_from(value) { return Ok(Tile::Digit(digit)); }