add blind structure
authorGeoffrey Allott <geoffrey@allott.email>
Sat, 20 Mar 2021 13:39:30 +0000 (13:39 +0000)
committerGeoffrey Allott <geoffrey@allott.email>
Sat, 20 Mar 2021 13:39:30 +0000 (13:39 +0000)
src/game/action.rs
src/game/poker/holdem.rs
src/util/timestamp.rs

index c6222067543be94fdfc40b495a696f63ebdad8b2..9e17c3500edcfeede46c612c3af7e333943847d6 100644 (file)
@@ -51,6 +51,7 @@ pub enum Action {
     TimeoutFold,
     Bet { chips: u64 },
     PostBlind { chips: u64 },
+    NewBlinds { level: i64, small_blind: u64, big_blind: u64 },
     WinTrick,
     WinCall,
     WinHand { chips: u64, hand: Option<String> },
index 56796d57149e3763911d3f3cb193a5f4fc91234a..7fab4893b6353a7ed74446c2e76f419ee7dbd55c 100644 (file)
@@ -31,12 +31,21 @@ enum State {
     Completed,
 }
 
+#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
+struct Level {
+    level: i64,
+    small_blind: u64,
+    big_blind: u64,
+}
+
 #[derive(Clone, Debug, Serialize, Deserialize)]
 pub struct TexasHoldEmSettings {
     title: String,
     max_players: u32,
     small_blind: u64,
     starting_stack: u64,
+    round_length: Option<i64>,
+    tournament_length: Option<i64>,
     action_timeout: Option<i64>,
 }
 
@@ -44,6 +53,38 @@ impl TexasHoldEmSettings {
     pub fn title(&self) -> &str {
         &self.title
     }
+
+    fn level(&self, elapsed: i64) -> Level {
+        match (self.round_length, self.tournament_length) {
+            (None, _) | (_, None) => Level { level: 0, small_blind: self.small_blind, big_blind: self.small_blind * 2 },
+            (Some(round_length), Some(tournament_length)) => {
+                const BIG_BLINDS_AT_TOURNAMENT_END: u64 = 10;
+                const fn round_big_blind(blind: u64) -> u64 {
+                    match blind {
+                        u64::MIN..=1 => 2,
+                        2..=10 => blind / 2 * 2,
+                        11..=100 => (blind + 4) / 10 * 10,
+                        101..=500 => (blind + 24) / 50 * 50,
+                        501..=1000 => (blind + 49) / 100 * 100,
+                        1001..=5000 => (blind + 249) / 500 * 500,
+                        5001..=10000 => (blind + 499) / 1000 * 1000,
+                        10001..=50000 => (blind + 2499) / 5000 * 5000,
+                        50001..=u64::MAX => (blind + 4999) / 10000 * 10000,
+                    }
+                }
+                let starting_big_blind = self.small_blind * 2;
+                let final_big_blind = self.starting_stack * self.max_players as u64 / 2 / BIG_BLINDS_AT_TOURNAMENT_END;
+                let expected_levels = (tournament_length / round_length).max(1);
+                let growth_rate = (final_big_blind as f64 / starting_big_blind as f64).ln() / expected_levels as f64;
+
+                let level = elapsed.max(0) / round_length;
+                let big_blind = round_big_blind(((level as f64 * growth_rate).exp() * starting_big_blind as f64) as u64);
+                let small_blind = big_blind / 2;
+
+                Level { level, big_blind, small_blind }
+            }
+        }
+    }
 }
 
 #[derive(Clone, Debug)]
@@ -52,6 +93,7 @@ pub struct TexasHoldEm {
     settings: TexasHoldEmSettings,
     rng: WaveRng,
     actions_len: usize,
+    start_time: Option<Timestamp>,
     last_action_time: Option<Timestamp>,
     state: State,
     seats: Seats,
@@ -66,20 +108,19 @@ pub struct TexasHoldEm {
     committed: HashMap<Username, u64>,
     in_hand: HashSet<Username>,
     pot: u64,
-    small_blind: u64,
-    big_blind: u64,
+    level: Level,
     ghosts: HashMap<Username, u8>,
 }
 
 impl TexasHoldEm {
     pub fn new(id: i64, settings: TexasHoldEmSettings, seed: Seed) -> Self {
-        let small_blind = settings.small_blind;
-        let big_blind = small_blind * 2;
+        let level = settings.level(0);
         Self {
             id,
             settings,
             rng: seed.into_rng(),
             actions_len: 0,
+            start_time: None,
             last_action_time: None,
             state: State::NotStarted,
             seats: Seats::new(),
@@ -94,8 +135,7 @@ impl TexasHoldEm {
             committed: HashMap::new(),
             in_hand: HashSet::new(),
             pot: 0,
-            small_blind,
-            big_blind,
+            level,
             ghosts: HashMap::new(),
         }
     }
@@ -110,10 +150,10 @@ impl TexasHoldEm {
 
     fn min_raise(&self) -> u64 {
         match (self.state, self.bets.values().max()) {
-            (State::PreFlopBetting, Some(&max)) if max < self.big_blind * 2 => self.big_blind,
-            (State::PreFlopBetting, Some(&max)) => max - self.big_blind,
+            (State::PreFlopBetting, Some(&max)) if max < self.level.big_blind * 2 => self.level.big_blind,
+            (State::PreFlopBetting, Some(&max)) => max - self.level.big_blind,
             (_, Some(&max)) => max,
-            (_, None) => self.big_blind,
+            (_, None) => self.level.big_blind,
         }
     }
 
@@ -167,7 +207,7 @@ impl TexasHoldEm {
     fn betting_round_completed(&self) -> bool {
         if self.state == State::PreFlopBetting {
             if let Some(big_blind) = self.big_blind_player() {
-                if self.active != Some(big_blind) && !self.player_is_all_in(big_blind) && self.bets.get(&big_blind) == Some(&self.big_blind) {
+                if self.active != Some(big_blind) && !self.player_is_all_in(big_blind) && self.bets.get(&big_blind) == Some(&self.level.big_blind) {
                     return false;
                 }
             }
@@ -303,7 +343,14 @@ impl Game for TexasHoldEm {
                 }
                 Ok(())
             }
+            (_, Action::NewBlinds { level, small_blind, big_blind }) => {
+                self.level = Level { level, small_blind, big_blind };
+                Ok(())
+            }
             (_, Action::NextToDeal) => {
+                if self.state == State::NotStarted {
+                    self.start_time = Some(timestamp);
+                }
                 self.remove_ghosts()?;
                 self.dealer = Some(username);
                 self.deck = FIFTY_TWO_CARD_DECK.iter().cloned().collect();
@@ -454,7 +501,7 @@ impl Game for TexasHoldEm {
             }
             State::PostingSmallBlind if self.seats.players_len() == 2 => {
                 if let Some(username) = self.dealer {
-                    let chips = self.stack(username).min(self.small_blind);
+                    let chips = self.stack(username).min(self.level.small_blind);
                     DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::PostBlind { chips } }))
                 } else {
                     error!("There is no player to post the small blind: {:#?}", self);
@@ -463,7 +510,7 @@ impl Game for TexasHoldEm {
             }
             State::PostingSmallBlind => {
                 if let Some(username) = self.dealer.and_then(|dealer| self.seats.player_after(dealer)) {
-                    let chips = self.stack(username).min(self.small_blind);
+                    let chips = self.stack(username).min(self.level.small_blind);
                     DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::PostBlind { chips } }))
                 } else {
                     error!("There is no player to post the small blind: {:#?}", self);
@@ -472,7 +519,7 @@ impl Game for TexasHoldEm {
             }
             State::PostingBigBlind if self.seats.players_len() == 2 => {
                 if let Some(username) = self.dealer.and_then(|dealer| self.seats.player_after(dealer)) {
-                    let chips = self.stack(username).min(self.small_blind * 2);
+                    let chips = self.stack(username).min(self.level.big_blind);
                     DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::PostBlind { chips } }))
                 } else {
                     error!("There is no player to post the big blind: {:#?}", self);
@@ -483,7 +530,7 @@ impl Game for TexasHoldEm {
                 if let Some(username) =
                     self.dealer.and_then(|dealer| self.seats.player_after(dealer)).and_then(|small_blind| self.seats.player_after(small_blind))
                 {
-                    let chips = self.stack(username).min(self.small_blind * 2);
+                    let chips = self.stack(username).min(self.level.big_blind);
                     DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::PostBlind { chips } }))
                 } else {
                     error!("There is no player to post the big blind: {:#?}", self);
@@ -508,7 +555,16 @@ impl Game for TexasHoldEm {
                     } else if let Some(username) = self.next_knocked_out_player() {
                         DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::KnockedOut }))
                     } else if let Some(username) = self.dealer.and_then(|dealer| self.seats.player_after(dealer)) {
-                        DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::NextToDeal }))
+                        let level = self.start_time.map(|start_time| self.settings.level(timestamp.millis_since(start_time))).unwrap_or(self.level);
+                        if level > self.level {
+                            DealerAction::TakeAction(ValidatedUserAction(UserAction {
+                                timestamp,
+                                username,
+                                action: Action::NewBlinds { level: level.level, small_blind: level.small_blind, big_blind: level.big_blind },
+                            }))
+                        } else {
+                            DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::NextToDeal }))
+                        }
                     } else {
                         error!("Logic error: no dealer could be chosen: {:#?}", self);
                         DealerAction::Leave
@@ -543,7 +599,16 @@ impl Game for TexasHoldEm {
                 } else if let Some(username) = self.only_player_left() {
                     DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::WinGame }))
                 } else if let Some(username) = self.dealer.and_then(|dealer| self.seats.player_after(dealer)) {
-                    DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::NextToDeal }))
+                        let level = self.start_time.map(|start_time| self.settings.level(timestamp.millis_since(start_time))).unwrap_or(self.level);
+                    if level > self.level {
+                        DealerAction::TakeAction(ValidatedUserAction(UserAction {
+                            timestamp,
+                            username,
+                            action: Action::NewBlinds { level: level.level, small_blind: level.small_blind, big_blind: level.big_blind },
+                        }))
+                    } else {
+                        DealerAction::TakeAction(ValidatedUserAction(UserAction { timestamp, username, action: Action::NextToDeal }))
+                    }
                 } else {
                     error!("Logic error: no dealer could be chosen: {:#?}", self);
                     DealerAction::Leave
@@ -583,6 +648,50 @@ impl Game for TexasHoldEm {
 mod tests {
     use super::*;
 
+    #[test]
+    fn no_blind_structure() {
+        let settings = TexasHoldEmSettings {
+            title: String::from("Test Blind Structure"),
+            max_players: 4,
+            small_blind: 50,
+            starting_stack: 1000,
+            round_length: None,
+            tournament_length: None,
+            action_timeout: None,
+        };
+
+        assert_eq!(50, settings.level(0).small_blind);
+        assert_eq!(100, settings.level(0).big_blind);
+
+        assert_eq!(50, settings.level(1000000000000).small_blind);
+        assert_eq!(100, settings.level(1000000000000).big_blind);
+    }
+
+    #[test]
+    fn blind_structure() {
+        let settings = TexasHoldEmSettings {
+            title: String::from("Test Blind Structure"),
+            max_players: 4,
+            small_blind: 25,
+            starting_stack: 5000,
+            round_length: Some(20 * 60 * 1000),
+            tournament_length: Some(4 * 60 * 60 * 1000),
+            action_timeout: None,
+        };
+
+        assert_eq!(25, settings.level(0).small_blind);
+        assert_eq!(50, settings.level(0).big_blind);
+
+        assert!(settings.level(settings.round_length.unwrap()).small_blind > 25);
+        assert!(settings.level(settings.round_length.unwrap()).big_blind > 50);
+
+        assert!(settings.level(settings.tournament_length.unwrap()).small_blind > 250);
+        assert!(settings.level(settings.tournament_length.unwrap()).big_blind > 500);
+
+        assert!(settings.level(settings.tournament_length.unwrap()).small_blind < 2500);
+        assert!(settings.level(settings.tournament_length.unwrap()).big_blind < 5000);
+    }
+
     fn test_game(actions: Vec<UserAction>, settings: TexasHoldEmSettings, seed: Seed) {
         let mut game = TexasHoldEm::new(0, settings, seed);
         for action in actions {
@@ -636,7 +745,7 @@ mod tests {
         ]"#;
         let actions = serde_json::from_str(actions).unwrap();
 
-        let settings = r#"{"title":"2-Player TexasHoldEm Test","max_players":2,"small_blind":100,"starting_stack":1000,"action_timeout":null}"#;
+        let settings = r#"{"title":"2-Player TexasHoldEm Test","max_players":2,"small_blind":100,"starting_stack":1000}"#;
         let settings = serde_json::from_str(settings).unwrap();
 
         let seed = r#"{"rng":"ChaCha20","seed":"e0355d5c6c63ef757d1b874b0392a3deec73cadfb0a2aa7947a04db651bf9269"}"#;
@@ -682,7 +791,7 @@ mod tests {
         ]"#;
         let actions = serde_json::from_str(actions).unwrap();
 
-        let settings = r#"{"title":"2-Player TexasHoldEm Test","max_players":2,"small_blind":25,"starting_stack":1000,"action_timeout":null}"#;
+        let settings = r#"{"title":"2-Player TexasHoldEm Test","max_players":2,"small_blind":25,"starting_stack":1000}"#;
         let settings = serde_json::from_str(settings).unwrap();
 
         let seed = r#"{"rng":"ChaCha20","seed":"f05dc83bdce966e72a3a81b19ccded2e70387eb68deacf60ed8de1ee78b9ff0e"}"#;
@@ -765,7 +874,7 @@ mod tests {
         ]"#;
         let actions = serde_json::from_str(actions).unwrap();
 
-        let settings = r#"{"title":"2-Player TexasHoldEm Test","max_players":2,"small_blind":100,"starting_stack":1000,"action_timeout":null}"#;
+        let settings = r#"{"title":"2-Player TexasHoldEm Test","max_players":2,"small_blind":100,"starting_stack":1000}"#;
         let settings = serde_json::from_str(settings).unwrap();
 
         let seed = r#"{"rng":"ChaCha20","seed":"fd87ec4b51fcaf056ef53c0460322e1fa5261cf2801d005065c9add8ec541bb4"}"#;
@@ -826,7 +935,7 @@ mod tests {
         ]"#;
         let actions = serde_json::from_str(actions).unwrap();
 
-        let settings = r#"{"title":"2-Player TexasHoldEm Test","max_players":2,"small_blind":100,"starting_stack":1000,"action_timeout":null}"#;
+        let settings = r#"{"title":"2-Player TexasHoldEm Test","max_players":2,"small_blind":100,"starting_stack":1000}"#;
         let settings = serde_json::from_str(settings).unwrap();
 
         let seed = r#"{"rng":"ChaCha20","seed":"fd87ec4b51fcaf056ef53c0460322e1fa5261cf2801d005065c9add8ec541bb4"}"#;
@@ -872,7 +981,7 @@ mod tests {
         ]"#;
         let actions = serde_json::from_str(actions).unwrap();
 
-        let settings = r#"{"title":"2-Player TexasHoldEm Test","max_players":2,"small_blind":100,"starting_stack":1000,"action_timeout":null}"#;
+        let settings = r#"{"title":"2-Player TexasHoldEm Test","max_players":2,"small_blind":100,"starting_stack":1000}"#;
         let settings = serde_json::from_str(settings).unwrap();
 
         let seed = r#"{"rng":"ChaCha20","seed":"8de0ac3be302e26cbc0a371044c8b349107108abb1f94a10fe84ba04a59d7f31"}"#;
@@ -1021,8 +1130,7 @@ mod tests {
         ]"#;
         let actions = serde_json::from_str(actions).unwrap();
 
-        let settings =
-            r#"{"format":"TexasHoldEm","title":"4-Player TexasHoldEm Test","max_players":4,"small_blind":25,"starting_stack":1000,"action_timeout":null}"#;
+        let settings = r#"{"format":"TexasHoldEm","title":"4-Player TexasHoldEm Test","max_players":4,"small_blind":25,"starting_stack":1000}"#;
         let settings = serde_json::from_str(settings).unwrap();
 
         let seed = r#"{"rng":"ChaCha20","seed":"48e2f45eb4a1ac6bc4ab4f2368ba2d9b0d7c1f132d7fc7f51036e92112dae136"}"#;
@@ -1082,8 +1190,7 @@ mod tests {
         ]"#;
         let actions = serde_json::from_str(actions).unwrap();
 
-        let settings =
-            r#"{"format":"TexasHoldEm","title":"4-Player TexasHoldEm Test","max_players":4,"small_blind":25,"starting_stack":1000,"action_timeout":null}"#;
+        let settings = r#"{"format":"TexasHoldEm","title":"4-Player TexasHoldEm Test","max_players":4,"small_blind":25,"starting_stack":1000}"#;
         let settings = serde_json::from_str(settings).unwrap();
 
         let seed = r#"{"rng":"ChaCha20","seed":"48e2f45eb4a1ac6bc4ab4f2368ba2d9b0d7c1f132d7fc7f51036e92112dae136"}"#;
@@ -1187,8 +1294,7 @@ mod tests {
         ]"#;
         let actions = serde_json::from_str(actions).unwrap();
 
-        let settings =
-            r#"{"format":"TexasHoldEm","title":"4-Player TexasHoldEm Test","max_players":4,"small_blind":25,"starting_stack":1000,"action_timeout":null}"#;
+        let settings = r#"{"format":"TexasHoldEm","title":"4-Player TexasHoldEm Test","max_players":4,"small_blind":25,"starting_stack":1000}"#;
         let settings = serde_json::from_str(settings).unwrap();
 
         let seed = r#"{"rng":"ChaCha20","seed":"092b99f45313fff167029dc7420ed69a92becae492e09b65bc06ddcaae3c9e9c"}"#;
@@ -1298,8 +1404,7 @@ mod tests {
         ]"#;
         let actions = serde_json::from_str(actions).unwrap();
 
-        let settings =
-            r#"{"format":"TexasHoldEm","title":"4-Player TexasHoldEm Test","max_players":4,"small_blind":25,"starting_stack":1000,"action_timeout":null}"#;
+        let settings = r#"{"format":"TexasHoldEm","title":"4-Player TexasHoldEm Test","max_players":4,"small_blind":25,"starting_stack":1000}"#;
         let settings = serde_json::from_str(settings).unwrap();
 
         let seed = r#"{"rng":"ChaCha20","seed":"092b99f45313fff167029dc7420ed69a92becae492e09b65bc06ddcaae3c9e9c"}"#;
@@ -1344,8 +1449,7 @@ mod tests {
         ]"#;
         let actions = serde_json::from_str(actions).unwrap();
 
-        let settings =
-            r#"{"format":"TexasHoldEm","title":"4-Player TexasHoldEm Test","max_players":4,"small_blind":25,"starting_stack":1000,"action_timeout":null}"#;
+        let settings = r#"{"format":"TexasHoldEm","title":"4-Player TexasHoldEm Test","max_players":4,"small_blind":25,"starting_stack":1000}"#;
         let settings = serde_json::from_str(settings).unwrap();
 
         let seed = r#"{"rng":"ChaCha20","seed":"092b99f45313fff167029dc7420ed69a92becae492e09b65bc06ddcaae3c9e9c"}"#;
index 7b7f8e062f4b4d0aea93f00f1a09e244dbf90e0b..387f70a53ecb626d513708dfd437115acd676c68 100644 (file)
@@ -10,6 +10,10 @@ impl Timestamp {
     pub fn plus_millis(self, millis: i64) -> Self {
         Timestamp(self.0.saturating_add(millis))
     }
+
+    pub fn millis_since(self, time: Timestamp) -> i64 {
+        self.0.saturating_sub(time.0)
+    }
 }
 
 impl Display for Timestamp {