improve board init performance, prefer cells with more neighbours when augmenting template

master
Ondřej Hruška 5 years ago
parent 670d7711ab
commit 59520ec2be
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 200
      script.js

@ -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 */

Loading…
Cancel
Save