Skip to content

Video Poker: saturate the payout so a big win cannot overflow the bank - #240

Merged
mishamyte merged 1 commit into
devfrom
videopoker-payout-overflow
Jul 23, 2026
Merged

Video Poker: saturate the payout so a big win cannot overflow the bank#240
mishamyte merged 1 commit into
devfrom
videopoker-payout-overflow

Conversation

@mishamyte

Copy link
Copy Markdown
Collaborator

Closes #239

score += bet * paytable[] was computed entirely in int, so both the payout and the resulting bank total could overflow. At the bet reported in #239 that corrupted the score, and the score <= 0 check immediately below then dropped the player on the "You have run out of money!" screen - a jackpot reported as a bankruptcy. Depending on the bet it could instead wrap positive and silently pay out the wrong amount, with no game-over at all.

The new bank total is now worked out in 64 bits and saturated:

int hand_rank = recognize(poker_player);
if(hand_rank != 9) {
    int64_t total = (int64_t)poker_player->score +
                    (int64_t)poker_player->bet * paytable[hand_rank];
    poker_player->score = total > MAX_BANK ? MAX_BANK : (int)total;
}

This is the only place the score grows (the other three writes are the two = 1000 resets and the pledge, which subtracts), so the bank - and highscore, which just copies it - can no longer overflow.

Why MAX_BANK and not INT_MAX

An earlier draft saturated at INT_MAX. Review caught that this makes a pre-existing overflow newly reachable: with the bank pinned at exactly INT_MAX, hold Up (bet = score/2) then tap Up (the doubling guard passes with equality, bet = INT_MAX-1) then press Right, and the bet += 10 step overflows. Before the fix the bank could never sit at INT_MAX - it wrapped negative instead - so that window was unreachable.

Capping at roughly half of INT_MAX keeps the whole bet domain away from the type limit rather than requiring a per-site overflow guard, and it composes with the existing invariant: clamp_bet() already binds bet to score, so bounding score bounds bet for free. It also avoids editing the betting logic that just shipped in 1.4 (#163). Every arithmetic site was enumerated against the new ceiling; the tightest (bet += 10) peaks at 1,000,000,009, leaving over INT_MAX/2 of slack.

The cap is disclosed in the changelog rather than being silent policy, and it only ever bites on the way up - a capped win can never look like a loss.

Also in this change

  • Hoist the doubled recognize() call into hand_rank. It is idempotent (it only rebuilds its own shand scratch buffer from hand), so this is behaviour-preserving, and it saves a full selection sort plus up to seven predicate checks per paying hand.
  • fap_version 1.4 -> 1.5 with a CHANGELOG entry covering both failure symptoms and the new ceiling.

Verification

Builds clean under ufbt (API 88.0), ufbt format applied. Reviewed by the code-review, silent-failure and comment agents plus a four-angle simplify pass; the silent-failure pass fuzzed 20,000 modelled sessions across banks from 1 to INT_MAX and saw zero wins ending as game-over, zero negative banks and zero invalid bets.

Two pre-existing issues were found during review and deliberately left out of scope: the draw callback still calls recognize() twice per frame, and playcard() has an out-of-bounds hold[5] stack access.

🤖 Generated with Claude Code

closes #239)

score += bet * paytable[] was computed entirely in int, so both the
payout and the resulting bank could overflow. At the reported bet that
corrupted the score, and the score <= 0 check right below then dropped
the player on the "You have run out of money!" screen - a jackpot
reported as a bankruptcy. Depending on the bet it could instead wrap
positive and silently pay out the wrong amount.

The new bank total is now worked out in 64 bits and saturated. This is
the only place the score grows, so the bank - and highscore, which just
copies it - can no longer overflow.

The ceiling is MAX_BANK rather than INT_MAX on purpose: parking the bank
on INT_MAX would have made a pre-existing overflow reachable, since
half-bank then double leaves the bet at INT_MAX-1 and the +10 step then
overflows. Capping at roughly half of INT_MAX keeps the whole bet domain
away from the type limit instead of needing a per-site guard, and since
clamp_bet already binds bet to score, bounding score covers bet too.

Also hoist the doubled recognize() call into hand_rank; it is idempotent,
so this is behaviour-preserving.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added category/games App category: games pack/catalog App in apps_source_code (catalog) labels Jul 23, 2026
@mishamyte mishamyte self-assigned this Jul 23, 2026
@mishamyte mishamyte added this to the unlshd-090 milestone Jul 23, 2026
@mishamyte
mishamyte merged commit 813ed13 into dev Jul 23, 2026
2 checks passed
Repository owner locked as resolved and limited conversation to collaborators Jul 25, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

category/games App category: games pack/catalog App in apps_source_code (catalog)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Video Poker: winning payout overflows int at very large bets and triggers a false game-over

1 participant