Skip to content

Instantly share code, notes, and snippets.

@straker
Last active March 17, 2024 07:03
Star You must be signed in to star a gist
Save straker/ff00b4b49669ad3dec890306d348adc4 to your computer and use it in GitHub Desktop.
Basic Snake HTML and JavaScript Game

Basic Snake HTML and JavaScript Game

Snake is a fun game to make as it doesn't require a lot of code (less than 100 lines with all comments removed). This is a basic implementation of the snake game, but it's missing a few things intentionally and they're left as further exploration for the reader.

Further Exploration

  • Score
    • When the snake eats an apple, the score should increase by one. Use context.fillText() to display the score to the screen
  • Mobile and touchscreen support
  • Better apple spawning
    • Currently the apple spawns in any random grid in the game, even if the snake is already on that spot. Improve it so it only spawns in empty grid locations

Important note: I will answer questions about the code but will not add more features or answer questions about adding more features. This series is meant to give a basic outline of the game but nothing more.

License

(CC0 1.0 Universal) You're free to use this game and code in any project, personal or commercial. There's no need to ask permission before using these. Giving attribution is not required, but appreciated.

Other Basic Games

Support

Basic HTML Games are made possible by users like you. When you become a Patron, you get access to behind the scenes development logs, the ability to vote on which games I work on next, and early access to the next Basic HTML Game.

Top Patrons

  • Karar Al-Remahy
  • UnbrandedTech
  • Innkeeper Games
  • Nezteb
<!DOCTYPE html>
<html>
<head>
<title>Basic Snake HTML Game</title>
<meta charset="UTF-8">
<style>
html, body {
height: 100%;
margin: 0;
}
body {
background: black;
display: flex;
align-items: center;
justify-content: center;
}
canvas {
border: 1px solid white;
}
</style>
</head>
<body>
<canvas width="400" height="400" id="game"></canvas>
<script>
var canvas = document.getElementById('game');
var context = canvas.getContext('2d');
// the canvas width & height, snake x & y, and the apple x & y, all need to be a multiples of the grid size in order for collision detection to work
// (e.g. 16 * 25 = 400)
var grid = 16;
var count = 0;
var snake = {
x: 160,
y: 160,
// snake velocity. moves one grid length every frame in either the x or y direction
dx: grid,
dy: 0,
// keep track of all grids the snake body occupies
cells: [],
// length of the snake. grows when eating an apple
maxCells: 4
};
var apple = {
x: 320,
y: 320
};
// get random whole numbers in a specific range
// @see https://stackoverflow.com/a/1527820/2124254
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min)) + min;
}
// game loop
function loop() {
requestAnimationFrame(loop);
// slow game loop to 15 fps instead of 60 (60/15 = 4)
if (++count < 4) {
return;
}
count = 0;
context.clearRect(0,0,canvas.width,canvas.height);
// move snake by it's velocity
snake.x += snake.dx;
snake.y += snake.dy;
// wrap snake position horizontally on edge of screen
if (snake.x < 0) {
snake.x = canvas.width - grid;
}
else if (snake.x >= canvas.width) {
snake.x = 0;
}
// wrap snake position vertically on edge of screen
if (snake.y < 0) {
snake.y = canvas.height - grid;
}
else if (snake.y >= canvas.height) {
snake.y = 0;
}
// keep track of where snake has been. front of the array is always the head
snake.cells.unshift({x: snake.x, y: snake.y});
// remove cells as we move away from them
if (snake.cells.length > snake.maxCells) {
snake.cells.pop();
}
// draw apple
context.fillStyle = 'red';
context.fillRect(apple.x, apple.y, grid-1, grid-1);
// draw snake one cell at a time
context.fillStyle = 'green';
snake.cells.forEach(function(cell, index) {
// drawing 1 px smaller than the grid creates a grid effect in the snake body so you can see how long it is
context.fillRect(cell.x, cell.y, grid-1, grid-1);
// snake ate apple
if (cell.x === apple.x && cell.y === apple.y) {
snake.maxCells++;
// canvas is 400x400 which is 25x25 grids
apple.x = getRandomInt(0, 25) * grid;
apple.y = getRandomInt(0, 25) * grid;
}
// check collision with all cells after this one (modified bubble sort)
for (var i = index + 1; i < snake.cells.length; i++) {
// snake occupies same space as a body part. reset game
if (cell.x === snake.cells[i].x && cell.y === snake.cells[i].y) {
snake.x = 160;
snake.y = 160;
snake.cells = [];
snake.maxCells = 4;
snake.dx = grid;
snake.dy = 0;
apple.x = getRandomInt(0, 25) * grid;
apple.y = getRandomInt(0, 25) * grid;
}
}
});
}
// listen to keyboard events to move the snake
document.addEventListener('keydown', function(e) {
// prevent snake from backtracking on itself by checking that it's
// not already moving on the same axis (pressing left while moving
// left won't do anything, and pressing right while moving left
// shouldn't let you collide with your own body)
// left arrow key
if (e.which === 37 && snake.dx === 0) {
snake.dx = -grid;
snake.dy = 0;
}
// up arrow key
else if (e.which === 38 && snake.dy === 0) {
snake.dy = -grid;
snake.dx = 0;
}
// right arrow key
else if (e.which === 39 && snake.dx === 0) {
snake.dx = grid;
snake.dy = 0;
}
// down arrow key
else if (e.which === 40 && snake.dy === 0) {
snake.dy = grid;
snake.dx = 0;
}
});
// start the game
requestAnimationFrame(loop);
</script>
</body>
</html>
@GavinWilliams-ctrl
Copy link

Not My code but the code that I'm using

@JoshuaDS27
Copy link

SO GOOD! THANK YOU!

@1forest
Copy link

1forest commented Dec 8, 2020

Does anyone know how to add a sound every time the snake eats an apple

@andrew-purchase
Copy link

Does anyone know how to add a sound every time the snake eats an apple
You can import to your file using:
eat = new Audio('example.mp3');
And make it play using:
eat.play();

@misterslime
Copy link

misterslime commented Sep 15, 2021

I created a python version.
Basic Snake Python Game

import pygame
import random

# define some colors
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
GREEN = (0, 128, 0)

pygame.init()

# set the width and height of the screen [width, height]
size = (400, 400)
screen = pygame.display.set_mode(size)
pygame.display.set_caption("Basic Snake Python Game")
clock = pygame.time.Clock()

# allow us to close the game
done = False

count = 0
grid = 16

class Snake:
  x = 160
  y = 160

  # snake velocity. moves one grid length every frame in either the x or y direction
  dx = grid
  dy = 0

  # keep track of all grids the snake body occupies
  cells = []

  # length of the snake. grows when eating an apple
  maxCells = 4

class Apple:
  x = 320
  y = 320

snake = Snake()
apple = Apple()

# game loop
while not done:
  # event loop
  for event in pygame.event.get():
    # listen to keyboard events to move the snake
    if event.type == pygame.KEYDOWN:
      # prevent snake from backtracking on itself by checking that it's
      # not already moving on the same axis (pressing left while moving
      # left won't do anything, and pressing right while moving left
      # shouldn't let you collide with your own body) 

      if event.key == pygame.K_LEFT and snake.dx == 0:
        snake.dx = -grid;
        snake.dy = 0;
      if event.key == pygame.K_UP and snake.dy == 0:
        snake.dy = -grid;
        snake.dx = 0;
      if event.key == pygame.K_RIGHT and snake.dx == 0:
        snake.dx = grid;
        snake.dy = 0;
      if event.key == pygame.K_DOWN and snake.dy == 0:
        snake.dy = grid;
        snake.dx = 0;

    # close when the X is clicked
    if event.type == pygame.QUIT:
      done = True

  # slow game updates to 15 per second instead of 60 (60/15 = 4)
  count += 1
  if count >= 4:
    # move snake by it's velocity
    snake.x += snake.dx;
    snake.y += snake.dy;

    # wrap snake position horizontally on edge of screen
    if snake.x < 0:
      snake.x = screen.get_width() - grid;
    elif snake.x >= screen.get_width():
      snake.x = 0;

    # wrap snake position vertically on edge of screen
    if snake.y < 0:
      snake.y = screen.get_height() - grid
    elif snake.y >= screen.get_height():
      snake.y = 0

    # keep track of where snake has been. front of the array is always the head
    snake.cells.append([snake.x, snake.y]);

    # remove cells as we move away from them
    if len(snake.cells) > snake.maxCells:
      snake.cells.pop(0)
    
    count = 0;

    screen.fill(BLACK)
  
    # draw apple
    pygame.draw.rect(screen, RED, pygame.Rect(apple.x, apple.y, grid - 1, grid - 1))

    # draw snake one cell at a time
    index = 0
    for cell in snake.cells:
      # drawing 1 px smaller than the grid creates a grid effect in the snake body so you can see how long it is
      pygame.draw.rect(screen, GREEN, pygame.Rect(cell[0], cell[1], grid - 1, grid - 1))

      # snake ate apple
      if cell[0] == apple.x and cell[1] == apple.y:
        snake.maxCells += 1

        # canvas is 400x400 which is 25x25 grids
        apple.x = random.randint(0, 24) * grid
        apple.y = random.randint(0, 24) * grid
      
      # check collision with all cells after this one (modified bubble sort)
      for i in range(index + 1, len(snake.cells)):
        # snake occupies same space as a body part. reset game
        if cell[0] == snake.cells[i][0] and cell[1] == snake.cells[i][1]:
          snake.x = 160
          snake.y = 160
          snake.cells = []
          snake.maxCells = 4
          snake.dx = grid
          snake.dy = 0

          apple.x = random.randint(0, 24) * grid
          apple.y = random.randint(0, 24) * grid
      
      index += 1

    # update the screen with what we've drawn.
    pygame.display.flip()

  # limit to 60 frames per second
  clock.tick(60)

# close the window and quit.
pygame.quit()

Edit 1: changed the snake color to better match the HTML version.
Edit 2: removed input lag by making the game run at 60 fps while updating every 4 frames.
Edit 3: game renders and updates every 4 frames, inputs registered every frame. this solves a issue where the game 'jittered'.

@lemonadeblast125
Copy link

Does anbody know how to change the snake's body shape from square to circle?

@straker
Copy link
Author

straker commented Sep 27, 2021

@lemonadeblast125
Copy link

lemonadeblast125 commented Sep 27, 2021 via email

@straker
Copy link
Author

straker commented Sep 27, 2021

@lemonadeblast125 Are there any JavaScript errors in the DevTools console? context.arc takes different numbers than context.fillRect.

@lemonadeblast125
Copy link

lemonadeblast125 commented Sep 27, 2021 via email

@privatedev11
Copy link

I've found a bug. Here's a video that explains it:

2022-01-09.16-18-52.mp4

@cookieukw
Copy link

I've found a bug. Here's a video that explains it:

2022-01-09.16-18-52.mp4

I wanted to learn how to create games with html, I researched and found a code that was a fork of this one (nothing has changed) and I created a base game. I added a lot of stuff and a lot of features, but most of my time in this game was trying to fix this bug. I believe I fixed it, if I'm not mistaken it's at the moment that it checks if the snake arrived from one side to spawn on the other. I realized this when I had already changed the code a lot, so it might not be. It was the code snippet that was buggy because it took a pixel out of the snake's position, so the collision check didn't work. I will post my version soon. In fact, the code is much larger. one of the differences is that the snake does not die when it is hit by itself. This version also has an evil apple that makes the snake shrink in size and has the ability to change the screen resolution. that is, instead of 400x400 (blocks of 16 which gives 25x25), I made a system that takes and does everything automatically (autotuning). very good, I will post soon with MIT license

@straker
Copy link
Author

straker commented May 28, 2022

@privatedev11 @cookieukw yes, changing the screen size from 400x400 will change the pixel collision. You'll need to use values that perfectly divide by the grid size (no remainder) in order for the current collision method to work. So if you keep the 16px grid size, you need to use multiples of 16 for the size of the game.

The other way to handle that is to choose a grid size and desired number of grids for the width and height, and then calculate the game size from that (so more automatic rather than static).

@cookieukw
Copy link

@privatedev11 @cookieukw yes, changing the screen size from 400x400 will change the pixel collision. You'll need to use values that perfectly divide by the grid size (no remainder) in order for the current collision method to work. So if you keep the 16px grid size, you need to use multiples of 16 for the size of the game.

The other way to handle that is to choose a grid size and desired number of grids for the width and height, and then calculate the game size from that (so more automatic rather than static).

It took me a while, but now I've managed to create the repository (I've never messed with commit, pull request and branches before). If there is an error, let me know. Thank you so much for providing the code for a simple snake game, if it weren't for that I wouldn't have learned to use canvas

@Zarycoder
Copy link

#privatedev11 I ran the code and what you had say is truly true. I say that bug. I do not know where people come with the codes.

@A130-cmd
Copy link

I don't mean to be rude, but I just figured out that I could make programs on my TI-86 calculator not plus or plus CE which code would my calculator use?

Copy link

ghost commented Nov 30, 2022

how do I change the speed of the snake?

@straker
Copy link
Author

straker commented Dec 5, 2022

@PizzaGod999873 you can change the speed of the snake by changing the rate at which the game updates.

@EntertainingIndustries
Copy link

@RamiCarmy-eng
I added a few changes like the pause stops movement instead of using an alert and the score resets every time you die

<title>Snake Game</title> <script src="https://code.jquery.com/jquery-1.11.3.js"></script> <style> html, body { height: 100%; margin: 0; } body { background: black; display: flex; align-items: center; justify-content: center; } canvas { border: 1px solid white; background-color:gold; } #p1 { color:red; position:absolute; left:100px; top:10px; } #p2 { color:red; position:absolute; left:100px; top:30px; } #score { color:yellow; position:absolute; left:155px; top:10px; } #high { color:yellow; position:absolute; left:195px; top:30px; } #btn_stop { color:brown; position:absolute; left:100px; top:80px; } #end_msg { color:brown; position:absolute; left:100px; top:100px; } </style>

SCORE:

HIGHSCORE:

PAUSE

<script> var canvas = document.getElementById('game'); var context = canvas.getContext('2d'); var grid = 16; var count = 0; var score=0; //reading last score value if(localStorage.score){ document.getElementById('score').innerHTML=localStorage.score; }

var max=0;

var speed = 8;

var snake = {
x: 160,
y: 160,

// snake velocity. moves one grid length every frame in either the x or y direction
dx: grid,
dy: 0,

// keep track of all grids the snake body occupies
cells: [],

// length of the snake. grows when eating an apple
maxCells: 4
};
var apple = {
x: 320,
y: 320
};

// get random whole numbers in a specific range
// @see https://stackoverflow.com/a/1527820/2124254
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min)) + min;
}

// game loop
function loop() {

requestAnimationFrame(loop);
// slow game loop to 15 fps instead of 60 (60/15 = 4)
if (++count < speed) {
return;
}
count = 0;
context.clearRect(0,0,canvas.width,canvas.height);
// move snake by it's velocity
snake.x += snake.dx;
snake.y += snake.dy;
// wrap snake position horizontally on edge of screen
if (snake.x < 0) {
snake.x = canvas.width - grid;
}
else if (snake.x >= canvas.width) {
snake.x = 0;
}

// wrap snake position vertically on edge of screen
if (snake.y < 0) {
snake.y = canvas.height - grid;
}
else if (snake.y >= canvas.height) {
snake.y = 0;
}
// keep track of where snake has been. front of the array is always the head
snake.cells.unshift({x: snake.x, y: snake.y});
// remove cells as we move away from them
if (snake.cells.length > snake.maxCells) {
snake.cells.pop();
}
// draw apple
context.fillStyle = 'red';
context.fillRect(apple.x, apple.y, grid-1, grid-1);
// draw snake one cell at a time
context.fillStyle = 'green';
snake.cells.forEach(function(cell, index) {

// drawing 1 px smaller than the grid creates a grid effect in the snake body so you can see how long it is
context.fillRect(cell.x, cell.y, grid-1, grid-1);  
// snake ate apple
if (cell.x === apple.x && cell.y === apple.y) {
  snake.maxCells++;
  score+=1;
//saving score for next playing.
localStorage.setItem('score',score);
  //max=score;
  document.getElementById('score').innerHTML=score;

  // canvas is 400x400 which is 25x25 grids 
  apple.x = getRandomInt(0, 25) * grid;
  apple.y = getRandomInt(0, 25) * grid;
}
// check collision with all cells after this one (modified bubble sort)
for (var i = index + 1; i < snake.cells.length; i++)
{
  
  // snake occupies same space as a body part. reset game
  if (cell.x === snake.cells[i].x && cell.y === snake.cells[i].y) 
 { 
 
    if(score>max)
    {
     max=score;
    }
    score = 0;
	snake.x = 160;
    snake.y = 160;
    snake.cells = [];
    snake.maxCells = 4;
    snake.dx = grid;
    snake.dy = 0;
	//score=0;
    apple.x = getRandomInt(0, 25) * grid;
    apple.y = getRandomInt(0, 25) * grid;
    document.getElementById('high').innerHTML=max;
    document.getElementById('score').innerHTML=score;
  }
}

}
);
}
// listen to keyboard events to move the snake
document.addEventListener('keydown', function(e) {
// prevent snake from backtracking on itself by checking that it's
// not already moving on the same axis (pressing left while moving
// left won't do anything, and pressing right while moving left
// shouldn't let you collide with your own body)

// left arrow key
if (e.which === 37 && snake.dx === 0) {
snake.dx = -grid;
snake.dy = 0;
}
// up arrow key
else if (e.which === 38 && snake.dy === 0) {
snake.dy = -grid;
snake.dx = 0;
}
// right arrow key
else if (e.which === 39 && snake.dx === 0) {
snake.dx = grid;
snake.dy = 0;
}
// down arrow key
else if (e.which === 40 && snake.dy === 0) {
snake.dy = grid;
snake.dx = 0;
}
});
// start the game
requestAnimationFrame(loop);

//pause (makes the snake infinitely slow)
function myFunction() {
if (speed == Infinity) {
speed = 8;
}
else if (speed == 8) {
speed = Infinity;
}
}

</script>

@tycocrypto
Copy link

Great game! Is there a way to add high scores that use cookies to save peoples scores in their browser to display??

I have read about it a few other places but was wondering if there was a simple string to use in this case.

@IRAMBONARevis
Copy link

thamks

@AstroIND
Copy link

I know that this is old, but can someone make a version that works on apple script editor?

Will it work?

@fyuval13
Copy link

how can i change this code so that when the snake hits the wall, the game ends and it does not teleport to the opposite wall?

@straker
Copy link
Author

straker commented Aug 21, 2023

@pra154
Copy link

pra154 commented Oct 5, 2023

good

@ZuuZoo
Copy link

ZuuZoo commented Nov 9, 2023

How can i change that to everytime when the snake ates a apple it's spawns a new delayed and i don't want the delay. How can i fix that?

@alexMGr8
Copy link

if you don't know how to play the game go onto https://onecompiler.com/html/ and paste all the code, you can play it on the window on the right

@riiamri23
Copy link

riiamri23 commented Dec 7, 2023

just wanna share my work, it's references from YouTube tutorial but the code is different

const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const assetsOnline = 'https://raw.githubusercontent.com/riiamri23/gamelist/3fa07387e471f823aa4bb7fc49525e0ece2f35bb/released/snake/';

const headX = 10;
const headY = 10;
const parts = [];

const appleX = 5;
const appleY = 5;
const tileCount = 20;
const tileSize = canvas.width / tileCount - 2;
let velocityX = 0;
let velocityY = 0;


const speed = 7;

const gulpSound = new Audio(`${assetsOnline}assets/gulp.mp3`);
const gameOverSound = new Audio(`${assetsOnline}assets/game-over.mp3`)

class SnakeParts{
    constructor(X,Y){
        this.X = X;
        this.Y = Y;
    }
}

class Snake{
    constructor(headX, headY, tileCount, tileSize, color, parts){
        this.headX = headX;
        this.headY = headY;
        this.tileCount = tileCount;
        this.tileSize = tileSize;
        this.color = color;
        this.parts = parts;
        this.tailLength = 0;
    }

    draw = function(){
        ctx.beginPath();
        ctx.fillStyle = this.color;
        ctx.fillRect(this.headX * this.tileCount
            , this.headY * this.tileCount
            , this.tileSize
            , this.tileSize
        );
        this.drawTail();

        ctx.closePath();
    }

    addTail = function(){
        this.tailLength++;
    }

    drawTail = function(){
        for(let i=0;i < this.parts.length;i++){
            const snakePart = this.parts[i];
            ctx.fillStyle = "grey";
            ctx.fillRect(
                snakePart.X * this.tileCount
                , snakePart.Y * this.tileCount
                , this.tileSize
                , this.tileSize
            );
        }
        this.parts.push(new SnakeParts(this.headX, this.headY));

        while(this.parts.length > this.tailLength)
            this.parts.shift();
    }

    changePosition = function(velocityX, velocityY){
        this.headX = this.headX + velocityX;
        this.headY = this.headY + velocityY;
    }
}

class Apple{
    constructor(appleX, appleY, tileCount, tileSize){
        this.appleX = appleX;
        this.appleY = appleY;
        this.tileCount = tileCount;
        this.tileSize = tileSize;

    }

    draw = function(){
        ctx.beginPath();
        ctx.fillStyle = "red";
        ctx.fillRect(this.appleX * this.tileCount, this.appleY * this.tileCount, this.tileSize, this.tileSize);
        ctx.closePath();
    }

    randomAppear = function(isCollision){
        if(isCollision){
            this.appleX = Math.floor(Math.random() * this.tileCount);
            this.appleY = Math.floor(Math.random() * this.tileCount);
        }
        this.draw();
    }
}

class Score{
    constructor(totalScore){
        this.totalScore = totalScore;
    }

    draw = function(){
        ctx.fillStyle = "white";
        ctx.font = "10px Verdana";
        ctx.fillText("Score " + this.totalScore, 0, 10);
    }

    drawHighestScore = function(){
        ctx.fillStyle = "white";
        ctx.font = "10px Verdana";
        const highestScore = localStorage.getItem("highestScore") !== null ? localStorage.getItem("highestScore") : 0;

        ctx.fillText("Highest Score " + highestScore, canvas.width-100, 10);
    }

    incScore = function(){
        this.totalScore++;
    }

    setHighestScore = function(){
        const highestScore = localStorage.getItem("highestScore") !== null ? localStorage.getItem("highestScore") : 0;
        if(this.totalScore > highestScore) localStorage.setItem("highestScore", this.totalScore);
    }
}
let snake = new Snake(
    headX,
    headY,
    tileCount,
    tileSize,
    "white",
    parts,
);

let apple = new Apple(
    Math.floor(Math.random() * tileCount),
    Math.floor(Math.random() * tileCount),
    tileCount,
    tileSize
);

let score = new Score(0);

function checkGameOver(snake){
    let isOver = false;

    // check collidate with wall;
    if(snake.headX < 0 
        || snake.headX === tileCount 
        || snake.headY < 0 
        || snake.headY === tileCount
        ) isOver = true;

    // check collidate with tail;
    for(let i = 0; i < snake.parts.length; i++)
        if(snake.headX === snake.parts[i].X 
            && snake.headY === snake.parts[i].Y
            ) isOver = true;

    if(isOver){
        // create label on screen
      
        ctx.beginPath();
      
        ctx.fillStyle = "white";
        ctx.font = "50px Verdana";
        
        ctx.fillText("Game Over", canvas.width / 6.5, canvas.height / 2);
      
        ctx.font = "25px Verdana";
        ctx.fillText("Enter to play again", canvas.width / 6.5, canvas.height / 1.8);
      
        ctx.closePath();
      
    }

    return isOver;
}

checkOnCollision = function(snake, apple){
    if(snake.headX === apple.appleX 
        && snake.headY === apple.appleY
        ) {
            gulpSound.play();
            return true;
        }
    
    return false;
}


function clearScreen(){
      // Start a new Path
      ctx.beginPath();
      ctx.fillStyle = 'black';
      ctx.fillRect(0,0, canvas.width, canvas.height);
      ctx.closePath();
  
}

document.body.addEventListener('keydown', keyDown);

function keyDown(event){
    if(event.keyCode === 38){ // up
        if(velocityY === 1) return;
        velocityX = 0;
        velocityY = -1;
    }
    if(event.keyCode === 40){ // down
        if(velocityY === -1) return;
        velocityX = 0;
        velocityY = 1;
    }

    if(event.keyCode === 37){ // left
        if(velocityX === 1) return;
        velocityX = -1;
        velocityY = 0;
    }
    if(event.keyCode === 39){ // right
        if(velocityX === -1) return;
        velocityX = 1;
        velocityY = 0;
    }
  
    if(event.keyCode === 13){
      velocityX = 0;
      velocityY = 0;
      snake = new Snake(
          headX,
          headY,
          tileCount,
          tileSize,
          "white",
          parts,
      );

      apple = new Apple(
          Math.floor(Math.random() * tileCount),
          Math.floor(Math.random() * tileCount),
          tileCount,
          tileSize
      );

      score = new Score(0);
      
      drawGame();
      
    } 
}

function drawGame(){
    snake.changePosition(velocityX, velocityY);

    // check game over
    let isOver = checkGameOver(snake);
    if(isOver) {
        gameOverSound.play();

        score.setHighestScore();
        score.drawHighestScore();
        return;
    }
    clearScreen();
    
    snake.draw();
    score.draw();
    score.drawHighestScore();

    let isCollision = checkOnCollision(snake, apple);
  
    apple.randomAppear(isCollision);
    if(isCollision){
      snake.addTail();
      score.incScore();
    }

    setTimeout(drawGame, 1000/speed);
}

drawGame();

@fal3n-4ngel
Copy link

So I kinda migrated it to a nextjs typescript project and added a touch screen support , also added a score function.. just basic level modifications

// app/components/SnakeGame.tsx
import React, { useEffect, useRef, useState } from 'react';

interface SnakeGameProps {
    onValueChange: (newValue: number) => void;
}

const SnakeGame: React.FC<SnakeGameProps> = ({ onValueChange }) => {
    let gameOver = false;
    const [go, setGo] = useState(false);
    const scoreRef = useRef(0);
    const grid = 20;
    let count = 0;

    function getRandomInt(min: number, max: number): number {
        return Math.floor(Math.random() * (max - min)) + min;
    }

    useEffect(() => {
        interface Snake {
            x: number;
            y: number;
            dx: number;
            dy: number;
            cells: Array<{ x: number; y: number }>;
            maxCells: number;
        }

        const snake: Snake = {
            x: 160,
            y: 160,
            dx: grid,
            dy: 0,
            cells: [],
            maxCells: 4,
        };
        const apple = {
            x: 60,
            y: 60,
        };

        function loop() {
            requestAnimationFrame(loop);
            if (!go) {
                if (++count < 20) {
                    return;
                }

                gameOver = false;
                count = 0;
                const canvas = document.getElementById('game') as HTMLCanvasElement;
                const context = canvas.getContext('2d') as CanvasRenderingContext2D;

                context.clearRect(0, 0, canvas.width, canvas.height);

                snake.x += snake.dx;
                snake.y += snake.dy;

                if (snake.x < 0) {
                    snake.x = canvas.width - grid;
                } else if (snake.x >= canvas.width) {
                    snake.x = 0;
                }

                if (snake.y < 0) {
                    snake.y = canvas.height - grid;
                } else if (snake.y >= canvas.height) {
                    snake.y = 0;
                }

                snake.cells.unshift({ x: snake.x, y: snake.y });

                if (snake.cells.length > snake.maxCells) {
                    snake.cells.pop();
                }

                context.fillStyle = 'red';
                context.fillRect(apple.x, apple.y, grid - 1, grid - 1);

                context.fillStyle = 'black';
                snake.cells.forEach(function (cell, index) {
                    context.fillRect(cell.x, cell.y, grid - 1, grid - 1);

                    if (cell.x === apple.x && cell.y === apple.y) {
                        snake.maxCells++;
                        scoreRef.current = scoreRef.current + 1;
                        onValueChange(scoreRef.current);
                        console.log(scoreRef.current);
                        apple.x = getRandomInt(0, 25) * grid;
                        apple.y = getRandomInt(0, 25) * grid;
                    }

                    for (var i = index + 1; i < snake.cells.length; i++) {
                        if (cell.x === snake.cells[i].x && cell.y === snake.cells[i].y) {
                            console.log('Game Over');
                            snake.x = 160;
                            snake.y = 160;
                            snake.cells = [];
                            snake.maxCells = 4;
                            snake.dx = grid;
                            snake.dy = 0;

                            apple.x = getRandomInt(0, 25) * grid;
                            apple.y = getRandomInt(0, 25) * grid;
                            gameOver = true;
                        }
                    }

                    if (gameOver) {
                        setGo(true);
                    }
                });
            }
        }

        const handleKeyDown = (e: KeyboardEvent) => {
            if (e.which === 37 && snake.dx === 0) {
                snake.dx = -grid;
                snake.dy = 0;
            } else if (e.which === 38 && snake.dy === 0) {
                snake.dy = -grid;
                snake.dx = 0;
            } else if (e.which === 39 && snake.dx === 0) {
                snake.dx = grid;
                snake.dy = 0;
            } else if (e.which === 40 && snake.dy === 0) {
                snake.dy = grid;
                snake.dx = 0;
            }
        };

        const handleTouchStart = (e: TouchEvent) => {
            const { clientX, clientY } = e.touches[0];
            const deltaX = clientX - snake.x;
            const deltaY = clientY - snake.y;

            if (Math.abs(deltaX) > Math.abs(deltaY)) {
                // Horizontal swipe
                if (deltaX > 0 && snake.dx === 0) {
                    snake.dx = grid;
                    snake.dy = 0;
                } else if (deltaX < 0 && snake.dx === 0) {
                    snake.dx = -grid;
                    snake.dy = 0;
                }
            } else {
                // Vertical swipe
                if (deltaY > 0 && snake.dy === 0) {
                    snake.dy = grid;
                    snake.dx = 0;
                } else if (deltaY < 0 && snake.dy === 0) {
                    snake.dy = -grid;
                    snake.dx = 0;
                }
            }
        };

        const handleTouchEnd = () => {
            // Handle touch end if needed
        };
        const handleResize = () => {
            const canvas = document.getElementById('game') as HTMLCanvasElement;
            canvas.width = canvas.offsetWidth;
            canvas.height = canvas.offsetHeight;
        };
    
        // Call handleResize once to set initial dimensions
        handleResize();
    
        // Add event listener for window resize
        window.addEventListener('resize', handleResize);

        document.addEventListener('keydown', handleKeyDown);
        document.addEventListener('touchstart', handleTouchStart);
        document.addEventListener('touchend', handleTouchEnd);

        requestAnimationFrame(loop);

        return () => {
            document.removeEventListener('keydown', handleKeyDown);
            document.removeEventListener('touchstart', handleTouchStart);
            document.removeEventListener('touchend', handleTouchEnd);
            window.removeEventListener('resize', handleResize);
        };
    }, [gameOver]);

    const handleRestart = () => {
        setGo(false);
        
        scoreRef.current = 0;
        onValueChange(scoreRef.current);
        localStorage.setItem('score', '0');
    };

    return (
        <div className="flex w-full justify-center items-center bg-white">
            <canvas id="game" width="1000" height="700" className={`bg-[#f8f8f8] ${go ? 'hidden' : ''} `}></canvas>
            {go && (
                <div className={`flex flex-col items-center justify-center w-full h-full text-black text-6xl   ${!go ? 'hidden' : ''}`}>
                    <div className='p-5'>Game Over. </div>
                    <button onClick={handleRestart} className='bg-black rounded-md p-2 text-3xl text-white'>Restart</button>
                </div>
            )}
        </div>
    );
};

export default SnakeGame;

@maddyapk
Copy link

maddyapk commented Mar 2, 2024

So I kinda migrated it to a nextjs typescript project and added a touch screen support , also added a score function.. just basic level modifications

// app/components/SnakeGame.tsx
import React, { useEffect, useRef, useState } from 'react';

interface SnakeGameProps {
    onValueChange: (newValue: number) => void;
}

const SnakeGame: React.FC<SnakeGameProps> = ({ onValueChange }) => {
    let gameOver = false;
    const [go, setGo] = useState(false);
    const scoreRef = useRef(0);
    const grid = 20;
    let count = 0;

    function getRandomInt(min: number, max: number): number {
        return Math.floor(Math.random() * (max - min)) + min;
    }

    useEffect(() => {
        interface Snake {
            x: number;
            y: number;
            dx: number;
            dy: number;
            cells: Array<{ x: number; y: number }>;
            maxCells: number;
        }

        const snake: Snake = {
            x: 160,
            y: 160,
            dx: grid,
            dy: 0,
            cells: [],
            maxCells: 4,
        };
        const apple = {
            x: 60,
            y: 60,
        };

        function loop() {
            requestAnimationFrame(loop);
            if (!go) {
                if (++count < 20) {
                    return;
                }

                gameOver = false;
                count = 0;
                const canvas = document.getElementById('game') as HTMLCanvasElement;
                const context = canvas.getContext('2d') as CanvasRenderingContext2D;

                context.clearRect(0, 0, canvas.width, canvas.height);

                snake.x += snake.dx;
                snake.y += snake.dy;

                if (snake.x < 0) {
                    snake.x = canvas.width - grid;
                } else if (snake.x >= canvas.width) {
                    snake.x = 0;
                }

                if (snake.y < 0) {
                    snake.y = canvas.height - grid;
                } else if (snake.y >= canvas.height) {
                    snake.y = 0;
                }

                snake.cells.unshift({ x: snake.x, y: snake.y });

                if (snake.cells.length > snake.maxCells) {
                    snake.cells.pop();
                }

                context.fillStyle = 'red';
                context.fillRect(apple.x, apple.y, grid - 1, grid - 1);

                context.fillStyle = 'black';
                snake.cells.forEach(function (cell, index) {
                    context.fillRect(cell.x, cell.y, grid - 1, grid - 1);

                    if (cell.x === apple.x && cell.y === apple.y) {
                        snake.maxCells++;
                        scoreRef.current = scoreRef.current + 1;
                        onValueChange(scoreRef.current);
                        console.log(scoreRef.current);
                        apple.x = getRandomInt(0, 25) * grid;
                        apple.y = getRandomInt(0, 25) * grid;
                    }

                    for (var i = index + 1; i < snake.cells.length; i++) {
                        if (cell.x === snake.cells[i].x && cell.y === snake.cells[i].y) {
                            console.log('Game Over');
                            snake.x = 160;
                            snake.y = 160;
                            snake.cells = [];
                            snake.maxCells = 4;
                            snake.dx = grid;
                            snake.dy = 0;

                            apple.x = getRandomInt(0, 25) * grid;
                            apple.y = getRandomInt(0, 25) * grid;
                            gameOver = true;
                        }
                    }

                    if (gameOver) {
                        setGo(true);
                    }
                });
            }
        }

        const handleKeyDown = (e: KeyboardEvent) => {
            if (e.which === 37 && snake.dx === 0) {
                snake.dx = -grid;
                snake.dy = 0;
            } else if (e.which === 38 && snake.dy === 0) {
                snake.dy = -grid;
                snake.dx = 0;
            } else if (e.which === 39 && snake.dx === 0) {
                snake.dx = grid;
                snake.dy = 0;
            } else if (e.which === 40 && snake.dy === 0) {
                snake.dy = grid;
                snake.dx = 0;
            }
        };

        const handleTouchStart = (e: TouchEvent) => {
            const { clientX, clientY } = e.touches[0];
            const deltaX = clientX - snake.x;
            const deltaY = clientY - snake.y;

            if (Math.abs(deltaX) > Math.abs(deltaY)) {
                // Horizontal swipe
                if (deltaX > 0 && snake.dx === 0) {
                    snake.dx = grid;
                    snake.dy = 0;
                } else if (deltaX < 0 && snake.dx === 0) {
                    snake.dx = -grid;
                    snake.dy = 0;
                }
            } else {
                // Vertical swipe
                if (deltaY > 0 && snake.dy === 0) {
                    snake.dy = grid;
                    snake.dx = 0;
                } else if (deltaY < 0 && snake.dy === 0) {
                    snake.dy = -grid;
                    snake.dx = 0;
                }
            }
        };

        const handleTouchEnd = () => {
            // Handle touch end if needed
        };
        const handleResize = () => {
            const canvas = document.getElementById('game') as HTMLCanvasElement;
            canvas.width = canvas.offsetWidth;
            canvas.height = canvas.offsetHeight;
        };
    
        // Call handleResize once to set initial dimensions
        handleResize();
    
        // Add event listener for window resize
        window.addEventListener('resize', handleResize);

        document.addEventListener('keydown', handleKeyDown);
        document.addEventListener('touchstart', handleTouchStart);
        document.addEventListener('touchend', handleTouchEnd);
        https://maddyapk.com/
        requestAnimationFrame(loop);

        return () => {
            document.removeEventListener('keydown', handleKeyDown);
            document.removeEventListener('touchstart', handleTouchStart);
            document.removeEventListener('touchend', handleTouchEnd);
            window.removeEventListener('resize', handleResize);
        };
    }, [gameOver]);

    const handleRestart = () => {
        setGo(false);
        
        scoreRef.current = 0;
        onValueChange(scoreRef.current);
        localStorage.setItem('score', '0');
    };

    return (
        <div className="flex w-full justify-center items-center bg-white">
            <canvas id="game" width="1000" height="700" className={`bg-[#f8f8f8] ${go ? 'hidden' : ''} `}></canvas>
            {go && (
                <div className={`flex flex-col items-center justify-center w-full h-full text-black text-6xl   ${!go ? 'hidden' : ''}`}>
                    <div className='p-5'>Game Over. </div>
                    <button onClick={handleRestart} className='bg-black rounded-md p-2 text-3xl text-white'>Restart</button>
                </div>
            )}
        </div>
    );
};

export default SnakeGame;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment