Help Debugging My Snake Game

im making a simple little snake game and i found a bug that is hard to explain and/or recreate but in game when putting inputs in rapidly in such a way you should not die you will just randomly die.

Heres a link to game landons-snake-gamev45.tiiny.site
Heres the code

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Landon's Snake Game</title>
    <style>
    #game-container {
        position: relative;
        width: 520px; /* Adjust width as necessary */
        margin: auto;
    }
    canvas {
        display: block;
        border: 5px solid #009BFF;
        margin: auto;
    }
    #speedMenu {
        position: absolute;
        top: 10px;
        right: 10px;
        z-index: 1;
    }
    </style>
</head>
<body>
<div id="game-container">
    <canvas id="gameCanvas" width="520" height="520"></canvas>
    <button id="pauseButton">Pause</button>
    <select id="speedMenu">
        <option value="1">Normal</option>
        <option value="0.75">Slow</option>
        <option value="1.55">Hard</option>
        <option value="3">Extreme</option>
    </select>
</div>
<script>
var
COLS = 26,
ROWS = 26,
EMPTY = 0,
SNAKE = 1,
FRUIT = 2,
POWER_UP = 3,
LEFT  = 0,
UP    = 1,
RIGHT = 2,
DOWN  = 3,
KEY_LEFT  = 37,
KEY_UP    = 38,
KEY_RIGHT = 39,
KEY_DOWN  = 40,
canvas,
ctx,
keystate,
frames,
score,
paused = false,
powerUpActive = false,
powerUpTimer = 0,
powerUpDuration = 30 * 60,
grid = {
    width: null,
    height: null,
    _grid: null,
    init: function(d, c, r) {
        this.width = c;
        this.height = r;
 
        this._grid = [];
        for (var x=0; x < c; x++) {
            this._grid.push([]);
            for (var y=0; y < r; y++) {
                this._grid[x].push(d);
            }
        }
    },
 
    set: function(val, x, y) {
        this._grid[x][y] = val;
    },
 
    get: function(x, y) {
        return this._grid[x][y];
    }
},
snake = {
    direction: null,
    last: null,
    _queue: null,
 
    init: function(d, x, y) {
        this.direction = d;
 
        this._queue = [];
        this.insert(x, y);
    },
 
    insert: function(x, y) {
        this._queue.unshift({x:x, y:y});
        this.last = this._queue[0];
    },
 
    remove: function() {
        return this._queue.pop();
    }
};
 
function setFood() {
    var empty = [];
    
    for (var x=0; x < grid.width; x++) {
        for (var y=0; y < grid.height; y++) {
            if (grid.get(x, y) === EMPTY) {
                empty.push({x:x, y:y});
            }
        }
    }
    
    var randpos = empty[Math.round(Math.random() * empty.length - 1)];
    var spawnType = Math.random() < 0.075 ? POWER_UP : FRUIT;
    grid.set(spawnType, randpos.x, randpos.y);
}
 
function main() {
    canvas = document.getElementById("gameCanvas");
    ctx = canvas.getContext("2d");
 
    ctx.font = "12px Helvetica";
    frames = 0;
    keystate = {};
    
    document.addEventListener("keydown", function(evt) {
        keystate[evt.keyCode] = true;

        if (evt.keyCode === 80) {
            paused = !paused;
            if (paused) {
                document.getElementById("pauseButton").innerText = "Resume";
            } else {
                document.getElementById("pauseButton").innerText = "Pause";
            }
        }
    });
    document.addEventListener("keyup", function(evt) {
        delete keystate[evt.keyCode];
    });
 
    document.getElementById("pauseButton").addEventListener("click", function() {
        paused = !paused;
        if (paused) {
            document.getElementById("pauseButton").innerText = "Resume";
        } else {
            document.getElementById("pauseButton").innerText = "Pause";
        }
    });

    var speedMenu = document.getElementById("speedMenu");
    speedMenu.addEventListener("change", function() {
        adjustSpeed(this.value);
    });
 
    init();
    loop();
}
function init() {
    score = 0;
    powerUpActive = false;
    powerUpTimer = 0;
    keystate = {};

    grid.init(EMPTY, COLS, ROWS);

    var sp = {x: Math.floor(COLS / 2), y: ROWS - 1};
    snake.init(UP, sp.x, sp.y);
    grid.set(SNAKE, sp.x, sp.y);

    setFood();
}
function loop() {
    if (!paused) {
        update();
        draw();
    }
    
    window.requestAnimationFrame(loop, canvas);
}
 
function update() {
    frames++;
    
    if (powerUpActive) {
        powerUpTimer++;
        if (powerUpTimer >= powerUpDuration) {
            powerUpActive = false;
            powerUpTimer = 0;
        }
    }

    var speedFactor = parseFloat(document.getElementById("speedMenu").value);
    
    if (keystate[KEY_LEFT] && snake.direction !== RIGHT) {
        snake.direction = LEFT;
    }
    if (keystate[KEY_UP] && snake.direction !== DOWN) {
        snake.direction = UP;
    }
    if (keystate[KEY_RIGHT] && snake.direction !== LEFT) {
        snake.direction = RIGHT;
    }
    if (keystate[KEY_DOWN] && snake.direction !== UP) {
        snake.direction = DOWN;
    }
 
    if (frames % Math.floor(7 / speedFactor) === 0) {
        var nx = snake.last.x;
        var ny = snake.last.y;
 
        switch (snake.direction) {
            case LEFT:
                nx--;
                break;
            case UP:
                ny--;
                break;
            case RIGHT:
                nx++;
                break;
            case DOWN:
                ny++;
                break;
        }
 
        if (0 > nx || nx > grid.width - 1  ||
            0 > ny || ny > grid.height - 1 ||
            grid.get(nx, ny) === SNAKE
        ) {
            alert("Game Over! Your score is: " + score);
            return init();
        }
 
        if (grid.get(nx, ny) === FRUIT || grid.get(nx, ny) === POWER_UP) {
            if (grid.get(nx, ny) === POWER_UP) {
                score *= 2;
                powerUpActive = true;
                powerUpTimer = 0;
            } else {
                score += powerUpActive ? 2 : 1;
            }
            snake.insert(nx, ny);
            setFood();
        } else {
            var tail = snake.remove();
            grid.set(EMPTY, tail.x, tail.y);
        }
 
        grid.set(SNAKE, nx, ny);
        snake.insert(nx, ny);
    }
}
function draw() {
    var tw = canvas.width / grid.width;
    var th = canvas.height / grid.height;
    
    for (var x = 0; x < grid.width; x++) {
        for (var y = 0; y < grid.height; y++) {
            switch (grid.get(x, y)) {
                case EMPTY:
                    ctx.fillStyle = "#fff";
                    break;
                case SNAKE:
                    if (powerUpActive) {
                        var hue = (frames + x + y) % 360;
                        ctx.fillStyle = "hsl(" + hue + ", 100%, 50%)";
                    } else {
                        ctx.fillStyle = "#333";
                    }
                    break;
                case FRUIT:
                    ctx.fillStyle = "#009BFF";
                    break;
                case POWER_UP:
                    ctx.fillStyle = "#FF69B4"; // Pink square color
                    break;
            }
            ctx.fillRect(x * tw, y * th, tw, th);
        }
    }
    
    ctx.fillStyle = "#000";
    ctx.fillText("SCORE: " + score, 10, canvas.height - 10);
    if (powerUpActive) {
        ctx.fillText("POWER-UP ACTIVE", 10, canvas.height - 25);
    }
}
 
main();
</script>
</body>
</html>

Your steps to reproduce are:

  • Build a snake of at least length = 3
  • Turn 90 degrees in either direction
  • IMMEDIATELY turn 90 degrees in the same direction, so that you are now traveling the opposite direction you were before turning.
  • Observe: the snake dies, your initial turn was not completed before ending the game

It seems like when you turn twice in rapid succession, the first turn does not complete before the is second made, and you turn back into your self, triggering your self-collision detection. See if you can chase down that to figure out what’s going wrong. Maybe if you turn twice in the same frame it leads to this problem?