diff --git a/script.js b/script.js index 7a7b137..da8c3dd 100644 --- a/script.js +++ b/script.js @@ -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 { + 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() } }