add timestamps to all game actions
authorGeoffrey Allott <geoffrey@allott.email>
Tue, 2 Mar 2021 21:14:41 +0000 (21:14 +0000)
committerGeoffrey Allott <geoffrey@allott.email>
Tue, 2 Mar 2021 21:14:41 +0000 (21:14 +0000)
src/client.rs
src/dealer.rs
src/game/action.rs
src/game/chatroom.rs
src/game/mod.rs
src/game/poker/holdem.rs
src/game/whist.rs
src/util/millis.rs [new file with mode: 0644]
src/util/mod.rs

index a32af50dc7dab4b35a622c53369b2f6372a5066d..bb6cca373c032a3448f01795e1db8b6e5934725a 100644 (file)
@@ -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();
index ba661f29338188d5100bcd211589ec822b49bfc9..fb56f65b4f2ff8202b5d28809e0f343f2a92f22a 100644 (file)
@@ -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);
index 890347f7f90393d6a967e9a133bdb0c4cb6516b7..68d6be8da7f3de73e5f16480ba305062cec2945b 100644 (file)
@@ -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() },
         }
     }
index 113ef7728d2878caa8fbbe9b6dd4f4c1b97604fd..fbb8f98db87d569281da335b507f2ef928960d69 100644 (file)
@@ -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<ChatroomUserAction>,
     users: HashSet<Username>,
 }
 
@@ -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<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,
         }
     }
index 6c73c3b1c0a12f3e77b76115ea10f95532d08974..690dea1c427a2f5a85c49338a28828ed350c7401 100644 (file)
@@ -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<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 {
index 33a81ea070d816d56e5bed3cfdd01c8bb7743fe9..505bd3fb654071ea699138f716827d4f76dee6a4 100644 (file)
@@ -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<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)
@@ -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<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
@@ -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<UserAction>, 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();
 
index ffa5c1cf918d83a986a10025d1823583e1f07a03..999fd98ed0b0ce0577834264a6d2ee51ff536602 100644 (file)
@@ -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<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)
@@ -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<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
@@ -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<UserAction>, 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 (file)
index 0000000..64f3603
--- /dev/null
@@ -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<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)
+        }
+    }
+}
index adc335c92469b4472ac50f3203f802300c8707d0..0fe572d92b11db1e53513aff16fc65ca0811db5a 100644 (file)
@@ -1 +1,2 @@
 pub mod max;
+pub mod millis;