Back to Rail Game
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.

tests/browser-campaign.mjs
// 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(); }