2048.html
author Oleksandr Gavenko <gavenkoa@gmail.com>
Sat, 06 Sep 2014 22:58:53 +0300
changeset 7 d8106c8c0481
parent 4 732aef931a9e
child 10 70ece7f758a0
permissions -rw-r--r--
Rename methods and use sequential numbers instead of power of 2.

<!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="board.js"></script>
  <script src="perf.js"></script>

  <style>
    body {
      width: 100%;
    }
    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;
    }
    .report > .name {
      font-weight: bold;
    }
  </style>
</head>
<body>

  <h1>2048</h1>

  <div id="score-area" class="area">Score: <span id="score">0</span>, Max: <span id="max">0</span>, Speed: <span id="speed">0</span> t/s, Turn: <span id="turn">0</span></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>
      <button id="step">Step</button>
      <button id="loop">Loop</button>
      <button id="finish">Finish</button>
    </div>
    <div>
      <button id="left">left</button>
      <button id="up">up</button>
      <button id="down">down</button>
      <button id="right">right</button>
    </div>
    <h1>AI</h1>
    <div>
      <button id="ai-random">random</button>
      <button id="ai-next-max-score">next max score</button>
      <button id="ai-next-max-value">next max value</button>
    </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 board = {};
    board.create = function() {
      var brd = [];
      for (var i = 0; i < 4; i++) {
        brd[i] = [];
        for (var j = 0; j < 4; j++) {
          brd[i][j] = 0;
        }
      }
      return brd;
    }
    board.copy = function(from, to) {
      for (var i = 0; i < 4; i++) {
        for (var j = 0; j < 4; j++) {
          to[i][j] = from[i][j];
        }
      }
    }
    board.freeCnt = function(brd) {
      var cnt = 0;
      for (var i = 0; i < 4; i++) {
        for (var j = 0; j < 4; j++) {
          if (brd[i][j] === 0)
            cnt++;
        }
      }
      return cnt;
    }
    board.gameOver = function(brd) {
      if (board.freeCnt(brd) > 0)
        return false;
      for (var i = 0; i < 4; i++) {
        for (var j = 0; j < 3; j++) {
          if (brd[i][j] === brd[i][j+1])
            return false;
        }
      }
      for (var j = 0; j < 4; j++) {
        for (var i = 0; i < 3; i++) {
          if (brd[i][j] === brd[i+1][j])
            return false;
        }
      }
      return true;
    }
    board.putRandom = function(brd) {
      var cnt = board.freeCnt(brd);
      cnt = Math.floor(Math.random() * cnt)+1;
      for (var i = 0; i < 4 && cnt > 0; i++) {
        for (var j = 0; j < 4 && cnt > 0; j++) {
          if (brd[i][j] !== 0)
            continue;
          if (cnt === 1)
            brd[i][j] = (Math.random() > .9) ? 2 : 1;
          cnt--;
        }
      }
    }
    /* http://www.reddit.com/r/2048/comments/214njx/highest_possible_score_for_2048_warning_math */
    var boardScoreTbl = [0];
    for (var i = 1, exp = 2; i < 16; i++, exp *= 2) {
      boardScoreTbl[i] = (i-1)*exp;
    }
    board.score = function(brd) {
      var score = 0;
      var max = 0;
      for (var i = 0; i < 4; i++) {
        for (var j = 0; j < 4; j++) {
          var val = brd[i][j];
          score += boardScoreTbl[val];
          if (max < val)
            max = val;
        }
      }
      return {score: score, max: Math.pow(2, max)};
    }

    board.row = {};
    board.row.init = function() {
      return {stack: [], curr: 0};
    }
    board.row.push = function(state, val) {
      if (val === 0)
        return;
      if (state.curr === 0) {
        state.curr = val;
        return;
      }
      if (state.curr === val) {
        state.stack.push(state.curr+1);
        state.curr = 0;
      } else {
        state.stack.push(state.curr);
        state.curr = val;
      }
    }
    board.row.finish = function(state) {
      if (state.curr !== 0)
        state.stack.push(state.curr);
    }
    board.move = {};
    board.move.up = function(brd) {
      var updated = false;
      for (var j = 0; j < 4; j++) {
        var state = board.row.init();
        for (var i = 0; i < 4; i++) {
          board.row.push(state, brd[i][j]);
        }
        board.row.finish(state);
        for (var i = 0; i < state.stack.length; i++) {
          if (brd[i][j] !== state.stack[i])
            updated = true;
          brd[i][j] = state.stack[i];
        }
        for (; i < 4; i++) {
          if (brd[i][j] !== 0)
            updated = true;
          brd[i][j] = 0;
        }
      }
      return updated;
    };
    board.move.down = function(brd) {
      var updated = false;
      for (var j = 0; j < 4; j++) {
        var state = board.row.init();
        for (var i = 3; i >= 0; i--) {
          board.row.push(state, brd[i][j]);
        }
        board.row.finish(state);
        for (var i = 0; i < state.stack.length; i++) {
          if (brd[3-i][j] !== state.stack[i])
            updated = true;
          brd[3-i][j] = state.stack[i];
        }
        for (; i < 4; i++) {
          if (brd[3-i][j] !== 0)
            updated = true;
          brd[3-i][j] = 0;
        }
      }
      return updated;
    };
    board.move.left = function(brd) {
      var updated = false;
      for (var i = 0; i < 4; i++) {
        var state = board.row.init();
        for (var j = 0; j < 4; j++) {
          board.row.push(state, brd[i][j]);
        }
        board.row.finish(state);
        for (var j = 0; j < state.stack.length; j++) {
          if (brd[i][j] !== state.stack[j])
            updated = true;
          brd[i][j] = state.stack[j];
        }
        for (; j < 4; j++) {
          if (brd[i][j] !== 0)
            updated = true;
          brd[i][j] = 0;
        }
      }
      return updated;
    };
    board.move.right = function(brd) {
      var updated = false;
      for (var i = 0; i < 4; i++) {
        var state = board.row.init();
        for (var j = 3; j >= 0; j--) {
          board.row.push(state, brd[i][j]);
        }
        board.row.finish(state);
        for (var j = 0; j < state.stack.length; j++) {
          if (brd[i][3-j] !== state.stack[j])
            updated = true;
          brd[i][3-j] = state.stack[j];
        }
        for (; j < 4; j++) {
          if (brd[i][3-j] !== 0)
            updated = true;
          brd[i][3-j] = 0;
        }
      }
      return updated;
    };

    var boardDom = document.getElementById("board");
    var ui = {};
    ui.board = {};
    ui.board.set = function(i, j, val) {
      boardDom.querySelectorAll("tr")[i].querySelectorAll("td")[j].innerHTML = val;
    }
    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] >= 1) ? brd[i][j] : "");
        }
      }
    }
    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;
    }

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

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

    document.body.addEventListener("keydown", function(event) {
      var key = event.keyCode || event.which;
      switch (key) {
          case 38: up(); break;
          case 40: down(); break;
          case 37: left(); break;
          case 39: right(); break;
      }
      return false;
    });
    
    ui.message = {};
    var messageDom = document.getElementById("message-area");
    ui.message.clear = function() {
      messageDom.innerHTML = "";
    }
    ui.message.set = function(msg) {
      messageDom.innerHTML = msg;
    }

    function step() {
      ui.message.clear();
      if (board.gameOver(board.current)) {
        ui.message.set("Game over!");
        return;
      }
      var tmpBrd = board.create();
      board.copy(board.current, tmpBrd);
      var fn = ai.current(tmpBrd);
      if (typeof fn === 'undefined') {
        ui.message.set("I don't know how to move!");
        return;
      }
      var updated = board.move[fn].call(null, board.current);
      if (updated) {
        board.putRandom(board.current);
        ui.board.update(board.current);
        ui.score.update(board.current);
      } else {
        ui.message.set("Wrong move!");
      }
    }
    document.getElementById("step").addEventListener("click", step);

    function finish() {
      ui.message.clear();
      var step = 0;
      var tsFrom = new Date().getTime();
      while (!board.gameOver(board.current)) {
        var tmpBrd = board.create();
        board.copy(board.current, tmpBrd);
        var fn = ai.current(tmpBrd);
        if (typeof fn === 'undefined') {
          ui.message.set("I don't know how to move!");
          return;
        }
        var updated = board.move[fn].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!");
    }
    document.getElementById("finish").addEventListener("click", finish);

    var ai = {};

    ai.random = function(brd) {
      var tmpBrd = board.create();
      do {
        var action = ["up", "down", "left", "right"][Math.floor(Math.random()*4)];
        board.copy(brd, tmpBrd);
      } while (!board.move[action](tmpBrd));
      return action;
    }
    document.getElementById("ai-random").addEventListener("click", function() {
      ai.current = ai.random;
    });

    ai.nextMaxScore = function(brd) {
      var tmpBrd = board.create();
      board.copy(brd, tmpBrd);
      var maxScore = -1;
      var action;
      if (board.move.up(tmpBrd)) {
        maxScore = board.score(tmpBrd).score;
        action = "up";
      }
      board.copy(brd, tmpBrd);
      if (board.move.left(tmpBrd)) {
        var score = board.score(tmpBrd).score;
        if (maxScore < score) {
          action = "left";
          maxScore = score;
        }
      }
      board.copy(brd, tmpBrd);
      if (board.move.down(tmpBrd)) {
        var score = board.score(tmpBrd).score;
        if (maxScore < score) {
          action = "down";
          maxScore = score;
        }
      }
      board.copy(brd, tmpBrd);
      if (board.move.right(tmpBrd)) {
        var score = board.score(tmpBrd).score;
        if (maxScore < score) {
          action = "right";
          maxScore = score;
        }
      }
      return action;
    }
    document.getElementById("ai-next-max-score").addEventListener("click", function() {
      ai.current = ai.nextMaxScore;
    });


    ai.nextMaxValue = function(brd) {
      var tmpBrd = board.create();
      board.copy(brd, tmpBrd);
      var maxMax = -1;
      var action;
      if (board.move.up(tmpBrd)) {
        maxMax = board.score(tmpBrd).max;
        action = "up";
      }
      board.copy(brd, tmpBrd);
      if (board.move.left(tmpBrd)) {
        var max = board.score(tmpBrd).max;
        if (maxMax < max) {
          action = "left";
          maxMax = max;
        }
      }
      board.copy(brd, tmpBrd);
      if (board.move.down(tmpBrd)) {
        var max = board.score(tmpBrd).max;
        if (maxMax < max) {
          action = "down";
          maxMax = max;
        }
      }
      board.copy(brd, tmpBrd);
      if (board.move.right(tmpBrd)) {
        var max = board.score(tmpBrd).max;
        if (maxMax < max) {
          action = "right";
          maxMax = max;
        }
      }
      return action;
    }
    document.getElementById("ai-next-max-value").addEventListener("click", function() {
      ai.current = ai.nextMaxValue;
    });

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