@ -59,10 +59,12 @@ class Board {
// Orb grid
this . grid = [ ] ;
this . tiles = [ ] ;
this . indexXyLookup = [ ] ;
for ( let i = 0 ; i <= 120 ; i ++ ) {
this . grid [ i ] = null ;
this . tiles [ i ] = null ;
this . indexXyLookup [ i ] = { x : i % 11 , y : Math . floor ( i / 11 ) } ;
}
this . onOrbClick = ( index , orb ) => {
@ -170,7 +172,7 @@ class Board {
* @ returns { { x : Number , y : Number } }
* /
gridIndexToXy ( index ) {
return { x : index % 11 , y : Math . floor ( index / 11 ) } ;
return this . indexXyLookup [ index ] ;
}
/ * *
@ -843,11 +845,11 @@ class Game {
placeOrbs ( template ) {
this . board . removeAllOrbs ( ) ;
let allowed = [ ] ;
let allowedTable = [ ] ;
let outsideTemplate = [ ] ;
for ( let i = 0 ; i <= 120 ; i ++ ) {
let allo = template . includes ( i ) ;
allowed . push ( allo ) ;
allowedTable . push ( allo ) ;
let { x , y } = this . board . gridIndexToXy ( i ) ;
if ( ! allo && ! this . isOutside ( x , y ) ) {
@ -866,48 +868,71 @@ class Game {
}
const place = ( n , symbol ) => {
if ( ! allowed [ n ] ) throw Error ( ` Position ${ n } not allowed by template ` ) ;
if ( this . board . grid [ n ] ) throw Error ( ` Position ${ n } is occupied ` ) ;
console . log ( ` Place ${ n } <- ${ symbol } ` ) ;
if ( ! allowedTable [ n ] ) throw Error ( ` Position ${ n } not allowed by template ` ) ;
if ( this . board . grid [ n ] ) throw Error ( ` Position ${ n } is occupied by ${ this . board . grid [ n ] } ` ) ;
// we use a hack to speed up generation here - SVG is not altered until we have a solution
this . board . grid [ n ] = symbol ;
} ;
const findAvailableIndexWithNeighbours = ( count ) => {
const unplace = ( n ) => {
console . log ( ` Unplace ${ n } ` ) ;
this . board . grid [ n ] = null ;
} ;
const findAvailableIndexWithNeighbours = ( count , except = null ) => {
let candidates = [ ] ;
for ( let i = 0 ; i < template . length ; i ++ ) {
const n = template [ i ] ;
if ( except && except . includes ( n ) ) continue ;
if ( ! this . board . grid [ n ] ) {
const neigh = this . getNeighbours ( n ) ;
// console.log(neigh);
if ( ! this . board . grid [ n ] && neigh . neighbours === count && neigh . freeSequence >= 3 ) {
if ( neigh . neighbours === count && neigh . freeSequence >= 3 ) {
candidates . push ( n ) ;
}
}
}
if ( candidates . length ) {
return candidates [ Math . floor ( this . rng . next ( ) * candidates . length ) ]
return this . arrayChoose ( candidates )
} else {
return false ;
}
} ;
const findAvailableIndex = ( ) => {
for ( let j = 0 ; j < 2 ; j ++ ) {
const findAvailableIndex = ( except = null ) => {
for ( let i = 6 ; i >= 0 ; i -- ) {
const n = findAvailableIndexWithNeighbours ( i ) ;
const n = findAvailableIndexWithNeighbours ( i , except ) ;
if ( n !== false ) return n ;
}
// this corrupts the template, but makes the likelihood of quickly finding a valid solution much higher.
if ( template . length !== 121 ) {
console . warn ( "Adding extra tile to template" ) ;
while ( 1 ) {
let toAdd = outsideTemplate [ this . rng . nextInt ( outsideTemplate . length ) ] ;
if ( allowed . includes ( toAdd ) ) continue ;
allowed [ toAdd ] = true ;
template . push ( toAdd ) ;
break ;
// Prefer tile with more neighbours to make the game harder
let candidates = [ ] ;
outsideTemplate . forEach ( ( n ) => {
if ( ! allowedTable [ n ] && this . isAvailable ( n ) ) {
const neigh = this . getNeighbours ( n ) ;
if ( ! candidates [ neigh . neighbours ] ) {
candidates [ neigh . neighbours ] = [ n ] ;
} else {
candidates [ neigh . neighbours ] . push ( n ) ;
}
}
} ) ;
for ( let i = 6 ; i >= 0 ; i -- ) {
if ( ! candidates [ i ] ) continue ;
let toAdd = this . arrayChoose ( candidates [ i ] ) ;
allowedTable [ toAdd ] = true ;
outsideTemplate . splice ( outsideTemplate . indexOf ( toAdd ) , 1 ) ;
template . push ( toAdd ) ;
console . warn ( ` Adding extra tile to template: ${ toAdd } ` ) ;
return toAdd ;
}
}
throw Error ( "Failed to find available tile" ) ;
@ -919,6 +944,8 @@ class Game {
let solution = [ ] ;
while ( toPlace . length > 0 ) {
console . log ( 'placing a pair.' ) ;
let symbol1 = toPlace . pop ( ) ;
let index1 = findAvailableIndex ( ) ;
place ( index1 , symbol1 ) ;
@ -928,25 +955,55 @@ class Game {
place ( index2 , symbol2 ) ;
if ( ! this . isAvailable ( index1 ) ) {
throw new Error ( "Solution contains a deadlock." ) ;
console . warn ( ` Deadlock, trying to work around it - ${ index1 } , ${ index2 } ` ) ;
unplace ( index2 ) ;
let except = [ index2 ] ;
let suc = false ;
for ( let i = 0 ; i < 5 ; i ++ ) {
console . log ( ` try # ${ i + 1 } ` ) ;
let index = findAvailableIndex ( except ) ;
// console.log(`try ${index} instead of ${index2}`);
place ( index , symbol2 ) ;
if ( this . isAvailable ( index1 ) ) {
suc = true ;
break ;
} else {
unplace ( index ) ;
except . push ( index ) ;
}
}
let p ;
p = this . board . gridIndexToXy ( index1 ) ;
solution . push ( [ symbol1 , ` ${ p . x } × ${ p . y } ` , index1 ] ) ;
if ( ! suc ) {
throw new Error ( "Solution contains a deadlock." ) ;
}
}
p = this . board . gridIndexToXy ( index1 ) ;
solution . push ( [ symbol1 , ` ${ p . x } × ${ p . y } ` , index1 ] ) ;
solution . push ( [ symbol1 , index1 ] ) ;
solution . push ( [ symbol2 , index2 ] ) ;
}
solution . reverse ( ) ;
console . info ( "Found a valid board!" ) ;
solution . forEach ( ( a ) => {
let p = this . board . gridIndexToXy ( a [ 1 ] ) ;
a [ 1 ] = ` ${ p . x } × ${ p . y } ` ;
} ) ;
console . log ( 'Solution: ' , solution ) ;
}
shuffleArray ( a ) {
/ * *
* Shuffle an array .
* The array is shuffled in place .
*
* @ return the array
* /
arrayShuffle ( a ) {
let j , x , i ;
for ( i = a . length - 1 ; i > 0 ; i -- ) {
j = this . rng . nextInt ( i ) ;
@ -957,6 +1014,32 @@ class Game {
return a ;
}
/ * *
* Count orbs in the game board
*
* @ return { number }
* /
countOrbs ( ) {
let n = 0 ;
// todo use reduce
this . board . grid . forEach ( ( x ) => {
if ( x !== null ) {
n ++ ;
}
} ) ;
return n ;
}
/ * *
* Choose a random emember of an array
*
* @ param array
* @ return { number }
* /
arrayChoose ( array ) {
return array [ Math . floor ( this . rng . next ( ) * array . length ) ] ;
}
buildPlacementList ( ) {
let toPlace = [
[ 'air' , 'air' ] ,
@ -1008,8 +1091,7 @@ class Game {
] ) ;
// shuffle the pairs that have random order (i.e. not metals)
this . shuffleArray ( toPlace ) ;
this . arrayShuffle ( toPlace ) ;
// the order here is actually significant, so let's pay attention...
const metals = [
@ -1061,13 +1143,19 @@ class Game {
updateOrbDisabledStatus ( ) {
for ( let n = 0 ; n <= 120 ; n ++ ) {
if ( this . board . grid [ n ] ) {
const ava = this . isAvailableAdvanced ( n ) ;
const ava = this . isAvailableAtPlaytime ( n ) ;
this . board . grid [ n ] . node . classList . toggle ( 'disabled' , ! ava ) ;
}
}
}
isAvailableAdvanced ( n ) {
/ * *
* Check if a tile is available at play - time ( checking unlocked metals )
*
* @ param n
* @ return { boolean }
* /
isAvailableAtPlaytime ( n ) {
let ava = this . isAvailable ( n ) ;
const sym = this . board . grid [ n ] . symbol ;
@ -1086,32 +1174,33 @@ class Game {
* @ param orb
* /
ingameBoardClick ( n , orb ) {
if ( this . isAvailableAdvanced ( n ) ) {
if ( ! this . isAvailableAtPlaytime ( n ) ) return ;
let wantRefresh = false ;
if ( orb . symbol === 'gold' ) {
// gold has no pairing
this . board . removeOrbByIndex ( n ) ;
this . selectedOrb = null ;
this . updateOrbDisabledStatus ( ) ;
if ( this . countOrbs ( ) === 0 ) {
console . info ( "Good work!" ) ;
wantRefresh = true ;
}
return ;
}
if ( this . selectedOrb === null ) {
else if ( this . selectedOrb === null ) {
// first selection
this . selectedOrb = { n , orb } ;
orb . node . classList . add ( 'selected' ) ;
}
else {
if ( this . selectedOrb . n === n ) {
// orb clicked twice
orb . node . classList . remove ( 'selected' ) ;
this . selectedOrb = null ;
}
else {
// second orb in a pair
const otherSymbol = this . selectedOrb . orb . symbol ;
if ( this . getPairSymbols ( orb . symbol ) . includes ( otherSymbol ) ) {
// paired
// compatible pair click ed
this . board . removeOrbByIndex ( n ) ;
this . board . removeOrbByIndex ( this . selectedOrb . n ) ;
@ -1120,11 +1209,23 @@ class Game {
}
this . selectedOrb = null ;
}
this . updateOrbDisabledStatus ( ) ;
wantRefresh = true ;
} else {
// Bad selection, select it as the first orb.
this . selectedOrb . orb . node . classList . remove ( 'selected' ) ;
this . selectedOrb = { n , orb } ;
orb . node . classList . add ( 'selected' ) ;
}
}
}
if ( wantRefresh ) {
if ( this . countOrbs ( ) === 0 ) {
console . info ( "Good work!" ) ;
}
this . updateOrbDisabledStatus ( ) ;
}
}
@ -1150,13 +1251,13 @@ class Game {
const template = this . getRandomTemplate ( ) ;
for ( let j = 0 ; j < RETRY _IN _TEMPLATE ; j ++ ) {
try {
this . placeOrbs ( template ) ;
this . placeOrbs ( template . slice ( 0 ) ) ; // clone
suc = true ;
break ;
} catch ( e ) {
if ( alertOnError ) alert ( 'welp' ) ;
numretries ++ ;
console . warn ( e . messag e) ;
console . error ( e ) ;
}
}
if ( ! suc ) {
@ -1198,17 +1299,6 @@ class Game {
this . nextMetal = this . board . metalSequence [ this . board . metalSequence . indexOf ( this . nextMetal ) + 1 ] ;
console . debug ( ` Next metal unlocked: ${ this . nextMetal } ` ) ;
}
countOrbs ( ) {
let n = 0 ;
// todo use reduce
this . board . grid . forEach ( ( x ) => {
if ( x !== null ) {
n ++ ;
}
} ) ;
return n ;
}
}
/* Start */