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/c2-check.mjs
// C2 regression: fx canvas must run board-sized in the wind-only steady
// state, flip to viewport while a burst is live, and flip back after —
// with wind pixels still landing in the right place either way.
import { chromium } from 'playwright';
import { spawn } from 'node:child_process';
import net from 'node:net';

const PORT = 4190;
const srv = spawn('npx', ['vite', 'preview', '--host', '127.0.0.1', '--port', String(PORT)], {
  cwd: new URL('..', import.meta.url).pathname, stdio: 'ignore',
});
for (let i = 0; i < 40; i++) {
  const up = await new Promise((res) => {
    const s = net.connect(PORT, '127.0.0.1');
    s.once('connect', () => { s.destroy(); res(true); });
    s.once('error', () => res(false));
  });
  if (up) break;
  await new Promise((r) => setTimeout(r, 500));
}

const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1600, height: 1000 } });
const errs = [];
page.on('pageerror', (e) => errs.push(String(e)));
await page.goto(`http://127.0.0.1:${PORT}/`);
await page.click('#startRunBtn');
await page.waitForSelector('.board');

// arm wind (depth-6-equivalent telegraph; QA hook, never game logic)
await page.evaluate(() => window.minesweeperDx.fx.setTelegraph({ dx: 1, dy: 0, urgency: 1, frozen: false }));
await page.waitForFunction(() => window.minesweeperDx.fx.fullCanvas === false, null, { timeout: 4000 });
await page.waitForTimeout(300); // a few steady frames at the new geometry

const steady = await page.evaluate(() => {
  const fx = window.minesweeperDx.fx;
  const cv = document.querySelector('#fx');
  const cb = cv.getBoundingClientRect();
  const r = document.querySelector('.board').getBoundingClientRect();
  const dpr = window.devicePixelRatio || 1;
  // wind pixels must land INSIDE the canvas region (mapping sanity):
  const ctx = cv.getContext('2d');
  const img = ctx.getImageData(0, 0, cv.width, cv.height).data;
  let sig = 0;
  for (let i = 3; i < img.length; i += 4004) sig += img[i];
  return {
    full: fx.fullCanvas,
    canvasPx: cv.width, viewportPx: Math.floor(window.innerWidth * dpr),
    boxedCoversBoard: cb.left <= r.left && cb.top <= r.top && cb.right >= r.right && cb.bottom >= r.bottom,
    boxedSmaller: cv.width < window.innerWidth * dpr && cv.height < window.innerHeight * dpr,
    windPixels: sig > 0,
  };
});
console.log('steady wind:', JSON.stringify(steady));

// boom -> full mode
await page.evaluate(() => window.minesweeperDx.fx.event({ kind: 'boom', cell: '5,5', cause: 'qa', shielded: false }));
await page.waitForTimeout(150);
const burst = await page.evaluate(() => {
  const fx = window.minesweeperDx.fx;
  const cv = document.querySelector('#fx');
  const dpr = window.devicePixelRatio || 1;
  return { full: fx.fullCanvas, fullViewport: cv.width === Math.floor(window.innerWidth * dpr) };
});
console.log('burst:', JSON.stringify(burst));

// after the burst tail drains -> back to board box
await page.waitForTimeout(2500);
const after = await page.evaluate(() => {
  const fx = window.minesweeperDx.fx;
  const cv = document.querySelector('#fx');
  const dpr = window.devicePixelRatio || 1;
  return {
    full: fx.fullCanvas,
    boxed: cv.width < window.innerWidth * dpr && cv.height < window.innerHeight * dpr,
  };
});
console.log('after tail:', JSON.stringify(after));

const ok =
  steady.full === false && steady.boxedCoversBoard && steady.boxedSmaller && steady.windPixels &&
  burst.full === true && burst.fullViewport &&
  after.full === false && after.boxed && errs.length === 0;
console.log(ok ? 'C2 PASS' : 'C2 FAIL', 'pageerrors:', errs.length);

await browser.close();
srv.kill();
process.exit(ok ? 0 : 1);