Consolidated edition · Pre-production
Fizz Frenzy
Full Specification
A 4-player grid brawler for iOS and Android. Anthropomorphic critters called Chuggers drop shaken soda cans and run. Two seconds later the can bursts, spraying foam down four lanes. Last Chugger fizzing wins.
Fizz Frenzy — Full Specification¶
Version 1.0 — Pre-production
A 4-player grid brawler for iOS and Android. Anthropomorphic critters called Chuggers drop shaken soda cans and run. Two seconds later the can bursts, spraying foam down four lanes. Last Chugger fizzing wins.
This document is the single source of truth. It is organised as five parts, delimited so that the set can be split into standalone documents at any time.
Component Documents¶
| Part | Document | Source file |
|---|---|---|
| Part 1 | Gameplay Rules & Mechanics | fizz-frenzy-gameplay-rules.md |
| Part 2 | Application Framework & Flow | fizz-frenzy-framework-and-flow.md |
| Part 3 | Design Style Guide | fizz-frenzy-style-guide.md |
| Part 4 | Visual Assets & Design Architecture | fizz-frenzy-asset-architecture.md |
| Part 5 | Economy & Progression | fizz-frenzy-economics.md |
The order is deliberate. Parts 1 and 2 are the technical half — what the simulation does, and what the application around it does. Parts 3 and 4 are the production half — what it looks like, and how the assets that make it look that way are cut and shipped. Reading straight through crosses from build to craft exactly once, instead of alternating. Part 5 stands apart from both: the economy — what a player earns, owns and spends across their lifetime on the game.
How This Document Splits¶
Each part is a level-1 heading. Nothing else in the document uses level 1. To divide the specification again:
- Split on each standalone HTML-comment line containing the words PART BREAK. Match whole lines, not the phrase — this paragraph mentions it in prose, and only the comment lines are delimiters. Do not split on the heading itself: Part 4's Appendix A contains worked map files whose ASCII grids begin with hash characters inside fenced code blocks, and a naive heading splitter will cut a map example in half
- Discard this front matter, which is everything above the first delimiter line
- Restore each part's original heading by replacing its "Part N" title line with the "FIZZ FRENZY" title shown in the table above
- Save under the source filename in the table above
Within each part, headings begin at level 2 and nest from there. Section numbers are local to each part, which is what makes the split lossless.
Reading section references¶
- A bare reference — §6.5 — points to a section of the same part
- A reference naming a document — Gameplay Rules §9.1 — points into another part
This convention survives both compilation and splitting.
Who Reads What¶
| Role | Read first | Then |
|---|---|---|
| Gameplay programmer | Part 1, entire | Part 2 §§7–8 |
| Netcode programmer | Part 2 §7 | Part 1 §13 |
| Server programmer | Part 2 §§6–7, 11, 17 | Part 1 §13, Part 5 §§5, 7 |
| Client programmer | Part 2 §§3–5, 13 | Part 4 §9, Part 2 §17.11, Part 5 §6 |
| Level designer | Part 4 Appendix A | Part 1 §§3, 8 |
| Artist, character | Part 3 §§1–6 | Part 4 §§2–4, 7 |
| Artist, environment | Part 3 §§1–3, 7–9 | Part 4 §§5, 8 + Appendix A |
| UI designer | Part 3 §§4, 11 | Part 2 §5, §17.12, Part 5 §6 |
| Producer | This front matter | Part 4 §9 + Appendix B, Part 2 §1, Part 5 entire |
Part 1 is the document a programmer implements the game from. Part 2 is the application around it; Parts 3 and 4 are production references.
Ownership¶
Where two parts describe the same subject, this table decides which one governs.
| Subject | Owner |
|---|---|
| Simulation rules, movement, collision, blast, pickup effects and caps, balance constants, resolution order | Part 1 — Gameplay Rules |
| Screens, navigation, state machines, netcode, session model, persistence, player count, grid dimensions | Part 2 — Framework & Flow |
| Leagues, leaderboards, weekly progression, club allocation | Part 2 — Framework & Flow |
| Colour values, silhouette rules, character design, arena mood, VFX budget, audio | Part 3 — Style Guide |
| Asset slots, naming, canvas sizes, atlases, draw order, map file format, validation, pickup appearance | Part 4 — Asset Architecture |
| Currency, XP, prices, yields, level curve, rollover payout policy | Part 5 — Economy & Progression |
Two spans worth stating explicitly:
- HUD — Part 3 owns appearance, Part 2 owns structure and position
- Pickups — Part 1 owns effect, cap and drop weight; Part 4 owns silhouette, animation and legibility
- Leagues — Part 2 owns the ladder, the rollover and the screen structure; Part 3 §3.4 owns tier and band colour; Part 4 §6.6 owns the slots. Badges are Core, non-variant — no pack, skin or flavour may override one
Locked Decisions¶
Every decision taken across the five parts, in one table. Each is expanded in the part cited. Numbers are stable identifiers referenced from other project documents; they are grouped by topic, not ordered.
Product¶
| # | Decision | Where |
|---|---|---|
| 1 | Title Fizz Frenzy; player characters are Chuggers; in-world soda brand is Frenzy Cola | P3 §2 |
| 2 | Launch arenas: Diner and Beach Kiosk | P3 §§8–9 |
| 3 | Launch cast: four Chuggers — Rax, Puff, Nim, Bolt | P3 §5 |
| 4 | 4 players. Six is a costed version 2 refactor, not a deferred switch | P2 §1.1 |
| 5 | Grid is 11 columns × 13 rows, portrait, fixed for every map | P2 §1.2, P1 §3.2 |
| 6 | Platforms: iOS and Android, phone and tablet. Unity + Nakama | P2 §0 |
The board¶
| # | Decision | Where |
|---|---|---|
| 7 | The map file defines every tile — solids, destructibles, spawns, hazards. No procedural fill, no engine-placed lattice, no forced corners | P1 §3.1 |
| 8 | Corridor exit rule — every walkable tile has a perpendicular exit within 3 tiles | P1 §3.4, P4 §A.7.16 |
| 9 | players is spawn capacity, not match size. A match may seat fewer, never more |
P4 §A.4 |
| 10 | Fewer players than spawns → maximum pairwise separation subset, reading-order tie-break | P1 §3.3 |
| 11 | Every round replays the identical authored board. No reseeding | P1 §11 |
| 12 | Maps: bundled baseline, server adds, additive only. IDs permanent and namespaced; server maps validated on upload | P4 §10.2, P2 §10 |
| 13 | Map capacity is a ceiling. Duels run on 4-spawn maps or on a map declaring capacity 2; a map is never offered to more players than it declares | P4 §10.3, P1 §3.3 |
| 14 | Version 1 supports one hazard type — a periodically lethal tile (H) |
P1 §12.1 |
| 84 | Rendering is deterministic, not just layout. Slot variants are a pure function of tile position, never of the match seed. The RNG seeds the drop table and nothing else | P4 §A.5, §A.8 |
| 85 | An optional second grid block carries floor decals — same 11×13 shape, glyphs a–d, purely cosmetic, absent block means none. One tile carries two facts, so it takes two aligned grids |
P4 §A.3, §A.5 |
Gameplay¶
| # | Decision | Where |
|---|---|---|
| 15 | Continuous movement with grid-aligned collision and corner assist. Not tile-stepping | P1 §§4.1–4.2 |
| 16 | Players pass through each other | P1 §4.3 |
| 17 | Base speed 3.0 tiles/s. No permanent speed upgrade | P1 §4.4 |
| 18 | Every can is pre-shaken. Fixed 2.0s fuse, single-tap drop, no charge input, no meter | P1 §5.3, P2 §8.7 |
| 19 | Walk-off rule — step off a can you placed, never back onto it | P1 §5.2 |
| 20 | Blast is a cross; each arm stops at the first destructible or solid block | P1 §6.2 |
| 21 | Your own blast kills you | P1 §6.4 |
| 22 | Damage window 3 ticks / 0.2s; VFX 0.4s. Only the window is lethal | P1 §6.3 |
| 23 | Chain detonation is immediate, resolved to a fixed point within one tick | P1 §5.4 |
| 24 | Blasts destroy uncollected pickups. Arm continues | P1 §6.5 |
| 25 | A sliding can entering a live blast tile detonates on contact | P1 §9.1 |
| 26 | A kicked can keeps its original fuse; sliding cans do no contact damage | P1 §9 |
| 27 | One life per round. No lives system, no respawn | P1 §7 |
| 28 | Eliminated players spectate, and may leave to Match Results. Ghost mode is post-launch | P1 §7.1 |
| 29 | Simultaneous death is a draw — no point awarded, round replayed. Round-wins matches only; single player scores by placement | P1 §7, §11.1, §11.3 |
| 30 | Sudden death from T−53s, one indestructible block every SuddenDeathTicksPerBlock ticks (8, 0.53s), spiralling inward across five interior rings |
P1 §12 |
| 31 | 2-player duels use the identical arena, grid and ruleset as 4-player | P1 §11.2 |
| 32 | Beach Kiosk seagulls are ambient only — they never interact with pickups | P1 §12.2, P3 §9 |
Pickups¶
| # | Decision | Where |
|---|---|---|
| 33 | Six effects, two classes. Buffs visible on the field; debuffs only inside a Surprise Cube | P1 §8.1, P4 §6.2 |
| 34 | Caps — Shaker 6 (max range 7), Extra Can 5 (max 6 cans), Ice Cube 3 | P1 §8.2 |
| 35 | Timed effects refresh, never extend. One movement modifier at a time, last wins | P1 §8.3 |
| 36 | Glue is 3s, Coffee Bean and Honeycomb Wax 5s | P1 §8.3 |
| 37 | Cube pool: all six effects at flat weight — 2/3 buff, worth opening | P1 §8.4 |
| 38 | The cube pool and the drop table are separate tables. A cube never contains a cube | P1 §8.4, P4 §10.1 |
| 39 | First cube of a new account is rigged to roll a buff. Server rule, no assets | P1 §8.4 |
| 40 | One drop source — every pickup spawns from a destroyed block, at that tile | P1 §8.5 |
| 41 | Drop weights are declared in the map header and required there. No pack default, no global config | P1 §8.5, P4 §A.4 |
| 42 | Power-ups are non-variant Core assets — identical in every arena and skin | P4 §1 |
| 43 | At cap, a pickup is still collectable and plays the ordinary pop | P1 §8.2 |
Production¶
| # | Decision | Where |
|---|---|---|
| 44 | Five variant axes — Core, Arena, Character, Blast, Can Flavour | P4 §1 |
| 45 | A variant fills a fixed slot manifest and never introduces new assets. Build-validated | P4 §1 |
| 46 | Only Classic can flavour ships at launch; the axis is reserved | P4 §5.4 |
| 47 | Blast skins are cosmetic-only. Store copy may not imply mechanical advantage | P4 §5.3 |
| 48 | Every character skin ever authored must ship a kick animation — build-enforced | P4 §4 |
| 49 | Three arena tiers — solid, destructible, hazard. The Reinforced two-hit tier is not built in v1 | P3 §7, P2 §16.2 |
| 50 | Maps are plain ASCII text files scoped to an arena pack | P4 Appendix A |
| 51 | Greyscale legibility at 32px is a release blocker, not a guideline | P4 §8 |
| 52 | No lives asset — hud/lives is vestigial, cut or repurposed as round pips |
P1 §7, P4 §6.4 |
| 74 | Pack sources live outside the engine project, at Content/; Core lives inside it at Assets/Game/Art/Core/. Maps live inside the pack directory |
P4 §2 |
| 75 | The three raster exports are sibling directories art/@1x\|@2x\|@3x, same filename in each. Core is exempt and authors one @3x set |
P4 §4 |
| 76 | Procedural fill stays cut in the tooling too — no fill_density, fill_weights or drop_rate fields anywhere. drop_weights is the only tuning key and it is required per map |
P4 §A.4, Pack Studio T4 |
| 86 | A pack art filename is its slot name, with no axis, pack or skin prefix — the directory path carries those. pack.json therefore holds no slot→file table |
P4 §3.1 |
Application¶
| # | Decision | Where |
|---|---|---|
| 53 | Silent device authentication. Registration is account linking, never a gate | P2 §6 |
| 54 | Authoritative server match handler, TypeScript, 15 ticks/s | P2 §7.2 |
| 55 | Clients predict movement only — never cans, blasts or pickups | P2 §7.2 |
| 56 | No pause in multiplayer. Single player only | P2 §8.4 |
| 57 | AI difficulty comes from behaviour and reaction delay, never stat cheating | P2 §8.5 |
| 58 | Menu screens are panels in a stack, not Unity scenes. Only Match loads and unloads |
P2 §13 |
| 59 | Currency and ownership are server-write only | P2 §11 |
| 60 | Chat exists in Lobby and Results only, and is quick-chat only — a fixed phrase set, no free text | P2 §9, §14 |
| 61 | Arena packs are playable when unowned, purchasable to host | P2 §10 |
| 62 | TLS is required before store submission — cleartext ws:// fails iOS ATS |
P2 §6.4 |
| 83 | Age rating 13+. No COPPA verifiable-consent path and no Designed for Families track. Chat stays quick-chat-only with server-side filtering, analytics is a consent-gated GDPR flow, and no ad SDK ships at launch | P2 §14, P5 §1.3 |
Progression¶
| # | Decision | Where |
|---|---|---|
| 63 | Six league tiers — Tin, Bronze, Silver, Gold, Diamond, Elite. Every account starts in Tin | P2 §17.2 |
| 64 | Clubs of 50. A tier shards into as many clubs as its population needs; assignment is a random shuffle seeded on the week ID, never score-seeded | P2 §17.4 |
| 65 | One league point per round won, plus one for completing the match. Nothing else scores, and points never decrease | P2 §17.3 |
| 66 | Rollover is Sunday 22:00 UTC, a fixed instant with no daylight adjustment. Clients show a duration, never a wall clock | P2 §17.5 |
| 67 | Demotion is 30% at every tier except Tin. Promotion is 30% below Gold and 20% at Gold and Diamond — 15 up from Tin, Bronze and Silver, 10 from Gold and Diamond. Tin never demotes, Elite never promotes | P2 §17.2 |
| 67a | The taper is what makes the ladder a pyramid. Flat counts settle every tier at equal population; 20-up-against-30-down settles the top two at two-thirds of the tier below, putting Elite near 8.7% | P2 §17.2.1 |
| 68 | Only matchmade Quick Match awards league points. Private, browsed and single-player matches award zero — this is the anti-collusion rule, not a scope cut | P2 §17.9 |
| 69 | Leagues are server-authoritative in full. The client displays a standing it never computes and never writes; cut lines arrive as ranks from the server | P2 §17.10, §17.11 |
| 70 | Cut counts, point yield, club size and the cutoff instant are server config, tunable without a client build | P2 §17.10 |
| 71 | League art is Core, non-variant — six tier badges carried by silhouette, not fill. ~40 assets | P4 §6.6, P3 §3.4 |
| 72 | The demotion band never uses the warm hazard band. Warm means lethal, everywhere, permanently | P3 §3.4 |
| 73 | No rank, badge or band renders on the play field or in the match HUD. Status is a menu concept | P3 §3.4, §11 |
Economy¶
| # | Decision | Where |
|---|---|---|
| 77 | One currency: Sugar Cubes, earned and purchased. No premium-only currency | P5 §1 |
| 78 | Sugar Cubes never buy anything competitive — no points, XP, promotion or power | P5 §1.1 |
| 79 | XP is lifetime; player level = 35·n² cumulative, no cap. Level-ups pay Sugar Cubes | P5 §2 |
| 80 | Single-player rewards capped at 5 matches per UTC day | P5 §1.2 |
| 81 | The rollover pays nothing in v1 — leagues ship as pure status; the reward strip stays reserved | P5 §3.2 |
| 82 | Yields, prices and caps are server config (economy_config), tunable without a client build |
P5 §7 |
Open Questions¶
Nothing on this list is a rule. What remains open is tuning, scope and commercial policy.
Tuning — only playtesting settles these¶
| # | Question | Part |
|---|---|---|
| 1 | Corridor exit distance — 3 tiles hard, 6 advisory? Derived, but verify against a real authored map | P1 §3.4 |
| 2 | Drop weights against the Shaker cap of 6 under four-player contention. 62,12,10,6,10 is the accepted default, but the cap-6 case has not been exercised with four real players. Owned by implementation-plan M8 |
P1 §8.5 |
| 3 | Economy numbers — yields, prices, curve coefficient. All starting values in server config; soft-launch data settles them | P5 §11 |
Scope¶
| # | Question | Part |
|---|---|---|
| 4 | How many maps in the bundled baseline? Three is the manifest floor. With no procedural fill and no reseeding, map count is replayability — five or six per pack is a better aim | P4 §5.1 |
| 5 | Is 22:00 UTC the right instant for the intended launch region? The fixed-offset decision stands regardless of the number | P2 §17.5 |
| 6 | Does the taper hold in practice? Elite at 8.7% is an equilibrium reached over several weeks and distorted upward by frozen accounts. If it inflates after four weeks of live data, Diamond's promotion count is the first lever | P2 §17.2.1 |
| 7 | Quick Match must never expose a round-count option while the participation point is flat, or short matches become the optimal grind. A standing constraint rather than a question — recorded here because it will be forgotten | P2 §17.3 |
Commercial and legal¶
| # | Question | Part |
|---|---|---|
| 8 | Region routing. One server in Germany means poor US and bad APAC latency | P2 §15 |
| 9 | IAP validation — through Nakama's hooks or a separate service. Owned by implementation-plan M0, because a separate service is a second host and a second certificate | P2 §15 |
| 10 | GDPR consent age. Article 8's digital-consent age is 16 in Germany, the soft-launch region, so consent-based analytics for 13–15 year olds in the EU needs parental authorisation or a lawful basis that is not consent | P2 §14 |
Glossary¶
| Term | Meaning |
|---|---|
| Chugger | A player character. Four at launch: Rax, Puff, Nim, Bolt |
| Frenzy Cola | The in-world soda brand. All cans are Frenzy Cola |
| Can | The bomb. Pre-shaken, fixed 2.0s fuse |
| Blast | The foam explosion. A cross of four cardinal arms |
| Blast range | Arm length in tiles. Base 1, +1 per Shaker, max 7 |
| Shaker | Class A pickup, +1 blast range, cap 6 |
| Extra Can | Class A pickup, +1 simultaneous can, cap 5 |
| Ice Cube | Class A pickup, unlocks kick at stack 1, cap 3 |
| Coffee Bean | Class B timed buff, speed ×1.4 for 5s. Cube-only |
| Honeycomb Wax | Class B timed debuff, speed ×0.6 for 5s. Cube-only |
| Glue | Class B timed debuff, no can placement, 3s. Cube-only |
| Surprise Cube | Opaque ? container. Rolls one of six effects at flat weight |
| Drop table | Per-map weights for what a destroyed block yields. Five entries |
| Cube pool | Fixed six-effect flat table for what an opened cube contains. Not the drop table |
| Kick | Walking into a can while holding an Ice Cube, sending it sliding |
| Corner assist | Automatic alignment nudge into an adjacent open lane |
| Corridor exit rule | Every walkable tile needs a perpendicular escape within 3 tiles |
| Walk-off rule | You may leave the tile of a can you placed; you may not return |
| Spawn capacity | A map's spawn count. A match may seat fewer players, never more |
| Sudden death | Closing rings of indestructible blocks from T−53s |
| Arena pack | A downloadable bundle: one theme, its slot manifest, and its maps |
| Variant axis | One of five dimensions along which assets vary |
| Slot manifest | The fixed list of assets a variant must supply to build |
| Live-blast tile set | Tiles lethal for the damage window, 3 ticks. The only simulation state crossing ticks |
| League tier | One of six rungs: Tin, Bronze, Silver, Gold, Diamond, Elite |
| Club | A 50-player cohort within one tier, competing for one week. The whole of the leaderboard a player sees |
| League point | One per round won, plus one for completing a matchmade game. The unit of weekly progression. Spent on nothing, converted to nothing — P5 §3.1 |
| Rollover | Sunday 22:00 UTC. Promotions and demotions resolve, clubs are re-dealt, scores reset |
| Cut line | The promotion or demotion rank in a club. Promotion is 30% below Gold and 20% above; demotion is 30% except in Tin. Sent by the server, never derived by the client |
| The taper | Promoting 10 rather than 15 from Gold and Diamond. What makes the ladder a pyramid instead of a column |
| Frozen | An account that played nothing this week. Holds its tier rather than demoting |
| Sugar Cube | The currency. Earned from matches and level-ups, purchasable in bundles, spent in the store. Never buys anything competitive — P5 §1 |
| XP | Lifetime experience. Never resets, never decreases. Earned per match in every mode, single-player capped daily — P5 §2 |
| Player level | Derived from XP: cumulative 35·n², no cap. Pays Sugar Cubes on level-up. Menu-only status — P5 §2 |
Part 1 — Gameplay Rules & Mechanics¶
One of five companion documents, and the one a programmer implements from. The Style Guide says what things look like, the Asset Architecture says how they are cut up and shipped, the Framework & Flow says what screens exist and who talks to the server. This document says what happens on the grid.
Orientation: this is a classical Bomberman ruleset. Where a rule is a genre convention, it is stated as such and adopted without argument — conventions in this genre are load-bearing, and players arrive already knowing them. Where Fizz Frenzy deliberately diverges, the divergence is marked ▲ DIVERGENCE and justified. Everything else is invention, and there is deliberately very little of it.
1. Ownership¶
| Subject | Owner |
|---|---|
| Simulation rules, movement, collision, blast, timing, balance constants | This document |
| Pickup effects, caps, stacking, drop table | This document |
| Pickup silhouettes, animation, icon rules, shape law | Asset Architecture §6.2 |
| Colour law, readability, VFX budget | Style Guide |
| Screens, netcode, session, match lifecycle | Framework & Flow |
Where this document and the asset architecture describe the same pickup, this one governs behaviour and that one governs appearance. Neither is a copy of the other.
2. The Core Loop¶
- Four Chuggers spawn at the positions the map defines, on an 11×13 grid dense with destructible blocks
- Each starts with one can, blast range 1, and no upgrades
- Dropping a can destroys blocks, opens paths, and reveals pickups
- Pickups increase range, can count, and unlock the kick
- The last Chugger alive wins the round
- First to win a majority of the configured rounds wins the match
A round is won by being alive, not by scoring. There are no points, no kill counts and no objectives. This is the genre contract and it is not negotiable — every mechanic below exists to make survival interesting.
3. The Grid¶
3.1 The map defines everything¶
Every tile on the board comes from the map file. The engine places nothing. There is no pillar lattice, no fill density, no random seeding, no assumed corner spawn. A .map file is a literal, complete description of the starting board.
| Element | Source |
|---|---|
| Solid blocks | Map glyphs #, %, @, & — at any position the author chooses |
| Destructible blocks | Map glyphs 1–4 — explicit, never probabilistic |
| Empty floor | Map glyph . |
| Player spawns | Map glyph S — anywhere on the board, not necessarily corners |
| Hazard tiles | Map glyph H |
The single exception is pickup seeding, which remains procedural: pickups spawn from destroyed blocks against the drop table (§8.5), seeded from the match seed so all clients agree.
This is a real simplification and it buys three things:
- What the author sees in the text file is what plays. No mental model of "this
?might become a block." A map can be reasoned about by reading it - Validation becomes exact rather than probabilistic. Connectivity, spawn safety and corridor length can be checked as facts about the file instead of as properties that hold for most seeds
- Full customisation. Asymmetric maps, duel maps, tutorial maps, unusual geometries — all become authoring decisions rather than engine features
3.2 Dimensions¶
11 columns × 13 rows, fixed. ▲ This is the one thing the map does not define.
The portrait layout budget (Framework & Flow §5.8.1) derives tile size from a fixed grid: 9.5 visible columns (the outer wall ring is cropped to a quarter tile) at ~119 logical px on the reference phone, against an 80px minimum for touch accuracy. A map declaring 13×17 would push tiles to 76px and break the minimum; a map declaring 9×11 would waste a fifth of the screen.
Version 1 maps are all exactly 11×13. The validator enforces it. Variable dimensions are a version 2 feature that arrives with a camera-fit rewrite, not a map format change.
- Columns index
x0–10 left to right, rows indexy0–12 top to bottom - The border — row 0, row 12, column 0, column 10 — must be solid in every map. A blast leaving the grid is undefined behaviour, and the map makes it impossible by construction
- That leaves 99 interior tiles for the author to fill however they like
3.3 Spawns¶
Spawns are S glyphs, placed anywhere, assigned in reading order (top-left first). A map declares players: 2–4 and must carry exactly that many.
Corners are conventional, not required. A map may spawn players along one edge, in a ring, or clustered — the validator checks fairness, not shape.
What the validator does enforce (Asset Architecture §A.7):
- Each spawn has an escape from its own first can: an empty orthogonal neighbour plus one more empty tile beyond it, around the corner or straight on. At the starting blast range of 1 that tile is outside the can's cross. A player who can be boxed in by their own first can has been killed by the level designer
- Minimum spawn separation of 6 tiles Manhattan
- Every spawn can reach every other spawn with all destructibles removed
A note for whoever authors maps: on an 11×13 field, corner spawns are not equidistant — vertical pairs sit 12 tiles apart, horizontal pairs 10. If you use corners, compensate through layout.
Spawn subset selection¶
players in the map header is spawn capacity, not match size. A map is offered only to matches at or below its declared capacity. Most maps declare 4 and carry four spawns, so a 2- or 3-player match runs on one of them and the engine must choose which spawns to use.
A map may declare 2. diner_booth_standoff declares players: 2 and the duel tag: it is selectable for a duel and never for a four-player match.
Rule: choose the subset of size N that maximises the minimum pairwise Manhattan distance between chosen spawns. Ties break by reading order, so the result is deterministic across clients.
On a conventional corner map with two players this selects a diagonal pair, which is the correct opening. Reading order would have selected the two top corners — adjacent, 10 tiles apart on a board where the diagonal is 22 — and turned a duel into a brawl at tick zero.
The rule is one line of code and it is doing real design work. Without it, every 2-player match on every map opens in the wrong place.
3.4 The corridor rule¶
The escape equation (§10) assumes a player caught in a blast lane can step sideways out of it. With arbitrary author-placed solids, a long straight corridor with no side exits is expressible — and at max blast range against a slowed player, it is an inescapable death lane that looks perfectly innocent in a text editor.
So the validator enforces a side-exit guarantee directly:
Every walkable tile must have a perpendicular exit within 3 tiles along its corridor.
Three tiles is derived from base speed. Reaching the exit tile is not safety — the player is still in the lane — so the escape is three tiles along plus one through the opening:
| Exit at | Travel | At 3.0 tiles/s | Under Wax, 1.8 tiles/s |
|---|---|---|---|
| 2 tiles | 3 | 1.00s | 1.67s |
| 3 tiles | 4 | 1.33s — the rule, 0.67s of margin | 2.22s — dead |
| 4 tiles | 5 | 1.67s | dead |
| 5 tiles | 6 | 2.00s — an exact dead heat | dead |
| 6 tiles | 7 | 2.33s — dead at base speed | dead |
Honeycomb Wax plus maximum blast range is meant to be lethal without a near corner. Under Wax, a three-tile exit is 2.22s against a 2.0s fuse, so there is no escape along the lane at all. Stacking a movement debuff against a maxed opponent should be close to a death sentence, and the player's answer is to already be near a corner rather than to outrun the arm. §10's tight case is survivable given a side exit within two tiles — which this rule does not guarantee, and is not required to.
- Hard failure above 6 tiles. No excuse; that corridor is a trap
- Advisory between 4 and 6. Legitimate for a deliberately tense chokepoint, but it should be a decision someone made on purpose
A warning for whoever uses that advisory band. The table above is at base speed with no debuff at all. A 6-tile corridor is already lethal to a player at full speed who drops a can in it — 2.33s against a 2.0s fuse — and a 5-tile one is an exact dead heat. The advisory band is not "tense"; it is "the author has chosen to make this stretch fatal". A legitimate choice, and one to make with the number in front of you.
This is the most important validation rule in the format, and the easiest one to forget: authored maps make it a live risk on every file.
4. Movement¶
4.1 The model¶
Continuous movement with grid-aligned collision. This is the genre convention and it is what the game must do.
A Chugger has a floating-point position and moves smoothly. Collision is resolved against the tile grid. The player is not stepping tile to tile — that model feels rigid, removes corner assist entirely, and is not what anyone means when they say "like Bomberman."
- Position is a float
(x, y)in tile units - The player occupies a collision box of 0.7 × 0.7 tiles, centred on position — narrower than a tile, which is what makes corner assist possible
- Movement is 4-directional only. Diagonal input resolves to the dominant axis; ties keep the current direction
- No acceleration, no momentum, no inertia. Input starts and stops movement on the same frame
4.2 Corner assist¶
The single most important feel mechanic in the genre. When a player moves toward a corridor they are not perfectly aligned with, the game nudges them into alignment rather than stopping them against a pillar.
- If the player presses a direction that is blocked, and the perpendicular offset from the nearest open lane is ≤ 0.42 tiles, the player slides perpendicular toward that lane at full speed until aligned, then proceeds
- Assist only ever helps toward an open tile. It never pushes a player into a blast, and it never moves them further from their input direction
- There is no junction exception. Assist fires wherever there is a lane the player could enter
Without this, players die to geometry and blame the game. With it, they die to their own decisions.
Why there is no junction exception. Suppressing assist wherever both perpendicular neighbours are walkable would ask whether the player could step sideways, not whether the choice is ambiguous — and on an open board the first is nearly always true, so assist would survive only beside the border wall. Ambiguity is handled where it actually arises: the nearest qualifying lane wins, and an exact tie breaks toward the lower coordinate.
Why 0.42. A player covers speed ÷ rate tiles a tick — 0.2 at §4.4's 3.0 tiles/s and 15 Hz — so from a lane centre the offsets they can come to rest on cycle 0.0, 0.2, 0.4, 0.4, 0.2, 0.0. The threshold must cover 0.4, and it must not equal it: at exactly 0.40 the comparison sits on a knife edge — 1.5 + 0.2×7 is 2.9000000000000004, whose offset exceeds 0.4 — and float error accumulates differently in the TypeScript server port than in the C# client, so the two would strand players on different tiles. The extra 0.02 is two pixels, below perception, and it is what stops the rule depending on floating-point representation. Never set this equal to a reachable offset.
Why not 0.5. It assists from anywhere in a tile and reads as the game steering.
One limit is accepted rather than fixed. §8.3's timed effects change the step mid-match: a Coffee Bean's ×1.4 steps 0.28 tiles and can come to rest 0.44 from a lane centre, Honeycomb Wax's ×0.6 steps 0.12 and can rest at 0.48. Both are past 0.42, so a player under either effect can still be stopped a few pixels short. Only 0.5 removes it, and 0.5 costs the whole mechanic's restraint. The two effects are transient, one of them is a debuff whose whole job is to make movement worse, and buying their edge cases would make every ordinary step feel steered.
This number and §4.4's base speed cannot be tuned independently. Changing either moves the offsets the other has to cover, and 0.42 has only two pixels of margin. It remains an input to the escape equation (§10) and the corridor exit distance (§3.4).
4.3 Player-player collision¶
Chuggers pass through each other. Genre convention. Solid players make body-blocking a viable grief tactic, make corridors deadlocked, and interact terribly with rollback in netcode.
4.4 Speed¶
| State | Speed | Time per tile |
|---|---|---|
| Base | 3.0 tiles/s | 0.33s |
| Coffee Bean (+40%) | 4.2 tiles/s | 0.24s |
| Honeycomb Wax (−40%) | 1.8 tiles/s | 0.56s |
At 3.0, crossing the board is neither tedious nor fast enough to make the 2.0s fuse hard to judge. Every constant derived from it — the escape equation (§10), the two derived speeds above, the corridor exit distance (§3.4) — depends on this number.
▲ DIVERGENCE: there is no permanent speed upgrade. Classic Bomberman has Roller Skates; Fizz Frenzy has only the 5-second Coffee Bean. This is deliberate. A permanent speed stat means the escape equation (§10) changes match to match and every other constant has to be safe across the whole range. Holding base speed constant means the danger radius of a can is a fixed, learnable distance for the entire match. If playtesting says the game is too slow late, raise the base — do not add a stat.
5. The Can¶
5.1 Placement¶
A player may place a can when all of these hold:
- Their active can count is below their limit (base 1, +1 per Extra Can, cap 5 → max 6)
- The tile they occupy contains no can
- They are not affected by Glue
The can snaps to the centre of the tile the player's centre is in. It is placed instantly on the input frame — no wind-up delay in the simulation, whatever the animation does.
5.2 The walk-off rule¶
A player may step off the can they just placed, but not back onto it. Genre convention, and it is most of what makes the game tactical.
Implementation: a can placed under a player is non-solid for that player only until they fully vacate its tile. The moment their collision box clears the tile, it becomes solid to everyone including them.
A can is solid to every other player from the instant of placement — except to one already standing on the tile. §4.3 makes Chuggers pass through each other, so two players sharing a tile when one of them drops a can is ordinary play rather than a corner case. The exemption is therefore per player, not per owner: every player whose box overlaps the tile at the instant of placement is exempt, and each clears independently, exactly as the owner does.
Forbidding the drop instead is rejected — it would let a player deny a can by standing still, which is precisely the body-blocking grief tactic §4.3 gave up solid players to avoid, reintroduced through the can.
Without the exemption the non-owner is teleported. Collision tests only the leading edge of a move, so walking out of a newly solid tile resolves cleanly; a step whose leading edge lands back in it takes the blocked branch and clamps to the face it entered from — a backwards snap of up to 0.8 tiles, in one tick, with no wall in front of the player.
5.3 The fuse¶
2.0 seconds, fixed, on every can, always. No charge, no variance, no pickup that modifies it. See Framework & Flow §8.7.
- Armed state (visual only) begins at T−0.8s
- The fuse ring is the only timing information in the game
5.4 Chain detonation¶
A blast reaching a live can detonates it immediately on that tick, ignoring its remaining fuse. Chains resolve to a fixed point within a single tick — a four-can chain is one simultaneous event, not four sequential ones.
Genre convention, and the alternative (shortening the fuse instead) reads as network lag on a mobile screen.
5.5 Can-player collision¶
A can is solid. Walking into one stops you whether or not you hold an Ice Cube. The cube decides whether the can moves; it never decides whether the player is blocked.
A kicked can occupies its tile until it has cleared, so the kicker is blocked on the tick they kick exactly as a player with no cube is. There is no tick on which a Chugger and a can share a tile by walking, and a rule that says otherwise invites an implementation that lets the kicker follow the can into the tile it is still leaving. The kick and its blocked-behind case are §9.
6. The Blast¶
6.1 Shape¶
A cross: the centre tile plus four cardinal arms. Arms extend up to the player's blast range, which is 1 at base, +1 per Shaker.
There is no diagonal component, no radius, no falloff.
6.2 Propagation, per arm¶
Walking outward one tile at a time, the arm resolves each tile in this order and stops when told to:
| Tile contains | Result |
|---|---|
| Empty floor | Blast fills it, arm continues |
| Solid block | Blast does not fill it, arm stops |
| Destructible block | Blast fills it, block is destroyed, arm stops |
| Live can | Blast fills it, that can detonates this tick, arm stops |
| Player | Blast fills it, player dies, arm continues |
| Pickup or cube | Blast fills it, see §6.5, arm continues |
Arena hazard (H) |
Blast fills it, arm continues — as floor |
Why the blast fills a hazard. H is passable floor (§12.1, Asset Architecture §A.5): a tile a player can stand on and die on is a tile the foam must be able to reach, or a hazard becomes a blast shelter, which is the opposite of what it is for. Collision never varies with the hazard's phase, so propagation is static — the arm does not care which part of the cycle the tile is in.
The destructible rule — one block per arm, then stop — is the genre convention and it is the reason blast range feels valuable rather than absurd. An arm that cleared a whole row would flatten the arena in three cans.
6.3 Timing¶
| Phase | Ticks @15 Hz | Seconds |
|---|---|---|
| Damage window | 3 | 0.2s |
| Blast VFX | 6 | 0.4s |
The tick count is the source and the seconds are derived. At a fixed server rate a duration stated in seconds is not implementable unless it happens to divide into whole ticks.
A player is in a blast tile when their tile is — the tile their centre occupies, not any tile their collision box overlaps. This is not a detail: the box is 0.7 tiles on a 1.0 tile, so a box test would kill anyone standing within a third of a tile of the foam's edge and make the lethal region visibly wider than the cross that is drawn. That is exactly the "the game lied to me" failure this section exists to prevent, and it would silently widen the region §10's escape equation — and therefore the 3.0 base speed — was verified against. It is also the same test §5.1 uses to decide which tile a can is placed on, so the two agree.
Everything lethal happens in the first 0.2 seconds. Foam visible on screen after that is decoration. A player who enters a blast tile during the window dies; one who enters after it does not. This gap is deliberate — it is the difference between "I got away" and "the game lied to me," and it must be tuned so the foam that is still rising is lethal and the foam that is settling is not.
Why 3 ticks. Two ticks would also work — nothing fails at 2 — but at 3 the lethal moment has time to register as one, and the blast reads better. It is a feel preference, not a defect fix: do not reconstruct a failure at 2 that never happened. At 3 ticks, half of the 0.4s foam is lethal.
6.4 Self-damage¶
Your own blast kills you. Genre convention, absolutely central, and stated here because no other document says it.
There is no friendly-fire distinction, no owner immunity and no grace period. The player who placed the can is treated identically to everyone else.
6.5 Blasts and pickups — LOCKED: destroyed¶
A blast destroys any uncollected pickup or Surprise Cube it touches. Genre convention. The arm continues past it.
- Blast range gains a second use: upgrade denial. Blasting a Shaker your opponent is running toward is a real play, and it gives high-range players something to do with reach they can't otherwise spend
- Requires one asset:
pickup_destroyed(8 frames), which must not resemblepickup_pop— a player who sees the collect animation and gets no stat change will report it as a bug - The endgame does not silt up with uncollected items, which matters because the board is closing at that point anyway (§12)
Ordering note: destruction resolves in step 8, before collection in step 11. A player entering the tile on the same tick the blast lands does not collect the pickup — it is already gone, and they are already dead.
7. Death and Round End¶
- A player caught in a blast dies instantly. One life per round — there is no life pool, no respawn, no continues
- Death drops nothing. Upgrades are lost with the player
- The round ends when fewer players remain alive than
min(2, seated)— so a four-player round ends at one survivor, and a solo match ends only when that player dies. What this rule means is that a winner has been decided, and that needs an opponent to have lost — a rule like "one or zero players remain" would be true at tick zero of a single-player match - Simultaneous death is a draw. No player is awarded the round
There is no lives system. Asset Architecture §6.4's hud/lives is vestigial — cut it, or repurpose it as the round-pip indicator.
7.1 What an eliminated player does — LOCKED: spectate¶
An eliminated player stays in the match as a spectator until the round ends. No ghost mode, no interference, no early exit to results.
What this requires:
- A spectator camera state. The board is fully visible at all times on an 11×13 portrait field, so this is a HUD change rather than a camera system: the control zone is replaced
- The player's marker leaves the field with their Chugger (the defeat animation plays, then both are gone), the same as everyone else sees it. There is no HUD roster to update (Framework & Flow §5.8)
- Leave. A spectating player may drop to Match Results at any time. It is built together with Match Results (implementation-plan M9) rather than against a placeholder, and it cannot apply in hotseat, where the one local player leaving ends the match for everyone sharing the device
- Chat stays unavailable during live play, spectating or not (Framework & Flow §9)
The cost to accept: a player eliminated at 15 seconds watches for up to 105 seconds. That is the worst churn moment in the game, and it is the reason best-of-3 with a 120s timer is a better structure than single elimination — the wait is bounded and another round is coming.
Ghost mode is the first post-launch feature (§14). It is the better answer to this problem, but it is a system with its own balance pass, and shipping spectate first is the correct order.
8. Pickups — Consolidated¶
Six effects, two delivery routes. All non-variant: identical in every arena, every match, every skin.
8.1 The delivery split¶
Buffs sit on the field as themselves. Debuffs only ever arrive inside a Surprise Cube.
A visible debuff is fair because you can path around it. A Surprise Cube is fair because the risk is disclosed even though the outcome is not — the same contract as a Mario Kart item box. What the game must never do is deliver a debuff from a container that looked safe. The ? glyph is doing that work.
▲ DIVERGENCE from classic Bomberman, and an improvement. Bomberman's skull disease sits on the floor looking collectable and is the most-hated pickup in the genre's history.
8.2 Class A — permanent upgrades¶
Persist until death. Stack to a hard cap. Not counted in the HUD since the lean HUD (Framework & Flow §5.8.1): the effect shows in play. Drop visibly from destroyed blocks.
| Pickup | Effect | Base | Cap | Max |
|---|---|---|---|---|
| Shaker | +1 blast range | 1 | 6 | range 7 |
| Extra Can | +1 simultaneous can | 1 | 5 | 6 cans |
| Ice Cube | Unlocks kick at stack 1; stacks 2–3 raise can slide speed | 0 | 3 | speed tier 3 |
The Shaker cap is 6, giving a maximum range of 7. Range 9 would span an 11-wide grid from any position, making the top stacks imperceptible. At 7 the cap is reachable, felt, and still short of full board width — a maxed player dominates a lane without owning the whole row. The simulation tracks 0–6; the HUD shows no counter.
Cap behaviour. At cap, a Class A pickup is still collectable — an uncollectable pickup squatting on a tile is worse than a wasted one. It plays the ordinary pickup_pop. There is no capped variant in version 1.
Why no capped animation. Solo, a player caps blast range and Ice Cubes readily, so the state is not rare enough to be a discovery; across four players contending for ~48 blocks it is not common enough to teach anything either. A distinct animation for a moment that is either unremarkable or unreachable is art nobody reads. The simulation still distinguishes the two outcomes — PickupCollectedCapped is a separate event — so a duller sound costs nothing later if the ceiling turns out to want announcing.
The HUD counts none of the three. Range and kick show in play, and a player reads them off the board. The one exception is derived, not a counter: DROP carries the number of cans the player can place right now (Extra Can stacks + 1, minus cans already on the field), because that is the one thing the board cannot show. Framework & Flow §5.8.1 owns the widget.
8.3 Class B — timed effects¶
Do not stack. Shown as a depleting fill, not a counter — what the player needs is time remaining, and a timed effect has no quantity to count. Only ever obtained from a Surprise Cube.
| Effect | Result | Duration | Type |
|---|---|---|---|
| Coffee Bean | Move speed ×1.4 | 5s | Buff |
| Honeycomb Wax | Move speed ×0.6 | 5s | Debuff |
| Glue | Cannot place cans | 3s | Debuff |
Rules:
- Refresh, never extend. A second Coffee Bean resets to 5s; it never accumulates. This is what stops a debuff chain from locking a player out of the match
- One movement modifier at a time, last one wins. Coffee Bean clears Honeycomb Wax and vice versa. That gives the bean a rescue role and makes cubes worth opening even while slowed
- Glue is orthogonal and coexists with either movement state. Glued-and-fast is useful; glued-and-slow is the worst place in the game to be
- All timed effects clear on death — irrelevant with one life per round, but stated for when ghost mode arrives
- Effects persist across the round end? No. Every round starts clean
The depleting fill and Framework & Flow §5.8.1's "effect chip with a drain ring" are one widget, described from two sides. This section says what the indicator must convey; that one says where it sits and what it is made of, and P2 owns HUD structure per the ownership table. Glue draws its own fill alongside a movement effect rather than replacing it, because the two coexist.
8.4 The Surprise Cube¶
Opaque ? container. All six effects at flat weight — so a cube is 1/3 debuff, 2/3 buff, and worth opening. Including the Class A permanents in the pool is exactly what makes that true.
The cube pool is not the drop table. They are two separate tables and conflating them is a silent bug:
| Drop table (§8.5) | Cube pool (here) | |
|---|---|---|
| Answers | What spawns when a block breaks? | What does an opened cube contain? |
| Entries | 5 — including nothing and Surprise Cube | 6 — all effects, flat, no nothing |
| Source | Map header, per map | Fixed constant, never authored |
A cube can never contain a cube, and a cube is never empty. If cube contents are rolled from the drop weights, a map that weights Surprise Cube highly starts producing cubes inside cubes, and it will present as a vague balance complaint rather than as an error.
- A cube that rolls a capped Class A effect plays the ordinary pop after the reveal, so a wasted roll still reads as a roll rather than a bug
- The reveal is public — it plays at world scale above the collecting player. Everyone sees who just got glued
- First-cube rigging: a new account's first-ever cube is rigged to roll a buff. Three of six effects can only be learned by experiencing them, and a Glue on someone's opening match is a bad first impression that costs nothing to avoid
Implementation note. "A new account's first-ever cube" is a fact about an account, not a match. The simulation implements the rule behind a flag the match is created with; account persistence (implementation-plan M9) is what sets that flag.
8.5 The drop table¶
One source, one position. Every pickup and cube spawns from a destroyed block, at that block's tile. Nothing seeds at round start; nothing spawns on an empty tile.
| Entry | Weight |
|---|---|
| Nothing | 62 |
| Shaker | 12 |
| Extra Can | 10 |
| Ice Cube | 6 |
| Surprise Cube | 10 |
62,12,10,6,10 is the accepted default. Solo on a 48-block map, blast range and Ice Cubes cap and five cans are reached, so power arrives fast enough alone. Under four-player contention the caps are expected to be mostly unreachable — a property of a map's block count, not a fault to weight around.
One case is still unverified: the table against the Shaker cap of 6 under real four-player contention, which only networked play can produce. That check belongs to implementation-plan M8 (front matter open question 2).
Three things to hold:
- Weights are declared in the map header and are required there.
drop_weights: 62,12,10,6,10— nothing, Shaker, Extra Can, Ice Cube, Surprise Cube. There is no pack default and no global fallback. A map's block density and its drop weights decide the same thing, the rate at which power arrives, and keeping them in one file is what stops a pack from feeling wrong for reasons nobody can locate. The table above is the accepted default to copy into a new map, not a placeholder the engine supplies - The arena is the pacing mechanism. Dense early board, poor in pickups; mid-game opens and floods; endgame has nothing left to break. That curve comes free and should not be fought
- Ice Cube is weighted lowest deliberately — it caps at 3, and the kick is a capability, not a stat. One is transformative; the third is marginal
8.6 Spawn sequencing¶
Three rules the shared spawn position forces:
- Debris first, pickup second. The break animation completes, then the spawn burst fires. A pickup materialising inside its own debris cloud is invisible for exactly the moment the player is watching that tile
- Debris never occludes a settled pickup. Debris lifetime is capped and no particle comes to rest on a tile
- Chain reactions spawn into live foam. The spawn burst waits for the blast on that tile to clear. A pickup appearing under active foam is a pickup nobody sees
9. The Kick¶
Unlocked by the first Ice Cube.
Trigger: walk into a can while holding at least one Ice Cube. The can slides in the direction of travel.
The trigger is automatic. There is no KICK button and no kick input. The trigger is a simulation rule, which the ownership table gives to this document, so it cannot be half a control scheme. The cost is real and accepted: an Ice Cube permanently removes the ability to back up against a can. A cornered player holding one shoves their own can away and keeps running, with no way to decline. On device this is not felt as the game fighting the player.
| Ice Cube stack | Slide speed |
|---|---|
| 1 | 6.0 tiles/s |
| 2 | 8.0 tiles/s |
| 3 | 10.0 tiles/s |
The blocked-behind rule decides whether a slide starts at all:
| Walking into a can while holding ≥ 1 Ice Cube | Result |
|---|---|
| The tile beyond the can, in the direction of travel, is free | The can slides. The player is still blocked this tick — the can occupies that tile until it clears |
| That tile holds a solid, a destructible, another can, a player, or the border | No kick happens. The can stays where it is and blocks the player exactly as it blocks a player with no Ice Cube |
A can wedged against a wall is a wall, for everybody. Without the second row the kick either has to fail silently after being announced, or has to push a can into an occupied tile, and both are worse than declining to start.
This never becomes fiddly, because the slowest kick is 6.0 tiles/s against a 3.0 tiles/s walk: a kicked can always outruns the player who kicked it. The player pushes once and follows. There is no repeated nudging.
A sliding can keeps its original fuse. Kicking moves a problem; it does not postpone one. This is what makes the kick offensive rather than defensive.
A slide stops when the can hits:
| Obstacle | Result |
|---|---|
| Solid or destructible block | Bounce, settle on the last open tile |
| Another can | Bounce, settle. The struck can does not move |
| A player | Settle on the tile before them. No damage |
| The border | Bounce, settle |
A sliding can that detonates mid-slide blasts from whatever tile it occupies at that instant, snapped to the nearest tile centre.
9.1 Sliding into a live blast¶
A sliding can that enters a tile within its live damage window detonates immediately, exactly as a stationary can does when a blast reaches it.
This is the right rule and it closes a hole. Without it, a can can be kicked through an explosion it should have set off, which is both physically absurd and a genuine exploit — a player with an Ice Cube could launch cans across a contested board and have them survive the fight in the middle.
Two constraints it imposes:
- A blast tile stays "live" for the damage window (3 ticks, 0.2s), not for the VFX duration (0.4s / 6 ticks). Cans and players obey the same window. Foam that cannot kill a player must not detonate a can, or the game is teaching two different things with one visual
- The engine must hold a live-blast tile set that persists across ticks, keyed by tile with an expiry. Stationary cans are caught by the chain pass in the tick the blast is computed; a sliding can may arrive one or two ticks later and needs something to arrive at. See step 3 in §13
Tunnelling is not a risk at these speeds. The fastest can travels 10 tiles/s, which at 15 ticks/s is 0.67 tiles per tick — under one tile, so no can can skip over a live blast tile between ticks. This is a real constraint on the Ice Cube speed table: any future slide speed above 15 tiles/s would need swept collision.
Sliding cans do not damage on contact. Adding contact damage makes the kick a ranged weapon and turns every corridor into a shooting gallery — a different game.
10. The Escape Equation¶
The relationship that decides whether the game is playable at all. Everything above is tuned to satisfy it.
A player who places a can must be able to reach safety within 2.0 seconds.
At base — range 1, speed 3.0 tiles/s — safety is 2 tiles away and takes 0.67s. Comfortable, which is correct: at base the player should die from trapping themselves, not from being outrun.
The tight case is maximum range plus Honeycomb Wax, where the player must clear the lane entirely — up to 2 tiles along, 1 tile perpendicular, so 3 tiles at 1.8 tiles/s = 1.67s of a 2.0s fuse. Survivable with 0.33s of margin, and it should — and does — feel like nearly dying.
This case assumes a side exit within two tiles, and §3.4 does not guarantee one. At the three tiles §3.4 permits, the escape is four tiles and 2.22s — beyond the fuse. That is accepted: Wax plus maximum range is meant to be lethal unless the player is already near a corner. The equation above describes the case where a corner is available; it is not a promise that one always is.
The three numbers that must never be changed independently: fuse duration, base speed, and the Honeycomb Wax multiplier. Any change to one requires re-verifying this case. If the wax multiplier drops to 0.5, the tight case becomes 2.0s exactly and the debuff becomes a death sentence.
11. Round and Match Structure¶
| Setting | Value |
|---|---|
| Rounds per match | 1 / 3 / 5, host-selected |
| Round timer | 120s |
| Sudden death begins | T−53s (i.e. at 67s elapsed) |
| Inter-round | 5s results, auto-advance |
| Round start | 3-2-1 countdown, no spawn invulnerability |
Between rounds: the board resets to the map's authored layout exactly. All upgrades reset to base. Nothing carries over.
▲ Every round of a match is played on an identical board. With no procedural fill there is nothing to reseed. This is the classic Bomberman behaviour and it is fine — but it relocates replayability from randomisation within a map to the number of maps, which makes the map count in each pack a content decision rather than a nice-to-have. The three-map pack minimum in the asset architecture is a floor, not a target.
The only per-round variation left is the drop table, which still rolls fresh from each destroyed block.
11.1 Ties¶
- A draw awards no round point and does not count toward the total. The round is replayed
- Hard cap:
configured_rounds + 2. At the cap, the highest scorer wins; only a tie at the top is a draw. A flat draw would discard a player who led 2-1-1-0 through seven rounds of a best-of-5. The cap bounds the match — without it, players who keep drawing could play forever. Only a drawn round fails to move anyone's total, so only repeated draws can reach the cap at all: a best-of-3 cannot see four decided rounds, because someone reached two first
11.2 Two-player duels¶
A 2-player match uses the same arena, the same 11×13 grid, the same drop table and the same round count as a 4-player match. No separate tuning, no altered round structure.
A duel usually runs on a standard 4-spawn map with two of its spawns occupied, selected by the maximum-separation rule in §3.3. It may also run on a map that declares a capacity of 2, such as diner_booth_standoff, which is then never offered to a four-player match.
Three things to hold:
- A four-player board is never offered to more players than it declares, and never fewer than it can seat. Capacity is a ceiling, not an equality: a 4-spawn map takes 2, 3 or 4 players, and a 2-spawn map takes only 2
- Two players on a 4-player board is sparse. The interior that feels tight with four feels open with two, so early rounds are slower and the sudden-death ring does more of the work of ending the match. Acceptable for v1, and the strongest argument for authoring more duel maps later
- This makes §3.3 load-bearing rather than a detail. With most duels on 4-spawn maps, spawn selection is what makes a 2-player match open sensibly. It is not optional polish
11.3 Single-player scoring¶
A match with exactly one human (single player against bots) scores by placement instead of round wins. Multiplayer, whether local or matchmade, keeps one point per round won (§11, §11.1), and league points stay built on that unit (Framework & Flow §17.3). This scoring is single player's own and never feeds the ladder.
| Place in the round | Points |
|---|---|
| 1st | 4 |
| 2nd | 2 |
| 3rd | 1 |
| 4th | 0 |
- Places come from pop order. First is the player left standing, second the player popped last before that, and so on
- Popped on the same tick: the players share the places they cover and split those places' points, rounded down to a whole point. Two players covering 2nd and 3rd get 1 each
- Timeout: the survivors split the 4, rounded down, and everyone popped scores 0
- Wipe-out: when the last players standing are all popped on the same tick, they count as the survivors and split the 4. Everyone popped earlier scores 0
- Fewer players: the same table. A 1-vs-1 loser scores 2
Every round counts, draws included. Nothing is replayed, §11.1's hard cap does not apply, and the match is always exactly the configured 1, 3 or 5 rounds: second and third place keep every player in it to the end. The highest total wins; a shared top total is a drawn match. There is no tie-break.
12. Sudden Death¶
Triggered at T−53s. The arena closes from the outside in, one block at a time, spiralling clockwise from the outer ring inward.
Five interior rings. The map border is already indestructible (Asset Architecture §A.7), so the ring closes the 9×11 interior — five concentric rings:
| Ring | Extent | Tiles |
|---|---|---|
| 1 | x 1–9, y 1–11 | 36 |
| 2 | x 2–8, y 2–10 | 28 |
| 3 | x 3–7, y 3–9 | 20 |
| 4 | x 4–6, y 4–8 | 12 |
| 5 | x 5, y 5–7 | 3 |
| 99 |
Each ring fills clockwise from its top-left, one tile at a time, so players can read the direction and run ahead of it.
One block every SuddenDeathTicksPerBlock ticks — 8, i.e. 0.53s. A constant per-block beat, rather than a whole ring at once, is what lets players read the direction and run ahead of it: a 36-tile ring landing on a single tick is a guillotine with nothing to outrun. It also makes the ring accelerate on its own as the rings get shorter, which is the tension the mechanic exists to create. Spreading each ring across a fixed time instead would make it decelerate as it tightens, and would need a rounding rule the C# simulation and the TypeScript server port must agree on exactly. SuddenDeathTicksPerBlock is a tuning value in SimTuning, not a constant. Faster intervals read on device as a guillotine rather than pressure.
The trigger is paired with the interval. SuddenDeathAtTick is T−53s so that the ring finishes before the round timer does. The first block lands on the start tick itself; the last of the 99 — 98 intervals later — lands at tick 1789, against the 1800-tick round timer. That margin is thin: 11 ticks, 0.73s. A dedicated test guards the arithmetic directly, at every tick rate the dev toggle offers, and any retune of either number must keep the ring inside the timer.
- A closing block fills the tile with an indestructible block
- Anything on a filled tile is destroyed: players die, pickups vanish, destructible blocks are replaced. A closing block draws nothing from the generator — the same rule §8.5 states for a blast over empty floor. There is one RNG stream by design; the drop roll belongs only inside the destructible branch of §13 step 8, and a ring block rolling too would shift every later pickup in the round exactly as an errant blast-tile roll would
- A can caught by a closing block detonates on the next tick, not this one. §13 step 10 runs after fuses, chain resolution, the blast tile set and damage have already resolved for this tick; same-tick detonation would need either a step inserted before step 5 or a second blast pass inside step 10, and a second pass breaks "all deaths this tick are simultaneous," the property that makes a mutual kill a genuine draw. The block instead marks the can for detonation, and next tick's step 6 chains it exactly as it chains a fuse expiry — which is also what makes a crushed can that touches off two more read as one event
- A survivor whose collision box merely overlaps a new block is clamped flush out of it. The 0.7-wide box reaches a quarter-tile into the next tile, so without this a player standing just outside a closing tile is left a quarter-tile inside a wall until they next try to move
This exists to end stalls. Two cautious players with full upgrades will circle each other indefinitely, and a 90-second match becomes four minutes. It is not a flourish.
The Beach Kiosk tide (style guide §9) is the same mechanic reskinned — the tide is the closing ring. It is not a second system.
12.1 Arena hazards¶
Version 1 supports exactly one hazard type: a tile that is periodically lethal, placed with map glyph H. It kills anything standing on it during its active phase and detonates cans. It is passable floor at every point in that cycle — see §A.5, and §6.2's propagation row.
The rhythm is a version 1 constant, not pack data. No map header key or pack.json field carries it. Period, warning and active durations live in the simulation's tuning table beside the other feel numbers, which is also where a cheat-menu slider can reach them. Promotion to authored data is a later change if a pack ever needs a second rhythm, and it changes no rule when it happens.
The rhythm: a 4.0s cycle, 0.8s of telegraph, 0.53s lethal. Brisk (2.0s) and slow (8.0s) cycles both read worse on device. The numbers stay in the tuning table rather than becoming constants, because that is what a later pack promotion needs.
The warning phase carries no rule. It exists so the tile telegraphs, and only the view reads it. A player who walks onto an already-active tile dies — "standing on it" includes arriving.
That covers the Diner's soda fountain. Teleporters, ricochet and persistent floor states need map format 2 and are not in v1.
The Beach Kiosk tide is not a hazard tile. The tide is the closing ring (§12) — sudden death, reskinned — because it closes a whole arena on a schedule, and H is one tile with a cycle. The soda fountain is the case only H serves.
12.2 The seagull — ambient only¶
Seagulls never interact with pickups. Blasts already handle uncollected pickups (§6.5), and a seagull that stole them would be a second solution to a solved problem that also broke the rule that pickups behave identically in every arena.
Seagulls remain as background dressing — style guide §9, ambient, non-interactive, never on a tile.
13. Deterministic Resolution Order¶
The server simulates at 15 ticks/s. Every tick resolves in exactly this order. Order matters more than any individual rule — get it wrong and two clients disagree about who died first.
- Apply movement inputs; resolve collision and corner assist
- Resolve kicks initiated this tick
- Advance sliding cans; resolve slide collisions; mark any can now occupying a live-blast tile for detonation (§9.1)
- Resolve can placements
- Decrement fuses; mark expired cans for detonation — and mark any can standing on a hazard tile whose phase is active (§12.1)
- Resolve chain detonations to a fixed point — repeat until no new can is marked
- Compute the full blast tile set for all detonating cans simultaneously
- Destroy blocks; destroy pickups and cubes in blast tiles (§6.5)
- Apply damage to every player in a blast tile, and to every player whose centre tile is an active hazard — all deaths this tick are simultaneous
- Advance sudden-death ring if due
- Resolve pickup collection
- Tick timed effects; expire finished ones
- Queue deferred spawn bursts for tiles whose blast has cleared
- Expire live-blast tiles whose damage window has elapsed
- Evaluate round end
The live-blast tile set is written in step 7 and read in step 3 of subsequent ticks, expiring in step 14 after the damage window — 3 ticks. It is the only piece of state that crosses tick boundaries, and it is what lets a can arriving two ticks late still detonate correctly.
Step 6 before step 7 is what makes a chain read as one event. Step 9 as a single simultaneous pass is what makes mutual kills a genuine draw rather than a race decided by iteration order over a player array.
The hazard gets two clauses and no step of its own. Its phase is a pure function of the tick and the §12.1 constants — nothing is stored and nothing is advanced — so there is nothing for a step to do. Step 5 marks a can on an active tile so that step 6 chains it exactly as it chains a fuse expiry, and a fountain setting off a three-can chain reads as one event. Step 9's clause is the load-bearing half: putting the hazard kill in the same simultaneous pass as blast damage is what makes a player taken by the fountain and a player taken by a blast on the same tick a genuine draw rather than an ordering artefact. Neither clause reorders anything, and inserting a step would invalidate every constant tuned on device, which is what this list exists to prevent.
Step 10 is stateless in the same way. It sits fixed after step 9's damage pass, and that position is deliberate: closing blocks land one at a time, 0.53s apart (§12), so two players taken by the ring die on different ticks rather than in the same simultaneous pass, and the round keeps a clean winner instead of every ring kill becoming a coincidental draw with the tick's blast damage. What has been placed by a given tick is a pure function of the tick and the §12 interval, exactly as the hazard's phase is — nothing is stored, so there is nothing to compare in state equality, serialise for the conformance canon below, or re-implement diverging in the TypeScript port.
The round timer is a clause on step 15, not a sixteenth step. If it reaches 120s with the round still live, the round ends in a draw — no player is credited. This should be unreachable: sudden death fully closes the field at tick 1789 by default — 119.3s, 0.73s before the 1800-tick timer would fire (§12). That margin is thin enough that this unreachable branch is worth taking seriously rather than filing away. It is defined anyway, because an unreachable branch that turns out to be reachable — a retuned ring interval, a hazard that stalls the last kill, a bug — is how a match hangs, and the alternative to a defined rule is an undefined one.
A tick index is resolved before the counter that names it advances. The tick counter increments only after all fifteen steps have run, so a rule inside a step reads the index it is resolving — call it N — while anything reading the counter between ticks sees N+1. At the start of resolving tick N, exactly N/15 seconds have elapsed at 15 Hz — which is why the round timer above fires at exactly 120.0s and why sudden death places its first block at exactly 67.0s, both comparing the resolving index against a tick count rather than the elapsed one. The two rules read the counter the same way on purpose; if they ever disagreed about which side of the increment they meant, one of them would be wrong.
M8's conformance suite replays this order against fixed input sequences. ffvector runs the vectors under tests/conformance/vectors/ and compares a canonical serialisation of the state, tick by tick and then once more at the final tick, so a divergence between this implementation and the TypeScript port is caught at the tick it starts rather than reported as "the two states differ somewhere." The canon separates its grid rows with commas: without that, a live-blast row of [1,12,0…] and one of [11,2,0…] both render as 112000000000, and two distinct states would share a digest — a false match, the worst outcome the suite can produce, since it reports agreement between implementations that disagree.
14. Deliberate Omissions¶
Classic Bomberman pickups not in version 1, and why. Listed so the absences read as decisions rather than oversights.
| Omitted | Reason |
|---|---|
| Remote detonator — scheduled for version 2 | Invalidates the fixed fuse, which is version 1's only timing signal. Arriving in v2 as a limited-use pickup, not a permanent stat: a player holds N remote charges, and a detonate button appears only while one is armed. That keeps the fixed fuse as the default reading of the board and makes the remote an event rather than a mode |
| Roller skates (permanent speed) | Destabilises the escape equation, §4.4 |
| Bomb pass / wall pass | Removes the trap mechanic that is the core of the game |
| Invincibility vest | Removes tension for its duration; also an unreadable state at 32px |
| Punch / throw | A whole second control verb and a second targeting model. Ghost mode wants it first |
| Skull disease | Superseded by the Surprise Cube, which delivers the same chaos with disclosed risk |
| Line bomb | Multi-tile placement needs a second input verb |
Version 2 scope, in rough order of value:
- Ghost mode — eliminated players lob cans from the sidelines
- Remote detonator — limited-use pickup, confirmed for v2
- Punch / throw
- 6-player — costed in Framework & Flow §1.1.1
The remote detonator has a consequence worth noting now rather than in a year: it is the first mechanic that would put a second action button in the control cluster alongside DROP. With the kick automatic (§9), DROP is the only button there, so the detonator would be the first thing to make the lower-right cluster a layout question rather than one icon.
15. Open Questions¶
| # | Question | Blocks | Recommendation |
|---|---|---|---|
| 1 | Corridor exit distance — 3 tiles hard, 6 advisory? (§3.4) | Map authoring | Derived from the escape equation, but verify against a real authored map before pack one |
Every open question in this document is a tuning question that only playtesting answers. There are no unresolved rules.
Part 2 — Application Framework & Flow¶
One of five companion documents. The Design Style Guide says what things look like. The Asset Architecture says how they are cut up, named and shipped. This document says what screens exist, how the player moves between them, and what the client and server each own.
0. Product Summary¶
| Title | Fizz Frenzy |
| Player characters | Chuggers |
| Genre | Grid brawler (Bomberman-lineage), real-time |
| Modes | Single player vs AI, online multiplayer |
| Platforms | iOS, Android — phone and tablet |
| Engine | Unity 3D (2D presentation, URP) |
| Backend | Nakama, self-hosted |
| Session length | 120s per round; 3-round match runs 4–7 minutes |
Store description (locked copy)¶
Welcome to the grid, Chugger.
Grab a can, give it a shake, and drop it before the pressure gets to you. Two seconds later it blows — foam spraying down four lanes, taking out walls, crates, and anyone slow enough to still be standing there.
Trap your friends. Chain your cans. Be the last Chugger fizzing.
• Fast 4-player matches you can finish on a bus ride • Grab power-ups: Shaker for reach, Extra Can for more firepower, Ice Cube to kick cans across the floor • Dozens of arenas and flavours to unlock • Simple to pick up, brutal to master
1. Locked Foundations¶
Both are closed. Everything downstream depends on them.
1.1 Player count — four, and only four¶
Version 1 is a 4-player game everywhere: content, code, HUD, netcode, matchmaking, maps. Six players is a version 2 feature and is scoped as a refactor, not as a switch waiting to be flipped.
This is the simpler decision and it is defensible. Building for a player count you have not designed content for costs real time in exchange for flexibility you may never use, and half-built abstractions rot: an untested 6-slot code path is not a head start, it is a source of bugs that only appear the day you finally enable it.
One exception, and it is nearly free. Keep max_players as a match config field, not a hard-coded literal, and have the match handler loop over the actual roster length rather than a fixed array of four. This costs an hour today, is testable at 4, and removes the single most invasive part of the eventual refactor — the part that otherwise means touching every gameplay file. Everything else is authored, tuned and validated at 4.
Everything below is explicitly 4-only:
| System | Version 1 |
|---|---|
| Player colours | 4 — Red, Blue, Green, Orange, deuteranopia-verified (style guide §3.2) |
| Shape badges | 4 — circle, square, triangle, star |
| Player markers | 4 — colour + shape under each Chugger's feet; there is no HUD roster (Framework & Flow §5.8) |
| Map header | players: 2–4, validated |
| Spawn markers | S, assigned in reading order, max 4 per map |
| Matchmaker | 4-player pools |
| Lobby / browser | 2–4 slots |
| Balance | Drop weights and round timer tuned at 4 |
1.1.1 The version 2 refactor, costed now¶
Written down so the decision stays informed rather than forgotten. When 6-player is scheduled, expect to touch:
| Area | Work | Size |
|---|---|---|
| Player colours 5 and 6 | Two new hues that survive deuteranopia, protanopia and tritanopia, avoid the warm hazard band (style guide §3.3), and stay distinct from the existing four and from Sunbeam | Hard. This is the genuinely difficult one |
| Shape badges 5 and 6 | Two more shapes readable at 32px — hexagon and diamond are the obvious candidates | Small |
| Player markers | Two more marker shapes, matching shape badges 5 and 6. There is no HUD roster to redesign | Small |
| Map format | players widened to 2–6, plus A–F explicit spawn assignment from the format 2 reserve. New maps required; existing 4-player maps stay valid |
Medium |
| Balance | Drop rate, fill density, round timer and sudden-death timing all re-tuned. 6 on an 11×13 field is 1 player per 24 tiles versus 1 per 36 | Medium |
| Grid | Possibly a larger field for 6, which would be a second grid size and a second layout budget | Decide before committing |
| Mode design | 6-player FFA on this field is cramped and may play better as 3v3 — which is a second scoring model, a team HUD state and friendly-fire rules | Design work, not engineering |
The honest summary: the expensive parts of 6-player are the colour palette and the mode design, and neither of them gets cheaper by writing flexible code now. That is why deferring is the right call.
1.2 Grid — 11 columns × 13 rows, portrait¶
Locked. Portrait, one-handed-capable, a slim top row (timer, round pips or single-player points, menu) above the field and the control zone below it. Landscape becomes a tablet presentation option only — same grid, wider margins, repositioned controls. It is never a second layout with different tile counts.
Consequences:
- Aspect ratio. The field is 11:13, or 0.846. On a 19.5:9 phone (0.462) the field cannot fill the screen vertically and horizontally at once — it fills width and leaves vertical room, which goes to the control zone. The tile size on a tall phone comes from cropping the outer wall ring, not from the vertical budget. On a 4:3 tablet the field fills height and leaves horizontal margin, which is where the tablet HUD goes. Both work; neither needs a separate grid
- The field is width-constrained on phones, height-constrained on tablets. Camera fit logic must handle both, not just scale to width
- Fixed dimensions are a layout constraint, not just a design one. The 80px minimum tile size in §5.8.1 is what pins the grid at 11×13; a larger field breaks touch accuracy on the reference phone. Maps define every tile within that frame but never the frame itself — Gameplay Rules §3.2
- Spawns are wherever the map puts them. Corners are conventional, not required. Where corners are used, note they are not equidistant on an 11×13 field — vertical pairs sit 12 tiles apart, horizontal pairs 10
- Vertical chains read better than horizontal ones on this field, which favours the taller axis for trap corridors. Worth telling whoever authors maps
See §5.8.1 for the vertical layout budget.
2. Screen Inventory¶
The full set. Each entry is there because something in the design implies it exists.
| # | Screen | Why it exists |
|---|---|---|
| 1 | Boot / Splash | Nakama session restore, asset manifest check, pack integrity |
| 2 | Home | Root hub |
| 3 | Single Player Setup | Arena, map, AI count, difficulty |
| 4 | Multiplayer Hub | Quick Match vs Browse vs Private |
| 5 | Account | Device auth is silent; this screen is upgrade & link, not gate |
| 6 | Match Browser | Lobby list, filter, join |
| 7 | Match Lobby | Pre-match room: roster, ready state, arena vote, chat |
| 8 | Loading / Handoff | Pack download, map parse, spawn assignment |
| 9 | Game Engine (Match) | The actual game |
| 10 | Pause | Single player only — see §8.4 |
| 11 | Round Results | Between-round scoreboard: round pips, or single-player points (Gameplay Rules §11.3) |
| 12 | Match Results | Winner, rewards, rematch, exit |
| 13 | Character Select | Four Chuggers, skins per Asset Architecture §5.2 |
| 14 | Store | Arena packs, character skins, blast skins, flavours |
| 15 | Profile / Collection | Owned cosmetics, stats, equipped loadout |
| 16 | Settings | Audio, haptics, control scheme, language, account |
| 17 | How to Play | Pickup vocabulary — Asset Architecture §6.5 depends on this existing |
| 18 | About | Version, legal, links |
| 19 | Credits | Child of About |
| 20 | Chat | Not a screen — an overlay component. See §9 |
| 21 | Network | Not a screen — a service. See §7 |
| 22 | Player AI | Not a screen — a system. See §8.5 |
| 23 | Leagues / Leaderboard | Weekly club standing, tier, cut lines — §17 |
There is no Registration gate. See §6.
3. Navigation Graph¶
Boot
└─ Home ──────────────────────────────────────────────┐
│ │
├─ Single Player │
│ └─ Setup ─→ Loading ─→ MATCH │
│ ├─ Pause ─→ (resume|quit)
│ └─ Match Results ─┬─ Rematch → Loading
│ └─ Home
│
├─ Multiplayer ─→ [session check]
│ ├─ Quick Match ─→ Matchmaker ─→ Lobby ─→ Loading ─→ MATCH
│ ├─ Browse ─→ Match Browser ─→ Lobby ─→ Loading ─→ MATCH
│ └─ Private ─→ Create/Join by code ─→ Lobby ─→ Loading ─→ MATCH
│ │
│ MATCH ─→ Round Results ─(loop)
│ └─→ Match Results ─┬─ Rematch → Lobby
│ └─ Home
│
├─ Character Select ─→ (equip) ─→ Home
├─ Store ─→ Item Detail ─→ [purchase] ─→ Store
├─ Leagues ─→ (standing) ─→ Home
├─ Profile ─→ Collection | Stats | Leagues
├─ Settings ─→ Account (link/unlink)
└─ About ─┬─ How to Play
├─ Credits
└─ Legal
Rules for the graph:
- Home is the only root. Every screen has a path back to Home in at most three steps. Android hardware back never exits the app from anywhere except Home, where it prompts.
- MATCH is a trapdoor. You cannot navigate out of a live multiplayer match by normal means — only Forfeit (confirmed) or disconnect. There is no back button during a match.
- Loading is never a destination. It has no back affordance and is entered only from a screen that has already committed the player.
- Store and Character Select are reachable from Home and from Match Results. The moment after a loss is where cosmetic intent is highest; a dead-end results screen wastes it.
- Leagues is reachable from Home, Profile and the league beat on Match Results, and from nowhere else. It is a destination, never a step on the way to a match — nothing about queueing for a game may route through a leaderboard.
4. Application State Machine¶
The client is always in exactly one of these. Illegal transitions are assertion failures, not silent no-ops.
BOOTING → AUTHENTICATING → IDLE ⇄ MATCHMAKING → LOBBY → LOADING → IN_MATCH → POST_MATCH → IDLE
↓ ↓
OFFLINE ←──────── RECONNECTING ←─────────────┘
| State | Socket | Description |
|---|---|---|
BOOTING |
none | Unity init, addressables catalogue, local config |
AUTHENTICATING |
none | Nakama session restore or device auth |
IDLE |
connected | Menus. Socket open, chat live, presence visible |
MATCHMAKING |
connected | Ticket submitted, waiting on pool |
LOBBY |
connected + match joined | Roster assembling, not yet simulating |
LOADING |
connected | Pack resolve, map parse, prewarm |
IN_MATCH |
connected, high traffic | Simulation running |
POST_MATCH |
connected | Results, rewards, rematch window |
RECONNECTING |
dropping | Backoff retry, see §7.4 |
OFFLINE |
none | Single player only, queued telemetry |
Single player runs the same machine with a null network layer: IDLE → LOADING → IN_MATCH → POST_MATCH. This is deliberate — one match lifecycle, one set of bugs. The AI opponents inject inputs at the same layer a remote player's inputs arrive at.
5. Screen Specifications¶
5.1 Boot / Splash¶
Owns: Unity bootstrap, addressables catalogue fetch, session restore, minimum-version check.
- Studio logo → game logo, ~2s minimum, dismissible after assets are ready
- Silent Nakama device authentication (§6.1). No UI unless it fails
- Version gate: if server reports
min_client_versionabove the build, hard-block with a store link. This RPC must exist from day one — it's the only lever you have when a netcode bug ships - On network failure: proceed to Home in offline mode, do not block. Single player must work on a plane
Exit: Home, always.
5.2 Home¶
The hub. Everything the player does starts here, and the layout should make the split obvious in under a second.
Primary (large, thumb-reachable lower half): - PLAY — single player, one tap to Setup - ONLINE — multiplayer hub
Secondary (upper band): - Player badge: equipped Chugger, name, level → Profile - League chip: tier badge and current club rank → Leagues (§5.17). Carries a notification dot when a rollover result has not been seen - Currency counter (Sugar Cubes) → Store - Settings gear
Tertiary (small, bottom edge): - Store, How to Play, About
Ambient: - Equipped Chugger idle-animates on screen. This is the cheapest place to make an unlocked skin feel like it was worth buying - Connection pip: green connected / amber reconnecting / grey offline. Small, always visible, never a modal
Notification surfaces: unclaimed rewards, friend invite, new pack. Badge counts only — no interstitial popups on the hub.
5.3 Single Player Setup¶
- Arena — horizontal carousel of owned packs, locked packs shown greyed with a price tag
- Map — thumbnail grid of maps in the selected pack, plus Random
- Opponents — 1 / 2 / 3 AI (up to
max_players - 1) - Difficulty — Easy / Normal / Hard / Frenzy. Per-slot difficulty is a nice-to-have; a single global setting ships first
- Rounds — 1 / 3 / 5
- START → Loading
Remembers last configuration. A returning player should be two taps from a match.
5.4 Multiplayer Hub¶
Three routes, presented in order of how much friction they carry:
- Quick Match — matchmaker ticket, no configuration. This is the default and should be visually dominant
- Browse Matches → Match Browser
- Private Match — Create (generates 4-character code) or Join by code
Below: friends-online strip if the friends system ships, otherwise omit entirely rather than showing an empty rail.
On entry, check session validity. If the socket is down, show the reconnect state inline on this screen rather than bouncing the player back to Home.
5.5 Match Browser¶
Lists open lobbies from Nakama's match listing, filtered to open == true and size < max_players.
| Column | Source |
|---|---|
| Host name | Match label |
| Arena / map | Match label |
| Players | size / max_players — 2–4 |
| Rounds | Match label |
| Ping band | Client-measured, three tiers: good / fair / poor |
| Lock | Private matches excluded from listing entirely |
- Pull-to-refresh, plus 10s auto-refresh while visible
- Filters: arena pack, has-space, ping band
- Tapping a full match shows Full and refreshes the row rather than failing on join
Implementation note: Nakama match listing queries the match label, so the label must be a structured JSON string containing every filterable field. Decide the label schema before the match handler is written — changing it later invalidates every live match.
{"mode":"ffa","pack":"diner","map":"diner_crossroads","rounds":3,"open":true,"region":"eu"}
5.6 Match Lobby¶
The room between joining and simulating. Exists for multiplayer only.
- Roster —
max_playersslots (2–4), each showing Chugger, name, ready state, shape badge, assigned player colour - Empty slots — Waiting… or Add AI if the host allows it
- Arena/map — host-selected, or vote if you want the extra system. Ship host-selected
- Ready toggle per player; host gets START enabled when ≥2 players are ready
- Chat panel — see §9
- Countdown — 5s auto-start when all slots ready, cancellable by anyone un-readying
- Leave — always available, returns to Multiplayer Hub
Host migration: if the host leaves pre-start, promote the longest-connected player. If nobody remains, dissolve the match. This is server-side logic in the match handler, not client.
5.7 Loading / Handoff¶
Runs between commit and simulation. Not skippable, but should rarely exceed 3 seconds.
Sequence:
1. Resolve arena pack — download if missing, show progress with byte count
2. Fetch the .map file if this client does not have it, then parse and validate against Appendix A of the asset architecture
3. Preload pack assets, prewarm blast VFX pool and audio
4. Report client_ready to the match handler
5. Server waits for all clients or times out at 20s, dropping non-ready players
6. Countdown 3-2-1, then simulation start
Never simulate until every client has confirmed ready. A client that starts late in a real-time grid game is a client that dies to a can it never saw.
Loading screens carry a rotating gameplay tip. Cheap, and it's where most players actually learn the Ice Cube kick.
5.8 Game Engine (Match)¶
Detailed in §8. Screen-level furniture only:
The lean HUD (adopted 2026-09-25). Every pixel of height goes to the field. There is no player roster and no pickup strip: players are recognised on the board, and power-ups show their effect in play.
HUD, top row (40 CSS px): - Round timer, tabular figures per style guide §4 - Round pips (best-of-N); in single player the point totals instead: yours on the left, the leading opponent's on the right (Gameplay Rules §11.3) - Menu button at the right edge (pause in single player, the match menu online)
On the field, instead of the roster: - Player marker under every Chugger: the player's shape badge (circle, square, triangle, star) in their player colour, lying flat on the floor at the feet. It is the who-is-who cue and the colourblind identity (style guide §3.2); the corners and points that show around the feet carry the shape - Opponent debuffs show as the status overlay on the Chugger itself (Asset Architecture §6.3), not on an avatar - A defeated player's Chugger and marker leave the field; there is no dead slot to grey out
5.8.1 Portrait layout budget¶
Reference device: 6.1" phone, 390×844 CSS px (1170×2532 at 3×), 19.5:9. The layout is computed, not a table of percentages: art/runtime/match-layout.js (matchLayout) is the implementation, and the values below are its constants, in CSS px.
| Band | Height | Contents |
|---|---|---|
| Safe area top | device | Notch / dynamic island. Never draw here |
| Top row | 40 | Timer, round pips (single player: point totals), menu button |
| Headroom | 0.3 tile | Top-row Chuggers' heads and ears draw over the frame into this gap |
| Playfield | 11.5 tiles | The 11×13 grid with the outer wall ring cropped to a quarter tile: 9.5 × 11.5 tiles visible |
| Control zone | the rest, ≥ 156 | Joystick origin area left, DROP right, the timed-effect chip between them |
| Safe area bottom | device | Home indicator. Never place a button here |
Playfield sizing. tile = floor(min((width − 12) / 9.5, (usable height − 40 − 156) / 11.8)). Tall phones are width-bound: on the reference phone the tile is 39 CSS px (117 device px), 22% larger than the v0.5 budget's 32, and the spare height lands in the control zone. Squarer screens (16:10 foldables, tablets in portrait) are height-bound, and gain the most (36 → 52 CSS px on a 540×864 screen). Never stretch tiles.
The frame. Only a quarter of the outer wall ring shows, inside a 3 px Cola Brown keyline with a 4 px cel drop edge and rounded corners (clipFrame in art/runtime/arena.js). Floor, residue, pickups, markers and blocks are clipped to it; what stands up (cans, Chuggers, status, blast VFX, debris) is not, so a top-row head overlaps the frame instead of being cut off. There is no floor_edge art and no arena backdrop behind the match: the background is the Home screen's plain slate gradient, so nothing competes with the board.
Four rules this budget exists to enforce:
- The control zone never overlaps the playfield. No transparent joystick floating over live tiles. In a game where the bottom row is a legitimate hiding spot, a thumb over the field is a death the player will blame on you
- The HUD takes no height from the field. Anything new that wants to be on screen during play goes in the top row, in the control zone or on the field in world space. It never adds a band
- Tile size never drops below 80 logical px. On the smallest supported device this is the constraint that decides minimum spec. If a device can't hit it, the field is scrolling or the device is unsupported — decide which before QA finds out
- The outer wall stays visible as a quarter-tile strip. Players need to see where the board ends; cropping it away completely gains only about 8% more tile size
Tablet. Same 11×13 field, fitted to height instead of width, HUD and controls move into the horizontal margins. This is a layout variant, not a second scene.
HUD, bottom control zone: - Movement input — see §8.6 - DROP button, thumb-side. It is the only button in the cluster — the kick is automatic. Gameplay Rules §9 owns the trigger - Cans ready as a badge on DROP: the number the player can place right now. It is the only count in the HUD (Gameplay Rules §8.2); there are no Shaker / Extra Can / Ice Cube counters - Active timed effect chips (Coffee Bean / Honeycomb Wax / Glue) with drain rings, centred above the joystick and DROP. A chip is a depleting fill, not a counter — Gameplay Rules §8.3 says why, and this section owns the widget
No pause in multiplayer. A menu button opens a slim overlay: forfeit, settings, mute — the simulation continues underneath.
5.9 Round Results¶
Between rounds. Fast — 5 seconds, auto-advance, skippable when everyone taps.
- Round winner callout
- Score pips updated; in single player, each player's points this round (+4, +2, +1, +0) and their running total, and a drawn round reads "Draw: points shared" (Gameplay Rules §11.3)
- One stat line ("Blocks blasted: 14")
5.10 Match Results¶
- Winner with victory animation
- Final standings, placement, per-player stats
- League beat — points earned this match, new club total, rank delta, tappable to Leagues. Quick Match only; silent in private, browsed and single-player matches, which award nothing (§17.9). It animates alongside the standings and never gates REMATCH
- Rewards: currency, XP, unlock progress — yields and flow defined in Economy & Progression §5
- REMATCH — single player: straight back to Loading. Multiplayer: returns the party to Lobby with the roster intact
- HOME
- Store hook: one contextual card, e.g. the arena that was just played, if unowned
5.11 Character Select¶
- Four Chuggers, each with owned skins in a sub-carousel
- Full-body preview with idle and victory animations
- Locked skins show source (store / reward / seasonal)
- Equip is instant and persists to Nakama storage
5.12 Store¶
Four shelves matching the variant axes:
| Shelf | Contents | Note |
|---|---|---|
| Arena packs | Themed pack, all maps included | Largest download; show size |
| Character skins | Per-Chugger | Must ship a kick animation — build-enforced |
| Blast skins | Foam colour/particles | Copy rule: no comparative language implying advantage |
| Can flavours | Reserved, post-launch | Only Classic ships |
Store copy rule from the asset architecture stands: cosmetic items must never be described in terms that imply mechanical benefit. No "sharper," no "faster-looking," no "intimidating." This is a store-review risk as much as a fairness one.
5.13 Profile / Collection¶
Equipped loadout, owned cosmetics grid, lifetime stats (matches, wins, blocks blasted, favourite arena), account status (guest vs linked) with a link prompt if guest.
League block: current tier, best tier ever reached, and the last 8 weeks as a compact strip of tier badges with the movement each week. This is the only place league history is surfaced — the Leagues screen itself is strictly about the week in progress.
5.14 Settings¶
Audio (music/SFX separate), haptics, control scheme (§8.6), language, colourblind assist, reduced motion, account link/unlink, privacy policy, data deletion request, restore purchases, delete local data.
Data deletion must be reachable in two taps from Settings. GDPR, and you are shipping in the EU.
5.15 How to Play¶
Per asset architecture §6.5 — icons redrawn at UI scale, silhouettes matching the field exactly. Sections: movement, drop & the two-second fuse, blast lanes, blocks, the three permanent pickups, the Surprise Cube and its six outcomes, kick.
Offered once automatically after first launch, then permanently available from Home and About.
5.16 About / Credits / Legal¶
Version string and build number (must be copyable — it's the first thing support asks for), studio, credits list, third-party licences, privacy policy, terms, support contact.
5.17 Leagues¶
The weekly standing. Rules and server behaviour are §17; this is the screen.
Header: - Tier badge and name — Tin through Elite - Countdown to rollover, rendered as a duration in device-local terms ("2d 14h"), never as a wall-clock time. §17.5 - Your rank and points, pinned and always visible
Body — the standing:
- All 50 rows: rank, Chugger portrait, display name, points
- Your row is sticky. It docks to the top or bottom edge when scrolled past. In a 50-row list this is the difference between a screen you read and a screen you hunt through
- Promotion band across the top rows and demotion band across the bottom, drawn from the server's promote_cut and demote_cut ranks — never from a hard-coded 15
- Late joiners carry a small marker rather than looking like they lost badly
- Frozen inactive members render greyed at the bottom (§17.7)
States:
- Offline or stale — last cached standing, staleness marker, countdown disabled
- Rollover in progress — closes_at in the past means "Results are being calculated", not a stuck or negative timer
- No placement yet — a brand-new account before its first Quick Match sees the ladder explained and a single CTA into Quick Match. Never an empty list
Two things this screen must not do:
- It never blocks anything. A failed
league_getshows an inline retry and nothing else breaks. Home loads, Play works - It carries no rewards UI at launch. The rollover pays nothing in version 1 (Economy & Progression §3.2), and the screen is laid out so a reward strip can be added under the header without a redesign
6. Identity & Session Model¶
Multiplayer is never gated behind registration. Account creation before first online match is the single largest drop-off point in casual mobile multiplayer.
6.1 Silent device authentication¶
At boot, authenticate with Nakama using the device identifier. No screen, no friction. The player has an account and a persistent Nakama user ID before they reach Home, and they never knew it happened.
Boot → session in local secure storage?
├─ yes → restore; expired? → refresh token
└─ no → authenticateDevice(deviceId, create: true)
→ prompt for a display name on first Home visit
6.2 Upgrade to a real account¶
Linking (email, Apple, Google Play Games) is offered — never forced — at the moments where the player has something to lose:
- After first multiplayer win
- After first purchase (required — an unlinked purchase that vanishes with the device is a refund and a one-star review)
- From Profile, permanently available
The Account screen is therefore account linking, not registration. Same screen handles sign-in on a new device.
6.3 Session handling¶
- Store the session and refresh tokens in platform secure storage — Keychain / Keystore. Not PlayerPrefs
- Refresh proactively when the token is within 5 minutes of expiry, not reactively on 401
- Session lost mid-match → §7.4
6.4 Security note on the server key¶
yokicloud is embedded in every shipped client and is trivially extractable. That is normal — the Nakama server key is not a secret and is not an authorisation boundary. Two consequences:
- All authority lives in the authoritative match handler and in RPCs, never in the client. Any client can connect and send any message; the server decides what is true
- Cleartext on 7350 will not pass App Store review. iOS App Transport Security blocks non-TLS connections, and
ws://fails the same check; Android has blocked cleartext by default since API 28. A TLS-terminating reverse proxy must sit in front of Nakama, with a real certificate on a real hostname, before a submission candidate is built. Retrofitting this after the client is written means touching every connection path.
There are two hostnames — prod.fizz-frenzy.com and dev.fizz-frenzy.com. TLS terminates once, in a reverse proxy in front of Nakama, which routes each hostname to its own Nakama VM; Nakama itself runs unmodified and holds no certificate. Both names carry a Let's Encrypt certificate, both environments connect on 443, and 7349, 7350 and 7351 are closed to the internet.
Routing by hostname rather than by address is what makes the dev/prod split a proxy-side change: a second environment is a second name on the same public address, not a second client build. Both names resolve to the same address, and the client cannot tell.
The hostname matters beyond the certificate: the host ships inside the client in a Resources ScriptableObject (§11), so a bare IP would pin the server to one address for the life of every installed build. A name is the indirection that lets the server move without an app update.
- The admin console on 7351 is not a public surface. It is administrative, weakly authenticated by default, and must never be reachable from the internet or proxied to a hostname. Reach it over an SSH tunnel. The only ports published to the internet are 80 and 443 — the proxy reaches Nakama across the private bridge, and 7349, 7350 and 7351 stay inside it
7. Network Component¶
A single service, owned by the app, not by any screen. Screens subscribe; they never talk to Nakama directly.
7.1 Responsibilities¶
| Concern | Detail |
|---|---|
| Session | Auth, refresh, secure storage |
| Socket | Connect, heartbeat, presence |
| Matchmaking | Ticket add/remove, matched callback |
| Match | Join, leave, send/receive match state |
| Chat | Channel join/leave, message send/receive |
| Storage | Loadout, settings, collection sync |
| RPC | Version check, config fetch, reward claim, league_get (§17.11) |
| Health | Latency sampling, connection state broadcast |
League points are not an opcode. They are written by the match handler at MATCH_END and read back over RPC; nothing about the ladder travels on the match socket. See §17.10.
7.2 Match architecture¶
Authoritative, not relayed. A relayed match means every client trusts every other client's claim about where it is and when its can exploded — which in a game decided by tile-level timing is an invitation.
- Match handler runs server-side; Nakama supports Go, TypeScript or Lua. TypeScript is the right choice here unless you have Go on the team — the logic is simple, iteration speed matters more than throughput at this scale, and the runtime is well-supported
- Tick rate 15/s. Grid movement is cheap and the simulation is coarse; 15 is comfortable on mobile networks and leaves headroom
- Clients send input intent, not position.
{dir, wantDrop}— opcode 4, §7.3. The server simulates and broadcasts authoritative state - Clients predict local movement and reconcile on divergence. Snapping on a grid is visually forgiving as long as you correct over 2–3 frames rather than teleporting
- Cans, blasts and pickups are never predicted. A blast the client drew that the server didn't is a death the player will not accept. Latency on the drop feedback is better than a phantom explosion
7.3 Opcode table¶
Lock this early; it's cheap now and painful later.
| Op | Direction | Payload |
|---|---|---|
1 MATCH_CONFIG |
S→C | Map, pack, seed, roster, colours |
2 CLIENT_READY |
C→S | Loaded and prewarmed |
3 ROUND_START |
S→C | Countdown complete, tick zero |
4 INPUT |
C→S | {seq, dir, drop} |
5 STATE_DELTA |
S→C | Positions, cans, blasts, pickups since last ack |
6 STATE_FULL |
S→C | Complete snapshot — join and reconnect only |
7 EVENT |
S→C | Discrete: pickup taken, pickup destroyed, player out, block destroyed, can detonated. Never delta-compressed — these drive audio and VFX |
8 ROUND_END |
S→C | Winner, scores |
9 MATCH_END |
S→C | Final standings, rewards |
10 FORFEIT |
C→S | Voluntary exit |
11 PING |
C↔S | RTT sampling |
Deltas for the common case, full snapshots for joins and recovery. EVENT is separate from STATE_DELTA because events drive audio and VFX and must not be dropped by delta compression.
Opcode 4 carries no kick bit. The kick is a consequence of walking into a can while holding an Ice Cube, which the server already knows from dir; a bit for it would be a second place the trigger could be decided, and a way for a client to claim a kick the simulation would not produce.
7.4 Disconnection & reconnection¶
| Situation | Behaviour |
|---|---|
| Brief drop in match (<10s) | Chugger stands still, is still killable. Client retries with backoff, rejoins via STATE_FULL |
| Drop >10s | Player marked out for the round, may rejoin for the next round |
| Host drops in lobby | Host migration, longest-connected player |
| App backgrounded in match | Treated as a drop. iOS suspends the socket regardless |
| Drop in menus | Silent retry, connection pip goes amber, no modal |
| Server unreachable at boot | Offline mode, single player only, banner on Home |
Never show a blocking error dialog for a transient network state. The pip and an inline banner carry it. Modals are for actions that failed and need a decision.
8. Game Engine¶
8.1 Grid¶
- 11 columns × 13 rows, portrait, fixed — §1.2. Every map is exactly this size and the validator enforces it
- The map file defines every tile: solids, destructibles, empty floor, spawns and hazards are all authored glyphs. The engine places nothing and there is no procedural fill — Gameplay Rules §3.1
- Spawns are
Sglyphs assigned in reading order, 2–4 per map, at any position the author chooses - The border ring is solid in every map, enforced at validation
- Movement is continuous with grid-aligned collision, including corner assist — see Gameplay Rules §4
One consequence for the loader: the board is fully deterministic from the file, in layout and in appearance, and needs no shared randomness to build. The match seed covers only the drop table. That removes a whole class of desync from the boot path — see Asset Architecture §A.8.
8.2 Match state machine¶
INIT → COUNTDOWN → PLAYING → ROUND_OVER → (next round | MATCH_OVER)
↓
SUDDEN_DEATH
Sudden death triggers at T-53s. Closing walls, or the Beach Kiosk's tide from the style guide — either way, the field closes inward one block at a time (Gameplay Rules §12). Without this, two cautious players stall forever and your 90-second match becomes four minutes.
8.3 Round & match structure¶
- Best of 3 by default, 1 or 5 selectable. Single player plays every round of the 1, 3 or 5 and scores by placement (Gameplay Rules §11.3)
- Round timer 120s, then sudden death at 67s
- Round ends when one Chugger remains, or all remaining die simultaneously (draw — no point awarded in round-wins matches)
- Inter-round: 5s results, then straight back in. Pickups reset; the board resets to the map's authored layout. Every round is played on an identical board — with no procedural fill there is nothing to reseed, which makes map count the replayability lever (Gameplay Rules §11)
8.4 Pause¶
Single player only. Multiplayer gets a non-blocking overlay. If pause exists in multiplayer at all, someone will use it as a stalling tactic within a week of launch.
8.5 Player AI¶
Runs client-side in single player, injecting inputs at the same interface remote players use. Four difficulty tiers, differentiated by behaviour, not by cheating:
| Tier | Behaviour |
|---|---|
| Easy | Random walk with blast avoidance. Drops cans near blocks. Ignores players |
| Normal | Pathfinds to pickups, drops on blocks, retreats to safe tiles. No chains |
| Hard | Targets players, sets traps in corridors, chains cans, contests pickups |
| Frenzy | Above, plus kick usage and predictive cutoff of escape routes |
Never give AI more range, speed or cans than the pickups it has actually collected. Difficulty comes from decision quality. Players detect stat cheating immediately and it destroys trust in the multiplayer matchmaking too.
Reaction delay per tier (300ms / 180ms / 90ms / 40ms) is the cheapest single lever for tuning difficulty.
8.6 Controls¶
Two schemes, selectable in Settings, floating joystick as default:
- Floating joystick — thumb anywhere in the lower-left zone, origin set on touch. Snaps to 4 directions with a small dead zone. Best for the one-handed portrait promise
- Swipe-to-move — directional swipe queues one tile of movement. Better for accessibility, worse for panic
DROP is a fixed button in the lower-right. One tap, one can, no hold, no charge — see §8.7. It is the only button in the cluster.
There is no KICK button. The kick fires automatically on walking into a can while holding an Ice Cube, and Gameplay Rules §9 owns the trigger. The cost lands on the player, not on the layout: an Ice Cube permanently removes the ability to back up against a can, because there is no way to decline a kick.
Haptic on drop, on pickup, on death. Nothing else, or it becomes noise.
8.7 The can — timing and inputs¶
There is no pre-placement mechanic. Every can is already shaken. The player taps DROP, the can is placed on the current tile, and it detonates 2.0 seconds later. There is no hold-to-charge, no accelerometer, no meter, and no variable fuse.
This is the right call and it buys more than it costs:
- One discrete input. DROP is a tap, not a gesture with a duration, so it never competes with the movement thumb. The whole point of the floating joystick is that the left thumb is busy; a charge input on the right thumb would fight it
- No motion-sensor permission and no mechanic that breaks when the phone is flat on a table or the player is lying down
- Netcode gets simpler. A drop is a single timestamped event with a deterministic outcome. The server places the can at tick
Tand everyone knows it bursts atT + 30ticks. There is no charge state to reconcile, and no way for two clients to disagree about how shaken a can was - Threat assessment is uniform. Every can on the grid is the same 2-second clock, so a player reads the board by counting fuse rings, not by remembering who charged what. In a 200ms-readability game that matters more than the depth a variable fuse would add
Consequences to hold:
- The fuse ring is the entire timing UI (style guide §6). With no meter and no variable fuse, that ring is the only thing telling a player how long they have. Its priority in the draw order is now non-negotiable
- Depth moves to position and chaining, which is where it should be in this genre. The interesting decision is where and when to drop, plus how the blast propagates through other cans — not how long you held a button
- The
place_cananimation carries the shake as fiction, not as input: wind-up, shake, throw, recovery hop. The store copy's "give it a shake" stays honest because the Chugger does it, in one motion, on your behalf
8.8 Canonical timing constants¶
Three documents reference these. This table is the source; everything else cites it.
| Constant | Value | Notes |
|---|---|---|
| Fuse, total | 2.0s | From placement to burst. Fixed, never modified by any pickup |
| Armed state begins | T−0.8s | Swelling, seam glow, pitch rise (style guide §6) |
| Burst flash | 1 frame | Full-opacity white |
| Blast damage window | 3 ticks / 0.2s | The only lethal period. Ticks are canonical |
| Blast VFX duration | 0.4s | Damage window is over long before the foam is |
| Chain detonation | immediate | A blast reaching a live can detonates it that tick, ignoring its remaining fuse. A sliding can entering a live blast tile detonates the same way — Gameplay Rules §9.1 |
| Kicked can | keeps its original fuse | Sliding does not reset or extend the clock — an Ice Cube kick is a way to move a problem, not to postpone it |
| Timed effects | 5s / 5s / 3s | Coffee Bean, Honeycomb Wax, Glue |
Chain detonation is the one that needs a decision recorded rather than assumed. Immediate propagation is the Bomberman convention and it is what makes chains feel like chains. The alternative — a short fuse-shortening instead of instant detonation — reads as lag on a mobile screen.
9. Chat Component¶
An overlay, not a screen. Available in Lobby and Match Results only.
- Nakama channel scoped to the match ID, created on lobby open, destroyed on match end
- No global chat. No direct messages. No chat during live play
Why no in-match chat: nobody types during a 90-second real-time round, the panel occludes the field, and it is a moderation surface with no gameplay benefit.
Quick-chat only — locked. A fixed set of 8–12 canned phrases and emotes, and no free text anywhere in the app. It is what makes the 13+ age rating (§14) defensible in a game whose art will attract players younger than it. It is also instant on mobile where typing is not, and is what most games in this genre converge on anyway. Free text can be added later; it cannot be taken away later, and adding it reopens the rating.
Moderation is still a launch requirement, not a phase two. A closed phrase set removes the authoring surface, not the reporting one — a player can still spam, and display names are user-generated text:
- Profanity filter, server-side, on send — client-side filtering is cosmetic. It governs display names in full, and the phrase set trivially, since a fixed set is filtered once at authoring time
- Report and block, with the reported message captured
- Mute in Settings
- Display name filtering at the same standard
10. Content Delivery¶
- Base app ships: core assets, HUD, four Chuggers with base skins, one arena pack, Classic flavour, Classic blast
- Additional packs: Unity Addressables, remote catalogue, downloaded on purchase or on first selection
- Maps: bundled baseline, server adds, never replaces. The base app ships a complete playable set for every bundled pack; the server may add maps to the rotation but may never replace or remove one. Three consequences for the loader: map IDs are permanent and namespaced (
diner/crossroads), a missing map is fetched at Lobby before the ready check (a.mapis under 4 KB), and server-added maps are validated server-side on upload — a bad bundled map breaks a build, a bad server map breaks a live match. See Asset Architecture §10.2 - Multiplayer pack rule: every player in a match needs the arena's assets, so the pack downloads for all players at Lobby. Packs are free to play, purchasable to host. Getting a taste of a pack you don't own is the strongest possible store pitch, and it removes the worst failure case — a lobby that can't start
11. Persistence¶
| Data | Where | Notes |
|---|---|---|
| Session tokens | Device secure storage | Keychain / Keystore |
| Settings | Local + storage sync | Local wins on conflict |
| Equipped loadout | Nakama storage, user-owned | Server authoritative |
| Owned cosmetics | Nakama storage, server-write only | Never client-writable |
| Currency | Nakama storage, server-write only | Client-writable currency is free currency |
| Stats | Nakama storage / leaderboards | Written by the match handler |
| Match history | Server | Last 20, for Profile |
| League points | Nakama leaderboard per club, server-write only | incr at MATCH_END. §17.10 |
| League placement | Nakama storage league, server-write only |
Tier, club ID, week ID |
| League history | Nakama storage league_history, server |
Last 8 weeks |
| League config | Nakama storage, server | Cut counts, point yield, club size, cutoff. Tunable without a client build |
| Single player progress | Local | No server dependency |
Rule: anything a player could gain by cheating is written only by the server. Client writes are limited to preferences and the equipped-cosmetic pointer, and even that is validated against ownership.
12. Development Environment¶
- Unity MCP server installed for agent access. Useful for scaffolding scenes, wiring prefabs and bulk asset operations — keep gameplay-critical code under normal review regardless of who wrote it
- The Nakama dev instance is separate from the production one — two VMs behind the shared reverse proxy, reached as
dev.fizz-frenzy.comandprod.fizz-frenzy.com. Match handler changes take a server restart, and this is what keeps that restart off the instance a playtest is running against - Build config: dev / staging / prod, differing in server host, log level and cheat menu availability. Staging is currently folded into dev; a third environment is a third site block on the proxy and a third VM, not a client change. The cheat menu (spawn pickup, set range, kill player, skip round) pays for itself in the first week of tuning
12.1 Frame rate — 60, set explicitly¶
The application sets Application.targetFrameRate = 60 at startup, and clears QualitySettings.vSyncCount so that it governs. Both live in AppRoot, which runs once in the Bootstrap scene.
It must be assigned rather than inherited. Unity's mobile default is 30, and a build that never sets the field simply runs at it. A frame-time figure gathered against an unset target measures the vsync cap, not the game, so it cannot answer the question §12.2 asks.
Why 60 and not 30. The simulation is tick-locked at 15 Hz regardless (§8.8), so the render rate buys no simulation fidelity at all — what it buys is input-to-photon latency, which at 60 is roughly half what it is at 30. §4.2 calls corner assist the single most important feel mechanic in the genre, and it is exactly the mechanic that reads as unresponsive when the display lags the thumb. A match is 4–7 minutes, which makes the battery argument for 30 weak.
No platform branch. The same target applies to the Windows dev build, which would otherwise free-run at the monitor's refresh rate while the phone ran at 60 — the dev-build-diverges-from-device split this section exists to prevent.
12.2 Performance measurement¶
The platform floor is AndroidMinSdkVersion 34 (Android 14), set in Project Settings — nothing older than API 34 is a target for this build. The hardware floor is not yet defined. §5.8.1 rule 3 decides screen geometry — tile size never below 80 logical px — but says nothing about a GPU, and no minimum-spec device is named.
Until one is, treat any frame-time number as provisional and record the device model alongside it. The dev overlay does this automatically.
Naming and measuring the device belongs to implementation-plan M5, when art and VFX land — the first point at which a frame budget is a real constraint. Two obligations:
- Name the device, and acquire one. Naming has to happen first; the geometric floor is not a device.
- Measure with art on screen. Greybox measurements on a flagship confirm only that the frame rate is set and being met. They say nothing about headroom and must not be cited as evidence that the game performs.
The risk being carried is that the named device turns out to be too slow for the art direction, which would cost a rendering rework.
A percentile is not a drop detector. The overlay reports p50, p90 and p99 over a 600-frame window, which discards the worst six frames — and a dropped frame, worth ~33 ms against a 16.7 ms budget, is exactly what lands there. On hardware that comfortably holds the target this is academic, because the distribution is flat. On minimum-spec hardware it is the entire question, and a clean-looking p99 can sit above a hitch a player would see. Any frame-time instrument used to qualify a device must report a maximum, not percentiles alone.
13. Unity Scene Architecture¶
Three scenes, additive:
| Scene | Contents | Lifetime |
|---|---|---|
Bootstrap |
Network service, audio manager, state machine, DI root | Permanent |
UI |
All menu screens as prefab panels under one canvas stack | Permanent |
Match |
Grid, entities, VFX, match HUD | Loaded/unloaded per match |
Menu screens are panels in a navigation stack, not scenes. Scene loads on a mid-range Android phone cost hundreds of milliseconds and a hub-and-spoke menu that pays that on every tap feels broken.
Only Match is loaded and unloaded, which is also where the memory pressure is — unloading it between matches gives you a clean slate and a natural GC point.
14. Age Rating & Compliance¶
The store listing targets 13+. A cartoon soda game will still attract under-13 players; targeting 13+ is a statement about who the app is offered to, and the design has to earn it rather than declare it. What follows:
- Chat is quick-chat only, permanently. The fixed phrase set of §9 with server-side filtering on send is what makes 13+ defensible. Free text is the change that would reopen the rating
- Analytics is a consent-gated GDPR flow, not a COPPA parental-consent flow: one opt-in screen, refusable without losing access to the game, and no SDK that fires before the answer
- No ad SDK at launch (Economy & Progression §8). If ads are ever added they are opt-in rewarded only, and the rating is re-examined first
- No COPPA verifiable-consent path and no Google Play Designed for Families track are built
- No gambling mechanics — no loot boxes, no gacha, no paid Surprise Cubes, ever (Economy & Progression §1.3). That holds whatever the rating is
A 13+ store rating is not a 13+ consent age. GDPR Article 8 lets each member state set the digital-consent age between 13 and 16, and Germany — the intended soft-launch region — sets it at 16. Consent-based analytics for 13–15 year olds in the EU therefore needs parental authorisation or a lawful basis that is not consent. That is a consent-flow problem for the submission checklist, not a reason to move the rating.
15. Open Questions¶
- Rounds or single elimination? Best-of-3 assumed. Single-round matches are faster but harsher on a player eliminated in the first 20 seconds — and spectating your friends for 70 seconds is a churn moment.
- Region routing. One server in Germany means playable EU latency, poor US, bad APAC. Fine for soft launch, needs a plan before wide release.
- IAP validation. Nakama has no IAP validation for every store variant out of the box; decide whether purchases validate server-side through Nakama's IAP hooks or a separate service. A separate service is a second host and a second certificate, so this is owned by implementation-plan M0 alongside the TLS hostnames.
- Is 22:00 UTC the right instant? It is 23:00 or midnight in Berlin depending on the season, and mid-afternoon in the Americas. If the intent is "Sunday night" for a specific launch region, the value changes; the fixed-offset decision does not. Confirm the number against the soft-launch region before the first week runs.
16. Cross-Document Consistency¶
Five parts, one project. This section says who owns what, so that when two parts disagree there is a rule rather than an argument, and records the dependencies that are invisible from inside a single part.
16.1 Ownership¶
| Subject | Authoritative document |
|---|---|
| Colour values, silhouette rules, character design, arena mood, VFX budget, audio | Design Style Guide |
| Asset slots, naming, canvas sizes, atlases, draw order, map file format, validation gates | Asset Architecture |
| Screens, navigation, state machines, netcode, session model, persistence, player count, grid dimensions | Framework & Flow (this document) |
| Leagues, leaderboards, weekly progression, club allocation | Framework & Flow (this document) — §17 |
| Simulation rules, movement, collision, blast, pickup effects, balance constants, resolution order | Gameplay Rules & Mechanics |
| Currency, XP, prices, yields, level curve, rollover payout policy | Economy & Progression |
Where a subject spans two documents — HUD layout, for instance — the style guide owns appearance and this document owns structure and position. For pickups: Gameplay Rules owns effect and cap, Asset Architecture owns silhouette and animation.
The simulation is specified in Gameplay Rules, not here. §8 of this document covers the match as an application state; the rules of the game itself — movement model, blast propagation, self-damage, the walk-off rule, drop table, resolution order — live in Gameplay Rules, which supersedes §8 wherever they overlap.
16.2 Dependencies and scope limits¶
1. The Reinforced tier is not built in version 1. Style Guide §7 describes a two-hit tier — ice blocks in the Diner, ice buckets at the Beach Kiosk — but it has no slot in the arena manifest (Asset Architecture §5.1) and no glyph in the map format (Asset Architecture §A.5). Anything dressed as reinforced ships as an ordinary destructible; R is reserved for format 2.
2. Version 1 hazards are whatever H can express — a tile that is periodically lethal on a fixed rhythm (Gameplay Rules §12.1). That covers the Diner's soda fountain. The Beach Kiosk tide is the sudden-death ring, not a hazard tile. Teleporters (T, reserved), ricochet and persistent floor states such as syrup puddles need map format 2.
3. Two netcode consequences of the simulation rules:
- Blasts destroy uncollected pickups — a
pickup_destroyedcase on theEVENTopcode, distinct frompickup_taken - The live-blast tile set crosses tick boundaries (Gameplay Rules §9.1, §13) — it is the only simulation state that does, and it must be included in
STATE_FULLor a reconnecting client can walk a can through a blast the server knows about and it does not
4. Round points are load-bearing outside the match. Gameplay Rules §11 defines the round point as a match-local scoring pip; §17.3 makes it the unit of weekly progression. Tuning the round structure tunes the ladder — changing rounds-per-match, or the draw rule in Gameplay Rules §11.1, changes the rate at which the entire player base climbs.
17. Leagues & Leaderboards¶
This is the retention system. Everything else in this document serves a single match; this section is the reason a player opens the app on a Tuesday. It is almost entirely server-side — the client displays a standing it never computes and never writes.
Scope boundary. This section defines placement: how points are earned, how players are grouped, how the ladder moves. It does not define rewards. Rollover payouts belong to Economy & Progression, which decides the question as none in version 1 (Part 5 §3.2) — and §17 is written so that rewards can be bolted on later without touching the placement logic.
17.1 The model in one paragraph¶
Every player belongs to exactly one club — 50 players competing over one week. A club sits at one of six league tiers. You earn league points by playing matchmade games: one per round won, plus one for finishing the match. At 22:00 UTC on Sunday the week closes: the top of every club moves up a tier, the bottom 15 move down, the rest hold. Everyone is then re-dealt into fresh 50-player clubs at their new tier and the score resets to zero.
The promotion count narrows at the top. Fifteen promote out of Tin, Bronze and Silver; only ten promote out of Gold and Diamond. Demotion is fifteen everywhere except Tin. That asymmetry is what makes the ladder a pyramid rather than a column — §17.2.1.
17.2 The ladder¶
Six tiers, bottom to top:
| # | Tier | Promotes to | Promote | Hold | Demote | Demotes to |
|---|---|---|---|---|---|---|
| 1 | Tin | Bronze | 15 | 35 | — | — (floor) |
| 2 | Bronze | Silver | 15 | 20 | 15 | Tin |
| 3 | Silver | Gold | 15 | 20 | 15 | Bronze |
| 4 | Gold | Diamond | 10 | 25 | 15 | Silver |
| 5 | Diamond | Elite | 10 | 25 | 15 | Gold |
| 6 | Elite | — (ceiling) | — | 35 | 15 | Diamond |
Counts are given at the reference club size of 50. As proportions: 30% promote below Gold, 20% promote from Gold and Diamond, 30% demote everywhere except Tin. Under-strength clubs scale by proportion (§17.4); nothing hard-codes 15 or 10.
- Every new account starts in Tin. There is no placement match, no seeding, no MMR-based entry. A player's first week is the placement
- Tin never demotes. The bottom 15 of a Tin club hold their tier
- Elite never promotes. The top of an Elite club holds
- The narrowing happens at Gold. Below that, the ladder is a conveyor and it should be — a player who plays regularly should reach Gold without being filtered. Above it, promotion is a cut
- Tier is a display and grouping concept only. It is not matchmaking input in version 1 — see §17.9
17.2.1 Why the taper exists, and what shape it produces¶
The counts above are not arbitrary, and the reasoning should survive the first person who proposes simplifying them.
Flat counts produce a column, not a ladder. Between any two adjacent tiers, the up-flow is promote_rate × population_below and the down-flow is demote_rate × population_above. Those balance when the populations are in the ratio promote_rate ÷ demote_rate. With 15 up and 15 down at every boundary, that ratio is 1 — every tier holds the same number of players and one in six ends up in Elite. A top tier containing 17% of the player base is not a top tier.
Promoting 10 from Gold and Diamond fixes it for the cost of two config values. The ratio above Silver becomes 20% ÷ 30% = two-thirds, so each of the two highest tiers settles at two-thirds the size of the one below.
| Tier | Share of players at equilibrium |
|---|---|
| Tin | 19.6% |
| Bronze | 19.6% |
| Silver | 19.6% |
| Gold | 19.6% |
| Diamond | 13.0% |
| Elite | 8.7% |
Roughly one player in eleven reaches Elite instead of one in six, and the four lower tiers stay flat — which is correct, because those tiers exist to move a regular player upward, not to filter them.
Three caveats on those numbers.
- It is an equilibrium, not week one. Everyone starts in Tin, so the distribution fills from the bottom and takes roughly as many weeks as there are tiers to approach these shares. Do not read the week-3 numbers as a failure of the model
- Inactivity distorts it. Frozen players (§17.7) hold their tier without contributing to either flow, which slows the whole system and thickens the middle. The live distribution will be flatter than the table above
- These are the levers if it drifts. Promotion and demotion counts are per-boundary server config (§17.10). If Elite still inflates after four weeks of real data, drop Diamond's promotion to 8 before touching anything else — it is the single highest-leverage number in the section
17.3 League points¶
One league point per round won, plus one for completing the match. No separate scoring model, no placement table, no kill counters — the round pips a player already watched accumulate on the Round Results screen are the points, and they are credited in a lump at MATCH_END.
| Event | Points |
|---|---|
| Round won | +1 |
| Match completed | +1, regardless of result |
| Round drawn (simultaneous death) | 0 — the round is replayed, Gameplay Rules §11.1 |
| Round lost | 0 |
| Match won | 0 additional — the match win is already worth more rounds |
| Forfeit, or disconnect beyond the §7.4 threshold | 0 for the whole match, retroactively — including the participation point and any rounds already won |
The participation point is what makes the week survivable for the median player. Without it, someone who loses every round scores zero across a whole evening and can finish a week on nothing, which reads as the game telling them not to bother. One point for finishing means showing up always moves the bar — a losing player still climbs, just far more slowly than a winning one. A best-of-3 pays 1 to the player who won nothing and 4 to the player who swept it, which is a wide enough gap to keep winning worth it.
Two things this deliberately does not do. It is not awarded for a forfeit or a long disconnect, or quitting a losing match would pay the same as finishing one. And it is not scaled by match length — see below.
Consequences to accept deliberately:
- A best-of-5 yields more round points than a best-of-3. Roughly proportional to time spent, which is the correct relationship. No normalisation
- The participation point is flat, and that would favour short matches — except Quick Match does not let the player choose. A fixed +1 on a 3-minute match is worth more per minute than the same +1 on a 10-minute one, so a player able to pick best-of-1 would grind those exclusively. Quick Match takes no configuration (§5.4) and the matchmaker sets the round count, which closes the exploit structurally rather than with a rule. If Quick Match ever gains a round-count option, the participation point has to scale with it or the option has to be dropped — this is the constraint that will be forgotten
- Points never decrease. There is no subtraction, ever. A losing streak costs a player rank relative to their club, which is punishment enough and reads as fair. Visible point loss in a casual game is the single most reliable source of one-star reviews
- Every point yield is a config key.
point_yield: {round_won: 1, match_completed: 1}lives inleague_config(§17.10). Tuning the ladder's climb rate must never require a client build
17.4 Clubs¶
A club is 50 players. It is the whole of the leaderboard a player ever sees, and the number is chosen so that the standing fits comfortably in a scrollable list, a rank feels legible ("31st of 50" means something) and both cut lines are visible after a few flicks.
| Property | Value |
|---|---|
| Target size | 50 |
| Minimum viable size | 30 |
| Lifetime | One week. A club is created at rollover and destroyed at the next |
| Identity | {week_id}:{tier}:{shard_index} — server-generated, never reused |
| Membership | Fixed at rollover, plus mid-week backfill (§17.8) |
Sharding. When a tier holds more than 50 players — which it will, immediately, in every tier below Elite — the tier's population is dealt into as many clubs as it takes. Assignment is a uniform random shuffle seeded on the week ID, so the job is reproducible if it has to be re-run.
- Random, not score-seeded. Grouping the tier's strongest 50 into one club builds a hidden second ladder inside every tier and makes promotion depend on which shard you landed in far more than on how you played
- Remainder rule: no club below 30. If the final shard would be short, redistribute across the tier's clubs so every club at that tier sits between 30 and 50. A tier with fewer than 30 players total forms one under-strength club and that is fine
- Cut lines scale with club size, and differ by tier. Promotion is 30% of the club below Gold and 20% at Gold and Diamond; demotion is 30% everywhere except Tin. At the reference size of 50 that is the 15 / 10 / 15 of §17.2. Under-strength clubs use
round(rate × size). Never hard-code either number, and never assume promotion and demotion counts match - Regional bucketing is a post-launch option. Sharding by region before shuffling would align club activity with a shared evening and make standings move when players are watching. It also fragments small tiers. Not v1
17.5 The weekly rollover¶
Sunday, 22:00 UTC. The single most important line in this section, and the one most likely to be got wrong.
- UTC, fixed, no daylight adjustment. A job scheduled against a DST-observing zone either runs twice or not at all one week a year, and both failures land on a live leaderboard. The cutoff is an absolute instant
- The client renders the countdown in device-local time, never as "22:00". A Berlin player sees 23:00 in winter and midnight in summer; a São Paulo player sees Sunday evening. The countdown is a duration, and durations do not have this problem
- Matches are credited to the week that is open when
MATCH_ENDfires. A match still running at 22:00:00 credits to the new week. Announce it that way in the UI — "results after the reset count toward next week" — rather than trying to drain in-flight matches. Matches run 4–7 minutes; a drain window is a five-minute race condition in exchange for nothing
The job, in order. Compute everything against a snapshot, then apply.
- Freeze at 22:00:00. Close point writes to the outgoing week
- Snapshot every club at every tier, resolved to final rank order (§17.6)
- Resolve movements for all clubs — promote / hold / demote — against the snapshot only
- Collect each tier's incoming population: holds, plus promotions from below, plus demotions from above
- Deal each tier into clubs of 50 (§17.4)
- Write new placements, zero the scores, open the new week
- Publish each player's outgoing result — old rank, points, movement — for the post-rollover notification
Step 3 is where the bug lives. If movements are applied tier by tier as they are computed, a player promoted out of Silver can be picked up again as part of Gold's population and evaluated twice in one rollover. Resolve against the frozen snapshot, apply once.
The job must be idempotent and resumable. It touches the entire player base at a fixed instant, which means it will eventually be interrupted by a deploy, a restart or an outage. Key every write on week_id so a re-run overwrites rather than double-applies, and checkpoint per tier.
17.6 Rank order and tie-breaks¶
At the cut lines, ties are not a display nicety — they decide who moves. The order is fully deterministic:
- League points, descending
- Earliest timestamp at which the player reached that point total — first to arrive holds the higher rank
- User ID, lexicographic ascending — a final tie-break that can never itself tie
Rule 2 is worth the extra column. It rewards playing early in the week rather than sniping the boundary on Sunday evening, it is intuitive when explained, and it makes the standing stable: a player's rank can only be taken by someone who actually overtakes them, so the list stops shuffling under a reader's thumb for no visible reason.
Store the reach-timestamp on the same write as the point increment. Reconstructing it later from match history is not possible once points have been aggregated.
17.7 Inactivity¶
| Situation | Behaviour |
|---|---|
| Played at least one scoring match this week | Ranked normally, promotes or demotes as the standing says |
| Played none | Frozen. Holds tier, does not demote. Occupies the bottom of the standing at 0 points and is drawn greyed |
| Two consecutive silent weeks | Removed from club allocation. Tier is retained on the account |
| Returns after removal | Placed into a club at their retained tier at the next rollover, or backfilled immediately per §17.8 |
Not demoting the inactive is the deliberate half. A player who took a week off and returns to find they have been knocked down a tier for not playing has been punished for having a life, and that is a churn event. The system's job is to reward playing, not to penalise absence.
Removing them after two weeks is the other half, and it matters more than it looks. Dead accounts sitting at zero make every club look abandoned and quietly guarantee that the bottom 15 is mostly ghosts — which makes demotion meaningless for the active players above them. Clubs must be built from people who are actually going to play this week.
17.8 Mid-week joins and backfill¶
New and returning players do not wait for Sunday.
- A player needing a club is placed into an existing club at their tier with a free slot, preferring clubs opened this week and least-full first
- If no club at that tier has a slot, open a new one. It may sit under 50 for the rest of the week; the 30-minimum applies at rollover, not at creation
- A backfilled player joins at 0 points with the week already in progress, is eligible for promotion and demotion on the same terms, and is flagged
joined_latein the payload so the client can render a small marker rather than leaving them looking like they lost badly
A brand-new account therefore lands in a Tin club within seconds of finishing its first match, which is the point — the ladder has to be visible in session one or it is not a retention system.
17.9 What earns points, and what farming this invites¶
Only matchmade Quick Match results award league points. Everything else awards zero:
| Mode | Awards league points |
|---|---|
| Quick Match (matchmaker ticket, §5.4) | Yes |
| Match Browser lobby | No |
| Private match by code | No |
| Single player vs AI | No |
This is not a scope cut, it is the anti-collusion rule. Two accounts in a private lobby can trade rounds indefinitely; there is no clever detection that beats a determined pair, and the only robust answer is that hand-assembled matches never touch the ladder. Single player therefore never feeds the league; what it pays in XP and currency is Economy & Progression §1.2.
Further guards:
- Daily soft cap. After 20 scoring matches in a UTC day, yield halves. Legitimate heavy play is unaffected; scripted farming stops being linear
- Forfeit and long disconnect score zero for the entire match, retroactively, including rounds already won and the participation point. Otherwise the optimal play is to win round one and quit
- A match in which the player issued no input scores zero. The participation point creates an incentive that did not previously exist — queue, stand still, collect 1, repeat. The match handler already sees every
INPUTmessage, so requiring at least one movement or drop input per round is a cheap check. Without it, the cheapest farm in the game is doing nothing - Queue-sync collusion is a real soft-launch risk and is not solved here. With a small population, two accounts queueing simultaneously will land in the same match often enough to be exploited. Instrument it — log the frequency of repeat roster sets per rolling hour — and hold the countermeasure until the data says it is needed. Do not build a heuristic against a population that does not exist yet
Tier is not matchmaking input in v1. A Tin player can and will be matched against Elite. Splitting the matchmaker by tier at soft-launch population sizes means queue times measured in minutes, which is a worse problem than an uneven match. Revisit when concurrency supports it.
17.10 Server implementation¶
Nakama has most of this. Do not build a bespoke scoring store.
| Concern | Mechanism |
|---|---|
| Club standing | One Nakama leaderboard per club, ID league:{week_id}:{tier}:{shard}, created on demand at rollover |
| Point credit | leaderboardRecordWrite with the incr operator, called from the match handler at MATCH_END. Never from the client |
| Tie-break metadata | Record subscore = reach-timestamp; metadata carries joined_late and match count |
| Placement | Nakama storage collection league, key = user ID, server-write only. Holds tier, club ID, week ID |
| History | Storage collection league_history, last 8 weeks. Enough for a "best tier reached" line on Profile |
| Rollover | A scheduled server job, not a Nakama tournament reset — the re-sharding step has no native equivalent |
| Config | league_config storage record: per-boundary promote/demote counts, point_yield, club size, daily cap, cutoff instant. All tunable without a client build |
Nakama's tournament feature is the wrong tool here despite the surface fit. It gives resets and durations, but it does not give per-shard cohorts of 50 that are re-dealt every cycle, and bending it into that shape costs more than the scheduled job does.
Load. The rollover reads and rewrites one record per active player at a fixed instant, once a week. At soft-launch scale this is trivial; at scale it is the single largest write burst the backend ever takes. Batch per tier, checkpoint between tiers, and do not schedule anything else at 22:00 UTC Sunday.
17.11 Client responsibilities¶
The client displays. It does not compute.
Everything below arrives in one RPC, league_get:
{
"week_id": "2026-W31",
"closes_at": 1785362400,
"tier": "silver",
"club_id": "2026-W31:silver:0042",
"club_size": 50,
"promote_cut": 15,
"demote_cut": 36,
"you": { "rank": 22, "points": 14 },
"standings": [
{ "rank": 1, "name": "…", "chugger": "nim", "skin": "…", "points": 41, "joined_late": false }
]
}
- The cut lines come from the server, as ranks. The client must never derive them from a hard-coded 15 — the Gold and Diamond taper (§17.2.1) and under-strength clubs both break that assumption immediately. The example above is a Silver club, where
promote_cutis 15; the same client renders a Gold club withpromote_cut10 and no code change - Payload is ~4 KB for a full 50-row club. Fetch it whole; there is no pagination
- Cache for 60 seconds. Refresh on entering the screen, on
POST_MATCH, and on app foreground - Offline shows the last cached standing with a staleness marker and a disabled countdown. Leagues never block Home and never gate Play
- The countdown is computed from
closes_atagainst device clock, and must tolerate a wrong device clock by clamping at zero rather than displaying negative time
17.12 Where it appears¶
| Screen | Addition |
|---|---|
| Home (§5.2) | Tier badge on the player badge chip → Leagues. Notification dot when an unseen rollover result is waiting |
| Match Results (§5.10) | The league beat — points earned, new total, rank delta. Quick Match only; silent in every other mode |
| Profile (§5.13) | Current tier, best tier reached, last 8 weeks |
| Leagues (§5.17) | The standing itself |
The Match Results beat must not delay REMATCH. It animates alongside the standings, not as a gate in front of them. A rank ticker that has to finish before a player can queue again is a system that punishes engagement.
17.13 Failure modes to design against¶
- The rollover job does not run. Players see a countdown at zero and a frozen standing. The client must render
closes_atin the past as "Results are being calculated", not as a negative number or a stuck timer. Alert on the job, not on the symptom - A club empties. Everyone in a 30-player club goes silent by Wednesday. Nothing breaks — the cut still resolves — but the screen is a wall of zeroes. Backfill (§17.8) preferring least-full clubs is the mitigation
- A player's placement record is missing. Treat as a new player: allocate a Tin club, log loudly. Never show an empty leagues screen and never fail Home because of it
- Two devices, one account, simultaneous matches. The
incroperator is atomic, so points are correct. The reach-timestamp is whichever write landed second — acceptable, and not worth a lock
Part 3 — Design Style Guide¶
A 4-player grid brawler. Anthropomorphic critters called Chuggers drop shaken soda cans and run. Two seconds later the can bursts, spraying foam down four lanes. Last Chugger fizzing wins.
1. Design Principles¶
Every art decision on this project answers to five rules. When a choice is contested, the earlier rule wins.
1. Readable in 200 milliseconds. The player is scanning a grid mid-panic on a 6-inch screen. Every tile must be classified — safe, breakable, solid, deadly — by silhouette and colour alone, at arm's length, with a thumb covering the bottom third of the display. If it only reads when the player stops to look at it, it has failed.
2. Danger is warm. Safety is cool. Anything that will hurt the player trends orange-red-yellow. Anything inert trends blue-grey-teal. This mapping is never broken for aesthetic reasons, in any arena, in any seasonal skin.
3. Silhouette before detail. Every asset must be identifiable as a pure black shape. Detail is a reward for players who look closely — it is never load-bearing.
4. Cartoon physics, honest hitboxes. Art can squash, stretch, wobble and overshoot as much as it likes. The collision grid underneath is rigid, square, and never lies. Foam VFX must never extend visually past the tiles it actually damages.
5. Funny, not silly. The humour is in the situation — a shaken can, a bad decision, a friend's timing. It is not in random gags, memes, or references. The world takes itself seriously; the player does not.
2. Brand Identity¶
2.1 The In-World Brand¶
All cans in the game are Frenzy Cola — a fictional soda brand that owns the fiction. This gives us a logo we control, a mascot for the can itself, and a clean in-world reason for flavour variants that double as monetised skins.
Frenzy Cola's own visual identity is deliberately retro-commercial: mid-century diner signage, hand-painted lettering, sun-faded advertising. It looks like a brand that has been around since 1955 and has never updated its packaging. This is what makes both arenas cohere — the Diner is where Frenzy Cola came from, the Beach Kiosk is where it goes on holiday.
2.2 Logo¶
The Fizz Frenzy wordmark is not the Frenzy Cola logo. Keep them separate.
- Heavy rounded sans, custom-drawn, slight upward arc
- The two Zs in "Fizz" carry the personality: angled like bubbles rising, never symmetric with each other
- Bubble cluster escaping the top-right of the mark — always odd-numbered, never a tidy arc
- Minimum clear space: the height of the cap F on all sides
- Never: outlined-only, rotated past 5°, on a busy photographic background, recoloured outside the approved palette
3. Colour¶
3.1 Core Palette¶
| Role | Name | Hex | Use |
|---|---|---|---|
| Primary | Frenzy Red | #E63B2E |
Logo, cans, primary CTAs |
| Primary | Cola Brown | #4A2618 |
Can bodies, deep shadow, outlines in warm areas |
| Accent | Fizz Cream | #FFF4E0 |
Foam, highlights, UI text on dark |
| Accent | Bottle Teal | #1FA5A0 |
Safe zones, cool props, secondary UI |
| Accent | Sunbeam | #FFC42E |
Power-ups, rewards, currency |
| Neutral | Slate | #2B3440 |
Indestructible walls, HUD chrome |
| Neutral | Chrome | #C9D4DC |
Metal, highlights on solid geometry |
3.2 Player Identity Colours¶
Locked and never reused for anything else on the field.
| Player | Hex | Notes |
|---|---|---|
| P1 | #E63B2E Red |
|
| P2 | #2E7FE6 Blue |
|
| P3 | #3FBF54 Green |
|
| P4 | #F2A23C Orange |
Deliberately warmer than Sunbeam so it never reads as a pickup |
Colour-blind support is mandatory, not optional. Each player also carries a permanent shape badge — circle, square, triangle, star — on every UI element referencing them, and on the field as the player marker under the Chugger's feet (Framework & Flow §5.8). The player colours above are chosen to survive deuteranopia; verify P3/P4 in simulation before locking any skin.
Four is the version 1 player count and these four colours are the complete set. A fifth and sixth colour is a version 2 problem, and a genuinely hard one — two more hues that survive all three colour-blindness simulations, stay clear of the warm hazard band in §3.3, and remain distinct from Sunbeam and from each other. Do not pre-author them; see Framework & Flow §1.1.1.
3.3 Hazard Colour Law¶
- Live can, armed: pulses Frenzy Red → Sunbeam, accelerating as the fuse burns
- Foam blast: Fizz Cream core, Frenzy Red rim
- Blocked / solid: never uses any warm hue above 20% saturation
- Power-up: always Sunbeam-cored, regardless of arena
An arena skin may change everything else. It may never change these four.
3.4 League & Progression Colour¶
The six league tiers and the two standing bands are the only place in the product where colour carries status rather than danger. That makes them a real risk to the hazard law above, and the rules below exist to contain it.
| Tier | Base | Hex | Treatment |
|---|---|---|---|
| Tin | Dull grey-blue | #8A97A3 |
Flat, matte, no specular. It should look cheap |
| Bronze | Warm brown | #A5703C |
Low specular. Kept desaturated so it never reads as a hazard cue |
| Silver | Cool light grey | #C9D4DC |
Chrome from the core palette. Broad soft highlight |
| Gold | Deep yellow | #D9A21B |
Darker and less saturated than Sunbeam #FFC42E |
| Diamond | Pale cyan | #7FD8E8 |
Translucent read, faceted highlight |
| Elite | Fizz Cream on Slate | #FFF4E0 / #2B3440 |
The only two-tone badge, and the only animated one |
Four rules, in priority order:
- Gold is not Sunbeam. Sunbeam
#FFC42Emeans power-up, everywhere, permanently. A Gold badge at chip scale sitting near a currency counter must not read as a collectable. Hold Gold at least 15% darker and 20% less saturated, and verify the two side by side before locking - The demotion band is never in the 10°–45° hazard band. Use a desaturated slate-mauve — drab and sagging, per the debuff motion vocabulary in §10. Red on a leaderboard teaches the player that warm sometimes means merely bad, and that lesson costs a life on the grid
- The promotion band is cool and rising, drawn from Bottle Teal at low saturation. It must not use any player identity colour from §3.2 — a green promotion band next to P3's row is a false association
- All six badges survive greyscale at 64px. Bronze, Gold and Tin collapse toward the same mid-value when desaturated, so silhouette carries the tier, not fill — see Asset Architecture §6.6.1. Verify in deuteranopia, protanopia and tritanopia simulation, the same gate the player colours passed
Tiers never appear on the play field. No badge, band or rank indicator renders inside the grid, in the HUD band, or on a Chugger. Status lives in the menus; the field is for danger.
4. Typography¶
| Role | Face | Treatment |
|---|---|---|
| Logo & headers | Custom rounded display | Hand-drawn, heavy, slight arc |
| UI & body | Nunito ExtraBold / Baloo 2 | Rounded terminals, generous counters |
| Numerals (timer, score) | Same, tabular figures | Non-tabular figures jitter and look broken when counting down |
- Minimum on-device size: 28px for anything the player must read during a match
- All in-match text carries a 3px Cola Brown outline — the background is chaos and unoutlined text disappears into it
- Never use all-caps for anything longer than three words
- No thin or light weights anywhere in the product
5. Character Design — The Chuggers¶
5.1 What They Are¶
Small, greedy, can-hoarding critters. They are creatures, not containers — this is a hard rule. The can is a distinct game object sitting on the grid, and if the player silhouette is also can-shaped, the two compete for recognition at exactly the moment recognition matters most.
5.2 Proportions¶
- 2.5 heads tall. Big head, small body, stubby limbs.
- Body fits within a single grid tile at 85% width — the 15% margin is what lets players judge whether they'll clear a gap.
- Head is the widest point. The silhouette reads top-heavy and slightly unstable, which sells the comedy of running away in a panic.
- Feet are simple ovals. No visible ankles, no toes, no shoes with detail.
5.3 Silhouette Rules¶
- Distinct from a can at 32px: cans are vertical cylinders, Chuggers are wide-topped blobs
- One strong asymmetric feature per character — an ear, a tuft, a tail — so players can distinguish characters even with the colour drained out
- Nothing thinner than 4px at reference resolution; thin appendages vanish on phone screens and alias badly in motion
- Held can always sits in the same position relative to the body, at 60% scale, so "this player is armed" is legible from any angle
5.4 Expression¶
Faces are simple: two eyes, a mouth, and a small species-appropriate muzzle and nose. Emotion comes from eye shape and body posture, not from detailed features.
Four required states, each distinguishable in peripheral vision: - Idle — half-lidded, bored, subtle breathing bob - Running — wide eyes, lean into direction of travel, ears/tufts trail behind - Panic (within blast radius, 0.5s before detonation) — pinprick pupils, mouth wide, whole body vibrating - Popped — comedic, not violent. The Chugger inflates, wobbles, and deflates with a raspberry, spinning off-screen. No gore, no damage state, no lingering corpse on the tile.
5.5 Launch Cast¶
Four characters, one per player colour, each a different animal with a strong distinct silhouette. The raccoon is the mascot and the default — raccoons and cans are a pairing that requires no explanation.
| Character | Animal | Colouring | Silhouette hook |
|---|---|---|---|
| Rax | Raccoon | Frenzy Red fur, Cola Brown mask, Fizz Cream belly | Mask, broad ringed tail — the mascot |
| Puff | Bear | Sunbeam yellow fur, Fizz Cream muzzle | Small round ears, broad head, cuddly round body — the roundest, stockiest silhouette |
| Nim | Squirrel | Warm chestnut #9A552E fur, cream belly |
Enormous bushy upright curling tail, clearly separated from the torso |
| Bolt | Bunny | Bottle Teal fur, Slate and Chrome steel-grey accents | Long upright ears — the tallest silhouette; tiny round tail, powerful rounded hind feet |
Species anatomy is literal: each Chugger gets its species' muzzle, nose, ears, paws and tail. Puff carries no pufferfish traits (no spikes, fins or inflate-when-panicked), and Bolt no lizard traits (no scales, dorsal plates, claws, low reptile posture or long horizontal tail).
Skins reskin the character but never alter the silhouette envelope. A seasonal hat may exceed the head bounds by no more than 20%, and never introduces a shape that could be mistaken for a power-up.
6. The Can¶
The single most important object in the game. It gets its own rules.
Three states, each unmistakable:
- Placed — sits upright, slight wobble, cool-toned, fuse ring at the base showing time remaining as a depleting arc. The full fuse is 2.0 seconds, fixed, on every can in the game
- Armed (final 0.8s) — visibly swelling, seams glowing, pulse accelerating, hissing louder
- Burst — one frame of white-hot flash, then foam sprays four lanes
The fuse ring is the most important UI element in the entire game and is never occluded by a character, prop, or particle. If a Chugger stands on the same tile, the ring renders on top of them.
Every can is identical. There is no shake level, no charge, no variable fuse and no meter — the can is already shaken when the Chugger throws it, and it bursts 2.0 seconds later, always. Bubble streaks on the can body are decorative and constant; they never encode a value.
This makes the fuse ring the only timing information in the game. With no meter anywhere on screen, a player reads the board entirely by counting rings. That is why the ring is never occluded, and why it gets smooth interpolation rather than the 12fps step used for characters (§12).
The shake lives in the animation, not the input: the place_can cycle is wind-up, shake, throw, recovery hop — one motion, performed on the player's behalf. See Framework & Flow §8.7 and §8.8.
7. Arena Readability System¶
Three tiers, distinguishable by silhouette and value alone. Every arena reskins these tiers; no arena adds a fourth.
Version 1 ships three: Indestructible, Destructible, Hazard. The Reinforced two-hit tier below is retained as design intent but is not buildable in version 1 — the asset architecture slot manifest has no reinforced slots and the map format has no glyph for it. Anything described as Reinforced in §8.3 and §9.3 either ships as an ordinary Destructible or waits for format 2. See Framework & Flow §16.2.
| Tier | Reads as | Value | Silhouette | Behaviour |
|---|---|---|---|---|
| Indestructible | Dark, cold, heavy | Darkest on screen | Full tile, flat top, hard edges | Never breaks |
| Destructible | Light, warm, cheap | Mid-value | 85% tile, one boxy parcel shape per arena; families differ only by a printed pictogram | One blast |
| Reinforced (v2) | Light but glassy/dense | Mid-value + specular | 80% tile, geometric, highlight | Two blasts |
| Hazard | Moving or glowing | Highest contrast | Breaks the grid rhythm | Kills, unowned |
Floor is always the lowest-contrast surface on screen. Nothing decorative ever stands on a tile. If it isn't one of the tiers above, it is painted flat on the floor or lives outside the play field.
8. Arena — The Diner¶
8.1 Mood¶
3am, empty, neon still humming. Chrome and cracked vinyl. Warm interior light against a cold blue night outside the windows. Nostalgic rather than grimy — this place is loved, just tired.
8.2 Palette Shift¶
Base palette, biased warm. Cola Brown and Frenzy Red dominate. Bottle Teal appears as vinyl booth upholstery and neon. Chrome does heavy lifting on the indestructibles.
8.3 Tier Dressing¶
Indestructible - Chrome-edged booth ends with red vinyl seats - The jukebox — corner anchor, glows, cycles colour on a slow loop - Counter segments with fixed stools - Walk-in fridge doors along the back wall
Destructible - Kraft cardboard parcels sealed with Frenzy Red tape, one shape for all four families. The family shows as a pictogram printed on the front: cola bottle, burger, fries, pie. (Changed 2026-09-25: the v0.5 mix of 12-packs, cup towers, napkin dispensers and pie cases was noisy at game size and players did not read it as blocks. A uniform box reads as the grid; the pictogram keeps the Diner's flavour.)
Reinforced (two-hit) — deferred to v2 - Ice blocks from the machine — crack, then shatter into a slippery puddle that persists for 4 seconds. Ships in v1 as a single-hit Destructible with no puddle, or not at all
Hazards & Props
Version 1 supports one hazard type — a tile that is periodically lethal on a fixed rhythm (map glyph H). The soda fountain qualifies. Teleporters, ricochet and persistent floor states such as syrup puddles need map format 2; see Framework & Flow §16.2.
- Bendy straw teleporters (v2) — chrome straw dispensers, paired by colour
- Syrup puddles — dark, glossy, slow the player and cling for one second after they leave
- Bottle caps — spin in place, ricochet a blast 90°
- Soda fountain — vents a low cloud of scalding steam (about one tile high) on a fixed timer, telegraphed by rising wisps, a gurgle and the grate's warm glow. Steam, never foam: it must not resemble the can blast (changed 2026-09-25: the v0.5 foam column, 2.5 tiles tall, was mistaken for a can going off). The warm hazard hue lives in the glow and the underside of the lowest puffs; the cloud itself is pale
8.4 Lighting¶
Warm pools under pendant lamps, cold blue spill through the windows. Neon rim-light on the left edge of every solid object. Characters always carry a bounce light from below so they never sink into the floor.
9. Arena — The Beach Kiosk¶
9.1 Mood¶
Blazing midday. Bleached wood, faded parasols, sand everywhere. Everything is slightly sun-damaged. The contrast to the Diner is total: that arena is enclosed and warm-dark, this one is open and bright.
9.2 Palette Shift¶
Base palette biased cool and bright. Bottle Teal and Sunbeam dominate. Frenzy Red survives only on the cans, the kiosk signage, and the parasols — which keeps hazard-red rare and therefore loud.
9.3 Tier Dressing¶
Indestructible - The kiosk itself — shuttered hatch, hand-painted Frenzy Cola sign - Concrete groynes and sea wall segments - Chained-up pedal boats - Full steel coolers
Destructible - Sun-bleached cardboard parcels sealed with Bottle Teal tape, a rope carry handle on top; one shape for all four families, pictograms ice cream, shell, sunglasses, beach ball. (Changed 2026-09-25, as for the Diner §8.3: the v0.5 crates, sandcastles, parasols and cooler boxes read as scenery, not blocks.)
Reinforced (two-hit) — deferred to v2 - Ice buckets — crack, then burst into slush. Ships in v1 as a single-hit Destructible
Hazards & Props
- Bendy straw teleporters (v2) — reskinned as buried pipe outflows
- Wet sand patches — the syrup equivalent, slows movement
- Incoming tide — the sudden-death ring, reskinned: the arena floods inward, shrinking it and forcing endgame resolution (Gameplay Rules §12)
- Seagulls — ambient dressing only, off the play field. They never touch a tile or a pickup (Gameplay Rules §12.2)
- Cola outflow pipe — the Beach's H tile: a buried pipe mouth that vents the same low steam cloud as the Diner's fountain (§8.3)
9.4 Lighting¶
Hard overhead sun, short sharp shadows, high ambient bounce off the sand. Heat shimmer at the horizon only — never over the play field, where it would compromise readability.
10. VFX¶
10.1 The Foam Blast¶
The signature effect. Budget accordingly — this is what players see thousands of times.
- Frame 1: white flash, full opacity, one frame only
- Frames 2–4: foam surges outward along four lanes, leading edge cream, trailing rim red
- The foam fills its tiles. Arms are about 82% of the lane wide, the core fills the origin tile (and carries the lanes under it, so the joins never show a cut edge), the arm ends are rounded caps, and the foam is warmed toward Sunbeam from the rim inward so a thick lane still reads warm at 32px (changed 2026-09-25: the v0.5 arms covered 43% of the lane and read thin)
- Frames 5–8: foam collapses, bubbles pop individually, residue fades
- Total duration 0.4s; damage window is the first 0.2s and is telegraphed entirely by the fuse ring, never by the VFX
- Foam never extends visually beyond damaged tiles. Not by a single pixel. Every complaint about unfair deaths in this genre traces back to VFX overhang.
10.2 Rules¶
- Particles are shapes, not smoke — bubbles, droplets, cardboard scraps, sugar sparkles
- Nothing persists longer than 2 seconds except designed floor state (syrup, puddles, wet sand)
- Screen shake: maximum 4px, 0.1s, and it scales down with the number of simultaneous blasts so chain reactions don't make the screen unplayable
- No full-screen flashes, no bloom over the play field, no motion blur
11. UI & HUD¶
Thumb zones are sacred. The bottom band belongs to the controls. No information, no hazard, no critical geometry ever appears there.
The screen is portrait, 11 columns × 13 rows, and the vertical budget is owned by Framework & Flow §5.8.1. In summary, top to bottom:
- Top row: round timer, round pips (point totals in single player) and the menu button. No player roster: players are recognised by their Chugger and the colour-and-shape marker under its feet
- Play field: the outer wall cropped to a quarter tile, inside a Cola Brown keyline, never stretched. Behind it the plain Home slate, no arena backdrop
- No pickup strip. Power-ups show their effect in play; the only count is cans ready, as a badge on DROP
- Control zone: joystick origin area left, DROP right, the timed-effect chip between them. It takes all the spare height of tall phones — the kick is automatic, triggered by walking into a can, so DROP is the only button in the cluster. It never overlaps the play field — no transparent joystick floating over live tiles
- Nothing is ever drawn under the notch or on the home indicator
League surfaces are menu-only. The tier chip sits in the Home upper band beside the player badge (Framework & Flow §5.2), and the standing has its own screen. No rank, badge or band ever appears in the match HUD — §3.4.
No pause button in multiplayer. A menu affordance opens a slim overlay while the simulation continues underneath; pause exists in single player only. Framework & Flow §8.4.
Style: chunky rounded rectangles, thick Cola Brown outlines, flat fills, minimal gradient. UI is a layer above the world and never pretends to be part of it.
Diegetic exception: menus are dressed as a diner countertop or a kiosk hatch. This is the one place the world and the interface are allowed to touch.
12. Animation¶
- 12fps stepped for character cycles — reads as deliberate and hand-crafted, and saves budget
- Smooth interpolation for anything gameplay-critical: can fuse, blast, power-up spawn
- Anticipation on every action, overshoot on every stop
- Idle animation is mandatory. A still Chugger looks like a broken game.
- Placing a can: a fast wind-up with the shake in it, a throw, and a recovery hop. The whole cycle must complete well inside the 2.0s fuse and must not obscure the tile the can lands on. It is animation, not input — the player taps once.
13. Audio Direction¶
Included here because it carries the same information as the art and must not contradict it.
- Fuse: a rising hiss, pitch climbing with the fuse ring. Audible above music at all times.
- Burst: a wet thwack plus a fizz tail. Never an explosion. Never a bang.
- Popped Chugger: a raspberry and a deflating balloon.
- Music: surf rock. Works for both arenas — reverb-heavy and diner-jukebox for the first, bright and open for the second.
- Design for silence. Most mobile sessions are muted. Every audio cue must have a visual equivalent that is sufficient on its own.
14. Seasonal & Skin Framework¶
Both arenas were selected because they reskin cheaply, and the framework must be respected to keep that true.
| Season | Diner | Beach Kiosk |
|---|---|---|
| Winter | Christmas lights, cocoa cans | Ice-swimming, snow on sand |
| Spring | Pastel vinyl, blossom outside | Kites, pastel parasols |
| Summer | Fans, condensation | Base state |
| Autumn | Pumpkin pie, orange neon | Storm, grey sea, packed-up stalls |
Non-negotiable across every skin: hazard colour law (§3.3), the three-tier silhouette system (§7), player identity colours and shape badges (§3.2), and the can's three states (§6). A skin changes surfaces. It never changes what something means.
15. Store Assets¶
- Icon: the can alone, three-quarter view, mid-burst, on flat Frenzy Red. Not the logo, not a character, not a scene. It must read at 48px.
- Screenshot 1: four Chuggers, one blast mid-frame, maximum visible chaos
- Screenshot 2: the Diner, wide, showing arena identity
- Screenshot 3: power-ups with names visible — this is where depth gets communicated
- Screenshot 4: the Beach Kiosk with the tide closing in
- Video: first 3 seconds must contain a drop, a fuse ring draining, and a burst. No logo intro. The shake is part of the drop animation, not a separate beat.
16. Legal Guardrails for Artists¶
Non-negotiable, and cheaper to respect now than to fix at submission.
- Frenzy Cola's can art must never use red-and-white Spencerian script, the dynamic ribbon device, or any silver-on-red wave. That is Coca-Cola trade dress.
- No real brand may appear on any prop, poster, wrapper or signage, however small or blurred. Every label in the world is a fictional brand we own.
- The mint-geyser hazard, if implemented, is a generic roll of mints. It is never called Mentos, never styled as Mentos, and is never referenced in marketing.
- No real-world celebrity likeness, no licensed music, no recognisable album art on the jukebox.
- Diner and beach photo reference is for mood only. Nothing is traced. Keep a reference log with sources and licences.
Open Questions¶
- Do Chuggers rescue each other, or is elimination final? A two-stage elimination — trapped in foam, freed by a teammate — would change team modes substantially and is worth prototyping before the character rig is locked.
Part 4 — Visual Assets & Design Architecture¶
Companion to the Design Style Guide, the Application Framework & Flow, the Gameplay Rules & Mechanics and the Economy & Progression. Where the style guide says what things look like, this document says how they are cut up, named, bundled and shipped.
1. The Spine: Five Variant Axes¶
Everything in the game belongs to exactly one of five axes. An asset's axis determines where it lives, who can change it, and what it is allowed to affect.
| Axis | Varies by | Ships as | May change | May never change |
|---|---|---|---|---|
| Core | Nothing | Base app | — | — |
| Arena | Arena pack | Downloadable pack | Surfaces, props, backdrop, lighting | Grid dimensions (11×13 portrait), tier silhouettes, hazard colour law |
| Character | Chugger skin | Per-skin bundle | Colours, textures, accessories inside the hat box | Silhouette class, proportions, tile footprint, animation set |
| Blast | Blast skin | Per-skin bundle | Core fill, particles, residue decal | Blast mask, timing, warm rim, arm length |
| Can flavour | Flavour skin | Per-flavour bundle | Label art, body tint | Silhouette, fuse pulse hue, timing, the three can states |
Can flavour is reserved, not built. Only classic fills the slot at launch. The manifest, the label layer separation and the naming convention exist from day one so the axis can be switched on later without touching a core asset.
The rule that makes this work: a variant never introduces a new asset. It fills a fixed slot manifest. If an arena pack ships without solid_block_c, the pack fails validation and does not build. Variants are interchangeable because they are structurally identical.
Power-ups are Core. This is a deliberate decision and it earns three things: a player learns the three pickups once and knows them in every arena forever; the store can never sell an advantage or a confusion; and the pickups can be authored once at high polish instead of once per pack. The cost is that every arena pack must prove its floor is readable behind them (§8).
2. Directory Structure¶
Two trees, split by one rule: anything belonging to a variant axis is engine-agnostic source and lives outside the engine project; Core ships in the binary and lives inside it.
2.1 Pack sources — Content/¶
The authoritative source form for the four variant axes. Never imported by the engine. The source contract is Pack Studio §8.2 and it is locked: loose PNGs, one file per slot, import settings as manifest data, no engine sidecar files, atlasing at build.
There is no per-axis manifest schema file. The required slot lists are §5.1–§5.4, and their single coded copy is SlotTable in FizzFrenzy.Maps — a schema file per axis would be a second statement of the same list, free to fall silently behind the first, which is the drift Pack Studio T1 exists to prevent.
Content/
├── arenas/
│ ├── diner/
│ │ ├── pack.json # slots, import settings, defaults
│ │ ├── art/
│ │ │ ├── @1x/ # loose PNGs, one per slot — see §4
│ │ │ ├── @2x/
│ │ │ └── @3x/
│ │ └── maps/ # .map text files, see Appendix A
│ └── beach_kiosk/ # identical subtree
│
├── characters/
│ ├── _rig/ # shared skeleton + attach points
│ ├── rax/
│ │ ├── base/ # the free launch skin
│ │ │ ├── pack.json
│ │ │ └── art/{@1x,@2x,@3x}/
│ │ └── skins/
│ │ └── soda_jerk/ # same shape as base/
│ ├── puff/
│ ├── nim/
│ └── bolt/
│
├── blast/
│ ├── classic/ # default, free, shipped in base app
│ └── skins/
│ ├── blue_raspberry/
│ ├── lime/
│ └── grape/
│
├── flavours/ # reserved axis — only classic at launch
│ ├── classic/ # Frenzy Cola, shipped in base app
│ └── skins/ # empty at launch
│
└── _source/ # working files, never shipped
├── vector/
├── rigs/
└── palettes/
There are no category subdirectories inside art/. The §5.1 slot names already carry the category — backdrop_far, floor_tile, solid_block, grade_profile, meta_card — so a floor/ directory would encode the same fact twice and give a filename two places to disagree with its slot. One flat directory per resolution, filename derived deterministically from the slot name — the exact rule is §3.1, and it is what lets a tool resolve a glyph to a file without a lookup table.
2.2 Core — inside the engine project¶
Assets/Game/Art/Core/
├── can/ # can geometry, states, fuse pulse — label layer excluded
├── powerups/ # Shaker, Ice Cube, Extra Can
├── status/ # player state overlays (frost, kick-ready, speed)
├── blast_mask/ # blast geometry + timing, skin-independent
├── fx_generic/ # dust, sparkle, pickup pop, spawn burst
├── hud/ # in-match interface
├── leagues/ # tier badges, bands, movement stingers — §6.6
├── economy/ # currency, XP, level — *Economy & Progression* §10
├── ui/ # menus, store, lobby, results
├── type/ # fonts, logo, wordmark
└── store_meta/ # app icon, screenshots, feature graphics
Why Core is not in Content/. Core ships in the binary, is not pack-swappable, fills no slot manifest and is never read by Pack Studio. None of the §8.2 source constraints apply to it, so it lives where the engine imports it normally and carries its own import settings.
Why the root is Content/ and not assets/. The engine project's own directory is Assets/, and Windows and macOS filesystems are case-insensitive by default. assets/ and Assets/ cannot coexist in one repository.
Maps live inside the pack directory. The map CI gate and the pack build gate therefore touch the same tree, which is intended rather than incidental — a map is meaningless without the pack whose slots its glyphs name, and separating them would let a map reference a slot its pack no longer has.
Bundling: Core is in the binary. Each arena, character skin and blast skin is an independently downloadable bundle with its own version stamp, produced from its source directory by the build step (Pack Studio §8.2). A match downloads only the arena in play plus the skins the four participants actually own.
3. Naming Convention¶
Two conventions, and the file's location decides which applies. Inside a pack's art/ directory a file is addressed by slot; everywhere else it is addressed by description.
3.1 Pack art — the filename is the slot¶
For every file in an art/@1x/, art/@2x/ or art/@3x/ directory under Content/:
{slot}[_{variant}][_{direction}][_{frame}].png
| Field | Rule |
|---|---|
{slot} |
A slot name from the §5 manifests, verbatim |
{variant} |
Two digits. Present only where the slot's count is greater than 1 |
{direction} |
n, s or e. Present only where the slot is directional |
{frame} |
Two digits. Present only where the slot is animated |
Lowercase, underscores only. No axis, set, pack or skin prefix — the directory path already carries all four.
File in Content/arenas/diner/art/@1x/ |
Slot |
|---|---|
backdrop_far.png |
backdrop_far, count 1 — no index |
floor_tile_01.png |
floor_tile[1] of 3 |
solid_block_00.png |
solid_block[0] of 3 — what the % glyph pins to |
solid_cap_03.png |
solid_cap[3] of 4 |
destructible_break_02_04.png |
destructible_break, family C, frame 4 |
File in Content/characters/rax/base/art/@1x/ |
Slot |
|---|---|
portrait.png |
portrait, count 1 |
run_e_04.png |
run, facing east, frame 4 |
hit_pop_07.png |
hit_pop, frame 7 — not directional |
Why there is no prefix. §2.1 forbids category subdirectories inside art/ because a directory "would encode the same fact twice and give a filename two places to disagree with its slot". A pack prefix inside that pack's own directory is the identical duplication one level up: Content/arenas/diner/art/@1x/arena_diner_solid_counter_a.png names the Diner twice and describes the slot in a word — counter — that no tool can derive from solid_block[0].
The consequence that makes this load-bearing: because the name is derivable, every tool resolves a glyph to a file by construction, with no lookup table. pack.json therefore carries import settings and defaults (Pack Studio T7) and no slot→file map. A pack cannot have a file whose name disagrees with its slot, because the name is the slot.
3.2 Core, and working files — the descriptive convention¶
Core lives inside the engine project (§2.2), fills no slot manifest, and its tree is organised by function rather than by variant axis. It needs the descriptive form — and so do _source/ working files and the pre-final output of the Asset Generation Brief, which is explicitly renamed to real slot names by the export step.
{axis}_{set}_{object}_{variant}_{state}_{direction}_{frame}
Lowercase, underscores only, no spaces, no capitals, no hyphens. Frame numbers are always two digits.
| Example | Reads as |
|---|---|
core_pu_shaker_idle_03 |
Power-up Shaker, idle loop, frame 3 |
core_can_armed_pulse_06 |
Can in armed state, pulse loop, frame 6 |
core_hud_fuse_ring_08 |
HUD fuse ring, frame 8 |
flav_classic_label_armed_06 |
Classic flavour label, armed state, frame 6 |
Direction codes: n s e w. West is mirrored from east at runtime and is never authored — in both conventions.
4. Canvas & Export Standards¶
- Grid tile reference: 192 × 192 px. All world assets are authored against this.
- Author in vector. Export raster at @1x / @2x / @3x; the tile is 64 / 128 / 192 device px.
- On disk the three exports are sibling directories —
art/@1x/,art/@2x/,art/@3x/— carrying the same filename in each. The scale is never a filename suffix. This keeps Pack Studio §8.2's one-file-per-slot rule true at all three resolutions and keeps the §3.1 slot-derived name free of a field it does not define. - Core is exempt and authors one set at @3x. Core lives inside the engine project (§2.2), where per-platform texture import overrides derive the lower tiers from a single source. Three hand-maintained copies of the same asset would be a maintenance cost with no benefit. Packs cannot use that mechanism, because their source form has to stay engine-agnostic.
- Atlas size 2048², one atlas per bundle, power-of-two, trimmed, 4px padding. Core gets two:
core_worldandcore_ui. - Pivots: world objects pivot at bottom-centre of their tile footprint. Characters pivot at foot centre. Blast segments pivot at tile centre.
- Character sprites occupy 85% of tile width, per §5.2 of the style guide. The remaining margin is gameplay-critical and no skin may spend it.
- Everything carries a 6px Cola Brown outline at reference scale, baked in, not shader-applied.
- Overhang budget: a prop may exceed its tile by up to 40% vertically upward only. Never sideways, never downward.
- PNG-32 for source, platform compression at build. No JPEG anywhere in the world layer.
5. Slot Manifests¶
5.1 Arena Pack — required slots¶
Every arena pack ships exactly this. Missing slot = build failure.
| Slot | Count | Notes |
|---|---|---|
backdrop_far |
1 | Static, no parallax. Not drawn behind the match since the lean HUD; kept for the tablet/landscape variant and menus |
backdrop_near |
1 | Parallax layer, max 8px travel. Same status as backdrop_far |
floor_tile |
3 | Randomised across the grid to break repetition |
floor_edge |
1 | 9-slice, frames the play field in menus and previews; not drawn in the match since the lean HUD (Framework & Flow §5.8.1). The insets live in SlotTable, not in pack.json — they are per-slot geometry like pivot, and a pack able to choose its own insets for floor_edge would break the format's claim |
floor_decal |
4 | Painted-on dressing, never occludes. Placed by the map's decal grid (§A.3), glyphs a–d |
solid_block |
3 | Indestructible tier — dark, cold, heavy |
solid_cap |
4 | Edge and corner caps for block runs |
destructible |
4 families | Light, warm, cardboard-y tier |
destructible_cracked |
4 | One damaged state per family |
destructible_break |
4 × 6 frames | Destruction animation per family |
debris_sheet |
1–4 | Particle sheet per family. A family without its own sheet uses [0]; the parcel packs ship only [0] (cardboard, tape) |
ambient_static |
6 | Backdrop or floor only. Never on a playable tile, so no map glyph places it |
ambient_animated |
2 × 8 frames | Index 0 is the hazard tile — the H glyph, walkable floor that periodically vents steam (§A.5; Design Style Guide §8.3). Index 1 is backdrop scenery — jukebox glow, gull, ceiling fan — pack-placed, never on a playable tile. Eight frames, matching every other animated slot in this document — idle, run, burst_core, hit_pop and cube_burst are all 8 — looping in under 3s |
grade_profile |
1 | Lighting/LUT. Constrained by §8 contrast gate. |
maps |
3 min bundled | .map text files, Appendix A. This is the bundled baseline; the server adds more, never replaces (§10.2). Three is a floor, not a target — with no procedural fill, every round of every match on a given map is the identical authored board, so map count is replayability. Five or six per pack is a better aim |
meta_card |
1 | Store thumbnail, 512² |
meta_loading |
1 | Full-bleed loading art |
| Total per pack | 78 files per scale + 3 maps | Diner and Beach Kiosk at launch. 78 is files, not slots — ambient_animated alone is 16 |
Maps are data, not art, and they are the cheapest content in the entire project. A new map costs one person an hour in a text editor and reuses every asset in the pack. If maps are delivered server-side rather than baked into the bundle, the rotation can change weekly without a store submission — which is the single highest-leverage decision in this document for retention per unit of production cost.
The three-tier read — solid / destructible / hazard — is the pack's contract with the player. A pack may dress those tiers however it likes and may not blur the line between them.
There is deliberately no Reinforced slot. The style guide §7 describes a two-hit tier; it is not built in version 1 and has no manifest slot, no glyph and no damage-state animation. Anything a pack wants to dress as reinforced ships as an ordinary destructible. See Framework & Flow §16.2.
5.2 Character Skin — required slots¶
Per skin, including each character's base skin.
| Slot | Frames | Directions | Sprites |
|---|---|---|---|
idle |
8 | 3 | 24 |
run |
8 | 3 | 24 |
place_can |
6 | 3 | 18 |
kick |
4 | 3 | 12 |
hit_pop |
8 | 1 | 8 |
victory |
12 | 1 | 12 |
defeat |
8 | 1 | 8 |
portrait |
1 | — | 1 |
emote_face |
4 | — | 4 |
store_card |
1 | — | 1 |
| Total per skin | ~112 sprites |
Two contracts skins must honour:
- The hat box. A skin may add geometry only inside a defined region above the head, up to 30% of body height. Everything else is recolour and retexture. This is what keeps a purchased skin from becoming a harder target to read.
- The kick animation is mandatory. Kick is unlocked by the Ice Cube, so every skin ever sold must ship it — including skins authored before anyone remembers why it's there. Manifest validation enforces this; do not rely on memory.
5.3 Blast Skin — required slots¶
| Slot | Frames | Notes |
|---|---|---|
burst_core |
8 | Origin tile |
arm_h_mid |
8 | Horizontal mid-segment, tiles |
arm_v_mid |
8 | Vertical mid-segment, tiles |
arm_cap |
8 × 3 | Tip, one per authored direction — n, s, e. West is mirrored from east at runtime and is never authored (§3.2), so four are rendered and three are drawn. Total: 8+8+8+24+4+1+1 = 54 |
residue_decal |
4 | Fading puddle left behind |
droplet_sheet |
1 | Particle atlas |
store_card |
1 | 512² |
| Total per skin | ~54 assets |
Blast skins are the one place the architecture pushes back on the design. Style guide §3.3 says the blast is Fizz Cream core with a Frenzy Red rim, and that a skin never changes what something means. Selling recoloured blasts collides with that, and worse, collides with player identity colours — a purchased blue blast on a grid containing the blue player is a readability failure the buyer paid for.
The resolution, and it should be treated as a hard constraint on the store:
- The mask is core.
core/blast_mask/owns the shape, extent and timing. Skins supply fill only. The lethal area is byte-identical across every skin. - The rim stays warm. Hue locked to the 10°–45° band. This is the pixel that says "this will kill you," and it is not for sale.
- The core fill is free. Interior colour, particles, residue decal, droplet shape — all sellable, all expressive, none of it load-bearing.
- Collision fallback. If two players in a match own blast skins within 20° hue of each other, the lower-seed player renders
classic. Silent, server-resolved, no message.
That gives the store a real product and keeps the colour law intact.
Locked: blast skins are cosmetic only. No blast skin changes range, timing, damage or travel. Store copy must not imply otherwise — no "explosive," no "supercharged," no comparative adjectives on a blast SKU. Flavour and mood words only. This is a store-copy review rule, not just an art rule, and it should live in the localisation brief so it survives translation.
5.4 Can Flavour — required slots¶
Reserved for post-launch. Only classic is authored at launch, but the slots exist now so the axis costs nothing to switch on later.
| Slot | Frames | Notes |
|---|---|---|
label_idle |
4 | Label decal only, transparent outside the label area |
label_armed |
12 | Registers to the core pulse frames — does not replace them |
label_slide |
6 | |
body_tint |
1 | Single-channel mask, tints the can body beneath the label |
store_card |
1 | 512² |
| Total per flavour | ~24 assets |
Three constraints on the axis:
- The label is a decal, not a repaint. Core owns the can silhouette, the fuse pulse and all timing. A flavour supplies artwork that rides on top, registered to the core frames.
- The fuse pulse hue is not for sale. Red → Sunbeam is the single most important tell in the game. A flavour may tint the body; it may never touch the pulse.
- Merchandise flavours with blast skins, but keep them technically independent. A grape can bursting into lime foam is a bad look, so sell them as a matched SKU. Do not couple them in code — a player who owns both should still be able to mix them if they want, and coupling the systems means every future flavour drags a blast skin's production cost with it.
6. Core Assets — Non-Variant¶
6.1 The Can¶
| Asset | Frames | Notes |
|---|---|---|
can_idle |
4 | Unplaced, held state reference |
can_armed_pulse |
12 | Red → Sunbeam, accelerating with the fuse. Fixed 2.0s on every can — the pulse curve is authored once and never parameterised |
can_slide |
6 | Kicked, travelling |
can_bounce |
4 | Impact against a block |
can_settle |
4 | Slide resolving to rest |
Flavour axis reserved — see §5.4. Every can state above is authored with the label on its own layer, and flavours/classic/ fills the slot at launch. The five states, the silhouette and the pulse timing belong to core permanently; a flavour only ever supplies decal artwork registered to these frames.
6.2 Pickups¶
Six effects, delivered two ways. All non-variant, all core, authored once, identical in every arena and every match.
Behaviour lives in Gameplay Rules & Mechanics §8. Effects, caps, stacking rules, drop weights and timed-effect interactions are specified there and that document governs them. What follows here is silhouette, motion, frame count and legibility — the production side. Effect columns are retained for context only; where the two disagree, Gameplay Rules wins.
The delivery split is the important part. Buffs sit on the field as themselves and are identifiable before you commit. Debuffs only ever arrive inside a Surprise Cube — an opaque ? container whose contents are unknown until collected. So the player never fails to see a debuff coming; they see a gamble coming, and choose whether to take it.
It is a sound fairness contract. A visible debuff is fair because you can path around the tile. A Surprise Cube is fair because the risk is disclosed even though the outcome isn't — the same contract as a Mario Kart item box. What it must never become is a debuff that arrives unannounced from a container that looked safe. The ? is doing that work, and it is the single most important glyph in the pickup set.
Shared spec:
- 70% of tile width — deliberately smaller than a can, so a pickup never reads as a threat
- 4px Fizz Cream outline so they survive any arena floor
- Slow bob, 1.2s loop, plus a per-item signature motion
- Shared spawn_burst (6 frames) from fx_generic
Class A — Permanent upgrades¶
Persist until death. Stack to a hard cap. Not counted in the HUD (Gameplay Rules §8.2). Drop visibly from destructibles.
| Power-up | Effect | Cap | Silhouette | Signature motion | Frames |
|---|---|---|---|---|---|
| Shaker | +1 soda blast range | 6 | Steel bar shaker, tilted ~25° mid-shake: tapered cup, cone top, red cap, red band, fizz spray | Rattles side to side, 3-frame jitter every 2s | 12 |
| Ice Cube | Kick at stack 1; stacks 2–3 increase can slide speed | 3 | Rounded cube, 3/4 view, single white highlight wedge | Slow rotate + occasional sparkle | 16 |
| Extra Can | +1 simultaneous can | 5 | Two-can sleeve — a mini six-pack, not a single can | Gentle tilt, cans clink on the beat | 12 |
Class B — Timed effects¶
Do not stack. Displayed as a depleting band, not a counter. Only ever obtained from a Surprise Cube — these three never appear on a tile as themselves.
| Effect | Result | Duration | Type | Reveal icon | Frames |
|---|---|---|---|---|---|
| Coffee Bean | Player move speed up | 5s | Buff | Small, dark, hard-edged bean with a centre crease | 12 |
| Honeycomb Wax | Player move speed down | 5s | Debuff | Pale amber hex-cell slab, soft edges, sagging | 14 |
| Glue | Cannot place cans | 3s | Debuff | Spilled white blob with a tipped-over bottle half-sunk in it | 14 |
Because these never sit on a tile, they need no bob loop and no field silhouette — only a reveal icon, a status overlay and a HUD band icon. It also rules out any Coffee Bean / Honeycomb Wax tile collision, since the two are never on the grid at the same scale.
The Surprise Cube¶
| Asset | Frames | Notes |
|---|---|---|
pu_cube_idle |
16 | Slow rotate, ? glyph faces the camera on every frame |
pu_cube_open |
8 | Shell splits, contents not yet legible |
pu_cube_reveal_good |
10 | Sunbeam burst, rising, sharp |
pu_cube_reveal_bad |
10 | Drab burst, sagging, viscous |
reveal_icon |
6 | One per effect, shown at the burst centre and above the player |
The rules the cube has to follow:
- The cube is one silhouette, always. It never telegraphs its contents by shape, colour, size or motion. Any variation is a tell, and a tell that only regular players notice is worse than no tell at all.
- The
?must survive 32px and greyscale. It is the entire disclosure. Heavy weight, Cola Brown, high-contrast plate behind it, never a thin outline. - The cube is visually distinct from the Ice Cube. Both are cubes. The Ice Cube is translucent, cool, single-highlight, smooth. The Surprise Cube is opaque, mid-tone, panel-lined, with the
?plate on every visible face. Test these two together at 32px before either is final — this is the collision that matters most in the pickup set. - The reveal reads before the effect lands. Open, burst, and icon all play in under 250ms, and the good/bad burst variants differ in the same three channels the field pickups use: rising vs sagging, saturated vs drab, sharp vs viscous. A player must know what happened without reading the icon.
- The reveal is public. It plays at world scale above the collecting player, not just in their own HUD. Everyone in the match sees who just got glued.
Contents pool. All six effects at equal weight, per the flat-probability decision — so a cube is 1/3 debuff, 2/3 buff, and worth opening. Including the Class A permanents in the pool is what makes that true; a cube containing only the three timed effects would be 2/3 debuff and no rational player would ever touch one.
Spawning¶
One source, one position. Every pickup and every cube spawns from a destroyed block, at the tile the block occupied. Nothing seeds at round start, nothing spawns on an empty tile, and the two entry types share a single drop table.
| Drop table entry | Notes |
|---|---|
| Nothing | The majority case — tune to taste |
| Shaker | Visible, identifiable before you commit |
| Ice Cube | Visible |
| Extra Can | Visible |
| Surprise Cube | Opaque, six possible effects at flat weight |
This makes the arena the pacing mechanism. Early rounds are dense with blocks and poor in pickups; the mid-game opens up and floods the board; the endgame has nothing left to break. That's a good curve and it comes free — but it means the drop rate has to be tuned against arena block count, not set globally. A dense Diner and an open Beach Kiosk with the same drop percentage will play at very different power levels.
Three spawn rules the shared position forces:
- Debris first, pickup second.
destructible_breakruns its 6 frames, thenspawn_burstfires. A pickup that materialises inside its own debris cloud is invisible for exactly the moment the player is looking at that tile. Sequence them; do not overlap. - Debris must not occlude a settled pickup. Draw order puts debris particles at layer 10, above pickups at layer 4 — correct while the block is breaking, wrong a half-second later. Debris lifetime is capped at the length of
destructible_breakplus 200ms, and no debris particle may come to rest on a tile. - Chain reactions spawn in a live blast. A four-block chain can drop four pickups inside foam that's still on screen, at the exact moment the player is reading the lethal layer. The
spawn_bursttherefore waits for the blast on that tile to clear before it plays. A pickup appearing under active foam is a pickup nobody sees.
Locked — blasts destroy uncollected pickups (Gameplay Rules §6.5). This adds one required asset:
| Asset | Frames | Notes |
|---|---|---|
fx_generic/pickup_destroyed |
8 | Foam swallowing the item — sagging, drab, downward. Must not resemble pickup_pop, which is rising, saturated and Sunbeam-cored. A player who sees the collect animation and gets no stat change reports it as a bug |
The two animations are the clearest possible test of the rising-versus-sagging channel already established for buffs and debuffs: destruction is the same motion vocabulary as a debuff reveal.
The pickup shape law¶
Three states, distinguishable in greyscale at 32px, no exceptions:
- Ringed and rising = take it. Clean circular Fizz Cream halo, Sunbeam core, bobs upward. The three Class A pickups.
- Panelled with a
?= gamble. Opaque cube, mid-tone, no halo, steady rotation. One asset, one meaning. - Nothing else on a tile is collectable. Live cans are always single, always vertical, never ringed and never panelled.
Two silhouette warnings, both worth solving in art rather than in a tutorial:
- Extra Can must not be a can. The most dangerous object in the game is a can on a tile. A pickup shaped like one is the wrong shape at the worst moment. The two-can sleeve solves it.
- Shaker must not be a can either. A canister and a can are the same family. Force them apart on silhouette: the can stands upright, the Shaker is a tapered bar shaker tilted about 25° with a cone top and cap. At 32px that survives; a texture difference does not. (Changed 2026-09-25: the v0.5 wide canister kept the can apart but players did not read it as a shaker; a bartender's shaker names the pickup at a glance.)
The name Shaker is unambiguous. There is no charge, no shake input and no meter anywhere in the HUD, so nothing else in the vocabulary competes with it. The can_armed_pulse frames run on a fixed clock.
Cap behaviour. At cap, a Class A pickup is still collectable — an uncollectable pickup squatting on a tile is worse than a wasted one. It plays the ordinary fx_generic/pickup_pop; there is no capped variant. A cube that rolls a capped Class A effect plays that same ordinary pop after the reveal. Gameplay Rules §8.2 carries the reasoning.
Timed-effect rules.
- Refresh, never extend. A second Coffee Bean resets the clock to 5s; it never accumulates to 10. This is what stops a debuff chain from locking a player out of the match.
- One movement modifier at a time, last one wins. Coffee Bean clears Honeycomb Wax and vice versa. That gives the bean a rescue role and makes cubes worth opening even while slowed.
- Glue is orthogonal and coexists with either movement state. Glued-and-fast is a real and useful combination; glued-and-slow is the worst place in the game to be.
- Glue is 3s. Losing placement for five seconds while boxed in is close to unavoidable death, and now that it arrives from a blind roll the player can't have positioned around it. Three seconds still denies an escape and still swings a duel.
6.3 Status Overlays¶
These render above the character skin and below the blast layer, and are non-variant precisely because a purchased skin must never be able to hide a gameplay state.
| Overlay | Trigger | Asset |
|---|---|---|
status_kick_ready |
Ice Cube stack 1 | Frost glint at feet, 8 frames |
status_frost |
Ice Cube stack 2 and 3 | Two intensity steps — visual states map 1:1 to the cap, no invisible stacks |
status_speedlines |
Can sliding at stack 2+ | Trailing streak on the can, 6 frames, 2 intensities |
status_armed |
Holding a can | Held-can attachment, from core/can/ |
status_speed |
Coffee Bean active | Warm streaks off the player, jittery bounce, 8 frames |
status_slow |
Honeycomb Wax active | Amber drips off the body, sagging posture, 8 frames |
status_glued |
Glue active, 3s | White strings from the hands plus a crossed-out can symbol above the head, 8 frames |
status_reveal |
Any cube collected | Effect icon rises above the head, holds 1s, fades. Public — all players see it. 10 frames |
Two overlay rules the timed effects force:
- A negative state needs a positive symbol. "You cannot place cans" is an absence, and absences are invisible. The crossed-out can above the head is what makes Glue legible — the string VFX alone reads as decoration. Same principle applies to any future debuff.
- Two speeds, two visual languages. Ice Cube speeds up the can; Coffee Bean speeds up the player.
status_speedlinesattaches to the can and stays cool;status_speedattaches to the character and stays warm. They must never share a motif, or the tutorial has to explain a difference the art should be making obvious. status_revealis where the game is taught. With debuffs hidden in cubes, the reveal above the head is the only place a player ever sees what Honeycomb Wax or Glue actually is. It carries the whole onboarding load for three of the six effects, so it gets the highest polish budget incore/status/and it plays at full world scale, never HUD scale.
6.4 HUD & UI¶
| Group | Assets | Notes |
|---|---|---|
hud/avatar_frame |
— | Cut with the lean HUD (no roster). Menus use the CSS .avatar |
hud/shape_badge |
4 | Circle, square, triangle, star — colourblind identity |
hud/lives |
3 states | Vestigial — there is no lives system (Gameplay Rules §7). Cut, or repurpose as round pips |
hud/timer |
1 + tabular numerals | |
hud/powerup_pip |
— | Cut with the lean HUD (no Class A counters). The pickup icons remain for How to Play |
hud/timed_band |
1 + fill | Depleting ring, 5s or 3s, in the control zone |
hud/timed_icon |
3 | Coffee Bean, Honeycomb Wax, Glue at band scale |
hud/opponent_state |
— | Cut with the lean HUD. A glued opponent shows the status overlay on their Chugger, which is where the eye already is |
hud/player_marker |
4 | Colour + shape under each Chugger's feet; drawn in code (worldkit.marker), no art |
hud/pause |
1 | |
hud/tile_telegraph |
4 frames | Pre-blast tile warning |
hud/countdown |
3 + "GO" | |
hud/banner |
2 | Victory, defeat |
ui/menu_chrome |
~20 | 9-slice panels, buttons, toggles |
ui/store |
~25 | Cards, currency, rarity frames, purchase flow |
ui/lobby |
~15 | Slots, ready states, character select |
ui/results |
~12 | Scoreboard, rewards, progression |
ui/leagues |
~14 | Leagues screen chrome: standing row, sticky self row, band dividers, countdown plate, empty and stale states. Badges themselves live in core/leagues/ — §6.6 |
ui/how_to_play |
~18 | Panel chrome, page dots, 7 effect icons at UI scale (6 effects + cube), 3 can-state icons, movement/placement diagrams |
type/ |
3 faces + logo lockups | Nunito ExtraBold, Baloo 2, custom display |
fx_generic/ |
~11 | Dust, sparkle, spawn burst, pickup pop, pickup destroyed |
6.5 How to Play — Icon Requirements¶
The How to Play screen documents all six effects and the cube. Three constraints, because a screen that contradicts the game is worse than no screen:
- Icons are redraws, not downscales. The field asset is authored at 192px with a 6px outline and read at 32px in motion. A UI icon sits still at ~64px on a plain panel. Downscaling produces mush; redraw at UI scale from the same source vector.
- The screen must match the field silhouette exactly. A player learns the shape here and looks for it on the grid. If the How to Play Shaker is a friendlier three-quarter view than the in-game one, the screen has taught the wrong thing.
- Debuff icons are the only place Honeycomb Wax and Glue appear at rest. Everywhere else they're a 250ms reveal burst. These two carry more explanatory weight than any other asset in
ui/, and they have to communicate slow and can't place through shape alone — the screen gets localised, and labels truncate.
Worth being honest about the limits of the screen. Most players skip How to Play, and the ones who read it do so once, before they have context for what "cannot place cans for 3 seconds" means in practice. It's necessary — it satisfies the store, it serves the minority who look, and it's the one place you're allowed to use words. But it does not replace status_reveal (§6.3) as the primary teaching surface. Budget accordingly: the screen gets clarity, the reveal gets polish.
6.6 Leagues¶
Required by Framework & Flow §17. All Core, non-variant — a tier badge is identical in every arena, under every skin, for every flavour. Nothing on this list is sellable, seasonal or pack-supplied, and no variant axis may override any of it.
The reason is the same reason power-ups are Core. A badge that meant Gold in the Diner and something else at the Beach Kiosk would make the one piece of status the player carries between matches unreadable. Rank is a property of the account, not of the arena it happens to be standing in.
6.6.1 Tier badges — required slots¶
| Slot | Count | Canvas | Notes |
|---|---|---|---|
league_badge |
6 | 256² | Tin, Bronze, Silver, Gold, Diamond, Elite. Full-detail, used on the Leagues header and Profile |
league_badge_chip |
6 | 64² | Redraws, not downscales, per the §6.5 rule. Home badge chip and standing rows |
league_badge_locked |
1 | 256² | Greyed treatment for the Profile history strip. One asset, tinted per tier — not six |
league_frame |
1 | 9-slice | Plate the badge sits in. Shared across all six |
| Subtotal | 14 assets |
The six must be distinguishable by silhouette alone. Six metals with the same shape and different fills is the obvious cheap approach and it fails the greyscale gate in §8 immediately — Silver and Chrome are the same value, and Tin and Silver are the same idea. Each tier gets a distinct outline: point count, edge treatment, or a rising element that grows with rank. Test all six side by side, desaturated, at 64px, before any is finalised. This is a release blocker, not a polish note.
Elite is allowed one animated frame set. league_badge_elite_shimmer, 12 frames, is the single exception on this list and exists because the top of a ladder should look like the top of a ladder. It never appears at chip scale, where the motion would be noise.
6.6.2 Standing bands and movement¶
| Slot | Count | Notes |
|---|---|---|
league_band_promote |
1 | 9-slice fill behind the promotion rows. Cool, rising treatment |
league_band_demote |
1 | 9-slice fill behind the demotion rows. Drab, sagging — never the hazard red |
league_band_divider |
2 | The cut lines themselves — the most important two pixels on the screen. One promote, one demote |
league_arrow |
3 | Up, down, hold. Shown against a player's row and in the Match Results beat |
league_stinger_promote |
10 frames | Rollover result animation |
league_stinger_demote |
8 frames | Deliberately shorter and quieter than promotion |
league_stinger_hold |
6 frames | |
| Subtotal | ~10 slots, 26 assets |
Colour law is Style Guide §3.4 and it is not negotiable from this document. The one constraint worth restating in production terms: the demotion band may not use any hue in the 10°–45° warm hazard band. Everywhere else in the game, warm means this will kill you. A leaderboard is read at rest, with nothing exploding, and teaching the player that warm sometimes just means disappointing is a cost paid back on the grid at the worst possible moment.
Movement direction reads without colour. The three league_arrow assets carry direction in shape — up, down, flat — because the bands they sit against are the only other cue and roughly one player in twelve will not separate them by hue. Same rule as the shape badges in §6.4, same reason.
6.6.3 What is deliberately absent¶
- No reward art. No chests, no claim buttons, no currency bursts. The rollover pays nothing in version 1 (Economy & Progression §3.2), and the Leagues screen is laid out to take a reward strip later; this manifest does not pre-author one. Currency and level art exists, but it is menu chrome, not league reward art: see
core/economy/(§2.2, Economy & Progression §10) - No per-tier frames, backgrounds or nameplates. One shared
league_frame, one shared locked treatment. Six of everything is how a 14-asset list becomes a 60-asset list for no legibility gain - No club identity art. Clubs are numbered shards with a one-week lifetime, not guilds. They get no crest, no name and no colour
Total for the axis: ~40 assets, all Core, all authored once. That is roughly two-thirds of a single arena pack for a system that appears on three screens and drives the retention loop.
6.7 Economy¶
Required by Economy & Progression §10. All Core, non-variant — the same rule as power-ups and league badges, for the same reason: currency, XP and level are account state, not arena dressing. Nothing on this list is sellable, seasonal or pack-supplied, and no variant axis may override any of it.
And nothing on this list ever renders during a match. Currency and level are menu concepts; the field is for danger (Style Guide §3.4, §11). These assets live on Home, Store, Match Results and Profile — the same screens the league chrome lives on, and no others.
6.7.1 Required slots¶
| Slot | Count | Canvas | Notes |
|---|---|---|---|
sugar_cube |
1 | 256² | The currency icon, hero scale — Store shelf headers, IAP bundle cards |
sugar_cube_chip |
1 | 64² | Redraw, not a downscale, per the §6.5 rule. Home counter, price tags, Match Results reward line |
cube_burst |
8 frames | 256² | Reward celebration on Match Results and level-up. World-scale feel at UI scale |
xp_bar |
1 frame + 1 fill | 9-slice + fill | Match Results and Profile. Two files: a 9-slice frame and a fill — one bar in two pieces. Insets live in SlotTable, as for floor_edge |
level_badge |
1 | 128² | Frame the level numeral sits in, Home chip and Profile. The numerals are the type/ system, not new assets — tabular figures per Style Guide §4 |
| Total | ~13 assets |
6.7.2 The third cube problem¶
The Sugar Cube is the game's third cube, and the only one that never shares a screen with the other two — so this is a store-UI legibility problem, not a field readability emergency. It is still a real one: a player who cannot tell the currency icon from the Surprise Cube icon in the Store has been taught that cubes are interchangeable.
| Cube | Reads as | Treatment |
|---|---|---|
| Ice Cube | Translucent, cool, smooth, single white highlight | Field pickup — §6.2 |
| Surprise Cube | Opaque, mid-tone, panel-lined, ? plate on every face |
Field pickup — §6.2 |
| Sugar Cube | Translucent-white crystal cluster, Sunbeam core | Menu currency — here |
Translucent-white crystal, Sunbeam core, no ? plate, no frost highlight. Verify all three side by side at 32px in greyscale before locking — the §8 gate carries this check.
Sunbeam is by law, not by taste. The palette assigns Sunbeam to power-ups, rewards, currency (Style Guide §3.1), so the currency icon is Sunbeam-cored permanently. The collision that law creates is with the Gold league badge, which is deliberately darker and less saturated than Sunbeam (Style Guide §3.4, rule 1) precisely because the two sit a thumb's width apart on Home. The badge moves; the icon does not. Check them adjacent, at 32px, in colour, before either is final.
6.7.3 The burst vocabulary¶
cube_burst is the economy's only celebration, and it speaks the same motion language as pickup_pop and pu_cube_reveal_good: rising, saturated, sharp. It must never borrow the sagging, drab, viscous vocabulary of pickup_destroyed and the debuff reveals — a reward that reads as a penalty is worse than no reward. Endpoints are authored; the 8 frames derive.
6.7.4 What is deliberately absent¶
- No league reward art. Chests, claim buttons, payout cards — the rollover pays nothing in v1 (Economy & Progression §3.2), and §6.6.3 stands exactly as written
- No per-bundle pile art. The five IAP bundles are priced cards in
ui/storechrome carrying the onesugar_cubeicon. Five denominations of near-identical cube piles is how a 13-asset list becomes a 30-asset list for no legibility gain - No loot-box or gacha art of any kind. There are no randomized paid items (Economy & Progression §1.3), so there is nothing to illustrate
- No level numeral set, no level-up stinger frames beyond
cube_burst. Numerals come fromtype/; the level-up moment is the burst plus the badge it already has
7. Draw Order¶
Fixed, and no variant may insert a layer.
0 backdrop_far arena
1 backdrop_near arena
2 floor + floor_decal arena
3 blast_residue blast skin
4 power-ups core
5 destructible / solid arena
6 cans core (+ flavour label decal, same layer)
7 characters character skin
8 status overlays core
9 blast VFX blast skin
10 debris particles arena
11 HUD core
Power-ups sit below tiles so a pickup can never occlude a block edge, and above floor decals so no arena dressing can swallow one. Status overlays sit above characters so no skin can hide them. Blast sits above everything in the world so the lethal layer is never ambiguous.
The one place this order is a liability: debris at layer 10 sits above pickups at layer 4, and pickups now spawn exactly where debris is thickest. The layering is right during the break and wrong immediately after, so it's resolved in time rather than in order — capped debris lifetime and a sequenced spawn_burst, per §6.2. Do not fix this by moving debris below pickups; debris needs to be above the blast to sell the destruction.
8. Compatibility Gates¶
Automated, run on every bundle before it can ship.
| Gate | Test | Threshold |
|---|---|---|
| Manifest completeness | Every required slot present | Pass/fail |
| Power-up contrast | 3 Class A pickups + Surprise Cube against every floor tile and decal in the pack | ΔE ≥ 30 |
| Cube glyph legibility | ? plate at 32px, greyscale, over every floor tile in the pack |
Human review — release blocker |
| Cube vs Ice Cube | Both at 32px, greyscale and colour, side by side and on adjacent tiles | Human review — release blocker |
| Currency vs cubes and Gold | economy/sugar_cube vs both game cubes, 32px greyscale; vs Gold badge, 32px colour, adjacent |
Human review — §6.7.2 |
| Reveal polarity | reveal_good vs reveal_bad, greyscale, first 6 frames only |
Human review — must be callable before the icon is legible |
| Tier separation | Solid vs destructible, silhouette-only, at 32px | Human review + luminance delta ≥ 25 |
| Blast rim hue | Rim pixels within 10°–45° | Pass/fail |
| Blast mask parity | Skin alpha silhouette matches core mask | Pixel-exact |
| Flavour pulse integrity | Label decal alpha never overlaps the fuse pulse region | Pixel-exact |
| Flavour silhouette | Can outline identical to core with label applied | Pixel-exact |
| Character footprint | Skin within 85% tile width, hat box respected | Pass/fail |
| Colourblind | Deuteranopia, protanopia, tritanopia simulation on the four player colours plus all blast skins | Human review |
| Atlas budget | Bundle ≤ 2048² per atlas | Pass/fail |
| Map validity | Every .map in the pack passes Appendix A §A.6 |
Pass/fail |
The power-up contrast gate is the price of the non-variant decision. It means an arena pack cannot use Sunbeam-adjacent hues on its floor — worth stating to artists on day one rather than discovering it at review.
9. Launch Scope¶
| Bundle | Unit count | Assets each | Total |
|---|---|---|---|
| Core — can, pickups, cube, status, blast mask, fx | 1 | ~205 | 205 |
| Core — HUD & UI | 1 | ~150 | 150 |
| Core — leagues | 1 | ~40 | 40 |
| Core — economy | 1 | ~13 | 13 |
| Arena packs | 2 | ~65 | 130 |
| Character base skins | 4 | ~112 | 448 |
| Blast — classic | 1 | ~54 | 54 |
| Flavour — classic | 1 | ~24 | 24 |
| Launch total | ~1,064 |
Post-launch unit costs, useful for roadmap planning:
| New content | Asset cost |
|---|---|
| One arena pack | ~65 |
| One character skin | ~112 |
| One blast skin | ~54 |
| One can flavour | ~24 |
| One flavour + matched blast SKU | ~78 |
Character skins are by far the most expensive thing to sell and flavours the cheapest — a useful shape for a store, provided §5.3 and §5.4's constraints hold. The matched flavour-and-blast SKU is the interesting middle: roughly two-thirds the cost of a character skin, visible to all four players in a match rather than only to the owner's own avatar, and the only cosmetic that reads at a glance from across the grid.
10. Maps and Probability Tables¶
10.1 Two probability tables, not one¶
Worth stating plainly, because they are easy to conflate and the bug is silent:
| Table | Question it answers | Where | Who owns it |
|---|---|---|---|
| Drop table | When a block is destroyed, what spawns on that tile? | Map header, drop_weights |
Level designer, per map |
| Cube pool | When a Surprise Cube is opened, which of the six effects does it roll? | Fixed in code | Nobody — it is a constant |
They share no values and never will. The drop table has five entries including nothing and Surprise Cube; the cube pool has six entries, all effects, flat, no nothing row. A cube can never contain a cube. If an implementation rolls cube contents from the drop weights, a map that weights Surprise Cube highly produces cubes containing cubes, and the bug will present as "power-ups feel wrong on the Beach Kiosk" rather than as an exception.
10.2 Map delivery — bundled baseline, server adds¶
The base app ships a complete, playable baseline set of maps for every bundled pack. The server may add maps. It may never replace or remove one.
This is the right shape and it is worth understanding why the additive constraint matters more than the delivery mechanism:
- Offline play always works, and works identically to online play. A player on a plane has the full baseline, not a degraded subset
- A server outage cannot remove content. The worst case is that no new maps appear
- No map ever changes under a player's feet. A map ID resolves to exactly one file, forever. If a map needs a fix, it ships as a new ID and the old one is retired from rotation rather than edited
- Rollback is free. Pulling a bad map from rotation is a list change, not a client update
Three requirements this places on the pipeline:
- Map IDs are globally unique and permanent. Namespace them by pack:
diner/crossroads. A server map must never collide with a bundled one, and the validator checks the whole namespace, not just the pack directory - Server-added maps are validated on upload, not at build. Every rule in §A.7 has to run server-side too. A bundled map that fails validation breaks a build; a server map that fails validation breaks a live match, which is considerably worse
- Clients fetch missing maps at Lobby. A
.mapfile is under 4 KB, so this is a negligible download — but the lobby must handle "one player has this map and another does not" rather than assuming a shared set. Fetch on match config, before the ready check
10.3 Duel maps — capacity is a ceiling¶
Most v1 maps declare players: 4 and carry four spawns. A 2-player match on that map uses two of them — so players is the map's spawn capacity, not the size of the match played on it. Validation rule 8: spawn count equals players, and a match may have fewer players than the map's capacity, never more.
A map is therefore offered only to matches at or below its capacity. A 4-spawn map takes 2, 3 or 4 players; diner_booth_standoff, at capacity 2, takes only 2 and is never selectable for a four-player match. MapLoader enforces this as a load-time assertion.
Which two spawns get used is a real decision. Reading order would put both players on the top edge, adjacent, which turns a duel into a knife fight at tick zero. The rule is in Gameplay Rules §3.3: choose the subset with maximum pairwise separation.
The duel tag and the worked duel example in §A.6 are the format's duel support.
Appendix A — Map File Specification¶
Format version 1.
A map is a plain text file describing one grid layout. It contains no art references. Every glyph names a slot in the arena manifest, and the pack the map lives in decides what that slot looks like — which is what lets the same layout play as a Diner or a Beach Kiosk without editing a byte.
A.1 The Glyph Principle¶
Dense symbols are solid. Digits are breakable. Punctuation is empty.
And every glyph is literal. There is no glyph that resolves probabilistically — the file is the board. ? and ~ are reserved.
That single rule is the whole format, and it means a designer can read a map's structure without consulting this document. # and % look like walls at a glance; 1 and 2 don't. The mapping is doing visual work in the text editor, which is the point of using text at all.
Two consequences worth stating up front:
#is solid, not a comment. Comments use;. A wall-shaped character should mean wall; giving the densest glyph in ASCII to metadata wastes it..is empty and only empty. It's the most common character in any map file, so it gets the lightest, quietest glyph. There is no separate void marker — see §A.9.- Rendering is deterministic, not merely layout. Two clients, a preview tool and a screenshot taken a year apart all draw the identical arena from the identical file. Which variant of a slot each tile uses is a pure function of the tile's position (§A.5), never of a match seed. This is the same principle as literal glyphs applied one level down, and it is what makes an offline preview trustworthy enough to tune art against.
A.2 File Conventions¶
| Property | Rule |
|---|---|
| Encoding | ASCII only. No UTF-8, no BOM, no box-drawing characters |
| Line endings | LF (\n). CRLF accepted on read and normalised; never written |
| Extension | .map |
| Location | arenas/{pack}/maps/{name}.map |
| Naming | Lowercase, underscores: diner_crossroads.map |
| Comments | Lines beginning ;. Permitted in the header only, never inside the grid |
| Whitespace | No tabs anywhere. No trailing spaces — a stray space is a parse error, not a warning |
| Final line | Must end with a newline |
| Max size | 4 KB. A map larger than this is a bug |
Why raw text and not JSON: the grid has to be readable and editable as a grid. A designer needs to see the arena in the file, count tiles by eye, and check symmetry by folding the page in half. Any format that wraps rows in quotes and commas destroys that, and the header is small enough not to need structure.
A.3 File Structure¶
; comment lines start with a semicolon
key: value
key: value
---
<terrain rows> ; 13 rows of 11, required
---
<decal rows> ; 13 rows of 11, optional — omit entirely for none
Everything before the first --- is the header. What follows is the terrain grid, one row per line, top row first — this is the board, and it is required.
An optional second --- introduces the decal grid, the same 11×13 shape, addressing the same tiles. It carries purely cosmetic floor dressing (§A.5) and affects no rule of play: nothing in it collides, blocks a blast, or changes a validation outcome other than its own.
Two blocks, because one tile carries two facts. A floor tile has a terrain identity and may carry a decal, and a single character cannot express both without a combinatorial glyph set. Two aligned grids keep one character per tile in each, which preserves the property §A.2 exists to defend: you can read the arena, and now its dressing, by eye, and check either for symmetry by folding the page.
A file with no decal block is valid and complete, and means no decals. That is the common case; do not write an all-. block to say nothing.
A.4 Header Keys¶
| Key | Required | Type | Notes |
|---|---|---|---|
format |
yes | int | Currently 1. Parser rejects unknown versions |
name |
yes | string | Display name, localisation key |
pack |
yes | string | Arena pack this map belongs to. Must match the containing directory |
size |
yes | 11x13 |
Must be exactly 11x13 in format 1. Redundant on purpose — catches truncated files. Variable dimensions break the portrait layout budget's 80px minimum tile; see Gameplay Rules §3.2 |
players |
yes | 2–4 | Spawn capacity, not match size. Must equal the spawn marker count. A match may seat fewer players than this; never more. Most v1 maps declare 4. Six-player is a version 2 change arriving with A–F spawn assignment |
drop_weights |
yes | 5 ints | The drop table for this map, in order: nothing, Shaker, Extra Can, Ice Cube, Surprise Cube. Required — there is no pack default and no global fallback. A map's block density and its drop weights are the same balance decision and belong in the same file. Gameplay Rules §8.5 |
symmetry |
no | enum | none | mirror_h | mirror_v | mirror_both | rot180. Validated if present |
tags |
no | csv | Free-form labels for rotation and matchmaking filters. duel, chaotic, tight, open, service, hazard are examples, not a closed vocabulary — see below |
There are no procedural keys. Every tile is authored.
The tags list is illustrative and the validator enforces no vocabulary, because a matchmaking filter is exactly the thing that wants a new label without a format revision, and a closed vocabulary would make adding one a format bump. The cost is that a typo is a silently useless tag rather than a validation error; that is worth less than the revision it would otherwise cost, and a pack lint can catch it without touching the format.
One cost to accept with drop_weights required per map: a global rebalance means editing every map file rather than one manifest. That is a real maintenance tax and it grows with the map count. It is the correct trade anyway — a map's block count and its drop weights determine the same thing, the rate at which power arrives, and splitting them across two files is how a pack ends up feeling wrong for reasons nobody can locate. Mitigate with a bulk-edit script, not with a fallback. The only randomness in a match is the drop table, seeded from the match seed — see A.8.
A.5 Grid Glyphs¶
Solid — indestructible¶
| Glyph | Meaning | Manifest slot |
|---|---|---|
# |
Solid, variant derived from position | solid_block[hash(x,y) % 3] |
% |
Solid, pinned to variant A | solid_block[0] |
@ |
Solid, pinned to variant B | solid_block[1] |
& |
Solid, pinned to variant C | solid_block[2] |
Use # by default and pin only when it matters. The derivation below distributes variants without visible repetition, and it does a better job of it than a human placing them by hand. Pinning exists for landmarks — the jukebox that must sit at a specific spot, the groyne that anchors the composition. A map where every solid is pinned is a map someone did unnecessary work on, and it will look more repetitive, not less.
Variant derivation — the exact function¶
Wherever a glyph names a slot with more than one variant and does not pin one, the index is a pure function of the tile's position. No seed, no match state, no device entropy.
variant(x, y, count):
h = (x * 73856093) XOR (y * 19349663) ; 32-bit, wrapping
h = h XOR (h >>> 13)
h = h * 1274126177 ; 32-bit, wrapping
h = h XOR (h >>> 16)
return (h AND 0x7FFFFFFF) MOD count
Applied to # → solid_block (3), and to every walkable tile → floor_tile (3). solid_cap is not derived this way — cap selection is a function of which neighbours are solid, which is already deterministic.
These constants are frozen. They are not a tuning parameter and not an implementation detail: changing one silently re-dresses every map in the game, and the first symptom is a screenshot that no longer matches a shipped build. A change here is a format change.
Why a hash rather than authoring. Determinism and authorship are separable, and only determinism is load-bearing. Floor is roughly a hundred of the hundred and forty-three tiles and is the quietest thing on screen; making a designer choose a variant for each would add noise to the file for a decision nobody would make deliberately. Solids get both — derived by default, pinnable by exception.
Destructible — breakable, drops from the table in §6.2¶
| Glyph | Meaning | Manifest slot |
|---|---|---|
1 |
Destructible, family A | destructible[0] |
2 |
Destructible, family B | destructible[1] |
3 |
Destructible, family C | destructible[2] |
4 |
Destructible, family D | destructible[3] |
Empty and directives¶
| Glyph | Meaning |
|---|---|
. |
Empty floor. Walkable, blast passes through. Renders floor_tile[hash(x,y) % 3] |
S |
Player spawn. Resolves to empty floor. Assigned in reading order, top-left first |
H |
Arena hazard tile. Resolves to ambient_animated[0]. Walkable floor, periodically lethal — Gameplay Rules §12.1. Pack-specific: the Diner's soda fountain |
H is always passable. A tile with collision could be neither stood on nor hold a can, which would make the hazard unreachable — a solid block with an animation playing on it. Passable and periodically lethal keeps collision, §A.7 connectivity and blast propagation (Gameplay Rules §6.2) all static: nothing about the board varies with the phase, and only damage does. The alternative — walkable when dormant, solid when active — is rejected on cost, because it makes walkability time-varying and every consumer of it, from §A.7 to corner assist to the player AI, would need a phase-aware query for one glyph.
On the second ambient_animated. The slot has a count of two (Part 4 §5.1) and only one of them is a hazard. ambient_animated[1] is backdrop dressing, placed by the pack and never by the map — the jukebox glow, the gull, the ceiling fan. It has no glyph because it never occupies a playfield tile, which is the same reason ambient_static has none. Index 0 is the hazard; index 1 is scenery. Both are still deterministic, because neither is chosen.
Decal glyphs — second grid block only¶
These are legal only in the optional decal block (§A.3) and are a parse error in the terrain grid.
| Glyph | Meaning | Manifest slot |
|---|---|---|
. |
No decal on this tile | |
a |
Floor decal A | floor_decal[0] |
b |
Floor decal B | floor_decal[1] |
c |
Floor decal C | floor_decal[2] |
d |
Floor decal D | floor_decal[3] |
Decals are painted onto the floor and never occlude, never collide, and never affect a rule of play (Part 4 §5.1). A decal may therefore only sit over a tile that is walkable in the terrain grid — see §A.7.21. Unlike floor and solid variants, decals are authored rather than derived, because a decal is a composition decision about a specific spot and a hash cannot make one.
Reserved and unused: 5–9, 0, $, X, T, G, R, ?, ~, A–F and all other characters are reserved for format version 2, in both blocks. A parser encountering one must reject the file rather than skip the glyph — a silently ignored character is a map that plays differently than it looks, and that bug is very hard to find later.
A.6 Worked Examples¶
Hand-authored¶
arenas/diner/maps/diner_crossroads.map
; Diner — Crossroads
; Launch map 1 of 3. Mirror-both. Dense corners, open centre.
; The two % tiles flanking the centre are the pinned jukeboxes.
; Decal block: spilled-soda ring at the crossing, tile scuffs on the approaches.
format: 1
name: diner_crossroads
pack: diner
size: 11x13
players: 4
drop_weights: 62,12,10,6,10
symmetry: mirror_both
tags: open, chaotic
---
###########
#S..1.1..S#
#.#1#2#1#.#
#.21.3.12.#
#2#3#1#3#2#
#.1243421.#
#1#2%.%2#1#
#.1243421.#
#2#3#1#3#2#
#.21.3.12.#
#.#1#2#1#.#
#S..1.1..S#
###########
---
...........
..c.....c..
...........
....b.b....
...........
...........
.....a.....
...........
...........
....b.b....
...........
..c.....c..
...........
Note how fast the structure reads — the lattice of # is visible as a pattern, and the digits stand out as the things that will disappear during a round.
Read the decal block against the terrain block by eye. Every mark sits on a tile that is . in the grid above it, and the layer folds along both axes exactly as symmetry: mirror_both promises. Both facts are checkable without running anything, which is the entire argument for a second aligned grid over a coordinate list in the header.
The same file dropped into arenas/beach_kiosk/maps/ with pack: beach_kiosk plays identically and looks nothing alike: solid becomes concrete groyne instead of chrome counter, destructibles become sun-bleached parcels with teal tape and beach pictograms instead of red-taped Diner parcels.
A duel map¶
Capacity 2 (§10.3). Same pack, two spawns, deliberately tighter. Note the drop weights differ from the 4-player map: fewer blocks means each one has to carry more. It carries no decal block at all, which is legal, complete and the common case — an absent block means no decals, and an all-. block written to say the same thing is noise.
; Diner — Booth Standoff
; Duel map. Rot180. Spawns diagonal, centre is the only crossing.
format: 1
name: diner_booth_standoff
pack: diner
size: 11x13
players: 2
drop_weights: 58,12,10,8,12
symmetry: rot180
tags: duel, tight
---
###########
#S..1.1...#
#.#1#2#1#.#
#..21.12..#
#2#3#.#3#2#
#.1234321.#
#1#2%.%2#1#
#.1234321.#
#2#3#.#3#2#
#..21.12..#
#.#1#2#1#.#
#...1.1..S#
###########
Note the . gaps at rows 4 and 8 in the pillar columns: those are the perpendicular exits that satisfy A.7.16. Without them the vertical corridors on this map would run nine tiles unbroken, which is a hard validation failure and — more to the point — an unfair death waiting to happen.
A.7 Validation Rules¶
Run at build time on every map in every pack, and server-side on upload for every server-added map (§10.2). All are hard failures unless marked advisory.
The two passability predicates, and which rule uses which¶
Every rule below that asks "can something be here?" asks it in one of exactly two ways. Which one each rule uses is load-bearing, because H is passable floor (§A.5) and the two predicates disagree about it:
| Predicate | True for | Used by |
|---|---|---|
| walkable | . and S only |
rules 9, 16a |
| open — "not solid" | everything except #, %, @, &: floor, spawns, destructibles and hazards |
rules 12, 13, 16 |
Rules 12, 13 and 16 evaluate the board with destructibles removed, so they go through the not-solid test, which admits H for free.
Rule 9 keeps the stricter test deliberately. A spawn's escape tiles must be tiles a player can wait on, and a periodically lethal tile is not a guaranteed escape. Letting H count would let an author ring a spawn with fountains and pass validation. Rule 16a keeps it for a different reason: a decal is drawn on the floor, and H already draws ambient_animated[0] on that tile.
Structural
- Header parses;
formatis a known version - Every row is the same length
- Grid dimensions equal
size, andsizeis exactly11x13 - Only glyphs from §A.5 appear, and only those legal for the block they appear in — terrain glyphs in the terrain grid, decal glyphs in the decal grid, neither in the other
4a.
drop_weightsis present and parses as five non-negative integers, not all zero 4b. The decal block, if present, is exactly 13 rows of 11 columns — the same shape as the terrain grid it addresses 4c. At most one decal block. A third---is a parse error packmatches the containing directory, andpack/nameis unique across the entire map namespace, bundled and server-side together (§10.2)- Every pinned solid glyph (
%,@,&) maps to a variant index that exists in the pack manifest. A map pinning variant C in a pack that ships two solid variants must fail loudly, not fall back silently
Boundary
- Every tile in row 0, row H−1, column 0 and column W−1 is a solid glyph. The border is always sealed — a blast that leaves the grid is undefined behaviour, and every map must make that impossible by construction
Spawns
- Spawn count equals
players.playersis spawn capacity — a match may seat fewer, never more (§10.3) - Every spawn has an escape from its own first can at the starting blast range of 1 (Gameplay Rules §2): an orthogonally adjacent empty tile, and beyond it one more empty tile that is either around the corner (a step perpendicular to the first, diagonal to the spawn) or straight on (two tiles out in the same direction). Either tile is outside the can's range-1 cross, two tiles from the spawn: 0.67s at 3.0 tiles/s against the 2.0s fuse. Put differently, an empty tile two steps from the spawn, reached through an empty neighbour. One such escape is enough; a player who can be trapped by the first can they place has been killed by the level designer. Walkable, not open — a hazard is not an escape. (Changed 2026-09-26: rule 9 required two escaping neighbours. One escape already survives the first can, and the stricter rule rejected playable spawns. If the starting blast range ever changes, the straight escape grows with it.)
- Minimum spawn separation: 6 tiles Manhattan
- Spawns may sit anywhere. Corners are conventional, not required — fairness is checked by rules 9, 10 and 12, not by position
Playability
- Connectivity with all destructibles removed: every empty tile is reachable from every spawn. An unreachable pocket is a place a pickup can spawn where nobody can go. Open
- Connectivity with all destructibles present: each spawn has a route out requiring no more than 2 blocks destroyed. Otherwise the opening fifteen seconds are pure digging. Open, at a cost of 1 per destructible crossed and 0 for everything else, hazards included. "Out" is reaching any tile at Manhattan distance 4 or more from the spawn. The alternative reading — cost to reach another spawn — costs 5 on all three shipped maps and would fail this appendix's own §A.6 worked example at that threshold, so distance from the spawn itself is the rule
- Destructible count ≥ 20% of interior tiles. Below that there is nothing to break, and with one drop table (§6.2) that means nothing to collect
- If
symmetryis set, the grid satisfies it exactly, spawns and pinned variants included - Corridor exit rule — every tile has a perpendicular exit within 3 tiles along its corridor. Hard failure above 6. See Gameplay Rules §3.4. Open, because this rule is evaluated with all destructibles removed exactly as rule 12 is — treating destructibles as walls makes §A.6's own worked examples fail, since both ring their floor with destructible families
Decal layer — only if the block is present
16a. Every non-. decal sits over a tile that is walkable in the terrain grid — . or S. A decal over a solid, destructible or hazard tile is a hard failure, because Part 4 §5.1 says a decal never occludes and there is nothing under it to not-occlude. Walkable, not open, and a hazard is excluded here even though it is now floor: H already draws ambient_animated[0] on its tile, so there is something under the decal after all
Rule 16 is the one that matters most. With authored solids, a nine-tile corridor with no exits is expressible, looks fine in a text editor, and is an inescapable death lane against a maxed blast range.
The three is derived from base speed. Reaching the exit tile is not safety, so the escape is three tiles along plus one through the opening — four tiles, 1.33s at 3.0 tiles/s, leaving 0.67s of margin. Under Honeycomb Wax the same escape takes 2.22s, past the fuse. Gameplay Rules §3.4 carries the full table and the decision that Wax plus maximum blast range is meant to be lethal without a near corner, and an author using the 4–6 advisory band should read it — a 6-tile corridor is fatal at base speed with no debuff at all.
Advisory — warnings, not failures
- Corridor exits between 4 and 6 tiles apart. Legitimate for a deliberate chokepoint; flag so it is a decision rather than an accident
- A 4-player map should be symmetric. Asymmetry is a legitimate choice for party modes and a serious problem for anything ranked
- Pinned solids under 20% of all solids. Above that, flag for review — see §A.5
- Solid blocks under 35% of interior tiles. Denser than that and the field is corridors rather than a grid
20a.
symmetryis set and the decal block does not satisfy it, while the terrain grid does. Deliberate asymmetric dressing on a symmetric arena is a legitimate art choice — a scuff in one corner and not its mirror — so this is flagged rather than failed. Rule 15 governs the terrain grid and is unaffected
A.8 Load Sequence¶
parse header
↓
parse terrain grid, then the decal grid if a second --- follows
↓
validate (§A.7) — reject the map, never repair it
↓
seed RNG from match seed + map name # the drop table, and nothing else
↓
resolve S → floor, register spawn points in reading order
↓
resolve H → ambient_animated[0], walkable floor
↓
resolve # → solid_block[variant(x, y, 3)]; %, @, & keep their pinned variant
↓
assign solid_cap edges by neighbour rules
↓
assign floor_tile[variant(x, y, 3)] to every walkable tile
↓
overlay decals from the decal grid, if present
↓
hand off to renderer
The RNG seed covers the drop table and nothing else. The board is deterministic from the file in both layout and appearance. Variant picks are a pure function of position (§A.5) rather than of the match seed, so every client, every replay, every preview tool and every screenshot draws the identical arena. Seed the drop table from the match seed and the map name, never wall-clock time and never a local device source: a pickup appearing on one client and not another is the same bug, just later in the match.
Why appearance determinism is worth a rule. Seeded cosmetics would mean a map had no canonical look, which makes an offline preview an approximation and art tuning a guess — you would grade a palette against one arrangement and ship another. It would also make a bug report's screenshot unreproducible.
A.9 A Note on Voids¶
Format 1 has no void — no tile without floor, and so no non-rectangular arena. . covers everything that isn't a block.
The reasoning: the border must be sealed with solid blocks anyway (§A.7.7), so a void could never touch the edge of the play field where an irregular outline would actually be visible. That left it doing very little for a real validation cost — interior voids read as holes in the floor and need their own contiguity rules. If a genuinely non-rectangular arena turns out to be worth building, X is reserved and the feature can arrive in format 2 with the border rule rewritten to suit it.
A.10 Format Version 2 — Reserved¶
Do not build these now; do keep the glyphs free.
5–9— additional destructible families, if a pack ever needs more than fourX— void, per §A.9T— teleport pairG— guaranteed drop, for tutorial and scripted mapsA–F— explicit per-player spawn assignment, replacing reading order, for asymmetric and team modes. Six letters, not four, so a version 2 six-player mode does not need a third format revisionR— reinforced two-hit destructible, if the style guide §7 tier is ever built. It is not in the version 1 slot manifest and has no glyph today$— moving hazard or conveyor tile
Appendix B — Asset Encyclopedia¶
Every shippable art asset in version 1, in two flat tables. This is a reading aid, not a manifest — §5 binds the packs, §6 binds Core, and they win any disagreement with this appendix.
Directories. Core/… below means Assets/Game/Art/Core/…, exported once at @3x (§4). All other paths hang off Content/, and every art/ directory is three sibling resolutions — @1x/, @2x/, @3x/ — carrying the same filename in each (§4). Launch fills: {pack} = diner, beach_kiosk · {chugger} = rax, puff, nim, bolt · {skin} and {flavour} = the free defaults (base, classic).
Frames. Shipped frames per animation. — means static, a particle sheet, or a count this document does not fix. West mirrors east at runtime and is never authored (§3.2); directional counts below are authored directions.
B.1 Core¶
| Slot | Asset | Directory | Frames |
|---|---|---|---|
can_idle |
Can, idle — held and placed reference | Core/can/ |
4 |
can_armed_pulse |
Can, armed pulse — Frenzy Red → Sunbeam, fixed 2.0s curve | Core/can/ |
12 |
can_slide |
Can, sliding (kicked) | Core/can/ |
6 |
can_bounce |
Can, impact bounce | Core/can/ |
4 |
can_settle |
Can, slide settling to rest | Core/can/ |
4 |
shaker |
Shaker — Class A pickup, +1 blast range, cap 6 | Core/powerups/ |
12 |
ice_cube |
Ice Cube — Class A pickup, unlocks kick, cap 3 | Core/powerups/ |
16 |
extra_can |
Extra Can — Class A pickup, +1 simultaneous can, cap 5 | Core/powerups/ |
12 |
pu_cube_idle |
Surprise Cube, idle — ? faces the camera every frame |
Core/powerups/ |
16 |
pu_cube_open |
Surprise Cube, open — shell splits, contents not yet legible | Core/powerups/ |
8 |
pu_cube_reveal_good |
Surprise Cube, buff reveal — rising, saturated, sharp | Core/powerups/ |
10 |
pu_cube_reveal_bad |
Surprise Cube, debuff reveal — sagging, drab, viscous | Core/powerups/ |
10 |
reveal_icon |
Effect reveal icons — one per effect, burst centre and above the player | Core/powerups/ |
6 icons |
coffee_bean |
Coffee Bean — Class B reveal icon and HUD band icon | Core/powerups/ |
12 |
honeycomb_wax |
Honeycomb Wax — Class B reveal icon and HUD band icon | Core/powerups/ |
14 |
glue |
Glue — Class B reveal icon and HUD band icon | Core/powerups/ |
14 |
status_kick_ready |
Kick-ready overlay — frost glint at feet | Core/status/ |
8 |
status_frost |
Frost overlay — Ice Cube stacks 2–3 | Core/status/ |
2 steps |
status_speedlines |
Can speedlines — sliding at stack 2+, cool | Core/status/ |
6 ×2 intensities |
status_armed |
Held-can attachment — from Core/can/ |
Core/status/ |
— |
status_speed |
Coffee Bean overlay — warm streaks off the player | Core/status/ |
8 |
status_slow |
Honeycomb Wax overlay — amber drips, sagging | Core/status/ |
8 |
status_glued |
Glue overlay — white strings + crossed-out can symbol | Core/status/ |
8 |
status_reveal |
Cube reveal — effect icon above the head, public | Core/status/ |
10 |
burst_core |
Blast mask, origin tile — geometry + timing, skin-independent | Core/blast_mask/ |
8 |
arm_h_mid |
Blast mask, horizontal arm segment | Core/blast_mask/ |
8 |
arm_v_mid |
Blast mask, vertical arm segment | Core/blast_mask/ |
8 |
arm_cap |
Blast mask, arm tip ×4 directions | Core/blast_mask/ |
8 ×3 dir |
residue_decal |
Blast mask, fading puddle | Core/blast_mask/ |
4 |
droplet_sheet |
Blast mask, particle shapes | Core/blast_mask/ |
— |
spawn_burst |
Pickup/cube arrival burst — shared, plays after debris | Core/fx_generic/ |
6 |
pickup_pop |
Collect burst — rising, saturated, Sunbeam-cored | Core/fx_generic/ |
— |
pickup_destroyed |
Blast destroying a pickup — sagging, drab, downward | Core/fx_generic/ |
8 |
dust |
Dust particles | Core/fx_generic/ |
— |
sparkle |
Sparkle particles | Core/fx_generic/ |
— |
avatar_frame |
HUD avatar frames ×4 — one per player slot | Core/hud/ |
— |
shape_badge |
Colourblind identity badges ×4 — circle, square, triangle, star | Core/hud/ |
— |
timer |
Match timer — numerals from Core/type/ |
Core/hud/ |
— |
powerup_pip |
Class A counters ×3 — current and cap (6/5/3) | Core/hud/ |
— |
timed_band |
Timed-effect depleting band — frame + fill | Core/hud/ |
— |
timed_icon |
Timed-effect band icons ×3 | Core/hud/ |
— |
opponent_state |
Opponent debuff indicators ×3 | Core/hud/ |
— |
pause |
Pause control — single player only | Core/hud/ |
— |
tile_telegraph |
Pre-blast tile warning | Core/hud/ |
4 |
countdown |
Round countdown — 3, 2, 1, "GO" | Core/hud/ |
4 |
banner |
Victory / defeat banners ×2 | Core/hud/ |
— |
fuse_ring |
Fuse ring — depleting arc, renders above everything, smooth-interpolated | Core/hud/ |
— |
menu_chrome |
Menu chrome — 9-slice panels, buttons, toggles (~20) | Core/ui/ |
— |
store |
Store chrome — cards, currency, rarity frames, purchase flow (~25) | Core/ui/ |
— |
lobby |
Lobby chrome — slots, ready states, character select (~15) | Core/ui/ |
— |
results |
Results chrome — scoreboard, rewards, progression (~12) | Core/ui/ |
— |
leagues_ui |
Leagues screen chrome — standing row, sticky self row, band dividers, countdown plate, empty and stale states (~14) | Core/ui/ |
— |
how_to_play |
How to Play — panel chrome, page dots, 7 effect icons, 3 can-state icons, diagrams (~18) | Core/ui/ |
— |
league_badge |
Tier badges ×6 — Tin, Bronze, Silver, Gold, Diamond, Elite, 256² | Core/leagues/ |
— |
league_badge_chip |
Tier badge chips ×6, 64² — redraws, not downscales | Core/leagues/ |
— |
league_badge_locked |
Locked badge ×1 — greyed, tinted per tier | Core/leagues/ |
— |
league_badge_elite_shimmer |
Elite shimmer — the one animated badge, never at chip scale | Core/leagues/ |
12 |
league_frame |
Badge plate ×1 — 9-slice, shared across all six tiers | Core/leagues/ |
— |
league_band_promote |
Promotion band — 9-slice, cool and rising | Core/leagues/ |
— |
league_band_demote |
Demotion band — 9-slice, drab and sagging, never the warm hazard band | Core/leagues/ |
— |
league_band_divider |
Cut-line dividers ×2 — one promote, one demote | Core/leagues/ |
— |
league_arrow |
Movement arrows ×3 — up, down, hold, direction in shape | Core/leagues/ |
— |
league_stinger_promote |
Promotion stinger | Core/leagues/ |
10 |
league_stinger_demote |
Demotion stinger — deliberately shorter and quieter | Core/leagues/ |
8 |
league_stinger_hold |
Hold stinger | Core/leagues/ |
6 |
sugar_cube |
Sugar Cube, hero — currency icon, 256² | Core/economy/ |
— |
sugar_cube_chip |
Sugar Cube chip, 64² — redraw, not a downscale | Core/economy/ |
— |
cube_burst |
Reward burst — Match Results and level-up | Core/economy/ |
8 |
xp_bar |
XP bar — 9-slice frame + fill | Core/economy/ |
— |
level_badge |
Level badge frame, 128² — numerals from Core/type/ |
Core/economy/ |
— |
nunito_extrabold |
UI and body face — licensed binary, not authored | Core/type/ |
— |
baloo_2 |
UI and body face — licensed binary, not authored | Core/type/ |
— |
display_custom |
Custom display face — headers and logo, authored | Core/type/ |
— |
logo_lockups |
Fizz Frenzy wordmark lockups | Core/type/ |
— |
app_icon |
App icon — can mid-burst on Frenzy Red, reads at 48px | Core/store_meta/ |
— |
screenshots |
Store screenshots — captures of shipped art | Core/store_meta/ |
— |
feature_graphic |
Store feature graphic — composition of shipped art | Core/store_meta/ |
— |
B.2 Packs¶
| Slot | Asset | Directory | Frames |
|---|---|---|---|
Arena pack — ~65 + maps. Launch: diner, beach_kiosk |
|||
backdrop_far |
Far backdrop — static, no parallax | arenas/{pack}/art/ |
— |
backdrop_near |
Near backdrop — parallax, max 8px travel | arenas/{pack}/art/ |
— |
floor_tile |
Floor tiles ×3 — lowest-contrast surface on screen | arenas/{pack}/art/ |
— |
floor_edge |
Field frame — 9-slice | arenas/{pack}/art/ |
— |
floor_decal |
Floor decals ×4 — painted flat, placed by the map's decal grid (glyphs a–d), never occludes |
arenas/{pack}/art/ |
— |
solid_block |
Solid blocks ×3 — dark, cold, heavy; full tile, flat top | arenas/{pack}/art/ |
— |
solid_cap |
Solid edge and corner caps ×4 | arenas/{pack}/art/ |
— |
destructible |
Destructible blocks ×4 families — light, warm, cheap; 85% tile; the launch packs build them from one parcel and four pictograms | arenas/{pack}/art/ |
— |
destructible_cracked |
Damaged states ×4 — one per family | arenas/{pack}/art/ |
— |
destructible_break |
Break animations ×4 families | arenas/{pack}/art/ |
6 ×4 fam |
debris_sheet |
Debris particle sheets ×1–4; a family without its own uses [0] |
arenas/{pack}/art/ |
— |
ambient_static |
Static dressing ×6 — backdrop or floor only, never a playable tile | arenas/{pack}/art/ |
— |
ambient_animated |
Animated dressing ×2 — [0] H hazard tile, walkable floor; [1] off-field scenery |
arenas/{pack}/art/ |
8 frames each |
grade_profile |
Lighting/LUT profile — config, not an image | arenas/{pack}/ |
— |
meta_card |
Store thumbnail, 512² | arenas/{pack}/art/ |
— |
meta_loading |
Loading art — full-bleed | arenas/{pack}/art/ |
— |
Character skin — ~112. Launch: rax/base, puff/base, nim/base, bolt/base |
|||
idle |
Idle loop | characters/{chugger}/{skin}/art/ |
8 ×3 dir |
run |
Run cycle — leaning into travel | characters/{chugger}/{skin}/art/ |
8 ×3 dir |
place_can |
Place-can — wind-up, shake, throw, recovery hop | characters/{chugger}/{skin}/art/ |
6 ×3 dir |
kick |
Kick — mandatory on every skin ever authored | characters/{chugger}/{skin}/art/ |
4 ×3 dir |
hit_pop |
Pop — inflate, wobble, deflate with a raspberry; no gore | characters/{chugger}/{skin}/art/ |
8 |
victory |
Victory pose | characters/{chugger}/{skin}/art/ |
12 |
defeat |
Defeat pose | characters/{chugger}/{skin}/art/ |
8 |
portrait |
HUD portrait — head and shoulders | characters/{chugger}/{skin}/art/ |
— |
emote_face |
Emote faces ×4 — face only | characters/{chugger}/{skin}/art/ |
— |
store_card |
Store card, 512² — full body, hero framing | characters/{chugger}/{skin}/art/ |
— |
Blast skin — ~54. Launch: classic, shipped in the base app |
|||
burst_core |
Origin tile fill — inside the Core mask | blast/{skin}/art/ |
8 |
arm_h_mid |
Horizontal arm fill | blast/{skin}/art/ |
8 |
arm_v_mid |
Vertical arm fill | blast/{skin}/art/ |
8 |
arm_cap |
Arm tip fill — 4 directions at runtime, west mirrors east | blast/{skin}/art/ |
8 ×3 dir |
residue_decal |
Residue puddle fill | blast/{skin}/art/ |
4 |
droplet_sheet |
Particle atlas — bubbles, droplets, sugar sparkles | blast/{skin}/art/ |
— |
store_card |
Store card, 512² | blast/{skin}/art/ |
— |
Can flavour — ~24. Launch: classic, shipped in the base app |
|||
label_idle |
Label decal, idle — transparent outside the label area | flavours/{flavour}/art/ |
4 |
label_armed |
Label decal, armed — registered to the Core pulse, never replacing it | flavours/{flavour}/art/ |
12 |
label_slide |
Label decal, slide | flavours/{flavour}/art/ |
6 |
body_tint |
Body tint — single-channel mask beneath the label | flavours/{flavour}/art/ |
— |
store_card |
Store card, 512² | flavours/{flavour}/art/ |
— |
B.3 Not in these tables¶
Everything shipped that is not an art asset, for completeness:
- Maps — plain ASCII
.mapfiles,arenas/{pack}/maps/— data, not art (Appendix A). Bundled baseline of ≥3 per pack; the server adds more, additive only (§10.2) pack.json— per-pack import settings and defaults (§2.1) — manifest data, not artcharacters/_rig/— the shared character skeleton and attach points (§2.1) — rig data, not art- Audio — music and SFX. Style Guide §13 gives direction (surf rock, a rising fuse hiss, design-for-silence) but no slot list, naming convention or count — the only shipped category without a manifest
- The greybox pack — scripted placeholder fills for every slot, proving the build pipeline. Obviously provisional; never shipped
_source/— working files, never shipped (§2.1). The Asset Generation Brief's masters land here and are renamed to real slot names at export (§3.2)
Totals. Launch scope — bundle counts, the ~1,064 total and post-launch unit costs: §9.
Part 5 — Economy & Progression¶
One of five companion documents. Gameplay Rules says what happens on the grid. Framework & Flow says what screens exist and who talks to the server. The Style Guide and Asset Architecture say what it looks like and how it is shipped. This document says what a player has, earns and spends across their lifetime on the game.
0. Scope and ownership¶
Three numbers live on an account. Everything in this document is one of them, or serves one of them.
| Number | Lifetime | Purpose | Owned by |
|---|---|---|---|
| XP | Forever. Never resets, never decreases | Lifetime player level. A "time served" signal that pays Sugar Cubes on level-up | This document |
| League points | One week. Reset at rollover | Rank inside the weekly club. Placement, promotion, demotion | Framework & Flow §17 — not this document |
| Sugar Cubes | Until spent | The store. Arena packs, character skins, blast skins, can flavours | This document |
The three never convert into each other. XP cannot be bought. League points cannot be bought and cannot be spent. Sugar Cubes cannot buy XP, league points, promotion, or anything that touches the grid. This is the pay-to-win firewall, stated once and applied everywhere below.
| Subject | Owner |
|---|---|
| Currency, XP, prices, yields, caps, level curve, rollover payout policy | This document |
| League placement — points, clubs, cuts, rollover job | Framework & Flow §17 |
| Round pips inside a match | Gameplay Rules §11 |
| Currency and reward art — icons, bursts, store cards | Asset Architecture §6.7 |
| Currency and reward colour law | Style Guide §3.1, §3.4 |
1. Sugar Cubes — the currency¶
One currency, earned and purchased. The in-world fiction: Chuggers hoard sugar. Sugar Cubes sit next to the Frenzy Cola brand the way bottle caps sit next to real soda — the thing you collect, the thing you trade.
There is no second premium currency in version 1. A purchased-only currency exists to create price opacity and to split the store into "earnable" and "not earnable," and both of those read as hostile in a casual game rated to attract children. Every item in the store is purchasable with earned play, always. Purchased cubes are the same cubes.
1.1 What they buy¶
Every store shelf (Framework & Flow §5.12) is priced in Sugar Cubes. All prices below are starting values, held in economy_config on the server (§7), never in the client.
| Shelf | Contents | Price (starting) |
|---|---|---|
| Arena packs | One theme, its maps, hosting rights (Framework & Flow §10, decision 61: playable unowned, purchasable to host) | 1,500 |
| Character skins | Per-Chugger, kick animation enforced | 800 |
| Blast skins | Cosmetic-only foam fill | 500 |
| Can flavours | Reserved axis, post-launch | 300 |
The price shape follows the production cost shape (Asset Architecture §9): character skins are the most expensive thing made and the second most expensive thing sold; arena packs cost the most because they are the only shelf that is content, not cosmetic — a pack is maps forever, and hosting rights.
What Sugar Cubes can never buy: league points, XP, promotion, demotion immunity, round pips, pickup effects, anything rendered on the play field as an advantage. There is no SKU now or later that touches the simulation. This is the same law as blast skins being cosmetic-only (Asset Architecture §5.3), extended to the whole economy.
1.2 Earning¶
Deliberately sparse per drop, casual-generous over a week. Per-match yields are single digits and low double digits; prices are in the hundreds. A cube should feel like a thing you picked up, not a number inflating. All yields are server config.
| Source | Sugar Cubes |
|---|---|
| Online match completed (any mode) | 6 |
| Round won | +2 each |
| Match won | +4 |
| First win of the day (any mode, once per UTC day) | +10 |
| Level-up (§2.3) | 10 / 50 / 150 by milestone |
| Weekly league rollover | 0 — see §3.2 |
A best-of-3 sweep pays 14; a 0–2 loss pays 6. A player running five matches a day at a coin-flip win rate lands near 60–65 cubes a day with the first-win bonus — roughly 450 a week, before level-ups. Against the prices above: a blast skin in a good week, a character skin in under two, an arena pack in about three. That is the intended starting pressure: IAP buys impatience, never access.
Mode rules:
| Mode | XP | Sugar Cubes | League points |
|---|---|---|---|
| Quick Match | Yes | Yes | Yes (Framework & Flow §17.9) |
| Match Browser lobby | Yes | Yes | No |
| Private match by code | Yes | Yes | No |
| Single player vs AI | Yes, capped | Yes, capped | No |
- Single player is capped: the first 5 single-player matches per UTC day pay full XP and cubes; beyond that, zero. Uncapped invites farming against Easy AI; zero kills the mode. Five is enough for the mode to feel rewarding and few enough that the farm is not worth running
- All online modes pay. Friends in a private lobby playing real matches to completion are not a farm; they are the game working as intended. The anti-collusion rule sits where Framework & Flow §17.9 puts it — on the ladder, which is the only competitive surface. Cosmetics-only economies do not need the same wall
Anti-farm rules — mirrors of §17.9, applied to every reward in this document:
- Forfeit or long disconnect pays zero for the whole match, retroactively — XP, cubes, and the round-won bonuses already earned in it
- A match in which the player issued no input pays zero. The match handler already runs this check for league points; the same verdict covers economy rewards
- Yields are per match, not per round, except the round-won bonus. Quick Match still exposes no round-count option (Framework & Flow §17.3), so the flat participation yield cannot be gamed by match length
1.3 Purchasing¶
Sugar Cubes are sold in fixed bundles through the platform stores, validated server-side. The validation path — Nakama's IAP hooks or a separate service — is an open question (Framework & Flow §15 question 3); this document consumes whichever answer lands there.
| Bundle | Price anchor | Bonus |
|---|---|---|
| 100 cubes | €0.99 | — |
| 550 cubes | €4.99 | +10% |
| 1,200 cubes | €9.99 | +20% |
| 2,600 cubes | €19.99 | +30% |
| 7,500 cubes | €49.99 | +50% |
Compliance, because this is the document where money appears:
- The store shows the local-currency equivalent next to any Sugar Cube price when the purchase can be made with bought cubes. EU consumer law is heading this way regardless; doing it on day one is cheaper than retrofitting it
- No randomized paid items, ever. No loot boxes, no gacha, no paid Surprise Cubes. The store is flat: you see the item, you see the price, you buy the item. This is also half of what makes the 13+ age rating defensible (Framework & Flow §14) — a game that will attract under-13s should have nothing a regulator can call gambling, whatever its rating says
- Restore purchases already lives in Settings (Framework & Flow §5.14). Unspent purchased cubes survive reinstall; unlinked accounts are still prompted to link after first purchase (§6.2), which is the flow that protects the money
- Purchase grants are idempotent on the store transaction ID. A retried validation must never credit twice (§9)
2. XP and player level¶
XP is the lifetime number. It only goes up, it never resets, and it is the one number on the account that says how long you have been here. Framework & Flow §5.2 already renders it — the Home badge carries "equipped Chugger, name, level" — without defining it. This is the definition.
2.1 Earning XP¶
| Source | XP |
|---|---|
| Match completed (any mode) | 10 |
| Round won | +5 each |
| Match won | +10 |
A best-of-3 sweep pays 30 XP; a 0–2 loss pays 10. Single-player awards the same rates inside the daily cap of §1.2. The anti-farm rules of §1.2 apply unchanged.
XP is not competitive, so it is not mode-restricted. League points are Quick-Match-only because the ladder is a competition and private collusion would corrupt it. XP ranks nothing — there is no XP leaderboard in version 1 — so there is nothing to corrupt, and a private match with friends deserves the same lifetime credit as a matchmade one.
2.2 The level curve¶
Cumulative XP to reach level n = 35 · n². One formula, no table.
| Level | Cumulative XP | Approx. matches | Feels like |
|---|---|---|---|
| 2 | 140 | ~6 | First evening |
| 5 | 875 | ~35 | First week |
| 10 | 3,500 | ~150 | First month of regular play |
| 25 | 21,875 | ~900 | A committed season |
| 50 | 87,500 | ~3,500 | Two years at 5/day |
The quadratic flattening is the point: early levels arrive fast enough to teach that levels pay Sugar Cubes, later levels space out until the number becomes a long-term possession. There is no level cap in version 1. A prestige or frame system is a post-launch decision and is deliberately not pre-built (§8).
2.3 What a level pays¶
Level-ups are the one place the economy celebrates. The payout is credited by the server the moment the level threshold is crossed, inside the same match-end write that granted the XP.
| Event | Sugar Cubes |
|---|---|
| Every level-up | 10 |
| Every 5th level (5, 10, 15, …) | 50 |
| Every 25th level (25, 50, 75, …) | 150 |
Milestones do not stack — a level pays the single highest row it qualifies for. Level 25 pays 150, not 10 + 50 + 150.
That is all a level does in version 1, and it is enough: the level is visible on Home and Profile, and it feeds the currency loop without ever gating it. No store item is level-gated — a new player who wants to buy a skin on day one is revenue, not a problem.
2.4 Where XP appears¶
- Home — the player badge chip, level number (Framework & Flow §5.2)
- Match Results — the XP bar beat: progress fill, level-up callout if one happened, cube payout alongside (§6)
- Profile — level, total matches, lifetime stats (Framework & Flow §5.13)
XP never appears inside a match, on the grid, or in the match HUD — the same rule the leagues follow (Style Guide §3.4). Status is a menu concept.
3. League points — weekly, reset, unchanged¶
League points are owned by Framework & Flow §17 and this document does not redefine them. One point per round won, plus one for completing a matchmade game; clubs of 50; rollover Sunday 22:00 UTC; scores reset to zero. Their purpose is singular: rank weekly in the leagues.
What this document adds is the answer §17 deliberately left open.
3.1 Points are not currency¶
League points are spent on nothing, buy nothing and convert to nothing. They exist to be compared, inside one club, for one week, and then they are gone. A player asking "what are my 14 points worth?" is answered by the standing itself — rank 22 of 50 — never by a payout table.
3.2 Rollover pays nothing in version 1¶
The weekly rollover publishes a result and pays no Sugar Cubes, no XP, and no items.
The reasoning is deliberate: Sugar Cubes stay sparse, and a weekly payout to every active account is the single largest faucet the economy would ever have. A faucet that size, set wrong at soft launch, cannot be quietly taken back — players experience a removed reward as a loss far more loudly than a never-given one. Leagues therefore ship as pure status, as Framework & Flow §17's scope boundary assumes.
What is kept ready:
- The Leagues screen retains its reserved reward-strip layout under the header (Framework & Flow §5.17) — adding payouts later is a content change, not a redesign
- The rollover job's publish step (Framework & Flow §17.5, step 7) already emits each player's outgoing result; a payout table would consume that event, keyed on
week_idfor idempotency - If payouts are ever added, they pay by tier × movement — promoted above hold above demoted, higher tier above lower tier — so that deliberately demoting into an easier club is never the optimal play. Rank-based prizes are rejected in advance: they make the last hour before rollover a sniping contest
3.3 The one interaction: nothing¶
Worth stating plainly because it looks like an omission: there is no "league points also grant Sugar Cubes per point" rule, no participation conversion, no end-of-week salvage. Points rank. Cubes buy. XP accumulates. Three numbers, three jobs, zero conversions — the firewall of §0 applies to the ladder too.
4. The numbers, end to end¶
One worked week, so the shape is auditable in one place. Player: five matches a day, coin-flip results, one level-up mid-week.
| Source | Per week |
|---|---|
| Match completions (35 × 6) | 210 |
| Rounds won (~45 × 2) | 90 |
| Matches won (~17 × 4) | 68 |
| First-win bonus (7 × 10) | 70 |
| Level-up (one, non-milestone) | 10 |
| Total | ~450 cubes |
Against prices: blast skin ≈ 1 week, character skin ≈ 2 weeks, arena pack ≈ 3.3 weeks. Sparse per drop, generous over a month, and every lever in the table is a server config key (§7). If live data says the store feels unreachable, the earn column moves before the price column does — raising prices on existing players is a one-star review; raising yields is invisible.
5. Match-end flow¶
Rewards are computed and credited by the match handler at MATCH_END, in the same server write that credits league points (Framework & Flow §17.10). The client never claims, calculates, or writes a reward.
MATCH_END
├─ guard: forfeit? no input? → zero, stop
├─ league points → incr on club leaderboard (§17, Quick Match only)
├─ XP → add yields, evaluate level threshold(s)
│ └─ crossed? → credit level-up Sugar Cubes
├─ Sugar Cubes → credit match yields
├─ first win of the day? → credit bonus, set flag (UTC day key)
└─ single player? → enforce daily cap counter before any of the above
Every credit is idempotent on match_id. The same match result written twice — reconnect, retry, two devices on one account — credits once. The first-win flag is a date key, the single-player cap is a date-keyed counter, and both are server storage.
MATCH_END (opcode 9, Framework & Flow §7.3) already promises "final standings, rewards." This document defines the rewards payload:
"rewards": {
"cubes_earned": 14,
"cubes_total": 1243,
"xp_earned": 30,
"xp_total": 4260,
"level": 11,
"level_progress": 0.03,
"level_ups": [ { "level": 11, "cubes": 10 } ],
"first_win_bonus": true,
"league": { "points": 3, "club_total": 14, "rank_delta": 4 }
}
The league block is present only in Quick Match (Framework & Flow §17.12); the rest is present in every mode that pays.
5.1 Single player is the one untrusted path¶
Single-player matches are simulated on the client (Framework & Flow §8.5), so their reward claims are the only client-originated reward data in the game. Three guards, in order of how much work they do:
- The daily cap of 5 — the absolute ceiling on what abuse can yield, ~50 cubes a day
- Plausibility validation server-side — match duration, round count, and timestamps are checked against the rules of the game before credit; a 40-second best-of-5 is rejected
- Cosmetic-only stakes — the worst case of a successful cheat is cosmetics the cheater already had a free path to. No ladder is touched, no other player is harmed
Offline play queues reward claims locally and syncs on reconnection, inside the same cap. A plane flight's worth of matches pays its five and no more.
6. Client responsibilities¶
The client displays. It never computes and never writes. Currency, XP and level are server-write only (Framework & Flow §11, decision 59).
- Home — one currency counter beside the league chip, tapping through to Store (Framework & Flow §5.2)
- Match Results — the reward beat: cubes earned, XP bar fill with level-up callout, league beat alongside in Quick Match. It animates with the standings and never gates REMATCH — the same law as the league beat (Framework & Flow §17.12), for the same reason
- Store — prices rendered from server config, never from a client table; Sugar Cube prices carry the local-currency equivalent (§1.3); the store copy rule stands — no cosmetic may be described with words that imply advantage
- Leagues — no reward UI. The reserved strip stays reserved (§3.2)
- Offline — cached balances render greyed with the same staleness treatment as the league standing; nothing economy-related blocks Home or Play, ever
7. Server implementation¶
Mirrors §17.10's approach: Nakama primitives, no bespoke stores, everything tunable without a client build.
| Concern | Mechanism |
|---|---|
| Sugar Cube balance | Nakama wallet, server-write only. The wallet ledger gives transaction history and idempotent writes natively |
| XP and level | Storage collection player_progression, key = user ID, server-write only. {xp_total, level}; level is derived from xp_total by the curve, stored to avoid recomputation on read |
| Level-up payouts | Evaluated inside the match-end write; credited to the wallet in the same transaction |
| First-win flag / single-player cap | Date-keyed counters in player_progression, UTC day |
| Purchase grants | Nakama IAP validation hooks (or a separate service, per Framework & Flow §15 question 3); wallet credit idempotent on store transaction ID |
| Config | economy_config storage record: xp_yield, cube_yield, level_curve_coeff, milestone_levels, sp_daily_cap, first_win_bonus, store_prices, iap_bundles. All tunable without a client build |
| Price changes | Prices are read by the client from config at Store entry. A price change is a config write, never an app update |
The league_config precedent stands: tuning the economy must never require a client build (Framework & Flow §17.10). If a number in this document is worth writing down, it is worth putting in economy_config.
8. Deliberate omissions¶
Listed so the absences read as decisions.
| Omitted | Reason |
|---|---|
| Second premium currency | Price opacity and a two-class store, in a game for children. Every item earnable, always — §1 |
| Weekly rollover payouts | The largest faucet the economy would ever have; irreversible if mistuned. Leagues ship as pure status — §3.2 |
| Rank-based weekly prizes | Makes the last hour before rollover a sniping contest — §3.2 |
| Level-gated store items | A day-one buyer is revenue, not a problem — §2.3 |
| Loot boxes / gacha / paid Surprise Cubes | Gambling mechanics in a child-attracting game; also store-review and regulatory risk — §1.3 |
| Daily quests / login calendar | A real retention lever, and version 2 material. Version 1's retention system is the league week; two chore systems at launch compete with each other |
| Battle pass | Needs a content cadence the launch asset plan does not have. Revisit after the second arena pack |
| Ads, rewarded video | None at launch. If ever added, opt-in rewarded only, and the age-rating answer decides first |
| Energy / lives / timers | A hard gate on playing is the opposite of the game's session pitch |
| Trading / gifting between players | A fraud and laundering surface with no gameplay benefit at this scale |
| Prestige / level frames | Post-launch. The curve has no cap to prestige against yet — §2.2 |
9. Failure modes to design against¶
- A reward is credited twice. Every credit is idempotent on
match_id; every purchase on the store transaction ID; every level-up payout is a function of the level threshold crossing, computed inside the XP write. Replays and retries credit once economy_configis malformed or missing. The server refuses to credit against bad config and falls back to the last known-good record, logging loudly. The client renders cached prices with a staleness marker. Nobody's balance ever depends on a parse succeeding- Client and server balances disagree. The server is the only truth. Balances refresh on
POST_MATCH, on Store entry, and on app foreground; a stale client display is cosmetic and self-heals - Single-player claim flooding. The daily cap and plausibility check bound it to ~50 cubes a day of cosmetic-only value — §5.1
- A purchase validates but the credit fails. The ungranted transaction sits in the wallet ledger as pending; restore purchases re-runs validation. No support flow may ever require the player to produce a receipt screenshot — the ledger is the record
- Price change mid-session. The client read a price at Store entry; the server validates the current price at purchase. A mismatch fails the purchase with a refresh, not a silent overcharge
10. Asset asks¶
Small, and all Core non-variant — currency is account state, not arena dressing. The slot manifest is Asset Architecture §6.7, which owns slots; this section is what the economy needs and why.
- A currency icon — the Sugar Cube. The game's third cube, and the only one that lives in menus. Translucent-white crystal, Sunbeam core per the palette's rewards/currency role; legible at 32px in greyscale and distinct from both the Surprise Cube and the Ice Cube, and from the Gold league badge it sits beside on Home. The pair checks are §6.7.2's, enforced by the §8 gate
- A reward burst. The celebration on Match Results and level-up — rising, saturated, the same motion language as
pickup_pop, never the sagging debuff vocabulary. The economy's only celebration, so it carries the whole "something good happened" channel - An XP bar and a level badge. Match Results, Profile, and the Home chip. The numerals are the type system, not new assets — tabular figures per Style Guide §4
Nothing from this group ever renders during a match. Currency, XP and level are menu concepts; the field is for danger (Style Guide §3.4, §11).
The Leagues reward strip remains un-authored per Asset Architecture §6.6.3 — §3.2 of this document confirms that as correct for version 1.
11. Open questions¶
| # | Question | Recommendation |
|---|---|---|
| 1 | All yields, prices, curve coefficient — every number in §§1–4 | Playtest and soft-launch data settle these. The structure is the proposal; the numbers are starting values in economy_config |
| 2 | First-purchase bonus (double cubes on first bundle)? | Standard practice, cheap, and converts the hardest purchase. Recommend yes at marketing's call — it is a config flag, not a system |
| 3 | Founder / soft-launch thank-you grant? | Decide with the soft-launch plan. If granted, a flat cube grant, never an exclusive item — exclusivity at soft launch punishes everyone who finds the game at real launch |
| 4 | Does the daily first-win bonus expire unused or bank? | Expire. A banking bonus is a chore and a hoard; a daily bonus is a reason to play today |