fuzzyworld

explodorama / commit

Deadly moves marked and refused by shortcuts; no tap-to-wait; Acid an area weapon

e3d8dc49752849b84b0a07e0678ec8e725669390 · Pavel Safronov · 2026-10-26 23:26:34 -0700 · parents 8711e8e51f73

Claude Code usage: this commit ~80.5K out / ~11.9M cached (first commit of its session: everything since it began) · cost unknown (no per-model trailer) · session 5 tally 200 in / 80.5K out / 11.9M cached · usage by commit and session

Deadly moves marked and refused by shortcuts; no tap-to-wait; Acid an area weapon

- A tap on yourself no longer waits (it could kill by accident).
- isDeadly (sim/danger.ts) plays an action on a copy with step() and says whether it
  costs a life this turn. Wait turns red when deadly; a picked button's deadly tiles are
  red with a cross. Taps, keys with nothing picked, the long press and auto-move refuse a
  deadly move (the tile flashes red, a buzz); the button then the tile still takes it.
- The Acid bomb burns the Nuke's area (every tile within 2 steps, through anything) and
  cracks every pillar in it, diagonal ones too. SIM_VERSION 40.
- Screenshots: deadly-wait, deadly-move, acid. Docs updated.

Usage: 0.2K in / 80.5K out / 11.9M cached (session tally)
rtk:   2.5M saved, 55.4% over 5252 cmds (global/lifetime)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
 CHANGELOG.md                     |   6 ++++
 PLAN.md                          |  16 +++++++++--
 README.md                        |  29 +++++++++++++++----
 decisions.md                     |  31 ++++++++++++++++++++
 docs/screenshots/acid.png        | Bin 0 -> 225076 bytes
 docs/screenshots/deadly-move.png | Bin 0 -> 219327 bytes
 docs/screenshots/deadly-wait.png | Bin 0 -> 219609 bytes
 src/main.ts                      |  60 ++++++++++++++++++++++++++++++---------
 src/render/board.ts              |  34 +++++++++++++++++++++-
 src/sim/danger.ts                |  14 ++++++++-
 src/sim/demo.ts                  |  11 +++----
 src/sim/explosions.ts            |  25 ++++++++++++----
 src/sim/picks.ts                 |  27 +++++++++++++-----
 src/sim/record.ts                |   2 +-
 src/style.css                    |   7 +++++
 src/ui/clip.ts                   |   5 ++--
 src/ui/controls.ts               |  14 +++++++--
 src/ui/info.ts                   |   6 ++--
 test/demo.test.ts                |  13 ++++++---
 test/hazards.test.ts             |  13 ++++++++-
 test/picks.test.ts               |  36 ++++++++++++++++++++++-
 tools/screenshots.mjs            |   6 ++++
 22 files changed, 301 insertions(+), 54 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 66be43f..868ab1f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,8 @@ saved before the change is dropped and the next visit starts a new run.
 
 ### Added
 
+- **Deadly moves are marked:** Wait turns red when waiting would cost you a life, and with a button picked, a tile that would is red with a cross. Taps, keys with nothing picked, the long press and auto-move refuse such a move (the tile flashes red); the button then the tile still makes it.
+
 - **Demo scenes for the board objects** (hiding in a bush, a bush fire, metal stopping flame, an oil spill, crossing spikes, a barrel going off), and a **Board** tab in the almanac listing crates, pillars, every object and rubble, with clips.
 - **Board objects:** bushes (they hide you from chasers, and burn a bush a turn), metal crates (nothing breaks them), oil spills (flame burns a whole spill at once), spikes (up every other turn) and oil barrels (flame bursts one: oil round it and a blast the next turn), each from its own level on. Version 37: runs saved before don't resume.
 - **The Acid charge:** a bomb that cracks pillars; a later blast breaks a cracked one, and 4 turns on the ceiling round it falls as rubble for good, crushing what is under it. Two new ways to lose a life: crushed, and walled in (trapped).
@@ -96,6 +98,10 @@ saved before the change is dropped and the next visit starts a new run.
 
 ### Changed
 
+- **The Acid bomb is an area weapon, like the Nuke:** it burns every tile within 2 steps, through crates, and cracks every pillar in that area, diagonal ones too (it used to burn a cross and crack only a pillar an arm of it ran into). Version 40: runs saved before don't resume.
+
+- **Tapping yourself no longer waits** (it could kill you by accident); use the Wait button or Space.
+
 - **Boxed in costs a life:** with nowhere to step and nowhere to put a bomb, and nothing left on the board that could change it (a Teleport into a gap between crates, rubble falling round you), you lose a life instead of being stuck for good. Version 39: runs saved before don't resume.
 - **The danger overlay marks a bomb's tiles from the moment it is placed**, as a red outline until its last two turns, however long the fuse.
 - **Shield and Flame Pass work by themselves while carried:** no button to press. A hit of their kind uses them up (Flame Pass after 3 fire hits) and the charge slot empties. Version 38: runs saved before don't resume.
diff --git a/PLAN.md b/PLAN.md
index 8c3e9ae..295e637 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -133,7 +133,14 @@ they first appear on).
 
 **Touch shortcuts (user request, 2026-10-20; built):**
 
-- **Tap the player** = Wait.
+- ~~**Tap the player** = Wait.~~ *Removed 2026-10-26 (user request): a stray tap on yourself
+  waited, which could kill. A tap on the player now does nothing; Wait is its button (Space).*
+- **Deadly moves (user request, 2026-10-26; built):** `isDeadly` (sim/danger.ts) plays an action
+  on a copy with `step()` and says whether the player loses a life this turn. Wait turns red when
+  deadly (still pressable); a picked button's deadly tiles are red with a cross (still tappable).
+  The shortcuts never take a deadly move: `tapMeaning` calls it `'deadly'` (the tile flashes red,
+  a short buzz), and the same goes for direction keys with nothing picked, the long press for a
+  bomb, and auto-move. The button then the tile still takes it. One turn ahead only.
 - **Press and hold** (450 ms) a free tile next to the player, with no action picked = Place bomb
   there. The bomb and its blast preview appear when the hold registers; release places it,
   sliding off cancels.
@@ -146,7 +153,7 @@ they first appear on).
   **Tapping the flag also takes one step** (user request, 2026-10-20). Tapping another far tile
   moves the flag; so does tapping the exit, the power-up or a drop (since 2026-10-22). Recomputed every turn; cleared on arrival, when unreachable, on Esc, by tapping
   a wall or crate, or on death / level change. Neither Go nor the flag takes a known-deadly step
-  (onto an enemy, onto an enemy's intent tile, or into flame this turn): the next step shows red
+  (onto an enemy, onto an enemy's intent tile, into flame this turn, or anything else `isDeadly` finds): the next step shows red
   and Go falls back to picking Move.
   **With no enemies left, auto-move walks by itself** (user request, 2026-10-22): one step 1 s
   after the last action, until the flag is reached — not while an action is picked, "What is
@@ -1390,6 +1397,11 @@ the exit or a gem. Crushed or trapped, a Star doesn't help; a Shield carried pus
 enemy, unreachable from the player through all but walls, metal and rubble (flame can't cross those
 either, so "reachable by flame" is the same test). Not yet: the acid puddle the bomb leaves (it
 comes with 19.3's second batch), the floor-collapse alternative.
+*Changed 2026-10-26 (user request): **Acid is an area weapon, like the Nuke.** Its flame burns
+every tile within `NUKE_RANGE` (2) steps, through anything (the Nuke's diamond, on top of its
+cross), and it cracks (or, cracked before, breaks) **every pillar in that diamond**, through
+anything, not just the first pillar an arm of the cross runs into: a pillar diagonal to the bomb
+was left untouched. The user chose the full Nuke area over cracks-only-in-area. `SIM_VERSION` 40.*
 
 ### 19.5 Gauntlet mode
 
diff --git a/README.md b/README.md
index b2073e6..c6c0f93 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ seven charges (one carried at a time, used when you choose), a hidden gem on eve
 mid-run work. Curses and the Reaper enemy are planned —
 see [PLAN.md](PLAN.md).
 
-Version: **39**, shown at the bottom of the menu with the commit the game was built from (a link
+Version: **40**, shown at the bottom of the menu with the commit the game was built from (a link
 to it on GitLab, plus "plus local changes" in a build with uncommitted changes) and a link to the
 [source code](https://code.fuzzyworld.net/explodorama/). The version is the rules version
 (`SIM_VERSION` in [src/sim/record.ts](src/sim/record.ts)): it goes up by one whenever a rule
@@ -90,7 +90,26 @@ they make Move and Bomb do more.
 |---|---|---|---|
 | **Move** | Tap a tile next to you, or tap **Move** and then any highlighted tile | **M**, then a direction — or W A S D / arrow keys straight away (Q E Z C diagonally, with Diagonal) | Step one tile up, down, left or right onto an empty tile. **With the Run power-up:** or 2 tiles in a straight line. **With Teleport:** or any empty tile up to 3 steps away (4 with Run too), over anything. **With Kick:** walking into a bomb kicks it. **With Diagonal:** or a diagonal step. **With Slide:** or slide in a straight line until a wall, crate or bomb stops you. |
 | **Bomb** | Tap **Bomb**, then a highlighted tile — or **press and hold** a tile next to you | **B**, then a direction | Place a bomb on an empty tile next to you. You can have 1 bomb on the board at a time. **With Throw:** or 2 tiles away in a straight line, over a crate, bomb or enemy (not a pillar). **With RPG:** or fired in a straight line, as far as it flies. Always a plain bomb: Pierce, Power, Nuke and ICBM are used with the charge button (see Charges). |
-| **Wait** | Tap **Wait**, or tap **yourself** | Space | Do nothing; the world takes its turn. |
+| **Wait** | Tap **Wait** | Space | Do nothing; the world takes its turn. (Tapping yourself did the same until 2026-10-26; now it does nothing, since a stray tap could kill you.) |
+
+**Deadly moves are marked, and the shortcuts refuse them** (since 2026-10-26). The game plays
+each choice out on a copy of the board, with the real rules, to see whether it would cost you a
+life **this turn** (a Shield or Flame Pass that would take the hit, or an Immortal run, means it
+wouldn't):
+
+- **Wait turns red** when waiting would kill you. It still works when pressed: sometimes there
+  is nothing better.
+- With a button picked, a tile that would kill you is **red with a cross** instead of blue. You
+  can still tap it, on purpose.
+- The **shortcuts never take a deadly move**: a tap on a tile with nothing picked, W A S D / the
+  arrow keys / Q E Z C with nothing picked, a long press for a bomb, and auto-move (Go, or a tap on
+  the destination). The tile flashes red (and the phone buzzes) instead. To make such a move
+  anyway, pick its button first (Move, then the tile).
+
+It only looks one turn ahead: walking into a dead end that a blast will fill next turn isn't
+marked; the danger overlay shows that.
+
+<img src="docs/screenshots/deadly-move.png" alt="Move picked next to a bomb about to go off: the tile back up into its blast is red with a cross, the safe tile below blue" width="260">
 
 When Move or Bomb can do several things, the tile you tap decides: next to you is a step (or a
 kick, into a bomb), 2 straight is a run, further is where a slide stops. Without picking anything, a tap on the tile where a slide stops slides there; next to you is a bomb, 2 away a throw, further an ICBM while you have launches, otherwise an
@@ -110,8 +129,8 @@ new way. Auto-move ends when you arrive, when you **move somewhere off the blue
 changed your mind), when the destination can no longer be reached, when you tap a wall or crate, or on **Esc**. The way **never crosses
 the open exit** (that would end the level on the way); it only steps onto the exit when the
 exit is where you are going. If the only way somewhere is over the open exit, auto-move can't go there.
-It **won't take a deadly step**: onto an enemy, onto a tile an enemy's arrow points at, or into
-flame this turn. The next step then shows red, and Go / the destination wait for you to deal with it
+It **won't take a deadly step**: onto an enemy, onto a tile an enemy's arrow points at, into
+flame this turn, or any other move that would cost a life this turn. The next step then shows red, and Go / the destination wait for you to deal with it
 (Go falls back to picking Move, so you can step aside yourself).
 **Once every enemy is gone**, the way needs no more presses: you walk there by yourself, two
 steps a second (still one turn per step).
@@ -188,7 +207,7 @@ piled up). The top bar shows it too.
 | **ICBM** | bomb with a target badge | 3 launches: the charge button fires a bomb as an RPG's (or **I**, then a direction) and it goes off the moment it lands, at any distance. Your bomb limit doesn't count. |
 | **Flame Pass** | flame with a shield badge | **Works by itself while you carry it:** flame can't hurt you for the next **3 fire hits** (an orange ring round you; the charge button and the top bar count them), level after level; when they are used the charge slot is empty. Enemies still can. A power-up that worked for good until 2026-10-25, when the balance lab found it won the game alone. |
 | **Teleport** | the Move figure with an outbox badge | **3 jumps**: press the charge button (or **P**), then tap any empty tile within 3 steps (counting across and down), over anything. **With Run, 4 steps;** **with Diagonal, a square** (a diagonal step counts as one). A power-up until 2026-10-25, for the same reason. |
-| **Acid** | bomb with a flask badge | **1 bomb** whose flame also **cracks** every pillar it reaches (cracks show on it). **Any later blast** that reaches a cracked pillar **breaks** it: its tile becomes floor, dust trickles from the ceiling over the 8 tiles round it, and **4 turns later** it falls as **rubble**, for good — nothing gets through rubble, not even flame. Whatever is under it is crushed: enemies (a trap to set), drops, bombs, and **you** — unless you carry a Shield, which pushes you out to an empty tile next to you (with none, nothing saves you; a Star doesn't). If the rubble cuts you off from the exit or from an enemy, the level can't be won: that costs a life at once (**trapped**). The outer wall never breaks. Since 2026-10-25. |
+| **Acid** | bomb with a flask badge | **1 bomb** that burns like a Nuke — **every tile within 2 steps** (across plus down), through crates and round corners — and **cracks every pillar** in that area, diagonal ones included (cracks show on it; since 2026-10-26: until then it burned a plain cross and cracked only a pillar an arm of it ran into). It can catch you too: get 3 steps away. **Any later blast** that reaches a cracked pillar **breaks** it: its tile becomes floor, dust trickles from the ceiling over the 8 tiles round it, and **4 turns later** it falls as **rubble**, for good — nothing gets through rubble, not even flame. Whatever is under it is crushed: enemies (a trap to set), drops, bombs, and **you** — unless you carry a Shield, which pushes you out to an empty tile next to you (with none, nothing saves you; a Star doesn't). If the rubble cuts you off from the exit or from an enemy, the level can't be won: that costs a life at once (**trapped**). The outer wall never breaks. Since 2026-10-25. |
 
 A Pierce, Power, Nuke or Acid bomb wears its charge's icon as a small badge. **Bomb** always puts down a
 plain bomb.
diff --git a/decisions.md b/decisions.md
index da0633a..61801a7 100644
--- a/decisions.md
+++ b/decisions.md
@@ -2210,3 +2210,34 @@ Desk is going away. `BUG_EMAIL` is now `bugs-k7q4+explodorama@fuzzyworld.net`: o
 address shared by Pavel's apps, with `+explodorama` telling them apart. The random part makes the
 address rotatable if it draws spam. (The old address was the *fuzzyworld* project's Service Desk,
 not this game's.) The email itself is unchanged.
+
+### 2026-10-26 — No tap-to-wait; deadly moves marked, and refused by the shortcuts
+
+User request: a tap on the player waited, which could be deadly; so could a tap in front of an
+enemy or into flame. Asked: remove the tap-yourself shortcut, and "mark or disallow" moves that
+guarantee a death, a button in red if pressing it would.
+- **A tap on yourself does nothing.** Wait is its button (Space). The Freeze charge's
+  confirming tap on yourself, with the charge button picked, stays.
+- **Deadly** means the player loses a life **this turn**, found by playing the action on a copy
+  with `step()` (`isDeadly`, sim/danger.ts), so it can't disagree with the rules: a Shield or Flame
+  Pass taking the hit, or Immortal, isn't deadly. Only one turn ahead: "every follow-up dies too"
+  (a dead end a blast fills next turn) would need a search, and later enemy moves aren't known yet.
+- **Both mark and disallow, split by how deliberate the input is.** Shortcuts (a tap with
+  nothing picked, direction keys with nothing picked, the long press, auto-move) refuse a deadly
+  move, with a red flash and a buzz. Deliberate inputs only mark it: Wait turns red, a picked
+  button's deadly tiles are red with a cross, and both still act. Disallowing those too could
+  leave a player with no move at all (every choice deadly, e.g. boxed in), and dying on purpose
+  is the player's call.
+- The demo's gestures follow the same rule (a deadly step is shown as Move then the tile), so
+  what it shows still works in play. No `SIM_VERSION` change: the rules are the same.
+
+### 2026-10-26 — Acid becomes an area weapon
+
+User report: an acid bomb placed diagonal to pillars left them untouched, since its flame was a
+plain cross and only the first pillar an arm ran into cracked. Asked for "an area weapon, like the
+nuke". Offered three readings (cracks in the Nuke's diamond with the usual cross of flame; the full
+Nuke area; cracks in the 8 tiles round it); **the user chose the full Nuke area**: the acid bomb
+burns every tile within `NUKE_RANGE` (2) steps, through anything, and every pillar in that diamond
+is reached (cracked, or broken if cracked before). It is now a Nuke plus cracks in a 1-bomb charge
+(the Nuke gives 3 bombs); its price (6) is unchanged, to revisit with the balance lab if it proves
+too strong. The Nuke itself still breaks a cracked pillar only along its cross. `SIM_VERSION` 40.
diff --git a/docs/screenshots/acid.png b/docs/screenshots/acid.png
new file mode 100644
index 0000000..76f03b7
Binary files /dev/null and b/docs/screenshots/acid.png differ
diff --git a/docs/screenshots/deadly-move.png b/docs/screenshots/deadly-move.png
new file mode 100644
index 0000000..fbefe43
Binary files /dev/null and b/docs/screenshots/deadly-move.png differ
diff --git a/docs/screenshots/deadly-wait.png b/docs/screenshots/deadly-wait.png
new file mode 100644
index 0000000..74151c0
Binary files /dev/null and b/docs/screenshots/deadly-wait.png differ
diff --git a/src/main.ts b/src/main.ts
index cdb6a42..e45c881 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -24,10 +24,10 @@ import { animateTurn as animateBoard, clearAnimation, newAnimation, pruneAnimati
 import { endPan, panView, resetPan, updateCamera, viewRect } from './render/camera'
 import { CHARGE_ICONS, MONO_ICONS, POWER_UP_ICONS, TEXTURES } from './render/sprites'
 import { isAvailable, isValidTarget, ACTIONS } from './sim/actions'
-import { canPick, membersOf, resolvePick, tapMeaning } from './sim/picks'
+import { canPick, membersOf, pickTargets, resolvePick, tapMeaning } from './sim/picks'
 import { enemyAt } from './sim/enemies'
 import { autoMoveAction, autoRoute, playerPath, safeStep as isSafeStep } from './sim/path'
-import { predictDanger, type Danger } from './sim/danger'
+import { isDeadly, predictDanger, type Danger } from './sim/danger'
 import { newRecord, replay, SIM_VERSION, type RunRecord } from './sim/record'
 import { cellAt, eq, inBounds, isBurning, newRun, PAR_BONUS } from './sim/state'
 import { LAST_LEVEL } from './sim/levelgen'
@@ -76,6 +76,15 @@ let snapCamera = true
 /** Auto-move: a far tile the player tapped. Each press of Move takes one step along `path` toward it. */
 let destination: Vec | null = null
 let path: Vec[] | null = null
+/**
+ * What would cost a life this turn (sim/danger.ts `isDeadly`; user request, 2026-10-26): Wait,
+ * the picked button's tiles, and auto-move's next move. Worked out once a turn, not every frame.
+ */
+let deadlyWait = false
+let deadlyTargets: Vec[] = []
+let deadlyAutoMove = false
+/** A tap refused because it would cost a life: the tile flashes red for a moment. */
+let refused: { tile: Vec; at: number } | null = null
 /** Long press: the adjacent tile a held finger has armed for a bomb, placed on release. */
 let armed: Vec | null = null
 let pressTimer: number | undefined
@@ -305,13 +314,24 @@ function afterChange(): void {
   if (destination && (eq(state.player, destination) || state.status !== 'playing')) destination = null
   path = destination ? playerPath(state, destination) : null
   if (!path) destination = null
-  controls.update(state, selected, destination !== null, inspecting)
+  deadlyWait = isAvailable(state, 'wait') && isDeadly(state, { id: 'wait' })
+  const picked = selected
+  deadlyTargets = picked
+    ? pickTargets(state, picked).filter((p) => {
+        const id = resolvePick(state, picked, p)
+        return !!id && isDeadly(state, { id, target: p })
+      })
+    : []
+  const next = destination && autoMoveAction(state, destination, safeStep)
+  deadlyAutoMove = !!next && isDeadly(state, next)
+  controls.update(state, selected, destination !== null, inspecting, deadlyWait)
 }
 
 /**
  * Two taps: pick an action, then a highlighted tile. With "What is that?" on: explain the tile.
- * With nothing picked:
- * - the player: Wait;
+ * With nothing picked (never a move that would cost a life: `tapMeaning` calls it 'deadly', and
+ * the tile flashes red; the button and then the tile still take it):
+ * - the player: nothing (it was Wait until 2026-10-26, user request: it could kill);
  * - the auto-move flag: one step toward it (like Go), with auto-move's safety check;
  * - a tile next to the player: move there; with Diagonal, a diagonal neighbour too (user report,
  *   2026-10-23: it was walked to round the corner, as auto-move);
@@ -341,7 +361,7 @@ function tapTile(p: Vec): void {
   } else {
     // What a tap means is worked out in the sim (picks.ts `tapMeaning`), shared with the demo.
     const means = tapMeaning(state, p)
-    if (means === 'wait') pick('wait')
+    if (means === 'deadly') refuse(p)
     else if (means === 'detonate' || means === 'kick') act({ id: means, target: p })
     else if (means === 'blocked') return
     else if (destination && eq(destination, p)) stepTowardDestination()
@@ -354,6 +374,12 @@ function tapTile(p: Vec): void {
   }
 }
 
+/** A tap or key that would cost a life, not taken: the tile flashes red, and a short buzz. */
+function refuse(p: Vec): void {
+  refused = { tile: p, at: time }
+  vibrate([30, 40, 30])
+}
+
 /** The "What is that?" button: on, or off again (dropping any action picked). */
 function inspect(): void {
   if (state.status !== 'playing') return
@@ -551,12 +577,12 @@ function safeStep(p: Vec): boolean {
 
 /**
  * One move of auto-move: a step, a Run or a Teleport, whichever gets closest (`autoMoveAction`),
- * only onto safe tiles. True if it moved.
+ * only onto safe tiles, and never one that would cost a life (`isDeadly`). True if it moved.
  */
 function stepTowardDestination(): boolean {
   if (!destination) return false
   const action = autoMoveAction(state, destination, safeStep)
-  if (!action) return false
+  if (!action || isDeadly(state, action)) return false
   act(action, true)
   return true
 }
@@ -605,7 +631,10 @@ function direction(d: Dir): void {
   for (const reach of reaches) {
     const p = { x: state.player.x + reach * DIRS[d].x, y: state.player.y + reach * DIRS[d].y }
     const id = resolvePick(state, picked, p)
-    if (id) return act({ id, target: p })
+    if (!id) continue
+    // Nothing picked, a key is a shortcut, as a tap is: never one that costs a life.
+    if (!selected && isDeadly(state, { id, target: p })) return refuse(p)
+    return act({ id, target: p })
   }
 }
 
@@ -652,7 +681,9 @@ window.addEventListener('keydown', (e) => {
   const diag = diagonalKeys ? DIAGONAL_KEYS[e.code] : undefined
   if (diag) {
     const p = { x: state.player.x + diag.x, y: state.player.y + diag.y }
-    if (isValidTarget(state, 'diagonal', p)) act({ id: 'diagonal', target: p })
+    if (!isValidTarget(state, 'diagonal', p)) return
+    if (!selected && isDeadly(state, { id: 'diagonal', target: p })) refuse(p)
+    else act({ id: 'diagonal', target: p })
   } else if (d) direction(d)
   else if (e.code === 'Space') pick('wait')
   else if (e.key === '?' || e.code === 'Slash') inspect()
@@ -683,6 +714,8 @@ function startPress(p: Vec | null): void {
   armed = null
   pressTile = p
   if (!p || selected || inspecting || state.status !== 'playing' || !isValidTarget(state, 'bomb', p)) return
+  // A shortcut, as a tap is: never a bomb that costs a life (with a fuse of 1).
+  if (isDeadly(state, { id: 'bomb', target: p })) return
   pressTimer = window.setTimeout(() => {
     armed = p
     navigator.vibrate?.(15)
@@ -1385,9 +1418,10 @@ await engineInit(
     // A dialog's clip first: it uses the canvas for a moment, then clears it (ui/clip.ts).
     drawClips()
     drawBoard({
-      state, danger, selected, hover, destination, path, armed, ...anim,
-      route: destination ? autoRoute(state, destination, safeStep) : null,
-      pathBlocked: !!path?.[0] && !safeStep(path[0]),
+      state, danger, selected, hover, destination, path, armed, ...anim, deadly: deadlyTargets, refused,
+      // A deadly next move shows as a blocked one: the first tile red.
+      route: destination && !deadlyAutoMove ? autoRoute(state, destination, safeStep) : null,
+      pathBlocked: (!!path?.[0] && !safeStep(path[0])) || deadlyAutoMove,
     })
     // What is out of sight on a board bigger than the view: arrows at its edge.
     if (!demo) drawIndicators(state, viewRect())
diff --git a/src/render/board.ts b/src/render/board.ts
index 6955fb0..0abea13 100644
--- a/src/render/board.ts
+++ b/src/render/board.ts
@@ -60,6 +60,10 @@ export interface View {
   pathBlocked: boolean
   /** A tile a long press has armed for a bomb. */
   armed: Vec | null
+  /** The picked button's tiles that would cost a life this turn (sim/danger.ts `isDeadly`): red. */
+  deadly?: Vec[]
+  /** A tap refused because it would cost a life, and when (LittleJS `time`): it flashes red. */
+  refused?: { tile: Vec; at: number } | null
   /** The player's latest move (from their change of tile) and pose, drawn move first (effects.ts). */
   playerMove: PlayerMove | null
   playerPose: PlayerPose
@@ -91,6 +95,10 @@ const C = {
   /** Auto-move's way, drawn as a line of arrows over its blue tiles. */
   pathLine: rgb(0.13, 0.42, 0.72, 0.9),
   targetHover: rgb(0.25, 0.62, 0.87, 0.7),
+  /** A picked button's tile that would cost a life: red with a cross, not blue. */
+  deadly: rgb(0.8, 0.1, 0.08, 0.5),
+  deadlyHover: rgb(0.8, 0.1, 0.08, 0.75),
+  deadlyCross: rgb(0.55, 0.05, 0.03, 0.9),
   preview: rgb(1, 0.55, 0.1, 0.4),
   flame: rgb(1, 0.6, 0.1),
   white: rgb(1, 1, 1),
@@ -131,6 +139,16 @@ const STONE_OUTLINE = 8 / 128
 export const BACKGROUND: Color = C.sky
 
 
+/** How long a refused tap's tile flashes red. */
+const REFUSED_SECONDS = 0.6
+
+/** An X across a tile: "this would cost a life". */
+function drawCross(at: Vector2, color: Color): void {
+  const r = 0.28
+  drawLine(at.add(vec2(-r, -r)), at.add(vec2(r, r)), 0.09, color)
+  drawLine(at.add(vec2(-r, r)), at.add(vec2(r, -r)), 0.09, color)
+}
+
 export function drawBoard(view: View): void {
   const { state: s, danger, selected, hover, fx } = view
   // A hair over a tile, so neighbouring walls overlap and no anti-aliased seam shows between them.
@@ -217,7 +235,21 @@ export function drawBoard(view: View): void {
   // The picked button's tiles: every action it stands for (sim/picks.ts).
   const targets = selected ? pickTargets(s, selected) : []
   const hovered = selected && hover && targets.some((t) => eq(t, hover)) ? hover : null
-  for (const t of targets) drawRect(toWorld(s, t), vec2(0.94), hovered && eq(t, hovered) ? C.targetHover : C.target)
+  for (const t of targets) {
+    const on = !!hovered && eq(t, hovered)
+    // One that would cost a life is red with a cross (user request, 2026-10-26): it can still be
+    // taken, on purpose.
+    if (view.deadly?.some((d) => eq(d, t))) {
+      drawRect(toWorld(s, t), vec2(0.94), on ? C.deadlyHover : C.deadly)
+      drawCross(toWorld(s, t), C.deadlyCross)
+    } else drawRect(toWorld(s, t), vec2(0.94), on ? C.targetHover : C.target)
+  }
+  // A tap refused because it would cost a life: the tile flashes red, then fades.
+  const flash = view.refused ? 1 - (time - view.refused.at) / REFUSED_SECONDS : 0
+  if (view.refused && flash > 0) {
+    drawRect(toWorld(s, view.refused.tile), vec2(0.94), C.deadlyHover.scale(1, flash))
+    drawCross(toWorld(s, view.refused.tile), C.deadlyCross.scale(1, flash))
+  }
   // Auto-move's way: every tile of it highlighted blue, the destination too, with no flag or dots
   // (user request, 2026-10-23). The next step is red when it isn't safe to take.
   // Since 2026-10-25 it shows where the player will land, move by move: with Run or Teleport, only
diff --git a/src/sim/danger.ts b/src/sim/danger.ts
index 42bc8dc..a1965f2 100644
--- a/src/sim/danger.ts
+++ b/src/sim/danger.ts
@@ -1,6 +1,7 @@
 import { collapse, detonate, proximityAlert, tickBombs } from './explosions'
 import { cloneState } from './state'
-import type { GameEvent, GameState } from './types'
+import { step } from './turn'
+import type { Action, GameEvent, GameState } from './types'
 
 /** Tile indices that will burn at the end of this turn, and at the end of the next one. */
 export interface Danger {
@@ -57,3 +58,14 @@ export function predictDanger(state: GameState): Danger {
   for (const i of [...now, ...next]) later.delete(i)
   return { now, next, later }
 }
+
+/**
+ * Whether `action` costs the player a life this turn (user request, 2026-10-26: a tap in front
+ * of an enemy, or into flame, did). It is played on a copy with `step()`, so it can't disagree
+ * with what happens: a Shield or Flame Pass that takes the hit, or an Immortal run, means it
+ * doesn't. False while no turn can be taken (an offer open, the level over).
+ */
+export function isDeadly(s: GameState, action: Action): boolean {
+  if (s.status !== 'playing' || s.offer || s.chargeOffer) return false
+  return step(s, action).events.some((e) => e.type === 'playerDied')
+}
diff --git a/src/sim/demo.ts b/src/sim/demo.ts
index 301eb69..47c86d2 100644
--- a/src/sim/demo.ts
+++ b/src/sim/demo.ts
@@ -1,5 +1,5 @@
 import { ACTIONS, isAvailable, isValidTarget } from './actions'
-import { predictDanger } from './danger'
+import { isDeadly, predictDanger } from './danger'
 import { blastTiles } from './explosions'
 import { planIntents } from './enemies'
 import { Rng } from './rng'
@@ -192,13 +192,13 @@ export const SCENES: Record<SceneKind, SceneDef> = {
         if (!far.length) return null
         const to = far[rng.int(far.length)]!
         const action = autoMoveAction(s, to, safe)
-        if (!action) return null
+        if (!action || isDeadly(s, action)) return null
         walking.to = to
         return { action, gesture: { kind: 'walk', tile: to } }
       }
-      // Not safe yet: wait for the way to clear (the destination stays).
+      // Not safe yet: wait for the way to clear (the destination stays). Go never takes a deadly move.
       const action = autoMoveAction(s, walking.to, safe)
-      return action ? { action, gesture: { kind: 'go' } } : { action: { id: 'wait' }, gesture: { kind: 'button', button: 'wait' } }
+      return action && !isDeadly(s, action) ? { action, gesture: { kind: 'go' } } : { action: { id: 'wait' }, gesture: { kind: 'button', button: 'wait' } }
     },
     shows: (t, seen) => !!seen.walk && at(t.next.player, seen.walk),
   },
@@ -855,7 +855,8 @@ function gestureFor(def: SceneDef, s: GameState, action: Action): Gesture | null
   if (action.id === 'wait') return { kind: 'button', button: 'wait' }
   if (!('target' in action) || !action.target) return null
   const tile = action.target
-  if (action.id === 'bomb' && !def.bombButton) return { kind: 'hold', tile }
+  // A long press never puts down a bomb that costs a life (main.ts `startPress`): its button does.
+  if (action.id === 'bomb' && !def.bombButton && !isDeadly(s, action)) return { kind: 'hold', tile }
   if (TAPPED.has(action.id) && tapMeaning(s, tile) === action.id) return { kind: 'tap', tile }
   const button: ButtonId = membersOf('move').includes(action.id) ? 'move' : membersOf('charged').includes(action.id) ? 'charged' : 'bomb'
   return resolvePick(s, button, tile) === action.id ? { kind: 'button', button, tile } : null
diff --git a/src/sim/explosions.ts b/src/sim/explosions.ts
index 7454afc..ebc34f5 100644
--- a/src/sim/explosions.ts
+++ b/src/sim/explosions.ts
@@ -7,9 +7,10 @@ import { DIRS, DIR_LIST, isSolid, type Bomb, type GameEvent, type GameState, typ
  * Flame stops before a wall, metal crate or rubble, and stops *on* a crate, an oil barrel or
  * another bomb (burning it). A Pierce bomb's flame carries on through crates, not barrels. A
  * Nuke bomb also burns every tile within NUKE_RANGE steps (across plus down), through anything,
- * except what is solid, which still stands.
+ * except what is solid, which still stands. So does an Acid bomb (user request, 2026-10-26: an
+ * area weapon like the Nuke; until then it burned a plain cross).
  */
-export function blastTiles(s: GameState, bomb: Pick<Bomb, 'pos' | 'fire' | 'pierce' | 'nuke'>): Vec[] {
+export function blastTiles(s: GameState, bomb: Pick<Bomb, 'pos' | 'fire' | 'pierce' | 'nuke' | 'acid'>): Vec[] {
   const tiles = [bomb.pos]
   for (const d of DIR_LIST) {
     let p = bomb.pos
@@ -21,7 +22,7 @@ export function blastTiles(s: GameState, bomb: Pick<Bomb, 'pos' | 'fire' | 'pier
       if ((cell === 'block' && !bomb.pierce) || cell === 'barrel' || bombAt(s, p)) break
     }
   }
-  if (bomb.nuke) {
+  if (bomb.nuke || bomb.acid) {
     for (let dy = -NUKE_RANGE; dy <= NUKE_RANGE; dy++) {
       for (let dx = -NUKE_RANGE; dx <= NUKE_RANGE; dx++) {
         const p = add(bomb.pos, { x: dx, y: dy })
@@ -38,15 +39,27 @@ export function isPillar(s: GameState, p: Vec): boolean {
   return cellAt(s, p) === 'wall' && p.x > 0 && p.y > 0 && p.x < s.width - 1 && p.y < s.height - 1
 }
 
-/** The pillars a bomb's flame runs into: where a flame, still within reach, stops at one. */
-export function pillarsReached(s: GameState, bomb: Pick<Bomb, 'pos' | 'fire' | 'pierce'>): Vec[] {
+/**
+ * The pillars a bomb's flame runs into: where a flame, still within reach, stops at one. An Acid
+ * bomb's also reaches every pillar within NUKE_RANGE steps, through anything (user request,
+ * 2026-10-26: one diagonal to it was left untouched).
+ */
+export function pillarsReached(s: GameState, bomb: Pick<Bomb, 'pos' | 'fire' | 'pierce' | 'acid'>): Vec[] {
   const pillars: Vec[] = []
+  if (bomb.acid) {
+    for (let dy = -NUKE_RANGE; dy <= NUKE_RANGE; dy++) {
+      for (let dx = -NUKE_RANGE; dx <= NUKE_RANGE; dx++) {
+        const p = add(bomb.pos, { x: dx, y: dy })
+        if (Math.abs(dx) + Math.abs(dy) <= NUKE_RANGE && isPillar(s, p)) pillars.push(p)
+      }
+    }
+  }
   for (const d of DIR_LIST) {
     let p = bomb.pos
     for (let i = 0; i < bomb.fire; i++) {
       p = add(p, DIRS[d])
       const cell = cellAt(s, p)
-      if (isPillar(s, p)) pillars.push(p)
+      if (isPillar(s, p) && !pillars.some((q) => eq(q, p))) pillars.push(p)
       if (isSolid(cell) || (cell === 'block' && !bomb.pierce) || cell === 'barrel' || bombAt(s, p)) break
     }
   }
diff --git a/src/sim/picks.ts b/src/sim/picks.ts
index f1de6cb..969444a 100644
--- a/src/sim/picks.ts
+++ b/src/sim/picks.ts
@@ -1,4 +1,5 @@
 import { ACTIONS, isAvailable, isValidTarget } from './actions'
+import { isDeadly } from './danger'
 import { enemyAt } from './enemies'
 import { playerPath } from './path'
 import { bombAt, cellAt, eq, flameSafe, isBurning } from './state'
@@ -49,19 +50,31 @@ export function canPick(s: GameState, picked: ActionId): boolean {
 }
 
 /** What a tap on a tile means with no button picked (`tapMeaning`). */
-export type TapMeaning = 'wait' | 'detonate' | 'kick' | 'blocked' | 'move' | 'diagonal' | 'slide' | 'walk' | 'teleport'
+export type TapMeaning = 'deadly' | 'detonate' | 'kick' | 'blocked' | 'move' | 'diagonal' | 'slide' | 'walk' | 'teleport'
+
+/** The meanings that are an action on the tapped tile, played at once. */
+const TAP_ACTIONS: ReadonlySet<TapMeaning> = new Set(['detonate', 'kick', 'move', 'diagonal', 'slide', 'teleport'])
 
 /**
  * What a tap on `p` means with nothing picked (main.ts `tapTile`; also the demo's taps), first
- * match wins: the player waits; one of your bombs, with the Detonator, goes off; a bomb next to
- * you, with Kick, is kicked; an enemy, any other bomb, or flame that can hurt you is 'blocked'
- * (never a move into it); then a step, a diagonal step, the tile where a slide stops (with Slide:
- * user report, 2026-10-24, it walked there instead), a far tile you can walk to ('walk': it
- * becomes auto-move's destination), or one you can only teleport to. Null: it means nothing.
+ * match wins: the player's own tile is 'blocked' (it was Wait until 2026-10-26, user request:
+ * a tap on yourself waited, which could kill); one of your bombs, with the Detonator, goes off; a
+ * bomb next to you, with Kick, is kicked; an enemy, any other bomb, or flame that can hurt you is
+ * 'blocked' (never a move into it); then a step, a diagonal step, the tile where a slide stops
+ * (with Slide: user report, 2026-10-24, it walked there instead), a far tile you can walk to
+ * ('walk': it becomes auto-move's destination), or one you can only teleport to. Null: it means
+ * nothing. An action that would cost a life this turn is 'deadly' instead (`isDeadly`, user
+ * request, 2026-10-26): a tap never takes it; its button still can.
  * Auto-move's own destination is the UI's to check: a tap on it is its next step.
  */
 export function tapMeaning(s: GameState, p: Vec): TapMeaning | null {
-  if (eq(p, s.player)) return 'wait'
+  const means = plainMeaning(s, p)
+  return means && TAP_ACTIONS.has(means) && isDeadly(s, { id: means as ActionId, target: p }) ? 'deadly' : means
+}
+
+/** `tapMeaning`, before asking whether it is deadly. */
+function plainMeaning(s: GameState, p: Vec): TapMeaning | null {
+  if (eq(p, s.player)) return 'blocked'
   if (isAvailable(s, 'detonate') && isValidTarget(s, 'detonate', p)) return 'detonate'
   if (isAvailable(s, 'kick') && isValidTarget(s, 'kick', p)) return 'kick'
   if (enemyAt(s, p) || bombAt(s, p) || (isBurning(s, p) && !flameSafe(s))) return 'blocked'
diff --git a/src/sim/record.ts b/src/sim/record.ts
index 0321dc1..fbf137e 100644
--- a/src/sim/record.ts
+++ b/src/sim/record.ts
@@ -6,7 +6,7 @@ import { DEFAULT_OPTIONS, type Action, type ChargeId, type GameEvent, type GameS
  * Bumped whenever a rule change would make an old record replay differently. Records from
  * another version can't be trusted to replay (PLAN.md §11).
  */
-export const SIM_VERSION = 39
+export const SIM_VERSION = 40
 
 /** Everything needed to reproduce a run exactly: mid-run saves, replays, certificates. */
 export interface RunRecord {
diff --git a/src/style.css b/src/style.css
index 5500094..cabd1ba 100644
--- a/src/style.css
+++ b/src/style.css
@@ -274,6 +274,13 @@ img {
   box-shadow: 0 4px 0 var(--accent-dark);
 }
 
+/* Wait, when waiting would cost a life (user request, 2026-10-26): red, like the danger overlay. */
+#actions > button.deadly:not(:disabled) {
+  border-color: #c0392b;
+  background: #ffd9d4;
+  box-shadow: 0 4px 0 #8e2a20;
+}
+
 #actions > button[aria-pressed='true'] {
   background: var(--accent);
   color: #fff;
diff --git a/src/ui/clip.ts b/src/ui/clip.ts
index b271e83..d5d258d 100644
--- a/src/ui/clip.ts
+++ b/src/ui/clip.ts
@@ -180,12 +180,13 @@ function play(c: Clip, action: Action): void {
 /**
  * A turn as the finger plays it (as the demo's, main.ts `beatsFor`, on the clip's own board): to
  * the tile, a tap or a long press — after a button, its tiles lit — and the turn on letting go.
- * Wait is a tap on the player (which waits, as in play); the action bar isn't in the clip.
+ * The action bar isn't in the clip, so Wait (its button) is just played, with no finger (a tap on
+ * the player waited until 2026-10-26).
  */
 function gestureBeats(c: Clip, g: Gesture, action: Action, last: boolean): Clip['beats'] {
   const s = c.state!
   // A choice in a dialog (the charge swap) has no tile on the board: the clip just plays it.
-  const tile = g.kind === 'go' || g.kind === 'choice' ? null : g.kind === 'button' ? (g.tile ?? s.player) : g.tile
+  const tile = g.kind === 'go' || g.kind === 'choice' ? null : g.kind === 'button' ? (g.tile ?? (g.button === 'wait' ? null : s.player)) : g.tile
   if (!tile) return [{ run: () => play(c, action), then: last ? HOLD_SECONDS : STEP_SECONDS }]
   const finger = c.finger!
   const hold = g.kind === 'hold'
diff --git a/src/ui/controls.ts b/src/ui/controls.ts
index e3c4fea..d24bb1b 100644
--- a/src/ui/controls.ts
+++ b/src/ui/controls.ts
@@ -161,8 +161,11 @@ export function createControls(handlers: Handlers) {
   }
 
   return {
-    /** `autoMove`: a destination is set, so Move takes one step toward it. `inspecting`: "What is that?" is on. */
-    update(s: GameState, selected: ActionId | null, autoMove: boolean, inspecting: boolean): void {
+    /**
+     * `autoMove`: a destination is set, so Move takes one step toward it. `inspecting`: "What is
+     * that?" is on. `deadlyWait`: waiting would cost a life this turn.
+     */
+    update(s: GameState, selected: ActionId | null, autoMove: boolean, inspecting: boolean, deadlyWait = false): void {
       inspectButton.setAttribute('aria-pressed', String(inspecting))
       inspectButton.disabled = s.status !== 'playing'
       current = s
@@ -187,6 +190,13 @@ export function createControls(handlers: Handlers) {
             b.setAttribute('aria-label', kind ? `${name} charge, ${s.charge!.left} left` : 'Charge: none carried')
           }
         }
+        if (def.id === 'wait') {
+          // Red when it would cost a life (user request, 2026-10-26). It still waits: with
+          // nothing else to do, that is the player's to choose.
+          b.classList.toggle('deadly', deadlyWait)
+          b.title = deadlyWait ? 'Wait (deadly!)' : def.label
+          b.setAttribute('aria-label', deadlyWait ? 'Wait: you would die' : def.label)
+        }
         if (def.id === 'move') {
           // Auto-move: Move has become Go, one step toward the destination per press. It keeps
           // the walk icon (user request, 2026-10-23: it used to turn into a flag); a green
diff --git a/src/ui/info.ts b/src/ui/info.ts
index b558901..7608bd2 100644
--- a/src/ui/info.ts
+++ b/src/ui/info.ts
@@ -75,7 +75,7 @@ export const CHARGE_TEXT: Record<ChargeId, { name: string; text: string }> = {
   icbm: { name: 'ICBM', text: `${BOMB_BOOST} launches: the charge button fires a bomb as far as it flies, as an RPG's, and it goes off the moment it lands. Your bomb limit doesn't count.` },
   // Power-ups until 2026-10-25 (the balance lab found each won the game alone): charges now.
   flamePass: { name: 'Flame Pass', text: `Works by itself while you carry it: flame can’t hurt you for the next ${FLAME_PASS_HITS} fire hits (and you may walk into flame), level after level. When they are used up the charge slot is empty. Enemies still can hurt you.` },
-  acid: { name: 'Acid', text: `One bomb whose flame cracks every pillar it reaches. Any blast that reaches a cracked pillar breaks it, and ${COLLAPSE_TURNS} turns later the ceiling round it comes down as rubble, for good: anything under it is crushed, you too (a Shield you carry pushes you out to an empty tile next to you). Don’t wall yourself in: if rubble cuts you off from the exit or an enemy, you lose a life. Put it down with the charge button.` },
+  acid: { name: 'Acid', text: `One bomb that burns every tile within ${NUKE_RANGE} steps, as a Nuke does, through crates and round corners, and cracks every pillar in that area. Get ${NUKE_RANGE + 1} steps away. Any blast that reaches a cracked pillar breaks it, and ${COLLAPSE_TURNS} turns later the ceiling round it comes down as rubble, for good: anything under it is crushed, you too (a Shield you carry pushes you out to an empty tile next to you). Don’t wall yourself in: if rubble cuts you off from the exit or an enemy, you lose a life. Put it down with the charge button.` },
   teleport: { name: 'Teleport', text: `${CHARGE_AMOUNT.teleport} jumps: press the charge button, then tap any empty tile up to ${TELEPORT_RANGE} steps away, over anything (${TELEPORT_RANGE + 1} with Run; with Diagonal, a diagonal step counts as one).` },
 }
 
@@ -360,7 +360,7 @@ export function tileInfo(s: GameState, p: Vec): Info | null {
       `Its blast reaches ${reach} in each direction. Flame stops at walls${bomb.pierce ? ' (it goes through crates)' : ' and on the first crate'}, and sets off any bomb it reaches.`,
     ]
     if (bomb.nuke) body.push(`Nuke: it also burns every tile within ${NUKE_RANGE} steps, through crates and round corners.`)
-    if (bomb.acid) body.push('Acid: its flame cracks every pillar it reaches.')
+    if (bomb.acid) body.push(`Acid: it burns every tile within ${NUKE_RANGE} steps, and cracks every pillar in that area.`)
     // Its clip: the charge that shapes its blast, or a bomb's countdown and blast.
     const clip = bomb.nuke ? 'nuke' : bomb.power ? 'power' : bomb.pierce ? 'pierce' : bomb.acid ? 'acid' : 'kill'
     return { icon: 'bomb', title: `${kinds.length ? `${kinds.join(' + ')} bomb` : 'Bomb'}: ${bomb.timer}`, body, clip }
@@ -535,7 +535,7 @@ export function actionInfo(s: GameState, picked: ActionId): Info {
       return { icon: CHARGE_ICONS[kind], title: `Charge: ${CHARGE_TEXT[kind].name}`, body, clip: kind }
     }
     case 'wait':
-      return { icon: 'icon_wait', title: 'Wait', body: ['Do nothing; the world takes its turn. Tapping yourself does the same.'], clip: 'wait' }
+      return { icon: 'icon_wait', title: 'Wait', body: ['Do nothing; the world takes its turn. The button turns red when waiting would cost you a life.'], clip: 'wait' }
   }
 }
 
diff --git a/test/demo.test.ts b/test/demo.test.ts
index 36165af..b0b1e29 100644
--- a/test/demo.test.ts
+++ b/test/demo.test.ts
@@ -1,6 +1,6 @@
 import { describe, expect, it } from 'vitest'
 import { isValidTarget } from '../src/sim/actions'
-import { predictDanger } from '../src/sim/danger'
+import { isDeadly, predictDanger } from '../src/sim/danger'
 import { generateScene, MAX_STEPS, MIN_STEPS, replayScene, SCENE_KINDS, SCENES, showsAtEnd, type Gesture } from '../src/sim/demo'
 import { autoMoveAction, safeStep } from '../src/sim/path'
 import { resolvePick, tapMeaning } from '../src/sim/picks'
@@ -19,9 +19,14 @@ function controlsTake(s: GameState, g: Gesture, destination: Vec | null): Action
       ? { id: means, target: g.tile }
       : null
   }
-  if (g.kind === 'hold') return isValidTarget(s, 'bomb', g.tile) ? { id: 'bomb', target: g.tile } : null
-  if (g.kind === 'walk') return tapMeaning(s, g.tile) === 'walk' ? autoMoveAction(s, g.tile, safe) : null
-  if (g.kind === 'go') return destination && autoMoveAction(s, destination, safe)
+  if (g.kind === 'hold') return isValidTarget(s, 'bomb', g.tile) && !isDeadly(s, { id: 'bomb', target: g.tile }) ? { id: 'bomb', target: g.tile } : null
+  // Auto-move never takes a move that costs a life.
+  const auto = (to: Vec) => {
+    const a = autoMoveAction(s, to, safe)
+    return a && !isDeadly(s, a) ? a : null
+  }
+  if (g.kind === 'walk') return tapMeaning(s, g.tile) === 'walk' ? auto(g.tile) : null
+  if (g.kind === 'go') return destination && auto(destination)
   if (g.kind === 'choice') return s.chargeOffer ? { id: g.swap ? 'swapCharge' : 'sellCharge' } : null
   if (g.button === 'wait') return { id: 'wait' }
   const id = g.tile && resolvePick(s, g.button, g.tile)
diff --git a/test/hazards.test.ts b/test/hazards.test.ts
index f224dbe..afb05be 100644
--- a/test/hazards.test.ts
+++ b/test/hazards.test.ts
@@ -1,7 +1,7 @@
 import { describe, expect, it } from 'vitest'
 import { predictDanger } from '../src/sim/danger'
 import { stepToward } from '../src/sim/enemies'
-import { COLLAPSE_TURNS } from '../src/sim/explosions'
+import { blastTiles, COLLAPSE_TURNS } from '../src/sim/explosions'
 import { generateLevel, OBJECTS_FROM } from '../src/sim/levelgen'
 import { idx } from '../src/sim/state'
 import { step } from '../src/sim/turn'
@@ -169,6 +169,17 @@ describe('Acid bomb (PLAN.md §19.4)', () => {
     expect(isSolid('rubble')).toBe(true)
   })
 
+  it('burns the Nuke’s area, and cracks every pillar in it, diagonal ones too (2026-10-26)', () => {
+    const s = room()
+    s.bombs.push({ pos: v(3, 3), timer: 1, fire: 1, placedTurn: 0, acid: true })
+    const r = wait(s).state
+    // (4, 2) is diagonal to the bomb: no arm of a cross reaches it.
+    expect(r.cracked).toEqual([idx(s, v(4, 2))])
+    expect(blastTiles(s, s.bombs[0]!)).toContainEqual(v(4, 3))
+    expect(blastTiles(s, s.bombs[0]!)).toContainEqual(v(2, 2))
+    expect(blastTiles(s, s.bombs[0]!)).toContainEqual(v(3, 1))
+  })
+
   it('a plain bomb doesn’t crack a pillar', () => {
     const s = room()
     s.bombs.push({ pos: v(4, 3), timer: 1, fire: 2, placedTurn: 0 })
diff --git a/test/picks.test.ts b/test/picks.test.ts
index 2427104..db1110e 100644
--- a/test/picks.test.ts
+++ b/test/picks.test.ts
@@ -1,5 +1,6 @@
 import { describe, expect, it } from 'vitest'
-import { canPick, pickTargets, resolvePick } from '../src/sim/picks'
+import { isDeadly } from '../src/sim/danger'
+import { canPick, pickTargets, resolvePick, tapMeaning } from '../src/sim/picks'
 import { board, v } from './helpers'
 
 describe('Move stands for every way of moving', () => {
@@ -95,3 +96,36 @@ describe('Bomb stands for placing and throwing', () => {
     expect(resolvePick(s, 'teleport', v(1, 1))).toBeNull()
   })
 })
+
+describe('a tap never costs a life (user request, 2026-10-26)', () => {
+  it('a tap on yourself does nothing (it was Wait)', () => {
+    const s = board(`
+      #####
+      #.P.#
+      #####`)
+    expect(tapMeaning(s, v(2, 1))).toBe('blocked')
+  })
+
+  it('a step in front of an enemy is deadly: a tap refuses it, Move then the tile still takes it', () => {
+    const s = board(`
+      ######
+      #P.B.#
+      ######`)
+    s.enemies[0]!.plan = [v(2, 1)]
+    expect(tapMeaning(s, v(2, 1))).toBe('deadly')
+    expect(resolvePick(s, 'move', v(2, 1))).toBe('move')
+    expect(isDeadly(s, { id: 'move', target: v(2, 1) })).toBe(true)
+    expect(isDeadly(s, { id: 'wait' })).toBe(false)
+  })
+
+  it('Wait is deadly when an enemy steps onto you, unless a Shield or Immortal takes the hit', () => {
+    const s = board(`
+      #####
+      #PB.#
+      #####`)
+    s.enemies[0]!.plan = [v(1, 1)]
+    expect(isDeadly(s, { id: 'wait' })).toBe(true)
+    expect(isDeadly({ ...s, charge: { kind: 'shield', left: 1 } }, { id: 'wait' })).toBe(false)
+    expect(isDeadly({ ...s, options: { ...s.options, immortal: true } }, { id: 'wait' })).toBe(false)
+  })
+})
diff --git a/tools/screenshots.mjs b/tools/screenshots.mjs
index 4d78e14..501736e 100644
--- a/tools/screenshots.mjs
+++ b/tools/screenshots.mjs
@@ -45,6 +45,10 @@ const SHOTS = [
   { name: 'lingering', act: press('KeyB', 'KeyD', 'KeyS', 'Space', 'Space') },
   // Standing next to our own bomb as it goes off.
   { name: 'death', act: press('KeyB', 'KeyD', 'Space', 'Space', 'Space') },
+  // One turn before that: waiting now would cost a life, so Wait is red...
+  { name: 'deadly-wait', act: press('KeyB', 'KeyD', 'Space', 'Space') },
+  // Stepped down out of the blast instead: with Move picked, the step back up into it is red with a cross.
+  { name: 'deadly-move', act: press('KeyB', 'KeyD', 'KeyS', 'Space', 'KeyM') },
   { name: 'desktop', viewport: DESKTOP, act: press('KeyB', 'KeyD', 'KeyS') },
   // Level 13 has all three enemy types so far, each with its intent arrow.
   { name: 'enemies', query: 'level=19', act: press('Space', 'Space') },
@@ -63,6 +67,8 @@ const SHOTS = [
   // Carrying a Nuke (?charge=nuke): the charge button (V) puts a Nuke bomb to the right, and after
   // a step down the danger shows its reach; the top bar and the button show what is left.
   { name: 'nuke', query: 'charge=nuke', act: press('KeyV', 'KeyD', 'KeyS') },
+  // Carrying Acid: its bomb's danger covers the Nuke's area (2026-10-26), with pillars diagonal to it.
+  { name: 'acid', query: 'charge=acid', act: press('KeyV', 'KeyS', 'KeyD') },
   // Seed 107: a bomb by the gem's crate, out of the way while it goes off, then back: the gem
   // uncovered beside the player... (found by searching random play with the sim, 2026-10-24; the
   // old seed-55 moves no longer reached a gem)