ryno
ryno

Reputation: 966

Javascript Snake Implementation

In an attempt to begin learning Javascript, I decided to implement the well-known snake game. I have been trying to follow as little as possible in regards to tutorials to try and best understand what I am doing.

So far I have gotten my snake to move, eat food, and "grow", however upon growing the cells do not follow the main snake cell. I have tried troubleshooting this for a while now, and some possible solutions I came up with include: starting with multiple cells rather than 1, storing the position of the prior location of the main cell and placing the new cell there, or even changing the implementation completely.

I am unsure what route to take, and I feel as if I am close to getting it to work, but just not quite. Currently, the cells are created but relocated onto the parent cell and follow that same cell.

const canvas = document.getElementById("canvas");
const context = canvas.getContext("2d");

const HEIGHT = 400;
const WIDTH = 400;
const SCALE = 20;

window.addEventListener("keydown", (event) => {
    const direction = event.key.replace("Arrow", "");
    snake.update(direction);
});

function setup() {
    canvas.height = HEIGHT;
    canvas.width = WIDTH;
    food.createFood();

    window.setInterval(() => {
        context.clearRect(0, 0, WIDTH, HEIGHT);
        snake.move(snake.xSpeed, snake.ySpeed);
        food.drawFood();
        snake.draw();
        snake.shiftCells();
    }, 200);
}

let snake = {
    cells: [{ x: 200, y: 200 }],
    position: {
        x: 200,
        y: 200,
    },
    xSpeed: SCALE,
    ySpeed: 0,
    length: 1,
    draw: () => {
        for (cell of snake.cells) {
            context.fillStyle = "#C2F970";
            context.fillRect(cell.x, cell.y, SCALE, SCALE);
        }
    },
    update: (direction) => {
        switch (direction) {
            case "Up":
                snake.ySpeed = -1 * SCALE;
                snake.xSpeed = 0;
                break;
            case "Down":
                snake.ySpeed = 1 * SCALE;
                snake.xSpeed = 0;
                break;

            case "Left":
                snake.xSpeed = -1 * SCALE;
                snake.ySpeed = 0;
                break;

            case "Right":
                snake.xSpeed = 1 * SCALE;
                snake.ySpeed = 0;
                break;
        }
    },
    move: (xDist, yDist) => {
        if (snake.cells[0].x == food.position.x && snake.cells[0].y == food.position.y) {
            snake.eat();
        }

        // x-position
        if (snake.cells[0].x + xDist < 0) {
            snake.cells[0].x = WIDTH;
        } else if (snake.cells[0].x + xDist > WIDTH) {
            snake.cells[0].x = 0;
        } else {
            snake.cells[0].x += xDist;
        }

        // y-position
        if (snake.cells[0].y + yDist < 0) {
            snake.cells[0].y = HEIGHT;
        } else if (snake.cells[0].y + yDist > HEIGHT) {
            snake.cells[0].y = 0;
        } else {
            snake.cells[0].y += yDist;
        }
    },
    shiftCells: () => {
        for (let i = length; i > 1; i--) {
            snake.cells[i] = snake.cells[i - 1];
        }
    },
    eat: () => {
        snake.grow();
        food.createFood();
    },
    grow: () => {
        snake.cells.push({
            x: snake.cells[snake.length - 1].x,
            y: snake.cells[snake.length - 1].y,
        });
        snake.cells[0].x += snake.xSpeed;
        snake.cells[0].y += snake.ySpeed;
        length++;
    },
};

let food = {
    position: {
        x: 0,
        y: 0,
    },
    createFood: () => {
        food.position.x = SCALE * Math.floor(Math.random() * (WIDTH / SCALE));
        food.position.y = SCALE * Math.floor(Math.random() * (HEIGHT / SCALE));
    },
    drawFood: () => {
        context.fillStyle = "#D3FCD5";
        context.fillRect(food.position.x, food.position.y, SCALE, SCALE);
    },
};

setup();

Any feedback on how to get the cells to follow properly and even any tips on how to more properly structure my code would be much appreciated (eg. should snake be implemented in a different file? In a class?).

Upvotes: 0

Views: 423

Answers (1)

Simon Jacobs
Simon Jacobs

Reputation: 6648

First, great stuff on the neat way you've laid out your code. You've clearly got a lot of potential at this, but there are nevertheless few things that you can improve.

The mechanism to get the snake's growth to work is a bit subtle. I've put some working code below but you'll probably want to ignore it at first and try and get it to work given the following observations:

  • When you access the snake's length you use length not snake.length
  • Your loop to shift cells starts with an index equal to the array length and stops before it gets to the second item in the array. You really want to be stepping from length - 1 to 1 inclusive
  • You want to call shiftCells before you move the first cell of the snake otherwise you are placing the second cell in the new position of the first cell instead of its one before the snake moves
  • When you copy over the cells in the snake's tail you copy the references to the cells, which results in the second cell holding a reference to the first, so that both move to the same point on subsequent turns... to avoid this you are better off just copying the coordinates, as you correctly do in the grow method
  • When the snake grows, you need to be careful during the shifting. The snake tail, if you will, needs to be left behind for one turn to allow it to grow. This can be achieved by not shifting the snake's tail in a turn where an eat occurs
  • In your grow method you increment the first cell based on the speed. This doesn't seem the right place for this given you have other code dedicated to moving the first cell and dealing with the wrapping problem at the same time

Some more (incomplete) observations on the code are:

  • You're better off without a snake length property as this information is contained in the length of the cells array. That's duplicating information and that means there's a risk it becomes inconsistent
  • You might want to look into the idea of classes v objects at some point soon. You're using single objects here with no capacity to produce unique objects of the same type: that might be nice with, for example, your (x,y) points

Working code:

const canvas = document.getElementById("canvas");
const context = canvas.getContext("2d");

const HEIGHT = 400;
const WIDTH = 400;
const SCALE = 20;

window.addEventListener("keydown", (event) => {
    const direction = event.key.replace("Arrow", "");
    snake.update(direction);
});

function setup() {
    canvas.height = HEIGHT;
    canvas.width = WIDTH;
    food.createFood();

    window.setInterval(() => {
        context.clearRect(0, 0, WIDTH, HEIGHT);
        snake.move(snake.xSpeed, snake.ySpeed);
        food.drawFood();
        snake.draw();
    }, 200);
}

let snake = {
    cells: [{ x: 200, y: 200 }],
    position: {
        x: 200,
        y: 200,
    },
    xSpeed: SCALE,
    ySpeed: 0,
    length: 1,
    draw: () => {
        snake.cells.forEach( cell => {
            context.fillStyle = "#C2F970";
            context.fillRect( cell.x, cell.y, SCALE, SCALE );
        })
    },
    update: (direction) => {
        switch (direction) {
            case "Up":
                snake.ySpeed = -1 * SCALE;
                snake.xSpeed = 0;
                break;
            case "Down":
                snake.ySpeed = 1 * SCALE;
                snake.xSpeed = 0;
                break;
            case "Left":
                snake.xSpeed = -1 * SCALE;
                snake.ySpeed = 0;
                break;

            case "Right":
                snake.xSpeed = 1 * SCALE;
                snake.ySpeed = 0;
                break;
        }
    },
    move: (xDist, yDist) => {

        if (snake.cells[0].x == food.position.x && snake.cells[0].y == food.position.y) {
            snake.eat();
            snake.shiftCells( snake.length - 1 )
            snake.moveFirstCell( xDist, yDist )
        }
        else {
            snake.shiftCells( snake.length )
            snake.moveFirstCell( xDist, yDist )
        }

        },

    moveFirstCell: ( xDist, yDist ) => { 
        // x-position
        if (snake.cells[0].x + xDist < 0) {
            snake.cells[0].x = WIDTH;
        } else if (snake.cells[0].x + xDist > WIDTH) {
            snake.cells[0].x = 0;
        } else {
            snake.cells[0].x += xDist;
        }

        // y-position
        if (snake.cells[0].y + yDist < 0) {
            snake.cells[0].y = HEIGHT;
        } else if (snake.cells[0].y + yDist > HEIGHT) {
            snake.cells[0].y = 0;
        } else {
            snake.cells[0].y += yDist;
        }

    },
    shiftCells: length => {
        for (let i = length - 1; i >= 1; i--) {
            snake.cells[i].x = snake.cells[i - 1].x;
            snake.cells[i].y = snake.cells[i - 1].y;
        }        
    },
    eat: () => {
        snake.grow();
        food.createFood();
    },
    grow: () => {
        snake.cells.push({
            x: snake.cells[snake.length - 1].x,
            y: snake.cells[snake.length - 1].y,
        });
        snake.length++;
    },
};

let food = {
    position: {
        x: 0,
        y: 0,
    },
    createFood: () => {
        food.position.x = SCALE * Math.floor(Math.random() * (WIDTH / SCALE));
        food.position.y = SCALE * Math.floor(Math.random() * (HEIGHT / SCALE));
    },
    drawFood: () => {
        context.fillStyle = "#D3FCD5";
        context.fillRect(food.position.x, food.position.y, SCALE, SCALE);
    },
};

setup();

Upvotes: 1

Related Questions