From 7a791d9560727f559d534c173fce44d5541c6bfb Mon Sep 17 00:00:00 2001 From: Geoffrey Allott Date: Sat, 20 Mar 2021 13:39:30 +0000 Subject: [PATCH] add blind structure --- src/game/action.rs | 1 + src/game/poker/holdem.rs | 166 +++++++++++++++++++++++++++++++-------- src/util/timestamp.rs | 4 + 3 files changed, 140 insertions(+), 31 deletions(-) diff --git a/src/game/action.rs b/src/game/action.rs index c622206..9e17c35 100644 --- a/src/game/action.rs +++ b/src/game/action.rs @@ -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 }, diff --git a/src/game/poker/holdem.rs b/src/game/poker/holdem.rs index 56796d5..7fab489 100644 --- a/src/game/poker/holdem.rs +++ b/src/game/poker/holdem.rs @@ -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, + tournament_length: Option, action_timeout: Option, } @@ -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, last_action_time: Option, state: State, seats: Seats, @@ -66,20 +108,19 @@ pub struct TexasHoldEm { committed: HashMap, in_hand: HashSet, pot: u64, - small_blind: u64, - big_blind: u64, + level: Level, ghosts: HashMap, } 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, 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"}"#; diff --git a/src/util/timestamp.rs b/src/util/timestamp.rs index 7b7f8e0..387f70a 100644 --- a/src/util/timestamp.rs +++ b/src/util/timestamp.rs @@ -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 { -- 2.34.1