diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..efecdb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2013 Will Drevo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md deleted file mode 100644 index ba6f80f..0000000 --- a/README.md +++ /dev/null @@ -1,172 +0,0 @@ -Deuces -======== - -A pure Python poker hand evaluation library - - [ 2 ❤ ] , [ 2 ♠ ] - -## Installation - -``` -$ pip install deuces -``` - -## Implementation notes - -Deuces, originally written for the MIT Pokerbots Competition, is lightweight and fast. All lookups are done with bit arithmetic and dictionary lookups. That said, Deuces won't beat a C implemenation (~250k eval/s) but it is useful for situations where Python is required or where bots are allocated reasonable thinking time (human time scale). - -Deuces handles 5, 6, and 7 card hand lookups. The 6 and 7 card lookups are done by combinatorially evaluating the 5 card choices, but later releases may have dedicated and faster algorithms for these. - -I also have lookup tables for 2 card rollouts, which is particularly handy in evaluating Texas Hold'em preflop pot equity, but they are forthcoming as well. - -See my blog for an explanation of how the library works and how the lookup table generation is done: -http://willdrevo.com/ (haven't posted yet) - -## Usage - -Deuces is easy to set up and use. - -```python ->>> from deuces import Card ->>> card = Card.new('Qh') -``` - -Card objects are represented as integers to keep Deuces performant and lightweight. - -Now let's create the board and an example Texas Hold'em hand: - -```python ->>> board = [ ->>> Card.new('Ah'), ->>> Card.new('Kd'), ->>> Card.new('Jc') ->>> ] ->>> hand = [ ->>> Card.new('Qs'), ->>> Card.new('Th') ->>> ] -``` - -Pretty print card integers to the terminal: - - >>> Card.print_pretty_cards(board + hand) - [ A ❤ ] , [ K ♦ ] , [ J ♣ ] , [ Q ♠ ] , [ T ❤ ] - -If you have [`termacolor`](http://pypi.python.org/pypi/termcolor) installed, they will be colored as well. - -Otherwise move straight to evaluating your hand strength: -```python ->>> from deuces import Evaluator ->>> evaluator = Evaluator() ->>> print evaluator.evaluate(board, hand) -1600 -``` - -Hand strength is valued on a scale of 1 to 7462, where 1 is a Royal Flush and 7462 is unsuited 7-5-4-3-2, as there are only 7642 distinctly ranked hands in poker. Once again, refer to my blog post for a more mathematically complete explanation of why this is so. - -If you want to deal out cards randomly from a deck, you can also do that with Deuces: -```python ->>> from deuces import Deck ->>> deck = Deck() ->>> board = deck.draw(5) ->>> player1_hand = deck.draw(2) ->>> player2_hand = deck.draw(2) -``` -and print them: - - >>> Card.print_pretty_cards(board) - [ 4 ♣ ] , [ A ♠ ] , [ 5 ♦ ] , [ K ♣ ] , [ 2 ♠ ] - >>> Card.print_pretty_cards(player1_hand) - [ 6 ♣ ] , [ 7 ❤ ] - >>> Card.print_pretty_cards(player2_hand) - [ A ♣ ] , [ 3 ❤ ] - -Let's evaluate both hands strength, and then bin them into classes, one for each hand type (High Card, Pair, etc) -```python ->>> p1_score = evaluator.evaluate(board, player1_hand) ->>> p2_score = evaluator.evaluate(board, player2_hand) ->>> p1_class = evaluator.get_rank_class(p1_score) ->>> p2_class = evaluator.get_rank_class(p2_score) -``` -or get a human-friendly string to describe the score, - - >>> print "Player 1 hand rank = %d (%s)\n" % (p1_score, evaluator.class_to_string(p1_class)) - Player 1 hand rank = 6330 (High Card) - - >>> print "Player 2 hand rank = %d (%s)\n" % (p2_score, evaluator.class_to_string(p2_class)) - Player 2 hand rank = 1609 (Straight) - -or, coolest of all, get a blow-by-blow analysis of the stages of the game with relation to hand strength: - - >>> hands = [player1_hand, player2_hand] - >>> evaluator.hand_summary(board, hands) - - ========== FLOP ========== - Player 1 hand = High Card, percentage rank among all hands = 0.893192 - Player 2 hand = Pair, percentage rank among all hands = 0.474672 - Player 2 hand is currently winning. - - ========== TURN ========== - Player 1 hand = High Card, percentage rank among all hands = 0.848298 - Player 2 hand = Pair, percentage rank among all hands = 0.452292 - Player 2 hand is currently winning. - - ========== RIVER ========== - Player 1 hand = High Card, percentage rank among all hands = 0.848298 - Player 2 hand = Straight, percentage rank among all hands = 0.215626 - - ========== HAND OVER ========== - Player 2 is the winner with a Straight - -And that's Deuces, yo. - -## Performance - -Just how fast is Deuces? Check out `performance` folder for a couple of tests comparing Deuces to other pure Python hand evaluators. - -Here are the results evaluating 10,000 random 5, 6, and 7 card boards: - - 5 card evaluation: - [*] Pokerhand-eval: Evaluations per second = 83.577580 - [*] Deuces: Evaluations per second = 235722.458889 - [*] SpecialK: Evaluations per second = 376833.177604 - - 6 card evaluation: - [*] Pokerhand-eval: Evaluations per second = 55.519042 - [*] Deuces: Evaluations per second = 45677.395466 - [*] SpecialK: N/A - - 7 card evaluation: - [*] Pokerhand-eval: Evaluations per second = 51.529784 - [*] Deuces: Evaluations per second = 15220.969303 - [*] SpecialK: Evaluations per second = 142698.833384 - -Compared to [`pokerhand-eval`](https://github.com/aliang/pokerhand-eval), Deuces is 2400x faster on 5 card evaluation, and drops to 300x faster on 7 card evaluation. - -However, [`SpecialKEval`](https://github.com/SpecialK/SpecialKEval/) reigns supreme, with an impressive nearly 400k evals / sec (a factor of ~1.7 improvement over Deuces) for 5 cards, and an impressive 140k /sec on 7 cards (factor of 10). - -For poker hand evaluation in Python, if you desire a cleaner user interface and more readable and adaptable code, I recommend Deuces, because if you *really* need speed, you should be using C anyway. The extra 10x on 7 cards with SpecialK won't get you much more in terms of Monte Carlo simulations, and SpecialK's 5 card evals are within a factor of 2 of Deuces's evals/s. - -For C/C++, I'd recommand [`pokerstove`](https://github.com/andrewprock/pokerstove), as its hyperoptimized C++ Boost routines can do 10+ million evals/s. - -## License - -Copyright (c) 2013 Will Drevo - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..4ea8c3f --- /dev/null +++ b/README.rst @@ -0,0 +1,148 @@ +Treys +===== + +A pure Python poker hand evaluation library + +:: + + [ 3 ❤ ] , [ 3 ♠ ] + +Installation +------------ + +:: + + $ pip install treys + +Implementation notes +-------------------- + +Treys is a Python 3 port of +`Deuces `__ based on the initial work in +`msaindon’s `__ fork. Deuces was written +by `Will Drevo `__ for the MIT Pokerbots Competition. + +Treys is lightweight and fast. All lookups are done with bit arithmetic and +dictionary lookups. That said, Treys won’t beat a C implemenation (~250k +eval/s) but it is useful for situations where Python is required or +where bots are allocated reasonable thinking time (human time scale). + +Treys handles 5, 6, and 7 card hand lookups. The 6 and 7 card lookups +are done by combinatorially evaluating the 5 card choices. + +Usage +----- + +Treys is easy to set up and use. + +.. code:: python + + >>> from treys import Card + >>> card = Card.new('Qh') + +Card objects are represented as integers to keep Treys performant and +lightweight. + +Now let’s create the board and an example Texas Hold’em hand: + +.. code:: python + + >>> board = [ + >>> Card.new('Ah'), + >>> Card.new('Kd'), + >>> Card.new('Jc') + >>> ] + >>> hand = [ + >>> Card.new('Qs'), + >>> Card.new('Th') + >>> ] + +Pretty print card integers to the terminal: + +:: + + >>> Card.print_pretty_cards(board + hand) + [ A ❤ ] , [ K ♦ ] , [ J ♣ ] , [ Q ♠ ] , [ T ❤ ] + +If you have `termcolor `__ +installed, they will be colored as well. + +Otherwise move straight to evaluating your hand strength: + +.. code:: python + + >>> from treys import Evaluator + >>> evaluator = Evaluator() + >>> print(evaluator.evaluate(board, hand)) + 1600 + +Hand strength is valued on a scale of 1 to 7462, where 1 is a Royal +Flush and 7462 is unsuited 7-5-4-3-2, as there are only 7642 distinctly +ranked hands in poker. + +If you want to deal out cards randomly from a deck, you can also do that +with Treys: + +.. code:: python + + >>> from treys import Deck + >>> deck = Deck() + >>> board = deck.draw(5) + >>> player1_hand = deck.draw(2) + >>> player2_hand = deck.draw(2) + +and print them: + +:: + + >>> Card.print_pretty_cards(board) + [ 4 ♣ ] , [ A ♠ ] , [ 5 ♦ ] , [ K ♣ ] , [ 2 ♠ ] + >>> Card.print_pretty_cards(player1_hand) + [ 6 ♣ ] , [ 7 ❤ ] + >>> Card.print_pretty_cards(player2_hand) + [ A ♣ ] , [ 3 ❤ ] + +Let’s evaluate both hands strength, and then bin them into classes, one +for each hand type (High Card, Pair, etc) + +.. code:: python + + >>> p1_score = evaluator.evaluate(board, player1_hand) + >>> p2_score = evaluator.evaluate(board, player2_hand) + >>> p1_class = evaluator.get_rank_class(p1_score) + >>> p2_class = evaluator.get_rank_class(p2_score) + +or get a human-friendly string to describe the score, + +:: + + >>> print("Player 1 hand rank = %d (%s)\n" % (p1_score, evaluator.class_to_string(p1_class))) + Player 1 hand rank = 6330 (High Card) + + >>> print("Player 2 hand rank = %d (%s)\n" % (p2_score, evaluator.class_to_string(p2_class))) + Player 2 hand rank = 1609 (Straight) + +or, coolest of all, get a blow-by-blow analysis of the stages of the +game with relation to hand strength: + +:: + + >>> hands = [player1_hand, player2_hand] + >>> evaluator.hand_summary(board, hands) + + ========== FLOP ========== + Player 1 hand = High Card, percentage rank among all hands = 0.893192 + Player 2 hand = Pair, percentage rank among all hands = 0.474672 + Player 2 hand is currently winning. + + ========== TURN ========== + Player 1 hand = High Card, percentage rank among all hands = 0.848298 + Player 2 hand = Pair, percentage rank among all hands = 0.452292 + Player 2 hand is currently winning. + + ========== RIVER ========== + Player 1 hand = High Card, percentage rank among all hands = 0.848298 + Player 2 hand = Straight, percentage rank among all hands = 0.215626 + + ========== HAND OVER ========== + Player 2 is the winner with a Straight diff --git a/__init__.py b/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/deuces/__init__.py b/deuces/__init__.py deleted file mode 100644 index 59e51fd..0000000 --- a/deuces/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .card import Card -from .deck import Deck -from .evaluator import Evaluator diff --git a/go.py b/go.py index 2b085cd..e47e35e 100644 --- a/go.py +++ b/go.py @@ -1,4 +1,7 @@ -from deuces import Card, Evaluator, Deck +from treys.card import Card +from treys.evaluator import Evaluator +from treys.deck import Deck + # create a card card = Card.new('Qh') @@ -7,7 +10,9 @@ board = [ Card.new('2h'), Card.new('2s'), - Card.new('Jc') + Card.new('Jc'), + Card.new('As'), + Card.new('Kc') ] hand = [ Card.new('Qs'), @@ -15,14 +20,18 @@ ] # pretty print cards to console -Card.print_pretty_cards(board + hand) +Card.print_pretty_cards(board) +Card.print_pretty_cards(hand) # create an evaluator evaluator = Evaluator() # and rank your hand -rank = evaluator.evaluate(board, hand) +rank = evaluator.evaluate(hand, board) +class_ = evaluator.get_rank_class(rank) +print("{} {}".format(rank, evaluator.class_to_string(class_))) print() + # or for random cards or games, create a deck print("Dealing a new hand...") deck = Deck() @@ -39,16 +48,16 @@ print("Player 2's cards:") Card.print_pretty_cards(player2_hand) -p1_score = evaluator.evaluate(board, player1_hand) -p2_score = evaluator.evaluate(board, player2_hand) +p1_score = evaluator.evaluate(player1_hand, board) +p2_score = evaluator.evaluate(player2_hand, board) # bin the scores into classes p1_class = evaluator.get_rank_class(p1_score) p2_class = evaluator.get_rank_class(p2_score) # or get a human-friendly string to describe the score -print(f"Player 1 hand rank = {p1_score} {evaluator.class_to_string(p1_class)}") -print(f"Player 2 hand rank = {p2_score} {evaluator.class_to_string(p2_class)}") +print("Player 1 hand rank = {} {}".format(p1_score, evaluator.class_to_string(p1_class))) +print("Player 2 hand rank = {} {}".format(p2_score, evaluator.class_to_string(p2_class))) # or just a summary of the entire hand hands = [player1_hand, player2_hand] diff --git a/perf.py b/perf.py new file mode 100644 index 0000000..f0f11af --- /dev/null +++ b/perf.py @@ -0,0 +1,62 @@ +import time + +from treys.deck import Deck +from treys.evaluator import Evaluator + + +def setup(n: int, m: int) -> tuple[list[list[int]], list[list[int]]]: + + deck = Deck() + + boards = [] + hands = [] + + for _ in range(n): + boards.append(deck.draw(m)) + hands.append(deck.draw(2)) + deck.shuffle() + + return boards, hands + + +n = 10000 +cumtime = 0.0 +evaluator = Evaluator() +boards, hands = setup(n, 5) +for i in range(len(boards)): + start = time.time() + evaluator.evaluate(hands[i], boards[i]) + cumtime += (time.time() - start) + +avg = float(cumtime / n) +print("7 card evaluation:") +print("[*] Treys: Average time per evaluation: %f" % avg) +print("[*] Treys: Evaluations per second = %f" % (1.0 / avg)) + +### + +cumtime = 0.0 +boards, hands = setup(n, 4) +for i in range(len(boards)): + start = time.time() + evaluator.evaluate(hands[i], boards[i]) + cumtime += (time.time() - start) + +avg = float(cumtime / n) +print("6 card evaluation:") +print("[*] Treys: Average time per evaluation: %f" % avg) +print("[*] Treys: Evaluations per second = %f" % (1.0 / avg)) + +### + +cumtime = 0.0 +boards, hands = setup(n, 3) +for i in range(len(boards)): + start = time.time() + evaluator.evaluate(hands[i], boards[i]) + cumtime += (time.time() - start) + +avg = float(cumtime / n) +print("5 card evaluation:") +print("[*] Treys: Average time per evaluation: %f" % avg) +print("[*] Treys: Evaluations per second = %f" % (1.0 / avg)) diff --git a/performance/perf_deuces.py b/performance/perf_deuces.py deleted file mode 100644 index f7a0059..0000000 --- a/performance/perf_deuces.py +++ /dev/null @@ -1,60 +0,0 @@ -import time -import random -from deuces import Card, Deck, Evaluator - -def setup(n, m): - - deck = Deck() - - boards = [] - hands = [] - - for i in range(n): - boards.append(deck.draw(m)) - hands.append(deck.draw(2)) - deck.shuffle() - - return boards, hands - - -n = 10000 -cumtime = 0.0 -evaluator = Evaluator() -boards, hands = setup(n, 5) -for i in range(len(boards)): - start = time.time() - evaluator.evaluate(boards[i], hands[i]) - cumtime += (time.time() - start) - -avg = float(cumtime / n) -print "7 card evaluation:" -print "[*] Deuces: Average time per evaluation: %f" % avg -print "[*] Decues: Evaluations per second = %f" % (1.0 / avg) - -### - -cumtime = 0.0 -boards, hands = setup(n, 4) -for i in range(len(boards)): - start = time.time() - evaluator.evaluate(boards[i], hands[i]) - cumtime += (time.time() - start) - -avg = float(cumtime / n) -print "6 card evaluation:" -print "[*] Deuces: Average time per evaluation: %f" % avg -print "[*] Decues: Evaluations per second = %f" % (1.0 / avg) - -### - -cumtime = 0.0 -boards, hands = setup(n, 3) -for i in range(len(boards)): - start = time.time() - evaluator.evaluate(boards[i], hands[i]) - cumtime += (time.time() - start) - -avg = float(cumtime / n) -print "5 card evaluation:" -print "[*] Deuces: Average time per evaluation: %f" % avg -print "[*] Decues: Evaluations per second = %f" % (1.0 / avg) diff --git a/performance/perf_handeval.py b/performance/perf_handeval.py deleted file mode 100644 index c0912a7..0000000 --- a/performance/perf_handeval.py +++ /dev/null @@ -1,43 +0,0 @@ -from card import Card -from hand_evaluator import HandEvaluator -import time -import random - -def setup(n): - - hands = [] - boards = [] - - full_deck = [] - for i in range(2, 14 + 1): - for j in range(1, 4 + 1): - full_deck.append(Card(i, j)) - - - for i in range(n): - - deck = list(full_deck) - random.shuffle(deck) - hand = [] - board = [] - for j in range(2): - hand.append(deck.pop(0)) - for j in range(5): - board.append(deck.pop(0)) - - hands.append(hand) - boards.append(board) - - return boards, hands - -N = 10000 -cumtime = 0.0 -boards, hands = setup(N) -for i in range(len(boards)): - start = time.time() - HandEvaluator.evaluate_hand(hands[i], boards[i]) - cumtime += (time.time() - start) - -avg = float(cumtime / N) -print "[*] Pokerhand-eval: Average time per evaluation: %f" % avg -print "[*] Pokerhand-eval: Evaluations per second = %f" % (1.0 / avg) \ No newline at end of file diff --git a/performance/perf_specialk.py b/performance/perf_specialk.py deleted file mode 100644 index 7568eb1..0000000 --- a/performance/perf_specialk.py +++ /dev/null @@ -1,56 +0,0 @@ -import time -from SevenEval import SevenEval -from FiveEval import FiveEval -import random - -def setup(n, m): - - hands = [] - boards = [] - - for i in range(n): - - deck = range(52) - random.shuffle(deck) - hand = [] - board = [] - for j in range(2): - hand.append(deck.pop(0)) - for j in range(m): - board.append(deck.pop(0)) - - hands.append(hand) - boards.append(board) - - return boards, hands - -s = SevenEval() - -N = 10000 -cumtime = 0.0 -boards, hands = setup(N, 5) -for i in range(len(boards)): - start = time.time() - s.getRankOfSeven(*(boards[i] + hands[i])) - cumtime += (time.time() - start) - -avg = float(cumtime / N) -print "7 card evaluation:" -print "[*] SpecialK: Average time per evaluation: %f" % avg -print "[*] SpecialK: Evaluations per second = %f" % (1.0 / avg) - -#### - -f = FiveEval() - -cumtime = 0.0 -boards, hands = setup(N, 3) -for i in range(len(boards)): - start = time.time() - f.getRankOfFive(*(boards[i] + hands[i])) - cumtime += (time.time() - start) - -avg = float(cumtime / N) -print "5 card evaluation:" -print "[*] SpecialK: Average time per evaluation: %f" % avg -print "[*] SpecialK: Evaluations per second = %f" % (1.0 / avg) \ No newline at end of file diff --git a/plo_go.py b/plo_go.py new file mode 100644 index 0000000..d16e192 --- /dev/null +++ b/plo_go.py @@ -0,0 +1,66 @@ +from treys.card import Card +from treys.evaluator import PLOEvaluator +from treys.deck import Deck + + +# create a card +card = Card.new('Qh') + +# create a board and hole cards +board = [ + Card.new('2h'), + Card.new('2s'), + Card.new('Jc'), + Card.new('Ah'), + Card.new('Ks') +] +hand = [ + Card.new('Qs'), + Card.new('Th'), + Card.new('9c'), + Card.new('8s') +] + +# pretty print cards to console +Card.print_pretty_cards(board) +Card.print_pretty_cards(hand) + +# create an evaluator +evaluator = PLOEvaluator() + +# and rank your hand +rank = evaluator.evaluate(hand, board) +class_ = evaluator.get_rank_class(rank) +print("{} {}".format(rank, evaluator.class_to_string(class_))) +print() + +# or for random cards or games, create a deck +print("Dealing a new hand...") +deck = Deck() +board = deck.draw(5) +player1_hand = deck.draw(4) +player2_hand = deck.draw(4) + +print("The board:") +Card.print_pretty_cards(board) + +print("Player 1's cards:") +Card.print_pretty_cards(player1_hand) + +print("Player 2's cards:") +Card.print_pretty_cards(player2_hand) + +p1_score = evaluator.evaluate(player1_hand, board) +p2_score = evaluator.evaluate(player2_hand, board) + +# bin the scores into classes +p1_class = evaluator.get_rank_class(p1_score) +p2_class = evaluator.get_rank_class(p2_score) + +# or get a human-friendly string to describe the score +print("Player 1 hand rank = {} {}".format(p1_score, evaluator.class_to_string(p1_class))) +print("Player 2 hand rank = {} {}".format(p2_score, evaluator.class_to_string(p2_class))) + +# or just a summary of the entire hand +hands = [player1_hand, player2_hand] +evaluator.hand_summary(board, hands) diff --git a/plo_perf.py b/plo_perf.py new file mode 100644 index 0000000..8ff9bb2 --- /dev/null +++ b/plo_perf.py @@ -0,0 +1,62 @@ +import time + +from treys.deck import Deck +from treys.evaluator import PLOEvaluator + + +def setup(n: int, m: int) -> tuple[list[list[int]], list[list[int]]]: + + deck = Deck() + + boards = [] + hands = [] + + for _ in range(n): + boards.append(deck.draw(m)) + hands.append(deck.draw(4)) + deck.shuffle() + + return boards, hands + + +n = 10000 +cumtime = 0.0 +evaluator = PLOEvaluator() +boards, hands = setup(n, 5) +for i in range(len(boards)): + start = time.time() + evaluator.evaluate(hands[i], boards[i]) + cumtime += (time.time() - start) + +avg = float(cumtime / n) +print("9 card evaluation:") +print("[*] Treys: Average time per evaluation: %f" % avg) +print("[*] Treys: Evaluations per second = %f" % (1.0 / avg)) + +### + +cumtime = 0.0 +boards, hands = setup(n, 4) +for i in range(len(boards)): + start = time.time() + evaluator.evaluate(hands[i], boards[i]) + cumtime += (time.time() - start) + +avg = float(cumtime / n) +print("8 card evaluation:") +print("[*] Treys: Average time per evaluation: %f" % avg) +print("[*] Treys: Evaluations per second = %f" % (1.0 / avg)) + +### + +cumtime = 0.0 +boards, hands = setup(n, 3) +for i in range(len(boards)): + start = time.time() + evaluator.evaluate(hands[i], boards[i]) + cumtime += (time.time() - start) + +avg = float(cumtime / n) +print("7 card evaluation:") +print("[*] Treys: Average time per evaluation: %f" % avg) +print("[*] Treys: Evaluations per second = %f" % (1.0 / avg)) diff --git a/setup.py b/setup.py index c4d454e..cac08a3 100644 --- a/setup.py +++ b/setup.py @@ -1,24 +1,26 @@ """ -Deuces: A pure Python poker hand evaluation library +Treys: A pure Python poker hand evaluation library """ -from setuptools import setup +from setuptools import setup, find_packages + setup( - name='deuces', - version='0.1', - description=__doc__, - long_description=open('README.md').read(), - author='Will Drevo', - url='https://github.com/worldveil/deuces', + name='treys', + version='0.1.8', + description='treys is a pure Python poker hand evaluation library', + long_description=open('README.rst').read(), + author='Will Drevo, Mark Saindon, Imran Hendley', + url='https://github.com/ihendley/treys', license='MIT', - packages=['deuces'], + packages=find_packages(include=['treys', 'treys.*']), classifiers=[ 'Development Status :: 3 - Alpha', 'Intended Audience :: Developers', 'Intended Audience :: Education', 'Intended Audience :: Science/Research', 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 3', 'Topic :: Games/Entertainment' ] ) diff --git a/treys/__init__.py b/treys/__init__.py new file mode 100644 index 0000000..74f4337 --- /dev/null +++ b/treys/__init__.py @@ -0,0 +1,3 @@ +from .card import Card +from .deck import Deck +from .evaluator import Evaluator, PLOEvaluator diff --git a/deuces/card.py b/treys/card.py similarity index 75% rename from deuces/card.py rename to treys/card.py index 753aea2..7956fbf 100644 --- a/deuces/card.py +++ b/treys/card.py @@ -1,3 +1,5 @@ +from typing import Sequence + class Card: """ Static class that handles cards. We represent cards as 32-bit integers, so @@ -26,33 +28,41 @@ class Card: """ # the basics - STR_RANKS = '23456789TJQKA' - INT_RANKS = range(13) - PRIMES = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41] + STR_RANKS: str = '23456789TJQKA' + STR_SUITS: str = 'shdc' + INT_RANKS: range = range(13) + PRIMES: list[int] = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41] # conversion from string => int - CHAR_RANK_TO_INT_RANK = dict(zip(list(STR_RANKS), INT_RANKS)) - CHAR_SUIT_TO_INT_SUIT = { + CHAR_RANK_TO_INT_RANK: dict[str, int] = dict(zip(list(STR_RANKS), INT_RANKS)) + CHAR_SUIT_TO_INT_SUIT: dict[str, int] = { 's': 1, # spades 'h': 2, # hearts 'd': 4, # diamonds 'c': 8, # clubs + '\u2660': 1, # spades (unicode) + '\u2764': 2, # hearts (unicode) + '\u2666': 4, # diamonds (unicode) + '\u2663': 8, # clubs (unicode) } - INT_SUIT_TO_CHAR_SUIT = 'xshxdxxxc' + INT_SUIT_TO_CHAR_SUIT: str = 'xshxdxxxc' # for pretty printing - PRETTY_SUITS = { + PRETTY_SUITS: dict[int, str] = { 1: chr(9824), # spades 2: chr(9829), # hearts 4: chr(9830), # diamonds 8: chr(9827) # clubs } - # hearts and diamonds - PRETTY_REDS = [2, 4] + SUIT_COLORS: dict[int, str] = { + 2: "red", + 4: "blue", + 8: "green" + } @staticmethod - def new(string): + def new(string: str) -> int: """ Converts Card string to binary integer representation of card, inspired by: @@ -72,29 +82,29 @@ def new(string): return bitrank | suit | rank | rank_prime @staticmethod - def int_to_str(card_int): + def int_to_str(card_int: int) -> str: rank_int = Card.get_rank_int(card_int) suit_int = Card.get_suit_int(card_int) return Card.STR_RANKS[rank_int] + Card.INT_SUIT_TO_CHAR_SUIT[suit_int] @staticmethod - def get_rank_int(card_int): + def get_rank_int(card_int: int) -> int: return (card_int >> 8) & 0xF @staticmethod - def get_suit_int(card_int): + def get_suit_int(card_int: int) -> int: return (card_int >> 12) & 0xF @staticmethod - def get_bitrank_int(card_int): + def get_bitrank_int(card_int: int) -> int: return (card_int >> 16) & 0x1FFF @staticmethod - def get_prime(card_int): + def get_prime(card_int: int) -> int: return card_int & 0x3F @staticmethod - def hand_to_binary(card_strs): + def hand_to_binary(card_strs: Sequence[str]) -> list[int]: """ Expects a list of cards as strings and returns a list of integers of same length corresponding to those strings. @@ -105,7 +115,7 @@ def hand_to_binary(card_strs): return bhand @staticmethod - def prime_product_from_hand(card_ints): + def prime_product_from_hand(card_ints: Sequence[int]) -> int: """ Expects a list of cards in integer form. """ @@ -117,7 +127,7 @@ def prime_product_from_hand(card_ints): return product @staticmethod - def prime_product_from_rankbits(rankbits): + def prime_product_from_rankbits(rankbits: int) -> int: """ Returns the prime product using the bitrank (b) bits of the hand. Each 1 in the sequence is converted @@ -148,7 +158,7 @@ def prime_product_from_rankbits(rankbits): return product @staticmethod - def int_to_binary(card_int): + def int_to_binary(card_int: int) -> str: """ For debugging purposes. Displays the binary number as a human readable string in groups of four digits. @@ -164,7 +174,7 @@ def int_to_binary(card_int): return "".join(output) @staticmethod - def int_to_pretty_str(card_int): + def int_to_pretty_str(card_int: int) -> str: """ Prints a single card """ @@ -182,24 +192,24 @@ def int_to_pretty_str(card_int): suit_int = Card.get_suit_int(card_int) rank_int = Card.get_rank_int(card_int) - # if we need to color red + # color s = Card.PRETTY_SUITS[suit_int] - if color and suit_int in Card.PRETTY_REDS: - s = colored(s, "red") + if color and suit_int in Card.SUIT_COLORS: + s = colored(s, Card.SUIT_COLORS[suit_int]) r = Card.STR_RANKS[rank_int] - return f"[{r}{s}]" + return "[{}{}]".format(r,s) @staticmethod - def print_pretty_card(card_int): + def print_pretty_card(card_int: int) -> None: """ Expects a single integer as input """ print(Card.int_to_pretty_str(card_int)) @staticmethod - def print_pretty_cards(card_ints): + def ints_to_pretty_str(card_ints: Sequence[int]) -> str: """ Expects a list of cards in integer form. """ @@ -211,4 +221,11 @@ def print_pretty_cards(card_ints): else: output += str(Card.int_to_pretty_str(c)) + " " - print(output) + return output + + @staticmethod + def print_pretty_cards(card_ints: Sequence[int]) -> None: + """ + Expects a list of cards in integer form. + """ + print(Card.ints_to_pretty_str(card_ints)) diff --git a/deuces/deck.py b/treys/deck.py similarity index 57% rename from deuces/deck.py rename to treys/deck.py index 59c24dd..7743fca 100644 --- a/deuces/deck.py +++ b/treys/deck.py @@ -1,4 +1,5 @@ -from random import shuffle as rshuffle +from random import Random + from .card import Card @@ -8,36 +9,34 @@ class Deck: deck with the list of unique card integers. Each object instantiated simply makes a copy of this object and shuffles it. """ - _FULL_DECK = [] + _FULL_DECK: list[int] = [] - def __init__(self): + def __init__(self, seed: int = None) -> None: + self._random = Random(seed) self.shuffle() - def shuffle(self): + def shuffle(self) -> None: # and then shuffle self.cards = Deck.GetFullDeck() - rshuffle(self.cards) - - def draw(self, n=1): - if n == 1: - return self.cards.pop(0) + self._random.shuffle(self.cards) + def draw(self, n: int = 1) -> list[int]: cards = [] - for i in range(n): - cards.append(self.draw()) + for _ in range(n): + cards.append(self.cards.pop()) return cards - def __str__(self): - return Card.print_pretty_cards(self.cards) + def __str__(self) -> str: + return Card.ints_to_pretty_str(self.cards) @staticmethod - def GetFullDeck(): + def GetFullDeck() -> list[int]: if Deck._FULL_DECK: return list(Deck._FULL_DECK) # create the standard 52 card deck for rank in Card.STR_RANKS: - for suit, val in Card.CHAR_SUIT_TO_INT_SUIT.items(): + for suit in Card.STR_SUITS: Deck._FULL_DECK.append(Card.new(rank + suit)) return list(Deck._FULL_DECK) \ No newline at end of file diff --git a/deuces/evaluator.py b/treys/evaluator.py similarity index 70% rename from deuces/evaluator.py rename to treys/evaluator.py index 2fa8e6d..df8fc2f 100644 --- a/deuces/evaluator.py +++ b/treys/evaluator.py @@ -1,9 +1,11 @@ import itertools +from typing import Sequence + from .card import Card -from .deck import Deck from .lookup import LookupTable -class Evaluator(object): + +class Evaluator: """ Evaluates hand strengths using a variant of Cactus Kev's algorithm: http://suffe.cool/poker/evaluator.html @@ -14,7 +16,10 @@ class Evaluator(object): all calculations are done with bit arithmetic and table lookups. """ - def __init__(self): + HAND_LENGTH = 2 + BOARD_LENGTH = 5 + + def __init__(self) -> None: self.table = LookupTable() @@ -24,17 +29,16 @@ def __init__(self): 7: self._seven } - def evaluate(self, cards, board): + def evaluate(self, hand: list[int], board: list[int]) -> int: """ This is the function that the user calls to get a hand rank. - Supports empty board, etc very flexible. No input validation - because that's cycles! + No input validation because that's cycles! """ - all_cards = cards + board + all_cards = hand + board return self.hand_size_map[len(all_cards)](all_cards) - def _five(self, cards): + def _five(self, cards: Sequence[int]) -> int: """ Performs an evalution given cards in integer form, mapping them to a rank in the range [1, 7462], with lower ranks being more powerful. @@ -53,7 +57,7 @@ def _five(self, cards): prime = Card.prime_product_from_hand(cards) return self.table.unsuited_lookup[prime] - def _six(self, cards): + def _six(self, cards: Sequence[int]) -> int: """ Performs five_card_eval() on all (6 choose 5) = 6 subsets of 5 cards in the set of 6 to determine the best ranking, @@ -61,8 +65,7 @@ def _six(self, cards): """ minimum = LookupTable.MAX_HIGH_CARD - all5cardcombobs = itertools.combinations(cards, 5) - for combo in all5cardcombobs: + for combo in itertools.combinations(cards, 5): score = self._five(combo) if score < minimum: @@ -70,7 +73,7 @@ def _six(self, cards): return minimum - def _seven(self, cards): + def _seven(self, cards: Sequence[int]) -> int: """ Performs five_card_eval() on all (7 choose 5) = 21 subsets of 5 cards in the set of 7 to determine the best ranking, @@ -78,8 +81,7 @@ def _seven(self, cards): """ minimum = LookupTable.MAX_HIGH_CARD - all5cardcombobs = itertools.combinations(cards, 5) - for combo in all5cardcombobs: + for combo in itertools.combinations(cards, 5): score = self._five(combo) if score < minimum: @@ -87,12 +89,14 @@ def _seven(self, cards): return minimum - def get_rank_class(self, hr): + def get_rank_class(self, hr: int) -> int: """ Returns the class of hand given the hand hand_rank returned from evaluate. """ - if hr >= 0 and hr <= LookupTable.MAX_STRAIGHT_FLUSH: + if hr >= 0 and hr <= LookupTable.MAX_ROYAL_FLUSH: + return LookupTable.MAX_TO_RANK_CLASS[LookupTable.MAX_ROYAL_FLUSH] + elif hr <= LookupTable.MAX_STRAIGHT_FLUSH: return LookupTable.MAX_TO_RANK_CLASS[LookupTable.MAX_STRAIGHT_FLUSH] elif hr <= LookupTable.MAX_FOUR_OF_A_KIND: return LookupTable.MAX_TO_RANK_CLASS[LookupTable.MAX_FOUR_OF_A_KIND] @@ -113,19 +117,19 @@ def get_rank_class(self, hr): else: raise Exception("Inavlid hand rank, cannot return rank class") - def class_to_string(self, class_int): + def class_to_string(self, class_int: int) -> str: """ Converts the integer class hand score into a human-readable string. """ return LookupTable.RANK_CLASS_TO_STRING[class_int] - def get_five_card_rank_percentage(self, hand_rank): + def get_five_card_rank_percentage(self, hand_rank: int) -> float: """ Scales the hand rank score to the [0.0, 1.0] range. """ return float(hand_rank) / float(LookupTable.MAX_HIGH_CARD) - def hand_summary(self, board, hands): + def hand_summary(self, board: list[int], hands: list[list[int]]) -> None: """ Gives a sumamry of the hand with ranks as time proceeds. @@ -133,16 +137,16 @@ def hand_summary(self, board, hands): analysis to make sense. """ - assert len(board) == 5, "Invalid board length" + assert len(board) == self.BOARD_LENGTH, "Invalid board length" for hand in hands: - assert len(hand) == 2, "Inavlid hand length" + assert len(hand) == self.HAND_LENGTH, "Invalid hand length" line_length = 10 stages = ["FLOP", "TURN", "RIVER"] for i in range(len(stages)): line = "=" * line_length - print(f"{line} {stages[i]} {line}") + print("{} {} {}".format(line,stages[i],line)) best_rank = 7463 # rank one worse than worst hand winners = [] @@ -153,7 +157,7 @@ def hand_summary(self, board, hands): rank_class = self.get_rank_class(rank) class_string = self.class_to_string(rank_class) percentage = 1.0 - self.get_five_card_rank_percentage(rank) # higher better here - print(f"Player {player + 1} hand = {class_string}, percentage rank among all hands = {percentage}") + print("Player {} hand = {}, percentage rank among all hands = {}".format(player + 1, class_string, percentage)) # detect winner if rank == best_rank: @@ -166,16 +170,32 @@ def hand_summary(self, board, hands): # if we're not on the river if i != stages.index("RIVER"): if len(winners) == 1: - print(f"Player {winners[0] + 1} hand is currently winning.\n") + print("Player {} hand is currently winning.\n".format(winners[0] + 1)) else: - print(f"Players {[x + 1 for x in winners]} are tied for the lead.\n") + print("Players {} are tied for the lead.\n".format([x + 1 for x in winners])) # otherwise on all other streets else: hand_result = self.class_to_string(self.get_rank_class(self.evaluate(hands[winners[0]], board))) print() - print(f"{line} HAND OVER {line}") + print("{} HAND OVER {}".format(line, line)) if len(winners) == 1: - print(f"Player {winners[0] + 1} is the winner with a {hand_result}\n") + print("Player {} is the winner with a {}\n".format(winners[0] + 1, hand_result)) else: - print(f"Players {winners} tied for the win with a {hand_result}\n") + print("Players {} tied for the win with a {}\n".format([x + 1 for x in winners],hand_result)) + + +class PLOEvaluator(Evaluator): + + HAND_LENGTH = 4 + + def evaluate(self, hand: list[int], board: list[int]) -> int: + minimum = LookupTable.MAX_HIGH_CARD + + for hand_combo in itertools.combinations(hand, 2): + for board_combo in itertools.combinations(board, 3): + score = Evaluator._five(self, list(board_combo) + list(hand_combo)) + if score < minimum: + minimum = score + + return minimum diff --git a/deuces/lookup.py b/treys/lookup.py similarity index 84% rename from deuces/lookup.py rename to treys/lookup.py index 2a30136..dacd332 100644 --- a/deuces/lookup.py +++ b/treys/lookup.py @@ -1,8 +1,11 @@ +from collections.abc import Iterator import itertools +from typing import Sequence + from .card import Card -class LookupTable(object): +class LookupTable: """ Number of Distinct Hand Values: @@ -25,17 +28,19 @@ class LookupTable(object): * Royal flush (best hand possible) => 1 * 7-5-4-3-2 unsuited (worst hand possible) => 7462 """ - MAX_STRAIGHT_FLUSH = 10 - MAX_FOUR_OF_A_KIND = 166 - MAX_FULL_HOUSE = 322 - MAX_FLUSH = 1599 - MAX_STRAIGHT = 1609 - MAX_THREE_OF_A_KIND = 2467 - MAX_TWO_PAIR = 3325 - MAX_PAIR = 6185 - MAX_HIGH_CARD = 7462 - - MAX_TO_RANK_CLASS = { + MAX_ROYAL_FLUSH: int = 1 + MAX_STRAIGHT_FLUSH: int = 10 + MAX_FOUR_OF_A_KIND: int = 166 + MAX_FULL_HOUSE: int = 322 + MAX_FLUSH: int = 1599 + MAX_STRAIGHT: int = 1609 + MAX_THREE_OF_A_KIND: int = 2467 + MAX_TWO_PAIR: int = 3325 + MAX_PAIR: int = 6185 + MAX_HIGH_CARD: int = 7462 + + MAX_TO_RANK_CLASS: dict[int, int] = { + MAX_ROYAL_FLUSH: 0, MAX_STRAIGHT_FLUSH: 1, MAX_FOUR_OF_A_KIND: 2, MAX_FULL_HOUSE: 3, @@ -47,7 +52,8 @@ class LookupTable(object): MAX_HIGH_CARD: 9 } - RANK_CLASS_TO_STRING = { + RANK_CLASS_TO_STRING: dict[int, str] = { + 0: "Royal Flush", 1: "Straight Flush", 2: "Four of a Kind", 3: "Full House", @@ -59,13 +65,13 @@ class LookupTable(object): 9: "High Card" } - def __init__(self): + def __init__(self) -> None: """ Calculates lookup tables """ # create dictionaries - self.flush_lookup = {} - self.unsuited_lookup = {} + self.flush_lookup: dict[int, int] = {} + self.unsuited_lookup: dict[int, int] = {} # create the lookup table in piecewise fashion # this will call straights and high cards method, @@ -73,7 +79,7 @@ def __init__(self): self.flushes() self.multiples() - def flushes(self): + def flushes(self) -> None: """ Straight flushes and flushes. @@ -145,7 +151,7 @@ def flushes(self): # and differ only by context self.straight_and_highcards(straight_flushes, flushes) - def straight_and_highcards(self, straights, highcards): + def straight_and_highcards(self, straights: Sequence[int], highcards: Sequence[int]) -> None: """ Unique five card sets. Straights and highcards. @@ -164,7 +170,7 @@ def straight_and_highcards(self, straights, highcards): self.unsuited_lookup[prime_product] = rank rank += 1 - def multiples(self): + def multiples(self) -> None: """ Pair, Two Pair, Three of a Kind, Full House, and 4 of a Kind. """ @@ -208,9 +214,9 @@ def multiples(self): kickers.remove(r) gen = itertools.combinations(kickers, 2) - for kickers in gen: + for kickers_2combo in gen: - c1, c2 = kickers + c1, c2 = kickers_2combo product = Card.PRIMES[r]**3 * Card.PRIMES[c1] * Card.PRIMES[c2] self.unsuited_lookup[product] = rank rank += 1 @@ -218,7 +224,7 @@ def multiples(self): # 4) Two Pair rank = LookupTable.MAX_THREE_OF_A_KIND + 1 - tpgen = itertools.combinations(backwards_ranks, 2) + tpgen = itertools.combinations(tuple(backwards_ranks), 2) for tp in tpgen: pair1, pair2 = tp @@ -239,25 +245,25 @@ def multiples(self): kickers = backwards_ranks[:] kickers.remove(pairrank) - kgen = itertools.combinations(kickers, 3) + kgen = itertools.combinations(tuple(kickers), 3) - for kickers in kgen: + for kickers_3combo in kgen: - k1, k2, k3 = kickers + k1, k2, k3 = kickers_3combo product = Card.PRIMES[pairrank]**2 * Card.PRIMES[k1] \ * Card.PRIMES[k2] * Card.PRIMES[k3] self.unsuited_lookup[product] = rank rank += 1 - def write_table_to_disk(self, table, filepath): + def write_table_to_disk(self, table: dict[int, int], filepath: str) -> None: """ Writes lookup table to disk """ with open(filepath, 'w') as f: - for prime_prod, rank in table.iteritems(): + for prime_prod, rank in table.items(): f.write(str(prime_prod) + "," + str(rank) + '\n') - def get_lexographically_next_bit_sequence(self, bits): + def get_lexographically_next_bit_sequence(self, bits: int) -> Iterator[int]: """ Bit hack from here: http://www-graphics.stanford.edu/~seander/bithacks.html#NextBitPermutation @@ -271,4 +277,4 @@ def get_lexographically_next_bit_sequence(self, bits): while True: t = (next | (next - 1)) + 1 next = t | ((((t & -t) // (next & -next)) >> 1) - 1) - yield next \ No newline at end of file + yield next