From: Geoffrey Allott Date: Tue, 2 Mar 2021 21:14:41 +0000 (+0000) Subject: add timestamps to all game actions X-Git-Url: https://git.pointlesshacks.com/?a=commitdiff_plain;h=1eb2775ff066694adbf1c89cca07d22d97b06739;p=pokerwave.git add timestamps to all game actions --- diff --git a/src/client.rs b/src/client.rs index a32af50..bb6cca3 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,4 +1,5 @@ use std::collections::HashSet; +use std::time::SystemTime; use futures::stream::{Stream, StreamExt, empty, iter, once}; @@ -173,7 +174,8 @@ impl ConnectionState { } } (&mut ClientState::LoggedIn{username, state: LoggedInState::InGame{ref mut game}}, ClientMessage::TakeAction{action}) => { - let action = UserAction{username, action}; + let timestamp = SystemTime::now(); // TODO use time from db? + let action = UserAction{timestamp, username, action}; let id = game.id(); loop { let len = game.actions_len(); diff --git a/src/dealer.rs b/src/dealer.rs index ba661f2..fb56f65 100644 --- a/src/dealer.rs +++ b/src/dealer.rs @@ -1,4 +1,5 @@ use std::collections::HashSet; +use std::time::SystemTime; use async_std::stream::StreamExt; use futures::channel::mpsc::Receiver; @@ -56,7 +57,8 @@ impl Dealer { debug!("Dealer: Game state: {:#?}", self.dealer.game); } 'take_action: loop { - match self.dealer.game.next_dealer_action() { + let timestamp = SystemTime::now(); + match self.dealer.game.next_dealer_action(timestamp) { Some(action) => match self.take_action(action).await { Ok(ActionStatus::Committed) => { debug!("Dealer: Game state: {:#?}", self.dealer.game); diff --git a/src/game/action.rs b/src/game/action.rs index 890347f..68d6be8 100644 --- a/src/game/action.rs +++ b/src/game/action.rs @@ -1,10 +1,14 @@ +use std::cmp::PartialEq; use std::fmt::{Debug, Display, Formatter}; +use std::time::SystemTime; use crate::card::{Card, Suit}; use crate::username::Username; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct UserAction { + #[serde(with = "crate::util::millis")] + pub timestamp: SystemTime, pub username: Username, pub action: Action, } @@ -16,7 +20,8 @@ pub struct ValidatedUserAction(pub UserAction); impl ValidatedUserAction { pub fn view_for(&self, username: Username) -> UserAction { UserAction { - username: self.0.username.clone(), + timestamp: self.0.timestamp, + username: self.0.username, action: if username == self.0.username { self.0.action.clone() } else { self.0.action.anonymise() }, } } diff --git a/src/game/chatroom.rs b/src/game/chatroom.rs index 113ef77..fbb8f98 100644 --- a/src/game/chatroom.rs +++ b/src/game/chatroom.rs @@ -1,4 +1,5 @@ use std::collections::HashSet; +use std::time::SystemTime; use crate::username::{DEALER, Username}; use crate::game::{Action, ActionError}; @@ -13,6 +14,13 @@ enum ChatroomAction { Leave, } +#[derive(Debug, Clone)] +struct ChatroomUserAction { + timestamp: SystemTime, + username: Username, + action: ChatroomAction, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ChatroomSettings { title: String, @@ -22,7 +30,7 @@ pub struct ChatroomSettings { pub struct Chatroom { id: i64, settings: ChatroomSettings, - messages: Vec<(Username, ChatroomAction)>, + messages: Vec, users: HashSet, } @@ -62,30 +70,29 @@ impl Game for Chatroom { } } - fn take_action(&mut self, action: ValidatedUserAction) -> Result<(), ActionError> { - let ValidatedUserAction(action) = action; - match action.action { + fn take_action(&mut self, ValidatedUserAction(UserAction{timestamp, username, action}): ValidatedUserAction) -> Result<(), ActionError> { + match action { Action::Join{..} => { - self.messages.push((action.username, ChatroomAction::Join)); - self.users.insert(action.username); + self.messages.push(ChatroomUserAction{timestamp, username, action: ChatroomAction::Join}); + self.users.insert(username); Ok(()) } Action::Message{message} => { - self.messages.push((action.username, ChatroomAction::Message(message))); + self.messages.push(ChatroomUserAction{timestamp, username, action: ChatroomAction::Message(message)}); Ok(()) } Action::Leave => { - self.messages.push((action.username, ChatroomAction::Leave)); - self.users.remove(&action.username); + self.messages.push(ChatroomUserAction{timestamp, username, action: ChatroomAction::Leave}); + self.users.remove(&username); Ok(()) } _ => Err(ActionError::InvalidActionForGameType), } } - fn next_dealer_action(&self) -> Option { + fn next_dealer_action(&self, timestamp: SystemTime) -> Option { match self.messages.len() { - n if n % 10 == 0 => Some(ValidatedUserAction(UserAction{username: DEALER, action: Action::Message{message: format!("{} messages posted so far", n)}})), + n if n % 10 == 0 => Some(ValidatedUserAction(UserAction{timestamp, username: DEALER, action: Action::Message{message: format!("{} messages posted so far", n)}})), _ => None, } } diff --git a/src/game/mod.rs b/src/game/mod.rs index 6c73c3b..690dea1 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -5,6 +5,7 @@ mod whist; use std::collections::HashSet; use std::fmt::Debug; +use std::time::SystemTime; use crate::rng::Seed; use crate::username::Username; @@ -21,7 +22,7 @@ pub trait Game : Debug + CloneBoxGame + Send + Sync { fn actions_len(&self) -> usize; fn validate_action(&self, action: UserAction) -> Result; fn take_action(&mut self, action: ValidatedUserAction) -> Result<(), ActionError>; - fn next_dealer_action(&self) -> Option; + fn next_dealer_action(&self, timestamp: SystemTime) -> Option; } pub trait CloneBoxGame { diff --git a/src/game/poker/holdem.rs b/src/game/poker/holdem.rs index 33a81ea..505bd3f 100644 --- a/src/game/poker/holdem.rs +++ b/src/game/poker/holdem.rs @@ -1,5 +1,6 @@ use std::collections::{HashMap, HashSet}; use std::convert::TryInto; +use std::time::SystemTime; use itertools::Itertools; @@ -141,7 +142,7 @@ impl Game for TexasHoldEm { self.actions_len } - fn validate_action(&self, UserAction{username, action}: UserAction) -> Result { + fn validate_action(&self, UserAction{timestamp, username, action}: UserAction) -> Result { match (self.state, action) { (_, Action::PlayCard{..}) | (_, Action::ChooseTrumps{..}) => { Err(ActionError::InvalidActionForGameType) @@ -158,14 +159,14 @@ impl Game for TexasHoldEm { } else if chips > self.settings.starting_stack { Err(ActionError::StartingStackTooLarge) } else { - Ok(ValidatedUserAction(UserAction{username, action: Action::Join{seat, chips}})) + Ok(ValidatedUserAction(UserAction{timestamp, username, action: Action::Join{seat, chips}})) } } (State::Completed, Action::Join{..}) => Err(ActionError::GameHasEnded), (_, Action::Join{..}) => Err(ActionError::GameHasStarted), (_, _) if !self.seats.contains_player(username) => Err(ActionError::NotAuthorised), (State::NotStarted, Action::Leave) => { - Ok(ValidatedUserAction(UserAction{username, action: Action::Leave})) + Ok(ValidatedUserAction(UserAction{timestamp, username, action: Action::Leave})) } (_, _) if !self.seats.contains_player(username) => Err(ActionError::NotAuthorised), (State::Completed, _) => Err(ActionError::GameHasEnded), @@ -176,13 +177,13 @@ impl Game for TexasHoldEm { (_, Action::Fold) | (_, Action::Bet{..}) if !self.players.contains(&username) => Err(ActionError::NotInHand), (_, Action::Fold) | (_, Action::Bet{..}) if self.active != Some(username) => Err(ActionError::OutOfTurn), (_, Action::Fold) if self.chips_to_call(username) == 0 => Err(ActionError::CannotFold), - (_, Action::Fold) => Ok(ValidatedUserAction(UserAction{username, action: Action::Fold})), + (_, Action::Fold) => Ok(ValidatedUserAction(UserAction{timestamp, username, action: Action::Fold})), (_, Action::Bet{chips}) => { let stack = self.stack(username); if chips > stack { Err(ActionError::NotEnoughChips) } else if chips == stack { - Ok(ValidatedUserAction(UserAction{username, action: Action::Bet{chips}})) + Ok(ValidatedUserAction(UserAction{timestamp, username, action: Action::Bet{chips}})) } else { let to_call = self.chips_to_call(username); let min_raise = self.min_raise(); @@ -191,7 +192,7 @@ impl Game for TexasHoldEm { } else if chips > to_call && chips < to_call + min_raise { Err(ActionError::BetSizeTooSmall) } else { - Ok(ValidatedUserAction(UserAction{username, action: Action::Bet{chips}})) + Ok(ValidatedUserAction(UserAction{timestamp, username, action: Action::Bet{chips}})) } } } @@ -199,7 +200,7 @@ impl Game for TexasHoldEm { } } - fn take_action(&mut self, ValidatedUserAction(UserAction{username, action}): ValidatedUserAction) -> Result<(), ActionError> { + fn take_action(&mut self, ValidatedUserAction(UserAction{username, action, ..}): ValidatedUserAction) -> Result<(), ActionError> { self.actions_len += 1; self.rng.advance(); match (self.state, action) { @@ -347,13 +348,13 @@ impl Game for TexasHoldEm { } } - fn next_dealer_action(&self) -> Option { + fn next_dealer_action(&self, timestamp: SystemTime) -> Option { let mut rng = self.rng.clone(); match self.state { State::NotStarted => { if self.seats.players_len() == self.settings.max_players as usize { // TODO if let Some(username) = rng.choose_from(self.seats.player_set()) { - return Some(ValidatedUserAction(UserAction{username, action: Action::NextToDeal})); + return Some(ValidatedUserAction(UserAction{timestamp, username, action: Action::NextToDeal})); } } None @@ -361,9 +362,9 @@ impl Game for TexasHoldEm { State::Dealing => { if let Some(username) = self.receiver { let card = rng.choose_from(&self.deck).cloned(); - Some(ValidatedUserAction(UserAction{username, action: Action::ReceiveCard{card}})) + Some(ValidatedUserAction(UserAction{timestamp, username, action: Action::ReceiveCard{card}})) } else if let Some(username) = self.dealer { - Some(ValidatedUserAction(UserAction{username, action: Action::EndDeal})) + Some(ValidatedUserAction(UserAction{timestamp, username, action: Action::EndDeal})) } else { None } @@ -372,21 +373,21 @@ impl Game for TexasHoldEm { self.dealer .map(|username| { let chips = self.stack(username).min(self.small_blind); - ValidatedUserAction(UserAction{username, action: Action::PostBlind{chips}}) + ValidatedUserAction(UserAction{timestamp, username, action: Action::PostBlind{chips}}) }) } State::PostingSmallBlind => { self.dealer.and_then(|dealer| self.seats.player_after(dealer)) .map(|username| { let chips = self.stack(username).min(self.small_blind); - ValidatedUserAction(UserAction{username, action: Action::PostBlind{chips}}) + ValidatedUserAction(UserAction{timestamp, username, action: Action::PostBlind{chips}}) }) } State::PostingBigBlind if self.seats.players_len() == 2 => { self.dealer.and_then(|dealer| self.seats.player_after(dealer)) .map(|username| { let chips = self.stack(username).min(self.small_blind * 2); - ValidatedUserAction(UserAction{username, action: Action::PostBlind{chips}}) + ValidatedUserAction(UserAction{timestamp, username, action: Action::PostBlind{chips}}) }) } State::PostingBigBlind => { @@ -394,7 +395,7 @@ impl Game for TexasHoldEm { .and_then(|small_blind| self.seats.player_after(small_blind)) .map(|username| { let chips = self.stack(username).min(self.small_blind * 2); - ValidatedUserAction(UserAction{username, action: Action::PostBlind{chips}}) + ValidatedUserAction(UserAction{timestamp, username, action: Action::PostBlind{chips}}) }) } State::PreFlopBetting | State::PostFlopBetting | State::TurnBetting | State::RiverBetting => { @@ -402,6 +403,7 @@ impl Game for TexasHoldEm { if self.pot > 0 { self.players.iter().next() .map(|&username| ValidatedUserAction(UserAction { + timestamp, username, action: Action::WinHand { chips: self.pot, @@ -410,11 +412,11 @@ impl Game for TexasHoldEm { })) } else if self.seats.players_len() == 1 { self.seats.player_set().iter().next() - .map(|&username| ValidatedUserAction(UserAction{username, action: Action::WinGame})) + .map(|&username| ValidatedUserAction(UserAction{timestamp, username, action: Action::WinGame})) } else if let Some((&username, _)) = self.stacks.iter().find(|&(_, &stack)| stack == 0) { - Some(ValidatedUserAction(UserAction{username, action: Action::KnockedOut})) + Some(ValidatedUserAction(UserAction{timestamp, username, action: Action::KnockedOut})) } else if let Some(username) = self.dealer.and_then(|dealer| self.seats.player_after(dealer)) { - Some(ValidatedUserAction(UserAction{username, action: Action::NextToDeal})) + Some(ValidatedUserAction(UserAction{timestamp, username, action: Action::NextToDeal})) } else { error!("Logic error: no dealer could be chosen: {:#?}", self); None @@ -426,16 +428,16 @@ impl Game for TexasHoldEm { State::DealingFlop | State::DealingTurn | State::DealingRiver => { self.dealer.and_then(|username| rng.choose_from(&self.deck).map(|&card| - ValidatedUserAction(UserAction{username, action: Action::CommunityCard{card}}))) + ValidatedUserAction(UserAction{timestamp, username, action: Action::CommunityCard{card}}))) } State::Showdown if self.pot == 0 => { if let Some((&username, _)) = self.stacks.iter().find(|&(_, &stack)| stack == 0) { - Some(ValidatedUserAction(UserAction{username, action: Action::KnockedOut})) + Some(ValidatedUserAction(UserAction{timestamp, username, action: Action::KnockedOut})) } else if self.seats.players_len() == 1 { self.seats.player_set().iter().next() - .map(|&username| ValidatedUserAction(UserAction{username, action: Action::WinGame})) + .map(|&username| ValidatedUserAction(UserAction{timestamp, username, action: Action::WinGame})) } else if let Some(username) = self.dealer.and_then(|dealer| self.seats.player_after(dealer)) { - Some(ValidatedUserAction(UserAction{username, action: Action::NextToDeal})) + Some(ValidatedUserAction(UserAction{timestamp, username, action: Action::NextToDeal})) } else { error!("Logic error: no dealer could be chosen: {:#?}", self); None @@ -451,6 +453,7 @@ impl Game for TexasHoldEm { info!("Showdown: winning hands: {:?}", winning_hands); winning_hands.first() .map(|&(username, hand)| ValidatedUserAction(UserAction { + timestamp, username, action: Action::WinHand { chips: (self.pot / winning_hands.len() as u64).min(self.max_winnings(username)), @@ -467,6 +470,8 @@ impl Game for TexasHoldEm { mod tests { use super::*; + use std::time::SystemTime; + fn test_game(actions: Vec, settings: TexasHoldEmSettings, seed: Seed) { let mut game = TexasHoldEm::new(0, settings, seed); for action in actions { @@ -477,7 +482,7 @@ mod tests { game.take_action(validated).unwrap(); } _ => { - let dealer_action = game.next_dealer_action().unwrap(); + let dealer_action = game.next_dealer_action(SystemTime::UNIX_EPOCH).unwrap(); assert_eq!(ValidatedUserAction(action), dealer_action); game.take_action(dealer_action).unwrap(); } @@ -488,31 +493,31 @@ mod tests { #[test] fn simple_heads_up_with_1_hand() { let actions = r#"[ - {"username":"kat","action":{"action":"Join","seat":0,"chips":1000}}, - {"username":"geoff","action":{"action":"Join","seat":1,"chips":1000}}, - {"username":"geoff","action":{"action":"NextToDeal"}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Eight","suit":"Diamonds"}}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Seven","suit":"Hearts"}}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Nine","suit":"Diamonds"}}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Seven","suit":"Clubs"}}}, - {"username":"geoff","action":{"action":"EndDeal"}}, - {"username":"geoff","action":{"action":"PostBlind","chips":100}}, - {"username":"kat","action":{"action":"PostBlind","chips":200}}, - {"username":"geoff","action":{"action":"Bet","chips":300}}, - {"username":"kat","action":{"action":"Bet","chips":200}}, - {"username":"geoff","action":{"action":"CommunityCard","card":{"rank":"Three","suit":"Hearts"}}}, - {"username":"geoff","action":{"action":"CommunityCard","card":{"rank":"Two","suit":"Diamonds"}}}, - {"username":"geoff","action":{"action":"CommunityCard","card":{"rank":"Two","suit":"Hearts"}}}, - {"username":"kat","action":{"action":"Bet","chips":0}}, - {"username":"geoff","action":{"action":"Bet","chips":200}}, - {"username":"kat","action":{"action":"Bet","chips":200}}, - {"username":"geoff","action":{"action":"CommunityCard","card":{"rank":"Nine","suit":"Spades"}}}, - {"username":"kat","action":{"action":"Bet","chips":400}}, - {"username":"geoff","action":{"action":"Bet","chips":400}}, - {"username":"geoff","action":{"action":"CommunityCard","card":{"rank":"Jack","suit":"Spades"}}}, - {"username":"kat","action":{"action":"WinHand","chips":2000,"hand":"Two Pair, 9s & 2s, J Kicker"}}, - {"username":"geoff","action":{"action":"KnockedOut"}}, - {"username":"kat","action":{"action":"WinGame"}} + {"timestamp":0,"username":"kat","action":{"action":"Join","seat":0,"chips":1000}}, + {"timestamp":0,"username":"geoff","action":{"action":"Join","seat":1,"chips":1000}}, + {"timestamp":0,"username":"geoff","action":{"action":"NextToDeal"}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Eight","suit":"Diamonds"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Seven","suit":"Hearts"}}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Nine","suit":"Diamonds"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Seven","suit":"Clubs"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"EndDeal"}}, + {"timestamp":0,"username":"geoff","action":{"action":"PostBlind","chips":100}}, + {"timestamp":0,"username":"kat","action":{"action":"PostBlind","chips":200}}, + {"timestamp":0,"username":"geoff","action":{"action":"Bet","chips":300}}, + {"timestamp":0,"username":"kat","action":{"action":"Bet","chips":200}}, + {"timestamp":0,"username":"geoff","action":{"action":"CommunityCard","card":{"rank":"Three","suit":"Hearts"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"CommunityCard","card":{"rank":"Two","suit":"Diamonds"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"CommunityCard","card":{"rank":"Two","suit":"Hearts"}}}, + {"timestamp":0,"username":"kat","action":{"action":"Bet","chips":0}}, + {"timestamp":0,"username":"geoff","action":{"action":"Bet","chips":200}}, + {"timestamp":0,"username":"kat","action":{"action":"Bet","chips":200}}, + {"timestamp":0,"username":"geoff","action":{"action":"CommunityCard","card":{"rank":"Nine","suit":"Spades"}}}, + {"timestamp":0,"username":"kat","action":{"action":"Bet","chips":400}}, + {"timestamp":0,"username":"geoff","action":{"action":"Bet","chips":400}}, + {"timestamp":0,"username":"geoff","action":{"action":"CommunityCard","card":{"rank":"Jack","suit":"Spades"}}}, + {"timestamp":0,"username":"kat","action":{"action":"WinHand","chips":2000,"hand":"Two Pair, 9s & 2s, J Kicker"}}, + {"timestamp":0,"username":"geoff","action":{"action":"KnockedOut"}}, + {"timestamp":0,"username":"kat","action":{"action":"WinGame"}} ]"#; let actions = serde_json::from_str(actions).unwrap(); @@ -528,37 +533,37 @@ mod tests { #[test] fn simple_heads_up_with_2_hands() { let actions = r#"[ - {"username":"p1","action":{"action":"Join","seat":0,"chips":1000}}, - {"username":"p2","action":{"action":"Join","seat":1,"chips":1000}}, - {"username":"p1","action":{"action":"NextToDeal"}}, - {"username":"p2","action":{"action":"ReceiveCard","card":{"rank":"Nine","suit":"Clubs"}}}, - {"username":"p1","action":{"action":"ReceiveCard","card":{"rank":"Nine","suit":"Spades"}}}, - {"username":"p2","action":{"action":"ReceiveCard","card":{"rank":"Ace","suit":"Diamonds"}}}, - {"username":"p1","action":{"action":"ReceiveCard","card":{"rank":"Six","suit":"Diamonds"}}}, - {"username":"p1","action":{"action":"EndDeal"}}, - {"username":"p1","action":{"action":"PostBlind","chips":25}}, - {"username":"p2","action":{"action":"PostBlind","chips":50}}, - {"username":"p1","action":{"action":"Fold"}}, - {"username":"p2","action":{"action":"WinHand","chips":75,"hand":null}}, - {"username":"p2","action":{"action":"NextToDeal"}}, - {"username":"p1","action":{"action":"ReceiveCard","card":{"rank":"Four","suit":"Clubs"}}}, - {"username":"p2","action":{"action":"ReceiveCard","card":{"rank":"King","suit":"Clubs"}}}, - {"username":"p1","action":{"action":"ReceiveCard","card":{"rank":"Nine","suit":"Hearts"}}}, - {"username":"p2","action":{"action":"ReceiveCard","card":{"rank":"Jack","suit":"Diamonds"}}}, - {"username":"p2","action":{"action":"EndDeal"}}, - {"username":"p2","action":{"action":"PostBlind","chips":25}}, - {"username":"p1","action":{"action":"PostBlind","chips":50}}, - {"username":"p2","action":{"action":"Bet","chips":25}}, - {"username":"p1","action":{"action":"Bet","chips":925}}, - {"username":"p2","action":{"action":"Bet","chips":925}}, - {"username":"p2","action":{"action":"CommunityCard","card":{"rank":"Eight","suit":"Hearts"}}}, - {"username":"p2","action":{"action":"CommunityCard","card":{"rank":"Four","suit":"Diamonds"}}}, - {"username":"p2","action":{"action":"CommunityCard","card":{"rank":"Eight","suit":"Spades"}}}, - {"username":"p2","action":{"action":"CommunityCard","card":{"rank":"Ace","suit":"Hearts"}}}, - {"username":"p2","action":{"action":"CommunityCard","card":{"rank":"Jack","suit":"Hearts"}}}, - {"username":"p2","action":{"action":"WinHand","chips":1950,"hand":"Two Pair, Js & 8s, A Kicker"}}, - {"username":"p1","action":{"action":"KnockedOut"}}, - {"username":"p2","action":{"action":"WinGame"}} + {"timestamp":0,"username":"p1","action":{"action":"Join","seat":0,"chips":1000}}, + {"timestamp":0,"username":"p2","action":{"action":"Join","seat":1,"chips":1000}}, + {"timestamp":0,"username":"p1","action":{"action":"NextToDeal"}}, + {"timestamp":0,"username":"p2","action":{"action":"ReceiveCard","card":{"rank":"Nine","suit":"Clubs"}}}, + {"timestamp":0,"username":"p1","action":{"action":"ReceiveCard","card":{"rank":"Nine","suit":"Spades"}}}, + {"timestamp":0,"username":"p2","action":{"action":"ReceiveCard","card":{"rank":"Ace","suit":"Diamonds"}}}, + {"timestamp":0,"username":"p1","action":{"action":"ReceiveCard","card":{"rank":"Six","suit":"Diamonds"}}}, + {"timestamp":0,"username":"p1","action":{"action":"EndDeal"}}, + {"timestamp":0,"username":"p1","action":{"action":"PostBlind","chips":25}}, + {"timestamp":0,"username":"p2","action":{"action":"PostBlind","chips":50}}, + {"timestamp":0,"username":"p1","action":{"action":"Fold"}}, + {"timestamp":0,"username":"p2","action":{"action":"WinHand","chips":75,"hand":null}}, + {"timestamp":0,"username":"p2","action":{"action":"NextToDeal"}}, + {"timestamp":0,"username":"p1","action":{"action":"ReceiveCard","card":{"rank":"Four","suit":"Clubs"}}}, + {"timestamp":0,"username":"p2","action":{"action":"ReceiveCard","card":{"rank":"King","suit":"Clubs"}}}, + {"timestamp":0,"username":"p1","action":{"action":"ReceiveCard","card":{"rank":"Nine","suit":"Hearts"}}}, + {"timestamp":0,"username":"p2","action":{"action":"ReceiveCard","card":{"rank":"Jack","suit":"Diamonds"}}}, + {"timestamp":0,"username":"p2","action":{"action":"EndDeal"}}, + {"timestamp":0,"username":"p2","action":{"action":"PostBlind","chips":25}}, + {"timestamp":0,"username":"p1","action":{"action":"PostBlind","chips":50}}, + {"timestamp":0,"username":"p2","action":{"action":"Bet","chips":25}}, + {"timestamp":0,"username":"p1","action":{"action":"Bet","chips":925}}, + {"timestamp":0,"username":"p2","action":{"action":"Bet","chips":925}}, + {"timestamp":0,"username":"p2","action":{"action":"CommunityCard","card":{"rank":"Eight","suit":"Hearts"}}}, + {"timestamp":0,"username":"p2","action":{"action":"CommunityCard","card":{"rank":"Four","suit":"Diamonds"}}}, + {"timestamp":0,"username":"p2","action":{"action":"CommunityCard","card":{"rank":"Eight","suit":"Spades"}}}, + {"timestamp":0,"username":"p2","action":{"action":"CommunityCard","card":{"rank":"Ace","suit":"Hearts"}}}, + {"timestamp":0,"username":"p2","action":{"action":"CommunityCard","card":{"rank":"Jack","suit":"Hearts"}}}, + {"timestamp":0,"username":"p2","action":{"action":"WinHand","chips":1950,"hand":"Two Pair, Js & 8s, A Kicker"}}, + {"timestamp":0,"username":"p1","action":{"action":"KnockedOut"}}, + {"timestamp":0,"username":"p2","action":{"action":"WinGame"}} ]"#; let actions = serde_json::from_str(actions).unwrap(); @@ -574,74 +579,74 @@ mod tests { #[test] fn simple_heads_up_with_4_hands() { let actions = r#"[ - {"username":"geoff","action":{"action":"Join","seat":0,"chips":1000}}, - {"username":"kat","action":{"action":"Join","seat":1,"chips":1000}}, - {"username":"geoff","action":{"action":"NextToDeal"}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Four","suit":"Hearts"}}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Two","suit":"Diamonds"}}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Four","suit":"Clubs"}}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Six","suit":"Spades"}}}, - {"username":"geoff","action":{"action":"EndDeal"}}, - {"username":"geoff","action":{"action":"PostBlind","chips":100}}, - {"username":"kat","action":{"action":"PostBlind","chips":200}}, - {"username":"geoff","action":{"action":"Bet","chips":100}}, - {"username":"kat","action":{"action":"Bet","chips":0}}, - {"username":"geoff","action":{"action":"CommunityCard","card":{"rank":"Queen","suit":"Clubs"}}}, - {"username":"geoff","action":{"action":"CommunityCard","card":{"rank":"Ace","suit":"Hearts"}}}, - {"username":"geoff","action":{"action":"CommunityCard","card":{"rank":"Seven","suit":"Hearts"}}}, - {"username":"kat","action":{"action":"Bet","chips":0}}, - {"username":"geoff","action":{"action":"Bet","chips":0}}, - {"username":"geoff","action":{"action":"CommunityCard","card":{"rank":"Ten","suit":"Hearts"}}}, - {"username":"kat","action":{"action":"Bet","chips":200}}, - {"username":"geoff","action":{"action":"Fold"}}, - {"username":"kat","action":{"action":"WinHand","chips":600,"hand":null}}, - {"username":"kat","action":{"action":"NextToDeal"}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Four","suit":"Spades"}}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Jack","suit":"Diamonds"}}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Six","suit":"Spades"}}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Four","suit":"Clubs"}}}, - {"username":"kat","action":{"action":"EndDeal"}}, - {"username":"kat","action":{"action":"PostBlind","chips":100}}, - {"username":"geoff","action":{"action":"PostBlind","chips":200}}, - {"username":"kat","action":{"action":"Bet","chips":100}}, - {"username":"geoff","action":{"action":"Bet","chips":0}}, - {"username":"kat","action":{"action":"CommunityCard","card":{"rank":"Ace","suit":"Clubs"}}}, - {"username":"kat","action":{"action":"CommunityCard","card":{"rank":"Seven","suit":"Diamonds"}}}, - {"username":"kat","action":{"action":"CommunityCard","card":{"rank":"Five","suit":"Spades"}}}, - {"username":"geoff","action":{"action":"Bet","chips":200}}, - {"username":"kat","action":{"action":"Fold"}}, - {"username":"geoff","action":{"action":"WinHand","chips":600,"hand":null}}, - {"username":"geoff","action":{"action":"NextToDeal"}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Five","suit":"Hearts"}}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Four","suit":"Diamonds"}}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Two","suit":"Diamonds"}}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Ten","suit":"Spades"}}}, - {"username":"geoff","action":{"action":"EndDeal"}}, - {"username":"geoff","action":{"action":"PostBlind","chips":100}}, - {"username":"kat","action":{"action":"PostBlind","chips":200}}, - {"username":"geoff","action":{"action":"Fold"}}, - {"username":"kat","action":{"action":"WinHand","chips":300,"hand":null}}, - {"username":"kat","action":{"action":"NextToDeal"}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Eight","suit":"Spades"}}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Ace","suit":"Spades"}}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Ten","suit":"Spades"}}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"King","suit":"Spades"}}}, - {"username":"kat","action":{"action":"EndDeal"}}, - {"username":"kat","action":{"action":"PostBlind","chips":100}}, - {"username":"geoff","action":{"action":"PostBlind","chips":200}}, - {"username":"kat","action":{"action":"Bet","chips":300}}, - {"username":"geoff","action":{"action":"Bet","chips":200}}, - {"username":"kat","action":{"action":"CommunityCard","card":{"rank":"Ten","suit":"Clubs"}}}, - {"username":"kat","action":{"action":"CommunityCard","card":{"rank":"Jack","suit":"Hearts"}}}, - {"username":"kat","action":{"action":"CommunityCard","card":{"rank":"Queen","suit":"Spades"}}}, - {"username":"geoff","action":{"action":"Bet","chips":0}}, - {"username":"kat","action":{"action":"Bet","chips":500}}, - {"username":"geoff","action":{"action":"Bet","chips":500}}, - {"username":"kat","action":{"action":"CommunityCard","card":{"rank":"Six","suit":"Hearts"}}}, - {"username":"kat","action":{"action":"CommunityCard","card":{"rank":"Eight","suit":"Clubs"}}}, - {"username":"kat","action":{"action":"WinHand","chips":1800,"hand":"Straight, A High"}}, - {"username":"geoff","action":{"action":"KnockedOut"}}, - {"username":"kat","action":{"action":"WinGame"}} + {"timestamp":0,"username":"geoff","action":{"action":"Join","seat":0,"chips":1000}}, + {"timestamp":0,"username":"kat","action":{"action":"Join","seat":1,"chips":1000}}, + {"timestamp":0,"username":"geoff","action":{"action":"NextToDeal"}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Four","suit":"Hearts"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Two","suit":"Diamonds"}}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Four","suit":"Clubs"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Six","suit":"Spades"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"EndDeal"}}, + {"timestamp":0,"username":"geoff","action":{"action":"PostBlind","chips":100}}, + {"timestamp":0,"username":"kat","action":{"action":"PostBlind","chips":200}}, + {"timestamp":0,"username":"geoff","action":{"action":"Bet","chips":100}}, + {"timestamp":0,"username":"kat","action":{"action":"Bet","chips":0}}, + {"timestamp":0,"username":"geoff","action":{"action":"CommunityCard","card":{"rank":"Queen","suit":"Clubs"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"CommunityCard","card":{"rank":"Ace","suit":"Hearts"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"CommunityCard","card":{"rank":"Seven","suit":"Hearts"}}}, + {"timestamp":0,"username":"kat","action":{"action":"Bet","chips":0}}, + {"timestamp":0,"username":"geoff","action":{"action":"Bet","chips":0}}, + {"timestamp":0,"username":"geoff","action":{"action":"CommunityCard","card":{"rank":"Ten","suit":"Hearts"}}}, + {"timestamp":0,"username":"kat","action":{"action":"Bet","chips":200}}, + {"timestamp":0,"username":"geoff","action":{"action":"Fold"}}, + {"timestamp":0,"username":"kat","action":{"action":"WinHand","chips":600,"hand":null}}, + {"timestamp":0,"username":"kat","action":{"action":"NextToDeal"}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Four","suit":"Spades"}}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Jack","suit":"Diamonds"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Six","suit":"Spades"}}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Four","suit":"Clubs"}}}, + {"timestamp":0,"username":"kat","action":{"action":"EndDeal"}}, + {"timestamp":0,"username":"kat","action":{"action":"PostBlind","chips":100}}, + {"timestamp":0,"username":"geoff","action":{"action":"PostBlind","chips":200}}, + {"timestamp":0,"username":"kat","action":{"action":"Bet","chips":100}}, + {"timestamp":0,"username":"geoff","action":{"action":"Bet","chips":0}}, + {"timestamp":0,"username":"kat","action":{"action":"CommunityCard","card":{"rank":"Ace","suit":"Clubs"}}}, + {"timestamp":0,"username":"kat","action":{"action":"CommunityCard","card":{"rank":"Seven","suit":"Diamonds"}}}, + {"timestamp":0,"username":"kat","action":{"action":"CommunityCard","card":{"rank":"Five","suit":"Spades"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"Bet","chips":200}}, + {"timestamp":0,"username":"kat","action":{"action":"Fold"}}, + {"timestamp":0,"username":"geoff","action":{"action":"WinHand","chips":600,"hand":null}}, + {"timestamp":0,"username":"geoff","action":{"action":"NextToDeal"}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Five","suit":"Hearts"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Four","suit":"Diamonds"}}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Two","suit":"Diamonds"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Ten","suit":"Spades"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"EndDeal"}}, + {"timestamp":0,"username":"geoff","action":{"action":"PostBlind","chips":100}}, + {"timestamp":0,"username":"kat","action":{"action":"PostBlind","chips":200}}, + {"timestamp":0,"username":"geoff","action":{"action":"Fold"}}, + {"timestamp":0,"username":"kat","action":{"action":"WinHand","chips":300,"hand":null}}, + {"timestamp":0,"username":"kat","action":{"action":"NextToDeal"}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Eight","suit":"Spades"}}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Ace","suit":"Spades"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Ten","suit":"Spades"}}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"King","suit":"Spades"}}}, + {"timestamp":0,"username":"kat","action":{"action":"EndDeal"}}, + {"timestamp":0,"username":"kat","action":{"action":"PostBlind","chips":100}}, + {"timestamp":0,"username":"geoff","action":{"action":"PostBlind","chips":200}}, + {"timestamp":0,"username":"kat","action":{"action":"Bet","chips":300}}, + {"timestamp":0,"username":"geoff","action":{"action":"Bet","chips":200}}, + {"timestamp":0,"username":"kat","action":{"action":"CommunityCard","card":{"rank":"Ten","suit":"Clubs"}}}, + {"timestamp":0,"username":"kat","action":{"action":"CommunityCard","card":{"rank":"Jack","suit":"Hearts"}}}, + {"timestamp":0,"username":"kat","action":{"action":"CommunityCard","card":{"rank":"Queen","suit":"Spades"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"Bet","chips":0}}, + {"timestamp":0,"username":"kat","action":{"action":"Bet","chips":500}}, + {"timestamp":0,"username":"geoff","action":{"action":"Bet","chips":500}}, + {"timestamp":0,"username":"kat","action":{"action":"CommunityCard","card":{"rank":"Six","suit":"Hearts"}}}, + {"timestamp":0,"username":"kat","action":{"action":"CommunityCard","card":{"rank":"Eight","suit":"Clubs"}}}, + {"timestamp":0,"username":"kat","action":{"action":"WinHand","chips":1800,"hand":"Straight, A High"}}, + {"timestamp":0,"username":"geoff","action":{"action":"KnockedOut"}}, + {"timestamp":0,"username":"kat","action":{"action":"WinGame"}} ]"#; let actions = serde_json::from_str(actions).unwrap(); diff --git a/src/game/whist.rs b/src/game/whist.rs index ffa5c1c..999fd98 100644 --- a/src/game/whist.rs +++ b/src/game/whist.rs @@ -1,4 +1,5 @@ use std::collections::{HashMap, HashSet}; +use std::time::SystemTime; use crate::card::{Card, Suit, FIFTY_TWO_CARD_DECK}; use crate::rng::{Seed, WaveRng}; @@ -114,7 +115,7 @@ impl Game for KnockOutWhist { self.actions_len } - fn validate_action(&self, UserAction{username, action}: UserAction) -> Result { + fn validate_action(&self, UserAction{timestamp, username, action}: UserAction) -> Result { match (self.state, action) { (_, Action::AddOn{..}) | (_, Action::RevealCard{..}) | (_, Action::Fold) | (_, Action::Bet{..}) => { Err(ActionError::InvalidActionForGameType) @@ -127,21 +128,21 @@ impl Game for KnockOutWhist { } else if !self.seats.seat_is_available(seat) { Err(ActionError::SeatNotAvailable) } else { - Ok(ValidatedUserAction(UserAction{username, action: Action::Join{seat, chips: 0}})) + Ok(ValidatedUserAction(UserAction{timestamp, username, action: Action::Join{seat, chips: 0}})) } } (State::Completed, Action::Join{..}) => Err(ActionError::GameHasEnded), (_, Action::Join{..}) => Err(ActionError::GameHasStarted), (_, _) if !self.seats.contains_player(username) => Err(ActionError::NotAuthorised), (State::NotStarted, Action::Leave) => { - Ok(ValidatedUserAction(UserAction{username, action: Action::Leave})) + Ok(ValidatedUserAction(UserAction{timestamp, username, action: Action::Leave})) } (State::Completed, Action::Leave) => Err(ActionError::GameHasEnded), (_, Action::Leave) => Err(ActionError::GameHasStarted), (State::Dealing, _) => Err(ActionError::Dealing), (State::ChoosingTrumps, Action::ChooseTrumps{suit}) => { if Some(username) == self.call { - Ok(ValidatedUserAction(UserAction{username, action: Action::ChooseTrumps{suit}})) + Ok(ValidatedUserAction(UserAction{timestamp, username, action: Action::ChooseTrumps{suit}})) } else { Err(ActionError::OutOfTurn) } @@ -155,7 +156,7 @@ impl Game for KnockOutWhist { } else if matches!(self.led, Some(led) if card.suit != led && self.hand_contains_suit(username, led)) { Err(ActionError::CardNotPlayable) } else { - Ok(ValidatedUserAction(UserAction{username, action: Action::PlayCard{card}})) + Ok(ValidatedUserAction(UserAction{timestamp, username, action: Action::PlayCard{card}})) } } (State::CutForCall, _) => Err(ActionError::Dealing), @@ -165,7 +166,7 @@ impl Game for KnockOutWhist { } } - fn take_action(&mut self, ValidatedUserAction(UserAction{username, action}): ValidatedUserAction) -> Result<(), ActionError> { + fn take_action(&mut self, ValidatedUserAction(UserAction{username, action, ..}): ValidatedUserAction) -> Result<(), ActionError> { self.actions_len += 1; self.rng.advance(); match (self.state, action) { @@ -302,13 +303,13 @@ impl Game for KnockOutWhist { } } - fn next_dealer_action(&self) -> Option { + fn next_dealer_action(&self, timestamp: SystemTime) -> Option { let mut rng = self.rng.clone(); match self.state { State::NotStarted => { if self.seats.players_len() == self.settings.max_players as usize { // TODO if let Some(username) = rng.choose_from(self.seats.player_set()) { - return Some(ValidatedUserAction(UserAction{username, action: Action::NextToDeal})); + return Some(ValidatedUserAction(UserAction{timestamp, username, action: Action::NextToDeal})); } } None @@ -316,17 +317,17 @@ impl Game for KnockOutWhist { State::Dealing => { if let Some(username) = self.receiver { let card = rng.choose_from(&self.deck).cloned(); - Some(ValidatedUserAction(UserAction{username, action: Action::ReceiveCard{card}})) + Some(ValidatedUserAction(UserAction{timestamp, username, action: Action::ReceiveCard{card}})) } else if let Some(username) = self.dealer { match (self.call, self.trump_card) { (None, None) => { if let Some(&card) = rng.choose_from(&self.deck) { - Some(ValidatedUserAction(UserAction{username, action: Action::CommunityCard{card}})) + Some(ValidatedUserAction(UserAction{timestamp, username, action: Action::CommunityCard{card}})) } else { None } } - (Some(_), _) | (None, Some(_)) => Some(ValidatedUserAction(UserAction{username, action: Action::EndDeal})), + (Some(_), _) | (None, Some(_)) => Some(ValidatedUserAction(UserAction{timestamp, username, action: Action::EndDeal})), } } else { None @@ -339,20 +340,20 @@ impl Game for KnockOutWhist { if !self.winners.is_empty() { for username in self.seats.player_set() { if matches!(self.tricks_won.get(&username), Some(0) | None) { - return Some(ValidatedUserAction(UserAction{username, action: Action::KnockedOut})); + return Some(ValidatedUserAction(UserAction{timestamp, username, action: Action::KnockedOut})); } } if self.seats.players_len() == 1 { if let Some(&username) = self.winners.iter().next() { - return Some(ValidatedUserAction(UserAction{username, action: Action::WinGame})); + return Some(ValidatedUserAction(UserAction{timestamp, username, action: Action::WinGame})); } } if let Some(username) = self.call { - return Some(ValidatedUserAction(UserAction{username, action: Action::WinCall})); + return Some(ValidatedUserAction(UserAction{timestamp, username, action: Action::WinCall})); } None } else if let Some(username) = self.trick_winner() { - Some(ValidatedUserAction(UserAction{username, action: Action::WinTrick})) + Some(ValidatedUserAction(UserAction{timestamp, username, action: Action::WinTrick})) } else { None } @@ -360,19 +361,19 @@ impl Game for KnockOutWhist { State::CutForCall => { if let Some(username) = self.receiver { if let Some(card) = rng.choose_from(&self.deck).cloned() { - Some(ValidatedUserAction(UserAction{username, action: Action::RevealCard{card}})) + Some(ValidatedUserAction(UserAction{timestamp, username, action: Action::RevealCard{card}})) } else { None } } else if let Some(username) = self.call { - Some(ValidatedUserAction(UserAction{username, action: Action::WinCall})) + Some(ValidatedUserAction(UserAction{timestamp, username, action: Action::WinCall})) } else { None } } State::RoundCompleted => { if let Some(username) = self.dealer.and_then(|dealer| self.seats.player_after(dealer)) { - Some(ValidatedUserAction(UserAction{username, action: Action::NextToDeal})) + Some(ValidatedUserAction(UserAction{timestamp, username, action: Action::NextToDeal})) } else { None } @@ -388,6 +389,8 @@ impl Game for KnockOutWhist { mod tests { use super::*; + use std::time::SystemTime; + fn test_game(actions: Vec, settings: KnockOutWhistSettings, seed: Seed) { let mut game = KnockOutWhist::new(0, settings, seed); for action in actions { @@ -398,7 +401,7 @@ mod tests { game.take_action(validated).unwrap(); } _ => { - let dealer_action = game.next_dealer_action().unwrap(); + let dealer_action = game.next_dealer_action(SystemTime::UNIX_EPOCH).unwrap(); assert_eq!(ValidatedUserAction(action), dealer_action); game.take_action(dealer_action).unwrap(); } @@ -409,156 +412,156 @@ mod tests { #[test] fn complete_2_player_knock_out_whist() { let actions = r#"[ - {"username":"geoff","action":{"action":"Join","seat":0,"chips":0}}, - {"username":"kat","action":{"action":"Join","seat":1,"chips":0}}, - {"username":"geoff","action":{"action":"NextToDeal"}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Eight","suit":"Diamonds"}}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Ace","suit":"Spades"}}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"King","suit":"Clubs"}}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Seven","suit":"Diamonds"}}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Two","suit":"Hearts"}}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Eight","suit":"Spades"}}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Three","suit":"Diamonds"}}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Two","suit":"Spades"}}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Seven","suit":"Spades"}}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Queen","suit":"Spades"}}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Six","suit":"Spades"}}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Jack","suit":"Hearts"}}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Ace","suit":"Diamonds"}}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Seven","suit":"Clubs"}}}, - {"username":"geoff","action":{"action":"CommunityCard","card":{"rank":"Ace","suit":"Clubs"}}}, - {"username":"geoff","action":{"action":"EndDeal"}}, - {"username":"kat","action":{"action":"PlayCard","card":{"rank":"Ace","suit":"Diamonds"}}}, - {"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Seven","suit":"Diamonds"}}}, - {"username":"kat","action":{"action":"WinTrick"}}, - {"username":"kat","action":{"action":"PlayCard","card":{"rank":"King","suit":"Clubs"}}}, - {"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Seven","suit":"Clubs"}}}, - {"username":"kat","action":{"action":"WinTrick"}}, - {"username":"kat","action":{"action":"PlayCard","card":{"rank":"Eight","suit":"Diamonds"}}}, - {"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Two","suit":"Spades"}}}, - {"username":"kat","action":{"action":"WinTrick"}}, - {"username":"kat","action":{"action":"PlayCard","card":{"rank":"Seven","suit":"Spades"}}}, - {"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Eight","suit":"Spades"}}}, - {"username":"geoff","action":{"action":"WinTrick"}}, - {"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Ace","suit":"Spades"}}}, - {"username":"kat","action":{"action":"PlayCard","card":{"rank":"Six","suit":"Spades"}}}, - {"username":"geoff","action":{"action":"WinTrick"}}, - {"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Queen","suit":"Spades"}}}, - {"username":"kat","action":{"action":"PlayCard","card":{"rank":"Two","suit":"Hearts"}}}, - {"username":"geoff","action":{"action":"WinTrick"}}, - {"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Jack","suit":"Hearts"}}}, - {"username":"kat","action":{"action":"PlayCard","card":{"rank":"Three","suit":"Diamonds"}}}, - {"username":"geoff","action":{"action":"WinTrick"}}, - {"username":"geoff","action":{"action":"WinCall"}}, - {"username":"kat","action":{"action":"NextToDeal"}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Ace","suit":"Hearts"}}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Ace","suit":"Clubs"}}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"King","suit":"Diamonds"}}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Nine","suit":"Clubs"}}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Four","suit":"Spades"}}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Ten","suit":"Diamonds"}}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Nine","suit":"Diamonds"}}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Queen","suit":"Clubs"}}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Ten","suit":"Spades"}}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Ace","suit":"Diamonds"}}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Jack","suit":"Diamonds"}}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"King","suit":"Hearts"}}}, - {"username":"kat","action":{"action":"EndDeal"}}, - {"username":"geoff","action":{"action":"ChooseTrumps","suit":"Diamonds"}}, - {"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Ace","suit":"Hearts"}}}, - {"username":"kat","action":{"action":"PlayCard","card":{"rank":"King","suit":"Hearts"}}}, - {"username":"geoff","action":{"action":"WinTrick"}}, - {"username":"geoff","action":{"action":"PlayCard","card":{"rank":"King","suit":"Diamonds"}}}, - {"username":"kat","action":{"action":"PlayCard","card":{"rank":"Ace","suit":"Diamonds"}}}, - {"username":"kat","action":{"action":"WinTrick"}}, - {"username":"kat","action":{"action":"PlayCard","card":{"rank":"Ace","suit":"Clubs"}}}, - {"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Nine","suit":"Diamonds"}}}, - {"username":"geoff","action":{"action":"WinTrick"}}, - {"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Jack","suit":"Diamonds"}}}, - {"username":"kat","action":{"action":"PlayCard","card":{"rank":"Ten","suit":"Diamonds"}}}, - {"username":"geoff","action":{"action":"WinTrick"}}, - {"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Ten","suit":"Spades"}}}, - {"username":"kat","action":{"action":"PlayCard","card":{"rank":"Nine","suit":"Clubs"}}}, - {"username":"geoff","action":{"action":"WinTrick"}}, - {"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Four","suit":"Spades"}}}, - {"username":"kat","action":{"action":"PlayCard","card":{"rank":"Queen","suit":"Clubs"}}}, - {"username":"geoff","action":{"action":"WinTrick"}}, - {"username":"geoff","action":{"action":"WinCall"}}, - {"username":"geoff","action":{"action":"NextToDeal"}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Ace","suit":"Spades"}}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Six","suit":"Spades"}}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Two","suit":"Spades"}}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Two","suit":"Clubs"}}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Four","suit":"Hearts"}}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Five","suit":"Diamonds"}}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Six","suit":"Diamonds"}}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Five","suit":"Spades"}}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"King","suit":"Hearts"}}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Ten","suit":"Clubs"}}}, - {"username":"geoff","action":{"action":"EndDeal"}}, - {"username":"geoff","action":{"action":"ChooseTrumps","suit":"Clubs"}}, - {"username":"kat","action":{"action":"PlayCard","card":{"rank":"Ace","suit":"Spades"}}}, - {"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Five","suit":"Spades"}}}, - {"username":"kat","action":{"action":"WinTrick"}}, - {"username":"kat","action":{"action":"PlayCard","card":{"rank":"King","suit":"Hearts"}}}, - {"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Two","suit":"Clubs"}}}, - {"username":"geoff","action":{"action":"WinTrick"}}, - {"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Ten","suit":"Clubs"}}}, - {"username":"kat","action":{"action":"PlayCard","card":{"rank":"Two","suit":"Spades"}}}, - {"username":"geoff","action":{"action":"WinTrick"}}, - {"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Six","suit":"Spades"}}}, - {"username":"kat","action":{"action":"PlayCard","card":{"rank":"Four","suit":"Hearts"}}}, - {"username":"geoff","action":{"action":"WinTrick"}}, - {"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Five","suit":"Diamonds"}}}, - {"username":"kat","action":{"action":"PlayCard","card":{"rank":"Six","suit":"Diamonds"}}}, - {"username":"kat","action":{"action":"WinTrick"}}, - {"username":"geoff","action":{"action":"WinCall"}}, - {"username":"kat","action":{"action":"NextToDeal"}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"King","suit":"Clubs"}}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Queen","suit":"Clubs"}}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Queen","suit":"Spades"}}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Nine","suit":"Spades"}}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Queen","suit":"Hearts"}}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Eight","suit":"Clubs"}}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Five","suit":"Clubs"}}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Two","suit":"Clubs"}}}, - {"username":"kat","action":{"action":"EndDeal"}}, - {"username":"geoff","action":{"action":"ChooseTrumps","suit":"Clubs"}}, - {"username":"geoff","action":{"action":"PlayCard","card":{"rank":"King","suit":"Clubs"}}}, - {"username":"kat","action":{"action":"PlayCard","card":{"rank":"Two","suit":"Clubs"}}}, - {"username":"geoff","action":{"action":"WinTrick"}}, - {"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Queen","suit":"Spades"}}}, - {"username":"kat","action":{"action":"PlayCard","card":{"rank":"Nine","suit":"Spades"}}}, - {"username":"geoff","action":{"action":"WinTrick"}}, - {"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Queen","suit":"Hearts"}}}, - {"username":"kat","action":{"action":"PlayCard","card":{"rank":"Eight","suit":"Clubs"}}}, - {"username":"kat","action":{"action":"WinTrick"}}, - {"username":"kat","action":{"action":"PlayCard","card":{"rank":"Queen","suit":"Clubs"}}}, - {"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Five","suit":"Clubs"}}}, - {"username":"kat","action":{"action":"WinTrick"}}, - {"username":"geoff","action":{"action":"RevealCard","card":{"rank":"Three","suit":"Diamonds"}}}, - {"username":"kat","action":{"action":"RevealCard","card":{"rank":"Ten","suit":"Clubs"}}}, - {"username":"kat","action":{"action":"WinCall"}}, - {"username":"geoff","action":{"action":"NextToDeal"}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"King","suit":"Spades"}}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Six","suit":"Spades"}}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Four","suit":"Spades"}}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Six","suit":"Hearts"}}}, - {"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Queen","suit":"Hearts"}}}, - {"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Ten","suit":"Clubs"}}}, - {"username":"geoff","action":{"action":"EndDeal"}}, - {"username":"kat","action":{"action":"ChooseTrumps","suit":"Spades"}}, - {"username":"kat","action":{"action":"PlayCard","card":{"rank":"King","suit":"Spades"}}}, - {"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Six","suit":"Spades"}}}, - {"username":"kat","action":{"action":"WinTrick"}}, - {"username":"kat","action":{"action":"PlayCard","card":{"rank":"Queen","suit":"Hearts"}}}, - {"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Six","suit":"Hearts"}}}, - {"username":"kat","action":{"action":"WinTrick"}}, - {"username":"kat","action":{"action":"PlayCard","card":{"rank":"Four","suit":"Spades"}}}, - {"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Ten","suit":"Clubs"}}}, - {"username":"kat","action":{"action":"WinTrick"}}, - {"username":"geoff","action":{"action":"KnockedOut"}}, - {"username":"kat","action":{"action":"WinGame"}} + {"timestamp":0,"username":"geoff","action":{"action":"Join","seat":0,"chips":0}}, + {"timestamp":0,"username":"kat","action":{"action":"Join","seat":1,"chips":0}}, + {"timestamp":0,"username":"geoff","action":{"action":"NextToDeal"}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Eight","suit":"Diamonds"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Ace","suit":"Spades"}}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"King","suit":"Clubs"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Seven","suit":"Diamonds"}}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Two","suit":"Hearts"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Eight","suit":"Spades"}}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Three","suit":"Diamonds"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Two","suit":"Spades"}}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Seven","suit":"Spades"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Queen","suit":"Spades"}}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Six","suit":"Spades"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Jack","suit":"Hearts"}}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Ace","suit":"Diamonds"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Seven","suit":"Clubs"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"CommunityCard","card":{"rank":"Ace","suit":"Clubs"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"EndDeal"}}, + {"timestamp":0,"username":"kat","action":{"action":"PlayCard","card":{"rank":"Ace","suit":"Diamonds"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Seven","suit":"Diamonds"}}}, + {"timestamp":0,"username":"kat","action":{"action":"WinTrick"}}, + {"timestamp":0,"username":"kat","action":{"action":"PlayCard","card":{"rank":"King","suit":"Clubs"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Seven","suit":"Clubs"}}}, + {"timestamp":0,"username":"kat","action":{"action":"WinTrick"}}, + {"timestamp":0,"username":"kat","action":{"action":"PlayCard","card":{"rank":"Eight","suit":"Diamonds"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Two","suit":"Spades"}}}, + {"timestamp":0,"username":"kat","action":{"action":"WinTrick"}}, + {"timestamp":0,"username":"kat","action":{"action":"PlayCard","card":{"rank":"Seven","suit":"Spades"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Eight","suit":"Spades"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"WinTrick"}}, + {"timestamp":0,"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Ace","suit":"Spades"}}}, + {"timestamp":0,"username":"kat","action":{"action":"PlayCard","card":{"rank":"Six","suit":"Spades"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"WinTrick"}}, + {"timestamp":0,"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Queen","suit":"Spades"}}}, + {"timestamp":0,"username":"kat","action":{"action":"PlayCard","card":{"rank":"Two","suit":"Hearts"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"WinTrick"}}, + {"timestamp":0,"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Jack","suit":"Hearts"}}}, + {"timestamp":0,"username":"kat","action":{"action":"PlayCard","card":{"rank":"Three","suit":"Diamonds"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"WinTrick"}}, + {"timestamp":0,"username":"geoff","action":{"action":"WinCall"}}, + {"timestamp":0,"username":"kat","action":{"action":"NextToDeal"}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Ace","suit":"Hearts"}}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Ace","suit":"Clubs"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"King","suit":"Diamonds"}}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Nine","suit":"Clubs"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Four","suit":"Spades"}}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Ten","suit":"Diamonds"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Nine","suit":"Diamonds"}}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Queen","suit":"Clubs"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Ten","suit":"Spades"}}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Ace","suit":"Diamonds"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Jack","suit":"Diamonds"}}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"King","suit":"Hearts"}}}, + {"timestamp":0,"username":"kat","action":{"action":"EndDeal"}}, + {"timestamp":0,"username":"geoff","action":{"action":"ChooseTrumps","suit":"Diamonds"}}, + {"timestamp":0,"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Ace","suit":"Hearts"}}}, + {"timestamp":0,"username":"kat","action":{"action":"PlayCard","card":{"rank":"King","suit":"Hearts"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"WinTrick"}}, + {"timestamp":0,"username":"geoff","action":{"action":"PlayCard","card":{"rank":"King","suit":"Diamonds"}}}, + {"timestamp":0,"username":"kat","action":{"action":"PlayCard","card":{"rank":"Ace","suit":"Diamonds"}}}, + {"timestamp":0,"username":"kat","action":{"action":"WinTrick"}}, + {"timestamp":0,"username":"kat","action":{"action":"PlayCard","card":{"rank":"Ace","suit":"Clubs"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Nine","suit":"Diamonds"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"WinTrick"}}, + {"timestamp":0,"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Jack","suit":"Diamonds"}}}, + {"timestamp":0,"username":"kat","action":{"action":"PlayCard","card":{"rank":"Ten","suit":"Diamonds"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"WinTrick"}}, + {"timestamp":0,"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Ten","suit":"Spades"}}}, + {"timestamp":0,"username":"kat","action":{"action":"PlayCard","card":{"rank":"Nine","suit":"Clubs"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"WinTrick"}}, + {"timestamp":0,"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Four","suit":"Spades"}}}, + {"timestamp":0,"username":"kat","action":{"action":"PlayCard","card":{"rank":"Queen","suit":"Clubs"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"WinTrick"}}, + {"timestamp":0,"username":"geoff","action":{"action":"WinCall"}}, + {"timestamp":0,"username":"geoff","action":{"action":"NextToDeal"}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Ace","suit":"Spades"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Six","suit":"Spades"}}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Two","suit":"Spades"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Two","suit":"Clubs"}}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Four","suit":"Hearts"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Five","suit":"Diamonds"}}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Six","suit":"Diamonds"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Five","suit":"Spades"}}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"King","suit":"Hearts"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Ten","suit":"Clubs"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"EndDeal"}}, + {"timestamp":0,"username":"geoff","action":{"action":"ChooseTrumps","suit":"Clubs"}}, + {"timestamp":0,"username":"kat","action":{"action":"PlayCard","card":{"rank":"Ace","suit":"Spades"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Five","suit":"Spades"}}}, + {"timestamp":0,"username":"kat","action":{"action":"WinTrick"}}, + {"timestamp":0,"username":"kat","action":{"action":"PlayCard","card":{"rank":"King","suit":"Hearts"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Two","suit":"Clubs"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"WinTrick"}}, + {"timestamp":0,"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Ten","suit":"Clubs"}}}, + {"timestamp":0,"username":"kat","action":{"action":"PlayCard","card":{"rank":"Two","suit":"Spades"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"WinTrick"}}, + {"timestamp":0,"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Six","suit":"Spades"}}}, + {"timestamp":0,"username":"kat","action":{"action":"PlayCard","card":{"rank":"Four","suit":"Hearts"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"WinTrick"}}, + {"timestamp":0,"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Five","suit":"Diamonds"}}}, + {"timestamp":0,"username":"kat","action":{"action":"PlayCard","card":{"rank":"Six","suit":"Diamonds"}}}, + {"timestamp":0,"username":"kat","action":{"action":"WinTrick"}}, + {"timestamp":0,"username":"geoff","action":{"action":"WinCall"}}, + {"timestamp":0,"username":"kat","action":{"action":"NextToDeal"}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"King","suit":"Clubs"}}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Queen","suit":"Clubs"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Queen","suit":"Spades"}}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Nine","suit":"Spades"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Queen","suit":"Hearts"}}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Eight","suit":"Clubs"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Five","suit":"Clubs"}}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Two","suit":"Clubs"}}}, + {"timestamp":0,"username":"kat","action":{"action":"EndDeal"}}, + {"timestamp":0,"username":"geoff","action":{"action":"ChooseTrumps","suit":"Clubs"}}, + {"timestamp":0,"username":"geoff","action":{"action":"PlayCard","card":{"rank":"King","suit":"Clubs"}}}, + {"timestamp":0,"username":"kat","action":{"action":"PlayCard","card":{"rank":"Two","suit":"Clubs"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"WinTrick"}}, + {"timestamp":0,"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Queen","suit":"Spades"}}}, + {"timestamp":0,"username":"kat","action":{"action":"PlayCard","card":{"rank":"Nine","suit":"Spades"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"WinTrick"}}, + {"timestamp":0,"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Queen","suit":"Hearts"}}}, + {"timestamp":0,"username":"kat","action":{"action":"PlayCard","card":{"rank":"Eight","suit":"Clubs"}}}, + {"timestamp":0,"username":"kat","action":{"action":"WinTrick"}}, + {"timestamp":0,"username":"kat","action":{"action":"PlayCard","card":{"rank":"Queen","suit":"Clubs"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Five","suit":"Clubs"}}}, + {"timestamp":0,"username":"kat","action":{"action":"WinTrick"}}, + {"timestamp":0,"username":"geoff","action":{"action":"RevealCard","card":{"rank":"Three","suit":"Diamonds"}}}, + {"timestamp":0,"username":"kat","action":{"action":"RevealCard","card":{"rank":"Ten","suit":"Clubs"}}}, + {"timestamp":0,"username":"kat","action":{"action":"WinCall"}}, + {"timestamp":0,"username":"geoff","action":{"action":"NextToDeal"}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"King","suit":"Spades"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Six","suit":"Spades"}}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Four","suit":"Spades"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Six","suit":"Hearts"}}}, + {"timestamp":0,"username":"kat","action":{"action":"ReceiveCard","card":{"rank":"Queen","suit":"Hearts"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"ReceiveCard","card":{"rank":"Ten","suit":"Clubs"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"EndDeal"}}, + {"timestamp":0,"username":"kat","action":{"action":"ChooseTrumps","suit":"Spades"}}, + {"timestamp":0,"username":"kat","action":{"action":"PlayCard","card":{"rank":"King","suit":"Spades"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Six","suit":"Spades"}}}, + {"timestamp":0,"username":"kat","action":{"action":"WinTrick"}}, + {"timestamp":0,"username":"kat","action":{"action":"PlayCard","card":{"rank":"Queen","suit":"Hearts"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Six","suit":"Hearts"}}}, + {"timestamp":0,"username":"kat","action":{"action":"WinTrick"}}, + {"timestamp":0,"username":"kat","action":{"action":"PlayCard","card":{"rank":"Four","suit":"Spades"}}}, + {"timestamp":0,"username":"geoff","action":{"action":"PlayCard","card":{"rank":"Ten","suit":"Clubs"}}}, + {"timestamp":0,"username":"kat","action":{"action":"WinTrick"}}, + {"timestamp":0,"username":"geoff","action":{"action":"KnockedOut"}}, + {"timestamp":0,"username":"kat","action":{"action":"WinGame"}} ]"#; let actions = serde_json::from_str(actions).unwrap(); diff --git a/src/util/millis.rs b/src/util/millis.rs new file mode 100644 index 0000000..64f3603 --- /dev/null +++ b/src/util/millis.rs @@ -0,0 +1,56 @@ +use std::fmt; +use std::time::{Duration, SystemTime}; + +use serde::{Serializer, Deserializer, de::{self, Visitor}}; + +pub fn deserialize<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de> +{ + struct SystemTimeVisitor; + + impl<'de> Visitor<'de> for SystemTimeVisitor { + type Value = SystemTime; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a timestamp in milliseconds since the unix epoch") + } + + fn visit_i64(self, value: i64) -> Result + where + E: de::Error + { + if value >= 0 { + Ok(SystemTime::UNIX_EPOCH + Duration::from_millis(value as u64)) + } else { + Ok(SystemTime::UNIX_EPOCH - Duration::from_millis((-value) as u64)) + } + } + + fn visit_u64(self, value: u64) -> Result + where + E: de::Error + { + Ok(SystemTime::UNIX_EPOCH + Duration::from_millis(value)) + } + } + + deserializer.deserialize_i64(SystemTimeVisitor) +} + +pub fn serialize(timestamp: &SystemTime, serializer: S) -> Result +where + S: Serializer +{ + match timestamp.duration_since(SystemTime::UNIX_EPOCH) { + Ok(duration) => { + let millis = duration.as_secs() as i64 * 1000 + duration.subsec_millis() as i64; + serializer.serialize_i64(millis) + } + Err(err) => { + let duration = err.duration(); + let millis = duration.as_secs() as i64 * 1000 + duration.subsec_millis() as i64; + serializer.serialize_i64(-millis) + } + } +} diff --git a/src/util/mod.rs b/src/util/mod.rs index adc335c..0fe572d 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1 +1,2 @@ pub mod max; +pub mod millis;