2048.html
author Oleksandr Gavenko <gavenkoa@gmail.com>
Sat, 20 Sep 2014 22:55:24 +0300
changeset 99 df4736e659f2
parent 98 eb4f89e2a001
child 100 40a3a4247e8b
permissions -rw-r--r--
Rename AI.

<!DOCTYPE html>
<html>
<head>
  <title>2048 AI</title>
  <meta name="viewport" content="width=device-width; initial-scale=1.0"/>
  <meta charset="utf-8"/>

  <script src="rule.js"></script>
  <script src="board.js"></script>
  <script src="perf.js"></script>
  <script src="ai.js"></script>

  <style>
    body {
      font-size: 11pt;
    }
    h1, div.area {
      text-align: center;
      margin: 10px auto;
    }
    #board {
      margin: 10px auto;
    }
    #board td {
      width: 40px;
      height: 40px;
      border: 1px solid red;
      margin: 0;
      text-align: center;
    }
    div.ai-control, div.move-control, div.score-conrol {
      border: 1px red solid;
      display: inline-block;
      margin: 10px 0 2px 0;
      background-color: cornsilk;
    }
    div.move-control, div.score-conrol {
      padding: 3px;
    }
    div.move-control button.suggestion {
      background-color: cyan;
    }
    div.ai-control {
      padding: 2px 2px 2px 1em;
    }
    div.ai-control:before {
      content: "AI: ";
      margin-left: -.5em;
      padding: 0;
    }
    div.ai, div.settings, div#reports {
      border: 1px red solid;
      margin-top: 20px;
      padding: 2px;
      position: relative;
      background-color: cornsilk;
    }
    div.ai.enabled {
      background-color: wheat;
    }
    div.ai > h5 {
      margin: 2px 0 2px 6em;
    }
    button.ai {
      position: absolute;
      left: 1em;
      top: -1em;
    }
    button#statistic, button#statistic-clean {
      display: inline-block;
      float: left;
      margin: 3px 4px;
    }
    div.ai > div.option, div.setting, div.report {
      display: inline-block;
      float: left;
      margin: 1px 4px;
      padding: 2px;
      border: 1px solid tan;
      border-radius: 4px;
    }
    div.ai > div.option > input.positive, div.settings > div.setting > input, div.ai-control input.positive {
      text-align: right;
      max-width: 4em;
      margin-right: 2px;
    }
    .clearfix {
      clear: both;
    }
    #test {
      display: none;
    }
    div.report > h5 {
      margin: 2px 0;
    }
    table.report-by-maxval {
      border-collapse: collapse;
      margin: 2px;
      font-size: 10pt;
    }
    table.report-by-maxval > tr > th {
      border: 2px solid green;
      padding: 2px;
    }
    table.report-by-maxval > tr > td {
      border: 1px solid green;
      padding: 2px;
    }
    table.report-by-maxval > tr > td:nth-child(3) {
      background-color: yellow;
    }
  </style>
</head>
<body>

  <div class="area">[ <a href="README.html">About</a> | <a href="CHANGES.html">Changes</a> | <a href="HACKING.html">Hacking</a> | <a href="AUTHORS.html">Authors</a> ]</div>

  <h1>2048</h1>

  <div id="score-area" class="area">
    <div class="score-conrol">
      Score: <span id="score">0</span>, Max: <span id="max">0</span>, Turn: <span id="turn">0</span>, Speed: <span id="speed">0</span> t/s
    </div>
  </div>

  <div id="message-area" class="area"></div>

  <table id="board">
    <tr>
      <td></td>
      <td></td>
      <td></td>
      <td></td>
    </tr>
    <tr>
      <td></td>
      <td></td>
      <td></td>
      <td></td>
    </tr>
    <tr>
      <td></td>
      <td></td>
      <td></td>
      <td></td>
    </tr>
    <tr>
      <td></td>
      <td></td>
      <td></td>
      <td></td>
    </tr>
  </table>

  <div id="control-area" class="area">
    <div>
      <button id="start">Start</button>
      <span><input type="checkbox" id="2048" checked> 2048</span>
    </div>
    <div class="ai-control">
      <button id="suggest">Suggest</button>
      <button id="step">Step</button>
      <button id="finish">Finish</button>
    </div>
    <div class="ai-control">
      <button id="until">Continue</button>
      until <input type="text" class="positive" id="until-score" value="10000"> score
      and <input type="text" class="positive" id="until-max-value" value="9"> max value
    </div>
    <div class="clearfix"></div>
    <div class="move-control">
      <table>
        <tr>
          <td></td><td><button id="up">↑</button></td><td></td>
        </tr>
        <tr>
          <td><button id="left">←</button></td><td><button id="down">↓</button></td><td><button id="right">→</button></td>
        </tr>
      </table>
      <button id="test">test</button>
    </div>
    <h1>AIs</h1>
    <div>
      <div class="ai" id="ai-blind-random">
        <button class="ai">enable</button>
        <h5>bling random</h5>
      </div>
      <div class="ai" id="ai-blind-weight-random">
        <button class="ai">enable</button>
        <h5>bling weight random</h5>
        <div class="option">
          <input type="text" name="left" class="positive" pattern="[0-9]*[.]?[0-9]*" value="1"/> left weight
        </div>
        <div class="option">
          <input type="text" name="right" class="positive" pattern="[0-9]*[.]?[0-9]*" value="1"/> right weight
        </div>
        <div class="option">
          <input type="text" name="up" class="positive" pattern="[0-9]*[.]?[0-9]*" value="1"/> up weight
        </div>
        <div class="option">
          <input type="text" name="down" class="positive" pattern="[0-9]*[.]?[0-9]*" value="1"/> down weight
        </div>
        <div class="clearfix"></div>
      </div>
      <div class="ai" id="ai-blind-cycle">
        <button class="ai">enable</button>
        <h5>blind cycle</h5>
        <div class="option">
          <input type="checkbox" name="clockwise"/> clockwise
        </div>
        <div class="option">
          <input type="checkbox" name="whilePossible"/> max move in one direction
        </div>
        <div class="clearfix"></div>
      </div>
      <div class="ai" id="ai-one-step-ahead">
        <button class="ai">enable</button>
        <h5>one step ahead</h5>
        <div class="option">
          <input type="text" name="scoreCoef" class="positive" pattern="[0-9]*[.]?[0-9]*" value="1"/> score weight
        </div>
        <div class="option">
          <input type="text" name="maxValCoef" class="positive" pattern="[0-9]*[.]?[0-9]*" value="0"/> max value weight
        </div>
        <div class="option">
          <input type="text" name="cornerBonus" class="positive" pattern="[0-9]*[.]?[0-9]*" value="100"/> max value at corner bonus
        </div>
        <div class="option">
          <input type="text" name="edgeBonus" class="positive" pattern="[0-9]*[.]?[0-9]*" value="0"/> max value at edge bonus
        </div>
        <div class="option">
          <input type="text" name="freeBonus" class="positive" pattern="[0-9]*[.]?[0-9]*" value="10"/> free cell coefficient
        </div>
        <div class="clearfix"></div>
      </div>
      <div class="ai" id="ai-deep-max-score">
        <button class="ai">enable</button>
        <h5>deep merges without simulation make max score</h5>
      </div>
      <div class="ai" id="ai-deep-max-score-corner">
        <button class="ai">enable</button>
        <h5>deep merges without simulation make max score + bonus if max value at corner/edge</h5>
      </div>
      <div class="ai" id="ai-expectimax">
        <button class="ai">enable</button>
        <h5>expectimax</h5>
        <div class="option">
          <input type="text" name="depth" class="positive" pattern="[0-9]*" value="3"/> recursion depth
        </div>
        <div class="option">
          <input type="text" name="balance" class="positive" pattern="[0-9]*[.]?[0-9]*" value=".9"/> probability of 2
        </div>
        <div class="option">
          <input type="text" name="scoreCoef" class="positive" pattern="[0-9]*[.]?[0-9]*" value="1"/> score weight
        </div>
        <div class="option">
          <input type="text" name="maxValCoef" class="positive" pattern="[0-9]*[.]?[0-9]*" value="0"/> max value weight
        </div>
        <div class="option">
          <input type="text" name="cornerBonus" class="positive" pattern="[0-9]*[.]?[0-9]*" value="100"/> max value at corner bonus
        </div>
        <div class="option">
          <input type="text" name="edgeBonus" class="positive" pattern="[0-9]*[.]?[0-9]*" value="0"/> max value at edge bonus
        </div>
        <div class="option">
          <input type="text" name="freeBonus" class="positive" pattern="[0-9]*[.]?[0-9]*" value="10"/> free cell coefficient
        </div>
        <div class="clearfix"></div>
      </div>
    </div>
  </div>

  <div id="report-area" class="area">
    <h1>Reports</h1>
    <div class="settings">
      <div class="setting"><input type="text" id="stat-count-limit" value="100"> times</div>
      <div class="setting">limit to <input type="text" id="stat-time-limit" value="10"> sec</div>
      <button id="statistic">Start</button>
      <button id="statistic-clean">Clean</button>
      <div class="setting"><input type="checkbox" id="report-score"/> score</div>
      <div class="setting"><input type="checkbox" id="report-turn"/> turn</div>
      <div class="setting"><input type="checkbox" id="report-speed"/> speed</div>
      <div class="clearfix"></div>
    </div>
    <div id="reports">
      <div class="clearfix"></div>
    </div>
  </div>

  <script>
    "use strict";

    var ui = {};

    ////////////////////////////////////////////////////////////////
    // UI widgets.

    ui.dom = {};
    ui.dom.clearfix = function() {
      var divDom = document.createElement('div');
      divDom.classList.add("clearfix");
      return divDom;
    }
    ui.dom.table = function(tbl, cols, cfg) {
      var tableDom = document.createElement('table');
      if (typeof cfg.tableClass === 'string')
        tableDom.classList.add(cfg.tableClass);
      var trDom = document.createElement('tr');
      for (var i = 0; i < cols.length; i++) {
        var thDom = document.createElement('td');
        thDom.appendChild(document.createTextNode(cols[i]));
        trDom.appendChild(thDom);
      }
      tableDom.appendChild(trDom);
      for (var i = 0; i < tbl.length; i++) {
        var row = tbl[i];
        var trDom = document.createElement('tr');
        for (var j = 0; j < row.length; j++) {
          var tdDom = document.createElement('td');
          tdDom.appendChild(document.createTextNode(row[j]));
          trDom.appendChild(tdDom);
        }
        tableDom.appendChild(trDom);
      }
      return tableDom;
    };

    ////////////////////////////////////////////////////////////////
    // Board.

    var boardDom = document.getElementById("board");
    ui.board = {};
    ui.board.val2048Dom = document.getElementById('2048');
    ui.board.val2048Dom.addEventListener("click", function(event) {
      event.preventDefault(); event.stopPropagation();
      ui.board.update(ui.board.position);
      localStorage.val2048 = ui.board.val2048Dom.checked;
      event.target.blur();
    }, false);
    ui.board.val2048 = function(val) {
      if (ui.board.val2048Dom.checked)
        return Math.pow(2, val);
      return val;
    }
    /* 'val' typesafe.  */
    ui.board.set = function(i, j, val) {
      if (typeof val !== 'number')
        val = 0;
      var dom = boardDom.querySelectorAll("tr")[i].querySelectorAll("td")[j];
      if (0 < val && val < 15) {
        dom.innerHTML = ui.board.val2048(val);
        var rgb = 'rgb(255,'+ (255-5*val-2*(val%2)) +','+ (255-10*val-5*((val-1)%2)) +')';
      } else {
        dom.innerHTML = '';
        var rgb = 'white';
      }
      dom.style.backgroundColor = rgb;
    }
    ui.board.update = function(brd) {
      for (var i = 0; i < 4; i++) {
        for (var j = 0; j < 4; j++) {
          ui.board.set(i, j, brd[i][j]);
        }
      }
    }
    ui.board.handleEdit = function(tdDom, i, j) {
      if (!tdDom.firstChild || tdDom.firstChild.nodeType === Node.TEXT_NODE) {
        if (tdDom.firstChild)
          tdDom.removeChild(tdDom.firstChild);
        var inputDom = document.createElement('input');
        inputDom.value = ui.board.position[i][j];
        inputDom.style.width = '1em';
        tdDom.appendChild(inputDom);
        inputDom.focus();
        inputDom.select();
        inputDom.addEventListener("blur", function(tdDom, i, j) {
          return function(event) {
            event.preventDefault(); event.stopPropagation();
            ui.board.handleEdit(tdDom, i, j);
          }
        }(tdDom, i, j), false);
      } else {
        var inputDom = tdDom.firstChild;
        var val = parseInt(inputDom.value);
        if (0 <= val && val < 15) {
          ui.board.position[i][j] = val;
          ui.board.set(i, j, val);
        } else {
          ui.board.set(i, j, ui.board.position[i][j]);
        }
      }
    }
    ui.board.enableEdit = function() {
      var trDoms = boardDom.querySelectorAll("tr");
      for (var i = 0; i < 4; i++) {
        var tdDoms = trDoms[i].querySelectorAll("td");
        for (var j = 0; j < 4; j++) {
          var tdDom = tdDoms[j];
          tdDom.addEventListener("click", function(tdDom, i, j) {
            return function(event) {
              event.preventDefault(); event.stopPropagation();
              ui.board.handleEdit(tdDom, i, j);
            }
          }(tdDom, i, j), false);
          tdDom.addEventListener("keyup", function(tdDom, i, j) {
            return function(event) {
              event.preventDefault(); event.stopPropagation();
              if (event.keyCode === 13)
                ui.board.handleEdit(tdDom, i, j);
            }
          }(tdDom, i, j), false);
        }
      }
    }
    ui.board.enableEdit();

    ////////////////////////////////////////////////////////////////
    // Score statistic.

    ui.score = {};
    var scoreDom = document.getElementById("score");
    var maxDom = document.getElementById("max");
    var speedDom = document.getElementById("speed");
    var turnDom = document.getElementById("turn");
    ui.score.clear = function(brd) {
      scoreDom.innerHTML = '0';
      maxDom.innerHTML = '0';
      speedDom.innerHTML = '0';
      turnDom.innerHTML = '0';
    }
    ui.score.update = function(brd, turn) {
      var score = board.score(brd);
      scoreDom.innerHTML = '' + score.score;
      maxDom.innerHTML = '' + ui.board.val2048(score.max);
      turnDom.innerHTML = '' + turn;
    }
    ui.score.speed = function(speed) {
      speedDom.innerHTML = '' + speed;
    }

    ////////////////////////////////////////////////////////////////
    // Suggestion UI.

    ui.suggestion = {};
    ui.suggestion.btns = document.querySelectorAll('div.move-control button');
    ui.suggestion.clear = function() {
      for (var i = 0; i < ui.suggestion.btns.length; i++)
        ui.suggestion.btns[i].classList.remove('suggestion');
    }
    ui.suggestion.set = function(dir) {
      document.getElementById(dir).classList.add('suggestion');
    }

    ////////////////////////////////////////////////////////////////
    // Game flow.

    ui.game = {};
    ui.game.dirs = ["up", "right", "down", "left"];

    var messageDom = document.getElementById("message-area");
    ui.game.clearMessage = function() {
      messageDom.innerHTML = "";
    }
    ui.game.setMessage = function(msg) {
      messageDom.innerHTML = msg;
    }

    ui.game.checkGameOver = function() {
      if (board.gameOver(ui.board.position)) {
        ui.game.setMessage("Game over!");
        return true;
      } else {
        return false;
      }
    }
    ui.game.checkMoveValid = function(move) {
      var tmpBrd = board.create();
      board.copy(ui.board.position, tmpBrd);
      if (ui.game.dirs.indexOf(move) === -1) {
        ui.game.setMessage("AI can't find move!");
        return false;
      }
      if ( ! board.move[move].call(null, tmpBrd)) {
        ui.game.setMessage("AI move "+move+" is ivalid!");
        return false;
      }
      return true;
    }

    ui.game.beginStep = function() {
      ui.suggestion.clear();
      ui.game.clearMessage();
      if (ui.game.checkGameOver())
        return false;
      return true;
    }
    ui.game.refresh = function() {
      ui.board.update(ui.board.position);
      ui.score.update(ui.board.position, ui.board.turn);
      localStorage.savedBoard = JSON.stringify(ui.board.position);
      localStorage.savedTurn = ui.board.turn;
    }
    ui.game.finishStep = function() {
      board.putRandom(ui.board.position);
      ui.board.turn++;
      ui.game.refresh();
    }

    ////////////////////////////////////////////////////////////////
    // Actions.

    ui.action = {};

    ui.action.start = function() {
      ui.score.clear();
      ui.suggestion.clear();
      ui.game.clearMessage();
      ui.board.turn = 0;
      ui.board.position = board.create();
      board.putRandom(ui.board.position);
      ui.board.update(ui.board.position);
    }
    document.getElementById("start").addEventListener("click", ui.action.start, false);

    for (var i = 0; i < ui.game.dirs.length; i++) {
      var dir = ui.game.dirs[i];
      ui.action[dir] = function(dir) {
        return function(event) {
          event.preventDefault(); event.stopPropagation();
          if (ui.game.checkGameOver())
            return;
          ui.suggestion.clear();
          var updated = board.move[dir](ui.board.position);
          if (updated) {
            ui.game.finishStep();
            ui.ai.current && ui.ai.current.cleanup();
          }
        }
      } (dir);
      document.getElementById(dir).addEventListener("click", ui.action[dir], false);
    }

    document.body.addEventListener("keydown", function(event) {
      if (document.activeElement.value)
        return;
      if (event.altKey || event.ctrlKey || event.shiftKey || event.metaKey)
        return;
      var key = event.keyCode || event.which;
      var keyMoves = {38: "up", 40: "down", 37: "left", 39: "right"};
      if (key in keyMoves) {
        ui.action[keyMoves[key]](event);
      }
    }, false);

    document.getElementById("test").addEventListener("click", function() {
      event.preventDefault(); event.stopPropagation();
      board.move.replaceByBoardJS(ui.brdEngine);
    }, false);

    ui.action.suggest = function(event) {
      event.preventDefault(); event.stopPropagation();
      if ( ! ui.ai.current) {
        ui.game.setMessage('Select AI!');
        return;
      }
      if ( ! ui.game.beginStep())
        return;
      var tmpBrd = board.create();
      board.copy(ui.board.position, tmpBrd);
      var move = ui.ai.current.analyse(tmpBrd);
      ui.ai.current.cleanup();
      if ( ! ui.game.checkMoveValid(move))
        return;
      ui.suggestion.set(move);
    }
    document.getElementById("suggest").addEventListener("click", ui.action.suggest, false);
    ui.action.step = function() {
      if ( ! ui.ai.current) {
        ui.game.setMessage('Select AI!');
        return;
      }
      if ( ! ui.game.beginStep())
        return;
      var tmpBrd = board.create();
      board.copy(ui.board.position, tmpBrd);
      var move = ui.ai.current.analyse(tmpBrd);
      ui.ai.current.cleanup();
      if ( ! ui.game.checkMoveValid(move))
        return;
      board.move[move].call(null, ui.board.position);
      ui.game.finishStep();
    }
    document.getElementById("step").addEventListener("click", ui.action.step, false);

    ui.action.finish = function() {
      if ( ! ui.ai.current) {
        ui.game.setMessage('Select AI!');
        return;
      }
      ui.game.beginStep();
      var step = 0;
      var safeBdr = board.create();
      var tsFrom = new Date().getTime();
      while (!board.gameOver(ui.board.position)) {
        board.copy(ui.board.position, safeBdr);
        var move = ui.ai.current.analyse(safeBdr);
        if (typeof move === 'undefined') {
          ui.game.setMessage("I don't know how to move!");
          return;
        }
        var updated = board.move[move].call(null, ui.board.position);
        if (updated) {
          board.putRandom(ui.board.position);
        } else {
          ui.board.turn += step;
          ui.game.refresh();
          ui.game.setMessage("Wrong move!");
          return;
        }
        step++; 
      }
      var tsTo = new Date().getTime();
      ui.board.turn += step;
      ui.game.finishStep();
      ui.score.speed(parseFloat((step*1000.0/(tsTo-tsFrom)).toPrecision(3)));
      ui.game.setMessage("Game over!");
      ui.ai.current.cleanup();
    }
    document.getElementById("finish").addEventListener("click", ui.action.finish, false);

    ui.action.until = function() {
      if ( ! ui.ai.current) {
        ui.game.setMessage('Select AI!');
        return;
      }
      if ( ! ui.game.beginStep())
        return;
      var step = 0;
      var safeBdr = board.create();
      var tsFrom = new Date().getTime();
      var scoreLimit = parseInt(document.getElementById("until-score").value);
      if (!isFinite(scoreLimit) || scoreLimit < 0) {
        scoreLimit = 1;
        document.getElementById("until-score").value = scoreLimit;
      }
      var maxValLimit = parseInt(document.getElementById("until-max-value").value);
      if (!isFinite(maxValLimit) || maxValLimit < 0 || maxValLimit > 13) {
        maxValLimit = 1;
        document.getElementById("until-max-value").value = maxValLimit;
      }
      localStorage.untilScore = scoreLimit;
      localStorage.untilMaxVal = maxValLimit;
      while (true) {
        if (board.gameOver(ui.board.position)) {
          ui.game.setMessage("Game over!");
          break;
        }
        var stat = board.score(ui.board.position);
        if (stat.score >= scoreLimit && stat.max >= maxValLimit)
          break;
        board.copy(ui.board.position, safeBdr);
        var move = ui.ai.current.analyse(safeBdr);
        if (typeof move === 'undefined') {
          ui.game.setMessage("I don't know how to move!");
          return;
        }
        var updated = board.move[move].call(null, ui.board.position);
        if (updated) {
          board.putRandom(ui.board.position);
        } else {
          ui.game.finishStep();
          ui.game.setMessage("Wrong move!");
          return;
        }
        step++; 
      }
      var tsTo = new Date().getTime();
      ui.board.turn += step;
      ui.game.refresh();
      ui.score.speed(parseFloat((step*1000.0/(tsTo-tsFrom)).toPrecision(3)));
      ui.ai.current.cleanup();
    }
    document.getElementById("until").addEventListener("click", ui.action.until, false);

    ////////////////////////////////////////////////////////////////
    // Register AIs.

    ui.ai = {};
    ui.ai.current = null;
    ui.ai.parseCfg = function(aiDom, cfg) {
      var optDoms = aiDom.querySelectorAll("div.option > input.positive[type='text']");
      for (var i = 0; i < optDoms.length; i++) {
        var val = parseFloat(optDoms[i].value);
        if (val === NaN || val < 0) {
          ui.game.setMessage('' + optDoms[i].name + ' is not a positive number!');
          cfg[optDoms[i].name] = 1;
          continue;
        }
        cfg[optDoms[i].name] = val;
      }
    }

    ui.ai.algList = {
      "ai-blind-random": function() {
        return new ai.BlindRandom(ui.brdEngine);
      },
      "ai-blind-weight-random": function(aiDom) {
        var cfg = {};
        ui.ai.parseCfg(aiDom, cfg);
        return new ai.BlindWeightRandom(ui.brdEngine, cfg);
      },
      "ai-blind-cycle": function(aiDom) {
        var cfg = {};
        cfg.clockwise = aiDom.querySelectorAll("input[name='clockwise']")[0].checked;
        cfg.whilePossible = aiDom.querySelectorAll("input[name='whilePossible']")[0].checked;
        return new ai.BlindCycle(ui.brdEngine, cfg);
      },
      "ai-one-step-ahead": function(aiDom) {
        var cfg = {};
        ui.ai.parseCfg(aiDom, cfg);
        return new ai.OneStepAhead(ui.brdEngine, cfg);
      },
      "ai-deep-max-score": function() {
        return new ai.DeepMaxScore(ui.brdEngine);
      },
      "ai-deep-max-score-corner": function() {
        return new ai.DeepMaxScoreCorner(ui.brdEngine);
      },
      "ai-expectimax": function(aiDom) {
        var cfg = {};
        ui.ai.parseCfg(aiDom, cfg);
        return new ai.expectimax(ui.brdEngine, cfg);
      },
      // "": function() {
      //   return new ai.(ui.brdEngine);
      // },
    };
    ui.ai.domList = document.querySelectorAll('div.ai');
    for (var i = 0; i < ui.ai.domList.length; i++) {
      ui.ai.domList[i].querySelectorAll('button.ai')[0].addEventListener("click", function (event) {
        ui.ai.enable(event.target.parentNode);
      }, false);
    }

    ui.ai.moveToTop = function(aiDom) {
      for (var i = 0; i < ui.ai.domList.length; i++) {
        ui.ai.domList[i].classList.remove('enabled');
      }
      var rootDom = aiDom.parentNode;
      rootDom.removeChild(aiDom);
      rootDom.insertBefore(aiDom, rootDom.firstChild);
      aiDom.classList.add('enabled');
    }
    ui.ai.enable = function(aiDom) {
      if (ui.ai.current)
        ui.ai.current.cleanup();
      ui.ai.moveToTop(aiDom);
      ui.ai.current = ui.ai.algList[aiDom.id](aiDom);
      ui.ai.currentName = aiDom.id;
    }


    ////////////////////////////////////////////////////////////////
    // Reports and statistic.

    var reportsDom = document.getElementById('reports');

    ui.report = {};

    ui.report.stat = function() {
      var stats = [];
      var cnt = parseInt(document.getElementById('stat-count-limit').value);
      if (isNaN(cnt) || !isFinite(cnt) || cnt < 1)
        cnt = 100;
      var tsLimit = parseFloat(document.getElementById('stat-count-limit').value);
      if (isNaN(tsLimit) || !isFinite(tsLimit) || tsLimit < 1 || tsLimit > 60)
        tsLimit = 1000 * 10;
      else
        tsLimit = 1000 * tsLimit;
      var safeBdr = board.create();
      var tsLimitFrom = new Date().getTime();
      for (var gameCnt = 0; gameCnt++ < cnt; void(0)) {
        var turn = 0;
        var brd = board.create();
        board.putRandom(brd);
        var tsFrom = new Date().getTime();
        while (!board.gameOver(brd)) {
          board.copy(brd, safeBdr);
          var move = ui.ai.current.analyse(safeBdr);
          if (ui.game.dirs.indexOf(move) < 0) {
            ui.game.setMessage("I don't know how to move!");
            return;
          }
          var updated = board.move[move].call(null, brd);
          if (!updated) {
            ui.game.setMessage("Wrong move!");
            return;
          }
          board.putRandom(brd);
          turn++;
        }
        var tsTo = new Date().getTime();
        var stat = board.score(brd);
        stat.turn = turn;
        stat.ts = tsTo - tsFrom;
        stats.push(stat);
        if (tsTo - tsLimitFrom >= tsLimit)
          break;
      }
      var scoreChecked = document.getElementById('report-score').checked;
      var turnChecked = document.getElementById('report-turn').checked;
      var speedChecked = document.getElementById('report-speed').checked;
      var histo = {};
      for (i = stats.length-1; i >= 0; i--) {
        var stat = stats[i];
        if ( ! histo[stat.max])
          histo[stat.max] = { n: 0, minSpeed: Infinity, meanSpeed: 0, maxSpeed: 0, minTurn: Infinity, meanTurn: 0, maxTurn: 0, minScore: Infinity, meanScore: 0, maxScore: 0 };
        var row = histo[stat.max];
        row.n++;
        if (scoreChecked) {
          row.minScore = Math.min(row.minScore, stat.score);
          row.meanScore += stat.score;
          row.maxScore = Math.max(row.maxScore, stat.score);
        }
        if (turnChecked) {
          row.minTurn = Math.min(row.minTurn, stat.turn);
          row.meanTurn += stat.turn;
          row.maxTurn = Math.max(row.maxTurn, stat.turn);
        }
        if (speedChecked) {
          var speed = (stat.turn * 1000.0) / stat.ts;
          row.minSpeed = Math.min(row.minSpeed, speed);
          row.meanSpeed += speed;
          row.maxSpeed = Math.max(row.maxSpeed, speed);
        }
      }
      for (var i in histo) {
        var row = histo[i];
        var n = row.n;
        if (scoreChecked)
          row.meanScore = row.meanScore / n;
        if (turnChecked)
          row.meanTurn = row.meanTurn / n;
        if (speedChecked)
          row.meanSpeed = row.meanSpeed / n;
      }
      var reportDom = document.createElement('div');
      reportDom.classList.add('report');
      var h5Dom = document.createElement('h5');
      h5Dom.appendChild(document.createTextNode(ui.ai.currentName));
      reportDom.appendChild(h5Dom);
      var maxVals = Object.keys(histo).sort(function(a,b){return a - b});
      var tbl = [];
      for (var i = maxVals.length-1; i >= 0; i--) {
        var maxVal = maxVals[i];
        var row = histo[maxVals[i]];
        var perc = parseFloat((100 * row.n / gameCnt).toPrecision(3));
        var tblRow = [maxVal, row.n, perc];
        if (scoreChecked) {
          tblRow.push(row.minScore);
          var meanScore = parseFloat(row.meanScore.toPrecision(3));
          tblRow.push(meanScore);
          tblRow.push(row.maxScore);
        }
        if (turnChecked) {
          tblRow.push(row.minTurn);
          tblRow.push(Math.floor(row.meanTurn));
          tblRow.push(row.maxTurn);
        }
        if (speedChecked) {
          tblRow.push(parseFloat(row.minSpeed.toPrecision(3)));
          tblRow.push(parseFloat(row.meanSpeed.toPrecision(3)));
          tblRow.push(parseFloat(row.maxSpeed.toPrecision(3)));
        }
        tbl.push(tblRow);
      }
      var tblCols = ['maxVal', 'n', '%'];
      if (scoreChecked) {
        tblCols.push('min score');
        tblCols.push('mean score');
        tblCols.push('max score');
      }
      if (turnChecked) {
        tblCols.push('min turn');
        tblCols.push('mean turn');
        tblCols.push('max turn');
      }
      if (speedChecked) {
        tblCols.push('min speed');
        tblCols.push('mean speed');
        tblCols.push('max speed');
      }
      var tableDom = ui.dom.table(tbl, tblCols, { tableClass: 'report-by-maxval' });
      reportDom.appendChild(tableDom);
      reportsDom.insertBefore(reportDom, reportsDom.firstChild);
    }
    var statisticBtn = document.getElementById('statistic');
    statisticBtn.addEventListener("click", ui.report.stat, false);

    ui.report.statClean = function() {
      while (reportsDom.hasChildNodes()) {
        reportsDom.removeChild(reportsDom.lastChild);
      }
      reportsDom.appendChild(ui.dom.clearfix());
    }
    var statisticCleanBtn = document.getElementById('statistic-clean');
    statisticCleanBtn.addEventListener("click", ui.report.statClean, false);

    ////////////////////////////////////////////////////////////////
    // Initialise game.

    ui.brdEngine = BoardArr2d; // TODO make user selectable

    if (localStorage.untilScore) {
      var scoreLimit = parseInt(localStorage.untilScore);
      if (isFinite(scoreLimit) && scoreLimit > 0)
        document.getElementById("until-score").value = scoreLimit;
    }
    if (localStorage.untilMaxVal) {
      var maxValLimit = parseInt(localStorage.untilMaxVal);
      if (isFinite(maxValLimit) && maxValLimit > 0 && maxValLimit <= 13)
        document.getElementById("until-max-value").value = maxValLimit;
    }

    if (localStorage.val2048) {
      try {
        ui.board.val2048Dom.checked = JSON.parse(localStorage.val2048);
      } catch (ex) { }
    }
    if (localStorage.savedBoard) {
      try {
        ui.board.position = JSON.parse(localStorage.savedBoard);
        if (localStorage.savedTurn)
          ui.board.turn = JSON.parse(localStorage.savedTurn);
        else
          ui.board.turn = 0;
        ui.board.update(ui.board.position);
        ui.score.update(ui.board.position, ui.board.turn);
      } catch (ex) { }
    } else {
      ui.action.start();
    }

  </script>
  
</body>
</html>