Exploratory: Weighted buckets manual mode - #629
Draft
noahm wants to merge 9 commits into
Draft
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Bucket bounds were bare numeric fields while the main Lvl Min/Max pair had caret buttons walking through the lvls a game actually has. Rather than reimplement that, the input is now its own component that both use, so they behave identically by construction. `NudgableLvlInput` takes the neighbouring lvls as props, leaving each caller to decide what "neighbouring" means. The main range keeps its existing rule that the two bounds can't collide; a bucket's bounds are allowed to meet, since a single-lvl bucket is a normal thing to want, but stepping stops there so the buttons can never invert a bucket. Typing can still invert one, which shows up as an invalid row the same way an out-of-range lvl always has. Typed values now follow the main range's rule too: only a lvl the game has gets committed, and blurring reverts anything else. That replaces snap-to-nearest, which silently moved a bound to a value nobody typed. The rows are one CSS grid with `display: contents` on each row instead of a grid per row. The steppers size themselves, and per-row grids only line their columns up when every column is a fixed width, so the FROM / TO / WEIGHT headers had drifted off the controls they label. Also drops a stray `console.log` from the input's change handler.
Each editor had its own Collapse, so switching modes ran two of them at once in opposite directions. Collapse doesn't only resize while closing, it translates its body to Y=-height, so the pair read as the whole section scrolling rather than one panel giving way to another. One Collapse now holds whichever editor is active. Switching between them is instant, since an open Collapse sits at height:auto and simply reflows, while going to and from Off still gets its reveal. Measured across the swap the container holds a steady 283px, where before both containers were animating. Picking the editor straight from `bucketMode` meant closing out of manual mode rendered the weighted editor for the length of the animation, so the panel visibly changed identity on its way out. The choice is now remembered separately and outlives the mode going "none". Collapse's only animation control is `transitionDuration`, so dropping the remaining open/close motion would mean passing 0 there.
One conflict, in src/song-search/index.tsx, where both sides added hooks at the top of SongSearch: main swapped fuzzy-search for a fuzzysort index and added overlayProps to clear the query after the overlay fades, while this branch added the bucket list that pocket picks are filtered against. They're independent, so the resolution keeps both. Main's removal of `fuzzySearch` from the draw store stands — nothing references it now. Everything below the conflict merged cleanly and is already consistent: main's empty-query short circuit sits outside the loop that calls chartIsValid with the bucket list. Verified pocket picks still filter against the buckets on top of main's new search. Searching "dream" with bucketing off (lvl 13-16) offers "SILVER DREAM Exp 15" and "Beautiful Dream Cha 15"; narrowing to a single lvl 16 bucket turns both into "No chart matching filters", and an empty query still returns nothing at all.
The bucket controls sat at the bottom of the general tab while the lvl range they depend on sat near the top, so choosing manual buckets made a control several sections away vanish with nothing tying the two together. The section now sits directly under the draw counts, where the lvl range used to be, and hosts the range itself. It shows for Off and By lvl range, which are the modes that draw from it, and goes away for Manual, which carries its own bounds — and because it is now nested under the picker that changed it, that reads as the section reconfiguring rather than something unrelated disappearing. The range gets a Collapse of its own so it eases out instead of popping. That is a second Collapse in the section, which earlier caused a lurch, but the failure mode there was two of them swapping the same slot and sliding past each other. These are stacked and independent, and the section's height moves monotonically through every transition: off -> auto 151 244 283 308 322 335 auto -> manual 434 419 395 378 364 356 manual -> auto 272 296 313 327 331 335 auto -> off 335 299 220 188 170 151 off -> manual 151 265 306 331 344 356 Only off -> manual runs both at once, closing the range while the editor opens, and the net travel is still one smooth growth. Two supporting style fixes: a rule down the left of the section to bind the picker, range and editor together, and `width: auto` on the lvl fields inside it. They carry caret buttons now, which overflowed the fixed width the drawer's other narrow inputs use and left the two bounds nearly touching.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces the current settings for weighted distributions with a separate mode selector with three options: