import { makeWorld, sim, tick, aimAt, check, section, report, CFG, Radicals, Input, ROOMS, } from './harness.mjs'; // ============================================================ // THE GO/NO-GO: does one radical eat a field? // ============================================================ section('THE RADICAL — outbreak dynamics'); { const w = makeWorld(); w.load(ROOMS[1]); w.particles.length = 0; // a dense-ish field, deterministic layout (no Math.random in the test) let i = 0; for (let gx = 0; gx < 16; gx++) for (let gy = 0; gy < 9; gy++) w.spawn({ x: 240 + gx * 42, y: 110 + gy * 46, el: ['H', 'C', 'O', 'N'][i++ % 4] }); w.molly.x = 80; w.molly.y = 660; Radicals.make(w.particles[0]); tick(w); const t0 = w.radicals.count; const trace = []; for (let s = 0; s < 6; s++) { sim(w, 1); trace.push(`t=${s + 1}s n=${w.radicals.count} hops=${w.radicals.chain}`); } console.log(' ' + trace.join(' | ')); check('starts with exactly one radical', t0 === 1, `n=${t0}`); check('the wound MOVES (chain hops)', w.radicals.chain > 5, `hops=${w.radicals.chain}`); check('branch factor crosses 1 in a dense field', w.radicals.peak > 1, `peak=${w.radicals.peak}`); check('respects the population cap', w.radicals.peak <= CFG.RAD_CAP, `peak=${w.radicals.peak}`); } // ============================================================ // TERMINATION — the wrestle is the kill move // ============================================================ section('TERMINATION — two radicals annihilate'); { const w = makeWorld(); w.load(ROOMS[1]); w.particles.length = 0; const a = w.spawn({ x: 620, y: 322, el: 'C' }); const b = w.spawn({ x: 620, y: 398, el: 'C' }); Radicals.make(a); Radicals.make(b); w.molly.x = 620; w.molly.y = 520; // stand clear; the squeeze point is lethal aimAt(w, 620, 360); Input.grab = true; let killT = null; for (let i = 0; i < 600; i++) { tick(w); if (!a.radical && !b.radical) { killT = i / 120; break; } } Input.grab = false; check('two radicals can be terminated', killT !== null, killT ? `in ${killT.toFixed(2)}s` : 'NEVER'); check('termination is FAST enough to be a combat move', killT !== null && killT < 0.9, `${killT ? killT.toFixed(2) : '-'}s (target ~0.4, must be < 0.9)`); check('they annihilate rather than bond', !a.isBondedTo(b)); } // ============================================================ // the barrier multiplier must actually be doing the work // ============================================================ section('BARRIER — radical pairs are barrierless'); { const { barrierHump } = await import('../src/physics.js'); const A = { r: 11, radical: false }, B = { r: 11, radical: false }; const lock = (A.r + B.r) * CFG.LOCK_MUL; const d = lock + CFG.BARRIER_BAND / 2; // peak of the hump const normal = barrierHump(d, A, B); const rad = barrierHump(d, { r: 11, radical: true }, { r: 11, radical: true }); check('normal pair hits the full barrier', normal > 0.9, `hump=${normal.toFixed(2)}`); check('radical pair is ~85% easier', rad < normal * 0.2, `hump=${rad.toFixed(2)}`); } // ============================================================ // MOLLY'S LIFE // ============================================================ section('MOLLY — she can die'); { const w = makeWorld(); w.load(ROOMS[1]); w.particles.length = 0; const r = w.spawn({ x: 400, y: 360, el: 'C' }); r.fixed = true; Radicals.make(r); w.molly.x = 400; w.molly.y = 360; check('starts with 3 electrons', w.molly.electrons === 3, `e=${w.molly.electrons}`); tick(w); check('radical contact costs an electron', w.molly.electrons === 2, `e=${w.molly.electrons}`); check('i-frames engage', w.molly.iframe > 0, `iframe=${w.molly.iframe.toFixed(2)}`); // i-frames should prevent instant death for (let i = 0; i < 30; i++) tick(w); check('i-frames prevent melt-through', w.molly.electrons === 2, `e=${w.molly.electrons}`); // now run it out — hold her in contact (knockback would otherwise save her) let deathT = null; for (let i = 0; i < 1200 && deathT === null; i++) { w.molly.x = 400; w.molly.y = 360; w.molly.vx = w.molly.vy = 0; tick(w); if (w.molly.dead) deathT = i / 120; } check('she dies when electrons run out', w.molly.dead, deathT ? `at ${deathT.toFixed(2)}s` : 'survived'); check('death takes long enough to read', deathT === null || deathT > 1.5, `${deathT ? deathT.toFixed(2) : '-'}s`); } // ============================================================ // REGRESSION — everything that worked before still works // ============================================================ section('REGRESSION'); { let ok = 0; for (let i = 0; i < ROOMS.length; i++) { const w = makeWorld(); try { w.load(ROOMS[i]); sim(w, 2); ok++; } catch (e) { console.log(` room ${i} threw: ${e.message}`); } } check('all rooms load + simulate', ok === ROOMS.length, `${ok}/${ROOMS.length}`); // the wrestle const w = makeWorld(); w.load(ROOMS[2]); const a = w.particles[0], b = w.particles[1]; a.x = 620; a.y = 322; a.vx = a.vy = 0; b.x = 620; b.y = 398; b.vx = b.vy = 0; w.molly.x = 620; w.molly.y = 360; aimAt(w, 620, 360); Input.grab = true; let t = null; for (let i = 0; i < 600; i++) { tick(w); if (a.isBondedTo(b)) { t = i / 120; break; } } Input.grab = false; check('bond wrestle still snaps', t !== null, t ? `${t.toFixed(2)}s` : 'FAILED'); check('wrestle stayed in its feel window', t !== null && t > 1.0 && t < 2.6, `${t ? t.toFixed(2) : '-'}s`); check('room 2 exit opens', w.exitOpen); // the heist const h = makeWorld(); h.load(ROOMS[5]); const k = h.find('key'); k.setCharge(0); k.grace = 0; k.zoneTimer = 0; k.x = 660; k.y = 360; k.vx = 260; let locked = null; for (let i = 0; i < 900 && locked === null; i++) { tick(h); if (h.exitOpen) locked = i / 120; } check('heist still solvable', locked !== null, locked ? `${locked.toFixed(2)}s` : 'FAILED'); // laserhands const g = makeWorld(); g.load(ROOMS[1]); g.particles.length = 0; const target = g.spawn({ x: 900, y: 360, el: 'H' }); target.fixed = true; g.molly.x = 200; g.molly.y = 360; g.molly.cool = 0; aimAt(g, 900, 360); g.bolts.fire(g, 1); let hit = null; for (let i = 0; i < 300 && hit === null; i++) { tick(g); if (target.charge === 1) hit = i / 120; } check('bolt travels and lands on hydrogen', hit !== null, hit ? `${hit.toFixed(2)}s over 700px` : 'MISSED'); check('bolt is not hitscan', hit !== null && hit > 0.3, `${hit ? hit.toFixed(2) : '-'}s`); } process.exit(report() ? 0 : 1);