Skip to content

Instantly share code, notes, and snippets.

@e1blue
Created July 24, 2018 03:48
Show Gist options
  • Select an option

  • Save e1blue/c3ba59b4392268a6073e5624fec69901 to your computer and use it in GitHub Desktop.

Select an option

Save e1blue/c3ba59b4392268a6073e5624fec69901 to your computer and use it in GitHub Desktop.
Klotski{15-Pizzle}
#elGame
#elTime .
#elStatus Swipe to start
// init
updateBoardDim(elGame);
// game states
let startTime = -1;
let gameStarted = false;
let blks = [];
renew();
setInterval(()=> {
if (gameStarted) {
const timeUsed = (Date.now() - startTime)/1000;
elTime.textContent = timeUsed.toFixed(1) + "s";
}
}, 100);
function onDone() {
gameStarted = false;
elStatus.innerHTML = `(Solved)`;
}
function onStart() {
gameStarted = true;
startTime = Date.now();
elStatus.textContent = "(solving)";
}
function renew() {
elGame.innerHTML = "";
blks = buildBlocks(elGame);
solvableShuffle(blks);
regEvents(elGame, blks);
append(elGame, blks);
gameStarted = false;
startTime = -1;
}
//
// functions
//
function updateBoardDim(el) {
const sf = 2/3;
const s = Math.min(innerWidth, innerHeight);
const mag = (s * sf) - (s * sf)%4;
el.style.width = `${mag}px`;
el.style.height = `${mag}px`;
}
function buildBlocks(el) {
const blks = [];
for (let i = 0; i < 16; i++) {
const blk = document.createElement("div");
blk.classList.add("block");
blk.$id = i;
blks.push(blk);
}
return blks;
}
/* DEPRECATED
This method may produce unsolvable puzzle
Ie.
1 2 3 4
5 6 7 8
9 10 11 12
13 15 14 _
*/
function shuffle(arr) {
let n = arr.length;
let i;
while (n) {
const i = Math.random() * n | 0;
const tmp = arr[i];
arr[i] = arr[n-1];
arr[n-1] = tmp;
n--;
}
return arr;
}
/*
Solvable shuffle by moving empty blk up, down, left or right N times.
*/
function solvableShuffle(arr) {
const randomCount = 1000;
const ids = [
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
[13, 14, 15, 0]
];
function swap(ax,ay,bx,by) {
if (bx < 0 || bx > 3 || by < 0 || by > 3)
return [ax, ay]; // the pos of empty blk
const tmp = ids[ay][ax];
ids[ay][ax] = ids[by][bx];
ids[by][bx] = tmp;
return [bx, by]; // new pos of empty blk
}
function up(ax,ay) { return swap(ax, ay, ax, ay-1) }
function down(ax,ay) { return swap(ax, ay, ax, ay+1) }
function left(ax,ay) { return swap(ax, ay, ax-1, ay) }
function right(ax,ay) { return swap(ax, ay, ax+1, ay) }
let i = 0;
let emptyBlkPos = [3, 3];
while( i++ < randomCount ) {
const mov = Math.random() * 4 | 0;
switch (mov) {
case 0: emptyBlkPos = up(...emptyBlkPos); break;
case 1: emptyBlkPos = down(...emptyBlkPos); break;
case 2: emptyBlkPos = left(...emptyBlkPos); break;
case 3: emptyBlkPos = right(...emptyBlkPos); break;
}
}
for (let i = 0; i < 4; i++) {
for(let j = 0; j < 4; j++) {
arr[i * 4 + j].$id = ids[i][j];
}
}
return arr;
}
function append(el, blks) {
for (const [i, blk] of blks.entries()) {
const y = i / 4 | 0;
const x = i % 4;
blk.style.left = `${x*25}%`;
blk.style.top = `${y*25}%`;
blk.textContent = blk.$id;
if (blk.$id === 0)
blk.classList.add("empty");
el.appendChild(blk);
}
}
function regEvents(el, blks) {
const TH = 20; //threshold in px, larger value, less sensitive
el.$downAt = {x:0, y:0};
el.$activeBlk = null; //mouse down on which blk
el.$movement = {left:false, right:false, up:false, down:false};
function onCancel(e) {
if (/touch/.test(e.type)) {
e.preventDefault();
}
el.$activeBlk = null;
}
document.body.addEventListener("mouseup", onCancel);
document.body.addEventListener("touchend", onCancel);
function onSelect(e) {
// In ff touchevent emu, it fires "mousedown" even if "preventDefault" when
// I hold down the mouse without moving then release after ~1s.
// Chrome does not have this issue.
console.log(e.type);
let ev = e;
if (/touch/.test(e.type)) {
e.preventDefault();
ev = e.targetTouches[0];
}
el.$activeBlk = ev.target;
el.$downAt.x = ev.clientX;
el.$downAt.y = ev.clientY;
}
function onMove(e) {
let ev = e;
if (/touch/.test(e.type)) {
e.preventDefault();
ev = e.targetTouches[0];
}
if (el.$activeBlk !== null) {
const ox = ev.clientX - el.$downAt.x;
const oy = ev.clientY - el.$downAt.y;
const mv = el.$movement;
if (ox > TH) mv.right = true;
else if (ox < -TH) mv.left = true;
if (oy > TH) mv.down = true;
else if (oy < -TH) mv.up = true;
handleMovement(el, blks);
}
}
for (const blk of blks) {
blk.addEventListener("mousedown", onSelect);
blk.addEventListener("mousemove", onMove);
blk.addEventListener("touchstart", onSelect);//passive?
blk.addEventListener("touchmove", onMove);//?
}
}
function handleMovement(el, blks) {
const blk = el.$activeBlk;
if (blk == null) {
return;
}
const blkPos = toBoardSpace(blk);
const emptyBlk = blks.find(b => b.$id == 0);
const emptyBlkPos = toBoardSpace(emptyBlk);
if (el.$movement.left && emptyBlkPos.x == blkPos.x - 1 && emptyBlkPos.y == blkPos.y ||
el.$movement.right && emptyBlkPos.x == blkPos.x + 1 && emptyBlkPos.y == blkPos.y ||
el.$movement.up && emptyBlkPos.y == blkPos.y - 1 && emptyBlkPos.x == blkPos.x ||
el.$movement.down && emptyBlkPos.y == blkPos.y + 1 && emptyBlkPos.x == blkPos.x) {
if (!gameStarted)
onStart();
swap(blk, emptyBlk, blks);
checkDone(blks);
}
el.$movement.left = false;
el.$movement.right = false;
el.$movement.up = false;
el.$movement.down = false;
}
function toBoardSpace(blk) {
const index = blks.indexOf(blk);
const y = index/4|0;
const x = index%4;
return {x, y};
}
function swap(blkA, blkB, blks) { // one of them is empty blk
blkA.style.left = `${toBoardSpace(blkB).x * 25}%`;
blkA.style.top = `${toBoardSpace(blkB).y * 25}%`;
blkB.style.left = `${toBoardSpace(blkA).x * 25}%`;
blkB.style.top = `${toBoardSpace(blkA).y * 25}%`;
const indexA = blks.indexOf(blkA);
const indexB = blks.indexOf(blkB);
const tmp = blks[indexA];
blks[indexA] = blks[indexB];
blks[indexB] = tmp;
}
function checkDone(blks) {
if (isDone(blks)) {
onDone();
}
}
function isDone(blks) {
for (const [i, blk] of blks.entries())
if (i != 15 && blk.$id != i + 1 || i == 15 && blk.$id != 0)
return false;
return true;
}
@import url('https://fonts.googleapis.com/css?family=Special+Elite');
body {
display:flex;
justify-content:center;
align-items:center;
min-height:100vh;
flex-flow:column nowrap;
font-family:"special elite", cursive;
}
#elGame {
border:medium solid black;
box-shadow:0 0 10px 1px rgba(0,0,0,1);
position:relative;
background:linear-gradient(rgba(0,0,0,0.1), rgba(0,0,0,0.3))
}
.block {
width:25%;
height:25%;
position:absolute;
background:linear-gradient(transparent, rgba(0,0,0,0.1));
box-shadow:inset 0 0 10px 1px rgba(0,0,0,0.3);
user-select:none; cursor:move;
font:3em "special elite", cursive;
display:flex; justify-content:center; align-items:center;
border:thin solid gray; box-sizing:border-box;
padding-top:0.35em;
transition:all 0.15s;
}
.empty {
background:none;
border:0 none;
box-shadow:none;
color:transparent;
cursor:default;
}
#elTime {
margin-top:0.5em;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment