fix: seat restoration + chip refund on hand error, NoLimit max_raises
Critical fix: complete_hand() now restores seats when run_to_completion() fails, preventing silent NotEnoughPlayers termination that killed simulations at random seeds. Wagered chips are refunded before reset to prevent chip destruction on error path. Also adds max_raises: Option<usize> to BetLimit::NoLimit, propagated across all game/bot/tournament construction sites. NotEnoughPlayers now logs a warning with hand count instead of silently breaking the simulation loop.
This commit is contained in:
parent
edf8d7ae3f
commit
d00df65a98
15 changed files with 131 additions and 41 deletions
|
|
@ -296,7 +296,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fold_is_always_valid() {
|
fn fold_is_always_valid() {
|
||||||
let bet_limit = BetLimit::NoLimit { min_raise: 10 };
|
let bet_limit = BetLimit::NoLimit { min_raise: 10, max_raises: None };
|
||||||
assert!(validate_action(
|
assert!(validate_action(
|
||||||
&Action::Fold,
|
&Action::Fold,
|
||||||
pid(1),
|
pid(1),
|
||||||
|
|
@ -313,7 +313,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_allowed_when_no_bet() {
|
fn check_allowed_when_no_bet() {
|
||||||
let bet_limit = BetLimit::NoLimit { min_raise: 10 };
|
let bet_limit = BetLimit::NoLimit { min_raise: 10, max_raises: None };
|
||||||
assert!(validate_action(
|
assert!(validate_action(
|
||||||
&Action::Check,
|
&Action::Check,
|
||||||
pid(1),
|
pid(1),
|
||||||
|
|
@ -330,7 +330,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_not_allowed_when_bet_exists() {
|
fn check_not_allowed_when_bet_exists() {
|
||||||
let bet_limit = BetLimit::NoLimit { min_raise: 10 };
|
let bet_limit = BetLimit::NoLimit { min_raise: 10, max_raises: None };
|
||||||
let result = validate_action(
|
let result = validate_action(
|
||||||
&Action::Check,
|
&Action::Check,
|
||||||
pid(1),
|
pid(1),
|
||||||
|
|
@ -347,7 +347,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn call_always_valid_with_chips() {
|
fn call_always_valid_with_chips() {
|
||||||
let bet_limit = BetLimit::NoLimit { min_raise: 10 };
|
let bet_limit = BetLimit::NoLimit { min_raise: 10, max_raises: None };
|
||||||
assert!(validate_action(
|
assert!(validate_action(
|
||||||
&Action::Call,
|
&Action::Call,
|
||||||
pid(1),
|
pid(1),
|
||||||
|
|
@ -364,7 +364,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn raise_below_minimum_rejected() {
|
fn raise_below_minimum_rejected() {
|
||||||
let bet_limit = BetLimit::NoLimit { min_raise: 20 };
|
let bet_limit = BetLimit::NoLimit { min_raise: 20, max_raises: None };
|
||||||
let result = validate_action(
|
let result = validate_action(
|
||||||
&Action::Raise { total: 30 },
|
&Action::Raise { total: 30 },
|
||||||
pid(1),
|
pid(1),
|
||||||
|
|
@ -384,7 +384,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn raise_with_insufficient_chips_rejected() {
|
fn raise_with_insufficient_chips_rejected() {
|
||||||
let bet_limit = BetLimit::NoLimit { min_raise: 10 };
|
let bet_limit = BetLimit::NoLimit { min_raise: 10, max_raises: None };
|
||||||
let result = validate_action(
|
let result = validate_action(
|
||||||
&Action::Raise { total: 200 },
|
&Action::Raise { total: 200 },
|
||||||
pid(1),
|
pid(1),
|
||||||
|
|
@ -404,7 +404,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn all_in_with_zero_chips_rejected() {
|
fn all_in_with_zero_chips_rejected() {
|
||||||
let bet_limit = BetLimit::NoLimit { min_raise: 10 };
|
let bet_limit = BetLimit::NoLimit { min_raise: 10, max_raises: None };
|
||||||
let result = validate_action(
|
let result = validate_action(
|
||||||
&Action::AllIn,
|
&Action::AllIn,
|
||||||
pid(1),
|
pid(1),
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@ pub enum BetLimit {
|
||||||
NoLimit {
|
NoLimit {
|
||||||
/// Minimum raise increment.
|
/// Minimum raise increment.
|
||||||
min_raise: ChipCount,
|
min_raise: ChipCount,
|
||||||
|
/// Optional cap on voluntary raises per street (None = unlimited).
|
||||||
|
/// When set, the Nth raise is converted to a call automatically.
|
||||||
|
max_raises: Option<usize>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Pot-Limit — players may raise up to the current pot size.
|
/// Pot-Limit — players may raise up to the current pot size.
|
||||||
|
|
@ -80,7 +83,7 @@ pub trait BettingRules {
|
||||||
impl BettingRules for BetLimit {
|
impl BettingRules for BetLimit {
|
||||||
fn min_raise_amount(&self, _current_bet: ChipCount, previous_raise: ChipCount) -> ChipCount {
|
fn min_raise_amount(&self, _current_bet: ChipCount, previous_raise: ChipCount) -> ChipCount {
|
||||||
match self {
|
match self {
|
||||||
BetLimit::NoLimit { min_raise } => (*min_raise).max(previous_raise),
|
BetLimit::NoLimit { min_raise, .. } => (*min_raise).max(previous_raise),
|
||||||
BetLimit::PotLimit { min_raise } => (*min_raise).max(previous_raise),
|
BetLimit::PotLimit { min_raise } => (*min_raise).max(previous_raise),
|
||||||
BetLimit::FixedLimit {
|
BetLimit::FixedLimit {
|
||||||
small_bet,
|
small_bet,
|
||||||
|
|
@ -147,6 +150,10 @@ impl BettingRules for BetLimit {
|
||||||
/// unused — the cap is intentionally the same on every street.
|
/// unused — the cap is intentionally the same on every street.
|
||||||
fn can_raise(&self, _street: Street, raises_this_round: usize) -> bool {
|
fn can_raise(&self, _street: Street, raises_this_round: usize) -> bool {
|
||||||
match self {
|
match self {
|
||||||
|
BetLimit::NoLimit {
|
||||||
|
max_raises: Some(cap),
|
||||||
|
..
|
||||||
|
} => raises_this_round < *cap,
|
||||||
BetLimit::NoLimit { .. } | BetLimit::PotLimit { .. } => true,
|
BetLimit::NoLimit { .. } | BetLimit::PotLimit { .. } => true,
|
||||||
BetLimit::FixedLimit { .. } => raises_this_round < 3,
|
BetLimit::FixedLimit { .. } => raises_this_round < 3,
|
||||||
}
|
}
|
||||||
|
|
@ -267,7 +274,7 @@ mod tests {
|
||||||
|
|
||||||
/// Helper — No-Limit with a minimum raise of 10 (big-blind sized).
|
/// Helper — No-Limit with a minimum raise of 10 (big-blind sized).
|
||||||
fn nl() -> BetLimit {
|
fn nl() -> BetLimit {
|
||||||
BetLimit::NoLimit { min_raise: 10 }
|
BetLimit::NoLimit { min_raise: 10, max_raises: None }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper — Pot-Limit with a minimum raise of 10.
|
/// Helper — Pot-Limit with a minimum raise of 10.
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ mod tests {
|
||||||
fn make_game_info(current_bet: ChipCount, wagered_this_round: ChipCount) -> GameInfo {
|
fn make_game_info(current_bet: ChipCount, wagered_this_round: ChipCount) -> GameInfo {
|
||||||
GameInfo {
|
GameInfo {
|
||||||
num_seats: 2,
|
num_seats: 2,
|
||||||
bet_limit: BetLimit::NoLimit { min_raise: 10 },
|
bet_limit: BetLimit::NoLimit { min_raise: 10, max_raises: None },
|
||||||
small_blind: 5,
|
small_blind: 5,
|
||||||
big_blind: 10,
|
big_blind: 10,
|
||||||
ante: 0,
|
ante: 0,
|
||||||
|
|
|
||||||
|
|
@ -321,7 +321,7 @@ mod tests {
|
||||||
) -> GameInfo {
|
) -> GameInfo {
|
||||||
GameInfo {
|
GameInfo {
|
||||||
num_seats: 2,
|
num_seats: 2,
|
||||||
bet_limit: BetLimit::NoLimit { min_raise: 20 },
|
bet_limit: BetLimit::NoLimit { min_raise: 20, max_raises: None },
|
||||||
small_blind: 10,
|
small_blind: 10,
|
||||||
big_blind: 20,
|
big_blind: 20,
|
||||||
ante: 0,
|
ante: 0,
|
||||||
|
|
|
||||||
|
|
@ -601,7 +601,7 @@ mod tests {
|
||||||
) -> GameInfo {
|
) -> GameInfo {
|
||||||
GameInfo {
|
GameInfo {
|
||||||
num_seats: 2,
|
num_seats: 2,
|
||||||
bet_limit: BetLimit::NoLimit { min_raise: 20 },
|
bet_limit: BetLimit::NoLimit { min_raise: 20, max_raises: None },
|
||||||
small_blind: 10,
|
small_blind: 10,
|
||||||
big_blind: 20,
|
big_blind: 20,
|
||||||
ante: 0,
|
ante: 0,
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ mod tests {
|
||||||
fn make_game_info(current_bet: ChipCount, wagered_this_round: ChipCount) -> GameInfo {
|
fn make_game_info(current_bet: ChipCount, wagered_this_round: ChipCount) -> GameInfo {
|
||||||
GameInfo {
|
GameInfo {
|
||||||
num_seats: 2,
|
num_seats: 2,
|
||||||
bet_limit: BetLimit::NoLimit { min_raise: 10 },
|
bet_limit: BetLimit::NoLimit { min_raise: 10, max_raises: None },
|
||||||
small_blind: 5,
|
small_blind: 5,
|
||||||
big_blind: 10,
|
big_blind: 10,
|
||||||
ante: 0,
|
ante: 0,
|
||||||
|
|
|
||||||
|
|
@ -213,7 +213,7 @@ mod tests {
|
||||||
) -> GameInfo {
|
) -> GameInfo {
|
||||||
GameInfo {
|
GameInfo {
|
||||||
num_seats: 2,
|
num_seats: 2,
|
||||||
bet_limit: BetLimit::NoLimit { min_raise: 10 },
|
bet_limit: BetLimit::NoLimit { min_raise: 10, max_raises: None },
|
||||||
small_blind: 5,
|
small_blind: 5,
|
||||||
big_blind: 10,
|
big_blind: 10,
|
||||||
ante: 0,
|
ante: 0,
|
||||||
|
|
|
||||||
|
|
@ -232,7 +232,7 @@ mod tests {
|
||||||
) -> GameInfo {
|
) -> GameInfo {
|
||||||
GameInfo {
|
GameInfo {
|
||||||
num_seats: 2,
|
num_seats: 2,
|
||||||
bet_limit: BetLimit::NoLimit { min_raise: 10 },
|
bet_limit: BetLimit::NoLimit { min_raise: 10, max_raises: None },
|
||||||
small_blind: 5,
|
small_blind: 5,
|
||||||
big_blind: 10,
|
big_blind: 10,
|
||||||
ante: 0,
|
ante: 0,
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,9 @@ pub enum BetLimitConfig {
|
||||||
NoLimit {
|
NoLimit {
|
||||||
/// Minimum raise increment.
|
/// Minimum raise increment.
|
||||||
min_raise: ChipCount,
|
min_raise: ChipCount,
|
||||||
|
/// Optional cap on voluntary raises per street (None = unlimited).
|
||||||
|
#[serde(default)]
|
||||||
|
max_raises: Option<usize>,
|
||||||
},
|
},
|
||||||
/// Pot-Limit — players may raise up to the current pot size.
|
/// Pot-Limit — players may raise up to the current pot size.
|
||||||
#[serde(rename = "pot_limit")]
|
#[serde(rename = "pot_limit")]
|
||||||
|
|
@ -206,7 +209,7 @@ pub enum BetLimitConfig {
|
||||||
impl From<BetLimitConfig> for BetLimit {
|
impl From<BetLimitConfig> for BetLimit {
|
||||||
fn from(config: BetLimitConfig) -> Self {
|
fn from(config: BetLimitConfig) -> Self {
|
||||||
match config {
|
match config {
|
||||||
BetLimitConfig::NoLimit { min_raise } => BetLimit::NoLimit { min_raise },
|
BetLimitConfig::NoLimit { min_raise, max_raises } => BetLimit::NoLimit { min_raise, max_raises },
|
||||||
BetLimitConfig::PotLimit { min_raise } => BetLimit::PotLimit { min_raise },
|
BetLimitConfig::PotLimit { min_raise } => BetLimit::PotLimit { min_raise },
|
||||||
BetLimitConfig::FixedLimit { small_bet, big_bet } => {
|
BetLimitConfig::FixedLimit { small_bet, big_bet } => {
|
||||||
BetLimit::FixedLimit { small_bet, big_bet }
|
BetLimit::FixedLimit { small_bet, big_bet }
|
||||||
|
|
@ -218,7 +221,7 @@ impl From<BetLimitConfig> for BetLimit {
|
||||||
impl From<BetLimit> for BetLimitConfig {
|
impl From<BetLimit> for BetLimitConfig {
|
||||||
fn from(limit: BetLimit) -> Self {
|
fn from(limit: BetLimit) -> Self {
|
||||||
match limit {
|
match limit {
|
||||||
BetLimit::NoLimit { min_raise } => BetLimitConfig::NoLimit { min_raise },
|
BetLimit::NoLimit { min_raise, max_raises } => BetLimitConfig::NoLimit { min_raise, max_raises },
|
||||||
BetLimit::PotLimit { min_raise } => BetLimitConfig::PotLimit { min_raise },
|
BetLimit::PotLimit { min_raise } => BetLimitConfig::PotLimit { min_raise },
|
||||||
BetLimit::FixedLimit { small_bet, big_bet } => {
|
BetLimit::FixedLimit { small_bet, big_bet } => {
|
||||||
BetLimitConfig::FixedLimit { small_bet, big_bet }
|
BetLimitConfig::FixedLimit { small_bet, big_bet }
|
||||||
|
|
@ -411,6 +414,7 @@ impl Default for GameConfig {
|
||||||
num_seats: default_num_seats(),
|
num_seats: default_num_seats(),
|
||||||
bet_limit: BetLimitConfig::NoLimit {
|
bet_limit: BetLimitConfig::NoLimit {
|
||||||
min_raise: 20,
|
min_raise: 20,
|
||||||
|
max_raises: None,
|
||||||
},
|
},
|
||||||
small_blind: default_small_blind(),
|
small_blind: default_small_blind(),
|
||||||
big_blind: default_big_blind(),
|
big_blind: default_big_blind(),
|
||||||
|
|
@ -874,7 +878,7 @@ big_bet = 20
|
||||||
fn bet_limit_config_to_bet_limit() {
|
fn bet_limit_config_to_bet_limit() {
|
||||||
let config = BetLimitConfig::NoLimit { min_raise: 20 };
|
let config = BetLimitConfig::NoLimit { min_raise: 20 };
|
||||||
let limit: BetLimit = config.into();
|
let limit: BetLimit = config.into();
|
||||||
assert_eq!(limit, BetLimit::NoLimit { min_raise: 20 });
|
assert_eq!(limit, BetLimit::NoLimit { min_raise: 20, max_raises: None });
|
||||||
|
|
||||||
let config = BetLimitConfig::FixedLimit {
|
let config = BetLimitConfig::FixedLimit {
|
||||||
small_bet: 10,
|
small_bet: 10,
|
||||||
|
|
@ -1170,7 +1174,7 @@ min_raise = 50
|
||||||
assert_eq!(table_config.small_blind, 10);
|
assert_eq!(table_config.small_blind, 10);
|
||||||
assert_eq!(table_config.big_blind, 20);
|
assert_eq!(table_config.big_blind, 20);
|
||||||
assert_eq!(table_config.ante, 5);
|
assert_eq!(table_config.ante, 5);
|
||||||
assert_eq!(table_config.bet_limit, BetLimit::NoLimit { min_raise: 20 });
|
assert_eq!(table_config.bet_limit, BetLimit::NoLimit { min_raise: 20, max_raises: None });
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -559,6 +559,17 @@ impl HandRunner {
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
let street = self.current_street();
|
let street = self.current_street();
|
||||||
|
|
||||||
|
// Enforce raise cap for No-Limit with max_raises: convert Raise to Call.
|
||||||
|
let action = if let Action::Raise { .. } = &action {
|
||||||
|
if !self.bet_limit.can_raise(street, raises_this_round) {
|
||||||
|
Action::Call
|
||||||
|
} else {
|
||||||
|
action
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
action
|
||||||
|
};
|
||||||
|
|
||||||
validate_action(
|
validate_action(
|
||||||
&action,
|
&action,
|
||||||
next_id,
|
next_id,
|
||||||
|
|
@ -1128,6 +1139,20 @@ impl HandRunner {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4b. Reveal all hole cards for RL training (no-op for production bots).
|
||||||
|
{
|
||||||
|
let all_cards: Vec<(PlayerId, HoleCards)> = self
|
||||||
|
.seats
|
||||||
|
.iter()
|
||||||
|
.filter_map(|s| s.hole_cards.map(|hc| (s.player_id, hc)))
|
||||||
|
.collect();
|
||||||
|
let game_info = self.build_game_info();
|
||||||
|
for seat in &mut self.seats {
|
||||||
|
seat.player
|
||||||
|
.cards_revealed_event(&all_cards, &game_info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 5. Set state to Complete.
|
// 5. Set state to Complete.
|
||||||
self.state = HandState::Complete;
|
self.state = HandState::Complete;
|
||||||
|
|
||||||
|
|
@ -1643,7 +1668,7 @@ mod tests {
|
||||||
|
|
||||||
let seats = vec![seat0, seat1];
|
let seats = vec![seat0, seat1];
|
||||||
let deck = Deck::new();
|
let deck = Deck::new();
|
||||||
let bet_limit = BetLimit::NoLimit { min_raise: 10 };
|
let bet_limit = BetLimit::NoLimit { min_raise: 10, max_raises: None };
|
||||||
|
|
||||||
let mut runner = HandRunner::new(seats, 0, bet_limit, deck, 1);
|
let mut runner = HandRunner::new(seats, 0, bet_limit, deck, 1);
|
||||||
assert_eq!(runner.state(), HandState::Idle);
|
assert_eq!(runner.state(), HandState::Idle);
|
||||||
|
|
@ -1678,7 +1703,7 @@ mod tests {
|
||||||
|
|
||||||
let seats = vec![seat0, seat1];
|
let seats = vec![seat0, seat1];
|
||||||
let deck = Deck::new();
|
let deck = Deck::new();
|
||||||
let bet_limit = BetLimit::NoLimit { min_raise: 10 };
|
let bet_limit = BetLimit::NoLimit { min_raise: 10, max_raises: None };
|
||||||
|
|
||||||
let mut runner = HandRunner::new(seats, 0, bet_limit, deck, 1);
|
let mut runner = HandRunner::new(seats, 0, bet_limit, deck, 1);
|
||||||
runner.start(10, 20, 0);
|
runner.start(10, 20, 0);
|
||||||
|
|
@ -1714,7 +1739,7 @@ mod tests {
|
||||||
|
|
||||||
let seats = vec![seat0, seat1, seat2];
|
let seats = vec![seat0, seat1, seat2];
|
||||||
let deck = Deck::new();
|
let deck = Deck::new();
|
||||||
let bet_limit = BetLimit::NoLimit { min_raise: 10 };
|
let bet_limit = BetLimit::NoLimit { min_raise: 10, max_raises: None };
|
||||||
|
|
||||||
let mut runner = HandRunner::new(seats, 0, bet_limit, deck, 1);
|
let mut runner = HandRunner::new(seats, 0, bet_limit, deck, 1);
|
||||||
runner.start(10, 20, 0);
|
runner.start(10, 20, 0);
|
||||||
|
|
@ -1740,7 +1765,7 @@ mod tests {
|
||||||
|
|
||||||
let seats = vec![seat0, seat1];
|
let seats = vec![seat0, seat1];
|
||||||
let deck = Deck::new();
|
let deck = Deck::new();
|
||||||
let bet_limit = BetLimit::NoLimit { min_raise: 10 };
|
let bet_limit = BetLimit::NoLimit { min_raise: 10, max_raises: None };
|
||||||
|
|
||||||
let mut runner = HandRunner::new(seats, 0, bet_limit, deck, 1);
|
let mut runner = HandRunner::new(seats, 0, bet_limit, deck, 1);
|
||||||
runner.start(10, 20, 0);
|
runner.start(10, 20, 0);
|
||||||
|
|
@ -1762,7 +1787,7 @@ mod tests {
|
||||||
|
|
||||||
let seats = vec![seat0, seat1];
|
let seats = vec![seat0, seat1];
|
||||||
let deck = Deck::new();
|
let deck = Deck::new();
|
||||||
let bet_limit = BetLimit::NoLimit { min_raise: 10 };
|
let bet_limit = BetLimit::NoLimit { min_raise: 10, max_raises: None };
|
||||||
|
|
||||||
let mut runner = HandRunner::new(seats, 0, bet_limit, deck, 1);
|
let mut runner = HandRunner::new(seats, 0, bet_limit, deck, 1);
|
||||||
assert_eq!(runner.state(), HandState::Idle);
|
assert_eq!(runner.state(), HandState::Idle);
|
||||||
|
|
@ -1785,7 +1810,7 @@ mod tests {
|
||||||
|
|
||||||
let seats = vec![seat0, seat1];
|
let seats = vec![seat0, seat1];
|
||||||
let deck = Deck::new();
|
let deck = Deck::new();
|
||||||
let bet_limit = BetLimit::NoLimit { min_raise: 10 };
|
let bet_limit = BetLimit::NoLimit { min_raise: 10, max_raises: None };
|
||||||
|
|
||||||
let mut runner = HandRunner::new(seats, 0, bet_limit, deck, 1);
|
let mut runner = HandRunner::new(seats, 0, bet_limit, deck, 1);
|
||||||
let result = runner.step();
|
let result = runner.step();
|
||||||
|
|
@ -1805,7 +1830,7 @@ mod tests {
|
||||||
|
|
||||||
let seats = vec![seat0, seat1];
|
let seats = vec![seat0, seat1];
|
||||||
let deck = Deck::new();
|
let deck = Deck::new();
|
||||||
let bet_limit = BetLimit::NoLimit { min_raise: 10 };
|
let bet_limit = BetLimit::NoLimit { min_raise: 10, max_raises: None };
|
||||||
|
|
||||||
let mut runner = HandRunner::new(seats, 0, bet_limit, deck, 1);
|
let mut runner = HandRunner::new(seats, 0, bet_limit, deck, 1);
|
||||||
runner.start(10, 20, 0);
|
runner.start(10, 20, 0);
|
||||||
|
|
@ -1841,7 +1866,7 @@ mod tests {
|
||||||
|
|
||||||
let seats = vec![seat0, seat1, seat2];
|
let seats = vec![seat0, seat1, seat2];
|
||||||
let deck = Deck::new();
|
let deck = Deck::new();
|
||||||
let bet_limit = BetLimit::NoLimit { min_raise: 10 };
|
let bet_limit = BetLimit::NoLimit { min_raise: 10, max_raises: None };
|
||||||
|
|
||||||
let mut runner = HandRunner::new(seats, 0, bet_limit, deck, 1);
|
let mut runner = HandRunner::new(seats, 0, bet_limit, deck, 1);
|
||||||
runner.start(10, 20, 0);
|
runner.start(10, 20, 0);
|
||||||
|
|
@ -1957,7 +1982,7 @@ mod tests {
|
||||||
|
|
||||||
let seats = vec![seat0, seat1, seat2];
|
let seats = vec![seat0, seat1, seat2];
|
||||||
let deck = Deck::new();
|
let deck = Deck::new();
|
||||||
let bet_limit = BetLimit::NoLimit { min_raise: 10 };
|
let bet_limit = BetLimit::NoLimit { min_raise: 10, max_raises: None };
|
||||||
|
|
||||||
let mut runner = HandRunner::new(seats, 0, bet_limit, deck, 1);
|
let mut runner = HandRunner::new(seats, 0, bet_limit, deck, 1);
|
||||||
runner.start(10, 20, 0);
|
runner.start(10, 20, 0);
|
||||||
|
|
@ -1983,7 +2008,7 @@ mod tests {
|
||||||
|
|
||||||
let seats = vec![seat0, seat1];
|
let seats = vec![seat0, seat1];
|
||||||
let deck = Deck::new();
|
let deck = Deck::new();
|
||||||
let bet_limit = BetLimit::NoLimit { min_raise: 10 };
|
let bet_limit = BetLimit::NoLimit { min_raise: 10, max_raises: None };
|
||||||
|
|
||||||
let mut runner = HandRunner::new(seats, 0, bet_limit, deck, 1);
|
let mut runner = HandRunner::new(seats, 0, bet_limit, deck, 1);
|
||||||
runner.start(10, 20, 0);
|
runner.start(10, 20, 0);
|
||||||
|
|
|
||||||
|
|
@ -230,6 +230,24 @@ pub trait HoldemPlayer: Send + Sync {
|
||||||
/// Corresponds to `com.biotools.meerkat.Player#gameOverEvent`.
|
/// Corresponds to `com.biotools.meerkat.Player#gameOverEvent`.
|
||||||
fn game_over_event(&mut self, game_info: &GameInfo);
|
fn game_over_event(&mut self, game_info: &GameInfo);
|
||||||
|
|
||||||
|
/// Called after the hand ends with ALL players' hole cards revealed.
|
||||||
|
///
|
||||||
|
/// **Training only** — allows RL agents to compute realized equity for
|
||||||
|
/// every decision point. The default implementation is a no-op; production
|
||||||
|
/// bots should never override this (they don't see opponents' cards).
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `all_hole_cards` — Slice of `(PlayerId, HoleCards)` for every player
|
||||||
|
/// who was dealt into this hand, including those who folded.
|
||||||
|
/// * `game_info` — Final game state snapshot.
|
||||||
|
fn cards_revealed_event(
|
||||||
|
&mut self,
|
||||||
|
_all_hole_cards: &[(PlayerId, HoleCards)],
|
||||||
|
_game_info: &GameInfo,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
/// Signal an early-fold intent.
|
/// Signal an early-fold intent.
|
||||||
///
|
///
|
||||||
/// Called by the framework layer when a player wants to fold before it is
|
/// Called by the framework layer when a player wants to fold before it is
|
||||||
|
|
|
||||||
|
|
@ -129,8 +129,15 @@ pub fn run_cash_game(config: &GameConfig) -> Result<(SimulationStats, Table), Ga
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(GameError::NotEnoughPlayers(_)) => {
|
Err(GameError::NotEnoughPlayers(count)) => {
|
||||||
// Not enough active players to continue.
|
// Not enough active players to continue. This should only
|
||||||
|
// happen if reset_bankroll_each_hand / auto_rebuy are both
|
||||||
|
// disabled, or if seats were lost due to a hand error.
|
||||||
|
log::warn!(
|
||||||
|
"Simulation stopped after {} hands: only {count} active players. \
|
||||||
|
Set reset_bankroll_each_hand=true or auto_rebuy=true to prevent this.",
|
||||||
|
stats.total_hands
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Err(GameError::RaiseBelowMinimum(raise, min)) => {
|
Err(GameError::RaiseBelowMinimum(raise, min)) => {
|
||||||
|
|
|
||||||
|
|
@ -887,7 +887,7 @@ mod tests {
|
||||||
fn record_tournament_result_processes_completed_tournament() {
|
fn record_tournament_result_processes_completed_tournament() {
|
||||||
let config = TableConfig {
|
let config = TableConfig {
|
||||||
num_seats: 9,
|
num_seats: 9,
|
||||||
bet_limit: BetLimit::NoLimit { min_raise: 20 },
|
bet_limit: BetLimit::NoLimit { min_raise: 20, max_raises: None },
|
||||||
small_blind: 10,
|
small_blind: 10,
|
||||||
big_blind: 20,
|
big_blind: 20,
|
||||||
ante: 0,
|
ante: 0,
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ impl Default for TableConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
num_seats: 9,
|
num_seats: 9,
|
||||||
bet_limit: BetLimit::NoLimit { min_raise: 10 },
|
bet_limit: BetLimit::NoLimit { min_raise: 10, max_raises: None },
|
||||||
small_blind: 10,
|
small_blind: 10,
|
||||||
big_blind: 20,
|
big_blind: 20,
|
||||||
ante: 0,
|
ante: 0,
|
||||||
|
|
@ -411,7 +411,36 @@ impl Table {
|
||||||
.ok_or(GameError::NoHandInProgress)?;
|
.ok_or(GameError::NoHandInProgress)?;
|
||||||
|
|
||||||
// Run the hand to completion.
|
// Run the hand to completion.
|
||||||
let result = runner.run_to_completion()?;
|
let result = match runner.run_to_completion() {
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(e) => {
|
||||||
|
// CRITICAL: Restore seats even on error. Without this, the
|
||||||
|
// seats are trapped inside the dropped runner, leaving
|
||||||
|
// self.seats empty. The next start_hand() would see 0 active
|
||||||
|
// players, return NotEnoughPlayers, and silently terminate
|
||||||
|
// the entire simulation.
|
||||||
|
log::warn!("Hand execution failed ({e:?}) — restoring seats to table");
|
||||||
|
let seats = runner.into_seats();
|
||||||
|
for seat in seats {
|
||||||
|
let idx = seat.index as usize;
|
||||||
|
if idx < self.seats.len() {
|
||||||
|
let mut seat = seat;
|
||||||
|
// Refund wagered chips before resetting — the pot_manager
|
||||||
|
// is dropped with the runner, so wagered chips would
|
||||||
|
// vanish without this refund.
|
||||||
|
seat.stack += seat.wagered_this_hand;
|
||||||
|
if seat.stack == 0 {
|
||||||
|
seat.state = PlayerState::SittingOut;
|
||||||
|
} else {
|
||||||
|
seat.reset_for_new_hand();
|
||||||
|
}
|
||||||
|
self.seats[idx] = Some(seat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.current_hand = None;
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Extract seats from the runner and restore them to the table.
|
// Extract seats from the runner and restore them to the table.
|
||||||
// HandRunner::into_seats() consumes the runner and returns the seats
|
// HandRunner::into_seats() consumes the runner and returns the seats
|
||||||
|
|
@ -829,7 +858,7 @@ mod tests {
|
||||||
fn default_config() -> TableConfig {
|
fn default_config() -> TableConfig {
|
||||||
TableConfig {
|
TableConfig {
|
||||||
num_seats: 6,
|
num_seats: 6,
|
||||||
bet_limit: BetLimit::NoLimit { min_raise: 10 },
|
bet_limit: BetLimit::NoLimit { min_raise: 10, max_raises: None },
|
||||||
small_blind: 10,
|
small_blind: 10,
|
||||||
big_blind: 20,
|
big_blind: 20,
|
||||||
ante: 0,
|
ante: 0,
|
||||||
|
|
@ -1284,7 +1313,7 @@ mod tests {
|
||||||
fn test_table_full() {
|
fn test_table_full() {
|
||||||
let config = TableConfig {
|
let config = TableConfig {
|
||||||
num_seats: 2,
|
num_seats: 2,
|
||||||
bet_limit: BetLimit::NoLimit { min_raise: 10 },
|
bet_limit: BetLimit::NoLimit { min_raise: 10, max_raises: None },
|
||||||
small_blind: 10,
|
small_blind: 10,
|
||||||
big_blind: 20,
|
big_blind: 20,
|
||||||
ante: 0,
|
ante: 0,
|
||||||
|
|
|
||||||
|
|
@ -1006,7 +1006,7 @@ mod tests {
|
||||||
fn default_table_config() -> TableConfig {
|
fn default_table_config() -> TableConfig {
|
||||||
TableConfig {
|
TableConfig {
|
||||||
num_seats: 9,
|
num_seats: 9,
|
||||||
bet_limit: BetLimit::NoLimit { min_raise: 20 },
|
bet_limit: BetLimit::NoLimit { min_raise: 20, max_raises: None },
|
||||||
small_blind: 10,
|
small_blind: 10,
|
||||||
big_blind: 20,
|
big_blind: 20,
|
||||||
ante: 0,
|
ante: 0,
|
||||||
|
|
@ -1188,7 +1188,7 @@ mod tests {
|
||||||
fn test_sng_registration_fills_table() {
|
fn test_sng_registration_fills_table() {
|
||||||
let config = TableConfig {
|
let config = TableConfig {
|
||||||
num_seats: 4,
|
num_seats: 4,
|
||||||
bet_limit: BetLimit::NoLimit { min_raise: 20 },
|
bet_limit: BetLimit::NoLimit { min_raise: 20, max_raises: None },
|
||||||
small_blind: 10,
|
small_blind: 10,
|
||||||
big_blind: 20,
|
big_blind: 20,
|
||||||
ante: 0,
|
ante: 0,
|
||||||
|
|
@ -1217,7 +1217,7 @@ mod tests {
|
||||||
fn test_sng_run_single_hand() {
|
fn test_sng_run_single_hand() {
|
||||||
let config = TableConfig {
|
let config = TableConfig {
|
||||||
num_seats: 4,
|
num_seats: 4,
|
||||||
bet_limit: BetLimit::NoLimit { min_raise: 20 },
|
bet_limit: BetLimit::NoLimit { min_raise: 20, max_raises: None },
|
||||||
small_blind: 10,
|
small_blind: 10,
|
||||||
big_blind: 20,
|
big_blind: 20,
|
||||||
ante: 0,
|
ante: 0,
|
||||||
|
|
@ -1283,7 +1283,7 @@ mod tests {
|
||||||
fn test_mtt_table_balancing() {
|
fn test_mtt_table_balancing() {
|
||||||
let config = TableConfig {
|
let config = TableConfig {
|
||||||
num_seats: 9,
|
num_seats: 9,
|
||||||
bet_limit: BetLimit::NoLimit { min_raise: 20 },
|
bet_limit: BetLimit::NoLimit { min_raise: 20, max_raises: None },
|
||||||
small_blind: 10,
|
small_blind: 10,
|
||||||
big_blind: 20,
|
big_blind: 20,
|
||||||
ante: 0,
|
ante: 0,
|
||||||
|
|
@ -1328,7 +1328,7 @@ mod tests {
|
||||||
fn test_blind_advancement() {
|
fn test_blind_advancement() {
|
||||||
let config = TableConfig {
|
let config = TableConfig {
|
||||||
num_seats: 4,
|
num_seats: 4,
|
||||||
bet_limit: BetLimit::NoLimit { min_raise: 20 },
|
bet_limit: BetLimit::NoLimit { min_raise: 20, max_raises: None },
|
||||||
small_blind: 10,
|
small_blind: 10,
|
||||||
big_blind: 20,
|
big_blind: 20,
|
||||||
ante: 0,
|
ante: 0,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue