← All decisions

Two-ahead tier queue and a next-tier UI swatch, matching the genre's standard 'next fruit' preview

merge-gameuipredictabilitytier-queue

Context

Continuing autonomously through the feature-gap backlog identified during the prior session's research (real Suika-style clones commonly show the tier coming up after the one about to drop, not just the current one). Model.current_tier previously only decided the *next* random tier at the exact moment a drop happened -- there was no value representing 'what's coming after that' for a UI to show in advance, since it hadn't been rolled yet. Showing 'next up' requires a genuine two-ahead queue, not just exposing an existing field.

Options considered

Roll and immediately expose a 'next' tier only at render time, without changing Model's own state shape — rejected
Not actually possible without changing when randomness is consumed: the whole reason a next-next value doesn't exist is that today's next_tier_random is only used *at drop time* to decide what comes after -- there is no yet-committed value to peek at beforehand. A real two-ahead queue needs a second persisted field.
Render the next-tier preview as another synthetic Entity via the WebGPU pipeline (extending pe.preview_entity's pattern) inside the game canvas — rejected
The genre convention (and what a player expects) is a small, separate 'next up' indicator outside the play area, not a second circle inside it that could be confused with the actual drop-preview ghost (decision 0030) or a real falling entity. A plain, static-position HTML/CSS swatch (updated on tier change, not physically simulated) matches both the convention and decision 0033's precedent for presentation-only, non-physics UI.
Add Model.next_tier: Int, advance the queue by one on every drop (current_tier <- old next_tier, next_tier <- freshly rolled), and show next_tier as a small color swatch via a new tier.color_css + minimal FFI write — chosen
Minimal, correct state addition -- the queue never skips straight to a brand-new, previously-unseen tier the same frame it drops, since current_tier always comes from a value the player already had visibility into. tier.color_css keeps tier.color as the single source of truth (decision 0020) rather than a second hand-maintained color table; the swatch write follows the exact established non-branching-FFI-write pattern (decision 0016) already used for score/game-over.

Decision

pe.Model gained next_tier: Int. tick's drop branch now sets current_tier: model.next_tier (not a fresh random pick) and next_tier: tier_table.random_droppable(input.next_tier_random, ...) (the fresh pick, one step further ahead than before). pe/tier.gleam gained color_css(tier) -> String, formatting color's existing RGB triplet as a CSS rgb(...) string. game_ffi.mjs gained setNextTierColor (a single non-branching style write), wrapped by game.gleam's set_next_tier_color, called both once in main() (so the swatch isn't empty before the first frame) and every frame in loop (since, unlike the static danger line, next_tier genuinely changes over the game's lifetime). index.html gained a #glemy-next-tier circular swatch. Two existing tests (tick_drop_advances_current_tier_via_random_droppable_test, tick_allows_a_drop_again_once_the_cooldown_has_fully_elapsed_test) were rewritten for the new two-field semantics rather than just mechanically patched, since their actual behavioral guarantees changed.

Verification

155 Erlang / 178 JavaScript gleam test cases passing on both targets (7 new: color_css's own 3 tests, plus 4 for the rewritten tier-queue tests -- one gained an explicit next_tier assertion it couldn't have had before). Extended tools/browser_check.ts with a real-browser assertion that #glemy-next-tier's actual computed backgroundColor reads "rgb(255, 128, 0)" (tier.color_css(1), main()'s starting next_tier) before any drop happens -- confirms the real Gleam-to-FFI-to-DOM path executes at startup, not just that the pure formatting logic is correct in isolation. Re-ran the check after the change: still passes cleanly, zero page errors.

Consequences

A player can now see both what will drop next (the existing hover-preview, decision 0030) and what's coming after that (this decision's swatch), matching standard genre convention for forward planning. The tier queue is now guaranteed never to hand a player a completely fresh, unseen tier with zero warning -- every current_tier was previously shown as next_tier at least one drop earlier. tier.color_css is now available for any future non-WebGPU, tier-colored UI element without introducing a second color table.

References