Explodorama — Plan
A turn-based, mobile-first, rogue-like take on the 1985 NES Bomberman: 20 levels (50 until 2026-10-23) and an ending. Built with LittleJS + TypeScript + Vite, tested with Vitest, deployed to GitLab Pages.
Status: M0–M5 done, M6 under way (see §18). This document is the design and the hand-off
for whoever continues the implementation. Read it fully before writing code, especially §1 (the
turn), §15 (notes for the implementer) and §18 (milestones). Reasons for decisions are in
decisions.md.
0. Decisions already made
| # | Question | Decision |
|---|---|---|
| 1 | Turn order | Enemies move before bombs explode. Enemies announce their move one turn ahead (intent arrows), so the player can plan traps. |
| 2 | Run: charge or always available? | Run is an ability the player gains (the NES roller skates, "Speed"). Once gained, always available — no charges. |
| 3 | Lives | 3 lives, plus a modern rogue-like layer: coins buy upgrades. |
| 4 | Undo | None. |
| 5 | Which actions | All actions named in this plan (§3), plus any agreed later. |
| 6 | Name | Explodorama. ("Bomberman" is a Konami trademark — never use it in the UI, title, manifest or repo name.) |
| 7 | Shop purchases | Last for the current run only. Nothing carries over from one run to the next. |
| 8 | Permanent upgrades | ~~Workshop between runs~~ Dropped 2026-10-24 (user's call): permanent upgrades across runs would ruin the game; every run stands alone. See §7. |
| 9 | Death | ~~Keep or Fresh~~ Dropped 2026-10-24 (user's call): no choice and no payout. A death with lives left retries the level as it was, keeping what the run had; nothing is gained by it but what the player learned. See §9. |
| 10 | Curses | Exist, but only appear after the first shop (level 2 onward). See §6. |
| 11 | Reapers / turn limit | Later (post-MVP). The par-turn coin bonus stays. |
| 12 | Run length | 50 levels with an ending, like the NES. A win produces a victory certificate with a QR code for that seed. See §11. Changed 2026-10-23 (user request): 20 levels, on the one 9 × 11 board. |
| 13 | Art | Kenney's New Platformer Pack 1.1 (CC0): sprites for the player, enemies, board, pickups, HUD and buttons. Changed 2026-10-20 at the user's request; it replaces the earlier plan of Icongeek26 Flaticon icons. See §13. |
Still open: see §14.
1. The turn (the core rule — everything else follows from it)
One turn, in this exact order:
- Player acts. The player picks one action and a target; it resolves immediately (move, run, place bomb, kick, …). Picking up whatever is on the destination tile happens here.
- Enemies execute their announced intents. Each enemy takes the first of its planned moves (it shows the next 3, §4). An enemy whose next tile became blocked this turn (e.g. the player put a bomb there) stays put — it does not re-plan mid-turn.
- Bombs tick. Every bomb's timer decreases by 1. Bombs reaching 0 explode. Chain reactions resolve within the same phase (a bomb caught in a blast explodes now, regardless of its timer).
- Damage. Flame kills the player (unless protected) and enemies, destroys soft blocks (revealing anything under them), and destroys pickups lying in the open (see §5, NES rule). Last turn's flame, still burning, also kills the player and enemies in it. An enemy sharing the player's tile kills the player.
- Enemies plan. Every surviving enemy tops its plan up to 3 moves and displays them. Curse and Star/Shield turn counters advance. Input unlocks.
Rules that fall out of this:
- Bomb number = how many more player actions until it explodes. A newly placed bomb shows its fuse value (default 3) and is not ticked on the turn it is placed. A bomb showing 1 explodes at the end of the player's next action.
- Flame lingers one extra turn (changed 2026-10-20 at the user's request; it used to be instantaneous). A blast burns in phase 4 of its turn, then stays burning through the whole next turn: enemies that step into it in that turn's phase 2 die in its phase 4, as does a player who waits in it. The player can't step, teleport or put a bomb into a burning tile. Lingering flame doesn't burn crates, drops, the exit or bombs again (so drops from a kill survive), and it doesn't set off bombs. This is the turn-based version of the NES flames staying on screen for a moment, which is where most early kills come from: with instant flame, a kill needed the enemy on the exact tile at the exact turn.
- Contact is checked twice: after phase 1 (player walked into an enemy — death) and after phase 2 (enemy walked into player — death). Player and enemy swapping tiles counts as contact.
- Bombs block movement for the player and for (most) enemies. Flame stops at the first hard wall or soft block (destroying the soft block); a bomb in the flame's path chain-explodes.
2. Player stats and abilities
| Stat | Start | Notes |
|---|---|---|
| Lives | 3 | The shop can add more. |
| Bomb capacity | 1 | Max own bombs on the board at once. |
| Fire (blast radius) | 1 | Tiles in each of 4 directions. |
| Fuse | 3 | Starting timer of the player's bombs. |
| Abilities | none | From pickups or the shop (§3.2, §5). |
| Charges | none | Temporary changes dropped by enemies, working on pickup (§3.3). |
| Coins | 0 | Spent in the shop (§8). |
| Curse | none | At most one active at a time (§6). |
3. Actions
3.1 Always available
| Action | Rule | Highlight when selected |
|---|---|---|
| Move | 1 tile up, down, left or right (WASD). Target must be free (no wall, soft block, bomb). | Up to 4 reachable tiles. |
| Place bomb | On any adjacent free tile (orthogonal). Limited by bomb capacity. | Valid tiles. Hovering/pressing one shows the faded blast cross and the starting timer. |
| Wait | Do nothing; the world advances one turn. | Nothing (or a subtle pulse on the player). |
Few buttons, icons only, explained when picked (user request, 2026-10-22; built). The bar
has Move, Bomb and Wait. (Until 2026-10-23 a second row held a button per charge held:
Shield, Freeze, Decoy, then also Pierce, Power and Line. Charges now work on pickup, §3.3, so
the bar is only Move, Bomb and Wait.)
(Detonate and Fuse had buttons there too until a follow-up the same day: the Detonator now
works by tapping a bomb, which pulses, and the Fuse ±1 charge is gone.) Nothing but Wait goes
off from its button alone (Shield and Freeze took a second tap, or a Use tap in the strip,
while they were actions). Buttons show icons, no names. Upgrades don't add buttons, they widen Move and
Bomb (sim/picks.ts): Move also kicks (Kick: walking into a bomb), runs (Run), teleports
(Teleport) and steps diagonally (while a Diagonal charge lasts); Bomb also throws (Throw) and detonates
(Detonator; a plain tap on a bomb does too, before a kick). The tile tapped
decides which, in that order of preference — a kick before walking onto a bomb with Bomb Pass,
anything free before spending a charge. ~~Picking an action opens a strip above the bar
saying what it does now~~ Removed 2026-10-23 (user request): first it covered the board's
bottom row, then a band kept free for it kept resizing the board. There is no explanation text
during play: power-ups are explained in the gem's choice, charges on their first pickup, and
"?" then a button explains it (actionInfo in ui/info.ts). Since 2026-10-23 (user request) the Move button's key is M (it said "WASD", untrue with Diagonal), as B is Bomb's; W A S D / arrows still step at once, and with Diagonal Q E Z C step diagonally at once. The old keys stay (R, T, P, G, I, O, L pick that one
action). With an action picked, a direction key uses the tile next to the player, or 2 away
if that one can't be used (a run, a throw).
Info and menu (user request, 2026-10-21; built; changed 2026-10-22): a dialog explains a
tile (ui/info.ts): enemies (their next moves in words), bombs, burning flame, walls and crates,
the exit, the power-up, drops. Since 2026-10-22 (user request) a plain tap no longer shows it:
the round ? button at the top right, beside the menu (key ?; it was beside Wait until
the button rework the same day), turns on "What is that?" for one tap on the board or on an
action button; tapping ? again, picking an action, or Esc turns it off. Without it, a tap
on an enemy, a bomb or burning flame does nothing (never a move into it), a wall or crate only
clears the auto-move flag, and any other tile the player can walk to — including the exit, the
power-up and drops — sets the auto-move destination. Things the player can walk to get a
Walk there button in their dialog. Top-bar items still explain themselves on a plain tap. A menu button (top right; Esc with nothing to
cancel) shows level and seed (no Kenney credit since 2026-10-31), and offers New run behind a confirmation and
an Almanac (2026-10-22, user request; ui/almanac.ts): tabs for power-ups, charges and
enemies, each with its sprite and the same text as "What is that?" (enemies also: the level
they first appear on).
Touch shortcuts (user request, 2026-10-20; built):
- ~~Tap the player = Wait.~~ Removed 2026-10-26 (user request): a stray tap on yourself waited, which could kill. A tap on the player now does nothing; Wait is its button (Space).
- Deadly moves (user request, 2026-10-26; built):
isDeadly(sim/danger.ts) plays an action on a copy withstep()and says whether the player loses a life this turn. Wait turns red when deadly (still pressable); a picked button's deadly tiles are red with a cross (still tappable). The shortcuts never take a deadly move:tapMeaningcalls it'deadly'(the tile flashes red, a short buzz), and the same goes for direction keys with nothing picked, the long press for a bomb, and auto-move. The button then the tile still takes it. One turn ahead only. - Press and hold (450 ms) a free tile next to the player, with no action picked = Place bomb there. The bomb and its blast preview appear when the hold registers; release places it, sliding off cancels.
- Auto-move: tap a reachable tile that isn't adjacent, with no action picked, to set it as a
destination (flag + dotted path, next step highlighted). The Move button turns into Go;
each press takes one step along the shortest walk (
sim/path.ts, over free tiles, ignoring enemies), so it is still one turn per step and fully recorded as ordinary Move actions. Moving anywhere off the path clears the flag ("changed my mind", user request, 2026-10-21): any move, run, diagonal step or teleport that doesn't land on the path. Tapping the flag also takes one step (user request, 2026-10-20). Tapping another far tile moves the flag; so does tapping the exit, the power-up or a drop (since 2026-10-22). Recomputed every turn; cleared on arrival, when unreachable, on Esc, by tapping a wall or crate, or on death / level change. Neither Go nor the flag takes a known-deadly step (onto an enemy, onto an enemy's intent tile, into flame this turn, or anything elseisDeadlyfinds): the next step shows red and Go falls back to picking Move. With no enemies left, auto-move walks by itself (user request, 2026-10-22): one step 1 s after the last action, until the flag is reached — not while an action is picked, "What is that?" is on or a dialog is open. With the Run power-up, a straight 2-tile stretch is one Run (same request), if both tiles pass the safety check; still recorded as an ordinary Run.
3.2 Abilities (permanent once gained — no charges)
| Action | Source | Rule |
|---|---|---|
| Run | Run pickup (called Skates until 2026-10-22; NES "Speed"), or shop | 2 tiles in a straight line. Both tiles must be free — no jumping over anything. Highlight: path tile + landing tile. An enemy on the path tile = death (you ran into it). Part of Move since 2026-10-22. |
| Kick | Kick power-up (was a charge until 2026-10-22) | Push an adjacent bomb by walking into it; it slides until the next tile is not free, keeping its timer. Part of Move. |
| Throw | Throw power-up (was a charge) | A bomb exactly 2 tiles away in a straight line, over a crate, bomb or enemy; since 2026-10-23 (user's choice) not over a pillar or the outer wall (it went over anything). Part of Bomb. |
| Teleport | Teleport power-up, rare (was a charge) | Move to any free tile within Manhattan distance 3 (4 with Run too, since 2026-10-24, user request: Teleport reaches every tile a run does, so Run extends it; and with Diagonal a diagonal step counts as one, a square of reach, as it reached every diagonal step already — user's choice, same day, so no order of picks wastes one), over anything. Part of Move. Since 2026-10-23 (user request) auto-move uses it: each move is a step, a Run or a Teleport, whichever leaves the walk left shortest (ties: Teleport, then Run), a neighbour always a step (autoMoveAction in sim/path.ts). |
| Diagonal | Diagonal power-up (a charge until 2026-10-23, user request; weight 8) | Step one tile diagonally; both orthogonal neighbours may be blocked — a squeeze. Part of Move (G, then Q E Z C). |
| Slide | Slide power-up (2026-10-24, user request; weight 6) | Slide in a straight line until the next tile is a wall, crate or bomb (user's choice); can't stop early. Picks up what it passes; an enemy on the way is crashed into (death, as Run's); flame on the way burns unless flame-safe; the open exit stops it. Highlight: the end tile, 2+ away. Part of Move, after Teleport (a teleport to the same tile wins). L. A tap on the end tile with nothing picked slides too (tapMeaning, before auto-move's walk; user report, same day). |
| RPG | RPG power-up (2026-10-24, user request; weight 6) | Fire a bomb in a straight line: it flies until the next tile is a wall, crate, bomb or enemy (or flame) and lands just before it (user's choice), 2+ tiles away (1 is a plain bomb); counts down as usual, within the bomb limit. Part of Bomb. F. |
| Proximity alert | Proximity power-up (2026-10-24, user request; weight 5) | Passive: the player's bombs placed while held go off the turn an enemy is next to one (one of its 4 neighbours; since 2026-10-25, user request — it was anywhere in its reach, which the balance lab found turned the defensive bomb into a trap), after the enemies move — one that walked up or one already there (user's choice). The danger overlay predicts it from the enemies' next moves. |
| Detonate | Detonator pickup (NES "Detonator"), or shop | Explode one chosen own bomb now. Since 2026-10-22: tap the bomb (all of them pulse while you have it); no button. |
Since 2026-10-23 (user request) every ability and pass is kept through a death. (Until then Detonate and the passes were lost, the NES rule.)
3.3 Charges (dropped by slain enemies; one carried at a time since 2026-10-24)
Redesigned again 2026-10-24 (user request; §19.7, built 2026-10-25): the player carries one
charge (state.charge: its kind and uses left; sim/charges.ts) and uses it on purpose
with the charge button beside Bomb (key V): a charged bomb (charged) for Pierce, Power and
Nuke, a launch (icbm), a Freeze (freeze, at once), a Decoy (decoy, on the player's tile or
one next to them). Bomb always places a plain bomb. A Shield is passive. The same kind picked up
adds to it; another kind asks (chargeOffer, the sellCharge / swapCharge actions, no turn):
keep yours and sell the new one, or take it and sell yours (or, from 2026-10-31, for a Freeze: use it first, fireCharge, then take the new one). A charge sells for half its shop
price, rounded up, for what is left (salePrice). Since 2026-10-25 (user report) one chosen
from a gem or bought in the shop asks the same (it went straight in, selling the one held), and
the charge button is always there: an empty dashed slot with none, greyed out while it can't act,
and explained by "What is that?" (which can now be used on any button, even a greyed one).
Also 2026-10-25 (user request): the Shield is raised with the charge button too (shield
action; carried, it does nothing) and then stays up until a hit lands, through any number of
levels (state.shieldUp, carried from level to level). The Decoy goes up to DECOY_RANGE 3
tiles away in a straight line, every tile on the way free — a wall, crate, bomb, enemy or flame
stops it, as a slide or a kick is stopped — and lasts until the level ends, an enemy reaches
it, or flame burns it. (For one round both lasted 3 turns; the user found that far too short.)
SIM_VERSION 31.
Also 2026-10-25 (user request): Shield and Freeze need a confirmation: the charge button
picks them, lighting the player's own tile (their only target); a tap on yourself, or the button
pressed again, uses them. SIM_VERSION 33.
Changed again 2026-10-25 (user request): Shield and Flame Pass work by themselves while in the
charge slot, no raising: a hit of their kind uses them up (Flame Pass: one of its 3 uses per fire
hit), and the slot is empty again. The shield and flamePass actions and shieldUp/flameHits
are gone; only Freeze still takes the confirming tap. SIM_VERSION 38. boosts and shield are gone from the state; ?charges=n became
?charge=<kind>. This also ends the Nuke trap the balance lab found (§19.1). The rest of this
section is the earlier design, kept as history.
Redesigned 2026-10-23 (user request): charges are no longer actions. Each works the
moment it is picked up (applyCharge in sim/actions.ts), as a temporary change; there are no
charge buttons. What they still give lives in state.boosts (and shield, frozen, decoy)
and shows in the top bar with its count. It is kept from level to level and through a death.
Picking up more of a kind adds to it. The first pickup of each kind in a run still opens a
dialog saying what it does (chargeInfo). Icons are the user's choices (§13).
| Charge | Rule |
|---|---|
| Pierce | The next BOMB_BOOST (3, user's choice) bombs' flame passes through soft blocks. Icon: the Crate Pass icon. |
| Power | The next 3 bombs' flame reaches to the walls (soft blocks still stop it). Icon: the Fire+ icon. |
| Nuke | New. The next 3 bombs also burn every tile within NUKE_RANGE (2, user's choice over 3) steps, Manhattan, through anything; walls stand. Escapable: a fuse of 3 gives 3 steps. Icon: bomb + skull badge. |
| ~~Diagonal~~ | Became a power-up the same day (user request, §3.2). |
| Shield | Armed on pickup; absorbs the next hit (and anything else that turn), then is gone. Kept to the next level. Icon: the Move figure in an empty ring. |
| Freeze | Every enemy freezes for FREEZE_TURNS = 3 turns, the pickup turn included (was 4, as an action). Icon: a padlock. |
| Decoy | A decoy on the pickup tile (user's choice over "next to you"); chasing enemies target it for 3 turns. Flame destroys it. Icon: three pawns. |
| ICBM | 2026-10-24, user request. 3 launches (boosts.icbm, user's choice): Bomb fires a bomb as an RPG's, at any distance, and it goes off that same turn (timer 0); the bomb limit doesn't apply; it works without the RPG power-up. While launches are left, a far tile picked with Bomb is an ICBM (before RPG in the group). Lasts to the next level. I. Icon: bomb + target badge. Since 2026-10-24 (user request) it is seen flying: straight from the player, speeding up, with a smoke trail (0.25 s + 0.05 s a tile; at first a thrown arc, which looked like a throw), the turn's blasts and all they do shown only when it lands (flights, blastAt in effects.ts; the sim is unchanged). |
| ~~Line~~ | Removed 2026-10-23 (not in the user's new list). |
| ~~Fuse +1 / Fuse −1~~ | Removed 2026-10-22 (user request). |
A bomb uses one of each bomb boost held, so Pierce, Power and Nuke combine on one bomb; it wears their icons as badges. Charges can also appear in the shop (§8).
4. Readability (turn-based only feels fair if everything is visible)
- Danger overlay (always on, unless cursed with Fog): tiles that will burn this turn =
strong red; next turn = light red. Includes chain reactions. Computed by a pure function
in
sim/danger.tsfrom the current state. - Enemy plans, 3 moves ahead (changed 2026-10-20 at the user's request; it used to be 1). Every enemy commits to its next 3 moves and shows them as a vertical stack of see-through arrows down the right side of its own tile (the enemy drawn slightly smaller and to the left to make room): the next move at the top, strongest; the 2nd and 3rd below it, fading. Each arrow is a single polygon, so a see-through arrow is evenly tinted (a shaft + head drawn separately showed a darker overlap). It makes exactly those moves unless the board blocks one (then it stays put that turn and plans afresh). Planned moves a new bomb blocks are dropped and re-planned in phase 5. Freeze keeps plans. Reason: bombs take 3 turns, so the player needs to see 3 turns ahead to set traps. (A 2-tile mover will show one arrow per tile.)
- Bomb timers: large number on each bomb; pulses at 1.
- Commit preview: after picking an action and pressing a target, show the outcome (player ghost at destination, faded blast cross, slide path for Kick) before release/confirm.
- Pickups, curses and drops have distinct icons (§13), readable at phone size.
5. Pickups (NES-style, hidden under soft blocks)
The NES original had exactly 8 power-ups, one hidden per level under a soft block (listed from memory — inspiration only, no need to verify):
| NES item | Explodorama equivalent | Kept by "Keep" on death (§9)? |
|---|---|---|
| Bombs | Bomb +1 (capacity) | yes |
| Flames | Fire +1 | yes |
| Speed (roller skates) | Run (called Skates until 2026-10-22) | yes |
| Wallpass | Crate Pass — walk through soft blocks | no — always lost on death (kept since 2026-10-23, see §9) |
| Detonator | Detonator → unlocks Detonate | no |
| Bombpass | Bomb Pass — walk over bombs | no |
| Flamepass | Flame Pass — immune to flame (tune later if too strong); walk through it (with a Star too, since 2026-10-22) | no |
| Mystery | Star — invincible for 8 turns | no (it's temporary anyway) |
Additions:
- Fuse +1 — the player's bombs start at 4. Kept by "Keep".
- Coin bag — +N coins.
- Heart — +1 life (rare).
NES rules kept (they create the classic tension):
- One pickup per level under a soft block (more via shop upgrades), plus the
exit door under another soft block. Since 2026-10-23 (user request) the pickup is a
gem that offers a choice: taking it offers
OFFER_SIZE(5) different power-ups, drawn by the weights below without repeats and leaving out abilities and passes already held, and the player picks one (chooseaction, no turn;ui/offer.ts, a dialog that must be answered). The gem's colour is random and means nothing. The offer comes from the seed and level (stream(seed, 'offer', level)). A death on the pickup turn drops the offer (the retry has the gem back); reaching the exit on the same step still needs the choice first. - Flame destroys an exposed pickup — and spawns extra enemies, as in the NES.
- Flaming the exit door spawns a group of enemies from it. Since 2026-10-23 (user's choice)
they can't be hurt by the flame that let them out, which lingers a turn and used to kill them
at once; new explosions still can (
Enemy.sheltered). The same for a flamed gem. - The exit opens only when all enemies are dead.
- ~~Run is guaranteed on level 1 or 2~~ Dropped 2026-10-23 (user's choice, with gems): Run is in the draw like the rest (weight 10).
- ~~Built (2026-10-22, user request): picking one up opens a dialog saying what it does~~ Replaced 2026-10-23: the gem's choice dialog opens 0.45 s after the pickup instead, once the player has arrived and the gem has risen away.
- Built (2026-10-20): the pickup is under any crate except the exit's. Draw weights (to tune):
Bomb+ 20, Fire+ 20, Fuse+ 6, Coin bag 12 (10 coins), Heart 4, Crate Pass 8, Bomb Pass 8,
Flame Pass 5, Detonator 9, Star 8 (8 turns), and since 2026-10-22 Kick 8, Throw 8, Teleport 3;
since 2026-10-23 Run 10, and they weight the gem's offer (
POWER_UP_WEIGHTSinsim/turn.ts). Sprites: §13 (since 2026-10-22, a bomb or the astronaut with a modifier badge for Bomb+, Fire+, Fuse+, Run, Kick and Throw; gems before). A death keeps stats, every power-up and pass (since 2026-10-23, user request; the planned Keep / Fresh choice was dropped 2026-10-24). A Run collects what is on the tile it passes through.
5b. Exclusive power-ups — a rogue-like direction (idea, user, 2026-10-21; not decided)
2026-10-23: the gem's choice of 1 from 5 (§5) is a first step in this direction — the player chooses instead of taking whatever is there — but nothing is exclusive yet: slots are still open.
The user wants to start thinking about making power-ups exclusive: picking one up can replace another, so the player can't simply collect everything, and is pushed to specialise, experiment and choose — which to keep, which to drop. A sketch to discuss:
- Slots. The player has a few power-up slots (e.g. 3). Taking a power-up with a free slot fills it; with all slots full, a dialog asks which one to give up (or to leave the new one). The dropped power-up stays on that tile, so a swap can be undone while still on the level.
- What takes a slot. Natural candidates: the abilities and passes (Run, Detonator, Wall / Bomb / Flame Pass), maybe later new ones (Kick as a permanent ability, Pierce bombs…). Open: do the stat gems (Bomb+, Fire+, Fuse+) also take slots (e.g. "Bomb+ ×2" in one slot, stacking within it), or stay uncapped as in the NES? Instant ones (Heart, Coin bag, Star) would not take a slot.
- Synergies and trade-offs make choices interesting: e.g. Bomb Pass + Kick, Crate Pass + Pierce, Flame Pass + Line bombs. A power-up could also have a downside (a "cursed" version).
Inconsistencies with the current plan, to resolve before building (CLAUDE.md asks to raise these, not paper over them):
- ~~Keep / Fresh on death (§9)~~ — dropped 2026-10-24.
- The shop (§8) sells abilities and stats; with slots, a purchase may also force a swap.
- ~~The Workshop (§7)~~ — dropped 2026-10-24.
- NES rule: passes and Detonator are lost on death (§5, §9) — with slots, perhaps nothing is lost on death and the slot choice itself is the cost. Settled 2026-10-23 (user request): nothing is lost on death, slots or not.
- ~~Developer mode (§13b) gives everything; it would bypass slots (fine for testing).~~ Resolved 2026-10-23: Developer mode now only uncovers the gem, so it bypasses nothing.
Open questions for the user: how many slots? Do stats take slots? Is a swapped-out power-up gone or left on the tile? Should action charges (one-use, from enemies) stay separate from slots (probably yes)?
6. Curses (from the Super Bomberman skull)
- Never before the first shop — i.e. not on level 1. From level 2 onward a skull can appear: under soft blocks (as an extra item, never replacing the level's pickup) and as a rare enemy drop. Frequency rises gently with level.
- The skull is visible once revealed, so the player can choose to avoid it. Flaming a skull destroys it safely (no enemy spawn — unlike pickups), which gives bombs a second use.
- Walking onto it applies a random curse for 10 turns (HUD shows icon + countdown). One curse at a time; a new skull replaces the current curse.
- Curse list:
| Curse | Effect |
|---|---|
| Short Fuse | Your bombs start at 2. |
| Leaky | Each turn, if capacity allows, a bomb drops on a random free adjacent tile. |
| Butterfingers | Your bombs land on a random free adjacent tile, not the one you chose. |
| Fog | Danger overlay and enemy intent arrows are hidden. |
| Heavy Feet | Run is disabled. |
| Damp | Fire is 1. |
- Cures: wait it out, a Star, or the shop's Cleanse charge.
- (Open, §14: should skulls also give coins — a risk-for-reward choice?)
7. ~~The Workshop~~ — dropped (user's call, 2026-10-24)
The plan had permanent upgrades between runs, Everspace-style: coins left over at the end of a run banked into upgrade tracks (more lives, bombs or fire to start with, Run from the start, …). Dropped: the user can't imagine cross-run permanent upgrades that wouldn't ruin the game. Every run starts the same; what a run gains, it keeps only until it ends. (The tracks and their costs are in git history and decisions.md.)
8. Coins and the in-run shop
- Coin sources: some enemy types always drop coins, others sometimes; occasional coins under soft blocks (not built); Coin bag pickups; since 2026-10-24 a clear bonus for every level; a par bonus for finishing a level within its turn budget.
- Built 2026-10-24 (the user's choices of the options offered): the shop opens every few
levels — after levels 3, 6, 9, 12, 15 and 18 (
SHOP_EVERY3, a choice made here; not after the last) — when the player goes on from the level-cleared banner, before the next level (sim/shop.ts,ui/shop.ts). - It sells power-ups and charges:
SHOP_SIZE4 items, drawn without repeats by the gem's weights (POWER_UP_WEIGHTS), charges at weight 7 each; no power-up already held, and no Coin bag (coins for coins). - Prices by rarity (
PRICES): Bomb +1 / Fire +1 4, Fuse +1 5, most abilities and passes 6, Slide / RPG / Detonator 8, Flame Pass 9, Teleport / Proximity alert 10, Heart 12, Star 6; charges 3–6 (Decoy 3; Pierce, Power, Freeze 4; Shield 5; Nuke, ICBM 6). To tune by playtesting. - Restock: 1 coin, one more each time in the same shop (
rerollPrice); a new draw of 4. -
The coin budget (2026-10-24, user request: make the shop reasonable, "leaning towards cheap"). Before, three levels earned about 2 coins from enemies (2 Blobs a level at 20% for 1.7 coins on average) plus 5 for each level within par: a playtest reached the first shop with 5, about half the cheapest item. Now each cleared level pays
CLEAR_BONUS2 coins (par addsPAR_BONUS5 on top), and enemies drop coins more often (§10), about 1 coin a kill. Expected coins per three-level stretch, collecting about 80% of the coin drops:Shop after level Kills From kills Clear bonus Within par Total 3 6 Blobs ≈ 5 6 + 15 ≈ 11 – 26 6 8, Blobs and Seekers ≈ 7 6 + 15 ≈ 13 – 28 9 10, Blobs and Seekers ≈ 10 6 + 15 ≈ 16 – 31 12 – 18 12 – 15 ≈ 12 – 15 6 + 15 ≈ 18 – 36 With the new prices, the least a player brings to the first shop buys two cheap things (a stat and an ability, or a charge), and par play buys three or four; a Coin bag (10) is a whole extra item. Later shops are a little richer, as the run gets harder. - Buying (
buy) and restocking (reroll) are actions of the run record that take no turn; a bought item works at once. Next level is thecontinueaction, as from the banner. - The draws come from the seed, the level and the restock count, so a seed always offers the same items. - Its dialog is like the gem's offer: a clip of the selected item above the cards, which show their prices; the selected card has its own Buy button (or "Need N more"), and Restock and Next level share one short row (since 2026-10-24, user report: three full-width buttons left the items too little room); it can't be closed otherwise. - Not (yet): the plan's run modifiers (Magnet, Lucky, Map, Coin multiplier) and Cleanse, which waits for curses (§6). - Purchases last for this run only (decision 7).
9. Death
Built 2026-10-24 (user request): the death banner (and the game-over one) says how and where
the player died — level, turn, and the cause (GameState.death): walked into an enemy, an
enemy walked in, a bomb's blast, a chain reaction's second bomb, or last turn's flame — and
replays the player's own last turns (§13c).
A death with lives left restarts the level from its seed: the same layout, the enemies back where they started, and the player keeping everything the run had (stats, power-ups, passes, charges, coins; since 2026-10-23). At game over, Same dungeon runs the same seed again from level 1, with nothing carried over, and New dungeon a new one.
~~Keep or Fresh~~ — dropped 2026-10-24 (user's call). The plan was a choice after each death: keep the run's power, or reset it for a coin payout, balanced so each was picked about half the time. Now there is no choice and no payout: a retry, of the level or of the whole dungeon, is just another try at the same board, its only benefit what the player now knows. (The table and the payout formula are in git history and decisions.md.)
10. Enemies
All move on their own schedule ("speed"), are blocked by bombs unless stated, and kill on contact. Intent shown every turn. Adapted from the NES eight:
| Name (ours) | NES inspiration | Speed | Behaviour | Drops |
|---|---|---|---|---|
| Blob | Balloom | 1 tile/turn | Random wander, prefers to keep its direction | rarely coins |
| Seeker | Oneal | 1 tile/turn | 50% step toward player (BFS), else wander | coins sometimes |
| Drifter | Doll | 1 tile/turn | Bounces straight until blocked, then turns | action charge sometimes |
| Sprinter | Minvo | 2 tiles every other turn | Toward player 50% | action charge |
| Ghost | Kondoria | moves every other turn | Passes through soft blocks; chases | coins |
| Wisp | Ovapi | 1 tile/turn | Passes through soft blocks; wanders | action charge |
| Hunter | Pass | 1 tile/turn | Always BFS shortest path to player | coins + charge |
| Reaper | Pontan | 2 tiles/turn | Passes soft blocks, always chases. Later milestone: spawns only as punishment (turn limit, destroyed exit/pickup) | big coins |
- Built 2026-10-24 (user request: "add more enemies"): Sprinter, Ghost, Wisp and Hunter, as in the table, with the sprites of §13. Choices made while building:
- Speed as rests: a plan still holds one move per turn; a Ghost's or Sprinter's every other
move is a rest (its own tile, drawn as a dot, eyes closed). A Sprinter's move is a dash of 2
tiles in a line (two arrows), or 1 when the second is blocked. A dash through the player's
tile stops there and catches them; a bomb on either tile stops it. (
RESTS,moveTiles.) - Through crates: Ghosts and Wisps may enter soft blocks (
PASSES_CRATES), drawn see-through inside one; bombs and walls still stop them. Flame that burns their crate kills them; a crate in front shelters them, as it stops the flame. - Hunter always chases (unless another enemy takes that tile, as a Seeker's chase).
- When they come: one new type every 3 levels (Blob 1, Seeker 4, Drifter 7, Sprinter 10, Ghost 13, Wisp 16, Hunter 19), so a 20-level run meets them all; it was every 6 levels while there were three. Each enemy's type is picked evenly from those met so far.
- Drops: Sprinter and Wisp 40% coins / 60% charge, Ghost 90% / 25%, Hunter 90% / 60%.
SIM_VERSION28. - Until Reapers exist, "destroyed pickup/exit" spawns a group of the level's tougher types.
- Base drop table: ~30–40% action charge, separate coin roll, rare skull from level 2. Tune.
Built (2026-10-20): per type — Blob 20% coins / 15% charge, Seeker 50% / 25%, Drifter
20% / 50% (coins since 2026-10-24, for the shop, §8: Blob 60%, Seeker 80%, Drifter 50%); a coin drop is worth 1 (60%), 2 (30%) or 5 (10%). Counts: 3 enemies on level 1,
+1 every 3 levels, at most 10. New type every 6 levels: Seeker from 7, Drifter from 13.
Enemies may share a tile (they don't block each other), but since 2026-10-23 (user request)
each plans around where the others will be, so they spread out and only share when forced
(
spreadSteps). Since 2026-10-23 (user's choice): when a turn's kills leave no enemy, their charge drops are only lasting ones (Pierce, Power, Nuke, Shield:LASTING_CHARGES), since a Freeze or Decoy from the last enemy had nothing left to work on. Nor is a Decoy, Freeze or Shield dropped while one already lies on the board or is working (user report, 2026-10-23: a second Decoy beside the first); Pierce, Power and Nuke add up, so they always can be. Enemies spawned from a flamed exit drop nothing, so the exit can't be farmed. - Drops land on the enemy's tile and stay until collected or flamed. Built (2026-10-21, user request): drops never stack — a drop whose tile is taken (by a drop, a bomb, since 2026-10-23 the uncovered exit, or the uncovered power-up) rolls to the nearest free floor tile within 2 steps (BFS over floor), else stays.
- Introduction schedule over 50 levels (like the NES): Blob only at first; one new type
every ~6 levels; later levels mix types. Exact table to be drafted in
design.md.
11. Levels, scrolling, progression, the ending
Since 2026-10-23 (user request): one board, 9 × 11, 20 levels, fitting the screen whole — the former "Small" board (density 25%, enemies 2 + 1 per 4 levels up to 5, par 25 + 8/enemy) — with no scrolling, and crates always crumbling once the last enemy is gone (§13b). The NES-size notes below (31 × 13, 50 levels, the side-scrolling camera) are kept as the history of the design; the board code for them was removed. Built 2026-10-25 (user request, §19.2): boards grow with the level (9 × 11 to 15 × 21), seen through a fixed 9 × 13 view that follows the player, with arrows at its edge; enemies by concentration. This undoes the one-size choice above.
- Grid 31 × 13 like the NES: hard walls around the border, indestructible pillars at every even (x, y), soft blocks placed by seeded RNG at ~30–40% density, start corner (3 tiles in an L) always clear, no enemy within ~4 tiles of the start. (Open, §14: narrower early levels?)
- Generation must guarantee: the exit and the pickup are under soft blocks; every free tile is reachable once soft blocks are gone (assert across many seeds in tests).
- Camera: all 13 rows always fit vertically (portrait phone is the primary target), about 7 columns visible on a 390 × 844 phone (measured), since 13 rows must fill the height. Follows the player horizontally, eased between turns, clamped to the level edges. Landscape/desktop show more columns.
- Par: each level has a turn budget; finishing under it pays a coin bonus. Built: par =
50 + 10 × enemies, bonus 5 coins; the HUD shows
turn / par. (Turn limit with Reapers: later milestone.) - 50 levels, difficulty rising through enemy count, types, speed and soft-block density.
- Game over at 0 lives → run summary (level reached, coins, kills, seed) → the same dungeon again or a new one.
- Ending after level 50: an ending screen, then the victory certificate.
- Mid-run save is required, not optional: a 50-level run won't happen in one sitting on a
phone. The save is just the run record (seed + action log, §12), replayed on
load — or a JSON snapshot of the state plus the log so far. Built (2026-10-20): the record,
in
localStorage, after every action, replayed on load; only for the sameSIM_VERSION, and never for test URLs (?seed,?level,?charges). Finished runs aren't resumed. Shelved (2026-10-24, user's call): keeping a run across a rules change (a state snapshot next to the log, the record noting the version change mid-run) and playing older versions. With no players but the developer, a run lost to a rules change costs nothing; revisit once the game has an audience.
Victory certificate
Since 2026-10-23 (user request): an Immortal run (§13b) can be played to the end but earns no certificate, nor any other achievement. A Developer run earns them all, but each shows a red border (user request, 2026-10-23, "like the red quarters some bars used in jukeboxes"); its victory banner is framed in red already.
Shows: seed, date, turns, deaths, sim version, and a
QR code linking to https://apps.fuzzyworld.net/explodorama/#cert=….
Being honest about what this can prove: the game is fully client-side and open source, so any code-only "signature" can be forged by someone determined. What can be verified is the run itself, because the sim is deterministic:
- The certificate carries a hash of the full run record (seed + loadout + every action).
- "Save replay" downloads the run record as a small file. A verify page replays it in the real sim and confirms it reaches level 50's ending and matches the certificate's hash.
- The full record (~7,500 turns × ~5 bits ≈ 5 KB) is too big for a single QR code, so the QR holds the summary + hash, and the replay file is the proof.
- Bonus: the same replay file lets anyone watch the winning run.
- Replays only work with the same rules, so the record stores
SIM_VERSION; the verify page must say clearly when a replay comes from a different version.
(Open, §14: is this level of proof wanted, or is a simple checksum enough?)
12. Seeds and randomness
Built 2026-10-24 (user request): the New run dialog has an optional seed field (empty:
random; a whole number: that seed; other text: hashed into one), so a dungeon can be shared or
replayed as a real, saved run (a ?seed link is a test board). Game over offers Same
dungeon (same seed and modes, from level 1) and New dungeon (a random seed).
- One run seed (shown on the run summary and certificate; can be entered to replay a seed).
- Separate RNG streams derived from the run seed by purpose and level, e.g.
hash(seed, "levelgen", level),hash(seed, "ai", level),hash(seed, "drops", level),hash(seed, "shop", level),hash(seed, "curses", level). This way an extra bomb doesn't change what the next shop offers, and the same seed always gives the same 50 level layouts. - Never
Math.random()insim/.
12b. High scores, a global board, and the daily dungeon (user request, 2026-10-24)
Built: the local board. Every finished run (game over or the win) goes on a board kept in
this browser (ui/scores.ts, localStorage), the best BOARD_SIZE (25), shown from the menu
and from the end-of-run banner (which also says "New high score: #N!"). Ranked (user's
choice) by a win first, then the level reached, then coins, then fewer turns. Each entry
(ScoreEntry, sim/scores.ts) keeps: date, seed, modes, won, level, coins, enemies killed
(GameState.runKills, new), turns, lives lost, how the run ended (the death: cause, level,
turn), the abilities and passes held and the stats — and the whole run record (seed and
actions) with format and simVersion. Immortal runs don't count; Developer runs count, shown
with a red border (§13b); test URLs (?seed and the like) never record.
Planned: a global board. Not built. It would be the app's first backend — so far there is none, on purpose (§9's note on measuring balance) — so it needs a decision before it is built. The entries are already shaped for it:
- Verification by replay. The sim is pure, deterministic TypeScript, so a server can run the
same code on a submitted record (seed + options + actions) and recompute the level, coins,
kills and turns; it accepts the entry only if they match. Faking a score then means playing it.
Only records with the server's current
SIM_VERSION(or one it still has the code for). - Identity: no accounts at first — a chosen display name and a random install id. Submitting is opt-in, per run or as a setting; nothing leaves the browser otherwise.
- Hosting: GitLab Pages is static, so a small function + storage (a serverless function and a key-value store, or a tiny server). Rate limits per install id; record size capped (compressed).
- Boards: all-time (per rules version), per seed, and the daily one below. Developer runs marked as on the local board; Immortal never submitted.
Planned: the daily dungeon. Not built; to discuss. One shared dungeon a day:
- Seed from the date:
hashSeed('daily', 'YYYY-MM-DD'), the date in UTC, so everyone everywhere plays the same dungeon on the same day (local dates would give the world two or three dungeons at once; the cost is a rollover at a local hour that isn't midnight). - Fixed modes: no Developer or Immortal, so results compare.
- Attempts: open question — only the first finished run of the day counts (Wordle-style, the most tension), or the best of the day (more play). Practice runs could follow the counted one.
- Its own board per day (locally: a history of daily results and a streak; globally: the
day's board). The rules version is part of it: if the rules change during a day, that day's
board splits by
SIM_VERSION. - Sharing: a short result text to paste ("Explodorama daily 2026-10-24: level 12, 84 coins, 37 enemies"), and the seed to try it later.
- Started from the menu (Daily dungeon); the top bar shows a Daily tag.
13. Look and icons
Changed 2026-10-20 at the user's request. The game uses Kenney's New Platformer Pack 1.1 (https://kenney.nl/assets/new-platformer-pack), which the user downloaded. It replaces the earlier plan of black Icongeek26 outline icons from Flaticon over a plain drawn board.
- Licence: CC0, so attribution isn't required, but the user wants Kenney credited
properly: in the README's Attribution section and in the game (a credit line with a link to
kenney.nl in the menu — only there, user request 2026-10-21; removed from the menu 2026-10-31, user request: the README's Attribution section is now the only credit; it used to be on the death /
game-over banner too). The
pack's
License.txtis kept inassets/kenney/. - Source files: only the sprites the game uses (or is about to use, below) are copied, unchanged
and under the pack's own file names, into
assets/kenney/, so each can be found again in the pack. All are 128 × 128: tiles and enemies from the pack'sDoublefolders, characters fromDefault(which are already 128 px). - Loading: no atlas.
src/render/sprites.tslists the board's sprites; each is its own LittleJS texture (engineInit's image list) and is drawn withdrawTile. Separate images cost nothing measurable (with Canvas 2D at first, and with WebGL since 2026-10-24).setTilesPixelated(false), since the art is smooth. The HTML HUD and buttons use the same PNGs as<img>. Vite keeps every sprite a separate file (assetsInlineLimit: 0), so art for things not in the game yet is never downloaded. - Board look: sprites everywhere. Colour still shows meaning, but shape alone must already tell things apart — never colour only.
- HUD: the pack's HUD digits for numbers, hearts (full/empty) for lives.
-
Sounds: the pack has 10
.oggeffects (sfx_bump,coin,disappear,gem,hurt,jump,jump-high,magic,select,throw). Decided (user, 2026-10-20): use them when sound is added (M7), copied intoassets/kenney/like the sprites; ZzFX only for anything the pack has no sound for (e.g. the explosion). Built 2026-10-24 (user request: "add sounds and vibrations (where possible)"):src/ui/feedback.ts. The 10 effects inassets/kenney/sounds/plus the UI Pack'stap-a(every button) andswitch-a(the gem's choice, a restock) inassets/kenney-ui/Sounds/, converted to.mp3(older Safari versions can't decode Ogg Vorbis). ZzFX for the blast, a bomb set down and the ICBM's launch. Each turn's sounds are timed with its animation: a blast and what it kills when the blast shows (fx.blastAt), a pickup at the end of the walk; a chain is one louder boom. Vibration (navigator.vibrate, so Android only; iOS Safari has no web vibration) for blasts, hits, a broken shield, pickups and a cleared level. Menu switches Sound and Vibration (the latter only where the browser has it), stored inlocalStorage(explodorama.feedback). Only the player's own turns make sound: the demo and the clips are silent. No music. -
UI (2026-10-21): dialogs and their buttons use Kenney's UI Pack 2.0 (rectangle depth buttons, green / red / grey; round grey button for close and the menu; play / repeat icons; the Kenney Future font for titles) and the UI Pack: RPG Expansion parchment panel, as CSS 9-slice
border-images. Files inassets/kenney-ui/(pack colour folders kept) andassets/kenney-ui-rpg/, each with its licence. Credited with the platformer pack. - Icons (2026-10-22): Kenney's Game Icons pack (CC0) for plain UI symbols the other packs
lack:
question(black, 2x) on the "What is that?" button, inverted to white while it is on. Files inassets/kenney-icons/with the pack's licence, unchanged, under the pack's names. Credited with the others. - Badged power-ups (2026-10-22, user request): a power-up that improves something you
already have is that thing plus a modifier badge at the top right — a green disc with a
dark rim and a white symbol. Symbols from Game Icons (
White/2x:plus,larger,fastForward,arrowRight,upRight, inassets/kenney-icons/White/) and Kenney's Board Game Icons pack (timer_CW_75,shield, inassets/kenney-board-icons/with its licence; credited); later alsoexport,exclamation,crossfrom Game Icons.tools/compose-icons.sh(ImageMagick) builds them intoassets/composed/, committed, so the build needs nothing extra;sprites.tsloads both folders.
Sprite assignments (✅ = drawn now; the rest are copied and waiting for their feature):
| Thing | Sprite(s) in assets/kenney/ |
|---|---|
| ✅ player | character_green_idle; character_green_hit when dead; mirrored when facing left. Other green frames (walk, jump, duck, climb, front) are there for animation. |
| ✅ outer wall | a bordered stone frame (2026-10-22): terrain_stone_horizontal_middle top and bottom, terrain_stone_vertical_middle left and right, terrain_stone_block_top_left / _top_right / _bottom_left / _bottom_right corners; the four inner corners, which the pack has no piece for, get a small square in the outline colour |
| ✅ pillars | block_blue, block_green, block_red, block_yellow, cycling by level |
| ✅ soft block | block_planks (wooden crate) |
| ✅ bomb | bomb; flashes to bomb_active on its last turn; timer drawn in hud_character_0–9 |
| ✅ flame | fireball (spinning, fading) over an orange tile |
| ✅ HUD | hud_heart / hud_heart_empty (lives), flag_green_a (level), slime_normal_rest (enemies left), hud_coin (coins), bomb, fireball, digits |
| ✅ action buttons | Move icon_move (Board Game Icons character), Bomb bomb, Wait icon_wait (hourglass; character_remove briefly), both recoloured to ink by tools/compose-icons.sh and turned white on dark or green (class mono); until 2026-10-22 the astronaut's walking and crouching frames, which looked like the player. Icons only since 2026-10-22. "What is that?" (top right): Game Icons question |
| ✅ banners | character_green_hit (lost a life), hud_heart_empty (game over), flag_green_a (level cleared); favicon bomb |
| ✅ exit (locked / open) | door_closed_top / door_open_top alone, filling the tile: a short arched doorway (user request, 2026-10-23; until then the two-tile door with door_closed / door_open, squeezed into one tile, which the user found too tall). (lock_yellow spare.) |
| ✅ coins | coin_bronze (1), coin_silver (2), coin_gold (5), bobbing; hud_coin in the HUD |
| ✅ charges | Since 2026-10-23 (user's choices): Pierce powerup_wall_pass, Power powerup_fire_plus, Nuke charge_nuke (bomb + Board Game Icons skull badge), Diagonal charge_diagonal (Move figure + arrow_diagonal_cross badge), Shield charge_shield (Move figure in the empty ring timer_0), Freeze charge_freeze (Game Icons locked), Decoy charge_decoy (pawns); built by tools/compose-icons.sh. On drops, in the top bar, and Pierce / Power / Nuke as badges on their bombs. (Until then: switch_blue, chain, ramp, spikes, weight, bridge_logs, hud_player_helmet_green.) |
| ✅ decoy | character_green_front, faded |
| ✅ auto-move | The Go button keeps Move's walk icon, with a green border (until 2026-10-23 it turned into flag_green_a). On the board, since 2026-10-23 (user request), the whole way is highlighted blue, destination included, with no flag on the destination and no dots. |
| ✅ intent arrows | drawn red lines with a head, not sprites |
| ✅ power-ups | badged (assets/composed/): powerup_bomb_plus Bomb+ (bomb + plus), powerup_fire_plus Fire+ (bomb + larger), powerup_fuse_plus Fuse+ (bomb + timer), powerup_run Run (the Move figure + fast-forward), powerup_kick Kick (bomb + arrow right), powerup_throw Throw (bomb + arrow up-right), powerup_teleport Teleport (Move figure + outbox), powerup_detonator Detonator (bomb + !), powerup_wall_pass Crate Pass (crate + ✕), powerup_bomb_pass Bomb Pass (bomb + shield), powerup_flame_pass Flame Pass (fireball + shield); star Star (and a gold ring), heart Heart, hud_coin Coin bag; in the gem's choice dialog and the almanac. On the board, since 2026-10-23, every level's pickup is a gem, gem_blue / gem_green / gem_red / gem_yellow (random colour), with no glow (the yellow glow was removed on 2026-10-23, user request). (Replaced and removed on 2026-10-22: the gems as fixed power-ups, spring_out, sign_right, bush, ladder_top, torch_off, window.) HUD: icon_fuse fuse (the Fuse +1 badge's timer, ink, white on the bar; the torch torch_on_a until 2026-10-22), small pass icons. |
| ✅ Blob | slime_normal_rest (nowhere to go), _walk_a / _walk_b alternating; mirrored when heading right. _flat spare (the death animation flips _rest over instead, the same for every enemy). |
| ✅ Seeker | fly_rest, fly_a, fly_b, as the Blob |
| ✅ Drifter | saw_rest, saw_a, saw_b; spins while moving |
| Sprinter | mouse_rest, mouse_walk_a, mouse_walk_b |
| Ghost (passes soft blocks) | slime_block_rest, _walk_a, _walk_b, _jump |
| Wisp | slime_fire_rest, _walk_a, _walk_b, _flat |
| Hunter | frog_rest, frog_idle, frog_jump |
The pack has more enemies (bee, ladybug, snail, worm, barnacle, fish, spiked slime, falling block) and more tiles (keys, locks, levers, spikes, torches, …) for later enemies, curses and actions; copy them in when they are used. Icons the pack doesn't have (shop, trophy, settings, sound, and most action charges) are still open.
Animation timing (user's guidance, 2026-10-24)
Most turns animate in a step's time (0.16 s), so play stays quick. A rare or involved turn —
a charge at work, an ICBM, something that changes a lot at once — may take longer and animate
a more complex change of state in stages (the ICBM flies, then its blast plays out), which gives
actions more room. The sim stays instant; only the drawing waits (blastAt-style holds in
effects.ts). Input is still never locked (decisions.md 2026-10-22): a quick next tap starts the
next turn over the end of the last one.
13a. The demo (user request, 2026-10-23; built)
Some players never played the original, so the game opens with a demo: generated scenes,
replayed view-only, a caption under each, until the player taps or presses a key (that input
does nothing else; then the saved run, or a new one). Scenes are made at play time
(sim/demo.ts, pure and seeded): a 9 × 7 room (walls, pillars on every other tile, crates on
about one tile in six, the player and 1–3 enemies apart), then up to 150 random games on it by
the real rules — mostly moves, sometimes bombs, never a move that kills the player — until one
shows the scene's moment; the shortest of those is the scene (a new room if none does).
Kinds (SCENES in sim/demo.ts, each with its moment): since 2026-10-23 (user request) every
feature on the board but the shop and the exit — kill, arrows (an enemy killed on the tile it
stepped onto, the bomb waiting where its arrows led: watch them; user request), escape, chain,
lingering flame, the first three enemy types, drops, the gem (uncovered and taken; its choice isn't
shown), each power-up that shows on the board (Bomb +1, Fire +1, Fuse +1, Run, Kick, Throw,
Teleport, Diagonal, the Detonator, Wall / Bomb / Flame Pass, Star), and each charge (Pierce,
Power, Nuke, Shield, Freeze, Decoy, picked up first). Heart and Coin bag only change numbers,
so they have none. Since 2026-10-23 (user request) also the exit — exit (it opens, the
player walks in and the scene ends there; once open, the random play mostly heads for it) and
doorBlast (flame on the uncovered exit lets 3 enemies out) — and the ways to die, each a
scene whose moment is the death: into a blast (from a safe tile onto a burning one), not far
enough (moved but stayed in reach; Fire 3), blocked in (no step out of danger), chain (caught
only by a bomb that wasn't due), an enemy, and two funny ones: the Detonator next to you, a Run
into an enemy. The random play keeps refusing deadly moves, except the one that is the scene's
death. Living and dying scenes take turns, each queue shuffled (until 2026-10-24: now one fixed
order, see below). 36 kinds, all found on every
seed tried, the slowest under 0.5 s; each caption has the thing's icon. The "Ouch!" and
level-cleared banners are hidden in the demo. The UI (main.ts: startDemo, demoTick) plays a
turn every 0.6 s through the same animation code as play, hides the top bar and buttons, and
fits the room above the caption panel. Every caption has an icon beside it: for a power-up or charge, the same icon as on the board, in
the top bar and the almanac.
Since 2026-10-24 (user request) every scene gets to its point within 3–6 turns and the demo
shows the controls. A random game still runs until it shows the moment, but the scene is only
its last 3–6 turns (as few as do), starting from a calm board — no bomb, no flame, no drop the
room didn't start with, auto-move not under way — and replayed from there to check the moment
still comes (ending in sim/demo.ts; Fuse +1 is now fuse 4, the real +1; Pierce's room has a
row of two crates with Fire 2, and its moment is a crate burnt behind another; Pierce, Power and
Nuke lie next to the player). Every turn comes with its gesture (Gesture: a tap, a long
press, a button and then a tile, a far tap for auto-move, Go), only ones the real controls read
as that action (tapMeaning in sim/picks.ts, shared with tapTile; resolvePick); a turn no
gesture can play isn't used. The UI acts each one out with a finger (Kenney's pointing hand) on
the real action bar, shown in the demo as a tray right under the board, apart from the caption
panel at the bottom (grouped by interactivity: board and buttons, then the words): a ripple where it taps, a ring closing in on a
long press, then the bomb's preview; a picked button lights up its tiles, a far tap shows
auto-move's way. Turns take about 1–2 s. Since 2026-10-24 (user request) the player wears a
green "You" tag just over their head, always (it wasn't obvious which moving figure is you), and each scene's finger
appears just below-right of the player, where the eye should start. Since 2026-10-24 (user
request) the top bar shows in the demo too (scenes start at turn 0 with no coins; rooms have
par 20), and two scenes acted out on a still room follow the control scenes: a top-bar tour
(the finger points at each item, ringed, the caption saying what it is) and "What is that?"
(? then an enemy, through the real inspect and tapTile; the dialog opens, the finger
taps Got it; in the demo the dialog opens over the scene only, not as a modal over the page, so
the caption stays undimmed). The demo plays on a CRT (user request): LittleJS
post-processing (PostProcessPlugin: colour fringes and static) on the board, a CSS layer of scan
lines, a rolling dark bar and sweeping bright lines, vignette, rounded glass and bezel, and every
12–25 s half a second of "vacuum cleaner" interference (tearing and snow; shader values set per frame) over the whole screen part (the HTML top bar and buttons
too), at most 4:3 with black bars; the finger is in front of the glass. Five control scenes come first (step, long-press
bomb, Bomb button, Wait, auto-move with Go), and again each time the living scenes have all been
shown. Captions of Run, Kick, Throw, Teleport, Diagonal and the Detonator say how to do them.
41 kinds; on 40 seeds each, all found, the slowest 0.5 s (made in a worker). Since 2026-10-24
(user request) the random play is sensible outside the scenes about dying: no flame on an
uncovered exit or gem (except in the scene about that), and a bomb only where its blast reaches a
crate, an enemy, a tile an enemy is going to, or another bomb. Since 2026-10-24 (user request) every room has its exit: under a crate,
or in one room in six already uncovered (locked while enemies live). Since 2026-10-24 (user
request) the menu's Watch the demo plays it again; a tap returns to the run as it was. Since 2026-10-24 (user request) the scenes play in one fixed
order, looped (DEMO_ORDER in main.ts: the controls' seven, then the table's living scenes with
a way to die after every four); the panel numbers them (n / 47 since the new power-ups; 43 at first), ◀ ▶ and ← → step through them,
↺ goes back to the first, and the place is kept in localStorage so the demo carries on there
next time, after a reload too (not on a test URL, which starts at the first). Scenes are made in
a Web Worker (ui/scenes.ts), the next while one plays. Ideas: a gem's choice, and showing the
demo again after the player is idle on the menu.
13c. Animated clips in the info dialogs (user request, 2026-10-24; planned)
Every info dialog gets a small looping clip of what it describes, made on the spot like the demo's
scenes (sim/demo.ts): a small room, random play by the real rules until the moment shows, the
last few turns replayed. Each loop holds its moment for 1 s, then starts over with a new
setup (a new seed), for variety. No CRT (that is the demo's joke). The finger only where
it helps: the action buttons and the power-ups you work yourself (Run, Kick, Throw, Teleport,
Diagonal, the Detonator). Reduced motion: a still of the moment wherever one frame says it
(a bomb at 1 with its blast previewed, a Nuke's shape, the exit with its three enemies coming);
for what is about moving (the enemies' rules), a still with the arrows.
What gets a clip (an existing demo scene where there is one; new otherwise):
- Action buttons ("What is that?" on them): Move (
tapMove, or with Run / Teleport / Kick / Diagonal held, theirs), Bomb (holdBomb,bombButton; with Throw or the Detonator, theirs), Wait (wait). - Top-bar items: lives (new: hit, a heart empties, the level starts over the same), level
(
exit), enemies left (new: the last one dies, the count reaches 0, the exit opens), coins (coins), turn and par (new: the count ticking), bombs (bombPlus), fire (firePlus), fuse (fusePlus/kill), Star (star), each pass held (its scene), each charge held (its scene, the charge already held), the modes (Developer: the rainbow gem's offer; Immortal: unharmed by a blast). - Tiles ("What is that?" on the board): a bomb (its countdown and blast; a Pierce / Power / Nuke bomb its shape),
the decoy (
decoy), still burning (lingering), a crate (new: burst, uncovering a drop, the gem or the exit), a pillar or the outer wall (new: a blast stopped by it), the gem and the rainbow gem (gem), the exit locked (doorBlast) and open (exit), coins or a charge on the floor (picked up; a charge also doing its thing). The enemies get no clip, only their picture (user's call, 2026-10-24: the first try, theenemiesscene, showed a busy room with all three kinds; the player learns how they move soon enough) — on the board and in the almanac alike. - Other dialogs: the first-pickup dialog of each charge (its top-bar clip), the gem's offer (a clip per card, perhaps only the card in focus), the almanac's entries (perhaps only one opened), and the "Ouch!" banner (the matching death scene, or a replay of the player's own last turns). Clips should keep their rooms simple: few crates and as few enemies as the moment needs.
How they are drawn (decided 2026-10-24): one LittleJS canvas, drawn twice a frame while a
dialog with a clip is open — first the clip's room, drawn at an offset in world space where the
camera already looks (LittleJS builds the camera's transform only at the start of a frame), copied into a small
<canvas> in the dialog, then cleared and the game as usual — so clips use exactly the board's
drawing and animation code. Fallback, if a copy in the middle of a frame doesn't work: LittleJS's
2D drawing straight into the dialog's canvas. It needs the board's view state (effects, flames,
the player's move and pose, enemy steps, …), now module variables in main.ts, to become an
object, one for the game and one per clip; and scene generation (up to ~0.45 s) to move to a
Web Worker (the sim is pure), so the next loop's setup is ready in time — the demo can use it
too.
Order: 1. a spike proving the two-pass drawing and copy (done 2026-10-24: ui/clip.ts;
"Still burning" plays the lingering scene as a first clip); 2. done 2026-10-24: the board's
animation state is one object (render/animate.ts: Animation, animateTurn), one for the game
and one per clip, and scenes are made in a Web Worker (sim/scene-worker.ts, ui/scenes.ts; on
the page where a worker can't be made), the next loop's while one plays — the demo's too; 3. the
clips that reuse demo scenes (done 2026-10-24: each Info names its scene, clip; the finger in
the clips about the actions and the hand-worked power-ups, FINGER_KINDS in ui/clip.ts — Wait
there is a tap on the player, as no action bar is in a clip; charges are still picked up first); 4. the
new scenes (done 2026-10-24, clip-only, clipOnly in sim/demo.ts: lastEnemy for enemies left,
crate, pillar for walls; lives play the dieEnemy scene; turn and par, which only a number
shows, and the modes have none); 5. the offer's cards,
the almanac and the "Ouch!" replay (done 2026-10-24, the user's choices: one clip above the
offer's cards, playing the card selected — since 2026-10-24 (user request) a tap on a card
selects it and Choose takes it;
almanac entries with a clip are buttons that open it in their own dialog, Back returning; the
banner replays the player's own last 4 turns of the attempt, a demo room's worth of the level
round where they died). Clips can now be several at once, fixed scenes or made each loop.
Also done 2026-10-24: the modes' tag plays rainbowGem (Developer) or immortal, and under
reduced motion every clip is a still of its moment (the scene played to its end, held).
13b. Run options (user request, 2026-10-21; built)
Chosen in the New run dialog (menu, or after game over / victory), remembered in
localStorage for the next new run, fixed for the run, stored in its record (RunRecord.options)
and state (GameState.options), so replays are exact. Shown as tags in the top bar and in the menu.
Since 2026-10-23 (user request) only two options remain, Developer and Immortal: the board
size and Crumbling crates are gone (one 9 × 11 board, §11; crates always crumble). URL test
parameters: ?dev, ?immortal.
| Option | Rules |
|---|---|
| ~~Board: Normal~~ | Removed 2026-10-23. 31 × 13, density 35%, 50 levels, enemies 3 + 1 per 3 levels (max 10), par 50 + 10/enemy. |
| ~~Board: Small~~ | Now the only board (no longer an option). 9 × 11 (fits a phone whole; the camera scales to fit both ways, no scrolling), density 25%, 20 levels, enemies 2 + 1 per 4 levels (max 5), par 25 + 8/enemy. Same enemy-type schedule. Levelgen tops up crates if fewer than 2 are far enough for the exit and power-up. |
| Developer | Since 2026-10-23 (user request), only this: an extra gem (PowerUp.extra; since 2026-10-24 a turning, glittering rainbow gem offering every power-up not held yet and every charge) lies right of the start, every level and after every death, to try the choice at once; the level keeps its own gem, hidden as usual (at first it was that gem, moved). Achievements count, with a red border (the victory banner is framed in red). (Until then: every ability and pass, 5 bombs, blast 5, unlimited charges shown as ∞; killable, split from Immortal by the user.) |
| Immortal | The player can't be hurt at all, so the run can be played to the end — but an Immortal run earns no achievements (the victory certificate, when it exists). |
| ~~Crumbling crates~~ | Always on since 2026-10-23 (no longer an option). When the last enemy dies, every crate gets a countdown of 1–3 turns (seeded, hash(seed, "crumble", level)), shown by shaking and fading; at 0 it is gone, revealing whatever was under it. It is not flame: no punishment, nothing burned. |
URL test parameters ?dev and ?immortal set them for a test board.
14. Open questions
- ~~Workshop banking~~ — no Workshop (dropped 2026-10-24, §7).
- ~~Workshop deposits~~ — no Workshop.
- ~~Fresh start and the level~~ — no Keep / Fresh (dropped 2026-10-24, §9).
- Skulls with coins: should a skull also give coins, making it a risk-for-reward choice rather than only something to avoid? Recommendation: yes, a small amount.
- Level width ramp: start narrower (e.g. 15 columns) and grow to the full 31 by ~level 10, so early levels are short phone sessions? Recommendation: yes.
- Shared / daily seeds: seeds already make runs reproducible. Add a "daily seed" everyone plays the same day? Recommendation: later milestone, cheap to add. 2026-10-24: seeds can be typed in (§12); the daily dungeon is planned in §12b.
- Certificate strength: replay-verifiable (§11, recommended; nearly free since the sim is deterministic) or a simple checksum?
- Board theme: ~~light "paper" board or dark board?~~ Settled by the Kenney art (2026-10-20): a pale sky-blue board under dark bars, coloured as the sprites are.
- Exclusive power-ups (§5b): slots instead of collecting everything? How many, and what takes one? Needs answers before the shop is built, since it depends on it.
- Enemies sharing a tile: they do now, as in the NES original (as far as remembered). Should they block each other instead?
15. Notes for the implementer
- Read the user's
CLAUDE.mdrules. In particular: never commit or push until the user explicitly confirms they reviewed screenshots and ran the app. Docs must be updated as part of each feature (the README lists every action, pickup, curse and enemy). Run the tests often. - Verify the LittleJS API against the installed
dist/littlejs.d.ts, not memory. Already confirmed in the pinned 1.25.0:engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources, rootElement): Promise<void>,cameraPos,cameraScale,drawTile(… color, angle, mirror, additiveColor …),drawTextScreen,setTouchInputEnable,drawRect,drawCircle,drawText,screenToWorld,setCanvasPixelRatio,setGLEnable. Still check:tile(),TileLayer,zzfx. - The game uses LittleJS's WebGL renderer (Canvas 2D from 2026-10-19 to 2026-10-24), and handles taps with
pointer events on the canvas — see
decisions.mdfor why. - LittleJS world space is y-up; the sim grid is row 0 = top. Convert in exactly one place
(
render/board.ts), e.g.worldY = (rows - 1) - row. - Fit the board:
cameraScale= canvas height available between HUD and action bar ÷ 13 rows; recompute on resize/orientation change. - Keep LittleJS out of
sim/. If a test needs to import LittleJS, the boundary is wrong. - Record the run from day one. Every input that changes state — actions, shop purchases,
rerolls — goes through
sim/and is appended to the run record. That one log gives mid-run save, replays and certificates. - Test priorities (Vitest): turn phase order; bomb timer semantics (not ticked on placement
turn); chain reactions; flame stops at walls and destroys the first soft block; Pierce/Power;
Run needs both tiles free; Kick slide stop; enemy blocked by a bomb placed this turn stays put;
contact checks including swaps; levelgen invariants across many seeds; danger overlay matches
what
step()actually does (predict → step → compare); replay determinism (record a random run, replay it, identical final state and hash); curses never before the first shop. - Screenshots: Playwright at a phone viewport (e.g. 390×844) and desktop, with URL params
like
?seed=123&level=1for reproducible scenes. The user reviews them before any commit. - Start simple: labelled shapes until icons arrive; no animation polish until M7. (The player's animation — idle bob and glance, walking slide, crouch, jump, hit — came early, at the user's request, 2026-10-21, and enemies got the same hop the same day. Everything else on the board got its animation on 2026-10-22, also at the user's request: decisions.md.)
16. Architecture
Golden rule: all game rules are pure TypeScript with zero LittleJS imports, deterministic given a seed, and fully unit-testable.
src/
sim/ pure, deterministic, no DOM, no LittleJS
types.ts Vec, Dir, Tile, GameState, Event, ActionId …
rng.ts seeded PRNG + per-purpose streams (§12)
state.ts create/clone state, grid helpers
levelgen.ts seeded level generation (§11)
actions.ts registry: { id, isAvailable(state), validTargets(state), apply(state, target) }
picks.ts what a tap means with Move or Bomb picked (they stand for groups of actions)
turn.ts step(state, action, target) → { state, events[] } — the 5 phases of §1
explosions.ts blast shapes, chain reactions
enemies.ts intent planning per enemy type
danger.ts predicted flame tiles this turn / next turn
path.ts auto-move's way, safe steps, `playerPath` (shared by auto-move, the route preview and the autopilot)
autopilot.ts the autopilot (Q73): targets, the cheapest order, the next turn; waits for crates and bombs
testScenes.ts the Developer-mode test scenes (a board and a state for each edge case)
curses.ts
shop.ts offers, prices, purchase
record.ts run record, replay, hash, SIM_VERSION
render/ LittleJS
board.ts sprites, highlights, danger overlay, intents
sprites.ts which Kenney sprites the board loads (§13)
effects.ts turns a turn's events into short animations (appear, slide, swell,
go away); no input lock — see decisions.md 2026-10-22
camera.ts the view: follows the player, drag/look pan, `revealTile` keeps the controller's target on screen
reveal.ts controller target: how far to move the view to keep a tile on screen (pure)
ui/ DOM: action bar, HUD, shop, death choice, certificate, About
padMap.ts controller: gamepad snapshot to intents (pure; decisions.md 2026-11-01)
padHold.ts controller: hold a face button until a ring fills, act when the ring fills (pure; decisions.md 2026-11-02)
holdRing.ts controller: the ring element moved over the held button's picture
padPlaces.ts controller demo: where each part sits on the controller picture, per family (pure)
padLayouts.ts controller demo: the measured positions (GENERATED by tools/make-pad-layouts.py; do not edit)
demoInput.ts the demo's input button: touch, mouse, keyboard or a pad family, and what the demo is shown for (pure)
spatial.ts controller: which element to focus next in a direction (pure)
gamepad.ts controller: reads the pad each frame, routes intents to main.ts (stage 2)
padTarget.ts controller: the first target, and jumping between targets (pure)
padGlyph.ts controller: which button picture goes on each action button (pure)
targetLabel.ts controller: the one-line label of what confirming does (pure)
padTiles.ts controller: actionable tiles, what each button does on a tile, cursor moves (pure)
routePreview.ts auto-move's way for a far tile only looked at (cursor on a faded tile, mouse hover): same landing tiles and red flag as the real route (pure)
keyCursor.ts keyboard cursor (Q65): who has the board (pad / keys / none), key roles, which button a key means, Enter's default, Esc's step, whether a focused control takes Enter/Space (pure)
padDialogMap.ts controller in dialogs: footer buttons to face buttons, which action a face button does (pure)
padDialog.ts controller in dialogs and the banner: DOM glue (selector, pictures, tabs, scroll; stage 3)
padOverlay.ts controller demo (Q58): the virtual controller: a line-drawing picture (assets/controller-art/) with Kenney glyphs on it (DOM, for the demo and clips)
padDemo.ts controller demo: a turn as pad presses, cursor path, pad scene order and still-scene captions (pure)
keyDemo.ts keyboard demo (Q65 B): a turn as the fewest key presses, replayed through the game's rules; key captions, scene order (pure)
keyOverlay.ts keyboard demo: the key overlay, Kenney's keyboard glyphs on a CSS grid (DOM)
audio.ts Kenney pack sound effects (§13); ZzFX for any it lacks
persist.ts localStorage save/load, export/import
main.ts
assets/kenney/ Kenney New Platformer Pack sprites + License.txt (§13)
static/ Vite publicDir (NOT public/ — see §17)
test/ Vitest
tools/ compose-icons.sh (composed sprites and the app icons), screenshots.mjs (Playwright),
make-pad-layouts.py (the controller pictures and ui/padLayouts.ts), audit-art.mjs (which art is byte-identical to Kenney's)
step()returns events (moved,bombPlaced,bombTicked,exploded,blockDestroyed,enemyDied,dropSpawned,pickedUp,cursed,playerDied, …). The renderer only plays events back.step()does not mutate its input (clone, then mutate the clone) — makes previews and the danger overlay safe.- Adding an action = one registry entry + tests + an icon.
17. Tooling and deploy (copy from ~/code/nordic-skating)
package.jsonscripts:dev=vite --host,build=tsc --noEmit && vite build,test=vitest run,screenshots,icons.- Dev deps as in nordic-skating:
typescript,vite,vitest,playwright,vite-plugin-qrcode(scan to open on a phone),@types/node.vite-plugin-pwaarrives with the icons in M7. - Dependency:
littlejsengine, pinned to exactly 1.25.0 (the newest release at least two weeks old on 2026-10-19). Upgrade deliberately. Upgraded 2026-10-24 to 1.28.0 (the latest, 4 days old: the user's call, for the speed on Android Chrome; decisions.md: "Android Chrome at half a frame a second"). - A QR library for the certificate (e.g.
qrcode, which~/code/prettyalready uses). tsconfig.json: copy nordic-skating's (strict,noUncheckedIndexedAccess, …).vite.config.ts:base: process.env.BASE || './',publicDir: 'static', QR plugin, PWA (display fullscreen, icons, offline cache incl. the sprites). Read nordic-skating's file — its comments explain each choice. Built 2026-10-23 (user request) without a PWA plugin:static/manifest.webmanifest(display fullscreen), icons instatic/icons/fromtools/compose-icons.sh, andstatic/sw.js, a network-first service worker that keeps a copy of everything loaded (so the game starts offline) and registers only in the build..gitlab-ci.yml: copy nordic-skating's (test stage, thenpageswithmv dist publicand the "fail loudly" checks; add a check that the sprites were built). Project atcode.fuzzyworld.net/explodorama, served athttps://apps.fuzzyworld.net/explodorama/.- Docs to create and keep current:
README.md(how to play; every action, pickup, curse, enemy; Attribution), thisPLAN.md,decisions.md(each decision + why — seed it from §0),design.md(balance tables: enemy schedule, prices, drop rates, payout formula).
18. Milestones
- M0 – Scaffold: ✅ npm, Vite, TS, LittleJS, Vitest,
git init, CI file, README with Attribution section. ⏳ GitLab remote, first commit and first Pages deploy — waiting on the user. - M1 – Sim core: ✅ types, RNG streams, grid, Move, Wait,
step()with all 5 phases (enemy phases are marked placeholders), run record + replay, tests. - M2 – Bombs: ✅ adjacent placement, timers, blast, chains, soft blocks, death, lives.
- M3 – Mobile UI: ✅ action bar, two-tap targeting, highlights, danger overlay, commit preview (press-and-hold / hover), keyboard controls; Kenney sprites (2026-10-20). ⏳ No animation of moves yet (instant), only a flame flash.
- M4 – World: ✅ levelgen (31 × 13), side-scrolling camera, exit door under a soft block (opens when all enemies are dead; flaming it spawns 3 enemies), par + coin bonus (2026-10-20).
- M5 – Enemies: ✅ Blob, Seeker, Drifter with intent arrows, both contact checks, coin and charge drops, coins, charge inventory (buttons with counts), and all 11 charge actions (2026-10-20). Touch shortcuts from the user's playtest: tap self = Wait, hold = Bomb, tap far = auto-move (§3.1).
- M6 – Run loop: ✅ NES pickups (all 11, one hidden per level, Run on level 1 or 2; since 2026-10-23 a gem offering 5 to choose from, no Run guarantee), Run and Detonate, the passes and Star, 50-level progression and ending, mid-run save (2026-10-20). ✅ the shop (2026-10-24, every 3rd level, §8); ⏳ curses from level 2. (The Keep/Fresh death choice was dropped 2026-10-24.)
- M7 – Look & feel: ✅ animations (2026-10-22: everything that appears, moves, changes or goes away on the board, the top bar's numbers, the banner, and dialogs opening and closing); ✅ installable with offline play (2026-10-23); ✅ the demo before the game (2026-10-23, §13a); ✅ sounds and vibration (2026-10-24, §13), ⏳ remaining enemies and actions, screenshots tool.
- M8 – Records: save export/import, victory certificate + QR, replay file and verify page. (The Workshop was here until 2026-10-24: dropped, §7.)
- Later: Reapers + turn limit, daily seed, final art.
Backlog
- Released and next copies (user request, 2026-10-31; built, not yet deployed): see decisions.md and
README "Two copies". To go live: commit, tag and push fuzzyworld-tools with
--as, update explodorama'sfuzzyworld-toolsdependency, thennpm run deploy -- --as explodorama-next. - Art provenance audit (user request, 2026-10-31): check every file under
assets/(and any art drawn in code, such as the oil highlight's ellipses intools/compose-icons.sh) is either Kenney's own or produced by a script in the repo from Kenney's files; list everything made by hand or composed by Claude, with how, so the user can keep, redo or replace it. Also confirm the menu and README credits cover every pack used. Done 2026-11-01: docs/art-provenance.md andtools/audit-art.mjs; the keep/redo/replace decisions are backlog (Q39). - A worn dungeon (user request, 2026-10-25; built, then switched off the same day): the user:
the splats looked too alike, and not the look wanted —
render/decor.tsstays, off (ENABLED), until a new approach is chosen. The first idea: use the Splat Pack (approved) to make each level look dilapidated, old, tarnished, wet or dirty — stains, puddles, soot, moss, rust — in a random mix and amount drawn from the seed and level, so a level always looks the same and levels differ. Decoration only: drawn on the floor (and walls) under everything, never in the sim, never mistaken for a hazard (19.3's oil and acid spills must look clearly different: brighter, outlined, or animated). Ideas: a "theme" per level (damp: blue-grey puddles and green moss; scorched: soot; rusty: orange stains on pillars), denser in corners and along walls, lighter on paths; a few splats shared with the demo's rooms. - Built 2026-10-25 (user request): TNT, a power-up worth two Fire +1s (blast +2; weight 6,
price 7), and limits on the stats: bombs, blast and fuse each at most 5 (
STAT_MAX). A stat power-up without room for all of its step isn't offered (heldAlready); its card shows the limit ("Blast 2 → 4 (max 5)"). Teleport's reach gets its own limit when it becomes a charge (next task). - ~~Next: Flame Pass and Teleport become charges.~~ Built 2026-10-25. Flame Pass: raised with
the charge button (a tap on yourself), it takes the next 3 fire hits (
flameHits, an orange ring, kept from level to level). Teleport: 3 jumps from the charge button (key P too); no longer in the Move group, a tap's meaning, or auto-move, so nothing spends a jump but the player. Their power-up entries are gone (powers.teleport,powers.flamePass). Shop: 7 each. Was: Flame Pass and Teleport become charges. The lab measured both turning a weak player into a near-certain winner (§19.1). Decided by the user: - Flame Pass → a charge counted in flame hits, not turns: it saves the player from 3 fire hits (a blast, a chain, lingering flame), one used up each time flame would have hurt them, then runs out. ~~Raised with the charge button first~~ — since later that day (user) it works while carried, its 3 hits being the charge's uses.
- Teleport → a charge of 3 jumps, as ICBM has 3 launches, with its reach as now (Run adds a step; Diagonal makes it a square).
- Crate Pass and Bomb Pass stay power-ups for now: the user wants to playtest them more (the lab finds them strong but in line with Run or Kick).
- Then run the lab's
powersexperiment again on the same seeds, and the 3s are first guesses. - Decided (user, 2026-10-25): on a death, the carried charge drops where the player fell, with
what is left of it, as a drop the retry can pick up again (option 1 below). Built 2026-10-25
(
Carry.fallen; a charge drop can carryleft; the nearest open floor if the player fell in a crate). Was: Lose the carried charge on a death? (user idea, 2026-10-25.) Today the charge carried goes through a death (§3.3). Dropping it would make a death cost more than the life and give a reason to use a charge rather than hoard it; but it also hits hardest the player who is already struggling, and the retry would start without the tool that might have saved them. A middle way: the charge drops on the tile where the player died, to be picked up again on the retry (the level restarts the same, so it is close by) — or only charges picked up during this attempt are lost, as the gem's choice already is. The balance lab can measure each. - ~~UI changes, queued (user, 2026-10-25)~~ Built 2026-10-25: the demo's title is its own
bar above the TV (the top bar and the TV move down under it;
--demo-title), and the menu has Resume and New side by side over a row of four small buttons (className: 'menu'). 1. The demo's title on top: "EXPLODORAMA!" moves to the very top, above the board, so the demo reads title, then the TV, then the caption. Once the game starts, both the title and the caption go. 2. A less packed menu: Resume and New (was "New run") smaller and side by side; the other buttons smaller, as icons and/or single words — Almanac, Scores, Demo, Bug. - ~~Bug reports from players~~ Built 2026-10-24 (user request): GitLab Service Desk is on
(the user turned it on; an email to its address becomes a confidential issue, no GitLab account
needed, replies go back by email), and the menu's Report a bug opens an email to it, filled
in with the version and build commit, the device, the seed, level, turn, lives and modes, the
last 5 errors the game caught (
recentErrorsin devlog.ts), and the run record. A record too long for an email link (over ~1,900 characters) stays out of the email and goes on the clipboard instead, and the email asks to paste it.
19. Proposed features (user, 2026-10-24; to discuss, then prioritise)
Five ideas from the user, worked out into proposals, then settled with the user (decisions marked in each part). Agreed 2026-10-24; order: the balance lab (19.1) first. Nothing here is built yet.
19.1 A balance lab: measuring what an addition does to the game
The question: some power-ups feel overpowered (Flame Pass, Crate Pass, Bomb Pass), but by how much? And would they be fairer as charges? Today that is judged by feel.
Why not search the whole game. The sim is pure and deterministic (step()), so any game can be
played out in Node at full speed, with no browser. But a turn has about 5–20 possible actions
(moves, a bomb on each neighbour, the abilities' targets), and a level takes 40–100 turns: the game
tree has more than 10^50 leaves. Plain DFS or BFS over whole games is out of reach. Search works
well locally, over the next few turns, and that is how it is used below.
Proposal: bots play thousands of games, and we compare the numbers.
- Bot players (
tools/balance/), each a function from a state to an action, using the sim's own helpers (predictDanger,playerPath,safeStep): 1. Careful: never steps into danger; walks to a crate or an enemy's path, bombs it, walks to the nearest safe tile; otherwise waits. Cheap, predictable — a cautious beginner. 2. Searcher: a depth-limited search over the next 3–5 turns (beam search: keep the best 50 states at each depth), scoring states by kills, crates burned, the exit's distance, and safety (how many safe tiles are reachable). This is where BFS / DFS fits: a few turns ahead, on the real rules, pruned by the danger overlay. A decent player. 3. Reckless (later): bombs often, escapes late — finds where a power-up rescues a bad player.
Bots are not humans: a bot gains differently from a power-up than a person does. So the numbers
are for comparing (this power-up against that one, before against after), not as truths.
- Experiments, paired: the same N seeds (say 500) played twice, once as the baseline and once
with the change — a power-up held from level 1, a charge instead of the power-up, a different
drop rate or price. The same seeds on both sides take out the luck of the boards, so a few
hundred games show even small effects. Each experiment runs on all CPU cores (worker threads).
- What is measured, per level and over the run:
- Survival: levels cleared before game over; lives lost per level, by cause (blast, chain,
lingering flame, enemy).
- Pace: turns per level, the share cleared within par, coins earned.
- Pressure: turns spent standing in danger, close calls (out of a blast's reach on the turn
it goes off), turns with only one safe move.
- Use: how often the power-up or charge was actually used, and how often it saved a life
(the same seed and turn without it would have died: a counterfactual replay).
- Structural measures, no bot needed: on many random boards, from the player's tile, count what
a power-up changes outright. Examples: the share of bomb placements with an escape route
(Flame Pass makes it 100%, which shows why it feels free); tiles reachable in 3 turns (Crate Pass,
Bomb Pass, Run, Teleport); blast reach (Fire +1, Power). Deterministic and quick, a good first
report before the bots exist.
- The report: a table per experiment — each power-up's effect on levels survived and lives lost,
with a confidence interval, sorted strongest first — published as an HTML page (an artifact), and
kept in docs/balance/ so a change can be checked against the last run.
- Power-up or charge: the rules get variant switches the lab can turn on (for example
"Flame Pass as a charge of 5 turns", "Crate Pass as 3 uses"), so the same experiment compares
the versions before the game commits to one.
Decided (user, 2026-10-24): the full version, with bots (the structural measures come along as its first report). Still open: which power-ups to measure first — the passes, given 19.5.
Built (2026-10-25): tools/balance/, run with npm run balance -- <command> (README, "The
balance lab"). Bots at three skill levels (careful: survives what it can see ahead, wins about
80% of runs; average and sloppy: look 2 and 1 turns ahead and slip at times, clearing about 5
and 3 levels). Experiments powers (each power-up held from level 1) and structure (escapable
bombs, reach in 3 turns, safe actions, on 1,781 real positions). Reports in docs/balance/.
The bots don't shop, and take the first thing a gem offers.
First findings (2026-10-25, rules version 28):
- Teleport and Flame Pass are in a class of their own. For the average bot each turns 5 levels into 20 (wins 2% → 99%); for the careful bot both are the top two. Teleport triples the tiles you can reach in 3 turns (10 → 39) and lets you escape 19% more bombs; Flame Pass and a Star make 23–25% more bombs escapable. Diagonal comes next (+8 levels for the average bot).
- Some power-ups hurt a weaker player: Fire +1 (−2.6 levels for the average bot: bigger blasts catch you), Fuse +1 (−1.7, and +24 turns a level), Slide (−1.1), Bomb Pass (−0.7; +6% of turns spent in danger).
- Proximity alert costs the careful bot 3 levels (wins 82% → 40%). Its bombs go off the moment an enemy is in reach, so the usual defence — a bomb dropped between you and an enemy coming down a corridor — blows up next to you instead. Enemies then catch the player in dead ends.
- The Nuke trap: 6% of careful runs stall because Nuke charges are forced onto every bomb, and in the closed start corner every Nuke kills the player — the level can't be finished. Fixed by 19.7's deliberate use.
- Within par: the careful bot clears 20–70% of levels within par; level 1 is the hardest (20%).
After one charge at a time and the Proximity change (2026-10-25, rules version 29), same seeds:
- The Nuke trap is gone: careful runs that stall fell from 6% to 1%; the careful bot clears 18.8 levels (was 18.5) and wins 85% (was 82%), in fewer turns a level (53, was 56). The average bot is unchanged (5.4 levels).
- Proximity alert now costs the careful bot 0.7 levels (was 3.2) and wins 11% fewer runs (was 42% fewer); for the average bot it still helps a little (+0.7).
- Teleport and Flame Pass are still far ahead (+14.5 and +14.3 levels for the average bot): untouched by these changes, and the next thing to decide (19.5: the passes leave the power-ups or become charges).
After Flame Pass and Teleport became charges, and the boards were fitted (2026-10-25, rules version 35), 150 seeds, now also each charge carried from level 1:
- Teleport and Flame Pass are tamed: as charges, +0.2 and +0.3 levels for the average bot (they were +14.5 and +14.3 as power-ups) and about 0 for the careful one.
- Diagonal is now the strongest thing for the average bot: +8 levels (wins 0% → 10%), and the best for the careful bot too (+0.8). Also boosted by auto-move cutting corners with it. A candidate for the next look.
- Kick (+2.6) comes next for the average bot; everything else is within about ±1.3 levels.
- Bigger blasts still hurt the weaker player: TNT −2.3, Fire +1 −1.8, Fuse +1 −1.3.
- No charge is far stronger or weaker than the rest (−0.7 to +0.4 for the average bot). The bots use charges crudely (whenever their scoring likes it), so these say little about a person.
- The careful bot: 91% of runs won (82% under rules 28), about 84 turns a level, stalls 3%.
Diagonal without squeezing (2026-10-25, user's question; npm run balance -- diagonal, 300
seeds, a lab-only rule switch options.lab.noSqueeze): a diagonal step then needs one of the
two tiles beside it open.
| Average bot | Careful bot | |
|---|---|---|
| Diagonal as it is | +7.7 levels (wins +11%) | +1.0 levels (wins +8%) |
| Diagonal, no squeeze | +3.4 levels (wins +1%) | +0.3 (not significant), but 9% of runs stall |
Most of Diagonal's strength is the squeeze: without it, it is worth about what Kick is (+2.6 for
the average bot). The careful bot's stalls rise (4% → 9%), probably its own pathing with the rule
rather than the game; to look at. Decided (user, 2026-10-25): no squeezing is the rule (SIM_VERSION
36); the lab's options.lab.squeeze brings the old rule back to compare.
19.2 Bigger boards for later levels
Superseded in part (2026-11-01): the fixed 9 × 13 view is to be replaced by showing the whole board when the screen has room (decisions.md, "A screen shows the whole board when it fits"). Not built yet.
Today: every level is 9 × 11, the whole board on screen at once (a choice made 2026-10-23). It suits the first levels; later ones feel small. This changes that choice.
First proposal (superseded by the decisions below): boards grow with the level, mostly taller, since a phone is tall:
| Levels | Board | On a phone (390 × 844) |
|---|---|---|
| 1–3 | 9 × 11 (as now) | whole board |
| 4–6 | 9 × 13 | whole board, tiles a little smaller |
| 7–10 | 11 × 15 | scrolls up and down |
| 11–15 | 11 × 19 | scrolls |
| 16–20 | 13 × 21 | scrolls both ways a little |
- Tile size: a tile must stay big enough to tap (about 36 CSS px at least). Below that the board scrolls instead of shrinking: the camera follows the player, eased, kept inside the board (the NES-style camera, removed 2026-10-23, comes back in a vertical form). A desktop shows more.
- Off-screen enemies: a small arrow at the screen's edge for each enemy out of view, and their count in the top bar as now, so nothing surprises the player from off screen.
- Everything scales with the area: enemies (about one per 20 open tiles), crates, par, and the number of new objects from 19.3. Auto-move matters more on a big board.
- Generation keeps its guarantees (every tile reachable once crates are gone, the exit and the gem under crates, nothing near the start), checked by the existing many-seed tests.
Decided (user, 2026-10-24), replacing the table above:
- A fixed view, the same everywhere. The game shows a window of the board with a set number of tiles — proposed 9 × 13 (a portrait shape, about the phone's) — on a phone and on a desktop alike, with bars beside it on a wide screen (as the demo's TV has). 9 × 13 agreed (user, 2026-10-24). Everyone sees the same amount of board, so no screen gives an advantage, and a level plays the same everywhere.
- Boards bigger in both directions, growing past the view; the view follows the player, eased and kept inside the board.
- Indicators at the view's edge for what is out of sight: each enemy (an arrow in its colour, nearer ones bolder), and the exit and the gem once uncovered. Lit bombs and fires near the edge too, since they can kill.
- Size by "concentration" (the user's idea, worked out below), instead of a size table.
Built 2026-10-25 (boardSize, concentration, ENEMY_THREAT in sim/levelgen.ts;
render/camera.ts; drawIndicators in render/board.ts):
- Sizes: 9 × 11 (levels 1–3), 11 × 13 (4–6), 11 × 15 (7–9), 13 × 17 (10–12), 13 × 19 (13–15), 15 × 21 (16 on). Odd, so pillars meet the walls evenly.
- The view: 9 × 13 tiles on every screen;
#view-maskbars cover the rest (and taps there do nothing). The demo's small rooms are still seen whole. - Following: the view centres on the player, eased, kept inside the board; a board narrower or shorter than the view is centred that way.
- Indicators: for each enemy, bomb, and the uncovered exit and gem out of view, a small picture just inside the view's edge on the way to it, with an arrow on.
- Enemies: added (random types of those met so far) until their threat reaches the level's concentration × open tiles, at least 2. Concentration rises linearly from 1/26 on level 1 (so its 51 open tiles get 2 Blobs, as before) to 1/11 on level 20, then by 0.002 a level toward 1/8. Threat weights (first guesses, to fit with the lab): Blob 1, Seeker 1.4, Drifter 1.1, Sprinter 1.6, Ghost 1.7, Wisp 1.3, Hunter 2. Level 20 gets about 12 enemies on 193 open tiles.
- Par = 10 + 0.3 × open tiles + 8 × enemies (41 on level 1, as before).
- Fitted with the lab (2026-10-25): with boards up to 15 × 21 the careful bot took 100–145 turns
on late levels; the sizes are now 9 × 11 (1–3), 11 × 13 (4–7), 11 × 15 (8–11), 13 × 15 (12–15),
13 × 17 (16 on), which take it about 75–95 turns throughout. Concentration rises to 1/9 by level
20 (not 1/11): sparser late levels left the bot hunting the last enemy for a long time (its stalls
went from 3% to 8%). The threat weights are still guesses.
SIM_VERSION32, then 35.
Concentration: one number that sets how crowded a level is. Today each level's enemy count is a table (2, then one more every 4 levels). Instead:
- A level's threat is the sum of its enemies' weights, each type weighted by how dangerous it is (a Blob 1, a Seeker 1.5, a Hunter 2.5 …; first guesses, then measured by the balance lab: how often each type kills the bots).
- Concentration = threat ÷ open tiles (the floor plus crates: everywhere an enemy could eventually be). It rises with the level along a curve, for example from 1 per 25 tiles on level 1 to 1 per 12 by level 20, then more slowly (for endless Gauntlet).
- The board size comes from the level; the enemies come from the size × the concentration. So a bigger board brings more enemies, but the same crowding — and crowding goes up with the level. Crates, hazards (19.3) and drops scale with the area the same way, each with its own density.
- Fitting the curve: the balance lab plays bots through levels and adjusts the concentration curve until the bots' chance of clearing each level follows a chosen difficulty curve (say 95% on level 1, 60% on level 20). Then "harder" means measurably harder.
- How big a board gets is then a question of time, not crowding: turns per level grow with the area. Proposed: grow until a level takes about 3–4 minutes for a bot at a human's pace (roughly 80–120 turns), which the lab can measure; a first guess is about 15 × 21. Smaller boards stay for the first levels and as a pause between big ones.
19.3 More on the board: hazards and objects
Goal: more to plan around than crates and pillars. Every object follows the readability rule (§4): visible from the start, behaving the same every time, in the danger overlay when it can hurt, explained by "What is that?" and in a new almanac tab, Board. New ones arrive a few levels apart, as enemies do, so each is met on its own first.
Sprites: mostly from the New Platformer Pack, already used. Spills from the Splat Pack: 36 white splats that can be tinted to any colour (oil, acid, water). Approved (user, 2026-10-24), and all the objects below are wanted.
| Object | What it does | Flame | Sprite | Proposed |
|---|---|---|---|---|
| Oil spill | Floor: anyone can walk on it. | Flame touching any part ignites the whole connected spill at once: it burns this turn and lingers next turn like flame, so a spill carries a blast round corners. Then it is gone. | Splat Pack, tinted dark | first |
| Oil barrel | Blocks like a crate. | Flame sets it off next turn (a 1-turn fuse, shown as a 1): a blast of reach 2, and it spills oil around it (more chains). | none in the packs — an open question; block_strong_danger or block_exclamation could stand in |
first |
| Bush | Anyone walks through it. | Burns away, and fire spreads to touching bushes one turn later — a slow, visible fire front to outrun or to use. | bush; since 2026-10-26 grass_a–grass_d (Foliage Sprites, tinted and outlined, one picked per tile) |
first |
| Metal crate | Blocks everything, like a pillar but anywhere. | Not hurt (even Pierce stops). Maybe Kick pushes it one tile (to discuss). | block_strong_empty |
first |
| Spikes | Floor trap, up every other turn (shown, with the turns until it changes). Up, it hurts anyone on it, enemies too; enemies plan around raised spikes. | — | spikes / block_spikes |
first |
| Acid puddle | Hurts anyone who ends a turn in it. Enemies avoid it. A bomb in it is a dud. | Flame dries it up. | Splat Pack, tinted green | second |
| Water | Not walkable for most; Ghosts and Wisps cross it. A bomb kicked into it fizzles. | Puts out an oil fire that reaches it. | water |
second |
| Conveyor | Each turn it carries whatever is on it (you, bombs, drops, enemies) one tile along. | — | conveyor |
second |
| Ice | Whoever steps on it slides until something stops them, like Slide; kicked bombs too. | Flame melts it to water. | snow-coloured tile |
later |
| Cracked floor | Gives way after something has crossed it once: a pit no one can walk into (enemies that would fall die). | — | a dark tile, to find | later (ties to 19.4) |
| Spring | Throws whoever steps on it 2 tiles on, over anything. | — | spring |
later |
Should bushes hide what is in them? (The user asked for the options, 2026-10-24.)
| Approach | For | Against |
|---|---|---|
| A. Hide nothing: a bush is see-through foliage, only a fire hazard. | Keeps "everything visible" (§4) whole; no surprise deaths. | Bushes add less: only the fire. |
| B. Hide drops and gems: treasure in the bushes. | A reason to burn or walk through them; no unfair deaths, since a hidden drop can't hurt. | Crates already hide the gem; similar. |
| C. Hide enemies: ambushes. | Tension; scouting matters. | Deaths the player couldn't foresee — the thing turn-based play is meant to avoid (§4). A hidden enemy's arrows would have to vanish too, so plans can't be read. |
| D. Hide the player: stealth. Chasers (Seeker's chase, Ghost, Hunter) lose you while you are in a bush and go to where they last saw you. | A new tactic; fair, since you know where you are; uses the enemies' plans. | Needs "last seen" in the enemy rules; enemies that wander aren't affected. |
| E. Half-hide: whatever is inside shows faint (as a Ghost in a crate does), arrows and all. | Atmosphere without hiding information. | Only a look; changes nothing in play. |
Decided (user, 2026-10-24): B + D, with E for enemies. Bushes hide drops (something to find) and hide the player from chasers (something to use), while an enemy inside shows faint, arrows still visible — so nothing that can kill you is ever hidden.
Built 2026-10-25 (the first five; SIM_VERSION 37): from level 2 bushes, 3 metal crates, 5 oil,
8 spikes, 11 oil barrels (OBJECTS_FROM, first guesses), from their own random stream after the
rest of the level, so crates and enemies are where they were. As built, where the table above left
it open: a burning bush is flame that turn, and the bushes touching it smoulder and catch the
next; oil burns once (then it is gone) and lingers a turn like flame; a barrel's blast is 2 and its
spill covers its tile and the floor round it; it shows a 1 and shakes; enemies never step on
spikes, up or down (simpler to read than planning round the raised ones, and spikes then never
hurt them); a metal crate is placed only where every open tile stays reachable, and Kick doesn't
move it. Sprites: bush, spikes, the strong crate in steel grey (block_metal, composed),
block_strong_danger for the barrel, a near-black glossy splat for oil. Death causes spikes and
spread (fire over oil or bushes). Also 2026-10-26: being boxed in (nowhere to step or put a bomb, nothing left to change it)
costs a life. Also built the same day: the almanac's Board tab (crates,
pillars, the five objects and rubble, with their first levels and clips) and a demo scene for each
object (a bush hiding you, a bush fire, metal stopping flame, oil, spikes, a barrel). Not yet: the
spikes' turn count.
Also 2026-10-31 (SIM_VERSION 41, user request): oil is slippery: the player and every enemy but the Ghost and Wisp slide on over a straight line of oil to its last tile (decisions.md, 2026-10-31). Water (below) would be the same sort of surface, later.
19.4 The Acid bomb: weakening pillars, bringing the roof down
Idea: a bomb that eats into the pillars (the fixed grid of walls), so that enough damage brings part of the ceiling down — a way to reshape the board, with a risk.
- The Acid charge, like Pierce or Power: acid bombs (green; how many a charge gives, to tune — the first guess 1). An acid bomb's flame works as usual and also cracks each pillar it touches (a pillar stops flame, so it is the first pillar in each direction). Acid also leaves an acid puddle (19.3) on its own tile for a few turns.
- Cracked pillar: cracked and green-stained. A second hit — any later blast that reaches it — breaks it. Decided (user, 2026-10-24): one acid bomb cracks, a second hit brings it down.
- A broken pillar brings the roof down: the 8 tiles around it show falling dust and a countdown of 4 turns (user, 2026-10-24; like a bomb's timer, and in the danger overlay). Then rubble falls: anything still there is crushed — the player, enemies (a trap to set), drops, bombs — and the tiles become rubble: impassable for good (no blast, Pierce or Crate Pass gets through), so the board can't be opened up into one big room. The pillar's own tile becomes floor. Decided (user, 2026-10-24).
- Two new ways to die (user, 2026-10-24):
- Crushed: standing under the rubble when it falls.
- Trapped: rubble can wall the player in. When the player can never again reach the exit or every remaining enemy — not even by burning crates, only walls, metal crates and rubble counting as permanent — the level can't be won, so it costs a life at once, rather than leaving the player stranded. (An enemy walled off from the player, but reachable by flame, still counts as reachable; the exact test is to settle when building.) The death banner names it, and the "Ouch!" replay shows the fall.
- Floor collapse instead (an alternative, or later): acid on a floor tile makes it cracked floor (19.3), which becomes a pit after one crossing.
- The outer wall never breaks.
Still to settle when building: whether the second hit must be any blast (as written) or another acid bomb, and how many acid bombs a charge gives.
Built 2026-10-25: any later blast breaks a cracked pillar (one cracked before that turn; an
acid flame and a second blast in the same turn only crack it), 1 bomb per charge (price 6),
in the gem's offers, the shop and enemies' drops like the other charges. The ceiling never falls on
the exit or a gem. Crushed or trapped, a Star doesn't help; a Shield carried pushes the player out to an empty tile next to them (straight before diagonal), and is used up — with none, it doesn't help (user request, 2026-10-25). Trapped: the exit, or any
enemy, unreachable from the player through all but walls, metal and rubble (flame can't cross those
either, so "reachable by flame" is the same test). Not yet: the acid puddle the bomb leaves (it
comes with 19.3's second batch), the floor-collapse alternative.
Changed 2026-10-26 (user request): Acid is an area weapon, like the Nuke. Its flame burns
every tile within NUKE_RANGE (2) steps, through anything (the Nuke's diamond, on top of its
cross), and it cracks (or, cracked before, breaks) every pillar in that diamond, through
anything, not just the first pillar an arm of the cross runs into: a pillar diagonal to the bomb
was left untouched. The user chose the full Nuke area over cracks-only-in-area. SIM_VERSION 40.
19.5 Gauntlet mode
Idea: a second way to play. You start with a random loadout and nothing more comes: survive as many levels as you can, each harder than the last.
- Choosing it: the New run dialog gets a choice of mode — Classic (the 20-level run) or Gauntlet. Developer and Immortal stay as options; an Immortal gauntlet would never end, so Immortal is off for it (or its runs don't count).
- The loadout: drawn from the seed, so the same seed gives the same start (and a daily gauntlet later is easy). To keep it fair, it is drawn to a budget, using the shop's prices as value: for example 3 power-ups and 2 stacks of charges and a stat or two, worth about 40 coins in all, so no start is far luckier than another. Later: picking it yourself (a draft — choose 1 of 3, a few times).
- Nothing more comes: no gems, no coins and no shop, and enemies drop nothing. The charges you start with are all you get, so each one is a decision.
- Hearts: one life back for every level cleared, up to the starting 3. Hearts are the only thing that comes back. Decided (user, 2026-10-24).
- Endless (decided, user, 2026-10-24), harder each level: more enemies, faster types sooner (then the Reaper, once it exists), bigger boards (19.2), more hazards (19.3). From the 20 levels of Classic it carries on past them, the ramp continuing (enemy count, then hazard density).
-
Its own high scores: a separate board, ranked by levels cleared, then by fewer turns; each entry shows its loadout and seed.
-
No passes in a loadout (decided, user, 2026-10-24): Flame Pass, Crate Pass and Bomb Pass never come in a gauntlet start.
The passes in the whole game (user, 2026-10-24): they should either leave the power-ups or become temporary charges (for example Flame Pass for 5 turns). Which one is the balance lab's first experiment (19.1): the passes as they are, as charges of a few lengths, and gone, compared on the same seeds.
19.7 One charge at a time (user, 2026-10-24; built 2026-10-25, see §3.3)
Idea: the player carries only one charge. Picking up another opens a choice: sell the new one for a few coins and keep what you have, or swap: take the new one instead. Every pickup becomes a decision, and charges no longer pile up (three Nukes and a Pierce on one bomb).
Proposal, worked out:
- The slot holds one charge and what is left of it: Pierce, Power or Nuke with its bombs left (3 when picked up), ICBM with its launches, or a Shield. The top bar shows it in one place.
- Freeze and Decoy become carried charges (decided, user, 2026-10-24): no longer used the moment they are picked up, but held in the slot like the rest and used with the charge button when the player chooses (a Freeze on the spot; a Decoy on a tile picked next to the player, or on the player's own tile).
- Picking one up with the slot empty: it goes in, as now.
- The same kind as the one held: it adds to it (3 more Pierce bombs), no question asked.
- A different kind: a dialog, like the gem's offer, taking no turn:
- Sell it: keep what you hold; the new one is sold for coins.
- Swap: the new one goes in the slot; the old one is sold (what is left of it) — decided, user, 2026-10-24 — since leaving it on the floor would ask again at once. The sale price: half its shop price (§8), rounded up, scaled by what is left of it — 2 to 3 coins.
- The shop: buying a charge while holding another asks the same, or simply swaps (the old one sold).
- Gauntlet (19.5): a loadout starts with one charge.
- Deliberate use (decided, user, 2026-10-24): the Bomb button places a plain bomb, and a second button (the charge's icon, with its count) uses the charge — a charged bomb for Pierce, Power and Nuke, a launch for ICBM, a Freeze, a Decoy. A Shield stays passive (it works when hit). That would also fix the Nuke trap the balance lab found (a Nuke forced on every bomb in the closed start corner kills the player whatever they do, so the level can't be finished), and give the player the choice of when to spend a charge.
SIM_VERSIONgoes up; the balance lab compares the game before and after (survival, coins, how often each choice is taken by a bot that sells or swaps by a simple rule).
All three questions settled by the user, 2026-10-24. Built right after the balance lab's first report, so the lab can compare the game before and after on the same seeds.
19.6 How these fit together
- 19.1 first helps the rest: it can tell whether the passes need nerfing, test a gauntlet loadout's budget, and tune hazard density.
- 19.2 before 19.3: more objects need more room. The concentration curve (19.2) is fitted with the lab's bots, so 19.1 comes first there too.
- 19.4 needs 19.3's acid puddles and (for the alternative) cracked floor.
- 19.5 uses all of them for its ramp, but could start with what exists today.
20. Bug reports as animated replays (Q104; planned 2026-11-03, nothing built beyond §20.1)
A planning section. The server side (what receives, stores and links reports) is in
~/code/fuzzyworld-tools/PLAN.md, "Issues" → "Report replays". This section is Explodorama's part.
Idea. A bug report already holds the whole run (Run record: {…}, the seed and the actions; see
reportBug in main.ts). The game can replay a record exactly, so an issue page on code.fuzzyworld.net
could show what the reporter saw as a short looping animation, and (for autopilot or rules bugs) what
today's game does instead. A report then answers itself: no need to reproduce by hand.
20.1 Built (2026-11-03, dev only; committed 351348d)
npm run report-replay -- <mail>reads a report's record (quoted-printable mail, a pasted text or a bare record) intoreplays/<name>.jsonand prints page addresses.?replay=<name>: two players on one board, red = the recorded turns, blue = the same until the autopilot took over and then today's autopilot (sim/replayCompare.tsbuildReplay).&rules=<commit>: the whole run under an older commit's rules (red) and today's (blue) (sim/ruleCompare.tscompareRules; the oldsrc/simis taken out of git intorules-versions/).- The page takes no input and is removed from builds (
import.meta.env.DEV). - The Q101 report (autopilot walking into its own bomb with Bomb Pass) is the first fixture
(
test/fixtures/q101-bomb-pass.json).
20.2 What a published replay needs from the game (to do, in order)
- A production replay entry (
replay.htmlbesideindex.html, its own small bundle, same-origin script only, so the site's CSPscript-src 'self'holds). It takes?report=<same-origin json path>, plays the record in a loop, takes no input, saves nothing, records no scores. The dev page's code (replayTick, therivalsecond player, labels) moves out ofmain.tsinto a module both use; the dev page stays as a thin wrapper that adds?rules=. - Treat the record as hostile data. It comes from a mail. Limits before anything runs: size at most
64 KB, at most 20,000 actions,
seedandsimVersionintegers, each action's shape checked; everystepcall in a try/catch (a refused action stops that side, ascompareRulesalready does); every text shown throughtextContent, never as HTML. No path or name from the record is ever used. - Which game plays it. A report is only exact on the rules it was made under (
simVersion). Options, cheapest first: - Today's rules only: right whileSIM_VERSIONis unchanged (the Q101 report replays exactly on today's rules, because only the autopilot's decisions changed). AsimVersionmismatch shows a visible "the rules have changed since this report" banner (the dev page's label does this only in the autopilot mode; make it show everywhere) and offers the next option. - The report's own sim:fw deployof a release also publishes juststate+turn(+ what they import, i.e.src/simbundled as one ES module, a few tens of KB) as/explodorama/sim/<simVersion>.js. The replay page loads the one matching the record, and today's for blue. One file per rules version, not per commit: they change rarely. This replacesrules-versions/+git archivefor the published page; the dev page can keep using git for any commit. - Full old builds per tag: heavier (each is the whole game with art); only if the sim-only way proves not enough. - Compact records. Today a long run's record (66 turns ≈ 8 KB) does not fit a
mailto:link (MAILTO_MAX1900), so the report asks the player to paste from the clipboard, and some will not. A compact encoding (actions as short codes, deflate, base64url) might fit a typical run in the link. Measure on real reports first (the Q101 one is 8 KB raw); keep the plainRun record:form readable for anything that does not fit. - Which fork to show. The dev page's "autopilot took over" fork is one of several comparisons.
For a published page the default should be the plain recorded run (what the reporter saw) with the
last turns slowed, and the comparison (
&rules=or autopilot) as an option, since an issue is often about something no code change touched.
20.3 Open points
- Whether the replay page is part of the game's deploy (
/explodorama/replay.html) or a separate small app; fuzzyworld-tools decides how an app announces its viewer (see its PLAN.md). - Whether the animated page may show the reporter's device string or error list (default: no; only the record and the version/commit).
- The
sim/<simVersion>.jsfiles: who builds them (fw deploywith a per-app hook, or the CI runner on a tag), and whether they are checked by a test that replaystest/fixtures/*.jsonthrough each file. - A regression-fixtures habit: every fixed bug whose report has a record keeps it in
test/fixtures/with a test that replays it (as Q101 does), so a published replay and a unit test stay one artefact.
Related queue items: Q101 (the first report), Q102 (admin panel), Q103 (the dev page), Q104 (this).