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.
// Minesweeper DX — Phase 3 progression + persistence (headless, no browser).
// Covers save-data hardening (§5.2: clamp/coerce/drop, never crash), the wire
// format (spec key names), the Scrap economy numbers from the Phase-3 ruling,
// and the ProgressionManager ledger/purchase rules.
import assert from 'node:assert/strict';
import test from 'node:test';
import {
BASE_SCRAP_PER_CLEAR,
ProgressionManager,
SAVE_KEY,
SAVE_VERSION,
SaveManager,
initialSave,
salvageRunScrap,
sanitizeSave,
scrapForClear,
} from '../src/save.ts';
import { MAX_UPGRADES, ZERO_UPGRADES } from '../src/upgrades.ts';
// ------------------------------ sanitizeSave ------------------------------
test('sanitizeSave: garbage in, clean default out (never throws)', () => {
for (const junk of [null, undefined, 42, 'save', [], {}, { scrap: 'x', depth: -3 }]) {
const s = sanitizeSave(junk);
assert.equal(s.version, SAVE_VERSION);
assert.ok(Number.isInteger(s.scrap) && s.scrap >= 0);
assert.ok(Number.isInteger(s.depth) && s.depth >= 0);
for (const lvl of Object.values(s.upgrades))
assert.ok(Number.isInteger(lvl) && lvl >= 0 && lvl <= 5, 'levels never exceed the 5-level max (shield_lvl)');
}
});
test('sanitizeSave: legacy anchor_radius_lvl is ignored (no anchor upgrade since 2.x)', () => {
const s = sanitizeSave({
version: 1, scrap: 300, depth: 7,
upgrades: {
anchor_radius_lvl: 3, mine_frequency_pct: 12, mine_value_pct: 160,
radar_lvl: 2, seeker_lvl: 1, shield_lvl: 1, economy_multiplier_lvl: 2,
},
});
assert.deepEqual(s.upgrades, { radar: 2, seeker: 1, shield: 1, stabilizer: 0, economy: 2, flow_cap: 0 },
'legacy save: flow_cap key absent defaults to LOCKED (base cap 100, ruling 2026-10-27)');
assert.equal(s.depth, 7);
});
test('sanitizeSave: out-of-range levels clamp, partial upgrades merge with defaults', () => {
const s = sanitizeSave({ scrap: 1e9, depth: 1e9, upgrades: { radar_lvl: 99, seeker_lvl: -5, shield_lvl: 99 } });
assert.equal(s.upgrades.radar, 3, 'clamped to max');
assert.equal(s.upgrades.seeker, 0, 'negative clamped to 0');
assert.equal(s.upgrades.shield, 5, 'shield clamps to its 5-level max (ruling 2026-10-27)');
assert.equal(s.upgrades.stabilizer, 0, 'missing stabilizer key defaults (locked)');
assert.equal(s.upgrades.economy, 0);
assert.equal(s.upgrades.flow_cap, 0, 'missing flow_cap key defaults (base cap 100, ruling 2026-10-27)');
assert.equal(s.scrap, 1e9, 'scrap kept when sane integer');
});
// ------------------------------ SaveManager -------------------------------
/** In-memory stand-in for the napplet storage KV port. */
function fakePort() {
const mem = new Map();
return {
mem,
async getItem(k) { return mem.has(k) ? mem.get(k) : null; },
async setItem(k, v) { mem.set(k, v); },
};
}
test('SaveManager round-trips through the SPEC wire format (storage.setItem/getItem)', async () => {
const port = fakePort();
const mgr = new SaveManager(port);
const save = initialSave();
save.scrap = 250;
save.depth = 4;
save.name = 'deep-diver';
save.name_prompted = true;
save.upgrades = { radar: 2, seeker: 1, shield: 1, stabilizer: 1, economy: 2, flow_cap: 2 };
assert.equal(await mgr.save(save), true);
// Wire shape: version/scrap/depth/name/name_prompted/upgrades, upgrades holds
// *_lvl keys only (name added with the depth-v1 leaderboard ruling
// 2026-10-28; name_prompted with the first-run name gate ruling 2026-10-29).
const raw = JSON.parse(port.mem.get(SAVE_KEY));
assert.deepEqual(Object.keys(raw).sort(), ['depth', 'name', 'name_prompted', 'scrap', 'upgrades', 'version']);
assert.equal(raw.name_prompted, true, 'name_prompted persisted (first-run gate 2026-10-29)');
assert.deepEqual(Object.keys(raw.upgrades).sort(),
['economy_multiplier_lvl', 'flow_cap_lvl', 'radar_lvl', 'seeker_lvl', 'shield_lvl', 'stabilizer_lvl']);
assert.equal(raw.upgrades.radar_lvl, 2);
assert.equal(raw.upgrades.stabilizer_lvl, 1, 'stabilizer_lvl persisted (ruling 2026-10-27)');
assert.equal(raw.upgrades.economy_multiplier_lvl, 2);
assert.equal(raw.upgrades.flow_cap_lvl, 2, 'flow_cap_lvl persisted (ruling 2026-10-27, flow_cap_ruling)');
assert.equal(raw.upgrades.anchor_radius_lvl, undefined, 'legacy keys never written');
assert.deepEqual(await mgr.load(), save);
});
test('Legacy saves without a name field default to "" (ruling D4)', async () => {
const port = fakePort();
// A pre-board save: no name key at all.
port.mem.set(SAVE_KEY, JSON.stringify({
version: 1,
scrap: 80,
depth: 3,
upgrades: { radar_lvl: 1, seeker_lvl: 0, shield_lvl: 0, stabilizer_lvl: 0, economy_lvl: 0, flow_cap_lvl: 0 },
}));
const mgr = new SaveManager(port);
const loaded = await mgr.load();
assert.equal(loaded.name, '', 'legacy save gets an empty name');
assert.equal(loaded.name_prompted, false,
'legacy save defaults to NOT-yet-gated — the first-run gate (2026-10-29) asks legacy players once');
// Over-long / whitespace names clamp on load.
port.mem.set(SAVE_KEY, JSON.stringify({
version: 1, scrap: 0, depth: 0, name: ' x'.repeat(30) + ' ', upgrades: {},
}));
const clamped = await mgr.load();
assert.ok(clamped.name.length <= 24, 'name clamped to 24 chars');
assert.ok(!/^\s|\s$/.test(clamped.name), 'name trimmed');
});
test('SaveManager: corrupt JSON degrades to defaults and records an error', async () => {
const port = fakePort();
port.mem.set(SAVE_KEY, '{not json');
const mgr = new SaveManager(port);
assert.deepEqual(await mgr.load(), initialSave());
assert.notEqual(mgr.error, null, 'degradation is surfaced via .error');
});
test('SaveManager: no storage domain (test host) → disabled no-op, app still runs', async () => {
const mgr = new SaveManager(null);
assert.equal(mgr.enabled, false);
assert.deepEqual(await mgr.load(), initialSave());
const save = initialSave();
save.scrap = 99;
assert.equal(await mgr.save(save), false, 'writes silently drop');
});
// ------------------------------ economy math ------------------------------
test('scrapForClear: 10 × 1.1^depth × (1 + 0.25 × economy level), rounded', () => {
assert.equal(scrapForClear(0, ZERO_UPGRADES), BASE_SCRAP_PER_CLEAR);
assert.equal(scrapForClear(5, ZERO_UPGRADES), Math.round(10 * 1.1 ** 5), 'depth 5 ≈ 16');
assert.equal(scrapForClear(0, { ...ZERO_UPGRADES, economy: 1 }), 13, 'economy Lv1: 12.5 → 13');
assert.equal(scrapForClear(2, { ...ZERO_UPGRADES, economy: 2 }), 18, '12.1 × 1.5 = 18.15 → 18');
});
test('salvageRunScrap: death costs half of unbanked Scrap (floor salvaged, rest destroyed)', () => {
assert.deepEqual(salvageRunScrap(0), { salvaged: 0, destroyed: 0 });
assert.deepEqual(salvageRunScrap(10), { salvaged: 5, destroyed: 5 });
assert.deepEqual(salvageRunScrap(11), { salvaged: 5, destroyed: 6 }, 'odd amount destroys the extra');
});
// --------------------------- ProgressionManager ---------------------------
function freshManager(scrap = 0, upgrades = { radar: 0, seeker: 0, shield: 0, stabilizer: 0, economy: 0, flow_cap: 0 }) {
const save = initialSave();
save.scrap = scrap;
save.upgrades = { ...upgrades };
return new ProgressionManager(save);
}
test('ProgressionManager: clear credits go to the run ledger; bank moves them to the vault', () => {
const p = freshManager();
assert.equal(p.creditClear(0), 10);
assert.equal(p.creditClear(1), 11, 'depth scaling bites immediately');
assert.equal(p.runScrap, 21);
assert.equal(p.scrap, 0, 'vault untouched until banked');
assert.equal(p.bank(), 21);
assert.equal(p.scrap, 21);
assert.equal(p.runScrap, 0, 'ledger emptied');
assert.equal(p.bank(), 0, 'banking nothing banks nothing');
p.startRun();
assert.equal(p.runScrap, 0);
assert.equal(p.scrap, 21, 'banked is safe across runs');
});
test('ProgressionManager: death salvages half the ledger into the vault, then zeroes it', () => {
const p = freshManager();
p.creditClear(0); // +10 to the ledger
const { earned, salvaged, destroyed } = p.endRunOnDeath();
assert.deepEqual({ earned, salvaged, destroyed }, { earned: 10, salvaged: 5, destroyed: 5 });
assert.equal(p.scrap, 5);
assert.equal(p.runScrap, 0, 'nothing survives the death screen');
});
test('ProgressionManager: clearing records the deepest sector (save.depth = depth + 1)', () => {
const p = freshManager();
p.creditClear(0);
assert.equal(p.save.depth, 1);
p.creditClear(4);
assert.equal(p.save.depth, 5);
});
test('ProgressionManager: purchases gate on affordability and level cap', () => {
const p = freshManager(100);
const tooExpensive = p.purchase('radar');
assert.equal(tooExpensive.ok, false);
assert.equal(tooExpensive.reason, 'affordable', '150 > 100');
assert.equal(p.scrap, 100, 'failed purchase never charges');
const bought = p.purchase('economy');
assert.equal(bought.ok, true);
assert.equal(bought.level, 1);
assert.equal(bought.cost, 50, 'economy Lv1 = 50 (ruling 2026-10-27 curve 50/150/300/500)');
assert.equal(p.scrap, 50);
assert.equal(p.levels.economy, 1);
assert.equal(p.canAfford('economy'), false, 'next economy level (150) > 50');
p.save.scrap = 10_000;
for (let lvl = 2; lvl <= 4; lvl++) assert.ok(p.purchase('economy').ok, `Lv${lvl}`);
assert.equal(p.levels.economy, 4);
const maxed = p.purchase('economy');
assert.equal(maxed.ok, false);
assert.equal(maxed.reason, 'maxed');
});
test('ProgressionManager: everythingMaxed only when all upgrades are maxed', () => {
const p = freshManager();
assert.equal(p.everythingMaxed, false);
p.save.upgrades = { ...MAX_UPGRADES, economy: MAX_UPGRADES.economy - 1 };
assert.equal(p.everythingMaxed, false);
p.save.upgrades = { ...MAX_UPGRADES };
assert.equal(p.everythingMaxed, true);
});
