Saeed Lootah – Slime Mold

I was inspired by this which I saw a while ago when looking at the discord channel of the class. It was sent by Professor Aaron Sherwood and it was an animation of some kind but not in p5js but rather in openFrameworks.

https://cargocollective.com/sagejenson/physarum

I didn’t really understand what I was looking at or how it was worked. I chose not to look at it too closely for now so that I could come up with my own rules.

I knew it was based off of a slime mold called Physarum. It again reminded me about a slime mold being used to replicate make the Tokyo subway system and when I searched online I found this:

 

The slime expands in all directions then when it reaches the nutrients the path connecting it to the center gets strengthened. Slime as far as I know doesn’t have any kind of central intelligence, instead each particle or unit (whatever its called) acts independently.

I thought back to Conway’s Game of Life. Made by the mathematician John Horton Conway it is meant to resemble cellular life. It consists of a grid where each cell can be considered either dead or alive. The user can turn a cell to be either dead or alive and then start the simulation. The simulation consists of simple rules:

(Taken from Wikipedia)

  1. Any live cell with fewer than two live neighbours dies, as if by underpopulation.
  2. Any live cell with two or three live neighbours lives on to the next generation.
  3. Any live cell with more than three live neighbours dies, as if by overpopulation.
  4. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.

Note that each rule is not based on where in the grid a cell is but rather is only based on the surroundings of each cell. I knew for my slime mold simulation to be passable as a simulation it would need to follow a similar ruleset / a similar philosophy.

This was what I came up with:

The way it works is that there are multiple what I called Cells which step outwards from the center of the canvas. Each cell’s movement is slightly randomized in that it has its intended direction say 45 degrees. But using a gaussian distribution and by setting the standard deviation to be some value say 5 degrees then adding the value to the intended direction so most values would be between 50 to 40 degrees. Here’s the corresponding function:

My Personal Highlight

vector_gaussian_heading(standard_deviation) {
    let new_vector;
    let current_heading = this.direction_vector.heading()
    let random_gaussian = randomGaussian(0, standard_deviation)
    // Constrain the value because it can in theory be any number albeit low chance
    random_gaussian = constrain(random_gaussian, -standard_deviation * 2, standard_deviation * 2)

    // Add the randomness to the heading but without updating the initial desired heading
    let random_heading = current_heading + random_gaussian;
    new_vector = createVector(this.direction_vector.x, this.direction_vector.y) // Note: I tried ... = this.direction_vector but it would update this.direction_vector as well :/
    new_vector.setHeading(random_heading)

    return new_vector;
  }

When first creating the code I got confused why at one point the cell would move in all kinds of random directions rather than sticking to a single direction and only deviating a small amount like I intended.

I originally wrote new_vector = this.direction_vector I believed that new_vector would be given the value of this.direction_vector but it would not change this.direction_vector. I was used to the equals sign meaning that what’s on the left would have the value of what’s on the right but not both.

My Progress

Randomly moving cells was not my only problem.

After implementing the slight randomness I began to add more than one Cell. In the screenshot above I believe that was around 10 of them. There’s a for loop in the setup() function within which I initialized all of the Cell classes and wanted to get the initial directions of the cells to span all 360 degrees evenly using a map function but for some reason I couldn’t get it to work.  Instead I kept it simple and used a random() function.

This is what it looked like afterwards.

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.