diff --git a/README.md b/README.md index 710e2da..a897238 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ Creating a game tree To generate a tree for a game, simply specify the rules of the game: ```python -from pokertrees import * -from pokergames import * +from pycfr.pokertrees import * +from pycfr.pokergames import * players = 2 deck = [Card(14,1),Card(13,2),Card(13,1),Card(12,1)] rounds = [RoundInfo(holecards=1,boardcards=0,betsize=2,maxbets=[2,2]),RoundInfo(holecards=0,boardcards=1,betsize=4,maxbets=[2,2])] @@ -25,7 +25,7 @@ gametree.build() Or use one of the pre-built games: ```python -from pokergames import * +from pycfr.pokergames import * gametree = leduc_gametree() ``` @@ -34,9 +34,9 @@ Evaluating a strategy profile You can calculate the expected value of a set of strategies for a game: ```python -from pokertrees import * -from pokergames import * -from pokerstrategy import * +from pycfr.pokertrees import * +from pycfr.pokergames import * +from pycfr.pokerstrategy import * rules = leduc_rules() # load first player strategy @@ -58,9 +58,9 @@ Getting the best response strategy Given a strategy profile, you can calculate the best response strategy for each agent: ```python -from pokertrees import * -from pokergames import * -from pokerstrategy import * +from pycfr.pokertrees import * +from pycfr.pokergames import * +from pycfr.pokerstrategy import * rules = leduc_rules() # load first player strategy @@ -95,7 +95,7 @@ Given the rules for a game, you can run the Counterfactual Regret (CFR) Minimiza hskuhn = half_street_kuhn_rules() # Create the CFR minimizer -from pokercfr import CounterfactualRegretMinimizer +from pycfr. pokercfr import CounterfactualRegretMinimizer cfr = CounterfactualRegretMinimizer(hskuhn) # Run a number of iterations, determining the exploitability of the agents periodically @@ -114,7 +114,14 @@ nash_strategies = cfr.profile Tests ----- -Tests for the game tree code are implemented in the `tests` directory. WARNING: Tests using Leduc poker are slow due to the size of the game. +Tests for the game tree code are implemented in the `pycfr/tests` directory. + +Run tests like so: + + nosetests -s + + +Tests are organized as follows: - test_gametree.py - Tests the game tree functionality against a leduc-like game and verifies some branches are built as expected. diff --git a/pokertrees.py b/pokertrees.py deleted file mode 100644 index 9d8373a..0000000 --- a/pokertrees.py +++ /dev/null @@ -1,396 +0,0 @@ -from itertools import combinations -from itertools import permutations -from itertools import product -from collections import Counter -from hand_evaluator import HandEvaluator -from copy import deepcopy -from functools import partial - -FOLD = 0 -CALL = 1 -RAISE = 2 - -def overlap(t1, t2): - for x in t1: - if x in t2: - return True - return False - -def all_unique(hc): - for i in range(len(hc)-1): - for j in range(i+1,len(hc)): - if overlap(hc[i], hc[j]): - return False - return True - -def default_infoset_format(player, holecards, board, bet_history): - return "{0}{1}:{2}:".format("".join([str(x) for x in holecards]), "".join([str(x) for x in board]), bet_history) - -class GameRules(object): - def __init__(self, players, deck, rounds, ante, blinds, handeval = HandEvaluator.evaluate_hand, infoset_format=default_infoset_format): - assert(players >= 2) - assert(ante >= 0) - assert(rounds != None) - assert(deck != None) - assert(len(rounds) > 0) - assert(len(deck) > 1) - if blinds != None: - if type(blinds) is int or type(blinds) is float: - blinds = [blinds] - for r in rounds: - assert(len(r.maxbets) == players) - self.players = players - self.deck = deck - self.roundinfo = rounds - self.ante = ante - self.blinds = blinds - self.handeval = handeval - self.infoset_format = infoset_format - -class RoundInfo(object): - def __init__(self, holecards, boardcards, betsize, maxbets): - self.holecards = holecards - self.boardcards = boardcards - self.betsize = betsize - self.maxbets = maxbets - -class GameTree(object): - def __init__(self, rules): - self.rules = deepcopy(rules) - self.information_sets = {} - self.root = None - - def build(self): - # Assume everyone is in - players_in = [True] * self.rules.players - # Collect antes - committed = [self.rules.ante] * self.rules.players - bets = [0] * self.rules.players - # Collect blinds - next_player = self.collect_blinds(committed, bets, 0) - holes = [()] * self.rules.players - board = () - bet_history = "" - self.root = self.build_rounds(None, players_in, committed, holes, board, self.rules.deck, bet_history, 0, bets, next_player) - - def collect_blinds(self, committed, bets, next_player): - if self.rules.blinds != None: - for blind in self.rules.blinds: - committed[next_player] += blind - bets[next_player] = int((committed[next_player] - self.rules.ante) / self.rules.roundinfo[0].betsize) - next_player = (next_player + 1) % self.rules.players - return next_player - - def deal_holecards(self, deck, holecards, players): - a = combinations(deck, holecards) - return filter(lambda x: all_unique(x), permutations(a, players)) - - def build_rounds(self, root, players_in, committed, holes, board, deck, bet_history, round_idx, bets = None, next_player = 0): - if round_idx == len(self.rules.roundinfo): - return self.showdown(root, players_in, committed, holes, board, deck, bet_history) - bet_history += "/" - cur_round = self.rules.roundinfo[round_idx] - while not players_in[next_player]: - next_player = (next_player + 1) % self.rules.players - if bets is None: - bets = [0] * self.rules.players - min_actions_this_round = players_in.count(True) - actions_this_round = 0 - if cur_round.holecards: - return self.build_holecards(root, next_player, players_in, committed, holes, board, deck, bet_history, round_idx, min_actions_this_round, actions_this_round, bets) - if cur_round.boardcards: - return self.build_boardcards(root, next_player, players_in, committed, holes, board, deck, bet_history, round_idx, min_actions_this_round, actions_this_round, bets) - return self.build_bets(root, next_player, players_in, committed, holes, board, deck, bet_history, round_idx, min_actions_this_round, actions_this_round, bets) - - def get_next_player(self, cur_player, players_in): - next_player = (cur_player + 1) % self.rules.players - while not players_in[next_player]: - next_player = (next_player + 1) % self.rules.players - return next_player - - def build_holecards(self, root, next_player, players_in, committed, holes, board, deck, bet_history, round_idx, min_actions_this_round, actions_this_round, bets): - cur_round = self.rules.roundinfo[round_idx] - hnode = HolecardChanceNode(root, committed, holes, board, self.rules.deck, "", cur_round.holecards) - # Deal holecards - all_hc = self.deal_holecards(deck, cur_round.holecards, players_in.count(True)) - # Create a child node for every possible distribution - for cur_holes in all_hc: - dealt_cards = () - cur_holes = list(cur_holes) - cur_idx = 0 - for i,hc in enumerate(holes): - # Only deal cards to players who are still in - if players_in[i]: - cur_holes[cur_idx] = hc + cur_holes[cur_idx] - cur_idx += 1 - for hc in cur_holes: - dealt_cards += hc - cur_deck = filter(lambda x: not (x in dealt_cards), deck) - if cur_round.boardcards: - self.build_boardcards(hnode, next_player, players_in, committed, cur_holes, board, cur_deck, bet_history, round_idx, min_actions_this_round, actions_this_round, bets) - else: - self.build_bets(hnode, next_player, players_in, committed, cur_holes, board, cur_deck, bet_history, round_idx, min_actions_this_round, actions_this_round, bets) - return hnode - - def build_boardcards(self, root, next_player, players_in, committed, holes, board, deck, bet_history, round_idx, min_actions_this_round, actions_this_round, bets): - cur_round = self.rules.roundinfo[round_idx] - bnode = BoardcardChanceNode(root, committed, holes, board, deck, bet_history, cur_round.boardcards) - all_bc = combinations(deck, cur_round.boardcards) - for bc in all_bc: - cur_board = board + bc - cur_deck = filter(lambda x: not (x in bc), deck) - self.build_bets(bnode, next_player, players_in, committed, holes, cur_board, cur_deck, bet_history, round_idx, min_actions_this_round, actions_this_round, bets) - return bnode - - def build_bets(self, root, next_player, players_in, committed, holes, board, deck, bet_history, round_idx, min_actions_this_round, actions_this_round, bets_this_round): - # if everyone else folded, end the hand - if players_in.count(True) == 1: - self.showdown(root, players_in, committed, holes, board, deck, bet_history) - return - # if everyone checked or the last raisor has been called, end the round - if actions_this_round >= min_actions_this_round and self.all_called_last_raisor_or_folded(players_in, bets_this_round): - self.build_rounds(root, players_in, committed, holes, board, deck, bet_history, round_idx + 1) - return - cur_round = self.rules.roundinfo[round_idx] - anode = ActionNode(root, committed, holes, board, deck, bet_history, next_player, self.rules.infoset_format) - # add the node to the information set - if not (anode.player_view in self.information_sets): - self.information_sets[anode.player_view] = [] - self.information_sets[anode.player_view].append(anode) - # get the next player to act - next_player = self.get_next_player(next_player, players_in) - # add a folding option if someone has bet more than this player - if committed[anode.player] < max(committed): - self.add_fold_child(anode, next_player, players_in, committed, holes, board, deck, bet_history, round_idx, min_actions_this_round, actions_this_round, bets_this_round) - # add a calling/checking option - self.add_call_child(anode, next_player, players_in, committed, holes, board, deck, bet_history, round_idx, min_actions_this_round, actions_this_round, bets_this_round) - # add a raising option if this player has not reached their max bet level - if cur_round.maxbets[anode.player] > max(bets_this_round): - self.add_raise_child(anode, next_player, players_in, committed, holes, board, deck, bet_history, round_idx, min_actions_this_round, actions_this_round, bets_this_round) - return anode - - def all_called_last_raisor_or_folded(self, players_in, bets): - betlevel = max(bets) - for i,v in enumerate(bets): - if players_in[i] and bets[i] < betlevel: - return False - return True - - def add_fold_child(self, root, next_player, players_in, committed, holes, board, deck, bet_history, round_idx, min_actions_this_round, actions_this_round, bets_this_round): - players_in[root.player] = False - bet_history += 'f' - self.build_bets(root, next_player, players_in, committed, holes, board, deck, bet_history, round_idx, min_actions_this_round, actions_this_round + 1, bets_this_round) - root.fold_action = root.children[-1] - players_in[root.player] = True - - def add_call_child(self, root, next_player, players_in, committed, holes, board, deck, bet_history, round_idx, min_actions_this_round, actions_this_round, bets_this_round): - player_commit = committed[root.player] - player_bets = bets_this_round[root.player] - committed[root.player] = max(committed) - bets_this_round[root.player] = max(bets_this_round) - bet_history += 'c' - self.build_bets(root, next_player, players_in, committed, holes, board, deck, bet_history, round_idx, min_actions_this_round, actions_this_round + 1, bets_this_round) - root.call_action = root.children[-1] - committed[root.player] = player_commit - bets_this_round[root.player] = player_bets - - def add_raise_child(self, root, next_player, players_in, committed, holes, board, deck, bet_history, round_idx, min_actions_this_round, actions_this_round, bets_this_round): - cur_round = self.rules.roundinfo[round_idx] - prev_betlevel = bets_this_round[root.player] - prev_commit = committed[root.player] - bets_this_round[root.player] = max(bets_this_round) + 1 - committed[root.player] += (bets_this_round[root.player] - prev_betlevel) * cur_round.betsize - bet_history += 'r' - self.build_bets(root, next_player, players_in, committed, holes, board, deck, bet_history, round_idx, min_actions_this_round, actions_this_round + 1, bets_this_round) - root.raise_action = root.children[-1] - bets_this_round[root.player] = prev_betlevel - committed[root.player] = prev_commit - - def showdown(self, root, players_in, committed, holes, board, deck, bet_history): - if players_in.count(True) == 1: - winners = [i for i,v in enumerate(players_in) if v] - else: - scores = [self.rules.handeval(hc, board) for hc in holes] - winners = [] - maxscore = -1 - for i,s in enumerate(scores): - if players_in[i]: - if len(winners) == 0 or s > maxscore: - maxscore = s - winners = [i] - elif s == maxscore: - winners.append(i) - pot = sum(committed) - payoff = pot / float(len(winners)) - payoffs = [-x for x in committed] - for w in winners: - payoffs[w] += payoff - return TerminalNode(root, committed, holes, board, deck, bet_history, payoffs, players_in) - - def holecard_distributions(self): - x = Counter(combinations(self.rules.deck, self.holecards)) - d = float(sum(x.values())) - return zip(x.keys(),[y / d for y in x.values()]) - -def multi_infoset_format(base_infoset_format, player, holecards, board, bet_history): - return tuple([base_infoset_format(player, hc, board, bet_history) for hc in holecards]) - -class PublicTree(GameTree): - def __init__(self, rules): - GameTree.__init__(self, GameRules(rules.players, rules.deck, rules.roundinfo, rules.ante, rules.blinds, rules.handeval, partial(multi_infoset_format, rules.infoset_format))) - - def build(self): - # Assume everyone is in - players_in = [True] * self.rules.players - # Collect antes - committed = [self.rules.ante] * self.rules.players - bets = [0] * self.rules.players - # Collect blinds - next_player = self.collect_blinds(committed, bets, 0) - holes = [[()]] * self.rules.players - board = () - bet_history = "" - self.root = self.build_rounds(None, players_in, committed, holes, board, self.rules.deck, bet_history, 0, bets, next_player) - - - def build_holecards(self, root, next_player, players_in, committed, holes, board, deck, bet_history, round_idx, min_actions_this_round, actions_this_round, bets): - cur_round = self.rules.roundinfo[round_idx] - hnode = HolecardChanceNode(root, committed, holes, board, self.rules.deck, "", cur_round.holecards) - # Deal holecards - all_hc = list(combinations(deck, cur_round.holecards)) - updated_holes = [] - for player in range(self.rules.players): - if not players_in[player]: - # Only deal to players who are still in the hand - updated_holes.append([old_hc for old_hc in holes[player]]) - elif len(holes[player]) == 0: - # If this player has no cards, just set their holecards to be the newly dealt ones - updated_holes.append([new_hc for new_hc in all_hc]) - else: - updated_holes.append([]) - # Filter holecards to valid combinations - # TODO: Speed this up by removing duplicate holecard combinations - for new_hc in all_hc: - for old_hc in holes[player]: - if not overlap(old_hc, new_hc): - updated_holes[player].append(old_hc + new_hc) - if cur_round.boardcards: - self.build_boardcards(hnode, next_player, players_in, committed, updated_holes, board, deck, bet_history, round_idx, min_actions_this_round, actions_this_round, bets) - else: - self.build_bets(hnode, next_player, players_in, committed, updated_holes, board, deck, bet_history, round_idx, min_actions_this_round, actions_this_round, bets) - return hnode - - def build_boardcards(self, root, next_player, players_in, committed, holes, board, deck, bet_history, round_idx, min_actions_this_round, actions_this_round, bets): - cur_round = self.rules.roundinfo[round_idx] - bnode = BoardcardChanceNode(root, committed, holes, board, deck, bet_history, cur_round.boardcards) - all_bc = combinations(deck, cur_round.boardcards) - for bc in all_bc: - cur_board = board + bc - cur_deck = filter(lambda x: not (x in bc), deck) - updated_holes = [] - # Filter any holecards that are now impossible - for player in range(self.rules.players): - updated_holes.append([]) - for hc in holes[player]: - if not overlap(hc, bc): - updated_holes[player].append(hc) - self.build_bets(bnode, next_player, players_in, committed, updated_holes, cur_board, cur_deck, bet_history, round_idx, min_actions_this_round, actions_this_round, bets) - return bnode - - def showdown(self, root, players_in, committed, holes, board, deck, bet_history): - # TODO: Speedup - # - Pre-order list of hands - pot = sum(committed) - showdowns_possible = self.showdown_combinations(holes) - if players_in.count(True) == 1: - fold_payoffs = [-x for x in committed] - fold_payoffs[players_in.index(True)] += pot - payoffs = { hands: fold_payoffs for hands in showdowns_possible } - else: - scores = {} - for i in range(self.rules.players): - if players_in[i]: - for hc in holes[i]: - if not (hc in scores): - scores[hc] = self.rules.handeval(hc, board) - payoffs = { hands: self.calc_payoffs(hands, scores, players_in, committed, pot) for hands in showdowns_possible } - return TerminalNode(root, committed, holes, board, deck, bet_history, payoffs, players_in) - - def showdown_combinations(self, holes): - # Get all the possible holecard matchups for a given showdown. - # Every card must be unique because two players cannot have the same holecard. - return list(filter(lambda x: all_unique(x), product(*holes))) - - def calc_payoffs(self, hands, scores, players_in, committed, pot): - winners = [] - maxscore = -1 - for i,hand in enumerate(hands): - if players_in[i]: - s = scores[hand] - if len(winners) == 0 or s > maxscore: - maxscore = s - winners = [i] - elif s == maxscore: - winners.append(i) - payoff = pot / float(len(winners)) - payoffs = [-x for x in committed] - for w in winners: - payoffs[w] += payoff - return payoffs - -class Node(object): - def __init__(self, parent, committed, holecards, board, deck, bet_history): - self.committed = deepcopy(committed) - self.holecards = deepcopy(holecards) - self.board = deepcopy(board) - self.deck = deepcopy(deck) - self.bet_history = deepcopy(bet_history) - if parent: - self.parent = parent - self.parent.add_child(self) - - def add_child(self, child): - if self.children is None: - self.children = [child] - else: - self.children.append(child) - -class TerminalNode(Node): - def __init__(self, parent, committed, holecards, board, deck, bet_history, payoffs, players_in): - Node.__init__(self, parent, committed, holecards, board, deck, bet_history) - self.payoffs = payoffs - self.players_in = deepcopy(players_in) - -class HolecardChanceNode(Node): - def __init__(self, parent, committed, holecards, board, deck, bet_history, todeal): - Node.__init__(self, parent, committed, holecards, board, deck, bet_history) - self.todeal = todeal - self.children = [] - -class BoardcardChanceNode(Node): - def __init__(self, parent, committed, holecards, board, deck, bet_history, todeal): - Node.__init__(self, parent, committed, holecards, board, deck, bet_history) - self.todeal = todeal - self.children = [] - -class ActionNode(Node): - def __init__(self, parent, committed, holecards, board, deck, bet_history, player, infoset_format): - Node.__init__(self, parent, committed, holecards, board, deck, bet_history) - self.player = player - self.children = [] - self.raise_action = None - self.call_action = None - self.fold_action = None - self.player_view = infoset_format(player, holecards[player], board, bet_history) - - def valid(self, action): - if action == FOLD: - return self.fold_action - if action == CALL: - return self.call_action - if action == RAISE: - return self.raise_action - raise Exception("Unknown action {0}. Action must be FOLD, CALL, or RAISE".format(action)) - - def get_child(self, action): - return self.valid(action) diff --git a/popcount.py b/popcount.py deleted file mode 100644 index 2834e6f..0000000 --- a/popcount.py +++ /dev/null @@ -1,14 +0,0 @@ -class PopCount: - # Generate table of popcounts for 16 bits - # Then just use it twice. - # Reference is some stanford paper. - # See http://www.valuedlessons.com/2009/01/popcount-in-python-with-benchmarks.html - POPCOUNT_TABLE16 = [0] * 2**16 - for index in xrange(len(POPCOUNT_TABLE16)): - POPCOUNT_TABLE16[index] = (index & 1) + POPCOUNT_TABLE16[index >> 1] - - def popcount32_table16(v): - return (PopCount.POPCOUNT_TABLE16[v & 0xffff] + \ - PopCount.POPCOUNT_TABLE16[(v >> 16) & 0xffff]) - - popcount = staticmethod(popcount32_table16) \ No newline at end of file diff --git a/pycfr/__init__.py b/pycfr/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/card.py b/pycfr/card.py similarity index 76% rename from card.py rename to pycfr/card.py index 5e1cfef..6b6aff1 100644 --- a/card.py +++ b/pycfr/card.py @@ -1,5 +1,6 @@ import re + class Card: SUIT_TO_STRING = { 1: "s", @@ -7,7 +8,7 @@ class Card: 3: "d", 4: "c" } - + RANK_TO_STRING = { 2: "2", 3: "3", @@ -28,34 +29,35 @@ class Card: RANK_QUEEN = 12 RANK_KING = 13 RANK_ACE = 14 - + STRING_TO_SUIT = dict([(v, k) for k, v in SUIT_TO_STRING.iteritems()]) STRING_TO_RANK = dict([(v, k) for k, v in RANK_TO_STRING.iteritems()]) - + REPR_RE = re.compile(r'\((.*?)\)') - + def __init__(self, rank, suit): """Create a card. Rank is 2-14, representing 2-A, while suit is 1-4 representing spades, hearts, diamonds, clubs""" self.rank = rank self.suit = suit - + def __repr__(self): - return "%s%s" % (self.RANK_TO_STRING[self.rank], self.SUIT_TO_STRING[self.suit]) - + return "%s%s" % (self.RANK_TO_STRING[self.rank], + self.SUIT_TO_STRING[self.suit]) + def __eq__(self, other): - return (isinstance(other, self.__class__) and self.rank == other.rank and self.suit == other.suit) - + return isinstance(other, self.__class__) and self.rank == other.rank\ + and self.suit == other.suit + def __hash__(self): return hash((self.rank, self.suit)) - + @classmethod - def from_repr(cls, repr): - """Return a card instance from repr. + def from_repr(cls, rpr): + """Return a card instance from rpr. This is really dirty--it just matches between the parens. It's meant for debugging.""" - between_parens = re.search(cls.REPR_RE, repr).group(1) + between_parens = re.search(cls.REPR_RE, rpr).group(1) rank = cls.STRING_TO_RANK[between_parens[0].upper()] suit = cls.STRING_TO_SUIT[between_parens[1].lower()] return Card(rank, suit) - \ No newline at end of file diff --git a/hand_evaluator.py b/pycfr/hand_evaluator.py similarity index 85% rename from hand_evaluator.py rename to pycfr/hand_evaluator.py index 29638ae..e435c62 100644 --- a/hand_evaluator.py +++ b/pycfr/hand_evaluator.py @@ -3,19 +3,23 @@ from itertools import combinations from operator import mul, __or__, __and__, __xor__ + class HandLengthException(Exception): pass + class HandEvaluator: - + class Two: + def evaluate_percentile(hand): """ Using lookup table, return percentile of your hand with two cards """ if len(hand) != 2: - raise HandLengthException("Only 2-card hands are supported by the Two evaluator") - + raise HandLengthException( + "Only 2-card hands are supported by the Two evaluator") + if hand[0].suit == hand[1].suit: if hand[0].rank < hand[1].rank: return LookupTables.Two.suited_ranks_to_percentile[hand[0].rank][hand[1].rank] @@ -24,8 +28,9 @@ def evaluate_percentile(hand): else: return LookupTables.Two.unsuited_ranks_to_percentile[hand[0].rank][hand[1].rank] evaluate_percentile = staticmethod(evaluate_percentile) - + class Five: + def card_to_binary(card): """ Convert the lookup_tables.Card representation to a binary @@ -62,8 +67,9 @@ def evaluate_rank(hand): Return the rank of this 5-card hand amongst all 5-card hands. """ if len(hand) != 5: - raise HandLengthException("Only 5-card hands are supported by the Five evaluator") - + raise HandLengthException( + "Only 5-card hands are supported by the Five evaluator") + # This implementation uses the binary representation from # card_to_binary card_to_binary = HandEvaluator.Five.card_to_binary_lookup @@ -96,8 +102,9 @@ def evaluate_rank(hand): card_to_binary = staticmethod(card_to_binary) card_to_binary_lookup = staticmethod(card_to_binary_lookup) evaluate_rank = staticmethod(evaluate_rank) - + class Six: + def card_to_binary(card): """ Convert the lookup_tables.Card representation to a binary @@ -106,11 +113,11 @@ def card_to_binary(card): # This a variant on Cactus Kev's algorithm. We need to replace # the 4-bit representation of suit with a prime number representation # so we can look up whether something is a flush by prime product - + # First we need to generate the following representation # Bits marked x are not used. # xxxbbbbb bbbbbbbb qqqqrrrr xxpppppp - + # b is one bit flipped for A-2 # q is 2, 3, 5, or 7 for spades, hearts, clubs, diamonds # r is just the numerical rank in binary, with deuce = 0 @@ -123,31 +130,34 @@ def card_to_binary(card): p_mask = LookupTables.primes[card.rank - 2] # OR them together to get the final result return b_mask | q_mask | r_mask | p_mask - + def card_to_binary_lookup(card): return LookupTables.Six.card_to_binary[card.rank][card.suit] - + def evaluate_rank(hand): """ Return the rank amongst all possible 5-card hands of any kind using the best 5-card hand from the given 6-card hand. """ if len(hand) != 6: - raise HandLengthException("Only 6-card hands are supported by the Six evaluator") - + raise HandLengthException( + "Only 6-card hands are supported by the Six evaluator") + # bh stands for binary hand, map to that representation card_to_binary = HandEvaluator.Six.card_to_binary_lookup bh = map(card_to_binary, hand) - + # We can determine if it's a flush using a lookup table. # Basically use prime number trick but map to bool instead of rank # Once you have a flush, there is no other higher hand you can make - # except straight flush, so just need to determine the highest flush + # except straight flush, so just need to determine the highest + # flush flush_prime = reduce(mul, map(lambda card: (card >> 12) & 0xF, bh)) flush_suit = False if flush_prime in LookupTables.Six.prime_products_to_flush: - flush_suit = LookupTables.Six.prime_products_to_flush[flush_prime] - + flush_suit = LookupTables.Six.prime_products_to_flush[ + flush_prime] + # Now use ranks to determine hand via lookup odd_xor = reduce(__xor__, bh) >> 16 even_xor = (reduce(__or__, bh) >> 16) ^ odd_xor @@ -166,9 +176,9 @@ def evaluate_rank(hand): # you have a pair, one card in the flush suit, # so just use the ranks you have by or'ing the two return LookupTables.Six.flush_rank_bits_to_rank[odd_xor | even_xor] - + # Otherwise, get ready for a wild ride: - + # Can determine this by using 2 XORs to reduce the size of the # lookup. You have an even number of cards, so any odd_xor with # an odd number of bits set is not possible. @@ -189,42 +199,46 @@ def evaluate_rank(hand): # Look up by even_xor # 0-2 => Four of a kind with pair (2,4) # Look up by prime product - + # Any time you can't disambiguate 2/4 or 1/3, use primes. # We also assume you can count bits or determine a power of two. # (see PopCount class.) - - if even_xor == 0: # x-0 + + if even_xor == 0: # x-0 odd_popcount = PopCount.popcount(odd_xor) - if odd_popcount == 4: # 4-0 - prime_product = reduce(mul, map(lambda card: card & 0xFF, bh)) + if odd_popcount == 4: # 4-0 + prime_product = reduce( + mul, map(lambda card: card & 0xFF, bh)) return LookupTables.Six.prime_products_to_rank[prime_product] - else: # 6-0, 2-0 + else: # 6-0, 2-0 return LookupTables.Six.odd_xors_to_rank[odd_xor] - elif odd_xor == 0: # 0-x + elif odd_xor == 0: # 0-x even_popcount = PopCount.popcount(even_xor) - if even_popcount == 2: # 0-2 - prime_product = reduce(mul, map(lambda card: card & 0xFF, bh)) + if even_popcount == 2: # 0-2 + prime_product = reduce( + mul, map(lambda card: card & 0xFF, bh)) return LookupTables.Six.prime_products_to_rank[prime_product] - else: # 0-3 + else: # 0-3 return LookupTables.Six.even_xors_to_rank[even_xor] - else: # odd_popcount is 4 or 2 + else: # odd_popcount is 4 or 2 odd_popcount = PopCount.popcount(odd_xor) - if odd_popcount == 4: # 4-1 + if odd_popcount == 4: # 4-1 return LookupTables.Six.even_xors_to_odd_xors_to_rank[even_xor][odd_xor] - else: # 2-x + else: # 2-x even_popcount = PopCount.popcount(even_xor) - if even_popcount == 2: # 2-2 + if even_popcount == 2: # 2-2 return LookupTables.Six.even_xors_to_odd_xors_to_rank[even_xor][odd_xor] - else: # 2-1 - prime_product = reduce(mul, map(lambda card: card & 0xFF, bh)) + else: # 2-1 + prime_product = reduce( + mul, map(lambda card: card & 0xFF, bh)) return LookupTables.Six.prime_products_to_rank[prime_product] card_to_binary = staticmethod(card_to_binary) card_to_binary_lookup = staticmethod(card_to_binary_lookup) evaluate_rank = staticmethod(evaluate_rank) - + class Seven: + def card_to_binary(card): """ Convert the lookup_tables.Card representation to a binary @@ -239,25 +253,27 @@ def card_to_binary(card): def card_to_binary_lookup(card): return LookupTables.Seven.card_to_binary[card.rank][card.suit] - + def evaluate_rank(hand): """ Return the rank amongst all possible 5-card hands of any kind using the best 5-card hand from the given 6-card hand. """ if len(hand) != 7: - raise HandLengthException("Only 7-card hands are supported by the Seven evaluator") - + raise HandLengthException( + "Only 7-card hands are supported by the Seven evaluator") + # bh stands for binary hand, map to that representation card_to_binary = HandEvaluator.Seven.card_to_binary_lookup bh = map(card_to_binary, hand) - + # Use a lookup table to determine if it's a flush as with 6 cards flush_prime = reduce(mul, map(lambda card: (card >> 12) & 0xF, bh)) flush_suit = False if flush_prime in LookupTables.Seven.prime_products_to_flush: - flush_suit = LookupTables.Seven.prime_products_to_flush[flush_prime] - + flush_suit = LookupTables.Seven.prime_products_to_flush[ + flush_prime] + # Now use ranks to determine hand via lookup odd_xor = reduce(__xor__, bh) >> 16 even_xor = (reduce(__or__, bh) >> 16) ^ odd_xor @@ -281,7 +297,7 @@ def evaluate_rank(hand): filter( lambda card: (card >> 12) & 0xF == flush_suit, bh))) return LookupTables.Seven.flush_rank_bits_to_rank[bits] - + # Odd-even XOR again, see Six.evaluate_rank for details # 7 is odd, so you have to have an odd number of bits in odd_xor # 7-0 => (1,1,1,1,1,1,1) - High card @@ -293,33 +309,36 @@ def evaluate_rank(hand): # 1-3 => (1,2,2,2) - Two pair # 1-2 => (1,2,4) or (3,2,2) - Quads or full house # 1-1 => (3,4) - Quads - - if even_xor == 0: # x-0 + + if even_xor == 0: # x-0 odd_popcount = PopCount.popcount(odd_xor) - if odd_popcount == 7: # 7-0 + if odd_popcount == 7: # 7-0 return LookupTables.Seven.odd_xors_to_rank[odd_xor] - else: # 5-0, 3-0 - prime_product = reduce(mul, map(lambda card: card & 0xFF, bh)) + else: # 5-0, 3-0 + prime_product = reduce( + mul, map(lambda card: card & 0xFF, bh)) return LookupTables.Seven.prime_products_to_rank[prime_product] else: odd_popcount = PopCount.popcount(odd_xor) - if odd_popcount == 5: # 5-1 + if odd_popcount == 5: # 5-1 return LookupTables.Seven.even_xors_to_odd_xors_to_rank[even_xor][odd_xor] elif odd_popcount == 3: even_popcount = PopCount.popcount(even_xor) - if even_popcount == 2: # 3-2 + if even_popcount == 2: # 3-2 return LookupTables.Seven.even_xors_to_odd_xors_to_rank[even_xor][odd_xor] - else: # 3-1 - prime_product = reduce(mul, map(lambda card: card & 0xFF, bh)) + else: # 3-1 + prime_product = reduce( + mul, map(lambda card: card & 0xFF, bh)) return LookupTables.Seven.prime_products_to_rank[prime_product] else: even_popcount = PopCount.popcount(even_xor) - if even_popcount == 3: # 1-3 + if even_popcount == 3: # 1-3 return LookupTables.Seven.even_xors_to_odd_xors_to_rank[even_xor][odd_xor] - elif even_popcount == 2: # 1-2 - prime_product = reduce(mul, map(lambda card: card & 0xFF, bh)) + elif even_popcount == 2: # 1-2 + prime_product = reduce( + mul, map(lambda card: card & 0xFF, bh)) return LookupTables.Seven.prime_products_to_rank[prime_product] - else: # 1-1 + else: # 1-1 return LookupTables.Seven.even_xors_to_odd_xors_to_rank[even_xor][odd_xor] card_to_binary = staticmethod(card_to_binary) card_to_binary_lookup = staticmethod(card_to_binary_lookup) @@ -332,10 +351,11 @@ def evaluate_hand(hand, board=[]): cards, against an equivalent number of cards. """ hand_lengths = [2] - + if len(hand) not in hand_lengths: - raise HandLengthException("Only %s hole cards are supported" % ", ".join(map(str, hand_lengths))) - + raise HandLengthException( + "Only %s hole cards are supported" % ", ".join(map(str, hand_lengths))) + cards = list(hand) + list(board) if len(cards) == 2: return HandEvaluator.Two.evaluate_percentile(hand) @@ -347,15 +367,17 @@ def evaluate_hand(hand, board=[]): evaluator = HandEvaluator.Seven else: # wrong number of cards - raise HandLengthException("Only 2, 5, 6, 7 cards total are supported by evaluate_hand") + raise HandLengthException( + "Only 2, 5, 6, 7 cards total are supported by evaluate_hand") # Default values in case we screw up rank = 7463 percentile = 0.0 - + rank = evaluator.evaluate_rank(cards) - - possible_opponent_hands = list(combinations(LookupTables.deck - set(cards), len(hand))) + + possible_opponent_hands = list( + combinations(LookupTables.deck - set(cards), len(hand))) hands_beaten = 0 for h in possible_opponent_hands: possible_opponent_rank = evaluator.evaluate_rank(list(h) + board) @@ -366,4 +388,4 @@ def evaluate_hand(hand, board=[]): hands_beaten += 0.5 return float(hands_beaten) / len(list(possible_opponent_hands)) - evaluate_hand = staticmethod(evaluate_hand) \ No newline at end of file + evaluate_hand = staticmethod(evaluate_hand) diff --git a/lookup_tables.py b/pycfr/lookup_tables.py similarity index 100% rename from lookup_tables.py rename to pycfr/lookup_tables.py diff --git a/pokercfr.py b/pycfr/pokercfr.py similarity index 64% rename from pokercfr.py rename to pycfr/pokercfr.py index dc887a6..2257cdb 100644 --- a/pokercfr.py +++ b/pycfr/pokercfr.py @@ -2,11 +2,15 @@ from pokerstrategy import * import random + class CounterfactualRegretMinimizer(object): + def __init__(self, rules): self.rules = rules - self.profile = StrategyProfile(rules, [Strategy(i) for i in range(rules.players)]) - self.current_profile = StrategyProfile(rules, [Strategy(i) for i in range(rules.players)]) + self.profile = StrategyProfile( + rules, [Strategy(i) for i in range(rules.players)]) + self.current_profile = StrategyProfile( + rules, [Strategy(i) for i in range(rules.players)]) self.iterations = 0 self.counterfactual_regret = [] self.action_reachprobs = [] @@ -15,8 +19,10 @@ def __init__(self, rules): print 'Information sets: {0}'.format(len(self.tree.information_sets)) for s in self.profile.strategies: s.build_default(self.tree) - self.counterfactual_regret.append({ infoset: [0,0,0] for infoset in s.policy }) - self.action_reachprobs.append({ infoset: [0,0,0] for infoset in s.policy }) + self.counterfactual_regret.append( + {infoset: [0, 0, 0] for infoset in s.policy}) + self.action_reachprobs.append( + {infoset: [0, 0, 0] for infoset in s.policy}) def run(self, num_iterations): for iteration in range(num_iterations): @@ -24,7 +30,8 @@ def run(self, num_iterations): self.iterations += 1 def cfr(self): - self.cfr_helper(self.tree.root, [{(): 1} for _ in range(self.rules.players)]) + self.cfr_helper(self.tree.root, [{(): 1} + for _ in range(self.rules.players)]) def cfr_helper(self, root, reachprobs): if type(root) is TerminalNode: @@ -40,17 +47,17 @@ def cfr_terminal_node(self, root, reachprobs): for player in range(self.rules.players): player_payoffs = {hc: 0 for hc in root.holecards[player]} counts = {hc: 0 for hc in root.holecards[player]} - for hands,winnings in root.payoffs.items(): + for hands, winnings in root.payoffs.items(): prob = 1.0 player_hc = None - for opp,hc in enumerate(hands): + for opp, hc in enumerate(hands): if opp == player: player_hc = hc else: prob *= reachprobs[opp][hc] player_payoffs[player_hc] += prob * winnings[player] counts[player_hc] += 1 - for hc,count in counts.items(): + for hc, count in counts.items(): if count > 0: player_payoffs[hc] /= float(count) payoffs[player] = player_payoffs @@ -59,25 +66,29 @@ def cfr_terminal_node(self, root, reachprobs): def cfr_holecard_node(self, root, reachprobs): assert(len(root.children) == 1) prevlen = len(reachprobs[0].keys()[0]) - possible_deals = float(choose(len(root.deck) - prevlen,root.todeal)) - next_reachprobs = [{ hc: reachprobs[player][hc[0:prevlen]] / possible_deals for hc in root.children[0].holecards[player] } for player in range(self.rules.players)] + possible_deals = float(choose(len(root.deck) - prevlen, root.todeal)) + next_reachprobs = [{hc: reachprobs[player][hc[0:prevlen]] / possible_deals for hc in root.children[0].holecards[player]} + for player in range(self.rules.players)] subpayoffs = self.cfr_helper(root.children[0], next_reachprobs) - payoffs = [{ hc: 0 for hc in root.holecards[player] } for player in range(self.rules.players)] + payoffs = [{hc: 0 for hc in root.holecards[player]} + for player in range(self.rules.players)] for player, subpayoff in enumerate(subpayoffs): - for hand,winnings in subpayoff.items(): + for hand, winnings in subpayoff.items(): hc = hand[0:prevlen] payoffs[player][hc] += winnings return payoffs def cfr_boardcard_node(self, root, reachprobs): prevlen = len(reachprobs[0].keys()[0]) - possible_deals = float(choose(len(root.deck) - prevlen,root.todeal)) - payoffs = [{ hc: 0 for hc in root.holecards[player] } for player in range(self.rules.players)] + possible_deals = float(choose(len(root.deck) - prevlen, root.todeal)) + payoffs = [{hc: 0 for hc in root.holecards[player]} + for player in range(self.rules.players)] for bc in root.children: - next_reachprobs = [{ hc: reachprobs[player][hc] / possible_deals for hc in bc.holecards[player] } for player in range(self.rules.players)] + next_reachprobs = [{hc: reachprobs[player][hc] / possible_deals for hc in bc.holecards[player]} + for player in range(self.rules.players)] subpayoffs = self.cfr_helper(bc, next_reachprobs) - for player,subpayoff in enumerate(subpayoffs): - for hand,winnings in subpayoff.items(): + for player, subpayoff in enumerate(subpayoffs): + for hand, winnings in subpayoff.items(): payoffs[player][hand] += winnings return payoffs @@ -85,25 +96,33 @@ def cfr_action_node(self, root, reachprobs): # Calculate strategy from counterfactual regret strategy = self.cfr_strategy_update(root, reachprobs) next_reachprobs = deepcopy(reachprobs) - action_probs = { hc: strategy.probs(self.rules.infoset_format(root.player, hc, root.board, root.bet_history)) for hc in reachprobs[root.player] } + action_probs = {hc: strategy.probs(self.rules.infoset_format(root.player, hc, root.board, root.bet_history)) + for hc in reachprobs[root.player]} action_payoffs = [None, None, None] if root.fold_action: - next_reachprobs[root.player] = { hc: action_probs[hc][FOLD] * reachprobs[root.player][hc] for hc in reachprobs[root.player] } - action_payoffs[FOLD] = self.cfr_helper(root.fold_action, next_reachprobs) + next_reachprobs[root.player] = {hc: action_probs[hc][FOLD] * reachprobs[root.player][hc] + for hc in reachprobs[root.player]} + action_payoffs[FOLD] = self.cfr_helper( + root.fold_action, next_reachprobs) if root.call_action: - next_reachprobs[root.player] = { hc: action_probs[hc][CALL] * reachprobs[root.player][hc] for hc in reachprobs[root.player] } - action_payoffs[CALL] = self.cfr_helper(root.call_action, next_reachprobs) + next_reachprobs[root.player] = {hc: action_probs[hc][CALL] * reachprobs[root.player][hc] + for hc in reachprobs[root.player]} + action_payoffs[CALL] = self.cfr_helper( + root.call_action, next_reachprobs) if root.raise_action: - next_reachprobs[root.player] = { hc: action_probs[hc][RAISE] * reachprobs[root.player][hc] for hc in reachprobs[root.player] } - action_payoffs[RAISE] = self.cfr_helper(root.raise_action, next_reachprobs) + next_reachprobs[root.player] = {hc: action_probs[hc][RAISE] * reachprobs[root.player][hc] + for hc in reachprobs[root.player]} + action_payoffs[RAISE] = self.cfr_helper( + root.raise_action, next_reachprobs) payoffs = [] for player in range(self.rules.players): - player_payoffs = { hc: 0 for hc in reachprobs[player] } - for i,subpayoff in enumerate(action_payoffs): + player_payoffs = {hc: 0 for hc in reachprobs[player]} + for i, subpayoff in enumerate(action_payoffs): if subpayoff is None: continue - for hc,winnings in subpayoff[player].iteritems(): - # action_probs is baked into reachprobs for everyone except the acting player + for hc, winnings in subpayoff[player].iteritems(): + # action_probs is baked into reachprobs for everyone except + # the acting player if player == root.player: player_payoffs[hc] += winnings * action_probs[hc][i] else: @@ -117,37 +136,45 @@ def cfr_strategy_update(self, root, reachprobs): if self.iterations == 0: default_strat = self.profile.strategies[root.player] for hc in root.holecards[root.player]: - infoset = self.rules.infoset_format(root.player, hc, root.board, root.bet_history) + infoset = self.rules.infoset_format( + root.player, hc, root.board, root.bet_history) probs = default_strat.probs(infoset) for i in range(3): - self.action_reachprobs[root.player][infoset][i] += reachprobs[root.player][hc] * probs[i] + self.action_reachprobs[root.player][infoset][ + i] += reachprobs[root.player][hc] * probs[i] return default_strat for hc in root.holecards[root.player]: - infoset = self.rules.infoset_format(root.player, hc, root.board, root.bet_history) + infoset = self.rules.infoset_format( + root.player, hc, root.board, root.bet_history) prev_cfr = self.counterfactual_regret[root.player][infoset] - sumpos_cfr = sum([max(0,x) for x in prev_cfr]) + sumpos_cfr = sum([max(0, x) for x in prev_cfr]) if sumpos_cfr == 0: probs = self.equal_probs(root) else: - probs = [max(0,x) / sumpos_cfr for x in prev_cfr] - self.current_profile.strategies[root.player].policy[infoset] = probs + probs = [max(0, x) / sumpos_cfr for x in prev_cfr] + self.current_profile.strategies[ + root.player].policy[infoset] = probs for i in range(3): - self.action_reachprobs[root.player][infoset][i] += reachprobs[root.player][hc] * probs[i] - self.profile.strategies[root.player].policy[infoset] = [self.action_reachprobs[root.player][infoset][i] / sum(self.action_reachprobs[root.player][infoset]) for i in range(3)] + self.action_reachprobs[root.player][infoset][ + i] += reachprobs[root.player][hc] * probs[i] + self.profile.strategies[root.player].policy[infoset] = [self.action_reachprobs[root.player][ + infoset][i] / sum(self.action_reachprobs[root.player][infoset]) for i in range(3)] return self.current_profile.strategies[root.player] def cfr_regret_update(self, root, action_payoffs, ev): - for i,subpayoff in enumerate(action_payoffs): + for i, subpayoff in enumerate(action_payoffs): if subpayoff is None: continue - for hc,winnings in subpayoff[root.player].iteritems(): + for hc, winnings in subpayoff[root.player].iteritems(): immediate_cfr = winnings - ev[hc] - infoset = self.rules.infoset_format(root.player, hc, root.board, root.bet_history) - self.counterfactual_regret[root.player][infoset][i] += immediate_cfr + infoset = self.rules.infoset_format( + root.player, hc, root.board, root.bet_history) + self.counterfactual_regret[root.player][ + infoset][i] += immediate_cfr def equal_probs(self, root): total_actions = len(root.children) - probs = [0,0,0] + probs = [0, 0, 0] if root.fold_action: probs[FOLD] = 1.0 / total_actions if root.call_action: @@ -158,33 +185,36 @@ def equal_probs(self, root): class PublicChanceSamplingCFR(CounterfactualRegretMinimizer): + def __init__(self, rules): CounterfactualRegretMinimizer.__init__(self, rules) def cfr(self): # Sample all board cards to be used - self.board = random.sample(self.rules.deck, sum([x.boardcards for x in self.rules.roundinfo])) + self.board = random.sample( + self.rules.deck, sum([x.boardcards for x in self.rules.roundinfo])) # Call the standard CFR algorithm - self.cfr_helper(self.tree.root, [{(): 1} for _ in range(self.rules.players)]) + self.cfr_helper(self.tree.root, [{(): 1} + for _ in range(self.rules.players)]) def cfr_terminal_node(self, root, reachprobs): payoffs = [None for _ in range(self.rules.players)] for player in range(self.rules.players): player_payoffs = {hc: 0 for hc in reachprobs[player]} counts = {hc: 0 for hc in reachprobs[player]} - for hands,winnings in root.payoffs.items(): + for hands, winnings in root.payoffs.items(): if not self.terminal_match(hands): continue prob = 1.0 player_hc = None - for opp,hc in enumerate(hands): + for opp, hc in enumerate(hands): if opp == player: player_hc = hc else: prob *= reachprobs[opp][hc] player_payoffs[player_hc] += prob * winnings[player] counts[player_hc] += 1 - for hc,count in counts.items(): + for hc, count in counts.items(): if count > 0: player_payoffs[hc] /= float(count) payoffs[player] = player_payoffs @@ -199,12 +229,15 @@ def terminal_match(self, hands): def cfr_holecard_node(self, root, reachprobs): assert(len(root.children) == 1) prevlen = len(reachprobs[0].keys()[0]) - possible_deals = float(choose(len(root.deck) - len(self.board) - prevlen,root.todeal)) - next_reachprobs = [{ hc: reachprobs[player][hc[0:prevlen]] / possible_deals for hc in root.children[0].holecards[player] if not self.has_boardcard(hc) } for player in range(self.rules.players)] + possible_deals = float( + choose(len(root.deck) - len(self.board) - prevlen, root.todeal)) + next_reachprobs = [{hc: reachprobs[player][hc[0:prevlen]] / possible_deals for hc in root.children[ + 0].holecards[player] if not self.has_boardcard(hc)} for player in range(self.rules.players)] subpayoffs = self.cfr_helper(root.children[0], next_reachprobs) - payoffs = [{ hc: 0 for hc in reachprobs[player] } for player in range(self.rules.players)] + payoffs = [{hc: 0 for hc in reachprobs[player]} + for player in range(self.rules.players)] for player, subpayoff in enumerate(subpayoffs): - for hand,winnings in subpayoff.items(): + for hand, winnings in subpayoff.items(): hc = hand[0:prevlen] payoffs[player][hc] += winnings return payoffs @@ -222,11 +255,11 @@ def cfr_boardcard_node(self, root, reachprobs): for bc in root.children: if self.boardmatch(num_dealt, bc): # Update the probabilities for each HC. Assume chance prob = 1 and renormalize reach probs by new holecard range - #next_reachprobs = [{ hc: reachprobs[player][hc] for hc in reachprobs[player] } for player in range(self.rules.players)] - #sumprobs = [sum(next_reachprobs[player].values()) for player in range(self.rules.players)] - #if min(sumprobs) == 0: + # next_reachprobs = [{ hc: reachprobs[player][hc] for hc in reachprobs[player] } for player in range(self.rules.players)] + # sumprobs = [sum(next_reachprobs[player].values()) for player in range(self.rules.players)] + # if min(sumprobs) == 0: # return [{ hc: 0 for hc in reachprobs[player] } for player in range(self.rules.players)] - #next_reachprobs = [{ hc: reachprobs[player][hc] / sumprobs[player] for hc in bc.holecards[player] } for player in range(self.rules.players)] + # next_reachprobs = [{ hc: reachprobs[player][hc] / sumprobs[player] for hc in bc.holecards[player] } for player in range(self.rules.players)] # Perform normal CFR results = self.cfr_helper(bc, reachprobs) # Return the payoffs @@ -243,33 +276,41 @@ def boardmatch(self, num_dealt, node): def cfr_strategy_update(self, root, reachprobs): # Update the strategies and regrets for each infoset for hc in reachprobs[root.player]: - infoset = self.rules.infoset_format(root.player, hc, root.board, root.bet_history) + infoset = self.rules.infoset_format( + root.player, hc, root.board, root.bet_history) # Get the current CFR prev_cfr = self.counterfactual_regret[root.player][infoset] # Get the total positive CFR - sumpos_cfr = float(sum([max(0,x) for x in prev_cfr])) + sumpos_cfr = float(sum([max(0, x) for x in prev_cfr])) if sumpos_cfr == 0: # Default strategy is equal probability probs = self.equal_probs(root) else: - # Use the strategy that's proportional to accumulated positive CFR - probs = [max(0,x) / sumpos_cfr for x in prev_cfr] + # Use the strategy that's proportional to accumulated positive + # CFR + probs = [max(0, x) / sumpos_cfr for x in prev_cfr] # Use the updated strategy as our current strategy - self.current_profile.strategies[root.player].policy[infoset] = probs - # Update the weighted policy probabilities (used to recover the average strategy) + self.current_profile.strategies[ + root.player].policy[infoset] = probs + # Update the weighted policy probabilities (used to recover the + # average strategy) for i in range(3): - self.action_reachprobs[root.player][infoset][i] += reachprobs[root.player][hc] * probs[i] + self.action_reachprobs[root.player][infoset][ + i] += reachprobs[root.player][hc] * probs[i] if sum(self.action_reachprobs[root.player][infoset]) == 0: # Default strategy is equal weight - self.profile.strategies[root.player].policy[infoset] = self.equal_probs(root) + self.profile.strategies[root.player].policy[ + infoset] = self.equal_probs(root) else: # Recover the weighted average strategy - self.profile.strategies[root.player].policy[infoset] = [self.action_reachprobs[root.player][infoset][i] / sum(self.action_reachprobs[root.player][infoset]) for i in range(3)] + self.profile.strategies[root.player].policy[infoset] = [self.action_reachprobs[root.player][ + infoset][i] / sum(self.action_reachprobs[root.player][infoset]) for i in range(3)] # Return and use the current CFR strategy return self.current_profile.strategies[root.player] class ChanceSamplingCFR(CounterfactualRegretMinimizer): + def __init__(self, rules): CounterfactualRegretMinimizer.__init__(self, rules) @@ -277,9 +318,11 @@ def cfr(self): # Sample all cards to be used holecards_per_player = sum([x.holecards for x in self.rules.roundinfo]) boardcards_per_hand = sum([x.boardcards for x in self.rules.roundinfo]) - todeal = random.sample(self.rules.deck, boardcards_per_hand + holecards_per_player * self.rules.players) + todeal = random.sample( + self.rules.deck, boardcards_per_hand + holecards_per_player * self.rules.players) # Deal holecards - self.holecards = [tuple(todeal[p*holecards_per_player:(p+1)*holecards_per_player]) for p in range(self.rules.players)] + self.holecards = [tuple(todeal[p * holecards_per_player:(p + 1) * holecards_per_player]) + for p in range(self.rules.players)] self.board = tuple(todeal[-boardcards_per_hand:]) # Set the top card of the deck self.top_card = len(todeal) - boardcards_per_hand @@ -288,12 +331,12 @@ def cfr(self): def cfr_terminal_node(self, root, reachprobs): payoffs = [0 for _ in range(self.rules.players)] - for hands,winnings in root.payoffs.items(): + for hands, winnings in root.payoffs.items(): if not self.terminal_match(hands): continue for player in range(self.rules.players): prob = 1.0 - for opp,hc in enumerate(hands): + for opp, hc in enumerate(hands): if opp != player: prob *= reachprobs[opp] payoffs[player] = prob * winnings[player] @@ -316,7 +359,7 @@ def hcmatch(self, hc, player): def cfr_holecard_node(self, root, reachprobs): assert(len(root.children) == 1) return self.cfr_helper(root.children[0], reachprobs) - + def cfr_boardcard_node(self, root, reachprobs): # Number of community cards dealt this round num_dealt = len(root.children[0].board) - len(root.board) @@ -341,23 +384,31 @@ def cfr_action_node(self, root, reachprobs): strategy = self.cfr_strategy_update(root, reachprobs) next_reachprobs = deepcopy(reachprobs) hc = self.holecards[root.player][0:len(root.holecards[root.player])] - action_probs = strategy.probs(self.rules.infoset_format(root.player, hc, root.board, root.bet_history)) + action_probs = strategy.probs( + self.rules.infoset_format(root.player, hc, root.board, root.bet_history)) action_payoffs = [None, None, None] if root.fold_action: - next_reachprobs[root.player] = action_probs[FOLD] * reachprobs[root.player] - action_payoffs[FOLD] = self.cfr_helper(root.fold_action, next_reachprobs) + next_reachprobs[root.player] = action_probs[ + FOLD] * reachprobs[root.player] + action_payoffs[FOLD] = self.cfr_helper( + root.fold_action, next_reachprobs) if root.call_action: - next_reachprobs[root.player] = action_probs[CALL] * reachprobs[root.player] - action_payoffs[CALL] = self.cfr_helper(root.call_action, next_reachprobs) + next_reachprobs[root.player] = action_probs[ + CALL] * reachprobs[root.player] + action_payoffs[CALL] = self.cfr_helper( + root.call_action, next_reachprobs) if root.raise_action: - next_reachprobs[root.player] = action_probs[RAISE] * reachprobs[root.player] - action_payoffs[RAISE] = self.cfr_helper(root.raise_action, next_reachprobs) + next_reachprobs[root.player] = action_probs[ + RAISE] * reachprobs[root.player] + action_payoffs[RAISE] = self.cfr_helper( + root.raise_action, next_reachprobs) payoffs = [0 for player in range(self.rules.players)] - for i,subpayoff in enumerate(action_payoffs): + for i, subpayoff in enumerate(action_payoffs): if subpayoff is None: continue - for player,winnings in enumerate(subpayoff): - # action_probs is baked into reachprobs for everyone except the acting player + for player, winnings in enumerate(subpayoff): + # action_probs is baked into reachprobs for everyone except the + # acting player if player == root.player: payoffs[player] += winnings * action_probs[i] else: @@ -369,41 +420,50 @@ def cfr_action_node(self, root, reachprobs): def cfr_strategy_update(self, root, reachprobs): # Update the strategies and regrets for each infoset hc = self.holecards[root.player][0:len(root.holecards[root.player])] - infoset = self.rules.infoset_format(root.player, hc, root.board, root.bet_history) + infoset = self.rules.infoset_format( + root.player, hc, root.board, root.bet_history) # Get the current CFR prev_cfr = self.counterfactual_regret[root.player][infoset] # Get the total positive CFR - sumpos_cfr = float(sum([max(0,x) for x in prev_cfr])) + sumpos_cfr = float(sum([max(0, x) for x in prev_cfr])) if sumpos_cfr == 0: # Default strategy is equal probability probs = self.equal_probs(root) else: # Use the strategy that's proportional to accumulated positive CFR - probs = [max(0,x) / sumpos_cfr for x in prev_cfr] + probs = [max(0, x) / sumpos_cfr for x in prev_cfr] # Use the updated strategy as our current strategy self.current_profile.strategies[root.player].policy[infoset] = probs - # Update the weighted policy probabilities (used to recover the average strategy) + # Update the weighted policy probabilities (used to recover the average + # strategy) for i in range(3): - self.action_reachprobs[root.player][infoset][i] += reachprobs[root.player] * probs[i] + self.action_reachprobs[root.player][infoset][ + i] += reachprobs[root.player] * probs[i] if sum(self.action_reachprobs[root.player][infoset]) == 0: # Default strategy is equal weight - self.profile.strategies[root.player].policy[infoset] = self.equal_probs(root) + self.profile.strategies[root.player].policy[ + infoset] = self.equal_probs(root) else: # Recover the weighted average strategy - self.profile.strategies[root.player].policy[infoset] = [self.action_reachprobs[root.player][infoset][i] / sum(self.action_reachprobs[root.player][infoset]) for i in range(3)] + self.profile.strategies[root.player].policy[infoset] = [self.action_reachprobs[root.player][ + infoset][i] / sum(self.action_reachprobs[root.player][infoset]) for i in range(3)] # Return and use the current CFR strategy return self.current_profile.strategies[root.player] def cfr_regret_update(self, root, action_payoffs, ev): hc = self.holecards[root.player][0:len(root.holecards[root.player])] - for i,subpayoff in enumerate(action_payoffs): + for i, subpayoff in enumerate(action_payoffs): if subpayoff is None: continue immediate_cfr = subpayoff[root.player] - ev - infoset = self.rules.infoset_format(root.player, hc, root.board, root.bet_history) - self.counterfactual_regret[root.player][infoset][i] += immediate_cfr + infoset = self.rules.infoset_format( + root.player, hc, root.board, root.bet_history) + self.counterfactual_regret[root.player][ + infoset][i] += immediate_cfr + class OutcomeSamplingCFR(ChanceSamplingCFR): + def __init__(self, rules, exploration=0.4): ChanceSamplingCFR.__init__(self, rules) self.exploration = exploration @@ -412,14 +472,17 @@ def cfr(self): # Sample all cards to be used holecards_per_player = sum([x.holecards for x in self.rules.roundinfo]) boardcards_per_hand = sum([x.boardcards for x in self.rules.roundinfo]) - todeal = random.sample(self.rules.deck, boardcards_per_hand + holecards_per_player * self.rules.players) + todeal = random.sample( + self.rules.deck, boardcards_per_hand + holecards_per_player * self.rules.players) # Deal holecards - self.holecards = [tuple(todeal[p*holecards_per_player:(p+1)*holecards_per_player]) for p in range(self.rules.players)] + self.holecards = [tuple(todeal[p * holecards_per_player:(p + 1) * holecards_per_player]) + for p in range(self.rules.players)] self.board = tuple(todeal[-boardcards_per_hand:]) # Set the top card of the deck self.top_card = len(todeal) - boardcards_per_hand # Call the standard CFR algorithm - self.cfr_helper(self.tree.root, [1 for _ in range(self.rules.players)], 1.0) + self.cfr_helper( + self.tree.root, [1 for _ in range(self.rules.players)], 1.0) def cfr_helper(self, root, reachprobs, sampleprobs): if type(root) is TerminalNode: @@ -432,12 +495,12 @@ def cfr_helper(self, root, reachprobs, sampleprobs): def cfr_terminal_node(self, root, reachprobs, sampleprobs): payoffs = [0 for _ in range(self.rules.players)] - for hands,winnings in root.payoffs.items(): + for hands, winnings in root.payoffs.items(): if not self.terminal_match(hands): continue for player in range(self.rules.players): prob = 1.0 - for opp,hc in enumerate(hands): + for opp, hc in enumerate(hands): if opp != player: prob *= reachprobs[opp] payoffs[player] = prob * winnings[player] / sampleprobs @@ -446,7 +509,7 @@ def cfr_terminal_node(self, root, reachprobs, sampleprobs): def cfr_holecard_node(self, root, reachprobs, sampleprobs): assert(len(root.children) == 1) return self.cfr_helper(root.children[0], reachprobs, sampleprobs) - + def cfr_boardcard_node(self, root, reachprobs, sampleprobs): # Number of community cards dealt this round num_dealt = len(root.children[0].board) - len(root.board) @@ -463,17 +526,21 @@ def cfr_action_node(self, root, reachprobs, sampleprobs): # Calculate strategy from counterfactual regret strategy = self.cfr_strategy_update(root, reachprobs, sampleprobs) hc = self.holecards[root.player][0:len(root.holecards[root.player])] - infoset = self.rules.infoset_format(root.player, hc, root.board, root.bet_history) + infoset = self.rules.infoset_format( + root.player, hc, root.board, root.bet_history) action_probs = strategy.probs(infoset) if random.random() < self.exploration: action = self.random_action(root) else: action = strategy.sample_action(infoset) reachprobs[root.player] *= action_probs[action] - csp = self.exploration * (1.0 / len(root.children)) + (1.0 - self.exploration) * action_probs[action] - payoffs = self.cfr_helper(root.get_child(action), reachprobs, sampleprobs * csp) + csp = self.exploration * (1.0 / len(root.children)) + ( + 1.0 - self.exploration) * action_probs[action] + payoffs = self.cfr_helper( + root.get_child(action), reachprobs, sampleprobs * csp) # Update regret calculations - self.cfr_regret_update(root, payoffs[root.player], action, action_probs[action]) + self.cfr_regret_update( + root, payoffs[root.player], action, action_probs[action]) payoffs[root.player] *= action_probs[action] return payoffs @@ -490,38 +557,45 @@ def random_action(self, root): def cfr_strategy_update(self, root, reachprobs, sampleprobs): # Update the strategies and regrets for each infoset hc = self.holecards[root.player][0:len(root.holecards[root.player])] - infoset = self.rules.infoset_format(root.player, hc, root.board, root.bet_history) + infoset = self.rules.infoset_format( + root.player, hc, root.board, root.bet_history) # Get the current CFR prev_cfr = self.counterfactual_regret[root.player][infoset] # Get the total positive CFR - sumpos_cfr = float(sum([max(0,x) for x in prev_cfr])) + sumpos_cfr = float(sum([max(0, x) for x in prev_cfr])) if sumpos_cfr == 0: # Default strategy is equal probability probs = self.equal_probs(root) else: # Use the strategy that's proportional to accumulated positive CFR - probs = [max(0,x) / sumpos_cfr for x in prev_cfr] + probs = [max(0, x) / sumpos_cfr for x in prev_cfr] # Use the updated strategy as our current strategy self.current_profile.strategies[root.player].policy[infoset] = probs - # Update the weighted policy probabilities (used to recover the average strategy) + # Update the weighted policy probabilities (used to recover the average + # strategy) for i in range(3): - self.action_reachprobs[root.player][infoset][i] += reachprobs[root.player] * probs[i] / sampleprobs + self.action_reachprobs[root.player][infoset][ + i] += reachprobs[root.player] * probs[i] / sampleprobs if sum(self.action_reachprobs[root.player][infoset]) == 0: # Default strategy is equal weight - self.profile.strategies[root.player].policy[infoset] = self.equal_probs(root) + self.profile.strategies[root.player].policy[ + infoset] = self.equal_probs(root) else: # Recover the weighted average strategy - self.profile.strategies[root.player].policy[infoset] = [self.action_reachprobs[root.player][infoset][i] / sum(self.action_reachprobs[root.player][infoset]) for i in range(3)] + self.profile.strategies[root.player].policy[infoset] = [self.action_reachprobs[root.player][ + infoset][i] / sum(self.action_reachprobs[root.player][infoset]) for i in range(3)] # Return and use the current CFR strategy return self.current_profile.strategies[root.player] def cfr_regret_update(self, root, ev, action, actionprob): hc = self.holecards[root.player][0:len(root.holecards[root.player])] - infoset = self.rules.infoset_format(root.player, hc, root.board, root.bet_history) + infoset = self.rules.infoset_format( + root.player, hc, root.board, root.bet_history) for i in range(3): if not root.valid(i): continue immediate_cfr = -ev * actionprob if action == i: immediate_cfr += ev - self.counterfactual_regret[root.player][infoset][i] += immediate_cfr + self.counterfactual_regret[root.player][ + infoset][i] += immediate_cfr diff --git a/pokergames.py b/pycfr/pokergames.py similarity index 61% rename from pokergames.py rename to pycfr/pokergames.py index 4ecc2f5..6c1f107 100644 --- a/pokergames.py +++ b/pycfr/pokergames.py @@ -1,15 +1,20 @@ -from pokertrees import * +from pokertrees import (RoundInfo, GameTree, GameRules, PublicTree) +from card import Card -def kuhn_eval(hc, board): + +def kuhn_eval(hc, _): return hc[0].rank + def half_street_kuhn_rules(): players = 2 - deck = [Card(14,1),Card(13,1),Card(12,1)] + deck = [Card(14, 1), Card(13, 1), Card(12, 1)] ante = 1 blinds = None - rounds = [RoundInfo(holecards=1,boardcards=0,betsize=1,maxbets=[1,0])] - return GameRules(players, deck, rounds, ante, blinds, handeval=kuhn_eval, infoset_format=leduc_format) + rounds = [RoundInfo(holecards=1, boardcards=0, betsize=1, maxbets=[1, 0])] + return GameRules(players, deck, rounds, ante, blinds, handeval=kuhn_eval, + infoset_format=leduc_format) + def half_street_kuhn_gametree(): rules = half_street_kuhn_rules() @@ -17,19 +22,24 @@ def half_street_kuhn_gametree(): tree.build() return tree + def half_street_kuhn_publictree(): rules = half_street_kuhn_rules() tree = PublicTree(rules) tree.build() return tree + def kuhn_rules(): players = 2 - deck = [Card(14,1),Card(13,1),Card(12,1)] + deck = [Card(14, 1), Card(13, 1), Card(12, 1)] ante = 1 blinds = None - rounds = [RoundInfo(holecards=1,boardcards=0,betsize=1,maxbets=[1,1])] - return GameRules(players, deck, rounds, ante, blinds, handeval=kuhn_eval, infoset_format=leduc_format) + rounds = [RoundInfo(holecards=1, boardcards=0, betsize=1, maxbets=[1, 1])] + + return GameRules(players, deck, rounds, ante, blinds, handeval=kuhn_eval, + infoset_format=leduc_format) + def kuhn_gametree(): rules = kuhn_rules() @@ -37,31 +47,40 @@ def kuhn_gametree(): tree.build() return tree + def kuhn_publictree(): rules = kuhn_rules() tree = PublicTree(rules) tree.build() return tree -def leduc_format(player, holecards, board, bet_history): + +def leduc_format(_, holecards, board, bet_history): cards = holecards[0].RANK_TO_STRING[holecards[0].rank] if len(board) > 0: cards += board[0].RANK_TO_STRING[board[0].rank] return "{0}:{1}:".format(cards, bet_history) -def leduc_eval(hc,board): + +def leduc_eval(hc, board): hand = hc + board if hand[0].rank == hand[1].rank: - return 15*14+hand[0].rank - return max(hand[0].rank, hand[1].rank) * 14 + min(hand[0].rank, hand[1].rank) + return 15 * 14 + hand[0].rank + return max(hand[0].rank, hand[1].rank) * 14 + min(hand[0].rank, + hand[1].rank) + def leduc_rules(): players = 2 - deck = [Card(13,1),Card(13,2),Card(12,1),Card(12,2),Card(11,1),Card(11,2)] + deck = [Card(13, 1), Card(13, 2), Card(12, 1), + Card(12, 2), Card(11, 1), Card(11, 2)] ante = 1 blinds = None - rounds = [RoundInfo(holecards=1,boardcards=0,betsize=2,maxbets=[2,2]),RoundInfo(holecards=0,boardcards=1,betsize=4,maxbets=[2,2])] - return GameRules(players, deck, rounds, ante, blinds, handeval=leduc_eval, infoset_format=leduc_format) + rounds = [RoundInfo(holecards=1, boardcards=0, betsize=2, maxbets=[2, 2]), + RoundInfo(holecards=0, boardcards=1, betsize=4, maxbets=[2, 2])] + return GameRules(players, deck, rounds, ante, blinds, handeval=leduc_eval, + infoset_format=leduc_format) + def leduc_gametree(): rules = leduc_rules() @@ -69,13 +88,15 @@ def leduc_gametree(): tree.build() return tree + def leduc_publictree(): rules = leduc_rules() tree = PublicTree(rules) tree.build() return tree -def royal_format(player, holecards, board, bet_history): + +def royal_format(_, holecards, board, bet_history): cards = holecards[0].RANK_TO_STRING[holecards[0].rank] for i in range(len(board)): cards += board[i].RANK_TO_STRING[board[i].rank] @@ -85,7 +106,8 @@ def royal_format(player, holecards, board, bet_history): cards += 'o' return "{0}:{1}:".format(cards, bet_history) -def royal_eval(hc,board): + +def royal_eval(hc, board): hand = hc + board # Flush if hand[0].suit == hand[1].suit and hand[0].suit == hand[2].suit: @@ -99,19 +121,23 @@ def royal_eval(hc,board): return 1000 + Card.RANK_JACK # Holecard used in a pair if hand[0].rank == hand[1].rank or hand[0].rank == hand[2].rank: - return 100+hand[0].rank + return 100 + hand[0].rank return hand[0].rank + def royal_rules(): players = 2 - deck = [Card(14,1),Card(14,2),Card(13,1),Card(13,2),Card(12,1),Card(12,2),Card(11,1),Card(11,2)] + deck = [Card(14, 1), Card(14, 2), Card(13, 1), Card(13, 2), + Card(12, 1), Card(12, 2), Card(11, 1), Card(11, 2)] ante = 1 blinds = None - preflop = RoundInfo(holecards=1,boardcards=0,betsize=2,maxbets=[2,2]) - flop = RoundInfo(holecards=0,boardcards=1,betsize=4,maxbets=[2,2]) - turn = RoundInfo(holecards=0,boardcards=1,betsize=4,maxbets=[2,2]) - rounds = [preflop,flop,turn] - return GameRules(players, deck, rounds, ante, blinds, handeval=royal_eval, infoset_format=royal_format) + preflop = RoundInfo(holecards=1, boardcards=0, betsize=2, maxbets=[2, 2]) + flop = RoundInfo(holecards=0, boardcards=1, betsize=4, maxbets=[2, 2]) + turn = RoundInfo(holecards=0, boardcards=1, betsize=4, maxbets=[2, 2]) + rounds = [preflop, flop, turn] + return GameRules(players, deck, rounds, ante, blinds, handeval=royal_eval, + infoset_format=royal_format) + def royal_gametree(): rules = royal_rules() @@ -119,8 +145,9 @@ def royal_gametree(): tree.build() return tree + def royal_publictree(): rules = royal_rules() tree = PublicTree(rules) tree.build() - return tree \ No newline at end of file + return tree diff --git a/pokerstrategy.py b/pycfr/pokerstrategy.py similarity index 62% rename from pokerstrategy.py rename to pycfr/pokerstrategy.py index 44780c1..1842b51 100644 --- a/pokerstrategy.py +++ b/pycfr/pokerstrategy.py @@ -1,6 +1,7 @@ from pokertrees import * import random + def choose(n, k): """ A fast way to calculate binomial coefficients by Andrew Dalke (contrib). @@ -16,7 +17,9 @@ def choose(n, k): else: return 0 + class Strategy(object): + def __init__(self, player, filename=None): self.player = player self.policy = {} @@ -30,7 +33,7 @@ def build_default(self, gametree): if test_node.player == self.player: for node in infoset: prob = 1.0 / float(len(node.children)) - probs = [0,0,0] + probs = [0, 0, 0] for action in range(3): if node.valid(action): probs[action] = prob @@ -60,7 +63,10 @@ def build_random(self, gametree): self.policy[node.player_view] = probs def probs(self, infoset): - assert(infoset in self.policy) + if infoset not in self.policy: + raise KeyError( + "Information set '%s' doesn'nt belong to policy %s" % ( + infoset, self.policy.keys())) return self.policy[infoset] def sample_action(self, infoset): @@ -68,11 +74,13 @@ def sample_action(self, infoset): probs = self.policy[infoset] val = random.random() total = 0 - for i,p in enumerate(probs): + for i, p in enumerate(probs): total += p if p > 0 and val <= total: return i - raise Exception('Invalid probability distribution. Infoset: {0} Probs: {1}'.format(infoset, probs)) + raise Exception( + 'Invalid probability distribution. Infoset: {0} Probs: {1}'.format( + infoset, probs)) def load_from_file(self, filename): self.policy = {} @@ -91,11 +99,14 @@ def save_to_file(self, filename): f = open(filename, 'w') for key in sorted(self.policy.keys()): val = self.policy[key] - f.write("{0} {1:.9f} {2:.9f} {3:.9f}\n".format(key, val[2], val[1], val[0])) + f.write("{0} {1:.9f} {2:.9f} {3:.9f}\n".format( + key, val[2], val[1], val[0])) f.flush() f.close() + class StrategyProfile(object): + def __init__(self, rules, strategies): assert(rules.players == len(strategies)) self.rules = rules @@ -112,10 +123,11 @@ def expected_value(self): self.gametree = PublicTree(self.rules) if self.gametree.root is None: self.gametree.build() - expected_values = self.ev_helper(self.gametree.root, [{(): 1} for _ in range(self.rules.players)]) + expected_values = self.ev_helper( + self.gametree.root, [{(): 1} for _ in range(self.rules.players)]) for ev in expected_values: assert(len(ev) == 1) - return tuple(next(ev.itervalues()) for ev in expected_values) # pull the EV from the dict returned + return tuple(next(ev.itervalues()) for ev in expected_values) # pull the EV from the dict returned def old_ev_helper(self, root, pathprobs): if type(root) is TerminalNode: @@ -125,23 +137,26 @@ def old_ev_helper(self, root, pathprobs): prob = pathprob / float(len(root.children)) for child in root.children: subpayoffs = self.ev_helper(child, prob) - for i,p in enumerate(subpayoffs): + for i, p in enumerate(subpayoffs): payoffs[i] += p return payoffs # Otherwise, it's an ActionNode probs = self.strategies[root.player].probs(root.player_view) payoffs = [0] * self.rules.players if root.fold_action and probs[FOLD] > 0.0000000001: - subpayoffs = self.ev_helper(root.fold_action, pathprob * probs[FOLD]) - for i,p in enumerate(subpayoffs): + subpayoffs = self.ev_helper( + root.fold_action, pathprob * probs[FOLD]) + for i, p in enumerate(subpayoffs): payoffs[i] += p if root.call_action and probs[CALL] > 0.0000000001: - subpayoffs = self.ev_helper(root.call_action, pathprob * probs[CALL]) - for i,p in enumerate(subpayoffs): + subpayoffs = self.ev_helper( + root.call_action, pathprob * probs[CALL]) + for i, p in enumerate(subpayoffs): payoffs[i] += p if root.raise_action and probs[RAISE] > 0.0000000001: - subpayoffs = self.ev_helper(root.raise_action, pathprob * probs[RAISE]) - for i,p in enumerate(subpayoffs): + subpayoffs = self.ev_helper( + root.raise_action, pathprob * probs[RAISE]) + for i, p in enumerate(subpayoffs): payoffs[i] += p return payoffs @@ -159,17 +174,17 @@ def ev_terminal_node(self, root, reachprobs): for player in range(self.rules.players): player_payoffs = {hc: 0 for hc in root.holecards[player]} counts = {hc: 0 for hc in root.holecards[player]} - for hands,winnings in root.payoffs.items(): + for hands, winnings in root.payoffs.items(): prob = 1.0 player_hc = None - for opp,hc in enumerate(hands): + for opp, hc in enumerate(hands): if opp == player: player_hc = hc else: prob *= reachprobs[opp][hc] player_payoffs[player_hc] += prob * winnings[player] counts[player_hc] += 1 - for hc,count in counts.items(): + for hc, count in counts.items(): if count > 0: player_payoffs[hc] /= float(count) payoffs[player] = player_payoffs @@ -178,53 +193,65 @@ def ev_terminal_node(self, root, reachprobs): def ev_holecard_node(self, root, reachprobs): assert(len(root.children) == 1) prevlen = len(reachprobs[0].keys()[0]) - possible_deals = float(choose(len(root.deck) - prevlen,root.todeal)) - next_reachprobs = [{ hc: reachprobs[player][hc[0:prevlen]] / possible_deals for hc in root.children[0].holecards[player] } for player in range(self.rules.players)] + possible_deals = float(choose(len(root.deck) - prevlen, root.todeal)) + next_reachprobs = [{hc: reachprobs[player][hc[0:prevlen]] / possible_deals for hc in root.children[0].holecards[player]} + for player in range(self.rules.players)] subpayoffs = self.ev_helper(root.children[0], next_reachprobs) - payoffs = [{ hc: 0 for hc in root.holecards[player] } for player in range(self.rules.players)] + payoffs = [{hc: 0 for hc in root.holecards[player]} + for player in range(self.rules.players)] for player, subpayoff in enumerate(subpayoffs): - for hand,winnings in subpayoff.items(): + for hand, winnings in subpayoff.items(): hc = hand[0:prevlen] payoffs[player][hc] += winnings return payoffs def ev_boardcard_node(self, root, reachprobs): prevlen = len(reachprobs[0].keys()[0]) - possible_deals = float(choose(len(root.deck) - prevlen,root.todeal)) - payoffs = [{ hc: 0 for hc in root.holecards[player] } for player in range(self.rules.players)] + possible_deals = float(choose(len(root.deck) - prevlen, root.todeal)) + payoffs = [{hc: 0 for hc in root.holecards[player]} + for player in range(self.rules.players)] for bc in root.children: - next_reachprobs = [{ hc: reachprobs[player][hc] / possible_deals for hc in bc.holecards[player] } for player in range(self.rules.players)] + next_reachprobs = [{hc: reachprobs[player][hc] / possible_deals for hc in bc.holecards[player]} + for player in range(self.rules.players)] subpayoffs = self.ev_helper(bc, next_reachprobs) - for player,subpayoff in enumerate(subpayoffs): - for hand,winnings in subpayoff.items(): + for player, subpayoff in enumerate(subpayoffs): + for hand, winnings in subpayoff.items(): payoffs[player][hand] += winnings return payoffs def ev_action_node(self, root, reachprobs): strategy = self.strategies[root.player] next_reachprobs = deepcopy(reachprobs) - action_probs = { hc: strategy.probs(self.rules.infoset_format(root.player, hc, root.board, root.bet_history)) for hc in root.holecards[root.player] } + action_probs = {hc: strategy.probs(self.rules.infoset_format(root.player, hc, root.board, root.bet_history)) + for hc in root.holecards[root.player]} action_payoffs = [None, None, None] if root.fold_action: - next_reachprobs[root.player] = { hc: action_probs[hc][FOLD] * reachprobs[root.player][hc] for hc in root.holecards[root.player] } - action_payoffs[FOLD] = self.ev_helper(root.fold_action, next_reachprobs) + next_reachprobs[root.player] = {hc: action_probs[hc][FOLD] * reachprobs[root.player][hc] + for hc in root.holecards[root.player]} + action_payoffs[FOLD] = self.ev_helper( + root.fold_action, next_reachprobs) if root.call_action: - next_reachprobs[root.player] = { hc: action_probs[hc][CALL] * reachprobs[root.player][hc] for hc in root.holecards[root.player] } - action_payoffs[CALL] = self.ev_helper(root.call_action, next_reachprobs) + next_reachprobs[root.player] = {hc: action_probs[hc][CALL] * reachprobs[root.player][hc] + for hc in root.holecards[root.player]} + action_payoffs[CALL] = self.ev_helper( + root.call_action, next_reachprobs) if root.raise_action: - next_reachprobs[root.player] = { hc: action_probs[hc][RAISE] * reachprobs[root.player][hc] for hc in root.holecards[root.player] } - action_payoffs[RAISE] = self.ev_helper(root.raise_action, next_reachprobs) + next_reachprobs[root.player] = {hc: action_probs[hc][RAISE] * reachprobs[root.player][hc] + for hc in root.holecards[root.player]} + action_payoffs[RAISE] = self.ev_helper( + root.raise_action, next_reachprobs) payoffs = [] for player in range(self.rules.players): - player_payoffs = { hc: 0 for hc in root.holecards[player] } - for action,subpayoff in enumerate(action_payoffs): + player_payoffs = {hc: 0 for hc in root.holecards[player]} + for action, subpayoff in enumerate(action_payoffs): if subpayoff is None: continue if root.player == player: - for hc,winnings in subpayoff[player].iteritems(): - player_payoffs[hc] += winnings * action_probs[hc][action] + for hc, winnings in subpayoff[player].iteritems(): + player_payoffs[ + hc] += winnings * action_probs[hc][action] else: - for hc,winnings in subpayoff[player].iteritems(): + for hc, winnings in subpayoff[player].iteritems(): player_payoffs[hc] += winnings payoffs.append(player_payoffs) return payoffs @@ -239,10 +266,12 @@ def best_response(self): if self.publictree.root is None: self.publictree.build() responses = [Strategy(player) for player in range(self.rules.players)] - expected_values = self.br_helper(self.publictree.root, [{(): 1} for _ in range(self.rules.players)], responses) + expected_values = self.br_helper(self.publictree.root, [ + {(): 1} for _ in range(self.rules.players)], responses) for ev in expected_values: assert(len(ev) == 1) - expected_values = tuple(next(ev.itervalues()) for ev in expected_values) # pull the EV from the dict returned + expected_values = tuple(next(ev.itervalues()) + for ev in expected_values) # pull the EV from the dict returned return (StrategyProfile(self.rules, responses), expected_values) def br_helper(self, root, reachprobs, responses): @@ -257,58 +286,71 @@ def br_helper(self, root, reachprobs, responses): def br_holecard_node(self, root, reachprobs, responses): assert(len(root.children) == 1) prevlen = len(reachprobs[0].keys()[0]) - possible_deals = float(choose(len(root.deck) - prevlen,root.todeal)) - next_reachprobs = [{ hc: reachprobs[player][hc[0:prevlen]] / possible_deals for hc in root.children[0].holecards[player] } for player in range(self.rules.players)] - subpayoffs = self.br_helper(root.children[0], next_reachprobs, responses) - payoffs = [{ hc: 0 for hc in root.holecards[player] } for player in range(self.rules.players)] + possible_deals = float(choose(len(root.deck) - prevlen, root.todeal)) + next_reachprobs = [{hc: reachprobs[player][hc[0:prevlen]] / possible_deals for hc in root.children[0].holecards[player]} + for player in range(self.rules.players)] + subpayoffs = self.br_helper( + root.children[0], next_reachprobs, responses) + payoffs = [{hc: 0 for hc in root.holecards[player]} + for player in range(self.rules.players)] for player, subpayoff in enumerate(subpayoffs): - for hand,winnings in subpayoff.items(): + for hand, winnings in subpayoff.items(): hc = hand[0:prevlen] payoffs[player][hc] += winnings return payoffs def br_boardcard_node(self, root, reachprobs, responses): prevlen = len(reachprobs[0].keys()[0]) - possible_deals = float(choose(len(root.deck) - prevlen,root.todeal)) - payoffs = [{ hc: 0 for hc in root.holecards[player] } for player in range(self.rules.players)] + possible_deals = float(choose(len(root.deck) - prevlen, root.todeal)) + payoffs = [{hc: 0 for hc in root.holecards[player]} + for player in range(self.rules.players)] for bc in root.children: - next_reachprobs = [{ hc: reachprobs[player][hc] / possible_deals for hc in bc.holecards[player] } for player in range(self.rules.players)] + next_reachprobs = [{hc: reachprobs[player][hc] / possible_deals for hc in bc.holecards[player]} + for player in range(self.rules.players)] subpayoffs = self.br_helper(bc, next_reachprobs, responses) - for player,subpayoff in enumerate(subpayoffs): - for hand,winnings in subpayoff.items(): + for player, subpayoff in enumerate(subpayoffs): + for hand, winnings in subpayoff.items(): payoffs[player][hand] += winnings return payoffs def br_action_node(self, root, reachprobs, responses): strategy = self.strategies[root.player] next_reachprobs = deepcopy(reachprobs) - action_probs = { hc: strategy.probs(self.rules.infoset_format(root.player, hc, root.board, root.bet_history)) for hc in root.holecards[root.player] } + action_probs = {hc: strategy.probs(self.rules.infoset_format(root.player, hc, root.board, root.bet_history)) + for hc in root.holecards[root.player]} action_payoffs = [None, None, None] if root.fold_action: - next_reachprobs[root.player] = { hc: action_probs[hc][FOLD] * reachprobs[root.player][hc] for hc in root.holecards[root.player] } - action_payoffs[FOLD] = self.br_helper(root.fold_action, next_reachprobs, responses) + next_reachprobs[root.player] = {hc: action_probs[hc][FOLD] * reachprobs[root.player][hc] + for hc in root.holecards[root.player]} + action_payoffs[FOLD] = self.br_helper( + root.fold_action, next_reachprobs, responses) if root.call_action: - next_reachprobs[root.player] = { hc: action_probs[hc][CALL] * reachprobs[root.player][hc] for hc in root.holecards[root.player] } - action_payoffs[CALL] = self.br_helper(root.call_action, next_reachprobs, responses) + next_reachprobs[root.player] = {hc: action_probs[hc][CALL] * reachprobs[root.player][hc] + for hc in root.holecards[root.player]} + action_payoffs[CALL] = self.br_helper( + root.call_action, next_reachprobs, responses) if root.raise_action: - next_reachprobs[root.player] = { hc: action_probs[hc][RAISE] * reachprobs[root.player][hc] for hc in root.holecards[root.player] } - action_payoffs[RAISE] = self.br_helper(root.raise_action, next_reachprobs, responses) + next_reachprobs[root.player] = {hc: action_probs[hc][RAISE] * reachprobs[root.player][hc] + for hc in root.holecards[root.player]} + action_payoffs[RAISE] = self.br_helper( + root.raise_action, next_reachprobs, responses) payoffs = [] for player in range(self.rules.players): if player is root.player: - payoffs.append(self.br_response_action(root, responses, action_payoffs)) + payoffs.append( + self.br_response_action(root, responses, action_payoffs)) else: - player_payoffs = { hc: 0 for hc in root.holecards[player] } + player_payoffs = {hc: 0 for hc in root.holecards[player]} for subpayoff in action_payoffs: if subpayoff is None: continue - for hc,winnings in subpayoff[player].iteritems(): + for hc, winnings in subpayoff[player].iteritems(): player_payoffs[hc] += winnings payoffs.append(player_payoffs) return payoffs def br_response_action(self, root, responses, action_payoffs): - player_payoffs = { } + player_payoffs = {} max_strategy = responses[root.player] for hc in root.holecards[root.player]: max_action = None @@ -329,13 +371,11 @@ def br_response_action(self, root, responses, action_payoffs): max_value = value elif max_value == value: max_action.append(RAISE) - probs = [0,0,0] + probs = [0, 0, 0] for action in max_action: probs[action] = 1.0 / float(len(max_action)) - infoset = self.rules.infoset_format(root.player, hc, root.board, root.bet_history) + infoset = self.rules.infoset_format( + root.player, hc, root.board, root.bet_history) max_strategy.policy[infoset] = probs player_payoffs[hc] = max_value return player_payoffs - - - diff --git a/pycfr/pokertrees.py b/pycfr/pokertrees.py new file mode 100644 index 0000000..3ad3219 --- /dev/null +++ b/pycfr/pokertrees.py @@ -0,0 +1,678 @@ +from itertools import combinations, permutations, product +from collections import Counter +from hand_evaluator import HandEvaluator +from copy import deepcopy +from functools import partial + +# Possible player actions. Note that a check is equivalent to calling 0 +# (a "free call") and so is omitted from the code +FOLD = 0 +CALL = 1 +RAISE = 2 + + +def overlap(t1, t2): + """Helper function to check whether two iterables t1 and t2 overlap.""" + for x in t1: + if x in t2: + return True + return False + + +def all_unique(hc): + """Helper function that the elements of hc are pairwise non-overlapping.""" + for i in range(len(hc) - 1): + for j in range(i + 1, len(hc)): + if overlap(hc[i], hc[j]): + return False + return + + +# Combinatoric helper functions. +def factorial(n): + if n < 0: + raise RuntimeError("n=%i" % n) + return 1 if n < 2 else n * factorial(n - 1) +n_combinations = lambda n, k: factorial(n) / (factorial(k) * factorial(n - k)) + + +def default_infoset_format(_, holecards, board, bet_history): + return "{0}{1}:{2}:".format("".join([str(x) for x in holecards]), + "".join([str(x) for x in board]), bet_history) + + +class GameRules(object): + """Game rules of a finte multi-round game. + + Parameters + ---------- + players : int + Number of players. + + rounds: list of `RoundInfo` object + Each item specifies the rules for the given round. + + deck : list of `Card` objects + ... + + """ + + def __init__(self, players, deck, rounds, ante, blinds, + handeval=HandEvaluator.evaluate_hand, + infoset_format=default_infoset_format): + assert(players >= 2) + assert(ante >= 0) + assert(rounds is not None) + assert(deck is not None) + assert(len(rounds) > 0) + assert(len(deck) > 1) + if blinds is not None: + if type(blinds) is int or type(blinds) is float: + blinds = [blinds] + for r in rounds: + assert(len(r.maxbets) == players) + self.players = players + self.deck = deck + self.roundinfo = rounds + self.ante = ante + self.blinds = blinds + self.handeval = handeval + self.infoset_format = infoset_format + + +class RoundInfo(object): + """Rules (= game parameters) for a given round. + + Parameters + ---------- + holecards : int + Number of hole (i.e private) cards per player. + + boardcards: int + Number of board (i.e community , i.e public) cards. + + betsize : int + Starting bet size for round. + + maxbets : int + Maximum bet size for round. + + """ + + def __init__(self, holecards, boardcards, betsize, maxbets): + self.holecards = holecards + self.boardcards = boardcards + self.betsize = betsize + self.maxbets = maxbets + + +class GameTree(object): + """Abstraction of game tree. + + Parameters + ---------- + rules : `GameRules` object + The rules of the game. + + Attributes + ---------- + nodes : list of `Node` objects + The nodes (decision and terminal nodes) of the game tree. + + information_sets : dict + Each key is a string representing the corresponding information set. + The correspoonding value is the list of nodes which belongs to this + information set. + """ + + def __init__(self, rules): + self.rules = deepcopy(rules) + self.nodes = [] + self.leafs = [] + self.information_sets = {} + self.root = None + + def build(self): + # Assume everyone is in + players_in = [True] * self.rules.players + # Collect antes + committed = [self.rules.ante] * self.rules.players + bets = [0] * self.rules.players + # Collect blinds + next_player = self.collect_blinds(committed, bets, 0) + holes = [() for _ in xrange(self.rules.players)] + board = () + bet_history = "" + self.root = self.build_rounds( + None, players_in, committed, holes, board, self.rules.deck, + bet_history, 0, bets, next_player) + + def collect_blinds(self, committed, bets, next_player): + if self.rules.blinds is not None: + for blind in self.rules.blinds: + committed[next_player] += blind + bets[next_player] = int( + (committed[next_player] - self.rules.ante) + / self.rules.roundinfo[0].betsize) + next_player = (next_player + 1) % self.rules.players + return next_player + + def deal_holecards(self, deck, holecards, players): + """Deals hole (i.e private) cards from given deck. + + Returns + ------- + holecards : generator of `Card` objects + All possible hole-card deals. + + proba : float + Each tuple of hole cards is drawn with this probability. + """ + cards = permutations(combinations(deck, holecards), players) + nchoices = n_combinations(len(deck), holecards) + + # XXX use numpy to execute the following computation + proba = 1. + low, high = nchoices - players + 1, nchoices + for salt in xrange(low, high + 1): + proba *= salt + proba = 1 / proba + + return cards, proba + + def commit_node(self, node): + """stores a given node in game tree database.""" + self.nodes.append(node) + + # place node into appropriate information set + if hasattr(node, "player_view"): + if node.player_view not in self.information_sets: + self.information_sets[node.player_view] = [] + self.information_sets[node.player_view].append(node) + + # handle leaf node + if isinstance(node, TerminalNode): + self.leafs.append(node) + + return self + + def build_rounds(self, root, players_in, committed, holes, board, deck, + bet_history, round_idx, bets=None, next_player=0): + """Recursively build the rounds of the game hand.""" + # if no rounds left, then end the hand + if round_idx == len(self.rules.roundinfo): + tnode = self.showdown(root, players_in, committed, holes, board, + deck, bet_history) + self.commit_node(tnode) + return + + bet_history += "/" + cur_round = self.rules.roundinfo[round_idx] + while not players_in[next_player]: + next_player = (next_player + 1) % self.rules.players + if bets is None: + bets = [0] * self.rules.players + min_actions_this_round = players_in.count(True) + actions_this_round = 0 + if cur_round.holecards: + return self.build_holecards( + root, next_player, players_in, committed, holes, board, deck, + bet_history, round_idx, min_actions_this_round, + actions_this_round, bets) + if cur_round.boardcards: + return self.build_boardcards( + root, next_player, players_in, committed, holes, board, deck, + bet_history, round_idx, min_actions_this_round, + actions_this_round, bets) + return self.build_bets( + root, next_player, players_in, committed, holes, board, deck, + bet_history, round_idx, min_actions_this_round, + actions_this_round, bets) + + def get_next_player(self, cur_player, players_in): + """The player to act at the corrent node""" + next_player = (cur_player + 1) % self.rules.players + while not players_in[next_player]: + next_player = (next_player + 1) % self.rules.players + return next_player + + def build_holecards(self, root, next_player, players_in, committed, holes, + board, deck, bet_history, round_idx, + min_actions_this_round, actions_this_round, bets): + cur_round = self.rules.roundinfo[round_idx] + + # Deal holecards + if not cur_round.holecards: + raise RuntimeError + all_hc, proba = self.deal_holecards( + deck, cur_round.holecards, players_in.count(True)) + hnode = HolecardChanceNode( + root, committed, holes, board, self.rules.deck, "", + cur_round.holecards, proba=proba) + self.commit_node(hnode) + + # Create a child node for every possible distribution + for cur_holes in all_hc: + dealt_cards = () + cur_holes = list(cur_holes) + cur_idx = 0 + for i, hc in enumerate(holes): + # Only deal cards to players who are still in + if players_in[i]: + cur_holes[cur_idx] = hc + cur_holes[cur_idx] + cur_idx += 1 + for hc in cur_holes: + dealt_cards += hc + cur_deck = filter(lambda x: not (x in dealt_cards), deck) + if cur_round.boardcards: + self.build_boardcards( + hnode, next_player, players_in, committed, cur_holes, + board, cur_deck, bet_history, round_idx, + min_actions_this_round, actions_this_round, bets) + else: + self.build_bets( + hnode, next_player, players_in, committed, cur_holes, + board, cur_deck, bet_history, round_idx, + min_actions_this_round, actions_this_round, bets) + return hnode + + def build_boardcards(self, root, next_player, players_in, committed, + holes, board, deck, bet_history, round_idx, + min_actions_this_round, actions_this_round, bets): + cur_round = self.rules.roundinfo[round_idx] + + # reveal community cards + if not cur_round.boardcards: + raise RuntimeError + all_bc = combinations(deck, cur_round.boardcards) + proba = 1. / n_combinations(len(deck), cur_round.boardcards) + bnode = BoardcardChanceNode( + root, committed, holes, board, deck, bet_history, + cur_round.boardcards, proba=proba) + self.commit_node(bnode) + + for bc in all_bc: + cur_board = board + bc + cur_deck = filter(lambda x: not (x in bc), deck) + self.build_bets( + bnode, next_player, players_in, committed, holes, cur_board, + cur_deck, bet_history, round_idx, min_actions_this_round, + actions_this_round, bets) + return bnode + + def build_bets(self, root, next_player, players_in, committed, holes, + board, deck, bet_history, round_idx, min_actions_this_round, + actions_this_round, bets_this_round): + # if everyone else folded, end the hand + if players_in.count(True) == 1: + tnode = self.showdown( + root, players_in, committed, holes, board, deck, bet_history) + self.commit_node(tnode) + return + + # if everyone checked or the last raisor has been called, end the round + if actions_this_round >= min_actions_this_round and \ + self.all_called_last_raisor_or_folded(players_in, bets_this_round): + self.build_rounds( + root, players_in, committed, holes, board, deck, bet_history, + round_idx + 1) + return + cur_round = self.rules.roundinfo[round_idx] + anode = ActionNode(root, committed, holes, board, deck, + bet_history, next_player, self.rules.infoset_format) + + # add the node to the information set + self.commit_node(anode) + + # get the next player to act + next_player = self.get_next_player(next_player, players_in) + + # add a folding option if someone has bet more than this player + if committed[anode.player] < max(committed): + self.add_fold_child( + anode, next_player, players_in, committed, holes, board, deck, + bet_history, round_idx, min_actions_this_round, + actions_this_round, bets_this_round) + + # add a calling/checking option + self.add_call_child( + anode, next_player, players_in, committed, holes, board, deck, + bet_history, round_idx, min_actions_this_round, actions_this_round, + bets_this_round) + + # add a raising option if this player has not reached their max bet + # level + if cur_round.maxbets[anode.player] > max(bets_this_round): + self.add_raise_child( + anode, next_player, players_in, committed, holes, board, deck, + bet_history, round_idx, min_actions_this_round, + actions_this_round, bets_this_round) + return anode + + def all_called_last_raisor_or_folded(self, players_in, bets): + betlevel = max(bets) + for i, _ in enumerate(bets): + if players_in[i] and bets[i] < betlevel: + return False + return True + + def add_fold_child(self, root, next_player, players_in, committed, holes, + board, deck, bet_history, round_idx, + min_actions_this_round, actions_this_round, + bets_this_round): + players_in[root.player] = False + bet_history += 'f' + self.build_bets( + root, next_player, players_in, committed, holes, board, deck, + bet_history, round_idx, min_actions_this_round, + actions_this_round + 1, bets_this_round) + root.fold_action = root.children[-1] + players_in[root.player] = True + + def add_call_child(self, root, next_player, players_in, committed, holes, + board, deck, bet_history, round_idx, + min_actions_this_round, actions_this_round, + bets_this_round): + player_commit = committed[root.player] + player_bets = bets_this_round[root.player] + committed[root.player] = max(committed) + bets_this_round[root.player] = max(bets_this_round) + bet_history += 'c' + self.build_bets( + root, next_player, players_in, committed, holes, board, deck, + bet_history, round_idx, min_actions_this_round, + actions_this_round + 1, bets_this_round) + root.call_action = root.children[-1] + committed[root.player] = player_commit + bets_this_round[root.player] = player_bets + + def add_raise_child(self, root, next_player, players_in, committed, holes, + board, deck, bet_history, round_idx, + min_actions_this_round, actions_this_round, + bets_this_round): + cur_round = self.rules.roundinfo[round_idx] + prev_betlevel = bets_this_round[root.player] + prev_commit = committed[root.player] + bets_this_round[root.player] = max(bets_this_round) + 1 + committed[root.player] += ( + bets_this_round[root.player] - prev_betlevel) * cur_round.betsize + bet_history += 'r' + self.build_bets( + root, next_player, players_in, committed, holes, board, deck, + bet_history, round_idx, min_actions_this_round, + actions_this_round + 1, bets_this_round) + root.raise_action = root.children[-1] + bets_this_round[root.player] = prev_betlevel + committed[root.player] = prev_commit + + def showdown(self, root, players_in, committed, holes, board, deck, + bet_history): + if players_in.count(True) == 1: + winners = [i for i, v in enumerate(players_in) if v] + else: + scores = [self.rules.handeval(hc, board) for hc in holes] + winners = [] + maxscore = -1 + for i, s in enumerate(scores): + if players_in[i]: + if len(winners) == 0 or s > maxscore: + maxscore = s + winners = [i] + elif s == maxscore: + winners.append(i) + pot = sum(committed) + payoff = pot / float(len(winners)) + payoffs = [-x for x in committed] + for w in winners: + payoffs[w] += payoff + return TerminalNode(root, committed, holes, board, deck, bet_history, + payoffs, players_in) + + def holecard_distributions(self): + x = Counter(combinations(self.rules.deck, self.holecards)) + d = float(sum(x.values())) + return zip(x.keys(), [y / d for y in x.values()]) + + def get_player_information_sets(self, player): + """Returns all information sets belonging to given player.""" + return dict((k, v) for k, v in self.information_sets.iteritems() + if v[0].player == player) + + +def multi_infoset_format(base_infoset_format, player, holecards, board, + bet_history): + return tuple([base_infoset_format(player, hc, board, bet_history) + for hc in holecards]) + + +class PublicTree(GameTree): + pass + def __init__(self, rules): + GameTree.__init__( + self, GameRules( + rules.players, rules.deck, rules.roundinfo, rules.ante, + rules.blinds, rules.handeval, partial(multi_infoset_format, + rules.infoset_format))) + + def build(self): + # Assume everyone is in + players_in = [True] * self.rules.players + # Collect antes + committed = [self.rules.ante] * self.rules.players + bets = [0] * self.rules.players + # Collect blinds + next_player = self.collect_blinds(committed, bets, 0) + holes = [[()]] * self.rules.players + board = () + bet_history = "" + self.root = self.build_rounds( + None, players_in, committed, holes, board, self.rules.deck, + bet_history, 0, bets, next_player) + + def build_holecards(self, root, next_player, players_in, committed, holes, + board, deck, bet_history, round_idx, + min_actions_this_round, actions_this_round, bets): + cur_round = self.rules.roundinfo[round_idx] + hnode = HolecardChanceNode( + root, committed, holes, board, self.rules.deck, "", + cur_round.holecards) + # Deal holecards + all_hc = list(combinations(deck, cur_round.holecards)) + updated_holes = [] + for player in range(self.rules.players): + if not players_in[player]: + # Only deal to players who are still in the hand + updated_holes.append([old_hc for old_hc in holes[player]]) + elif len(holes[player]) == 0: + # If this player has no cards, just set their holecards to be + # the newly dealt ones + updated_holes.append([new_hc for new_hc in all_hc]) + else: + updated_holes.append([]) + # Filter holecards to valid combinations + # TODO: Speed this up by removing duplicate holecard + # combinations + for new_hc in all_hc: + for old_hc in holes[player]: + if not overlap(old_hc, new_hc): + updated_holes[player].append(old_hc + new_hc) + if cur_round.boardcards: + self.build_boardcards( + hnode, next_player, players_in, committed, updated_holes, + board, deck, bet_history, round_idx, min_actions_this_round, + actions_this_round, bets) + else: + self.build_bets( + hnode, next_player, players_in, committed, updated_holes, + board, deck, bet_history, round_idx, min_actions_this_round, + actions_this_round, bets) + return hnode + + def build_boardcards(self, root, next_player, players_in, committed, holes, + board, deck, bet_history, round_idx, + min_actions_this_round, actions_this_round, bets): + cur_round = self.rules.roundinfo[round_idx] + bnode = BoardcardChanceNode( + root, committed, holes, board, deck, bet_history, + cur_round.boardcards) + all_bc = combinations(deck, cur_round.boardcards) + for bc in all_bc: + cur_board = board + bc + cur_deck = filter(lambda x: not (x in bc), deck) + updated_holes = [] + # Filter any holecards that are now impossible + for player in range(self.rules.players): + updated_holes.append([]) + for hc in holes[player]: + if not overlap(hc, bc): + updated_holes[player].append(hc) + self.build_bets( + bnode, next_player, players_in, committed, updated_holes, + cur_board, cur_deck, bet_history, round_idx, + min_actions_this_round, actions_this_round, bets) + return bnode + + def showdown(self, root, players_in, committed, holes, board, deck, + bet_history): + # TODO: Speedup + # - Pre-order list of hands + pot = sum(committed) + showdowns_possible = self.showdown_combinations(holes) + if players_in.count(True) == 1: + fold_payoffs = [-x for x in committed] + fold_payoffs[players_in.index(True)] += pot + payoffs = {hands: fold_payoffs for hands in showdowns_possible} + else: + scores = {} + for i in range(self.rules.players): + if players_in[i]: + for hc in holes[i]: + if not (hc in scores): + scores[hc] = self.rules.handeval(hc, board) + payoffs = {hands: self.calc_payoffs(hands, scores, players_in, + committed, pot) + for hands in showdowns_possible} + return TerminalNode(root, committed, holes, board, deck, bet_history, + payoffs, players_in) + + def showdown_combinations(self, holes): + # Get all the possible holecard matchups for a given showdown. + # Every card must be unique because two players cannot have the same + # holecard. + return list(filter(lambda x: all_unique(x), product(*holes))) + + def calc_payoffs(self, hands, scores, players_in, committed, pot): + winners = [] + maxscore = -1 + for i, hand in enumerate(hands): + if players_in[i]: + s = scores[hand] + if len(winners) == 0 or s > maxscore: + maxscore = s + winners = [i] + elif s == maxscore: + winners.append(i) + payoff = pot / float(len(winners)) + payoffs = [-x for x in committed] + for w in winners: + payoffs[w] += payoff + return payoffs + + +class Node(object): + """Abstraction of game tree node. + + Parameters + ---------- + proba : float in the interval [0, 1] + Probability with which this node is forked from its parent. + + Attributes + ---------- + proba_ : float in the interval [0, 1] + The probability (induced by the chance player) of the path from + the root node to this node. + """ + def __init__(self, parent, committed, holecards, board, deck, bet_history, + proba=1.): + self.committed = deepcopy(committed) + self.holecards = deepcopy(holecards) + self.board = deepcopy(board) + self.deck = deepcopy(deck) + self.bet_history = deepcopy(bet_history) + self.proba = self.proba_ = proba + if parent: + self.parent = parent + self.parent.add_child(self) + self.proba_ *= parent.proba_ + + def add_child(self, child): + if self.children is None: + self.children = [child] + else: + self.children.append(child) + + def __repr__(self): + """For pretty-printing the node.""" + tokenize = lambda stuff: ('"%s"' % stuff) if isinstance( + stuff, basestring) else stuff + return "%s(" % (self.__class__.__name__) + ", ".join( + ["%s=%s" % (k, tokenize(getattr(self, k))) + for k in ["bet_history", "deck", "holecards", "player", + "player_view", "committed", "proba_", "payoffs"] + if hasattr(self, k)]) + ")" + + +class TerminalNode(Node): + def __init__(self, parent, committed, holecards, board, deck, bet_history, + payoffs, players_in, **kwargs): + Node.__init__(self, parent, committed, + holecards, board, deck, bet_history, **kwargs) + self.payoffs = payoffs + self.players_in = deepcopy(players_in) + + +class HolecardChanceNode(Node): + def __init__(self, parent, committed, holecards, board, deck, bet_history, + todeal, **kwargs): + Node.__init__(self, parent, committed, + holecards, board, deck, bet_history, **kwargs) + self.todeal = todeal + self.children = [] + + +class BoardcardChanceNode(Node): + def __init__(self, parent, committed, holecards, board, deck, bet_history, + todeal, **kwargs): + Node.__init__(self, parent, committed, + holecards, board, deck, bet_history, **kwargs) + self.todeal = todeal + self.children = [] + + +class ActionNode(Node): + def __init__(self, parent, committed, holecards, board, deck, bet_history, + player, infoset_format, **kwargs): + Node.__init__(self, parent, committed, + holecards, board, deck, bet_history, **kwargs) + self.player = player + self.children = [] + self.raise_action = None + self.call_action = None + self.fold_action = None + self.player_view = infoset_format( + player, holecards[player], board, bet_history) + + def valid(self, action): + if action == FOLD: + return self.fold_action + if action == CALL: + return self.call_action + if action == RAISE: + return self.raise_action + raise Exception( + "Unknown action {0}. Action must be FOLD, CALL, or RAISE".format( + action)) + + def get_child(self, action): + return self.valid(action) diff --git a/pycfr/popcount.py b/pycfr/popcount.py new file mode 100644 index 0000000..869ea8f --- /dev/null +++ b/pycfr/popcount.py @@ -0,0 +1,15 @@ +class PopCount: + # Generate table of popcounts for 16 bits + # Then just use it twice. + # Reference is some stanford paper. + # See + # http://www.valuedlessons.com/2009/01/popcount-in-python-with-benchmarks.html + POPCOUNT_TABLE16 = [0] * 2 ** 16 + for index in xrange(len(POPCOUNT_TABLE16)): + POPCOUNT_TABLE16[index] = (index & 1) + POPCOUNT_TABLE16[index >> 1] + + def popcount32_table16(v): + return (PopCount.POPCOUNT_TABLE16[v & 0xffff] + + PopCount.POPCOUNT_TABLE16[(v >> 16) & 0xffff]) + + popcount = staticmethod(popcount32_table16) diff --git a/tests/.DS_Store b/pycfr/tests/.DS_Store similarity index 100% rename from tests/.DS_Store rename to pycfr/tests/.DS_Store diff --git a/pycfr/tests/__init__.py b/pycfr/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/hskuhn_eq0.strat b/pycfr/tests/hskuhn_eq0.strat similarity index 100% rename from tests/hskuhn_eq0.strat rename to pycfr/tests/hskuhn_eq0.strat diff --git a/tests/hskuhn_eq1.strat b/pycfr/tests/hskuhn_eq1.strat similarity index 100% rename from tests/hskuhn_eq1.strat rename to pycfr/tests/hskuhn_eq1.strat diff --git a/tests/leduc_eq0.strat b/pycfr/tests/leduc_eq0.strat similarity index 100% rename from tests/leduc_eq0.strat rename to pycfr/tests/leduc_eq0.strat diff --git a/tests/leduc_eq1.strat b/pycfr/tests/leduc_eq1.strat similarity index 100% rename from tests/leduc_eq1.strat rename to pycfr/tests/leduc_eq1.strat diff --git a/tests/leduc_nash0_br.strat b/pycfr/tests/leduc_nash0_br.strat similarity index 100% rename from tests/leduc_nash0_br.strat rename to pycfr/tests/leduc_nash0_br.strat diff --git a/tests/leduc_nash1_br.strat b/pycfr/tests/leduc_nash1_br.strat similarity index 100% rename from tests/leduc_nash1_br.strat rename to pycfr/tests/leduc_nash1_br.strat diff --git a/pycfr/tests/test_cfr.py b/pycfr/tests/test_cfr.py new file mode 100644 index 0000000..8147f61 --- /dev/null +++ b/pycfr/tests/test_cfr.py @@ -0,0 +1,64 @@ +from ..pokerstrategy import * +from ..pokergames import * +from ..pokercfr import * + + +def near(val, expected, distance=0.0001): + return val >= (expected - distance) and val <= (expected + distance) + +print '' +print '' +print 'Testing CFR' +print '' +print '' + +print 'Computing NE for Half-Street Kuhn poker' + +hskuhn = half_street_kuhn_rules() +cfr = CounterfactualRegretMinimizer(hskuhn) +iterations_per_block = 1000 +blocks = 10 +for block in range(blocks): + print 'Iterations: {0}'.format(block * iterations_per_block) + cfr.run(iterations_per_block) + result = cfr.profile.best_response() + print 'Best response EV: {0}'.format(result[1]) + print 'Total exploitability: {0}'.format(sum(result[1])) +print cfr.profile.strategies[0].policy +print cfr.profile.strategies[1].policy +print cfr.counterfactual_regret + + +def test_verify_p1_policy(): + assert(near(cfr.profile.strategies[0].policy['Q:/:'][CALL], 2.0 / 3.0, 0.01)) + assert(near(cfr.profile.strategies[0].policy['Q:/:'][RAISE], 1.0 / 3.0, 0.01)) + assert(near(cfr.profile.strategies[0].policy['K:/:'][CALL], 1, 0.01)) + assert(near(cfr.profile.strategies[0].policy['K:/:'][RAISE], 0, 0.01)) + assert(near(cfr.profile.strategies[0].policy['A:/:'][CALL], 0, 0.01)) + assert(near(cfr.profile.strategies[0].policy['A:/:'][RAISE], 1.0, 0.01)) + +def test_verify_p2_policy(): + assert(near(cfr.profile.strategies[1].policy['Q:/r:'][FOLD], 1.0, 0.01)) + assert(near(cfr.profile.strategies[1].policy['Q:/r:'][CALL], 0, 0.01)) + assert(near(cfr.profile.strategies[1].policy['K:/r:'][FOLD], 2.0 / 3.0, 0.01)) + assert(near(cfr.profile.strategies[1].policy['K:/r:'][CALL], 1.0 / 3.0, 0.01)) + assert(near(cfr.profile.strategies[1].policy['A:/r:'][FOLD], 0, 0.01)) + assert(near(cfr.profile.strategies[1].policy['A:/r:'][CALL], 1.0, 0.01)) + + +# # XXX too long to run + is not testing anythx +# print 'Computing NE for Leduc poker' +# leduc = leduc_rules() + +# cfr = CounterfactualRegretMinimizer(leduc) + +# iterations_per_block = 10 +# blocks = 1000 +# for block in range(blocks): +# print 'Iterations: {0}'.format(block * iterations_per_block) +# cfr.run(iterations_per_block) +# result = cfr.profile.best_response() +# print 'Best response EV: {0}'.format(result[1]) +# print 'Total exploitability: {0}'.format(sum(result[1])) +# print 'Done!' +# print '' diff --git a/pycfr/tests/test_cscfr.py b/pycfr/tests/test_cscfr.py new file mode 100644 index 0000000..e8c541d --- /dev/null +++ b/pycfr/tests/test_cscfr.py @@ -0,0 +1,60 @@ +from ..pokerstrategy import * +from ..pokergames import * +from ..pokercfr import * + + +def near(val, expected, distance=0.0001): + return val >= (expected - distance) and val <= (expected + distance) + +print '' +print '' +print 'Testing Chance Sampling (CS) CFR' +print '' +print '' + +print 'Computing NE for Half-Street Kuhn poker' + +hskuhn = half_street_kuhn_rules() +cfr = ChanceSamplingCFR(hskuhn) +iterations_per_block = 1000 +blocks = 100 +for block in range(blocks): + print 'Iterations: {0}'.format(block * iterations_per_block) + cfr.run(iterations_per_block) + result = cfr.profile.best_response() + print 'Best response EV: {0} '.format(result[1]) + print 'Total exploitability: {0}'.format(sum(result[1])) +print cfr.profile.strategies[0].policy +print cfr.profile.strategies[1].policy +print cfr.counterfactual_regret + +def test_p1_policy(): + assert(near(cfr.profile.strategies[0].policy['Q:/:'][CALL], 2.0 / 3.0, 0.1)) + assert(near(cfr.profile.strategies[0].policy['Q:/:'][RAISE], 1.0 / 3.0, 0.1)) + assert(near(cfr.profile.strategies[0].policy['K:/:'][CALL], 1, 0.1)) + assert(near(cfr.profile.strategies[0].policy['K:/:'][RAISE], 0, 0.1)) + assert(near(cfr.profile.strategies[0].policy['A:/:'][CALL], 0, 0.1)) + assert(near(cfr.profile.strategies[0].policy['A:/:'][RAISE], 1.0, 0.1)) + + +def test_p2_policy(): + assert(near(cfr.profile.strategies[1].policy['Q:/r:'][FOLD], 1.0, 0.1)) + assert(near(cfr.profile.strategies[1].policy['Q:/r:'][CALL], 0, 0.1)) + assert(near(cfr.profile.strategies[1].policy['K:/r:'][FOLD], 2.0 / 3.0, 0.1)) + assert(near(cfr.profile.strategies[1].policy['K:/r:'][CALL], 1.0 / 3.0, 0.1)) + assert(near(cfr.profile.strategies[1].policy['A:/r:'][FOLD], 0, 0.1)) + assert(near(cfr.profile.strategies[1].policy['A:/r:'][CALL], 1.0, 0.1)) + + +# # XXX not testing anythx! +# print 'Computing NE for Leduc poker' +# leduc = leduc_rules() +# cfr = ChanceSamplingCFR(leduc) +# iterations_per_block = 1000 +# blocks = 1000 +# for block in range(blocks): +# print 'Iterations: {0}'.format(block * iterations_per_block) +# cfr.run(iterations_per_block) +# result = cfr.profile.best_response() +# print 'Best response EV: {0}'.format(result[1]) +# print 'Total exploitability: {0}'.format(sum(result[1])) diff --git a/pycfr/tests/test_gametree.py b/pycfr/tests/test_gametree.py new file mode 100644 index 0000000..ccb96c8 --- /dev/null +++ b/pycfr/tests/test_gametree.py @@ -0,0 +1,245 @@ +from nose.tools import assert_equal +from ..pokertrees import (GameTree, PublicTree, GameRules, RoundInfo, + HolecardChanceNode, BoardcardChanceNode, + ActionNode, TerminalNode) +from ..card import Card +from ..pokergames import leduc_eval, kuhn_eval, leduc_format + + +rules = GameRules(players=2, deck=[Card(14, 1), Card(13, 2), Card(13, 1), + Card(12, 1)], + rounds=[RoundInfo(holecards=1, boardcards=0, betsize=1, + maxbets=[2, 2]), + RoundInfo(holecards=0, boardcards=1, betsize=2, + maxbets=[2, 2])], ante=1, blinds=[1, 2], + handeval=leduc_eval) + + +def test_gametree(): + tree = GameTree(rules) + tree.build() + assert(type(tree.root) == HolecardChanceNode) + assert(len(tree.root.children) == 12) + assert(type(tree.root.children[0]) == ActionNode) + assert(tree.root.children[0].player == 0) + assert(len(tree.root.children[0].children) == 2) + assert(tree.root.children[0].player_view == "As:/:") + # /f + assert(type(tree.root.children[0].children[0]) == TerminalNode) + assert(tree.root.children[0].children[0].payoffs == [-2, 2]) + assert(tree.root.children[0].children[0].bet_history == '/f') + # /c + assert(type(tree.root.children[0].children[1]) == ActionNode) + assert(tree.root.children[0].children[1].bet_history == '/c') + assert(len(tree.root.children[0].children[1].children) == 1) + assert(tree.root.children[0].children[1].player == 1) + assert(tree.root.children[0].children[1].fold_action is None) + assert(tree.root.children[0].children[1].call_action != None) + assert(tree.root.children[0].children[1].raise_action is None) + assert(tree.root.children[0].children[1].player_view == "Kh:/c:") + # /cc/ [boardcard] + assert( + type(tree.root.children[0].children[1].children[0]) == BoardcardChanceNode) + assert(tree.root.children[0].children[1].children[0].bet_history == '/cc/') + assert(len(tree.root.children[0].children[1].children[0].children) == 2) + # /cc/ [action] + assert( + type(tree.root.children[0].children[1].children[0].children[0]) == ActionNode) + assert(tree.root.children[0].children[ + 1].children[0].children[0].bet_history == '/cc/') + assert( + len(tree.root.children[0].children[1].children[0].children[0].children) == 2) + assert(tree.root.children[0].children[1].children[0].children[0].player == 0) + assert(tree.root.children[0].children[ + 1].children[0].children[0].fold_action is None) + assert(tree.root.children[0].children[ + 1].children[0].children[0].call_action != None) + assert(tree.root.children[0].children[ + 1].children[0].children[0].raise_action != None) + assert(tree.root.children[0].children[1].children[ + 0].children[0].player_view == 'AsKs:/cc/:') + # /cc/r + assert( + type(tree.root.children[0].children[1].children[0].children[0].children[1]) == ActionNode) + assert(tree.root.children[0].children[1].children[ + 0].children[0].children[1].bet_history == '/cc/r') + assert( + len(tree.root.children[0].children[1].children[0].children[0].children[1].children) == 3) + assert(tree.root.children[0].children[1].children[ + 0].children[0].children[1].player == 1) + assert(tree.root.children[0].children[1].children[ + 0].children[0].children[1].fold_action != None) + assert(tree.root.children[0].children[1].children[ + 0].children[0].children[1].call_action != None) + assert(tree.root.children[0].children[1].children[ + 0].children[0].children[1].raise_action != None) + assert(tree.root.children[0].children[1].children[ + 0].children[0].children[1].player_view == 'KhKs:/cc/r:') + # /cc/c + assert( + type(tree.root.children[0].children[1].children[0].children[0].children[0]) == ActionNode) + assert(tree.root.children[0].children[1].children[ + 0].children[0].children[0].bet_history == '/cc/c') + assert( + len(tree.root.children[0].children[1].children[0].children[0].children[0].children) == 2) + assert(tree.root.children[0].children[1].children[ + 0].children[0].children[0].player == 1) + assert(tree.root.children[0].children[1].children[ + 0].children[0].children[0].fold_action is None) + assert(tree.root.children[0].children[1].children[ + 0].children[0].children[0].call_action != None) + assert(tree.root.children[0].children[1].children[ + 0].children[0].children[0].raise_action != None) + assert(tree.root.children[0].children[1].children[ + 0].children[0].children[0].player_view == 'KhKs:/cc/c:') + # /cc/cc + assert(type(tree.root.children[0].children[1].children[ + 0].children[0].children[0].children[0]) == TerminalNode) + assert(tree.root.children[0].children[1].children[0].children[ + 0].children[0].children[0].bet_history == '/cc/cc') + assert(tree.root.children[0].children[1].children[ + 0].children[0].children[0].children[0].payoffs == [-3, 3]) + # /cc/cr + assert(type(tree.root.children[0].children[1].children[ + 0].children[0].children[0].children[1]) == ActionNode) + assert(tree.root.children[0].children[1].children[0].children[ + 0].children[0].children[1].bet_history == '/cc/cr') + assert(len(tree.root.children[0].children[1].children[ + 0].children[0].children[0].children[1].children) == 3) + assert(tree.root.children[0].children[1].children[ + 0].children[0].children[0].children[1].player == 0) + assert(tree.root.children[0].children[1].children[ + 0].children[0].children[0].children[1].fold_action != None) + assert(tree.root.children[0].children[1].children[ + 0].children[0].children[0].children[1].call_action != None) + assert(tree.root.children[0].children[1].children[ + 0].children[0].children[0].children[1].raise_action != None) + assert(tree.root.children[0].children[1].children[0].children[ + 0].children[0].children[1].player_view == 'AsKs:/cc/cr:') + # /cc/crr + assert(type(tree.root.children[0].children[1].children[ + 0].children[0].children[0].children[1].children[2]) == ActionNode) + assert(tree.root.children[0].children[1].children[0].children[ + 0].children[0].children[1].children[2].bet_history == '/cc/crr') + assert(len(tree.root.children[0].children[1].children[ + 0].children[0].children[0].children[1].children[2].children) == 2) + assert(tree.root.children[0].children[1].children[0].children[ + 0].children[0].children[1].children[2].player == 1) + assert(tree.root.children[0].children[1].children[0].children[ + 0].children[0].children[1].children[2].fold_action != None) + assert(tree.root.children[0].children[1].children[0].children[ + 0].children[0].children[1].children[2].call_action != None) + assert(tree.root.children[0].children[1].children[0].children[ + 0].children[0].children[1].children[2].raise_action is None) + assert(tree.root.children[0].children[1].children[0].children[ + 0].children[0].children[1].children[2].player_view == 'KhKs:/cc/crr:') + # /cc/crrf + assert(type(tree.root.children[0].children[1].children[0].children[ + 0].children[0].children[1].children[2].children[0]) == TerminalNode) + assert(tree.root.children[0].children[1].children[0].children[0].children[ + 0].children[1].children[2].children[0].bet_history == '/cc/crrf') + assert(tree.root.children[0].children[1].children[0].children[ + 0].children[0].children[1].children[2].children[0].payoffs == [5, -5]) + # /cc/crrc + assert(type(tree.root.children[0].children[1].children[0].children[ + 0].children[0].children[1].children[2].children[1]) == TerminalNode) + assert(tree.root.children[0].children[1].children[0].children[0].children[ + 0].children[1].children[2].children[1].bet_history == '/cc/crrc') + assert(tree.root.children[0].children[1].children[0].children[ + 0].children[0].children[1].children[2].children[1].payoffs == [-7, 7]) + + +def test_publictree(): + tree = PublicTree(rules) + tree.build() + + assert(type(tree.root) == HolecardChanceNode) + assert(len(tree.root.children) == 1) + # / + assert(type(tree.root.children[0]) == ActionNode) + assert(tree.root.children[0].player == 0) + assert(tree.root.children[0].player_view == ( + 'As:/:', 'Kh:/:', 'Ks:/:', 'Qs:/:')) + assert(len(tree.root.children[0].children) == 2) + assert(tree.root.children[0].fold_action != None) + assert(tree.root.children[0].call_action != None) + assert(tree.root.children[0].raise_action == None) + # /f + assert(type(tree.root.children[0].children[0]) == TerminalNode) + assert(tree.root.children[0].children[0].payoffs == {((Card(14, 1),), (Card(13, 1),)): [-2, 2], ((Card(14, 1),), (Card(13, 2),)): [-2, 2], ((Card(14, 1),), (Card(12, 1),)): [-2, 2], ((Card(13, 1),), (Card(14, 1),)): [-2, 2], ((Card(13, 1),), (Card(13, 2),)): [-2, 2], ( + (Card(13, 1),), (Card(12, 1),)): [-2, 2], ((Card(13, 2),), (Card(14, 1),)): [-2, 2], ((Card(13, 2),), (Card(13, 1),)): [-2, 2], ((Card(13, 2),), (Card(12, 1),)): [-2, 2], ((Card(12, 1),), (Card(14, 1),)): [-2, 2], ((Card(12, 1),), (Card(13, 2),)): [-2, 2], ((Card(12, 1),), (Card(13, 1),)): [-2, 2]}) + # /c + assert(type(tree.root.children[0].children[1]) == ActionNode) + assert(tree.root.children[0].children[1].player == 1) + assert(tree.root.children[0].children[1].player_view == ( + 'As:/c:', 'Kh:/c:', 'Ks:/c:', 'Qs:/c:')) + assert(len(tree.root.children[0].children[1].children) == 1) + # /cc/ [boardcard] + assert( + type(tree.root.children[0].children[1].children[0]) == BoardcardChanceNode) + assert(tree.root.children[0].children[1].children[0].bet_history == '/cc/') + assert(len(tree.root.children[0].children[1].children[0].children) == 4) + # xAs:/cc/ [action] + assert( + type(tree.root.children[0].children[1].children[0].children[0]) == ActionNode) + assert(tree.root.children[0].children[ + 1].children[0].children[0].bet_history == '/cc/') + assert( + len(tree.root.children[0].children[1].children[0].children[0].children) == 2) + assert(tree.root.children[0].children[1].children[0].children[0].player == 0) + assert(tree.root.children[0].children[ + 1].children[0].children[0].fold_action is None) + assert(tree.root.children[0].children[ + 1].children[0].children[0].call_action != None) + assert(tree.root.children[0].children[ + 1].children[0].children[0].raise_action != None) + assert(tree.root.children[0].children[1].children[0].children[ + 0].player_view == ('KhAs:/cc/:', 'KsAs:/cc/:', 'QsAs:/cc/:')) + # xAs:/cc/r + assert( + type(tree.root.children[0].children[1].children[0].children[0].children[1]) == ActionNode) + assert(tree.root.children[0].children[1].children[ + 0].children[0].children[1].bet_history == '/cc/r') + assert( + len(tree.root.children[0].children[1].children[0].children[0].children[1].children) == 3) + assert(tree.root.children[0].children[1].children[ + 0].children[0].children[1].player == 1) + assert(tree.root.children[0].children[1].children[ + 0].children[0].children[1].fold_action != None) + assert(tree.root.children[0].children[1].children[ + 0].children[0].children[1].call_action != None) + assert(tree.root.children[0].children[1].children[ + 0].children[0].children[1].raise_action != None) + assert(tree.root.children[0].children[1].children[0].children[0].children[ + 1].player_view == ('KhAs:/cc/r:', 'KsAs:/cc/r:', 'QsAs:/cc/r:')) + # xAs:/cc/rc + assert(type(tree.root.children[0].children[1].children[ + 0].children[0].children[1].children[1]) == TerminalNode) + assert(tree.root.children[0].children[1].children[0].children[ + 0].children[1].children[1].bet_history == '/cc/rc') + assert(tree.root.children[0].children[1].children[0].children[0].children[1].children[1].payoffs == {((Card(13, 1),), (Card(13, 2),)): [0, 0], ((Card(13, 1),), (Card(12, 1),)): [ + 5, -5], ((Card(13, 2),), (Card(13, 1),)): [0, 0], ((Card(13, 2),), (Card(12, 1),)): [5, -5], ((Card(12, 1),), (Card(13, 2),)): [-5, 5], ((Card(12, 1),), (Card(13, 1),)): [-5, 5]}) + # xKh:/cc/rc + assert(type(tree.root.children[0].children[1].children[ + 0].children[1].children[1].children[1]) == TerminalNode) + assert(tree.root.children[0].children[1].children[0].children[ + 1].children[1].children[1].bet_history == '/cc/rc') + assert(tree.root.children[0].children[1].children[0].children[1].children[1].children[1].payoffs == {((Card(13, 1),), (Card(14, 1),)): [5, -5], ((Card(13, 1),), (Card(12, 1),)): [ + 5, -5], ((Card(14, 1),), (Card(13, 1),)): [-5, 5], ((Card(14, 1),), (Card(12, 1),)): [5, -5], ((Card(12, 1),), (Card(14, 1),)): [-5, 5], ((Card(12, 1),), (Card(13, 1),)): [-5, 5]}) + + +def test_deal_holecards(): + players = 2 + deck = [Card(14, 1), Card(13, 1), Card(12, 1)] + ante = 1 + blinds = None + rounds = [RoundInfo(holecards=1, boardcards=0, betsize=1, maxbets=[1, 1])] + rules = GameRules(players, deck, rounds, ante, blinds, handeval=kuhn_eval, + infoset_format=leduc_format) + tree = GameTree(rules) + holes, proba = tree.deal_holecards(tree.rules.deck, + tree.rules.roundinfo[0].holecards, + tree.rules.players) + holes = list(holes) + assert_equal(len(holes), 6) + assert_equal(proba, 1. / len(holes)) diff --git a/pycfr/tests/test_kuhn.py b/pycfr/tests/test_kuhn.py new file mode 100644 index 0000000..ef21667 --- /dev/null +++ b/pycfr/tests/test_kuhn.py @@ -0,0 +1,33 @@ +from nose.tools import assert_equal +from ..pokergames import kuhn_gametree + + +def test_infosets(): + # It's known that each non-chance player in Kuhn's poker has exactly 6 + # information sets, each containing 2 nodes + kuhn_gt = kuhn_gametree() + for player in xrange(2): + infosets = kuhn_gt.get_player_information_sets(player) + assert_equal(len(infosets), 6) + for infoset in infosets.itervalues(): + assert_equal(len(infoset), 2) + + +def test_node_count(): + # It's known that the game tree for Kuhn Poker has 55 nodes, of which + # 30 are leafs + kuhn_gt = kuhn_gametree() + assert_equal(len(kuhn_gt.nodes), 55) + assert_equal(len(kuhn_gt.leafs), 30) + + +def test_payoffs(): + kuhn_gt = kuhn_gametree() + payoffs = [] + for node in kuhn_gt.nodes: + if "Terminal" in node.__class__.__name__: + payoffs.append(node.payoffs[0]) + payoffs = sorted(payoffs) + assert_equal(payoffs, [-2, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, -1, + -1, -1, -1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, + 2, 2, 2]) diff --git a/pycfr/tests/test_oscfr.py b/pycfr/tests/test_oscfr.py new file mode 100644 index 0000000..1ff7238 --- /dev/null +++ b/pycfr/tests/test_oscfr.py @@ -0,0 +1,84 @@ +from ..pokerstrategy import * +from ..pokergames import * +from ..pokercfr import * + + +def near(val, expected, distance=0.0001): + return val >= (expected - distance) and val <= (expected + distance) + +print '' +print '' +print 'Testing Outcome Sampling (OS) CFR' +print '' +print '' + +print 'Computing NE for Kuhn poker' +kuhn = kuhn_rules() + +cfr = OutcomeSamplingCFR(kuhn) + +iterations_per_block = 2000 + +blocks = 100 +for block in range(blocks): + print 'Iterations: {0}'.format(block * iterations_per_block) + cfr.run(iterations_per_block) + result = cfr.profile.best_response() + print 'Best response EV: {0}'.format(result[1]) + print 'Total exploitability: {0}'.format(sum(result[1])) +print 'Done!' +print '' + +print 'Computing NE for Half-Street Kuhn poker' + +hskuhn = half_street_kuhn_rules() +cfr = OutcomeSamplingCFR(hskuhn) +iterations_per_block = 1000 +blocks = 100 +for block in range(blocks): + print 'Iterations: {0}'.format(block * iterations_per_block) + cfr.run(iterations_per_block) + result = cfr.profile.best_response() + print 'Best response EV: {0} '.format(result[1]) + print 'Total exploitability: {0}'.format(sum(result[1])) +print cfr.profile.strategies[0].policy +print cfr.profile.strategies[1].policy +print cfr.counterfactual_regret + + +def test_p1_policy(): + assert(near(cfr.profile.strategies[0].policy['Q:/:'][CALL], + 2.0 / 3.0, .1)) + assert(near(cfr.profile.strategies[0].policy['Q:/:'][RAISE], + 1.0 / 3.0, .1)) + assert(near(cfr.profile.strategies[0].policy['K:/:'][CALL], + 1, .1)) + assert(near(cfr.profile.strategies[0].policy['K:/:'][RAISE], + 0, .1)) + assert(near(cfr.profile.strategies[0].policy['A:/:'][CALL], 0, 0.1)) + assert(near(cfr.profile.strategies[0].policy['A:/:'][RAISE], 1.0, 0.1)) + + +def test_p2_policy(): + assert(near(cfr.profile.strategies[1].policy['Q:/r:'][FOLD], 1.0, 0.1)) + assert(near(cfr.profile.strategies[1].policy['Q:/r:'][CALL], 0, 0.1)) + assert(near(cfr.profile.strategies[1].policy['K:/r:'][FOLD], 2.0 / 3.0, 0.1)) + assert(near(cfr.profile.strategies[1].policy['K:/r:'][CALL], 1.0 / 3.0, 0.1)) + assert(near(cfr.profile.strategies[1].policy['A:/r:'][FOLD], 0, 0.1)) + assert(near(cfr.profile.strategies[1].policy['A:/r:'][CALL], 1.0, 0.1)) + + +# # XXX not testing anythx! +# print 'Computing NE for Leduc poker' +# leduc = leduc_rules() + +# cfr = OutcomeSamplingCFR(leduc) + +# iterations_per_block = 10000 +# blocks = 1000 +# for block in range(blocks): +# print 'Iterations: {0}'.format(block * iterations_per_block) +# cfr.run(iterations_per_block) +# result = cfr.profile.best_response() +# print 'Best response EV: {0}'.format(result[1]) +# print 'Total exploitability: {0}'.format(sum(result[1])) diff --git a/pycfr/tests/test_pcscfr.py b/pycfr/tests/test_pcscfr.py new file mode 100644 index 0000000..0279de5 --- /dev/null +++ b/pycfr/tests/test_pcscfr.py @@ -0,0 +1,66 @@ +from ..pokerstrategy import * +from ..pokergames import * +from ..pokercfr import * + + +def near(val, expected, distance=0.0001): + return val >= (expected - distance) and val <= (expected + distance) + + +hskuhn = half_street_kuhn_rules() +cfr = PublicChanceSamplingCFR(hskuhn) + + +def test_public_chance_sampling(): + print '' + print '' + print 'Testing Public Chance Sampling (PCS) CFR' + print '' + print '' + + print 'Computing NE for Half-Street Kuhn poker' + + iterations_per_block = 1000 + blocks = 10 + for block in range(blocks): + print 'Iterations: {0}'.format(block * iterations_per_block) + cfr.run(iterations_per_block) + result = cfr.profile.best_response() + print 'Best response EV: {0}'.format(result[1]) + print 'Total exploitability: {0}'.format(sum(result[1])) + print cfr.profile.strategies[0].policy + print cfr.profile.strategies[1].policy + print cfr.counterfactual_regret + + +def test_p1_policy(): + assert(near(cfr.profile.strategies[0].policy['Q:/:'][CALL], 2.0 / 3.0, 0.01)) + assert(near(cfr.profile.strategies[0].policy['Q:/:'][RAISE], 1.0 / 3.0, 0.01)) + assert(near(cfr.profile.strategies[0].policy['K:/:'][CALL], 1, 0.01)) + assert(near(cfr.profile.strategies[0].policy['K:/:'][RAISE], 0, 0.01)) + assert(near(cfr.profile.strategies[0].policy['A:/:'][CALL], 0, 0.01)) + assert(near(cfr.profile.strategies[0].policy['A:/:'][RAISE], 1.0, 0.01)) + + +def test_p2_policy(): + assert(near(cfr.profile.strategies[1].policy['Q:/r:'][FOLD], 1.0, 0.01)) + assert(near(cfr.profile.strategies[1].policy['Q:/r:'][CALL], 0, 0.01)) + assert(near(cfr.profile.strategies[1].policy['K:/r:'][FOLD], 2.0 / 3.0, 0.01)) + assert(near(cfr.profile.strategies[1].policy['K:/r:'][CALL], 1.0 / 3.0, 0.01)) + assert(near(cfr.profile.strategies[1].policy['A:/r:'][FOLD], 0, 0.01)) + assert(near(cfr.profile.strategies[1].policy['A:/r:'][CALL], 1.0, 0.01)) + + + +# # XXX Takes too long to run + is not testing anythx +# def test_compute_leduc_nash_e(): +# leduc = leduc_rules() +# cfr = PublicChanceSamplingCFR(leduc) +# iterations_per_block = 1000 +# blocks = 1000 +# for block in range(blocks): +# print 'Iterations: {0}'.format(block * iterations_per_block) +# cfr.run(iterations_per_block) +# result = cfr.profile.best_response() +# print 'Best response EV: {0}'.format(result[1]) +# print 'Total exploitability: {0}'.format(sum(result[1])) diff --git a/pycfr/tests/test_strategy.py b/pycfr/tests/test_strategy.py new file mode 100644 index 0000000..8e50336 --- /dev/null +++ b/pycfr/tests/test_strategy.py @@ -0,0 +1,231 @@ +from ..pokerstrategy import StrategyProfile, Strategy +from ..pokertrees import PublicTree, RoundInfo, GameRules, GameTree +from ..pokergames import (leduc_rules, leduc_eval, leduc_format, + half_street_kuhn_rules, half_street_kuhn_gametree) +from ..card import Card + + +def near(val, expected): + return val >= (expected - 0.0001) and val <= (expected + 0.0001) + + +def validate_strategy(s, filename): + validation_strategy = Strategy(s.player) + validation_strategy.load_from_file(filename) + assert(s.policy == validation_strategy.policy) + + +def test_noaction_game(): + print 'No-action game with 1 holecard followed by 1 boardcard' + players = 2 + deck = [Card(14, 1), Card(13, 1), Card(12, 1), Card(11, 1)] + ante = 1 + blinds = None + rounds = [RoundInfo(holecards=1, boardcards=1, betsize=1, maxbets=[0, 0])] + no_action_rules = GameRules(players, deck, rounds, ante, + blinds, handeval=leduc_eval, infoset_format=leduc_format) + no_action_gametree = GameTree(no_action_rules) + no_action_gametree.build() + s0 = Strategy(0) + s0.build_default(no_action_gametree) + s1 = Strategy(1) + s1.build_default(no_action_gametree) + profile = StrategyProfile(no_action_rules, [s0, s1]) + expected_value = profile.expected_value() + # print 'Expected values: [{0:.9f},{1:.9f}]'.format(expected_value[0], + # expected_value[1]) + print 'Expected values: [{0},{1}]'.format(expected_value[0], expected_value[1]) + assert(expected_value >= (-0.0001, -0.0001) + and expected_value <= (0.0001, 0.0001)) + best_responses = profile.best_response() + br = best_responses[0] + brev = best_responses[1] + # print 'Best responses: {0}'.format([r.policy for r in br.strategies]) + print 'Best response EV: {0}'.format(brev) + assert(brev >= (-0.0001, -0.0001) and brev <= (0.0001, 0.0001)) + + +def test_half_street_kuhn(): + hskuhn_rules = half_street_kuhn_rules() + hskuhn_gametree = half_street_kuhn_gametree() + + s0 = Strategy(0) + s0.policy = {'A:/:': [0, 0, 1], 'K:/:': [ + 0, 1, 0], 'Q:/:': [0, 2.0 / 3.0, 1.0 / 3.0]} + + s1 = Strategy(1) + s1.policy = {'A:/r:': [0, 1, 0], 'K:/r:': [2.0 / 3.0, 1.0 / 3.0, 0], 'Q:/r:': [ + 1, 0, 0], 'A:/c:': [0, 1, 0], 'K:/c:': [0, 1, 0], 'Q:/c:': [0, 1, 0]} + + eq0 = Strategy(0) + eq0.build_default(hskuhn_gametree) + # eq0.save_to_file('hskuhn_eq0.strat') + + eq1 = Strategy(1) + eq1.build_default(hskuhn_gametree) + # eq1.save_to_file('hskuhn_eq1.strat') + + print "Half-Street Kuhn Expected Value" + print "Nash0 vs. Nash1: {0}".format(StrategyProfile(hskuhn_rules, [s0, s1]).expected_value()) + print "Nash0 vs. Eq1: {0}".format(StrategyProfile(hskuhn_rules, [s0, eq1]).expected_value()) + print "Eq0 vs. Nash1: {0}".format(StrategyProfile(hskuhn_rules, [eq0, s1]).expected_value()) + print "Eq0 vs. Eq1: {0}".format(StrategyProfile(hskuhn_rules, [eq0, eq1]).expected_value()) + print "" + + print "Half-Street Kuhn Best Response" + overbet0 = Strategy(0) + overbet0.policy = { + 'Q:/:': [0, 0.5, 0.5], 'K:/:': [0, 1, 0], 'A:/:': [0, 0, 1]} + overbet1 = Strategy(1) + overbet1.policy = {'Q:/c:': [0, 1, 0], 'K:/c:': [0, 1, 0], 'A:/c:': [ + 0, 1, 0], 'Q:/r:': [1, 0, 0], 'K:/r:': [0.5, 0.5, 0], 'A:/r:': [0, 1, 0]} + profile = StrategyProfile(hskuhn_rules, [overbet0, overbet1]) + result = profile.best_response() + br = result[0].strategies[0] + ev = result[1][0] + print "Overbet0 BR EV: {0}".format(ev) + print br.policy + assert(len(br.policy) == 3) + assert(br.probs('Q:/:') == [0, 1, 0]) + assert(br.probs('K:/:') == [0, 1, 0]) + assert(br.probs('A:/:') == [0, 0, 1]) + assert(near(ev, 1.0 / 12.0)) + + br = result[0].strategies[1] + ev = result[1][1] + print "Overbet1 BR: {0}".format(ev) + assert(len(br.policy) == 6) + assert(br.probs('Q:/c:') == [0, 1, 0]) + assert(br.probs('K:/c:') == [0, 1, 0]) + assert(br.probs('A:/c:') == [0, 1, 0]) + assert(br.probs('Q:/r:') == [1, 0, 0]) + assert(br.probs('K:/r:') == [0, 1, 0]) + assert(br.probs('A:/r:') == [0, 1, 0]) + assert(near(ev, 0)) + + +def test_leduc(): + rules = leduc_rules() + leduc_gt = PublicTree(rules) + leduc_gt.build() + + s0 = Strategy(0) + s0.load_from_file('strategies/leduc/0.strat') + # Test a couple of arbitrary values + assert(s0.probs('J:/:') == [0.000000000, 0.927357111, 0.072642889]) + assert(s0.probs('KJ:/rrc/rr:') == [0.546821151, 0.453178849, 0.000000000]) + # Verify we loaded all of infosets + assert(len(s0.policy) == 144) + + s1 = Strategy(1) + s1.load_from_file('strategies/leduc/1.strat') + # Test a couple of arbitrary values + assert(s1.probs('J:/r:') == [0.819456679, 0.125672407, 0.054870914]) + assert(s1.probs('KJ:/rrc/crr:') == [0.000031258, 0.999968742, 0.000000000]) + # Verify we loaded all of infosets + assert(len(s1.policy) == 144) + + # Generate a default strategy for player 0 + eq0 = Strategy(0) + eq0.build_default(leduc_gt) + # eq0.save_to_file('leduc_eq0.strat') + assert(len(eq0.policy) == 144) + + # Generate a default strategy for player 1 + eq1 = Strategy(1) + eq1.build_default(leduc_gt) + # eq1.save_to_file('leduc_eq1.strat') + assert(len(eq1.policy) == 144) + + rand0 = Strategy(0) + rand0.load_from_file('strategies/leduc/random.strat') + rand1 = Strategy(1) + rand1.load_from_file('strategies/leduc/random.strat') + + """ + All leduc test values were derived using the open_cfr implementation from UoAlberta + """ + print "Leduc Expected Value" + profile = StrategyProfile(rules, [s0, s1]) + profile.gametree = leduc_gt + result = profile.expected_value() + print "Nash0 vs. Nash1 EV: {0}".format(result) + assert(result[0] >= -0.085653 and result[0] <= -0.085651) + + profile = StrategyProfile(rules, [s0, eq1]) + profile.gametree = leduc_gt + result = profile.expected_value() + print "Nash0 vs. Eq1 EV: {0}".format(result) + assert(result[0] >= 0.59143 and result[0] <= 0.59145) + + profile = StrategyProfile(rules, [eq0, eq1]) + profile.gametree = leduc_gt + result = profile.expected_value() + print "Eq0 vs. Eq1: {0}".format(result) + assert(result[0] >= -0.078126 and result[0] <= -0.078124) + + profile = StrategyProfile(rules, [eq0, s1]) + profile.gametree = leduc_gt + result = profile.expected_value() + print "Eq0 vs. Nash1 EV: {0}".format(result) + assert(result[0] >= -0.840442 and result[0] <= -0.840440) + + profile = StrategyProfile(rules, [s0, rand1]) + profile.gametree = leduc_gt + result = profile.expected_value() + print "Nash0 vs. Random: {0}".format(result) + assert(result[0] >= 0.591873 and result[0] <= 0.591875) + + profile = StrategyProfile(rules, [rand0, s1]) + profile.gametree = leduc_gt + result = profile.expected_value() + print "Random vs. Nash1: {0}".format(result) + assert(result[0] >= -0.84791 and result[0] <= -0.84790) + + print "" + + print "Leduc Best Response" + profile = StrategyProfile(rules, [s0, s1]) + result = profile.best_response() + br = result[0].strategies[0] + ev = result[1][0] + print "Nash0 BR EV: {0}".format(ev) + # br.save_to_file('leduc_nash0_br.strat') + assert(near(ev, -0.0854363)) + validate_strategy(br, 'strategies/leduc/nash_br0.strat') + + br = result[0].strategies[1] + ev = result[1][1] + print "Nash1 BR EV: {0}".format(ev) + # br.save_to_file('leduc_nash1_br.strat') + assert(near(ev, 0.0858749)) + validate_strategy(br, 'strategies/leduc/nash_br1.strat') + + profile = StrategyProfile(rules, [eq0, eq1]) + result = profile.best_response() + br = result[0].strategies[0] + ev = result[1][0] + print "Eq0 BR EV: {0}".format(ev) + assert(near(ev, 2.0875)) + validate_strategy(br, 'strategies/leduc/equal_br0.strat') + + br = result[0].strategies[1] + ev = result[1][1] + print "Eq1 BR EV: {0}".format(ev) + assert(near(ev, 2.65972)) + validate_strategy(br, 'strategies/leduc/equal_br1.strat') + + profile = StrategyProfile(rules, [rand0, rand1]) + result = profile.best_response() + br = result[0].strategies[0] + ev = result[1][0] + print "Rand0 BR: {0}".format(ev) + assert(near(ev, 2.14414)) + validate_strategy(br, 'strategies/leduc/random_br0.strat') + + br = result[0].strategies[1] + ev = result[1][1] + print "Rand1 BR: {0}".format(ev) + assert(near(ev, 3.21721)) + validate_strategy(br, 'strategies/leduc/random_br1.strat') + diff --git a/tests/test_cfr.py b/tests/test_cfr.py deleted file mode 100644 index 2292dd9..0000000 --- a/tests/test_cfr.py +++ /dev/null @@ -1,64 +0,0 @@ -import sys -import os -sys.path.insert(0,os.path.realpath('.')) -from pokerstrategy import * -from pokergames import * -from pokercfr import * - -def near(val, expected, distance=0.0001): - return val >= (expected - distance) and val <= (expected + distance) - -print '' -print '' -print 'Testing CFR' -print '' -print '' - -print 'Computing NE for Half-Street Kuhn poker' - -hskuhn = half_street_kuhn_rules() -cfr = CounterfactualRegretMinimizer(hskuhn) -iterations_per_block = 1000 -blocks = 10 -for block in range(blocks): - print 'Iterations: {0}'.format(block * iterations_per_block) - cfr.run(iterations_per_block) - result = cfr.profile.best_response() - print 'Best response EV: {0}'.format(result[1]) - print 'Total exploitability: {0}'.format(sum(result[1])) -print cfr.profile.strategies[0].policy -print cfr.profile.strategies[1].policy -print cfr.counterfactual_regret -print 'Verifying P1 policy' -assert(near(cfr.profile.strategies[0].policy['Q:/:'][CALL], 2.0 / 3.0, 0.01)) -assert(near(cfr.profile.strategies[0].policy['Q:/:'][RAISE], 1.0 / 3.0, 0.01)) -assert(near(cfr.profile.strategies[0].policy['K:/:'][CALL], 1, 0.01)) -assert(near(cfr.profile.strategies[0].policy['K:/:'][RAISE], 0, 0.01)) -assert(near(cfr.profile.strategies[0].policy['A:/:'][CALL], 0, 0.01)) -assert(near(cfr.profile.strategies[0].policy['A:/:'][RAISE], 1.0, 0.01)) -print 'Verifying P2 policy' -assert(near(cfr.profile.strategies[1].policy['Q:/r:'][FOLD], 1.0, 0.01)) -assert(near(cfr.profile.strategies[1].policy['Q:/r:'][CALL], 0, 0.01)) -assert(near(cfr.profile.strategies[1].policy['K:/r:'][FOLD], 2.0 / 3.0, 0.01)) -assert(near(cfr.profile.strategies[1].policy['K:/r:'][CALL], 1.0 / 3.0, 0.01)) -assert(near(cfr.profile.strategies[1].policy['A:/r:'][FOLD], 0, 0.01)) -assert(near(cfr.profile.strategies[1].policy['A:/r:'][CALL], 1.0, 0.01)) - -print 'Done!' -print '' - -print 'Computing NE for Leduc poker' -leduc = leduc_rules() - -cfr = CounterfactualRegretMinimizer(leduc) - -iterations_per_block = 10 -blocks = 1000 -for block in range(blocks): - print 'Iterations: {0}'.format(block * iterations_per_block) - cfr.run(iterations_per_block) - result = cfr.profile.best_response() - print 'Best response EV: {0}'.format(result[1]) - print 'Total exploitability: {0}'.format(sum(result[1])) -print 'Done!' -print '' \ No newline at end of file diff --git a/tests/test_cscfr.py b/tests/test_cscfr.py deleted file mode 100644 index d2308df..0000000 --- a/tests/test_cscfr.py +++ /dev/null @@ -1,64 +0,0 @@ -import sys -import os -sys.path.insert(0,os.path.realpath('.')) -from pokerstrategy import * -from pokergames import * -from pokercfr import * - -def near(val, expected, distance=0.0001): - return val >= (expected - distance) and val <= (expected + distance) - -print '' -print '' -print 'Testing Chance Sampling (CS) CFR' -print '' -print '' - -print 'Computing NE for Half-Street Kuhn poker' - -hskuhn = half_street_kuhn_rules() -cfr = ChanceSamplingCFR(hskuhn) -iterations_per_block = 1000 -blocks = 50 -for block in range(blocks): - print 'Iterations: {0}'.format(block * iterations_per_block) - cfr.run(iterations_per_block) - result = cfr.profile.best_response() - print 'Best response EV: {0} '.format(result[1]) - print 'Total exploitability: {0}'.format(sum(result[1])) -print cfr.profile.strategies[0].policy -print cfr.profile.strategies[1].policy -print cfr.counterfactual_regret -print 'Verifying P1 policy' -assert(near(cfr.profile.strategies[0].policy['Q:/:'][CALL], 2.0 / 3.0, 0.01)) -assert(near(cfr.profile.strategies[0].policy['Q:/:'][RAISE], 1.0 / 3.0, 0.01)) -assert(near(cfr.profile.strategies[0].policy['K:/:'][CALL], 1, 0.01)) -assert(near(cfr.profile.strategies[0].policy['K:/:'][RAISE], 0, 0.01)) -assert(near(cfr.profile.strategies[0].policy['A:/:'][CALL], 0, 0.01)) -assert(near(cfr.profile.strategies[0].policy['A:/:'][RAISE], 1.0, 0.01)) -print 'Verifying P2 policy' -assert(near(cfr.profile.strategies[1].policy['Q:/r:'][FOLD], 1.0, 0.01)) -assert(near(cfr.profile.strategies[1].policy['Q:/r:'][CALL], 0, 0.01)) -assert(near(cfr.profile.strategies[1].policy['K:/r:'][FOLD], 2.0 / 3.0, 0.01)) -assert(near(cfr.profile.strategies[1].policy['K:/r:'][CALL], 1.0 / 3.0, 0.01)) -assert(near(cfr.profile.strategies[1].policy['A:/r:'][FOLD], 0, 0.01)) -assert(near(cfr.profile.strategies[1].policy['A:/r:'][CALL], 1.0, 0.01)) - -print 'Done!' -print '' - -print 'Computing NE for Leduc poker' -leduc = leduc_rules() - -cfr = ChanceSamplingCFR(leduc) - -iterations_per_block = 1000 -blocks = 1000 -for block in range(blocks): - print 'Iterations: {0}'.format(block * iterations_per_block) - cfr.run(iterations_per_block) - result = cfr.profile.best_response() - print 'Best response EV: {0}'.format(result[1]) - print 'Total exploitability: {0}'.format(sum(result[1])) -print 'Done!' -print '' \ No newline at end of file diff --git a/tests/test_gametree.py b/tests/test_gametree.py deleted file mode 100644 index e09b980..0000000 --- a/tests/test_gametree.py +++ /dev/null @@ -1,150 +0,0 @@ -import sys -import os -sys.path.insert(0,os.path.realpath('.')) -from pokertrees import * -from pokergames import * - -print 'Testing GameTree' -rules = GameRules(players = 2, deck = [Card(14,1),Card(13,2),Card(13,1),Card(12,1)], rounds = [RoundInfo(holecards=1,boardcards=0,betsize=1,maxbets=[2,2]),RoundInfo(holecards=0,boardcards=1,betsize=2,maxbets=[2,2])], ante = 1, blinds = [1,2], handeval=leduc_eval) -tree = GameTree(rules) -tree.build() -assert(type(tree.root) == HolecardChanceNode) -assert(len(tree.root.children) == 12) -assert(type(tree.root.children[0]) == ActionNode) -assert(tree.root.children[0].player == 0) -assert(len(tree.root.children[0].children) == 2) -assert(tree.root.children[0].player_view == "As:/:") -# /f -assert(type(tree.root.children[0].children[0]) == TerminalNode) -assert(tree.root.children[0].children[0].payoffs == [-2,2]) -assert(tree.root.children[0].children[0].bet_history == '/f') -# /c -assert(type(tree.root.children[0].children[1]) == ActionNode) -assert(tree.root.children[0].children[1].bet_history == '/c') -assert(len(tree.root.children[0].children[1].children) == 1) -assert(tree.root.children[0].children[1].player == 1) -assert(tree.root.children[0].children[1].fold_action is None) -assert(tree.root.children[0].children[1].call_action != None) -assert(tree.root.children[0].children[1].raise_action is None) -assert(tree.root.children[0].children[1].player_view == "Kh:/c:") -# /cc/ [boardcard] -assert(type(tree.root.children[0].children[1].children[0]) == BoardcardChanceNode) -assert(tree.root.children[0].children[1].children[0].bet_history == '/cc/') -assert(len(tree.root.children[0].children[1].children[0].children) == 2) -# /cc/ [action] -assert(type(tree.root.children[0].children[1].children[0].children[0]) == ActionNode) -assert(tree.root.children[0].children[1].children[0].children[0].bet_history == '/cc/') -assert(len(tree.root.children[0].children[1].children[0].children[0].children) == 2) -assert(tree.root.children[0].children[1].children[0].children[0].player == 0) -assert(tree.root.children[0].children[1].children[0].children[0].fold_action is None) -assert(tree.root.children[0].children[1].children[0].children[0].call_action != None) -assert(tree.root.children[0].children[1].children[0].children[0].raise_action != None) -assert(tree.root.children[0].children[1].children[0].children[0].player_view == 'AsKs:/cc/:') -# /cc/r -assert(type(tree.root.children[0].children[1].children[0].children[0].children[1]) == ActionNode) -assert(tree.root.children[0].children[1].children[0].children[0].children[1].bet_history == '/cc/r') -assert(len(tree.root.children[0].children[1].children[0].children[0].children[1].children) == 3) -assert(tree.root.children[0].children[1].children[0].children[0].children[1].player == 1) -assert(tree.root.children[0].children[1].children[0].children[0].children[1].fold_action != None) -assert(tree.root.children[0].children[1].children[0].children[0].children[1].call_action != None) -assert(tree.root.children[0].children[1].children[0].children[0].children[1].raise_action != None) -assert(tree.root.children[0].children[1].children[0].children[0].children[1].player_view == 'KhKs:/cc/r:') -# /cc/c -assert(type(tree.root.children[0].children[1].children[0].children[0].children[0]) == ActionNode) -assert(tree.root.children[0].children[1].children[0].children[0].children[0].bet_history == '/cc/c') -assert(len(tree.root.children[0].children[1].children[0].children[0].children[0].children) == 2) -assert(tree.root.children[0].children[1].children[0].children[0].children[0].player == 1) -assert(tree.root.children[0].children[1].children[0].children[0].children[0].fold_action is None) -assert(tree.root.children[0].children[1].children[0].children[0].children[0].call_action != None) -assert(tree.root.children[0].children[1].children[0].children[0].children[0].raise_action != None) -assert(tree.root.children[0].children[1].children[0].children[0].children[0].player_view == 'KhKs:/cc/c:') -# /cc/cc -assert(type(tree.root.children[0].children[1].children[0].children[0].children[0].children[0]) == TerminalNode) -assert(tree.root.children[0].children[1].children[0].children[0].children[0].children[0].bet_history == '/cc/cc') -assert(tree.root.children[0].children[1].children[0].children[0].children[0].children[0].payoffs == [-3,3]) -# /cc/cr -assert(type(tree.root.children[0].children[1].children[0].children[0].children[0].children[1]) == ActionNode) -assert(tree.root.children[0].children[1].children[0].children[0].children[0].children[1].bet_history == '/cc/cr') -assert(len(tree.root.children[0].children[1].children[0].children[0].children[0].children[1].children) == 3) -assert(tree.root.children[0].children[1].children[0].children[0].children[0].children[1].player == 0) -assert(tree.root.children[0].children[1].children[0].children[0].children[0].children[1].fold_action != None) -assert(tree.root.children[0].children[1].children[0].children[0].children[0].children[1].call_action != None) -assert(tree.root.children[0].children[1].children[0].children[0].children[0].children[1].raise_action != None) -assert(tree.root.children[0].children[1].children[0].children[0].children[0].children[1].player_view == 'AsKs:/cc/cr:') -# /cc/crr -assert(type(tree.root.children[0].children[1].children[0].children[0].children[0].children[1].children[2]) == ActionNode) -assert(tree.root.children[0].children[1].children[0].children[0].children[0].children[1].children[2].bet_history == '/cc/crr') -assert(len(tree.root.children[0].children[1].children[0].children[0].children[0].children[1].children[2].children) == 2) -assert(tree.root.children[0].children[1].children[0].children[0].children[0].children[1].children[2].player == 1) -assert(tree.root.children[0].children[1].children[0].children[0].children[0].children[1].children[2].fold_action != None) -assert(tree.root.children[0].children[1].children[0].children[0].children[0].children[1].children[2].call_action != None) -assert(tree.root.children[0].children[1].children[0].children[0].children[0].children[1].children[2].raise_action is None) -assert(tree.root.children[0].children[1].children[0].children[0].children[0].children[1].children[2].player_view == 'KhKs:/cc/crr:') -# /cc/crrf -assert(type(tree.root.children[0].children[1].children[0].children[0].children[0].children[1].children[2].children[0]) == TerminalNode) -assert(tree.root.children[0].children[1].children[0].children[0].children[0].children[1].children[2].children[0].bet_history == '/cc/crrf') -assert(tree.root.children[0].children[1].children[0].children[0].children[0].children[1].children[2].children[0].payoffs == [5,-5]) -# /cc/crrc -assert(type(tree.root.children[0].children[1].children[0].children[0].children[0].children[1].children[2].children[1]) == TerminalNode) -assert(tree.root.children[0].children[1].children[0].children[0].children[0].children[1].children[2].children[1].bet_history == '/cc/crrc') -assert(tree.root.children[0].children[1].children[0].children[0].children[0].children[1].children[2].children[1].payoffs == [-7,7]) -print 'All passed!' - -print 'Testing PublicTree' -tree = PublicTree(rules) -tree.build() - -assert(type(tree.root) == HolecardChanceNode) -assert(len(tree.root.children) == 1) -# / -assert(type(tree.root.children[0]) == ActionNode) -assert(tree.root.children[0].player == 0) -assert(tree.root.children[0].player_view == ('As:/:', 'Kh:/:', 'Ks:/:', 'Qs:/:')) -assert(len(tree.root.children[0].children) == 2) -assert(tree.root.children[0].fold_action != None) -assert(tree.root.children[0].call_action != None) -assert(tree.root.children[0].raise_action == None) -# /f -assert(type(tree.root.children[0].children[0]) == TerminalNode) -assert(tree.root.children[0].children[0].payoffs == { ((Card(14,1),),(Card(13,1),)): [-2,2], ((Card(14,1),),(Card(13,2),)): [-2,2], ((Card(14,1),),(Card(12,1),)): [-2,2], ((Card(13,1),),(Card(14,1),)): [-2,2], ((Card(13,1),),(Card(13,2),)): [-2,2], ((Card(13,1),),(Card(12,1),)): [-2,2], ((Card(13,2),),(Card(14,1),)): [-2,2], ((Card(13,2),),(Card(13,1),)): [-2,2], ((Card(13,2),),(Card(12,1),)): [-2,2], ((Card(12,1),),(Card(14,1),)): [-2,2], ((Card(12,1),),(Card(13,2),)): [-2,2], ((Card(12,1),),(Card(13,1),)): [-2,2] }) -# /c -assert(type(tree.root.children[0].children[1]) == ActionNode) -assert(tree.root.children[0].children[1].player == 1) -assert(tree.root.children[0].children[1].player_view == ('As:/c:', 'Kh:/c:', 'Ks:/c:', 'Qs:/c:')) -assert(len(tree.root.children[0].children[1].children) == 1) -# /cc/ [boardcard] -assert(type(tree.root.children[0].children[1].children[0]) == BoardcardChanceNode) -assert(tree.root.children[0].children[1].children[0].bet_history == '/cc/') -assert(len(tree.root.children[0].children[1].children[0].children) == 4) -# xAs:/cc/ [action] -assert(type(tree.root.children[0].children[1].children[0].children[0]) == ActionNode) -assert(tree.root.children[0].children[1].children[0].children[0].bet_history == '/cc/') -assert(len(tree.root.children[0].children[1].children[0].children[0].children) == 2) -assert(tree.root.children[0].children[1].children[0].children[0].player == 0) -assert(tree.root.children[0].children[1].children[0].children[0].fold_action is None) -assert(tree.root.children[0].children[1].children[0].children[0].call_action != None) -assert(tree.root.children[0].children[1].children[0].children[0].raise_action != None) -assert(tree.root.children[0].children[1].children[0].children[0].player_view == ('KhAs:/cc/:','KsAs:/cc/:','QsAs:/cc/:')) -# xAs:/cc/r -assert(type(tree.root.children[0].children[1].children[0].children[0].children[1]) == ActionNode) -assert(tree.root.children[0].children[1].children[0].children[0].children[1].bet_history == '/cc/r') -assert(len(tree.root.children[0].children[1].children[0].children[0].children[1].children) == 3) -assert(tree.root.children[0].children[1].children[0].children[0].children[1].player == 1) -assert(tree.root.children[0].children[1].children[0].children[0].children[1].fold_action != None) -assert(tree.root.children[0].children[1].children[0].children[0].children[1].call_action != None) -assert(tree.root.children[0].children[1].children[0].children[0].children[1].raise_action != None) -assert(tree.root.children[0].children[1].children[0].children[0].children[1].player_view == ('KhAs:/cc/r:','KsAs:/cc/r:','QsAs:/cc/r:')) -# xAs:/cc/rc -assert(type(tree.root.children[0].children[1].children[0].children[0].children[1].children[1]) == TerminalNode) -assert(tree.root.children[0].children[1].children[0].children[0].children[1].children[1].bet_history == '/cc/rc') -assert(tree.root.children[0].children[1].children[0].children[0].children[1].children[1].payoffs == { ((Card(13,1),),(Card(13,2),)): [0,0], ((Card(13,1),),(Card(12,1),)): [5,-5], ((Card(13,2),),(Card(13,1),)): [0,0], ((Card(13,2),),(Card(12,1),)): [5,-5], ((Card(12,1),),(Card(13,2),)): [-5,5], ((Card(12,1),),(Card(13,1),)): [-5,5] }) -# xKh:/cc/rc -assert(type(tree.root.children[0].children[1].children[0].children[1].children[1].children[1]) == TerminalNode) -assert(tree.root.children[0].children[1].children[0].children[1].children[1].children[1].bet_history == '/cc/rc') -assert(tree.root.children[0].children[1].children[0].children[1].children[1].children[1].payoffs == { ((Card(13,1),),(Card(14,1),)): [5,-5], ((Card(13,1),),(Card(12,1),)): [5,-5], ((Card(14,1),),(Card(13,1),)): [-5,5], ((Card(14,1),),(Card(12,1),)): [5,-5], ((Card(12,1),),(Card(14,1),)): [-5,5], ((Card(12,1),),(Card(13,1),)): [-5,5] }) -print 'All passed!' - - - - - diff --git a/tests/test_oscfr.py b/tests/test_oscfr.py deleted file mode 100644 index 5391b2d..0000000 --- a/tests/test_oscfr.py +++ /dev/null @@ -1,84 +0,0 @@ -import sys -import os -sys.path.insert(0,os.path.realpath('.')) -from pokerstrategy import * -from pokergames import * -from pokercfr import * - -def near(val, expected, distance=0.0001): - return val >= (expected - distance) and val <= (expected + distance) - -print '' -print '' -print 'Testing Outcome Sampling (OS) CFR' -print '' -print '' - -""" -print 'Computing NE for Kuhn poker' -kuhn = kuhn_rules() - -cfr = OutcomeSamplingCFR(kuhn) - -iterations_per_block = 1000 -blocks = 100 -for block in range(blocks): - print 'Iterations: {0}'.format(block * iterations_per_block) - cfr.run(iterations_per_block) - result = cfr.profile.best_response() - print 'Best response EV: {0}'.format(result[1]) - print 'Total exploitability: {0}'.format(sum(result[1])) -print 'Done!' -print '' -""" - -""" -print 'Computing NE for Half-Street Kuhn poker' - -hskuhn = half_street_kuhn_rules() -cfr = OutcomeSamplingCFR(hskuhn) -iterations_per_block = 10000 -blocks = 100 -for block in range(blocks): - print 'Iterations: {0}'.format(block * iterations_per_block) - cfr.run(iterations_per_block) - result = cfr.profile.best_response() - print 'Best response EV: {0} '.format(result[1]) - print 'Total exploitability: {0}'.format(sum(result[1])) -print cfr.profile.strategies[0].policy -print cfr.profile.strategies[1].policy -print cfr.counterfactual_regret -print 'Verifying P1 policy' -assert(near(cfr.profile.strategies[0].policy['Q:/:'][CALL], 2.0 / 3.0, 0.01)) -assert(near(cfr.profile.strategies[0].policy['Q:/:'][RAISE], 1.0 / 3.0, 0.01)) -assert(near(cfr.profile.strategies[0].policy['K:/:'][CALL], 1, 0.01)) -assert(near(cfr.profile.strategies[0].policy['K:/:'][RAISE], 0, 0.01)) -assert(near(cfr.profile.strategies[0].policy['A:/:'][CALL], 0, 0.01)) -assert(near(cfr.profile.strategies[0].policy['A:/:'][RAISE], 1.0, 0.01)) -print 'Verifying P2 policy' -assert(near(cfr.profile.strategies[1].policy['Q:/r:'][FOLD], 1.0, 0.01)) -assert(near(cfr.profile.strategies[1].policy['Q:/r:'][CALL], 0, 0.01)) -assert(near(cfr.profile.strategies[1].policy['K:/r:'][FOLD], 2.0 / 3.0, 0.01)) -assert(near(cfr.profile.strategies[1].policy['K:/r:'][CALL], 1.0 / 3.0, 0.01)) -assert(near(cfr.profile.strategies[1].policy['A:/r:'][FOLD], 0, 0.01)) -assert(near(cfr.profile.strategies[1].policy['A:/r:'][CALL], 1.0, 0.01)) - -print 'Done!' -print '' -""" - -print 'Computing NE for Leduc poker' -leduc = leduc_rules() - -cfr = OutcomeSamplingCFR(leduc) - -iterations_per_block = 10000 -blocks = 1000 -for block in range(blocks): - print 'Iterations: {0}'.format(block * iterations_per_block) - cfr.run(iterations_per_block) - result = cfr.profile.best_response() - print 'Best response EV: {0}'.format(result[1]) - print 'Total exploitability: {0}'.format(sum(result[1])) -print 'Done!' -print '' \ No newline at end of file diff --git a/tests/test_pcscfr.py b/tests/test_pcscfr.py deleted file mode 100644 index 2a6774b..0000000 --- a/tests/test_pcscfr.py +++ /dev/null @@ -1,64 +0,0 @@ -import sys -import os -sys.path.insert(0,os.path.realpath('.')) -from pokerstrategy import * -from pokergames import * -from pokercfr import * - -def near(val, expected, distance=0.0001): - return val >= (expected - distance) and val <= (expected + distance) - -print '' -print '' -print 'Testing Public Chance Sampling (PCS) CFR' -print '' -print '' -""" -print 'Computing NE for Half-Street Kuhn poker' - -hskuhn = half_street_kuhn_rules() -cfr = PublicChanceSamplingCFR(hskuhn) -iterations_per_block = 1000 -blocks = 10 -for block in range(blocks): - print 'Iterations: {0}'.format(block * iterations_per_block) - cfr.run(iterations_per_block) - result = cfr.profile.best_response() - print 'Best response EV: {0}'.format(result[1]) - print 'Total exploitability: {0}'.format(sum(result[1])) -print cfr.profile.strategies[0].policy -print cfr.profile.strategies[1].policy -print cfr.counterfactual_regret -print 'Verifying P1 policy' -assert(near(cfr.profile.strategies[0].policy['Q:/:'][CALL], 2.0 / 3.0, 0.01)) -assert(near(cfr.profile.strategies[0].policy['Q:/:'][RAISE], 1.0 / 3.0, 0.01)) -assert(near(cfr.profile.strategies[0].policy['K:/:'][CALL], 1, 0.01)) -assert(near(cfr.profile.strategies[0].policy['K:/:'][RAISE], 0, 0.01)) -assert(near(cfr.profile.strategies[0].policy['A:/:'][CALL], 0, 0.01)) -assert(near(cfr.profile.strategies[0].policy['A:/:'][RAISE], 1.0, 0.01)) -print 'Verifying P2 policy' -assert(near(cfr.profile.strategies[1].policy['Q:/r:'][FOLD], 1.0, 0.01)) -assert(near(cfr.profile.strategies[1].policy['Q:/r:'][CALL], 0, 0.01)) -assert(near(cfr.profile.strategies[1].policy['K:/r:'][FOLD], 2.0 / 3.0, 0.01)) -assert(near(cfr.profile.strategies[1].policy['K:/r:'][CALL], 1.0 / 3.0, 0.01)) -assert(near(cfr.profile.strategies[1].policy['A:/r:'][FOLD], 0, 0.01)) -assert(near(cfr.profile.strategies[1].policy['A:/r:'][CALL], 1.0, 0.01)) - -print 'Done!' -print '' -""" -print 'Computing NE for Leduc poker' -leduc = leduc_rules() - -cfr = PublicChanceSamplingCFR(leduc) - -iterations_per_block = 1000 -blocks = 1000 -for block in range(blocks): - print 'Iterations: {0}'.format(block * iterations_per_block) - cfr.run(iterations_per_block) - result = cfr.profile.best_response() - print 'Best response EV: {0}'.format(result[1]) - print 'Total exploitability: {0}'.format(sum(result[1])) -print 'Done!' -print '' \ No newline at end of file diff --git a/tests/test_strategy.py b/tests/test_strategy.py deleted file mode 100644 index 625c993..0000000 --- a/tests/test_strategy.py +++ /dev/null @@ -1,234 +0,0 @@ -import sys -import os -sys.path.insert(0,os.path.realpath('.')) -from pokerstrategy import * -from pokergames import * - -def near(val, expected): - return val >= (expected - 0.0001) and val <= (expected + 0.0001) - -def validate_strategy(s, filename): - validation_strategy = Strategy(s.player) - validation_strategy.load_from_file(filename) - assert(s.policy == validation_strategy.policy) - -print '' -print '' -print 'Testing Strategy' -print '' -print '' - - -print 'No-action game with 1 holecard followed by 1 boardcard' -players = 2 -deck = [Card(14,1),Card(13,1),Card(12,1),Card(11,1)] -ante = 1 -blinds = None -rounds = [RoundInfo(holecards=1,boardcards=1,betsize=1,maxbets=[0,0])] -no_action_rules = GameRules(players, deck, rounds, ante, blinds, handeval=leduc_eval, infoset_format=leduc_format) -no_action_gametree = GameTree(no_action_rules) -no_action_gametree.build() -s0 = Strategy(0) -s0.build_default(no_action_gametree) -s1 = Strategy(1) -s1.build_default(no_action_gametree) -profile = StrategyProfile(no_action_rules, [s0,s1]) -expected_value = profile.expected_value() -#print 'Expected values: [{0:.9f},{1:.9f}]'.format(expected_value[0], expected_value[1]) -print 'Expected values: [{0},{1}]'.format(expected_value[0], expected_value[1]) -assert(expected_value >= (-0.0001,-0.0001) and expected_value <= (0.0001,0.0001)) -best_responses = profile.best_response() -br = best_responses[0] -brev = best_responses[1] -#print 'Best responses: {0}'.format([r.policy for r in br.strategies]) -print 'Best response EV: {0}'.format(brev) -assert(brev >= (-0.0001,-0.0001) and brev <= (0.0001,0.0001)) -print 'All passed!' -print '' - - -hskuhn_rules = half_street_kuhn_rules() -hskuhn_gametree = half_street_kuhn_gametree() - -s0 = Strategy(0) -s0.policy = { 'A:/:': [0,0,1], 'K:/:': [0,1,0], 'Q:/:': [0,2.0/3.0,1.0/3.0] } - -s1 = Strategy(1) -s1.policy = { 'A:/r:': [0,1,0], 'K:/r:': [2.0/3.0,1.0/3.0,0], 'Q:/r:': [1,0,0], 'A:/c:': [0,1,0], 'K:/c:': [0,1,0], 'Q:/c:': [0,1,0] } - -eq0 = Strategy(0) -eq0.build_default(hskuhn_gametree) -eq0.save_to_file('tests/hskuhn_eq0.strat') - -eq1 = Strategy(1) -eq1.build_default(hskuhn_gametree) -eq1.save_to_file('tests/hskuhn_eq1.strat') - -print "Half-Street Kuhn Expected Value" -print "Nash0 vs. Nash1: {0}".format(StrategyProfile(hskuhn_rules, [s0,s1]).expected_value()) -print "Nash0 vs. Eq1: {0}".format(StrategyProfile(hskuhn_rules, [s0,eq1]).expected_value()) -print "Eq0 vs. Nash1: {0}".format(StrategyProfile(hskuhn_rules, [eq0,s1]).expected_value()) -print "Eq0 vs. Eq1: {0}".format(StrategyProfile(hskuhn_rules, [eq0,eq1]).expected_value()) -print "" - -print "Half-Street Kuhn Best Response" -overbet0 = Strategy(0) -overbet0.policy = { 'Q:/:': [0,0.5,0.5], 'K:/:': [0,1,0], 'A:/:': [0,0,1] } -overbet1 = Strategy(1) -overbet1.policy = { 'Q:/c:': [0,1,0], 'K:/c:': [0,1,0], 'A:/c:': [0,1,0], 'Q:/r:': [1,0,0], 'K:/r:': [0.5,0.5,0], 'A:/r:': [0,1,0] } -profile = StrategyProfile(hskuhn_rules, [overbet0,overbet1]) -result = profile.best_response() -br = result[0].strategies[0] -ev = result[1][0] -print "Overbet0 BR EV: {0}".format(ev) -print br.policy -assert(len(br.policy) == 3) -assert(br.probs('Q:/:') == [0,1,0]) -assert(br.probs('K:/:') == [0,1,0]) -assert(br.probs('A:/:') == [0,0,1]) -assert(near(ev, 1.0/12.0)) - -br = result[0].strategies[1] -ev = result[1][1] -print "Overbet1 BR: {0}".format(ev) -assert(len(br.policy) == 6) -assert(br.probs('Q:/c:') == [0,1,0]) -assert(br.probs('K:/c:') == [0,1,0]) -assert(br.probs('A:/c:') == [0,1,0]) -assert(br.probs('Q:/r:') == [1,0,0]) -assert(br.probs('K:/r:') == [0,1,0]) -assert(br.probs('A:/r:') == [0,1,0]) -assert(near(ev, 0)) - -print 'All passed!' -print "" - -#sys.exit(0) - -leduc_rules = leduc_rules() -leduc_gt = PublicTree(leduc_rules) -leduc_gt.build() - -s0 = Strategy(0) -s0.load_from_file('strategies/leduc/0.strat') -# Test a couple of arbitrary values -assert(s0.probs('J:/:') == [0.000000000, 0.927357111, 0.072642889]) -assert(s0.probs('KJ:/rrc/rr:') == [0.546821151, 0.453178849, 0.000000000]) -# Verify we loaded all of infosets -assert(len(s0.policy) == 144) - -s1 = Strategy(1) -s1.load_from_file('strategies/leduc/1.strat') -# Test a couple of arbitrary values -assert(s1.probs('J:/r:') == [0.819456679, 0.125672407, 0.054870914]) -assert(s1.probs('KJ:/rrc/crr:') == [0.000031258, 0.999968742, 0.000000000]) -# Verify we loaded all of infosets -assert(len(s1.policy) == 144) - -# Generate a default strategy for player 0 -eq0 = Strategy(0) -eq0.build_default(leduc_gt) -eq0.save_to_file('tests/leduc_eq0.strat') -assert(len(eq0.policy) == 144) - -# Generate a default strategy for player 1 -eq1 = Strategy(1) -eq1.build_default(leduc_gt) -eq1.save_to_file('tests/leduc_eq1.strat') -assert(len(eq1.policy) == 144) - -rand0 = Strategy(0) -rand0.load_from_file('strategies/leduc/random.strat') -rand1 = Strategy(1) -rand1.load_from_file('strategies/leduc/random.strat') - -""" -All leduc test values were derived using the open_cfr implementation from UoAlberta -""" -print "Leduc Expected Value" -profile = StrategyProfile(leduc_rules, [s0,s1]) -profile.gametree = leduc_gt -result = profile.expected_value() -print "Nash0 vs. Nash1 EV: {0}".format(result) -assert(result[0] >= -0.085653 and result[0] <= -0.085651) - -profile = StrategyProfile(leduc_rules, [s0,eq1]) -profile.gametree = leduc_gt -result = profile.expected_value() -print "Nash0 vs. Eq1 EV: {0}".format(result) -assert(result[0] >= 0.59143 and result[0] <= 0.59145) - -profile = StrategyProfile(leduc_rules, [eq0,eq1]) -profile.gametree = leduc_gt -result = profile.expected_value() -print "Eq0 vs. Eq1: {0}".format(result) -assert(result[0] >= -0.078126 and result[0] <= -0.078124) - -profile = StrategyProfile(leduc_rules, [eq0,s1]) -profile.gametree = leduc_gt -result = profile.expected_value() -print "Eq0 vs. Nash1 EV: {0}".format(result) -assert(result[0] >= -0.840442 and result[0] <= -0.840440) - -profile = StrategyProfile(leduc_rules, [s0,rand1]) -profile.gametree = leduc_gt -result = profile.expected_value() -print "Nash0 vs. Random: {0}".format(result) -assert(result[0] >= 0.591873 and result[0] <= 0.591875) - -profile = StrategyProfile(leduc_rules, [rand0,s1]) -profile.gametree = leduc_gt -result = profile.expected_value() -print "Random vs. Nash1: {0}".format(result) -assert(result[0] >= -0.84791 and result[0] <= -0.84790) - -print "" - -print "Leduc Best Response" -profile = StrategyProfile(leduc_rules, [s0,s1]) -result = profile.best_response() -br = result[0].strategies[0] -ev = result[1][0] -print "Nash0 BR EV: {0}".format(ev) -br.save_to_file('tests/leduc_nash0_br.strat') -assert(near(ev, -0.0854363)) -validate_strategy(br, 'strategies/leduc/nash_br0.strat') - -br = result[0].strategies[1] -ev = result[1][1] -print "Nash1 BR EV: {0}".format(ev) -br.save_to_file('tests/leduc_nash1_br.strat') -assert(near(ev, 0.0858749)) -validate_strategy(br, 'strategies/leduc/nash_br1.strat') - -profile = StrategyProfile(leduc_rules, [eq0,eq1]) -result = profile.best_response() -br = result[0].strategies[0] -ev = result[1][0] -print "Eq0 BR EV: {0}".format(ev) -assert(near(ev, 2.0875)) -validate_strategy(br, 'strategies/leduc/equal_br0.strat') - -br = result[0].strategies[1] -ev = result[1][1] -print "Eq1 BR EV: {0}".format(ev) -assert(near(ev, 2.65972)) -validate_strategy(br, 'strategies/leduc/equal_br1.strat') - -profile = StrategyProfile(leduc_rules, [rand0, rand1]) -result = profile.best_response() -br = result[0].strategies[0] -ev = result[1][0] -print "Rand0 BR: {0}".format(ev) -assert(near(ev, 2.14414)) -validate_strategy(br, 'strategies/leduc/random_br0.strat') - -br = result[0].strategies[1] -ev = result[1][1] -print "Rand1 BR: {0}".format(ev) -assert(near(ev, 3.21721)) -validate_strategy(br, 'strategies/leduc/random_br1.strat') - -print "" - -print 'All passed!'