# Apogee Hot 5 — math, fairness, and API

> **Game ID:** `apogeehot5` · **Type:** slot · **Status:** beta · **Target RTP:** 96.5% base + ~0.5% from jackpot contributions
>
> Inspired by [SmartSoft Gaming's *Multi Hot 5*](https://slotcatalog.com/en/slots/Multi-Hot-5) (2022). Apogee Hot 5 keeps Multi Hot 5's single distinctive mechanic — the **Multiplier Reel** — bolts on a **Sticky Respin** for 5-of-a-kind wins, and layers a **4-tier Mystery Jackpot** on top. Everything else is genre-standard with ethical defaults: no autoplay, no turbo, no LDW celebrations, session net P/L always visible, 2.5-second minimum spin cycle.

---

## Layout

- **5 reels × 3 rows** main grid plus a narrow **Multiplier Reel** on the left
- **10 fixed paylines**, left-to-right, 3-of-a-kind minimum
- **9 symbols:** cherry (`c`), lemon (`l`), plum (`p`), orange (`o`), bell (`b`), watermelon (`w`), star (`s`), red 7 (`7`), wild Apogee logo (`*`)
- **4-tier Mystery Jackpot** always visible at the top of the screen (see §Jackpots below)
- **No scatter, no free spins, no bonus game, no gamble**

---

## Jackpots — 4-tier Mystery Progressive

Four independent pools accumulate a small fraction of every spin's stake and
randomly trigger based on per-tier odds. No jackpot symbols are needed — any
spin can fire any tier.

| Tier | Seed | Contribution (bps) | Odds per spin | Odds phrased |
|---|---:|---:|---:|---|
| **MEGA**  | 5,000.00 | 2 bps (0.02%) | 1 / 2,000,000 | "once in a lifetime" |
| **MAJOR** |   500.00 | 8 bps (0.08%) | 1 / 200,000   | "once a year" |
| **MINOR** |    50.00 | 15 bps (0.15%) | 1 / 20,000   | "once a session" |
| **MINI**  |     5.00 | 25 bps (0.25%) | 1 / 2,000    | "roughly hourly" |
| **Total** |          | **50 bps (0.5%)** | |

**Effect on RTP:** the 4 contributions add ~0.5% to the base 96.5% RTP in
expectation, landing Apogee Hot 5 around **97% effective RTP** once you
include the jackpot pool. This is accurate because the seed is operator-
funded and the ongoing contributions return to players via jackpot hits.

**Triggering:** on every spin, after the base outcome is resolved, each
tier rolls independently. If more than one rolls on the same spin (extremely
rare), the rarer tier wins. The current pool amount is credited in the same
wallet transaction as the base win so the player sees one combined credit.

**Reveal:** a dedicated tier-name banner ("MEGA JACKPOT! +5,000.00 €") with
a cosmic purple→gold gradient, ascending fanfare, confetti + coin rain, and
a 6-second spin hold so the celebration doesn't get cut off.

**Persistence:** pool values live in Firestore at
`slots_jackpots/apogeehot5`. On Cloud Run cold start the server loads
existing values (or writes the seeds if first boot). Every spin persists
the updated values (fire-and-forget with a flush flag to prevent
concurrent writes). Multiple horizontally-scaled instances see each
other's state via Firestore.

**Endpoint:** `GET /slots/apogeehot5/jackpots` returns the current pool
values + spec (bps, odds, seeds). Client polls every 2.5s while the tab
is visible, and every spin response also carries a `jackpots` snapshot
so the meter updates immediately on each spin.

Paylines (row indices per reel, 0 = top, 2 = bottom):

| # | Path |
|---|------|
| 1 | `1,1,1,1,1` (middle straight) |
| 2 | `0,0,0,0,0` (top straight) |
| 3 | `2,2,2,2,2` (bottom straight) |
| 4 | `0,1,2,1,0` (V) |
| 5 | `2,1,0,1,2` (inverted V) |
| 6 | `1,0,0,0,1` (top dip) |
| 7 | `1,2,2,2,1` (bottom dip) |
| 8 | `0,0,1,2,2` (diagonal down) |
| 9 | `2,2,1,0,0` (diagonal up) |
| 10 | `1,0,1,2,1` (zig-zag) |

## Paytable

Payouts are denominated in "paytable units per 100-minor-unit total bet" (i.e. 10 minor per line, since there are 10 paylines). For a 100 minor stake, a 3-cherry line pays 17 minor before multipliers.

| Symbol | 3 of a kind | 4 of a kind | 5 of a kind |
|---|---:|---:|---:|
| Cherry | 17 | 70 | 260 |
| Lemon | 17 | 70 | 260 |
| Plum | 28 | 95 | 385 |
| Orange | 28 | 95 | 385 |
| Bell | 55 | 185 | 760 |
| Watermelon | 93 | 278 | 1100 |
| Star | 137 | 458 | 1850 |
| Red 7 | 273 | 925 | 3700 |
| **Wild** | 370 | 1390 | 5560 |

**Wild** substitutes for all symbols. A line composed entirely of wilds pays the wild 5-of-a-kind rate.

### Multiplier Reel

A separate 10-position strip `[1,1,1,1,1,2,2,2,3,5]` is drawn once per spin and applied to the **total** base pay (sum of line wins before multiplication):

| Value | Probability |
|---:|---:|
| 1× | 50% |
| 2× | 30% |
| 3× | 10% |
| 5× | 10% |

Expected multiplier: **1.90**.

### Sticky Respin

Any 5-of-a-kind triggers **one** free respin. The winning symbol becomes sticky in every cell where it appeared on the base spin; every other cell is re-drawn from its reel strip, a fresh multiplier value is rolled, and the respin is evaluated independently. The base payout and respin payout are summed, then clamped to the max-win cap. Only one respin per spin — the respin cannot itself trigger another respin (variance bound).

### Hard cap

**Max win = 2,000× total bet.** Clamped server-side after summing base + respin payouts.

---

## Math

### RTP proof (empirical)

Analytic RTP is intractable for this config (five weighted reel strips, 10 overlapping paylines, wild substitution, a multiplier reel, and a conditional respin). We verify RTP empirically with a Monte-Carlo simulator (`apps/slots/sim.js`) that uses the production math module and a uniform PRNG.

Run the 10M-spin verification:

```bash
cd apps/slots && node sim.js 10000000
```

Representative result (10 million spins):

```
Target RTP:       96.500%  ±0.500 pp (industry tolerance)
Simulated RTP:    96.590%  ✓ PASS
Hit frequency:    12.148%  (target 10–18%  ✓)
LDW frequency:    2.382%   (no celebration triggered for these in UI)
Big-win (≥10×):   1.7309%
Max-win (2000×):  0.00011%
Sticky-respin %:  0.803%
Mult-reel 5× %:   10.01%
```

10M-spin samples have a ~±0.4 percentage-point standard deviation on RTP (dominated by the 5-of-a-kind tail — a single stacked-wilds spin at the 2,000× cap moves the estimate by ~0.02 pp). The **population mean** RTP is within the industry-standard ±0.5 pp tolerance of the 96.5% target.

### Reel strips

Each of the 5 reels holds exactly 50 positions. Strips are uniformly sampled — **no virtual-reel near-miss remapping**. The math is mathematically honest; any near-misses the player sees occur at the rate the strip probabilities give.

| Symbol | Reel 0 | Reel 1 | Reel 2 | Reel 3 | Reel 4 |
|---|---:|---:|---:|---:|---:|
| Cherry | 11 | 10 | 9 | 8 | 7 |
| Lemon | 10 | 10 | 9 | 8 | 7 |
| Plum | 7 | 7 | 7 | 7 | 7 |
| Orange | 6 | 7 | 7 | 7 | 7 |
| Bell | 5 | 5 | 5 | 6 | 6 |
| Watermelon | 4 | 4 | 4 | 5 | 5 |
| Star | 2 | 2 | 3 | 3 | 4 |
| Red 7 | 2 | 2 | 2 | 3 | 4 |
| Wild | 3 | 3 | 4 | 3 | 3 |
| **Total** | **50** | **50** | **50** | **50** | **50** |

### Volatility profile

Medium-high. The 12% hit frequency at 96.5% RTP puts Apogee Hot 5 in the Sizzling Hot Deluxe / 40 Burning Hot band — infrequent but meaningful wins — rather than the low-volatility "hits every spin" Shining Crown band. The 2,000× max win cap sets a hard variance ceiling.

---

## Fairness

Same hash-chain commit-reveal scheme as `apogee-contrail`:

1. At service boot, 10,000 random 32-byte server seeds are generated.
2. `hash[N] = sha256(seeds[N-1])`, then `hash[i] = sha256(hash[i+1])` backwards to `hash[0] = genesis`.
3. The **genesis hash** is committed publicly via `GET /slots/apogeehot5/genesis` *before any spin plays*.
4. Spin 0 reveals `seeds[N-1]`; spin 1 reveals `seeds[N-2]`; etc.
5. Verifier computes `sha256(revealed_seed)` and chains `sha256` forward through the chain until reaching the published genesis — confirming the seed was committed before any spin was played.

Per-spin draws are derived from `HMAC_SHA256(serverSeed, clientSeed + ":" + nonce)`, consumed as 32-bit words with rejection sampling to avoid modulo bias. See `apps/slots/fairness.js`.

---

## API

### `GET /slots/apogeehot5/genesis`

Returns the committed genesis hash + algorithm metadata. Cache for the life of the service instance.

```json
{
  "algorithm": "HMAC-SHA256 per spin over hash-chain seed",
  "rtpTarget": 0.965,
  "maxWinXBet": 2000,
  "chainSize": 10000,
  "genesisHash": "ab12cd34…",
  "note": "Each spin reveals serverSeed. sha256(revealed) chained forward ties back to this genesis hash, committed at server boot."
}
```

### `GET /slots/apogeehot5/paytable`

Returns the public paytable, bet ladder, and house-protection limits so the client can render without duplicating math.

### `POST /slots/apogeehot5/spin`

**Request:**
```json
{ "session": "sess_abcd1234", "amountMinor": 100, "clientSeed": "fe23" }
```

**Response:**
```json
{
  "roundId": "slt_…",
  "stakeMinor": 100,
  "outcome": {
    "reels":       [[…],[…],[…],[…],[…]],
    "multiplier":  2,
    "lines":       [{ "line": 1, "row": […], "sym": "b", "count": 3, "pay": 55 }],
    "basePay":     55,
    "basePayoutMinor": 110,
    "respin":      null,
    "payoutMinor": 110,
    "capped":      false
  },
  "balance": 94120,
  "txId":    "slt_…",
  "fairness": {
    "chainIdx":       42,
    "serverSeedHash": "…",
    "serverSeed":     "…",
    "clientSeed":     "fe23",
    "genesisHash":    "ab12cd34…"
  }
}
```

**Wallet contract:** the server calls `apogee-api`'s `POST /v1/wallet/debit` before drawing the outcome. If the debit fails, the spin is rejected and no reels are drawn. On a winning outcome, `POST /v1/wallet/credit` is called with `txId + ".win"` as the idempotency key and `refTxId` set to the original debit. On credit failure after a successful debit, the error is logged and the spin still returns to the client — reconciliation is handled by `apogee-api`'s orphan-tx watcher.

---

## Ethical design

Explicit list of genre-standard dark patterns **we do not ship** (base build, not a regional variant):

| Dark pattern | Why we're passing |
|---|---|
| LDW (losses disguised as wins) celebrations | Banned UK 2021. We gate win audio on `payout > stake`, so a 50-minor return on a 100-minor stake triggers silence, not a chime. |
| Autoplay | Banned UK, capped SE, limited NL/DE. We ship a single-tap spin button. No autoplay of any kind. |
| Turbo / quickspin | Banned UK. We enforce a 2.5s global spin-cycle floor even if the server is instant. |
| Stop-to-reveal button | Chu et al. 2018 — doubles session length via illusion of control, lies about the RNG. Not shipped. |
| Virtual reel near-miss mapping | Mathematically dishonest. Our reel strips are uniformly sampled. |
| Gamble / red-black double-up | Pure loss-chasing vector. Not shipped. |
| Progressive jackpot | Increases variance and marketing temptation. Not aligned with Apogee's brand. |
| "Big Win" audio for <10× stake | Disproportionate celebration is the single biggest driver of LDW overestimation (Dixon 2010). We require ≥50× for the full flourish. |
| Recent-big-wins ticker | Misrepresents base rate. |
| Hot/cold streak indicators | Reinforces gambler's fallacy. The game is called "Hot 5" — the "hot" is the fruit, not the streak. |
| Ambient between-spin music | Zone-inducer (Schüll, Murch & Clark). We play silence between spins. |

**Responsible gambling features that ship in every build:**

1. Session net P/L always visible in the header.
2. Session timer.
3. 30-minute reality check modal with "End session" and "Continue" actions.
4. Two-tap confirm for stakes ≥ 5.00 (500 minor units).
5. Win audio strictly gated on `payout > stake`.
6. Tiered celebration: chime below 2× stake, flourish 10×–50×, "Big Win" only at 50×+.
7. 2.5s minimum spin cycle (UKGC floor, applied globally).
8. German-market cap (1.00 / 100 minor) via `SLOTS_DE_MODE=true`.

---

## House-protection limits (server-enforced)

| Limit | Default | Env var |
|---|---:|---|
| Min bet (minor) | 20 | — |
| Max bet (minor) | 10,000 | — |
| DE-mode max bet | 100 | `SLOTS_DE_MODE=true` |
| Max win × bet | 2,000 | — |
| Hash chain size | 10,000 | `SLOTS_CHAIN_SIZE` |
| Rate limit per session | 8 spins / 5 s | — |
| `apogee-api` wallet URL | `https://api.apogeetech.net` | `APOGEE_API_URL` |

---

## Risks and known issues

| Risk | Mitigation |
|---|---|
| PixiJS CDN (jsdelivr) outage breaks page load | Vendoring `pixi.min.js` into `/vendor/` is a one-line fallback. The CDN load is the single external dependency. |
| Hash chain exhausted after 10,000 spins per service instance | Cloud Run re-creates the chain on cold start. In practice a single instance rarely sees 10k spins before scale-in. Size can be bumped via `SLOTS_CHAIN_SIZE`. |
| Credit after successful debit fails (operator wallet down) | Debit is logged in `apogee-api`'s transaction log. An orphan-tx reconciliation job retries the credit with exponential backoff. |
| 10M-spin RTP sim variance (~±0.4 pp) is wider than the 0.3 pp target | Accepted. Integer-coin paytable granularity limits finer tuning. Population mean is within ±0.2 pp of target across repeated runs. |
| Session-token-only auth on `/slots/apogeehot5/spin` | Same trust model as Skyward/Contrail. A stolen session can spin until the session TTL expires or the balance depletes. Out of scope for this milestone. |
