From 483643a4c0837728565a0ec645c3bddbcaeaf9fa Mon Sep 17 00:00:00 2001 From: OusmBlueNinja <89956790+OusmBlueNinja@users.noreply.github.com> Date: Mon, 7 Apr 2025 11:53:24 -0500 Subject: [PATCH] algorithms --- algorithm-visualisations/Langton.py | 100 +++++++++++++ algorithm-visualisations/Sierpinski.py | 51 +++++++ algorithm-visualisations/boids.py | 194 +++++++++++++++++++++++++ algorithm-visualisations/particles.py | 174 ++++++++++++++++++++++ 4 files changed, 519 insertions(+) create mode 100644 algorithm-visualisations/Langton.py create mode 100644 algorithm-visualisations/Sierpinski.py create mode 100644 algorithm-visualisations/boids.py create mode 100644 algorithm-visualisations/particles.py diff --git a/algorithm-visualisations/Langton.py b/algorithm-visualisations/Langton.py new file mode 100644 index 0000000..ae4cd5e --- /dev/null +++ b/algorithm-visualisations/Langton.py @@ -0,0 +1,100 @@ +import pygame + +# Constants +WIDTH, HEIGHT = 800, 600 +CELL_SIZE = 10 +GRID_COLS = WIDTH // CELL_SIZE +GRID_ROWS = HEIGHT // CELL_SIZE + +# Colors +WHITE = (255, 255, 255) +BLACK = (0, 0, 0) +RED = (255, 0, 0) + +# Directions: 0=Up, 1=Right, 2=Down, 3=Left +direction_vectors = { + 0: (0, -1), + 1: (1, 0), + 2: (0, 1), + 3: (-1, 0) +} +direction_names = { + 0: "Up", + 1: "Right", + 2: "Down", + 3: "Left" +} + +pygame.init() +screen = pygame.display.set_mode((WIDTH, HEIGHT)) +pygame.display.set_caption("Langton's Ant Visualization with Debug Info") +clock = pygame.time.Clock() +font = pygame.font.SysFont("Arial", 18) + +# Initialize grid: False means white cell, True means black cell. +grid = [[False for _ in range(GRID_COLS)] for _ in range(GRID_ROWS)] + +# Initialize ant at center of grid. +ant_col = GRID_COLS // 2 +ant_row = GRID_ROWS // 2 +ant_direction = 0 # Starting direction: Up +steps = 0 + +running = True +while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + + # Run several simulation steps per frame. + for _ in range(10): + # Get the current cell's color. + current_color = grid[ant_row][ant_col] + + # Langton's Ant rules: + # If on a white cell, flip it to black, turn right. + # If on a black cell, flip it to white, turn left. + if not current_color: # White cell + grid[ant_row][ant_col] = True + ant_direction = (ant_direction + 1) % 4 + else: # Black cell + grid[ant_row][ant_col] = False + ant_direction = (ant_direction - 1) % 4 + + # Move ant forward one cell in the current direction, with wrap-around. + dx, dy = direction_vectors[ant_direction] + ant_col = (ant_col + dx) % GRID_COLS + ant_row = (ant_row + dy) % GRID_ROWS + steps += 1 + + # Clear screen (fill with white). + screen.fill(WHITE) + + # Draw grid: fill black cells. + for row in range(GRID_ROWS): + for col in range(GRID_COLS): + if grid[row][col]: + rect = (col * CELL_SIZE, row * CELL_SIZE, CELL_SIZE, CELL_SIZE) + pygame.draw.rect(screen, BLACK, rect) + + # Draw the ant as a red cell. + ant_rect = (ant_col * CELL_SIZE, ant_row * CELL_SIZE, CELL_SIZE, CELL_SIZE) + pygame.draw.rect(screen, RED, ant_rect) + + # Debug text (no background, just text): + debug_text1 = font.render(f"Steps: {steps}", True, BLACK) + debug_text2 = font.render(f"Ant Position: ({ant_col}, {ant_row})", True, BLACK) + debug_text3 = font.render(f"Ant Direction: {direction_names[ant_direction]}", True, BLACK) + current_cell_color = "Black" if grid[ant_row][ant_col] else "White" + debug_text4 = font.render(f"Current Cell: {current_cell_color}", True, BLACK) + + # Blit debug text in the top-left corner. + screen.blit(debug_text1, (10, 10)) + screen.blit(debug_text2, (10, 30)) + screen.blit(debug_text3, (10, 50)) + screen.blit(debug_text4, (10, 70)) + + pygame.display.flip() + clock.tick(60) + +pygame.quit() diff --git a/algorithm-visualisations/Sierpinski.py b/algorithm-visualisations/Sierpinski.py new file mode 100644 index 0000000..6fe925a --- /dev/null +++ b/algorithm-visualisations/Sierpinski.py @@ -0,0 +1,51 @@ +import pygame +import random + +# Initialize pygame +pygame.init() + +# Screen dimensions +WIDTH, HEIGHT = 800, 600 +screen = pygame.display.set_mode((WIDTH, HEIGHT)) +pygame.display.set_caption("Chaos Game - Sierpinski Triangle") + +# Define the triangle vertices +vertex1 = (WIDTH // 2, 50) +vertex2 = (50, HEIGHT - 50) +vertex3 = (WIDTH - 50, HEIGHT - 50) +vertices = [vertex1, vertex2, vertex3] + +# Start with a random point +current_point = (random.randint(0, WIDTH), random.randint(0, HEIGHT)) + +# Set up the clock for controlling frame rate +clock = pygame.time.Clock() + +# Fill background color (black) +screen.fill((0, 0, 0)) + +# Main loop +running = True +while running: + # Handle events (close window) + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + + # Plot several points per frame for a faster build-up + for _ in range(1): + # Choose a random vertex + chosen_vertex = random.choice(vertices) + # Compute the midpoint between current point and chosen vertex + current_point = ((current_point[0] + chosen_vertex[0]) // 2, + (current_point[1] + chosen_vertex[1]) // 2) + # Draw the point (using a small rectangle as a pixel) + screen.fill((255, 255, 255), (current_point[0], current_point[1], 1, 1)) + + # Update display + pygame.display.flip() + + # Limit to 60 frames per second + clock.tick() + +pygame.quit() diff --git a/algorithm-visualisations/boids.py b/algorithm-visualisations/boids.py new file mode 100644 index 0000000..6c4257c --- /dev/null +++ b/algorithm-visualisations/boids.py @@ -0,0 +1,194 @@ +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() diff --git a/algorithm-visualisations/particles.py b/algorithm-visualisations/particles.py new file mode 100644 index 0000000..1fc6bd0 --- /dev/null +++ b/algorithm-visualisations/particles.py @@ -0,0 +1,174 @@ +import pygame +import random +import math + +# Initialize pygame +pygame.init() + +# Screen dimensions and panel sizes +WIDTH, HEIGHT = 800, 600 +PANEL_HEIGHT = 150 # Bottom panel for sliders +SIM_HEIGHT = HEIGHT - PANEL_HEIGHT # Simulation area height + +screen = pygame.display.set_mode((WIDTH, HEIGHT)) +pygame.display.set_caption("Particle Simulation with Adjustable Variables") +clock = pygame.time.Clock() + +# Define a simple Slider class +class Slider: + def __init__(self, x, y, w, h, min_val, max_val, init_val, label): + self.x = x + self.y = y + self.w = w + self.h = h + self.min_val = min_val + self.max_val = max_val + self.value = init_val + self.label = label + self.dragging = False + + def handle_event(self, event): + if event.type == pygame.MOUSEBUTTONDOWN: + if event.button == 1: + if self.get_knob_rect().collidepoint(event.pos): + self.dragging = True + elif event.type == pygame.MOUSEBUTTONUP: + if event.button == 1: + self.dragging = False + elif event.type == pygame.MOUSEMOTION: + if self.dragging: + mx = event.pos[0] + # Clamp mx inside slider range + relative = (mx - self.x) / self.w + relative = max(0, min(1, relative)) + self.value = self.min_val + relative * (self.max_val - self.min_val) + + def get_knob_rect(self): + # Determine knob position along slider track + relative = (self.value - self.min_val) / (self.max_val - self.min_val) + knob_x = self.x + relative * self.w + knob_width = 10 + knob_height = self.h + return pygame.Rect(knob_x - knob_width // 2, self.y, knob_width, knob_height) + + def draw(self, surface, font): + # Draw slider track (a thin rectangle) + track_rect = pygame.Rect(self.x, self.y + self.h // 2 - 2, self.w, 4) + pygame.draw.rect(surface, (200, 200, 200), track_rect) + # Draw knob + knob_rect = self.get_knob_rect() + pygame.draw.rect(surface, (100, 100, 100), knob_rect) + # Draw label and current value (as plain text) + label_surface = font.render(f"{self.label}: {self.value:.2f}", True, (0, 0, 0)) + surface.blit(label_surface, (self.x, self.y - 20)) + +# Particle class +class Particle: + def __init__(self, pos, velocity, lifetime, size, color=(255, 255, 255)): + self.pos = pygame.math.Vector2(pos) + self.vel = pygame.math.Vector2(velocity) + self.lifetime = lifetime + self.size = size + self.initial_lifetime = lifetime + self.color = color + + def update(self, gravity): + self.vel.y += gravity + self.pos += self.vel + self.lifetime -= 1 + + def is_dead(self): + return self.lifetime <= 0 + + def draw(self, surface, glow_intensity): + # Draw glow effect if glow_intensity is greater than zero + if glow_intensity > 0: + # Create a temporary surface for glow with per-pixel alpha + glow_surface = pygame.Surface((self.size*6, self.size*6), pygame.SRCALPHA) + glow_center = self.size * 3 + # Draw several concentric circles for the glow effect + for i in range(1, 6): + # Alpha decreases as the circle radius increases + alpha = max(0, int(255 * (glow_intensity / 10) * (1 - i/6))) + radius = int(self.size + i * 2) + pygame.draw.circle(glow_surface, + (self.color[0], self.color[1], self.color[2], alpha), + (glow_center, glow_center), + radius) + # Blit the glow surface with additive blending + surface.blit(glow_surface, (self.pos.x - self.size*3, self.pos.y - self.size*3), special_flags=pygame.BLEND_RGBA_ADD) + # Draw the particle itself + pygame.draw.circle(surface, self.color, (int(self.pos.x), int(self.pos.y)), int(self.size)) + +# Create sliders for each variable, arranged in two rows within the bottom panel. +font = pygame.font.SysFont("Arial", 16) +sliders = [] +# Row 1: Emission Rate, Speed, Lifetime +sliders.append(Slider(x=20, y=SIM_HEIGHT + 10, w=200, h=20, min_val=0, max_val=50, init_val=5, label="Emission Rate")) +sliders.append(Slider(x=240, y=SIM_HEIGHT + 10, w=200, h=20, min_val=0, max_val=10, init_val=5, label="Speed")) +sliders.append(Slider(x=460, y=SIM_HEIGHT + 10, w=200, h=20, min_val=10, max_val=300, init_val=100, label="Lifetime")) +# Row 2: Size, Gravity, Glow +sliders.append(Slider(x=20, y=SIM_HEIGHT + 50, w=200, h=20, min_val=1, max_val=20, init_val=5, label="Size")) +sliders.append(Slider(x=240, y=SIM_HEIGHT + 50, w=200, h=20, min_val=0, max_val=2, init_val=0.1, label="Gravity")) +sliders.append(Slider(x=460, y=SIM_HEIGHT + 50, w=200, h=20, min_val=0, max_val=10, init_val=3, label="Glow")) + +# List to hold all particles +particles = [] + +# Emitter position (center of simulation area) +emitter = pygame.math.Vector2(WIDTH // 2, SIM_HEIGHT // 2) + +# Main loop +running = True +while running: + # Process events (for quitting and slider events) + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + for slider in sliders: + slider.handle_event(event) + + # Get simulation variables from slider values + emission_rate = sliders[0].value # particles per frame + speed = sliders[1].value # initial speed magnitude + lifetime = sliders[2].value # lifetime in frames + size = sliders[3].value # particle size (radius) + gravity = sliders[4].value # gravity acceleration + glow = sliders[5].value # glow intensity + + # Spawn new particles (round emission_rate to an integer count) + for _ in range(int(emission_rate)): + # Spawn at the emitter with a random direction + angle = random.uniform(0, 2 * math.pi) + velocity = pygame.math.Vector2(math.cos(angle), math.sin(angle)) * speed + particles.append(Particle(emitter, velocity, lifetime, size)) + + # Update particles and remove dead ones + for particle in particles[:]: + particle.update(gravity) + if particle.is_dead(): + particles.remove(particle) + + # Clear simulation area (fill with black) + screen.fill((0, 0, 0), rect=pygame.Rect(0, 0, WIDTH, SIM_HEIGHT)) + # Optionally fill the control area with a light color (or leave it as is) + screen.fill((220, 220, 220), rect=pygame.Rect(0, SIM_HEIGHT, WIDTH, PANEL_HEIGHT)) + + # Draw all particles + for particle in particles: + particle.draw(screen, glow_intensity=glow) + + # Draw slider controls in the bottom panel + for slider in sliders: + slider.draw(screen, font) + + # Draw debug text (plain text, no background) at the top-left corner of the simulation area + debug_text = (f"Particles: {len(particles)} | Emission Rate: {emission_rate:.2f} | Speed: {speed:.2f} | " + f"Lifetime: {lifetime:.2f} | Size: {size:.2f} | Gravity: {gravity:.2f} | Glow: {glow:.2f}") + debug_surface = font.render(debug_text, True, (255, 255, 255)) + screen.blit(debug_surface, (10, 10)) + + pygame.display.flip() + clock.tick(60) + +pygame.quit()