'use strict'; const {vm,assert,functionSource,loadAppLogic,app,css}=require('./helpers/app-source'); const {createFrameScheduler}=require('../client/input/frame-scheduler'); 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`${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&¢erVelocity[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'); let capturedPointerSample=null; const sampleContext={perfNow:()=>100,DRAG_MAX_POINTER_SAMPLES:12,ensureBoardDragScheduler:()=>({push:sample=>{capturedPointerSample=sample;return 1}})}; 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={}; sampleContext.pointer.appendBoardPointerSamples(sampleBoard,{pointerId:12,pointerType:'pen',getCoalescedEvents:()=>cornerSamples}); assert(JSON.stringify([capturedPointerSample.clientX,capturedPointerSample.clientY])===JSON.stringify([290,250]),'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++,commitConnectedLineVisuals:()=>{},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},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')}\nthis.commitCameraInteraction=commitCameraInteraction;`,cameraContext); cameraContext.cameraInteractionScheduler=createFrameScheduler({ requestFrame:cameraContext.requestAnimationFrame,cancelFrame:cameraContext.cancelAnimationFrame,setDelay:cameraContext.setTimeout,clearDelay:cameraContext.clearTimeout, now:cameraContext.perfNow,interval:1000/60,tolerance:1.25,watchdogDelay:18,commit:(next,timestamp)=>cameraContext.commitCameraInteraction(next,timestamp) }); vm.runInContext(`${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.cameraInteractionScheduler.inspect().armed&&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=[...Array.from({length:12},(_,index)=>({id:`O${index+1}`})),...Array.from({length:6},(_,index)=>({id:`C${index+1}`,cursorStyle:`face-${index+1}`}))]; assert(functionSource('renderStorePanel').includes('storeInventoryItems(meta,store)')&&functionSource('renderStorePanel').includes("category.compact?' store-compact'"),'Shop rendering bypasses the fixed 12+6 item inventory or compact cosmetic layout'); 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}),puzzleOf:meta=>meta.puzzle }; vm.createContext(storeRateContext); vm.runInContext(`${functionSource('storeObstacleCell')}\n${functionSource('maybeOpenStore')}\nthis.maybeOpenStore=maybeOpenStore;`,storeRateContext); const eligibleStoreState=()=>({solved:true,store:null,paths:[{cells:[[0,0],[0,1]]}]}); const storeMeta={seed:1,puzzle:{obstacles:[[1,1],[2,2]]}}; const belowThreshold=eligibleStoreState(),atThreshold=eligibleStoreState(); storeRateContext.maybeOpenStore(storeMeta,belowThreshold,0,.099999); storeRateContext.maybeOpenStore(storeMeta,atThreshold,0,.10); assert(belowThreshold.store&&atThreshold.store===null,'Shop appearance boundary is not exactly 1/10'); assert(storeMeta.puzzle.obstacles.some(cell=>cell[0]===belowThreshold.store.cell[0]&&cell[1]===belowThreshold.store.cell[1]),'Shop did not replace an obstacle cell'); 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'),LINE_EFFECT_IDS:new Set(['glow','neon'])}; 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:()=>{}, commitConnectedLineVisuals:()=>{},cancelBoardDragFrame:()=>{},clearDragRender:()=>{},hidePickupHandleOverlay:()=>{},safeRelease:()=>{} }; 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(!app.includes('shouldTeleportToUnsolvedBoard')&&!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');