import random
from config import *

def is_in_nest(x, y):
    if x > CELLS_X - CELLS_X //5 and x < CELLS_X - CELLS_X //10:
        if y > CELLS_Y // 3 and y < CELLS_Y - (CELLS_Y // 3):
            return True
    return False

class Ant():
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.foraging = True

    def update(self, foraging_pheromone, retreating_pheromone, food):
        if food[self.x][self.y]:
            self.foraging = False
            food[self.x][self.y] = False
        
        if is_in_nest(self.x, self.y):
            self.foraging = True

        if self.foraging:
            foraging_pheromone[self.x][self.y] = min(1, foraging_pheromone[self.x][self.y] + PHEROMONE_AMOUNT)
        else:
            retreating_pheromone[self.x][self.y] = min(1, retreating_pheromone[self.x][self.y] + PHEROMONE_AMOUNT)

        pheromone = retreating_pheromone
        if not self.foraging:
            pheromone = foraging_pheromone

        highest_pheromone = 0
        highest_position = []

        for neighbor_x in range(self.x - 1, self.x + 2, 1):
                for neighbor_y in range(self.y - 1, self.y + 2, 1):
                    if neighbor_x >= CELLS_X or neighbor_x < 0:
                        continue
                    if neighbor_y >= CELLS_Y or neighbor_y < 0:
                        continue
                    if neighbor_y == self.y and neighbor_x == self.x:
                        continue
                    
                    value = pheromone[neighbor_x][neighbor_y]
                    if value == highest_pheromone:
                        highest_position.append([neighbor_x, neighbor_y])
                    elif value > highest_pheromone:
                        highest_pheromone = value
                        highest_position = []
                        highest_position.append([neighbor_x, neighbor_y])

        chosen_position = highest_position[random.randint(0, len(highest_position) - 1)]
        self.x = chosen_position[0]
        self.y = chosen_position[1]
        
        



ants/ant.py
kylerosser/emergent-behaviour
CELLS_X = 100
CELLS_Y = 100

FPS = 20

ANT_COUNT = 100

PHEROMONE_AMOUNT = 1

DIFFUSE_FACTOR = 0.005
DECAY_FACTOR = 0.01

PIXELS_PER_CELL = 5
ants/config.py
kylerosser/emergent-behaviour
import pygame
from config import *
from ant import *

running = True
foraging_pheromone = [[0 for y in range(CELLS_Y)] for x in range(CELLS_X)]
retreating_pheromone = [[0 for y in range(CELLS_Y)] for x in range(CELLS_X)]
food = [[False for y in range(CELLS_Y)] for x in range(CELLS_X)]
ants = []

pygame.init()

screen = pygame.display.set_mode((CELLS_X * PIXELS_PER_CELL, CELLS_Y * PIXELS_PER_CELL))
pygame.display.set_caption('Ant Colony Simulation')

clock = pygame.time.Clock()

def default_values():
    food[0] = [True for y in range(CELLS_Y)]
    food[1] = [True for y in range(CELLS_Y)]
    food[2] = [True for y in range(CELLS_Y)]
    food[3] = [True for y in range(CELLS_Y)]
    food[4] = [True for y in range(CELLS_Y)]
    food[5] = [True for y in range(CELLS_Y)]

def generate_ants():
    global ants
    for x in range(ANT_COUNT):
        ants.append(Ant(CELLS_X // 2, CELLS_Y // 2))

def update_ants():
    for ant in ants:
        ant.update(foraging_pheromone, retreating_pheromone, food)

def blur_cells(cells):
    new_cells = [[0 for y in range(CELLS_Y)] for x in range(CELLS_X)]
    for x in range(CELLS_X):
        for y in range(CELLS_Y):
            sum = cells[x][y]
            neighbor_count = 0
            for neighbor_x in range(x - 1, x + 2, 1):
                for neighbor_y in range(y - 1, y + 2, 1):
                    if neighbor_x >= CELLS_X or neighbor_x < 0:
                        continue
                    if neighbor_y >= CELLS_Y or neighbor_y < 0:
                        continue
                    if neighbor_y == y and neighbor_x == x:
                        continue

                    sum += cells[neighbor_x][neighbor_y] * DIFFUSE_FACTOR
                    neighbor_count += 1
            
            average = sum / (DIFFUSE_FACTOR * neighbor_count + 1 + DECAY_FACTOR)
            new_cells[x][y] = average
    return new_cells

def blur_cells_2(cells):
    for x in range(CELLS_X):
        for y in range(CELLS_Y):
            cells[x][y] = cells[x][y] * DECAY_FACTOR

def draw_cells():
    for cell_x in range(CELLS_X):
        for cell_y in range(CELLS_Y):
            cell_color = (
                retreating_pheromone[cell_x][cell_y] * 255, 
                0,
                foraging_pheromone[cell_x][cell_y] * 255
            )
            if food[cell_x][cell_y]:
                cell_color = (200, 200, 10)
            if is_in_nest(cell_x, cell_y):
                cell_color = (20, 180, 20)

            pygame.draw.rect(
                screen, 
                cell_color, 
                pygame.Rect(
                    cell_x * PIXELS_PER_CELL, 
                    cell_y * PIXELS_PER_CELL, 
                    PIXELS_PER_CELL, 
                    PIXELS_PER_CELL
                )
            )

def draw_ants():
    for ant in ants:
        color = (200, 200, 10)
        if ant.foraging:
            color = (250, 220, 220)
        pygame.draw.rect(
                screen, 
                color, 
                pygame.Rect(
                    ant.x * PIXELS_PER_CELL, 
                    ant.y * PIXELS_PER_CELL, 
                    PIXELS_PER_CELL, 
                    PIXELS_PER_CELL
                )
            )

default_values()
generate_ants()

while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    screen.fill("black")

    foraging_pheromone = blur_cells(foraging_pheromone)
    retreating_pheromone = blur_cells(retreating_pheromone)
    update_ants()

    draw_cells()
    draw_ants()

    pygame.display.flip()

    clock.tick(FPS)

pygame.quit()
ants/main.py
kylerosser/emergent-behaviour
import pygame
import random
from config import *

class Boid():
    def __init__(self, position):
        self.position = position
        self.velocity = pygame.Vector2(random.uniform(-1, 1), random.uniform(-1, 1)).normalize() * MAX_SPEED
        self.acceleration = pygame.Vector2(0, 0)

    def steer_towards_point(self, point):
        steer_vector = (point - self.position).normalize() * MAX_SPEED
        steer_vector -= self.velocity
        if steer_vector.magnitude() > 0.000001:
            steer_vector = steer_vector.clamp_magnitude(MAX_FORCE)
        return steer_vector

    def separation(self, boids):
        # Returns a steer vector towards the point with least amount of neighbouring boids
        steer_vector = pygame.Vector2(0, 0)
        n = 0
        for other_boid in boids:
            distance = (other_boid.position - self.position).magnitude()
            if distance > 0 and distance < DESIRED_SEPARATION:
                steer_vector += (self.position - other_boid.position).normalize() / distance
                n += 1
        
        if n > 0:
            steer_vector /= n

        if steer_vector.magnitude() > 0:
            steer_vector = steer_vector.normalize()
            steer_vector *= MAX_SPEED
            steer_vector -= self.velocity
            if steer_vector.magnitude() > 0.000001:
                steer_vector = steer_vector.clamp_magnitude(MAX_FORCE)
        
        return steer_vector

    def alignment(self, boids):
        # Returns a steer vector towards average velocity of neighbouring boids
        velocity_sum = pygame.Vector2(0, 0)
        n = 0
        for other_boid in boids:
            distance = (other_boid.position - self.position).magnitude()
            if distance < MAX_ALIGNMENT_VISION:
                velocity_sum += other_boid.velocity
                n += 1
        
        if n > 0:
            average_velocity = velocity_sum / n
            steer_vector = average_velocity.normalize()
            steer_vector *= MAX_SPEED
            steer_vector -= self.velocity
            if steer_vector.magnitude() > 0.00001:
                steer_vector.clamp_magnitude(MAX_FORCE)
            return steer_vector
        else:
            return pygame.Vector2(0, 0)


    def cohesion(self, boids):
        # Returns a steer vector towards average position of neighbouring boids
        position_sum = pygame.Vector2(0, 0)
        n = 0
        for other_boid in boids:
            distance = (other_boid.position - self.position).magnitude()
            if distance < MAX_COHESION_VISION:
                position_sum += other_boid.position
                n += 1

        if n > 0:
            average_position = position_sum / n
            if abs((average_position - self.position).magnitude()) <= 0.00001:
                
                return pygame.Vector2(0, 0)
            
            steer_vector = self.steer_towards_point(average_position)
            return steer_vector
        else:
            return pygame.Vector2(0, 0)

    
    def update(self, boids, move_towards_mouse, mouse_position):
        # Calculate steering velocities for each rule
        seperation_velocity = self.separation(boids)
        alignment_velocity = self.alignment(boids)
        cohesion_velocity = self.cohesion(boids)
        towards_mouse_velocity = pygame.Vector2(0, 0)
        
        # If mouse button is down, apply the mouse attraction rule
        if move_towards_mouse:
            distance = (mouse_position - self.position).magnitude()
            if distance < TOWARDS_MOUSE_VISION:
                towards_mouse_velocity = self.steer_towards_point(mouse_position)
        
        # Weight each velocity to taste, and add to acceleration
        self.acceleration += seperation_velocity * SEPARATION_WEIGHT
        self.acceleration += alignment_velocity * ALIGNMENT_WEIGHT
        self.acceleration += cohesion_velocity * COHESION_WEIGHT
        self.acceleration += towards_mouse_velocity * TOWARDS_MOUSE_WEIGHT

        # Step velocity/position values
        self.velocity += self.acceleration
        if not self.velocity.magnitude() == 0:
            self.velocity = self.velocity.clamp_magnitude(MAX_SPEED)
        self.position += self.velocity
        self.acceleration *= 0
        
        # Wrap-around borders of screen
        border_tolerance = 10
        if self.position.x < -border_tolerance: self.position.x = WIDTH + border_tolerance
        if self.position.x > WIDTH + border_tolerance: self.position.x = -border_tolerance
        if self.position.y < -border_tolerance: self.position.y = HEIGHT + border_tolerance
        if self.position.y > HEIGHT + border_tolerance: self.position.y = -border_tolerance
        

    def render(self, screen):
        # Render the boid on the pygame screen
        if not self.velocity.magnitude() == 0:
            direction_vector = self.velocity.normalize()
        else:
            direction_vector = pygame.Vector2(0, 1)
        tip_point = self.position + direction_vector * 5
        left_point = self.position - direction_vector.rotate(-30) * 7
        right_point = self.position - direction_vector.rotate(30) * 7
        pygame.draw.polygon(
            screen, 
            "white", 
            [tip_point, left_point, right_point]
        )
boids/boid.py
kylerosser/emergent-behaviour
WIDTH = 700 # Screen size
HEIGHT = 700

NUMBER_OF_BOIDS = 100 # Number of boids (m ore boids results in quadratically poorer performance :) )

MAX_SPEED = 2 # The maximum speed of boids
MAX_FORCE = 0.01 # The maximum magnitude of force applied to boids each step

DESIRED_SEPARATION = 25 # Boids will try to steer away from eachother if they are within this distance
MAX_COHESION_VISION = 50 # Boids will try to steer towards eachother if they are within this distance
MAX_ALIGNMENT_VISION = 50 # Boids will try to steer such that they align with eachother if they are within this distance
TOWARDS_MOUSE_VISION = 150 # Boids will try to steer towards the mouse if they are within this distance of it

# Arbitrary weights for each steering force
SEPARATION_WEIGHT = 10
ALIGNMENT_WEIGHT = 0.5
COHESION_WEIGHT = 0.5
TOWARDS_MOUSE_WEIGHT = 20
boids/config.py
kylerosser/emergent-behaviour
import pygame
import random
from config import *
from boid import Boid

running = True
boids = []

for i in range(NUMBER_OF_BOIDS):
    boids.append(Boid(pygame.Vector2(random.randint(1, WIDTH), random.randint(1, HEIGHT))))

pygame.init()

screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption('Boids')

clock = pygame.time.Clock()

while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    screen.fill("black")

    mouse_down = pygame.mouse.get_pressed()[0]
    mouse_position = pygame.mouse.get_pos()
    
    for boid in boids:
        if mouse_down:
            boid.update(boids, True, mouse_position)
        else:
            boid.update(boids, False, None)
        boid.render(screen)

    if mouse_down:
        pygame.draw.circle(screen, (255, 255, 255, 150), mouse_position, TOWARDS_MOUSE_VISION, 1)
        
    pygame.display.flip()

    clock.tick(60)

pygame.quit()
boids/main.py
kylerosser/emergent-behaviour
CELLS_X = 70 # Number of cells on the grid
CELLS_Y = 70

PIXELS_PER_CELL = 10 # Size of each cell in pixels

FPS = 10 # Maximum number of updates per second

BORN_NEIGHBORS = [3] # A dead cell will be reborn if it has this many neighbors
SURVIVE_NEIGHBORS = [2, 3] # A living cell will survive if it has this many neighbors
# ( All other living cells die )

'''
Common rulesets include: 
B3/S23 (Conway's Game Of Life)
B36/S23 (HighLife)
B3/S12345 (Maze Generation)

Where B represents means BORN_NEIGHBORS and S SURVIVE_NEIGHBORS
'''
life/config.py
kylerosser/emergent-behaviour
import pygame
from config import *

cells = [ [ False for y in range(CELLS_Y) ] for x in range(CELLS_X) ]
running = True
paused = True
age = 0

pygame.init()

screen = pygame.display.set_mode((CELLS_X * PIXELS_PER_CELL, CELLS_Y * PIXELS_PER_CELL))
pygame.display.set_caption('Game of Life')

clock = pygame.time.Clock()
font = pygame.font.SysFont("Arial", 16)

def update_cells():
    global cells
    global age

    new_cells = [ [False for y in range(CELLS_Y)] for x in range(CELLS_X)]

    for x in range(CELLS_X):
        for y in range(CELLS_Y):
            alive = cells[x][y]
            alive_neighbors = 0

            for neighbor_x in range(x - 1, x + 2, 1):
                for neighbor_y in range(y - 1, y + 2, 1):
                    # Reject neighbors out of bounds
                    if neighbor_x >= CELLS_X or neighbor_x < 0:
                        continue
                    if neighbor_y >= CELLS_Y or neighbor_y < 0:
                        continue
                    if neighbor_y == y and neighbor_x == x:
                        continue

                    if cells[neighbor_x][neighbor_y]:
                        alive_neighbors += 1
            
            if alive:
                should_live = False
                for n in SURVIVE_NEIGHBORS:
                    if alive_neighbors == n:
                        should_live = True
                new_cells[x][y] = should_live
            else:
                should_live = False
                for n in BORN_NEIGHBORS:
                    if alive_neighbors == n:
                        should_live = True
                new_cells[x][y] = should_live

    cells = new_cells
    age += 1

def draw_cells():
    for x in range(CELLS_X):
        for y in range(CELLS_Y):
            alive = cells[x][y]
            if alive:
                pygame.draw.rect(screen, "white", pygame.Rect(x * PIXELS_PER_CELL, y * PIXELS_PER_CELL, PIXELS_PER_CELL, PIXELS_PER_CELL))

def draw_ui():
    stats_string = f"RULE: B{''.join(map(str, BORN_NEIGHBORS))}/S{''.join(map(str, SURVIVE_NEIGHBORS))}  FPS: {FPS}  AGE: {age}"
    stats_image = font.render(stats_string, True, 'white')
    controls_string = "SPACE to pause/play the simulation; MOUSE1 to toggle cell"
    controls_image = font.render(controls_string, True, 'white')
    screen.blit(stats_image, (10, 10))
    screen.blit(controls_image, (10, 30))

def handle_events():
    global running
    global paused

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_SPACE:
                paused = not paused
        elif event.type == pygame.MOUSEBUTTONDOWN:
            mouse_position = pygame.mouse.get_pos()
            cell_x = mouse_position[0] // PIXELS_PER_CELL
            cell_y = mouse_position[1] // PIXELS_PER_CELL
            cells[cell_x][cell_y] = not cells[cell_x][cell_y]

while running:
    screen.fill("black")

    handle_events()

    if not paused:
        update_cells()

    draw_cells()
    draw_ui()
    
    pygame.display.flip()
    clock.tick(FPS)

pygame.quit()

life/main.py
kylerosser/emergent-behaviour