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.

Revisions

  1. HenkPoley revised this gist Dec 31, 2024. No changes.
  2. HenkPoley created this gist Dec 31, 2024.
    590 changes: 590 additions & 0 deletions calculator_worms_game7.html
    Original 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>