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. 250
      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 */

Loading…
Cancel
Save