|
|
|
@ -194,11 +194,16 @@ class Board { |
|
|
|
|
* Remove orb by array index |
|
|
|
|
* |
|
|
|
|
* @param {Number} index |
|
|
|
|
* @param {Boolean} errorIfEmpty |
|
|
|
|
*/ |
|
|
|
|
removeOrbByIndex(index) { |
|
|
|
|
removeOrbByIndex(index, errorIfEmpty = false) { |
|
|
|
|
if (this.grid[index]) { |
|
|
|
|
this.$orbs.removeChild(this.grid[index].node); |
|
|
|
|
delete this.grid[index]; |
|
|
|
|
} else { |
|
|
|
|
if (errorIfEmpty) { |
|
|
|
|
throw new Error(`Position ${index} is already empty.`); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -573,7 +578,7 @@ class Rng { |
|
|
|
|
*/ |
|
|
|
|
constructor(seed=null) { |
|
|
|
|
if (seed === null) { |
|
|
|
|
seed = Math.random(); |
|
|
|
|
seed = +new Date; |
|
|
|
|
} |
|
|
|
|
this.seed(seed); |
|
|
|
|
} |
|
|
|
@ -606,10 +611,12 @@ class Game { |
|
|
|
|
*/ |
|
|
|
|
constructor() { |
|
|
|
|
this.board = new Board(); |
|
|
|
|
this.rng = new Rng(); |
|
|
|
|
this.rng = new Rng(1); |
|
|
|
|
|
|
|
|
|
this.layoutTemplates = { |
|
|
|
|
'wheel': [0,1,2,3,4,5,11,12,13,14,15,16,17,22,23,24,27,28,29,33,34,36,38,40,41,44,45,48,49,52,53,55,56,57,58,59,60,61,62,63,64,65,67,68,71,72,75,76,79,80,82,84,86,87,91,92,93,96,97,98,103,104,105,106,107,108,109,115,116,117,118,119,120], |
|
|
|
|
// templates apparently all have 55 items
|
|
|
|
|
|
|
|
|
|
//'wheel': [0,1,2,3,4,5,11,12,13,14,15,16,17,22,23,24,27,28,29,33,34,36,38,40,41,44,45,48,49,52,53,55,56,57,58,59,60,61,62,63,64,65,67,68,71,72,75,76,79,80,82,84,86,87,91,92,93,96,97,98,103,104,105,106,107,108,109,115,116,117,118,119,120],
|
|
|
|
|
'beyblade': [0,1,2,3,4,5,12,14,15,23,26,34,35,37,38,39,40,46,47,48,50,51,52,55,58,60,61,64,65,67,68,69,70,71,73,76,79,80,83,84,85,86,87,91,94,95,97,98,103,104,105,106,109,115,120], |
|
|
|
|
'tulip': [4,14,15,16,23,24,25,26,27,28,34,35,36,37,39,40,45,46,47,48,49,50,51,52,56,57,59,60,61,62,63,67,68,69,70,71,72,73,74,80,81,82,83,85,86,93,94,95,96,97,105,106,107,108,109], |
|
|
|
|
'alien': [3,4,14,15,16,22,25,26,27,28,34,35,36,37,38,39,40,41,45,46,48,49,51,56,57,58,59,60,61,62,67,68,69,70,71,72,73,74,79,80,81,82,84,85,86,94,95,96,97,98,106,107,108,109,117], |
|
|
|
@ -683,17 +690,22 @@ class Game { |
|
|
|
|
/** |
|
|
|
|
* Get a random and randomly flipped template |
|
|
|
|
* |
|
|
|
|
* @return {{template: number[], name: string, flipped: boolean}} |
|
|
|
|
* @return Number[] |
|
|
|
|
*/ |
|
|
|
|
getRandomTemplate() { |
|
|
|
|
let names = Object.keys(this.layoutTemplates); |
|
|
|
|
let name = names[Math.floor(this.rng.next() * names.length)]; |
|
|
|
|
let flipped = this.rng.next() > 0.5; |
|
|
|
|
return { |
|
|
|
|
template: this.getTemplate(name, flipped), |
|
|
|
|
name, |
|
|
|
|
flipped |
|
|
|
|
}; |
|
|
|
|
let tpl = this.getTemplate(name, flipped); |
|
|
|
|
|
|
|
|
|
// 60 (center) must be included to place gold
|
|
|
|
|
if (!tpl.includes(60)) { |
|
|
|
|
throw Error(`Template "${name}", flip=${+flipped}, lacks 60.`); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
console.info(`Selected board layout template "${name}", flipped=${+flipped}`); |
|
|
|
|
|
|
|
|
|
return tpl; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
@ -701,20 +713,23 @@ class Game { |
|
|
|
|
* |
|
|
|
|
* The array is modified in place! |
|
|
|
|
* |
|
|
|
|
* @param tpl |
|
|
|
|
* @returns {Uint16Array} |
|
|
|
|
* @param {Number[]} tpl |
|
|
|
|
* @returns {Number[]} |
|
|
|
|
*/ |
|
|
|
|
flipTemplate(tpl) { |
|
|
|
|
return tpl.sort((a,b) => a-b).map((n) => { |
|
|
|
|
let { x, y } = this.board.gridIndexToXy(n); |
|
|
|
|
return this.board.xyToGridIndex(5 + y - x, y); |
|
|
|
|
}); |
|
|
|
|
return tpl |
|
|
|
|
.sort((a, b) => a - b) |
|
|
|
|
.map((n) => { |
|
|
|
|
let { x, y } = this.board.gridIndexToXy(n); |
|
|
|
|
return this.board.xyToGridIndex(5 + y - x, y); |
|
|
|
|
}) |
|
|
|
|
.sort((a, b) => a - b); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Print array of all occupied orbs as a layout template |
|
|
|
|
* |
|
|
|
|
* @returns {number[]} |
|
|
|
|
* @returns {Number[]} |
|
|
|
|
*/ |
|
|
|
|
toTemplate() { |
|
|
|
|
return Object.keys(game.board.grid) |
|
|
|
@ -742,8 +757,256 @@ class Game { |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
isOutside(x, y) { |
|
|
|
|
return x < 0 |
|
|
|
|
|| x > 10 |
|
|
|
|
|| y < 0 |
|
|
|
|
|| y > 10 |
|
|
|
|
|| (y <= 5 && x > 5 + y) |
|
|
|
|
|| (y > 5 && x < y - 5); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
isAvailable(n) { |
|
|
|
|
let {x, y} = this.board.gridIndexToXy(n); |
|
|
|
|
|
|
|
|
|
let neighbors = [ |
|
|
|
|
this.isOutside(x - 1, y) || !this.board.grid[n - 1], |
|
|
|
|
this.isOutside(x - 1, y - 1) || !this.board.grid[n - 12], |
|
|
|
|
this.isOutside(x, y - 1) || !this.board.grid[n - 11], |
|
|
|
|
this.isOutside(x + 1, y) || !this.board.grid[n + 1], |
|
|
|
|
this.isOutside(x + 1, y + 1) || !this.board.grid[n + 12], |
|
|
|
|
this.isOutside(x, y + 1) || !this.board.grid[n + 11], |
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
neighbors.push(neighbors[0]); |
|
|
|
|
neighbors.push(neighbors[1]); |
|
|
|
|
|
|
|
|
|
let conseq = 0; |
|
|
|
|
for (let i = 0; i < neighbors.length; i++) { |
|
|
|
|
if (neighbors[i]) { |
|
|
|
|
conseq++; |
|
|
|
|
if (conseq === 3) { |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
conseq = 0; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
placeOrbs() { |
|
|
|
|
this.board.removeAllOrbs(); |
|
|
|
|
|
|
|
|
|
let template = this.getRandomTemplate(); |
|
|
|
|
let allowed = []; |
|
|
|
|
for(let i = 0; i <= 120; i++) { |
|
|
|
|
allowed.push(template.includes(i)); |
|
|
|
|
if (!template.includes(i)) { |
|
|
|
|
if (this.board.tiles[i]) { |
|
|
|
|
this.board.$bg.removeChild(this.board.tiles[i]) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let place = (n, symbol) => { |
|
|
|
|
if (!allowed[n]) throw Error(`Position ${n} not allowed by template`); |
|
|
|
|
if (this.board.grid[n]) throw Error(`Position ${n} is occupied`); |
|
|
|
|
this.board.placeOrbByIndex(n, symbol); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
let findAvailableIndex = () => { |
|
|
|
|
let candidates = []; |
|
|
|
|
for(let i=0; i<template.length;i++) { |
|
|
|
|
let n = template[i]; |
|
|
|
|
if (!this.board.grid[n] && this.isAvailable(n)) { |
|
|
|
|
candidates.push(n); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (candidates.length) { |
|
|
|
|
return candidates[Math.floor(this.rng.next()*candidates.length)] |
|
|
|
|
} else { |
|
|
|
|
throw Error("Failed to find available tile"); |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
|
|
place(60, 'gold'); |
|
|
|
|
|
|
|
|
|
let toPlace = [ |
|
|
|
|
// gold is placed always to 60 (5x5)
|
|
|
|
|
['salt', 'salt'], |
|
|
|
|
['salt', 'salt'], |
|
|
|
|
['mors', 'vitae'], |
|
|
|
|
['mors', 'vitae'], |
|
|
|
|
['mors', 'vitae'], |
|
|
|
|
['mors', 'vitae'], |
|
|
|
|
['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'], |
|
|
|
|
['lead', 'mercury'], |
|
|
|
|
['tin', 'mercury'], |
|
|
|
|
['iron', 'mercury'], |
|
|
|
|
['copper', 'mercury'], |
|
|
|
|
['silver', 'mercury'], |
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
let shuffle = (a) => { |
|
|
|
|
let j, x, i; |
|
|
|
|
for (i = a.length - 1; i > 0; i--) { |
|
|
|
|
j = Math.floor(this.rng.next() * (i + 1)); |
|
|
|
|
x = a[i]; |
|
|
|
|
a[i] = a[j]; |
|
|
|
|
a[j] = x; |
|
|
|
|
} |
|
|
|
|
return a; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
shuffle(toPlace); |
|
|
|
|
toPlace = toPlace.reduce((a,c) => { |
|
|
|
|
a.push(c[0]); |
|
|
|
|
a.push(c[1]); |
|
|
|
|
return a; |
|
|
|
|
}, []); |
|
|
|
|
|
|
|
|
|
console.log(toPlace); |
|
|
|
|
|
|
|
|
|
let history = []; |
|
|
|
|
|
|
|
|
|
const maxiter = 1000; |
|
|
|
|
for (let itern = 0; itern < maxiter && toPlace.length > 0; itern++) { |
|
|
|
|
console.log('--- main iteration -----------'); |
|
|
|
|
|
|
|
|
|
let symbol = toPlace.pop(); |
|
|
|
|
console.log('take ' + symbol); |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
console.log('?place ' + symbol); |
|
|
|
|
let index = findAvailableIndex(); |
|
|
|
|
place(index, symbol); |
|
|
|
|
console.log('placed ' + symbol + ' to ' + index); |
|
|
|
|
history.push({symbol, index}); |
|
|
|
|
} catch (e) { |
|
|
|
|
toPlace.push(symbol); |
|
|
|
|
console.log('*return unplaced ' + symbol); |
|
|
|
|
|
|
|
|
|
console.log('BACKTRACK----------- failed to place ' + symbol); |
|
|
|
|
console.log('toPlace=',toPlace); |
|
|
|
|
console.log('history=',history); |
|
|
|
|
|
|
|
|
|
let depth = Math.min(1, history.length); |
|
|
|
|
let backtracking = true; |
|
|
|
|
|
|
|
|
|
// let retryAtThisLevel=0;
|
|
|
|
|
while (backtracking) { |
|
|
|
|
// if (retryAtThisLevel > 0) {
|
|
|
|
|
// retryAtThisLevel = 0;
|
|
|
|
|
// depth++;
|
|
|
|
|
// console.log(`DEEPER`);
|
|
|
|
|
// } else {
|
|
|
|
|
// retryAtThisLevel++;
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
console.log('>>> Attempt backtrack of depth ' + depth); |
|
|
|
|
|
|
|
|
|
for (let j = 0; j < depth; j++) { |
|
|
|
|
let hist = history.pop(); |
|
|
|
|
this.board.removeOrbByIndex(hist.index, true); |
|
|
|
|
toPlace.push(hist.symbol); |
|
|
|
|
console.log('undo & return (hist) ' + hist.symbol + ' at ' + hist.index) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
console.log(`try getting back ${depth + 1} steps`); |
|
|
|
|
let successes = 0; |
|
|
|
|
for (let j = 0; j <= depth; j++) { |
|
|
|
|
let symbol = toPlace.pop(); |
|
|
|
|
console.log('take ' + symbol); |
|
|
|
|
|
|
|
|
|
itern++; |
|
|
|
|
if (itern > maxiter) { |
|
|
|
|
throw Error("Exceeded max iteration depth"); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
console.log('?place ' + symbol); |
|
|
|
|
let index = findAvailableIndex(); |
|
|
|
|
place(index, symbol); |
|
|
|
|
history.push({symbol, index}); |
|
|
|
|
console.log('placed ' + symbol + ' to ' + index); |
|
|
|
|
|
|
|
|
|
if (j === depth) { |
|
|
|
|
console.log('Backtracking success'); |
|
|
|
|
backtracking = false; |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
successes++; |
|
|
|
|
} catch (e) { |
|
|
|
|
console.log(`backtrack of depth ${depth} fail`); |
|
|
|
|
|
|
|
|
|
toPlace.push(symbol); |
|
|
|
|
console.log('return unplaced ' + symbol); |
|
|
|
|
|
|
|
|
|
// for(let k = 0; k < successes; k++) {
|
|
|
|
|
// let hist = history.pop();
|
|
|
|
|
// this.board.removeOrbByIndex(hist.index, true);
|
|
|
|
|
// toPlace.push(hist.symbol);
|
|
|
|
|
// console.log('undo & return (hist) ' + hist.symbol + ' at ' + hist.index);
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
console.log(`DEEPER`); |
|
|
|
|
depth++; |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
console.log('end of backtrack loop, toPlace=', toPlace); |
|
|
|
|
console.log('history=',history); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (toPlace.length > 0) { |
|
|
|
|
throw Error("Failed to place some symbols: " + JSON.stringify(toPlace)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
//place(findAvailableIndex(), metals.pop());
|
|
|
|
|
|
|
|
|
|
} catch (e) { |
|
|
|
|
console.error(e) |
|
|
|
|
|
|
|
|
|
for(let i=0;i<120;i++) { |
|
|
|
|
if (!this.board.grid[i] && allowed[i]) { |
|
|
|
|
console.log('Free cell '+i+' -> isAvail? '+this.isAvailable(i), this.board.gridIndexToXy(i)); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
newGame() { |
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
this.board.onTileClick = (n) => { |
|
|
|
|
console.log(n, this.board.gridIndexToXy(n)); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
this.placeOrbs() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|