Skip to main content

F015 — Payout Calculation

FieldValue
IDF015
Phase2 — Betting
JiraHEDGE-136
StatusTS ✅ fixed · JS ⬜ · UMA ⬜

Business Rule

At river, the game resolves all bets:
payout = Σ(stakeAmount × oddsAtBet)   for all bets on WINNING hands, all stages
  • Winning hand only: bets on losing hands and dead hands contribute £0
  • Locked-in odds: oddsAtBet is the oddsRounded value at the moment the bet was placed, not the river odds
  • All stages included: hole, flop, and turn bets on the winner all pay out
  • Credits: credits = (initialCredits − totalBet) + payout at end of round

Worked Example (from Game Concept Document)

“If a hand has a 25% chance of winning this is first converted to European odds (4:1) then a house margin is subtracted of (say) 3% so odds become 3.88:1. If the player bets £1 on the hand and it wins they will receive £3.88 payout. If the player has four successive bets of same value statistically only one will win; thus in this case RTP = payout/bet = 3.88/4 = 0.97.”
Value
Win probability25%
Fair odds4.00
House margin3%
oddsAtBet3.88
Stake£1.00 (100p)
Payout if wins£3.88 (388p)
RTP (1 in 4 wins)3.88 / 4 = 97%
The stake (£1.00) is deducted from credits at bet placement. On win, £3.88 is added — net gain £2.88.

JavaScript Reference

JS Source: HedgeEmJavaScriptClient/odobo/src/js/hands.js
// hands.js:957 — ResolveHands()
var winAmount = 0;
for (var hand = 0; hand < NUMBER_OF_HANDS; hand++) {
    for (var bet = 0; bet < 3; bet++) {
        winAmount += GetHandOdds(hand, bet) * GetHandBet(hand, bet);
    }
}
SetWinAmount(winAmount);
GetHandOdds(hand, stage) returns the locked-in odds for that hand at that betting stage — non-zero only for the winning hand; 0 for losing and dead hands. This is why the JS loop over all hands naturally produces 0 for non-winners without an explicit winner check. GetHandBet(hand, stage) returns the bet placed on that hand at that stage (0 if no bet). Credit deduction and award (control.js):
// CC_DEDUCT_BETS — deducts total bets after final betting window
case CC_DEDUCT_BETS:
    SetCreditAmount(credits - GetBetAmount());

// CC_ADD_WIN — adds payout after resolution
case CC_ADD_WIN:
    SetCreditAmount(credits + GetWinAmount());
Note: JS deducts credits in a single batch at CC_DEDUCT_BETS (after all betting). TS deducts per-bet immediately in placeBet(). Both produce the same net credit result.

TypeScript Implementation

File: standalone_reference_client/src/engine/GameEngine.ts
resolveBets(): number {
    let totalPayout = 0;
    for (const bet of this.bets) {
        if (this.isHandWinner(bet.handIndex)) {
            totalPayout += Math.round(bet.stakeAmount * bet.oddsAtBet);
        }
    }
    this.credits += totalPayout;
    this.winAmount = totalPayout;
    return totalPayout;
}
isHandWinner(hand) reads statusIsWinner from the game record at dealStatus=3 (river).

Bug fixed (HEDGE-136)

The previous implementation paid ALL bets regardless of hand outcome. Winning-hand filter was missing.

Acceptance Criteria

#CriterionJSTSUMA
AC1Bet on winning hand → credits increase by stake × oddsAtBet
AC2Bet on losing hand only → winAmount = 0
AC3Bets on all hands (3-hand game) → winAmount > 0
AC4credits = (startCredits − totalBet) + winAmount at river
AC5Bets on dead hand → contribute £0 to payout

Version Parity

VersionStatusNotes
JS (reference)⬜ Not auditedGetHandOdds returns 0 for non-winners implicitly
TS✅ FixedHEDGE-136 — explicit isHandWinner guard added
UMA⬜ Not auditedPhase 5

Test Coverage

TestLocation
Full cycle — winner payoutstandalone_reference_client/tests/e2e/smoke.spec.tsbetting: full cycle
Payout feature spectests/features/F015-payout-calculation.spec.ts (to add)