Add PKO bounty-adjusted shove/call ranges - #5
Merged
Conversation
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 addsBountyEquity, an overlay onPushFoldRange— it never modifies the base model, andbountyBB == 0is a proven exact no-op.Branched off
maindirectly (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.mdfor 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: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:
This codebase has no equity model anywhere (
ChenScoreranks hand strength, it doesn't compute win probability), so rather than applying this multiplier to a raw equity number,BountyEquityapplies its reciprocal toPushFoldRange's existing "percentage of hands to shove" — the quantity that actually flows through this codebase's decision pipeline (PushFoldRange.scoreThreshold(forPercentage:), reused unchanged):potis approximated as2 × effectiveStackBB(hero's shove + a covering call, ignoring blinds/antes) — a standard push/fold simplification, and not a new missing input:PushFoldRangeitself 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:heroCoversVillain: false⇒ exact no-op), not just documented. If hero doesn't cover villain, they can't eliminate them, so no bounty is collectible.widenedPercentageis written generically (takes any base percentage), so it'll compose withCallingRange.callPercentagethe same way once that PR lands — butCallingRangeisn't onmainyet, so this PR only wires up the shove side. Not a redesign later, just an extension.Confirming bounty=0 is a no-op
decideWithZeroBountyReproducesPushFoldRangeExactlyasserts, across every position/stack/hand combination tested, thatBountyEquity.decide(..., bountyBB: 0, ...)matchesPushFoldRange.decide(...)exactly on action, hand score, threshold, and percentage. Also true structurally:thresholdMultipliershort-circuits to1wheneverbountyBB == 0(or hero doesn't cover villain), andwidenedPercentagereturns the base percentage unchanged whenever the multiplier is1.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 toai-docs/BOUNTY.mdand swaps to an explicit "not collectible here" message when hero doesn't cover villain.Testing
swift test: 113/113 passing (94 baseline onmain+ 19 new).Not merging — flagging for review on the math/honesty per instructions.