Back to Shiftfield
SOURCE / PINNED RELEASE

Made of little things.

Shiftfield

Release
5a170c505f6b…
Author-recorded commit
a2e23e0171aa…
License
LICENSE
Author’s source reference
nostr://npub16nzc4ql748r2kv9202rgzrt0fmtk4n3tzsjl86yf6ty9agqe9u0qjcc7hc/wss%3A%2F%2Fgit.napplet.soy%2F/n-e4fb6341ccd

Archive hash verified: 66de7623efd91da1…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

tests/engine.test.mjs
// Minesweeper DX — Phase 1 collision suite.
// Headless, pure logic, no DOM. Node loads the .ts engine module directly
// (strip-types). Every assertion maps 1:1 to a "law" in master spec §2.

import assert from 'node:assert/strict';
import test from 'node:test';
import { MinesweeperEngine, GRID_SIZE, MINE_COUNT, mulberry32 } from '../src/engine.ts';
import { GameEngine, shiftIntervalFor } from '../src/game.ts';
import { ZERO_UPGRADES } from '../src/upgrades.ts';

// Deterministic authoring helper: clear the board, then stamp given cells.
function placeMines(e, cells) {
  for (const t of e.tiles) t.hasMine = false;
  for (const [x, y] of cells) e.tileAt(x, y).hasMine = true;
}

// 1. MINE_ON_REVEALED — revealing a tile with a mine detonates.
test('mine on a REVEALED tile detonates', () => {
  const e = new MinesweeperEngine(1, 0);
  placeMines(e, [[3, 4]]);
  const r = e.reveal(3, 4);
  assert.equal(r.result, 'detonated');
  assert.equal(e.explosion, 'MINE_ON_REVEALED');
  assert.ok(e.isExploded());
});

// 2. MINE_ON_MINE — placing a mine onto an occupied tile detonates.
test('placing a mine on an existing mine detonates', () => {
  const e = new MinesweeperEngine(1, 0);
  placeMines(e, [[0, 0]]);
  assert.equal(e.placeMineAt(0, 0), 'MINE_ON_MINE');
  assert.ok(e.isExploded());
});

// 3. Toroidal wrap — x=9 shifting east wraps around to x=0.
test('a mine at x=9 shifting east wraps around to x=0', () => {
  const e = new MinesweeperEngine(1, 0);
  placeMines(e, [[9, 5]]);
  e.shift(1, 0);
  assert.equal(e.tileAt(0, 5).hasMine, true);
  assert.equal(e.tileAt(9, 5).hasMine, false);
});

// 4. Toroidal wrap — y=0 shifting north (dy=-1) wraps to y=9.
test('a mine at y=0 shifting north wraps around to y=9', () => {
  const e = new MinesweeperEngine(1, 0);
  placeMines(e, [[4, 0]]);
  e.shift(0, -1);
  assert.equal(e.tileAt(4, 9).hasMine, true);
});

// 5. Anchor — a mine whose TARGET is FLAGGED is canceled (impassable into).
test('a mine cannot shift into a flagged (locked) tile', () => {
  const e = new MinesweeperEngine(1, 0);
  placeMines(e, [[0, 0]]);
  e.flag(1, 0);
  const r = e.shift(1, 0);
  assert.equal(r.blocked, 1);
  assert.equal(r.moved, 0);
  assert.equal(e.tileAt(0, 0).hasMine, true);
  assert.equal(e.explosion, null);
});

// 6. Anchor — a mine SITTING ON a flagged tile cannot move out of it.
test('a mine on a flagged tile is locked and cannot shift away', () => {
  const e = new MinesweeperEngine(1, 0);
  placeMines(e, [[3, 3]]);
  e.flag(3, 3);
  const r = e.shift(1, 0);
  assert.equal(r.blocked, 1);
  assert.equal(r.moved, 0);
  assert.equal(e.tileAt(3, 3).hasMine, true);
  assert.equal(e.explosion, null);
});

// 7. Impassability both ways — two mines adjacent to a flagged tile: the
//    flagged mine can't move OUT, and the neighbor can't move IN. Both
//    blocked, no explosion. (The old sequential engine let the neighbor
//    "pass through"; the anchor rule forbids this.)
test('no mine may move out of or into a locked tile', () => {
  const e = new MinesweeperEngine(1, 0);
  placeMines(e, [[3, 0], [2, 0]]);
  e.flag(3, 0);
  const r = e.shift(1, 0);
  assert.equal(r.blocked, 2);
  assert.equal(r.moved, 0);
  assert.equal(e.explosion, null);
  assert.equal(e.tileAt(3, 0).hasMine, true);
  assert.equal(e.tileAt(2, 0).hasMine, true);
});

// 7a'. Shift collision: an UNFLAGGED mine blocked because its TARGET is
//      locked stays in place; a mine behind it would land ON it →
//      MINE_ON_MINE, shift aborts, and ShiftResult.collisionCell reports the
//      cell the shield absorb must flag (ruling 2026-10-27).
test('a shift collision aborts the shift and reports the collision cell', () => {
  const e = new MinesweeperEngine(1, 0);
  placeMines(e, [[3, 0], [2, 0]]); // (2,0) behind (3,0), moving east
  e.flag(4, 0);                    // (3,0)'s target is locked → (3,0) stays
  const r = e.shift(1, 0);
  assert.equal(e.explosion, 'MINE_ON_MINE');
  assert.equal(r.collisionCell, 0 * 10 + 3);
  assert.equal(e.tileAt(2, 0).hasMine, true, 'shift aborted: nobody moved');
  assert.equal(e.tileAt(3, 0).hasMine, true);
});

// 7b. MINE_ON_MINE cannot arise from a uniform shift: a toroidal translation
//    is a bijection, so two distinct mines always end on distinct tiles.
//    Sifting a full shift and checking the invariant confirms it.
test('a uniform shift can never produce mine-on-mine', () => {
  const dirs = [[1,0],[-1,0],[0,1],[0,-1],[1,1],[1,-1],[-1,1],[-1,-1]];
  for (const seed of [1, 2, 3, 7, 100, 999]) {
    const e = new MinesweeperEngine(seed);
    for (const [dx, dy] of dirs) e.shift(dx, dy);
    const counts = new Map();
    for (const t of e.tiles) {
      if (t.hasMine) counts.set(t.coord[0] * 10 + t.coord[1], (counts.get(t.coord[0] * 10 + t.coord[1]) ?? 0) + 1);
      const n = t.coord[0] * 10 + t.coord[1];
      if ((counts.get(n) ?? 0) > 1) throw new Error('mine-on-mine leaked');
    }
  }
});

// 8. Uniform simultaneity — two adjacent mines both move: no cascade, no
//    collision. (Under a sequential shift the second would land on the first's
//    empty square target; uniform motion keeps them one cell apart.)
test('adjacent mines shift uniformly, not in cascade', () => {
  const e = new MinesweeperEngine(1, 0);
  placeMines(e, [[3, 0], [4, 0]]);
  const r = e.shift(1, 0);
  assert.equal(r.moved, 2);
  assert.equal(r.blocked, 0);
  assert.equal(e.explosion, null);
  assert.equal(e.tileAt(4, 0).hasMine, true);
  assert.equal(e.tileAt(5, 0).hasMine, true);
  assert.equal(e.tileAt(3, 0).hasMine, false);
});

// 9. Flag never detonates.
test('flagging a mine never detonates', () => {
  const e = new MinesweeperEngine(1, 0);
  placeMines(e, [[5, 5]]);
  e.flag(5, 5);
  assert.equal(e.explosion, null);
  assert.equal(e.tileAt(5, 5).hasMine, true);
});

// 10. Reveal on a safe hidden tile succeeds.
test('revealing a hidden tile with no mine is safe', () => {
  const e = new MinesweeperEngine(1, 0);
  placeMines(e, [[9, 9]]);
  assert.equal(e.reveal(5, 5).result, 'safe'); // numbered tile: opens alone
});

// 11. Reveal on a locked tile is inert.
test('revealing a flagged tile is a no-op and never detonates', () => {
  const e = new MinesweeperEngine(1, 0);
  placeMines(e, [[5, 5]]);
  e.flag(5, 5);
  const r = e.reveal(5, 5);
  assert.equal(r.result, 'safe');
  assert.equal(r.cleared, 0);
  assert.equal(e.explosion, null);
});

// 12. A mine revealed, then shifted onto another revealed cell, detonates.
test('a shift that puts a mine onto a REVEALED tile detonates', () => {
  const e = new MinesweeperEngine(1, 0);
  placeMines(e, [[1, 5]]);
  e.reveal(2, 5);
  e.shift(1, 0);
  assert.equal(e.explosion, 'MINE_ON_REVEALED');
});

// 13. Default board: exactly 15 mines, no duplicates, all within bounds.
test('default board has exactly 15 unique mines', () => {
  const e = new MinesweeperEngine(1);
  assert.equal(e.mineCount(), MINE_COUNT);
  assert.equal(MINE_COUNT, 15);
  const seen = new Set();
  for (const t of e.tiles) {
    if (t.hasMine) {
      const key = t.coord[0] * 100 + t.coord[1];
      assert.ok(!seen.has(key), `duplicate mine at ${t.coord}`);
      seen.add(key);
      assert.ok(t.coord[0] >= 0 && t.coord[0] < GRID_SIZE);
      assert.ok(t.coord[1] >= 0 && t.coord[1] < GRID_SIZE);
    }
  }
});

// 14. isBoardCleared — true only when every non-mine tile is REVEALED.
test('isBoardCleared requires every non-mine tile to be revealed', () => {
  const e = new MinesweeperEngine(1, 1);
  placeMines(e, [[0, 0]]);
  assert.equal(e.isBoardCleared(), false);
  for (const t of e.tiles) {
    if (!t.hasMine && t.state === 'HIDDEN') t.state = 'REVEALED';
  }
  assert.equal(e.isBoardCleared(), true);
});

// 15. Counts: reveals and flags are tracked.
test('revealed and flagged counters track tile states', () => {
  const e = new MinesweeperEngine(1, 0);
  placeMines(e, [[2, 2]]);
  e.reveal(0, 0, { flood: false }); // counter test: one tile, no cascade
  e.flag(1, 1);
  assert.equal(e.revealedCount(), 1);
  assert.equal(e.flaggedCount(), 1);
});

// 16. RNG distribution — REGRESSION GUARD for the broken-mulberry32 bug.
// A mulberry32 missing its Math.imul avalanche steps outputs near-perfectly
// correlated consecutive draws. placeMines() draws x then y back-to-back, so
// the broken generator pinned EVERY mine onto two toroidal diagonals
// (y - x mod 10 ∈ {5, 6}) — minesweeper with a guaranteed mine highway.
// Check: across many random seeds, mines spread over ALL 10 diagonals.
test('mines spread uniformly across toroidal diagonals (RNG avalanche)', () => {
  const RUNS = 300;
  const diag = new Array(10).fill(0);
  for (let run = 0; run < RUNS; run++) {
    // Spread the seeds hard: mixed high/low, odd/even — not a tidy 0..N sweep.
    const seed = (run * 0x9e3779b9 + 0xdeadbeef) >>> 0;
    const e = new MinesweeperEngine(seed);
    for (const t of e.tiles) {
      if (t.hasMine) diag[((t.coord[1] - t.coord[0]) % 10 + 10) % 10]++;
    }
  }
  const total = RUNS * MINE_COUNT;
  const expected = total / 10;
  // Under the broken generator, 8 of 10 diagonals are EXACTLY zero.
  for (let d = 0; d < 10; d++) {
    assert.ok(
      diag[d] > expected * 0.5,
      `diagonal ${d} starved (${diag[d]} vs expected ~${expected}) — RNG lost its avalanche?`,
    );
    assert.ok(diag[d] < expected * 1.5, `diagonal ${d} overloaded (${diag[d]} vs ~${expected})`);
  }
});

// 17. Canonical mulberry32 vectors — pins the reference implementation so a
//     future "cleanup" can't silently swap in a weaker scramble again.
test('mulberry32 matches canonical reference outputs', () => {
  const r = mulberry32(42);
  assert.equal(r(), 0.6011037519201636);
  assert.equal(r(), 0.44829055899754167);
  assert.equal(r(), 0.8524657934904099);
});

// --- flood reveal (classic zero-region open) -----------------------------

// 18. A numbered tile (adjacent mine count > 0) opens ALONE: no cascade.
test('a numbered reveal opens only the clicked tile', () => {
  const e = new MinesweeperEngine(1, 0);
  placeMines(e, [[5, 5]]);
  const r = e.reveal(5, 4); // adjacent to the mine → number
  assert.equal(e.adjacentMineCount(5, 4), 1);
  assert.equal(r.cleared, 1);
  assert.equal(e.revealedCount(), 1);
});

// 19. A zero-adjacent reveal cascades: zero region opens, numbered border
//     opens but stops the cascade; mine tiles stay hidden.
test('zero-region reveal floods the open area with a numbered border', () => {
  const e = new MinesweeperEngine(1, 0);
  placeMines(e, [[0, 0]]);
  const r = e.reveal(5, 5);
  assert.equal(e.adjacentMineCount(5, 5), 0);
  // 10×10 torus with one mine at (0,0): 91 zero-adjacent tiles open plus the
  // 8-tile numbered border ring; the mine itself is a wall. 99 of 100.
  assert.equal(r.cleared, 99);
  assert.equal(e.tileAt(0, 0).state, 'HIDDEN'); // wall — flood never detonates
  assert.equal(e.tileAt(1, 1).state, 'REVEALED'); // numbered border opens...
  assert.equal(e.adjacentMineCount(1, 1), 1); // the lone mine contributes 1
  assert.equal(e.explosion, null);
  for (const t of e.tiles) if (t.state === 'REVEALED') assert.equal(t.hasMine, false);
});

// 20. Flood stops at the numbered border: a region enclosed by numbers does
//     not leak past it. Corner mine → flood from the far side stops at the
//     mine's neighbor ring.
test('flood opens the numbered border but does not conduct through it', () => {
  const e = new MinesweeperEngine(1, 0);
  // Ring of mines around (5,5) leaves (5,5) a numbered tile surrounded by
  // walls; flood from (5,5) must open nothing beyond it.
  const ring = [];
  for (let dx = -1; dx <= 1; dx++)
    for (let dy = -1; dy <= 1; dy++)
      if (!(dx === 0 && dy === 0)) ring.push([5 + dx, 5 + dy]);
  placeMines(e, ring);
  const r = e.reveal(5, 5);
  assert.equal(e.adjacentMineCount(5, 5), 8);
  assert.equal(r.cleared, 1);
});

// 21. FLAGGED tiles are flood walls: they neither open nor conduct.
test('flood cannot open or pass through a flagged tile', () => {
  const e = new MinesweeperEngine(1, 0);
  placeMines(e, [[9, 9]]);
  e.flag(0, 5); // sits in the open region (torus: (9,9) neighbors are 0,8..8,0)
  const before = e.revealedCount();
  const r = e.reveal(5, 5);
  assert.ok(r.cleared > 1);
  assert.equal(e.tileAt(0, 5).state, 'FLAGGED');
  assert.equal(e.explosion, null);
  assert.equal(e.revealedCount(), before + r.cleared);
});

// 22. flood:false opts out (ability square reveals use it).
test('reveal with flood disabled opens exactly one tile', () => {
  const e = new MinesweeperEngine(1, 0);
  placeMines(e, [[9, 9]]);
  const r = e.reveal(5, 5, { flood: false });
  assert.equal(r.cleared, 1);
  assert.equal(e.revealedCount(), 1);
});

// ---------------------------------------------------------------------------
// T1 regression (audit telegraph_depth6_bug_audit_2026-10-28): an exploding or
// collision-aborted shift must STILL re-arm the countdown and reroll the
// telegraph. Before the fix, tickShift() early-returned on MINE_ON_MINE /
// MINE_ON_FIRE BEFORE the reset, leaving nextShiftIn stuck at 0 → negative:
// a full shift fired on EVERY later turn (pulse storm, telegraph locked
// forever, shields drained one per turn).
// ---------------------------------------------------------------------------

// Depth-6 game (first shift depth) on an empty hand-stamped world, 1 shield.
function shieldedDepth6(seed) {
  return new GameEngine(
    6, undefined, seed,
    new MinesweeperEngine(seed, 0),
    { ...ZERO_UPGRADES, shield: 1 },
  );
}

test('a shield-absorbed shift collision re-arms the countdown (T1)', () => {
  const g = shieldedDepth6(11);
  g.flag(0, 9); // setup action: burns board turn 1
  placeMines(g.world, [[3, 0], [2, 0]]); // (2,0) behind (3,0), shifting east
  g.world.flag(4, 0);                    // (3,0)'s target locked → collision
  g.telegraph = [1, 0];
  g.nextShiftIn = 1;
  g.flag(0, 9); // remove flag → commit → tickShift fires the colliding shift
  assert.equal(g.world.explosion, null, 'the charge absorbed the collision');
  assert.equal(g.shieldCharges, 0);
  assert.equal(g.world.tileAt(2, 0).hasMine, true, 'shift aborted: nobody moved');
  assert.equal(g.world.tileAt(3, 0).state, 'FLAGGED', 'absorb anchored the chain');
  assert.equal(g.nextShiftIn, shiftIntervalFor(6), 'countdown re-armed, not stuck ≤ 0');
  // The cascade is dead: 20 more turns must not re-fire a shift or explode.
  for (let i = 0; i < 20; i++) g.flag(0, 9);
  assert.equal(g.world.explosion, null, 'no per-turn shift storm after the absorb');
  assert.ok(g.nextShiftIn > 0, 'countdown still positive');
});

test('a shield-absorbed shift-onto-fire re-arms the countdown (T1)', () => {
  const g = shieldedDepth6(12);
  g.flag(0, 9); // board turn 1 (fire empty → spread no-ops)
  placeMines(g.world, [[3, 0]]);
  g.fire.add(0 * 10 + 4); // fire exactly where the mine will land
  g.telegraph = [1, 0];
  g.nextShiftIn = 1;
  g.flag(0, 9); // board turn 2 (even → no fire-spread noise this commit)
  assert.equal(g.world.explosion, null, 'the charge absorbed MINE_ON_FIRE');
  assert.equal(g.shieldCharges, 0);
  assert.equal(g.world.tileAt(4, 0).hasMine, true, 'the shift itself landed');
  assert.ok(!g.fire.has(4), 'the absorb extinguished the contact fire');
  assert.equal(g.nextShiftIn, shiftIntervalFor(6), 'countdown re-armed after absorbed fire contact');
});