Expectimax implementation.
authorOleksandr Gavenko <gavenkoa@gmail.com>
Mon, 08 Sep 2014 19:42:32 +0300
changeset 21 ed0292f0c7c6
parent 20 ab294e8db00c
child 22 b041338d7e88
Expectimax implementation.
2048.html
ai.js
--- a/2048.html	Mon Sep 08 17:43:10 2014 +0300
+++ b/2048.html	Mon Sep 08 19:42:32 2014 +0300
@@ -89,6 +89,7 @@
       <button id="ai-next-max-value">next max value</button>
       <button id="ai-deep-max-score">deep max score</button>
       <button id="ai-deep-max-score-corner">deep max score corner</button>
+      <button id="ai-expectimax">expectimax</button>
     </div>
   </div>
 
@@ -358,6 +359,9 @@
     document.getElementById("ai-deep-max-score-corner").addEventListener("click", function() {
       ui.ai = new ai.deepMaxScoreCorner(ui.brdEngine);
     });
+    document.getElementById("ai-expectimax").addEventListener("click", function() {
+      ui.ai = new ai.expectimax(ui.brdEngine);
+    });
 
   </script>
   
--- a/ai.js	Mon Sep 08 17:43:10 2014 +0300
+++ b/ai.js	Mon Sep 08 19:42:32 2014 +0300
@@ -215,3 +215,119 @@
 /* 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 = [];
+}
+