|
|
@ -59,10 +59,12 @@ class Board { |
|
|
|
// Orb grid
|
|
|
|
// Orb grid
|
|
|
|
this.grid = []; |
|
|
|
this.grid = []; |
|
|
|
this.tiles = []; |
|
|
|
this.tiles = []; |
|
|
|
|
|
|
|
this.indexXyLookup = []; |
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i <= 120; i++) { |
|
|
|
for (let i = 0; i <= 120; i++) { |
|
|
|
this.grid[i] = null; |
|
|
|
this.grid[i] = null; |
|
|
|
this.tiles[i] = null; |
|
|
|
this.tiles[i] = null; |
|
|
|
|
|
|
|
this.indexXyLookup[i] = {x: i % 11, y: Math.floor(i / 11)}; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
this.onOrbClick = (index, orb) => { |
|
|
|
this.onOrbClick = (index, orb) => { |
|
|
@ -170,7 +172,7 @@ class Board { |
|
|
|
* @returns {{x: Number, y: Number}} |
|
|
|
* @returns {{x: Number, y: Number}} |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
gridIndexToXy(index) { |
|
|
|
gridIndexToXy(index) { |
|
|
|
return {x: index % 11, y: Math.floor(index / 11)}; |
|
|
|
return this.indexXyLookup[index]; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
@ -843,11 +845,11 @@ class Game { |
|
|
|
placeOrbs(template) { |
|
|
|
placeOrbs(template) { |
|
|
|
this.board.removeAllOrbs(); |
|
|
|
this.board.removeAllOrbs(); |
|
|
|
|
|
|
|
|
|
|
|
let allowed = []; |
|
|
|
let allowedTable = []; |
|
|
|
let outsideTemplate = []; |
|
|
|
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); |
|
|
|
allowedTable.push(allo); |
|
|
|
|
|
|
|
|
|
|
|
let { x, y } = this.board.gridIndexToXy(i); |
|
|
|
let { x, y } = this.board.gridIndexToXy(i); |
|
|
|
if (!allo && !this.isOutside(x, y)) { |
|
|
|
if (!allo && !this.isOutside(x, y)) { |
|
|
@ -866,48 +868,71 @@ class Game { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const place = (n, symbol) => { |
|
|
|
const place = (n, symbol) => { |
|
|
|
if (!allowed[n]) throw Error(`Position ${n} not allowed by template`); |
|
|
|
console.log(`Place ${n} <- ${symbol}`); |
|
|
|
if (this.board.grid[n]) throw Error(`Position ${n} is occupied`); |
|
|
|
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
|
|
|
|
// we use a hack to speed up generation here - SVG is not altered until we have a solution
|
|
|
|
this.board.grid[n] = symbol; |
|
|
|
this.board.grid[n] = symbol; |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const findAvailableIndexWithNeighbours = (count) => { |
|
|
|
const unplace = (n) => { |
|
|
|
|
|
|
|
console.log(`Unplace ${n}`); |
|
|
|
|
|
|
|
this.board.grid[n] = null; |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const findAvailableIndexWithNeighbours = (count, except=null) => { |
|
|
|
let candidates = []; |
|
|
|
let candidates = []; |
|
|
|
for (let i = 0; i < template.length; i++) { |
|
|
|
for (let i = 0; i < template.length; i++) { |
|
|
|
const n = template[i]; |
|
|
|
const n = template[i]; |
|
|
|
|
|
|
|
if (except && except.includes(n)) continue; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!this.board.grid[n]) { |
|
|
|
const neigh = this.getNeighbours(n); |
|
|
|
const neigh = this.getNeighbours(n); |
|
|
|
// console.log(neigh);
|
|
|
|
if (neigh.neighbours === count && neigh.freeSequence >= 3) { |
|
|
|
if (!this.board.grid[n] && neigh.neighbours === count && neigh.freeSequence >= 3) { |
|
|
|
|
|
|
|
candidates.push(n); |
|
|
|
candidates.push(n); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (candidates.length) { |
|
|
|
if (candidates.length) { |
|
|
|
return candidates[Math.floor(this.rng.next() * candidates.length)] |
|
|
|
return this.arrayChoose(candidates) |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
return false; |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const findAvailableIndex = () => { |
|
|
|
const findAvailableIndex = (except=null) => { |
|
|
|
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, except); |
|
|
|
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.
|
|
|
|
// this corrupts the template, but makes the likelihood of quickly finding a valid solution much higher.
|
|
|
|
if (template.length !== 121) { |
|
|
|
if (template.length !== 121) { |
|
|
|
console.warn("Adding extra tile to template"); |
|
|
|
// Prefer tile with more neighbours to make the game harder
|
|
|
|
while (1) { |
|
|
|
let candidates = []; |
|
|
|
let toAdd = outsideTemplate[this.rng.nextInt(outsideTemplate.length)]; |
|
|
|
outsideTemplate.forEach((n) => { |
|
|
|
if (allowed.includes(toAdd)) continue; |
|
|
|
if (!allowedTable[n] && this.isAvailable(n)) { |
|
|
|
allowed[toAdd] = true; |
|
|
|
const neigh = this.getNeighbours(n); |
|
|
|
template.push(toAdd); |
|
|
|
if (!candidates[neigh.neighbours]) { |
|
|
|
break; |
|
|
|
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); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
console.warn(`Adding extra tile to template: ${toAdd}`); |
|
|
|
|
|
|
|
return toAdd; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
throw Error("Failed to find available tile"); |
|
|
|
throw Error("Failed to find available tile"); |
|
|
@ -919,6 +944,8 @@ class Game { |
|
|
|
|
|
|
|
|
|
|
|
let solution = []; |
|
|
|
let solution = []; |
|
|
|
while (toPlace.length > 0) { |
|
|
|
while (toPlace.length > 0) { |
|
|
|
|
|
|
|
console.log('placing a pair.'); |
|
|
|
|
|
|
|
|
|
|
|
let symbol1 = toPlace.pop(); |
|
|
|
let symbol1 = toPlace.pop(); |
|
|
|
let index1 = findAvailableIndex(); |
|
|
|
let index1 = findAvailableIndex(); |
|
|
|
place(index1, symbol1); |
|
|
|
place(index1, symbol1); |
|
|
@ -928,25 +955,55 @@ class Game { |
|
|
|
place(index2, symbol2); |
|
|
|
place(index2, symbol2); |
|
|
|
|
|
|
|
|
|
|
|
if (!this.isAvailable(index1)) { |
|
|
|
if (!this.isAvailable(index1)) { |
|
|
|
throw new Error("Solution contains a deadlock."); |
|
|
|
console.warn(`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}`); |
|
|
|
|
|
|
|
let index = findAvailableIndex(except); |
|
|
|
|
|
|
|
// console.log(`try ${index} instead of ${index2}`);
|
|
|
|
|
|
|
|
place(index, symbol2); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (this.isAvailable(index1)) { |
|
|
|
|
|
|
|
suc = true; |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
unplace(index); |
|
|
|
|
|
|
|
except.push(index); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
let p; |
|
|
|
if (!suc) { |
|
|
|
p = this.board.gridIndexToXy(index1); |
|
|
|
throw new Error("Solution contains a deadlock."); |
|
|
|
solution.push([symbol1, `${p.x}×${p.y}`, index1]); |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
p = this.board.gridIndexToXy(index1); |
|
|
|
solution.push([symbol1, index1]); |
|
|
|
solution.push([symbol1, `${p.x}×${p.y}`, index1]); |
|
|
|
solution.push([symbol2, index2]); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
solution.reverse(); |
|
|
|
solution.reverse(); |
|
|
|
|
|
|
|
|
|
|
|
console.info("Found a valid board!"); |
|
|
|
console.info("Found a valid board!"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
solution.forEach((a) => { |
|
|
|
|
|
|
|
let p = this.board.gridIndexToXy(a[1]); |
|
|
|
|
|
|
|
a[1] = `${p.x} × ${p.y}`; |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
console.log('Solution: ', solution); |
|
|
|
console.log('Solution: ', solution); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
shuffleArray(a) { |
|
|
|
/** |
|
|
|
|
|
|
|
* Shuffle an array. |
|
|
|
|
|
|
|
* The array is shuffled in place. |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @return the array |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
arrayShuffle(a) { |
|
|
|
let j, x, i; |
|
|
|
let j, x, i; |
|
|
|
for (i = a.length - 1; i > 0; i--) { |
|
|
|
for (i = a.length - 1; i > 0; i--) { |
|
|
|
j = this.rng.nextInt(i); |
|
|
|
j = this.rng.nextInt(i); |
|
|
@ -957,6 +1014,32 @@ class Game { |
|
|
|
return a; |
|
|
|
return a; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Count orbs in the game board |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @return {number} |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
countOrbs() { |
|
|
|
|
|
|
|
let n = 0; |
|
|
|
|
|
|
|
// todo use reduce
|
|
|
|
|
|
|
|
this.board.grid.forEach((x) => { |
|
|
|
|
|
|
|
if (x !== null) { |
|
|
|
|
|
|
|
n++; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
return n; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Choose a random emember of an array |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @param array |
|
|
|
|
|
|
|
* @return {number} |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
arrayChoose(array) { |
|
|
|
|
|
|
|
return array[Math.floor(this.rng.next() * array.length)]; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
buildPlacementList() { |
|
|
|
buildPlacementList() { |
|
|
|
let toPlace = [ |
|
|
|
let toPlace = [ |
|
|
|
['air', 'air'], |
|
|
|
['air', 'air'], |
|
|
@ -1008,8 +1091,7 @@ class Game { |
|
|
|
]); |
|
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
// shuffle the pairs that have random order (i.e. not metals)
|
|
|
|
// shuffle the pairs that have random order (i.e. not metals)
|
|
|
|
this.shuffleArray(toPlace); |
|
|
|
this.arrayShuffle(toPlace); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// the order here is actually significant, so let's pay attention...
|
|
|
|
// the order here is actually significant, so let's pay attention...
|
|
|
|
const metals = [ |
|
|
|
const metals = [ |
|
|
@ -1061,13 +1143,19 @@ class Game { |
|
|
|
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]) { |
|
|
|
const ava = this.isAvailableAdvanced(n); |
|
|
|
const ava = this.isAvailableAtPlaytime(n); |
|
|
|
this.board.grid[n].node.classList.toggle('disabled', !ava); |
|
|
|
this.board.grid[n].node.classList.toggle('disabled', !ava); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
isAvailableAdvanced(n) { |
|
|
|
/** |
|
|
|
|
|
|
|
* Check if a tile is available at play-time (checking unlocked metals) |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @param n |
|
|
|
|
|
|
|
* @return {boolean} |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
isAvailableAtPlaytime(n) { |
|
|
|
let ava = this.isAvailable(n); |
|
|
|
let ava = this.isAvailable(n); |
|
|
|
|
|
|
|
|
|
|
|
const sym = this.board.grid[n].symbol; |
|
|
|
const sym = this.board.grid[n].symbol; |
|
|
@ -1086,32 +1174,33 @@ class Game { |
|
|
|
* @param orb |
|
|
|
* @param orb |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
ingameBoardClick(n, orb) { |
|
|
|
ingameBoardClick(n, orb) { |
|
|
|
if (this.isAvailableAdvanced(n)) { |
|
|
|
if (!this.isAvailableAtPlaytime(n)) return; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let wantRefresh = false; |
|
|
|
|
|
|
|
|
|
|
|
if (orb.symbol === 'gold') { |
|
|
|
if (orb.symbol === 'gold') { |
|
|
|
|
|
|
|
// gold has no pairing
|
|
|
|
this.board.removeOrbByIndex(n); |
|
|
|
this.board.removeOrbByIndex(n); |
|
|
|
this.selectedOrb = null; |
|
|
|
this.selectedOrb = null; |
|
|
|
this.updateOrbDisabledStatus(); |
|
|
|
wantRefresh = true; |
|
|
|
|
|
|
|
|
|
|
|
if (this.countOrbs() === 0) { |
|
|
|
|
|
|
|
console.info("Good work!"); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
return; |
|
|
|
else if (this.selectedOrb === null) { |
|
|
|
} |
|
|
|
// first selection
|
|
|
|
|
|
|
|
|
|
|
|
if (this.selectedOrb === null) { |
|
|
|
|
|
|
|
this.selectedOrb = { n, orb }; |
|
|
|
this.selectedOrb = { n, orb }; |
|
|
|
orb.node.classList.add('selected'); |
|
|
|
orb.node.classList.add('selected'); |
|
|
|
} |
|
|
|
} |
|
|
|
else { |
|
|
|
else { |
|
|
|
if (this.selectedOrb.n === n) { |
|
|
|
if (this.selectedOrb.n === n) { |
|
|
|
|
|
|
|
// orb clicked twice
|
|
|
|
orb.node.classList.remove('selected'); |
|
|
|
orb.node.classList.remove('selected'); |
|
|
|
this.selectedOrb = null; |
|
|
|
this.selectedOrb = null; |
|
|
|
} |
|
|
|
} |
|
|
|
else { |
|
|
|
else { |
|
|
|
|
|
|
|
// second orb in a pair
|
|
|
|
const otherSymbol = this.selectedOrb.orb.symbol; |
|
|
|
const otherSymbol = this.selectedOrb.orb.symbol; |
|
|
|
|
|
|
|
|
|
|
|
if (this.getPairSymbols(orb.symbol).includes(otherSymbol)) { |
|
|
|
if (this.getPairSymbols(orb.symbol).includes(otherSymbol)) { |
|
|
|
// paired
|
|
|
|
// compatible pair clicked
|
|
|
|
this.board.removeOrbByIndex(n); |
|
|
|
this.board.removeOrbByIndex(n); |
|
|
|
this.board.removeOrbByIndex(this.selectedOrb.n); |
|
|
|
this.board.removeOrbByIndex(this.selectedOrb.n); |
|
|
|
|
|
|
|
|
|
|
@ -1120,11 +1209,23 @@ class Game { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
this.selectedOrb = null; |
|
|
|
this.selectedOrb = null; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.updateOrbDisabledStatus(); |
|
|
|
wantRefresh = true; |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
// Bad selection, select it as the first orb.
|
|
|
|
|
|
|
|
this.selectedOrb.orb.node.classList.remove('selected'); |
|
|
|
|
|
|
|
this.selectedOrb = { n, orb }; |
|
|
|
|
|
|
|
orb.node.classList.add('selected'); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (wantRefresh) { |
|
|
|
|
|
|
|
if (this.countOrbs() === 0) { |
|
|
|
|
|
|
|
console.info("Good work!"); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.updateOrbDisabledStatus(); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
@ -1150,13 +1251,13 @@ class Game { |
|
|
|
const template = this.getRandomTemplate(); |
|
|
|
const template = this.getRandomTemplate(); |
|
|
|
for (let j = 0; j < RETRY_IN_TEMPLATE; j++) { |
|
|
|
for (let j = 0; j < RETRY_IN_TEMPLATE; j++) { |
|
|
|
try { |
|
|
|
try { |
|
|
|
this.placeOrbs(template); |
|
|
|
this.placeOrbs(template.slice(0)); // clone
|
|
|
|
suc = true; |
|
|
|
suc = true; |
|
|
|
break; |
|
|
|
break; |
|
|
|
} catch (e) { |
|
|
|
} catch (e) { |
|
|
|
if (alertOnError) alert('welp'); |
|
|
|
if (alertOnError) alert('welp'); |
|
|
|
numretries++; |
|
|
|
numretries++; |
|
|
|
console.warn(e.message); |
|
|
|
console.error(e); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
if (!suc) { |
|
|
|
if (!suc) { |
|
|
@ -1198,17 +1299,6 @@ class Game { |
|
|
|
this.nextMetal = this.board.metalSequence[this.board.metalSequence.indexOf(this.nextMetal) + 1]; |
|
|
|
this.nextMetal = this.board.metalSequence[this.board.metalSequence.indexOf(this.nextMetal) + 1]; |
|
|
|
console.debug(`Next metal unlocked: ${this.nextMetal}`); |
|
|
|
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; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* Start */ |
|
|
|
/* Start */ |
|
|
|