diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 74318bf..9a897f1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,24 @@ Changelog All notable changes to this project will be documented in this file. +Version 0.5.4 (September 7, 2024) +--------------------------------- + +**Added** + +- Post bet support. + + - Post bets are a type of forced bet which a player who just seated must pay to play right away instead of waiting for the button to pass. + - To denote a post bet, it must be passed alongside ``raw_blinds_or_straddles`` variable during state construction. + + - For example, say UTG+1 wants to put a post-bet in a 6-max game. Then, ``[1, 2, 0, -2, 0, 0]`` or, equivalently, ``{0: 1, 1: 2, 3: -2}``. + +- ``pokerkit.notation.HandHistory.state_actions`` is a new alias for ``pokerkit.notation.HandHistory.iter_state_actions()``. + +**Deprecated** + +- ``pokerkit.notation.HandHistory.iter_state_actions()`` due to poor naming. It is superceded by ``pokerkit.notation.HandHistory.state_actions`` which behaves identically. This method will be removed in PokerKit Version 0.6. + Version 0.5.3 (September 1, 2024) --------------------------------- diff --git a/TODOS.rst b/TODOS.rst index 2fbca62..3848b8b 100644 --- a/TODOS.rst +++ b/TODOS.rst @@ -4,12 +4,6 @@ Todos Here are some of the features that are planned to be implemented in the future. -- Post bets support. - - - Post bets are posted when a player wants to play a game immediately after joining without waiting for the button to pass him or her. - - This is demonstrably different from blinds or straddles - - As an optional parameter - - Fully comply with the Poker Hand History file format specs. - URL: https://arxiv.org/abs/2312.11753 diff --git a/docs/conf.py b/docs/conf.py index 879d2f7..2c82319 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -14,7 +14,7 @@ project = 'PokerKit' copyright = '2023, University of Toronto Computer Poker Student Research Group' author = 'University of Toronto Computer Poker Student Research Group' -release = '0.5.3' +release = '0.5.4' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/docs/notation.rst b/docs/notation.rst index 287023e..6f64ed2 100644 --- a/docs/notation.rst +++ b/docs/notation.rst @@ -30,7 +30,7 @@ Reading hands ... # Iterate through each action step - for state, action in hh.iter_state_actions(): + for state, action in hh.state_actions: ... It is possible to supply your own chip value parsing function, divmod, or rake function to construct the game states. Additionally, the default value parsing function is defined as :func:`pokerkit.utilities.parse_value`. This parser automatically parses integers or floats based on the raw string value. You may supply your own number-type parsers as well. diff --git a/docs/simulation.rst b/docs/simulation.rst index 61b360e..1efbaf3 100644 --- a/docs/simulation.rst +++ b/docs/simulation.rst @@ -637,7 +637,7 @@ Helper Method/Attribute Description :attr:`pokerkit.state.State.divmod` (Defined during initialization and described above in this page) :attr:`pokerkit.state.State.rake` (Defined during initialization and described above in this page) :attr:`pokerkit.state.State.antes` Cleaned ante amounts. -:attr:`pokerkit.state.State.blinds_or_straddles` Cleaned blind/straddle amounts. +:attr:`pokerkit.state.State.blinds_or_straddles` Cleaned blind/straddle/post-bet amounts. If a value is a post bet, it must be negated (i.e. ``[1, 2, 0, 0, -2, 0]``). This is to tell PokerKit that this forced bet does not impact who opens the preflop action. :attr:`pokerkit.state.State.starting_stacks` Cleaned starting stack chip amounts. :attr:`pokerkit.state.State.deck_cards` Shuffled deck from which cards are drawn. :attr:`pokerkit.state.State.board_cards` Community cards. diff --git a/pokerkit/__init__.py b/pokerkit/__init__.py index 9ecdf80..287904a 100644 --- a/pokerkit/__init__.py +++ b/pokerkit/__init__.py @@ -85,6 +85,7 @@ 'ShortDeckHoldemHand', 'ShortDeckHoldemLookup', 'shuffled', + 'sign', 'SingleDraw', 'StandardBadugiHand', 'StandardBadugiLookup', @@ -208,6 +209,7 @@ Rank, RankOrder, shuffled, + sign, Suit, ValuesLike, ) diff --git a/pokerkit/notation.py b/pokerkit/notation.py index 1567a7a..12cb940 100644 --- a/pokerkit/notation.py +++ b/pokerkit/notation.py @@ -547,11 +547,18 @@ def append_dealing_actions() -> None: return HandHistory(**cls._filter_non_fields(**kwargs)) def __iter__(self) -> Iterator[State]: - yield from map(itemgetter(0), self.iter_state_actions()) + yield from map(itemgetter(0), self.state_actions) - def iter_state_actions(self) -> Iterator[tuple[State, str | None]]: + @property + def state_actions(self) -> Iterator[tuple[State, str | None]]: """Iterate through state-actions. + If an action from the + :attr:`pokerkit.notation.HandHistory.actions` field was just + applied, the ``str`` representation of the action is yielded + alongside the newly transitioned state. Otherwise, the + corresponding second value of the pair is ``None``. + :return: The state actions. """ state = self.create_state() @@ -589,6 +596,24 @@ def iter_state_actions(self) -> Iterator[tuple[State, str | None]]: yield state, action + def iter_state_actions(self) -> Iterator[tuple[State, str | None]]: + """Deprecated. Now, an alias of + :attr:`pokerkit.notation.HandHistory.state_actions`. + + This method will be removed in PokerKit Version 0.6. + + :return: The state-actions. + """ + warn( + ( + 'pokerkit.notation.HandHistory.iter_state_actions() is' + ' deprecated and will be removed on PokerKit Version 0.6' + ), + DeprecationWarning, + ) + + yield from self.state_actions + @property def game_type(self) -> type[Poker]: """Return the game type. diff --git a/pokerkit/state.py b/pokerkit/state.py index a93f3be..4feb248 100644 --- a/pokerkit/state.py +++ b/pokerkit/state.py @@ -9,7 +9,7 @@ from enum import StrEnum, unique from functools import partial from itertools import chain, filterfalse, islice, starmap -from operator import getitem, sub +from operator import getitem, gt, sub from random import shuffle from warnings import warn @@ -27,6 +27,7 @@ Rank, RankOrder, shuffled, + sign, Suit, ValuesLike, ) @@ -938,6 +939,10 @@ class State: If the bring-in is non-zero, the all blind/straddle values must be zero. If any of the bring-in is zero, there must be at least one positive blind/straddle value. + + Negative value is interpreted as a post bet, used to denote what a + player who just got seated pays to begin playing right away without + waiting for the button. """ bring_in: int """The bring-in. @@ -1148,14 +1153,8 @@ def __post_init__( raise ValueError('The streets are empty.') elif not self.streets[0].hole_dealing_statuses: 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 was supplied.', - ) + elif min(self.antes) < 0 or self.bring_in < 0: + raise ValueError('Negative antes or bring-in was supplied.') elif ( not any(self.antes) and not any(self.blinds_or_straddles) @@ -1166,7 +1165,10 @@ def __post_init__( ) elif min(self.starting_stacks) <= 0: raise ValueError('Non-positive starting stacks was supplied.') - elif any(self.blinds_or_straddles) and self.bring_in: + elif ( + any(map(partial(gt, 0), self.blinds_or_straddles)) + and self.bring_in + ): raise ValueError( ( 'Only one of bring-in or (blinds or straddles) must' @@ -3269,9 +3271,9 @@ def get_effective_blind_or_straddle(self, player_index: int) -> int: :return: The effective blind or straddle. """ if self.player_count == 2: - blind_or_straddle = self.blinds_or_straddles[not player_index] + blind_or_straddle = abs(self.blinds_or_straddles[not player_index]) else: - blind_or_straddle = self.blinds_or_straddles[player_index] + blind_or_straddle = abs(self.blinds_or_straddles[player_index]) return min( blind_or_straddle, @@ -4121,7 +4123,9 @@ def card_key(rank_order: RankOrder, card: Card) -> tuple[int, Suit]: case Opening.POSITION: max_bet_index = max( self.player_indices, - key=lambda i: (self.bets[i], i), + key=lambda i: ( + (self.bets[i] * sign(self.blinds_or_straddles[i]), i) + ), ) self.opener_index = (max_bet_index + 1) % self.player_count case Opening.LOW_CARD: diff --git a/pokerkit/tests/test_papers.py b/pokerkit/tests/test_papers.py index 2fb10cf..fc9d34b 100644 --- a/pokerkit/tests/test_papers.py +++ b/pokerkit/tests/test_papers.py @@ -218,7 +218,7 @@ def test_dwan_ivey_2009(self) -> None: new_state = tuple(hh)[-1] self.assertEqual( - list(map(itemgetter(1), hh.iter_state_actions())), + list(map(itemgetter(1), hh.state_actions)), [None] + hh.actions, ) self.assertEqual(new_state.stacks, [572100, 1997500, 1109500]) @@ -230,7 +230,7 @@ def test_dwan_ivey_2009(self) -> None: self.assertEqual(len(tuple(hh)), len(new_state.operations) + 1) self.assertEqual( - list(filter(None, map(itemgetter(1), hh.iter_state_actions()))), + list(filter(None, map(itemgetter(1), hh.state_actions))), hh.actions, ) self.assertEqual(new_state.stacks, [572100, 1997500, 1109500]) @@ -323,7 +323,7 @@ def test_phua_xuan_2019(self) -> None: new_state = tuple(hh)[-1] self.assertEqual( - list(map(itemgetter(1), hh.iter_state_actions())), + list(map(itemgetter(1), hh.state_actions)), [None] + hh.actions, ) self.assertEqual( @@ -338,7 +338,7 @@ def test_phua_xuan_2019(self) -> None: self.assertEqual(len(tuple(hh)), len(new_state.operations) + 1) self.assertEqual( - list(filter(None, map(itemgetter(1), hh.iter_state_actions()))), + list(filter(None, map(itemgetter(1), hh.state_actions))), hh.actions, ) self.assertEqual( @@ -417,7 +417,7 @@ def test_antonius_blom_2009(self) -> None: new_state = tuple(hh)[-1] self.assertEqual( - list(map(itemgetter(1), hh.iter_state_actions())), + list(map(itemgetter(1), hh.state_actions)), [None] + hh.actions, ) self.assertEqual(new_state.stacks, [1937923.75, 0.0]) @@ -429,7 +429,7 @@ def test_antonius_blom_2009(self) -> None: self.assertEqual(len(tuple(hh)), len(new_state.operations) + 1) self.assertEqual( - list(filter(None, map(itemgetter(1), hh.iter_state_actions()))), + list(filter(None, map(itemgetter(1), hh.state_actions))), hh.actions, ) self.assertEqual(new_state.stacks, [1937923.75, 0.0]) @@ -582,7 +582,7 @@ def test_arieh_yockey_2019(self) -> None: new_state = tuple(hh)[-1] self.assertEqual( - list(map(itemgetter(1), hh.iter_state_actions())), + list(map(itemgetter(1), hh.state_actions)), [None] + hh.actions, ) self.assertEqual(new_state.stacks, [0, 4190000, 5910000, 12095000]) @@ -594,9 +594,7 @@ def test_arieh_yockey_2019(self) -> None: self.assertEqual(len(tuple(hh)), len(new_state.operations) + 1) self.assertEqual( - list( - filter(None, map(itemgetter(1), hh.iter_state_actions())), - ), + list(filter(None, map(itemgetter(1), hh.state_actions))), hh.actions, ) self.assertEqual(new_state.stacks, [0, 4190000, 5910000, 12095000]) @@ -691,7 +689,7 @@ def test_alice_carol_wikipedia(self) -> None: new_state = tuple(hh)[-1] self.assertEqual( - list(map(itemgetter(1), hh.iter_state_actions())), + list(map(itemgetter(1), hh.state_actions)), [None] + hh.actions, ) self.assertEqual(new_state.stacks, [196, 220, 200, 184]) @@ -703,7 +701,7 @@ def test_alice_carol_wikipedia(self) -> None: self.assertEqual(len(tuple(hh)), len(new_state.operations) + 1) self.assertEqual( - list(filter(None, map(itemgetter(1), hh.iter_state_actions()))), + list(filter(None, map(itemgetter(1), hh.state_actions))), hh.actions, ) self.assertEqual(new_state.stacks, [196, 220, 200, 184]) diff --git a/pokerkit/tests/test_state.py b/pokerkit/tests/test_state.py index d044fbe..05013f6 100644 --- a/pokerkit/tests/test_state.py +++ b/pokerkit/tests/test_state.py @@ -20,6 +20,7 @@ from pokerkit.state import ( Automation, BettingStructure, + CheckingOrCalling, _HighHandOpeningLookup, _LowHandOpeningLookup, Opening, @@ -1295,6 +1296,76 @@ def test_unknown_showdown(self) -> None: self.assertEqual(state.starting_stacks, (200,) * 3) self.assertEqual(state.stacks, [198] * 3) + def test_post_bets(self) -> None: + def create_state(blinds_or_straddles: ValuesLike) -> State: + return NoLimitTexasHoldem.create_state( + ( + Automation.ANTE_POSTING, + Automation.BET_COLLECTION, + Automation.BLIND_OR_STRADDLE_POSTING, + Automation.CARD_BURNING, + Automation.HOLE_DEALING, + Automation.BOARD_DEALING, + Automation.RUNOUT_COUNT_SELECTION, + Automation.HOLE_CARDS_SHOWING_OR_MUCKING, + Automation.HAND_KILLING, + Automation.CHIPS_PUSHING, + Automation.CHIPS_PULLING, + ), + True, + 0, + blinds_or_straddles, + 2, + 200, + 6, + ) + + self.assertEqual(create_state({0: 1, 1: 2}).actor_index, 2) + self.assertEqual(create_state({0: 1, 1: 2, 2: -2}).actor_index, 2) + self.assertEqual(create_state({0: 1, 1: 2, 3: -2}).actor_index, 2) + self.assertEqual(create_state({0: 1, 1: 2, 4: -2}).actor_index, 2) + self.assertEqual( + create_state({0: 1, 1: 2, 2: -2, 5: -2}).actor_index, + 2, + ) + self.assertEqual(create_state({0: 2, 1: 2}).actor_index, 2) + self.assertEqual(create_state({0: 1, 1: 2, 2: 4}).actor_index, 3) + self.assertEqual( + create_state({0: 1, 1: 2, 2: 4, 3: -2, 4: -2}).actor_index, + 3, + ) + + state = create_state({0: 1, 1: 2, 4: -2, 5: -2}) + + self.assertEqual( + state.check_or_call(), + CheckingOrCalling(commentary=None, player_index=2, amount=2), + ) + self.assertEqual( + state.check_or_call(), + CheckingOrCalling(commentary=None, player_index=3, amount=2), + ) + self.assertEqual( + state.check_or_call(), + CheckingOrCalling(commentary=None, player_index=4, amount=0), + ) + self.assertEqual( + state.check_or_call(), + CheckingOrCalling(commentary=None, player_index=5, amount=0), + ) + self.assertEqual( + state.check_or_call(), + CheckingOrCalling(commentary=None, player_index=0, amount=1), + ) + self.assertEqual( + state.check_or_call(), + CheckingOrCalling(commentary=None, player_index=1, amount=0), + ) + self.assertEqual( + state.check_or_call(), + CheckingOrCalling(commentary=None, player_index=0, amount=0), + ) + if __name__ == '__main__': main() # pragma: no cover diff --git a/pokerkit/utilities.py b/pokerkit/utilities.py index b66fa60..afdce2f 100644 --- a/pokerkit/utilities.py +++ b/pokerkit/utilities.py @@ -772,3 +772,24 @@ def parse_value(raw_value: str) -> int: value = float(raw_value) return cast(int, value) + + +def sign(value: int) -> int: + """Get sign of a value. + + >>> sign(-5) + -1 + >>> sign(10) + 1 + >>> sign(0) + 0 + + :param value: The value to get a sign from. + :return: The sign. + """ + if value > 0: + return 1 + elif value < 0: + return -1 + else: + return 0 diff --git a/setup.py b/setup.py index 6b67ef3..7435a81 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name='pokerkit', - version='0.5.3', + version='0.5.4', 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',