|
|
@ -264,15 +264,15 @@ class Board { |
|
|
|
orb.dataset.symbol = symbol; |
|
|
|
orb.dataset.symbol = symbol; |
|
|
|
this.$orbs.appendChild(orb); |
|
|
|
this.$orbs.appendChild(orb); |
|
|
|
|
|
|
|
|
|
|
|
orb.addEventListener('click', () => { |
|
|
|
|
|
|
|
this.onOrbClick(arrayIndex, orb); |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let object = { |
|
|
|
let object = { |
|
|
|
node: orb, |
|
|
|
node: orb, |
|
|
|
symbol |
|
|
|
symbol |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
orb.addEventListener('click', () => { |
|
|
|
|
|
|
|
this.onOrbClick(arrayIndex, object); |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
this.grid[arrayIndex] = object; |
|
|
|
this.grid[arrayIndex] = object; |
|
|
|
return object; |
|
|
|
return object; |
|
|
|
} |
|
|
|
} |
|
|
@ -518,6 +518,8 @@ class Board { |
|
|
|
'mercury', 'lead', 'tin', 'iron', 'copper', 'silver', 'gold' |
|
|
|
'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', |
|
|
@ -717,7 +719,7 @@ class Game { |
|
|
|
/** |
|
|
|
/** |
|
|
|
* Get a random and randomly flipped template |
|
|
|
* Get a random and randomly flipped template |
|
|
|
* |
|
|
|
* |
|
|
|
* @return Number[] |
|
|
|
* @return Number[] - template indices; this is a clone, free to modify |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
getRandomTemplate() { |
|
|
|
getRandomTemplate() { |
|
|
|
let names = Object.keys(this.layoutTemplates); |
|
|
|
let names = Object.keys(this.layoutTemplates); |
|
|
@ -842,10 +844,16 @@ class Game { |
|
|
|
this.board.removeAllOrbs(); |
|
|
|
this.board.removeAllOrbs(); |
|
|
|
|
|
|
|
|
|
|
|
let allowed = []; |
|
|
|
let allowed = []; |
|
|
|
|
|
|
|
let outsideTemplate = []; |
|
|
|
for (let i = 0; i <= 120; i++) { |
|
|
|
for (let i = 0; i <= 120; i++) { |
|
|
|
let allo = template.includes(i); |
|
|
|
let allo = template.includes(i); |
|
|
|
allowed.push(allo); |
|
|
|
allowed.push(allo); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let { x, y } = this.board.gridIndexToXy(i); |
|
|
|
|
|
|
|
if (!allo && !this.isOutside(x, y)) { |
|
|
|
|
|
|
|
outsideTemplate.push(i); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Highlight pattern shape
|
|
|
|
// Highlight pattern shape
|
|
|
|
|
|
|
|
|
|
|
|
// if (this.board.tiles[i]) {
|
|
|
|
// if (this.board.tiles[i]) {
|
|
|
@ -883,11 +891,25 @@ class Game { |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const findAvailableIndex = () => { |
|
|
|
const findAvailableIndex = () => { |
|
|
|
|
|
|
|
for (let j = 0; j < 2; j++) { |
|
|
|
for (let i = 6; i >= 0; i--) { |
|
|
|
for (let i = 6; i >= 0; i--) { |
|
|
|
const n = findAvailableIndexWithNeighbours(i); |
|
|
|
const n = findAvailableIndexWithNeighbours(i); |
|
|
|
if (n !== false) return n; |
|
|
|
if (n !== false) return n; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// this corrupts the template, but makes the likelihood of quickly finding a valid solution much higher.
|
|
|
|
|
|
|
|
if (template.length !== 121) { |
|
|
|
|
|
|
|
console.warn("Adding extra tile to template"); |
|
|
|
|
|
|
|
while (1) { |
|
|
|
|
|
|
|
let toAdd = outsideTemplate[this.rng.nextInt(outsideTemplate.length)]; |
|
|
|
|
|
|
|
if (allowed.includes(toAdd)) continue; |
|
|
|
|
|
|
|
allowed[toAdd] = true; |
|
|
|
|
|
|
|
template.push(toAdd); |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
throw Error("Failed to find available tile"); |
|
|
|
throw Error("Failed to find available tile"); |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
@ -895,20 +917,33 @@ class Game { |
|
|
|
|
|
|
|
|
|
|
|
const toPlace = this.buildPlacementList(); |
|
|
|
const toPlace = this.buildPlacementList(); |
|
|
|
|
|
|
|
|
|
|
|
// unpack the pairs
|
|
|
|
let solution = []; |
|
|
|
let toPlace_stack = toPlace.reduce((a, c) => { |
|
|
|
while (toPlace.length > 0) { |
|
|
|
a.push(c[0]); |
|
|
|
let symbol1 = toPlace.pop(); |
|
|
|
a.push(c[1]); |
|
|
|
let index1 = findAvailableIndex(); |
|
|
|
return a; |
|
|
|
place(index1, symbol1); |
|
|
|
}, []); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
while (toPlace_stack.length > 0) { |
|
|
|
let symbol2 = toPlace.pop(); |
|
|
|
place(findAvailableIndex(), toPlace_stack.pop()) |
|
|
|
let index2 = findAvailableIndex(); |
|
|
|
|
|
|
|
place(index2, symbol2); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!this.isAvailable(index1)) { |
|
|
|
|
|
|
|
throw new Error("Solution contains a deadlock."); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let p; |
|
|
|
|
|
|
|
p = this.board.gridIndexToXy(index1); |
|
|
|
|
|
|
|
solution.push([symbol1, `${p.x}×${p.y}`, index1]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
p = this.board.gridIndexToXy(index1); |
|
|
|
|
|
|
|
solution.push([symbol1, `${p.x}×${p.y}`, index1]); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
solution.reverse(); |
|
|
|
|
|
|
|
|
|
|
|
console.info("Found a valid board!"); |
|
|
|
console.info("Found a valid board!"); |
|
|
|
|
|
|
|
|
|
|
|
console.log('Solution: ', toPlace); |
|
|
|
console.log('Solution: ', solution); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
shuffleArray(a) { |
|
|
|
shuffleArray(a) { |
|
|
@ -1000,9 +1035,16 @@ class Game { |
|
|
|
toPlace.splice(mPos[i] + i, 0, pair); |
|
|
|
toPlace.splice(mPos[i] + i, 0, pair); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
return toPlace; |
|
|
|
return toPlace.reduce((a, c) => { |
|
|
|
|
|
|
|
a.push(c[0]); |
|
|
|
|
|
|
|
a.push(c[1]); |
|
|
|
|
|
|
|
return a; |
|
|
|
|
|
|
|
}, []); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* 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 <= 120; n++) { |
|
|
|
if (this.board.grid[n] !== null) { |
|
|
|
if (this.board.grid[n] !== null) { |
|
|
@ -1013,10 +1055,74 @@ class Game { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Update orb availability status (includes effects) |
|
|
|
|
|
|
|
*/ |
|
|
|
updateOrbDisabledStatus() { |
|
|
|
updateOrbDisabledStatus() { |
|
|
|
for (let n = 0; n <= 120; n++) { |
|
|
|
for (let n = 0; n <= 120; n++) { |
|
|
|
if (this.board.grid[n]) { |
|
|
|
if (this.board.grid[n]) { |
|
|
|
this.board.grid[n].node.classList.toggle('disabled', !this.isAvailable(n)); |
|
|
|
const ava = this.isAvailableAdvanced(n); |
|
|
|
|
|
|
|
this.board.grid[n].node.classList.toggle('disabled', !ava); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
isAvailableAdvanced(n) { |
|
|
|
|
|
|
|
let ava = this.isAvailable(n); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const sym = this.board.grid[n].symbol; |
|
|
|
|
|
|
|
if (this.board.metalSequence.includes(sym)) { |
|
|
|
|
|
|
|
if (sym !== this.nextMetal) { |
|
|
|
|
|
|
|
ava = false; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return ava; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Handle orb click |
|
|
|
|
|
|
|
* @param n |
|
|
|
|
|
|
|
* @param orb |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
ingameBoardClick(n, orb) { |
|
|
|
|
|
|
|
if (this.isAvailableAdvanced(n)) { |
|
|
|
|
|
|
|
if (orb.symbol === 'gold') { |
|
|
|
|
|
|
|
this.board.removeOrbByIndex(n); |
|
|
|
|
|
|
|
this.selectedOrb = null; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (this.countOrbs() === 0) { |
|
|
|
|
|
|
|
console.info("Good work!"); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (this.selectedOrb === null) { |
|
|
|
|
|
|
|
this.selectedOrb = { n, orb }; |
|
|
|
|
|
|
|
orb.node.classList.add('selected'); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
else { |
|
|
|
|
|
|
|
if (this.selectedOrb.n === n) { |
|
|
|
|
|
|
|
orb.node.classList.remove('selected'); |
|
|
|
|
|
|
|
this.selectedOrb = null; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
else { |
|
|
|
|
|
|
|
const otherSymbol = this.selectedOrb.orb.symbol; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (this.getPairSymbols(orb.symbol).includes(otherSymbol)) { |
|
|
|
|
|
|
|
// paired
|
|
|
|
|
|
|
|
this.board.removeOrbByIndex(n); |
|
|
|
|
|
|
|
this.board.removeOrbByIndex(this.selectedOrb.n); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if ([orb.symbol, otherSymbol].includes(this.nextMetal)) { |
|
|
|
|
|
|
|
this.advanceMetal(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.selectedOrb = null; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.updateOrbDisabledStatus(); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
@ -1026,8 +1132,13 @@ class Game { |
|
|
|
// console.log(n, this.board.gridIndexToXy(n));
|
|
|
|
// console.log(n, this.board.gridIndexToXy(n));
|
|
|
|
// };
|
|
|
|
// };
|
|
|
|
|
|
|
|
|
|
|
|
const RETRY_IN_TEMPLATE = 100; |
|
|
|
this.selectedOrb = null; |
|
|
|
const RETRY_NEW_TEMPLATE = 15; |
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
// retry loop, should not be needed if everything is correct
|
|
|
|
let suc = false; |
|
|
|
let suc = false; |
|
|
@ -1052,19 +1163,50 @@ class Game { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (!suc) { |
|
|
|
this.nextMetal = 'lead'; |
|
|
|
// show the failed attempt
|
|
|
|
|
|
|
|
this.renderPreparedBoard(); |
|
|
|
this.renderPreparedBoard(); |
|
|
|
this.updateOrbDisabledStatus(); |
|
|
|
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 ${numretries} retries.`); |
|
|
|
return; |
|
|
|
|
|
|
|
} else { |
|
|
|
} else { |
|
|
|
console.info(`Found valid solution (with ${numretries} retries)`); |
|
|
|
console.info(`Found valid solution (with ${numretries} retries)`); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
this.renderPreparedBoard(); |
|
|
|
getPairSymbols(first) { |
|
|
|
this.updateOrbDisabledStatus() |
|
|
|
return { |
|
|
|
|
|
|
|
'salt': ['salt', 'air', 'fire', 'water', 'earth'], |
|
|
|
|
|
|
|
'air': ['salt', 'air'], |
|
|
|
|
|
|
|
'fire': ['salt', 'fire'], |
|
|
|
|
|
|
|
'water': ['salt', 'water'], |
|
|
|
|
|
|
|
'earth': ['salt', 'earth'], |
|
|
|
|
|
|
|
'mercury': [this.nextMetal], |
|
|
|
|
|
|
|
'lead': ['mercury'], |
|
|
|
|
|
|
|
'tin': ['mercury'], |
|
|
|
|
|
|
|
'iron': ['mercury'], |
|
|
|
|
|
|
|
'copper': ['mercury'], |
|
|
|
|
|
|
|
'silver': ['mercury'], |
|
|
|
|
|
|
|
'gold': [], |
|
|
|
|
|
|
|
'mors': ['vitae'], |
|
|
|
|
|
|
|
'vitae': ['mors'], |
|
|
|
|
|
|
|
}[first]; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
advanceMetal() { |
|
|
|
|
|
|
|
this.nextMetal = this.board.metalSequence[this.board.metalSequence.indexOf(this.nextMetal) + 1]; |
|
|
|
|
|
|
|
console.debug(`Next metal unlocked: ${this.nextMetal}`); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
countOrbs() { |
|
|
|
|
|
|
|
let n = 0; |
|
|
|
|
|
|
|
// todo use reduce
|
|
|
|
|
|
|
|
this.board.grid.forEach((x) => { |
|
|
|
|
|
|
|
if (x !== null) { |
|
|
|
|
|
|
|
n++; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
return n; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|