fuzzyworld

explodorama / files / PLAN.md

PLAN.md

121.7 KB · history · blame

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:

  1. 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.
  2. 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.
  3. 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).
  4. 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.
  5. 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:


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):

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)


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:

NES rules kept (they create the classic tension):


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:

Inconsistencies with the current plan, to resolve before building (CLAUDE.md asks to raise these, not paper over them):

  1. ~~Keep / Fresh on death (§9)~~ — dropped 2026-10-24.
  2. The shop (§8) sells abilities and stats; with slots, a purchase may also force a swap.
  3. ~~The Workshop (§7)~~ — dropped 2026-10-24.
  4. 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.
  5. ~~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)

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.

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


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

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.

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:

(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).


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:

Planned: the daily dungeon. Not built; to discuss. One shared dungeon a day:


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.

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):

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

  1. ~~Workshop banking~~ — no Workshop (dropped 2026-10-24, §7).
  2. ~~Workshop deposits~~ — no Workshop.
  3. ~~Fresh start and the level~~ — no Keep / Fresh (dropped 2026-10-24, §9).
  4. 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.
  5. 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.
  6. 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.
  7. Certificate strength: replay-verifiable (§11, recommended; nearly free since the sim is deterministic) or a simple checksum?
  8. 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.
  9. 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.
  10. 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


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)

17. Tooling and deploy (copy from ~/code/nordic-skating)


18. Milestones

  1. 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.
  2. M1 – Sim core: ✅ types, RNG streams, grid, Move, Wait, step() with all 5 phases (enemy phases are marked placeholders), run record + replay, tests.
  3. M2 – Bombs: ✅ adjacent placement, timers, blast, chains, soft blocks, death, lives.
  4. 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.
  5. 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).
  6. 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).
  7. 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.)
  8. 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.
  9. M8 – Records: save export/import, victory certificate + QR, replay file and verify page. (The Workshop was here until 2026-10-24: dropped, §7.)
  10. Later: Reapers + turn limit, daily seed, final art.

Backlog


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.

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):

After one charge at a time and the Proximity change (2026-10-25, rules version 29), same seeds:

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:

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

Decided (user, 2026-10-24), replacing the table above:

Built 2026-10-25 (boardSize, concentration, ENEMY_THREAT in sim/levelgen.ts; render/camera.ts; drawIndicators in render/board.ts):

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:

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.

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.

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:

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

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)

20.2 What a published replay needs from the game (to do, in order)

  1. A production replay entry (replay.html beside index.html, its own small bundle, same-origin script only, so the site's CSP script-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, the rival second player, labels) moves out of main.ts into a module both use; the dev page stays as a thin wrapper that adds ?rules=.
  2. 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, seed and simVersion integers, each action's shape checked; every step call in a try/catch (a refused action stops that side, as compareRules already does); every text shown through textContent, never as HTML. No path or name from the record is ever used.
  3. 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 while SIM_VERSION is unchanged (the Q101 report replays exactly on today's rules, because only the autopilot's decisions changed). A simVersion mismatch 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 deploy of a release also publishes just state + turn (+ what they import, i.e. src/sim bundled 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 replaces rules-versions/ + git archive for 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.
  4. Compact records. Today a long run's record (66 turns ≈ 8 KB) does not fit a mailto: link (MAILTO_MAX 1900), 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 plain Run record: form readable for anything that does not fit.
  5. 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

Related queue items: Q101 (the first report), Q102 (admin panel), Q103 (the dev page), Q104 (this).

Source
1# Explodorama — Plan
2
3A turn-based, mobile-first, rogue-like take on the 1985 NES *Bomberman*: 20 levels (50 until 2026-10-23) and an ending.
4Built with **LittleJS + TypeScript + Vite**, tested with **Vitest**, deployed to **GitLab Pages**.
5
6Status: **M0–M5 done, M6 under way** (see §18). This document is the design and the hand-off
7for whoever continues the implementation. Read it fully before writing code, especially §1 (the
8turn), §15 (notes for the implementer) and §18 (milestones). Reasons for decisions are in
9`decisions.md`.
10
11---
12
13## 0. Decisions already made
14
15| # | Question | Decision |
16|---|---|---|
17| 1 | Turn order | **Enemies move *before* bombs explode.** Enemies announce their move one turn ahead (intent arrows), so the player can plan traps. |
18| 2 | Run: charge or always available? | **Run is an ability the player gains** (the NES roller skates, "Speed"). Once gained, always available — no charges. |
19| 3 | Lives | **3 lives**, plus a modern rogue-like layer: **coins** buy upgrades. |
20| 4 | Undo | **None.** |
21| 5 | Which actions | **All actions named in this plan** (§3), plus any agreed later. |
22| 6 | Name | **Explodorama.** ("Bomberman" is a Konami trademark — never use it in the UI, title, manifest or repo name.) |
23| 7 | Shop purchases | **Last for the current run only.** Nothing carries over from one run to the next. |
24| 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. |
25| 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. |
26| 10 | Curses | **Exist, but only appear after the first shop** (level 2 onward). See §6. |
27| 11 | Reapers / turn limit | **Later** (post-MVP). The par-turn coin bonus stays. |
28| 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.* |
29| 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. |
30
31Still open: see §14.
32
33---
34
35## 1. The turn (the core rule — everything else follows from it)
36
37One turn, in this exact order:
38
391. **Player acts.** The player picks one action and a target; it resolves immediately
40 (move, run, place bomb, kick, …). Picking up whatever is on the destination tile happens here.
412. **Enemies execute their announced intents.** Each enemy takes the first of its planned moves
42 (it shows the next 3, §4). An enemy whose next tile became blocked this turn (e.g. the player
43 put a bomb there) stays put — it does **not** re-plan mid-turn.
443. **Bombs tick.** Every bomb's timer decreases by 1. Bombs reaching **0** explode.
45 Chain reactions resolve **within the same phase** (a bomb caught in a blast explodes now,
46 regardless of its timer).
474. **Damage.** Flame kills the player (unless protected) and enemies, destroys soft blocks
48 (revealing anything under them), and destroys pickups lying in the open (see §5, NES rule).
49 **Last turn's flame, still burning, also kills** the player and enemies in it.
50 An enemy sharing the player's tile kills the player.
515. **Enemies plan.** Every surviving enemy tops its plan up to **3 moves** and displays them.
52 Curse and Star/Shield turn counters advance. Input unlocks.
53
54Rules that fall out of this:
55
56- **Bomb number = how many more player actions until it explodes.** A newly placed bomb shows
57 its fuse value (default **3**) and is **not** ticked on the turn it is placed. A bomb showing
58 **1** explodes at the end of the player's next action.
59- **Flame lingers one extra turn** (*changed 2026-10-20 at the user's request*; it used to be
60 instantaneous). A blast burns in phase 4 of its turn, then stays burning through the whole
61 next turn: enemies that step into it in that turn's phase 2 die in its phase 4, as does a
62 player who waits in it. The player can't step, teleport or put a bomb into a burning tile.
63 Lingering flame doesn't burn crates, drops, the exit or bombs again (so drops from a kill
64 survive), and it doesn't set off bombs. This is the turn-based version of the NES flames
65 staying on screen for a moment, which is where most early kills come from: with instant
66 flame, a kill needed the enemy on the exact tile at the exact turn.
67- **Contact is checked twice:** after phase 1 (player walked into an enemy — death) and after
68 phase 2 (enemy walked into player — death). Player and enemy swapping tiles counts as contact.
69- **Bombs block movement** for the player and for (most) enemies. Flame stops at the first hard
70 wall or soft block (destroying the soft block); a bomb in the flame's path chain-explodes.
71
72---
73
74## 2. Player stats and abilities
75
76| Stat | Start | Notes |
77|---|---|---|
78| Lives | 3 | The shop can add more. |
79| Bomb capacity | 1 | Max own bombs on the board at once. |
80| Fire (blast radius) | 1 | Tiles in each of 4 directions. |
81| Fuse | 3 | Starting timer of the player's bombs. |
82| Abilities | none | From pickups or the shop (§3.2, §5). |
83| Charges | none | Temporary changes dropped by enemies, working on pickup (§3.3). |
84| Coins | 0 | Spent in the shop (§8). |
85| Curse | none | At most one active at a time (§6). |
86
87---
88
89## 3. Actions
90
91### 3.1 Always available
92
93| Action | Rule | Highlight when selected |
94|---|---|---|
95| **Move** | 1 tile up, down, left or right (WASD). Target must be free (no wall, soft block, bomb). | Up to 4 reachable tiles. |
96| **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. |
97| **Wait** | Do nothing; the world advances one turn. | Nothing (or a subtle pulse on the player). |
98
99**Few buttons, icons only, explained when picked (user request, 2026-10-22; built).** The bar
100has Move, Bomb and Wait. *(Until 2026-10-23 a second row held a button per charge held:
101Shield, Freeze, Decoy, then also Pierce, Power and Line. Charges now work on pickup, §3.3, so
102the bar is only Move, Bomb and Wait.)*
103*(Detonate and Fuse had buttons there too until a follow-up the same day: the Detonator now
104works by tapping a bomb, which pulses, and the Fuse ±1 charge is gone.)* Nothing but Wait goes
105off from its button alone (Shield and Freeze took a second tap, or a **Use** tap in the strip,
106while they were actions). Buttons show icons, no names. Upgrades don't add buttons, they widen Move and
107Bomb (`sim/picks.ts`): Move also kicks (Kick: walking into a bomb), runs (Run), teleports
108(Teleport) and steps diagonally (while a Diagonal charge lasts); Bomb also throws (Throw) and detonates
109(Detonator; a plain tap on a bomb does too, before a kick). The tile tapped
110decides which, in that order of preference — a kick before walking onto a bomb with Bomb Pass,
111anything free before spending a charge. ~~Picking an action opens a **strip above the bar**
112saying what it does now~~ *Removed 2026-10-23 (user request):* first it covered the board's
113bottom row, then a band kept free for it kept resizing the board. There is no explanation text
114during play: power-ups are explained in the gem's choice, charges on their first pickup, and
115"?" 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
116action). With an action picked, a direction key uses the tile next to the player, or 2 away
117if that one can't be used (a run, a throw).
118
119**Info and menu (user request, 2026-10-21; built; changed 2026-10-22):** a dialog explains a
120tile (`ui/info.ts`): enemies (their next moves in words), bombs, burning flame, walls and crates,
121the exit, the power-up, drops. *Since 2026-10-22 (user request)* a plain tap no longer shows it:
122the round **?** button at the top right, beside the menu (key **?**; it was beside Wait until
123the button rework the same day), turns on "What is that?" for one tap on the board or on an
124action button; tapping **?** again, picking an action, or Esc turns it off. Without it, a tap
125on an enemy, a bomb or burning flame does nothing (never a move into it), a wall or crate only
126clears the auto-move flag, and any other tile the player can walk to — including the exit, the
127power-up and drops — sets the auto-move destination. Things the player can walk to get a
128**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
129cancel) shows level and seed (no Kenney credit since 2026-10-31), and offers **New run** behind a confirmation and
130an **Almanac** (2026-10-22, user request; `ui/almanac.ts`): tabs for power-ups, charges and
131enemies, each with its sprite and the same text as "What is that?" (enemies also: the level
132they first appear on).
133
134**Touch shortcuts (user request, 2026-10-20; built):**
135
136- ~~**Tap the player** = Wait.~~ *Removed 2026-10-26 (user request): a stray tap on yourself
137 waited, which could kill. A tap on the player now does nothing; Wait is its button (Space).*
138- **Deadly moves (user request, 2026-10-26; built):** `isDeadly` (sim/danger.ts) plays an action
139 on a copy with `step()` and says whether the player loses a life this turn. Wait turns red when
140 deadly (still pressable); a picked button's deadly tiles are red with a cross (still tappable).
141 The shortcuts never take a deadly move: `tapMeaning` calls it `'deadly'` (the tile flashes red,
142 a short buzz), and the same goes for direction keys with nothing picked, the long press for a
143 bomb, and auto-move. The button then the tile still takes it. One turn ahead only.
144- **Press and hold** (450 ms) a free tile next to the player, with no action picked = Place bomb
145 there. The bomb and its blast preview appear when the hold registers; release places it,
146 sliding off cancels.
147- **Auto-move:** tap a reachable tile that isn't adjacent, with no action picked, to set it as a
148 destination (flag + dotted path, next step highlighted). The Move button turns into **Go**;
149 each press takes one step along the shortest walk (`sim/path.ts`, over free tiles, ignoring
150 enemies), so it is still one turn per step and fully recorded as ordinary Move actions.
151 **Moving anywhere off the path clears the flag** ("changed my mind", user request, 2026-10-21):
152 any move, run, diagonal step or teleport that doesn't land on the path.
153 **Tapping the flag also takes one step** (user request, 2026-10-20). Tapping another far tile
154 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
155 a wall or crate, or on death / level change. Neither Go nor the flag takes a known-deadly step
156 (onto an enemy, onto an enemy's intent tile, into flame this turn, or anything else `isDeadly` finds): the next step shows red
157 and Go falls back to picking Move.
158 **With no enemies left, auto-move walks by itself** (user request, 2026-10-22): one step 1 s
159 after the last action, until the flag is reached — not while an action is picked, "What is
160 that?" is on or a dialog is open. **With the Run power-up, a straight 2-tile stretch is one Run**
161 (same request), if both tiles pass the safety check; still recorded as an ordinary Run.
162
163### 3.2 Abilities (permanent once gained — no charges)
164
165| Action | Source | Rule |
166|---|---|---|
167| **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.* |
168| **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. |
169| **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. |
170| **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`). |
171| **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). |
172| **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). |
173| **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. |
174| **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. |
175| **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. |
176
177*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.)
178
179### 3.3 Charges (dropped by slain enemies; one carried at a time since 2026-10-24)
180
181*Redesigned again 2026-10-24 (user request; §19.7, built 2026-10-25):* the player **carries one
182charge** (`state.charge`: its kind and uses left; `sim/charges.ts`) and **uses it on purpose**
183with the **charge button** beside Bomb (key V): a charged bomb (`charged`) for Pierce, Power and
184Nuke, a launch (`icbm`), a Freeze (`freeze`, at once), a Decoy (`decoy`, on the player's tile or
185one next to them). Bomb always places a plain bomb. A Shield is passive. The same kind picked up
186adds to it; another kind asks (`chargeOffer`, the `sellCharge` / `swapCharge` actions, no turn):
187keep 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
188price, rounded up, for what is left (`salePrice`). *Since 2026-10-25 (user report)* one chosen
189from a gem or bought in the shop asks the same (it went straight in, selling the one held), and
190the charge button is always there: an empty dashed slot with none, greyed out while it can't act,
191and explained by "What is that?" (which can now be used on any button, even a greyed one).
192*Also 2026-10-25 (user request):* the **Shield** is raised with the charge button too (`shield`
193action; carried, it does nothing) and then **stays up until a hit lands**, through any number of
194levels (`state.shieldUp`, carried from level to level). The **Decoy** goes up to `DECOY_RANGE` 3
195tiles away in a straight line, every tile on the way free — a wall, crate, bomb, enemy or flame
196stops it, as a slide or a kick is stopped — and **lasts until the level ends**, an enemy reaches
197it, or flame burns it. (For one round both lasted 3 turns; the user found that far too short.)
198`SIM_VERSION` 31.
199*Also 2026-10-25 (user request):* **Shield and Freeze need a confirmation**: the charge button
200picks them, lighting the player's own tile (their only target); a tap on yourself, or the button
201pressed again, uses them. `SIM_VERSION` 33.
202*Changed again 2026-10-25 (user request):* **Shield and Flame Pass work by themselves while in the
203charge slot**, no raising: a hit of their kind uses them up (Flame Pass: one of its 3 uses per fire
204hit), and the slot is empty again. The `shield` and `flamePass` actions and `shieldUp`/`flameHits`
205are gone; only Freeze still takes the confirming tap. `SIM_VERSION` 38. `boosts` and `shield` are gone from the state; `?charges=n` became
206`?charge=<kind>`. This also ends the Nuke trap the balance lab found (§19.1). The rest of this
207section is the earlier design, kept as history.
208
209*Redesigned 2026-10-23 (user request):* charges are **no longer actions**. Each works the
210moment it is picked up (`applyCharge` in `sim/actions.ts`), as a temporary change; there are no
211charge buttons. What they still give lives in `state.boosts` (and `shield`, `frozen`, `decoy`)
212and shows in the top bar with its count. It is kept from level to level and through a death.
213Picking up more of a kind adds to it. The first pickup of each kind in a run still opens a
214dialog saying what it does (`chargeInfo`). Icons are the user's choices (§13).
215
216| Charge | Rule |
217|---|---|
218| **Pierce** | The next `BOMB_BOOST` (3, user's choice) bombs' flame passes **through** soft blocks. Icon: the Crate Pass icon. |
219| **Power** | The next 3 bombs' flame reaches to the walls (soft blocks still stop it). Icon: the Fire+ icon. |
220| **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. |
221| ~~**Diagonal**~~ | *Became a power-up the same day* (user request, §3.2). |
222| **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. |
223| **Freeze** | Every enemy freezes for `FREEZE_TURNS` = 3 turns, the pickup turn included (was 4, as an action). Icon: a padlock. |
224| **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. |
225| **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). |
226| ~~**Line**~~ | *Removed 2026-10-23* (not in the user's new list). |
227| ~~**Fuse +1 / Fuse −1**~~ | *Removed 2026-10-22 (user request).* |
228
229A bomb uses one of each bomb boost held, so Pierce, Power and Nuke combine on one bomb; it wears
230their icons as badges. Charges can also appear in the shop (§8).
231
232---
233
234## 4. Readability (turn-based only feels fair if everything is visible)
235
236- **Danger overlay (always on, unless cursed with Fog):** tiles that will burn **this turn** =
237 strong red; **next turn** = light red. Includes chain reactions. Computed by a pure function
238 in `sim/danger.ts` from the current state.
239- **Enemy plans, 3 moves ahead** (*changed 2026-10-20 at the user's request*; it used to be
240 1). Every enemy commits to its next 3 moves and shows them as a **vertical stack of see-through
241 arrows down the right side of its own tile** (the enemy drawn slightly smaller and to the
242 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
243 arrow is evenly tinted (a shaft + head drawn separately showed a darker overlap). It makes
244 exactly those moves unless the board blocks one (then it stays put that turn and plans
245 afresh). Planned moves a new bomb blocks are dropped and re-planned in phase 5. Freeze keeps
246 plans. Reason: bombs take 3 turns, so the player needs to see 3 turns ahead to set traps.
247 (A 2-tile mover will show one arrow per tile.)
248- **Bomb timers:** large number on each bomb; pulses at 1.
249- **Commit preview:** after picking an action and pressing a target, show the outcome
250 (player ghost at destination, faded blast cross, slide path for Kick) before release/confirm.
251- **Pickups, curses and drops** have distinct icons (§13), readable at phone size.
252
253---
254
255## 5. Pickups (NES-style, hidden under soft blocks)
256
257The NES original had exactly 8 power-ups, one hidden per level under a soft block
258(listed from memory — inspiration only, no need to verify):
259
260| NES item | Explodorama equivalent | Kept by "Keep" on death (§9)? |
261|---|---|---|
262| Bombs | **Bomb +1** (capacity) | yes |
263| Flames | **Fire +1** | yes |
264| Speed (roller skates) | **Run** (called Skates until 2026-10-22) | yes |
265| Wallpass | **Crate Pass** — walk through soft blocks | no — always lost on death (*kept since 2026-10-23, see §9*) |
266| Detonator | **Detonator → unlocks Detonate** | no |
267| Bombpass | **Bomb Pass** — walk over bombs | no |
268| Flamepass | **Flame Pass** — immune to flame (tune later if too strong); walk through it (with a Star too, since 2026-10-22) | no |
269| Mystery | **Star** — invincible for 8 turns | no (it's temporary anyway) |
270
271Additions:
272
273- **Fuse +1** — the player's bombs start at 4. Kept by "Keep".
274- **Coin bag** — +N coins.
275- **Heart** — +1 life (rare).
276
277NES rules kept (they create the classic tension):
278
279- **One pickup per level** under a soft block (more via shop upgrades), plus the
280 **exit door** under another soft block. *Since 2026-10-23 (user request) the pickup is a
281 **gem** that offers a choice:* taking it offers `OFFER_SIZE` (5) different power-ups, drawn
282 by the weights below without repeats and leaving out abilities and passes already held, and
283 the player picks one (`choose` action, no turn; `ui/offer.ts`, a dialog that must be
284 answered). The gem's colour is random and means nothing. The offer comes from the seed and
285 level (`stream(seed, 'offer', level)`). A death on the pickup turn drops the offer (the retry
286 has the gem back); reaching the exit on the same step still needs the choice first.
287- **Flame destroys an exposed pickup** — and spawns extra enemies, as in the NES.
288- **Flaming the exit door** spawns a group of enemies from it. *Since 2026-10-23 (user's choice)*
289 they can't be hurt by the flame that let them out, which lingers a turn and used to kill them
290 at once; new explosions still can (`Enemy.sheltered`). The same for a flamed gem.
291- **The exit opens only when all enemies are dead.**
292- ~~**Run is guaranteed on level 1 or 2**~~ *Dropped 2026-10-23 (user's choice, with gems):* Run
293 is in the draw like the rest (weight 10).
294- ~~*Built (2026-10-22, user request):* picking one up opens a dialog saying what it does~~
295 *Replaced 2026-10-23:* the gem's choice dialog opens 0.45 s after the pickup instead, once the
296 player has arrived and the gem has risen away.
297- *Built (2026-10-20):* the pickup is under any crate except the exit's. Draw weights (to tune):
298 Bomb+ 20, Fire+ 20, Fuse+ 6, Coin bag 12 (10 coins), Heart 4, Crate Pass 8, Bomb Pass 8,
299 Flame Pass 5, Detonator 9, Star 8 (8 turns), and since 2026-10-22 Kick 8, Throw 8, Teleport 3;
300 since 2026-10-23 Run 10, and they weight the gem's offer (`POWER_UP_WEIGHTS` in `sim/turn.ts`). Sprites: §13 (since 2026-10-22, a bomb or the astronaut with a
301 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
302 2026-10-24). A Run collects what is on the tile it passes through.
303
304---
305
306## 5b. Exclusive power-ups — a rogue-like direction (idea, user, 2026-10-21; *not decided*)
307
308*2026-10-23:* the gem's choice of 1 from 5 (§5) is a first step in this direction — the player
309chooses instead of taking whatever is there — but nothing is exclusive yet: slots are still open.
310
311The user wants to start thinking about making power-ups **exclusive**: picking one up can
312**replace** another, so the player can't simply collect everything, and is pushed to
313**specialise, experiment and choose** — which to keep, which to drop. A sketch to discuss:
314
315- **Slots.** The player has a few power-up slots (e.g. 3). Taking a power-up with a free slot
316 fills it; with all slots full, a dialog asks which one to give up (or to leave the new one).
317 The dropped power-up stays on that tile, so a swap can be undone while still on the level.
318- **What takes a slot.** Natural candidates: the abilities and passes (Run, Detonator,
319 Wall / Bomb / Flame Pass), maybe later new ones (Kick as a permanent ability, Pierce bombs…).
320 Open: do the stat gems (Bomb+, Fire+, Fuse+) also take slots (e.g. "Bomb+ ×2" in one slot,
321 stacking within it), or stay uncapped as in the NES? Instant ones (Heart, Coin bag, Star)
322 would not take a slot.
323- **Synergies and trade-offs** make choices interesting: e.g. Bomb Pass + Kick, Crate Pass +
324 Pierce, Flame Pass + Line bombs. A power-up could also have a downside (a "cursed" version).
325
326**Inconsistencies with the current plan, to resolve before building** (CLAUDE.md asks to raise
327these, not paper over them):
328
3291. ~~**Keep / Fresh on death (§9)**~~ — dropped 2026-10-24.
3302. **The shop (§8)** sells abilities and stats; with slots, a purchase may also force a swap.
3313. ~~**The Workshop (§7)**~~ — dropped 2026-10-24.
3324. **NES rule: passes and Detonator are lost on death (§5, §9)** — with slots, perhaps nothing
333 is lost on death and the slot choice itself is the cost. *Settled 2026-10-23 (user request):
334 nothing is lost on death, slots or not.*
3355. ~~**Developer mode (§13b)** gives everything; it would bypass slots (fine for testing).~~
336 *Resolved 2026-10-23:* Developer mode now only uncovers the gem, so it bypasses nothing.
337
338Open questions for the user: how many slots? Do stats take slots? Is a swapped-out power-up
339gone or left on the tile? Should action charges (one-use, from enemies) stay separate from slots
340(probably yes)?
341
342## 6. Curses (from the *Super Bomberman* skull)
343
344- **Never before the first shop** — i.e. not on level 1. From level 2 onward a **skull** can
345 appear: under soft blocks (as an extra item, never replacing the level's pickup) and as a rare
346 enemy drop. Frequency rises gently with level.
347- The skull is **visible** once revealed, so the player can choose to avoid it.
348 **Flaming a skull destroys it safely** (no enemy spawn — unlike pickups), which gives bombs a
349 second use.
350- Walking onto it applies a **random curse for 10 turns** (HUD shows icon + countdown).
351 One curse at a time; a new skull replaces the current curse.
352- Curse list:
353
354| Curse | Effect |
355|---|---|
356| **Short Fuse** | Your bombs start at 2. |
357| **Leaky** | Each turn, if capacity allows, a bomb drops on a random free adjacent tile. |
358| **Butterfingers** | Your bombs land on a random free adjacent tile, not the one you chose. |
359| **Fog** | Danger overlay and enemy intent arrows are hidden. |
360| **Heavy Feet** | Run is disabled. |
361| **Damp** | Fire is 1. |
362
363- **Cures:** wait it out, a Star, or the shop's **Cleanse** charge.
364- (Open, §14: should skulls also give coins — a risk-for-reward choice?)
365
366---
367
368## 7. ~~The Workshop~~ — dropped (user's call, 2026-10-24)
369
370The plan had permanent upgrades between runs, Everspace-style: coins left over at the end of a
371run banked into upgrade tracks (more lives, bombs or fire to start with, Run from the start, …).
372Dropped: the user can't imagine cross-run permanent upgrades that wouldn't ruin the game. Every
373run starts the same; what a run gains, it keeps only until it ends. (The tracks and their costs
374are in git history and decisions.md.)
375
376---
377
378## 8. Coins and the in-run shop
379
380- **Coin sources:** some enemy types always drop coins, others sometimes; occasional coins under
381 soft blocks (*not built*); Coin bag pickups; *since 2026-10-24* a **clear bonus** for every
382 level; a **par bonus** for finishing a level within its turn budget.
383- *Built 2026-10-24 (the user's choices of the options offered):* **the shop opens every few
384 levels** — after levels 3, 6, 9, 12, 15 and 18 (`SHOP_EVERY` 3, a choice made here; not after
385 the last) — when the player goes on from the level-cleared banner, before the next level
386 (`sim/shop.ts`, `ui/shop.ts`).
387 - **It sells power-ups and charges:** `SHOP_SIZE` 4 items, drawn without repeats by the gem's
388 weights (`POWER_UP_WEIGHTS`), charges at weight 7 each; no power-up already held, and no Coin
389 bag (coins for coins).
390 - **Prices by rarity** (`PRICES`): Bomb +1 / Fire +1 4, Fuse +1 5, most abilities and passes 6,
391 Slide / RPG / Detonator 8, Flame Pass 9, Teleport / Proximity alert 10, Heart 12, Star 6;
392 charges 3–6 (Decoy 3; Pierce, Power, Freeze 4; Shield 5; Nuke, ICBM 6). To tune by playtesting.
393 - **Restock:** 1 coin, one more each time in the same shop (`rerollPrice`); a new draw of 4.
394 - **The coin budget** (*2026-10-24, user request: make the shop reasonable, "leaning towards
395 cheap"*). Before, three levels earned about 2 coins from enemies (2 Blobs a level at 20% for
396 1.7 coins on average) plus 5 for each level within par: a playtest reached the first shop with
397 5, about half the cheapest item. Now each cleared level pays `CLEAR_BONUS` 2 coins (par adds
398 `PAR_BONUS` 5 on top), and enemies drop coins more often (§10), about 1 coin a kill. Expected
399 coins per three-level stretch, collecting about 80% of the coin drops:
400
401 | Shop after level | Kills | From kills | Clear bonus | Within par | Total |
402 |---|---|---|---|---|---|
403 | 3 | 6 Blobs | ≈ 5 | 6 | + 15 | ≈ 11 – 26 |
404 | 6 | 8, Blobs and Seekers | ≈ 7 | 6 | + 15 | ≈ 13 – 28 |
405 | 9 | 10, Blobs and Seekers | ≈ 10 | 6 | + 15 | ≈ 16 – 31 |
406 | 12 – 18 | 12 – 15 | ≈ 12 – 15 | 6 | + 15 | ≈ 18 – 36 |
407
408 With the new prices, the least a player brings to the first shop buys two cheap things (a stat
409 and an ability, or a charge), and par play buys three or four; a Coin bag (10) is a whole
410 extra item. Later shops are a little richer, as the run gets harder.
411 - Buying (`buy`) and restocking (`reroll`) are actions of the run record that take no turn; a
412 bought item works at once. **Next level** is the `continue` action, as from the banner.
413 - The draws come from the seed, the level and the restock count, so a seed always offers the
414 same items.
415 - Its dialog is like the gem's offer: a clip of the selected item above the cards, which show
416 their prices; the selected card has its own Buy button (or "Need N more"), and Restock and
417 Next level share one short row (*since 2026-10-24, user report: three full-width buttons left
418 the items too little room*); it can't be closed otherwise.
419- Not (yet): the plan's run modifiers (Magnet, Lucky, Map, Coin multiplier) and Cleanse, which
420 waits for curses (§6).
421- Purchases last for this run only (decision 7).
422
423---
424
425## 9. Death
426
427*Built 2026-10-24 (user request):* the death banner (and the game-over one) says how and where
428the player died — level, turn, and the cause (`GameState.death`): walked into an enemy, an
429enemy walked in, a bomb's blast, a chain reaction's second bomb, or last turn's flame — and
430replays the player's own last turns (§13c).
431
432A death with lives left restarts the level from its seed: the same layout, the enemies back where
433they started, and the player keeping everything the run had (stats, power-ups, passes, charges,
434coins; since 2026-10-23). At game over, **Same dungeon** runs the same seed again from level 1,
435with nothing carried over, and **New dungeon** a new one.
436
437~~**Keep or Fresh**~~ — *dropped 2026-10-24 (user's call).* The plan was a choice after each
438death: keep the run's power, or reset it for a coin payout, balanced so each was picked about half
439the time. Now there is no choice and no payout: a retry, of the level or of the whole dungeon, is
440just another try at the same board, its only benefit what the player now knows. (The table and
441the payout formula are in git history and decisions.md.)
442
443---
444
445## 10. Enemies
446
447All move on their own schedule ("speed"), are blocked by bombs unless stated, and kill on
448contact. Intent shown every turn. Adapted from the NES eight:
449
450| Name (ours) | NES inspiration | Speed | Behaviour | Drops |
451|---|---|---|---|---|
452| Blob | Balloom | 1 tile/turn | Random wander, prefers to keep its direction | rarely coins |
453| Seeker | Oneal | 1 tile/turn | 50% step toward player (BFS), else wander | coins sometimes |
454| Drifter | Doll | 1 tile/turn | Bounces straight until blocked, then turns | action charge sometimes |
455| Sprinter | Minvo | 2 tiles every other turn | Toward player 50% | action charge |
456| Ghost | Kondoria | moves every other turn | Passes through soft blocks; chases | coins |
457| Wisp | Ovapi | 1 tile/turn | Passes through soft blocks; wanders | action charge |
458| Hunter | Pass | 1 tile/turn | Always BFS shortest path to player | coins + charge |
459| Reaper | Pontan | 2 tiles/turn | Passes soft blocks, always chases. **Later milestone:** spawns only as punishment (turn limit, destroyed exit/pickup) | big coins |
460
461- *Built 2026-10-24 (user request: "add more enemies"):* Sprinter, Ghost, Wisp and Hunter, as in
462 the table, with the sprites of §13. Choices made while building:
463 - **Speed as rests:** a plan still holds one move per turn; a Ghost's or Sprinter's every other
464 move is a rest (its own tile, drawn as a dot, eyes closed). A Sprinter's move is a dash of 2
465 tiles in a line (two arrows), or 1 when the second is blocked. A dash through the player's
466 tile stops there and catches them; a bomb on either tile stops it. (`RESTS`, `moveTiles`.)
467 - **Through crates:** Ghosts and Wisps may enter soft blocks (`PASSES_CRATES`), drawn
468 see-through inside one; bombs and walls still stop them. Flame that burns their crate kills
469 them; a crate in front shelters them, as it stops the flame.
470 - **Hunter** always chases (unless another enemy takes that tile, as a Seeker's chase).
471 - **When they come:** one new type every 3 levels (Blob 1, Seeker 4, Drifter 7, Sprinter 10,
472 Ghost 13, Wisp 16, Hunter 19), so a 20-level run meets them all; it was every 6 levels while
473 there were three. Each enemy's type is picked evenly from those met so far.
474 - **Drops:** Sprinter and Wisp 40% coins / 60% charge, Ghost 90% / 25%, Hunter 90% / 60%.
475 `SIM_VERSION` 28.
476- Until Reapers exist, "destroyed pickup/exit" spawns a group of the level's tougher types.
477- Base drop table: ~30–40% action charge, separate coin roll, rare skull from level 2. Tune.
478 *Built (2026-10-20):* per type — Blob 20% coins / 15% charge, Seeker 50% / 25%, Drifter
479 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,
480 +1 every 3 levels, at most 10. New type every 6 levels: Seeker from 7, Drifter from 13.
481 Enemies may share a tile (they don't block each other), but *since 2026-10-23 (user request)*
482 each plans around where the others will be, so they spread out and only share when forced
483 (`spreadSteps`). *Since 2026-10-23 (user's choice):* when a turn's kills leave no enemy, their
484 charge drops are only lasting ones (Pierce, Power, Nuke, Shield: `LASTING_CHARGES`), since a
485 Freeze or Decoy from the last enemy had nothing left to work on. Nor is a Decoy, Freeze or
486 Shield dropped while one already lies on the board or is working (user report, 2026-10-23: a
487 second Decoy beside the first); Pierce, Power and Nuke add up, so they always can be. Enemies spawned from a flamed exit
488 drop nothing, so the exit can't be farmed.
489- Drops land on the enemy's tile and stay until collected or flamed. *Built (2026-10-21, user
490 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
491 power-up) rolls to the nearest free floor tile within 2 steps (BFS over floor), else stays.
492- **Introduction schedule over 50 levels** (like the NES): Blob only at first; one new type
493 every ~6 levels; later levels mix types. Exact table to be drafted in `design.md`.
494
495---
496
497## 11. Levels, scrolling, progression, the ending
498
499*Since 2026-10-23 (user request):* **one board, 9 × 11, 20 levels**, fitting the screen whole —
500the former "Small" board (density 25%, enemies 2 + 1 per 4 levels up to 5, par 25 + 8/enemy) —
501with no scrolling, and crates always crumbling once the last enemy is gone (§13b). The NES-size
502notes below (31 × 13, 50 levels, the side-scrolling camera) are kept as the history of the
503design; the board code for them was removed.
504*Built 2026-10-25 (user request, §19.2):* boards grow with the level (9 × 11 to 15 × 21), seen
505through a fixed 9 × 13 view that follows the player, with arrows at its edge; enemies by
506concentration. This undoes the one-size choice above.
507
508- **Grid 31 × 13** like the NES: hard walls around the border, indestructible pillars at every
509 even (x, y), soft blocks placed by seeded RNG at ~30–40% density, start corner (3 tiles in an L)
510 always clear, no enemy within ~4 tiles of the start. (Open, §14: narrower early levels?)
511- **Generation must guarantee:** the exit and the pickup are under soft blocks; every free tile is
512 reachable once soft blocks are gone (assert across many seeds in tests).
513- **Camera:** all 13 rows always fit vertically (portrait phone is the primary target), about
514 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
515 level edges. Landscape/desktop show more columns.
516- **Par:** each level has a turn budget; finishing under it pays a coin bonus. *Built:* par =
517 50 + 10 × enemies, bonus 5 coins; the HUD shows `turn / par`.
518 (Turn **limit** with Reapers: later milestone.)
519- **50 levels**, difficulty rising through enemy count, types, speed and soft-block density.
520- **Game over** at 0 lives → run summary (level reached, coins, kills, seed) → the same dungeon
521 again or a new one.
522- **Ending** after level 50: an ending screen, then the **victory certificate**.
523- **Mid-run save is required**, not optional: a 50-level run won't happen in one sitting on a
524 phone. The save is just the run record (seed + action log, §12), replayed on
525 load — or a JSON snapshot of the state plus the log so far. *Built (2026-10-20):* the record,
526 in `localStorage`, after every action, replayed on load; only for the same `SIM_VERSION`, and
527 never for test URLs (`?seed`, `?level`, `?charges`). Finished runs aren't resumed.
528 *Shelved (2026-10-24, user's call):* keeping a run across a rules change (a state snapshot
529 next to the log, the record noting the version change mid-run) and playing older versions.
530 With no players but the developer, a run lost to a rules change costs nothing; revisit once
531 the game has an audience.
532
533### Victory certificate
534
535*Since 2026-10-23 (user request):* an **Immortal** run (§13b) can be played to the end but earns
536no certificate, nor any other achievement.
537A **Developer** run earns them all, but each shows a **red border** (user request, 2026-10-23,
538"like the red quarters some bars used in jukeboxes"); its victory banner is framed in red already.
539
540Shows: **seed, date, turns, deaths, sim version**, and a
541**QR code** linking to `https://apps.fuzzyworld.net/explodorama/#cert=…`.
542
543Being honest about what this can prove: the game is fully client-side and open source, so any
544code-only "signature" can be forged by someone determined. What *can* be verified is the run
545itself, because the sim is deterministic:
546
547- The certificate carries a **hash of the full run record** (seed + loadout + every action).
548- **"Save replay"** downloads the run record as a small file. A **verify page** replays it in the
549 real sim and confirms it reaches level 50's ending and matches the certificate's hash.
550- The full record (~7,500 turns × ~5 bits ≈ 5 KB) is too big for a single QR code, so the QR holds
551 the summary + hash, and the replay file is the proof.
552- Bonus: the same replay file lets anyone **watch** the winning run.
553- Replays only work with the same rules, so the record stores `SIM_VERSION`; the verify page
554 must say clearly when a replay comes from a different version.
555
556(Open, §14: is this level of proof wanted, or is a simple checksum enough?)
557
558---
559
560## 12. Seeds and randomness
561
562*Built 2026-10-24 (user request):* the New run dialog has an optional **seed** field (empty:
563random; a whole number: that seed; other text: hashed into one), so a dungeon can be shared or
564replayed as a real, saved run (a `?seed` link is a test board). **Game over** offers **Same
565dungeon** (same seed and modes, from level 1) and **New dungeon** (a random seed).
566
567- One **run seed** (shown on the run summary and certificate; can be entered to replay a seed).
568- **Separate RNG streams** derived from the run seed by purpose and level, e.g.
569 `hash(seed, "levelgen", level)`, `hash(seed, "ai", level)`, `hash(seed, "drops", level)`,
570 `hash(seed, "shop", level)`, `hash(seed, "curses", level)`. This way an extra bomb doesn't
571 change what the next shop offers, and the same seed always gives the same 50 level layouts.
572- Never `Math.random()` in `sim/`.
573
574---
575
576## 12b. High scores, a global board, and the daily dungeon (user request, 2026-10-24)
577
578**Built: the local board.** Every finished run (game over or the win) goes on a board kept in
579this browser (`ui/scores.ts`, `localStorage`), the best `BOARD_SIZE` (25), shown from the menu
580and from the end-of-run banner (which also says "New high score: #N!"). **Ranked** (user's
581choice) by a win first, then the level reached, then coins, then fewer turns. Each entry
582(`ScoreEntry`, `sim/scores.ts`) keeps: date, seed, modes, won, level, coins, enemies killed
583(`GameState.runKills`, new), turns, lives lost, how the run ended (the death: cause, level,
584turn), the abilities and passes held and the stats — and **the whole run record** (seed and
585actions) with `format` and `simVersion`. Immortal runs don't count; Developer runs count, shown
586with a red border (§13b); test URLs (`?seed` and the like) never record.
587
588**Planned: a global board.** *Not built.* It would be the app's first backend — so far there is
589none, on purpose (§9's note on measuring balance) — so it needs a decision before it is built.
590The entries are already shaped for it:
591
592- **Verification by replay.** The sim is pure, deterministic TypeScript, so a server can run the
593 same code on a submitted record (seed + options + actions) and recompute the level, coins,
594 kills and turns; it accepts the entry only if they match. Faking a score then means playing it.
595 Only records with the server's current `SIM_VERSION` (or one it still has the code for).
596- **Identity:** no accounts at first — a chosen display name and a random install id. Submitting
597 is opt-in, per run or as a setting; nothing leaves the browser otherwise.
598- **Hosting:** GitLab Pages is static, so a small function + storage (a serverless function and a
599 key-value store, or a tiny server). Rate limits per install id; record size capped (compressed).
600- **Boards:** all-time (per rules version), per seed, and the daily one below. Developer runs
601 marked as on the local board; Immortal never submitted.
602
603**Planned: the daily dungeon.** *Not built; to discuss.* One shared dungeon a day:
604
605- **Seed from the date:** `hashSeed('daily', 'YYYY-MM-DD')`, the date in **UTC**, so everyone
606 everywhere plays the same dungeon on the same day (local dates would give the world two or
607 three dungeons at once; the cost is a rollover at a local hour that isn't midnight).
608- **Fixed modes:** no Developer or Immortal, so results compare.
609- **Attempts:** open question — only the first finished run of the day counts (Wordle-style, the
610 most tension), or the best of the day (more play). Practice runs could follow the counted one.
611- **Its own board** per day (locally: a history of daily results and a streak; globally: the
612 day's board). The rules version is part of it: if the rules change during a day, that day's
613 board splits by `SIM_VERSION`.
614- **Sharing:** a short result text to paste ("Explodorama daily 2026-10-24: level 12, 84 coins,
615 37 enemies"), and the seed to try it later.
616- Started from the menu (Daily dungeon); the top bar shows a Daily tag.
617
618---
619
620## 13. Look and icons
621
622*Changed 2026-10-20 at the user's request.* The game uses **Kenney's New Platformer Pack 1.1**
623(<https://kenney.nl/assets/new-platformer-pack>), which the user downloaded. It replaces the earlier
624plan of black Icongeek26 outline icons from Flaticon over a plain drawn board.
625
626- **Licence:** CC0, so attribution isn't required, but **the user wants Kenney credited
627 properly**: in the README's Attribution section and in the game (a credit line with a link to
628 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 /
629 game-over banner too). The
630 pack's `License.txt` is kept in `assets/kenney/`.
631- **Source files:** only the sprites the game uses (or is about to use, below) are copied, unchanged
632 and under the pack's own file names, into `assets/kenney/`, so each can be found again in the
633 pack. All are 128 × 128: tiles and enemies from the pack's `Double` folders, characters from
634 `Default` (which are already 128 px).
635- **Loading:** no atlas. `src/render/sprites.ts` lists the board's sprites; each is its own LittleJS
636 texture (`engineInit`'s image list) and is drawn with `drawTile`. Separate images cost nothing
637 measurable (with Canvas 2D at first, and with WebGL since 2026-10-24). `setTilesPixelated(false)`, since the art is smooth.
638 The HTML HUD and buttons use the same PNGs as `<img>`. Vite keeps every sprite a separate file
639 (`assetsInlineLimit: 0`), so art for things not in the game yet is never downloaded.
640- **Board look:** sprites everywhere. Colour still shows meaning, but **shape alone must already
641 tell things apart** — never colour only.
642- **HUD:** the pack's HUD digits for numbers, hearts (full/empty) for lives.
643- **Sounds:** the pack has 10 `.ogg` effects (`sfx_bump`, `coin`, `disappear`, `gem`, `hurt`,
644 `jump`, `jump-high`, `magic`, `select`, `throw`). **Decided (user, 2026-10-20): use them** when
645 sound is added (M7), copied into `assets/kenney/` like the sprites; ZzFX only for anything the
646 pack has no sound for (e.g. the explosion).
647 *Built 2026-10-24 (user request: "add sounds and vibrations (where possible)"):*
648 `src/ui/feedback.ts`. The 10 effects in `assets/kenney/sounds/` plus the UI Pack's `tap-a` (every
649 button) and `switch-a` (the gem's choice, a restock) in `assets/kenney-ui/Sounds/`, converted
650 to `.mp3` (older Safari versions can't decode Ogg Vorbis). ZzFX for the blast, a bomb set down and the
651 ICBM's launch. Each turn's sounds are timed with its animation: a blast and what it kills when
652 the blast shows (`fx.blastAt`), a pickup at the end of the walk; a chain is one louder boom.
653 Vibration (`navigator.vibrate`, so Android only; iOS Safari has no web vibration) for blasts,
654 hits, a broken shield, pickups and a cleared level. Menu switches **Sound** and **Vibration**
655 (the latter only where the browser has it), stored in `localStorage`
656 (`explodorama.feedback`). Only the player's own turns make sound: the demo and the clips are
657 silent. No music.
658
659- **UI (2026-10-21):** dialogs and their buttons use Kenney's **UI Pack 2.0** (rectangle depth
660 buttons, green / red / grey; round grey button for close and the menu; play / repeat icons; the
661 **Kenney Future** font for titles) and the **UI Pack: RPG Expansion** parchment panel, as CSS
662 9-slice `border-image`s. Files in `assets/kenney-ui/` (pack colour folders kept) and
663 `assets/kenney-ui-rpg/`, each with its licence. Credited with the platformer pack.
664- **Icons (2026-10-22):** Kenney's **Game Icons** pack (CC0) for plain UI symbols the other packs
665 lack: `question` (black, 2x) on the "What is that?" button, inverted to white while it is on.
666 Files in `assets/kenney-icons/` with the pack's licence, unchanged, under the pack's names.
667 Credited with the others.
668- **Badged power-ups (2026-10-22, user request):** a power-up that improves something you
669 already have is that thing plus a **modifier badge** at the top right — a green disc with a
670 dark rim and a white symbol. Symbols from Game Icons (`White/2x`: `plus`, `larger`,
671 `fastForward`, `arrowRight`, `upRight`, in `assets/kenney-icons/White/`) and Kenney's **Board
672 Game Icons** pack (`timer_CW_75`, `shield`, in `assets/kenney-board-icons/` with its licence;
673 credited); later also `export`, `exclamation`, `cross` from Game Icons.
674 `tools/compose-icons.sh` (ImageMagick) builds them into `assets/composed/`, committed, so the
675 build needs nothing extra; `sprites.ts` loads both folders.
676
677Sprite assignments (✅ = drawn now; the rest are copied and waiting for their feature):
678
679| Thing | Sprite(s) in `assets/kenney/` |
680|---|---|
681| ✅ 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. |
682| ✅ 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 |
683| ✅ pillars | `block_blue`, `block_green`, `block_red`, `block_yellow`, cycling by level |
684| ✅ soft block | `block_planks` (wooden crate) |
685| ✅ bomb | `bomb`; flashes to `bomb_active` on its last turn; timer drawn in `hud_character_0`–`9` |
686| ✅ flame | `fireball` (spinning, fading) over an orange tile |
687| ✅ HUD | `hud_heart` / `hud_heart_empty` (lives), `flag_green_a` (level), `slime_normal_rest` (enemies left), `hud_coin` (coins), `bomb`, `fireball`, digits |
688| ✅ 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` |
689| ✅ banners | `character_green_hit` (lost a life), `hud_heart_empty` (game over), `flag_green_a` (level cleared); favicon `bomb` |
690| ✅ 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.) |
691| ✅ coins | `coin_bronze` (1), `coin_silver` (2), `coin_gold` (5), bobbing; `hud_coin` in the HUD |
692| ✅ 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`.) |
693| ✅ decoy | `character_green_front`, faded |
694| ✅ 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. |
695| ✅ intent arrows | drawn red lines with a head, not sprites |
696| ✅ 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. |
697| ✅ 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). |
698| ✅ Seeker | `fly_rest`, `fly_a`, `fly_b`, as the Blob |
699| ✅ Drifter | `saw_rest`, `saw_a`, `saw_b`; spins while moving |
700| Sprinter | `mouse_rest`, `mouse_walk_a`, `mouse_walk_b` |
701| Ghost (passes soft blocks) | `slime_block_rest`, `_walk_a`, `_walk_b`, `_jump` |
702| Wisp | `slime_fire_rest`, `_walk_a`, `_walk_b`, `_flat` |
703| Hunter | `frog_rest`, `frog_idle`, `frog_jump` |
704
705The pack has more enemies (bee, ladybug, snail, worm, barnacle, fish, spiked slime, falling
706block) and more tiles (keys, locks, levers, spikes, torches, …) for later enemies, curses and
707actions; copy them in when they are used. Icons the pack doesn't have (shop, trophy,
708settings, sound, and most action charges) are still open.
709
710---
711
712### Animation timing (user's guidance, 2026-10-24)
713
714Most turns animate in a step's time (0.16 s), so play stays quick. **A rare or involved turn —
715a charge at work, an ICBM, something that changes a lot at once — may take longer** and animate
716a more complex change of state in stages (the ICBM flies, then its blast plays out), which gives
717actions more room. The sim stays instant; only the drawing waits (`blastAt`-style holds in
718effects.ts). Input is still never locked (decisions.md 2026-10-22): a quick next tap starts the
719next turn over the end of the last one.
720
721## 13a. The demo (user request, 2026-10-23; built)
722
723Some players never played the original, so the game opens with a demo: generated scenes,
724replayed view-only, a caption under each, until the player taps or presses a key (that input
725does nothing else; then the saved run, or a new one). Scenes are made **at play time**
726(`sim/demo.ts`, pure and seeded): a 9 × 7 room (walls, pillars on every other tile, crates on
727about one tile in six, the player and 1–3 enemies apart), then up to 150 random games on it by
728the real rules — mostly moves, sometimes bombs, never a move that kills the player — until one
729shows the scene's moment; the shortest of those is the scene (a new room if none does).
730Kinds (`SCENES` in `sim/demo.ts`, each with its moment): *since 2026-10-23 (user request)* every
731feature on the board but the shop and the exit — kill, **arrows** (an enemy killed on the tile it
732stepped onto, the bomb waiting where its arrows led: watch them; user request), escape, chain,
733lingering flame, the first three enemy types, drops, the gem (uncovered and taken; its choice isn't
734shown), each power-up that shows on the board (Bomb +1, Fire +1, Fuse +1, Run, Kick, Throw,
735Teleport, Diagonal, the Detonator, Wall / Bomb / Flame Pass, Star), and each charge (Pierce,
736Power, Nuke, Shield, Freeze, Decoy, picked up first). Heart and Coin bag only change numbers,
737so they have none. *Since 2026-10-23 (user request)* also the exit — **exit** (it opens, the
738player walks in and the scene ends there; once open, the random play mostly heads for it) and
739**doorBlast** (flame on the uncovered exit lets 3 enemies out) — and **the ways to die**, each a
740scene whose moment is the death: into a blast (from a safe tile onto a burning one), not far
741enough (moved but stayed in reach; Fire 3), blocked in (no step out of danger), chain (caught
742only by a bomb that wasn't due), an enemy, and two funny ones: the Detonator next to you, a Run
743into an enemy. The random play keeps refusing deadly moves, except the one that is the scene's
744death. Living and dying scenes take turns, each queue shuffled (*until 2026-10-24: now one fixed
745order, see below*). 36 kinds, all found on every
746seed tried, the slowest under 0.5 s; each caption has the thing's icon. The "Ouch!" and
747level-cleared banners are hidden in the demo. The UI (`main.ts`: `startDemo`, `demoTick`) plays a
748turn every 0.6 s through the same animation code as play, hides the top bar and buttons, and
749fits 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
750the top bar and the almanac.
751*Since 2026-10-24 (user request)* **every scene gets to its point within 3–6 turns** and **the demo
752shows the controls**. A random game still runs until it shows the moment, but the scene is only
753its last 3–6 turns (as few as do), starting from a calm board — no bomb, no flame, no drop the
754room didn't start with, auto-move not under way — and replayed from there to check the moment
755still comes (`ending` in `sim/demo.ts`; Fuse +1 is now fuse 4, the real +1; Pierce's room has a
756row of two crates with Fire 2, and its moment is a crate burnt behind another; Pierce, Power and
757Nuke lie next to the player). Every turn comes with its **gesture** (`Gesture`: a tap, a long
758press, a button and then a tile, a far tap for auto-move, Go), only ones the real controls read
759as that action (`tapMeaning` in `sim/picks.ts`, shared with `tapTile`; `resolvePick`); a turn no
760gesture can play isn't used. The UI acts each one out with a finger (Kenney's pointing hand) on
761the real action bar, shown in the demo as a tray right under the board, apart from the caption
762panel at the bottom (grouped by interactivity: board and buttons, then the words): a ripple where it taps, a ring closing in on a
763long press, then the bomb's preview; a picked button lights up its tiles, a far tap shows
764auto-move's way. Turns take about 1–2 s. *Since 2026-10-24 (user request)* the player wears a
765green **"You"** tag just over their head, always (it wasn't obvious which moving figure is you), and each scene's finger
766appears just below-right of the player, where the eye should start. *Since 2026-10-24 (user
767request)* the **top bar shows** in the demo too (scenes start at turn 0 with no coins; rooms have
768par 20), and two scenes acted out on a still room follow the control scenes: a **top-bar tour**
769(the finger points at each item, ringed, the caption saying what it is) and **"What is that?"**
770(? then an enemy, through the real `inspect` and `tapTile`; the dialog opens, the finger
771taps Got it; in the demo the dialog opens over the scene only, not as a modal over the page, so
772the caption stays undimmed). The demo plays **on a CRT** (user request): LittleJS
773post-processing (`PostProcessPlugin`: colour fringes and static) on the board, a CSS layer of scan
774lines, a rolling dark bar and sweeping bright lines, vignette, rounded glass and bezel, and every
77512–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
776too), at most 4:3 with black bars; the finger is in front of the glass. **Five control scenes** come first (step, long-press
777bomb, Bomb button, Wait, auto-move with Go), and again each time the living scenes have all been
778shown. Captions of Run, Kick, Throw, Teleport, Diagonal and the Detonator say how to do them.
77941 kinds; on 40 seeds each, all found, the slowest 0.5 s (made in a worker). *Since 2026-10-24
780(user request)* the random play is **sensible** outside the scenes about dying: no flame on an
781uncovered exit or gem (except in the scene about that), and a bomb only where its blast reaches a
782crate, 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,
783or in one room in six already uncovered (locked while enemies live). *Since 2026-10-24 (user
784request)* 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
785order**, looped (`DEMO_ORDER` in main.ts: the controls' seven, then the table's living scenes with
786a way to die after every four); the panel numbers them (n / 47 since the new power-ups; 43 at first), ◀ ▶ and ← → step through them,
787↺ goes back to the first, and the place is kept in `localStorage` so the demo carries on there
788next time, after a reload too (not on a test URL, which starts at the first). Scenes are made in
789a Web Worker (`ui/scenes.ts`), the next while one plays. Ideas: a gem's choice, and showing the
790demo again after the player is idle on the menu.
791
792## 13c. Animated clips in the info dialogs (user request, 2026-10-24; planned)
793
794Every info dialog gets a small looping clip of what it describes, made on the spot like the demo's
795scenes (`sim/demo.ts`): a small room, random play by the real rules until the moment shows, the
796last few turns replayed. Each loop holds its moment for **1 s**, then starts over **with a new
797setup** (a new seed), for variety. **No CRT** (that is the demo's joke). **The finger** only where
798it helps: the action buttons and the power-ups you work yourself (Run, Kick, Throw, Teleport,
799Diagonal, the Detonator). **Reduced motion:** a still of the moment wherever one frame says it
800(a bomb at 1 with its blast previewed, a Nuke's shape, the exit with its three enemies coming);
801for what is about moving (the enemies' rules), a still with the arrows.
802
803**What gets a clip** (an existing demo scene where there is one; *new* otherwise):
804
805- *Action buttons* ("What is that?" on them): Move (`tapMove`, or with Run / Teleport / Kick /
806 Diagonal held, theirs), Bomb (`holdBomb`, `bombButton`; with Throw or the Detonator, theirs),
807 Wait (`wait`).
808- *Top-bar items:* lives (*new*: hit, a heart empties, the level starts over the same), level
809 (`exit`), enemies left (*new*: the last one dies, the count reaches 0, the exit opens), coins
810 (`coins`), turn and par (*new*: the count ticking), bombs (`bombPlus`), fire (`firePlus`), fuse
811 (`fusePlus` / `kill`), Star (`star`), each pass held (its scene), each charge held (its scene,
812 the charge already held), the modes (Developer: the rainbow gem's offer; Immortal: unharmed by
813 a blast).
814- *Tiles* ("What is that?" on the board): a bomb (its countdown and blast; a Pierce / Power / Nuke bomb its shape),
815 the decoy (`decoy`), still burning (`lingering`), a crate (*new*: burst, uncovering a drop, the
816 gem or the exit), a pillar or the outer wall (*new*: a blast stopped by it), the gem and the
817 rainbow gem (`gem`), the exit locked (`doorBlast`) and open (`exit`), coins or a charge on the
818 floor (picked up; a charge also doing its thing). *The enemies get no clip, only their picture*
819 (user's call, 2026-10-24: the first try, the `enemies` scene, showed a busy room with all three
820 kinds; the player learns how they move soon enough) — on the board and in the almanac alike.
821- *Other dialogs:* the first-pickup dialog of each charge (its top-bar clip), the gem's offer (a
822 clip per card, perhaps only the card in focus), the almanac's entries (perhaps only one opened),
823 and the "Ouch!" banner (the matching death scene, or a replay of the player's own last turns).
824Clips should keep their rooms simple: few crates and as few enemies as the moment needs.
825
826**How they are drawn** (decided 2026-10-24): one LittleJS canvas, drawn twice a frame while a
827dialog with a clip is open — first the clip's room, drawn at an offset in world space where the
828camera already looks (LittleJS builds the camera's transform only at the start of a frame), copied into a small
829`<canvas>` in the dialog, then cleared and the game as usual — so clips use exactly the board's
830drawing and animation code. Fallback, if a copy in the middle of a frame doesn't work: LittleJS's
8312D drawing straight into the dialog's canvas. It needs the board's view state (effects, flames,
832the player's move and pose, enemy steps, …), now module variables in `main.ts`, to become an
833object, one for the game and one per clip; and scene generation (up to ~0.45 s) to move to a
834**Web Worker** (the sim is pure), so the next loop's setup is ready in time — the demo can use it
835too.
836
837**Order:** 1. a spike proving the two-pass drawing and copy (*done 2026-10-24*: `ui/clip.ts`;
838"Still burning" plays the `lingering` scene as a first clip); 2. *done 2026-10-24*: the board's
839animation state is one object (`render/animate.ts`: `Animation`, `animateTurn`), one for the game
840and one per clip, and scenes are made in a Web Worker (`sim/scene-worker.ts`, `ui/scenes.ts`; on
841the page where a worker can't be made), the next loop's while one plays — the demo's too; 3. the
842clips that reuse demo scenes (*done 2026-10-24*: each `Info` names its scene, `clip`; the finger in
843the clips about the actions and the hand-worked power-ups, `FINGER_KINDS` in ui/clip.ts — Wait
844there is a tap on the player, as no action bar is in a clip; charges are still picked up first); 4. the
845new scenes (*done 2026-10-24*, clip-only, `clipOnly` in sim/demo.ts: `lastEnemy` for enemies left,
846`crate`, `pillar` for walls; lives play the `dieEnemy` scene; turn and par, which only a number
847shows, and the modes have none); 5. the offer's cards,
848the almanac and the "Ouch!" replay (*done 2026-10-24*, the user's choices: one clip above the
849offer's cards, playing the card selected — *since 2026-10-24 (user request) a tap on a card
850selects it and Choose takes it*;
851almanac entries with a clip are buttons that open it in their own dialog, Back returning; the
852banner replays the player's own last 4 turns of the attempt, a demo room's worth of the level
853round where they died). Clips can now be several at once, fixed scenes or made each loop.
854*Also done 2026-10-24:* the modes' tag plays `rainbowGem` (Developer) or `immortal`, and under
855reduced motion every clip is a still of its moment (the scene played to its end, held).
856
857## 13b. Run options (user request, 2026-10-21; built)
858
859Chosen in the **New run** dialog (menu, or after game over / victory), remembered in
860`localStorage` for the next new run, fixed for the run, stored in its record (`RunRecord.options`)
861and state (`GameState.options`), so replays are exact. Shown as tags in the top bar and in the menu.
862
863*Since 2026-10-23 (user request) only two options remain, Developer and Immortal:* the board
864size and Crumbling crates are gone (one 9 × 11 board, §11; crates always crumble). URL test
865parameters: `?dev`, `?immortal`.
866
867| Option | Rules |
868|---|---|
869| ~~**Board: Normal**~~ | *Removed 2026-10-23.* 31 × 13, density 35%, 50 levels, enemies 3 + 1 per 3 levels (max 10), par 50 + 10/enemy. |
870| ~~**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. |
871| **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.) |
872| **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). |
873| ~~**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. |
874
875URL test parameters `?dev` and `?immortal` set them for a test board.
876
877---
878
879## 14. Open questions
880
8811. ~~**Workshop banking**~~ — no Workshop (dropped 2026-10-24, §7).
8822. ~~**Workshop deposits**~~ — no Workshop.
8833. ~~**Fresh start and the level**~~ — no Keep / Fresh (dropped 2026-10-24, §9).
8844. **Skulls with coins:** should a skull also give coins, making it a risk-for-reward choice
885 rather than only something to avoid? *Recommendation:* yes, a small amount.
8865. **Level width ramp:** start narrower (e.g. 15 columns) and grow to the full 31 by ~level 10,
887 so early levels are short phone sessions? *Recommendation:* yes.
8886. **Shared / daily seeds:** seeds already make runs reproducible. Add a "daily seed" everyone
889 plays the same day? *Recommendation:* later milestone, cheap to add. *2026-10-24:* seeds can be
890 typed in (§12); the daily dungeon is planned in §12b.
8917. **Certificate strength:** replay-verifiable (§11, recommended; nearly free since the sim is
892 deterministic) or a simple checksum?
8938. **Board theme:** ~~light "paper" board or dark board?~~ Settled by the Kenney art
894 (2026-10-20): a pale sky-blue board under dark bars, coloured as the sprites are.
8959. **Exclusive power-ups (§5b):** slots instead of collecting everything? How many, and what
896 takes one? Needs answers before the shop is built, since it depends on it.
89710. **Enemies sharing a tile:** they do now, as in the NES original (as far as remembered). Should
898 they block each other instead?
899
900---
901
902## 15. Notes for the implementer
903
904- **Read the user's `CLAUDE.md` rules.** In particular: **never commit or push** until the user
905 explicitly confirms they reviewed screenshots and ran the app. Docs must be updated as part of
906 each feature (the README lists every action, pickup, curse and enemy). Run the tests often.
907- **Verify the LittleJS API against the installed `dist/littlejs.d.ts`**, not memory. Already
908 confirmed in the pinned 1.25.0: `engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender,
909 gameRenderPost, imageSources, rootElement): Promise<void>`, `cameraPos`, `cameraScale`,
910 `drawTile(… color, angle, mirror, additiveColor …)`, `drawTextScreen`,
911 `setTouchInputEnable`, `drawRect`, `drawCircle`, `drawText`, `screenToWorld`,
912 `setCanvasPixelRatio`, `setGLEnable`. Still check: `tile()`, `TileLayer`, `zzfx`.
913- **The game uses LittleJS's WebGL renderer** (Canvas 2D from 2026-10-19 to 2026-10-24), and handles taps with
914 pointer events on the canvas — see `decisions.md` for why.
915- **LittleJS world space is y-up**; the sim grid is row 0 = top. Convert in exactly one place
916 (`render/board.ts`), e.g. `worldY = (rows - 1) - row`.
917- **Fit the board:** `cameraScale` = canvas height available between HUD and action bar ÷ 13 rows;
918 recompute on resize/orientation change.
919- **Keep LittleJS out of `sim/`.** If a test needs to import LittleJS, the boundary is wrong.
920- **Record the run from day one.** Every input that changes state — actions, shop purchases,
921 rerolls — goes through `sim/` and is appended to the run
922 record. That one log gives mid-run save, replays and certificates.
923- **Test priorities** (Vitest): turn phase order; bomb timer semantics (not ticked on placement
924 turn); chain reactions; flame stops at walls and destroys the first soft block; Pierce/Power;
925 Run needs both tiles free; Kick slide stop; enemy blocked by a bomb placed this turn stays put;
926 contact checks including swaps; levelgen invariants across many seeds; danger overlay matches
927 what `step()` actually does (predict → step → compare); **replay determinism** (record a random
928 run, replay it, identical final state and hash); curses never before
929 the first shop.
930- **Screenshots:** Playwright at a phone viewport (e.g. 390×844) and desktop, with URL params
931 like `?seed=123&level=1` for reproducible scenes. The user reviews them before any commit.
932- **Start simple:** labelled shapes until icons arrive; no animation polish until M7. *(The
933 player's animation — idle bob and glance, walking slide, crouch, jump, hit — came early, at
934 the user's request, 2026-10-21, and enemies got the same hop the same day. Everything else
935 on the board got its animation on 2026-10-22, also at the user's request: decisions.md.)*
936
937---
938
939## 16. Architecture
940
941Golden rule: **all game rules are pure TypeScript with zero LittleJS imports**, deterministic
942given a seed, and fully unit-testable.
943
944```
945src/
946 sim/ pure, deterministic, no DOM, no LittleJS
947 types.ts Vec, Dir, Tile, GameState, Event, ActionId …
948 rng.ts seeded PRNG + per-purpose streams (§12)
949 state.ts create/clone state, grid helpers
950 levelgen.ts seeded level generation (§11)
951 actions.ts registry: { id, isAvailable(state), validTargets(state), apply(state, target) }
952 picks.ts what a tap means with Move or Bomb picked (they stand for groups of actions)
953 turn.ts step(state, action, target) → { state, events[] } — the 5 phases of §1
954 explosions.ts blast shapes, chain reactions
955 enemies.ts intent planning per enemy type
956 danger.ts predicted flame tiles this turn / next turn
957 path.ts auto-move's way, safe steps, `playerPath` (shared by auto-move, the route preview and the autopilot)
958 autopilot.ts the autopilot (Q73): targets, the cheapest order, the next turn; waits for crates and bombs
959 testScenes.ts the Developer-mode test scenes (a board and a state for each edge case)
960 curses.ts
961 shop.ts offers, prices, purchase
962 record.ts run record, replay, hash, SIM_VERSION
963 render/ LittleJS
964 board.ts sprites, highlights, danger overlay, intents
965 sprites.ts which Kenney sprites the board loads (§13)
966 effects.ts turns a turn's events into short animations (appear, slide, swell,
967 go away); no input lock — see decisions.md 2026-10-22
968 camera.ts the view: follows the player, drag/look pan, `revealTile` keeps the controller's target on screen
969 reveal.ts controller target: how far to move the view to keep a tile on screen (pure)
970 ui/ DOM: action bar, HUD, shop, death choice, certificate, About
971 padMap.ts controller: gamepad snapshot to intents (pure; decisions.md 2026-11-01)
972 padHold.ts controller: hold a face button until a ring fills, act when the ring fills (pure; decisions.md 2026-11-02)
973 holdRing.ts controller: the ring element moved over the held button's picture
974 padPlaces.ts controller demo: where each part sits on the controller picture, per family (pure)
975 padLayouts.ts controller demo: the measured positions (GENERATED by tools/make-pad-layouts.py; do not edit)
976 demoInput.ts the demo's input button: touch, mouse, keyboard or a pad family, and what the demo is shown for (pure)
977 spatial.ts controller: which element to focus next in a direction (pure)
978 gamepad.ts controller: reads the pad each frame, routes intents to main.ts (stage 2)
979 padTarget.ts controller: the first target, and jumping between targets (pure)
980 padGlyph.ts controller: which button picture goes on each action button (pure)
981 targetLabel.ts controller: the one-line label of what confirming does (pure)
982 padTiles.ts controller: actionable tiles, what each button does on a tile, cursor moves (pure)
983 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)
984 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)
985 padDialogMap.ts controller in dialogs: footer buttons to face buttons, which action a face button does (pure)
986 padDialog.ts controller in dialogs and the banner: DOM glue (selector, pictures, tabs, scroll; stage 3)
987 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)
988 padDemo.ts controller demo: a turn as pad presses, cursor path, pad scene order and still-scene captions (pure)
989 keyDemo.ts keyboard demo (Q65 B): a turn as the fewest key presses, replayed through the game's rules; key captions, scene order (pure)
990 keyOverlay.ts keyboard demo: the key overlay, Kenney's keyboard glyphs on a CSS grid (DOM)
991 audio.ts Kenney pack sound effects (§13); ZzFX for any it lacks
992 persist.ts localStorage save/load, export/import
993 main.ts
994assets/kenney/ Kenney New Platformer Pack sprites + License.txt (§13)
995static/ Vite publicDir (NOT public/ — see §17)
996test/ Vitest
997tools/ compose-icons.sh (composed sprites and the app icons), screenshots.mjs (Playwright),
998 make-pad-layouts.py (the controller pictures and ui/padLayouts.ts), audit-art.mjs (which art is byte-identical to Kenney's)
999```
1000
1001- `step()` **returns events** (`moved`, `bombPlaced`, `bombTicked`, `exploded`, `blockDestroyed`,
1002 `enemyDied`, `dropSpawned`, `pickedUp`, `cursed`, `playerDied`, …). The renderer only plays
1003 events back.
1004- `step()` does not mutate its input (clone, then mutate the clone) — makes previews and the
1005 danger overlay safe.
1006- Adding an action = one registry entry + tests + an icon.
1007
1008---
1009
1010## 17. Tooling and deploy (copy from `~/code/nordic-skating`)
1011
1012- `package.json` scripts: `dev` = `vite --host`, `build` = `tsc --noEmit && vite build`,
1013 `test` = `vitest run`, `screenshots`, `icons`.
1014- Dev deps as in nordic-skating: `typescript`, `vite`, `vitest`, `playwright`,
1015 `vite-plugin-qrcode` (scan to open on a phone), `@types/node`. `vite-plugin-pwa` arrives with
1016 the icons in M7.
1017- Dependency: **`littlejsengine`**, **pinned to exactly 1.25.0** (the newest release at least two
1018 weeks old on 2026-10-19). Upgrade deliberately. *Upgraded 2026-10-24 to 1.28.0* (the latest, 4 days
1019 old: the user's call, for the speed on Android Chrome; decisions.md: "Android Chrome at half a
1020 frame a second").
1021- A QR library for the certificate (e.g. `qrcode`, which `~/code/pretty` already uses).
1022- `tsconfig.json`: copy nordic-skating's (strict, `noUncheckedIndexedAccess`, …).
1023- `vite.config.ts`: `base: process.env.BASE || './'`, `publicDir: 'static'`, QR plugin, PWA
1024 (display fullscreen, icons, offline cache incl. the sprites). Read nordic-skating's file — its
1025 comments explain each choice. *Built 2026-10-23 (user request) without a PWA plugin:*
1026 `static/manifest.webmanifest` (display fullscreen), icons in `static/icons/` from
1027 `tools/compose-icons.sh`, and `static/sw.js`, a network-first service worker that keeps a copy
1028 of everything loaded (so the game starts offline) and registers only in the build.
1029- `.gitlab-ci.yml`: copy nordic-skating's (test stage, then `pages` with `mv dist public` and the
1030 "fail loudly" checks; add a check that the sprites were built). Project at
1031 `code.fuzzyworld.net/explodorama`, served at `https://apps.fuzzyworld.net/explodorama/`.
1032- Docs to create and keep current: `README.md` (how to play; every action, pickup, curse, enemy;
1033 Attribution), this `PLAN.md`, `decisions.md` (each decision + why — seed it
1034 from §0), `design.md` (balance tables: enemy schedule, prices, drop rates, payout formula).
1035
1036---
1037
1038## 18. Milestones
1039
10401. **M0 – Scaffold:** ✅ npm, Vite, TS, LittleJS, Vitest, `git init`, CI file, README with
1041 Attribution section. ⏳ GitLab remote, first commit and first Pages deploy — waiting on the user.
10422. **M1 – Sim core:** ✅ types, RNG streams, grid, Move, Wait, `step()` with all 5 phases
1043 (enemy phases are marked placeholders), run record + replay, tests.
10443. **M2 – Bombs:** ✅ adjacent placement, timers, blast, chains, soft blocks, death, lives.
10454. **M3 – Mobile UI:** ✅ action bar, two-tap targeting, highlights, danger overlay, commit preview
1046 (press-and-hold / hover), keyboard controls; Kenney sprites (2026-10-20). ⏳ No animation of moves yet
1047 (instant), only a flame flash.
10485. **M4 – World:** ✅ levelgen (31 × 13), side-scrolling camera, exit door under a soft block
1049 (opens when all enemies are dead; flaming it spawns 3 enemies), par + coin bonus (2026-10-20).
10506. **M5 – Enemies:** ✅ Blob, Seeker, Drifter with intent arrows, both contact checks, coin and
1051 charge drops, coins, charge inventory (buttons with counts), and all 11 charge actions
1052 (2026-10-20). Touch shortcuts from the user's playtest: tap self = Wait, hold = Bomb,
1053 tap far = auto-move (§3.1).
10547. **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),
1055 Run and Detonate, the passes and Star, 50-level progression and ending, mid-run save
1056 (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.)
10578. **M7 – Look & feel:** ✅ animations (2026-10-22: everything that appears, moves, changes or
1058 goes away on the board, the top bar's numbers, the banner, and dialogs opening and closing);
1059 ✅ installable with offline play (2026-10-23); ✅ the demo before the game (2026-10-23, §13a); ✅ sounds and vibration (2026-10-24, §13), ⏳ remaining
1060 enemies and actions, screenshots tool.
10619. **M8 – Records:** save export/import, victory certificate + QR, replay file and verify page.
1062 (The Workshop was here until 2026-10-24: dropped, §7.)
106310. **Later:** Reapers + turn limit, daily seed, final art.
1064
1065### Backlog
1066
1067- **Released and next copies (user request, 2026-10-31; built, not yet deployed):** see decisions.md and
1068 README "Two copies". To go live: commit, tag and push fuzzyworld-tools with `--as`, update
1069 explodorama's `fuzzyworld-tools` dependency, then `npm run deploy -- --as explodorama-next`.
1070- **Art provenance audit (user request, 2026-10-31):** check every file under `assets/` (and any
1071 art drawn in code, such as the oil highlight's ellipses in `tools/compose-icons.sh`) is either
1072 Kenney's own or produced by a script in the repo from Kenney's files; list everything made by
1073 hand or composed by Claude, with how, so the user can keep, redo or replace it. Also confirm the
1074 menu and README credits cover every pack used. Done 2026-11-01: [docs/art-provenance.md](docs/art-provenance.md)
1075 and `tools/audit-art.mjs`; the keep/redo/replace decisions are backlog (Q39).
1076- **A worn dungeon (user request, 2026-10-25; built, then switched off the same day):** *the user:
1077 the splats looked too alike, and not the look wanted* — `render/decor.ts` stays, off
1078 (`ENABLED`), until a new approach is chosen. The first idea: use the Splat Pack (approved) to make
1079 each level look dilapidated, old, tarnished, wet or dirty — stains, puddles, soot, moss, rust — in
1080 a random mix and amount drawn from the seed and level, so a level always looks the same and
1081 levels differ. Decoration only: drawn on the floor (and walls) under everything, never in the
1082 sim, never mistaken for a hazard (19.3's oil and acid spills must look clearly different: brighter,
1083 outlined, or animated). Ideas: a "theme" per level (damp: blue-grey puddles and green moss;
1084 scorched: soot; rusty: orange stains on pillars), denser in corners and along walls, lighter on
1085 paths; a few splats shared with the demo's rooms.
1086- *Built 2026-10-25 (user request):* **TNT**, a power-up worth two Fire +1s (blast +2; weight 6,
1087 price 7), and **limits on the stats**: bombs, blast and fuse each at most 5 (`STAT_MAX`). A stat
1088 power-up without room for all of its step isn't offered (`heldAlready`); its card shows the limit
1089 ("Blast 2 → 4 (max 5)"). Teleport's reach gets its own limit when it becomes a charge (next task).
1090- ~~**Next: Flame Pass and Teleport become charges.**~~ *Built 2026-10-25.* Flame Pass: raised with
1091 the charge button (a tap on yourself), it takes the next 3 fire hits (`flameHits`, an orange
1092 ring, kept from level to level). Teleport: 3 jumps from the charge button (key P too); no longer
1093 in the Move group, a tap's meaning, or auto-move, so nothing spends a jump but the player. Their
1094 power-up entries are gone (`powers.teleport`, `powers.flamePass`). Shop: 7 each. Was:
1095 **Flame Pass and Teleport become charges.** The lab measured both turning a weak player into a near-certain winner
1096 (§19.1). Decided by the user:
1097 - **Flame Pass → a charge counted in flame hits, not turns:** it saves the player from **3 fire
1098 hits** (a blast, a chain, lingering flame), one used up each time flame would have hurt them,
1099 then runs out. ~~Raised with the charge button first~~ — since later that day (user) it works
1100 while carried, its 3 hits being the charge's uses.
1101 - **Teleport → a charge of 3 jumps**, as ICBM has 3 launches, with its reach as now (Run adds a
1102 step; Diagonal makes it a square).
1103 - **Crate Pass and Bomb Pass stay power-ups for now**: the user wants to playtest them more (the lab
1104 finds them strong but in line with Run or Kick).
1105 - Then run the lab's `powers` experiment again on the same seeds, and the 3s are first guesses.
1106- **Decided (user, 2026-10-25): on a death, the carried charge drops where the player fell**, with
1107 what is left of it, as a drop the retry can pick up again (option 1 below). *Built 2026-10-25*
1108 (`Carry.fallen`; a charge drop can carry `left`; the nearest open floor if the player fell in a
1109 crate).
1110 Was: **Lose the carried charge on a death? (user idea, 2026-10-25.)** Today the charge
1111 carried goes through a death (§3.3). Dropping it would make a death cost more than the life and
1112 give a reason to use a charge rather than hoard it; but it also hits hardest the player who is
1113 already struggling, and the retry would start without the tool that might have saved them. A
1114 middle way: the charge drops on the tile where the player died, to be picked up again on the
1115 retry (the level restarts the same, so it is close by) — or only charges *picked up during this
1116 attempt* are lost, as the gem's choice already is. The balance lab can measure each.
1117- ~~**UI changes, queued (user, 2026-10-25)**~~ *Built 2026-10-25:* the demo's title is its own
1118 bar above the TV (the top bar and the TV move down under it; `--demo-title`), and the menu has
1119 Resume and New side by side over a row of four small buttons (`className: 'menu'`).
1120 1. **The demo's title on top:** "EXPLODORAMA!" moves to the very top, above the board, so the
1121 demo reads title, then the TV, then the caption. Once the game starts, both the title and the
1122 caption go.
1123 2. **A less packed menu:** Resume and **New** (was "New run") smaller and side by side; the other
1124 buttons smaller, as icons and/or single words — **Almanac**, **Scores**, **Demo**, **Bug**.
1125- ~~**Bug reports from players**~~ *Built 2026-10-24* (user request): GitLab Service Desk is on
1126 (the user turned it on; an email to its address becomes a confidential issue, no GitLab account
1127 needed, replies go back by email), and the menu's **Report a bug** opens an email to it, filled
1128 in with the version and build commit, the device, the seed, level, turn, lives and modes, the
1129 last 5 errors the game caught (`recentErrors` in devlog.ts), and the run record. A record too
1130 long for an email link (over ~1,900 characters) stays out of the email and goes on the
1131 clipboard instead, and the email asks to paste it.
1132
1133
1134---
1135
1136## 19. Proposed features (user, 2026-10-24; to discuss, then prioritise)
1137
1138Five ideas from the user, worked out into proposals, then settled with the user (decisions marked
1139in each part). *Agreed 2026-10-24; order: the balance lab (19.1) first.* Nothing here is built yet.
1140
1141### 19.1 A balance lab: measuring what an addition does to the game
1142
1143**The question:** some power-ups feel overpowered (Flame Pass, Crate Pass, Bomb Pass), but by how
1144much? And would they be fairer as charges? Today that is judged by feel.
1145
1146**Why not search the whole game.** The sim is pure and deterministic (`step()`), so any game can be
1147played out in Node at full speed, with no browser. But a turn has about 5–20 possible actions
1148(moves, a bomb on each neighbour, the abilities' targets), and a level takes 40–100 turns: the game
1149tree has more than 10^50 leaves. Plain DFS or BFS over whole games is out of reach. Search works
1150well *locally*, over the next few turns, and that is how it is used below.
1151
1152**Proposal: bots play thousands of games, and we compare the numbers.**
1153
1154- **Bot players** (`tools/balance/`), each a function from a state to an action, using the sim's
1155 own helpers (`predictDanger`, `playerPath`, `safeStep`):
1156 1. **Careful:** never steps into danger; walks to a crate or an enemy's path, bombs it, walks to
1157 the nearest safe tile; otherwise waits. Cheap, predictable — a cautious beginner.
1158 2. **Searcher:** a depth-limited search over the next 3–5 turns (beam search: keep the best 50
1159 states at each depth), scoring states by kills, crates burned, the exit's distance, and
1160 safety (how many safe tiles are reachable). This is where BFS / DFS fits: a few turns ahead,
1161 on the real rules, pruned by the danger overlay. A decent player.
1162 3. **Reckless** (later): bombs often, escapes late — finds where a power-up rescues a bad player.
1163
1164 Bots are not humans: a bot gains differently from a power-up than a person does. So the numbers
1165 are for *comparing* (this power-up against that one, before against after), not as truths.
1166- **Experiments, paired:** the same N seeds (say 500) played twice, once as the baseline and once
1167 with the change — a power-up held from level 1, a charge instead of the power-up, a different
1168 drop rate or price. The same seeds on both sides take out the luck of the boards, so a few
1169 hundred games show even small effects. Each experiment runs on all CPU cores (worker threads).
1170- **What is measured**, per level and over the run:
1171 - **Survival:** levels cleared before game over; lives lost per level, by cause (blast, chain,
1172 lingering flame, enemy).
1173 - **Pace:** turns per level, the share cleared within par, coins earned.
1174 - **Pressure:** turns spent standing in danger, close calls (out of a blast's reach on the turn
1175 it goes off), turns with only one safe move.
1176 - **Use:** how often the power-up or charge was actually used, and how often it saved a life
1177 (the same seed and turn without it would have died: a counterfactual replay).
1178- **Structural measures, no bot needed:** on many random boards, from the player's tile, count what
1179 a power-up changes outright. Examples: the share of bomb placements with an escape route
1180 (Flame Pass makes it 100%, which shows why it feels free); tiles reachable in 3 turns (Crate Pass,
1181 Bomb Pass, Run, Teleport); blast reach (Fire +1, Power). Deterministic and quick, a good first
1182 report before the bots exist.
1183- **The report:** a table per experiment — each power-up's effect on levels survived and lives lost,
1184 with a confidence interval, sorted strongest first — published as an HTML page (an artifact), and
1185 kept in `docs/balance/` so a change can be checked against the last run.
1186- **Power-up or charge:** the rules get variant switches the lab can turn on (for example
1187 "Flame Pass as a charge of 5 turns", "Crate Pass as 3 uses"), so the same experiment compares
1188 the versions before the game commits to one.
1189
1190*Decided (user, 2026-10-24):* **the full version, with bots** (the structural measures come along
1191as its first report). Still open: which power-ups to measure first — the passes, given 19.5.
1192
1193**Built (2026-10-25):** `tools/balance/`, run with `npm run balance -- <command>` (README, "The
1194balance lab"). Bots at three skill levels (`careful`: survives what it can see ahead, wins about
119580% of runs; `average` and `sloppy`: look 2 and 1 turns ahead and slip at times, clearing about 5
1196and 3 levels). Experiments `powers` (each power-up held from level 1) and `structure` (escapable
1197bombs, reach in 3 turns, safe actions, on 1,781 real positions). Reports in `docs/balance/`.
1198The bots don't shop, and take the first thing a gem offers.
1199
1200**First findings (2026-10-25, rules version 28):**
1201
1202- **Teleport and Flame Pass are in a class of their own.** For the average bot each turns 5 levels
1203 into 20 (wins 2% → 99%); for the careful bot both are the top two. Teleport triples the tiles
1204 you can reach in 3 turns (10 → 39) and lets you escape 19% more bombs; Flame Pass and a Star make
1205 23–25% more bombs escapable. Diagonal comes next (+8 levels for the average bot).
1206- **Some power-ups hurt a weaker player:** Fire +1 (−2.6 levels for the average bot: bigger blasts
1207 catch you), Fuse +1 (−1.7, and +24 turns a level), Slide (−1.1), Bomb Pass (−0.7; +6% of turns
1208 spent in danger).
1209- **Proximity alert costs the careful bot 3 levels** (wins 82% → 40%). Its bombs go off the moment an
1210 enemy is in reach, so the usual defence — a bomb dropped between you and an enemy coming down a
1211 corridor — blows up next to you instead. Enemies then catch the player in dead ends.
1212- **The Nuke trap:** 6% of careful runs stall because Nuke charges are forced onto every bomb, and in
1213 the closed start corner every Nuke kills the player — the level can't be finished. Fixed by 19.7's
1214 deliberate use.
1215- **Within par:** the careful bot clears 20–70% of levels within par; level 1 is the hardest (20%).
1216
1217**After one charge at a time and the Proximity change (2026-10-25, rules version 29),** same seeds:
1218
1219- **The Nuke trap is gone:** careful runs that stall fell from 6% to 1%; the careful bot clears
1220 18.8 levels (was 18.5) and wins 85% (was 82%), in fewer turns a level (53, was 56). The average
1221 bot is unchanged (5.4 levels).
1222- **Proximity alert** now costs the careful bot 0.7 levels (was 3.2) and wins 11% fewer runs (was
1223 42% fewer); for the average bot it still helps a little (+0.7).
1224- **Teleport and Flame Pass are still far ahead** (+14.5 and +14.3 levels for the average bot):
1225 untouched by these changes, and the next thing to decide (19.5: the passes leave the power-ups
1226 or become charges).
1227
1228**After Flame Pass and Teleport became charges, and the boards were fitted (2026-10-25, rules
1229version 35),** 150 seeds, now also each charge carried from level 1:
1230
1231- **Teleport and Flame Pass are tamed:** as charges, +0.2 and +0.3 levels for the average bot
1232 (they were +14.5 and +14.3 as power-ups) and about 0 for the careful one.
1233- **Diagonal is now the strongest thing** for the average bot: +8 levels (wins 0% → 10%), and the
1234 best for the careful bot too (+0.8). Also boosted by auto-move cutting corners with it. A
1235 candidate for the next look.
1236- **Kick** (+2.6) comes next for the average bot; everything else is within about ±1.3 levels.
1237- **Bigger blasts still hurt the weaker player:** TNT −2.3, Fire +1 −1.8, Fuse +1 −1.3.
1238- **No charge is far stronger or weaker than the rest** (−0.7 to +0.4 for the average bot). The
1239 bots use charges crudely (whenever their scoring likes it), so these say little about a person.
1240- The careful bot: 91% of runs won (82% under rules 28), about 84 turns a level, stalls 3%.
1241
1242**Diagonal without squeezing (2026-10-25, user's question; `npm run balance -- diagonal`, 300
1243seeds, a lab-only rule switch `options.lab.noSqueeze`):** a diagonal step then needs one of the
1244two tiles beside it open.
1245
1246| | Average bot | Careful bot |
1247|---|---|---|
1248| Diagonal as it is | +7.7 levels (wins +11%) | +1.0 levels (wins +8%) |
1249| Diagonal, no squeeze | **+3.4 levels** (wins +1%) | +0.3 (not significant), but 9% of runs stall |
1250
1251Most of Diagonal's strength is the squeeze: without it, it is worth about what Kick is (+2.6 for
1252the average bot). The careful bot's stalls rise (4% → 9%), probably its own pathing with the rule
1253rather than the game; to look at. *Decided (user, 2026-10-25): no squeezing is the rule* (`SIM_VERSION`
125436); the lab's `options.lab.squeeze` brings the old rule back to compare.
1255
1256### 19.2 Bigger boards for later levels
1257
1258> **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.
1259
1260
1261**Today:** every level is 9 × 11, the whole board on screen at once (a choice made 2026-10-23). It
1262suits the first levels; later ones feel small. *This changes that choice.*
1263
1264**First proposal** (*superseded by the decisions below*): boards grow with the level, mostly
1265taller, since a phone is tall:
1266
1267| Levels | Board | On a phone (390 × 844) |
1268|---|---|---|
1269| 1–3 | 9 × 11 (as now) | whole board |
1270| 4–6 | 9 × 13 | whole board, tiles a little smaller |
1271| 7–10 | 11 × 15 | scrolls up and down |
1272| 11–15 | 11 × 19 | scrolls |
1273| 16–20 | 13 × 21 | scrolls both ways a little |
1274
1275- **Tile size:** a tile must stay big enough to tap (about 36 CSS px at least). Below that the board
1276 scrolls instead of shrinking: the camera follows the player, eased, kept inside the board (the
1277 NES-style camera, removed 2026-10-23, comes back in a vertical form). A desktop shows more.
1278- **Off-screen enemies:** a small arrow at the screen's edge for each enemy out of view, and their
1279 count in the top bar as now, so nothing surprises the player from off screen.
1280- **Everything scales with the area:** enemies (about one per 20 open tiles), crates, par, and the
1281 number of new objects from 19.3. Auto-move matters more on a big board.
1282- **Generation keeps its guarantees** (every tile reachable once crates are gone, the exit and the
1283 gem under crates, nothing near the start), checked by the existing many-seed tests.
1284
1285*Decided (user, 2026-10-24), replacing the table above:*
1286
1287- **A fixed view, the same everywhere.** The game shows a window of the board with a set number of
1288 tiles — proposed **9 × 13** (a portrait shape, about the phone's) — on a phone and on a desktop
1289 alike, with bars beside it on a wide screen (as the demo's TV has). *9 × 13 agreed (user,
1290 2026-10-24).* Everyone sees the same
1291 amount of board, so no screen gives an advantage, and a level plays the same everywhere.
1292- **Boards bigger in both directions**, growing past the view; the view follows the player,
1293 eased and kept inside the board.
1294- **Indicators** at the view's edge for what is out of sight: each enemy (an arrow in its colour,
1295 nearer ones bolder), and the exit and the gem once uncovered. Lit bombs and fires near the edge
1296 too, since they can kill.
1297- **Size by "concentration"** (the user's idea, worked out below), instead of a size table.
1298
1299**Built 2026-10-25** (`boardSize`, `concentration`, `ENEMY_THREAT` in sim/levelgen.ts;
1300render/camera.ts; `drawIndicators` in render/board.ts):
1301
1302- **Sizes:** 9 × 11 (levels 1–3), 11 × 13 (4–6), 11 × 15 (7–9), 13 × 17 (10–12), 13 × 19 (13–15),
1303 15 × 21 (16 on). Odd, so pillars meet the walls evenly.
1304- **The view:** 9 × 13 tiles on every screen; `#view-mask` bars cover the rest (and taps there do
1305 nothing). The demo's small rooms are still seen whole.
1306- **Following:** the view centres on the player, eased, kept inside the board; a board narrower or
1307 shorter than the view is centred that way.
1308- **Indicators:** for each enemy, bomb, and the uncovered exit and gem out of view, a small picture
1309 just inside the view's edge on the way to it, with an arrow on.
1310- **Enemies:** added (random types of those met so far) until their threat reaches the level's
1311 concentration × open tiles, at least 2. Concentration rises linearly from 1/26 on level 1 (so its
1312 51 open tiles get 2 Blobs, as before) to 1/11 on level 20, then by 0.002 a level toward 1/8.
1313 Threat weights (first guesses, to fit with the lab): Blob 1, Seeker 1.4, Drifter 1.1, Sprinter
1314 1.6, Ghost 1.7, Wisp 1.3, Hunter 2. Level 20 gets about 12 enemies on 193 open tiles.
1315- **Par** = 10 + 0.3 × open tiles + 8 × enemies (41 on level 1, as before).
1316- **Fitted with the lab (2026-10-25):** with boards up to 15 × 21 the careful bot took 100–145 turns
1317 on late levels; the sizes are now 9 × 11 (1–3), 11 × 13 (4–7), 11 × 15 (8–11), 13 × 15 (12–15),
1318 13 × 17 (16 on), which take it about 75–95 turns throughout. Concentration rises to 1/9 by level
1319 20 (not 1/11): sparser late levels left the bot hunting the last enemy for a long time (its stalls
1320 went from 3% to 8%). The threat weights are still guesses. `SIM_VERSION` 32, then 35.
1321
1322**Concentration: one number that sets how crowded a level is.** Today each level's enemy count is
1323a table (2, then one more every 4 levels). Instead:
1324
1325- A level's **threat** is the sum of its enemies' weights, each type weighted by how dangerous it
1326 is (a Blob 1, a Seeker 1.5, a Hunter 2.5 …; first guesses, then measured by the balance lab:
1327 how often each type kills the bots).
1328- **Concentration** = threat ÷ open tiles (the floor plus crates: everywhere an enemy could
1329 eventually be). It rises with the level along a curve, for example from 1 per 25 tiles on level
1330 1 to 1 per 12 by level 20, then more slowly (for endless Gauntlet).
1331- The board size comes from the level; the enemies come from the size × the concentration. So a
1332 bigger board brings more enemies, but the same crowding — and crowding goes up with the level.
1333 Crates, hazards (19.3) and drops scale with the area the same way, each with its own density.
1334- **Fitting the curve:** the balance lab plays bots through levels and adjusts the concentration
1335 curve until the bots' chance of clearing each level follows a chosen difficulty curve (say 95%
1336 on level 1, 60% on level 20). Then "harder" means measurably harder.
1337- **How big a board gets** is then a question of time, not crowding: turns per level grow with the
1338 area. Proposed: grow until a level takes about 3–4 minutes for a bot at a human's pace (roughly
1339 80–120 turns), which the lab can measure; a first guess is about 15 × 21. Smaller boards stay
1340 for the first levels and as a pause between big ones.
1341
1342### 19.3 More on the board: hazards and objects
1343
1344**Goal:** more to plan around than crates and pillars. Every object follows the readability rule
1345(§4): visible from the start, behaving the same every time, in the danger overlay when it can
1346hurt, explained by "What is that?" and in a new almanac tab, **Board**. New ones arrive a few
1347levels apart, as enemies do, so each is met on its own first.
1348
1349Sprites: mostly from the New Platformer Pack, already used. Spills from the **Splat Pack**: 36
1350white splats that can be tinted to any colour (oil, acid, water). *Approved (user, 2026-10-24),
1351and all the objects below are wanted.*
1352
1353| Object | What it does | Flame | Sprite | Proposed |
1354|---|---|---|---|---|
1355| **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 |
1356| **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 |
1357| **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 |
1358| **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 |
1359| **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 |
1360| **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 |
1361| **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 |
1362| **Conveyor** | Each turn it carries whatever is on it (you, bombs, drops, enemies) one tile along. | — | `conveyor` | second |
1363| **Ice** | Whoever steps on it slides until something stops them, like Slide; kicked bombs too. | Flame melts it to water. | `snow`-coloured tile | later |
1364| **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) |
1365| **Spring** | Throws whoever steps on it 2 tiles on, over anything. | — | `spring` | later |
1366
1367**Should bushes hide what is in them?** (The user asked for the options, 2026-10-24.)
1368
1369| Approach | For | Against |
1370|---|---|---|
1371| **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. |
1372| **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. |
1373| **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. |
1374| **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. |
1375| **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. |
1376
1377*Decided (user, 2026-10-24):* **B + D, with E for enemies.** Bushes hide drops (something to
1378find) and hide the player from chasers (something to use), while an enemy inside shows faint,
1379arrows still visible — so nothing that can kill you is ever hidden.
1380
1381*Built 2026-10-25 (the first five; `SIM_VERSION` 37):* from level 2 bushes, 3 metal crates, 5 oil,
13828 spikes, 11 oil barrels (`OBJECTS_FROM`, first guesses), from their own random stream after the
1383rest of the level, so crates and enemies are where they were. As built, where the table above left
1384it open: a burning bush is flame that turn, and the bushes touching it **smoulder** and catch the
1385next; oil burns once (then it is gone) and lingers a turn like flame; a barrel's blast is 2 and its
1386spill covers its tile and the floor round it; it shows a 1 and shakes; **enemies never step on
1387spikes**, up or down (simpler to read than planning round the raised ones, and spikes then never
1388hurt them); a metal crate is placed only where every open tile stays reachable, and Kick doesn't
1389move it. Sprites: `bush`, `spikes`, the strong crate in steel grey (`block_metal`, composed),
1390`block_strong_danger` for the barrel, a near-black glossy splat for oil. Death causes `spikes` and
1391`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)
1392costs a life. *Also built the same day:* the almanac's **Board** tab (crates,
1393pillars, the five objects and rubble, with their first levels and clips) and a demo scene for each
1394object (a bush hiding you, a bush fire, metal stopping flame, oil, spikes, a barrel). Not yet: the
1395spikes' turn count.
1396*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.
1397
1398### 19.4 The Acid bomb: weakening pillars, bringing the roof down
1399
1400**Idea:** a bomb that eats into the pillars (the fixed grid of walls), so that enough damage brings
1401part of the ceiling down — a way to reshape the board, with a risk.
1402
1403- **The Acid charge**, like Pierce or Power: acid bombs (green; how many a charge gives, to tune —
1404 the first guess 1). An acid bomb's flame works as usual and also **cracks** each pillar it
1405 touches (a pillar stops flame, so it is the first pillar in each direction). Acid also leaves an
1406 acid puddle (19.3) on its own tile for a few turns.
1407- **Cracked pillar:** cracked and green-stained. A **second hit** — any later blast that reaches it
1408 — **breaks** it. *Decided (user, 2026-10-24): one acid bomb cracks, a second hit brings it
1409 down.*
1410- **A broken pillar brings the roof down:** the 8 tiles around it show falling dust and a
1411 countdown of **4 turns** (*user, 2026-10-24*; like a bomb's timer, and in the danger overlay). Then rubble falls:
1412 anything still there is crushed — the player, enemies (a trap to set), drops, bombs — and the
1413 tiles become **rubble: impassable for good** (no blast, Pierce or Crate Pass gets through), so
1414 the board can't be opened up into one big room. The pillar's own tile becomes floor.
1415 *Decided (user, 2026-10-24).*
1416- **Two new ways to die** (*user, 2026-10-24*):
1417 - **Crushed:** standing under the rubble when it falls.
1418 - **Trapped:** rubble can wall the player in. When the player can never again reach the exit or
1419 every remaining enemy — not even by burning crates, only walls, metal crates and rubble
1420 counting as permanent — the level can't be won, so it costs a life at once, rather than
1421 leaving the player stranded. (An enemy walled off from the player, but reachable by flame,
1422 still counts as reachable; the exact test is to settle when building.) The death banner
1423 names it, and the "Ouch!" replay shows the fall.
1424- **Floor collapse instead** (an alternative, or later): acid on a floor tile makes it cracked
1425 floor (19.3), which becomes a pit after one crossing.
1426- The outer wall never breaks.
1427
1428Still to settle when building: whether the second hit must be any blast (as written) or another
1429acid bomb, and how many acid bombs a charge gives.
1430
1431*Built 2026-10-25:* **any later blast** breaks a cracked pillar (one cracked before that turn; an
1432acid flame and a second blast in the same turn only crack it), **1 bomb** per charge (price 6),
1433in the gem's offers, the shop and enemies' drops like the other charges. The ceiling never falls on
1434the 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
1435enemy, unreachable from the player through all but walls, metal and rubble (flame can't cross those
1436either, so "reachable by flame" is the same test). Not yet: the acid puddle the bomb leaves (it
1437comes with 19.3's second batch), the floor-collapse alternative.
1438*Changed 2026-10-26 (user request): **Acid is an area weapon, like the Nuke.** Its flame burns
1439every tile within `NUKE_RANGE` (2) steps, through anything (the Nuke's diamond, on top of its
1440cross), and it cracks (or, cracked before, breaks) **every pillar in that diamond**, through
1441anything, not just the first pillar an arm of the cross runs into: a pillar diagonal to the bomb
1442was left untouched. The user chose the full Nuke area over cracks-only-in-area. `SIM_VERSION` 40.*
1443
1444### 19.5 Gauntlet mode
1445
1446**Idea:** a second way to play. You start with a random loadout and nothing more comes: survive as
1447many levels as you can, each harder than the last.
1448
1449- **Choosing it:** the New run dialog gets a choice of mode — **Classic** (the 20-level run) or
1450 **Gauntlet**. Developer and Immortal stay as options; an Immortal gauntlet would never end, so
1451 Immortal is off for it (or its runs don't count).
1452- **The loadout:** drawn from the seed, so the same seed gives the same start (and a daily
1453 gauntlet later is easy). To keep it fair, it is drawn to a **budget**, using the shop's prices as
1454 value: for example 3 power-ups and 2 stacks of charges and a stat or two, worth about 40 coins in
1455 all, so no start is far luckier than another. Later: picking it yourself (a draft — choose 1 of 3,
1456 a few times).
1457- **Nothing more comes:** no gems, no coins and no shop, and enemies drop nothing. The charges
1458 you start with are all you get, so each one is a decision.
1459- **Hearts:** one life back for every level cleared, up to the starting 3. Hearts are the only
1460 thing that comes back. *Decided (user, 2026-10-24).*
1461- **Endless** (*decided, user, 2026-10-24*), **harder each level:** more enemies, faster types sooner (then the Reaper, once it
1462 exists), bigger boards (19.2), more hazards (19.3). From the 20 levels of Classic it carries on
1463 past them, the ramp continuing (enemy count, then hazard density).
1464- **Its own high scores:** a separate board, ranked by levels cleared, then by fewer turns; each
1465 entry shows its loadout and seed.
1466
1467- **No passes in a loadout** (*decided, user, 2026-10-24*): Flame Pass, Crate Pass and Bomb Pass
1468 never come in a gauntlet start.
1469
1470**The passes in the whole game** (*user, 2026-10-24*): they should either **leave the power-ups**
1471or **become temporary charges** (for example Flame Pass for 5 turns). Which one is the balance
1472lab's first experiment (19.1): the passes as they are, as charges of a few lengths, and gone,
1473compared on the same seeds.
1474
1475### 19.7 One charge at a time (user, 2026-10-24; built 2026-10-25, see §3.3)
1476
1477**Idea:** the player carries **only one charge**. Picking up another opens a choice: **sell** the
1478new one for a few coins and keep what you have, or **swap**: take the new one instead. Every
1479pickup becomes a decision, and charges no longer pile up (three Nukes and a Pierce on one bomb).
1480
1481**Proposal, worked out:**
1482
1483- **The slot** holds one charge and what is left of it: Pierce, Power or Nuke with its bombs left
1484 (3 when picked up), ICBM with its launches, or a Shield. The top bar shows it in one place.
1485- **Freeze and Decoy become carried charges** (*decided, user, 2026-10-24*): no longer used the
1486 moment they are picked up, but held in the slot like the rest and used with the charge button when
1487 the player chooses (a Freeze on the spot; a Decoy on a tile picked next to the player, or on the
1488 player's own tile).
1489- **Picking one up with the slot empty:** it goes in, as now.
1490- **The same kind as the one held:** it adds to it (3 more Pierce bombs), no question asked.
1491- **A different kind:** a dialog, like the gem's offer, taking no turn:
1492 - **Sell it:** keep what you hold; the new one is sold for coins.
1493 - **Swap:** the new one goes in the slot; the old one is **sold** (what is left of it) — *decided,
1494 user, 2026-10-24* — since leaving it on the floor would ask again at once.
1495 The sale price: half its shop price (§8), rounded up, scaled by what is left of it — 2 to 3 coins.
1496- **The shop:** buying a charge while holding another asks the same, or simply swaps (the old one
1497 sold).
1498- **Gauntlet (19.5):** a loadout starts with one charge.
1499- **Deliberate use** (*decided, user, 2026-10-24*): the Bomb button places a **plain** bomb, and a
1500 second button (the charge's icon, with its count) uses the charge — a charged bomb for Pierce,
1501 Power and Nuke, a launch for ICBM, a Freeze, a Decoy. A Shield stays passive (it works when hit). That would also
1502 fix the Nuke trap the balance lab found (a Nuke forced on every bomb in the closed start corner
1503 kills the player whatever they do, so the level can't be finished), and give the player the choice
1504 of when to spend a charge.
1505- `SIM_VERSION` goes up; the balance lab compares the game before and after (survival, coins, how
1506 often each choice is taken by a bot that sells or swaps by a simple rule).
1507
1508*All three questions settled by the user, 2026-10-24.* Built right after the balance lab's first
1509report, so the lab can compare the game before and after on the same seeds.
1510
1511### 19.6 How these fit together
1512
1513- **19.1 first** helps the rest: it can tell whether the passes need nerfing, test a gauntlet
1514 loadout's budget, and tune hazard density.
1515- **19.2 before 19.3:** more objects need more room. The concentration curve (19.2) is fitted
1516 with the lab's bots, so 19.1 comes first there too.
1517- **19.4 needs 19.3's** acid puddles and (for the alternative) cracked floor.
1518- **19.5 uses all of them** for its ramp, but could start with what exists today.
1519
1520## 20. Bug reports as animated replays (Q104; planned 2026-11-03, nothing built beyond §20.1)
1521
1522*A planning section. The server side (what receives, stores and links reports) is in
1523`~/code/fuzzyworld-tools/PLAN.md`, "Issues" → "Report replays". This section is Explodorama's part.*
1524
1525**Idea.** A bug report already holds the whole run (`Run record: {…}`, the seed and the actions; see
1526`reportBug` in `main.ts`). The game can replay a record exactly, so an issue page on code.fuzzyworld.net
1527could show *what the reporter saw* as a short looping animation, and (for autopilot or rules bugs) what
1528today's game does instead. A report then answers itself: no need to reproduce by hand.
1529
1530### 20.1 Built (2026-11-03, dev only; committed 351348d)
1531
1532- `npm run report-replay -- <mail>` reads a report's record (quoted-printable mail, a pasted text or a
1533 bare record) into `replays/<name>.json` and prints page addresses.
1534- `?replay=<name>`: two players on one board, red = the recorded turns, blue = the same until the
1535 autopilot took over and then today's autopilot (`sim/replayCompare.ts` `buildReplay`).
1536- `&rules=<commit>`: the whole run under an older commit's rules (red) and today's (blue)
1537 (`sim/ruleCompare.ts` `compareRules`; the old `src/sim` is taken out of git into `rules-versions/`).
1538- The page takes no input and is removed from builds (`import.meta.env.DEV`).
1539- The Q101 report (autopilot walking into its own bomb with Bomb Pass) is the first fixture
1540 (`test/fixtures/q101-bomb-pass.json`).
1541
1542### 20.2 What a published replay needs from the game (to do, in order)
1543
15441. **A production replay entry** (`replay.html` beside `index.html`, its own small bundle, same-origin
1545 script only, so the site's CSP `script-src 'self'` holds). It takes `?report=<same-origin json path>`,
1546 plays the record in a loop, takes no input, saves nothing, records no scores. The dev page's code
1547 (`replayTick`, the `rival` second player, labels) moves out of `main.ts` into a module both use; the
1548 dev page stays as a thin wrapper that adds `?rules=`.
15492. **Treat the record as hostile data.** It comes from a mail. Limits before anything runs: size at most
1550 64 KB, at most 20,000 actions, `seed` and `simVersion` integers, each action's shape checked; every
1551 `step` call in a try/catch (a refused action stops that side, as `compareRules` already does); every
1552 text shown through `textContent`, never as HTML. No path or name from the record is ever used.
15533. **Which game plays it.** A report is only exact on the rules it was made under (`simVersion`).
1554 Options, cheapest first:
1555 - *Today's rules only*: right while `SIM_VERSION` is unchanged (the Q101 report replays exactly on
1556 today's rules, because only the autopilot's decisions changed). A `simVersion` mismatch shows a
1557 visible "the rules have changed since this report" banner (the dev page's label does this only in
1558 the autopilot mode; make it show everywhere) and offers the next option.
1559 - *The report's own sim*: `fw deploy` of a release also publishes just `state` + `turn` (+ what they
1560 import, i.e. `src/sim` bundled as one ES module, a few tens of KB) as `/explodorama/sim/<simVersion>.js`.
1561 The replay page loads the one matching the record, and today's for blue. One file per **rules
1562 version**, not per commit: they change rarely. This replaces `rules-versions/` + `git archive`
1563 for the published page; the dev page can keep using git for any commit.
1564 - *Full old builds* per tag: heavier (each is the whole game with art); only if the sim-only way proves
1565 not enough.
15664. **Compact records.** Today a long run's record (66 turns ≈ 8 KB) does not fit a `mailto:` link
1567 (`MAILTO_MAX` 1900), so the report asks the player to paste from the clipboard, and some will not. A
1568 compact encoding (actions as short codes, deflate, base64url) might fit a typical run in the link.
1569 Measure on real reports first (the Q101 one is 8 KB raw); keep the plain `Run record:` form readable
1570 for anything that does not fit.
15715. **Which fork to show.** The dev page's "autopilot took over" fork is one of several comparisons.
1572 For a published page the default should be the plain recorded run (what the reporter saw) with the
1573 last turns slowed, and the comparison (`&rules=` or autopilot) as an option, since an issue is often
1574 about something no code change touched.
1575
1576### 20.3 Open points
1577
1578- Whether the replay page is part of the game's deploy (`/explodorama/replay.html`) or a separate small
1579 app; fuzzyworld-tools decides how an app announces its viewer (see its PLAN.md).
1580- Whether the animated page may show the reporter's device string or error list (default: no; only the
1581 record and the version/commit).
1582- The `sim/<simVersion>.js` files: who builds them (`fw deploy` with a per-app hook, or the CI runner on a
1583 tag), and whether they are checked by a test that replays `test/fixtures/*.json` through each file.
1584- A **regression-fixtures habit**: every fixed bug whose report has a record keeps it in `test/fixtures/`
1585 with a test that replays it (as Q101 does), so a published replay and a unit test stay one artefact.
1586
1587Related queue items: Q101 (the first report), Q102 (admin panel), Q103 (the dev page), Q104 (this).