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.
// Board scenario — real submit/read round-trips against soyLI's disposable
// local CVM (depth-v1 ruling 2026-10-28, ratified test tier 2).
// Run: soyli multiplayer tests/board-scenario.mjs --players 2
// Trusted local test code (docs/napplet-backend.md §Repeatable multiplayer
// scenarios). Players are sequential here on purpose: deterministic ranks.
// The sandboxed game exposes the same window.minesweeperDx QA hook the bare
// browser suite uses; clearings are driven through the engine, bankings and
// navigation through real frame clicks.
/** Reveal every safe tile on the current board (browser.test.mjs pattern). */
async function clearBoard(frame) {
await frame.evaluate(() => {
const app = window.minesweeperDx;
const eng = app.board.game;
for (const t of eng.world.tiles) {
if (!t.hasMine && t.state === 'HIDDEN') eng.reveal(t.coord[0], t.coord[1]);
}
app.board.refresh();
app.sync(eng);
});
}
/** Wait for the summary's board note to settle off 'Posting score…'. */
async function settleBoardNote(frame) {
const note = frame.locator('#summaryBoardNote', {
hasText: /Deepest|posted|not posted/i,
});
await note.waitFor({ state: 'visible', timeout: 15000 });
// One more beat: the rank line arrives via a follow-up read after submit.
const text = await note.textContent();
if (/posting score/i.test(text ?? '')) {
await note.evaluate((el) =>
new Promise((resolve) => {
if (!/posting score/i.test(el.textContent ?? '')) return resolve();
new MutationObserver((_, obs) => {
if (!/posting score/i.test(el.textContent ?? '')) {
obs.disconnect();
resolve();
}
}).observe(el, { childList: true, characterData: true, subtree: true });
}),
);
}
return (await note.textContent()) ?? '';
}
export default async ({ players, check }) => {
const [p1, p2] = players.map((p) => p.frame);
// ---- Player 1: one cleared board, banked (score 1), named by the
// ONE-TIME first-run gate (first-run name gate ruling 2026-10-29): the
// FIRST Enter The Depths click must be interrupted by #nameGate; naming
// there guarantees the first accepted submit carries the handle.
await p1.locator('#startRunBtn').click();
check(
'p1 first start is gated by the one-time runner-name modal',
await p1.locator('#nameGate').isVisible() &&
(await p1.locator('#nameGateInput').getAttribute('maxlength')) === '24',
);
await p1.locator('#nameGateInput').fill('scen-p1');
await p1.locator('#nameGateOk').click();
await clearBoard(p1);
await p1.locator('#bankBtn').click();
const armed = await p1.locator('#summaryBoardNote').textContent();
check(
'p1 summary arms the post and invites a runner name (no instant submit)',
/runner name/i.test(armed ?? '') && !/posted|posting score/i.test(armed ?? ''),
);
await p1.locator('#playerNameInput').fill('scen-p1');
await p1.locator('#playerNameInput').press('Enter'); // flushes the armed post
const note1 = await settleBoardNote(p1);
check(
'p1 run-end note confirms a posted personal best',
/#1 · Deepest Depth 1|Deepest depth 1 posted/.test(note1),
);
// ---- Player 2: one cleared board too (score 1, first submit accepted). -
// Named by their own first-run gate (identical gate in a separate identity).
await p2.locator('#startRunBtn').click();
await p2.locator('#nameGateInput').fill('scen-p2');
await p2.locator('#nameGateInput').press('Enter'); // Enter also confirms
await clearBoard(p2);
await p2.locator('#bankBtn').click();
await p2.locator('#playerNameInput').fill('scen-p2');
await p2.locator('#playerNameInput').press('Enter');
const note2 = await settleBoardNote(p2);
check(
'p2 run-end note confirms a posted personal best',
/Deepest [Dd]epth 1|posted/.test(note2),
);
// ---- Player 2 renames mid-summary, then beats their best. ---------------
// A same-score resubmit can NOT rename a row (the service only promotes
// strictly-better personal bests), so the rename is re-tested with score 2:
// clearing two boards posts a strictly-better submit carrying the new name.
await p2.locator('#playerNameInput').fill('scen-p2-renamed');
await p2.locator('#playerNameInput').press('Tab'); // fires the change persist
await p2.locator('#summaryMenuBtn').click();
await p2.locator('#startRunBtn').click();
check(
'the name gate never interrupts a second time (name_prompted latched)',
await p2.locator('#nameGate').isHidden(),
);
await clearBoard(p2);
await p2.locator('#continueBtn').click();
await clearBoard(p2);
await p2.locator('#bankBtn').click();
// The input pre-fills with the persisted name; Enter flushes the armed post
// with it (the score here is strictly better, so it renames the row).
await p2.locator('#playerNameInput').press('Enter');
const note3 = await settleBoardNote(p2);
check(
'p2 better score ranks first with the new personal best',
/#1 · Deepest Depth 2|Deepest depth 2 posted/.test(note3),
);
// ---- Player 1 opens the shared leaderboard: both rows, own highlighted. -
await p1.locator('#summaryBoardBtn').click();
await p1.locator('#boardRows .ledger-row').first().waitFor({ timeout: 10000 });
const board = await p1.evaluate(() => ({
rows: [...document.querySelectorAll('#boardRows .ledger-row')].map((r) => ({
text: r.textContent,
own: r.classList.contains('own'),
})),
note: document.querySelector('#boardNote').textContent,
}));
check('shared board shows both runners', board.rows.length === 2);
check(
'top row is p2 with the rename carried by the better score and score 2',
board.rows[0]?.text.includes('scen-p2-renamed') &&
board.rows[0]?.text.includes('2'),
);
check(
'p1 own row is highlighted and carries the name typed at first post',
board.rows.some((r) => r.own && r.text.includes('scen-p1')) &&
/Top 2 · client-reported/.test(board.note),
);
// Regression pin for the 2026-10-29 bug: a row named via the summary field
// must NEVER fall back to the fallback handle.
check(
'no board row shows the unnamed-runner fallback',
board.rows.every((r) => !r.text.includes('unnamed-runner')),
);
};
