Post

Setting up an evolution simulation using Pygame [devlog 1]

This project is available here if you’d like to check it out.

There aren’t many animations that will remain unique over 100 hours. And since I love AI, I chose to have my animation be an evolution simulation. I will port it to unity eventually but just to get the prototype out, I’m using Pygame. I’d say this has gotten off to a pretty great start!

the squares are the organisms, white circles are food and green circles are eggs

Pygame runs one loop continuously and all the game updates happens in that one loop. Here’s the basic structure of the pygame project.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import pygame
import sys

# Initialize Pygame
pygame.init()

# Screen dimensions
WIDTH, HEIGHT = 800, 600

# Create the game window
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Evolution Simulation")

# Main game loop
running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # Do stuff

    # Update the display
    pygame.display.update()

# Quit Pygame
pygame.quit()
sys.exit()

First, let’s start with the most important element, the organism. I’ve coded it as a seperate python module that I’ll import into the main file.

Here’s the code for organism.py. I must admit that the organism is rather simplistic as it takes the distance to nearest food as the sole input to its neural network. Before implementing a more complex neural network though, I want to work on optimising the code because right now the time complexity of one game loop is O(of) where o is the number of organisms and f is the number of food particles. You’ll see this in the evolutionsim.py file later.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
from keras import Sequential
import numpy as np
import random
from keras import layers
from math import cos,sin,radians
import pygame

class organism:
    '''
    Defines an organism that moves around, eats food and lays eggs
    '''
    # converts a sample space from [0,1] to [-1,1]
    normprob = lambda self,x: x*2-1
    # divides distance with the diagonal length of the screen
    divdist = lambda self,x,settings: x/(settings['x_max']**2+settings['y_max']**2)**.5

    def __init__(self,settings,name=None):
        # The neural network of the organism takes in 1 input: distance to nearest food
        self.brain = Sequential([
            layers.Input(shape=(1,1)),
            layers.Dense(5,activation='tanh',name='l1'),
            layers.Dense(5,activation='tanh',name='l2'),
            layers.Dense(2,activation='tanh',name='l3')
            ])

        # Organism's coordinates
        self.x = random.uniform(settings['x_min'],settings['x_max'])
        self.y = random.uniform(settings['y_min'],settings['y_max'])

        # Organism's rotation, velocity and acceleration
        self.r = random.uniform(0,360)
        self.v = random.uniform(0,settings['v_max']//2)  
        self.dv = random.uniform(-settings['dv_max'], settings['dv_max'])

        # health, distance to nearest food, orientation of nearest food
        self.health = 700
        self.d_food = 100   
        self.r_food = 0    

        # its name
        self.name = name

    def move(self,settings):
        ''' 
        moves the organism 

        sends the organism distance to nearest food to the NN and updates the
        x,y coordinates based on the NN output
        '''
        # normalising distance to nearest food to [-1,1] (to avoid NN saturation)
        input_data = np.array([[[self.normprob(self.divdist(self.d_food,settings))]]])
        # the outputs of the NN are the change in rotation and velocity
        nndr,nndv = [x.numpy() for x in self.brain(input_data)[0][0]]

        # scaling neural network rotation output
        self.r += nndr * settings['dr_max'] * settings['dt']
        self.r %= 360

        # scaling neural network velocity output and and checking for max velocity
        self.v += nndv * settings['dv_max'] * settings['dt']
        if self.v < 0: self.v = 0
        if self.v > settings['v_max']: self.v = settings['v_max']

        # updating the organism's coordinates
        dx = self.v * cos(radians(self.r)) * settings['dt']
        dy = self.v * sin(radians(self.r)) * settings['dt']
        self.x += dx
        self.y += dy

    def layEgg(self,mutation_rate):
        '''
        the organism lays an egg!
        '''
        egg = Egg(self,mutation_rate)
        return egg

    def draw(self, screen, width, height):
        '''
        pygame stuff to make the organism visible on screen
        '''
        rotated_rect = pygame.Surface((width, height), pygame.SRCALPHA)
        pygame.draw.rect(rotated_rect, (255,255,255), (0, 0, width, height))
        rotated_rect = pygame.transform.rotate(rotated_rect, -self.r)
        new_rect = rotated_rect.get_rect(center=(self.x, self.y))
        screen.blit(rotated_rect, new_rect.topleft)

class Egg:
    '''
    defines an egg that hatches into a mutated version of its parent
    '''
    def __init__(self,parent,mutation_rate):
        # the time it was laid
        self.laid = pygame.time.get_ticks()
        # copies the parent's NN
        self.brain = parent.brain
        # and x,y coordinates
        self.x = parent.x
        self.y = parent.y
        # probability of a mutation occuring
        self.mutation_rate = mutation_rate

    def hatch(self,settings):
        '''
        after incubation period, the egg hatches into an organism that's 
        slightly mutated from its parent
        '''
        # Copies the parent organism
        egg_organism = organism(settings)
        egg_organism.brain = self.brain
        egg_organism.x = self.x
        egg_organism.y = self.y

        # Applies mutations to the organism
        # This for loop slightly modifies the weights of the NN based on the
        # mutation rate
        for layer in egg_organism.brain.layers:
            weights = layer.get_weights()
            x,y = weights[0].shape
            for m in range(x):
                for n in range(y):
                    if random.random() <= self.mutation_rate:
                        weights[0][m][n] += random.gauss(0,.04)
            layer.set_weights(weights)

        # ADD capability for neural network architecture to change

        return egg_organism

    def draw(self,screen):
        '''
        pygame stuff to make the egg visible
        '''
        pygame.draw.circle(screen, (100, 100, 0), (self.x, self.y), 7)

Food is very simple in this simulation. I’ll eventually let food evolve, but that would require a more complex environment to interact with and more optimised code.

I present to you food.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pygame
from random import uniform

class food:
    '''
    a pretty simple class, the food just appears and waits to get eaten
    '''
    def __init__(self,settings):
        self.x = uniform(settings['x_min'],settings['x_max'])
        self.y = uniform(settings['y_min'],settings['y_max'])
        # nutrition is directly added to the health of an organism
        self.nutrition = 200

    def spawn(self,screen,radius):
        pygame.draw.circle(screen, (255, 255, 255), (self.x, self.y), radius)

Finally, here’s evolutionsim.py that runs all the calculations and updates the display of the sumulation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import pygame
from organism import organism
import random
from food import food
import sys
import os

# Initialize Pygame
pygame.init()

# Screen dimensions
WIDTH, HEIGHT = 800, 600

# Create the game window
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Evolution Simulation")

# Colors
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)

# Simulation Settings
settings = {
    'x_min': 0,
    'x_max': WIDTH,
    'y_min': 0,
    'y_max': HEIGHT,
    'v_max': 30,
    'dv_max': 20,
    'dr_max': 90,
    'dt': .2,
    'incubation':15000,
    'FPS' : 30
}

# Initialize list of organisms
my_organisms = [organism(settings) for _ in range(15)]
# Initialize list of food
many_food = [food(settings) for _ in range(70)]
# Initialize list of eggs
eggs = []

# Main game loop
running = True

last_food_spawn = pygame.time.get_ticks()
clock = pygame.time.Clock()

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

    screen.fill(BLACK)

    # Spawns food randomly between 1 and 5 seconds
    food_spawn_interval = random.uniform(1000,5000)
    cur_tick = pygame.time.get_ticks()
    if cur_tick - last_food_spawn > food_spawn_interval:
        many_food.append(food(settings))
        last_food_spawn = cur_tick

    # This loop does a lot of calculations
    for my_organism in my_organisms:
        min_dist = WIDTH+HEIGHT
        for some_food in many_food:
           cur_dist = (my_organism.x - some_food.x)**2 + (my_organism.y - some_food.y)**2
           # Searching for closest food to each organism
           min_dist = min(min_dist,cur_dist)
           # Checks for collision with food
           if cur_dist <= 400:
               # Eats food, updates organism health, if organism is healthy it might lay an egg
               many_food.remove(some_food)
               my_organism.health += some_food.nutrition
               if my_organism.health >= 600 and random.random() < .8:
                   # Lays the egg
                   eggs.append(my_organism.layEgg(.1))
        my_organism.d_food = min_dist

    # This loop moves the organisms and reduces their health
    for my_organism in my_organisms:
        my_organism.move(settings)
        my_organism.draw(screen,20,20)
        my_organism.health-=1.5
        # Organism dies
        if my_organism.health <= 0:
            my_organisms.remove(my_organism)

    # Displays the food
    for some_food in many_food:
        some_food.spawn(screen,5)

    # Waits for incubation time to elapse and hatches the egg into an organism
    for egg in eggs:
        if cur_tick >= egg.laid + settings['incubation']:
            my_organisms.append(egg.hatch(settings))
            eggs.remove(egg)
        egg.draw(screen)


    # Update the display
    pygame.display.update()
    clock.tick(settings['FPS'])

# Quit Pygame
# Don't ask me why I use such a forceful way to close the program, the normal method just wasn't working.
os._exit(0)

Feel free to reach out to me if you need me to explain some parts in more detail. Next up in my todolist is to implement some form of spatial optimisation, because the biggest limitations of the project currently is the tiny simulation size and simplistic organisms.

Until next time!

This post is licensed under CC BY 4.0 by the author.

Trending Tags