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:
- Idle State: Displays the start screen with instructions and the game title.
- Playing State: Runs the game loop where the snakes move, food is consumed, and collisions are handled.
- 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.
Future Considerations
While the game is already engaging, there are plenty of opportunities for enhancement:
- Improved AI:
- Introduce smarter pathfinding for the computer snake, like using the AI
- Multiplayer Support:
- Allow two players to compete head-to-head on the same grid.
- Power-Ups:
- Include items that provide temporary benefits, such as invincibility, speed boosts, or traps for the computer snake.
- Dynamic Environments:
- Add levels with different grid layouts, moving obstacles, or shrinking safe zones.
- Scoring System and Leaderboards:
- Introduce a scoring mechanism based on time survived or food consumed.
- Display high scores for competitive play.
- 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.