Search code examples
javascriptcanvascollision-detection

Cannot insert checkCollision method in Snake game in JavaScript


I am trying to make a Snake game by canvas in JavaScript. I have completed almost all the settings but the collision check that the game will crash when the snake hit itself or the wall as it cannot insert the checkCollision method which I have defined.

<script>
//Create canvas
const canvas = document.getElementById('canvas'); 
const ctx = canvas.getContext('2d'); 
let width = canvas.width;
let height = canvas.height;
let blockSize = 10; 
let widthInBlocks = width / blockSize; 
let heightInBlocks = height / blockSize; 
let drawBorder = function () { 
    ctx.fillStyle = 'Black';
    ctx.fillRect(0, 0, width, blockSize); 
    ctx.fillRect(0, height - blockSize, width, blockSize); 
    ctx.fillRect(0, 0, blockSize, height); 
    ctx.fillRect(width - blockSize, 0, blockSize, height); 
};
drawBorder();

//Create score 
var score = 0;
let drawScore = function () {
    ctx.clearRect(10, 10, width - 20, 40); 
    ctx.fillStyle = 'Black';
    ctx.textBaseLine = 'top'; 
    ctx.textAlign = 'left'; 
    ctx.font = '24px Arial'; 
    ctx.fillText('Score : ' + score, 15, 45); 
};

//Block constrcutor
const Block = function (col, row) { 
    this.col = col;
    this.row = row;
};
Block.prototype.drawSquare = function (color) { 
    let x = this.col * blockSize; 
    let y = this.row * blockSize;
    ctx.fillStyle = color; 
    ctx.fillRect(x, y, blockSize, blockSize); 
};

//Create food
const circle = function (x, y, radius, color, fill) {
    ctx.fillStyle = color;
    ctx.strokeStyle = color;
    ctx.beginPath();
    ctx.arc(x, y, radius, 0, Math.PI * 2, false);
    if (fill) {
        ctx.fill();
    } else {
        ctx.stroke();
    }
};
Block.prototype.drawCircle = function (color) {
    let x = this.col * blockSize + blockSize / 2; 
    let y = this.row * blockSize + blockSize / 2; 
    ctx.fillStyle = color; 
    circle(x, y, blockSize / 2, color, true); 
};
let Apple = function () {
    this.position = new Block(10, 10);
}; 
Apple.prototype.draw = function () {
    this.position.drawCircle(colorList[Math.floor(Math.random() * 7)]);
};
Apple.prototype.move = function () { 
    let randomCol = Math.floor(Math.random() * (widthInBlocks - 2)) + 1; 
    let randomRow = Math.floor(Math.random() * (heightInBlocks - 2)) + 1;
    if (randomCol !== this.segments && randomRow !== this.segments) {
        this.position = new Block(randomCol, randomRow); 
    }
};

//Setting keycode
const directions = {
    37:'left',
    38:'up',
    39:'right',
    40:'down'
};
$('body').keydown(function (event) { 
    let newDirection = directions[event.keyCode]; 
    if (newDirection !== undefined) { 
        snake.setDirection(newDirection);
    }
});

//Create snake
var colorList = ['Blue','Green','Red','Gold','Silver','Purple','Cyan']
var Snake = function () { 
    this.segments = [ 
        new Block(7,5),
        new Block(6,5),
        new Block(5,5)
    ];
    this.direction = 'right'; 
    this.nextDirection = 'right'; 
};
Snake.prototype.draw = function () { 
    for (let i = 0; i < this.segments.length; i ++) { 
        this.segments[i].drawSquare(colorList[Math.floor(Math.random() * 7)]); 
    };
};
//Setting moving directions
Snake.prototype.move = function () {
    let head = this.segments[0];
    let newHead; 
    this.direction = this.nextDirection;

    if (this.direction === 'right'){
        newHead = new Block(head.col + 1, head.row);
    } else if (this.direction === 'left') {
        newHead = new Block(head.col - 1, head.row);
    } else if (this.direction === 'up') {
        newHead = new Block(head.col, head.row - 1);
    } else if (this.direction === 'down') {
        newHead = new Block(head.col, head.row + 1)
    }

    if (this.checkCollision(newHead)) { 
        gameOver(); 
        return; 
    }

    this.segments.unshift(newHead); 

    if (newHead.equal(apple.position)) { 
        score ++; 
        apple.move();
        aniTime -= 1; 
    } else {
        this.segments.pop(); 
    }
};
//Define collision
Block.prototype.equal = function (otherBlock) { 
    return this.col === otherBlock.col && this.row === otherBlock.row; 
};
Snake.prototype.checkCollision = function (head) {
    var leftCollision = (head.col === 0);
    var topCollision = (head.row === 0); 
    var rightCollision = (head.col === widthInBlocks - 1); 
    var bottomCollision = (head.row === heightInBlocks - 1); 

    var wallCollision = leftCollision || topCollision || 
    rightCollision || bottomCollision; 

    var selfCollision = false; 

    for (let i = 0; i < this.segments.length; i ++) {
        if (head.equal(this.segments[i])) {
            selfCollision = true;
        }
    }
    return wallCollision || selfCollision 
};

Snake.prototype.setDirection = function (newDirection) {
    if (this.direction === 'up' && newDirection === 'down') {
        return; 
    } else if (this.direction === 'right' && newDirection ==='left') { 
        return;
    } else if (this.direction === 'down' && newDirection ==='up') { 
        return;
    } else if (this.direction === 'left' && newDirection ==='right') { 
        return;
    }
    this.nextDirection = newDirection; 
};

//run the game
let snake = new Snake(); 
let apple = new Apple(); 
var aniTime = 100; 
function core () { 
    ctx.clearRect(0, 0, width, height); 
    drawScore(); 
    snake.move(); 
    snake.draw(); 
    apple.draw(); 
    drawBorder();
    timeOutId = setTimeout(core, aniTime);
    if (snake.checkCollision() === true) { //**the PROBLEM
        clearTimeout(timeOutId);
        gameOver();
    };
};
core();

//Game over condition
var gameOver = function () {
    clearTimeout(timeOutId);
    ctx.font = '60px Arial';
    ctx.fillStyle = 'Black';
    ctx.textAlign = 'center';
    ctx.textBaseLine = 'middle';
    ctx.fillText('Game Over', width / 2, height / 2);
};
</script>

When the snake hit itself or the wall, the error message are as below:

Uncaught TypeError: Cannot read properties of undefined (reading 'col')
at Snake.checkCollision (snake.html:148:43)
at core (snake.html:191:27)
at snake.html:196:13
Uncaught TypeError: Cannot read properties of undefined (reading 'col')
at Snake.checkCollision (snake.html:148:43)
at core (snake.html:191:27)
snake.html:129 
Uncaught TypeError: gameOver is not a function
at Snake.move (snake.html:129:21)
at core (snake.html:186:23)

Solution

  • There seem to be several minor mistakes that are luckily easy to fix:

    • gameOver is declared as a var after calling core(). Declare it as a function to make sure its definition gets hoisted to the top. (or call core() below var gameOver = ...)
    • timeOutId is used by both the core and gameOver functions. Declare it outside of those functions to make sure both have access.
    • Snake.prototype.checkCollision expects head as an argument, but core calls it without one.
    • The self collision loop starts at i = 0, which means it will check wether the head collides with itself, which is always the case.

    Here's an example that has all the issues fixed:

    //Create canvas
    const canvas = document.getElementById('canvas'); 
    const ctx = canvas.getContext('2d'); 
    let width = canvas.width;
    let height = canvas.height;
    let blockSize = 15; 
    let widthInBlocks = width / blockSize; 
    let heightInBlocks = height / blockSize; 
    let drawBorder = function () { 
        ctx.fillStyle = 'Black';
        ctx.fillRect(0, 0, width, blockSize); 
        ctx.fillRect(0, height - blockSize, width, blockSize); 
        ctx.fillRect(0, 0, blockSize, height); 
        ctx.fillRect(width - blockSize, 0, blockSize, height); 
    };
    drawBorder();
    
    //Create score 
    var score = 0;
    let drawScore = function () {
        ctx.clearRect(10, 10, width - 20, 40); 
        ctx.fillStyle = 'Black';
        ctx.textBaseLine = 'top'; 
        ctx.textAlign = 'left'; 
        ctx.font = '24px Arial'; 
        ctx.fillText('Score : ' + score, 15, 45); 
    };
    
    //Block constrcutor
    const Block = function (col, row) { 
        this.col = col;
        this.row = row;
    };
    Block.prototype.drawSquare = function (color) { 
        let x = this.col * blockSize; 
        let y = this.row * blockSize;
        ctx.fillStyle = color; 
        ctx.fillRect(x, y, blockSize, blockSize); 
    };
    
    //Create food
    const circle = function (x, y, radius, color, fill) {
        ctx.fillStyle = color;
        ctx.strokeStyle = color;
        ctx.beginPath();
        ctx.arc(x, y, radius, 0, Math.PI * 2, false);
        if (fill) {
            ctx.fill();
        } else {
            ctx.stroke();
        }
    };
    Block.prototype.drawCircle = function (color) {
        let x = this.col * blockSize + blockSize / 2; 
        let y = this.row * blockSize + blockSize / 2; 
        ctx.fillStyle = color; 
        circle(x, y, blockSize / 2, color, true); 
    };
    let Apple = function () {
        this.position = new Block(10, 10);
    }; 
    Apple.prototype.draw = function () {
        this.position.drawCircle(colorList[Math.floor(Math.random() * 7)]);
    };
    Apple.prototype.move = function () {
        let availableCells = [];
        
        for (let r = 1; r < heightInBlocks - 2; r += 1) {
          for (let c = 1; c < widthInBlocks - 2; c += 1) {
            if (snake.segments.some(s => s.row === r && s.col === c)) continue;
            availableCells.push([r, c]);
          };   
        }; 
        
        if (availableCells.length === 0) {
          console.log("You won!");
          return;
        }
        
        const [ randomRow, randomCol ] = availableCells[Math.floor(Math.random() * availableCells.length)];
        this.position = new Block(randomCol, randomRow); 
    };
    
    //Setting keycode
    const directions = {
        37:'left',
        38:'up',
        39:'right',
        40:'down'
    };
    $('body').keydown(function (event) { 
        let newDirection = directions[event.keyCode]; 
        if (newDirection !== undefined) { 
            snake.setDirection(newDirection);
        }
    });
    
    //Create snake
    var colorList = ['Blue','Green','Red','Gold','Silver','Purple','Cyan']
    var Snake = function () { 
        this.segments = [ 
            new Block(7,5),
            new Block(6,5),
            new Block(5,5)
        ];
        this.direction = 'right'; 
        this.nextDirection = 'right'; 
    };
    Snake.prototype.draw = function () { 
        for (let i = 0; i < this.segments.length; i ++) { 
            this.segments[i].drawSquare(colorList[Math.floor(Math.random() * 7)]); 
        };
    };
    //Setting moving directions
    Snake.prototype.move = function () {
        let head = this.segments[0];
        let newHead; 
        this.direction = this.nextDirection;
    
        if (this.direction === 'right'){
            newHead = new Block(head.col + 1, head.row);
        } else if (this.direction === 'left') {
            newHead = new Block(head.col - 1, head.row);
        } else if (this.direction === 'up') {
            newHead = new Block(head.col, head.row - 1);
        } else if (this.direction === 'down') {
            newHead = new Block(head.col, head.row + 1)
        }
    
        if (this.checkCollision(newHead)) { 
            gameOver(); 
            return; 
        }
    
        this.segments.unshift(newHead); 
    
        if (newHead.equal(apple.position)) { 
            score ++; 
            apple.move();
            aniTime -= 1; 
        } else {
            this.segments.pop(); 
        }
    };
    //Define collision
    Block.prototype.equal = function (otherBlock) { 
        return this.col === otherBlock.col && this.row === otherBlock.row; 
    };
    Snake.prototype.checkCollision = function () {
        // FIX 3: make sure head is defined
        var head = this.segments[0];
        var leftCollision = (head.col === 0);
        var topCollision = (head.row === 0); 
        var rightCollision = (head.col === widthInBlocks - 1); 
        var bottomCollision = (head.row === heightInBlocks - 1); 
    
        var wallCollision = leftCollision || topCollision || 
        rightCollision || bottomCollision; 
    
        var selfCollision = false; 
    
        // Fix 4: start at 1 so head does not self collide
        for (let i = 1; i < this.segments.length; i ++) {
            if (head.equal(this.segments[i])) {
                selfCollision = true;
            }
        }
        return wallCollision || selfCollision 
    };
    
    Snake.prototype.setDirection = function (newDirection) {
        if (this.direction === 'up' && newDirection === 'down') {
            return; 
        } else if (this.direction === 'right' && newDirection ==='left') { 
            return;
        } else if (this.direction === 'down' && newDirection ==='up') { 
            return;
        } else if (this.direction === 'left' && newDirection ==='right') { 
            return;
        }
        this.nextDirection = newDirection; 
    };
    
    //run the game
    let snake = new Snake(); 
    let apple = new Apple(); 
    var aniTime = 100; 
    // FIX 2: declare at top level
    var timeOutId;
    function core () { 
        ctx.clearRect(0, 0, width, height); 
        drawScore(); 
        snake.move(); 
        snake.draw(); 
        apple.draw(); 
        drawBorder();
        timeOutId = setTimeout(core, aniTime);
        if (snake.checkCollision() === true) {
            clearTimeout(timeOutId);
            gameOver();
        };
    };
    core();
    
    //Game over condition
    // FIX 1: declare as function
    function gameOver() {
        clearTimeout(timeOutId);
        ctx.font = '60px Arial';
        ctx.fillStyle = 'Black';
        ctx.textAlign = 'center';
        ctx.textBaseLine = 'middle';
        ctx.fillText('Game Over', width / 2, height / 2);
    };
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <canvas id="canvas" width="300" height="300"></canvas>