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>,
}
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)]
settings: TexasHoldEmSettings,
rng: WaveRng,
actions_len: usize,
+ start_time: Option<Timestamp>,
last_action_time: Option<Timestamp>,
state: State,
seats: Seats,
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(),
committed: HashMap::new(),
in_hand: HashSet::new(),
pot: 0,
- small_blind,
- big_blind,
+ level,
ghosts: HashMap::new(),
}
}
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,
}
}
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;
}
}
}
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();
}
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);
}
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);
}
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);
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);
} 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
} 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
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 {
]"#;
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"}"#;
]"#;
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"}"#;
]"#;
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"}"#;
]"#;
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"}"#;
]"#;
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"}"#;
]"#;
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"}"#;
]"#;
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"}"#;
]"#;
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"}"#;
]"#;
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"}"#;
]"#;
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"}"#;