big cleanup, more config & get args for setting some config opts; improve debug logging

master
Ondřej Hruška 5 years ago
parent c4e86ef1a4
commit 8ec8ac6818
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 330
      script.js
  2. 59
      style.css

@ -1,5 +1,3 @@
!(function () {
class Svg {
/**
* Build a node from XML
@ -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);
})
}
@ -615,10 +612,13 @@
* @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;
}
@ -656,15 +658,17 @@
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,7 +697,58 @@
}
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 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;
}
}
@ -701,19 +756,65 @@
/**
* 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]);
}
@ -1088,6 +1214,11 @@
});
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(`Found valid solution (with ${numretries} retries)`);
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 {
if (entry[0] !== 'gold') {
s += " + ";
}
}
return s;
}, ''));
}
}
@ -1398,4 +1579,3 @@
window.game = new Game();
})();

@ -1,4 +1,4 @@
*,*:before,*:after {
*, *::before, *::after {
box-sizing: border-box;
}
@ -22,6 +22,7 @@ html,body {
opacity: 0.6;
}
/* Highlighting */
.highlight-salt .symbol-salt .orb-fill,
.highlight-air .symbol-air .orb-fill,
.highlight-fire .symbol-fire .orb-fill,
@ -40,33 +41,33 @@ html,body {
stroke-width: 7px;
}
.orb.disabled .orb-glow,
.orb.disabled .orb-shadow,
.orb.disabled:hover .orb-glow,
.orb.disabled:hover .orb-shadow {
opacity: 0;
}
/* Orb is clickable */
.orb {
cursor: pointer;
}
/* Disabled effect */
.orb.disabled {
cursor: default;
opacity: 0.6;
}
.cfg-no-fade-disabled .orb.disabled {
opacity: 1;
/* Disabled orb has no glow or shadow */
.orb.disabled .orb-glow,
.orb.disabled .orb-shadow,
.orb.disabled:hover .orb-glow,
.orb.disabled:hover .orb-shadow {
opacity: 0;
}
.orb-glow,
.orb-shadow {
transition: opacity linear 0.1s;
/* Blur effect */
.orb-shadow, .orb-glow {
filter: url('#filterDropshadow');
}
.cfg-no-anim * {
transition: none !important;
/* Hover and select effects */
.orb-glow, .orb-shadow {
transition: opacity linear 0.1s;
}
.orb.selected .orb-glow,
@ -78,3 +79,31 @@ html,body {
.orb:hover .orb-shadow {
opacity: 0;
}
/* No-anim version applies animations instantly */
.cfg-no-anim * {
transition: none !important;
}
/* No-blur version uses white ring around selected orbs, and has no shadow */
.cfg-no-blur .orb-shadow,
.cfg-no-blur .orb-glow {
filter: none !important;
opacity: 0 !important;
}
.cfg-no-blur .orb.selected .orb-fill,
.cfg-no-blur .orb:hover .orb-fill {
stroke: white;
stroke-width: 10px;
paint-order: stroke;
}
.cfg-no-blur .orb.disabled .orb-fill {
stroke: transparent !important;
}
/* No-disabled-fade version has all orbs at full opacity */
.cfg-no-fade-disabled .orb.disabled {
opacity: 1;
}

Loading…
Cancel
Save