├── .gitignore
├── .readthedocs.yml
├── CHANGELOG.rst
├── CONTRIBUTING.rst
├── LICENSE
├── README.rst
├── TODOS.rst
├── docs
├── Makefile
├── _static
│ ├── 2022-WSOP-Live-Action-Rules.pdf
│ ├── 2022-WSOP-Tournament-Rules.pdf
│ ├── 2023-WSOP-Live-Action-Rules.pdf
│ ├── 2023-WSOP-Tournament-Rules.pdf
│ ├── cards.drawio
│ ├── cards.drawio.png
│ ├── hands.drawio
│ ├── hands.drawio.png
│ ├── phases.drawio
│ ├── phases.drawio.png
│ └── protocol.pdf
├── analysis.rst
├── changelog.rst
├── conf.py
├── contributing.rst
├── evaluation.rst
├── examples.rst
├── index.rst
├── make.bat
├── notation.rst
├── reference.rst
├── simulation.rst
└── tips.rst
├── pokerkit
├── __init__.py
├── analysis.py
├── games.py
├── hands.py
├── lookups.py
├── notation.py
├── py.typed
├── state.py
├── tests
│ ├── __init__.py
│ ├── test_analysis.py
│ ├── test_games.py
│ ├── test_lookups.py
│ ├── test_notation.py
│ ├── test_papers.py
│ ├── test_state.py
│ ├── test_utilities.py
│ └── test_wsop
│ │ ├── __init__.py
│ │ ├── test_2023_43_5.py
│ │ └── test_2023_54_4.py
└── utilities.py
├── requirements.txt
└── setup.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Covers Python
2 |
3 | # Byte-compiled / optimized / DLL files
4 | __pycache__/
5 | *.py[cod]
6 | *$py.class
7 |
8 | # C extensions
9 | *.so
10 |
11 | # Distribution / packaging
12 | .Python
13 | build/
14 | develop-eggs/
15 | dist/
16 | downloads/
17 | eggs/
18 | .eggs/
19 | lib/
20 | lib64/
21 | parts/
22 | sdist/
23 | var/
24 | wheels/
25 | share/python-wheels/
26 | *.egg-info/
27 | .installed.cfg
28 | *.egg
29 | MANIFEST
30 |
31 | # PyInstaller
32 | # Usually these files are written by a python script from a template
33 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
34 | *.manifest
35 | *.spec
36 |
37 | # Installer logs
38 | pip-log.txt
39 | pip-delete-this-directory.txt
40 |
41 | # Unit test / coverage reports
42 | htmlcov/
43 | .tox/
44 | .nox/
45 | .coverage
46 | .coverage.*
47 | .cache
48 | nosetests.xml
49 | coverage.xml
50 | *.cover
51 | *.py,cover
52 | .hypothesis/
53 | .pytest_cache/
54 | cover/
55 |
56 | # Translations
57 | *.mo
58 | *.pot
59 |
60 | # Django stuff:
61 | *.log
62 | local_settings.py
63 | db.sqlite3
64 | db.sqlite3-journal
65 |
66 | # Flask stuff:
67 | instance/
68 | .webassets-cache
69 |
70 | # Scrapy stuff:
71 | .scrapy
72 |
73 | # Sphinx documentation
74 | docs/_build/
75 |
76 | # PyBuilder
77 | .pybuilder/
78 | target/
79 |
80 | # Jupyter Notebook
81 | .ipynb_checkpoints
82 |
83 | # IPython
84 | profile_default/
85 | ipython_config.py
86 |
87 | # pyenv
88 | # For a library or package, you might want to ignore these files since the code is
89 | # intended to run in multiple environments; otherwise, check them in:
90 | # .python-version
91 |
92 | # pipenv
93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
96 | # install all needed dependencies.
97 | #Pipfile.lock
98 |
99 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
100 | __pypackages__/
101 |
102 | # Celery stuff
103 | celerybeat-schedule
104 | celerybeat.pid
105 |
106 | # SageMath parsed files
107 | *.sage.py
108 |
109 | # Environments
110 | .env
111 | .venv
112 | env/
113 | venv/
114 | ENV/
115 | env.bak/
116 | venv.bak/
117 |
118 | # Spyder project settings
119 | .spyderproject
120 | .spyproject
121 |
122 | # Rope project settings
123 | .ropeproject
124 |
125 | # mkdocs documentation
126 | /site
127 |
128 | # mypy
129 | .mypy_cache/
130 | .dmypy.json
131 | dmypy.json
132 |
133 | # Pyre type checker
134 | .pyre/
135 |
136 | # pytype static type analyzer
137 | .pytype/
138 |
139 | # Cython debug symbols
140 | cython_debug/
141 |
142 |
143 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
144 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
145 |
146 | # User-specific stuff
147 | .idea/**/workspace.xml
148 | .idea/**/tasks.xml
149 | .idea/**/usage.statistics.xml
150 | .idea/**/dictionaries
151 | .idea/**/shelf
152 |
153 | # Generated files
154 | .idea/**/contentModel.xml
155 |
156 | # Sensitive or high-churn files
157 | .idea/**/dataSources/
158 | .idea/**/dataSources.ids
159 | .idea/**/dataSources.local.xml
160 | .idea/**/sqlDataSources.xml
161 | .idea/**/dynamic.xml
162 | .idea/**/uiDesigner.xml
163 | .idea/**/dbnavigator.xml
164 |
165 | # Gradle
166 | .idea/**/gradle.xml
167 | .idea/**/libraries
168 |
169 | # Gradle and Maven with auto-import
170 | # When using Gradle or Maven with auto-import, you should exclude module files,
171 | # since they will be recreated, and may cause churn. Uncomment if using
172 | # auto-import.
173 | # .idea/artifacts
174 | # .idea/compiler.xml
175 | # .idea/jarRepositories.xml
176 | # .idea/modules.xml
177 | # .idea/*.iml
178 | # .idea/modules
179 | # *.iml
180 | # *.ipr
181 |
182 | # CMake
183 | cmake-build-*/
184 |
185 | # Mongo Explorer plugin
186 | .idea/**/mongoSettings.xml
187 |
188 | # File-based project format
189 | *.iws
190 |
191 | # IntelliJ
192 | out/
193 |
194 | # mpeltonen/sbt-idea plugin
195 | .idea_modules/
196 |
197 | # JIRA plugin
198 | atlassian-ide-plugin.xml
199 |
200 | # Cursive Clojure plugin
201 | .idea/replstate.xml
202 |
203 | # Crashlytics plugin (for Android Studio and IntelliJ)
204 | com_crashlytics_export_strings.xml
205 | crashlytics.properties
206 | crashlytics-build.properties
207 | fabric.properties
208 |
209 | # Editor-based Rest Client
210 | .idea/httpRequests
211 |
212 | # Android studio 3.1+ serialized cache file
213 | .idea/caches/build_file_checksums.ser
214 |
--------------------------------------------------------------------------------
/.readthedocs.yml:
--------------------------------------------------------------------------------
1 | # .readthedocs.yaml
2 | # Read the Docs configuration file
3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
4 |
5 | # Required
6 | version: 2
7 |
8 | # Set the OS, Python version and other tools you might need
9 | build:
10 | os: ubuntu-22.04
11 | tools:
12 | python: "3.11"
13 | # You can also specify other tool versions:
14 | # nodejs: "19"
15 | # rust: "1.64"
16 | # golang: "1.19"
17 |
18 | # Build documentation in the "docs/" directory with Sphinx
19 | sphinx:
20 | configuration: docs/conf.py
21 |
22 | # Optionally build your docs in additional formats such as PDF and ePub
23 | # formats:
24 | # - pdf
25 | # - epub
26 |
27 | # Optional but recommended, declare the Python requirements required
28 | # to build your documentation
29 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
30 | python:
31 | install:
32 | - requirements: requirements.txt
33 |
--------------------------------------------------------------------------------
/CHANGELOG.rst:
--------------------------------------------------------------------------------
1 | =========
2 | Changelog
3 | =========
4 |
5 | All notable changes to this project will be documented in this file.
6 |
7 | Version 0.5.4 (September 7, 2024)
8 | ---------------------------------
9 |
10 | **Added**
11 |
12 | - Post bet support.
13 |
14 | - 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.
15 | - To denote a post bet, it must be passed alongside ``raw_blinds_or_straddles`` variable during state construction.
16 |
17 | - 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}``.
18 |
19 | - ``pokerkit.notation.HandHistory.state_actions`` is a new alias for ``pokerkit.notation.HandHistory.iter_state_actions()``.
20 |
21 | **Deprecated**
22 |
23 | - ``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.
24 |
25 | Version 0.5.3 (September 1, 2024)
26 | ---------------------------------
27 |
28 | **Changed**
29 |
30 | - Fix incorrect implementation of ``pokerkit.hands.StandardBadugi.from_game``.
31 |
32 | Version 0.5.2 (June 13, 2024)
33 | -----------------------------
34 |
35 | **Changed**
36 |
37 | - Allow ``numbers.Number`` like ``decimal.Decimal`` to be used as chip values. While documented as allowed, usage of non-``int`` or non-``float`` used to result in error.
38 | - The main pot is pushed first, followed by side pots (reverse was true previously).
39 | - Chips pushing operation is more fine-grained in that each operation pushes a portion of the main/side pot should there be multiple boards or hand types.
40 | - Removed ``pokerkit.state.ChipsPushing.raked_amount`` attribute.
41 | - Removed ``pokerkit.state.ChipsPushing.unraked_amount`` property.
42 |
43 | **Added**
44 |
45 | - Added ``pokerkit.state.ChipsPushing.pot_index``, ``pokerkit.state.ChipsPushing.board_index``, and ``pokerkit.state.ChipsPushing.hand_type_index`` attributes to provide information on what portion of the pot was pushed.
46 | - Added ICM calculation ``pokerkit.analysis.calculate_icm`` function.
47 |
48 | Version 0.5.1 (May 24, 2024)
49 | ----------------------------
50 |
51 | **Added**
52 |
53 | - Add standard error property ``pokerkit.analysis.Statistics.payoff_stderr`` to statistics.
54 |
55 | Version 0.5.0 (April 25, 2024)
56 | ------------------------------
57 |
58 | This version release introduces a number of backward incompatible changes. Please read the below content carefully!
59 |
60 | **Summary of changes**
61 |
62 | - Minor cleanup that **may** break older code.
63 | - Option to choose cash-game vs. tournament (default) mode (defaults to tournament mode).
64 |
65 | - Unlike in tourneys, in cash-games, players can select the number of runouts during all-in situations.
66 |
67 | - Option to choose the number of runouts during all-in situations (disabled in tournament mode).
68 |
69 | - In theory, people choose number of runouts before they show their hands. But, this isn't always followed. It is also unclear who must select the number of runouts first. As such, after all-in, when showdown takes place,
70 |
71 | - Multi-board games.
72 | - More degree of freedom in hole dealing/showdown order.
73 | - Docstring and documentation overhaul.
74 | - Unknown starting stacks can be expressed with ``math.inf``.
75 | - More flexible raking system.
76 |
77 | **Changed**
78 |
79 | - The parameters ``divmod``, and ``rake`` for relevant poker game/state initialization methods are now keyword-only arguments. Before, one could supply them as positional arguments but this is no longer allowed!
80 | - ``pokerkit.state.State.board_cards`` (previously ``list[Card]``) is now of type ``list[list[Card]]``.
81 |
82 | - For example, if an all-in happens on the flop (AsKsQs) and is run twice (JsTs, JhTh), ``state.board_cards == [[As], [Ks], [Qs], [Js, Jh], [Ts, Th]]``. Or, when double board omaha is played, something like ``state.board_cards == [[??, ??], [??, ??], [??, ??]]`` will develop after the flop.
83 | - The function signatures for ``pokerkit.state.State.get_hand``, ``pokerkit.state.State.get_up_hand``, and ``pokerkit.state.State.get_up_hands`` now also requires the ``board_index`` to be supplied.
84 | - The properties/method ``pokerkit.state.State.reserved_cards``, ``pokerkit.state.State.cards_in_play``, ``pokerkit.state.State.cards_not_in_play``, and ``pokerkit.state.State.get_dealable_cards(deal_count: int)`` now return ``Iterator[Card]`` instead of ``tuple[Card, ...]``.
85 | - The method triplets for the hole dealing and showdown operation ``pokerkit.state.State.verify_hole_dealing()``, ``pokerkit.state.State.can_deal_hole()``, ``pokerkit.state.State.deal_hole()``, ``pokerkit.state.State.verify_hole_cards_showing_or_mucking()``, ``pokerkit.state.State.can_show_or_muck_hole_cards()``, and ``pokerkit.state.State.show_or_muck_hole_cards()`` also accepts an optional positional argument ``player_index`` to control the dealee, or the showdown performer. The verifiers also returns a player dealt if the dealee is not specified.
86 |
87 | - The card-burning-related methods ``pokerkit.state.State.verify_card_burning``, ``pokerkit.state.State.can_burn_card``, and ``pokerkit.state.State.burn_card`` also accept a singleton card iterable.
88 | - The ``pokerkit.state.State.all_in_show_status`` was renamed to ``pokerkit.state.State.all_in_status``.
89 | - Renamed ``pokerkit.state.ChipsPushing.rake`` to ``pokerkit.state.ChipsPushing.raked_amount``.
90 | - The attribute ``pokerkit.state.Pot.amount`` is now a property and no longer a parameter during initialization.
91 |
92 | **Added**
93 |
94 | - New enum class ``pokerkit.state.State.Mode`` for setting tournament/cash-game mode while initializing poker states.
95 |
96 | - Tournament mode: ``pokerkit.state.Mode.TOURNAMENT``
97 | - Cash-game mode: ``pokerkit.state.Mode.CASH_GAME``
98 |
99 | - In all-in situations, players have a chance to choose the number of runouts during showdown.
100 |
101 | - New parameter ``mode`` in relevant poker game/state initialization methods. It defaults to tournament mode.
102 | - New parameter ``starting_board_count`` in relevant poker game/state initialization methods. It defaults to ``1``. This allow multiple boards to be dealt if wished.
103 | - New automation ``pokerkit.state.State.Automation.RUNOUT_COUNT_SELECTION`` which instructs PokerKit to carry out only one run-out.
104 | - New ``pokerkit.state.RunoutCountSelection`` operation.
105 |
106 | - Arguments: ``runout_count`` and ``player_index`` who gives out the selection.
107 | - Querier: ``pokerkit.state.State.can_select_runout_count(player_index: int | None = None, runout_count: int | None = None)``.
108 | - Validator: ``pokerkit.state.State.verify_runout_count_selection(player_index: int | None = None, runout_count: int | None = None)``.
109 | - Operator: ``pokerkit.state.State.select_runout_count(player_index: int | None = None, runout_count: int | None = None, *, commentary: str | None = None)``.
110 | - People who can select run count: ``pokerkit.state.State.runout_count_selector_indices``.
111 | - If ``runout_count`` are in disagreement among active players, only ``1`` runout is performed.
112 | - When multiple runs are selected, the state will be incompatible with the PHH file format, as it stands.
113 |
114 | - New attributes ``pokerkit.state.State.street_return_index`` and ``pokerkit.state.State.street_return_count`` that internally keeps track what street to return to and how many times to do so during multiple runouts.
115 | - New attribute ``pokerkit.state.State.runout_count`` that shows the players' preferences on the number of runouts. It maybe ``None`` in which case the runout selection was skipped due to the state being of tournament mode or all players showed no preference by passing in ``None`` (or leaving empty) for the ``runout_count`` argument during the corresponding method call of ``pokerkit.state.select_runout_count()``.
116 | - New attributes ``pokerkit.state.State.board_count`` and ``pokerkit.state.State.board_indices`` on the number of boards and the range of its indices. The number of boards is at least ``1`` but may be more due to multiple runouts or the variant being played.
117 | - New method ``pokerkit.state.State.get_board_cards(board_index: int)`` on getting the ``board_index``'th board.
118 |
119 | - The maximum number of boards is either equal to the number of boards of the variant or (in case of multiple runouts) the product of it and the number of runouts.
120 |
121 | - New attribute ``pokerkit.state.State.runout_count_selector_statuses`` that keeps track of who can select the number of runouts.
122 | - New attribute ``pokerkit.state.State.runout_count_selection_flag`` that keeps track of whether the runout count selection has been carried out.
123 | - In ``pokerkit.utilities.rake``, added parameters ``state``, ``cap``, and ``no_flop_no_drop``, and ``rake`` is now renamed as ``percentage`` and is a keyword parameter.
124 | - New attributes ``pokerkit.state.Pot.raked_amount`` and ``pokerkit.state.Pot.unraked_amount`` that gives the raked and the unraked amounts of the pot.
125 | - New property ``pokerkit.state.ChipsPushing.unraked_amount``.
126 | - New attribute ``pokerkit.state.payoffs`` for keeping track of payoffs (rewards).
127 |
128 | Version 0.4.17 (April 9, 2024)
129 | ------------------------------
130 |
131 | **Changed**
132 |
133 | - Make error/warning messages more descriptive.
134 |
135 | **Added**
136 |
137 | - Censored hole cards ``pokerkit.state.State.get_censored_hole_cards()``.
138 | - Turn index ``pokerkit.state.State.turn_index``.
139 |
140 | Version 0.4.16 (April 5, 2024)
141 | ------------------------------
142 |
143 | **Added**
144 |
145 | - Restore action notation ``pn sm -`` for showing hole cards.
146 |
147 | Version 0.4.15 (March 29, 2024)
148 | -------------------------------
149 |
150 | **Added**
151 |
152 | - Raise error for ACPC protocol converter when hole cards unknown.
153 | - PHH to Pluribus protocol converter.
154 |
155 | Version 0.4.14 (March 25, 2024)
156 | -------------------------------
157 |
158 | **Added**
159 |
160 | - Analysis module
161 |
162 | - Range parser ``pokerkit.analysis.parse_range`` (e.g. ``"AKs,T8o-KJo,6h5h,A2+"``).
163 | - Equity calculator ``pokerkit.analysis.calculate_equities``.
164 | - Hand strength calculator ``pokerkit.analysis.calculate_hand_strength``.
165 | - Player statistics ``pokerkit.analysis.Statistics``.
166 |
167 | Version 0.4.13 (March 23, 2024)
168 | -------------------------------
169 |
170 | **Changed**
171 |
172 | - Renamed ``pokerkit.state.State.all_in_show_status`` to ``pokerkit.state.State.all_in_status``.
173 |
174 | **Added**
175 |
176 | - ``pokerkit.state.State.reserved_cards``
177 | - ``pokerkit.state.State.cards_in_play``
178 | - ``pokerkit.state.State.cards_not_in_play``
179 |
180 | Version 0.4.12 (March 21, 2024)
181 | -------------------------------
182 |
183 | **Removed**
184 |
185 | - Remove non-compliant action notation ``pn sm -`` for showing hole cards.
186 |
187 | **Added**
188 |
189 | - Commentary for state actions.
190 | - User-defined field support for PHH.
191 | - PHH to ACPC protocol converter
192 |
193 | Version 0.4.11 (March 15, 2024)
194 | -------------------------------
195 |
196 | **Added**
197 |
198 | - Deuce-to-seven badugi hand lookup/evaluator.
199 |
200 | Version 0.4.10 (February 11, 2024)
201 | ----------------------------------
202 |
203 | **Added**
204 |
205 | - ``pokerkit.state.State.pot_amounts`` for iterating through main/side pot amounts.
206 |
207 | **Changed**
208 |
209 | - Forbid showdown without specifying cards if unknown hole cards are dealt.
210 |
211 | Version 0.4.9 (January 28, 2024)
212 | --------------------------------
213 |
214 | **Changed**
215 |
216 | - New field ``rake`` for ``pokerkit.notation.HandHistory`` when constructing games/states.
217 |
218 | Version 0.4.8 (January 22, 2024)
219 | --------------------------------
220 |
221 | **Changed**
222 |
223 | - New action notation ``pn sm -`` for showing hole cards.
224 | - ``pokerkit.notation.HandHistory.iter_state_actions`` for iterating through states with actions.
225 |
226 | Version 0.4.7 (January 20, 2024)
227 | --------------------------------
228 |
229 | **Changed**
230 |
231 | - If there are multiple pots (main + side), ``pokerkit.state.State.push_chips`` must be called multiple times.
232 | - Custom automations are passed through the constructor for ``pokerkit.notation.HandHistory``.
233 | - Support rakes.
234 |
235 | Version 0.4.6 (January 8, 2024)
236 | -------------------------------
237 |
238 | **Changed**
239 |
240 | - Collapse pots (main + side) that have the same players in the ``pokerkit.state.State.pots`` property.
241 | - Allow default automations to be overridden in ``pokerkit.notation.HandHistory.create_game`` and ``pokerkit.notation.HandHistory.create_game``.
242 |
243 | Version 0.4.5 (January 4, 2024)
244 | -------------------------------
245 |
246 | **Changed**
247 |
248 | - Fix incorrect type annotation for class attribute ``optional_field_names`` in ``optional_field_names`` in``pokerkit.notation.HandHistory``.
249 | - Operation queries also catch ``UserWarning``.
250 |
251 | Version 0.4.4 (January 1, 2024)
252 | -------------------------------
253 |
254 | **Added**
255 |
256 | - Add class attributes ``game_field_names`` and ``ignored_field_names`` to ``pokerkit.notation.HandHistory``.
257 |
258 | **Changed**
259 |
260 | - Remove class attributes ``game_field_names`` and ``ignored_field_names`` from ``pokerkit.notation.HandHistory``
261 |
262 | Version 0.4.3 (December 17, 2023)
263 | ---------------------------------
264 |
265 | **Added**
266 |
267 | - The new .phh optional fields: ``time_zone``
268 |
269 | Version 0.4.2 (December 15, 2023)
270 | ---------------------------------
271 |
272 | **Added**
273 |
274 | - New .phh optional fields: ``time``, ``time_limit``, ``time_banks``, ``level``.
275 |
276 | Version 0.4.1 (December 13, 2023)
277 | ---------------------------------
278 |
279 | **Added**
280 |
281 | - New .phh optional fields: ``url``, ``city``, ``region``, ``postal_code``,
282 | ``country``.
283 |
284 | **Changed**
285 |
286 | - ``ante_trimming_status`` is now an optional field for .phh files.
287 |
288 | Version 0.4.0 (December 11, 2023)
289 | ---------------------------------
290 |
291 | **Changed**
292 |
293 | - When not enough cards to deal everybody's hole cards, a board dealing is done.
294 | - Showdown can specify what cards the player showed.
295 | - More generous state operations when it comes to cards. Some things that were errors are now warnings.
296 | - When all-in, cards are shown via ``show_or_muck_hole_cards``.
297 | - ``None`` is no longer ``ValuesLike`` or ``CardsLike``.
298 |
299 | **Added**
300 |
301 | - Cards with unknown rank or suit.
302 | - ``float`` compatibility (without static typing support).
303 | - Poker action notation support.
304 | - Poker hand history file format (.phh) support.
305 |
306 | Version 0.3.2 (December 4, 2023)
307 | --------------------------------
308 |
309 | **Changed**
310 |
311 | - When saving state configuration, ``player_count`` is not saved.
312 |
313 | Version 0.3.1 (December 4, 2023)
314 | --------------------------------
315 |
316 | **Added**
317 |
318 | - Allow state configuration to be saved.
319 |
320 | Version 0.3.0 (October 7, 2023)
321 | -------------------------------
322 |
323 | **Changed**
324 |
325 | - Call ``unittest.main`` in unit test files when executed as ``__main__``.
326 | - Move the ``automations`` parameter to be the first parameter of ``pokerkit.state.State``.
327 |
328 | Version 0.2.1 (September 27, 2023)
329 | ----------------------------------
330 |
331 | **Changed**
332 |
333 | - Make ``pokerkit.state.Operation`` available as ``pokerkit.Operation`` by importing it in ``pokerkit.__init__``.
334 |
335 | Version 0.2.0 (September 10, 2023)
336 | ----------------------------------
337 |
338 | **Changed**
339 |
340 | - Limit the maximum number of completions, bets, or raises to 4 in the pre-configured Fixed-limit deuce-to-seven triple draw and Fixed-limit badugi variants.
341 | - Flip antes just like blinds during heads-up play (in the case of big blind antes).
342 | - Also reshuffle all discarded cards (including from the current draw round) along with mucked and burned cards when the deck runs out. Previously, discarded cards from the same draw round was excluded.
343 | - Rename ``pokerkit.state.State.verify_card_availability_making`` to ``pokerkit.state.State.verify_cards_availability_making``.
344 |
345 | **Added**
346 |
347 | - Add more unit tests and doctests to achieve 99% code coverage.
348 |
349 | Version 0.1.1 (August 29, 2023)
350 | -------------------------------
351 |
352 | **Bugfixes**
353 |
354 | - Fix ``AssertionError`` being raised in certain scenarios after discards are made when the state was configured to automatically deal with hole cards.
355 |
356 | **Changed**
357 |
358 | - When the dealer deals hole cards after standing pat or discarding, an explicit ``ValueError`` is raised unless every player has stood pat or discarded.
359 |
360 | Version 0.1.0 (August 27, 2023)
361 | -------------------------------
362 |
363 | **Added**
364 |
365 | - ``pokerkit.state.Operation`` abstract base class for all operation classes.
366 | - ``pokerkit.utilities.shuffled`` helper function.
367 | - ``pokerkit.state.State.discarded_cards`` to keep track of discarded cards.
368 | - ``pokerkit.state.State.street_count`` property.
369 | - ``pokerkit.state.State.street_indices`` property.
370 |
371 | **Changed**
372 |
373 | - ``pokerkit.state.State`` now also accepts ``pokerkit.utilities.ValuesLike`` instances as arguments for various parameters.
374 | - ``pokerkit.state.State`` requires ``player_count`` argument to be passed during initialization.
375 | - Various operation classes such as ``pokerkit.state.State.AntePosting`` moved to ``pokerkit.state`` and is no longer a nested class of ``pokerkit.state.State``.
376 | - Renamed ``pokerkit.lookups.RegularLowLookup`` to ``pokerkit.lookups.RegularLookup`` for enhanced consistency.
377 | - Renamed ``pokerkit.state.State.burned_cards`` to ``pokerkit.state.State.burn_cards``.
378 | - Renamed ``pokerkit.state.State.verify_card_availabilities`` to ``pokerkit.state.State.verify_card_availability_making``.
379 | - Changed the property ``pokerkit.state.State.available_cards`` to method ``pokerkit.state.State.get_available_cards``.
380 | - Cards can be dealt from the mucked cards or burn cards if the deck is empty.
381 | - Warning is printed if cards are dealt from burn cards without any good reason.
382 |
383 | Version 0.0.2 (August 17, 2023)
384 | -------------------------------
385 |
386 | **Added**
387 |
388 | - Introduce ``pokerkit.utilities.CardsLike`` and ``pokerkit.utilities.ValuesLike`` type aliases to simplify type annotations of various methods.
389 |
390 | Version 0.0.1 (August 7, 2023)
391 | ------------------------------
392 |
393 | **Changed**
394 |
395 | - Modify the methods that only accept an iterable of ``Card`` so they can accept any card-like object.
396 | - Make the protected attributes of the instances of the ``Hand`` type and its descendants public.
397 | - Move ``pokerkit.state.State._clean_cards`` and ``pokerkit.games.Game._clean_values`` to ``pokerkit.utilities``.
398 |
399 | Version 0.0.0 (August 2, 2023)
400 | ------------------------------
401 |
402 | **Initial Release**
403 |
--------------------------------------------------------------------------------
/CONTRIBUTING.rst:
--------------------------------------------------------------------------------
1 | ============
2 | Contributing
3 | ============
4 |
5 | Thanks for considering contributing to PokerKit! Your contributions are greatly appreciated, and help make PokerKit a better tool for everyone in the Poker AI and research communities.
6 |
7 | Setting up Your Development Environment
8 | ---------------------------------------
9 |
10 | 1. Fork the PokerKit repository on GitHub.
11 | 2. Clone your fork locally: ``git clone git@github.com:uoftcprg/pokerkit.git``
12 | 3. Setup virtual environment: ``python -m venv venv``
13 | 4. Activate the virtual environment: ``source venv/bin/activate``
14 | 5. Install requirements: ``pip install -r requirements.txt``
15 | 6. Create a branch for your changes: ``git checkout -b branch-name``
16 |
17 | Making Changes
18 | --------------
19 |
20 | When making changes, please follow these guidelines:
21 |
22 | - Always write your code in compliance with `PEP8 `_.
23 | - Write unit tests for your changes, and make sure all tests pass before submitting a pull request.
24 | - Document your changes in the code and update the `README `_ file if necessary.
25 | - After making changes, please validate your changes.
26 |
27 | 1. Run style checking: ``flake8 pokerkit``
28 | 2. Run static type checking with ``--strict`` flag: ``mypy --strict pokerkit``
29 | 3. Run checks for missing docstrings: ``interrogate -f 100 -i -m -n -p -s -r '^\w+TestCase' pokerkit``
30 | 4. Run unit tests: ``python -m unittest``
31 | 5. Run doctests: ``python -m doctest pokerkit/*.py``
32 |
33 | Submitting a Pull Request
34 | -------------------------
35 |
36 | 1. Commit your changes: ``git commit -am 'Add some feature'``
37 | 2. Push to the branch: ``git push origin branch-name``
38 | 3. Submit a pull request to the ``main`` branch in the PokerKit repository.
39 |
40 | Before submitting your pull request, please make sure the mypy static type checking with ``--strict`` flag, flake8, doctests, unit tests pass, and your code adheres to `PEP8 `_.
41 |
42 | After Your Pull Request Is Merged
43 | ---------------------------------
44 |
45 | After your pull request is merged, you can safely delete your branch and pull the changes from the main repository:
46 |
47 | - Delete the remote branch on GitHub: ``git push origin --delete branch-name``
48 | - Check out the main branch: ``git checkout main``
49 | - Delete the local branch: ``git branch -d branch-name``
50 | - Update your main with the latest upstream version: ``git pull upstream main``
51 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 University of Toronto Computer Poker Student Research Group
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | ========
2 | PokerKit
3 | ========
4 |
5 | PokerKit is an open-source software library, written in pure Python, for simulating games, evaluating hands, and facilitating statistical analysis, developed by the University of Toronto Computer Poker Student Research Group. PokerKit supports an extensive array of poker variants and it provides a flexible architecture for users to define their custom games. These facilities are exposed via an intuitive unified high-level programmatic API. The library can be used in a variety of use cases, from poker AI development, and tool creation, to online poker casino implementation. PokerKit's reliability has been established through static type checking, extensive doctests, and unit tests, achieving 99% code coverage.
6 |
7 | Features
8 | --------
9 |
10 | * Extensive poker game logic for major and minor poker variants.
11 | * High-speed hand evaluations.
12 | * Customizable game states and parameters.
13 | * Robust implementation with static type checking and extensive unit tests and doctests.
14 |
15 | Installation
16 | ------------
17 |
18 | The PokerKit library requires Python Version 3.11 or above and can be installed using pip:
19 |
20 | .. code-block:: bash
21 |
22 | pip install pokerkit
23 |
24 | Usages
25 | ------
26 |
27 | Example usages of PokerKit is shown below.
28 |
29 | Multi-Runout in an All-In Situation
30 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
31 |
32 | Below shows the 4-runout hand between Phil Hellmuth and the Loose Cannon Ernest Wiggins.
33 |
34 | Link: https://youtu.be/cnjJv7x0HMY?si=4l05Ez7lQVczt8DI&t=638
35 |
36 | Note that the starting stacks for some players are set to be ``math.inf`` as they are not mentioned.
37 |
38 | .. code-block:: python
39 |
40 | from math import inf
41 |
42 | from pokerkit import Automation, Mode, NoLimitTexasHoldem
43 |
44 | state = NoLimitTexasHoldem.create_state(
45 | # Automations
46 | (
47 | Automation.ANTE_POSTING,
48 | Automation.BET_COLLECTION,
49 | Automation.BLIND_OR_STRADDLE_POSTING,
50 | Automation.HOLE_CARDS_SHOWING_OR_MUCKING,
51 | Automation.HAND_KILLING,
52 | Automation.CHIPS_PUSHING,
53 | Automation.CHIPS_PULLING,
54 | ),
55 | False, # Uniform antes?
56 | {-1: 600}, # Antes
57 | (200, 400, 800), # Blinds or straddles
58 | 400, # Min-bet
59 | (inf, 116400, 86900, inf, 50000, inf), # Starting stacks
60 | 6, # Number of players
61 | mode=Mode.CASH_GAME,
62 | )
63 |
64 | Below are the pre-flop dealings and actions.
65 |
66 | .. code-block:: python
67 |
68 | state.deal_hole('JsTh') # Tony G
69 | state.deal_hole('Ah9d') # Hellmuth
70 | state.deal_hole('KsKc') # Wiggins
71 | state.deal_hole('5c2h') # Negreanu
72 | state.deal_hole('6h5h') # Brunson
73 | state.deal_hole('6s3s') # Laak
74 |
75 | state.fold() # Negreanu
76 | state.complete_bet_or_raise_to(2800) # Brunson
77 | state.fold() # Laak
78 | state.check_or_call() # Tony G
79 | state.complete_bet_or_raise_to(12600) # Hellmuth
80 | state.check_or_call() # Wiggins
81 | state.check_or_call() # Brunson
82 | state.check_or_call() # Tony G
83 |
84 | Below are the flop dealing and actions.
85 |
86 | .. code-block:: python
87 |
88 | state.burn_card('??')
89 | state.deal_board('9hTs9s')
90 |
91 | state.check_or_call() # Tony G
92 | state.complete_bet_or_raise_to(17000) # Hellmuth
93 | state.complete_bet_or_raise_to(36000) # Wiggins
94 | state.fold() # Brunson
95 | state.fold() # Tony G
96 | state.complete_bet_or_raise_to(103800) # Hellmuth
97 | state.check_or_call() # Wiggins
98 |
99 | Below is selecting the number of runouts.
100 |
101 | .. code-block:: python
102 |
103 | state.select_runout_count(4) # Hellmuth
104 | state.select_runout_count(None) # Wiggins
105 |
106 | Below is the first runout.
107 |
108 | .. code-block:: python
109 |
110 | state.burn_card('??')
111 | state.deal_board('Jh') # Turn
112 | state.burn_card('??')
113 | state.deal_board('Ad') # River
114 |
115 | Below is the second runout.
116 |
117 | .. code-block:: python
118 |
119 | state.burn_card('??')
120 | state.deal_board('Kh') # Turn
121 | state.burn_card('??')
122 | state.deal_board('3c') # River
123 |
124 | Below is the third runout.
125 |
126 | .. code-block:: python
127 |
128 | state.burn_card('??')
129 | state.deal_board('7s') # Turn
130 | state.burn_card('??')
131 | state.deal_board('8s') # River
132 |
133 | Below is the fourth runout.
134 |
135 | .. code-block:: python
136 |
137 | state.burn_card('??')
138 | state.deal_board('Qc') # Turn
139 | state.burn_card('??')
140 | state.deal_board('Kd') # River
141 |
142 | Below are the final stacks.
143 |
144 | .. code-block:: python
145 |
146 | print(state.stacks) # [inf, 79400, 149700, inf, 37400, inf]
147 |
148 | A Sample No-Limit Texas Hold'em Hand
149 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
150 |
151 | Below shows the first televised million-dollar pot between Tom Dwan and Phil Ivey.
152 |
153 | Link: https://youtu.be/GnxFohpljqM
154 |
155 | Note that the starting stack of Patrik Antonius is set to be ``math.inf`` as it is not mentioned.
156 |
157 | .. code-block:: python
158 |
159 | from math import inf
160 |
161 | from pokerkit import Automation, NoLimitTexasHoldem
162 |
163 | state = NoLimitTexasHoldem.create_state(
164 | # Automations
165 | (
166 | Automation.ANTE_POSTING,
167 | Automation.BET_COLLECTION,
168 | Automation.BLIND_OR_STRADDLE_POSTING,
169 | Automation.HOLE_CARDS_SHOWING_OR_MUCKING,
170 | Automation.HAND_KILLING,
171 | Automation.CHIPS_PUSHING,
172 | Automation.CHIPS_PULLING,
173 | ),
174 | True, # Uniform antes?
175 | 500, # Antes
176 | (1000, 2000), # Blinds or straddles
177 | 2000, # Min-bet
178 | (1125600, inf, 553500), # Starting stacks
179 | 3, # Number of players
180 | )
181 |
182 | Below are the pre-flop dealings and actions.
183 |
184 | .. code-block:: python
185 |
186 | state.deal_hole('Ac2d') # Ivey
187 | state.deal_hole('????') # Antonius
188 | state.deal_hole('7h6h') # Dwan
189 |
190 | state.complete_bet_or_raise_to(7000) # Dwan
191 | state.complete_bet_or_raise_to(23000) # Ivey
192 | state.fold() # Antonius
193 | state.check_or_call() # Dwan
194 |
195 | Below are the flop dealing and actions.
196 |
197 | .. code-block:: python
198 |
199 | state.burn_card('??')
200 | state.deal_board('Jc3d5c')
201 |
202 | state.complete_bet_or_raise_to(35000) # Ivey
203 | state.check_or_call() # Dwan
204 |
205 | Below are the turn dealing and actions.
206 |
207 | .. code-block:: python
208 |
209 | state.burn_card('??')
210 | state.deal_board('4h')
211 |
212 | state.complete_bet_or_raise_to(90000) # Ivey
213 | state.complete_bet_or_raise_to(232600) # Dwan
214 | state.complete_bet_or_raise_to(1067100) # Ivey
215 | state.check_or_call() # Dwan
216 |
217 | Below is the river dealing.
218 |
219 | .. code-block:: python
220 |
221 | state.burn_card('??')
222 | state.deal_board('Jh')
223 |
224 | Below are the final stacks.
225 |
226 | .. code-block:: python
227 |
228 | print(state.stacks) # [572100, inf, 1109500]
229 |
230 | A Sample Short-Deck Hold'em Hand
231 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
232 |
233 | Below shows an all-in hand between Xuan and Phua.
234 |
235 | Link: https://youtu.be/QlgCcphLjaQ
236 |
237 | .. code-block:: python
238 |
239 | from pokerkit import Automation, NoLimitShortDeckHoldem
240 |
241 | state = NoLimitShortDeckHoldem.create_state(
242 | # Automations
243 | (
244 | Automation.ANTE_POSTING,
245 | Automation.BET_COLLECTION,
246 | Automation.BLIND_OR_STRADDLE_POSTING,
247 | Automation.HOLE_CARDS_SHOWING_OR_MUCKING,
248 | Automation.HAND_KILLING,
249 | Automation.CHIPS_PUSHING,
250 | Automation.CHIPS_PULLING,
251 | ),
252 | True, # Uniform antes?
253 | 3000, # Antes
254 | {-1: 3000}, # Blinds or straddles
255 | 3000, # Min-bet
256 | (495000, 232000, 362000, 403000, 301000, 204000), # Starting stacks
257 | 6, # Number of players
258 | )
259 |
260 | Below are the pre-flop dealings and actions.
261 |
262 | .. code-block:: python
263 |
264 | state.deal_hole('Th8h') # Badziakouski
265 | state.deal_hole('QsJd') # Zhong
266 | state.deal_hole('QhQd') # Xuan
267 | state.deal_hole('8d7c') # Jun
268 | state.deal_hole('KhKs') # Phua
269 | state.deal_hole('8c7h') # Koon
270 |
271 | state.check_or_call() # Badziakouski
272 | state.check_or_call() # Zhong
273 | state.complete_bet_or_raise_to(35000) # Xuan
274 | state.fold() # Jun
275 | state.complete_bet_or_raise_to(298000) # Phua
276 | state.fold() # Koon
277 | state.fold() # Badziakouski
278 | state.fold() # Zhong
279 | state.check_or_call() # Xuan
280 |
281 | Below is the flop dealing.
282 |
283 | .. code-block:: python
284 |
285 | state.burn_card('??')
286 | state.deal_board('9h6cKc')
287 |
288 | Below is the turn dealing.
289 |
290 | .. code-block:: python
291 |
292 | state.burn_card('??')
293 | state.deal_board('Jh')
294 |
295 | Below is the river dealing.
296 |
297 | .. code-block:: python
298 |
299 | state.burn_card('??')
300 | state.deal_board('Ts')
301 |
302 | Below are the final stacks.
303 |
304 | .. code-block:: python
305 |
306 | print(state.stacks) # [489000, 226000, 684000, 400000, 0, 198000]
307 |
308 | A Sample Pot-Limit Omaha Hold'em Hand
309 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
310 |
311 | Below shows the largest online poker pot ever played between Patrik Antonius and Viktor Blom.
312 |
313 | Link: https://youtu.be/UMBm66Id2AA
314 |
315 | .. code-block:: python
316 |
317 | from pokerkit import Automation, PotLimitOmahaHoldem
318 |
319 | state = PotLimitOmahaHoldem.create_state(
320 | # Automations
321 | (
322 | Automation.ANTE_POSTING,
323 | Automation.BET_COLLECTION,
324 | Automation.BLIND_OR_STRADDLE_POSTING,
325 | Automation.HOLE_CARDS_SHOWING_OR_MUCKING,
326 | Automation.HAND_KILLING,
327 | Automation.CHIPS_PUSHING,
328 | Automation.CHIPS_PULLING,
329 | ),
330 | True, # Uniform antes?
331 | 0, # Antes
332 | (500, 1000), # Blinds or straddles
333 | 1000, # Min-bet
334 | (1259450.25, 678473.5), # Starting stacks
335 | 2, # Number of players
336 | )
337 |
338 | Below are the pre-flop dealings and actions.
339 |
340 | .. code-block:: python
341 |
342 | state.deal_hole('Ah3sKsKh') # Antonius
343 | state.deal_hole('6d9s7d8h') # Blom
344 |
345 | state.complete_bet_or_raise_to(3000) # Blom
346 | state.complete_bet_or_raise_to(9000) # Antonius
347 | state.complete_bet_or_raise_to(27000) # Blom
348 | state.complete_bet_or_raise_to(81000) # Antonius
349 | state.check_or_call() # Blom
350 |
351 | Below are the flop dealing and actions.
352 |
353 | .. code-block:: python
354 |
355 | state.burn_card('??')
356 | state.deal_board('4s5c2h')
357 |
358 | state.complete_bet_or_raise_to(91000) # Antonius
359 | state.complete_bet_or_raise_to(435000) # Blom
360 | state.complete_bet_or_raise_to(779000) # Antonius
361 | state.check_or_call() # Blom
362 |
363 | Below is the turn dealing.
364 |
365 | .. code-block:: python
366 |
367 | state.burn_card('??')
368 | state.deal_board('5h')
369 |
370 | Below is the river dealing.
371 |
372 | .. code-block:: python
373 |
374 | state.burn_card('??')
375 | state.deal_board('9c')
376 |
377 | Below are the final stacks.
378 |
379 | .. code-block:: python
380 |
381 | print(state.stacks) # [1937923.75, 0.0]
382 |
383 | A Sample Fixed-Limit Deuce-To-Seven Lowball Triple Draw Hand
384 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
385 |
386 | Below shows a bad beat between Yockey and Arieh.
387 |
388 | Link: https://youtu.be/pChCqb2FNxY
389 |
390 | .. code-block:: python
391 |
392 | from pokerkit import Automation, FixedLimitDeuceToSevenLowballTripleDraw
393 |
394 | state = FixedLimitDeuceToSevenLowballTripleDraw.create_state(
395 | # Automations
396 | (
397 | Automation.ANTE_POSTING,
398 | Automation.BET_COLLECTION,
399 | Automation.BLIND_OR_STRADDLE_POSTING,
400 | Automation.HOLE_CARDS_SHOWING_OR_MUCKING,
401 | Automation.HAND_KILLING,
402 | Automation.CHIPS_PUSHING,
403 | Automation.CHIPS_PULLING,
404 | ),
405 | True, # Uniform antes?
406 | 0, # Antes
407 | (75000, 150000), # Blinds or straddles
408 | 150000, # Small-bet
409 | 300000, # Big-bet
410 | (1180000, 4340000, 5910000, 10765000), # Starting stacks
411 | 4, # Number of players
412 | )
413 |
414 | Below are the pre-flop dealings and actions.
415 |
416 | .. code-block:: python
417 |
418 | state.deal_hole('7h6c4c3d2c') # Yockey
419 | state.deal_hole('??????????') # Hui
420 | state.deal_hole('??????????') # Esposito
421 | state.deal_hole('AsQs6s5c3c') # Arieh
422 |
423 | state.fold() # Esposito
424 | state.complete_bet_or_raise_to() # Arieh
425 | state.complete_bet_or_raise_to() # Yockey
426 | state.fold() # Hui
427 | state.check_or_call() # Arieh
428 |
429 | Below are the first draw and actions.
430 |
431 | .. code-block:: python
432 |
433 | state.stand_pat_or_discard() # Yockey
434 | state.stand_pat_or_discard('AsQs') # Arieh
435 | state.burn_card('??')
436 | state.deal_hole('2hQh') # Arieh
437 |
438 | state.complete_bet_or_raise_to() # Yockey
439 | state.check_or_call() # Arieh
440 |
441 | Below are the second draw and actions.
442 |
443 | .. code-block:: python
444 |
445 | state.stand_pat_or_discard() # Yockey
446 | state.stand_pat_or_discard('Qh') # Arieh
447 | state.burn_card('??')
448 | state.deal_hole('4d') # Arieh
449 |
450 | state.complete_bet_or_raise_to() # Yockey
451 | state.check_or_call() # Arieh
452 |
453 | Below are the third draw and actions.
454 |
455 | .. code-block:: python
456 |
457 | state.stand_pat_or_discard() # Yockey
458 | state.stand_pat_or_discard('6s') # Arieh
459 | state.burn_card('??')
460 | state.deal_hole('7c') # Arieh
461 |
462 | state.complete_bet_or_raise_to() # Yockey
463 | state.check_or_call() # Arieh
464 |
465 | Below are the final stacks.
466 |
467 | .. code-block:: python
468 |
469 | print(state.stacks) # [0, 4190000, 5910000, 12095000]
470 |
471 | A Sample Badugi Hand
472 | ^^^^^^^^^^^^^^^^^^^^
473 |
474 | Below shows an example badugi hand from Wikipedia.
475 |
476 | Link: https://en.wikipedia.org/wiki/Badugi
477 |
478 | Note that the starting stacks are set to be ``math.inf`` as they are not mentioned.
479 |
480 | .. code-block:: python
481 |
482 | from math import inf
483 |
484 | from pokerkit import Automation, FixedLimitBadugi
485 |
486 | state = FixedLimitBadugi.create_state(
487 | # Automations
488 | (
489 | Automation.ANTE_POSTING,
490 | Automation.BET_COLLECTION,
491 | Automation.BLIND_OR_STRADDLE_POSTING,
492 | Automation.HAND_KILLING,
493 | Automation.CHIPS_PUSHING,
494 | Automation.CHIPS_PULLING,
495 | ),
496 | True, # Uniform antes?
497 | 0, # Antes
498 | (1, 2), # Blinds or straddles
499 | 2, # Small-bet
500 | 4, # Big-bet
501 | inf, # Starting stacks
502 | 4, # Number of players
503 | )
504 |
505 | Below are the pre-flop dealings and actions.
506 |
507 | .. code-block:: python
508 |
509 | state.deal_hole('????????') # Bob
510 | state.deal_hole('????????') # Carol
511 | state.deal_hole('????????') # Ted
512 | state.deal_hole('????????') # Alice
513 |
514 | state.fold() # Ted
515 | state.check_or_call() # Alice
516 | state.check_or_call() # Bob
517 | state.check_or_call() # Carol
518 |
519 | Below are the first draw and actions.
520 |
521 | .. code-block:: python
522 |
523 | state.stand_pat_or_discard('????') # Bob
524 | state.stand_pat_or_discard('????') # Carol
525 | state.stand_pat_or_discard('??') # Alice
526 | state.burn_card('??')
527 | state.deal_hole('????') # Bob
528 | state.deal_hole('????') # Carol
529 | state.deal_hole('??') # Alice
530 |
531 | state.check_or_call() # Bob
532 | state.complete_bet_or_raise_to() # Carol
533 | state.check_or_call() # Alice
534 | state.check_or_call() # Bob
535 |
536 | Below are the second draw and actions.
537 |
538 | .. code-block:: python
539 |
540 | state.stand_pat_or_discard('??') # Bob
541 | state.stand_pat_or_discard() # Carol
542 | state.stand_pat_or_discard('??') # Alice
543 | state.burn_card('??')
544 | state.deal_hole('??') # Bob
545 | state.deal_hole('??') # Alice
546 |
547 | state.check_or_call() # Bob
548 | state.complete_bet_or_raise_to() # Carol
549 | state.complete_bet_or_raise_to() # Alice
550 | state.fold() # Bob
551 | state.check_or_call() # Carol
552 |
553 | Below are the third draw and actions.
554 |
555 | .. code-block:: python
556 |
557 | state.stand_pat_or_discard('??') # Carol
558 | state.stand_pat_or_discard() # Alice
559 | state.burn_card('??')
560 | state.deal_hole('??') # Carol
561 |
562 | state.check_or_call() # Carol
563 | state.complete_bet_or_raise_to() # Alice
564 | state.check_or_call() # Carol
565 |
566 | Below is the showdown.
567 |
568 | .. code-block:: python
569 |
570 | state.show_or_muck_hole_cards('2s4c6d9h') # Alice
571 | state.show_or_muck_hole_cards('3s5d7c8h') # Carol
572 |
573 | Below are the final stacks.
574 |
575 | .. code-block:: python
576 |
577 | print(state.stacks) # [inf, inf, inf, inf]
578 | print(state.payoffs) # [-4, 20, 0, -16]
579 |
580 | Testing and Validation
581 | ----------------------
582 |
583 | PokerKit has extensive test coverage, passes mypy static type checking with strict mode, and has been validated through extensive use in real-life scenarios.
584 |
585 | Contributing
586 | ------------
587 |
588 | Contributions are welcome! Please read our Contributing Guide for more information.
589 |
590 | License
591 | -------
592 |
593 | PokerKit is distributed under the MIT license.
594 |
595 | Citing
596 | ------
597 |
598 | If you use PokerKit in your research, please cite our library:
599 |
600 | .. code-block:: bibtex
601 |
602 | @ARTICLE{10287546,
603 | author={Kim, Juho},
604 | journal={IEEE Transactions on Games},
605 | title={PokerKit: A Comprehensive Python Library for Fine-Grained Multi-Variant Poker Game Simulations},
606 | year={2023},
607 | volume={},
608 | number={},
609 | pages={1-8},
610 | doi={10.1109/TG.2023.3325637}}
611 |
--------------------------------------------------------------------------------
/TODOS.rst:
--------------------------------------------------------------------------------
1 | =====
2 | Todos
3 | =====
4 |
5 | Here are some of the features that are planned to be implemented in the future.
6 |
7 | - Fully comply with the Poker Hand History file format specs.
8 |
9 | - URL: https://arxiv.org/abs/2312.11753
10 |
11 | - Parser for the PokerStars hand history file format.
12 | - Fully pre-define all variants in the 2023 World Series of Poker Tournament Rules.
13 |
14 | - URL: https://www.wsop.com/2022/2023-WSOP-Tournament-Rules.pdf
15 | - Add mock games to the unit test for each variant.
16 |
17 | - Improved type annotations.
18 |
19 | - The code supports both ``int`` and ``float`` but type annotations for static type checking only support ``int``.
20 |
21 | - Sandbox mode
22 |
23 | - Do not care about errors
24 |
25 | - If both hole and board dealings are pending, card burning can be deferred so that one of the dealings is carried out before (for Courchevel).
26 | - In non-uniform ante situations (e.g. button ante, BB ante), the paid ante(s) does not impact the pot bet during pre-flop (after flop, ante contributions are also considered to calculate the pot value).
27 | - Faster hand evaluation for 6/7 card combinations.
28 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = .
9 | BUILDDIR = _build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/docs/_static/2022-WSOP-Live-Action-Rules.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WTEngineer/pockerkit_python/4ed4a492b7e56bffccd25364af0827147ff872c7/docs/_static/2022-WSOP-Live-Action-Rules.pdf
--------------------------------------------------------------------------------
/docs/_static/2022-WSOP-Tournament-Rules.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WTEngineer/pockerkit_python/4ed4a492b7e56bffccd25364af0827147ff872c7/docs/_static/2022-WSOP-Tournament-Rules.pdf
--------------------------------------------------------------------------------
/docs/_static/2023-WSOP-Live-Action-Rules.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WTEngineer/pockerkit_python/4ed4a492b7e56bffccd25364af0827147ff872c7/docs/_static/2023-WSOP-Live-Action-Rules.pdf
--------------------------------------------------------------------------------
/docs/_static/2023-WSOP-Tournament-Rules.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WTEngineer/pockerkit_python/4ed4a492b7e56bffccd25364af0827147ff872c7/docs/_static/2023-WSOP-Tournament-Rules.pdf
--------------------------------------------------------------------------------
/docs/_static/cards.drawio:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
--------------------------------------------------------------------------------
/docs/_static/cards.drawio.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WTEngineer/pockerkit_python/4ed4a492b7e56bffccd25364af0827147ff872c7/docs/_static/cards.drawio.png
--------------------------------------------------------------------------------
/docs/_static/hands.drawio:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/docs/_static/hands.drawio.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WTEngineer/pockerkit_python/4ed4a492b7e56bffccd25364af0827147ff872c7/docs/_static/hands.drawio.png
--------------------------------------------------------------------------------
/docs/_static/phases.drawio:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
--------------------------------------------------------------------------------
/docs/_static/phases.drawio.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WTEngineer/pockerkit_python/4ed4a492b7e56bffccd25364af0827147ff872c7/docs/_static/phases.drawio.png
--------------------------------------------------------------------------------
/docs/_static/protocol.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WTEngineer/pockerkit_python/4ed4a492b7e56bffccd25364af0827147ff872c7/docs/_static/protocol.pdf
--------------------------------------------------------------------------------
/docs/analysis.rst:
--------------------------------------------------------------------------------
1 | Statistical Analysis
2 | ====================
3 |
4 | PokerKit contains tools for various poker statistical analysis methods, compatible with any variant.
5 |
6 | Range Parsing
7 | -------------
8 |
9 | PokerKit can parse common range notations to come up with set of hole card combinations.
10 |
11 | .. code-block:: pycon
12 |
13 | >>> from pokerkit import *
14 | >>> parse_range('AKs')
15 | {frozenset({As, Ks}), frozenset({Kd, Ad}), frozenset({Kh, Ah}), frozenset({Ac, Kc})}
16 | >>> parse_range('22')
17 | {frozenset({2s, 2d}), frozenset({2d, 2h}), frozenset({2c, 2d}), frozenset({2s, 2h}), frozenset({2c, 2s}), frozenset({2c, 2h})}
18 | >>> parse_range('T9o') | parse_range('T9s') == parse_range('T9')
19 | True
20 | >>> parse_range('33', '44;55') == parse_range('33-55')
21 | True
22 | >>> parse_range('T9s') | parse_range('JTs') | parse_range('QJs') == parse_range('T9s-QJs')
23 | True
24 | >>> parse_range('T9s-QJs') | parse_range('T9o-QJo') == parse_range('T9-QJ')
25 | True
26 | >>> parse_range('J8s,J9s JTs') == parse_range('J8s+')
27 | True
28 | >>> parse_range('T9') - parse_range('T9s') == parse_range('T9o')
29 | True
30 | >>> parse_range('AdAh') < parse_range('AA')
31 | True
32 |
33 | The notations can be separated either by whitespace(s), comma(s) (``,``), and/or semicolon(s) (``;``). In PokerKit, a range is simply a set of frozen sets of cards and thus can be manipulated through set operations.
34 |
35 | Equity Calculations
36 | -------------------
37 |
38 | Monte Carlo simulations can be carried out to estimate player equities. The hole cards (if any), board cards (if any), total number of hole dealings (including those already dealt), total number of board dealings (including those already dealt), deck (including those already dealt), hand types (multiple if split-pot) must be supplied. The user must also supply the number of samples to use. Concurrency mechanisms can be leveraged by passing relevant executor to the equity calculator. Below show some equity calculations in Texas hold'em.
39 |
40 | .. code-block:: pycon
41 |
42 | >>> from concurrent.futures import ProcessPoolExecutor
43 | >>> from pokerkit import *
44 | >>> with ProcessPoolExecutor() as executor:
45 | ... calculate_equities(
46 | ... (
47 | ... parse_range('AK'),
48 | ... parse_range('22'),
49 | ... ),
50 | ... (),
51 | ... 2,
52 | ... 5,
53 | ... Deck.STANDARD,
54 | ... (StandardHighHand,),
55 | ... sample_count=10000,
56 | ... executor=executor,
57 | ... )
58 | ...
59 | [0.4807, 0.5193]
60 | >>> with ProcessPoolExecutor() as executor:
61 | ... calculate_equities(
62 | ... (
63 | ... parse_range('AsKs'),
64 | ... parse_range('AcJc'),
65 | ... ),
66 | ... Card.parse('Js8s5d'),
67 | ... 2,
68 | ... 5,
69 | ... Deck.STANDARD,
70 | ... (StandardHighHand,),
71 | ... sample_count=1000,
72 | ... executor=executor,
73 | ... )
74 | ...
75 | [0.485, 0.515]
76 | >>> with ProcessPoolExecutor() as executor:
77 | ... calculate_equities(
78 | ... (
79 | ... parse_range('2h2c'),
80 | ... parse_range('3h3c'),
81 | ... parse_range('AsKs'),
82 | ... ),
83 | ... Card.parse('QsJsTs'),
84 | ... 2,
85 | ... 5,
86 | ... Deck.STANDARD,
87 | ... (StandardHighHand,),
88 | ... sample_count=1000,
89 | ... executor=executor,
90 | ... )
91 | ...
92 | [0.0, 0.0, 1.0]
93 | >>> calculate_equities(
94 | ... (
95 | ... parse_range('33'),
96 | ... parse_range('33'),
97 | ... ),
98 | ... Card.parse('Tc8d6h4s'),
99 | ... 2,
100 | ... 5,
101 | ... Deck.STANDARD,
102 | ... (StandardHighHand,),
103 | ... sample_count=1000,
104 | ... )
105 | [0.5, 0.5]
106 |
107 | Hand Strength Calculations
108 | --------------------------
109 |
110 | Monte Carlo simulations can be carried out to estimate hand strengths: the odds of beating a single other hand chosen uniformly at random. Just like in equity calculations, the number of active players, hole cards, board cards (if any), total number of hole dealings (including those already dealt), total number of board dealings (including those already dealt), deck (including those already dealt), hand types (multiple if split-pot) must be supplied. The user must also supply the number of samples to use. Concurrency mechanisms can be leveraged by passing relevant executor to the equity calculator.
111 |
112 | .. code-block:: pycon
113 |
114 | >>> from concurrent.futures import ProcessPoolExecutor
115 | >>> from pokerkit import *
116 | >>> with ProcessPoolExecutor() as executor:
117 | ... calculate_hand_strength(
118 | ... 2,
119 | ... parse_range('AsKs'),
120 | ... Card.parse('Kc8h8d'),
121 | ... 2,
122 | ... 5,
123 | ... Deck.STANDARD,
124 | ... (StandardHighHand,),
125 | ... sample_count=1000,
126 | ... executor=executor,
127 | ... )
128 | ...
129 | 0.885
130 | >>> with ProcessPoolExecutor() as executor:
131 | ... calculate_hand_strength(
132 | ... 3,
133 | ... parse_range('AsKs'),
134 | ... Card.parse('QsJsTs'),
135 | ... 2,
136 | ... 5,
137 | ... Deck.STANDARD,
138 | ... (StandardHighHand,),
139 | ... sample_count=1000,
140 | ... executor=executor,
141 | ... )
142 | ...
143 | 1.0
144 | >>> calculate_hand_strength(
145 | ... 3,
146 | ... parse_range('3h3c'),
147 | ... Card.parse('3s3d2c'),
148 | ... 2,
149 | ... 5,
150 | ... Deck.STANDARD,
151 | ... (StandardHighHand,),
152 | ... sample_count=1000,
153 | ... )
154 | 1.0
155 |
156 | Player Statistics
157 | -----------------
158 |
159 | Hand histories can be analyzed through PokerKit. Information for each player is aggregated and can be accessed as attributes or properties.
160 |
161 | .. code-block:: python
162 |
163 | from pokerkit import *
164 |
165 | hh0 = ...
166 | hh1 = ...
167 | hh2 = ...
168 | ...
169 |
170 | ss = Statistics.from_hand_history(hh0, hh1, hh2, ...)
171 |
172 | print(ss['John Smith'].payoff_mean)
173 | print(ss['John Smith'].payoff_stdev)
174 | print(ss['Jane Doe'].payoff_mean)
175 | print(ss['Jane Doe'].payoff_stdev)
176 |
177 | Statistics can be merged.
178 |
179 | .. code-block:: python
180 |
181 | from pokerkit import *
182 |
183 | s0 = ...
184 | s1 = ...
185 | s2 = ...
186 | ...
187 |
188 | s = Statistics.merge(s0, s1, s2, ...)
189 |
190 | For a full list of accessible statistics, please see the API references for the class :class:`pokerkit.analysis.Statistics`.
191 |
--------------------------------------------------------------------------------
/docs/changelog.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../CHANGELOG.rst
2 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # For the full list of built-in configuration values, see the documentation:
4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
5 |
6 | import sys
7 | import os
8 |
9 | sys.path.insert(0, os.path.abspath('..'))
10 |
11 | # -- Project information -----------------------------------------------------
12 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
13 |
14 | project = 'PokerKit'
15 | copyright = '2023, University of Toronto Computer Poker Student Research Group'
16 | author = 'University of Toronto Computer Poker Student Research Group'
17 | release = '0.5.4'
18 |
19 | # -- General configuration ---------------------------------------------------
20 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
21 |
22 | extensions = ['sphinx.ext.autodoc', 'sphinx_rtd_theme']
23 |
24 | templates_path = ['_templates']
25 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
26 |
27 | # -- Options for HTML output -------------------------------------------------
28 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
29 |
30 | html_theme = 'sphinx_rtd_theme'
31 | html_static_path = ['_static']
32 |
--------------------------------------------------------------------------------
/docs/contributing.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../CONTRIBUTING.rst
2 |
--------------------------------------------------------------------------------
/docs/evaluation.rst:
--------------------------------------------------------------------------------
1 | Hand Evaluation
2 | ===============
3 |
4 | Not every poker software involves game simulations. Beyond its use of providing a simulated poker environment, PokerKit serves as a valuable resource for evaluating poker hands. It supports the largest selection of hand types in any mainstream open-source poker library. This makes it an invaluable tool for users interested in studying the statistical properties of poker, regardless of their interest in game simulations.
5 |
6 | The following is the list of hand types supported by PokerKit.
7 |
8 | +-----------------------------+---------------------------------------------------+-------------------------------------------------+
9 | | **Hand Type** | **Class** | **Lookup** |
10 | +-----------------------------+---------------------------------------------------+-------------------------------------------------+
11 | | Standard high hands | :class:`pokerkit.hands.StandardHighHand` | :class:`pokerkit.lookups.StandardLookup` |
12 | +-----------------------------+---------------------------------------------------+ |
13 | | Standard low hands | :class:`pokerkit.hands.StandardLowHand` | |
14 | +-----------------------------+---------------------------------------------------+ |
15 | | Greek hold'em hands | :class:`pokerkit.hands.GreekHoldemHand` | |
16 | +-----------------------------+---------------------------------------------------+ |
17 | | Omaha hold'em hands | :class:`pokerkit.hands.OmahaHoldemHand` | |
18 | +-----------------------------+---------------------------------------------------+-------------------------------------------------+
19 | | 8 or better low hands | :class:`pokerkit.hands.EightOrBetterLowHand` | :class:`pokerkit.lookups.EightOrBetterLookup` |
20 | +-----------------------------+---------------------------------------------------+ |
21 | | Omaha 8 or better low hands | :class:`pokerkit.hands.OmahaEightOrBetterLowHand` | |
22 | +-----------------------------+---------------------------------------------------+-------------------------------------------------+
23 | | Short-deck hold'em hands | :class:`pokerkit.hands.ShortDeckHoldemHand` | :class:`pokerkit.lookups.ShortDeckHoldemLookup` |
24 | +-----------------------------+---------------------------------------------------+-------------------------------------------------+
25 | | Regular low hands | :class:`pokerkit.hands.RegularLowHand` | :class:`pokerkit.lookups.RegularLookup` |
26 | +-----------------------------+---------------------------------------------------+-------------------------------------------------+
27 | | Badugi hands | :class:`pokerkit.hands.BadugiHand` | :class:`pokerkit.lookups.BadugiLookup` |
28 | +-----------------------------+---------------------------------------------------+-------------------------------------------------+
29 | | Standard badugi hands | :class:`pokerkit.hands.StandardBadugiHand` | :class:`pokerkit.lookups.StandardBadugiLookup` |
30 | +-----------------------------+---------------------------------------------------+-------------------------------------------------+
31 |
32 | Some of these types share the same base lookup. They just differ in the way the hands are evaluated. For example, standard high hands and Omaha hold'em hands use the same lookup.
33 |
34 | Typically, when a hand name contains the term ``low``, it means it is a low hand.
35 |
36 | Benchmarks
37 | ----------
38 |
39 | The benchmark of the hand evaluation suite for the standard hand on a single core of Intel Core i7-1255U with 16GB of RAM and Python 3.11.5 is shown in the below table.
40 |
41 | ================= ========= =========
42 | Metric PokerKit Treys
43 | ================= ========= =========
44 | Speed (# hands/s) 1016740.7 3230966.4
45 | ================= ========= =========
46 |
47 | PokerKit performs in the same magnitude as ``treys``. But, it is a bit faster. This is an inevitable consequence of having a generalized high-level interface for evaluating hands. If speed is paramount, the user is recommended to explore various C++ solutions such as ``OMPEval``.
48 |
49 | Representing Cards
50 | ------------------
51 |
52 | In order to evaluate hands, one must understand how to represent cards in PokerKit. There are multiple ways these can be represented. The below statements define an identical set of cards.
53 |
54 | .. code-block:: python
55 |
56 | from pokerkit import *
57 |
58 | cards = Card(Rank.ACE, Suit.SPADE), Card(Rank.KING, Suit.SPADE)
59 | cards = [Card(Rank.ACE, Suit.SPADE), Card(Rank.KING, Suit.SPADE)]
60 | cards = {Card(Rank.ACE, Suit.SPADE), Card(Rank.KING, Suit.SPADE)}
61 | cards = Card.parse('AsKs')
62 | cards = 'AsKs'
63 |
64 | String Representations
65 | ^^^^^^^^^^^^^^^^^^^^^^
66 |
67 | All functions and methods in PokerKit that accept cards also accept strings that represent cards. A single card as a string is composed of two characters: a rank and a suit, the valid values of each are shown in the below tables. A single string can contain multiple card representations.
68 |
69 | ======= ========= =======================================
70 | Rank Character Class
71 | ======= ========= =======================================
72 | Ace A :attr:`pokerkit.utilities.Rank.ACE`
73 | Deuce 2 :attr:`pokerkit.utilities.Rank.DEUCE`
74 | Trey 3 :attr:`pokerkit.utilities.Rank.TREY`
75 | Four 4 :attr:`pokerkit.utilities.Rank.FOUR`
76 | Five 5 :attr:`pokerkit.utilities.Rank.FIVE`
77 | Six 6 :attr:`pokerkit.utilities.Rank.SIX`
78 | Seven 7 :attr:`pokerkit.utilities.Rank.SEVEN`
79 | Eight 8 :attr:`pokerkit.utilities.Rank.EIGHT`
80 | Nine 9 :attr:`pokerkit.utilities.Rank.NINE`
81 | Ten T :attr:`pokerkit.utilities.Rank.TEN`
82 | Jack J :attr:`pokerkit.utilities.Rank.JACK`
83 | Queen Q :attr:`pokerkit.utilities.Rank.QUEEN`
84 | King K :attr:`pokerkit.utilities.Rank.KING`
85 | Unknown ? :attr:`pokerkit.utilities.Rank.UNKNOWN`
86 | ======= ========= =======================================
87 |
88 | ======= ========= ======================================
89 | Suit Character Class
90 | ======= ========= ======================================
91 | Club c :attr:`pokerkit.utilities.Suit.CLUB`
92 | Diamond d :attr:`pokerkit.utilities.Suit.DIAMOND`
93 | Heart h :attr:`pokerkit.utilities.Suit.HEART`
94 | Spade s :attr:`pokerkit.utilities.Suit.SPADE`
95 | Unknown ? :attr:`pokerkit.utilities.Suit.UNKNOWN`
96 | ======= ========= ======================================
97 |
98 | Note that **parsing the strings is computationally expensive**. Therefore, **if performance is key, one should parse it beforehand and avoid using raw strings as cards**. The parsing can be facilitated with :meth:`pokerkit.utilities.Card.parse`.
99 |
100 | .. code-block:: pycon
101 |
102 | >>> Card.parse('AsKsQsJsTs') # doctest: +ELLIPSIS
103 |
104 | >>> list(Card.parse('2c8d5sKh'))
105 | [2c, 8d, 5s, Kh]
106 | >>> next(Card.parse('AcAh'))
107 | Ac
108 | >>> tuple(Card.parse('??2?3??c'))
109 | (??, 2?, 3?, ?c)
110 |
111 | Unknowns
112 | ^^^^^^^^
113 |
114 | PokerKit allow cards to contain an unknown value. If a rank and/or suit are unknown, they can be represented as a question mark character (``"?"``). Note that these values are not treated as jokers in the hand evaluation suite, but instead are ignored/complained about by them. Unknown cards are intended to be used during game simulation when some hole card values might not be known.
115 |
116 | Creating Hands
117 | --------------
118 |
119 | There are two ways of creating hands. They are both very straightforward.
120 |
121 | The first method is simply by giving the cards that make up a hand.
122 |
123 | .. code-block:: pycon
124 |
125 | >>> from pokerkit import *
126 | >>> h0 = ShortDeckHoldemHand('6s7s8s9sTs')
127 | >>> h1 = ShortDeckHoldemHand('7c8c9cTcJc')
128 | >>> h2 = ShortDeckHoldemHand('2c2d2h2s3h')
129 | Traceback (most recent call last):
130 | ...
131 | ValueError: The cards '2c2d2h2s3h' form an invalid ShortDeckHoldemHand hand.
132 | >>> h0
133 | 6s7s8s9sTs
134 | >>> h1
135 | 7c8c9cTcJc
136 |
137 | The second method is useful in game scenarios where you put in the user's hole cards and the board cards (maybe empty).
138 |
139 | .. code-block:: pycon
140 |
141 | >>> from pokerkit import *
142 | >>> h0 = OmahaHoldemHand.from_game('6c7c8c9c', '8s9sTc')
143 | >>> h1 = OmahaHoldemHand('6c7c8s9sTc')
144 | >>> h0 == h1
145 | True
146 | >>> h0 = OmahaEightOrBetterLowHand.from_game('As2s3s4s', '2c3c4c5c6c')
147 | >>> h1 = OmahaEightOrBetterLowHand('Ad2d3d4d5d')
148 | >>> h0 == h1
149 | True
150 | >>> hole = 'AsAc'
151 | >>> board = 'Kh3sAdAh'
152 | >>> hand = StandardHighHand.from_game(hole, board)
153 | >>> hand.cards
154 | (As, Ac, Kh, Ad, Ah)
155 |
156 | Comparing Hands
157 | ---------------
158 |
159 | Let us define what the "strength" of a hand means. The strength decides who wins the pot and who loses the pot. Realize that stronger or weaker hands do not necessarily always mean higher or lower hands. For instance, in some variants, lower hands are considered stronger, and vice versa.
160 |
161 | PokerKit's hand comparison interface allows hand strengths to be compared using standard comparison operators.
162 |
163 | .. code-block:: pycon
164 |
165 | >>> from pokerkit import *
166 | >>> h0 = StandardHighHand('7c5d4h3s2c')
167 | >>> h1 = StandardHighHand('7c6d4h3s2c')
168 | >>> h2 = StandardHighHand('8c7d6h4s2c')
169 | >>> h3 = StandardHighHand('AcAsAd2s4s')
170 | >>> h4 = StandardHighHand('TsJsQsKsAs')
171 | >>> h0 < h1 < h2 < h3 < h4
172 | True
173 |
174 | .. code-block:: pycon
175 |
176 | >>> from pokerkit import *
177 | >>> h0 = StandardLowHand('TsJsQsKsAs')
178 | >>> h1 = StandardLowHand('AcAsAd2s4s')
179 | >>> h2 = StandardLowHand('8c7d6h4s2c')
180 | >>> h3 = StandardLowHand('7c6d4h3s2c')
181 | >>> h4 = StandardLowHand('7c5d4h3s2c')
182 | >>> h0 < h1 < h2 < h3 < h4
183 | True
184 |
185 | Custom Hands
186 | ------------
187 |
188 | .. image:: _static/hands.drawio.png
189 |
190 | The library generates a lookup table for each hand type. The hands are generated in the order or reverse order of strength and assigned indices, which are used to compare hands. High-level interfaces allow users to construct hands by passing in the necessary cards and using standard comparison operators to compare the hand strengths. Each hand type in PokerKit handles this distinction internally, making it transparent to the end user.
191 |
192 | If the user wishes to define custom hand types, they can leverage existing lookups or create an entirely new lookup table from which hand types are derived. :mod:`pokerkit.lookups` and :mod:`pokerkit.hands` contain plenty of examples of this that the user can take inspiration from.
193 |
194 | Algorithm
195 | ---------
196 |
197 | In the lookup construction process, cards are converted into unique integers that represent their ranks. Each rank corresponds to a unique prime number and the converted integers are multiplied together. The suitedness of the cards is then checked. Using the product and the suitedness, the library looks for the matching hand entries which are then used to compare hands.
198 |
199 | This approach was used by the ``deuces`` and ``treys`` hand evaluation libraries.
200 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. pokerkit documentation master file, created by
2 | sphinx-quickstart on Mon Jan 16 17:34:35 2023.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | .. include:: ../README.rst
7 |
8 | .. toctree::
9 | :maxdepth: 1
10 | :caption: Contents:
11 |
12 | examples
13 | simulation
14 | evaluation
15 | notation
16 | analysis
17 | tips
18 | contributing
19 | changelog
20 | reference
21 |
22 | Indices and tables
23 | ==================
24 |
25 | * :ref:`genindex`
26 | * :ref:`modindex`
27 | * :ref:`search`
28 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=.
11 | set BUILDDIR=_build
12 |
13 | %SPHINXBUILD% >NUL 2>NUL
14 | if errorlevel 9009 (
15 | echo.
16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
17 | echo.installed, then set the SPHINXBUILD environment variable to point
18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
19 | echo.may add the Sphinx directory to PATH.
20 | echo.
21 | echo.If you don't have Sphinx installed, grab it from
22 | echo.https://www.sphinx-doc.org/
23 | exit /b 1
24 | )
25 |
26 | if "%1" == "" goto help
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/docs/notation.rst:
--------------------------------------------------------------------------------
1 | Hand History
2 | ============
3 |
4 | PokerKit can save and/or load hand histories in two formats: the `Poker Hand History (PHH) file format `_, the `Annual Computer Poker Competition (ACPC) protocol <_static/protocol.pdf>`_, and the protocol used in the supplementary of the paper introducing `Pluribus `_.
5 |
6 | Poker Hand History (PHH) File Format
7 | ------------------------------------
8 |
9 | The PokerKit library features `PHH file format `_ reader and writer utilities. It offers "load" and "dump" programmatic APIs akin to those provided by Python's standard libraries such as "json," and "pickle". Below are sample usages of the PHH file format utilities in PokerKit. The hand history object in Python serves as an iterator of the corresponding poker state which first yields the initial state, followed by the same state after applying each action one-by-one in the “actions” field. From game and state objects that are interacted with programmatically, the hand history object can also be created which can subsequently be saved in the file system.
10 |
11 | Reading hands
12 | ^^^^^^^^^^^^^
13 |
14 | .. code-block:: python
15 |
16 | from pokerkit import *
17 |
18 | # Hand loading
19 | with open("...", "rb") as file:
20 | hh = HandHistory.load(file)
21 |
22 | # Create game
23 | game = hh.create_game()
24 |
25 | # Create state
26 | state = hh.create_state()
27 |
28 | # Iterate through each action step
29 | for state in hh:
30 | ...
31 |
32 | # Iterate through each action step
33 | for state, action in hh.state_actions:
34 | ...
35 |
36 | 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.
37 |
38 | .. code-block:: python
39 |
40 | from pokerkit import *
41 |
42 | hh = HandHistory.load(
43 | ...,
44 | automations=...,
45 | divmod=...,
46 | rake=...,
47 | parse_value=...,
48 | )
49 |
50 | hh = HandHistory.loads(
51 | ...,
52 | automations=...,
53 | divmod=...,
54 | rake=...,
55 | parse_value=...,
56 | )
57 |
58 | Writing Hands
59 | ^^^^^^^^^^^^^
60 |
61 | .. code-block:: python
62 |
63 | from pokerkit import *
64 |
65 | # Game state construction
66 | game = PotLimitOmahaHoldem(
67 | (
68 | Automation.ANTE_POSTING,
69 | Automation.BET_COLLECTION,
70 | Automation.BLIND_OR_STRADDLE_POSTING,
71 | Automation.CARD_BURNING,
72 | Automation.HOLE_CARDS_SHOWING_OR_MUCKING,
73 | Automation.HAND_KILLING,
74 | Automation.CHIPS_PUSHING,
75 | Automation.CHIPS_PULLING,
76 | ),
77 | True,
78 | 0,
79 | (500, 1000),
80 | 1000,
81 | )
82 | state = game((1259450.25, 678473.5), 2)
83 |
84 | # State progression; Pre-flop
85 | state.deal_hole("Ah3sKsKh") # Antonius
86 | state.deal_hole("6d9s7d8h") # Blom
87 | state.complete_bet_or_raise_to(3000) # Blom
88 | state.complete_bet_or_raise_to(9000) # Antonius
89 | state.complete_bet_or_raise_to(27000) # Blom
90 | state.complete_bet_or_raise_to(81000) # Antonius
91 | state.check_or_call() # Blom
92 |
93 | # Flop
94 | state.deal_board("4s5c2h")
95 | state.complete_bet_or_raise_to(91000) # Antonius
96 | state.complete_bet_or_raise_to(435000) # Blom
97 | state.complete_bet_or_raise_to(779000) # Antonius
98 | state.check_or_call() # Blom
99 |
100 | # Turn & River
101 | state.deal_board("5h")
102 | state.deal_board("9c")
103 |
104 | # Creating hand history
105 | hh = HandHistory.from_game_state(game, state)
106 | hh.players = ["Patrik Antonius", "Viktor Blom"]
107 |
108 | # Dump hand
109 | with open("...", "wb") as file:
110 | hh.dump(file)
111 |
112 | Annual Computer Poker Competition (ACPC) Protocol
113 | -------------------------------------------------
114 |
115 | Instead of saving hand histories as PHH files, `ACPC <_static/protocol.pdf>`_ logs can be generated.
116 |
117 | .. code-block:: python
118 |
119 | hh = ...
120 | lines = [
121 | f'{sender} {message}' for sender, message in hh.to_acpc_protocol(0, 0)
122 | ]
123 |
124 | with open("...", "w") as file:
125 | file.write("".join(lines))
126 |
127 | Pluribus Protocol
128 | -----------------
129 |
130 | This format was used to record games by `Brown and Sandholm `_ in the supplementary for their Science paper on Pluribus.
131 |
132 | .. code-block:: python
133 |
134 | hh = ...
135 | line = hh.to_pluribus_protocol(10)
136 |
137 | with open("...", "w") as file:
138 | file.write(line)
139 |
--------------------------------------------------------------------------------
/docs/reference.rst:
--------------------------------------------------------------------------------
1 | Reference
2 | =========
3 |
4 | pokerkit.analysis module
5 | ------------------------
6 |
7 | .. automodule:: pokerkit.analysis
8 | :members:
9 | :undoc-members:
10 | :show-inheritance:
11 |
12 | pokerkit.games module
13 | ---------------------
14 |
15 | .. automodule:: pokerkit.games
16 | :members:
17 | :undoc-members:
18 | :show-inheritance:
19 |
20 | pokerkit.hands module
21 | ---------------------
22 |
23 | .. automodule:: pokerkit.hands
24 | :members:
25 | :undoc-members:
26 | :show-inheritance:
27 |
28 | pokerkit.lookups module
29 | -----------------------
30 |
31 | .. automodule:: pokerkit.lookups
32 | :members:
33 | :undoc-members:
34 | :show-inheritance:
35 |
36 | pokerkit.notation module
37 | ------------------------
38 |
39 | .. automodule:: pokerkit.notation
40 | :members:
41 | :undoc-members:
42 | :show-inheritance:
43 |
44 | pokerkit.state module
45 | ---------------------
46 |
47 | .. automodule:: pokerkit.state
48 | :members:
49 | :undoc-members:
50 | :show-inheritance:
51 |
52 | pokerkit.utilities module
53 | -------------------------
54 |
55 | .. automodule:: pokerkit.utilities
56 | :members:
57 | :undoc-members:
58 | :show-inheritance:
59 |
--------------------------------------------------------------------------------
/docs/tips.rst:
--------------------------------------------------------------------------------
1 | Tips and Tricks
2 | ===============
3 |
4 | Some tips and tricks are shown below. If you would like to see more, please create an issue.
5 |
6 | Full Manual Usage
7 | -----------------
8 |
9 | In some use cases, it might be better not to use any automation and handle each operation step by step. For such a purpose, one might use the code below.
10 |
11 | .. code-block:: python
12 |
13 | from pokerkit import *
14 |
15 | def create_state(): ...
16 |
17 | state = create_state()
18 |
19 | while state.status:
20 | if state.can_post_ante():
21 | state.post_ante()
22 | elif state.can_collect_bets():
23 | state.collect_bets()
24 | elif state.can_post_blind_or_straddle():
25 | state.post_blind_or_straddle()
26 | elif state.can_burn_card():
27 | state.burn_card('??')
28 | elif state.can_deal_hole():
29 | state.deal_hole()
30 | elif state.can_deal_board():
31 | state.deal_board()
32 | elif state.can_kill_hand():
33 | state.kill_hand()
34 | elif state.can_push_chips():
35 | state.push_chips()
36 | elif state.can_pull_chips():
37 | state.pull_chips()
38 | else:
39 | action = input('Action: ')
40 |
41 | parse_action(state, action)
42 |
43 | Type Checking
44 | -------------
45 |
46 | All code in PokerKit passes ``--strict`` type checking with MyPy, with one caveat: all chip values are pretended to be integral (``int``). Technically, PokerKit also supports alternative numeric types like ``float``, etc. However, the type annotations for chip values do not reflect this as there is no simple way to express the acceptance of different numeric types (although I suspect it is possible).
47 |
48 | Game Tree Construction
49 | ----------------------
50 |
51 | PokerKit's game simulation mechanics are well-suited for monte-carlo simulations. But, to use this library for game state construction, a careful consideration of the implementation and Python's dataclasses are necessary. One can use Python's ``copy.deepcopy`` function and checking of the validity of operations to build a game tree.
52 |
53 | Automations
54 | -----------
55 |
56 | PokerKit allows fine-grained control of very detailed aspect of poker games, but many people may not be familiar with the details of each operations. As such, it is recommended for new users to first automate as much as possible before removing automations and incorporating fine-grained control away from PokerKit's automation mechanics.
57 |
58 | Read-only and Read-Write Values
59 | -------------------------------
60 |
61 | Some fields/attributes in PokerKit are designed to be readonly. Most of these accesses (but not all) are enforced at the Python's ``dataclasses.dataclass`` level. An exception is for the attributes of :class:`pokerkit.state.State`. They should never be modified but instead let PokerKit modify them through public method calls. In other words, the user must only read from the state's attributes or call public methods (which may modify them).
62 |
63 | In general, one should never call protected (denoted with a preceding ``_`` character in their names) or private methods for anything in PokerKit.
64 |
65 | Assertions
66 | ----------
67 |
68 | Inside PokerKit, assertions are only used for sanity checks. It is **never** used for anything meaningful in PokerKit. As there are many assertions throughout the code, if speed is a concern, one can safely disable assertions in Python by turning on appropriate optimizations for the Python interpreter (e.g. ``-O`` flag or the ``PYTHONOPTIMIZE`` environmental variable).
69 |
70 | Standpatters
71 | ------------
72 |
73 | Unfortunately, when I was writing this library, I did not realize the word "standpatter" (or "stand-patter") exists (see `this Wikipedia entry `_). This is why I used the term "stander-pat" instead (e.g. :attr:`stander_pat_or_discarder_index`). If I had known this earlier, I would have named the attribute differently.
74 |
--------------------------------------------------------------------------------
/pokerkit/__init__.py:
--------------------------------------------------------------------------------
1 | """:mod:`pokerkit` is the top-level package for the PokerKit library.
2 |
3 | All poker tools are imported here.
4 | """
5 |
6 | __all__ = (
7 | 'AntePosting',
8 | 'Automation',
9 | 'BadugiHand',
10 | 'BadugiLookup',
11 | 'BetCollection',
12 | 'BettingStructure',
13 | 'BlindOrStraddlePosting',
14 | 'BoardCombinationHand',
15 | 'BoardDealing',
16 | 'BringInPosting',
17 | 'calculate_equities',
18 | 'calculate_hand_strength',
19 | 'calculate_icm',
20 | 'Card',
21 | 'CardBurning',
22 | 'CardsLike',
23 | 'CheckingOrCalling',
24 | 'ChipsPulling',
25 | 'ChipsPushing',
26 | 'clean_values',
27 | 'CombinationHand',
28 | 'CompletionBettingOrRaisingTo',
29 | 'Deck',
30 | 'DeuceToSevenLowballMixin',
31 | 'divmod',
32 | 'Draw',
33 | 'EightOrBetterLookup',
34 | 'EightOrBetterLowHand',
35 | 'Entry',
36 | 'filter_none',
37 | 'FixedLimitBadugi',
38 | 'FixedLimitDeuceToSevenLowballTripleDraw',
39 | 'FixedLimitOmahaHoldemHighLowSplitEightOrBetter',
40 | 'FixedLimitPokerMixin',
41 | 'FixedLimitRazz',
42 | 'FixedLimitSevenCardStud',
43 | 'FixedLimitSevenCardStudHighLowSplitEightOrBetter',
44 | 'FixedLimitTexasHoldem',
45 | 'Folding',
46 | 'GreekHoldemHand',
47 | 'Hand',
48 | 'HandHistory',
49 | 'HandKilling',
50 | 'Holdem',
51 | 'HoleBoardCombinationHand',
52 | 'HoleCardsShowingOrMucking',
53 | 'HoleDealing',
54 | 'KuhnPokerHand',
55 | 'KuhnPokerLookup',
56 | 'Label',
57 | 'Lookup',
58 | 'max_or_none',
59 | 'min_or_none',
60 | 'Mode',
61 | 'NoLimitDeuceToSevenLowballSingleDraw',
62 | 'NoLimitPokerMixin',
63 | 'NoLimitShortDeckHoldem',
64 | 'NoLimitTexasHoldem',
65 | 'NoOperation',
66 | 'OmahaEightOrBetterLowHand',
67 | 'OmahaHoldemHand',
68 | 'OmahaHoldemMixin',
69 | 'Opening',
70 | 'Operation',
71 | 'parse_action',
72 | 'parse_range',
73 | 'parse_value',
74 | 'Poker',
75 | 'Pot',
76 | 'PotLimitOmahaHoldem',
77 | 'PotLimitPokerMixin',
78 | 'rake',
79 | 'Rank',
80 | 'RankOrder',
81 | 'RegularLookup',
82 | 'RegularLowHand',
83 | 'RunoutCountSelection',
84 | 'SevenCardStud',
85 | 'ShortDeckHoldemHand',
86 | 'ShortDeckHoldemLookup',
87 | 'shuffled',
88 | 'sign',
89 | 'SingleDraw',
90 | 'StandardBadugiHand',
91 | 'StandardBadugiLookup',
92 | 'StandardHand',
93 | 'StandardHighHand',
94 | 'StandardLookup',
95 | 'StandardLowHand',
96 | 'StandingPatOrDiscarding',
97 | 'State',
98 | 'Statistics',
99 | 'Street',
100 | 'Suit',
101 | 'TexasHoldemMixin',
102 | 'TripleDraw',
103 | 'UnfixedLimitHoldem',
104 | 'ValuesLike',
105 | )
106 |
107 | from pokerkit.analysis import (
108 | calculate_equities,
109 | calculate_hand_strength,
110 | calculate_icm,
111 | parse_range,
112 | Statistics,
113 | )
114 | from pokerkit.games import (
115 | DeuceToSevenLowballMixin,
116 | Draw,
117 | FixedLimitBadugi,
118 | FixedLimitDeuceToSevenLowballTripleDraw,
119 | FixedLimitOmahaHoldemHighLowSplitEightOrBetter,
120 | FixedLimitPokerMixin,
121 | FixedLimitRazz,
122 | FixedLimitSevenCardStud,
123 | FixedLimitSevenCardStudHighLowSplitEightOrBetter,
124 | FixedLimitTexasHoldem,
125 | Holdem,
126 | NoLimitDeuceToSevenLowballSingleDraw,
127 | NoLimitPokerMixin,
128 | NoLimitShortDeckHoldem,
129 | NoLimitTexasHoldem,
130 | OmahaHoldemMixin,
131 | Poker,
132 | PotLimitOmahaHoldem,
133 | PotLimitPokerMixin,
134 | SevenCardStud,
135 | SingleDraw,
136 | TexasHoldemMixin,
137 | TripleDraw,
138 | UnfixedLimitHoldem,
139 | )
140 | from pokerkit.hands import (
141 | BadugiHand,
142 | BoardCombinationHand,
143 | CombinationHand,
144 | EightOrBetterLowHand,
145 | GreekHoldemHand,
146 | Hand,
147 | HoleBoardCombinationHand,
148 | KuhnPokerHand,
149 | OmahaEightOrBetterLowHand,
150 | OmahaHoldemHand,
151 | RegularLowHand,
152 | ShortDeckHoldemHand,
153 | StandardBadugiHand,
154 | StandardHand,
155 | StandardHighHand,
156 | StandardLowHand,
157 | )
158 | from pokerkit.lookups import (
159 | BadugiLookup,
160 | EightOrBetterLookup,
161 | Entry,
162 | KuhnPokerLookup,
163 | Label,
164 | Lookup,
165 | RegularLookup,
166 | ShortDeckHoldemLookup,
167 | StandardBadugiLookup,
168 | StandardLookup,
169 | )
170 | from pokerkit.notation import HandHistory, parse_action
171 | from pokerkit.state import (
172 | AntePosting,
173 | Automation,
174 | BetCollection,
175 | BettingStructure,
176 | BlindOrStraddlePosting,
177 | BoardDealing,
178 | BringInPosting,
179 | CardBurning,
180 | CheckingOrCalling,
181 | ChipsPulling,
182 | ChipsPushing,
183 | CompletionBettingOrRaisingTo,
184 | Folding,
185 | HandKilling,
186 | HoleCardsShowingOrMucking,
187 | HoleDealing,
188 | Mode,
189 | NoOperation,
190 | Opening,
191 | Operation,
192 | Pot,
193 | RunoutCountSelection,
194 | StandingPatOrDiscarding,
195 | State,
196 | Street,
197 | )
198 | from pokerkit.utilities import (
199 | Card,
200 | CardsLike,
201 | clean_values,
202 | Deck,
203 | divmod,
204 | filter_none,
205 | max_or_none,
206 | min_or_none,
207 | parse_value,
208 | rake,
209 | Rank,
210 | RankOrder,
211 | shuffled,
212 | sign,
213 | Suit,
214 | ValuesLike,
215 | )
216 |
--------------------------------------------------------------------------------
/pokerkit/analysis.py:
--------------------------------------------------------------------------------
1 | """:mod:`pokerkit.analysis` implements classes related to poker
2 | analysis.
3 | """
4 |
5 | from __future__ import annotations
6 |
7 | from collections.abc import Iterable, Iterator
8 | from collections import Counter, defaultdict
9 | from concurrent.futures import Executor
10 | from dataclasses import dataclass
11 | from functools import partial
12 | from itertools import (
13 | chain,
14 | combinations,
15 | permutations,
16 | product,
17 | repeat,
18 | starmap,
19 | )
20 | from math import sqrt
21 | from operator import eq
22 | from random import choices, sample
23 | from statistics import mean, stdev
24 | from typing import Any
25 |
26 | from pokerkit.hands import Hand
27 | from pokerkit.notation import HandHistory
28 | from pokerkit.utilities import Card, Deck, max_or_none, RankOrder, Suit
29 |
30 | __SUITS = Suit.CLUB, Suit.DIAMOND, Suit.HEART, Suit.SPADE
31 |
32 |
33 | def __parse_range(
34 | raw_range: str,
35 | rank_order: RankOrder,
36 | ) -> Iterator[frozenset[Card]]:
37 |
38 | def index(r: str) -> int:
39 | return rank_order.index(r)
40 |
41 | def iterate(ss: Any) -> Iterator[frozenset[Card]]:
42 | for s0, s1 in ss:
43 | yield frozenset(Card.parse(f'{r0}{s0}{r1}{s1}'))
44 |
45 | def iterate_plus(s: str) -> Iterator[frozenset[Card]]:
46 | if r0 == r1:
47 | r = rank_order[-1]
48 |
49 | yield from __parse_range(f'{r0}{r1}{s}-{r}{r}{s}', rank_order)
50 | else:
51 | i0 = index(r0)
52 | i1 = index(r1)
53 |
54 | if i0 > i1:
55 | i0, i1 = i1, i0
56 |
57 | for r in rank_order[i0:i1]:
58 | yield from __parse_range(f'{rank_order[i1]}{r}{s}', rank_order)
59 |
60 | def iterate_interval(s: str) -> Iterator[frozenset[Card]]:
61 | i0 = index(r0)
62 | i1 = index(r1)
63 | i2 = index(r2)
64 | i3 = index(r3)
65 |
66 | if i1 - i0 != i3 - i2:
67 | raise ValueError(
68 | (
69 | f'Pattern {repr(raw_range)} is invalid because the two'
70 | ' pairs of ranks that bounds the dash-separated notation'
71 | ' must be a shifted version of the other.'
72 | ),
73 | )
74 |
75 | if i0 > i2:
76 | i0, i1, i2, i3 = i2, i3, i0, i1
77 |
78 | for ra, rb in zip(
79 | rank_order[i0:i2 + 1],
80 | rank_order[i1:i3 + 1],
81 | ):
82 | yield from __parse_range(f'{ra}{rb}{s}', rank_order)
83 |
84 | match tuple(raw_range):
85 | case r0, r1:
86 | if r0 == r1:
87 | yield from iterate(combinations(__SUITS, 2))
88 | else:
89 | yield from iterate(product(__SUITS, repeat=2))
90 | case r0, r1, 's':
91 | if r0 != r1:
92 | yield from iterate(zip(__SUITS, __SUITS))
93 | case r0, r1, 'o':
94 | if r0 == r1:
95 | yield from __parse_range(f'{r0}{r1}', rank_order)
96 | else:
97 | yield from iterate(permutations(__SUITS, 2))
98 | case r0, r1, '+':
99 | yield from iterate_plus('')
100 | case r0, r1, 's', '+':
101 | yield from iterate_plus('s')
102 | case r0, r1, 'o', '+':
103 | yield from iterate_plus('o')
104 | case r0, r1, '-', r2, r3:
105 | yield from iterate_interval('')
106 | case r0, r1, 's', '-', r2, r3, 's':
107 | yield from iterate_interval('s')
108 | case r0, r1, 'o', '-', r2, r3, 'o':
109 | yield from iterate_interval('o')
110 | case _:
111 | yield frozenset(Card.parse(raw_range))
112 |
113 |
114 | def parse_range(
115 | *raw_ranges: str,
116 | rank_order: RankOrder = RankOrder.STANDARD,
117 | ) -> set[frozenset[Card]]:
118 | """Parse the range.
119 |
120 | The notations can be separated by a whitespace, comma, or a
121 | semicolon. The returned range is a set of frozensets of cards.
122 |
123 | >>> rng = parse_range('AKs')
124 | >>> len(rng)
125 | 4
126 | >>> frozenset(Card.parse('AsKs')) in rng
127 | True
128 | >>> frozenset(Card.parse('AcKd')) in rng
129 | False
130 |
131 | :param raw_ranges: The raw ranges to be parsed.
132 | :param rank_order: The rank ordering to be used, defaults to
133 | :attr:`pokerkit.utilities.RankOrder`.
134 | :return: The range.
135 | """
136 | raw_ranges = tuple(
137 | ' '.join(raw_ranges).replace(',', ' ').replace(';', ' ').split(),
138 | )
139 | range_ = set[frozenset[Card]]()
140 |
141 | for raw_range in raw_ranges:
142 | range_.update(__parse_range(raw_range, rank_order))
143 |
144 | return range_
145 |
146 |
147 | def __calculate_equities_0(
148 | hole_cards: tuple[list[Card], ...],
149 | board_cards: list[Card],
150 | hole_dealing_count: int,
151 | board_dealing_count: int,
152 | deck_cards: list[Card],
153 | hand_types: tuple[type[Hand], ...],
154 | ) -> list[float]:
155 | hole_cards = tuple(map(list.copy, hole_cards))
156 | board_cards = board_cards.copy()
157 | sample_count = (
158 | (hole_dealing_count * len(hole_cards))
159 | - sum(map(len, hole_cards))
160 | + board_dealing_count
161 | - len(board_cards)
162 | )
163 | sampled_cards = sample(deck_cards, k=sample_count)
164 | begin = 0
165 |
166 | for i in range(len(hole_cards)):
167 | end = begin + hole_dealing_count - len(hole_cards[i])
168 |
169 | hole_cards[i].extend(sampled_cards[begin:end])
170 |
171 | assert len(hole_cards[i]) == hole_dealing_count
172 |
173 | begin = end
174 |
175 | board_cards.extend(sampled_cards[begin:])
176 |
177 | assert len(board_cards) == board_dealing_count
178 |
179 | equities = [0.0] * len(hole_cards)
180 |
181 | for hand_type in hand_types:
182 | hands = list(
183 | map(
184 | partial(hand_type.from_game, board_cards=board_cards),
185 | hole_cards,
186 | ),
187 | )
188 | max_hand = max_or_none(hands)
189 | statuses = list(map(partial(eq, max_hand), hands))
190 | increment = 1 / (len(hand_types) * sum(statuses))
191 |
192 | for i, status in enumerate(statuses):
193 | if status:
194 | equities[i] += increment
195 |
196 | return equities
197 |
198 |
199 | def __calculate_equities_1(
200 | hole_cards: list[tuple[list[Card], ...]],
201 | board_cards: list[Card],
202 | hole_dealing_count: int,
203 | board_dealing_count: int,
204 | deck_cards: list[list[Card]],
205 | hand_types: tuple[type[Hand], ...],
206 | index: int,
207 | ) -> list[float]:
208 | return __calculate_equities_0(
209 | hole_cards[index],
210 | board_cards,
211 | hole_dealing_count,
212 | board_dealing_count,
213 | deck_cards[index],
214 | hand_types,
215 | )
216 |
217 |
218 | def calculate_equities(
219 | hole_ranges: Iterable[Iterable[Iterable[Card]]],
220 | board_cards: Iterable[Card],
221 | hole_dealing_count: int,
222 | board_dealing_count: int,
223 | deck: Deck,
224 | hand_types: Iterable[type[Hand]],
225 | *,
226 | sample_count: int,
227 | executor: Executor | None = None,
228 | ) -> list[float]:
229 | """Calculate the equities.
230 |
231 | The user may supply an executor to use parallelization. If not
232 | given, a single-threaded evaluation is performed.
233 |
234 | >>> from concurrent.futures import ProcessPoolExecutor
235 | >>> from pokerkit import *
236 | >>> calculate_equities(
237 | ... (
238 | ... parse_range('33'),
239 | ... parse_range('33'),
240 | ... ),
241 | ... Card.parse('Tc8d6h4s'),
242 | ... 2,
243 | ... 5,
244 | ... Deck.STANDARD,
245 | ... (StandardHighHand,),
246 | ... sample_count=1000,
247 | ... )
248 | [0.5, 0.5]
249 | >>> calculate_equities(
250 | ... (
251 | ... parse_range('2h2c'),
252 | ... parse_range('3h3c'),
253 | ... parse_range('AhKh'),
254 | ... ),
255 | ... Card.parse('3s3d4c'),
256 | ... 2,
257 | ... 5,
258 | ... Deck.STANDARD,
259 | ... (StandardHighHand,),
260 | ... sample_count=1000,
261 | ... )
262 | [0.0, 1.0, 0.0]
263 | >>> with ProcessPoolExecutor() as executor:
264 | ... calculate_equities(
265 | ... (
266 | ... parse_range('2h2c'),
267 | ... parse_range('3h3c'),
268 | ... parse_range('AsKs'),
269 | ... ),
270 | ... Card.parse('QsJsTs'),
271 | ... 2,
272 | ... 5,
273 | ... Deck.STANDARD,
274 | ... (StandardHighHand,),
275 | ... sample_count=1000,
276 | ... executor=executor,
277 | ... )
278 | ...
279 | [0.0, 0.0, 1.0]
280 |
281 | :param hole_ranges: The ranges of each player in the pot.
282 | :param board_cards: The board cards, may be empty.
283 | :param hole_dealing_count: The final number of hole cards; for
284 | hold'em, it is ``2``.
285 | :param board_dealing_count: The final number of board cards; for
286 | hold'em, it is ``5``.
287 | :param deck: The deck; most games typically use
288 | :attr:`pokerkit.utilities.Deck.STANDARD`.
289 | :param hand_types: The hand types; most games typically just use
290 | :class:`pokerkit.hands.StandardHighHand`.
291 | :param sample_count: The number of samples to simulate, higher value
292 | gives greater accuracy and fidelity.
293 | :param executor: The optional executor, defaults to ``None`` which
294 | is just using 1 thread/process. The user can supply
295 | a ``ProcessPoolExecutor`` to use processes.
296 | :return: The equity values.
297 | """
298 | hole_ranges = tuple(map(list, map(partial(map, list), hole_ranges)))
299 | board_cards = list(board_cards)
300 | hand_types = tuple(hand_types)
301 | hole_cards = []
302 | deck_cards = []
303 |
304 | for selection in product(*hole_ranges):
305 | counter = Counter(chain(chain.from_iterable(selection), board_cards))
306 |
307 | if all(map(partial(eq, 1), counter.values())):
308 | hole_cards.append(selection)
309 | deck_cards.append(list(set(deck) - counter.keys()))
310 |
311 | fn = partial(
312 | __calculate_equities_1,
313 | hole_cards,
314 | board_cards,
315 | hole_dealing_count,
316 | board_dealing_count,
317 | deck_cards,
318 | hand_types,
319 | )
320 | mapper: Any = map if executor is None else executor.map
321 | indices = choices(range(len(hole_cards)), k=sample_count)
322 | equities = [0.0] * len(hole_ranges)
323 |
324 | for i, equity in chain.from_iterable(map(enumerate, mapper(fn, indices))):
325 | equities[i] += equity
326 |
327 | for i, equity in enumerate(equities):
328 | equities[i] = equity / sample_count
329 |
330 | return equities
331 |
332 |
333 | def calculate_hand_strength(
334 | player_count: int,
335 | hole_range: Iterable[Iterable[Card]],
336 | board_cards: Iterable[Card],
337 | hole_dealing_count: int,
338 | board_dealing_count: int,
339 | deck: Deck,
340 | hand_types: Iterable[type[Hand]],
341 | *,
342 | sample_count: int,
343 | executor: Executor | None = None,
344 | ) -> float:
345 | """Calculate the hand strength: odds of beating a single other hand
346 | chosen uniformly at random.
347 |
348 | The user may supply an executor to use parallelization. If not
349 | given, a single-threaded evaluation is performed.
350 |
351 | >>> from concurrent.futures import ProcessPoolExecutor
352 | >>> from pokerkit import *
353 | >>> calculate_hand_strength(
354 | ... 3,
355 | ... parse_range('3h3c'),
356 | ... Card.parse('3s3d2c2h'),
357 | ... 2,
358 | ... 5,
359 | ... Deck.STANDARD,
360 | ... (StandardHighHand,),
361 | ... sample_count=1000,
362 | ... )
363 | 1.0
364 | >>> with ProcessPoolExecutor() as executor:
365 | ... calculate_hand_strength(
366 | ... 3,
367 | ... parse_range('AsKs'),
368 | ... Card.parse('QsJsTs'),
369 | ... 2,
370 | ... 5,
371 | ... Deck.STANDARD,
372 | ... (StandardHighHand,),
373 | ... sample_count=1000,
374 | ... executor=executor,
375 | ... )
376 | ...
377 | 1.0
378 |
379 | :param player_count: Number of players in the pot.
380 | :param hole_range: The range of the player.
381 | :param board_cards: The board cards, may be empty.
382 | :param hole_dealing_count: The final number of hole cards; for
383 | hold'em, it is ``2``.
384 | :param board_dealing_count: The final number of board cards; for
385 | hold'em, it is ``5``.
386 | :param deck: The deck; most games typically use
387 | :attr:`pokerkit.utilities.Deck.STANDARD`.
388 | :param hand_types: The hand types; most games typically just use
389 | :class:`pokerkit.hands.StandardHighHand`.
390 | :param sample_count: The number of samples to simulate, higher value
391 | gives greater accuracy and fidelity.
392 | :param executor: The optional executor, defaults to ``None`` which
393 | is just using 1 thread/process. The user can supply
394 | a ``ProcessPoolExecutor`` to use processes.
395 | :return: The equity values.
396 | """
397 | hole_ranges: list[Iterable[Iterable[Card]]] = [
398 | [[]] for _ in range(player_count - 1)
399 | ]
400 |
401 | hole_ranges.append(hole_range)
402 |
403 | equities = calculate_equities(
404 | hole_ranges,
405 | board_cards,
406 | hole_dealing_count,
407 | board_dealing_count,
408 | deck,
409 | hand_types,
410 | sample_count=sample_count,
411 | executor=executor,
412 | )
413 |
414 | return equities[-1]
415 |
416 |
417 | @dataclass
418 | class Statistics:
419 | """The class for player statistics.
420 |
421 | :param payoffs: The payoffs of each hand.
422 | """
423 |
424 | payoffs: list[int]
425 | """The payoffs."""
426 |
427 | @classmethod
428 | def merge(cls, *statistics: Statistics) -> Statistics:
429 | """Merge the statistics.
430 |
431 | :param statistics: The statistics to merge.
432 | :return: The merged stats.
433 | """
434 | payoffs = []
435 |
436 | for sub_statistics in statistics:
437 | payoffs.extend(sub_statistics.payoffs)
438 |
439 | return Statistics(payoffs=payoffs)
440 |
441 | @classmethod
442 | def from_hand_history(cls, *hhs: HandHistory) -> dict[str, Statistics]:
443 | """Obtain statistics for each position and players (if any) for
444 | a hand history or hand histories.
445 |
446 | :param hh: The hand history/histories to analyze.
447 | :return: The hand history statistics.
448 | """
449 | statistics = defaultdict[str, list[Statistics]](list)
450 |
451 | for hh in hhs:
452 | if hh.finishing_stacks is None:
453 | end_state = tuple(hh)[-1]
454 | finishing_stacks = end_state.stacks
455 | else:
456 | finishing_stacks = hh.finishing_stacks
457 |
458 | players: Any
459 |
460 | if hh.players is None:
461 | players = repeat(None)
462 | else:
463 | players = hh.players
464 |
465 | for i, (starting_stack, stack, player) in enumerate(
466 | zip(hh.starting_stacks, finishing_stacks, players),
467 | ):
468 | if player is not None:
469 | statistics[player].append(
470 | Statistics(payoffs=[stack - starting_stack]),
471 | )
472 |
473 | return dict(
474 | zip(statistics.keys(), starmap(cls.merge, statistics.values())),
475 | )
476 |
477 | @property
478 | def sample_count(self) -> int:
479 | """Return the sample size.
480 |
481 | :return: The sample size.
482 | """
483 | return len(self.payoffs)
484 |
485 | @property
486 | def payoff_sum(self) -> float:
487 | """Return the total payoff.
488 |
489 | :return: The total payoff.
490 | """
491 | return sum(self.payoffs)
492 |
493 | @property
494 | def payoff_mean(self) -> float:
495 | """Return the payoff rate (per hand).
496 |
497 | :return: The payoff rate.
498 | """
499 | return mean(self.payoffs)
500 |
501 | @property
502 | def payoff_stdev(self) -> float:
503 | """Return the payoff standard deviation.
504 |
505 | :return: The payoff standard deviation.
506 | """
507 | return stdev(self.payoffs)
508 |
509 | @property
510 | def payoff_stderr(self) -> float:
511 | """Return the payoff standard error.
512 |
513 | :return: The payoff standard error.
514 | """
515 | return self.payoff_stdev / sqrt(self.sample_count)
516 |
517 |
518 | def calculate_icm(
519 | payouts: Iterable[float],
520 | chips: Iterable[float],
521 | ) -> tuple[float, ...]:
522 | """Calculate the independent chip model (ICM) values.
523 |
524 | >>> calculate_icm([70, 30], [50, 30, 20]) # doctest: +ELLIPSIS
525 | (45.17..., 32.25, 22.57...)
526 | >>> calculate_icm([50, 30, 20], [25, 87, 88]) # doctest: +ELLIPSIS
527 | (25.69..., 37.08..., 37.21...)
528 | >>> calculate_icm([50, 30, 20], [21, 89, 90]) # doctest: +ELLIPSIS
529 | (24.85..., 37.51..., 37.63...)
530 | >>> calculate_icm([50, 30, 20], [198, 1, 1]) # doctest: +ELLIPSIS
531 | (49.79..., 25.10..., 25.10...)
532 |
533 | :param payouts: The payouts.
534 | :param chips: The players' chips.
535 | :return: The ICM values.
536 | """
537 | payouts = tuple(payouts)
538 | chips = tuple(chips)
539 | chip_sum = sum(chips)
540 | chip_percentages = [chip / chip_sum for chip in chips]
541 | icms = [0.0] * len(chips)
542 |
543 | for player_indices in permutations(range(len(chips)), len(payouts)):
544 | probability = 1.0
545 | denominator = 1.0
546 |
547 | for player_index in player_indices:
548 | chip_percentage = chip_percentages[player_index]
549 | probability *= chip_percentage / denominator
550 | denominator -= chip_percentage
551 |
552 | for payout, player_index in zip(payouts, player_indices):
553 | icms[player_index] += payout * probability
554 |
555 | return tuple(icms)
556 |
--------------------------------------------------------------------------------
/pokerkit/hands.py:
--------------------------------------------------------------------------------
1 | """:mod:`pokerkit.hands` implements classes related to poker hands."""
2 |
3 | from __future__ import annotations
4 |
5 | from abc import ABC, abstractmethod
6 | from collections.abc import Hashable
7 | from functools import total_ordering
8 | from itertools import chain, combinations
9 | from typing import Any, ClassVar
10 |
11 | from pokerkit.lookups import (
12 | BadugiLookup,
13 | EightOrBetterLookup,
14 | Entry,
15 | KuhnPokerLookup,
16 | Lookup,
17 | RegularLookup,
18 | ShortDeckHoldemLookup,
19 | StandardBadugiLookup,
20 | StandardLookup,
21 | )
22 | from pokerkit.utilities import Card, CardsLike
23 |
24 |
25 | @total_ordering
26 | class Hand(Hashable, ABC):
27 | """The abstract base class for poker hands.
28 |
29 | Stronger hands are considered greater than weaker hands.
30 |
31 | >>> h0 = ShortDeckHoldemHand('6s7s8s9sTs')
32 | >>> h1 = ShortDeckHoldemHand('7c8c9cTcJc')
33 | >>> h2 = ShortDeckHoldemHand('2c2d2h2s3h') # doctest: +ELLIPSIS
34 | Traceback (most recent call last):
35 | ...
36 | ValueError: The cards '2c2d2h2s3h' form an invalid ShortDeckHoldemHand h...
37 | >>> h0
38 | 6s7s8s9sTs
39 | >>> h1
40 | 7c8c9cTcJc
41 | >>> print(h0)
42 | Straight flush (6s7s8s9sTs)
43 | >>> h0 < h1
44 | True
45 |
46 | It does not make sense to compare hands of different types.
47 |
48 | >>> h = BadugiHand('6d7s8h9c')
49 | >>> h < 500
50 | Traceback (most recent call last):
51 | ...
52 | TypeError: '<' not supported between instances of 'BadugiHand' and 'int'
53 |
54 | The hands are hashable.
55 |
56 | >>> h0 = ShortDeckHoldemHand('6s7s8s9sTs')
57 | >>> h1 = ShortDeckHoldemHand('7c8c9cTcJc')
58 | >>> hands = {h0, h1}
59 |
60 | :param cards: The cards that form the hand.
61 | :raises ValueError: If the cards form an invalid hand.
62 | """
63 |
64 | lookup: ClassVar[Lookup]
65 | """The hand lookup."""
66 | low: ClassVar[bool]
67 | """The low status."""
68 |
69 | @classmethod
70 | @abstractmethod
71 | def from_game(
72 | cls,
73 | hole_cards: CardsLike,
74 | board_cards: CardsLike = (),
75 | ) -> Hand:
76 | """Create a poker hand from a game setting.
77 |
78 | In a game setting, a player uses private cards from their hole
79 | and the public cards from the board to make their hand.
80 |
81 | :param hole_cards: The hole cards.
82 | :param board_cards: The optional board cards.
83 | :return: The strongest hand from possible card combinations.
84 | """
85 | pass # pragma: no cover
86 |
87 | def __init__(self, cards: CardsLike) -> None:
88 | self.__cards = Card.clean(cards)
89 |
90 | if not self.lookup.has_entry(self.cards):
91 | raise ValueError(
92 | (
93 | f'The cards {repr(cards)} form an invalid'
94 | f' {type(self).__qualname__} hand.'
95 | ),
96 | )
97 |
98 | def __eq__(self, other: Any) -> bool:
99 | if type(self) != type(other): # noqa: E721
100 | return NotImplemented
101 |
102 | assert isinstance(other, Hand)
103 |
104 | return self.entry == other.entry
105 |
106 | def __hash__(self) -> int:
107 | return hash(self.entry)
108 |
109 | def __lt__(self, other: Hand) -> bool:
110 | if type(self) != type(other): # noqa: E721
111 | return NotImplemented
112 |
113 | assert isinstance(other, Hand)
114 |
115 | if self.low:
116 | ordering = self.entry > other.entry
117 | else:
118 | ordering = self.entry < other.entry
119 |
120 | return ordering
121 |
122 | def __repr__(self) -> str:
123 | return ''.join(map(repr, self.cards))
124 |
125 | def __str__(self) -> str:
126 | return f'{self.entry.label.value} ({repr(self)})'
127 |
128 | @property
129 | def cards(self) -> tuple[Card, ...]:
130 | """Return the cards that form this hand.
131 |
132 | >>> hole = 'AsAc'
133 | >>> board = 'Kh3sAdAh'
134 | >>> hand = StandardHighHand.from_game(hole, board)
135 | >>> hand.cards
136 | (As, Ac, Kh, Ad, Ah)
137 |
138 | :return: The cards that form this hand.
139 | """
140 | return self.__cards
141 |
142 | @property
143 | def entry(self) -> Entry:
144 | """Return the hand entry.
145 |
146 | >>> hole = 'AsAc'
147 | >>> board = 'Kh3sAdAh'
148 | >>> hand = StandardHighHand.from_game(hole, board)
149 | >>> hand.entry.label
150 |
151 |
152 | :return: The hand entry.
153 | """
154 | return self.lookup.get_entry(self.cards)
155 |
156 |
157 | class CombinationHand(Hand, ABC):
158 | """The abstract base class for combination hands."""
159 |
160 | card_count: ClassVar[int]
161 | """The number of cards."""
162 |
163 | @classmethod
164 | def from_game(
165 | cls,
166 | hole_cards: CardsLike,
167 | board_cards: CardsLike = (),
168 | ) -> Hand:
169 | """Create a poker hand from a game setting.
170 |
171 | In a game setting, a player uses private cards from their hole
172 | and the public cards from the board to make their hand.
173 |
174 | >>> h0 = StandardHighHand.from_game('AcAdAhAsKc')
175 | >>> h1 = StandardHighHand('AcAdAhAsKc')
176 | >>> h0 == h1
177 | True
178 | >>> h0 = StandardHighHand.from_game('Ac9c', 'AhKhQhJhTh')
179 | >>> h1 = StandardHighHand('AhKhQhJhTh')
180 | >>> h0 == h1
181 | True
182 |
183 | >>> h0 = StandardLowHand.from_game('AcAdAhAsKc', '')
184 | >>> h1 = StandardLowHand('AcAdAhAsKc')
185 | >>> h0 == h1
186 | True
187 | >>> h0 = StandardLowHand.from_game('Ac9c', 'AhKhQhJhTh')
188 | >>> h1 = StandardLowHand('AcQhJhTh9c')
189 | >>> h0 == h1
190 | True
191 |
192 | >>> h0 = ShortDeckHoldemHand.from_game('AcKs', 'AhAsKcJsTs')
193 | >>> h1 = ShortDeckHoldemHand('AcAhAsKcKs')
194 | >>> h0 == h1
195 | True
196 | >>> h0 = ShortDeckHoldemHand.from_game('AcAd', '6s7cKcKd')
197 | >>> h1 = ShortDeckHoldemHand('AcAdKcKd7c')
198 | >>> h0 == h1
199 | True
200 |
201 | >>> h0 = EightOrBetterLowHand.from_game('As2s', '2c3c4c5c6c')
202 | >>> h1 = EightOrBetterLowHand('Ad2d3d4d5d')
203 | >>> h0 == h1
204 | True
205 |
206 | >>> h0 = RegularLowHand.from_game('AcAd', 'AhAsKcQdQh')
207 | >>> h1 = RegularLowHand('AcAsQdQhKc')
208 | >>> h0 == h1
209 | True
210 | >>> h0 = RegularLowHand.from_game('AcAd', 'AhAsKcQd')
211 | >>> h1 = RegularLowHand('AdAhAsKcQd')
212 | >>> h0 == h1
213 | True
214 |
215 | :param hole_cards: The hole cards.
216 | :param board_cards: The optional board cards.
217 | :return: The strongest hand from possible card combinations.
218 | """
219 | max_hand = None
220 |
221 | for combination in combinations(
222 | chain(Card.clean(hole_cards), Card.clean(board_cards)),
223 | cls.card_count,
224 | ):
225 | try:
226 | hand = cls(combination)
227 | except ValueError:
228 | pass
229 | else:
230 | if max_hand is None or hand > max_hand:
231 | max_hand = hand
232 |
233 | if max_hand is None:
234 | raise ValueError(
235 | (
236 | f'No valid {type(cls).__qualname__} hand can be formed'
237 | ' from the hole and board cards.'
238 | ),
239 | )
240 |
241 | return max_hand
242 |
243 |
244 | class StandardHand(CombinationHand, ABC):
245 | """The abstract base class for standard hands."""
246 |
247 | lookup = StandardLookup()
248 | card_count = 5
249 |
250 |
251 | class StandardHighHand(StandardHand):
252 | """The class for standard high hands.
253 |
254 | >>> h0 = StandardHighHand('7c5d4h3s2c')
255 | >>> h1 = StandardHighHand('7c6d4h3s2c')
256 | >>> h2 = StandardHighHand('8c7d6h4s2c')
257 | >>> h3 = StandardHighHand('AcAsAd2s4s')
258 | >>> h4 = StandardHighHand('TsJsQsKsAs')
259 | >>> h0 < h1 < h2 < h3 < h4
260 | True
261 |
262 | >>> h = StandardHighHand('4c5dThJsAcKh2h') # doctest: +ELLIPSIS
263 | Traceback (most recent call last):
264 | ...
265 | ValueError: The cards '4c5dThJsAcKh2h' form an invalid StandardHighHand ...
266 | >>> h = StandardHighHand('Ac2c3c4c')
267 | Traceback (most recent call last):
268 | ...
269 | ValueError: The cards 'Ac2c3c4c' form an invalid StandardHighHand hand.
270 | >>> h = StandardHighHand(())
271 | Traceback (most recent call last):
272 | ...
273 | ValueError: The cards () form an invalid StandardHighHand hand.
274 | """
275 |
276 | low = False
277 |
278 |
279 | class StandardLowHand(StandardHand):
280 | """The class for standard low hands.
281 |
282 | >>> h0 = StandardLowHand('TsJsQsKsAs')
283 | >>> h1 = StandardLowHand('AcAsAd2s4s')
284 | >>> h2 = StandardLowHand('8c7d6h4s2c')
285 | >>> h3 = StandardLowHand('7c6d4h3s2c')
286 | >>> h4 = StandardLowHand('7c5d4h3s2c')
287 | >>> h0 < h1 < h2 < h3 < h4
288 | True
289 |
290 | >>> h = StandardLowHand('4c5dThJsAcKh2h') # doctest: +ELLIPSIS
291 | Traceback (most recent call last):
292 | ...
293 | ValueError: The cards '4c5dThJsAcKh2h' form an invalid StandardLowHand h...
294 | >>> h = StandardLowHand('Ac2c3c4c')
295 | Traceback (most recent call last):
296 | ...
297 | ValueError: The cards 'Ac2c3c4c' form an invalid StandardLowHand hand.
298 | >>> h = StandardLowHand(())
299 | Traceback (most recent call last):
300 | ...
301 | ValueError: The cards () form an invalid StandardLowHand hand.
302 | """
303 |
304 | low = True
305 |
306 |
307 | class ShortDeckHoldemHand(CombinationHand):
308 | """The class for short-deck hold'em hands.
309 |
310 | Here, flushes beat full houses.
311 |
312 | >>> h0 = ShortDeckHoldemHand('6c7d8h9sJc')
313 | >>> h1 = ShortDeckHoldemHand('7c7d7hTsQc')
314 | >>> h2 = ShortDeckHoldemHand('As6c7h8h9h')
315 | >>> h3 = ShortDeckHoldemHand('AsAhKcKhKd')
316 | >>> h4 = ShortDeckHoldemHand('6s7s8sTsQs')
317 | >>> h0 < h1 < h2 < h3 < h4
318 | True
319 |
320 | >>> h = ShortDeckHoldemHand('4c5dThJsAcKh2h') # doctest: +ELLIPSIS
321 | Traceback (most recent call last):
322 | ...
323 | ValueError: The cards '4c5dThJsAcKh2h' form an invalid ShortDeckHoldemHa...
324 | >>> h = ShortDeckHoldemHand('Ac2c3c4c5c') # doctest: +ELLIPSIS
325 | Traceback (most recent call last):
326 | ...
327 | ValueError: The cards 'Ac2c3c4c5c' form an invalid ShortDeckHoldemHand ...
328 | >>> h = ShortDeckHoldemHand(())
329 | Traceback (most recent call last):
330 | ...
331 | ValueError: The cards () form an invalid ShortDeckHoldemHand hand.
332 | """
333 |
334 | lookup = ShortDeckHoldemLookup()
335 | low = False
336 | card_count = 5
337 |
338 |
339 | class EightOrBetterLowHand(CombinationHand):
340 | """The class for eight or better low hands.
341 |
342 | >>> h0 = EightOrBetterLowHand('8c7d6h4s2c')
343 | >>> h1 = EightOrBetterLowHand('7c5d4h3s2c')
344 | >>> h2 = EightOrBetterLowHand('5d4h3s2dAd')
345 | >>> h0 < h1 < h2
346 | True
347 |
348 | >>> h = EightOrBetterLowHand('AcAsAd2s4s') # doctest: +ELLIPSIS
349 | Traceback (most recent call last):
350 | ...
351 | ValueError: The cards 'AcAsAd2s4s' form an invalid EightOrBetterLowHand ...
352 | >>> h = EightOrBetterLowHand('TsJsQsKsAs') # doctest: +ELLIPSIS
353 | Traceback (most recent call last):
354 | ...
355 | ValueError: The cards 'TsJsQsKsAs' form an invalid EightOrBetterLowHand ...
356 | >>> h = EightOrBetterLowHand('4c5dThJsAcKh2h') # doctest: +ELLIPSIS
357 | Traceback (most recent call last):
358 | ...
359 | ValueError: The cards '4c5dThJsAcKh2h' form an invalid EightOrBetterLowH...
360 | >>> h = EightOrBetterLowHand('Ac2c3c4c')
361 | Traceback (most recent call last):
362 | ...
363 | ValueError: The cards 'Ac2c3c4c' form an invalid EightOrBetterLowHand hand.
364 | >>> h = EightOrBetterLowHand(())
365 | Traceback (most recent call last):
366 | ...
367 | ValueError: The cards () form an invalid EightOrBetterLowHand hand.
368 | """
369 |
370 | lookup = EightOrBetterLookup()
371 | low = True
372 | card_count = 5
373 |
374 |
375 | class RegularLowHand(CombinationHand):
376 | """The class for low regular hands.
377 |
378 | Here, flushes are ignored.
379 |
380 | >>> h0 = RegularLowHand('KhKsKcKdAc')
381 | >>> h1 = RegularLowHand('2s2c3s3cAh')
382 | >>> h2 = RegularLowHand('6c4d3h2sAc')
383 | >>> h3 = RegularLowHand('Ac2c3c4c5c')
384 | >>> h0 < h1 < h2 < h3
385 | True
386 |
387 | >>> h = RegularLowHand('4c5dThJsAcKh2h')
388 | Traceback (most recent call last):
389 | ...
390 | ValueError: The cards '4c5dThJsAcKh2h' form an invalid RegularLowHand hand.
391 | >>> h = RegularLowHand(())
392 | Traceback (most recent call last):
393 | ...
394 | ValueError: The cards () form an invalid RegularLowHand hand.
395 | """
396 |
397 | lookup = RegularLookup()
398 | low = True
399 | card_count = 5
400 |
401 |
402 | class BoardCombinationHand(CombinationHand, ABC):
403 | """The abstract base class for board-combination hands."""
404 |
405 | board_card_count: ClassVar[int]
406 | """The number of board cards."""
407 |
408 | @classmethod
409 | def from_game(
410 | cls,
411 | hole_cards: CardsLike,
412 | board_cards: CardsLike = (),
413 | ) -> Hand:
414 | """Create a poker hand from a game setting.
415 |
416 | In a game setting, a player uses private cards from their hole
417 | and the public cards from the board to make their hand.
418 |
419 | >>> h0 = GreekHoldemHand.from_game('Ac2d', 'QdJdTh2sKs')
420 | >>> h1 = GreekHoldemHand('2s2dAcKsQd')
421 | >>> h0 == h1
422 | True
423 | >>> h0 = GreekHoldemHand.from_game('AsKs', 'QdJdTh2s2d')
424 | >>> h1 = GreekHoldemHand('AsKsQdJdTh')
425 | >>> h0 == h1
426 | True
427 | >>> h0 = GreekHoldemHand.from_game('Ac9c', 'AhKhQhJhTh')
428 | >>> h1 = GreekHoldemHand('AcAhKhQh9c')
429 | >>> h0 == h1
430 | True
431 |
432 | :param hole_cards: The hole cards.
433 | :param board_cards: The optional board cards.
434 | :return: The strongest hand from possible card combinations.
435 | """
436 | hole_cards = Card.clean(hole_cards)
437 | board_cards = Card.clean(board_cards)
438 | max_hand = None
439 |
440 | for combination in combinations(board_cards, cls.board_card_count):
441 | try:
442 | hand = super().from_game(hole_cards, combination)
443 | except ValueError:
444 | pass
445 | else:
446 | if max_hand is None or hand > max_hand:
447 | max_hand = hand
448 |
449 | if max_hand is None:
450 | raise ValueError(
451 | (
452 | f'No valid {type(cls).__qualname__} hand can be formed'
453 | ' from the hole and board cards.'
454 | ),
455 | )
456 |
457 | return max_hand
458 |
459 |
460 | class GreekHoldemHand(BoardCombinationHand):
461 | """The class for Greek hold'em hands.
462 |
463 | In Greek hold'em, the player must use all of their hole cards to
464 | make a hand.
465 |
466 | >>> h0 = GreekHoldemHand('7c5d4h3s2c')
467 | >>> h1 = GreekHoldemHand('7c6d4h3s2c')
468 | >>> h2 = GreekHoldemHand('8c7d6h4s2c')
469 | >>> h3 = GreekHoldemHand('AcAsAd2s4s')
470 | >>> h4 = GreekHoldemHand('TsJsQsKsAs')
471 | >>> h0 < h1 < h2 < h3 < h4
472 | True
473 | """
474 |
475 | lookup = StandardLookup()
476 | low = False
477 | card_count = 5
478 | board_card_count = 3
479 |
480 |
481 | class HoleBoardCombinationHand(BoardCombinationHand, ABC):
482 | """The abstract base class for hole-board-combination hands."""
483 |
484 | hole_card_count: ClassVar[int]
485 | """The number of hole cards."""
486 |
487 | @classmethod
488 | def from_game(
489 | cls,
490 | hole_cards: CardsLike,
491 | board_cards: CardsLike = (),
492 | ) -> Hand:
493 | """Create a poker hand from a game setting.
494 |
495 | In a game setting, a player uses private cards from their hole
496 | and the public cards from the board to make their hand.
497 |
498 | >>> h0 = OmahaHoldemHand.from_game('6c7c8c9c', '8s9sTc')
499 | >>> h1 = OmahaHoldemHand('6c7c8s9sTc')
500 | >>> h0 == h1
501 | True
502 | >>> h0 = OmahaHoldemHand.from_game('6c7c8s9s', '8c9cTc')
503 | >>> h1 = OmahaHoldemHand('6c7c8c9cTc')
504 | >>> h0 == h1
505 | True
506 | >>> h0 = OmahaHoldemHand.from_game('6c7c8c9c', '8s9sTc9hKs')
507 | >>> h1 = OmahaHoldemHand('8c8s9c9s9h')
508 | >>> h0 == h1
509 | True
510 | >>> h0 = OmahaHoldemHand.from_game('6c7c8sAh', 'As9cTc2sKs')
511 | >>> h1 = OmahaHoldemHand('AhAsKsTc8s')
512 | >>> h0 == h1
513 | True
514 |
515 | >>> h0 = OmahaEightOrBetterLowHand.from_game('As2s3s4s', '2c3c4c5c6c')
516 | >>> h1 = OmahaEightOrBetterLowHand('Ad2d3d4d5d')
517 | >>> h0 == h1
518 | True
519 | >>> h0 = OmahaEightOrBetterLowHand.from_game('As6s7s8s', '2c3c4c5c6c')
520 | >>> h1 = OmahaEightOrBetterLowHand('Ad2d3d4d6d')
521 | >>> h0 == h1
522 | True
523 |
524 | :param hole_cards: The hole cards.
525 | :param board_cards: The optional board cards.
526 | :return: The strongest hand from possible card combinations.
527 | """
528 | hole_cards = Card.clean(hole_cards)
529 | board_cards = Card.clean(board_cards)
530 | max_hand = None
531 |
532 | for combination in combinations(hole_cards, cls.hole_card_count):
533 | try:
534 | hand = super().from_game(combination, board_cards)
535 | except ValueError:
536 | pass
537 | else:
538 | if max_hand is None or hand > max_hand:
539 | max_hand = hand
540 |
541 | if max_hand is None:
542 | raise ValueError(
543 | (
544 | f'No valid {type(cls).__qualname__} hand can be formed'
545 | ' from the hole and board cards.'
546 | ),
547 | )
548 |
549 | return max_hand
550 |
551 |
552 | class OmahaHoldemHand(HoleBoardCombinationHand):
553 | """The class for Omaha hold'em hands.
554 |
555 | In Omaha hold'em, the player must use a fixed number of their hole
556 | cards to make a hand.
557 |
558 | >>> h0 = OmahaHoldemHand('7c5d4h3s2c')
559 | >>> h1 = OmahaHoldemHand('7c6d4h3s2c')
560 | >>> h2 = OmahaHoldemHand('8c7d6h4s2c')
561 | >>> h3 = OmahaHoldemHand('AcAsAd2s4s')
562 | >>> h4 = OmahaHoldemHand('TsJsQsKsAs')
563 | >>> h0 < h1 < h2 < h3 < h4
564 | True
565 | """
566 |
567 | lookup = StandardLookup()
568 | low = False
569 | card_count = 5
570 | board_card_count = 3
571 | hole_card_count = 2
572 |
573 |
574 | class OmahaEightOrBetterLowHand(HoleBoardCombinationHand):
575 | """The class for Omaha eight or better low hands.
576 |
577 | >>> h0 = OmahaEightOrBetterLowHand('8c7d6h4s2c')
578 | >>> h1 = OmahaEightOrBetterLowHand('7c5d4h3s2c')
579 | >>> h2 = OmahaEightOrBetterLowHand('5d4h3s2dAd')
580 | >>> h0 < h1 < h2
581 | True
582 | """
583 |
584 | lookup = EightOrBetterLookup()
585 | low = True
586 | card_count = 5
587 | board_card_count = 3
588 | hole_card_count = 2
589 |
590 |
591 | class BadugiHand(Hand):
592 | """The class for badugi hands.
593 |
594 | >>> h0 = BadugiHand('Kc')
595 | >>> h1 = BadugiHand('Ac')
596 | >>> h2 = BadugiHand('4c8dKh')
597 | >>> h3 = BadugiHand('Ac2d3h5s')
598 | >>> h4 = BadugiHand('Ac2d3h4s')
599 | >>> h0 < h1 < h2 < h3 < h4
600 | True
601 |
602 | >>> h = BadugiHand('Ac2d3c4s5c')
603 | Traceback (most recent call last):
604 | ...
605 | ValueError: The cards 'Ac2d3c4s5c' form an invalid BadugiHand hand.
606 | >>> h = BadugiHand('Ac2d3c4s')
607 | Traceback (most recent call last):
608 | ...
609 | ValueError: The cards 'Ac2d3c4s' form an invalid BadugiHand hand.
610 | >>> h = BadugiHand('AcAd3h4s')
611 | Traceback (most recent call last):
612 | ...
613 | ValueError: The cards 'AcAd3h4s' form an invalid BadugiHand hand.
614 | >>> h = BadugiHand('Ac2c')
615 | Traceback (most recent call last):
616 | ...
617 | ValueError: The cards 'Ac2c' form an invalid BadugiHand hand.
618 | >>> h = BadugiHand(())
619 | Traceback (most recent call last):
620 | ...
621 | ValueError: The cards () form an invalid BadugiHand hand.
622 | """
623 |
624 | lookup = BadugiLookup()
625 | low = True
626 |
627 | @classmethod
628 | def from_game(
629 | cls,
630 | hole_cards: CardsLike,
631 | board_cards: CardsLike = (),
632 | ) -> Hand:
633 | """Create a poker hand from a game setting.
634 |
635 | In a game setting, a player uses private cards from their hole
636 | and the public cards from the board to make their hand.
637 |
638 | >>> h0 = BadugiHand.from_game('2s4c5d6h')
639 | >>> h1 = BadugiHand('2s4c5d6h')
640 | >>> h0 == h1
641 | True
642 | >>> h0 = BadugiHand.from_game('2s3s4d7h')
643 | >>> h1 = BadugiHand('2s4d7h')
644 | >>> h0 == h1
645 | True
646 | >>> h0 = BadugiHand.from_game('KcKdKhKs')
647 | >>> h1 = BadugiHand('Ks')
648 | >>> h0 == h1
649 | True
650 | >>> h0 = BadugiHand.from_game('Ac2c3c4c')
651 | >>> h1 = BadugiHand('Ac')
652 | >>> h0 == h1
653 | True
654 |
655 | >>> h0 = BadugiHand.from_game('AcAdAhAs')
656 | >>> h1 = BadugiHand('As')
657 | >>> h0 == h1
658 | True
659 | >>> h0 = StandardBadugiHand.from_game('Ac2c3c4c')
660 | >>> h1 = StandardBadugiHand('2c')
661 | >>> h0 == h1
662 | True
663 |
664 | >>> h0 = StandardBadugiHand.from_game('2s3d3c4d')
665 | >>> h1 = StandardBadugiHand.from_game('2s3c3d4d')
666 | >>> h0 == h1
667 | True
668 |
669 | :param hole_cards: The hole cards.
670 | :param board_cards: The optional board cards.
671 | :return: The strongest hand from possible card combinations.
672 | """
673 | cards = tuple(chain(Card.clean(hole_cards), Card.clean(board_cards)))
674 | max_hand = None
675 |
676 | for count in range(4, 0, -1):
677 | for combination in combinations(cards, count):
678 | try:
679 | hand = cls(combination)
680 | except ValueError:
681 | pass
682 | else:
683 | if max_hand is None or hand > max_hand:
684 | max_hand = hand
685 |
686 | if max_hand is not None:
687 | break
688 |
689 | if max_hand is None:
690 | raise ValueError(
691 | (
692 | f'No valid {type(cls).__qualname__} hand can be formed'
693 | ' from the hole and board cards.'
694 | ),
695 | )
696 |
697 | return max_hand
698 |
699 |
700 | class StandardBadugiHand(BadugiHand):
701 | """The class for standard badugi hands."""
702 |
703 | lookup = StandardBadugiLookup()
704 |
705 |
706 | class KuhnPokerHand(Hand):
707 | """The class for Kuhn poker hands.
708 |
709 | >>> h0 = KuhnPokerHand('Js')
710 | >>> h1 = KuhnPokerHand('Qs')
711 | >>> h2 = KuhnPokerHand('Ks')
712 | >>> h0 < h1 < h2
713 | True
714 |
715 | >>> h = KuhnPokerHand('As')
716 | Traceback (most recent call last):
717 | ...
718 | ValueError: The cards 'As' form an invalid KuhnPokerHand hand.
719 | """
720 |
721 | lookup = KuhnPokerLookup()
722 | low = False
723 |
724 | @classmethod
725 | def from_game(
726 | cls,
727 | hole_cards: CardsLike,
728 | board_cards: CardsLike = (),
729 | ) -> Hand:
730 | """Create a poker hand from a game setting.
731 |
732 | In a game setting, a player uses private cards from their hole
733 | and the public cards from the board to make their hand.
734 |
735 | >>> h0 = KuhnPokerHand.from_game('Ks')
736 | >>> h1 = KuhnPokerHand('Ks')
737 | >>> h0 == h1
738 | True
739 |
740 | :param hole_cards: The hole cards.
741 | :param board_cards: The optional board cards.
742 | :return: The strongest hand from possible card combinations.
743 | """
744 | return max(
745 | map(cls, chain(Card.clean(hole_cards), Card.clean(board_cards))),
746 | )
747 |
--------------------------------------------------------------------------------
/pokerkit/lookups.py:
--------------------------------------------------------------------------------
1 | """:mod:`pokerkit.lookups` implements classes related to poker hand
2 | lookups.
3 | """
4 |
5 | from abc import ABC, abstractmethod
6 | from collections.abc import Iterable, Reversible, Sequence
7 | from collections import Counter
8 | from dataclasses import dataclass, field, replace
9 | from enum import StrEnum, unique
10 | from functools import partial
11 | from itertools import combinations, filterfalse
12 | from math import prod
13 | from operator import contains
14 | from typing import ClassVar
15 |
16 | from pokerkit.utilities import Card, CardsLike, Rank, RankOrder
17 |
18 |
19 | @unique
20 | class Label(StrEnum):
21 | """The enum class for all hand classification labels.
22 |
23 | >>> Label.ONE_PAIR
24 |
25 | >>> Label.FULL_HOUSE
26 |
27 | """
28 |
29 | HIGH_CARD: str = 'High card'
30 | """The label of high cards."""
31 | ONE_PAIR: str = 'One pair'
32 | """The label of one pair."""
33 | TWO_PAIR: str = 'Two pair'
34 | """The label of two pair."""
35 | THREE_OF_A_KIND: str = 'Three of a kind'
36 | """The label of three of a kind."""
37 | STRAIGHT: str = 'Straight'
38 | """The label of straight."""
39 | FLUSH: str = 'Flush'
40 | """The label of flush."""
41 | FULL_HOUSE: str = 'Full house'
42 | """The label of full house."""
43 | FOUR_OF_A_KIND: str = 'Four of a kind'
44 | """The label of four of a kind."""
45 | STRAIGHT_FLUSH: str = 'Straight flush'
46 | """The label of straight flush."""
47 |
48 |
49 | @dataclass(order=True, frozen=True)
50 | class Entry:
51 | """The class for hand lookup entries.
52 |
53 | The :attr:`index` represents the strength of the corresponding
54 | hand. In this library, a stronger hand is considered greater. In
55 | other words, stronger hands have either a greater or less (depending
56 | on whether using a high/low hand) index with which different entries
57 | and hands are compared.
58 |
59 | The attributes are read-only.
60 |
61 | Note that the entries in the example below are meaningless. They
62 | are only meant to show how entries are used by other poker
63 | utilities.
64 |
65 | >>> e0 = Entry(0, Label.HIGH_CARD)
66 | >>> e1 = Entry(1, Label.HIGH_CARD)
67 | >>> e2 = Entry(1, Label.HIGH_CARD)
68 | >>> e0 < e1
69 | True
70 | >>> e1 < e2
71 | False
72 | >>> e0 == e1
73 | False
74 | >>> e1 == e2
75 | True
76 | >>> e0.index
77 | 0
78 | >>> e0.label
79 |
80 |
81 | :param index: The index of the entry.
82 | :param label: The label of the hand.
83 | """
84 |
85 | index: int
86 | """The index of the corresponding hand."""
87 | label: Label = field(compare=False)
88 | """The label of the corresponding hand."""
89 |
90 |
91 | @dataclass
92 | class Lookup(ABC):
93 | """The abstract base class for hand lookups.
94 |
95 | Lookups are used internally by hands. If you want to evaluate poker
96 | hands, please use one of the hand classes.
97 |
98 | >>> lookup = StandardLookup()
99 | >>> e0 = lookup.get_entry('As3sQhJsJc')
100 | >>> e1 = lookup.get_entry('2s4sKhKsKc')
101 | >>> e0 < e1
102 | True
103 | >>> e0.label
104 |
105 | >>> e1.label
106 |
107 | """
108 |
109 | __primes = 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41
110 |
111 | assert len(__primes) >= len(tuple(Rank)) - 1 # except unknown
112 |
113 | __multipliers = dict(zip(Rank, __primes))
114 | rank_order: ClassVar[RankOrder]
115 | """The rank order."""
116 | __entries: dict[tuple[int, bool], Entry] = field(
117 | default_factory=dict,
118 | init=False,
119 | repr=False,
120 | )
121 | __entry_count: int = field(default=0, init=False, repr=False)
122 |
123 | @classmethod
124 | def __hash(cls, ranks: Iterable[Rank]) -> int:
125 | return prod(map(cls.__multipliers.__getitem__, ranks))
126 |
127 | @classmethod
128 | def __hash_multisets(
129 | cls,
130 | ranks: Reversible[Rank],
131 | counter: Counter[int],
132 | ) -> Sequence[int]:
133 | if not counter:
134 | return (cls.__hash(()),)
135 |
136 | hashes = []
137 | multiplicity = max(counter)
138 | count = counter.pop(multiplicity)
139 |
140 | for samples in combinations(reversed(ranks), count):
141 | hash_ = cls.__hash(samples) ** multiplicity
142 |
143 | for partial_hash in cls.__hash_multisets(
144 | tuple(filterfalse(partial(contains, samples), ranks)),
145 | counter,
146 | ):
147 | hashes.append(hash_ * partial_hash)
148 |
149 | counter[multiplicity] = count
150 |
151 | return hashes
152 |
153 | def __post_init__(self) -> None:
154 | self._add_entries()
155 | self.__reset_ranks()
156 |
157 | @abstractmethod
158 | def _add_entries(self) -> None:
159 | pass # pragma: no cover
160 |
161 | def __reset_ranks(self) -> None:
162 | indices = set()
163 |
164 | for entry in self.__entries.values():
165 | indices.add(entry.index)
166 |
167 | sorted_indices = sorted(indices)
168 | reset_indices = dict(zip(sorted_indices, range(len(indices))))
169 |
170 | for key, value in self.__entries.items():
171 | self.__entries[key] = replace(
172 | value,
173 | index=reset_indices[value.index],
174 | )
175 |
176 | def has_entry(self, cards: CardsLike) -> bool:
177 | """Return whether the cards can be looked up.
178 |
179 | The cards can be looked up if the lookup contains an entry with
180 | the given cards.
181 |
182 | >>> lookup = ShortDeckHoldemLookup()
183 | >>> lookup.has_entry('Ah6h7s8c9s')
184 | True
185 | >>> lookup.has_entry('Ah6h7s8c2s')
186 | False
187 |
188 | :param cards: The cards to look up.
189 | :return: ``True`` if the cards can looked up, otherwise
190 | ``False``.
191 | """
192 | try:
193 | key = self._get_key(cards)
194 | except ValueError:
195 | key = None
196 |
197 | return key in self.__entries
198 |
199 | def get_entry(self, cards: CardsLike) -> Entry:
200 | """Return the corresponding lookup entry of the hand that the
201 | cards form.
202 |
203 | >>> lookup = ShortDeckHoldemLookup()
204 | >>> entry = lookup.get_entry('Ah6h7s8c9s')
205 | >>> entry.index
206 | 1128
207 | >>> entry.label
208 |
209 | >>> entry = lookup.get_entry('Ah6h7s8c2s')
210 | Traceback (most recent call last):
211 | ...
212 | ValueError: The cards 'Ah6h7s8c2s' form an invalid hand.
213 |
214 | :param cards: The cards to look up.
215 | :return: The corresponding lookup entry.
216 | :raises ValueError: If cards do not form a valid hand.
217 | """
218 | key = self._get_key(cards)
219 |
220 | if key not in self.__entries:
221 | raise ValueError(f'The cards {repr(cards)} form an invalid hand.')
222 |
223 | return self.__entries[key]
224 |
225 | def get_entry_or_none(self, cards: CardsLike) -> Entry | None:
226 | """Return the corresponding lookup entry of the hand that the
227 | cards form if it exists. Otherwise, return ``None``.
228 |
229 | >>> lookup = ShortDeckHoldemLookup()
230 | >>> lookup.get_entry('Ah6h7s8c2s')
231 | Traceback (most recent call last):
232 | ...
233 | ValueError: The cards 'Ah6h7s8c2s' form an invalid hand.
234 | >>> lookup.get_entry_or_none('Ah6h7s8c2s') is None
235 | True
236 |
237 | :param cards: The cards to look up.
238 | :return: The optional corresponding lookup entry.
239 | """
240 | return self.__entries.get(self._get_key(cards))
241 |
242 | def _get_key(self, cards: CardsLike) -> tuple[int, bool]:
243 | cards = Card.clean(cards)
244 | hash_ = self.__hash(Card.get_ranks(cards))
245 | suitedness = Card.are_suited(cards)
246 |
247 | return hash_, suitedness
248 |
249 | def _add_multisets(
250 | self,
251 | counter: Counter[int],
252 | suitednesses: tuple[bool, ...],
253 | label: Label,
254 | ) -> None:
255 | hashes = self.__hash_multisets(self.rank_order, counter)
256 |
257 | for hash_ in reversed(hashes):
258 | self.__add_entry(hash_, suitednesses, label)
259 |
260 | def _add_straights(
261 | self,
262 | count: int,
263 | suitednesses: tuple[bool, ...],
264 | label: Label,
265 | ) -> None:
266 | self.__add_entry(
267 | self.__hash(self.rank_order[-1:] + self.rank_order[: count - 1]),
268 | suitednesses,
269 | label,
270 | )
271 |
272 | for i in range(len(self.rank_order) - count + 1):
273 | self.__add_entry(
274 | self.__hash(self.rank_order[i:i + count]),
275 | suitednesses,
276 | label,
277 | )
278 |
279 | def __add_entry(
280 | self,
281 | hash_: int,
282 | suitednesses: Iterable[bool],
283 | label: Label,
284 | ) -> None:
285 | entry = Entry(self.__entry_count, label)
286 | self.__entry_count += 1
287 |
288 | for suitedness in suitednesses:
289 | self.__entries[hash_, suitedness] = entry
290 |
291 |
292 | @dataclass
293 | class StandardLookup(Lookup):
294 | """The class for standard hand lookups.
295 |
296 | Lookups are used by evaluators. If you want to evaluate poker hands,
297 | please subclasses of :class:`pokerkit.hands.Hand` that use this
298 | lookup.
299 |
300 | >>> lookup = StandardLookup()
301 | >>> e0 = lookup.get_entry('Ah6h7s8c9s')
302 | >>> e1 = lookup.get_entry('AhAc6s6hTd')
303 | >>> e2 = lookup.get_entry('AcAdAhAsAc')
304 | Traceback (most recent call last):
305 | ...
306 | ValueError: The cards 'AcAdAhAsAc' form an invalid hand.
307 | >>> e0 < e1
308 | True
309 | >>> e0.label
310 |
311 | >>> e1.label
312 |
313 | """
314 |
315 | rank_order = RankOrder.STANDARD
316 |
317 | def _add_entries(self) -> None:
318 | self._add_multisets(Counter({1: 5}), (False,), Label.HIGH_CARD)
319 | self._add_multisets(Counter({2: 1, 1: 3}), (False,), Label.ONE_PAIR)
320 | self._add_multisets(Counter({2: 2, 1: 1}), (False,), Label.TWO_PAIR)
321 | self._add_multisets(
322 | Counter({3: 1, 1: 2}),
323 | (False,),
324 | Label.THREE_OF_A_KIND,
325 | )
326 | self._add_straights(5, (False,), Label.STRAIGHT)
327 | self._add_multisets(Counter({1: 5}), (True,), Label.FLUSH)
328 | self._add_multisets(Counter({3: 1, 2: 1}), (False,), Label.FULL_HOUSE)
329 | self._add_multisets(
330 | Counter({4: 1, 1: 1}),
331 | (False,),
332 | Label.FOUR_OF_A_KIND,
333 | )
334 | self._add_straights(5, (True,), Label.STRAIGHT_FLUSH)
335 |
336 |
337 | @dataclass
338 | class ShortDeckHoldemLookup(Lookup):
339 | """The class for short-deck hold'em hand lookups.
340 |
341 | Here, flushes beat full houses.
342 |
343 | Lookups are used by evaluators. If you want to evaluate poker hands,
344 | please use :class:`pokerkit.hands.ShortDeckHoldemHand`.
345 |
346 | >>> lookup = ShortDeckHoldemLookup()
347 | >>> e0 = lookup.get_entry('AhAc6s6hTd')
348 | >>> e1 = lookup.get_entry('Ah6h7s8c9s')
349 | >>> e2 = lookup.get_entry('Ah2h3s4c5s')
350 | Traceback (most recent call last):
351 | ...
352 | ValueError: The cards 'Ah2h3s4c5s' form an invalid hand.
353 | >>> e0 < e1
354 | True
355 | >>> e0.label
356 |
357 | >>> e1.label
358 |
359 | """
360 |
361 | rank_order = RankOrder.SHORT_DECK_HOLDEM
362 |
363 | def _add_entries(self) -> None:
364 | self._add_multisets(Counter({1: 5}), (False,), Label.HIGH_CARD)
365 | self._add_multisets(Counter({2: 1, 1: 3}), (False,), Label.ONE_PAIR)
366 | self._add_multisets(Counter({2: 2, 1: 1}), (False,), Label.TWO_PAIR)
367 | self._add_multisets(
368 | Counter({3: 1, 1: 2}),
369 | (False,),
370 | Label.THREE_OF_A_KIND,
371 | )
372 | self._add_straights(5, (False,), Label.STRAIGHT)
373 | self._add_multisets(Counter({3: 1, 2: 1}), (False,), Label.FULL_HOUSE)
374 | self._add_multisets(Counter({1: 5}), (True,), Label.FLUSH)
375 | self._add_multisets(
376 | Counter({4: 1, 1: 1}),
377 | (False,),
378 | Label.FOUR_OF_A_KIND,
379 | )
380 | self._add_straights(5, (True,), Label.STRAIGHT_FLUSH)
381 |
382 |
383 | @dataclass
384 | class EightOrBetterLookup(Lookup):
385 | """The class for eight or better hand lookups.
386 |
387 | Lookups are used by evaluators. If you want to evaluate poker hands,
388 | please use :class:`pokerkit.hands.EightOrBetterLowHand`.
389 | """
390 |
391 | rank_order = RankOrder.EIGHT_OR_BETTER_LOW
392 |
393 | def _add_entries(self) -> None:
394 | self._add_multisets(Counter({1: 5}), (False, True), Label.HIGH_CARD)
395 |
396 |
397 | @dataclass
398 | class RegularLookup(Lookup):
399 | """The class for regular hand lookups.
400 |
401 | Here, flushes are ignored.
402 |
403 | Lookups are used by evaluators. If you want to evaluate poker hands,
404 | please use :class:`pokerkit.hands.RegularLowHand`.
405 |
406 | >>> lookup = RegularLookup()
407 | >>> e0 = lookup.get_entry('Ah6h7s8c9s')
408 | >>> e1 = lookup.get_entry('AhAc6s6hTd')
409 | >>> e2 = lookup.get_entry('3s4sQhTc')
410 | Traceback (most recent call last):
411 | ...
412 | ValueError: The cards '3s4sQhTc' form an invalid hand.
413 | >>> e0 < e1
414 | True
415 | >>> e0.label
416 |
417 | >>> e1.label
418 |
419 | """
420 |
421 | rank_order = RankOrder.REGULAR
422 |
423 | def _add_entries(self) -> None:
424 | self._add_multisets(Counter({1: 5}), (False, True), Label.HIGH_CARD)
425 | self._add_multisets(Counter({2: 1, 1: 3}), (False,), Label.ONE_PAIR)
426 | self._add_multisets(Counter({2: 2, 1: 1}), (False,), Label.TWO_PAIR)
427 | self._add_multisets(
428 | Counter({3: 1, 1: 2}),
429 | (False,),
430 | Label.THREE_OF_A_KIND,
431 | )
432 | self._add_multisets(Counter({3: 1, 2: 1}), (False,), Label.FULL_HOUSE)
433 | self._add_multisets(
434 | Counter({4: 1, 1: 1}),
435 | (False,),
436 | Label.FOUR_OF_A_KIND,
437 | )
438 |
439 |
440 | @dataclass
441 | class BadugiLookup(Lookup):
442 | """The class for badugi hand lookups.
443 |
444 | Lookups are used by evaluators. If you want to evaluate poker hands,
445 | please use :class:`pokerkit.hands.BadugiHand`.
446 |
447 | >>> lookup = BadugiLookup()
448 | >>> e0 = lookup.get_entry('2s')
449 | >>> e1 = lookup.get_entry('KhQc')
450 | >>> e2 = lookup.get_entry('AcAdAhAs')
451 | Traceback (most recent call last):
452 | ...
453 | ValueError: The cards 'AcAdAhAs' form an invalid hand.
454 | >>> e0 > e1
455 | True
456 | >>> e0.label
457 |
458 | >>> e1.label
459 |
460 | """
461 |
462 | rank_order = RankOrder.REGULAR
463 |
464 | def _add_entries(self) -> None:
465 | for i in range(4, 0, -1):
466 | self._add_multisets(Counter({1: i}), (i == 1,), Label.HIGH_CARD)
467 |
468 | def _get_key(self, cards: CardsLike) -> tuple[int, bool]:
469 | cards = Card.clean(cards)
470 |
471 | if not Card.are_rainbow(cards):
472 | raise ValueError(
473 | (
474 | 'Badugi hands must be rainbow (i.e. of distinct suits) but'
475 | f' the cards {repr(cards)} are not.'
476 | ),
477 | )
478 |
479 | return super()._get_key(cards)
480 |
481 |
482 | @dataclass
483 | class StandardBadugiLookup(BadugiLookup):
484 | """The class for standard badugi hand lookups.
485 |
486 | Lookups are used by evaluators. If you want to evaluate poker hands,
487 | please use :class:`pokerkit.hands.StandardBadugiHand`.
488 | """
489 |
490 | rank_order = RankOrder.STANDARD
491 |
492 |
493 | @dataclass
494 | class KuhnPokerLookup(Lookup):
495 | """The class for Kuhn poker hand lookups.
496 |
497 | Lookups are used by evaluators. If you want to evaluate poker hands,
498 | please use :class:`pokerkit.hands.KuhnPokerHand`.
499 |
500 | >>> lookup = KuhnPokerLookup()
501 | >>> e0 = lookup.get_entry('J?')
502 | >>> e1 = lookup.get_entry('Q?')
503 | >>> e2 = lookup.get_entry('2?')
504 | Traceback (most recent call last):
505 | ...
506 | ValueError: The cards '2?' form an invalid hand.
507 | >>> e0 < e1
508 | True
509 | >>> e0.label
510 |
511 | >>> e1.label
512 |
513 | """
514 |
515 | rank_order = RankOrder.KUHN_POKER
516 |
517 | def _add_entries(self) -> None:
518 | self._add_multisets(Counter({1: 1}), (True,), Label.HIGH_CARD)
519 |
--------------------------------------------------------------------------------
/pokerkit/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WTEngineer/pockerkit_python/4ed4a492b7e56bffccd25364af0827147ff872c7/pokerkit/py.typed
--------------------------------------------------------------------------------
/pokerkit/tests/__init__.py:
--------------------------------------------------------------------------------
1 | """:mod:`pokerkit.tests` is the package for the unit tests in the
2 | PokerKit library.
3 | """
4 |
--------------------------------------------------------------------------------
/pokerkit/tests/test_analysis.py:
--------------------------------------------------------------------------------
1 | """:mod:`pokerkit.tests.test_analysis` implements unit tests for
2 | analysis related tools on PokerKit.
3 | """
4 |
5 | from concurrent.futures import ProcessPoolExecutor
6 | from unittest import TestCase, main
7 |
8 | from pokerkit.analysis import calculate_equities, parse_range
9 | from pokerkit.hands import StandardHighHand
10 | from pokerkit.utilities import Card, Deck
11 |
12 |
13 | class HandHistoryTestCase(TestCase):
14 | def test_parse_range(self) -> None:
15 | self.assertSetEqual(
16 | parse_range('JJ'),
17 | set(
18 | map(
19 | frozenset,
20 | (
21 | Card.parse('JcJd'),
22 | Card.parse('JcJh'),
23 | Card.parse('JcJs'),
24 | Card.parse('JdJh'),
25 | Card.parse('JdJs'),
26 | Card.parse('JhJs'),
27 | ),
28 | ),
29 | ),
30 | )
31 | self.assertSetEqual(
32 | parse_range('AK'),
33 | set(
34 | map(
35 | frozenset,
36 | (
37 | Card.parse('AcKd'),
38 | Card.parse('AcKh'),
39 | Card.parse('AcKs'),
40 | Card.parse('AdKc'),
41 | Card.parse('AdKh'),
42 | Card.parse('AdKs'),
43 | Card.parse('AhKc'),
44 | Card.parse('AhKd'),
45 | Card.parse('AhKs'),
46 | Card.parse('AsKc'),
47 | Card.parse('AsKd'),
48 | Card.parse('AsKh'),
49 | Card.parse('AcKc'),
50 | Card.parse('AdKd'),
51 | Card.parse('AhKh'),
52 | Card.parse('AsKs'),
53 | ),
54 | ),
55 | ),
56 | )
57 | self.assertSetEqual(
58 | parse_range('QJs'),
59 | set(
60 | map(
61 | frozenset,
62 | (
63 | Card.parse('QcJc'),
64 | Card.parse('QdJd'),
65 | Card.parse('QhJh'),
66 | Card.parse('QsJs'),
67 | ),
68 | ),
69 | ),
70 | )
71 | self.assertSetEqual(
72 | parse_range('QTs'),
73 | set(
74 | map(
75 | frozenset,
76 | (
77 | Card.parse('QcTc'),
78 | Card.parse('QdTd'),
79 | Card.parse('QhTh'),
80 | Card.parse('QsTs'),
81 | ),
82 | ),
83 | ),
84 | )
85 | self.assertSetEqual(
86 | parse_range('AKo'),
87 | set(
88 | map(
89 | frozenset,
90 | (
91 | Card.parse('AcKd'),
92 | Card.parse('AcKh'),
93 | Card.parse('AcKs'),
94 | Card.parse('AdKc'),
95 | Card.parse('AdKh'),
96 | Card.parse('AdKs'),
97 | Card.parse('AhKc'),
98 | Card.parse('AhKd'),
99 | Card.parse('AhKs'),
100 | Card.parse('AsKc'),
101 | Card.parse('AsKd'),
102 | Card.parse('AsKh'),
103 | ),
104 | ),
105 | ),
106 | )
107 | self.assertSetEqual(
108 | parse_range('JJ+'),
109 | set(
110 | map(
111 | frozenset,
112 | (
113 | Card.parse('JcJd'),
114 | Card.parse('JcJh'),
115 | Card.parse('JcJs'),
116 | Card.parse('JdJh'),
117 | Card.parse('JdJs'),
118 | Card.parse('JhJs'),
119 | Card.parse('QcQd'),
120 | Card.parse('QcQh'),
121 | Card.parse('QcQs'),
122 | Card.parse('QdQh'),
123 | Card.parse('QdQs'),
124 | Card.parse('QhQs'),
125 | Card.parse('KcKd'),
126 | Card.parse('KcKh'),
127 | Card.parse('KcKs'),
128 | Card.parse('KdKh'),
129 | Card.parse('KdKs'),
130 | Card.parse('KhKs'),
131 | Card.parse('AcAd'),
132 | Card.parse('AcAh'),
133 | Card.parse('AcAs'),
134 | Card.parse('AdAh'),
135 | Card.parse('AdAs'),
136 | Card.parse('AhAs'),
137 | ),
138 | ),
139 | ),
140 | )
141 | self.assertSetEqual(
142 | parse_range('QT+'),
143 | set(
144 | map(
145 | frozenset,
146 | (
147 | Card.parse('QcTd'),
148 | Card.parse('QcTh'),
149 | Card.parse('QcTs'),
150 | Card.parse('QdTc'),
151 | Card.parse('QdTh'),
152 | Card.parse('QdTs'),
153 | Card.parse('QhTc'),
154 | Card.parse('QhTd'),
155 | Card.parse('QhTs'),
156 | Card.parse('QsTc'),
157 | Card.parse('QsTd'),
158 | Card.parse('QsTh'),
159 | Card.parse('QcTc'),
160 | Card.parse('QdTd'),
161 | Card.parse('QhTh'),
162 | Card.parse('QsTs'),
163 | Card.parse('QcJd'),
164 | Card.parse('QcJh'),
165 | Card.parse('QcJs'),
166 | Card.parse('QdJc'),
167 | Card.parse('QdJh'),
168 | Card.parse('QdJs'),
169 | Card.parse('QhJc'),
170 | Card.parse('QhJd'),
171 | Card.parse('QhJs'),
172 | Card.parse('QsJc'),
173 | Card.parse('QsJd'),
174 | Card.parse('QsJh'),
175 | Card.parse('QcJc'),
176 | Card.parse('QdJd'),
177 | Card.parse('QhJh'),
178 | Card.parse('QsJs'),
179 | ),
180 | ),
181 | ),
182 | )
183 | self.assertSetEqual(
184 | parse_range('JTs+'),
185 | set(
186 | map(
187 | frozenset,
188 | (
189 | Card.parse('JcTc'),
190 | Card.parse('JdTd'),
191 | Card.parse('JhTh'),
192 | Card.parse('JsTs'),
193 | ),
194 | ),
195 | ),
196 | )
197 | self.assertSetEqual(
198 | parse_range('JTo+'),
199 | set(
200 | map(
201 | frozenset,
202 | (
203 | Card.parse('JcTd'),
204 | Card.parse('JcTh'),
205 | Card.parse('JcTs'),
206 | Card.parse('JdTc'),
207 | Card.parse('JdTh'),
208 | Card.parse('JdTs'),
209 | Card.parse('JhTc'),
210 | Card.parse('JhTd'),
211 | Card.parse('JhTs'),
212 | Card.parse('JsTc'),
213 | Card.parse('JsTd'),
214 | Card.parse('JsTh'),
215 | ),
216 | ),
217 | ),
218 | )
219 | self.assertSetEqual(
220 | parse_range('JJ-TT'),
221 | set(
222 | map(
223 | frozenset,
224 | (
225 | Card.parse('JcJd'),
226 | Card.parse('JcJh'),
227 | Card.parse('JcJs'),
228 | Card.parse('JdJh'),
229 | Card.parse('JdJs'),
230 | Card.parse('JhJs'),
231 | Card.parse('TcTd'),
232 | Card.parse('TcTh'),
233 | Card.parse('TcTs'),
234 | Card.parse('TdTh'),
235 | Card.parse('TdTs'),
236 | Card.parse('ThTs'),
237 | ),
238 | ),
239 | ),
240 | )
241 | self.assertSetEqual(
242 | parse_range('JTs-KQs'),
243 | set(
244 | map(
245 | frozenset,
246 | (
247 | Card.parse('JcTc'),
248 | Card.parse('JdTd'),
249 | Card.parse('JhTh'),
250 | Card.parse('JsTs'),
251 | Card.parse('QcJc'),
252 | Card.parse('QdJd'),
253 | Card.parse('QhJh'),
254 | Card.parse('QsJs'),
255 | Card.parse('KcQc'),
256 | Card.parse('KdQd'),
257 | Card.parse('KhQh'),
258 | Card.parse('KsQs'),
259 | ),
260 | ),
261 | ),
262 | )
263 | self.assertSetEqual(
264 | parse_range('AsKh'),
265 | {frozenset(Card.parse('AsKh'))},
266 | )
267 | self.assertSetEqual(
268 | parse_range('2s3s'),
269 | set(map(frozenset, (Card.parse('2s3s'),))),
270 | )
271 | self.assertSetEqual(
272 | parse_range('4s5h;2s3s'),
273 | set(map(frozenset, (Card.parse('4s5h'), Card.parse('2s3s')))),
274 | )
275 | self.assertSetEqual(
276 | parse_range('7s8s 27 AK', 'ATs+'),
277 | set(
278 | map(
279 | frozenset,
280 | (
281 | Card.parse('7s8s'),
282 | Card.parse('2c7d'),
283 | Card.parse('2c7h'),
284 | Card.parse('2c7s'),
285 | Card.parse('2d7c'),
286 | Card.parse('2d7h'),
287 | Card.parse('2d7s'),
288 | Card.parse('2h7c'),
289 | Card.parse('2h7d'),
290 | Card.parse('2h7s'),
291 | Card.parse('2s7c'),
292 | Card.parse('2s7d'),
293 | Card.parse('2s7h'),
294 | Card.parse('2c7c'),
295 | Card.parse('2d7d'),
296 | Card.parse('2h7h'),
297 | Card.parse('2s7s'),
298 | Card.parse('AcKd'),
299 | Card.parse('AcKh'),
300 | Card.parse('AcKs'),
301 | Card.parse('AdKc'),
302 | Card.parse('AdKh'),
303 | Card.parse('AdKs'),
304 | Card.parse('AhKc'),
305 | Card.parse('AhKd'),
306 | Card.parse('AhKs'),
307 | Card.parse('AsKc'),
308 | Card.parse('AsKd'),
309 | Card.parse('AsKh'),
310 | Card.parse('AcKc'),
311 | Card.parse('AdKd'),
312 | Card.parse('AhKh'),
313 | Card.parse('AsKs'),
314 | Card.parse('AcQc'),
315 | Card.parse('AdQd'),
316 | Card.parse('AhQh'),
317 | Card.parse('AsQs'),
318 | Card.parse('AcJc'),
319 | Card.parse('AdJd'),
320 | Card.parse('AhJh'),
321 | Card.parse('AsJs'),
322 | Card.parse('AcTc'),
323 | Card.parse('AdTd'),
324 | Card.parse('AhTh'),
325 | Card.parse('AsTs'),
326 | ),
327 | ),
328 | ),
329 | )
330 | self.assertSetEqual(
331 | parse_range('99+', '2h7s'),
332 | set(
333 | map(
334 | frozenset,
335 | (
336 | Card.parse('9c9d'),
337 | Card.parse('9c9h'),
338 | Card.parse('9c9s'),
339 | Card.parse('9d9h'),
340 | Card.parse('9d9s'),
341 | Card.parse('9h9s'),
342 | Card.parse('TcTd'),
343 | Card.parse('TcTh'),
344 | Card.parse('TcTs'),
345 | Card.parse('TdTh'),
346 | Card.parse('TdTs'),
347 | Card.parse('ThTs'),
348 | Card.parse('JcJd'),
349 | Card.parse('JcJh'),
350 | Card.parse('JcJs'),
351 | Card.parse('JdJh'),
352 | Card.parse('JdJs'),
353 | Card.parse('JhJs'),
354 | Card.parse('QcQd'),
355 | Card.parse('QcQh'),
356 | Card.parse('QcQs'),
357 | Card.parse('QdQh'),
358 | Card.parse('QdQs'),
359 | Card.parse('QhQs'),
360 | Card.parse('KcKd'),
361 | Card.parse('KcKh'),
362 | Card.parse('KcKs'),
363 | Card.parse('KdKh'),
364 | Card.parse('KdKs'),
365 | Card.parse('KhKs'),
366 | Card.parse('AcAd'),
367 | Card.parse('AcAh'),
368 | Card.parse('AcAs'),
369 | Card.parse('AdAh'),
370 | Card.parse('AdAs'),
371 | Card.parse('AhAs'),
372 | Card.parse('2h7s'),
373 | ),
374 | ),
375 | ),
376 | )
377 |
378 | def test_calculate_equities(self) -> None:
379 | with ProcessPoolExecutor() as executor:
380 | equities = calculate_equities(
381 | (parse_range('AsKs'), parse_range('2h2c')),
382 | (),
383 | 2,
384 | 5,
385 | Deck.STANDARD,
386 | (StandardHighHand,),
387 | sample_count=10000,
388 | executor=executor,
389 | )
390 |
391 | self.assertEqual(len(equities), 2)
392 | self.assertAlmostEqual(sum(equities), 1)
393 |
394 | equities = calculate_equities(
395 | (parse_range('JsTs'), parse_range('AhAd')),
396 | Card.parse('9s8s2c'),
397 | 2,
398 | 5,
399 | Deck.STANDARD,
400 | (StandardHighHand,),
401 | sample_count=10000,
402 | executor=executor,
403 | )
404 |
405 | self.assertEqual(len(equities), 2)
406 | self.assertAlmostEqual(sum(equities), 1)
407 |
408 | equities = calculate_equities(
409 | (parse_range('AKs'), parse_range('22')),
410 | (),
411 | 2,
412 | 5,
413 | Deck.STANDARD,
414 | (StandardHighHand,),
415 | sample_count=10000,
416 | executor=executor,
417 | )
418 |
419 | self.assertEqual(len(equities), 2)
420 | self.assertAlmostEqual(sum(equities), 1)
421 |
422 | equities = calculate_equities(
423 | (parse_range('AA'), parse_range('22')),
424 | (),
425 | 2,
426 | 5,
427 | Deck.STANDARD,
428 | (StandardHighHand,),
429 | sample_count=10000,
430 | executor=executor,
431 | )
432 |
433 | self.assertEqual(len(equities), 2)
434 | self.assertAlmostEqual(sum(equities), 1)
435 |
436 | equities = calculate_equities(
437 | (
438 | parse_range('2h2c'),
439 | parse_range('3h3c'),
440 | parse_range('AsKs'),
441 | ),
442 | Card.parse('QsJsTs'),
443 | 2,
444 | 5,
445 | Deck.STANDARD,
446 | (StandardHighHand,),
447 | sample_count=10000,
448 | executor=executor,
449 | )
450 |
451 | self.assertEqual(len(equities), 3)
452 | self.assertAlmostEqual(sum(equities), 1)
453 | self.assertAlmostEqual(equities[0], 0)
454 | self.assertAlmostEqual(equities[1], 0)
455 | self.assertAlmostEqual(equities[2], 1)
456 |
457 | equities = calculate_equities(
458 | (
459 | parse_range('2h2c'),
460 | parse_range('3h3c'),
461 | parse_range('AhKh'),
462 | ),
463 | Card.parse('3s3d4c'),
464 | 2,
465 | 5,
466 | Deck.STANDARD,
467 | (StandardHighHand,),
468 | sample_count=10000,
469 | executor=executor,
470 | )
471 |
472 | self.assertEqual(len(equities), 3)
473 | self.assertAlmostEqual(sum(equities), 1)
474 | self.assertAlmostEqual(equities[0], 0)
475 | self.assertAlmostEqual(equities[1], 1)
476 | self.assertAlmostEqual(equities[2], 0)
477 |
478 | equities = calculate_equities(
479 | (parse_range('3d3h'), parse_range('3c3s')),
480 | Card.parse('Tc8d6h4s'),
481 | 2,
482 | 5,
483 | Deck.STANDARD,
484 | (StandardHighHand,),
485 | sample_count=10000,
486 | executor=executor,
487 | )
488 |
489 | self.assertEqual(len(equities), 2)
490 | self.assertAlmostEqual(sum(equities), 1)
491 | self.assertAlmostEqual(equities[0], 0.5)
492 | self.assertAlmostEqual(equities[1], 0.5)
493 |
494 |
495 | if __name__ == '__main__':
496 | main() # pragma: no cover
497 |
--------------------------------------------------------------------------------
/pokerkit/tests/test_games.py:
--------------------------------------------------------------------------------
1 | """:mod:`pokerkit.tests.test_games` implements unit tests for
2 | :mod:`pokerkit.games`.
3 | """
4 |
5 | from unittest import main, TestCase
6 |
7 | from pokerkit.games import PotLimitOmahaHoldem
8 | from pokerkit.state import Automation, Mode
9 | from pokerkit.utilities import Card
10 |
11 |
12 | class PotLimitOmahaHoldemTestCase(TestCase):
13 | def test_double_board(self) -> None:
14 | state = PotLimitOmahaHoldem.create_state(
15 | (
16 | Automation.ANTE_POSTING,
17 | Automation.BET_COLLECTION,
18 | Automation.BLIND_OR_STRADDLE_POSTING,
19 | Automation.HOLE_CARDS_SHOWING_OR_MUCKING,
20 | Automation.HAND_KILLING,
21 | Automation.CHIPS_PUSHING,
22 | Automation.CHIPS_PULLING,
23 | ),
24 | True,
25 | 0,
26 | [1, 2],
27 | 2,
28 | 200,
29 | 4,
30 | starting_board_count=2,
31 | mode=Mode.CASH_GAME,
32 | )
33 |
34 | state.deal_hole('AcKc3c2c')
35 | state.deal_hole('AdKd3d2d')
36 | state.deal_hole('AhKh3h2h')
37 | state.deal_hole('AsKs3s2s')
38 | state.check_or_call()
39 | state.check_or_call()
40 | state.check_or_call()
41 | state.check_or_call()
42 |
43 | state.burn_card('??')
44 | state.deal_board('QcJcTc')
45 | state.deal_board('QdJdTd')
46 | state.check_or_call()
47 | state.check_or_call()
48 | state.check_or_call()
49 | state.check_or_call()
50 |
51 | state.burn_card('??')
52 | state.deal_board('9c')
53 | state.deal_board('9d')
54 | state.check_or_call()
55 | state.check_or_call()
56 | state.check_or_call()
57 | state.check_or_call()
58 |
59 | state.burn_card('??')
60 | state.deal_board('8c')
61 | state.deal_board('8d')
62 | state.check_or_call()
63 | state.check_or_call()
64 | state.check_or_call()
65 | state.check_or_call()
66 |
67 | self.assertFalse(state.status)
68 | self.assertEqual(state.stacks, [202, 202, 198, 198])
69 | self.assertEqual(state.board_count, 2)
70 | self.assertEqual(
71 | list(state.get_board_cards(0)),
72 | list(Card.parse('QcJcTc9c8c')),
73 | )
74 | self.assertEqual(
75 | list(state.get_board_cards(1)),
76 | list(Card.parse('QdJdTd9d8d')),
77 | )
78 |
79 | state = PotLimitOmahaHoldem.create_state(
80 | (
81 | Automation.ANTE_POSTING,
82 | Automation.BET_COLLECTION,
83 | Automation.BLIND_OR_STRADDLE_POSTING,
84 | Automation.HOLE_CARDS_SHOWING_OR_MUCKING,
85 | Automation.HAND_KILLING,
86 | Automation.CHIPS_PUSHING,
87 | Automation.CHIPS_PULLING,
88 | ),
89 | True,
90 | 0,
91 | [1, 2],
92 | 2,
93 | 200,
94 | 4,
95 | starting_board_count=2,
96 | mode=Mode.CASH_GAME,
97 | )
98 |
99 | state.deal_hole('AcKc3c2c')
100 | state.deal_hole('AdKd3d2d')
101 | state.deal_hole('AhKh3h2h')
102 | state.deal_hole('AsKs3s2s')
103 | state.complete_bet_or_raise_to(7)
104 | state.complete_bet_or_raise_to(24)
105 | state.complete_bet_or_raise_to(81)
106 | state.complete_bet_or_raise_to(200)
107 | state.check_or_call()
108 | state.check_or_call()
109 | state.check_or_call()
110 |
111 | state.select_runout_count(1)
112 | state.select_runout_count(1)
113 | state.select_runout_count(1)
114 | state.select_runout_count(1)
115 |
116 | state.burn_card('??')
117 | state.deal_board('QcJcTc')
118 | state.deal_board('QdJdTd')
119 |
120 | state.burn_card('??')
121 | state.deal_board('9c')
122 | state.deal_board('9d')
123 |
124 | state.burn_card('??')
125 | state.deal_board('8c')
126 | state.deal_board('8d')
127 |
128 | self.assertFalse(state.status)
129 | self.assertEqual(state.stacks, [400, 400, 0, 0])
130 | self.assertEqual(state.board_count, 2)
131 | self.assertEqual(
132 | list(state.get_board_cards(0)),
133 | list(Card.parse('QcJcTc9c8c')),
134 | )
135 | self.assertEqual(
136 | list(state.get_board_cards(1)),
137 | list(Card.parse('QdJdTd9d8d')),
138 | )
139 |
140 | state = PotLimitOmahaHoldem.create_state(
141 | (
142 | Automation.ANTE_POSTING,
143 | Automation.BET_COLLECTION,
144 | Automation.BLIND_OR_STRADDLE_POSTING,
145 | Automation.HOLE_CARDS_SHOWING_OR_MUCKING,
146 | Automation.HAND_KILLING,
147 | Automation.CHIPS_PUSHING,
148 | Automation.CHIPS_PULLING,
149 | ),
150 | True,
151 | 0,
152 | [1, 2],
153 | 2,
154 | 200,
155 | 4,
156 | starting_board_count=2,
157 | mode=Mode.CASH_GAME,
158 | )
159 |
160 | state.deal_hole('AcKc3c2c')
161 | state.deal_hole('AdKd3d2d')
162 | state.deal_hole('AhKh3h2h')
163 | state.deal_hole('AsKs3s2s')
164 | state.check_or_call()
165 | state.check_or_call()
166 | state.check_or_call()
167 | state.check_or_call()
168 |
169 | state.burn_card('??')
170 | state.deal_board('QcJcTc')
171 | state.deal_board('QdJhTs')
172 | state.complete_bet_or_raise_to(8)
173 | state.complete_bet_or_raise_to(32)
174 | state.complete_bet_or_raise_to(112)
175 | state.complete_bet_or_raise_to(198)
176 | state.check_or_call()
177 | state.check_or_call()
178 | state.check_or_call()
179 |
180 | state.select_runout_count(None)
181 | state.select_runout_count(None)
182 | state.select_runout_count(None)
183 | state.select_runout_count(2)
184 |
185 | state.burn_card('??')
186 | state.deal_board('7c')
187 | state.deal_board('7d')
188 |
189 | state.burn_card('??')
190 | state.deal_board('6c')
191 | state.deal_board('6d')
192 |
193 | state.burn_card('??')
194 | state.deal_board('5c')
195 | state.deal_board('7s')
196 |
197 | state.burn_card('??')
198 | state.deal_board('4c')
199 | state.deal_board('6s')
200 |
201 | self.assertFalse(state.status)
202 | self.assertEqual(state.stacks, [450, 50, 50, 250])
203 | self.assertEqual(state.board_count, 4)
204 | self.assertEqual(
205 | list(state.get_board_cards(0)),
206 | list(Card.parse('QcJcTc7c6c')),
207 | )
208 | self.assertEqual(
209 | list(state.get_board_cards(1)),
210 | list(Card.parse('QcJcTc7d6d')),
211 | )
212 | self.assertEqual(
213 | list(state.get_board_cards(2)),
214 | list(Card.parse('QdJhTs5c4c')),
215 | )
216 | self.assertEqual(
217 | list(state.get_board_cards(3)),
218 | list(Card.parse('QdJhTs7s6s')),
219 | )
220 |
221 |
222 | if __name__ == '__main__':
223 | main() # pragma: no cover
224 |
--------------------------------------------------------------------------------
/pokerkit/tests/test_lookups.py:
--------------------------------------------------------------------------------
1 | """:mod:`pokerkit.tests.test_lookups` implements unit tests for
2 | :mod:`pokerkit.test_lookups`.
3 | """
4 |
5 | from collections.abc import Iterable
6 | from hashlib import md5
7 | from itertools import combinations
8 | from unittest import main, TestCase
9 |
10 | from pokerkit.lookups import (
11 | BadugiLookup,
12 | EightOrBetterLookup,
13 | KuhnPokerLookup,
14 | RegularLookup,
15 | ShortDeckHoldemLookup,
16 | StandardBadugiLookup,
17 | StandardLookup,
18 | )
19 | from pokerkit.utilities import Card, Deck
20 |
21 |
22 | class LookupTestCaseMixin:
23 | @classmethod
24 | def serialize_combinations(
25 | cls,
26 | combinations_: Iterable[Iterable[Card]],
27 | ) -> str:
28 | return '\n'.join(map(cls.serialize_combination, combinations_))
29 |
30 | @classmethod
31 | def serialize_combination(cls, combination: Iterable[Card]) -> str:
32 | return ''.join(map(repr, combination))
33 |
34 |
35 | class StandardLookupTestCase(LookupTestCaseMixin, TestCase):
36 | def test_get_entry(self) -> None:
37 | lookup = StandardLookup()
38 | combinations_ = sorted(
39 | combinations(Deck.STANDARD, 5),
40 | key=lookup.get_entry,
41 | )
42 | string = self.serialize_combinations(combinations_)
43 | algorithm = md5()
44 | algorithm.update(string.encode())
45 |
46 | self.assertEqual(
47 | algorithm.hexdigest(),
48 | '488cdd27873395ba75205cd02fb9d6b2',
49 | )
50 |
51 |
52 | class ShortDeckHoldemLookupTestCase(LookupTestCaseMixin, TestCase):
53 | def test_get_entry(self) -> None:
54 | lookup = ShortDeckHoldemLookup()
55 | combinations_ = sorted(
56 | combinations(Deck.SHORT_DECK_HOLDEM, 5),
57 | key=lookup.get_entry,
58 | )
59 | string = self.serialize_combinations(combinations_)
60 | algorithm = md5()
61 | algorithm.update(string.encode())
62 |
63 | self.assertEqual(
64 | algorithm.hexdigest(),
65 | '5b46b727ce4526a68d41b0e55939353c',
66 | )
67 |
68 |
69 | class EightOrBetterLookupTestCase(LookupTestCaseMixin, TestCase):
70 | def test_get_entry(self) -> None:
71 | lookup = EightOrBetterLookup()
72 | combinations_ = sorted(
73 | filter(lookup.has_entry, combinations(Deck.REGULAR, 5)),
74 | key=lookup.get_entry,
75 | )
76 | string = self.serialize_combinations(combinations_)
77 | algorithm = md5()
78 | algorithm.update(string.encode())
79 |
80 | self.assertEqual(
81 | algorithm.hexdigest(),
82 | 'ecf2b6b16031562a6761932b1ce1de91',
83 | )
84 |
85 |
86 | class RegularLookupTestCase(LookupTestCaseMixin, TestCase):
87 | def test_get_entry(self) -> None:
88 | lookup = RegularLookup()
89 | combinations_ = sorted(
90 | combinations(Deck.REGULAR, 5),
91 | key=lookup.get_entry,
92 | )
93 | string = self.serialize_combinations(combinations_)
94 | algorithm = md5()
95 | algorithm.update(string.encode())
96 |
97 | self.assertEqual(
98 | algorithm.hexdigest(),
99 | '28925b97b06a1e674eaabf241a441e15',
100 | )
101 |
102 |
103 | class BadugiLookupTestCase(LookupTestCaseMixin, TestCase):
104 | def test_get_entry(self) -> None:
105 | lookup = BadugiLookup()
106 | combinations_ = []
107 |
108 | for n in range(1, 5):
109 | for cards in combinations(Deck.REGULAR, n):
110 | pairedness = Card.are_paired(cards)
111 | suitedness = Card.are_suited(cards)
112 | rainbowness = Card.are_rainbow(cards)
113 |
114 | if not pairedness and rainbowness:
115 | combinations_.append(cards)
116 | elif pairedness or suitedness:
117 | self.assertRaises(ValueError, lookup.get_entry, cards)
118 |
119 | combinations_.sort(key=lookup.get_entry)
120 | string = self.serialize_combinations(combinations_)
121 | algorithm = md5()
122 | algorithm.update(string.encode())
123 |
124 | self.assertEqual(
125 | algorithm.hexdigest(),
126 | '9d29ddbc3f76d815e166c6faa2af9021',
127 | )
128 |
129 |
130 | class StandardBadugiLookupTestCase(LookupTestCaseMixin, TestCase):
131 | def test_get_entry(self) -> None:
132 | lookup = StandardBadugiLookup()
133 | combinations_ = []
134 |
135 | for n in range(1, 5):
136 | for cards in combinations(Deck.STANDARD, n):
137 | pairedness = Card.are_paired(cards)
138 | suitedness = Card.are_suited(cards)
139 | rainbowness = Card.are_rainbow(cards)
140 |
141 | if not pairedness and rainbowness:
142 | combinations_.append(cards)
143 | elif pairedness or suitedness:
144 | self.assertRaises(ValueError, lookup.get_entry, cards)
145 |
146 | combinations_.sort(key=lookup.get_entry)
147 | string = self.serialize_combinations(combinations_)
148 | algorithm = md5()
149 | algorithm.update(string.encode())
150 |
151 | self.assertEqual(
152 | algorithm.hexdigest(),
153 | '06886dd57a4c7b780953f5090c63bcfe',
154 | )
155 |
156 |
157 | class KuhnPokerLookupTestCase(LookupTestCaseMixin, TestCase):
158 | def test_get_entry(self) -> None:
159 | lookup = KuhnPokerLookup()
160 | combinations_ = sorted(
161 | combinations(Deck.KUHN_POKER, 1),
162 | key=lookup.get_entry,
163 | )
164 | string = self.serialize_combinations(combinations_)
165 | algorithm = md5()
166 | algorithm.update(string.encode())
167 |
168 | self.assertEqual(
169 | algorithm.hexdigest(),
170 | 'bb7d5e9f8fe4f404fd1551d8995ca1a2',
171 | )
172 |
173 |
174 | if __name__ == '__main__':
175 | main() # pragma: no cover
176 |
--------------------------------------------------------------------------------
/pokerkit/tests/test_utilities.py:
--------------------------------------------------------------------------------
1 | """:mod:`pokerkit.tests.test_utilities` implements unit tests for
2 | :mod:`pokerkit.utilities`.
3 | """
4 |
5 | from unittest import main, TestCase
6 |
7 | from pokerkit.utilities import Card, Deck, Rank, RankOrder, Suit
8 |
9 |
10 | class RankTestCase(TestCase):
11 | def test_members(self) -> None:
12 | self.assertEqual(''.join(Rank), 'A23456789TJQK?')
13 |
14 |
15 | class RankOrderTestCase(TestCase):
16 | def test_members(self) -> None:
17 | self.assertEqual(''.join(RankOrder.EIGHT_OR_BETTER_LOW), 'A2345678')
18 | self.assertEqual(''.join(RankOrder.STANDARD), '23456789TJQKA')
19 | self.assertEqual(''.join(RankOrder.SHORT_DECK_HOLDEM), '6789TJQKA')
20 | self.assertEqual(''.join(RankOrder.REGULAR), 'A23456789TJQK')
21 |
22 |
23 | class SuitTestCase(TestCase):
24 | def test_members(self) -> None:
25 | self.assertEqual(''.join(Suit), 'cdhs?')
26 |
27 |
28 | class DeckTestCase(TestCase):
29 | def test_members(self) -> None:
30 | self.assertEqual(len(Deck.STANDARD), 52)
31 | self.assertCountEqual(
32 | Deck.STANDARD,
33 | Card.parse(
34 | '2c3c4c5c6c7c8c9cTcJcQcKcAc',
35 | '2d3d4d5d6d7d8d9dTdJdQdKdAd',
36 | '2h3h4h5h6h7h8h9hThJhQhKhAh',
37 | '2s3s4s5s6s7s8s9sTsJsQsKsAs',
38 | ),
39 | )
40 | self.assertEqual(len(Deck.SHORT_DECK_HOLDEM), 36)
41 | self.assertCountEqual(
42 | Deck.SHORT_DECK_HOLDEM,
43 | Card.parse(
44 | '6c7c8c9cTcJcQcKcAc',
45 | '6d7d8d9dTdJdQdKdAd',
46 | '6h7h8h9hThJhQhKhAh',
47 | '6s7s8s9sTsJsQsKsAs',
48 | ),
49 | )
50 | self.assertEqual(len(Deck.REGULAR), 52)
51 | self.assertCountEqual(
52 | Deck.STANDARD,
53 | Card.parse(
54 | 'Ac2c3c4c5c6c7c8c9cTcJcQcKc',
55 | 'Ad2d3d4d5d6d7d8d9dTdJdQdKd',
56 | 'Ah2h3h4h5h6h7h8h9hThJhQhKh',
57 | 'As2s3s4s5s6s7s8s9sTsJsQsKs',
58 | ),
59 | )
60 | self.assertEqual(len(Deck.KUHN_POKER), 3)
61 | self.assertCountEqual(Deck.KUHN_POKER, Card.parse('JsQsKs'))
62 |
63 |
64 | if __name__ == '__main__':
65 | main() # pragma: no cover
66 |
--------------------------------------------------------------------------------
/pokerkit/tests/test_wsop/__init__.py:
--------------------------------------------------------------------------------
1 | """:mod:`pokerkit.tests.test_wsop` is the package for the unit tests in
2 | the PokerKit library using hands from the World Series of Poker.
3 | """
4 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | build~=1.2.1
2 | coverage~=7.5.1
3 | flake8~=7.0.0
4 | interrogate~=1.7.0
5 | mypy~=1.10.0
6 | Sphinx~=7.3.7
7 | sphinx-rtd-theme~=2.0.0
8 | twine~=5.1.0
9 |
10 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | from setuptools import find_packages, setup
4 |
5 | setup(
6 | name='pokerkit',
7 | version='0.5.4',
8 | description='An open-source Python library for poker game simulations, hand evaluations, and statistical analysis',
9 | long_description=open('README.rst').read(),
10 | long_description_content_type='text/x-rst',
11 | url='https://github.com/uoftcprg/pokerkit',
12 | author='University of Toronto Computer Poker Student Research Group',
13 | author_email='uoftcprg@studentorg.utoronto.ca',
14 | license='MIT',
15 | classifiers=[
16 | 'Development Status :: 5 - Production/Stable',
17 | 'Intended Audience :: Developers',
18 | 'Intended Audience :: Education',
19 | 'Topic :: Education',
20 | 'Topic :: Games/Entertainment',
21 | 'Topic :: Games/Entertainment :: Board Games',
22 | 'Topic :: Scientific/Engineering',
23 | 'Topic :: Scientific/Engineering :: Artificial Intelligence',
24 | 'License :: OSI Approved :: MIT License',
25 | 'Operating System :: OS Independent',
26 | 'Programming Language :: Python',
27 | 'Programming Language :: Python :: 3',
28 | 'Programming Language :: Python :: 3 :: Only',
29 | 'Programming Language :: Python :: 3.11',
30 | 'Programming Language :: Python :: 3.12',
31 | 'Programming Language :: Python :: 3.13',
32 | ],
33 | keywords=[
34 | 'artificial-intelligence',
35 | 'deep-learning',
36 | 'game',
37 | 'game-development',
38 | 'game-theory',
39 | 'holdem-poker',
40 | 'imperfect-information-game',
41 | 'libratus',
42 | 'pluribus',
43 | 'poker',
44 | 'poker-engine',
45 | 'poker-evaluator',
46 | 'poker-game',
47 | 'poker-hands',
48 | 'poker-library',
49 | 'poker-strategies',
50 | 'python',
51 | 'reinforcement-learning',
52 | 'texas-holdem',
53 | ],
54 | project_urls={
55 | 'Documentation': 'https://pokerkit.readthedocs.io/en/latest/',
56 | 'Source': 'https://github.com/uoftcprg/pokerkit',
57 | 'Tracker': 'https://github.com/uoftcprg/pokerkit/issues',
58 | },
59 | packages=find_packages(),
60 | python_requires='>=3.11',
61 | package_data={'pokerkit': ['py.typed']},
62 | )
63 |
--------------------------------------------------------------------------------