diff --git a/cc_py_bot_v1/commands.json b/cc_py_bot_v1/commands.json new file mode 100644 index 0000000..3f383ba --- /dev/null +++ b/cc_py_bot_v1/commands.json @@ -0,0 +1,4 @@ +{ + "build": [], + "run": ["python3", "player.py"] +} \ No newline at end of file diff --git a/cc_py_bot_v1/player.py b/cc_py_bot_v1/player.py new file mode 100644 index 0000000..bfee5e3 --- /dev/null +++ b/cc_py_bot_v1/player.py @@ -0,0 +1,123 @@ +''' +Not-So-Simple example pokerbot, written in Python. +''' +from skeleton.actions import FoldAction, CallAction, CheckAction, RaiseAction, DiscardAction +from skeleton.states import GameState, TerminalState, RoundState +from skeleton.states import NUM_ROUNDS, STARTING_STACK, BIG_BLIND, SMALL_BLIND +from skeleton.bot import Bot +from skeleton.runner import parse_args, run_bot + +import random + + +class Player(Bot): + ''' + A pokerbot. + ''' + + def __init__(self): + ''' + Called when a new game starts. Called exactly once. + + Arguments: + Nothing. + + Returns: + Nothing. + ''' + x = 3 + pass + + def handle_new_round(self, game_state, round_state, active): + ''' + Called when a new round starts. Called NUM_ROUNDS times. + + Arguments: + game_state: the GameState object. + round_state: the RoundState object. + active: your player's index. + + Returns: + Nothing. + ''' + my_bankroll = game_state.bankroll # the total number of chips you've gained or lost from the beginning of the game to the start of this round + # the total number of seconds your bot has left to play this game + game_clock = game_state.game_clock + round_num = game_state.round_num # the round number from 1 to NUM_ROUNDS + my_cards = round_state.hands[active] # your cards + big_blind = bool(active) # True if you are the big blind + pass + + def handle_round_over(self, game_state, terminal_state, active): + ''' + Called when a round ends. Called NUM_ROUNDS times. + + Arguments: + game_state: the GameState object. + terminal_state: the TerminalState object. + active: your player's index. + + Returns: + Nothing. + ''' + my_delta = terminal_state.deltas[active] # your bankroll change from this round + previous_state = terminal_state.previous_state # RoundState before payoffs + street = previous_state.street # 0,2,3,4,5,6 representing when this round ended + my_cards = previous_state.hands[active] # your cards + # opponent's cards or [] if not revealed + opp_cards = previous_state.hands[1-active] + pass + + def get_action(self, game_state, round_state, active): + ''' + Where the magic happens - your code should implement this function. + Called any time the engine needs an action from your bot. + + Arguments: + game_state: the GameState object. + round_state: the RoundState object. + active: your player's index. + + Returns: + Your action. + ''' + legal_actions = round_state.legal_actions() # the actions you are allowed to take + # 0, 3, 4, or 5 representing pre-flop, flop, turn, or river respectively + street = round_state.street + my_cards = round_state.hands[active] # your cards + board_cards = round_state.board # the board cards + # the number of chips you have contributed to the pot this round of betting + my_pip = round_state.pips[active] + # the number of chips your opponent has contributed to the pot this round of betting + opp_pip = round_state.pips[1-active] + # the number of chips you have remaining + my_stack = round_state.stacks[active] + # the number of chips your opponent has remaining + opp_stack = round_state.stacks[1-active] + continue_cost = opp_pip - my_pip # the number of chips needed to stay in the pot + # the number of chips you have contributed to the pot + my_contribution = STARTING_STACK - my_stack + # the number of chips your opponent has contributed to the pot + opp_contribution = STARTING_STACK - opp_stack + + # Only use DiscardAction if it's in legal_actions (which already checks street) + # legal_actions() returns DiscardAction only when street is 2 or 3 + if DiscardAction in legal_actions: + # Always discards the first card in the bot's hand + return DiscardAction(0) + if RaiseAction in legal_actions: + # the smallest and largest numbers of chips for a legal bet/raise + min_raise, max_raise = round_state.raise_bounds() + min_cost = min_raise - my_pip # the cost of a minimum bet/raise + max_cost = max_raise - my_pip # the cost of a maximum bet/raise + if random.random() < 0.5: + return RaiseAction(min_raise) + if CheckAction in legal_actions: # check-call + return CheckAction() + if random.random() < 0.25: + return FoldAction() + return CallAction() + + +if __name__ == '__main__': + run_bot(Player(), parse_args()) diff --git a/cc_py_bot_v1/skeleton/actions.py b/cc_py_bot_v1/skeleton/actions.py new file mode 100644 index 0000000..81176b0 --- /dev/null +++ b/cc_py_bot_v1/skeleton/actions.py @@ -0,0 +1,11 @@ +''' +The actions that the player is allowed to take. +''' +from collections import namedtuple + +FoldAction = namedtuple('FoldAction', []) +CallAction = namedtuple('CallAction', []) +CheckAction = namedtuple('CheckAction', []) +DiscardAction = namedtuple('DiscardAction', ['card'])## Card should be the index of the card in your hand [0,1,2] +# we coalesce BetAction and RaiseAction for convenience +RaiseAction = namedtuple('RaiseAction', ['amount']) diff --git a/cc_py_bot_v1/skeleton/bot.py b/cc_py_bot_v1/skeleton/bot.py new file mode 100644 index 0000000..4a45fae --- /dev/null +++ b/cc_py_bot_v1/skeleton/bot.py @@ -0,0 +1,52 @@ +''' +This file contains the base class that you should implement for your pokerbot. +''' + + +class Bot(): + ''' + The base class for a pokerbot. + ''' + + def handle_new_round(self, game_state, round_state, active): + ''' + Called when a new round starts. Called NUM_ROUNDS times. + + Arguments: + game_state: the GameState object. + round_state: the RoundState object. + active: your player's index. + + Returns: + Nothing. + ''' + raise NotImplementedError('handle_new_round') + + def handle_round_over(self, game_state, terminal_state, active): + ''' + Called when a round ends. Called NUM_ROUNDS times. + + Arguments: + game_state: the GameState object. + terminal_state: the TerminalState object. + active: your player's index. + + Returns: + Nothing. + ''' + raise NotImplementedError('handle_round_over') + + def get_action(self, game_state, round_state, active): + ''' + Where the magic happens - your code should implement this function. + Called any time the engine needs an action from your bot. + + Arguments: + game_state: the GameState object. + round_state: the RoundState object. + active: your player's index. + + Returns: + Your action. + ''' + raise NotImplementedError('get_action') diff --git a/cc_py_bot_v1/skeleton/runner.py b/cc_py_bot_v1/skeleton/runner.py new file mode 100644 index 0000000..406dcbd --- /dev/null +++ b/cc_py_bot_v1/skeleton/runner.py @@ -0,0 +1,147 @@ +''' +The infrastructure for interacting with the engine. +''' +import argparse +import socket +from .actions import FoldAction, CallAction, CheckAction, RaiseAction, DiscardAction +from .states import GameState, TerminalState, RoundState +from .states import STARTING_STACK, BIG_BLIND, SMALL_BLIND +from .bot import Bot + + +class Runner(): + ''' + Interacts with the engine. + ''' + + def __init__(self, pokerbot, socketfile): + self.pokerbot = pokerbot + self.socketfile = socketfile + + def receive(self): + ''' + Generator for incoming messages from the engine. + ''' + while True: + packet = self.socketfile.readline().strip().split(' ') + if not packet: + break + yield packet + + def send(self, action): + ''' + Encodes an action and sends it to the engine. + ''' + if isinstance(action, FoldAction): + code = 'F' + elif isinstance(action, CallAction): + code = 'C' + elif isinstance(action, CheckAction): + code = 'K' + elif isinstance(action, DiscardAction): + code = 'D' + str(action.card)## action.card is the index of the action card in the player's hand + else: # isinstance(action, RaiseAction) + code = 'R' + str(action.amount) + self.socketfile.write(code + '\n') + self.socketfile.flush() + + def run(self): + ''' + Reconstructs the game tree based on the action history received from the engine. + ''' + game_state = GameState(0, 0., 1) + round_state = None + active = 0 + round_flag = True + for packet in self.receive(): + for clause in packet: + if clause[0] == 'T': + game_state = GameState(game_state.bankroll, float(clause[1:]), game_state.round_num) + elif clause[0] == 'P': + active = int(float(clause[1:])) + elif clause[0] == 'H': + hands = [[], []] + + hands[active] = clause[1:].split(',') + pips = [SMALL_BLIND, BIG_BLIND] + stacks = [STARTING_STACK - SMALL_BLIND, STARTING_STACK - BIG_BLIND] + round_state = RoundState(0, 0, pips, stacks, hands, [], None) + elif clause[0] == 'G': + # 'G' clause indicates game/round start - just update the round_state without changing values + round_state = RoundState(round_state.button, round_state.street, round_state.pips, round_state.stacks, + round_state.hands, round_state.board, round_state.previous_state) + if round_flag: + self.pokerbot.handle_new_round(game_state, round_state, active) + round_flag = False + elif clause[0] == 'F': + round_state = round_state.proceed(FoldAction()) + elif clause[0] == 'C': + round_state = round_state.proceed(CallAction()) + elif clause[0] == 'K': + round_state = round_state.proceed(CheckAction()) + elif clause[0] == 'D': + if isinstance(round_state, RoundState): + round_state = round_state.proceed(DiscardAction(int(clause[1:]))) + else: + pass + elif clause[0] == 'R': + round_state = round_state.proceed(RaiseAction(int(float(clause[1:])))) + elif clause[0] == 'B': + # 'B' clause contains the board cards for the current street + # The street should already be correct from previous proceed() calls + # Just update the board with the cards from the engine + board_cards = clause[1:].split(',') if len(clause) > 1 else [] + round_state = RoundState(round_state.button, round_state.street, round_state.pips, round_state.stacks, + round_state.hands, board_cards, round_state.previous_state) + elif clause[0] == 'O': + # backtrack + round_state = round_state.previous_state + revised_hands = list(round_state.hands) + revised_hands[1-active] = clause[1:].split(',') + # rebuild history + round_state = RoundState(round_state.button, round_state.street, round_state.pips, round_state.stacks, + revised_hands, round_state.board, round_state.previous_state) + round_state = TerminalState([0, 0], round_state) + elif clause[0] == 'A': + assert isinstance(round_state, TerminalState) + delta = int(float(clause[1:])) + deltas = [-delta, -delta] + deltas[active] = delta + round_state = TerminalState(deltas, round_state.previous_state) + self.pokerbot.handle_round_over(game_state, round_state, active) + game_state = GameState(game_state.bankroll + delta, game_state.game_clock, game_state.round_num) + round_flag = True + elif clause[0] == 'Q': + return + if round_flag or isinstance(round_state, TerminalState): # ack the engine + self.send(CheckAction()) + else: + ##assert active == round_state.button % 2 + action = self.pokerbot.get_action(game_state, round_state, active) + self.send(action) + + +def parse_args(): + ''' + Parses arguments corresponding to socket connection information. + ''' + parser = argparse.ArgumentParser(prog='python3 player.py') + parser.add_argument('--host', type=str, default='localhost', help='Host to connect to, defaults to localhost') + parser.add_argument('port', type=int, help='Port on host to connect to') + return parser.parse_args() + +def run_bot(pokerbot, args): + ''' + Runs the pokerbot. + ''' + assert isinstance(pokerbot, Bot) + try: + sock = socket.create_connection((args.host, args.port)) + except OSError: + print('Could not connect to {}:{}'.format(args.host, args.port)) + return + socketfile = sock.makefile('rw') + runner = Runner(pokerbot, socketfile) + runner.run() + socketfile.close() + sock.close() diff --git a/cc_py_bot_v1/skeleton/states.py b/cc_py_bot_v1/skeleton/states.py new file mode 100644 index 0000000..03aaefd --- /dev/null +++ b/cc_py_bot_v1/skeleton/states.py @@ -0,0 +1,131 @@ +''' +Encapsulates game and round state information for the player. +''' +from collections import namedtuple +from .actions import FoldAction, CallAction, CheckAction, RaiseAction, DiscardAction + +GameState = namedtuple('GameState', ['bankroll', 'game_clock', 'round_num']) +TerminalState = namedtuple('TerminalState', ['deltas', 'previous_state']) + +NUM_ROUNDS = 1000 +STARTING_STACK = 400 +BIG_BLIND = 2 +SMALL_BLIND = 1 + + +class RoundState(namedtuple('_RoundState', ['button', 'street', 'pips', 'stacks', 'hands', 'board', 'previous_state'])): + ''' + Encodes the game tree for one round of poker. + ''' + def showdown(self): + ''' + Compares the players' hands and computes payoffs. + ''' + return TerminalState([0, 0], self) + + def legal_actions(self): + ''' + Returns a set which corresponds to the active player's legal moves. + ''' + active = self.button % 2 + continue_cost = self.pips[1-active] - self.pips[active] + if self.street in (2, 3): + return {DiscardAction} if active != self.street % 2 else {CheckAction} + if continue_cost == 0: + # we can only raise the stakes if both players can afford it + bets_forbidden = (self.stacks[0] == 0 or self.stacks[1] == 0) + return {CheckAction, FoldAction} if bets_forbidden else {CheckAction, RaiseAction, FoldAction} + # continue_cost > 0 + # similarly, re-raising is only allowed if both players can afford it + raises_forbidden = (continue_cost == self.stacks[active] or self.stacks[1-active] == 0) + return {FoldAction, CallAction} if raises_forbidden else {FoldAction, CallAction, RaiseAction} + + def raise_bounds(self): + ''' + Returns a tuple of the minimum and maximum legal raises. + ''' + active = self.button % 2 + continue_cost = self.pips[1-active] - self.pips[active] + max_contribution = min(self.stacks[active], self.stacks[1-active] + continue_cost) + min_contribution = min(max_contribution, continue_cost + max(continue_cost, BIG_BLIND)) + return (self.pips[active] + min_contribution, self.pips[active] + max_contribution) + + def proceed_street(self): + ''' + Resets the players' pips and advances the game tree to the next round of betting and updates the board state. + + possible streets: 0, 2, 3, 4, 5, 6 + ''' + ### Put the board as peek deck of the street number and make sure board includes this peek + the players discarded cards after state + if self.street == 6: + return self.showdown() + elif self.street == 0: + new_street = 2 + button = 1 ### Player B discards first, since they are out of position + elif self.street == 2: + new_street = 3 + button = 0 ### Player A discards second + else: + new_street = self.street + 1 + button = 1 ### Player B acts first after the discard phase + + return RoundState(button, new_street, [0, 0], self.stacks, self.hands, self.board, self) + + + def proceed(self, action): + ''' + Advances the game tree by one action performed by the active player. + + Args: + action: The action being performed. Must be one of: + - DiscardAction: Player discards a card from their hand and adds it to the board + - FoldAction: Player forfeits the hand + - CallAction: Player matches the current bet + - CheckAction: Player passes when no bet to match + - RaiseAction: Player increases the current bet + + Returns: + Either: + - RoundState: The new state after the action is performed + - TerminalState: If the action ends the hand (e.g., fold or final call) + + Note: + The button value is incremented after each action to track whose turn it is. + For DiscardAction, the card is added to the board and the hand is updated. Also, advances to the next street. + For FoldAction, the inactive player is awarded the pot. + For CallAction on button 0, both players post blinds. + For CheckAction, advances to next street if both players have acted. + For RaiseAction, updates pips and stacks based on raise amount. + ''' + active = self.button % 2 + if isinstance(action, DiscardAction): + if len(self.hands[active]) != 0: + self.board.append(self.hands[active].pop(action.card)) + state = RoundState((1 - active) % 2, self.street, self.pips, self.stacks, self.hands, self.board, self) + return state + if isinstance(action, FoldAction): + delta = self.stacks[0] - STARTING_STACK if active == 0 else STARTING_STACK - self.stacks[1] + return TerminalState([delta, -delta], self) + if isinstance(action, CallAction): + if self.button == 0: # sb calls bb + return RoundState(1, 0, [BIG_BLIND] * 2, [STARTING_STACK - BIG_BLIND] * 2, self.hands, self.board,self) + # both players acted + new_pips = list(self.pips) + new_stacks = list(self.stacks) + contribution = new_pips[1-active] - new_pips[active] + new_stacks[active] -= contribution + new_pips[active] += contribution + state = RoundState(self.button + 1, self.street, new_pips, new_stacks, self.hands, self.board, self) + return state.proceed_street() + if isinstance(action, CheckAction): + if (self.street == 0 and self.button > 0) or self.button > 1 or self.street == 2 or self.street == 3: # both players acted + return self.proceed_street() + # let opponent act + return RoundState(self.button + 1, self.street, self.pips, self.stacks, self.hands, self.board, self) + # isinstance(action, RaiseAction) + new_pips = list(self.pips) + new_stacks = list(self.stacks) + contribution = action.amount - new_pips[active] + new_stacks[active] -= contribution + new_pips[active] += contribution + return RoundState(self.button + 1, self.street, new_pips, new_stacks, self.hands, self.board, self) \ No newline at end of file diff --git a/gamelog_analyzer.py b/gamelog_analyzer.py new file mode 100644 index 0000000..0a30cea --- /dev/null +++ b/gamelog_analyzer.py @@ -0,0 +1,77 @@ +import matplotlib.pyplot as plt +from statistics import mean, stdev + +def AnalyzeGame(): + ''' Assume config file format remains the same. ''' + with open('config.py', 'r') as f: + while True: + line = f.readline() + if 'GAME_LOG_FILENAME' in line: + log_file_name = line.split('"')[-2] + '.txt' + if 'PLAYER_1_NAME' in line: + P1 = line.split('"')[-2] + if 'PLAYER_2_NAME' in line: + P2 = line.split('"')[-2] + if line == '': + break + + with open(log_file_name, 'r') as f: + log = f.read() + + log_game, log_final = log.split('Final')[0], log.split('Final')[1] + log_rounds = log_game.split('#')[1:] + + P1_bankroll, P1_PnL = [0], [] + P2_bankroll, P2_PnL = [0], [] + + for round_x in log_rounds: + for line_x in round_x.split('\n'): + if 'awarded' in line_x: + if P1 in line_x: + p1_pnl = int(line_x.split('awarded ')[-1]) + if P2 in line_x: + p2_pnl = int(line_x.split('awarded ')[-1]) + + P1_bankroll.append(P1_bankroll[-1] + p1_pnl) + P1_PnL.append(p1_pnl) + + P2_bankroll.append(P2_bankroll[-1] + p2_pnl) + P2_PnL.append(p2_pnl) + + + mean_win_p1 = mean(P1_PnL) + std_win_p1 = stdev(P1_PnL) + sr1 = mean_win_p1/std_win_p1 + + print(f'\n---- {P1} ----') + print(f'Mean PnL per Round: {mean_win_p1:.3f}') + print(f'Std PnL per Round: {std_win_p1:.3f}') + print(f'PnL Sharpe Ratio: {sr1:.3f}') + + mean_win_p2 = mean(P2_PnL) + std_win_p2 = stdev(P2_PnL) + sr2 = mean_win_p2/std_win_p2 + + print(f'\n---- {P2} ----') + print(f'Mean PnL per Round: {mean_win_p2:.3f}') + print(f'Std PnL per Round: {std_win_p2:.3f}') + print(f'PnL Sharpe Ratio: {sr2:.3f}') + + fig1 = plt.figure(figsize=(16, 9)) + plt.plot(P1_bankroll) + plt.title(f'{P1} Bankroll') + plt.grid() + plt.show() + plt.close() + + fig2 = plt.figure(figsize=(16, 9)) + plt.plot(P2_bankroll) + plt.title(f'{P2} Bankroll') + plt.grid() + plt.show() + plt.close() + + print('\n----------------') + + return P1_bankroll, P1_PnL, P2_bankroll, P2_PnL + \ No newline at end of file diff --git a/mit-poker-2026-master 2.code-workspace b/mit-poker-2026-master 2.code-workspace new file mode 100644 index 0000000..c295711 --- /dev/null +++ b/mit-poker-2026-master 2.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": "../Downloads/mit-poker-2026-master 2" + } + ], + "settings": {} +} \ No newline at end of file diff --git a/player.py b/player.py new file mode 100644 index 0000000..b428e61 --- /dev/null +++ b/player.py @@ -0,0 +1,691 @@ +''' +Poker Bot v4.0 +Core Strategies: +1. Winning Probability (Outs × 4% / 2%) +2. Positional Advantage + Top20% Aggression +3. Opponent Modeling + Exploitation +4. Bankroll Protection +''' + +from skeleton.actions import FoldAction, CallAction, CheckAction, RaiseAction, DiscardAction +from skeleton.states import GameState, TerminalState, RoundState +from skeleton.states import NUM_ROUNDS, STARTING_STACK, BIG_BLIND, SMALL_BLIND +from skeleton.bot import Bot +from skeleton.runner import parse_args, run_bot +import random +from collections import deque + + +class PokerMath: + @staticmethod + def count_outs(my_cards, board_cards, RANK_MAP): + """Calculate Outs""" + all_cards = list(my_cards) + list(board_cards) + all_ranks = [RANK_MAP[c[0]] for c in all_cards] + all_suits = [c[1] for c in all_cards] + my_ranks = [RANK_MAP[c[0]] for c in my_cards] + my_suits = [c[1] for c in my_cards] + outs = 0 + draw_type = None + + # Flush Draw + suit_counts = {'s': 0, 'h': 0, 'd': 0, 'c': 0} + for s in all_suits: + suit_counts[s] += 1 + max_suit_count = max(suit_counts.values()) + + if max_suit_count == 4: + outs += 9 + draw_type = 'flush' + + # Straight Draw + unique_ranks = sorted(set(all_ranks)) + if 14 in unique_ranks: + unique_ranks_with_ace = [1] + unique_ranks + else: + unique_ranks_with_ace = unique_ranks + + max_straight_draw = 0 + for base in unique_ranks_with_ace: + window = [x for x in unique_ranks_with_ace if base <= x < base + 5] + if len(window) >= 4: + max_straight_draw = max(max_straight_draw, len(window)) + + if max_straight_draw == 4: + if outs == 0: + outs += 8 + draw_type = 'straight' + else: + outs += 6 + draw_type = 'combo' + + # Pair to Trips + rank_counts = {} + for r in my_ranks: + rank_counts[r] = rank_counts.get(r, 0) + 1 + + if any(count == 2 for count in rank_counts.values()): + pair_rank = [r for r, c in rank_counts.items() if c == 2][0] + if outs == 0: + outs += 2 + draw_type = 'pair_to_trips' + + #Two Pair to Full House + board_rank_counts = {} + for r in [RANK_MAP[c[0]] for c in board_cards]: + board_rank_counts[r] = board_rank_counts.get(r, 0) + 1 + all_rank_counts = {} + for r in all_ranks: + all_rank_counts[r] = all_rank_counts.get(r, 0) + 1 + + num_pairs = sum(1 for c in all_rank_counts.values() if c == 2) + if num_pairs >= 2: + if outs == 0: + outs += 4 + draw_type = 'two_pair_to_fh' + + return outs, draw_type + + @staticmethod + def calculate_equity(outs, streets_remaining): + "calculate winning equity based on outs and remaining streets" + if streets_remaining >= 2: + equity = min(outs * 4, 100) / 100.0 + elif streets_remaining == 1: + equity = min(outs * 2, 100) / 100.0 + else: + equity = 0.0 + + return equity + + @staticmethod + def calculate_pot_odds(continue_cost, pot_total): + if continue_cost == 0: + return 0.0 + + # investment need vs possible total pot after call + pot_odds = continue_cost / (pot_total + continue_cost) + return pot_odds + + @staticmethod + def calculate_ev(equity, continue_cost, pot_total): + """EV""" + total_pot_if_win = pot_total + continue_cost + ev = (equity * total_pot_if_win) - ((1 - equity) * continue_cost) + return ev + + @staticmethod + def should_call_mathematically(equity, pot_odds): + """数学上是否应该call""" + required_equity = pot_odds + return equity > required_equity + + +class OpponentModel: + def __init__(self, window_size=60): + self.window_size = window_size + self.vpip_history = deque(maxlen=window_size) + self.pfr_history = deque(maxlen=window_size) + self.fold_to_raise_history = deque(maxlen=window_size) + self.postflop_aggression = deque(maxlen=window_size) + self.opponent_type = 'unknown' + self.confidence = 0.0 + + def record_preflop_action(self, action_type, is_voluntary): + if is_voluntary: + self.vpip_history.append(action_type in ['call', 'raise']) + self.pfr_history.append(action_type == 'raise') + + def record_postflop_action(self, action_type, faced_bet): + if faced_bet: + self.fold_to_raise_history.append(action_type == 'fold') + self.postflop_aggression.append(action_type == 'raise') + + def end_hand(self): + if len(self.vpip_history) < 10: + self.opponent_type = 'unknown' + self.confidence = 0.0 + return + + vpip = sum(self.vpip_history) / len(self.vpip_history) + pfr = sum(self.pfr_history) / len(self.pfr_history) if self.pfr_history else 0 + fold_to_raise = (sum(self.fold_to_raise_history) / len(self.fold_to_raise_history) + if self.fold_to_raise_history else 0.5) + + if vpip > 0.70: + self.opponent_type = 'FISH' if pfr < 0.50 else 'LAG' + elif vpip < 0.25: + self.opponent_type = 'NITY' + elif pfr > 0.40: + self.opponent_type = 'TAG' + else: + self.opponent_type = 'BALANCED' + + if fold_to_raise > 0.75: + self.opponent_type += '_WEAK' + elif fold_to_raise < 0.20: + self.opponent_type += '_STICKY' + + self.confidence = min(1.0, len(self.vpip_history) / self.window_size) + + def get_exploit_adjustments(self): + if self.confidence < 0.25: + return None + + opp = self.opponent_type + adjustments = { + 'bluff_more': False, + 'value_bet_bigger': False, + 'call_lighter': False, + 'fold_more': False + } + + if 'FISH' in opp: + adjustments['value_bet_bigger'] = True + adjustments['fold_more'] = True + elif 'NITY' in opp or 'WEAK' in opp: + adjustments['bluff_more'] = True + elif 'LAG' in opp: + adjustments['call_lighter'] = True + + if 'STICKY' in opp: + adjustments['bluff_more'] = False + + return adjustments + + def get_stats(self): + if not self.vpip_history: + return None + + return { + 'vpip': sum(self.vpip_history) / len(self.vpip_history), + 'pfr': sum(self.pfr_history) / len(self.pfr_history) if self.pfr_history else 0, + 'fold_to_raise': (sum(self.fold_to_raise_history) / len(self.fold_to_raise_history) + if self.fold_to_raise_history else 0.5), + 'type': self.opponent_type, + 'confidence': self.confidence + } + +class Player(Bot): + def __init__(self): + self.RANK_MAP = { + "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9, + "T": 10, "J": 11, "Q": 12, "K": 13, "A": 14 + } + + self.poker_math = PokerMath() + self.opponent_model = OpponentModel(window_size=60) + self.hands_played = 0 + self.exploit_threshold = 10 + + # Bankroll protection + self.LOCK_THRESHOLD = 150 + self.SECURE_THRESHOLD = 300 + + print("✓ EV-Based Bot + Probability + Exploitation + Position") + + def handle_new_round(self, game_state, round_state, active): + self.hands_played += 1 + self.current_bankroll = game_state.bankroll + + if self.hands_played % 50 == 0: + stats = self.opponent_model.get_stats() + print(f"\n=== Hand {self.hands_played} | Bankroll: {self.current_bankroll} ===") + if stats: + print(f"对手: {stats['type']} (置信: {stats['confidence']:.2f})") + + def handle_round_over(self, game_state, terminal_state, active): + self.opponent_model.end_hand() + + def get_action(self, game_state, round_state, active): + legal_actions = round_state.legal_actions() + street = round_state.street + my_cards = round_state.hands[active] + board_cards = round_state.board + my_pip = round_state.pips[active] + opp_pip = round_state.pips[1-active] + continue_cost = opp_pip - my_pip + pot_total = my_pip + opp_pip + + # Bankroll Protection + bankroll = game_state.bankroll + remaining_rounds = NUM_ROUNDS - game_state.round_num + + if remaining_rounds < 100 and bankroll >= self.SECURE_THRESHOLD: + if FoldAction in legal_actions: + return FoldAction() + elif CheckAction in legal_actions: + return CheckAction() + + if bankroll >= self.LOCK_THRESHOLD: + if street == 0: + strength = self._evaluate_preflop_strength(my_cards) + if strength < 0.75 and FoldAction in legal_actions: + return FoldAction() + + # DISCARD + if DiscardAction in legal_actions: + return self._handle_discard(my_cards, board_cards, active) + + if len(legal_actions) == 1: + if CheckAction in legal_actions: + return CheckAction() + elif CallAction in legal_actions: + return CallAction() + else: + return list(legal_actions)[0]() + + # PREFLOP + if street == 0: + return self._handle_preflop(round_state, active, my_cards, continue_cost, pot_total) + + # POSTFLOP + return self._handle_postflop_ev(round_state, active, my_cards, board_cards, + continue_cost, pot_total, street) + + def _handle_preflop(self, round_state, active, my_cards, continue_cost, pot_total): + + hand_ranks = [self.RANK_MAP[c[0]] for c in my_cards] + hand_suits = [c[1] for c in my_cards] + ranks_sorted = sorted(hand_ranks, reverse=True) + + strength = self._calculate_preflop_strength(hand_ranks, hand_suits, ranks_sorted) + + is_top20 = strength >= 0.60 + in_position = (active == 0) + + exploit_adjustments = None + if self.hands_played >= self.exploit_threshold: + exploit_adjustments = self.opponent_model.get_exploit_adjustments() + + legal_actions = round_state.legal_actions() + + if continue_cost > 0: + if is_top20: + # Top 20% - aggressive + if RaiseAction in legal_actions and random.random() < 0.75: + min_raise, max_raise = round_state.raise_bounds() + size_factor = 0.6 + (strength - 0.60) * 0.8 + target = int(min_raise + (max_raise - min_raise) * size_factor * 0.5) + target = max(min_raise, min(target, max_raise)) + return RaiseAction(target) + if CallAction in legal_actions: + return CallAction() + return CheckAction() if CheckAction in legal_actions else FoldAction() + + else: + # Not Top 20% - cautious + if not in_position: + # BB - conservative + if strength >= 0.40: + pot_odds = continue_cost / (pot_total + continue_cost) if pot_total > 0 else 0 + if pot_odds <= 0.35 and CallAction in legal_actions: + return CallAction() + + if FoldAction in legal_actions: + return FoldAction() + return CheckAction() if CheckAction in legal_actions else CallAction() + + else: + # Dealer - random bluff + if random.random() < 0.05 and RaiseAction in legal_actions: + min_raise, max_raise = round_state.raise_bounds() + return RaiseAction(min_raise) + + if strength >= 0.35 and CallAction in legal_actions: + return CallAction() + + if FoldAction in legal_actions: + return FoldAction() + return CheckAction() if CheckAction in legal_actions else CallAction() + + else: + if is_top20: + if RaiseAction in legal_actions and random.random() < 0.90: + min_raise, max_raise = round_state.raise_bounds() + if strength > 0.75: + target = int((min_raise + max_raise) * 0.5) + else: + target = int((min_raise + max_raise) * 0.35) + target = max(min_raise, min(target, max_raise)) + return RaiseAction(target) + if CheckAction in legal_actions: + return CheckAction() + return CallAction() + + else: + if in_position: + if strength >= 0.45 and RaiseAction in legal_actions: + if random.random() < 0.50: + min_raise, max_raise = round_state.raise_bounds() + return RaiseAction(min_raise) + + if random.random() < 0.05 and RaiseAction in legal_actions: + min_raise, max_raise = round_state.raise_bounds() + return RaiseAction(min_raise) + + if CheckAction in legal_actions: + return CheckAction() + return CallAction() + + def _handle_postflop_ev(self, round_state, active, my_cards, board_cards, + continue_cost, pot_total, street): + + all_cards = list(my_cards) + list(board_cards) + all_ranks = [self.RANK_MAP[c[0]] for c in all_cards] + all_suits = [c[1] for c in all_cards] + + bucket, made_hand_strength = self._evaluate_postflop(all_ranks, all_suits) + + in_position = (active == 0) + legal_actions = round_state.legal_actions() + my_pip = round_state.pips[active] + opp_pip = round_state.pips[1-active] + + # === calculate Outs & Equity === + outs, draw_type = self.poker_math.count_outs(my_cards, board_cards, self.RANK_MAP) + + if street == 1: # Flop + streets_remaining = 2 # Turn + River + elif street == 2: # Turn + streets_remaining = 1 # River + else: # River + streets_remaining = 0 + + draw_equity = self.poker_math.calculate_equity(outs, streets_remaining) + + if draw_type: + total_equity = max(made_hand_strength, draw_equity) + else: + total_equity = made_hand_strength + + # Exploitation + exploit_adjustments = None + if self.hands_played >= self.exploit_threshold: + exploit_adjustments = self.opponent_model.get_exploit_adjustments() + + # === decision logic === + + if continue_cost == 0: + + if bucket in ["nuts", "very-strong", "strong"]: + if RaiseAction in legal_actions: + bet_prob = 0.95 if bucket == "nuts" else 0.85 + if random.random() < bet_prob: + min_raise, max_raise = round_state.raise_bounds() + multiplier = 1.2 if bucket == "nuts" else 1.0 if bucket == "very-strong" else 0.8 + pot_bet = int(pot_total * 0.75 * multiplier) + target = my_pip + pot_bet + target = max(min_raise, min(target, max_raise)) + return RaiseAction(target) + return CheckAction() if CheckAction in legal_actions else FoldAction() + + elif draw_type and outs >= 8: + if in_position and RaiseAction in legal_actions: + bluff_prob = min(draw_equity * 1.5, 0.60) + if random.random() < bluff_prob: + min_raise, max_raise = round_state.raise_bounds() + return RaiseAction(min_raise) + return CheckAction() if CheckAction in legal_actions else FoldAction() + + # semi-strong + elif bucket in ["medium-strong", "medium"]: + if in_position and RaiseAction in legal_actions: + bluff_prob = 0.30 + if exploit_adjustments and exploit_adjustments['bluff_more']: + bluff_prob = 0.50 + if random.random() < bluff_prob: + min_raise, max_raise = round_state.raise_bounds() + return RaiseAction(min_raise) + return CheckAction() if CheckAction in legal_actions else FoldAction() + + # weak card + else: + if in_position and random.random() < 0.05 and RaiseAction in legal_actions: + min_raise, max_raise = round_state.raise_bounds() + return RaiseAction(min_raise) + return CheckAction() if CheckAction in legal_actions else FoldAction() + + else: + + pot_odds = self.poker_math.calculate_pot_odds(continue_cost, pot_total) + ev = self.poker_math.calculate_ev(total_equity, continue_cost, pot_total) + should_call_math = self.poker_math.should_call_mathematically(total_equity, pot_odds) + + if bucket in ["nuts", "very-strong", "strong"]: + if RaiseAction in legal_actions: + raise_prob = 0.85 if bucket in ["nuts", "very-strong"] else 0.60 + if random.random() < raise_prob: + min_raise, max_raise = round_state.raise_bounds() + target = opp_pip + int(pot_total * 1.0) + target = max(min_raise, min(target, max_raise)) + return RaiseAction(target) + if CallAction in legal_actions: + return CallAction() + return CheckAction() if CheckAction in legal_actions else FoldAction() + + elif draw_type: + if should_call_math: + # EV > 0,call + if CallAction in legal_actions: + print(f" [Draw Call] Outs:{outs} Equity:{total_equity:.2%} PotOdds:{pot_odds:.2%} EV:+{ev:.1f}") + return CallAction() + else: + # EV < 0,fold( + if in_position and outs >= 8 and random.random() < 0.15: + if RaiseAction in legal_actions: + min_raise, max_raise = round_state.raise_bounds() + return RaiseAction(min_raise) + + if FoldAction in legal_actions: + return FoldAction() + return CheckAction() if CheckAction in legal_actions else CallAction() + + elif bucket in ["medium-strong", "medium"]: + adjusted_equity = total_equity + if exploit_adjustments and exploit_adjustments['call_lighter']: + adjusted_equity *= 1.2 + + if adjusted_equity > pot_odds: + if CallAction in legal_actions: + return CallAction() + + if FoldAction in legal_actions: + return FoldAction() + return CheckAction() if CheckAction in legal_actions else CallAction() + + else: + if pot_odds <= 0.20 and random.random() < 0.10: + if CallAction in legal_actions: + return CallAction() + + if FoldAction in legal_actions: + return FoldAction() + return CheckAction() if CheckAction in legal_actions else CallAction() + + # Fallback + if CheckAction in legal_actions: + return CheckAction() + elif CallAction in legal_actions: + return CallAction() + elif FoldAction in legal_actions: + return FoldAction() + else: + return list(legal_actions)[0]() + + def _handle_discard(self, my_cards, board_cards, active): + """Discard""" + hand_ranks = [self.RANK_MAP[c[0]] for c in my_cards] + hand_suits = [c[1] for c in my_cards] + board_ranks = [self.RANK_MAP[c[0]] for c in board_cards] + board_suits = [c[1] for c in board_cards] + + keep_values = [] + for i in range(len(my_cards)): + val = 0.0 + r = hand_ranks[i] + s = hand_suits[i] + + pair_count = hand_ranks.count(r) + if pair_count == 2: + val += 12.0 + elif pair_count == 3: + val += 15.0 + + board_pair_count = board_ranks.count(r) + if board_pair_count >= 2: + val += 10.0 + elif board_pair_count == 1: + val += 4.0 + + my_suit_count = hand_suits.count(s) + board_suit_count = board_suits.count(s) + total_suit = my_suit_count + board_suit_count + if total_suit >= 4: + val += 8.0 + elif total_suit == 3: + val += 4.0 + + all_ranks = hand_ranks + board_ranks + if r == 14: + all_ranks.append(1) + unique_ranks = sorted(set(all_ranks)) + max_straight_draw = 0 + for base in unique_ranks: + window = [x for x in unique_ranks if base <= x < base + 5] + max_straight_draw = max(max_straight_draw, len(window)) + if max_straight_draw >= 4: + contributes = False + for base in unique_ranks: + if base <= r < base + 5: + contributes = True + break + if r == 14 and (1 in unique_ranks or 2 in unique_ranks): + contributes = True + if contributes: + val += 6.0 + + if r == 14: + val += 5.0 + elif r >= 12: + val += 3.0 + elif r >= 10: + val += 1.5 + + keep_values.append(val) + + discard_idx = keep_values.index(min(keep_values)) + return DiscardAction(discard_idx) + + def _calculate_preflop_strength(self, hand_ranks, hand_suits, ranks_sorted): + strength = 0.0 + + if len(set(hand_ranks)) == 1: + strength = 0.95 + elif len(set(hand_ranks)) == 2: + pair_rank = max([r for r in hand_ranks if hand_ranks.count(r) == 2]) + kicker = max([r for r in hand_ranks if r != pair_rank]) + strength = 0.55 + (pair_rank / 14) * 0.25 + (kicker / 14) * 0.05 + else: + top2_sum = ranks_sorted[0] + ranks_sorted[1] + if ranks_sorted[0] == 14: + if ranks_sorted[1] >= 12: + strength = 0.50 + (ranks_sorted[1] - 12) * 0.05 + elif ranks_sorted[1] >= 10: + strength = 0.40 + (ranks_sorted[1] - 10) * 0.05 + else: + strength = 0.25 + (ranks_sorted[1] / 14) * 0.10 + elif top2_sum >= 24: + strength = 0.40 + elif top2_sum >= 22: + strength = 0.32 + else: + strength = (top2_sum / 28) * 0.30 + + max_suit_count = max(hand_suits.count(s) for s in ['s','h','d','c']) + if max_suit_count == 3: + strength += 0.12 + elif max_suit_count == 2: + strength += 0.06 + + gaps = [ranks_sorted[i] - ranks_sorted[i+1] for i in range(len(ranks_sorted) - 1)] + if all(g <= 1 for g in gaps): + strength += 0.10 + elif all(g <= 2 for g in gaps): + strength += 0.05 + + strength = min(strength, 1.0) + + return strength + + def _evaluate_preflop_strength(self, my_cards): + hand_ranks = [self.RANK_MAP[c[0]] for c in my_cards] + hand_suits = [c[1] for c in my_cards] + ranks_sorted = sorted(hand_ranks, reverse=True) + return self._calculate_preflop_strength(hand_ranks, hand_suits, ranks_sorted) + + def _evaluate_postflop(self, all_ranks, all_suits): + rank_counts = {} + for r in all_ranks: + rank_counts[r] = rank_counts.get(r, 0) + 1 + sorted_counts = sorted(rank_counts.values(), reverse=True) + + suit_counts = {'s': 0, 'h': 0, 'd': 0, 'c': 0} + for s in all_suits: + suit_counts[s] += 1 + max_suit_count = max(suit_counts.values()) + has_flush = max_suit_count >= 5 + + unique_ranks = sorted(set(all_ranks)) + if 14 in unique_ranks: + unique_ranks = [1] + unique_ranks + has_straight = False + straight_run = 1 + for i in range(1, len(unique_ranks)): + if unique_ranks[i] == unique_ranks[i-1] + 1: + straight_run += 1 + if straight_run >= 5: + has_straight = True + break + else: + straight_run = 1 + + if has_flush and has_straight: + return "nuts", 0.95 + elif sorted_counts and sorted_counts[0] == 4: + return "nuts", 0.92 + elif has_flush or has_straight: + return "very-strong", 0.85 + elif len(sorted_counts) >= 2 and sorted_counts[0] == 3 and sorted_counts[1] >= 2: + return "very-strong", 0.82 + elif sorted_counts and sorted_counts[0] == 3: + return "strong", 0.70 + elif len(sorted_counts) >= 2 and sorted_counts[0] == 2 and sorted_counts[1] == 2: + pairs = sorted([r for r, c in rank_counts.items() if c == 2], reverse=True) + if pairs[0] >= 10: + return "strong", 0.65 + else: + return "medium-strong", 0.55 + elif sorted_counts and sorted_counts[0] == 2: + pair_rank = [r for r, c in rank_counts.items() if c == 2][0] + if pair_rank >= 11: + return "medium-strong", 0.50 + elif pair_rank >= 8: + return "medium", 0.40 + else: + return "medium-weak", 0.32 + else: + has_flush_draw = max_suit_count == 4 + has_straight_draw = any( + len([x for x in unique_ranks if base <= x <= base + 4]) >= 4 + for base in unique_ranks + ) + + if has_flush_draw or has_straight_draw: + return "draw", 0.35 + else: + return "weak", 0.15 + + +if __name__ == '__main__': + run_bot(Player(), parse_args()) \ No newline at end of file diff --git a/python_skeleton/player.py b/python_skeleton/player.py index c579682..12c2422 100644 --- a/python_skeleton/player.py +++ b/python_skeleton/player.py @@ -1,121 +1,312 @@ ''' -Simple example pokerbot, written in Python. +None RL pokerbot ''' from skeleton.actions import FoldAction, CallAction, CheckAction, RaiseAction, DiscardAction from skeleton.states import GameState, TerminalState, RoundState from skeleton.states import NUM_ROUNDS, STARTING_STACK, BIG_BLIND, SMALL_BLIND from skeleton.bot import Bot from skeleton.runner import parse_args, run_bot - import random class Player(Bot): - ''' - A pokerbot. - ''' def __init__(self): - ''' - Called when a new game starts. Called exactly once. - - Arguments: - Nothing. - - Returns: - Nothing. - ''' - pass + self.RANK_MAP = { + "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9, + "T": 10, "J": 11, "Q": 12, "K": 13, "A": 14 + } def handle_new_round(self, game_state, round_state, active): - ''' - Called when a new round starts. Called NUM_ROUNDS times. - - Arguments: - game_state: the GameState object. - round_state: the RoundState object. - active: your player's index. - - Returns: - Nothing. - ''' - my_bankroll = game_state.bankroll # the total number of chips you've gained or lost from the beginning of the game to the start of this round - # the total number of seconds your bot has left to play this game + my_bankroll = game_state.bankroll game_clock = game_state.game_clock - round_num = game_state.round_num # the round number from 1 to NUM_ROUNDS - my_cards = round_state.hands[active] # your cards - big_blind = bool(active) # True if you are the big blind - pass + round_num = game_state.round_num + my_cards = round_state.hands[active] + big_blind = bool(active) def handle_round_over(self, game_state, terminal_state, active): - ''' - Called when a round ends. Called NUM_ROUNDS times. - - Arguments: - game_state: the GameState object. - terminal_state: the TerminalState object. - active: your player's index. - - Returns: - Nothing. - ''' - my_delta = terminal_state.deltas[active] # your bankroll change from this round - previous_state = terminal_state.previous_state # RoundState before payoffs - street = previous_state.street # 0,2,3,4,5,6 representing when this round ended - my_cards = previous_state.hands[active] # your cards - # opponent's cards or [] if not revealed + my_delta = terminal_state.deltas[active] + previous_state = terminal_state.previous_state + street = previous_state.street + my_cards = previous_state.hands[active] opp_cards = previous_state.hands[1-active] - pass def get_action(self, game_state, round_state, active): - ''' - Where the magic happens - your code should implement this function. - Called any time the engine needs an action from your bot. - - Arguments: - game_state: the GameState object. - round_state: the RoundState object. - active: your player's index. - - Returns: - Your action. - ''' - legal_actions = round_state.legal_actions() # the actions you are allowed to take - # 0, 3, 4, or 5 representing pre-flop, flop, turn, or river respectively + legal_actions = round_state.legal_actions() street = round_state.street - my_cards = round_state.hands[active] # your cards - board_cards = round_state.board # the board cards - # the number of chips you have contributed to the pot this round of betting + my_cards = round_state.hands[active] + board_cards = round_state.board my_pip = round_state.pips[active] - # the number of chips your opponent has contributed to the pot this round of betting opp_pip = round_state.pips[1-active] - # the number of chips you have remaining my_stack = round_state.stacks[active] - # the number of chips your opponent has remaining opp_stack = round_state.stacks[1-active] - continue_cost = opp_pip - my_pip # the number of chips needed to stay in the pot - # the number of chips you have contributed to the pot + continue_cost = opp_pip - my_pip my_contribution = STARTING_STACK - my_stack - # the number of chips your opponent has contributed to the pot opp_contribution = STARTING_STACK - opp_stack + pot_total = my_pip + opp_pip - # Only use DiscardAction if it's in legal_actions (which already checks street) - # legal_actions() returns DiscardAction only when street is 2 or 3 + #===DISCARD PHASE=== if DiscardAction in legal_actions: - # Always discards the first card in the bot's hand - return DiscardAction(0) - if RaiseAction in legal_actions: - # the smallest and largest numbers of chips for a legal bet/raise - min_raise, max_raise = round_state.raise_bounds() - min_cost = min_raise - my_pip # the cost of a minimum bet/raise - max_cost = max_raise - my_pip # the cost of a maximum bet/raise - if random.random() < 0.5: - return RaiseAction(min_raise) - if CheckAction in legal_actions: # check-call + hand_ranks = [self.RANK_MAP[c[0]] for c in my_cards] + hand_suits = [c[1] for c in my_cards] + board_ranks = [self.RANK_MAP[c[0]] for c in board_cards] + board_suits = [c[1] for c in board_cards] + + keep_values = [] + for i in range(3): + val = 0.0 + r = hand_ranks[i] + s = hand_suits[i] + + # Pair bonus + pair_count = hand_ranks.count(r) + if pair_count == 2: + val += 12.0 + elif pair_count == 3: + val += 15.0 + + # Board pair synergy + board_pair_count = board_ranks.count(r) + if board_pair_count >= 2: + val += 10.0 + elif board_pair_count == 1: + val += 4.0 + + # Flush potential + my_suit_count = hand_suits.count(s) + board_suit_count = board_suits.count(s) + total_suit = my_suit_count + board_suit_count + if total_suit >= 4: + val += 8.0 + elif total_suit == 3: + val += 4.0 + + # Straight potential + all_ranks = hand_ranks + board_ranks + if r == 14: + all_ranks.append(1) + unique_ranks = sorted(set(all_ranks)) + max_straight_draw = 0 + for base in unique_ranks: + window = [x for x in unique_ranks if base <= x < base + 5] + max_straight_draw = max(max_straight_draw, len(window)) + if max_straight_draw >= 4: + contributes = False + for base in unique_ranks: + if base <= r < base + 5: + contributes = True + break + if r == 14 and (1 in unique_ranks or 2 in unique_ranks): + contributes = True + if contributes: + val += 6.0 + + # High card value + if r == 14: + val += 5.0 + elif r >= 12: + val += 3.0 + elif r >= 10: + val += 1.5 + + keep_values.append(val) + + discard_idx = keep_values.index(min(keep_values)) + return DiscardAction(discard_idx) + + if len(legal_actions) == 1 and CheckAction in legal_actions: + return CheckAction() + + #===PREFLOP=== + if street == 0: + hand_ranks = [self.RANK_MAP[c[0]] for c in my_cards] + hand_suits = [c[1] for c in my_cards] + ranks_sorted = sorted(hand_ranks, reverse=True) + strength = 0.0 + + # Trips + if len(set(hand_ranks)) == 1: + strength = 0.95 + # Pair + elif len(set(hand_ranks)) == 2: + pair_rank = max([r for r in hand_ranks if hand_ranks.count(r) == 2]) + kicker = max([r for r in hand_ranks if r != pair_rank]) + strength = 0.55 + (pair_rank / 14) * 0.25 + (kicker / 14) * 0.05 + # High card + else: + top2_sum = ranks_sorted[0] + ranks_sorted[1] + if ranks_sorted[0] == 14: + if ranks_sorted[1] >= 12: + strength = 0.50 + (ranks_sorted[1] - 12) * 0.05 + elif ranks_sorted[1] >= 10: + strength = 0.40 + (ranks_sorted[1] - 10) * 0.05 + else: + strength = 0.25 + (ranks_sorted[1] / 14) * 0.10 + elif top2_sum >= 24: + strength = 0.40 + elif top2_sum >= 22: + strength = 0.32 + else: + strength = (top2_sum / 28) * 0.30 + + # Suited bonus + max_suit_count = max(hand_suits.count("s"), hand_suits.count("h"), + hand_suits.count("d"), hand_suits.count("c")) + if max_suit_count == 3: + strength += 0.12 + elif max_suit_count == 2: + strength += 0.06 + # Connected bonus + gaps = [ranks_sorted[i] - ranks_sorted[i+1] for i in range(len(ranks_sorted) - 1)] + if all(g <= 1 for g in gaps): + strength += 0.10 + elif all(g <= 2 for g in gaps): + strength += 0.05 + strength = min(strength, 1.0) + + # Decision logic + if continue_cost > 0: + pot_odds = continue_cost / (pot_total + continue_cost) if (pot_total + continue_cost) > 0 else 0 + + if strength < 0.20: + if FoldAction in legal_actions: + return FoldAction() + return CheckAction() if CheckAction in legal_actions else CallAction() + elif strength < 0.35: + if pot_odds > 0.35 and FoldAction in legal_actions: + return FoldAction() + if CallAction in legal_actions: + return CallAction() + return CheckAction() if CheckAction in legal_actions else FoldAction() + elif strength < 0.60: + if CallAction in legal_actions: + return CallAction() + return CheckAction() if CheckAction in legal_actions else FoldAction() + else: + if RaiseAction in legal_actions: + min_raise, max_raise = round_state.raise_bounds() + size_factor = 0.4 + (strength - 0.60) * 0.6 + target = int(min_raise + (max_raise - min_raise) * size_factor * 0.3) + target = max(min_raise, min(target, max_raise)) + return RaiseAction(target) + if CallAction in legal_actions: + return CallAction() + return CheckAction() + else: + if CheckAction in legal_actions: + return CheckAction() + return CallAction() + + # ====POST-DISCARD BETTING=== + all_cards = list(my_cards) + list(board_cards) + all_ranks = [self.RANK_MAP[c[0]] for c in all_cards] + all_suits = [c[1] for c in all_cards] + + # Count ranks + rank_counts = {} + for r in all_ranks: + rank_counts[r] = rank_counts.get(r, 0) + 1 + sorted_counts = sorted(rank_counts.values(), reverse=True) + + # Check flush + suit_counts = {"s": 0, "h": 0, "d": 0, "c": 0} + for s in all_suits: + suit_counts[s] += 1 + max_suit_count = max(suit_counts.values()) + has_flush = max_suit_count >= 5 + + # Check straight + unique_ranks = sorted(set(all_ranks)) + if 14 in unique_ranks: + unique_ranks = [1] + unique_ranks + has_straight = False + straight_run = 1 + for i in range(1, len(unique_ranks)): + if unique_ranks[i] == unique_ranks[i-1] + 1: + straight_run += 1 + if straight_run >= 5: + has_straight = True + break + else: + straight_run = 1 + + # Determine bucket + if has_flush or has_straight: + bucket = "strong" + elif sorted_counts and sorted_counts[0] >= 3: + bucket = "strong" + elif len(sorted_counts) >= 2 and sorted_counts[0] == 2 and sorted_counts[1] == 2: + bucket = "strong" + elif sorted_counts and sorted_counts[0] == 2: + bucket = "medium" + else: + has_flush_draw = max_suit_count == 4 + unique_ranks2 = sorted(set(all_ranks)) + if 14 in unique_ranks2: + unique_ranks2 = [1] + unique_ranks2 + has_straight_draw = False + for base in unique_ranks2: + window = [x for x in unique_ranks2 if base <= x <= base + 4] + if len(window) >= 4: + has_straight_draw = True + break + if has_flush_draw or has_straight_draw: + bucket = "medium" + else: + bucket = "weak" + in_position = (active == 0) + + # Decision + if continue_cost == 0: + if bucket == "strong": + if RaiseAction in legal_actions: + min_raise, max_raise = round_state.raise_bounds() + pot_bet = pot_total + target = my_pip + pot_bet + target = max(min_raise, min(target, max_raise)) + return RaiseAction(target) + return CheckAction() if CheckAction in legal_actions else FoldAction() + elif bucket == "medium": + if in_position and RaiseAction in legal_actions: + if random.random() < 0.35: + min_raise, max_raise = round_state.raise_bounds() + return RaiseAction(min_raise) + return CheckAction() if CheckAction in legal_actions else FoldAction() + else: + return CheckAction() if CheckAction in legal_actions else FoldAction() + else: + pot_odds = continue_cost / (pot_total + continue_cost) if (pot_total + continue_cost) > 0 else 0 + if bucket == "strong": + if RaiseAction in legal_actions: + min_raise, max_raise = round_state.raise_bounds() + target = opp_pip + pot_total + target = max(min_raise, min(target, max_raise)) + if random.random() < 0.65: + return RaiseAction(target) + if CallAction in legal_actions: + return CallAction() + return CheckAction() if CheckAction in legal_actions else FoldAction() + elif bucket == "medium": + if pot_odds <= 0.40: + if CallAction in legal_actions: + return CallAction() + if FoldAction in legal_actions: + return FoldAction() + return CheckAction() if CheckAction in legal_actions else CallAction() + else: + if FoldAction in legal_actions: + return FoldAction() + return CheckAction() if CheckAction in legal_actions else CallAction() + + # Fallback + if CheckAction in legal_actions: return CheckAction() - if random.random() < 0.25: + elif CallAction in legal_actions: + return CallAction() + elif FoldAction in legal_actions: return FoldAction() - return CallAction() + else: + return list(legal_actions)[0]() if __name__ == '__main__': diff --git a/run_game_and_get_results.py b/run_game_and_get_results.py new file mode 100644 index 0000000..436c1f0 --- /dev/null +++ b/run_game_and_get_results.py @@ -0,0 +1,6 @@ +import engine +import gamelog_analyzer + +if __name__ == "__main__": + engine.Game().run() + P1_bankroll, P1_PnL, P2_bankroll, P2_PnL = gamelog_analyzer.AnalyzeGame()