--- /dev/null
+"""Calculate the probability of various poker hands
+"""
+
+from pbot.combinations import xunique_combinations
+from pbot.deck import new_deck, SevenChooseFiveHand
+
+
+class Odds (object):
+ """Calculate the odds of winning given the current game state.
+
+ >>> from pprint import pprint
+ >>> from pbot.deck import unpp_card
+
+ The only thing that can beat 4 aces is a straight flush. With
+ spaced out public cards, that is impossible, so we only get wins
+ in that case. The total number of opposing hands is
+ `choose(52-7,2)=990`.
+
+ >>> o = Odds(hole=[unpp_card(c) for c in ['As', 'Ah']],
+ ... public=[unpp_card(c) for c in ['Ad', 'Ac', 'Ts', '7h', '4d']])
+ >>> pprint(o.results)
+ {'losses': 0, 'ties': 0, 'wins': 990}
+
+ However, with properly aligned public cards, the straight flush is
+ within reach, so we get losses whenever the opposing hole contains
+ `Tc`. This leaves `52-8=44` options for the other hole card.
+
+ >>> o.public = [unpp_card(c) for c in ['Ad', 'Ac', 'Kc', 'Qc', 'Jc']]
+ >>> pprint(o.calculate())
+ {'losses': 44, 'ties': 0, 'wins': 946}
+
+ The odds caclulator will cycle through unknown public cards, but
+ this can be slow.
+
+ >>> o.public.pop()
+ >>> pprint(o.calculate())
+ {'losses': 134, 'ties': 0, 'wins': 47476}
+ """
+ deck = new_deck()
+
+ def __init__(self, hole, public=None):
+ self.hole = hole
+ if public == None:
+ public = []
+ self.public = public
+ self.results = self.calculate()
+
+ def calculate(self):
+ known = self.hole + self.public
+ hidden = [c for c in self.deck if c not in known]
+ remaining = 5 - len(self.public)
+ results = {'wins': 0, 'ties': 0, 'losses': 0}
+ for new_public in xunique_combinations(hidden, remaining):
+ p = self.public + new_public
+ h = [c for c in hidden if c not in p]
+ hand = SevenChooseFiveHand(self.hole + p)
+ for hole in xunique_combinations(hidden, 2):
+ other_hand = SevenChooseFiveHand(hole + p)
+ result = cmp(hand, other_hand)
+ if result == 1:
+ results['wins'] += 1
+ elif result == 0:
+ results['ties'] += 1
+ else:
+ results['losses'] += 1
+ return results