2026-07-30 15:13:53 +09:00
'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 ) ) ;
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 < rotation ; step ++ ) cells = cells . map ( ( [ x , y ] ) => [ - 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 < best ) best = key ;
}
return best || '' ;
}
function balancedShapeCandidates ( candidates , seed , deps ) {
const { hash32 , rngFrom , shuffle } = deps || { } ;
if ( typeof hash32 !== 'function' || typeof rngFrom !== 'function' || typeof shuffle !== 'function' ) throw new TypeError ( 'balancedShapeCandidates requires random dependencies' ) ;
const groups = new Map ( ) ;
for ( const shape of candidates || [ ] ) { const family = generatedShapeFamilyKey ( shape ) ; if ( ! groups . has ( family ) ) groups . set ( family , [ ] ) ; groups . get ( family ) . push ( shape ) }
const rng = rngFrom ( hash32 ( ( seed >>> 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 < bucket . length ) { result . push ( bucket [ round ] ) ; remaining = true }
}
return result ;
}
function growConnectedShape ( rng , size ) {
if ( size <= 1 ) return [ [ 0 , 0 ] ] ;
const cells = [ [ 0 , 0 ] ] , set = new Set ( [ '0,0' ] ) ;
while ( cells . length < size ) {
const frontier = [ ] , seen = new Set ( ) ;
for ( const [ x , y ] of cells ) for ( const [ dx , dy ] of [ [ 1 , 0 ] , [ - 1 , 0 ] , [ 0 , 1 ] , [ 0 , - 1 ] ] ) {
const cell = [ x + dx , y + dy ] , key = key2 ( ... cell ) ; if ( set . has ( key ) || seen . has ( key ) ) continue ; seen . add ( key ) ; frontier . push ( cell ) ;
}
frontier . sort ( ( a , b ) => {
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 < text . length ; index ++ ) { hash ^= text . charCodeAt ( index ) ; hash = Math . imul ( hash , 0x01000193 ) }
hash ^= hash >>> 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 } ;
2026-07-31 12:54:46 +09:00
const types = [ 'warp' , 'lock' , 'crossing' , 'internalGate' ] , encountered = new Set ( encounteredTypes || [ ] ) , recent = ( recentTypes || [ ] ) . filter ( type => types . includes ( type ) ) . slice ( - 3 ) ,
2026-07-30 15:13:53 +09:00
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 ) ,
2026-07-31 12:54:46 +09:00
specialSwitches = ( puzzle ? . specialCells ? . crossings ? . length || 0 ) + ( puzzle ? . specialCells ? . warps ? . length || 0 ) + ( puzzle ? . specialCells ? . locks ? . length || 0 ) + ( puzzle ? . specialCells ? . internalGates ? . length || 0 ) ,
2026-07-30 15:13:53 +09:00
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 < attempts ; attempt ++ ) {
const rng = rngFrom ( hash32 ( seed ^ Math . imul ( size + 17 , 0x45d9f3b ) ^ attempt ) ) , shape = growConnectedShape ( rng , size ) , key = generatedShapeKey ( shape ) ;
if ( ! seen . has ( key ) ) { seen . add ( key ) ; candidates . push ( shape ) }
}
}
return balancedShapeCandidates ( candidates , hash32 ( ( seed >>> 0 ) ^ 0x3c6ef372 ) , { hash32 , rngFrom , shuffle } ) ;
}
2026-07-31 12:54:46 +09:00
function analyzePathTurns ( path , gates , warpPairs , includeEnd = true , internalGateIndexes = [ ] ) {
2026-07-30 15:13:53 +09:00
if ( ! path ? . cells ? . length || includeEnd && path . endGate == null ) return { count : 0 , cells : [ ] } ;
2026-07-31 12:54:46 +09:00
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 || [ ] ) ;
2026-07-30 15:13:53 +09:00
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 < path . cells . length ; index ++ ) if ( isWarp ( path . cells [ index - 1 ] , path . cells [ index ] ) ) { segments . push ( path . cells . slice ( start , index ) ) ; start = index }
segments . push ( path . cells . slice ( start ) ) ;
const cells = [ ] ;
segments . forEach ( ( segment , segmentIndex ) => {
2026-07-31 12:54:46 +09:00
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 ;
2026-07-30 15:13:53 +09:00
for ( let index = 1 ; index < nodes . length - 1 ; index ++ ) {
const previous = nodes [ index - 1 ] , cell = nodes [ index ] , next = nodes [ index + 1 ] , before = [ cell [ 0 ] - previous [ 0 ] , cell [ 1 ] - previous [ 1 ] ] , after = [ next [ 0 ] - cell [ 0 ] , next [ 1 ] - cell [ 1 ] ] ;
if ( before [ 0 ] !== after [ 0 ] || before [ 1 ] !== after [ 1 ] ) { const sourceIndex = index - offset ; if ( sourceIndex >= 0 && sourceIndex < segment . length ) cells . push ( segment [ sourceIndex ] ) }
}
} ) ;
return { count : cells . length , cells } ;
}
function stateForStorage ( state , options ) {
const scoreVersion = options ? . scoreVersion || 0 , normalizeState = options ? . normalizeState ,
source = state && typeof state === 'object' ? state : typeof normalizeState === 'function' ? normalizeState ( null ) : { paths : [ ] , specialProgress : { crossings : [ ] } } ;
return { paths : source . paths || [ ] , specialProgress : source . specialProgress || { crossings : [ ] } , solved : source . solved === true , expanded : source . expanded === true , expansionRetryRound : Number . isInteger ( source . expansionRetryRound ) && source . expansionRetryRound >= 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
} ) ;
} ) ;