> ## 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.

# F013 — Multi-hand Betting

> Betting independently on each of the multiple hands dealt simultaneously in one game round

# F013 — Multi-hand Betting

| Field  | Value               |
| ------ | ------------------- |
| ID     | F013                |
| Phase  | 2 — Betting         |
| Jira   | HEDGE-50            |
| Status | TS ✅ · JS ⬜ · UMA ✅ |

## Business Rule

Each game round deals multiple hands simultaneously. The player may bet independently on any or
all of the live hands during each betting window (HOLE, FLOP, TURN). A hand is live if its
current odds are `> 1`. Bets on different hands are entirely independent — betting on hand A does
not affect the bet on hand B. Dead hands cannot be bet on.

## Hand Count by Version

| Version        | Default hands | Configurable?                        | Min | Max |
| -------------- | ------------- | ------------------------------------ | --- | --- |
| JS (reference) | 3             | No (hardcoded `NUMBER_OF_HANDS = 3`) | 3   | 3   |
| TS             | 3             | Yes — F001/F030 config               | 2   | 4   |
| UMA            | 3             | No (hardcoded `NUM_HANDS = 3`)       | 3   | 3   |

## JavaScript Reference

**Source:** `hedgeem-v4/odobo/src/js/hands.js`

* `NUMBER_OF_HANDS = 3` — hardcoded
* `handBetValue[hand][stage]` — 2D array tracks bets independently per hand per stage
* `BetOnHand(handIndex)` → `AddBetToHand(hand)` — per-hand bet placement

## TypeScript Implementation

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

* `numHands` config parameter (2–4)
* `this.bets[]` — flat array of `{ handIndex, stage, stakeAmount, oddsAtBet }` entries
* `placeBet(handIndex, stakeAmount)` — guards dead hands independently per `handIndex`

## UMA Implementation

**Source:** `gameClient/builds/hedgeem/platform.js`

`NUM_HANDS = 3` (line 19). Three hands are always dealt. The GDK (game.min.js) sends one bet
entry per hand in each spin packet:

```javascript theme={null}
// Spin packet from game.min.js
{ type: 'connect:GameRequest', action: 'spin',
  bets: [
    { boxId: 1, bet: 100 },   // hand 0 — 100p
    { boxId: 2, bet: 200 },   // hand 1 — 200p
    // boxId 3 absent → no bet on hand 2
  ]
}
```

Platform.js iterates `gdkBets` and posts one `/bet` request per non-zero entry:

```javascript theme={null}
gdkBets.forEach(function (b) {
  if (!b || !b.bet || b.bet <= 0) return;
  var handIndex = (b.boxId || 1) - 1;   // boxId is 1-indexed → 0-indexed handIndex
  var betBody = { handIndex: handIndex, betAmount: betPence / 100, seatIndex: 0 };
  // POST /api/tables/{id}/bet
});
```

Hands with no bet entry in `gdkBets` are skipped — no API call is made for them.

After each spin, `_lastStages` is rebuilt from API state. Each hand's `stages` array always
ends with a trailing sentinel `{ bet: 0, multiplier: currentOdds }` so the GDK can always
read `_.last(stages).multiplier` for the current odds display (HEDGE-150).

### State bridge

| Variable             | Type                         | Description                                        |
| -------------------- | ---------------------------- | -------------------------------------------------- |
| `window._lastStages` | `StageEntry[][]`             | Per-hand stages; `length === NUM_HANDS`            |
| `window._holeCards`  | `string[]`                   | Per-hand hole card strings; `length === NUM_HANDS` |
| `window._holeOdds`   | `number[]`                   | Per-hand offered odds; `length === NUM_HANDS`      |
| `window._betLog`     | `Array<{request, response}>` | One entry per `/bet` call this spin                |

### Test helpers

```javascript theme={null}
// Trigger a spin with explicit per-hand bets (bypasses GDK canvas)
window._platformApi.triggerSpin([
  { boxId: 1, bet: 100 },  // 100p on hand 0
  { boxId: 2, bet: 200 },  // 200p on hand 1
  // hand 2 (boxId 3) — no bet
]);
// Poll for: window._stagePhase !== phaseBefore
```

`triggerSpin` resets `window._betLog = []` before firing so assertions capture only that
spin's bets. After the spin, `_stagePhase` changes to `STATUS_FLOP`.

## Acceptance Criteria

| #   | Criterion                                                                       | JS  | TS  | UMA |
| --- | ------------------------------------------------------------------------------- | --- | --- | --- |
| AC1 | Multiple hands are dealt per round (≥ 2)                                        | ✅   | ✅   | ✅   |
| AC2 | Exactly 3 hands dealt in UMA default config                                     | N/A | N/A | ✅   |
| AC3 | Each hand has independent hole cards                                            | ✅   | ✅   | ✅   |
| AC4 | Each hand has independent odds at HOLE                                          | ✅   | ✅   | ✅   |
| AC5 | Bet on hand A does not affect bet on hand B                                     | ✅   | ✅   | ✅   |
| AC6 | Unbet hands are skipped (no API call)                                           | N/A | N/A | ✅   |
| AC7 | After spin with multi-hand bets, `_lastStages` reflects per-hand committed bets | N/A | N/A | ✅   |

### UMA-testable ACs (via state bridge)

| #       | UMA Criterion                                                                                              | Notes                            |
| ------- | ---------------------------------------------------------------------------------------------------------- | -------------------------------- |
| AC1-UMA | `window._lastStages.length >= 2` after init                                                                | Multi-hand structure confirmed   |
| AC2-UMA | `window._lastStages.length === 3`                                                                          | UMA hardcodes `NUM_HANDS = 3`    |
| AC3-UMA | `window._holeCards.length === window._lastStages.length`                                                   | One hole card pair per hand      |
| AC4-UMA | `window._holeOdds.length === window._lastStages.length`                                                    | One odds value per hand          |
| AC5-UMA | Each hand's `_lastStages[i][last].bet === 0` at HOLE init (no committed bets)                              | Trailing sentinel always present |
| AC6-UMA | After `triggerSpin([{boxId:1, bet:100}])`, `_betLog.length === 1` and `_betLog[0].request.handIndex === 0` | Only one bet placed              |
| AC7-UMA | After `triggerSpin([{boxId:1, bet:100},{boxId:2, bet:200}])`, `_betLog` has entries for handIndex 0 and 1  | Two bets placed independently    |

## Version Parity

| Version        | Status        | Notes                                                                              |
| -------------- | ------------- | ---------------------------------------------------------------------------------- |
| JS (reference) | ⬜ Not audited | `handBetValue[hand][stage]` 2D array — multi-hand by design                        |
| TS             | ✅ Verified    | `placeBet(handIndex, stakeAmount)` — per-hand with dead-hand guard                 |
| UMA            | ✅ Verified    | `tests/features/F013-multi-hand-betting.spec.ts` — AC1–AC7 (UMA) pass (2026-04-15) |

## Test Coverage

Playwright spec: `tests/features/F013-multi-hand-betting.spec.ts` — AC1–AC7 (UMA).

Run with:

```bash theme={null}
VERSION=uma npx playwright test tests/features/F013-multi-hand-betting.spec.ts --project=features --workers=1
```

## Related Features

* F001 — Hand count rules (number of hands per round)
* F011 — Bet Placement (single-hand bet mechanics)
* F015 — Payout calculation (multi-hand payouts)
* F010 — Chip Denominations (bet amounts used per hand)
