GAZAR

Principal Engineer | Mentor

Use arrow keys to rotate the snake

Snake Game

Snake Game

The Snake game is a classic arcade-style game implemented using JavaScript, HTML, and CSS. In this game, players control a snake that moves around a grid-like game board, eating food pellets to grow longer while avoiding collisions with the walls of the game area and the snake's own body. The snake's movement is controlled by arrow keys or touch gestures on mobile devices, allowing players to navigate the snake across the screen. As the snake consumes food pellets, its length increases, making navigation more challenging. The game continues until the snake collides with a wall or itself, at which point the game ends, and the player's score is recorded. The Snake game is a popular choice for beginner programmers learning JavaScript due to its simplicity in design and implementation, making it an excellent project for practising programming concepts such as event handling, game logic, and user interface design.

Food Class

import type Board from "./Board";
class Food {
  private x: number;
  private y: number;
  private eaten: boolean = false;
  private board: Board;

  constructor(board: Board, x: number, y: number) {
    this.x = x;
    this.y = y;
    this.board = board;
    const cell = this.board.getCell(x, y);
    if (!cell) {
      throw new Error("Cell not found");
    }
    cell.occupy(this);
  }

  public getFoodLocation() {
    return {
      x: this.x,
      y: this.y,
    };
  }

  public setFoodLocation(x: number, y: number) {
    const cell = this.board.getCell(x, y);
    if (!cell) {
      throw new Error("Cell not found");
    }
    cell.occupy(this);
    this.x = x;
    this.y = y;
  }

  public setEaten() {
    const cell = this.board.getCell(this.x, this.y);
    if (!cell) {
      throw new Error("Cell not found");
    }
    cell.unoccupy();
    this.eaten = true;
  }

  public getEaten() {
    return this.eaten;
  }
}

export default Food;

And Snake Class will be something like:

import type Board from "./Board";
import type Cell from "./Cell";
import Food from "./Food";

// 0 -> Head
// length - 1 -> Tail

class Snake {
  private body: Array<{ x: number; y: number }> = [];
  private board: Board;
  private direction: string = "RIGHT";

  constructor(board: Board, x: number, y: number) {
    this.body.push({ x, y });
    this.board = board;
    const cell = this.board.getCell(x, y);
    if (!cell) {
      throw new Error("Cell not found");
    }
    cell.occupy(this);
  }

  public getBody() {
    return this.body;
  }

  public getDirection() {
    return this.direction;
  }

  public setDirection(direction: string) {
    this.direction = direction;
  }

  public turnRight() {
    switch (this.direction) {
      case "UP":
        this.direction = "RIGHT";
        break;
      case "DOWN":
        this.direction = "LEFT";
        break;
      case "RIGHT":
        this.direction = "DOWN";
        break;
      case "LEFT":
        this.direction = "UP";
        break;
    }
  }

  public turnLeft() {
    switch (this.direction) {
      case "UP":
        this.direction = "LEFT";
        break;
      case "DOWN":
        this.direction = "RIGHT";
        break;
      case "LEFT":
        this.direction = "DOWN";
        break;
      case "RIGHT":
        this.direction = "UP";
        break;
    }
  }

  public getNewHead() {
    const newHead = { ...this.body[0] };
    switch (this.direction) {
      case "UP":
        newHead.y += 1;
        break;
      case "DOWN":
        newHead.y -= 1;
        break;
      case "LEFT":
        newHead.x -= 1;
        break;
      case "RIGHT":
        newHead.x += 1;
        break;
    }
    return newHead;
  }

  public move(): string {
    const newHead = this.getNewHead();
    if (!this.canIMove(newHead)) {
      return "Game Over";
    }
    if (this.isCollidingWithFood(newHead)) {
      this.cleanTheBoardFromSnake();
      this.grow(newHead);
      this.occupyTheBoardWithSnake();
      return "Eaten";
    }
    this.cleanTheBoardFromSnake();
    this.body.pop();
    this.body.unshift(newHead);
    this.occupyTheBoardWithSnake();
    return "Moved";
  }

  public isHead = (cell: Cell) => {
    return this.body[0].x === cell.getX() && this.body[0].y === cell.getY();
  };

  public canIMove(newHead: { x: number; y: number }) {
    if (this.isCollidingWithItself(newHead)) {
      return false;
    }

    if (this.isCollidingWithWall(newHead)) {
      return false;
    }
    return true;
  }

  public cleanTheBoardFromSnake() {
    this.body.forEach((part) => {
      this.board.getCell(part.x, part.y).unoccupy();
    });
  }

  public occupyTheBoardWithSnake() {
    this.body.forEach((part) => {
      this.board.getCell(part.x, part.y).occupy(this);
    });
  }

  public grow(newHead: { x: number; y: number }) {
    this.body.unshift(newHead);
  }

  public isCollidingWithItself(newHead: { x: number; y: number }) {
    return this.body
      .slice(1)
      .some((part) => part.x === newHead.x && part.y === newHead.y);
  }

  public isCollidingWithWall(newHead: { x: number; y: number }) {
    return (
      newHead.x < 0 ||
      newHead.y < 0 ||
      newHead.x >= this.board.getBoardSize() ||
      newHead.y >= this.board.getBoardSize()
    );
  }

  public isCollidingWithFood(newHead: { x: number; y: number }) {
    const food = this.board.getCell(newHead.x, newHead.y)?.occupant;
    if (!food || food instanceof Snake) {
      return false;
    }
    food.setEaten();
    return food instanceof Food;
  }

  public isCollidingWithPoint(point: { x: number; y: number }) {
    const head = this.body[0];
    return head.x === point.x && head.y === point.y;
  }

  public isCollidingWithPoints(points: Array<{ x: number; y: number }>) {
    return points.some((point) => this.isCollidingWithPoint(point));
  }
}

export default Snake;