|
|
@ -739,11 +739,10 @@ class Board { |
|
|
|
this.buttons.undo = addButton(x0, y0 + ysp*2, 'Undo', 'disabled'); |
|
|
|
this.buttons.undo = addButton(x0, y0 + ysp*2, 'Undo', 'disabled'); |
|
|
|
this.buttons.working = addButton(x0, y0 + ysp*3, 'Working…', 'working'); |
|
|
|
this.buttons.working = addButton(x0, y0 + ysp*3, 'Working…', 'working'); |
|
|
|
|
|
|
|
|
|
|
|
const cfgy0 = 10; |
|
|
|
const cfgy0 = 10.5; |
|
|
|
|
|
|
|
|
|
|
|
this.buttons.optFancy = addButton(x0, cfgy0, 'Effects:', 'config'); |
|
|
|
this.buttons.optFancy = addButton(x0, cfgy0, 'Effects:', 'config'); |
|
|
|
this.buttons.optBlockedEffect = addButton(x0, cfgy0+ysp2, 'Dim Blocked:', 'config'); |
|
|
|
this.buttons.optBlockedEffect = addButton(x0, cfgy0+ysp2, 'Dim Blocked:', 'config'); |
|
|
|
this.buttons.optSloppy = addButton(x0, cfgy0+ysp2*2, 'Sloppy Mode:', 'config'); |
|
|
|
|
|
|
|
this.buttons.toggleFullscreen = addButton(x0, cfgy0+ysp2*-1.5, 'Fullscreen', 'config'); |
|
|
|
this.buttons.toggleFullscreen = addButton(x0, cfgy0+ysp2*-1.5, 'Fullscreen', 'config'); |
|
|
|
|
|
|
|
|
|
|
|
this.buttons.btnAbout = addButton(x0, cfgy0+ysp2*-2.5, 'Help', 'config'); |
|
|
|
this.buttons.btnAbout = addButton(x0, cfgy0+ysp2*-2.5, 'Help', 'config'); |
|
|
@ -761,7 +760,6 @@ class Board { |
|
|
|
updateSettingsGUI(cfg) { |
|
|
|
updateSettingsGUI(cfg) { |
|
|
|
this.buttons.optFancy.textContent = 'Effects: '+(cfg.svgEffects ? 'On' : 'Off'); |
|
|
|
this.buttons.optFancy.textContent = 'Effects: '+(cfg.svgEffects ? 'On' : 'Off'); |
|
|
|
this.buttons.optBlockedEffect.textContent = 'Dim Blocked: '+(cfg.dimBlocked ? 'On' : 'Off'); |
|
|
|
this.buttons.optBlockedEffect.textContent = 'Dim Blocked: '+(cfg.dimBlocked ? 'On' : 'Off'); |
|
|
|
this.buttons.optSloppy.textContent = 'Sloppy Mode: '+(cfg.allowTemplateAugmenting ? 'On' : 'Off'); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
@ -876,10 +874,8 @@ class SettingsStorage { |
|
|
|
this.defaults = { |
|
|
|
this.defaults = { |
|
|
|
version: 1, |
|
|
|
version: 1, |
|
|
|
log: 'info', |
|
|
|
log: 'info', |
|
|
|
allowTemplateAugmenting: false, |
|
|
|
retryTemplate: 70, |
|
|
|
retryTemplate: 120, |
|
|
|
|
|
|
|
attemptTemplates: 20, |
|
|
|
attemptTemplates: 20, |
|
|
|
difficulty: 35, |
|
|
|
|
|
|
|
svgEffects: false, |
|
|
|
svgEffects: false, |
|
|
|
dimBlocked: true, |
|
|
|
dimBlocked: true, |
|
|
|
logSolution: false, |
|
|
|
logSolution: false, |
|
|
@ -1238,7 +1234,6 @@ class Game { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
placeOrbs(template) { |
|
|
|
placeOrbs(template) { |
|
|
|
// let placer = new RadialOrbPlacer(this, template);
|
|
|
|
|
|
|
|
let placer = new RecursiveOrbPlacer(this, template); |
|
|
|
let placer = new RecursiveOrbPlacer(this, template); |
|
|
|
return placer.place(); |
|
|
|
return placer.place(); |
|
|
|
} |
|
|
|
} |
|
|
@ -1431,7 +1426,9 @@ class Game { |
|
|
|
installButtonHandlers() { |
|
|
|
installButtonHandlers() { |
|
|
|
this.board.buttons.restart.addEventListener('click', () => { |
|
|
|
this.board.buttons.restart.addEventListener('click', () => { |
|
|
|
this.info("New Game with the same seed"); |
|
|
|
this.info("New Game with the same seed"); |
|
|
|
this.newGameWithLoader(this.rng.seed); |
|
|
|
while (this.undoStack.length) { |
|
|
|
|
|
|
|
this.undo(); |
|
|
|
|
|
|
|
} |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
this.board.buttons.randomize.addEventListener('click', () => { |
|
|
|
this.board.buttons.randomize.addEventListener('click', () => { |
|
|
@ -1467,13 +1464,6 @@ class Game { |
|
|
|
}) |
|
|
|
}) |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
this.board.buttons.optSloppy.addEventListener('click', () => { |
|
|
|
|
|
|
|
this.info("Toggle sloppy placement mode"); |
|
|
|
|
|
|
|
this.setCfg({ |
|
|
|
|
|
|
|
allowTemplateAugmenting: !this.cfg.allowTemplateAugmenting, |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.board.buttons.toggleFullscreen.addEventListener('click', () => { |
|
|
|
this.board.buttons.toggleFullscreen.addEventListener('click', () => { |
|
|
|
this.info("Toggle Fullscreen"); |
|
|
|
this.info("Toggle Fullscreen"); |
|
|
|
if (document.fullscreenElement) { |
|
|
|
if (document.fullscreenElement) { |
|
|
@ -1833,150 +1823,6 @@ class BaseOrbPlacer { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Orb placement algorithm that starts in the center and places orbs in rings, with some |
|
|
|
|
|
|
|
* small jitter allowed. |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
class RadialOrbPlacer extends BaseOrbPlacer { |
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Find a candidate cell |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @param {number[]|null} except - indices to exclude |
|
|
|
|
|
|
|
* @return {number} |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
findBestCandidate(except = null) { |
|
|
|
|
|
|
|
let candidates = []; |
|
|
|
|
|
|
|
for (let n of this.template) { |
|
|
|
|
|
|
|
if (except && except.includes(n)) continue; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!this.board.grid[n]) { |
|
|
|
|
|
|
|
// is free
|
|
|
|
|
|
|
|
const neigh = this.getCellInfo(n); |
|
|
|
|
|
|
|
if (neigh.freeSequence >= 3) { |
|
|
|
|
|
|
|
candidates.push({ |
|
|
|
|
|
|
|
n, |
|
|
|
|
|
|
|
cw: neigh.centerWeight |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
candidates.sort((a, b) => a.cw - b.cw); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (candidates.length) { |
|
|
|
|
|
|
|
// return candidates[0].n;
|
|
|
|
|
|
|
|
let top = []; |
|
|
|
|
|
|
|
let topw = candidates[0].cw; |
|
|
|
|
|
|
|
for(let cand of candidates) { |
|
|
|
|
|
|
|
if (cand.cw <= topw + this.cfg.difficulty) { // TODO this could vary by template
|
|
|
|
|
|
|
|
top.push(cand); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//the neighbor count is not used for anything anymore, oops
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// console.log('Got a choice of '+top.length+' tiles');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return this.rng.arrayChoose(top).n; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return false; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Find index for next placement |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @param {number[]|null} except - indices to exclude |
|
|
|
|
|
|
|
* @return {number} - index |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
findAvailableIndex(except = null) { |
|
|
|
|
|
|
|
const n = this.findBestCandidate(except); |
|
|
|
|
|
|
|
if (n !== false) return n; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// this corrupts the template, but makes the likelihood of quickly finding a valid solution much higher.
|
|
|
|
|
|
|
|
if (this.template.length !== BOARD_SIZE && this.cfg.allowTemplateAugmenting) { |
|
|
|
|
|
|
|
// Prefer tile with more neighbours to make the game harder
|
|
|
|
|
|
|
|
let candidates = []; |
|
|
|
|
|
|
|
this.outsideTemplate.forEach((n) => { |
|
|
|
|
|
|
|
if (!this.templateMap[n] && this.isAvailable(n)) { |
|
|
|
|
|
|
|
const neigh = this.getCellInfo(n); |
|
|
|
|
|
|
|
if (!candidates[neigh.neighbours]) { |
|
|
|
|
|
|
|
candidates[neigh.neighbours] = [n]; |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
candidates[neigh.neighbours].push(n); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 6; i >= 0; i--) { |
|
|
|
|
|
|
|
if (!candidates[i]) continue; |
|
|
|
|
|
|
|
let toAdd = this.rng.arrayChoose(candidates[i]); |
|
|
|
|
|
|
|
this.templateMap[toAdd] = true; |
|
|
|
|
|
|
|
this.outsideTemplate.splice(this.outsideTemplate.indexOf(toAdd), 1); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.template.push(toAdd); |
|
|
|
|
|
|
|
this.tilesAdded++; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.warn(`Adding extra tile to template: ${toAdd}`); |
|
|
|
|
|
|
|
return toAdd; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
throw Error("Failed to find available board tile."); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
doPlace(toPlace) { |
|
|
|
|
|
|
|
this.tilesAdded = 0; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
while (toPlace.length > 0) { |
|
|
|
|
|
|
|
this.trace('placing a pair.'); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let symbol1 = toPlace.pop(); |
|
|
|
|
|
|
|
let index1 = this.findAvailableIndex(); |
|
|
|
|
|
|
|
this.placeOrb(index1, symbol1); |
|
|
|
|
|
|
|
this.solution.push([symbol1, index1]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let symbol2 = toPlace.pop(); |
|
|
|
|
|
|
|
let index2 = this.findAvailableIndex(); |
|
|
|
|
|
|
|
this.placeOrb(index2, symbol2); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!this.isAvailable(index1)) { |
|
|
|
|
|
|
|
this.debug(`Deadlock, trying to work around it - ${index1}, ${index2}`); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.removeOrb(index2); |
|
|
|
|
|
|
|
let except = [index2]; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let suc = false; |
|
|
|
|
|
|
|
for (let i = 0; i < 5; i++) { |
|
|
|
|
|
|
|
this.trace(`try #${i + 1}`); |
|
|
|
|
|
|
|
let index = this.findAvailableIndex(except); |
|
|
|
|
|
|
|
this.placeOrb(index, symbol2); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (this.isAvailable(index1)) { |
|
|
|
|
|
|
|
suc = true; |
|
|
|
|
|
|
|
index2 = index; |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
this.removeOrb(index); |
|
|
|
|
|
|
|
except.push(index); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!suc) { |
|
|
|
|
|
|
|
throw new Error("Solution contains a deadlock."); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// index2 is updated in the fixing loop
|
|
|
|
|
|
|
|
this.solution.push([symbol2, index2]); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return { |
|
|
|
|
|
|
|
tilesAdded: this.tilesAdded, |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RecursiveOrbPlacer extends BaseOrbPlacer { |
|
|
|
class RecursiveOrbPlacer extends BaseOrbPlacer { |
|
|
|
doPlace(toPlace) { |
|
|
|
doPlace(toPlace) { |
|
|
|
this.toPlace = toPlace; |
|
|
|
this.toPlace = toPlace; |
|
|
|