Magic Tiles – Minimal MVP

by Ben Gillbanks

HTML

<div id="wrap">
		<canvas id="game" width="360" height="640"></canvas>
		<div id="ui">
			<div id="msg" class="small">Click the tile in the hit zone. Miss and it’s over.</div>
			<button id="play">Play</button>
		</div>
	</div>

CSS

html, body { height: 100%; margin: 0; background: #111; color: #fff; font-family: system-ui, sans-serif; }
		#wrap { display: grid; place-items: center; height: 100%; gap: 12px; }
		canvas { background: #222; border: 2px solid #444; touch-action: manipulation; }
		#ui { text-align: center; }
		button { padding: 8px 14px; font-size: 16px; background: #08f; color: #fff; border: 0; cursor: pointer; }
		button:hover { filter: brightness(1.1); }
		.small { opacity: .8; font-size: 14px; }

JavaScript

/* Minimal Magic Tiles MVP
		   - 4 columns
		   - Tap black tiles in the bottom hit zone
		   - Miss or tap empty = game over
		*/

		const canvas = document.getElementById('game');
		const ctx = canvas.getContext('2d');
		const playBtn = document.getElementById('play');
		const msgEl = document.getElementById('msg');

		const W = canvas.width;
		const H = canvas.height;

		// Grid
		const COLS = 4;
		const colW = W / COLS;

		// Hit zone
		const HIT_H = 80;                  // height of the hit zone
		const HIT_Y = H - HIT_H;          // y position
		const HIT_SLACK = 12;             // leniency in pixels

		// Game state
		let tiles = [];                    // {col, y, h}
		let running = false;
		let score = 0;
		let best = 0;
		let speed = 20;                   // px/s
		let spawnEvery = 2000;              // ms
		let lastTime = 0;
		let spawnTimer = 0;

		function reset() {
			tiles = [];
			score = 0;
			speed = 20;
			spawnEvery = 2000;
			spawnTimer = 0;
			lastTime = performance.now();
		}

		function start() {
			reset();
			running = true;
			msgEl.textContent = 'Good luck.';
			requestAnimationFrame(loop);
		}

		function gameOver(text = 'Game over') {
			running = false;
			best = Math.max(best, score);
			msgEl.textContent = `${text} | Score ${score} | Best ${best}`;
			playBtn.textContent = 'Play again';
		}

		function spawnTile() {
			const col = Math.floor(Math.random() * COLS);
			const h = 110; // tile height
			tiles.push({ col, y: -h, h });
		}

		function update(dt) {
			// Increase difficulty over time
			speed += dt * 4;            // very light ramp
			spawnEvery = Math.max(180, spawnEvery - dt * 8);

			// Move tiles
			for (const t of tiles) t.y += speed * dt / 1000;

			// Check miss: tile passed bottom of hit zone without a tap
			for (const t of tiles) {
				if (t.y + t.h >= H && !t.passed) {
					// If it reaches the bottom, that means you missed it in the hit...