Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Autonomous Multi-Snake Game V5</title>
- <style>
- body { margin: 0; overflow: hidden; display: flex; justify-content: center; align-items: center; min-height: 100vh; background-color: #333; }
- canvas { background-color: #000; border: 2px solid #555; }
- </style>
- </head>
- <body>
- <canvas id="gameCanvas"></canvas>
- <script>
- /**
- * @overview
- * This file contains the complete source code for the Autonomous Multi-Snake Game V5.
- * This version introduces a smarter AI that can decide whether to wait for food to ripen,
- * and dead snakes now leave their bodies behind as obstacles.
- *
- * Architecture:
- * - Game: The main controller class that manages the game loop, state, and all game objects.
- * - UI: A dedicated class for all rendering tasks, including the new scoreboard format.
- * - Snake: A model class representing a snake, with enhanced AI and a persistent dead state.
- * - Food: A model class for the food, which has a 'type' ('blue' or 'red') and a lifecycle.
- */
- /**
- * =================================================================================
- * GAME CONSTANTS
- * =================================================================================
- */
- const STAGE_WIDTH = 1000;
- const STAGE_HEIGHT = 800;
- const GRID_SIZE = 20;
- const INITIAL_SNAKE_LENGTH = 5;
- const GAME_SPEED = 100; // Milliseconds per game tick.
- const NUM_SNAKES = 4;
- const FOOD_RIPENING_TIME = 3000; // 3 seconds for a blue apple to turn red.
- const FOOD_TYPE_BLUE = 'blue';
- const FOOD_TYPE_RED = 'red';
- /**
- * =================================================================================
- * UI Class
- * =================================================================================
- */
- class UI {
- constructor(ctx) {
- this.ctx = ctx;
- this.scoreFont = '20px Arial';
- this.gameOverFont = '40px Arial';
- this.restartFont = '20px Arial';
- }
- clear() {
- this.ctx.clearRect(0, 0, STAGE_WIDTH, STAGE_HEIGHT);
- }
- drawFood(food) {
- this.ctx.fillStyle = food.type === FOOD_TYPE_RED ? 'red' : 'deepskyblue';
- this.ctx.fillRect(food.x, food.y, GRID_SIZE, GRID_SIZE);
- }
- drawSnakes(snakes) {
- snakes.forEach(snake => {
- // The snake's own draw method will handle its appearance (alive or dead).
- snake.draw(this.ctx);
- });
- }
- drawScoreboard(snakes) {
- this.ctx.font = this.scoreFont;
- const scoreMarginLeft = 10;
- const scoreMarginTop = 30;
- const scoreLineHeight = 25;
- const colorBoxSize = 15;
- const colorBoxMargin = 10;
- snakes.forEach((snake, index) => {
- const yPos = scoreMarginTop + (index * scoreLineHeight);
- this.ctx.fillStyle = snake.color;
- this.ctx.fillRect(scoreMarginLeft, yPos - colorBoxSize, colorBoxSize, colorBoxSize);
- this.ctx.fillStyle = 'white';
- const aliveStatus = snake.isDead ? '[Alive: False]' : '';
- const scoreText = `${snake.name}: ${snake.score} (Size: ${snake.body.length}) ${aliveStatus}`;
- this.ctx.fillText(scoreText, scoreMarginLeft + colorBoxSize + colorBoxMargin, yPos);
- });
- }
- drawGameOver() {
- this.ctx.fillStyle = 'white';
- this.ctx.textAlign = 'center';
- this.ctx.font = this.gameOverFont;
- this.ctx.fillText('Game Over!', STAGE_WIDTH / 2, STAGE_HEIGHT / 2);
- this.ctx.font = this.restartFont;
- this.ctx.fillText('Refresh to play again', STAGE_WIDTH / 2, STAGE_HEIGHT / 2 + 40);
- this.ctx.textAlign = 'left';
- }
- }
- /**
- * =================================================================================
- * Food Class
- * =================================================================================
- */
- class Food {
- constructor(x, y, type) {
- this.x = x;
- this.y = y;
- this.type = type;
- this.spawnTime = Date.now();
- }
- }
- /**
- * =================================================================================
- * Snake Class
- * =================================================================================
- */
- class Snake {
- constructor(id, name, color, startX, startY) {
- this.id = id;
- this.name = name;
- this.color = color;
- this.body = [];
- this.direction = { x: 0, y: 0 };
- this.score = 0;
- this.isDead = false;
- this.init(startX, startY);
- }
- init(startX, startY) {
- this.body = [];
- for (let i = 0; i < INITIAL_SNAKE_LENGTH; i++) {
- this.body.push({ x: startX - i * GRID_SIZE, y: startY });
- }
- this.direction = this.getRandomInitialDirection();
- this.score = 0;
- this.isDead = false;
- }
- getRandomInitialDirection() {
- const directions = [
- { x: GRID_SIZE, y: 0 }, { x: -GRID_SIZE, y: 0 },
- { x: 0, y: GRID_SIZE }, { x: 0, y: -GRID_SIZE }
- ];
- return directions[Math.floor(Math.random() * directions.length)];
- }
- /**
- * Draws the snake. Dead snakes are drawn in grey.
- * @param {CanvasRenderingContext2D} ctx The rendering context.
- */
- draw(ctx) {
- ctx.fillStyle = this.isDead ? '#555' : this.color;
- this.body.forEach(segment => {
- ctx.fillRect(segment.x, segment.y, GRID_SIZE, GRID_SIZE);
- });
- }
- move(food, snakes) {
- if (this.isDead) return false;
- const head = { x: this.body[0].x + this.direction.x, y: this.body[0].y + this.direction.y };
- if (this.isCollision(head.x, head.y, snakes)) {
- this.isDead = true;
- return false;
- }
- this.body.unshift(head);
- let ateFood = false;
- if (head.x === food.x && head.y === food.y) {
- ateFood = true;
- if (food.type === FOOD_TYPE_RED) {
- this.score++;
- } else {
- this.body.pop();
- if (this.body.length > 1) {
- this.body.pop();
- }
- }
- } else {
- this.body.pop();
- }
- return ateFood;
- }
- /**
- * Advanced AI logic. The snake calculates if it's safe to approach the food
- * or if it should wait for it to ripen.
- */
- think(food, snakes) {
- if (this.isDead) return;
- const head = this.body[0];
- const isFoodUnripe = food.type === FOOD_TYPE_BLUE;
- const timeToRipen = Math.max(0, FOOD_RIPENING_TIME - (Date.now() - food.spawnTime));
- const distanceToFood = (Math.abs(head.x - food.x) + Math.abs(head.y - food.y)) / GRID_SIZE;
- const timeToReach = distanceToFood * GAME_SPEED;
- // If the food is blue and the snake will arrive before it ripens, avoid it.
- const shouldAvoidFood = isFoodUnripe && timeToReach < timeToRipen;
- const potentialDirections = [
- { x: GRID_SIZE, y: 0 }, { x: -GRID_SIZE, y: 0 },
- { x: 0, y: GRID_SIZE }, { x: 0, y: -GRID_SIZE }
- ];
- let bestDirection = null;
- let maxDistance = -1; // For avoidance behavior
- let minDistance = Infinity; // For seeking behavior
- potentialDirections.forEach(dir => {
- if (dir.x === -this.direction.x && dir.y === -this.direction.y) return; // Avoid reversing
- const nextX = head.x + dir.x;
- const nextY = head.y + dir.y;
- if (!this.isCollision(nextX, nextY, snakes)) {
- const distance = Math.abs(nextX - food.x) + Math.abs(nextY - food.y);
- if (shouldAvoidFood) {
- // Find the safe move that takes it FARTHEST from the blue food
- if (distance > maxDistance) {
- maxDistance = distance;
- bestDirection = dir;
- }
- } else {
- // Find the safe move that takes it CLOSEST to the food
- if (distance < minDistance) {
- minDistance = distance;
- bestDirection = dir;
- }
- }
- }
- });
- if (bestDirection) {
- this.direction = bestDirection;
- } else {
- // Fallback if all moves are blocked
- const safeFallback = potentialDirections.find(dir => {
- if (dir.x === -this.direction.x && dir.y === -this.direction.y) return false;
- return !this.isCollision(head.x + dir.x, head.y + dir.y, snakes);
- });
- if (safeFallback) {
- this.direction = safeFallback;
- }
- }
- }
- isCollision(x, y, snakes) {
- if (x < 0 || x >= STAGE_WIDTH || y < 0 || y >= STAGE_HEIGHT) return true;
- // Check for collision with self (excluding the tail, which will move)
- for (let i = 0; i < this.body.length - 1; i++) {
- if (x === this.body[i].x && y === this.body[i].y) return true;
- }
- // Check for collision with all other snakes (alive or dead)
- for (const otherSnake of snakes) {
- if (otherSnake.id === this.id) continue;
- if (otherSnake.body.some(segment => x === segment.x && y === segment.y)) return true;
- }
- return false;
- }
- }
- /**
- * =================================================================================
- * Game Class
- * =================================================================================
- */
- class Game {
- constructor() {
- const canvas = document.getElementById('gameCanvas');
- this.ctx = canvas.getContext('2d');
- canvas.width = STAGE_WIDTH;
- canvas.height = STAGE_HEIGHT;
- this.ui = new UI(this.ctx);
- this.snakes = [];
- this.food = null;
- this.gameInterval = null;
- this.isGameOver = false;
- this.init();
- }
- init() {
- this.isGameOver = false;
- this.snakes = [];
- const colors = ['lime', 'blue', 'orange', 'purple'];
- const names = ["Viper", "Cobra", "Taipan", "Mamba"];
- const startPositions = [
- { x: GRID_SIZE * 5, y: GRID_SIZE * 5 },
- { x: STAGE_WIDTH - GRID_SIZE * (5 + INITIAL_SNAKE_LENGTH), y: GRID_SIZE * 5 },
- { x: GRID_SIZE * 5, y: STAGE_HEIGHT - GRID_SIZE * 5 },
- { x: STAGE_WIDTH - GRID_SIZE * (5 + INITIAL_SNAKE_LENGTH), y: STAGE_HEIGHT - GRID_SIZE * 5 }
- ];
- for (let i = 0; i < NUM_SNAKES; i++) {
- const pos = startPositions[i % startPositions.length];
- this.snakes.push(new Snake(i, names[i], colors[i], pos.x, pos.y));
- }
- this.generateFood();
- this.startGameLoop();
- }
- startGameLoop() {
- if (this.gameInterval) clearInterval(this.gameInterval);
- this.gameInterval = setInterval(() => this.update(), GAME_SPEED);
- }
- generateFood() {
- let newFoodX, newFoodY, collision;
- do {
- collision = false;
- newFoodX = Math.floor(Math.random() * (STAGE_WIDTH / GRID_SIZE)) * GRID_SIZE;
- newFoodY = Math.floor(Math.random() * (STAGE_HEIGHT / GRID_SIZE)) * GRID_SIZE;
- for (const snake of this.snakes) {
- if (snake.body.some(segment => newFoodX === segment.x && newFoodY === segment.y)) {
- collision = true;
- break;
- }
- }
- } while (collision);
- this.food = new Food(newFoodX, newFoodY, FOOD_TYPE_BLUE);
- }
- update() {
- if (this.isGameOver) {
- clearInterval(this.gameInterval);
- return;
- }
- if (this.food.type === FOOD_TYPE_BLUE && Date.now() - this.food.spawnTime > FOOD_RIPENING_TIME) {
- this.food.type = FOOD_TYPE_RED;
- }
- let livingSnakes = 0;
- this.snakes.forEach(snake => {
- if (!snake.isDead) {
- snake.think(this.food, this.snakes);
- const ateFood = snake.move(this.food, this.snakes);
- if (ateFood) {
- this.generateFood();
- }
- livingSnakes++;
- }
- });
- this.draw();
- if (livingSnakes === 0) {
- this.isGameOver = true;
- this.ui.drawGameOver();
- clearInterval(this.gameInterval);
- }
- }
- draw() {
- this.ui.clear();
- this.ui.drawFood(this.food);
- this.ui.drawSnakes(this.snakes);
- this.ui.drawScoreboard(this.snakes);
- }
- }
- // Entry point: Create a new Game instance when the script loads.
- window.onload = () => new Game();
- </script>
- </body>
- </html>
Advertisement
Add Comment
Please, Sign In to add comment