Include a serde feature in the shared crate
continuous-integration/drone/push Build is passing Details

main
Clément FRÉVILLE 1 year ago
parent 3d93bf0fda
commit f1962503b8

@ -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

@ -3,6 +3,7 @@ strip = true
panic = "abort"
[workspace]
resolver = "2"
members = [
"board-shared",
"board-frontend",

@ -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"

@ -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 {

@ -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::<ServerMessage>(&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::<ServerMessage>(&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 = {

@ -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! {
<div class="tile" onclick={Callback::from(move |_| {
on_select.emit(idx)
})}>{ tile }</div>
})}>{ tile.to_string() }</div>
}
}

@ -1,2 +1 @@
pub mod protocol;
pub mod types;

@ -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<TileRef>),
SyncHand(Vec<Tile>),
/// 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,

@ -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<Option<TileRef>>,
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::<Vec<_>>(),
width: value.width(),
height: value.height(),
}
}
}
impl From<Tile> 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<TileRef> 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<Digit> 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<DigitRef> 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<Operator> 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<OperatorRef> 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<Position2d> for Position2dRef {
fn from(value: Position2d) -> Self {
Self {
x: value.x,
y: value.y,
}
}
}
impl From<Position2dRef> for Position2d {
fn from(value: Position2dRef) -> Self {
Self {
x: value.x,
y: value.y,
}
}
}

@ -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"

@ -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

@ -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<Vec<LeaderboardEntry>, 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<LeaderboardEntry> = con
.zrange_withscores(LEADERBOARD, 0, (count - 1).min(LEADERBOARD_SIZE))

@ -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| <Tile as Into<TileRef>>::into(*t))
.collect::<Vec<_>>(),
),
ServerMessage::SyncHand(self.players[player_id].hand.tiles.clone()),
);
}
}

@ -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"]

@ -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<Option<Tile>>,
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<Position2d>, 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() {

@ -54,11 +54,7 @@ impl Hand {
}
pub fn remove(&mut self, idx: usize) -> Option<Tile> {
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<Item = &Tile> {

@ -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,

@ -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;

@ -39,20 +39,17 @@ pub fn parse(tokens: &[Token]) -> Result<Expressions, ()> {
fn parse_primary<'a>(
tokens: &mut Peekable<impl Iterator<Item = &'a Token>>,
) -> Result<Expression, ()> {
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),

@ -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,

@ -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

@ -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<char> 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, Self::Error> {
Self::from_str(value)
}
}
impl FromStr for Tile {
type Err = ();
fn from_str(value: &str) -> Result<Self, Self::Err> {
if let Ok(digit) = Digit::try_from(value) {
return Ok(Tile::Digit(digit));
}

Loading…
Cancel
Save