[
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2017 Michael J Welsh\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Bot Evolution\nBot 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.\n\n\n![](https://github.com/MichaelJWelsh/bot-evolution/blob/master/example.gif)\n\n\n## Usage\nSimply go into the source folder and type this in terminal:\n```python\npython3.5 main.py\n```\n\n\n## Dependencies\n - numpy\n - pygame\n"
  },
  {
    "path": "src/main.py",
    "content": "\"\"\"\nBot Evolution v1.0.0\n\"\"\"\n\nimport os\nimport sys\nimport pickle\nimport pygame as pg\nfrom pygame.locals import *\nimport numpy as np\nimport datetime\nimport settings\nimport population\n\ndef main():\n    np.random.seed()\n    pg.init()\n\n    # Initialize runtime variables.\n    periodically_save = False\n    pop = None\n    if os.path.isfile(\"save.txt\") and input(\"Save file detected! Use it? (y/n): \").lower() == 'y':\n        settings.FPS, settings.WINDOW_WIDTH, settings.WINDOW_HEIGHT, settings.TIME_MULTIPLIER, pop = pickle.load(open(\"save.txt\", \"rb\"))\n    else:\n        pop_size = 0\n        mutation_rate = 0\n        while True:\n            pop_size = int(input(\"Population size: \"))\n            if pop_size < 5:\n                print(\"Population size must be at least 5!\")\n            else:\n                break\n        while True:\n            mutation_rate = float(input(\"Mutation rate: \"))\n            if mutation_rate <= 0 or mutation_rate >= 1:\n                print(\"Mutation rate must be in the range (0, 1)!\")\n            else:\n                break\n        while True:\n            settings.TIME_MULTIPLIER = float(input(\"Time multiplier: \"))\n            if settings.TIME_MULTIPLIER < 1:\n                print(\"Time multiplier must be at least 1!\")\n            else:\n                break\n        if input(\"Advance options? (y/n): \").lower() == 'y':\n            while True:\n                settings.FPS = int(input(\"Frames per second: \"))\n                if settings.FPS < 1:\n                    print(\"FPS must be at least 1!\")\n                else:\n                    break\n            while True:\n                settings.WINDOW_WIDTH = int(input(\"Window width: \"))\n                if settings.WINDOW_WIDTH < 50:\n                    print(\"Window width must be at least 50!\")\n                else:\n                    break\n            while True:\n                settings.WINDOW_HEIGHT = int(input(\"Window height: \"))\n                if settings.WINDOW_HEIGHT < 50:\n                    print(\"Window height must be at least 50!\")\n                else:\n                    break\n        pop = population.Population(pop_size, mutation_rate)\n    if input(\"Periodically save every half hour? (y/n): \").lower() == 'y':\n        periodically_save = True\n    print(\"\\nNote: \")\n    print(\"\\tPress 'r' to reset the population.\")\n    print(\"\\tPress 'p' to pause / unpause.\")\n    print(\"\\tPress 's' to save population's data (for use next time).\")\n    print(\"\\tPress 'up' / 'down' to change the populations mutation rate.\")\n    print(\"\\tPress 'left' / 'right' to change the time multiplier.\")\n    print(\"\\tClick on the screen to lay down food.\")\n\n    # Core variables.\n    FONT_SIZE = 30\n    FONT = pg.font.SysFont(\"Arial\", FONT_SIZE)\n    fps_clock = pg.time.Clock()\n    window = pg.display.set_mode((settings.WINDOW_WIDTH, settings.WINDOW_HEIGHT))\n    pg.display.set_caption(\"Bot Evolution\")\n\n    # Main loop.\n    dt = 0.0\n    fps_clock.tick(int(settings.FPS / (settings.TIME_MULTIPLIER / 5.0 + 1)))\n    paused = False\n    while True:\n        key_pressed = {\"up\": False, \"down\": False, \"left\": False, \"right\": False}\n        for event in pg.event.get():\n            if event.type == QUIT:\n                pg.quit()\n                sys.exit()\n            elif event.type == pg.KEYDOWN:\n                if event.key == pg.K_r:\n                    pop = population.Population(pop.SIZE, pop.mutation_rate)\n                if event.key == pg.K_s:\n                    pickle.dump([settings.FPS, settings.WINDOW_WIDTH, settings.WINDOW_HEIGHT, settings.TIME_MULTIPLIER, pop], open(\"save.txt\", \"wb\"))\n                if event.key == pg.K_p:\n                    paused = not paused\n            elif event.type == pg.MOUSEBUTTONUP:\n                pos = pg.mouse.get_pos()\n                pop.food.pop()\n                food = population.Food(pop)\n                food.x = pos[0]\n                food.y = pos[1]\n                pop.food.append(food)\n        if paused:\n            dt = fps_clock.tick(int(settings.FPS / (settings.TIME_MULTIPLIER / 5.0 + 1))) / 1000.0 * int(settings.FPS / (settings.TIME_MULTIPLIER / 5.0 + 1))\n            continue\n        if periodically_save and datetime.datetime.now().minute % 30 == 0:\n            pickle.dump([settings.FPS, settings.WINDOW_WIDTH, settings.WINDOW_HEIGHT, settings.TIME_MULTIPLIER, pop], open(\"save.txt\", \"wb\"))\n        keys = pg.key.get_pressed()\n        if keys[pg.K_UP]:\n            key_pressed[\"up\"] = True\n        if keys[pg.K_DOWN]:\n            key_pressed[\"down\"] = True\n        if key_pressed[\"up\"] and key_pressed[\"down\"]:\n            key_pressed[\"up\"] = False\n            key_pressed[\"down\"] = False\n        if keys[pg.K_LEFT]:\n            key_pressed[\"left\"] = True\n        if keys[pg.K_RIGHT]:\n            key_pressed[\"right\"] = True\n        if key_pressed[\"left\"] and key_pressed[\"right\"]:\n            key_pressed[\"left\"] = False\n            key_pressed[\"right\"] = False\n        update(dt, pop, key_pressed)\n        window.fill((0, 0, 0))\n        render(window, FONT, pop)\n        pg.display.update()\n        dt = fps_clock.tick(int(settings.FPS / (settings.TIME_MULTIPLIER / 5.0 + 1))) / 1000.0 * int(settings.FPS / (settings.TIME_MULTIPLIER / 5.0 + 1))\n\ndisplay_time_remaining = 0.0\ndef update(dt, pop, key_pressed):\n    global display_time_remaining\n    if key_pressed[\"up\"] or key_pressed[\"down\"] or key_pressed[\"left\"] or key_pressed[\"right\"]:\n        display_time_remaining = 3.0\n\n        if key_pressed[\"up\"]:\n            pop.mutation_rate += 0.001\n        elif key_pressed[\"down\"]:\n            pop.mutation_rate -= 0.001\n        if pop.mutation_rate <= 0:\n            pop.mutation_rate = 0.001\n        elif pop.mutation_rate >= 1:\n            pop.mutation_rate = 0.999\n\n        if key_pressed[\"left\"]:\n            settings.TIME_MULTIPLIER -= 0.1\n        elif key_pressed[\"right\"]:\n            settings.TIME_MULTIPLIER += 0.1\n        if settings.TIME_MULTIPLIER < 1:\n            settings.TIME_MULTIPLIER = 1.0\n    else:\n        display_time_remaining -= 1.0 / settings.FPS * dt\n        if display_time_remaining < 0:\n            display_time_remaining = 0.0\n\n    pop.update(dt)\n\ndef render(window, FONT, pop):\n    for food in pop.food:\n        pg.draw.circle(window, food.RGB, (int(food.x), int(food.y)), food.HITBOX_RADIUS)\n\n    for bot in pop.bots:\n        # Draw body.\n        pg.draw.circle(window, bot.RGB, (int(bot.x), int(bot.y)), bot.HITBOX_RADIUS)\n\n        # Draw field-of-vision lines.\n        LINE_THICKNESS = 1\n        PROTRUSION = int(bot.HITBOX_RADIUS * 1.5)\n        to_x = int(bot.x + (bot.HITBOX_RADIUS + PROTRUSION) * np.cos(bot.theta - bot.FIELD_OF_VISION_THETA / 2))\n        to_y = int(bot.y - (bot.HITBOX_RADIUS + PROTRUSION) * np.sin(bot.theta - bot.FIELD_OF_VISION_THETA / 2))\n        pg.draw.line(window, bot.RGB, (bot.x, bot.y), (to_x, to_y), LINE_THICKNESS)\n        to_x = int(bot.x + (bot.HITBOX_RADIUS + PROTRUSION) * np.cos(bot.theta + bot.FIELD_OF_VISION_THETA / 2))\n        to_y = int(bot.y - (bot.HITBOX_RADIUS + PROTRUSION) * np.sin(bot.theta + bot.FIELD_OF_VISION_THETA / 2))\n        pg.draw.line(window, bot.RGB, (bot.x, bot.y), (to_x, to_y), LINE_THICKNESS)\n\n    if display_time_remaining > 0:\n        resultSurf = FONT.render(\"Mutation Rate: %.3f       Speed: %.1fx\" % (pop.mutation_rate, settings.TIME_MULTIPLIER), True, (255, 255, 255))\n        resultRect = resultSurf.get_rect()\n        resultRect.topleft = (25, 25)\n        window.blit(resultSurf, resultRect)\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "src/neural_network.py",
    "content": "\"\"\"\nThis module implements the neural network class and all components necessary to\nmodularize the build/use process of the neural network.\n\"\"\"\n\nimport numpy as np\nimport pickle\n\nclass NNetwork:\n    \"\"\"\n    The representation of a feed forward neural network with a bias in every\n    layer (excluding output layer obviously).\n    \"\"\"\n\n    def __init__(self, layer_sizes, activation_funcs, bias_neuron = False):\n        \"\"\"\n        Creates a 'NNetwork'. 'layer_sizes' provides information about the\n        number of neurons in each layer, as well as the total number of layers\n        in the neural network. 'activation_funcs' provides information about the\n        activation functions to use on each respective hidden layers and output\n        layer. This means that the length of 'activation_funcs' is always one\n        less than the length of 'layer_sizes'.\n        \"\"\"\n        assert(len(layer_sizes) >= 2)\n        assert(len(layer_sizes) - 1 == len(activation_funcs))\n        assert(min(layer_sizes) >= 1)\n        self.layers = []\n        self.connections = []\n\n        # Initialize layers.\n        for i in range(len(layer_sizes)):\n            # Input layer.\n            if i == 0:\n                self.layers.append(Layer(layer_sizes[i], None, bias_neuron))\n            # Hidden layer.\n            elif i < len(layer_sizes) - 1:\n                self.layers.append(Layer(layer_sizes[i], activation_funcs[i - 1], bias_neuron))\n            # Output layer.\n            else:\n                self.layers.append(Layer(layer_sizes[i], activation_funcs[i - 1]))\n\n        # Initialize connections.\n        num_connections = len(layer_sizes) - 1\n        for i in range(num_connections):\n            self.connections.append(Connection(self.layers[i], self.layers[i + 1]))\n\n    def feed_forward(self, data, one_hot_encoding = True):\n        \"\"\"\n        Feeds given data through neural network and stores output in output\n        layer's data field. Output can optionally be one-hot encoded.\n        \"\"\"\n        if self.layers[0].HAS_BIAS_NEURON:\n            assert(len(data) == self.layers[0].SIZE - 1)\n            self.layers[0].data = data\n            self.layers[0].data.append(1)\n        else:\n            assert(len(data) == self.layers[0].SIZE)\n            self.layers[0].data = data\n        for i in range(len(self.connections)):\n            self.connections[i].TO.data = np.dot(self.layers[i].data, self.connections[i].weights)\n            self.connections[i].TO.activate()\n        if one_hot_encoding:\n            this_data = self.layers[len(self.layers) - 1].data\n            MAX = max(this_data)\n            for i in range(len(this_data)):\n                if this_data[i] == MAX:\n                    this_data[i] = 1\n                else:\n                    this_data[i] = 0\n\n    def output(self):\n        \"\"\"\n        Retrieves data in output layer.\n        \"\"\"\n        return self.layers[len(self.layers) - 1].data\n\nclass Layer:\n    \"\"\"\n    The representation of a layer in a neural network. Used as a medium for\n    passing data through the network in an efficent manner.\n    \"\"\"\n\n    def __init__(self, num_neurons, activation_func, bias_neuron = False):\n        \"\"\"\n        Creates a 'Layer' with 'num_neurons' and an additional (optional) bias\n        neuron (which always has a value of '1'). The layer will utilize the\n        'activation_func' during activation.\n        \"\"\"\n        assert(num_neurons > 0)\n        self.ACTIVATION_FUNC = activation_func\n        self.HAS_BIAS_NEURON = bias_neuron\n        if bias_neuron:\n            self.SIZE = num_neurons + 1\n            self.data = np.array([0] * num_neurons + [1])\n        else:\n            self.SIZE = num_neurons\n            self.data = np.array([0] * num_neurons)\n\n    def activate(self):\n        \"\"\"\n        Calls activation function on layer's data.\n        \"\"\"\n        if self.ACTIVATION_FUNC != None:\n            self.ACTIVATION_FUNC(self.data)\n\nclass Connection:\n    \"\"\"\n    The representation of a connection between layers in a neural network.\n    \"\"\"\n\n    def __init__(self, layer_from, layer_to):\n        \"\"\"\"\n        Creates a 'Connection' between 'layer_from' and 'layer_to' that contains\n        all required weights, which are randomly initialized with random numbers\n        in a guassian distribution of mean '0' and standard deviation '1'.\n        \"\"\"\n        self.FROM = layer_from\n        self.TO = layer_to\n        self.weights = np.zeros((layer_from.SIZE, layer_to.SIZE))\n        for i in range(layer_from.SIZE):\n            for j in range(layer_to.SIZE):\n                self.weights[i][j] = np.random.standard_normal()\n\ndef sigmoid(data):\n    \"\"\"\n    Uses sigmoid transformation on given data. This is an activation function.\n    \"\"\"\n    for i in range(len(data)):\n        data[i] = 1 / (1 + np.exp(-data[i]))\n\ndef softmax(data):\n    \"\"\"\n    Uses softmax transformation on given data. This is an activation function.\n    \"\"\"\n    sum = 0.0\n    for i in range(len(data)):\n        sum += np.exp(data[i])\n    for i in range(len(data)):\n        data[i] = np.exp(data[i]) / sum\n"
  },
  {
    "path": "src/population.py",
    "content": "\"\"\"\nThis modules implements the bulk of Bot Evolution.\n\"\"\"\n\nimport numpy as np\nimport copy\nimport settings\nfrom utility import seq_is_equal, distance_between, angle_is_between, find_angle\nfrom neural_network import NNetwork, sigmoid, softmax\n\nclass Population:\n    \"\"\"\n    The environment of bots and food.\n    \"\"\"\n\n    def __init__(self, size, mutation_rate):\n        assert(size >= 5)\n        assert(0 < mutation_rate < 1)\n        self.SIZE = size\n        self.mutation_rate = mutation_rate\n        self.bots = []\n        self.food = []\n        self.time_since_last_death = 0.0\n\n        # The neural network will have 1 neuron in the input layer, 1 hidden\n        # layer with 2 neurons, and 4 neurons in the output layer. The sigmoid\n        # activation function will be used on the hidden layer, and a softmax\n        # activation function will be used on the output layer. Input consists\n        # of the bot's direction and if there is or isn't food in the bots field\n        # of vision. Output consists of whether or not to move foward, turn\n        # left, turn right, or do nothing.\n        for i in range(size):\n            random_rgb = (np.random.randint(30, 256), np.random.randint(30, 256), np.random.randint(30, 256))\n            self.bots.append(Bot(NNetwork((1, 2, 4), (sigmoid, softmax)), random_rgb, self))\n        self.food.append(Food(self))\n\n    def eliminate(self, bot, replace = False):\n        self.time_since_last_death = 0.0\n        self.bots.remove(bot)\n        if replace:\n            random_rgb = (np.random.randint(30, 256), np.random.randint(30, 256), np.random.randint(30, 256))\n            self.bots.append(Bot(NNetwork((1, 2, 4), (sigmoid, softmax)), random_rgb, self))\n\n    def feed(self, bot, food):\n        bot.score = 1.0\n        self.food.remove(food)\n        self.food.append(Food(self))\n        num_to_replace = int(self.SIZE / 7 - 1)\n        if num_to_replace < 2:\n            num_to_replace = 2\n        for i in range(num_to_replace):\n            weakest = self.bots[0]\n            for other in self.bots:\n                if other.score < weakest.score:\n                    weakest = other\n            self.eliminate(weakest)\n        for i in range(num_to_replace):\n            if np.random.uniform(0, 1) <= self.mutation_rate:\n                new_rgb = [bot.RGB[0], bot.RGB[1], bot.RGB[2]]\n                new_rgb[np.random.choice((0, 1, 2))] = np.random.uniform(30, 256)\n                new_bot = Bot(bot.nnet, new_rgb, self)\n                new_bot.x = bot.x + Bot.HITBOX_RADIUS * 4 * np.random.uniform(0, 1) * np.random.choice((-1, 1))\n                new_bot.y = bot.y + Bot.HITBOX_RADIUS * 4 * np.random.uniform(0, 1) * np.random.choice((-1, 1))\n                nb_c = new_bot.nnet.connections\n                mutated = False\n                while not mutated:\n                    for k in range(len(nb_c)):\n                        for i in range(nb_c[k].FROM.SIZE):\n                            for j in range(nb_c[k].TO.SIZE):\n                                if np.random.uniform(0, 1) <= self.mutation_rate:\n                                    nb_c[k].weights[i][j] = nb_c[k].weights[i][j] * np.random.normal(1, 0.5) + np.random.standard_normal()\n                                    mutated = True\n                self.bots.append(new_bot)\n            else:\n                new_bot = Bot(bot.nnet, bot.RGB, self)\n                new_bot.x = bot.x + Bot.HITBOX_RADIUS * 4 * np.random.uniform(0, 1) * np.random.choice((-1, 1))\n                new_bot.y = bot.y + Bot.HITBOX_RADIUS * 4 * np.random.uniform(0, 1) * np.random.choice((-1, 1))\n                self.bots.append(new_bot)\n\n    def update(self, dt):\n        \"\"\"\n        Updates the population's internals. The bulk of event handling for all\n        bots and food starts here.\n        \"\"\"\n        self.time_since_last_death += 1.0 / settings.FPS * dt * settings.TIME_MULTIPLIER\n\n        for food in self.food[:]:\n            if food not in self.food:\n                continue\n            food.update(dt)\n\n        for bot in self.bots[:]:\n            if bot not in self.bots:\n                continue\n\n            sensory_input = []\n\n            # This is where the bot's field of vision is put into action.\n            min_theta = bot.theta - Bot.FIELD_OF_VISION_THETA / 2\n            max_theta = bot.theta + Bot.FIELD_OF_VISION_THETA / 2\n            food_in_sight = False\n            for food in self.food:\n                if angle_is_between(find_angle(bot.x, bot.y, food.x, food.y), min_theta, max_theta):\n                    food_in_sight = True\n                    break\n            if food_in_sight:\n                sensory_input.append(1.0)\n            else:\n                sensory_input.append(0.0)\n\n            # Useful debugging outputs.\n            #print(bot.RGB)\n            #print(sensory_input)\n\n            bot.update(dt, sensory_input)\n\n        if self.time_since_last_death >= 5:\n            weakest = self.bots[0]\n            for bot in self.bots:\n                if bot.score < weakest.score:\n                    weakest = bot\n            self.eliminate(weakest, replace = True)\n\nclass Bot:\n    \"\"\"\n    The representation of the circle thing with probes.\n    \"\"\"\n\n    # In pixels/pixels per second/revolutions per second/radians.\n    SPAWN_RADIUS = int(settings.WINDOW_WIDTH / 20) if settings.WINDOW_WIDTH <= settings.WINDOW_HEIGHT else int(settings.WINDOW_HEIGHT / 20)\n    HITBOX_RADIUS = 6\n    SPEED = 350.0\n    TURN_RATE = 2 * np.pi\n    FIELD_OF_VISION_THETA = 45 * np.pi / 180\n\n    # These lists represent the output from the neural network. Note that the\n    # output '[0, 0, 0, 1]' means \"do nothing\".\n    MOVE_FORWARD =  [1, 0, 0, 0]\n    TURN_LEFT =     [0, 1, 0, 0]\n    TURN_RIGHT =    [0, 0, 1, 0]\n\n    def __init__(self, nnet, rgb, population):\n        self.nnet = copy.deepcopy(nnet)\n        self.RGB = rgb\n        self.pop = population\n        self.theta = np.random.uniform(0, 1) * 2 * np.pi\n        self.x = settings.WINDOW_WIDTH / 2.0 + Bot.SPAWN_RADIUS * np.random.uniform(0, 1) * np.cos(self.theta)\n        self.y = settings.WINDOW_HEIGHT / 2.0 + Bot.SPAWN_RADIUS * np.random.uniform(0, 1) * np.sin(self.theta)\n        self.score = 0.0\n\n    def _move_forward(self, dt):\n        self.x += Bot.SPEED / settings.FPS * dt * np.cos(self.theta) * settings.TIME_MULTIPLIER\n        self.y -= Bot.SPEED / settings.FPS * dt * np.sin(self.theta) * settings.TIME_MULTIPLIER\n        if self.x < -Bot.HITBOX_RADIUS * 6 or self.x > settings.WINDOW_WIDTH + Bot.HITBOX_RADIUS * 6 \\\n        or self.y < -Bot.HITBOX_RADIUS * 6 or self.y > settings.WINDOW_HEIGHT + Bot.HITBOX_RADIUS * 6:\n            self.pop.eliminate(self, replace = True)\n\n    def _turn_left(self, dt):\n        self.theta += Bot.TURN_RATE / settings.FPS * dt * settings.TIME_MULTIPLIER\n        while self.theta >= 2 * np.pi:\n            self.theta -= 2 * np.pi\n\n    def _turn_right(self, dt):\n        self.theta -= Bot.TURN_RATE / settings.FPS * dt * settings.TIME_MULTIPLIER\n        while self.theta < 0:\n            self.theta += 2 * np.pi\n\n    def update(self, dt, sensory_input):\n        \"\"\"\n        Updates the bot's internals. \"Hunger\" can be thought of as a score\n        between '-1' and '1' where a greater value means less hungry.\n        \"\"\"\n        self.score -= 1.0 / settings.FPS / 10.0 * dt * settings.TIME_MULTIPLIER\n        if self.score < -1:\n            self.score = -1.0\n        self.nnet.feed_forward(sensory_input)\n        output = self.nnet.output()\n        if seq_is_equal(output, Bot.MOVE_FORWARD):\n            self._move_forward(dt)\n        elif seq_is_equal(output, Bot.TURN_LEFT):\n            self._turn_left(dt)\n        elif seq_is_equal(output, Bot.TURN_RIGHT):\n            self._turn_right(dt)\n\nclass Food:\n    \"\"\"\n    The representation of the red circles.\n    \"\"\"\n\n    # In pixels.\n    HITBOX_RADIUS = 5\n    RGB = (255, 0, 0)\n\n    def __init__(self, population):\n        mid_x = int(settings.WINDOW_WIDTH / 2)\n        mid_y = int(settings.WINDOW_HEIGHT / 2)\n        max_left_x = mid_x - (Bot.SPAWN_RADIUS + Bot.HITBOX_RADIUS + 5)\n        min_right_x = mid_x + (Bot.SPAWN_RADIUS + Bot.HITBOX_RADIUS + 5)\n        max_top_y = mid_y - (Bot.SPAWN_RADIUS + Bot.HITBOX_RADIUS + 5)\n        min_bottom_y = mid_y + (Bot.SPAWN_RADIUS + Bot.HITBOX_RADIUS + 5)\n        self.x = np.random.choice((np.random.uniform(0, max_left_x), np.random.uniform(min_right_x, settings.WINDOW_WIDTH)))\n        self.y = np.random.choice((np.random.uniform(0, max_top_y), np.random.uniform(min_bottom_y, settings.WINDOW_HEIGHT)))\n        self.pop = population\n\n    def update(self, dt):\n        \"\"\"\n        Updates the food's internals and handles bot<->food collision.\n        \"\"\"\n        for bot in self.pop.bots:\n            if distance_between(self.x, self.y, bot.x, bot.y) <= Bot.HITBOX_RADIUS + Food.HITBOX_RADIUS:\n                self.pop.feed(bot, self)\n                break\n"
  },
  {
    "path": "src/settings.py",
    "content": "\"\"\"\nThis module contains the general settings used across modules.\n\"\"\"\n\nFPS = 60\nWINDOW_WIDTH = 1100\nWINDOW_HEIGHT = 600\nTIME_MULTIPLIER = 1.0\n"
  },
  {
    "path": "src/utility.py",
    "content": "\"\"\"\nThis module contains general-use functions.\n\"\"\"\n\nimport numpy as np\n\ndef seq_is_equal(a, b):\n    \"\"\"\n    Helper function that checks if two sequences are equal (assuming they have\n    the same length).\n    \"\"\"\n    for i in range(len(a)):\n        if a[i] != b[i]:\n            return False\n    return True\n\ndef angle_is_between(angle, a, b):\n    \"\"\"\n    Takes in 3 angles (in radians) and returns 'a' <= 'angle' <= 'b'.\n    \"\"\"\n    to_degrees = lambda rads: int(rads * 180 / np.pi)\n    reduce_degrees = lambda degr: (360 + degr % 360) % 360\n    angle = to_degrees(angle)\n    a = to_degrees(a)\n    b = to_degrees(b)\n    angle = reduce_degrees(angle)\n    a = reduce_degrees(a)\n    b = reduce_degrees(b)\n    if a < b:\n        return a <= angle <= b\n    return a <= angle or angle <= b\n\ndef find_angle(x1, y1, x2, y2):\n    \"\"\"\n    Finds the angle between two points (in radians).\n    \"\"\"\n    angle = (360 + int(np.arctan2(y1 - y2, x2 - x1) * 180 / np.pi) % 360) % 360\n    return angle * np.pi / 180\n\ndef distance_between(x1, y1, x2, y2):\n    \"\"\"\n    Calculates the distance between 2 points.\n    \"\"\"\n    return np.hypot(x1 - x2, y1 - y2)\n"
  }
]