|
|
|
@ -1,6 +1,4 @@ |
|
|
|
|
!(function () { |
|
|
|
|
|
|
|
|
|
class Svg { |
|
|
|
|
class Svg { |
|
|
|
|
/** |
|
|
|
|
* Build a node from XML |
|
|
|
|
* |
|
|
|
@ -38,70 +36,70 @@ |
|
|
|
|
static fromXMLg(code, wrapperOpts = null) { |
|
|
|
|
return Svg.fromXML(code, 'g', wrapperOpts); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* --------- Shared Constants --------- */ |
|
|
|
|
const BOARD_SIZE = 121; |
|
|
|
|
/* --------- Shared Constants --------- */ |
|
|
|
|
const BOARD_SIZE = 121; |
|
|
|
|
|
|
|
|
|
const METAL_SEQ = ['lead', 'tin', 'iron', 'copper', 'silver', 'gold']; |
|
|
|
|
const METAL_SEQ = ['lead', 'tin', 'iron', 'copper', 'silver', 'gold']; |
|
|
|
|
|
|
|
|
|
const SYMBOLS_METALS = [ |
|
|
|
|
const SYMBOLS_METALS = [ |
|
|
|
|
'mercury', 'lead', 'tin', 'iron', 'copper', 'silver', 'gold' |
|
|
|
|
]; |
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
const SYMBOLS_ALL = [ |
|
|
|
|
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) { |
|
|
|
|
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) { |
|
|
|
|
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) { |
|
|
|
|
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 |
|
|
|
|
* |
|
|
|
|
* 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 { |
|
|
|
|
class Board { |
|
|
|
|
constructor() { |
|
|
|
|
this.$bg = document.getElementById('boardbg'); |
|
|
|
|
this.$orbs = document.getElementById('orbs'); |
|
|
|
@ -122,8 +120,10 @@ |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
this.onOrbClick = (index, orb) => { |
|
|
|
|
// placeholder
|
|
|
|
|
}; |
|
|
|
|
this.onTileClick = (index) => { |
|
|
|
|
// placeholder
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
this.initOrb(); |
|
|
|
@ -190,7 +190,7 @@ |
|
|
|
|
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.$root.setAttribute('transform', `translate(${w / 2},${h / 2}) scale(${scale})`); |
|
|
|
|
|
|
|
|
|
this.rescaleTimeout = null; |
|
|
|
|
} |
|
|
|
@ -363,15 +363,13 @@ |
|
|
|
|
r="50" cy="5" cx="0" |
|
|
|
|
fill="black" |
|
|
|
|
class="orb-shadow" |
|
|
|
|
filter="url('#filterDropshadow')" |
|
|
|
|
opacity="1" |
|
|
|
|
fill-opacity="0.7" /> |
|
|
|
|
<circle |
|
|
|
|
r="55" cy="2" cx="0" |
|
|
|
|
r="55" cy="0" cx="0" |
|
|
|
|
fill="white" |
|
|
|
|
class="orb-glow" |
|
|
|
|
opacity="0" |
|
|
|
|
filter="url('#filterDropshadow')" /> |
|
|
|
|
opacity="0" /> |
|
|
|
|
<circle |
|
|
|
|
r="49" cy="0" cx="0" |
|
|
|
|
fill="#9F9F9F" |
|
|
|
@ -550,7 +548,6 @@ |
|
|
|
|
water: '#0fdac3', |
|
|
|
|
earth: '#99ff11', |
|
|
|
|
|
|
|
|
|
// TODO metals need color adjust + ideally should have different visuals
|
|
|
|
|
mercury: '#f2e7b4', |
|
|
|
|
lead: '#728686', |
|
|
|
|
tin: '#c5be9b', |
|
|
|
@ -588,7 +585,7 @@ |
|
|
|
|
*/ |
|
|
|
|
removeAllOrbs() { |
|
|
|
|
Object.keys(this.grid).forEach((n) => { |
|
|
|
|
this.removeOrbByIndex(n); |
|
|
|
|
this.removeOrbByIndex(+n); |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -601,24 +598,27 @@ |
|
|
|
|
getOrbByIndex(n) { |
|
|
|
|
return this.grid[n]; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
/** |
|
|
|
|
* Random number generator |
|
|
|
|
* |
|
|
|
|
* Uses Mullbery32 from https://stackoverflow.com/a/47593316/2180189
|
|
|
|
|
*/ |
|
|
|
|
class Rng { |
|
|
|
|
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.seed(seed); |
|
|
|
|
|
|
|
|
|
this.setSeed(+seed); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
@ -626,7 +626,9 @@ |
|
|
|
|
* |
|
|
|
|
* @param {Number} seed |
|
|
|
|
*/ |
|
|
|
|
seed(seed) { |
|
|
|
|
setSeed(seed) { |
|
|
|
|
seed = +seed; |
|
|
|
|
this.seed = seed; |
|
|
|
|
this.state = seed; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -651,20 +653,22 @@ |
|
|
|
|
nextInt(max) { |
|
|
|
|
return Math.floor((max + 1) * this.next()); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
class SettingsStorage { |
|
|
|
|
class SettingsStorage { |
|
|
|
|
constructor() { |
|
|
|
|
// this object is never overwritten, references are stable
|
|
|
|
|
this.settings = { |
|
|
|
|
debug: false, |
|
|
|
|
this.defaults = { |
|
|
|
|
log: 'info', |
|
|
|
|
version: 1, |
|
|
|
|
allowTemplateAugmenting: true, |
|
|
|
|
allowTemplateAugmenting: false, |
|
|
|
|
retryTemplate: 30, |
|
|
|
|
attemptTemplates: 50, |
|
|
|
|
animations: true, |
|
|
|
|
svgAnimations: false, |
|
|
|
|
svgBlur: false, |
|
|
|
|
disabledEffect: true, |
|
|
|
|
}; |
|
|
|
|
this.settings = Object.assign({}, this.defaults); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
load() { |
|
|
|
@ -693,27 +697,124 @@ |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
save() { |
|
|
|
|
localStorage.setItem('sigmar_settings', JSON.stringify(this.settings)); |
|
|
|
|
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 Game { |
|
|
|
|
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(seed = null) { |
|
|
|
|
constructor() { |
|
|
|
|
this.LOGLEVELS = ['error', 'warn', 'info', 'debug', 'trace']; |
|
|
|
|
|
|
|
|
|
this.settingsStore = new SettingsStorage(); |
|
|
|
|
this.cfg = this.settingsStore.load(); |
|
|
|
|
this.debug("Game settings:", this.cfg); |
|
|
|
|
this.applyLogFilter(); |
|
|
|
|
|
|
|
|
|
// TODO take seed from hash
|
|
|
|
|
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(); |
|
|
|
|
if (seed === null) { |
|
|
|
|
seed = +new Date(); |
|
|
|
|
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) { |
|
|
|
|
let pretty = !!+args.pretty; |
|
|
|
|
this.setCfg({ |
|
|
|
|
svgAnimations: pretty, |
|
|
|
|
svgBlur: pretty, |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
this.rng = new Rng(seed); |
|
|
|
|
this.info("Game settings:", this.cfg); |
|
|
|
|
|
|
|
|
|
this.layoutTemplates = { |
|
|
|
|
// templates apparently all have 55 items
|
|
|
|
@ -747,28 +848,44 @@ |
|
|
|
|
|
|
|
|
|
this.applySettings(); |
|
|
|
|
|
|
|
|
|
setTimeout(() => this.newGame(), 100); |
|
|
|
|
// Defer start to give browser time to render the background
|
|
|
|
|
setTimeout(() => { |
|
|
|
|
this.newGame(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.cfg.debug) console.log(...args); |
|
|
|
|
if (this.logging_debug) console.log(...args); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
info(...args) { |
|
|
|
|
console.info(...args); |
|
|
|
|
if (this.logging_info) console.info(...args); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
warn(...args) { |
|
|
|
|
console.warn(...args); |
|
|
|
|
if (this.logging_warn) console.warn(...args); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
error(...args) { |
|
|
|
|
console.error(...args); |
|
|
|
|
if (this.logging_error) console.error(...args); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
applySettings() { |
|
|
|
|
this.board.$svg.classList.toggle('cfg-no-anim', !this.cfg.animations); |
|
|
|
|
this.board.$svg.classList.toggle('cfg-no-anim', !this.cfg.svgAnimations); |
|
|
|
|
this.board.$svg.classList.toggle('cfg-no-fade-disabled', !this.cfg.disabledEffect); |
|
|
|
|
this.board.$svg.classList.toggle('cfg-no-blur', !this.cfg.svgBlur); |
|
|
|
|
this.applyLogFilter(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
setCfg(update) { |
|
|
|
@ -790,9 +907,9 @@ |
|
|
|
|
showTemplate(name, flip = false) { |
|
|
|
|
this.board.removeAllOrbs(); |
|
|
|
|
|
|
|
|
|
this.getTemplate(name, flip).forEach((n) => { |
|
|
|
|
for (let n of this.getTemplate(name, flip).positions) { |
|
|
|
|
this.board.placeOrbByIndex(n, 'lead'); |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
@ -800,9 +917,9 @@ |
|
|
|
|
*/ |
|
|
|
|
showRandomTemplate() { |
|
|
|
|
this.board.removeAllOrbs(); |
|
|
|
|
this.getRandomTemplate().forEach((n) => { |
|
|
|
|
for (let n of this.getRandomTemplate().positions) { |
|
|
|
|
this.board.placeOrbByIndex(n, 'lead'); |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
@ -810,22 +927,27 @@ |
|
|
|
|
* |
|
|
|
|
* @param {String} name |
|
|
|
|
* @param {boolean} flipped |
|
|
|
|
* @returns {number[]} |
|
|
|
|
* @returns {{name: String, flipped: Boolean, positions: Number[]}} |
|
|
|
|
*/ |
|
|
|
|
getTemplate(name, flipped) { |
|
|
|
|
let tpl = this.layoutTemplates[name].slice(0); // this slice takes a copy so the array is not corrupted by later manipulations
|
|
|
|
|
let positions = this.layoutTemplates[name].slice(0); // this slice takes a copy so the array is not corrupted by later manipulations
|
|
|
|
|
|
|
|
|
|
if (flipped) { |
|
|
|
|
tpl = this.flipTemplate(tpl); |
|
|
|
|
positions = this.flipTemplate(positions); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return tpl; |
|
|
|
|
return { |
|
|
|
|
basename: name, |
|
|
|
|
name: name+(flipped?'_flip':''), |
|
|
|
|
flipped, |
|
|
|
|
positions, |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Get a random and randomly flipped template |
|
|
|
|
* |
|
|
|
|
* @return Number[] - template indices; this is a clone, free to modify |
|
|
|
|
* @returns {{name: String, flipped: Boolean, positions: Number[]}} |
|
|
|
|
*/ |
|
|
|
|
getRandomTemplate() { |
|
|
|
|
let names = Object.keys(this.layoutTemplates); |
|
|
|
@ -834,12 +956,10 @@ |
|
|
|
|
let tpl = this.getTemplate(name, flipped); |
|
|
|
|
|
|
|
|
|
// 60 (center) must be included to place gold
|
|
|
|
|
if (!tpl.includes(60)) { |
|
|
|
|
if (!tpl.positions.includes(60)) { |
|
|
|
|
throw Error(`Template "${name}", flip=${+flipped}, lacks 60.`); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
console.info(`Selected board layout template "${name}", flipped=${+flipped}`); |
|
|
|
|
|
|
|
|
|
return tpl; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -936,6 +1056,8 @@ |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
placeOrbs(template) { |
|
|
|
|
let tilesAdded = 0; |
|
|
|
|
|
|
|
|
|
this.board.removeAllOrbs(); |
|
|
|
|
|
|
|
|
|
let allowedTable = []; |
|
|
|
@ -961,7 +1083,7 @@ |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const place = (n, symbol) => { |
|
|
|
|
this.debug(`Place ${n} <- ${symbol}`); |
|
|
|
|
this.trace(`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
|
|
|
|
@ -969,14 +1091,13 @@ |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const unplace = (n) => { |
|
|
|
|
this.debug(`Unplace ${n}`); |
|
|
|
|
this.trace(`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]; |
|
|
|
|
for (let n of template) { |
|
|
|
|
if (except && except.includes(n)) continue; |
|
|
|
|
|
|
|
|
|
if (!this.board.grid[n]) { |
|
|
|
@ -1022,6 +1143,7 @@ |
|
|
|
|
outsideTemplate.splice(outsideTemplate.indexOf(toAdd), 1); |
|
|
|
|
|
|
|
|
|
template.push(toAdd); |
|
|
|
|
tilesAdded++; |
|
|
|
|
|
|
|
|
|
this.warn(`Adding extra tile to template: ${toAdd}`); |
|
|
|
|
return toAdd; |
|
|
|
@ -1035,13 +1157,16 @@ |
|
|
|
|
|
|
|
|
|
const toPlace = this.buildPlacementList(); |
|
|
|
|
|
|
|
|
|
let solution = []; |
|
|
|
|
let solution = [ |
|
|
|
|
['gold', 60], |
|
|
|
|
]; |
|
|
|
|
while (toPlace.length > 0) { |
|
|
|
|
this.debug('placing a pair.'); |
|
|
|
|
this.trace('placing a pair.'); |
|
|
|
|
|
|
|
|
|
let symbol1 = toPlace.pop(); |
|
|
|
|
let index1 = findAvailableIndex(); |
|
|
|
|
place(index1, symbol1); |
|
|
|
|
solution.push([symbol1, index1]); |
|
|
|
|
|
|
|
|
|
let symbol2 = toPlace.pop(); |
|
|
|
|
let index2 = findAvailableIndex(); |
|
|
|
@ -1055,12 +1180,13 @@ |
|
|
|
|
|
|
|
|
|
let suc = false; |
|
|
|
|
for (let i = 0; i < 5; i++) { |
|
|
|
|
this.debug(`try #${i + 1}`); |
|
|
|
|
this.trace(`try #${i + 1}`); |
|
|
|
|
let index = findAvailableIndex(except); |
|
|
|
|
place(index, symbol2); |
|
|
|
|
|
|
|
|
|
if (this.isAvailable(index1)) { |
|
|
|
|
suc = true; |
|
|
|
|
index2 = index; |
|
|
|
|
break; |
|
|
|
|
} else { |
|
|
|
|
unplace(index); |
|
|
|
@ -1073,7 +1199,7 @@ |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
solution.push([symbol1, index1]); |
|
|
|
|
// index2 is updated in the fixing loop
|
|
|
|
|
solution.push([symbol2, index2]); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -1084,10 +1210,15 @@ |
|
|
|
|
|
|
|
|
|
solution.forEach((a) => { |
|
|
|
|
let p = gridIndexToXy(a[1]); |
|
|
|
|
a[1] = `${p.x} × ${p.y}`; |
|
|
|
|
a[1] = `${p.x}×${p.y}`; |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
this.debug('Solution: ', solution); |
|
|
|
|
this.debug('Solution:', solution); |
|
|
|
|
|
|
|
|
|
return { |
|
|
|
|
solution, |
|
|
|
|
tilesAdded: tilesAdded, |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
@ -1113,14 +1244,7 @@ |
|
|
|
|
* @return {number} |
|
|
|
|
*/ |
|
|
|
|
countOrbs() { |
|
|
|
|
let n = 0; |
|
|
|
|
// todo use reduce
|
|
|
|
|
this.board.grid.forEach((x) => { |
|
|
|
|
if (x !== null) { |
|
|
|
|
n++; |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
return n; |
|
|
|
|
return this.board.grid.reduce((acu, x) => acu + (x !== null), 0); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
@ -1162,7 +1286,7 @@ |
|
|
|
|
while (true) { |
|
|
|
|
const n = this.rng.nextInt(toPlace.length - 1); |
|
|
|
|
if (toPlace[n][1] !== 'salt') { |
|
|
|
|
this.debug(`Pairing ${toPlace[n][1]} with salt.`); |
|
|
|
|
this.trace(`Pairing ${toPlace[n][1]} with salt.`); |
|
|
|
|
newSaltedPairs.push([toPlace[n][1], 'salt']); |
|
|
|
|
toPlace[n][1] = 'salt'; |
|
|
|
|
break; |
|
|
|
@ -1204,12 +1328,14 @@ |
|
|
|
|
mPos.push(x) |
|
|
|
|
} |
|
|
|
|
mPos.sort((a, b) => a - b); |
|
|
|
|
this.debug('Metal positions ', mPos); |
|
|
|
|
this.trace('Metal positions ', mPos); |
|
|
|
|
// inject them into the array
|
|
|
|
|
metals.forEach((pair, i) => { |
|
|
|
|
toPlace.splice(mPos[i] + i, 0, pair); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
this.debug('Placement order (last first):', toPlace); |
|
|
|
|
|
|
|
|
|
return toPlace.reduce((a, c) => { |
|
|
|
|
a.push(c[0]); |
|
|
|
|
a.push(c[1]); |
|
|
|
@ -1267,35 +1393,48 @@ |
|
|
|
|
* @param n |
|
|
|
|
* @param orb |
|
|
|
|
*/ |
|
|
|
|
ingameBoardClick(n, orb) { |
|
|
|
|
if (!this.isAvailableAtPlaytime(n)) return; |
|
|
|
|
inGameBoardClick(n, orb) { |
|
|
|
|
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.board.removeOrbByIndex(n); |
|
|
|
|
this.selectedOrb = null; |
|
|
|
|
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}`); |
|
|
|
|
|
|
|
|
|
// compatible pair clicked
|
|
|
|
|
this.board.removeOrbByIndex(n); |
|
|
|
|
this.board.removeOrbByIndex(this.selectedOrb.n); |
|
|
|
|
|
|
|
|
|
if ([orb.symbol, otherSymbol].includes(this.nextMetal)) { |
|
|
|
|
this.debug("Advance metal transmutation sequence."); |
|
|
|
|
this.advanceMetal(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -1303,6 +1442,8 @@ |
|
|
|
|
|
|
|
|
|
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}; |
|
|
|
@ -1320,7 +1461,20 @@ |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
newGame() { |
|
|
|
|
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)); |
|
|
|
|
}; |
|
|
|
@ -1328,32 +1482,42 @@ |
|
|
|
|
this.selectedOrb = null; |
|
|
|
|
|
|
|
|
|
let self = this; |
|
|
|
|
this.board.onOrbClick = (n, orb) => self.ingameBoardClick(n, orb); |
|
|
|
|
this.board.onOrbClick = (n, orb) => self.inGameBoardClick(n, orb); |
|
|
|
|
|
|
|
|
|
// retry loop, should not be needed if everything is correct
|
|
|
|
|
let suc = false; |
|
|
|
|
let numretries = 0; |
|
|
|
|
const alertOnError = false; |
|
|
|
|
for (let i = 0; i < this.cfg.attemptTemplates && !suc; i++) { |
|
|
|
|
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); |
|
|
|
|
const template = this.getRandomTemplate(); |
|
|
|
|
for (let j = 0; j < this.cfg.retryTemplate; j++) { |
|
|
|
|
|
|
|
|
|
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 { |
|
|
|
|
this.placeOrbs(template.slice(0)); // clone
|
|
|
|
|
board_info = this.placeOrbs(template.positions.slice(0)); // clone
|
|
|
|
|
board_info.template = template; |
|
|
|
|
suc = true; |
|
|
|
|
break; |
|
|
|
|
} catch (e) { |
|
|
|
|
if (alertOnError) alert('welp'); |
|
|
|
|
numretries++; |
|
|
|
|
if (this.cfg.debug) { |
|
|
|
|
retry_count++; |
|
|
|
|
if (this.logging_trace) { |
|
|
|
|
this.error(e); |
|
|
|
|
} else { |
|
|
|
|
this.warn(e.message); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (!suc) { |
|
|
|
|
this.warn("Exhausted all retries for the template, getting a new one"); |
|
|
|
|
this.warn(`Exhausted all retries for the template "${template.name}", getting a new one`); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -1362,9 +1526,26 @@ |
|
|
|
|
this.updateOrbDisabledStatus(); |
|
|
|
|
|
|
|
|
|
if (!suc) { |
|
|
|
|
alert(`Sorry, could not find a valid board setup after ${numretries} retries.`); |
|
|
|
|
alert(`Sorry, could not find a valid board setup after ${retry_count} retries.`); |
|
|
|
|
} else { |
|
|
|
|
this.info( |
|
|
|
|
`Board set up with ${retry_count} retries:\n` + |
|
|
|
|
`teplate "${board_info.template.name}"` + |
|
|
|
|
(this.cfg.allowTemplateAugmenting ? ` with ${board_info.tilesAdded} extra tiles` : '')); |
|
|
|
|
|
|
|
|
|
this.info('Reference solution:\n ' + board_info.solution.reduce((s, entry, i) => { |
|
|
|
|
s += `${entry[0]} ${entry[1]}`; |
|
|
|
|
|
|
|
|
|
if (i % 2 === 1) { |
|
|
|
|
s += "\n "; |
|
|
|
|
} else { |
|
|
|
|
this.info(`Found valid solution (with ${numretries} retries)`); |
|
|
|
|
if (entry[0] !== 'gold') { |
|
|
|
|
s += " + "; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return s; |
|
|
|
|
}, '')); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -1392,10 +1573,9 @@ |
|
|
|
|
this.nextMetal = METAL_SEQ[METAL_SEQ.indexOf(this.nextMetal) + 1]; |
|
|
|
|
console.debug(`Next metal unlocked: ${this.nextMetal}`); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* Start */ |
|
|
|
|
/* Start */ |
|
|
|
|
|
|
|
|
|
window.game = new Game(); |
|
|
|
|
window.game = new Game(); |
|
|
|
|
|
|
|
|
|
})(); |
|
|
|
|