Reps With Friends β Game Design Document
Version 1.0 Β· 2 Sep 2026 Β· Owner: Alexei Brown (build) + Ben (design/vision) Companion doc: docs/25_DESIGN_BRIEF.md β the 2-page handout. This is the bible.
What this document is. The complete game design of Reps With Friends as it exists today β a working system, not a concept deck. Every mechanic described here is implemented and tested somewhere in this repo, and every section says where. Where the design is genuinely undecided (five founder-level decisions), this document says so honestly and lays out both sides.
How to run everything in this repo (a reader who has never seen it):
bun serve.ts # β http://localhost:4173
| Route | What it is |
|---|---|
/figma-app | The offline test app β Ben's full design, every screen, running our real engine (lane F4) |
/styles | Five-theme design exploration, side-by-side gallery |
/system | Complete dissemination: tokens, 16 components, 36 elements AβG with status |
/wiki | This documentation as browsable pages (Game Rules, App Screens, Bots, Verification, Avatars, Ops, Design, Status) |
/atelier | Outfit Atelier β avatar garment inspection tool (x-ray, seams, poses) |
/demo, /debug, /hub, /connect, /slack, /avatars | 90-second match replay Β· live bot simulator Β· ops console Β· WhatsApp linking Β· Slack setup Β· avatar playground |
The same content is publicly deployed at rwf.qalarc.com.
Source documents: Ben's original brief (summarised in docs/01_BUSINESS_ANALYSIS.md Β§2) Β· the founder-decision audit of his production Figma (figma/notes/analysis.md) Β· the element backlog (docs/07) Β· our MVP design spec (docs/13) Β· the wiki's own rules chapter (apps/wiki/game.html). Code references: packages/game-core (the TypeScript spec), apps/figma-app/engine.js (faithful browser port), apps/figma-app/daily.js (play-day/temporal layer), packages/bot-core (the chat bots).
1. The fantasy
Training stops being something you grind alone.
The target user is a 35β50 year old who used to be fit, isn't anymore, and has a group chat full of mates in exactly the same position. Every fitness app on earth sells them the same lonely loop: open app β log workout β stare at your own graph β feel vaguely guilty β churn by week three.
RWF replaces that with your mates. The gym session becomes a fixture of the group chat β with standings, deadlines, comebacks, theft, taunts and a charity pot. You are never training alone; someone is always ahead of you or coming for you. The game's promise, mechanically enforced (Β§4): everyone has a genuine shot until the last set.
Three load-bearing ideas arrived fully formed in Ben's original brief (docs/01 Β§2) and have survived every design pass unchanged:
- The "300" format β a real training-ground game Ben brought with him:<br>the group agrees on exercises, then any reps, any order, any mix, first to<br>300 total. Self-balancing (you choose your own mix), zero setup friction,<br>instantly understandable in one chat message. The name does half the<br>marketing ("tonight we do a 300" needs no explanation to anyone who has<br>ever seen <em>300</em>).
- The handicap system β effort and consistency compete, not raw fitness.<br>A couch player's push-up is <em>worth more</em> than an athlete's. This is the<br>moat: golf proved decades ago that handicaps keep weak players paying;<br>nobody in fitness does this well. It is also the hardest thing to get<br>right β which is why it's defensible (Β§4).
- Taunting as a feature, charity as the stakes. Banter is the content<br>engine; filming a mate mid-set is the ad. The pot gives trash talk real<br>consequence without gambling: the winner directs the pot to charity β<br>never receives it (Nico's idea, adopted; legal structure pending,<br>docs/01 Β§5). Losing money to a mate is a grudge; losing it to charity is a<br>story.
Everything else in this document is those three ideas, elaborated.
2. The core loop
ββ PREP ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β crew picks play days + exercises β match created β
β (waiting room + crew code) Β· bot: `rwf new` Β· app: create-battle screen β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
ββ EVERY PLAY DAY (e.g. Mon/Wed/Fri) β loops βββββββββββββββββββββββββββββββββ
β β
β log reps ββββΆ live standings ββββΆ deadline pressure βββΆ day closes β
β (chat or app) effort-adjusted DZ1 ββΆ DZ2 ββΆ DZ3 21:00 AEST β
β comeback armed β€3h β€1h β€30min β β
β βΌ β
β DAILY WINNER + RECAP β "YOU WON TUESDAY" Β· MOMENTS Β· next day opens β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β at any moment: the first RAW total to reach the targetβ¦
βΌ
ββ FINALE βββββββββββββββββββββββββββββββ ββ SEASON Β· 4 weeks βββββββββββββ
β match CLOSES Β· closure bonus +15 β β 3/2/1 points Β· MVP +1 β
β RESULT CARD + CHARITY POT β ββββββΆ β champion Β· A/B swap β
β (the winner directs the pot) β β streaks Β· forgiveness $2 β
βββββββββββββββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββββββ
β
βΌ
REMATCH β one tap, same crew, fresh pot βββΆ back to PREP
Each step, mapped to where it lives:
| Step | Where it's implemented (screen / bot command / engine) |
|---|---|
| Group picks play days + exercises | App: create-battle screen (/figma-app, shot 10-create-battle.png) β config.playDays; bot: rwf new [target] (default 300) announces exercises + play days (packages/bot-core/src/bus.ts) |
| Match created | createMatch() β apps/figma-app/engine.js / packages/game-core/src/match.ts; waiting room with 5-char crew code (no 0/O/1/I) + QR/invite (18-waiting-room.png); bot link <code> binds the chat |
| Log reps from chat | log pushups 25! β trailing ! = camera-verified; chains applyComeback β closure check (bus.ts cmdLog) |
| Log reps from app | Log sheet: bottom sheet, preset chips 5/10/20/30/50, camera toggle (27-log-sheet.png, 57-camera-note.png) |
| Live standings (effort-adjusted) | standings() in engine β raw reps, adjusted score, progress %, verified %; app battle screen (24-battle-live.png); bot s replies with medal bars + β‘ comeback markers + π spectator count |
| Deadline pressure (danger zone) | apps/figma-app/daily.js dangerLevel / dzCopy / deadlineClock β 1-second app ticker; DZ1/2/3 chrome (Β§3.3) |
| Daily winner + recap | closeDay / settleDay / recapFor in daily.js β the day auto-closes at the 21:00 Australia/Sydney deadline, winner recorded to dailyHistory, recap card renders (d48-recap-you-won.png, d59-recap-sam-took.png) |
| Match closes at target | logReps() β the first log taking a player's raw total to β₯ target flips status to complete, stamps closedBy, logging locks |
| Result + charity pot | finalStandings() (closure bonus applied) β result screen (38-39-result-final.png), pot screens (41-43-result-pot*.png); engine pot.ts; bot result writes a 1200Γ675 SVG share card, pot 500 = "$5.00 banked. Winner picks where it goes." |
| Season arc | packages/game-core/src/season.ts β recordMatch 3/2/1+MVP, seasonLadder, endSeason; app ladder screen (48-49-season-ladder.png); bot season new / ladder |
| Rematch | One tap in app (45-46-rematch-live.png); bot rematch / again / runitback β same roster/rules, fresh pot |
The loop is deliberately chat-first: every broadcast (standings, result cards, taunts) is an ad seen by the whole group chat; spectators convert via watch <code> (docs/01 Β§2 β "the second moat").
3. The battles
This is the founder's explicit ask: map out the battles. There are five battle forms β three shipped, one designed, one genuinely open.
3.1 The Classic 300 (shipped β the canonical form)
- Setup: the crew agrees on 2β4 exercises and a play schedule (2β3<br>days/week β anti-burnout by design; "rest day, never *missed day*").<br>Default target 300 raw reps; a "Light" match is 150 (<code>new 150</code>).
- Play: any reps, any exercise from the agreed set, any order, any mix,<br>from the chat or the app. Reps can be camera-verified (<code>log pushups 25!</code>).
- Win condition β the twist that IS the product: the first player to hit<br>the <strong>raw</strong> target <em>closes</em> the match (logging locks instantly), but the<br>winner is the highest <strong>effort-adjusted</strong> score at closure. The closer gets<br>+15. An athlete can close on raw reps and still lose to a couch player who<br>moved less but earned more per rep.
entry value = reps Γ tierMultiplier Γ (comeback? Γ1.2) Γ (lightning? Γ3)
match winner = max(Ξ£ entry values), +15 to the player who closed
Worked match (real numbers, from the live bot transcript β wiki β Bots): Target 150. Ben (couch Γ1.5) logs 40 push-ups β 60 adj. Nico (athlete Γ0.85) logs 80 squats β 68 adj. Ben is now >30% behind on raw (40 vs 80 β gap 50%) β comeback armed β‘. Ben logs 110 push-ups: 110 Γ 1.5 Γ 1.2 = 198, and his raw (150) hits the target β match closes, +15 closure bonus. Final: Ben 273 (60 + 198 + 15), Nico 68. Ben closed and won β but against a fitter closer, the upset is live: if Nico had also finished, the adjusted column decides, not the clock.
3.2 Fast Battle vs Custom Battle (the two creation paths)
Ben's Figma defines both; the app ships the creation flow, the bots ship the one-line version.
| Fast Battle | Custom Battle | |
|---|---|---|
| Setup | One screen: name, days, exercise pack, target tier | 8-step wizard: schedule & clock β format β exercises & scoring β power-ups β stakes β review ("THE RULES, IN PLAIN ENGLISH") β create |
| Use it when | 95% of matches β Tuesday-night energy | Corporate leagues, charity specials, weird stakes |
| Where | /figma-app create flow (Figma FLOW-02/03); bot new [target] is the chat-native fast path | Designed in full fidelity in Ben's Figma; rules lock at start, everyone accepts the summary before joining |
| Stakes step | Defaults (bragging) | Bragging Β· dares Β· charity Β· cash-18+ (cash feature-flagged OFF pending legal, Β§6) |
3.3 Daily-rhythm multi-day battles (shipped in the app β the hybrid, see 3.5)
- Setup: a battle spans days β e.g. 2 weeks, active Mon/Wed/Fri. The group<br>shares one clock: every play day ends <strong>21:00 Australia/Sydney</strong> (each player<br>sees the local equivalent β the dual clock: "ends 9:00 PM AEST Β· 7:00 PM for<br>you"). Time Freeze (+30 min, Β§5) is the only thing that moves a deadline.
- Play: log any time during the day. The final three hours escalate the<br><strong>danger zone</strong>: DZ1 β€3h (gold chip) β DZ2 β€1h (orange banner, pulse) β<br>DZ3 β€30min (red banner with live minute count β "DANGER ZONE β 24 MINUTES<br>LEFT" β heartbeat on the timer, full screen wash, "LOG NOW"). Layout never<br>changes; only colour and the timer escalate (motion law, docs/25).
- Win condition: at the deadline the day closes itself exactly once β<br>logging locks, the day's winner (highest adjusted among <em>that day's</em> entries<br>only) is recorded to <code>dailyHistory</code>, a recap renders, and the next play day<br>begins. Tie-break: adjusted β raw reps β earliest first entry. A day nobody<br>logs closes as "NOBODY LOGGED. BRUTAL DAY." (streaks survive; pride doesn't).<br>Absences never chain-generate a pile of brutal days β only the boundary day<br>auto-closes empty.
- The recap card (FLOW-07): "YOU WON TUESDAY" / "SAM TOOK TUESDAY" Β· your<br>line ("You finished 2nd at 92% β 10 RUF short.") Β· the sting ("You led until<br>7:04 PM. Tomorrow, finish the job.") Β· nemesis tease Β· MOMENTS strip<br>(biggest set, comeback claimed, clean sweep, lead change) Β· tomorrow's<br>stakes in your timezone.
-
Where: all of it is
apps/figma-app/daily.js(isPlayDay /<br>deadlineFor / dangerLevel / closeDay / settleDay / recapFor), proven by<br><code>e2e-daily.mjs</code> which time-travels the whole ramp. Screens: <code>d13</code> calm β<br><code>d19</code> DZ1 β <code>d23</code> DZ2 β <code>d30</code> DZ3 β <code>d42</code> close β <code>d48/d59</code> recaps.
Worked day (numbers via the documented formula): You (casual Γ1.25) log 40 push-ups at 7 AM β 50 adj. Sam (fit Γ1.0) logs 60 squats at 6:30 PM β 60 adj. DZ2 fires at 8 PM. You log 20 push-ups at 8:20 β you're >30% behind on raw (40 vs 60), comeback armed β 20 Γ 1.25 Γ 1.2 = 30 β You 80, Sam 60. Day closes at 9 PM: "YOU WON TUESDAY" Β· "You beat Sam by 20 RUF today. They're coming for you." Β· pot grew $3 since yesterday's close Β· "NEXT BATTLE DAY: WEDNESDAY β logs open now".
3.4 Crew vs Crew (shipped in bots; corporate form designed)
- Setup: two group chats, one match. Crew A's captain runs<br><strong><code>challenge <CREW-XXXX></code></strong>, crew B accepts (<code>challenge accept</code>). Corporate<br>variant in Ben's Figma: dept-vs-dept / office-vs-office with a<br><strong>contribution floor</strong> (team score only counts in full once every member<br>clears ~40 RUF β nobody rides the bench) and an <strong>Assist Boost</strong> send CTA.
- Play: identical to the classic 300, standings merged across both chats;<br>spectators in either chat see the combined board.
- Win condition: highest cumulative adjusted crew score at closure (bot<br><code>s</code> shows both crests); employer-funded charity pot is the cleanest stakes<br>structure for this form (docs/08 Β§6).
-
Where: bot
challenge+watch(bus.ts, live today); team battles +<br>contribution floor designed in Figma (V1.x); corporate surfaces seeded in<br>the <code>/hub</code> console (aggregate-only, kβ₯5 suppression).
3.5 The rhythm decision β his model vs ours, honestly (OPEN, Β§9 D3)
This is the one genuine structural fork left in the battle design, and it deserves a straight presentation rather than a dodge:
-
Ben's model (his Figma, built as designed in
/figma-app): multi-day<br>battles, <em>personalised daily targets</em> in RUF β Light 60 / Solid 120 / Hero<br>200, picked privately when joining β and the daily winner is whoever hits<br>the highest completion-% of their own target. Every level can win the day;<br>ability differences are hidden rather than spotlighted. Retention thesis:<br>losers get a fresh start tomorrow, so the loop never feels over. - Our model (the bots / original engine): one shared target (300), one<br>match, first-to-close finale, handicap multipliers as the visible<br>spectacle β the standings are a running argument about who's really trying.
- What's actually shipped is the hybrid, and that's the honest answer:<br>the app runs Ben's rhythm (play days, nightly deadline, DZ ramp, daily<br>winners, recaps) on top of our engine (shared target, tier multipliers,<br>comeback, closure finale, charity pot). Daily winners give everyone a<br>fresh start tomorrow; the 300 closure gives the match a real finale.
- Recommendation: keep the hybrid, and resolve the last sub-question β<br>shared 300 target vs personalised RUF targets β at the founder call with<br>both variants playable side by side (Β§9).
4. The handicap system (the moat)
Thesis, verbatim from the code comment: effort and consistency compete, not raw fitness.
4.1 Tier multipliers (v1 β live)
Chosen at onboarding (self-set, socially policed), applied to every rep:
| Tier | Multiplier | 100 raw reps score as | Expected weekly volume (anti-sandbag baseline) |
|---|---|---|---|
| Couch | Γ1.5 | 150 | 150 reps |
| Casual | Γ1.25 | 125 | 250 reps |
| Fit | Γ1.0 | 100 | 400 reps |
| Athlete | Γ0.85 | 85 | 550 reps |
TIER_MULTIPLIERS β apps/figma-app/engine.js / packages/game-core/src/handicap.ts; expected volumes: TIER_EXPECTED_WEEKLY_REPS in baseline.ts.
4.2 The comeback multiplier Γ1.2 β "everyone has a shot", mechanically enforced
More than 30% behind the leader in raw reps while the match is live β your next entry is tagged Γ1.2. Once per player per match. Eligibility recomputes on every log; bot standings mark eligible players β‘ so the group can see it coming. The boost is tagged onto the entry at log time, not a global multiplier β so it composes (a comeback entry inside a Lightning window scores Γ1.2 Γ3 = Γ3.6, Β§5).
COMEBACK_THRESHOLD = 0.3, COMEBACK_MULTIPLIER = 1.2 β comeback.ts / engine.js comebackEligible / applyComeback.
4.3 The closure bonus +15
The player whose raw total hits the target first gets +15 adjusted points β urgency and glory for the closer, but deliberately small enough that real effort still beats raw speed. A closer who sandbagged their tier wins nothing: +15 doesn't rescue a thin adjusted column. (Found by the test suite β a bug in the original bonus placement was caught and fixed before it ever shipped.)
CLOSURE_BONUS = 15 β engine.js / match.ts finalStandings / winner.
4.4 Effort handicap v2 β %HRR blend (engine-ready, waiting on strap data)
When an entry carries a measured avgHrrPct (heart-rate reserve %, Karvonen, from a BLE strap) and the player has a learned baseline, the multiplier blends live evidence with the declared tier:
effortMultiplier = 0.7 Γ (avgHrrPct / baselineHrrPct) + 0.3 Γ tierMultiplier
70% measured, 30% declared. This makes effort itself the score β a couch player red-lining scores like a champion; an athlete phoning it in doesn't. The blend math is implemented and tested; it activates the day heart-rate straps land (docs/05 phasing).
HRR_WEIGHT = 0.7 β handicap.ts / engine.js effortMultiplier.
4.5 Anti-sandbagging β baseline drift (engine-ready)
Self-reported tiers have a loophole: a ringer declares "couch" and farms the Γ1.5. Two drift correctors close it (baseline.ts, gated to at most one tier step per week):
-
HR path:
baselineHrrPctdrifts toward your rolling measured average,<br>max <strong>10% per update</strong> β you can't tank your baseline in a weekend. - Volume path: consistently logging >1.3Γ your tier's expected weekly<br>volume drifts your tier one step <em>fitter</em>. The "couch" player doing 5Γ<br>everyone's reps quietly becomes a casual.
4.6 The couch-beats-athlete worked example
The full thesis in one table (target 150, from the bot transcript):
| Ben β couch Γ1.5 | Nico β athlete Γ0.85 | |
|---|---|---|
| Log 1 | 40 push-ups β 60.0 adj | β |
| Log 2 | β | 80 squats β 68.0 adj |
| State | 40 raw vs 80 raw β 50% behind β comeback armed β‘ | leading |
| Log 3 | 110 push-ups β 110Γ1.5Γ1.2 = 198.0 β raw 150 β CLOSES | β |
| Final | 273.0 (60+198+15 closure) β WINNER | 68.0 |
The athlete did the bigger single set and led most of the match. The adjusted column says: Ben tried harder per rep, from further back, when it counted. That sentence is the product.
5. Power-ups & the economy
Four cards, live in the app (/figma-app, Figma FLOW-05; engine.js β figma-app only, deliberately not yet in game-core). All activations are pure functions returning a new match state + a result card; every activation is audit-logged to powerLog.
| Card | Rarity (drop %) | Exact effect |
|---|---|---|
| β‘ Lightning Round | legendary (5%) | Reps count Γ3 for 10 minutes. One activation per player per match β the window can expire, the once-per-match flag never resets. |
| π‘ Rep Steal | epic (15%) | Take 10% of the leading rival's raw reps (floor, min 1 while they're above zero), instantly. Both raw ledgers move; entries are logged as ledger transfers, so tier multipliers apply β a couch thief gains more adjusted than the athlete victim loses. |
| π‘ Shield | common (50%) | Blocks one Rep Steal against you, then breaks. A blocked steal never fires: the shield is consumed, the thief keeps their card β the shield buys the block, not a free kill. |
| β± Time Freeze | rare (30%) | +30 minutes on the play-day deadline, for everyone. The one card that bends time. |
Drop odds (DROP_ODDS): common 50 / rare 30 / epic 15 / legendary 5 β daily-drop style, one card per battle day in the Figma economy. Rarity is cosmetic in v1: chip colour + drop odds only, no mechanical effect.
The counter-web is the design. Lightning is capped at one activation so it can't be chained. Steal targets only the current leading rival (preview shows exactly what you'd take and whether a shield is up β stealPreview), so you can't snipe someone harmless. Shield makes Steal honest: theft against a prepared leader is a wasted card, so steals happen when someone has actually run away with it β which is exactly when the table wants a correction. Steals never trigger the closure check; only real logged reps close a match.
Stacking rule: comeback and lightning both tag the entry, so they multiply β a couch player's comeback set inside a lightning window is Γ1.5 Γ1.2 Γ3 = Γ5.4 per rep. Rare, loud, and earned from behind. That's the point.
What is deliberately unpriced. Nothing in the current build can be bought: cards are granted (grantPowerUp / daily drop), never sold. Ben's Figma designs the full economy β store packs ($1.99β$4.99), daily loot chest, Reps Pro subscription with 12 inventory slots β under one hard rule we adopt verbatim: "Cosmetic and convenience only β you can't buy the win." Whether any of that ships in the MVP is open decision #4 (Β§9).
6. The social layer
-
Taunts (live). Bot
taunt <name>β AI-generated cheek via the<br>server-side GLM proxy with canned fallback (2s timeout), because taunting<br>is a feature, not abuse: the crew opted in by joining. Proposed: per-crew<br><strong>roast-tier setting</strong> β gentle / standard / feral β gates the AI engine<br>(also the corporate-safe switch, docs/07 G-34). Line that must never move:<br>cheeky, never mean; "rest day", never "missed day"; money screens drop all<br>banter. - MVP vote (live). One vote, best effort not winner, locks on tap.<br>Feeds the season (+1 point) β the second podium, so showing up hard with a<br>bad multiplier still gets recognised.
- Nemesis (live). Auto-detected from head-to-head history: your nemesis<br>is the opponent who has beaten you the most, with β₯2 shared matches and β₯1<br>loss (someone you always beat is prey, not a rival). Bot <code>nemesis</code>; shown<br>on profiles and result cards β every player gets a personal storyline, not<br>just the leaders'. <code>packages/game-core/src/nemesis.ts</code>.
- Photo finish (live). Top two within 5% adjusted β special result<br>card + slow-mo styling. Manufactures shareable drama from close matches.<br><code>photo-finish.ts</code>, <code>PHOTO_FINISH_PCT = 5</code>.
- Monday digest (live). AI-written weekly recap auto-postable to the<br>chat: results, margins, MVPs, pot total, rivalry callout, one-liner. Bot<br><code>digest</code> / alias <code>monday</code>; <code>packages/bot-core/src/digest.ts</code>. Re-opens the<br>loop every week without anyone lifting a finger.
-
Spectators (live).
watch <code>β read-only standings of another crew<br>in your chat; standings show π count. Spectators are the funnel: they see<br>the leaderboard without joining, and joining is one command. - Result cards β the viral artefact (live). Every match end auto-<br>generates a branded 1200Γ675 card (SVG from the bots β <code>/cards/β¦</code>, PNG<br>export from the app). Direction adopted from Ben's file: server-rendered,<br>no health data, pre-written editable message, join link β every result<br>card is an ad with a call to action.
- Drop-cam (proposed). 10-second clip capture during verified sets β<br>Ben's "friends filming friends" content engine (docs/07 C-14).
- The charity pot ritual (live as a pledge ledger). Players chip in<br>(<code>pot 500</code> = $5.00); at result time the <strong>winner directs</strong> the pot to a<br>charity of their choice (<code>designate</code>) β a dedicated picker screen, the one<br>moment the product goes quiet and warm. Pot growth is recapped daily<br>(<code>potDeltaCents</code> β growth since the previous day's close).
- The legal line. No cash ever moves to a winner; the pot is a pledge<br>ledger today and stays one until a proper legal opinion lands (AU<br>state-law raffle/betting classification risk β docs/01 Β§5, docs/08 Β§6).<br><strong>Employer-funded pots are the clean interim structure</strong> β the company<br>puts the money in, the winning crew directs it; nothing to classify.<br>Ben's full cash-wagers suite (KYC, regions, responsible-play limits,<br>separate ledger, never the word "escrow") is designed behind a<br>feature flag and stays there (Β§9, D17).
7. Progression & retention
The retention stack answers the week-3 cliff (docs/01 Β§5) with four layers β each one cheap, each compounding:
- Seasons (live). 4-week series. Every finished match feeds<br><code>recordMatch</code>: 1st +3, 2nd +2, 3rd +1, MVP vote +1. Ladder sorts<br>points β wins β MVPs. At season end: champion crowned (the belt β<br>physical trophies proposed, docs/07 F), and <strong>A/B division swap</strong> β<br>bottom of A β top of B β so mid-table stays meaningful all season.<br><code>season.ts</code>; app <code>48-49-season-ladder.png</code>; bot <code>season ladder</code>.
- Streaks + forgiveness (live). Streaks count played weeks (you chose<br>your days β no streak guilt for resting). A streak about to break can be<br>saved <strong>once per season</strong> by topping the charity pot <strong>β₯ $2</strong> β<br>preserved, not extended. Guilt converts to pot money; no free rides<br>(<code>FORGIVE_MIN_CENTS = 200</code>, <code>forgiveStreak</code>).
- The avatar/creature system β the identity layer (in active build).<br>The founder's tamagotchi concept: your avatar evolves with your<br>training. The reference creature is the dragon β three stages already<br>modelled and rigged: <strong>Hatchling</strong> (round, stub-winged, all eyes) β<br><strong>Fledgling</strong> (wings come in, nub horns, first spikes) β <strong>Elder</strong><br>(<code>site/avatar-styles/dragon2.js</code> β stages driven by a <code>stage</code> param,<br>ready to wire to season/streak milestones). Learned the hard way<br>(documented post-mortem, <code>notes_avatars_investigation.md</code>): creatures are<br>built on a <strong>dedicated creature rig</strong> (<code>site/creature-rig.js</code>, horizontal<br>silhouette β a dragon doesn't squat), never as humans-in-suits. The same<br>pipeline already carries 11 humanoid styles + 12 rigged GLBs across<br>species; the Outfit Atelier (<code>/atelier</code>) stress-tests garments through<br>every extreme exercise pose. Design intent: your creature grows when you<br>show up β the identity layer makes absence <em>visible</em> and progress<br><em>adorable</em>.
- Awards & badges (designed, from Ben's file β adopt). Auto-computed on<br>final results: π most consistent Β· π best comeback Β· β‘ most active Β·<br>πͺ personal best β zero coordination, covers the "effort β winning" value<br>the MVP vote serves. Plus PB detection cards for the non-competitive half<br>(docs/07 G-29).
Non-winner retention is a stated design lane in Ben's architecture ("the loop must create a social ripple even when you lose") and everything above feeds it: comeback, MVP, awards, nemesis, revenge rematch prompts, daily fresh starts.
8. Verification & fairness β the trust ladder
The existential technical problem (docs/01 Β§5): if reps are self-reported the game is a lying contest; if verification is annoying nobody plays. The answer is a ladder, not a gate β every rung is optional and every rung is visible:
| Rung | Mechanism | Status |
|---|---|---|
| 1 Β· Honour | Self-report + socially-policed tier + crew chat visibility (liar gets roasted) | Live β the default |
| 2 Β· Camera | In-browser pose counting (MoveNet; push-ups/squats via angle-threshold state machine). On-device only β no video ever leaves the phone. Entry logs verified:true; standings show each player's verified % | Live in app (apps/web/src/verify/); bot grammar log pushups 25! |
| 3 Β· Heart rate | BLE straps via Web Bluetooth (GATT 0x180D), Karvonen %HRR per set β avgHrrPct on the entry β feeds the v2 handicap blend (Β§4.4) β effort becomes verifiable even when reps aren't | Engine live; strap hardware pending (docs/05) |
| 4 Β· Peers | Long-press any log β challenge (typo / impossible pace / repeated pattern), anonymous, 3 per battle, misuse lowers your trust score; dispute timeline; admin queue | Designed in Ben's Figma (V1) β sequence decision Β§9 |
| 5 Β· Cloud | Apple Watch / HealthKit, WHOOP/Garmin cross-check, recovery-adjusted handicap | Phase 3 (docs/05) |
Server-authoritative everything (Ben's engineering posture, adopted): the server computes all scoring, multipliers and deadlines; the client never decides a danger-zone level. The camera rung is deliberately the positive verification ("honest by design" β show off) with peer challenge as the social backstop, in that order (the sequencing itself is one of the adjacent open calls, Β§9).
9. The five open decisions
Full evidence and the 30-row divergence log: figma/notes/analysis.md (Β§4, Β§6 β the founder-call one-pager). The five that reshape the game:
β Fairness model β the deepest divergence
- Ben's design: personalised daily targets (Light 60 / Solid 120 / Hero<br>200 RUF), winner = completion-% of <em>your own</em> target; tier private.<br>Ability differences hidden β privacy, simple math, "every level can win<br>the day".
- Our build: shared target + tier multipliers; the handicap is the<br>spectacle β effort visible in the numbers.
- Recommended hybrid: personalised targets and our comeback multiplier<br>on top (both "everyone can win" mechanics, one hidden one visible).
- Downstream: standings math, comeback triggers, result cards, the whole<br>adjusted-score narrative reshape around this. Decide first.
β‘ Platform / distribution
- Ben: native app, app-store IAP, push notifications (DZ nudges basically<br>require push).
- Ours (shipped): PWA + WhatsApp/Slack bots day one β no install, <30s<br>cold start, the chat IS the arena. The bots are built and are the<br>differentiation.
- Recommended: staged β bots + PWA now, native when retention is proven.
- Downstream: onboarding (12-screen native flow vs our <20s name+tier),<br>notification strategy, payments.
β’ Battle rhythm
- Ben: daily winners inside multi-day battles + nightly danger zone.
- Ours: single match to 300 with the close-moment finale.
- What's built: the hybrid β daily winners + DZ on top of the 300<br>closure (Β§3.5), running in <code>/figma-app</code> today.
- Recommended: keep the hybrid; settle the shared-target vs personalised-<br>target sub-question with both playable side by side.
- Downstream: if daily rhythm fully wins, seasons become its container<br>and the closure finale becomes the season finale.
β£ Power-ups in the MVP?
- Ben: 4 power-ups + rarity + daily drop + store + Pro inventory slots in<br>the launch gate β the monetisation spine.
- Ours (shipped): the same 4 power-ups, fully mechanised app-side; no<br>store, no prices, drops only.
- Recommended: ship the four cards (they're done and tested), hold the<br>store/Pro economy for post-retention-proof. Never break: "you can't buy<br>the win."
- Downstream: a whole component family (cards, chest, banners) and the<br>Pro paywall design hang on this.
β€ Brand β gold vs lime
-
Ben's file: gold
#FFC821+ purple#8B5CF6+ Anton/Inter, domain<br>reps.fit β "game" energy; he flags the palette as needing sign-off. -
Ours: lime
#c6f32e+ coral#ff5c38+ Space Grotesk, rwf.app β<br>"athletic" energy. - Where it stands: both now exist as complete, switchable themes<br>(<code>/styles</code> β Lime Athletic, Gold Arcade, Sunset Brutalist, Midnight Neon,<br>Forest Retro); the test app ships Anton + Inter <em>and</em> Space Grotesk. This<br>is now a founder-taste call, not a build problem.
-
Downstream: D5/D6 lock β tokens map 1:1 (
figma/notesF3-ready), the<br>Figma variable table converts, the domain registers.
Adjacent decisions already leaning resolved β money at launch (charity-only, wagers flagged; D17) and verification sequence (camera MVP β peer challenges V1; D10) β both with Ben's craft adopted and our shipped work kept.
10. Numbers appendix β every constant, one table
All values verified against the code on 2 Sep 2026. Any change to a number here must update this table, the wiki Game Rules page, and the tests.
| Constant | Value | Where it lives |
|---|---|---|
| Default match target | 300 raw reps ("Light" = 150) | match.ts / engine.js createMatch Β· bot new [target] |
| Tier multipliers | couch Γ1.5 Β· casual Γ1.25 Β· fit Γ1.0 Β· athlete Γ0.85 | TIER_MULTIPLIERS β engine.js / handicap.ts |
| Comeback threshold | >30% behind leader (raw) | COMEBACK_THRESHOLD = 0.3 β comeback.ts / engine.js |
| Comeback multiplier | Γ1.2, once per player per match, tagged on entry | COMEBACK_MULTIPLIER β comeback.ts / engine.js |
| Closure bonus | +15 adjusted, to the closer only | CLOSURE_BONUS β engine.js / match.ts |
| Lightning window | 10 minutes, Γ3 rep value, 1 activation/player/match | LIGHTNING_MS, LIGHTNING_MULTIPLIER β engine.js |
| Rep Steal share | 10% of leading rival's raw (floor, min 1) | STEAL_SHARE = 0.1 β engine.js |
| Time Freeze | +30 min on the deadline | FREEZE_MS β engine.js |
| Power-up drop odds | common 50% Β· rare 30% Β· epic 15% Β· legendary 5% | DROP_ODDS β engine.js |
| Play-day deadline | 21:00 Australia/Sydney (dual clock; freeze extends) | daily.js deadlineFor Β· state.js playDayEndMs |
| Danger zone ramp | DZ1 β€3h gold Β· DZ2 β€1h orange Β· DZ3 β€30min red | daily.js dangerLevel |
| Daily tie-break | adjusted β raw reps β earliest first entry | daily.js dailyStandings |
| HR blend weight | 0.7 measured + 0.3 declared tier (v2) | HRR_WEIGHT β handicap.ts / engine.js |
| Baseline HR drift | max 10% per update | HR_DRIFT_MAX = 0.1 β baseline.ts |
| Sandbag volume ratio | >1.3Γ expected weekly reps β drift one tier fitter (max 1 step/week) | VOLUME_SANDBAG_RATIO = 1.3 β baseline.ts |
| Expected weekly reps/tier | 150 / 250 / 400 / 550 (couchβathlete) | TIER_EXPECTED_WEEKLY_REPS β baseline.ts |
| Photo finish | top two within 5% of leader's adjusted | PHOTO_FINISH_PCT = 5 β photo-finish.ts |
| Season length | 4 weeks | season.ts createSeason |
| Season points | 1st +3 Β· 2nd +2 Β· 3rd +1 Β· MVP +1 | recordMatch β season.ts / engine.js |
| Streak forgiveness | β₯ $2 to pot, once per season, preserves (not extends) | FORGIVE_MIN_CENTS = 200 β season.ts / engine.js |
| Nemesis eligibility | β₯2 shared matches, β₯1 loss | nemesis.ts |
| Result card | 1200Γ675 (16:9), SVG from bots / PNG from app | packages/bot-core/src/card-image.ts |
| Crew code | CREW-XXXXX, 5 chars, alphabet excludes 0/O/1/I (design rule) | docs/13 Β§21 Β· generated in apps/figma-app/state.js crewCode() |
| Bot commands | 15 commands + aliases (s,h,again,runitback,nem,monday) | bus.ts COMMANDS / ALIASES Β· wiki β Bots |
Proof of correctness (run any of these yourself):
cd packages/game-core && bun test # 59 tests, 134 expect() calls β engine spec
cd apps/figma-app && bun test engine.test.js # 23 parity tests: TS vs JS side by side
cd packages/bot-core && bun test # 64 tests β command bus, cards, AI fallback
cd apps/figma-app && bun e2e.mjs && bun e2e-daily.mjs # full app walks, injected time
90+ green across the repo, CI-gated on every push (docs/17).
Cross-links: docs/25_DESIGN_BRIEF.md (design handout) Β· docs/01 (Ben's brief + market) Β· docs/02 (phasing) Β· docs/05 (verification research) Β· docs/07 (element backlog AβG) Β· docs/08 (legal) Β· docs/11 (systems) Β· docs/13 (our MVP spec + 30-row divergence log) Β· docs/17 (progress) Β· figma/notes/analysis.md (the Figma audit + founder-call one-pager) Β· wiki: /wiki/game.html, /wiki/app.html, /wiki/bots.html, /wiki/verification.html, /wiki/avatars.html, /wiki/status.html.