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.
// Capture real gameplay in the soyLI iframe; no game-state injection.
// node tests/capture-preview.mjs http://127.0.0.1:PORT/
import assert from 'node:assert/strict';
import { mkdir, 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 compiled = ts.transpileModule(await readFile(new URL('../src/game.ts', import.meta.url),'utf8'), {compilerOptions:{module:ts.ModuleKind.ESNext,target:ts.ScriptTarget.ES2022}}).outputText;
const {RailGame,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/promotion',{recursive:true});
const browser = await chromium.launch({headless:true});
try {
const page = await browser.newPage({viewport:{width:900,height:600}});
await page.goto(url);
const 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.locator('#primaryButton').click();
await page.mouse.move(450,400); await page.mouse.down();
const layout = new RailGame().entities;
let x = 0, reached = false;
for (let tick=0; tick<120; tick++) {
const distance = 900-parseInt(await frame.locator('#routeLabel').innerText());
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 (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);
if(Number(await frame.locator('#squad').innerText())===MAX_SQUAD){reached=true;break;}
}
assert.equal(reached,true); await page.mouse.up();
for (const [width,height,name] of [[1200,750,'landscape'],[390,844,'portrait']]) {
await page.setViewportSize({width,height});
// Canvas resize clears its pixels. Virtual time must run a new animation frame.
await page.clock.runFor(100);
const painted = await frame.evaluate(() => {
const canvas=document.getElementById('game');
const pixels=canvas.getContext('2d').getImageData(0,0,canvas.width,canvas.height).data;
let opaque=0; for(let i=3;i<pixels.length;i+=400) if(pixels[i]===255) opaque++;
return {width:innerWidth,height:innerHeight,opaque};
});
assert.equal(painted.width,width); assert.equal(painted.height,height); assert.ok(painted.opaque>100);
await page.locator('iframe').screenshot({path:`.napplet-space/promotion/release-${name}.png`});
console.log('Captured',name,JSON.stringify(painted));
}
} finally { await browser.close(); }
