# Hit for Six — the cricket wall (contract v1.7)

Last updated Oct 27, 2026 · Page: https://freebeeads.com/docs/repo/cricket-design · Markdown: https://freebeeads.com/docs/md/repo/cricket-design.md · All docs: https://freebeeads.com/llms-full.txt

A floor-to-ceiling portrait LED wall in a mall with a **kiosk** (a tablet) beside
it. Shoppers scan the wall's QR and get a **4-digit code** on their phone (and on
WhatsApp when configured). They queue at the kiosk in person, enter the code,
and pick **one of the 10 sponsor packs at a time**; the wall plays each ball and
the pack flips to reveal 0 / 1 / 2 / 4 / 6 / OUT. The super over ends at 6 balls
or the first out. Highest runs of the day tops the leaderboard. (v1.7.1 — this
replaced the v1.7 phone picker and its notifications: no push, no phone-side
timing, the queue is physical.)

This document is the design the code follows. Protocol detail is in
`API_CONTRACT.md` §v1.7; the engine is `web/public/cricket.html`; the phone page
is `web/app/play/[screen]`; the backend is `app/services/cricket.py` +
`app/api/cricket.py`; the shell plan is §6.

## 1. Why it works, and the two rules that make it fair

* The screen is the stage and the phone is the controller. Stadium "big screen"
  games draw crowds because everyone can see whose turn it is.
* The ad canvas is the stadium: the pack tiles, the boundary boards, the batter's
  helmet / jersey / bat, the scoreboard sponsor, the "Brite Super Six" sting.
* **Picks are blind.** Every innings gets a fresh shuffle of the config's `bag`
  (default 4×0, 5×1, 4×2, 3×4, 2×6, 2×OUT over 20 packs). Values are sealed
  server-side when the player locks their picks and only revealed by the wall.
  Nothing the player can see tells them where the sixes are.
* **The server scores.** The wall reports an innings, but runs are re-derived
  from the sealed balls before any prize is minted. A tampered wall cannot
  inflate a score.

Expected score with the default bag ≈ 9 runs over 6 balls; max 36; ~45 % of
innings end in an OUT before the sixth ball. Tune `bag` per campaign.

## 2. The turn, timed

| step | who | seconds |
|---|---|---|
| intro card / walk-in clip | wall | 2.6 (or clip) |
| per ball: pack flips 1.6 → ball bowled 0.9 → clip or procedural shot 3–9 → score 0.5 | wall | 6–12 |
| result card with QR | wall | 7 |

≈ 50–70 s per innings → 50–70 innings an hour on one wall. Players pick while
they wait, so a turn is only the watching part. For peak hours set `balls` to
3 in the config.

## 3. Queue rules (v1.7.1)

* A **ticket** (`queued`) is a session with a 4-digit code, good for 30 min.
* The **kiosk** claims a code → `playing`. One innings per wall; a second
  claim while someone bats is refused with their name, so the person in the
  queue just waits.
* The kiosk sends one pick at a time; the server reveals the sealed value and
  the wall plays it; the next pick unlocks only after the wall reports it has
  **shown** the ball. An innings idle for 90 s closes as it stands.
* No notifications anywhere. The phone mirrors the innings and ends with the
  scorecard + claim link.

## 3a. Queue rules (v1.7, phone picker — retired)

* `queued` → `ready` (called) → `playing` → `done`.
* A player is only **called** once their picks are sealed; players without
  picks keep their place but cannot be promoted.
* Called players have `turn_timeout_s` (default 25) to tap **Play**. No tap →
  `skipped`; the phone shows "missed your turn, join again".
* A started innings the wall never reports is `expired` after 180 s.
* The wall polls every 2 s; phones poll every 2 s while live. Push (WhatsApp /
  Web Push) is a later upgrade; the phone buzzes with the Vibration API when
  called.
* One player per wall at a time. `MAX_QUEUE` = 200.

## 4. Prizes, names and sharing

* Prize bands by runs (`prizes[].min_runs`), highest band first, walking down
  when a band's `daily_budget` is spent. The 0-run band is what everyone gets.
* A prize is minted through the **spin ledger** (a `Spin` row with
  `symbol_key = "cricket"` + a `WinToken`), so the wallet, OTP, coupons,
  redeem and metrics from v1.5 are unchanged. The phone's result screen links
  to `/w/claim/{token}`.
* **Names** are moderated before insert (`app/services/moderation.py`):
  normalise (accents, leet, repeats) → URL / phone rejection → block list
  (English + Hindi / Telugu / Tamil / Urdu in Latin script; `MODERATION_EXTRA_WORDS`
  env extends it). Strict on purpose: a false reject costs one retry.
* **No photos on the wall** in v1. Players pick an emoji avatar. The share
  card (`GET /v1/play/sessions/{id}/card.png`) is a server-rendered PNG with
  name, runs, balls, rank, venue — shared through the phone's native share sheet.
* Rule 3 holds: a session stores a display name and an emoji only. Rows feed
  the day's leaderboard; a cleanup job can purge rows older than 7 days.

## 5. Assets a sponsor uploads (portal → Creatives → Cricket wall)

| key | required | where it shows |
|---|---|---|
| `logo` | yes | header, "Scoreboard by", callout sting |
| `hero` | yes | portal card / attract |
| `pack` | yes | **the 20 tiles** on the wall and the picker on every phone |
| `board` | no | scrolling boundary-board strip |
| `scoreboard_logo`, `helmet_logo`, `jersey_logo`, `bat_sticker` | no | the player card (helmet / chest / bat anchors) |
| `batter`, `stadium` | no | override the template's batter cutout / stadium still |
| `clip_six`, `clip_four`, `clip_out` | no | stadium clips per ball (fall back to the procedural shot) |
| `clip_dot`, `clip_single`, `clip_double`, `clip_walkin` | no | more clips |

Clips carry **no logos**; the wall overlays the sponsor's. Demo set: `Brite`
(fictional detergent) in `web/public/cricket/brite/`; clips generated with
Seedance from one start frame so the batter stays consistent.

## 6. Native player shell (step 4 — plan)

The browser player is enough for the pilot. For a wall that must run for
weeks, a thin native shell wraps the **same HTML5 engines**:

* **Flutter** (the repo already has `mobile/`), targets Android boxes and
  Windows mini PCs on HDMI; Linux for free.
* Boots on power-on, full screen, watchdog that restarts the web view on a hang
  (no `gody:stats` for 120 s), reload at a slot boundary after an update.
* Downloads and caches the whole content pack (clips, packs, engines) on
  schedule refresh; plays the ad loop and attract mode fully offline.
* Offline queue of plays / events exactly as the browser player (`storage.ts`
  semantics), drained in `batch_seq` order.
* Polls the cricket queue only; when the API is unreachable the wall shows the
  attract mode with "come back in a moment" and the phone says the screen is
  offline.
* Remote: version + logs in `GET /v1/ops/fleet`, "reload" and "update" commands
  via the heartbeat body.

Not started: the Flutter SDK is not installed on the build machine. Estimate
2 weeks once the pilot proves the game.

## 7. Open items

* Web Push / WhatsApp nudge when a player is called.
* Per-day leaderboard prize (top of the board at closing) — needs an ops action.
* Purge job for `cricket_sessions` older than 7 days.
* Portal preview of the wall in ops approvals plays the local simulator
  (`cricket.html` without a host) — fine for approval, says so in the UI.
