| from telltale.game.engine import HoldemEngine |
| from telltale.game.cards import Card |
| from telltale.game.holdem import ActionType, PlayerState |
| from telltale.poker.policy import board_texture |
| from telltale.poker import PokerPolicyConfig, PokerPolicyWriter, native |
|
|
|
|
| class FixedSolver: |
| def __init__(self, result: native.EquityResult): |
| self.result = result |
|
|
| def estimate_equity(self, *args, **kwargs): |
| return self.result |
|
|
|
|
| def make_players() -> list[PlayerState]: |
| players: list[PlayerState] = [] |
| for index in range(3): |
| players.append(PlayerState(f"p{index}", f"Player {index}", index, 100)) |
| return players |
|
|
|
|
| def equity(value: float) -> native.EquityResult: |
| wins = int(value * 100) |
| losses = 100 - wins |
| return native.EquityResult(wins, 0, losses, 100, value, 0.0, 1.0 - value) |
|
|
|
|
| def test_recommendation_contains_only_legal_actions(): |
| hand = HoldemEngine().start_hand(make_players(), seed=42, dealer_button_index=0) |
| writer = PokerPolicyWriter(FixedSolver(equity(0.5))) |
|
|
| decision = writer.choose_action(hand) |
|
|
| assert decision.action in hand.legal_actions() |
|
|
|
|
| def test_native_policy_probabilities_sum_to_one(): |
| features = native.DecisionFeatures( |
| street="flop", |
| hero_equity=0.5, |
| pot_size=100, |
| amount_to_call=30, |
| pot_odds=30 / 130, |
| stack_to_pot_ratio=2.0, |
| players_remaining=3, |
| can_check=False, |
| legal_actions=("fold", "call", "raise", "all_in"), |
| ) |
|
|
| recommendation = native.recommend_action(features) |
|
|
| assert abs(sum(recommendation.probabilities.values()) - 1.0) < 1e-12 |
| assert set(recommendation.probabilities) <= set(features.legal_actions) |
| assert recommendation.amount_options is not None |
| assert recommendation.board_texture == "dry" |
|
|
|
|
| def test_native_policy_returns_richer_advice_fields(): |
| features = native.DecisionFeatures( |
| street="turn", |
| hero_equity=0.76, |
| pot_size=120, |
| amount_to_call=0, |
| pot_odds=0.0, |
| stack_to_pot_ratio=2.0, |
| players_remaining=2, |
| can_check=True, |
| legal_actions=("check", "bet", "all_in"), |
| hero_stack=200, |
| minimum_raise_amount=20, |
| board_texture="two_tone", |
| street_action_count=1, |
| previous_aggression_count=0, |
| ) |
|
|
| recommendation = native.recommend_action(features) |
|
|
| assert recommendation.risk_label in {"low", "medium", "high"} |
| assert recommendation.board_texture == "two_tone" |
| assert recommendation.decision_margin >= 0 |
| assert recommendation.amount_options == { |
| "all_in": 200, |
| "large": 120, |
| "medium": 60, |
| "small": 40, |
| } |
| assert recommendation.hand_strength_bucket == "strong" |
| assert recommendation.spr_bucket == "medium" |
| assert recommendation.abstract_actions == ("check", "bet_33", "bet_66", "bet_100", "all_in") |
|
|
|
|
| def test_weak_equity_facing_bad_pot_odds_favors_fold(): |
| hand = HoldemEngine().start_hand(make_players(), seed=42, dealer_button_index=0) |
| writer = PokerPolicyWriter(FixedSolver(equity(0.05))) |
|
|
| decision = writer.choose_action(hand) |
|
|
| assert decision.action == ActionType.FOLD |
|
|
|
|
| def test_weak_equity_with_check_available_favors_check(): |
| hand = HoldemEngine().start_hand(make_players(), seed=42, dealer_button_index=0) |
| hand.apply_action(ActionType.CALL) |
| hand.apply_action(ActionType.CALL) |
| hand.apply_action(ActionType.CHECK) |
| writer = PokerPolicyWriter(FixedSolver(equity(0.12))) |
|
|
| decision = writer.choose_action(hand) |
|
|
| assert decision.action == ActionType.CHECK |
|
|
|
|
| def test_strong_equity_favors_bet_or_raise(): |
| hand = HoldemEngine().start_hand(make_players(), seed=42, dealer_button_index=0) |
| writer = PokerPolicyWriter(FixedSolver(equity(0.85)), PokerPolicyConfig(simulations=100)) |
|
|
| decision = writer.choose_action(hand) |
|
|
| assert decision.action == ActionType.RAISE |
|
|
|
|
| def test_suggested_bet_or_raise_amount_is_legal_when_present(): |
| hand = HoldemEngine().start_hand(make_players(), seed=42, dealer_button_index=0) |
| writer = PokerPolicyWriter(FixedSolver(equity(0.85)), PokerPolicyConfig(simulations=100)) |
|
|
| decision = writer.apply_action(hand) |
|
|
| assert decision.action == ActionType.RAISE |
| assert hand.action_history[-1].amount >= 20 |
|
|
|
|
| def test_explanation_includes_equity_and_pot_odds(): |
| hand = HoldemEngine().start_hand(make_players(), seed=42, dealer_button_index=0) |
| writer = PokerPolicyWriter(FixedSolver(equity(0.45))) |
|
|
| decision = writer.choose_action(hand) |
|
|
| assert "equity" in decision.reason |
| assert "pot odds" in decision.reason |
|
|
|
|
| def test_wet_board_reduces_strong_bet_probability(): |
| dry = native.DecisionFeatures( |
| street="flop", |
| hero_equity=0.68, |
| pot_size=100, |
| amount_to_call=0, |
| pot_odds=0.0, |
| stack_to_pot_ratio=2.0, |
| players_remaining=2, |
| can_check=True, |
| legal_actions=("check", "bet", "all_in"), |
| hero_stack=200, |
| minimum_raise_amount=10, |
| board_texture="dry", |
| ) |
| wet = native.DecisionFeatures( |
| street="flop", |
| hero_equity=0.68, |
| pot_size=100, |
| amount_to_call=0, |
| pot_odds=0.0, |
| stack_to_pot_ratio=2.0, |
| players_remaining=2, |
| can_check=True, |
| legal_actions=("check", "bet", "all_in"), |
| hero_stack=200, |
| minimum_raise_amount=10, |
| board_texture="wet", |
| ) |
|
|
| assert native.recommend_action(wet).probabilities["bet"] < native.recommend_action(dry).probabilities["bet"] |
|
|
|
|
| def test_multiway_pot_reduces_marginal_raise_probability(): |
| heads_up = native.DecisionFeatures( |
| street="turn", |
| hero_equity=0.74, |
| pot_size=100, |
| amount_to_call=20, |
| pot_odds=20 / 120, |
| stack_to_pot_ratio=2.0, |
| players_remaining=2, |
| can_check=False, |
| legal_actions=("fold", "call", "raise", "all_in"), |
| hero_stack=200, |
| minimum_raise_amount=20, |
| board_texture="dry", |
| ) |
| multiway = native.DecisionFeatures( |
| street="turn", |
| hero_equity=0.74, |
| pot_size=100, |
| amount_to_call=20, |
| pot_odds=20 / 120, |
| stack_to_pot_ratio=2.0, |
| players_remaining=4, |
| can_check=False, |
| legal_actions=("fold", "call", "raise", "all_in"), |
| hero_stack=200, |
| minimum_raise_amount=20, |
| board_texture="dry", |
| ) |
|
|
| assert native.recommend_action(multiway).probabilities["raise"] < native.recommend_action(heads_up).probabilities["raise"] |
|
|
|
|
| def test_low_spr_strong_equity_can_suggest_all_in(): |
| features = native.DecisionFeatures( |
| street="river", |
| hero_equity=0.9, |
| pot_size=200, |
| amount_to_call=0, |
| pot_odds=0.0, |
| stack_to_pot_ratio=0.8, |
| players_remaining=2, |
| can_check=True, |
| legal_actions=("check", "bet", "all_in"), |
| hero_stack=160, |
| minimum_raise_amount=20, |
| board_texture="dry", |
| ) |
|
|
| recommendation = native.recommend_action(features) |
|
|
| assert recommendation.probabilities["all_in"] > 0 |
| assert recommendation.risk_label == "high" |
|
|
|
|
| def test_suggested_amounts_are_clamped_to_stack_and_minimum_raise(): |
| features = native.DecisionFeatures( |
| street="flop", |
| hero_equity=0.8, |
| pot_size=300, |
| amount_to_call=40, |
| pot_odds=40 / 340, |
| stack_to_pot_ratio=0.25, |
| players_remaining=2, |
| can_check=False, |
| legal_actions=("fold", "call", "raise", "all_in"), |
| hero_stack=75, |
| minimum_raise_amount=30, |
| board_texture="dry", |
| ) |
|
|
| recommendation = native.recommend_action(features) |
|
|
| assert recommendation.suggested_amount <= 75 |
| assert recommendation.amount_options is not None |
| assert recommendation.amount_options["small"] == 75 |
| assert recommendation.amount_options["all_in"] == 75 |
|
|
|
|
| def test_board_texture_extraction(): |
| hand = HoldemEngine().start_hand(make_players(), seed=42, dealer_button_index=0) |
|
|
| hand.board_cards = [Card.parse("Ah"), Card.parse("7h"), Card.parse("2h")] |
| assert board_texture(hand) == "monotone" |
|
|
| hand.board_cards = [Card.parse("Ah"), Card.parse("Ad"), Card.parse("7c")] |
| assert board_texture(hand) == "paired" |
|
|
| hand.board_cards = [Card.parse("9h"), Card.parse("8d"), Card.parse("7h")] |
| assert board_texture(hand) == "wet" |
|
|
| hand.board_cards = [Card.parse("Kh"), Card.parse("8h"), Card.parse("2c")] |
| assert board_texture(hand) == "two_tone" |
|
|
| hand.board_cards = [Card.parse("Kh"), Card.parse("8d"), Card.parse("2c")] |
| assert board_texture(hand) == "dry" |
|
|
|
|
| def test_runtime_and_build_references_use_action_policy_name(): |
| checked_paths = [ |
| "telltale/native/poker_solver/bindings.cpp", |
| "telltale/native/poker_solver/CMakeLists.txt", |
| "telltale/poker/native.py", |
| ] |
|
|
| for path in checked_paths: |
| with open(path) as file: |
| assert "cfr_lite" not in file.read() |
|
|