Skip to content

feat(stats): add clutch rate stat rendered as a percentage#318

Open
Amund211 wants to merge 1 commit into
mainfrom
add-clutch-rate-stat
Open

feat(stats): add clutch rate stat rendered as a percentage#318
Amund211 wants to merge 1 commit into
mainfrom
add-clutch-rate-stat

Conversation

@Amund211

@Amund211 Amund211 commented Jul 9, 2026

Copy link
Copy Markdown
Owner

What

Adds a clutch rate stat: the win rate in games where you lost your bed. In Bedwars every loss loses your bed, so bed-loss games = bedsLost and clutch wins = bedsLost - losses, giving:

clutchRate = (bedsLost - losses) / bedsLost

Like winrate (added in #316) it is stored/computed as a fraction in [0, 1] but rendered everywhere as a percentage (e.g. 85.4%) via the existing "percentage" format kind: stat cards, sessions table, progression value + milestone + caption, and chart tooltips. On charts the plotted value stays in [0, 1]; only tooltip/label text shows %, and the Y-axis stays numeric.

Progress computation (red-green TDD)

The milestone/progression math is the heart of this change, so it was built test-first:

  • Red: declared the clutchRate key and wrote the progression spec (up/down 5% steps across every gamemode, the clamp-at-100% "reached" case, the no-beds-lost degenerate case, and nextNaturalMilestone cases). 22 failing assertions.
  • Green: implemented the computation until all pass.

Clutch rate joins the QuotientProgression family and reuses winrate's 5%-step, [0, 1]-clamped milestones. Its dividend is synthetic (bedsLost - losses) rather than a single stored stat, so computeQuotientProgression is generalized to take dividend/divisor value accessors instead of stat keys — behavior-preserving for the five existing quotient stats (fkdr/kdr/bblr/wlr/winrate), which now pass (pd) => getStat(pd, gamemode, "…") and keep their original dividend stat as the fallback.

The zero-divisor case (never lost a bed ⇒ no clutch data) has no dividend counter to collapse to, so it reports "no progress" — mirroring winrate when no games are played.

No cache-buster bump

Clutch rate is derived and changes no cached PlayerDataPIT type; the only touched persisted surface is the z.enum(ALL_STAT_KEYS) URL param, which is not cached query data. Chart series, URL schemas, explore checkboxes, and the stat-card grid pick clutch rate up automatically by iterating ALL_STAT_KEYS.

Preview

Open the same page on both and compare. On each, search a player, then select Clutch rate (staging — the new stat) or an existing ratio like WLR (current — to sanity-check the shared quotient path still renders identically). Clutch rate is a new stat, so it only appears on the staging column.

Page Current app (production) Staging (this PR)
Session https://prismoverlay.com/session https://add-clutch-rate-stat.rainbow-ctx.pages.dev/session
History explorer https://prismoverlay.com/history/explore https://add-clutch-rate-stat.rainbow-ctx.pages.dev/history/explore

Staging is the Cloudflare Pages branch preview for add-clutch-rate-stat (tracks the branch head).

Test plan

  • pnpm tsc (app) + pnpm tsc:node (tests) — clean. The Record<StatKey> format map, the no-default label switches, and computeStatProgression's satisfies never force every required edit.
  • pnpm lint:check — clean (oxfmt + oxlint).
  • pnpm test:unit — 807 pass (incl. dedicated clutch rate progression cases: up/down 5% steps across all gamemodes, clamp-at-100%, no-beds-lost, plus nextNaturalMilestone clutch rate cases).
  • pnpm test (browser) — 1008 pass.

🤖 Generated with Claude Code

Add `clutchRate` as a gamemode stat: the win rate in games where you lost
your bed. Every loss loses your bed, so bed-loss games = bedsLost and clutch
wins = bedsLost - losses, giving clutchRate = (bedsLost - losses) / bedsLost.
Like winrate it is a fraction in [0, 1] rendered everywhere as a percentage
(e.g. 85.4%) via the existing "percentage" format kind; on charts the plotted
value stays in [0, 1] and only tooltip/label text shows %.

Wiring:
- keys: promote the commented `clutchRate` placeholder into GAMEMODE_STAT_KEYS
- format: STAT_FORMAT_KIND entry ("percentage") — routes cards, table,
  progression value/milestone, and both chart tooltips through it for free
- index: getStat/computeStat compute (bedsLost - losses) / bedsLost, guarding
  the zero case as a fraction (a loss always increments bedsLost)
- labels: "Clutch rate" / "Clutch"
- progression: clutchRate joins the QuotientProgression family and reuses
  winrate's 5%-step, [0, 1]-clamped milestones. Its dividend is synthetic
  (bedsLost - losses), not a single stored stat, so computeQuotientProgression
  is generalized to take dividend/divisor value accessors instead of stat keys
  (behavior-preserving for the five existing quotient stats). The zero-divisor
  "collapses to the dividend counter" fallback takes a nullable stat; clutchRate
  passes null (no counter for a synthetic dividend) and reports no progress,
  mirroring winrate when no games are played.
- session page: isLinearStat, getRelatedStats (bedsLost, losses), the
  close-to-all-time set, and a bespoke ProgressionCaption case (% for the
  quotient, plain counts for clutch wins/day and beds lost/day)

Chart series, URL schemas, explore checkboxes, and the stat-card grid pick
clutchRate up automatically by iterating ALL_STAT_KEYS. No cache-buster bump:
clutchRate is derived and touches no cached PlayerDataPIT type.

Progress computation built red-green TDD: dedicated clutch rate progression
cases (up/down 5% steps across all gamemodes, the clamp-at-100% reached case,
and the no-beds-lost degenerate case) plus nextNaturalMilestone cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 9, 2026 18:16
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying rainbow with  Cloudflare Pages  Cloudflare Pages

Latest commit: 608c6dc
Status: ✅  Deploy successful!
Preview URL: https://6e229bb5.rainbow-ctx.pages.dev
Branch Preview URL: https://add-clutch-rate-stat.rainbow-ctx.pages.dev

View logs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new derived Bedwars stat, clutch rate ((bedsLost - losses) / bedsLost), and integrates it across formatting, labels, session UI, and progression/milestone projection (including generalized quotient progression to support synthetic dividends).

Changes:

  • Introduces clutchRate as a new StatKey, computes it from bedsLost and losses, and renders it using the existing "percentage" stat format kind.
  • Generalizes computeQuotientProgression to accept dividend/divisor value accessors (enabling synthetic dividend stats like clutch rate) and handles the zero-divisor/no-data case for clutch rate.
  • Adds comprehensive unit tests for clutch rate progression behavior and milestone selection.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/stats/progression.unit.test.ts Adds clutch-rate progression and milestone unit tests across all gamemodes.
src/stats/progression.ts Extends milestone strategy for clutch rate and refactors quotient progression to support accessor-based dividends/divisors with a synthetic-dividend degenerate-case path.
src/stats/labels.ts Adds full/short labels for clutchRate.
src/stats/keys.ts Adds clutchRate to the exported stat key set.
src/stats/index.ts Implements getStat/computeStat logic for clutchRate.
src/stats/format.ts Marks clutchRate as "percentage" for consistent rendering.
src/routes/session/$uuid.tsx Wires clutchRate into session stat selection, related-stats list, and progression caption rendering.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Amund211 added a commit that referenced this pull request Jul 10, 2026
DeltaTag decided good/bad from a per-call goodIfPositive flag, a
hand-maintained inverse of the app's isBadStat semantics. Take a `stat`
instead and route the decision through getTrendSentiment/getTrendDirection
(#318), so the delta's colour follows the same good/bad rules as the rest
of the app and the caller no longer encodes which way is "good".

Make it tri-state: an exactly-flat delta now renders neutral (muted, flat
arrow) instead of a green "+0.00". Colour follows sentiment; the arrow
follows raw direction.

The sentiment→colour map is typed against a ThemeColor union derived from
the MUI Palette, so palette paths like "success.main" are typo-checked at
compile time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants