Join all one step deep AIs into one with linear weight function.
"use strict";
var ai = {};
ai.dirs = ["up", "right", "down", "left"];
ai.canDirs = ["canUp", "canRight", "canDown", "canLeft"];
/* Create empty 'to' if argument missing. */
ai.copyObj = function(from, to) {
if (to == null || typeof to !== "object")
to = {};
if (from == null || typeof obj !== "object")
return to;
for (var attr in cfg) {
if (from.hasOwnProperty(attr))
to[attr] = from[attr];
}
return to;
}
// Each strategy is a function that except current board position as 2d array and context from
// previous call to share state/precomputed values between calls.
////////////////////////////////////////////////////////////////
// Blind random AI.
////////////////////////////////////////////////////////////////
ai.blindRandom = function(brdEngine) {
this.brdEngine = brdEngine;
}
ai.blindRandom.prototype.analyse = function(brd) {
var origBrd = new this.brdEngine(brd);
while (true) {
var rnd = Math.floor(Math.random()*4);
if (origBrd[ai.canDirs[rnd]]())
return ai.dirs[rnd];
}
}
/* Mark that next board will be unrelated to previous, so any stored precompution can be cleared. */
ai.blindRandom.prototype.cleanup = function() { }
////////////////////////////////////////////////////////////////
// Blind weight random AI.
////////////////////////////////////////////////////////////////
ai.blindWeightRandom = function(brdEngine, cfg) {
this.brdEngine = brdEngine;
this.cfg = this.cfg || {};
var left = ai.blindWeightRandom.fixWeight(this.cfg.left);
var right = ai.blindWeightRandom.fixWeight(this.cfg.right);
var up = ai.blindWeightRandom.fixWeight(this.cfg.up);
var down = ai.blindWeightRandom.fixWeight(this.cfg.down);
var total = left + right + up + down;
this.threshold1 = left/total;
this.threshold2 = (left+down)/total;
this.threshold3 = (left+down+right)/total;
}
ai.blindWeightRandom.fixWeight = function(val) {
val = val && parseFloat(v) || 1;
if (val <= 0)
val = 1;
return val;
}
ai.blindWeightRandom.prototype.analyse = function(brd) {
var origBrd = new this.brdEngine(brd);
while (true) {
var rnd = Math.random();
if (rnd < this.threshold1)
var dir = 0;
else if (rnd < this.threshold2)
var dir = 1;
else if (rnd < this.threshold3)
var dir = 2;
else
var dir = 3;
if (origBrd[ai.canDirs[dir]]())
return ai.dirs[dir];
}
}
/* Mark that next board will be unrelated to previous, so any stored precompution can be cleared. */
ai.blindWeightRandom.prototype.cleanup = function() { }
////////////////////////////////////////////////////////////////
// Blind cycle AI.
////////////////////////////////////////////////////////////////
ai.blindCycle = function(brdEngine, cfg) {
this.brdEngine = brdEngine;
this.cfg = cfg || {};
this.cfg.whilePossible = this.cfg.whilePossible || false;
this.cfg.clockwise = this.cfg.clockwise || false;
}
ai.blindCycle.dirs = ["left", "down", "right", "up"];
ai.blindCycle.canDirs = ["canLeft", "canDown", "canRight", "canUp"];
ai.blindCycle.prototype.nextDir = function(dir) {
if (this.cfg.clockwise)
return (dir + (4-1)) % 4;
else
return (dir + 1) % 4;
}
ai.blindCycle.prototype.analyse = function(brd) {
var origBrd = new this.brdEngine(brd);
this.prevDir = this.prevDir || 0;
if (!this.cfg.whilePossible)
this.prevDir = this.nextDir(this.prevDir);
while (true) {
if (origBrd[ai.blindCycle.canDirs[this.prevDir]]())
return ai.blindCycle.dirs[this.prevDir];
this.prevDir = this.nextDir(this.prevDir);
}
}
/* Mark that next board will be unrelated to previous, so any stored precompution can be cleared. */
ai.blindCycle.prototype.cleanup = function() {
delete this.prevDir;
}
////////////////////////////////////////////////////////////////
// 1 step deep with linear weight function on score, max value,
// bonuses for max value stay at corner or edge and bonuses
// for each free field.
////////////////////////////////////////////////////////////////
ai.oneStepDeep = function(brdEngine, cfg) {
this.brdEngine = brdEngine;
this.cfg = ai.copyObj(ai.oneStepDeep.bestCfg);
ai.copyObj(cfg, this.cfg);
}
ai.oneStepDeep.bestCfg = {scoreCoef: 1, maxValCoef: 0, cornerBonus: 0, edgeBonus: 0, freeBonus: 0};
ai.oneStepDeep.prototype.weight = function(brd) {
var weight = 0;
if (this.cfg.scoreCoef > 0)
weight += this.cfg.scoreCoef * brd.score();
var max = brd.max();
if (this.cfg.maxValCoef > 0)
weight += this.cfg.maxValCoef * max;
if (this.cfg.cornerBonus > 0 && brd.atCorner(max))
weight += this.cfg.cornerBonus;
if (this.cfg.edgeBonus > 0 && brd.atEdge(max))
weight += this.cfg.edgeBonus;
if (this.cfg.freeBonus > 0)
weight += this.cfg.freeBonus * brd.free();
return weight;
}
ai.oneStepDeep.prototype.analyse = function(brd) {
var origBrd = new this.brdEngine(brd);
var nextBrd = new this.brdEngine();
var maxWeight = -1;
var bestDir;
for (var i = 0; i < ai.dirs.length; i++) {
var dir = ai.dirs[i];
if (origBrd[dir](nextBrd)) {
var weight = this.weight(nextBrd);
if (maxWeight < weight) {
bestDir = dir;
maxWeight = weight;
}
}
}
return bestDir;
}
/* Mark that next board will be unrelated to previous, so any stored precompution can be cleared. */
ai.oneStepDeep.prototype.cleanup = function() { }
////////////////////////////////////////////////////////////////
// N level deep on score value without random simulation.
////////////////////////////////////////////////////////////////
ai.deepMaxScore = function(brdEngine) {
this.brdEngine = brdEngine;
}
ai.deepMaxScore.prototype.analyse = function(brd) {
var origBrd = new this.brdEngine(brd);
var nextBrd = new this.brdEngine();
var prevScore = -1, nextScore = -1;
var maxScore = -1;
var bestDir;
for (var i = 0; i < ai.dirs.length; i++) {
var dir = ai.dirs[i];
if (origBrd[dir](nextBrd)) {
nextScore = nextBrd.score();
var score = this.bestScore(nextBrd);
// console.log("dir: %o, prevScore: %o, nextScore: %o, maxScore: %o, score: %o", dir, prevScore, nextScore, maxScore, score);
if (maxScore < score || (maxScore === score && prevScore < nextScore)) {
prevScore = nextScore;
maxScore = score;
bestDir = dir;
}
}
}
return bestDir;
}
ai.deepMaxScore.prototype.bestScore = function(brd, seenBrds) {
if (seenBrds) {
for (var i = 0; i < seenBrds.length; i++)
if (brd.equals(seenBrds[i]))
return 0;
} else {
seenBrds = [];
}
seenBrds.push(brd);
var currScore = brd.score();
var maxScore = currScore;
var nextBrd = new this.brdEngine();
for (var i = 0; i < ai.dirs.length; i++) {
if (brd[ai.dirs[i]](nextBrd)) {
var score = nextBrd.score();
if (score > currScore)
maxScore = Math.max(maxScore, this.bestScore(nextBrd, seenBrds));
}
}
return maxScore;
}
/* Mark that next board will be unrelated to previous, so any stored precompution can be cleared. */
ai.deepMaxScore.prototype.cleanup = function() { }
////////////////////////////////////////////////////////////////
// N level deep on score value + max value prefer corner,
// without random simulation.
////////////////////////////////////////////////////////////////
/* cfg.cornerBonus - value to add if max value at corner. */
/* cfg.edgeBonus - value to add if max value at edge. */
ai.deepMaxScoreCorner = function(brdEngine, cfg) {
this.brdEngine = brdEngine;
this.cfg = cfg || {};
this.cfg.cornerBonus = this.cfg.cornerBonus || 20000;
this.cfg.edgeBonus = this.cfg.edgeBonus || 100;
this.cfg.freeBonus = this.cfg.edgeBonus || 100;
}
ai.deepMaxScoreCorner.prototype.scoreCorner = function(brd) {
var score = brd.score();
var max = brd.max();
if (brd.atCorner(max))
score += this.cfg.cornerBonus;
else if (brd.atEdge(max))
score += this.cfg.edgeBonus;
score += brd.free() * this.cfg.freeBonus;
return score;
}
ai.deepMaxScoreCorner.prototype.scoreEdge = function(brd) {
var score = brd.score();
var max = brd.max();
if (brd.atEdge(max))
score += this.cfg.edgeBonus;
score += brd.free() * this.cfg.freeBonus;
return score;
}
ai.deepMaxScoreCorner.prototype.analyse = function(brd) {
var origBrd = new this.brdEngine(brd);
var nextBrd = new this.brdEngine();
var prevScore = -1, nextScore = -1;
var maxScore = -1;
var bestDir;
for (var i = 0; i < ai.dirs.length; i++) {
var dir = ai.dirs[i];
if (origBrd[dir](nextBrd)) {
nextScore = this.scoreCorner(nextBrd);
var score = this.bestScore(nextBrd);
// console.log("dir: %o, prevScore: %o, nextScore: %o, maxScore: %o, score: %o", dir, prevScore, nextScore, maxScore, score);
if (maxScore < score || (maxScore === score && prevScore < nextScore)) {
prevScore = nextScore;
maxScore = score;
bestDir = dir;
}
}
}
return bestDir;
}
ai.deepMaxScoreCorner.prototype.bestScore = function(brd, seenBrds) {
if (seenBrds) {
for (var i = 0; i < seenBrds.length; i++)
if (brd.equals(seenBrds[i]))
return 0;
} else {
seenBrds = [];
}
seenBrds.push(brd);
var currScore = this.scoreEdge(brd);
var maxScore = currScore;
var nextBrd = new this.brdEngine();
for (var i = 0; i < ai.dirs.length; i++) {
if (brd[ai.dirs[i]](nextBrd)) {
var score = this.scoreEdge(nextBrd);
if (score > currScore)
maxScore = Math.max(maxScore, this.bestScore(nextBrd, seenBrds));
}
}
return maxScore;
}
/* Mark that next board will be unrelated to previous, so any stored precompution can be cleared. */
ai.deepMaxScoreCorner.prototype.cleanup = function() { }
////////////////////////////////////////////////////////////////
// N level deep with random simulation.
////////////////////////////////////////////////////////////////
/* cfg.cornerBonus - value to add if max value at corner. */
/* cfg.edgeBonus - value to add if max value at edge. */
ai.expectimax = function(brdEngine, cfg) {
this.brdEngine = brdEngine;
this.cfg = cfg || {};
this.cfg.balance = this.cfg.balance || .9;
if (!this.cfg.depth || this.cfg.depth < 0 || 8 <= this.cfg.depth)
this.cfg.depth = 5;
this.cfg.cornerBonus = this.cfg.cornerBonus || 20000;
this.cfg.edgeBonus = this.cfg.edgeBonus || 100;
this.cfg.freeBonus = this.cfg.edgeBonus || 100;
this.cfg.weightPriority = this.cfg.weightPriority || 10;
}
ai.expectimax.prototype.lvl1Score = function(brd) {
var score = brd.score();
var max = brd.max();
if (brd.atCorner(max))
score += this.cfg.cornerBonus;
else if (brd.atEdge(max))
score += this.cfg.edgeBonus;
score += brd.free() * this.cfg.freeBonus;
return score;
}
ai.expectimax.prototype.lvlnScore = function(brd) {
var score = brd.score();
// var max = brd.max();
// if (brd.atCorner(max))
// score += this.cfg.cornerBonus;
// else if (brd.atEdge(max))
// score += this.cfg.edgeBonus;
// score += brd.free() * this.cfg.freeBonus;
return score;
}
ai.expectimax.prototype.analyse = function(brd) {
var origBrd = new this.brdEngine(brd);
var nextBrd = new this.brdEngine();
var prevScore = -1, nextScore = -1;
var maxWeight = -1;
var bestDir;
this.cleanup();
for (var i = 0; i < ai.dirs.length; i++) {
var dir = ai.dirs[i];
if (origBrd[dir](nextBrd)) {
nextScore = this.lvl1Score(nextBrd);
var weight = this.weight(nextBrd, 0);
// console.log("dir: %o, prevScore: %o, nextScore: %o, maxWeight: %o, weight: %o", dir, prevScore, nextScore, maxWeight, weight);
if (maxWeight + this.cfg.weightPriority < weight || (maxWeight <= weight && prevScore < nextScore)) {
prevScore = nextScore;
maxWeight = weight;
bestDir = dir;
}
}
}
this.cleanup();
return bestDir;
}
ai.expectimax.prototype.weight = function(brd, depth) {
if (depth === this.cfg.depth)
return this.lvlnScore(brd);
if (this.cache[depth]) {
var cache = this.cache[depth];
for (var i = cache.length-1; i >= 0; i--) {
if (brd.equals(cache[i].brd))
return cache[i].weight;
}
} else {
this.cache[depth] = [];
}
var weight = 0;
var free = 0;
for (var i = 0; i < 3; i++) {
for (var j = 0; j < 3; j++) {
if (brd.get(i, j) === 0) {
var randBoard = brd.copy();
randBoard.set(i, j, 1);
var nextBrd = new this.brdEngine();
var n = 0, w = 0;
for (var diri = 0; diri < ai.dirs.length; diri++) {
if (randBoard[ai.dirs[diri]](nextBrd)) {
w += this.weight(nextBrd, depth+1);
n++;
}
}
if (n > 0)
w = w / n;
weight += this.cfg.balance * w;
randBoard.set(i, j, 2);
var n = 0, w = 0;
for (var diri = 0; diri < ai.dirs.length; diri++) {
if (randBoard[ai.dirs[diri]](nextBrd)) {
w += this.weight(nextBrd, depth+1);
n++;
}
}
if (n > 0)
w = w / n;
weight += this.cfg.balance * w;
free++;
}
}
}
if (free > 0)
weight = weight / free;
this.cache[depth].push({brd: brd, weight: weight});
return weight;
}
/* Mark that next board will be unrelated to previous, so any stored precompution can be cleared. */
ai.expectimax.prototype.cleanup = function() {
this.cache = [];
}