Skip to content

Instantly share code, notes, and snippets.

@bellbind
Last active October 6, 2018 20:03
Show Gist options
  • Save bellbind/e1b726100c4ab76c8b9f to your computer and use it in GitHub Desktop.
Save bellbind/e1b726100c4ab76c8b9f to your computer and use it in GitHub Desktop.
[javascript][promise][css3]2048 game from scratch with CSS3 transition and ES6 Promise
window.addEventListener("load", function () {
"use strict";
// input
var getCurrentStage = function () {
var cells = [0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0];
var cellNodes =
document.querySelectorAll(".g2048-container .g2048-cell");
Array.prototype.forEach.call(cellNodes, function (cell) {
cells[cell.u + cell.v * 4] = cell.n;
});
return cells;
};
/// output
var spawnAction = function (move) {
//console.log(move);
var keyCodes = {
left: 37, right: 39, up: 38, down: 40,
};
var keyCode = keyCodes[move];
var event = document.createEvent("KeyboardEvent");
if (event.initKeyEvent) {
// for firefox
event.initKeyEvent("keydown", true, true, window,
0, 0, 0, 0, keyCode, 0);
} else if (event.initKeyboardEvent) {
// for chrome
event.initKeyboardEvent("keydown", true, true, window,
move, 0, "", false, "");
Object.defineProperty(event, "keyCode", {
get: function () {return keyCode;}});
}
window.dispatchEvent(event);
};
// 2048 system
var dirs = [
{
move: "left",
index: function (lane, i) {return 4 * lane + i;}
},
{
move: "right",
index: function (lane, i) {return 4 * lane + (3 - i);}
},
{
move: "up",
index: function (lane, i) {return 4 * i + lane;}
},
{
move: "down",
index: function (lane, i) {return 4 * (3 - i) + lane;}
},
];
var isGameOver = function (cells) {
if (cells.some(function (n) {return n === 0;})) return false;
for (var lane = 0; lane < 4; lane++) {
for (var i = 0; i < 3; i++) {
var left = cells[dirs[0].index(lane, i)];
var right = cells[dirs[0].index(lane, i + 1)];
var up = cells[dirs[2].index(lane, i)];
var down = cells[dirs[2].index(lane, i + 1)];
if (left === right || up === down) return false;
}
}
return true;
};
var slide = function (cells, dir) {
var moved = false;
for (var lane = 0; lane < 4; lane++) {
for (var i = 0; i < 4; i++) {
if (shift(cells, dir, lane, i)) moved = true;
if (merge(cells, dir, lane, i)) moved = true;
}
}
return moved;
};
var sibling = function (cells, dir, lane, i) {
for (var j = i + 1; j < 4; j++) {
var target = dir.index(lane, j);
if (cells[target] !== 0) return target;
}
return -1;
};
var shift = function (cells, dir, lane, i) {
var cur = dir.index(lane, i);
if (cells[cur] !== 0) return false;
var target = sibling(cells, dir, lane, i);
if (target < 0) return false;
cells[cur] = cells[target];
cells[target] = 0;
return true;
};
var merge = function (cells, dir, lane, i) {
var cur = dir.index(lane, i);
if (cells[cur] === 0) return false;
var target = sibling(cells, dir, lane, i);
if (target < 0) return false;
if (cells[target] !== cells[cur]) return false;
var n = cells[cur] * 2;
cells[cur] = n;
cells[target] = 0;
return true;
};
// -- 2048 AI --
var rot90 = function (cells) {
var ret = cells.slice();
for (var lane = 0; lane < 4; lane++) {
for (var i = 0; i < 4; i++) {
ret[4 * i + (3 - lane)] = cells[4 * lane + i];
}
}
return ret;
};
var flip = function (cells) {
var ret = cells.slice();
for (var lane = 0; lane < 4; lane++) {
for (var i = 0; i < 4; i++) {
ret[4 * lane + (3 - i)] = cells[4 * lane + i];
}
}
return ret;
};
var getWeightPatterns = function (baseWeight) {
var weights = [];
var curA = baseWeight;
var curB = flip(curA);
for (var i = 0; i < 4; i++) {
weights.push(curA);
weights.push(curB);
curA = rot90(curA);
curB = rot90(curB);
}
return weights;
};
var log2 = function (n) {
var i = 0;
for (;Math.pow(2, i) <= n; i++) {}
return i;
};
var calcScore = function (cells, weights) {
return weights.reduce(function (max, weight) {
var score = cells.reduce(function (sum, n, i) {
return sum + log2(n) * weight[i];
}, 0);
return max < score ? score : max;
}, 0);
};
var fill2 = function (cells, dir) {
var ret = cells.slice();
for (var lane = 0; lane < 4; lane++) {
for (var i = 0; i < 4; i++) {
var index = dir.index(lane, i);
if (ret[index] === 0) {
ret[index] = 2;
break;
}
}
}
return ret;
};
var guessDir = function (dir, cells, weights) {
var next = cells.slice();
if (!slide(next, dir)) return -1;
//return calcScore(next, weights);
return dirs.reduce(function (max, nextDir) {
//var next2 = next.slice();
var next2 = fill2(next, dir);
slide(next2, nextDir);
var score = calcScore(next2, weights);
return max < score ? score : max;
}, 0);
};
var guess = function (cells, weights) {
return dirs.reduce(function (max, dir) {
var cur = {dir: dir, score: guessDir(dir, cells, weights)};
//console.log(cur);
return max.score < cur.score ? cur : max;
}, {dir: dirs[0], score: 0});
};
// AI setting
var baseWeight = [
Math.pow(2, 0), Math.pow(2, 1), Math.pow(2, 2), Math.pow(2, 12),
Math.pow(2, 3), Math.pow(2, 5), Math.pow(2, 10), Math.pow(2, 14),
Math.pow(2, 4), Math.pow(2, 11), Math.pow(2, 18), Math.pow(2, 19),
Math.pow(2, 13), Math.pow(2, 15), Math.pow(2, 20), Math.pow(2, 25),
];
var weights = getWeightPatterns(baseWeight);
//console.log(weights);
// -- 2048 AI --
// UI
var id = null;
var stop = function () {
clearInterval(id);
id = null;
};
document.getElementById("ai").addEventListener("click", function () {
if (id) return stop();
id = setInterval(function () {
var stage = getCurrentStage();
if (isGameOver(stage)) {
//console.log("game over");
return stop();
}
var result = guess(stage, weights);
spawnAction(result.dir.move);
}, 500);
}, false);
}, false);
.g2048-container {
position: relative;
width: 490px;
height: 490px;
margin: 0px;
padding: 0px;
background-color: #BBADA0;
border-radius: 5px;
}
.g2048-pane {
position: absolute;
width: 110px;
height: 110px;
background-color: #E5DFD9;
border-radius: 5px;
}
.g2048-cell {
position: absolute;
width: 110px;
height: 110px;
line-height: 110px;
text-align: center;
vertical-align: middle;
border-radius: 5px;
font-family: Consolus, Courier, monospace;
transition: all 200ms ease 100ms;
}
.g2048-2 {color: #776E65; background-color: #EEE4DA; font-size: 55px;}
.g2048-4 {color: #776E65; background-color: #EDE0C8; font-size: 55px;}
.g2048-8 {color: #F9F6F2; background-color: #F2B179; font-size: 55px;}
.g2048-16 {color: #F9F6F2; background-color: #F59563; font-size: 55px;}
.g2048-32 {color: #F9F6F2; background-color: #F67C5F; font-size: 55px;}
.g2048-64 {color: #F9F6F2; background-color: #F65E3B; font-size: 55px;}
.g2048-128 {color: #F9F6F2; background-color: #EDCF72; font-size: 45px;}
.g2048-256 {color: #F9F6F2; background-color: #EDCC61; font-size: 45px;}
.g2048-512 {color: #F9F6F2; background-color: #EDC850; font-size: 45px;}
.g2048-1024 {color: #F9F6F2; background-color: #EDC53F; font-size: 35px;}
.g2048-2048 {color: #F9F6F2; background-color: #EDC22E; font-size: 35px;}
.g2048-cell-pop {
animation: g2048-pop 200ms ease 100ms 1 normal;
-webkit-animation: g2048-pop 200ms ease 100ms 1 normal;
}
@keyframes g2048-pop {
0% {padding: 0px; margin-left: 0px; margin-top: 0px;}
50% {padding: 8px; margin-left: -4px; margin-top: -4px;}
100% {padding: 0px; margin-left: 0px; margin-top: 0px;}
}
@-webkit-keyframes g2048-pop {
0% {padding: 0px; margin-left: 0px; margin-top: 0px;}
50% {padding: 8px; margin-left: -4px; margin-top: -4px;}
100% {padding: 0px; margin-left: 0px; margin-top: 0px;}
}
.g2048-score {
position: relative;
margin: 5px;
text-align: center;
width: 100px;
color: white;
background-color: #BBADA0;
border-radius: 5px;
}
.g2048-score-plus {
position: absolute;
color: black;
background-color: rgba(255,255,255,0);
top: 20px;
left: 40px;
transition: all 200ms ease 100ms;
}
.g2048-newgame {
color: white;
background-color: #BBADA0;
border-radius: 5px;
}
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>2048 from scratch with CSS3 transition and ES6 Promise</title>
<link rel="stylesheet" href="2048.css" type="text/css" />
<script src="2048.js"></script>
<script src="2048-ai.js"></script>
</head>
<body style="text-align: center">
<h1 style="display: inline-block">2048 from scratch
(<a href="https://gist.github.com/bellbind/e1b726100c4ab76c8b9f">source</a>)
</h1><br />
<div style="display: inline-block">
with CSS3 transition and ES6 Promise <br />
(both are available on firefox 30 and chrome 35)
</div><br />
<div id="score" style="display: inline-block"></div><br />
<div id="2048" style="display: inline-block"></div><br />
<button id="newgame" style="display: inline-block">new game</button>
<button id="ai" style="display: inline-block">AI toggle</button>
</body>
</html>
// 2048 game from scratch with CSS3 transition and Promise
// - avaiable for Firefox and Chrome
window.addEventListener("load", function () {
"use strict";
// helpers
var onTransEnd = function (elem, listener, msec) {
// assure listener call (transitionend event may not spawn)
var id = setTimeout(function () {
id = null;
listener({target: elem});
}, msec);
var transitionend = "transitionend";
elem.addEventListener(transitionend, function (ev) {
if (id === null) return;
clearTimeout(id);
listener(ev);
}, false);
};
var doRemove = function (ev) {
if (ev.target.parentNode) ev.target.parentNode.removeChild(ev.target);
};
// UI functions
var initStage = function (id) {
var container = document.getElementById(id);
container.classList.add("g2048-container");
for (var i = 0; i < 16; i++) {
var u = i % 4;
var v = 0|i / 4;
var pane = document.createElement("div");
pane.classList.add("g2048-pane");
container.appendChild(pane);
pane.style.left = (10 + 120 * u) + "px";
pane.style.top = (10 + 120 * v) + "px";
}
return container;
};
var newCell = function (u, v, n) {
var cell = document.createElement("div");
cell.classList.add("g2048-cell");
cell.u = u;
cell.v = v;
cell.n = n;
cell.style.left = (10 + 120 * u) + "px";
cell.style.top = (10 + 120 * v) + "px";
cell.display = function () {
var className = "g2048-" + (cell.n > 2048 ? 2048: cell.n);
cell.classList.add(className);
cell.textContent = cell.n;
};
cell.display();
return cell;
};
var move = function (cell, u, v) {
//console.log("("+cell.u+","+cell.v+")=>("+u+","+v+")");
cell.u = u;
cell.v = v;
cell.style.left = (10 + 120 * cell.u) + "px";
cell.style.top = (10 + 120 * cell.v) + "px";
};
var pop = function (cell) {
cell.classList.remove("g2048-cell-pop");
var _ = cell.offsetWidth; // hack for re-animation
cell.classList.add("g2048-cell-pop");
};
// new game button
var initNewGame = function (id) {
var newgame = document.getElementById(id);
newgame.classList.add("g2048-newgame");
return newgame;
};
// score view
var initScore = function (id, value) {
var score = document.getElementById(id);
score.classList.add("g2048-score");
score.score = value;
updateScore(score);
return score;
};
var updateScore = function (scorePane) {
scorePane.innerHTML = "score<br />" + scorePane.score;
}
var upScore = function (scorePane, value) {
var plus = document.createElement("div");
plus.classList.add("g2048-score-plus");
onTransEnd(plus, function (ev) {
if (plus.parentNode) plus.parentNode.removeChild(plus);
scorePane.score += value;
updateScore(scorePane);
}, 400);
plus.textContent = "+" + value;
scorePane.appendChild(plus);
setTimeout(function () {
plus.style.top = "0px";
}, 100);
};
// 2048 game system
var stage = initStage("2048");
var scorePane = initScore("score", 0);
var newgame = initNewGame("newgame");
var cells = [null, null, null, null,
null, null, null, null,
null, null, null, null,
null, null, null, null];
var processing = false;
var win = false;
// restart
var restart = function () {
win = false;
for (var i = 0; i < cells.length; i++) {
var cell = cells[i];
if (cell) cell.parentNode.removeChild(cell);
cells[i] = null;
}
next();
scorePane.score = 0;
updateScore(scorePane);
};
// game over check
var isGameOver = function () {
if (cells.some(function (cell) {return cell === null;})) return false;
for (var lane = 0; lane < 4; lane++) {
for (var i = 0; i < 3; i++) {
var left = cells[dirIndexes.left(lane, i)];
var right = cells[dirIndexes.left(lane, i + 1)];
var up = cells[dirIndexes.up(lane, i)];
var down = cells[dirIndexes.up(lane, i + 1)];
if (left.n === right.n || up.n === down.n) return false;
}
}
return true;
};
// redraw numbers then put new panel at randome position
var next = function () {
cells.forEach(function (cell) {
if (cell) cell.display();
});
var empties = cells.reduce(function (es, cell, index) {
if (!cell) es.push(index);
return es;
}, []);
if (empties.length === 0) return;
var index = empties[0|Math.random() * empties.length];
var value = Math.random() > 0.1 ? 2 : 4;
var cell = newCell(index % 4, 0|index / 4, value);
stage.appendChild(cell);
cells[index] = cell;
if (!win) {
if (cells.some(function (cell) {return cell && cell.n >= 2048;})) {
win = true;
return alert("You Win!");
}
}
if (isGameOver()) {
return alert("Game Over");
}
};
// for direction independent cell tracing
var dirIndexes = {
left: function (lane, i) {
return 4 * lane + i;
},
right: function (lane, i) {
return 4 * lane + (3 - i);
},
up: function (lane, i) {
return 4 * i + lane;
},
down: function (lane, i) {
return 4 * (3 - i) + lane;
},
};
// slide the stage
var slide = function (dirIndex) {
processing = true;
var promises = [];
var got = 0;
for (var lane = 0; lane < 4; lane++) {
for (var i = 0; i < 4; i++) {
// for each cell positions
shift(dirIndex, lane, i, promises);
got += merge(dirIndex, lane, i, promises);
}
}
if (got > 0) upScore(scorePane, got);
if (promises.length > 0) {
Promise.all(promises).then(next, function () {}).then(function () {
processing = false;
});
} else processing = false;
}
var nextCell = function (dirIndex, lane, i) {
for (var j = i + 1; j < 4; j++) {
var target = dirIndex(lane, j);
if (cells[target] !== null) {
return {cell: cells[target], index: target};
}
}
return null;
};
// 1. shift the left most cell
var shift = function (dirIndex, lane, i, promises) {
var cur = dirIndex(lane, i)
if (cells[cur] !== null) return false;
var target = nextCell(dirIndex, lane, i);
if (!target) return false;
cells[target.index] = null;
cells[cur] = target.cell;
promises.push(new Promise(function (fulfill, reject) {
onTransEnd(target.cell, fulfill, 400);
}));
move(target.cell, cur % 4, 0|cur / 4);
return true;
};
// 2. search right sibling and merge if having same value
var merge = function (dirIndex, lane, i, promises) {
var cur = dirIndex(lane, i)
if (cells[cur] === null) return 0;
var target = nextCell(dirIndex, lane, i);
if (!target) return 0;
if (target.cell.n !== cells[cur].n) return 0;
cells[target.index] = null;
var n = cells[cur].n * 2;
cells[cur].n = n;
pop(cells[cur]);
onTransEnd(target.cell, doRemove, 400);
promises.push(new Promise(function (fulfill, reject) {
onTransEnd(target.cell, fulfill, 400);
}));
move(target.cell, cur % 4, 0|cur / 4);
return n;
};
// Event handling and start game
newgame.addEventListener("mousedown", restart, false);
window.addEventListener("keydown", function (ev) {
ev.preventDefault();
if (processing) return;
var keyCode = ev.keyCode;
switch (keyCode) {
case 37:/*left*/ return slide(dirIndexes.left);
case 39:/*right*/ return slide(dirIndexes.right);
case 38:/*up*/ return slide(dirIndexes.up);
case 40:/*down*/ return slide(dirIndexes.down);
}
}, false);
var finger = {id: null, x: 0, y: 0};
window.addEventListener("touchstart", function (ev) {
// limit only single swipe
if (ev.touches.length !== 1) {
finger.id = null;
return;
}
ev.preventDefault(), ev.stopPropagation();
var touch = ev.changedTouches[0];
finger.id = touch.identifier;
finger.x = touch.screenX, finger.y = touch.screenY;
}, false);
window.addEventListener("touchend", function (ev) {
if (ev.changedTouches.length !== 1 || ev.touches.length !== 0 ||
finger.id === null) return;
var touch = ev.changedTouches[0];
if (finger.id !== touch.identifier) return;
ev.preventDefault(), ev.stopPropagation();
finger.id = null;
var x = touch.screenX - finger.x, y = touch.screenY - finger.y;
var dist = Math.sqrt(x * x + y * y);
if (dist < 120) return;
var angle = Math.atan2(y, x);
var deg180 = Math.PI, deg90 = Math.PI / 2, deg30 = Math.PI / 6;
if (-deg30 <= angle && angle <= deg30)
return slide(dirIndexes.right);
if (deg90 - deg30 <= angle && angle <= deg90 + deg30)
return slide(dirIndexes.down);
if (-deg90 - deg30 <= angle && angle <= -deg90 + deg30)
return slide(dirIndexes.up);
if (deg180 - deg30 <= angle || angle <= -deg180 + deg30)
return slide(dirIndexes.left);
}, false);
next();
}, false);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment