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

# F006 — Odds Calculation

> Converting win probability to player-facing payout odds with RTP margin applied

# F006 — Odds Calculation

| Field  | Value                |
| ------ | -------------------- |
| ID     | F006                 |
| Phase  | 1 — Core Game Engine |
| Jira   | HEDGE-133            |
| Status | TS ⬜ · JS ⬜ · UMA ⬜  |

## Business Rule

For each hand at each betting stage, the game calculates the payout odds the player will receive if they bet on that hand and it wins. The calculation converts the raw win probability into European (decimal) odds, then subtracts the operator-configured RTP margin.

**Formula:**

```
europeanOdds = 1 / percentWin
payoutOdds   = europeanOdds × (1 - houseMargin)
```

**Worked example (from game concept document):**

| Step            | Calculation       | Result   |
| --------------- | ----------------- | -------- |
| Win probability | 25%               | 0.25     |
| European odds   | 1 / 0.25          | 4.00     |
| House margin    | 3%                | 0.03     |
| Payout odds     | 4.00 × (1 − 0.03) | **3.88** |

If the player bets £1 and the hand wins, they receive **£3.88**. Over four statistically fair plays (one win):

```
RTP = payout / (bets × stake) = 3.88 / (4 × 1.00) = 0.97 = 97%
```

The odds presented to the player may be further rounded to player-friendly values by the Rounding Algorithm (see F033).

## Source: 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."

> "Texas Hedge'Em has a very simple maths model that is derived from the direct probability of an outcome occurring and a margin that the operator configures to take from each bet."

## JavaScript Reference

**JS Source:** `hedgeem-v4/odobo/src/js/` (not yet located — audit required)

The JS client receives pre-calculated `odds_actual` and `odds_rounded` from the server-generated game records. The odds calculation is performed server-side (C# server / Node server) at game generation time, not in the client. The client only displays and uses the pre-calculated values.

`odds_actual` = raw computed payout odds (e.g. 3.88)
`odds_rounded` = rounded value after Rounding Algorithm (e.g. 3.9 or 3) — see F033

## TypeScript Implementation

**Current status:** The TS standalone\_reference\_client does **not** calculate odds — it reads pre-computed values from `coredata.ts` (generated from JS game records). The `oddsRounded` field in `HandStageInfo` is used directly.

```typescript theme={null}
// GameEngine.ts
getHandOdds(hand: number): number {
    const info = this._infoAt(hand);
    return info?.oddsRounded ?? 0;  // pre-calculated, server-generated
}
```

Full odds calculation will be needed for:

* Live API integration (F031) when the TS client connects to a Node/C# server
* Any server-side TS implementation

## Data Structure

`HandStageInfo` (from `types.ts`):

| Field              | Type     | Description                                      |
| ------------------ | -------- | ------------------------------------------------ |
| `percentWin`       | `number` | Raw win probability (0–1)                        |
| `percentDraw`      | `number` | Draw probability                                 |
| `percentWinOrDraw` | `number` | Win + draw combined                              |
| `oddsActual`       | `number` | `1 / percentWinOrDraw` before margin             |
| `oddsRounded`      | `number` | Post-margin, post-rounding value shown to player |

## Rounding Algorithm

The HedgeEm Rounding Algorithm (`computeOddsChain` in `api/_lib/utils.js`) operates on a **percentage scale** (0–100, not 0–1):

```
oddsActual  = 100 / percentWinOrDraw          (European odds before margin)
oddsMargin  = oddsActual × targetRtp          (e.g. × 0.97 for 97% RTP)
oddsRounded = Math.round(oddsMargin × 10) / 10  (1 decimal place)
```

The `oddsRounded` value is what the player sees and bets at. All displayed odds are affected by this rounding — the rounding itself creates a small additional rake (`roundingRake`).

## Theoretical Odds Ranges by Stage

Ranges depend on hand count and the poker evaluator's combination count. Using **unordered board combinations** (standard for hold'em probability):

| Stage | Hand count  | Remaining cards | Max boards          | Max `oddsActual` | Max `oddsRounded` (×0.97)          |
| ----- | ----------- | --------------- | ------------------- | ---------------- | ---------------------------------- |
| TURN  | 1           | 52–2–4 = 46     | 46                  | 46.0             | 44.6                               |
| TURN  | 3 (default) | 52–6–4 = 42     | 42                  | 42.0             | 40.7                               |
| FLOP  | 1           | 52–2–3 = 47     | C(47,2) = 1,081     | 1,081            | 1,048.6                            |
| FLOP  | 3 (default) | 52–6–3 = 43     | C(43,2) = 903       | 903              | 875.9                              |
| HOLE  | 3 (default) | 52–6 = 46       | C(46,5) = 1,370,754 | \~1.37M          | \~1.33M (theoretical extreme only) |

Note: 46 × 45 = 2,070 is the **permutation** count for 2 cards from 46 (TURN remaining in a 1-hand game). The combination count (standard) is C(46,2) = 1,035.

## Acceptance Criteria

| #   | Criterion                                                       | JS | TS | UMA |
| --- | --------------------------------------------------------------- | -- | -- | --- |
| AC1 | `oddsActual = 1 / percentWinOrDraw` for all hands at all stages | ⬜  | ⬜  | ⬜   |
| AC2 | `oddsRounded` reflects margin-adjusted and rounded value        | ⬜  | ⬜  | ⬜   |
| AC3 | Odds displayed to player match `oddsRounded` at bet time        | ⬜  | ✅  | ⬜   |
| AC4 | Odds update correctly at each stage transition                  | ⬜  | ✅  | ⬜   |
| AC5 | Dead hands display 0 odds                                       | ⬜  | ✅  | ⬜   |

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

The UMA client exposes pre-computed odds via `window._holeOdds` (at HOLE) and `window._lastStages[i][last].multiplier` (after each advance). Raw `winPercentage` / `oddsActual` require the debug extension (not yet enabled — see below).

| #       | UMA Criterion                                                                             | Notes                                                  |
| ------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------ |
| AC1-UMA | After HOLE, `_holeOdds` has one entry per hand; all values > 1.0                          | European odds always > 1.0; HEDGE-146 regression guard |
| AC2-UMA | After advancing to FLOP, `_lastStages[i][last].multiplier` > 1.0 for all active hands     | Flop odds recalculated from HOLE                       |
| AC3-UMA | After advancing to TURN, `_lastStages[i][last].multiplier` ∈ (1.0, 44.6] for active hands | Max 44.6 = 46 × 0.97 for 1-hand game                   |
| AC4-UMA | FLOP multipliers differ from `_holeOdds` for at least one hand                            | Confirms odds update as community cards are revealed   |
| AC5-UMA | No active hand has `multiplier === 0` at HOLE or FLOP or TURN                             | 0 is reserved for dead hands (post-RIVER)              |

### Debug extension (not yet enabled)

To verify AC1/AC2 (formula correctness), `platform.js` needs to expose raw values:

* Add `oddsActual: chain.oddsActual` to `buildHandOdds()` return in `advance.ts`
* Add `window._rawOdds` per stage in `platform.js` (mapping `winPercentage`, `drawPercentage`, `oddsActual`)

## Version Parity

| Version        | Status        | Notes                                                                            |
| -------------- | ------------- | -------------------------------------------------------------------------------- |
| JS (reference) | ⬜ Not audited | Odds pre-computed server-side; client reads from game record                     |
| TS             | ⬜ Not audited | Reads `oddsRounded` from coredata; no client-side calculation                    |
| UMA            | ✅ Verified    | `tests/features/F006-odds-calculation.spec.ts` — AC1–AC5 (UMA) pass (2026-04-15) |

## Test Coverage

Playwright spec: `tests/features/F006-odds-calculation.spec.ts` — AC1–AC5 (UMA).

Run with:

```bash theme={null}
VERSION=uma npx playwright test tests/features/F006-odds-calculation.spec.ts --project=features --workers=1
```

## Related Features

* F007 — RTP Margin (the `houseMargin` parameter)
* F033 — Rounding Algorithms (post-margin rounding of `oddsActual → oddsRounded`)
* F015 — Payout Calculation (uses `oddsAtBet` captured from `oddsRounded`)
