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/tickshift-repro.mjs
// Probe 4b: deterministic tickShift countdown-leak repro via real actions.
import { createServer } from 'node:http';
import { readFile } from 'node:fs/promises';
import { join, extname } from 'node:path';
import { chromium } from 'playwright';

const dist = '/mnt/Mirror/Projects/Hermes/minesweeper-dx/dist';
const MIME = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css' };
async function serveDist() {
  const server = createServer(async (req, res) => {
    const p = req.url === '/' ? '/index.html' : req.url.split('?')[0];
    try {
      const body = await readFile(join(dist, p.slice(1)));
      res.writeHead(200, { 'content-type': MIME[extname(p)] ?? 'application/octet-stream' });
      res.end(body);
    } catch { res.writeHead(403).end('denied'); }
  });
  await new Promise((ok) => server.listen(0, '127.0.0.1', ok));
  return { url: `http://127.0.0.1:${server.address().port}/`, close: () => server.close() };
}
const svc = await serveDist();
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 900, height: 600 } });
const errors = [];
page.on('pageerror', (e) => errors.push(String(e).slice(0, 160)));
await page.goto(svc.url, { waitUntil: 'load' });
await page.waitForFunction(() => window.minesweeperDx !== undefined);
await page.evaluate(() => {
  const fx = window.minesweeperDx.fx;
  window.__p = { evs: [] };
  const oe = fx.event.bind(fx);
  fx.event = (ev) => { window.__p.evs.push(ev.kind); return oe(ev); };
});

await page.click('#startRunBtn');
for (let d = 0; d < 6; d++) {
  await page.evaluate(() => {
    const g = window.minesweeperDx; const eng = g.board.game;
    // Bulk-reveal loops can span 28+ turns; a shift firing mid-loop moves a
    // shifter onto a still-HIDDEN tile the loop then "safely" reveals =>
    // detonation flake. Freeze the countdown (the setup below re-arms it).
    eng.nextShiftIn = 1e9;
    for (const t of eng.world.tiles) if (!t.hasMine && t.state === 'HIDDEN') eng.reveal(t.coord[0], t.coord[1]);
    g.board.refresh(); g.sync(eng);
  });
  await page.click('#continueBtn');
}
await page.evaluate(() => { const m = document.querySelector('#shifterIntro'); if (m && !m.hidden) document.querySelector('#shifterIntroOk').click(); });
await page.waitForTimeout(300);


// open the fresh depth-6 board: reveal all safe ground (creates the frontier)
await page.evaluate(() => {
  const g = window.minesweeperDx; const eng = g.board.game;
  eng.nextShiftIn = 1e9; // same countdown freeze as the depth ladder
  for (const t of eng.world.tiles)
    if (!t.hasMine && t.state === 'HIDDEN' && t.coord[1] <= 6) eng.reveal(t.coord[0], t.coord[1]);
  g.board.refresh(); g.sync(eng);
});
await page.waitForTimeout(200);
// Arm: 2 shields; find a HIDDEN mine whose +x neighbor is REVEALED safe ground;
// telegraph right; countdown=1.
const setup = await page.evaluate(() => {
  const g = window.minesweeperDx; const eng = g.board.game; const W = eng.world.width;
  eng.shieldCharges = 2;
  let found = null;
  for (const t of eng.world.tiles) {
    if (!t.hasMine || t.state !== 'HIDDEN') continue;
    const [x, y] = t.coord;
    const nb = eng.world.tileAt((x + 1) % W, y);
    if (nb.state === 'REVEALED' && !nb.hasMine) { found = [x, y]; break; }
  }
  if (!found) return { found: null, mines: eng.world.tiles.filter((t) => t.hasMine).length };
  eng.telegraph = [1, 0];
  eng.nextShiftIn = 1;
  g.board.refresh(); g.sync(eng);
  return { found, shields: eng.shieldCharges, nextShiftIn: eng.nextShiftIn };
});
console.log('setup:', JSON.stringify(setup));
if (!setup.found) { console.log('no adjacent-to-revealed mine; abort'); process.exit(0); }

// find a hidden mine cell far from the (3,3)-path to flag/unflag = safe player action
const actionCell = await page.evaluate(() => {
  const g = window.minesweeperDx; const eng = g.board.game;
  const t = eng.world.tiles.find((q) => q.hasMine && q.state === 'HIDDEN');
  return `${t.coord[0]},${t.coord[1]}`;
});
const [ax, ay] = actionCell.split(',').map(Number);

const state = () => page.evaluate(() => {
  const eng = window.minesweeperDx.board.game;
  return {
    nextShiftIn: eng.nextShiftIn,
    telegraph: eng.telegraph,
    shields: eng.shieldCharges,
    absorbed: eng.absorbed ? eng.absorbed.cause : null,
    chip: document.getElementById('shiftIn')?.textContent,
    summaryUp: (() => { const s = document.getElementById('summaryScreen'); return s ? !s.hidden && getComputedStyle(s).display !== 'none' : false; })(),
    depth: eng.depth,
    pulses: document.querySelectorAll('.cell-count-pulse').length,
    evs: window.__p.evs.slice(-8),
    wind: window.minesweeperDx.fx.telegraphWind,
  };
});

console.log('BEFORE action:', JSON.stringify(await state()));
// the collision turn: right-click flags a mine tile (never detonates), turn ticks,
// tickShift moves the armed mine onto REVEALED ground => MINE_ON_REVEALED.
await page.locator(`.cell[data-x="${ax}"][data-y="${ay}"]`).click({ button: 'right' });
await page.waitForTimeout(150);
console.log('AFTER collision-absorb:', JSON.stringify(await state()));

// unflag (toggle) = next turn: countdown was NOT reset => shift fires AGAIN same turn
for (let i = 0; i < 6; i++) {
  await page.locator(`.cell[data-x="${ax}"][data-y="${ay}"]`).click({ button: 'right' });
  await page.waitForTimeout(120);
  console.log(`turn+${i + 1}:`, JSON.stringify(await state()));
}
console.log('errors:', errors.length ? errors : 'none');
await browser.close();
svc.close();