summaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
authorKjetil Orbekk <kj@orbekk.com>2022-12-22 11:36:48 -0500
committerKjetil Orbekk <kj@orbekk.com>2022-12-22 11:36:48 -0500
commitc2145b91775be375779884a2a97365396923aba1 (patch)
treee8d1c52b98e8d2fcf1fda741bef8bc85ddeaa6f9 /server
parent0ba28546b94a794d56c56bba35f035200fd0a434 (diff)
Add typed journals
Diffstat (limited to 'server')
-rw-r--r--server/src/main.rs30
-rw-r--r--server/src/play.rs174
2 files changed, 126 insertions, 78 deletions
diff --git a/server/src/main.rs b/server/src/main.rs
index 5b81fc0..35019c5 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -153,11 +153,20 @@ async fn get_table_view(
let jnl = DbJournal::new(extension.db.clone(), id);
let mut table = play::Table::new_or_replay(jnl).await?;
info!("Advancing play");
- while table.game().current_player() != Some(player_position) {
+ while table
+ .game()
+ .ok_or(BridgeError::InvalidRequest(
+ "game not in progress".to_string(),
+ ))?
+ .current_player()
+ != player_position
+ {
advance_play(&mut table).await?;
}
let response = Json(GameStatePlayerView::from_game_state(
- table.game(),
+ table.game().ok_or(BridgeError::InvalidRequest(
+ "game not in progress".to_string(),
+ ))?,
player_position,
));
info!("Response: {response:#?}");
@@ -173,14 +182,27 @@ async fn post_bid(
info!("Getting table state for {id:}");
let jnl = DbJournal::new(extension.db.clone(), id);
let mut table = play::Table::replay(jnl).await?;
- if !table.game().is_bidding() {
+ if !table
+ .game()
+ .ok_or(BridgeError::InvalidRequest(
+ "game not in progress".to_string(),
+ ))?
+ .is_bidding()
+ {
return Err(BridgeError::InvalidRequest(
"Posting a bid requires that the game is in the bidding phase"
.to_string(),
));
}
let player_position = Player::South;
- if table.game().current_player() != Some(player_position) {
+ if table
+ .game()
+ .ok_or(BridgeError::InvalidRequest(
+ "game not in progress".to_string(),
+ ))?
+ .current_player()
+ != player_position
+ {
return Err(BridgeError::InvalidRequest(format!(
"It is not {player_position:?} to play"
)));
diff --git a/server/src/play.rs b/server/src/play.rs
index 86a4e06..19fa0a0 100644
--- a/server/src/play.rs
+++ b/server/src/play.rs
@@ -1,20 +1,23 @@
+use std::mem;
+
use async_trait::async_trait;
use protocol::{
bot::BiddingBot,
bridge_engine::{Bid, BiddingStatePlayerView, Deal, GameState, Player},
+ card::Card,
+ play_result::MoveResult,
simple_bots::AlwaysPassBiddingBot,
};
use rand::random;
-use serde::{Deserialize, Serialize};
+use serde::{Deserialize, Serialize, de::DeserializeOwned};
use serde_json::json;
use sqlx::{query, PgPool};
-use tracing::info;
use uuid::Uuid;
use crate::error::BridgeError;
#[async_trait]
-pub trait Journal {
+pub trait Journal<Item> {
// Next sequence number to use.
fn next(&self) -> i64;
@@ -22,14 +25,11 @@ pub trait Journal {
async fn append(
&mut self,
seq: i64,
- payload: serde_json::Value,
+ payload: Item,
) -> Result<(), BridgeError>;
// Fetch all journal entries with sequence number greater or equal to `seq`.
- async fn replay(
- &mut self,
- seq: i64,
- ) -> Result<Vec<serde_json::Value>, BridgeError>;
+ async fn replay(&mut self, seq: i64) -> Result<Vec<Item>, BridgeError>;
}
pub struct DbJournal {
@@ -45,11 +45,11 @@ impl DbJournal {
}
#[async_trait]
-impl Journal for DbJournal {
+impl<Item: Serialize + DeserializeOwned + Send + 'static> Journal<Item> for DbJournal {
async fn append(
&mut self,
seq: i64,
- payload: serde_json::Value,
+ payload: Item,
) -> Result<(), BridgeError> {
let result = query!(
r#"
@@ -58,7 +58,7 @@ impl Journal for DbJournal {
"#,
self.id,
seq,
- payload,
+ json!(payload),
)
.execute(&self.db)
.await;
@@ -74,13 +74,10 @@ impl Journal for DbJournal {
Ok(())
}
- async fn replay(
- &mut self,
- seq: i64,
- ) -> Result<Vec<serde_json::Value>, BridgeError> {
+ async fn replay(&mut self, seq: i64) -> Result<Vec<Item>, BridgeError> {
let rows = query!(
r#"
- select seq, payload from object_journal
+ select seq, payload as "payload!: String" from object_journal
where id = $1 and seq >= $2
order by seq
"#,
@@ -91,7 +88,7 @@ impl Journal for DbJournal {
.await?;
let mut payloads = vec![];
for v in rows {
- payloads.push(v.payload);
+ payloads.push(serde_json::from_str(&v.payload)?);
self.seq = v.seq;
}
Ok(payloads)
@@ -107,6 +104,7 @@ pub enum TableUpdate {
NewDeal(Deal, Player),
ChangeSettings(TableSettings),
Bid(Bid),
+ SetState(TableState),
}
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize, Default)]
@@ -119,55 +117,97 @@ pub struct TableSettings {
pub struct Table<J>
where
- J: Journal,
+ J: Journal<TableUpdate>,
{
journal: J,
settings: TableSettings,
- game: GameState,
+ state: TableState,
+}
+
+#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
+pub enum TableState {
+ Unknown,
+ Game(GameState),
}
-impl<J: Journal> Table<J> {
- pub fn game(&self) -> &GameState {
- &self.game
+impl Into<TableState> for GameState {
+ fn into(self) -> TableState {
+ TableState::Game(self)
}
}
-impl<J: Journal> Table<J> {
+impl<J: Journal<TableUpdate>> Table<J> {
+ pub fn game(&self) -> Option<&GameState> {
+ match &self.state {
+ TableState::Game(g) => Some(g),
+ _ => None,
+ }
+ }
+}
+
+impl<J: Journal<TableUpdate>> Table<J> {
pub async fn new(mut journal: J) -> Result<Self, BridgeError> {
let game = Self::init(&mut journal).await?;
Ok(Table {
journal,
- game,
+ state: game.into(),
settings: Default::default(),
})
}
async fn init(journal: &mut J) -> Result<GameState, BridgeError> {
let game = GameState::new(random(), random());
- journal.append(0, json!(game)).await?;
+ journal
+ .append(0, TableUpdate::SetState(game.clone().into()))
+ .await?;
Ok(game)
}
pub async fn bid(&mut self, bid: Bid) -> Result<(), BridgeError> {
- let game = self.game.clone().bid(bid)?;
+ let mut state = TableState::Unknown;
+ mem::swap(&mut state, &mut self.state);
+ let game = match state {
+ TableState::Game(game) => game,
+ _ => {
+ return Err(BridgeError::InvalidRequest(
+ "no game in progress".to_string(),
+ ))
+ }
+ };
+ let mut state: TableState = match game.bid(bid)? {
+ MoveResult::Stay(game) => game.into(),
+ MoveResult::Go(_) => todo!(),
+ };
self.journal
- .append(self.journal.next(), json!(game))
+ .append(self.journal.next(), TableUpdate::SetState(state.clone()))
.await?;
- self.game = game;
+ mem::swap(&mut state, &mut self.state);
+ Ok(())
+ }
+
+ pub async fn play(&mut self, card: Card) -> Result<(), BridgeError> {
Ok(())
}
pub async fn replay(mut journal: J) -> Result<Self, BridgeError> {
- let games = journal.replay(0).await?;
- if games.is_empty() {
+ let log = journal.replay(0).await?;
+ if log.is_empty() {
return Err(BridgeError::NotFound(
"table journal missing".to_string(),
));
}
- let game = serde_json::from_value(games[games.len() - 1].clone())?;
+ let mut state = TableState::Unknown;
+ for update in log {
+ match update {
+ TableUpdate::NewDeal(_, _) => todo!(),
+ TableUpdate::ChangeSettings(_) => todo!(),
+ TableUpdate::Bid(_) => todo!(),
+ TableUpdate::SetState(s) => state = s,
+ }
+ }
Ok(Table {
journal,
- game,
+ state: state,
settings: Default::default(),
})
}
@@ -179,29 +219,23 @@ impl<J: Journal> Table<J> {
}
Ok(Self {
journal,
- game: game?,
+ state: game?.into(),
settings: Default::default(),
})
}
}
-pub async fn advance_play<J: Journal>(
+pub async fn advance_play<J: Journal<TableUpdate>>(
table: &mut Table<J>,
) -> Result<(), BridgeError> {
- let current_player = match table.game().current_player() {
- Some(player) => player,
- None => {
- info!("Could not make play. Game: {:#?}", table.game());
- return Err(BridgeError::InvalidRequest(format!(
- "No play to make for game"
- )));
- }
- };
- match table.game() {
+ let game = table
+ .game()
+ .ok_or(BridgeError::InvalidRequest(format!("no game in progress")))?;
+ match game {
GameState::Bidding(bidding) => {
let player_view = BiddingStatePlayerView::from_bidding_state(
bidding,
- current_player,
+ game.current_player(),
);
let bot = AlwaysPassBiddingBot {};
let bid = bot.bid(&player_view).await;
@@ -209,11 +243,6 @@ pub async fn advance_play<J: Journal>(
Ok(())
}
GameState::Play(_) => todo!(),
- GameState::PassedOut {
- dealer,
- deal,
- bidding,
- } => Err(BridgeError::InvalidRequest(format!("The game is over"))),
}
}
@@ -221,17 +250,22 @@ pub async fn advance_play<J: Journal>(
mod test {
use super::*;
- #[derive(Default)]
- pub struct TestJournal {
- log: Vec<Option<serde_json::Value>>,
+ pub struct TestJournal<Item> {
+ log: Vec<Option<Item>>,
+ }
+
+ impl<Item> TestJournal<Item> {
+ pub fn new() -> Self {
+ Self { log: vec![] }
+ }
}
#[async_trait]
- impl Journal for TestJournal {
+ impl<Item: Send + Clone> Journal<Item> for TestJournal<Item> {
async fn append(
&mut self,
seq: i64,
- payload: serde_json::Value,
+ payload: Item,
) -> Result<(), BridgeError> {
if seq != self.log.len() as i64 {
return Err(BridgeError::UpdateConflict(
@@ -243,10 +277,7 @@ mod test {
Ok(())
}
- async fn replay(
- &mut self,
- seq: i64,
- ) -> Result<Vec<serde_json::Value>, BridgeError> {
+ async fn replay(&mut self, seq: i64) -> Result<Vec<Item>, BridgeError> {
Ok(self.log[seq as usize..]
.into_iter()
.filter_map(|e| e.clone())
@@ -260,7 +291,7 @@ mod test {
#[tokio::test]
async fn test_journal() {
- let mut jnl: TestJournal = Default::default();
+ let mut jnl = TestJournal::new();
let seq = jnl.next();
assert_eq!(jnl.next(), 0);
assert_eq!(jnl.append(seq, json!(10)).await.unwrap(), ());
@@ -273,31 +304,26 @@ mod test {
#[tokio::test]
async fn test_new_table() {
- let t1: Table<TestJournal> =
- Table::new(Default::default()).await.unwrap();
- match t1.game {
- GameState::Bidding { .. } => (),
- _ => panic!("should be Bidding"),
- };
+ let t1 = Table::new(TestJournal::new()).await.unwrap();
+ assert!(t1.game().unwrap().is_bidding());
}
#[tokio::test]
async fn test_replay_table() {
- let t1: Table<TestJournal> =
- Table::new(Default::default()).await.unwrap();
- let game = t1.game;
+ let t1 = Table::new(TestJournal::new()).await.unwrap();
+ let game = t1.game().unwrap().clone();
let journal = t1.journal;
let t2 = Table::replay(journal).await.unwrap();
- assert_eq!(game, t2.game);
+ assert_eq!(&game, t2.game().unwrap());
}
#[tokio::test]
async fn test_advance_play() {
- let mut t1: Table<TestJournal> =
- Table::new(Default::default()).await.unwrap();
- let player = t1.game().current_player();
+ let mut t1 =
+ Table::new(TestJournal::new()).await.unwrap();
+ let player = t1.game().unwrap().current_player();
advance_play(&mut t1).await.unwrap();
- assert_ne!(player, t1.game().current_player());
+ assert_ne!(player, t1.game().unwrap().current_player());
}
}