Last active
October 6, 2018 20:03
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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
demo: https://rawgit.com/bellbind/e1b726100c4ab76c8b9f/raw/2048.html
(old version: https://dl.dropboxusercontent.com/u/14499563/2048/2048.html )