Snake: Survival of the Fittest Edition (Final)

Introduction

Snake: Survival of the Fittest Edition adds a modern twist to the classic snake game you know and love. This game pits you, the player, against a computer-controlled snake in a battle. As you maneuver to collect food and grow in size, you’ll need to avoid obstacles, strategize your movements, and ensure that you don’t become the prey.

 

Code Overview

let scl; // Grid scale (calculated dynamically)
let cols, rows;
let playerSnake, computerSnake;
let food;
let obstacles = [];
let obstacleTimer = 0; // Timer to track when to change obstacles
let obstacleInterval = 300; // Minimum frames before obstacles relocate
let help = "Press 'f' (possibly twice) to toggle fullscreen";
let gameState = 'idle'; // 'idle', 'playing', or 'gameOver' // NEW CODE
let hasEnded = false;

function setup() {
  createCanvas(windowWidth, windowHeight);
  frameRate(10); // Control the game speed
  print(help);

  updateGridDimensions(); // Calculate grid dimensions and scale

  // Initialize snakes
  playerSnake = new Snake(color(0, 0, 255)); // Blue snake for the player
  computerSnake = new Snake(color(255, 0, 0)); // Red snake for the computer
  computerSnake.chaseState = "food"; // Initial chase state

  // Place food and obstacles
  placeFood();
  placeObstacles();
}

function draw() {
  if (gameState === 'idle') {
    drawStartScreen(); // NEW CODE
  } else if (gameState === 'playing') {
    runGame(); // NEW CODE
  } else if (gameState === 'gameOver') {
    drawGameOverScreen(); // NEW CODE
  }
}

function runGame() {
  background(220);

  // Update obstacles at random intervals
  if (frameCount > obstacleTimer + obstacleInterval) {
    placeObstacles();
    obstacleTimer = frameCount; // Reset the timer
  }

  // Draw the grid
  drawGrid();

  // Update the chase state
  if (computerSnake.body.length > 0 && playerSnake.body.length > 0) {
    if (computerSnake.len >= playerSnake.len * 3) {
      computerSnake.chaseState = "user"; // Switch to chasing the user
    } else {
      computerSnake.chaseState = "food"; // Chase food
    }
  }

  // Update and show the snakes
  if (playerSnake.body.length > 0) {
    playerSnake.update();
    playerSnake.show();
  }

  if (computerSnake.body.length > 0) {
    computerSnake.update();
    computerSnake.show();
  }

  // Handle computer snake logic
  if (computerSnake.body.length > 0 && computerSnake.chaseState === "food") {
    computerSnake.chase(food); // Chase food
  } else if (
    computerSnake.body.length > 0 &&
    playerSnake.body.length > 0 &&
    computerSnake.chaseState === "user"
  ) {
    computerSnake.chase(playerSnake.body[playerSnake.body.length - 1]); // Chase player's head

    if (snakeCollidesWithSnake(computerSnake, playerSnake)) {
      playerSnake.body.shift(); // Remove one cell from the player's snake
      playerSnake.len--; // Decrease player's snake length

      // NEW CODE: Check if player snake is engulfed completely
      if (playerSnake.len <= 0) {
        gameState = 'gameOver';
        drawGameOverScreen();
        fill(255, 255, 0);
        textSize(32);
        textAlign(CENTER, CENTER);
        text("YOU WERE ENGULFED BY THE COMPUTER SNAKE!", width / 2, (2 * height) / 3);
        return; // Exit the function immediately
      }
    }
  }

  // Check if the player snake eats the food
  if (playerSnake.body.length > 0 && playerSnake.eat(food)) {
    placeFood();
  }

  // Check if the computer snake eats the food
  if (computerSnake.body.length > 0 && computerSnake.eat(food)) {
    placeFood();
  }

  // Check for collisions with obstacles (only for the player snake)
  if (playerSnake.body.length > 0 && snakeCollidesWithObstacles(playerSnake)) {
    gameState = 'gameOver'; // NEW CODE: End the game
    drawGameOverScreen();
    return; // Exit the function immediately
  }

  // Draw the food
  fill(0, 255, 0);
  rect(food.x * scl, food.y * scl, scl, scl);

  // Draw the obstacles
  fill(100); // Gray obstacles
  for (let obs of obstacles) {
    rect(obs.x * scl, obs.y * scl, scl, scl);
  }

  // Check for game over conditions
  if (playerSnake.body.length > 0 && playerSnake.isDead()) {
    gameState = 'gameOver';
    drawGameOverScreen();
    return;
  }

  if (computerSnake.body.length > 0 && computerSnake.isDead()) {
    gameState = 'gameOver';
    drawGameOverScreen();
    return;
  }
}


function drawStartScreen() { // UPDATED FUNCTION
  background(0);
  fill(255, 255, 0); // Bright yellow color for arcade-like feel
  textFont('monospace'); // Arcade-style font
  textSize(48); // Large text size
  textAlign(CENTER, CENTER);
  text("SNAKE: SURVIVAL OF THE FITTEST", width / 2, height / 3);
  fill(0, 255, 0); // Green color for instructions
  textSize(24);
  text("PRESS SPACE TO START", width / 2, height / 2);
  fill(255, 0, 0); // Red color for help
  textSize(18);
  text(help, width / 2, (2 * height) / 3);
}

function drawGameOverScreen() { // UPDATED FUNCTION
  background(0);
  fill(255, 0, 0); // Red for game over message
  textFont('monospace'); // Arcade-style font
  textSize(48);
  textAlign(CENTER, CENTER);
  text("GAME OVER!", width / 2, height / 3);
  fill(0, 255, 0); // Green for restart instructions
  textSize(24);
  text("PRESS SPACE TO RESTART", width / 2, height / 2);
  noLoop(); // Stop the game loop
}



function updateGridDimensions() {
  // Set desired number of columns and rows
  cols = 40;
  rows = 30;

  // Calculate scale to fit the window size
  scl = min(floor(windowWidth / cols), floor(windowHeight / rows));

  // Adjust cols and rows to fill the screen exactly
  cols = floor(windowWidth / scl);
  rows = floor(windowHeight / scl);

  // Resize the canvas to match the new grid
  resizeCanvas(cols * scl, rows * scl);
}

function windowResized() {
  // Update grid and canvas dimensions when the window is resized
  updateGridDimensions();

  // Reposition snakes, food, and obstacles to ensure they are within the new bounds
  playerSnake.reposition();
  computerSnake.reposition();
  placeFood();
  placeObstacles();
}

function resetGame() { // NEW FUNCTION
  playerSnake = new Snake(color(0, 0, 255)); // Reset player snake
  computerSnake = new Snake(color(255, 0, 0)); // Reset computer snake
  computerSnake.chaseState = "food"; // Reset chase state
  placeFood();
  placeObstacles();
  loop(); // Start the game loop // FIX
}

function keyTyped() {
  if (key === 'f') {
    toggleFullscreen(); // Toggle fullscreen mode
  }
}

function toggleFullscreen() {
  let fs = fullscreen();
  fullscreen(!fs); // Flip fullscreen state
}
function keyPressed() {
  if (gameState === 'idle' && key === ' ') { // NEW CODE
    gameState = 'playing';
    resetGame(); // NEW CODE
  } else if (gameState === 'gameOver' && key === ' ') { // NEW CODE
    gameState = 'playing';
    resetGame(); // NEW CODE
  } else if (gameState === 'playing') { // NEW CONDITION
    switch (keyCode) {
      case UP_ARROW:
        if (playerSnake.ydir !== 1) playerSnake.setDir(0, -1);
        break;
      case DOWN_ARROW:
        if (playerSnake.ydir !== -1) playerSnake.setDir(0, 1);
        break;
      case LEFT_ARROW:
        if (playerSnake.xdir !== 1) playerSnake.setDir(-1, 0);
        break;
      case RIGHT_ARROW:
        if (playerSnake.xdir !== -1) playerSnake.setDir(1, 0);
        break;
    }
  }
}


function drawGrid() {
  stroke(200);
  for (let i = 0; i <= cols; i++) {
    line(i * scl, 0, i * scl, rows * scl);
  }
  for (let j = 0; j <= rows; j++) {
    line(0, j * scl, cols * scl, j * scl);
  }
}

function placeFood() {
  // Place food at a random position, avoiding obstacles and snakes
  food = createVector(
    floor(random(1, cols - 1)), // Avoid edges
    floor(random(1, rows - 1))
  );

  // Ensure food does not overlap with obstacles or snakes
  while (
    obstacles.some(obs => obs.x === food.x && obs.y === food.y) ||
    playerSnake.body.some(part => part.x === food.x && part.y === food.y) ||
    computerSnake.body.some(part => part.x === food.x && part.y === food.y)
  ) {
    food = createVector(
      floor(random(1, cols - 1)),
      floor(random(1, rows - 1))
    );
  }
}

function placeObstacles() {
  // Place random obstacles
  obstacles = []; // Clear existing obstacles
  let numObstacles = floor(random(4, 9)); // Random number of obstacles between 4 and 8

  for (let i = 0; i < numObstacles; i++) {
    let obs = createVector(floor(random(cols)), floor(random(rows)));

    // Ensure obstacles do not overlap with food or snakes
    while (
      (food && food.x === obs.x && food.y === obs.y) ||
      playerSnake.body.some(part => part.x === obs.x && part.y === obs.y) ||
      computerSnake.body.some(part => part.x === obs.x && part.y === obs.y)
    ) {
      obs = createVector(floor(random(cols)), floor(random(rows)));
    }
    obstacles.push(obs);
  }
}

function snakeCollidesWithObstacles(snake) {
  // Check if the snake's head collides with any obstacle
  if (snake === playerSnake) {
    let head = snake.body[snake.body.length - 1];
    return obstacles.some(obs => head.x === obs.x && head.y === obs.y);
  }
  return false; // Obstacles do not affect the computer snake
}

function snakeCollidesWithSnake(snake1, snake2) {
  // Check if snake1's head collides with any part of snake2
  let head1 = snake1.body[snake1.body.length - 1];
  return snake2.body.some(part => part.x === head1.x && part.y === head1.y);
}

function endGame(message) {
  // End the game and display a message
  noLoop();
  fill(0); // Black color for text
  textSize(32);
  textAlign(CENTER, CENTER);
  text(message, width / 2, height / 2);
}

class Snake {
  constructor(snakeColor) {
    // Initialize snake properties
    this.body = [createVector(floor(cols / 2), floor(rows / 2))];
    this.xdir = 0;
    this.ydir = 0;
    this.len = 1;
    this.dead = false;
    this.snakeColor = snakeColor;
  }

  setDir(x, y) {
    // Set snake's movement direction
    this.xdir = x;
    this.ydir = y;
  }

  update() {
    // Update snake's position
    let head = this.body[this.body.length - 1].copy();
    head.x += this.xdir;
    head.y += this.ydir;

    // Check for wall collision
    if (head.x < 0 || head.x >= cols || head.y < 0 || head.y >= rows) {
      this.dead = true;
    }

    this.body.push(head);

    // Remove the tail if the snake has not grown
    if (this.body.length > this.len) {
      this.body.shift();
    }
  }

  eat(pos) {
    // Check if the snake's head is at the same position as the food
    let head = this.body[this.body.length - 1];
    if (head.x === pos.x && head.y === pos.y) {
      this.len++;
      return true;
    }
    return false;
  }

  isDead() {
    // Check if the snake runs into itself
    let head = this.body[this.body.length - 1];
    for (let i = 0; i < this.body.length - 1; i++) {
      let part = this.body[i];
      if (part.x === head.x && part.y === head.y) {
        return true;
      }
    }
    return this.dead;
  }

  show() {
    // Display the snake on the canvas
    fill(this.snakeColor);
    for (let part of this.body) {
      rect(part.x * scl, part.y * scl, scl, scl);
    }
  }

  chase(target) {
    // Chase a target (food or player's head)
    let head = this.body[this.body.length - 1];
    if (target.x > head.x && this.xdir !== -1) {
      this.setDir(1, 0); // Move right
    } else if (target.x < head.x && this.xdir !== 1) {
      this.setDir(-1, 0); // Move left
    } else if (target.y > head.y && this.ydir !== -1) {
      this.setDir(0, 1); // Move down
    } else if (target.y < head.y && this.ydir !== 1) {
      this.setDir(0, -1); // Move up
    }
  }

  reposition() {
    // Reposition the snake if the window is resized
    for (let part of this.body) {
      part.x = constrain(part.x, 0, cols - 1);
      part.y = constrain(part.y, 0, rows - 1);
    }
  }
}

 

Here’s a high-level breakdown of code:

1. Game Structure

The game is divided into three primary states:

  1. Idle State: Displays the start screen with instructions and the game title.
  2. Playing State: Runs the game loop where the snakes move, food is consumed, and collisions are handled.
  3. Game Over State: Displays a game-over message and prompts the user to restart.

This is achieved using a game state variable, which transitions between ‘idle’, ‘playing’, and ‘gameover’.

2. Grid System

The game grid dynamically adjusts to fit the browser window. The grid is divided into cells, and all game elements (snakes, food, obstacles) are positioned on this grid. The scale of the grid (scl) is calculated based on the window size to ensure the game looks consistent on any screen.

3. Snakes

Both the player and computer-controlled snakes are objects created using the snake class. Each snake:

  • Has a body represented as an array of segments (each segment is a grid cell).
  • Moves in a specified direction and grows when it consumes food.
  • Can “die” if it collides with itself, obstacles, or the grid boundaries.

The computer snake is programmed to switch between two behaviors:

  • Chasing Food: Moves toward food on the grid.
  • Chasing the Player: Pursues the player’s snake when it is three times larger, adding a competitive element.
4. Game Logic

The core gameplay logic is handled in the runGame function, which:

  • Updates the positions of the snakes.
  • Checks for collisions (e.g., snake colliding with obstacles or itself).
  • Manages interactions, such as consuming food or one snake engulfing the other.
  • Adjusts the state of the game based on player or computer actions.
5. Visuals

The game’s visuals, including the grid, snakes, food, and obstacles, are drawn dynamically using p5.js functions like rect and line. To create an arcade-like feel:

  • The title screen and game-over messages use bold, vibrant colors and pixelated fonts.
  • The game background, snakes, and food are kept simple yet visually distinct for clarity.
6. Interaction

The player controls their snake using the arrow keys. Pressing the space bar transitions the game between the start screen and gameplay or restarts the game after it ends. Additionally, pressing ‘f’ toggles fullscreen mode for an immersive experience.

7. Obstacles

Randomly placed obstacles add complexity to the game. They are repositioned periodically to ensure dynamic gameplay. The player’s snake must avoid these obstacles, while the computer snake is immune to them, giving it an advantage.

User Testing

I got feedback to include an info text about the game details and also the user suggested to reduce the speed of the computer’s snake.

IMG_0104

Future Considerations

While the game is already engaging, there are plenty of opportunities for enhancement:

  1. Improved AI:
    • Introduce smarter pathfinding for the computer snake, like using the AI
  2. Multiplayer Support:
    • Allow two players to compete head-to-head on the same grid.
  3. Power-Ups:
    • Include items that provide temporary benefits, such as invincibility, speed boosts, or traps for the computer snake.
  4. Dynamic Environments:
    • Add levels with different grid layouts, moving obstacles, or shrinking safe zones.
  5. Scoring System and Leaderboards:
    • Introduce a scoring mechanism based on time survived or food consumed.
    • Display high scores for competitive play.
  6. Sound Effects and Music:
    • Add classic arcade sound effects for eating food, colliding, and game-over events.
    • Include background music that changes tempo based on the intensity of gameplay.