@ -1,3 +1,5 @@
! ( function ( ) {
class Svg {
/ * *
* Build a node from XML
@ -38,6 +40,60 @@ class Svg {
}
}
/* --------- 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 ) ;
}
/ * *
* Game board
*
@ -59,12 +115,10 @@ class Board {
// Orb grid
this . grid = [ ] ;
this . tiles = [ ] ;
this . indexXyLookup = [ ] ;
for ( let i = 0 ; i <= 120 ; i ++ ) {
for ( let i = 0 ; i < BOARD _SIZE ; i ++ ) {
this . grid [ i ] = null ;
this . tiles [ i ] = null ;
this . indexXyLookup [ i ] = { x : i % 11 , y : Math . floor ( i / 11 ) } ;
}
this . onOrbClick = ( index , orb ) => {
@ -147,32 +201,9 @@ class Board {
* @ param { String | null } symbol - symbol to highlight , null to hide highlights
* /
highlight ( symbol = null ) {
this . $svg . setAttribute ( 'class' , '' ) ;
if ( symbol !== null ) {
this . $svg . classList . add ( ` highlight- ${ symbol } ` ) ;
}
}
/ * *
* Convert grid coordinates to gameGrid array index
*
* @ param { Number } x
* @ param { Number } y
* @ returns { Number }
* /
xyToGridIndex ( x , y ) {
return y * 11 + x
}
/ * *
* Convert grid index to X , Y
*
* @ param { Number } index
* @ returns { { x : Number , y : Number } }
* /
gridIndexToXy ( index ) {
return this . indexXyLookup [ index ] ;
SYMBOLS _ALL . forEach ( ( s ) => {
this . $svg . classList . toggle ( ` highlight- ${ symbol } ` , symbol === s ) ;
} ) ;
}
/ * *
@ -195,7 +226,7 @@ class Board {
* @ param { Number } y - board Y
* /
removeOrb ( x , y ) {
const index = this . xyToGridIndex ( x , y ) ;
const index = xyToGridIndex ( x , y ) ;
this . removeOrbByIndex ( index )
}
@ -230,7 +261,7 @@ class Board {
* @ return { { node : Node , symbol : String } }
* /
placeOrbByIndex ( index , symbol ) {
const { x , y } = this . gridIndexToXy ( index ) ;
const { x , y } = gridIndexToXy ( index ) ;
return this . placeOrb ( x , y , symbol ) ;
}
@ -244,12 +275,12 @@ class Board {
* /
placeOrb ( x , y , symbol ) {
const { rx , ry } = this . gridXyToCoord ( x , y ) ;
const arrayIndex = this . xyToGridIndex ( x , y ) ;
const arrayIndex = xyToGridIndex ( x , y ) ;
this . removeOrbByIndex ( arrayIndex ) ;
let template ;
if ( this . metals . includes ( symbol ) ) {
if ( SYMBOLS _METALS . includes ( symbol ) ) {
template = this . metallicOrbTpl ;
} else {
template = this . orbTpl ;
@ -479,7 +510,7 @@ class Board {
polygon _shadow . setAttribute ( 'transform' , ` translate( ${ rx } , ${ ry } ),scale(1.1) ` ) ;
this . buf0 . push ( polygon _shadow ) ;
const index = this . xyToGridIndex ( x , y ) ;
const index = xyToGridIndex ( x , y ) ;
let tile = this . tileTpl . cloneNode ( true ) ;
tile . setAttribute ( 'transform' , ` translate( ${ rx } , ${ ry } ) ` ) ;
this . buf1 . push ( tile ) ;
@ -512,16 +543,6 @@ class Board {
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 . symbols = [
'salt' , 'air' , 'fire' , 'water' , 'earth' , 'mercury' , 'lead' ,
'tin' , 'iron' , 'copper' , 'silver' , 'gold' , 'mors' , 'vitae'
] ;
this . metals = [
'mercury' , 'lead' , 'tin' , 'iron' , 'copper' , 'silver' , 'gold'
] ;
this . metalSequence = [ 'lead' , 'tin' , 'iron' , 'copper' , 'silver' , 'gold' ] ;
this . orbColors = {
salt : '#f2e7b4' ,
air : '#9ee0ff' ,
@ -632,11 +653,61 @@ class Rng {
}
}
class SettingsStorage {
constructor ( ) {
// this object is never overwritten, references are stable
this . settings = {
debug : false ,
version : 1 ,
allowTemplateAugmenting : true ,
retryTemplate : 30 ,
attemptTemplates : 50 ,
animations : true ,
disabledEffect : true ,
} ;
}
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 ( ) {
localStorage . setItem ( 'sigmar_settings' , JSON . stringify ( this . settings ) ) ;
}
}
class Game {
/ * *
* Init the game
* /
constructor ( seed = null ) {
this . settingsStore = new SettingsStorage ( ) ;
this . cfg = this . settingsStore . load ( ) ;
this . debug ( "Game settings:" , this . cfg ) ;
// TODO take seed from hash
this . board = new Board ( ) ;
if ( seed === null ) {
seed = + new Date ( ) ;
@ -674,9 +745,42 @@ class Game {
'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 ( ) ;
setTimeout ( ( ) => this . newGame ( ) , 100 ) ;
}
debug ( ... args ) {
if ( this . cfg . debug ) console . log ( ... args ) ;
}
info ( ... args ) {
console . info ( ... args ) ;
}
warn ( ... args ) {
console . warn ( ... args ) ;
}
error ( ... args ) {
console . error ( ... args ) ;
}
applySettings ( ) {
this . board . $svg . classList . toggle ( 'cfg-no-anim' , ! this . cfg . animations ) ;
this . board . $svg . classList . toggle ( 'cfg-no-fade-disabled' , ! this . cfg . disabledEffect ) ;
}
setCfg ( update ) {
this . settingsStore . update ( update ) ;
this . applySettings ( ) ;
this . settingsStore . save ( ) ;
}
getCfg ( key ) {
return this . cfg [ key ] ;
}
/ * *
* Show a selected template , for debug
*
@ -751,8 +855,8 @@ class Game {
return tpl
. sort ( ( a , b ) => a - b )
. map ( ( n ) => {
let { x , y } = this . board . gridIndexToXy ( n ) ;
return this . board . xyToGridIndex ( 5 + y - x , y ) ;
let { x , y } = gridIndexToXy ( n ) ;
return xyToGridIndex ( 5 + y - x , y ) ;
} )
. sort ( ( a , b ) => a - b ) ;
}
@ -788,29 +892,20 @@ class Game {
} ;
}
isOutside ( x , y ) {
return x < 0
|| x > 10
|| y < 0
|| y > 10
|| ( y <= 5 && x > 5 + y )
|| ( y > 5 && x < y - 5 ) ;
}
isAvailable ( n ) {
return this . getNeighbours ( n ) . freeSequence >= 3 ;
}
getNeighbours ( n ) {
let { x , y } = this . board . gridIndexToXy ( n ) ;
let { x , y } = gridIndexToXy ( n ) ;
let freeSpaces = [
this . isOutside ( x - 1 , y ) || ! this . board . grid [ n - 1 ] ,
this . isOutside ( x - 1 , y - 1 ) || ! this . board . grid [ n - 12 ] ,
this . isOutside ( x , y - 1 ) || ! this . board . grid [ n - 11 ] ,
this . isOutside ( x + 1 , y ) || ! this . board . grid [ n + 1 ] ,
this . isOutside ( x + 1 , y + 1 ) || ! this . board . grid [ n + 12 ] ,
this . isOutside ( x , y + 1 ) || ! this . board . grid [ n + 11 ] ,
isXyOutside ( x - 1 , y ) || ! this . board . grid [ n - 1 ] ,
isXy Outside ( x - 1 , y - 1 ) || ! this . board . grid [ n - 12 ] ,
isXy Outside ( x , y - 1 ) || ! this . board . grid [ n - 11 ] ,
isXy Outside ( x + 1 , y ) || ! this . board . grid [ n + 1 ] ,
isXy Outside ( x + 1 , y + 1 ) || ! this . board . grid [ n + 12 ] ,
isXy Outside ( x , y + 1 ) || ! this . board . grid [ n + 11 ] ,
] ;
let nOccupied = 0 ;
@ -820,8 +915,6 @@ class Game {
}
}
// if(this.debuggetneigh) console.log(`${x}×${y} #${n}, nocc ${nOccupied} `+JSON.stringify(freeSpaces));
let freeSequence = 0 ;
let maxFreeSequence = 0 ;
for ( let i = 0 ; i < 12 ; i ++ ) {
@ -847,12 +940,12 @@ class Game {
let allowedTable = [ ] ;
let outsideTemplate = [ ] ;
for ( let i = 0 ; i <= 120 ; i ++ ) {
le t allo = template . includes ( i ) ;
for ( let i = 0 ; i < BOARD _SIZE ; i ++ ) {
cons t allo = template . includes ( i ) ;
allowedTable . push ( allo ) ;
let { x , y } = this . board . gridIndexToXy ( i ) ;
if ( ! allo && ! this . isOutside ( x , y ) ) {
let { x , y } = gridIndexToXy ( i ) ;
if ( ! allo && ! isXy Outside ( x , y ) ) {
outsideTemplate . push ( i ) ;
}
@ -868,7 +961,7 @@ class Game {
}
const place = ( n , symbol ) => {
console . lo g( ` Place ${ n } <- ${ symbol } ` ) ;
this . debu g( ` 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
@ -876,7 +969,7 @@ class Game {
} ;
const unplace = ( n ) => {
console . lo g( ` Unplace ${ n } ` ) ;
this . debu g( ` Unplace ${ n } ` ) ;
this . board . grid [ n ] = null ;
} ;
@ -908,7 +1001,7 @@ class Game {
}
// this corrupts the template, but makes the likelihood of quickly finding a valid solution much higher.
if ( template . length !== 121 ) {
if ( template . length !== BOARD _SIZE && this . cfg . allowTemplateAugmenting ) {
// Prefer tile with more neighbours to make the game harder
let candidates = [ ] ;
outsideTemplate . forEach ( ( n ) => {
@ -930,7 +1023,7 @@ class Game {
template . push ( toAdd ) ;
console . warn ( ` Adding extra tile to template: ${ toAdd } ` ) ;
this . warn ( ` Adding extra tile to template: ${ toAdd } ` ) ;
return toAdd ;
}
}
@ -944,7 +1037,7 @@ class Game {
let solution = [ ] ;
while ( toPlace . length > 0 ) {
console . lo g( 'placing a pair.' ) ;
this . debu g( 'placing a pair.' ) ;
let symbol1 = toPlace . pop ( ) ;
let index1 = findAvailableIndex ( ) ;
@ -955,16 +1048,15 @@ class Game {
place ( index2 , symbol2 ) ;
if ( ! this . isAvailable ( index1 ) ) {
console . warn ( ` Deadlock, trying to work around it - ${ index1 } , ${ index2 } ` ) ;
this . debug ( ` Deadlock, trying to work around it - ${ index1 } , ${ index2 } ` ) ;
unplace ( index2 ) ;
let except = [ index2 ] ;
let suc = false ;
for ( let i = 0 ; i < 5 ; i ++ ) {
console . lo g( ` try # ${ i + 1 } ` ) ;
this . debu g( ` try # ${ i + 1 } ` ) ;
let index = findAvailableIndex ( except ) ;
// console.log(`try ${index} instead of ${index2}`);
place ( index , symbol2 ) ;
if ( this . isAvailable ( index1 ) ) {
@ -985,16 +1077,17 @@ class Game {
solution . push ( [ symbol2 , index2 ] ) ;
}
solution . reverse ( ) ;
// Show the solution for debug
console . info ( "Found a valid board!" ) ;
solution . reverse ( ) ;
this . info ( "Found a valid board!" ) ;
solution . forEach ( ( a ) => {
let p = this . board . gridIndexToXy ( a [ 1 ] ) ;
let p = gridIndexToXy ( a [ 1 ] ) ;
a [ 1 ] = ` ${ p . x } × ${ p . y } ` ;
} ) ;
console . lo g( 'Solution: ' , solution ) ;
this . debu g( 'Solution: ' , solution ) ;
}
/ * *
@ -1069,7 +1162,7 @@ class Game {
while ( true ) {
const n = this . rng . nextInt ( toPlace . length - 1 ) ;
if ( toPlace [ n ] [ 1 ] !== 'salt' ) {
// console.log(`Pairing ${toPlace[n][1]} with salt.`);
this . debug ( ` Pairing ${ toPlace [ n ] [ 1 ] } with salt. ` ) ;
newSaltedPairs . push ( [ toPlace [ n ] [ 1 ] , 'salt' ] ) ;
toPlace [ n ] [ 1 ] = 'salt' ;
break ;
@ -1111,7 +1204,7 @@ class Game {
mPos . push ( x )
}
mPos . sort ( ( a , b ) => a - b ) ;
// console.log('Metal positions ', mPos);
this . debug ( 'Metal positions ' , mPos ) ;
// inject them into the array
metals . forEach ( ( pair , i ) => {
toPlace . splice ( mPos [ i ] + i , 0 , pair ) ;
@ -1128,7 +1221,7 @@ class Game {
* 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 <= 120 ; n ++ ) {
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 ;
@ -1141,7 +1234,7 @@ class Game {
* Update orb availability status ( includes effects )
* /
updateOrbDisabledStatus ( ) {
for ( let n = 0 ; n <= 120 ; n ++ ) {
for ( let n = 0 ; n < BOARD _SIZE ; n ++ ) {
if ( this . board . grid [ n ] ) {
const ava = this . isAvailableAtPlaytime ( n ) ;
this . board . grid [ n ] . node . classList . toggle ( 'disabled' , ! ava ) ;
@ -1153,13 +1246,13 @@ class Game {
* Check if a tile is available at play - time ( checking unlocked metals )
*
* @ param n
* @ return { b oolean}
* @ return { B oolean}
* /
isAvailableAtPlaytime ( n ) {
let ava = this . isAvailable ( n ) ;
const sym = this . board . grid [ n ] . symbol ;
if ( this . board . metalSequence . includes ( sym ) ) {
if ( METAL _SEQ . includes ( sym ) ) {
if ( sym !== this . nextMetal ) {
ava = false ;
}
@ -1170,6 +1263,7 @@ class Game {
/ * *
* Handle orb click
*
* @ param n
* @ param orb
* /
@ -1222,7 +1316,7 @@ class Game {
if ( wantRefresh ) {
if ( this . countOrbs ( ) === 0 ) {
console . info ( "Good work!" ) ;
this . info ( "Good work!" ) ;
}
this . updateOrbDisabledStatus ( ) ;
@ -1230,26 +1324,23 @@ class Game {
}
newGame ( ) {
// this.board.onTileClick = (n) => {
// console.log(n, this.board.gridIndexToXy(n));
// };
this . board . onTileClick = ( n ) => {
this . debug ( n , gridIndexToXy ( n ) ) ;
} ;
this . selectedOrb = null ;
let self = this ;
this . board . onOrbClick = ( n , orb ) => self . ingameBoardClick ( n , orb ) ;
const RETRY _IN _TEMPLATE = 50 ;
const RETRY _NEW _TEMPLATE = 50 ;
// retry loop, should not be needed if everything is correct
let suc = false ;
let numretries = 0 ;
const alertOnError = false ;
for ( let i = 0 ; i < RETRY _NEW _TEMPLATE && ! suc ; i ++ ) {
console . lo g( 'RNG seed is: ' + this . rng . state ) ;
for ( let i = 0 ; i < this . cfg . attemptTemplates && ! suc ; i ++ ) {
this . debu g( 'RNG seed is: ' + this . rng . state ) ;
const template = this . getRandomTemplate ( ) ;
for ( let j = 0 ; j < RETRY _IN _TEMPLATE ; j ++ ) {
for ( let j = 0 ; j < this . cfg . retryTemplate ; j ++ ) {
try {
this . placeOrbs ( template . slice ( 0 ) ) ; // clone
suc = true ;
@ -1257,11 +1348,15 @@ class Game {
} catch ( e ) {
if ( alertOnError ) alert ( 'welp' ) ;
numretries ++ ;
console . error ( e ) ;
if ( this . cfg . debug ) {
this . error ( e ) ;
} else {
this . warn ( e . message ) ;
}
}
}
if ( ! suc ) {
console . warn ( "Exhausted all retries for the template, getting a new one" ) ;
this . warn ( "Exhausted all retries for the template, getting a new one" ) ;
}
}
@ -1272,7 +1367,7 @@ class Game {
if ( ! suc ) {
alert ( ` Sorry, could not find a valid board setup after ${ numretries } retries. ` ) ;
} else {
console . info ( ` Found valid solution (with ${ numretries } retries) ` ) ;
this . info ( ` Found valid solution (with ${ numretries } retries) ` ) ;
}
}
@ -1296,7 +1391,8 @@ class Game {
}
advanceMetal ( ) {
this . nextMetal = this . board . metalSequence [ this . board . metalSequence . indexOf ( this . nextMetal ) + 1 ] ;
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 } ` ) ;
}
}
@ -1304,3 +1400,5 @@ class Game {
/* Start */
window . game = new Game ( ) ;
} ) ( ) ;