- [OK] Touchscreen
- [OK] Solvable
Created
July 24, 2018 03:48
-
-
Save e1blue/c3ba59b4392268a6073e5624fec69901 to your computer and use it in GitHub Desktop.
Klotski{15-Pizzle}
This file contains hidden or 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
| #elGame | |
| #elTime . | |
| #elStatus Swipe to start |
This file contains hidden or 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
| // 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; | |
| } |
This file contains hidden or 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
| @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