diff --git a/script.js b/script.js index 9de35ac..cab1ce5 100644 --- a/script.js +++ b/script.js @@ -264,15 +264,15 @@ class Board { orb.dataset.symbol = symbol; this.$orbs.appendChild(orb); - orb.addEventListener('click', () => { - this.onOrbClick(arrayIndex, orb); - }); - let object = { node: orb, symbol }; + orb.addEventListener('click', () => { + this.onOrbClick(arrayIndex, object); + }); + this.grid[arrayIndex] = object; return object; } @@ -518,6 +518,8 @@ class Board { 'mercury', 'lead', 'tin', 'iron', 'copper', 'silver', 'gold' ]; + this.metalSequence = ['lead', 'tin', 'iron', 'copper', 'silver', 'gold']; + this.orbColors = { salt: '#f2e7b4', air: '#9ee0ff', @@ -717,7 +719,7 @@ class Game { /** * Get a random and randomly flipped template * - * @return Number[] + * @return Number[] - template indices; this is a clone, free to modify */ getRandomTemplate() { let names = Object.keys(this.layoutTemplates); @@ -842,10 +844,16 @@ class Game { this.board.removeAllOrbs(); let allowed = []; + let outsideTemplate = []; for (let i = 0; i <= 120; i++) { let allo = template.includes(i); allowed.push(allo); + let { x, y } = this.board.gridIndexToXy(i); + if (!allo && !this.isOutside(x, y)) { + outsideTemplate.push(i); + } + // Highlight pattern shape // if (this.board.tiles[i]) { @@ -883,9 +891,23 @@ class Game { }; const findAvailableIndex = () => { - for (let i = 6; i >= 0; i--) { - const n = findAvailableIndexWithNeighbours(i); - if (n !== false) return n; + for (let j = 0; j < 2; j++) { + for (let i = 6; i >= 0; i--) { + const n = findAvailableIndexWithNeighbours(i); + 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"); @@ -895,20 +917,33 @@ class Game { const toPlace = this.buildPlacementList(); - // unpack the pairs - let toPlace_stack = toPlace.reduce((a, c) => { - a.push(c[0]); - a.push(c[1]); - return a; - }, []); + let solution = []; + while (toPlace.length > 0) { + let symbol1 = toPlace.pop(); + let index1 = findAvailableIndex(); + place(index1, symbol1); + + let symbol2 = toPlace.pop(); + let index2 = findAvailableIndex(); + place(index2, symbol2); - while (toPlace_stack.length > 0) { - place(findAvailableIndex(), toPlace_stack.pop()) + 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.log('Solution: ', toPlace); + console.log('Solution: ', solution); } shuffleArray(a) { @@ -1000,9 +1035,16 @@ class Game { 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() { for (let n = 0; n <= 120; n++) { if (this.board.grid[n] !== null) { @@ -1013,10 +1055,74 @@ class Game { } } + /** + * Update orb availability status (includes effects) + */ updateOrbDisabledStatus() { for (let n = 0; n <= 120; 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)); // }; - const RETRY_IN_TEMPLATE = 100; - const RETRY_NEW_TEMPLATE = 15; + this.selectedOrb = null; + + 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 let suc = false; @@ -1052,19 +1163,50 @@ class Game { } } - if (!suc) { - // show the failed attempt - this.renderPreparedBoard(); - this.updateOrbDisabledStatus(); + this.nextMetal = 'lead'; + this.renderPreparedBoard(); + this.updateOrbDisabledStatus(); + if (!suc) { alert(`Sorry, could not find a valid board setup after ${numretries} retries.`); - return; } else { console.info(`Found valid solution (with ${numretries} retries)`); } + } - this.renderPreparedBoard(); - this.updateOrbDisabledStatus() + getPairSymbols(first) { + 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; } }