diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fcab66c..b8ad69e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,18 @@ Changelog All notable changes to this project will be documented in this file. +Version 0.4.17 (April 9, 2024) +------------------------------ + +**Changed** + +- Make error/warning messages more descriptive. + +**Added** + +- Censored hole cards ``pokerkit.state.State.get_censored_hole_cards()``. +- Turn index ``pokerkit.state.State.turn_index``. + Version 0.4.16 (April 5, 2024) ------------------------------ diff --git a/pokerkit/analysis.py b/pokerkit/analysis.py index 488824a..40fce90 100644 --- a/pokerkit/analysis.py +++ b/pokerkit/analysis.py @@ -63,7 +63,13 @@ def iterate_interval(s: str) -> Iterator[frozenset[Card]]: i3 = index(r3) if i1 - i0 != i3 - i2: - raise ValueError(f'error in pattern {repr(raw_range)}') + raise ValueError( + ( + f'Pattern {repr(raw_range)} is invalid because the two' + ' pairs of ranks that bounds the dash-separated notation' + ' must be a shifted version of the other.' + ), + ) if i0 > i2: i0, i1, i2, i3 = i2, i3, i0, i1 diff --git a/pokerkit/hands.py b/pokerkit/hands.py index cca334b..d85c797 100644 --- a/pokerkit/hands.py +++ b/pokerkit/hands.py @@ -30,10 +30,10 @@ class Hand(Hashable, ABC): >>> h0 = ShortDeckHoldemHand('6s7s8s9sTs') >>> h1 = ShortDeckHoldemHand('7c8c9cTcJc') - >>> h2 = ShortDeckHoldemHand('2c2d2h2s3h') + >>> h2 = ShortDeckHoldemHand('2c2d2h2s3h') # doctest: +ELLIPSIS Traceback (most recent call last): ... - ValueError: invalid hand '2c2d2h2s3h' + ValueError: The cards '2c2d2h2s3h' form an invalid ShortDeckHoldemHand h... >>> h0 6s7s8s9sTs >>> h1 @@ -85,7 +85,12 @@ def __init__(self, cards: CardsLike) -> None: self.__cards = Card.clean(cards) if not self.lookup.has_entry(self.cards): - raise ValueError(f'invalid hand \'{repr(self)}\'') + raise ValueError( + ( + f'The cards {repr(cards)} form an invalid' + f' {type(self).__qualname__} hand.' + ), + ) def __eq__(self, other: Any) -> bool: if type(self) != type(other): # noqa: E721 @@ -223,7 +228,12 @@ def from_game( max_hand = hand if max_hand is None: - raise ValueError('no valid hand') + raise ValueError( + ( + f'No valid {type(cls).__qualname__} hand can be formed' + ' from the hole and board cards.' + ), + ) return max_hand @@ -246,18 +256,18 @@ class StandardHighHand(StandardHand): >>> h0 < h1 < h2 < h3 < h4 True - >>> h = StandardHighHand('4c5dThJsAcKh2h') + >>> h = StandardHighHand('4c5dThJsAcKh2h') # doctest: +ELLIPSIS Traceback (most recent call last): ... - ValueError: invalid hand '4c5dThJsAcKh2h' + ValueError: The cards '4c5dThJsAcKh2h' form an invalid StandardHighHand ... >>> h = StandardHighHand('Ac2c3c4c') Traceback (most recent call last): ... - ValueError: invalid hand 'Ac2c3c4c' + ValueError: The cards 'Ac2c3c4c' form an invalid StandardHighHand hand. >>> h = StandardHighHand(()) Traceback (most recent call last): ... - ValueError: invalid hand '' + ValueError: The cards () form an invalid StandardHighHand hand. """ low = False @@ -274,18 +284,18 @@ class StandardLowHand(StandardHand): >>> h0 < h1 < h2 < h3 < h4 True - >>> h = StandardLowHand('4c5dThJsAcKh2h') + >>> h = StandardLowHand('4c5dThJsAcKh2h') # doctest: +ELLIPSIS Traceback (most recent call last): ... - ValueError: invalid hand '4c5dThJsAcKh2h' + ValueError: The cards '4c5dThJsAcKh2h' form an invalid StandardLowHand h... >>> h = StandardLowHand('Ac2c3c4c') Traceback (most recent call last): ... - ValueError: invalid hand 'Ac2c3c4c' + ValueError: The cards 'Ac2c3c4c' form an invalid StandardLowHand hand. >>> h = StandardLowHand(()) Traceback (most recent call last): ... - ValueError: invalid hand '' + ValueError: The cards () form an invalid StandardLowHand hand. """ low = True @@ -304,18 +314,18 @@ class ShortDeckHoldemHand(CombinationHand): >>> h0 < h1 < h2 < h3 < h4 True - >>> h = ShortDeckHoldemHand('4c5dThJsAcKh2h') + >>> h = ShortDeckHoldemHand('4c5dThJsAcKh2h') # doctest: +ELLIPSIS Traceback (most recent call last): ... - ValueError: invalid hand '4c5dThJsAcKh2h' - >>> h = ShortDeckHoldemHand('Ac2c3c4c5c') + ValueError: The cards '4c5dThJsAcKh2h' form an invalid ShortDeckHoldemHa... + >>> h = ShortDeckHoldemHand('Ac2c3c4c5c') # doctest: +ELLIPSIS Traceback (most recent call last): ... - ValueError: invalid hand 'Ac2c3c4c5c' + ValueError: The cards 'Ac2c3c4c5c' form an invalid ShortDeckHoldemHand ... >>> h = ShortDeckHoldemHand(()) Traceback (most recent call last): ... - ValueError: invalid hand '' + ValueError: The cards () form an invalid ShortDeckHoldemHand hand. """ lookup = ShortDeckHoldemLookup() @@ -332,26 +342,26 @@ class EightOrBetterLowHand(CombinationHand): >>> h0 < h1 < h2 True - >>> h = EightOrBetterLowHand('AcAsAd2s4s') + >>> h = EightOrBetterLowHand('AcAsAd2s4s') # doctest: +ELLIPSIS Traceback (most recent call last): ... - ValueError: invalid hand 'AcAsAd2s4s' - >>> h = EightOrBetterLowHand('TsJsQsKsAs') + ValueError: The cards 'AcAsAd2s4s' form an invalid EightOrBetterLowHand ... + >>> h = EightOrBetterLowHand('TsJsQsKsAs') # doctest: +ELLIPSIS Traceback (most recent call last): ... - ValueError: invalid hand 'TsJsQsKsAs' - >>> h = EightOrBetterLowHand('4c5dThJsAcKh2h') + ValueError: The cards 'TsJsQsKsAs' form an invalid EightOrBetterLowHand ... + >>> h = EightOrBetterLowHand('4c5dThJsAcKh2h') # doctest: +ELLIPSIS Traceback (most recent call last): ... - ValueError: invalid hand '4c5dThJsAcKh2h' + ValueError: The cards '4c5dThJsAcKh2h' form an invalid EightOrBetterLowH... >>> h = EightOrBetterLowHand('Ac2c3c4c') Traceback (most recent call last): ... - ValueError: invalid hand 'Ac2c3c4c' + ValueError: The cards 'Ac2c3c4c' form an invalid EightOrBetterLowHand hand. >>> h = EightOrBetterLowHand(()) Traceback (most recent call last): ... - ValueError: invalid hand '' + ValueError: The cards () form an invalid EightOrBetterLowHand hand. """ lookup = EightOrBetterLookup() @@ -374,11 +384,11 @@ class RegularLowHand(CombinationHand): >>> h = RegularLowHand('4c5dThJsAcKh2h') Traceback (most recent call last): ... - ValueError: invalid hand '4c5dThJsAcKh2h' + ValueError: The cards '4c5dThJsAcKh2h' form an invalid RegularLowHand hand. >>> h = RegularLowHand(()) Traceback (most recent call last): ... - ValueError: invalid hand '' + ValueError: The cards () form an invalid RegularLowHand hand. """ lookup = RegularLookup() @@ -434,7 +444,12 @@ def from_game( max_hand = hand if max_hand is None: - raise ValueError('no valid hand') + raise ValueError( + ( + f'No valid {type(cls).__qualname__} hand can be formed' + ' from the hole and board cards.' + ), + ) return max_hand @@ -521,7 +536,12 @@ def from_game( max_hand = hand if max_hand is None: - raise ValueError('no valid hand') + raise ValueError( + ( + f'No valid {type(cls).__qualname__} hand can be formed' + ' from the hole and board cards.' + ), + ) return max_hand @@ -579,23 +599,23 @@ class BadugiHand(Hand): >>> h = BadugiHand('Ac2d3c4s5c') Traceback (most recent call last): ... - ValueError: invalid hand 'Ac2d3c4s5c' + ValueError: The cards 'Ac2d3c4s5c' form an invalid BadugiHand hand. >>> h = BadugiHand('Ac2d3c4s') Traceback (most recent call last): ... - ValueError: invalid hand 'Ac2d3c4s' + ValueError: The cards 'Ac2d3c4s' form an invalid BadugiHand hand. >>> h = BadugiHand('AcAd3h4s') Traceback (most recent call last): ... - ValueError: invalid hand 'AcAd3h4s' + ValueError: The cards 'AcAd3h4s' form an invalid BadugiHand hand. >>> h = BadugiHand('Ac2c') Traceback (most recent call last): ... - ValueError: invalid hand 'Ac2c' + ValueError: The cards 'Ac2c' form an invalid BadugiHand hand. >>> h = BadugiHand(()) Traceback (most recent call last): ... - ValueError: invalid hand '' + ValueError: The cards () form an invalid BadugiHand hand. """ lookup = BadugiLookup() @@ -676,7 +696,7 @@ class KuhnPokerHand(Hand): >>> h = KuhnPokerHand('As') Traceback (most recent call last): ... - ValueError: invalid hand 'As' + ValueError: The cards 'As' form an invalid KuhnPokerHand hand. """ lookup = KuhnPokerLookup() diff --git a/pokerkit/lookups.py b/pokerkit/lookups.py index 3951182..993a167 100644 --- a/pokerkit/lookups.py +++ b/pokerkit/lookups.py @@ -153,7 +153,7 @@ def __post_init__(self) -> None: @abstractmethod def _add_entries(self) -> None: - pass + pass # pragma: no cover def __reset_ranks(self) -> None: indices = set() @@ -206,7 +206,7 @@ def get_entry(self, cards: CardsLike) -> Entry: >>> entry = lookup.get_entry('Ah6h7s8c2s') Traceback (most recent call last): ... - ValueError: cards form an invalid hand + ValueError: The cards 'Ah6h7s8c2s' form an invalid hand. :param cards: The cards to look up. :return: The corresponding lookup entry. @@ -215,7 +215,7 @@ def get_entry(self, cards: CardsLike) -> Entry: key = self._get_key(cards) if key not in self.__entries: - raise ValueError('cards form an invalid hand') + raise ValueError(f'The cards {repr(cards)} form an invalid hand.') return self.__entries[key] @@ -227,7 +227,7 @@ def get_entry_or_none(self, cards: CardsLike) -> Entry | None: >>> lookup.get_entry('Ah6h7s8c2s') Traceback (most recent call last): ... - ValueError: cards form an invalid hand + ValueError: The cards 'Ah6h7s8c2s' form an invalid hand. >>> lookup.get_entry_or_none('Ah6h7s8c2s') is None True @@ -300,7 +300,7 @@ class StandardLookup(Lookup): >>> e2 = lookup.get_entry('AcAdAhAsAc') Traceback (most recent call last): ... - ValueError: cards form an invalid hand + ValueError: The cards 'AcAdAhAsAc' form an invalid hand. >>> e0 < e1 True >>> e0.label @@ -346,7 +346,7 @@ class ShortDeckHoldemLookup(Lookup): >>> e2 = lookup.get_entry('Ah2h3s4c5s') Traceback (most recent call last): ... - ValueError: cards form an invalid hand + ValueError: The cards 'Ah2h3s4c5s' form an invalid hand. >>> e0 < e1 True >>> e0.label @@ -406,7 +406,7 @@ class RegularLookup(Lookup): >>> e2 = lookup.get_entry('3s4sQhTc') Traceback (most recent call last): ... - ValueError: cards form an invalid hand + ValueError: The cards '3s4sQhTc' form an invalid hand. >>> e0 < e1 True >>> e0.label @@ -447,7 +447,7 @@ class BadugiLookup(Lookup): >>> e2 = lookup.get_entry('AcAdAhAs') Traceback (most recent call last): ... - ValueError: cards form an invalid hand + ValueError: The cards 'AcAdAhAs' form an invalid hand. >>> e0 > e1 True >>> e0.label @@ -466,7 +466,12 @@ def _get_key(self, cards: CardsLike) -> tuple[int, bool]: cards = Card.clean(cards) if not Card.are_rainbow(cards): - raise ValueError('cards not rainbow') + raise ValueError( + ( + 'Badugi hands must be rainbow (i.e. of distinct suits) but' + f' the cards {repr(cards)} are not.' + ), + ) return super()._get_key(cards) @@ -495,7 +500,7 @@ class KuhnPokerLookup(Lookup): >>> e2 = lookup.get_entry('2?') Traceback (most recent call last): ... - ValueError: cards form an invalid hand + ValueError: The cards '2?' form an invalid hand. >>> e0 < e1 True >>> e0.label diff --git a/pokerkit/notation.py b/pokerkit/notation.py index 0722aaa..0bf03b7 100644 --- a/pokerkit/notation.py +++ b/pokerkit/notation.py @@ -295,7 +295,13 @@ def _filter_non_fields(cls, **kwargs: Any) -> dict[str, Any]: filtered_fields[key] = value else: if not key.startswith('_'): - warn(f'unexpected field \'{key}\'') + warn( + ( + f'The field {repr(key)} is an unexpected field and' + ' should probably be prefixed with an underscore' + ' character \'_\'.' + ), + ) filtered_fields['user_defined_fields'][key] = value @@ -414,9 +420,9 @@ def append_dealing_actions() -> None: if operation.commentary is not None: if action is None: - action = '# {operation.commentary}' + action = f'# {operation.commentary}' else: - action = action.strip() + ' # {operation.commentary}' + action = action.strip() + f' # {operation.commentary}' if action is not None: actions.append(action.strip()) @@ -599,11 +605,22 @@ def to_acpc_protocol( number cannot be determined. """ if self.variant not in self.ACPC_PROTOCOL_VARIANTS: - raise ValueError('unsupported variant') + raise ValueError( + ( + f'The variant {repr(self.variant)} is not among the' + ' supported ACPC variants' + f' {repr(self.ACPC_PROTOCOL_VARIANTS)}.' + ), + ) if hand_number is None: if self.hand is None: - raise ValueError('hand number is unknown') + raise ValueError( + ( + 'Since the hand number is not defined in the hand' + ' history object, it must be passed as an argument.' + ), + ) hand_number = self.hand @@ -617,7 +634,9 @@ def to_acpc_protocol( def egress() -> tuple[str, str]: if not all(raw_hole_cards[position]): - raise ValueError('the hole cards at position must be known') + raise ValueError( + 'The hole cards at the desired position must be known.', + ) return 'S->', f'{match_state}\r\n' @@ -717,12 +736,23 @@ def to_pluribus_protocol( :raises ValueError: If the game is not supported or the hand number cannot be determined. """ - if self.variant not in self.ACPC_PROTOCOL_VARIANTS: - raise ValueError('unsupported variant') + if self.variant not in self.PLURIBUS_PROTOCOL_VARIANTS: + raise ValueError( + ( + f'The variant {repr(self.variant)} is not among the' + ' supported variants for pluribus notation' + f' {repr(self.PLURIBUS_PROTOCOL_VARIANTS)}.' + ), + ) if hand_number is None: if self.hand is None: - raise ValueError('hand number is unknown') + raise ValueError( + ( + 'Since the hand number is not defined in the hand' + ' history object, it must be passed as an argument.' + ), + ) hand_number = self.hand @@ -814,7 +844,12 @@ def verify_player(index: int | None) -> None: label, parsed_index = player[:1], int(player[1:]) - 1 if label != 'p' or parsed_index != index: - raise ValueError(f'invalid Player \'{player}\'') + raise ValueError( + ( + f'The player {repr(player)} is not a valid player for the' + f' action {repr(action)}.' + ), + ) commentary = action[action.index('#') + 2:] if '#' in action else None words = action.split() @@ -861,4 +896,6 @@ def verify_player(index: int | None) -> None: case (): state.no_operate(commentary=commentary) case _: - raise ValueError(f'invalid action \'{action}\'') + raise ValueError( + f'The action {repr(action)} is an invalid action.', + ) diff --git a/pokerkit/state.py b/pokerkit/state.py index 3535b0a..6296738 100644 --- a/pokerkit/state.py +++ b/pokerkit/state.py @@ -23,6 +23,7 @@ max_or_none, min_or_none, rake, + Rank, RankOrder, shuffled, Suit, @@ -168,7 +169,7 @@ class Street: ... ) Traceback (most recent call last): ... - ValueError: negative number of dealt cards + ValueError: The number of dealt cards -1 is negative. >>> street = Street( ... True, ... (False, False), @@ -177,10 +178,10 @@ class Street: ... Opening.POSITION, ... 0, ... None, - ... ) + ... ) # doctest: +ELLIPSIS Traceback (most recent call last): ... - ValueError: non-positive minimum completion, betting, or raising amount + ValueError: Non-positive minimum completion, betting, or raising amount ... >>> street = Street( ... True, ... (False, False), @@ -189,10 +190,10 @@ class Street: ... Opening.POSITION, ... 2, ... -1, - ... ) + ... ) # doctest: +ELLIPSIS Traceback (most recent call last): ... - ValueError: negative maximum number of completions, bettings, or raisings + ValueError: Negative maximum number of completion, bets, or raises -1 wa... """ card_burning_status: bool @@ -212,18 +213,32 @@ class Street: def __post_init__(self) -> None: if self.board_dealing_count < 0: - raise ValueError('negative number of dealt cards') + raise ValueError( + ( + f'The number of dealt cards {self.board_dealing_count}' + ' is negative.' + ), + ) elif ( not self.hole_dealing_statuses and not self.board_dealing_count and not self.draw_status ): - raise ValueError('no dealing') + raise ValueError('At least one dealing must be carried out.') elif self.hole_dealing_statuses and self.draw_status: - raise ValueError('dealing hole and standing pat or discarding') + raise ValueError( + ( + 'Only one of hole dealing or drawing is permitted as draws' + ' are followed by hole dealings.' + ), + ) elif self.min_completion_betting_or_raising_amount <= 0: raise ValueError( - 'non-positive minimum completion, betting, or raising amount', + ( + 'Non-positive minimum completion, betting, or raising' + f' amount {self.min_completion_betting_or_raising_amount}' + ' was supplied.' + ), ) elif ( self.max_completion_betting_or_raising_count is not None @@ -231,8 +246,9 @@ def __post_init__(self) -> None: ): raise ValueError( ( - 'negative maximum number of completions, bettings, or ' - 'raisings' + 'Negative maximum number of completion, bets, or raises' + f' {self.max_completion_betting_or_raising_count} was' + ' supplied.' ), ) @@ -282,7 +298,7 @@ class Pot: >>> pot = Pot(-1, (1, 3)) Traceback (most recent call last): ... - ValueError: negative pot amount + ValueError: The pot amount -1 is negative. """ amount: int @@ -292,7 +308,7 @@ class Pot: def __post_init__(self) -> None: if self.amount < 0: - raise ValueError('negative pot amount') + raise ValueError(f'The pot amount {self.amount} is negative.') @dataclass(frozen=True) @@ -644,7 +660,7 @@ class State: ... ) Traceback (most recent call last): ... - ValueError: empty streets + ValueError: The streets are empty. >>> state = State( ... (), ... Deck.KUHN_POKER, @@ -670,7 +686,7 @@ class State: ... ) Traceback (most recent call last): ... - ValueError: first street not hole dealing + ValueError: The first street must be of hole dealing. >>> state = State( ... (), ... Deck.KUHN_POKER, @@ -696,7 +712,7 @@ class State: ... ) Traceback (most recent call last): ... - ValueError: negative antes, blinds, straddles, or bring-in + ValueError: Negative antes, blinds, straddles, or bring-in was supplied. >>> state = State( ... (), ... Deck.KUHN_POKER, @@ -722,7 +738,7 @@ class State: ... ) Traceback (most recent call last): ... - ValueError: no antes, blinds, straddles, or bring-in + ValueError: No antes, blinds, straddles, or bring-in was supplied. >>> state = State( ... (), ... Deck.KUHN_POKER, @@ -748,7 +764,7 @@ class State: ... ) Traceback (most recent call last): ... - ValueError: non-positive starting stacks + ValueError: Non-positive starting stacks was supplied. >>> state = State( ... (), ... Deck.KUHN_POKER, @@ -771,10 +787,10 @@ class State: ... 1, ... (2,) * 2, ... 2, - ... ) + ... ) # doctest: +ELLIPSIS Traceback (most recent call last): ... - ValueError: both bring-in and blinds or straddles specified + ValueError: Only one of bring-in or (blinds or straddles) must specified... >>> state = State( ... (), ... Deck.KUHN_POKER, @@ -800,7 +816,7 @@ class State: ... ) Traceback (most recent call last): ... - ValueError: bring-in must be less than the min bet + ValueError: The bring-in must be less than the min bet. >>> state = State( ... (), ... Deck.KUHN_POKER, @@ -826,7 +842,7 @@ class State: ... ) Traceback (most recent call last): ... - ValueError: not enough players + ValueError: There must be at least 2 players (currently 1). """ __low_hand_opening_lookup = _LowHandOpeningLookup() @@ -922,32 +938,46 @@ def __post_init__( ) if not self.streets: - raise ValueError('empty streets') + raise ValueError('The streets are empty.') elif not self.streets[0].hole_dealing_statuses: - raise ValueError('first street not hole dealing') + raise ValueError('The first street must be of hole dealing.') elif ( min(self.antes) < 0 or min(self.blinds_or_straddles) < 0 or self.bring_in < 0 ): - raise ValueError('negative antes, blinds, straddles, or bring-in') + raise ValueError( + 'Negative antes, blinds, straddles, or bring-in was supplied.', + ) elif ( not any(self.antes) and not any(self.blinds_or_straddles) and not self.bring_in ): - raise ValueError('no antes, blinds, straddles, or bring-in') + raise ValueError( + 'No antes, blinds, straddles, or bring-in was supplied.', + ) elif min(self.starting_stacks) <= 0: - raise ValueError('non-positive starting stacks') + raise ValueError('Non-positive starting stacks was supplied.') elif any(self.blinds_or_straddles) and self.bring_in: - raise ValueError('both bring-in and blinds or straddles specified') + raise ValueError( + ( + 'Only one of bring-in or (blinds or straddles) must' + ' specified, but both were.' + ), + ) elif ( self.bring_in >= self.streets[0].min_completion_betting_or_raising_amount ): - raise ValueError('bring-in must be less than the min bet') + raise ValueError('The bring-in must be less than the min bet.') elif self.player_count < 2: - raise ValueError('not enough players') + raise ValueError( + ( + 'There must be at least 2 players (currently' + f' {self.player_count}).' + ), + ) self._setup() self._begin() @@ -1309,6 +1339,82 @@ def street(self) -> Street | None: return self.streets[self.street_index] + @property + def turn_index(self) -> int | None: + """Return the turn index. + + Regardless of what stage the state is in, whoever must make a + decision will be returned, which is one of the following: + + - :attr:`pokerkit.state.State.stander_pat_or_discarder_index` + - :attr:`pokerkit.state.State.actor_index` + - :attr:`pokerkit.state.State.showdown_index` + + :return: The index of the player whose turn it is or ``None`` if + no one is in turn. + """ + if self.stander_pat_or_discarder_index is not None: + player_index = self.stander_pat_or_discarder_index + elif self.actor_index is not None: + player_index = self.actor_index + elif self.showdown_index is not None: + player_index = self.showdown_index + else: + player_index = None + + return player_index + + def get_censored_hole_cards(self, player_index: int) -> Iterator[Card]: + """Return the censored hole cards of the player. + + The hole cards that are face-down are yielded as an unknown + card. + + >>> from pokerkit import * + >>> state = ( + ... FixedLimitSevenCardStudHighLowSplitEightOrBetter.create_state( + ... ( + ... Automation.ANTE_POSTING, + ... Automation.BET_COLLECTION, + ... Automation.BLIND_OR_STRADDLE_POSTING, + ... Automation.CARD_BURNING, + ... Automation.BOARD_DEALING, + ... Automation.HOLE_CARDS_SHOWING_OR_MUCKING, + ... Automation.HAND_KILLING, + ... Automation.CHIPS_PUSHING, + ... Automation.CHIPS_PULLING, + ... ), + ... True, + ... 0, + ... 1, + ... 2, + ... 4, + ... (50, 100), + ... 2, + ... ) + ... ) + >>> state.deal_hole('AcAdAh') # doctest: +ELLIPSIS + HoleDealing(commentary=None, player_index=0, cards=(Ac, Ad, Ah), sta... + >>> state.deal_hole('KcKdKh') # doctest: +ELLIPSIS + HoleDealing(commentary=None, player_index=1, cards=(Kc, Kd, Kh), sta... + >>> state.get_down_cards(0) # doctest: +ELLIPSIS + + >>> tuple(state.get_censored_hole_cards(0)) + (??, ??, Ah) + >>> tuple(state.get_censored_hole_cards(1)) + (??, ??, Kh) + + :return: The censored hole cards. + """ + for card, status in zip( + self.hole_cards[player_index], + self.hole_card_statuses[player_index], + ): + if status: + yield card + else: + yield Card(Rank.UNKNOWN, Suit.UNKNOWN) + def get_down_cards(self, player_index: int) -> Iterator[Card]: """Return the down cards of the player. @@ -1926,7 +2032,7 @@ def _verify_cards_consumption( dealable_cards = self.get_dealable_cards(cards) if len(dealable_cards) < cards: - raise ValueError('not enough cards in deck') + raise ValueError('There are not enough cards to be dealt.') cards = dealable_cards[:cards] else: @@ -1935,7 +2041,12 @@ def _verify_cards_consumption( for card in cards: if card not in dealable_cards and not card.unknown_status: - warn(f'dealing {card} that is not recommended to be dealt') + warn( + ( + f'A card being dealt {repr(card)} is not' + ' recommended to be dealt.' + ), + ) return cards @@ -2030,7 +2141,7 @@ def pot_amounts(self) -> Iterator[int]: >>> state.complete_bet_or_raise_to(1000) Traceback (most recent call last): ... - ValueError: irrelevant completion, betting, or raising + ValueError: There is no reason to complete, bet, or raise. >>> state.check_or_call() CheckingOrCalling(commentary=None, player_index=3, amount=200) >>> state.check_or_call() @@ -2284,7 +2395,7 @@ def pots(self) -> Iterator[Pot]: >>> state.complete_bet_or_raise_to(1000) Traceback (most recent call last): ... - ValueError: irrelevant completion, betting, or raising + ValueError: There is no reason to complete, bet, or raise. >>> state.check_or_call() CheckingOrCalling(commentary=None, player_index=3, amount=200) >>> state.check_or_call() @@ -2487,7 +2598,7 @@ def ante_poster_indices(self) -> Iterator[int]: def _verify_ante_posting(self) -> None: if not any(self.ante_posting_statuses): - raise ValueError('nobody can post the ante') + raise ValueError('Nobody can post the ante.') def verify_ante_posting(self, player_index: int | None = None) -> int: """Verify the ante posting. @@ -2502,7 +2613,9 @@ def verify_ante_posting(self, player_index: int | None = None) -> int: player_index = next(self.ante_poster_indices) if not self.ante_posting_statuses[player_index]: - raise ValueError('player cannot post the ante') + raise ValueError( + f'The Player {player_index} cannot post the ante.', + ) return player_index @@ -2629,7 +2742,7 @@ def verify_bet_collection(self) -> None: :raises ValueError: If the bet collection cannot be done. """ if not self.bet_collection_status: - raise ValueError('bet collection prohibited') + raise ValueError('The bet collection is currently prohibited.') def can_collect_bets(self) -> bool: """Return whether the bet collection can be done. @@ -2798,7 +2911,7 @@ def blind_or_straddle_poster_indices(self) -> Iterator[int]: def _verify_blind_or_straddle_posting(self) -> None: if not any(self.blind_or_straddle_posting_statuses): - raise ValueError('nobody can post the blind or straddle') + raise ValueError('Nobody can post the blind or straddle.') def verify_blind_or_straddle_posting( self, @@ -2816,7 +2929,9 @@ def verify_blind_or_straddle_posting( player_index = next(self.blind_or_straddle_poster_indices) if not self.blind_or_straddle_posting_statuses[player_index]: - raise ValueError('player cannot post the blind or straddle') + raise ValueError( + f'The Player {player_index} cannot post the blind or straddle', + ) return player_index @@ -3018,11 +3133,21 @@ def verify_card_burning( ) if not self.card_burning_status: - raise ValueError('no pending burns') + raise ValueError('No card burning is pending.') elif any(self.standing_pat_or_discarding_statuses): - raise ValueError('not all have stood pat or discarded') + raise ValueError( + ( + 'Not all have stood pat or discarded as should be done' + ' when burning a card.' + ), + ) elif len(cards) != 1: - raise ValueError('expected one card') + raise ValueError( + ( + f'One card must be burned, not {len(cards)} as in' + f' {repr(cards)}.' + ), + ) card, = cards @@ -3106,11 +3231,16 @@ def hole_dealee_index(self) -> int | None: def _verify_hole_dealing(self) -> None: if self.card_burning_status: - raise ValueError('card must be burnt') + raise ValueError('A card must be burnt before hole dealing.') elif not any(self.hole_dealing_statuses): - raise ValueError('nobody can be dealt hole cards') + raise ValueError('Currently, nobody can be dealt hole cards.') elif any(self.standing_pat_or_discarding_statuses): - raise ValueError('not all have stood pat or discarded') + raise ValueError( + ( + 'Not all have stood pat or discarded, as should be when' + ' hole dealing.' + ), + ) def verify_hole_dealing( self, @@ -3132,7 +3262,14 @@ def verify_hole_dealing( assert player_index is not None if not 0 < len(cards) <= len(self.hole_dealing_statuses[player_index]): - raise ValueError('invalid number of cards') + raise ValueError( + ( + 'The number of cards dealt must be non-zero and less than' + ' or equal to' + f' {len(self.hole_dealing_statuses[player_index])}, not' + f' {len(cards)} as for {repr(cards)}.' + ), + ) return cards @@ -3201,18 +3338,24 @@ def verify_board_dealing( :raises ValueError: If the board dealing cannot be done. """ if self.card_burning_status: - raise ValueError('card must be burnt') + raise ValueError('A card must be burnt before board dealing.') elif not self.board_dealing_count: - raise ValueError('no pending board dealing') + raise ValueError('No board dealing is pending.') elif any(self.standing_pat_or_discarding_statuses): - raise ValueError('not all have stood pat or discarded') + raise ValueError('Not all have stood pat or discarded.') cards = self._verify_cards_consumption( self.board_dealing_count if cards is None else cards, ) if not 0 < len(cards) <= self.board_dealing_count: - raise ValueError('invalid number of cards') + raise ValueError( + ( + 'The number of dealt cards must be non-zero and less than' + f' or equal to {self.board_dealing_count}, not' + f' {len(cards)} as for {repr(cards)}.' + ), + ) return cards @@ -3275,7 +3418,7 @@ def stander_pat_or_discarder_index(self) -> int | None: def _verify_standing_pat_or_discarding(self) -> None: if not any(self.standing_pat_or_discarding_statuses): - raise ValueError('no pending discards') + raise ValueError('There are no pending draws.') def verify_standing_pat_or_discarding( self, @@ -3295,7 +3438,13 @@ def verify_standing_pat_or_discarding( assert player_index is not None if not set(cards) <= set(self.hole_cards[player_index]): - raise ValueError('discarded cards not a subset of hole cards') + raise ValueError( + ( + f'The discarded cards {repr(cards)} must be a subset of' + f' hole cards {repr(self.hole_cards[player_index])}, but' + ' it is not.' + ), + ) return cards @@ -3442,7 +3591,7 @@ def card_key(rank_order: RankOrder, card: Card) -> tuple[int, Suit]: ] self.opener_index = entries.index(max_or_none(entries)) case _: # pragma: no cover - raise NotImplementedError + raise AssertionError assert self.opener_index is not None @@ -3531,16 +3680,16 @@ def verify_folding(self) -> None: :raises ValueError: If the folding cannot be done. """ if not self.actor_indices: - raise ValueError('no player to act') + raise ValueError('There is no player to act.') elif self.bring_in_status: - raise ValueError('bring-in not posted') + raise ValueError('The player must post a bring-in or complete.') player_index = self.actor_index assert player_index is not None if self.bets[player_index] >= max(self.bets): - raise ValueError('redundant fold') + raise ValueError('There is no reason for this player to fold.') def can_fold(self) -> bool: """Return whether theing fold can be done. @@ -3646,9 +3795,9 @@ def verify_checking_or_calling(self) -> None: :raises ValueError: If the checking or calling cannot be done. """ if not self.actor_indices: - raise ValueError('no player to act') + raise ValueError('There is no player to act.') elif self.bring_in_status: - raise ValueError('bring-in not posted') + raise ValueError('The player must post a bring-in or complete.') def can_check_or_call(self) -> bool: """Return whether the checking or calling can be done. @@ -3754,9 +3903,9 @@ def verify_bring_in_posting(self) -> None: :raises ValueError: If the bring-in posting cannot be done. """ if not self.actor_indices: - raise ValueError('no player to act') + raise ValueError('There is no player to act.') elif not self.bring_in_status: - raise ValueError('bring-in cannot be posted') + raise ValueError('The bring-in posting is forbidden.') def can_post_bring_in(self) -> bool: """Return whether the bring-in posting can be done. @@ -3886,7 +4035,7 @@ def max_completion_betting_or_raising_to_amount(self) -> int | None: + self.bets[self.actor_index] ) case _: # pragma: no cover - raise NotImplementedError + raise AssertionError assert amount is not None assert ( @@ -3898,7 +4047,7 @@ def max_completion_betting_or_raising_to_amount(self) -> int | None: def _verify_completion_betting_or_raising(self) -> None: if not self.actor_indices: - raise ValueError('no player to act') + raise ValueError('There is no player to act.') assert self.street is not None @@ -3907,7 +4056,7 @@ def _verify_completion_betting_or_raising(self) -> None: == self.street.max_completion_betting_or_raising_count ): raise ValueError( - 'no more completion, betting, or raising permitted', + 'No more completion, betting, or raising is permitted.', ) player_index = self.actor_index @@ -3918,7 +4067,7 @@ def _verify_completion_betting_or_raising(self) -> None: self.stacks[player_index] <= max(self.bets) - self.bets[player_index] ): - raise ValueError('not enough chips in stack') + raise ValueError('There are not enough chips in stack.') for i in self.player_indices: if ( @@ -3928,7 +4077,7 @@ def _verify_completion_betting_or_raising(self) -> None: ): break else: - raise ValueError('irrelevant completion, betting, or raising') + raise ValueError('There is no reason to complete, bet, or raise.') def verify_completion_betting_or_raising_to( self, @@ -3952,11 +4101,17 @@ def verify_completion_betting_or_raising_to( if amount < self.min_completion_betting_or_raising_to_amount: raise ValueError( - 'below min completion, betting, or raising to amount', + ( + f'The amount {amount} is below the minimum allowed' + f' {self.min_completion_betting_or_raising_to_amount}.' + ), ) elif amount > self.max_completion_betting_or_raising_to_amount: raise ValueError( - 'above max completion, betting, or raising to amount', + ( + f'The amount {amount} is above the maximum allowed' + f' {self.max_completion_betting_or_raising_to_amount}.' + ), ) return amount @@ -4059,7 +4214,7 @@ def can_complete_bet_or_raise_to( >>> state.complete_bet_or_raise_to() Traceback (most recent call last): ... - ValueError: no more completion, betting, or raising permitted + ValueError: No more completion, betting, or raising is permitted. :param amount: The optional completion, betting, or raising to amount. @@ -4267,7 +4422,7 @@ def _pop_showdown_index(self) -> int: def _verify_hole_cards_showing_or_mucking(self) -> None: if not self.showdown_indices: - raise ValueError('no player to act') + raise ValueError('There is no player to showdown.') def verify_hole_cards_showing_or_mucking( self, @@ -4304,7 +4459,7 @@ def verify_hole_cards_showing_or_mucking( ) if not status and self.all_in_status: - raise ValueError('must show hole cards in all-in pots') + raise ValueError('The player must show in an all-in situation.') if hole_cards is None and status: hole_cards = tuple(self.hole_cards[player_index]) @@ -4312,7 +4467,7 @@ def verify_hole_cards_showing_or_mucking( if hole_cards is not None and status: for card in hole_cards: if card.unknown_status: - raise ValueError('unknown card shown') + raise ValueError('An unknown card is shown.') return status, hole_cards @@ -4583,7 +4738,7 @@ def hand_killing_indices(self) -> Iterator[int]: def _verify_hand_killing(self) -> None: if not any(self.hand_killing_statuses): - raise ValueError('nobody can kill their hand') + raise ValueError('Nobody can kill their hand.') def verify_hand_killing(self, player_index: int | None = None) -> int: """Verify the hand killing. @@ -4598,7 +4753,9 @@ def verify_hand_killing(self, player_index: int | None = None) -> int: player_index = next(self.hand_killing_indices) if not self.hand_killing_statuses[player_index]: - raise ValueError('player cannot kill their hand') + raise ValueError( + f'The Player {player_index} cannot kill their hand.', + ) return player_index @@ -4741,7 +4898,7 @@ def verify_chips_pushing(self) -> None: :raises ValueError: If the chips pushing cannot be done. """ if not self._pots: - raise ValueError('chips push not allowed') + raise ValueError('The chip pushing is not allowed.') def can_push_chips(self) -> bool: """Return whether the chips pushing can be done. @@ -4952,7 +5109,7 @@ def chips_pulling_indices(self) -> Iterator[int]: def _verify_chips_pulling(self) -> None: if not any(self.chips_pulling_statuses): - raise ValueError('no one can pull chips') + raise ValueError('No one can pull chips.') def verify_chips_pulling(self, player_index: int | None = None) -> int: """Verify the chips pulling. @@ -4967,7 +5124,7 @@ def verify_chips_pulling(self, player_index: int | None = None) -> int: player_index = next(self.chips_pulling_indices) if not self.chips_pulling_statuses[player_index]: - raise ValueError('no chip to be pulled') + raise ValueError('There is no chip to be pulled.') return player_index diff --git a/pokerkit/tests/test_state.py b/pokerkit/tests/test_state.py index 975d869..472be3f 100644 --- a/pokerkit/tests/test_state.py +++ b/pokerkit/tests/test_state.py @@ -12,6 +12,7 @@ FixedLimitOmahaHoldemHighLowSplitEightOrBetter, FixedLimitRazz, FixedLimitSevenCardStud, + NoLimitDeuceToSevenLowballSingleDraw, NoLimitShortDeckHoldem, NoLimitTexasHoldem, ) @@ -567,6 +568,90 @@ def test_automated_dealing(self) -> None: state.burn_card('??') state.check_or_call() + def test_turn_index(self) -> None: + state = NoLimitDeuceToSevenLowballSingleDraw.create_state( + ( + Automation.ANTE_POSTING, + Automation.BET_COLLECTION, + Automation.BLIND_OR_STRADDLE_POSTING, + Automation.CARD_BURNING, + Automation.HOLE_DEALING, + Automation.BOARD_DEALING, + Automation.HAND_KILLING, + Automation.CHIPS_PUSHING, + Automation.CHIPS_PULLING, + ), + True, + 0, + (1, 2), + 2, + 200, + 6, + ) + + self.assertEqual(state.turn_index, 2) + self.assertEqual(state.stander_pat_or_discarder_index, None) + self.assertEqual(state.actor_index, 2) + self.assertEqual(state.showdown_index, None) + state.fold() + self.assertEqual(state.turn_index, 3) + self.assertEqual(state.stander_pat_or_discarder_index, None) + self.assertEqual(state.actor_index, 3) + self.assertEqual(state.showdown_index, None) + state.fold() + self.assertEqual(state.turn_index, 4) + self.assertEqual(state.stander_pat_or_discarder_index, None) + self.assertEqual(state.actor_index, 4) + self.assertEqual(state.showdown_index, None) + state.fold() + self.assertEqual(state.turn_index, 5) + self.assertEqual(state.stander_pat_or_discarder_index, None) + self.assertEqual(state.actor_index, 5) + self.assertEqual(state.showdown_index, None) + state.fold() + self.assertEqual(state.turn_index, 0) + self.assertEqual(state.stander_pat_or_discarder_index, None) + self.assertEqual(state.actor_index, 0) + self.assertEqual(state.showdown_index, None) + state.check_or_call() + self.assertEqual(state.turn_index, 1) + self.assertEqual(state.stander_pat_or_discarder_index, None) + self.assertEqual(state.actor_index, 1) + self.assertEqual(state.showdown_index, None) + state.check_or_call() + + self.assertEqual(state.turn_index, 0) + self.assertEqual(state.stander_pat_or_discarder_index, 0) + self.assertEqual(state.actor_index, None) + self.assertEqual(state.showdown_index, None) + state.stand_pat_or_discard() + self.assertEqual(state.turn_index, 1) + self.assertEqual(state.stander_pat_or_discarder_index, 1) + self.assertEqual(state.actor_index, None) + self.assertEqual(state.showdown_index, None) + state.stand_pat_or_discard() + + self.assertEqual(state.turn_index, 0) + self.assertEqual(state.stander_pat_or_discarder_index, None) + self.assertEqual(state.actor_index, 0) + self.assertEqual(state.showdown_index, None) + state.check_or_call() + self.assertEqual(state.turn_index, 1) + self.assertEqual(state.stander_pat_or_discarder_index, None) + self.assertEqual(state.actor_index, 1) + self.assertEqual(state.showdown_index, None) + state.check_or_call() + + self.assertEqual(state.turn_index, 0) + self.assertEqual(state.stander_pat_or_discarder_index, None) + self.assertEqual(state.actor_index, None) + self.assertEqual(state.showdown_index, 0) + state.show_or_muck_hole_cards() + self.assertEqual(state.turn_index, 1) + self.assertEqual(state.stander_pat_or_discarder_index, None) + self.assertEqual(state.actor_index, None) + self.assertEqual(state.showdown_index, 1) + def test_reshuffling(self) -> None: state = FixedLimitDeuceToSevenLowballTripleDraw.create_state( ( diff --git a/pokerkit/utilities.py b/pokerkit/utilities.py index 693d39f..53d274c 100644 --- a/pokerkit/utilities.py +++ b/pokerkit/utilities.py @@ -343,7 +343,7 @@ def clean(cls, values: CardsLike) -> tuple[Card, ...]: >>> Card.clean(None) Traceback (most recent call last): ... - ValueError: invalid values + ValueError: The card values None are invalid. :param values: The cards. :return: The cleaned cards. @@ -357,7 +357,7 @@ def clean(cls, values: CardsLike) -> tuple[Card, ...]: values = tuple(values) else: - raise ValueError('invalid values') + raise ValueError(f'The card values {repr(values)} are invalid.') return values @@ -371,10 +371,10 @@ def parse(cls, *raw_cards: str) -> Iterator[Card]: [2c, 8d, 5s, Kh] >>> next(Card.parse('AcAh')) Ac - >>> next(Card.parse('AcA')) + >>> next(Card.parse('AcA')) # doctest: +ELLIPSIS Traceback (most recent call last): ... - ValueError: content length not a multiple of 2 + ValueError: The lengths of valid card representations must be multip... >>> next(Card.parse('1d')) Traceback (most recent call last): ... @@ -391,7 +391,12 @@ def parse(cls, *raw_cards: str) -> Iterator[Card]: for contents in raw_cards: for content in contents.split(): if len(content) % 2 != 0: - raise ValueError('content length not a multiple of 2') + raise ValueError( + ( + 'The lengths of valid card representations must be' + f' multiples of 2, unlike {repr(content)}' + ), + ) for i in range(0, len(content), 2): rank = Rank(content[i]) @@ -582,7 +587,7 @@ def clean_values(values: ValuesLike, count: int) -> tuple[int, ...]: >>> clean_values(None, 2) Traceback (most recent call last): ... - ValueError: invalid values + ValueError: The values None are invalid. :param values: The values. :param count: The number of values. @@ -605,7 +610,7 @@ def clean_values(values: ValuesLike, count: int) -> tuple[int, ...]: values = tuple(parsed_values) else: - raise ValueError('invalid values') + raise ValueError(f'The values {repr(values)} are invalid.') return values diff --git a/setup.py b/setup.py index 8898f18..e9232c9 100644 --- a/setup.py +++ b/setup.py @@ -4,8 +4,8 @@ setup( name='pokerkit', - version='0.4.16', - description='An open-source Python library for poker simulations and hand evaluations', + version='0.4.17', + description='An open-source Python library for poker game simulations, hand evaluations, and statistical analysis', long_description=open('README.rst').read(), long_description_content_type='text/x-rst', url='https://github.com/uoftcprg/pokerkit',