explodorama / files / PLAN.md
PLAN.md
5551eb37 2026-10-20 Pavel Safronov | 1 | # Explodorama — Plan |
| 2 | ||
3321c921 2026-10-27 Pavel Safronov | 3 | A turn-based, mobile-first, rogue-like take on the 1985 NES *Bomberman*: 20 levels (50 until 2026-10-23) and an ending. |
5551eb37 2026-10-20 Pavel Safronov | 4 | Built with **LittleJS + TypeScript + Vite**, tested with **Vitest**, deployed to **GitLab Pages**. |
| 5 | ||
27f24fed 2026-10-20 Pavel Safronov | 6 | Status: **M0–M5 done, M6 under way** (see §18). This document is the design and the hand-off |
5551eb37 2026-10-20 Pavel Safronov | 7 | for whoever continues the implementation. Read it fully before writing code, especially §1 (the |
| 8 | turn), §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. | | |
3321c921 2026-10-27 Pavel Safronov | 19 | | 3 | Lives | **3 lives**, plus a modern rogue-like layer: **coins** buy upgrades. | |
5551eb37 2026-10-20 Pavel Safronov | 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.) | | |
d603166d 2026-10-25 Pavel Safronov | 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. | | |
5551eb37 2026-10-20 Pavel Safronov | 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. | | |
00ad7d0a 2026-10-24 Pavel Safronov | 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.* | |
644cfd0b 2026-10-20 Pavel Safronov | 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. | |
5551eb37 2026-10-20 Pavel Safronov | 30 | |
| 31 | Still open: see §14. | |
| 32 | ||
| 33 | --- | |
| 34 | ||
| 35 | ## 1. The turn (the core rule — everything else follows from it) | |
| 36 | ||
| 37 | One turn, in this exact order: | |
| 38 | ||
| 39 | 1. **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. | |
82adee18 2026-10-20 Pavel Safronov | 41 | 2. **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. | |
5551eb37 2026-10-20 Pavel Safronov | 44 | 3. **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). | |
| 47 | 4. **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). | |
82adee18 2026-10-20 Pavel Safronov | 49 | **Last turn's flame, still burning, also kills** the player and enemies in it. |
5551eb37 2026-10-20 Pavel Safronov | 50 | An enemy sharing the player's tile kills the player. |
82adee18 2026-10-20 Pavel Safronov | 51 | 5. **Enemies plan.** Every surviving enemy tops its plan up to **3 moves** and displays them. |
5551eb37 2026-10-20 Pavel Safronov | 52 | Curse and Star/Shield turn counters advance. Input unlocks. |
| 53 | ||
| 54 | Rules 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. | |
82adee18 2026-10-20 Pavel Safronov | 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. | |
5551eb37 2026-10-20 Pavel Safronov | 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 | |---|---|---| | |
d603166d 2026-10-25 Pavel Safronov | 78 | | Lives | 3 | The shop can add more. | |
5551eb37 2026-10-20 Pavel Safronov | 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). | | |
52d68b59 2026-10-24 Pavel Safronov | 83 | | Charges | none | Temporary changes dropped by enemies, working on pickup (§3.3). | |
d603166d 2026-10-25 Pavel Safronov | 84 | | Coins | 0 | Spent in the shop (§8). | |
5551eb37 2026-10-20 Pavel Safronov | 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 | ||
99869630 2026-10-23 Pavel Safronov | 99 | **Few buttons, icons only, explained when picked (user request, 2026-10-22; built).** The bar |
52d68b59 2026-10-24 Pavel Safronov | 100 | has Move, Bomb and Wait. *(Until 2026-10-23 a second row held a button per charge held: |
| 101 | Shield, Freeze, Decoy, then also Pierce, Power and Line. Charges now work on pickup, §3.3, so | |
| 102 | the bar is only Move, Bomb and Wait.)* | |
99869630 2026-10-23 Pavel Safronov | 103 | *(Detonate and Fuse had buttons there too until a follow-up the same day: the Detonator now |
| 104 | works by tapping a bomb, which pulses, and the Fuse ±1 charge is gone.)* Nothing but Wait goes | |
52d68b59 2026-10-24 Pavel Safronov | 105 | off from its button alone (Shield and Freeze took a second tap, or a **Use** tap in the strip, |
| 106 | while they were actions). Buttons show icons, no names. Upgrades don't add buttons, they widen Move and | |
99869630 2026-10-23 Pavel Safronov | 107 | Bomb (`sim/picks.ts`): Move also kicks (Kick: walking into a bomb), runs (Run), teleports |
52d68b59 2026-10-24 Pavel Safronov | 108 | (Teleport) and steps diagonally (while a Diagonal charge lasts); Bomb also throws (Throw) and detonates |
99869630 2026-10-23 Pavel Safronov | 109 | (Detonator; a plain tap on a bomb does too, before a kick). The tile tapped |
| 110 | decides which, in that order of preference — a kick before walking onto a bomb with Bomb Pass, | |
52d68b59 2026-10-24 Pavel Safronov | 111 | anything free before spending a charge. ~~Picking an action opens a **strip above the bar** |
| 112 | saying what it does now~~ *Removed 2026-10-23 (user request):* first it covered the board's | |
| 113 | bottom row, then a band kept free for it kept resizing the board. There is no explanation text | |
| 114 | during play: power-ups are explained in the gem's choice, charges on their first pickup, and | |
00ad7d0a 2026-10-24 Pavel Safronov | 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 |
99869630 2026-10-23 Pavel Safronov | 116 | action). With an action picked, a direction key uses the tile next to the player, or 2 away |
| 117 | if that one can't be used (a run, a throw). | |
| 118 | ||
ea380ee8 2026-10-22 Pavel Safronov | 119 | **Info and menu (user request, 2026-10-21; built; changed 2026-10-22):** a dialog explains a |
| 120 | tile (`ui/info.ts`): enemies (their next moves in words), bombs, burning flame, walls and crates, | |
| 121 | the exit, the power-up, drops. *Since 2026-10-22 (user request)* a plain tap no longer shows it: | |
99869630 2026-10-23 Pavel Safronov | 122 | the round **?** button at the top right, beside the menu (key **?**; it was beside Wait until |
| 123 | the button rework the same day), turns on "What is that?" for one tap on the board or on an | |
| 124 | action button; tapping **?** again, picking an action, or Esc turns it off. Without it, a tap | |
ea380ee8 2026-10-22 Pavel Safronov | 125 | on an enemy, a bomb or burning flame does nothing (never a move into it), a wall or crate only |
| 126 | clears the auto-move flag, and any other tile the player can walk to — including the exit, the | |
| 127 | power-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 | |
1f0038ac 2026-11-01 Pavel Safronov | 129 | cancel) shows level and seed (no Kenney credit since 2026-10-31), and offers **New run** behind a confirmation and |
99869630 2026-10-23 Pavel Safronov | 130 | an **Almanac** (2026-10-22, user request; `ui/almanac.ts`): tabs for power-ups, charges and |
| 131 | enemies, each with its sprite and the same text as "What is that?" (enemies also: the level | |
| 132 | they first appear on). | |
a4fab0fb 2026-10-21 Pavel Safronov | 133 | |
82adee18 2026-10-20 Pavel Safronov | 134 | **Touch shortcuts (user request, 2026-10-20; built):** |
| 135 | ||
e3d8dc49 2026-10-27 Pavel Safronov | 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. | |
82adee18 2026-10-20 Pavel Safronov | 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. | |
a4fab0fb 2026-10-21 Pavel Safronov | 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. | |
82adee18 2026-10-20 Pavel Safronov | 153 | **Tapping the flag also takes one step** (user request, 2026-10-20). Tapping another far tile |
ea380ee8 2026-10-22 Pavel Safronov | 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 |
82adee18 2026-10-20 Pavel Safronov | 155 | a wall or crate, or on death / level change. Neither Go nor the flag takes a known-deadly step |
e3d8dc49 2026-10-27 Pavel Safronov | 156 | (onto an enemy, onto an enemy's intent tile, into flame this turn, or anything else `isDeadly` finds): the next step shows red |
82adee18 2026-10-20 Pavel Safronov | 157 | and Go falls back to picking Move. |
ea380ee8 2026-10-22 Pavel Safronov | 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 | |
99869630 2026-10-23 Pavel Safronov | 160 | that?" is on or a dialog is open. **With the Run power-up, a straight 2-tile stretch is one Run** |
ea380ee8 2026-10-22 Pavel Safronov | 161 | (same request), if both tiles pass the safety check; still recorded as an ordinary Run. |
82adee18 2026-10-20 Pavel Safronov | 162 | |
5551eb37 2026-10-20 Pavel Safronov | 163 | ### 3.2 Abilities (permanent once gained — no charges) |
| 164 | ||
| 165 | | Action | Source | Rule | | |
| 166 | |---|---|---| | |
99869630 2026-10-23 Pavel Safronov | 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. | | |
28670c3e 2026-10-24 Pavel Safronov | 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. | |
e78b7781 2026-10-25 Pavel Safronov | 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`). | |
52d68b59 2026-10-24 Pavel Safronov | 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). | |
7f242d36 2026-10-25 Pavel Safronov | 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. | | |
d3e4ed26 2026-10-25 Pavel Safronov | 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. | |
99869630 2026-10-23 Pavel Safronov | 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 | ||
52d68b59 2026-10-24 Pavel Safronov | 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.) |
5551eb37 2026-10-20 Pavel Safronov | 178 | |
d3e4ed26 2026-10-25 Pavel Safronov | 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 | |
| 182 | charge** (`state.charge`: its kind and uses left; `sim/charges.ts`) and **uses it on purpose** | |
| 183 | with the **charge button** beside Bomb (key V): a charged bomb (`charged`) for Pierce, Power and | |
| 184 | Nuke, a launch (`icbm`), a Freeze (`freeze`, at once), a Decoy (`decoy`, on the player's tile or | |
| 185 | one next to them). Bomb always places a plain bomb. A Shield is passive. The same kind picked up | |
| 186 | adds to it; another kind asks (`chargeOffer`, the `sellCharge` / `swapCharge` actions, no turn): | |
bb3823d2 2026-10-31 Pavel Safronov | 187 | 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 |
d3e4ed26 2026-10-25 Pavel Safronov | 188 | price, rounded up, for what is left (`salePrice`). *Since 2026-10-25 (user report)* one chosen |
| 189 | from a gem or bought in the shop asks the same (it went straight in, selling the one held), and | |
| 190 | the charge button is always there: an empty dashed slot with none, greyed out while it can't act, | |
| 191 | and 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` | |
| 193 | action; carried, it does nothing) and then **stays up until a hit lands**, through any number of | |
| 194 | levels (`state.shieldUp`, carried from level to level). The **Decoy** goes up to `DECOY_RANGE` 3 | |
| 195 | tiles away in a straight line, every tile on the way free — a wall, crate, bomb, enemy or flame | |
| 196 | stops it, as a slide or a kick is stopped — and **lasts until the level ends**, an enemy reaches | |
| 197 | it, or flame burns it. (For one round both lasted 3 turns; the user found that far too short.) | |
e6bb7c42 2026-10-25 Pavel Safronov | 198 | `SIM_VERSION` 31. |
| 199 | *Also 2026-10-25 (user request):* **Shield and Freeze need a confirmation**: the charge button | |
| 200 | picks them, lighting the player's own tile (their only target); a tap on yourself, or the button | |
5f066960 2026-10-26 Pavel Safronov | 201 | pressed again, uses them. `SIM_VERSION` 33. |
| 202 | *Changed again 2026-10-25 (user request):* **Shield and Flame Pass work by themselves while in the | |
| 203 | charge slot**, no raising: a hit of their kind uses them up (Flame Pass: one of its 3 uses per fire | |
| 204 | hit), and the slot is empty again. The `shield` and `flamePass` actions and `shieldUp`/`flameHits` | |
| 205 | are gone; only Freeze still takes the confirming tap. `SIM_VERSION` 38. `boosts` and `shield` are gone from the state; `?charges=n` became | |
d3e4ed26 2026-10-25 Pavel Safronov | 206 | `?charge=<kind>`. This also ends the Nuke trap the balance lab found (§19.1). The rest of this |
| 207 | section is the earlier design, kept as history. | |
5551eb37 2026-10-20 Pavel Safronov | 208 | |
52d68b59 2026-10-24 Pavel Safronov | 209 | *Redesigned 2026-10-23 (user request):* charges are **no longer actions**. Each works the |
| 210 | moment it is picked up (`applyCharge` in `sim/actions.ts`), as a temporary change; there are no | |
| 211 | charge buttons. What they still give lives in `state.boosts` (and `shield`, `frozen`, `decoy`) | |
| 212 | and shows in the top bar with its count. It is kept from level to level and through a death. | |
| 213 | Picking up more of a kind adds to it. The first pickup of each kind in a run still opens a | |
| 214 | dialog saying what it does (`chargeInfo`). Icons are the user's choices (§13). | |
5551eb37 2026-10-20 Pavel Safronov | 215 | |
52d68b59 2026-10-24 Pavel Safronov | 216 | | Charge | Rule | |
5551eb37 2026-10-20 Pavel Safronov | 217 | |---|---| |
ebcd0a8f 2026-11-02 Pavel Safronov | 218 | | **Pierce** | The next `BOMB_BOOST` (3, user's choice) bombs' flame passes **through** soft blocks. Icon: the Crate Pass icon. | |
52d68b59 2026-10-24 Pavel Safronov | 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. | | |
e78b7781 2026-10-25 Pavel Safronov | 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). | |
52d68b59 2026-10-24 Pavel Safronov | 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 | ||
| 229 | A bomb uses one of each bomb boost held, so Pierce, Power and Nuke combine on one bomb; it wears | |
| 230 | their icons as badges. Charges can also appear in the shop (§8). | |
5551eb37 2026-10-20 Pavel Safronov | 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. | |
82adee18 2026-10-20 Pavel Safronov | 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.) | |
5551eb37 2026-10-20 Pavel Safronov | 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 | ||
| 257 | The 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 | | |
99869630 2026-10-23 Pavel Safronov | 264 | | Speed (roller skates) | **Run** (called Skates until 2026-10-22) | yes | |
ebcd0a8f 2026-11-02 Pavel Safronov | 265 | | Wallpass | **Crate Pass** — walk through soft blocks | no — always lost on death (*kept since 2026-10-23, see §9*) | |
5551eb37 2026-10-20 Pavel Safronov | 266 | | Detonator | **Detonator → unlocks Detonate** | no | |
| 267 | | Bombpass | **Bomb Pass** — walk over bombs | no | | |
99869630 2026-10-23 Pavel Safronov | 268 | | Flamepass | **Flame Pass** — immune to flame (tune later if too strong); walk through it (with a Star too, since 2026-10-22) | no | |
5551eb37 2026-10-20 Pavel Safronov | 269 | | Mystery | **Star** — invincible for 8 turns | no (it's temporary anyway) | |
| 270 | ||
| 271 | Additions: | |
| 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 | ||
| 277 | NES rules kept (they create the classic tension): | |
| 278 | ||
d603166d 2026-10-25 Pavel Safronov | 279 | - **One pickup per level** under a soft block (more via shop upgrades), plus the |
52d68b59 2026-10-24 Pavel Safronov | 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. | |
5551eb37 2026-10-20 Pavel Safronov | 287 | - **Flame destroys an exposed pickup** — and spawns extra enemies, as in the NES. |
52d68b59 2026-10-24 Pavel Safronov | 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. | |
5551eb37 2026-10-20 Pavel Safronov | 291 | - **The exit opens only when all enemies are dead.** |
52d68b59 2026-10-24 Pavel Safronov | 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. | |
27f24fed 2026-10-20 Pavel Safronov | 297 | - *Built (2026-10-20):* the pickup is under any crate except the exit's. Draw weights (to tune): |
ebcd0a8f 2026-11-02 Pavel Safronov | 298 | Bomb+ 20, Fire+ 20, Fuse+ 6, Coin bag 12 (10 coins), Heart 4, Crate Pass 8, Bomb Pass 8, |
52d68b59 2026-10-24 Pavel Safronov | 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 | |
d603166d 2026-10-25 Pavel Safronov | 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. | |
5551eb37 2026-10-20 Pavel Safronov | 303 | |
| 304 | --- | |
| 305 | ||
3321c921 2026-10-27 Pavel Safronov | 306 | ## 5b. Exclusive power-ups — a rogue-like direction (idea, user, 2026-10-21; *not decided*) |
beb9f2b7 2026-10-21 Pavel Safronov | 307 | |
52d68b59 2026-10-24 Pavel Safronov | 308 | *2026-10-23:* the gem's choice of 1 from 5 (§5) is a first step in this direction — the player |
| 309 | chooses instead of taking whatever is there — but nothing is exclusive yet: slots are still open. | |
| 310 | ||
beb9f2b7 2026-10-21 Pavel Safronov | 311 | The 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. | |
99869630 2026-10-23 Pavel Safronov | 318 | - **What takes a slot.** Natural candidates: the abilities and passes (Run, Detonator, |
beb9f2b7 2026-10-21 Pavel Safronov | 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. | |
ebcd0a8f 2026-11-02 Pavel Safronov | 323 | - **Synergies and trade-offs** make choices interesting: e.g. Bomb Pass + Kick, Crate Pass + |
beb9f2b7 2026-10-21 Pavel Safronov | 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 | |
| 327 | these, not paper over them): | |
| 328 | ||
d603166d 2026-10-25 Pavel Safronov | 329 | 1. ~~**Keep / Fresh on death (§9)**~~ — dropped 2026-10-24. |
beb9f2b7 2026-10-21 Pavel Safronov | 330 | 2. **The shop (§8)** sells abilities and stats; with slots, a purchase may also force a swap. |
d603166d 2026-10-25 Pavel Safronov | 331 | 3. ~~**The Workshop (§7)**~~ — dropped 2026-10-24. |
beb9f2b7 2026-10-21 Pavel Safronov | 332 | 4. **NES rule: passes and Detonator are lost on death (§5, §9)** — with slots, perhaps nothing |
52d68b59 2026-10-24 Pavel Safronov | 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.* | |
| 335 | 5. ~~**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. | |
beb9f2b7 2026-10-21 Pavel Safronov | 337 | |
| 338 | Open questions for the user: how many slots? Do stats take slots? Is a swapped-out power-up | |
| 339 | gone or left on the tile? Should action charges (one-use, from enemies) stay separate from slots | |
| 340 | (probably yes)? | |
| 341 | ||
5551eb37 2026-10-20 Pavel Safronov | 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 | ||
d603166d 2026-10-25 Pavel Safronov | 363 | - **Cures:** wait it out, a Star, or the shop's **Cleanse** charge. |
5551eb37 2026-10-20 Pavel Safronov | 364 | - (Open, §14: should skulls also give coins — a risk-for-reward choice?) |
| 365 | ||
| 366 | --- | |
| 367 | ||
d603166d 2026-10-25 Pavel Safronov | 368 | ## 7. ~~The Workshop~~ — dropped (user's call, 2026-10-24) |
5551eb37 2026-10-20 Pavel Safronov | 369 | |
d603166d 2026-10-25 Pavel Safronov | 370 | The plan had permanent upgrades between runs, Everspace-style: coins left over at the end of a |
| 371 | run banked into upgrade tracks (more lives, bombs or fire to start with, Run from the start, …). | |
| 372 | Dropped: the user can't imagine cross-run permanent upgrades that wouldn't ruin the game. Every | |
| 373 | run starts the same; what a run gains, it keeps only until it ends. (The tracks and their costs | |
| 374 | are in git history and decisions.md.) | |
5551eb37 2026-10-20 Pavel Safronov | 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 | |
0b63d158 2026-10-25 Pavel Safronov | 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. | |
e78b7781 2026-10-25 Pavel Safronov | 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). | |
0b63d158 2026-10-25 Pavel Safronov | 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 | | |
565a6873 2026-10-25 Pavel Safronov | 404 | | 6 | 8, Blobs and Seekers | ≈ 7 | 6 | + 15 | ≈ 13 – 28 | |
0b63d158 2026-10-25 Pavel Safronov | 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. | |
e78b7781 2026-10-25 Pavel Safronov | 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). | |
5551eb37 2026-10-20 Pavel Safronov | 421 | - Purchases last for this run only (decision 7). |
| 422 | ||
| 423 | --- | |
| 424 | ||
d603166d 2026-10-25 Pavel Safronov | 425 | ## 9. Death |
5551eb37 2026-10-20 Pavel Safronov | 426 | |
0580e85e 2026-10-24 Pavel Safronov | 427 | *Built 2026-10-24 (user request):* the death banner (and the game-over one) says how and where |
| 428 | the player died — level, turn, and the cause (`GameState.death`): walked into an enemy, an | |
d603166d 2026-10-25 Pavel Safronov | 429 | enemy walked in, a bomb's blast, a chain reaction's second bomb, or last turn's flame — and |
| 430 | replays the player's own last turns (§13c). | |
5551eb37 2026-10-20 Pavel Safronov | 431 | |
d603166d 2026-10-25 Pavel Safronov | 432 | A death with lives left restarts the level from its seed: the same layout, the enemies back where |
| 433 | they started, and the player keeping everything the run had (stats, power-ups, passes, charges, | |
| 434 | coins; since 2026-10-23). At game over, **Same dungeon** runs the same seed again from level 1, | |
| 435 | with nothing carried over, and **New dungeon** a new one. | |
5551eb37 2026-10-20 Pavel Safronov | 436 | |
d603166d 2026-10-25 Pavel Safronov | 437 | ~~**Keep or Fresh**~~ — *dropped 2026-10-24 (user's call).* The plan was a choice after each |
| 438 | death: keep the run's power, or reset it for a coin payout, balanced so each was picked about half | |
| 439 | the time. Now there is no choice and no payout: a retry, of the level or of the whole dungeon, is | |
| 440 | just another try at the same board, its only benefit what the player now knows. (The table and | |
| 441 | the payout formula are in git history and decisions.md.) | |
5551eb37 2026-10-20 Pavel Safronov | 442 | |
| 443 | --- | |
| 444 | ||
| 445 | ## 10. Enemies | |
| 446 | ||
| 447 | All move on their own schedule ("speed"), are blocked by bombs unless stated, and kill on | |
| 448 | contact. 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 | ||
565a6873 2026-10-25 Pavel Safronov | 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. | |
5551eb37 2026-10-20 Pavel Safronov | 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. | |
c37a2b0b 2026-10-20 Pavel Safronov | 478 | *Built (2026-10-20):* per type — Blob 20% coins / 15% charge, Seeker 50% / 25%, Drifter |
0b63d158 2026-10-25 Pavel Safronov | 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, |
c37a2b0b 2026-10-20 Pavel Safronov | 480 | +1 every 3 levels, at most 10. New type every 6 levels: Seeker from 7, Drifter from 13. |
52d68b59 2026-10-24 Pavel Safronov | 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 | |
8da5a7c0 2026-10-24 Pavel Safronov | 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 | |
c37a2b0b 2026-10-20 Pavel Safronov | 488 | drop nothing, so the exit can't be farmed. |
beb9f2b7 2026-10-21 Pavel Safronov | 489 | - Drops land on the enemy's tile and stay until collected or flamed. *Built (2026-10-21, user |
28670c3e 2026-10-24 Pavel Safronov | 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 |
beb9f2b7 2026-10-21 Pavel Safronov | 491 | power-up) rolls to the nearest free floor tile within 2 steps (BFS over floor), else stays. |
5551eb37 2026-10-20 Pavel Safronov | 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 | ||
00ad7d0a 2026-10-24 Pavel Safronov | 499 | *Since 2026-10-23 (user request):* **one board, 9 × 11, 20 levels**, fitting the screen whole — |
| 500 | the former "Small" board (density 25%, enemies 2 + 1 per 4 levels up to 5, par 25 + 8/enemy) — | |
| 501 | with no scrolling, and crates always crumbling once the last enemy is gone (§13b). The NES-size | |
| 502 | notes below (31 × 13, 50 levels, the side-scrolling camera) are kept as the history of the | |
| 503 | design; the board code for them was removed. | |
e6bb7c42 2026-10-25 Pavel Safronov | 504 | *Built 2026-10-25 (user request, §19.2):* boards grow with the level (9 × 11 to 15 × 21), seen |
| 505 | through a fixed 9 × 13 view that follows the player, with arrows at its edge; enemies by | |
| 506 | concentration. This undoes the one-size choice above. | |
00ad7d0a 2026-10-24 Pavel Safronov | 507 | |
5551eb37 2026-10-20 Pavel Safronov | 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. | |
c37a2b0b 2026-10-20 Pavel Safronov | 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`. | |
5551eb37 2026-10-20 Pavel Safronov | 518 | (Turn **limit** with Reapers: later milestone.) |
| 519 | - **50 levels**, difficulty rising through enemy count, types, speed and soft-block density. | |
d603166d 2026-10-25 Pavel Safronov | 520 | - **Game over** at 0 lives → run summary (level reached, coins, kills, seed) → the same dungeon |
| 521 | again or a new one. | |
5551eb37 2026-10-20 Pavel Safronov | 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 | |
d603166d 2026-10-25 Pavel Safronov | 524 | phone. The save is just the run record (seed + action log, §12), replayed on |
27f24fed 2026-10-20 Pavel Safronov | 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. | |
3cf3607b 2026-10-24 Pavel Safronov | 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. | |
5551eb37 2026-10-20 Pavel Safronov | 532 | |
| 533 | ### Victory certificate | |
| 534 | ||
00ad7d0a 2026-10-24 Pavel Safronov | 535 | *Since 2026-10-23 (user request):* an **Immortal** run (§13b) can be played to the end but earns |
| 536 | no certificate, nor any other achievement. | |
| 537 | A **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 | ||
d603166d 2026-10-25 Pavel Safronov | 540 | Shows: **seed, date, turns, deaths, sim version**, and a |
2f667e95 2026-10-27 Pavel Safronov | 541 | **QR code** linking to `https://apps.fuzzyworld.net/explodorama/#cert=…`. |
5551eb37 2026-10-20 Pavel Safronov | 542 | |
| 543 | Being honest about what this can prove: the game is fully client-side and open source, so any | |
| 544 | code-only "signature" can be forged by someone determined. What *can* be verified is the run | |
| 545 | itself, 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 | ||
0580e85e 2026-10-24 Pavel Safronov | 562 | *Built 2026-10-24 (user request):* the New run dialog has an optional **seed** field (empty: |
| 563 | random; a whole number: that seed; other text: hashed into one), so a dungeon can be shared or | |
| 564 | replayed as a real, saved run (a `?seed` link is a test board). **Game over** offers **Same | |
| 565 | dungeon** (same seed and modes, from level 1) and **New dungeon** (a random seed). | |
| 566 | ||
5551eb37 2026-10-20 Pavel Safronov | 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 | ||
0580e85e 2026-10-24 Pavel Safronov | 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 | |
| 579 | this browser (`ui/scores.ts`, `localStorage`), the best `BOARD_SIZE` (25), shown from the menu | |
| 580 | and from the end-of-run banner (which also says "New high score: #N!"). **Ranked** (user's | |
| 581 | choice) 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, | |
| 584 | turn), the abilities and passes held and the stats — and **the whole run record** (seed and | |
| 585 | actions) with `format` and `simVersion`. Immortal runs don't count; Developer runs count, shown | |
| 586 | with 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 | |
| 589 | none, on purpose (§9's note on measuring balance) — so it needs a decision before it is built. | |
| 590 | The 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 | ||
5551eb37 2026-10-20 Pavel Safronov | 620 | ## 13. Look and icons |
| 621 | ||
644cfd0b 2026-10-20 Pavel Safronov | 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 | |
| 624 | plan 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 | |
1f0038ac 2026-11-01 Pavel Safronov | 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 / |
beb9f2b7 2026-10-21 Pavel Safronov | 629 | game-over banner too). The |
644cfd0b 2026-10-20 Pavel Safronov | 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 | |
6e94f995 2026-10-24 Pavel Safronov | 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. | |
644cfd0b 2026-10-20 Pavel Safronov | 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). | |
0b63d158 2026-10-25 Pavel Safronov | 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. | |
644cfd0b 2026-10-20 Pavel Safronov | 658 | |
a4fab0fb 2026-10-21 Pavel Safronov | 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. | |
ea380ee8 2026-10-22 Pavel Safronov | 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. | |
99869630 2026-10-23 Pavel Safronov | 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. | |
a4fab0fb 2026-10-21 Pavel Safronov | 676 | |
644cfd0b 2026-10-20 Pavel Safronov | 677 | Sprite 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. | | |
d05d464d 2026-10-22 Pavel Safronov | 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 | |
644cfd0b 2026-10-20 Pavel Safronov | 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 | | |
c37a2b0b 2026-10-20 Pavel Safronov | 687 | | ✅ HUD | `hud_heart` / `hud_heart_empty` (lives), `flag_green_a` (level), `slime_normal_rest` (enemies left), `hud_coin` (coins), `bomb`, `fireball`, digits | |
99869630 2026-10-23 Pavel Safronov | 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` | |
c37a2b0b 2026-10-20 Pavel Safronov | 689 | | ✅ banners | `character_green_hit` (lost a life), `hud_heart_empty` (game over), `flag_green_a` (level cleared); favicon `bomb` | |
52d68b59 2026-10-24 Pavel Safronov | 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.) | |
c37a2b0b 2026-10-20 Pavel Safronov | 691 | | ✅ coins | `coin_bronze` (1), `coin_silver` (2), `coin_gold` (5), bobbing; `hud_coin` in the HUD | |
52d68b59 2026-10-24 Pavel Safronov | 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`.) | |
82adee18 2026-10-20 Pavel Safronov | 693 | | ✅ decoy | `character_green_front`, faded | |
52d68b59 2026-10-24 Pavel Safronov | 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. | |
c37a2b0b 2026-10-20 Pavel Safronov | 695 | | ✅ intent arrows | drawn red lines with a head, not sprites | |
ebcd0a8f 2026-11-02 Pavel Safronov | 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. | |
ea380ee8 2026-10-22 Pavel Safronov | 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). | |
c37a2b0b 2026-10-20 Pavel Safronov | 698 | | ✅ Seeker | `fly_rest`, `fly_a`, `fly_b`, as the Blob | |
| 699 | | ✅ Drifter | `saw_rest`, `saw_a`, `saw_b`; spins while moving | | |
644cfd0b 2026-10-20 Pavel Safronov | 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 | ||
| 705 | The pack has more enemies (bee, ladybug, snail, worm, barnacle, fish, spiked slime, falling | |
| 706 | block) and more tiles (keys, locks, levers, spikes, torches, …) for later enemies, curses and | |
d603166d 2026-10-25 Pavel Safronov | 707 | actions; copy them in when they are used. Icons the pack doesn't have (shop, trophy, |
644cfd0b 2026-10-20 Pavel Safronov | 708 | settings, sound, and most action charges) are still open. |
5551eb37 2026-10-20 Pavel Safronov | 709 | |
| 710 | --- | |
| 711 | ||
e78b7781 2026-10-25 Pavel Safronov | 712 | ### Animation timing (user's guidance, 2026-10-24) |
| 713 | ||
| 714 | Most turns animate in a step's time (0.16 s), so play stays quick. **A rare or involved turn — | |
| 715 | a charge at work, an ICBM, something that changes a lot at once — may take longer** and animate | |
| 716 | a more complex change of state in stages (the ICBM flies, then its blast plays out), which gives | |
| 717 | actions more room. The sim stays instant; only the drawing waits (`blastAt`-style holds in | |
| 718 | effects.ts). Input is still never locked (decisions.md 2026-10-22): a quick next tap starts the | |
| 719 | next turn over the end of the last one. | |
| 720 | ||
52d68b59 2026-10-24 Pavel Safronov | 721 | ## 13a. The demo (user request, 2026-10-23; built) |
| 722 | ||
| 723 | Some players never played the original, so the game opens with a demo: generated scenes, | |
| 724 | replayed view-only, a caption under each, until the player taps or presses a key (that input | |
| 725 | does 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 | |
| 727 | about one tile in six, the player and 1–3 enemies apart), then up to 150 random games on it by | |
| 728 | the real rules — mostly moves, sometimes bombs, never a move that kills the player — until one | |
| 729 | shows the scene's moment; the shortest of those is the scene (a new room if none does). | |
fa3e81a7 2026-10-24 Pavel Safronov | 730 | Kinds (`SCENES` in `sim/demo.ts`, each with its moment): *since 2026-10-23 (user request)* every |
| 731 | feature on the board but the shop and the exit — kill, **arrows** (an enemy killed on the tile it | |
| 732 | stepped onto, the bomb waiting where its arrows led: watch them; user request), escape, chain, | |
565a6873 2026-10-25 Pavel Safronov | 733 | lingering flame, the first three enemy types, drops, the gem (uncovered and taken; its choice isn't |
fa3e81a7 2026-10-24 Pavel Safronov | 734 | shown), each power-up that shows on the board (Bomb +1, Fire +1, Fuse +1, Run, Kick, Throw, |
| 735 | Teleport, Diagonal, the Detonator, Wall / Bomb / Flame Pass, Star), and each charge (Pierce, | |
| 736 | Power, Nuke, Shield, Freeze, Decoy, picked up first). Heart and Coin bag only change numbers, | |
d5e3ad4d 2026-10-24 Pavel Safronov | 737 | so they have none. *Since 2026-10-23 (user request)* also the exit — **exit** (it opens, the |
| 738 | player 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 | |
| 740 | scene whose moment is the death: into a blast (from a safe tile onto a burning one), not far | |
| 741 | enough (moved but stayed in reach; Fire 3), blocked in (no step out of danger), chain (caught | |
| 742 | only by a bomb that wasn't due), an enemy, and two funny ones: the Detonator next to you, a Run | |
| 743 | into an enemy. The random play keeps refusing deadly moves, except the one that is the scene's | |
33a0256b 2026-10-24 Pavel Safronov | 744 | death. Living and dying scenes take turns, each queue shuffled (*until 2026-10-24: now one fixed |
| 745 | order, see below*). 36 kinds, all found on every | |
d5e3ad4d 2026-10-24 Pavel Safronov | 746 | seed tried, the slowest under 0.5 s; each caption has the thing's icon. The "Ouch!" and |
| 747 | level-cleared banners are hidden in the demo. The UI (`main.ts`: `startDemo`, `demoTick`) plays a | |
52d68b59 2026-10-24 Pavel Safronov | 748 | turn every 0.6 s through the same animation code as play, hides the top bar and buttons, and |
fa3e81a7 2026-10-24 Pavel Safronov | 749 | 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 |
36c5d6a2 2026-10-24 Pavel Safronov | 750 | the 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 | |
| 752 | shows the controls**. A random game still runs until it shows the moment, but the scene is only | |
| 753 | its last 3–6 turns (as few as do), starting from a calm board — no bomb, no flame, no drop the | |
| 754 | room didn't start with, auto-move not under way — and replayed from there to check the moment | |
| 755 | still comes (`ending` in `sim/demo.ts`; Fuse +1 is now fuse 4, the real +1; Pierce's room has a | |
| 756 | row of two crates with Fire 2, and its moment is a crate burnt behind another; Pierce, Power and | |
| 757 | Nuke lie next to the player). Every turn comes with its **gesture** (`Gesture`: a tap, a long | |
| 758 | press, a button and then a tile, a far tap for auto-move, Go), only ones the real controls read | |
| 759 | as that action (`tapMeaning` in `sim/picks.ts`, shared with `tapTile`; `resolvePick`); a turn no | |
| 760 | gesture can play isn't used. The UI acts each one out with a finger (Kenney's pointing hand) on | |
| 761 | the real action bar, shown in the demo as a tray right under the board, apart from the caption | |
| 762 | panel at the bottom (grouped by interactivity: board and buttons, then the words): a ripple where it taps, a ring closing in on a | |
| 763 | long press, then the bomb's preview; a picked button lights up its tiles, a far tap shows | |
58f1e700 2026-10-24 Pavel Safronov | 764 | auto-move's way. Turns take about 1–2 s. *Since 2026-10-24 (user request)* the player wears a |
| 765 | green **"You"** tag just over their head, always (it wasn't obvious which moving figure is you), and each scene's finger | |
e3384c6b 2026-10-24 Pavel Safronov | 766 | appears just below-right of the player, where the eye should start. *Since 2026-10-24 (user |
| 767 | request)* the **top bar shows** in the demo too (scenes start at turn 0 with no coins; rooms have | |
| 768 | par 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 | |
| 771 | taps Got it; in the demo the dialog opens over the scene only, not as a modal over the page, so | |
| 772 | the caption stays undimmed). The demo plays **on a CRT** (user request): LittleJS | |
| 773 | post-processing (`PostProcessPlugin`: colour fringes and static) on the board, a CSS layer of scan | |
| 774 | lines, a rolling dark bar and sweeping bright lines, vignette, rounded glass and bezel, and every | |
| 775 | 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 | |
| 776 | too), at most 4:3 with black bars; the finger is in front of the glass. **Five control scenes** come first (step, long-press | |
36c5d6a2 2026-10-24 Pavel Safronov | 777 | bomb, Bomb button, Wait, auto-move with Go), and again each time the living scenes have all been |
| 778 | shown. Captions of Run, Kick, Throw, Teleport, Diagonal and the Detonator say how to do them. | |
33a0256b 2026-10-24 Pavel Safronov | 779 | 41 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 | |
| 781 | uncovered exit or gem (except in the scene about that), and a bomb only where its blast reaches a | |
| 782 | 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, | |
| 783 | or in one room in six already uncovered (locked while enemies live). *Since 2026-10-24 (user | |
| 784 | 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 | |
| 785 | order**, looped (`DEMO_ORDER` in main.ts: the controls' seven, then the table's living scenes with | |
7f242d36 2026-10-25 Pavel Safronov | 786 | 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, |
33a0256b 2026-10-24 Pavel Safronov | 787 | ↺ goes back to the first, and the place is kept in `localStorage` so the demo carries on there |
| 788 | next time, after a reload too (not on a test URL, which starts at the first). Scenes are made in | |
| 789 | a Web Worker (`ui/scenes.ts`), the next while one plays. Ideas: a gem's choice, and showing the | |
| 790 | demo 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 | ||
| 794 | Every info dialog gets a small looping clip of what it describes, made on the spot like the demo's | |
| 795 | scenes (`sim/demo.ts`): a small room, random play by the real rules until the moment shows, the | |
| 796 | last few turns replayed. Each loop holds its moment for **1 s**, then starts over **with a new | |
| 797 | setup** (a new seed), for variety. **No CRT** (that is the demo's joke). **The finger** only where | |
| 798 | it helps: the action buttons and the power-ups you work yourself (Run, Kick, Throw, Teleport, | |
| 799 | Diagonal, 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); | |
| 801 | for 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). | |
| 824 | Clips 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 | |
| 827 | dialog with a clip is open — first the clip's room, drawn at an offset in world space where the | |
| 828 | camera 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 | |
| 830 | drawing and animation code. Fallback, if a copy in the middle of a frame doesn't work: LittleJS's | |
| 831 | 2D drawing straight into the dialog's canvas. It needs the board's view state (effects, flames, | |
| 832 | the player's move and pose, enemy steps, …), now module variables in `main.ts`, to become an | |
| 833 | object, 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 | |
| 835 | too. | |
| 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 | |
| 839 | animation state is one object (`render/animate.ts`: `Animation`, `animateTurn`), one for the game | |
| 840 | and one per clip, and scenes are made in a Web Worker (`sim/scene-worker.ts`, `ui/scenes.ts`; on | |
| 841 | the page where a worker can't be made), the next loop's while one plays — the demo's too; 3. the | |
32dd53cd 2026-10-25 Pavel Safronov | 842 | clips that reuse demo scenes (*done 2026-10-24*: each `Info` names its scene, `clip`; the finger in |
| 843 | the clips about the actions and the hand-worked power-ups, `FINGER_KINDS` in ui/clip.ts — Wait | |
| 844 | there is a tap on the player, as no action bar is in a clip; charges are still picked up first); 4. the | |
| 845 | new 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 | |
| 847 | shows, and the modes have none); 5. the offer's cards, | |
| 848 | the almanac and the "Ouch!" replay (*done 2026-10-24*, the user's choices: one clip above the | |
ac534e57 2026-10-25 Pavel Safronov | 849 | offer's cards, playing the card selected — *since 2026-10-24 (user request) a tap on a card |
| 850 | selects it and Choose takes it*; | |
32dd53cd 2026-10-25 Pavel Safronov | 851 | almanac entries with a clip are buttons that open it in their own dialog, Back returning; the |
| 852 | banner replays the player's own last 4 turns of the attempt, a demo room's worth of the level | |
| 853 | round where they died). Clips can now be several at once, fixed scenes or made each loop. | |
ac534e57 2026-10-25 Pavel Safronov | 854 | *Also done 2026-10-24:* the modes' tag plays `rainbowGem` (Developer) or `immortal`, and under |
| 855 | reduced motion every clip is a still of its moment (the scene played to its end, held). | |
52d68b59 2026-10-24 Pavel Safronov | 856 | |
beb9f2b7 2026-10-21 Pavel Safronov | 857 | ## 13b. Run options (user request, 2026-10-21; built) |
| 858 | ||
| 859 | Chosen 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`) | |
| 861 | and state (`GameState.options`), so replays are exact. Shown as tags in the top bar and in the menu. | |
| 862 | ||
00ad7d0a 2026-10-24 Pavel Safronov | 863 | *Since 2026-10-23 (user request) only two options remain, Developer and Immortal:* the board |
| 864 | size and Crumbling crates are gone (one 9 × 11 board, §11; crates always crumble). URL test | |
| 865 | parameters: `?dev`, `?immortal`. | |
| 866 | ||
beb9f2b7 2026-10-21 Pavel Safronov | 867 | | Option | Rules | |
| 868 | |---|---| | |
00ad7d0a 2026-10-24 Pavel Safronov | 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. | | |
18ff790e 2026-10-24 Pavel Safronov | 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.) | |
d603166d 2026-10-25 Pavel Safronov | 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). | |
00ad7d0a 2026-10-24 Pavel Safronov | 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. | |
beb9f2b7 2026-10-21 Pavel Safronov | 874 | |
18ff790e 2026-10-24 Pavel Safronov | 875 | URL test parameters `?dev` and `?immortal` set them for a test board. |
beb9f2b7 2026-10-21 Pavel Safronov | 876 | |
| 877 | --- | |
| 878 | ||
5551eb37 2026-10-20 Pavel Safronov | 879 | ## 14. Open questions |
| 880 | ||
d603166d 2026-10-25 Pavel Safronov | 881 | 1. ~~**Workshop banking**~~ — no Workshop (dropped 2026-10-24, §7). |
| 882 | 2. ~~**Workshop deposits**~~ — no Workshop. | |
| 883 | 3. ~~**Fresh start and the level**~~ — no Keep / Fresh (dropped 2026-10-24, §9). | |
5551eb37 2026-10-20 Pavel Safronov | 884 | 4. **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. | |
| 886 | 5. **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. | |
| 888 | 6. **Shared / daily seeds:** seeds already make runs reproducible. Add a "daily seed" everyone | |
0580e85e 2026-10-24 Pavel Safronov | 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. | |
5551eb37 2026-10-20 Pavel Safronov | 891 | 7. **Certificate strength:** replay-verifiable (§11, recommended; nearly free since the sim is |
| 892 | deterministic) or a simple checksum? | |
644cfd0b 2026-10-20 Pavel Safronov | 893 | 8. **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. | |
beb9f2b7 2026-10-21 Pavel Safronov | 895 | 9. **Exclusive power-ups (§5b):** slots instead of collecting everything? How many, and what |
d603166d 2026-10-25 Pavel Safronov | 896 | takes one? Needs answers before the shop is built, since it depends on it. |
beb9f2b7 2026-10-21 Pavel Safronov | 897 | 10. **Enemies sharing a tile:** they do now, as in the NES original (as far as remembered). Should |
| 898 | they block each other instead? | |
5551eb37 2026-10-20 Pavel Safronov | 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`. | |
6e94f995 2026-10-24 Pavel Safronov | 913 | - **The game uses LittleJS's WebGL renderer** (Canvas 2D from 2026-10-19 to 2026-10-24), and handles taps with |
5551eb37 2026-10-20 Pavel Safronov | 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, | |
d603166d 2026-10-25 Pavel Safronov | 921 | rerolls — goes through `sim/` and is appended to the run |
5551eb37 2026-10-20 Pavel Safronov | 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 | |
d603166d 2026-10-25 Pavel Safronov | 928 | run, replay it, identical final state and hash); curses never before |
5551eb37 2026-10-20 Pavel Safronov | 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. | |
beb9f2b7 2026-10-21 Pavel Safronov | 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 | |
ea380ee8 2026-10-22 Pavel Safronov | 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.)* | |
5551eb37 2026-10-20 Pavel Safronov | 936 | |
| 937 | --- | |
| 938 | ||
| 939 | ## 16. Architecture | |
| 940 | ||
| 941 | Golden rule: **all game rules are pure TypeScript with zero LittleJS imports**, deterministic | |
| 942 | given a seed, and fully unit-testable. | |
| 943 | ||
| 944 | ``` | |
| 945 | src/ | |
| 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) } | |
99869630 2026-10-23 Pavel Safronov | 952 | picks.ts what a tap means with Move or Bomb picked (they stand for groups of actions) |
5551eb37 2026-10-20 Pavel Safronov | 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 | |
92cc4ee3 2026-11-02 Pavel Safronov | 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) | |
5551eb37 2026-10-20 Pavel Safronov | 960 | curses.ts |
| 961 | shop.ts offers, prices, purchase | |
| 962 | record.ts run record, replay, hash, SIM_VERSION | |
| 963 | render/ LittleJS | |
644cfd0b 2026-10-20 Pavel Safronov | 964 | board.ts sprites, highlights, danger overlay, intents |
| 965 | sprites.ts which Kenney sprites the board loads (§13) | |
ea380ee8 2026-10-22 Pavel Safronov | 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 | |
ebcd0a8f 2026-11-02 Pavel Safronov | 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) | |
d603166d 2026-10-25 Pavel Safronov | 970 | ui/ DOM: action bar, HUD, shop, death choice, certificate, About |
ebcd0a8f 2026-11-02 Pavel Safronov | 971 | padMap.ts controller: gamepad snapshot to intents (pure; decisions.md 2026-11-01) |
92cc4ee3 2026-11-02 Pavel Safronov | 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) | |
ebcd0a8f 2026-11-02 Pavel Safronov | 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) | |
92cc4ee3 2026-11-02 Pavel Safronov | 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) | |
ebcd0a8f 2026-11-02 Pavel Safronov | 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) | |
92cc4ee3 2026-11-02 Pavel Safronov | 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) | |
644cfd0b 2026-10-20 Pavel Safronov | 991 | audio.ts Kenney pack sound effects (§13); ZzFX for any it lacks |
5551eb37 2026-10-20 Pavel Safronov | 992 | persist.ts localStorage save/load, export/import |
| 993 | main.ts | |
644cfd0b 2026-10-20 Pavel Safronov | 994 | assets/kenney/ Kenney New Platformer Pack sprites + License.txt (§13) |
5551eb37 2026-10-20 Pavel Safronov | 995 | static/ Vite publicDir (NOT public/ — see §17) |
| 996 | test/ Vitest | |
92cc4ee3 2026-11-02 Pavel Safronov | 997 | tools/ 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) | |
5551eb37 2026-10-20 Pavel Safronov | 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`, | |
644cfd0b 2026-10-20 Pavel Safronov | 1013 | `test` = `vitest run`, `screenshots`, `icons`. |
5551eb37 2026-10-20 Pavel Safronov | 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 | |
0b63d158 2026-10-25 Pavel Safronov | 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"). | |
5551eb37 2026-10-20 Pavel Safronov | 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 | |
644cfd0b 2026-10-20 Pavel Safronov | 1024 | (display fullscreen, icons, offline cache incl. the sprites). Read nordic-skating's file — its |
52d68b59 2026-10-24 Pavel Safronov | 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. | |
5551eb37 2026-10-20 Pavel Safronov | 1029 | - `.gitlab-ci.yml`: copy nordic-skating's (test stage, then `pages` with `mv dist public` and the |
644cfd0b 2026-10-20 Pavel Safronov | 1030 | "fail loudly" checks; add a check that the sprites were built). Project at |
2f667e95 2026-10-27 Pavel Safronov | 1031 | `code.fuzzyworld.net/explodorama`, served at `https://apps.fuzzyworld.net/explodorama/`. |
d603166d 2026-10-25 Pavel Safronov | 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 | |
5551eb37 2026-10-20 Pavel Safronov | 1034 | from §0), `design.md` (balance tables: enemy schedule, prices, drop rates, payout formula). |
| 1035 | ||
| 1036 | --- | |
| 1037 | ||
| 1038 | ## 18. Milestones | |
| 1039 | ||
| 1040 | 1. **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. | |
| 1042 | 2. **M1 – Sim core:** ✅ types, RNG streams, grid, Move, Wait, `step()` with all 5 phases | |
| 1043 | (enemy phases are marked placeholders), run record + replay, tests. | |
| 1044 | 3. **M2 – Bombs:** ✅ adjacent placement, timers, blast, chains, soft blocks, death, lives. | |
| 1045 | 4. **M3 – Mobile UI:** ✅ action bar, two-tap targeting, highlights, danger overlay, commit preview | |
644cfd0b 2026-10-20 Pavel Safronov | 1046 | (press-and-hold / hover), keyboard controls; Kenney sprites (2026-10-20). ⏳ No animation of moves yet |
5551eb37 2026-10-20 Pavel Safronov | 1047 | (instant), only a flame flash. |
c37a2b0b 2026-10-20 Pavel Safronov | 1048 | 5. **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). | |
| 1050 | 6. **M5 – Enemies:** ✅ Blob, Seeker, Drifter with intent arrows, both contact checks, coin and | |
82adee18 2026-10-20 Pavel Safronov | 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). | |
52d68b59 2026-10-24 Pavel Safronov | 1054 | 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), |
27f24fed 2026-10-20 Pavel Safronov | 1055 | Run and Detonate, the passes and Star, 50-level progression and ending, mid-run save |
e78b7781 2026-10-25 Pavel Safronov | 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.) |
ea380ee8 2026-10-22 Pavel Safronov | 1057 | 8. **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); | |
0b63d158 2026-10-25 Pavel Safronov | 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 |
5551eb37 2026-10-20 Pavel Safronov | 1060 | enemies and actions, screenshots tool. |
d603166d 2026-10-25 Pavel Safronov | 1061 | 9. **M8 – Records:** save export/import, victory certificate + QR, replay file and verify page. |
| 1062 | (The Workshop was here until 2026-10-24: dropped, §7.) | |
5551eb37 2026-10-20 Pavel Safronov | 1063 | 10. **Later:** Reapers + turn limit, daily seed, final art. |
33a0256b 2026-10-24 Pavel Safronov | 1064 | |
| 1065 | ### Backlog | |
| 1066 | ||
bb3823d2 2026-10-31 Pavel Safronov | 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 | |
d64916df 2026-11-01 Pavel Safronov | 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). | |
5f066960 2026-10-26 Pavel Safronov | 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 | |
fcf44f32 2026-10-25 Pavel Safronov | 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. | |
e6bb7c42 2026-10-25 Pavel Safronov | 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). | |
fcf44f32 2026-10-25 Pavel Safronov | 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 | |
e6bb7c42 2026-10-25 Pavel Safronov | 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, | |
5f066960 2026-10-26 Pavel Safronov | 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. | |
e6bb7c42 2026-10-25 Pavel Safronov | 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). | |
ebcd0a8f 2026-11-02 Pavel Safronov | 1103 | - **Crate Pass and Bomb Pass stay power-ups for now**: the user wants to playtest them more (the lab |
e6bb7c42 2026-10-25 Pavel Safronov | 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. | |
de91d371 2026-10-25 Pavel Safronov | 1106 | - **Decided (user, 2026-10-25): on a death, the carried charge drops where the player fell**, with |
fcf44f32 2026-10-25 Pavel Safronov | 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). | |
de91d371 2026-10-25 Pavel Safronov | 1110 | Was: **Lose the carried charge on a death? (user idea, 2026-10-25.)** Today the charge |
0bfdc8e6 2026-10-25 Pavel Safronov | 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'`). | |
d3e4ed26 2026-10-25 Pavel Safronov | 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**. | |
33a0256b 2026-10-24 Pavel Safronov | 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 | ||
b80fa925 2026-10-25 Pavel Safronov | 1133 | |
| 1134 | --- | |
| 1135 | ||
| 1136 | ## 19. Proposed features (user, 2026-10-24; to discuss, then prioritise) | |
| 1137 | ||
| 1138 | Five ideas from the user, worked out into proposals, then settled with the user (decisions marked | |
| 1139 | in 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 | ||
ebcd0a8f 2026-11-02 Pavel Safronov | 1143 | **The question:** some power-ups feel overpowered (Flame Pass, Crate Pass, Bomb Pass), but by how |
b80fa925 2026-10-25 Pavel Safronov | 1144 | much? 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 | |
| 1147 | played 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 | |
| 1149 | tree has more than 10^50 leaves. Plain DFS or BFS over whole games is out of reach. Search works | |
| 1150 | well *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 | |
ebcd0a8f 2026-11-02 Pavel Safronov | 1180 | (Flame Pass makes it 100%, which shows why it feels free); tiles reachable in 3 turns (Crate Pass, |
b80fa925 2026-10-25 Pavel Safronov | 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 | |
ebcd0a8f 2026-11-02 Pavel Safronov | 1187 | "Flame Pass as a charge of 5 turns", "Crate Pass as 3 uses"), so the same experiment compares |
b80fa925 2026-10-25 Pavel Safronov | 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 | |
| 1191 | as its first report). Still open: which power-ups to measure first — the passes, given 19.5. | |
| 1192 | ||
d3e4ed26 2026-10-25 Pavel Safronov | 1193 | **Built (2026-10-25):** `tools/balance/`, run with `npm run balance -- <command>` (README, "The |
| 1194 | balance lab"). Bots at three skill levels (`careful`: survives what it can see ahead, wins about | |
| 1195 | 80% of runs; `average` and `sloppy`: look 2 and 1 turns ahead and slip at times, clearing about 5 | |
| 1196 | and 3 levels). Experiments `powers` (each power-up held from level 1) and `structure` (escapable | |
| 1197 | bombs, reach in 3 turns, safe actions, on 1,781 real positions). Reports in `docs/balance/`. | |
| 1198 | The 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 | ||
fcf44f32 2026-10-25 Pavel Safronov | 1228 | **After Flame Pass and Teleport became charges, and the boards were fitted (2026-10-25, rules |
| 1229 | version 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 | ||
a313eb7d 2026-10-25 Pavel Safronov | 1242 | **Diagonal without squeezing (2026-10-25, user's question; `npm run balance -- diagonal`, 300 |
| 1243 | seeds, a lab-only rule switch `options.lab.noSqueeze`):** a diagonal step then needs one of the | |
| 1244 | two 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 | ||
| 1251 | Most of Diagonal's strength is the squeeze: without it, it is worth about what Kick is (+2.6 for | |
| 1252 | the average bot). The careful bot's stalls rise (4% → 9%), probably its own pathing with the rule | |
| 1253 | rather than the game; to look at. *Decided (user, 2026-10-25): no squeezing is the rule* (`SIM_VERSION` | |
| 1254 | 36); the lab's `options.lab.squeeze` brings the old rule back to compare. | |
| 1255 | ||
b80fa925 2026-10-25 Pavel Safronov | 1256 | ### 19.2 Bigger boards for later levels |
| 1257 | ||
ebcd0a8f 2026-11-02 Pavel Safronov | 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 | ||
b80fa925 2026-10-25 Pavel Safronov | 1261 | **Today:** every level is 9 × 11, the whole board on screen at once (a choice made 2026-10-23). It |
| 1262 | suits 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 | |
| 1265 | taller, 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 | ||
e6bb7c42 2026-10-25 Pavel Safronov | 1299 | **Built 2026-10-25** (`boardSize`, `concentration`, `ENEMY_THREAT` in sim/levelgen.ts; |
| 1300 | render/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). | |
fcf44f32 2026-10-25 Pavel Safronov | 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. | |
e6bb7c42 2026-10-25 Pavel Safronov | 1321 | |
b80fa925 2026-10-25 Pavel Safronov | 1322 | **Concentration: one number that sets how crowded a level is.** Today each level's enemy count is |
| 1323 | a 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 | |
| 1346 | hurt, explained by "What is that?" and in a new almanac tab, **Board**. New ones arrive a few | |
| 1347 | levels apart, as enemies do, so each is met on its own first. | |
| 1348 | ||
| 1349 | Sprites: mostly from the New Platformer Pack, already used. Spills from the **Splat Pack**: 36 | |
| 1350 | white splats that can be tinted to any colour (oil, acid, water). *Approved (user, 2026-10-24), | |
| 1351 | and 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 | | |
05e79f5f 2026-10-27 Pavel Safronov | 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 | |
b80fa925 2026-10-25 Pavel Safronov | 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 | |
| 1378 | find) and hide the player from chasers (something to use), while an enemy inside shows faint, | |
| 1379 | arrows still visible — so nothing that can kill you is ever hidden. | |
| 1380 | ||
5f066960 2026-10-26 Pavel Safronov | 1381 | *Built 2026-10-25 (the first five; `SIM_VERSION` 37):* from level 2 bushes, 3 metal crates, 5 oil, |
| 1382 | 8 spikes, 11 oil barrels (`OBJECTS_FROM`, first guesses), from their own random stream after the | |
| 1383 | rest of the level, so crates and enemies are where they were. As built, where the table above left | |
| 1384 | it open: a burning bush is flame that turn, and the bushes touching it **smoulder** and catch the | |
| 1385 | next; oil burns once (then it is gone) and lingers a turn like flame; a barrel's blast is 2 and its | |
| 1386 | spill covers its tile and the floor round it; it shows a 1 and shakes; **enemies never step on | |
| 1387 | spikes**, up or down (simpler to read than planning round the raised ones, and spikes then never | |
| 1388 | hurt them); a metal crate is placed only where every open tile stays reachable, and Kick doesn't | |
| 1389 | move 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 | |
6c3fcc31 2026-10-26 Pavel Safronov | 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) |
| 1392 | costs a life. *Also built the same day:* the almanac's **Board** tab (crates, | |
| 1393 | pillars, the five objects and rubble, with their first levels and clips) and a demo scene for each | |
| 1394 | object (a bush hiding you, a bush fire, metal stopping flame, oil, spikes, a barrel). Not yet: the | |
| 1395 | spikes' turn count. | |
1f0038ac 2026-11-01 Pavel Safronov | 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. |
5f066960 2026-10-26 Pavel Safronov | 1397 | |
b80fa925 2026-10-25 Pavel Safronov | 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 | |
| 1401 | part 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 | |
ebcd0a8f 2026-11-02 Pavel Safronov | 1413 | tiles become **rubble: impassable for good** (no blast, Pierce or Crate Pass gets through), so |
b80fa925 2026-10-25 Pavel Safronov | 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 | ||
| 1428 | Still to settle when building: whether the second hit must be any blast (as written) or another | |
| 1429 | acid bomb, and how many acid bombs a charge gives. | |
| 1430 | ||
5f066960 2026-10-26 Pavel Safronov | 1431 | *Built 2026-10-25:* **any later blast** breaks a cracked pillar (one cracked before that turn; an |
| 1432 | acid flame and a second blast in the same turn only crack it), **1 bomb** per charge (price 6), | |
| 1433 | in the gem's offers, the shop and enemies' drops like the other charges. The ceiling never falls on | |
| 1434 | 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 | |
| 1435 | enemy, unreachable from the player through all but walls, metal and rubble (flame can't cross those | |
| 1436 | either, so "reachable by flame" is the same test). Not yet: the acid puddle the bomb leaves (it | |
| 1437 | comes with 19.3's second batch), the floor-collapse alternative. | |
e3d8dc49 2026-10-27 Pavel Safronov | 1438 | *Changed 2026-10-26 (user request): **Acid is an area weapon, like the Nuke.** Its flame burns |
| 1439 | every tile within `NUKE_RANGE` (2) steps, through anything (the Nuke's diamond, on top of its | |
| 1440 | cross), and it cracks (or, cracked before, breaks) **every pillar in that diamond**, through | |
| 1441 | anything, not just the first pillar an arm of the cross runs into: a pillar diagonal to the bomb | |
| 1442 | was left untouched. The user chose the full Nuke area over cracks-only-in-area. `SIM_VERSION` 40.* | |
5f066960 2026-10-26 Pavel Safronov | 1443 | |
b80fa925 2026-10-25 Pavel Safronov | 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 | |
| 1447 | many 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 | ||
ebcd0a8f 2026-11-02 Pavel Safronov | 1467 | - **No passes in a loadout** (*decided, user, 2026-10-24*): Flame Pass, Crate Pass and Bomb Pass |
b80fa925 2026-10-25 Pavel Safronov | 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** | |
| 1471 | or **become temporary charges** (for example Flame Pass for 5 turns). Which one is the balance | |
| 1472 | lab's first experiment (19.1): the passes as they are, as charges of a few lengths, and gone, | |
| 1473 | compared on the same seeds. | |
| 1474 | ||
d3e4ed26 2026-10-25 Pavel Safronov | 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 | |
| 1478 | new one for a few coins and keep what you have, or **swap**: take the new one instead. Every | |
| 1479 | pickup 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 | |
| 1509 | report, so the lab can compare the game before and after on the same seeds. | |
| 1510 | ||
b80fa925 2026-10-25 Pavel Safronov | 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. | |
5588b4e7 2026-11-04 Pavel Safronov | 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 | |
| 1527 | could show *what the reporter saw* as a short looping animation, and (for autopilot or rules bugs) what | |
| 1528 | today'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 | ||
| 1544 | 1. **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=`. | |
| 1549 | 2. **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. | |
| 1553 | 3. **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. | |
| 1566 | 4. **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. | |
| 1571 | 5. **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 | ||
| 1587 | Related queue items: Q101 (the first report), Q102 (admin panel), Q103 (the dev page), Q104 (this). |