pokerkit-plus 0.0.1__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.
- pokerkit_plus-0.0.1/.gitignore +21 -0
- pokerkit_plus-0.0.1/LICENSE +21 -0
- pokerkit_plus-0.0.1/PKG-INFO +102 -0
- pokerkit_plus-0.0.1/README.md +83 -0
- pokerkit_plus-0.0.1/pokerkit_plus/__init__.py +131 -0
- pokerkit_plus-0.0.1/pokerkit_plus/_semantic.py +249 -0
- pokerkit_plus-0.0.1/pokerkit_plus/blockers.py +146 -0
- pokerkit_plus-0.0.1/pokerkit_plus/combos.py +333 -0
- pokerkit_plus-0.0.1/pokerkit_plus/compat.py +52 -0
- pokerkit_plus-0.0.1/pokerkit_plus/draws.py +651 -0
- pokerkit_plus-0.0.1/pokerkit_plus/facade.py +202 -0
- pokerkit_plus-0.0.1/pokerkit_plus/outs.py +222 -0
- pokerkit_plus-0.0.1/pokerkit_plus/py.typed +0 -0
- pokerkit_plus-0.0.1/pokerkit_plus/ranges.py +368 -0
- pokerkit_plus-0.0.1/pokerkit_plus/tags.py +664 -0
- pokerkit_plus-0.0.1/pokerkit_plus/texture.py +656 -0
- pokerkit_plus-0.0.1/pyproject.toml +39 -0
- pokerkit_plus-0.0.1/tests/test_blockers.py +72 -0
- pokerkit_plus-0.0.1/tests/test_combos.py +137 -0
- pokerkit_plus-0.0.1/tests/test_compat.py +23 -0
- pokerkit_plus-0.0.1/tests/test_draws.py +55 -0
- pokerkit_plus-0.0.1/tests/test_facade.py +28 -0
- pokerkit_plus-0.0.1/tests/test_outs.py +75 -0
- pokerkit_plus-0.0.1/tests/test_ranges.py +94 -0
- pokerkit_plus-0.0.1/tests/test_reexport.py +33 -0
- pokerkit_plus-0.0.1/tests/test_tags.py +83 -0
- pokerkit_plus-0.0.1/tests/test_texture.py +95 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
|
|
11
|
+
# Tooling caches
|
|
12
|
+
.pytest_cache/
|
|
13
|
+
.mypy_cache/
|
|
14
|
+
.ruff_cache/
|
|
15
|
+
|
|
16
|
+
# OS
|
|
17
|
+
.DS_Store
|
|
18
|
+
.gstack/
|
|
19
|
+
|
|
20
|
+
# Internal planning docs (reference private projects; kept local, not published)
|
|
21
|
+
docs/SEMANTIC_LAYER_PLAN.md
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 billcheung10
|
|
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.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pokerkit-plus
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: PokerKit Plus — fork-free capability extensions on top of PokerKit; a drop-in superset of pokerkit.
|
|
5
|
+
Project-URL: Homepage, https://github.com/billcheung10/pokerkit-plus
|
|
6
|
+
Project-URL: Repository, https://github.com/billcheung10/pokerkit-plus
|
|
7
|
+
Project-URL: Issues, https://github.com/billcheung10/pokerkit-plus/issues
|
|
8
|
+
Author: PokerKit Plus contributors
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: equity,holdem,omaha,poker,pokerkit,solver
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Requires-Dist: pokerkit==0.7.3
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
16
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
17
|
+
Requires-Dist: ruff>=0.7; extra == 'dev'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# PokerKit Plus
|
|
21
|
+
|
|
22
|
+
Fork-free capability extensions on top of [PokerKit](https://github.com/uoftcprg/pokerkit).
|
|
23
|
+
|
|
24
|
+
`pokerkit_plus` is a **drop-in superset** of `pokerkit`: it re-exports the entire
|
|
25
|
+
upstream surface unchanged and adds new capabilities on top via subclasses, free
|
|
26
|
+
functions, and thin wrappers. The upstream package is never modified, so PokerKit
|
|
27
|
+
Plus keeps riding upstream's maintenance (correctness fixes, new parsers, etc.).
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
# Anywhere you used pokerkit, you can use pokerkit_plus instead:
|
|
31
|
+
from pokerkit_plus import NoLimitTexasHoldem, Automation, State # all of pokerkit
|
|
32
|
+
# ...plus the PokerKit Plus additions as they land.
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Design
|
|
36
|
+
|
|
37
|
+
- **Form:** standalone extension package, `pip`-depends on `pokerkit==0.7.3` (pinned).
|
|
38
|
+
- **No fork, no monkeypatch.** PokerKit is built for fork-free extension
|
|
39
|
+
(`_begin_/_update_/_end_` phase hooks, mixin-composed variants, injectable
|
|
40
|
+
`rake`/`divmod`, `ClassVar` registries). Most additions need zero core edits.
|
|
41
|
+
- **`compat.py`** pins the pokerkit version and is the single home for stable
|
|
42
|
+
aliases of version-fragile upstream names.
|
|
43
|
+
|
|
44
|
+
## Status
|
|
45
|
+
|
|
46
|
+
Semantic layer P0/P1 implemented (drop-in superset of pokerkit + first
|
|
47
|
+
capabilities):
|
|
48
|
+
|
|
49
|
+
- `pokerkit_plus.texture` — `BoardTexture.from_board` plus `Wetness`,
|
|
50
|
+
`Connectivity`, `RankBand`, `StraightDraw`, `FlushDraw`, and
|
|
51
|
+
`are_two_tone`/`are_monotone`/`are_rainbow`.
|
|
52
|
+
- `pokerkit_plus.combos` — `Nuts.from_board`, `CategoryCombos.from_board`,
|
|
53
|
+
`HoleCombo`, `made_label`, `meets`, `CATEGORY_ORDER` (made-hand category is
|
|
54
|
+
pokerkit's own `Label`).
|
|
55
|
+
- `pokerkit_plus.draws` — `Draws.from_hand` (hero flush/straight draw + nut
|
|
56
|
+
rank) and the shared `NutRank`.
|
|
57
|
+
- `pokerkit_plus.tags` — `HandTier.from_hand` (made-hand tier + `labels()`)
|
|
58
|
+
with `PairTier`/`KickerTier`/`TwoPairTier`/`ThreeOfAKindTier`.
|
|
59
|
+
- `pokerkit_plus.outs` — `Outs.from_hand` (category-upgrade outs, grouped).
|
|
60
|
+
- `pokerkit_plus.blockers` — `BlockerReport.from_hand` (count-based: how many
|
|
61
|
+
of the board's nut combos a holding removes).
|
|
62
|
+
- `pokerkit_plus.ranges` — `ComboClass`/`expand_range` (notation),
|
|
63
|
+
`build_value_range`, `calculate_range_advantage` (Monte-Carlo equity), and
|
|
64
|
+
`nut_advantage` (exact nut-category share).
|
|
65
|
+
- `pokerkit_plus.facade` — `HandReport.from_hand` and `BoardReport.from_board`,
|
|
66
|
+
the one-call composed entry points for callers wanting a complete reading.
|
|
67
|
+
|
|
68
|
+
Built on pokerkit's lookup-table total order (no hand-rolled scoring); every
|
|
69
|
+
per-board / per-hand enumeration is memoized, and equity is delegated to
|
|
70
|
+
pokerkit's Monte-Carlo sampler. Verified: mypy `--strict` clean, ruff clean,
|
|
71
|
+
unit tests + doctests green, `Nuts`/`Outs`/blockers match brute-force oracles,
|
|
72
|
+
and a 2000-hand sweep shows no `is_nut`/`nut_rank` contradictions.
|
|
73
|
+
|
|
74
|
+
### Candidate capabilities (menu, not yet scheduled)
|
|
75
|
+
|
|
76
|
+
| Capability | Category | Effort |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| FastState: snapshot/restore + undo for tree search | performance | L |
|
|
79
|
+
| Exact + deterministic equity engine with sampled-hand callbacks | equity | M |
|
|
80
|
+
| Range engine v2: weights, %-ranges, removal-aware combos | equity | M |
|
|
81
|
+
| Structured action-history & rich State view layer | dev-experience | M |
|
|
82
|
+
| Positions + multi-hand Table/session object | dev-experience | M |
|
|
83
|
+
| Bot/agent framework + baseline agents | AI/bots | M |
|
|
84
|
+
| Missing variants: 5/6-card PLO, Big O, Courchevel, 5-Card Draw, A-5 | variants | M |
|
|
85
|
+
| Mixed-game rotation (HORSE / 8-Game) | variants | M |
|
|
86
|
+
| Variant registry + parse-by-name + uniform create | integration | S |
|
|
87
|
+
| Fast lookup-table hand evaluator (5/6/7-card) | performance | L |
|
|
88
|
+
| Robust async multi-variant hand-history I/O | integration | L |
|
|
89
|
+
| State visualization & serialization (ASCII / SVG / JSON) | visualization | S |
|
|
90
|
+
| compat shim + Decimal/currency value type | dev-experience | S |
|
|
91
|
+
|
|
92
|
+
## Development
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
python3 -m venv .venv && . .venv/bin/activate
|
|
96
|
+
pip install -e ".[dev]"
|
|
97
|
+
pytest
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT (same as PokerKit).
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# PokerKit Plus
|
|
2
|
+
|
|
3
|
+
Fork-free capability extensions on top of [PokerKit](https://github.com/uoftcprg/pokerkit).
|
|
4
|
+
|
|
5
|
+
`pokerkit_plus` is a **drop-in superset** of `pokerkit`: it re-exports the entire
|
|
6
|
+
upstream surface unchanged and adds new capabilities on top via subclasses, free
|
|
7
|
+
functions, and thin wrappers. The upstream package is never modified, so PokerKit
|
|
8
|
+
Plus keeps riding upstream's maintenance (correctness fixes, new parsers, etc.).
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
# Anywhere you used pokerkit, you can use pokerkit_plus instead:
|
|
12
|
+
from pokerkit_plus import NoLimitTexasHoldem, Automation, State # all of pokerkit
|
|
13
|
+
# ...plus the PokerKit Plus additions as they land.
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Design
|
|
17
|
+
|
|
18
|
+
- **Form:** standalone extension package, `pip`-depends on `pokerkit==0.7.3` (pinned).
|
|
19
|
+
- **No fork, no monkeypatch.** PokerKit is built for fork-free extension
|
|
20
|
+
(`_begin_/_update_/_end_` phase hooks, mixin-composed variants, injectable
|
|
21
|
+
`rake`/`divmod`, `ClassVar` registries). Most additions need zero core edits.
|
|
22
|
+
- **`compat.py`** pins the pokerkit version and is the single home for stable
|
|
23
|
+
aliases of version-fragile upstream names.
|
|
24
|
+
|
|
25
|
+
## Status
|
|
26
|
+
|
|
27
|
+
Semantic layer P0/P1 implemented (drop-in superset of pokerkit + first
|
|
28
|
+
capabilities):
|
|
29
|
+
|
|
30
|
+
- `pokerkit_plus.texture` — `BoardTexture.from_board` plus `Wetness`,
|
|
31
|
+
`Connectivity`, `RankBand`, `StraightDraw`, `FlushDraw`, and
|
|
32
|
+
`are_two_tone`/`are_monotone`/`are_rainbow`.
|
|
33
|
+
- `pokerkit_plus.combos` — `Nuts.from_board`, `CategoryCombos.from_board`,
|
|
34
|
+
`HoleCombo`, `made_label`, `meets`, `CATEGORY_ORDER` (made-hand category is
|
|
35
|
+
pokerkit's own `Label`).
|
|
36
|
+
- `pokerkit_plus.draws` — `Draws.from_hand` (hero flush/straight draw + nut
|
|
37
|
+
rank) and the shared `NutRank`.
|
|
38
|
+
- `pokerkit_plus.tags` — `HandTier.from_hand` (made-hand tier + `labels()`)
|
|
39
|
+
with `PairTier`/`KickerTier`/`TwoPairTier`/`ThreeOfAKindTier`.
|
|
40
|
+
- `pokerkit_plus.outs` — `Outs.from_hand` (category-upgrade outs, grouped).
|
|
41
|
+
- `pokerkit_plus.blockers` — `BlockerReport.from_hand` (count-based: how many
|
|
42
|
+
of the board's nut combos a holding removes).
|
|
43
|
+
- `pokerkit_plus.ranges` — `ComboClass`/`expand_range` (notation),
|
|
44
|
+
`build_value_range`, `calculate_range_advantage` (Monte-Carlo equity), and
|
|
45
|
+
`nut_advantage` (exact nut-category share).
|
|
46
|
+
- `pokerkit_plus.facade` — `HandReport.from_hand` and `BoardReport.from_board`,
|
|
47
|
+
the one-call composed entry points for callers wanting a complete reading.
|
|
48
|
+
|
|
49
|
+
Built on pokerkit's lookup-table total order (no hand-rolled scoring); every
|
|
50
|
+
per-board / per-hand enumeration is memoized, and equity is delegated to
|
|
51
|
+
pokerkit's Monte-Carlo sampler. Verified: mypy `--strict` clean, ruff clean,
|
|
52
|
+
unit tests + doctests green, `Nuts`/`Outs`/blockers match brute-force oracles,
|
|
53
|
+
and a 2000-hand sweep shows no `is_nut`/`nut_rank` contradictions.
|
|
54
|
+
|
|
55
|
+
### Candidate capabilities (menu, not yet scheduled)
|
|
56
|
+
|
|
57
|
+
| Capability | Category | Effort |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| FastState: snapshot/restore + undo for tree search | performance | L |
|
|
60
|
+
| Exact + deterministic equity engine with sampled-hand callbacks | equity | M |
|
|
61
|
+
| Range engine v2: weights, %-ranges, removal-aware combos | equity | M |
|
|
62
|
+
| Structured action-history & rich State view layer | dev-experience | M |
|
|
63
|
+
| Positions + multi-hand Table/session object | dev-experience | M |
|
|
64
|
+
| Bot/agent framework + baseline agents | AI/bots | M |
|
|
65
|
+
| Missing variants: 5/6-card PLO, Big O, Courchevel, 5-Card Draw, A-5 | variants | M |
|
|
66
|
+
| Mixed-game rotation (HORSE / 8-Game) | variants | M |
|
|
67
|
+
| Variant registry + parse-by-name + uniform create | integration | S |
|
|
68
|
+
| Fast lookup-table hand evaluator (5/6/7-card) | performance | L |
|
|
69
|
+
| Robust async multi-variant hand-history I/O | integration | L |
|
|
70
|
+
| State visualization & serialization (ASCII / SVG / JSON) | visualization | S |
|
|
71
|
+
| compat shim + Decimal/currency value type | dev-experience | S |
|
|
72
|
+
|
|
73
|
+
## Development
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
python3 -m venv .venv && . .venv/bin/activate
|
|
77
|
+
pip install -e ".[dev]"
|
|
78
|
+
pytest
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT (same as PokerKit).
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""PokerKit Plus — a fork-free, drop-in superset of :mod:`pokerkit`.
|
|
2
|
+
|
|
3
|
+
Everything importable from :mod:`pokerkit` is re-exported here unchanged, so
|
|
4
|
+
any ``from pokerkit import X`` can become ``from pokerkit_plus import X`` with
|
|
5
|
+
no other change. PokerKit Plus capabilities are layered on top via subclasses,
|
|
6
|
+
free functions, and thin wrappers — the upstream package is never modified.
|
|
7
|
+
|
|
8
|
+
As capabilities land, their public names are appended to ``_PLUS_ALL`` (and the
|
|
9
|
+
relevant submodule is imported below) so they show up in ``pokerkit_plus``'s
|
|
10
|
+
star-import surface alongside the upstream names.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
# Re-export the entire upstream surface. pokerkit ships a complete ``__all__``,
|
|
16
|
+
# so the star import is exact and stable.
|
|
17
|
+
from pokerkit import * # noqa: F401, F403
|
|
18
|
+
from pokerkit import __all__ as _POKERKIT_ALL
|
|
19
|
+
|
|
20
|
+
# PokerKit Plus additions. Submodules own their public names (no per-module
|
|
21
|
+
# ``__all__``, matching pokerkit's leaf modules); this aggregator imports and
|
|
22
|
+
# registers them, exactly as pokerkit's own ``__init__`` does.
|
|
23
|
+
from pokerkit_plus.combos import ( # noqa: F401
|
|
24
|
+
CATEGORY_ORDER,
|
|
25
|
+
CategoryCombos,
|
|
26
|
+
HoleCombo,
|
|
27
|
+
Nuts,
|
|
28
|
+
made_label,
|
|
29
|
+
meets,
|
|
30
|
+
)
|
|
31
|
+
from pokerkit_plus.texture import ( # noqa: F401
|
|
32
|
+
BoardTexture,
|
|
33
|
+
Connectivity,
|
|
34
|
+
CONNECTIVITY_ORDER,
|
|
35
|
+
FlushDraw,
|
|
36
|
+
FLUSH_DRAW_ORDER,
|
|
37
|
+
RankBand,
|
|
38
|
+
RANK_BAND_ORDER,
|
|
39
|
+
StraightDraw,
|
|
40
|
+
STRAIGHT_DRAW_ORDER,
|
|
41
|
+
Wetness,
|
|
42
|
+
WETNESS_ORDER,
|
|
43
|
+
are_monotone,
|
|
44
|
+
are_rainbow,
|
|
45
|
+
are_two_tone,
|
|
46
|
+
)
|
|
47
|
+
from pokerkit_plus.draws import ( # noqa: F401
|
|
48
|
+
Draws,
|
|
49
|
+
NutRank,
|
|
50
|
+
NUT_RANK_ORDER,
|
|
51
|
+
)
|
|
52
|
+
from pokerkit_plus.tags import ( # noqa: F401
|
|
53
|
+
HandTier,
|
|
54
|
+
KickerTier,
|
|
55
|
+
PairTier,
|
|
56
|
+
ThreeOfAKindTier,
|
|
57
|
+
TwoPairTier,
|
|
58
|
+
)
|
|
59
|
+
from pokerkit_plus.outs import Outs # noqa: F401
|
|
60
|
+
from pokerkit_plus.blockers import BlockerReport # noqa: F401
|
|
61
|
+
from pokerkit_plus.ranges import ( # noqa: F401
|
|
62
|
+
Advantage,
|
|
63
|
+
AdvantageBasis,
|
|
64
|
+
Aggression,
|
|
65
|
+
AGGRESSION_ORDER,
|
|
66
|
+
ComboClass,
|
|
67
|
+
build_value_range,
|
|
68
|
+
calculate_range_advantage,
|
|
69
|
+
expand_range,
|
|
70
|
+
nut_advantage,
|
|
71
|
+
)
|
|
72
|
+
from pokerkit_plus.facade import ( # noqa: F401
|
|
73
|
+
BoardReport,
|
|
74
|
+
HandReport,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
__version__ = "0.0.1"
|
|
78
|
+
|
|
79
|
+
_PLUS_ALL: tuple[str, ...] = (
|
|
80
|
+
# combos
|
|
81
|
+
'CATEGORY_ORDER',
|
|
82
|
+
'CategoryCombos',
|
|
83
|
+
'HoleCombo',
|
|
84
|
+
'Nuts',
|
|
85
|
+
'made_label',
|
|
86
|
+
'meets',
|
|
87
|
+
# texture
|
|
88
|
+
'BoardTexture',
|
|
89
|
+
'Connectivity',
|
|
90
|
+
'CONNECTIVITY_ORDER',
|
|
91
|
+
'FlushDraw',
|
|
92
|
+
'FLUSH_DRAW_ORDER',
|
|
93
|
+
'RankBand',
|
|
94
|
+
'RANK_BAND_ORDER',
|
|
95
|
+
'StraightDraw',
|
|
96
|
+
'STRAIGHT_DRAW_ORDER',
|
|
97
|
+
'Wetness',
|
|
98
|
+
'WETNESS_ORDER',
|
|
99
|
+
'are_monotone',
|
|
100
|
+
'are_rainbow',
|
|
101
|
+
'are_two_tone',
|
|
102
|
+
# draws
|
|
103
|
+
'Draws',
|
|
104
|
+
'NutRank',
|
|
105
|
+
'NUT_RANK_ORDER',
|
|
106
|
+
# tags
|
|
107
|
+
'HandTier',
|
|
108
|
+
'KickerTier',
|
|
109
|
+
'PairTier',
|
|
110
|
+
'ThreeOfAKindTier',
|
|
111
|
+
'TwoPairTier',
|
|
112
|
+
# outs
|
|
113
|
+
'Outs',
|
|
114
|
+
# blockers
|
|
115
|
+
'BlockerReport',
|
|
116
|
+
# ranges
|
|
117
|
+
'Advantage',
|
|
118
|
+
'AdvantageBasis',
|
|
119
|
+
'Aggression',
|
|
120
|
+
'AGGRESSION_ORDER',
|
|
121
|
+
'ComboClass',
|
|
122
|
+
'build_value_range',
|
|
123
|
+
'calculate_range_advantage',
|
|
124
|
+
'expand_range',
|
|
125
|
+
'nut_advantage',
|
|
126
|
+
# facade
|
|
127
|
+
'BoardReport',
|
|
128
|
+
'HandReport',
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
__all__ = (*_POKERKIT_ALL, *_PLUS_ALL)
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
""":mod:`pokerkit_plus._semantic` implements private foundations shared
|
|
2
|
+
by the semantic layer.
|
|
3
|
+
|
|
4
|
+
Nothing here is part of the public surface; these helpers centralize live
|
|
5
|
+
card enumeration, royal-flush refinement, and the single memoized nut
|
|
6
|
+
enumeration that :mod:`pokerkit_plus.combos` (and later blocker/range
|
|
7
|
+
modules) build on. Everything is expressed in terms of :mod:`pokerkit`
|
|
8
|
+
primitives so that strength and category come from PokerKit's lookup-table
|
|
9
|
+
total order rather than any hand-rolled scoring.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from collections.abc import Iterator
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from functools import lru_cache
|
|
17
|
+
from itertools import combinations
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from pokerkit.hands import Hand
|
|
21
|
+
from pokerkit.lookups import Label
|
|
22
|
+
from pokerkit.utilities import Card, CardsLike, Deck, Rank, RankOrder
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from collections.abc import Mapping
|
|
26
|
+
|
|
27
|
+
__BROADWAY: frozenset[Rank] = frozenset(RankOrder.STANDARD[-5:])
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _used(*groups: CardsLike) -> frozenset[Card]:
|
|
31
|
+
"""Return the set of cards used (and hence dead) across the groups.
|
|
32
|
+
|
|
33
|
+
Each group is normalized through :meth:`pokerkit.utilities.Card.clean`,
|
|
34
|
+
so hole cards, board cards, and dead cards can be supplied in any
|
|
35
|
+
cards-like form (a string, a single card, or an iterable of cards).
|
|
36
|
+
|
|
37
|
+
>>> sorted(map(repr, _used('AsKs', 'Qd')))
|
|
38
|
+
['As', 'Ks', 'Qd']
|
|
39
|
+
>>> _used() == frozenset()
|
|
40
|
+
True
|
|
41
|
+
|
|
42
|
+
:param groups: The card groups to union together.
|
|
43
|
+
:return: The frozen set of all used cards.
|
|
44
|
+
"""
|
|
45
|
+
used: set[Card] = set()
|
|
46
|
+
|
|
47
|
+
for group in groups:
|
|
48
|
+
used.update(Card.clean(group))
|
|
49
|
+
|
|
50
|
+
return frozenset(used)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _live_cards(
|
|
54
|
+
*groups: CardsLike,
|
|
55
|
+
deck: Deck = Deck.STANDARD,
|
|
56
|
+
) -> Iterator[Card]:
|
|
57
|
+
"""Yield the live cards: deck cards not used by any group.
|
|
58
|
+
|
|
59
|
+
The deck is iterated in its native (deterministic) order, so the live
|
|
60
|
+
cards are yielded deterministically. This is a generator; callers
|
|
61
|
+
materialize it with ``tuple(...)`` only when they need a collection.
|
|
62
|
+
|
|
63
|
+
>>> live = tuple(_live_cards('AsKsQs'))
|
|
64
|
+
>>> len(live)
|
|
65
|
+
49
|
|
66
|
+
|
|
67
|
+
:param groups: The card groups whose cards are dead.
|
|
68
|
+
:param deck: The deck of candidate cards, defaults to
|
|
69
|
+
:attr:`pokerkit.utilities.Deck.STANDARD`.
|
|
70
|
+
:return: The iterator of live cards in deck order.
|
|
71
|
+
"""
|
|
72
|
+
used = _used(*groups)
|
|
73
|
+
|
|
74
|
+
for card in deck:
|
|
75
|
+
if card not in used:
|
|
76
|
+
yield card
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _is_royal(hand: Hand) -> bool:
|
|
80
|
+
"""Return whether the hand is a royal flush.
|
|
81
|
+
|
|
82
|
+
PokerKit reports a royal flush as :attr:`pokerkit.lookups.Label`
|
|
83
|
+
``.STRAIGHT_FLUSH``; this refines that by checking the five card ranks
|
|
84
|
+
are exactly the broadway set (ten through ace). The check is on the
|
|
85
|
+
rank *set*, not on the top card by index, because the steel wheel
|
|
86
|
+
(``As2s3s4s5s``) also has the ace as its highest card by
|
|
87
|
+
:attr:`pokerkit.utilities.RankOrder` ``.STANDARD`` index yet is not a
|
|
88
|
+
royal flush.
|
|
89
|
+
|
|
90
|
+
>>> from pokerkit.hands import StandardHighHand
|
|
91
|
+
>>> _is_royal(StandardHighHand.from_game('AsKs', 'QsJsTs'))
|
|
92
|
+
True
|
|
93
|
+
>>> _is_royal(StandardHighHand.from_game('As2s', '3s4s5s'))
|
|
94
|
+
False
|
|
95
|
+
>>> _is_royal(StandardHighHand.from_game('AcAd', 'AhAsKc'))
|
|
96
|
+
False
|
|
97
|
+
|
|
98
|
+
:param hand: The hand to test.
|
|
99
|
+
:return: ``True`` if the hand is a royal flush, otherwise ``False``.
|
|
100
|
+
"""
|
|
101
|
+
if hand.entry.label is not Label.STRAIGHT_FLUSH:
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
return frozenset(card.rank for card in hand.cards) == __BROADWAY
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True)
|
|
108
|
+
class HoleCombo:
|
|
109
|
+
"""A two-card hole combination together with its made hand.
|
|
110
|
+
|
|
111
|
+
Instances are produced by the nut enumeration core and never
|
|
112
|
+
constructed by users directly; the embedded :attr:`hand` is the
|
|
113
|
+
already-evaluated best hand for these hole cards on the enumerated
|
|
114
|
+
board, so consumers never re-evaluate.
|
|
115
|
+
|
|
116
|
+
>>> from pokerkit.hands import StandardHighHand
|
|
117
|
+
>>> hand = StandardHighHand.from_game('TsJs', 'AsKsQs')
|
|
118
|
+
>>> combo = HoleCombo(tuple(Card.parse('TsJs')), hand)
|
|
119
|
+
>>> sorted(map(repr, combo.cards))
|
|
120
|
+
['Js', 'Ts']
|
|
121
|
+
>>> combo.as_frozenset == frozenset(Card.parse('TsJs'))
|
|
122
|
+
True
|
|
123
|
+
|
|
124
|
+
:param cards: The two hole cards, in deck order.
|
|
125
|
+
:param hand: The evaluated best hand these cards make on the board.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
cards: tuple[Card, ...]
|
|
129
|
+
"""The two hole cards, in deck order."""
|
|
130
|
+
hand: Hand
|
|
131
|
+
"""The evaluated best hand these cards make on the board."""
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def as_frozenset(self) -> frozenset[Card]:
|
|
135
|
+
"""Return the hole cards as a frozen set.
|
|
136
|
+
|
|
137
|
+
This is the exact shape consumed by
|
|
138
|
+
:func:`pokerkit.analysis.parse_range` output and
|
|
139
|
+
:func:`pokerkit.analysis.calculate_equities` input, so a combo can
|
|
140
|
+
round-trip into an equity call.
|
|
141
|
+
|
|
142
|
+
>>> from pokerkit.hands import StandardHighHand
|
|
143
|
+
>>> hand = StandardHighHand.from_game('TsJs', 'AsKsQs')
|
|
144
|
+
>>> combo = HoleCombo(tuple(Card.parse('TsJs')), hand)
|
|
145
|
+
>>> combo.as_frozenset == frozenset(Card.parse('JsTs'))
|
|
146
|
+
True
|
|
147
|
+
|
|
148
|
+
:return: The hole cards as a frozen set.
|
|
149
|
+
"""
|
|
150
|
+
return frozenset(self.cards)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass(frozen=True)
|
|
154
|
+
class _NutsCore:
|
|
155
|
+
"""The shared, immutable result of one nut enumeration over a board.
|
|
156
|
+
|
|
157
|
+
This is the value cached by :func:`_nuts_core`. A single enumeration
|
|
158
|
+
pass produces both the nut hand (and the combos tying it) and the
|
|
159
|
+
per-category grouping of every live combo, so consumers never
|
|
160
|
+
re-evaluate.
|
|
161
|
+
|
|
162
|
+
:param hand: The strongest hand makeable on the board, or ``None`` if
|
|
163
|
+
the board is too short to enumerate.
|
|
164
|
+
:param combos: Every two-card combo whose best hand ties the strongest
|
|
165
|
+
hand, each carrying its already-evaluated hand.
|
|
166
|
+
:param candidate_count: The number of live two-card combos enumerated.
|
|
167
|
+
:param by_category: Every live combo grouped by its made-hand label, in
|
|
168
|
+
enumeration order.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
hand: Hand | None
|
|
172
|
+
"""The strongest hand makeable on the board, or ``None``."""
|
|
173
|
+
combos: tuple[HoleCombo, ...]
|
|
174
|
+
"""The combos tying the strongest hand, with evaluated hands."""
|
|
175
|
+
candidate_count: int
|
|
176
|
+
"""The number of live two-card combos enumerated."""
|
|
177
|
+
by_category: Mapping[Label, tuple[HoleCombo, ...]]
|
|
178
|
+
"""Every live combo grouped by its made-hand label."""
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@lru_cache(maxsize=None)
|
|
182
|
+
def _nuts_core(
|
|
183
|
+
board: frozenset[Card],
|
|
184
|
+
hand_type: type[Hand],
|
|
185
|
+
dead: frozenset[Card],
|
|
186
|
+
) -> _NutsCore:
|
|
187
|
+
"""Enumerate a board once, memoized by board, type, and dead cards.
|
|
188
|
+
|
|
189
|
+
This is the single source of nut enumeration shared by
|
|
190
|
+
:class:`pokerkit_plus.combos.Nuts` and
|
|
191
|
+
:class:`pokerkit_plus.combos.CategoryCombos`. It enumerates every live
|
|
192
|
+
two-card combo ONCE, calls ``hand_type.from_game_or_none`` exactly once
|
|
193
|
+
per combo, and in that same pass tracks the running maximum hand (with
|
|
194
|
+
every tying combo) and groups every combo by its made-hand label. No
|
|
195
|
+
combo is ever re-classified, and nothing uses ``list.index`` or rebuilds
|
|
196
|
+
a set per element.
|
|
197
|
+
|
|
198
|
+
The cache is keyed on :class:`frozenset` of
|
|
199
|
+
:class:`pokerkit.utilities.Card`, which is a correct key because
|
|
200
|
+
``Card`` is a frozen, hashable dataclass: two boards with the same card
|
|
201
|
+
set hash and compare equal regardless of order.
|
|
202
|
+
``functools.lru_cache`` is chosen over a hand-rolled module dict because
|
|
203
|
+
all three arguments are already hashable and immutable, the function is
|
|
204
|
+
pure, and ``lru_cache`` gives thread-safe memoization plus
|
|
205
|
+
``cache_info``/``cache_clear`` introspection for free.
|
|
206
|
+
|
|
207
|
+
A board with fewer than three cards returns an honest empty core
|
|
208
|
+
(``hand is None``, no combos) rather than raising or returning ``None``.
|
|
209
|
+
A board that is itself the nuts (e.g. a quad board) yields *every* live
|
|
210
|
+
combo as a tying combo, exposed via :attr:`_NutsCore.combos` and
|
|
211
|
+
:attr:`_NutsCore.candidate_count` so blockers can detect the
|
|
212
|
+
"no blocker possible" case.
|
|
213
|
+
|
|
214
|
+
:param board: The board cards as a frozen set.
|
|
215
|
+
:param hand_type: The hand type to evaluate with.
|
|
216
|
+
:param dead: Additional dead (used) cards as a frozen set.
|
|
217
|
+
:return: The cached enumeration result.
|
|
218
|
+
"""
|
|
219
|
+
board_cards = tuple(board)
|
|
220
|
+
|
|
221
|
+
if len(board_cards) < 3:
|
|
222
|
+
return _NutsCore(None, (), 0, {})
|
|
223
|
+
|
|
224
|
+
max_hand: Hand | None = None
|
|
225
|
+
tying: list[HoleCombo] = []
|
|
226
|
+
grouped: dict[Label, list[HoleCombo]] = {}
|
|
227
|
+
candidate_count = 0
|
|
228
|
+
|
|
229
|
+
for hole in combinations(_live_cards(board, dead), 2):
|
|
230
|
+
candidate_count += 1
|
|
231
|
+
hand = hand_type.from_game_or_none(hole, board_cards)
|
|
232
|
+
|
|
233
|
+
if hand is None:
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
combo = HoleCombo(hole, hand)
|
|
237
|
+
grouped.setdefault(hand.entry.label, []).append(combo)
|
|
238
|
+
|
|
239
|
+
if max_hand is None or hand > max_hand:
|
|
240
|
+
max_hand = hand
|
|
241
|
+
tying = [combo]
|
|
242
|
+
elif hand == max_hand:
|
|
243
|
+
tying.append(combo)
|
|
244
|
+
|
|
245
|
+
by_category = {
|
|
246
|
+
label: tuple(combos) for label, combos in grouped.items()
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return _NutsCore(max_hand, tuple(tying), candidate_count, by_category)
|