social overhaul

This commit is contained in:
yohlo
2026-07-22 15:47:34 -07:00
parent 9db3fa697d
commit 86cda294f9
45 changed files with 3026 additions and 430 deletions
+84
View File
@@ -0,0 +1,84 @@
# Side Bets — Design Spec (v1, locked)
**Status:** design locked, not yet built. Sequenced for Wave 3 (after player score reporting + the home "Upcoming" surface; ideally after web push for bet-open nudges).
No real money, ever. This is an in-tournament engagement layer.
## Currency
- Each player gets **10 🪙 tokens fresh at the start of every tournament**. No carryover, **no seasons** (this app has no season concept).
- **Whole numbers only.**
- **No hard bust.** If you hit 0 tokens with matches remaining, you get a **1-token "comeback" bet each remaining match** so you're never fully benched. Not abusable — comeback stakes are smaller than the 10 you started with, so busting on purpose is strictly worse.
## When you can bet
- A match's book **opens when the match is *set*** (both teams populated → status `ready`).
- The book **closes the instant the admin *starts* the match** (status `started`) — same moment walkouts/announcements play. No bets after start.
- This dovetails with score reporting, which only begins once a match is `started`.
## Bet type 1 — Winner (social pool / parimutuel)
The core, social bet. You bet against your friends, not the house.
- Pick the winning team. Stake `1..(your tokens)`.
- All stakes on the match form **one pot**. On finalize, everyone who picked the actual winner **splits the whole pot proportional to their stake**; wrong picks forfeit their tokens to the pot.
- **Integer payout rule:** floor each winner's proportional share, then hand the leftover tokens out one-each to the largest fractional remainders (tie-break: larger stake, then earlier bet). Guarantees whole tokens and exact pot conservation.
- **House floor:** a correct bet always returns **at least ~1.5× the stake** even if nobody took the other side, so thin/late pools still feel worth it. (Tunable.)
- **Upset dynamics are automatic:** betting the less-popular team pays more because fewer winners split a bigger pot — no odds engine, no stats needed.
- **Edge cases:**
- Everyone picked the same team → no losers → **all stakes refunded** (no action).
- Nobody picked the winner → **everyone refunded**.
- Solo bettor → the **house floor** applies (this is exactly what it's for).
- **Live projected payout:** before close, show "if you're right you'd win ~X", ticking up via SSE as friends pile in. Final pot locks at match start.
## Bet type 2 — Overtime long shots (house-backed, fixed payout)
NOT a pool — you bet against a fixed multiplier, so including these does **not** thin the Winner pool. Cumulative "reaches at least N overtimes" tiers, keyed off the match `ot_count` field (the app tracks multiple OTs):
| Bet | Wins if | Payout |
|---|---|---|
| Reaches OT | `ot_count ≥ 1` | **2×** |
| Reaches Double OT | `ot_count ≥ 2` | **4×** |
| Reaches Triple OT | `ot_count ≥ 3` | **8×** |
Each is an independent yes/no long shot; **each additional OT doubles the payout** (memorable rule, jackpot feel for triple OT). Multipliers are tunable once real OT frequency is observed.
## Leaderboard & payoff
- **Per-tournament Side Bets leaderboard**, ranked by token count; tie-break by number of correct bets (so a hoarder who never bet ranks below active players at the same count).
- A few **badges** (global, like existing badges): e.g. "Called the Upset", "House Money", "Stone Cold Lock", "Degenerate" (most all-ins).
## Data model (proposed — finalize at build)
- New `bets` collection: `{ tournament, match, player, market: "winner"|"ot1"|"ot2"|"ot3", pick (team id for winner), stake, status: pending|won|lost|refunded, payout }`.
- **Settlement hooks into match finalization** (`finalizeMatch` / the `match` SSE): compute Winner pool splits + house-backed OT results, write payouts, update balances.
- Per-(player, tournament) balance: start 10 stakes + payouts (materialized or derived).
## UI/UX
- Bettable matches surface on the home **Upcoming** carousel (Wave 2) + the match card/dock.
- **Bet slip sheet:** choose market, stake slider, live projected payout.
- Win/loss **reveal animation** on settle; the reaction rail already exists next to matches.
- **Side Bets leaderboard screen** — mirror the existing predictions leaderboard component.
## Reuses / dependencies
- SSE bus, the predictions leaderboard pattern, the badges system.
- Depends on: score reporting (done), home Upcoming carousel (Wave 2), web push (Wave 2) for "betting's open on the Final" nudges.
## Side Bets on the home screen (reserved slot)
The Wave 2 home redesign **leaves a labeled slot for this** — build it here later:
- A compact **"Your Side Bets" card** near the top of the **Upcoming** section, showing your 🪙 token balance, number of open (unsettled) bets, and a link to the Side Bets leaderboard. Hidden if the tournament isn't bettable (no set-but-unstarted matches) or the user has no involvement.
- On each card in the **Upcoming** carousel (matches that are `ready`, not yet `started`), a **"Bet" affordance** that opens the bet-slip sheet. This is the primary entry point — you bet on matches that are about to start.
- Once a match is `started`, its bets are locked; the card flips to showing the pool result live as it settles.
- The redesign should reserve this vertical space and (in code) leave a clear `{/* SIDE BETS SLOT — see docs/side-bets.md */}` marker where the card mounts, so wiring it in later is a drop-in.
## Implementation notes for a fresh agent (build this cold)
Context: worktree `social` branch; changes are typically left uncommitted for review. Read `docs/side-bets.md` (this file) fully first, then:
- **Server fns:** follow the `createServerFn(...).validator(zod).middleware([...]).handler(...)` + `toServerResult` pattern. Player-facing (non-admin) fns use `.middleware([superTokensFunctionMiddleware, serverFnLoggingMiddleware])` and resolve the caller via `getPlayerByAuthId(context.userAuthId)` — copy `toggleMatchReaction` in `src/features/matches/server.ts` as the template. All DB access is server-side via `pbAdmin`; collections have null rules.
- **Settlement:** hook into `finalizeMatch` in `src/features/matches/server.ts` (the single match-finalization path — admin `endMatch` and player `confirmMatchScore` both call it). When a match finalizes, settle its Winner pool (integer largest-remainder split + house floor) and the OT long shots. Emit an SSE event so balances/leaderboard update live.
- **Bet open/close** is derived from match `status`: bettable while `ready`, locked at `started` (identical trigger to score reporting).
- **Migration:** mimic an existing `pb_migrations/*_created_*.js` (e.g. `1784007613_created_predictions.js`) for the new `bets` collection — plain `migrate((app)=>{...})` with an up + down. Teams collection id `pbc_1568971955`, players `pbc_3072146508`, matches `pbc_2541054544`, tournaments `pbc_340646327`.
- **Client data:** use the `useServerQuery` / `useServerMutation` hooks (`src/lib/tanstack-query/hooks/*`) and the query-key convention seen in `src/features/predictions/queries.ts`. Add the new event type to `src/lib/events/emitter.ts` + handle it in `src/hooks/use-server-events.ts` (invalidate side-bets keys, mirroring how `match`/`reaction` are handled).
- **Leaderboard UI:** mirror `src/features/predictions/components/prediction-leaderboard.tsx` and its route `src/app/routes/_authed/tournaments/$id.predictions.tsx` for a `$id.side-bets.tsx` route.
- **Toasts:** `import { toast } from "@/lib/sonner"``toast.success` / `toast.error` only.
- **New feature dir:** `src/features/side-bets/{types.ts, server.ts, queries.ts, utils.ts, components/}` — keep pure logic (pool split, house floor, OT payout) in `utils.ts` with unit-testable functions, like `src/features/predictions/utils.ts`.
- **Design system:** Mantine 8 + the app theme; press feedback via the `flxn-press` theme; tasteful motion only (iOS-like easing, 120350ms) — match neighboring components, don't invent a new look.
## Tunables to revisit after first live use
- House floor multiplier (default **1.5×**).
- OT multipliers (**2× / 4× / 8×**).
- Comeback token amount (**1**).
- Whether an all-agreed pool refunds or voids.