poker-equity
An equity engine for poker. Give it a game, some hands, a board and some dead cards, and it tells you what share of the pot each player wins.
Beta, and written almost entirely by AI. This is 0.1 and should be read as one. The API is not settled: names, signatures and the notation may all change before 1.0, and a few corners of the behaviour are still being decided. Pin an exact version rather than a range. The hand rankings are cross-checked against pokerkit for the games whose hand spaces are small enough to check — see Correctness — but nothing here has been used in anger yet.
Install
Rust.
cargo add poker-equity
Then a whole program is:
use poker_equity::{odds::{equity, EquityRequest, Target}, variants::Holdem};
fn main() -> Result<(), poker_equity::error::PokerError> {
let request = EquityRequest::from_text(Holdem, &["AhKh", "QsQd"], "", "")?;
let result = equity(&request, Target::Samples(500_000))?;
println!("{:.2}%", result.equities()[0].percent()); // about 46.2%
Ok(())
}
Python. Any CPython from 3.10 up:
pip install poker-equity
Wheels are built for Linux (x86_64 and aarch64), macOS (Intel and Apple Silicon) and Windows (x64). Anywhere else pip builds from source, which needs a Rust toolchain.
import poker_equity as pe
r = pe.chunk_from_text("holdem", ["AhKh", "QsQd"], samples=500_000)
for seat in r["players"]:
print(f"{seat['equity']:.4f} +/- {seat['std_error']:.4f}")
More of it in Python.
From a clone. For Rust, point the dependency at the clone with
path = "../poker-equity". For Python, pip install . builds and installs the
module with the same settings the published wheels use.
Games
Showdowns a second, per thread: dealing, evaluating and splitting the pot,
end to end. Measured heads-up on one core of an AMD Ryzen 7 9800X3D. Run
cargo run --release --bin benchmark for your own machine.
| Key | Game | Per thread |
|---|---|---|
holdem |
Hold'em | 6.86 M/s |
short_deck |
Short Deck Hold'em — 36 cards, a flush beats a full house | 7.14 M/s |
omaha |
Omaha | 2.86 M/s |
omaha_five |
5-Card Omaha | 2.15 M/s |
omaha_six |
6-Card Omaha | 1.67 M/s |
omaha_hi_lo |
Omaha Hi/Lo, eight or better | 1.77 M/s |
omaha_five_hi_lo |
5-Card Omaha Hi/Lo | 1.29 M/s |
courchevel |
Courchevel | 2.26 M/s |
courchevel_hi_lo |
Courchevel Hi/Lo | 1.33 M/s |
stud |
Seven-Card Stud | 4.52 M/s |
stud_hi_lo |
Seven-Card Stud Hi/Lo | 4.12 M/s |
razz |
Razz | 4.84 M/s |
deuce_seven |
2-7 Lowball, single draw | 10.55 M/s |
five_card_draw |
5-Card Draw, single draw | 10.83 M/s |
badugi |
Badugi, single draw | 5.35 M/s |
Threads share nothing while they sample, so multiply by however many you give it. More seats cost more: six-handed hold'em runs at 3.51 M/s a thread, and six-handed Omaha at 1.31 M/s.
A run spreads over four threads unless told otherwise, which is polite rather
than greedy: several calculators may be open at once and one must not starve
the others. EquityRequest::with_threads takes as many as you want to give
it, and nothing needs passing to get the default.
The lowball draw games model one draw. Equity in triple draw is undefined without a drawing strategy — a made eight-low and a four-card draw are not comparable until you say how the draw resolves — so one draw is modelled and said so, rather than a number published from an invented model. 5-card draw has a single draw by its own rules, so there the one draw is the whole game.
Examples
Two games, to show that the shape of the question does not change with the game being asked about.
Hold'em: ace-king suited against a pair of queens
use poker_equity::{odds::{equity, EquityRequest, Target}, variants::Holdem};
let hero = "AhKh"; // one field per seat
let villain = "QsQd";
let board = ""; // nothing dealt yet
let dead = ""; // no cards out of play
let request = EquityRequest::from_text(Holdem, &[hero, villain], board, dead)?;
let result = equity(&request, Target::Samples(500_000))?;
for (seat, player) in result.equities().iter().enumerate() {
println!("seat {}: {:.2}% ± {:.2}", seat, player.percent(), player.margin_percent());
}
// seat 0: 46.20% ± 0.13
// seat 1: 53.80% ± 0.13
Seats come back in the order they were passed in, so seat 0 is hero.
2-7 draw: a pat nine against a one-card draw
Change the variant, and the fields mean what that game means by them:
use poker_equity::{odds::{equity, EquityRequest, Target}, variants::DeuceSeven};
let villain = "9s7d5c4h2s"; // five cards: a made nine-low, standing pat
let hero = "8h6d4s3c"; // four cards, so one is still to come
let board = ""; // a draw game never has a board
let dead = "Kc"; // the king hero threw away
let request = EquityRequest::from_text(DeuceSeven, &[villain, hero], board, dead)?;
let result = equity(&request, Target::Samples(1_000_000))?;
// seat 0 (villain): 78.57% ± 0.00
// seat 1 (hero): 21.43% ± 0.00
Three things differ from the hold'em example, and every one of them is the game rather than the API.
A draw or stud field may be short. Five cards is a hand standing pat; four
is a hand drawing one, and the card still to come is dealt. A community game
may not do this. Hold'em deals both hole cards at once, so a field holding
one card is a miscount rather than a hand in progress, and is an error — write
A* there if you mean one known card and one unknown.
Discards are dead cards. A card thrown away is out of the deck but was seen, so it belongs in the dead field rather than nowhere.
There is no error bar, though a million deals were asked for. Only
forty-two cards can arrive, so the whole question is forty-two deals wide and
was walked rather than sampled: result.samples comes back as 42. Nine of
those cards — the deuces, fives and sevens still in the deck — give hero a
better low than a nine, and 9/42 is 21.43% exactly.
Hands may be only partly known — AKs, 2 c, A** are all valid — and the
answer comes with an error bar, or none at all when the spot was small enough
to walk rather than sample. To follow a long run while it goes, see
Watching a long run. For how the hand rankings are
checked, see Correctness.
Writing hands
A card is a rank and a suit: uppercase rank, lowercase suit on the way out, but anything on the way in. Beyond that, four patterns:
| Written | Means |
|---|---|
Ah |
the ace of hearts |
A |
any ace |
c |
any club |
* |
any card |
A rank binds to a suit immediately after it, and nothing else binds. So
2c is one card and 2 c is two — a deuce and a club. Both are valid and
they differ by a whole card, so the wrong reading gives a plausible wrong
answer rather than an error, which is why the rule is worth knowing.
* never binds, which is what makes AA** unambiguously four cards. If that
makes the count wrong, the error says so.
"AhKh" // two named cards
"A Kh" // any ace, and the king of hearts
"2 c" // a deuce and a club — two cards
"2c" // the deuce of clubs — one card
"AA**" // two aces and two unknowns (Omaha)
What a wildcard costs
A card you name exactly gets dealt. A card you only describe -- A, c, *
-- has to be found: the engine deals, checks the cards fit every seat, and
deals again if they do not. The answer is the same either way; only the time changes.
It is worst in stud, where each player holds seven private cards -- four-handed stud claims twenty-eight of the fifty-two before anything is scored.
| Spot | Deals tried per simulation |
|---|---|
AA vs KK, hold'em |
1 |
AA** vs KK**, Omaha |
1.2 |
A23 vs 456, stud |
2.9 |
two AA** at a six-handed Omaha table |
6.5 |
A23 any number of ways, razz |
1 |
Where searching stops working -- below one kept deal in twenty -- the
engine deals each seat from what is left instead, and weights the deal to undo
the bias that introduces. Same answer, sooner, and spots that were refused
outright now answer. Read effective_samples there rather than acceptance:
it is what the error bar is divided by, and is_weighted says which applies.
Such an average is consistent rather than exactly unbiased, by a 1/n term
under the bar's 1/√n.
Razz needs no suits. It has no flushes and never looks at a suit, so A23
and Ah2h3h ask the same question -- and the engine pins the suits for you
when you leave them off. Razz runs at one deal per simulation however many
players are in the hand, so writing suits there buys nothing.
Everywhere else, writing the suits you actually mean is still the cheapest thing you can do. A real hand has suits; naming them is both more faithful to the spot and quicker to answer than either way of dealing it.
The underlying problem, if you want to read about it, is sampling a uniformly random bipartite matching -- one deal that satisfies every seat at once, drawn without favouring any of them.
Ranges
Where a hand holds exactly two cards, a field may be a range instead:
"AKs" // suited ace-king, four combinations
"AKo" // offsuit, twelve
"AK" // either, sixteen
"22+" // every pair
"A2s+" // A2s through AKs
"KTs+" // KTs, KJs, KQs
"JTs-76s" // the suited connectors between them
"AKs, 22" // alternatives, comma separated — ten combinations
Ranges exist only where a hand is two cards. In Omaha and stud there is no
range grammar at all, which is what makes the collision between AKs the
range and A Ks the two cards impossible there rather than merely unlikely.
Percentage ranges (top 15%) are not supported. There is no one ordering of
starting hands by strength — it changes with the game, the table size and
whose chart you trust — so naming the hands is the honest way to ask.
Boards, dead cards, and hands still being dealt
A board is how much of it you know, not which street you are on. Nought to five cards, wildcards allowed anywhere among them:
"" // nothing showing
"2c 7d 9h" // a flop
"2c 7d 9h * Ks" // flop and river known, turn not
"2c 7d 9h Ts 4c"// finished: who won?
So "what if the turn is a heart" is "2c 7d 9h h", and a finished board is a
fair question with an exact answer. Courchevel is the one game with a floor:
its first board card is face up before the betting, so a Courchevel request
showing nothing is refused — that spot is five-card Omaha, not Courchevel.
Write "*" there if the card has been dealt and you have not seen it. That
is a fair question, and it is how to ask what the turned card was worth: run
the spot with the card named, run it again with *, and the gap between them
is the answer. It comes back as the five-card Omaha number, because that is
what Courchevel with an unknown first card is.
Dead cards must be exact, because "a club is dead" does not say which club and every reading changes the answer. They must also be cards the game actually deals — a deuce is not dead in short deck, it is a mistake about the game, and is refused in the dead field exactly as it would be in a hand.
How many cards a field may name is three rules, not one, because the games deal differently:
| A short field | Why | |
|---|---|---|
| Community | refused | both hole cards arrive at once |
| Stud | allowed, equally for every seat | the whole table is on the same street |
| Draw | allowed, seat by seat | a short field is the draw |
Stud is the one worth care. Third street is three cards for everybody, so
A23 against 2345 is not a table caught mid-deal, it is a miscount, and it
is refused. A draw game is the opposite: five against four is a pat hand
against a one-card draw, and saying so is the point of the notation.
In stud and the draw games, cards arrive over time, so a field says what a player holds now and whatever is missing is still to come:
EquityRequest::from_text(Stud, &["Ah2c3d", "QsQdJs"], "", "")?;
// three known, four still to be dealt
EquityRequest::from_text(DeuceSeven, &["7h5c4d3s", "9h8c6d5h2c"], "", "Kd")?;
// Hero draws one; the king he threw is dead. Villain stands pat.
A community game wants every hole card, because they are all dealt at once:
there is no moment at which a hold'em player holds one card. So a short field
there is a miscount and is rejected. To say "an ace and something I cannot
see", write the unknown card as a wildcard — A*, not A.
Getting an answer
equity takes a target, which is how you say what "good enough" means:
| Target | Runs until |
|---|---|
Target::Samples(500_000) |
half a million deals are done |
Target::StandardError(0.001) |
the answer is that precise — "make this good enough to trust" |
Target::Exact |
every possible deal has been walked, falling back to tight sampling when there are too many |
StandardError is usually the one you want. A sample count is a guess at how
long precision takes; a precision target just asks for the precision.
If a spot has fewer possible deals than you asked to sample, it is walked
instead of sampled — AhAd against KsKc on a flop has only 990 run-outs, so
asking for half a million deals gets all 990 and a std_error of exactly
zero. You are never given more work than you asked for: a spot with two
million possible deals is sampled, not walked, when you asked for five
hundred thousand.
StandardError and Exact put no cap on the work, so they walk whatever can
be walked — an exact answer beats any error bar.
Watching a long run
A run of millions of deals takes long enough that you will want to repaint a table while it goes, and to notice if the user cancelled. Pass a function to be called after each batch:
use poker_equity::odds::equity_with_progress;
let result = equity_with_progress(&request, Target::Samples(5_000_000), |progress| {
println!("{} deals: {:.2}% (± {:.2}), keeping {:.0}% of deals",
progress.samples,
progress.equities[0].percent(),
progress.equities[0].margin_percent(),
progress.acceptance * 100.0);
})?;
|progress| { ... } is Rust's syntax for a function written inline — the
names between the bars are its arguments, and the braces are its body. So
that one takes a Progress and prints from it. A batch is a few milliseconds,
which is a good rate to repaint at and a fine granularity to cancel at.
progress.acceptance is the share of attempted deals that could be used. It
is 1.0 unless hands are competing for the same cards — several seats all
wanting a five when only two are left — and a low figure is the difference
between an answer that is slow and one that looks stuck.
progress.effective_samples is what the run is worth so far — equal to
progress.samples unless the deals were weighted, and what the error bar is
divided by.
Driving the loop yourself
If you want to own the loop — because cancelling, or threading, or merging results across machines is your business rather than the library's — the layer underneath is a single batch that returns sums:
use poker_equity::odds::{run_chunk, ChunkResult};
let mut total = ChunkResult::empty(2);
for seed in 0..10 {
total.merge(&run_chunk(&request, 50_000, seed)?);
if user_cancelled() { break; }
}
// 500,000 deals, or fewer if the user stopped it
run_chunk is pure and stateless: no callbacks, no cancellation token, no
shared state. Everything in ChunkResult is a sum, so batches merge by
addition, and the sums of squares are carried too — which is what makes the
error bar free rather than something to compute separately. The deals' weights
are summed the same way, so effective_samples merges as cleanly as the rest.
What comes back
pub struct PlayerEquity {
pub equity: f64, // share of the pot — the number that matters
pub win: f64, // taken outright
pub tie: f64, // shared
pub low_equity: f64, // the low half alone, in split games
pub scoop: f64, // both halves
pub high_win: f64, // the high half, outright
pub high_tie: f64, // the high half, shared
pub low_win: f64, // the low half, outright
pub low_tie: f64, // the low half, shared
pub std_error: f64, // zero when the answer was enumerated
}
Equity leads because it is the answer — it is what the money does over time.
Wins and ties are colour. In a split game the halves are what equity cannot
say: taking every low and never the high is half the pot, and so is splitting
both halves every time. Outside split games the high half is the whole pot, so
high_win and high_tie are win and tie, and the low figures are zero. percent() and margin_percent() give the two
numbers a table usually shows: the equity, and the half-width of a 95%
interval around it.
The mask API
Text is a convenience. Underneath, a card is a u8 and a set of cards is a
u64 with one bit per card, and that is what the engine speaks. A caller that
already holds masks can skip the parser entirely:
use poker_equity::{cards::{Card, CardSet, Rank, Suit}, notation::HandSpec,
odds::EquityRequest, variants::Holdem};
assert_eq!(Card::new(Suit::Heart, Rank::Ace).index(), 50); // rank * 4 + suit
let queen_of_spades = CardSet::from_cards(&["Qs".parse::<Card>()?]);
let request = EquityRequest::from_masks(
Holdem,
&[
// any ace and any club
HandSpec::from_slots(&[CardSet::of_rank(Rank::Ace), CardSet::of_suit(Suit::Club)]),
// the queen of spades and anything
HandSpec::from_slots(&[queen_of_spades, CardSet::FULL_DECK]),
],
&[], // no board yet
CardSet::EMPTY, // nothing dead
)?;
A named card is a mask with one bit; "any ace" is a mask with four; "any card" is all fifty-two. The sampler never asks which kind it was handed, which is why a wildcard costs nothing on the fast path.
The layout is worth knowing if you are building masks yourself: index is
rank * 4 + suit, ranks 0..13 running 23456789TJQKA and suits 0..4
running cdhs. So a rank is four adjacent bits and a suit is every fourth bit:
of_rank(Ace) = 0b1111 << 48
of_suit(Club) = 0x1_1111_1111_1111
FULL_DECK = (1 << 52) - 1
Both APIs are the same request — from_text parses into exactly what
from_masks takes, once, before any card is dealt. A test runs both from the
same seed and compares the raw sums.
Speed
The per-game figures are in Games above.
Underneath, a hand is scored by two array reads:
| Hands/s | |
|---|---|
score — reads the lookup tables |
99,726,346 |
evaluate_hand — names the hand |
1,956,053 |
Both are kept. score is what the sampling loop compares; evaluate_hand is
what tells a player they have two pair, and is the independent second
implementation the tables are checked against.
The tables total 1,104 KB — four rankings sharing one perfect hash — and are
generated by build.rs into OUT_DIR. Nothing generated is committed.
Correctness
Hand ranking is checked three ways. None of it stands in for use: the library is new and has not yet been run in earnest by anyone.
Against a reference evaluator, exhaustively. Every lookup table is swept
against the readable evaluator beside it — all 2,598,960 five-card hands per
ranking, plus seven-card hands. cargo test --release -- --ignored.
Against an outside implementation. The two above were both written here,
from one reading of the rules, so they catch a mistake in either but not a rule
misunderstood in both. validation/ checks the rankings against
pokerkit, as far as each game allows:
every five-card hand of every ranking and every four-card badugi holding are
walked exhaustively, while seven-card hands are sampled and Omaha — 4.6 ×
10¹¹ deals — cannot be walked at all. Everything checked agrees. See
validation/README.md.
Against arithmetic. Sampling is checked against exact enumeration within four standard errors, which is the only test that catches a sampler that is uniform over the wrong thing. Equities sum to one; permuting seats permutes the answer; a hand against its own mirror splits exactly; a wildcard narrowed to a single card equals naming that card.
Weighted dealing needs this most, since a wrong weight moves the answer without moving anything a reader would notice. Where a spot is too large to walk it is checked by symmetry: seats asking for the same thing must be given the same equity.
Python
Four ways in — two that parse the notation, two that take masks. Building the module is under Install.
| Call | Does |
|---|---|
chunk_from_text(variant, hands, board="", dead="", samples=100_000, seed=0, threads=0) |
samples that many deals |
exact_from_text(variant, hands, board="", dead="") |
walks every deal, or returns None when there are too many |
chunk(variant, hands, board, dead, samples, seed=0, threads=0) |
the same by mask |
exact(variant, hands, board, dead) |
likewise |
import poker_equity as pe
r = pe.exact_from_text("holdem", ["AhAd", "KsKc"], "2c 7d 9h")
print(r["players"][0]["equity"]) # 0.916161...
print(r["exact"], r["samples"]) # True 990
# or masks, skipping the parser: hands[seat][alternative][slot]
hands = [[[1 << pe.card_index("Ah"), 1 << pe.card_index("Kh")]],
[[1 << pe.card_index("Qs"), 1 << pe.card_index("Qd")]]]
r = pe.chunk("holdem", hands, [], 0, 200_000, seed=3)
Every seat in r["players"] carries equity, win, tie, low_equity,
scoop, high_win, high_tie, low_win, low_tie and std_error, as in
What comes back. The counts behind the wins, ties and
halves come back as raw sums too — win_count, high_win_count and so on. Beside them sit the run's own totals — samples,
weight_sum, share_sum, acceptance, effective_samples, weighted —
which are sums rather than averages, so calling again with another seed and
adding them is how you sample further. There is no precision target here as
there is in Rust: ask for a count.
abi3-py310, so one wheel per platform covers CPython 3.10 upward. The GIL is
released around sampling, so a long batch does not block the interpreter.
pe.variants() lists every game with how it deals, so a caller needs no table
of its own, and pe.parse_hand_field hands back masks so another parser can
be checked against this one.
Averaging the sums yourself: divide by weight_sum, not samples. The two
are equal for almost every request, so the wrong one costs nothing until a
spot is weighted, then returns equities hundreds of times too small.
r = pe.chunk_from_text("stud", ["A23", "456", "789", "TJQ"], samples=200_000)
equity = [total / r["weight_sum"] for total in r["share_sum"]] # not / samples
spread = (r["share_square_sum"][0]
- 2 * equity[0] * r["square_weight_share_sum"][0]
+ equity[0] ** 2 * r["weight_square_sum"])
std_error = max(spread, 0.0) ** 0.5 / r["weight_sum"]
r["weighted"] and r["effective_samples"] are that run's own figures;
tests/python/test_binding.py checks this rebuild against the engine's.
Building
cargo test # 234 tests, about ten seconds
cargo test --release -- --ignored # the exhaustive sweeps
cargo run --release --bin benchmark # speed, per game
cargo bench # the paths a refactor could slow
benches/engine.rs runs locally as ordinary criterion timings. CI runs the
same definitions under callgrind, where what is counted is instructions rather
than seconds, so a pull request's figures can be compared with the branch it
came from without either number depending on the machine that produced it.
The tests spread across every core, which is not always what you want on the
machine you are also using. scripts/quiet runs the same commands pinned to
four cores at the lowest priority, and costs about fifteen percent of wall
time:
scripts/quiet test
QUIET_CORES=2 scripts/quiet run --release --bin benchmark
Tests build at opt-level = 2, because the equity tests run a hundred
thousand Monte Carlo deals apiece and that is minutes unoptimised against
seconds optimised.
build.rs reports what it generated — how full the perfect hash is, how many
distinct values each kernel has, what the tables weigh — when asked:
POKER_EQUITY_BUILD_STATS=1 cargo build
tests/fixtures/notation.tsv is the grammar's source of truth — a plain table
of input and expected output, with a row for every rule above. It is a flat
file rather than Rust so that another implementation of the notation can be
checked against the same rows.
Releasing
A release goes to PyPI and crates.io together, from a version tag, and only from a tag: pushing to main tests and benchmarks but never publishes.
-
Bump
versioninCargo.toml— the Python package reads it from there — and merge that to main. -
Tag the merge and push the tag:
git tag vX.Y.Z && git push origin vX.Y.Z
-
.github/workflows/release.ymlchecks the tag matchesCargo.tomland sits on main, builds a wheel for every platform, installs each one and runstests/python/test_binding.pyagainst it, and packs the crate. -
Nothing is published until the
releaseenvironment is approved in the Actions tab. Then both registries are uploaded to, each by trusted publishing, so there is no token stored anywhere.
A version cannot be published twice to either registry — only yanked — so the
tag is the point of no return. To rehearse, run the workflow by hand from the
Actions tab: nowhere builds and checks every wheel, and testpypi goes on to
publish them to TestPyPI.
Metadata
Release files for poker-equity 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| poker_equity-0.1.1.tar.gz | 206.3 kB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| poker_equity-0.1.1-cp310-abi3-win_amd64.whl | CPython 3.10 | abi3 | Windows x86-64 | Details |
| poker_equity-0.1.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | CPython 3.10 | abi3 | Linux glibc 2.17+ x86-64 | Details |
| poker_equity-0.1.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | CPython 3.10 | abi3 | Linux glibc 2.17+ ARM64 | Details |
| poker_equity-0.1.1-cp310-abi3-macosx_11_0_arm64.whl | CPython 3.10 | abi3 | macOS 11.0+ ARM64 | Details |
| poker_equity-0.1.1-cp310-abi3-macosx_10_12_x86_64.whl | CPython 3.10 | abi3 | macOS 10.12+ x86-64 | Details |
Total release size: 6.0 MB
Release files / poker_equity-0.1.1.tar.gz
| Download URL | poker_equity-0.1.1.tar.gz |
|---|---|
| Size | 206.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6684ab0f5cfcadb3ec17ed0281e95501f4145b11b7705f1c2ad91ee3e3acd545
|
|
BLAKE2b-256 checksum How to use checksums |
2d8c279d80a2a7297eb72eea0cdeb21c0509dbb76ec6327c1f4176282862633e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.
Transparency logRelease files / poker_equity-0.1.1-cp310-abi3-win_amd64.whl
| Download URL | poker_equity-0.1.1-cp310-abi3-win_amd64.whl |
|---|---|
| Size | 1.0 MB |
| Tags | CPython 3.10 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
eb67123363fc48993a6b7f4a8be41a6399588f65eb26889341adde4a4f99bfab
|
|
BLAKE2b-256 checksum How to use checksums |
740e1c2d10ae815a02ea05228a3ff92adca65e4494d8bf45baa7a61b402360d7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.
Transparency logRelease files / poker_equity-0.1.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | poker_equity-0.1.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 1.2 MB |
| Tags | CPython 3.10 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
b45c93067668ef6394ad5b6be6257c65b55cd6650aa9f65ace88e4594ef45b3c
|
|
BLAKE2b-256 checksum How to use checksums |
b2b567963a375911742a5063d0c134ecfef877052cc3d4dab63b0e03d1124036
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.
Transparency logRelease files / poker_equity-0.1.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | poker_equity-0.1.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 1.2 MB |
| Tags | CPython 3.10 Linux glibc 2.17+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
a45e8bb75c5093c6972f952b4e8525ec93bcda53c867bedd911e359f891df47f
|
|
BLAKE2b-256 checksum How to use checksums |
39c3945407e78e4588de97bc2474ee74c4bb76986c6d7ff2620c46f348da09b0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.
Transparency logRelease files / poker_equity-0.1.1-cp310-abi3-macosx_11_0_arm64.whl
| Download URL | poker_equity-0.1.1-cp310-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 1.2 MB |
| Tags | CPython 3.10 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
d7f2ab690a2fa41d44774a1d32b1983c0e1d0a359ab4cd14f400f18b4c88721f
|
|
BLAKE2b-256 checksum How to use checksums |
8b2fd7f5de851c4617684f187bf5f57195a4e6f9ded912443566ff336c5a4b36
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.
Transparency logRelease files / poker_equity-0.1.1-cp310-abi3-macosx_10_12_x86_64.whl
| Download URL | poker_equity-0.1.1-cp310-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 1.2 MB |
| Tags | CPython 3.10 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
59b421cb01077bdfc45af97b3fa677371398bd87ded2234a357389e35068884f
|
|
BLAKE2b-256 checksum How to use checksums |
50ebe1db8f0fcbd48e719845b1bd557b85b2dd818bc69f073924a5edd2e43c37
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.
Transparency log