Skip to content

Commit

Permalink
Implement post-bet support
Browse files Browse the repository at this point in the history
  • Loading branch information
AussieSeaweed committed Sep 8, 2024
1 parent 17bcb96 commit 9a35ee3
Show file tree
Hide file tree
Showing 12 changed files with 170 additions and 37 deletions.
18 changes: 18 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
---------------------------------

Expand Down
6 changes: 0 additions & 6 deletions TODOS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/notation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion docs/simulation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions pokerkit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
'ShortDeckHoldemHand',
'ShortDeckHoldemLookup',
'shuffled',
'sign',
'SingleDraw',
'StandardBadugiHand',
'StandardBadugiLookup',
Expand Down Expand Up @@ -208,6 +209,7 @@
Rank,
RankOrder,
shuffled,
sign,
Suit,
ValuesLike,
)
29 changes: 27 additions & 2 deletions pokerkit/notation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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.
Expand Down
30 changes: 17 additions & 13 deletions pokerkit/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -27,6 +27,7 @@
Rank,
RankOrder,
shuffled,
sign,
Suit,
ValuesLike,
)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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'
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
22 changes: 10 additions & 12 deletions pokerkit/tests/test_papers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand All @@ -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])
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -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])
Expand All @@ -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])
Expand Down Expand Up @@ -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])
Expand All @@ -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])
Expand Down Expand Up @@ -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])
Expand All @@ -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])
Expand Down
71 changes: 71 additions & 0 deletions pokerkit/tests/test_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from pokerkit.state import (
Automation,
BettingStructure,
CheckingOrCalling,
_HighHandOpeningLookup,
_LowHandOpeningLookup,
Opening,
Expand Down Expand Up @@ -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
Loading

0 comments on commit 9a35ee3

Please sign in to comment.