summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKjetil Orbekk <kj@orbekk.com>2022-11-15 19:49:54 -0500
committerKjetil Orbekk <kj@orbekk.com>2022-11-15 19:49:54 -0500
commitb114fe7940e77090861ac9ba60f4d0b8caec8978 (patch)
treed5c7a1f2250a899719225642bf876a29316a30b0
parent87ede1e2b367a997440626ad9f583600d7cc42fc (diff)
Add journaling GameState
-rw-r--r--protocol/src/bridge_engine.rs60
-rw-r--r--protocol/src/card.rs9
-rw-r--r--server/src/error.rs3
-rw-r--r--server/src/play.rs105
-rw-r--r--webapp/src/components/game.rs46
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<Card>,
@@ -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<Trick>,
@@ -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<Suit>,
@@ -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<Bid>,
@@ -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<Card>,
pub west: Vec<Card>,
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<Vec<serde_json::Value>,BridgeError> {
- let results = query!(
- r#"
+ async fn replay(&mut self, seq: i64) -> Result<Vec<serde_json::Value>, 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<J>
+where
+ J: Journal,
+{
+ journal: J,
+ game: GameState,
+}
+
+impl<J: Journal> Table<J> {
+ pub async fn new(mut journal: J) -> Result<Self, BridgeError> {
+ 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<Self, BridgeError> {
+ 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<TestJournal> = 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<TestJournal> = 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,
},
});
})