XTaylorSpenceX

Autonomous Snake Plus Plus

Sep 18th, 2025
90
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
HTML 15.37 KB | None | 0 0
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6.     <title>Autonomous Multi-Snake Game V5</title>
  7.     <style>
  8.         body { margin: 0; overflow: hidden; display: flex; justify-content: center; align-items: center; min-height: 100vh; background-color: #333; }
  9.         canvas { background-color: #000; border: 2px solid #555; }
  10.     </style>
  11. </head>
  12. <body>
  13.     <canvas id="gameCanvas"></canvas>
  14.  
  15.     <script>
  16.         /**
  17.          * @overview
  18.          * This file contains the complete source code for the Autonomous Multi-Snake Game V5.
  19.          * This version introduces a smarter AI that can decide whether to wait for food to ripen,
  20.          * and dead snakes now leave their bodies behind as obstacles.
  21.          *
  22.          * Architecture:
  23.          * - Game: The main controller class that manages the game loop, state, and all game objects.
  24.          * - UI: A dedicated class for all rendering tasks, including the new scoreboard format.
  25.          * - Snake: A model class representing a snake, with enhanced AI and a persistent dead state.
  26.          * - Food: A model class for the food, which has a 'type' ('blue' or 'red') and a lifecycle.
  27.          */
  28.  
  29.         /**
  30.          * =================================================================================
  31.          * GAME CONSTANTS
  32.          * =================================================================================
  33.          */
  34.         const STAGE_WIDTH = 1000;
  35.         const STAGE_HEIGHT = 800;
  36.         const GRID_SIZE = 20;
  37.         const INITIAL_SNAKE_LENGTH = 5;
  38.         const GAME_SPEED = 100; // Milliseconds per game tick.
  39.         const NUM_SNAKES = 4;
  40.         const FOOD_RIPENING_TIME = 3000; // 3 seconds for a blue apple to turn red.
  41.         const FOOD_TYPE_BLUE = 'blue';
  42.         const FOOD_TYPE_RED = 'red';
  43.  
  44.         /**
  45.          * =================================================================================
  46.          * UI Class
  47.          * =================================================================================
  48.          */
  49.         class UI {
  50.             constructor(ctx) {
  51.                 this.ctx = ctx;
  52.                 this.scoreFont = '20px Arial';
  53.                 this.gameOverFont = '40px Arial';
  54.                 this.restartFont = '20px Arial';
  55.             }
  56.  
  57.             clear() {
  58.                 this.ctx.clearRect(0, 0, STAGE_WIDTH, STAGE_HEIGHT);
  59.             }
  60.  
  61.             drawFood(food) {
  62.                 this.ctx.fillStyle = food.type === FOOD_TYPE_RED ? 'red' : 'deepskyblue';
  63.                 this.ctx.fillRect(food.x, food.y, GRID_SIZE, GRID_SIZE);
  64.             }
  65.  
  66.             drawSnakes(snakes) {
  67.                 snakes.forEach(snake => {
  68.                     // The snake's own draw method will handle its appearance (alive or dead).
  69.                     snake.draw(this.ctx);
  70.                 });
  71.             }
  72.  
  73.             drawScoreboard(snakes) {
  74.                 this.ctx.font = this.scoreFont;
  75.                 const scoreMarginLeft = 10;
  76.                 const scoreMarginTop = 30;
  77.                 const scoreLineHeight = 25;
  78.                 const colorBoxSize = 15;
  79.                 const colorBoxMargin = 10;
  80.  
  81.                 snakes.forEach((snake, index) => {
  82.                     const yPos = scoreMarginTop + (index * scoreLineHeight);
  83.                     this.ctx.fillStyle = snake.color;
  84.                     this.ctx.fillRect(scoreMarginLeft, yPos - colorBoxSize, colorBoxSize, colorBoxSize);
  85.  
  86.                     this.ctx.fillStyle = 'white';
  87.                     const aliveStatus = snake.isDead ? '[Alive: False]' : '';
  88.                     const scoreText = `${snake.name}: ${snake.score} (Size: ${snake.body.length}) ${aliveStatus}`;
  89.                     this.ctx.fillText(scoreText, scoreMarginLeft + colorBoxSize + colorBoxMargin, yPos);
  90.                 });
  91.             }
  92.  
  93.             drawGameOver() {
  94.                 this.ctx.fillStyle = 'white';
  95.                 this.ctx.textAlign = 'center';
  96.                 this.ctx.font = this.gameOverFont;
  97.                 this.ctx.fillText('Game Over!', STAGE_WIDTH / 2, STAGE_HEIGHT / 2);
  98.                 this.ctx.font = this.restartFont;
  99.                 this.ctx.fillText('Refresh to play again', STAGE_WIDTH / 2, STAGE_HEIGHT / 2 + 40);
  100.                 this.ctx.textAlign = 'left';
  101.             }
  102.         }
  103.  
  104.         /**
  105.          * =================================================================================
  106.          * Food Class
  107.          * =================================================================================
  108.          */
  109.         class Food {
  110.             constructor(x, y, type) {
  111.                 this.x = x;
  112.                 this.y = y;
  113.                 this.type = type;
  114.                 this.spawnTime = Date.now();
  115.             }
  116.         }
  117.  
  118.         /**
  119.          * =================================================================================
  120.          * Snake Class
  121.          * =================================================================================
  122.          */
  123.         class Snake {
  124.             constructor(id, name, color, startX, startY) {
  125.                 this.id = id;
  126.                 this.name = name;
  127.                 this.color = color;
  128.                 this.body = [];
  129.                 this.direction = { x: 0, y: 0 };
  130.                 this.score = 0;
  131.                 this.isDead = false;
  132.                 this.init(startX, startY);
  133.             }
  134.  
  135.             init(startX, startY) {
  136.                 this.body = [];
  137.                 for (let i = 0; i < INITIAL_SNAKE_LENGTH; i++) {
  138.                    this.body.push({ x: startX - i * GRID_SIZE, y: startY });
  139.                }
  140.                this.direction = this.getRandomInitialDirection();
  141.                this.score = 0;
  142.                this.isDead = false;
  143.            }
  144.  
  145.            getRandomInitialDirection() {
  146.                const directions = [
  147.                    { x: GRID_SIZE, y: 0 }, { x: -GRID_SIZE, y: 0 },
  148.                    { x: 0, y: GRID_SIZE }, { x: 0, y: -GRID_SIZE }
  149.                ];
  150.                return directions[Math.floor(Math.random() * directions.length)];
  151.            }
  152.  
  153.            /**
  154.             * Draws the snake. Dead snakes are drawn in grey.
  155.             * @param {CanvasRenderingContext2D} ctx The rendering context.
  156.             */
  157.            draw(ctx) {
  158.                ctx.fillStyle = this.isDead ? '#555' : this.color;
  159.                this.body.forEach(segment => {
  160.                     ctx.fillRect(segment.x, segment.y, GRID_SIZE, GRID_SIZE);
  161.                 });
  162.             }
  163.  
  164.             move(food, snakes) {
  165.                 if (this.isDead) return false;
  166.  
  167.                 const head = { x: this.body[0].x + this.direction.x, y: this.body[0].y + this.direction.y };
  168.  
  169.                 if (this.isCollision(head.x, head.y, snakes)) {
  170.                     this.isDead = true;
  171.                     return false;
  172.                 }
  173.  
  174.                 this.body.unshift(head);
  175.  
  176.                 let ateFood = false;
  177.                 if (head.x === food.x && head.y === food.y) {
  178.                    ateFood = true;
  179.                     if (food.type === FOOD_TYPE_RED) {
  180.                         this.score++;
  181.                     } else {
  182.                         this.body.pop();
  183.                         if (this.body.length > 1) {
  184.                             this.body.pop();
  185.                         }
  186.                     }
  187.                 } else {
  188.                     this.body.pop();
  189.                 }
  190.  
  191.                 return ateFood;
  192.             }
  193.  
  194.             /**
  195.              * Advanced AI logic. The snake calculates if it's safe to approach the food
  196.              * or if it should wait for it to ripen.
  197.              */
  198.             think(food, snakes) {
  199.                 if (this.isDead) return;
  200.  
  201.                 const head = this.body[0];
  202.                 const isFoodUnripe = food.type === FOOD_TYPE_BLUE;
  203.                 const timeToRipen = Math.max(0, FOOD_RIPENING_TIME - (Date.now() - food.spawnTime));
  204.                 const distanceToFood = (Math.abs(head.x - food.x) + Math.abs(head.y - food.y)) / GRID_SIZE;
  205.                 const timeToReach = distanceToFood * GAME_SPEED;
  206.  
  207.                 // If the food is blue and the snake will arrive before it ripens, avoid it.
  208.                 const shouldAvoidFood = isFoodUnripe && timeToReach < timeToRipen;
  209.  
  210.                 const potentialDirections = [
  211.                     { x: GRID_SIZE, y: 0 }, { x: -GRID_SIZE, y: 0 },
  212.                     { x: 0, y: GRID_SIZE }, { x: 0, y: -GRID_SIZE }
  213.                 ];
  214.  
  215.                 let bestDirection = null;
  216.                 let maxDistance = -1; // For avoidance behavior
  217.                 let minDistance = Infinity; // For seeking behavior
  218.  
  219.                 potentialDirections.forEach(dir => {
  220.                     if (dir.x === -this.direction.x && dir.y === -this.direction.y) return; // Avoid reversing
  221.  
  222.                     const nextX = head.x + dir.x;
  223.                     const nextY = head.y + dir.y;
  224.  
  225.                     if (!this.isCollision(nextX, nextY, snakes)) {
  226.                         const distance = Math.abs(nextX - food.x) + Math.abs(nextY - food.y);
  227.  
  228.                         if (shouldAvoidFood) {
  229.                             // Find the safe move that takes it FARTHEST from the blue food
  230.                             if (distance > maxDistance) {
  231.                                 maxDistance = distance;
  232.                                 bestDirection = dir;
  233.                             }
  234.                         } else {
  235.                             // Find the safe move that takes it CLOSEST to the food
  236.                             if (distance < minDistance) {
  237.                                minDistance = distance;
  238.                                bestDirection = dir;
  239.                            }
  240.                        }
  241.                    }
  242.                });
  243.  
  244.                if (bestDirection) {
  245.                    this.direction = bestDirection;
  246.                } else {
  247.                    // Fallback if all moves are blocked
  248.                    const safeFallback = potentialDirections.find(dir => {
  249.                         if (dir.x === -this.direction.x && dir.y === -this.direction.y) return false;
  250.                         return !this.isCollision(head.x + dir.x, head.y + dir.y, snakes);
  251.                     });
  252.                     if (safeFallback) {
  253.                         this.direction = safeFallback;
  254.                     }
  255.                 }
  256.             }
  257.  
  258.             isCollision(x, y, snakes) {
  259.                 if (x < 0 || x >= STAGE_WIDTH || y < 0 || y >= STAGE_HEIGHT) return true;
  260.                 // Check for collision with self (excluding the tail, which will move)
  261.                 for (let i = 0; i < this.body.length - 1; i++) {
  262.                    if (x === this.body[i].x && y === this.body[i].y) return true;
  263.                }
  264.  
  265.                // Check for collision with all other snakes (alive or dead)
  266.                for (const otherSnake of snakes) {
  267.                    if (otherSnake.id === this.id) continue;
  268.                    if (otherSnake.body.some(segment => x === segment.x && y === segment.y)) return true;
  269.                 }
  270.                 return false;
  271.             }
  272.         }
  273.  
  274.         /**
  275.          * =================================================================================
  276.          * Game Class
  277.          * =================================================================================
  278.          */
  279.         class Game {
  280.             constructor() {
  281.                 const canvas = document.getElementById('gameCanvas');
  282.                 this.ctx = canvas.getContext('2d');
  283.                 canvas.width = STAGE_WIDTH;
  284.                 canvas.height = STAGE_HEIGHT;
  285.  
  286.                 this.ui = new UI(this.ctx);
  287.                 this.snakes = [];
  288.                 this.food = null;
  289.                 this.gameInterval = null;
  290.                 this.isGameOver = false;
  291.  
  292.                 this.init();
  293.             }
  294.  
  295.             init() {
  296.                 this.isGameOver = false;
  297.                 this.snakes = [];
  298.                 const colors = ['lime', 'blue', 'orange', 'purple'];
  299.                 const names = ["Viper", "Cobra", "Taipan", "Mamba"];
  300.                 const startPositions = [
  301.                     { x: GRID_SIZE * 5, y: GRID_SIZE * 5 },
  302.                     { x: STAGE_WIDTH - GRID_SIZE * (5 + INITIAL_SNAKE_LENGTH), y: GRID_SIZE * 5 },
  303.                     { x: GRID_SIZE * 5, y: STAGE_HEIGHT - GRID_SIZE * 5 },
  304.                     { x: STAGE_WIDTH - GRID_SIZE * (5 + INITIAL_SNAKE_LENGTH), y: STAGE_HEIGHT - GRID_SIZE * 5 }
  305.                 ];
  306.  
  307.                 for (let i = 0; i < NUM_SNAKES; i++) {
  308.                    const pos = startPositions[i % startPositions.length];
  309.                    this.snakes.push(new Snake(i, names[i], colors[i], pos.x, pos.y));
  310.                }
  311.  
  312.                this.generateFood();
  313.                this.startGameLoop();
  314.            }
  315.  
  316.            startGameLoop() {
  317.                if (this.gameInterval) clearInterval(this.gameInterval);
  318.                this.gameInterval = setInterval(() => this.update(), GAME_SPEED);
  319.             }
  320.  
  321.             generateFood() {
  322.                 let newFoodX, newFoodY, collision;
  323.                 do {
  324.                     collision = false;
  325.                     newFoodX = Math.floor(Math.random() * (STAGE_WIDTH / GRID_SIZE)) * GRID_SIZE;
  326.                     newFoodY = Math.floor(Math.random() * (STAGE_HEIGHT / GRID_SIZE)) * GRID_SIZE;
  327.  
  328.                     for (const snake of this.snakes) {
  329.                         if (snake.body.some(segment => newFoodX === segment.x && newFoodY === segment.y)) {
  330.                            collision = true;
  331.                             break;
  332.                         }
  333.                     }
  334.                 } while (collision);
  335.  
  336.                 this.food = new Food(newFoodX, newFoodY, FOOD_TYPE_BLUE);
  337.             }
  338.  
  339.             update() {
  340.                 if (this.isGameOver) {
  341.                     clearInterval(this.gameInterval);
  342.                     return;
  343.                 }
  344.                
  345.                 if (this.food.type === FOOD_TYPE_BLUE && Date.now() - this.food.spawnTime > FOOD_RIPENING_TIME) {
  346.                    this.food.type = FOOD_TYPE_RED;
  347.                 }
  348.  
  349.                 let livingSnakes = 0;
  350.                 this.snakes.forEach(snake => {
  351.                     if (!snake.isDead) {
  352.                         snake.think(this.food, this.snakes);
  353.                         const ateFood = snake.move(this.food, this.snakes);
  354.                        
  355.                         if (ateFood) {
  356.                             this.generateFood();
  357.                         }
  358.                         livingSnakes++;
  359.                     }
  360.                 });
  361.  
  362.                 this.draw();
  363.  
  364.                 if (livingSnakes === 0) {
  365.                     this.isGameOver = true;
  366.                     this.ui.drawGameOver();
  367.                     clearInterval(this.gameInterval);
  368.                 }
  369.             }
  370.  
  371.             draw() {
  372.                 this.ui.clear();
  373.                 this.ui.drawFood(this.food);
  374.                 this.ui.drawSnakes(this.snakes);
  375.                 this.ui.drawScoreboard(this.snakes);
  376.             }
  377.         }
  378.  
  379.         // Entry point: Create a new Game instance when the script loads.
  380.         window.onload = () => new Game();
  381.     </script>
  382. </body>
  383. </html>
  384.  
Advertisement
Add Comment
Please, Sign In to add comment