bend_puzzle/test/expansion-smoke-test.js
2026-08-01 16:06:14 +09:00

93 lines
22 KiB
JavaScript

'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.canExpandSharedBoard=()=>true;
context.ensureMetaState=id=>context.metaState(id);
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));