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 {
/**
* 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],
isXyOutside(x - 1, y - 1) || !this.board.grid[n - 12],
isXyOutside(x, y - 1) || !this.board.grid[n - 11],
isXyOutside(x + 1, y) || !this.board.grid[n + 1],
isXyOutside(x + 1, y + 1) || !this.board.grid[n + 12],
isXyOutside(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++) {
let allo = template.includes(i);
for (let i = 0; i < BOARD_SIZE; i++) {
const 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 && !isXyOutside(x, y)) {
outsideTemplate.push(i);
}
@ -868,7 +961,7 @@ class Game {
}
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 (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.log(`Unplace ${n}`);
this.debug(`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.log('placing a pair.');
this.debug('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.log(`try #${i + 1}`);
this.debug(`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.log('Solution: ', solution);
this.debug('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 {boolean}
* @return {Boolean}
*/
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.log('RNG seed is: ' + this.rng.state);
for (let i = 0; i < this.cfg.attemptTemplates && !suc; i++) {
this.debug('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();
})();

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

Loading…
Cancel
Save