const Game = {
ongoing: true,
grid: [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
],
swipeStart: null,
render: () => {
const rawGrid = Game.grid.flat();
const displayGrid = $$("div.tile");
for (let i = 0; i < 16; i++) {
displayGrid[i].dataset.value = rawGrid[i];
displayGrid[i].dataset.long = rawGrid[i] > 8192;
displayGrid[i].setAttribute(
"style",
rawGrid[i] <= 8192
? ""
: `--chars: ${
1 + floor(log(10, rawGrid[i] || 1))
}; --exp: ${log(2, rawGrid[i] || 1)}`
);
displayGrid[i].innerHTML = rawGrid[i] || "";
}
},
turn: fn => {
if (!Game.ongoing) {
return;
}
if (fn) {
fn();
}
try {
Game.addTwo(1);
Game.render();
} catch (err) {
Game.render();
if (err.message == "No free tiles") {
Game.stop();
}
}
},
stop: () => {
Game.ongoing = false;
const flatGrid = Game.grid.flat();
const maxIdx = flatGrid.indexOf(max(...flatGrid));
const blankIdx = flatGrid.indexOf(0);
$("#grid").classList.add("stopped");
$$(".tile")[maxIdx].classList.add("maximum");
$$(".tile")[
blankIdx
].outerHTML = `<button class="tile reset" data-value="0"><strong>⟳</strong><small>${
$("#grid").dataset.reset
}</small></button>`;
$(".reset").on("click", () => {
Game.reset();
});
},
reset: () => {
Game.ongoing = true;
Game.grid = [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
];
$("#grid").classList.remove("stopped");
$(".maximum").classList.remove("maximum");
$(".reset").outerHTML = `<div class="tile" data-value="0"></div>`;
Game.addTwo(2);
Game.render();
},
slide: {
left: () => {
Game.grid = Game.grid.map(Game.dev.shiftTiles);
},
right: () => {
Game.grid = Game.grid.map(row =>
Game.dev.shiftTiles(row.reverse()).reverse()
);
},
up: () => {
Game.grid = Game.dev.transpose(
Game.dev.transpose(Game.grid).map(Game.dev.shiftTiles)
);
},
down: () => {
Game.grid = Game.dev.transpose(
Game.dev
.transpose(Game.grid)
.map(col => Game.dev.shiftTiles(col.reverse()).reverse())
);
}
},
addTwo: (tileCount = 1) => {
const freeTiles = Game.grid
.map((row, rowIdx) =>
row.map((tile, colIdx) => (tile ? 0 : [rowIdx, colIdx]))
)
.flat()
.filter(tile => tile);
if (freeTiles.length <= tileCount) {
throw new Error("No free tiles");
} else {
if (tileCount == 1) {
const tileCoords =
freeTiles[Math.floor(Math.random() * freeTiles.length)];
Game.grid[tileCoords[0]][tileCoords[1]] = 2;
} else {
freeTiles
.map(value => ({ value, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort)
.slice(0, tileCount)
.map(({ value }) => value)
.forEach(tileCoords => {
Game.grid[tileCoords[0]][tileCoords[1]] = 2;
});
}
return freeTiles;
}
},
dev: {
shiftTiles: oldTiles => {
let tiles = oldTiles.filter(tile => tile);
if (tiles.length == 0) {
return [0, 0, 0, 0];
}
for (i = 1; i < tiles.length; i++) {
if (tiles[i - 1] == tiles[i]) {
tiles[i - 1] *= 2;
tiles[i] = 0;
}
tiles = tiles.filter(tile => tile);
}
return tiles.concat(Array(4 - tiles.length).fill(0));
},
transpose: matrix =>
matrix[0].map((col, colIdx) => matrix.map(row => row[colIdx]))
}
};
documentReady(() => {
Game.addTwo(2);
Game.render();
$("#grid").on("touchstart", event => {
Game.swipeStart = event.changedTouches[0];
});
$("#grid").on("touchend", event => {
let swipe = {
dx: event.changedTouches[0].clientX - Game.swipeStart.clientX,
dy: event.changedTouches[0].clientY - Game.swipeStart.clientY
};
Game.swipeStart = null;
swipe.distance = sqrt(swipe.dx ** 2 + swipe.dy ** 2);
if (swipe.distance < $("#grid").getBoundingClientRect().width / 5) {
return;
}
swipe.direction =
abs(swipe.dx) > abs(swipe.dy)
? swipe.dx > 0
? "right"
: "left"
: swipe.dy > 0
? "down"
: "up";
Game.turn(Game.slide[swipe.direction]);
});
document.body.on("keydown", event => {
switch (event.code) {
case "KeyW":
case "ArrowUp":
Game.turn(Game.slide.up);
break;
case "KeyA":
case "ArrowLeft":
Game.turn(Game.slide.left);
break;
case "KeyS":
case "ArrowDown":
Game.turn(Game.slide.down);
break;
case "KeyD":
case "ArrowRight":
Game.turn(Game.slide.right);
break;
}
});
});