forked from TheAlgorithms/Python
-
Notifications
You must be signed in to change notification settings - Fork 0
/
sol1.py
383 lines (328 loc) · 13.5 KB
/
sol1.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
"""
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):
msg = f"Hand should be of type 'str': {hand!r}"
raise TypeError(msg)
# split removes duplicate whitespaces so no need of strip
if len(hand.split(" ")) != 5:
msg = f"Hand should contain only 5 cards: {hand!r}"
raise ValueError(msg)
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()