<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Tile game, redux</title>
<link rel="icon" type="image/gif" href="/images/favicon.png" />
<link rel="stylesheet" type="text/css" href="/css" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="viewport" content="width=400, height=500" />
<style type="text/css">
* { touch-action: manipulation; }
html { display: table; width: 100%; height: 100%; }
body {
display: table-cell;
vertical-align: middle;
font-size: 2.5em;
line-height: 2.5em;
background: #000;
color: #000;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
}
table {
width: auto;
/*width: 400px;*/
/*height: 400px;*/
margin: auto;
border: thick ridge #fff;
border-collapse: separate;
border-spacing: 0;
background: #555;
padding: 4px;
cursor: default;
-webkit-text-stroke: 2px white;
-moz-text-stroke: 2px white;
-ms-text-stroke: 2px white;
-o-text-stroke: 2px white;
text-stroke: 2px white;
}
h1, h2, h3 { font-size: .2em; color: #ccc; border: none; padding: 0; margin: auto; }
td {
background-position: -2px -2px;
font-weight: bold;
border: 2px outset #fff;
width: 98px;
height: 98px;
text-align: center;
vertical-align: middle;
-webkit-animation: .1s linear;
-moz-animation: .1s linear;
-ms-animation: .1s linear;
-o-animation: .1s linear;
animation: .1s linear;
}
#moveup {
-webkit-animation-name: moveup;
-moz-animation-name: moveup;
-ms-animation-name: moveup;
-o-animation-name: moveup;
animation-name: moveup;
}
@-webkit-keyframes moveup { to { -webkit-transform: translatey(-98px); } }
@-moz-keyframes moveup { to { -moz-transform: translatey(-98px); } }
@-ms-keyframes moveup { to { -ms-transform: translatey(-98px); } }
@-o-keyframes moveup { to { -o-transform: translatey(-98px); } }
@keyframes moveup { to { transform: translatey(-98px); } }
#movedown {
-webkit-animation-name: movedown;
-moz-animation-name: movedown;
-ms-animation-name: movedown;
-o-animation-name: movedown;
animation-name: movedown;
}
@-webkit-keyframes movedown { to { -webkit-transform: translatey(98px); } }
@-moz-keyframes movedown { to { -moz-transform: translatey(98px); } }
@-ms-keyframes movedown { to { -ms-transform: translatey(98px); } }
@-o-keyframes movedown { to { -o-transform: translatey(98px); } }
@keyframes movedown { to { transform: translatey(98px); } }
#moveleft {
-webkit-animation-name: moveleft;
-moz-animation-name: moveleft;
-ms-animation-name: moveleft;
-o-animation-name: moveleft;
animation-name: moveleft;
}
@-webkit-keyframes moveleft { to { -webkit-transform: translatex(-98px); } }
@-moz-keyframes moveleft { to { -moz-transform: translatex(-98px); } }
@-ms-keyframes moveleft { to { -ms-transform: translatex(-98px); } }
@-o-keyframes moveleft { to { -o-transform: translatex(-98px); } }
@keyframes moveleft { to { transform: translatex(-98px); } }
#moveright {
-webkit-animation-name: moveright;
-moz-animation-name: moveright;
-ms-animation-name: moveright;
-o-animation-name: moveright;
animation-name: moveright;
}
@-webkit-keyframes moveright { to { -webkit-transform: translatex(98px); } }
@-moz-keyframes moveright { to { -moz-transform: translatex(98px); } }
@-ms-keyframes moveright { to { -ms-transform: translatex(98px); } }
@-o-keyframes moveright { to { -o-transform: translatex(98px); } }
@keyframes moveright { to { transform: translatex(98px); } }
.fade {
position: relative; /* required for MSIE for some reason. */
opacity: 0;
filter: alpha(opacity = 0);
-webkit-animation: disappear 1s;
-moz-animation: disappear 1s;
-ms-animation: disappear 1s;
-o-animation: disappear 1s;
animation: disappear 1s;
}
@-webkit-keyframes disappear { from { opacity: 1; } }
@-moz-keyframes disappear { from { opacity: 1; } }
@-ms-keyframes disappear { from { opacity: 1; filter: alpha(opacity = 100); } }
@-o-keyframes disappear { from { opacity: 1; } }
@keyframes disappear { from { opacity: 1; } }
</style>
</head>
<body>
<table>
<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>
<h1>tile game v2 by Pegasus Epsilon</h1>
<h2>made for my daughter, Eternal Epsilon, built for <a href="https://google.com/chrome">Chrome</a>(tested in Chrome 31, 33, and 34)</h2>
<h3>tested working in FF26 and IE11 (although MSIE didn't support text-stroke)</h3>
<h3>too easy? try <a href="?hard">hard mode</a> - too noisy? play <a href="?quiet">without sound</a> - or <a href="?quiet&amp;hard">both</a></h3>
<audio id="click" preload="auto">
<source src="/audio/click.ogg" type="audio/ogg" />
<source src="/audio/click.mp3" type="audio/mpeg" />
</audio>
<audio id="eternal" preload="auto">
<source src="/audio/eternal.ogg" type="audio/ogg" />
<source src="/audio/eternal.mp3" type="audio/mpeg" />
</audio>
<script type="text/javascript">
/* Stop iPad overscroll */
document.body.addEventListener('touchmove', function (e) {
e.preventDefault();
});
/* disable selection */
document.body.onselectstart = function () { return false; }
/* extend Array.prototype to add .shuffle() method
* JS 1.8.5 Object.defineProperty, Chrome supported
* enumerable property not FF supported, making this useless
* Object.defineProperty not MSIE9 supported full stop
* which means windows users can't use MSIE out of the
* box in win7 and earlier, and have this work.
* Whatever, it's still the right way to add things now.
*/
Object.defineProperty(Array.prototype, "shuffle", {
enumerable: false,
value: function () {
for (var r, t, i = this.length; i--; ) {
r = Math.floor(Math.random() * i);
/* JS 1.7 destructuring syntax, Not Chrome supported.
* Fuckin' Kangaroos.
* [this[r], this[i]] = [this[i], this[r]];
*/
t = this[r];
this[r] = this[i];
this[i] = t;
}
return this;
}
});
var cells = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,15,14];
var board = document.getElementsByTagName("td");
/* preload winning image */
var eternal = new Image();
eternal.src = "/images/eternal.png";
Object.defineProperty(board, "checkWon", {
enumerable: false,
value: function () {
for (var i = cells.length; i--; ) {
if (i != cells[i]) { return false; }
}
var t = document.getElementsByTagName("table")[0]
t.style.background = "url(" + eternal.src + ")";
setTimeout((function (t) {
return function () {
t.onclick = this.start.bind(this);
}.bind(this);
}.bind(this)(t)), 1000);
for (var i = this.length; i--; ) { this[i].setAttribute("class", "fade"); }
if (!document.location.href.match(/quiet/)) {
try { document.getElementById("eternal").play(); } catch (e) { alert(e); };
}
this.transitionCallback = true;
return true;
}
});
/* updates board*/
Object.defineProperty(board, "refresh", {
enumerable: false,
value: function () {
for (var i = cells.length; i--; ) {
if (!document.location.href.match(/hard/)) { this[i].innerHTML = cells[i] + 1; }
this[i].style.background = "url(/images/tiles/"+cells[i]+".png) -2px -2px";
this[i].style.border = "2px outset #fff";
this[i].removeAttribute("class");
}
this[cells.indexOf(15)].innerHTML = "";
this[cells.indexOf(15)].style.background = this[cells.indexOf(15)].style.border = "transparent";
if (!this.checkWon() && document.location.href.match(/hard/) && 120 == this.count++) { this.start(); }
}
});
/* determines solvability */
Object.defineProperty(board, "solvable", {
enumerable: false,
value: function () {
/* probably a better way to do this, i'll have to meditate on it again... */
var check = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];
var parity = 0;
var x;
while (0 <= (x = check.indexOf(0))) {
check[x] = ++parity;
for (var y = cells[x]; y != x; y = cells[y]) { check[y] = parity; }
if (parity > 16) { return false; }
}
var d = cells.indexOf(15);
return parity % 2 == (d + Math.floor(d / 4)) % 2;
}
});
Object.defineProperty(board, "transistionCallback", { enumerable: false, value: null });
Object.defineProperty(board, "start", {
enumerable: false,
value: function () {
var t = document.getElementsByTagName("table")[0];
t.onclick = null;
t.style.background = "#555";
do { cells.shuffle(); } while (!this.solvable());
this.refresh();
this.count = 0;
return this.transitionCallback = null;
}
});
/* handles moves */
Object.defineProperty(board, "sfx", { enumerable: false, value: document.getElementById("click") });
Object.defineProperty(board, "move", {
enumerable: false,
value: function (s, d, a) {
if (this.transitionCallback) { return; }
this.transitionCallback = (function () {
var t = cells[s];
cells[s] = cells[d];
cells[d] = t;
this.removeEventListener("webkitAnimationEnd", board.transitionCallback, false);
this.removeEventListener("oAnimationEnd", board.transitionCallback, false); /* ? */
this.removeEventListener("animationend", board.transitionCallback, false);
this.removeAttribute("id");
board.transitionCallback = null;
board.refresh();
}.bind(this[s]));
if (!document.location.href.match(/quiet/)) {
try { this.sfx.pause(); if (this.sfx.currentTime) this.sfx.currentTime = 0; this.sfx.play(); } catch (e) { alert(e); };
}
this[s].addEventListener("webkitAnimationEnd", this.transitionCallback, false);
this[s].addEventListener("oAnimationEnd", this.transitionCallback, false); /* ? */
this[s].addEventListener("animationend", this.transitionCallback, false);
this[s].setAttribute("id", a);
}
});
/* handles clicks */
// why does this defineProperty break things?
//Object.defineProperty(board, "pushCallback", { enumerable: false });
Object.defineProperty(board, "click", {
enumerable: false,
value: function (i) {
var b = cells.indexOf(15);
/*
HOW TO ALGORITHM:
Step one: make it work to begin to understand
// stupid amounts of instructions, no library calls, no memory
var tgt;
if (i - 1 == b) { tgt = 4 == i || 8 == i || 12 == i ? -2 : i; }
else if (i - 2 == b) { tgt = (
5 == i || 9 == i || 13 == i ||
4 == i || 8 == i || 12 == i
) ? -3 : i - 1; }
else if (i - 3 == b) { tgt = (
6 == i || 10 == i || 14 == i ||
5 == i || 9 == i || 13 == i ||
4 == i || 8 == i || 12 == i
) ? -4 : i - 2; }
else if (i - 4 == b) { tgt = i; }
else if (i - 8 == b) { tgt = i - 4; }
else if (i - 12 == b) { tgt = i - 8; }
else if (i + 1 == b) { tgt = 3 == i || 7 == i || 11 == i ? -2 : i; }
else if (i + 2 == b) { tgt = (
3 == i || 7 == i || 11 == i ||
2 == i || 6 == i || 10 == i
) ? -3 : i + 1; }
else if (i + 3 == b) { tgt = (
1 == i || 5 == i || 9 == i ||
3 == i || 7 == i || 11 == i ||
2 == i || 6 == i || 10 == i
) ? -4 : i + 2; }
else if (i + 4 == b) { tgt = i; }
else if (i + 8 == b) { tgt = i + 4; }
else if (i + 12 == b) { tgt = i + 8; }
else { tgt = -1; }
Step two: waste memory to find patterns
// 3 instructions (mul, add, xlatb), no library calls, lots of memory
var tgt = [
[-1, 0, 1, 2, 0,-1,-1,-1, 4,-1,-1,-1, 8,-1,-1,-1],
[ 1,-1, 1, 2,-4, 1,-1,-1,-1, 5,-1,-1,-1, 9,-1,-1],
[ 1, 2,-1, 2,-3,-4, 2,-1,-1,-1, 6,-1,-1,-1,10,-1],
[ 1, 2, 3,-1,-2,-3,-4, 3,-1,-1,-1, 7,-1,-1,-1,11],
[ 4,-4,-3,-2,-1, 4, 5, 6, 4,-1,-1,-1, 8,-1,-1,-1],
[-1, 5,-4,-3, 5,-1, 5, 6,-4, 5,-1,-1,-1, 9,-1,-1],
[-1,-1, 6,-4, 5, 6,-1, 6,-3,-4, 6,-1,-1,-1,10,-1],
[-1,-1,-1, 7, 5, 6, 7,-1,-2,-3,-4, 7,-1,-1,-1,11],
[ 4,-1,-1,-1, 8,-4,-3,-2,-1, 8, 9,10, 8,-1,-1,-1],
[-1, 5,-1,-1,-1, 9,-4,-3, 9,-1, 9,10,-4, 9,-1,-1],
[-1,-1, 6,-1,-1,-1,10,-4, 9,10,-1,10,-3,-4,10,-1],
[-1,-1,-1, 7,-1,-1,-1,11, 9,10,11,-1,-2,-3,-4,11],
[ 4,-1,-1,-1, 8,-1,-1,-1,12,-4,-3,-2,-1,12,13,14],
[-1, 5,-1,-1,-1, 9,-1,-1,-1,13,-4,-3,13,-1,13,14],
[-1,-1, 6,-1,-1,-1,10,-1,-1,-1,14,-4,13,14,-1,14],
[-1,-1,-1, 7,-1,-1,-1,11,-1,-1,-1,15,13,14,15,-1]
][i][b];
Step three: invent algorithm, and debug
// 13 instructions, one library call (same arg twice, cacheable), not much memory
var tgt = [-1, 0, 1, 2, 0, -1, -1, -1, 4, -1, -1, -1, 8, -1, -1, -1][Math.abs(b - i)];
tgt = tgt >= 0 && (Math.abs(b - i) > 3 || i >> 2 == b >> 2) ? (b > i ? tgt : -tgt) + i : -1;
Step four: break it all down and do it again until you've got a proper algorithm
*/
// 19 instructions, no library calls, no memory
// j* is faster (usually) than imul
// cmp is faster (barely) than sub
var p = b - i; // used 3x
var q = (0 > p) - (0 < p); // used 3x
var d = p & 3 ? 0 : (q << 1) + q; // used 2x
var tgt = (i != b && (i >> 2) == (b >> 2) | !!d) ? (b + d + q) : -1;
if (0 > tgt) { return; }
if (i == tgt) {
if (i - 4 == b) { return this.move(i, b, "moveup"); }
if (i + 4 == b) { return this.move(i, b, "movedown"); }
if (i - 1 == b) { return this.move(i, b, "moveleft"); }
if (i + 1 == b) { return this.move(i, b, "moveright"); }
}
// why can this not be a tail call?
this.click(tgt);
this.pushCallback = (function (i, tgt) {
return (function () {
this[tgt].removeEventListener("webkitAnimationEnd", this.pushCallback, false);
this[tgt].removeEventListener("oAnimationEnd", this.pushCallback, false); /* ? */
this[tgt].removeEventListener("animationend", this.pushCallback, false);
this.pushCallback = null;
return this.click(i);
}.bind(this));
}.bind(this)(i, tgt));
this[tgt].addEventListener("webkitAnimationEnd", this.pushCallback, false);
this[tgt].addEventListener("oAnimationEnd", this.pushCallback, false); /* ? */
this[tgt].addEventListener("animationend", this.pushCallback, false);
}
});
/* set event handlers */
/* probably switch to addEventListener one day */
/* this uses a self-replacing handler to preload the sounds on first click */
/* to work around intentional crippling in iOS */
for (var x = board.length; x--; ) {
board[x].onmousedown = (function (x) {
return function () {
console.log("click: "+x);
board.click(x);
document.getElementById("eternal").load();
for (var y = board.length; y--; ) {
board[y].onmousedown = (function (y) {
return function () {
board.click(y);
}
}(y));
}
};
}(x));
}
board.start();
</script>
</body>
</html>