SOURCE / PINNED RELEASE
Made of little things.
Rail Game
- Release
- ccdd5b913413…
- Author-recorded commit
- ddf0122b6acc…
- License
- LICENSE
- Author’s source reference
- nostr://npub1n8ga89w8h6tvwamxusfyzexw8gjy84yxu9rxgnmk955cxtml4ujswzxydd/wss%3A%2F%2Fgit.napplet.soy%2F/n-91ec90c6fdf
Archive hash verified: dad7b678e25f96a3…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
// Full UI campaign with real pointer input and accelerated animation time.
// node tests/browser-campaign.mjs http://127.0.0.1:PORT/
import assert from 'node:assert/strict';
import { mkdir, writeFile, readFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const { chromium } = createRequire(require.resolve('@napplet/conformance-cli/package.json'))('playwright');
const ts = require('typescript');
const source = await readFile(new URL('../src/game.ts', import.meta.url), 'utf8');
const compiled = ts.transpileModule(source, {compilerOptions:{module:ts.ModuleKind.ESNext,target:ts.ScriptTarget.ES2022}}).outputText;
const {RailGame, STAGES, MAX_SQUAD} = await import(`data:text/javascript;base64,${Buffer.from(compiled).toString('base64')}`);
const url = process.argv[2];
if (!url) throw new Error('Pass the running soyli dev URL.');
await mkdir('.napplet-space/qa', {recursive:true});
const browser = await chromium.launch({headless:true});
try {
const page = await browser.newPage({viewport:{width:900,height:600}});
const errors = [], runs = [];
page.on('pageerror', e => errors.push(e.message));
// Retain the native performance clock before Playwright installs virtual time.
await page.addInitScript(() => { window.measureFrameTime = performance.now.bind(performance); });
await page.goto(url);
let frame = await (await page.locator('iframe').elementHandle()).contentFrame();
await frame.locator('#primaryButton').waitFor();
await page.locator('iframe').evaluate(f => Object.assign(f.style,{position:'fixed',inset:'0',width:'100vw',height:'100vh',minHeight:'0',border:'0',zIndex:'1000'}));
await page.evaluate(() => document.querySelector('iframe').contentWindow.postMessage({type:'theme.changed',theme:{colors:{background:'#101d25',text:'#e9f2f2',primary:'#78edbb'}}},'*'));
await page.clock.install();
await page.clock.pauseAt(new Date(Date.now() + 1000));
await frame.evaluate(() => {
const raf = window.requestAnimationFrame.bind(window);
window.heavyFrameTimes = [];
window.requestAnimationFrame = callback => raf(time => {
const start = window.measureFrameTime(); callback(time);
if (Number(document.getElementById('squad').textContent) >= 200 && document.getElementById('app').dataset.phase === 'playing') {
window.heavyFrameTimes.push(window.measureFrameTime() - start);
}
});
});
let reachedFullSquad = false;
for (let stage = 0; stage < STAGES.length; stage++) {
const layout = new RailGame(stage).entities;
let x = 0;
await frame.locator('#primaryButton').click();
const runStart = await frame.evaluate(() => performance.now());
await page.mouse.move(450,400); await page.mouse.down();
for (let tick = 0; tick < 480; tick++) {
const state = await frame.evaluate(() => ({label: document.getElementById('routeLabel').textContent, now: performance.now()}));
const distance = STAGES[stage].length - parseInt(state.label);
const upcoming = layout.filter(e => !['enemy','boss'].includes(e.kind) && e.z > distance + .5 && e.z < distance + 36).sort((a,b) => a.z-b.z);
if (Number.isNaN(distance)) x = Math.sin(((state.now - runStart) / 1000 + .25) * .8) * 1.65;
else if (upcoming.length) {
const row = upcoming.filter(e => Math.abs(e.z-upcoming[0].z) < 1);
const pickup = row.find(e => e.kind === 'multiply') ?? row.find(e => e.kind === 'damage') ?? row.find(e => ['recruit','rapid','shield'].includes(e.kind));
if (pickup) x = pickup.x;
else if (row.some(e => e.kind === 'barrier' && Math.abs(e.x-x)<1.1)) x = [-2,0,2].find(lane => row.every(e => Math.abs(e.x-lane)>1)) ?? 0;
}
await page.mouse.move(450 + x * 112.5,400);
await page.clock.runFor(500);
const squad = Number(await frame.locator('#squad').innerText());
if (squad === MAX_SQUAD && !reachedFullSquad) {
reachedFullSquad = true;
await page.locator('iframe').screenshot({path:'.napplet-space/qa/squad-250.png'});
// Release drag before a live resize; resize deliberately cancels input.
await page.mouse.up();
await page.setViewportSize({width:390,height:844});
await page.clock.runFor(100);
await page.locator('iframe').screenshot({path:'.napplet-space/qa/squad-250-portrait.png'});
await page.setViewportSize({width:900,height:600});
await page.clock.runFor(100);
// Establish a new relative drag at the current world position.
await page.mouse.move(450,400); await page.mouse.down();
await page.mouse.move(900,400); await page.clock.runFor(400);
await page.mouse.up(); await page.mouse.move(450 + 2.25 * 112.5,400); await page.mouse.down();
}
const phase = await frame.locator('#app').getAttribute('data-phase');
if (phase === 'won' || phase === 'lost') {
assert.equal(phase,'won',`Stage ${stage+1} ended with ${squad} soldiers`);
const result = {stage:stage+1, score:await frame.locator('#score').innerText(), squad};
runs.push(result); console.log('PASS full stage',JSON.stringify(result)); break;
}
}
await page.mouse.up(); await page.clock.runFor(150);
assert.equal(await frame.locator('#app').getAttribute('data-phase'),'won');
}
assert.equal(reachedFullSquad,true);
assert.deepEqual(errors,[]);
const frameTimes = await frame.evaluate(() => window.heavyFrameTimes.sort((a,b)=>a-b));
const timing = {samples:frameTimes.length,medianMs:frameTimes[Math.floor(frameTimes.length*.5)],p95Ms:frameTimes[Math.floor(frameTimes.length*.95)]};
console.log('Heavy-scene JS frame time',JSON.stringify(timing));
await page.clock.runFor(500); await page.reload();
frame = await (await page.locator('iframe').elementHandle()).contentFrame();
await frame.locator('#primaryButton').waitFor(); await page.clock.runFor(500);
assert.equal(await frame.locator('#stagePicker button').nth(STAGES.length - 1).isEnabled(),true);
console.log('PASS campaign unlock persisted through reload');
await writeFile('.napplet-space/qa/campaign-results.json',JSON.stringify({runs,reachedFullSquad,timing,errors},null,2));
} finally { await browser.close(); }
