Last active
December 31, 2024 05:21
-
-
Save HenkPoley/16ebde26404c7ef9b7a5c442bfb70ebc to your computer and use it in GitHub Desktop.
Revisions
-
HenkPoley revised this gist
Dec 31, 2024 . No changes.There are no files selected for viewing
-
HenkPoley created this gist
Dec 31, 2024 .There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,590 @@ <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Advanced Calculator Snake</title> <style> html, body { margin: 0; padding: 0; height: 100%; overflow: hidden; /* prevent scrollbars */ background: #222; color: #fff; font-family: sans-serif; } /* The game canvas takes up the full window, behind the overlay */ canvas { display: block; background: #333; position: absolute; top: 0; left: 0; } /* Expression area at the top with semi-transparent background + text outline */ #expression { position: fixed; top: 8px; left: 50%; transform: translateX(-50%); font-size: 1.2rem; color: #0f0; background: rgba(0, 0, 0, 0.6); padding: 6px 10px; border-radius: 6px; z-index: 10; pointer-events: none; /* "Closed captioning" style text outline via shadow: */ text-shadow: -1px 0 0 #000, 1px 0 0 #000, 0 -1px 0 #000, 0 1px 0 #000; } /* Button to toggle finite/infinite */ #finiteToggle { position: fixed; top: 50px; left: 50%; transform: translateX(-50%); z-index: 10; background: #444; color: #fff; border: 1px solid #666; padding: 5px 10px; cursor: pointer; } /* The initial overlay (modal) that shows instructions and the speed slider */ #startOverlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.8); color: #fff; display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 999; } #overlayContent { max-width: 500px; text-align: center; background: #444; padding: 20px; border-radius: 8px; border: 2px solid #666; } #speedRange { width: 80%; } #startButton { margin-top: 20px; padding: 10px 20px; background: #222; color: #fff; border: 1px solid #666; cursor: pointer; font-size: 1rem; } </style> </head> <body> <!-- Expression overlay --> <div id="expression">Expression: (none)</div> <!-- Toggle for finite vs. infinite --> <button id="finiteToggle">Toggle Finite Grid (Currently: Infinite)</button> <!-- The game canvas --> <canvas id="gameCanvas"></canvas> <!-- Initial overlay with instructions & speed slider --> <div id="startOverlay"> <div id="overlayContent"> <h2>Welcome to Advanced Calculator Snake</h2> <p>Use <strong>WASD</strong> or <strong>Arrow Keys</strong> to move.<br/> Press <strong>Space</strong> to "eat" the calculator button where your snake's head is.</p> <p>Build an expression with digits/operators. Eat "=" to evaluate. "AC" clears the expression, "Del" removes last character, and "±" toggles sign.</p> <p> <label for="speedRange"><strong>Game Speed (ms per move):</strong></label><br/> <input type="range" min="50" max="1000" step="10" value="300" id="speedRange"/> <span id="speedValue">300</span> ms </p> <button id="startButton">Start Game</button> </div> </div> <script> // ----------------------------------------------------- // CONFIG // ----------------------------------------------------- let IS_FINITE = false; // toggled by button // If finite, define total grid size: const FINITE_WIDTH = 50; const FINITE_HEIGHT = 30; const CELL_SIZE = 40; // Calculator layout: 5 rows × 4 columns // We'll color "." in bright orange as requested const CALC_LAYOUT = [ ["AC", "±", "%", "/" ], ["7", "8", "9", "*" ], ["4", "5", "6", "-" ], ["1", "2", "3", "+" ], ["0", ".", "=", "Del"] ]; const CHUNK_WIDTH = 4; const CHUNK_HEIGHT = 5; // margin (in cells) near edges => camera scroll const SCROLL_MARGIN = 3; let currentSpeed = 300; let gameTimer = null; let gameOver = false; // Snake: let worm = []; // we'll set initial position after user clicks "Start" let wormSet = new Set(); let direction = { x: 1, y: 0 }; // Camera offset let offsetX = 0; let offsetY = 0; // Expression let expression = ""; // Canvas const canvas = document.getElementById("gameCanvas"); const ctx = canvas.getContext("2d"); // # of cells that fit on-screen horizontally/vertically let VIEW_CELLS_X = 0; let VIEW_CELLS_Y = 0; // HTML elements const startOverlay = document.getElementById("startOverlay"); const speedRange = document.getElementById("speedRange"); const speedValue = document.getElementById("speedValue"); const startButton = document.getElementById("startButton"); const finiteBtn = document.getElementById("finiteToggle"); // ----------------------------------------------------- // HELPER FUNCTIONS // ----------------------------------------------------- function keyOf(x, y) { return `${x},${y}`; } function mod(a, m) { return ((a % m) + m) % m; } function getSymbolAt(worldX, worldY) { const tileX = mod(worldX, CHUNK_WIDTH); const tileY = mod(worldY, CHUNK_HEIGHT); return CALC_LAYOUT[tileY][tileX]; } // Color for each button function getButtonColor(symbol) { if (/^[0-9]$/.test(symbol)) { const d = parseInt(symbol, 10); return `hsl(200, 50%, ${30 + 5*d}%)`; // digit gradient } if (symbol === '.') return 'orange'; // requested bright orange for decimal if (symbol === '=') return 'lime'; if (symbol === 'Del' || symbol === 'AC') return '#666'; if (symbol === '±' || symbol === '%') return '#fe8'; // Operators: +, -, *, / return '#F66'; } // Evaluate expression function evaluateExpression() { try { // treat ± as minus, naive approach: let safeExpr = expression.replace(/±/g, "-"); let result = eval(safeExpr); expression = result.toString(); } catch (e) { expression = "Error"; } updateExpressionDisplay(); } // Update expression text function updateExpressionDisplay() { const exprElem = document.getElementById("expression"); if (!expression) { exprElem.textContent = "Expression: (none)"; } else { exprElem.textContent = "Expression: " + expression; } } // ----------------------------------------------------- // CAMERA SCROLLING // ----------------------------------------------------- function updateCamera() { if (gameOver) return; const head = worm[0]; const headScreenX = head.x - offsetX; const headScreenY = head.y - offsetY; // near left if (headScreenX < SCROLL_MARGIN) { offsetX = head.x - SCROLL_MARGIN; } // near right if (headScreenX >= VIEW_CELLS_X - SCROLL_MARGIN) { offsetX = head.x - (VIEW_CELLS_X - SCROLL_MARGIN - 1); } // near top if (headScreenY < SCROLL_MARGIN) { offsetY = head.y - SCROLL_MARGIN; } // near bottom if (headScreenY >= VIEW_CELLS_Y - SCROLL_MARGIN) { offsetY = head.y - (VIEW_CELLS_Y - SCROLL_MARGIN - 1); } // clamp if finite if (IS_FINITE) { if (offsetX < 0) offsetX = 0; if (offsetY < 0) offsetY = 0; if (offsetX > FINITE_WIDTH - VIEW_CELLS_X) { offsetX = FINITE_WIDTH - VIEW_CELLS_X; } if (offsetY > FINITE_HEIGHT - VIEW_CELLS_Y) { offsetY = FINITE_HEIGHT - VIEW_CELLS_Y; } } } // ----------------------------------------------------- // RENDERING // ----------------------------------------------------- function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); // 1) Draw chunk boundary lines in bright orange ctx.strokeStyle = "orange"; ctx.lineWidth = 1; // vertical chunk lines for (let sx = 0; sx <= VIEW_CELLS_X; sx++) { const wx = offsetX + sx; if ((wx % CHUNK_WIDTH) === 0) { ctx.beginPath(); ctx.moveTo(sx * CELL_SIZE, 0); ctx.lineTo(sx * CELL_SIZE, VIEW_CELLS_Y * CELL_SIZE); ctx.stroke(); } } // horizontal chunk lines for (let sy = 0; sy <= VIEW_CELLS_Y; sy++) { const wy = offsetY + sy; if ((wy % CHUNK_HEIGHT) === 0) { ctx.beginPath(); ctx.moveTo(0, sy * CELL_SIZE); ctx.lineTo(VIEW_CELLS_X * CELL_SIZE, sy * CELL_SIZE); ctx.stroke(); } } // 2) Draw calculator buttons or walls ctx.font = "20px monospace"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; for (let sy = 0; sy < VIEW_CELLS_Y; sy++) { for (let sx = 0; sx < VIEW_CELLS_X; sx++) { const wx = offsetX + sx; const wy = offsetY + sy; // In finite mode, if outside boundary => draw wall if (IS_FINITE && (wx < 0 || wx >= FINITE_WIDTH || wy < 0 || wy >= FINITE_HEIGHT)) { drawEmoji("🧱", sx, sy); continue; } // Otherwise, draw the normal calculator button const symbol = getSymbolAt(wx, wy); if (symbol) { ctx.fillStyle = getButtonColor(symbol); ctx.fillRect( sx * CELL_SIZE + 5, sy * CELL_SIZE + 5, CELL_SIZE - 10, CELL_SIZE - 10 ); ctx.fillStyle = "#000"; ctx.fillText( symbol, sx * CELL_SIZE + CELL_SIZE / 2, sy * CELL_SIZE + CELL_SIZE / 2 ); } } } // 3) Draw the worm ctx.fillStyle = "lime"; for (let i = 0; i < worm.length; i++) { const seg = worm[i]; const sx = seg.x - offsetX; const sy = seg.y - offsetY; if (sx >= 0 && sx < VIEW_CELLS_X && sy >= 0 && sy < VIEW_CELLS_Y) { ctx.fillRect(sx * CELL_SIZE, sy * CELL_SIZE, CELL_SIZE, CELL_SIZE); } } } // Helper: draw an emoji in a cell function drawEmoji(emoji, sx, sy) { ctx.fillStyle = "#444"; ctx.fillRect( sx * CELL_SIZE + 5, sy * CELL_SIZE + 5, CELL_SIZE - 10, CELL_SIZE - 10 ); ctx.fillStyle = "#fff"; ctx.fillText( emoji, sx * CELL_SIZE + CELL_SIZE / 2, sy * CELL_SIZE + CELL_SIZE / 2 ); } // ----------------------------------------------------- // GAME LOOP // ----------------------------------------------------- function gameTick() { if (gameOver) return; // 1) Move the worm const oldHead = worm[0]; const newHead = { x: oldHead.x + direction.x, y: oldHead.y + direction.y }; // 2) Check boundaries if finite if (IS_FINITE) { if (newHead.x < 0 || newHead.x >= FINITE_WIDTH || newHead.y < 0 || newHead.y >= FINITE_HEIGHT) { gameOver = true; alert("Game Over! You hit the wall."); return; } } // 3) Self collision if (wormSet.has(keyOf(newHead.x, newHead.y))) { gameOver = true; alert("Game Over! You crashed into yourself."); return; } // 4) Move worm.unshift(newHead); wormSet.add(keyOf(newHead.x, newHead.y)); const tail = worm.pop(); wormSet.delete(keyOf(tail.x, tail.y)); // 5) Camera updateCamera(); // 6) Render draw(); // 7) Next tick if (!gameOver) { gameTimer = setTimeout(gameLoop, currentSpeed); } } function gameLoop() { gameTick(); } // ----------------------------------------------------- // EATING // ----------------------------------------------------- function eat() { if (gameOver) return; const head = worm[0]; // If finite & outside boundary, do nothing if (IS_FINITE && (head.x < 0 || head.x >= FINITE_WIDTH || head.y < 0 || head.y >= FINITE_HEIGHT)) { return; } const symbol = getSymbolAt(head.x, head.y); if (!symbol) return; switch (symbol) { case "=": evaluateExpression(); currentSpeed = Math.max(50, currentSpeed - 20); // speed up break; case "AC": expression = ""; updateExpressionDisplay(); break; case "Del": expression = expression.slice(0, -1); updateExpressionDisplay(); break; case "±": expression += "±"; updateExpressionDisplay(); break; default: // digits, operators, etc. expression += symbol; updateExpressionDisplay(); } // Grow the worm by duplicating its tail const tail = worm[worm.length - 1]; worm.push({ x: tail.x, y: tail.y }); wormSet.add(keyOf(tail.x, tail.y)); draw(); } // ----------------------------------------------------- // INPUT // ----------------------------------------------------- window.addEventListener("keydown", (e) => { // Prevent scrolling if (["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"," "].includes(e.key)) { e.preventDefault(); } if (gameOver) return; switch (e.key) { case "ArrowUp": case "w": if (direction.y === 0) direction = { x: 0, y: -1 }; break; case "ArrowDown": case "s": if (direction.y === 0) direction = { x: 0, y: 1 }; break; case "ArrowLeft": case "a": if (direction.x === 0) direction = { x: -1, y: 0 }; break; case "ArrowRight": case "d": if (direction.x === 0) direction = { x: 1, y: 0 }; break; case " ": case "Enter": eat(); break; } }); // ----------------------------------------------------- // FINITE TOGGLE BUTTON // ----------------------------------------------------- finiteBtn.addEventListener("click", () => { IS_FINITE = !IS_FINITE; if (IS_FINITE) { finiteBtn.textContent = `Toggle Finite Grid (Currently: Finite)`; } else { finiteBtn.textContent = `Toggle Finite Grid (Currently: Infinite)`; } draw(); }); // ----------------------------------------------------- // RESIZE & CANVAS FIT // ----------------------------------------------------- function resizeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; VIEW_CELLS_X = Math.floor(canvas.width / CELL_SIZE); VIEW_CELLS_Y = Math.floor(canvas.height / CELL_SIZE); // clamp offset if finite if (IS_FINITE) { if (offsetX < 0) offsetX = 0; if (offsetY < 0) offsetY = 0; if (offsetX > FINITE_WIDTH - VIEW_CELLS_X) { offsetX = FINITE_WIDTH - VIEW_CELLS_X; } if (offsetY > FINITE_HEIGHT - VIEW_CELLS_Y) { offsetY = FINITE_HEIGHT - VIEW_CELLS_Y; } } draw(); } window.addEventListener("resize", resizeCanvas); // ----------------------------------------------------- // INIT & START OVERLAY // ----------------------------------------------------- // Update speed label on slider input speedRange.addEventListener("input", (e) => { currentSpeed = parseInt(e.target.value, 10); speedValue.textContent = currentSpeed; }); // Start button startButton.addEventListener("click", () => { // Hide overlay startOverlay.style.display = "none"; // If finite, start in the middle // Otherwise, start at e.g. (10,10) if (IS_FINITE) { const midX = Math.floor(FINITE_WIDTH / 2); const midY = Math.floor(FINITE_HEIGHT / 2); worm = [{ x: midX, y: midY }]; wormSet.clear(); wormSet.add(keyOf(midX, midY)); direction = { x: 1, y: 0 }; offsetX = midX - 2; offsetY = midY - 2; } else { worm = [{ x: 10, y: 10 }]; wormSet.clear(); wormSet.add(keyOf(10, 10)); direction = { x: 1, y: 0 }; offsetX = 8; offsetY = 8; } // Re-check size & start game resizeCanvas(); updateExpressionDisplay(); gameOver = false; gameLoop(); }); // On load function init() { // set speed from slider currentSpeed = parseInt(speedRange.value, 10); speedValue.textContent = currentSpeed; // Canvas size resizeCanvas(); // We do NOT start the gameLoop yet. Wait for user to click "Start Game." } init(); </script> </body> </html>