small-projects/algorithm-visualisations/boids.py
OusmBlueNinja 483643a4c0 algorithms
2025-04-07 11:53:24 -05:00

195 lines
6.7 KiB
Python

import pygame
import random
import math
# Screen dimensions and simulation parameters
WIDTH, HEIGHT = 800, 600
NUM_BOIDS = 30
MAX_SPEED = 4
MAX_FORCE = 0.05
NEIGHBOR_RADIUS = 50
SEPARATION_RADIUS = 20
# Debug mode flag (toggle with "D")
DEBUG_MODE = True
class Boid:
def __init__(self, x, y):
self.position = pygame.math.Vector2(x, y)
angle = random.uniform(0, 2 * math.pi)
self.velocity = pygame.math.Vector2(math.cos(angle), math.sin(angle))
self.velocity.scale_to_length(random.uniform(1, MAX_SPEED))
self.acceleration = pygame.math.Vector2(0, 0)
def edges(self):
# Wrap-around behavior for screen edges
if self.position.x > WIDTH:
self.position.x = 0
elif self.position.x < 0:
self.position.x = WIDTH
if self.position.y > HEIGHT:
self.position.y = 0
elif self.position.y < 0:
self.position.y = HEIGHT
def update(self):
# Update velocity and position
self.velocity += self.acceleration
if self.velocity.length() > MAX_SPEED:
self.velocity.scale_to_length(MAX_SPEED)
self.position += self.velocity
self.acceleration *= 0
def apply_force(self, force):
self.acceleration += force
def flock(self, boids):
# Calculate steering vectors from the three flocking rules
alignment = self.align(boids)
cohesion = self.cohere(boids)
separation = self.separate(boids)
# Weighing the forces
alignment *= 1.0
cohesion *= 0.8
separation *= 1.5
self.apply_force(alignment)
self.apply_force(cohesion)
self.apply_force(separation)
def align(self, boids):
steering = pygame.math.Vector2(0, 0)
total = 0
for other in boids:
if other != self and self.position.distance_to(other.position) < NEIGHBOR_RADIUS:
steering += other.velocity
total += 1
if total > 0:
steering /= total
if steering.length() > 0:
steering.scale_to_length(MAX_SPEED)
steering -= self.velocity
if steering.length() > MAX_FORCE:
steering.scale_to_length(MAX_FORCE)
return steering
def cohere(self, boids):
steering = pygame.math.Vector2(0, 0)
total = 0
for other in boids:
if other != self and self.position.distance_to(other.position) < NEIGHBOR_RADIUS:
steering += other.position
total += 1
if total > 0:
steering /= total
steering -= self.position
if steering.length() > 0:
steering.scale_to_length(MAX_SPEED)
steering -= self.velocity
if steering.length() > MAX_FORCE:
steering.scale_to_length(MAX_FORCE)
return steering
def separate(self, boids):
steering = pygame.math.Vector2(0, 0)
total = 0
for other in boids:
distance = self.position.distance_to(other.position)
if other != self and distance < SEPARATION_RADIUS:
diff = self.position - other.position
if distance > 0:
diff /= distance # weight by distance
steering += diff
total += 1
if total > 0:
steering /= total
if steering.length() > 0:
steering.scale_to_length(MAX_SPEED)
steering -= self.velocity
if steering.length() > MAX_FORCE:
steering.scale_to_length(MAX_FORCE)
return steering
def draw(self, screen):
# Calculate orientation for drawing the boid as a triangle
angle = self.velocity.angle_to(pygame.math.Vector2(1, 0))
# Triangle points: front and two rear corners
p1 = self.position + pygame.math.Vector2(10, 0).rotate(-angle)
p2 = self.position + pygame.math.Vector2(-5, 5).rotate(-angle)
p3 = self.position + pygame.math.Vector2(-5, -5).rotate(-angle)
pygame.draw.polygon(screen, (255, 255, 255), [p1, p2, p3])
# If debug is enabled, draw additional information (velocity vector)
if DEBUG_MODE:
end_pos = self.position + self.velocity * 10
pygame.draw.line(screen, (0, 255, 0), self.position, end_pos, 2)
def draw_debug_panel(screen, boids, clock):
# Draw a semi-transparent panel in the top-left corner
font = pygame.font.SysFont("Arial", 14)
# Collect debug information
fps_text = font.render(f"FPS: {int(clock.get_fps())}", True, (255, 255, 255))
boids_text = font.render(f"Boids: {len(boids)}", True, (255, 255, 255))
max_speed_text = font.render(f"Max Speed: {MAX_SPEED}", True, (255, 255, 255))
neighbor_text = font.render(f"Neighbor Radius: {NEIGHBOR_RADIUS}", True, (255, 255, 255))
separation_text = font.render(f"Separation Radius: {SEPARATION_RADIUS}", True, (255, 255, 255))
# Blit debug info on the panel
screen.blit(fps_text, (10, 10))
screen.blit(boids_text, (10, 30))
screen.blit(max_speed_text, (10, 50))
screen.blit(neighbor_text, (10, 70))
screen.blit(separation_text, (10, 90))
# Additionally, display details of the first boid for deeper debugging
if boids:
boid = boids[0]
pos_text = font.render(f"Boid0 Pos: ({int(boid.position.x)}, {int(boid.position.y)})", True, (255, 255, 255))
vel_text = font.render(f"Boid0 Vel: ({int(boid.velocity.x)}, {int(boid.velocity.y)})", True, (255, 255, 255))
screen.blit(pos_text, (10, 110))
screen.blit(vel_text, (10, 130))
def main():
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Boids Simulation with Debug Menu")
clock = pygame.time.Clock()
# Create boids at random positions
boids = [Boid(random.randint(0, WIDTH), random.randint(0, HEIGHT)) for _ in range(NUM_BOIDS)]
global DEBUG_MODE
debug_toggle_key = pygame.K_d # Press D to toggle the debug menu
running = True
while running:
screen.fill((0, 0, 0))
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == debug_toggle_key:
DEBUG_MODE = not DEBUG_MODE
# Update and draw each boid
for boid in boids:
boid.flock(boids)
boid.update()
boid.edges()
boid.draw(screen)
# Draw debug panel if debug mode is on
if DEBUG_MODE:
draw_debug_panel(screen, boids, clock)
pygame.display.flip()
clock.tick(60)
pygame.quit()
if __name__ == "__main__":
main()