This commit is contained in:
33333-33333 2026-07-30 15:13:53 +09:00
commit 0a343ecbc5
326 changed files with 15584 additions and 0 deletions

View file

@ -0,0 +1,11 @@
'use strict';
const {vm,assert,functionSource,loadBendPuzzle,loadAppLogic}=require('./helpers/app-source');
const BendPuzzle=loadBendPuzzle(),AppLogic=loadAppLogic();
const context={AppLogic,hash32:BendPuzzle.hash32,rngFrom:BendPuzzle.rngFrom,shuffle:BendPuzzle.shuffle,deepClone:value=>JSON.parse(JSON.stringify(value)),ckey:(r,c)=>`${r},${c}`,sameCell:(a,b)=>a[0]===b[0]&&a[1]===b[1],manhattan:(a,b)=>Math.abs(a[0]-b[0])+Math.abs(a[1]-b[1]),SIDE_D:{N:[-1,0],S:[1,0],W:[0,-1],E:[0,1]}};
vm.createContext(context);
vm.runInContext(['specialCellSet','warpMap','warpPairForCell','isWarpTransition','gateObj','outsidePoint','analyzeTurns','turnAnalysis','obstacleDetours','obstacleCellLimit','addObstaclePattern'].map(name=>functionSource(name)).join('\n')+'\nthis.logic={obstacleCellLimit,addObstaclePattern};',context);
let obstacleCells=0,allCells=0,boards=0;for(let seed=1;seed<=240;seed++){let puzzle;try{puzzle=BendPuzzle.generatePuzzle([[0,0]],0x330000+seed,3,seed%7,-seed%5)}catch(_){continue}const changed=context.logic.addObstaclePattern(puzzle,0x550000+seed);obstacleCells+=(changed.obstacles||[]).length;allCells+=puzzle.valid.length;boards++}
const obstacleRate=obstacleCells/allCells;assert(boards>200,'Too few obstacle samples');assert(obstacleRate>=.008&&obstacleRate<=.05,`Obstacle rate ${(obstacleRate*100).toFixed(2)}% is implausible`);
let largeObstacleCells=0,largeAllCells=0,largeBoards=0;for(let seed=1;seed<=60;seed++){let puzzle;try{puzzle=BendPuzzle.generatePuzzle([[0,0],[1,0],[0,1],[1,1]],0x660000+seed,5,20,-20)}catch(_){continue}const changed=context.logic.addObstaclePattern(puzzle,0x770000+seed),totalCells=changed.valid.length+(changed.obstacles||[]).length;assert((changed.obstacles||[]).length<=Math.floor(totalCells*.2),'Obstacle generation exceeded 20% of the puzzle');largeObstacleCells+=(changed.obstacles||[]).length;largeAllCells+=totalCells;largeBoards++}
const largeObstacleRate=largeObstacleCells/largeAllCells;assert(largeBoards>=45,'Too few large obstacle samples');assert(largeObstacleRate>obstacleRate+.005,`Large-puzzle obstacle rate ${(largeObstacleRate*100).toFixed(2)}% is not higher than small puzzles`);
console.log(`Obstacle test passed: ${(obstacleRate*100).toFixed(2)}% small and ${(largeObstacleRate*100).toFixed(2)}% large obstacle cells`);

View file

@ -0,0 +1,110 @@
'use strict';
const fs=require('fs');
const os=require('os');
const path=require('path');
const http=require('http');
const crypto=require('crypto');
const {spawn,spawnSync}=require('child_process');
const {assert,root}=require('./helpers/app-source');
const browserPath=process.env.BEND_FIELD_BROWSER_PATH||process.env.BEND_FIELD_EDGE_PATH||(
process.platform==='win32'?'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe':'/usr/bin/chromium'
);
const requested=(process.env.BEND_FIELD_SCALE_SIZES||'10000,100000,200000').split(',').map(Number).filter(value=>Number.isSafeInteger(value)&&value>0&&value<=200000);
if(!requested.length)throw new Error('BEND_FIELD_SCALE_SIZES did not contain a supported board count.');
const worldDbName='bend-field:v30:v47-field-reset-20260728-interaction-fix:world',explicitBenchmarkUrl=process.env.BEND_FIELD_BENCHMARK_URL||'',benchmarkHost=process.env.BEND_FIELD_BENCHMARK_HOST||'localhost',useExtension=process.env.BEND_FIELD_BENCHMARK_EXTENSION==='1';
const debuggingPort=22000+Math.floor(Math.random()*1000),temporaryRoot=fs.mkdtempSync(path.join(os.tmpdir(),'bend-field-v2-scale-')),profilePath=path.join(temporaryRoot,'browser-profile');
const sleep=milliseconds=>new Promise(resolve=>setTimeout(resolve,milliseconds));
function stopBrowserTree(child){
if(!child?.pid)return;
if(process.platform==='win32'){
const taskkill=path.join(process.env.SystemRoot||'C:\\Windows','System32','taskkill.exe'),
result=spawnSync(taskkill,['/pid',String(child.pid),'/T','/F'],{stdio:'ignore',windowsHide:true});
if(result.status!==0&&!child.killed)try{child.kill()}catch(_){}
}else if(!child.killed)child.kill('SIGTERM');
}
function extensionIdFromPublicKey(der){
const digest=crypto.createHash('sha256').update(der).digest().subarray(0,16);
return[...digest].map(byte=>String.fromCharCode(97+(byte>>4),97+(byte&15))).join('');
}
function prepareBenchmarkExtension(){
const directory=path.join(temporaryRoot,'extension');fs.mkdirSync(directory,{recursive:true});
for(const name of['index.html','style.css','favicon.svg','favicon.ico','puzzle-core.js','app-logic.js','field-persistence.js','field-persistence-worker.js','app.js','puzzle-worker.js'])fs.copyFileSync(path.join(root,name),path.join(directory,name));
const{publicKey}=crypto.generateKeyPairSync('rsa',{modulusLength:2048}),der=publicKey.export({type:'spki',format:'der'}),key=der.toString('base64'),id=extensionIdFromPublicKey(der);
fs.writeFileSync(path.join(directory,'manifest.json'),JSON.stringify({manifest_version:3,name:'Bend Field V2 Storage Benchmark',version:'47.36.0',key,permissions:['unlimitedStorage']},null,2));
return{directory,id,url:`chrome-extension://${id}/index.html`};
}
class CdpClient{
constructor(url){this.url=url;this.sequence=0;this.pending=new Map();this.socket=null}
async connect(){
this.socket=new WebSocket(this.url);
await new Promise((resolve,reject)=>{const timer=setTimeout(()=>reject(new Error('CDP connection timed out')),15000);this.socket.addEventListener('open',()=>{clearTimeout(timer);resolve()},{once:true});this.socket.addEventListener('error',event=>{clearTimeout(timer);reject(event.error||new Error('CDP connection failed'))},{once:true})});
this.socket.addEventListener('message',event=>{const message=JSON.parse(event.data);if(!message.id)return;const pending=this.pending.get(message.id);if(!pending)return;this.pending.delete(message.id);if(message.error)pending.reject(new Error(`${pending.method}: ${message.error.message}`));else pending.resolve(message.result)});
}
send(method,params={}){const id=++this.sequence;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject,method});this.socket.send(JSON.stringify({id,method,params}))})}
async evaluate(expression){const response=await this.send('Runtime.evaluate',{expression,awaitPromise:true,returnByValue:true,userGesture:true});if(response.exceptionDetails)throw new Error(response.exceptionDetails.exception?.description||response.exceptionDetails.text||'Browser evaluation failed');return response.result.value}
close(){try{this.socket?.close()}catch(_){}}
}
async function waitFor(check,{timeout=30000,interval=50,label='condition'}={}){const deadline=Date.now()+timeout;let lastError=null;while(Date.now()<deadline){try{const value=await check();if(value)return value}catch(error){lastError=error}await sleep(interval)}throw lastError||new Error(`Timed out waiting for ${label}`)}
async function endpoint(){return waitFor(async()=>{const response=await fetch(`http://127.0.0.1:${debuggingPort}/json/list`,{signal:AbortSignal.timeout(1500)});if(!response.ok)return null;return(await response.json()).find(target=>target.type==='page'&&target.webSocketDebuggerUrl)},{timeout:20000,label:'browser DevTools endpoint'})}
async function startServer(){
const types={'.html':'text/html; charset=utf-8','.js':'text/javascript; charset=utf-8','.css':'text/css; charset=utf-8','.svg':'image/svg+xml','.ttf':'font/ttf','.ico':'image/x-icon'};
const server=http.createServer((request,response)=>{const pathname=new URL(request.url,'http://localhost').pathname,relative=pathname==='/'?'index.html':decodeURIComponent(pathname.slice(1)),file=path.resolve(root,relative);if(file!==root&&!file.startsWith(root+path.sep)){response.writeHead(403);response.end();return}fs.readFile(file,(error,body)=>{if(error){response.writeHead(error.code==='ENOENT'?404:500);response.end();return}response.writeHead(200,{'content-type':types[path.extname(file)]||'application/octet-stream','cache-control':'no-store'});response.end(body)})});
await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(0,'127.0.0.1',()=>{server.off('error',reject);resolve()})});return server;
}
const transactionDoneSource=`tx=>new Promise((resolve,reject)=>{tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error);tx.onabort=()=>reject(tx.error)})`;
async function waitUntilReady(client,timeout=60000){return waitFor(()=>client.evaluate("document.body?.dataset?.ready==='true'&&globalThis.__bendStartupSnapshot"),{timeout,label:'game startup'})}
async function waitForV2(client){
return waitFor(()=>client.evaluate(`(async()=>{const db=await new Promise((resolve,reject)=>{const request=indexedDB.open(${JSON.stringify(worldDbName)});request.onsuccess=()=>resolve(request.result);request.onerror=()=>reject(request.error)});const tx=db.transaction('control','readonly'),request=tx.objectStore('control').get('active'),control=await new Promise((resolve,reject)=>{request.onsuccess=()=>resolve(request.result);request.onerror=()=>reject(request.error)});db.close();return control?.activeFormat===2&&control.activeEpoch})()`),{timeout:60000,label:'V2 storage migration'});
}
async function readStarter(client){
return client.evaluate(`(async()=>{const done=${transactionDoneSource},read=request=>new Promise((resolve,reject)=>{request.onsuccess=()=>resolve(request.result);request.onerror=()=>reject(request.error)}),db=await new Promise((resolve,reject)=>{const request=indexedDB.open(${JSON.stringify(worldDbName)});request.onsuccess=()=>resolve(request.result);request.onerror=()=>reject(request.error)});let tx=db.transaction('control','readonly'),control=await read(tx.objectStore('control').get('active'));await done(tx);const epoch=control.activeEpoch;tx=db.transaction(['worlds','boardIndex','boardPuzzles','boardStates'],'readonly');const[world,index,puzzle,state]=await Promise.all([read(tx.objectStore('worlds').get(epoch)),read(tx.objectStore('boardIndex').get([epoch,'B0'])),read(tx.objectStore('boardPuzzles').get([epoch,'B0'])),read(tx.objectStore('boardStates').get([epoch,'B0']))]);await done(tx);db.close();return{control,world,index,puzzle,state}})()`);
}
async function seedV2(client,starter,count){
const expression=`(async()=>{
const starter=${JSON.stringify(starter)},count=${count},dbName=${JSON.stringify(worldDbName)},batchSize=4000,
done=${transactionDoneSource},open=()=>new Promise((resolve,reject)=>{const request=indexedDB.open(dbName);request.onsuccess=()=>resolve(request.result);request.onerror=()=>reject(request.error)}),db=await open(),epoch=starter.control.activeEpoch;
let tx=db.transaction(['boardIndex','boardPuzzles','boardStates','worlds'],'readwrite');tx.objectStore('boardIndex').clear();tx.objectStore('boardPuzzles').clear();tx.objectStore('boardStates').clear();tx.objectStore('worlds').clear();await done(tx);
const puzzleValue=starter.puzzle.puzzle,stateValue=starter.state.value,started=performance.now();
for(let offset=0;offset<count;offset+=batchSize){
tx=db.transaction(['boardIndex','boardPuzzles','boardStates'],'readwrite');const indexStore=tx.objectStore('boardIndex'),puzzleStore=tx.objectStore('boardPuzzles'),stateStore=tx.objectStore('boardStates'),end=Math.min(count,offset+batchSize);
for(let number=offset;number<end;number++){
const id='B'+number,x=number%1000,y=Math.floor(number/1000),metaRev=1000000+number*2,stateRev=metaRev+1,author='scale-benchmark';
indexStore.put({epoch,id,number,x,y,chunks:[[0,0]],level:1,targetLevel:1,seed:number+1,axis:starter.index.axis,entrySide:null,metaRev,stateRev,revAuthor:author,solved:false,expanded:false,scoreAwarded:0,hasProgress:false,specialFlags:{crossing:false,warp:false,lock:false},shop:null});
puzzleStore.put({epoch,id,metaRev,revAuthor:author,generatorVersion:starter.puzzle.generatorVersion,puzzle:puzzleValue});
stateStore.put({epoch,id,stateRev,revAuthor:author,value:{...stateValue,paths:[],specialProgress:{crossings:[]},solved:false,expanded:false,scoreAwarded:0,store:null,rev:stateRev,revAuthor:author}});
}
await done(tx);await new Promise(resolve=>setTimeout(resolve,0));
}
const global={...starter.world.global,worldEpoch:epoch,nextId:count,solved:0,score:0,globalRev:(starter.world.global.globalRev||0)+count+1,globalRevAuthor:'scale-benchmark',selectedBoardId:'B0',cameraAnchor:null,updatedAt:Date.now()},world={...starter.world,epoch,status:'active',global,boardCount:count,solvedCount:0,score:0,bounds:{minX:0,minY:0,maxX:Math.min(1000,count),maxY:Math.ceil(count/1000)},approximateBytes:count*2048,source:{kind:'benchmark'},progress:null};
tx=db.transaction(['control','worlds'],'readwrite');tx.objectStore('worlds').put(world);tx.objectStore('control').put({...starter.control,key:'active',activeFormat:2,activeEpoch:epoch,previousEpoch:undefined,activationVerified:true});await done(tx);db.close();localStorage.clear();sessionStorage.clear();return{seedMs:performance.now()-started,count};
})()`;
const result=await client.evaluate(expression);assert(result?.count===count,`Only ${result?.count||0} of ${count} V2 boards were seeded`);return result;
}
async function measure(client,count){
const navigationStarted=Date.now();await client.send('Page.reload',{ignoreCache:true});const startup=await waitUntilReady(client,180000),readyMs=Date.now()-navigationStarted;
const snapshot=await client.evaluate(`(()=>({capture:globalThis.__bendStartupSnapshot,ready:document.body.dataset.ready,loaded:fieldIndexLoadedCount,expected:fieldIndexExpectedCount,complete:fieldIndexComplete,b0:Boolean(data?.metas?.B0?.puzzle),navigation:performance.getEntriesByType('navigation')[0]?.duration||0,memory:performance.memory?.usedJSHeapSize||null}))()`);
assert(snapshot.ready==='true'&&snapshot.b0,`${count} board V2 startup did not hydrate B0`);assert(snapshot.capture.expected===count,`${count} board V2 startup declared ${snapshot.capture.expected} boards`);assert(snapshot.capture.loaded<=512,`${count} board V2 startup eagerly loaded ${snapshot.capture.loaded} board indexes`);assert(snapshot.capture.complete===false,`${count} board V2 startup completed the full index scan before first paint`);
const scanStarted=Date.now();await waitFor(()=>client.evaluate('fieldIndexComplete===true&&fieldIndexLoadedCount===fieldIndexExpectedCount'),{timeout:600000,interval:100,label:`${count} board index scan`});const scanMs=Date.now()-scanStarted;
const final=await client.evaluate(`({loaded:fieldIndexLoadedCount,expected:fieldIndexExpectedCount,metas:Object.keys(data.metas).length,memory:performance.memory?.usedJSHeapSize||null})`);assert(final.loaded===count&&final.metas===count,`${count} board V2 background index scan was incomplete`);
return{boards:count,readyMs,navigationMs:snapshot.navigation,capturedReadyMs:snapshot.capture.at,initialIndexes:snapshot.capture.loaded,scanMs,heapAtReady:snapshot.memory,heapAfterScan:final.memory};
}
async function main(){
if(!fs.existsSync(browserPath))throw new Error(`Browser was not found at ${browserPath}`);
const extension=useExtension?prepareBenchmarkExtension():null;console.log(`Launching ${browserPath}`);const server=await startServer(),port=server.address().port,args=['--headless','--no-first-run','--no-sandbox','--enable-unsafe-swiftshader','--allow-file-access-from-files','--disable-dev-shm-usage','--disable-background-networking',`--remote-debugging-port=${debuggingPort}`,'--remote-allow-origins=*',`--user-data-dir=${profilePath}`,'--window-size=1280,900','about:blank'];if(extension){args.splice(-1,0,`--disable-extensions-except=${extension.directory}`,`--load-extension=${extension.directory}`)}const browser=spawn(browserPath,args,{stdio:'ignore'});let client=null;
try{
console.log('Waiting for DevTools');const target=await endpoint();console.log('Connecting to page');client=new CdpClient(target.webSocketDebuggerUrl);await client.connect();await client.send('Page.enable');await client.send('Runtime.enable');
await client.send('Page.addScriptToEvaluateOnNewDocument',{source:`(()=>{const capture=()=>{if(document.body?.dataset?.ready==='true'&&!globalThis.__bendStartupSnapshot)globalThis.__bendStartupSnapshot={at:performance.now(),loaded:typeof fieldIndexLoadedCount==='number'?fieldIndexLoadedCount:null,expected:typeof fieldIndexExpectedCount==='number'?fieldIndexExpectedCount:null,complete:typeof fieldIndexComplete==='boolean'?fieldIndexComplete:null};};const observe=()=>{if(document.documentElement)new MutationObserver(capture).observe(document.documentElement,{subtree:true,attributes:true,attributeFilter:['data-ready']});capture();};if(document.documentElement)observe();else addEventListener('DOMContentLoaded',observe,{once:true})})();`});
const benchmarkUrl=explicitBenchmarkUrl||(extension?extension.url:`http://${benchmarkHost}:${port}/`);console.log(`Navigating to ${benchmarkUrl}`);await client.send('Page.navigate',{url:benchmarkUrl});try{await waitUntilReady(client,30000)}catch(error){const state=await client.evaluate("({href:location.href,ready:document.body?.dataset?.ready,error:document.body?.dataset?.error,status:document.querySelector('#statusMessage')?.textContent||'',text:document.body?.innerText?.slice(0,800)||'',scripts:[...document.scripts].map(s=>s.src)})").catch(()=>null);throw new Error(`${error.message}: ${JSON.stringify(state)}`)}console.log('Game ready; waiting for V2');await waitForV2(client);console.log('V2 active; reading starter');const starter=await readStarter(client);assert(starter?.index?.id==='B0'&&starter?.puzzle?.puzzle&&starter?.state?.value,'Could not read the V2 starter board');
const results=[];for(const count of requested){console.log(`Seeding ${count.toLocaleString()} boards`);const seeded=await seedV2(client,starter,count),measured=await measure(client,count),result={...measured,seedMs:seeded.seedMs};results.push(result);console.log(`${count.toLocaleString()} boards | seed ${(seeded.seedMs/1000).toFixed(1)} s | ready ${measured.readyMs} ms (${measured.initialIndexes} indexes) | full scan ${(measured.scanMs/1000).toFixed(1)} s`)}
console.log(`FIELD_STORAGE_BENCHMARK_JSON=${JSON.stringify(results)}`);console.log('V2 field storage scale benchmark passed');
}finally{
client?.close();stopBrowserTree(browser);
server.closeAllConnections?.();await new Promise(resolve=>server.close(()=>resolve()));for(let attempt=0;attempt<8;attempt++){try{fs.rmSync(temporaryRoot,{recursive:true,force:true});break}catch(error){if(attempt===7)console.warn(`Benchmark cleanup deferred: ${error.message}`);await sleep(200)}}
}
}
main().then(()=>process.exit(0)).catch(error=>{console.error(error);process.exit(1)});

View file

@ -0,0 +1,635 @@
'use strict';
const fs=require('fs');
const os=require('os');
const path=require('path');
const http=require('http');
const {spawn,spawnSync}=require('child_process');
const {assert,root}=require('./helpers/app-source');
const edgePath=process.env.BEND_FIELD_EDGE_PATH||'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe';
const sleep=milliseconds=>new Promise(resolve=>setTimeout(resolve,milliseconds));
let serverPort=0;
const debuggingPort=20000+Math.floor(Math.random()*1000),benchmarkHost=process.env.BEND_FIELD_BENCHMARK_HOST||'localhost';
const startupOnly=process.env.BEND_FIELD_STARTUP_ONLY==='1';
const temporaryRoot=fs.mkdtempSync(path.join(os.tmpdir(),'bend-field-browser-benchmark-'));
const profilePath=path.join(temporaryRoot,'edge-profile');
const worldDbName='bend-field:v30:v47-field-reset-20260728-interaction-fix:world';
const allProfiles=[
{name:'small',boards:16,mode:'grid'},
{name:'medium',boards:128,mode:'grid'},
{name:'large',boards:512,mode:'grid'},
{name:'complex',boards:16,mode:'complex',chunks:10},
{name:'long-line',boards:256,mode:'line'}
];
const requestedProfile=process.env.BEND_FIELD_BENCHMARK_PROFILE||'',profiles=requestedProfile?allProfiles.filter(profile=>profile.name===requestedProfile):allProfiles;
if(requestedProfile&&!profiles.length)throw new Error(`Unknown benchmark profile: ${requestedProfile}`);
function stopBrowserTree(child){
if(!child?.pid)return;
if(process.platform==='win32'){
const taskkill=path.join(process.env.SystemRoot||'C:\\Windows','System32','taskkill.exe'),
result=spawnSync(taskkill,['/pid',String(child.pid),'/T','/F'],{stdio:'ignore',windowsHide:true});
if(result.status!==0&&!child.killed)try{child.kill()}catch(_){}
}else if(!child.killed)child.kill('SIGTERM');
}
class CdpClient{
constructor(url){this.url=url;this.sequence=0;this.pending=new Map();this.events=[];this.socket=null}
async connect(){
this.socket=new WebSocket(this.url);
await new Promise((resolve,reject)=>{
const timer=setTimeout(()=>reject(new Error('CDP WebSocket connection timed out')),10000);
this.socket.addEventListener('open',()=>{clearTimeout(timer);resolve()},{once:true});
this.socket.addEventListener('error',event=>{clearTimeout(timer);reject(event.error||new Error('CDP WebSocket failed'))},{once:true});
});
this.socket.addEventListener('message',event=>{
const message=JSON.parse(event.data);
if(!message.id){this.events.push(message);if(this.events.length>200)this.events.shift();return}
const pending=this.pending.get(message.id);if(!pending)return;this.pending.delete(message.id);
if(message.error)pending.reject(new Error(`${pending.method}: ${message.error.message}`));
else pending.resolve(message.result);
});
}
send(method,params={}){
const id=++this.sequence;
return new Promise((resolve,reject)=>{
this.pending.set(id,{resolve,reject,method});
this.socket.send(JSON.stringify({id,method,params}));
});
}
async evaluate(expression){
const response=await this.send('Runtime.evaluate',{expression,awaitPromise:true,returnByValue:true,userGesture:true});
if(response.exceptionDetails)throw new Error(response.exceptionDetails.exception?.description||response.exceptionDetails.text||'Browser evaluation failed');
return response.result.value;
}
close(){try{this.socket?.close()}catch(_){}}
}
async function waitFor(check,{timeout=20000,interval=100,label='condition'}={}){
const deadline=Date.now()+timeout;let lastError=null;
while(Date.now()<deadline){
try{const value=await check();if(value)return value}catch(error){lastError=error}
await sleep(interval);
}
throw lastError||new Error(`Timed out waiting for ${label}`);
}
async function endpoint(){
return waitFor(async()=>{
const response=await fetch(`http://127.0.0.1:${debuggingPort}/json/list`,{signal:AbortSignal.timeout(1500)});
if(!response.ok)return null;
const targets=await response.json();
return targets.find(target=>target.type==='page'&&target.webSocketDebuggerUrl);
},{label:'Edge DevTools endpoint'});
}
async function startStaticServer(){
const types={'.html':'text/html; charset=utf-8','.js':'text/javascript; charset=utf-8','.css':'text/css; charset=utf-8','.svg':'image/svg+xml','.ttf':'font/ttf','.ico':'image/x-icon'};
const server=http.createServer((request,response)=>{
const pathname=new URL(request.url,'http://localhost').pathname;
const relative=pathname==='/'?'index.html':decodeURIComponent(pathname.slice(1));
const file=path.resolve(root,relative);
if(file!==root&&!file.startsWith(root+path.sep)){response.writeHead(403);response.end();return}
fs.readFile(file,(error,body)=>{
if(error){response.writeHead(error.code==='ENOENT'?404:500);response.end();return}
response.writeHead(200,{'content-type':types[path.extname(file)]||'application/octet-stream','cache-control':'no-store'});
response.end(body);
});
});
await new Promise((resolve,reject)=>{
server.once('error',reject);server.listen(0,'127.0.0.1',()=>{server.off('error',reject);resolve()});
});
serverPort=server.address().port;
return server;
}
async function ready(client){
try{
await waitFor(()=>client.evaluate("document.body?.dataset?.ready==='true'&&Boolean(globalThis.BEND_PERF)"),
{timeout:30000,label:'game startup'});
}catch(error){
const state=await client.evaluate("({href:location.href,ready:document.body?.dataset?.ready,hasPerf:Boolean(globalThis.BEND_PERF),status:document.querySelector('#statusMessage')?.textContent||'',text:document.body?.innerText?.slice(0,500)||''})").catch(()=>null);
const exception=client.events.findLast?.(event=>event.method==='Runtime.exceptionThrown')?.params?.exceptionDetails;
const consoleError=client.events.findLast?.(event=>event.method==='Runtime.consoleAPICalled'&&event.params?.type==='error')?.params;
const consoleText=consoleError?.args?.map(argument=>argument.description||argument.value||'').join(' ')||'';
throw new Error(`${error.message}: ${JSON.stringify(state)} ${exception?.exception?.description||exception?.text||consoleText}`);
}
await client.evaluate("document.querySelector('#normalPlayBtn')?.click();true");
await sleep(350);
}
async function readStarterRows(client){
const expression=`(async()=>{
const db=await new Promise((resolve,reject)=>{const request=indexedDB.open(${JSON.stringify(worldDbName)});request.onsuccess=()=>resolve(request.result);request.onerror=()=>reject(request.error)});
const read=request=>new Promise((resolve,reject)=>{request.onsuccess=()=>resolve(request.result);request.onerror=()=>reject(request.error)});
let tx=db.transaction('control','readonly'),control=await read(tx.objectStore('control').get('active'));
await new Promise((resolve,reject)=>{tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error);tx.onabort=()=>reject(tx.error)});
const epoch=control?.activeEpoch;tx=db.transaction(['worlds','boardIndex','boardPuzzles','boardStates'],'readonly');
const [world,index,puzzle,state]=await Promise.all([
read(tx.objectStore('worlds').get(epoch)),read(tx.objectStore('boardIndex').get([epoch,'B0'])),
read(tx.objectStore('boardPuzzles').get([epoch,'B0'])),read(tx.objectStore('boardStates').get([epoch,'B0']))
]);
await new Promise((resolve,reject)=>{tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error);tx.onabort=()=>reject(tx.error)});
db.close();return{control,world,index,puzzle,state};
})()`;
return waitFor(async()=>{
const rows=await client.evaluate(expression);
return rows?.control?.activeFormat===2&&rows?.world?.global&&rows?.index&&rows?.puzzle?.puzzle&&rows?.state?.value?rows:null;
},{timeout:20000,label:'starter IndexedDB rows'});
}
async function populateWorld(client,starter,profile){
const expression=`(async()=>{
const starter=${JSON.stringify(starter)},profile=${JSON.stringify(profile)},count=profile.boards;
const makeComplexPuzzle=(source,chunkCount)=>{
const puzzle=structuredClone(source),columns=5,g=[],n=[],valid=[],solution=[];
for(let chunk=0;chunk<chunkCount;chunk++){
const rowOffset=Math.floor(chunk/columns)*5,columnOffset=(chunk%columns)*5,gateOffset=g.length;
for(const gate of source.g)g.push([gate[0]+rowOffset,gate[1]+columnOffset,gate[2]]);
for(const number of source.n||[])n.push([number[0]+rowOffset,number[1]+columnOffset,number[2]]);
for(const cell of source.valid||[])valid.push([cell[0]+rowOffset,cell[1]+columnOffset]);
for(const path of source.solution||[])solution.push({...structuredClone(path),startGate:path.startGate+gateOffset,endGate:path.endGate+gateOffset,cells:path.cells.map(cell=>[cell[0]+rowOffset,cell[1]+columnOffset])});
}
puzzle.g=g;puzzle.n=n;puzzle.valid=valid;puzzle.solution=solution;puzzle.bounds={w:Math.min(columns,chunkCount)*5,h:Math.ceil(chunkCount/columns)*5};puzzle.level=10;puzzle.difficulty=10;puzzle.maxTurns=Math.max(...solution.map(path=>path.cells.length));puzzle.totalTurns=solution.reduce((sum,path)=>sum+path.cells.length,0);return puzzle;
};
const starterPuzzle=starter.puzzle.puzzle,starterState=starter.state.value,
complexPuzzle=profile.mode==='complex'?makeComplexPuzzle(starterPuzzle,profile.chunks||10):null,
complexChunks=profile.mode==='complex'?Array.from({length:profile.chunks||10},(_,index)=>[index%5,Math.floor(index/5)]):null;
const db=await new Promise((resolve,reject)=>{const request=indexedDB.open(${JSON.stringify(worldDbName)});request.onsuccess=()=>resolve(request.result);request.onerror=()=>reject(request.error)});
const tx=db.transaction(['control','worlds','boardIndex','boardPuzzles','boardStates','outboxV2','recoveryV2','tombstonesV2'],'readwrite'),
indexStore=tx.objectStore('boardIndex'),puzzleStore=tx.objectStore('boardPuzzles'),stateStore=tx.objectStore('boardStates');
for(const name of ['worlds','boardIndex','boardPuzzles','boardStates','outboxV2','recoveryV2','tombstonesV2'])tx.objectStore(name).clear();
const columns=Math.ceil(Math.sqrt(count)),spacingX=profile.mode==='complex'?7:2,spacingY=profile.mode==='complex'?4:2;
for(let index=0;index<count;index++){
const id='B'+index,x=profile.mode==='line'?index:(index%columns)*spacingX,y=profile.mode==='line'?0:Math.floor(index/columns)*spacingY,
metaRev=100000+index,stateRev=200000+index,author='benchmark',puzzle=profile.mode==='complex'?structuredClone(complexPuzzle):structuredClone(starterPuzzle),
chunks=profile.mode==='complex'?structuredClone(complexChunks):structuredClone(starter.index.chunks),state=structuredClone(starterState);
state.paths=profile.mode==='line'&&index>0?[{startGate:0,endGate:3,openGate:null,cells:[[0,0],[0,1],[0,2],[0,3],[0,4]],colorIndex:0,startColorIndex:0,endColorIndex:0}]:[];
state.solved=false;state.specialProgress={crossings:[]};state.rev=stateRev;state.revAuthor=author;
indexStore.put({epoch:starter.control.activeEpoch,id,number:index,x,y,chunks,level:profile.mode==='complex'?10:starter.index.level,targetLevel:profile.mode==='complex'?10:starter.index.targetLevel,seed:index+1,axis:starter.index.axis,entrySide:null,sealedSides:[],metaRev,stateRev,metaRevAuthor:author,stateRevAuthor:author,revAuthor:author,solved:false,expanded:false,scoreAwarded:0,hasProgress:state.paths.length>0,specialFlags:{crossing:false,warp:false,lock:false},shop:null});
puzzleStore.put({epoch:starter.control.activeEpoch,id,metaRev,revAuthor:author,generatorVersion:starter.puzzle.generatorVersion,puzzle});
stateStore.put({epoch:starter.control.activeEpoch,id,stateRev,revAuthor:author,value:state});
}
const current=structuredClone(starter.world.global),epoch=starter.control.activeEpoch;
current.worldEpoch=epoch;current.nextId=count;current.solved=0;current.score=0;current.bonusScore=0;current.bonusEvents={};
current.globalRev=300000+count;current.globalRevAuthor='benchmark';current.updatedAt=Date.now();current.clockFloor=Date.now();
const world={...starter.world,epoch,status:'active',global:current,boardCount:count,solvedCount:0,score:0,bounds:{minX:0,minY:0,maxX:profile.mode==='line'?count:columns*spacingX,maxY:profile.mode==='line'?1:Math.ceil(count/columns)*spacingY},source:{kind:'benchmark'},progress:null};
tx.objectStore('worlds').put(world);tx.objectStore('control').put({key:'active',activeFormat:2,activeEpoch:epoch,activationVerified:true});
await new Promise((resolve,reject)=>{tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error);tx.onabort=()=>reject(tx.error)});
db.close();localStorage.clear();sessionStorage.clear();return{count,solution:profile.mode==='complex'?complexPuzzle.solution:starterPuzzle.solution};
})()`;
const result=await client.evaluate(expression);assert(result?.count===profile.boards,'Synthetic benchmark world was not written completely');return result.solution;
}
async function reload(client){
await client.evaluate("document.body.dataset.ready='reloading';true");
await client.send('Page.reload',{ignoreCache:true});
await ready(client);
await client.evaluate("centerMeta(data.metas.B0);ensureBoards();renderAll();true");
await waitFor(()=>client.evaluate("Boolean(document.querySelector('.board-card[data-id=\"B0\"]:not(.board-static)'))"),
{timeout:15000,label:'interactive origin board'});
await sleep(500);
}
async function waitForPersistenceIdle(client){
await client.evaluate("(async()=>{if(hasPendingPersistence())await persistNow({skipCloud:true});return true})()");
await waitFor(()=>client.evaluate("!hasPendingPersistence()&&!saveTimer"),{timeout:20000,label:'benchmark persistence idle'});
await client.evaluate("persistQueue.then(()=>true)");
}
async function prepareInteractionMeasurement(client){
await waitForPersistenceIdle(client);
await client.evaluate("globalThis.gc?.();true");
await sleep(120);
await client.evaluate('BEND_PERF.reset();true');
}
async function restoreOriginState(client,state){
const restored=await client.evaluate(`(async()=>{
const board=rendered.get('B0'),snapshot=${JSON.stringify(state)},nextFrame=()=>new Promise(resolve=>requestAnimationFrame(()=>resolve())),
applySnapshot=()=>{
if(board){cancelBoardDragFrame(board);clearDragRender(board);board.drawing=null;board.armedGate=null;if(board.pendingClaimPointer)clearPendingClaimPointer(board,board.pendingClaimPointer.pointerId)}
realtimeHeldPointers.clear();const current=metaState('B0');replaceObjectContents(current,deepClone(snapshot));normalizedStateObjects.add(current);return current;
};
applySnapshot();refreshInteractionState();await nextFrame();await new Promise(resolve=>setTimeout(resolve,80));
const settledPathCount=metaState('B0').paths.length;applySnapshot();changed('B0');
if(board)renderBoardNow(board);refreshInteractionState();centerMeta(data.metas.B0,{select:false});await nextFrame();await persistNow({skipCloud:true});
return{pathCount:metaState('B0').paths.length,settledPathCount,solved:metaState('B0').solved,drawing:Boolean(board?.drawing),pending:Boolean(board?.pendingClaimPointer)};
})()`);
assert(restored.pathCount===(state.paths||[]).length&&restored.solved===Boolean(state.solved)&&!restored.drawing&&!restored.pending,`Canonical origin restore failed: ${JSON.stringify(restored)}`);
}
async function pointFor(client,selector){
const point=await client.evaluate(`(()=>{const element=document.querySelector(${JSON.stringify(selector)});if(!element)return null;const rect=element.getBoundingClientRect();return{x:rect.left+rect.width/2,y:rect.top+rect.height/2}})()`);
if(!point)throw new Error(`Missing benchmark target: ${selector}`);
return point;
}
async function pointForCell(client,boardId,row,column){
const point=await client.evaluate(`(()=>{const board=rendered.get(${JSON.stringify(boardId)});if(!board)return null;const rect=board.svg.getBoundingClientRect(),vb=board.svg.viewBox.baseVal,x=PAD+(${column}+.5)*CELL,y=PAD+(${row}+.5)*CELL;return{x:rect.left+x*rect.width/vb.width,y:rect.top+y*rect.height/vb.height}})()`);
if(!point)throw new Error(`Missing benchmark board cell: ${boardId} ${row},${column}`);
return point;
}
async function pointsForCells(client,boardId,cells){
const points=await client.evaluate(`(()=>{const board=rendered.get(${JSON.stringify(boardId)}),cells=${JSON.stringify(cells)};if(!board)return null;const rect=board.svg.getBoundingClientRect(),vb=board.svg.viewBox.baseVal;return cells.map(([row,column])=>{const x=PAD+(column+.5)*CELL,y=PAD+(row+.5)*CELL;return{x:rect.left+x*rect.width/vb.width,y:rect.top+y*rect.height/vb.height}})})()`);
if(!points)throw new Error(`Missing benchmark board cells: ${boardId}`);
return points;
}
async function safePanPoint(client,selector){
const point=await client.evaluate(`(()=>{const card=document.querySelector(${JSON.stringify(selector)}),view=document.querySelector('#viewport')?.getBoundingClientRect(),rect=card?.getBoundingClientRect();if(!card||!view||!rect)return null;for(const fy of[.18,.32,.5,.68,.82])for(const fx of[.18,.32,.5,.68,.82]){const x=rect.left+rect.width*fx,y=rect.top+rect.height*fy;if(x<view.left||x>view.right||y<view.top||y>view.bottom)continue;const target=document.elementFromPoint(x,y);if(target?.closest?.(${JSON.stringify(selector)})&&!target.closest?.('button,a,input,select,textarea,[role="button"]'))return{x,y}}return null})()`);
return point||pointFor(client,selector);
}
async function solveOrigin(client,solution,cpuRate=1){
for(const pathRow of solution){
const start=await pointFor(client,`.board-card[data-id="B0"] .gate-hit[data-gate="${pathRow.startGate}"]`),
end=await pointFor(client,`.board-card[data-id="B0"] .gate-hit[data-gate="${pathRow.endGate}"]`);
const cellPoints=await pointsForCells(client,'B0',pathRow.cells);
await client.send('Input.dispatchMouseEvent',{type:'mousePressed',x:start.x,y:start.y,button:'left',buttons:1,clickCount:1});
await sleep(20);
for(const point of cellPoints){
await client.send('Input.dispatchMouseEvent',{type:'mouseMoved',x:point.x,y:point.y,button:'left',buttons:1});
await sleep(cpuRate===1?8:16);
}
await client.send('Input.dispatchMouseEvent',{type:'mouseMoved',x:end.x,y:end.y,button:'left',buttons:1});
await sleep(25*Math.max(1,cpuRate));
await client.send('Input.dispatchMouseEvent',{type:'mouseReleased',x:end.x,y:end.y,button:'left',buttons:0,clickCount:1});
await waitFor(()=>client.evaluate("!rendered.get('B0')?.drawing"),{timeout:3000*Math.max(1,cpuRate),label:`origin path ${pathRow.startGate}-${pathRow.endGate} release`});
await sleep(45*Math.max(1,cpuRate));
}
}
async function viewportCenter(client){
return client.evaluate("(()=>{const r=document.querySelector('#viewport').getBoundingClientRect();return{x:r.left+r.width/2,y:r.top+r.height/2}})()");
}
async function pan(client,steps=24,{button='right',selector=null,startPoint=null}={}){
const center=await viewportCenter(client),start=startPoint||(selector?await safePanPoint(client,selector):{x:center.x+180,y:center.y+80}),buttons=button==='left'?1:2;
await client.send('Input.dispatchMouseEvent',{type:'mousePressed',x:start.x,y:start.y,button,buttons,clickCount:1});
for(let index=1;index<=steps;index++){
await client.send('Input.dispatchMouseEvent',{
type:'mouseMoved',x:start.x-index*9,y:start.y-index*4,button:'none',buttons
});
await sleep(12);
}
await client.send('Input.dispatchMouseEvent',{type:'mouseReleased',x:start.x-steps*9,y:start.y-steps*4,button,buttons:0,clickCount:1});
await sleep(250);
return start;
}
async function panCadenceProbe(client,steps=24){
return client.evaluate(`new Promise(resolve=>{
const viewport=document.querySelector('#viewport'),rect=viewport.getBoundingClientRect(),pointerId=91,startX=rect.left+rect.width*.7,startY=rect.top+rect.height*.6;
const [worldX,worldY]=worldUnitAtClient(startX,startY),reactionNow=Date.now(),remoteId='benchmark-remote';
applyRemotePlayer({presenceId:remoteId,playerId:'benchmark',name:'Remote',cursorStyle:'default',x:worldX,y:worldY,vx:.08,vy:.03,sentAt:reactionNow});
applyRealtimeReaction({id:'benchmark-reaction',emoji:REACTION_EMOJIS[0],x:worldX,y:worldY,createdAt:reactionNow,expiresAt:reactionNow+Math.max(3000,${steps}*25)});
const dispatch=(type,index,buttons)=>viewport.dispatchEvent(new PointerEvent(type,{bubbles:true,cancelable:true,pointerId,pointerType:'mouse',isPrimary:true,button:type==='pointerdown'||type==='pointerup'?2:-1,buttons,clientX:startX-index*7,clientY:startY-index*3}));
dispatch('pointerdown',0,2);let index=0;
const tick=()=>{index++;dispatch('pointermove',index,2);if(index===10)queueWorldSignal({sessionId:'benchmark-cross-tab',commitId:'benchmark:'+Date.now(),worldEpoch:data.worldEpoch,stateIds:['B1']});if(index<${steps})requestAnimationFrame(tick);else{dispatch('pointerup',index,0);setTimeout(()=>Promise.resolve(syncQueue).finally(()=>{remotePlayers.delete(remoteId);realtimeReactions.delete('benchmark-reaction');resolve(index)}),80)}};
requestAnimationFrame(tick);
})`);
}
async function pinchZoomProbe(client){
return client.evaluate(`new Promise(resolve=>{
const viewport=document.querySelector('#viewport'),rect=viewport.getBoundingClientRect(),cx=rect.left+rect.width/2,cy=rect.top+rect.height/2,before=cam.scale;
const dispatch=(type,pointerId,x)=>viewport.dispatchEvent(new PointerEvent(type,{bubbles:true,cancelable:true,pointerId,pointerType:'touch',isPrimary:pointerId===111,button:type==='pointerdown'?0:-1,buttons:type==='pointerup'?0:1,clientX:x,clientY:cy}));
dispatch('pointerdown',111,cx-45);dispatch('pointerdown',112,cx+45);let frame=0;
const tick=()=>{frame++;dispatch('pointermove',111,cx-45-frame*5);dispatch('pointermove',112,cx+45+frame*5);if(frame<8)requestAnimationFrame(tick);else{dispatch('pointerup',111,cx-85);dispatch('pointerup',112,cx+85);setTimeout(()=>resolve({before,after:cam.scale,changed:Math.abs(cam.scale-before)>.01}),100)}};
requestAnimationFrame(tick);
})`);
}
async function pickupEdgePanProbe(client,pathRow){
const originalState=await client.evaluate("deepClone(metaState('B0'))"),start=await pointFor(client,`.board-card[data-id="B0"] .gate-hit[data-gate="${pathRow.startGate}"]`),
edge=await client.evaluate("(()=>{const r=document.querySelector('#viewport').getBoundingClientRect();return{x:r.right-2,y:r.top+r.height/2}})()"),
before=await client.evaluate("({x:cam.x,y:cam.y,frames:BEND_PERF.snapshot().counters.dragFrames||0})");
await client.send('Input.dispatchMouseEvent',{type:'mousePressed',x:start.x,y:start.y,button:'left',buttons:1,clickCount:1});
try{
await waitFor(()=>client.evaluate("Boolean(rendered.get('B0')?.drawing)"),{timeout:5000,label:'pickup edge-pan activation'});
await client.send('Input.dispatchMouseEvent',{type:'mouseMoved',x:edge.x,y:edge.y,button:'left',buttons:1});await sleep(350);
const after=await client.evaluate("({x:cam.x,y:cam.y,frames:BEND_PERF.snapshot().counters.dragFrames||0})");
return{cameraMoved:Math.hypot(after.x-before.x,after.y-before.y)>5,dragFrames:after.frames-before.frames};
}finally{
await client.send('Input.dispatchMouseEvent',{type:'mouseReleased',x:edge.x,y:edge.y,button:'left',buttons:0,clickCount:1}).catch(()=>{});
await client.evaluate(`(()=>{data.states.B0=${JSON.stringify(originalState)};normalizedStateObjects.add(data.states.B0);markStateDirty('B0');renderBoardNow(rendered.get('B0'));return true})()`);
await client.evaluate("(async()=>{await persistNow({skipCloud:true});return true})()");
}
}
async function pickupCadenceProbe(client,pathRow,steps=120,sampleDelay=8){
const start=await pointFor(client,`.board-card[data-id="B0"] .gate-hit[data-gate="${pathRow.startGate}"]`),
center=await pointForCell(client,'B0',pathRow.cells[0][0],pathRow.cells[0][1]),
originalState=await client.evaluate("deepClone(metaState('B0'))");
await client.send('Input.dispatchMouseEvent',{type:'mousePressed',x:start.x,y:start.y,button:'left',buttons:1,clickCount:1});
try{
await waitFor(()=>client.evaluate("Boolean(rendered.get('B0')?.drawing)"),{timeout:5000,label:'continuous pickup activation'});
const pendingMoves=[];
for(let index=0;index<steps;index++){
pendingMoves.push(client.send('Input.dispatchMouseEvent',{type:'mouseMoved',x:center.x+(index%2?3:-3),y:center.y+((index%3)-1)*2,button:'left',buttons:1}));
await sleep(sampleDelay);
}
await Promise.all(pendingMoves);
return{moves:steps,reason:'complete',snapshot:await client.evaluate('BEND_PERF.snapshot()')};
}finally{
await client.send('Input.dispatchMouseEvent',{type:'mouseReleased',x:center.x,y:center.y,button:'left',buttons:0,clickCount:1}).catch(()=>{});
await sleep(120);
await client.evaluate(`(()=>{data.states.B0=${JSON.stringify(originalState)};normalizedStateObjects.add(data.states.B0);markStateDirty('B0');renderBoardNow(rendered.get('B0'));return true})()`);
await client.evaluate("(async()=>{await persistNow({skipCloud:true});return true})()");
}
}
async function claimLatencyProbe(client,pathRow,approve){
return client.evaluate(`(async()=>{
const board=rendered.get('B0'),state=metaState('B0'),originalState=deepClone(state),originalEnsure=ensureBoardClaimForInput,pointerId=${approve?93:94};let resolveClaim;
const
wait=milliseconds=>new Promise(resolve=>setTimeout(resolve,milliseconds)),nextFrame=()=>new Promise(resolve=>requestAnimationFrame(resolve));
const cleanup=()=>{cancelBoardDragFrame(board);clearDragRender(board);board.drawing=null;board.armedGate=null;clearPendingClaimPointer(board,pointerId,{release:false});realtimeHeldPointers.delete(pointerId);try{if(board.svg.hasPointerCapture?.(pointerId))board.svg.releasePointerCapture(pointerId)}catch(_){}data.states.B0=originalState;normalizedStateObjects.add(originalState);renderBoardNow(board);refreshInteractionState()};
try{
const cells=${JSON.stringify(pathRow.cells)},startGate=${pathRow.startGate},startColor=canonicalGateColorIndex(board.meta,startGate,null);
state.paths=[{startGate,endGate:null,openGate:null,cells:cells.slice(0,2).map(cell=>[...cell]),colorIndex:startColor,startColorIndex:startColor,endColorIndex:null}];board.drawing=null;renderBoardNow(board);
const hit=board.pathLayer.querySelector('.endpoint-hit[data-path-index="0"][data-endpoint-side="end"]'),rect=board.svg.getBoundingClientRect(),vb=board.svg.viewBox.baseVal,
screenPoint=([row,column])=>({clientX:rect.left+(PAD+(column+.5)*CELL)*rect.width/vb.width,clientY:rect.top+(PAD+(row+.5)*CELL)*rect.height/vb.height}),
down=screenPoint(cells[1]),latest=screenPoint(cells[Math.min(2,cells.length-1)]),before=JSON.stringify(state.paths),fullBefore=perfCounters.fullBoardRenders||0;
ensureBoardClaimForInput=()=>new Promise(resolve=>{resolveClaim=resolve});
hit.dispatchEvent(new PointerEvent('pointerdown',{bubbles:true,cancelable:true,pointerId,pointerType:'mouse',isPrimary:true,button:0,buttons:1,...down}));
await nextFrame();board.svg.dispatchEvent(new PointerEvent('pointermove',{bubbles:true,cancelable:true,pointerId,pointerType:'mouse',isPrimary:true,button:-1,buttons:1,...latest}));
await wait(70);await nextFrame();
const pending=board.pendingClaimPointer,previewWithinFrame=Boolean(pending?.preview?.getAttribute?.('transform')),tracksLatest=Boolean(pending&&Math.abs(pending.clientX-latest.clientX)<.1&&Math.abs(pending.clientY-latest.clientY)<.1),
modelUntouched=JSON.stringify(state.paths)===before&&!board.drawing;
resolveClaim(${approve});await wait(20);await nextFrame();await nextFrame();
const approved=Boolean(board.drawing?.pointerId===pointerId),processed=board.lastProcessedPointerMove,
noJump=${approve}?Boolean(processed&&Math.abs(processed.clientX-latest.clientX)<.1&&Math.abs(processed.clientY-latest.clientY)<.1):!board.drawing&&!board.pendingClaimPointer&&JSON.stringify(state.paths)===before,
fullBoardRenders=(perfCounters.fullBoardRenders||0)-fullBefore;
return{previewWithinFrame,tracksLatest,modelUntouched,approved,noJump,fullBoardRenders};
}finally{ensureBoardClaimForInput=originalEnsure;cleanup()}
})()`);
}
async function measureCursorModes(client){
return client.evaluate(`(async()=>{
const emoji=CURSOR_ITEMS.find(item=>item.cursorEmoji&&!item.flagAsset),flag=CURSOR_ITEMS.find(item=>item.flagAsset),nextFrame=()=>new Promise(resolve=>requestAnimationFrame(resolve));
try{
localStorage.removeItem('bend-field-cursor-renderer');syncCursorAppearance('default');const defaultMode=document.body.dataset.cursorMode;
syncCursorAppearance(emoji.cursorStyle);const emojiMode=document.body.dataset.cursorMode;
syncCursorAppearance(flag.cursorStyle);const flagMode=document.body.dataset.cursorMode;
localStorage.setItem('bend-field-cursor-renderer','dom');syncCursorAppearance(emoji.cursorStyle);
const viewport=document.querySelector('#viewport'),cursorEvent='onpointerrawupdate'in window?'pointerrawupdate':'pointermove',first=new PointerEvent(cursorEvent,{bubbles:true,pointerId:95,pointerType:'mouse',clientX:120,clientY:130});
viewport.dispatchEvent(first);await nextFrame();const firstTransform=customEmojiCursor.style.transform,visibleBeforeDrag=customEmojiCursor.classList.contains('visible');
document.body.classList.add('is-drawing');viewport.dispatchEvent(new PointerEvent(cursorEvent,{bubbles:true,pointerId:95,pointerType:'mouse',clientX:160,clientY:170}));await nextFrame();
return{defaultMode,emojiMode,flagMode,domMode:document.body.dataset.cursorMode,visibleBeforeDrag,visibleDuringDrag:customEmojiCursor.classList.contains('visible'),movedDuringDrag:firstTransform!==customEmojiCursor.style.transform};
}finally{document.body.classList.remove('is-drawing');localStorage.removeItem('bend-field-cursor-renderer');syncCursorAppearance('default')}
})()`);
}
async function measureDisplayCadence(client,frames=90){
return client.evaluate(`new Promise(resolve=>{
const gaps=[];let previous=0,count=0;
const tick=timestamp=>{if(previous)gaps.push(timestamp-previous);previous=timestamp;if(++count<${frames})requestAnimationFrame(tick);else{const sorted=gaps.sort((a,b)=>a-b),pick=q=>sorted[Math.floor((sorted.length-1)*q)]||0;resolve({count:gaps.length,p50:pick(.5),p95:pick(.95),max:sorted[sorted.length-1]||0})}};
requestAnimationFrame(tick);
})`);
}
async function measureCursorCadence(client,steps=180){
const setup=await client.evaluate(`(()=>{
const emoji=CURSOR_ITEMS.find(item=>item.cursorEmoji&&!item.flagAsset),rect=document.querySelector('#viewport').getBoundingClientRect();
localStorage.setItem('bend-field-cursor-renderer','dom');syncCursorAppearance(emoji.cursorStyle);BEND_PERF.reset();
return{left:rect.left+40,top:rect.top+40,width:Math.max(120,rect.width-80),height:Math.max(120,rect.height-80)};
})()`);
try{
for(let index=0;index<steps;index++){
await client.send('Input.dispatchMouseEvent',{type:'mouseMoved',x:setup.left+(index*7)%setup.width,y:setup.top+(index*3)%setup.height,button:'none',buttons:0});
await sleep(8);
}
await sleep(120);const snapshot=await client.evaluate('BEND_PERF.snapshot()'),gap=timing(snapshot,'cursorFrameGap'),age=timing(snapshot,'cursorInputAge');
assert(gap.count>=30&&gap.p50<=20,`DOM cursor cadence missed acceptance: ${JSON.stringify(gap)}`);
assert(age.count>=25&&age.p95<25,`DOM cursor input age missed acceptance: ${JSON.stringify(age)}`);
return snapshot;
}finally{await client.evaluate("localStorage.removeItem('bend-field-cursor-renderer');syncCursorAppearance('default');true")}
}
async function zoom(client,deltaY,repetitions){
const center=await viewportCenter(client);
for(let index=0;index<repetitions;index++){
await client.send('Input.dispatchMouseEvent',{type:'mouseWheel',x:center.x,y:center.y,deltaX:0,deltaY});
await sleep(22);
}
await sleep(300);
}
function timing(snapshot,name){return snapshot.timings?.[name]||{count:0,p50:0,p95:0,max:0}}
async function measureGameplaySimplificationBudgets(client){
const result=await client.evaluate(`(async()=>{
const percentile=(rows,p)=>{const ordered=[...rows].sort((a,b)=>a-b);return ordered[Math.min(ordered.length-1,Math.floor(ordered.length*p))]||0};
const sample=(operation,count=2000)=>{const rows=[];for(let index=0;index<count;index++){const started=performance.now();operation(index);rows.push(performance.now()-started)}return{p95:percentile(rows,.95),max:Math.max(...rows)}};
const snap=sample(index=>directionFromDelta(CELL*(index%2?.46:.6),index%2?0:CELL*.55));
const pointerSamples=sample(()=>pointerEventSamples({pointerId:1,pointerType:'pen',getCoalescedEvents:()=>[{clientX:10,clientY:20},{clientX:20,clientY:20},{clientX:20,clientY:10}]}));
const rect=getMinimapRect(),event={clientX:rect.left+rect.width*.5,clientY:rect.top+rect.height*.5};
const minimap=sample(()=>minimapWorldPoint(event));
const noise=sample(()=>paintNoiseBackground(false),80);
const controls={
origin:Boolean(document.querySelector('#minimapOriginBtn')),
random:Boolean(document.querySelector('#minimapRandomBtn')),
unsolved:Boolean(document.querySelector('#minimapUnsolvedBtn')),
current:Boolean(document.querySelector('#minimapCurrentBtn'))
};
const originalScale=cam.scale;cam.scale=OVERVIEW_ZOOM_THRESHOLD-.001;const overviewBelow=inWorldOverview();
cam.scale=OVERVIEW_ZOOM_THRESHOLD+.001;const overviewAbove=inWorldOverview();cam.scale=originalScale;inWorldOverview();
const summaryProbe=document.createElementNS('http://www.w3.org/2000/svg','path');summaryProbe.classList.add('static-summary-fill');
document.body.append(summaryProbe);const summaryFill=getComputedStyle(summaryProbe).fill;summaryProbe.remove();
const active=rendered.get(activeBoard);let northHudGap=null;
if(active){positionBoardLabel(active);if(active.label.dataset.side==='N'){const placement=hudPlacementCandidates(active.meta)[0],edgeY=PAD+placement.dy*UNIT;northHudGap=edgeY-parseFloat(active.label.style.top)}}
let heartbeats=0;const heartbeat=setInterval(()=>heartbeats++,0),workerStarted=performance.now();
const workerResult=await verifyPuzzleUniquenessAsync(data.metas.B0.puzzle,2000);
const workerElapsed=performance.now()-workerStarted;clearInterval(heartbeat);
return{
snap,pointerSamples,minimap,noise,workerElapsed,heartbeats,workerStatus:workerResult.status,
controls,overviewBelow,overviewAbove,summaryFill,northHudGap,
cellHitCount:document.querySelectorAll('.cell-hit').length,
cellShapeCount:document.querySelectorAll('.board-card .cell-shape').length,
renderedBoardCount:rendered.size,
visibleDetailedCount:[...visibleMetaIds()].filter(id=>data.metas[id]?.puzzle&&rendered.has(id)).length,
visiblePuzzleCount:[...visibleMetaIds()].filter(id=>data.metas[id]?.puzzle).length,
fpsVisible:Boolean(document.querySelector('#fpsCounter')),
difficultyItems:STORE_ITEMS.filter(item=>item.id==='level-min-10'||item.id==='level-max-10').length
};
})()`);
assert(result.snap.p95<1,`Pointer snap p95 ${result.snap.p95.toFixed(3)} ms exceeded 1 ms`);
assert(result.pointerSamples.p95<1,`Pointer-sample p95 ${result.pointerSamples.p95.toFixed(3)} ms exceeded 1 ms`);
assert(result.minimap.p95<1,`Minimap conversion p95 ${result.minimap.p95.toFixed(3)} ms exceeded 1 ms`);
assert(result.noise.p95<10&&result.noise.max<20,`Noise update p95/max ${result.noise.p95.toFixed(3)}/${result.noise.max.toFixed(3)} ms exceeded the budget`);
assert(result.controls.origin&&result.controls.random&&!result.controls.unsolved&&!result.controls.current,'Minimap teleport controls do not match origin + random');
assert(result.overviewBelow&&!result.overviewAbove,'World overview retained zoom hysteresis');
assert(result.summaryFill==='rgba(0, 0, 0, 0)'||result.summaryFill==='transparent',`Nearby board summary still has a filled square (${result.summaryFill})`);
if(result.northHudGap!=null)assert(Math.abs(result.northHudGap-22)<.01,`Top puzzle HUD gap is ${result.northHudGap}, expected 22 world pixels`);
assert(result.cellHitCount===0&&result.cellShapeCount>0,'Detailed boards still allocate one hit node per cell or lack compound cell paths');
assert(result.visibleDetailedCount===result.visiblePuzzleCount,'A visible valid board was hidden from detailed rendering');
assert(result.fpsVisible&&result.difficultyItems===0,'FPS display is missing or retired difficulty items remain');
assert(result.heartbeats>0&&['unique','multiple','unsolved','timeout','invalid'].includes(result.workerStatus),'Uniqueness verification did not yield to the main thread');
return result;
}
function validateMeasurement(result){
const {snapshot,cpuRate,profile}=result,cadenceSnapshot=result.pickupCadence||snapshot,
drag=timing(snapshot,'processBoardDragFrame'),
probeDrag=timing(cadenceSnapshot,'processBoardDragFrame'),
camera=timing(snapshot,'commitCameraInteraction'),
dragGap=timing(cadenceSnapshot,'pickupVisualFrameGap'),cameraGap=timing(snapshot,'cameraFrameGap'),
dragAge=timing(cadenceSnapshot,'pickupVisualInputAge'),cameraAge=timing(snapshot,'cameraInputAge'),
minimap=timing(snapshot,'drawMinimap'),ensure=timing(snapshot,'ensureBoards'),save=timing(snapshot,'persistDirtyToDb'),
overview=timing(snapshot,'drawWorldOverview'),mirrorChunk=timing(snapshot,'mirrorChunkWrite'),
dragLimit=cpuRate===1?8:16,minimapLimit=cpuRate===1?20:33,lodLimit=cpuRate===1?40:100;
assert(drag.count+probeDrag.count>=5,`${profile}/${cpuRate}x captured only ${drag.count} functional and ${probeDrag.count} continuous-input drag frames: ${JSON.stringify(result.pickupProbe)}`);
assert(camera.count>=5,`${profile}/${cpuRate}x did not capture frame-coalesced camera work`);
assert(minimap.count>=2,`${profile}/${cpuRate}x did not capture minimap work`);
assert(ensure.count>=2,`${profile}/${cpuRate}x did not capture LOD work`);
assert(save.count>=1,`${profile}/${cpuRate}x did not capture an autosave`);
assert(overview.count>=1,`${profile}/${cpuRate}x did not capture overview rendering`);
assert(cameraGap.count>=5&&(cpuRate!==1||dragGap.count>=4),`${profile}/${cpuRate}x did not capture enough real interaction cadence samples (pickup ${dragGap.count}, camera ${cameraGap.count})`);
if(cpuRate===1){
const approved=result.claimApproved,denied=result.claimDenied;
assert(approved?.previewWithinFrame&&approved.tracksLatest&&approved.modelUntouched&&approved.approved&&approved.noJump&&approved.fullBoardRenders===0,`${profile} claim approval preview/commit failed: ${JSON.stringify(approved)}`);
assert(denied?.previewWithinFrame&&denied.tracksLatest&&denied.modelUntouched&&!denied.approved&&denied.noJump&&denied.fullBoardRenders===0,`${profile} claim denial rollback failed: ${JSON.stringify(denied)}`);
assert(result.edgePan?.cameraMoved&&result.edgePan.dragFrames>=2,`${profile} pickup edge-pan did not retain continuous drag frames: ${JSON.stringify(result.edgePan)}`);
}
assert(result.pinch?.changed,`${profile}/${cpuRate}x pinch zoom did not change camera scale: ${JSON.stringify(result.pinch)}`);
assert(result.overviewPathsObserved===0,`${profile}/${cpuRate}x far overview rendered route lines`);
assert((snapshot.counters.overviewCacheBuilds||0)>result.overviewBuildBaseline,`${profile}/${cpuRate}x long overview pan did not rebuild the exhausted cache after settlement`);
if(cpuRate===1){
const cadenceHot=Object.entries(cadenceSnapshot.timings||{}).filter(([,value])=>value.max>1).sort((a,b)=>b[1].max-a[1].max).slice(0,12);
assert(dragGap.p50<=18&&dragGap.p95<=22,`${profile} pickup visual median/p95 gap ${dragGap.p50.toFixed(2)}/${dragGap.p95.toFixed(2)} ms exceeded the capped 60 Hz cadence budget; active timings ${JSON.stringify(cadenceHot)}`);
assert(cameraGap.p50<=20,`${profile} camera median gap ${cameraGap.p50.toFixed(2)} ms exceeded 20 ms`);
assert(dragAge.p95<30,`${profile} pickup input age ${dragAge.p95.toFixed(2)} ms exceeded 30 ms`);
assert(cameraAge.p95<25,`${profile} camera input age ${cameraAge.p95.toFixed(2)} ms exceeded 25 ms`);
}
assert(Math.max(drag.p95,probeDrag.p95)<=dragLimit,`${profile}/${cpuRate}x drag p95 ${Math.max(drag.p95,probeDrag.p95).toFixed(2)} ms exceeded acceptance`);
assert(camera.p95<=dragLimit,`${profile}/${cpuRate}x camera p95 ${camera.p95.toFixed(2)} ms exceeded acceptance`);
assert(minimap.p95<=minimapLimit,`${profile}/${cpuRate}x minimap p95 ${minimap.p95.toFixed(2)} ms exceeded acceptance`);
assert(ensure.p95<=lodLimit,`${profile}/${cpuRate}x LOD p95 ${ensure.p95.toFixed(2)} ms exceeded acceptance`);
if(mirrorChunk.count)assert(mirrorChunk.max<50,`${profile}/${cpuRate}x mirror chunk write ${mirrorChunk.max.toFixed(2)} ms became a long task`);
if(cpuRate===1){
const modelWork=timing(snapshot,'pickupModelWork'),visualWork=timing(snapshot,'pickupVisualWork'),dragRender=timing(snapshot,'renderDragFrame');
assert(result.pickupProbeLongTasks===0&&result.pickupLongTasks===0&&result.panLongTasks===0,`${profile} recorded a 50 ms long task during continuous pickup (${result.pickupProbeLongTasks}), real pickup (${result.pickupLongTasks}), or the ten-second pan (${result.panLongTasks}); last ${snapshot.gauges.lastInteractionLongTaskMs||0} ms; phases preview ${timing(snapshot,'pickupPointerDownPreview').max.toFixed(1)}, commit ${timing(snapshot,'pickupPointerDownCommit').max.toFixed(1)}, finish ${timing(snapshot,'pickupPointerFinish').max.toFixed(1)}, drag ${drag.max.toFixed(1)}, model ${modelWork.max.toFixed(1)}, visual ${visualWork.max.toFixed(1)}, render ${dragRender.max.toFixed(1)}; ${snapshot.gauges.lastInteractionLoafScripts||'no LoAF attribution'}`);
}
for(const name of ['minimapDrawsDuringInteraction','lodPassesDuringInteraction','overviewBuildsDuringInteraction','persistenceDuringInteraction','worldRefreshesDuringInteraction'])
assert((snapshot.counters[name]||0)===0&&(cadenceSnapshot.counters[name]||0)===0,`${profile}/${cpuRate}x ran ${name} during an active gesture`);
assert((snapshot.counters.worldRefreshesDeferredDuringInteraction||0)>=1,`${profile}/${cpuRate}x did not defer the injected cross-tab refresh until gesture settlement`);
assert(snapshot.gauges.renderedBoards>=snapshot.gauges.visibleUnsolvedBoards,`${profile}/${cpuRate}x omitted a visible unsolved board from detailed rendering`);
assert((cadenceSnapshot.rates.dragFramesPerSecond||0)<=65,`${profile}/${cpuRate}x pickup presentation exceeded the 60 FPS ceiling (${(cadenceSnapshot.rates.dragFramesPerSecond||0).toFixed(1)} FPS)`);
assert(snapshot.gauges.domNodes<18000,`${profile}/${cpuRate}x DOM size is not viewport-bounded`);
}
async function measureScenario(client,starter,profile,cpuRate){
await client.send('Emulation.setCPUThrottlingRate',{rate:1});
await client.evaluate("if(typeof lifecyclePersistenceSuppressed!=='undefined')lifecyclePersistenceSuppressed=true;true");
const solution=await populateWorld(client,starter,profile);
await reload(client);
const pristineOriginState=await client.evaluate("deepClone(metaState('B0'))");
await client.send('Emulation.setCPUThrottlingRate',{rate:cpuRate});
const claimApproved=cpuRate===1?await claimLatencyProbe(client,solution[0],true):null;
if(cpuRate===1)await restoreOriginState(client,pristineOriginState);
const claimDenied=cpuRate===1?await claimLatencyProbe(client,solution[0],false):null;
if(cpuRate===1)await restoreOriginState(client,pristineOriginState);
const edgePan=cpuRate===1?await pickupEdgePanProbe(client,solution[0]):null;
if(cpuRate===1)await restoreOriginState(client,pristineOriginState);
if(cpuRate===1)await sleep(300);
await prepareInteractionMeasurement(client);
const pickupProbe=await pickupCadenceProbe(client,solution[0],cpuRate===1?180:120,cpuRate===1?8:16);
const pickupCadence=pickupProbe.snapshot;delete pickupProbe.snapshot;
const pickupProbeLongTasks=pickupCadence?.counters.interactionLongTasks||0;
await restoreOriginState(client,pristineOriginState);
await sleep(300);
await prepareInteractionMeasurement(client);
const preSolveState=await client.evaluate("({pathCount:metaState('B0').paths.length,solved:metaState('B0').solved,drawing:Boolean(rendered.get('B0')?.drawing),pending:Boolean(rendered.get('B0')?.pendingClaimPointer)})");
assert(preSolveState.pathCount===0&&!preSolveState.solved&&!preSolveState.drawing&&!preSolveState.pending,`${profile.name}/${cpuRate}x pickup probes did not restore a pristine origin board: ${JSON.stringify(preSolveState)}`);
await solveOrigin(client,solution,cpuRate);
try{await waitFor(()=>client.evaluate("metaState('B0').solved===true"),{timeout:5000*Math.max(1,cpuRate),label:`${profile.name}/${cpuRate}x solved origin`})}
catch(error){const diagnostic=await client.evaluate("(()=>{const state=metaState('B0'),board=rendered.get('B0');return{solved:state.solved,paths:state.paths.map(path=>({startGate:path.startGate,endGate:path.endGate,openGate:path.openGate,cells:path.cells})),drawing:board?.drawing?{pathIndex:board.drawing.pathIndex,catchupPending:board.drawing.catchupPending}:null,queued:board?.pointerMoveSamples?.length||0,logicalTrace:board?.logicalPointerCellTrace||[],flushTrace:board?.lastFlushPointerCells||[],staticBoard:staticRendered.has('B0')}})()");throw new Error(`${error.message}: ${JSON.stringify(diagnostic)}`)}
await sleep(850);
const pickupLongTasks=await client.evaluate('BEND_PERF.snapshot().counters.interactionLongTasks||0');
await client.evaluate('finishCompletionVisual("B0",true);centerMeta(data.metas.B0);true');await sleep(350);
const leftStart=await safePanPoint(client,'.board-card[data-id="B0"]'),
leftProbe=await client.evaluate(`(()=>{const target=document.elementFromPoint(${leftStart.x},${leftStart.y});return{tag:target?.tagName||null,classes:target?.getAttribute?.('class')||null,board:target?.closest?.('.board-card')?.dataset?.id||null,allowed:leftFieldPanAllowed({button:0,target})}})()`);
const beforeLeftPan=await client.evaluate('({x:cam.x,y:cam.y,solved:metaState("B0").solved})');
assert(beforeLeftPan.solved&&leftProbe.allowed,`${profile.name}/${cpuRate}x solved-board left drag is not eligible for panning: ${JSON.stringify({leftStart,leftProbe,beforeLeftPan})}`);
await zoom(client,180,15);
const overviewPathsObserved=await client.evaluate('drawWorldOverview();BEND_PERF.snapshot().gauges.overviewPaths||0');
const overviewBuildBaseline=await client.evaluate('BEND_PERF.snapshot().counters.overviewCacheBuilds||0');
const panLongTaskBaseline=await client.evaluate('BEND_PERF.snapshot().counters.interactionLongTasks||0');
await pan(client);
await panCadenceProbe(client,cpuRate===1?600:60);
await waitFor(()=>client.evaluate(`(()=>{if(!inWorldOverview()||!overviewCache)return false;const[centerX,centerY]=cameraCenterInChunks();return!overviewDirty&&Math.abs(centerX-overviewCache.anchorX)*overviewCache.unit<=overviewCache.overscan*.82&&Math.abs(centerY-overviewCache.anchorY)*overviewCache.unit<=overviewCache.overscan*.82})()`),{timeout:10000,label:'settled overview cache rebuild'});
const pinch=await pinchZoomProbe(client);
await sleep(80);
const panLongTasks=(await client.evaluate('BEND_PERF.snapshot().counters.interactionLongTasks||0'))-panLongTaskBaseline;
await zoom(client,-180,15);
await sleep(650);
try{await waitFor(()=>client.evaluate("!hasPendingPersistence()"),{timeout:20000,label:'durable persistence drain'})}
catch(error){
const diagnostic=await client.evaluate(`({globalDirty,dirtyMetaIds:[...dirtyMetaIds],dirtyStateIds:[...dirtyStateIds],deletedBoardIds:[...deletedBoardIds],cloudOutboxDeleteKeys:[...cloudOutboxDeleteKeys],saveTimer:Boolean(saveTimer),status:saveStatusEl?.dataset?.state||null,message:statusMessage?.textContent||null,storageFormat:activeStorageFormat,epoch:data?.worldEpoch})`);
throw new Error(`${error.message}: ${JSON.stringify(diagnostic)}`);
}
await sleep(500);
const snapshot=await client.evaluate('BEND_PERF.snapshot()');
const result={profile:profile.name,boards:profile.boards,cpuRate,snapshot,pickupCadence,pickupProbe,overviewPathsObserved,overviewBuildBaseline,pickupProbeLongTasks,pickupLongTasks,panLongTasks,claimApproved,claimDenied,edgePan,pinch};
validateMeasurement(result);
return result;
}
async function main(runProfiles=profiles,cpuRates=[1,4]){
if(!fs.existsSync(edgePath))throw new Error(`Microsoft Edge was not found at ${edgePath}`);
const server=await startStaticServer();
const edge=spawn(edgePath,[
'--headless','--no-first-run','--no-sandbox','--force-device-scale-factor=1','--js-flags=--expose-gc','--disable-dev-shm-usage','--disable-extensions','--disable-background-networking','--disable-background-timer-throttling','--disable-renderer-backgrounding','--disable-backgrounding-occluded-windows','--disable-crash-reporter','--disable-breakpad','--disable-component-update','--disable-features=OptimizationGuideModelDownloading,OnDeviceModelService,PromptApiForGeminiNano',
`--remote-debugging-port=${debuggingPort}`,'--remote-allow-origins=*',`--user-data-dir=${profilePath}`,
`--host-resolver-rules=MAP ${benchmarkHost} 127.0.0.1`,'--window-size=1440,1000','about:blank'
],{stdio:'ignore',windowsHide:true});
let client=null;
try{
const target=await endpoint();client=new CdpClient(target.webSocketDebuggerUrl);await client.connect();
await client.send('Page.enable');await client.send('Runtime.enable');
await client.send('Page.navigate',{url:`http://${benchmarkHost}:${serverPort}/`});
if(startupOnly){
await sleep(12000);
const state=await client.evaluate("({ready:document.body?.dataset?.ready||null,version:document.querySelector('.brand small')?.textContent||null,boards:typeof data!=='undefined'?Object.keys(data.metas).length:null,origin:typeof data!=='undefined'&&Boolean(data.metas?.B0?.puzzle),worldGeneration:typeof data!=='undefined'?data.worldGeneration:null,turnFont:getComputedStyle(document.querySelector('.board-svg text')||document.body).fontFamily,status:document.querySelector('#statusMessage')?.textContent||'',text:document.body?.innerText?.slice(0,300)||''})");
assert(state.ready==='true'&&state.version==='v47.77'&&state.boards===1&&state.origin&&state.worldGeneration==='v47-field-reset-20260728-interaction-fix',`Real-browser startup state is incomplete: ${JSON.stringify(state)}`);
assert(/DotGothic16|Press Start 2P|MS Gothic|monospace/i.test(state.turnFont),'Dot-styled game font is not active in the browser');
console.log(`Real-browser startup passed: ${JSON.stringify(state)}`);return;
}
await ready(client);await sleep(1000);
const displayCadence=await measureDisplayCadence(client);
assert(displayCadence.count>=60&&displayCadence.p50<=20,`Headless display baseline is not 60 Hz: ${JSON.stringify(displayCadence)}`);
const gameplayBudgets=await measureGameplaySimplificationBudgets(client);
const cursorModes=await measureCursorModes(client);
assert(cursorModes.defaultMode==='default'&&cursorModes.emojiMode==='native'&&cursorModes.flagMode==='native'&&cursorModes.domMode==='dom'&&cursorModes.visibleBeforeDrag&&cursorModes.visibleDuringDrag&&cursorModes.movedDuringDrag,`Cursor mode runtime probe failed: ${JSON.stringify(cursorModes)}`);
const cursorCadence=await measureCursorCadence(client);
console.log(`Display cadence | median ${displayCadence.p50.toFixed(2)} ms | p95 ${displayCadence.p95.toFixed(2)} ms`);
console.log(`Gameplay budgets | snap ${gameplayBudgets.snap.p95.toFixed(3)} ms | pointer samples ${gameplayBudgets.pointerSamples.p95.toFixed(3)} ms | minimap ${gameplayBudgets.minimap.p95.toFixed(3)} ms | noise max ${gameplayBudgets.noise.max.toFixed(3)} ms | worker ${gameplayBudgets.workerElapsed.toFixed(1)} ms`);
console.log(`Cursor cadence | median gap ${timing(cursorCadence,'cursorFrameGap').p50.toFixed(2)} ms | input p95 ${timing(cursorCadence,'cursorInputAge').p95.toFixed(2)} ms`);
const starter=await readStarterRows(client),results=[];
for(const profile of runProfiles)for(const cpuRate of cpuRates){
const result=await measureScenario(client,starter,profile,cpuRate);results.push(result);
const drag=timing(result.snapshot,'processBoardDragFrame'),camera=timing(result.snapshot,'commitCameraInteraction'),minimap=timing(result.snapshot,'drawMinimap'),
ensure=timing(result.snapshot,'ensureBoards'),save=timing(result.snapshot,'persistDirtyToDb');
console.log(`${profile.name.padEnd(6)} ${cpuRate}x CPU | drag p95 ${drag.p95.toFixed(2)} ms | camera ${camera.p95.toFixed(2)} ms | minimap ${minimap.p95.toFixed(2)} ms | LOD ${ensure.p95.toFixed(2)} ms | save ${save.p95.toFixed(2)} ms | DOM ${result.snapshot.gauges.domNodes}`);
}
console.log(`BROWSER_BENCHMARK_JSON=${JSON.stringify(results.map(result=>({
profile:result.profile,boards:result.boards,cpuRate:result.cpuRate,
dragP95:timing(result.snapshot,'processBoardDragFrame').p95,
cameraP95:timing(result.snapshot,'commitCameraInteraction').p95,
dragGapP95:timing(result.pickupCadence||result.snapshot,'pickupVisualFrameGap').p95,
dragInputAgeP95:timing(result.pickupCadence||result.snapshot,'pickupVisualInputAge').p95,
cameraGapP50:timing(result.snapshot,'cameraFrameGap').p50,
cameraInputAgeP95:timing(result.snapshot,'cameraInputAge').p95,
minimapP95:timing(result.snapshot,'drawMinimap').p95,
lodP95:timing(result.snapshot,'ensureBoards').p95,
saveP95:timing(result.snapshot,'persistDirtyToDb').p95,
overviewP95:timing(result.snapshot,'drawWorldOverview').p95,
renderedBoards:result.snapshot.gauges.renderedBoards,
staticBoards:result.snapshot.gauges.staticBoards,
domNodes:result.snapshot.gauges.domNodes,
longTasks:result.snapshot.counters.longTasks||0
})))}`);
console.log('Real-browser performance benchmark passed');
}finally{
client?.close();
stopBrowserTree(edge);
server.closeAllConnections?.();await new Promise(resolve=>server.close(()=>resolve()));
for(let attempt=0;attempt<8;attempt++){
try{fs.rmSync(temporaryRoot,{recursive:true,force:true});break}
catch(error){if(attempt===7)console.warn(`Benchmark cleanup deferred: ${error.message}`);await sleep(150)}
}
}
}
module.exports={main,allProfiles};
if(require.main===module)main().then(()=>process.exit(0)).catch(error=>{console.error(error);process.exit(1)});

View file

@ -0,0 +1,54 @@
'use strict';
const assert=require('assert/strict');
const fs=require('fs');
const path=require('path');
const {root,app,html,css,functionSource,read}=require('./helpers/app-source');
const server=read('server.js'),realtime=read('realtime-server.js'),catalog=JSON.parse(read('store-catalog.json'));
// Retired difficulty-adjustment system must be absent from every runtime layer.
assert.equal(catalog.some(item=>item.id==='level-min-10'||item.id==='level-max-10'||item.targetMode||item.fieldRadius),false);
for(const [name,source] of [['app',app],['server',server],['realtime',realtime],['html',html]]){
assert(!source.includes('fieldOverlayCanvas'),`${name}: field overlay remains`);
assert(!source.includes('level-min-10')&&!source.includes('level-max-10'),`${name}: retired item id remains`);
assert(!source.includes('fieldAdjusted')&&!source.includes('generatedDifficulty'),`${name}: retired difficulty metadata remains`);
}
assert(!server.includes("url.pathname==='/api/player/place-field'")&&!realtime.includes('broadcastFieldEffect'));
assert(!app.includes('invalidateFieldOverlay')&&!app.includes('invalidateStoreEffectCache'));
assert.equal(catalog.length>0,true);assert.equal(catalog.filter(item=>item.id==='score-lens').length,1);
// Clicking a board must select/input only; camera navigation is explicit elsewhere.
assert(functionSource('shouldTeleportToUnsolvedBoard').includes('return false'));
assert(!functionSource('bindBoard').includes('centerMeta('));
assert(!app.includes('function promoteStaticBoard('));
assert(functionSource('makeStaticBoard').includes('card.tabIndex=-1')&&!functionSource('makeStaticBoard').includes("setAttribute('role','button')"));
// Every hydrated puzzle in the visible field remains fully detailed.
assert(functionSource('desiredInteractiveBoardIds').includes('for(const id of ids)'));
assert(!app.includes('INTERACTIVE_DETAIL_CELL_BUDGET')&&!functionSource('ensureBoards').includes('makeStaticBoard(meta)'));
const makeBoard=functionSource('makeBoard');
assert(makeBoard.includes('boardCellsPath(p.valid)')&&makeBoard.includes("class:'cell-shape'"));
assert(!makeBoard.includes('cellHits.set')&&!makeBoard.includes("class:'cell-hit'"));
assert(functionSource('drawOuterEdges').includes("svgEl('path'"));
assert(functionSource('flashConfirmedCell').includes("svgEl('rect'")&&functionSource('flashConfirmedCell').includes('node.remove()'));
// Work and memory are bounded without hiding visible boards.
assert(app.includes('LOD_CHANGES_PER_PASS=1')&&app.includes('BOARD_RENDERS_PER_FRAME=6')&&app.includes('HYDRATE_CONCURRENCY=4'));
assert(functionSource('evictHydratedBoardDetails').includes('hydratedBoardLru.size>32'));
assert(functionSource('evictHydratedBoardDetails').includes('hydratedBoardBytes>8*1024*1024'));
assert(functionSource('drawPresenceLayer').includes('dpr=1')&&functionSource('drawReactionLayer').includes('dpr=1'));
assert(functionSource('scheduleNoiseBackground').includes('noisePainted')&&!functionSource('scheduleNoiseBackground').includes('setTimeout'));
assert(!functionSource('applyCamera').includes('FRAME_INTERVAL')&&app.includes('AUXILIARY_FPS=30')&&app.includes('DRAG_TARGET_FPS=60'));
// Lightweight diagnostics and larger/longer completion reward visuals.
assert(html.includes('id="fpsCounter"')&&functionSource('refreshFpsCounter').includes('FPS ${fpsLastValue}'));
assert(css.includes('.gem-particle{position:fixed')&&css.includes('width:28px;height:28px'));
assert(functionSource('completionEffect').includes('1800'));
assert(functionSource('playGemCollectionAnimation').includes('duration=reduced?520:1450'));
assert(!functionSource('bindBoard').includes('skipCompletionVisuals'));
// Purchase normalization must remain functional after deleting field purchases.
assert(server.includes('function cleanId(')&&server.includes('function storeItem('));
assert.equal(fs.existsSync(path.join(root,'docs','v47.71-input-ui-performance.md')),false);
const internalSpec=fs.readFileSync(path.join(root,'docs','internal-system.md'),'utf8');
assert(internalSpec.includes('Do not add changelog')&&internalSpec.includes('update the relevant current specification in place'));
console.log('Difficulty removal, no-click-teleport, rendering, memory, FPS, and completion visual guards passed');

View file

@ -0,0 +1,235 @@
'use strict';
const {vm,app,assert,functionSource}=require('./helpers/app-source');
const persistSource=functionSource('persistDirtyToDb');
const startupSource=functionSource('loadSnapshotFromDb');
const startupV2Source=functionSource('loadV2SnapshotFromDb');
const initialSource=functionSource('readInitialDataAsync');
const replaceSource=functionSource('beginWorldReplacement');
const clearSource=functionSource('clearDatabaseWorld');
assert(startupSource.includes('loadV2SnapshotFromDb')&&startupV2Source.includes("db.transaction(['worlds','boardIndex','outboxV2','recoveryV2','tombstonesV2'],'readonly')"),
'Startup does not read the snapshot, recovery coverage, and tombstones atomically');
assert(startupV2Source.includes("objectStore('recoveryV2').getAll")&&startupV2Source.includes("objectStore('tombstonesV2').getAll"),
'Startup omits database recovery journals or tombstones');
assert(!initialSource.includes('loadRecoveryCoverage('),
'Startup still performs the recovery coverage read in a second transaction');
assert(replaceSource.includes('await preserveRecoveryDurably('),
'World replacement does not await a verified recovery backup');
assert(functionSource('retryRecovery').includes('readRecoveryEnvelope')&&functionSource('retryRecovery').includes('stageRecoverySnapshotV2')&&functionSource('retryRecovery').includes("kind:'recovery'"),
'Recovery backup is not restored through the validated staged-world path');
assert(clearSource.includes("activateReadyWorldV2({epoch:newEpoch,expected,world},{kind:'reset'})"),
'Fresh-world reset does not use atomic epoch activation');
assert(!app.includes('worldMutationLockDepth')&&functionSource('withWorldMutationLock').includes("mode:'exclusive'"),
'World mutation locking still bypasses unrelated asynchronous callers');
assert(functionSource('persistNow').includes('if(options.lockHeld===true)return run()')&&functionSource('expandMetaNow').includes('lockHeld:true'),
'Nested expansion persistence can deadlock behind a queued lock waiter');
assert(app.includes('if(!worldInitReady){deferredWorldSignals.push(signal)'),
'Cross-tab messages are not buffered until initialization is complete');
assert(functionSource('initCloudSync').includes('if(cloudApiEnabled&&!cloudOutboxReady)'),
'Cloud synchronization is not fail-closed when IndexedDB health is uncertain');
const request=result=>({result});
function memoryStore(initial=[],keyOf=row=>row.id){
const normalizeKey=key=>Array.isArray(key)?key.join(':'):key,rows=new Map(initial.map(row=>[normalizeKey(keyOf(row)),structuredClone(row)]));
return{
rows,
get:key=>request(rows.has(normalizeKey(key))?structuredClone(rows.get(normalizeKey(key))):null),
put:row=>{rows.set(normalizeKey(keyOf(row)),structuredClone(row));return request(keyOf(row))},
delete:key=>{rows.delete(normalizeKey(key));return request(undefined)},
clear:()=>{rows.clear();return request(undefined)}
};
}
function persistenceContext({localMeta=null,localState=null,existingMeta=null,existingState=null,deletion=null,identityEpoch='world:test'}={}){
const existingId=existingMeta?.id||'B0',existingIndex=existingMeta?{epoch:'world:test',id:existingId,metaRev:existingMeta.rev||0,stateRev:existingState?.rev||0}:null;
const remoteGlobal={worldEpoch:'world:test',globalRev:20,globalRevAuthor:'remote',nextId:9,cursorStyle:'remote'};
const stores={
control:memoryStore([{key:'active',activeFormat:2,activeEpoch:identityEpoch}],row=>row.key),
worlds:memoryStore([{epoch:'world:test',status:'active',createdAt:1,source:{kind:'fresh'},global:remoteGlobal}],row=>row.epoch),
boardIndex:memoryStore(existingIndex?[existingIndex]:[],row=>`${row.epoch}:${row.id}`),
boardPuzzles:memoryStore(existingMeta?[{epoch:'world:test',id:existingId,metaRev:existingMeta.rev||0,meta:existingMeta}]:[],row=>`${row.epoch}:${row.id}`),
boardStates:memoryStore(existingState?[{epoch:'world:test',id:existingId,stateRev:existingState.rev||0,value:existingState}]:[],row=>`${row.epoch}:${row.id}`),
outboxV2:memoryStore([],row=>`${row.epoch}:${row.key}`),recoveryV2:memoryStore([],row=>`${row.epoch}:${row.key}`),tombstonesV2:memoryStore([],row=>`${row.epoch}:${row.id}`)
};
const id=localMeta?.id||existingMeta?.id||'B0';
const data={
worldEpoch:'world:test',globalRev:10,globalRevAuthor:'local',nextId:2,cursorStyle:'local',
metas:localMeta?{[id]:structuredClone(localMeta)}:{},
states:localState?{[id]:structuredClone(localState)}:{},
updatedAt:0,clockFloor:0
};
const context={
console,structuredClone,sessionId:'local',lastRevision:100,data,
dirtyMetaIds:new Set(localMeta?[id]:[]),dirtyStateIds:new Set(localState?[id]:[]),
deletedBoardIds:new Set(deletion?[id]:[]),
deletedBoardRevisions:new Map(deletion?[[id,deletion.rev]]:[]),
deletedBoardAuthors:new Map(deletion?[[id,deletion.revAuthor]]:[]),
recoveryJournalsToCover:[],recoveryJournalSeq:-1,recoveryWalPromise:Promise.resolve(),
cloudOutboxDeleteKeys:new Set(),cloudJournalMetaIds:new Set(),cloudJournalStateIds:new Set(),
cloudJournalDeletedIds:new Set(deletion?[id]:[]),cloudApiEnabled:false,
globalDirty:true,globalChangeSeq:1,cloudJournalGlobalChanged:true,worldSignalSeq:0,
idbAvailable:true,activeStorageFormat:2,FIELD_STORAGE_FORMAT:2,storageAccessError:null,normalizedStateObjects:new WeakSet(),
boardIndexSummaries:new Map(),
statsDirty:false,resetHistory:[],
pruneAndCount:()=>{},
hasPendingPersistence:()=>context.globalDirty||context.dirtyMetaIds.size>0||context.dirtyStateIds.size>0||context.deletedBoardIds.size>0,
validWorldEpoch:value=>typeof value==='string'&&value.startsWith('world:'),
storedWorldEpoch:()=>null,createWorldEpoch:()=> 'world:test',rememberWorldEpoch:()=>true,
revisionVersion:value=>({rev:value?.rev||0,revAuthor:value?.revAuthor||value?.author||''}),
compareRevisionVersions:(a,b)=>(a?.rev||0)-(b?.rev||0)||String(a?.revAuthor||a?.author||'').localeCompare(String(b?.revAuthor||b?.author||'')),
newerRevisionValue:(a,b)=>context.compareRevisionVersions(a,b)>=0?a:b,
trustedNow:()=>1234,
globalForStorage:(source,updatedAt)=>({...source,metas:undefined,states:undefined,updatedAt,clockFloor:updatedAt}),
mergeGlobalRecords:(stored,incoming)=>context.compareRevisionVersions(
{rev:incoming.globalRev,revAuthor:incoming.globalRevAuthor},
{rev:stored?.globalRev,revAuthor:stored?.globalRevAuthor}
)>=0?incoming:stored,
applyGlobalRecordToData:record=>Object.assign(data,structuredClone(record)),
staleWorldEpochError:()=>Object.assign(new Error('stale world'),{code:'STALE_WORLD_EPOCH'}),
metaRowsForStorage:ids=>ids.map(rowId=>data.metas[rowId]&&structuredClone(data.metas[rowId])).filter(Boolean),
stateRowsForStorage:ids=>ids.map(rowId=>data.states[rowId]&&({id:rowId,value:structuredClone(data.states[rowId])})).filter(Boolean),
snapshotForStorage:()=>({}),writeCompactMirror:()=>true,
openWorldDb:async()=>({transaction:()=>({objectStore:name=>stores[name],abort(){this.aborted=true}})}),
transactionDone:async()=>{},requestValue:async req=>req.result,
updateStorageRevision:()=>{},scheduleMirrorCheckpoint:()=>{},
normalizeMeta:(_id,value)=>structuredClone(value),normalizeState:value=>structuredClone(value||{paths:[],rev:0,revAuthor:''}),
metaFromV2Records:(_index,puzzle)=>puzzle?.meta?structuredClone(puzzle.meta):null,
stateFromV2Record:(_index,state)=>state?.value?structuredClone(state.value):null,
puzzleRecordV2:(meta,epoch)=>({epoch,id:meta.id,metaRev:meta.rev||0,meta:structuredClone(meta)}),
stateRecordV2:(rowId,state,epoch)=>({epoch,id:rowId,stateRev:state?.rev||0,value:structuredClone(state)}),
summarizeBoardV2:(meta,state,epoch)=>({epoch,id:meta.id,metaRev:meta.rev||0,stateRev:state?.rev||0}),
fieldBoundsFromMetas:()=>({minX:0,minY:0,maxX:1,maxY:1}),SAVE_SCHEMA:31,WORLD_GENERATION:'current-only-test',
clearRecoveryJournalIfCovered:()=>{},broadcastWorldSignal:()=>{},scheduleCloudPush:()=>{},
refreshWorldView:()=>{context.refreshes++},refreshes:0,
perfStart:()=>0,perfEnd:()=>{},perfGauge:()=>{}
};
vm.createContext(context);
vm.runInContext(`${persistSource}\nthis.persistDirtyToDb=persistDirtyToDb;`,context);
return{context,stores,id};
}
async function verifyPersistenceConflicts(){
{
const localMeta={id:'B0',x:1,rev:5,revAuthor:'local'},localState={paths:[],rev:5,revAuthor:'local'};
const existingMeta={id:'B0',x:7,rev:10,revAuthor:'remote'},existingState={paths:[[1]],rev:11,revAuthor:'remote'};
const {context,stores}=persistenceContext({localMeta,localState,existingMeta,existingState});
await context.persistDirtyToDb();
assert(stores.boardPuzzles.rows.get('world:test:B0').meta.x===7&&stores.boardStates.rows.get('world:test:B0').value.rev===11,
'A stale tab overwrote a newer database row');
assert(context.data.metas.B0.x===7&&context.data.states.B0.rev===11&&context.refreshes===1,
'A stale in-memory board was not reconciled to the committed winner');
assert(context.data.cursorStyle==='remote'&&context.data.nextId===9,
'A stale global record overwrote the newer global winner');
}
{
const existingMeta={id:'B0',x:4,rev:20,revAuthor:'remote'},existingState={paths:[],rev:21,revAuthor:'remote'};
const {context,stores}=persistenceContext({
existingMeta,existingState,deletion:{rev:15,revAuthor:'local'}
});
await context.persistDirtyToDb();
assert(stores.boardPuzzles.rows.has('world:test:B0')&&stores.boardStates.rows.has('world:test:B0')&&!stores.tombstonesV2.rows.has('world:test:B0'),
'A stale deletion removed a newer database board');
assert(context.data.metas.B0?.rev===20&&context.data.states.B0?.rev===21&&context.refreshes===1,
'A rejected stale deletion was not restored in the losing tab');
}
{
const existingMeta={id:'B0',rev:20,revAuthor:'remote'},existingState={paths:[],rev:20,revAuthor:'remote'};
const {context,stores}=persistenceContext({
existingMeta,existingState,deletion:{rev:25,revAuthor:'local'}
});
await context.persistDirtyToDb();
assert(!stores.boardPuzzles.rows.has('world:test:B0')&&!stores.boardStates.rows.has('world:test:B0'),
'A newer deletion did not remove the older board rows');
assert(stores.tombstonesV2.rows.get('world:test:B0')?.rev===25,
'A committed deletion did not leave a durable tombstone');
}
{
const localMeta={id:'B0',rev:5,revAuthor:'local'};
const {context}=persistenceContext({localMeta,identityEpoch:'world:replacement'});
let error=null;try{await context.persistDirtyToDb()}catch(caught){error=caught}
assert(error?.code==='STALE_WORLD_EPOCH'&&context.dirtyMetaIds.has('B0'),
'A stale tab was allowed to write into a replaced world epoch');
}
}
function verifyGlobalMerge(){
const context={
deepClone:value=>structuredClone(value),structuredClone,
validWorldEpoch:value=>typeof value==='string'&&value.startsWith('world:'),
compareRevisionVersions:(a,b)=>(a?.rev||0)-(b?.rev||0)||String(a?.revAuthor||'').localeCompare(String(b?.revAuthor||'')),
bonusEventTotal:events=>Object.values(events||{}).reduce((sum,value)=>sum+(value||0),0)
};
vm.createContext(context);
vm.runInContext(`${functionSource('mergeGlobalRecords')}\nthis.mergeGlobalRecords=mergeGlobalRecords;`,context);
const merged=context.mergeGlobalRecords(
{worldEpoch:'world:test',globalRev:30,globalRevAuthor:'remote',cursorStyle:'remote',nextId:8,clockFloor:11,cloudRevision:3,bonusEvents:{A:4},quarantine:{B:{failedAt:5}},lastSolveAt:10,specialMechanicsSeen:['warp'],combo:2},
{worldEpoch:'world:test',globalRev:20,globalRevAuthor:'local',cursorStyle:'local',nextId:12,clockFloor:15,cloudRevision:9,bonusEvents:{A:2,C:7},quarantine:{B:{failedAt:8}},lastSolveAt:14,specialMechanicsSeen:['lock'],combo:4}
);
assert(merged.cursorStyle==='remote'&&merged.nextId===12&&merged.clockFloor===15&&merged.cloudRevision===9,
'Global LWW and monotonic fields were not merged independently');
assert(merged.bonusEvents.A===4&&merged.bonusEvents.C===7&&merged.bonusScore===11,
'Global additive bonus records were lost during a concurrent merge');
assert(merged.quarantine.B.failedAt===8&&merged.lastSolveAt===14&&!Object.prototype.hasOwnProperty.call(merged,'combo')&&merged.specialMechanicsSeen.join(',')==='lock,warp',
'Global timestamped records, encounter memory, or retired combo data were merged incorrectly');
}
async function verifySignalRetry(){
const timers=[];let attempts=0,remembered=0;
const context={
console:{warn:()=>{}},sessionId:'self',worldInitReady:false,deferredWorldSignals:[],
seenWorldCommitIds:new Set(),pendingWorldCommitIds:new Set(),syncQueue:Promise.resolve(),
worldCommitId:signal=>signal.commitId||'',applyWorldSignal:async()=>{attempts++;if(attempts===1)throw new Error('transient')},
rememberWorldCommit:()=>remembered++,
setTimeout:callback=>{timers.push(callback);return timers.length}
};
vm.createContext(context);
vm.runInContext(`${functionSource('queueWorldSignal')}\n${functionSource('drainWorldSignals')}\nthis.queueWorldSignal=queueWorldSignal;this.drainWorldSignals=drainWorldSignals;`,context);
const signal={commitId:'remote:1',sessionId:'remote'};
context.queueWorldSignal(signal);
assert(context.deferredWorldSignals.length===1&&attempts===0,'A startup signal ran before initialization');
context.drainWorldSignals();context.queueWorldSignal(signal);
await context.syncQueue;
assert(attempts===1&&remembered===0&&timers.length===1,
'A failed signal was deduplicated as if it had succeeded');
timers.shift()();await Promise.resolve();await context.syncQueue;
assert(attempts===2&&remembered===1,'A transient signal failure was not retried and committed once');
}
async function verifyDurableBackup(){
const source=functionSource('preserveRecoveryDurably');
const failing={
Date,safeLocalSet:()=>false,sessionStorage:{setItem(){throw new Error('blocked')}},
persistRecoveryEnvelope:async()=>false,volatileRecovery:null,recoveryStorageKey:'recovery'
};
vm.createContext(failing);vm.runInContext(`${source}\nthis.preserveRecoveryDurably=preserveRecoveryDurably;`,failing);
let rejected=false;try{await failing.preserveRecoveryDurably('save','reset')}catch(_){rejected=true}
assert(rejected,'Destructive replacement proceeded without any durable backup');
const database={
Date,safeLocalSet:()=>false,sessionStorage:{setItem(){throw new Error('blocked')}},
persistRecoveryEnvelope:async()=>true,volatileRecovery:null,recoveryStorageKey:'recovery'
};
vm.createContext(database);vm.runInContext(`${source}\nthis.preserveRecoveryDurably=preserveRecoveryDurably;`,database);
const result=await database.preserveRecoveryDurably('save','reset');
assert(!result.browserCopy&&result.databaseCopy,'A verified database-only backup was not accepted');
}
function verifyRevisionSeed(){
const context={
lastRevision:3,data:{},recoveredDeletionTombstones:new Map([['B9',{rev:81}]])
};
vm.createContext(context);
vm.runInContext(`${functionSource('seedRevisionClock')}\nthis.seedRevisionClock=seedRevisionClock;`,context);
const maximum=context.seedRevisionClock({
globalRev:50,clockFloor:0,metas:{B0:{rev:60}},states:{B0:{rev:70}}
});
assert(maximum===81&&context.lastRevision===81,'The revision clock was not seeded from all persisted row types');
}
(async()=>{
await verifyPersistenceConflicts();
verifyGlobalMerge();
await verifySignalRetry();
await verifyDurableBackup();
verifyRevisionSeed();
console.log('Concurrency and replacement safety test passed');
})().catch(error=>{console.error(error);process.exitCode=1});

View file

@ -0,0 +1,22 @@
'use strict';
const {assert,loadAppLogic}=require('./helpers/app-source');
const AppLogic=loadAppLogic(),samples=[],storeCoefficients=[];
for(let seed=1;seed<=5000;seed++){
const level=1+(seed%10),baseScore=20+level*level*4,meta={id:`B${seed}`,seed,generatorVersion:5},
state={paths:[{startGate:0,endGate:1,cells:[[0,0],[0,1],[1,1],[1,2]]}]},
reward=AppLogic.deterministicBoardReward(baseScore,{worldSeed:0x51a7f00d,meta,state});
assert(reward.coefficient>=.8&&reward.coefficient<=1.2,'Board coefficient escaped its bound');
samples.push(reward.award);
const prices=[700,1400,1800,2400].map(base=>AppLogic.deterministicStorePrice(base,0x51a7f00d,seed%97,Math.floor(seed/97),1));
assert(prices.every(entry=>entry.coefficient===prices[0].coefficient),'Products in one store received different coefficients');
storeCoefficients.push(prices[0].coefficient);
}
const mean=values=>values.reduce((sum,value)=>sum+value,0)/values.length,
rewardMean=mean(samples),storeMean=mean(storeCoefficients);
assert(storeMean>.99&&storeMean<1.01,'Store coefficient distribution is materially biased');
assert(rewardMean>100&&rewardMean<230,'Reward rebalance left the measured progression band');
assert(700<1400&&1400<1800&&1800<2400,'Cosmetics/lens/field price ladder is invalid');
assert(1400/rewardMean>5&&1400/rewardMean<15,'Score lens is either trivial or effectively mandatory');
assert(2400/rewardMean<24,'MAX field is outside a meaningful attainable score-sink range');
assert(AppLogic.deterministicBoardReward(100,{meta:{id:'B1',seed:1,generatorVersion:5},state:{paths:[]},scoreLensCount:100}).lensMultiplier===1.5,'Repeated lenses create unbounded score inflation');
console.log(`Economy simulation passed: mean reward ${rewardMean.toFixed(2)}, mean coefficient ${storeMean.toFixed(4)}`);

View file

@ -0,0 +1,38 @@
'use strict';
const {vm,assert,functionSource}=require('./helpers/app-source');
const state={solved:true,expanded:false,expansionRetryRound:0,rev:0};
const meta={id:'B0',puzzle:null,x:0,y:0};
const chosen=[],attemptBases=[];
let revision=0,missing=true,hydrations=0;
const context={
metaState:()=>state,
hydrateMeta:async target=>{hydrations++;target.puzzle={}},
hydrateAdjacentMetas:async()=>{},
rebuildOccupancy:()=>{},
repairFacingGateConnections:()=>{},
unresolvedExpansionCandidates:()=>[{name:'north'},{name:'east'}],
placeChildAtFrontierAttempt:async(_meta,frontier,attemptBase)=>{
chosen.push(frontier.name);attemptBases.push(attemptBase);return null;
},
missingGateConnections:()=>missing?[1]:[],
nextRevision:()=>++revision,
markStateDirty:()=>{}
};
vm.createContext(context);
vm.runInContext(`${functionSource('repairMetaFrontierNow')}\nthis.repairMetaFrontierNow=repairMetaFrontierNow;`,context);
(async()=>{
await context.repairMetaFrontierNow(meta);
await context.repairMetaFrontierNow(meta);
await context.repairMetaFrontierNow(meta);
assert(chosen.join(',')==='north,east,north','Expansion repair stayed on one linear frontier instead of rotating fairly');
assert(hydrations===1,'Expansion repair skipped or repeatedly hydrated an off-screen compact board');
assert(new Set(attemptBases).size===3,'Expansion repair repeated a deterministic generation seed');
assert(state.expansionRetryRound===3&&!state.expanded,'Incomplete expansion repair did not persist its retry round');
missing=false;
await context.repairMetaFrontierNow(meta);
assert(state.expanded&&state.expansionRetryRound===0,'Completed expansion repair did not clear its retry state');
console.log('Fair expansion-frontier retry test passed');
})().catch(error=>{console.error(error);process.exitCode=1});

View file

@ -0,0 +1,91 @@
'use strict';
const {vm,assert,functionSource,loadBendPuzzle,loadAppLogic,starterPuzzle}=require('./helpers/app-source');
const BendPuzzle=loadBendPuzzle(),AppLogic=loadAppLogic();
const calls={refresh:0,render:0,hud:0,paint:0,save:0,sync:0};
const testWatchdog=setTimeout(()=>{console.error('Expansion integration exceeded 45 seconds');process.exit(1)},45000);testWatchdog.unref?.();
const context={console,BendPuzzle,AppLogic,GENERATOR_VERSION:5,UNIQUE_SOLUTION_MIN_LEVEL:6,deepClone:value=>structuredClone(value),structuredClone,hash32:BendPuzzle.hash32,rngFrom:BendPuzzle.rngFrom,shuffle:BendPuzzle.shuffle,generatedShapeKey:AppLogic.generatedShapeKey,generatedShapeFamilyKey:AppLogic.generatedShapeFamilyKey,balancedShapeCandidates:AppLogic.balancedShapeCandidates,growConnectedShape:AppLogic.growConnectedShape,macroDifficulty:BendPuzzle.macroDifficulty,difficultyFitsRegion:BendPuzzle.difficultyFitsRegion,solverDifficulty:BendPuzzle.solverDifficulty,SHAPES:BendPuzzle.SHAPES,H_PORT_PROFILES:BendPuzzle.H_PORT_PROFILES,V_PORT_PROFILES:BendPuzzle.V_PORT_PROFILES,horizontalBoundaryKey:BendPuzzle.horizontalBoundaryKey,verticalBoundaryKey:BendPuzzle.verticalBoundaryKey,key2:(x,y)=>`${x},${y}`,ckey:(r,c)=>`${r},${c}`,SIDE_D:{N:[-1,0],S:[1,0],W:[0,-1],E:[0,1]},OPP:{N:'S',S:'N',W:'E',E:'W'},data:{metas:{},states:{},nextId:1,specialMechanicsSeen:[]},occupancy:new Map(),closedVoidKeys:new Set(),adjacencyCache:new Map(),areaStoreEffects:()=>({forcedLevel:null,scoreLens:false}),targetLevelForNewBoard:(x,y)=>context.macroDifficulty(x,y),collectFieldEffectSources:()=>[],sleep:async()=>{},addObstaclePattern:p=>p,addSpecialCellPattern:p=>p,generatedPuzzleIssue:()=>null,generatePuzzleAsync:async(chunks,seed,level,x,y,timeout,options)=>BendPuzzle.generatePuzzle(chunks,seed,level,x,y,options),verifyPuzzleUniquenessAsync:async()=>({status:'unique',signature:'test',ruleVersion:2,nodes:1,quality:{score:80,accepted:true}}),mechanicTypesForPuzzle:()=>[],nextRevision:(()=>{let n=1;return()=>++n})(),markMetaDirty:()=>{},markStateDirty:()=>{},ensureBoards:()=>{},renderAll:()=>{calls.render++},updateHud:()=>{calls.hud++},nextPaint:async()=>{calls.paint++},save:async()=>{calls.save++;return true},hideStatus:()=>{},showStatus:()=>{},hydrateAdjacentMetas:async()=>{},repairFacingGateConnections:()=>0,syncBoundaryConnections:()=>{calls.sync++;return 0},puzzleOf:meta=>meta.puzzle,matchingNeighborGate:()=>null,metaAtGlobalCell:(r,c)=>{const id=context.occupancy.get(context.key2(Math.floor(c/5),Math.floor(r/5)));return id?context.data.metas[id]||null:null},globalCell:(meta,cell)=>[meta.y*5+cell[0],meta.x*5+cell[1]],sectionCountRange:AppLogic.sectionCountRange};
context.addMetaToOccupancy=meta=>{for(const[dx,dy]of meta.chunks)context.occupancy.set(context.key2(meta.x+dx,meta.y+dy),meta.id);context.adjacencyCache.clear();return true};
context.refreshWorldView=options=>{calls.refresh++;if(options?.syncConnections!==false)context.syncBoundaryConnections();context.renderAll();context.updateHud();if(options?.hide)context.hideStatus();return options?.persist?context.save(options.immediate):true};
context.rebuildOccupancy=()=>{context.occupancy=new Map();context.closedVoidKeys=new Set();for(const meta of Object.values(context.data.metas))for(const[dx,dy]of meta.chunks)context.occupancy.set(context.key2(meta.x+dx,meta.y+dy),meta.id);const candidates=new Set();for(const key of context.occupancy.keys()){const[x,y]=key.split(',').map(Number);for(const[dr,dc]of Object.values(context.SIDE_D))candidates.add(context.key2(x+dc,y+dr))}for(const key of candidates){const[x,y]=key.split(',').map(Number);if(!context.occupancy.has(key)&&[...Object.values(context.SIDE_D)].every(([dr,dc])=>context.occupancy.has(context.key2(x+dc,y+dr))))context.closedVoidKeys.add(key)}};
context.metaState=id=>context.data.states[id]||(context.data.states[id]={solved:false,expanded:false,paths:[],specialProgress:{crossings:[]},rev:0});
context.unsolvedBoardCount=()=>Object.keys(context.data.metas).filter(id=>!context.metaState(id).solved).length;
vm.createContext(context);
vm.runInContext(`
const SHAPES_BY_SIZE=new Map();for(const shape of SHAPES){const size=shape.length;if(!SHAPES_BY_SIZE.has(size))SHAPES_BY_SIZE.set(size,[]);SHAPES_BY_SIZE.get(size).push(shape)}
${['cellSet','gateAtCell','gateConnectionAllowed','matchingNeighborGate','shapeCandidatesForLevel','nearbyShapeFamilyCounts','shapeCandidatesForArea','occupiedNeighborCount','isClosedVoidUnit','fitsMeta','placedShapeKeys','unitOccupiedWithExtra','isClosedVoidWithExtra','openUnitFrontiers','shapeFitsWithExtra','viableFuturePlacementAt','viableExpansionExists','prospectiveShapeHasFrontier','placementPreservesUnsolvedFrontiers','frontierCandidates','gateFrontierCandidates','unresolvedExpansionCandidates','closedVoidRepairCandidates','missingGateConnections','placementConnectionRequirements','puzzleSupportsConnectionRequirements','sameNumberList','fixedPortProfilesForRequirements','installPreparedChild','placeChildAtFrontierAttempt','frontierGeometryStillViable','placeChildAtFrontier','expandMetaNow'].map(name=>functionSource(name)).join('\n')}
const boundedPlaceChildAtFrontier=async(source,frontier,attemptBase=0)=>{for(let cycle=0;cycle<3;cycle++){const made=await placeChildAtFrontierAttempt(source,frontier,attemptBase+cycle*2000003);if(made)return made}return null};
placeChildAtFrontier=boundedPlaceChildAtFrontier;
this.logic={gateFrontierCandidates,unresolvedExpansionCandidates,closedVoidRepairCandidates,missingGateConnections,placementConnectionRequirements,puzzleSupportsConnectionRequirements,openUnitFrontiers,viableExpansionExists,placementPreservesUnsolvedFrontiers,placeChildAtFrontierAttempt,placeChildAtFrontier,expandMetaNow};`,context);
const starter=starterPuzzle();
const origin={id:'B0',x:0,y:0,chunks:[[0,0]],seed:0x51a7f00d,puzzle:starter,sealedSides:[],rev:1};context.data.metas.B0=origin;context.data.states.B0={solved:true,expanded:false,paths:starter.solution,specialProgress:{crossings:[]},rev:1};context.rebuildOccupancy();
(async()=>{
const targets=context.logic.gateFrontierCandidates(origin);assert(targets.length===4,'Origin does not expose four procedural directions');
const started=Date.now(),made=await context.logic.expandMetaNow(origin),elapsed=Date.now()-started,remaining=context.logic.unresolvedExpansionCandidates(origin);
assert(made>=4,`Full expansion generated only ${made} fields`);assert(remaining.length===0,`Full expansion left ${remaining.length} frontiers`);assert(context.data.states.B0.expanded===true,'Origin was not marked fully expanded');
const metaAtUnit=(unitX,unitY)=>Object.values(context.data.metas).find(meta=>meta.chunks.some(([dx,dy])=>meta.x+dx===unitX&&meta.y+dy===unitY));
for(const gate of origin.puzzle.g){
const[dr,dc]=context.SIDE_D[gate[2]],globalRow=origin.y*5+gate[0],globalCol=origin.x*5+gate[1],neighborRow=globalRow+dr,neighborCol=globalCol+dc,
neighbor=metaAtUnit(Math.floor(neighborCol/5),Math.floor(neighborRow/5));
assert(neighbor&&neighbor.id!==origin.id,`Origin gate ${gate.join(',')} has no generated neighbor`);
const localRow=neighborRow-neighbor.y*5,localCol=neighborCol-neighbor.x*5,
matching=neighbor.puzzle.g.find(candidate=>candidate[0]===localRow&&candidate[1]===localCol&&candidate[2]===context.OPP[gate[2]]);
assert(matching,`Origin gate ${gate.join(',')} generated a board without the matching opposite gate`);
assert(!(neighbor.sealedSides||[]).includes(context.OPP[gate[2]]),`Matching gate for ${gate.join(',')} was sealed`);
}
assert(context.logic.missingGateConnections(origin).length===0,'Origin was marked expanded without an actual facing board for every gate');
for(const meta of Object.values(context.data.metas).filter(meta=>meta.id!=='B0')){const range=AppLogic.sectionCountRange(meta.level);assert(meta.chunks.length>=range.min&&meta.chunks.length<=range.max,`${meta.id}: level ${meta.level} has ${meta.chunks.length} sections outside ${range.min}-${range.max}`);assert(context.logic.viableExpansionExists(meta),`${meta.id}: newly generated board has no viable future expansion placement`)}
assert(elapsed<8000,`Origin expansion took ${elapsed} ms`);
assert(calls.refresh===1&&calls.render===1&&calls.hud===1&&calls.save===1&&calls.paint===1,`Expansion was not committed once: ${JSON.stringify(calls)}`);
const branch=Object.values(context.data.metas).find(meta=>meta.id!=='B0');context.data.states[branch.id].solved=true;context.data.states[branch.id].expanded=false;const beforeBranchIds=new Set(Object.keys(context.data.metas)),branchStarted=Date.now(),branchMade=await context.logic.expandMetaNow(branch),branchElapsed=Date.now()-branchStarted;
assert(branchMade>0,`${branch.id}: solved child did not generate a branch`);const branchRemaining=context.logic.gateFrontierCandidates(branch);assert(context.data.states[branch.id].expanded===(branchRemaining.length===0),`${branch.id}: expanded flag does not match remaining gate targets`);assert(Object.keys(context.data.metas).some(id=>!beforeBranchIds.has(id)),`${branch.id}: no adjacent field was added`);
for(const meta of Object.values(context.data.metas))if(!context.metaState(meta.id).solved)assert(context.logic.viableExpansionExists(meta),`${meta.id}: branch expansion stranded an unsolved board`);
assert(branchElapsed<15000,`${branch.id}: branch expansion took ${branchElapsed} ms`);
let chained=0;
for(let step=0;step<8;step++){
const candidates=Object.values(context.data.metas).filter(meta=>!context.metaState(meta.id).solved&&context.logic.viableExpansionExists(meta));
assert(candidates.length,`Expansion chain stopped at step ${step}`);let generated=0,candidate=null;
for(const option of candidates){context.metaState(option.id).solved=true;context.metaState(option.id).expanded=false;generated=await context.logic.expandMetaNow(option);candidate=option;if(generated>0)break}
assert(candidate,`Expansion chain had no candidate at step ${step}`);chained+=generated;if(context.metaState(candidate.id).expanded)assert(context.logic.missingGateConnections(candidate).length===0,`${candidate.id}: expanded despite a missing facing gate at step ${step}`);
for(const meta of Object.values(context.data.metas))if(!context.metaState(meta.id).solved)assert(context.logic.viableExpansionExists(meta),`${meta.id}: expansion chain created a non-expandable board`);
}
const source100=BendPuzzle.generatePuzzle([[0,0]],0x7a1100,6,100,100);context.data={metas:{S:{id:'S',x:100,y:100,chunks:[[0,0]],seed:0x7a1100,puzzle:source100,sealedSides:[],rev:1}},states:{S:{solved:true,expanded:false,paths:source100.solution,specialProgress:{crossings:[]},rev:1}},nextId:100};context.rebuildOccupancy();context.macroDifficulty=()=>6;
const child=await context.logic.placeChildAtFrontier(context.data.metas.S,{unitX:101,unitY:100,side:'E',targetSide:'W',contacts:[]},0);
assert(child,'Level-6 normal frontier could not generate a board');const range=AppLogic.sectionCountRange(child.targetLevel);assert(child.chunks.length>=range.min&&child.chunks.length<=range.max,'Higher-level board violates its authoritative regional section range');assert(child.chunks.length>1,'Higher-level multi-section generation still depends on a retired special mode');assert(!('labyrinth' in child)&&child.anomaly!=='giant','Retired special-board metadata was generated');assert(context.logic.viableExpansionExists(child),'Higher-level child was generated as a dead end');
const originalGenerate=context.generatePuzzleAsync;
vm.runInContext('this.originalShapeCandidatesForLevel=shapeCandidatesForLevel;this.originalShapes=SHAPES;shapeCandidatesForLevel=()=>[[[0,0],[1,0]]];SHAPES=[[[0,0],[1,0]]];',context);
const fallbackPuzzle=BendPuzzle.generatePuzzle([[0,0]],0x4187,3,300,300),fallbackSource={id:'F',x:300,y:300,chunks:[[0,0]],seed:0x4187,puzzle:fallbackPuzzle,sealedSides:[],rev:1};context.data={metas:{F:fallbackSource},states:{F:{solved:true,expanded:false,paths:fallbackPuzzle.solution,specialProgress:{crossings:[]},rev:1}},nextId:700};context.rebuildOccupancy();context.macroDifficulty=()=>6;context.generatePuzzleAsync=async(chunks,seed,level,x,y,timeout,options)=>{if(chunks.length>1)throw new Error('synthetic unsupported shape');return BendPuzzle.generatePuzzle(chunks,seed,level,x,y,options)};
const fallbackEast=fallbackPuzzle.g.findIndex(g=>g[2]==='E'),fallbackGate=fallbackPuzzle.g[fallbackEast],[fdr,fdc]=context.SIDE_D.E,fgr=fallbackSource.y*5+fallbackGate[0],fgc=fallbackSource.x*5+fallbackGate[1],fallbackChild=await context.logic.placeChildAtFrontierAttempt(fallbackSource,{unitX:Math.floor((fgc+fdc)/5),unitY:Math.floor((fgr+fdr)/5),side:'E',targetSide:'W',contacts:[{gateIndex:fallbackEast}],gateDriven:true},0);
assert(fallbackChild===null,'Invalid single-section fallback was forced into a level requiring multiple sections');assert(Object.keys(context.data.metas).length===1&&context.data.metas.F===fallbackSource,'Rejected fallback mutated the world');assert(context.data.nextId===700,'Rejected fallback consumed a board id');
context.generatePuzzleAsync=originalGenerate;vm.runInContext('shapeCandidatesForLevel=originalShapeCandidatesForLevel;SHAPES=originalShapes;',context);
const multiShape=[[0,0],[1,0]],multiPuzzle=BendPuzzle.generatePuzzle(multiShape,987654,3,12,-9),multiSource={id:'M',x:12,y:-9,chunks:multiShape,seed:987654,puzzle:multiPuzzle,sealedSides:['N','S','W'],rev:1};context.data={metas:{M:multiSource},states:{M:{solved:true,expanded:false,paths:multiPuzzle.solution,specialProgress:{crossings:[]},rev:1}},nextId:800};context.rebuildOccupancy();context.macroDifficulty=()=>3;
const multiMade=await context.logic.expandMetaNow(multiSource);assert(multiMade>0,'Clearing a multi-section board did not unlock any new board');assert(context.unsolvedBoardCount()>0,'Multi-section clear left no playable board');if(context.metaState('M').expanded)assert(context.logic.missingGateConnections(multiSource).length===0,'Multi-section board was marked expanded with missing gate neighbors');
const requirementPuzzle=BendPuzzle.generatePuzzle([[0,0]],77,1,50,50),requirementSource={id:'R',x:50,y:50,chunks:[[0,0]],seed:77,puzzle:requirementPuzzle,sealedSides:[],rev:1};context.data={metas:{R:requirementSource},states:{R:{solved:true,expanded:false,paths:requirementPuzzle.solution,specialProgress:{crossings:[]},rev:1}},nextId:500};context.rebuildOccupancy();
const eastGateIndex=requirementPuzzle.g.findIndex(g=>g[2]==='E'),eastGate=requirementPuzzle.g[eastGateIndex],[edr,edc]=context.SIDE_D.E,egr=requirementSource.y*5+eastGate[0],egc=requirementSource.x*5+eastGate[1],targetX=Math.floor((egc+edc)/5),targetY=Math.floor((egr+edr)/5),requirements=context.logic.placementConnectionRequirements(targetX,targetY,[[0,0]]),validChild=BendPuzzle.generatePuzzle([[0,0]],123,1,targetX,targetY),invalidChild=JSON.parse(JSON.stringify(validChild));invalidChild.g=invalidChild.g.filter(g=>!(g[0]===egr+edr-targetY*5&&g[1]===egc+edc-targetX*5&&g[2]==='W'));
assert(requirements.some(req=>req.metaId==='R'&&req.gateIndex===eastGateIndex),'Proposed placement did not capture the source gate requirement');assert(context.logic.puzzleSupportsConnectionRequirements(validChild,targetX,targetY,requirements),'A deterministic matching child was rejected');assert(!context.logic.puzzleSupportsConnectionRequirements(invalidChild,targetX,targetY,requirements),'A child without the facing gate was accepted');
const regenPuzzle=BendPuzzle.generatePuzzle([[0,0]],0x471100,1,600,600),regenSource={id:'Q',x:600,y:600,chunks:[[0,0]],seed:0x471100,puzzle:regenPuzzle,sealedSides:[],rev:1};context.data={metas:{Q:regenSource},states:{Q:{solved:true,expanded:false,paths:regenPuzzle.solution,specialProgress:{crossings:[]},rev:1}},nextId:900};context.rebuildOccupancy();context.macroDifficulty=()=>1;
const regenEast=regenPuzzle.g.findIndex(g=>g[2]==='E'),regenGate=regenPuzzle.g[regenEast],[rdr,rdc]=context.SIDE_D.E,rgr=regenSource.y*5+regenGate[0],rgc=regenSource.x*5+regenGate[1],regenOptions=[],regenSpecialSeeds=[];let integrityChecks=0;
const regenOriginalGenerate=context.generatePuzzleAsync,regenOriginalSpecial=context.addSpecialCellPattern,regenOriginalIssue=context.generatedPuzzleIssue;
context.generatePuzzleAsync=async(chunks,seed,level,x,y,timeout,options)=>{regenOptions.push(options||null);return BendPuzzle.generatePuzzle(chunks,seed,level,x,y,options)};context.addSpecialCellPattern=(puzzle,seed)=>{regenSpecialSeeds.push(seed);return puzzle};context.generatedPuzzleIssue=()=>integrityChecks++===0?'synthetic impossible board':null;
vm.runInContext('this.regenOriginalShapeCandidates=shapeCandidatesForLevel;shapeCandidatesForLevel=()=>[[[0,0]]];',context);
const regenerated=await context.logic.placeChildAtFrontier(regenSource,{unitX:Math.floor((rgc+rdc)/5),unitY:Math.floor((rgr+rdr)/5),side:'E',targetSide:'W',contacts:[{gateIndex:regenEast}],gateDriven:true},0);
assert(regenerated,'Whole-board regeneration did not recover after a rejected completed candidate');assert(regenOptions.length>=2&&regenSpecialSeeds.length>=2,'Rejected board did not rerun routing and special-cell generation');assert(regenOptions[0]?.portSeed==null&&Number.isInteger(regenOptions[1]?.portSeed),'Unconfirmed gate positions were not rerolled after rejection');assert(regenSpecialSeeds[0]!==regenSpecialSeeds[1],'Special-cell seed was not rerolled after rejection');
context.generatePuzzleAsync=regenOriginalGenerate;context.addSpecialCellPattern=regenOriginalSpecial;context.generatedPuzzleIssue=regenOriginalIssue;vm.runInContext('shapeCandidatesForLevel=regenOriginalShapeCandidates;',context);
context.data.metas.U={id:'U',x:200,y:200,chunks:[[0,0]],seed:9,puzzle:starter,sealedSides:[],rev:1};context.data.states.U={solved:false,expanded:false,paths:[],specialProgress:{crossings:[]},rev:1};context.data.metas.N={id:'N',x:199,y:200,chunks:[[0,0]],seed:10,puzzle:starter,sealedSides:[],rev:1};context.data.states.N={solved:true,expanded:true,paths:[],specialProgress:{crossings:[]},rev:1};context.data.metas.S2={id:'S2',x:200,y:201,chunks:[[0,0]],seed:11,puzzle:starter,sealedSides:[],rev:1};context.data.states.S2={solved:true,expanded:true,paths:[],specialProgress:{crossings:[]},rev:1};context.data.metas.W2={id:'W2',x:200,y:199,chunks:[[0,0]],seed:12,puzzle:starter,sealedSides:[],rev:1};context.data.states.W2={solved:true,expanded:true,paths:[],specialProgress:{crossings:[]},rev:1};context.rebuildOccupancy();assert(context.logic.openUnitFrontiers(context.data.metas.U).length===1,'Synthetic board does not have exactly one remaining frontier');assert(!context.logic.placementPreservesUnsolvedFrontiers(201,200,[[0,0]]),"Placement was allowed to consume an unsolved board's final frontier");
const closedPuzzle=BendPuzzle.generatePuzzle([[0,0]],0x47c105ed,3,900,900),closedSource={id:'C',x:900,y:900,chunks:[[0,0]],seed:0x47c105ed,puzzle:closedPuzzle,sealedSides:[],rev:1},closedGateIndex=0,closedGate=closedPuzzle.g[closedGateIndex],[cdr,cdc]=context.SIDE_D[closedGate[2]],closedGlobalRow=closedSource.y*5+closedGate[0],closedGlobalCol=closedSource.x*5+closedGate[1],closedX=Math.floor((closedGlobalCol+cdc)/5),closedY=Math.floor((closedGlobalRow+cdr)/5);
context.data={metas:{C:closedSource},states:{C:{solved:true,expanded:false,paths:closedPuzzle.solution,specialProgress:{crossings:[]},rev:1}},nextId:1000,specialMechanicsSeen:[]};
let blockerIndex=0;for(const[dx,dy]of[[0,-1],[0,1],[-1,0],[1,0]]){const x=closedX+dx,y=closedY+dy;if(x===closedSource.x&&y===closedSource.y)continue;const id=`V${blockerIndex++}`;context.data.metas[id]={id,x,y,chunks:[[0,0]],seed:blockerIndex,puzzle:null,sealedSides:[],rev:1};context.data.states[id]={solved:true,expanded:true,paths:[],specialProgress:{crossings:[]},rev:1}}
context.rebuildOccupancy();context.macroDifficulty=()=>10;
const closedFrontier=context.logic.gateFrontierCandidates(closedSource).find(candidate=>candidate.unitX===closedX&&candidate.unitY===closedY);
assert(closedFrontier?.terminalFill,'A missing gate inside a one-section enclosed gap was still discarded');
const closedChild=await context.logic.placeChildAtFrontier(closedSource,closedFrontier,0);
assert(closedChild&&closedChild.chunks.length===1&&context.occupancy.get(context.key2(closedX,closedY))===closedChild.id,'The enclosed field gap was not repaired with a generated puzzle');
const ungatedX=1201,ungatedY=1200,ungatedSource={id:'G',x:1200,y:1200,chunks:[[0,0]],seed:0x47c105ee,puzzle:BendPuzzle.generatePuzzle([[0,0]],0x47c105ee,3,1200,1200),sealedSides:['E'],rev:1};
context.data={metas:{G:ungatedSource},states:{G:{solved:true,expanded:true,paths:ungatedSource.puzzle.solution,specialProgress:{crossings:[]},rev:1}},nextId:1100,specialMechanicsSeen:[]};
blockerIndex=0;for(const[dx,dy]of[[0,-1],[0,1],[-1,0],[1,0]]){const x=ungatedX+dx,y=ungatedY+dy;if(x===ungatedSource.x&&y===ungatedSource.y)continue;const id=`W${blockerIndex++}`;context.data.metas[id]={id,x,y,chunks:[[0,0]],seed:blockerIndex,puzzle:null,sealedSides:[],rev:1};context.data.states[id]={solved:true,expanded:true,paths:[],specialProgress:{crossings:[]},rev:1}}
context.rebuildOccupancy();context.macroDifficulty=()=>8;
const ungatedRepair=context.logic.closedVoidRepairCandidates().find(candidate=>candidate.frontier.unitX===ungatedX&&candidate.frontier.unitY===ungatedY);
assert(ungatedRepair?.frontier.closedVoidRepair&&!ungatedRepair.frontier.gateDriven,'A closed occupancy hole without an open gate was not detected');
const ungatedChild=await context.logic.placeChildAtFrontier(ungatedRepair.source,ungatedRepair.frontier,0);
assert(ungatedChild&&ungatedChild.chunks.length===1&&ungatedChild.sealedSides.includes('N')&&ungatedChild.sealedSides.includes('S')&&ungatedChild.sealedSides.includes('W')&&ungatedChild.sealedSides.includes('E'),`Ungated enclosed gap was not generated as a sealed puzzle: ${JSON.stringify(ungatedChild&&{chunks:ungatedChild.chunks,sealedSides:ungatedChild.sealedSides})}`);
console.log(`Expansion integration passed: ${made} origin fields in ${elapsed} ms; ${branchMade} branch fields; ${chained} chained fields; level ${child.level} normal board has ${child.chunks.length} sections`);
})().catch(error=>{console.error(error);process.exitCode=1}).finally(()=>clearTimeout(testWatchdog));

View file

@ -0,0 +1,32 @@
<!doctype html>
<meta charset="utf-8">
<title>Field persistence browser test</title>
<body data-ready="running"></body>
<script src="../field-persistence.js"></script>
<script>
(async()=>{
const api=globalThis.BendFieldPersistence,assert=(condition,message)=>{if(!condition)throw new Error(message)};
const chunks=[];let closed=false;
const writable=new WritableStream({write(chunk){chunks.push(new Uint8Array(chunk).slice())},close(){closed=true}});
const boards=[
{id:'B0',meta:{id:'B0',seed:1},state:{solved:true}},
{id:'B2',meta:{id:'B2',seed:2},state:{solved:false}}
];
const written=await api.writeArchive({
writable,
manifest:{saveSchema:31,gameplayVersion:3,worldGeneration:'test-world',appVersion:'test',generatorVersion:1,exportedAt:'2026-01-01T00:00:00.000Z',boardCount:boards.length,estimatedRawBytes:512,encoding:'ndjson',compression:'identity'},
globalState:{bonusEvents:{}},
boards,
onProgress(){}
});
assert(closed,'archive output did not close');assert(written.boardCount===2,'archive board count is wrong');
const file=new File(chunks,'roundtrip.bfsave',{type:'application/x-bend-field-save'}),seen=[];
const summary=await api.inspectArchive(file);assert(summary.boardCount===2&&summary.compression==='identity','archive inspection failed');
const read=await api.readArchive(file,{onBoard(record){seen.push(record.id)}});
assert(read.boardCount===2&&seen.join(',')==='B0,B2','archive round trip failed');
const bytes=new Uint8Array(await file.arrayBuffer()),text=new TextDecoder().decode(bytes),tampered=text.replace('"crc32":"','"crc32":"f');
let corruptRejected=false;try{await api.readArchive(new File([tampered],'corrupt.bfsave'))}catch(error){corruptRejected=error.code==='CHECKSUM_MISMATCH'}
assert(corruptRejected,'corrupt archive was accepted');
document.body.dataset.ready='pass';document.body.textContent=JSON.stringify({written,seen});
})().catch(error=>{document.body.dataset.ready='fail';document.body.textContent=error.stack||String(error)});
</script>

View file

@ -0,0 +1,74 @@
'use strict';
const path=require('path');
const vm=require('vm');
const {assert,root,read}=require('./helpers/app-source');
delete global.BendFieldPersistence;
require(path.join(root,'field-persistence.js'));
const persistence=global.BendFieldPersistence;
assert(persistence&&persistence.ARCHIVE_VERSION===2,'Archive persistence module did not initialize');
const manifest={
saveSchema:31,gameplayVersion:3,worldGeneration:'test-world',appVersion:'47.36',generatorVersion:1,
exportedAt:'2026-07-28T00:00:00.000Z',boardCount:2,estimatedRawBytes:4096,encoding:'ndjson',compression:'identity'
};
const globalState={gameplayVersion:3,bonusEvents:{},fieldEffects:[],specialMechanicsSeen:[],clockFloor:0,lastSolveAt:0,timeAttack:null,timeAttackCooldowns:{3:0,5:0,10:0},lastTimeAttack:null};
const boardRows=[
{id:'B0',meta:{id:'B0',x:0,y:0,chunks:[[0,0]],puzzle:{g:[[0,0,0],[0,1,2]],n:[],valid:[[0,0],[0,1]],solution:[]}},state:{paths:[],specialProgress:{crossings:[]},solved:false,expanded:false,scoreAwarded:0}},
{id:'B1',meta:{id:'B1',x:1,y:0,chunks:[[0,0]],puzzle:{g:[[0,0,0],[0,1,2]],n:[],valid:[[0,0],[0,1]],solution:[]}},state:{paths:[],specialProgress:{crossings:[]},solved:false,expanded:false,scoreAwarded:0}}
];
async function* boards(){for(const row of boardRows)yield structuredClone(row)}
async function encode(compression,module=persistence){
const chunks=[];
const writable=new WritableStream({write(chunk){chunks.push((chunk instanceof Uint8Array?chunk:new Uint8Array(chunk)).slice())}});
const result=await module.writeArchive({writable,manifest:{...manifest,compression},globalState:structuredClone(globalState),boards:boards()});
return{result,blob:new Blob(chunks,{type:'application/x-bend-field-save'}),bytes:Buffer.concat(chunks.map(chunk=>Buffer.from(chunk)))};
}
async function decode(blob,module=persistence){
const seen=[];
const result=await module.readArchive(blob,{onBoard:record=>seen.push(record)});
return{result,seen};
}
(async()=>{
for(const compression of ['identity','gzip']){
if(compression==='gzip'&&typeof CompressionStream!=='function')continue;
const first=await encode(compression),second=await encode(compression),decoded=await decode(first.blob);
assert(first.result.boardCount===2&&decoded.result.boardCount===2&&decoded.seen.map(row=>row.id).join(',')==='B0,B1',`${compression} archive did not round-trip`);
assert(first.result.crc32===decoded.result.footer.crc32,`${compression} archive checksum changed during round-trip`);
assert(first.bytes.equals(second.bytes),`${compression} archive output is not deterministic for identical records`);
}
const identity=await encode('identity'),text=identity.bytes.toString('utf8'),modified=text.replace('"x":1','"x":2');
assert(modified!==text,'Archive corruption fixture did not modify a board record');
let checksumRejected=false;
try{await decode(new Blob([modified]))}catch(error){checksumRejected=error?.code==='CHECKSUM_MISMATCH'}
assert(checksumRejected,'Archive checksum corruption was not rejected');
let truncationRejected=false;
try{await decode(new Blob([text.split('\n').slice(0,-2).join('\n')+'\n']))}catch(error){truncationRejected=/footer|incomplete/i.test(error?.message||'')}
assert(truncationRejected,'Truncated archive was not rejected');
let manifestRejected=false;
try{await persistence.inspectArchive(new Blob([text.replace('\"encoding\":\"ndjson\"','\"encoding\":\"json\"')]))}catch(error){manifestRejected=error?.code==='UNSUPPORTED_ARCHIVE'}
assert(manifestRejected,'Unsupported archive encoding was not rejected at inspection time');
const lines=text.trimEnd().split('\n');
const expectArchiveFailure=async(candidate,pattern,label)=>{let rejected=false;try{await decode(new Blob([candidate]))}catch(error){rejected=pattern.test(`${error?.code||''} ${error?.message||''}`)}assert(rejected,label)};
const reordered=[...lines];[reordered[2],reordered[3]]=[reordered[3],reordered[2]];await expectArchiveFailure(`${reordered.join('\n')}\n`,/order|B0/i,'Reordered archive boards were accepted');
const duplicate=[...lines];duplicate[3]=duplicate[2];await expectArchiveFailure(`${duplicate.join('\n')}\n`,/duplicated|order/i,'Duplicate archive board IDs were accepted');
const unknown=[...lines],unknownRecord=JSON.parse(unknown[3]);unknownRecord.type='mystery';unknown[3]=JSON.stringify(unknownRecord);await expectArchiveFailure(`${unknown.join('\n')}\n`,/unknown|misplaced/i,'Unknown archive records were accepted');
const wrongFooter=[...lines],footer=JSON.parse(wrongFooter[4]);footer.rawBytes++;wrongFooter[4]=JSON.stringify(footer);await expectArchiveFailure(`${wrongFooter.join('\n')}\n`,/CHECKSUM|integrity/i,'Incorrect footer byte counts were accepted');
const wrongCount=[...lines],declared=JSON.parse(wrongCount[0]);declared.boardCount=1;wrongCount[0]=JSON.stringify(declared);await expectArchiveFailure(`${wrongCount.join('\n')}\n`,/more boards|count/i,'Incorrect manifest board counts were accepted');
const aborted=new AbortController();aborted.abort();let canceled=false;try{await persistence.readArchive(identity.blob,{signal:aborted.signal})}catch(error){canceled=error?.name==='AbortError'}assert(canceled,'Archive cancellation was not acknowledged');
let writeFailed=false;try{await persistence.writeArchive({writable:new WritableStream({write(){throw new Error('injected destination failure')}}),manifest:{...manifest},globalState:structuredClone(globalState),boards:boards()})}catch(_){writeFailed=true}assert(writeFailed,'Destination write failure did not abort export');
if(typeof CompressionStream==='function'&&typeof DecompressionStream==='function'){const compressed=await encode('gzip'),damaged=Buffer.from(compressed.bytes);damaged[Math.max(10,Math.floor(damaged.length/2))]^=0xff;let gzipRejected=false;try{await decode(new Blob([damaged]))}catch(_){gzipRejected=true}assert(gzipRejected,'Corrupt gzip archive was accepted')}
const worker=read('field-persistence-worker.js'),moduleSource=read('field-persistence.js');
class FakeWorker{
constructor(){this.onmessage=null;this.onerror=null;const owner=this,context={TextEncoder,Uint8Array,Uint32Array,JSON,Error,self:{postMessage(data){queueMicrotask(()=>owner.onmessage?.({data}))}}};vm.createContext(context);vm.runInContext(worker,context);this.workerSelf=context.self}
postMessage(data){queueMicrotask(()=>{try{this.workerSelf.onmessage({data:structuredClone(data)})}catch(error){this.onerror?.({message:error.message})}})}
terminate(){}
}
global.Worker=FakeWorker;delete global.BendFieldPersistence;delete require.cache[require.resolve(path.join(root,'field-persistence.js'))];require(path.join(root,'field-persistence.js'));const workerPersistence=global.BendFieldPersistence;
const workerEncoded=await encode('identity',workerPersistence),workerDecoded=await decode(workerEncoded.blob,workerPersistence);
assert(workerEncoded.result.worker===true&&workerDecoded.result.worker===true&&workerDecoded.seen.length===2,'Archive worker execution path did not round-trip records');
delete global.Worker;global.BendFieldPersistence=persistence;
assert(worker.includes('MAX_CHUNK_BYTES=1024*1024')&&worker.includes('postMessage({id,chunks,rawBytes,crc32:crc32Hex()},chunks)'),'Archive worker does not enforce transferable 1 MiB output chunks');
assert(moduleSource.includes("new Worker('field-persistence-worker.js?v=47.77')")&&moduleSource.includes('item.byteLength>MAX_ARCHIVE_LINE_BYTES')&&moduleSource.includes('WORKER_TARGET_BYTES=1024*1024')&&moduleSource.includes('pendingLineBytes'),'Archive worker path, 1 MiB batching, or worker line limit is missing');
console.log('Field archive round-trip and corruption checks passed');
})().catch(error=>{console.error(error);process.exitCode=1});

View file

@ -0,0 +1,38 @@
'use strict';
const {assert,functionSource,app,read}=require('./helpers/app-source');
const openDb=functionSource('openWorldDb');
const loadV2=functionSource('loadV2SnapshotFromDb');
const persistence=functionSource('persistDirtyToDb');
const stageArchive=functionSource('stageArchiveV2');
const stageRecovery=functionSource('stageRecoverySnapshotV2');
const activation=functionSource('activateReadyWorldV2');
const reset=functionSource('clearDatabaseWorld');
const verify=functionSource('verifyActiveWorldActivation');
const readiness=functionSource('validateActiveWorldReadiness');
const validation=functionSource('validateStagedWorldV2');
const collect=functionSource('collectV2Garbage');
const batchDelete=functionSource('deleteV2EpochBatch');
const persistNow=functionSource('persistNow');
const init=functionSource('init');
const importSave=functionSource('importSaveFile');
const activateStaged=functionSource('activateStagedWorldV2');
const postflight=functionSource('verifyStagedStorageEstimate');
assert(openDb.includes('activeFormat:FIELD_STORAGE_FORMAT')&&openDb.includes("source:{kind:'fresh'}")&&!openDb.includes('activeFormat:1'),'Fresh storage does not start directly in the current V2 format');
assert(openDb.includes("for(const retired of['metas','states','global','recovery','tombstones','outbox'])")&&openDb.includes('db.deleteObjectStore(retired)'),'Retired object stores are not deleted during the current-only upgrade');
assert(!persistence.includes("objectStore('metas')")&&!persistence.includes("objectStore('states')")&&!persistence.includes("objectStore('outbox')")&&!persistence.includes("objectStore('recovery')")&&!persistence.includes("objectStore('tombstones')"),'Persistence still dual-writes the retired storage format');
assert(!init.includes('migrateLoadedWorldToV2')&&!importSave.includes('migrateLoadedWorldToV2'),'Runtime still invokes the retired storage migration');
assert(!app.includes('legacyCost')&&!app.includes('migrateScoreAmount')&&!app.includes('loadedFromLegacy')&&!app.includes("kind:'migration'")&&!app.includes('full-resync'),'Retired score, startup, or cloud migration compatibility remains');
assert(functionSource('assertCurrentArchive').includes('manifest?.saveSchema!==SAVE_SCHEMA')&&functionSource('assertCurrentArchive').includes('manifest?.worldGeneration!==WORLD_GENERATION'),'Archive import does not require the exact current format');
assert(!app.includes('archiveMigrationRegistry')&&!app.includes('PREVIOUS_WORLD_GENERATION')&&!importSave.includes('looksJson')&&!importSave.includes('.json'),'Old-generation or legacy JSON import compatibility remains');
assert(stageArchive.includes('validateStagedWorldV2')&&stageArchive.includes('verifyStagedStorageEstimate')&&stageArchive.includes('archive global record is not canonical')&&stageArchive.includes("status:'ready'")&&stageRecovery.includes('validateStagedWorldV2')&&stageRecovery.includes('verifyStagedStorageEstimate')&&stageRecovery.includes("status:'ready'"),'Current archive/recovery staging skips canonical, database, or storage validation');
assert(validation.includes('boardIndexMatchesDetails')&&validation.includes('count(IDBKeyRange.only(epoch))')&&validation.includes('global aggregates')&&validation.includes('originOwnedByB0'),'Staged validation does not verify detail revisions, row counts, summaries, aggregates, and B0 origin ownership');
assert(activation.includes("olderPrevious.status='garbage'")&&activation.includes("currentWorld.status='rollback'")&&activation.includes('activationVerified:false')&&reset.includes("activateReadyWorldV2({epoch:newEpoch,expected,world},{kind:'reset'})")&&reset.includes('deleteV2Epoch(newEpoch)')&&activateStaged.includes('deleteV2Epoch(staged?.epoch)'),'Import and reset do not share the atomic ready-world activation primitive or clean failed staging epochs');
assert(readiness.includes("indexes[0].id!=='B0'")&&readiness.includes('B0 revision check')&&readiness.includes('meta.x+dx===0')&&init.includes('rollbackUnverifiedWorld'),'Startup does not validate B0 origin ownership and roll back an unverified activation');
assert(persistNow.includes('result.count>0')&&persistNow.includes('verifyActiveWorldActivation')&&verify.includes("previous.status='garbage'"),'Previous epoch is not retained until the first durable board checkpoint');
assert(batchDelete.includes('getAllKeys(epochKeyRange(epoch),limit)')&&batchDelete.includes("phase:'cleanup'")&&app.includes('GC_BATCH_ROWS=500'),'Epoch garbage collection is not bounded and resumable');
assert(collect.includes('control?.previousEpoch')&&collect.includes("startsWith('pin:')")&&collect.includes("world.status==='ready'")&&collect.includes('cleanupTemporaryExports'),'Garbage collection does not protect rollback/recovery epochs or clean stale ready/export artifacts');
assert(postflight.includes('QUOTA_POSTFLIGHT'),'Import postflight does not recheck browser storage headroom');
assert(app.includes('cameraAnchor')&&app.includes('selectedBoardId')&&init.includes('restoreSavedCamera')&&loadV2.includes('selectedIndex'),'Saved viewport and selected-board startup hints are not restored before the full index scan');
assert(persistence.includes('v2Active')&&read('test/browser-field-storage-benchmark.js').includes("'10000,100000,200000'"),'Current V2 persistence or large-field benchmark profiles are missing');
console.log('Current-only field save/load V2 integration guards passed');

View file

@ -0,0 +1,143 @@
'use strict';
const {vm,app,html,css,worker,assert,functionSource,loadBendPuzzle,loadAppLogic}=require('./helpers/app-source');
const AppLogic=loadAppLogic(),BendPuzzle=loadBendPuzzle();
// 1. Combo/undo removal and legacy omission.
const retiredFieldCleanup=functionSource('mergeGlobalRecords'),activeGameplaySource=app.replace(retiredFieldCleanup,'');
assert(!/\bcombo\b/i.test(activeGameplaySource)&&!/\bundo\b/i.test(activeGameplaySource)&&!/\b(combo|undo)\b/i.test(html),'Combo or visible undo remains in active gameplay/UI source');
assert(/\bcombo\b/.test(retiredFieldCleanup)&&/\bundo\b/.test(retiredFieldCleanup),'Legacy combo/undo fields are not explicitly removed during synchronization');
assert(!functionSource('globalForStorage').includes('combo')&&!functionSource('globalForStorage').includes('undo'),'Retired fields are still written');
// 2. Seeded score/store economy invariants.
for(let seed=0;seed<500;seed++){
const coefficient=AppLogic.deterministicStorePrice(1000,seed,4,-7,1).coefficient;
assert(coefficient>=.8&&coefficient<=1.2,'Seeded coefficient escaped 0.8-1.2');
const first=AppLogic.deterministicStorePrice(1000,seed,4,-7,1),second=AppLogic.deterministicStorePrice(1000,seed,4,-7,1);
assert(JSON.stringify(first)===JSON.stringify(second),'Store price rerolled for one location');
}
const lensReward=count=>AppLogic.deterministicBoardReward(100,{meta:{id:'B1',seed:1,generatorVersion:5},state:{paths:[]},scoreLensCount:count}).lensMultiplier;
assert(lensReward(0)===1&&lensReward(1)===1.25&&lensReward(2)===1.4&&lensReward(99)===1.5,'Score-lens diminishing cap is invalid');
assert(functionSource('rewardDetailsForBoard').includes('timeAttackRewardModifier()')&&functionSource('recordTimeAttackScore').includes('baseCollected')&&functionSource('finishTimeAttack').includes('if(result.bonus>0)'),'Time-attack reward modifier is deferred or double-applied');
// 3. Difficulty-field feature removal and lightweight detailed-board rendering.
assert(typeof AppLogic.winningLevelField==='undefined'&&typeof AppLogic.visibleFieldUnits==='undefined','Retired difficulty-field helpers remain exported');
assert(!app.includes('level-min-10')&&!app.includes('level-max-10')&&!app.includes('fieldOverlayCanvas')&&!html.includes('fieldPreviewCanvas'),'Difficulty adjustment items or overlays remain active');
assert(functionSource('makeBoard').includes('boardCellsPath')&&!functionSource('makeBoard').includes('cellHits')&&functionSource('desiredInteractiveBoardIds').includes('for(const id of ids)'),'Visible detailed boards are missing or still allocate per-cell hit nodes');
// 4-6. Center snapping without sticky hysteresis/speculative turns, and silent invalid motion.
for(const marker of ['rawPointerPosition','currentConfirmedCell','candidateDirection','currentCandidateCell','renderedHandlePosition'])assert(functionSource('drawingForPath').includes(marker),`Pointer state is missing ${marker}`);
assert(app.includes('POINTER_SNAP_THRESHOLD=CELL*.45')&&!app.includes('POINTER_SNAP_RELEASE')&&!app.includes('hysteresisDragPoint'),'Single-threshold pointer snapping or hysteresis removal is incomplete');
assert(app.includes('POINTER_DOMINANT_RATIO=1.25')&&!app.includes('POINTER_BUFFER_MS')&&!app.includes('consumeBufferedPointerTurn'),'Speculative one-step turn extension remains active');
assert(functionSource('pointerEventSamples').includes("pointerType!=='mouse'")&&functionSource('processBoardDragFrame').includes('const move=b.pendingPointerMove'),'Ordered pointer samples are not preserved through a drag frame');
assert(!functionSource('extendOne').includes('toast(')&&functionSource('extendOne').includes("if(lock&&!pathHasLockKey(path,lock))return false"),'Invalid direction still produces error feedback');
assert(functionSource('openTipMergePlan').includes('tipDistance!==0')&&!app.includes('function pickupJoinStepsAtPoint'),'Adjacent path pickups can still auto-connect');
assert(functionSource('openTipMergePlan').includes('seen.has(key)')&&functionSource('joinTips').includes('cancelBoardDragFrame(b)')&&functionSource('joinTips').includes('safeRelease(b.svg,pointerId)'),'Pickup merging can duplicate an overlapping route or retain pointer work after joining');
// 7. Level schedule and introduction memory.
for(let level=1;level<=4;level++)assert(AppLogic.specialSchedule(level,100,level).types.length===0,`Level ${level} scheduled a special`);
for(const level of[5,6]){const schedule=AppLogic.specialSchedule(level,100,level,['warp','lock','crossing'],[]);assert(schedule.types.length===1&&schedule.setCount===1,`Level ${level} did not schedule exactly one minimum type`)}
const introduction=AppLogic.specialSchedule(9,200,77,['warp'],[]);
assert(introduction.types.length===1&&introduction.setCount===1&&introduction.introduction,'First encounter is not isolated at minimum density');
assert(functionSource('addSpecialCellPattern').includes('AppLogic.specialSchedule')&&!app.includes('SPECIAL_CELL_PRODUCTION_CHANCE'),'Legacy independent special chance remains active');
assert(functionSource('generatedPuzzleIssue').includes("crossingCoverage.has(key)?2:1"),'Generated crossing boards are rejected by single-owner coverage validation');
const plain={
valid:[[0,0],[0,1]],g:[[0,0,'W'],[0,1,'E']],n:[[0,0,0]],obstacles:[],
specialCells:{crossings:[],warps:[],locks:[]},solution:[{startGate:0,endGate:1,cells:[[0,0],[0,1]]}]
};
const multiple={
valid:[[0,0],[0,1],[1,0],[1,1]],g:[[0,0,'S'],[0,1,'S'],[1,0,'N'],[1,1,'N']],n:[[0,0,2],[1,1,2]],obstacles:[],
specialCells:{crossings:[],warps:[],locks:[]},solution:[{startGate:0,endGate:1,cells:[[0,0],[0,1]]},{startGate:2,endGate:3,cells:[[1,0],[1,1]]}]
};
const crossing={
valid:[[1,0],[1,1],[1,2],[0,1],[2,1]],g:[[1,0,'W'],[1,2,'E'],[0,1,'N'],[2,1,'S']],n:[[1,0,0],[0,1,0]],obstacles:[],
specialCells:{crossings:[[1,1]],warps:[],locks:[]},solution:[{startGate:0,endGate:1,cells:[[1,0],[1,1],[1,2]]},{startGate:2,endGate:3,cells:[[0,1],[1,1],[2,1]]}]
};
const warp={
valid:[[0,0],[0,2]],g:[[0,0,'W'],[0,2,'E']],n:[[0,0,0]],obstacles:[],
specialCells:{crossings:[],warps:[{a:[0,0],b:[0,2]}],locks:[]},solution:[{startGate:0,endGate:1,cells:[[0,0],[0,2]]}]
};
const lock={
...multiple,
specialCells:{crossings:[],warps:[],locks:[{key:[0,0],door:[0,1]}]}
};
// 8. Worker-oriented uniqueness outcomes and deterministic fixtures.
for(const fixture of[plain,crossing,warp,lock]){
const first=BendPuzzle.verifyPuzzleUniqueness(fixture,{maxMs:1000,nodeCap:10000}),second=BendPuzzle.verifyPuzzleUniqueness(fixture,{maxMs:1000,nodeCap:10000});
assert(first.status==='unique',`Expected unique ${JSON.stringify(first)}`);
assert(first.status===second.status&&first.signature===second.signature,'Uniqueness result is not stable');
}
assert(BendPuzzle.verifyPuzzleUniqueness(multiple,{maxMs:1000,nodeCap:10000}).status==='multiple','Known multi-solution fixture was accepted');
assert(BendPuzzle.verifyPuzzleUniqueness({...plain,n:[[0,0,1]]},{maxMs:1000,nodeCap:10000}).status==='unsolved','Known unsolved fixture was not reported');
assert(BendPuzzle.verifyPuzzleUniqueness(multiple,{maxMs:1000,nodeCap:1}).status==='timeout','Node-cap timeout was not reported');
assert(worker.includes("action === 'verify'")&&functionSource('verifyPuzzleUniquenessAsync').includes("action:'verify'")&&!functionSource('verifyPuzzleUniquenessAsync').includes('verifyPuzzleUniqueness('),'Uniqueness is not worker-isolated');
// 9. Session suggestion.
assert(functionSource('scheduleTimeAttackSuggestionAfterCompletion').includes('sessionCompletionCount!==10')&&functionSource('tryShowTimeAttackSuggestion').includes('7000')&&html.includes('id="timeAttackSuggestion" role="status" aria-live="polite"'),'Completion-10 suggestion timing/live region is missing');
// 10. Minimap teleport and controls.
assert(!html.includes('id="teleportBtn"')&&html.includes('id="minimapOriginBtn"')&&html.includes('id="minimapRandomBtn"')&&!html.includes('id="minimapUnsolvedBtn"')&&!html.includes('id="minimapCurrentBtn"'),'Minimap teleport controls do not match origin/random');
assert(functionSource('centerRandomBoard').includes('getRandomValues')&&!app.includes('centerRandomUnsolved'),'Random minimap teleport still cycles through unsolved boards');
assert(functionSource('beginMinimapPointer').includes('centerWorldUnit')&&functionSource('moveMinimapPointer').includes('centerWorldUnit')&&html.includes('id="minimapCanvas" width="210" height="132" role="application" tabindex="0"'),'Direct/keyboard minimap interaction is missing');
assert(functionSource('centerWorldUnit').includes('worldNavigationBounds'),'Minimap teleport is not clamped to world navigation bounds');
assert(functionSource('inWorldOverview').includes('cam.scale<=OVERVIEW_ZOOM_THRESHOLD')&&!app.includes('OVERVIEW_HYSTERESIS')&&css.includes('.static-summary-fill{fill:transparent!important'),'Zoomed board summaries retain sticky green/gray squares');
assert(!functionSource('drawWorldOverview').includes('drawWorldOverviewPaths')&&functionSource('rebuildWorldOverviewCache').includes('drawMapLongLines'),'Far zoom does not use the minimap long-line renderer');
assert(functionSource('makeStaticBoard').includes('renderedConnectedLineWidth(meta,index)')&&!functionSource('makeStaticBoard').includes('state.solved')&&css.includes('.static-summary-path{')&&css.includes('vector-effect:non-scaling-stroke;opacity:.9'),'Line thickness disappears from an unsolved zoomed-in board');
assert(functionSource('rebuildWorldOverviewCache').includes('drawMapBoardCells')&&functionSource('rebuildWorldOverviewCache').includes('drawMapLongLines')&&!functionSource('rebuildWorldOverviewCache').includes('storeCellForMeta'),'Zoomed-out view no longer matches the minimap renderer');
assert(functionSource('leftFieldPanAllowed').includes("classList?.contains('solved')")&&functionSource('beginPan').includes('leftFieldPanAllowed(e)'),'Solved/undiscovered left-drag field panning is missing');
const priceContext={MIN_STORE_ITEM_PRICE:3000,storePriceDetails:()=>({coefficient:.8})};
vm.createContext(priceContext);
vm.runInContext(`${functionSource('storeItemPrice')}\nthis.storeItemPrice=storeItemPrice;`,priceContext);
assert(priceContext.storeItemPrice({},null,{cost:2500})===3000&&priceContext.storeItemPrice({},null,{cost:10000})===8000,'Store prices do not enforce a 3,000 minimum while retaining higher price variation');
const cursorItems=Array.from({length:12},(_,index)=>({id:`C${index}`,cursorStyle:`face-${index}`})),
otherItems=Array.from({length:1},(_,index)=>({id:`O${index}`})),allStoreItems=[...cursorItems,...otherItems],
shopContext={
CURSOR_ITEMS:cursorItems,STORE_ITEMS:allStoreItems,
hash32:BendPuzzle.hash32,rngFrom:BendPuzzle.rngFrom,shuffle:BendPuzzle.shuffle,
storeItem:id=>allStoreItems.find(item=>item.id===id)||null
};
vm.createContext(shopContext);
vm.runInContext(`${functionSource('normalizeStoreItemIds')}\n${functionSource('seededStoreItemIds')}\n${functionSource('storeInventoryItems')}\nthis.shop={normalizeStoreItemIds,seededStoreItemIds,storeInventoryItems};`,shopContext);
const seedA=shopContext.shop.seededStoreItemIds(123456),seedARepeat=shopContext.shop.seededStoreItemIds(123456),seedB=shopContext.shop.seededStoreItemIds(654321),
selected=seedA.map(shopContext.storeItem);
assert(JSON.stringify(seedA)===JSON.stringify(seedARepeat)&&JSON.stringify(seedA)!==JSON.stringify(seedB),'Store inventory is not deterministic per field seed');
assert(seedA.length===13&&new Set(seedA).size===13&&selected.filter(item=>item.cursorStyle).length===12&&selected.filter(item=>!item.cursorStyle).length===1,'Seeded store inventory is not exactly twelve cursors and one other item');
assert(JSON.stringify(shopContext.shop.storeInventoryItems({seed:9},{itemIds:seedA}).map(item=>item.id))===JSON.stringify(seedA),'Persisted store inventory IDs are not honored');
// 11-12. Noise/reduced motion and bounded caches/worker.
assert(html.includes('id="noiseCanvas" width="80" height="64"')&&functionSource('scheduleNoiseBackground').includes('noisePainted')&&!functionSource('scheduleNoiseBackground').includes('setTimeout')&&functionSource('paintNoiseBackground').includes("perfCount('noiseFrames')"),'Static low-cost noise background is missing');
assert(css.includes('#noiseCanvas{')&&css.includes('opacity:.06'),'Noise effective opacity exceeds the low-contrast budget');
assert(functionSource('evictHydratedBoardDetails').includes('hydratedBoardLru.size>32')&&functionSource('evictHydratedBoardDetails').includes('hydratedBoardBytes>8*1024*1024')&&worker.includes('uniquenessCache.size > 256'),'Hydration or uniqueness cache is unbounded/missing');
// 13-14. Contextual HUD and active-board isolation.
assert(css.includes('#topbar.drawing-active')&&css.includes(':focus-within')&&css.includes('transition:opacity'),'Contextual HUD fading constraints are missing');
assert(!css.includes('.board-card:not(.input-active) .static-layer')&&!css.includes('filter:saturate')&&!css.includes('backdrop-filter')&&!css.includes('drop-shadow'),'Non-selected boards are dimmed or global presentation still creates costly filter surfaces');
// 15. Skippable completion independent of persistence.
const solveSource=functionSource('checkSolvedAndExpand');
assert(solveSource.indexOf('completionEffect(immediateBoard,award)')<solveSource.indexOf('persistence=save(true)'),'Completion waits for persistence');
assert(solveSource.includes('preparation=prepareExpansionCandidate(b.meta)')&&solveSource.includes('expandMeta(durableMeta,prepared)'),'Expansion generation does not start with the clear display or does not install against durable metadata');
assert(solveSource.includes('playGemCollectionAnimation(immediateBoard,award)')&&functionSource('playGemCollectionAnimation').includes('gemCollectionSources')&&functionSource('playGemCollectionAnimation').includes('scoreCountEl'),'Clear rewards do not travel from the board to the gem wallet');
assert(functionSource('skipCompletionVisuals').includes('finishCompletionVisual')&&functionSource('finishCompletionVisual').includes('visual.resolve'),'Completion visual is not independently skippable');
assert(app.includes('MAX_FRONTIER_GENERATION_CYCLES=3')&&functionSource('placeChildAtFrontier').includes('cycle<MAX_FRONTIER_GENERATION_CYCLES'),'Expansion generation can enter an unbounded retry loop');
assert(functionSource('expandMetaNow').includes('expansionRetryRound')&&functionSource('repairMetaFrontierNow').includes('round%frontiers.length')&&functionSource('repairExpansions').includes('repairMetaFrontier(meta)'),'Expansion repair repeats one deterministic seed/order and can leave permanent gaps');
assert(!functionSource('repairExpansions').includes('&&meta.puzzle')&&!functionSource('pendingExpansionCount').includes('&&meta.puzzle'),'Off-screen compact solved boards are excluded from expansion repair');
// 16. Bounded hydration and memory use without hiding visible puzzle details.
assert(functionSource('desiredInteractiveBoardIds').includes('for(const id of ids)')&&!app.includes('function promoteStaticBoard('),'Visible puzzles can still be replaced by on-demand summaries');
assert(app.includes('HYDRATE_CONCURRENCY=4')&&functionSource('evictHydratedBoardDetails').includes('hydratedBoardLru.size>32')&&functionSource('evictHydratedBoardDetails').includes('hydratedBoardBytes>8*1024*1024')&&app.includes('BOARD_RENDERS_PER_FRAME=6'),'Hydration, cache, or render work is not bounded');
assert(functionSource('drawPresenceLayer').includes('dpr=1')&&functionSource('drawReactionLayer').includes('dpr=1'),'Fullscreen online canvases still allocate high-DPR backing stores');
assert(html.includes('id="fpsCounter"')&&functionSource('refreshFpsCounter').includes('FPS ${fpsLastValue}')&&app.includes('AUXILIARY_FPS=30')&&app.includes('DRAG_TARGET_FPS=60'),'FPS diagnostics or split interaction budgets are missing');
assert(functionSource('scheduleNoiseBackground').includes('noisePainted')&&!functionSource('scheduleNoiseBackground').includes('setTimeout'),'Noise background still repaints continuously');
// 17. Bounded interaction burden in candidate selection.
assert(AppLogic.interactionBurden(plain).score<=40&&functionSource('placeChildAtFrontierAttempt').includes('AppLogic.interactionBurden')&&functionSource('placeChildAtFrontierAttempt').includes('boundedBurdenPenalty'),'Interaction burden is unbounded or not integrated');
assert(BendPuzzle.UNIQUENESS_RULE_VERSION===2&&BendPuzzle.verifyPuzzleUniqueness(lock,{maxMs:1000,nodeCap:10000}).quality?.contributingSpecials>=1,'Solution quality does not measure special contribution');
// 18. Encounter migration and synchronization.
assert(functionSource('normalizeSnapshot').includes('mechanicTypesForPuzzle')&&functionSource('mergeGlobalRecords').includes('specialMechanicsSeen')&&functionSource('addSpecialCellPattern').includes('data.specialMechanicsSeen'),'Special encounter memory is not migrated, synchronized, and consumed');
console.log('Gameplay simplification regression groups 1-18 passed');

View file

@ -0,0 +1,46 @@
'use strict';
const fs=require('fs');
const path=require('path');
const vm=require('vm');
const root=path.resolve(__dirname,'../..');
const read=name=>fs.readFileSync(path.join(root,name),'utf8');
const app=read('app.js'),html=read('index.html'),css=read('style.css'),worker=read('puzzle-worker.js'),appLogicSource=read('app-logic.js');
function assert(value,message){if(!value)throw new Error(message)}
function functionSource(name,source=app){
let start=source.indexOf(`async function ${name}(`);if(start<0)start=source.indexOf(`function* ${name}(`);if(start<0)start=source.indexOf(`function ${name}(`);
assert(start>=0,`Missing ${name}`);
let paren=source.indexOf('(',start),parenDepth=0,body=-1;
for(let i=paren;i<source.length;i++){
if(source[i]==='(')parenDepth++;
else if(source[i]===')'&&--parenDepth===0){body=source.indexOf('{',i);break}
}
assert(body>=0,`Missing body for ${name}`);let depth=0;
for(let i=body;i<source.length;i++){
if(source[i]==='{')depth++;
else if(source[i]==='}'&&--depth===0)return source.slice(start,i+1);
}
throw new Error(`Unclosed ${name}`);
}
function createContext(context={}){vm.createContext(context);return context}
function runFunctions(names,context,expose=names){
createContext(context);
const sources=names.map(name=>functionSource(name)).join('\n');
vm.runInContext(`${sources}\nthis.logic={${expose.join(',')}};`,context);
return context.logic;
}
function loadBendPuzzle(){
delete global.BendPuzzle;
delete require.cache[require.resolve(path.join(root,'puzzle-core.js'))];
require(path.join(root,'puzzle-core.js'));
return global.BendPuzzle;
}
function loadAppLogic(){
delete require.cache[require.resolve(path.join(root,'app-logic.js'))];
return require(path.join(root,'app-logic.js'));
}
function starterPuzzle(){
const match=app.match(/const STARTER_PUZZLE=Object\.freeze\((\{.*\})\);\nconst SCORE_VERSION/s);
assert(match,'Bundled origin puzzle missing');
return JSON.parse(match[1]);
}
module.exports={root,read,app,html,css,worker,appLogicSource,assert,functionSource,createContext,runFunctions,loadBendPuzzle,loadAppLogic,starterPuzzle,vm,path,fs};

View file

@ -0,0 +1,52 @@
'use strict';
const assert=require('assert/strict');
async function connectRealtime(base,session,{timeout=4000}={}){
assert(session?.playerId&&session?.token,'Realtime session is required');
const url=base.replace(/^http/,'ws')+'/api/realtime';
const socket=new WebSocket(url);
const queue=[];
const waiters=[];
let closed=false;
function dispatch(message){
for(let index=0;index<waiters.length;index++){
const waiter=waiters[index];
if(waiter.predicate(message)){
waiters.splice(index,1);clearTimeout(waiter.timer);waiter.resolve(message);return;
}
}
queue.push(message);
}
socket.addEventListener('message',event=>{
try{dispatch(JSON.parse(String(event.data)))}catch(error){dispatch({type:'test-parse-error',error})}
});
socket.addEventListener('close',()=>{closed=true;for(const waiter of waiters.splice(0)){clearTimeout(waiter.timer);waiter.reject(new Error('Realtime socket closed'))}});
await new Promise((resolve,reject)=>{
const timer=setTimeout(()=>reject(new Error('Realtime socket open timed out')),timeout);
socket.addEventListener('open',()=>{clearTimeout(timer);resolve()},{once:true});
socket.addEventListener('error',()=>{clearTimeout(timer);reject(new Error('Realtime socket failed'))},{once:true});
});
socket.send(JSON.stringify({type:'hello',playerId:session.playerId,token:session.token}));
async function waitFor(predicateOrType,waitTimeout=timeout){
const predicate=typeof predicateOrType==='function'?predicateOrType:message=>message?.type===predicateOrType;
const found=queue.findIndex(predicate);
if(found>=0)return queue.splice(found,1)[0];
if(closed)throw new Error('Realtime socket is closed');
return new Promise((resolve,reject)=>{
const waiter={predicate,resolve,reject,timer:null};
waiter.timer=setTimeout(()=>{const index=waiters.indexOf(waiter);if(index>=0)waiters.splice(index,1);reject(new Error('Realtime message timed out'))},waitTimeout);
waiters.push(waiter);
});
}
const ready=await waitFor('ready');
return{
socket,ready,
send(message){socket.send(JSON.stringify(message))},
waitFor,
close(){try{socket.close()}catch(_){}},
queued:queue
};
}
module.exports={connectRealtime};

View file

@ -0,0 +1,285 @@
'use strict';
const {vm,assert,functionSource,loadAppLogic,app,css}=require('./helpers/app-source');
const AppLogic=loadAppLogic();
const context={PAD:16,CELL:40,ckey:(r,c)=>`${r},${c}`};
vm.createContext(context);
vm.runInContext(`${functionSource('cellsCrossedBySegment')}\n${functionSource('liveEndpointPoint')}\n${functionSource('hexRgb')}\n${functionSource('mixHex')}\n${functionSource('pathStrokePieces')}\nthis.logic={cellsCrossedBySegment,liveEndpointPoint,pathStrokePieces};`,context);
const {cellsCrossedBySegment,liveEndpointPoint,pathStrokePieces}=context.logic;
const horizontal=cellsCrossedBySegment([36,36],[76,76],{preferredAxis:'H',validCells:new Set(['0,0','0,1','1,1'])});
assert(JSON.stringify(horizontal)==='[[0,1],[1,1]]','Exact-corner drag did not follow the horizontal valid route');
const vertical=cellsCrossedBySegment([36,36],[76,76],{preferredAxis:'H',validCells:new Set(['0,0','1,0','1,1'])});
assert(JSON.stringify(vertical)==='[[1,0],[1,1]]','Exact-corner drag ignored the only valid route');
const board={drawing:{pointerId:7,pathIndex:2,lastPoint:[120,44],renderedHandlePosition:[80,44]}},tip=liveEndpointPoint(board,2,[80,44]);
assert(tip[0]===80&&tip[1]===44,'Live endpoint followed raw pointer noise instead of the confirmed cell center');
board.drawing.renderedHandlePosition=[120,44];
assert(liveEndpointPoint(board,2,[80,44])[0]===120,'Live endpoint did not show the buffered next-cell center');
assert(liveEndpointPoint(board,1,[80,44])[0]===80,'Live endpoint affected a non-active line');
const pieces=pathStrokePieces([[[0,0],[30,0],[30,40],[50,40]]],'#000000','#ffffff');
assert(pieces.length===1&&pieces[0].points.length===4,'A continuous bent route was split into excess SVG gradient nodes');
assert(Math.abs(pieces[0].startProgress)<1e-9&&Math.abs(pieces.at(-1).endProgress-1)<1e-9,'Route color blend does not span the full path');
const splitPieces=pathStrokePieces([[[0,0],[30,0]],[[70,0],[70,40],[90,40]]],'#000000','#ffffff');
assert(splitPieces.length===2,'A warp-separated route did not retain one gradient node per visible segment');
assert(splitPieces[0].endColor===splitPieces[1].startColor,'Route color blend has a seam between visible segments');
assert(splitPieces[0].endProgress<splitPieces[1].endProgress,'Route color blend progress is not monotonic');
console.log('Pointer traversal, live endpoint, and path-length color blending test passed');
const gestureContext={
CELL:40,POINTER_SNAP_THRESHOLD:18,POINTER_DOMINANT_RATIO:1.25,DRAG_EDGE_MARGIN:76,DRAG_EDGE_MAX_SPEED:.58,
ckey:(r,c)=>`${r},${c}`,activePath:b=>b.path,boardCellCenter:([r,c])=>[16+(c+.5)*40,16+(r+.5)*40],pathTailAxis:()=> 'H',
getViewportRect:()=>({left:0,top:0,right:1000,bottom:800,width:1000,height:800})
};
vm.createContext(gestureContext);
vm.runInContext(`${functionSource('directionFromDelta')}\n${functionSource('gateHitBox')}\n${functionSource('edgePanVelocity')}\nthis.gesture={directionFromDelta,gateHitBox,edgePanVelocity};`,gestureContext);
assert(gestureContext.gesture.directionFromDelta(17,0)===null&&gestureContext.gesture.directionFromDelta(19,0)==='E','Single snap threshold is not applied directly');
assert(gestureContext.gesture.directionFromDelta(19,18,'H')==='E'&&gestureContext.gesture.directionFromDelta(19,18,'V')==='S'&&gestureContext.gesture.directionFromDelta(0,-19)==='N','Diagonal drag does not select a stable orthogonal axis');
const eastHit=gestureContext.gesture.gateHitBox([200,120],'E'),westHit=gestureContext.gesture.gateHitBox([200,120],'W');
assert(eastHit.x<200&&eastHit.x+eastHit.width===200&&westHit.x===200&&westHit.x+westHit.width>200,'Gate hit boxes are not confined to the owning board');
assert(eastHit.width<=gestureContext.CELL*.5&&westHit.width<=gestureContext.CELL*.5,'Gate hit boxes extend too far into the puzzle');
const centerVelocity=gestureContext.gesture.edgePanVelocity(500,400),rightVelocity=gestureContext.gesture.edgePanVelocity(998,400);
assert(centerVelocity[0]===0&&centerVelocity[1]===0&&rightVelocity[0]>0,'Edge auto-pan velocity is not limited to the viewport edge');
console.log('Direct snap threshold, outward gate reach, and edge auto-pan test passed');
const sampleContext={perfNow:()=>100,DRAG_MAX_POINTER_SAMPLES:12};
vm.createContext(sampleContext);
vm.runInContext(`${functionSource('pointerEventSamples')}\n${functionSource('trimBoardPointerSamples')}\n${functionSource('setBoardPointerSample')}\n${functionSource('appendBoardPointerSamples')}\nthis.pointer={pointerEventSamples,appendBoardPointerSamples};`,sampleContext);
const cornerSamples=[
{clientX:220,clientY:280},{clientX:140,clientY:280},{clientX:140,clientY:196},
{clientX:290,clientY:196},{clientX:290,clientY:250}
],sampleBoard={pendingPointerMove:null};
sampleContext.pointer.appendBoardPointerSamples(sampleBoard,{pointerId:12,pointerType:'pen',getCoalescedEvents:()=>cornerSamples});
assert(JSON.stringify([sampleBoard.pendingPointerMove.clientX,sampleBoard.pendingPointerMove.clientY])===JSON.stringify([290,250])&&sampleBoard.pointerMoveSamples.length===1,'Pointer coalescing did not retain the newest sample in the bounded logic queue');
const threeTurnPath={startGate:0,endGate:null,cells:[[4,6],[4,5],[4,4],[4,3],[4,2],[4,1],[3,1],[2,1],[2,2],[2,3],[3,3]]};
assert(AppLogic.analyzePathTurns(threeTurnPath,[[4,6,'E']],[],false).count===3,'The pictured left-up-right-down pickup gesture does not display three turns');
assert(!functionSource('extendPointerTo').includes('consumeBufferedPointerTurn'),'Pickup drag still extends a speculative second cell');
assert(!functionSource('renderDragFrame').includes('motion')&&!functionSource('extendPointerTo').includes('Motion'),'Line extension still performs animation bookkeeping');
console.log('Bounded ordered-sample pickup gesture and immediate line rendering test passed');
const pickupState={paths:[{startGate:0,endGate:null,cells:[[0,0]]},{startGate:1,endGate:null,cells:[[0,1]]}]},pickupContext={
manhattan:(a,b)=>Math.abs(a[0]-b[0])+Math.abs(a[1]-b[1]),ckey:(r,c)=>`${r},${c}`,cellSet:p=>p.validSet,metaState:()=>pickupState
};
vm.createContext(pickupContext);
vm.runInContext(`${functionSource('openTipMergePlan')}\nthis.openTipMergePlan=openTipMergePlan;`,pickupContext);
const pickupBoard={id:'B0',p:{validSet:new Set(['0,0','0,1'])},drawing:{pathIndex:0}};
assert(pickupContext.openTipMergePlan(pickupBoard,0,1)===null,'Adjacent pickups connected without occupying the same cell');
assert(pickupContext.openTipMergePlan(pickupBoard,0,1,'end',true)?.merged.cells.length===2,'A knob explicitly moved into the other endpoint cell did not connect');
pickupContext.metaState=()=>({paths:[{startGate:0,endGate:null,cells:[[0,0]]},{startGate:1,endGate:null,cells:[[0,0]]}]});
const sameCellPickupBoard={id:'B0',p:{validSet:new Set(['0,0'])},drawing:{pathIndex:0}},sameCellMerge=pickupContext.openTipMergePlan(sameCellPickupBoard,0,1);
assert(sameCellMerge&&sameCellMerge.merged.cells.length===1,'Two pickups in the same cell did not merge without duplicating the cell');
const overlapState={paths:[
{startGate:0,endGate:null,cells:[[0,0],[1,0],[1,1]]},
{startGate:1,endGate:null,cells:[[0,2],[1,2],[1,1],[2,1]]}
]};
pickupContext.metaState=()=>overlapState;
const overlapBoard={id:'B0',p:{validSet:new Set(['0,0','1,0','1,1','0,2','1,2','2,1'])},drawing:{pathIndex:0}};
assert(pickupContext.openTipMergePlan(overlapBoard,0,1)===null,'Overlapping pickups were allowed to create a self-duplicated path');
const mergeState={paths:[
{startGate:0,endGate:null,cells:[[0,0]]},
{startGate:1,endGate:null,cells:[[0,0]]}
]};
let cancelledMergeFrames=0,releasedMergePointers=0;
const mergeContext={
metaState:()=>mergeState,manhattan:(a,b)=>Math.abs(a[0]-b[0])+Math.abs(a[1]-b[1]),ckey:(r,c)=>`${r},${c}`,cellSet:p=>p.validSet,
cancelBoardDragFrame:()=>cancelledMergeFrames++,
applyBoardCommand:(_board,mutate)=>mutate()===false?false:true,
safeRelease:()=>releasedMergePointers++,playSound:()=>{},queueMicrotask:callback=>callback(),reconcileDrawingPresentation:()=>{}
};
vm.createContext(mergeContext);
vm.runInContext(`${functionSource('openTipMergePlan')}\n${functionSource('joinTips')}\nthis.joinTips=joinTips;`,mergeContext);
const mergeBoard={id:'B0',p:{validSet:new Set(['0,0'])},drawing:{pathIndex:0,pointerId:17},svg:{},joiningPaths:false};
assert(mergeContext.joinTips(mergeBoard,0,1),'Same-cell pickup merge was rejected');
assert(mergeState.paths.length===1&&mergeState.paths[0].cells.length===1,'Pickup merge duplicated the shared cell');
assert(mergeBoard.drawing===null&&cancelledMergeFrames===1&&releasedMergePointers===1,'Pickup merge did not terminate its pending frame and pointer capture exactly once');
assert(!app.includes('function pickupJoinStepsAtPoint')&&!app.includes('function gateConnectionSteps'),'Removed adjacent pickup/gate auto-bridging code is still present');
let cameraCallback=null,cameraTimerCallback=null,cameraApplies=0;
const cameraContext={
CAMERA_DISPLAY_WATCHDOG_MS:18,DRAG_FRAME_INTERVAL:1000/60,
cam:{x:0,y:0,scale:1},cameraInteractionFrame:0,cameraInteractionDelayTimer:0,cameraInteractionLastDraw:0,pendingCameraInteraction:null,
requestAnimationFrame:callback=>{cameraCallback=callback;return 7},cancelAnimationFrame:()=>{},setTimeout:callback=>{cameraTimerCallback=callback;return 8},clearTimeout:()=>{},applyCamera:immediate=>{assert(immediate===true,'Queued camera update was not committed directly')},
perfNow:()=>100,perfStart:()=>0,perfEnd:()=>0,perfCount:()=>{},recordInteractionCommit:()=>{},observeInteractionFrame:()=>{},data:{cameraAnchor:null},currentCameraAnchor:()=>({centerX:0,centerY:0,scale:1}),markGlobalDirty:()=>{},
};
vm.createContext(cameraContext);
vm.runInContext(`${functionSource('commitCameraInteraction')}\n${functionSource('queueCameraInteraction')}\nthis.queueCameraInteraction=queueCameraInteraction;`,cameraContext);
cameraContext.applyCamera=()=>cameraApplies++;
cameraContext.queueCameraInteraction({x:10,y:20,scale:1});cameraContext.queueCameraInteraction({x:30,y:40,scale:1});
assert(cameraContext.cameraInteractionFrame===7&&cameraContext.cam.x===0,'Raw pan events were applied before the animation frame');
cameraCallback();
assert(cameraContext.cam.x===30&&cameraContext.cam.y===40&&cameraApplies===1,'Pan events were not coalesced to the latest frame');
cameraTimerCallback();
assert(cameraApplies===1,'Camera watchdog duplicated a completed animation-frame commit');
console.log('Pickup capture and frame-coalesced panning test passed');
const blankTarget={closest:()=>null};
const card=(solved=false)=>({dataset:{id:'B0'},classList:{contains:name=>name==='solved'&&solved}});
const targetForCard=board=>({closest:selector=>selector==='.board-card'?board:null});
const panContext={pendingFieldItemPlacement:null,data:{metas:{B0:{id:'B0'}}},metaState:()=>({solved:false})};
vm.createContext(panContext);
vm.runInContext(`${functionSource('leftFieldPanAllowed')}\nthis.leftFieldPanAllowed=leftFieldPanAllowed;`,panContext);
assert(panContext.leftFieldPanAllowed({button:0,target:blankTarget}),'Blank undiscovered field did not allow left-drag panning');
assert(!panContext.leftFieldPanAllowed({button:0,target:targetForCard(card(false))}),'Unsolved puzzle area stole left-drag for field panning');
assert(panContext.leftFieldPanAllowed({button:0,target:targetForCard(card(true))}),'Solved puzzle area did not allow left-drag panning');
assert(!panContext.leftFieldPanAllowed({button:2,target:blankTarget}),'Left-pan eligibility accepted a non-left button');
console.log('Solved/undiscovered left-drag pan eligibility test passed');
const overviewContext={
metaState:id=>({solved:id==='S'}),currentLineGraphCaches:()=>({components:new Map(),geometries:new Map()}),
collectConnectedLineComponent:()=>[],minimapGeometryForComponent:()=>({segments:[]}),MINIMAP_LONG_LINE:4
};
vm.createContext(overviewContext);
vm.runInContext(`${functionSource('drawMapBoardCells')}\nthis.drawMapBoardCells=drawMapBoardCells;`,overviewContext);
let fills=0,strokes=0;const mapContext={beginPath:()=>{},rect:()=>{},fill:()=>{fills++},stroke:()=>{strokes++},set fillStyle(_value){}};
overviewContext.drawMapBoardCells(mapContext,[{id:'S',x:0,y:0,chunks:[[0,0]]},{id:'U',x:1,y:0,chunks:[[0,0]]}],x=>x,y=>y,1);
assert(fills===2&&strokes===0,'Minimap/overview board renderer still draws board outlines');
assert(functionSource('rebuildWorldOverviewCache').includes('drawMapBoardCells')&&functionSource('rebuildWorldOverviewCache').includes('drawMapLongLines')&&!functionSource('rebuildWorldOverviewCache').includes('storeCellForMeta'),'Far overview does not exactly reuse minimap board/line rendering');
console.log('Borderless shared minimap/overview renderer test passed');
let resetRenderCount=0,resetChangeCount=0,resetSyncCount=0;
const resetState={paths:[{cells:[[0,0],[0,1]]}],specialProgress:{crossings:[[0,0]]},solved:false};
const resetBoard={id:'B0',drawing:{pointerId:9},armedGate:1,solvedPathsRendered:true,svg:{hasPointerCapture:()=>true,releasePointerCapture:()=>{}}};
const resetContext={
rendered:new Map(),activeBoard:'B0',metaState:()=>resetState,cancelBoardDragFrame:()=>{},clearTipMotion:()=>{},
applyBoardCommand:(board,mutate,options)=>{mutate(resetState);if(options.persist)resetChangeCount++},
renderBoardNow:()=>resetRenderCount++,updateSelectedProgress:()=>{},queueMicrotask:callback=>callback(),reconcileDrawingPresentation:()=>{},
syncBoundaryConnections:()=>resetSyncCount++,toast:()=>{}
};
vm.createContext(resetContext);
vm.runInContext(`${functionSource('resetSelectedBoard')}\nthis.resetSelectedBoard=resetSelectedBoard;`,resetContext);
resetContext.resetSelectedBoard(resetBoard);
assert(resetState.paths.length===0&&resetState.specialProgress.crossings.length===0,'First reset click did not clear the puzzle');
assert(resetRenderCount===1&&resetChangeCount===1&&resetSyncCount===0,'Reset was not immediately rendered and persisted exactly once');
console.log('First-click reset test passed');
const shopItems=[{id:'O1'},{id:'O2'},...Array.from({length:12},(_,index)=>({id:`C${index+1}`,cursorStyle:`face-${index+1}`}))];
assert(functionSource('renderStorePanel').includes('storeInventoryItems(meta,store)')&&functionSource('renderStorePanel').includes("'store-cursor':'store-other'"),'Shop rendering bypasses the fixed 2+12 item inventory');
const storeRateContext={
STORE_CHANCE:.10,hash32:value=>value>>>0,LOCAL_SOLVER:'tester',STORE_PRICE_VERSION:1,SCORE_VERSION:3,
trustedNow:()=>1000,currentPlayerName:()=>"tester",seededStoreItemIds:()=>shopItems.map(item=>item.id),storePriceDetails:()=>({coefficient:1})
};
vm.createContext(storeRateContext);
vm.runInContext(`${functionSource('maybeOpenStore')}\nthis.maybeOpenStore=maybeOpenStore;`,storeRateContext);
const eligibleStoreState=()=>({solved:true,store:null,paths:[{cells:[[0,0],[0,1]]}]});
const belowThreshold=eligibleStoreState(),atThreshold=eligibleStoreState();
storeRateContext.maybeOpenStore({seed:1},belowThreshold,0,.099999);
storeRateContext.maybeOpenStore({seed:1},atThreshold,0,.10);
assert(belowThreshold.store&&atThreshold.store===null,'Shop appearance boundary is not exactly 1/10');
assert(!app.includes('function overviewShopAtClient(')&&!functionSource('beginPan').includes('overviewShopAtClient'),'Zoomed-out view still has a shop-only hit target absent from the minimap');
const orphanState={paths:[{startGate:0,endGate:null,cells:[[0,0]]}]},orphanContext={
metaState:()=>orphanState,applyBoardCommand:(_board,mutate)=>{mutate();return true}
};
vm.createContext(orphanContext);
vm.runInContext(`${functionSource('discardUnmovedCreatedPath')}\nthis.discardUnmovedCreatedPath=discardUnmovedCreatedPath;`,orphanContext);
const orphanBoard={id:'B0',drawing:{pathIndex:0,createdPath:true},armedGate:0};
assert(orphanContext.discardUnmovedCreatedPath(orphanBoard)&&orphanState.paths.length===0&&orphanBoard.drawing===null,'Cancelled one-cell drag leaves an orphan pickup');
const numberNode={
classList:{on:false,toggle(_name,value){this.on=value}},
style:{value:null,setProperty(_name,value){this.value=value},removeProperty(){this.value=null}}
},numberContext={
ckey:(r,c)=>`${r},${c}`,metaState:()=>({paths:[{cells:[[0,0]]}]}),occupiedMap:()=>new Map([['0,0',0]]),pathColorAtCell:()=> '#72e38f'
};
vm.createContext(numberContext);
vm.runInContext(`${functionSource('refreshDragNumberColors')}\nthis.refreshDragNumberColors=refreshDragNumberColors;`,numberContext);
numberContext.refreshDragNumberColors({id:'B0',p:{},drawing:{pathIndex:0,segmentOccupancy:new Map([['0,0',0]])},numberNodes:new Map([['0,0',numberNode]])});
assert(numberNode.classList.on&&numberNode.style.value==='#72e38f'&&functionSource('renderDragFrame').includes('refreshDragNumberColors(b)'),'Number color does not update during pickup dragging');
const detachedState={paths:[{startGate:0,endGate:null,openGate:null,cells:[[0,0],[0,1],[0,2]],colorIndex:2,startColorIndex:2,endColorIndex:null}]};
const detachedContext={
metaState:()=>detachedState,applyBoardCommand:(_board,mutate)=>{mutate();return true},
drawingFromGate:(_board,pathIndex,gateIndex,pointerId)=>({pathIndex,gateIndex,pointerId})
};
vm.createContext(detachedContext);
vm.runInContext(`${functionSource('pathUsesGate')}\n${functionSource('detachPathFromStartGate')}\n${functionSource('reverseDetachedPath')}\nthis.detached={pathUsesGate,detachPathFromStartGate,reverseDetachedPath};`,detachedContext);
const detachedBoard={id:'B0',drawing:null,armedGate:null};
assert(detachedContext.detached.detachPathFromStartGate(detachedBoard,0,0,19),'Dragging the gate of an open line did not detach its anchored end');
const detachedPath=detachedState.paths[0];
assert(detachedPath.detachedStart&&JSON.stringify(detachedPath.cells)==='[[0,2],[0,1],[0,0]]','Detached line does not retain two oriented edge pickups');
assert(detachedContext.detached.pathUsesGate(detachedPath,0)&&detachedBoard.drawing.pointerId===19,'New gate-side pickup is not associated with the active drag');
assert(!functionSource('renderBoardNow').includes("whitePickupEnd")&&functionSource('renderBoardNow').includes("'data-endpoint-side':'start'"),'Two-ended line does not render both colored pickup handles');
const normalizeDetachedContext={isPlainObject:value=>value&&typeof value==='object'&&!Array.isArray(value),LINE_COLORS:Array(10).fill('#000')};
vm.createContext(normalizeDetachedContext);
vm.runInContext(`${functionSource('normalizePath')}\nthis.normalizePath=normalizePath;`,normalizeDetachedContext);
const normalizedDetached=normalizeDetachedContext.normalizePath(detachedPath);
assert(normalizedDetached.detachedStart&&!('whitePickupEnd' in normalizedDetached),'Two-ended pickup state is lost or retains the obsolete white pickup marker during persistence normalization');
pickupContext.metaState=()=>({paths:[
{startGate:0,endGate:null,detachedStart:true,cells:[[0,0],[0,1]],colorIndex:2,startColorIndex:2,endColorIndex:2},
{startGate:1,endGate:null,cells:[[0,3],[0,2],[0,1]],colorIndex:4,startColorIndex:4,endColorIndex:null}
]});
const detachedMergeBoard={id:'B0',p:{validSet:new Set(['0,0','0,1','0,2','0,3'])},drawing:{pathIndex:0}},
detachedMerge=pickupContext.openTipMergePlan(detachedMergeBoard,0,1);
assert(detachedMerge&&!detachedMerge.merged.detachedStart&&detachedMerge.merged.startGate===1&&detachedMerge.merged.endGate==null&&JSON.stringify(detachedMerge.merged.cells)==='[[0,3],[0,2],[0,1],[0,0]]','An isolated line could not merge into an anchored pickup');
pickupContext.metaState=()=>({paths:[
{startGate:0,endGate:null,cells:[[0,0]],colorIndex:1,startColorIndex:1},
{startGate:1,endGate:null,detachedStart:true,cells:[[0,0],[0,1],[0,2]],colorIndex:3,startColorIndex:3,endColorIndex:3}
]});
const detachedStartMerge=pickupContext.openTipMergePlan(detachedMergeBoard,0,1,'start');
assert(detachedStartMerge&&JSON.stringify(detachedStartMerge.merged.cells)==='[[0,0],[0,1],[0,2]]','The opposite endpoint of an isolated line could not be joined');
const graphData={metas:{A:{id:'A',puzzle:{}},B:{id:'B',puzzle:{}}},states:{
A:{paths:[{startGate:0,endGate:null,detachedStart:true,cells:[[0,0],[0,1]]}]},
B:{paths:[{startGate:0,endGate:null,cells:[[0,0],[1,0],[2,0]]}]}
}};
const graphEnvironment={data:graphData,metaState:id=>graphData.states[id],matchingNeighborGate:meta=>meta.id==='A'?{meta:graphData.metas.B,gateIndex:0}:{meta:graphData.metas.A,gateIndex:0}};
const detachedComponent=AppLogic.collectConnectedLineComponent(graphData.metas.A,0,new Map(),graphEnvironment);
assert(detachedComponent.members.length===1&&detachedComponent.length===2,'Detached pickup still inherits thickness through its former gate');
const reconnectState={paths:[{startGate:0,endGate:null,openGate:0,detachedStart:true,cells:[[0,1],[0,0]],colorIndex:2,startColorIndex:2,endColorIndex:2}]},gateEffects=[];
const reconnectContext={
metaState:()=>reconnectState,usedGateSet:()=>new Set(),LINE_COLORS:['#0af','#fa0','#0f0'],canonicalGateColorIndex:()=>1,
applyBoardCommand:(_board,mutate)=>{mutate();return true},neighborColor:()=>null,playSound:()=>{},
usesLightweightDragOverlay:()=>false,requestAnimationFrame:callback=>callback(),
gateConnectEffect:(_board,_gate,color)=>gateEffects.push(color),queueMicrotask:callback=>callback(),reconcileDrawingPresentation:()=>{}
};
vm.createContext(reconnectContext);
vm.runInContext(`${functionSource('finalizeAtGate')}\nthis.finalizeAtGate=finalizeAtGate;`,reconnectContext);
const reconnectBoard={id:'B0',p:{g:[[0,0,'W']]},drawing:{pathIndex:0},armedGate:0};
assert(reconnectContext.finalizeAtGate(reconnectBoard,0),'Detached pickup could not reconnect to its original gate');
assert(!reconnectState.paths[0].detachedStart&&reconnectState.paths[0].startGate===0&&gateEffects[0]==='#0f0','Reconnected two-ended line did not collapse to one anchored line with its line-color effect');
console.log('Shop access/order, orphan cleanup, live number color, and dual-pickup tests passed');
const rewardContext={MAX_SCORE:1000000000,anomalyScoreMultiplier:()=>1};
vm.createContext(rewardContext);
vm.runInContext(`${functionSource('scoreFromThickness')}\nthis.scoreFromThickness=scoreFromThickness;`,rewardContext);
const smallEasy=rewardContext.scoreFromThickness({level:2,chunks:[[0,0]]},20),
smallHard=rewardContext.scoreFromThickness({level:10,chunks:[[0,0]]},20),
largeHard=rewardContext.scoreFromThickness({level:10,chunks:Array.from({length:8},(_,index)=>[index,0])},20);
assert(smallHard>smallEasy*8&&largeHard>smallHard*2.5,'Hard and large puzzle rewards do not scale strongly enough with expected solve time');
const gateArmingState={paths:[{startGate:0,endGate:null,openGate:0,cells:[[0,0]]}]},gateArmingContext={
metaState:()=>gateArmingState,sameCell:(a,b)=>a&&b&&a[0]===b[0]&&a[1]===b[1]
};
vm.createContext(gateArmingContext);
vm.runInContext(`${functionSource('updateGateSnapArming')}\nthis.updateGateSnapArming=updateGateSnapArming;`,gateArmingContext);
const gateArmingBoard={id:'B0'},gateDrawing={pathIndex:0,originGate:0,originGateCell:[0,0],gateSnapArmed:false,leftOriginGateCell:false};
gateArmingContext.updateGateSnapArming(gateArmingBoard,gateDrawing);
assert(!gateDrawing.gateSnapArmed&&!gateDrawing.leftOriginGateCell,'A connected gate rearmed before the line endpoint left its gate cell');
gateArmingState.paths[0].cells.push([0,1]);gateArmingContext.updateGateSnapArming(gateArmingBoard,gateDrawing);
assert(gateDrawing.gateSnapArmed&&gateDrawing.leftOriginGateCell,'Dragging a connected gate endpoint out of its cell did not unlock movement');
assert(functionSource('extendPointerTo').includes("snappedGate===drawing.originGate&&!drawing.leftOriginGateCell"),'The origin gate can recapture a connected endpoint before it moves away');
const disappearingState={paths:[{startGate:0,endGate:null,detachedStart:true,cells:[[0,0],[0,1]],colorIndex:1,startColorIndex:1,endColorIndex:1}]};
let disappearingReleased=0,disappearingPersisted=0;
const disappearingContext={
CELL:40,metaState:()=>disappearingState,boardCellCenter:([r,c])=>[20+c*40,20+r*40],pointerLocalRadius:(_board,pixels)=>pixels,
cancelBoardDragFrame:()=>{},clearTipMotion:()=>{},applyBoardCommand:(_board,mutate,options)=>{const result=mutate();if(result===false)return false;if(options?.persist)disappearingPersisted++;return true},
safeRelease:()=>disappearingReleased++,playSound:()=>{},queueMicrotask:callback=>callback(),reconcileDrawingPresentation:()=>{}
};
vm.createContext(disappearingContext);
vm.runInContext(`${functionSource('removeDetachedPathAtOwnEndpoint')}\nthis.removeDetachedPathAtOwnEndpoint=removeDetachedPathAtOwnEndpoint;`,disappearingContext);
const disappearingBoard={id:'B0',drawing:{pathIndex:0,pointerId:23},armedGate:null,svg:{}};
assert(!disappearingContext.removeDetachedPathAtOwnEndpoint(disappearingBoard,[60,20]),'A two-ended line vanished before its handles overlapped');
assert(disappearingContext.removeDetachedPathAtOwnEndpoint(disappearingBoard,[20,20]),'Overlapping both handles did not remove the two-ended line');
assert(disappearingState.paths.length===0&&disappearingBoard.drawing===null&&disappearingReleased===1&&disappearingPersisted===1,'Two-ended line removal did not cleanly persist and release pointer capture');
console.log('Connected-gate dragging and two-handle line disappearance tests passed');
assert(!app.includes('RETRACTION_HOLD_MS')&&!app.includes('pendingRetraction'),'Own-line retraction still has a delayed state');
assert(functionSource('extendOne').includes('path.cells.splice(rewind+1)')&&functionSource('extendPointerTo').includes('rewindActivePathToCell(b,targetCell'),'Original immediate own-line shortening was not restored');
assert(functionSource('gateFromPointOrCell').includes('maxPixels:0')&&!functionSource('extendPointerTo').includes('gateConnectionSteps'),'A knob can still snap to a gate from an adjacent cell');
assert(functionSource('shouldTeleportToUnsolvedBoard').includes('return false')&&!functionSource('bindBoard').includes('centerMeta(')&&!app.includes('function promoteStaticBoard('),'Board input can still promote a summary or teleport the camera');
assert(functionSource('matchingNumberKeys').includes('partialTurnCount')&&functionSource('updateNumberMatchFeedback').includes("classList.toggle('number-match',current.size>0)")&&functionSource('updateNumberMatchFeedback').includes("matchOrbit.classList.toggle('show'")&&css.includes('@keyframes numberMatchOrbit')&&css.includes('stroke-dasharray:1.2 5.35'),'Matching turn-number feedback does not keep an enlarged knob with a rotating perforated orbit');
assert(functionSource('renderBoardNow').includes("'\\u66f2\\u304c\\u308b'")&&app.includes('numberLayer.append(node,warning)')&&css.includes('.number-turn-warning.show{opacity:1}'),'The 曲がる warning is not prominent or is behind the number');
console.log('Immediate retraction, exact-cell gate, no-click-teleport, and number-match feedback guards passed');

View file

@ -0,0 +1,38 @@
'use strict';
const {vm,app,read,assert,functionSource}=require('./helpers/app-source');
const checkpointSource=functionSource('runMirrorCheckpoint'),benchmark=read('test/browser-performance-benchmark.js');
assert(checkpointSource.includes("build.stage==='serialize'")&&checkpointSource.includes('writeMirrorBuildChunk(build)'),'Idle mirror checkpoint does not serialize and write bounded chunks');
assert(!checkpointSource.includes('writeCompactMirror(')&&!checkpointSource.includes('JSON.stringify(snapshot)'),'Idle mirror checkpoint still performs a full synchronous mirror write');
assert(app.includes("MIRROR_CHUNK_BYTES=64*1024")&&app.includes("MIRROR_CHUNK_FORMAT='bend-field-chunked-v1'"),'Chunked mirror format or size bound is missing');
assert(!benchmark.includes('33*multiplier')&&!benchmark.includes('20*multiplier')&&!benchmark.includes('40*multiplier'),'CPU-throttled benchmark thresholds are still multiplied beyond the documented targets');
assert(benchmark.includes("name:'complex'")&&benchmark.includes("chunks:10")&&benchmark.includes("name:'long-line'"),'Browser benchmark does not cover a ten-chunk board and a long connected line');
const serializerContext={JSON,Number,Object,Array};
vm.createContext(serializerContext);
vm.runInContext(`${functionSource('jsonValueFragments')}\nthis.jsonValueFragments=jsonValueFragments;`,serializerContext);
const sample={schema:30,title:'曲線\\"test',enabled:true,missing:null,metas:{A:{id:'A',chunks:[[0,0],[1,0]],puzzle:{valid:[[0,0],[0,1]]}}},states:{A:{paths:[{cells:[[0,0],[0,1]]}]}}};
const serialized=[...serializerContext.jsonValueFragments(sample)].join('');
assert(serialized===JSON.stringify(sample)&&JSON.parse(serialized).metas.A.chunks.length===2,'Incremental JSON serializer changed the mirror payload');
const storage=new Map(),manifest={format:'bend-field-chunked-v1',generation:'g1',chunks:3,bytes:11,updatedAt:12};
storage.set('save',JSON.stringify(manifest));storage.set('chunk:g1:0','{"a":');storage.set('chunk:g1:1','[1,2]');storage.set('chunk:g1:2','}');
const readContext={
storageKey:'save',mirrorChunkPrefix:'chunk:',MIRROR_CHUNK_FORMAT:'bend-field-chunked-v1',LOCAL_MIRROR_MAX_BYTES:1024,MIRROR_CHUNK_BYTES:4,
safeLocalGet:key=>storage.has(key)?storage.get(key):null,JSON,Number,Math
};
vm.createContext(readContext);
vm.runInContext(`${functionSource('mirrorChunkKey')}\n${functionSource('parseMirrorManifest')}\n${functionSource('readCompactMirrorRaw')}\nthis.readCompactMirrorRaw=readCompactMirrorRaw;`,readContext);
assert(readContext.readCompactMirrorRaw()==='{"a":[1,2]}','Chunked mirror manifest was not reconstructed correctly');
storage.delete('chunk:g1:1');assert(readContext.readCompactMirrorRaw()===null,'Incomplete chunked mirror was accepted');
const writes=[],chunkContext={
mirrorChunkKey:(generation,index)=>`chunk:${generation}:${index}`,safeLocalSet:(key,value)=>{writes.push([key,value]);return true},
perfStart:()=>0,perfNow:()=>3,perfGauge:()=>{},perfEnd:()=>{},perfCount:()=>{}
};
vm.createContext(chunkContext);
vm.runInContext(`${functionSource('writeMirrorBuildChunk')}\nthis.writeMirrorBuildChunk=writeMirrorBuildChunk;`,chunkContext);
const build={generation:'g2',chunkIndex:0,pendingChunk:'x'.repeat(65536),bytes:0};
assert(chunkContext.writeMirrorBuildChunk(build)===true&&writes.length===1&&writes[0][1].length===65536&&build.pendingChunk===''&&build.bytes===65536,'Mirror chunk writer did not perform exactly one bounded write');
console.log('Chunked recovery mirror and strict performance acceptance regression passed');

View file

@ -0,0 +1,174 @@
'use strict';
const {vm,app,css,assert,functionSource}=require('./helpers/app-source');
const flashSource=functionSource('flashConfirmedCell'),eventSource=functionSource('eventToSvg'),
dragSource=functionSource('renderDragFrame'),extendSource=functionSource('extendPointerTo'),
extendOneSource=functionSource('extendOne'),bindBoardSource=functionSource('bindBoard'),
dragCacheSource=functionSource('buildDragCache'),
minimapSource=functionSource('drawMinimap'),minimapBuildSource=functionSource('rebuildMinimapWorld'),
widthRefreshSource=functionSource('refreshRenderedLineWidths'),ensureBoardsSource=functionSource('ensureBoards');
assert(!flashSource.includes('getBoundingClientRect')&&!flashSource.includes('querySelector'),'Cell confirmation flash performs a layout read or DOM query');
assert(flashSource.includes("svgEl('rect'")&&flashSource.includes('b.dragLayer.append(node)')&&flashSource.includes("typeof node.animate==='function'"),'Cell confirmation flash does not use one temporary SVG node and WAAPI');
assert(!functionSource('makeBoard').includes('cellHits.set')&&functionSource('makeBoard').includes('cellFlashAnimations:new Map()'),'Board construction still retains one hit node per cell or lacks animation tracking');
assert(!eventSource.includes('getBoundingClientRect')&&eventSource.includes('boardScreenRect(b)'),'Pointer conversion reads live SVG layout');
assert(!functionSource('pointerLocalRadius').includes('getBoundingClientRect')&&functionSource('edgePanVelocity').includes('getViewportRect()'),'High-frequency input still reads live element geometry');
assert(extendSource.includes('renderDragFrame(b)'),'Pointer drawing does not enter the drag-only renderer');
assert(!extendSource.includes('invalidateLineGraphCaches')&&!extendSource.includes('queueLineWidthRefresh'),'Pointer frames still invalidate or traverse connected-line components');
assert(dragSource.includes('drawingLineWidth(b,index)')&&functionSource('drawingLineWidth').includes('drawing.lineWidth=width'),'Drag width is not fixed for the active gesture');
assert(dragSource.includes('pathVisualNodes')&&dragSource.includes('dragCache')&&dragSource.includes('refreshDragSpecialState')&&dragCacheSource.includes('b.dragLayer')&&dragCacheSource.includes('strokes'),'Drag-only renderer does not retain and selectively replace active visuals');
for(const forbidden of ['pathValid(','straightNumberWarningKeys(','multipleNumberWarningKeys(','queueLineWidthRefresh(','renderBoard(b)'])assert(!dragSource.includes(forbidden),`Drag-only renderer performs full-render work: ${forbidden}`);
assert(functionSource('processBoardDragFrame').includes('extendPointerTo(b,point,true)')&&!functionSource('processBoardDragFrame').includes('renderBoard('),'Pointer RAF does not defer raw-sample rendering to one drag-frame repaint');
assert(extendOneSource.includes('isCrossing=b.crossingKeySet.has(key)')&&extendOneSource.includes('currentOwner=')&&extendOneSource.includes('occ.get(exitKey)'),'Ordinary drag occupancy bypasses its retained map');
assert(extendOneSource.includes('if(!suppressMotion){flashConfirmedCell')&&(extendSource.match(/emitBatchedFeedback\(\)/g)||[]).length===2,'Batched pointer traversal emits per-cell confirmation effects');
assert((bindBoardSource.match(/b\.drawing\.keyboardActive=true/g)||[]).length>=2,'Keyboard continuation does not keep its board at interactive LOD');
assert(functionSource('renderedConnectedLineWidth').includes('currentLineGraphCaches()')&&functionSource('markStateDirty').includes('invalidateLineGraphCaches(id,pathIndex)'),'Connected-line caches are not persistent or invalidated by the touched state/path');
assert(widthRefreshSource.includes('board.pathStrokeNodes')&&widthRefreshSource.includes('board.connectorNodes'),'Line-width refresh does not prefer retained SVG node references');
assert(!widthRefreshSource.includes('querySelectorAll'),'Line-width refresh retains a DOM-query fallback');
assert(minimapBuildSource.includes('minimapCache={revision:minimapWorldRevision')&&minimapSource.includes('if(stale)rebuildMinimapWorld')&&minimapSource.includes('drawImage(minimapBase'),'Minimap does not retain and reuse an overscanned world layer');
assert(functionSource('drawMapLongLines').includes('minimapGeometryForComponent(component,caches.geometries)'),'Long-line minimap geometry is recalculated during every world-layer rebuild');
assert(functionSource('markStateDirty').includes('invalidateWorldPresentation()'),'State changes do not invalidate minimap/overview content');
assert(app.includes('globalThis.BEND_PERF=')&&functionSource('perfObserve').includes('samples.length>240'),'Performance measurements are missing or unbounded');
assert(css.includes('#noiseCanvas.interaction-muted')&&css.includes('body.reduced-effects #noiseCanvas')&&functionSource('scheduleNoiseBackground').includes('noisePainted')&&!functionSource('scheduleNoiseBackground').includes('setTimeout'),'Decorative noise is not static or interaction/reduced-motion suppression is missing');
assert((ensureBoardsSource.match(/changes<LOD_CHANGES_PER_PASS/g)||[]).length>=3&&ensureBoardsSource.includes('if(pending)scheduleLodPass()'),'LOD creation or eviction bypasses the per-pass budget');
assert(functionSource('desiredInteractiveBoardIds').includes('for(const id of ids)')&&!app.includes('INTERACTIVE_DETAIL_CELL_BUDGET'),'Visible-puzzle detail selection is incomplete or still budget-culls boards');
assert(ensureBoardsSource.includes("perfGauge('visibleUnsolvedBoards'"),'LOD performance telemetry cannot prove visible unsolved-board coverage');
const visibleIdsForDetail=new Set(Array.from({length:30},(_,index)=>`B${index}`)),
visibleContext={data:{metas:Object.fromEntries([...visibleIdsForDetail].map(id=>[id,{id,x:Number(id.slice(1)),y:0,puzzle:{bounds:{w:5,h:5},valid:Array.from({length:25})}}]))}};
vm.createContext(visibleContext);
vm.runInContext(`${functionSource('desiredInteractiveBoardIds')}\nthis.desiredInteractiveBoardIds=desiredInteractiveBoardIds;`,visibleContext);
const desiredVisible=visibleContext.desiredInteractiveBoardIds(visibleIdsForDetail);
assert(desiredVisible.has('B0')&&desiredVisible.size===visibleIdsForDetail.size,'Not every visible hydrated puzzle remained detailed');
let animationStarts=0,animationCancels=0,flashRemovals=0;
function makeFlashNode(){return{animate(){animationStarts++;const animation={cancel(){animationCancels++},finished:Promise.resolve()};return animation},remove(){flashRemovals++}}}
const flashContext={
PAD:10,CELL:40,ckey:(r,c)=>`${r},${c}`,svgEl:()=>makeFlashNode(),setTimeout:callback=>{callback();return 1},clearTimeout(){}
};
vm.createContext(flashContext);
vm.runInContext(`${flashSource}\nthis.flashConfirmedCell=flashConfirmedCell;`,flashContext);
const flashBoard={svg:{isConnected:true},dragLayer:{append(){}},cellFlashAnimations:new Map()};
flashContext.flashConfirmedCell(flashBoard,[1,2]);flashContext.flashConfirmedCell(flashBoard,[1,2]);
assert(animationStarts===2&&animationCancels>=1&&flashBoard.cellFlashAnimations.size<=1,'Repeated cell confirmation did not replace one temporary animation');
let rectCalls=0;
const eventContext={boardScreenRect:()=>{rectCalls++;return{left:10,top:20,width:200,height:100}}};
vm.createContext(eventContext);
vm.runInContext(`${eventSource}\nthis.eventToSvg=eventToSvg;`,eventContext);
const eventBoard={svg:{viewBox:{baseVal:{width:400,height:200}},getBoundingClientRect(){throw new Error('event conversion forced layout')}}},
mapped=eventContext.eventToSvg(eventBoard,{clientX:110,clientY:70});
assert(rectCalls===1&&mapped[0]===200&&mapped[1]===100,'Cached board geometry produced incorrect pointer coordinates');
const cacheContext={lineGraphRevision:0,lineGraphCacheRevision:-1,lineComponentCache:new Map(),lineWidthCache:new Map(),lineMinimapGeometryCache:new WeakMap(),data:{metas:{},states:{}},linePathKey:(id,index)=>`${id}:${index}`,matchingNeighborGate:()=>null};
vm.createContext(cacheContext);
vm.runInContext(`${functionSource('invalidateLineGraphCaches')}\n${functionSource('currentLineGraphCaches')}\nthis.logic={invalidateLineGraphCaches,currentLineGraphCaches};`,cacheContext);
const firstCaches=cacheContext.logic.currentLineGraphCaches(),secondCaches=cacheContext.logic.currentLineGraphCaches();
assert(firstCaches.components===secondCaches.components&&firstCaches.widths===secondCaches.widths,'Connected-line caches do not persist across reads');
cacheContext.logic.invalidateLineGraphCaches();const thirdCaches=cacheContext.logic.currentLineGraphCaches();
assert(thirdCaches.components!==firstCaches.components&&thirdCaches.widths!==firstCaches.widths,'Connected-line invalidation reused stale cache maps');
const shared={members:[[{id:'A'},0],[{id:'B'},0]],length:10},unrelated={members:[[{id:'C'},0]],length:3};
thirdCaches.components.set('A:0',shared);thirdCaches.components.set('B:0',shared);thirdCaches.components.set('C:0',unrelated);
thirdCaches.widths.set('A:0',5);thirdCaches.widths.set('B:0',5);thirdCaches.widths.set('C:0',4);
cacheContext.logic.invalidateLineGraphCaches('A',0);
assert(!thirdCaches.components.has('A:0')&&!thirdCaches.components.has('B:0')&&thirdCaches.components.get('C:0')===unrelated&&thirdCaches.widths.has('C:0'),'Path-granular invalidation cleared an unrelated connected-line component');
const widthWrites=[],meta={id:'A',puzzle:{}},
pathNode={dataset:{pathIndex:'0'},style:{setProperty:(name,value)=>widthWrites.push(['path',name,value])}},
connectorNode={dataset:{ownerBoard:'A',pathIndex:'0'},style:{setProperty:(name,value)=>widthWrites.push(['connector',name,value])}},
widthBoard={id:'A',meta,pathStrokeNodes:[pathNode],connectorNodes:[connectorNode],pathLayer:{querySelectorAll(){throw new Error('path DOM scan')}},connectorLayer:{querySelectorAll(){throw new Error('connector DOM scan')}}};
const widthContext={
rendered:new Map([['A',widthBoard]]),data:{metas:{A:meta}},metaState:()=>({paths:[{}]}),
linePathKey:(id,index)=>`${id}:${index}`,splitLinePathKey:key=>{const split=key.lastIndexOf(':');return[key.slice(0,split),Number(key.slice(split+1))]},
currentLineGraphCaches:()=>({components:new Map(),widths:new Map()}),
collectConnectedLineComponent:()=>({members:[[meta,0]]}),renderedConnectedLineWidth:()=>8,
applyLineWidth:(node,width)=>node.style.setProperty('--line-width',width.toFixed(2)),
perfStart:()=>0,perfEnd:()=>0,perfGauge:()=>{}
};
vm.createContext(widthContext);
vm.runInContext(`${widthRefreshSource}\nthis.refreshRenderedLineWidths=refreshRenderedLineWidths;`,widthContext);
widthContext.refreshRenderedLineWidths(new Set(['A:0']));
assert(widthWrites.length===2&&widthWrites.every(([,name,value])=>name==='--line-width'&&value==='8.00'),'Retained line nodes were not updated without DOM scans');
let geometryStateReads=0,geometryBuilds=0;
const geometryMeta={id:'G',x:2,y:3,puzzle:{}},geometryComponent={length:1500,members:[[geometryMeta,0]]},geometryContext={
CHUNK:5,metaState:()=>{geometryStateReads++;return{paths:[{startGate:0,endGate:1,cells:[[0,0],[0,1],[1,1]]}]}},
puzzleOf:()=>({}),gateObj:(_p,index)=>index?{cell:[1,1],side:'S'}:{cell:[0,0],side:'W'},pathColor:()=> '#fff',perfCount:()=>geometryBuilds++,currentLineGraphCaches:()=>({geometries:new WeakMap()})
};
vm.createContext(geometryContext);vm.runInContext(`${functionSource('minimapGeometryForComponent')}\nthis.minimapGeometryForComponent=minimapGeometryForComponent;`,geometryContext);
const geometryCache=new WeakMap(),firstGeometry=geometryContext.minimapGeometryForComponent(geometryComponent,geometryCache),secondGeometry=geometryContext.minimapGeometryForComponent(geometryComponent,geometryCache);
assert(firstGeometry===secondGeometry&&geometryStateReads===1&&geometryBuilds===1&&firstGeometry.segments[0].points.length>=2,'Simplified long-line geometry was not retained per connected component');
let minimapBuilds=0,center=[1,0],drawCopies=0;
const mapContext2d={setTransform(){},clearRect(){},drawImage(){drawCopies++},beginPath(){},moveTo(){},lineTo(){},stroke(){}},
attributes=new Map(),minimapCanvas={
width:210,height:132,getBoundingClientRect:()=>({width:210,height:132}),getContext:()=>mapContext2d,
getAttribute:name=>attributes.get(name)||null,setAttribute:(name,value)=>attributes.set(name,value)
},
minimapContext={
minimapCanvas,minimapStatus:{textContent:''},minimapBase:{},minimapDirty:true,minimapLongSegments:0,minimapWorldRevision:1,
minimapCache:{revision:1,width:210,height:132,dpr:1,anchorX:0,anchorY:0,scale:5,overscanPixels:50,baseWidth:310,baseHeight:232,longSegments:0},
window:{devicePixelRatio:1},MINIMAP_VIEW_CHUNKS_X:42,data:{metas:{}},getMinimapRect:()=>({width:210,height:132}),
cameraCenterInChunks:()=>center,visibleMetaIdsForBounds:()=>new Set(),metaState:()=>({solved:false}),
perfStart:()=>0,perfEnd:()=>0,perfCount:()=>{}
};
minimapContext.rebuildMinimapWorld=(width,height,dpr,x,y)=>{
minimapBuilds++;minimapContext.minimapCache={...minimapContext.minimapCache,revision:minimapContext.minimapWorldRevision,width,height,dpr,anchorX:x,anchorY:y};
};
vm.createContext(minimapContext);
vm.runInContext(`${minimapSource}\nthis.drawMinimap=drawMinimap;`,minimapContext);
minimapContext.drawMinimap();assert(minimapBuilds===0&&drawCopies===1,'Small camera movement rebuilt the cached minimap world layer');
center=[20,0];minimapContext.drawMinimap();assert(minimapBuilds===1&&drawCopies===2,'Camera movement beyond minimap overscan did not rebuild exactly once');
let overviewLodSchedules=0;
const overviewRendered=new Map(Array.from({length:6},(_,index)=>[`D${index}`,{id:`D${index}`,drawing:null}])),
overviewStatic=new Map(Array.from({length:6},(_,index)=>[`S${index}`,{id:`S${index}`}]));
const overviewLodContext={
LOD_CHANGES_PER_PASS:4,rendered:overviewRendered,staticRendered:overviewStatic,inWorldOverview:()=>true,
destroyBoard:board=>overviewRendered.delete(board.id),destroyStaticBoard:board=>overviewStatic.delete(board.id),
scheduleLodPass:()=>overviewLodSchedules++,scheduleWorldOverview:()=>{},perfStart:()=>0,perfCount:()=>{},perfEnd:()=>{}
};
vm.createContext(overviewLodContext);vm.runInContext(`${ensureBoardsSource}\nthis.ensureBoards=ensureBoards;`,overviewLodContext);overviewLodContext.ensureBoards();
assert(overviewRendered.size+overviewStatic.size===8&&overviewLodSchedules===1,'Overview eviction exceeded its four-change budget or failed to schedule continuation');
let creationLodSchedules=0;
const visibleIds=new Set(Array.from({length:10},(_,index)=>`B${index}`)),creationStatic=new Map(),
creationData={metas:Object.fromEntries([...visibleIds].map(id=>[id,{id,puzzle:{}}]))};
const creationLodContext={
LOD_CHANGES_PER_PASS:4,rendered:new Map(),staticRendered:creationStatic,data:creationData,inWorldOverview:()=>false,
visibleMetaIds:()=>visibleIds,desiredInteractiveBoardIds:ids=>new Set(ids),metaState:()=>({solved:false}),
makeStaticBoard:meta=>creationStatic.set(meta.id,{id:meta.id,signature:'current'}),destroyStaticBoard:board=>board&&creationStatic.delete(board.id),
destroyBoard:()=>{},makeBoard:meta=>creationLodContext.rendered.set(meta.id,{id:meta.id,drawing:null}),staticBoardSignature:()=> 'current',
scheduleLodPass:()=>creationLodSchedules++,perfStart:()=>0,perfCount:()=>{},perfGauge:()=>{},perfEnd:()=>{}
};
vm.createContext(creationLodContext);vm.runInContext(`${ensureBoardsSource}\nthis.ensureBoards=ensureBoards;`,creationLodContext);creationLodContext.ensureBoards();
assert(creationLodContext.rendered.size===4&&creationStatic.size===0&&creationLodSchedules===1,'Detailed board creation exceeded its four-change budget or hid visible boards behind static summaries');
(async()=>{
let workerConstructions=0,workerNow=1000,timerId=0;
const restartCallbacks=[],restartDelays=[],workerRetryContext={
APP_VERSION:'47.36',GENERATOR_VERSION:5,puzzleWorker:null,workerRestartTimer:null,workerFailureCount:0,workerDisabledUntil:0,workerRetryAt:0,
Date:{now:()=>workerNow},Worker:function Worker(){workerConstructions++;throw new Error('synthetic worker failure')},
setTimeout:(callback,delay)=>{restartCallbacks.push(callback);restartDelays.push(delay);return++timerId},clearTimeout:()=>{},console:{warn:()=>{}}
};
vm.createContext(workerRetryContext);
vm.runInContext(`${functionSource('scheduleWorkerRestart')}\n${functionSource('createPuzzleWorker')}\nthis.createPuzzleWorker=createPuzzleWorker;`,workerRetryContext);
workerRetryContext.createPuzzleWorker();workerRetryContext.createPuzzleWorker();
assert(workerConstructions===1&&restartDelays[0]===1000,'Worker creation retried immediately while a backoff timer was armed');
workerNow+=1000;restartCallbacks.shift()();
assert(workerConstructions===2&&restartDelays[1]===2000,'Worker restart did not advance through the gated exponential backoff');
let mainThreadCalls=0;
const workerContext={
puzzleWorker:null,workerSeq:0,workerJobs:new Map(),location:{protocol:'https:'},Worker:function Worker(){},
createPuzzleWorker:()=>null,perfCount:()=>{},generatePuzzleOnMainThread:()=>{mainThreadCalls++;return Promise.resolve({})}
};
vm.createContext(workerContext);
vm.runInContext(`${functionSource('generatePuzzleAsync')}\nthis.generatePuzzleAsync=generatePuzzleAsync;`,workerContext);
let rejected=false;try{await workerContext.generatePuzzleAsync([[0,0]],1,1)}catch(_){rejected=true}
assert(rejected&&mainThreadCalls===0,'Served-mode worker failure fell back to synchronous main-thread generation');
console.log('Performance regression smoke test passed');
})().catch(error=>{console.error(error);process.exitCode=1});

View file

@ -0,0 +1,24 @@
'use strict';
const fs=require('fs');
const path=require('path');
const assert=require('assert/strict');
const {root,functionSource}=require('./helpers/app-source');
const app=fs.readFileSync(path.join(root,'app.js'),'utf8');
const server=fs.readFileSync(path.join(root,'server.js'),'utf8');
const realtime=fs.readFileSync(path.join(root,'realtime-server.js'),'utf8');
const html=fs.readFileSync(path.join(root,'index.html'),'utf8');
const css=fs.readFileSync(path.join(root,'style.css'),'utf8');
const pkg=JSON.parse(fs.readFileSync(path.join(root,'package.json'),'utf8'));
assert(html.includes('<canvas id="presenceCanvas"'),'Presence canvas is missing');
assert(css.includes('#presenceCanvas')&&css.includes('.board-claim-badge'),'Presence or claim presentation styles are missing');
assert(app.includes('REALTIME_CURSOR_INTERVAL=50')&&app.includes('REALTIME_CURSOR_HEARTBEAT_INTERVAL=5000')&&app.includes('AUXILIARY_FRAME_INTERVAL'),'Cursor transport is not capped at 20 Hz or presence drawing is not tied to the auxiliary frame budget');
assert(functionSource('scheduleRealtimeViewport').includes('REALTIME_VIEWPORT_INTERVAL'),'Viewport subscription is not throttled');
assert(functionSource('drawPresenceLayer').includes('remotePlayers')&&functionSource('drawPresenceLayer').includes('scheduleMinimap'),'Remote cursors are not rendered through the shared canvas layer');
assert(functionSource('drawMinimap').includes('nearbyPlayers'),'Remote players are missing from the minimap');
assert(functionSource('requestBoardClaim').includes("type:'claim'")&&functionSource('touchBoardClaim').includes("type:'claim-touch'"),'Client claim lease messages are incomplete');
assert(server.includes("status:423")&&server.includes('hasClaim(player.playerId,rawRow.id)'),'Server clear validation does not require the active claimant');
assert(realtime.includes('5 * 60 * 1000')&&realtime.includes("releaseBoardClaim(boardId, 'moved')")&&realtime.includes("reason:'expired'"),'Five-minute lease or board-switch release behavior is missing');
assert(realtime.includes("message.type === 'viewport'")&&realtime.includes("message.type === 'cursor-hide'")&&realtime.includes('pointInViewport'),'Realtime fan-out is not viewport-filtered');
assert(!Object.keys(pkg.dependencies||{}).length&&!Object.keys(pkg.devDependencies||{}).length,'Phase 2 added an unnecessary runtime dependency');
console.log('Shared-world phase 2 source guards passed');

View file

@ -0,0 +1,33 @@
'use strict';
const http=require('http');
const assert=require('assert/strict');
const {createRealtimeHub}=require('../realtime-server');
const {connectRealtime}=require('./helpers/realtime-client');
(async()=>{
const server=http.createServer((_req,res)=>{res.writeHead(404);res.end()});
const identities=new Map([
['alice',{playerId:'alice',token:'a',name:'Alice'}],
['bob',{playerId:'bob',token:'b',name:'Bob'}]
]);
const hub=createRealtimeHub({
server,claimTtlMs:120,
authenticate:async({playerId,token})=>{const identity=identities.get(playerId);if(!identity||identity.token!==token)throw new Error('bad auth');return identity},
getBoardInfo:async boardId=>boardId==='B0'?{solved:false,bounds:{minX:0,minY:0,maxX:1,maxY:1}}:null
});
await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve));
const base=`http://127.0.0.1:${server.address().port}`;
let alice=null,bob=null;
try{
alice=await connectRealtime(base,{playerId:'alice',token:'a'});bob=await connectRealtime(base,{playerId:'bob',token:'b'});
alice.send({type:'viewport',minX:-2,minY:-2,maxX:2,maxY:2});bob.send({type:'viewport',minX:-2,minY:-2,maxX:2,maxY:2});await alice.waitFor('snapshot');await bob.waitFor('snapshot');
alice.send({type:'claim',requestId:'lease-a',boardId:'B0'});assert.equal((await alice.waitFor(message=>message.type==='claim-result'&&message.requestId==='lease-a')).ok,true);await bob.waitFor(message=>message.type==='claim'&&message.claim?.boardId==='B0');
alice.close();alice=null;
bob.send({type:'claim',requestId:'lease-b-early',boardId:'B0'});const early=await bob.waitFor(message=>message.type==='claim-result'&&message.requestId==='lease-b-early');assert.equal(early.ok,false);assert.equal(early.reason,'occupied');
await new Promise(resolve=>setTimeout(resolve,170));bob.send({type:'snapshot-request'});await bob.waitFor(message=>message.type==='claim-release'&&message.boardId==='B0'&&message.reason==='expired');
bob.send({type:'claim',requestId:'lease-b-late',boardId:'B0'});const late=await bob.waitFor(message=>message.type==='claim-result'&&message.requestId==='lease-b-late');assert.equal(late.ok,true);
console.log('Realtime lease expiry and disconnect semantics passed');
}finally{
alice?.close();bob?.close();hub.close();await new Promise(resolve=>server.close(resolve));
}
})().catch(error=>{console.error(error);process.exitCode=1});

View file

@ -0,0 +1,70 @@
'use strict';
const {spawn}=require('child_process');
const fs=require('fs');
const os=require('os');
const path=require('path');
const assert=require('assert/strict');
const {root,starterPuzzle}=require('./helpers/app-source');
const {connectRealtime}=require('./helpers/realtime-client');
const port=20000+Math.floor(Math.random()*10000);
const dataDir=fs.mkdtempSync(path.join(os.tmpdir(),'bend-field-realtime-phase2-'));
const child=spawn(process.execPath,[path.join(root,'server.js')],{
env:{...process.env,PORT:String(port),HOST:'127.0.0.1',BEND_FIELD_DATA_DIR:dataDir},stdio:['ignore','pipe','pipe']
});
let stderr='',aliceRealtime=null,bobRealtime=null;child.stderr.on('data',chunk=>stderr+=chunk);
const base=`http://127.0.0.1:${port}`;
const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));
async function request(url,options={}){const response=await fetch(base+url,options),body=await response.json();return{response,body}}
function auth(session){return{authorization:`Bearer ${session.playerId}.${session.token}`,'content-type':'application/json'}}
function boardMeta(id,x,seed,puzzle){return{id,x,y:0,chunks:[[0,0]],level:1,targetLevel:1,seed,axis:'MIX',sealedSides:[],puzzle,rev:1,revAuthor:'client'}}
function solvedState(puzzle){return{paths:puzzle.solution.map(route=>({startGate:route.startGate,endGate:route.endGate,cells:route.cells.map(cell=>[...cell])})),solved:true,expanded:false,rev:2,revAuthor:'client'}}
(async()=>{
for(let index=0;index<80;index++){try{const status=await request('/api/cloud/status');if(status.response.ok)break}catch(_){}await sleep(50)}
const status=await request('/api/cloud/status');assert.equal(status.body.realtime,true);assert.equal(status.body.claimTtlMs,300000);
const alice=(await request('/api/cloud/session',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({name:'Alice'})})).body;
const bob=(await request('/api/cloud/session',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({name:'Bob'})})).body;
const puzzle=starterPuzzle(),b0=boardMeta('B0',0,111,puzzle),b1=boardMeta('B1',2,222,puzzle);
const bootstrap=await request('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify({baseRevision:0,global:{schema:31,appVersion:'47.70',generatorVersion:5,worldGeneration:'v47-field-reset-20260728-interaction-fix',nextId:2},metas:[b0,b1],states:[{id:'B0',value:{paths:[],solved:false,rev:1,revAuthor:'alice'}},{id:'B1',value:{paths:[],solved:false,rev:1,revAuthor:'alice'}}],deleted:[]})});
assert.equal(bootstrap.response.status,200);assert.equal(bootstrap.body.revision,1);
aliceRealtime=await connectRealtime(base,alice);bobRealtime=await connectRealtime(base,bob);
aliceRealtime.send({type:'viewport',minX:-8,minY:-8,maxX:8,maxY:8});bobRealtime.send({type:'viewport',minX:-8,minY:-8,maxX:8,maxY:8});
await aliceRealtime.waitFor('snapshot');await bobRealtime.waitFor('snapshot');
aliceRealtime.send({type:'cursor',x:.25,y:.5,cursorStyle:'default'});
const cursor=await bobRealtime.waitFor(message=>message.type==='cursor'&&message.playerId===alice.playerId);
assert.equal(cursor.name,'Alice');assert.equal(cursor.x,.25);assert.equal(cursor.y,.5);assert.equal(cursor.cursorStyle,'default');
aliceRealtime.send({type:'cursor-hide'});await bobRealtime.waitFor(message=>message.type==='player-left'&&message.presenceId===aliceRealtime.ready.presenceId);
await sleep(55);aliceRealtime.send({type:'cursor',x:.25,y:.5,cursorStyle:'default'});await bobRealtime.waitFor(message=>message.type==='cursor'&&message.playerId===alice.playerId);
aliceRealtime.send({type:'claim',requestId:'alice-b0',boardId:'B0'});
const aliceClaim=await aliceRealtime.waitFor(message=>message.type==='claim-result'&&message.requestId==='alice-b0');assert.equal(aliceClaim.ok,true);assert.equal(aliceClaim.claim.boardId,'B0');
const observedClaim=await bobRealtime.waitFor(message=>message.type==='claim'&&message.claim?.boardId==='B0');assert.equal(observedClaim.claim.playerName,'Alice');
bobRealtime.send({type:'claim',requestId:'bob-b0',boardId:'B0'});
const denied=await bobRealtime.waitFor(message=>message.type==='claim-result'&&message.requestId==='bob-b0');assert.equal(denied.ok,false);assert.equal(denied.reason,'occupied');
const rejectedClear=await request('/api/cloud/push',{method:'POST',headers:auth(bob),body:JSON.stringify({baseRevision:1,global:{nextId:2},metas:[],states:[{id:'B0',value:solvedState(puzzle)}],deleted:[]})});
assert.equal(rejectedClear.response.status,423);assert.match(rejectedClear.body.error,/claim/i);
const clearPromise=bobRealtime.waitFor(message=>message.type==='board-cleared'&&message.event?.id==='B0');
const releasePromise=bobRealtime.waitFor(message=>message.type==='claim-release'&&message.boardId==='B0'&&message.reason==='cleared');
const cleared=await request('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify({baseRevision:1,global:{nextId:2,lastSolveAt:Date.now()},metas:[],states:[{id:'B0',value:solvedState(puzzle)}],deleted:[]})});
assert.equal(cleared.response.status,200);assert.equal(cleared.body.clearEvents[0].playerName,'Alice');
const clearEvent=await clearPromise;assert.equal(clearEvent.event.level,1);await releasePromise;
const profilePromise=bobRealtime.waitFor(message=>message.type==='player-profile'&&message.playerId===alice.playerId);
const renamed=await request('/api/cloud/profile',{method:'POST',headers:auth(alice),body:JSON.stringify({name:'Alice 2'})});assert.equal(renamed.response.status,200);assert.equal((await profilePromise).name,'Alice 2');
aliceRealtime.send({type:'claim',requestId:'alice-b1',boardId:'B1'});
const b1Claim=await aliceRealtime.waitFor(message=>message.type==='claim-result'&&message.requestId==='alice-b1');assert.equal(b1Claim.ok,true);
await bobRealtime.waitFor(message=>message.type==='claim'&&message.claim?.boardId==='B1');
aliceRealtime.close();aliceRealtime=null;
await bobRealtime.waitFor(message=>message.type==='player-left'&&message.playerId===alice.playerId);
bobRealtime.send({type:'claim',requestId:'bob-b1-after-disconnect',boardId:'B1'});
const retained=await bobRealtime.waitFor(message=>message.type==='claim-result'&&message.requestId==='bob-b1-after-disconnect');assert.equal(retained.ok,false);assert.equal(retained.reason,'occupied');
console.log('BEND FIELD realtime phase 2 smoke test passed');
})().catch(error=>{console.error(error);if(stderr)console.error(stderr);process.exitCode=1}).finally(()=>{aliceRealtime?.close();bobRealtime?.close();child.kill('SIGTERM');fs.rmSync(dataDir,{recursive:true,force:true})});

24
test/reset-smoke-test.js Normal file
View file

@ -0,0 +1,24 @@
'use strict';
const {vm,app,assert,functionSource}=require('./helpers/app-source');
const removed=[],deleted=[];
const staleSessionJournal='bend-field:v30:v47-field-reset-20260727:journal:stale-session',retired=[
{schema:31,generation:'v47-field-reset-20260728-bugfix'},
{schema:30,generation:'v47-field-reset-20260727'}
];
const context={
RETIRED_WORLD_STORES:retired,
safeLocalRemove:key=>{removed.push(key);return true},
safeLocalKeys:prefix=>staleSessionJournal.startsWith(prefix)?[staleSessionJournal]:[],
indexedDB:{deleteDatabase:name=>deleted.push(name)}
};
vm.createContext(context);
vm.runInContext(`${functionSource('deleteRetiredWorldData')}\nthis.run=deleteRetiredWorldData;`,context);
context.run();
for(const item of retired){
const prefix=`bend-field:v${item.schema}:${item.generation}`;
for(const suffix of[':compact',':recovery',':journal',':revision',':signal',':world:lease'])assert(removed.includes(prefix+suffix),`Old save ${suffix} was not removed`);
assert(deleted.includes(`${prefix}:world`),'Old IndexedDB was not deleted');
}
assert(removed.includes(staleSessionJournal),'Old per-session recovery journal was not removed');
assert(app.includes("SAVE_SCHEMA=31,STORAGE_SCHEMA=30,IDB_LAYOUT_VERSION=8,FIELD_STORAGE_FORMAT=2,GAMEPLAY_DATA_VERSION=3,WORLD_GENERATION='v47-field-reset-20260728-interaction-fix'"),'v47.36 interaction-fix field reset generation is not active');
console.log('Full field reset test passed');

16
test/run-all.js Normal file
View file

@ -0,0 +1,16 @@
'use strict';
const path=require('path');
const fs=require('fs');
const {execFileSync}=require('child_process');
const tests=[
'source-smoke-test.js','v4771-ui-input-smoke-test.js','v4772-ownership-reaction-name-smoke-test.js',
'v4774-drag-overview-smoke-test.js','v4775-pan-cursor-performance-smoke-test.js','v4776-frame-pipeline-smoke-test.js','v4777-hud-input-performance-smoke-test.js','v4778-interaction-scheduler-smoke-test.js','cleanup-performance-smoke-test.js','field-persistence-smoke-test.js','field-save-load-v2-smoke-test.js','gameplay-simplification-smoke-test.js','economy-simulation-test.js','performance-smoke-test.js','mirror-chunk-smoke-test.js','storage-smoke-test.js','save-pipeline-smoke-test.js','concurrency-smoke-test.js','stage34-smoke-test.js',
'expansion-repair-smoke-test.js','expansion-smoke-test.js','interaction-smoke-test.js','anomaly-smoke-test.js','special-cell-smoke-test.js','reset-smoke-test.js','shared-world-client-smoke-test.js','phase2-source-smoke-test.js','realtime-lease-unit-test.js','realtime-phase2-smoke-test.js','server-smoke-test.js','shared-world-complete-smoke-test.js','security-authority-smoke-test.js'
];
for(const file of tests)execFileSync(process.execPath,[path.join(__dirname,file)],{stdio:'inherit'});
const browserPath=process.env.BEND_FIELD_BROWSER_PATH||process.env.BEND_FIELD_EDGE_PATH||(process.platform==='win32'?'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe':'/usr/bin/chromium');
if(process.env.BEND_FIELD_RUN_BROWSER_TESTS==='1'){
if(!fs.existsSync(browserPath))throw new Error(`No supported browser was found at ${browserPath}`);
execFileSync(process.execPath,[path.join(__dirname,'browser-performance-benchmark.js')],{stdio:'inherit',env:{...process.env,BEND_FIELD_BROWSER_PATH:browserPath,BEND_FIELD_EDGE_PATH:browserPath,BEND_FIELD_BENCHMARK_PROFILE:'small'}});
execFileSync(process.execPath,[path.join(__dirname,'browser-field-storage-benchmark.js')],{stdio:'inherit',env:{...process.env,BEND_FIELD_BROWSER_PATH:browserPath,BEND_FIELD_SCALE_SIZES:'1000'}});
}else console.warn('Real-browser tests are opt-in. Set BEND_FIELD_RUN_BROWSER_TESTS=1 or run the benchmark scripts directly.');

View file

@ -0,0 +1,143 @@
'use strict';
const {vm,app,assert,functionSource}=require('./helpers/app-source');
for(const marker of ['GATE_ARROW','legacyStorageKeys','saveFailed','minimapTransform','generatorMode','hydrateRemaining','boardAtGlobalCell','areaDifficultyShift','plannedExpansionFrontiers','metaIsVisible','mergeLatestFromStorage','shopBtn','shopCountEl']){
assert(!app.includes(marker),`Dead marker remains: ${marker}`);
}
const saveSource=functionSource('save'),persistSource=functionSource('persistDirtyToDb'),
pullCloudSource=functionSource('pullCloudWorld'),pushCloudSource=functionSource('pushCloudPending');
assert(!saveSource.includes('writeCompactMirror'),'save() still writes the full mirror before persistence');
assert((persistSource.match(/snapshotForStorage\(/g)||[]).length===1&&(persistSource.match(/writeCompactMirror\(/g)||[]).length===1,'IndexedDB persistence still builds or writes a full mirror');
assert(persistSource.includes('scheduleMirrorCheckpoint()'),'Successful IndexedDB persistence does not schedule an idle recovery checkpoint');
assert(!app.includes("pagehide',()=>{try{writeCompactMirror")&&!app.includes("visibilitychange',()=>{if(document.visibilityState==='hidden')try{writeCompactMirror"),'Lifecycle handlers still rebuild the mirror directly');
assert(pullCloudSource.indexOf('cloudProfileIdentity()!==profileIdentity')<pullCloudSource.indexOf('data.cloudRevision='),'Cloud pull mutates revision before confirming the active profile');
assert(pushCloudSource.includes('mergeCloudPending(cloudPushPending,pending)')&&pushCloudSource.indexOf('cloudProfileIdentity()!==profileIdentity')<pushCloudSource.indexOf('acknowledgeCloudPending('),'Cloud push acknowledges work before confirming the active profile');
{
const batchContext={};vm.createContext(batchContext);vm.runInContext(`${functionSource('emptyCloudPending')}\n${functionSource('takeCloudPendingBatch')}\nthis.takeCloudPendingBatch=takeCloudPendingBatch;`,batchContext);
const source={metaIds:new Set(Array.from({length:600},(_,index)=>`B${index}`)),stateIds:new Set(['B0']),deleted:new Set(['B999']),globalChanged:true},
{batch,remainder}=batchContext.takeCloudPendingBatch(source,512);
assert(batch.metaIds.size===512&&remainder.metaIds.size===88&&remainder.stateIds.has('B0')&&remainder.deleted.has('B999')&&batch.globalChanged,'Cloud change batching lost or exceeded work');
}
const writes={meta:0,state:0,global:0,outbox:0,recovery:0,mirror:0,snapshot:0,checkpoint:0,revision:0,journalClear:0,signal:0,cloud:0};
const recoveryWrites=[],journalClears=[];
const request=result=>({result});
const stores={
control:{get:()=>request({key:'active',activeFormat:2,activeEpoch:'world:test-epoch'})},
worlds:{get:()=>request({epoch:'world:test-epoch',status:'active',createdAt:1,source:{kind:'fresh'},global:null}),put:()=>writes.global++},
boardIndex:{put:()=>{},delete:()=>{},get:()=>request(null)},boardPuzzles:{put:()=>writes.meta++,delete:()=>{},get:()=>request(null)},boardStates:{put:()=>writes.state++,delete:()=>{},get:()=>request(null)},
outboxV2:{put:()=>writes.outbox++,delete:()=>{}},recoveryV2:{put:row=>{writes.recovery++;recoveryWrites.push(row)},delete:()=>{}},tombstonesV2:{put:()=>{},delete:()=>{},get:()=>request(null)}
};
const context={
console,sessionId:'test-session',lastRevision:100,
data:{worldEpoch:'world:test-epoch',globalRev:10,globalRevAuthor:'test-session',metas:{B0:{id:'B0',rev:7,revAuthor:'test-session'}},states:{B0:{rev:9,revAuthor:'test-session'}},updatedAt:0,clockFloor:0},
dirtyMetaIds:new Set(['B0']),dirtyStateIds:new Set(['B0']),deletedBoardIds:new Set(),deletedBoardRevisions:new Map(),
deletedBoardAuthors:new Map(),
recoveryJournalsToCover:[{sessionId:'prior-session',seq:6,_storageKey:'journal:prior'}],recoveryJournalSeq:3,
cloudOutboxDeleteKeys:new Set(),cloudJournalMetaIds:new Set(),cloudJournalStateIds:new Set(),cloudJournalDeletedIds:new Set(),cloudApiEnabled:false,
globalDirty:true,globalChangeSeq:3,cloudJournalGlobalChanged:true,worldSignalSeq:0,idbAvailable:true,activeStorageFormat:2,FIELD_STORAGE_FORMAT:2,storageAccessError:null,storageKey:'save',
pruneAndCount:()=>{},hasPendingPersistence:()=>context.globalDirty||context.dirtyMetaIds.size>0||context.dirtyStateIds.size>0||context.deletedBoardIds.size>0,
validWorldEpoch:value=>typeof value==='string'&&value.startsWith('world:'),storedWorldEpoch:()=>null,createWorldEpoch:()=> 'world:test-epoch',rememberWorldEpoch:()=>true,
revisionVersion:value=>({rev:value?.rev||0,revAuthor:value?.revAuthor||value?.author||''}),compareRevisionVersions:(a,b)=>(a?.rev||0)-(b?.rev||0)||String(a?.revAuthor||a?.author||'').localeCompare(String(b?.revAuthor||b?.author||'')),newerRevisionValue:(a,b)=>((a?.rev||0)>=(b?.rev||0)?a:b),
trustedNow:()=>123456,
globalForStorage:(source,updatedAt)=>({worldEpoch:source.worldEpoch,globalRev:source.globalRev,globalRevAuthor:source.globalRevAuthor,updatedAt,clockFloor:updatedAt}),
mergeGlobalRecords:(_stored,incoming)=>incoming,applyGlobalRecordToData:record=>Object.assign(context.data,record),staleWorldEpochError:()=>Object.assign(new Error('stale'),{code:'STALE_WORLD_EPOCH'}),
metaForStorage:value=>({...value}),stateForStorage:value=>({...value}),
metaRowsForStorage:ids=>ids.map(id=>context.data.metas[id]&&({...context.data.metas[id]})).filter(Boolean),
stateRowsForStorage:ids=>ids.map(id=>context.data.states[id]&&({id,value:{...context.data.states[id]}})).filter(Boolean),
snapshotForStorage:updatedAt=>{writes.snapshot++;return{updatedAt}},
openWorldDb:async()=>({transaction:()=>({objectStore:name=>stores[name],abort(){}})}),transactionDone:async()=>{},requestValue:async req=>req.result,
recoveryWalPromise:Promise.resolve(),normalizeMeta:(_id,value)=>value,normalizeState:value=>value,normalizedStateObjects:new WeakSet(),
boardIndexSummaries:new Map(),
metaFromV2Records:()=>null,stateFromV2Record:()=>null,puzzleRecordV2:value=>value,stateRecordV2:(_id,value)=>value,summarizeBoardV2:value=>value,
fieldBoundsFromMetas:()=>({minX:0,minY:0,maxX:1,maxY:1}),SAVE_SCHEMA:31,WORLD_GENERATION:'current-only-test',
writeCompactMirror:snapshot=>{assert(snapshot.updatedAt===123456,'Mirror did not use the captured persistence snapshot');writes.mirror++;return true},safeLocalSet:()=>{writes.mirror++;return true},
scheduleMirrorCheckpoint:()=>writes.checkpoint++,
updateStorageRevision:()=>writes.revision++,clearRecoveryJournalIfCovered:(seq,covered)=>{writes.journalClear++;journalClears.push({seq,covered})},
broadcastWorldSignal:()=>writes.signal++,scheduleCloudPush:()=>writes.cloud++,
perfStart:()=>0,perfEnd:()=>0,perfGauge:()=>{},deepClone:value=>JSON.parse(JSON.stringify(value)),resetHistory:[],invalidateStoreEffectCache:()=>{},statsDirty:false
};
vm.createContext(context);
vm.runInContext(`${persistSource}\nthis.persistDirtyToDb=persistDirtyToDb;`,context);
(async()=>{
const first=await context.persistDirtyToDb();
assert(!first.skipped,'Dirty save was skipped');
assert(writes.meta===1&&writes.state===1&&writes.global===1,'Incremental database stores were not written once');
assert(writes.snapshot===0&&writes.mirror===0,'Ordinary IndexedDB persistence built or synchronously wrote a full mirror');
assert(writes.checkpoint===1,'Ordinary IndexedDB persistence did not schedule one idle mirror checkpoint');
assert(recoveryWrites.some(row=>row.key==='covered:test-session'&&row.value.seq===3)&&recoveryWrites.some(row=>row.key==='covered:prior-session'&&row.value.seq===6),'Persistence did not atomically write current and recovered-session coverage markers');
assert(journalClears.length===1&&journalClears[0].seq===3&&journalClears[0].covered[0]._storageKey==='journal:prior','Committed recovery journals were not passed to cleanup after persistence');
assert(context.dirtyMetaIds.size===0&&context.dirtyStateIds.size===0&&!context.globalDirty,'Dirty state was not cleared after persistence');
const second=await context.persistDirtyToDb();
assert(second.skipped,'Unchanged persistence did not exit early');
assert(writes.snapshot===0&&writes.mirror===0&&writes.checkpoint===1&&writes.global===1,'Unchanged persistence performed additional work');
context.deletedBoardIds.add('B9');context.deletedBoardRevisions.set('B9',10);
context.transactionDone=async()=>{context.deletedBoardRevisions.set('B9',11)};
const deletionCommit=await context.persistDirtyToDb();
assert(!deletionCommit.skipped&&context.deletedBoardIds.has('B9')&&context.deletedBoardRevisions.get('B9')===11,'A newer deletion tombstone was cleared by an older in-flight commit');
context.dirtyMetaIds.add('B0');context.dirtyStateIds.add('B0');context.globalDirty=true;context.globalChangeSeq++;
const beforeFailure={mirror:writes.mirror,snapshot:writes.snapshot,checkpoint:writes.checkpoint,signal:writes.signal,cloud:writes.cloud};
context.transactionDone=async()=>{throw new Error('transaction failed')};
let failed=false;try{await context.persistDirtyToDb()}catch(_){failed=true}
assert(failed,'Database failure was swallowed');
assert(context.dirtyMetaIds.has('B0')&&context.dirtyStateIds.has('B0')&&context.globalDirty,'Database failure cleared dirty state');
assert(writes.mirror===beforeFailure.mirror&&writes.snapshot===beforeFailure.snapshot&&writes.checkpoint===beforeFailure.checkpoint&&writes.signal===beforeFailure.signal&&writes.cloud===beforeFailure.cloud,'Database failure published or checkpointed an uncommitted save');
context.idbAvailable=false;context.transactionDone=async()=>{};context.writeCompactMirror=()=>false;context.storageAccessError=new Error('mirror failed');
failed=false;try{await context.persistDirtyToDb()}catch(_){failed=true}
assert(failed,'Mirror-only storage failure was swallowed');
assert(writes.snapshot===1,'Mirror-only storage did not build its required fallback snapshot');
assert(context.dirtyMetaIds.has('B0')&&context.dirtyStateIds.has('B0')&&context.globalDirty,'Mirror failure cleared dirty state');
let scheduled=0,immediate=0;
const saveContext={
Promise,SAVE_DELAY:180,saveTimer:null,lifecyclePersistenceSuppressed:false,
hasPendingPersistence:()=>false,persistNow:()=>{immediate++;return Promise.resolve(true)},
setSaveStatus:()=>{},clearTimeout:()=>{},setTimeout:()=>{scheduled++;return 1}
};
vm.createContext(saveContext);vm.runInContext(`${saveSource}\nthis.save=save;`,saveContext);
assert(saveContext.save()===true&&scheduled===0,'No-op save scheduled work');
await saveContext.save(true);assert(immediate===0,'No-op immediate save invoked persistence');
saveContext.hasPendingPersistence=()=>true;
saveContext.save();assert(scheduled===1,'Dirty deferred save was not scheduled');
await saveContext.save(true);assert(immediate===1,'Dirty immediate save did not persist');
let checkpointPersists=0;
const checkpointTimers=[],checkpointContext={
cloudCheckpointRetryTimer:0,hasPendingPersistence:()=>true,persistNow:async()=>{checkpointPersists++;return false},
setTimeout:(callback,delay)=>{checkpointTimers.push({callback,delay});return checkpointTimers.length}
};
vm.createContext(checkpointContext);vm.runInContext(`${functionSource('scheduleCloudCheckpointRetry')}\nthis.scheduleCloudCheckpointRetry=scheduleCloudCheckpointRetry;`,checkpointContext);
checkpointContext.scheduleCloudCheckpointRetry();checkpointContext.scheduleCloudCheckpointRetry();
assert(checkpointTimers.length===1&&checkpointTimers[0].delay===1000,'Cloud checkpoint retries were not coalesced behind one timer');
checkpointTimers[0].callback();await Promise.resolve();await Promise.resolve();
assert(checkpointPersists===1&&checkpointTimers.length===2,'Failed cloud checkpoint was not retried after the guarded persistence attempt');
let profileRetrySchedules=0;
const profileContext={
data:{cloudProfile:null,cloudRevision:9},serverClockOffset:null,Date,
fetchJson:async()=>({playerId:'a'.repeat(16),token:'b'.repeat(32),serverTime:Date.now()}),
markGlobalDirty:()=>{},persistNow:async()=>false,scheduleCloudCheckpointRetry:()=>profileRetrySchedules++
};
vm.createContext(profileContext);vm.runInContext(`${functionSource('createCloudProfile')}\nthis.createCloudProfile=createCloudProfile;`,profileContext);
let profileFailed=false;try{await profileContext.createCloudProfile()}catch(_){profileFailed=true}
assert(profileFailed&&profileRetrySchedules===1&&profileContext.data.cloudProfile?.playerId==='a'.repeat(16),'Uncommitted cloud profile was treated as a completed checkpoint');
let acknowledged=0,armedDelay=null;
const pending={metaIds:new Set(['B0']),stateIds:new Set(),deleted:new Set(),globalChanged:false};
const cloudContext={
data:{cloudProfile:{playerId:'a'.repeat(16),token:'b'.repeat(32)},cloudRevision:5,metas:{B0:{id:'B0',rev:7}},states:{}},
cloudAvailable:true,cloudSyncing:false,cloudPushTimer:null,cloudPushPending:pending,cloudJournalChangeSeq:1,serverClockOffset:null,
clearTimeout:()=>{},setCloudStatus:()=>{},sharedWorldGlobalSignature:()=>"world",sharedWorldGlobalForCloud:()=>({}),lastCloudWorldGlobalSignature:"old",applyCloudEnvelope:()=>{},cloudRowsForStorage:async ids=>({metas:[...ids].map(id=>({id,rev:7})),states:[]}),
cloudAuthHeaders:()=>({}),fetchJson:async()=>{cloudContext.data.cloudProfile={playerId:'c'.repeat(16),token:'d'.repeat(32)};return{revision:6,serverTime:Date.now()}},
acknowledgeCloudPending:()=>acknowledged++,markGlobalDirty:()=>{},persistNow:async()=>true,scheduleCloudCheckpointRetry:()=>{},
pullCloudWorld:async()=>true,armCloudPush:delay=>{armedDelay=delay;return true},console:{warn:()=>{}},Date
};
vm.createContext(cloudContext);
vm.runInContext(`${functionSource('emptyCloudPending')}\n${functionSource('mergeCloudPending')}\n${functionSource('takeCloudPendingBatch')}\n${functionSource('cloudPendingHasWork')}\n${functionSource('cloudProfileIdentity')}\n${pushCloudSource}\nthis.pushCloudPending=pushCloudPending;`,cloudContext);
const profileChangedResult=await cloudContext.pushCloudPending();
assert(profileChangedResult===false&&acknowledged===0&&cloudContext.data.cloudRevision===5,'Cloud response from an obsolete profile was acknowledged');
assert(cloudContext.cloudPushPending.metaIds.has('B0')&&armedDelay===5000,'Profile-switch guard did not restore and defer pending cloud work');
console.log('Save pipeline cleanup test passed');
})().catch(error=>{console.error(error);process.exitCode=1});

View file

@ -0,0 +1,14 @@
'use strict';
const {spawn}=require('child_process');const fs=require('fs');const os=require('os');const path=require('path');const assert=require('assert/strict');
const {root,starterPuzzle}=require('./helpers/app-source');const {connectRealtime}=require('./helpers/realtime-client');
const port=32000+Math.floor(Math.random()*2000),dataDir=fs.mkdtempSync(path.join(os.tmpdir(),'bend-auth-')),child=spawn(process.execPath,[path.join(root,'server.js')],{env:{...process.env,PORT:String(port),HOST:'127.0.0.1',BEND_FIELD_DATA_DIR:dataDir},stdio:['ignore','pipe','pipe']});let stderr='',ws;child.stderr.on('data',c=>stderr+=c);const base=`http://127.0.0.1:${port}`,sleep=ms=>new Promise(r=>setTimeout(r,ms));
async function req(url,opt={}){const response=await fetch(base+url,opt);return{response,body:await response.json()}}function auth(s){return{authorization:`Bearer ${s.playerId}.${s.token}`,'content-type':'application/json'}}
function meta(id,x,seed,p){return{id,x,y:0,chunks:[[0,0]],level:1,targetLevel:1,seed,axis:'MIX',sealedSides:[],puzzle:p,rev:1,revAuthor:'x'}}
(async()=>{for(let i=0;i<100;i++){try{if((await req('/api/cloud/status')).response.ok)break}catch{}await sleep(30)}const alice=(await req('/api/cloud/session',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({name:'Alice'})})).body,p=starterPuzzle(),b0=meta('B0',0,15,p);
let r=await req('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify({baseRevision:0,global:{nextId:1},metas:[b0],states:[{id:'B0',value:{paths:[],solved:false}}]})});assert.equal(r.response.status,200);ws=await connectRealtime(base,alice);ws.send({type:'viewport',minX:-5,minY:-5,maxX:5,maxY:5});await ws.waitFor('snapshot');ws.send({type:'claim',requestId:'c',boardId:'B0'});assert.equal((await ws.waitFor(m=>m.type==='claim-result'&&m.requestId==='c')).ok,true);
const solved={paths:p.solution.map(q=>({startGate:q.startGate,endGate:q.endGate,cells:q.cells})),solved:true,scoreAwarded:Number.MAX_SAFE_INTEGER,store:{pathIndex:0,cellIndex:0,itemIds:['level-min-10'],priceCoefficient:0,purchases:[]}};
r=await req('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify({baseRevision:1,global:{nextId:1},metas:[],states:[{id:'B0',value:solved}]})});assert.equal(r.response.status,200);const pulled=await req('/api/cloud/pull?since=0',{headers:auth(alice)}),state=pulled.body.page.states.B0;assert(state.scoreAwarded>0&&state.scoreAwarded<100000,'server must replace forged reward');assert(state.store&&state.store.priceCoefficient>=.8&&state.store.priceCoefficient<=1.2,'server must replace forged store pricing');assert.equal(state.store.itemIds.length,13);
const econ=(await req('/api/player/state',{headers:auth(alice)})).body.player;assert.equal(econ.earnedScore,state.scoreAwarded);assert.equal(econ.availableScore,state.scoreAwarded);
const forged=meta('B1',999999,123,p);r=await req('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify({baseRevision:2,global:{nextId:2},metas:[forged],states:[{id:'B1',value:{paths:[],solved:false}}]})});assert.notEqual(r.response.status,200,'non-adjacent forged board must be rejected');
console.log('Server authority security smoke test passed');
})().catch(e=>{console.error(e);if(stderr)console.error(stderr);process.exitCode=1}).finally(()=>{ws?.close();child.kill('SIGTERM');fs.rmSync(dataDir,{recursive:true,force:true})});

69
test/server-smoke-test.js Normal file
View file

@ -0,0 +1,69 @@
'use strict';
const {spawn}=require('child_process');
const fs=require('fs');
const os=require('os');
const path=require('path');
const assert=require('assert/strict');
const {root,starterPuzzle}=require('./helpers/app-source');
const {connectRealtime}=require('./helpers/realtime-client');
const port=19000+Math.floor(Math.random()*10000);
const dataDir=fs.mkdtempSync(path.join(os.tmpdir(),'bend-field-shared-world-'));
const child=spawn(process.execPath,[path.join(root,'server.js')],{
env:{...process.env,PORT:String(port),HOST:'127.0.0.1',BEND_FIELD_DATA_DIR:dataDir},stdio:['ignore','pipe','pipe']
});
let stderr='';child.stderr.on('data',chunk=>stderr+=chunk);
const base=`http://127.0.0.1:${port}`;
const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));
let aliceRealtime=null;
async function request(url,options={}){
const response=await fetch(base+url,options),body=await response.json();
return{response,body};
}
async function requestText(url){const response=await fetch(base+url),body=await response.text();return{response,body}}
function auth(session){return{authorization:`Bearer ${session.playerId}.${session.token}`,'content-type':'application/json'}}
function boardMeta(id,x,seed,puzzle){return{id,x,y:0,chunks:[[0,0]],level:1,targetLevel:1,seed,axis:'MIX',sealedSides:[],puzzle,rev:1,revAuthor:'client'}}
function solvedState(puzzle){return{paths:puzzle.solution.map(route=>({startGate:route.startGate,endGate:route.endGate,cells:route.cells.map(cell=>[...cell])})),solved:true,expanded:false,rev:2,revAuthor:'client'}}
(async()=>{
for(let i=0;i<60;i++){try{const {response}=await request('/api/cloud/status');if(response.ok)break}catch(_){}await sleep(50)}
const status=await request('/api/cloud/status');assert.equal(status.response.status,200);assert.equal(status.body.sharedWorld,true);
const page=await requestText('/');assert.equal(page.response.status,200);assert(page.body.indexOf('app-logic.js?v=47.77')>page.body.indexOf('puzzle-core.js?v=47.77-5')&&page.body.indexOf('app-logic.js?v=47.77')<page.body.indexOf('app.js?v=47.77'));
const logicAsset=await requestText('/app-logic.js');assert.equal(logicAsset.response.status,200);assert.match(logicAsset.body,/BendAppLogic/);
const alice=(await request('/api/cloud/session',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({name:'Alice'})})).body;
const bob=(await request('/api/cloud/session',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({name:'Bob'})})).body;
for(const session of[alice,bob]){assert.match(session.playerId,/^[a-f0-9]{16,64}$/);assert.match(session.token,/^[a-f0-9]{32,128}$/)}
assert.equal(alice.name,'Alice');assert.equal(bob.name,'Bob');
const starter=starterPuzzle(),b0=boardMeta('B0',0,123456,starter);
const bootstrap={baseRevision:0,global:{schema:31,appVersion:'47.70',generatorVersion:5,worldGeneration:'v47-field-reset-20260728-interaction-fix',nextId:1,score:999,cursorStyle:'do-not-share',cloudProfile:{token:'do-not-store'}},metas:[b0],states:[{id:'B0',value:{paths:[],solved:false,rev:1,revAuthor:'alice'}}],deleted:[]};
const pushed=await request('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify(bootstrap)});assert.equal(pushed.response.status,200);assert.equal(pushed.body.revision,1);assert.deepEqual(pushed.body.clearEvents,[]);
const bobInitial=await request('/api/cloud/pull?since=0&eventsSince=0',{headers:auth(bob)});assert.equal(bobInitial.response.status,200);assert.equal(bobInitial.body.changed,true);assert.equal(bobInitial.body.fullSnapshot,true);assert.equal(bobInitial.body.page.metas.B0.seed,123456);assert.equal(bobInitial.body.page.states.B0.solved,false);assert(bobInitial.body.page.metas.B0.rev>1_000_000_000_000);assert(bobInitial.body.page.states.B0.rev>1_000_000_000_000);assert.equal(bobInitial.body.page.global.score,undefined);assert.equal(bobInitial.body.page.global.cursorStyle,undefined);assert.equal(bobInitial.body.page.global.cloudProfile,null);assert.equal(bobInitial.body.player.name,'Bob');
aliceRealtime=await connectRealtime(base,alice);aliceRealtime.send({type:'viewport',minX:-10,minY:-10,maxX:10,maxY:10});await aliceRealtime.waitFor('snapshot');aliceRealtime.send({type:'claim',requestId:'server-smoke-claim',boardId:'B0'});const claimResult=await aliceRealtime.waitFor(message=>message.type==='claim-result'&&message.requestId==='server-smoke-claim');assert.equal(claimResult.ok,true);
const clearPayload={baseRevision:1,global:{nextId:1,lastSolveAt:Date.now()},metas:[],states:[{id:'B0',value:solvedState(starter)}],deleted:[]};
const cleared=await request('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify(clearPayload)});assert.equal(cleared.response.status,200);assert.equal(cleared.body.revision,2);assert.equal(cleared.body.clearEvents.length,1);assert.equal(cleared.body.clearEvents[0].playerName,'Alice');assert.equal(cleared.body.clearEvents[0].id,'B0');assert.equal(cleared.body.clearEvents[0].level,1);
const bobAfterClear=await request('/api/cloud/pull?since=1&eventsSince=0',{headers:auth(bob)});assert.equal(bobAfterClear.response.status,200);assert.equal(bobAfterClear.body.fullSnapshot,false);assert.equal(bobAfterClear.body.page.states.B0.solved,true);assert.equal(bobAfterClear.body.page.states.B0.solvedBy,'Alice');assert.equal(bobAfterClear.body.page.states.B0.solvedById,alice.playerId);assert(Number.isFinite(bobAfterClear.body.page.states.B0.solvedAt));assert.equal(bobAfterClear.body.clearEvents.length,1);assert.equal(bobAfterClear.body.clearEvents[0].playerName,'Alice');
const stale=await request('/api/cloud/push',{method:'POST',headers:auth(bob),body:JSON.stringify({...clearPayload,baseRevision:1})});assert.equal(stale.response.status,409);assert.equal(stale.body.revision,2);
const repeat=await request('/api/cloud/push',{method:'POST',headers:auth(bob),body:JSON.stringify({...clearPayload,baseRevision:2})});assert.equal(repeat.response.status,200);assert.equal(repeat.body.revision,3);assert.deepEqual(repeat.body.clearEvents,[]);
const preserved=await request('/api/cloud/pull?since=0&eventsSince=0',{headers:auth(bob)});assert.equal(preserved.body.page.states.B0.solvedBy,'Alice');
const renamed=await request('/api/cloud/profile',{method:'POST',headers:auth(bob),body:JSON.stringify({name:'Bob 2'})});assert.equal(renamed.response.status,200);assert.equal(renamed.body.name,'Bob 2');
const unchanged=await request('/api/cloud/pull?since=3&eventsSince=2',{headers:auth(bob)});assert.equal(unchanged.body.changed,false);assert.equal(unchanged.body.player.name,'Bob 2');
const b1=boardMeta('B1',1,987654,starter);
const generated=await request('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify({baseRevision:3,global:{nextId:2},metas:[b1],states:[{id:'B1',value:{paths:[],solved:false,rev:4,revAuthor:'alice'}}],deleted:[]})});assert.equal(generated.response.status,200);assert.equal(generated.body.revision,4);
const bobGenerated=await request('/api/cloud/pull?since=3&eventsSince=2',{headers:auth(bob)});assert.equal(bobGenerated.response.status,200);assert.equal(bobGenerated.body.page.metas.B1.seed,987654);assert.equal(bobGenerated.body.page.states.B1.solved,false);
const invalid=await request('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify({baseRevision:4,global:{},metas:[{...b1,id:'B2',x:2,chunks:[]}],states:[],deleted:[]})});assert.equal(invalid.response.status,400);
const overlap=await request('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify({baseRevision:4,global:{},metas:[boardMeta('B2',1,222,starter)],states:[],deleted:[]})});assert.equal(overlap.response.status,400);
const aliceRecord=JSON.parse(fs.readFileSync(path.join(dataDir,`${alice.playerId}.json`),'utf8'));assert.equal(aliceRecord.name,'Alice');assert.notEqual(aliceRecord.tokenHash,alice.token);assert.equal(aliceRecord.boardVersions,undefined);assert.equal(aliceRecord.global,undefined);
const world=JSON.parse(fs.readFileSync(path.join(dataDir,'shared-world.json'),'utf8'));assert.equal(world.revision,4);assert.equal(world.global.score,undefined);assert.equal(world.global.cursorStyle,undefined);assert.equal(world.boardVersions.B0,3);assert.equal(world.boardVersions.B1,4);assert.equal(world.clearEvents[0].playerName,'Alice');
const b0Shard=JSON.parse(fs.readFileSync(path.join(dataDir,'shared-world.boards','B0.3.json'),'utf8'));assert.equal(b0Shard.state.solvedBy,'Alice');assert.equal(b0Shard.state.paths.length,starter.solution.length);
console.log('BEND FIELD shared-world phase 2 server smoke test passed');
})().catch(error=>{console.error(error);if(stderr)console.error(stderr);process.exitCode=1}).finally(()=>{aliceRealtime?.close();child.kill('SIGTERM');fs.rmSync(dataDir,{recursive:true,force:true})});

View file

@ -0,0 +1,72 @@
'use strict';
const assert=require('assert/strict');
const vm=require('vm');
const {functionSource,loadAppLogic}=require('./helpers/app-source');
const outboxContext={
data:{states:{B0:{solved:false},B1:{solved:true}}},cloudApiEnabled:true,
cloudJournalMetaIds:new Set(),cloudJournalStateIds:new Set(),cloudJournalDeletedIds:new Set(),cloudOutboxDeleteKeys:new Set(),cloudJournalChangeSeq:0,cloudJournalGlobalChanged:false
};
vm.createContext(outboxContext);
vm.runInContext(`${functionSource('currentCloudPending')}\n${functionSource('noteCloudRow')}\nthis.logic={currentCloudPending,noteCloudRow};`,outboxContext);
outboxContext.logic.noteCloudRow('state','B0');
assert.equal(outboxContext.cloudJournalStateIds.has('B0'),false,'Unsolved personal path entered the shared journal');
assert.equal(outboxContext.cloudOutboxDeleteKeys.has('state:B0'),true,'Stale unsolved shared outbox row was not scheduled for deletion');
outboxContext.logic.noteCloudRow('state','B1');
assert.equal(outboxContext.cloudJournalStateIds.has('B1'),true,'Solved state was not added to the shared journal');
assert.deepEqual([...outboxContext.logic.currentCloudPending().stateIds],['B1']);
const AppLogic=loadAppLogic(),now=1_800_000_000_000;
const leaseContext={
cloudAvailable:true,data:{cloudProfile:{playerId:'bbbbbbbbbbbbbbbb'}},currentPlayerId:()=> 'bbbbbbbbbbbbbbbb',trustedNow:()=>now,AppLogic,
SHARED_EXPANSION_GRACE_MS:60_000,SHARED_EXPANSION_JITTER_MS:30_000
};
vm.createContext(leaseContext);
vm.runInContext(`${functionSource('sharedExpansionRepairDelay')}\nthis.sharedExpansionRepairDelay=sharedExpansionRepairDelay;`,leaseContext);
assert.equal(leaseContext.sharedExpansionRepairDelay({solved:true,solvedById:'bbbbbbbbbbbbbbbb',solvedAt:now}),0,'The solving client cannot expand its own clear');
const wait=leaseContext.sharedExpansionRepairDelay({solved:true,solvedById:'aaaaaaaaaaaaaaaa',solvedAt:now});
assert(wait>=60_000&&wait<90_000,'A non-solving client can race the solver before the recovery grace period');
assert.equal(leaseContext.sharedExpansionRepairDelay({solved:true,solvedById:'aaaaaaaaaaaaaaaa',solvedAt:now-100_000}),0,'A disconnected solver can leave expansion permanently blocked');
leaseContext.cloudAvailable=false;
assert.equal(leaseContext.sharedExpansionRepairDelay({solved:true,solvedById:'aaaaaaaaaaaaaaaa',solvedAt:now}),0,'Offline expansion was incorrectly lease-gated');
const localMeta={id:'B0',x:99,y:99,seed:1,chunks:[[0,0]],sealedSides:[],rev:9_999};
const remoteMeta={id:'B0',x:0,y:0,seed:2,chunks:[[0,0]],sealedSides:[],rev:2_000};
const authoritativeContext={
data:{
metas:{B0:localMeta},states:{B0:{solved:true,solvedBy:'Local',paths:[{cells:[[0,0]]}],rev:9_999}},
nextId:99,solved:1,lastSolveAt:123,specialMechanicsSeen:['warp'],quarantine:{local:true},
playerName:'Player',score:777,cursorStyle:'flag-jp'
},
isPlainObject:value=>value&&typeof value==='object'&&!Array.isArray(value),lastRevision:0,normalizedStateObjects:new Set(),
cloudJournalMetaIds:new Set(['B0']),cloudJournalStateIds:new Set(['B0']),cloudJournalDeletedIds:new Set(),cloudOutboxDeleteKeys:new Set(),
sameMetaGeometry:(a,b)=>a.x===b.x&&a.y===b.y&&a.seed===b.seed&&JSON.stringify(a.chunks)===JSON.stringify(b.chunks)&&JSON.stringify(a.sealedSides||[])===JSON.stringify(b.sealedSides||[]),
destroyBoard:()=>{},destroyStaticBoard:()=>{},rendered:new Map(),staticRendered:new Map(),deepClone:value=>JSON.parse(JSON.stringify(value)),
compareRevisionVersions:(a,b)=>(a?.rev||0)-(b?.rev||0),
mergeBoardStates:(current,incoming)=>{
if(current?.solved&&!incoming?.solved)return JSON.parse(JSON.stringify(incoming));
if(current&&!current.solved&&!incoming.solved)return{...JSON.parse(JSON.stringify(incoming)),paths:JSON.parse(JSON.stringify(current.paths||[]))};
return JSON.parse(JSON.stringify(incoming));
},
markStateDirty:id=>authoritativeContext.dirtyStateIds.add(id),dirtyStateIds:new Set(),sanitizeStateForPuzzle:()=>{},
mergeGlobalFields:()=>{throw new Error('Authoritative shared global unexpectedly used generic merge')},resolveMergedOverlaps:()=>[],statsDirty:false
};
vm.createContext(authoritativeContext);
vm.runInContext(`${functionSource('clearSharedWorldJournalRow')}\n${functionSource('applyAuthoritativeSharedGlobal')}\n${functionSource('mergeSnapshotIntoData')}\nthis.mergeSnapshotIntoData=mergeSnapshotIntoData;`,authoritativeContext);
authoritativeContext.mergeSnapshotIntoData({
metas:{B0:remoteMeta},states:{B0:{solved:false,paths:[],rev:2_000}},
nextId:2,solved:0,lastSolveAt:0,specialMechanicsSeen:['lock'],quarantine:{shared:true}
},{finalize:false,authoritativeWorld:true});
assert.equal(authoritativeContext.data.metas.B0.seed,2,'An existing local world row overrode the authoritative shared board');
assert.equal(authoritativeContext.data.states.B0.solved,false,'An unconfirmed local clear survived authoritative shared adoption');
assert.equal(authoritativeContext.data.nextId,2,'A private local board counter leaked into the shared world');
assert.equal(authoritativeContext.data.playerName,'Player');assert.equal(authoritativeContext.data.score,777);assert.equal(authoritativeContext.data.cursorStyle,'flag-jp');
assert.equal(authoritativeContext.cloudJournalMetaIds.size,0);assert.equal(authoritativeContext.cloudJournalStateIds.size,0);
assert(authoritativeContext.cloudOutboxDeleteKeys.has('meta:B0')&&authoritativeContext.cloudOutboxDeleteKeys.has('state:B0'),'Adopted shared rows did not clear stale local outbox records');
// Matching unsolved boards keep the player's unfinished line locally while the board definition stays shared.
authoritativeContext.data.metas.B0=remoteMeta;
authoritativeContext.data.states.B0={solved:false,paths:[{startGate:0,cells:[[0,0],[0,1]]}],rev:3_000};
authoritativeContext.mergeSnapshotIntoData({metas:{B0:{...remoteMeta,rev:4_000}},states:{B0:{solved:false,paths:[],rev:4_000}},nextId:2},{finalize:false,authoritativeWorld:true});
assert.equal(authoritativeContext.data.states.B0.paths.length,1,'Authoritative shared refresh erased a matching board\'s personal unfinished path');
console.log('Shared-world phase 1 client synchronization smoke test passed');

View file

@ -0,0 +1,47 @@
'use strict';
const {spawn}=require('child_process');
const fs=require('fs');
const os=require('os');
const path=require('path');
const assert=require('assert/strict');
const {root,starterPuzzle,read,functionSource}=require('./helpers/app-source');
const {connectRealtime}=require('./helpers/realtime-client');
const app=read('app.js'),html=read('index.html'),css=read('style.css'),serverSource=read('server.js'),realtimeSource=read('realtime-server.js');
assert(html.includes('id="reactionCanvas"')&&html.includes('id="reactionRadial"')&&css.includes('#reactionCanvas')&&css.includes('z-index:2'),'Reaction layer is not behind boards');
assert(functionSource('beginReactionGesture').includes('REACTION_LONG_PRESS_MS')&&functionSource('reactionAllowedAt').includes('metaState(boardId).solved'),'Reaction click/long-press eligibility is missing');
assert(functionSource('drawReactionLayer').includes('REALTIME_REACTION_DURATION')||app.includes('REALTIME_REACTION_DURATION=4500'),'Reaction animation is not bounded');
assert(functionSource('inventoryEntries').includes('onlinePlayerEconomy()')&&functionSource('purchaseStoreItem').includes('buyPersonalStoreItem'),'Player inventory or personal purchasing is not server-backed');
assert(serverSource.includes("url.pathname==='/api/player/purchase'")&&serverSource.includes('assertPlayerCanAfford')&&!serverSource.includes("url.pathname==='/api/player/place-field'"),'Player purchase validation is missing or retired field-placement API remains');
assert(realtimeSource.includes("message.type === 'reaction'")&&realtimeSource.includes('REACTION_MIN_INTERVAL_MS')&&!realtimeSource.includes('broadcastFieldEffect'),'Realtime reaction throttling is missing or field broadcasts remain');
const port=24000+Math.floor(Math.random()*8000),dataDir=fs.mkdtempSync(path.join(os.tmpdir(),'bend-field-complete-'));
const child=spawn(process.execPath,[path.join(root,'server.js')],{env:{...process.env,PORT:String(port),HOST:'127.0.0.1',BEND_FIELD_DATA_DIR:dataDir},stdio:['ignore','pipe','pipe']});
let stderr='',aliceWs=null,bobWs=null;child.stderr.on('data',chunk=>stderr+=chunk);
const base=`http://127.0.0.1:${port}`,sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));
async function request(url,options={}){const response=await fetch(base+url,options),body=await response.json();return{response,body}}
function auth(session){return{authorization:`Bearer ${session.playerId}.${session.token}`,'content-type':'application/json'}}
function boardMeta(id,x,seed,puzzle){return{id,x,y:0,chunks:[[0,0]],level:1,targetLevel:1,seed,axis:'MIX',sealedSides:[],puzzle,rev:1,revAuthor:'client'}}
function solvedState(puzzle){return{paths:puzzle.solution.map(route=>({startGate:route.startGate,endGate:route.endGate,cells:route.cells.map(cell=>[...cell])})),solved:true,expanded:false,scoreAwarded:100000,scoreVersion:6,store:{owner:'',pathIndex:0,cellIndex:0,itemIds:['score-lens'],purchases:[],priceVersion:1,priceCoefficient:1},rev:2,revAuthor:'client'}}
(async()=>{
for(let i=0;i<100;i++){try{if((await request('/api/cloud/status')).response.ok)break}catch(_){}await sleep(40)}
const status=(await request('/api/cloud/status')).body;assert.equal(status.reactions,true);assert.equal(status.playerEconomy,true);assert.equal(status.sharedItems,false);
const alice=(await request('/api/cloud/session',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({name:'Alice'})})).body;
const bob=(await request('/api/cloud/session',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({name:'Bob'})})).body;
const puzzle=starterPuzzle(),b0=boardMeta('B0',0,15,puzzle);
let pushed=await request('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify({baseRevision:0,global:{schema:31,appVersion:'47.70',generatorVersion:5,worldGeneration:'v47-field-reset-20260728-interaction-fix',nextId:1},metas:[b0],states:[{id:'B0',value:{paths:[],solved:false,rev:1,revAuthor:'alice'}}],deleted:[]})});assert.equal(pushed.response.status,200);
aliceWs=await connectRealtime(base,alice);bobWs=await connectRealtime(base,bob);aliceWs.send({type:'viewport',minX:-8,minY:-8,maxX:8,maxY:8});bobWs.send({type:'viewport',minX:-8,minY:-8,maxX:8,maxY:8});await aliceWs.waitFor('snapshot');await bobWs.waitFor('snapshot');
aliceWs.send({type:'reaction',id:'reaction-one',emoji:'🎉',x:.4,y:.6});const reaction=await bobWs.waitFor(message=>message.type==='reaction'&&message.reaction?.id==='reaction-one');assert.equal(reaction.reaction.emoji,'🎉');assert.equal(reaction.reaction.playerName,'Alice');assert(reaction.reaction.expiresAt>reaction.reaction.createdAt);
aliceWs.send({type:'claim',requestId:'claim-b0',boardId:'B0'});assert.equal((await aliceWs.waitFor(message=>message.type==='claim-result'&&message.requestId==='claim-b0')).ok,true);
pushed=await request('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify({baseRevision:1,global:{nextId:1},metas:[],states:[{id:'B0',value:solvedState(puzzle)}],deleted:[]})});assert.equal(pushed.response.status,200);let revision=pushed.body.revision;
const alicePath=path.join(dataDir,`${alice.playerId}.json`),aliceRecord=JSON.parse(fs.readFileSync(alicePath,'utf8'));aliceRecord.earnedScore=250000;fs.writeFileSync(alicePath,JSON.stringify(aliceRecord));
const purchase=await request('/api/player/purchase',{method:'POST',headers:auth(alice),body:JSON.stringify({boardId:'B0',itemId:'score-lens'})});assert.equal(purchase.response.status,201);assert.equal(purchase.body.player.purchases.length,1);assert.equal(purchase.body.purchase.itemId,'score-lens');
const duplicate=await request('/api/player/purchase',{method:'POST',headers:auth(alice),body:JSON.stringify({boardId:'B0',itemId:'score-lens'})});assert.equal(duplicate.response.status,200);assert.equal(duplicate.body.purchase.purchaseId,purchase.body.purchase.purchaseId);
const bobState=await request('/api/player/state',{headers:auth(bob)});assert.deepEqual(bobState.body.player.purchases,[]);
const retiredPurchase=await request('/api/player/purchase',{method:'POST',headers:auth(alice),body:JSON.stringify({boardId:'B0',itemId:'level-min-10'})});assert.equal(retiredPurchase.response.status,400);
const retiredPlacement=await request('/api/player/place-field',{method:'POST',headers:auth(alice),body:JSON.stringify({boardId:'B0',itemId:'level-min-10',x:4.25,y:-2.5})});assert.equal(retiredPlacement.response.status,404);
const pull=await request('/api/cloud/pull?since=0&eventsSince=0',{headers:auth(bob)});assert.equal(pull.response.status,200);assert.equal(pull.body.page.global.fieldEffects,undefined);
assert(!app.includes('level-min-10')&&!app.includes('level-max-10')&&!app.includes('fieldOverlayCanvas'),'Retired difficulty items remain in the client bundle');
console.log('Shared-world reactions and personal item economy smoke test passed');
})().catch(error=>{console.error(error);if(stderr)console.error(stderr);process.exitCode=1}).finally(()=>{aliceWs?.close();bobWs?.close();child.kill('SIGTERM');fs.rmSync(dataDir,{recursive:true,force:true})});

163
test/source-smoke-test.js Normal file
View file

@ -0,0 +1,163 @@
'use strict';
const cp=require('child_process');
const fs=require('fs');
const {root,path,vm,app,html,css,worker,appLogicSource,assert,functionSource,loadBendPuzzle,loadAppLogic,starterPuzzle,read}=require('./helpers/app-source');
const serverSource=read('server.js');
const packageVersion=JSON.parse(read('package.json')).version,appVersion=packageVersion.split('.').slice(0,2).join('.');
for(const file of ['app.js','app-logic.js','puzzle-core.js','puzzle-worker.js','field-persistence.js','field-persistence-worker.js','server.js'])cp.execFileSync(process.execPath,['--check',path.join(root,file)],{stdio:'inherit'});
for(const marker of [
`APP_VERSION='${appVersion}',SAVE_SCHEMA=31,STORAGE_SCHEMA=30,IDB_LAYOUT_VERSION=8,FIELD_STORAGE_FORMAT=2,GAMEPLAY_DATA_VERSION=3,WORLD_GENERATION='v47-field-reset-20260728-interaction-fix'`,
'SPECIAL_CELL_MIN_LEVEL=5,SPECIAL_CELL_DEBUG_ALL_LEVELS=false',
'shapeCandidatesForLevel','addSpecialCellPattern','addWarpSpecial','addLockSpecial','addCrossingSpecial',
'specialPathValid','crossingsSatisfied','activateCrossing','pathRenderSegments','pathStrokePieces','pathProgressAtCell','specialCellInfoMap','normalizeSpecialCells'
])assert(app.includes(marker),`Missing v47 marker: ${marker}`);
assert(app.includes('const AppLogic=globalThis.BendAppLogic')&&app.includes('AppLogic.shapeCandidatesForLevel')&&app.includes('AppLogic.stateForStorage')&&app.includes('AppLogic.collectConnectedLineComponent'),'Application does not consume the shared pure-logic module');
assert(html.includes(`style.css?v=${appVersion}`)&&html.includes(`puzzle-core.js?v=${appVersion}-5`)&&html.includes(`app-logic.js?v=${appVersion}`)&&html.includes(`field-persistence.js?v=${appVersion}`)&&html.includes(`app.js?v=${appVersion}`),'Web assets do not match package/app/generator version');
assert(worker.includes(`puzzle-core.js?v=${appVersion}-5`),'Worker imports an old puzzle-core asset');
assert(functionSource('createPuzzleWorker').includes('message.error?job.reject')&&!functionSource('createPuzzleWorker').includes('message.error?generatePuzzleOnMainThread'),'Algorithmic worker failures are retried redundantly on the main thread');
assert(html.includes(`<small>v${appVersion}</small>`)&&html.includes(`v${appVersion}</title>`),'Visible version does not match package/app version');
assert(html.includes('id="viewport" tabindex="-1"')&&['modal','storeModal','inventoryModal','timeAttackModal'].every(id=>html.includes(`id="${id}" aria-hidden="true" inert`)),'Hidden dialogs are not inert or the viewport is not programmatically focusable');
assert(functionSource('closeDialogRoot').includes('focusOutsideDialog(root,preferredFocus)')&&functionSource('closeDialogRoot').indexOf('focusOutsideDialog')<functionSource('closeDialogRoot').indexOf("setAttribute('aria-hidden','true')"),'Dialog hiding occurs before focus leaves the dialog');
assert(functionSource('openDialogRoot').includes('setDialogInert(root,false)')&&functionSource('closeDialogRoot').includes('setDialogInert(root,true)'),'Dialog inert state is not synchronized with visibility');
assert(!app.includes('labyrinth-seed')&&!app.includes('giantCompactShapes')&&!app.includes("anomaly==='giant'"),'Retired special multi-section boards remain active');
assert(!app.includes('anomalyAt')&&!app.includes('anomalyScoreMultiplier')&&!app.includes('renderAnomalyOverlays')&&!css.includes('.anomaly'),'Retired anomaly code remains');
assert(!serverSource.includes('migrateLegacyPlayer')&&!serverSource.includes('value.metas')&&!serverSource.includes('value.states'),'Cloud server still reads or migrates the retired monolithic player format');
assert(html.includes('id="clearFeed"')&&css.includes('.clear-feed-item'),'Shared clear feed is missing above the minimap');
assert(serverSource.includes("const WORLD_FILE = path.join(DATA_DIR, 'shared-world.json')")&&serverSource.includes("url.pathname==='/api/cloud/profile'")&&serverSource.includes('withWorldQueue')&&serverSource.includes('clearEvents'),'Shared-world storage, profile naming, serialization, or clear feed API is missing');
assert(functionSource('checkSolvedAndExpand').indexOf('pullCloudWorld(true)')<functionSource('checkSolvedAndExpand').indexOf('st.solved=true')&&functionSource('checkSolvedAndExpand').indexOf('pushCloudPending()')<functionSource('checkSolvedAndExpand').indexOf('expandMeta('),'Clear publication is not server-validated before shared expansion');
assert(functionSource('mergeSnapshotIntoData').includes('authoritativeWorld')&&functionSource('pullCloudWorld').includes('initial&&previousRevision===0&&targetRevision>0')&&functionSource('clearSharedWorldJournalRow').includes('cloudOutboxDeleteKeys'),'Initial shared-world adoption does not replace stale local world rows or clean their outbox entries');
assert(functionSource('noteCloudRow').includes("kind==='state'&&data?.states?.[id]?.solved!==true")&&functionSource('currentCloudPending').includes("solved===true"),'Unfinished personal paths can still enter the shared durable outbox');
assert(functionSource('sharedExpansionRepairDelay').includes('SHARED_EXPANSION_GRACE_MS')&&functionSource('repairExpansions').includes('sharedExpansionRepairDelay(st)<=0'),'Non-solving clients can race the solver while publishing newly generated boards');
assert(app.includes('solvedById')&&appLogicSource.includes('solvedById')&&serverSource.includes('state.solvedById=player.playerId'),'Shared solver identity is not persisted independently of the display name');
assert(serverSource.includes('rowRevision=Math.max')&&serverSource.includes('serverTime()*1000'),'Server row revisions are not comparable with client revisions');
assert(!app.includes('nearestUnselectedEndpointAtClient'),'Unselected endpoint clicks are still intercepted before dragging');
assert(functionSource('extendPointerTo').includes('renderDragFrame(b)')&&functionSource('renderDragFrame').includes('liveEndpointPoint')&&!functionSource('renderDragFrame').includes('pathValid')&&!app.includes('path-live-tail'),'Pointer-following line does not use the lightweight live-endpoint renderer');
assert(!app.includes('pathChangeMotion')&&!app.includes('renderDragTipMotion')&&!css.includes('.path-growth')&&!css.includes('.path-retraction'),'Line growth or retraction animation remains');
assert(css.includes('.turn-count{fill:#fff;font-family:var(--dot-font)'),'Turn counts do not explicitly use the dot-styled game font');
assert(functionSource('addWarpSpecial').includes('invalidateSpecialCellCaches'),'Warp insertion does not invalidate turn-analysis caches');
assert(functionSource('specialCellInfoMap').includes('description')&&functionSource('specialInfoFromEvent').includes('b.specialInfo')&&html.includes('id="specialTooltip"'),'Special-cell hover descriptions are missing');
for(const selector of ['.special-cross','.special-warp','.special-key','.special-door'])assert(css.includes(selector),`Missing special-cell style ${selector}`);
assert(functionSource('makeSpecialMarker').includes("class:'key-ring'")&&!functionSource('makeSpecialMarker').includes(String.raw`textContent='\u9375'`),'Key special cell still uses a kanji glyph');
assert(css.includes('.board-card.solved .special-cell-layer'),'Solved boards do not hide special cells');
assert(functionSource('selectBoard').includes('setActiveBoard(b.id)')&&functionSource('setActiveBoard').includes('previous.drawing?.pointerId==null'),'Board selection does not preserve an active pointer draw while reconciling inactive keyboard state');
assert(!css.includes('.board-card:not(.active)'),'Unselected-board styling remains active');
assert(css.includes('.board-card.hud-current:not(.solved) .board-label')&&functionSource('boardPlayHudVisible').includes('hudBoardId===b?.id')&&functionSource('renderBoardNow').includes("card.classList.toggle('hud-current',hudVisible)"),'Board HUD is not limited to an actively played board');
assert(functionSource('gateFromCell').includes('maxPixels')&&functionSource('extendPointerTo').includes('active.startGate,20'),'Opposite gate selection is not distance-limited');
assert(functionSource('renderBoardNow').includes('pathStrokePieces(segments,startColor,endColor)')&&functionSource('pathColorAtCell').includes('pathProgressAtCell'),'Line colors are not blended along cumulative route length');
assert(functionSource('updateSelectedProgress').includes('b.meta.level')&&!functionSource('updateSelectedProgress').includes('filled'),'Top HUD includes information other than level');
assert(functionSource('renderBoardNow').includes('label.replaceChildren')&&functionSource('makeBoard').includes('label.append(boardActions)'),'Board HUD does not contain the level and attached actions');
assert(html.includes('id="noiseCanvas" width="80" height="64"')&&functionSource('paintNoiseBackground').includes("perfCount('noiseFrames')")&&!css.includes('starTwinkle'),'Low-resolution noise background is missing or the retired starfield remains');
assert(functionSource('unresolvedExpansionCandidates').includes('gateFrontierCandidates(meta)')&&!functionSource('unresolvedExpansionCandidates').includes('frontierCandidates(meta)'),'Normal expansion still creates non-gate frontier boards');
assert(functionSource('syncBoundaryConnections').includes('boundaryColorSource(meta,gi,hit)'),'Connected gate colors are not canonicalized');
assert(!app.includes('level-min-10')&&!app.includes('level-max-10')&&!app.includes('fieldEffects')&&!app.includes('fieldOverlayCanvas')&&!serverSource.includes('/api/player/place-field'),'Difficulty adjustment items or their field implementation remain active');
assert(functionSource('makeBoard').includes('cellShape')&&functionSource('makeBoard').includes('boardCellsPath')&&!functionSource('makeBoard').includes('cellHits'),'Detailed boards do not use compound SVG paths or still allocate per-cell hit nodes');
assert(functionSource('desiredInteractiveBoardIds').includes('for(const id of ids)')&&!app.includes('INTERACTIVE_DETAIL_CELL_BUDGET'),'Visible puzzles are not all selected for detailed rendering');
assert(functionSource('shouldTeleportToUnsolvedBoard').includes('return false')&&!app.includes('function promoteStaticBoard(')&&!functionSource('bindBoard').includes('centerMeta('),'Board input can still promote a summary or teleport the camera');
assert(html.includes('id="fpsCounter"')&&functionSource('refreshFpsCounter').includes('FPS ${fpsLastValue}')&&app.includes('AUXILIARY_FPS=30')&&app.includes('DRAG_TARGET_FPS=60'),'Lightweight FPS display or split interaction budgets are missing');
assert(css.includes('.gem-particle{position:fixed')&&css.includes('width:28px;height:28px')&&functionSource('completionEffect').includes('1800'),'Completion gems are not enlarged or retained long enough');
assert(css.includes('#customEmojiCursor{')&&css.includes('overflow:visible')&&css.includes('#customEmojiCursor.flag-cursor{width:32px;height:32px;overflow:hidden'),'Non-flag custom cursors are still clipped or flag clipping is no longer isolated');
assert(functionSource('extendOne').includes('warpedDuringExtend=true')&&!functionSource('extendOne').includes('safeRelease(b.svg,pointerId)'),'Warp traversal still releases pointer capture');
assert(functionSource('extendPointerTo').includes('pointerOffset'),'Warp continuation does not remap the pointer to the exit');
assert(!app.includes('hysteresisDragPoint')&&!app.includes('DRAG_AXIS_LOCK_DISTANCE')&&!app.includes('POINTER_SNAP_RELEASE'),'Retired drawing hysteresis remains active');
assert(functionSource('scheduleBoardDragFrame').includes('requestAnimationFrame')&&functionSource('queueBoardPointerMove').includes('scheduleBoardDragFrame')&&functionSource('bindBoard').includes('queueBoardPointerMove')&&functionSource('processBoardDragFrame').includes('const move=b.pendingPointerMove'),'Pointer movement is not frame-batched with ordered samples');
assert(functionSource('queueCameraInteraction').includes('requestAnimationFrame')&&functionSource('movePan').includes('queueCameraInteraction'),'Camera panning is not frame-coalesced');
assert(functionSource('leftFieldPanAllowed').includes("classList?.contains('solved')")&&functionSource('beginPan').includes('e.button!==2&&!leftFieldPanAllowed(e)'),'Left-drag field panning is not isolated to solved or undiscovered space');
assert(functionSource('processBoardDragFrame').includes('edgePanVelocity')&&functionSource('processBoardDragFrame').includes('applyCamera(true)'),'Drag edge auto-pan is missing');
assert(!functionSource('makeBoard').includes('darkness')&&!css.includes('.darkness'),'Retired darkness rendering remains');
assert(functionSource('positionBoardLabel').includes('hudPlacementCandidates')&&functionSource('positionBoardLabel').includes("placement.side==='S'"),'HUD does not move to a free edge');
assert(functionSource('positionBoardLabel').includes("b.label.style.width=innerWidth+'px'")&&functionSource('positionBoardLabel').includes("b.label.style.top=(PAD-22)+'px'"),'Top/bottom HUD does not span the board edge or clear upper gates');
assert(functionSource('gateHitBox').includes("side==='N'")&&functionSource('makeBoard').includes('...gateHitBox(gp,g.side)'),'Gate hit areas are not constrained to the owning board');
assert(!app.includes('sharedGateVisible'),'No-op shared-gate visibility wrapper remains');
assert(functionSource('isSolved').includes('crossingsSatisfied(st,p)'),'Crossing is not a prerequisite for normal board completion');
assert(functionSource('crossingsSatisfied').includes('crossingStateAtCell')&&functionSource('activateCrossing').includes('path.cells.push(cell)'),'Crossing is not derived from the live overlapping line state');
assert(functionSource('extendOne').includes('lockForDoor')&&functionSource('extendOne').includes('pathHasLockKey'),'Door traversal does not require the same line to touch the key');
assert(functionSource('extendOne').includes('warpPairForCell')&&functionSource('extendOne').includes('path.cells.push(cell,[...warpExit])'),'Warp traversal does not move to its paired cell');
assert(functionSource('resetSelectedBoard').includes('specialProgress={crossings:[]}'),'Reset does not clear crossing progress');
assert(functionSource('resetSelectedBoard').includes('renderBoardNow(b)')&&!functionSource('resetSelectedBoard').includes('syncBoundaryConnections'),'Reset is not immediate or still recreates inherited routes');
assert(!app.toLowerCase().includes('undolastreset')&&!html.toLowerCase().includes('undo'),'Visible reset undo remains active');
assert(app.includes('STORE_CHANCE=1/30'),'Store appearance rate is not 1/30');
assert(app.includes('MINIMAP_VIEW_CHUNKS_X=42'),'Minimap does not use the wider scale');
assert(app.includes('SOUND_GAIN_MULTIPLIER=3.6')&&functionSource('soundTone').includes('Math.min(.28'),'Sound effects were not amplified');
assert(app.includes('UNIQUE_SOLUTION_MIN_LEVEL=6')&&functionSource('placeChildAtFrontierAttempt').includes('level>=UNIQUE_SOLUTION_MIN_LEVEL'),'Unique-solution selection does not begin at level 6');
assert(functionSource('shapeCandidatesForArea').includes('nearbyShapeFamilyCounts')&&appLogicSource.includes('generatedShapeFamilyKey')&&appLogicSource.includes('balancedShapeCandidates'),'Area-local board shape balancing is missing');
assert(!html.includes('&#x77E2;&#x5370;&#x30AD;&#x30FC;&#xFF1A;&#x7DDA;&#x3092;&#x4F38;&#x3070;&#x3059;')&&css.includes('.control-chips span{padding:9px 12px')&&css.includes('font-size:13px'),'Help controls are too small or still list arrow keys');
assert(css.includes('.path.invalid{stroke:#cfa2a7')&&css.includes('.unfilled-warning-cells{fill:#3b3430')&&!css.includes('.num.turn-warning{fill:#ffbd69;animation'),'Rule warnings remain overly aggressive');
assert(functionSource('addCrossingSpecial').includes('neighbors.some(candidate=>!valid.has')&&functionSource('addCrossingSpecial').includes('gateCells.has(key)'),'Crossing cells can still appear on a board edge or gate cell');
assert(functionSource('updateZoomPresentation').includes('world-overview')&&functionSource('drawWorldOverview').includes('overviewCanvas')&&html.includes('id="overviewCanvas"')&&css.includes('#viewport.canvas-overview #world'),'Canvas overview mode is missing');
assert(!functionSource('drawWorldOverview').includes('drawWorldOverviewPaths')&&functionSource('rebuildWorldOverviewCache').includes('drawMapLongLines'),'Far zoom does not use the minimap long-line renderer');
assert(functionSource('rebuildWorldOverviewCache').includes('drawMapBoardCells')&&functionSource('rebuildWorldOverviewCache').includes('drawMapLongLines')&&!app.includes('function drawWorldOverviewShops('),'Zoomed-out rendering does not share the minimap renderer');
assert(functionSource('makeStaticBoard').includes('renderedConnectedLineWidth(meta,index)')&&!functionSource('makeStaticBoard').includes('state.solved')&&css.includes('.static-summary-path{'),'Unsolved nearby boards do not preserve route thickness');
assert(functionSource('updateTimeAttackUi').includes("classList.toggle('starting'")&&css.includes('@keyframes timeAttackStartEmphasis'),'Time-attack start clock emphasis is missing');
const yellowFaces=app.match(/const YELLOW_FACE_CURSOR_SOURCE=`([\s\S]*?)`;/)?.[1]?.split('\n')||[];
assert(yellowFaces.length===101&&yellowFaces.some(row=>row.startsWith('1FAE9|'))&&yellowFaces.some(row=>row.startsWith('1FAEA|')),'Complete Unicode Emoji 17.0 yellow-face cursor catalog is missing');
assert(app.includes('MAX_FACE_CURSOR_PRICE=50000')&&app.includes('cost=MIN_CURSOR_PRICE+((index*61+37)%100)*MIN_CURSOR_PRICE')&&functionSource('storeItemPrice').includes('Math.min(MAX_FACE_CURSOR_PRICE,adjusted)'),'Yellow-face cursor prices are not deterministic random values spanning 500-50000 gems');
const flagCodes=app.match(/const FLAG_REGION_CODES=`([^`]+)`\.split\(' '\)/)?.[1]?.split(' ')||[];
assert(flagCodes.length===259&&new Set(flagCodes).size===259&&app.includes("['gbeng','England'],['gbsct','Scotland'],['gbwls','Wales']"),'Complete Unicode Emoji 17.0 flag cursor catalog is missing');
const oecdCodes=app.match(/const OECD_FLAG_CODES=new Set\('([^']+)'\.split\(' '\)\)/)?.[1]?.split(' ')||[];
assert(oecdCodes.length===38&&new Set(oecdCodes).size===38&&app.includes('OECD_FLAG_CURSOR_BASE_PRICE=20000')&&app.includes('FLAG_CURSOR_BASE_PRICE=10000'),'Flag cursor base prices or the 38-country OECD tier are missing');
const flagAssetDir=path.join(root,'assets','flags'),flagAssets=fs.readdirSync(flagAssetDir).filter(name=>name.endsWith('.svg'));
assert(flagAssets.length===262&&fs.existsSync(path.join(flagAssetDir,'LICENSE-TWEMOJI.txt')),'Bundled cross-platform flag SVG catalog or attribution is incomplete');
assert(functionSource('syncCursorAppearance').includes("image.src=selected.flagAsset")&&functionSource('setItemIcon').includes("image.src=item.flagAsset")&&functionSource('syncCursorAppearance').includes('nativeSupported')&&css.includes('cursor:none!important')&&css.includes('#customEmojiCursor.flag-cursor{width:32px;height:32px;overflow:hidden')&&css.includes('border-radius:50%'),'Native/DOM SVG-backed circular flag cursor rendering is missing');
assert(functionSource('buildDragCache').includes('endpoint-cursor-image')&&functionSource('buildDragCache').includes('DRAG_FLAG_CLIP_RADIUS')&&functionSource('buildDragCache').includes('x:-DRAG_FLAG_CURSOR_SIZE/2')&&functionSource('buildDragCache').includes("class:'drag-tip-group'")&&functionSource('updateDragCursorDesign').includes('activeCustomCursorItem')&&functionSource('renderDragFrame').includes('cache.tipGroup.style.transform')&&functionSource('bindBoard').includes('queueMicrotask(()=>updateCustomCursorFromPointer(e))')&&!css.includes('body.is-drawing #customEmojiCursor{display:none!important}'),'Grabbed cursor sizing, centering, clipping, or smooth cursor continuity is missing');
assert(css.includes('@font-face{font-family:"DotGothic16Local"')&&css.includes('--emoji-font:')&&css.includes('body,button,input,select,textarea{font-family:var(--dot-font)}')&&!css.includes(':root{--dot-font:"DotGothic16"')&&html.includes('id="customEmojiCursor"'),'Bundled Japanese dot font is overridden or emoji-specific isolation is missing');
assert(!html.includes('&#x6240;&#x6301;&#x30B8;&#x30A7;&#x30E0;')&&!functionSource('completionEffect').includes('ジェム')&&!functionSource('updateScoreLensBadge').includes('予想ジェム'),'Standalone gem terminology remains in the reward UI');
assert(functionSource('renderStorePanel').includes('storeInventoryItems(meta,store)')&&functionSource('purchaseStoreItem').includes('storeInventoryItems(meta,store).some'),'Store UI or purchase validation bypasses its seeded thirteen-item inventory');
assert(functionSource('seededStoreItemIds').includes('.slice(0,12)')&&functionSource('seededStoreItemIds').includes('.slice(0,1)')&&functionSource('maybeOpenStore').includes('itemIds:seededStoreItemIds(meta.seed)'),'Stores do not persist twelve seeded cursors and one seeded non-cursor item');
assert(functionSource('renderStorePanel').includes("{title:'アイテム'")&&functionSource('renderStorePanel').includes("{title:'カーソル'")&&functionSource('renderStorePanel').includes('if(category.cursor)card.append(icon,buy)')&&css.includes('.store-cursor-list{grid-template-columns:repeat(6'),'Shop is not split into item and horizontal twelve-cursor sections');
assert(functionSource('renderInventoryPanel').includes("'inventory-cursor-grid'")&&functionSource('renderInventoryPanel').includes("option.classList.toggle('selected'")&&functionSource('useInventoryItemLoaded').includes("previousCursor===item.cursorStyle?'default':item.cursorStyle"),'Persistent click-to-toggle cursor inventory is missing');
assert(functionSource('makeStaticBoard').includes("openStoreMeta(meta)")&&!functionSource('beginPan').includes('overviewShopAtClient'),'Distant overview still exposes a shop-only hit target instead of matching the minimap');
assert(functionSource('discardUnmovedCreatedPath').includes('path.cells.length!==1')&&functionSource('bindBoard').includes('discardUnmovedCreatedPath(b)'),'Cancelled pickup creation can leave an orphan handle');
assert(functionSource('renderDragFrame').includes('refreshDragNumberColors(b)'),'Number colors do not update in the live pickup renderer');
assert(functionSource('detachPathFromStartGate').includes('path.detachedStart=true')&&functionSource('renderBoardNow').includes("'data-endpoint-side':'start'")&&!app.includes('whitePickupEnd')&&!functionSource('finalizeAtGate').includes("'#fff'"),'Two-ended colored pickup support is incomplete');
assert(!functionSource('openTipMergePlan').includes('a.detachedStart||o.detachedStart')&&functionSource('openTipMergePlan').includes('tipDistance!==0')&&!app.includes('function gateConnectionSteps'),'Same-cell pickup joining or exact-cell gate snapping is incomplete');
assert(functionSource('updateScoreLensBadge').includes('projectedScoreForBoard')&&functionSource('scoreLensVisibleForMeta').includes('SCORE_LENS_ZOOM_THRESHOLD')&&functionSource('useInventoryItemLoaded').includes('data.scoreLensEnabled=!previous')&&/id:'score-lens'[^\n]+scoreLens:true[^\n]+icon:/.test(app)&&!app.includes('SCORE_LENS_RADIUS')&&css.includes('.score-lens-badge'),'Persistent ON/OFF score lens display is missing or still asks for a position');
assert(functionSource('renderStorePanel').includes('formatScore(price)')&&!functionSource('renderStorePanel').includes('price-data.score')&&!functionSource('purchaseStoreItem').includes('price-data.score'),'Store buttons do not always show the actual item price');
assert(functionSource('zoomAt').includes('MIN_CAMERA_SCALE'),'Camera cannot zoom out to overview scale');
assert(functionSource('addObstaclePattern').includes('puzzle.difficulty=sourcePuzzle.difficulty'),'Obstacle generation changes the displayed level and section constraint');
assert(functionSource('addObstaclePattern').includes('largePuzzleBoost')&&functionSource('obstacleCellLimit').includes('Math.floor(total*.2)')&&functionSource('addCrossingSpecial').includes('obstacleCellLimit(p)'),'Large-puzzle obstacle scaling or the strict 20% cap is missing');
assert(functionSource('scoreFromThickness').includes('hardMultiplier')&&functionSource('scoreFromThickness').includes('largeMultiplier'),'Hard and large puzzle score scaling is missing');
const storeDescriptions=[...app.slice(app.indexOf('const STORE_ITEM_BASE='),app.indexOf('const YELLOW_FACE_CURSOR_SOURCE=')).matchAll(/description:'([^']*)'/g)].map(match=>match[1].replace(/\\u3002/g,'。'));
assert(storeDescriptions.length===1&&storeDescriptions.every(description=>(description.match(/。/g)||[]).length===1),'Current non-cursor store items are missing or their descriptions are not exactly one sentence');
assert([...appLogicSource].every(ch=>ch.charCodeAt(0)<128),'app-logic.js contains non-ASCII source');
const BendPuzzle=loadBendPuzzle(),AppLogic=loadAppLogic();
assert(BendPuzzle?.GENERATOR_VERSION===5,'Generator version mismatch');
const starter=starterPuzzle();assert(BendPuzzle.solverDifficulty(starter)===1,'Bundled origin puzzle is not level 1');
const generatedA=BendPuzzle.generatePuzzle([[0,0]],123456,2,0,0),generatedB=BendPuzzle.generatePuzzle([[0,0]],123456,2,0,0);
assert(JSON.stringify(generatedA)===JSON.stringify(generatedB),'Puzzle generation is not deterministic');
const multi=BendPuzzle.generatePuzzle([[0,0],[1,0],[0,1]],987654,6,12,-9);
assert(multi.valid.length===75,'Multi-section normal puzzle generation failed');
assert(multi.difficulty>5&&multi.n.filter(clue=>clue[2]>=4).length>=Math.ceil(multi.n.length*.25),'A demanding high-level puzzle was incorrectly collapsed to level 5');
assert(multi.complexity.rawRating===BendPuzzle.solutionComplexity(multi).rating&&BendPuzzle.difficultyFitsRegion(multi.difficulty,6),'Raw complexity is not retained or regional difficulty classification escaped its band');
const highCluePuzzle=BendPuzzle.generatePuzzle([[2,0],[0,1],[1,1],[2,1],[0,2],[1,2],[0,3],[1,3]],7496588,9,27,-1);
assert(highCluePuzzle.difficulty>5&&highCluePuzzle.n.filter(clue=>clue[2]>=4).length>=Math.ceil(highCluePuzzle.n.length*.25),'A valid level 6-10 puzzle lacks meaningful bend clues');
assert(multi.solution.some(path=>path.cells.some((cell,index)=>index>0&&(Math.floor(cell[0]/5)!==Math.floor(path.cells[index-1][0]/5)||Math.floor(cell[1]/5)!==Math.floor(path.cells[index-1][1]/5)))),'Normal multi-section puzzle does not connect sections');
const shapesBySize=new Map();for(const shape of BendPuzzle.SHAPES){const size=shape.length;if(!shapesBySize.has(size))shapesBySize.set(size,[]);shapesBySize.get(size).push(shape)}
const shapeDeps={shapesBySize,hash32:BendPuzzle.hash32,rngFrom:BendPuzzle.rngFrom,shuffle:BendPuzzle.shuffle};
const familyA=[[0,0],[1,0],[2,0]],familyARotated=[[0,0],[0,1],[0,2]],familyB=[[0,0],[1,0],[0,1]];
assert(AppLogic.generatedShapeFamilyKey(familyA)===AppLogic.generatedShapeFamilyKey(familyARotated)&&AppLogic.generatedShapeFamilyKey(familyA)!==AppLogic.generatedShapeFamilyKey(familyB),'Shape-family normalization does not combine rotations/reflections correctly');
const balancedFamilies=AppLogic.balancedShapeCandidates([familyA,familyARotated,familyB],12345,{hash32:BendPuzzle.hash32,rngFrom:BendPuzzle.rngFrom,shuffle:BendPuzzle.shuffle}).slice(0,2).map(AppLogic.generatedShapeFamilyKey);
assert(new Set(balancedFamilies).size===2,'Shape balancing still exhausts one orientation-rich family before another family');
for(let level=1;level<=10;level++){
const range=AppLogic.sectionCountRange(level);assert(range.max===level&&range.min===Math.max(1,level-3),`Level ${level} section range is invalid`);
const candidates=AppLogic.shapeCandidatesForLevel(0x470000+level,level,8,shapeDeps);assert(candidates.length>0,`Level ${level} has no section shapes`);
for(const shape of candidates){assert(shape.length>=range.min&&shape.length<=range.max,`Level ${level} generated ${shape.length} sections outside ${range.min}-${range.max}`);const set=new Set(shape.map(([x,y])=>`${x},${y}`));let reached=new Set([`${shape[0][0]},${shape[0][1]}`]),changed=true;while(changed){changed=false;for(const[x,y]of shape)if(!reached.has(`${x},${y}`)&&[[1,0],[-1,0],[0,1],[0,-1]].some(([dx,dy])=>reached.has(`${x+dx},${y+dy}`))){reached.add(`${x},${y}`);changed=true}}assert(reached.size===set.size,'Generated section shape is disconnected')}
}
const normalizeContext={AppLogic,LINE_COLORS:Array(10).fill('#000'),isPlainObject:value=>!!value&&typeof value==='object'&&!Array.isArray(value),ckey:(r,c)=>`${r},${c}`,sameCell:(a,b)=>a[0]===b[0]&&a[1]===b[1],manhattan:(a,b)=>Math.abs(a[0]-b[0])+Math.abs(a[1]-b[1]),solverDifficulty:BendPuzzle.solverDifficulty,deepClone:value=>JSON.parse(JSON.stringify(value))};
vm.createContext(normalizeContext);
vm.runInContext([functionSource('normalizePath'),functionSource('normalizeSpecialCells'),functionSource('repairWarpNumberClues'),functionSource('normalizeStoredPuzzle'),functionSource('puzzleForStorage'),'this.logic={normalizeStoredPuzzle,puzzleForStorage}'].join('\n'),normalizeContext);
const normalized=normalizeContext.logic.normalizeStoredPuzzle(starter,[[0,0]],1);assert(normalized,'Starter fails stored-puzzle validation');
const stored=normalizeContext.logic.puzzleForStorage(normalized);assert(stored.solution.length===starter.solution.length,'Stored puzzle round-trip lost its solution');
assert(functionSource('gateCandidateAtPoint').includes('gateCandidatesInCell')&&functionSource('gateStartCandidate').includes('gateCandidateAtPoint')&&functionSource('bindBoard').includes('gateStartCandidate(b,point,hintCell,directGate)'),'Gate and gate-cell input do not share one selector');
assert(functionSource('placeChildAtFrontierAttempt').includes('puzzleSupportsConnectionRequirements')&&functionSource('placeChildAtFrontierAttempt').includes('fallbackShape=[[0,0]]')&&functionSource('placeChildAtFrontier').includes('frontierGeometryStillViable')&&functionSource('expandMetaNow').includes('missingGateConnections(meta)'),'Expansion lacks validated safe fallback or actual connection verification');
assert(functionSource('placeChildAtFrontierAttempt').includes('fixedPortProfilesForRequirements')&&functionSource('placeChildAtFrontierAttempt').includes('portSeed')&&functionSource('placeChildAtFrontierAttempt').includes('specialSeed')&&functionSource('placeChildAtFrontierAttempt').includes('generatedPuzzleIssue'),'Failed boards are not fully regenerated with provisional gates and special cells');
assert(functionSource('closedVoidRepairCandidates').includes('closedVoidRepair:true')&&functionSource('repairExpansions').includes('closedVoidRepairCandidates().filter')&&functionSource('repairExpansions').includes('.slice(0,2)')&&functionSource('pendingExpansionCount').includes('closedVoidRepairCandidates().length'),'Saved fields do not detect and repair enclosed missing puzzle squares');
assert(functionSource('generatePuzzleAsync').includes('generationOptions')&&worker.includes('generationOptions || null'),'Generation options are not passed through the worker');
assert(functionSource('reopenMissingGateExpansions').includes('st.expanded=false')&&functionSource('reopenMissingGateExpansions').includes('missingGateConnections(meta)'),'Persisted false-positive expansion states are not reopened safely');
console.log(`BEND FIELD v${appVersion} source and shared-logic smoke test passed`);

View file

@ -0,0 +1,88 @@
'use strict';
const {vm,assert,functionSource,loadBendPuzzle,loadAppLogic}=require('./helpers/app-source');
const BendPuzzle=loadBendPuzzle(),AppLogic=loadAppLogic();
const context={
AppLogic,
SPECIAL_CELL_MIN_LEVEL:5,SPECIAL_CELL_DEBUG_ALL_LEVELS:false,data:{metas:{},specialMechanicsSeen:[]},
hash32:BendPuzzle.hash32,rngFrom:BendPuzzle.rngFrom,
deepClone:value=>JSON.parse(JSON.stringify(value)),key2:(x,y)=>`${x},${y}`,ckey:(r,c)=>`${r},${c}`,
SIDE_D:{N:[-1,0],S:[1,0],W:[0,-1],E:[0,1]},sameCell:(a,b)=>a[0]===b[0]&&a[1]===b[1],manhattan:(a,b)=>Math.abs(a[0]-b[0])+Math.abs(a[1]-b[1])
};
vm.createContext(context);
vm.runInContext([
'specialCellSet','invalidateSpecialCellCaches','reservedSpecialKeys','cellSet','warpMap','warpPairForCell','isWarpTransition','pathCellsAdjacent','lockForDoor','pathHasLockKey','crossingKeys','pathIndexesAtCell','crossingStateAtCell','crossingsSatisfied','gateObj','outsidePoint','analyzeTurns','turnAnalysis','partialTurnCount','numbersForPath','specialPathValid','pathAxisAtCell','pathValid','usedGateSet','occupiedMap','isSolved','rebuildSolutionClues','addWarpSpecial','addLockSpecial','obstacleCellLimit','buildCrossingTemplate','addCrossingSpecial','recentSpecialMechanicTypes','addScheduledSpecial','specialCellUsage','addSpecialCellPattern'
].map(name=>functionSource(name)).join('\n')+'\nthis.logic={specialCellSet,turnAnalysis,pathValid,isSolved,addWarpSpecial,addLockSpecial,addCrossingSpecial,addSpecialCellPattern,isWarpTransition,crossingStateAtCell,crossingsSatisfied};',context);
const logic=context.logic;
for(let level=1;level<=4;level++)assert(AppLogic.specialSchedule(level,100,level).types.length===0,`Level ${level} scheduled a production special`);
for(const level of[5,6]){const schedule=AppLogic.specialSchedule(level,100,level,['warp','lock','crossing'],[]);assert(schedule.types.length===1&&schedule.setCount===1,`Level ${level} did not schedule exactly one minimum set`)}
const level7=AppLogic.specialSchedule(7,200,7,['warp','lock','crossing'],[]),level8=AppLogic.specialSchedule(8,200,8,['warp','lock','crossing'],[]);
assert(level7.setCount===2&&level8.setCount===3,'Level 7-8 special density does not increase deterministically');
{
const puzzle={valid:[[2,0],[2,1],[0,3],[0,4],[1,4],[2,4]],g:[[2,0,'W'],[2,4,'S']],n:[[0,4,1]],specialCells:{crossings:[],warps:[{a:[2,1],b:[0,3]}],locks:[]},solution:[]},path={startGate:0,endGate:1,openGate:null,cells:[[2,0],[2,1],[0,3],[0,4],[1,4],[2,4]]};
assert(logic.turnAnalysis(path,puzzle).count===1,'Warp transition itself was counted as a turn');
}
let base=null;
const shapes=[[[0,0],[1,0],[0,1],[1,1]],[[0,0],[1,0],[2,0],[0,1],[1,1]],[[0,0],[1,0],[2,0],[0,1],[1,1],[2,1]]];
for(const shape of shapes)for(let seed=1;seed<200&&!base;seed++){
let puzzle;try{puzzle=BendPuzzle.generatePuzzle(shape,0x470000+seed,8,0,0)}catch(_){continue}
const reserved=new Set(),warp=context.deepClone(puzzle),lock=context.deepClone(puzzle);
if(logic.addWarpSpecial(warp,context.rngFrom(seed),reserved)&&logic.addLockSpecial(lock,context.rngFrom(seed+1),new Set()))base=puzzle;
}
assert(base,'Could not find a puzzle supporting warp and key/door special cells');
{
const single=BendPuzzle.generatePuzzle([[0,0]],0x47c205ed,5,0,0),cross=context.deepClone(single);
assert(logic.addCrossingSpecial(cross,context.rngFrom(0x47c205ed),new Set()),'A one-section crossing template could not be generated within the obstacle cap');
assert((cross.obstacles||[]).length<=Math.floor((cross.valid.length+(cross.obstacles||[]).length)*.2),'Crossing template exceeded the 20% obstacle cap');
}
{
const cached=context.deepClone(base);cached.specialCells={crossings:[],warps:[],locks:[]};cached._warpMap=new Map();
const added=logic.addWarpSpecial(cached,context.rngFrom(0x9e3779b9),new Set());assert(added,'Could not add a warp for stale-cache regression');
assert(!Object.prototype.hasOwnProperty.call(cached,'_warpMap'),'Warp mutation did not invalidate the stale warp map');const addedPair=cached.specialCells.warps[0];assert(logic.isWarpTransition(cached,addedPair.a,addedPair.b),'Warp lookup did not rebuild after cache invalidation');
const after=cached.solution.map(path=>logic.turnAnalysis(path,cached).count);assert(cached._warpMap&&typeof cached._warpMap.get==='function'&&cached._warpMap.size===2,'Warp map was not rebuilt after adding a warp');
const warpedIndex=cached.solution.findIndex(path=>cached.specialCells.warps.some(pair=>path.cells.some(cell=>context.sameCell(cell,pair.a))));assert(warpedIndex>=0&&Number.isFinite(after[warpedIndex]),'Warp turn analysis did not run');
}
for(let level=1;level<=4;level++){const low=logic.addSpecialCellPattern(base,0x700000+level,level),special=logic.specialCellSet(low);assert(!special.warps.length&&!special.locks.length&&!special.crossings.length,`Level ${level} generated a production special`)}
const counts={warp:0,lock:0,cross:0,boards:0};let examples={};
for(let seed=0;seed<900;seed++){
context.data.specialMechanicsSeen=[];
const puzzle=logic.addSpecialCellPattern(base,0x710000+seed,5);if(!puzzle)continue;const special=logic.specialCellSet(puzzle),
present=[special.warps.length&&'warp',special.locks.length&&'lock',special.crossings.length&&'cross'].filter(Boolean);
assert(present.length===1&&puzzle.specialSchedule?.introduction===true&&puzzle.specialSchedule?.setCount===1,'Level-5 first encounter was not one minimum special type');
if(special.warps.length){counts.warp++;examples.warp||=puzzle}
if(special.locks.length){counts.lock++;examples.lock||=puzzle}
if(special.crossings.length){counts.cross++;examples.cross||=puzzle}
if(special.warps.length||special.locks.length||special.crossings.length)counts.boards++;
}
for(const [type,count] of Object.entries({warp:counts.warp,lock:counts.lock}))assert(count>0,`${type} was never selected by deterministic first-encounter scheduling`);
assert(counts.boards>0,'Level-5 scheduling generated no viable special boards');
function verifyPuzzle(puzzle){
const special=logic.specialCellSet(puzzle),valid=new Set(puzzle.valid.map(cell=>context.ckey(...cell))),crossingKeys=new Set(special.crossings.map(cell=>context.ckey(...cell))),covered=new Map();
assert((puzzle.obstacles||[]).length<=Math.floor((puzzle.valid.length+(puzzle.obstacles||[]).length)*.2),'Special-cell conversion exceeded the 20% obstacle cap');
for(const path of puzzle.solution){
for(let i=0;i<path.cells.length;i++){
const cell=path.cells[i],key=context.ckey(...cell),visits=covered.get(key)||0;assert(valid.has(key),'Special solution leaves the valid area');assert(visits===0||visits===1&&crossingKeys.has(key),'Special solution overlaps a non-crossing cell or crosses more than twice');covered.set(key,visits+1);
if(i){const prev=path.cells[i-1];assert(context.manhattan(prev,cell)===1||logic.isWarpTransition(puzzle,prev,cell),'Special solution has an invalid transition')}
}
assert(logic.pathValid(path,puzzle),'Special solution no longer satisfies its clue');
}
assert(covered.size===valid.size,'Special solution does not cover every valid cell');
for(const pair of special.warps){let found=false;for(const path of puzzle.solution){const ai=path.cells.findIndex(cell=>context.sameCell(cell,pair.a)),bi=path.cells.findIndex(cell=>context.sameCell(cell,pair.b));if(ai>=0||bi>=0){assert(Math.abs(ai-bi)===1,'Warp endpoints are not consecutive');found=true}}assert(found,'Warp pair is absent from the solution')}
for(const lock of special.locks){let found=false;for(const path of puzzle.solution){const ki=path.cells.findIndex(cell=>context.sameCell(cell,lock.key)),di=path.cells.findIndex(cell=>context.sameCell(cell,lock.door));if(ki>=0||di>=0){assert(ki>=0&&di>ki,'Key is not before its door on the solution line');found=true}}assert(found,'Key/door pair is absent from the solution')}
const state={paths:context.deepClone(puzzle.solution),specialProgress:{crossings:special.crossings.map(cell=>context.ckey(...cell))}};assert(logic.isSolved(state,puzzle),'Generated special-cell solution does not solve its board');
for(const cell of special.crossings){const key=context.ckey(...cell),validSet=new Set(puzzle.valid.map(candidate=>context.ckey(...candidate))),gateSet=new Set(puzzle.g.map(g=>context.ckey(g[0],g[1])));assert(covered.get(key)===2,'Crossing solution does not use the crossing cell exactly twice');assert(!gateSet.has(key),'Crossing cell was placed on a gate cell');for(const neighbor of[[cell[0]-1,cell[1]],[cell[0]+1,cell[1]],[cell[0],cell[1]-1],[cell[0],cell[1]+1]])assert(validSet.has(context.ckey(...neighbor)),'Crossing cell was placed on the board edge')}
}
for(const puzzle of Object.values(examples))verifyPuzzle(puzzle);
{
const puzzle={valid:[[1,2],[2,1],[2,2],[2,3],[3,2]],g:[],n:[],specialCells:{crossings:[[2,2]],warps:[],locks:[]}},state={paths:[{cells:[[2,1],[2,2],[2,3]]},{cells:[[1,2],[2,2],[3,2]]}],specialProgress:{crossings:[]}};
assert(logic.crossingStateAtCell(state,puzzle,[2,2]),'Perpendicular live crossing was not detected');
state.paths[1].cells=[[1,2],[2,2],[2,3]];assert(!logic.crossingsSatisfied(state,puzzle),'Two lines with the same axis were accepted as a crossing');
const solvedPuzzle={valid:[[2,0],[2,1],[2,2],[2,3],[1,3],[0,2],[1,2],[3,2],[3,1]],g:[[2,0,'W'],[1,3,'N'],[0,2,'N'],[3,1,'W']],n:[[2,3,1],[3,2,1]],specialCells:{crossings:[[2,2]],warps:[],locks:[]}},solvedState={paths:[{startGate:0,endGate:1,cells:[[2,0],[2,1],[2,2],[2,3],[1,3]]},{startGate:2,endGate:3,cells:[[0,2],[1,2],[2,2],[3,2],[3,1]]}],specialProgress:{crossings:[]}};
assert(logic.isSolved(solvedState,solvedPuzzle),'A live perpendicular crossing did not permit normal board clear');
solvedState.paths[1].cells=[[0,2],[1,2],[1,1],[2,1],[3,1]];solvedState.specialProgress.crossings=['2,2'];assert(!logic.isSolved(solvedState,solvedPuzzle),'Crossing history permitted clear after the crossing state was removed');
}
console.log(`Special-cell generation passed: ${counts.warp} warp, ${counts.lock} key/door, ${counts.cross} crossing first-encounter boards`);

View file

@ -0,0 +1,39 @@
'use strict';
const {app,assert,functionSource,loadAppLogic}=require('./helpers/app-source');
const AppLogic=loadAppLogic();
const placeSource=functionSource('placeChildAtFrontier'),expandSource=functionSource('expandMetaNow'),metaStateSource=functionSource('metaState'),snapshotSource=functionSource('snapshotForStorage');
for(const forbidden of ['ensureBoards','renderAll','updateHud','nextPaint','save(','rebuildOccupancy'])assert(!placeSource.includes(forbidden),`Child placement still performs ${forbidden}`);
assert((expandSource.match(/refreshWorldView\(/g)||[]).length===1,'Expansion does not use one presentation/persistence commit');
for(const forbidden of ['ensureBoards','renderAll','updateHud'])assert(!expandSource.includes(forbidden),`Expansion bypasses the shared view pipeline with ${forbidden}`);
assert(metaStateSource.includes('normalizeState(state)'),'Live state normalization is not delegated to normalizeState');
assert(snapshotSource.includes('metaRowsForStorage()')&&snapshotSource.includes('stateRowsForStorage()'),'Snapshot serialization bypasses shared row serializers');
assert(functionSource('pushCloudPending').includes('cloudRowsForStorage')&&functionSource('cloudRowsForStorage').includes('metaForStorage')&&functionSource('cloudRowsForStorage').includes('stateForStorage'),'Cloud serialization bypasses full-detail row serializers');
assert(functionSource('stateForStorage').includes('AppLogic.stateForStorage'),'State serialization is not delegated to the shared module');
assert(functionSource('collectConnectedLineComponent').includes('AppLogic.collectConnectedLineComponent'),'Connected-line traversal is not delegated to the shared module');
const data={
metas:{A:{id:'A',puzzle:{}},B:{id:'B',puzzle:{}}},
states:{A:{paths:[{startGate:0,endGate:1,cells:[[0,0],[0,1]]}]},B:{paths:[{startGate:0,endGate:1,cells:[[0,0],[1,0],[2,0]]}]}}
};
let matchingCalls=0;
const environment={
data,
metaState:id=>data.states[id],
matchingNeighborGate(meta,gate){
matchingCalls++;
if(meta.id==='A'&&gate===1)return{meta:data.metas.B,gateIndex:0};
if(meta.id==='B'&&gate===0)return{meta:data.metas.A,gateIndex:1};
return null;
}
};
const componentCache=new Map(),lengthA=AppLogic.connectedLineLength(data.metas.A,0,componentCache,environment),callsAfterFirst=matchingCalls,lengthB=AppLogic.connectedLineLength(data.metas.B,0,componentCache,environment);
assert(lengthA===5&&lengthB===5,'Connected-line component length is incorrect');
assert(matchingCalls===callsAfterFirst,'Connected-line component was traversed again for a cached member');
const widthCache=new Map(),widthA=AppLogic.renderedConnectedLineWidth(data.metas.A,0,widthCache,environment,componentCache),callsAfterWidth=matchingCalls,widthB=AppLogic.renderedConnectedLineWidth(data.metas.B,0,widthCache,environment,componentCache);
assert(widthA===widthB&&matchingCalls===callsAfterWidth,'Connected-line width did not reuse the cached component');
const stateRow=AppLogic.stateForStorage({paths:[],specialProgress:{crossings:[]},solved:false,expanded:false,expansionRetryRound:7,solvedBy:null,scoreAwarded:0,scoreVersion:2,store:null,rev:5,transient:'remove'},{scoreVersion:2});
assert(stateRow.rev===5&&stateRow.expansionRetryRound===7&&!('transient' in stateRow),'State serializer lost expansion repair progress or leaked transient fields');
const emptyState=AppLogic.stateForStorage(null,{scoreVersion:3,normalizeState:()=>({paths:[],specialProgress:{crossings:[]},scoreVersion:3})});
assert(emptyState.scoreVersion===3&&Array.isArray(emptyState.paths),'State serializer fallback is invalid');
console.log('Stage 3/4 regression passed: batched expansion, shared traversal, normalization, serialization, and view pipeline');

101
test/storage-smoke-test.js Normal file
View file

@ -0,0 +1,101 @@
'use strict';
const {vm,assert,functionSource}=require('./helpers/app-source');
const initSource=functionSource('init');
const initialSource=functionSource('readInitialDataAsync');
assert(initialSource.includes("Object.prototype.hasOwnProperty.call(bundle,'snapshot')?bundle.snapshot:bundle"),
'A fresh database bundle with snapshot:null can be mistaken for game data');
assert(!initSource.includes('fullResync')&&!initSource.includes("cloudOutboxDeleteKeys.add('full-resync')"),'Retired full-resync migration remains in startup');
assert(initSource.includes('recoveredDeletionTombstones')&&initSource.includes('deletedBoardRevisions.set(id,tombstone.rev')&&initSource.includes('deletedBoardAuthors.set(id,tombstone.revAuthor'),'Recovered deletion tombstones are not restored into the durable deletion queue');
async function runUpgradeCase(oldVersion){
const worldWrites=[],deletedStores=[];let openedVersion=0;
const genericStore=()=>({indexNames:{contains:()=>true},createIndex:()=>{},put:()=>{},clear:()=>{},get:()=>{const request={result:null};queueMicrotask(()=>request.onsuccess?.());return request}}),
worldsStore={...genericStore(),put:row=>worldWrites.push(row)},
stores=new Proxy({worlds:worldsStore},{get:(target,name)=>target[name]||(target[name]=genericStore())}),db={objectStoreNames:{contains:()=>true},deleteObjectStore:name=>deletedStores.push(name),close(){}},request={result:db,transaction:{objectStore:name=>stores[name]}};
const context={
Promise,console:{warn:()=>{}},idbAvailable:true,idbHealth:'unverified',worldDbAbandoned:false,worldDbPromise:null,worldDbName:'test-world',IDB_LAYOUT_VERSION:8,FIELD_STORAGE_FORMAT:2,SAVE_SCHEMA:31,WORLD_GENERATION:'current-only-test',IDB_STARTUP_TIMEOUT:8000,cloudApiEnabled:false,cloudOutboxReady:true,
indexedDB:{open(_name,version){openedVersion=version;queueMicrotask(()=>{request.onupgradeneeded?.({oldVersion});queueMicrotask(()=>request.onsuccess?.())});return request}},
validWorldEpoch:value=>typeof value==='string'&&value.startsWith('world:'),createWorldEpoch:()=> 'world:upgrade-test',rememberWorldEpoch:()=>true,
defaultData:()=>({metas:{},states:{},worldEpoch:null}),globalForStorage:value=>({...value,metas:undefined,states:undefined}),
requestValue:request=>new Promise((resolve,reject)=>{request.onsuccess=()=>resolve(request.result);request.onerror=()=>reject(request.error)}),
queueMicrotask,setTimeout,clearTimeout
};
vm.createContext(context);vm.runInContext(`${functionSource('openWorldDb')}\nthis.openWorldDb=openWorldDb;`,context);await context.openWorldDb();
return{openedVersion,worldWrites,deletedStores};
}
async function runCase({mirror,stored,loadError=null,journals=[],coverage=new Map()}){
const removedKeys=[];
const context={
console:{warn:()=>{}},idbAvailable:true,idbHealth:'ready',indexedDbExpectedAtStartup:true,worldDbAbandoned:false,worldDbPromise:null,cloudApiEnabled:false,cloudOutboxReady:true,cloudAvailable:false,activeStorageFormat:2,FIELD_STORAGE_FORMAT:2,IDB_STARTUP_TIMEOUT:8000,
storageKey:'save',loadNotices:[],readInitialData:()=>mirror,safeLocalGet:()=>mirror?'{mirror}':null,
readRecoveryJournals:()=>journals,safeLocalRemove:key=>{removedKeys.push(key);return true},
recoveryJournalsToCover:[],recoveredDeletionTombstones:new Map(),
startupRecoveredMetaIds:new Set(),startupRecoveredStateIds:new Set(),startupRecoveredDeletedIds:new Set(),
loadSnapshotFromDb:async()=>{if(loadError)throw loadError;return{snapshot:stored,coverage,databaseJournals:[],worldEpoch:'world:test-epoch'}},
withStartupTimeout:promise=>promise,
validWorldEpoch:value=>typeof value==='string'&&value.startsWith('world:'),storedWorldEpoch:()=>null,createWorldEpoch:()=> 'world:test-epoch',rememberWorldEpoch:()=>true,
defaultData:()=>({metas:{},states:{}}),
attachWorldEpoch:(snapshot,epoch)=>Object.assign(snapshot||{metas:{},states:{}},{worldEpoch:epoch}),abandonWorldDatabase:()=>{context.worldDbAbandoned=true;context.idbAvailable=false;context.idbHealth='uncertain'},
isPlainObject:value=>!!value&&typeof value==='object'&&!Array.isArray(value),
deepClone:value=>JSON.parse(JSON.stringify(value)),
mergeBoardStates:(current,incoming)=>!current?JSON.parse(JSON.stringify(incoming)):!incoming?current:((incoming.rev||0)>(current.rev||0)?JSON.parse(JSON.stringify(incoming)):current),
normalizeCloudPending:value=>({metaIds:[...new Set(value?.metaIds||[])],stateIds:[...new Set(value?.stateIds||[])],deleted:[...new Set(value?.deleted||[])],globalChanged:value?.globalChanged===true}),
normalizeMeta:(_id,value)=>value,normalizeState:value=>value,
normalizeSnapshot:value=>value
};
vm.createContext(context);
vm.runInContext(`${functionSource('revisionVersion')}\n${functionSource('compareRevisionVersions')}\n${functionSource('setRecoveryJournalsToCover')}\n${functionSource('mergeRecoveryJournal')}\n${functionSource('mergeRecoveryJournals')}\n${functionSource('combineRecoveryJournals')}\n${functionSource('mergeV2RecoveryJournals')}\n${functionSource('readInitialDataAsync')}\nthis.logic={readInitialDataAsync,mergeRecoveryJournal};`,context);
return{result:await context.logic.readInitialDataAsync(),context,removedKeys};
}
(async()=>{
let test=await runCase({mirror:{metas:{},states:{}},stored:null});
assert(test.result.metas&&test.result.states,'A fresh database bundle did not produce a valid default snapshot');
const mirror={metas:{B0:{id:'B0'}},updatedAt:20},emptyDb={metas:{},updatedAt:30};
test=await runCase({mirror,stored:emptyDb});
assert(Object.keys(test.result.metas).length===0,'The retired compact mirror overrode the current database');
const db={metas:{B0:{id:'B0'},B1:{id:'B1'}},updatedAt:40};
test=await runCase({mirror,stored:db});
assert(Object.keys(test.result.metas).length===2&&test.result.metas.B1,'Populated database was not loaded');
const richerMirror={metas:{B0:{id:'B0'},B1:{id:'B1'},B2:{id:'B2'},B3:{id:'B3'}},updatedAt:35};
const staleDb={metas:{B0:{id:'B0'}},updatedAt:50};
test=await runCase({mirror:richerMirror,stored:staleDb});
assert(Object.keys(test.result.metas).length===1&&test.result.metas.B0&&!test.result.metas.B3,'A stale richer mirror resurrected a board deleted from the newer database');
const newerMirror={metas:{B0:{id:'B0'},B1:{id:'B1'}},updatedAt:60};
const olderSameSizeDb={metas:{B0:{id:'B0'},B1:{id:'B1'}},updatedAt:55};
test=await runCase({mirror:newerMirror,stored:olderSameSizeDb});
assert(Object.keys(test.result.metas).length===2&&test.result.updatedAt===55,'The current database was not authoritative over a retired mirror');
test=await runCase({mirror,stored:null,loadError:new Error('timeout')});
assert(Object.keys(test.result.metas).length===1&&test.result.metas.B0,'Database failure did not fall back to the compact mirror');
assert(test.context.idbAvailable===false&&test.context.idbHealth==='uncertain','Unverified database fallback remained writable');
const journalBase={metas:{B0:{id:'B0',rev:100}},states:{B0:{rev:100}},updatedAt:100,nextId:2};
const staleDelete={schema:30,worldGeneration:'test',updatedAt:101,sessionId:'delete-session',seq:1,deleted:[{id:'B0',rev:99}]};
const freshDelete={...staleDelete,seq:2,deleted:[{id:'B0',rev:101}]};
let merged=test.context.logic.mergeRecoveryJournal(journalBase,staleDelete);
assert(merged.metas.B0&&merged.states.B0,'Older deletion tombstone removed a newer board revision');
merged=test.context.logic.mergeRecoveryJournal(journalBase,freshDelete);
assert(!merged.metas.B0&&!merged.states.B0,'Newer deletion tombstone did not remove the covered board and state');
const currentDb={metas:{B0:{id:'B0',rev:10}},states:{B0:{rev:10}},updatedAt:100,nextId:2};
const coveredJournal={schema:30,worldGeneration:'test',updatedAt:105,sessionId:'covered',seq:3,_storageKey:'journal:covered',metas:[{id:'B2',rev:1}],states:[],deleted:[]};
const uncoveredJournal={schema:30,worldGeneration:'test',updatedAt:110,sessionId:'pending',seq:4,_storageKey:'journal:pending',metas:[{id:'B1',rev:11}],states:[],deleted:[]};
test=await runCase({mirror:currentDb,stored:currentDb,journals:[coveredJournal,uncoveredJournal],coverage:new Map([['covered',3],['pending',2]])});
assert(test.result.metas.B1&&!test.result.metas.B2,'Recovery coverage did not merge only the uncovered session journal');
assert(test.removedKeys.includes('journal:covered')&&!test.removedKeys.includes('journal:pending'),'Covered journal cleanup removed the wrong per-session journal');
assert(test.context.recoveryJournalsToCover.length===1&&test.context.recoveryJournalsToCover[0].sessionId==='pending','Uncovered journal was not retained for the next coverage commit');
let upgrade=await runUpgradeCase(2);assert(upgrade.openedVersion===8&&['metas','states','global','recovery','tombstones','outbox'].every(name=>upgrade.deletedStores.includes(name))&&upgrade.worldWrites.some(row=>row.status==='active'),'Existing database did not delete retired stores and initialize a current-only world');
upgrade=await runUpgradeCase(0);assert(upgrade.openedVersion===8&&upgrade.worldWrites.some(row=>row.status==='active'),'Fresh current-only world was not initialized');
const outboxContext={normalizeCloudPending:value=>({metaIds:[...new Set(value?.metaIds||[])],stateIds:[...new Set(value?.stateIds||[])],deleted:[...new Set(value?.deleted||[])],globalChanged:value?.globalChanged===true})};
vm.createContext(outboxContext);vm.runInContext(`${functionSource('cloudPendingFromOutboxRows')}\nthis.readOutbox=cloudPendingFromOutboxRows;`,outboxContext);
const pending=outboxContext.readOutbox([{type:'meta',id:'B1'},{type:'deleted',id:'B2'},{type:'global'}]);
assert(pending.metaIds[0]==='B1'&&pending.deleted[0]==='B2'&&pending.globalChanged,'Startup outbox rows were not integrated into pending cloud work');
const journalPrefix='journal:',journalValues=new Map([
['journal',JSON.stringify({schema:30,worldGeneration:'world',updatedAt:3,sessionId:'retired-single-key',seq:1})],
[`${journalPrefix}one`,JSON.stringify({schema:30,worldGeneration:'world',updatedAt:1,sessionId:'one',seq:2})],
[`${journalPrefix}two`,JSON.stringify({schema:30,worldGeneration:'world',updatedAt:2,sessionId:'two',seq:3})]
]),journalContext={
recoveryJournalKey:'journal',recoveryJournalPrefix:journalPrefix,LOCAL_MIRROR_MAX_BYTES:10000,SAVE_SCHEMA:30,WORLD_GENERATION:'world',
safeLocalKeys:prefix=>[...journalValues.keys()].filter(key=>key.startsWith(prefix)),safeLocalGet:key=>journalValues.get(key)||null
};
vm.createContext(journalContext);vm.runInContext(`${functionSource('readRecoveryJournals')}\nthis.readRecoveryJournals=readRecoveryJournals;`,journalContext);
const discovered=journalContext.readRecoveryJournals();
assert(discovered.map(journal=>journal.sessionId).join(',')==='one,two'&&discovered.every(journal=>journal._storageKey),'Current per-session recovery journals were not discovered and ordered');
console.log('Storage recovery integration passed');
})().catch(error=>{console.error(error);process.exitCode=1});

View file

@ -0,0 +1,119 @@
'use strict';
const http=require('http');
const path=require('path');
const {spawn}=require('child_process');
const {chromium}=require('playwright');
const {assert,root}=require('./helpers/app-source');
const port=61000+Math.floor(Math.random()*1000);
const url=`http://127.0.0.1:${port}/`;
const edgePath=process.env.BEND_FIELD_BROWSER_PATH||process.env.BEND_FIELD_EDGE_PATH||'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe';
const sleep=milliseconds=>new Promise(resolve=>setTimeout(resolve,milliseconds));
const server=spawn(process.execPath,['server.js'],{cwd:root,env:{...process.env,HOST:'127.0.0.1',PORT:String(port)},stdio:'ignore'});
let browser=null;
async function waitForServer(){
for(let attempt=0;attempt<80;attempt++){
const ready=await new Promise(resolve=>{
const request=http.get(url,response=>{response.resume();resolve(response.statusCode===200)});
request.on('error',()=>resolve(false));request.setTimeout(250,()=>{request.destroy();resolve(false)});
});
if(ready)return;
await sleep(100);
}
throw new Error('Local game server did not start');
}
(async()=>{
try{
await waitForServer();
browser=await chromium.launch({executablePath:edgePath,headless:true});
const page=await browser.newPage({viewport:{width:1000,height:900}});
const browserErrors=[];page.on('pageerror',error=>browserErrors.push(String(error?.stack||error)));page.on('console',message=>{if(message.type()==='error')browserErrors.push(message.text())});
await page.goto(url,{waitUntil:'domcontentloaded'});
try{await page.waitForFunction(()=>document.body?.dataset?.ready==='true')}
catch(error){console.error('Store UI startup diagnostics:',JSON.stringify({errors:browserErrors,state:await page.evaluate(()=>({ready:document.body?.dataset?.ready||null,status:document.querySelector('#statusMessage')?.textContent||'',text:document.body?.innerText?.slice(0,500)||''}))}));throw error}
await page.evaluate(async()=>{await document.fonts.load('16px DotGothic16Local','日本語盤面');await document.fonts.ready});
const result=await page.evaluate(async()=>{
const meta=data.metas.B0,st=metaState('B0'),itemIds=seededStoreItemIds(meta.seed);
st.store={owner:'UI TEST',pathIndex:0,cellIndex:0,itemIds,purchases:[],priceVersion:STORE_PRICE_VERSION,priceCoefficient:1};
data.score=1e9;openStoreMeta(meta);
const itemSection=document.querySelector('.store-items-section'),cursorSection=document.querySelector('.store-cursors-section'),
cursorCards=[...cursorSection.querySelectorAll('.store-item')],itemList=itemSection.querySelector('.store-section-list'),
cursorList=cursorSection.querySelector('.store-section-list'),flag=FLAG_CURSOR_ITEMS.find(item=>item.id==='cursor-flag-jp');
const itemColumns=getComputedStyle(itemList).gridTemplateColumns.split(' ').length,cursorColumns=getComputedStyle(cursorList).gridTemplateColumns.split(' ').length,
cursorVertical=cursorCards.every(card=>card.querySelector('.store-item-icon').getBoundingClientRect().bottom<=card.querySelector('.store-buy').getBoundingClientRect().top+1);
const pricesBefore=[...document.querySelectorAll('.store-buy:not(:disabled)')].map(button=>button.textContent);
data.score=0;renderStorePanel();
const pricesAfter=[...document.querySelectorAll('.store-buy')].map(button=>button.textContent);
const storeResult={
headings:[...document.querySelectorAll('.store-section-title')].map(node=>node.textContent),
items:itemSection.querySelectorAll('.store-item').length,cursors:cursorCards.length,
cursorHasCopy:cursorCards.some(card=>card.querySelector('h4,p,strong,.store-item-copy')),
cursorIcons:cursorCards.map(card=>Boolean(card.querySelector('.store-item-icon')?.textContent||card.querySelector('.store-item-icon img')?.getAttribute('src'))),
itemColumns,cursorColumns,cursorVertical,
pricesBefore,pricesAfter,actualPrices:pricesAfter.every(text=>/[\d,]+$/.test(text)&&!text.includes('あと')&&!text.endsWith('個'))
};
st.store.purchases.push({id:flag.id,buyer:'UI TEST',boughtAt:1,paidCost:3000,usedAt:0},{id:'score-lens',buyer:'UI TEST',boughtAt:2,paidCost:200000,usedAt:0});inventoryCache=null;applyCursorStyle('default');
closeStore(false);renderInventoryPanel();
const inventoryOptions=[...document.querySelectorAll('.inventory-cursor-option')],inventoryHasCopy=Boolean(document.querySelector('.inventory-cursors-section p,.inventory-cursors-section strong,.inventory-cursors-section .store-item-copy'));
const persistentSave=save;save=async()=>true;
await useInventoryItemLoaded(flag.id);const selectedOnce=data.cursorStyle;
await useInventoryItemLoaded(flag.id);const selectedTwice=data.cursorStyle;
const lensButton=[...document.querySelectorAll('.inventory-item h3')].find(node=>node.textContent.startsWith('ジェムレンズ'))?.closest('.inventory-item')?.querySelector('.inventory-use'),
lensInitial=lensButton?.textContent;
await useInventoryItemLoaded('score-lens');const lensOn=data.scoreLensEnabled,lensOnText=[...document.querySelectorAll('.inventory-item h3')].find(node=>node.textContent.startsWith('ジェムレンズ'))?.closest('.inventory-item')?.querySelector('.inventory-use')?.textContent;
await useInventoryItemLoaded('score-lens');const lensOff=data.scoreLensEnabled,lensOffText=[...document.querySelectorAll('.inventory-item h3')].find(node=>node.textContent.startsWith('ジェムレンズ'))?.closest('.inventory-item')?.querySelector('.inventory-use')?.textContent;
save=persistentSave;
applyCursorStyle(flag.cursorStyle);
viewport.dispatchEvent(new PointerEvent('pointermove',{bubbles:true,clientX:300,clientY:300,pointerType:'mouse'}));
await new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)));
applyCursorStyle(flag.cursorStyle);
const board=rendered.get('B0'),dragCache=buildDragCache(board,0,false,1);updateDragCursorDesign(dragCache,[50,50]);
const dragDesign={image:dragCache.cursorImage.getAttribute('href'),display:getComputedStyle(dragCache.cursorImage).display,clip:dragCache.cursorImage.getAttribute('clip-path')};
clearDragRender(board);
const gemAnimationStarted=playGemCollectionAnimation(board,1000),gemParticles=document.querySelectorAll('.gem-particle').length;document.querySelectorAll('.gem-particle').forEach(node=>node.remove());
const normalDisplay=getComputedStyle(customEmojiCursor).display;document.body.classList.add('is-drawing');const drawingDisplay=getComputedStyle(customEmojiCursor).display;document.body.classList.remove('is-drawing');
const firstNumber=board.numberNodes.values().next().value,firstWarning=board.numberWarningNodes.values().next().value;
return{
flags:FLAG_CURSOR_ITEMS.length,fontReady:document.fonts.check('16px DotGothic16Local'),fontFaces:[...document.fonts].map(face=>({family:face.family,status:face.status,weight:face.weight})),
bodyFont:getComputedStyle(document.body).fontFamily,buttonFont:getComputedStyle(document.querySelector('button')).fontFamily,numberFont:getComputedStyle(firstNumber).fontFamily,
faceMin:Math.min(...YELLOW_FACE_CURSOR_ITEMS.map(item=>item.cost)),faceMax:Math.max(...YELLOW_FACE_CURSOR_ITEMS.map(item=>item.cost)),
flagBase:FLAG_CURSOR_ITEMS.find(item=>item.id==='cursor-flag-af').cost,oecdBase:flag.cost,shopChance:STORE_CHANCE,
meta:document.querySelector('#storeMeta').textContent,
...storeResult,inventoryOptions:inventoryOptions.length,inventoryHasCopy,selectedOnce,selectedTwice,lensInitial,lensOn,lensOnText,lensOff,lensOffText,
customText:customEmojiCursor.textContent,customImage:customEmojiCursor.querySelector('img')?.getAttribute('src')||'',customVisible:customEmojiCursor.classList.contains('visible'),
customOpacity:getComputedStyle(customEmojiCursor).opacity,customFont:getComputedStyle(customEmojiCursor).fontFamily,
customWidth:getComputedStyle(customEmojiCursor).width,customRadius:getComputedStyle(customEmojiCursor).borderRadius,customFlagFit:getComputedStyle(customEmojiCursor.querySelector('img')).objectFit,
normalDisplay,drawingDisplay,dragDesign,gemAnimationStarted,gemParticles,warningInFront:Boolean(firstNumber.compareDocumentPosition(firstWarning)&Node.DOCUMENT_POSITION_FOLLOWING),
gemLabel:document.querySelector('.stat.score').textContent.trim()
};
});
if(!result.fontReady)console.log('Store UI diagnostics:',JSON.stringify(result));
assert(result.flags===262,'Flag cursor catalog is incomplete');
assert(result.fontReady&&/DotGothic16Local/.test(result.bodyFont)&&/DotGothic16Local/.test(result.buttonFont)&&/DotGothic16Local/.test(result.numberFont),'Bundled Japanese dot font did not load or was overridden');
assert(result.faceMin===500&&result.faceMax===50000&&result.flagBase===10000&&result.oecdBase===20000&&Math.abs(result.shopChance-1/30)<1e-12,'Cursor prices or the 1/30 shop chance are incorrect');
assert(result.meta==='店主UI TEST'&&!result.meta.includes('価格は固定'),'Fixed-price shop copy remains');
assert(result.headings.join('|')==='アイテム|カーソル','Shop sections are not separated');
assert(result.items===2&&result.cursors===12,'Shop does not render its 2+12 inventory');
assert(!result.cursorHasCopy&&result.cursorIcons.every(Boolean)&&result.actualPrices,'Cursor cards still expose names/descriptions, lack designs, or do not show actual prices');
assert(result.itemColumns===1&&result.cursorColumns===6&&result.cursorVertical,'Shop cursor designs are not horizontally arranged above their purchase buttons');
assert(result.inventoryOptions===1&&!result.inventoryHasCopy&&result.selectedOnce==='flag-jp'&&result.selectedTwice==='default',`Inventory cursor grid is not persistent or click-to-toggle: ${JSON.stringify({inventoryOptions:result.inventoryOptions,inventoryHasCopy:result.inventoryHasCopy,selectedOnce:result.selectedOnce,selectedTwice:result.selectedTwice})}`);
assert(result.lensInitial==='OFF'&&result.lensOn===true&&result.lensOnText==='ON'&&result.lensOff===false&&result.lensOffText==='OFF',`Score lens is not an ON/OFF inventory toggle: ${JSON.stringify({lensInitial:result.lensInitial,lensOn:result.lensOn,lensOnText:result.lensOnText,lensOff:result.lensOff,lensOffText:result.lensOffText})}`);
assert(!result.customText&&result.customImage.endsWith('assets/flags/1f1ef-1f1f5.svg')&&result.customVisible&&result.customOpacity==='0.76'&&result.customWidth==='14px'&&result.customRadius==='50%'&&result.customFlagFit==='cover','Japanese flag cursor is not a translucent circular knob-shaped SVG overlay');
assert(result.normalDisplay==='grid'&&result.drawingDisplay==='none'&&result.dragDesign.display!=='none'&&result.dragDesign.image.endsWith('assets/flags/1f1ef-1f1f5.svg')&&/^url\(#/.test(result.dragDesign.clip),`The custom cursor does not transfer to the grabbed knob: ${JSON.stringify({normalDisplay:result.normalDisplay,drawingDisplay:result.drawingDisplay,dragDesign:result.dragDesign})}`);
assert(result.gemAnimationStarted&&result.gemParticles>=4,'Purple gem collection particles did not spawn from the board');
assert(result.warningInFront&&!result.gemLabel.includes('ジェム')&&result.gemLabel.startsWith('◆'),'The 曲がる warning layer or standalone currency label is incorrect');
if(process.env.BEND_FIELD_UI_SCREENSHOT){
const base=path.resolve(process.env.BEND_FIELD_UI_SCREENSHOT);
await page.evaluate(()=>openStoreMeta(data.metas.B0));await page.screenshot({path:`${base}-store.png`});
await page.evaluate(()=>{closeStore(false);closeHelp(false,true);const flag=FLAG_CURSOR_ITEMS.find(item=>item.id==='cursor-flag-jp');applyCursorStyle(flag.cursorStyle);viewport.dispatchEvent(new PointerEvent('pointermove',{bubbles:true,clientX:500,clientY:450,pointerType:'mouse'}))});
await page.screenshot({path:`${base}-cursor.png`});
}
console.log(`Store UI browser test passed: ${result.flags} flags, ${result.items}+${result.cursors} entries, ${result.cursorColumns} cursor columns`);
}finally{
if(browser)await browser.close();
server.kill();
}
})().catch(error=>{console.error(error);process.exitCode=1});

View file

@ -0,0 +1,13 @@
'use strict';
const fs=require('fs'),path=require('path');
const {assert,functionSource,app,css,html}=require('./helpers/app-source');
const root=path.resolve(__dirname,'..'),catalog=JSON.parse(fs.readFileSync(path.join(root,'store-catalog.json'),'utf8'));
assert(functionSource('positionBoardLabel').includes('viewportRect')&&functionSource('applyCamera').includes('repositionActiveBoardHud'),'Board HUD is not repositioned against viewport bounds');
assert(css.includes('.fps-stat{position:fixed')&&css.includes('left:calc(6px')&&css.includes('bottom:calc(5px')&&functionSource('refreshFpsCounter').includes('FPS 待機'),'FPS display is not bottom-left or idle-aware');
assert(functionSource('pointerEventSamples').includes('coalesced[coalesced.length-1]')&&functionSource('appendBoardPointerSamples').includes('setBoardPointerSample')&&functionSource('setBoardPointerSample').includes('b.pendingPointerMove=sample')&&functionSource('renderDragFrame').includes('renderGeometryRevision'),'Knob dragging still processes stale samples or recomputes static geometry every frame');
assert(functionSource('makeBoard').includes("class:'board-input-surface'")&&css.includes('.board-input-surface{fill:transparent;cursor:crosshair;pointer-events:fill}')&&css.includes('.board-svg{cursor:crosshair;pointer-events:none}'),'Board input remains active across overlapping transparent SVG padding');
assert(css.includes('#customEmojiCursor.flag-cursor{width:32px;height:32px')&&css.includes('body[data-cursor-mode="dom"] *{cursor:none!important}')&&functionSource('updateCustomCursorFromPointer').includes("dataset.cursorMode==='dom'"),'Custom cursor size or UI-wide DOM fallback is missing');
assert(catalog.find(item=>item.id==='score-lens')?.cost===200000&&app.includes("id:'score-lens',name:'ジェムレンズ',cost:200000"),'Gem lens base price is not 200000');
assert(app.includes('TIME_ATTACK_MINUTES=Object.freeze([3,5,10])')&&html.includes('data-time-minutes="3"')&&html.includes('data-time-minutes="5"')&&html.includes('data-time-minutes="10"'),'Time attack durations are not 3, 5, and 10 minutes');
assert(css.includes('.time-attack-panel{width:min(760px')&&html.includes('📋 結果をコピー')&&functionSource('timeAttackResultText').includes('⏱️'),'Time attack UI/result decoration is incomplete');
console.log('v47.71 HUD, input, cursor, economy, and time-attack regression test passed');

View file

@ -0,0 +1,29 @@
'use strict';
const {assert,functionSource,app,css,html,read,loadBendPuzzle,root,fs,path}=require('./helpers/app-source');
assert(app.includes("const APP_VERSION='47.77'")&&html.includes('v47.77'),'Version was not advanced to v47.77');
assert(functionSource('makeBoard').includes('board-input-clip-')&&functionSource('renderBoardNow').includes("'clip-path':`url(#${b.inputClipId})`") ,'Endpoint hit regions are not clipped to their owning board');
assert(functionSource('sharedBoundaryColorIndex').includes('boundaryColorSource')&&functionSource('renderBoardNow').includes('sharedBoundaryColorIndex(meta,i)'),'Facing gate colors are not unified');
assert(functionSource('renderBoardNow').includes('else renderDragFrame(b)'),'Stationary drag cursor is not restored after a full board redraw');
assert(functionSource('bindBoard').includes('b.drawing=null;b.armedGate=null')&&!functionSource('bindBoard').includes('b.drawing.pointerId=e.pointerId'),'Released drawings can still be resumed from unrelated cells');
assert(functionSource('radialReactionIndex').includes('bestDistance')&&functionSource('beginReactionGesture').includes('setPointerCapture')&&app.includes("window.addEventListener('pointermove',moveReactionGesture,true)"),'Radial hold-drag-release selection is incomplete');
assert(html.includes('id="settingsBtn"')&&html.includes('id="settingsPlayerName"')&&html.includes('id="lightweightRenderingToggle"')&&html.includes('id="soundEnabledToggle"')&&functionSource('saveSettings').includes('/api/cloud/profile')&&functionSource('init').includes('createAutomaticPlayerName'),'Settings-based player naming, rendering, sound, or automatic initial name is missing');
assert(functionSource('soundTone').includes('if(!uiSettings.soundEnabled)return')&&functionSource('applyUiSettings').includes("classList.toggle('lightweight-rendering'"),'Settings are not applied to sound and rendering');
assert(functionSource('renderBoardNow').includes('st.solvedBy!==LEGACY_LOCAL_SOLVER'),'Legacy 「あなた」 solver labels are still displayed');
assert(css.includes('.solver-badge strong{display:block;max-width:100%;white-space:nowrap')&&css.includes('text-overflow:ellipsis'),'Solver names can still wrap repeatedly');
assert(functionSource('positionBoardLabel').includes("b.label.style.width=innerWidth+'px'"),'North/south board HUD does not extend to the board edge');
assert(css.includes('#customEmojiCursor.flag-cursor img{position:absolute;left:50%;top:50%')&&css.includes('transform:translate(-50%,-50%)'),'Flag cursor is not centered');
assert(css.includes('.fps-stat{position:fixed')&&css.includes('background:none')&&css.includes('font-size:8px'),'FPS display is not subtle and background-free');
assert(html.includes('長くつながって太くなった線ほど高得点です。'),'Help does not explain long/thick-line scoring');
assert(functionSource('rebuildMinimapWorld').includes('drawMapBoardCells')&&!functionSource('drawMapBoardCells').includes('stroke('),'Minimap board outlines are still drawn');
assert(functionSource('rebuildWorldOverviewCache').includes('drawMapBoardCells')&&functionSource('rebuildWorldOverviewCache').includes('drawMapLongLines')&&!app.includes('function drawWorldOverviewShops('),'Zoomed-out rendering does not match the minimap');
assert(functionSource('maybeGrantGenerationFailureBonus').includes('GENERATION_FAILURE_MIN_MS')&&functionSource('maybeGrantGenerationFailureBonus').includes('unresolvedExpansionCandidates(meta).length'),'Generation failure bonus is not limited to long unresolved expansion attempts');
const server=read('server.js');assert(server.includes("row.state.expanded===true")&&server.includes("Expansion already succeeded")&&server.includes('GENERATION_FAILURE_BONUS = 2500'),'Generation bonus server guard or level-6-equivalent amount is missing');
const internal=read('docs/internal-system.md');assert(internal.includes('Do not add changelog')&&internal.includes('update the relevant current specification in place'),'Documentation policy does not prohibit update-history documents');
for(const name of fs.readdirSync(path.join(root,'docs')))assert(!/(changelog|release|history|update|v\d+|phase\d+)/i.test(name),`Historical update document remains: ${name}`);
const BendPuzzle=loadBendPuzzle();let hasLevel10Region=false,hasLevel6Region=false;
for(let y=-100;y<=100;y++)for(let x=-100;x<=100;x++){const level=BendPuzzle.macroDifficulty(x,y);if(level>=6)hasLevel6Region=true;if(level===10)hasLevel10Region=true}
assert(hasLevel6Region&&hasLevel10Region,'World difficulty map still lacks level 6+ or level 10 regions');
const high=BendPuzzle.generatePuzzle([[0,0],[1,0],[0,1]],987654,6,12,-9);
assert(high.difficulty===6&&high.complexity.rawRating>=6,'Level-6 regional generation is still discarded or misclassified');
assert(functionSource('normalizeStoredPuzzle').includes('solverDifficulty(puzzle,targetLevel)'),'Stored high-level boards lose their regional level after reload');
console.log('v47.77 settings, map rendering, generation bonus, docs policy, and high-level generation regression test passed');

View file

@ -0,0 +1,11 @@
'use strict';
const {assert,functionSource,app,css}=require('./helpers/app-source');
assert(functionSource('boardPlayHudVisible').includes('hudBoardId===b?.id')&&functionSource('selectBoard').includes('setActiveBoard(b.id)'),'Board HUD does not persist on the selected board');
assert(functionSource('rebuildWorldOverviewCache').includes('{showLevels:true}')&&functionSource('drawMapBoardCells').includes('showLevels&&!solved')&&functionSource('rebuildMinimapWorld').includes('drawMapBoardCells(base,visibleMetas,mapX,mapY,scale)'),'Overview levels are missing or leaked into the minimap');
assert(functionSource('directionFromDelta').includes("preferredAxis!=='V'")&&!functionSource('directionFromDelta').includes('largest<smallest*POINTER_DOMINANT_RATIO'),'Diagonal pointer motion is still rejected');
assert(functionSource('extendPointerTo').includes('from=boardCellCenter(path.cells[path.cells.length-1])'),'Diagonal traversal still starts from a stale raw pointer location');
assert(functionSource('renderDragFrame').includes("setSvgAttr(cache.liveTail,'x2'")&&functionSource('renderDragFrame').includes('cache.geometryRevision!==logicalRevision')&&functionSource('buildDragCache').includes("class:'path drag-path drag-live-tail'"),'Drag renderer still rebuilds the full route every visual frame');
assert(functionSource('renderDragFrame').includes('blended=false')&&functionSource('renderBoardNow').includes('blended=!uiSettings.lightweightRendering')&&functionSource('pathColorAtCell').includes('uiSettings.lightweightRendering'),'Lightweight rendering still blends line colors');
assert(functionSource('refreshDragNumberColors').includes('drawing.dragNumberKeys')&&!functionSource('refreshDragNumberColors').includes('for(const[key,node]of b.numberNodes'),'Drag number styling still scans every number each step');
assert(css.includes('.drag-tip-group{pointer-events:none;will-change:transform;transform-origin:0 0}')&&css.includes('body.lightweight-rendering .path{filter:none!important;mix-blend-mode:normal!important}'),'Drag transform or lightweight path styles are missing');
console.log('v47.75 selected HUD, cached overview, diagonal drag, and drag performance regression test passed');

View file

@ -0,0 +1,12 @@
'use strict';
const {assert,functionSource,app}=require('./helpers/app-source');
assert(functionSource('boardPlayHudVisible').includes('hudBoardId===b?.id')&&functionSource('selectBoard').includes("classList.add('hud-current')"),'HUD must remain attached to the selected board');
assert(functionSource('drawWorldOverview').includes('overviewCache')&&functionSource('positionCachedWorldOverview').includes('overviewCanvas.style.transform')&&functionSource('drawWorldOverview').includes('drawImage(overviewBase'),'Overview panning must move a cached raster layer without viewport-sized frame copies');
assert(functionSource('rebuildWorldOverviewCache').includes('drawMapBoardCells'),'Overview cache rebuild is missing');
assert(!functionSource('queueRealtimeCursor').includes('schedulePresenceRender'),'Local cursor movement must not repaint the remote-presence canvas');
assert(functionSource('applyCamera').includes('shiftOnlineLayersForCamera')&&functionSource('redrawOnlineLayersAfterCamera').includes('schedulePresenceRender'),'Camera movement must transform online layers and repaint only after interaction');
assert(!functionSource('applyCamera').includes('queueMicrotask(repositionActiveBoardHud)'), 'Camera frames must not force HUD layout');
assert(functionSource('applyCamera').includes('visibilityTimer=setTimeout'),'Board LOD updates must be deferred until interaction settles');
assert(app.includes('OVERVIEW_CACHE_OVERSCAN_PX=192'),'Overview cache overscan is missing');
assert(!functionSource('visibleMetaIds').includes('ids.add(activeBoard)'),'A far selected board must not enlarge the composited world layer');
console.log('v47.75 pan, cursor, overview cache, and HUD regression test passed');

View file

@ -0,0 +1,25 @@
'use strict';
const {assert,functionSource,app,css}=require('./helpers/app-source');
for(const name of ['scheduleBoardDragFrame','queueCameraInteraction','scheduleWorldOverview','schedulePresenceRender','scheduleReactionRender']){
const source=functionSource(name);
if(name==='queueCameraInteraction')assert(source.includes('CAMERA_DISPLAY_WATCHDOG_MS')&&source.includes('cancelAnimationFrame')&&source.includes('clearTimeout'),'Camera missed-vsync fallback does not cancel its paired scheduler');
else if(name==='scheduleBoardDragFrame')assert(source.includes('DRAG_DISPLAY_WATCHDOG_MS')&&source.includes('cancelAnimationFrame')&&source.includes('clearTimeout'),'Pickup missed-vsync fallback does not cancel its paired scheduler');
else if(name!=='scheduleWorldOverview')assert(!source.includes('setTimeout('),`${name} still double-throttles through setTimeout plus requestAnimationFrame`);
assert(source.includes('requestAnimationFrame'),`${name} must remain frame-synchronized`);
}
const minimap=functionSource('scheduleMinimap');
assert(minimap.includes("classList?.contains?.('is-interacting')"),'Minimap must not repaint during an active gesture');
const overview=functionSource('drawWorldOverview');
assert(functionSource('positionCachedWorldOverview').includes('overviewCanvas.style.transform'),'Overview panning must move the cached bitmap as a compositor layer');
assert(functionSource('scheduleWorldOverview').includes('requestIdleCallback'),'Overview cache rebuilds must be deferred to idle time');
assert(overview.includes('overviewBitmapCopies'),'Overview bitmap copy must only occur during cache rebuild');
assert(!overview.includes('sourceX='),'Overview must not copy a viewport-sized sub-rectangle every pan frame');
const camera=functionSource('applyCamera');
assert(camera.includes('translate3d('),'Nearby field panning must use a compositor transform');
assert(camera.includes('shiftOnlineLayersForCamera'),'Online canvases must move without full repaint during camera gestures');
assert(camera.includes('minimapDirty=true'),'Camera movement must mark, not immediately repaint, the minimap');
assert(functionSource('boardPlayHudVisible').includes('hudBoardId===b?.id'),'Selected-board HUD persistence changed unexpectedly');
assert(css.includes('#viewport.panning::after{display:none}'),'Full-screen vignette must be suppressed while panning');
assert(css.includes('#presenceCanvas,#reactionCanvas{transform-origin:0 0'),'Online canvases are not compositor-ready');
assert(app.includes("const APP_VERSION='47.77'"),'Application version was not advanced');
console.log('v47.77 compositor frame-pipeline regression test passed');

View file

@ -0,0 +1,20 @@
'use strict';
const {assert,functionSource,app,css}=require('./helpers/app-source');
const hud=functionSource('boardPlayHudVisible'),select=functionSource('selectBoard'),center=functionSource('centerMeta');
assert(hud.includes('hudBoardId===b?.id'),'HUD must use explicit session selection, not drag/claim state');
assert(select.includes('hudBoardId=b.id')&&select.includes("classList.add('hud-current')"),'Click selection must persist the board HUD');
assert(center.includes('select=true')&&app.includes("centerMeta(initialMeta||data.metas.B0,{select:false})"),'Startup camera centering must not select an untouched board');
assert(functionSource('resetSelectedBoard').includes('rendered.get(hudBoardId)'),'Reset must target the persistent HUD selection');
assert(!css.includes("body.is-drawing .board-card:not(.input-active) .static-layer"),'Non-selected boards must not be dimmed while drawing');
const sanitize=functionSource('sanitizeStateForPuzzle');
assert(sanitize.includes('path.detachedStart&&path.endGate==null')&&sanitize.includes('warpKeys.has'),'Loose lines with both knobs on warp cells must be removed');
const realtime=functionSource('queueRealtimeCursor');
assert(realtime.includes("classList.contains('is-drawing')")&&realtime.includes('realtimePendingCursorClient')&&realtime.indexOf('worldUnitAtClient')>realtime.indexOf('setTimeout'),'Raw pointer events must not perform realtime world conversion or send during drawing');
const custom=functionSource('updateCustomCursorFromPointer');
assert(custom.includes("dataset.cursorMode==='dom'")&&custom.includes('requestAnimationFrame')&&!custom.includes('FRAME_INTERVAL'),'DOM cursor fallback must remain visible during drag and commit at display cadence');
assert(functionSource('syncCursorAppearance').includes('nativeSupported')&&css.includes('data-cursor-mode="native"'),'Static cursor skins must prefer the native cursor path');
const drag=functionSource('extendPointerTo'),render=functionSource('renderDragFrame');
assert(drag.includes('lastModelProbeKey')&&drag.includes('probeStep=6'),'Drag model work must be quantized instead of repeated for every raw move');
assert(render.includes('blended=false')&&render.includes('lastRenderedTip')&&render.includes('setSvgAttr'),'Live drag rendering must avoid gradients and redundant SVG writes');
assert(app.includes("const APP_VERSION='47.77'"),'Application version was not advanced');
console.log('v47.77 HUD, warp cleanup, cursor, and drag performance regression test passed');

View file

@ -0,0 +1,144 @@
'use strict';
const {assert,functionSource,app,css,vm}=require('./helpers/app-source');
const fs=require('fs'),path=require('path'),browserBenchmark=fs.readFileSync(path.join(__dirname,'browser-performance-benchmark.js'),'utf8');
assert(app.includes('AUXILIARY_FPS=30')&&app.includes('DRAG_TARGET_FPS=60')&&app.includes('DRAG_FRAME_INTERVAL=1000/DRAG_TARGET_FPS')&&app.includes('CAMERA_DISPLAY_WATCHDOG_MS=18'),'Interaction and auxiliary frame budgets are not separated');
assert(app.includes('BEND_INTERACTION_SCHEDULER')&&app.includes('bend-field-interaction-scheduler-variant')&&app.includes('batteryDischargingTime'),'Scheduler rollout variant or battery telemetry guardrail is missing');
assert(app.includes('DRAG_MAX_CATCHUP_CELLS=24')&&functionSource('extendPointerTo').includes('catchupPending')&&functionSource('processBoardDragFrame').includes('catchupPending'),'Large pointer jumps are not bounded and resumed across drag frames');
const cursor=functionSource('updateCustomCursorFromPointer'),cursorSync=functionSource('syncCursorAppearance');
assert(cursor.includes('requestAnimationFrame')&&!cursor.includes('FRAME_INTERVAL'),'DOM cursor fallback is not synchronized to every display frame');
assert(app.includes("addEventListener('pointermove',updateCustomCursorFromPointer")&&app.includes("addEventListener('pointerrawupdate',updateCustomCursorFromPointer"),'DOM cursor does not tolerate sparse raw-input delivery');
assert(cursor.includes('customCursorActiveUntil=perfNow()+40')&&cursor.includes('customCursorFrame=requestAnimationFrame(step)')&&cursor.includes('Math.exp(-elapsed/8)'),'DOM cursor does not keep and smooth its display-rAF loop between active pointer events');
assert(!cursor.includes("classList.contains('is-drawing')")&&!css.includes('body.is-drawing #customEmojiCursor'),'Custom cursor is hidden during pickup dragging');
assert(cursorSync.includes('nativeSupported')&&cursorSync.includes('--active-native-cursor')&&css.includes('data-cursor-mode="native"'),'Native cursor assets or the DOM fallback switch are missing');
const cursorClasses=new Set(),cursorFrames=[],cursorRafQueue=[],cursorTransforms=[],cursorPerfSamples=new Map();
let cursorClock=0,cursorRafId=0;
const cursorContext={
document:{body:{dataset:{cursorMode:'dom'}}},
customEmojiCursor:{classList:{contains:name=>cursorClasses.has(name),add:name=>cursorClasses.add(name),remove:name=>cursorClasses.delete(name)},style:{set transform(value){cursorTransforms.push(value)}}},
perfNow:()=>cursorClock,
requestAnimationFrame:callback=>{cursorRafQueue.push(callback);return++cursorRafId},
markVisualFrame:timestamp=>cursorFrames.push(timestamp),
perfObserve:(name,value)=>{const samples=cursorPerfSamples.get(name)||[];samples.push(value);cursorPerfSamples.set(name,samples);return value},
perfCount:()=>{}
};
vm.createContext(cursorContext);
vm.runInContext(`const interactionCommitAt=Object.create(null),interactionInputAt=Object.create(null);${functionSource('recordInteractionCommit')}let customCursorFrame=0,customCursorX=0,customCursorY=0,customCursorInputAt=0,customCursorActiveUntil=0,customCursorRenderedX=NaN,customCursorRenderedY=NaN,customCursorLastFrameAt=0,customCursorInputRevision=0,customCursorCommittedRevision=0;${cursor};this.updateCustomCursorFromPointer=updateCustomCursorFromPointer;`,cursorContext);
let cursorEventAt=0;
for(let frameAt=1000/60;frameAt<=1100;frameAt+=1000/60){
while(cursorEventAt<=1000&&cursorEventAt<=frameAt+.001){
cursorClock=cursorEventAt;cursorContext.updateCustomCursorFromPointer({clientX:cursorEventAt,clientY:cursorEventAt/2,timeStamp:cursorEventAt||.001,pointerType:'mouse',target:{isConnected:true}});
cursorEventAt+=1000/30;
}
cursorClock=frameAt;
for(const callback of cursorRafQueue.splice(0))callback(frameAt);
}
const cursorFrameGaps=cursorFrames.slice(1).map((timestamp,index)=>timestamp-cursorFrames[index]).sort((a,b)=>a-b),
cursorMedianGap=cursorFrameGaps[Math.floor((cursorFrameGaps.length-1)*.5)],
sortedCursorAges=[...(cursorPerfSamples.get('cursorInputAge')||[])].sort((a,b)=>a-b),cursorP95Age=sortedCursorAges[Math.floor((sortedCursorAges.length-1)*.95)],
measuredCursorGaps=[...(cursorPerfSamples.get('cursorFrameGap')||[])].sort((a,b)=>a-b),measuredCursorMedianGap=measuredCursorGaps[Math.floor((measuredCursorGaps.length-1)*.5)],
changedCursorTransforms=cursorTransforms.filter((value,index)=>index===0||value!==cursorTransforms[index-1]).length;
assert(cursorFrames.length>=60&&cursorMedianGap<=20,`Warm DOM cursor loop produced ${cursorFrames.length} frames with a ${cursorMedianGap} ms median gap`);
assert(measuredCursorGaps.length>=30&&measuredCursorMedianGap<=20,`Production cursor instrumentation captured ${measuredCursorGaps.length} gaps with a ${measuredCursorMedianGap} ms median`);
assert(changedCursorTransforms>=55,`Warm DOM cursor loop changed its transform on only ${changedCursorTransforms} display frames`);
assert(sortedCursorAges.length>=25,`Production cursor instrumentation captured only ${sortedCursorAges.length} fresh-input latency samples`);
assert(cursorP95Age<25,`Warm DOM cursor loop produced ${cursorP95Age} ms p95 input age`);
const cameraQueue=functionSource('queueCameraInteraction'),cameraApply=functionSource('applyCamera');
assert(cameraQueue.includes('pendingCameraInteraction=next')&&cameraQueue.includes('requestAnimationFrame')&&cameraQueue.includes('CAMERA_DISPLAY_WATCHDOG_MS')&&cameraQueue.includes('cancelAnimationFrame')&&cameraQueue.includes('cameraInteractionLastDraw+DRAG_FRAME_INTERVAL'),'Camera does not use a latest-input 60 Hz queue with a missed-vsync watchdog');
assert(cameraApply.includes('positionCachedWorldOverview')&&functionSource('positionCachedWorldOverview').includes('translate3d'),'Overview position is not updated in the camera fast path');
assert(functionSource('scheduleWorldOverview').includes('requestIdleCallback')&&functionSource('scheduleWorldOverview').includes("classList.contains('is-interacting')"),'Overview rebuilds are not idle and interaction-safe');
const dragSchedule=functionSource('scheduleBoardDragFrame'),dragFrame=functionSource('processBoardDragFrame');
assert(dragSchedule.includes('requestAnimationFrame')&&dragSchedule.includes('DRAG_DISPLAY_WATCHDOG_MS')&&dragSchedule.includes('cancelAnimationFrame')&&dragSchedule.includes('clearTimeout')&&dragSchedule.includes('b.dragVisualLastFrameAt+DRAG_FRAME_INTERVAL')&&dragSchedule.includes('INTERACTION_FRAME_TOLERANCE_MS')&&dragFrame.includes('DRAG_FRAME_INTERVAL'),'Pickup visuals or logic are not capped to their 60 Hz lane');
assert(functionSource('queueBoardPointerMove').includes('appendBoardPointerSamples')&&functionSource('queueBoardPointerMove').includes('commitBoardDragFromInputDeadline')&&functionSource('queueBoardPointerMove').includes('scheduleBoardDragFrame')&&!functionSource('queueBoardPointerMove').includes('updatePickupHandleOverlay')&&!functionSource('queueBoardPointerMove').includes('markVisualFrame')&&!functionSource('queueBoardPointerMove').includes('extendPointerTo'),'Pointer events mutate presentation or model work outside the capped scheduler');
assert(functionSource('commitBoardDragFromInputDeadline').includes('b.dragVisualLastFrameAt+DRAG_FRAME_INTERVAL')&&functionSource('commitBoardDragFromInputDeadline').includes('INTERACTION_FRAME_TOLERANCE_MS')&&functionSource('commitBoardDragFromInputDeadline').includes('processBoardDragFrame(b,now)'),'Pickup input deadline fallback is not governed by the 60 Hz presentation ceiling');
assert(!app.includes('DRAG_INPUT_RESCUE_MS')&&!app.includes('function rescuePickupVisualFromInput('),'An input-rate pickup presentation path can still exceed 60 Hz');
assert(functionSource('setBoardPointerSample').includes('dragVisualActiveUntil=perfNow()+40')&&dragFrame.includes('Math.exp(-visualElapsed/8)')&&dragFrame.includes('updateDrawingHandlePosition')&&dragFrame.includes('perfNow()<b.dragVisualActiveUntil'),'Pickup visual lane does not remain warm and ease toward sparse pointer samples at display cadence');
assert(functionSource('renderDragFrame').includes('translate3d')&&functionSource('renderDragFrame').includes('tailNow-drawing.lastTailRenderAt>=32'),'Complex pickup handles are not compositor-driven or the SVG live tail is repainted every display frame');
assert(dragFrame.includes('updatePickupHandleOverlay')&&dragFrame.includes('usesLightweightDragOverlay')&&dragFrame.includes('dirtyBoards.delete(b)')&&app.includes('LIGHTWEIGHT_DRAG_BOARD_CELLS=120')&&functionSource('updatePickupHandleOverlay').includes('translate3d')&&css.includes('#pickupHandleOverlay{position:fixed'),'Large-board pickup still repaints the SVG during the gesture instead of using the lightweight display-rate handle');
assert(functionSource('activateBoardPointerDrag').includes('setPickupScenePresentation(b,true)')&&functionSource('setLightweightDragPresentation').includes("style.opacity=next?'0':''")&&functionSource('setPickupScenePresentation').includes('setLightweightDragPresentation(board,next&&board===activeBoard)')&&functionSource('scheduleInteractionSettlePresentation').includes('setPickupScenePresentation(null,false)'),'Active large-board decorative layers are not compositor-suppressed during pickup and restored after settlement');
assert(dragFrame.includes("recordInteractionCommit('pickupVisual'")&&dragFrame.includes('workDuration'),'Pickup cadence, input age, or callback work is not instrumented');
assert(dragFrame.includes('timestamp<=b.dragVisualActiveUntil')&&dragFrame.includes("recordInteractionCommit('pickupVisual',timestamp,move.inputAt,false)")&&css.includes('transition:transform 16.67ms linear'),'Pickup sample gaps are not compositor-interpolated or warm visual ticks are not measured');
assert(dragFrame.includes('freshVisualInput')&&dragFrame.includes('freshLogicalInput'),'Pickup latency instrumentation resamples stale input on warm display frames');
assert(app.includes('DRAG_MAX_LOGICAL_SAMPLES_PER_FRAME=4')&&app.includes('DRAG_MODEL_BUDGET_MS=.5')&&dragFrame.includes('b.pointerMoveSamples.shift()')&&dragFrame.includes('perfNow()-modelStarted<DRAG_MODEL_BUDGET_MS')&&dragFrame.includes('logicalSlotDue&&(freshLogicalInput||velocity[0]||velocity[1]||b.drawing?.catchupPending||b.pointerMoveSamples?.length)'),'Pickup logic does not time-bound ordered-sample catch-up or warm frames still run model traversal without pending work');
assert(functionSource('setBoardPointerSample').includes('trimBoardPointerSamples(samples)')&&functionSource('trimBoardPointerSamples').includes('leastTurn')&&functionSource('trimBoardPointerSamples').includes('samples.splice(removeIndex,1)'),'Bounded pickup input overflow does not preserve sharp turns');
assert(dragFrame.includes('pointerInsideBoardScreen(b,logicalMove)?eventToSvg')&&functionSource('pointerInsideBoardScreen').includes('boardScreenRect'),'Off-board edge panning can still traverse and prematurely finish the pickup path');
assert(dragFrame.includes("perfEnd('pickupModelWork'")&&dragFrame.includes("perfEnd('pickupVisualWork'"),'Pickup model and visual callback costs are not independently instrumented');
assert(functionSource('clearDragRender').includes("style.display='none'")&&!functionSource('clearDragRender').includes('replaceChildren'),'Drag cache nodes are destroyed between gestures');
const bind=functionSource('bindBoard');
assert(bind.indexOf('beginPendingClaimPointer')<bind.indexOf('await ensureBoardClaimForInput'),'Shared-session pickup preview does not start before the ownership round-trip');
assert(bind.includes('updatePendingClaimPointer')&&bind.includes('b.pendingClaimPointer?.pointerId')&&functionSource('updatePendingClaimPointer').includes('pending.samples')&&functionSource('updatePendingClaimPointer').includes('trimBoardPointerSamples'),'Pending claim movement, turns, or release are not buffered');
assert(bind.includes('activateBoardPointerDrag')&&functionSource('activateBoardPointerDrag').includes('cancelBoardDragFrame(b)')&&functionSource('activateBoardPointerDrag').includes('for(const sample of samples)setBoardPointerSample')&&functionSource('activateBoardPointerDrag').includes('scheduleBoardDragFrame(b)'),'Approved pending claims do not enter the ordered drag fast path');
assert(functionSource('selectBoard').includes('alreadySelected')&&functionSource('renderDragFrame').includes('pickupStartFullBoardRenders'),'Pickup start does not avoid and measure full-board redraws');
assert(functionSource('applyBoardCommand').includes('pointerInteraction&&b.drawing?.pointerId==null')&&bind.includes('b.drawing=null;b.armedGate=null;refreshInteractionState()')&&bind.includes('scheduleBoardCommandSettlement(b,{paint:true,invalidate:true,pathIndex:finishedPathIndex})'),'Pickup completion does not end interaction state before deferring broad render and cache work');
assert(functionSource('safeRelease').includes('scheduleInteractionSettlePresentation')&&!functionSource('safeRelease').includes('queueMicrotask')&&functionSource('scheduleInteractionSettlePresentation').includes('requestAnimationFrame')&&!functionSource('scheduleInteractionSettlePresentation').includes('ensureBoards()'),'Pickup release still runs broad settlement work inside the pointer task');
assert(functionSource('finalizeAtGate').includes('deferSettlement:true')&&functionSource('scheduleBoardCommandSettlement').includes('requestAnimationFrame')&&functionSource('scheduleBoardCommandSettlement').includes("classList.contains('is-interacting')"),'Pickup completion still performs render, persistence, or solve settlement inside the active drag callback');
assert(functionSource('finalizeAtGate').includes('usesLightweightDragOverlay(b)')&&functionSource('finalizeAtGate').includes('requestAnimationFrame(()=>gateConnectEffect'),'Large-board gate decoration still mutates the SVG inside the finishing interaction task');
assert(functionSource('runDeferredSave').includes("classList?.contains('is-interacting')"),'Ordinary persistence is not deferred during interactions');
assert(functionSource('applyWorldSignal').includes('await waitForInteractionSettle()')&&functionSource('pullCloudWorld').includes('await waitForInteractionSettle()'),'Cross-tab or cloud reconciliation can still run broad refresh work during a gesture');
assert(functionSource('createPuzzleWorker').includes("perfCount('workerResultsDeferredDuringInteraction')")&&functionSource('createPuzzleWorker').includes('waitForInteractionSettle().then(deliver)'),'Puzzle-worker promise continuations can still run during an active gesture');
assert(functionSource('desiredInteractiveBoardIds').includes('for(const id of ids)')&&!app.includes('INTERACTIVE_DETAIL_CELL_BUDGET')&&!functionSource('ensureBoards').includes('makeStaticBoard(meta)'),'Visible puzzles can still be hidden behind a clicked-only summary LOD');
assert(functionSource('observeInteractionFrame').includes('workDuration>slowWorkThreshold')&&!functionSource('observeInteractionFrame').includes('elapsed>24'),'Quality fallback still mistakes frame spacing for callback overload');
assert(functionSource('refreshInteractionState').includes('interactionQualityDowngradePending'),'Quality changes are not deferred until the gesture ends');
assert(functionSource('refreshInteractionState').includes("topbar?.classList.toggle('drawing-active'")&&functionSource('refreshInteractionState').includes("world?.classList.toggle('camera-interacting'")&&functionSource('refreshInteractionState').includes('!usesLightweightDragOverlay(drawingBoard)')&&!css.includes('body.is-interacting .gate-dot')&&!css.includes('body.is-drawing #topbar'),'Pickup styling still invalidates the entire document or a lightweight large-board subtree');
assert(functionSource('recordInteractionCommit').includes('InputAge')&&functionSource('observeInteractionFrame').includes('interactionDroppedFrameRatio')&&app.includes("perfCount('longTasks')"),'Interaction latency, dropped-frame, or long-task diagnostics are missing');
assert(functionSource('ensureBoards').includes("classList?.contains?.('is-interacting')")&&functionSource('ensureBoards').includes('lodPassesDeferredDuringInteraction'),'Detailed and overview LOD work is not deferred during gestures');
assert(functionSource('drawingForPath').includes('pathCellIndex:new Map')&&functionSource('extendOne').includes('pathCellIndex?.set'),'Long pickup paths do not retain a cell index');
assert(functionSource('rewindActivePathToCell').includes('index===path.cells.length-1)return false'),'A pickup hovering on its current tip is incorrectly classified as a geometry-changing rewind');
assert(functionSource('makeBoard').includes('gateIndexesByCell')&&functionSource('gateCandidatesInCell').includes('gateIndexesByCell'),'Gate lookup is not indexed by cell');
assert(functionSource('makeBoard').includes('endpointIndexesByCell')&&functionSource('nearestEndpointAtPoint').includes('endpointCandidatesNearPoint'),'Open endpoint lookup is not indexed by cell');
assert(app.includes('REALTIME_CURSOR_INTERVAL=50')&&functionSource('queueRealtimeCursor').includes('vx:')&&functionSource('applyRemotePlayer').includes('lead=.035'),'Remote cursors are not sent at 20 Hz with velocity-based short extrapolation');
for(const probe of['measureDisplayCadence','measureCursorCadence','claimLatencyProbe','pickupEdgePanProbe','pinchZoomProbe','panCadenceProbe'])
assert(browserBenchmark.includes(`function ${probe}`)||browserBenchmark.includes(`function ${probe}(`),`Browser performance matrix is missing ${probe}`);
assert(browserBenchmark.includes('--force-device-scale-factor=1'),'Browser performance matrix must normalize device scale for stable display-cadence measurements');
const pickupCadenceProbeSource=browserBenchmark.slice(browserBenchmark.indexOf('async function pickupCadenceProbe'),browserBenchmark.indexOf('async function claimLatencyProbe'));
assert(pickupCadenceProbeSource.includes("deepClone(metaState('B0'))")&&pickupCadenceProbeSource.includes('data.states.B0=')&&pickupCadenceProbeSource.includes("persistNow({skipCloud:true})")&&pickupCadenceProbeSource.includes("snapshot:await client.evaluate('BEND_PERF.snapshot()')"),'Continuous pickup benchmark does not capture clean cadence data and restore its original board state');
assert(browserBenchmark.includes('async function restoreOriginState')&&browserBenchmark.includes('replaceObjectContents(current,deepClone(snapshot))')&&browserBenchmark.includes('setTimeout(resolve,80)')&&browserBenchmark.includes("applySnapshot();changed('B0')")&&browserBenchmark.includes("centerMeta(data.metas.B0,{select:false})")&&browserBenchmark.match(/restoreOriginState\(client,pristineOriginState\)/g)?.length>=4,'Browser matrix does not settle, revision-stamp, and canonically reset origin state and camera between pickup probes');
assert(browserBenchmark.includes('pickup probes did not restore a pristine origin board')&&browserBenchmark.includes("metaState('B0').solved===true"),'Browser matrix does not verify scenario isolation and solve completion before panning');
assert(browserBenchmark.includes("spawnSync(taskkill,['/pid',String(child.pid),'/T','/F']")&&browserBenchmark.includes('stopBrowserTree(edge)')&&browserBenchmark.includes('fs.rmSync(temporaryRoot,{recursive:true,force:true})'),'Browser benchmark does not guarantee process-tree and temporary-profile cleanup');
for(const profile of['small','medium','large','complex','long-line'])assert(browserBenchmark.includes(`name:'${profile}'`),`Browser benchmark profile ${profile} is missing`);
const longCells=Array.from({length:250},(_,index)=>[0,index]),longPath={startGate:0,endGate:null,openGate:null,cells:longCells.map(cell=>[...cell])},
longState={paths:[longPath]},longOccupancy=new Map(longCells.map(cell=>[`${cell[0]},${cell[1]}`,0])),longDrawing={pathIndex:0,pointerId:1,pathCellIndex:new Map(longCells.map((cell,index)=>[`${cell[0]},${cell[1]}`,index]))},
longBoard={id:'L',p:{g:Array.from({length:100},()=>[0,0,'N'])},drawing:longDrawing,crossingKeySet:new Set()},longContext={
activePath:()=>longPath,ckey:(r,c)=>`${r},${c}`,warpPairForCell:()=>null,isWarpTransition:()=>false,pathIndexesAtCell:()=>[],metaState:()=>longState,
applyCalls:0,applyBoardCommand:(_board,mutate)=>{longContext.applyCalls++;mutate();return true}
};
vm.createContext(longContext);vm.runInContext(`${functionSource('rewindActivePathToCell')}\nthis.rewindActivePathToCell=rewindActivePathToCell;`,longContext);
assert(longContext.rewindActivePathToCell(longBoard,[0,10],longOccupancy)&&longPath.cells.length===11&&longDrawing.pathCellIndex.size===11&&longOccupancy.size===11&&longContext.applyCalls===1,'A 250-cell pickup does not rewind with one suffix splice');
const traversalContext={PAD:0,CELL:1,ckey:(r,c)=>`${r},${c}`};vm.createContext(traversalContext);
vm.runInContext(`${functionSource('cellsCrossedBySegment')}\nthis.cellsCrossedBySegment=cellsCrossedBySegment;`,traversalContext);
const validLongCells=new Set(longCells.map(([r,c])=>`${r},${c}`)),forward=traversalContext.cellsCrossedBySegment([.5,.5],[249.5,.5],{preferredAxis:'H',validCells:validLongCells});
assert(forward.length===249&&forward[248][1]===249,'A 250-cell pointer segment does not preserve every forward grid cell');
let remainingCatchup=forward.length,batches=0,maxBatch=0;while(remainingCatchup){const batch=Math.min(24,remainingCatchup);maxBatch=Math.max(maxBatch,batch);remainingCatchup-=batch;batches++}
assert(maxBatch===24&&batches===11,'A 250-cell catch-up is not bounded to constant-size frame batches');
const endpointState={paths:Array.from({length:100},(_,index)=>({endGate:null,detachedStart:index%2===0,cells:[[0,index],[1,index]]}))},
endpointContext={Map,ckey:(r,c)=>`${r},${c}`,metaState:()=>endpointState};
vm.createContext(endpointContext);vm.runInContext(`${functionSource('rebuildEndpointIndexes')}\nthis.rebuildEndpointIndexes=rebuildEndpointIndexes;`,endpointContext);
const endpointBoard={id:'E',endpointIndexesByCell:new Map()},endpointIndex=endpointContext.rebuildEndpointIndexes(endpointBoard);
assert(endpointIndex.size===150&&endpointIndex.get('1,99')[0].index===99,'The 100-path endpoint index is incomplete');
const remoteContext={Number,Map,realtimePresenceId:null,remotePlayers:new Map(),schedulePresenceRender:()=>{},scheduleMinimap:()=>{}};
vm.createContext(remoteContext);vm.runInContext(`${functionSource('applyRemotePlayer')}\nthis.applyRemotePlayer=applyRemotePlayer;`,remoteContext);
assert(remoteContext.applyRemotePlayer({presenceId:'R',x:10,y:20,vx:2,vy:-1,sentAt:123,name:'R'}),'Remote cursor sample was rejected');
const remoteSample=remoteContext.remotePlayers.get('R');
assert(Math.abs(remoteSample.targetX-10.07)<1e-9&&Math.abs(remoteSample.targetY-19.965)<1e-9&&remoteSample.sentAt===123,'Remote cursor velocity extrapolation is incorrect');
for(const refreshRate of[60,120,144]){
const displayInterval=1000/refreshRate,visualCommits=[],logicalCommits=[];let nextVisual=0,nextLogical=0;
for(let timestamp=0;timestamp<1000;timestamp+=displayInterval){
if(!nextVisual||timestamp+.01>=nextVisual){visualCommits.push(timestamp);do nextVisual=(nextVisual||timestamp)+1000/60;while(nextVisual<=timestamp+.01)}
if(!nextLogical||timestamp+2>=nextLogical){logicalCommits.push(timestamp);do nextLogical=(nextLogical||timestamp)+1000/60;while(nextLogical<=timestamp+2)}
}
assert(visualCommits.length>=59&&visualCommits.length<=61,`${refreshRate} Hz display produced ${visualCommits.length} pickup presentation frames instead of about 60`);
assert(logicalCommits.length>=59&&logicalCommits.length<=61,`${refreshRate} Hz display does not retain a 60 Hz pickup logic lane`);
}
console.log('Display-rate cursor/pan and 60 Hz pickup scheduler regression test passed');