Full Code of MichaelJWelsh/bot-evolution for AI

master 6d8e3449fc53 cached
7 files
24.2 KB
6.3k tokens
32 symbols
1 requests
Download .txt
Repository: MichaelJWelsh/bot-evolution
Branch: master
Commit: 6d8e3449fc53
Files: 7
Total size: 24.2 KB

Directory structure:
gitextract_rb342yw4/

├── LICENSE
├── README.md
└── src/
    ├── main.py
    ├── neural_network.py
    ├── population.py
    ├── settings.py
    └── utility.py

================================================
FILE CONTENTS
================================================

================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2017 Michael J Welsh

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# Bot Evolution
Bot evolution is an interesting display of evolution through neural networks and a genetic algorithm. Bot's have a field of vision represented by their antenna's. They are told if they "see" food in their field of vision, and are then asked to either move forward, turn counterclockwise, turn clockwise, or do nothing. If a bot does not recieve food after a certain period of time, it will die off. When a bot gets food, it reproduces asexually with a chance of mutation. If a bot goes too far out of the map, it dies and a completely random bot is spawned in the middle of the map. Each bot has its own neural network. You can see species emerge based on their colors. A random group of bots is spawned in the middle of the map upon startup.


![](https://github.com/MichaelJWelsh/bot-evolution/blob/master/example.gif)


## Usage
Simply go into the source folder and type this in terminal:
```python
python3.5 main.py
```


## Dependencies
 - numpy
 - pygame


================================================
FILE: src/main.py
================================================
"""
Bot Evolution v1.0.0
"""

import os
import sys
import pickle
import pygame as pg
from pygame.locals import *
import numpy as np
import datetime
import settings
import population

def main():
    np.random.seed()
    pg.init()

    # Initialize runtime variables.
    periodically_save = False
    pop = None
    if os.path.isfile("save.txt") and input("Save file detected! Use it? (y/n): ").lower() == 'y':
        settings.FPS, settings.WINDOW_WIDTH, settings.WINDOW_HEIGHT, settings.TIME_MULTIPLIER, pop = pickle.load(open("save.txt", "rb"))
    else:
        pop_size = 0
        mutation_rate = 0
        while True:
            pop_size = int(input("Population size: "))
            if pop_size < 5:
                print("Population size must be at least 5!")
            else:
                break
        while True:
            mutation_rate = float(input("Mutation rate: "))
            if mutation_rate <= 0 or mutation_rate >= 1:
                print("Mutation rate must be in the range (0, 1)!")
            else:
                break
        while True:
            settings.TIME_MULTIPLIER = float(input("Time multiplier: "))
            if settings.TIME_MULTIPLIER < 1:
                print("Time multiplier must be at least 1!")
            else:
                break
        if input("Advance options? (y/n): ").lower() == 'y':
            while True:
                settings.FPS = int(input("Frames per second: "))
                if settings.FPS < 1:
                    print("FPS must be at least 1!")
                else:
                    break
            while True:
                settings.WINDOW_WIDTH = int(input("Window width: "))
                if settings.WINDOW_WIDTH < 50:
                    print("Window width must be at least 50!")
                else:
                    break
            while True:
                settings.WINDOW_HEIGHT = int(input("Window height: "))
                if settings.WINDOW_HEIGHT < 50:
                    print("Window height must be at least 50!")
                else:
                    break
        pop = population.Population(pop_size, mutation_rate)
    if input("Periodically save every half hour? (y/n): ").lower() == 'y':
        periodically_save = True
    print("\nNote: ")
    print("\tPress 'r' to reset the population.")
    print("\tPress 'p' to pause / unpause.")
    print("\tPress 's' to save population's data (for use next time).")
    print("\tPress 'up' / 'down' to change the populations mutation rate.")
    print("\tPress 'left' / 'right' to change the time multiplier.")
    print("\tClick on the screen to lay down food.")

    # Core variables.
    FONT_SIZE = 30
    FONT = pg.font.SysFont("Arial", FONT_SIZE)
    fps_clock = pg.time.Clock()
    window = pg.display.set_mode((settings.WINDOW_WIDTH, settings.WINDOW_HEIGHT))
    pg.display.set_caption("Bot Evolution")

    # Main loop.
    dt = 0.0
    fps_clock.tick(int(settings.FPS / (settings.TIME_MULTIPLIER / 5.0 + 1)))
    paused = False
    while True:
        key_pressed = {"up": False, "down": False, "left": False, "right": False}
        for event in pg.event.get():
            if event.type == QUIT:
                pg.quit()
                sys.exit()
            elif event.type == pg.KEYDOWN:
                if event.key == pg.K_r:
                    pop = population.Population(pop.SIZE, pop.mutation_rate)
                if event.key == pg.K_s:
                    pickle.dump([settings.FPS, settings.WINDOW_WIDTH, settings.WINDOW_HEIGHT, settings.TIME_MULTIPLIER, pop], open("save.txt", "wb"))
                if event.key == pg.K_p:
                    paused = not paused
            elif event.type == pg.MOUSEBUTTONUP:
                pos = pg.mouse.get_pos()
                pop.food.pop()
                food = population.Food(pop)
                food.x = pos[0]
                food.y = pos[1]
                pop.food.append(food)
        if paused:
            dt = fps_clock.tick(int(settings.FPS / (settings.TIME_MULTIPLIER / 5.0 + 1))) / 1000.0 * int(settings.FPS / (settings.TIME_MULTIPLIER / 5.0 + 1))
            continue
        if periodically_save and datetime.datetime.now().minute % 30 == 0:
            pickle.dump([settings.FPS, settings.WINDOW_WIDTH, settings.WINDOW_HEIGHT, settings.TIME_MULTIPLIER, pop], open("save.txt", "wb"))
        keys = pg.key.get_pressed()
        if keys[pg.K_UP]:
            key_pressed["up"] = True
        if keys[pg.K_DOWN]:
            key_pressed["down"] = True
        if key_pressed["up"] and key_pressed["down"]:
            key_pressed["up"] = False
            key_pressed["down"] = False
        if keys[pg.K_LEFT]:
            key_pressed["left"] = True
        if keys[pg.K_RIGHT]:
            key_pressed["right"] = True
        if key_pressed["left"] and key_pressed["right"]:
            key_pressed["left"] = False
            key_pressed["right"] = False
        update(dt, pop, key_pressed)
        window.fill((0, 0, 0))
        render(window, FONT, pop)
        pg.display.update()
        dt = fps_clock.tick(int(settings.FPS / (settings.TIME_MULTIPLIER / 5.0 + 1))) / 1000.0 * int(settings.FPS / (settings.TIME_MULTIPLIER / 5.0 + 1))

display_time_remaining = 0.0
def update(dt, pop, key_pressed):
    global display_time_remaining
    if key_pressed["up"] or key_pressed["down"] or key_pressed["left"] or key_pressed["right"]:
        display_time_remaining = 3.0

        if key_pressed["up"]:
            pop.mutation_rate += 0.001
        elif key_pressed["down"]:
            pop.mutation_rate -= 0.001
        if pop.mutation_rate <= 0:
            pop.mutation_rate = 0.001
        elif pop.mutation_rate >= 1:
            pop.mutation_rate = 0.999

        if key_pressed["left"]:
            settings.TIME_MULTIPLIER -= 0.1
        elif key_pressed["right"]:
            settings.TIME_MULTIPLIER += 0.1
        if settings.TIME_MULTIPLIER < 1:
            settings.TIME_MULTIPLIER = 1.0
    else:
        display_time_remaining -= 1.0 / settings.FPS * dt
        if display_time_remaining < 0:
            display_time_remaining = 0.0

    pop.update(dt)

def render(window, FONT, pop):
    for food in pop.food:
        pg.draw.circle(window, food.RGB, (int(food.x), int(food.y)), food.HITBOX_RADIUS)

    for bot in pop.bots:
        # Draw body.
        pg.draw.circle(window, bot.RGB, (int(bot.x), int(bot.y)), bot.HITBOX_RADIUS)

        # Draw field-of-vision lines.
        LINE_THICKNESS = 1
        PROTRUSION = int(bot.HITBOX_RADIUS * 1.5)
        to_x = int(bot.x + (bot.HITBOX_RADIUS + PROTRUSION) * np.cos(bot.theta - bot.FIELD_OF_VISION_THETA / 2))
        to_y = int(bot.y - (bot.HITBOX_RADIUS + PROTRUSION) * np.sin(bot.theta - bot.FIELD_OF_VISION_THETA / 2))
        pg.draw.line(window, bot.RGB, (bot.x, bot.y), (to_x, to_y), LINE_THICKNESS)
        to_x = int(bot.x + (bot.HITBOX_RADIUS + PROTRUSION) * np.cos(bot.theta + bot.FIELD_OF_VISION_THETA / 2))
        to_y = int(bot.y - (bot.HITBOX_RADIUS + PROTRUSION) * np.sin(bot.theta + bot.FIELD_OF_VISION_THETA / 2))
        pg.draw.line(window, bot.RGB, (bot.x, bot.y), (to_x, to_y), LINE_THICKNESS)

    if display_time_remaining > 0:
        resultSurf = FONT.render("Mutation Rate: %.3f       Speed: %.1fx" % (pop.mutation_rate, settings.TIME_MULTIPLIER), True, (255, 255, 255))
        resultRect = resultSurf.get_rect()
        resultRect.topleft = (25, 25)
        window.blit(resultSurf, resultRect)

if __name__ == "__main__":
    main()


================================================
FILE: src/neural_network.py
================================================
"""
This module implements the neural network class and all components necessary to
modularize the build/use process of the neural network.
"""

import numpy as np
import pickle

class NNetwork:
    """
    The representation of a feed forward neural network with a bias in every
    layer (excluding output layer obviously).
    """

    def __init__(self, layer_sizes, activation_funcs, bias_neuron = False):
        """
        Creates a 'NNetwork'. 'layer_sizes' provides information about the
        number of neurons in each layer, as well as the total number of layers
        in the neural network. 'activation_funcs' provides information about the
        activation functions to use on each respective hidden layers and output
        layer. This means that the length of 'activation_funcs' is always one
        less than the length of 'layer_sizes'.
        """
        assert(len(layer_sizes) >= 2)
        assert(len(layer_sizes) - 1 == len(activation_funcs))
        assert(min(layer_sizes) >= 1)
        self.layers = []
        self.connections = []

        # Initialize layers.
        for i in range(len(layer_sizes)):
            # Input layer.
            if i == 0:
                self.layers.append(Layer(layer_sizes[i], None, bias_neuron))
            # Hidden layer.
            elif i < len(layer_sizes) - 1:
                self.layers.append(Layer(layer_sizes[i], activation_funcs[i - 1], bias_neuron))
            # Output layer.
            else:
                self.layers.append(Layer(layer_sizes[i], activation_funcs[i - 1]))

        # Initialize connections.
        num_connections = len(layer_sizes) - 1
        for i in range(num_connections):
            self.connections.append(Connection(self.layers[i], self.layers[i + 1]))

    def feed_forward(self, data, one_hot_encoding = True):
        """
        Feeds given data through neural network and stores output in output
        layer's data field. Output can optionally be one-hot encoded.
        """
        if self.layers[0].HAS_BIAS_NEURON:
            assert(len(data) == self.layers[0].SIZE - 1)
            self.layers[0].data = data
            self.layers[0].data.append(1)
        else:
            assert(len(data) == self.layers[0].SIZE)
            self.layers[0].data = data
        for i in range(len(self.connections)):
            self.connections[i].TO.data = np.dot(self.layers[i].data, self.connections[i].weights)
            self.connections[i].TO.activate()
        if one_hot_encoding:
            this_data = self.layers[len(self.layers) - 1].data
            MAX = max(this_data)
            for i in range(len(this_data)):
                if this_data[i] == MAX:
                    this_data[i] = 1
                else:
                    this_data[i] = 0

    def output(self):
        """
        Retrieves data in output layer.
        """
        return self.layers[len(self.layers) - 1].data

class Layer:
    """
    The representation of a layer in a neural network. Used as a medium for
    passing data through the network in an efficent manner.
    """

    def __init__(self, num_neurons, activation_func, bias_neuron = False):
        """
        Creates a 'Layer' with 'num_neurons' and an additional (optional) bias
        neuron (which always has a value of '1'). The layer will utilize the
        'activation_func' during activation.
        """
        assert(num_neurons > 0)
        self.ACTIVATION_FUNC = activation_func
        self.HAS_BIAS_NEURON = bias_neuron
        if bias_neuron:
            self.SIZE = num_neurons + 1
            self.data = np.array([0] * num_neurons + [1])
        else:
            self.SIZE = num_neurons
            self.data = np.array([0] * num_neurons)

    def activate(self):
        """
        Calls activation function on layer's data.
        """
        if self.ACTIVATION_FUNC != None:
            self.ACTIVATION_FUNC(self.data)

class Connection:
    """
    The representation of a connection between layers in a neural network.
    """

    def __init__(self, layer_from, layer_to):
        """"
        Creates a 'Connection' between 'layer_from' and 'layer_to' that contains
        all required weights, which are randomly initialized with random numbers
        in a guassian distribution of mean '0' and standard deviation '1'.
        """
        self.FROM = layer_from
        self.TO = layer_to
        self.weights = np.zeros((layer_from.SIZE, layer_to.SIZE))
        for i in range(layer_from.SIZE):
            for j in range(layer_to.SIZE):
                self.weights[i][j] = np.random.standard_normal()

def sigmoid(data):
    """
    Uses sigmoid transformation on given data. This is an activation function.
    """
    for i in range(len(data)):
        data[i] = 1 / (1 + np.exp(-data[i]))

def softmax(data):
    """
    Uses softmax transformation on given data. This is an activation function.
    """
    sum = 0.0
    for i in range(len(data)):
        sum += np.exp(data[i])
    for i in range(len(data)):
        data[i] = np.exp(data[i]) / sum


================================================
FILE: src/population.py
================================================
"""
This modules implements the bulk of Bot Evolution.
"""

import numpy as np
import copy
import settings
from utility import seq_is_equal, distance_between, angle_is_between, find_angle
from neural_network import NNetwork, sigmoid, softmax

class Population:
    """
    The environment of bots and food.
    """

    def __init__(self, size, mutation_rate):
        assert(size >= 5)
        assert(0 < mutation_rate < 1)
        self.SIZE = size
        self.mutation_rate = mutation_rate
        self.bots = []
        self.food = []
        self.time_since_last_death = 0.0

        # The neural network will have 1 neuron in the input layer, 1 hidden
        # layer with 2 neurons, and 4 neurons in the output layer. The sigmoid
        # activation function will be used on the hidden layer, and a softmax
        # activation function will be used on the output layer. Input consists
        # of the bot's direction and if there is or isn't food in the bots field
        # of vision. Output consists of whether or not to move foward, turn
        # left, turn right, or do nothing.
        for i in range(size):
            random_rgb = (np.random.randint(30, 256), np.random.randint(30, 256), np.random.randint(30, 256))
            self.bots.append(Bot(NNetwork((1, 2, 4), (sigmoid, softmax)), random_rgb, self))
        self.food.append(Food(self))

    def eliminate(self, bot, replace = False):
        self.time_since_last_death = 0.0
        self.bots.remove(bot)
        if replace:
            random_rgb = (np.random.randint(30, 256), np.random.randint(30, 256), np.random.randint(30, 256))
            self.bots.append(Bot(NNetwork((1, 2, 4), (sigmoid, softmax)), random_rgb, self))

    def feed(self, bot, food):
        bot.score = 1.0
        self.food.remove(food)
        self.food.append(Food(self))
        num_to_replace = int(self.SIZE / 7 - 1)
        if num_to_replace < 2:
            num_to_replace = 2
        for i in range(num_to_replace):
            weakest = self.bots[0]
            for other in self.bots:
                if other.score < weakest.score:
                    weakest = other
            self.eliminate(weakest)
        for i in range(num_to_replace):
            if np.random.uniform(0, 1) <= self.mutation_rate:
                new_rgb = [bot.RGB[0], bot.RGB[1], bot.RGB[2]]
                new_rgb[np.random.choice((0, 1, 2))] = np.random.uniform(30, 256)
                new_bot = Bot(bot.nnet, new_rgb, self)
                new_bot.x = bot.x + Bot.HITBOX_RADIUS * 4 * np.random.uniform(0, 1) * np.random.choice((-1, 1))
                new_bot.y = bot.y + Bot.HITBOX_RADIUS * 4 * np.random.uniform(0, 1) * np.random.choice((-1, 1))
                nb_c = new_bot.nnet.connections
                mutated = False
                while not mutated:
                    for k in range(len(nb_c)):
                        for i in range(nb_c[k].FROM.SIZE):
                            for j in range(nb_c[k].TO.SIZE):
                                if np.random.uniform(0, 1) <= self.mutation_rate:
                                    nb_c[k].weights[i][j] = nb_c[k].weights[i][j] * np.random.normal(1, 0.5) + np.random.standard_normal()
                                    mutated = True
                self.bots.append(new_bot)
            else:
                new_bot = Bot(bot.nnet, bot.RGB, self)
                new_bot.x = bot.x + Bot.HITBOX_RADIUS * 4 * np.random.uniform(0, 1) * np.random.choice((-1, 1))
                new_bot.y = bot.y + Bot.HITBOX_RADIUS * 4 * np.random.uniform(0, 1) * np.random.choice((-1, 1))
                self.bots.append(new_bot)

    def update(self, dt):
        """
        Updates the population's internals. The bulk of event handling for all
        bots and food starts here.
        """
        self.time_since_last_death += 1.0 / settings.FPS * dt * settings.TIME_MULTIPLIER

        for food in self.food[:]:
            if food not in self.food:
                continue
            food.update(dt)

        for bot in self.bots[:]:
            if bot not in self.bots:
                continue

            sensory_input = []

            # This is where the bot's field of vision is put into action.
            min_theta = bot.theta - Bot.FIELD_OF_VISION_THETA / 2
            max_theta = bot.theta + Bot.FIELD_OF_VISION_THETA / 2
            food_in_sight = False
            for food in self.food:
                if angle_is_between(find_angle(bot.x, bot.y, food.x, food.y), min_theta, max_theta):
                    food_in_sight = True
                    break
            if food_in_sight:
                sensory_input.append(1.0)
            else:
                sensory_input.append(0.0)

            # Useful debugging outputs.
            #print(bot.RGB)
            #print(sensory_input)

            bot.update(dt, sensory_input)

        if self.time_since_last_death >= 5:
            weakest = self.bots[0]
            for bot in self.bots:
                if bot.score < weakest.score:
                    weakest = bot
            self.eliminate(weakest, replace = True)

class Bot:
    """
    The representation of the circle thing with probes.
    """

    # In pixels/pixels per second/revolutions per second/radians.
    SPAWN_RADIUS = int(settings.WINDOW_WIDTH / 20) if settings.WINDOW_WIDTH <= settings.WINDOW_HEIGHT else int(settings.WINDOW_HEIGHT / 20)
    HITBOX_RADIUS = 6
    SPEED = 350.0
    TURN_RATE = 2 * np.pi
    FIELD_OF_VISION_THETA = 45 * np.pi / 180

    # These lists represent the output from the neural network. Note that the
    # output '[0, 0, 0, 1]' means "do nothing".
    MOVE_FORWARD =  [1, 0, 0, 0]
    TURN_LEFT =     [0, 1, 0, 0]
    TURN_RIGHT =    [0, 0, 1, 0]

    def __init__(self, nnet, rgb, population):
        self.nnet = copy.deepcopy(nnet)
        self.RGB = rgb
        self.pop = population
        self.theta = np.random.uniform(0, 1) * 2 * np.pi
        self.x = settings.WINDOW_WIDTH / 2.0 + Bot.SPAWN_RADIUS * np.random.uniform(0, 1) * np.cos(self.theta)
        self.y = settings.WINDOW_HEIGHT / 2.0 + Bot.SPAWN_RADIUS * np.random.uniform(0, 1) * np.sin(self.theta)
        self.score = 0.0

    def _move_forward(self, dt):
        self.x += Bot.SPEED / settings.FPS * dt * np.cos(self.theta) * settings.TIME_MULTIPLIER
        self.y -= Bot.SPEED / settings.FPS * dt * np.sin(self.theta) * settings.TIME_MULTIPLIER
        if self.x < -Bot.HITBOX_RADIUS * 6 or self.x > settings.WINDOW_WIDTH + Bot.HITBOX_RADIUS * 6 \
        or self.y < -Bot.HITBOX_RADIUS * 6 or self.y > settings.WINDOW_HEIGHT + Bot.HITBOX_RADIUS * 6:
            self.pop.eliminate(self, replace = True)

    def _turn_left(self, dt):
        self.theta += Bot.TURN_RATE / settings.FPS * dt * settings.TIME_MULTIPLIER
        while self.theta >= 2 * np.pi:
            self.theta -= 2 * np.pi

    def _turn_right(self, dt):
        self.theta -= Bot.TURN_RATE / settings.FPS * dt * settings.TIME_MULTIPLIER
        while self.theta < 0:
            self.theta += 2 * np.pi

    def update(self, dt, sensory_input):
        """
        Updates the bot's internals. "Hunger" can be thought of as a score
        between '-1' and '1' where a greater value means less hungry.
        """
        self.score -= 1.0 / settings.FPS / 10.0 * dt * settings.TIME_MULTIPLIER
        if self.score < -1:
            self.score = -1.0
        self.nnet.feed_forward(sensory_input)
        output = self.nnet.output()
        if seq_is_equal(output, Bot.MOVE_FORWARD):
            self._move_forward(dt)
        elif seq_is_equal(output, Bot.TURN_LEFT):
            self._turn_left(dt)
        elif seq_is_equal(output, Bot.TURN_RIGHT):
            self._turn_right(dt)

class Food:
    """
    The representation of the red circles.
    """

    # In pixels.
    HITBOX_RADIUS = 5
    RGB = (255, 0, 0)

    def __init__(self, population):
        mid_x = int(settings.WINDOW_WIDTH / 2)
        mid_y = int(settings.WINDOW_HEIGHT / 2)
        max_left_x = mid_x - (Bot.SPAWN_RADIUS + Bot.HITBOX_RADIUS + 5)
        min_right_x = mid_x + (Bot.SPAWN_RADIUS + Bot.HITBOX_RADIUS + 5)
        max_top_y = mid_y - (Bot.SPAWN_RADIUS + Bot.HITBOX_RADIUS + 5)
        min_bottom_y = mid_y + (Bot.SPAWN_RADIUS + Bot.HITBOX_RADIUS + 5)
        self.x = np.random.choice((np.random.uniform(0, max_left_x), np.random.uniform(min_right_x, settings.WINDOW_WIDTH)))
        self.y = np.random.choice((np.random.uniform(0, max_top_y), np.random.uniform(min_bottom_y, settings.WINDOW_HEIGHT)))
        self.pop = population

    def update(self, dt):
        """
        Updates the food's internals and handles bot<->food collision.
        """
        for bot in self.pop.bots:
            if distance_between(self.x, self.y, bot.x, bot.y) <= Bot.HITBOX_RADIUS + Food.HITBOX_RADIUS:
                self.pop.feed(bot, self)
                break


================================================
FILE: src/settings.py
================================================
"""
This module contains the general settings used across modules.
"""

FPS = 60
WINDOW_WIDTH = 1100
WINDOW_HEIGHT = 600
TIME_MULTIPLIER = 1.0


================================================
FILE: src/utility.py
================================================
"""
This module contains general-use functions.
"""

import numpy as np

def seq_is_equal(a, b):
    """
    Helper function that checks if two sequences are equal (assuming they have
    the same length).
    """
    for i in range(len(a)):
        if a[i] != b[i]:
            return False
    return True

def angle_is_between(angle, a, b):
    """
    Takes in 3 angles (in radians) and returns 'a' <= 'angle' <= 'b'.
    """
    to_degrees = lambda rads: int(rads * 180 / np.pi)
    reduce_degrees = lambda degr: (360 + degr % 360) % 360
    angle = to_degrees(angle)
    a = to_degrees(a)
    b = to_degrees(b)
    angle = reduce_degrees(angle)
    a = reduce_degrees(a)
    b = reduce_degrees(b)
    if a < b:
        return a <= angle <= b
    return a <= angle or angle <= b

def find_angle(x1, y1, x2, y2):
    """
    Finds the angle between two points (in radians).
    """
    angle = (360 + int(np.arctan2(y1 - y2, x2 - x1) * 180 / np.pi) % 360) % 360
    return angle * np.pi / 180

def distance_between(x1, y1, x2, y2):
    """
    Calculates the distance between 2 points.
    """
    return np.hypot(x1 - x2, y1 - y2)
Download .txt
gitextract_rb342yw4/

├── LICENSE
├── README.md
└── src/
    ├── main.py
    ├── neural_network.py
    ├── population.py
    ├── settings.py
    └── utility.py
Download .txt
SYMBOL INDEX (32 symbols across 4 files)

FILE: src/main.py
  function main (line 15) | def main():
  function update (line 133) | def update(dt, pop, key_pressed):
  function render (line 160) | def render(window, FONT, pop):

FILE: src/neural_network.py
  class NNetwork (line 9) | class NNetwork:
    method __init__ (line 15) | def __init__(self, layer_sizes, activation_funcs, bias_neuron = False):
    method feed_forward (line 47) | def feed_forward(self, data, one_hot_encoding = True):
    method output (line 71) | def output(self):
  class Layer (line 77) | class Layer:
    method __init__ (line 83) | def __init__(self, num_neurons, activation_func, bias_neuron = False):
    method activate (line 99) | def activate(self):
  class Connection (line 106) | class Connection:
    method __init__ (line 111) | def __init__(self, layer_from, layer_to):
  function sigmoid (line 124) | def sigmoid(data):
  function softmax (line 131) | def softmax(data):

FILE: src/population.py
  class Population (line 11) | class Population:
    method __init__ (line 16) | def __init__(self, size, mutation_rate):
    method eliminate (line 37) | def eliminate(self, bot, replace = False):
    method feed (line 44) | def feed(self, bot, food):
    method update (line 80) | def update(self, dt):
  class Bot (line 124) | class Bot:
    method __init__ (line 142) | def __init__(self, nnet, rgb, population):
    method _move_forward (line 151) | def _move_forward(self, dt):
    method _turn_left (line 158) | def _turn_left(self, dt):
    method _turn_right (line 163) | def _turn_right(self, dt):
    method update (line 168) | def update(self, dt, sensory_input):
  class Food (line 185) | class Food:
    method __init__ (line 194) | def __init__(self, population):
    method update (line 205) | def update(self, dt):

FILE: src/utility.py
  function seq_is_equal (line 7) | def seq_is_equal(a, b):
  function angle_is_between (line 17) | def angle_is_between(angle, a, b):
  function find_angle (line 33) | def find_angle(x1, y1, x2, y2):
  function distance_between (line 40) | def distance_between(x1, y1, x2, y2):
Condensed preview — 7 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (26K chars).
[
  {
    "path": "LICENSE",
    "chars": 1072,
    "preview": "MIT License\n\nCopyright (c) 2017 Michael J Welsh\n\nPermission is hereby granted, free of charge, to any person obtaining a"
  },
  {
    "path": "README.md",
    "chars": 977,
    "preview": "# Bot Evolution\nBot evolution is an interesting display of evolution through neural networks and a genetic algorithm. Bo"
  },
  {
    "path": "src/main.py",
    "chars": 7500,
    "preview": "\"\"\"\nBot Evolution v1.0.0\n\"\"\"\n\nimport os\nimport sys\nimport pickle\nimport pygame as pg\nfrom pygame.locals import *\nimport "
  },
  {
    "path": "src/neural_network.py",
    "chars": 5068,
    "preview": "\"\"\"\nThis module implements the neural network class and all components necessary to\nmodularize the build/use process of "
  },
  {
    "path": "src/population.py",
    "chars": 8881,
    "preview": "\"\"\"\nThis modules implements the bulk of Bot Evolution.\n\"\"\"\n\nimport numpy as np\nimport copy\nimport settings\nfrom utility "
  },
  {
    "path": "src/settings.py",
    "chars": 143,
    "preview": "\"\"\"\nThis module contains the general settings used across modules.\n\"\"\"\n\nFPS = 60\nWINDOW_WIDTH = 1100\nWINDOW_HEIGHT = 600"
  },
  {
    "path": "src/utility.py",
    "chars": 1136,
    "preview": "\"\"\"\nThis module contains general-use functions.\n\"\"\"\n\nimport numpy as np\n\ndef seq_is_equal(a, b):\n    \"\"\"\n    Helper func"
  }
]

About this extraction

This page contains the full source code of the MichaelJWelsh/bot-evolution GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 7 files (24.2 KB), approximately 6.3k tokens, and a symbol index with 32 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!