Skip to content

Exploratory: Weighted buckets manual mode - #629

Draft
noahm wants to merge 5 commits into
mainfrom
claude/weighted-buckets-manual-mode-2jhvhk
Draft

Exploratory: Weighted buckets manual mode#629
noahm wants to merge 5 commits into
mainfrom
claude/weighted-buckets-manual-mode-2jhvhk

Conversation

@noahm

@noahm noahm commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Replaces the current settings for weighted distributions with a separate mode selector with three options:

  1. Basic level range (same as current app default behavior)
  2. Bucket per lvl (old version of enabled weighted distributions)
  3. Manual bucket sizes (replaces manual bucket count option)

claude added 5 commits August 11, 2026 22:37
Replaces the `useWeights` boolean with a three-way `bucketMode`
(`none` / `auto` / `manual`) and adds a manual bucket editor where users
author each bucket's start, end, and weight by hand. In manual mode the
top-level Lvl Min/Lvl Max controls go away, since the buckets carry
their own bounds.

The main goal was to add this without growing the draw logic, so the
modes are normalized before the draw ever sees them:

- `src/draw-buckets.ts` owns bucket layout. `getDrawBuckets` turns any
  mode into one `DrawBucket[]` (a lvl range plus a weight), and
  `planDraw` turns that into a `DrawPlan` (a deck plus a min/max per
  bucket). Nothing downstream branches on mode, weights, or lvl bounds.
- `card-draw.ts` now only collects charts per bucket and executes a
  plan. `draw()` dropped from ~190 lines to ~40, and the whole file from
  429 to 307. Difficulty lvl is applied in exactly one place
  (`drawableBucketForChart`) rather than as a raw range check plus a
  separate zero-weight filter.
- Buckets are `{low, high}` ranges everywhere, retiring the
  `number | [low, high]` union and its two matching rules.
- `planDraw` is the seam for the planned flat-count-per-bucket mode: it
  already emits min/max, so only that function needs to learn the new
  strategy.

The draw loop is now constructive (cover minimums, then fill from the
weighted deck) instead of rejection sampling. The old
`do { ... } while (redraw)` could never terminate when a bucket's forced
minimum exceeded the charts available to it, which manual buckets make
trivial to hit; the new loop always terminates and returns a short
drawing instead, which the existing "couldn't draw enough charts" guard
already handles.

Other notes:
- Manual buckets match in list order, first hit wins, so a chart is
  never drawable twice. Overlaps are surfaced with a warning.
- Each manual bucket row shows its actual pool size, so empty or
  over-subscribed buckets are visible before drawing.
- Switching into manual mode seeds from the current layout rather than
  starting empty.
- Saved configs with `useWeights` migrate to the matching `bucketMode`.
- `getAvailableLevels` is now cached per game data file, since bucket
  resolution calls it from render paths.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TLpPRExceU5W4B37UncuFY
SDVX has whole lvls up to 17, then 17.5, then 0.1 steps from 18.0 to
20.9, with no `granularTierResolution` set. The manual editor derived
its step size from that field, so it fell back to 1 and the spinner
jumped 19 -> 20, skipping the entire fractional range.

- Derive the step from the smallest gap between adjacent available lvls
  instead, which covers granular-tier games and inherently fractional
  ones alike.
- Snap a typed bound onto a lvl the game actually has, on blur. Buckets
  match on a plain range so an off-lvl bound was never wrong, but it
  made the bucket's real extent hard to read and accumulated float noise
  while stepping. Replaces `clampValueOnBlur`, since a snapped value is
  always in range.
- Show a bucket's literal bounds as a tooltip in the by-lvl-range editor
  when they differ from its label: a bucket labelled "18" covers
  18.0-18.9 on SDVX, which was invisible before.
maimai stores chart constants (13.7) rather than the lvls players use
("13+"), and the app rendered the raw number everywhere — on cards, in
Lvl Min/Max, and in the new bucket editor. A "+" lvl is a span of
constants, not one value, so this needs more than a display swap.

- New `src/lvl-display.ts` builds a list of `LvlBand`s per game: the
  label a player uses plus the range of internal values behind it. Games
  whose lvls map one to one get single-value bands, so they're unchanged.
  Driven by a new optional `meta.lvlPlusThreshold` rather than hardcoded
  per-game logic; maimai sets 0.6.
- `LvlBoundInput` renders and parses bounds in that notation. Which end
  of a band a bound resolves to depends on whether it opens or closes a
  range, so a bucket from "13" to "13+" covers 13.0 through 13.9 the way
  a player expects. Arrow keys step band to band, Enter commits, Escape
  reverts, and unparseable text is flagged instead of coerced.
- Lvl Min/Max and song cards use the same notation, so a lvl reads the
  same everywhere in the app.
- Tier-drawing games label bounds "T07", matching how the by-lvl-range
  editor already showed them.

Buckets are still plain `{low, high}` numbers — none of this reaches the
draw logic, which continues to match on raw values.

Two fixes found while testing this:

- Escape inside a lvl input closed the entire settings drawer, since the
  keydown propagated up to the Drawer. Cancelling an edit now stays local.
- Cards on tier-drawing games briefly rendered a mix of "T11" and "13":
  bound picking enumerates tiers while a card shows the chart's own lvl,
  and those are different numeric spaces. `getChartLvlBands` keeps them
  apart.

This supersedes the SDVX step-size derivation from the previous commit;
bounds no longer step by a raw increment at all, they step band to band.
This reverts commit 9038266.

maimai's plus lvls are a display convention in the game, but the decimal
chart constants behind them are meaningful in their own right and are
what the app should show and accept. Collapsing 13.0-13.9 into "13" and
"13+" threw that detail away.

So maimai wants the same handling as any other game with non-integer
lvls: raw values, stepped and snapped by the data's own increment. That
is what the preceding commit already does, generically, via the smallest
gap between adjacent available lvls — for maimai that comes out to 0.1,
the same as SDVX.

Reverting takes the two fixes in that commit with it, but both were
fixes to problems it introduced: Escape closing the settings drawer came
from the keydown handling it added, and the T11/13 mixup on tier-game
cards came from routing card lvls through the bound-picking band list.
Neither exists without it.

Also drops `meta.lvlPlusThreshold` from the schema, maimai's data file,
and the maimai import script, since nothing reads it now.
Turning on granular lvls left hand-authored buckets holding whole-lvl
bounds while chart matching switched to sanbai tiers, so a bucket of
"13 to 13" went from every lvl 13 chart to only the few sitting exactly
on 13.00. The by-lvl-range layout already redraws itself across that
toggle; manual buckets now get the same treatment and land on identical
spans (13.00-13.95 on DDR World), because both go through the same
`topOfWholeLvl` helper rather than reimplementing the edge.

Toggling back collapses bounds to whole lvls again, so a whole-lvl
bucket round-trips unchanged.

Adding a bucket now spans the whole lvl it opens on for the same reason:
in granular mode a fresh single-tier bucket matches almost nothing. On
games with inherently fractional lvls this also improves the default —
adding after a bucket ending at 19.4 gives 19.5-19.9 instead of
19.5-19.5.

Pool sizes dip slightly with granular lvls on (304 -> 303 for lvl 13),
which is the pre-existing rule that granular mode skips charts with no
sanbai tier, not an artifact of the rescale.
@vercel

vercel Bot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ddr-tools Ready Ready Preview Aug 12, 2026 12:51am

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