'use strict'; (function(root,factory){ const shared=root.BendPuzzle||(typeof module==='object'&&module.exports?require('./puzzle-core'):null),api=factory(shared); if(typeof module==='object'&&module.exports)module.exports=api; root.BendAppLogic=api; })(typeof globalThis!=='undefined'?globalThis:this,function(shared){ if(typeof shared?.key2!=='function')throw new Error('BendPuzzle coordinate helpers are not loaded'); const key2=shared.key2; function sectionCountRange(level){ const max=Math.max(1,Math.min(10,Math.round(level)||1)); if(max<=3)return{min:1,max:1}; return{min:Math.max(1,max-3),max}; } function normalizeGeneratedShape(chunks){ const minX=Math.min(...chunks.map(cell=>cell[0])),minY=Math.min(...chunks.map(cell=>cell[1])); return chunks.map(([x,y])=>[x-minX,y-minY]).sort((a,b)=>a[1]-b[1]||a[0]-b[0]); } function generatedShapeKey(chunks){return normalizeGeneratedShape(chunks).map(cell=>cell.join(',')).join(';')} function transformedGeneratedShape(chunks,rotation,flip){ let cells=chunks.map(([x,y])=>[flip?-x:x,y]); for(let step=0;step[-y,x]); return normalizeGeneratedShape(cells); } function generatedShapeFamilyKey(chunks){ let best=null; for(let flip=0;flip<2;flip++)for(let rotation=0;rotation<4;rotation++){ const key=generatedShapeKey(transformedGeneratedShape(chunks,rotation,flip===1)); if(best==null||key>>0)^0x6a09e667)),buckets=shuffle([...groups.values()],rng).map(group=>shuffle([...group],rng)),result=[]; for(let round=0,remaining=true;remaining;round++){ remaining=false; for(const bucket of buckets)if(round{ const ac=Math.abs(a[0])+Math.abs(a[1]),bc=Math.abs(b[0])+Math.abs(b[1]); return ac-bc||rng()-.5; }); const compactPool=frontier.slice(0,Math.max(1,Math.ceil(frontier.length*.65))),chosen=compactPool[Math.floor(rng()*compactPool.length)]; set.add(key2(...chosen));cells.push(chosen); } return normalizeGeneratedShape(cells); } function stableHash(parts){ const text=(Array.isArray(parts)?parts:[parts]).map(value=>value==null?'':typeof value==='object'?JSON.stringify(value):String(value)).join('\u241f'); let hash=0x811c9dc5; for(let index=0;index>>16;hash=Math.imul(hash,0x7feb352d);hash^=hash>>>15;hash=Math.imul(hash,0x846ca68b);hash^=hash>>>16; return hash>>>0; } function seededUnit(parts){return stableHash(parts)/0x100000000} function boundedCoefficient(parts){return .8+seededUnit(parts)*.4} function deterministicStorePrice(baseCost,worldSeed,x,y,priceVersion=1){ const coefficient=boundedCoefficient(['store-price',worldSeed,x,y,priceVersion]); return{coefficient,price:Math.max(1,Math.round(Math.max(0,Number(baseCost)||0)*coefficient))}; } function scoreLensMultiplier(activeCount){ const count=Math.max(0,Math.floor(Number(activeCount)||0)); return count<=0?1:count===1?1.25:count===2?1.4:1.5; } function completionIdentity(meta,state){ const paths=(state?.paths||[]).map(path=>({ startGate:path.startGate,endGate:path.endGate, cells:(path.cells||[]).map(cell=>[cell[0],cell[1]]) })).sort((left,right)=>left.startGate-right.startGate||left.endGate-right.endGate); return stableHash(['completion',meta?.id,meta?.seed,meta?.generatorVersion,paths]); } function deterministicBoardReward(baseScore,{worldSeed=0,meta,state,timeAttackModifier=1,scoreLensCount=0}={}){ const identity=completionIdentity(meta,state),coefficient=boundedCoefficient(['board-reward',worldSeed,meta?.id,meta?.seed,meta?.generatorVersion,identity]), lensMultiplier=scoreLensMultiplier(scoreLensCount),timeMultiplier=Math.max(0,Number(timeAttackModifier)||1), award=Math.max(0,Math.round(Math.max(0,Number(baseScore)||0)*coefficient*timeMultiplier*lensMultiplier)); return{identity,coefficient,lensMultiplier,timeMultiplier,award}; } function specialSchedule(level,validCellCount,seed,encounteredTypes=[],recentTypes=[]){ const boundedLevel=Math.max(1,Math.min(10,Math.floor(Number(level)||1))),cells=Math.max(1,Math.floor(Number(validCellCount)||1)); if(boundedLevel<5)return{types:[],setCount:0,maxCells:0}; const types=['warp','lock','crossing','internalGate'],encountered=new Set(encounteredTypes||[]),recent=(recentTypes||[]).filter(type=>types.includes(type)).slice(-3), unseen=types.filter(type=>!encountered.has(type)); if(unseen.length){ const introduced=[...unseen].sort((a,b)=>stableHash([seed,'introduction',a])-stableHash([seed,'introduction',b])||a.localeCompare(b))[0]; return{types:[introduced],setCount:1,maxCells:introduced==='crossing'?1:2,introduction:true}; } const lastType=recent.at(-1),rotated=types.filter(type=>type!==lastType),pool=rotated.length?rotated:types, ordered=[...pool].sort((a,b)=>stableHash([seed,b])-stableHash([seed,a])||a.localeCompare(b)), typeCount=boundedLevel<=6?1:boundedLevel<=8?Math.min(2,1+(stableHash([seed,'type-count'])%2)):boundedLevel===9?2:2+(stableHash([seed,'type-count'])%2), maxByArea=Math.max(1,Math.floor(cells/18)),setCount=Math.max(1,Math.min(maxByArea,boundedLevel<=6?1:boundedLevel===7?2:boundedLevel===8?3:boundedLevel===9?4:5)); return{types:ordered.slice(0,Math.min(typeCount,ordered.length)),setCount,maxCells:Math.max(2,Math.min(cells,2+setCount*2)),introduction:false}; } function interactionBurden(puzzle){ const paths=puzzle?.solution||[],totalPathLength=paths.reduce((sum,path)=>sum+(path.cells?.length||0),0),pickups=paths.length, gateTravel=(puzzle?.g||[]).reduce((sum,gate,index,gates)=>index?sum+Math.abs(gate[0]-gates[index-1][0])+Math.abs(gate[1]-gates[index-1][1]):sum,0), specialSwitches=(puzzle?.specialCells?.crossings?.length||0)+(puzzle?.specialCells?.warps?.length||0)+(puzzle?.specialCells?.locks?.length||0)+(puzzle?.specialCells?.internalGates?.length||0), occupancy=new Map();let ambiguousAdjacency=0; paths.forEach((path,pathIndex)=>(path.cells||[]).forEach(cell=>occupancy.set(key2(cell[0],cell[1]),pathIndex))); for(const[key,pathIndex]of occupancy){const[row,column]=key.split(',').map(Number);for(const[dr,dc]of[[1,0],[0,1]]){const neighbor=occupancy.get(key2(row+dr,column+dc));if(neighbor!=null&&neighbor!==pathIndex)ambiguousAdjacency++}} const raw=totalPathLength*.16+pickups*2+Math.min(20,gateTravel*.08)+ambiguousAdjacency*.35+specialSwitches*1.5; return{totalPathLength,pickups,gateTravel,ambiguousAdjacency,specialSwitches,score:Math.min(40,raw)}; } function sectionCountWeight(level,size){ const range=sectionCountRange(level),span=Math.max(1,range.max-range.min),position=(size-range.min)/span; return 1+Math.max(0,Math.min(1,position))*(1.4+Math.max(1,Math.min(10,Math.round(level)||1))*.32); } function sectionSizeOrder(seed,level,deps){ const{hash32,rngFrom}=deps||{}; if(typeof hash32!=='function'||typeof rngFrom!=='function')throw new TypeError('sectionSizeOrder requires random dependencies'); const range=sectionCountRange(level),rng=rngFrom(hash32(seed^0x5a17c9e3)),ranked=[]; for(let size=range.min;size<=range.max;size++){ const weight=sectionCountWeight(level,size),sample=Math.max(Number.EPSILON,rng()); ranked.push({size,key:-Math.log(sample)/weight}); } return ranked.sort((a,b)=>a.key-b.key||b.size-a.size).map(entry=>entry.size); } function shapeCandidatesForLevel(seed,level,attemptsPerSize,deps){ const{shapesBySize,hash32,rngFrom,shuffle}=deps||{}; if(!shapesBySize||typeof shapesBySize.get!=='function'||typeof hash32!=='function'||typeof rngFrom!=='function'||typeof shuffle!=='function')throw new TypeError('shapeCandidatesForLevel requires shape-generation dependencies'); const attempts=Number.isInteger(attemptsPerSize)&&attemptsPerSize>=0?attemptsPerSize:12, sizes=sectionSizeOrder(seed,level,{hash32,rngFrom}),candidates=[],seen=new Set(); for(const size of sizes){ for(const known of shapesBySize.get(size)||[]){const key=generatedShapeKey(known);if(!seen.has(key)){seen.add(key);candidates.push(known)}} for(let attempt=0;attempt>>0)^0x3c6ef372),{hash32,rngFrom,shuffle}); } function analyzePathTurns(path,gates,warpPairs,includeEnd=true,internalGateIndexes=[]){ if(!path?.cells?.length||includeEnd&&path.endGate==null)return{count:0,cells:[]}; const sideDelta={N:[-1,0],S:[1,0],W:[0,-1],E:[0,1]},same=(a,b)=>a&&b&&a[0]===b[0]&&a[1]===b[1],warpMap=new Map(),internal=new Set(internalGateIndexes||[]); for(const pair of Array.isArray(warpPairs)?warpPairs:[]){if(pair?.a&&pair?.b){warpMap.set(pair.a.join(','),pair.b);warpMap.set(pair.b.join(','),pair.a)}} const isWarp=(a,b)=>same(warpMap.get(a?.join(',')),b),outside=gate=>{const delta=sideDelta[gate?.[2]]||[0,0];return[(gate?.[0]||0)+delta[0],(gate?.[1]||0)+delta[1]]}; const segments=[];let start=0; for(let index=1;index{ if(!segment.length)return;const first=segmentIndex===0,last=segmentIndex===segments.length-1,nodes=[],hasStartOutside=first&&!internal.has(path.startGate),hasEndOutside=last&&includeEnd&&!internal.has(path.endGate); if(hasStartOutside)nodes.push(outside(gates?.[path.startGate]));nodes.push(...segment);if(hasEndOutside)nodes.push(outside(gates?.[path.endGate])); const offset=hasStartOutside?1:0; for(let index=1;index=0&&sourceIndex=0?source.expansionRetryRound:0,solvedBy:source.solvedBy||null,solvedById:source.solvedById||null,solvedAt:Number.isFinite(source.solvedAt)&&source.solvedAt>0?source.solvedAt:null,scoreAwarded:source.scoreAwarded||0,scoreVersion:source.scoreVersion||scoreVersion,rewardIdentity:source.rewardIdentity||null,rewardCoefficient:source.rewardCoefficient||null,store:source.store||null,rev:source.rev||0,revAuthor:source.revAuthor||''}; } function collectConnectedLineComponent(meta,pathIndex,cache,environment){ const targetCache=cache&&typeof cache.has==='function'&&typeof cache.set==='function'?cache:new Map(),data=environment?.data,metaState=environment?.metaState,matchingNeighborGate=environment?.matchingNeighborGate; if(!data?.metas||!data?.states||typeof metaState!=='function'||typeof matchingNeighborGate!=='function')throw new TypeError('collectConnectedLineComponent requires line-graph dependencies'); const startKey=`${meta?.id}:${pathIndex}`;if(targetCache.has(startKey))return targetCache.get(startKey); const members=[],visited=new Set(),stack=[[meta?.id,pathIndex]];let length=0; while(stack.length){ const[id,index]=stack.pop(),key=`${id}:${index}`;if(visited.has(key))continue; const currentMeta=data.metas[id],state=data.states[id];if(!currentMeta?.puzzle||!state)continue; const path=metaState(id).paths[index];if(!path)continue; visited.add(key);members.push([currentMeta,index]);length+=path.cells.length; for(const gateIndex of[...(path.detachedStart?[]:[path.startGate]),path.endGate]){ if(gateIndex==null)continue;let hit;try{hit=matchingNeighborGate(currentMeta,gateIndex)}catch(_){hit=null}if(!hit)continue; const neighborState=metaState(hit.meta.id),neighborIndex=neighborState.paths.findIndex(candidate=>!candidate.detachedStart&&candidate.startGate===hit.gateIndex||candidate.endGate===hit.gateIndex); if(neighborIndex>=0)stack.push([hit.meta.id,neighborIndex]); } } const component={members,length:Math.max(1,length)}; for(const key of visited)targetCache.set(key,component);targetCache.set(startKey,component);return component; } function connectedLineLength(meta,pathIndex,cache,environment){return collectConnectedLineComponent(meta,pathIndex,cache,environment).length} function lineStrokeWidth(length){return 3+12*(1-Math.exp(-(Math.max(1,length)-1)/90))} function renderedConnectedLineWidth(meta,pathIndex,widthCache,environment,componentCache){ const targetCache=widthCache&&typeof widthCache.has==='function'&&typeof widthCache.set==='function'?widthCache:new Map(),startKey=`${meta?.id}:${pathIndex}`;if(targetCache.has(startKey))return targetCache.get(startKey); const component=collectConnectedLineComponent(meta,pathIndex,componentCache,environment),width=lineStrokeWidth(component.length); for(const[currentMeta,index]of component.members)targetCache.set(`${currentMeta.id}:${index}`,width);targetCache.set(startKey,width);return width; } return Object.freeze({ sectionCountRange,normalizeGeneratedShape,generatedShapeKey,generatedShapeFamilyKey,balancedShapeCandidates,growConnectedShape,shapeCandidatesForLevel, stableHash,deterministicStorePrice,deterministicBoardReward, specialSchedule,interactionBurden, analyzePathTurns,stateForStorage,collectConnectedLineComponent,connectedLineLength,lineStrokeWidth,renderedConnectedLineWidth }); });