class Svg {
/ * *
* Build a node from XML
*
* @ param { String } code
* @ param { String | null } wrapper - wrapper element , by default no wrapper .
* @ param { Object | null } wrapperOpts - opts to add to the wrapper
* @ returns { Node }
* /
static fromXML ( code , wrapper = null , wrapperOpts = null ) {
let n = document . createElementNS ( "http://www.w3.org/2000/svg" , wrapper || 'g' ) ;
if ( wrapperOpts ) {
for ( let p in wrapperOpts ) {
if ( wrapperOpts . hasOwnProperty ( p ) ) {
n . setAttribute ( p , wrapperOpts [ p ] ) ;
}
}
}
n . innerHTML = code . trim ( ) ;
if ( wrapper === null ) {
return n . childNodes [ 0 ] ;
} else {
return n ;
}
}
/ * *
* Build a node from XML , wrapped in ` <g> `
*
* @ param { String } code
* @ param { Object | null } wrapperOpts - opts to add to the wrapper
* @ returns { Node }
* /
static fromXMLg ( code , wrapperOpts = null ) {
return Svg . fromXML ( code , 'g' , wrapperOpts ) ;
}
}
/* --------- Shared Constants --------- */
const BOARD _SIZE = 121 ;
const METAL _SEQ = [ 'lead' , 'tin' , 'iron' , 'copper' , 'silver' , 'gold' ] ;
const SYMBOLS _METALS = [
'mercury' , 'lead' , 'tin' , 'iron' , 'copper' , 'silver' , 'gold'
] ;
const SYMBOLS _ALL = [
'salt' , 'air' , 'fire' , 'water' , 'earth' , 'mercury' , 'lead' ,
'tin' , 'iron' , 'copper' , 'silver' , 'gold' , 'mors' , 'vitae'
] ;
/ * *
* Convert grid coordinates to gameGrid array index
*
* @ param { Number } x
* @ param { Number } y
* @ returns { Number }
* /
function xyToGridIndex ( x , y ) {
return y * 11 + x
}
/ * *
* Convert grid index to X , Y
*
* @ param { Number } index
* @ returns { { x : Number , y : Number } }
* /
function gridIndexToXy ( index ) {
return {
x : index % 11 ,
y : Math . floor ( index / 11 )
} ;
}
/ * *
* Get if a coordinate is outside the game board .
*
* @ param x
* @ param y
* @ return { boolean | boolean }
* /
function isXyOutside ( x , y ) {
return x < 0
|| x > 10
|| y < 0
|| y > 10
|| ( y <= 5 && x > 5 + y )
|| ( y > 5 && x < y - 5 ) ;
}
/ * *
* Get if a coordinate is on the board border .
*
* @ param x
* @ param y
* @ return { boolean | boolean }
* /
function isXyBorder ( x , y ) {
return x === 0
|| x === 10
|| y === 0
|| y === 10
|| ( y <= 5 && x === 5 + y )
|| ( y > 5 && x === y - 5 ) ;
}
/ * *
* Game board
*
* Orb grid coordinates :
* x - grid X coordinate ( counted from the left edge of a triangle starting in the top left corner )
* y - grid Y coordinate ( row )
* /
class Board {
constructor ( ) {
this . $bg = document . getElementById ( 'boardbg' ) ;
this . $orbs = document . getElementById ( 'orbs' ) ;
this . $svg = document . getElementById ( 'board' ) ;
this . $root = document . getElementById ( 'root' ) ;
this . TILE _W = 91 ;
this . TILE _H = 79 ;
this . SCREEN _PAD = 20 ;
// Orb grid
this . grid = [ ] ;
this . tiles = [ ] ;
this . buttons = { } ;
for ( let i = 0 ; i < BOARD _SIZE ; i ++ ) {
this . grid [ i ] = null ;
this . tiles [ i ] = null ;
}
this . onOrbClick = ( index , orb ) => {
// placeholder
} ;
this . onTileClick = ( index ) => {
// placeholder
} ;
this . initOrb ( ) ;
this . initGlyphs ( ) ;
this . initTile ( ) ;
this . buildBackground ( ) ;
this . buildGui ( ) ;
this . initAutoScaling ( ) ;
}
/ * *
* Show all orbs for graphics debugging
* /
testGraphics ( ) {
let o ;
this . placeOrb ( 0 , 0 , 'salt' ) ;
this . placeOrb ( 1 , 0 , 'air' ) ;
this . placeOrb ( 2 , 0 , 'fire' ) ;
o = this . placeOrb ( 3 , 0 , 'water' ) ;
o . node . classList . add ( 'selected' ) ;
this . placeOrb ( 4 , 0 , 'earth' ) ;
this . placeOrb ( 5 , 0 , 'mercury' ) ;
this . placeOrb ( 0 , 1 , 'lead' ) ;
this . placeOrb ( 1 , 1 , 'tin' ) ;
this . placeOrb ( 2 , 1 , 'iron' ) ;
this . placeOrb ( 3 , 1 , 'copper' ) ;
this . placeOrb ( 4 , 1 , 'silver' ) ;
this . placeOrb ( 5 , 1 , 'gold' ) ;
this . placeOrb ( 6 , 1 , 'vitae' ) ;
this . placeOrb ( 0 , 2 , 'mors' ) ;
o = this . placeOrb ( 1 , 2 , 'copper' ) ;
o . node . classList . add ( 'disabled' ) ;
o = this . placeOrb ( 2 , 2 , 'copper' ) ;
o . node . classList . add ( 'disabled' ) ;
this . highlight ( 'copper' ) ;
}
/ * *
* Resize to window and set up window resize event handler .
* /
initAutoScaling ( ) {
this . rescaleTimeout = null ;
window . addEventListener ( 'resize' , ( ) => {
if ( this . rescaleTimeout === null ) {
this . rescaleTimeout = setTimeout ( ( ) => this . rescaleCanvas ( ) , 60 )
}
} ) ;
this . rescaleCanvas ( ) ;
}
/ * *
* Resize to current window size
* /
rescaleCanvas ( ) {
let w = window . innerWidth ;
let h = window . innerHeight ;
const pad = this . SCREEN _PAD ;
let scaleX = w / ( 1066 + pad * 2 ) ;
let scaleY = h / ( 926 + pad * 2 ) ;
let scale = Math . min ( scaleX , scaleY ) ;
this . $root . setAttribute ( 'transform' , ` translate( ${ w / 2 } , ${ h / 2 } ) scale( ${ scale } ) ` ) ;
this . rescaleTimeout = null ;
}
/ * *
* Highlight a symbol . All other highlights are removed .
*
* @ param { String | null } symbol - symbol to highlight , null to hide highlights
* /
highlight ( symbol = null ) {
SYMBOLS _ALL . forEach ( ( s ) => {
this . $svg . classList . toggle ( ` highlight- ${ symbol } ` , symbol === s ) ;
} ) ;
}
/ * *
* Convert grid coordinates to graphic coordinates
*
* @ param { Number } x
* @ param { Number } y
* @ returns { { rx : number , ry : number } }
* /
gridXyToCoord ( x , y ) {
let rx = this . TILE _W * ( - 2.5 + x - y * 0.5 ) ;
let ry = this . TILE _H * ( - 5 + y ) ;
return { rx , ry } ;
}
/ * *
* Convert GUI coordinates to graphic coordinates
*
* @ param { Number } x
* @ param { Number } y
* @ returns { { rx : number , ry : number } }
* /
guiXyToCoord ( x , y ) {
let rx = this . TILE _W * ( - 5.9 + x ) ;
let ry = this . TILE _H * ( - 5.5 + y ) ;
return { rx , ry } ;
}
/ * *
* Remove an orb from the grid at the given coordinates .
*
* @ param { Number } x - board X
* @ param { Number } y - board Y
* /
removeOrb ( x , y ) {
const index = xyToGridIndex ( x , y ) ;
this . removeOrbByIndex ( index )
}
/ * *
* Count orbs in the game board
*
* @ return { number }
* /
countOrbs ( ) {
return this . grid . reduce ( ( acu , x ) => acu + ( x !== null ) , 0 ) ;
}
/ * *
* Remove orb by array index
*
* @ param { Number } index
* @ param { Boolean } errorIfEmpty
* /
removeOrbByIndex ( index , errorIfEmpty = false ) {
// placeholder orb
if ( typeof this . grid [ index ] === 'string' ) {
this . grid [ index ] = null ;
return ;
}
if ( this . grid [ index ] ) {
this . $orbs . removeChild ( this . grid [ index ] . node ) ;
this . grid [ index ] = null ;
} else {
if ( errorIfEmpty ) {
throw new Error ( ` Position ${ index } is already empty. ` ) ;
}
}
}
/ * *
* Place an orb by array index
*
* @ param { Number } index
* @ param { String } symbol
* @ return { { node : Node , symbol : String } }
* /
placeOrbByIndex ( index , symbol ) {
const { x , y } = gridIndexToXy ( index ) ;
return this . placeOrb ( x , y , symbol ) ;
}
/ * *
* Place an orb on the grid
*
* @ param { Number } x - board X
* @ param { Number } y - board Y
* @ param { String } symbol - alchemical symbol name
* @ returns { { node : Node , symbol : String } } - orb object
* /
placeOrb ( x , y , symbol ) {
const { rx , ry } = this . gridXyToCoord ( x , y ) ;
const arrayIndex = xyToGridIndex ( x , y ) ;
this . removeOrbByIndex ( arrayIndex ) ;
let template ;
if ( SYMBOLS _METALS . includes ( symbol ) ) {
template = this . metallicOrbTpl ;
} else {
template = this . orbTpl ;
}
let orb = template . cloneNode ( true ) ;
orb . classList . add ( ` symbol- ${ symbol } ` ) ;
orb . setAttribute ( 'transform' , ` translate( ${ rx } , ${ ry } ) ` ) ;
orb . querySelector ( '.orb-fill' )
. setAttribute ( 'fill' , this . orbColors [ symbol ] ) ;
orb . appendChild ( this . symbolTpls [ symbol ] . cloneNode ( true ) ) ;
this . $orbs . appendChild ( orb ) ;
let object = {
node : orb ,
symbol
} ;
orb . addEventListener ( 'click' , ( ) => {
this . onOrbClick ( arrayIndex , object ) ;
} ) ;
this . grid [ arrayIndex ] = object ;
return object ;
}
/ * *
* Build board background
* /
buildBackground ( ) {
// Background hexagon
let polygon = Svg . fromXML ( `
< polygon
points = "43,-25 0,-50 -43,-25 -43,25 0,50 43,25"
transform = "translate(0,0) scale(10.7) rotate(30)"
class = "board-bg-hex"
fill = "#7e6c56" / >
` );
this . $bg . appendChild ( polygon ) ;
// -- Tile grid --
// Grid is built in two passes - shadows must be placed first
this . buf0 = [ ] ;
this . buf1 = [ ] ;
for ( let y = 0 ; y < 6 ; y ++ ) {
for ( let x = 0 ; x < 6 + y ; x ++ ) {
this . placeTile ( x , y ) ;
}
}
for ( let y = 0 ; y < 5 ; y ++ ) {
for ( let x = 0 ; x < 10 - y ; x ++ ) {
this . placeTile ( x + y + 1 , 6 + y ) ;
}
}
this . buf0 . forEach ( ( elem ) => {
this . $bg . appendChild ( elem ) ;
} ) ;
// inner fill so the inner tiles don't have to use blur shadow
// The color is precisely the shadow in the middle of the border shadows
// superimposed over the background brown.
let polygon2 = Svg . fromXML ( `
< polygon
points = "43,-25 0,-50 -43,-25 -43,25 0,50 43,25"
transform = "translate(0,0) scale(9.3) rotate(30)"
class = "tile-shadow"
fill = "#372F26" / >
` );
this . $bg . appendChild ( polygon2 ) ;
this . buf1 . forEach ( ( elem ) => {
this . $bg . appendChild ( elem ) ;
} ) ;
delete this . buf0 ;
delete this . buf1 ;
}
/ * *
* Init the orb template
* /
initOrb ( ) {
this . orbTpl = Svg . fromXMLg ( `
< g transform = "scale(0.75)" >
< circle
r = "50" cy = "5" cx = "0"
fill = "black"
class = "orb-shadow" / >
< circle
r = "55" cy = "0" cx = "0"
fill = "white"
class = "orb-glow"
fill - opacity = "0" / >
< circle
r = "49" cy = "0" cx = "0"
fill = "#9F9F9F"
class = "orb-background" / >
< circle
r = "50" cy = "0" cx = "0"
fill = "red"
class = "orb-fill"
fill - opacity = "0.80" / >
< circle
cx = "0" cy = "0" r = "50"
fill = "url(#radGradOrbDark)" / >
< ellipse
cx = "0" cy = "36.5" rx = "28" ry = "13"
fill = "url(#radGradOrbBottom)" / >
< ellipse
ry = "24" rx = "35" cy = "-23" cx = "0"
fill = "url(#radGradOrbTop)" / >
< / g >
` , {
'class' : 'orb' ,
} ) ;
let metallicEffect = Svg . fromXML ( `
< g fill - opacity = "0.3" transform = "translate(-3,0) scale(0.75)" >
< path
d = "M -43.75,9.2857139 C -30.010378,16.025483 26.753132,-7.8871183 45.535714,11.964285 15.246713,-2.1819387 -29.821429,19.702381 -43.75,9.2857139 Z"
fill = "url(#linGradMetallicInlay)" / >
< path
d = "M -45.357143,-2.8766642 C -31.617521,3.8631045 25.145989,-20.049496 43.928571,-0.19809327 13.63957,-14.344317 -31.428572,7.5400025 -45.357143,-2.8766642 Z"
fill = "url(#linGradMetallicInlay)" / >
< path
d = "m -41.071429,-14.222551 c 13.08012,5.7255132 67.118982,-14.588524 85,2.275477 -28.835129,-12.017384 -71.74,6.5736044 -85,-2.275477 z"
fill = "url(#linGradMetallicInlay)" / >
< path
d = "M -41.785714,21.6174 C -28.925428,27.053125 24.205217,7.7672504 41.785714,23.777708 13.435209,12.368564 -28.748572,30.018599 -41.785714,21.6174 Z"
fill = "url(#linGradMetallicInlay)" / >
< path
d = "m -35.178572,32.540035 c 10.771863,4.131683 55.274455,-10.527466 70,1.642045 -23.746577,-8.672064 -59.08,4.743688 -70,-1.642045 z"
fill = "url(#linGradMetallicInlay)" / >
< path
d = "m -39.107144,-24.427576 c 12.33818,5.58062 63.311813,-14.219337 80.178571,2.217893 -27.199522,-11.713264 -67.670714,6.407248 -80.178571,-2.217893 z"
fill = "url(#linGradMetallicInlay)" / >
< path
d = "m -36.428571,-35.852822 c 10.771863,4.131683 55.274455,-10.527466 70,1.642045 -23.7465774,-8.672064 -59.08,4.743688 -70,-1.642045 z"
fill = "url(#linGradMetallicInlay)" / >
< / g >
` );
this . metallicOrbTpl = this . orbTpl . cloneNode ( true ) ;
this . metallicOrbTpl . appendChild ( metallicEffect ) ;
}
/ * *
* Init the tile template
* /
initTile ( ) {
const offsetX = - 13.25 ;
const offsetY = - 283.75 ;
const scale = 3.9 ;
const scaleShadow = scale + 0.1 ;
this . tileShadowTpl = Svg . fromXMLg ( `
< g transform = "scale(${scaleShadow}) translate(${offsetX},${offsetY})" class = "tile-shadow" >
< path
transform = "matrix(0.81746386,-0.47196298,0.47519507,0.823062,-132.56101,57.743511)"
d = "M 28.028064,282.66339 21.029591,294.7851 H 7.0326457 L 0.03417301,282.66339 7.0326457,270.54168 H 21.029591 Z"
fill = "#000000"
fill - opacity = "0.6"
filter = "url('#filterDropshadow')" / >
< / g >
` );
/ *
paths :
back - and - bottom
top - left _right
left
top - right
inner
circle
* /
this . tileTpl = Svg . fromXMLg ( `
< g transform = "scale(${scale}) translate(${offsetX},${offsetY})" >
< path
transform = "matrix(0.81746386,-0.47196298,0.47519507,0.823062,-132.56101,57.743511)"
d = "M 28.028064,282.66339 21.029591,294.7851 H 7.0326457 L 0.03417301,282.66339 7.0326457,270.54168 H 21.029591 Z"
fill = "#9e9272" / >
< path
transform = "matrix(0.81746386,-0.47196298,0.47519507,0.823062,-132.56101,57.743511)"
d = "M 28.028064,282.66339 21.029591,294.7851 7.0326457,270.54168 H 21.029591 Z"
fill = "#d0c6ab" / >
< path
transform = "matrix(0.81746386,-0.47196298,0.47519507,0.823062,-132.56101,57.743511)"
d = "M 14.031114,282.66339 H 0.03417301 L 7.0326457,270.54168 Z"
fill = "#b4a682" / >
< path
transform = "matrix(0.81746386,-0.47196298,0.47519507,0.823062,-132.56101,57.743511)"
d = "m 28.028064,282.66339 h -13.99695 l 6.998477,-12.12171 z"
fill = "#d7d3c1" / >
< path
transform = "matrix(0.74967562,-0.43282542,0.43578949,0.75480954,-120.47135,76.486841)"
d = "m 28.343049,282.66339 -7.155965,12.3945 H 6.8751531 L -0.28081226,282.66339 6.8751531,270.2689 H 21.187084 Z"
fill = "#ab9f7e" / >
< circle
r = "10.155904"
cy = "283.77084"
cx = "13.229166"
fill = "url(#radGradSlotBg)" / >
< / g >
` , {class: 'tile'});
}
/ * *
* Add a board tile to the image .
*
* @ param x
* @ param y
* /
placeTile ( x , y ) {
const { rx , ry } = this . gridXyToCoord ( x , y ) ;
/ *
// Debug circle
this . $svg . appendChild ( Svg . makeNode ( 'circle' , {
r : this . TILE _W / 2 ,
cy : ry , cx : rx ,
fill : 'none' ,
stroke : 'black' ,
'stroke-width' : 2 ,
} ) ) ;
* /
if ( isXyBorder ( x , y ) ) {
let polygon _shadow = this . tileShadowTpl . cloneNode ( true ) ;
polygon _shadow . setAttribute ( 'transform' , ` translate( ${ rx } , ${ ry } ),scale(1.1) ` ) ;
this . buf0 . push ( polygon _shadow ) ;
}
const index = xyToGridIndex ( x , y ) ;
let tile = this . tileTpl . cloneNode ( true ) ;
tile . setAttribute ( 'transform' , ` translate( ${ rx } , ${ ry } ) ` ) ;
this . buf1 . push ( tile ) ;
tile . addEventListener ( 'click' , ( ) => {
this . onTileClick ( index ) ;
} ) ;
this . tiles [ index ] = tile ;
}
/ * *
* Init the orb glyph templates
* /
initGlyphs ( ) {
let symbolPaths = {
salt : 'm 13.229166,291.52894 c -3.193365,0.056 -6.2293596,-2.11498 -7.2925669,-5.11388 -1.1013996,-2.92841 -0.2242509,-6.48596 2.1908174,-8.50166 2.5380605,-2.28864 6.6278455,-2.54558 9.4211355,-0.56925 2.346844,1.54401 3.721353,4.41788 3.400731,7.21296 -0.300944,3.62792 -3.485132,6.74361 -7.122041,6.95045 -0.19901,0.0144 -0.398548,0.0214 -0.598076,0.0214 z m -6.1435917,-8.51297 c 4.0957287,0 8.1914567,0 12.2871847,0 -0.315436,-3.07409 -3.239662,-5.6097 -6.336645,-5.4279 -2.970056,0.0389 -5.6195586,2.4852 -5.9505397,5.4279 z m 6.1435917,6.94037 c 2.96606,0.0757 5.716029,-2.30671 6.114844,-5.23796 0.09389,-0.3732 -0.542301,-0.11772 -0.794471,-0.19272 -3.821322,0 -7.642643,0 -11.4639647,0 0.352809,3.02068 3.0769237,5.49713 6.1435917,5.43068 z' ,
air : 'm 5.1145913,291.51845 c 2.7048586,-5.16509 5.4097167,-10.33018 8.1145757,-15.49527 2.704858,5.16509 5.409717,10.33018 8.114575,15.49527 -5.409717,0 -10.819434,0 -16.2291507,0 z m 6.0387527,-7.84199 c 1.383881,0 2.767763,0 4.151644,0 -0.69194,-1.35592 -1.383881,-2.71185 -2.075821,-4.06777 -0.691941,1.35592 -1.383882,2.71185 -2.075823,4.06777 z m -3.1661517,6.18553 c 3.4946497,0 6.9892987,0 10.4839477,0 -0.789791,-1.54463 -1.579581,-3.08927 -2.369372,-4.6339 -1.915068,0 -3.830136,0 -5.745204,0 -0.7897906,1.54463 -1.5795811,3.08927 -2.3693717,4.6339 z' ,
fire : 'm 5.1145923,291.51845 c 2.7048582,-5.16509 5.4097167,-10.33018 8.1145747,-15.49527 2.704858,5.16509 5.409716,10.33018 8.114574,15.49527 -5.409716,0 -10.819432,0 -16.2291487,0 z m 2.872601,-1.65646 c 3.4946487,0 6.9892977,0 10.4839467,0 -1.747324,-3.41777 -3.494649,-6.83553 -5.241973,-10.2533 -1.747325,3.41777 -3.4946491,6.83553 -5.2419737,10.2533 z' ,
water : 'm 13.229169,291.51845 c -2.704859,-5.16509 -5.409717,-10.33018 -8.1145755,-15.49527 5.4097155,0 10.8194315,0 16.2291465,0 -2.704857,5.16509 -5.409714,10.33018 -8.114571,15.49527 z m 0,-3.58551 c 1.747323,-3.41777 3.494647,-6.83553 5.24197,-10.2533 -3.494648,0 -6.989296,0 -10.4839445,0 1.7473248,3.41777 3.4946495,6.83553 5.2419745,10.2533 z' ,
earth : 'm 13.229167,291.51845 c -2.704858,-5.16509 -5.409717,-10.33018 -8.1145755,-15.49527 5.4097165,0 10.8194335,0 16.2291505,0 -2.704858,5.16509 -5.409717,10.33018 -8.114575,15.49527 z m -2.872601,-9.2049 c 1.915068,0 3.830135,0 5.745203,0 0.789791,-1.54464 1.579581,-3.08927 2.369372,-4.63391 -3.494649,0 -6.989298,0 -10.4839475,0 0.7897908,1.54464 1.5795817,3.08927 2.3693725,4.63391 z m 2.872601,5.61939 c 0.691941,-1.35592 1.383881,-2.71185 2.075822,-4.06777 -1.383881,0 -2.767762,0 -4.151643,0 0.69194,1.35592 1.383881,2.71185 2.075821,4.06777 z' ,
mercury : 'm 15.732407,279.01472 c 1.457188,0.91755 2.327793,2.6639 2.118676,4.38195 -0.130888,2.01739 -1.899178,3.73377 -3.900128,3.89053 0,0.71327 0,1.42654 0,2.13981 0.737153,-0.007 1.474306,-0.0132 2.211459,-0.0198 0,0.34126 0,0.68253 0,1.02379 -0.750804,0.003 -1.501607,0.007 -2.252411,0.01 0.0034,0.68938 0.0068,1.37875 0.01024,2.06813 -0.440244,0 -0.880489,0 -1.320733,0 -0.0034,-0.69279 -0.0068,-1.38558 -0.01024,-2.07837 -0.689374,0 -1.378747,0 -2.068121,0 0,-0.33444 0,-0.66888 0,-1.00332 0.689374,0 1.378747,0 2.068121,0 -0.0068,-0.71327 -0.01365,-1.42654 -0.02047,-2.13981 -2.15006,-0.13324 -4.015907,-2.07311 -3.9813417,-4.24183 -0.084702,-1.60894 0.7922593,-3.17204 2.1384617,-4.03065 -1.3321084,-0.8479 -2.2089181,-2.39019 -2.1397915,-3.98265 0.4846095,0 0.969219,0 1.4538285,0 -0.129447,1.801 1.552959,3.41833 3.338775,3.2762 1.681768,-0.0229 3.155505,-1.58904 3.039642,-3.2762 0.48461,0 0.969221,0 1.453831,0 0.06599,1.601 -0.803406,3.12723 -2.139798,3.98222 z m -2.303602,0.71667 c -1.758778,-0.16425 -3.4619579,1.37986 -3.388854,3.16361 -0.083477,1.71263 1.535734,3.14586 3.204567,3.07148 1.719365,0.0703 3.342464,-1.48587 3.170211,-3.23621 -0.02747,-1.57245 -1.423299,-2.948 -2.985924,-2.99888 z' ,
lead : 'm 9.5126894,279.50182 c -0.3617507,0 -0.7235013,0 -1.085252,0 0,-0.51532 0,-1.03065 0,-1.54597 0.3617507,0 0.7235013,0 1.085252,0 0,-0.6143 0,-1.2286 0,-1.8429 0.5187366,0 1.0374736,0 1.5562106,0 0,0.6143 0,1.2286 0,1.8429 0.607468,0 1.214936,0 1.822404,0 0,0.51532 0,1.03065 0,1.54597 -0.607468,0 -1.214936,0 -1.822404,0 0,1.32073 0,2.64145 0,3.96218 1.537514,-1.62806 4.417116,-1.42308 5.808997,0.28791 1.184192,1.31365 1.566419,3.41483 0.604294,4.96063 -0.519367,0.80942 -0.90468,1.73924 -0.884648,2.71614 -0.515324,0 -1.030647,0 -1.545971,0 -0.04659,-1.19876 0.527295,-2.27553 1.100009,-3.28428 0.92482,-1.59336 -0.125612,-3.92412 -1.965862,-4.19999 -1.635761,-0.31517 -3.124345,1.21479 -3.11374,2.80432 -0.0071,1.55992 -0.0011,3.11998 -0.0031,4.67995 -0.518737,0 -1.037474,0 -1.5562106,0 0,-3.97562 0,-7.95124 0,-11.92686 z' ,
tin : 'm 11.836769,285.99296 c 1.581694,0.0451 3.054161,-1.39068 2.989562,-2.99956 0.06903,-1.76826 -1.701886,-3.28981 -3.441524,-2.95861 -1.4247698,0.18442 -2.5932003,1.53599 -2.5376015,2.97841 -0.5119117,0 -1.0238233,0 -1.535735,0 -0.085695,-2.3801 2.0195976,-4.57675 4.4081085,-4.55471 2.311495,-0.14461 4.495576,1.78385 4.652567,4.08999 0.127445,1.23443 -0.250024,2.52397 -1.095332,3.44405 0.774693,0 1.549385,0 2.324078,0 0,-3.2933 0,-6.58661 0,-9.87991 0.515324,0 1.030647,0 1.545971,0 0,5.10547 0,10.21093 0,15.3164 -0.515324,0 -1.030647,0 -1.545971,0 0,-1.29684 0,-2.59368 0,-3.89052 -3.429807,0 -6.859614,0 -10.2894215,0 0,-0.51532 0,-1.03065 0,-1.54597 1.5084328,1.4e-4 3.0168655,2.9e-4 4.5252985,4.3e-4 z' ,
iron : 'm 11.237833,281.77434 c -2.1129725,-0.0765 -4.0685065,1.7834 -4.0429529,3.90895 -0.1246246,1.8905 1.3843212,3.64089 3.2341329,3.92585 1.98814,0.41339 4.242048,-0.80512 4.735939,-2.82798 0.42126,-1.59401 -0.247897,-3.38375 -1.626517,-4.29444 -0.666348,-0.47624 -1.483823,-0.71835 -2.300602,-0.71238 z m 4.218149,8.01654 c -2.219729,2.26802 -6.2673049,2.23092 -8.4717123,-0.0411 -2.1829952,-2.03247 -2.1495359,-5.81571 -0.085686,-7.91896 1.2047257,-1.38003 3.0857918,-2.10369 4.9042713,-1.84319 1.04112,0.0976 2.126139,0.37802 2.915975,1.10076 1.232001,-1.18763 2.464001,-2.37526 3.696002,-3.56289 -1.266128,0 -2.532257,0 -3.798385,0 0.477784,-0.48461 0.955568,-0.96921 1.433352,-1.45382 1.685895,0 3.371789,0 5.057684,0 0,1.67906 0,3.35812 0,5.03718 -0.488023,0.48803 -0.976045,0.97606 -1.464068,1.46409 0,-1.27296 0,-2.54591 0,-3.81887 -1.221761,1.18763 -2.443522,2.37526 -3.665283,3.56289 1.692564,2.15746 1.51732,5.5939 -0.52215,7.47392 z' ,
copper : 'm 13.229167,276.61943 c -2.112981,-0.0765 -4.0685271,1.78337 -4.04295,3.90894 -0.1277359,1.92476 1.447325,3.68809 3.331873,3.93182 2.025344,0.379 4.29148,-0.9459 4.680309,-3.02816 0.350755,-1.61444 -0.419466,-3.36035 -1.832442,-4.20966 -0.633357,-0.40672 -1.386458,-0.60708 -2.13679,-0.60294 z m -4.1567215,8.05748 c -2.3050816,-2.02524 -2.2795847,-5.96633 -0.1160579,-8.08364 2.0292494,-2.23826 5.8431584,-2.42533 8.0888084,-0.41068 1.604759,1.29217 2.357262,3.46519 1.988157,5.48009 -0.363367,2.37123 -2.511902,4.28305 -4.882744,4.48852 0,0.90097 0,1.80194 0,2.70291 0.928266,0 1.856531,0 2.784797,0 0,0.42318 0,0.84635 0,1.26953 -0.928266,0 -1.856531,0 -2.784797,0 0,0.87708 0,1.75415 0,2.63123 -0.614294,0 -1.228589,0 -1.842883,0 0,-0.87708 0,-1.75415 0,-2.63123 -0.928266,0 -1.856532,0 -2.7847978,0 0,-0.42318 0,-0.84635 0,-1.26953 0.9282658,0 1.8565318,0 2.7847978,0 0,-0.90438 0,-1.80876 0,-2.71314 -1.207048,-0.10173 -2.3584982,-0.63012 -3.2352805,-1.46406 z' ,
silver : 'm 9.9755063,275.73179 c -0.6014253,0.18082 -1.8508344,-0.12736 -1.9078968,0.74722 0.1176844,0.4442 0.7067667,0.30018 1.0195284,0.4996 2.4102181,0.84689 4.3760261,3.03219 4.6611691,5.61268 0.329015,2.2322 -0.292857,4.61262 -1.845188,6.27899 -0.950816,1.0625 -2.3118271,1.60982 -3.6314341,2.04088 -0.3010461,0.22955 -0.1186419,0.78 0.2735733,0.75652 2.3443258,0.46012 4.8961488,-0.14011 6.7090618,-1.72152 1.916707,-1.49245 3.236515,-3.83656 3.14282,-6.30499 -0.01445,-1.96756 -0.864897,-3.88727 -2.285751,-5.24448 -1.577433,-1.67996 -3.811286,-2.7316 -6.1358827,-2.6649 z m 2.7602927,2.03153 c 1.072404,0.39734 1.870102,1.27809 2.623097,2.10375 1.308542,1.47534 1.847166,3.59612 1.292761,5.50102 -0.534285,1.8245 -1.854209,3.45004 -3.574828,4.26864 -0.191682,0.0234 0.647922,-0.682 0.817217,-1.00958 1.833102,-2.3961 2.027928,-5.83879 0.732872,-8.5234 -0.452679,-0.90448 -1.108696,-1.7015 -1.891119,-2.34043 z' ,
gold : 'm 13.219492,275.7189 c -2.986621,0.01 -5.9729442,1.75519 -7.2157492,4.51399 -1.4447671,2.86838 -0.9743467,6.60206 1.2502049,8.94534 1.6486642,1.87365 4.2016723,2.85934 6.6817843,2.61607 3.746213,-0.22926 7.026658,-3.46775 7.307955,-7.20938 0.345627,-3.27431 -1.477563,-6.74322 -4.539965,-8.07025 -1.086444,-0.51129 -2.281439,-0.79866 -3.48423,-0.79577 z m 0,1.55427 c 2.485623,0.002 4.974789,1.48839 5.934079,3.82276 1.125698,2.44571 0.57943,5.58664 -1.449826,7.40023 -2.35696,2.37064 -6.611547,2.37064 -8.9685065,0 -2.1169053,-1.88824 -2.608931,-5.21179 -1.3089579,-7.69696 1.0280707,-2.17935 3.4218714,-3.51551 5.7932114,-3.52603 z m 0.0066,4.19203 c -1.370356,-0.0494 -2.523547,1.30386 -2.277873,2.64791 0.150011,1.3618 1.658854,2.29772 2.951679,1.86852 1.391568,-0.36367 2.080533,-2.14843 1.33192,-3.36846 -0.389128,-0.716 -1.20151,-1.14581 -2.005726,-1.14797 z' ,
mors : 'm 14.482436,292.72274 -0.358077,-0.35808 v -2.86461 h 2.864614 v -1.79038 h -2.864614 v -3.93885 h 5.371152 l -6.445382,-8.95192 -6.0873063,8.95192 h 5.3711513 v 3.93885 H 9.4693607 v 1.79038 h 2.8646133 v 2.86461 l -0.358076,0.35808 z M 16.27282,282.33852 H 9.8274367 l 3.2226923,-4.65501 z' ,
vitae : 'm 11.975898,274.8189 0.358077,0.35808 v 2.86461 H 9.4693607 v 1.79038 h 2.8646143 v 3.93885 H 6.9628227 l 6.4453823,8.95192 6.087306,-8.95192 H 14.12436 v -3.93885 h 2.864613 v -1.79038 H 14.12436 v -2.86461 l 0.358076,-0.35808 z m -1.790384,10.38422 h 6.445383 l -3.222692,4.65501 z' ,
} ;
this . orbColors = {
salt : '#f2e7b4' ,
air : '#9ee0ff' ,
fire : '#ff540b' ,
water : '#0fdac3' ,
earth : '#99ff11' ,
mercury : '#f2e7b4' ,
lead : '#728686' ,
tin : '#c5be9b' ,
iron : '#b59e97' ,
copper : '#f5a581' ,
silver : '#cfcac3' ,
gold : '#ffba50' ,
mors : '#433c29' ,
vitae : '#F5A79E' ,
} ;
this . symbolTpls = { } ;
for ( let s in symbolPaths ) {
if ( symbolPaths . hasOwnProperty ( s ) ) {
let path = symbolPaths [ s ] ;
this . symbolTpls [ s ] = Svg . fromXMLg ( `
< path
d = "${path}"
transform = "scale(2) translate(-13,-283.5)"
stroke = "black"
stroke - width = "2"
stroke - opacity = "0.3"
paint - order = "stroke"
fill = "white"
/ >
` );
}
}
}
/ * *
* Remove all boards on the board
* /
removeAllOrbs ( ) {
Object . keys ( this . grid ) . forEach ( ( n ) => {
this . removeOrbByIndex ( + n ) ;
} )
}
/ * *
* Get orb by array index
*
* @ param { Number } n
* @ returns { object } grid object
* /
getOrbByIndex ( n ) {
return this . grid [ n ] ;
}
/ * *
* Check if a cell is available for selection in - game
* ( checking surroundings only ) . This should work the same regardless
* of whether the cell is occupied or not .
*
* @ param n
* @ return { boolean }
* /
isAvailable ( n ) {
return this . getCellInfo ( n ) . freeSequence >= 3 ;
}
/ * *
* Get info about a cell surroundings and position
*
* neighbours - number of occupied neighbouring cells
* centerWeight - distance from the board center
* freeSequence - number of contiguous free tiles in the direct neighbourhood
*
* @ param n
* @ return { { neighbours : number , centerWeight : number , freeSequence : number } }
* /
getCellInfo ( n ) {
let { x , y } = gridIndexToXy ( n ) ;
let freeSpaces = [
isXyOutside ( x - 1 , y ) || ! this . grid [ n - 1 ] ,
isXyOutside ( x - 1 , y - 1 ) || ! this . grid [ n - 12 ] ,
isXyOutside ( x , y - 1 ) || ! this . grid [ n - 11 ] ,
isXyOutside ( x + 1 , y ) || ! this . grid [ n + 1 ] ,
isXyOutside ( x + 1 , y + 1 ) || ! this . grid [ n + 12 ] ,
isXyOutside ( x , y + 1 ) || ! this . grid [ n + 11 ] ,
] ;
let nOccupied = 0 ;
for ( let i = 0 ; i < 6 ; i ++ ) {
if ( ! freeSpaces [ i ] ) {
nOccupied ++ ;
}
}
let freeSequence = 0 ;
let maxFreeSequence = 0 ;
for ( let i = 0 ; i < 12 ; i ++ ) {
if ( freeSpaces [ i % 6 ] ) {
freeSequence ++ ;
if ( freeSequence >= 6 ) {
maxFreeSequence = freeSequence ;
break ;
}
if ( freeSequence > maxFreeSequence ) {
maxFreeSequence = freeSequence ;
}
} else {
freeSequence = 0 ;
}
}
let { rx , ry } = this . gridXyToCoord ( x , y ) ;
return {
neighbours : nOccupied ,
freeSequence : maxFreeSequence ,
centerWeight : Math . round ( Math . sqrt ( Math . pow ( rx , 2 ) + Math . pow ( ry , 2 ) ) ) ,
} ;
}
/ * *
* Build the game GUI ( buttons , links )
* /
buildGui ( ) {
const addButton = ( x , y , text , classes = '' ) => {
let { rx , ry } = this . guiXyToCoord ( x , y ) ;
let button = Svg . fromXML ( ` <text class="button-text ${ classes ? ' ' + classes : '' } " x=" ${ rx } " y=" ${ ry } "> ${ text } </text> ` ) ;
this . $root . appendChild ( button ) ;
return button ;
} ;
const y0 = 0.05 ;
const x0 = 0 ;
const ysp = 0.75 ;
const ysp2 = 0.6 ;
this . buttons . randomize = addButton ( x0 , y0 , 'Randomize' ) ;
this . buttons . restart = addButton ( x0 , y0 + ysp , 'Try Again' , 'disabled' ) ;
this . buttons . undo = addButton ( x0 , y0 + ysp * 2 , 'Undo' , 'disabled' ) ;
this . buttons . working = addButton ( x0 , y0 + ysp * 3 , 'Working…' , 'working' ) ;
const cfgy0 = 10.5 ;
this . buttons . optFancy = addButton ( x0 , cfgy0 , 'Effects:' , 'config' ) ;
this . buttons . optBlockedEffect = addButton ( x0 , cfgy0 + ysp2 , 'Dim Blocked:' , 'config' ) ;
this . buttons . toggleFullscreen = addButton ( x0 , cfgy0 + ysp2 * - 1.5 , 'Fullscreen' , 'config' ) ;
this . buttons . btnAbout = addButton ( x0 , cfgy0 + ysp2 * - 2.5 , 'Help' , 'config' ) ;
let youWin = Svg . fromXML ( ` <text class="you-win" opacity="0" x="0" y="0">Good work!</text> ` ) ;
this . $root . appendChild ( youWin ) ;
this . youWin = youWin ;
}
/ * *
* Update toggles
*
* @ param cfg
* /
updateSettingsGUI ( cfg ) {
this . buttons . optFancy . textContent = 'Effects: ' + ( cfg . svgEffects ? 'On' : 'Off' ) ;
this . buttons . optBlockedEffect . textContent = 'Dim Blocked: ' + ( cfg . dimBlocked ? 'On' : 'Off' ) ;
}
}
/ * *
* Random number generator
*
* Uses Mullbery32 from https : //stackoverflow.com/a/47593316/2180189
* /
class Rng {
/ * *
* Construct with a given or random seed
*
* @ param { Number | null } seed
* /
constructor ( seed = null ) {
this . seed = null ;
if ( seed === null ) {
seed = + new Date ;
}
this . setSeed ( + seed ) ;
}
/ * *
* Set seed for following rolls
*
* @ param { Number } seed
* /
setSeed ( seed ) {
seed = + seed ;
this . seed = seed ;
this . state = seed ;
}
/ * *
* Get a pseudo - random number
*
* @ returns { Number }
* /
next ( ) {
let t = this . state += 0x6D2B79F5 ;
t = Math . imul ( t ^ t >>> 15 , t | 1 ) ;
t ^= t + Math . imul ( t ^ t >>> 7 , t | 61 ) ;
return ( ( t ^ t >>> 14 ) >>> 0 ) / 4294967296 ;
}
/ * *
* Get next int , inclusive
*
* @ param { Number } max
* @ return { Number }
* /
nextInt ( max ) {
return Math . floor ( ( max + 1 ) * this . next ( ) ) ;
}
/ * *
* 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 . nextInt ( i ) ;
x = a [ i ] ;
a [ i ] = a [ j ] ;
a [ j ] = x ;
}
return a ;
}
/ * *
* Choose a random member of an array
*
* @ param array
* @ return { number }
* /
arrayChoose ( array ) {
return array [ Math . floor ( this . next ( ) * array . length ) ] ;
}
/ * *
* Sneak items in the given order into another array
*
* @ param array - the modified array
* @ param startKeepout - how many leading lements to leave untouched
* @ param endKeepout - how many trailing elements to leave untouched
* @ param items - items to sneak in
* @ return the modified array
* /
arraySneak ( array , startKeepout , endKeepout , items ) {
let positions = [ ] ;
for ( let i = 0 ; i < items . length ; i ++ ) {
positions . push ( startKeepout + this . nextInt ( array . length - startKeepout - endKeepout ) )
}
positions . sort ( ( a , b ) => a - b ) ;
// inject them into the array
items . forEach ( ( pair , i ) => {
array . splice ( positions [ i ] + i , 0 , pair ) ;
} ) ;
return array ;
}
}
class SettingsStorage {
constructor ( ) {
// this object is never overwritten, references are stable
this . defaults = {
version : 1 ,
log : 'info' ,
retryTemplate : 70 ,
attemptTemplates : 20 ,
svgEffects : false ,
dimBlocked : true ,
logSolution : false ,
highlightTemplate : false ,
} ;
this . settings = Object . assign ( { } , this . defaults ) ;
}
load ( ) {
let saved = localStorage . getItem ( 'sigmar_settings' ) ;
if ( saved ) {
let parsed ;
try {
parsed = JSON . parse ( saved ) ;
// XXX some validation / version conversion could be done here
delete parsed . version ;
Object . assign ( this . settings , parsed ) ;
} catch ( e ) {
console . error ( "Error loading settings:" , e ) ;
}
}
return this . settings ;
}
update ( update ) {
Object . assign ( this . settings , update ) ;
return this . settings ;
}
save ( ) {
let changed = Object . entries ( this . settings ) . reduce ( ( acu , [ k , v ] ) => {
if ( this . defaults [ k ] !== v ) {
acu [ k ] = v ;
}
return acu ;
} , { } ) ;
localStorage . setItem ( 'sigmar_settings' , JSON . stringify ( changed ) ) ;
}
}
class Nav {
/ * *
* Replace URL in the address bar
* @ param new _url
* /
static replaceUrl ( new _url ) {
history . replaceState ( null , null , new _url ) ;
}
/ * *
* Set URL args ( GET ) .
*
* @ param args - arguments to set , or delete ( when the value is null )
* /
static setUrlArgs ( args ) {
let url = new URL ( location . href ) ;
let query = new URLSearchParams ( url . search ) ;
for ( let [ k , v ] of Object . entries ( args ) ) {
if ( v === null ) {
query . delete ( k ) ;
} else {
query . set ( k , v ) ;
}
}
url . search = query . toString ( ) ;
history . replaceState ( null , null , url . href ) ;
}
/ * *
* Get URL arguments
*
* @ return { object }
* /
static getUrlArgs ( ) {
let url = new URL ( location . href ) ;
let query = new URLSearchParams ( url . search ) ;
let params = { } ;
for ( const [ key , value ] of query ) {
params [ key ] = value ;
}
return params ;
}
}
class Game {
/ * *
* Init the game
* /
constructor ( ) {
this . LOGLEVELS = [ 'error' , 'warn' , 'info' , 'debug' , 'trace' ] ;
this . settingsStore = new SettingsStorage ( ) ;
this . cfg = this . settingsStore . load ( ) ;
this . applyLogFilter ( ) ;
this . get _opts = {
url _seed : true ,
template : null ,
template _flip : null ,
} ;
let args = Object . assign ( {
seed : null ,
debug : null ,
trace : null ,
log : null ,
pretty : null ,
rnd : null ,
template : null ,
} , Nav . getUrlArgs ( ) ) ;
this . board = new Board ( ) ;
this . rng = new Rng ( ) ;
// Debug can be toggled via the debug=0/1 GET arg
if ( args . debug !== null ) {
this . setCfg ( { log : ( ! ! + args . debug ) ? 'debug' : 'info' } ) ;
}
if ( args . trace !== null ) {
this . setCfg ( { log : ( ! ! + args . trace ) ? 'trace' : 'debug' } ) ;
}
if ( args . log !== null ) {
this . setCfg ( { log : args . log } ) ;
}
if ( args . rnd !== null ) {
this . get _opts . url _seed = ! ! ! + args . rnd ;
}
if ( args . template !== null ) {
let tpl = args . template ;
this . get _opts . template = tpl ;
this . get _opts . template _flip = false ;
if ( tpl . endsWith ( '_flip' ) ) {
this . get _opts . template _flip = true ;
this . get _opts . template = tpl . substring ( 0 , tpl . length - '_flip' . length ) ;
}
}
// Toggle GPU intensive effects via the pretty=0/1 GET arg
if ( args . pretty !== null ) {
this . setCfg ( { svgEffects : ! ! + args . pretty } ) ;
}
this . info ( "Game settings:" , this . cfg ) ;
this . layoutTemplates = {
// templates apparently all have 55 items
//'wheel': [0,1,2,3,4,5,11,12,13,14,15,16,17,22,23,24,27,28,29,33,34,36,38,40,41,44,45,48,49,52,53,55,56,57,58,59,60,61,62,63,64,65,67,68,71,72,75,76,79,80,82,84,86,87,91,92,93,96,97,98,103,104,105,106,107,108,109,115,116,117,118,119,120],
'beyblade' : [ 0 , 1 , 2 , 3 , 4 , 5 , 12 , 14 , 15 , 23 , 26 , 34 , 35 , 37 , 38 , 39 , 40 , 46 , 47 , 48 , 50 , 51 , 52 , 55 , 58 , 60 , 61 , 64 , 65 , 67 , 68 , 69 , 70 , 71 , 73 , 76 , 79 , 80 , 83 , 84 , 85 , 86 , 87 , 91 , 94 , 95 , 97 , 98 , 103 , 104 , 105 , 106 , 109 , 115 , 120 ] ,
'tulip' : [ 4 , 14 , 15 , 16 , 23 , 24 , 25 , 26 , 27 , 28 , 34 , 35 , 36 , 37 , 39 , 40 , 45 , 46 , 47 , 48 , 49 , 50 , 51 , 52 , 56 , 57 , 59 , 60 , 61 , 62 , 63 , 67 , 68 , 69 , 70 , 71 , 72 , 73 , 74 , 80 , 81 , 82 , 83 , 85 , 86 , 93 , 94 , 95 , 96 , 97 , 105 , 106 , 107 , 108 , 109 ] ,
'alien' : [ 3 , 4 , 14 , 15 , 16 , 22 , 25 , 26 , 27 , 28 , 34 , 35 , 36 , 37 , 38 , 39 , 40 , 41 , 45 , 46 , 48 , 49 , 51 , 56 , 57 , 58 , 59 , 60 , 61 , 62 , 67 , 68 , 69 , 70 , 71 , 72 , 73 , 74 , 79 , 80 , 81 , 82 , 84 , 85 , 86 , 94 , 95 , 96 , 97 , 98 , 106 , 107 , 108 , 109 , 117 ] ,
'cube' : [ 1 , 5 , 12 , 13 , 14 , 15 , 16 , 17 , 23 , 27 , 29 , 33 , 34 , 35 , 36 , 37 , 38 , 40 , 44 , 48 , 49 , 51 , 52 , 55 , 56 , 57 , 58 , 59 , 60 , 61 , 62 , 64 , 68 , 70 , 71 , 72 , 73 , 75 , 76 , 80 , 82 , 84 , 86 , 92 , 94 , 96 , 97 , 103 , 104 , 105 , 106 , 108 , 118 , 119 , 120 ] ,
'star' : [ 3 , 14 , 15 , 22 , 23 , 24 , 25 , 26 , 27 , 34 , 35 , 36 , 37 , 38 , 39 , 40 , 41 , 46 , 47 , 48 , 49 , 50 , 51 , 52 , 57 , 58 , 59 , 60 , 61 , 62 , 63 , 68 , 69 , 70 , 71 , 72 , 73 , 74 , 79 , 80 , 81 , 82 , 83 , 84 , 85 , 86 , 93 , 94 , 95 , 96 , 97 , 98 , 105 , 106 , 117 ] ,
'flower' : [ 3 , 11 , 12 , 13 , 14 , 15 , 23 , 25 , 27 , 28 , 34 , 36 , 37 , 38 , 39 , 40 , 45 , 46 , 47 , 48 , 49 , 50 , 52 , 53 , 57 , 58 , 59 , 60 , 61 , 62 , 64 , 68 , 70 , 71 , 72 , 73 , 74 , 75 , 79 , 80 , 81 , 82 , 83 , 84 , 86 , 92 , 95 , 96 , 97 , 98 , 104 , 105 , 106 , 107 , 116 ] ,
'windmill' : [ 4 , 11 , 12 , 13 , 14 , 15 , 16 , 23 , 24 , 25 , 27 , 28 , 34 , 37 , 39 , 40 , 45 , 46 , 47 , 48 , 49 , 50 , 52 , 53 , 56 , 57 , 59 , 60 , 61 , 63 , 64 , 67 , 68 , 70 , 71 , 72 , 73 , 74 , 75 , 80 , 81 , 83 , 86 , 92 , 93 , 95 , 96 , 97 , 104 , 105 , 106 , 107 , 108 , 109 , 116 ] ,
'propeller' : [ 1 , 2 , 3 , 4 , 13 , 14 , 15 , 16 , 22 , 25 , 28 , 34 , 36 , 37 , 38 , 39 , 40 , 41 , 45 , 46 , 47 , 48 , 50 , 56 , 58 , 60 , 61 , 62 , 67 , 68 , 70 , 71 , 73 , 74 , 75 , 76 , 79 , 80 , 81 , 82 , 83 , 84 , 86 , 87 , 91 , 92 , 95 , 97 , 98 , 103 , 106 , 107 , 108 , 109 , 117 ] ,
'garden' : [ 0 , 1 , 2 , 3 , 4 , 5 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 22 , 23 , 28 , 29 , 33 , 34 , 40 , 41 , 44 , 45 , 52 , 53 , 55 , 56 , 60 , 64 , 65 , 67 , 68 , 75 , 76 , 79 , 80 , 86 , 87 , 91 , 92 , 97 , 98 , 103 , 104 , 105 , 106 , 107 , 108 , 109 , 115 , 116 , 117 , 118 , 119 , 120 ] ,
'windmill2' : [ 1 , 12 , 13 , 14 , 15 , 16 , 17 , 23 , 24 , 26 , 27 , 28 , 34 , 35 , 36 , 37 , 38 , 40 , 44 , 45 , 47 , 50 , 51 , 52 , 56 , 57 , 58 , 60 , 62 , 63 , 64 , 68 , 69 , 70 , 73 , 75 , 76 , 80 , 82 , 83 , 84 , 85 , 86 , 92 , 93 , 94 , 96 , 97 , 103 , 104 , 105 , 106 , 107 , 108 , 119 ] ,
'bird' : [ 2 , 3 , 4 , 14 , 15 , 25 , 27 , 28 , 29 , 33 , 34 , 35 , 36 , 37 , 38 , 39 , 40 , 45 , 46 , 47 , 48 , 49 , 50 , 51 , 57 , 58 , 59 , 60 , 61 , 62 , 67 , 68 , 70 , 71 , 72 , 73 , 74 , 79 , 80 , 81 , 82 , 83 , 84 , 86 , 87 , 91 , 94 , 95 , 96 , 97 , 98 , 106 , 107 , 109 , 118 ] ,
'strider' : [ 1 , 2 , 3 , 4 , 11 , 12 , 13 , 14 , 15 , 24 , 25 , 26 , 36 , 37 , 38 , 47 , 48 , 49 , 50 , 53 , 58 , 59 , 60 , 61 , 62 , 63 , 64 , 67 , 68 , 69 , 70 , 71 , 72 , 73 , 74 , 75 , 76 , 79 , 80 , 81 , 82 , 83 , 84 , 85 , 86 , 87 , 91 , 92 , 93 , 97 , 98 , 103 , 104 , 109 , 116 ] ,
'campfire' : [ 0 , 1 , 2 , 3 , 4 , 5 , 11 , 15 , 17 , 22 , 23 , 24 , 25 , 26 , 27 , 29 , 33 , 35 , 39 , 41 , 44 , 46 , 51 , 52 , 53 , 55 , 57 , 60 , 63 , 65 , 67 , 68 , 69 , 74 , 76 , 79 , 81 , 85 , 87 , 91 , 93 , 94 , 95 , 96 , 97 , 98 , 103 , 105 , 109 , 115 , 116 , 117 , 118 , 119 , 120 ] ,
'skillet' : [ 0 , 1 , 2 , 5 , 11 , 13 , 17 , 22 , 25 , 26 , 27 , 28 , 29 , 33 , 37 , 39 , 41 , 44 , 45 , 46 , 47 , 48 , 49 , 50 , 53 , 55 , 57 , 59 , 60 , 61 , 65 , 69 , 70 , 71 , 72 , 73 , 74 , 75 , 76 , 81 , 83 , 85 , 87 , 91 , 92 , 95 , 96 , 103 , 107 , 115 , 116 , 117 , 118 , 119 , 120 ] ,
'digger' : [ 2 , 3 , 4 , 5 , 16 , 17 , 22 , 27 , 28 , 29 , 33 , 34 , 35 , 36 , 37 , 38 , 39 , 40 , 41 , 44 , 45 , 46 , 47 , 48 , 50 , 51 , 55 , 56 , 57 , 58 , 60 , 61 , 62 , 67 , 70 , 71 , 73 , 79 , 82 , 83 , 84 , 87 , 91 , 94 , 95 , 96 , 98 , 106 , 107 , 108 , 109 , 117 , 118 , 119 , 120 ] ,
'chestnut' : [ 3 , 4 , 12 , 13 , 14 , 15 , 16 , 23 , 25 , 27 , 28 , 34 , 36 , 38 , 39 , 40 , 45 , 46 , 47 , 48 , 49 , 50 , 52 , 56 , 57 , 58 , 59 , 60 , 61 , 62 , 64 , 67 , 68 , 71 , 72 , 74 , 75 , 79 , 80 , 81 , 82 , 83 , 84 , 86 , 92 , 95 , 96 , 97 , 98 , 104 , 105 , 106 , 107 , 108 , 109 ] ,
'manta' : [ 0 , 5 , 11 , 16 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 , 33 , 34 , 35 , 39 , 40 , 44 , 45 , 46 , 47 , 51 , 52 , 56 , 57 , 58 , 59 , 60 , 61 , 62 , 63 , 64 , 68 , 69 , 73 , 74 , 75 , 76 , 80 , 81 , 85 , 86 , 87 , 91 , 92 , 93 , 94 , 95 , 96 , 97 , 98 , 104 , 109 , 115 , 120 ] ,
'pyramids' : [ 3 , 4 , 14 , 15 , 16 , 23 , 25 , 26 , 28 , 34 , 35 , 36 , 37 , 38 , 39 , 40 , 45 , 46 , 47 , 48 , 49 , 50 , 51 , 52 , 56 , 58 , 59 , 60 , 61 , 62 , 67 , 68 , 69 , 70 , 71 , 72 , 73 , 74 , 79 , 80 , 81 , 82 , 83 , 84 , 85 , 86 , 94 , 95 , 97 , 98 , 105 , 106 , 107 , 108 , 109 ] ,
'bigwheel' : [ 0 , 1 , 2 , 3 , 4 , 5 , 11 , 13 , 17 , 22 , 25 , 28 , 29 , 33 , 36 , 37 , 38 , 39 , 41 , 44 , 45 , 46 , 47 , 50 , 53 , 55 , 58 , 60 , 62 , 65 , 67 , 70 , 73 , 74 , 75 , 76 , 79 , 81 , 82 , 83 , 84 , 87 , 91 , 92 , 95 , 98 , 103 , 107 , 109 , 115 , 116 , 117 , 118 , 119 , 120 ] ,
'handshake' : [ 0 , 1 , 2 , 3 , 4 , 5 , 11 , 12 , 13 , 15 , 16 , 22 , 23 , 24 , 25 , 27 , 33 , 34 , 35 , 36 , 38 , 46 , 48 , 49 , 50 , 58 , 59 , 60 , 61 , 62 , 70 , 71 , 72 , 74 , 82 , 84 , 85 , 86 , 87 , 93 , 95 , 96 , 97 , 98 , 104 , 105 , 107 , 108 , 109 , 115 , 116 , 117 , 118 , 119 , 120 ] ,
'thinwheel' : [ 0 , 1 , 2 , 3 , 4 , 5 , 11 , 12 , 16 , 17 , 22 , 24 , 27 , 29 , 33 , 36 , 38 , 41 , 44 , 48 , 49 , 53 , 55 , 56 , 57 , 58 , 59 , 60 , 61 , 62 , 63 , 64 , 65 , 67 , 71 , 72 , 76 , 79 , 82 , 84 , 87 , 91 , 93 , 96 , 98 , 103 , 104 , 108 , 109 , 115 , 116 , 117 , 118 , 119 , 120 ] ,
'heavywheel' : [ 12 , 13 , 14 , 15 , 16 , 23 , 24 , 25 , 27 , 28 , 34 , 36 , 37 , 38 , 39 , 40 , 45 , 46 , 47 , 48 , 49 , 50 , 52 , 56 , 57 , 58 , 59 , 60 , 61 , 62 , 63 , 64 , 68 , 70 , 71 , 72 , 73 , 74 , 75 , 80 , 81 , 82 , 83 , 84 , 86 , 92 , 93 , 95 , 96 , 97 , 104 , 105 , 106 , 107 , 108 ] ,
'virus' : [ 2 , 3 , 13 , 14 , 15 , 22 , 24 , 26 , 27 , 34 , 35 , 36 , 37 , 38 , 39 , 40 , 41 , 46 , 47 , 48 , 49 , 50 , 51 , 57 , 58 , 59 , 60 , 61 , 62 , 63 , 68 , 69 , 70 , 71 , 72 , 73 , 75 , 79 , 80 , 82 , 83 , 84 , 85 , 86 , 87 , 91 , 92 , 93 , 94 , 95 , 96 , 97 , 98 , 106 , 117 ] ,
'frisbee' : [ 0 , 11 , 12 , 13 , 14 , 15 , 22 , 25 , 26 , 27 , 28 , 29 , 33 , 34 , 36 , 38 , 39 , 40 , 41 , 45 , 46 , 47 , 49 , 50 , 53 , 57 , 58 , 59 , 60 , 62 , 64 , 65 , 68 , 69 , 72 , 74 , 75 , 80 , 81 , 82 , 83 , 84 , 85 , 86 , 92 , 95 , 96 , 97 , 104 , 106 , 107 , 115 , 116 , 117 , 118 ] ,
} ;
this . applySettings ( ) ;
this . installButtonHandlers ( ) ;
// this.board.testGraphics();
// window.orb = this.board.getOrbByIndex(0).node;
// return;
// Defer start to give browser time to render the background
setTimeout ( ( ) => {
this . newGameWithLoader ( args . seed )
} , 50 ) ;
}
applyLogFilter ( ) {
let index = this . LOGLEVELS . indexOf ( this . cfg . log ) ;
for ( let level of this . LOGLEVELS ) {
this [ 'logging_' + level ] = index >= this . LOGLEVELS . indexOf ( level ) ;
}
}
trace ( ... args ) {
if ( this . logging _trace ) console . debug ( ... args ) ;
}
debug ( ... args ) {
if ( this . logging _debug ) console . log ( ... args ) ;
}
info ( ... args ) {
if ( this . logging _info ) console . info ( ... args ) ;
}
warn ( ... args ) {
if ( this . logging _warn ) console . warn ( ... args ) ;
}
error ( ... args ) {
if ( this . logging _error ) console . error ( ... args ) ;
}
applySettings ( ) {
this . board . $svg . classList . toggle ( 'cfg-no-anim' , ! this . cfg . svgEffects ) ;
this . board . $svg . classList . toggle ( 'cfg-anim' , this . cfg . svgEffects ) ;
this . board . $svg . classList . toggle ( 'cfg-no-blur' , ! this . cfg . svgEffects ) ;
this . board . $svg . classList . toggle ( 'cfg-blur' , this . cfg . svgEffects ) ;
this . board . $svg . classList . toggle ( 'cfg-fade-disabled' , this . cfg . dimBlocked ) ;
this . board . $svg . classList . toggle ( 'cfg-no-fade-disabled' , ! this . cfg . dimBlocked ) ;
this . applyLogFilter ( ) ;
this . board . updateSettingsGUI ( this . cfg ) ;
}
setCfg ( update ) {
this . settingsStore . update ( update ) ;
this . applySettings ( ) ;
this . settingsStore . save ( ) ;
}
getCfg ( key ) {
return this . cfg [ key ] ;
}
/ * *
* Show a selected template , for debug
*
* @ param { String } name - template name
* @ param { boolean } flip - flip horizontally
* /
showTemplate ( name , flip = false ) {
this . board . removeAllOrbs ( ) ;
for ( let n of this . getTemplate ( name , flip ) . positions ) {
this . board . placeOrbByIndex ( n , 'lead' ) ;
}
}
/ * *
* Show a random template , for debug
* /
showRandomTemplate ( ) {
this . board . removeAllOrbs ( ) ;
for ( let n of this . getRandomTemplate ( ) . positions ) {
this . board . placeOrbByIndex ( n , 'lead' ) ;
}
}
/ * *
* Get a template - a sequence of numbers that are allowed as orb positions
*
* @ param { String } name
* @ param { boolean } flipped
* @ returns { { name : String , flipped : Boolean , positions : Number [ ] } }
* /
getTemplate ( name , flipped ) {
let positions = this . layoutTemplates [ name ] . slice ( 0 ) ; // this slice takes a copy so the array is not corrupted by later manipulations
if ( flipped ) {
positions = this . flipTemplate ( positions ) ;
}
return {
basename : name ,
name : name + ( flipped ? '_flip' : '' ) ,
flipped ,
positions ,
} ;
}
/ * *
* Get a random and randomly flipped template
*
* @ returns { { name : String , flipped : Boolean , positions : Number [ ] } }
* /
getRandomTemplate ( ) {
let names = Object . keys ( this . layoutTemplates ) ;
let name = names [ Math . floor ( this . rng . next ( ) * names . length ) ] ;
let flipped = this . rng . next ( ) > 0.5 ;
let tpl = this . getTemplate ( name , flipped ) ;
// 60 (center) must be included to place gold
if ( ! tpl . positions . includes ( 60 ) ) {
throw Error ( ` Template " ${ name } ", flip= ${ + flipped } , lacks 60. ` ) ;
}
return tpl ;
}
/ * *
* Flip a template array .
*
* The array is modified in place !
*
* @ param { Number [ ] } tpl
* @ returns { Number [ ] }
* /
flipTemplate ( tpl ) {
return tpl
. sort ( ( a , b ) => a - b )
. map ( ( n ) => {
let { x , y } = gridIndexToXy ( n ) ;
return xyToGridIndex ( 5 + y - x , y ) ;
} )
. sort ( ( a , b ) => a - b ) ;
}
/ * *
* Print array of all occupied orbs as a layout template
*
* @ returns { Number [ ] }
* /
toTemplate ( ) {
return Object . keys ( game . board . grid )
. map ( ( x ) => + x )
. sort ( ( a , b ) => a - b ) ;
}
/ * *
* Run a template editor
*
* - click on tiles to toggle orbs
* - call ` game.showTemplate('wheel') ` to show an existing template on the board
* - call ` JSON.stringify(game.toTemplate()) ` to print the current template array to console
* /
templateBuilder ( ) {
this . board . removeAllOrbs ( ) ;
this . board . onTileClick = ( n ) => {
let symbol = 'lead' ;
this . board . placeOrbByIndex ( n , symbol ) ;
} ;
this . board . onOrbClick = ( n , orb ) => {
this . board . removeOrbByIndex ( n )
} ;
}
placeOrbs ( template ) {
let placer = new RecursiveOrbPlacer ( this , template ) ;
return placer . place ( ) ;
}
getPairSymbols ( first ) {
return {
'salt' : [ 'salt' , 'air' , 'fire' , 'water' , 'earth' ] ,
'air' : [ 'salt' , 'air' ] ,
'fire' : [ 'salt' , 'fire' ] ,
'water' : [ 'salt' , 'water' ] ,
'earth' : [ 'salt' , 'earth' ] ,
'mercury' : [ this . nextMetal ] ,
'lead' : [ 'mercury' ] ,
'tin' : [ 'mercury' ] ,
'iron' : [ 'mercury' ] ,
'copper' : [ 'mercury' ] ,
'silver' : [ 'mercury' ] ,
'gold' : [ ] ,
'mors' : [ 'vitae' ] ,
'vitae' : [ 'mors' ] ,
} [ first ] ;
}
advanceMetal ( ) {
if ( this . nextMetal === 'gold' ) throw new Error ( "No metals to unlock beyond gold." ) ;
this . nextMetal = METAL _SEQ [ METAL _SEQ . indexOf ( this . nextMetal ) + 1 ] ;
console . debug ( ` Next metal unlocked: ${ this . nextMetal } ` ) ;
}
/ * *
* Convert the strings in the board array to actual SVG orbs ( strings are a placeholder to speed up board solving )
* /
renderPreparedBoard ( ) {
for ( let n = 0 ; n < BOARD _SIZE ; n ++ ) {
if ( this . board . grid [ n ] !== null ) {
const symbol = this . board . grid [ n ] ;
this . board . grid [ n ] = null ;
this . board . placeOrbByIndex ( n , symbol ) ;
}
}
}
/ * *
* Check if a tile is available at play - time ( checking unlocked metals )
*
* @ param n
* @ return { Boolean }
* /
isAvailableAtPlaytime ( n ) {
let ava = this . board . isAvailable ( n ) ;
const sym = this . board . grid [ n ] . symbol ;
if ( METAL _SEQ . includes ( sym ) ) {
if ( sym !== this . nextMetal ) {
ava = false ;
}
}
return ava ;
}
addUndoRecord ( orbs ) {
this . undoStack . push ( {
nextMetal : this . nextMetal ,
orbs ,
} )
}
undo ( ) {
if ( ! this . undoStack . length ) {
console . warn ( "Undo stack is empty." ) ;
return ;
}
let item = this . undoStack . pop ( ) ;
this . nextMetal = item . nextMetal ;
for ( let entry of item . orbs ) {
this . debug ( ` Undo orb ${ entry . symbol } at ${ entry . n } ` ) ;
this . board . placeOrbByIndex ( entry . n , entry . symbol ) ;
}
this . updateGameGui ( ) ;
}
/ * *
* Handle orb click
*
* @ param n
* @ param orb
* /
inGameBoardClick ( n , orb ) {
let removed = false ;
this . debug ( ` Clicked orb ${ n } : ${ orb . symbol } ` ) ;
if ( ! this . isAvailableAtPlaytime ( n ) ) {
this . debug ( ` Orb is blocked ` ) ;
return ;
}
let wantRefresh = false ;
if ( orb . symbol === 'gold' ) {
// gold has no pairing
this . debug ( ` Removing gold. ` ) ;
this . addUndoRecord ( [ {
symbol : orb . symbol ,
n ,
} ] ) ;
this . board . removeOrbByIndex ( n ) ;
this . selectedOrb = null ;
removed = true ;
wantRefresh = true ;
} else if ( this . selectedOrb === null ) {
this . debug ( ` Select orb ` ) ;
// first selection
this . selectedOrb = { n , orb } ;
orb . node . classList . add ( 'selected' ) ;
} else {
if ( this . selectedOrb . n === n ) {
this . debug ( ` Unselect orb ` ) ;
// orb clicked twice
orb . node . classList . remove ( 'selected' ) ;
this . selectedOrb = null ;
} else {
this . debug ( ` Second selection, try to match ` ) ;
// second orb in a pair
const otherSymbol = this . selectedOrb . orb . symbol ;
if ( this . getPairSymbols ( orb . symbol ) . includes ( otherSymbol ) ) {
this . debug ( ` Match confirmed, removing ${ this . selectedOrb . n } ( ${ this . selectedOrb . orb . symbol } + ${ n } ( ${ orb . symbol } ` ) ;
this . addUndoRecord ( [
{
symbol : this . selectedOrb . orb . symbol ,
n : this . selectedOrb . n ,
} ,
{
symbol : orb . symbol ,
n ,
}
] ) ;
// compatible pair clicked
this . board . removeOrbByIndex ( n ) ;
this . board . removeOrbByIndex ( this . selectedOrb . n ) ;
removed = true ;
if ( [ orb . symbol , otherSymbol ] . includes ( this . nextMetal ) ) {
this . debug ( "Advance metal transmutation sequence." ) ;
this . advanceMetal ( ) ;
}
this . selectedOrb = null ;
wantRefresh = true ;
} else {
this . debug ( "No match, start new pair selection." ) ;
// 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 . board . countOrbs ( ) === 0 ) {
this . info ( "Good work!" ) ;
if ( removed ) {
setTimeout ( ( ) => {
this . board . youWin . classList . add ( 'show' ) ;
} , 500 ) ;
}
}
this . updateGameGui ( ) ;
}
}
/ * *
* Add event handlers for the menu buttons
* /
installButtonHandlers ( ) {
this . board . buttons . restart . addEventListener ( 'click' , ( ) => {
this . info ( "New Game with the same seed" ) ;
while ( this . undoStack . length ) {
this . undo ( ) ;
}
} ) ;
this . board . buttons . randomize . addEventListener ( 'click' , ( ) => {
this . info ( "New Game with a random seed" ) ;
this . newGameWithLoader ( + new Date ) ;
} ) ;
this . board . buttons . btnAbout . addEventListener ( 'click' , ( ) => {
let url = 'https://git.ondrovo.com/MightyPork/sigmar' ;
this . info ( ` Opening docs page in new tab: ${ url } ` ) ;
window . open ( url ) ;
} ) ;
this . board . buttons . undo . addEventListener ( 'click' , ( ) => {
if ( this . undoStack . length ) {
this . undo ( ) ;
} else {
this . warn ( "Nothing to undo." ) ;
}
} ) ;
this . board . buttons . optFancy . addEventListener ( 'click' , ( ) => {
this . info ( "Toggle effects" ) ;
this . setCfg ( {
svgEffects : ! this . cfg . svgEffects ,
} )
} ) ;
this . board . buttons . optBlockedEffect . addEventListener ( 'click' , ( ) => {
this . info ( "Toggle blocked dim" ) ;
this . setCfg ( {
dimBlocked : ! this . cfg . dimBlocked ,
} )
} ) ;
this . board . buttons . toggleFullscreen . addEventListener ( 'click' , ( ) => {
this . info ( "Toggle Fullscreen" ) ;
if ( document . fullscreenElement ) {
document . exitFullscreen ( ) ;
} else {
this . board . $svg . requestFullscreen ( ) ;
}
} ) ;
}
/ * *
* Update button hiding attributes , disabled orb effects , etc
* /
updateGameGui ( ) {
let nOrbs = this . board . countOrbs ( ) ;
this . board . buttons . restart
. classList . toggle ( 'disabled' , nOrbs === 55 ) ;
this . board . buttons . undo
. classList . toggle ( 'disabled' , this . undoStack . length === 0 ) ;
// Update orb disabled status
for ( let n = 0 ; n < BOARD _SIZE ; n ++ ) {
if ( this . board . grid [ n ] ) {
const disabled = ! this . isAvailableAtPlaytime ( n ) ;
let node = this . board . grid [ n ] . node ;
if ( node . classList . contains ( 'disabled' ) !== disabled ) {
node . classList . toggle ( 'disabled' , disabled ) ;
}
}
}
if ( nOrbs !== 0 ) {
this . board . youWin . classList . remove ( 'show' ) ;
}
}
newGameWithLoader ( seed ) {
this . board . buttons . working . classList . add ( 'show' ) ;
setTimeout ( ( ) => {
this . newGame ( seed ) ;
this . board . buttons . working . classList . remove ( 'show' ) ;
} , 20 ) ;
}
newGame ( seed ) {
if ( seed !== null ) {
this . rng . setSeed ( seed ) ;
}
this . info ( "RNG seed is: " + this . rng . seed ) ;
if ( this . get _opts . url _seed ) {
// Place seed in the navbar for bookmarking / sharing
Nav . setUrlArgs ( {
seed : this . rng . seed ,
} ) ;
}
this . board . onTileClick = ( n ) => {
this . debug ( n , gridIndexToXy ( n ) ) ;
} ;
this . selectedOrb = null ;
this . nextMetal = 'lead' ;
this . undoStack = [ ] ;
let self = this ;
this . board . onOrbClick = ( n , orb ) => self . inGameBoardClick ( n , orb ) ;
// retry loop, should not be needed if everything is correct
let suc = false ;
let retry _count = 0 ;
let board _info = null ;
for ( let n _tpl = 0 ; n _tpl < this . cfg . attemptTemplates && ! suc ; n _tpl ++ ) {
this . debug ( 'RNG seed is: ' + this . rng . state ) ;
let template ;
if ( n _tpl === 0 && this . get _opts . template !== null ) {
template = this . getTemplate ( this . get _opts . template , this . get _opts . template _flip ) ;
} else {
template = this . getRandomTemplate ( ) ;
}
this . info ( ` Selected board layout template " ${ template . name } " ` ) ;
for ( let n _solution = 0 ; n _solution < this . cfg . retryTemplate ; n _solution ++ ) {
try {
board _info = this . placeOrbs ( template . positions . slice ( 0 ) ) ; // clone
board _info . template = template ;
suc = true ;
break ;
} catch ( e ) {
retry _count ++ ;
this . warn ( e . message ) ;
}
}
if ( ! suc ) {
this . warn ( ` Exhausted all retries for the template " ${ template . name } ", getting a new one ` ) ;
}
}
this . renderPreparedBoard ( ) ;
this . updateGameGui ( ) ;
if ( ! suc ) {
alert ( ` Sorry, could not find a valid board setup after ${ retry _count } retries. ` ) ;
} else {
this . info ( ` Board set up with ${ retry _count } retries. ` ) ;
if ( this . cfg . logSolution ) {
this . info ( 'Reference solution:\n ' + board _info . solution . reduce ( ( s , entry , i ) => {
s += ` ${ entry [ 0 ] } ${ entry [ 1 ] } ` ;
if ( i % 2 === 1 ) {
s += "\n " ;
} else {
if ( entry [ 0 ] !== 'gold' ) {
s += " + " ;
}
}
return s ;
} , '' ) ) ;
}
}
}
}
/ * *
* Base class for orb placer .
* /
class BaseOrbPlacer {
constructor ( game , template ) {
this . template = template ;
this . rng = game . rng ;
this . cfg = game . cfg ;
this . game = game ;
this . board = game . board ;
}
/ * *
* Place an orb . The position must be inside template and free .
*
* @ param n - position
* @ param symbol - symbol to place
* /
placeOrb ( n , symbol ) {
if ( ! this . templateMap [ 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 ] } ` ) ;
}
this . trace ( ` Place ${ n } <- ${ symbol } ` ) ;
this . board . grid [ n ] = symbol ;
}
/ * *
* Remove an orb , if any .
*
* @ param n - position
* /
removeOrb ( n ) {
let old = this . board . grid [ n ] ;
this . board . grid [ n ] = null ;
this . trace ( ` Unplace ${ n } ( ${ old } ) ` ) ;
return old ;
}
/** Check if a cell is available for selection - 3 free slots */
isAvailable ( n ) {
return this . board . isAvailable ( n ) ;
}
/** Get cell info by number */
getCellInfo ( n ) {
return this . board . getCellInfo ( n ) ;
}
trace ( ... args ) {
this . game . debug ( ... args ) ;
}
debug ( ... args ) {
this . game . debug ( ... args ) ;
}
info ( ... args ) {
this . game . info ( ... args ) ;
}
warn ( ... args ) {
this . game . warn ( ... args ) ;
}
error ( ... args ) {
this . game . error ( ... args ) ;
}
/ * *
* Build a list of orbs in the right order .
* They are always grouped in matching pairs , and metals are always in
* the correct reaction order .
*
* @ return { string [ ] }
* /
buildPlacementList ( ) {
let toPlace = [
[ 'air' , 'air' ] ,
[ 'air' , 'air' ] ,
[ 'air' , 'air' ] ,
[ 'air' , 'air' ] ,
[ 'fire' , 'fire' ] ,
[ 'fire' , 'fire' ] ,
[ 'fire' , 'fire' ] ,
[ 'fire' , 'fire' ] ,
[ 'water' , 'water' ] ,
[ 'water' , 'water' ] ,
[ 'water' , 'water' ] ,
[ 'water' , 'water' ] ,
[ 'earth' , 'earth' ] ,
[ 'earth' , 'earth' ] ,
[ 'earth' , 'earth' ] ,
[ 'earth' , 'earth' ] ,
] ;
let newSaltedPairs = [ ] ;
const nsalted = this . rng . nextInt ( 2 ) ;
for ( let i = 0 ; i < nsalted ; i ++ ) {
while ( true ) {
const n = this . rng . nextInt ( toPlace . length - 1 ) ;
if ( toPlace [ n ] [ 1 ] !== 'salt' ) {
this . trace ( ` Pairing ${ toPlace [ n ] [ 1 ] } with salt. ` ) ;
newSaltedPairs . push ( [ toPlace [ n ] [ 1 ] , 'salt' ] ) ;
toPlace [ n ] [ 1 ] = 'salt' ;
break ;
}
}
}
toPlace = toPlace . concat ( newSaltedPairs ) ;
// if we have some salt pairs left
for ( let i = 0 ; i < 2 - nsalted ; i ++ ) {
toPlace . push ( [ 'salt' , 'salt' ] ) ;
}
// shuffle the pairs that have random order (i.e. not metals)
this . rng . arrayShuffle ( toPlace ) ;
this . rng . arraySneak ( toPlace , 3 , 0 , [
[ 'mors' , 'vitae' ] ,
[ 'mors' , 'vitae' ] ,
[ 'mors' , 'vitae' ] ,
[ 'mors' , 'vitae' ] ,
] ) ;
this . rng . arraySneak ( toPlace , 4 , 0 , [
[ 'lead' , 'mercury' ] ,
[ 'tin' , 'mercury' ] ,
[ 'iron' , 'mercury' ] ,
[ 'copper' , 'mercury' ] ,
[ 'silver' , 'mercury' ] ,
] ) ;
this . debug ( 'Placement order (last first):' , toPlace ) ;
return toPlace . reduce ( ( a , c ) => {
a . push ( c [ 0 ] ) ;
a . push ( c [ 1 ] ) ;
return a ;
} , [ ] ) ;
}
/ * *
* Run the placement logic .
* /
place ( ) {
this . board . removeAllOrbs ( ) ;
this . solution = [ ] ;
let allowedTable = [ ] ;
let outsideTemplate = [ ] ;
for ( let i = 0 ; i < BOARD _SIZE ; i ++ ) {
const allo = this . template . includes ( i ) ;
allowedTable . push ( allo ) ;
let { x , y } = gridIndexToXy ( i ) ;
if ( ! allo && ! isXyOutside ( x , y ) ) {
outsideTemplate . push ( i ) ;
}
// Highlight pattern shape
if ( this . cfg . highlightTemplate ) {
if ( this . board . tiles [ i ] ) {
if ( allo ) {
this . board . tiles [ i ] . setAttribute ( 'opacity' , 1 )
} else {
this . board . tiles [ i ] . setAttribute ( 'opacity' , 0.6 )
}
}
}
}
this . templateMap = allowedTable ;
this . outsideTemplate = outsideTemplate ;
this . placeOrb ( 60 , 'gold' ) ;
this . solution . push ( [ 'gold' , 60 ] ) ;
let toPlace = this . buildPlacementList ( ) ;
let rv = this . doPlace ( toPlace ) ;
let solution = this . solution ;
solution . reverse ( ) ;
this . info ( "Found a valid board!" ) ;
solution . forEach ( ( a ) => {
let p = gridIndexToXy ( a [ 1 ] ) ;
a [ 1 ] = ` ${ p . x } × ${ p . y } ` ;
} ) ;
this . debug ( 'Solution:' , solution ) ;
rv . solution = solution ;
return rv ;
}
/ * *
* Perform the orbs placement .
*
* Orb symbols to place are given as an argument .
*
* The layout template is in ` this.template ` , also ` this.templateMap ` as an array
* with indices 0 - 121 containing true / false to indicate template membership .
*
* ` this.outsideTemplate ` is a list of cell indices that are not in the template .
*
* this . solution is an array to populate with orb placements in the format [ n , symbol ] .
*
* The board is cleared now . When the function ends , the board should contain
* the new layout ( as symbol name strings )
*
* After placing each orb , make sure to call ` this.solution.push([symbol, index]) ` ;
* in case of backtracking , pop it again .
*
* Return object to return to parent ; 'solution' will be added automatically .
* /
doPlace ( toPlace ) {
throw new Error ( "Not implemented" ) ;
}
}
class RecursiveOrbPlacer extends BaseOrbPlacer {
doPlace ( toPlace ) {
this . toPlace = toPlace ;
this . recDepth = 0 ;
this . placeOne ( 0 ) ;
return { } ;
}
placeOne ( ) {
this . recDepth ++ ;
if ( this . recDepth > 1000 ) {
throw new Error ( "Too many backtracks" ) ;
}
let symbol = this . toPlace . pop ( ) ;
let candidates = [ [ ] , [ ] , [ ] , [ ] , [ ] , [ ] , [ ] ] ;
for ( let n of this . template ) {
if ( ! this . board . grid [ n ] ) {
// is free
const cell = this . getCellInfo ( n ) ;
if ( cell . freeSequence >= 3 ) {
candidates [ cell . neighbours ] . push ( n ) ;
}
}
}
for ( let neighs = 6 ; neighs >= 0 ; neighs -- ) {
this . rng . arrayShuffle ( candidates [ neighs ] ) ;
}
for ( let i = 6 ; i >= 0 ; i -- ) {
if ( candidates [ i ] . length ) {
for ( let n of candidates [ i ] ) {
this . placeOrb ( n , symbol ) ;
// avoid deadlocking
if ( this . solution . length % 2 === 0 ) {
// this is the second orb in a pair (solution starts with gold)
let prevn = this . solution [ this . solution . length - 1 ] [ 1 ] ;
if ( ! this . isAvailable ( prevn ) ) {
this . trace ( "Avoid deadlock (this is the second of a pair)" ) ;
this . removeOrb ( n ) ;
continue ;
}
}
this . solution . push ( [ symbol , n ] ) ;
if ( this . toPlace . length ) {
if ( this . placeOne ( ) ) {
return true ;
} else {
this . trace ( "Undo and continue" ) ;
this . removeOrb ( n ) ;
this . solution . pop ( ) ;
// and continue the iteration
}
} else {
return true ;
}
}
}
}
// give it back
this . toPlace . push ( symbol ) ;
return false ;
}
}
/* Start */
window . game = new Game ( ) ;