/* ========================= CANVAS / RESIZE ========================= */ const LOGICAL_WIDTH = 480; const LOGICAL_HEIGHT = 260; const UI_VERTICAL_MARGIN = 80; const canvas = document.getElementById("game"); const ctx = canvas.getContext("2d"); const overlay = document.getElementById("overlay"); const input = document.getElementById("answer"); function resize() { const vw = window.visualViewport ? window.visualViewport.width : window.innerWidth; const vh = window.visualViewport ? window.visualViewport.height : window.innerHeight; const CANVAS_CHROME = 12; const scale = Math.min( vw / LOGICAL_WIDTH, (vh - CANVAS_CHROME) / LOGICAL_HEIGHT ); canvas.width = LOGICAL_WIDTH; canvas.height = LOGICAL_HEIGHT; canvas.style.width = LOGICAL_WIDTH * scale + "px"; canvas.style.height = LOGICAL_HEIGHT * scale + "px"; } resize(); window.addEventListener("resize", resize); if (window.visualViewport) visualViewport.addEventListener("resize", resize); /* ========================= GAME STATE ========================= */ let started = false; let bombs = []; let level = 1; let score = 0; let diffused = 0; let leaked = 0; let lastSpawn = 0; let lastTime = performance.now(); let highScore = 0; function resetGameState() { bombs = []; buffer = ""; input.value = ""; diffused = 0; leaked = 0; level = 1; score = 0; lastSpawn = 0; } const BOMB_RADIUS = 24; /* ========================= INPUT BUFFER ========================= */ let buffer = ""; const prevButtons = {}; /* ========================= NUMPAD DEFINITION ===========================*/ const numpadLayout = [ "7","8","9", "4","5","6", "1","2","3", "←","0","✓" ]; const numpad = document.getElementById("numpad"); let padIndex = 0; numpadLayout.forEach((key, i) => { const btn = document.createElement("div"); btn.className = "numpad-btn"; btn.textContent = key; btn.addEventListener("click", () => handlePadInput(key)); numpad.appendChild(btn); }); function handlePadInput(key) { if (key === "←") backspace(); else if (key === "✓") submitAnswer(); else appendDigit(key); } function updateNumpadHighlight() { [...numpad.children].forEach((btn, i) => { btn.classList.toggle("active", i === padIndex); }); } padIndex = 10; // highlights "0" updateNumpadHighlight(); /* ========================= PROBLEMS ========================= */ function makeProblem() { // keep numbers friendly const addMax = 10; const subMax = 10; const multMax = 10; // choose operation (weighted slightly toward +) const roll = Math.random(); let op; if (roll < 0.4) op = "+"; // 40% else if (roll < 0.7) op = "−"; // 30% else op = "×"; // 30% if (op === "+") { const a = rand(1, addMax); const b = rand(1, addMax); return { top: a, bottom: b, op, answer: a + b }; } if (op === "−") { const a = rand(1, subMax); const b = rand(1, subMax); const top = Math.max(a, b); const bottom = Math.min(a, b); return { top, bottom, op, answer: top - bottom }; } // multiplication const a = rand(1, multMax); const b = rand(1, multMax); return { top: a, bottom: b, op, answer: a * b }; } function rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } function spawnBomb() { const p = makeProblem(); const baseSpeed = 0.015; const slowRamp = 0.002; const fastRamp = 0.004; const speed = level <= 10 ? baseSpeed + level * slowRamp : baseSpeed + 10 * slowRamp + (level - 10) * fastRamp; bombs.push({ x: LOGICAL_WIDTH - 24, y: 60 + Math.random() * 140, speed, ...p }); } /* ========================= INPUT ACTIONS ========================= */ function appendDigit(d) { buffer += d; input.value = buffer; } function backspace() { buffer = buffer.slice(0, -1); input.value = buffer; } function submitAnswer() { if (!buffer) return; const val = Number(buffer); buffer = ""; input.value = ""; for (let i = 0; i < bombs.length; i++) { if (bombs[i].answer === val) { bombs.splice(i, 1); diffused++; score += 100 * level; if (diffused >= 10) nextLevel(); return; } } } /* ========================= FLOW ========================= */ function startGame() { if (started) return; resetGameState(); started = true; overlay.style.display = "none"; lastTime = performance.now(); requestAnimationFrame(loop); } function nextLevel() { level++; diffused = 0; bombs = []; } function gameOver() { started = false; // update high score if (score > highScore) { highScore = score; } overlay.innerHTML = `
GAME OVER
Score: ${score}
High Score: ${highScore}
Press Enter / A / Tap to Restart
`; overlay.style.display = "flex"; resetGameState(); } /* ========================= INPUT — KEYBOARD ========================= */ window.addEventListener("keydown", e => { if (!started) startGame(); if (e.key >= "0" && e.key <= "9") appendDigit(e.key); if (e.key === "Backspace") backspace(); if (e.key === "Enter") submitAnswer(); }); /* ========================= INPUT — TOUCH / MOBILE ========================= */ overlay.addEventListener("click", startGame); input.addEventListener("input", () => { buffer = input.value.replace(/\D/g, ""); }); /* ========================= INPUT — GAMEPAD ========================= */ function pollGamepad() { const pads = navigator.getGamepads(); if (!pads) return; for (const p of pads) { if (!p) continue; const press = (i) => { const v = p.buttons[i]?.pressed; const w = prevButtons[i] || false; prevButtons[i] = v; return v && !w; }; if (press(1)) backspace(); // B if (press(9)) submitAnswer(); // Start if (press(12)) padIndex = (padIndex + 9) % 12; // up if (press(13)) padIndex = (padIndex + 3) % 12; // down if (press(14)) padIndex = (padIndex + 11) % 12; // left if (press(15)) padIndex = (padIndex + 1) % 12; // right if (press(0)) handlePadInput(numpadLayout[padIndex]); // X = submit answer (arcade-friendly) if (press(2)) { submitAnswer(); } // B = backspace if (press(1)) { backspace(); } updateNumpadHighlight(); } } /* ========================= UPDATE ========================= */ function update(dt) { pollGamepad(); lastSpawn += dt; if (lastSpawn > Math.max(1600 - level * 80, 700)) { spawnBomb(); lastSpawn = 0; } bombs.forEach(b => b.x -= dt * b.speed); bombs = bombs.filter(b => { if (b.x < 20) { leaked++; if (leaked >= 3) gameOver(); return false; } return true; }); } /* ========================= DRAW ========================= */ function draw() { ctx.clearRect(0,0,LOGICAL_WIDTH,LOGICAL_HEIGHT); ctx.fillStyle = "#0f0"; ctx.font = "14px monospace"; ctx.textAlign = "center"; const hudY = 16; ctx.fillText(`Level ${level}`, LOGICAL_WIDTH * 0.25, hudY); ctx.fillText(`Score ${score}`, LOGICAL_WIDTH * 0.50, hudY); ctx.fillText(`Leaks ${leaked}/3`, LOGICAL_WIDTH * 0.75, hudY); ctx.textAlign = "center"; // reset for rest of draw ctx.font = "16px monospace"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; bombs.forEach(b => { const danger = 1 - (b.x / LOGICAL_WIDTH); const r = Math.floor(100 + 155 * danger); ctx.fillStyle = `rgb(${r}, 0, 0)`; ctx.strokeStyle = "#ff0000"; ctx.beginPath(); ctx.arc(b.x, b.y, BOMB_RADIUS, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.fillStyle = "#04ff00"; ctx.font = "16px monospace"; ctx.textBaseline = "middle"; /* Right-aligned math column */ const colX = b.x + 12; ctx.textAlign = "right"; ctx.fillText(String(b.top), colX, b.y - 10); ctx.fillText(String(b.bottom), colX, b.y + 10); /* Operator sits to the left of the bottom number */ ctx.textAlign = "center"; ctx.fillText(b.op, colX - 18, b.y + 10); /* Underline */ ctx.beginPath(); ctx.moveTo(colX - 22, b.y + 18); ctx.lineTo(colX + 2, b.y + 18); ctx.strokeStyle = "#04ff00"; ctx.stroke(); }); } /* ========================= LOOP ========================= */ function loop(t) { if (!started) return; const dt = t - lastTime; lastTime = t; update(dt); draw(); requestAnimationFrame(loop); }