From b114fe7940e77090861ac9ba60f4d0b8caec8978 Mon Sep 17 00:00:00 2001 From: Kjetil Orbekk Date: Tue, 15 Nov 2022 19:49:54 -0500 Subject: Add journaling GameState --- protocol/src/bridge_engine.rs | 60 +++++++++++++++++++----- protocol/src/card.rs | 9 ++-- server/src/error.rs | 3 ++ server/src/play.rs | 105 ++++++++++++++++++++++++++++++++++-------- webapp/src/components/game.rs | 46 ++---------------- 5 files changed, 147 insertions(+), 76 deletions(-) diff --git a/protocol/src/bridge_engine.rs b/protocol/src/bridge_engine.rs index a914efe..e9c09f6 100644 --- a/protocol/src/bridge_engine.rs +++ b/protocol/src/bridge_engine.rs @@ -1,4 +1,5 @@ -use crate::card::{Card, Deal, Suit}; +use crate::card::{Card, Deal, Suit, RankOrder}; +use serde::{Deserialize, Serialize}; use anyhow::{anyhow, bail}; use log::{error, info}; use regex::Regex; @@ -8,7 +9,9 @@ use std::str::FromStr; use strum::{EnumCount, IntoEnumIterator}; use strum_macros::{EnumCount as EnumCountMacro, EnumIter, FromRepr}; -#[derive(PartialEq, Eq, Clone, Copy, Debug, FromRepr, EnumCountMacro)] +pub const SUIT_DISPLAY_ORDER: [Suit; 4] = [Suit::Diamond, Suit::Club, Suit::Heart, Suit::Spade]; + +#[derive(PartialEq, Eq, Clone, Copy, Debug, FromRepr, EnumCountMacro, Serialize, Deserialize)] #[repr(u8)] pub enum Player { West = 0, @@ -45,7 +48,7 @@ impl Player { } } -#[derive(PartialEq, Eq, Debug, Clone)] +#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] pub struct Trick { pub leader: Player, pub cards_played: Vec, @@ -58,7 +61,7 @@ impl Trick { } } -#[derive(PartialEq, Eq, Debug, Clone)] +#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] pub struct TurnInPlay { trick: Trick, } @@ -108,7 +111,7 @@ impl TurnInPlay { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct DealInPlay { deal: Deal, tricks_played: Vec, @@ -175,7 +178,7 @@ impl DealInPlay { } } -#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, EnumIter)] +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, EnumIter, Serialize, Deserialize)] pub enum ContractLevel { One = 1, Two, @@ -215,7 +218,7 @@ impl FromStr for ContractLevel { } } -#[derive(PartialEq, Eq, Clone, Copy)] +#[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] pub enum Bid { Pass, Double, @@ -266,7 +269,7 @@ impl FromStr for Bid { } } -#[derive(PartialEq, Eq, Clone, Copy)] +#[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] pub struct Raise { pub level: ContractLevel, pub suit: Option, @@ -347,7 +350,7 @@ impl FromStr for Raise { } } -#[derive(Debug, PartialEq, Eq, Copy, Clone)] +#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)] pub enum ContractModifier { None, Doubled, @@ -364,7 +367,7 @@ impl fmt::Display for ContractModifier { } } -#[derive(Debug, PartialEq, Eq, Copy, Clone)] +#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)] pub struct Contract { declarer: Player, highest_bid: Raise, @@ -383,7 +386,7 @@ impl fmt::Display for Contract { } } -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] pub struct Bidding { pub dealer: Player, pub bids: Vec, @@ -469,6 +472,41 @@ impl BiddingResult { } } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum GameState { + Bidding { + dealer: Player, + deal: Deal, + }, + PassedOut { + dealer: Player, + deal: Deal, + bidding: Bidding, + }, + Play { + playing_deal: DealInPlay, + contract: Contract, + bidding: Bidding, + }, +} + +impl GameState { + pub fn deal(&self) -> &Deal { + match self { + Self::Bidding { deal, .. } => deal, + Self::PassedOut { deal, .. } => deal, + Self::Play { playing_deal, .. } => &playing_deal.deal(), + } + } +} + +pub fn deal() -> Deal { + let mut rng = rand::thread_rng(); + let mut deal = crate::card::deal(&mut rng); + deal.sort(&SUIT_DISPLAY_ORDER, RankOrder::Descending); + deal +} + #[cfg(test)] mod tests { use super::*; diff --git a/protocol/src/card.rs b/protocol/src/card.rs index 621bae1..e27f29a 100644 --- a/protocol/src/card.rs +++ b/protocol/src/card.rs @@ -1,3 +1,4 @@ +use serde::{Deserialize, Serialize}; use anyhow::anyhow; use rand::prelude::SliceRandom; use rand::Rng; @@ -7,7 +8,7 @@ use strum::IntoEnumIterator; use strum_macros::EnumCount; use strum_macros::EnumIter; -#[derive(PartialOrd, Ord, PartialEq, Eq, Clone, Copy, EnumIter, EnumCount)] +#[derive(PartialOrd, Ord, PartialEq, Eq, Clone, Copy, EnumIter, EnumCount, Serialize, Deserialize)] pub enum Suit { Club, Diamond, @@ -15,7 +16,7 @@ pub enum Suit { Spade, } -#[derive(PartialOrd, Ord, PartialEq, Eq, Clone, Copy, EnumIter)] +#[derive(PartialOrd, Ord, PartialEq, Eq, Clone, Copy, EnumIter, Serialize, Deserialize)] pub enum Rank { Two = 2, Three, @@ -119,7 +120,7 @@ impl std::str::FromStr for Rank { } } -#[derive(PartialEq, Eq, Clone, Copy)] +#[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] pub struct Card(pub Suit, pub Rank); impl fmt::Display for Card { @@ -240,7 +241,7 @@ mod tests { } } -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] pub struct Deal { pub north: Vec, pub west: Vec, diff --git a/server/src/error.rs b/server/src/error.rs index cbd00c3..03735a7 100644 --- a/server/src/error.rs +++ b/server/src/error.rs @@ -41,6 +41,9 @@ pub enum BridgeError { #[error("Uuid parse failed")] UuidError(#[from] uuid::Error), + #[error("Serialization error")] + SerdeError(#[from] serde_json::Error), + #[error("Internal server error: {0}")] Internal(String), diff --git a/server/src/play.rs b/server/src/play.rs index 242bdb8..2632b24 100644 --- a/server/src/play.rs +++ b/server/src/play.rs @@ -1,11 +1,16 @@ use async_trait::async_trait; -use sqlx::{PgPool, query}; +use protocol::bridge_engine::{self, GameState, Player}; +use serde_json::json; +use sqlx::{query, PgPool}; use uuid::Uuid; use crate::error::BridgeError; #[async_trait] pub trait Journal { + // Next sequence number to use. + fn next(&self) -> i64; + // Append payload to the journal at sequence number `seq`. async fn append(&mut self, seq: i64, payload: serde_json::Value) -> Result<(), BridgeError>; @@ -16,41 +21,79 @@ pub trait Journal { pub struct DbJournal { db: PgPool, id: Uuid, + seq: i64 } impl DbJournal { pub fn new(db: PgPool, id: Uuid) -> Self { - Self { db, id } + Self { db, id, seq: -1 } } } #[async_trait] impl Journal for DbJournal { - async fn append(&mut self,seq:i64,payload:serde_json::Value) -> Result<(),BridgeError> { - query!( - r#" + async fn append(&mut self, seq: i64, payload: serde_json::Value) -> Result<(), BridgeError> { + query!( + r#" insert into object_journal (id, seq, payload) values ($1, $2, $3) "#, - self.id, seq, payload, - ).execute(&self.db).await?; - Ok(()) + self.id, + seq, + payload, + ) + .execute(&self.db) + .await?; + Ok(()) } - async fn replay(&mut self,seq:i64) -> Result,BridgeError> { - let results = query!( - r#" + async fn replay(&mut self, seq: i64) -> Result, BridgeError> { + let results = query!( + r#" select payload from object_journal where id = $1 and seq >= $2 order by seq "#, - self.id, seq - ).fetch_all(&self.db).await?; - Ok(results.into_iter().map(|v| v.payload).collect()) + self.id, + seq + ) + .fetch_all(&self.db) + .await?; + Ok(results.into_iter().map(|v| v.payload).collect()) + } + + fn next(&self) -> i64 { + self.seq + 1 } } -pub struct Table {} +pub struct Table +where + J: Journal, +{ + journal: J, + game: GameState, +} + +impl Table { + pub async fn new(mut journal: J) -> Result { + let game = GameState::Bidding { + dealer: Player::East, + deal: bridge_engine::deal(), + }; + journal.append(0, json!(game)).await?; + Ok(Table { journal, game }) + } + + pub async fn replay(mut journal: J) -> Result { + let games = journal.replay(0).await?; + if games.is_empty() { + return Err(BridgeError::Internal(format!("empty journal"))); + } + let game = serde_json::from_value(games[games.len() - 1].clone())?; + Ok(Table { journal, game } ) + } +} #[cfg(test)] mod test { @@ -81,15 +124,41 @@ mod test { .filter_map(|e| e.clone()) .collect()) } + + fn next(&self) -> i64 { + self.log.len() as i64 + } } #[tokio::test] async fn test_journal() { - use serde_json::json; let mut jnl: TestJournal = Default::default(); - assert_eq!(jnl.append(0, json!(10)).await.unwrap(), ()); + let seq = jnl.next(); + assert_eq!(jnl.next(), 0); + assert_eq!(jnl.append(seq, json!(10)).await.unwrap(), ()); + assert_eq!(jnl.next(), 1); assert!(jnl.append(0, json!(0)).await.is_err()); - assert_eq!(jnl.append(1, json!(20)).await.unwrap(), ()); + let seq = jnl.next(); + assert_eq!(jnl.append(seq, json!(20)).await.unwrap(), ()); assert_eq!(jnl.replay(1).await.unwrap(), vec!(json!(20))); } + + #[tokio::test] + async fn test_new_table() { + let t1: Table = Table::new(Default::default()).await.unwrap(); + match t1.game { + GameState::Bidding { dealer, deal } => (), + _ => panic!("should be Bidding"), + }; + } + + #[tokio::test] + async fn test_replay_table() { + let t1: Table = Table::new(Default::default()).await.unwrap(); + let game = t1.game; + let journal = t1.journal; + + let t2 = Table::replay(journal).await.unwrap(); + assert_eq!(game, t2.game); + } } diff --git a/webapp/src/components/game.rs b/webapp/src/components/game.rs index 45ad035..02774e7 100644 --- a/webapp/src/components/game.rs +++ b/webapp/src/components/game.rs @@ -1,48 +1,8 @@ -use protocol::bridge_engine::{self, Contract, DealInPlay, DealInPlayResult, Player}; -use protocol::card; -use protocol::card::Deal; -use protocol::card::Suit; +use protocol::bridge_engine::{DealInPlay, DealInPlayResult, Player, GameState, deal}; use crate::components::{Bidding, Hand, ShowBid, TrickInPlay, TricksPlayed}; use log::{error, info}; use yew::prelude::*; -pub const SUIT_DISPLAY_ORDER: [Suit; 4] = [Suit::Diamond, Suit::Club, Suit::Heart, Suit::Spade]; - -#[derive(Debug, Clone)] -enum GameState { - Bidding { - dealer: Player, - deal: Deal, - }, - PassedOut { - _dealer: Player, - deal: Deal, - _bidding: bridge_engine::Bidding, - }, - Play { - playing_deal: DealInPlay, - contract: Contract, - bidding: bridge_engine::Bidding, - }, -} - -impl GameState { - fn deal(&self) -> &Deal { - match self { - Self::Bidding { deal, .. } => deal, - Self::PassedOut { deal, .. } => deal, - Self::Play { playing_deal, .. } => &playing_deal.deal(), - } - } -} - -pub fn deal() -> card::Deal { - let mut rng = rand::thread_rng(); - let mut deal = card::deal(&mut rng); - deal.sort(&SUIT_DISPLAY_ORDER, card::RankOrder::Descending); - deal -} - fn init_state() -> GameState { let dealer = Player::East; let deal = deal(); @@ -97,9 +57,9 @@ pub fn game() -> Html { bidding, }, None => GameState::PassedOut { - _dealer: dealer, + dealer: dealer, deal: deal.clone(), - _bidding: bidding, + bidding: bidding, }, }); }) -- cgit v1.2.3