2048.html
author Oleksandr Gavenko <gavenkoa@gmail.com>
Wed, 10 Sep 2014 17:33:04 +0300
changeset 37 fe75d168a605
parent 36 a18fc2601ce8
child 38 d476efde7242
permissions -rw-r--r--
Ignore board movement when user edit input.

<!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;
    }
    .report > .name {
      font-weight: bold;
    }
    div.ai {
      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;
    }
    div.ai > div.option {
      display: inline-block;
      float: left;
      margin: 1px 4px;
      padding: 2px;
      border: 1px solid tan;
      border-radius: 4px;
    }
    div.ai > div.option > input.int {
      text-align: right;
      max-width: 4em;
      margin-rght: 2px;
    }
    .clearfix {
      clear: both;
    }
    #test {
      display: none;
    }
  </style>
</head>
<body>

  <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>
    </div>
    <div class="ai-control">
      <button id="suggest">Suggest</button>
      <button id="step">Step</button>
      <button id="finish">Finish</button>
    </div>
    <div class="clearfix"></div>
    <div class="move-control">
      <table>
        <tr>
          <td></td><td><button id="up">up</button></td><td></td>
        </tr>
        <tr>
          <td><button id="left">left</button></td><td><button id="down">down</button></td><td><button id="right">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="int" pattern="[0-9]*" value="1"/> left weight
        </div>
        <div class="option">
          <input type="text" name="right" class="int" pattern="[0-9]*" value="1"/> right weight
        </div>
        <div class="option">
          <input type="text" name="up" class="int" pattern="[0-9]*" value="1"/> up weight
        </div>
        <div class="option">
          <input type="text" name="down" class="int" pattern="[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-next-max-value">
        <button class="ai">enable</button>
        <h5>next merge makes max value</h5>
      </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>
    </div>
  </div>

  <div id="report-area" class="area">
    <h1>Reports</h1>
    <div class="report" class="area">
      <div class="name">next max score</div>
      <table>
        <tr>
          <th>Max</th>
          <th>Count</th>
          <th>%</th>
        </tr>
        <tr>
          <td>128</td><td>8</td><td><div style="background: red; width: 100px">x</div></td>
        </tr>
        <tr>
          <td>256</td><td>4</td><td><div style="background: red; width: 50px">x</div></td>
        </tr>
        <tr>
          <td>512</td><td>12</td><td><div style="background: red; width: 150px">x</div></td>
        </tr>
      </table>
    </div>
  </div>

  <script>
    "use strict";

    var boardDom = document.getElementById("board");
    var ui = {};
    ui.board = {};
    /* '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 = 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 = board.current[i][j];
        inputDom.style.width = '1em';
        tdDom.appendChild(inputDom);
        inputDom.focus();
        inputDom.select();
        inputDom.addEventListener("blur", function(tdDom, i, j) {
          return function(event) {
            ui.board.handleEdit(tdDom, i, j);
            return false;
          }
        }(tdDom, i, j));
      } else {
        var inputDom = tdDom.firstChild;
        var val = parseInt(inputDom.value);
        if (0 <= val && val < 15) {
          board.current[i][j] = val;
          ui.board.set(i, j, val);
        } else {
          ui.board.set(i, j, board.current[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) {
              ui.board.handleEdit(tdDom, i, j);
              return false;
            }
          }(tdDom, i, j));
          tdDom.addEventListener("keyup", function(tdDom, i, j) {
            return function(event) {
              if (event.keyCode === 13)
                ui.board.handleEdit(tdDom, i, j);
            }
          }(tdDom, i, j));
        }
      }
    }
    ui.board.enableEdit();

    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) {
      var score = board.score(brd);
      scoreDom.innerHTML = '' + score.score;
      maxDom.innerHTML = '' + score.max;
    }
    ui.score.speed = function(speed, turn) {
      speedDom.innerHTML = '' + speed;
      turnDom.innerHTML = '' + turn;
    }

    ui.action = {};

    ui.action.start = function() {
      ui.score.clear();
      ui.message.clear();
      ui.action.clearSuggestion();
      board.current = board.create();
      board.putRandom(board.current);
      ui.board.update(board.current);
    }
    document.getElementById("start").addEventListener("click", ui.action.start);

    ui.action.up = function() {
      ui.action.clearSuggestion();
      var updated = board.move.up(board.current);
      if (updated) {
        board.putRandom(board.current);
        ui.board.update(board.current);
        ui.score.update(board.current);
        ui.ai.current && ui.ai.current.cleanup();
      }
    }
    document.getElementById("up").addEventListener("click", ui.action.up);
    ui.action.down = function() {
      ui.action.clearSuggestion();
      var updated = board.move.down(board.current);
      if (updated) {
        board.putRandom(board.current);
        ui.board.update(board.current);
        ui.score.update(board.current);
        ui.ai.current && ui.ai.current.cleanup();
      }
    }
    document.getElementById("down").addEventListener("click", ui.action.down);
    ui.action.left = function() {
      ui.action.clearSuggestion();
      var updated = board.move.left(board.current);
      if (updated) {
        board.putRandom(board.current);
        ui.board.update(board.current);
        ui.score.update(board.current);
        ui.ai.current && ui.ai.current.cleanup();
      }
    }
    document.getElementById("left").addEventListener("click", ui.action.left);
    ui.action.right = function() {
      ui.action.clearSuggestion();
      var updated = board.move.right(board.current);
      if (updated) {
        board.putRandom(board.current);
        ui.board.update(board.current);
        ui.score.update(board.current);
        ui.ai.current && ui.ai.current.cleanup();
      }
    }
    document.getElementById("right").addEventListener("click", ui.action.right);

    document.body.addEventListener("keydown", function(event) {
      if (document.activeElement.value)
        return false;
      var key = event.keyCode || event.which;
      switch (key) {
          case 38: ui.action.up(); break;
          case 40: ui.action.down(); break;
          case 37: ui.action.left(); break;
          case 39: ui.action.right(); break;
      }
      return false;
    });

    document.getElementById("test").addEventListener("click", function() {
      board.move.replaceByBoardJS(ui.brdEngine);
    });
    
    ui.message = {};
    var messageDom = document.getElementById("message-area");
    ui.message.clear = function() {
      messageDom.innerHTML = "";
    }
    ui.message.set = function(msg) {
      messageDom.innerHTML = msg;
    }

    ui.action.checkGameOver = function() {
      if (board.gameOver(board.current)) {
        ui.message.set("Game over!");
        return true;
      } else {
        return false;
      }
    }
    ui.action.checkMoveValid = function(move) {
      var tmpBrd = board.create();
      board.copy(board.current, tmpBrd);
      if (!(move in {"left":0,"right":0,"up":0,"down":0})) {
        ui.message.set("AI can't find move!");
        return false;
      }
      if ( ! board.move[move].call(null, tmpBrd)) {
        ui.message.set("AI move "+move+" is ivalid!");
        return false;
      }
      return true;
    }

    ui.action.clearSuggestion = function() {
      var btns = document.querySelectorAll('div.move-control button');
      for (var i = 0; i < btns.length; i++)
        btns[i].classList.remove('suggestion');
    }
    ui.action.suggest = function() {
      ui.action.clearSuggestion();
      ui.message.clear();
      if (ui.action.checkGameOver())
        return;
      var tmpBrd = board.create();
      board.copy(board.current, tmpBrd);
      var move = ui.ai.current.analyse(tmpBrd);
      ui.ai.current.cleanup();
      if ( ! ui.action.checkMoveValid(move))
        return;
      document.getElementById(move).classList.add('suggestion');
    }
    document.getElementById("suggest").addEventListener("click", ui.action.suggest);
    ui.action.step = function() {
      ui.message.clear();
      ui.action.clearSuggestion();
      if (ui.action.checkGameOver())
        return;
      var tmpBrd = board.create();
      board.copy(board.current, tmpBrd);
      var move = ui.ai.current.analyse(tmpBrd);
      ui.ai.current.cleanup();
      if ( ! ui.action.checkMoveValid(move))
        return;
      board.move[move].call(null, board.current);
      board.putRandom(board.current);
      ui.board.update(board.current);
      ui.score.update(board.current);
    }
    document.getElementById("step").addEventListener("click", ui.action.step);

    ui.action.finish = function() {
      ui.message.clear();
      ui.action.clearSuggestion();
      var step = 0;
      var tsFrom = new Date().getTime();
      while (!board.gameOver(board.current)) {
        var tmpBrd = board.create();
        board.copy(board.current, tmpBrd);
        var move = ui.ai.current.analyse(tmpBrd);
        if (typeof move === 'undefined') {
          ui.message.set("I don't know how to move!");
          return;
        }
        var updated = board.move[move].call(null, board.current);
        if (updated) {
          board.putRandom(board.current);
        } else {
          ui.board.update(board.current);
          ui.score.update(board.current);
          ui.message.set("Wrong move!");
          return;
        }
        step++; 
      }
      var tsTo = new Date().getTime();
      ui.board.update(board.current);
      ui.score.update(board.current);
      ui.score.speed(step*1000.0/(tsTo-tsFrom), step);
      ui.message.set("Game over!");
      ui.ai.current && ui.ai.current.cleanup();
    }
    document.getElementById("finish").addEventListener("click", ui.action.finish);

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

    ui.ai = {};
    ui.ai.current = null;
    ui.ai.algList = {
      "ai-blind-random": function() {
        return new ai.blindRandom(ui.brdEngine);
      },
      "ai-blind-weight-random": function(aiDom) {
        var cfg = {};
        cfg.left = aiDom.querySelectorAll("input[name='left']")[0].value;
        cfg.right = aiDom.querySelectorAll("input[name='right']")[0].value;
        cfg.up = aiDom.querySelectorAll("input[name='up']")[0].value;
        cfg.down = aiDom.querySelectorAll("input[name='down']")[0].value;
        return new ai.blindWeightRandom(ui.brdEngine);
      },
      "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-next-max-score": function() {
        return new ai.nextMaxScore(ui.brdEngine);
      },
      "ai-next-max-value": function() {
        return new ai.nextMaxValue(ui.brdEngine);
      },
      "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() {
        return new ai.expectimax(ui.brdEngine);
      },
      // "": 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);
      });
    }

    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.brdEngine = BoardArr2d; // TODO make user selectable

    ui.action.start();

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