@@ -350,6 +350,14 @@ impl PreflopChartActionGenerator {
350350 let player_stack = game_state. stacks [ player_idx] ;
351351 let to_call = current_bet - player_bet;
352352
353+ // When the raise cap has been reached, any Raise/3-bet/4-bet the
354+ // chart prescribes is illegal. Fall back to Call so the agent still
355+ // plays the hand the chart flagged as playable rather than folding
356+ // it — and, more importantly, so the validated action set is never
357+ // empty (which would panic the picker with "cannot sample empty
358+ // range").
359+ let raise_capped = game_state. is_raise_capped ( ) ;
360+
353361 match action {
354362 PreflopActionType :: Fold => {
355363 // If there's nothing to call, fold becomes check
@@ -361,6 +369,9 @@ impl PreflopChartActionGenerator {
361369 }
362370 PreflopActionType :: Call => Some ( AgentAction :: Call ) ,
363371 PreflopActionType :: Raise => {
372+ if raise_capped {
373+ return Some ( AgentAction :: Call ) ;
374+ }
364375 // Standard open raise: raise_size_bb * big_blind
365376 let raise_amount = self . config . preflop_config . raise_size_bb * big_blind;
366377
@@ -376,12 +387,18 @@ impl PreflopChartActionGenerator {
376387 }
377388 }
378389 PreflopActionType :: ThreeBet => {
390+ if raise_capped {
391+ return Some ( AgentAction :: Call ) ;
392+ }
379393 // 3-bet: multiply current bet by three_bet_multiplier
380394 let three_bet_amount =
381395 current_bet * self . config . preflop_config . three_bet_multiplier ;
382396 Some ( self . bet_or_all_in ( three_bet_amount, player_stack, player_bet) )
383397 }
384398 PreflopActionType :: FourBet => {
399+ if raise_capped {
400+ return Some ( AgentAction :: Call ) ;
401+ }
385402 // 4-bet: typically 2.5x the 3-bet
386403 let four_bet_amount = current_bet * 2.5 ;
387404 Some ( self . bet_or_all_in ( four_bet_amount, player_stack, player_bet) )
@@ -491,6 +508,92 @@ mod tests {
491508 )
492509 }
493510
511+ /// Regression: when the raise cap is reached preflop and the chart says
512+ /// Raise, the generator used to output a `Bet(three_bet_amount)` that
513+ /// `validate_actions` would then strip as a capped raise, leaving an
514+ /// empty action set and panicking the picker with "cannot sample empty
515+ /// range". A preflop chart agent must fall back to a non-raise action
516+ /// (Call or Fold) so the validated set is never empty.
517+ #[ test]
518+ fn test_chart_raise_when_raise_capped_falls_back_to_call ( ) {
519+ use crate :: arena:: cfr:: { ValidatorMode , validate_actions} ;
520+ use crate :: arena:: game_state:: { Round , RoundData } ;
521+ use crate :: core:: { Hand , PlayerBitSet } ;
522+
523+ // 6-handed preflop, three raises already in the pot.
524+ // Player 0 is to act; they hold AA so the chart entry says Raise.
525+ let num_players = 6 ;
526+ let big_blind = 5.0 ;
527+ let small_blind = 2.5 ;
528+
529+ // Round bets: P1 opens to 12.5, P3 3-bets to 37.5, P5 4-bets to 112.5.
530+ // Everyone else has 0 in this round (P0 is about to act).
531+ let round_player_bet = vec ! [ 0.0 , 12.5 , 0.0 , 37.5 , 0.0 , 112.5 ] ;
532+ // Starting stacks uniform.
533+ let stacks: Vec < f32 > = vec ! [ 500.0 ; num_players] ;
534+ // Carry round bets into total player_bet for accurate state.
535+ let player_bet = round_player_bet. clone ( ) ;
536+
537+ let mut round_data = RoundData :: new_with_bets (
538+ big_blind, // min raise
539+ PlayerBitSet :: new ( num_players) ,
540+ 0 , // P0 to act
541+ round_player_bet,
542+ ) ;
543+ // Force raise-capped state (default cap is 3).
544+ round_data. total_raise_count = 3 ;
545+
546+ let mut hands = vec ! [ Hand :: default ( ) ; num_players] ;
547+ hands[ 0 ] = Hand :: new_from_str ( "AsAh" ) . unwrap ( ) ;
548+
549+ let game_state = GameStateBuilder :: new ( )
550+ . round ( Round :: Preflop )
551+ . round_data ( round_data)
552+ . stacks ( stacks)
553+ . player_bet ( player_bet)
554+ . big_blind ( big_blind)
555+ . small_blind ( small_blind)
556+ . hands ( hands)
557+ . build ( )
558+ . unwrap ( ) ;
559+
560+ assert ! (
561+ game_state. is_raise_capped( ) ,
562+ "test setup: expected raise cap reached"
563+ ) ;
564+
565+ // Pure-Raise chart for AA, matching the experiment config shape.
566+ let mut chart = PreflopChart :: new ( ) ;
567+ let aa = PreflopHand :: new ( Value :: Ace , Value :: Ace , false ) ;
568+ chart. set ( aa, PreflopStrategy :: pure ( PreflopActionType :: Raise ) ) ;
569+ let config = PreflopChartActionConfig {
570+ preflop_config : PreflopChartConfig :: with_single_chart ( chart) ,
571+ postflop_config : ConfigurableActionConfig :: default ( ) ,
572+ } ;
573+
574+ let generator = create_generator ( & game_state, config) ;
575+ let raw = generator. gen_possible_actions ( & game_state) ;
576+ assert ! (
577+ !raw. is_empty( ) ,
578+ "generator must always produce at least one action"
579+ ) ;
580+
581+ let validated = validate_actions ( raw. clone ( ) , & game_state, ValidatorMode :: Standard ) ;
582+ assert ! (
583+ !validated. is_empty( ) ,
584+ "validated action set must not be empty (raw was {raw:?})"
585+ ) ;
586+
587+ // Intelligent fallback: calling should always be possible when
588+ // facing a raise-capped bet, so the agent can still see the flop.
589+ assert ! (
590+ validated
591+ . iter( )
592+ . any( |a| matches!( a, AgentAction :: Call | AgentAction :: Fold ) ) ,
593+ "expected Call or Fold in fallback action set, got {validated:?}"
594+ ) ;
595+ }
596+
494597 #[ test]
495598 fn test_preflop_actions_for_chart_hand ( ) {
496599 let game_state = create_test_game_state ( ) ;
0 commit comments