'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('scheduler.latest()'),'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')&&!app.includes('makeStaticBoard')&&!css.includes('.static-summary'),'Retired zoomed board summaries remain active');
assert(!functionSource('drawWorldOverview').includes('drawWorldOverviewPaths')&&functionSource('rebuildWorldOverviewCache').includes('drawMapLongLines'),'Far zoom does not use the minimap long-line renderer');
assert(functionSource('desiredInteractiveBoardIds').includes('for(const id of ids)')&&functionSource('renderBoardNow').includes('renderedConnectedLineWidth'),'Visible detailed boards do not preserve connected-line thickness');
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:6},(_,index)=>({id:`C${index}`,cursorStyle:`face-${index}`})),
otherItems=Array.from({length:12},(_,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===12&&new Set(seedA).size===12&&selected.filter(item=>item.cursorStyle).length===6&&selected.filter(item=>!item.cursorStyle).length===6,'Seeded store inventory is not exactly six cursors and six cosmetic/tool items');
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('.board-card{filter:drop-shadow'),'Non-selected boards are dimmed or global presentation still creates costly filter surfaces');
// 15. Completion is shown only after durable shared-world confirmation.
const solveSource=functionSource('checkSolvedAndExpand');
assert(solveSource.indexOf('persistence=save(true)')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('MAX_RENDER_FPS=30')&&app.includes('AUXILIARY_FPS=30')&&app.includes('DRAG_TARGET_FPS=30'),'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');