explodorama / commit
Diagonal no longer squeezes between two blocked tiles
CHANGELOG.md | 1 + PLAN.md | 14 ++++++++++++++ README.md | 4 ++-- decisions.md | 12 ++++++++++++ docs/balance/diagonal-average.md | 40 +++++++++++++++++++++++++++++++++++++++ docs/balance/diagonal-careful.md | 40 +++++++++++++++++++++++++++++++++++++++ docs/screenshots/almanac.png | Bin 514897 -> 521865 bytes docs/screenshots/auto-move.png | Bin 216796 -> 216771 bytes docs/screenshots/bomb-picked.png | Bin 215828 -> 215850 bytes docs/screenshots/charges.png | Bin 224644 -> 224630 bytes docs/screenshots/countdown.png | Bin 219691 -> 219721 bytes docs/screenshots/death.png | Bin 233745 -> 233723 bytes docs/screenshots/demo.png | Bin 958036 -> 952035 bytes docs/screenshots/desktop.png | Bin 270193 -> 270235 bytes docs/screenshots/dev-offer.png | Bin 198900 -> 218864 bytes docs/screenshots/enemies.png | Bin 246259 -> 246353 bytes docs/screenshots/gem-offer.png | Bin 207918 -> 207227 bytes docs/screenshots/gem.png | Bin 200124 -> 200172 bytes docs/screenshots/info-hud.png | Bin 182283 -> 178628 bytes docs/screenshots/info-mode.png | Bin 214142 -> 214113 bytes docs/screenshots/lingering.png | Bin 219247 -> 219165 bytes docs/screenshots/long-press.png | Bin 219450 -> 219398 bytes docs/screenshots/menu.png | Bin 151057 -> 150195 bytes docs/screenshots/move-picked.png | Bin 216082 -> 216088 bytes docs/screenshots/nuke.png | Bin 231959 -> 231990 bytes docs/screenshots/shop.png | Bin 209118 -> 209166 bytes docs/screenshots/start.png | Bin 215447 -> 215464 bytes src/sim/actions.ts | 16 ++++++++++++++-- src/sim/path.ts | 5 +++-- src/sim/record.ts | 2 +- src/sim/types.ts | 5 +++++ src/ui/info.ts | 2 +- test/charges.test.ts | 4 ++-- test/new-powers.test.ts | 17 +++++++++++++++++ test/picks.test.ts | 2 +- tools/balance/lab.ts | 19 ++++++++++++++++++- 36 files changed, 171 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8454cd7..f0e5501 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -93,6 +93,7 @@ saved before the change is dropped and the next visit starts a new run. ### Changed +- **Diagonal no longer squeezes** between two blocked tiles: one of the two tiles beside a diagonal step must be open. The squeeze was most of what made Diagonal so strong. Version 36. - **Flame Pass and Teleport are charges now**, not power-ups: Flame Pass, raised with the charge button, takes the next 3 fire hits; Teleport gives 3 jumps from the charge button. Each alone used to win the game. - **A death drops the charge you carry** where you fell, to pick up again on the retry. - **Board sizes fitted:** up to 13 × 17, so late levels take about as long as early ones; slightly more enemies on late levels. Version 35. diff --git a/PLAN.md b/PLAN.md index 7fe5c4a..5ca043f 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1192,6 +1192,20 @@ version 35),** 150 seeds, now also each charge carried from level 1: bots use charges crudely (whenever their scoring likes it), so these say little about a person. - The careful bot: 91% of runs won (82% under rules 28), about 84 turns a level, stalls 3%. +**Diagonal without squeezing (2026-10-25, user's question; `npm run balance -- diagonal`, 300 +seeds, a lab-only rule switch `options.lab.noSqueeze`):** a diagonal step then needs one of the +two tiles beside it open. + +| | Average bot | Careful bot | +|---|---|---| +| Diagonal as it is | +7.7 levels (wins +11%) | +1.0 levels (wins +8%) | +| Diagonal, no squeeze | **+3.4 levels** (wins +1%) | +0.3 (not significant), but 9% of runs stall | + +Most of Diagonal's strength is the squeeze: without it, it is worth about what Kick is (+2.6 for +the average bot). The careful bot's stalls rise (4% → 9%), probably its own pathing with the rule +rather than the game; to look at. *Decided (user, 2026-10-25): no squeezing is the rule* (`SIM_VERSION` +36); the lab's `options.lab.squeeze` brings the old rule back to compare. + ### 19.2 Bigger boards for later levels **Today:** every level is 9 × 11, the whole board on screen at once (a choice made 2026-10-23). It diff --git a/README.md b/README.md index 543c295..bbad3e1 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: **35**, shown at the bottom of the menu with the commit the game was built from (a link +Version: **36**, 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://gitlab.com/PavelSafronov/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 @@ -310,7 +310,7 @@ go: **Blast 2 → 4 (max 5)**. | **Run** | the Move figure with a fast-forward badge | **Move** can run: 2 tiles in a straight line in one turn (or **R**, then a direction). Both tiles must be empty; an enemy on the tile in between is run into. Anything on that tile is picked up. | | **Kick** | bomb with a right-arrow badge | Walk into a bomb next to you and it slides away until the next tile is blocked (a wall, crate, bomb or enemy), keeping its number. | | **Throw** | bomb with an up-right-arrow badge | **Bomb** can also go exactly 2 tiles away in a straight line, over a crate, bomb or enemy in between — but not over a pillar or the outer wall (or **T**, then a direction). | -| **Diagonal** | the Move figure with a diagonal-arrows badge | **Move** can also step diagonally, even squeezing between two blocked tiles (or **Q E Z C**: up-left, up-right, down-left, down-right); auto-move cuts corners with it. With the Teleport charge, it widens Teleport's reach to a square. A charge until 2026-10-23. | +| **Diagonal** | the Move figure with a diagonal-arrows badge | **Move** can also step diagonally, where one of the two tiles beside the step is open — not between two blocked tiles (no squeezing since 2026-10-25; the lab found the squeeze was most of its strength) (or **Q E Z C**: up-left, up-right, down-left, down-right); auto-move cuts corners with it. With the Teleport charge, it widens Teleport's reach to a square. A charge until 2026-10-23. | | **Slide** | the Move figure with a two-way-arrow badge | **Move** can also slide in a straight line until the next tile is a wall, crate or bomb — **tap the tile where it stops** (or **L**, then a direction) — picking up whatever it passes. It can't stop early: an enemy on the way is crashed into (a life lost), and flame on the way burns (unless Flame Pass or a Star). The open exit stops it. | | **RPG** | bomb with a forward-arrow badge | **Bomb** can also fire a bomb in a straight line (or **F**, then a direction): it flies until the next tile is a wall, crate, bomb or enemy and lands just before it, then counts down as usual. | | **Detonator** | bomb with a ! badge | Your bombs pulse with a red glow: **tap one** and it goes off this turn (or **X**, then tap it). No button. | diff --git a/decisions.md b/decisions.md index 8e8dc42..7bb9398 100644 --- a/decisions.md +++ b/decisions.md @@ -2091,3 +2091,15 @@ The user's order: stages 1–3 of the plan. A raised Shield or Flame Pass is kept. A pending choice now carries how much is left too. `SIM_VERSION` 35. The lab now also measures each charge carried from level 1. + +### 2026-10-25 — Measured: Diagonal without squeezing + +User question: how would a no-squeeze Diagonal change things? A lab-only rule switch +(`RunOptions.lab.noSqueeze`, never set in play; the game is unchanged) makes a diagonal step need one +of the two tiles beside it open, for the action and for auto-move's walk alike. On 300 seeds: the +average bot gains +3.4 levels with it instead of +7.7 (wins +1% instead of +11%); the careful bot ++0.3 instead of +1.0, with more stalled runs (9%). So the squeeze is most of Diagonal's strength. +Left to the user to decide. +- **Decided (user, same day): no squeezing is the rule.** A diagonal step needs one of the two + tiles beside it passable, for the action and auto-move alike. The lab keeps the old rule as a + variant (`options.lab.squeeze`) to compare against. `SIM_VERSION` 36. diff --git a/docs/balance/diagonal-average.md b/docs/balance/diagonal-average.md new file mode 100644 index 0000000..fcc0978 --- /dev/null +++ b/docs/balance/diagonal-average.md @@ -0,0 +1,40 @@ +# Balance lab: diagonal (average bot) + +2026-10-25 · rules version 35 · 300 seeds per arm · 79 s. + +Each arm plays the same seeds as the baseline; its columns are the difference from the baseline, +per run, with a 95% interval. ✱ marks a difference the interval says is real. Sorted by levels +gained. The bots answer a gem by taking the first thing offered and never shop. + +| Arm | Levels cleared | Won | Lives lost | Turns per level | Turns in danger | Stalled | +|---|---|---|---|---|---|---| +| **baseline** | 4.42 | 0% | 2.98 | 74.5 | 3.5% | 1% | +| diagonal | +7.68 ± 0.62 ✱ | +11 ± 4 ✱% | −0.22 ± 0.08 ✱ | −7.8 ± 2.7 ✱ | −0.2 ± 0.2 ✱ | 1% | +| diagonal, no squeeze | +3.42 ± 0.52 ✱ | +1 ± 1% | −0.05 ± 0.05 | −4.8 ± 3.9 ✱ | +0.2 ± 0.2 | 2% | + +## The baseline, level by level + +| Level | Runs reaching it | Lives lost there | Turns there | Within par | +|---|---|---|---|---| +| 1 | 100% | 0.18 | 73 | 14% | +| 2 | 97% | 0.39 | 76 | 23% | +| 3 | 90% | 0.56 | 67 | 27% | +| 4 | 75% | 0.85 | 87 | 26% | +| 5 | 54% | 0.78 | 83 | 24% | +| 6 | 37% | 0.75 | 75 | 30% | +| 7 | 25% | 0.66 | 89 | 29% | +| 8 | 19% | 0.79 | 97 | 27% | +| 9 | 14% | 0.61 | 75 | 37% | +| 10 | 10% | 1.03 | 75 | 35% | +| 11 | 6% | 0.47 | 56 | 53% | +| 12 | 4% | 0.25 | 79 | 50% | +| 13 | 3% | 0.20 | 65 | 80% | +| 14 | 3% | 0.67 | 64 | 67% | +| 15 | 2% | 0.17 | 76 | 83% | +| 16 | 2% | 0.60 | 40 | 60% | +| 17 | 1% | 1.00 | 25 | 33% | +| 18 | 0% | 0.00 | 64 | 100% | +| 19 | 0% | 1.00 | 73 | 100% | +| 20 | 0% | 0.00 | 64 | 100% | + +Lives lost in the baseline, by cause: blast 786, chain 13, lingering 13, walkedIntoEnemy 60, caughtByEnemy 22. diff --git a/docs/balance/diagonal-careful.md b/docs/balance/diagonal-careful.md new file mode 100644 index 0000000..397b103 --- /dev/null +++ b/docs/balance/diagonal-careful.md @@ -0,0 +1,40 @@ +# Balance lab: diagonal (careful bot) + +2026-10-25 · rules version 35 · 300 seeds per arm · 426 s. + +Each arm plays the same seeds as the baseline; its columns are the difference from the baseline, +per run, with a 95% interval. ✱ marks a difference the interval says is real. Sorted by levels +gained. The bots answer a gem by taking the first thing offered and never shop. + +| Arm | Levels cleared | Won | Lives lost | Turns per level | Turns in danger | Stalled | +|---|---|---|---|---|---|---| +| **baseline** | 18.84 | 90% | 0.36 | 82.4 | 5.1% | 4% | +| diagonal | +1.02 ± 0.43 ✱ | +8 ± 3 ✱% | −0.24 ± 0.10 ✱ | −11.2 ± 1.8 ✱ | −2.2 ± 0.2 ✱ | 1% | +| diagonal, no squeeze | +0.27 ± 0.56 | 0 ± 5% | −0.24 ± 0.10 ✱ | −6.7 ± 2.2 ✱ | −2.5 ± 0.2 ✱ | 9% | + +## The baseline, level by level + +| Level | Runs reaching it | Lives lost there | Turns there | Within par | +|---|---|---|---|---| +| 1 | 100% | 0.01 | 68 | 18% | +| 2 | 100% | 0.03 | 69 | 29% | +| 3 | 99% | 0.00 | 64 | 32% | +| 4 | 99% | 0.01 | 81 | 36% | +| 5 | 99% | 0.04 | 82 | 39% | +| 6 | 98% | 0.05 | 78 | 46% | +| 7 | 97% | 0.01 | 86 | 44% | +| 8 | 96% | 0.01 | 98 | 47% | +| 9 | 95% | 0.03 | 91 | 51% | +| 10 | 94% | 0.01 | 86 | 57% | +| 11 | 94% | 0.00 | 83 | 67% | +| 12 | 94% | 0.03 | 96 | 64% | +| 13 | 93% | 0.02 | 87 | 81% | +| 14 | 92% | 0.01 | 81 | 83% | +| 15 | 92% | 0.04 | 76 | 88% | +| 16 | 91% | 0.04 | 91 | 88% | +| 17 | 91% | 0.00 | 87 | 90% | +| 18 | 91% | 0.01 | 85 | 92% | +| 19 | 91% | 0.03 | 81 | 94% | +| 20 | 90% | 0.00 | 74 | 98% | + +Lives lost in the baseline, by cause: walkedIntoEnemy 63, blast 30, caughtByEnemy 13, lingering 1. diff --git a/docs/screenshots/almanac.png b/docs/screenshots/almanac.png index ee8922f..b036e4b 100644 Binary files a/docs/screenshots/almanac.png and b/docs/screenshots/almanac.png differ diff --git a/docs/screenshots/auto-move.png b/docs/screenshots/auto-move.png index a7825b8..208fec1 100644 Binary files a/docs/screenshots/auto-move.png and b/docs/screenshots/auto-move.png differ diff --git a/docs/screenshots/bomb-picked.png b/docs/screenshots/bomb-picked.png index 6c627c9..406f055 100644 Binary files a/docs/screenshots/bomb-picked.png and b/docs/screenshots/bomb-picked.png differ diff --git a/docs/screenshots/charges.png b/docs/screenshots/charges.png index f743733..bba8c13 100644 Binary files a/docs/screenshots/charges.png and b/docs/screenshots/charges.png differ diff --git a/docs/screenshots/countdown.png b/docs/screenshots/countdown.png index f49cd68..cfc73ad 100644 Binary files a/docs/screenshots/countdown.png and b/docs/screenshots/countdown.png differ diff --git a/docs/screenshots/death.png b/docs/screenshots/death.png index a4a2a8b..61aa5ab 100644 Binary files a/docs/screenshots/death.png and b/docs/screenshots/death.png differ diff --git a/docs/screenshots/demo.png b/docs/screenshots/demo.png index fb92286..b1be056 100644 Binary files a/docs/screenshots/demo.png and b/docs/screenshots/demo.png differ diff --git a/docs/screenshots/desktop.png b/docs/screenshots/desktop.png index 928815e..d3d7f06 100644 Binary files a/docs/screenshots/desktop.png and b/docs/screenshots/desktop.png differ diff --git a/docs/screenshots/dev-offer.png b/docs/screenshots/dev-offer.png index 7f5c2f3..b100dd2 100644 Binary files a/docs/screenshots/dev-offer.png and b/docs/screenshots/dev-offer.png differ diff --git a/docs/screenshots/enemies.png b/docs/screenshots/enemies.png index 0fb7762..19fcba8 100644 Binary files a/docs/screenshots/enemies.png and b/docs/screenshots/enemies.png differ diff --git a/docs/screenshots/gem-offer.png b/docs/screenshots/gem-offer.png index e398cab..422cd18 100644 Binary files a/docs/screenshots/gem-offer.png and b/docs/screenshots/gem-offer.png differ diff --git a/docs/screenshots/gem.png b/docs/screenshots/gem.png index cb161cb..610f9a3 100644 Binary files a/docs/screenshots/gem.png and b/docs/screenshots/gem.png differ diff --git a/docs/screenshots/info-hud.png b/docs/screenshots/info-hud.png index 25b68e7..66a61b7 100644 Binary files a/docs/screenshots/info-hud.png and b/docs/screenshots/info-hud.png differ diff --git a/docs/screenshots/info-mode.png b/docs/screenshots/info-mode.png index 32da675..34ce5e7 100644 Binary files a/docs/screenshots/info-mode.png and b/docs/screenshots/info-mode.png differ diff --git a/docs/screenshots/lingering.png b/docs/screenshots/lingering.png index ce39c0b..5a9e8b8 100644 Binary files a/docs/screenshots/lingering.png and b/docs/screenshots/lingering.png differ diff --git a/docs/screenshots/long-press.png b/docs/screenshots/long-press.png index 2184f30..ffbd691 100644 Binary files a/docs/screenshots/long-press.png and b/docs/screenshots/long-press.png differ diff --git a/docs/screenshots/menu.png b/docs/screenshots/menu.png index 4d54205..e29d4eb 100644 Binary files a/docs/screenshots/menu.png and b/docs/screenshots/menu.png differ diff --git a/docs/screenshots/move-picked.png b/docs/screenshots/move-picked.png index e05b382..b128d72 100644 Binary files a/docs/screenshots/move-picked.png and b/docs/screenshots/move-picked.png differ diff --git a/docs/screenshots/nuke.png b/docs/screenshots/nuke.png index 78b13c7..a1b38d3 100644 Binary files a/docs/screenshots/nuke.png and b/docs/screenshots/nuke.png differ diff --git a/docs/screenshots/shop.png b/docs/screenshots/shop.png index 2759f9a..59dcacc 100644 Binary files a/docs/screenshots/shop.png and b/docs/screenshots/shop.png differ diff --git a/docs/screenshots/start.png b/docs/screenshots/start.png index 12386de..4b4846d 100644 Binary files a/docs/screenshots/start.png and b/docs/screenshots/start.png differ diff --git a/src/sim/actions.ts b/src/sim/actions.ts index 05f84d4..21ae1e8 100644 --- a/src/sim/actions.ts +++ b/src/sim/actions.ts @@ -1,6 +1,6 @@ import { BOMB_CHARGES, FLAME_PASS_HITS, holds, spendCharge } from './charges' import { enemyAt } from './enemies' -import { add, bombAt, cellAt, eq, exitOpen, isBurning, isFree, playerCanEnter } from './state' +import { add, bombAt, cellAt, eq, exitOpen, isBurning, isFree, playerCanEnter, playerCanPass } from './state' import { DIRS, DIR_LIST, type ActionId, type Bomb, type GameEvent, type GameState, type Powers, type Vec } from './types' /** @@ -88,6 +88,17 @@ export function slidePath(s: GameState, d: Vec): Vec[] { return path } +/** + * Whether a diagonal step from `from` by `d` may be taken: one of the two tiles beside the step + * must be passable — no squeezing between two blocked tiles (user's choice, 2026-10-25: the lab + * found the squeeze was most of Diagonal's strength, +7.7 levels for the average bot, +3.4 + * without). The lab's `squeeze` variant brings the old rule back, to compare. + */ +export function diagonalAllowed(s: GameState, from: Vec, d: Vec): boolean { + if (s.options.lab?.squeeze) return true + return playerCanPass(s, add(from, { x: d.x, y: 0 })) || playerCanPass(s, add(from, { x: 0, y: d.y })) +} + /** How far Teleport reaches: TELEPORT_RANGE, one more with Run (user request, 2026-10-24). */ export function teleportRange(s: GameState): number { return TELEPORT_RANGE + (s.powers.run ? 1 : 0) @@ -228,7 +239,7 @@ export const ACTIONS: Record<ActionId, ActionDef> = { label: 'Diagonal', needsTarget: true, ability: 'diagonal', - // A squeeze: the two orthogonal tiles in between may both be blocked. + // Not between two blocked tiles (no squeezing, since 2026-10-25). validTargets: (s) => [ { x: -1, y: -1 }, @@ -236,6 +247,7 @@ export const ACTIONS: Record<ActionId, ActionDef> = { { x: -1, y: 1 }, { x: 1, y: 1 }, ] + .filter((d) => diagonalAllowed(s, s.player, d)) .map((d) => add(s.player, d)) .filter((p) => playerCanEnter(s, p)), apply: (s, target, events) => walkTo(s, target!, events), diff --git a/src/sim/path.ts b/src/sim/path.ts index deafc20..7bd32f3 100644 --- a/src/sim/path.ts +++ b/src/sim/path.ts @@ -1,4 +1,4 @@ -import { isAvailable, isValidTarget } from './actions' +import { diagonalAllowed, isAvailable, isValidTarget } from './actions' import type { Danger } from './danger' import { enemyAt, moveTiles } from './enemies' import { add, eq, exitOpen, flameSafe, idx, playerCanPass } from './state' @@ -15,7 +15,7 @@ const DIAGONALS: readonly Vec[] = [{ x: -1, y: -1 }, { x: 1, y: -1 }, { x: -1, y * the UI refuses a step onto an enemy instead. * An open exit is only walked onto as the destination: passing over it would end the level on * the way somewhere else (user report, 2026-10-23). - * With Diagonal, a diagonal step counts as one too, squeezes included, as the action allows (user + * With Diagonal, a diagonal step counts as one too, where the action allows it (no squeezing; user * report, 2026-10-25: auto-move went round two sides of a corner it could cut). */ export function playerPath(s: GameState, to: Vec): Vec[] | null { @@ -28,6 +28,7 @@ export function playerPath(s: GameState, to: Vec): Vec[] | null { for (let i = 0; i < queue.length; i++) { const p = queue[i]! for (const d of steps) { + if (d.x && d.y && !diagonalAllowed(s, p, d)) continue const q = add(p, d) if (prev.has(key(q)) || !playerCanPass(s, q) || (avoidExit && eq(q, s.exit))) continue prev.set(key(q), p) diff --git a/src/sim/record.ts b/src/sim/record.ts index 9467df0..b261477 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 = 35 +export const SIM_VERSION = 36 /** Everything needed to reproduce a run exactly: mid-run saves, replays, certificates. */ export interface RunRecord { diff --git a/src/sim/types.ts b/src/sim/types.ts index 743b228..31c0441 100644 --- a/src/sim/types.ts +++ b/src/sim/types.ts @@ -72,6 +72,11 @@ export interface RunOptions { * certificate). */ immortal: boolean + /** + * Rule variants for the balance lab to compare (PLAN.md §19.1), never set in play. `squeeze`: the + * old Diagonal, which could step between two blocked tiles (the rule until 2026-10-25). + */ + lab?: { squeeze?: boolean } } export const DEFAULT_OPTIONS: RunOptions = { dev: false, immortal: false } diff --git a/src/ui/info.ts b/src/ui/info.ts index 062f647..4f55681 100644 --- a/src/ui/info.ts +++ b/src/ui/info.ts @@ -47,7 +47,7 @@ export const POWER_UP_TEXT: Record<PowerUpKind, { name: string; text: string }> heart: { name: 'Heart', text: 'One more life.' }, kick: { name: 'Kick', text: 'Walk into a bomb next to you and it slides away until something stops it, keeping its number.' }, throw: { name: 'Throw', text: 'Bomb can also put a bomb 2 tiles away in a straight line, over a crate, bomb or enemy in between (not a pillar).' }, - diagonal: { name: 'Diagonal', text: 'Move can also step diagonally, even squeezing between two blocked tiles. With Teleport, it reaches a square around you: a diagonal step counts as one.' }, + diagonal: { name: 'Diagonal', text: 'Move can also step diagonally, where one of the two tiles beside the step is open (not between two blocked ones). With Teleport, it reaches a square around you: a diagonal step counts as one.' }, rpg: { name: 'RPG', text: 'Bomb can also fire a bomb in a straight line: it flies until a wall, crate, bomb or enemy is next, and lands just before it.' }, proximity: { name: 'Proximity alert', text: 'Your bombs go off as soon as an enemy is right next to one — wherever you are standing.' }, slide: { name: 'Slide', text: 'Move can also slide in a straight line until a wall, crate or bomb stops you, picking up what you pass. An enemy on the way stops you the hard way; flame on the way burns.' }, diff --git a/test/charges.test.ts b/test/charges.test.ts index 2d889d5..017e9b9 100644 --- a/test/charges.test.ts +++ b/test/charges.test.ts @@ -216,10 +216,10 @@ describe('charges are used on purpose', () => { }) describe('Diagonal', () => { - it('is a power-up: squeezes between two blocked tiles once gained, kept through a death', () => { + it('is a power-up: a diagonal step once gained, kept through a death', () => { const s = board(` ##### - #P+.# + #P..# #+..# #####`) expect(isAvailable(s, 'diagonal')).toBe(false) diff --git a/test/new-powers.test.ts b/test/new-powers.test.ts index 2e90c78..0b518e9 100644 --- a/test/new-powers.test.ts +++ b/test/new-powers.test.ts @@ -204,3 +204,20 @@ describe('Diagonal widens Teleport', () => { expect(ACTIONS.teleport.validTargets(s)).toContainEqual(v(7, 1)) }) }) + +describe('Diagonal doesn’t squeeze between two blocked tiles (since 2026-10-25)', () => { + it('needs one of the two tiles beside the step open; the lab’s squeeze variant allows the old way', () => { + const s = board(` + ##### + #P+.# + #+..# + #####`) + s.powers.diagonal = true + expect(ACTIONS.diagonal.validTargets(s)).toEqual([]) + s.options = { ...s.options, lab: { squeeze: true } } + expect(ACTIONS.diagonal.validTargets(s)).toEqual([v(2, 2)]) + s.options = { ...s.options, lab: undefined } + s.cells[1 * s.width + 2] = 'floor' + expect(ACTIONS.diagonal.validTargets(s)).toEqual([v(2, 2)]) + }) +}) diff --git a/test/picks.test.ts b/test/picks.test.ts index 8d549b4..2427104 100644 --- a/test/picks.test.ts +++ b/test/picks.test.ts @@ -63,7 +63,7 @@ describe('Move stands for every way of moving', () => { const s = board(` ##### #P#.# - ##..# + #...# #####`) s.powers.diagonal = true expect(resolvePick(s, 'move', v(2, 2))).toBe('diagonal') diff --git a/tools/balance/lab.ts b/tools/balance/lab.ts index 2327558..6e7b04f 100644 --- a/tools/balance/lab.ts +++ b/tools/balance/lab.ts @@ -19,6 +19,7 @@ import { measureShape, samplePositions, type Shape } from './structure' * same boards. The runs are spread over the CPU's cores. * * npm run balance -- powers [--seeds 200] [--skill careful|sloppy] each power-up held from level 1 + * npm run balance -- diagonal [--seeds 200] [--skill …] Diagonal now, and with the old squeeze * npm run balance -- structure [--seeds 60] what each power-up changes on real positions * npm run balance -- runs [--seeds 20] [--skill …] the baseline, run by run * npm run balance -- trace <seed> <level> [turns] one level, board by board @@ -39,8 +40,24 @@ function powerArms(): Arms { return arms } +/** + * Diagonal as it is (no squeezing, since 2026-10-25) against the old rule that let it step between + * two blocked tiles: without it, with it, and with the squeeze back. + */ +function diagonalArms(): Arms { + return { + baseline: () => {}, + diagonal: (s) => applyPowerUp(s, 'diagonal'), + 'diagonal, with the old squeeze': (s) => { + applyPowerUp(s, 'diagonal') + s.options = { ...s.options, lab: { squeeze: true } } + }, + } +} + const EXPERIMENTS: Record<string, () => Arms> = { powers: powerArms, + diagonal: diagonalArms, // The baseline alone, or with one power-up (`--arm <power-up>`), run by run. runs: () => ({ baseline: powerArms()[process.env.BALANCE_ARM ?? 'baseline'] ?? (() => {}) }), } @@ -83,7 +100,7 @@ async function main(): Promise<void> { if (command === 'structure') return structure(Number(option(args, 'seeds', '60'))) if (command === 'record-until') return recordUntil(args[0] ?? 'chargeOffered') if (!EXPERIMENTS[command]) { - console.log('Commands: powers, structure, runs, trace <seed> <level> [turns]. Options: --seeds N, --skill careful|average|sloppy, --levels N.') + console.log('Commands: powers, diagonal, structure, runs, trace <seed> <level> [turns]. Options: --seeds N, --skill careful|average|sloppy, --levels N.') return } const seeds = Number(option(args, 'seeds', command === 'runs' ? '20' : '200'))