def test_player_stats(self): e = Engine( 'CoinPoker', 1, { 1: { 'name': 'joe', 'balance': 1000, 'status': 1 }, 2: { 'name': 'jane', 'balance': 1000, 'status': 1 }, }, 50, 100, 0, ) e.available_actions() stats = ES.player_stats(e, e.s) assert 'actions' in stats assert len(stats['actions']) >= 4 assert 0 < stats['hs'] < 100
def __init__(self, site_name, button, players, sb, bb, ante=0, *args, **kwargs): logger.info(f'Engine site_name: {site_name}') logger.info(f'Engine button: {button}') logger.info(f'Engine players: {len(players)}') logger.info(f'Engine kwargs: {kwargs}') self.site_name = site_name self.button = button self.players = players self.sb_amt = sb self.bb_amt = bb self.ante = ante self.go_to_showdown = False self.mc = False if hasattr(kwargs, 'data'): self.data = kwargs['data'] else: self.data = {s: { 'status': 'in' if p.get('status') else 'out', 'sitout': False, 'hand': ['__', '__'] if p.get('status') else [' ', ' '], 'contrib': 0, 'matched': 0, 'preflop': [], 'flop': [], 'turn': [], 'river': [], 'showdown': [], # for error at stats 'is_SB': False, 'is_BB': False, } for s, p in players.items()} self.vs = sum([1 if d['status'] == 'in' else 0 for d in self.data.values()]) self.rivals = self.vs self.winner = None # leave empty: scraper compares length self.board = kwargs.get('board', []) self.pot = kwargs.get('pot', 0) self.phase = kwargs.get('phase', self.PHASE_PREFLOP) self.preflop = kwargs.get('preflop', {}) self.flop = kwargs.get('flop', {}) self.turn = kwargs.get('turn', {}) self.river = kwargs.get('river', {}) self.showdown = kwargs.get('showdown', {}) self.q = None self.pe_equities = {} # hand_strength = PE.hand_strength(['__', '__'], self.board, self.rivals) for s, d in self.data.items(): if 'in' not in d['status']: continue self.data[s]['stats'] = ES.player_stats(self, s) self.players[s]['hand_range'] = ES.cut_hand_range(self.data[s]['stats']) self.data[s]['strength'] = 0.20
def test_player_stats(self): e = Engine( 'CoinPoker', 1, { 1: {'name': 'joe', 'balance': 1000, 'status': 1}, 2: {'name': 'jane', 'balance': 1000, 'status': 1}, }, 50, 100, 0, ) e.available_actions() stats = ES.player_stats(e, e.s) assert 'actions' in stats assert len(stats['actions']) >= 4 assert 0 < stats['hs'] < 100
def test_player_stats_on_hand(self): e = Engine( 'CoinPoker', 1, { 1: {'name': 'joe', 'balance': 1000, 'status': 1}, 2: {'name': 'jane', 'balance': 1000, 'status': 1}, 3: {'name': 'jane', 'balance': 1000, 'status': 1}, 4: {'name': 'jane', 'balance': 1000, 'status': 1}, 5: {'name': 'jane', 'balance': 1000, 'status': 1}, 6: {'name': 'jane', 'balance': 1000, 'status': 1}, }, 50, 100, 0, ) e.available_actions() # p4 e.do(['r', 100]) e.available_actions() # p5 e.do(['f']) e.available_actions() # p6 e.do(['f']) e.available_actions() # p1 e.do(['c']) e.available_actions() # p2 e.do(['c']) e.available_actions() # p3 e.do(['k']) e.available_actions() # p2 e.do(['b', 100]) e.available_actions() # p3 stats = ES.player_stats(e, e.s) # hs = res.aggregations['hs']['hs_agg']['values']['50.0'] assert len(stats['actions']) >= 4
def test_player_stats_on_hand(self): e = Engine( 'CoinPoker', 1, { 1: { 'name': 'joe', 'balance': 1000, 'status': 1 }, 2: { 'name': 'jane', 'balance': 1000, 'status': 1 }, 3: { 'name': 'jane', 'balance': 1000, 'status': 1 }, 4: { 'name': 'jane', 'balance': 1000, 'status': 1 }, 5: { 'name': 'jane', 'balance': 1000, 'status': 1 }, 6: { 'name': 'jane', 'balance': 1000, 'status': 1 }, }, 50, 100, 0, ) e.available_actions() # p4 e.do(['r', 100]) e.available_actions() # p5 e.do(['f']) e.available_actions() # p6 e.do(['f']) e.available_actions() # p1 e.do(['c']) e.available_actions() # p2 e.do(['c']) e.available_actions() # p3 e.do(['k']) e.available_actions() # p2 e.do(['b', 100]) e.available_actions() # p3 stats = ES.player_stats(e, e.s) # hs = res.aggregations['hs']['hs_agg']['values']['50.0'] assert len(stats['actions']) >= 4
def add_actions(self, e, parent): """Add actions available to this node If in GG phase then no actions possible, ever. Remove 'hand' Bets: - preflop are 2-4x BB - postflop are 40-100% pot Raise: - always double Allin: - only on river - if out of money then converted to allin Scale non-fold probabilities even though it should not have an effect. """ # logger.info('adding actions to {}'.format(parent.tag)) actions = e.available_actions() s, p = e.q[0] d = e.data[s] balance_left = p['balance'] - d['contrib'] if not actions: # logger.warn('no actions to add to node') return if 'gg' in actions: # logger.debug('no actions available, got gg') return actions.remove('hand') # remove fold if player can check if 'check' in actions: actions.remove('fold') # # logger.debug('removed fold when check available') # remove fold for hero # if s == self.hero and 'fold' in actions: # actions.remove('fold') # # logger.debug('removed fold from hero') # remove raise if player has already been aggressive if 'raise' in actions and any(pa['action'] in 'br' for pa in d[e.phase]): actions.remove('raise') # # logger.debug('removed raise as player has already been aggressive') # remove allin, but add it later with final stats (if increased from bet/raised) if 'allin' in actions: actions.remove('allin') # logger.debug('removed allin by default') # load stats (codes with counts) stats = ES.player_stats(e, s) max_contrib = max(pd['contrib'] for pd in e.data.values()) # contrib_short = max_contrib - d['contrib'] # allin needs to be the doc count # where bets and raises result in allin, add those prob dists to this # that will give proper probability go_allin = stats['actions'].get('a', 0) # # logger.info('filtered actions: {}'.format(actions)) # ev 0 instead of none because of root node sum when not all traversed it gives error action_nodes = [] for a in actions: node_data = { 'stats': stats['actions'].get(ACTIONS_TO_ABBR[a], 0.01), 'divider': 1, 'action': a, 'phase': e.phase, 'seat': s, 'name': p['name'], 'traversed': 0, 'ev': 0, } if a in ['bet', 'raise']: btps_and_amts = [] total_pot = sum(pd['contrib'] for pd in e.data.values()) + e.pot # for preflop only do 2x and 3x if e.phase == e.PHASE_PREFLOP: btps_and_amts.append(('double', e.bb_amt * 2)) btps_and_amts.append(('triple', e.bb_amt * 3)) # else do half and full pots else: btps_and_amts.append(('half_pot', total_pot * 0.50)) btps_and_amts.append(('full_pot', total_pot * 1.00)) # round bets up to a BB # btps_and_amts = [(btp, -(amt // -e.bb_amt) * e.bb_amt) # for btp, amt in btps_and_amts] betting_info = [] amts_seen = [] for btp, amt in btps_and_amts: if amt in amts_seen: # logger.debug('already using {}, skipping duplicate'.format(amt)) continue if a == 'bet' and amt < e.bb_amt: # logger.debug('bet cannot be less than BB {}'.format(e.bb_amt)) continue if a == 'raise' and amt < (max_contrib * 2): # logger.debug('raise cannot be less than 2x contrib of {}'.format(max_contrib * 2)) continue betting_info.append((btp, amt)) amts_seen.append(amt) # change raises that cause allin betting_info_final = [] for btp, amt in betting_info: # if amt is more than player balance, it is an allin if amt >= balance_left: go_allin += node_data['stats'] / len(betting_info) else: betting_info_final.append((btp, amt)) # all good, can have this bet as option for btp, amt in betting_info_final: node_data_copy = deepcopy(node_data) node_data_copy['divider'] = len(betting_info_final) node_data_copy['action'] = f'{a}_{btp}' node_data_copy['amount'] = amt action_nodes.append(node_data_copy) else: action_nodes.append(node_data) # allin will have doc counts (from stat, maybe from bets, maybe from raise) if go_allin: node_data = { 'stats': go_allin, 'divider': 1, 'action': 'allin', 'phase': e.phase, 'seat': s, 'name': p['name'], 'traversed': 0, 'ev': 0, 'amount': balance_left, } action_nodes.append(node_data) # logger.debug('added allin to actions with stat {}'.format(node_data['stats'])) # scale the stats (it is currently term counts aka histogram) and it is required to be # a probability distribution (p~1) # Also, certain actions like fold can be removed, and the total stats is not 1 total_stats = sum(an['stats'] / an['divider'] for an in action_nodes) for action_node in action_nodes: action_node['stats'] = max(0.01, action_node['stats'] / action_node['divider'] / total_stats) action_node['cum_stats'] = parent.data['cum_stats'] * action_node['stats'] node_tag = f'{action_node["action"]}_{s}_{e.phase}' identifier = f'{node_tag}_{str(uuid.uuid4())[:8]}' self.tree.create_node(identifier=identifier, tag=node_tag, parent=parent.identifier, data=action_node) # logger.debug('new {} for {} with data {}'.format(node_tag, s, action_node)) item = ( 1 - action_node['cum_stats'], self.leaf_path + [identifier] ) self.queue.put(item)
def __init__(self, site_name, button, players, sb, bb, ante=0, *args, **kwargs): logger.info(f'Engine site_name: {site_name}') logger.info(f'Engine button: {button}') logger.info(f'Engine players: {len(players)}') logger.info(f'Engine kwargs: {kwargs}') self.site_name = site_name self.button = button self.players = players self.sb_amt = sb self.bb_amt = bb self.ante = ante self.go_to_showdown = False self.mc = False if hasattr(kwargs, 'data'): self.data = kwargs['data'] else: self.data = { s: { 'status': 'in' if p.get('status') else 'out', 'sitout': False, 'hand': ['__', '__'] if p.get('status') else [' ', ' '], 'contrib': 0, 'matched': 0, 'preflop': [], 'flop': [], 'turn': [], 'river': [], 'showdown': [], # for error at stats 'is_SB': False, 'is_BB': False, } for s, p in players.items() } self.vs = sum( [1 if d['status'] == 'in' else 0 for d in self.data.values()]) self.rivals = self.vs self.winner = None # leave empty: scraper compares length self.board = kwargs.get('board', []) self.pot = kwargs.get('pot', 0) self.phase = kwargs.get('phase', self.PHASE_PREFLOP) self.preflop = kwargs.get('preflop', {}) self.flop = kwargs.get('flop', {}) self.turn = kwargs.get('turn', {}) self.river = kwargs.get('river', {}) self.showdown = kwargs.get('showdown', {}) self.q = None self.pe_equities = {} # hand_strength = PE.hand_strength(['__', '__'], self.board, self.rivals) for s, d in self.data.items(): if 'in' not in d['status']: continue self.data[s]['stats'] = ES.player_stats(self, s) self.players[s]['hand_range'] = ES.cut_hand_range( self.data[s]['stats']) self.data[s]['strength'] = 0.20
def add_actions(self, e, parent): """Add actions available to this node If in GG phase then no actions possible, ever. Remove 'hand' Bets: - preflop are 2-4x BB - postflop are 40-100% pot Raise: - always double Allin: - only on river - if out of money then converted to allin Scale non-fold probabilities even though it should not have an effect. """ # logger.info('adding actions to {}'.format(parent.tag)) actions = e.available_actions() s, p = e.q[0] d = e.data[s] balance_left = p['balance'] - d['contrib'] if not actions: # logger.warn('no actions to add to node') return if 'gg' in actions: # logger.debug('no actions available, got gg') return actions.remove('hand') # remove fold if player can check if 'check' in actions: actions.remove('fold') # # logger.debug('removed fold when check available') # remove fold for hero # if s == self.hero and 'fold' in actions: # actions.remove('fold') # # logger.debug('removed fold from hero') # remove raise if player has already been aggressive if 'raise' in actions and any(pa['action'] in 'br' for pa in d[e.phase]): actions.remove('raise') # # logger.debug('removed raise as player has already been aggressive') # remove allin, but add it later with final stats (if increased from bet/raised) if 'allin' in actions: actions.remove('allin') # logger.debug('removed allin by default') # load stats (codes with counts) stats = ES.player_stats(e, s) max_contrib = max(pd['contrib'] for pd in e.data.values()) # contrib_short = max_contrib - d['contrib'] # allin needs to be the doc count # where bets and raises result in allin, add those prob dists to this # that will give proper probability go_allin = stats['actions'].get('a', 0) # # logger.info('filtered actions: {}'.format(actions)) # ev 0 instead of none because of root node sum when not all traversed it gives error action_nodes = [] for a in actions: node_data = { 'stats': stats['actions'].get(ACTIONS_TO_ABBR[a], 0.01), 'divider': 1, 'action': a, 'phase': e.phase, 'seat': s, 'name': p['name'], 'traversed': 0, 'ev': 0, } if a in ['bet', 'raise']: btps_and_amts = [] total_pot = sum(pd['contrib'] for pd in e.data.values()) + e.pot # for preflop only do 2x and 3x if e.phase == e.PHASE_PREFLOP: btps_and_amts.append(('double', e.bb_amt * 2)) btps_and_amts.append(('triple', e.bb_amt * 3)) # else do half and full pots else: btps_and_amts.append(('half_pot', total_pot * 0.50)) btps_and_amts.append(('full_pot', total_pot * 1.00)) # round bets up to a BB # btps_and_amts = [(btp, -(amt // -e.bb_amt) * e.bb_amt) # for btp, amt in btps_and_amts] betting_info = [] amts_seen = [] for btp, amt in btps_and_amts: if amt in amts_seen: # logger.debug('already using {}, skipping duplicate'.format(amt)) continue if a == 'bet' and amt < e.bb_amt: # logger.debug('bet cannot be less than BB {}'.format(e.bb_amt)) continue if a == 'raise' and amt < (max_contrib * 2): # logger.debug('raise cannot be less than 2x contrib of {}'.format(max_contrib * 2)) continue betting_info.append((btp, amt)) amts_seen.append(amt) # change raises that cause allin betting_info_final = [] for btp, amt in betting_info: # if amt is more than player balance, it is an allin if amt >= balance_left: go_allin += node_data['stats'] / len(betting_info) else: betting_info_final.append((btp, amt)) # all good, can have this bet as option for btp, amt in betting_info_final: node_data_copy = deepcopy(node_data) node_data_copy['divider'] = len(betting_info_final) node_data_copy['action'] = f'{a}_{btp}' node_data_copy['amount'] = amt action_nodes.append(node_data_copy) else: action_nodes.append(node_data) # allin will have doc counts (from stat, maybe from bets, maybe from raise) if go_allin: node_data = { 'stats': go_allin, 'divider': 1, 'action': 'allin', 'phase': e.phase, 'seat': s, 'name': p['name'], 'traversed': 0, 'ev': 0, 'amount': balance_left, } action_nodes.append(node_data) # logger.debug('added allin to actions with stat {}'.format(node_data['stats'])) # scale the stats (it is currently term counts aka histogram) and it is required to be # a probability distribution (p~1) # Also, certain actions like fold can be removed, and the total stats is not 1 total_stats = sum(an['stats'] / an['divider'] for an in action_nodes) for action_node in action_nodes: action_node['stats'] = max( 0.01, action_node['stats'] / action_node['divider'] / total_stats) action_node[ 'cum_stats'] = parent.data['cum_stats'] * action_node['stats'] node_tag = f'{action_node["action"]}_{s}_{e.phase}' identifier = f'{node_tag}_{str(uuid.uuid4())[:8]}' self.tree.create_node(identifier=identifier, tag=node_tag, parent=parent.identifier, data=action_node) # logger.debug('new {} for {} with data {}'.format(node_tag, s, action_node)) item = (1 - action_node['cum_stats'], self.leaf_path + [identifier]) self.queue.put(item)