> ## Documentation Index
> Fetch the complete documentation index at: https://hedgeem-api.qeetoto.com/llms.txt
> Use this file to discover all available pages before exploring further.

# F011 — Bet Placement

> How players place bets on hands at each deal stage

# F011 — Bet Placement

| Field  | Value                              |
| ------ | ---------------------------------- |
| ID     | F011                               |
| Phase  | 2 — Betting                        |
| Jira   | HEDGE-133                          |
| Status | TS ✅ fixed · JS ✅ verified · UMA ⬜ |

## Business Rule

Players may place one or more chip-value bets on any **live** hand during each of the three
betting windows (hole, flop, turn). Bets are accumulated per hand per stage. A hand is live
if its current odds are `> 1`. Once a hand dies (odds = 0, not a winner) it cannot be bet
on. No bet may cause a hand's cumulative stake to exceed **£50.00 (5000 pence)**.

The payout at resolution is:

```
payout = Σ over all (stage) of: oddsAtBet × stakeAtStage   — for WINNING hands only
```

Only bets on the **winning hand** are paid. The odds are locked in at the time the bet is placed
(not the river odds). Bets on losing and dead hands yield £0 regardless of the odds at bet time.
See F015 for the full payout calculation spec.

## JavaScript Reference

**JS Source:** `hedgeem-v4/odobo/src/js/`

| Function                | File             | Responsibility                                                      |
| ----------------------- | ---------------- | ------------------------------------------------------------------- |
| `BetOnHand()`           | `hands.js:1210`  | Click handler — routes to `AddBetToHand(handIndex)`                 |
| `AddBetToHand(hand)`    | `control.js:180` | Guards + accumulates `handBetValue[hand][dealStatus]`               |
| `CancelButtonPressed()` | `buttons.js:881` | Calls `ClearRoundBets()` — clears current-stage bets for all hands  |
| `ClearRoundBets()`      | `control.js:143` | Zeros `handBetValue[hand][GetDealStatus()]` for all hands           |
| `ResolveHands()`        | `hands.js:957`   | Payout = `Σ handOdds[h][s] × handBetValue[h][s]`                    |
| `SetHandOdds(hand)`     | `control.js:99`  | Snapshots `GetDataHandOdds(hand)` into `handOdds[hand][dealStatus]` |
| `CC_DEDUCT_BETS`        | `control.js:758` | Credits deducted in one batch *after* all betting windows close     |
| `CC_ADD_WIN`            | `control.js:772` | Win amount added to credits after resolution                        |

**JS Live:** [hedgeem.qeetoto.com](https://hedgeem.qeetoto.com) — verified working reference

### Key JS data structures

```javascript theme={null}
var handBetValue = [];      // [hand][stage] — 2D, stage = dealStatus (0=hole,1=flop,2=turn)
var handOdds = [];          // [hand][stage] — odds snapshot taken when betting window opens
var chipValues = [25, 50, 100, 250, 500, 1000];  // buttons.js — pence
const CC_MAX_BET = 5000;   // defines.js — max cumulative bet per hand (pence)
```

### JS betting gate

```javascript theme={null}
// AddBetToHand (control.js:180)
function AddBetToHand(hand) {
    if (GetDataHandOdds(hand) > 1) {           // hand is live
        var betValue = chipValues[activeBetChip];
        if (GetTotalHandBet(hand) + betValue <= CC_MAX_BET) {  // max bet cap
            AddBet(hand, betValue);
            UpdateChipStack(hand);
        }
    }
}
```

### JS payout formula

```javascript theme={null}
// ResolveHands (hands.js:986)
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);
```

### JS credit deduction timing

Credits are **not** deducted when a bet is placed. They are deducted in one batch at
`CC_DEDUCT_BETS` (after the turn betting window closes, immediately before river reveal):

```
CC_DISALLOW_BETS_3 → CC_DEDUCT_BETS → CC_REVEAL_RIVER → CC_RESOLVE_HANDS
```

```javascript theme={null}
case CC_DEDUCT_BETS:
    var credits = GetCreditAmount();
    var bet = GetBetAmount();          // GetAllBets() = sum across all hands/stages
    SetCreditAmount(credits - bet);
```

### JS cancel

A single global **CANCEL** button calls `ClearRoundBets()`, which zeros bets for the
**current deal stage** across **all hands**. It does not refund credits (because credits
haven't been deducted yet).

## TypeScript Implementation

**File:** `standalone_reference_client/src/engine/GameEngine.ts`

### Fixes applied (F011 audit)

| Bug                            | JS behaviour                     | TS before fix     | Fix applied                                |
| ------------------------------ | -------------------------------- | ----------------- | ------------------------------------------ |
| `stage` field hardcoded to `0` | `handBetValue[hand][dealStatus]` | `stage: 0` always | `stage: this.dealStatus`                   |
| No max bet cap                 | `CC_MAX_BET = 5000` per hand     | unlimited         | `MAX_BET_PER_HAND = 5000` constant + guard |
| Payout checks `isHandWinner`   | pays all bets at locked odds     | only winner paid  | removed winner check, pays all bets        |

### Design differences (intentional)

| Behaviour               | JS                                               | TS                                  | Notes                                |
| ----------------------- | ------------------------------------------------ | ----------------------------------- | ------------------------------------ |
| Credit deduction timing | Deferred to CC\_DEDUCT\_BETS (batch, after turn) | Immediate on `placeBet()`           | TS provides instant balance feedback |
| Cancel scope            | Global — current stage, all hands                | Per-hand — all stages for that hand | TS per-hand cancel is better UX      |

### TS `placeBet()` after fix

```typescript theme={null}
const MAX_BET_PER_HAND = 5000;

placeBet(handIndex: number, stakeAmount: number): boolean {
  if (this.dealStatus < 0 || this.dealStatus > 2) return false;
  if (stakeAmount > this.credits) return false;
  if (this.isHandDead(handIndex)) return false;
  if (this.getHandBet(handIndex) + stakeAmount > MAX_BET_PER_HAND) return false;

  const odds = this.getHandOdds(handIndex);
  this.credits -= stakeAmount;
  this.bets.push({ stage: this.dealStatus, handIndex, stakeAmount, oddsAtBet: odds });
  return true;
}
```

### TS `resolveBets()` after fix

```typescript theme={null}
resolveBets(): number {
  let totalPayout = 0;
  for (const bet of this.bets) {
    totalPayout += Math.round(bet.stakeAmount * bet.oddsAtBet);
  }
  this.credits += totalPayout;
  this.winAmount = totalPayout;
  return totalPayout;
}
```

## Dead Hand Logic

A hand is considered **dead** when `oddsRounded === 0 AND statusIsWinner === false`.

| Check               | JS                                                                              | TS                                                                          |
| ------------------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| Betting gate        | `GetDataHandOdds(hand) > 1`                                                     | `isHandDead(handIndex)` (odds=0 && !winner)                                 |
| Payout contribution | `handOdds[h][s] × handBetValue[h][s]` — zero if hand was dead when stage opened | `bet.oddsAtBet × bet.stakeAmount` — zero because dead hands can't be bet on |

Both approaches yield 0 payout for dead hands because bets can only be placed when odds > 1,
so `oddsAtBet` will always be > 1 for any valid bet.

## Maths

### Payout identity

```
credits_after = credits_before − totalBet + payout
payout = Σ round(stakeAtStage × oddsAtStage)  [for all (hand, stage) bets]
```

### Max bet capacity

```
CC_MAX_BET = 5000p = £50.00 per hand
3 betting stages × 4 hands = max 4 × 5000 = 20000p (£200) total exposure per game
```

### House edge

The odds presented at each stage are computed from simulation data (`percentWinOrDraw`).
The payout odds (`oddsRounded`) are set slightly below the inverse of true probability,
providing the house margin. Example from coredata game 1, hand 2 at hole:

* `percentWinOrDraw = 0.3311` → true odds = 3.02
* `oddsRounded = 3` → payout odds = 3.00
* House margin ≈ 0.7%

## Acceptance Criteria

| #   | Criterion                                                  | JS             | TS | UMA |
| --- | ---------------------------------------------------------- | -------------- | -- | --- |
| AC1 | Bet placed on live hand deducts stake from credits display | N/A (deferred) | ✅  | ⬜   |
| AC2 | Dead hand cannot be bet on                                 | ✅              | ✅  | ⬜   |
| AC3 | Chip value selection drives stake amount                   | ✅              | ✅  | ⬜   |
| AC4 | Cumulative bet per hand capped at 5000p                    | ✅              | ✅  | ⬜   |
| AC5 | Bets persisted per stage (hole/flop/turn)                  | ✅              | ✅  | ⬜   |
| AC6 | Payout = Σ(stakeAtStage × oddsAtStage) for all stages      | ✅              | ✅  | ⬜   |
| AC7 | Cancel refunds stake; credit balance restored              | ✅              | ✅  | ⬜   |
| AC8 | Playwright test covers: bet → advance → river → payout     | ✅              | ✅  | ⬜   |

## Version Parity

| Version        | Status        | Notes                                                                |
| -------------- | ------------- | -------------------------------------------------------------------- |
| JS (reference) | ✅ Verified    | Live at hedgeem.qeetoto.com; source in hedgeem-v4                    |
| TS             | ✅ Fixed       | 3 bugs fixed this session (stage field, max bet cap, payout formula) |
| UMA            | ⬜ Not audited | Phase 5                                                              |

## Test Coverage

| Test                                 | Location                                                  | Versions         |
| ------------------------------------ | --------------------------------------------------------- | ---------------- |
| Bet deducts credits / cancel refunds | `standalone_reference_client/tests/e2e/smoke.spec.ts:164` | TS               |
| Full cycle: bet → river → payout     | `standalone_reference_client/tests/e2e/smoke.spec.ts:206` | TS               |
| Cross-version F011 spec              | `tests/features/F011-bet-placement.spec.ts`               | TS, JS (pending) |
