pokerfast 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pokerfast-0.1.0/.gitattributes +4 -0
- pokerfast-0.1.0/.github/workflows/ci.yml +45 -0
- pokerfast-0.1.0/.github/workflows/publish.yml +61 -0
- pokerfast-0.1.0/.gitignore +9 -0
- pokerfast-0.1.0/.gitmodules +3 -0
- pokerfast-0.1.0/LICENSE +21 -0
- pokerfast-0.1.0/NOTICE +22 -0
- pokerfast-0.1.0/PKG-INFO +261 -0
- pokerfast-0.1.0/README.md +216 -0
- pokerfast-0.1.0/benchmarks/benchmark.py +207 -0
- pokerfast-0.1.0/native/CMakeLists.txt +44 -0
- pokerfast-0.1.0/native/build.sh +43 -0
- pokerfast-0.1.0/native/equity_batch.cpp +64 -0
- pokerfast-0.1.0/native/patches/0001-constexpr-urbg.patch +19 -0
- pokerfast-0.1.0/pyproject.toml +42 -0
- pokerfast-0.1.0/src/pokerfast/__init__.py +44 -0
- pokerfast-0.1.0/src/pokerfast/engine.py +747 -0
- pokerfast-0.1.0/src/pokerfast/equity.py +153 -0
- pokerfast-0.1.0/src/pokerfast/evaluator.py +173 -0
- pokerfast-0.1.0/src/pokerfast/patches/__init__.py +62 -0
- pokerfast-0.1.0/src/pokerfast/patches/deepcopy.py +155 -0
- pokerfast-0.1.0/src/pokerfast/patches/guard.py +147 -0
- pokerfast-0.1.0/src/pokerfast/patches/lookup_cache.py +347 -0
- pokerfast-0.1.0/src/pokerfast/rake.py +164 -0
- pokerfast-0.1.0/tests/_pokerkit_ref.py +158 -0
- pokerfast-0.1.0/tests/conftest.py +8 -0
- pokerfast-0.1.0/tests/test_engine.py +224 -0
- pokerfast-0.1.0/tests/test_evaluator.py +116 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
runs-on: ${{ matrix.os }}
|
|
15
|
+
strategy:
|
|
16
|
+
fail-fast: false
|
|
17
|
+
matrix:
|
|
18
|
+
os: [ubuntu-latest, windows-latest]
|
|
19
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
20
|
+
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
|
|
24
|
+
- uses: actions/setup-python@v5
|
|
25
|
+
with:
|
|
26
|
+
python-version: ${{ matrix.python-version }}
|
|
27
|
+
|
|
28
|
+
- name: Install
|
|
29
|
+
run: |
|
|
30
|
+
python -m pip install --upgrade pip
|
|
31
|
+
python -m pip install -e ".[test]"
|
|
32
|
+
|
|
33
|
+
- name: Test
|
|
34
|
+
# The differential tests against pokerkit are the correctness argument.
|
|
35
|
+
# `[test]` pins pokerkit, so a skip here means something is wrong with
|
|
36
|
+
# the install rather than with the environment -- treat it as a failure.
|
|
37
|
+
run: pytest -ra -q
|
|
38
|
+
|
|
39
|
+
- name: Fail if the differential tests were skipped
|
|
40
|
+
shell: bash
|
|
41
|
+
run: |
|
|
42
|
+
python - <<'PY'
|
|
43
|
+
import pokerkit, sys
|
|
44
|
+
print("pokerkit", pokerkit.__name__, "importable -- differential tests ran")
|
|
45
|
+
PY
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Trusted publishing (OIDC): no API token is stored anywhere. PyPI verifies
|
|
4
|
+
# that the request came from this workflow, in this repository, in the `pypi`
|
|
5
|
+
# environment, and mints a short-lived credential for that one upload.
|
|
6
|
+
#
|
|
7
|
+
# Fires on a published GitHub Release, so cutting a release IS the publish.
|
|
8
|
+
# workflow_dispatch is kept for a manual re-run if an upload fails halfway.
|
|
9
|
+
|
|
10
|
+
on:
|
|
11
|
+
release:
|
|
12
|
+
types: [published]
|
|
13
|
+
workflow_dispatch:
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
build:
|
|
20
|
+
name: Build sdist and wheel
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
# No submodules: the wheel is pure Python (src/pokerfast only). OMPEval
|
|
25
|
+
# is needed to BUILD the optional native equity helper, never to build
|
|
26
|
+
# the distribution.
|
|
27
|
+
|
|
28
|
+
- uses: actions/setup-python@v5
|
|
29
|
+
with:
|
|
30
|
+
python-version: "3.12"
|
|
31
|
+
|
|
32
|
+
- name: Build
|
|
33
|
+
run: |
|
|
34
|
+
python -m pip install --upgrade pip build twine
|
|
35
|
+
python -m build
|
|
36
|
+
python -m twine check dist/*
|
|
37
|
+
|
|
38
|
+
- uses: actions/upload-artifact@v4
|
|
39
|
+
with:
|
|
40
|
+
name: dist
|
|
41
|
+
path: dist/
|
|
42
|
+
|
|
43
|
+
publish:
|
|
44
|
+
name: Publish
|
|
45
|
+
needs: build
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
# This environment must exist in the repository settings, and its name must
|
|
48
|
+
# match the one registered with the trusted publisher on PyPI, or the
|
|
49
|
+
# upload is rejected.
|
|
50
|
+
environment:
|
|
51
|
+
name: pypi
|
|
52
|
+
url: https://pypi.org/p/pokerfast
|
|
53
|
+
permissions:
|
|
54
|
+
id-token: write # the OIDC token. Without this, publishing fails.
|
|
55
|
+
steps:
|
|
56
|
+
- uses: actions/download-artifact@v4
|
|
57
|
+
with:
|
|
58
|
+
name: dist
|
|
59
|
+
path: dist/
|
|
60
|
+
|
|
61
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
pokerfast-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tie.yob
|
|
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.
|
pokerfast-0.1.0/NOTICE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
pokerfast bundles no third-party source.
|
|
2
|
+
|
|
3
|
+
The optional exact-equity helper (`pokerfast.equity`, `native/`) builds against
|
|
4
|
+
OMPEval, which is fetched as a git submodule into vendor/OMPEval and is NOT
|
|
5
|
+
redistributed here.
|
|
6
|
+
|
|
7
|
+
OMPEval -- https://github.com/vkresch/OMPEval
|
|
8
|
+
(a fork of https://github.com/zekyll/OMPEval)
|
|
9
|
+
pinned at commit 725a819
|
|
10
|
+
ISC License, Copyright (c) 2016, Timo A.
|
|
11
|
+
|
|
12
|
+
OMPEval vendors libdivide, which carries its own licence; see
|
|
13
|
+
vendor/OMPEval/LICENSE-libdivide.txt after initialising the submodule.
|
|
14
|
+
|
|
15
|
+
If you distribute a BINARY built by native/build.sh, it links OMPEval and you
|
|
16
|
+
must carry OMPEval's copyright and permission notice with it. The ISC licence
|
|
17
|
+
text travels with the submodule at vendor/OMPEval/LICENSE.txt.
|
|
18
|
+
|
|
19
|
+
`native/patches/0001-constexpr-urbg.patch` is a two-line change to OMPEval's
|
|
20
|
+
omp/Random.h making min()/max() constexpr, which newer C++ standard libraries
|
|
21
|
+
require of a UniformRandomBitGenerator. It is applied to the submodule working
|
|
22
|
+
tree at build time and is not committed upstream here.
|
pokerfast-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pokerfast
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Fast hold'em hand evaluation, a lean 6-max NLHE engine, and runtime accelerators for pokerkit
|
|
5
|
+
Project-URL: Source, https://github.com/wesboyt/pokerfast
|
|
6
|
+
Project-URL: Issues, https://github.com/wesboyt/pokerfast/issues
|
|
7
|
+
License: MIT License
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2026 tie.yob
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
License-File: NOTICE
|
|
30
|
+
Keywords: equity,hand-evaluator,holdem,poker,pokerkit
|
|
31
|
+
Classifier: Development Status :: 4 - Beta
|
|
32
|
+
Classifier: Intended Audience :: Developers
|
|
33
|
+
Classifier: Intended Audience :: Science/Research
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Programming Language :: Python :: 3
|
|
36
|
+
Classifier: Topic :: Games/Entertainment
|
|
37
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
38
|
+
Requires-Python: >=3.9
|
|
39
|
+
Provides-Extra: pokerkit
|
|
40
|
+
Requires-Dist: pokerkit==0.7.3; extra == 'pokerkit'
|
|
41
|
+
Provides-Extra: test
|
|
42
|
+
Requires-Dist: pokerkit==0.7.3; extra == 'test'
|
|
43
|
+
Requires-Dist: pytest>=7; extra == 'test'
|
|
44
|
+
Description-Content-Type: text/markdown
|
|
45
|
+
|
|
46
|
+
# pokerfast
|
|
47
|
+
|
|
48
|
+
Fast Texas hold'em primitives: a table-driven 7-card evaluator, a lean 6-max
|
|
49
|
+
no-limit engine, and runtime accelerators for [pokerkit](https://github.com/uoftcprg/pokerkit).
|
|
50
|
+
|
|
51
|
+
Three independent pieces. Take whichever you need — none of them requires the
|
|
52
|
+
others.
|
|
53
|
+
|
|
54
|
+
| you want | use | against pokerkit |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| compare 7-card hands | `eval7` | **74x** (1 core) → **331x** (4 cores) |
|
|
57
|
+
| play out millions of hands | `FastHand` | **45x** (1 core) → **160x** (4 cores) |
|
|
58
|
+
| keep pokerkit, lose the cost | `pokerfast.patches` | ~2x on the evaluation path |
|
|
59
|
+
| exact multi-way equity | `pokerfast.equity` | C++ (OMPEval), batched |
|
|
60
|
+
|
|
61
|
+
The speedup **grows with core count**, because the two engines scale in
|
|
62
|
+
opposite directions on a free-threaded build. See [Benchmarks](#benchmarks).
|
|
63
|
+
|
|
64
|
+
## Install
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install pokerfast # core: eval7 + FastHand
|
|
68
|
+
pip install "pokerfast[pokerkit]" # + the pokerkit patches
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 1. `eval7` — 7-card evaluation
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from pokerfast import eval7
|
|
75
|
+
|
|
76
|
+
eval7(('As', 'Ks', 'Qs', 'Js', 'Ts', '2c', '3d')) # royal flush
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
A rank-histogram evaluator with two precomputed 8192-entry tables. One pass
|
|
80
|
+
over seven cards, a flush test, a straight lookup, a branch on the count
|
|
81
|
+
pattern — no enumeration of the 21 five-card combinations.
|
|
82
|
+
|
|
83
|
+
**Only the ORDER is defined.** The integer is not a hand rank anyone else's
|
|
84
|
+
code will recognise: compare two of them, don't interpret one. The test suite
|
|
85
|
+
checks the induced order against pokerkit over thousands of random hands, with
|
|
86
|
+
ties counted separately so an evaluator that called everything equal could not
|
|
87
|
+
pass, plus an explicit sweep of every category (random deals almost never
|
|
88
|
+
produce a steel wheel).
|
|
89
|
+
|
|
90
|
+
## 2. `FastHand` — a 6-max NLHE engine
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from pokerfast import FastHand
|
|
94
|
+
|
|
95
|
+
h = FastHand() # deal 6 players, 100/200 blinds
|
|
96
|
+
while not h.done:
|
|
97
|
+
space = h.get_action_space() # {'call': 200, 'fold': 0, 'min_bet': 400, ...}
|
|
98
|
+
h.call()
|
|
99
|
+
h.state.payoffs
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
pokerkit is a general, validating framework for dozens of variants, and that
|
|
103
|
+
generality costs per action. This implements exactly one game — 6-max NLHE,
|
|
104
|
+
cash mode, fixed blinds, one runout — where the state is a few integer lists,
|
|
105
|
+
an action is a few additions, and a snapshot is a list copy.
|
|
106
|
+
|
|
107
|
+
**Why you can trust it:** `tests/test_engine.py` is a differential test. It
|
|
108
|
+
drives this engine and pokerkit through the same random action sequence and
|
|
109
|
+
compares the action space, the exact chip amounts, turn order, pot, board
|
|
110
|
+
length and the per-street action log at *every step*, over hundreds of hands,
|
|
111
|
+
with raises over-sampled at the extremes because the min-raise increment and
|
|
112
|
+
all-in-for-less rules are where engines go wrong.
|
|
113
|
+
|
|
114
|
+
### Observers
|
|
115
|
+
|
|
116
|
+
Watch the action stream without re-deriving it:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
class Log:
|
|
120
|
+
def __init__(self): self.events = []
|
|
121
|
+
def action(self, seat, amount): self.events.append((seat, amount))
|
|
122
|
+
def board(self, street, cards): self.events.append((street, cards))
|
|
123
|
+
def clone(self): ... # called on deepcopy
|
|
124
|
+
|
|
125
|
+
h = FastHand(observer=Log())
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`clone()` matters: a branch must not write history back into the position it
|
|
129
|
+
branched from. Amounts are `-1` for a fold, `0` for a check, chips otherwise.
|
|
130
|
+
|
|
131
|
+
## 3. `pokerfast.patches` — make pokerkit itself faster
|
|
132
|
+
|
|
133
|
+
When you need pokerkit's exact semantics but not its cost:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from pokerfast import patches
|
|
137
|
+
patches.install_all()
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Nothing in `site-packages` is modified — these are in-process monkey-patches:
|
|
141
|
+
|
|
142
|
+
- an identity `__deepcopy__` on pokerkit's frozen dataclasses (copying an
|
|
143
|
+
immutable object is pure waste), applied only to classes verified to declare
|
|
144
|
+
no mutable field;
|
|
145
|
+
- memoisation of `Lookup._get_key` and `<HandType>.from_game`;
|
|
146
|
+
- a tuple fast path for `Card.clean`.
|
|
147
|
+
|
|
148
|
+
**Guarded.** The dangerous failure isn't "the attribute is gone" — that raises,
|
|
149
|
+
and is safe. It's "the attribute is still there and means something else",
|
|
150
|
+
which is silent. So the version is pinned, the signatures of everything patched
|
|
151
|
+
are fingerprinted, and a mismatch disables the patches loudly rather than
|
|
152
|
+
applying one that was never verified. `POKERFAST_STRICT=0` overrides, once
|
|
153
|
+
you've checked it yourself.
|
|
154
|
+
|
|
155
|
+
## 4. `pokerfast.equity` — exact equity (optional)
|
|
156
|
+
|
|
157
|
+
Needs a C++ toolchain and CMake. OMPEval is a **git submodule**, not vendored
|
|
158
|
+
source:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
git submodule update --init --recursive
|
|
162
|
+
bash native/build.sh
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from pokerfast.equity import EquityCalculator
|
|
167
|
+
|
|
168
|
+
with EquityCalculator() as eq:
|
|
169
|
+
eq.equity('AhKd', '2c3d4h5s6c')
|
|
170
|
+
eq.equity_many([('AhKd', '2c3d4h5s6c'), ('7c7d', '')])
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The helper keeps **one** process alive and serves queries over stdin/stdout. A
|
|
174
|
+
single-query process spends ~19 ms of its ~26 ms building an 86,547-entry
|
|
175
|
+
lookup table it then throws away, so batching is most of the win. It also
|
|
176
|
+
enumerates exhaustively rather than sampling (with a full board the opponent
|
|
177
|
+
has only C(45,2) = 990 hands, so exact is both cheaper *and* exact) and runs
|
|
178
|
+
single-threaded, because a thread pool costs more than a 990-combination
|
|
179
|
+
problem.
|
|
180
|
+
|
|
181
|
+
See `NOTICE` for OMPEval's ISC licence and what it requires if you ship a
|
|
182
|
+
binary.
|
|
183
|
+
|
|
184
|
+
## Environment variables
|
|
185
|
+
|
|
186
|
+
| variable | default | effect |
|
|
187
|
+
|---|---|---|
|
|
188
|
+
| `POKERFAST_TABLE_EVAL` | `1` | `0` routes showdowns through pokerkit instead |
|
|
189
|
+
| `POKERFAST_STRICT` | `1` | `0` patches pokerkit despite a fingerprint mismatch |
|
|
190
|
+
| `POKERFAST_EVAL_CACHE` | `65536` | `Lookup._get_key` cache size |
|
|
191
|
+
| `POKERFAST_FROMGAME_CACHE` | `32768` | `from_game` cache size |
|
|
192
|
+
| `POKERFAST_OMPEVAL` | — | path to a prebuilt `ompeval_batch` |
|
|
193
|
+
|
|
194
|
+
## Tests
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
pip install "pokerfast[test]"
|
|
198
|
+
pytest
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
The differential tests need pokerkit and are the reason to trust any of this;
|
|
202
|
+
they skip without it, which makes the suite much weaker. Don't read a green run
|
|
203
|
+
that skipped them as a pass.
|
|
204
|
+
|
|
205
|
+
## Benchmarks
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
PYTHON_GIL=0 python benchmarks/benchmark.py --threads 1,2,4 --rounds 5
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Paired and interleaved: every round runs both engines back to back, alternating
|
|
212
|
+
which goes first, and reports the **ratio** — which survives a busy machine,
|
|
213
|
+
because contention hits both arms. Each arm gets the same wall budget and
|
|
214
|
+
reports how much it finished, so neither is penalised by a miscalibrated work
|
|
215
|
+
count. The per-round ratios are printed; if they disagree with each other, the
|
|
216
|
+
machine was too noisy and the median means nothing.
|
|
217
|
+
|
|
218
|
+
CPython 3.14.7 free-threaded, pokerkit 0.7.3, 5 rounds, 3 s per arm:
|
|
219
|
+
|
|
220
|
+
| threads | full hands played to completion | 7-card evaluation |
|
|
221
|
+
|---|---|---|
|
|
222
|
+
| 1 | **45x** (5,151/s vs 116/s) | **74x** (269k/s vs 3,476/s) |
|
|
223
|
+
| 2 | **76x** (10,029/s vs 135/s) | **122x** (480k/s vs 3,700/s) |
|
|
224
|
+
| 4 | **160x** (11,110/s vs 67/s) | **331x** (558k/s vs 1,610/s) |
|
|
225
|
+
|
|
226
|
+
### Why the speedup grows with cores
|
|
227
|
+
|
|
228
|
+
Relative to each engine's *own* single-thread rate:
|
|
229
|
+
|
|
230
|
+
| threads | pokerfast | pokerkit |
|
|
231
|
+
|---|---|---|
|
|
232
|
+
| 1 | 1.00x | 1.00x |
|
|
233
|
+
| 2 | 1.95x | 1.17x |
|
|
234
|
+
| 4 | 2.16x | **0.58x** |
|
|
235
|
+
|
|
236
|
+
pokerfast scales sublinearly, as you would expect. pokerkit goes *backwards* —
|
|
237
|
+
at four threads it does less total work than at one. That reproduced across
|
|
238
|
+
three separate runs (0.49x, 0.70x, 0.58x), so it is an effect rather than
|
|
239
|
+
noise, but **the cause is untested**: the likely candidate is reference-count
|
|
240
|
+
contention on shared immutable objects, which a free-threaded build turns into
|
|
241
|
+
cache-line ping-pong between cores. Treat that as a hypothesis, not a finding.
|
|
242
|
+
|
|
243
|
+
The compounding of those two curves is the whole story: 45x becomes 160x not
|
|
244
|
+
because pokerfast got faster, but because pokerkit got slower.
|
|
245
|
+
|
|
246
|
+
### What these numbers are not
|
|
247
|
+
|
|
248
|
+
Measured on one machine — an 8-thread laptop — **with an unrelated GPU training
|
|
249
|
+
job occupying ~1.5 cores throughout**. The paired ratios are protected against
|
|
250
|
+
that; the scaling column compares an engine against itself and is only
|
|
251
|
+
partially protected, and 8 threads was not measured at all because the machine
|
|
252
|
+
was not free. Expect different absolutes elsewhere.
|
|
253
|
+
|
|
254
|
+
They also describe a *bulk* workload that plays hands to completion. If you
|
|
255
|
+
deal one hand and inspect it, pokerkit's constant factor is irrelevant to you
|
|
256
|
+
and you should use pokerkit — it is a far more general library, and pokerfast
|
|
257
|
+
is only faster because it does much less.
|
|
258
|
+
|
|
259
|
+
## Licence
|
|
260
|
+
|
|
261
|
+
MIT — see `LICENSE`. OMPEval is ISC and is not redistributed here; see `NOTICE`.
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# pokerfast
|
|
2
|
+
|
|
3
|
+
Fast Texas hold'em primitives: a table-driven 7-card evaluator, a lean 6-max
|
|
4
|
+
no-limit engine, and runtime accelerators for [pokerkit](https://github.com/uoftcprg/pokerkit).
|
|
5
|
+
|
|
6
|
+
Three independent pieces. Take whichever you need — none of them requires the
|
|
7
|
+
others.
|
|
8
|
+
|
|
9
|
+
| you want | use | against pokerkit |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| compare 7-card hands | `eval7` | **74x** (1 core) → **331x** (4 cores) |
|
|
12
|
+
| play out millions of hands | `FastHand` | **45x** (1 core) → **160x** (4 cores) |
|
|
13
|
+
| keep pokerkit, lose the cost | `pokerfast.patches` | ~2x on the evaluation path |
|
|
14
|
+
| exact multi-way equity | `pokerfast.equity` | C++ (OMPEval), batched |
|
|
15
|
+
|
|
16
|
+
The speedup **grows with core count**, because the two engines scale in
|
|
17
|
+
opposite directions on a free-threaded build. See [Benchmarks](#benchmarks).
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install pokerfast # core: eval7 + FastHand
|
|
23
|
+
pip install "pokerfast[pokerkit]" # + the pokerkit patches
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 1. `eval7` — 7-card evaluation
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from pokerfast import eval7
|
|
30
|
+
|
|
31
|
+
eval7(('As', 'Ks', 'Qs', 'Js', 'Ts', '2c', '3d')) # royal flush
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
A rank-histogram evaluator with two precomputed 8192-entry tables. One pass
|
|
35
|
+
over seven cards, a flush test, a straight lookup, a branch on the count
|
|
36
|
+
pattern — no enumeration of the 21 five-card combinations.
|
|
37
|
+
|
|
38
|
+
**Only the ORDER is defined.** The integer is not a hand rank anyone else's
|
|
39
|
+
code will recognise: compare two of them, don't interpret one. The test suite
|
|
40
|
+
checks the induced order against pokerkit over thousands of random hands, with
|
|
41
|
+
ties counted separately so an evaluator that called everything equal could not
|
|
42
|
+
pass, plus an explicit sweep of every category (random deals almost never
|
|
43
|
+
produce a steel wheel).
|
|
44
|
+
|
|
45
|
+
## 2. `FastHand` — a 6-max NLHE engine
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from pokerfast import FastHand
|
|
49
|
+
|
|
50
|
+
h = FastHand() # deal 6 players, 100/200 blinds
|
|
51
|
+
while not h.done:
|
|
52
|
+
space = h.get_action_space() # {'call': 200, 'fold': 0, 'min_bet': 400, ...}
|
|
53
|
+
h.call()
|
|
54
|
+
h.state.payoffs
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
pokerkit is a general, validating framework for dozens of variants, and that
|
|
58
|
+
generality costs per action. This implements exactly one game — 6-max NLHE,
|
|
59
|
+
cash mode, fixed blinds, one runout — where the state is a few integer lists,
|
|
60
|
+
an action is a few additions, and a snapshot is a list copy.
|
|
61
|
+
|
|
62
|
+
**Why you can trust it:** `tests/test_engine.py` is a differential test. It
|
|
63
|
+
drives this engine and pokerkit through the same random action sequence and
|
|
64
|
+
compares the action space, the exact chip amounts, turn order, pot, board
|
|
65
|
+
length and the per-street action log at *every step*, over hundreds of hands,
|
|
66
|
+
with raises over-sampled at the extremes because the min-raise increment and
|
|
67
|
+
all-in-for-less rules are where engines go wrong.
|
|
68
|
+
|
|
69
|
+
### Observers
|
|
70
|
+
|
|
71
|
+
Watch the action stream without re-deriving it:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
class Log:
|
|
75
|
+
def __init__(self): self.events = []
|
|
76
|
+
def action(self, seat, amount): self.events.append((seat, amount))
|
|
77
|
+
def board(self, street, cards): self.events.append((street, cards))
|
|
78
|
+
def clone(self): ... # called on deepcopy
|
|
79
|
+
|
|
80
|
+
h = FastHand(observer=Log())
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`clone()` matters: a branch must not write history back into the position it
|
|
84
|
+
branched from. Amounts are `-1` for a fold, `0` for a check, chips otherwise.
|
|
85
|
+
|
|
86
|
+
## 3. `pokerfast.patches` — make pokerkit itself faster
|
|
87
|
+
|
|
88
|
+
When you need pokerkit's exact semantics but not its cost:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from pokerfast import patches
|
|
92
|
+
patches.install_all()
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Nothing in `site-packages` is modified — these are in-process monkey-patches:
|
|
96
|
+
|
|
97
|
+
- an identity `__deepcopy__` on pokerkit's frozen dataclasses (copying an
|
|
98
|
+
immutable object is pure waste), applied only to classes verified to declare
|
|
99
|
+
no mutable field;
|
|
100
|
+
- memoisation of `Lookup._get_key` and `<HandType>.from_game`;
|
|
101
|
+
- a tuple fast path for `Card.clean`.
|
|
102
|
+
|
|
103
|
+
**Guarded.** The dangerous failure isn't "the attribute is gone" — that raises,
|
|
104
|
+
and is safe. It's "the attribute is still there and means something else",
|
|
105
|
+
which is silent. So the version is pinned, the signatures of everything patched
|
|
106
|
+
are fingerprinted, and a mismatch disables the patches loudly rather than
|
|
107
|
+
applying one that was never verified. `POKERFAST_STRICT=0` overrides, once
|
|
108
|
+
you've checked it yourself.
|
|
109
|
+
|
|
110
|
+
## 4. `pokerfast.equity` — exact equity (optional)
|
|
111
|
+
|
|
112
|
+
Needs a C++ toolchain and CMake. OMPEval is a **git submodule**, not vendored
|
|
113
|
+
source:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
git submodule update --init --recursive
|
|
117
|
+
bash native/build.sh
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from pokerfast.equity import EquityCalculator
|
|
122
|
+
|
|
123
|
+
with EquityCalculator() as eq:
|
|
124
|
+
eq.equity('AhKd', '2c3d4h5s6c')
|
|
125
|
+
eq.equity_many([('AhKd', '2c3d4h5s6c'), ('7c7d', '')])
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The helper keeps **one** process alive and serves queries over stdin/stdout. A
|
|
129
|
+
single-query process spends ~19 ms of its ~26 ms building an 86,547-entry
|
|
130
|
+
lookup table it then throws away, so batching is most of the win. It also
|
|
131
|
+
enumerates exhaustively rather than sampling (with a full board the opponent
|
|
132
|
+
has only C(45,2) = 990 hands, so exact is both cheaper *and* exact) and runs
|
|
133
|
+
single-threaded, because a thread pool costs more than a 990-combination
|
|
134
|
+
problem.
|
|
135
|
+
|
|
136
|
+
See `NOTICE` for OMPEval's ISC licence and what it requires if you ship a
|
|
137
|
+
binary.
|
|
138
|
+
|
|
139
|
+
## Environment variables
|
|
140
|
+
|
|
141
|
+
| variable | default | effect |
|
|
142
|
+
|---|---|---|
|
|
143
|
+
| `POKERFAST_TABLE_EVAL` | `1` | `0` routes showdowns through pokerkit instead |
|
|
144
|
+
| `POKERFAST_STRICT` | `1` | `0` patches pokerkit despite a fingerprint mismatch |
|
|
145
|
+
| `POKERFAST_EVAL_CACHE` | `65536` | `Lookup._get_key` cache size |
|
|
146
|
+
| `POKERFAST_FROMGAME_CACHE` | `32768` | `from_game` cache size |
|
|
147
|
+
| `POKERFAST_OMPEVAL` | — | path to a prebuilt `ompeval_batch` |
|
|
148
|
+
|
|
149
|
+
## Tests
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
pip install "pokerfast[test]"
|
|
153
|
+
pytest
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The differential tests need pokerkit and are the reason to trust any of this;
|
|
157
|
+
they skip without it, which makes the suite much weaker. Don't read a green run
|
|
158
|
+
that skipped them as a pass.
|
|
159
|
+
|
|
160
|
+
## Benchmarks
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
PYTHON_GIL=0 python benchmarks/benchmark.py --threads 1,2,4 --rounds 5
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Paired and interleaved: every round runs both engines back to back, alternating
|
|
167
|
+
which goes first, and reports the **ratio** — which survives a busy machine,
|
|
168
|
+
because contention hits both arms. Each arm gets the same wall budget and
|
|
169
|
+
reports how much it finished, so neither is penalised by a miscalibrated work
|
|
170
|
+
count. The per-round ratios are printed; if they disagree with each other, the
|
|
171
|
+
machine was too noisy and the median means nothing.
|
|
172
|
+
|
|
173
|
+
CPython 3.14.7 free-threaded, pokerkit 0.7.3, 5 rounds, 3 s per arm:
|
|
174
|
+
|
|
175
|
+
| threads | full hands played to completion | 7-card evaluation |
|
|
176
|
+
|---|---|---|
|
|
177
|
+
| 1 | **45x** (5,151/s vs 116/s) | **74x** (269k/s vs 3,476/s) |
|
|
178
|
+
| 2 | **76x** (10,029/s vs 135/s) | **122x** (480k/s vs 3,700/s) |
|
|
179
|
+
| 4 | **160x** (11,110/s vs 67/s) | **331x** (558k/s vs 1,610/s) |
|
|
180
|
+
|
|
181
|
+
### Why the speedup grows with cores
|
|
182
|
+
|
|
183
|
+
Relative to each engine's *own* single-thread rate:
|
|
184
|
+
|
|
185
|
+
| threads | pokerfast | pokerkit |
|
|
186
|
+
|---|---|---|
|
|
187
|
+
| 1 | 1.00x | 1.00x |
|
|
188
|
+
| 2 | 1.95x | 1.17x |
|
|
189
|
+
| 4 | 2.16x | **0.58x** |
|
|
190
|
+
|
|
191
|
+
pokerfast scales sublinearly, as you would expect. pokerkit goes *backwards* —
|
|
192
|
+
at four threads it does less total work than at one. That reproduced across
|
|
193
|
+
three separate runs (0.49x, 0.70x, 0.58x), so it is an effect rather than
|
|
194
|
+
noise, but **the cause is untested**: the likely candidate is reference-count
|
|
195
|
+
contention on shared immutable objects, which a free-threaded build turns into
|
|
196
|
+
cache-line ping-pong between cores. Treat that as a hypothesis, not a finding.
|
|
197
|
+
|
|
198
|
+
The compounding of those two curves is the whole story: 45x becomes 160x not
|
|
199
|
+
because pokerfast got faster, but because pokerkit got slower.
|
|
200
|
+
|
|
201
|
+
### What these numbers are not
|
|
202
|
+
|
|
203
|
+
Measured on one machine — an 8-thread laptop — **with an unrelated GPU training
|
|
204
|
+
job occupying ~1.5 cores throughout**. The paired ratios are protected against
|
|
205
|
+
that; the scaling column compares an engine against itself and is only
|
|
206
|
+
partially protected, and 8 threads was not measured at all because the machine
|
|
207
|
+
was not free. Expect different absolutes elsewhere.
|
|
208
|
+
|
|
209
|
+
They also describe a *bulk* workload that plays hands to completion. If you
|
|
210
|
+
deal one hand and inspect it, pokerkit's constant factor is irrelevant to you
|
|
211
|
+
and you should use pokerkit — it is a far more general library, and pokerfast
|
|
212
|
+
is only faster because it does much less.
|
|
213
|
+
|
|
214
|
+
## Licence
|
|
215
|
+
|
|
216
|
+
MIT — see `LICENSE`. OMPEval is ISC and is not redistributed here; see `NOTICE`.
|