Skip to content

Add PKO bounty-adjusted shove/call ranges - #5

Merged
testtest126 merged 3 commits into
mainfrom
feature/pko-bounty-equity
Jul 21, 2026
Merged

Add PKO bounty-adjusted shove/call ranges#5
testtest126 merged 3 commits into
mainfrom
feature/pko-bounty-equity

Conversation

@testtest126

Copy link
Copy Markdown
Owner

Summary

Makes short-stack shove decisions bounty-aware for PKO tournaments. In PKO, winning a pot against a covered opponent also claims their bounty — extra equity a pure chip-EV model (PushFoldRange) has no way to see, since it only ever reasons about chips. This PR adds BountyEquity, an overlay on PushFoldRange — it never modifies the base model, and bountyBB == 0 is a proven exact no-op.

Branched off main directly (independent of #4, the calling/defense-ranges PR — no dependency, no conflict).

The formula — and exactly what it's built on

The standard treatment found across PKO strategy sources (see ai-docs/BOUNTY.md for full citations) folds a bounty into an all-in decision by adding its chip-equivalent value to the pot on the winning side of a pot-odds calculation:

requiredEquity = amountToCall / (pot + bountyValueInChips)

Primary source: GTO Wizard's "The Theory of Progressive Knockout Tournaments" — works a full numeric example (a $50 bounty, converted through a "bounty power" figure, adds 9.55bb to the pot side, dropping required equity from 43.6% to 32.4% in that spot). This PR implements the same algebraic shape with the bounty pre-converted to bb.

Dividing the bounty case by the no-bounty case gives the threshold multiplier this module actually computes:

thresholdMultiplier = pot / (pot + bounty)          // always in (0, 1]

This codebase has no equity model anywhere (ChenScore ranks hand strength, it doesn't compute win probability), so rather than applying this multiplier to a raw equity number, BountyEquity applies its reciprocal to PushFoldRange's existing "percentage of hands to shove" — the quantity that actually flows through this codebase's decision pipeline (PushFoldRange.scoreThreshold(forPercentage:), reused unchanged):

widenedPercentage = min(basePercentage × (pot + bounty) / pot, 100)

pot is approximated as 2 × effectiveStackBB (hero's shove + a covering call, ignoring blinds/antes) — a standard push/fold simplification, and not a new missing input: PushFoldRange itself never took a pot-size parameter either.

What's flagged uncertain / explicitly out of scope

Full list in ai-docs/BOUNTY.md, but the load-bearing ones:

  • Only applies when hero covers villain — enforced in code (heroCoversVillain: false ⇒ exact no-op), not just documented. If hero doesn't cover villain, they can't eliminate them, so no bounty is collectible.
  • Being covered is explicitly out of scope. Real PKO strategy is the net of two competing forces — bounty-equity widening (this PR) and an ICM-like tightening for the risk of being knocked out yourself. GTO Wizard's own material frames it this way. This PR implements only the first; a spot where neither player fully covers the other will be overstated as wider than it really is.
  • No ICM at all, same posture as every range model in this codebase.
  • Assumes the whole bounty is realized on a win — doesn't model split-bounty structures (e.g. "50% pays out immediately, 50% rolls onto the winner").
  • Ignores stack dynamics across a tournament — single-spot calculation, not a running strategy.
  • Calling side deferred. widenedPercentage is written generically (takes any base percentage), so it'll compose with CallingRange.callPercentage the same way once that PR lands — but CallingRange isn't on main yet, so this PR only wires up the shove side. Not a redesign later, just an extension.

Confirming bounty=0 is a no-op

decideWithZeroBountyReproducesPushFoldRangeExactly asserts, across every position/stack/hand combination tested, that BountyEquity.decide(..., bountyBB: 0, ...) matches PushFoldRange.decide(...) exactly on action, hand score, threshold, and percentage. Also true structurally: thresholdMultiplier short-circuits to 1 whenever bountyBB == 0 (or hero doesn't cover villain), and widenedPercentage returns the base percentage unchanged whenever the multiplier is 1.

App

PreflopRangeView's Push/Fold mode gets an optional bounty toggle, a bounty-size slider (bb), and a "you cover villain" toggle — off by default. When on, hands that only shove because of the bounty render in a third grid color (fold in the base model, push once bounty-adjusted), so the widening is visible directly on the grid, not just in the summary percentage. A persistent on-screen caveat links to ai-docs/BOUNTY.md and swaps to an explicit "not collectible here" message when hero doesn't cover villain.

Testing

  • swift test: 113/113 passing (94 baseline on main + 19 new).
  • App: built and ran clean on iOS Simulator (Xcode 27 beta); all 7 UI tests pass, including a new test exercising the full bounty flow (toggle on → slider → summary shows both percentages → caveat text → toggle "covers villain" off → caveat text switches to the not-collectible message).

Not merging — flagging for review on the math/honesty per instructions.

New BountyEquity module widens PushFoldRange's shove percentage to account
for a collectible bounty when hero covers villain — busting a covered
opponent wins their bounty on top of the pot, extra equity a pure chip-EV
model has no way to see. Overlay only: never touches PushFoldRange, and
bountyBB == 0 is a proven exact no-op.

Formula: pot / (pot + bounty) as a break-even-equity-threshold multiplier,
sourced from GTO Wizard's PKO theory writeup (a worked numeric example
converting a $50 bounty to bb and applying it to a pot-odds calculation).
Applied as a widening reciprocal to PushFoldRange's percentage rather than
to a raw equity number, since this codebase has no equity model — see
ai-docs/BOUNTY.md for the full derivation and every simplification
(pot approximated as 2x effective stack, no ICM, being-covered risk
explicitly out of scope, calling side deferred pending the CallingRange PR).

19 new tests: bounty=0 reproduces the base model exactly, monotonic
widening by bounty size and by shorter stack, clamping at 100%, no-op when
hero doesn't cover villain, and a borderline hand flipping fold->push with
a large enough bounty.
Push/Fold mode gets an optional bounty toggle, a bounty-size slider (bb),
and a "you cover villain" toggle. Off by default so the base chip-EV grid
is unchanged unless a user opts in.

When active, hands that only shove because of the bounty (fold in the base
model, push once BountyEquity is applied) render in a third grid color, so
the widening is visible directly rather than only in the summary
percentage. Base and bounty percentages are both computed empirically by
counting the actual rendered grid cells (not read off the raw formula
output) so the displayed numbers can never drift from what's colored on
screen. A persistent caveat line points at ai-docs/BOUNTY.md, and swaps to
an explicit "not collectible here" message when hero doesn't cover villain.
Resolves conflicts in PreflopRangeView.swift and PreflopRangeUITests.swift
by keeping both features: the four-way mode picker (Push/Fold, Opening,
Facing Shove, Facing Open) from the calling-ranges PR, and the PKO bounty
overlay (toggle, bounty slider, "you cover villain" toggle, bounty-only
grid color) from this branch — the overlay only ever applies in Push/Fold
mode, so the two never actually compete for the same UI state.

CellStyle gains a fourth case (.bountyOnly) alongside the existing
.primary/.secondary/.fold from the calling-ranges merge. summaryText and
the caveat line both switch on mode plus the bounty-active flag rather than
mode alone. ai-docs/RANGES.md and PreflopGrid.swift merged cleanly with no
conflicts — the two PRs touched non-overlapping sections/functions there.

swift test: 135/135 passing (116 post-#4 baseline + 19 bounty tests, none
lost). App builds clean; all 4 PreflopRangeUITests pass (base grid, bounty
overlay, facing shove, facing open).
@testtest126
testtest126 merged commit a778c20 into main Jul 21, 2026
2 checks passed
@testtest126
testtest126 deleted the feature/pko-bounty-equity branch July 21, 2026 13:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant