Skip to content

Instantly share code, notes, and snippets.

@streichsbaer
Created February 6, 2025 03:29
Show Gist options
  • Save streichsbaer/7341497ad5b3ab1c564cb6f4da9a869b to your computer and use it in GitHub Desktop.
Save streichsbaer/7341497ad5b3ab1c564cb6f4da9a869b to your computer and use it in GitHub Desktop.
"100-Snake Battle Royale Game" by o3 deep research
import pygame
import random
import colorsys
# -------------------- Configuration Constants -------------------- #
CELL_SIZE = 10 # pixel size of each grid cell (snake segment size)
GRID_WIDTH = 80 # number of cells horizontally
GRID_HEIGHT = 60 # number of cells vertically
NUM_SNAKES = 100 # number of snakes in the battle royale
INITIAL_LENGTH = 3 # initial length of each snake (in segments)
INITIAL_FOOD = 20 # initial number of food items on the board
FPS = 15 # frames per second (speed of the game)
# Colors
BACKGROUND_COLOR = (0, 0, 0) # black background
FOOD_COLOR = (255, 255, 255) # white food
# Generate distinct colors for snakes by varying hue
SNAKE_COLORS = [
tuple(int(c * 255) for c in colorsys.hsv_to_rgb(i / NUM_SNAKES, 1.0, 1.0))
for i in range(NUM_SNAKES)
]
# -------------------- Snake Class (AI and state) -------------------- #
class Snake:
def __init__(self, snake_id, start_pos, start_dir, color):
"""Initialize a snake with an ID, starting position, direction, and color."""
self.id = snake_id
self.body = [start_pos] # list of (x,y) positions; body[0] is head
self.direction = start_dir # current moving direction (dx, dy)
self.alive = True
self.color = color
def get_head(self):
return self.body[0]
def choose_direction(self, food_positions, occupancy):
"""
Decide the next movement direction based on nearest food,
while avoiding immediate collisions. Returns a (dx, dy) direction.
"""
if not self.alive:
return self.direction # no change if snake is dead
hx, hy = self.get_head()
# Find the closest food (Manhattan distance)
target = None
min_dist = float("inf")
for fx, fy in food_positions:
dist = abs(fx - hx) + abs(fy - hy)
if dist < min_dist:
min_dist = dist
target = (fx, fy)
if target is None:
# No food available (shouldn't happen since we maintain some food)
# Just keep moving in the same direction
target = (hx + self.direction[0], hy + self.direction[1])
tx, ty = target
# Determine the preferred direction to move towards the target food
dx, dy = 0, 0
if abs(tx - hx) > abs(ty - hy):
dx = 1 if tx > hx else -1 if tx < hx else 0
dy = 0
else:
dx = 0
dy = 1 if ty > hy else -1 if ty < hy else 0
desired_dir = (dx, dy)
if desired_dir == (0, 0):
desired_dir = self.direction # target is on current cell (shouldn't happen)
# Avoid reversing into itself (no 180Β° turn if length > 1)
if len(self.body) > 1 and (
desired_dir[0] == -self.direction[0]
and desired_dir[1] == -self.direction[1]
):
# If the desired direction is directly opposite, choose a different direction (left/right turn)
# Determine perpendicular directions:
if self.direction[0] != 0: # current moving horizontally
# perpendicular moves: up or down
candidates = [(0, 1), (0, -1)]
else: # current moving vertically
# perpendicular moves: left or right
candidates = [(1, 0), (-1, 0)]
# Pick the candidate that brings closer to the target (if any)
candidates.sort(key=lambda d: abs((hx + d[0]) - tx) + abs((hy + d[1]) - ty))
if candidates:
desired_dir = candidates[0]
# Avoid immediate collision: if desired direction leads to a wall or snake body, try another
def is_safe(dir_tuple):
nx, ny = hx + dir_tuple[0], hy + dir_tuple[1]
# Check wall boundaries
if nx < 0 or nx >= GRID_WIDTH or ny < 0 or ny >= GRID_HEIGHT:
return False
occ = occupancy.get((nx, ny))
# Safe if the next cell is empty or just has food (not a snake body)
return occ is None or occ == "food"
if not is_safe(desired_dir):
# Try other directions (excluding the reverse of current direction)
possible_dirs = [(0, -1), (0, 1), (-1, 0), (1, 0)]
# Remove the reverse direction from possible moves
rev_dir = (-self.direction[0], -self.direction[1])
possible_dirs = [d for d in possible_dirs if d != rev_dir]
# Sort by closeness to target food to choose a reasonable alternative
possible_dirs.sort(
key=lambda d: abs((hx + d[0]) - tx) + abs((hy + d[1]) - ty)
)
for d in possible_dirs:
if is_safe(d):
desired_dir = d
break
# If none are safe, the snake will end up colliding (dead end scenario)
# Update the snake's direction
self.direction = desired_dir
return desired_dir
# -------------------- Game Class (Game state and logic) -------------------- #
class Game:
def __init__(self):
"""Initialize the game: create snakes, place initial food, set up grid."""
# Grid occupancy dictionary: maps (x,y) -> 'food' or snake_id
self.occupancy = {}
# List of snakes
self.snakes = []
# List of food positions (tuples)
self.food_positions = []
# Spawn snakes at random positions
for i in range(NUM_SNAKES):
# Find an empty start position for the snake's head
while True:
start_x = random.randrange(GRID_WIDTH)
start_y = random.randrange(GRID_HEIGHT)
if (start_x, start_y) not in self.occupancy:
break
# Random initial direction for variety
start_dir = random.choice([(1, 0), (-1, 0), (0, 1), (0, -1)])
# Initialize snake's body (starting length). Place segments in opposite direction of movement to avoid immediate self-collision.
body = [(start_x, start_y)]
self.occupancy[(start_x, start_y)] = (
i # mark head position as occupied by this snake
)
bx, by = start_x, start_y
for seg in range(1, INITIAL_LENGTH):
bx -= start_dir[0]
by -= start_dir[1]
# Stop if out of bounds or hitting another snake (shouldn't normally happen with careful placement)
if (
bx < 0
or bx >= GRID_WIDTH
or by < 0
or by >= GRID_HEIGHT
or (bx, by) in self.occupancy
):
break
body.append((bx, by))
self.occupancy[(bx, by)] = i # mark this segment on grid
# Create Snake object
snake = Snake(
snake_id=i,
start_pos=(start_x, start_y),
start_dir=start_dir,
color=SNAKE_COLORS[i],
)
snake.body = body # override body if we added segments
self.snakes.append(snake)
# Spawn initial food items
for f in range(INITIAL_FOOD):
self.spawn_food()
def spawn_food(self):
"""Place a new food item on a random empty cell."""
# Try random positions until an empty cell is found
for attempt in range(1000):
fx = random.randrange(GRID_WIDTH)
fy = random.randrange(GRID_HEIGHT)
if (fx, fy) not in self.occupancy: # empty spot
self.occupancy[(fx, fy)] = "food"
self.food_positions.append((fx, fy))
return (fx, fy)
# Fallback: do a linear scan for empty cell (in case the grid is very full)
for x in range(GRID_WIDTH):
for y in range(GRID_HEIGHT):
if (x, y) not in self.occupancy:
self.occupancy[(x, y)] = "food"
self.food_positions.append((x, y))
return (x, y)
return None # no space (game board full)
def update(self):
"""
Perform one update step: move all snakes (AI decisions), handle collisions and eating,
and spawn new food if needed.
Returns a tuple: (alive_count, cells_cleared, cells_filled).
- alive_count: number of snakes still alive after the update.
- cells_cleared: list of grid cells that were emptied this turn (to redraw background).
- cells_filled: list of (cell, color) for new snake segments or food that need drawing.
"""
if self.get_alive_count() <= 1:
# If game is already over (should be handled outside before calling update)
return self.get_alive_count(), [], []
alive_snakes = [s for s in self.snakes if s.alive]
cells_cleared = [] # cells to clear (became empty)
cells_filled = [] # cells to draw (became occupied by snake or food)
food_set = set(self.food_positions) # for quick lookup
# **1. Determine moves for each snake (their intended new head position)**
moves = [] # list of (snake, new_head_x, new_head_y, will_eat)
for snake in alive_snakes:
dir_x, dir_y = snake.choose_direction(self.food_positions, self.occupancy)
hx, hy = snake.get_head()
new_x = hx + dir_x
new_y = hy + dir_y
will_eat = (
new_x,
new_y,
) in food_set # True if there's food at the target cell
moves.append((snake, new_x, new_y, will_eat))
# **2. Remove snake tails from occupancy (before moving) for those that will move forward**
# (If a snake is going to eat, its tail stays, because it grows.)
for snake, nx, ny, will_eat in moves:
if not snake.alive:
continue
if not will_eat:
# Snake is not eating, so it will move forward normally: remove its tail segment
tail_x, tail_y = snake.body[-1]
if (tail_x, tail_y) in self.occupancy and self.occupancy[
(tail_x, tail_y)
] == snake.id:
del self.occupancy[(tail_x, tail_y)]
# Mark this cell for clearing (it becomes empty)
cells_cleared.append((tail_x, tail_y))
# Remove tail from the snake's body list
snake.body.pop()
# **3. Handle collisions (and determine which snakes survive to move)**
head_targets = {} # maps target cell -> snake that intends to move there
for snake, nx, ny, will_eat in moves:
if not snake.alive:
continue
# Check wall collision
if nx < 0 or nx >= GRID_WIDTH or ny < 0 or ny >= GRID_HEIGHT:
snake.alive = False
continue
# Check collision with any existing snake body in that cell
occ = self.occupancy.get((nx, ny))
if occ is not None and occ != "food":
# The cell is occupied by a snake segment (since it's not 'food')
if occ == snake.id:
# It's this snake's own body (should only happen if it tried to go into its tail that wasn't removed due to eating)
snake.alive = False
else:
# It's another snake's body
snake.alive = False
continue
# Check for head-on collision: two snakes aiming for the same cell
if (nx, ny) in head_targets:
# Another snake is also moving into this cell
other_snake = head_targets[(nx, ny)]
if other_snake is not None:
other_snake.alive = False
snake.alive = False
# Mark the cell with None to indicate a head-on crash (no snake gets it)
head_targets[(nx, ny)] = None
else:
head_targets[(nx, ny)] = snake
# **4. Apply movements for surviving snakes and handle food consumption**
new_food_needed = 0
for snake, nx, ny, will_eat in moves:
if not snake.alive:
# Snake died this turn: clear all its body from the grid
for bx, by in snake.body:
if (bx, by) in self.occupancy and self.occupancy[
(bx, by)
] == snake.id:
del self.occupancy[(bx, by)]
cells_cleared.append((bx, by))
snake.body.clear()
continue
# Snake is alive and will move into (nx, ny)
# If there's food at the new cell, consume it
if will_eat:
# Remove the food from game state
if (nx, ny) in self.food_positions:
self.food_positions.remove((nx, ny))
# We don't immediately remove 'food' from occupancy here, because we're about to override it with snake.id below.
new_food_needed += 1 # we will spawn a new food elsewhere later
# Add new head position to snake's body
snake.body.insert(0, (nx, ny))
# Mark the new head position in the occupancy grid with the snake's id
self.occupancy[(nx, ny)] = snake.id
# Mark this cell to draw (snake's head moved here)
cells_filled.append(((nx, ny), snake.color))
# **5. Spawn new food for each eaten piece**
for _ in range(new_food_needed):
pos = self.spawn_food()
if pos:
# Mark new food cell to draw
cells_filled.append((pos, FOOD_COLOR))
# Return number of alive snakes and lists of cells to redraw
return self.get_alive_count(), cells_cleared, cells_filled
def get_alive_count(self):
"""Utility to get current count of alive snakes."""
return sum(1 for s in self.snakes if s.alive)
# -------------------- Main Game Loop -------------------- #
pygame.init()
# Create game window
window_width = GRID_WIDTH * CELL_SIZE
window_height = GRID_HEIGHT * CELL_SIZE
screen = pygame.display.set_mode((window_width, window_height))
pygame.display.set_caption("100-Snake Battle Royale")
# Font for scoreboard
font = pygame.font.Font(None, 24) # default font, size 24
clock = pygame.time.Clock()
# Initialize game state
game = Game()
# Fill background once at start
screen.fill(BACKGROUND_COLOR)
# Draw initial snakes and food
for (x, y), occ in game.occupancy.items():
if occ == "food":
# draw food
rect = pygame.Rect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE)
pygame.draw.rect(screen, FOOD_COLOR, rect)
else:
# occ is a snake id -> draw snake segment
snake_id = occ
rect = pygame.Rect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE)
pygame.draw.rect(screen, SNAKE_COLORS[snake_id], rect)
# Initial scoreboard
alive = game.get_alive_count()
score_text = f"Snakes remaining: {alive}"
score_surf = font.render(score_text, True, (255, 255, 255))
# We draw scoreboard text with a background fill for clarity
text_rect = score_surf.get_rect(topleft=(5, 5))
# Fill a rectangle behind text (to cover any snake segments underneath)
screen.fill(BACKGROUND_COLOR, text_rect.inflate(5, 5))
screen.blit(score_surf, (5, 5))
pygame.display.update() # update entire screen for initial frame
# Game loop
running = True
winner_shown = False
while running:
# Cap the frame rate
clock.tick(
FPS
) # limit to FPS frames per second&#8203;:contentReference[oaicite:4]{index=4}
# Handle quit events
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# Update game state (move snakes, handle collisions/eating)
alive_count, cells_cleared, cells_filled = game.update()
# Redraw the parts of the screen that changed
dirty_rects = [] # list of pygame.Rect to update
# 1. Clear emptied cells (draw background)
for cx, cy in cells_cleared:
rect = pygame.Rect(cx * CELL_SIZE, cy * CELL_SIZE, CELL_SIZE, CELL_SIZE)
pygame.draw.rect(screen, BACKGROUND_COLOR, rect)
dirty_rects.append(rect)
# 2. Draw new snake segments or food in cells that became occupied
for (cx, cy), color in cells_filled:
rect = pygame.Rect(cx * CELL_SIZE, cy * CELL_SIZE, CELL_SIZE, CELL_SIZE)
pygame.draw.rect(screen, color, rect)
dirty_rects.append(rect)
# 3. Update scoreboard text if number of snakes changed or game ended
if alive_count != alive or alive_count <= 1:
alive = alive_count
if alive_count > 1:
score_text = f"Snakes remaining: {alive_count}"
elif alive_count == 1:
# One snake left – declare the winner
# Find the winner's ID (the one snake still alive)
winner_id = next(s.id for s in game.snakes if s.alive)
score_text = f"πŸŽ‰ Snake {winner_id} wins! πŸŽ‰"
else: # alive_count == 0
score_text = "All snakes died - No winners!"
score_surf = font.render(score_text, True, (255, 255, 255))
text_rect = score_surf.get_rect(topleft=(5, 5))
# Clear background behind text and draw new text
screen.fill(BACKGROUND_COLOR, text_rect.inflate(5, 5))
screen.blit(score_surf, (5, 5))
dirty_rects.append(text_rect.copy())
# Refresh just the changed areas on screen
if dirty_rects:
pygame.display.update(dirty_rects)
# End condition: if 0 or 1 snake remains, end the game loop
if alive_count <= 1:
# Keep the final winner/game-over message on screen for a moment
winner_shown = True
pygame.time.wait(2000) # wait 2 seconds so the player can see the result
running = False
# Exit Pygame
pygame.quit()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment