|
|
@ -262,6 +262,15 @@ class Board { |
|
|
|
this.removeOrbByIndex(index) |
|
|
|
this.removeOrbByIndex(index) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Count orbs in the game board |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @return {number} |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
countOrbs() { |
|
|
|
|
|
|
|
return this.grid.reduce((acu, x) => acu + (x !== null), 0); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* Remove orb by array index |
|
|
|
* Remove orb by array index |
|
|
|
* |
|
|
|
* |
|
|
@ -642,41 +651,115 @@ class Board { |
|
|
|
return this.grid[n]; |
|
|
|
return this.grid[n]; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Check if a cell is available for selection in-game |
|
|
|
|
|
|
|
* (checking surroundings only). This should work the same regardless |
|
|
|
|
|
|
|
* of whether the cell is occupied or not. |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @param n |
|
|
|
|
|
|
|
* @return {boolean} |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
isAvailable(n) { |
|
|
|
|
|
|
|
return this.getCellInfo(n).freeSequence >= 3; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Get info about a cell surroundings and position |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* neighbours - number of occupied neighbouring cells |
|
|
|
|
|
|
|
* centerWeight - distance from the board center |
|
|
|
|
|
|
|
* freeSequence - number of contiguous free tiles in the direct neighbourhood |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @param n |
|
|
|
|
|
|
|
* @return {{neighbours: number, centerWeight: number, freeSequence: number}} |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
getCellInfo(n) { |
|
|
|
|
|
|
|
let {x, y} = gridIndexToXy(n); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let freeSpaces = [ |
|
|
|
|
|
|
|
isXyOutside(x - 1, y) || !this.grid[n - 1], |
|
|
|
|
|
|
|
isXyOutside(x - 1, y - 1) || !this.grid[n - 12], |
|
|
|
|
|
|
|
isXyOutside(x, y - 1) || !this.grid[n - 11], |
|
|
|
|
|
|
|
isXyOutside(x + 1, y) || !this.grid[n + 1], |
|
|
|
|
|
|
|
isXyOutside(x + 1, y + 1) || !this.grid[n + 12], |
|
|
|
|
|
|
|
isXyOutside(x, y + 1) || !this.grid[n + 11], |
|
|
|
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let nOccupied = 0; |
|
|
|
|
|
|
|
for (let i = 0; i < 6; i++) { |
|
|
|
|
|
|
|
if (!freeSpaces[i]) { |
|
|
|
|
|
|
|
nOccupied++; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let freeSequence = 0; |
|
|
|
|
|
|
|
let maxFreeSequence = 0; |
|
|
|
|
|
|
|
for (let i = 0; i < 12; i++) { |
|
|
|
|
|
|
|
if (freeSpaces[i % 6]) { |
|
|
|
|
|
|
|
freeSequence++; |
|
|
|
|
|
|
|
if (freeSequence >= 6) { |
|
|
|
|
|
|
|
maxFreeSequence = freeSequence; |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (freeSequence > maxFreeSequence) { |
|
|
|
|
|
|
|
maxFreeSequence = freeSequence; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
freeSequence = 0; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let { rx, ry } = this.gridXyToCoord(x, y); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return { |
|
|
|
|
|
|
|
neighbours: nOccupied, |
|
|
|
|
|
|
|
freeSequence: maxFreeSequence, |
|
|
|
|
|
|
|
centerWeight: Math.round(Math.sqrt(Math.pow(rx, 2) + Math.pow(ry, 2))), |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Build the game GUI (buttons, links) |
|
|
|
|
|
|
|
*/ |
|
|
|
buildGui() { |
|
|
|
buildGui() { |
|
|
|
|
|
|
|
const addButton = (x, y, text, classes='') => { |
|
|
|
|
|
|
|
let { rx, ry } = this.guiXyToCoord(x, y); |
|
|
|
|
|
|
|
let button = Svg.fromXML(`<text class="button-text${classes?' '+classes:''}" x="${rx}" y="${ry}">${text}</text>`); |
|
|
|
|
|
|
|
this.$root.appendChild(button); |
|
|
|
|
|
|
|
return button; |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const y0 = 0.05; |
|
|
|
const y0 = 0.05; |
|
|
|
const x0 = 0; |
|
|
|
const x0 = 0; |
|
|
|
const ysp = 0.75; |
|
|
|
const ysp = 0.75; |
|
|
|
const ysp2 = 0.6; |
|
|
|
const ysp2 = 0.6; |
|
|
|
|
|
|
|
|
|
|
|
this.buttons.randomize = this.addButton(x0, y0, 'Randomize'); |
|
|
|
this.buttons.randomize = addButton(x0, y0, 'Randomize'); |
|
|
|
this.buttons.restart = this.addButton(x0, y0 + ysp, 'Try Again', 'disabled'); |
|
|
|
this.buttons.restart = addButton(x0, y0 + ysp, 'Try Again', 'disabled'); |
|
|
|
this.buttons.undo = this.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'); |
|
|
|
|
|
|
|
|
|
|
|
const cfgy0 = 10; |
|
|
|
const cfgy0 = 10.5; |
|
|
|
|
|
|
|
|
|
|
|
this.buttons.optFancy = this.addButton(x0, cfgy0, 'Effects:', 'config'); |
|
|
|
this.buttons.optFancy = addButton(x0, cfgy0, 'Effects:', 'config'); |
|
|
|
this.buttons.optBlockedEffect = this.addButton(x0, cfgy0+ysp2, 'Dim Blocked:', 'config'); |
|
|
|
this.buttons.optBlockedEffect = addButton(x0, cfgy0+ysp2, 'Dim Blocked:', 'config'); |
|
|
|
this.buttons.optSloppy = this.addButton(x0, cfgy0+ysp2*2, 'Sloppy Mode:', 'config'); |
|
|
|
this.buttons.toggleFullscreen = addButton(x0, cfgy0+ysp2*-1.5, 'Fullscreen', 'config'); |
|
|
|
this.buttons.toggleFullscreen = this.addButton(x0, cfgy0+ysp2*-1.5, 'Fullscreen', 'config'); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.buttons.btnAbout = this.addButton(x0, cfgy0+ysp2*-2.5, 'Help', 'config'); |
|
|
|
this.buttons.btnAbout = addButton(x0, cfgy0+ysp2*-2.5, 'Help', 'config'); |
|
|
|
|
|
|
|
|
|
|
|
let youWin = Svg.fromXML(`<text class="you-win" opacity="0" x="0" y="0">Good work!</text>`); |
|
|
|
let youWin = Svg.fromXML(`<text class="you-win" opacity="0" x="0" y="0">Good work!</text>`); |
|
|
|
this.$root.appendChild(youWin); |
|
|
|
this.$root.appendChild(youWin); |
|
|
|
this.youWin = youWin; |
|
|
|
this.youWin = youWin; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Update toggles |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @param cfg |
|
|
|
|
|
|
|
*/ |
|
|
|
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'); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
addButton(x, y, text, classes='') { |
|
|
|
|
|
|
|
let { rx, ry } = this.guiXyToCoord(x, y); |
|
|
|
|
|
|
|
let button = Svg.fromXML(`<text class="button-text${classes?' '+classes:''}" x="${rx}" y="${ry}">${text}</text>`); |
|
|
|
|
|
|
|
this.$root.appendChild(button); |
|
|
|
|
|
|
|
return button; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
@ -733,6 +816,56 @@ class Rng { |
|
|
|
nextInt(max) { |
|
|
|
nextInt(max) { |
|
|
|
return Math.floor((max + 1) * this.next()); |
|
|
|
return Math.floor((max + 1) * this.next()); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Shuffle an array. |
|
|
|
|
|
|
|
* The array is shuffled in place. |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @return the array |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
arrayShuffle(a) { |
|
|
|
|
|
|
|
let j, x, i; |
|
|
|
|
|
|
|
for (i = a.length - 1; i > 0; i--) { |
|
|
|
|
|
|
|
j = this.nextInt(i); |
|
|
|
|
|
|
|
x = a[i]; |
|
|
|
|
|
|
|
a[i] = a[j]; |
|
|
|
|
|
|
|
a[j] = x; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return a; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Choose a random member of an array |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @param array |
|
|
|
|
|
|
|
* @return {number} |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
arrayChoose(array) { |
|
|
|
|
|
|
|
return array[Math.floor(this.next() * array.length)]; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Sneak items in the given order into another array |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @param array - the modified array |
|
|
|
|
|
|
|
* @param startKeepout - how many leading lements to leave untouched |
|
|
|
|
|
|
|
* @param endKeepout - how many trailing elements to leave untouched |
|
|
|
|
|
|
|
* @param items - items to sneak in |
|
|
|
|
|
|
|
* @return the modified array |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
arraySneak(array, startKeepout, endKeepout, items) { |
|
|
|
|
|
|
|
let positions = []; |
|
|
|
|
|
|
|
for (let i = 0; i < items.length; i++) { |
|
|
|
|
|
|
|
positions.push(startKeepout + this.nextInt(array.length - startKeepout - endKeepout)) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
positions.sort((a, b) => a - b); |
|
|
|
|
|
|
|
// inject them into the array
|
|
|
|
|
|
|
|
items.forEach((pair, i) => { |
|
|
|
|
|
|
|
array.splice(positions[i] + i, 0, pair); |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return array; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
class SettingsStorage { |
|
|
|
class SettingsStorage { |
|
|
@ -741,13 +874,12 @@ 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, |
|
|
|
|
|
|
|
highlightTemplate: false, |
|
|
|
}; |
|
|
|
}; |
|
|
|
this.settings = Object.assign({}, this.defaults); |
|
|
|
this.settings = Object.assign({}, this.defaults); |
|
|
|
} |
|
|
|
} |
|
|
@ -932,7 +1064,7 @@ class Game { |
|
|
|
|
|
|
|
|
|
|
|
// Defer start to give browser time to render the background
|
|
|
|
// Defer start to give browser time to render the background
|
|
|
|
setTimeout(() => { |
|
|
|
setTimeout(() => { |
|
|
|
this.newGame(args.seed) |
|
|
|
this.newGameWithLoader(args.seed) |
|
|
|
}, 50); |
|
|
|
}, 50); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
@ -1101,358 +1233,9 @@ class Game { |
|
|
|
}; |
|
|
|
}; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
isAvailable(n) { |
|
|
|
|
|
|
|
return this.getNeighbours(n).freeSequence >= 3; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getNeighbours(n) { |
|
|
|
|
|
|
|
let {x, y} = gridIndexToXy(n); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let freeSpaces = [ |
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
for (let i = 0; i < 6; i++) { |
|
|
|
|
|
|
|
if (!freeSpaces[i]) { |
|
|
|
|
|
|
|
nOccupied++; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let freeSequence = 0; |
|
|
|
|
|
|
|
let maxFreeSequence = 0; |
|
|
|
|
|
|
|
for (let i = 0; i < 12; i++) { |
|
|
|
|
|
|
|
if (freeSpaces[i % 6]) { |
|
|
|
|
|
|
|
freeSequence++; |
|
|
|
|
|
|
|
if (freeSequence >= 6) { |
|
|
|
|
|
|
|
maxFreeSequence = freeSequence; |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (freeSequence > maxFreeSequence) { |
|
|
|
|
|
|
|
maxFreeSequence = freeSequence; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
freeSequence = 0; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let { rx, ry } = this.board.gridXyToCoord(x, y); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return { |
|
|
|
|
|
|
|
neighbours: nOccupied, |
|
|
|
|
|
|
|
freeSequence: maxFreeSequence, |
|
|
|
|
|
|
|
centerWeight: Math.round(Math.sqrt(Math.pow(rx, 2) + Math.pow(ry, 2))), |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
placeOrbs(template) { |
|
|
|
placeOrbs(template) { |
|
|
|
let tilesAdded = 0; |
|
|
|
let placer = new RecursiveOrbPlacer(this, template); |
|
|
|
|
|
|
|
return placer.place(); |
|
|
|
this.board.removeAllOrbs(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let allowedTable = []; |
|
|
|
|
|
|
|
let outsideTemplate = []; |
|
|
|
|
|
|
|
for (let i = 0; i < BOARD_SIZE; i++) { |
|
|
|
|
|
|
|
const allo = template.includes(i); |
|
|
|
|
|
|
|
allowedTable.push(allo); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let {x, y} = gridIndexToXy(i); |
|
|
|
|
|
|
|
if (!allo && !isXyOutside(x, y)) { |
|
|
|
|
|
|
|
outsideTemplate.push(i); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Highlight pattern shape
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// if (this.board.tiles[i]) {
|
|
|
|
|
|
|
|
// if (allo) {
|
|
|
|
|
|
|
|
// this.board.tiles[i].setAttribute('opacity', 1)
|
|
|
|
|
|
|
|
// } else {
|
|
|
|
|
|
|
|
// this.board.tiles[i].setAttribute('opacity', 0.6)
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const 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
|
|
|
|
|
|
|
|
this.board.grid[n] = symbol; |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const unplace = (n) => { |
|
|
|
|
|
|
|
this.trace(`Unplace ${n}`); |
|
|
|
|
|
|
|
this.board.grid[n] = null; |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const findBestCandidate = (except = null) => { |
|
|
|
|
|
|
|
let candidates = []; |
|
|
|
|
|
|
|
for (let n of template) { |
|
|
|
|
|
|
|
if (except && except.includes(n)) continue; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!this.board.grid[n]) { |
|
|
|
|
|
|
|
// is free
|
|
|
|
|
|
|
|
const neigh = this.getNeighbours(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.arrayChoose(top).n; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return false; |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const findAvailableIndex = (except = null) => { |
|
|
|
|
|
|
|
const n = findBestCandidate(except); |
|
|
|
|
|
|
|
if (n !== false) return n; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// this corrupts the template, but makes the likelihood of quickly finding a valid solution much higher.
|
|
|
|
|
|
|
|
if (template.length !== BOARD_SIZE && this.cfg.allowTemplateAugmenting) { |
|
|
|
|
|
|
|
// Prefer tile with more neighbours to make the game harder
|
|
|
|
|
|
|
|
let candidates = []; |
|
|
|
|
|
|
|
outsideTemplate.forEach((n) => { |
|
|
|
|
|
|
|
if (!allowedTable[n] && this.isAvailable(n)) { |
|
|
|
|
|
|
|
const neigh = this.getNeighbours(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.arrayChoose(candidates[i]); |
|
|
|
|
|
|
|
allowedTable[toAdd] = true; |
|
|
|
|
|
|
|
outsideTemplate.splice(outsideTemplate.indexOf(toAdd), 1); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
template.push(toAdd); |
|
|
|
|
|
|
|
tilesAdded++; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.warn(`Adding extra tile to template: ${toAdd}`); |
|
|
|
|
|
|
|
return toAdd; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
throw Error("Failed to find available board tile."); |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
place(60, 'gold'); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const toPlace = this.buildPlacementList(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let solution = [ |
|
|
|
|
|
|
|
['gold', 60], |
|
|
|
|
|
|
|
]; |
|
|
|
|
|
|
|
while (toPlace.length > 0) { |
|
|
|
|
|
|
|
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(); |
|
|
|
|
|
|
|
place(index2, symbol2); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!this.isAvailable(index1)) { |
|
|
|
|
|
|
|
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++) { |
|
|
|
|
|
|
|
this.trace(`try #${i + 1}`); |
|
|
|
|
|
|
|
let index = findAvailableIndex(except); |
|
|
|
|
|
|
|
place(index, symbol2); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (this.isAvailable(index1)) { |
|
|
|
|
|
|
|
suc = true; |
|
|
|
|
|
|
|
index2 = index; |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
unplace(index); |
|
|
|
|
|
|
|
except.push(index); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!suc) { |
|
|
|
|
|
|
|
throw new Error("Solution contains a deadlock."); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// index2 is updated in the fixing loop
|
|
|
|
|
|
|
|
solution.push([symbol2, index2]); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Show the solution for debug
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
solution.reverse(); |
|
|
|
|
|
|
|
this.info("Found a valid board!"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
solution.forEach((a) => { |
|
|
|
|
|
|
|
let p = gridIndexToXy(a[1]); |
|
|
|
|
|
|
|
a[1] = `${p.x}×${p.y}`; |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.debug('Solution:', solution); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return { |
|
|
|
|
|
|
|
solution, |
|
|
|
|
|
|
|
tilesAdded: tilesAdded, |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Shuffle an array. |
|
|
|
|
|
|
|
* The array is shuffled in place. |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @return the array |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
arrayShuffle(a) { |
|
|
|
|
|
|
|
let j, x, i; |
|
|
|
|
|
|
|
for (i = a.length - 1; i > 0; i--) { |
|
|
|
|
|
|
|
j = this.rng.nextInt(i); |
|
|
|
|
|
|
|
x = a[i]; |
|
|
|
|
|
|
|
a[i] = a[j]; |
|
|
|
|
|
|
|
a[j] = x; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return a; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Count orbs in the game board |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @return {number} |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
countOrbs() { |
|
|
|
|
|
|
|
return this.board.grid.reduce((acu, x) => acu + (x !== null), 0); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Choose a random emember of an array |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @param array |
|
|
|
|
|
|
|
* @return {number} |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
arrayChoose(array) { |
|
|
|
|
|
|
|
return array[Math.floor(this.rng.next() * array.length)]; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
buildPlacementList() { |
|
|
|
|
|
|
|
let toPlace = [ |
|
|
|
|
|
|
|
['air', 'air'], |
|
|
|
|
|
|
|
['air', 'air'], |
|
|
|
|
|
|
|
['air', 'air'], |
|
|
|
|
|
|
|
['air', 'air'], |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
['fire', 'fire'], |
|
|
|
|
|
|
|
['fire', 'fire'], |
|
|
|
|
|
|
|
['fire', 'fire'], |
|
|
|
|
|
|
|
['fire', 'fire'], |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
['water', 'water'], |
|
|
|
|
|
|
|
['water', 'water'], |
|
|
|
|
|
|
|
['water', 'water'], |
|
|
|
|
|
|
|
['water', 'water'], |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
['earth', 'earth'], |
|
|
|
|
|
|
|
['earth', 'earth'], |
|
|
|
|
|
|
|
['earth', 'earth'], |
|
|
|
|
|
|
|
['earth', 'earth'], |
|
|
|
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let newSaltedPairs = []; |
|
|
|
|
|
|
|
const nsalted = this.rng.nextInt(2); |
|
|
|
|
|
|
|
for (let i = 0; i < nsalted; i++) { |
|
|
|
|
|
|
|
while (true) { |
|
|
|
|
|
|
|
const n = this.rng.nextInt(toPlace.length - 1); |
|
|
|
|
|
|
|
if (toPlace[n][1] !== 'salt') { |
|
|
|
|
|
|
|
this.trace(`Pairing ${toPlace[n][1]} with salt.`); |
|
|
|
|
|
|
|
newSaltedPairs.push([toPlace[n][1], 'salt']); |
|
|
|
|
|
|
|
toPlace[n][1] = 'salt'; |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
toPlace = toPlace.concat(newSaltedPairs); |
|
|
|
|
|
|
|
// if we have some salt pairs left
|
|
|
|
|
|
|
|
for (let i = 0; i < 2 - nsalted; i++) { |
|
|
|
|
|
|
|
toPlace.push(['salt', 'salt']); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// these are always paired like this, and don't support salt
|
|
|
|
|
|
|
|
toPlace = toPlace.concat([ |
|
|
|
|
|
|
|
['mors', 'vitae'], |
|
|
|
|
|
|
|
['mors', 'vitae'], |
|
|
|
|
|
|
|
['mors', 'vitae'], |
|
|
|
|
|
|
|
['mors', 'vitae'], |
|
|
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// shuffle the pairs that have random order (i.e. not metals)
|
|
|
|
|
|
|
|
this.arrayShuffle(toPlace); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// the order here is actually significant, so let's pay attention...
|
|
|
|
|
|
|
|
const metals = [ |
|
|
|
|
|
|
|
['lead', 'mercury'], |
|
|
|
|
|
|
|
['tin', 'mercury'], |
|
|
|
|
|
|
|
['iron', 'mercury'], |
|
|
|
|
|
|
|
['copper', 'mercury'], |
|
|
|
|
|
|
|
['silver', 'mercury'], |
|
|
|
|
|
|
|
]; |
|
|
|
|
|
|
|
let mPos = []; |
|
|
|
|
|
|
|
for (let i = 0; i < metals.length; i++) { |
|
|
|
|
|
|
|
let x; |
|
|
|
|
|
|
|
// find a unique position
|
|
|
|
|
|
|
|
do { |
|
|
|
|
|
|
|
x = this.rng.nextInt(toPlace.length + i); |
|
|
|
|
|
|
|
} while (mPos.includes(x)); |
|
|
|
|
|
|
|
mPos.push(x) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
mPos.sort((a, b) => a - b); |
|
|
|
|
|
|
|
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]); |
|
|
|
|
|
|
|
return a; |
|
|
|
|
|
|
|
}, []); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
getPairSymbols(first) { |
|
|
|
getPairSymbols(first) { |
|
|
@ -1500,7 +1283,7 @@ class Game { |
|
|
|
* @return {Boolean} |
|
|
|
* @return {Boolean} |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
isAvailableAtPlaytime(n) { |
|
|
|
isAvailableAtPlaytime(n) { |
|
|
|
let ava = this.isAvailable(n); |
|
|
|
let ava = this.board.isAvailable(n); |
|
|
|
|
|
|
|
|
|
|
|
const sym = this.board.grid[n].symbol; |
|
|
|
const sym = this.board.grid[n].symbol; |
|
|
|
if (METAL_SEQ.includes(sym)) { |
|
|
|
if (METAL_SEQ.includes(sym)) { |
|
|
@ -1623,7 +1406,7 @@ class Game { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (wantRefresh) { |
|
|
|
if (wantRefresh) { |
|
|
|
if (this.countOrbs() === 0) { |
|
|
|
if (this.board.countOrbs() === 0) { |
|
|
|
this.info("Good work!"); |
|
|
|
this.info("Good work!"); |
|
|
|
|
|
|
|
|
|
|
|
if (removed) { |
|
|
|
if (removed) { |
|
|
@ -1643,12 +1426,14 @@ 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.newGame(this.rng.seed); |
|
|
|
while (this.undoStack.length) { |
|
|
|
|
|
|
|
this.undo(); |
|
|
|
|
|
|
|
} |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
this.board.buttons.randomize.addEventListener('click', () => { |
|
|
|
this.board.buttons.randomize.addEventListener('click', () => { |
|
|
|
this.info("New Game with a random seed"); |
|
|
|
this.info("New Game with a random seed"); |
|
|
|
this.newGame(+new Date); |
|
|
|
this.newGameWithLoader(+new Date); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
this.board.buttons.btnAbout.addEventListener('click', () => { |
|
|
|
this.board.buttons.btnAbout.addEventListener('click', () => { |
|
|
@ -1679,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) { |
|
|
@ -1700,7 +1478,7 @@ class Game { |
|
|
|
* Update button hiding attributes, disabled orb effects, etc |
|
|
|
* Update button hiding attributes, disabled orb effects, etc |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
updateGameGui() { |
|
|
|
updateGameGui() { |
|
|
|
let nOrbs = this.countOrbs(); |
|
|
|
let nOrbs = this.board.countOrbs(); |
|
|
|
|
|
|
|
|
|
|
|
this.board.buttons.restart |
|
|
|
this.board.buttons.restart |
|
|
|
.classList.toggle('disabled', nOrbs === 55); |
|
|
|
.classList.toggle('disabled', nOrbs === 55); |
|
|
@ -1724,6 +1502,15 @@ class Game { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
newGameWithLoader(seed) { |
|
|
|
|
|
|
|
this.board.buttons.working.classList.add('show'); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
|
|
|
|
|
this.newGame(seed); |
|
|
|
|
|
|
|
this.board.buttons.working.classList.remove('show'); |
|
|
|
|
|
|
|
}, 20); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
newGame(seed) { |
|
|
|
newGame(seed) { |
|
|
|
if (seed !== null) { |
|
|
|
if (seed !== null) { |
|
|
|
this.rng.setSeed(seed); |
|
|
|
this.rng.setSeed(seed); |
|
|
@ -1773,11 +1560,7 @@ class Game { |
|
|
|
break; |
|
|
|
break; |
|
|
|
} catch (e) { |
|
|
|
} catch (e) { |
|
|
|
retry_count++; |
|
|
|
retry_count++; |
|
|
|
if (this.logging_trace) { |
|
|
|
this.warn(e.message); |
|
|
|
this.error(e); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
this.warn(e.message); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
@ -1792,10 +1575,7 @@ class Game { |
|
|
|
if (!suc) { |
|
|
|
if (!suc) { |
|
|
|
alert(`Sorry, could not find a valid board setup after ${retry_count} retries.`); |
|
|
|
alert(`Sorry, could not find a valid board setup after ${retry_count} retries.`); |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
this.info( |
|
|
|
this.info(`Board set up with ${retry_count} retries.`); |
|
|
|
`Board set up with ${retry_count} retries:\n` + |
|
|
|
|
|
|
|
`teplate "${board_info.template.name}"` + |
|
|
|
|
|
|
|
(this.cfg.allowTemplateAugmenting ? ` with ${board_info.tilesAdded} extra tiles` : '')); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (this.cfg.logSolution) { |
|
|
|
if (this.cfg.logSolution) { |
|
|
|
this.info('Reference solution:\n ' + board_info.solution.reduce((s, entry, i) => { |
|
|
|
this.info('Reference solution:\n ' + board_info.solution.reduce((s, entry, i) => { |
|
|
@ -1816,6 +1596,306 @@ class Game { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Base class for orb placer. |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
class BaseOrbPlacer { |
|
|
|
|
|
|
|
constructor(game, template) { |
|
|
|
|
|
|
|
this.template = template; |
|
|
|
|
|
|
|
this.rng = game.rng; |
|
|
|
|
|
|
|
this.cfg = game.cfg; |
|
|
|
|
|
|
|
this.game = game; |
|
|
|
|
|
|
|
this.board = game.board; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Place an orb. The position must be inside template and free. |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @param n - position |
|
|
|
|
|
|
|
* @param symbol - symbol to place |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
placeOrb(n, symbol) { |
|
|
|
|
|
|
|
if (!this.templateMap[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]}`); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
this.trace(`Place ${n} <- ${symbol}`); |
|
|
|
|
|
|
|
this.board.grid[n] = symbol; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Remove an orb, if any. |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @param n - position |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
removeOrb(n) { |
|
|
|
|
|
|
|
let old = this.board.grid[n]; |
|
|
|
|
|
|
|
this.board.grid[n] = null; |
|
|
|
|
|
|
|
this.trace(`Unplace ${n} (${old})`); |
|
|
|
|
|
|
|
return old; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Check if a cell is available for selection - 3 free slots */ |
|
|
|
|
|
|
|
isAvailable(n) { |
|
|
|
|
|
|
|
return this.board.isAvailable(n); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Get cell info by number */ |
|
|
|
|
|
|
|
getCellInfo(n) { |
|
|
|
|
|
|
|
return this.board.getCellInfo(n); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
trace(...args) { |
|
|
|
|
|
|
|
this.game.debug(...args); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
debug(...args) { |
|
|
|
|
|
|
|
this.game.debug(...args); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
info(...args) { |
|
|
|
|
|
|
|
this.game.info(...args); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
warn(...args) { |
|
|
|
|
|
|
|
this.game.warn(...args); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
error(...args) { |
|
|
|
|
|
|
|
this.game.error(...args); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Build a list of orbs in the right order. |
|
|
|
|
|
|
|
* They are always grouped in matching pairs, and metals are always in |
|
|
|
|
|
|
|
* the correct reaction order. |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @return {string[]} |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
buildPlacementList() { |
|
|
|
|
|
|
|
let toPlace = [ |
|
|
|
|
|
|
|
['air', 'air'], |
|
|
|
|
|
|
|
['air', 'air'], |
|
|
|
|
|
|
|
['air', 'air'], |
|
|
|
|
|
|
|
['air', 'air'], |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
['fire', 'fire'], |
|
|
|
|
|
|
|
['fire', 'fire'], |
|
|
|
|
|
|
|
['fire', 'fire'], |
|
|
|
|
|
|
|
['fire', 'fire'], |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
['water', 'water'], |
|
|
|
|
|
|
|
['water', 'water'], |
|
|
|
|
|
|
|
['water', 'water'], |
|
|
|
|
|
|
|
['water', 'water'], |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
['earth', 'earth'], |
|
|
|
|
|
|
|
['earth', 'earth'], |
|
|
|
|
|
|
|
['earth', 'earth'], |
|
|
|
|
|
|
|
['earth', 'earth'], |
|
|
|
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let newSaltedPairs = []; |
|
|
|
|
|
|
|
const nsalted = this.rng.nextInt(2); |
|
|
|
|
|
|
|
for (let i = 0; i < nsalted; i++) { |
|
|
|
|
|
|
|
while (true) { |
|
|
|
|
|
|
|
const n = this.rng.nextInt(toPlace.length - 1); |
|
|
|
|
|
|
|
if (toPlace[n][1] !== 'salt') { |
|
|
|
|
|
|
|
this.trace(`Pairing ${toPlace[n][1]} with salt.`); |
|
|
|
|
|
|
|
newSaltedPairs.push([toPlace[n][1], 'salt']); |
|
|
|
|
|
|
|
toPlace[n][1] = 'salt'; |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
toPlace = toPlace.concat(newSaltedPairs); |
|
|
|
|
|
|
|
// if we have some salt pairs left
|
|
|
|
|
|
|
|
for (let i = 0; i < 2 - nsalted; i++) { |
|
|
|
|
|
|
|
toPlace.push(['salt', 'salt']); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// shuffle the pairs that have random order (i.e. not metals)
|
|
|
|
|
|
|
|
this.rng.arrayShuffle(toPlace); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.rng.arraySneak(toPlace, 3, 0, [ |
|
|
|
|
|
|
|
['mors', 'vitae'], |
|
|
|
|
|
|
|
['mors', 'vitae'], |
|
|
|
|
|
|
|
['mors', 'vitae'], |
|
|
|
|
|
|
|
['mors', 'vitae'], |
|
|
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.rng.arraySneak(toPlace, 4, 0, [ |
|
|
|
|
|
|
|
['lead', 'mercury'], |
|
|
|
|
|
|
|
['tin', 'mercury'], |
|
|
|
|
|
|
|
['iron', 'mercury'], |
|
|
|
|
|
|
|
['copper', 'mercury'], |
|
|
|
|
|
|
|
['silver', 'mercury'], |
|
|
|
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.debug('Placement order (last first):', toPlace); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return toPlace.reduce((a, c) => { |
|
|
|
|
|
|
|
a.push(c[0]); |
|
|
|
|
|
|
|
a.push(c[1]); |
|
|
|
|
|
|
|
return a; |
|
|
|
|
|
|
|
}, []); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Run the placement logic. |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
place() { |
|
|
|
|
|
|
|
this.board.removeAllOrbs(); |
|
|
|
|
|
|
|
this.solution = []; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let allowedTable = []; |
|
|
|
|
|
|
|
let outsideTemplate = []; |
|
|
|
|
|
|
|
for (let i = 0; i < BOARD_SIZE; i++) { |
|
|
|
|
|
|
|
const allo = this.template.includes(i); |
|
|
|
|
|
|
|
allowedTable.push(allo); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let {x, y} = gridIndexToXy(i); |
|
|
|
|
|
|
|
if (!allo && !isXyOutside(x, y)) { |
|
|
|
|
|
|
|
outsideTemplate.push(i); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Highlight pattern shape
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (this.cfg.highlightTemplate) { |
|
|
|
|
|
|
|
if (this.board.tiles[i]) { |
|
|
|
|
|
|
|
if (allo) { |
|
|
|
|
|
|
|
this.board.tiles[i].setAttribute('opacity', 1) |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
this.board.tiles[i].setAttribute('opacity', 0.6) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
this.templateMap = allowedTable; |
|
|
|
|
|
|
|
this.outsideTemplate = outsideTemplate; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.placeOrb(60, 'gold'); |
|
|
|
|
|
|
|
this.solution.push(['gold', 60]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let toPlace = this.buildPlacementList(); |
|
|
|
|
|
|
|
let rv = this.doPlace(toPlace); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let solution = this.solution; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
solution.reverse(); |
|
|
|
|
|
|
|
this.info("Found a valid board!"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
solution.forEach((a) => { |
|
|
|
|
|
|
|
let p = gridIndexToXy(a[1]); |
|
|
|
|
|
|
|
a[1] = `${p.x}×${p.y}`; |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.debug('Solution:', solution); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
rv.solution = solution; |
|
|
|
|
|
|
|
return rv; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Perform the orbs placement. |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* Orb symbols to place are given as an argument. |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* The layout template is in `this.template`, also `this.templateMap` as an array |
|
|
|
|
|
|
|
* with indices 0-121 containing true/false to indicate template membership. |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* `this.outsideTemplate` is a list of cell indices that are not in the template. |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* this.solution is an array to populate with orb placements in the format [n, symbol]. |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* The board is cleared now. When the function ends, the board should contain |
|
|
|
|
|
|
|
* the new layout (as symbol name strings) |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* After placing each orb, make sure to call `this.solution.push([symbol, index])`; |
|
|
|
|
|
|
|
* in case of backtracking, pop it again. |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* Return object to return to parent; 'solution' will be added automatically. |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
doPlace(toPlace) { |
|
|
|
|
|
|
|
throw new Error("Not implemented"); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RecursiveOrbPlacer extends BaseOrbPlacer { |
|
|
|
|
|
|
|
doPlace(toPlace) { |
|
|
|
|
|
|
|
this.toPlace = toPlace; |
|
|
|
|
|
|
|
this.recDepth = 0; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.placeOne(0); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return {}; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
placeOne() { |
|
|
|
|
|
|
|
this.recDepth++; |
|
|
|
|
|
|
|
if (this.recDepth > 1000) { |
|
|
|
|
|
|
|
throw new Error("Too many backtracks"); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let symbol = this.toPlace.pop(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let candidates = [[],[],[],[],[],[],[]]; |
|
|
|
|
|
|
|
for (let n of this.template) { |
|
|
|
|
|
|
|
if (!this.board.grid[n]) { |
|
|
|
|
|
|
|
// is free
|
|
|
|
|
|
|
|
const cell = this.getCellInfo(n); |
|
|
|
|
|
|
|
if (cell.freeSequence >= 3) { |
|
|
|
|
|
|
|
candidates[cell.neighbours].push(n); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (let neighs = 6; neighs >= 0; neighs--) { |
|
|
|
|
|
|
|
this.rng.arrayShuffle(candidates[neighs]); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 6; i >= 0; i--) { |
|
|
|
|
|
|
|
if (candidates[i].length) { |
|
|
|
|
|
|
|
for (let n of candidates[i]) { |
|
|
|
|
|
|
|
this.placeOrb(n, symbol); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// avoid deadlocking
|
|
|
|
|
|
|
|
if (this.solution.length % 2 === 0) { |
|
|
|
|
|
|
|
// this is the second orb in a pair (solution starts with gold)
|
|
|
|
|
|
|
|
let prevn = this.solution[this.solution.length-1][1]; |
|
|
|
|
|
|
|
if (!this.isAvailable(prevn)) { |
|
|
|
|
|
|
|
this.trace("Avoid deadlock (this is the second of a pair)"); |
|
|
|
|
|
|
|
this.removeOrb(n); |
|
|
|
|
|
|
|
continue; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.solution.push([symbol, n]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (this.toPlace.length) { |
|
|
|
|
|
|
|
if (this.placeOne()) { |
|
|
|
|
|
|
|
return true; |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
this.trace("Undo and continue"); |
|
|
|
|
|
|
|
this.removeOrb(n); |
|
|
|
|
|
|
|
this.solution.pop(); |
|
|
|
|
|
|
|
// and continue the iteration
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
return true; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// give it back
|
|
|
|
|
|
|
|
this.toPlace.push(symbol); |
|
|
|
|
|
|
|
return false; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* Start */ |
|
|
|
/* Start */ |
|
|
|
|
|
|
|
|
|
|
|
window.game = new Game(); |
|
|
|
window.game = new Game(); |
|
|
|