From 57a7e9216ede549fa836f2820502fe1cc9341317 Mon Sep 17 00:00:00 2001 From: jianghaiying Date: Wed, 24 Sep 2025 16:54:52 +0800 Subject: [PATCH] task4 --- .coverage | Bin 0 -> 53248 bytes main.py | 2 +- poker/card.py | 112 +++++++-- poker/hand_evaluator.py | 31 ++- poker/hand_ranking.py | 46 ++-- pyproject.toml | 6 + run_tests.py | 12 +- shortdeck/__init__.py | 10 + shortdeck/hand_evaluator.py | 103 ++++++++ shortdeck/hand_ranking.py | 52 ++++ shortdeck_main.py | 28 +++ task4_readme.md | 60 +++++ tests/shortdeck/__init__.py | 3 + tests/shortdeck/test_card.py | 203 +++++++++++++++ tests/shortdeck/test_hand_evaluator.py | 326 +++++++++++++++++++++++++ tests/shortdeck/test_hand_ranking.py | 177 ++++++++++++++ tests/shortdeck/test_integration.py | 225 +++++++++++++++++ tests/test_card.py | 28 +-- tests/test_coverage_edge_cases.py | 179 ++++++++++++++ tests/test_hand_evaluator.py | 38 +-- tests/test_integration.py | 16 +- uv.lock | 92 ++++++- 22 files changed, 1647 insertions(+), 102 deletions(-) create mode 100644 .coverage create mode 100644 shortdeck/__init__.py create mode 100644 shortdeck/hand_evaluator.py create mode 100644 shortdeck/hand_ranking.py create mode 100644 shortdeck_main.py create mode 100644 task4_readme.md create mode 100644 tests/shortdeck/__init__.py create mode 100644 tests/shortdeck/test_card.py create mode 100644 tests/shortdeck/test_hand_evaluator.py create mode 100644 tests/shortdeck/test_hand_ranking.py create mode 100644 tests/shortdeck/test_integration.py create mode 100644 tests/test_coverage_edge_cases.py diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..2c73f883d07f8bf406ae403315f55844a442dabb GIT binary patch literal 53248 zcmeI4Z)_CD6~K3I?~m_pcjs)NCt@ORBZzUZ&p0$dNDDYXC7QG}ElR^bU-s_y?2CQ3 z_U<0WX(75K4N|JM5|ye_1*rNVRU7_%pg=@|s8m%^L(_zOu-gx*BTG#pxh?7+ivl6^ z&Frpk@9^1|6ftPz z@~SbS^lKySOu(tl()L((0u-XOc z{C|7L@_@|I|nrEiY>jCW-VPycN$xF-^|dL<@Q!|r;D8SsI65jku0!){`wbl>l}YCum_`Wo{XQ-c8lbAc*{|q@fh@hvEws@Fjah(0Pfz0u_y*Ddizh zwGh;A{-A30DL30rlfm%TKrsHmf?9@C6Ovu@7<^`jkD0uGA)}v%NuR!i3X^F{+2n_` zk&fN!0L&(XYR-Tm3v;l&0w`24qk^!S(u)@JUwX;hlde5wd+h?s*DiaIYGdLsyuYrS zW)7BBx@?(i+*GBtPc`W*WVR|PT_4cYg1t6?#%2@h%$W+a;pSCoUAYW%gqG2}yOr7G zE-rMM-wK1y9ba&g&#Q%uvtH3<*e(_3emXd5J@zC5ib^XCN)v`?Rhe#~PA;9X-0~BH zz=W2XS2Ol-r=8*{akx*rsW^Kz+NfHZmCI`i6=B9O#~M~mC2eSs32SLvt1z1z(B4XR z(Ied@H_*3or4~LEU$%@d6`jFo&ofCA7WKU9n(bWrDx9P$v+OThV8VrWJ6=Db>^wN= zrCkG^-0U7q*%VwU=e2xB!2m#{DS-~d9gGp{aPB5I>j5}dGWjAS3w8^ z5kry3ubA!c$mw4(k1yk+f8^ziX$ZnU=|p zNp>Hx0gqbI3vl!G%0_`R>rv~2o4}LsY?FR?$O-7JjtSgw-Fh0m*+*flJq=t{NGTUoSy@fjJb+KHab~TDpHGM%^Nm5c+r}RN=ZW zI$Q6QYp+sU6%n{%&5EnkypX%X0+*{pF$wOFvyw^Rw4ni78Lg>fdr-MbdYxqEdh1;g z61aTLGMGp1yh*7mIGeE^rLz0A1q5!}9QD9cduac6_yta#L)3#|wJ^71S5a^(TIHLq zv#R-ds_furv(Q%bh|}-?o5VOrmXm;dNPb&hBK=J|McyU{Nm71HGNk1aC;mo!PW&1O zVL$>%00|%gB!C2v01`j~NZ@uPuu>SRYgb*l|8H6)n02XD?Mxf*7l!NC^KKDi>-;6Y zF4dX$|D;n$*Qajp|Kx6eDKM8p)sC)`k@TRy6#AGYRN)e5?VWP%Rcg`ILa}zmRcco~|KIQhAz!-;&sN@RY-mV(&YiCvS^Hk~U5Y%wZPbh3fk2pU6?{=KNu}w$-2_OL^fCP{L z51_{V?*kN^@u0!RP}AOR$R1dsp{Kmter3EYMR z1VIY3`~T%$j$9%CByW+|$cy9v`8IioY=CD0e4YsMxcqzhCHaT)2nb<70!RP}AOR$R z1dsp{Kmter2_OL^Fgt-*lb;XY)iiPPQU2ZSfn6`1Tr|HCQtoM-I6C>{=!wa*$0ygo zLAWCZsVid>+je-vh0$Lf8|N1h$Xi1uPTsrhC|ImFTV21>0`+g$E3R?Nd2-j(f*YKi#Px4=ZibDiR_P-yzJjIa`5DT*B#mP zR(yr9NTlV86Gv}Mo!a-t)#9$P;;w^mT=M48cb*W$DCFJ|UAzCr_1!;cZi>Lw-I0Ay zCiSz2&+Q!_+k0aw7=|=CoJcf4+|w|z?Z{tyubYRa%rQiMKqf6)a zo%vEr0Mgb6{&?mFB))Ow(wWQey_~;xZutHDzrWMJ`ux}fAwLMj{1d(wfl3Og(WkDD zUEMy}doH_c$M8GG*y!oevC-|F<6_VU0=T?~!rxCwLa% z>*TlORdR|PC%+&+gJ%I8Am0Na3`hV8AOR$R1dsp{Kmter2_OL^fCP}hCni8Q{CpGJ z_w$X7>?#&x5g{y+Wfn;ii$sw{(I|@|5f+8RENW Union[Rank, 'ShortDeckRank']: + return self.rank + + def get_suit(self) -> Suit: + return self.suit + + def is_short_deck(self) -> bool: + return isinstance(self.rank, ShortDeckRank) def __str__(self): return f"{self.rank}{self.suit}" def __eq__(self, other): if isinstance(other, Card): - return self.rank == other.rank and self.suit == other.suit + self_value = getattr(self.rank, 'numeric_value', self.rank.value) + other_value = getattr(other.rank, 'numeric_value', other.rank.value) + return self_value == other_value and self.suit == other.suit return NotImplemented def __lt__(self, other): if isinstance(other, Card): - if self.rank != other.rank: - return self.rank < other.rank + self_value = getattr(self.rank, 'numeric_value', self.rank.value) + other_value = getattr(other.rank, 'numeric_value', other.rank.value) + + if self_value != other_value: + return self_value < other_value return self.suit.value < other.suit.value return NotImplemented - + + @staticmethod + def _find_rank(rank_char, is_short_deck): + if is_short_deck: + # 在ShortDeckRank中查找 + for r in ShortDeckRank: + if r.symbol == rank_char: + return r + else: + # 在Rank中查找 + for r in Rank: + if r.symbol == rank_char: + return r + return None + @classmethod - def createCard(cls, card_str) -> 'Card': + def create_card(cls, card_str, is_short_deck=False) -> 'Card': + """创建卡片,支持标准扑克和短牌扑克""" if len(card_str) != 2: raise ValueError(f"Invalid card string: {card_str}") @@ -81,14 +153,10 @@ class Card: suit_char = card_str[1].lower() # 查找rank - rank = None - for r in Rank: - if r.symbol == rank_char: - rank = r - break - + rank = cls._find_rank(rank_char, is_short_deck) if rank is None: - raise ValueError(f"Invalid rank: {rank_char}") + rank_type = "short deck" if is_short_deck else "standard" + raise ValueError(f"Invalid rank for {rank_type}: {rank_char}") # 查找suit suit = None @@ -103,9 +171,10 @@ class Card: return cls(rank, suit) @classmethod - def parseCards(cls, cards_str) -> List['Card']: + def parse_cards(cls, cards_str, is_short_deck=False) -> List['Card']: """ - 从字符串拆解多张牌, "AsKs AhAdAc6s7s" + 从字符串拆解多张牌,支持标准扑克和短牌扑克 + 例如: "AsKs AhAdAc6s7s" """ cards_str = cards_str.strip() if not cards_str: @@ -125,4 +194,13 @@ class Card: i += 2 else: raise ValueError(f"Invalid card format at position {i}") - return [cls.createCard(card_str) for card_str in card_strings] \ No newline at end of file + + return [cls.create_card(card_str, is_short_deck) for card_str in card_strings] + @classmethod + def parseLongCards(cls, cards_str) -> List['Card']: + return cls.parse_cards(cards_str, is_short_deck=False) + + @classmethod + def parse_short_deck_cards(cls, cards_str) -> List['Card']: + return cls.parse_cards(cards_str, is_short_deck=True) + \ No newline at end of file diff --git a/poker/hand_evaluator.py b/poker/hand_evaluator.py index de9491e..861183c 100644 --- a/poker/hand_evaluator.py +++ b/poker/hand_evaluator.py @@ -7,7 +7,7 @@ from .hand_ranking import HandRanking, HandType class HandEvaluator: @staticmethod - def evaluateHand(cards) -> HandRanking: + def evaluate_hand(cards) -> HandRanking: """ 从7张牌中找出最好的5张牌组合 """ @@ -19,7 +19,7 @@ class HandEvaluator: # 所有可能的5张牌组合 for five_cards in combinations(cards, 5): - ranking = HandEvaluator.evaluate5Cards(list(five_cards)) + ranking = HandEvaluator.evaluate_5_cards(list(five_cards)) if best_ranking is None or ranking > best_ranking: best_ranking = ranking @@ -28,11 +28,8 @@ class HandEvaluator: best_ranking.cards = best_cards return best_ranking - @staticmethod - def evaluate5Cards(cards) -> HandRanking: - if len(cards) != 5: - raise ValueError(f"Expected 5 cards, got {len(cards)}") - + @classmethod + def _analyze_cards(cls, cards: List[Card]) -> tuple: # 按点数排序(降序) sorted_cards = sorted(cards, key=lambda c: c.rank.numeric_value, reverse=True) ranks = [card.rank for card in sorted_cards] @@ -45,8 +42,16 @@ class HandEvaluator: # 同花 is_flush = len(set(suits)) == 1 - # 顺子 - is_straight, straight_high = HandEvaluator._isStraight(ranks) + is_straight, straight_high = cls._is_straight(ranks) + + return sorted_cards, ranks, suits, rank_counts, count_values, is_flush, is_straight, straight_high + + @staticmethod + def evaluate_5_cards(cards) -> HandRanking: + if len(cards) != 5: + raise ValueError(f"Expected 5 cards, got {len(cards)}") + + sorted_cards, ranks, suits, rank_counts, count_values, is_flush, is_straight, straight_high = HandEvaluator._analyze_cards(cards) # 根据牌型返回相应的HandRanking if is_straight and is_flush: @@ -90,7 +95,7 @@ class HandEvaluator: return HandRanking(HandType.HIGH_CARD, ranks, sorted_cards) @staticmethod - def _isStraight(ranks) -> Tuple[bool, Rank]: + def _is_straight(ranks) -> Tuple[bool, Rank]: # 排序点数值 values = sorted([rank.numeric_value for rank in ranks], reverse=True) @@ -117,6 +122,6 @@ class HandEvaluator: return False, None @staticmethod - def evaluateFromInput(cards_str) -> HandRanking: - cards = Card.parseCards(cards_str) - return HandEvaluator.evaluateHand(cards) \ No newline at end of file + def evaluate_from_input(cards_str) -> HandRanking: + cards = Card.parse_cards(cards_str) + return HandEvaluator.evaluate_hand(cards) \ No newline at end of file diff --git a/poker/hand_ranking.py b/poker/hand_ranking.py index 6e50ae8..013d784 100644 --- a/poker/hand_ranking.py +++ b/poker/hand_ranking.py @@ -1,7 +1,9 @@ from enum import Enum from functools import total_ordering -from typing import List, Tuple -from .card import Card, Rank +from typing import List, Tuple, Union, TypeVar, Any +from .card import Card, Rank, ShortDeckRank + +HandTypeVar = TypeVar('HandTypeVar', bound=Enum) @total_ordering @@ -42,32 +44,36 @@ class HandType(Enum): class HandRanking: - def __init__(self, hand_type: HandType, key_ranks: List[Rank], cards: List[Card]): + """通用手牌排名类,支持标准扑克和短牌扑克的手牌类型""" + + def __init__(self, hand_type: Any, key_ranks: List[Union[Rank, ShortDeckRank]], cards: List[Card]): self.hand_type = hand_type self.key_ranks = key_ranks # 用于比较的关键点数 self.cards = cards # 组成这个ranking的5张牌 def __str__(self): - if self.hand_type == HandType.FOUR_OF_A_KIND: + hand_type_name = self.hand_type.name if hasattr(self.hand_type, 'name') else str(self.hand_type) + + if hand_type_name == "FOUR_OF_A_KIND": return f"Quad({self.key_ranks[0].symbol})" - elif self.hand_type == HandType.FULL_HOUSE: + elif hand_type_name == "FULL_HOUSE": return f"Full House({self.key_ranks[0].symbol} over {self.key_ranks[1].symbol})" - elif self.hand_type == HandType.FLUSH: + elif hand_type_name == "FLUSH": return f"Flush({self.key_ranks[0].symbol} high)" - elif self.hand_type == HandType.STRAIGHT: + elif hand_type_name == "STRAIGHT": return f"Straight({self.key_ranks[0].symbol} high)" - elif self.hand_type == HandType.STRAIGHT_FLUSH: - if self.key_ranks[0] == Rank.ACE: + elif hand_type_name == "STRAIGHT_FLUSH": + if hasattr(self.key_ranks[0], 'symbol') and self.key_ranks[0].symbol == 'A': return "Royal Flush" else: return f"Straight Flush({self.key_ranks[0].symbol} high)" - elif self.hand_type == HandType.ROYAL_FLUSH: + elif hand_type_name == "ROYAL_FLUSH": return "Royal Flush" - elif self.hand_type == HandType.THREE_OF_A_KIND: + elif hand_type_name == "THREE_OF_A_KIND": return f"Three of a Kind({self.key_ranks[0].symbol})" - elif self.hand_type == HandType.TWO_PAIR: + elif hand_type_name == "TWO_PAIR": return f"Two Pair({self.key_ranks[0].symbol} and {self.key_ranks[1].symbol})" - elif self.hand_type == HandType.ONE_PAIR: + elif hand_type_name == "ONE_PAIR": return f"Pair({self.key_ranks[0].symbol})" else: # HIGH_CARD return f"High Card({self.key_ranks[0].symbol})" @@ -75,14 +81,20 @@ class HandRanking: def __eq__(self, other): if not isinstance(other, HandRanking): return False - return (self.hand_type == other.hand_type and self.key_ranks == other.key_ranks) + self_strength = getattr(self.hand_type, 'strength', self.hand_type.value[0] if hasattr(self.hand_type.value, '__getitem__') else self.hand_type.value) + other_strength = getattr(other.hand_type, 'strength', other.hand_type.value[0] if hasattr(other.hand_type.value, '__getitem__') else other.hand_type.value) + return (self_strength == other_strength and self.key_ranks == other.key_ranks) def __lt__(self, other): if not isinstance(other, HandRanking): return NotImplemented - if self.hand_type != other.hand_type: - return self.hand_type < other.hand_type - # 手牌类型相同比较点数 + + self_strength = getattr(self.hand_type, 'strength', self.hand_type.value[0] if hasattr(self.hand_type.value, '__getitem__') else self.hand_type.value) + other_strength = getattr(other.hand_type, 'strength', other.hand_type.value[0] if hasattr(other.hand_type.value, '__getitem__') else other.hand_type.value) + + if self_strength != other_strength: + return self_strength < other_strength + for self_rank, other_rank in zip(self.key_ranks, other.key_ranks): if self_rank != other_rank: return self_rank < other_rank diff --git a/pyproject.toml b/pyproject.toml index ed7c748..6cb15bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,3 +11,9 @@ dependencies = [ [tool.pytest.ini_options] testpaths = ["tests"] python_files = ["test_*.py"] + +[dependency-groups] +dev = [ + "coverage>=7.10.7", + "pytest-cov>=7.0.0", +] diff --git a/run_tests.py b/run_tests.py index 8a07862..c94def6 100644 --- a/run_tests.py +++ b/run_tests.py @@ -19,7 +19,7 @@ def test_basic_card_functionality(): assert str(card) == "As", f"Expected 'As', got '{str(card)}'" # Test card parsing - parsed_card = Card.createCard("Kh") + parsed_card = Card.create_card("Kh") assert parsed_card.rank == Rank.KING assert parsed_card.suit == Suit.HEARTS @@ -35,7 +35,7 @@ def test_hand_parsing(): """Test hand parsing from string""" print("Testing hand parsing...") - cards = Card.parseCards("AsKs AhAdAc6s7s") + cards = Card.parse_cards("AsKs AhAdAc6s7s") assert len(cards) == 7, f"Expected 7 cards, got {len(cards)}" assert str(cards[0]) == "As" assert str(cards[6]) == "7s" @@ -61,7 +61,7 @@ def test_hand_evaluation_examples(): ] for cards_str, expected_str, expected_type in test_cases: - ranking = HandEvaluator.evaluateFromInput(cards_str) + ranking = HandEvaluator.evaluate_from_input(cards_str) actual_str = str(ranking) assert ranking.hand_type == expected_type, f"Wrong hand type for {cards_str}: expected {expected_type}, got {ranking.hand_type}" @@ -75,7 +75,7 @@ def test_wheel_straight(): print("Testing wheel straight...") cards_str = "As2h3d4c5h7s8s" - ranking = HandEvaluator.evaluateFromInput(cards_str) + ranking = HandEvaluator.evaluate_from_input(cards_str) assert ranking.hand_type == HandType.STRAIGHT assert ranking.key_ranks[0] == Rank.FIVE, "In wheel straight, 5 should be the high card" @@ -90,14 +90,14 @@ def test_error_handling(): # Test invalid card string try: - Card.createCard("Xx") + Card.create_card("Xx") assert False, "Should have raised ValueError for invalid card" except ValueError: pass # Expected # Test wrong number of cards try: - HandEvaluator.evaluateFromInput("AsKh") + HandEvaluator.evaluate_from_input("AsKh") assert False, "Should have raised ValueError for wrong number of cards" except ValueError: pass # Expected diff --git a/shortdeck/__init__.py b/shortdeck/__init__.py new file mode 100644 index 0000000..0715453 --- /dev/null +++ b/shortdeck/__init__.py @@ -0,0 +1,10 @@ +from poker.card import ShortDeckRank +from .hand_evaluator import ShortDeckHandEvaluator +from .hand_ranking import ShortDeckHandType, ShortDeckHandRanking + +__all__ = [ + 'ShortDeckRank', + 'ShortDeckHandEvaluator', + 'ShortDeckHandType', + 'ShortDeckHandRanking' +] \ No newline at end of file diff --git a/shortdeck/hand_evaluator.py b/shortdeck/hand_evaluator.py new file mode 100644 index 0000000..537c937 --- /dev/null +++ b/shortdeck/hand_evaluator.py @@ -0,0 +1,103 @@ +from collections import Counter +from typing import List, Optional +from poker.hand_evaluator import HandEvaluator +from poker.card import Suit, Card, ShortDeckRank +from .hand_ranking import ShortDeckHandType, ShortDeckHandRanking +from itertools import combinations + + +class ShortDeckHandEvaluator(HandEvaluator): + + @classmethod + def evaluate_hand(cls, cards: List[Card]) -> ShortDeckHandRanking: + if len(cards) < 5: + raise ValueError("Need at least 5 cards to evaluate a hand") + + best_ranking = None + for combo in combinations(cards, 5): + ranking = cls.evaluate_5_cards(list(combo)) + if best_ranking is None or ranking > best_ranking: + best_ranking = ranking + + return best_ranking + + @classmethod + def evaluate_5_cards(cls, cards: List[Card]) -> ShortDeckHandRanking: + if len(cards) != 5: + raise ValueError("Must provide exactly 5 cards") + + from poker.hand_evaluator import HandEvaluator + sorted_cards, ranks, suits, rank_counts, count_values, is_flush, _, _ = HandEvaluator._analyze_cards(cards) + + # 重写顺子判断 + is_straight, straight_high = cls.is_straight(ranks) + + if is_straight and is_flush: + if straight_high == ShortDeckRank.ACE: + return ShortDeckHandRanking(ShortDeckHandType.ROYAL_FLUSH, [straight_high], sorted_cards) + else: + return ShortDeckHandRanking(ShortDeckHandType.STRAIGHT_FLUSH, [straight_high], sorted_cards) + + if count_values == [4, 1]: # 四条 + quad_rank = [rank for rank, count in rank_counts.items() if count == 4][0] + kicker = [rank for rank, count in rank_counts.items() if count == 1][0] + return ShortDeckHandRanking(ShortDeckHandType.FOUR_OF_A_KIND, [quad_rank, kicker], sorted_cards) + + # 关键差异:Flush > Full House + if is_flush: + return ShortDeckHandRanking(ShortDeckHandType.FLUSH, ranks, sorted_cards) + + if count_values == [3, 2]: # 满堂红 + trip_rank = [rank for rank, count in rank_counts.items() if count == 3][0] + pair_rank = [rank for rank, count in rank_counts.items() if count == 2][0] + return ShortDeckHandRanking(ShortDeckHandType.FULL_HOUSE, [trip_rank, pair_rank], sorted_cards) + + if is_straight: + return ShortDeckHandRanking(ShortDeckHandType.STRAIGHT, [straight_high], sorted_cards) + + if count_values == [3, 1, 1]: # 三条 + trip_rank = [rank for rank, count in rank_counts.items() if count == 3][0] + kickers = sorted([rank for rank, count in rank_counts.items() if count == 1], + key=lambda x: x.numeric_value, reverse=True) + return ShortDeckHandRanking(ShortDeckHandType.THREE_OF_A_KIND, [trip_rank] + kickers, sorted_cards) + + if count_values == [2, 2, 1]: # 两对 + pairs = sorted([rank for rank, count in rank_counts.items() if count == 2], + key=lambda x: x.numeric_value, reverse=True) + kicker = [rank for rank, count in rank_counts.items() if count == 1][0] + return ShortDeckHandRanking(ShortDeckHandType.TWO_PAIR, pairs + [kicker], sorted_cards) + + if count_values == [2, 1, 1, 1]: # 一对 + pair_rank = [rank for rank, count in rank_counts.items() if count == 2][0] + kickers = sorted([rank for rank, count in rank_counts.items() if count == 1], + key=lambda x: x.numeric_value, reverse=True) + return ShortDeckHandRanking(ShortDeckHandType.ONE_PAIR, [pair_rank] + kickers, sorted_cards) + + return ShortDeckHandRanking(ShortDeckHandType.HIGH_CARD, ranks, sorted_cards) + + @classmethod + def is_straight(cls, ranks: List[ShortDeckRank]) -> tuple[bool, Optional[ShortDeckRank]]: + unique_ranks = sorted(set(rank.numeric_value for rank in ranks)) + + if len(unique_ranks) < 5: + return False, None + ace_low_straight = [6, 7, 8, 9, 14] # A(14), 6, 7, 8, 9 + if all(val in unique_ranks for val in ace_low_straight): + return True, ShortDeckRank.NINE + + for i in range(len(unique_ranks) - 4): + if unique_ranks[i+4] - unique_ranks[i] == 4: + high_value = unique_ranks[i+4] + high_rank = None + for rank in ShortDeckRank: + if rank.numeric_value == high_value: + high_rank = rank + break + return True, high_rank + + return False, None + + @classmethod + def evaluate_from_input(cls, input_str) -> ShortDeckHandRanking: + cards = Card.parse_short_deck_cards(input_str) + return cls.evaluate_hand(cards) \ No newline at end of file diff --git a/shortdeck/hand_ranking.py b/shortdeck/hand_ranking.py new file mode 100644 index 0000000..da2cee2 --- /dev/null +++ b/shortdeck/hand_ranking.py @@ -0,0 +1,52 @@ +from functools import total_ordering +from enum import Enum +from typing import List +from poker.hand_ranking import HandType, HandRanking +from poker.card import ShortDeckRank, Card + + +@total_ordering +class ShortDeckHandType(Enum): + """ + 标准牌型扑克与短牌型扑克的区别: + - 同花(Flush) > 满堂红(Full House) + - 其他牌型等级不变 + """ + HIGH_CARD = (1, "High Card") + ONE_PAIR = (2, "One Pair") + TWO_PAIR = (3, "Two Pair") + THREE_OF_A_KIND = (4, "Three of a Kind") + STRAIGHT = (5, "Straight") + FULL_HOUSE = (6, "Full House") + FLUSH = (7, "Flush") + FOUR_OF_A_KIND = (8, "Four of a Kind") + STRAIGHT_FLUSH = (9, "Straight Flush") + ROYAL_FLUSH = (10, "Royal Flush") + + def __new__(cls, strength, type_name): + obj = object.__new__(cls) + obj._value_ = strength + obj.strength = strength + obj.type_name = type_name + return obj + + def __str__(self): + return self.type_name + + def __eq__(self, other): + if isinstance(other, ShortDeckHandType): + return self.strength == other.strength + return NotImplemented + + def __lt__(self, other): + if isinstance(other, ShortDeckHandType): + return self.strength < other.strength + return NotImplemented + + +class ShortDeckHandRanking(HandRanking): + def __init__(self, hand_type: ShortDeckHandType, key_ranks: List[ShortDeckRank], cards: List[Card]): + super().__init__(hand_type, key_ranks, cards) + + def __str__(self): + return super().__str__() \ No newline at end of file diff --git a/shortdeck_main.py b/shortdeck_main.py new file mode 100644 index 0000000..0106724 --- /dev/null +++ b/shortdeck_main.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +""" +Short Deck Poker Hand Evaluation Script + +Usage: python shortdeck_main.py "6sKs AhAdAc6s7s" +""" + +import sys +from shortdeck import ShortDeckHandEvaluator +from poker.hand_ranking import HandType + + +def main(): + if len(sys.argv) != 2: + print("输入: python shortdeck_main.py \"AsKs QhQdQc6s7s\"") + sys.exit(1) + + try: + hand_input = sys.argv[1] + result = ShortDeckHandEvaluator.evaluate_from_input(hand_input) + print(str(result)) + except Exception as e: + print(f"Error: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/task4_readme.md b/task4_readme.md new file mode 100644 index 0000000..1dd1597 --- /dev/null +++ b/task4_readme.md @@ -0,0 +1,60 @@ +# Task 4 + +## 项目概述 + +从Task1标准扑克扩展到短牌扑克 + +## 支持的手牌类型 + +1. **皇家同花顺** (Royal Flush) - A♠K♠Q♠J♠T♠ +2. **同花顺** (Straight Flush) - 连续的五张同花色牌 +3. **四条** (Four of a Kind) - 四张相同点数的牌 +4. **同花** (Flush) - 五张同花色的牌 +5. **满堂红** (Full House) - 三条加一对 +6. **顺子** (Straight) - 五张连续点数的牌(包括A-2-3-4-5轮子) +7. **三条** (Three of a Kind) - 三张相同点数的牌 +8. **两对** (Two Pair) - 两个对子 +9. **一对** (One Pair) - 一个对子 +10. **高牌** (High Card) - 没有组合的单张高牌 + +### 环境要求 + +**牌组特性**: +- 移除2、3、4、5,保留6-A(36张牌) +- 支持A-6-7-8-9特殊顺子 + +**手牌排名调整**: +``` +短牌vs标准扑克的关键差异: +- Flush (同花) > Full House (满堂红) +- 其他排名保持一致 +``` + +**可能的顺子组合**: +1. A-6-7-8-9 +2. 6-7-8-9-T +3. 7-8-9-T-J +4. 8-9-T-J-Q +5. 9-T-J-Q-K +6. T-J-Q-K-A + +### 测试 + +**单元测试**: +- Card类:创建、解析、验证、比较 +- HandEvaluator:各种手牌类型识别 +- HandRanking:排名比较和字符串表示 + +**关键验证用例**: +```bash +# 标准扑克 - Royal Flush +python main.py "AsKs QsJsTsAdAc" +# 输出: Royal Flush + +# 短牌扑克 - 关键规则验证 +python shortdeck_main.py "AsKs QsJs9s" # Flush +python shortdeck_main.py "AsAh AcKsKh" # Full House + +# 短牌扑克 +python shortdeck_main.py "As9h 8d7c6s9dKs" +``` diff --git a/tests/shortdeck/__init__.py b/tests/shortdeck/__init__.py new file mode 100644 index 0000000..e9747ae --- /dev/null +++ b/tests/shortdeck/__init__.py @@ -0,0 +1,3 @@ +""" +Test module for Short Deck Poker implementation +""" \ No newline at end of file diff --git a/tests/shortdeck/test_card.py b/tests/shortdeck/test_card.py new file mode 100644 index 0000000..a473f29 --- /dev/null +++ b/tests/shortdeck/test_card.py @@ -0,0 +1,203 @@ +""" +Tests for Short Deck Card implementation +""" + +import pytest +from poker.card import Card, ShortDeckRank, Suit # 使用统一的Card类 + + +class TestShortDeckRank: + """Test cases for ShortDeckRank enum""" + + def test_rank_values(self): + """Test that short deck ranks have correct values""" + assert ShortDeckRank.SIX.numeric_value == 6 + assert ShortDeckRank.SEVEN.numeric_value == 7 + assert ShortDeckRank.EIGHT.numeric_value == 8 + assert ShortDeckRank.NINE.numeric_value == 9 + assert ShortDeckRank.TEN.numeric_value == 10 + assert ShortDeckRank.JACK.numeric_value == 11 + assert ShortDeckRank.QUEEN.numeric_value == 12 + assert ShortDeckRank.KING.numeric_value == 13 + assert ShortDeckRank.ACE.numeric_value == 14 + + def test_rank_symbols(self): + """Test that short deck ranks have correct symbols""" + assert ShortDeckRank.SIX.symbol == '6' + assert ShortDeckRank.SEVEN.symbol == '7' + assert ShortDeckRank.EIGHT.symbol == '8' + assert ShortDeckRank.NINE.symbol == '9' + assert ShortDeckRank.TEN.symbol == 'T' + assert ShortDeckRank.JACK.symbol == 'J' + assert ShortDeckRank.QUEEN.symbol == 'Q' + assert ShortDeckRank.KING.symbol == 'K' + assert ShortDeckRank.ACE.symbol == 'A' + + def test_rank_string_representation(self): + """Test string representation of ranks""" + assert str(ShortDeckRank.SIX) == '6' + assert str(ShortDeckRank.ACE) == 'A' + + def test_rank_comparison(self): + """Test rank comparison""" + assert ShortDeckRank.SIX < ShortDeckRank.SEVEN + assert ShortDeckRank.KING < ShortDeckRank.ACE + assert ShortDeckRank.ACE > ShortDeckRank.KING + assert ShortDeckRank.SIX <= ShortDeckRank.SIX + assert ShortDeckRank.ACE >= ShortDeckRank.ACE + + def test_rank_equality(self): + """Test rank equality""" + assert ShortDeckRank.SIX == ShortDeckRank.SIX + assert ShortDeckRank.ACE != ShortDeckRank.KING + + def test_rank_hash(self): + """Test rank hash functionality""" + rank_set = {ShortDeckRank.SIX, ShortDeckRank.SIX, ShortDeckRank.SEVEN} + assert len(rank_set) == 2 # No duplicate SIX + + def test_no_low_ranks(self): + """Test that ranks 2,3,4,5 are not present in short deck""" + rank_values = [rank.numeric_value for rank in ShortDeckRank] + assert 2 not in rank_values + assert 3 not in rank_values + assert 4 not in rank_values + assert 5 not in rank_values + assert len(list(ShortDeckRank)) == 9 # Only 9 ranks + + +class TestSuit: + """Test cases for Suit enum (same as regular poker)""" + + def test_suit_values(self): + """Test suit values""" + assert Suit.SPADES.value == 's' + assert Suit.HEARTS.value == 'h' + assert Suit.DIAMONDS.value == 'd' + assert Suit.CLUBS.value == 'c' + + def test_suit_string_representation(self): + """Test string representation of suits""" + assert str(Suit.SPADES) == 's' + assert str(Suit.HEARTS) == 'h' + + +class TestCard: + """Test cases for Card class""" + + def test_card_creation(self): + """Test card creation""" + card = Card(ShortDeckRank.ACE, Suit.SPADES) + assert card.rank == ShortDeckRank.ACE + assert card.suit == Suit.SPADES + + def test_card_string_representation(self): + """Test card string representation""" + card = Card(ShortDeckRank.ACE, Suit.SPADES) + assert str(card) == "As" + + card2 = Card(ShortDeckRank.KING, Suit.HEARTS) + assert str(card2) == "Kh" + + def test_card_comparison(self): + """Test card comparison""" + ace_spades = Card(ShortDeckRank.ACE, Suit.SPADES) + king_spades = Card(ShortDeckRank.KING, Suit.SPADES) + ace_hearts = Card(ShortDeckRank.ACE, Suit.HEARTS) + + assert king_spades < ace_spades + assert ace_hearts < ace_spades # Same rank, suit comparison + assert ace_spades > king_spades + + def test_card_equality(self): + """Test card equality""" + card1 = Card(ShortDeckRank.ACE, Suit.SPADES) + card2 = Card(ShortDeckRank.ACE, Suit.SPADES) + card3 = Card(ShortDeckRank.KING, Suit.SPADES) + + assert card1 == card2 + assert card1 != card3 + + def test_create_card_valid(self): + """Test creating cards from valid strings""" + # 使用短牌模式创建卡片 + card = Card.create_card("As", is_short_deck=True) + assert card.rank == ShortDeckRank.ACE + assert card.suit == Suit.SPADES + + card2 = Card.create_card("6h", is_short_deck=True) + assert card2.rank == ShortDeckRank.SIX + assert card2.suit == Suit.HEARTS + + def test_create_card_invalid_length(self): + """Test creating card with invalid string length""" + with pytest.raises(ValueError, match="Invalid card string"): + Card.create_card("A") + + with pytest.raises(ValueError, match="Invalid card string"): + Card.create_card("Ash") + + def test_create_card_invalid_rank(self): + """Test creating card with invalid rank (should reject 2,3,4,5)""" + with pytest.raises(ValueError, match="Invalid rank"): + Card.create_card("2s", is_short_deck=True) + + with pytest.raises(ValueError, match="Invalid rank"): + Card.create_card("5h", is_short_deck=True) + + with pytest.raises(ValueError, match="Invalid rank"): + Card.create_card("Xs", is_short_deck=True) + + def test_create_card_invalid_suit(self): + """Test creating card with invalid suit""" + with pytest.raises(ValueError, match="Invalid suit"): + Card.create_card("Ax") + + def test_parse_cards_valid(self): + """Test parsing multiple valid cards""" + cards = Card.parse_cards("AsKs AhAdAc6s7s") + assert len(cards) == 7 + assert str(cards[0]) == "As" + assert str(cards[1]) == "Ks" + assert str(cards[6]) == "7s" + + def test_parse_cards_empty(self): + """Test parsing empty string""" + cards = Card.parse_cards("") + assert len(cards) == 0 + + cards = Card.parse_cards(" ") + assert len(cards) == 0 + + def test_parse_cards_invalid_format(self): + """Test parsing cards with invalid format""" + with pytest.raises(ValueError, match="Invalid card format"): + Card.parse_cards("AsK") # Odd length + + def test_parse_cards_with_invalid_cards(self): + """Test parsing string containing invalid cards""" + with pytest.raises(ValueError, match="Invalid rank"): + Card.parse_cards("As 2s Kh", is_short_deck=True) # Contains '2s' + + def test_get_all_cards(self): + """Test getting all 36 cards in short deck""" + all_cards = [] + for rank in ShortDeckRank: + for suit in Suit: + all_cards.append(Card(rank, suit)) + + assert len(all_cards) == 36 # 9 ranks * 4 suits + + # Check that all combinations are present + ranks_found = set() + suits_found = set() + for card in all_cards: + ranks_found.add(card.rank) + suits_found.add(card.suit) + + assert len(ranks_found) == 9 # All 9 short deck ranks + assert len(suits_found) == 4 # All 4 suits + + # Verify no low cards (2,3,4,5) are present + for card in all_cards: + assert card.rank.numeric_value >= 6 \ No newline at end of file diff --git a/tests/shortdeck/test_hand_evaluator.py b/tests/shortdeck/test_hand_evaluator.py new file mode 100644 index 0000000..70fd306 --- /dev/null +++ b/tests/shortdeck/test_hand_evaluator.py @@ -0,0 +1,326 @@ +""" +Tests for Short Deck Hand Evaluator +""" + +import pytest +from poker.card import Card, ShortDeckRank, Suit # 使用统一的Card类 +from shortdeck.hand_evaluator import ShortDeckHandEvaluator +from shortdeck.hand_ranking import ShortDeckHandType, ShortDeckHandRanking + + +class TestShortDeckHandEvaluator: + """Test cases for ShortDeckHandEvaluator class""" + + def test_evaluate_hand_insufficient_cards(self): + """Test evaluation with less than 5 cards""" + cards = [ + Card(ShortDeckRank.ACE, Suit.SPADES), + Card(ShortDeckRank.KING, Suit.HEARTS), + Card(ShortDeckRank.QUEEN, Suit.DIAMONDS), + Card(ShortDeckRank.JACK, Suit.CLUBS) + ] + + with pytest.raises(ValueError, match="Need at least 5 cards"): + ShortDeckHandEvaluator.evaluate_hand(cards) + + def test_evaluate_hand_exactly_five_cards(self): + """Test evaluation with exactly 5 cards""" + # Royal flush + cards = [ + Card(ShortDeckRank.ACE, Suit.SPADES), + Card(ShortDeckRank.KING, Suit.SPADES), + Card(ShortDeckRank.QUEEN, Suit.SPADES), + Card(ShortDeckRank.JACK, Suit.SPADES), + Card(ShortDeckRank.TEN, Suit.SPADES) + ] + + result = ShortDeckHandEvaluator.evaluate_hand(cards) + assert result.hand_type == ShortDeckHandType.ROYAL_FLUSH + + def test_evaluate_hand_seven_cards(self): + """Test evaluation with 7 cards - should find best 5""" + cards = [ + Card(ShortDeckRank.ACE, Suit.SPADES), + Card(ShortDeckRank.ACE, Suit.HEARTS), + Card(ShortDeckRank.ACE, Suit.DIAMONDS), + Card(ShortDeckRank.ACE, Suit.CLUBS), # Four Aces + Card(ShortDeckRank.KING, Suit.SPADES), + Card(ShortDeckRank.SIX, Suit.HEARTS), + Card(ShortDeckRank.SEVEN, Suit.DIAMONDS) + ] + + result = ShortDeckHandEvaluator.evaluate_hand(cards) + assert result.hand_type == ShortDeckHandType.FOUR_OF_A_KIND + assert result.key_ranks[0] == ShortDeckRank.ACE + + def test_evaluate_five_cards_wrong_count(self): + """Test evaluate5Cards with wrong number of cards""" + cards = [ + Card(ShortDeckRank.ACE, Suit.SPADES), + Card(ShortDeckRank.KING, Suit.HEARTS) + ] + + with pytest.raises(ValueError, match="Must provide exactly 5 cards"): + ShortDeckHandEvaluator.evaluate_5_cards(cards) + + def test_royal_flush(self): + """Test royal flush detection""" + cards = [ + Card(ShortDeckRank.ACE, Suit.SPADES), + Card(ShortDeckRank.KING, Suit.SPADES), + Card(ShortDeckRank.QUEEN, Suit.SPADES), + Card(ShortDeckRank.JACK, Suit.SPADES), + Card(ShortDeckRank.TEN, Suit.SPADES) + ] + + result = ShortDeckHandEvaluator.evaluate_5_cards(cards) + assert result.hand_type == ShortDeckHandType.ROYAL_FLUSH + assert result.key_ranks[0] == ShortDeckRank.ACE + + def test_straight_flush(self): + """Test straight flush detection""" + cards = [ + Card(ShortDeckRank.KING, Suit.HEARTS), + Card(ShortDeckRank.QUEEN, Suit.HEARTS), + Card(ShortDeckRank.JACK, Suit.HEARTS), + Card(ShortDeckRank.TEN, Suit.HEARTS), + Card(ShortDeckRank.NINE, Suit.HEARTS) + ] + + result = ShortDeckHandEvaluator.evaluate_5_cards(cards) + assert result.hand_type == ShortDeckHandType.STRAIGHT_FLUSH + assert result.key_ranks[0] == ShortDeckRank.KING + + def test_four_of_a_kind(self): + """Test four of a kind detection""" + cards = [ + Card(ShortDeckRank.ACE, Suit.SPADES), + Card(ShortDeckRank.ACE, Suit.HEARTS), + Card(ShortDeckRank.ACE, Suit.DIAMONDS), + Card(ShortDeckRank.ACE, Suit.CLUBS), + Card(ShortDeckRank.KING, Suit.SPADES) + ] + + result = ShortDeckHandEvaluator.evaluate_5_cards(cards) + assert result.hand_type == ShortDeckHandType.FOUR_OF_A_KIND + assert result.key_ranks[0] == ShortDeckRank.ACE + assert result.key_ranks[1] == ShortDeckRank.KING # Kicker + + def test_flush_beats_full_house(self): + """Test that flush beats full house in short deck""" + # Flush + flush_cards = [ + Card(ShortDeckRank.ACE, Suit.SPADES), + Card(ShortDeckRank.KING, Suit.SPADES), + Card(ShortDeckRank.QUEEN, Suit.SPADES), + Card(ShortDeckRank.JACK, Suit.SPADES), + Card(ShortDeckRank.NINE, Suit.SPADES) + ] + + # Full house + full_house_cards = [ + Card(ShortDeckRank.ACE, Suit.SPADES), + Card(ShortDeckRank.ACE, Suit.HEARTS), + Card(ShortDeckRank.ACE, Suit.DIAMONDS), + Card(ShortDeckRank.KING, Suit.SPADES), + Card(ShortDeckRank.KING, Suit.HEARTS) + ] + + flush_result = ShortDeckHandEvaluator.evaluate_5_cards(flush_cards) + full_house_result = ShortDeckHandEvaluator.evaluate_5_cards(full_house_cards) + + assert flush_result.hand_type == ShortDeckHandType.FLUSH + assert full_house_result.hand_type == ShortDeckHandType.FULL_HOUSE + assert flush_result > full_house_result # Flush beats Full House in Short Deck! + + def test_full_house(self): + """Test full house detection""" + cards = [ + Card(ShortDeckRank.ACE, Suit.SPADES), + Card(ShortDeckRank.ACE, Suit.HEARTS), + Card(ShortDeckRank.ACE, Suit.DIAMONDS), + Card(ShortDeckRank.KING, Suit.SPADES), + Card(ShortDeckRank.KING, Suit.HEARTS) + ] + + result = ShortDeckHandEvaluator.evaluate_5_cards(cards) + assert result.hand_type == ShortDeckHandType.FULL_HOUSE + assert result.key_ranks[0] == ShortDeckRank.ACE # Trips + assert result.key_ranks[1] == ShortDeckRank.KING # Pair + + def test_flush(self): + """Test flush detection""" + cards = [ + Card(ShortDeckRank.ACE, Suit.SPADES), + Card(ShortDeckRank.KING, Suit.SPADES), + Card(ShortDeckRank.QUEEN, Suit.SPADES), + Card(ShortDeckRank.JACK, Suit.SPADES), + Card(ShortDeckRank.NINE, Suit.SPADES) # Not a straight + ] + + result = ShortDeckHandEvaluator.evaluate_5_cards(cards) + assert result.hand_type == ShortDeckHandType.FLUSH + assert result.key_ranks[0] == ShortDeckRank.ACE + + def test_straight_regular(self): + """Test regular straight detection""" + cards = [ + Card(ShortDeckRank.ACE, Suit.SPADES), + Card(ShortDeckRank.KING, Suit.HEARTS), + Card(ShortDeckRank.QUEEN, Suit.DIAMONDS), + Card(ShortDeckRank.JACK, Suit.CLUBS), + Card(ShortDeckRank.TEN, Suit.SPADES) + ] + + result = ShortDeckHandEvaluator.evaluate_5_cards(cards) + assert result.hand_type == ShortDeckHandType.STRAIGHT + assert result.key_ranks[0] == ShortDeckRank.ACE + + def test_straight_low_wheel(self): + """Test short deck wheel straight: A-6-7-8-9""" + cards = [ + Card(ShortDeckRank.ACE, Suit.SPADES), + Card(ShortDeckRank.NINE, Suit.HEARTS), + Card(ShortDeckRank.EIGHT, Suit.DIAMONDS), + Card(ShortDeckRank.SEVEN, Suit.CLUBS), + Card(ShortDeckRank.SIX, Suit.SPADES) + ] + + result = ShortDeckHandEvaluator.evaluate_5_cards(cards) + assert result.hand_type == ShortDeckHandType.STRAIGHT + assert result.key_ranks[0] == ShortDeckRank.NINE # 9-high straight in short deck wheel + + def test_three_of_a_kind(self): + """Test three of a kind detection""" + cards = [ + Card(ShortDeckRank.ACE, Suit.SPADES), + Card(ShortDeckRank.ACE, Suit.HEARTS), + Card(ShortDeckRank.ACE, Suit.DIAMONDS), + Card(ShortDeckRank.KING, Suit.SPADES), + Card(ShortDeckRank.QUEEN, Suit.HEARTS) + ] + + result = ShortDeckHandEvaluator.evaluate_5_cards(cards) + assert result.hand_type == ShortDeckHandType.THREE_OF_A_KIND + assert result.key_ranks[0] == ShortDeckRank.ACE + assert ShortDeckRank.KING in result.key_ranks + assert ShortDeckRank.QUEEN in result.key_ranks + + def test_two_pair(self): + """Test two pair detection""" + cards = [ + Card(ShortDeckRank.ACE, Suit.SPADES), + Card(ShortDeckRank.ACE, Suit.HEARTS), + Card(ShortDeckRank.KING, Suit.DIAMONDS), + Card(ShortDeckRank.KING, Suit.SPADES), + Card(ShortDeckRank.QUEEN, Suit.HEARTS) + ] + + result = ShortDeckHandEvaluator.evaluate_5_cards(cards) + assert result.hand_type == ShortDeckHandType.TWO_PAIR + assert result.key_ranks[0] == ShortDeckRank.ACE # Higher pair first + assert result.key_ranks[1] == ShortDeckRank.KING # Lower pair second + assert result.key_ranks[2] == ShortDeckRank.QUEEN # Kicker + + def test_one_pair(self): + """Test one pair detection""" + cards = [ + Card(ShortDeckRank.ACE, Suit.SPADES), + Card(ShortDeckRank.ACE, Suit.HEARTS), + Card(ShortDeckRank.KING, Suit.DIAMONDS), + Card(ShortDeckRank.QUEEN, Suit.SPADES), + Card(ShortDeckRank.JACK, Suit.HEARTS) + ] + + result = ShortDeckHandEvaluator.evaluate_5_cards(cards) + assert result.hand_type == ShortDeckHandType.ONE_PAIR + assert result.key_ranks[0] == ShortDeckRank.ACE + assert ShortDeckRank.KING in result.key_ranks + assert ShortDeckRank.QUEEN in result.key_ranks + assert ShortDeckRank.JACK in result.key_ranks + + def test_high_card(self): + """Test high card detection""" + cards = [ + Card(ShortDeckRank.ACE, Suit.SPADES), + Card(ShortDeckRank.KING, Suit.HEARTS), + Card(ShortDeckRank.QUEEN, Suit.DIAMONDS), + Card(ShortDeckRank.JACK, Suit.CLUBS), + Card(ShortDeckRank.NINE, Suit.HEARTS) # Not a straight (missing 10) + ] + + result = ShortDeckHandEvaluator.evaluate_5_cards(cards) + assert result.hand_type == ShortDeckHandType.HIGH_CARD + assert result.key_ranks[0] == ShortDeckRank.ACE + + def test_is_straight_regular_straights(self): + """Test isStraight with regular straights""" + # T-J-Q-K-A + ranks1 = [ShortDeckRank.ACE, ShortDeckRank.KING, ShortDeckRank.QUEEN, ShortDeckRank.JACK, ShortDeckRank.TEN] + is_straight1, high1 = ShortDeckHandEvaluator.is_straight(ranks1) + assert is_straight1 == True + assert high1 == ShortDeckRank.ACE + + # 6-7-8-9-T + ranks2 = [ShortDeckRank.TEN, ShortDeckRank.NINE, ShortDeckRank.EIGHT, ShortDeckRank.SEVEN, ShortDeckRank.SIX] + is_straight2, high2 = ShortDeckHandEvaluator.is_straight(ranks2) + assert is_straight2 == True + assert high2 == ShortDeckRank.TEN + + def test_is_straight_wheel(self): + """Test isStraight with short deck wheel: A-6-7-8-9""" + ranks = [ShortDeckRank.ACE, ShortDeckRank.NINE, ShortDeckRank.EIGHT, ShortDeckRank.SEVEN, ShortDeckRank.SIX] + is_straight, high = ShortDeckHandEvaluator.is_straight(ranks) + assert is_straight == True + assert high == ShortDeckRank.NINE # 9-high straight in short deck wheel + + def test_is_straight_not_straight(self): + """Test isStraight with non-straight""" + ranks = [ShortDeckRank.ACE, ShortDeckRank.KING, ShortDeckRank.QUEEN, ShortDeckRank.JACK, ShortDeckRank.NINE] + is_straight, high = ShortDeckHandEvaluator.is_straight(ranks) + assert is_straight == False + assert high is None + + def test_is_straight_insufficient_cards(self): + """Test isStraight with less than 5 unique ranks""" + ranks = [ShortDeckRank.ACE, ShortDeckRank.ACE, ShortDeckRank.KING, ShortDeckRank.KING] # Only 2 unique + is_straight, high = ShortDeckHandEvaluator.is_straight(ranks) + assert is_straight == False + assert high is None + + def test_evaluate_from_input_valid(self): + """Test evaluateFromInput with valid input""" + # Four of a kind + result = ShortDeckHandEvaluator.evaluate_from_input("AsAhAdAc Ks") + assert result.hand_type == ShortDeckHandType.FOUR_OF_A_KIND + assert result.key_ranks[0] == ShortDeckRank.ACE + + def test_evaluate_from_input_seven_cards(self): + """Test evaluateFromInput with 7 cards""" + result = ShortDeckHandEvaluator.evaluate_from_input("AsAhAdAc KsQsJs") + assert result.hand_type == ShortDeckHandType.FOUR_OF_A_KIND + assert result.key_ranks[0] == ShortDeckRank.ACE + + def test_evaluate_from_input_invalid_cards(self): + """Test evaluateFromInput with invalid cards (should reject 2,3,4,5)""" + with pytest.raises(ValueError, match="Invalid rank for short deck"): + ShortDeckHandEvaluator.evaluate_from_input("2s3s4s5s6s") + + def test_all_possible_straights(self): + """Test all possible straights in short deck""" + # All possible short deck straights: + # A-6-7-8-9, 6-7-8-9-T, 7-8-9-T-J, 8-9-T-J-Q, 9-T-J-Q-K, T-J-Q-K-A + + straight_combinations = [ + ([ShortDeckRank.ACE, ShortDeckRank.NINE, ShortDeckRank.EIGHT, ShortDeckRank.SEVEN, ShortDeckRank.SIX], ShortDeckRank.NINE), + ([ShortDeckRank.TEN, ShortDeckRank.NINE, ShortDeckRank.EIGHT, ShortDeckRank.SEVEN, ShortDeckRank.SIX], ShortDeckRank.TEN), + ([ShortDeckRank.JACK, ShortDeckRank.TEN, ShortDeckRank.NINE, ShortDeckRank.EIGHT, ShortDeckRank.SEVEN], ShortDeckRank.JACK), + ([ShortDeckRank.QUEEN, ShortDeckRank.JACK, ShortDeckRank.TEN, ShortDeckRank.NINE, ShortDeckRank.EIGHT], ShortDeckRank.QUEEN), + ([ShortDeckRank.KING, ShortDeckRank.QUEEN, ShortDeckRank.JACK, ShortDeckRank.TEN, ShortDeckRank.NINE], ShortDeckRank.KING), + ([ShortDeckRank.ACE, ShortDeckRank.KING, ShortDeckRank.QUEEN, ShortDeckRank.JACK, ShortDeckRank.TEN], ShortDeckRank.ACE), + ] + + for ranks, expected_high in straight_combinations: + is_straight, high = ShortDeckHandEvaluator.is_straight(ranks) + assert is_straight == True, f"Failed to detect straight: {[r.symbol for r in ranks]}" + assert high == expected_high, f"Wrong high card for {[r.symbol for r in ranks]}: expected {expected_high.symbol}, got {high.symbol if high else None}" \ No newline at end of file diff --git a/tests/shortdeck/test_hand_ranking.py b/tests/shortdeck/test_hand_ranking.py new file mode 100644 index 0000000..5afbb3f --- /dev/null +++ b/tests/shortdeck/test_hand_ranking.py @@ -0,0 +1,177 @@ +""" +Tests for Short Deck HandRanking and HandType classes +""" + +import pytest +from poker.card import Card, ShortDeckRank, Suit # 使用统一的Card类 +from shortdeck.hand_ranking import ShortDeckHandRanking, ShortDeckHandType + + +class TestShortDeckHandType: + """Test cases for ShortDeckHandType enum""" + + def test_hand_type_values(self): + """Test hand type strength values - Flush beats Full House in Short Deck""" + assert ShortDeckHandType.HIGH_CARD.strength == 1 + assert ShortDeckHandType.ONE_PAIR.strength == 2 + assert ShortDeckHandType.TWO_PAIR.strength == 3 + assert ShortDeckHandType.THREE_OF_A_KIND.strength == 4 + assert ShortDeckHandType.STRAIGHT.strength == 5 + assert ShortDeckHandType.FULL_HOUSE.strength == 6 + assert ShortDeckHandType.FLUSH.strength == 7 # Higher than Full House! + assert ShortDeckHandType.FOUR_OF_A_KIND.strength == 8 + assert ShortDeckHandType.STRAIGHT_FLUSH.strength == 9 + assert ShortDeckHandType.ROYAL_FLUSH.strength == 10 + + def test_hand_type_names(self): + """Test hand type names""" + assert ShortDeckHandType.HIGH_CARD.type_name == "High Card" + assert ShortDeckHandType.FOUR_OF_A_KIND.type_name == "Four of a Kind" + assert ShortDeckHandType.ROYAL_FLUSH.type_name == "Royal Flush" + assert ShortDeckHandType.FLUSH.type_name == "Flush" + assert ShortDeckHandType.FULL_HOUSE.type_name == "Full House" + + def test_hand_type_string_representation(self): + """Test string representation of hand types""" + assert str(ShortDeckHandType.HIGH_CARD) == "High Card" + assert str(ShortDeckHandType.ROYAL_FLUSH) == "Royal Flush" + + def test_hand_type_comparison(self): + """Test hand type comparison - KEY: Flush > Full House in Short Deck""" + assert ShortDeckHandType.HIGH_CARD < ShortDeckHandType.ONE_PAIR + assert ShortDeckHandType.FLUSH > ShortDeckHandType.FULL_HOUSE # Short Deck rule! + assert ShortDeckHandType.ROYAL_FLUSH >= ShortDeckHandType.STRAIGHT_FLUSH + assert ShortDeckHandType.TWO_PAIR <= ShortDeckHandType.THREE_OF_A_KIND + assert ShortDeckHandType.STRAIGHT < ShortDeckHandType.FULL_HOUSE + assert ShortDeckHandType.FULL_HOUSE < ShortDeckHandType.FLUSH + + def test_hand_type_equality(self): + """Test hand type equality""" + assert ShortDeckHandType.FLUSH == ShortDeckHandType.FLUSH + assert ShortDeckHandType.FLUSH != ShortDeckHandType.FULL_HOUSE + + +class TestShortDeckHandRanking: + """Test cases for ShortDeckHandRanking class""" + + def test_hand_ranking_creation(self): + """Test hand ranking creation""" + cards = [ + Card(ShortDeckRank.ACE, Suit.SPADES), + Card(ShortDeckRank.ACE, Suit.HEARTS), + Card(ShortDeckRank.ACE, Suit.DIAMONDS), + Card(ShortDeckRank.ACE, Suit.CLUBS), + Card(ShortDeckRank.KING, Suit.SPADES) + ] + ranking = ShortDeckHandRanking(ShortDeckHandType.FOUR_OF_A_KIND, [ShortDeckRank.ACE, ShortDeckRank.KING], cards) + + assert ranking.hand_type == ShortDeckHandType.FOUR_OF_A_KIND + assert ranking.key_ranks == [ShortDeckRank.ACE, ShortDeckRank.KING] + assert ranking.cards == cards + + def test_quad_string_representation(self): + """Test string representation for four of a kind""" + cards = [Card(ShortDeckRank.ACE, Suit.SPADES)] * 5 + ranking = ShortDeckHandRanking(ShortDeckHandType.FOUR_OF_A_KIND, [ShortDeckRank.ACE, ShortDeckRank.KING], cards) + assert str(ranking) == "Quad(A)" + + def test_full_house_string_representation(self): + """Test string representation for full house""" + cards = [Card(ShortDeckRank.ACE, Suit.SPADES)] * 5 + ranking = ShortDeckHandRanking(ShortDeckHandType.FULL_HOUSE, [ShortDeckRank.ACE, ShortDeckRank.KING], cards) + assert str(ranking) == "Full House(A over K)" + + def test_flush_string_representation(self): + """Test string representation for flush""" + cards = [Card(ShortDeckRank.ACE, Suit.SPADES)] * 5 + ranking = ShortDeckHandRanking(ShortDeckHandType.FLUSH, [ShortDeckRank.ACE], cards) + assert str(ranking) == "Flush(A high)" + + def test_straight_string_representation(self): + """Test string representation for straight""" + cards = [Card(ShortDeckRank.ACE, Suit.SPADES)] * 5 + ranking = ShortDeckHandRanking(ShortDeckHandType.STRAIGHT, [ShortDeckRank.ACE], cards) + assert str(ranking) == "Straight(A high)" + + def test_straight_flush_string_representation(self): + """Test string representation for straight flush""" + cards = [Card(ShortDeckRank.KING, Suit.SPADES)] * 5 + ranking = ShortDeckHandRanking(ShortDeckHandType.STRAIGHT_FLUSH, [ShortDeckRank.KING], cards) + assert str(ranking) == "Straight Flush(K high)" + + def test_royal_flush_string_representation(self): + """Test string representation for royal flush""" + cards = [Card(ShortDeckRank.ACE, Suit.SPADES)] * 5 + + # Direct royal flush + ranking1 = ShortDeckHandRanking(ShortDeckHandType.ROYAL_FLUSH, [ShortDeckRank.ACE], cards) + assert str(ranking1) == "Royal Flush" + + # Straight flush with Ace high should also show as Royal Flush + ranking2 = ShortDeckHandRanking(ShortDeckHandType.STRAIGHT_FLUSH, [ShortDeckRank.ACE], cards) + assert str(ranking2) == "Royal Flush" + + def test_three_of_a_kind_string_representation(self): + """Test string representation for three of a kind""" + cards = [Card(ShortDeckRank.ACE, Suit.SPADES)] * 5 + ranking = ShortDeckHandRanking(ShortDeckHandType.THREE_OF_A_KIND, [ShortDeckRank.ACE], cards) + assert str(ranking) == "Three of a Kind(A)" + + def test_two_pair_string_representation(self): + """Test string representation for two pair""" + cards = [Card(ShortDeckRank.ACE, Suit.SPADES)] * 5 + ranking = ShortDeckHandRanking(ShortDeckHandType.TWO_PAIR, [ShortDeckRank.ACE, ShortDeckRank.KING], cards) + assert str(ranking) == "Two Pair(A and K)" + + def test_one_pair_string_representation(self): + """Test string representation for one pair""" + cards = [Card(ShortDeckRank.ACE, Suit.SPADES)] * 5 + ranking = ShortDeckHandRanking(ShortDeckHandType.ONE_PAIR, [ShortDeckRank.ACE], cards) + assert str(ranking) == "Pair(A)" + + def test_high_card_string_representation(self): + """Test string representation for high card""" + cards = [Card(ShortDeckRank.ACE, Suit.SPADES)] * 5 + ranking = ShortDeckHandRanking(ShortDeckHandType.HIGH_CARD, [ShortDeckRank.ACE], cards) + assert str(ranking) == "High Card(A)" + + def test_hand_ranking_comparison(self): + """Test hand ranking comparison""" + # Create some test cards + cards = [Card(ShortDeckRank.ACE, Suit.SPADES)] * 5 + + # Create different rankings + quad_aces = ShortDeckHandRanking(ShortDeckHandType.FOUR_OF_A_KIND, [ShortDeckRank.ACE], cards) + flush_ace = ShortDeckHandRanking(ShortDeckHandType.FLUSH, [ShortDeckRank.ACE], cards) + full_house = ShortDeckHandRanking(ShortDeckHandType.FULL_HOUSE, [ShortDeckRank.ACE, ShortDeckRank.KING], cards) + straight_ace = ShortDeckHandRanking(ShortDeckHandType.STRAIGHT, [ShortDeckRank.ACE], cards) + + # Test Short Deck specific comparison: Flush > Full House + assert flush_ace > full_house + assert full_house < flush_ace + + # Test other comparisons + assert quad_aces > flush_ace + assert straight_ace < full_house + + def test_hand_ranking_equality(self): + """Test hand ranking equality""" + cards1 = [Card(ShortDeckRank.ACE, Suit.SPADES)] * 5 + cards2 = [Card(ShortDeckRank.KING, Suit.HEARTS)] * 5 + + ranking1 = ShortDeckHandRanking(ShortDeckHandType.FLUSH, [ShortDeckRank.ACE], cards1) + ranking2 = ShortDeckHandRanking(ShortDeckHandType.FLUSH, [ShortDeckRank.ACE], cards2) + ranking3 = ShortDeckHandRanking(ShortDeckHandType.FLUSH, [ShortDeckRank.KING], cards2) + + assert ranking1 == ranking2 # Same hand type and key ranks + assert ranking1 != ranking3 # Different key ranks + + def test_same_type_different_ranks(self): + """Test comparison of same hand type with different key ranks""" + cards = [Card(ShortDeckRank.ACE, Suit.SPADES)] * 5 + + ace_high = ShortDeckHandRanking(ShortDeckHandType.HIGH_CARD, [ShortDeckRank.ACE], cards) + king_high = ShortDeckHandRanking(ShortDeckHandType.HIGH_CARD, [ShortDeckRank.KING], cards) + + assert ace_high > king_high + assert king_high < ace_high \ No newline at end of file diff --git a/tests/shortdeck/test_integration.py b/tests/shortdeck/test_integration.py new file mode 100644 index 0000000..4059832 --- /dev/null +++ b/tests/shortdeck/test_integration.py @@ -0,0 +1,225 @@ +""" +Integration tests for Short Deck Poker implementation +""" + +import pytest +from poker.card import Card, ShortDeckRank, Suit # 使用统一的Card类 +from shortdeck import ShortDeckHandEvaluator, ShortDeckHandType, ShortDeckHandRanking + + +class TestShortDeckIntegration: + """Integration tests for the complete short deck poker system""" + + def test_shortdeck_example_cases(self): + """Test various short deck example hands""" + + # Test cases with expected results + test_cases = [ + # Royal Flush + ("AsKsQsJsTs", ShortDeckHandType.ROYAL_FLUSH, "Royal Flush"), + + # Straight Flush (not royal) + ("KsQsJsTs9s", ShortDeckHandType.STRAIGHT_FLUSH, "Straight Flush(K high)"), + + # Four of a Kind + ("AsAhAdAcKs", ShortDeckHandType.FOUR_OF_A_KIND, "Quad(A)"), + + # Flush (beats Full House in Short Deck!) + ("AsKsQsJs9s", ShortDeckHandType.FLUSH, "Flush(A high)"), + + # Full House (weaker than Flush in Short Deck) + ("AsAhAdKsKh", ShortDeckHandType.FULL_HOUSE, "Full House(A over K)"), + + # Straight - regular + ("AsKsQhJdTs", ShortDeckHandType.STRAIGHT, "Straight(A high)"), + + # Straight - short deck wheel (A-6-7-8-9) + ("As9h8d7c6s", ShortDeckHandType.STRAIGHT, "Straight(9 high)"), + + # Three of a Kind + ("AsAhAdKsQs", ShortDeckHandType.THREE_OF_A_KIND, "Three of a Kind(A)"), + + # Two Pair + ("AsAhKdKsQs", ShortDeckHandType.TWO_PAIR, "Two Pair(A and K)"), + + # One Pair + ("AsAhKdQsJs", ShortDeckHandType.ONE_PAIR, "Pair(A)"), + + # High Card + ("AsKhQdJs9s", ShortDeckHandType.HIGH_CARD, "High Card(A)") + ] + + for hand_str, expected_type, expected_str in test_cases: + result = ShortDeckHandEvaluator.evaluate_from_input(hand_str) + assert result.hand_type == expected_type, f"Failed for {hand_str}: expected {expected_type}, got {result.hand_type}" + assert str(result) == expected_str, f"Failed string for {hand_str}: expected '{expected_str}', got '{str(result)}'" + + def test_shortdeck_seven_card_hands(self): + """Test evaluation with 7 cards (like Texas Hold'em)""" + + # Should find the best 5-card hand from 7 cards + test_cases = [ + # Four of a kind + extra cards + ("AsAhAdAcKs Qs 6h", ShortDeckHandType.FOUR_OF_A_KIND, "Quad(A)"), + + # Full house from 7 cards + ("AsAhAdKsKh Qc 7d", ShortDeckHandType.FULL_HOUSE, "Full House(A over K)"), + + # Flush with extra cards + ("AsKsQsJsTs 6h 7c", ShortDeckHandType.ROYAL_FLUSH, "Royal Flush"), + ] + + for hand_str, expected_type, expected_str in test_cases: + result = ShortDeckHandEvaluator.evaluate_from_input(hand_str) + assert result.hand_type == expected_type + assert str(result) == expected_str + + def test_shortdeck_flush_vs_full_house(self): + """Test the key Short Deck rule: Flush beats Full House""" + + # Create flush + flush_hand = "AsKsQsJs9s" # A-high flush + flush_result = ShortDeckHandEvaluator.evaluate_from_input(flush_hand) + + # Create full house + full_house_hand = "AsAhAdKsKh" # Aces full of Kings + full_house_result = ShortDeckHandEvaluator.evaluate_from_input(full_house_hand) + + # Flush should beat Full House + assert flush_result > full_house_result + assert flush_result.hand_type == ShortDeckHandType.FLUSH + assert full_house_result.hand_type == ShortDeckHandType.FULL_HOUSE + + def test_shortdeck_wheel_straight(self): + """Test Short Deck wheel straight: A-6-7-8-9""" + + # Wheel straight + wheel_hand = "As9h8d7c6s" + wheel_result = ShortDeckHandEvaluator.evaluate_from_input(wheel_hand) + + # Regular straight + regular_hand = "TsJhQdKcAs" + regular_result = ShortDeckHandEvaluator.evaluate_from_input(regular_hand) + + assert wheel_result.hand_type == ShortDeckHandType.STRAIGHT + assert regular_result.hand_type == ShortDeckHandType.STRAIGHT + + # A-high straight should beat 9-high wheel + assert regular_result > wheel_result + + # Check string representations + assert str(wheel_result) == "Straight(9 high)" + assert str(regular_result) == "Straight(A high)" + + def test_no_invalid_short_deck_cards(self): + """Test that cards 2, 3, 4, 5 are properly rejected""" + + invalid_hands = [ + "2s3s4s5s6s", # Contains 2, 3, 4, 5 + "AsKs2h3h4h", # Mixed valid and invalid + "5sAsKsQsJs" # Contains 5 + ] + + for invalid_hand in invalid_hands: + with pytest.raises(ValueError, match="Invalid rank for short deck"): + ShortDeckHandEvaluator.evaluate_from_input(invalid_hand) + + def test_all_short_deck_straights(self): + """Test all 6 possible straights in Short Deck""" + + straight_tests = [ + ("As9h8d7c6s", "Straight(9 high)"), # A-6-7-8-9 (wheel) + ("TsJhQdKcAs", "Straight(A high)"), # T-J-Q-K-A (broadway) + ("Ts9h8d7c6s", "Straight(T high)"), # 6-7-8-9-T + ("JsTh9d8c7s", "Straight(J high)"), # 7-8-9-T-J + ("QsJhTd9c8s", "Straight(Q high)"), # 8-9-T-J-Q + ("KsQhJdTc9s", "Straight(K high)"), # 9-T-J-Q-K + ] + + for hand_str, expected_str in straight_tests: + result = ShortDeckHandEvaluator.evaluate_from_input(hand_str) + assert result.hand_type == ShortDeckHandType.STRAIGHT + assert str(result) == expected_str + + def test_shortdeck_card_count(self): + """Test that Short Deck has exactly 36 cards""" + # 使用Card类的ShortDeck模式 + all_cards = [] + for rank in ShortDeckRank: + for suit in Suit: + all_cards.append(Card(rank, suit)) + + assert len(all_cards) == 36 # 9 ranks * 4 suits + + # Verify no duplicates + card_strings = [str(card) for card in all_cards] + assert len(set(card_strings)) == 36 + + # Verify all ranks 6-A are present + ranks_present = set(card.rank for card in all_cards) + expected_ranks = set(ShortDeckRank) + assert ranks_present == expected_ranks + + # Verify no low cards (2,3,4,5) + for card in all_cards: + assert card.rank.numeric_value >= 6 + + def test_shortdeck_hand_rankings_order(self): + """Test the complete hand ranking order for Short Deck""" + + # Create hands of different types + hands = [ + ("AsKhQdJs9c", ShortDeckHandType.HIGH_CARD), # High Card + ("AsAhKdQsJc", ShortDeckHandType.ONE_PAIR), # One Pair + ("AsAhKdKsQc", ShortDeckHandType.TWO_PAIR), # Two Pair + ("AsAhAdKsQc", ShortDeckHandType.THREE_OF_A_KIND), # Three of a Kind + ("AsKhQdJsTc", ShortDeckHandType.STRAIGHT), # Straight + ("AsAhAdKsKc", ShortDeckHandType.FULL_HOUSE), # Full House + ("AsKsQsJs9s", ShortDeckHandType.FLUSH), # Flush (beats Full House!) + ("AsAhAdAcKs", ShortDeckHandType.FOUR_OF_A_KIND), # Four of a Kind + ("9s8s7s6sTs", ShortDeckHandType.STRAIGHT_FLUSH), # Straight Flush + ("AsKsQsJsTs", ShortDeckHandType.ROYAL_FLUSH), # Royal Flush + ] + + results = [] + for hand_str, expected_type in hands: + result = ShortDeckHandEvaluator.evaluate_from_input(hand_str) + assert result.hand_type == expected_type + results.append(result) + + # Test that they are in ascending order of strength + for i in range(len(results) - 1): + assert results[i] < results[i + 1], f"{results[i].hand_type} should be less than {results[i + 1].hand_type}" + + def test_shortdeck_main_script_functionality(self): + """Test that the main script input format works correctly""" + + # Test various input formats that the main script should handle + test_inputs = [ + "AsKsQsJsTs", # 5 cards + "AsKs QsJs TsAh Ad", # 7 cards with spaces + "6s7s8s9sTs", # Low straight flush + ] + + for input_str in test_inputs: + # This should not raise an exception + result = ShortDeckHandEvaluator.evaluate_from_input(input_str) + assert result is not None + assert hasattr(result, 'hand_type') + assert hasattr(result, 'key_ranks') + assert len(str(result)) > 0 + + def test_edge_cases(self): + """Test various edge cases""" + + # All same rank except one (should be four of a kind) + result1 = ShortDeckHandEvaluator.evaluate_from_input("6s6h6d6cAs") + assert result1.hand_type == ShortDeckHandType.FOUR_OF_A_KIND + + # Almost straight but not quite + result2 = ShortDeckHandEvaluator.evaluate_from_input("AsKhQdJs8c") # Missing 10 + assert result2.hand_type == ShortDeckHandType.HIGH_CARD + + # Almost flush but not quite + result3 = ShortDeckHandEvaluator.evaluate_from_input("AsKsQsJs8h") # Mixed suits, not straight + assert result3.hand_type == ShortDeckHandType.HIGH_CARD \ No newline at end of file diff --git a/tests/test_card.py b/tests/test_card.py index 91b18df..88d5c7b 100644 --- a/tests/test_card.py +++ b/tests/test_card.py @@ -52,39 +52,39 @@ class TestCard: def test_from_string_valid(self): """Test creating card from valid string""" - card = Card.createCard("As") + card = Card.create_card("As") assert card.rank == Rank.ACE assert card.suit == Suit.SPADES - card2 = Card.createCard("Kh") + card2 = Card.create_card("Kh") assert card2.rank == Rank.KING assert card2.suit == Suit.HEARTS - card3 = Card.createCard("2c") + card3 = Card.create_card("2c") assert card3.rank == Rank.TWO assert card3.suit == Suit.CLUBS - card4 = Card.createCard("Td") + card4 = Card.create_card("Td") assert card4.rank == Rank.TEN assert card4.suit == Suit.DIAMONDS def test_from_string_invalid(self): """Test creating card from invalid string""" with pytest.raises(ValueError): - Card.createCard("A") # Too short + Card.create_card("A") # Too short with pytest.raises(ValueError): - Card.createCard("Asx") # Too long + Card.create_card("Asx") # Too long with pytest.raises(ValueError): - Card.createCard("Xs") # Invalid rank + Card.create_card("Xs") # Invalid rank with pytest.raises(ValueError): - Card.createCard("Ax") # Invalid suit + Card.create_card("Ax") # Invalid suit def test_parse_cards_valid(self): """Test parsing multiple cards from string""" - cards = Card.parseCards("AsKs AhAdAc6s7s") + cards = Card.parse_cards("AsKs AhAdAc6s7s") assert len(cards) == 7 assert str(cards[0]) == "As" assert str(cards[1]) == "Ks" @@ -93,23 +93,23 @@ class TestCard: def test_parse_cards_with_spaces(self): """Test parsing cards with various spacing""" - cards = Card.parseCards("As Ks Ah Ad Ac 6s 7s") + cards = Card.parse_cards("As Ks Ah Ad Ac 6s 7s") assert len(cards) == 7 assert str(cards[0]) == "As" assert str(cards[6]) == "7s" def test_parse_cards_empty(self): """Test parsing empty string""" - cards = Card.parseCards("") + cards = Card.parse_cards("") assert len(cards) == 0 - cards = Card.parseCards(" ") + cards = Card.parse_cards(" ") assert len(cards) == 0 def test_parse_cards_invalid(self): """Test parsing invalid card strings""" with pytest.raises(ValueError): - Card.parseCards("AsKs A") # Incomplete card + Card.parse_cards("AsKs A") # Incomplete card with pytest.raises(ValueError): - Card.parseCards("AsKs Ax") # Invalid suit \ No newline at end of file + Card.parse_cards("AsKs Ax") # Invalid suit \ No newline at end of file diff --git a/tests/test_coverage_edge_cases.py b/tests/test_coverage_edge_cases.py new file mode 100644 index 0000000..fe348b7 --- /dev/null +++ b/tests/test_coverage_edge_cases.py @@ -0,0 +1,179 @@ +""" +Edge case tests to achieve 100% code coverage +""" + +import pytest +from poker.card import Card, Rank, ShortDeckRank, Suit +from poker.hand_evaluator import HandEvaluator +from poker.hand_ranking import HandType, HandRanking +from shortdeck.hand_ranking import ShortDeckHandType, ShortDeckHandRanking + + +class TestCoverageEdgeCases: + """Tests for edge cases to improve code coverage""" + + def test_rank_comparison_with_invalid_types(self): + """Test rank comparisons with invalid types""" + rank = Rank.ACE + + # Test __eq__ with non-Rank object + result = rank.__eq__("invalid") + assert result is NotImplemented + + # Test __lt__ with non-Rank object + result = rank.__lt__("invalid") + assert result is NotImplemented + + def test_shortdeck_rank_comparison_with_invalid_types(self): + """Test short deck rank comparisons with invalid types""" + rank = ShortDeckRank.ACE + + # Test __eq__ with non-ShortDeckRank object + result = rank.__eq__("invalid") + assert result is NotImplemented + + # Test __lt__ with non-ShortDeckRank object + result = rank.__lt__("invalid") + assert result is NotImplemented + + def test_card_creation_with_valid_ranks(self): + """Test Card creation with valid ranks""" + # Standard deck card + card1 = Card(Rank.ACE, Suit.HEARTS) + assert card1.rank == Rank.ACE + assert card1.suit == Suit.HEARTS + assert not card1.is_short_deck() + + # Short deck card + card2 = Card(ShortDeckRank.ACE, Suit.HEARTS) + assert card2.rank == ShortDeckRank.ACE + assert card2.suit == Suit.HEARTS + assert card2.is_short_deck() + + def test_hand_evaluator_exactly_5_cards_error(self): + """Test HandEvaluator with wrong number of cards""" + cards = [Card(Rank.ACE, Suit.HEARTS)] # Only 1 card + + with pytest.raises(ValueError, match="Expected 5 cards, got 1"): + HandEvaluator.evaluate_5_cards(cards) + + def test_hand_ranking_comparison_with_invalid_types(self): + """Test HandRanking comparisons with invalid types""" + cards = [ + Card(Rank.ACE, Suit.HEARTS), + Card(Rank.KING, Suit.HEARTS), + Card(Rank.QUEEN, Suit.HEARTS), + Card(Rank.JACK, Suit.HEARTS), + Card(Rank.TEN, Suit.HEARTS) + ] + + ranking = HandRanking(HandType.ROYAL_FLUSH, [Rank.ACE], cards) + + # Test comparisons with non-HandRanking objects should return False, not NotImplemented + assert ranking != "invalid" + assert not (ranking == "invalid") + + # These should not raise exceptions + try: + ranking < "invalid" + ranking <= "invalid" + ranking > "invalid" + ranking >= "invalid" + except TypeError: + pass # Expected behavior + + def test_shortdeck_hand_ranking_comparison_with_invalid_types(self): + """Test ShortDeckHandRanking comparisons with invalid types""" + cards = [ + Card(ShortDeckRank.ACE, Suit.HEARTS), + Card(ShortDeckRank.KING, Suit.HEARTS), + Card(ShortDeckRank.QUEEN, Suit.HEARTS), + Card(ShortDeckRank.JACK, Suit.HEARTS), + Card(ShortDeckRank.TEN, Suit.HEARTS) + ] + + ranking = ShortDeckHandRanking(ShortDeckHandType.ROYAL_FLUSH, [ShortDeckRank.ACE], cards) + + # Test comparisons with non-ShortDeckHandRanking objects + assert ranking != "invalid" + assert not (ranking == "invalid") + + def test_card_comparison_and_equality(self): + """Test Card comparison and equality edge cases""" + card1 = Card(Rank.ACE, Suit.HEARTS) + card2 = Card(Rank.ACE, Suit.HEARTS) + card3 = Card(Rank.KING, Suit.HEARTS) + + # Test equality + assert card1 == card2 + assert card1 != card3 + + # Test comparison + assert card1 > card3 # Ace > King + + # Test inequality with non-Card object + result = card1.__eq__("not a card") + assert result is NotImplemented + + result = card1.__lt__("not a card") + assert result is NotImplemented + + def test_rank_hash_consistency(self): + """Test rank hash consistency""" + rank1 = Rank.ACE + rank2 = Rank.ACE + rank3 = Rank.KING + + assert hash(rank1) == hash(rank2) + assert hash(rank1) != hash(rank3) + + def test_shortdeck_rank_hash_consistency(self): + """Test short deck rank hash consistency""" + rank1 = ShortDeckRank.ACE + rank2 = ShortDeckRank.ACE + rank3 = ShortDeckRank.KING + + assert hash(rank1) == hash(rank2) + assert hash(rank1) != hash(rank3) + + def test_card_string_parsing_valid_cases(self): + """Test valid card string parsing cases""" + # Test valid input + cards = Card.parse_cards("As Kh") + assert len(cards) == 2 + assert cards[0].rank == Rank.ACE + assert cards[0].suit == Suit.SPADES + assert cards[1].rank == Rank.KING + assert cards[1].suit == Suit.HEARTS + + # Test empty string returns empty list + cards = Card.parse_cards("") + assert len(cards) == 0 + + def test_card_getter_methods(self): + """Test Card getter methods for coverage""" + card = Card(Rank.ACE, Suit.HEARTS) + assert card.get_rank() == Rank.ACE + assert card.get_suit() == Suit.HEARTS + + def test_card_legacy_parse_methods(self): + """Test legacy parsing methods""" + # Test parseLongCards method + cards = Card.parseLongCards("As Kh") + assert len(cards) == 2 + + # Test parse_short_deck_cards method + cards = Card.parse_short_deck_cards("As Kh") + assert len(cards) == 2 + + def test_hand_type_comparisons_with_invalid_types(self): + """Test HandType comparisons with invalid types""" + hand_type = HandType.ROYAL_FLUSH + + # Test __lt__ with invalid type + result = hand_type.__lt__("invalid") + assert result is NotImplemented + + # Test __eq__ with invalid type + result = hand_type.__eq__("invalid") + assert result is NotImplemented \ No newline at end of file diff --git a/tests/test_hand_evaluator.py b/tests/test_hand_evaluator.py index 0c0b2f0..01ba59a 100644 --- a/tests/test_hand_evaluator.py +++ b/tests/test_hand_evaluator.py @@ -14,7 +14,7 @@ class TestHandEvaluator: def test_royal_flush(self): """Test royal flush detection""" cards_str = "AhKhQhJhTh2c3c" - ranking = HandEvaluator.evaluateFromInput(cards_str) + ranking = HandEvaluator.evaluate_from_input(cards_str) assert ranking.hand_type == HandType.ROYAL_FLUSH assert str(ranking) == "Royal Flush" @@ -22,7 +22,7 @@ class TestHandEvaluator: def test_straight_flush(self): """Test straight flush detection""" cards_str = "2h3h4h5h6h7s8s" - ranking = HandEvaluator.evaluateFromInput(cards_str) + ranking = HandEvaluator.evaluate_from_input(cards_str) assert ranking.hand_type == HandType.STRAIGHT_FLUSH assert str(ranking) == "Straight Flush(6 high)" @@ -30,7 +30,7 @@ class TestHandEvaluator: def test_four_of_a_kind(self): """Test four of a kind detection""" cards_str = "AsKs AhAdAc6s7s" - ranking = HandEvaluator.evaluateFromInput(cards_str) + ranking = HandEvaluator.evaluate_from_input(cards_str) assert ranking.hand_type == HandType.FOUR_OF_A_KIND assert str(ranking) == "Quad(A)" @@ -38,7 +38,7 @@ class TestHandEvaluator: def test_full_house(self): """Test full house detection""" cards_str = "AsAhAd KsKh6s7s" - ranking = HandEvaluator.evaluateFromInput(cards_str) + ranking = HandEvaluator.evaluate_from_input(cards_str) assert ranking.hand_type == HandType.FULL_HOUSE assert "Full House(A over K)" in str(ranking) @@ -46,7 +46,7 @@ class TestHandEvaluator: def test_flush(self): """Test flush detection""" cards_str = "AhKh6h4h2h7s8s" - ranking = HandEvaluator.evaluateFromInput(cards_str) + ranking = HandEvaluator.evaluate_from_input(cards_str) assert ranking.hand_type == HandType.FLUSH assert "Flush(A high)" in str(ranking) @@ -54,7 +54,7 @@ class TestHandEvaluator: def test_straight(self): """Test straight detection""" cards_str = "As2h3d4c5h7s8s" - ranking = HandEvaluator.evaluateFromInput(cards_str) + ranking = HandEvaluator.evaluate_from_input(cards_str) assert ranking.hand_type == HandType.STRAIGHT assert str(ranking) == "Straight(5 high)" # A-2-3-4-5 wheel @@ -62,7 +62,7 @@ class TestHandEvaluator: def test_straight_ace_high(self): """Test straight with ace high""" cards_str = "AsTsJhQdKh7s8s" - ranking = HandEvaluator.evaluateFromInput(cards_str) + ranking = HandEvaluator.evaluate_from_input(cards_str) assert ranking.hand_type == HandType.STRAIGHT assert str(ranking) == "Straight(A high)" @@ -70,7 +70,7 @@ class TestHandEvaluator: def test_three_of_a_kind(self): """Test three of a kind detection""" cards_str = "AsAhAd6s7h8s9s" - ranking = HandEvaluator.evaluateFromInput(cards_str) + ranking = HandEvaluator.evaluate_from_input(cards_str) assert ranking.hand_type == HandType.THREE_OF_A_KIND assert str(ranking) == "Three of a Kind(A)" @@ -78,7 +78,7 @@ class TestHandEvaluator: def test_two_pair(self): """Test two pair detection""" cards_str = "AsAh6d6s7h8s9s" - ranking = HandEvaluator.evaluateFromInput(cards_str) + ranking = HandEvaluator.evaluate_from_input(cards_str) assert ranking.hand_type == HandType.TWO_PAIR assert "Two Pair(A and 6)" in str(ranking) @@ -86,7 +86,7 @@ class TestHandEvaluator: def test_one_pair(self): """Test one pair detection""" cards_str = "AsAh6d4s2h3cJd" - ranking = HandEvaluator.evaluateFromInput(cards_str) + ranking = HandEvaluator.evaluate_from_input(cards_str) assert ranking.hand_type == HandType.ONE_PAIR assert str(ranking) == "Pair(A)" @@ -94,7 +94,7 @@ class TestHandEvaluator: def test_high_card(self): """Test high card detection""" cards_str = "As6h4d8s9hJdKc" - ranking = HandEvaluator.evaluateFromInput(cards_str) + ranking = HandEvaluator.evaluate_from_input(cards_str) assert ranking.hand_type == HandType.HIGH_CARD assert str(ranking) == "High Card(A)" @@ -102,7 +102,7 @@ class TestHandEvaluator: def test_wheel_straight(self): """Test A-2-3-4-5 straight (wheel)""" cards_str = "As2h3d4c5h7s8s" - ranking = HandEvaluator.evaluateFromInput(cards_str) + ranking = HandEvaluator.evaluate_from_input(cards_str) assert ranking.hand_type == HandType.STRAIGHT assert ranking.key_ranks[0] == Rank.FIVE # 5 is high in wheel @@ -110,21 +110,21 @@ class TestHandEvaluator: def test_invalid_input_not_seven_cards(self): """Test error handling for wrong number of cards""" with pytest.raises(ValueError): - HandEvaluator.evaluateFromInput("AsKh") + HandEvaluator.evaluate_from_input("AsKh") with pytest.raises(ValueError): - HandEvaluator.evaluateFromInput("AsKhQdJc9h8s7d6c") + HandEvaluator.evaluate_from_input("AsKhQdJc9h8s7d6c") def test_invalid_card_format(self): """Test error handling for invalid card format""" with pytest.raises(ValueError): - HandEvaluator.evaluateFromInput("AsKhQdJcXh8s7d") + HandEvaluator.evaluate_from_input("AsKhQdJcXh8s7d") def test_seven_cards_best_five_selected(self): """Test that best 5 cards are selected from 7""" # Should pick the straight flush over the pair cards_str = "2h3h4h5h6hAsAd" - ranking = HandEvaluator.evaluateFromInput(cards_str) + ranking = HandEvaluator.evaluate_from_input(cards_str) assert ranking.hand_type == HandType.STRAIGHT_FLUSH assert str(ranking) == "Straight Flush(6 high)" @@ -132,7 +132,7 @@ class TestHandEvaluator: def test_multiple_possible_straights(self): """Test selecting highest straight from multiple possibilities""" cards_str = "As2h3d4c5h6s7s" - ranking = HandEvaluator.evaluateFromInput(cards_str) + ranking = HandEvaluator.evaluate_from_input(cards_str) assert ranking.hand_type == HandType.STRAIGHT # Should pick 3-4-5-6-7 over A-2-3-4-5 @@ -141,7 +141,7 @@ class TestHandEvaluator: def test_multiple_possible_flushes(self): """Test selecting best flush from multiple suits""" cards_str = "AhKh6h4h2h7s8s" - ranking = HandEvaluator.evaluateFromInput(cards_str) + ranking = HandEvaluator.evaluate_from_input(cards_str) assert ranking.hand_type == HandType.FLUSH assert ranking.key_ranks[0] == Rank.ACE @@ -155,6 +155,6 @@ class TestHandEvaluator: Card(Rank.ACE, Suit.CLUBS), Card(Rank.KING, Suit.SPADES) ] - ranking = HandEvaluator.evaluate5Cards(cards) + ranking = HandEvaluator.evaluate_5_cards(cards) assert ranking.hand_type == HandType.FOUR_OF_A_KIND assert ranking.key_ranks[0] == Rank.ACE \ No newline at end of file diff --git a/tests/test_integration.py b/tests/test_integration.py index 96b34cd..c0c2a8c 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -15,7 +15,7 @@ class TestMainProgram: def test_example_input(self): """Test the example input from the requirements""" cards_str = "AsKs AhAdAc6s7s" - ranking = HandEvaluator.evaluateFromInput(cards_str) + ranking = HandEvaluator.evaluate_from_input(cards_str) assert str(ranking) == "Quad(A)" @@ -35,13 +35,13 @@ class TestMainProgram: ] for cards_str, expected_result in test_cases: - ranking = HandEvaluator.evaluateFromInput(cards_str) + ranking = HandEvaluator.evaluate_from_input(cards_str) assert str(ranking) == expected_result, f"Failed for {cards_str}" def test_wheel_straight(self): """Test A-2-3-4-5 straight""" cards_str = "As2h3d4c5h7s8s" - ranking = HandEvaluator.evaluateFromInput(cards_str) + ranking = HandEvaluator.evaluate_from_input(cards_str) assert str(ranking) == "Straight(5 high)" @@ -56,29 +56,29 @@ class TestMainProgram: expected_result = "Quad(A)" for cards_str in formats: - ranking = HandEvaluator.evaluateFromInput(cards_str) + ranking = HandEvaluator.evaluate_from_input(cards_str) assert str(ranking) == expected_result def test_case_insensitive_suits(self): """Test that suits are case insensitive""" cards_str = "AsKsAhAdAc6s7s" - ranking = HandEvaluator.evaluateFromInput(cards_str) + ranking = HandEvaluator.evaluate_from_input(cards_str) assert str(ranking) == "Quad(A)" def test_edge_cases(self): """Test edge cases and boundary conditions""" cards_str = "2s2h2d2c3s4h5d" - ranking = HandEvaluator.evaluateFromInput(cards_str) + ranking = HandEvaluator.evaluate_from_input(cards_str) assert str(ranking) == "Quad(2)" # Minimum straight cards_str = "As2h3d4c5h6s7s" - ranking = HandEvaluator.evaluateFromInput(cards_str) + ranking = HandEvaluator.evaluate_from_input(cards_str) # Should pick 3-4-5-6-7 over A-2-3-4-5 assert "Straight(7 high)" in str(ranking) # Maximum straight cards_str = "9sTsJhQdKhAsAd" - ranking = HandEvaluator.evaluateFromInput(cards_str) + ranking = HandEvaluator.evaluate_from_input(cards_str) assert str(ranking) == "Straight(A high)" \ No newline at end of file diff --git a/uv.lock b/uv.lock index 2cb44aa..a3a03f1 100644 --- a/uv.lock +++ b/uv.lock @@ -11,6 +11,67 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "coverage" +version = "7.10.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, + { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, + { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, + { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, + { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, + { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, + { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, + { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, + { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, + { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, + { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, + { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, + { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, + { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, + { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, + { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, + { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, + { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, +] + [[package]] name = "iniconfig" version = "2.1.0" @@ -41,22 +102,25 @@ wheels = [ [[package]] name = "poker-emd-task" version = "0.1.0" -source = { editable = "." } +source = { virtual = "." } dependencies = [ { name = "pytest" }, ] -[package.optional-dependencies] +[package.dev-dependencies] dev = [ - { name = "pytest" }, + { name = "coverage" }, + { name = "pytest-cov" }, ] [package.metadata] -requires-dist = [ - { name = "pytest", specifier = ">=8.4.2" }, - { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, +requires-dist = [{ name = "pytest", specifier = ">=8.4.2" }] + +[package.metadata.requires-dev] +dev = [ + { name = "coverage", specifier = ">=7.10.7" }, + { name = "pytest-cov", specifier = ">=7.0.0" }, ] -provides-extras = ["dev"] [[package]] name = "pygments" @@ -82,3 +146,17 @@ sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a wheels = [ { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, ] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +]