'use strict'; const cp=require('child_process'); const fs=require('fs'); const {root,path,vm,app,html,css,worker,appLogicSource,buildMeta,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('.'); const storeCatalog=JSON.parse(read('store-catalog.json')); 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'}); assert(buildMeta.APP_VERSION===appVersion&&buildMeta.SAVE_SCHEMA===31&&buildMeta.STORAGE_SCHEMA===30&&buildMeta.IDB_LAYOUT_VERSION===8&&buildMeta.FIELD_STORAGE_FORMAT===2&&buildMeta.GAMEPLAY_DATA_VERSION===3&&buildMeta.WORLD_GENERATION==='linkfield-single-world-20260801','Canonical build metadata does not match the package or persistence contracts'); for(const marker of [ 'SPECIAL_CELL_MIN_LEVEL=5', 'shapeCandidatesForLevel','addSpecialCellPattern','addWarpSpecial','addLockSpecial','addCrossingSpecial', 'specialPathValid','crossingsSatisfied','activateCrossing','pathRenderSegments','pathStrokePieces','pathProgressAtCell','specialCellInfoMap','normalizeSpecialCells' ])assert(app.includes(marker),`Missing v47 marker: ${marker}`); assert(!app.includes('SPECIAL_CELL_DEBUG_ALL_LEVELS'),'Special-cell generation still contains a debug-all-levels switch'); 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('build-meta.js')&&html.indexOf('build-meta.js')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')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("solved!==true")&&functionSource('currentCloudPending').includes('stateIds:[...cloudJournalStateIds]'),'Unfinished shared paths are still excluded from the durable outbox'); assert(functionSource('canExpandSharedBoard').includes('state.solvedById===currentPlayerId()')&&functionSource('repairExpansions').includes('canExpandSharedBoard(meta,st)'),'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(html.includes('id="boardHudLayer"')&&css.includes('.board-label.hud-visible:not([hidden])')&&css.includes('.board-label[hidden]{display:none!important}')&&functionSource('boardPlayHudVisible').includes('hudBoardId===b?.id')&&functionSource('activateBoardPointerDrag').includes('activateBoardHud(b)')&&functionSource('refreshInteractionState').includes('setBoardHudVisibility(board,boardPlayHudVisible(board))')&&functionSource('renderBoardNow').includes('setBoardHudVisibility(b,hudVisible)'),'Board HUD is not retained for the last manipulated board or is still clipped inside the 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')==='function updateSelectedProgress(){}'&&!html.includes('id="selectedInfo"'),'Top HUD still exposes the current board 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(!app.includes('shouldTeleportToUnsolvedBoard')&&!app.includes('makeStaticBoard')&&!app.includes('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('MAX_RENDER_FPS=30')&&app.includes('AUXILIARY_FPS=30')&&app.includes('DRAG_TARGET_FPS=30'),'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')&&css.includes('.board-input-surface')&&css.includes('body.is-drawing #viewport')&&functionSource('updateCustomCursorFromPointer').includes("classList.contains('is-drawing')"),'Custom cursor coverage or drag-time cursor hiding is incomplete'); 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('requestFrame')&&functionSource('queueBoardPointerMove').includes('scheduleBoardDragFrame')&&!functionSource('queueBoardPointerMove').includes('updatePickupHandleOverlay')&&functionSource('bindBoard').includes('queueBoardPointerMove')&&functionSource('processBoardDragFrame').includes('scheduler.latest()')&&functionSource('processBoardDragFrame').includes('updatePickupHandleOverlay')&&!functionSource('processBoardDragFrame').includes('visualBlend')&&!css.includes('transition:transform 16.67ms linear'),'Pointer logic or pickup presentation is not confined to the capped frame lane'); assert(functionSource('queueCameraInteraction').includes('cameraInteractionScheduler.push')&&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("side==='S'"),'HUD does not move to a free edge'); assert(functionSource('positionBoardLabel').includes('viewportRect.width-margin-labelWidth')&&functionSource('positionBoardLabel').includes('viewportRect.height-margin-labelHeight')&&functionSource('makeBoard').includes('boardHudLayer?.append(label)'),'Board HUD is not viewport-clamped in a detached overlay layer'); assert(functionSource('gateHitBox').includes("side==='N'")&&functionSource('makeBoard').includes('...gateHitBox(gp,g.side,g.internal)'),'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/10'),'Store appearance rate is not 1/10'); assert(app.includes('MINIMAP_VIEW_CHUNKS_X=42'),'Minimap does not use the wider scale'); assert(app.includes('SOUND_GAIN_MULTIPLIER=5.2')&&functionSource('soundTone').includes('Math.min(.42'),'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('矢印キー:線を伸ばす')&&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(!app.includes('makeStaticBoard')&&!css.includes('.static-summary'),'Inactive static-board LOD code remains'); 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===97&&!yellowFaces.some(row=>/^(?:1FAE9|1FAEA|1F642 200D 219[45] FE0F)\|/.test(row)),'Glitchy or undisplayed yellow-face cursors remain'); const faceContracts=storeCatalog.filter(item=>item.id.startsWith('cursor-face-')); assert(faceContracts.length===97&&Math.min(...faceContracts.map(item=>item.cost))===500&&Math.max(...faceContracts.map(item=>item.cost))===50000&&app.includes('MAX_FACE_CURSOR_PRICE=50000')&&functionSource('storeItemPrice').includes('Math.min(MAX_FACE_CURSOR_PRICE,adjusted)'),'Canonical supported yellow-face cursor prices do not span 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','イングランド'],['gbsct','スコットランド'],['gbwls','ウェールズ']"),'Complete Unicode Emoji 17.0 flag cursor catalog is missing'); const oecdCodes='AU AT BE CA CL CO CR CZ DK EE FI FR DE GR HU IS IE IL IT JP KR LV LT LU MX NL NZ NO PL PT SK SI ES SE CH TR GB US'.split(' '); assert(oecdCodes.length===38&&oecdCodes.every(code=>storeCatalog.find(item=>item.id===`cursor-flag-${code.toLowerCase()}`)?.cost===20000)&&storeCatalog.filter(item=>item.id.startsWith('cursor-flag-')).every(item=>item.cost===10000||item.cost===20000),'Canonical flag 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('dataset.cursorMode=presentation.mode')&&css.includes('cursor:none!important')&&css.includes('#customEmojiCursor.flag-cursor{width:32px;height:32px;overflow:hidden')&&css.includes('border-radius:50%'),'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 drag-time cursor hiding 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('所持ジェム')&&!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 eighteen-item inventory'); assert(functionSource('seededStoreItemIds').includes('.slice(0,6)')&&functionSource('seededStoreItemIds').includes('6-fixedTools.length')&&functionSource('seededStoreItemIds').includes('item.scoreLens')&&functionSource('maybeOpenStore').includes('itemIds:seededStoreItemIds(meta.seed)'),'Stores do not persist six seeded cursors and six non-cursor items'); assert(['カーソル','その他のアイテム'].every(title=>functionSource('renderStorePanel').includes(`title:'${title}'`))&&functionSource('renderStorePanel').includes('.slice(0,6)')&&functionSource('renderStorePanel').includes('store-section-brief')&&css.includes('.store-compact-list{grid-template-columns:repeat(6'),'Shop is not arranged as six cursors above six described non-cursor items'); assert(functionSource('createInventoryCategoryView').includes("'inventory-cursor-grid'")&&functionSource('updateInventoryItemView').includes("option.classList.toggle('selected'")&&functionSource('useInventoryItemLoaded').includes("previousCursor===item.cursorStyle?'default':item.cursorStyle"),'Persistent click-to-toggle cursor inventory is missing'); assert(!app.includes('static-shop-icon')&&functionSource('beginPan').includes('nearestStoreMetaAtWorldPoint')&&!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')&&storeCatalog.find(item=>item.id==='score-lens')?.scoreLens===true&&/id:'score-lens'[^\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'),LINE_EFFECT_IDS:new Set(['glow','neon']),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().filter'),'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`);