sourcehypertextpublicmatrixmmxlviii.js

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 {
			// Don’t bother shuffling the array if we’re only adding one tile
			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;
		}
	});
});