le monde de crabs

Un fichier HTML5-CSS3-vanilla JS pour un 2048

28/06/2015

Attention Vieux navigateur passe ton chemin... Attention

Le chalenge...

Afin d'approfondir mes connaissances sur le HTML5, le CSS3 et le javascript, voici les quelques règles du jeu que je me suis donné pour la réalisation d'un 2048 :

  • tout dans un fichier HTML5,
  • ne fonctionne qu'avec des navigateurs modernes,
  • jouable au clavier,
  • jouable en tactile,
  • avec la possibilité d'annuler quelques mouvements (on sait jamais avec le tactile ),
  • moins de 16kio en restant « human-readable », avec la license et en passant sous jslint (hormis une indentation avec 2 espaces).

Et voici ma version du 2048.

Point d'intérêt

je vais mettre en exergue quelques éléments qui n'ont pas été aboutis dès le premier jet  :

  • Le CSS commence par un « reset » ciblé.
  • Ligne 111 : la section #loose embargue une image PNG encodée en base64.
  • Ligne 141-165 : la magie des « CSS3 media queries » pour le « responsive web design (RWD) » du jeu.
  • Usage immodére des closures du javascript, le code suffisament explicite.
  • Ligne 299-446 : gestion des conséquences des déplacements sur le plateau à l'aide objet (299: moveUp, 334: moveDown, 369: moveRight et 404: moveLeft).
  • Gestion des événement clavier et celle des événements tactiles dissociés mais générant des événement communs sur le jeu.
  • Ligne 485-537 : mini gestionnaire d'événement tactile pour traduire un toucher « simple » en mouvement sur le jeu.

Mode Offline ?

Aujourd'hui le HTML5 ne permet pas de faire du « offline » sans utiliser un fichier supplémentaire, et là hors challenge.

Mais rien n'empeche de faire un fichier manifest pour rendre ce seul fichier disponible « offline » sur votre navigateur. La plus part des navigateur propose aussi de rendre accessible une page « offline » via leur menu.

Le source

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>2048 : crabs's version</title>
<!--
http://www.crabs-world.com/2048/
Copyright (c) 2014 Christophe Cazajus
The rules of the games are povided by the work of Gabriele Cirulli (2014).
  https://github.com/gabrielecirulli/2048

My work keep the license choose by Gabriele Cirulli.

The MIT License (MIT)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<style>
html, body, section, header, footer, aside, article,
div, span, div, table, td, tr {
  margin: 0;
  padding: 0;
  border: 0 ;
  border-radius: 0 ;
  font-size: 100%;
  font: inherit;
  vertical-align: baseline;
  line-height: inherit ;
  box-sizing: border-box ;
  color: inherit ;
  text-decoration: none ;
  }
button {
  font-size: 100%;
  font: inherit;
  }
#board td.v2     { background: #feba8e ; }
#board td.v4     { background: #fdccac ; }
#board td.v8     { background: #fe8ea4 ; }
#board td.v16    { background: #fdacbc ; }
#board td.v32    { background: #8efea4 ; }
#board td.v64    { background: #acfdec ; }
#board td.v128   { background: #8efee7 ; }
#board td.v256   { background: #acdcfd ; }
#board td.v512   { background: #8ed1fe ; }
#board td.v1024  { background: #acacfd ; }
#board td.v2048  { background: #8e8efe ; }
#board td.v4096  { background: #dcacfd ; }
#board td.v8192  { background: #d18efe ; }
#board td.v16384 { background: #fdacec ; }
#board td.v32768 { background: #fe8ee7 ; }

aside {
  text-align: center ;
  }
.info {
  margin: 1rem auto ;
  font-weight: bold ;
  line-height: 2;
  }
.info .rub {
  display: block;
  }
.info .label {
  display: inline-block ;
  width: 4em ;
  text-align: left ;
  }

.info .value {
  display: inline-block ;
  text-align: right ;
  width: 6em ;
  }

#board table {
  margin: 1rem auto ;
  border-collapse: separate ;
  border-spacing: 2px ;
  border-radius: 10px ;
  background: #fee5d5 ;
  border: 1px solid #fee5d5 ;
  }
#board td {
  border-radius: 6px ;
  background: white ;
  color: black ;
  width: 4rem ;
  height: 4rem ;
  text-align: center ;
  vertical-align: middle ;
  }

#loose {
  background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAABGdBTUEAALGPC/xhBQAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB94HBQgCCSTncEMAAAAdaVRYdENvbW1lbnQAAAAAAENyZWF0ZWQgd2l0aCBHSU1QZC5lBwAAABZJREFUGNNj/N/AUc+ABzAxEADDQwEAG4MCFg9hnycAAAAASUVORK5CYII=') repeat ;
  color: #e00000;
  z-index:10;
  position: absolute; top:0; bottom:0; left:0; right:0 ;
  display: none ;
  text-align: center;
  }

#loose article {
  line-height: 3;
  position: absolute;
  border: 2px solid #e00000;
  background: #f8d0d0;
  padding: 2em 4em ;
  border-radius: 8px ;
  }

button {
  vertical-align: middle ;
  line-height: 2;
  font-weight: bold ;
  border-radius: 5px ;
  margin-left: 2em ;
  margin-right: 2em ;
  }


html { font: bold 18px/1 sans-serif ; }

@media screen and (max-height: 600px) and (orientation: portrait) {
  html { font-size: 4.0vmin ; }
  #r1 { background: blue ; }
  }

@media screen and (max-height: 600px) and (orientation: landscape) {
  html { font-size: 3.8vmin ; }
  #r2 { background: blue ; }
  aside {
    position: absolute;
    top: 4rem ; right: 58% ;
    }
  button {
    margin-left: 1em ;
    margin-right: 1em ;
    }
  #board {
    position: absolute;
    top: 2rem ; left: 44% ;
    }
  #board table {
    margin: 0 ;
    }
  }

</style>
</head>
<body id="body">
<aside>
  <div class="info">
    <button id="new">New</button>
    <button id="undo">Undo</button>
  </div>
  <div class="info">
    <span class="rub"
      ><span class="label">Score: </span
      ><span class="value" id="score">0</span
    ></span>
    <span class="rub"
      ><span class="label">Moves: </span
      ><span class="value" id="move">0</span
    ></span>
    <span class="rub"
      ><span class="label">Best: </span
      ><span class="value" id="best">0</span
    ></span>
  </div>
</aside>
<section id="board">
  <table>
    <tr>
      <td id="c00"></td><td id="c10"></td><td id="c20"></td><td id="c30"></td>
    </tr><tr>
      <td id="c01"></td><td id="c11"></td><td id="c21"></td><td id="c31"></td>
    </tr><tr>
      <td id="c02"></td><td id="c12"></td><td id="c22"></td><td id="c32"></td>
    </tr><tr>
      <td id="c03"></td><td id="c13"></td><td id="c23"></td><td id="c33"></td>
    </tr>
  </table>
</section>

<section id="loose">
  <article>You loose !!!<br><button id="loose_close">Close</button></article>
</section>
<script>
/*global document, console, jQuery */
/*jslint browser: true, indent: 2 */

(function (doc, win, storage) {
  "use strict";
  var tmp, x, y,   // variable multi-usage
    // VARIABLES
    best = 0,      // best score
    bs_id,         // element html for best score
    cell = [],     // element html for value
    game = null,   // board values
    history = [],  // for undo
    lock = false,  // ignore key event if true
    mv_id,         // element html for move
    mvt = 0,       // moves
    sc_id,         // element html for score
    score = 0,     // score
    // CONSTANTE
    $STORAGE = "crabs_2048",
    $UNDO = 10,
    $TOUCH_MOVE = 40, // nb de pixel pour valider un deplacement par "touch"
    // FUNCTION
    canMove,
    draw,
    eventMove,
    keyup,
    load,
    loose,
    move,
    moveDown,
    moveLeft,
    moveRight,
    moveUp,
    newGame,
    newNumber,
    touch,
    store,
    undo;

  // dessine le tableau
  draw = function () {
    for (x = 0; x <= 3; x += 1) {
      for (y = 0; y <= 3; y += 1) {
        if (game[x][y] > 0) {
          cell[x][y].innerHTML = game[x][y].toFixed(0);
          cell[x][y].className = "v" + game[x][y];
        } else {
          cell[x][y].innerHTML = "";
          cell[x][y].className = "";
        }
      }
    }
    sc_id.innerHTML = score;
    mv_id.innerHTML = mvt;
    bs_id.innerHTML = best;
  };

  loose = function () {
    var top, left, idl, aid;
    idl = doc.getElementById("loose");
    aid = idl.querySelector("article");
    idl.style.display = "block";
    top = Math.floor((idl.offsetHeight - aid.offsetHeight) / 2);
    left = Math.floor((idl.offsetWidth - aid.offsetWidth) / 2);
    aid.style.top = top + "px";
    aid.style.left = left + "px";
    lock = true;
    return false;
  };

  canMove = function () {
    // free cell ?
    for (x = 0; x <= 3; x += 1) {
      for (y = 0; y <= 3; y += 1) {
        if (game[x][y] === 0) { return true; }
      }
    }
    // horizontaly move
    for (x = 0; x < 3; x += 1) {
      for (y = 0; y <= 3; y += 1) {
        if (game[x][y] === game[x + 1][y]) { return true; }
      }
    }
    // verticaly move
    for (x = 0; x <= 3; x += 1) {
      for (y = 0; y < 3; y += 1) {
        if (game[x][y] === game[x][y + 1]) { return true; }
      }
    }
    return false;
  };

  moveUp = {
    move: function () {
      var r = false, ny;
      for (x = 0; x <= 3; x += 1) {
        for (y = 1; y <= 3; y += 1) {
          if (game[x][y] !== 0) {
            ny = y;
            while ((ny > 0) && (game[x][ny - 1] === 0)) { ny -= 1; }
            if (ny !== y) {
              game[x][ny] = game[x][y];
              game[x][y] = 0;
              r = true;
            }
          }
        }
      }
      return r;
    },
    sum: function () {
      var s = 0;
      for (x = 0; x <= 3; x += 1) {
        for (y = 0; y < 3; y += 1) {
          if (game[x][y] !== 0) {
            if (game[x][y] === game[x][y + 1]) {
              game[x][y] *= 2;
              s += game[x][y];
              game[x][y + 1] = 0;
            }
          }
        }
      }
      return s;
    }
  };

  moveDown = {
    move: function () {
      var r = false, ny;
      for (x = 0; x <= 3; x += 1) {
        for (y = 2; y >= 0; y -= 1) {
          if (game[x][y] !== 0) {
            ny = y;
            while ((ny < 3) && (game[x][ny + 1] === 0)) { ny += 1; }
            if (ny !== y) {
              game[x][ny] = game[x][y];
              game[x][y] = 0;
              r = true;
            }
          }
        }
      }
      return r;
    },
    sum: function () {
      var s = 0;
      for (x = 0; x <= 3; x += 1) {
        for (y = 3; y > 0; y -= 1) {
          if (game[x][y] !== 0) {
            if (game[x][y] === game[x][y - 1]) {
              game[x][y] *= 2;
              s += game[x][y];
              game[x][y - 1] = 0;
            }
          }
        }
      }
      return s;
    }
  };

  moveRight = {
    move: function () {
      var r = false, nx;
      for (y = 0; y <= 3; y += 1) {
        for (x = 2; x >= 0; x -= 1) {
          if (game[x][y] !== 0) {
            nx = x;
            while ((nx < 3) && (game[nx + 1][y] === 0)) { nx += 1; }
            if (nx !== x) {
              game[nx][y] = game[x][y];
              game[x][y] = 0;
              r = true;
            }
          }
        }
      }
      return r;
    },
    sum: function () {
      var s = 0;
      for (y = 0; y < 4; y += 1) {
        for (x = 3; x > 0; x -= 1) {
          if (game[x][y] !== 0) {
            if (game[x][y] === game[x - 1][y]) {
              game[x][y] *= 2;
              s += game[x][y];
              game[x - 1][y] = 0;
            }
          }
        }
      }
      return s;
    }
  };

  moveLeft = {
    move: function () {
      var r = false, nx;
      for (y = 0; y <= 3; y += 1) {
        for (x = 1; x <= 3; x += 1) {
          if (game[x][y] !== 0) {
            nx = x;
            while ((nx > 0) && (game[nx - 1][y] === 0)) { nx -= 1; }
            if (nx !== x) {
              game[nx][y] = game[x][y];
              game[x][y] = 0;
              r = true;
            }
          }
        }
      }
      return r;
    },
    sum: function () {
      var s = 0;
      for (y = 0; y <= 3; y += 1) {
        for (x = 0; x < 3; x += 1) {
          if (game[x][y] !== 0) {
            if (game[x][y] === game[x + 1][y]) {
              game[x][y] *= 2;
              s += game[x][y];
              game[x + 1][y] = 0;
            }
          }
        }
      }
      return s;
    }
  };

  move = function (direction) {
    var ret = { st: false, s: 0 };
    if (direction.move() === true) { ret.st = true; }
    ret.s = direction.sum();
    if (ret.s > 0) { ret.st = true; }
    if (direction.move() === true) { ret.st = true; }
    return ret;
  };

  newNumber = function () {
    var libre = [], n = 0, c;
    for (x = 0; x <= 3; x += 1) {
      for (y = 0; y <= 3; y += 1) {
        if (game[x][y] === 0) {
          libre[n] = { x: x, y: y };
          n += 1;
        }
      }
    }
    if (n === 0) { return loose(); }
    c = libre[Math.floor(Math.random() * n)];
    game[c.x][c.y] = Math.floor(Math.random() * 3) === 0 ? 4 : 2;
    draw();
    store();
    if (!canMove()) { return loose(); }
  };

  keyup = function (e) {
    e.stopPropagation();
    if (lock === true) { return; }
    switch (e.keyCode) {
    case 38:
      eventMove("UP");
      break;
    case 40:
      eventMove("DOWN");
      break;
    case 37:
      eventMove("LEFT");
      break;
    case 39:
      eventMove("RIGHT");
      break;
    }
  };

  touch = function (el) {
    var touchStart, touchEnd, touchMove, x0, y0, x1, y1,
      track = false;

    touchStart = function (e) {
      if (e.touches.length === 1) {
        if (e.touches[0].target.id === "undo") { return; }
        if (e.touches[0].target.id === "new") { return; }
        if (e.touches[0].target.id === "loose_close") { return; }
        e.preventDefault();
        x0 = e.touches[0].pageX;
        y0 = e.touches[0].pageY;
        track = true;
      }
    };

    touchMove = function (e) {
      if (track) {
        x1 = e.touches[0].pageX;
        y1 = e.touches[0].pageY;
        e.preventDefault();
      }
    };

    touchEnd = function (e) {
      var dx, dy;
      if (track) {
        track = false;
        dx = x1 - x0;
        dy = y1 - y0;
        if ((Math.abs(dx) > $TOUCH_MOVE) || (Math.abs(dy) > $TOUCH_MOVE)) {
          if (Math.abs(dx) > Math.abs(dy)) {
            if (Math.abs(dy / dx) < 0.7) {
              if (dx > 0) {
                eventMove("RIGHT");
              } else {
                eventMove("LEFT");
              }
            }
          } else {
            if (Math.abs(dx / dy) < 0.7) {
              if (dy > 0) {
                eventMove("DOWN");
              } else {
                eventMove("UP");
              }
            }
          }
        }
        track = false;
        e.preventDefault();
      }
    };
    el.addEventListener("touchstart", touchStart, false);
    el.addEventListener("touchmove", touchMove, false);
    el.addEventListener("touchend", touchEnd, false);
  };

  eventMove = function (mv) {
    var ret = { st: false },
      before = JSON.stringify({game: game, score: score, mvt: mvt, best: best});

    switch (mv) {
    case "UP":
      ret = move(moveUp);
      break;
    case "DOWN":
      ret = move(moveDown);
      break;
    case "LEFT":
      ret = move(moveLeft);
      break;
    case "RIGHT":
      ret = move(moveRight);
      break;
    }
    if (ret.st === true) {
      if (history.length >= $UNDO) { history.shift(); }
      history.push(before);
      score += ret.s;
      if (best < score) { best = score; }
      mvt += 1;
      draw();
      setTimeout(newNumber, 300);
    }
  };

  undo = function () {
    var last;
    if (history.length > 0) {
      last = JSON.parse(history.pop());
      game = last.game;
      score = last.score;
      mvt = last.mvt;
      best = last.best;
      draw(); // store in history is after newNumber
    }
  };

  newGame = function () {
    game = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]];
    score = 0;
    mvt = 0;
    draw();
    setTimeout(newNumber, 300);
  };

  load = function () {
    var obj, str = storage.getItem($STORAGE);
    if (str === null) { return false; }
    obj = JSON.parse(str);
    game = obj.game;
    score = obj.score || 0;
    mvt = obj.mvt || 0;
    best = obj.best || 0;
    return true;
  };

  store = function () {
    var obj = { game: game, score: score, mvt: mvt, best: best },
      str = JSON.stringify(obj);
    storage.setItem($STORAGE, str);
  };

  // find id
  for (x = 0; x <= 3; x += 1) {
    tmp = [];
    for (y = 0; y <= 3; y += 1) {
      tmp[y] = doc.getElementById("c" + x + y);
    }
    cell[x] = tmp;
  }
  sc_id = doc.getElementById("score");
  mv_id = doc.getElementById("move");
  bs_id = doc.getElementById("best");

  // add events listeners
  win.addEventListener("keyup", keyup, false);
  touch(win);
  doc.getElementById("loose_close").addEventListener("click", function () {
    doc.getElementById("loose").style.display = "none";
    lock = false;
  }, false);

  doc.getElementById("new").addEventListener("click", function () {
    newGame();
  }, false);

  doc.getElementById("undo").addEventListener("click", function () {
    undo();
  }, false);

  // load last game
  if (load()) {
    draw();
    if (!canMove()) { return loose(); }
  } else {
    newGame();
  }

}(document, window, window.localStorage));
</script>
</body>
</html>