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.
@@ -0,0 +1,4 @@
1
+ * text=auto eol=lf
2
+ *.sh text eol=lf
3
+ *.png binary
4
+ *.exe binary
@@ -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
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .venv/
7
+ .pytest_cache/
8
+ native/build/
9
+ dist/
@@ -0,0 +1,3 @@
1
+ [submodule "vendor/OMPEval"]
2
+ path = vendor/OMPEval
3
+ url = https://github.com/vkresch/OMPEval.git
@@ -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.
@@ -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`.