# Poker solver suit-frequency audit

This bundle supports the GTO Gecko article **“Why Can a Poker Solver Play the Same Hand Differently by Suit?”** It freezes a small, synthetic heads-up Hold’em game so that a suit split can be tested rather than explained from appearance alone.

## What the experiment tests

The engine receives a 100-chip pot, 100 chips behind, no rake, and exact physical-combination ranges. Out of position can only check the flop. In position can check or bet 75 chips; facing the bet, out of position can fold or call. There is no later betting, but the engine enumerates legal turn and river cards to showdown.

Five scenarios are each solved at three exploitability targets: 0.1, 0.01, and 0.001 chips.

1. A hearts/spades-symmetric board and two symmetric weighted ranges.
2. The same symmetric board with one exact IP combination reduced from weight 1.0 to 0.2.
3. A global hearts↔spades relabel of that weighted-range asymmetry.
4. A `Q♣ 7♥ 2♦` board where `A♥5♥` has 45 runner-runner flush-completing unordered runouts and `A♠5♠` has none.
5. A global hearts↔spades relabel of that board-driven asymmetry.

The global control applies one bijection to the board, both full weighted ranges, focal hands, future cards, and output labels. Changing only a hand while the board stays fixed is deliberately **not** called isomorphism.

## Files

- `experiment.json`: complete frozen engine configuration and all relevant combo-level output for 15 runs.
- `suit-audit.csv`: article-facing results derived from the frozen experiment.
- `suit-audit.svg`: deterministic four-test diagram generated from the same data.
- `generate.mjs`: Node.js parser, runout enumerator, mapping check, CSV/SVG generator, and manifest writer.
- `verify.py`: independent Python standard-library verifier; it does not import JavaScript or Rust.
- `solver-harness/`: the exact Rust harness, lockfile, and AGPL license used to run the pinned engine.
- `MANIFEST.sha256`: SHA-256 hashes for every release artifact except the manifest itself.

## Reproduce the published transformations

From the website repository root:

```text
node scripts/generate-poker-solver-suit-frequencies.mjs
node --test tests/poker-solver-suit-frequencies-data.test.mjs tests/poker-solver-suit-frequencies-article.test.mjs
python public/data/poker-solver-suit-frequencies/verify.py
python -O public/data/poker-solver-suit-frequencies/verify.py
```

The generator uses only Node.js built-ins unless the explicit authoring-only `--refresh-hero` flag is passed. Ordinary builds never rerun the solver or re-encode the reviewed WebP hero.

## Re-run the external solver

The retained run used `b-inary/postflop-solver` at commit `9d1509fe5077d019825f833eed04b16d342dfda1`, Rust `1.85.0`, 32-bit engine storage, sequential execution (`default-features = false`), and the checked-in Cargo lockfile. The engine and harness are AGPL-3.0-or-later. The repository states that the open-source project has been suspended since 2023; the commit is pinned for reproducibility, not presented as current product support.

Create this sibling layout outside the website tree:

```text
reproduction/
  open-source/postflop-solver/
  solver-harness/
  results/
```

Clone the engine into `open-source/postflop-solver`, check out the exact commit, copy the published `solver-harness` directory beside `open-source`, then run from `solver-harness`:

```text
cargo run --release -- ../results
```

The harness refuses a different or dirty engine checkout. Compare the new `raw-results.json` with `experiment.json` while excluding `generated_unix_seconds`, `runtime_ms`, and environment-specific timing. Floating-point results can vary across toolchains or hardware; the article uses the retained run and its declared exploitability values, not an undocumented universal tolerance.

## Headline result and its limit

At the tight checkpoint on `Q♣ 7♥ 2♦`, the retained solution bets `A♥5♥` 71.9585% and `A♠5♠` 49.8732%, a 22.0853-percentage-point split. Their bet-minus-check EVs are only +0.002781 and −0.000121 chips in the 100-chip-pot model. The robust conclusion is not that a learner should copy either frequency. It is that the states have different future-card relationships, while the selected actions are almost indifferent at the declared solve precision.

The mapped controls reproduce every retained strategy, action EV, mixed EV, weighted legal-matchup mass, and exploitability value exactly after labels are swapped. That validates this frozen harness/output mapping; it does not certify every solver, every tree, or any GTO Gecko production solution.

## Independent checks and limits

The Python verifier checks legal cards, range weights, three convergence levels, the complete H↔S mapping for both paired scenarios, the symmetric control, the deliberately broken controls, all 1,081 unordered two-card runouts for each focal hand, CSV values, SVG accessibility/safety, and the manifest. Passing proves internal consistency of the published bundle.

This is a constructed chip-EV game, not an observed hand, population sample, product benchmark, or strategy recommendation. Its narrow ranges and restricted bet tree were chosen to make the diagnostic inspectable. Exploitability is a whole-strategy measure; it is not a per-combo frequency error bar. A finite solve is approximate, and matching values do not always force one unique equilibrium frequency table.

## Primary sources

- Kevin Waugh, *A Fast and Optimal Hand Isomorphism Algorithm*: <https://www.cs.cmu.edu/~waugh/publications/isomorphism13.pdf>
- Zinkevich et al., *Regret Minimization in Games with Incomplete Information*: <https://proceedings.neurips.cc/paper_files/paper/2007/file/08d98638c6fcd194a4b1e6992063e944-Paper.pdf>
- PioSOLVER UPI commands and suit-display mapping: <https://piosolver.com/docs/upi/commands/>
- PioSOLVER concepts, range/reach, current-solution EV, and exploitability: <https://piosolver.com/docs/faq/gto_concept/>
- Tested open-source engine: <https://github.com/b-inary/postflop-solver/tree/9d1509fe5077d019825f833eed04b16d342dfda1>
