├── .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 | --------------------------------------------------------------------------------