mirror of
https://github.com/TheAlgorithms/Python.git
synced 2024-11-30 16:31:08 +00:00
cecf43d648
* Pyupgrade to Python 3.9 * updating DIRECTORY.md Co-authored-by: github-actions <${GITHUB_ACTOR}@users.noreply.github.com>
382 lines
14 KiB
Python
382 lines
14 KiB
Python
"""
|
|
Problem: https://projecteuler.net/problem=54
|
|
|
|
In the card game poker, a hand consists of five cards and are ranked,
|
|
from lowest to highest, in the following way:
|
|
|
|
High Card: Highest value card.
|
|
One Pair: Two cards of the same value.
|
|
Two Pairs: Two different pairs.
|
|
Three of a Kind: Three cards of the same value.
|
|
Straight: All cards are consecutive values.
|
|
Flush: All cards of the same suit.
|
|
Full House: Three of a kind and a pair.
|
|
Four of a Kind: Four cards of the same value.
|
|
Straight Flush: All cards are consecutive values of same suit.
|
|
Royal Flush: Ten, Jack, Queen, King, Ace, in same suit.
|
|
|
|
The cards are valued in the order:
|
|
2, 3, 4, 5, 6, 7, 8, 9, 10, Jack, Queen, King, Ace.
|
|
|
|
If two players have the same ranked hands then the rank made up of the highest
|
|
value wins; for example, a pair of eights beats a pair of fives.
|
|
But if two ranks tie, for example, both players have a pair of queens, then highest
|
|
cards in each hand are compared; if the highest cards tie then the next highest
|
|
cards are compared, and so on.
|
|
|
|
The file, poker.txt, contains one-thousand random hands dealt to two players.
|
|
Each line of the file contains ten cards (separated by a single space): the
|
|
first five are Player 1's cards and the last five are Player 2's cards.
|
|
You can assume that all hands are valid (no invalid characters or repeated cards),
|
|
each player's hand is in no specific order, and in each hand there is a clear winner.
|
|
|
|
How many hands does Player 1 win?
|
|
|
|
Resources used:
|
|
https://en.wikipedia.org/wiki/Texas_hold_%27em
|
|
https://en.wikipedia.org/wiki/List_of_poker_hands
|
|
|
|
Similar problem on codewars:
|
|
https://www.codewars.com/kata/ranking-poker-hands
|
|
https://www.codewars.com/kata/sortable-poker-hands
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
|
|
|
|
class PokerHand:
|
|
"""Create an object representing a Poker Hand based on an input of a
|
|
string which represents the best 5 card combination from the player's hand
|
|
and board cards.
|
|
|
|
Attributes: (read-only)
|
|
hand: string representing the hand consisting of five cards
|
|
|
|
Methods:
|
|
compare_with(opponent): takes in player's hand (self) and
|
|
opponent's hand (opponent) and compares both hands according to
|
|
the rules of Texas Hold'em.
|
|
Returns one of 3 strings (Win, Loss, Tie) based on whether
|
|
player's hand is better than opponent's hand.
|
|
|
|
hand_name(): Returns a string made up of two parts: hand name
|
|
and high card.
|
|
|
|
Supported operators:
|
|
Rich comparison operators: <, >, <=, >=, ==, !=
|
|
|
|
Supported builtin methods and functions:
|
|
list.sort(), sorted()
|
|
"""
|
|
|
|
_HAND_NAME = [
|
|
"High card",
|
|
"One pair",
|
|
"Two pairs",
|
|
"Three of a kind",
|
|
"Straight",
|
|
"Flush",
|
|
"Full house",
|
|
"Four of a kind",
|
|
"Straight flush",
|
|
"Royal flush",
|
|
]
|
|
|
|
_CARD_NAME = [
|
|
"", # placeholder as lists are zero indexed
|
|
"One",
|
|
"Two",
|
|
"Three",
|
|
"Four",
|
|
"Five",
|
|
"Six",
|
|
"Seven",
|
|
"Eight",
|
|
"Nine",
|
|
"Ten",
|
|
"Jack",
|
|
"Queen",
|
|
"King",
|
|
"Ace",
|
|
]
|
|
|
|
def __init__(self, hand: str) -> None:
|
|
"""
|
|
Initialize hand.
|
|
Hand should of type str and should contain only five cards each
|
|
separated by a space.
|
|
|
|
The cards should be of the following format:
|
|
[card value][card suit]
|
|
|
|
The first character is the value of the card:
|
|
2, 3, 4, 5, 6, 7, 8, 9, T(en), J(ack), Q(ueen), K(ing), A(ce)
|
|
|
|
The second character represents the suit:
|
|
S(pades), H(earts), D(iamonds), C(lubs)
|
|
|
|
For example: "6S 4C KC AS TH"
|
|
"""
|
|
if not isinstance(hand, str):
|
|
raise TypeError(f"Hand should be of type 'str': {hand!r}")
|
|
# split removes duplicate whitespaces so no need of strip
|
|
if len(hand.split(" ")) != 5:
|
|
raise ValueError(f"Hand should contain only 5 cards: {hand!r}")
|
|
self._hand = hand
|
|
self._first_pair = 0
|
|
self._second_pair = 0
|
|
self._card_values, self._card_suit = self._internal_state()
|
|
self._hand_type = self._get_hand_type()
|
|
self._high_card = self._card_values[0]
|
|
|
|
@property
|
|
def hand(self):
|
|
"""Returns the self hand"""
|
|
return self._hand
|
|
|
|
def compare_with(self, other: PokerHand) -> str:
|
|
"""
|
|
Determines the outcome of comparing self hand with other hand.
|
|
Returns the output as 'Win', 'Loss', 'Tie' according to the rules of
|
|
Texas Hold'em.
|
|
|
|
Here are some examples:
|
|
>>> player = PokerHand("2H 3H 4H 5H 6H") # Stright flush
|
|
>>> opponent = PokerHand("KS AS TS QS JS") # Royal flush
|
|
>>> player.compare_with(opponent)
|
|
'Loss'
|
|
|
|
>>> player = PokerHand("2S AH 2H AS AC") # Full house
|
|
>>> opponent = PokerHand("2H 3H 5H 6H 7H") # Flush
|
|
>>> player.compare_with(opponent)
|
|
'Win'
|
|
|
|
>>> player = PokerHand("2S AH 4H 5S 6C") # High card
|
|
>>> opponent = PokerHand("AD 4C 5H 6H 2C") # High card
|
|
>>> player.compare_with(opponent)
|
|
'Tie'
|
|
"""
|
|
# Breaking the tie works on the following order of precedence:
|
|
# 1. First pair (default 0)
|
|
# 2. Second pair (default 0)
|
|
# 3. Compare all cards in reverse order because they are sorted.
|
|
|
|
# First pair and second pair will only be a non-zero value if the card
|
|
# type is either from the following:
|
|
# 21: Four of a kind
|
|
# 20: Full house
|
|
# 17: Three of a kind
|
|
# 16: Two pairs
|
|
# 15: One pair
|
|
if self._hand_type > other._hand_type:
|
|
return "Win"
|
|
elif self._hand_type < other._hand_type:
|
|
return "Loss"
|
|
elif self._first_pair == other._first_pair:
|
|
if self._second_pair == other._second_pair:
|
|
return self._compare_cards(other)
|
|
else:
|
|
return "Win" if self._second_pair > other._second_pair else "Loss"
|
|
return "Win" if self._first_pair > other._first_pair else "Loss"
|
|
|
|
# This function is not part of the problem, I did it just for fun
|
|
def hand_name(self) -> str:
|
|
"""
|
|
Return the name of the hand in the following format:
|
|
'hand name, high card'
|
|
|
|
Here are some examples:
|
|
>>> PokerHand("KS AS TS QS JS").hand_name()
|
|
'Royal flush'
|
|
|
|
>>> PokerHand("2D 6D 3D 4D 5D").hand_name()
|
|
'Straight flush, Six-high'
|
|
|
|
>>> PokerHand("JC 6H JS JD JH").hand_name()
|
|
'Four of a kind, Jacks'
|
|
|
|
>>> PokerHand("3D 2H 3H 2C 2D").hand_name()
|
|
'Full house, Twos over Threes'
|
|
|
|
>>> PokerHand("2H 4D 3C AS 5S").hand_name() # Low ace
|
|
'Straight, Five-high'
|
|
|
|
Source: https://en.wikipedia.org/wiki/List_of_poker_hands
|
|
"""
|
|
name = PokerHand._HAND_NAME[self._hand_type - 14]
|
|
high = PokerHand._CARD_NAME[self._high_card]
|
|
pair1 = PokerHand._CARD_NAME[self._first_pair]
|
|
pair2 = PokerHand._CARD_NAME[self._second_pair]
|
|
if self._hand_type in [22, 19, 18]:
|
|
return name + f", {high}-high"
|
|
elif self._hand_type in [21, 17, 15]:
|
|
return name + f", {pair1}s"
|
|
elif self._hand_type in [20, 16]:
|
|
join = "over" if self._hand_type == 20 else "and"
|
|
return name + f", {pair1}s {join} {pair2}s"
|
|
elif self._hand_type == 23:
|
|
return name
|
|
else:
|
|
return name + f", {high}"
|
|
|
|
def _compare_cards(self, other: PokerHand) -> str:
|
|
# Enumerate gives us the index as well as the element of a list
|
|
for index, card_value in enumerate(self._card_values):
|
|
if card_value != other._card_values[index]:
|
|
return "Win" if card_value > other._card_values[index] else "Loss"
|
|
return "Tie"
|
|
|
|
def _get_hand_type(self) -> int:
|
|
# Number representing the type of hand internally:
|
|
# 23: Royal flush
|
|
# 22: Straight flush
|
|
# 21: Four of a kind
|
|
# 20: Full house
|
|
# 19: Flush
|
|
# 18: Straight
|
|
# 17: Three of a kind
|
|
# 16: Two pairs
|
|
# 15: One pair
|
|
# 14: High card
|
|
if self._is_flush():
|
|
if self._is_five_high_straight() or self._is_straight():
|
|
return 23 if sum(self._card_values) == 60 else 22
|
|
return 19
|
|
elif self._is_five_high_straight() or self._is_straight():
|
|
return 18
|
|
return 14 + self._is_same_kind()
|
|
|
|
def _is_flush(self) -> bool:
|
|
return len(self._card_suit) == 1
|
|
|
|
def _is_five_high_straight(self) -> bool:
|
|
# If a card is a five high straight (low ace) change the location of
|
|
# ace from the start of the list to the end. Check whether the first
|
|
# element is ace or not. (Don't want to change again)
|
|
# Five high straight (low ace): AH 2H 3S 4C 5D
|
|
# Why use sorted here? One call to this function will mutate the list to
|
|
# [5, 4, 3, 2, 14] and so for subsequent calls (which will be rare) we
|
|
# need to compare the sorted version.
|
|
# Refer test_multiple_calls_five_high_straight in test_poker_hand.py
|
|
if sorted(self._card_values) == [2, 3, 4, 5, 14]:
|
|
if self._card_values[0] == 14:
|
|
# Remember, our list is sorted in reverse order
|
|
ace_card = self._card_values.pop(0)
|
|
self._card_values.append(ace_card)
|
|
return True
|
|
return False
|
|
|
|
def _is_straight(self) -> bool:
|
|
for i in range(4):
|
|
if self._card_values[i] - self._card_values[i + 1] != 1:
|
|
return False
|
|
return True
|
|
|
|
def _is_same_kind(self) -> int:
|
|
# Kind Values for internal use:
|
|
# 7: Four of a kind
|
|
# 6: Full house
|
|
# 3: Three of a kind
|
|
# 2: Two pairs
|
|
# 1: One pair
|
|
# 0: False
|
|
kind = val1 = val2 = 0
|
|
for i in range(4):
|
|
# Compare two cards at a time, if they are same increase 'kind',
|
|
# add the value of the card to val1, if it is repeating again we
|
|
# will add 2 to 'kind' as there are now 3 cards with same value.
|
|
# If we get card of different value than val1, we will do the same
|
|
# thing with val2
|
|
if self._card_values[i] == self._card_values[i + 1]:
|
|
if not val1:
|
|
val1 = self._card_values[i]
|
|
kind += 1
|
|
elif val1 == self._card_values[i]:
|
|
kind += 2
|
|
elif not val2:
|
|
val2 = self._card_values[i]
|
|
kind += 1
|
|
elif val2 == self._card_values[i]:
|
|
kind += 2
|
|
# For consistency in hand type (look at note in _get_hand_type function)
|
|
kind = kind + 2 if kind in [4, 5] else kind
|
|
# first meaning first pair to compare in 'compare_with'
|
|
first = max(val1, val2)
|
|
second = min(val1, val2)
|
|
# If it's full house (three count pair + two count pair), make sure
|
|
# first pair is three count and if not then switch them both.
|
|
if kind == 6 and self._card_values.count(first) != 3:
|
|
first, second = second, first
|
|
self._first_pair = first
|
|
self._second_pair = second
|
|
return kind
|
|
|
|
def _internal_state(self) -> tuple[list[int], set[str]]:
|
|
# Internal representation of hand as a list of card values and
|
|
# a set of card suit
|
|
trans: dict = {"T": "10", "J": "11", "Q": "12", "K": "13", "A": "14"}
|
|
new_hand = self._hand.translate(str.maketrans(trans)).split()
|
|
card_values = [int(card[:-1]) for card in new_hand]
|
|
card_suit = {card[-1] for card in new_hand}
|
|
return sorted(card_values, reverse=True), card_suit
|
|
|
|
def __repr__(self):
|
|
return f'{self.__class__}("{self._hand}")'
|
|
|
|
def __str__(self):
|
|
return self._hand
|
|
|
|
# Rich comparison operators (used in list.sort() and sorted() builtin functions)
|
|
# Note that this is not part of the problem but another extra feature where
|
|
# if you have a list of PokerHand objects, you can sort them just through
|
|
# the builtin functions.
|
|
def __eq__(self, other):
|
|
if isinstance(other, PokerHand):
|
|
return self.compare_with(other) == "Tie"
|
|
return NotImplemented
|
|
|
|
def __lt__(self, other):
|
|
if isinstance(other, PokerHand):
|
|
return self.compare_with(other) == "Loss"
|
|
return NotImplemented
|
|
|
|
def __le__(self, other):
|
|
if isinstance(other, PokerHand):
|
|
return self < other or self == other
|
|
return NotImplemented
|
|
|
|
def __gt__(self, other):
|
|
if isinstance(other, PokerHand):
|
|
return not self < other and self != other
|
|
return NotImplemented
|
|
|
|
def __ge__(self, other):
|
|
if isinstance(other, PokerHand):
|
|
return not self < other
|
|
return NotImplemented
|
|
|
|
def __hash__(self):
|
|
return object.__hash__(self)
|
|
|
|
|
|
def solution() -> int:
|
|
# Solution for problem number 54 from Project Euler
|
|
# Input from poker_hands.txt file
|
|
answer = 0
|
|
script_dir = os.path.abspath(os.path.dirname(__file__))
|
|
poker_hands = os.path.join(script_dir, "poker_hands.txt")
|
|
with open(poker_hands) as file_hand:
|
|
for line in file_hand:
|
|
player_hand = line[:14].strip()
|
|
opponent_hand = line[15:].strip()
|
|
player, opponent = PokerHand(player_hand), PokerHand(opponent_hand)
|
|
output = player.compare_with(opponent)
|
|
if output == "Win":
|
|
answer += 1
|
|
return answer
|
|
|
|
|
|
if __name__ == "__main__":
|
|
solution()
|