Last active
August 14, 2023 19:58
-
-
Save bennuttall/6e6e0b8b6aed4d8e4b4ddc3a41ab98c9 to your computer and use it in GitHub Desktop.
Poker hands solution
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
High Card: 1,302,540 (50.1177%) | |
Pair: 1,098,240 (42.2569%) | |
Two Pair: 123,552 (4.7539%) | |
Three of a Kind: 54,912 (2.1128%) | |
Straight: 10,200 (0.3925%) | |
Flush: 5,108 (0.1965%) | |
Full House: 3,744 (0.1441%) | |
Four of a Kind: 624 (0.0240%) | |
Straight Flush: 36 (0.0014%) | |
Royal Flush: 4 (0.0002%) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from itertools import product | |
import random | |
from itertools import combinations | |
from collections import defaultdict, Counter | |
from functools import cached_property, total_ordering | |
VALUES = "23456789TJQKA" | |
SUITS = "HDSC" | |
HAND_RANKS = ( | |
"High Card", | |
"Pair", | |
"Two Pair", | |
"Three of a Kind", | |
"Straight", | |
"Flush", | |
"Full House", | |
"Four of a Kind", | |
"Straight Flush", | |
"Royal Flush", | |
) | |
class Card: | |
""" | |
Represents a single playing card. Initialise with a two-character string in | |
the form <value><suit> e.g. AS for the ace of spades: | |
>>> card = Card("AS") | |
""" | |
def __init__(self, value_suit): | |
value, suit = value_suit | |
if value not in VALUES: | |
raise ValueError | |
self.value = value | |
if suit not in SUITS: | |
raise ValueError | |
self.suit = suit | |
def __repr__(self): | |
return f"<Card object {self}>" | |
def __str__(self): | |
return f"{self.value}{self.suit}" | |
def __eq__(self, other): | |
return self.value == other.value | |
def __gt__(self, other): | |
return self.value_index > other.value_index | |
@property | |
def value_index(self): | |
return VALUES.index(self.value) | |
@total_ordering | |
class PokerHand: | |
""" | |
Represents a hand of five cards. Initialise with five 2-character strings | |
each separated by a space, e.g: | |
>>> hand = PokerHand.from_str("3D JC 8S 4H 2C") | |
Evaluate the hand rank by accessing the rank property: | |
>>> hand.rank | |
'High Card' | |
""" | |
def __init__(self, *cards: list[Card]): | |
card_set = {repr(card) for card in cards} | |
duplicate_cards = len(card_set) < len(cards) | |
if duplicate_cards: | |
raise ValueError | |
self.cards = sorted(cards) | |
@classmethod | |
def from_str(cls, s: str): | |
return cls(*[Card(sv) for sv in s.split(' ')]) | |
def __repr__(self): | |
return f"<PokerHand object ({self})>" | |
def __str__(self): | |
card_strings = (str(card) for card in self) | |
return " ".join(card_strings) | |
def __iter__(self): | |
return iter(self.cards) | |
def __eq__(self, other: "PokerHand"): | |
if self.rank_index == other.rank_index: | |
return self.hand_comparison == other.hand_comparison | |
return False | |
def __gt__(self, other: "PokerHand"): | |
if self.rank_index == other.rank_index: | |
return self.hand_comparison > other.hand_comparison | |
return self.rank_index > other.rank_index | |
@cached_property | |
def rank(self): | |
suits_set = {card.suit for card in self} | |
values = [card.value for card in self] | |
values_set = {card.value for card in self} | |
value_counts = {values.count(v) for v in values} | |
values_str = ''.join(values) | |
flush = len(suits_set) == 1 | |
straight = values_str in VALUES or values_str == '2345A' | |
royal = values_str == 'TJQKA' | |
if flush: | |
if royal: | |
return "Royal Flush" | |
if straight: | |
return "Straight Flush" | |
return "Flush" | |
if straight: | |
return "Straight" | |
if len(values_set) == 2: | |
if 4 in value_counts: | |
return "Four of a Kind" | |
return "Full House" | |
if len(values_set) == 3: | |
if 3 in value_counts: | |
return "Three of a Kind" | |
return "Two Pair" | |
if len(values_set) == 4: | |
return "Pair" | |
return "High Card" | |
@cached_property | |
def rank_index(self): | |
return HAND_RANKS.index(self.rank) | |
@cached_property | |
def hand_comparison(self): | |
sort_by_value = lambda v: -VALUES.index(v) | |
values = Counter([card.value for card in self.cards]) | |
if self.rank == "High Card": | |
return sorted(self.cards) | |
if self.rank == "Pair": | |
pair_card = values.most_common()[0][0] | |
other_cards = sorted([v[0] for v in values.most_common()[1:]], key=sort_by_value) | |
return [VALUES.index(pair_card)] + [VALUES.index(v) for v in other_cards] | |
if self.rank == "Two Pair": | |
pairs = sorted([v[0] for v in values.most_common()[:2]], key=sort_by_value) | |
other_card = values.most_common()[-1][0] | |
return [VALUES.index(p) for p in pairs] + [other_card] | |
if self.rank == "Three of a Kind": | |
three = values.most_common()[0][0] | |
return VALUES.index(three) | |
if self.rank == "Straight": | |
return sorted(self.cards) | |
if self.rank == "Flush": | |
return sorted(self.cards) | |
if self.rank == "Full House": | |
three = values.most_common()[0][0] | |
return VALUES.index(three) | |
if self.rank == "Four of a Kind": | |
four = values.most_common()[0][0] | |
return VALUES.index(four) | |
if self.rank == "Straight Flush": | |
return sorted(self.cards) | |
if self.rank == "Royal Flush": | |
return True | |
def from_two_cards(card_1: Card, card_2: Card): | |
deck = [ | |
Card(f"{v}{s}") | |
for v, s in product(VALUES, SUITS) | |
if f"{v}{s}" not in {str(card_1), str(card_2)} | |
] | |
ranks = defaultdict(int) | |
for card_3, card_4, card_5 in combinations(deck, 3): | |
hand = PokerHand(card_1, card_2, card_3, card_4, card_5) | |
ranks[hand.rank] += 1 | |
total = sum(ranks.values()) | |
for rank in HAND_RANKS: | |
print(f"{rank}: {100 * ranks[rank] / total:.0f}%") | |
def best_hand(*cards: list[Card]): | |
hands = sorted(PokerHand(*five_cards) for five_cards in combinations(cards, 5)) | |
def from_seven_cards(card_1: Card, card_2: Card, card_3: Card, card_4: Card, card_5: Card, card_6: Card, card_7: Card): | |
deck = [ | |
Card(f"{v}{s}") | |
for v, s in product(VALUES, SUITS) | |
if f"{v}{s}" not in {str(card_1), str(card_2)} | |
] | |
ranks = defaultdict(int) | |
for card_6, card_7 in combinations(deck, 3): | |
hand = PokerHand(card_1, card_2, card_3, card_4, card_5) | |
ranks[hand.rank] += 1 | |
total = sum(ranks.values()) | |
for rank in HAND_RANKS: | |
print(f"{rank}: {100 * ranks[rank] / total:.0f}%") | |
deck = [Card(f"{v}{s}") for v, s in product(VALUES, SUITS)] | |
print("Made deck", len(deck)) | |
all_hands = sorted(PokerHand(*five_cards) for five_cards in combinations(deck, 5)) | |
print("Made all hands", len(all_hands)) | |
hands = sorted(PokerHand(*five_cards) for five_cards in combinations(deck, 5)) | |
print("Sorted hands") | |
all_ranks = {} | |
score = 0 | |
last_hand = None | |
for hand in hands: | |
if last_hand is None or hand > last_hand: | |
score += 1 | |
print(score, hand.rank, hand.hand_comparison) | |
last_hand = hand | |
all_ranks[str(hand)] = score | |
print("Scored hands", len(all_ranks)) | |
# random.shuffle(deck) | |
# card_1 = deck.pop() | |
# card_2 = deck.pop() | |
# print(card_1, card_2) | |
# from_two_cards(card_1, card_2) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from poker import PokerHand | |
def test_compare_pair(): | |
hand_1 = PokerHand.from_str("AS AH 4D 6S 8H") # Pair of Aces | |
hand_2 = PokerHand.from_str("KH KD 4S 6H 8S") # Pair of Kings | |
hand_3 = PokerHand.from_str("JC JD 6S 8H TH") # Pair of Jacks | |
hand_4 = PokerHand.from_str("7D 7H 2S 3C 4D") # Pair of Sevens / 2 3 4 | |
hand_5 = PokerHand.from_str("7S 7C 3H 4H 5S") # Pair of Sevens / 3 4 5 | |
for hand in [hand_1, hand_2, hand_3, hand_4, hand_5]: | |
assert hand.rank == "Pair" | |
assert hand_1 > hand_2 | |
assert hand_2 > hand_3 | |
assert hand_4 < hand_3 | |
assert hand_4 < hand_5 | |
def test_compare_two_pair(): | |
hand_1 = PokerHand.from_str("AS AH 4D 4S 8H") # Two Pair: Aces and Fours | |
hand_2 = PokerHand.from_str("KH KD 4S 4H 8S") # Two Pair: Kings and Fours | |
hand_3 = PokerHand.from_str("JC JD 6S 6H TH") # Two Pair: Jacks and Sixes | |
hand_4 = PokerHand.from_str("7D 7H 3S 3C 5D") # Two Pair: Sevens and Threes | |
hand_5 = PokerHand.from_str("6S 6C 9H 9C 2S") # Two Pair: Sixes and Nines | |
hand_6 = PokerHand.from_str("AS AC 2D 2H 9S") # Two Pair: Aces and Twos / 9 | |
hand_7 = PokerHand.from_str("AD AH 2C 2S TS") # Two Pair: Aces and Twos / T | |
for hand in [hand_1, hand_2, hand_3, hand_4, hand_5, hand_6, hand_7]: | |
assert hand.rank == "Two Pair" | |
assert hand_1 > hand_2 | |
assert hand_2 > hand_3 | |
assert hand_3 > hand_4 | |
assert hand_4 < hand_5 | |
assert hand_5 < hand_6 | |
assert hand_6 < hand_7 | |
def test_compare_three_of_a_kind(): | |
hand_1 = PokerHand.from_str("AS AH AD 4S 8H") # Three of a Kind: Aces | |
hand_2 = PokerHand.from_str("KH KD KS 6H 8S") # Three of a Kind: Kings | |
hand_3 = PokerHand.from_str("JC JD JH 8S TH") # Three of a Kind: Jacks | |
hand_4 = PokerHand.from_str("7D 7H 7S 3C 5D") # Three of a Kind: Sevens | |
hand_5 = PokerHand.from_str("5S 5C 5H 9H 2S") # Three of a Kind: Fives | |
hand_6 = PokerHand.from_str("9S 9C 9D 2H 3D") # Three of a Kind: Nines / 2 3 | |
for hand in [hand_1, hand_2, hand_3, hand_4, hand_5, hand_6]: | |
assert hand.rank == "Three of a Kind" | |
assert hand_1 > hand_2 | |
assert hand_2 > hand_3 | |
assert hand_3 > hand_4 | |
assert hand_4 > hand_5 | |
assert hand_5 < hand_6 | |
def test_compare_straight(): | |
hand_1 = PokerHand.from_str("2S 3H 4D 5S 6H") # Straight: 2 to 6 | |
hand_2 = PokerHand.from_str("KH QD JS TH 9S") # Straight: 9 to K | |
hand_3 = PokerHand.from_str("7C 8D 9H TS JH") # Straight: 7 to Jack | |
hand_4 = PokerHand.from_str("6D 7H 8S 9H TC") # Straight: 6 to 10 | |
hand_5 = PokerHand.from_str("3S 4C 5H 6H 7S") # Straight: 3 to 7 | |
hand_6 = PokerHand.from_str("AS KS QD JD TH") # Straight: 10 to Ace | |
for hand in [hand_1, hand_2, hand_3, hand_4, hand_5, hand_6]: | |
assert hand.rank == "Straight" | |
assert hand_1 < hand_2 | |
assert hand_2 > hand_3 | |
assert hand_3 > hand_4 | |
assert hand_4 > hand_5 | |
assert hand_5 < hand_6 | |
def test_compare_flush(): | |
hand_1 = PokerHand.from_str("2S 4S 6S 8S TS") # Flush: 2 to T | |
hand_2 = PokerHand.from_str("KH QH JH 9H 7H") # Flush: 7 to K | |
hand_3 = PokerHand.from_str("3D 5D 7D 9D JD") # Flush: 3 to J | |
hand_4 = PokerHand.from_str("6C 8C TC JC AC") # Flush: 6 to A | |
hand_5 = PokerHand.from_str("3H 5H 7H 9H KH") # Flush: 3 to K | |
hand_6 = PokerHand.from_str("7D 8D 9D JD QD") # Flush: 7 to Q | |
for hand in [hand_1, hand_2, hand_3, hand_4, hand_5, hand_6]: | |
assert hand.rank == "Flush" | |
assert hand_1 < hand_2 | |
assert hand_2 > hand_3 | |
assert hand_3 < hand_4 | |
assert hand_4 > hand_5 | |
assert hand_5 < hand_6 | |
def test_compare_full_house(): | |
hand_1 = PokerHand.from_str("AS AH AD 8S 8H") # Full House: Aces over Eights | |
hand_2 = PokerHand.from_str("KH KD KS 6H 6S") # Full House: Kings over Sixes | |
hand_3 = PokerHand.from_str("JC JD JH 8S 8H") # Full House: Jacks over Eights | |
hand_4 = PokerHand.from_str("7D 7H 7S 3C 3H") # Full House: Sevens over Threes | |
hand_5 = PokerHand.from_str("6S 6C 6H 9C 9S") # Full House: Sixes over Nines | |
hand_6 = PokerHand.from_str("TS TH TC AH AD") # Full House: Tens over Aces | |
for hand in [hand_1, hand_2, hand_3, hand_4, hand_5, hand_6]: | |
assert hand.rank == "Full House" | |
assert hand_1 > hand_2 | |
assert hand_2 > hand_3 | |
assert hand_3 > hand_4 | |
assert hand_4 > hand_5 | |
assert hand_5 < hand_6 | |
def test_compare_four_of_a_kind(): | |
hand_1 = PokerHand.from_str("AS AH AD AC 8H") # Four of a Kind: Aces | |
hand_2 = PokerHand.from_str("KH KD KS KC 8S") # Four of a Kind: Kings | |
hand_3 = PokerHand.from_str("JC JD JH JS 8H") # Four of a Kind: Jacks | |
hand_4 = PokerHand.from_str("7D 7H 7S 7C 5H") # Four of a Kind: Sevens | |
hand_5 = PokerHand.from_str("6S 6C 6H 6D 2S") # Four of a Kind: Sixes | |
hand_6 = PokerHand.from_str("TS TH TC TD 9H") # Four of a Kind: Tens | |
for hand in [hand_1, hand_2, hand_3, hand_4, hand_5, hand_6]: | |
assert hand.rank == "Four of a Kind" | |
assert hand_1 > hand_2 | |
assert hand_2 > hand_3 | |
assert hand_3 > hand_4 | |
assert hand_4 > hand_5 | |
assert hand_5 < hand_6 | |
def test_compare_straight_flush(): | |
hand_1 = PokerHand.from_str("2S 3S 4S 5S 6S") # Straight Flush: 2 to 6, Spades | |
hand_2 = PokerHand.from_str("KH QH JH TH 9H") # Straight Flush: 9 to K, Hearts | |
hand_3 = PokerHand.from_str("AC 2C 3C 4C 5C") # Straight Flush: A to 5, Clubs | |
hand_4 = PokerHand.from_str("6D 7D 8D 9D TD") # Straight Flush: 6 to 10, Diamonds | |
hand_5 = PokerHand.from_str("3S 4S 5S 6S 7S") # Straight Flush: 3 to 7, Spades | |
hand_6 = PokerHand.from_str("KS QS JS TS 9S") # Straight Flush: 10 to A, Spades | |
for hand in [hand_1, hand_2, hand_3, hand_4, hand_5, hand_6]: | |
assert hand.rank == "Straight Flush" | |
assert hand_1 < hand_2 | |
assert hand_2 > hand_3 | |
assert hand_3 < hand_4 | |
assert hand_4 > hand_5 | |
assert hand_5 < hand_6 | |
def test_compare_royal_flush(): | |
hand_1 = PokerHand.from_str("TS JS QS KS AS") # Royal Flush: Spades | |
hand_2 = PokerHand.from_str("AH KH QH JH TH") # Royal Flush: Hearts | |
hand_3 = PokerHand.from_str("TC JC QC KC AC") # Royal Flush: Clubs | |
hand_4 = PokerHand.from_str("TD JD QD KD AD") # Royal Flush: Diamonds | |
for hand in [hand_1, hand_2, hand_3, hand_4]: | |
assert hand.rank == "Royal Flush" | |
assert hand_1 == hand_2 | |
assert hand_2 == hand_3 | |
assert hand_3 == hand_4 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment