diff --git a/script.js b/script.js index 4a72ab0..03b16ee 100644 --- a/script.js +++ b/script.js @@ -59,10 +59,12 @@ class Board { // Orb grid this.grid = []; this.tiles = []; + this.indexXyLookup = []; for (let i = 0; i <= 120; i++) { this.grid[i] = null; this.tiles[i] = null; + this.indexXyLookup[i] = {x: i % 11, y: Math.floor(i / 11)}; } this.onOrbClick = (index, orb) => { @@ -170,7 +172,7 @@ class Board { * @returns {{x: Number, y: Number}} */ gridIndexToXy(index) { - return {x: index % 11, y: Math.floor(index / 11)}; + return this.indexXyLookup[index]; } /** @@ -843,11 +845,11 @@ class Game { placeOrbs(template) { this.board.removeAllOrbs(); - let allowed = []; + let allowedTable = []; let outsideTemplate = []; for (let i = 0; i <= 120; i++) { let allo = template.includes(i); - allowed.push(allo); + allowedTable.push(allo); let { x, y } = this.board.gridIndexToXy(i); if (!allo && !this.isOutside(x, y)) { @@ -866,47 +868,70 @@ class Game { } const 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`); + console.log(`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 findAvailableIndexWithNeighbours = (count) => { + const unplace = (n) => { + console.log(`Unplace ${n}`); + this.board.grid[n] = null; + }; + + const findAvailableIndexWithNeighbours = (count, except=null) => { let candidates = []; for (let i = 0; i < template.length; i++) { const n = template[i]; - const neigh = this.getNeighbours(n); - // console.log(neigh); - if (!this.board.grid[n] && neigh.neighbours === count && neigh.freeSequence >= 3) { - candidates.push(n); + if (except && except.includes(n)) continue; + + if (!this.board.grid[n]) { + const neigh = this.getNeighbours(n); + if (neigh.neighbours === count && neigh.freeSequence >= 3) { + candidates.push(n); + } } } if (candidates.length) { - return candidates[Math.floor(this.rng.next() * candidates.length)] + return this.arrayChoose(candidates) } else { return false; } }; - const findAvailableIndex = () => { - for (let j = 0; j < 2; j++) { - for (let i = 6; i >= 0; i--) { - const n = findAvailableIndexWithNeighbours(i); - if (n !== false) return n; - } + const findAvailableIndex = (except=null) => { + for (let i = 6; i >= 0; i--) { + const n = findAvailableIndexWithNeighbours(i, 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 !== 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; + // this corrupts the template, but makes the likelihood of quickly finding a valid solution much higher. + if (template.length !== 121) { + // 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); + + console.warn(`Adding extra tile to template: ${toAdd}`); + return toAdd; } } @@ -919,6 +944,8 @@ class Game { let solution = []; while (toPlace.length > 0) { + console.log('placing a pair.'); + let symbol1 = toPlace.pop(); let index1 = findAvailableIndex(); place(index1, symbol1); @@ -928,25 +955,55 @@ class Game { place(index2, symbol2); 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; - p = this.board.gridIndexToXy(index1); - solution.push([symbol1, `${p.x}×${p.y}`, index1]); + if (!suc) { + throw new Error("Solution contains a deadlock."); + } + } - p = this.board.gridIndexToXy(index1); - solution.push([symbol1, `${p.x}×${p.y}`, index1]); + solution.push([symbol1, index1]); + solution.push([symbol2, index2]); } solution.reverse(); 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); } - shuffleArray(a) { + /** + * 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); @@ -957,6 +1014,32 @@ class Game { 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() { let toPlace = [ ['air', 'air'], @@ -1008,8 +1091,7 @@ class Game { ]); // 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... const metals = [ @@ -1061,13 +1143,19 @@ class Game { updateOrbDisabledStatus() { for (let n = 0; n <= 120; 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); } } } - 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); const sym = this.board.grid[n].symbol; @@ -1086,46 +1174,59 @@ class Game { * @param orb */ ingameBoardClick(n, orb) { - if (this.isAvailableAdvanced(n)) { - if (orb.symbol === 'gold') { - this.board.removeOrbByIndex(n); - this.selectedOrb = null; - this.updateOrbDisabledStatus(); + if (!this.isAvailableAtPlaytime(n)) return; - if (this.countOrbs() === 0) { - console.info("Good work!"); - } - return; - } + let wantRefresh = false; - if (this.selectedOrb === null) { - this.selectedOrb = { n, orb }; - orb.node.classList.add('selected'); + if (orb.symbol === 'gold') { + // gold has no pairing + this.board.removeOrbByIndex(n); + this.selectedOrb = null; + wantRefresh = true; + } + else if (this.selectedOrb === null) { + // first selection + this.selectedOrb = { n, orb }; + orb.node.classList.add('selected'); + } + else { + if (this.selectedOrb.n === n) { + // orb clicked twice + orb.node.classList.remove('selected'); + this.selectedOrb = null; } else { - if (this.selectedOrb.n === n) { - orb.node.classList.remove('selected'); - this.selectedOrb = null; - } - else { - const otherSymbol = this.selectedOrb.orb.symbol; + // second orb in a pair + 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 (this.getPairSymbols(orb.symbol).includes(otherSymbol)) { + // compatible pair clicked + this.board.removeOrbByIndex(n); + this.board.removeOrbByIndex(this.selectedOrb.n); - if ([orb.symbol, otherSymbol].includes(this.nextMetal)) { - this.advanceMetal(); - } - - this.selectedOrb = null; + if ([orb.symbol, otherSymbol].includes(this.nextMetal)) { + this.advanceMetal(); } - this.updateOrbDisabledStatus(); + this.selectedOrb = null; + + 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(); + } } newGame() { @@ -1150,13 +1251,13 @@ class Game { const template = this.getRandomTemplate(); for (let j = 0; j < RETRY_IN_TEMPLATE; j++) { try { - this.placeOrbs(template); + this.placeOrbs(template.slice(0)); // clone suc = true; break; } catch (e) { if (alertOnError) alert('welp'); numretries++; - console.warn(e.message); + console.error(e); } } if (!suc) { @@ -1198,17 +1299,6 @@ class Game { 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; - } } /* Start */