pkpython 0.0.54__cp310-cp310-win_amd64.whl

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,874 @@
1
+ Metadata-Version: 2.4
2
+ Name: pkpython
3
+ Version: 0.0.54
4
+ Classifier: Programming Language :: Rust
5
+ Classifier: Programming Language :: Python :: Implementation :: CPython
6
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
7
+ Classifier: Topic :: Games/Entertainment
8
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
9
+ License-File: LICENSE
10
+ Summary: Python bindings for pkcore, a high-performance poker analysis library
11
+ Author-email: folkengine <gaoler@electronicpanopticon.com>
12
+ License: GPL-3.0-or-later
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
15
+ Project-URL: Bug Tracker, https://github.com/ImperialBower/pkpy/issues
16
+ Project-URL: Homepage, https://github.com/ImperialBower/pkpy
17
+ Project-URL: Repository, https://github.com/ImperialBower/pkpy
18
+
19
+ [![CI](https://github.com/ImperialBower/pkpy/actions/workflows/ci.yml/badge.svg)](https://github.com/ImperialBower/pkpy/actions/workflows/ci.yml)
20
+ [![PyPI](https://img.shields.io/pypi/v/pkpython.svg)](https://pypi.org/project/pkpython/)
21
+ [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](LICENSE)
22
+
23
+ # pkpy
24
+
25
+ Python bindings for [pkcore](https://github.com/folkengine/pkcore), a high-performance poker analysis library written in Rust.
26
+
27
+ ## What This Project Does
28
+
29
+ pkpy lets Python developers use pkcore's poker engine — card parsing, hand evaluation, Texas Hold'em game simulation, outs calculation, and more — without writing any Rust. The Rust library runs natively and is called directly from Python with no subprocess overhead or serialization round-trips.
30
+
31
+ ---
32
+
33
+ ## Dependencies
34
+
35
+ - [pkcore](https://crates.io/crates/pkcore) — the underlying Rust poker analysis library
36
+ - [PyO3](https://pyo3.rs) — Rust/Python bindings framework
37
+ - [Maturin](https://maturin.rs) — build tool for PyO3 extension modules
38
+
39
+ See [docs/STACK.md](docs/STACK.md) for more details on the technology stack.
40
+
41
+ ---
42
+
43
+ ## Cactus Kev Binary Card Representation
44
+
45
+ pkcore represents each card as a single `u32` using a variation of [Cactus Kev's binary encoding](https://suffe.cool/poker/evaluator.html), designed for O(1) hand evaluation via lookup tables.
46
+
47
+ ```
48
+ +--------+--------+--------+--------+
49
+ |mmmbbbbb|bbbbbbbb|SHDCrrrr|xxpppppp|
50
+ +--------+--------+--------+--------+
51
+ ```
52
+
53
+ | Bits | Meaning |
54
+ |------|---------|
55
+ | `p` (6 bits) | Prime number for the rank (Deuce=2, Trey=3, ..., Ace=41) |
56
+ | `r` (4 bits) | Rank index (Deuce=0, Trey=1, ..., Ace=12) |
57
+ | `SHDC` (4 bits) | Suit flags — one bit per suit |
58
+ | `b` (13 bits) | One bit set per rank — used for flush/straight detection |
59
+ | `m` (3 bits) | Frequency flags (paired, tripped, quaded) — stripped during eval |
60
+
61
+ This encoding makes many operations branch-free bit manipulations. For example, detecting a flush is a single bitwise AND across five cards' suit bits.
62
+
63
+ ## Hand Evaluation
64
+
65
+ pkcore uses a two-level lookup table strategy (the same approach as the original Cactus Kev evaluator):
66
+
67
+ 1. **Flushes and straights** are detected via the rank-bit field (`b` bits). A 13-bit mask uniquely identifies every possible straight and flush pattern.
68
+ 2. **All other hands** are identified by multiplying the five rank primes together. Since every rank maps to a distinct prime, the product uniquely identifies the rank multiset — pairs, trips, quads, and full houses all have unique products. The product indexes into a lookup table that returns the `HandRankValue`.
69
+
70
+ A lower `HandRankValue` is a stronger hand (1 = royal flush, 7462 = worst high card).
71
+
72
+ ---
73
+
74
+ ## Project Structure
75
+
76
+ ```
77
+ pkpy/
78
+ ├── Cargo.toml # Rust crate manifest
79
+ ├── pyproject.toml # Python build config (maturin)
80
+ ├── src/
81
+ │ └── lib.rs # All PyO3 bindings
82
+ ├── python/
83
+ │ └── pkpy/
84
+ │ └── __init__.py # Python package — re-exports everything from the extension
85
+ └── tests/
86
+ └── test_pkpy.py # pytest test suite
87
+ ```
88
+
89
+ The `python/pkpy/` directory is the Python package. The compiled Rust extension (`_pkpy.so`) is dropped into it by maturin. `__init__.py` re-exports everything so users write `from pkpy import Card` rather than `from pkpy._pkpy import Card`.
90
+
91
+ ---
92
+
93
+ ## API Reference
94
+
95
+ ### `Card`
96
+
97
+ A single playing card. Internally a `u32` in Cactus Kev format.
98
+
99
+ ```python
100
+ from pkpy import Card, Rank, Suit
101
+
102
+ # Parse from string — accepts "As", "A♠", "a♠", "AH", etc.
103
+ ace_spades = Card.parse("As")
104
+ king_hearts = Card.parse("K♥")
105
+
106
+ # Construct from rank and suit
107
+ card = Card.from_rank_suit(Rank.QUEEN, Suit.DIAMONDS)
108
+
109
+ # Inspect
110
+ card.rank() # -> Rank
111
+ card.suit() # -> Suit
112
+ card.is_dealt() # -> bool (False for blank/sentinel cards)
113
+ card.as_u32() # -> int (raw Cactus Kev encoding)
114
+ card.bit_string() # -> str (binary representation of the encoding)
115
+ card.get_rank_prime() # -> int (rank prime used in hand evaluation)
116
+ card.get_letter_index() # -> str (letter-index form, e.g. "As")
117
+
118
+ str(card) # -> "Q♦"
119
+ card == Card.parse("Qd") # -> True
120
+ ```
121
+
122
+ ### `Deck`
123
+
124
+ A standard 52-card deck. All methods are static — `Deck` is a namespace for deck-level operations.
125
+
126
+ ```python
127
+ from pkpy import Deck
128
+
129
+ deck = Deck.poker_cards() # -> Cards, ordered A♠ down to 2♣
130
+ shuffled = Deck.poker_cards_shuffled() # -> Cards, randomly shuffled
131
+ Deck.get(0) # -> Card (A♠, the first card in deck order)
132
+ Deck.len() # -> 52
133
+ ```
134
+
135
+ ### `Cards`
136
+
137
+ An ordered, unique collection of cards backed by an `IndexSet` (ordered hash set). Duplicate inserts are silently ignored.
138
+
139
+ ```python
140
+ from pkpy import Cards
141
+
142
+ hand = Cards.parse("As Ks Qh")
143
+ deck = Cards.deck() # full 52-card deck in order
144
+
145
+ len(hand) # -> 3
146
+ hand.is_empty() # -> False
147
+ hand.contains(Card.parse("As")) # -> True
148
+ hand.remaining() # -> Cards with 49 cards (deck minus hand)
149
+ hand.remaining_after(board) # -> deck minus hand minus board
150
+ hand.is_dealt() # -> True if no blank cards
151
+ hand.are_unique() # -> True if no duplicates
152
+
153
+ for card in hand: # iterable
154
+ print(card)
155
+
156
+ hand.to_list() # -> list[Card]
157
+ hand.get_index(0) # -> Card | None (card at position 0)
158
+
159
+ # Mutation
160
+ hand.insert(Card.parse("Jh")) # -> bool (True if card was new)
161
+ hand.remove(Card.parse("As")) # -> bool (True if card was present)
162
+ hand.append(Cards.parse("Tc 9c")) # merge another Cards in place
163
+ hand.shuffle_in_place() # shuffle in place
164
+
165
+ # Non-mutating transformations
166
+ hand.shuffle() # -> Cards (shuffled copy)
167
+ hand.sort() # -> Cards (sorted highest rank first)
168
+ hand.minus(other) # -> Cards (this minus other)
169
+ hand.filter_by_suit(Suit.SPADES) # -> Cards (only spades)
170
+ hand.combinations(2) # -> list[Cards] (all 2-card combos)
171
+
172
+ # Drawing (mutates the source collection)
173
+ card = hand.draw_one() # -> Card (removes and returns the top card)
174
+ drawn = hand.draw(3) # -> Cards (removes and returns 3 cards)
175
+ rest = hand.draw_all() # -> Cards (empties the collection)
176
+
177
+ # Deck-relative operations
178
+ hand.deck_minus() # -> Cards (52-card deck minus this collection)
179
+ hand.deck_primed() # -> Cards (this collection first, then rest of deck)
180
+ ```
181
+
182
+ ### `HoleCards`
183
+
184
+ A collection of two-card hands for one or more players. Cards are parsed in pairs: the first two belong to player 1, the next two to player 2, and so on.
185
+
186
+ ```python
187
+ from pkpy import HoleCards
188
+
189
+ # Two players
190
+ hc = HoleCards.parse("As Kh 8d Kc")
191
+ len(hc) # -> 2
192
+ hc.is_empty() # -> False
193
+ hc.get(0) # -> Two | None (0-indexed)
194
+ hc.to_list() # -> list[Two]
195
+
196
+ # Build programmatically
197
+ hc = HoleCards.parse("As Kh")
198
+ hc.push(Two.parse("Qd Jc"))
199
+ len(hc) # -> 2
200
+ ```
201
+
202
+ ### `Board`
203
+
204
+ The community cards (flop, turn, river).
205
+
206
+ ```python
207
+ from pkpy import Board
208
+
209
+ board = Board.parse("Ac 8h 7h 9s") # flop + turn
210
+ board = Board.parse("Ac 8h 7h 9s 5s") # full board
211
+
212
+ board.turn_cards() # -> Cards (flop + turn, 4 cards)
213
+ str(board) # -> "FLOP: A♣ 8♥ 7♥, TURN: 9♠, RIVER: _"
214
+ ```
215
+
216
+ ### `Game`
217
+
218
+ Combines hole cards and a board. The main entry point for analysis.
219
+
220
+ ```python
221
+ from pkpy import Game, HoleCards, Board, Outs
222
+
223
+ hc = HoleCards.parse("As Kh 8d Kc")
224
+ board = Board.parse("Ac 8h 7h 9s")
225
+ game = Game(hc, board)
226
+
227
+ game.has_dealt_turn() # -> bool (True if board has a turn card)
228
+ case_evals = game.turn_case_evals() # evaluates all possible river cards
229
+ game.turn_eval_for_player(0) # -> Eval for player at index 0 (raises on missing turn)
230
+ game.turn_remaining_board() # -> Cards (deck cards not yet on the board or in hands)
231
+ game.flop_and_turn() # -> Cards (the 4 board cards through the turn)
232
+
233
+ flop_eval = game.flop_eval() # -> FlopEval | None
234
+ turn_eval = game.turn_eval() # -> TurnEval | None
235
+
236
+ print(game.turn_nuts_display()) # best hands possible at the turn
237
+ print(game.river_display()) # final result with winner
238
+ ```
239
+
240
+ ### `CaseEvals`
241
+
242
+ The result of `game.turn_case_evals()`. Contains one evaluation per possible river card (typically 44–46 entries depending on how many cards are already accounted for).
243
+
244
+ ```python
245
+ len(case_evals) # -> number of possible river cards evaluated
246
+ ```
247
+
248
+ ### `Outs`
249
+
250
+ Cards that, if dealt on the river, cause a specific player to win. Built from `CaseEvals`.
251
+
252
+ ```python
253
+ from pkpy import Outs
254
+
255
+ outs = Outs.from_case_evals(case_evals)
256
+
257
+ outs.len_for_player(1) # -> int: number of winning river cards for player 1
258
+ outs.len_for_player(2) # -> int: number of winning river cards for player 2
259
+ outs.get(1) # -> Cards | None: the actual out cards for player 1
260
+ outs.longest_player() # -> int: player id with the most outs
261
+ outs.is_longest(2) # -> bool
262
+ outs.len_longest() # -> int: how many outs the leading player has
263
+ ```
264
+
265
+ Players are 1-indexed.
266
+
267
+ ### `HandRank` and `HandRankClass`
268
+
269
+ `HandRank` holds the numeric strength of a five-card hand. Lower `value` = stronger hand.
270
+
271
+ `HandRankClass` is the detailed category (e.g., `RoyalFlush`, `FourAces`, `AcesOverKings`).
272
+
273
+ ```python
274
+ from pkpy import HandRankClass
275
+
276
+ HandRankClass.ROYAL_FLUSH.is_straight_flush() # -> True
277
+ str(HandRankClass.ROYAL_FLUSH) # -> "RoyalFlush"
278
+ ```
279
+
280
+ `HandRank` is obtained from `Eval` objects, which come out of `CaseEvals`. Direct construction is not exposed since you'd normally get them via game evaluation.
281
+
282
+ ### Constants
283
+
284
+ ```python
285
+ from pkpy import (
286
+ unique_5_card_hands, # 2,598,960
287
+ distinct_5_card_hands, # 7,462
288
+ unique_2_card_hands, # 1,326
289
+ distinct_2_card_hands, # 169
290
+ )
291
+ ```
292
+
293
+ ---
294
+
295
+ ## GTO Range Analysis
296
+
297
+ ### `Combo`
298
+
299
+ An abstract hand combination defined by rank(s) and a suit qualifier.
300
+
301
+ ```python
302
+ from pkpy import Combo
303
+
304
+ c = Combo.parse("AKs")
305
+ c.is_suited() # -> True
306
+ c.is_pair() # -> False
307
+ c.is_ace_x() # -> True
308
+ c.total_pairs() # -> 4 (four suited AK combos)
309
+ c.first # -> Rank.ACE
310
+ c.second # -> Rank.KING
311
+ c.plus # -> False
312
+
313
+ Combo.parse("JJ+").plus # -> True
314
+ Combo.parse("QQ").total_pairs() # -> 6 (six ways to make QQ)
315
+ Combo.parse("AKo").total_pairs()# -> 12 (twelve offsuit AK combos)
316
+ ```
317
+
318
+ ### `Combos`
319
+
320
+ A range of abstract hand combinations, parsed from standard poker range notation.
321
+
322
+ ```python
323
+ from pkpy import Combos
324
+
325
+ r = Combos.parse("QQ+, AK")
326
+ len(r) # -> 5 (QQ, KK, AA, AKs, AKo as abstract combos)
327
+
328
+ twos = r.explode()
329
+ len(twos) # -> 34 (all concrete two-card hands)
330
+
331
+ # Predefined ranges (returned as strings, pass to Combos.parse)
332
+ Combos.PERCENT_2_5 # "QQ+, AK" — top ~2.5% of hands
333
+ Combos.PERCENT_5 # "TT+, AQ+" — top ~5%
334
+ Combos.PERCENT_10 # "44+, AJ+, ..." — top ~10%
335
+ Combos.PERCENT_20 # top ~20%
336
+ Combos.PERCENT_33 # top ~33%
337
+
338
+ # Parse a predefined range
339
+ tight = Combos.parse(Combos.PERCENT_2_5)
340
+ ```
341
+
342
+ ### `Two`
343
+
344
+ A concrete two-card hand — the unit produced by combo explosion.
345
+
346
+ ```python
347
+ from pkpy import Two
348
+
349
+ t = Two.parse("As Kh")
350
+ t.first() # -> Card (A♠)
351
+ t.second() # -> Card (K♥)
352
+ t.is_suited() # -> False
353
+ t.is_pair() # -> False
354
+ t.contains_rank(Rank.ACE) # -> True
355
+ t.contains_suit(Suit.SPADES) # -> True
356
+ ```
357
+
358
+ ### `Twos`
359
+
360
+ The collection returned by `Combos.explode()`. Supports filtering.
361
+
362
+ ```python
363
+ from pkpy import Combos
364
+
365
+ twos = Combos.parse("QQ+, AK").explode()
366
+
367
+ twos.filter_is_paired() # -> Twos (only pocket pairs)
368
+ twos.filter_is_not_paired() # -> Twos (only non-paired hands)
369
+ twos.filter_is_suited() # -> Twos (only suited hands)
370
+ twos.filter_is_not_suited() # -> Twos (only offsuit hands)
371
+ twos.filter_on_rank(Rank.ACE) # -> Twos (hands containing an Ace)
372
+ twos.filter_on_card(Card.parse("As")) # -> Twos (hands containing A♠)
373
+
374
+ twos.to_list() # -> list[Two]
375
+ twos.contains(Two.parse("As Kh")) # -> bool
376
+ ```
377
+
378
+ ### `Qualifier`
379
+
380
+ The suit qualifier for a combo: `SUITED`, `OFFSUIT`, or `ALL`.
381
+
382
+ ```python
383
+ from pkpy import Combo, Qualifier
384
+
385
+ Combo.parse("AKs").qualifier == Qualifier.SUITED # -> True
386
+ Combo.parse("AKo").qualifier == Qualifier.OFFSUIT # -> True
387
+ Combo.parse("AK").qualifier == Qualifier.ALL # -> True
388
+ ```
389
+
390
+ ### GTO Example
391
+
392
+ ```python
393
+ from pkpy import Combos, Rank
394
+
395
+ # Villain's opening range
396
+ villain_range = Combos.parse("66+,AJs+,KQs,AJo+,KQo")
397
+
398
+ # Expand to all concrete two-card hands
399
+ twos = villain_range.explode()
400
+ print(f"Total hands in range: {len(twos)}")
401
+
402
+ # How many are pocket pairs vs. unpaired?
403
+ pairs = twos.filter_is_paired()
404
+ unpaired = twos.filter_is_not_paired()
405
+ print(f"Pairs: {len(pairs)}, Unpaired: {len(unpaired)}")
406
+
407
+ # Hands containing an Ace
408
+ ace_hands = twos.filter_on_rank(Rank.ACE)
409
+ print(f"Ace-x hands: {len(ace_hands)}")
410
+
411
+ # Suited vs. offsuit breakdowns
412
+ print(f"Suited: {len(twos.filter_is_suited())}")
413
+ print(f"Offsuit: {len(twos.filter_is_not_suited())}")
414
+ ```
415
+
416
+ ---
417
+
418
+ ## Binary Card Maps
419
+
420
+ pkpy exposes pkcore's binary card map types, which provide compact, high-performance hand evaluation storage. These are the building blocks for precomputed lookup tables.
421
+
422
+ ### `Bard`
423
+
424
+ A 64-bit bitset where each of the 52 cards occupies one bit. Set operations (union, intersection, membership) are single CPU instructions.
425
+
426
+ ```python
427
+ from pkpy import Bard, Card, Cards
428
+
429
+ # Construct
430
+ b = Bard.from_card(Card.parse("As")) # single card
431
+ b = Bard.from_cards(Cards.parse("As Ks")) # from a Cards collection
432
+ b = Bard.from_u64(4_362_862_139_015_168) # from a raw u64
433
+
434
+ # Constants
435
+ Bard.BLANK # all bits zero
436
+ Bard.ALL # all 52 card bits set
437
+
438
+ # Operations
439
+ b2 = b.fold_in(Card.parse("Qs")) # returns new Bard with that card added
440
+ b.as_u64() # -> int (raw bit value)
441
+ b.to_cards() # -> Cards (reconstruct card set)
442
+ b.as_guided_string() # -> str (debug visualization)
443
+ ```
444
+
445
+ ### `SevenFiveBCM`
446
+
447
+ A binary card map entry for a 5- or 7-card hand. Stores the hand's `Bard`, the best 5-card sub-hand's `Bard`, and the hand rank value. This is the format used by pkcore's precomputed CSV lookup table.
448
+
449
+ `rank` follows the Cactus Kev convention: **lower is stronger** (1 = royal flush, 7462 = worst high card).
450
+
451
+ ```python
452
+ from pkpy import Cards, SevenFiveBCM
453
+
454
+ # Build from a 5-card hand
455
+ bcm = SevenFiveBCM.from_cards(Cards.parse("As Ks Qs Js Ts"))
456
+ bcm.rank # -> 1 (royal flush)
457
+ bcm.bc # -> Bard (bitset of the full hand)
458
+ bcm.best # -> Bard (bitset of the best 5-card sub-hand; same as bc for 5 cards)
459
+
460
+ # Build from a 7-card hand — bc is the full 7-card bard, best is the best 5
461
+ bcm7 = SevenFiveBCM.from_cards(Cards.parse("As Ks Qs Js Ts 9s 8s"))
462
+ bcm7.rank # -> 1
463
+ bcm7.bc.to_cards() # -> Cards (7 cards)
464
+ bcm7.best.to_cards() # -> Cards (best 5 cards)
465
+
466
+ # CSV generation (produces the ~5 GB bcm.csv lookup file — slow)
467
+ SevenFiveBCM.default_csv_path # -> "generated/bcm.csv"
468
+ SevenFiveBCM.generate_csv("bcm.csv") # enumerate all 5- and 7-card combos
469
+ ```
470
+
471
+ ### `IndexCardMap`
472
+
473
+ Like `SevenFiveBCM` but stores card hands as human-readable display strings instead of `Bard` bitsets. Useful for inspectable CSV output.
474
+
475
+ ```python
476
+ from pkpy import Cards, IndexCardMap
477
+
478
+ icm = IndexCardMap.from_cards(Cards.parse("As Ks Qs Js Ts"))
479
+ icm.rank # -> 1
480
+ icm.cards # -> "A♠ K♠ Q♠ J♠ T♠"
481
+ icm.best # -> "A♠ K♠ Q♠ J♠ T♠" (same for 5-card hand)
482
+
483
+ icm7 = IndexCardMap.from_cards(Cards.parse("As Ks Qs Js Ts 9s 8s"))
484
+ icm7.cards # -> "A♠ K♠ Q♠ J♠ T♠ 9♠ 8♠" (all 7 cards)
485
+ icm7.best # -> "A♠ K♠ Q♠ J♠ T♠" (best 5)
486
+
487
+ IndexCardMap.generate_csv("icm.csv")
488
+ ```
489
+
490
+ ### BCM example
491
+
492
+ ```python
493
+ from pkpy import Cards, SevenFiveBCM, IndexCardMap
494
+
495
+ hands = [
496
+ Cards.parse("As Ks Qs Js Ts"), # royal flush
497
+ Cards.parse("As Ks Qs Js 9s"), # king-high straight flush
498
+ Cards.parse("As Ad Ah Ac Ks"), # four aces
499
+ ]
500
+
501
+ for hand in hands:
502
+ bcm = SevenFiveBCM.from_cards(hand)
503
+ icm = IndexCardMap.from_cards(hand)
504
+ print(f"{icm.cards} rank={bcm.rank} best={icm.best}")
505
+ ```
506
+
507
+ ---
508
+
509
+ ## Pluribus Log Parsing
510
+
511
+ pkpy can parse hand histories from the [Pluribus](https://en.wikipedia.org/wiki/Pluribus_(poker_bot)) AI poker logs. Each line in a log file is a `STATE` record encoding one hand.
512
+
513
+ ### Log format
514
+
515
+ ```
516
+ STATE:{index}:{rounds}:{cards}:{winnings}:{players}
517
+ ```
518
+
519
+ - **rounds** — slash-separated betting round strings, e.g. `r200ffcfc/cr850cf`. Each character is `f` (fold), `c` (call), or `r{n}` (raise to n).
520
+ - **cards** — pipe-separated two-card hands, optionally followed by `/board`, e.g. `Qc4h|Tc9c|5h5d/3h7s5c/Qs/6c`.
521
+ - **winnings** — pipe-separated signed integers, one per player.
522
+ - **players** — pipe-separated player names.
523
+
524
+ ### `PluribusEvent`
525
+
526
+ A single action: fold, call, or raise.
527
+
528
+ ```python
529
+ from pkpy import Pluribus
530
+
531
+ hand = Pluribus.parse("STATE:0:ffr225fff:3c9s|6d5s|9dTs|2sQs|AdKd|7cTc:-50|-100|0|0|150|0:MrWhite|Gogo|Budd|Eddie|Bill|Pluribus")
532
+
533
+ for event in hand.actions():
534
+ print(event) # "Fold", "Call", "Raise(225)", etc.
535
+ event.is_fold() # -> bool
536
+ event.is_call() # -> bool
537
+ event.is_raise() # -> bool
538
+ event.raise_amount() # -> int | None
539
+ ```
540
+
541
+ ### `Pluribus`
542
+
543
+ A parsed hand record.
544
+
545
+ ```python
546
+ from pkpy import Pluribus
547
+
548
+ # Parse a single log line
549
+ hand = Pluribus.parse("STATE:27:r200ffcfc/cr850cf/cr1825r3775c/r10000c:Qc4h|Tc9c|8sAs|Qh7c|JcQd|5h5d/3h7s5c/Qs/6c:-50|-200|-10000|0|0|10250:Eddie|Bill|Pluribus|MrWhite|Gogo|Budd")
550
+
551
+ hand.index # -> 27
552
+ hand.players # -> ['Eddie', 'Bill', 'Pluribus', 'MrWhite', 'Gogo', 'Budd']
553
+ hand.winnings # -> [-50, -200, -10000, 0, 0, 10250]
554
+ hand.hole_cards # -> HoleCards (6 players' hands)
555
+ hand.board # -> Board (3h 7s 5c Qs 6c)
556
+ hand.raw # -> the original log line string
557
+
558
+ hand.rounds() # -> list[str] raw round strings
559
+ hand.actions() # -> list[PluribusEvent] all actions flat
560
+ hand.actions_for_round(0) # -> list[PluribusEvent] actions in round 0
561
+ hand.display_results() # -> str formatted winnings summary
562
+
563
+ # Parse an entire log file — invalid lines are silently skipped
564
+ hands = Pluribus.read_log("/path/to/pluribus.log")
565
+ print(f"Loaded {len(hands)} hands")
566
+ ```
567
+
568
+ ### Pluribus example
569
+
570
+ ```python
571
+ from pkpy import Pluribus
572
+
573
+ LOG_LINE = "STATE:27:r200ffcfc/cr850cf/cr1825r3775c/r10000c:Qc4h|Tc9c|8sAs|Qh7c|JcQd|5h5d/3h7s5c/Qs/6c:-50|-200|-10000|0|0|10250:Eddie|Bill|Pluribus|MrWhite|Gogo|Budd"
574
+
575
+ hand = Pluribus.parse(LOG_LINE)
576
+
577
+ print(f"Hand #{hand.index}")
578
+ print(f"Players: {', '.join(hand.players)}")
579
+ print(f"Board: {hand.board}")
580
+ print(f"Hole cards dealt: {len(hand.hole_cards)} players")
581
+
582
+ raises = [e for e in hand.actions() if e.is_raise()]
583
+ print(f"Raises this hand: {len(raises)}")
584
+ for r in raises:
585
+ print(f" {r.raise_amount()}")
586
+
587
+ print(hand.display_results())
588
+ ```
589
+
590
+ ---
591
+
592
+ ## Casino Table Simulation
593
+
594
+ pkpy exposes pkcore's casino table simulation layer, which models a heads-up or multi-player poker table with blinds, betting, and chip accounting. The key types are `Dealer` (the engine), `Player`, `ForcedBets`, and the log/result types.
595
+
596
+ ### `ForcedBets`
597
+
598
+ Configures the blinds and optional ante for a hand.
599
+
600
+ ```python
601
+ from pkpy import ForcedBets
602
+
603
+ bets = ForcedBets(small_blind=50, big_blind=100)
604
+ bets = ForcedBets(small_blind=50, big_blind=100, ante=25)
605
+ ```
606
+
607
+ ### `Stack`
608
+
609
+ A chip count wrapper.
610
+
611
+ ```python
612
+ from pkpy import Stack
613
+
614
+ s = Stack(1000)
615
+ s.count() # -> 1000
616
+ s.is_empty() # -> False
617
+ ```
618
+
619
+ ### `Player`
620
+
621
+ A player seated at the table with a name and chip stack.
622
+
623
+ ```python
624
+ from pkpy import Player
625
+
626
+ p = Player("Alice", 1000)
627
+ p.handle # -> "Alice"
628
+ p.chips() # -> 1000 (current stack, excluding chips committed to pot)
629
+ p.total_chips() # -> 1000 (chips + any committed amount)
630
+ p.state() # -> PlayerState
631
+ p.is_active() # -> bool
632
+ p.is_folded() # -> bool
633
+ p.is_all_in() # -> bool
634
+ p.is_sitting_out()# -> bool
635
+ ```
636
+
637
+ ### `PlayerState`
638
+
639
+ Describes what a player is currently doing at the table.
640
+
641
+ ```python
642
+ state = player.state()
643
+ state.kind() # -> str ("Active", "Folded", "AllIn", "SittingOut")
644
+ state.amount() # -> int (chips committed in current state, e.g. blind amount)
645
+ state.is_active() # -> bool
646
+ state.is_folded() # -> bool
647
+ state.is_all_in() # -> bool
648
+ state.is_sitting_out()# -> bool
649
+ ```
650
+
651
+ ### `Seatbit`
652
+
653
+ A compact bitset of occupied seat numbers (seats 0–15).
654
+
655
+ ```python
656
+ from pkpy import Seatbit
657
+
658
+ sb = dealer.ready()
659
+ sb.contains(0) # -> bool (is seat 0 occupied?)
660
+ sb.count() # -> int (number of occupied seats)
661
+ sb.as_u16() # -> int (raw bitset value)
662
+ ```
663
+
664
+ ### `SeatEquity`
665
+
666
+ Chip allocation tied to a set of seats — used inside `Win` to record who wins what.
667
+
668
+ ```python
669
+ se = win.equity
670
+ se.chips # -> int (chip amount)
671
+ se.seats # -> Seatbit
672
+ se.count() # -> int (number of winning seats)
673
+ se.is_nada() # -> bool (True if chips == 0)
674
+ ```
675
+
676
+ ### `Win`
677
+
678
+ One entry in a `Winnings` result. Pairs an equity award with the `Eval` that justified it.
679
+
680
+ ```python
681
+ win = winnings.first()
682
+ win.equity # -> SeatEquity
683
+ win.eval # -> Eval
684
+ ```
685
+
686
+ ### `Winnings`
687
+
688
+ The payout result returned by `Dealer.end_hand()`.
689
+
690
+ ```python
691
+ winnings = dealer.end_hand()
692
+ len(winnings) # -> int (number of pots/side-pots awarded)
693
+ winnings.first() # -> Win (main pot winner)
694
+ winnings.to_list() # -> list[Win]
695
+ ```
696
+
697
+ ### `TableAction`
698
+
699
+ A single event recorded in the table log.
700
+
701
+ ```python
702
+ action = log.last()
703
+ action.kind() # -> str ("Bet", "Raise", "Call", "Check", "Fold", "PostBlind", etc.)
704
+ action.seat() # -> int (seat number that took the action)
705
+ action.amount() # -> int (chip amount, 0 for non-chip actions like fold/check)
706
+ ```
707
+
708
+ ### `TableLog`
709
+
710
+ A running record of all actions taken during the hand.
711
+
712
+ ```python
713
+ log = dealer.event_log()
714
+ log.entries() # -> list[TableAction] (all recorded events)
715
+ log.last() # -> TableAction | None
716
+ log.last_player_action() # -> TableAction | None (last non-system action)
717
+ log.have_posted_blinds() # -> bool
718
+ ```
719
+
720
+ ### `Dealer`
721
+
722
+ The table engine. Manages seating, hand flow, betting, and chip accounting.
723
+
724
+ ```python
725
+ from pkpy import Dealer, ForcedBets, Player
726
+
727
+ dealer = Dealer(ForcedBets(50, 100))
728
+
729
+ # Seat players — consumes the Player object (ownership transfer)
730
+ seat0 = dealer.seat_player(alice) # -> int (assigned seat number)
731
+ seat1 = dealer.seat_player(bob)
732
+
733
+ # Hand lifecycle
734
+ dealer.start_hand() # post blinds, deal hole cards
735
+ dealer.advance_street() # deal flop / turn / river
736
+ winnings = dealer.end_hand() # showdown, chip transfer
737
+
738
+ # Betting actions (seat is the acting seat number)
739
+ dealer.bet(seat, amount)
740
+ dealer.call(seat)
741
+ dealer.check(seat)
742
+ dealer.raise_to(seat, amount)
743
+ dealer.all_in(seat)
744
+ dealer.fold(seat)
745
+
746
+ # State queries
747
+ dealer.ready() # -> Seatbit (seats with players ready to play)
748
+ dealer.next_to_act() # -> int | None (seat that must act next)
749
+ dealer.pot() # -> int (current pot total)
750
+ dealer.chips_at(seat) # -> int (chip count at a seat, 0 if empty)
751
+ dealer.event_log() # -> TableLog
752
+ ```
753
+
754
+ ### Casino example
755
+
756
+ ```python
757
+ from pkpy import Dealer, ForcedBets, Player, Winnings
758
+
759
+ # Set up a heads-up table: 50/100 blinds
760
+ dealer = Dealer(ForcedBets(50, 100))
761
+
762
+ alice = Player("Alice", 1000)
763
+ bob = Player("Bob", 1000)
764
+
765
+ s_alice = dealer.seat_player(alice)
766
+ s_bob = dealer.seat_player(bob)
767
+
768
+ # Start the hand — posts blinds, deals hole cards
769
+ dealer.start_hand()
770
+
771
+ print(f"Pot after blinds: {dealer.pot()}") # -> 0 (blinds not in pot yet)
772
+ print(f"Next to act: {dealer.next_to_act()}") # -> seat of first actor
773
+
774
+ # Simple action: big blind checks, small blind raises, BB calls
775
+ acting = dealer.next_to_act()
776
+ dealer.call(acting) # SB calls
777
+ acting = dealer.next_to_act()
778
+ dealer.check(acting) # BB checks
779
+
780
+ # Deal the flop, turn, river
781
+ dealer.advance_street() # flop
782
+ dealer.advance_street() # turn
783
+ dealer.advance_street() # river
784
+
785
+ # Showdown
786
+ winnings = dealer.end_hand()
787
+ winner = winnings.first()
788
+ print(f"Pot won: {winner.equity.chips}")
789
+ print(f"Winning seat(s): {winner.equity.seats.as_u16()}")
790
+
791
+ # Inspect the action log
792
+ for action in dealer.event_log().entries():
793
+ print(f" seat {action.seat()}: {action.kind()} {action.amount() or ''}")
794
+ ```
795
+
796
+ ---
797
+
798
+ ## Complete Example
799
+
800
+ ```python
801
+ from pkpy import HoleCards, Board, Game, Outs
802
+
803
+ # Recreate the famous Negreanu vs Hansen hand:
804
+ # Daniel holds 6♠ 6♥, Gus holds 5♦ 5♣
805
+ # Flop: 9♣ 6♦ 5♥ — Daniel flops top set, Gus flops bottom set
806
+ # Turn: 5♠ — Gus rivers quads. What are the outs for each player?
807
+
808
+ hc = HoleCards.parse("6s 6h 5d 5c")
809
+ board = Board.parse("9c 6d 5h 5s")
810
+ game = Game(hc, board)
811
+
812
+ outs = Outs.from_case_evals(game.turn_case_evals())
813
+
814
+ print(f"Player 1 (Daniel, 6♠6♥) outs: {outs.len_for_player(1)}")
815
+ print(f"Player 2 (Gus, 5♦5♣) outs: {outs.len_for_player(2)}")
816
+ print(f"Leading player: {outs.longest_player()}")
817
+ ```
818
+
819
+ ---
820
+
821
+ ## Development Setup
822
+
823
+ **Prerequisites:** Rust toolchain (`rustup`), Python 3.8+
824
+
825
+ ```bash
826
+ # Clone and enter the project
827
+ git clone <repo-url> pkpy
828
+ cd pkpy
829
+
830
+ # Create a virtual environment
831
+ python3 -m venv .venv
832
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
833
+
834
+ # Install build and test tools
835
+ pip install maturin pytest
836
+
837
+ # Compile the Rust extension and install it into the venv
838
+ python3 -m maturin develop
839
+
840
+ # Run tests
841
+ pytest
842
+ ```
843
+
844
+ After changing `src/lib.rs`, re-run `python3 -m maturin develop` to recompile. Only the Rust source is recompiled on subsequent runs — Cargo's incremental compilation keeps this fast.
845
+
846
+ ### Building a Release Wheel
847
+
848
+ ```bash
849
+ python3 -m maturin build --release
850
+ # Wheel lands in target/wheels/pkpy-*.whl
851
+ pip install target/wheels/pkpy-*.whl
852
+ ```
853
+
854
+ For distribution, maturin can also publish directly to PyPI:
855
+
856
+ ```bash
857
+ python3 -m maturin publish
858
+ ```
859
+
860
+ ---
861
+
862
+ ## Relationship to pkcore
863
+
864
+ This project wraps pkcore as a versioned crates.io dependency. The wrapper exposes the
865
+ analysis-focused surface most useful from Python: card/deck primitives, hand evaluation, outs
866
+ calculation, GTO range analysis, heads-up equity, binary card maps, Pluribus log parsing, and
867
+ casino table simulation. Lower-level types (SQLite storage) are not exposed.
868
+
869
+ ---
870
+
871
+ ## License
872
+
873
+ GPL-3.0-or-later, matching pkcore.
874
+