solutions now really solvable

master
Ondřej Hruška 5 years ago
parent e6e23ca218
commit e8b538c8e8
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 370
      script.js

@ -57,11 +57,18 @@ class Board {
this.SCREEN_PAD = 20;
// Orb grid
this.grid = {};
this.tiles = {};
this.grid = [];
this.tiles = [];
this.onOrbClick = (index, orb) => {};
this.onTileClick = (index) => {};
for(let i=0; i<120;i++) {
this.grid[i] = null;
this.tiles[i] = null;
}
this.onOrbClick = (index, orb) => {
};
this.onTileClick = (index) => {
};
this.initOrb();
this.initGlyphs();
@ -199,7 +206,7 @@ class Board {
removeOrbByIndex(index, errorIfEmpty = false) {
if (this.grid[index]) {
this.$orbs.removeChild(this.grid[index].node);
delete this.grid[index];
this.grid[index] = null;
} else {
if (errorIfEmpty) {
throw new Error(`Position ${index} is already empty.`);
@ -603,15 +610,29 @@ class Rng {
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
return ((t ^ t >>> 14) >>> 0) / 4294967296;
}
/**
* Get next int, inclusive
*
* @param {Number} max
* @return {Number}
*/
nextInt(max) {
return Math.floor((max + 1) * this.next());
}
}
class Game {
/**
* Init the game
*/
constructor() {
constructor(seed=null) {
this.board = new Board();
this.rng = new Rng(1);
if (seed === null) {
seed = +new Date();
}
this.rng = new Rng(seed);
this.layoutTemplates = {
// templates apparently all have 55 items
@ -767,9 +788,13 @@ class Game {
}
isAvailable(n) {
return this.getNeighbours(n).freeSequence >= 3;
}
getNeighbours(n) {
let {x, y} = this.board.gridIndexToXy(n);
let neighbors = [
let freeSpaces = [
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],
@ -778,49 +803,67 @@ class Game {
this.isOutside(x, y + 1) || !this.board.grid[n + 11],
];
neighbors.push(neighbors[0]);
neighbors.push(neighbors[1]);
let nOccupied = 0;
for (let i = 0; i < 6; i++) {
if (!freeSpaces[i]) {
nOccupied++;
}
}
// if(this.debuggetneigh) console.log(`${x}×${y} #${n}, nocc ${nOccupied} `+JSON.stringify(freeSpaces));
let conseq = 0;
for (let i = 0; i < neighbors.length; i++) {
if (neighbors[i]) {
conseq++;
if (conseq === 3) {
return true;
let freeSequence = 0;
let maxFreeSequence = 0;
for (let i = 0; i < 12; i++) {
if (freeSpaces[i % 6]) {
freeSequence++;
if (freeSequence >= 6) {
maxFreeSequence = freeSequence;
break;
}
if (freeSequence > maxFreeSequence) {
maxFreeSequence = freeSequence;
}
} else {
conseq = 0;
freeSequence = 0;
}
}
return false;
return {neighbours: nOccupied, freeSequence: maxFreeSequence};
}
placeOrbs() {
placeOrbs(template) {
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)) {
let allo = template.includes(i);
allowed.push(allo);
// Highlight pattern shape
if (this.board.tiles[i]) {
this.board.$bg.removeChild(this.board.tiles[i])
if (allo) {
this.board.tiles[i].setAttribute('opacity', 1)
} else {
this.board.tiles[i].setAttribute('opacity', 0.6)
}
}
}
let place = (n, symbol) => {
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`);
this.board.placeOrbByIndex(n, symbol);
};
let findAvailableIndex = () => {
const findAvailableIndexWithNeighbours = (count) => {
let candidates = [];
for (let i = 0; i < template.length; i++) {
let n = template[i];
if (!this.board.grid[n] && this.isAvailable(n)) {
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);
}
}
@ -828,185 +871,178 @@ class Game {
if (candidates.length) {
return candidates[Math.floor(this.rng.next() * candidates.length)]
} else {
throw Error("Failed to find available tile");
return false;
}
};
try {
const findAvailableIndex = () => {
for (let i = 6; i >= 0; i--) {
const n = findAvailableIndexWithNeighbours(i);
if (n !== false) return n;
}
place(60, 'gold');
throw Error("Failed to find available tile");
};
let toPlace = [
// gold is placed always to 60 (5x5)
['salt', 'salt'],
['salt', 'salt'],
['mors', 'vitae'],
['mors', 'vitae'],
['mors', 'vitae'],
['mors', 'vitae'],
['air', 'air'],
['air', 'air'],
['air', 'air'],
['air', 'air'],
['fire', 'fire'],
['fire', 'fire'],
['fire', 'fire'],
['fire', 'fire'],
['water', 'water'],
['water', 'water'],
['water', 'water'],
['water', 'water'],
['earth', 'earth'],
['earth', 'earth'],
['earth', 'earth'],
['earth', 'earth'],
['lead', 'mercury'],
['tin', 'mercury'],
['iron', 'mercury'],
['copper', 'mercury'],
['silver', 'mercury'],
];
place(60, 'gold');
let shuffle = (a) => {
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;
};
const toPlace = this.buildPlacementList();
shuffle(toPlace);
toPlace = toPlace.reduce((a,c) => {
// unpack the pairs
let toPlace_stack = 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++;
// }
while (toPlace_stack.length > 0) {
place(findAvailableIndex(), toPlace_stack.pop())
}
console.log('>>> Attempt backtrack of depth ' + depth);
console.info("Found a valid board!");
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('Solution: ', toPlace);
}
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");
updateOrbDisabledStatus() {
for (let n = 0; n < 120; n++) {
if (this.board.grid[n]) {
this.board.grid[n].node.classList.toggle('disabled', !this.isAvailable(n));
}
}
}
newGame() {
// this.board.onTileClick = (n) => {
// console.log(n, this.board.gridIndexToXy(n));
// };
const RETRY_IN_TEMPLATE = 100;
const RETRY_NEW_TEMPLATE = 15;
// retry loop, should not be needed if everything is correct
let suc = false;
let numretries = 0;
const alertOnError = false;
for (let i = 0; i < RETRY_NEW_TEMPLATE && !suc; i++) {
console.log('RNG seed is: ' + this.rng.state);
const template = this.getRandomTemplate();
for (let j = 0; j < RETRY_IN_TEMPLATE; j++) {
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;
this.placeOrbs(template);
suc = true;
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;
if (alertOnError) alert('welp');
numretries++;
console.warn(e.message);
}
}
if (!suc) {
console.warn("Exhausted all retries for the template, getting a new one");
}
}
console.log('end of backtrack loop, toPlace=', toPlace);
console.log('history=',history);
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.updateOrbDisabledStatus()
}
if (toPlace.length > 0) {
throw Error("Failed to place some symbols: " + JSON.stringify(toPlace));
shuffleArray(a) {
let j, x, i;
for (i = a.length - 1; i > 0; i--) {
j = this.rng.nextInt(i);
x = a[i];
a[i] = a[j];
a[j] = x;
}
return a;
}
//place(findAvailableIndex(), metals.pop());
buildPlacementList() {
let toPlace = [
['air', 'air'],
['air', 'air'],
['air', 'air'],
['air', 'air'],
} catch (e) {
console.error(e)
['fire', 'fire'],
['fire', 'fire'],
['fire', 'fire'],
['fire', 'fire'],
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));
['water', 'water'],
['water', 'water'],
['water', 'water'],
['water', 'water'],
['earth', 'earth'],
['earth', 'earth'],
['earth', 'earth'],
['earth', 'earth'],
];
let newSaltedPairs = [];
const nsalted = this.rng.nextInt(2);
for (let i = 0; i < nsalted; i++) {
while (true) {
const n = this.rng.nextInt(toPlace.length-1);
if (toPlace[n][1] !== 'salt') {
// console.log(`Pairing ${toPlace[n][1]} with salt.`);
newSaltedPairs.push([toPlace[n][1], 'salt']);
toPlace[n][1] = 'salt';
break;
}
}
}
toPlace = toPlace.concat(newSaltedPairs);
// if we have some salt pairs left
for (let i = 0; i < 2 - nsalted; i++) {
toPlace.push(['salt', 'salt']);
}
newGame() {
//
// these are always paired like this, and don't support salt
toPlace = toPlace.concat([
['mors', 'vitae'],
['mors', 'vitae'],
['mors', 'vitae'],
['mors', 'vitae'],
]);
this.board.onTileClick = (n) => {
console.log(n, this.board.gridIndexToXy(n));
};
// shuffle the pairs that have random order (i.e. not metals)
this.shuffleArray(toPlace);
// the order here is actually significant, so let's pay attention...
const metals = [
['lead', 'mercury'],
['tin', 'mercury'],
['iron', 'mercury'],
['copper', 'mercury'],
['silver', 'mercury'],
];
let mPos = [];
for (let i = 0; i < metals.length; i++) {
let x;
// find a unique position
do {
x = this.rng.nextInt(toPlace.length + i);
} while (mPos.includes(x));
mPos.push(x)
}
mPos.sort((a, b) => a - b);
// console.log('Metal positions ', mPos);
// inject them into the array
metals.forEach((pair, i) => {
toPlace.splice(mPos[i] + i, 0, pair);
});
this.placeOrbs()
return toPlace;
}
}

Loading…
Cancel
Save