add setting storage and some things are now configurable

master
Ondřej Hruška 5 years ago
parent 59520ec2be
commit 6f10b87577
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 298
      script.js
  2. 8
      style.css

@ -1,3 +1,5 @@
!(function() {
class Svg { class Svg {
/** /**
* Build a node from XML * 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 * Game board
* *
@ -59,12 +115,10 @@ class Board {
// Orb grid // Orb grid
this.grid = []; this.grid = [];
this.tiles = []; this.tiles = [];
this.indexXyLookup = [];
for (let i = 0; i <= 120; i++) { for (let i = 0; i < BOARD_SIZE; i++) {
this.grid[i] = null; this.grid[i] = null;
this.tiles[i] = null; this.tiles[i] = null;
this.indexXyLookup[i] = {x: i % 11, y: Math.floor(i / 11)};
} }
this.onOrbClick = (index, orb) => { this.onOrbClick = (index, orb) => {
@ -147,32 +201,9 @@ class Board {
* @param {String|null} symbol - symbol to highlight, null to hide highlights * @param {String|null} symbol - symbol to highlight, null to hide highlights
*/ */
highlight(symbol = null) { highlight(symbol = null) {
this.$svg.setAttribute('class', ''); SYMBOLS_ALL.forEach((s) => {
this.$svg.classList.toggle(`highlight-${symbol}`, symbol === s);
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];
} }
/** /**
@ -195,7 +226,7 @@ class Board {
* @param {Number} y - board Y * @param {Number} y - board Y
*/ */
removeOrb(x, y) { removeOrb(x, y) {
const index = this.xyToGridIndex(x, y); const index = xyToGridIndex(x, y);
this.removeOrbByIndex(index) this.removeOrbByIndex(index)
} }
@ -230,7 +261,7 @@ class Board {
* @return {{node: Node, symbol: String}} * @return {{node: Node, symbol: String}}
*/ */
placeOrbByIndex(index, symbol) { placeOrbByIndex(index, symbol) {
const {x, y} = this.gridIndexToXy(index); const {x, y} = gridIndexToXy(index);
return this.placeOrb(x, y, symbol); return this.placeOrb(x, y, symbol);
} }
@ -244,12 +275,12 @@ class Board {
*/ */
placeOrb(x, y, symbol) { placeOrb(x, y, symbol) {
const {rx, ry} = this.gridXyToCoord(x, y); const {rx, ry} = this.gridXyToCoord(x, y);
const arrayIndex = this.xyToGridIndex(x, y); const arrayIndex = xyToGridIndex(x, y);
this.removeOrbByIndex(arrayIndex); this.removeOrbByIndex(arrayIndex);
let template; let template;
if (this.metals.includes(symbol)) { if (SYMBOLS_METALS.includes(symbol)) {
template = this.metallicOrbTpl; template = this.metallicOrbTpl;
} else { } else {
template = this.orbTpl; template = this.orbTpl;
@ -479,7 +510,7 @@ class Board {
polygon_shadow.setAttribute('transform', `translate(${rx},${ry}),scale(1.1)`); polygon_shadow.setAttribute('transform', `translate(${rx},${ry}),scale(1.1)`);
this.buf0.push(polygon_shadow); this.buf0.push(polygon_shadow);
const index = this.xyToGridIndex(x, y); const index = xyToGridIndex(x, y);
let tile = this.tileTpl.cloneNode(true); let tile = this.tileTpl.cloneNode(true);
tile.setAttribute('transform', `translate(${rx},${ry})`); tile.setAttribute('transform', `translate(${rx},${ry})`);
this.buf1.push(tile); 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', 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 = { this.orbColors = {
salt: '#f2e7b4', salt: '#f2e7b4',
air: '#9ee0ff', 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 { class Game {
/** /**
* Init the game * Init the game
*/ */
constructor(seed = null) { 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(); this.board = new Board();
if (seed === null) { if (seed === null) {
seed = +new Date(); 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], '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); 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 * Show a selected template, for debug
* *
@ -751,8 +855,8 @@ class Game {
return tpl return tpl
.sort((a, b) => a - b) .sort((a, b) => a - b)
.map((n) => { .map((n) => {
let {x, y} = this.board.gridIndexToXy(n); let {x, y} = gridIndexToXy(n);
return this.board.xyToGridIndex(5 + y - x, y); return xyToGridIndex(5 + y - x, y);
}) })
.sort((a, b) => a - b); .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) { isAvailable(n) {
return this.getNeighbours(n).freeSequence >= 3; return this.getNeighbours(n).freeSequence >= 3;
} }
getNeighbours(n) { getNeighbours(n) {
let {x, y} = this.board.gridIndexToXy(n); let {x, y} = gridIndexToXy(n);
let freeSpaces = [ let freeSpaces = [
this.isOutside(x - 1, y) || !this.board.grid[n - 1], isXyOutside(x - 1, y) || !this.board.grid[n - 1],
this.isOutside(x - 1, y - 1) || !this.board.grid[n - 12], isXyOutside(x - 1, y - 1) || !this.board.grid[n - 12],
this.isOutside(x, y - 1) || !this.board.grid[n - 11], isXyOutside(x, y - 1) || !this.board.grid[n - 11],
this.isOutside(x + 1, y) || !this.board.grid[n + 1], isXyOutside(x + 1, y) || !this.board.grid[n + 1],
this.isOutside(x + 1, y + 1) || !this.board.grid[n + 12], isXyOutside(x + 1, y + 1) || !this.board.grid[n + 12],
this.isOutside(x, y + 1) || !this.board.grid[n + 11], isXyOutside(x, y + 1) || !this.board.grid[n + 11],
]; ];
let nOccupied = 0; 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 freeSequence = 0;
let maxFreeSequence = 0; let maxFreeSequence = 0;
for (let i = 0; i < 12; i++) { for (let i = 0; i < 12; i++) {
@ -847,12 +940,12 @@ class Game {
let allowedTable = []; let allowedTable = [];
let outsideTemplate = []; let outsideTemplate = [];
for (let i = 0; i <= 120; i++) { for (let i = 0; i < BOARD_SIZE; i++) {
let allo = template.includes(i); const allo = template.includes(i);
allowedTable.push(allo); allowedTable.push(allo);
let { x, y } = this.board.gridIndexToXy(i); let { x, y } = gridIndexToXy(i);
if (!allo && !this.isOutside(x, y)) { if (!allo && !isXyOutside(x, y)) {
outsideTemplate.push(i); outsideTemplate.push(i);
} }
@ -868,7 +961,7 @@ class Game {
} }
const place = (n, symbol) => { const place = (n, symbol) => {
console.log(`Place ${n} <- ${symbol}`); this.debug(`Place ${n} <- ${symbol}`);
if (!allowedTable[n]) throw Error(`Position ${n} not allowed by template`); 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]}`); 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 // 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) => { const unplace = (n) => {
console.log(`Unplace ${n}`); this.debug(`Unplace ${n}`);
this.board.grid[n] = null; 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. // 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 // Prefer tile with more neighbours to make the game harder
let candidates = []; let candidates = [];
outsideTemplate.forEach((n) => { outsideTemplate.forEach((n) => {
@ -930,7 +1023,7 @@ class Game {
template.push(toAdd); template.push(toAdd);
console.warn(`Adding extra tile to template: ${toAdd}`); this.warn(`Adding extra tile to template: ${toAdd}`);
return toAdd; return toAdd;
} }
} }
@ -944,7 +1037,7 @@ class Game {
let solution = []; let solution = [];
while (toPlace.length > 0) { while (toPlace.length > 0) {
console.log('placing a pair.'); this.debug('placing a pair.');
let symbol1 = toPlace.pop(); let symbol1 = toPlace.pop();
let index1 = findAvailableIndex(); let index1 = findAvailableIndex();
@ -955,16 +1048,15 @@ class Game {
place(index2, symbol2); place(index2, symbol2);
if (!this.isAvailable(index1)) { 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); unplace(index2);
let except = [index2]; let except = [index2];
let suc = false; let suc = false;
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
console.log(`try #${i + 1}`); this.debug(`try #${i + 1}`);
let index = findAvailableIndex(except); let index = findAvailableIndex(except);
// console.log(`try ${index} instead of ${index2}`);
place(index, symbol2); place(index, symbol2);
if (this.isAvailable(index1)) { if (this.isAvailable(index1)) {
@ -985,16 +1077,17 @@ class Game {
solution.push([symbol2, index2]); 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) => { solution.forEach((a) => {
let p = this.board.gridIndexToXy(a[1]); let p = gridIndexToXy(a[1]);
a[1] = `${p.x} × ${p.y}`; a[1] = `${p.x} × ${p.y}`;
}); });
console.log('Solution: ', solution); this.debug('Solution: ', solution);
} }
/** /**
@ -1069,7 +1162,7 @@ class Game {
while (true) { while (true) {
const n = this.rng.nextInt(toPlace.length - 1); const n = this.rng.nextInt(toPlace.length - 1);
if (toPlace[n][1] !== 'salt') { 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']); newSaltedPairs.push([toPlace[n][1], 'salt']);
toPlace[n][1] = 'salt'; toPlace[n][1] = 'salt';
break; break;
@ -1111,7 +1204,7 @@ class Game {
mPos.push(x) mPos.push(x)
} }
mPos.sort((a, b) => a - b); mPos.sort((a, b) => a - b);
// console.log('Metal positions ', mPos); this.debug('Metal positions ', mPos);
// inject them into the array // inject them into the array
metals.forEach((pair, i) => { metals.forEach((pair, i) => {
toPlace.splice(mPos[i] + i, 0, pair); 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) * Convert the strings in the board array to actual SVG orbs (strings are a placeholder to speed up board solving)
*/ */
renderPreparedBoard() { renderPreparedBoard() {
for (let n = 0; n <= 120; n++) { for (let n = 0; n < BOARD_SIZE; n++) {
if (this.board.grid[n] !== null) { if (this.board.grid[n] !== null) {
const symbol = this.board.grid[n]; const symbol = this.board.grid[n];
this.board.grid[n] = null; this.board.grid[n] = null;
@ -1141,7 +1234,7 @@ class Game {
* Update orb availability status (includes effects) * Update orb availability status (includes effects)
*/ */
updateOrbDisabledStatus() { updateOrbDisabledStatus() {
for (let n = 0; n <= 120; n++) { for (let n = 0; n < BOARD_SIZE; n++) {
if (this.board.grid[n]) { if (this.board.grid[n]) {
const ava = this.isAvailableAtPlaytime(n); const ava = this.isAvailableAtPlaytime(n);
this.board.grid[n].node.classList.toggle('disabled', !ava); 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) * Check if a tile is available at play-time (checking unlocked metals)
* *
* @param n * @param n
* @return {boolean} * @return {Boolean}
*/ */
isAvailableAtPlaytime(n) { isAvailableAtPlaytime(n) {
let ava = this.isAvailable(n); let ava = this.isAvailable(n);
const sym = this.board.grid[n].symbol; const sym = this.board.grid[n].symbol;
if (this.board.metalSequence.includes(sym)) { if (METAL_SEQ.includes(sym)) {
if (sym !== this.nextMetal) { if (sym !== this.nextMetal) {
ava = false; ava = false;
} }
@ -1170,6 +1263,7 @@ class Game {
/** /**
* Handle orb click * Handle orb click
*
* @param n * @param n
* @param orb * @param orb
*/ */
@ -1222,7 +1316,7 @@ class Game {
if (wantRefresh) { if (wantRefresh) {
if (this.countOrbs() === 0) { if (this.countOrbs() === 0) {
console.info("Good work!"); this.info("Good work!");
} }
this.updateOrbDisabledStatus(); this.updateOrbDisabledStatus();
@ -1230,26 +1324,23 @@ class Game {
} }
newGame() { newGame() {
// this.board.onTileClick = (n) => { this.board.onTileClick = (n) => {
// console.log(n, this.board.gridIndexToXy(n)); this.debug(n, gridIndexToXy(n));
// }; };
this.selectedOrb = null; this.selectedOrb = null;
let self = this; let self = this;
this.board.onOrbClick = (n, orb) => self.ingameBoardClick(n, orb); 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 // retry loop, should not be needed if everything is correct
let suc = false; let suc = false;
let numretries = 0; let numretries = 0;
const alertOnError = false; const alertOnError = false;
for (let i = 0; i < RETRY_NEW_TEMPLATE && !suc; i++) { for (let i = 0; i < this.cfg.attemptTemplates && !suc; i++) {
console.log('RNG seed is: ' + this.rng.state); this.debug('RNG seed is: ' + this.rng.state);
const template = this.getRandomTemplate(); const template = this.getRandomTemplate();
for (let j = 0; j < RETRY_IN_TEMPLATE; j++) { for (let j = 0; j < this.cfg.retryTemplate; j++) {
try { try {
this.placeOrbs(template.slice(0)); // clone this.placeOrbs(template.slice(0)); // clone
suc = true; suc = true;
@ -1257,11 +1348,15 @@ class Game {
} catch (e) { } catch (e) {
if (alertOnError) alert('welp'); if (alertOnError) alert('welp');
numretries++; numretries++;
console.error(e); if (this.cfg.debug) {
this.error(e);
} else {
this.warn(e.message);
}
} }
} }
if (!suc) { 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) { if (!suc) {
alert(`Sorry, could not find a valid board setup after ${numretries} retries.`); alert(`Sorry, could not find a valid board setup after ${numretries} retries.`);
} else { } else {
console.info(`Found valid solution (with ${numretries} retries)`); this.info(`Found valid solution (with ${numretries} retries)`);
} }
} }
@ -1296,7 +1391,8 @@ class Game {
} }
advanceMetal() { 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}`); console.debug(`Next metal unlocked: ${this.nextMetal}`);
} }
} }
@ -1304,3 +1400,5 @@ class Game {
/* Start */ /* Start */
window.game = new Game(); window.game = new Game();
})();

@ -56,11 +56,19 @@ html,body {
opacity: 0.6; opacity: 0.6;
} }
.cfg-no-fade-disabled .orb.disabled {
opacity: 1;
}
.orb-glow, .orb-glow,
.orb-shadow { .orb-shadow {
transition: opacity linear 0.1s; transition: opacity linear 0.1s;
} }
.cfg-no-anim * {
transition: none !important;
}
.orb.selected .orb-glow, .orb.selected .orb-glow,
.orb:hover .orb-glow { .orb:hover .orb-glow {
opacity: 1; opacity: 1;

Loading…
Cancel
Save