use std::collections::HashSet;
+use std::time::SystemTime;
use futures::stream::{Stream, StreamExt, empty, iter, once};
}
}
(&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();
use std::collections::HashSet;
+use std::time::SystemTime;
use async_std::stream::StreamExt;
use futures::channel::mpsc::Receiver;
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);
+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,
}
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() },
}
}
use std::collections::HashSet;
+use std::time::SystemTime;
use crate::username::{DEALER, Username};
use crate::game::{Action, ActionError};
Leave,
}
+#[derive(Debug, Clone)]
+struct ChatroomUserAction {
+ timestamp: SystemTime,
+ username: Username,
+ action: ChatroomAction,
+}
+
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatroomSettings {
title: String,
pub struct Chatroom {
id: i64,
settings: ChatroomSettings,
- messages: Vec<(Username, ChatroomAction)>,
+ messages: Vec<ChatroomUserAction>,
users: HashSet<Username>,
}
}
}
- 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<ValidatedUserAction> {
+ fn next_dealer_action(&self, timestamp: SystemTime) -> Option<ValidatedUserAction> {
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,
}
}
use std::collections::HashSet;
use std::fmt::Debug;
+use std::time::SystemTime;
use crate::rng::Seed;
use crate::username::Username;
fn actions_len(&self) -> usize;
fn validate_action(&self, action: UserAction) -> Result<ValidatedUserAction, ActionError>;
fn take_action(&mut self, action: ValidatedUserAction) -> Result<(), ActionError>;
- fn next_dealer_action(&self) -> Option<ValidatedUserAction>;
+ fn next_dealer_action(&self, timestamp: SystemTime) -> Option<ValidatedUserAction>;
}
pub trait CloneBoxGame {
use std::collections::{HashMap, HashSet};
use std::convert::TryInto;
+use std::time::SystemTime;
use itertools::Itertools;
self.actions_len
}
- fn validate_action(&self, UserAction{username, action}: UserAction) -> Result<ValidatedUserAction, ActionError> {
+ fn validate_action(&self, UserAction{timestamp, username, action}: UserAction) -> Result<ValidatedUserAction, ActionError> {
match (self.state, action) {
(_, Action::PlayCard{..}) | (_, Action::ChooseTrumps{..}) => {
Err(ActionError::InvalidActionForGameType)
} 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),
(_, 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();
} 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}}))
}
}
}
}
}
- 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) {
}
}
- fn next_dealer_action(&self) -> Option<ValidatedUserAction> {
+ fn next_dealer_action(&self, timestamp: SystemTime) -> Option<ValidatedUserAction> {
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
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
}
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 => {
.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 => {
if self.pot > 0 {
self.players.iter().next()
.map(|&username| ValidatedUserAction(UserAction {
+ timestamp,
username,
action: Action::WinHand {
chips: self.pot,
}))
} 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
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
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)),
mod tests {
use super::*;
+ use std::time::SystemTime;
+
fn test_game(actions: Vec<UserAction>, settings: TexasHoldEmSettings, seed: Seed) {
let mut game = TexasHoldEm::new(0, settings, seed);
for action in actions {
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();
}
#[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();
#[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();
#[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();
use std::collections::{HashMap, HashSet};
+use std::time::SystemTime;
use crate::card::{Card, Suit, FIFTY_TWO_CARD_DECK};
use crate::rng::{Seed, WaveRng};
self.actions_len
}
- fn validate_action(&self, UserAction{username, action}: UserAction) -> Result<ValidatedUserAction, ActionError> {
+ fn validate_action(&self, UserAction{timestamp, username, action}: UserAction) -> Result<ValidatedUserAction, ActionError> {
match (self.state, action) {
(_, Action::AddOn{..}) | (_, Action::RevealCard{..}) | (_, Action::Fold) | (_, Action::Bet{..}) => {
Err(ActionError::InvalidActionForGameType)
} 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)
}
} 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),
}
}
- 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) {
}
}
- fn next_dealer_action(&self) -> Option<ValidatedUserAction> {
+ fn next_dealer_action(&self, timestamp: SystemTime) -> Option<ValidatedUserAction> {
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
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
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
}
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
}
mod tests {
use super::*;
+ use std::time::SystemTime;
+
fn test_game(actions: Vec<UserAction>, settings: KnockOutWhistSettings, seed: Seed) {
let mut game = KnockOutWhist::new(0, settings, seed);
for action in actions {
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();
}
#[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();
--- /dev/null
+use std::fmt;
+use std::time::{Duration, SystemTime};
+
+use serde::{Serializer, Deserializer, de::{self, Visitor}};
+
+pub fn deserialize<'de, D>(deserializer: D) -> Result<SystemTime, D::Error>
+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<E>(self, value: i64) -> Result<Self::Value, E>
+ 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<E>(self, value: u64) -> Result<Self::Value, E>
+ where
+ E: de::Error
+ {
+ Ok(SystemTime::UNIX_EPOCH + Duration::from_millis(value))
+ }
+ }
+
+ deserializer.deserialize_i64(SystemTimeVisitor)
+}
+
+pub fn serialize<S>(timestamp: &SystemTime, serializer: S) -> Result<S::Ok, S::Error>
+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)
+ }
+ }
+}
pub mod max;
+pub mod millis;