diff --git a/python/benchmark.py b/python/benchmark.py index 9aaf635..5143b26 100644 --- a/python/benchmark.py +++ b/python/benchmark.py @@ -1,32 +1,34 @@ import time from itertools import combinations -from phevaluator import _evaluate_cards, _evaluate_omaha_cards, sample_cards +from phevaluator import _evaluate_cards +from phevaluator import _evaluate_omaha_cards +from phevaluator import sample_cards -def evaluate_all_five_card_hands(): +def evaluate_all_five_card_hands() -> None: for cards in combinations(range(52), 5): _evaluate_cards(*cards) -def evaluate_all_six_card_hands(): +def evaluate_all_six_card_hands() -> None: for cards in combinations(range(52), 6): _evaluate_cards(*cards) -def evaluate_all_seven_card_hands(): +def evaluate_all_seven_card_hands() -> None: for cards in combinations(range(52), 7): _evaluate_cards(*cards) -def evaluate_random_omaha_card_hands(): +def evaluate_random_omaha_card_hands() -> None: total = 100_000 for _ in range(total): cards = sample_cards(9) _evaluate_omaha_cards(cards[:5], cards[5:]) -def benchmark(): +def benchmark() -> None: print("--------------------------------------------------------------------") print("Benchmark Time") t = time.process_time() diff --git a/python/examples.py b/python/examples.py index 6b949bd..b075afe 100644 --- a/python/examples.py +++ b/python/examples.py @@ -1,7 +1,8 @@ -from phevaluator import evaluate_cards, evaluate_omaha_cards +from phevaluator import evaluate_cards +from phevaluator import evaluate_omaha_cards -def example1(): +def example1() -> None: print("Example 1: A Texas Holdem example") a = 7 * 4 + 0 # 9c @@ -26,7 +27,7 @@ def example1(): print("Player 2 has a stronger hand") -def example2(): +def example2() -> None: print("Example 2: Another Texas Holdem example") rank1 = evaluate_cards("9c", "4c", "4s", "9d", "4h", "Qc", "6c") # expected 292 @@ -37,7 +38,7 @@ def example2(): print("Player 2 has a stronger hand") -def example3(): +def example3() -> None: print("Example 3: An Omaha poker example") # fmt: off rank1 = evaluate_omaha_cards( diff --git a/python/phevaluator/__init__.py b/python/phevaluator/__init__.py index 6e0ea55..6ad9ad8 100644 --- a/python/phevaluator/__init__.py +++ b/python/phevaluator/__init__.py @@ -1,6 +1,6 @@ """Package for evaluating a poker hand.""" -from . import hash as hash_ # FIXME: `hash` collides to built-in function +from typing import Any # import mapping to objects in other modules all_by_module = { @@ -8,7 +8,7 @@ "phevaluator.card": ["Card"], "phevaluator.evaluator": ["_evaluate_cards", "evaluate_cards"], "phevaluator.evaluator_omaha": ["_evaluate_omaha_cards", "evaluate_omaha_cards"], - "phevaluator.utils": ["sample_cards"] + "phevaluator.utils": ["sample_cards"], } # Based on werkzeug library @@ -21,10 +21,11 @@ # Based on https://peps.python.org/pep-0562/ and werkzeug library -def __getattr__(name): - """lazy submodule imports""" +def __getattr__(name: str) -> Any: # noqa: ANN401 + """Lazy submodule imports.""" if name in object_origins: module = __import__(object_origins[name], None, None, [name]) return getattr(module, name) - else: - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + msg = f"module {__name__!r} has no attribute {name!r}" + raise AttributeError(msg) diff --git a/python/phevaluator/card.py b/python/phevaluator/card.py index 631afe1..bdfd0a6 100644 --- a/python/phevaluator/card.py +++ b/python/phevaluator/card.py @@ -1,7 +1,9 @@ """Module for card.""" + from __future__ import annotations -from typing import Any, Union + +CARD_DESCRIPTION_LENGTH = 2 # fmt: off rank_map = { @@ -10,7 +12,7 @@ } suit_map = { "C": 0, "D": 1, "H": 2, "S": 3, - "c": 0, "d": 1, "h": 2, "s": 3 + "c": 0, "d": 1, "h": 2, "s": 3, } # fmt: on @@ -63,33 +65,33 @@ class Card: The string parameter of the constructor should be exactly 2 characters. >>> Card("9h") # OK - >>> Card("9h ") # ERROR + >>> Card("9h ") # ERROR TypeError: Construction with unsupported type The parameter of the constructor should be one of the following types: [int, str, Card]. - >>> Card(0) # OK. The 0 stands 2 of Clubs - >>> Card("2c") # OK - >>> Card("2C") # OK. Capital letter is also accepted. - >>> Card(Card(0)) # OK - >>> Card(0.0) # ERROR. float is not allowed + >>> Card(0) # OK. The 0 stands 2 of Clubs + >>> Card("2c") # OK + >>> Card("2C") # OK. Capital letter is also accepted. + >>> Card(Card(0)) # OK + >>> Card(0.0) # ERROR. float is not allowed TypeError: Setting attribute >>> c = Card("2c") - >>> c.__id = 1 # ERROR - >>> c._Card__id = 1 # ERROR + >>> c.__id = 1 # ERROR + >>> c._Card__id = 1 # ERROR TypeError: Deliting attribute >>> c = Card("2c") - >>> del c.__id # ERROR - >>> del c._Card__id # ERROR + >>> del c.__id # ERROR + >>> del c._Card__id # ERROR """ __slots__ = ["__id"] __id: int - def __init__(self, other: Union[int, str, Card]): + def __init__(self, other: int | str | Card) -> None: """Construct card object. If the passed argument is integer, it's set to `self.__id`. @@ -128,7 +130,7 @@ def id_(self) -> int: return self.__id @staticmethod - def to_id(other: Union[int, str, Card]) -> int: + def to_id(other: int | str | Card) -> int: """Return the Card ID integer as API. If the passed argument is integer, it's returned with doing nothing. @@ -149,17 +151,20 @@ def to_id(other: Union[int, str, Card]) -> int: """ if isinstance(other, int): return other - elif isinstance(other, str): - if len(other) != 2: - raise ValueError(f"The length of value must be 2. passed: {other}") + if isinstance(other, str): + if len(other) != CARD_DESCRIPTION_LENGTH: + msg = ( + f"The length of value must be {CARD_DESCRIPTION_LENGTH}. " + f"passed: {other}" + ) + raise ValueError(msg) rank, suit, *_ = tuple(other) return rank_map[rank] * 4 + suit_map[suit] - elif isinstance(other, Card): + if isinstance(other, Card): return other.id_ - raise TypeError( - f"Type of parameter must be int, str or Card. passed: {type(other)}" - ) + msg = f"Type of parameter must be int, str or Card. passed: {type(other)}" + raise TypeError(msg) def describe_rank(self) -> str: """Calculate card rank. @@ -212,7 +217,7 @@ def describe_card(self) -> str: """ return self.describe_rank() + self.describe_suit() - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: """Return equality. This is special method. Args: @@ -262,10 +267,12 @@ def __hash__(self) -> int: """int: Special method for `hash(self)`.""" return hash(self.id_) - def __setattr__(self, name: str, value: Any) -> None: - """Set an attribute. This causes TypeError since assignment to attribute is prevented.""" - raise TypeError("Card object does not support assignment to attribute") + def __setattr__(self, name: str, value: object) -> None: + """Set an attribute. This causes TypeError since assignment is prevented.""" + msg = "Card object does not support assignment to attribute" + raise TypeError(msg) def __delattr__(self, name: str) -> None: - """Delete an attribute. This causes TypeError since deletion of attribute is prevented.""" - raise TypeError("Card object does not support deletion of attribute") + """Delete an attribute. This causes TypeError since deletion is prevented.""" + msg = "Card object does not support deletion of attribute" + raise TypeError(msg) diff --git a/python/phevaluator/evaluator.py b/python/phevaluator/evaluator.py index 524a09b..2a2b07d 100644 --- a/python/phevaluator/evaluator.py +++ b/python/phevaluator/evaluator.py @@ -1,17 +1,16 @@ """Module evaluating cards.""" -from typing import Union + +from __future__ import annotations from .card import Card from .hash import hash_quinary -from .tables import ( - BINARIES_BY_ID, - FLUSH, - NO_FLUSH_5, - NO_FLUSH_6, - NO_FLUSH_7, - SUITBIT_BY_ID, - SUITS, -) +from .tables import BINARIES_BY_ID +from .tables import FLUSH +from .tables import NO_FLUSH_5 +from .tables import NO_FLUSH_6 +from .tables import NO_FLUSH_7 +from .tables import SUITBIT_BY_ID +from .tables import SUITS MIN_CARDS = 5 MAX_CARDS = 7 @@ -19,7 +18,7 @@ NO_FLUSHES = {5: NO_FLUSH_5, 6: NO_FLUSH_6, 7: NO_FLUSH_7} -def evaluate_cards(*cards: Union[int, str, Card]) -> int: +def evaluate_cards(*cards: int | str | Card) -> int: """Evaluate cards for the best five cards. This function selects the best combination of the five cards from given cards and @@ -27,7 +26,7 @@ def evaluate_cards(*cards: Union[int, str, Card]) -> int: The number of cards must be between 5 and 7. Args: - cards(Union[int, str, Card]): List of cards + cards(int | str | Card): List of cards Raises: ValueError: Unsupported size of the cards @@ -39,17 +38,18 @@ def evaluate_cards(*cards: Union[int, str, Card]) -> int: >>> rank1 = evaluate_cards("Ac", "Ad", "Ah", "As", "Kc") >>> rank2 = evaluate_cards("Ac", "Ad", "Ah", "As", "Kd") >>> rank3 = evaluate_cards("Ac", "Ad", "Ah", "As", "Kc", "Qh") - >>> rank1 == rank2 == rank3 # Those three are evaluated by `A A A A K` + >>> rank1 == rank2 == rank3 # Those three are evaluated by `A A A A K` True """ int_cards = list(map(Card.to_id, cards)) hand_size = len(cards) if not (MIN_CARDS <= hand_size <= MAX_CARDS) or (hand_size not in NO_FLUSHES): - raise ValueError( + msg = ( f"The number of cards must be between {MIN_CARDS} and {MAX_CARDS}." f"passed size: {hand_size}" ) + raise ValueError(msg) return _evaluate_cards(*int_cards) diff --git a/python/phevaluator/evaluator_omaha.py b/python/phevaluator/evaluator_omaha.py index 8af672d..1984c96 100644 --- a/python/phevaluator/evaluator_omaha.py +++ b/python/phevaluator/evaluator_omaha.py @@ -2,14 +2,20 @@ from __future__ import annotations -from typing import List, Union - from .card import Card -from .hash import hash_binary, hash_quinary -from .tables import BINARIES_BY_ID, FLUSH, FLUSH_OMAHA, NO_FLUSH_OMAHA +from .hash import hash_binary +from .hash import hash_quinary +from .tables import BINARIES_BY_ID +from .tables import FLUSH +from .tables import FLUSH_OMAHA +from .tables import NO_FLUSH_OMAHA + +COMMUNITY_CARD_COUNT = 5 +HOLE_CARD_COUNT = 4 +TOTAL_CARD_COUNT = COMMUNITY_CARD_COUNT + HOLE_CARD_COUNT -def evaluate_omaha_cards(*cards: Union[int, str, Card]) -> int: +def evaluate_omaha_cards(*cards: int | str | Card) -> int: """Evaluate cards in Omaha game. In the Omaha rule, players can make hand with 3 cards from the 5 community cards and @@ -17,7 +23,7 @@ def evaluate_omaha_cards(*cards: Union[int, str, Card]) -> int: This function selects the best combination and return its rank. Args: - cards(Union[int, str, Card]): List of cards + cards(int | str | Card]): List of cards The first five parameters are the community cards. The later four parameters are the player hole cards. @@ -38,21 +44,27 @@ def evaluate_omaha_cards(*cards: Union[int, str, Card]) -> int: "Ad", "Kd", "Qd", "Jd" # ["Ad", "Kd"] ) - >>> rank1 == rank2 # Both of them are evaluated by `A K 9 9 6` + >>> rank1 == rank2 # Both of them are evaluated by `A K 9 9 6` True """ int_cards = list(map(Card.to_id, cards)) hand_size = len(cards) - if hand_size != 9: - raise ValueError(f"The number of cards must be 9. passed size: {hand_size}") + if hand_size != TOTAL_CARD_COUNT: + msg = ( + f"The number of cards must be {TOTAL_CARD_COUNT}.", + f"passed size: {hand_size}", + ) + raise ValueError(msg) - community_cards = int_cards[:5] - hole_cards = int_cards[5:] + community_cards = int_cards[:COMMUNITY_CARD_COUNT] + hole_cards = int_cards[COMMUNITY_CARD_COUNT:] return _evaluate_omaha_cards(community_cards, hole_cards) -def _evaluate_omaha_cards(community_cards: List[int], hole_cards: List[int]) -> int: +# TODO(@azriel1rf): `_evaluate_omaha_cards` is too complex. Consider refactoring. +# https://github.com/HenryRLee/PokerHandEvaluator/issues/92 +def _evaluate_omaha_cards(community_cards: list[int], hole_cards: list[int]) -> int: # noqa: C901, PLR0912 value_flush = 10000 value_noflush = 10000 suit_count_board = [0] * 4 @@ -64,9 +76,15 @@ def _evaluate_omaha_cards(community_cards: List[int], hole_cards: List[int]) -> for hole_card in hole_cards: suit_count_hole[hole_card % 4] += 1 + min_flush_count_board = 3 + min_flush_count_hole = 2 + flush_suit = -1 for i in range(4): - if suit_count_board[i] >= 3 and suit_count_hole[i] >= 2: + if ( + suit_count_board[i] >= min_flush_count_board + and suit_count_hole[i] >= min_flush_count_hole + ): flush_suit = i break @@ -84,17 +102,20 @@ def _evaluate_omaha_cards(community_cards: List[int], hole_cards: List[int]) -> if hole_card % 4 == flush_suit: suit_binary_hole |= BINARIES_BY_ID[hole_card] - if flush_count_board == 3 and flush_count_hole == 2: + if ( + flush_count_board == min_flush_count_board + and flush_count_hole == min_flush_count_hole + ): value_flush = FLUSH[suit_binary_board | suit_binary_hole] else: padding = [0x0000, 0x2000, 0x6000] - suit_binary_board |= padding[5 - flush_count_board] - suit_binary_hole |= padding[4 - flush_count_hole] + suit_binary_board |= padding[COMMUNITY_CARD_COUNT - flush_count_board] + suit_binary_hole |= padding[HOLE_CARD_COUNT - flush_count_hole] - board_hash = hash_binary(suit_binary_board, 5) - hole_hash = hash_binary(suit_binary_hole, 4) + board_hash = hash_binary(suit_binary_board, COMMUNITY_CARD_COUNT) + hole_hash = hash_binary(suit_binary_hole, HOLE_CARD_COUNT) value_flush = FLUSH_OMAHA[board_hash * 1365 + hole_hash] @@ -107,8 +128,8 @@ def _evaluate_omaha_cards(community_cards: List[int], hole_cards: List[int]) -> for hole_card in hole_cards: quinary_hole[hole_card // 4] += 1 - board_hash = hash_quinary(quinary_board, 5) - hole_hash = hash_quinary(quinary_hole, 4) + board_hash = hash_quinary(quinary_board, COMMUNITY_CARD_COUNT) + hole_hash = hash_quinary(quinary_hole, HOLE_CARD_COUNT) value_noflush = NO_FLUSH_OMAHA[board_hash * 1820 + hole_hash] diff --git a/python/phevaluator/hash.py b/python/phevaluator/hash.py index 030883b..d20c199 100644 --- a/python/phevaluator/hash.py +++ b/python/phevaluator/hash.py @@ -2,16 +2,15 @@ from __future__ import annotations -from typing import List +from .tables import CHOOSE +from .tables import DP -from .tables import CHOOSE, DP - -def hash_quinary(quinary: List[int], num_cards: int) -> int: +def hash_quinary(quinary: list[int], num_cards: int) -> int: """Hash list of cards. Args: - quinary (List[int]): List of the count of the cards. + quinary (list[int]): List of the count of the cards. num_cards (int): The number of cards. Returns: @@ -64,7 +63,6 @@ def hash_binary(binary: int, num_cards: int) -> int: length = 15 for rank in range(length): - if (binary >> rank) % 2: sum_numb += CHOOSE[length - rank - 1][num_cards] num_cards -= 1 diff --git a/python/phevaluator/utils.py b/python/phevaluator/utils.py index 85de613..537c529 100644 --- a/python/phevaluator/utils.py +++ b/python/phevaluator/utils.py @@ -3,16 +3,15 @@ from __future__ import annotations import random -from typing import List -def sample_cards(size: int) -> List[int]: +def sample_cards(size: int) -> list[int]: """Sample random cards with size. Args: size (int): The size of the sample. Returns: - List[int]: The list of the sampled cards. + list[int]: The list of the sampled cards. """ return random.sample(range(52), k=size) diff --git a/python/setup.cfg b/python/setup.cfg index e653dcb..c6fed38 100644 --- a/python/setup.cfg +++ b/python/setup.cfg @@ -4,37 +4,31 @@ version = 0.5.3.1 description = PH Evaluator - an efficient Poker Hand Evaluator based on a Perfect Hash algorithm long_description = file: README.md long_description_content_type = text/markdown -keywords = poker, texas-holdem, poker-evaluator +url = https://github.com/HenryRLee/PokerHandEvaluator/ author = Henry Lee author_email = lee0906@hotmail.com -url = https://github.com/HenryRLee/PokerHandEvaluator/ -license = Apache License, Version 2.0 -license_files = LICENSE.txt +license = Apache-2.0 +license_files = LICENSE classifiers = - # How mature is this project? Common values are - # 3 - Alpha - # 4 - Beta - # 5 - Production/Stable Development Status :: 3 - Alpha Intended Audience :: Developers License :: OSI Approved :: Apache Software License - Topic :: Software Development :: Libraries Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 - Programming Language :: Python :: 3 :: Only + Topic :: Software Development :: Libraries +keywords = poker, texas-holdem, poker-evaluator project_urls = Bug Tracker = https://github.com/HenryRLee/PokerHandEvaluator/issues Documentation = https://github.com/HenryRLee/PokerHandEvaluator/tree/master/Documentation Source = https://github.com/HenryRLee/PokerHandEvaluator/tree/master/python [options] -python_requires= >=3.8, <4 packages = find: -install_requires = - numpy +python_requires = >=3.8, <4 [options.package_data] phevaluator = tables/*.dat diff --git a/python/tests/table_tests/test_dptables.py b/python/tests/table_tests/test_dptables.py index dd9cbaa..add9876 100644 --- a/python/tests/table_tests/test_dptables.py +++ b/python/tests/table_tests/test_dptables.py @@ -1,80 +1,93 @@ +from __future__ import annotations + import unittest from itertools import combinations_with_replacement +from typing import ClassVar + +from phevaluator.tables import CHOOSE +from phevaluator.tables import DP +from phevaluator.tables import SUITS -from phevaluator.tables import CHOOSE, DP, SUITS +MADE_HAND_CARD_COUNT = 5 class TestSuitsTable(unittest.TestCase): - DP = [0] * len(SUITS) + DP: ClassVar[list[int]] = [0] * len(SUITS) @classmethod - def setUpClass(cls): + def setUpClass(cls) -> None: for k in [5, 6, 7, 8, 9]: cls.update_k(cls.DP, k) @staticmethod - def update_k(table, k): - iterable = list(range(0, k + 1)) + def update_k(table: list[int], k: int) -> None: + iterable = list(range(k + 1)) combs = combinations_with_replacement(iterable, 3) for comb in combs: # comb is in lexicographically sorted order cnts = (comb[0], comb[1] - comb[0], comb[2] - comb[1], k - comb[2]) for suit, cnt in enumerate(cnts): - if cnt >= 5: + if cnt >= MADE_HAND_CARD_COUNT: idx = ( 0x1 * cnts[0] + 0x8 * cnts[1] + 0x40 * cnts[2] + 0x200 * cnts[3] ) - # TODO: Need to check these cases: - # There exist three cases that idxes are same. - # For two different cnts in case of k=9. The - # cases are 72, 520, 576. + # TODO(@ohwi): Check these cases: + # https://github.com/HenryRLee/PokerHandEvaluator/issues/93 + # There exist three cases that idxes are same. + # For two different cnts in case of k=9. + # The cases are 72, 520, 576. if idx in [72, 520, 576] and SUITS[idx] != suit + 1: continue table[idx] = suit + 1 - def test_suits_table(self): + def test_suits_table(self) -> None: self.assertListEqual(self.DP, SUITS) class TestChooseTable(unittest.TestCase): - DP = [[0] * len(CHOOSE[idx]) for idx in range(len(CHOOSE))] - VISIT = [[0] * len(CHOOSE[idx]) for idx in range(len(CHOOSE))] + DP: ClassVar[list[list[int]]] = [ + [0] * len(CHOOSE[idx]) for idx in range(len(CHOOSE)) + ] + VISIT: ClassVar[list[list[int]]] = [ + [0] * len(CHOOSE[idx]) for idx in range(len(CHOOSE)) + ] @classmethod - def setUpClass(cls): + def setUpClass(cls) -> None: for n, row in enumerate(CHOOSE): for r in range(len(row)): cls.nCr(n, r) @classmethod - def nCr(cls, n, r): + def nCr(cls, n: int, r: int) -> int: # noqa: N802 if n < r: return 0 - elif r == 0: + if r == 0: cls.DP[n][r] = 1 return 1 - else: - if cls.VISIT[n][r] == 0: - cls.DP[n][r] = cls.nCr(n - 1, r) + cls.nCr(n - 1, r - 1) - cls.VISIT[n][r] = 1 - return cls.DP[n][r] + if cls.VISIT[n][r] == 0: + cls.DP[n][r] = cls.nCr(n - 1, r) + cls.nCr(n - 1, r - 1) + cls.VISIT[n][r] = 1 + return cls.DP[n][r] - def test_choose_table(self): + def test_choose_table(self) -> None: self.assertListEqual(self.DP, CHOOSE) class TestDpTable(unittest.TestCase): - DP = [[[0] * len(DP[i][j]) for j in range(len(DP[i]))] for i in range(len(DP))] + DP: ClassVar[list[list[list[int]]]] = [ + [[0] * len(DP[i][j]) for j in range(len(DP[i]))] for i in range(len(DP)) + ] @classmethod - def setUpClass(cls): + def setUpClass(cls) -> None: cls.fill_table() @classmethod - def fill_table(cls): + def fill_table(cls) -> None: # Recursion formula: # dp[l][i][j] = dp[l-1][i][j] + dp[1][i][j-l+1] # @@ -91,7 +104,7 @@ def fill_table(cls): # We need (2) because of the restriction. # Make base cases - for j in range(0, 5): + for j in range(5): cls.DP[1][1][j] = 1 for i in range(2, 14): for j in range(10): @@ -107,7 +120,7 @@ def fill_table(cls): if j - l + 1 >= 0: cls.DP[l][i][j] += cls.DP[1][i][j - l + 1] - def test_dp_table(self): + def test_dp_table(self) -> None: self.assertListEqual(self.DP, DP) diff --git a/python/tests/table_tests/test_hashtable.py b/python/tests/table_tests/test_hashtable.py index 37bc6cc..c46ec53 100644 --- a/python/tests/table_tests/test_hashtable.py +++ b/python/tests/table_tests/test_hashtable.py @@ -2,28 +2,27 @@ import unittest from itertools import combinations -from typing import List +from typing import ClassVar from phevaluator.tables import FLUSH class TestFlushTable(unittest.TestCase): - TABLE = [0] * len(FLUSH) - VISIT = [0] * len(FLUSH) - CUR_RANK = 1 - - CACHE: List[int] = [] - BINARIES: List[List[int]] = [] + TABLE: ClassVar[list[int]] = [0] * len(FLUSH) + VISIT: ClassVar[list[int]] = [0] * len(FLUSH) + CUR_RANK: ClassVar[int] = 1 + CACHE: ClassVar[list[int]] = [] + BINARIES: ClassVar[list[list[int]]] = [] @classmethod - def setUpClass(cls): + def setUpClass(cls) -> None: cls.mark_straight() cls.mark_four_of_a_kind() cls.mark_full_house() cls.mark_non_straight() @classmethod - def gen_binary(cls, highest, k, n): + def gen_binary(cls, highest: int, k: int, n: int) -> None: if k == 0: cls.BINARIES.append(cls.CACHE[:]) else: @@ -33,7 +32,7 @@ def gen_binary(cls, highest, k, n): cls.CACHE.remove(i) @classmethod - def mark_straight(cls): + def mark_straight(cls) -> None: for highest in range(12, 3, -1): # From Ace to 6 # k=5 case for base base = [highest - i for i in range(5)] @@ -58,7 +57,7 @@ def mark_straight(cls): cls.CUR_RANK += 1 @classmethod - def mark_non_straight(cls): + def mark_non_straight(cls) -> None: cls.gen_binary(12, 5, 5) for base in cls.BINARIES: base_idx = 0 @@ -76,7 +75,7 @@ def mark_non_straight(cls): cls.CUR_RANK += 1 @classmethod - def mark_six_to_nine(cls, base, base_idx): + def mark_six_to_nine(cls, base: list[int], base_idx: int) -> None: # k=6-9 cases pos_candidates = [i for i in range(13) if i not in base] for r in [1, 2, 3, 4]: # Need to select additional cards @@ -92,20 +91,20 @@ def mark_six_to_nine(cls, base, base_idx): cls.VISIT[idx] = 1 @classmethod - def mark_four_of_a_kind(cls): + def mark_four_of_a_kind(cls) -> None: # Four of a kind # The rank of the four cards: 13C1 # The rank of the other card: 12C1 cls.CUR_RANK += 13 * 12 @classmethod - def mark_full_house(cls): + def mark_full_house(cls) -> None: # Full house # The rank of the cards of three of a kind: 13C1 # The rank of the cards of a pair: 12C1 cls.CUR_RANK += 13 * 12 - def test_flush_table(self): + def test_flush_table(self) -> None: self.assertListEqual(self.TABLE, FLUSH) diff --git a/python/tests/table_tests/test_hashtable5.py b/python/tests/table_tests/test_hashtable5.py index f59bff1..0690a38 100644 --- a/python/tests/table_tests/test_hashtable5.py +++ b/python/tests/table_tests/test_hashtable5.py @@ -1,18 +1,23 @@ +from __future__ import annotations + import unittest -from itertools import combinations, permutations +from itertools import combinations +from itertools import permutations +from typing import ClassVar +from typing import Iterable from phevaluator.hash import hash_quinary from phevaluator.tables import NO_FLUSH_5 class TestNoFlush5Table(unittest.TestCase): - TABLE = [0] * len(NO_FLUSH_5) - VISIT = [0] * len(NO_FLUSH_5) - CUR_RANK = 1 - NUM_CARDS = 5 + TABLE: ClassVar[list[int]] = [0] * len(NO_FLUSH_5) + VISIT: ClassVar[list[int]] = [0] * len(NO_FLUSH_5) + CUR_RANK: ClassVar[int] = 1 + NUM_CARDS: ClassVar[int] = 5 @classmethod - def setUpClass(cls): + def setUpClass(cls) -> None: cls.mark_straight_flush() cls.mark_four_of_a_kind() cls.mark_full_house() @@ -24,15 +29,15 @@ def setUpClass(cls): cls.mark_high_card() @staticmethod - def quinaries(n): + def quinaries(n: int) -> Iterable[tuple[int, ...]]: return permutations(range(13)[::-1], n) @staticmethod - def quinaries_without_duplication(): + def quinaries_without_duplication() -> Iterable[tuple[int, ...]]: return combinations(range(13)[::-1], 5) @classmethod - def mark_four_of_a_kind(cls): + def mark_four_of_a_kind(cls) -> None: # Order 13C2 lexicographically for base in cls.quinaries(2): hand = [0] * 13 @@ -44,7 +49,7 @@ def mark_four_of_a_kind(cls): cls.CUR_RANK += 1 @classmethod - def mark_full_house(cls): + def mark_full_house(cls) -> None: for base in cls.quinaries(2): hand = [0] * 13 hand[base[0]] = 3 @@ -55,7 +60,7 @@ def mark_full_house(cls): cls.CUR_RANK += 1 @classmethod - def mark_straight(cls): + def mark_straight(cls) -> None: for lowest in range(9)[::-1]: # From 10 to 2 hand = [0] * 13 for i in range(lowest, lowest + 5): @@ -77,7 +82,7 @@ def mark_straight(cls): cls.CUR_RANK += 1 @classmethod - def mark_three_of_a_kind(cls): + def mark_three_of_a_kind(cls) -> None: for base in cls.quinaries(3): hand = [0] * 13 hand[base[0]] = 3 @@ -90,7 +95,7 @@ def mark_three_of_a_kind(cls): cls.CUR_RANK += 1 @classmethod - def mark_two_pair(cls): + def mark_two_pair(cls) -> None: for base in cls.quinaries(3): hand = [0] * 13 hand[base[0]] = 2 @@ -103,7 +108,7 @@ def mark_two_pair(cls): cls.CUR_RANK += 1 @classmethod - def mark_one_pair(cls): + def mark_one_pair(cls) -> None: for base in cls.quinaries(4): hand = [0] * 13 hand[base[0]] = 2 @@ -117,7 +122,7 @@ def mark_one_pair(cls): cls.CUR_RANK += 1 @classmethod - def mark_high_card(cls): + def mark_high_card(cls) -> None: for base in cls.quinaries_without_duplication(): hand = [0] * 13 hand[base[0]] = 1 @@ -132,17 +137,17 @@ def mark_high_card(cls): cls.CUR_RANK += 1 @classmethod - def mark_straight_flush(cls): + def mark_straight_flush(cls) -> None: # A-5 High Straight Flush: 10 cls.CUR_RANK += 10 @classmethod - def mark_flush(cls): + def mark_flush(cls) -> None: # Selecting 5 cards in 13: 13C5 # Need to exclude straight: -10 cls.CUR_RANK += int(13 * 12 * 11 * 10 * 9 / (5 * 4 * 3 * 2)) - 10 - def test_noflush5_table(self): + def test_noflush5_table(self) -> None: self.assertListEqual(self.TABLE, NO_FLUSH_5) diff --git a/python/tests/table_tests/test_hashtable6.py b/python/tests/table_tests/test_hashtable6.py index 11cd237..48cc33b 100644 --- a/python/tests/table_tests/test_hashtable6.py +++ b/python/tests/table_tests/test_hashtable6.py @@ -14,10 +14,10 @@ class TestNoFlush6Table(BaseTestNoFlushTable): NUM_CARDS = 6 @classmethod - def setUpClass(cls): + def setUpClass(cls) -> None: super().setUpClass() - def test_noflush6_table(self): + def test_noflush6_table(self) -> None: self.assertListEqual(self.TABLE, self.TOCOMPARE) diff --git a/python/tests/table_tests/test_hashtable7.py b/python/tests/table_tests/test_hashtable7.py index e1d6585..87fc020 100644 --- a/python/tests/table_tests/test_hashtable7.py +++ b/python/tests/table_tests/test_hashtable7.py @@ -14,10 +14,10 @@ class TestNoFlush7Table(BaseTestNoFlushTable): NUM_CARDS = 7 @classmethod - def setUpClass(cls): + def setUpClass(cls) -> None: super().setUpClass() - def test_noflush7_table(self): + def test_noflush7_table(self) -> None: self.assertListEqual(self.TABLE, self.TOCOMPARE) diff --git a/python/tests/table_tests/utils.py b/python/tests/table_tests/utils.py index 6a9a69c..712bd2a 100644 --- a/python/tests/table_tests/utils.py +++ b/python/tests/table_tests/utils.py @@ -1,20 +1,30 @@ from __future__ import annotations import unittest -from itertools import combinations, combinations_with_replacement, permutations -from typing import List +from itertools import combinations +from itertools import combinations_with_replacement +from itertools import permutations +from typing import Iterable from phevaluator.hash import hash_quinary from phevaluator.tables import NO_FLUSH_5 +SUITS_COUNT = 4 + class BaseTestNoFlushTable(unittest.TestCase): - TABLE: List[int] = NotImplemented - VISIT: List[int] = NotImplemented + TABLE: list[int] = NotImplemented + VISIT: list[int] = NotImplemented NUM_CARDS: int = NotImplemented + CACHE: list[int] + USED: list[int] + QUINARIES: list[tuple[list[int], list[list[int]]]] + CACHE_ADDITIONAL: list[int] + USED_ADDITIONAL: list[int] + QUINARIES_ADDITIONAL: list[list[int]] @classmethod - def setUpClass(cls): + def setUpClass(cls) -> None: cls.CACHE = [] cls.USED = [0] * 13 cls.QUINARIES = [] @@ -34,19 +44,19 @@ def setUpClass(cls): cls.mark_high_card() @staticmethod - def quinary_permutations(n): + def quinary_permutations(n: int) -> Iterable[tuple[int, ...]]: return permutations(range(13)[::-1], n) @staticmethod - def quinary_combinations(n): + def quinary_combinations(n: int) -> Iterable[tuple[int, ...]]: return combinations(range(13)[::-1], n) @staticmethod - def quinary_combinations_with_replacement(n): + def quinary_combinations_with_replacement(n: int) -> Iterable[tuple[int, ...]]: return combinations_with_replacement(range(13)[::-1], n) @classmethod - def gen_quinary(cls, ks, cur, additional): + def gen_quinary(cls, ks: tuple[int, ...], cur: int, additional: int) -> None: if cur == len(ks): cls.get_additional(additional) cls.QUINARIES.append((cls.CACHE[:], cls.QUINARIES_ADDITIONAL[:])) @@ -62,12 +72,12 @@ def gen_quinary(cls, ks, cur, additional): cls.USED[i] = 0 @classmethod - def get_additional(cls, n): + def get_additional(cls, n: int) -> None: if n == 0: cls.QUINARIES_ADDITIONAL.append(cls.CACHE_ADDITIONAL[:]) else: for i in range(12, -1, -1): - if cls.USED[i] + cls.USED_ADDITIONAL[i] >= 4: + if cls.USED[i] + cls.USED_ADDITIONAL[i] >= SUITS_COUNT: continue cls.CACHE_ADDITIONAL.append(i) cls.USED_ADDITIONAL[i] += 1 @@ -76,7 +86,7 @@ def get_additional(cls, n): cls.USED_ADDITIONAL[i] -= 1 @classmethod - def mark_template(cls, ks): + def mark_template(cls, ks: tuple[int, ...]) -> None: cls.gen_quinary(ks, 0, cls.NUM_CARDS - 5) for base, additionals in cls.QUINARIES: hand = [0] * 13 @@ -85,7 +95,6 @@ def mark_template(cls, ks): base_rank = NO_FLUSH_5[hash_quinary(hand, 5)] for additional in additionals: for i in additional: - hand[i] += 1 hash_ = hash_quinary(hand, cls.NUM_CARDS) @@ -101,23 +110,23 @@ def mark_template(cls, ks): cls.QUINARIES = [] @classmethod - def mark_four_of_a_kind(cls): + def mark_four_of_a_kind(cls) -> None: cls.mark_template((4, 1)) @classmethod - def mark_full_house(cls): + def mark_full_house(cls) -> None: cls.mark_template((3, 2)) @classmethod - def mark_three_of_a_kind(cls): + def mark_three_of_a_kind(cls) -> None: cls.mark_template((3, 1, 1)) @classmethod - def mark_two_pair(cls): + def mark_two_pair(cls) -> None: cls.mark_template((2, 2, 1)) @classmethod - def mark_one_pair(cls): + def mark_one_pair(cls) -> None: for paired_card in range(13)[::-1]: for other_cards in cls.quinary_combinations(cls.NUM_CARDS - 2): if paired_card in other_cards: @@ -136,7 +145,7 @@ def mark_one_pair(cls): cls.TABLE[hash_] = base_rank @classmethod - def mark_high_card(cls): + def mark_high_card(cls) -> None: for base in cls.quinary_combinations(cls.NUM_CARDS): hand = [0] * 13 for i in range(5): @@ -151,7 +160,7 @@ def mark_high_card(cls): cls.TABLE[hash_] = base_rank @classmethod - def mark_straight(cls): + def mark_straight(cls) -> None: hands = [] for lowest in range(9)[::-1]: # From 10 to 2 hand = [0] * 13 diff --git a/python/tests/test_card.py b/python/tests/test_card.py index c166999..c979e82 100644 --- a/python/tests/test_card.py +++ b/python/tests/test_card.py @@ -1,20 +1,23 @@ +from __future__ import annotations + import unittest +from typing import ClassVar from phevaluator import Card class TestCard(unittest.TestCase): - # lowercase name, capitalcase name, value - testcases = [ - ["2c", "2C", 0], - ["2d", "2D", 1], - ["2h", "2H", 2], - ["2s", "2S", 3], - ["Tc", "TC", 32], - ["Ac", "AC", 48], + # lowercase name, capital name, value + testcases: ClassVar[list[tuple[str, str, int]]] = [ + ("2c", "2C", 0), + ("2d", "2D", 1), + ("2h", "2H", 2), + ("2s", "2S", 3), + ("Tc", "TC", 32), + ("Ac", "AC", 48), ] - def test_card_equality(self): + def test_card_equality(self) -> None: for name, capital_name, number in self.testcases: # equality between cards # e.g. Card("2c") == Card(0) @@ -27,7 +30,7 @@ def test_card_equality(self): # equality between Card and int self.assertEqual(Card(number), number) # e.g. Card(0) == 0 - def test_card_immutability(self): + def test_card_immutability(self) -> None: # Once a Card is assigned or constructed from another Card, # it's not affected by any changes to source variable c_source = Card(1) @@ -40,9 +43,9 @@ def test_card_immutability(self): self.assertEqual(c_assign, Card(1)) self.assertEqual(c_construct, Card(1)) - def test_card_describe(self): + def test_card_describe(self) -> None: for name, capital_name, number in self.testcases: - rank, suit = name + rank, suit, *_ = tuple(name) c_name = Card(name) c_capital_name = Card(capital_name) c_number = Card(number) diff --git a/python/tests/test_evalator_omaha.py b/python/tests/test_evalator_omaha.py index 1c936fa..9ab9e77 100644 --- a/python/tests/test_evalator_omaha.py +++ b/python/tests/test_evalator_omaha.py @@ -2,30 +2,31 @@ import unittest from itertools import combinations -from typing import List -from phevaluator import ( - Card, - _evaluate_cards, - _evaluate_omaha_cards, - evaluate_omaha_cards, - sample_cards, -) +from phevaluator import Card +from phevaluator import _evaluate_cards +from phevaluator import _evaluate_omaha_cards +from phevaluator import evaluate_omaha_cards +from phevaluator import sample_cards -def evaluate_omaha_exhaustive(community_cards: List[int], hole_cards: List[int]) -> int: +def evaluate_omaha_exhaustive(community_cards: list[int], hole_cards: list[int]) -> int: """Evaluate omaha cards with `_evaluate_cards`.""" - best_rank = min( + return min( _evaluate_cards(c1, c2, c3, h1, h2) for c1, c2, c3 in combinations(community_cards, 3) for h1, h2 in combinations(hole_cards, 2) ) - return best_rank class TestEvaluatorOmaha(unittest.TestCase): - def test_omaha(self): - """Compare the evaluation between `_evaluate_omaha_cards` and `_evaluate_cards`""" + def test_omaha(self) -> None: + """Test two functions yield the same results. + + Compare: + `_evaluate_omaha_cards` + `_evaluate_cards` + """ total = 10000 for _ in range(total): cards = sample_cards(9) @@ -37,28 +38,28 @@ def test_omaha(self): evaluate_omaha_exhaustive(community_cards, hole_cards), ) - def test_evaluator_interface(self): + def test_evaluator_interface(self) -> None: # int, str and Card can be passed to evaluate_omaha_cards() # fmt: off rank1 = evaluate_omaha_cards( 48, 49, 47, 43, 35, # community cards - 51, 50, 39, 34 # hole cards + 51, 50, 39, 34, # hole cards ) rank2 = evaluate_omaha_cards( "Ac", "Ad", "Ks", "Qs", "Ts", # community cards - "As", "Ah", "Js", "Th" # hole cards + "As", "Ah", "Js", "Th", # hole cards ) rank3 = evaluate_omaha_cards( "AC", "AD", "KS", "QS", "TS", # community cards - "AS", "AH", "JS", "TH" # hole cards + "AS", "AH", "JS", "TH", # hole cards ) rank4 = evaluate_omaha_cards( - Card("Ac"), Card("Ad"), Card("Ks"), Card("Qs"), Card("Ts"), # community cards - Card("As"), Card("Ah"), Card("Js"), Card("Th") # hole cards + Card("Ac"), Card("Ad"), Card("Ks"), Card("Qs"), Card("Ts"), # community cards # noqa: E501 + Card("As"), Card("Ah"), Card("Js"), Card("Th"), # hole cards ) rank5 = evaluate_omaha_cards( 48, "Ad", "KS", Card(43), Card("Ts"), # community cards - Card("AS"), 50, "Js", "TH" # hole cards + Card("AS"), 50, "Js", "TH", # hole cards ) # fmt: on self.assertEqual(rank1, rank2) diff --git a/python/tests/test_evaluator.py b/python/tests/test_evaluator.py index f4c9191..d99e156 100644 --- a/python/tests/test_evaluator.py +++ b/python/tests/test_evaluator.py @@ -1,16 +1,21 @@ import json -import os import unittest +from pathlib import Path -from phevaluator import Card, evaluate_cards, evaluate_omaha_cards +from phevaluator import Card +from phevaluator import evaluate_cards +from phevaluator import evaluate_omaha_cards -CARDS_FILE_5 = os.path.join(os.path.dirname(__file__), "cardfiles/5cards.json") -CARDS_FILE_6 = os.path.join(os.path.dirname(__file__), "cardfiles/6cards.json") -CARDS_FILE_7 = os.path.join(os.path.dirname(__file__), "cardfiles/7cards.json") +BASE_DIR = Path(__file__).parent +CARD_FILES_DIR = BASE_DIR / "cardfiles" + +CARDS_FILE_5 = CARD_FILES_DIR / "5cards.json" +CARDS_FILE_6 = CARD_FILES_DIR / "6cards.json" +CARDS_FILE_7 = CARD_FILES_DIR / "7cards.json" class TestEvaluator(unittest.TestCase): - def test_example(self): + def test_example(self) -> None: rank1 = evaluate_cards("9c", "4c", "4s", "9d", "4h", "Qc", "6c") rank2 = evaluate_cards("9c", "4c", "4s", "9d", "4h", "2c", "9h") @@ -18,7 +23,7 @@ def test_example(self): self.assertEqual(rank2, 236) self.assertLess(rank2, rank1) - def test_omaha_example(self): + def test_omaha_example(self) -> None: # fmt: off rank1 = evaluate_omaha_cards( "4c", "5c", "6c", "7s", "8s", # community cards @@ -34,25 +39,25 @@ def test_omaha_example(self): self.assertEqual(rank1, 1578) self.assertEqual(rank2, 1604) - def test_5cards(self): - with open(CARDS_FILE_5, "r", encoding="UTF-8") as read_file: + def test_5cards(self) -> None: + with CARDS_FILE_5.open(encoding="UTF-8") as read_file: hand_dict = json.load(read_file) for key, value in hand_dict.items(): self.assertEqual(evaluate_cards(*key.split()), value) - def test_6cards(self): - with open(CARDS_FILE_6, "r", encoding="UTF-8") as read_file: + def test_6cards(self) -> None: + with CARDS_FILE_6.open(encoding="UTF-8") as read_file: hand_dict = json.load(read_file) for key, value in hand_dict.items(): self.assertEqual(evaluate_cards(*key.split()), value) - def test_7cards(self): - with open(CARDS_FILE_7, "r", encoding="UTF-8") as read_file: + def test_7cards(self) -> None: + with CARDS_FILE_7.open(encoding="UTF-8") as read_file: hand_dict = json.load(read_file) for key, value in hand_dict.items(): self.assertEqual(evaluate_cards(*key.split()), value) - def test_evaluator_interface(self): + def test_evaluator_interface(self) -> None: # int, str and Card can be passed to evaluate_cards() rank1 = evaluate_cards(1, 2, 3, 32, 48) rank2 = evaluate_cards("2d", "2h", "2s", "Tc", "Ac")