Skip to content

Instantly share code, notes, and snippets.

@HenkPoley
Last active December 31, 2024 05:21
Show Gist options
  • Save HenkPoley/16ebde26404c7ef9b7a5c442bfb70ebc to your computer and use it in GitHub Desktop.
Save HenkPoley/16ebde26404c7ef9b7a5c442bfb70ebc to your computer and use it in GitHub Desktop.
Infinite scrolling calculator snake game. A silly impractical UI. Made using GPT-o1.
<!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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment