├── __init__.py ├── docs ├── images │ ├── .gitkeep │ └── crawlai.webp ├── manuals │ ├── .gitkeep │ ├── design_patterns_guide.md │ ├── commit_conventions.md │ ├── git_commands.md │ └── developer_setup.md └── Architecture │ ├── user_stories.md │ ├── cots.md │ ├── quality_attribute_scenarios.md │ ├── atam.md │ ├── test_report.md │ ├── diagrams │ └── high_level_logic_view.puml │ ├── stakeholders.md │ ├── architectural_design.md │ └── requirements.md ├── src ├── audio │ └── __init__.py ├── agent_parts │ ├── __init__.py │ ├── motorjoint.py │ ├── vision.py │ ├── limb.py │ ├── creature_test.py │ ├── creature.py │ └── rectangle.py ├── textures │ └── __init__.py ├── program_states │ ├── __init__.py │ ├── evolve_state.py │ ├── insights_state.py │ ├── fitness_state.py │ ├── state.py │ └── game_state_manager.py ├── __init__.py ├── render_object.py ├── globals.py ├── species.py ├── agent.py ├── environment.py ├── NEATnetwork.py ├── interface.py ├── genetic_algorithm.py ├── runner_display.py ├── genome.py └── ground.py ├── tests ├── __init__.py ├── test_enviroment.py ├── test_NEATnetwork.py └── test_nn_topological.py ├── requirements.txt ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── pymunkJointTest.py ├── .gitignore ├── README.md ├── agent_parts_main.py ├── models ├── best_genome2961_555819068663.json ├── best_genome3159_670865969072.json ├── best_genome2346_7973381259126.json └── best_genome5080_920825409008.json └── main.py /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/images/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/manuals/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/audio/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/agent_parts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/textures/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/program_states/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/Architecture/user_stories.md: -------------------------------------------------------------------------------- 1 | # User Stories 2 | -------------------------------------------------------------------------------- /docs/Architecture/cots.md: -------------------------------------------------------------------------------- 1 | # Commercial off-the-shelf (COTS) 2 | 3 | ## Database Solution 4 | -------------------------------------------------------------------------------- /docs/images/crawlai.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CogitoNTNU/CrawlAI/HEAD/docs/images/crawlai.webp -------------------------------------------------------------------------------- /docs/Architecture/quality_attribute_scenarios.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | ## Quality Attribute Scenarios 4 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | # """Init file for src folder""" 2 | 3 | # from . import agent_parts #Enviroment, RenderObject, 4 | 5 | # __all__ = ["agent_parts"] #"Enviroment", "RenderObject", -------------------------------------------------------------------------------- /src/program_states/evolve_state.py: -------------------------------------------------------------------------------- 1 | from state import State 2 | 3 | class EvolveState(State): 4 | def __init__(self): 5 | pass 6 | 7 | def changeState(self): 8 | pass -------------------------------------------------------------------------------- /src/program_states/insights_state.py: -------------------------------------------------------------------------------- 1 | from state import State 2 | 3 | 4 | class InsightsState(State): 5 | def __init__(self): 6 | pass 7 | 8 | def changeState(self): 9 | pass -------------------------------------------------------------------------------- /src/program_states/fitness_state.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from state import State 4 | 5 | 6 | class FitnessState(State): 7 | def __init__(self): 8 | pass 9 | 10 | def changeState(self): 11 | pass 12 | 13 | -------------------------------------------------------------------------------- /tests/test_enviroment.py: -------------------------------------------------------------------------------- 1 | from src.environment import Environment 2 | 3 | def test_create_enviroment(): 4 | env = Environment(state="Env") 5 | assert env is not None 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/program_states/state.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from GameStateManager import GameStateManager 3 | 4 | 5 | class State(ABC): 6 | gamestate_manager: GameStateManager 7 | 8 | @abstractmethod 9 | def changeState(self): 10 | pass -------------------------------------------------------------------------------- /docs/manuals/design_patterns_guide.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | In order to maintain a good architecture it is important that everyone is consequently using the architectural paterns and design patterns that are described below: 5 | 6 | https://refactoring.guru/design-patterns 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cffi==1.17.1 2 | contourpy==1.3.0 3 | cycler==0.12.1 4 | fonttools==4.54.1 5 | kiwisolver==1.4.7 6 | matplotlib==3.9.2 7 | numpy==2.1.3 8 | packaging==24.2 9 | pillow==11.0.0 10 | pycparser==2.22 11 | pygame==2.6.1 12 | pymunk==6.9.0 13 | pyparsing==3.2.0 14 | python-dateutil==2.9.0.post0 15 | six==1.16.0 16 | -------------------------------------------------------------------------------- /src/render_object.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | 3 | 4 | class RenderObject: 5 | @abstractmethod 6 | def render(self): 7 | pass 8 | 9 | @abstractmethod 10 | def get_position(self): 11 | pass 12 | 13 | @abstractmethod 14 | def set_position(self): 15 | pass 16 | 17 | def set_relative_position(self): 18 | pass 19 | 20 | def get_relative_position(self): 21 | pass 22 | -------------------------------------------------------------------------------- /docs/Architecture/atam.md: -------------------------------------------------------------------------------- 1 | # Architecutre Tradeoff Analysis Method (ATAM) 2 | 3 | The goal of this report is to discover possible risks of the architectural decisions of CrawlAI 4 | 5 | ## Attribute Utility Tree 6 | 7 | ## Analyzing Architectural Approach 8 | 9 | ## Sensitivity Points 10 | 11 | ## Tradeoff Points 12 | 13 | ## Risks and Non-Risks 14 | 15 | This report defines risks as potentially problematic architectural decisions that can lead to problems later in development and non-risks as good decisions. 16 | 17 | ## Questionnaire 18 | 19 | ## Problems and Issues 20 | 21 | ## Possible Solutions 22 | -------------------------------------------------------------------------------- /src/globals.py: -------------------------------------------------------------------------------- 1 | BLACK = (0, 0, 0) 2 | BLUE = (0, 0, 255) 3 | RED = (255, 0, 0) 4 | YELLOW = (255, 255, 0) 5 | WHITE = (255, 255, 255) 6 | 7 | PERLIN_SEGMENTS = 40 8 | 9 | FONT_SIZE = 14 10 | SCREEN_WIDTH = 900 11 | SCREEN_HEIGHT = 600 12 | FLOOR_HEIGHT = 100 13 | 14 | 15 | AMPLITUDE = 20 # Max height of hills 16 | FREQUENCY = 0.01 # Max frequency of hills 17 | SEGMENT_WIDTH = 600 # Width of each terrain segment 18 | 19 | 20 | ## Hyperparameters 21 | MUTATION_RATE_WEIGHT = 0.2 22 | MUTATION_RATE_CONNECTION = 0.05 23 | MUTATION_RATE_NODE = 0.03 24 | 25 | POPULATION_SIZE = 100 26 | NUM_GENERATIONS = 20 27 | SPECIATION_THRESHOLD = 3.0 28 | 29 | SIMULATION_STEPS = 400 30 | -------------------------------------------------------------------------------- /src/species.py: -------------------------------------------------------------------------------- 1 | import random 2 | from typing import List 3 | from src.genome import Genome 4 | 5 | 6 | class Species: 7 | def __init__(self, species_id: int): 8 | self.species_id = species_id 9 | self.members: List[Genome] = [] 10 | self.average_fitness: float = 0.0 11 | 12 | def add_member(self, genome: Genome): 13 | self.members.append(genome) 14 | 15 | def adjust_fitness(self): 16 | total_fitness = sum(genome.fitness for genome in self.members) 17 | for genome in self.members: 18 | genome.adjusted_fitness = genome.fitness / len(self.members) 19 | self.average_fitness = total_fitness / len(self.members) 20 | -------------------------------------------------------------------------------- /src/program_states/game_state_manager.py: -------------------------------------------------------------------------------- 1 | from state import State 2 | 3 | 4 | class GameStateManager: 5 | __instance = None 6 | currentState: State 7 | def __init__(self): 8 | """ Virtually private constructor. """ 9 | if GameStateManager.__instance != None: 10 | raise Exception("This class is a singleton!") 11 | else: 12 | GameStateManager.__instance = self 13 | 14 | 15 | def set_state(self, state: State): 16 | self.currentState = state 17 | 18 | def get_state(self): 19 | pass 20 | 21 | @staticmethod 22 | def get_instance(): 23 | """ Static access method. """ 24 | if GameStateManager.__instance == None: 25 | GameStateManager() 26 | return GameStateManager.__instance 27 | -------------------------------------------------------------------------------- /tests/test_NEATnetwork.py: -------------------------------------------------------------------------------- 1 | from ..src.genome import Node, Connection, Genome 2 | from src.NEATnetwork import NEATNetwork 3 | 4 | 5 | def test_NEAT_network(): 6 | 7 | node_1 = Node(1, "input") 8 | node_2 = Node(2, "input") 9 | node_3 = Node(3, "input") 10 | node_4 = Node(4, "hidden") 11 | node_5 = Node(5, "output") 12 | 13 | nodes_list = [node_1, node_2, node_3, node_4, node_5] 14 | 15 | con_1 = Connection(node_1, node_4, 0.7, True, 1) 16 | con_2 = Connection(node_2, node_4, 0.7, True, 2) 17 | con_3 = Connection(node_3, node_4, 0.5, True, 3) 18 | con_4 = Connection(node_2, node_5, 0.2, True, 4) 19 | con_5 = Connection(node_5, node_4, 0.4, True, 5) 20 | con_6 = Connection(node_1, node_5, 0.6, False, 6) 21 | con_7 = Connection(node_1, node_5, 0.6, True, 11) 22 | 23 | conns = [con_1, con_2, con_3, con_4, con_5, con_6, con_7] 24 | 25 | genome = Genome(id=1, nodes=nodes_list, connections=conns) 26 | 27 | assert NEATNetwork(genome=genome) == 1.52 28 | -------------------------------------------------------------------------------- /docs/Architecture/test_report.md: -------------------------------------------------------------------------------- 1 | # Test Report 2 | 3 | - This section should contain test reports for both functional- and quality 4 | requirements (quality scenarios) 5 | - The reports must include requirement ID (e.g. F1, F2, A2, A3…), description of 6 | requirement, who performed the test, when it was performed, duration of 7 | the test, evaluation (failure/success), and comment (discussion/comment 8 | about the result). 9 | - Quality requirement tests must additionally include stimuli, expected 10 | response measure, and observed response measure. 11 | 12 | ### Functional Requirement Test Report 13 | 14 | | ID | Requirement | Tester | Date | Duration | Evaluation | Comment | 15 | | --- | ----------- | ------ | ---- | -------- | ---------- | ------- | 16 | 17 | ### Quality Requirement Test Report 18 | 19 | | ID | Requirement | Tester | Date | Duration | Stimuli | Expected Response Measure | Observed Response Measure | Comment | 20 | | --- | ----------- | ------ | ---- | -------- | ------- | ------------------------- | ------------------------- | ------- | 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: CI 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | matrix: 20 | python-version: ["3.9", "3.10", "3.11"] 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v3 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | pip install flake8 pytest 31 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 32 | - name: Test with pytest 33 | run: | 34 | pytest 35 | -------------------------------------------------------------------------------- /docs/manuals/commit_conventions.md: -------------------------------------------------------------------------------- 1 | # Quick examples 2 | ----------------------------------------------- 3 | feat: new feature 4 | fix(scope): bug in scope 5 | feat!: breaking change / feat(scope)!: rework API 6 | chore(deps): update dependencies 7 | 8 | # Commit types 9 | ------------------------------------------------ 10 | build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm) 11 | 12 | ci: Changes to CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs) 13 | 14 | chore: Changes which doesn't change source code or tests e.g. changes to the build process, auxiliary tools, libraries 15 | 16 | docs: Documentation only changes 17 | 18 | feat: A new feature 19 | 20 | fix: A bug fix 21 | 22 | perf: A code change that improves performance 23 | 24 | refactor: A code change that neither fixes a bug nor adds a feature 25 | 26 | revert: Revert something 27 | 28 | style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 29 | 30 | test: Adding missing tests or correcting existing tests -------------------------------------------------------------------------------- /docs/manuals/git_commands.md: -------------------------------------------------------------------------------- 1 | **Q: What are some useful git commands?** 2 | 3 | - Cloning a file 4 | - “git clone link” — cloning a repository (copy link from shared file github - and open file in terminal you wanna clone to) 5 | - Pushing changes from one branch onto main branch (3 steps) 6 | - “git add . “ 7 | - “git commit -m “feat: message” “ (this is for adding a message to the changes for friends) 8 | - “git push” 9 | - Pulling changes from main into your own branch 10 | - “git pull” 11 | - Making a new branch 12 | - “git branch name” 13 | - Switching to the new branch or any 14 | - “git checkout name” 15 | - Making and switching in one go 16 | - “git checkout -b name” 17 | - Delete a branch 18 | - “git checkout -d name” 19 | - Checking status of pushing 20 | - “git status” 21 | - Listing all branches 22 | - “git branch -a” 23 | - Rename a branch 24 | - “git branch -m name” 25 | - Pushing a newly created branch onto github or another program 26 | - “git push -u origin name” 27 | - Merge branch name into current branch 28 | - “git merge branch-name” 29 | 30 | 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Cogito NTNU 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/test_nn_topological.py: -------------------------------------------------------------------------------- 1 | import tensorflow as tf 2 | from tensorflow import keras 3 | from tensorflow.keras.layers import Dense, Flatten, Conv2D 4 | from tensorflow.keras import Model 5 | 6 | from src.agent_parts.rectangle import Point 7 | from src.agent import Agent 8 | 9 | class MockAgent(Agent): 10 | def act(self, env) -> int: 11 | return 0 12 | 13 | 14 | def test_do_inference_with_correct_input(): 15 | 16 | agent = Agent() 17 | 18 | input_data = tf.random.uniform((1, input_size)) 19 | x = [alpha1, 20 | alpha2, 21 | viewPos1, 22 | viewPos2, 23 | limbPos, 24 | joint_angle, 25 | health 26 | ] 27 | model = None 28 | 29 | output = model.predict(x) 30 | assert output is not None 31 | 32 | 33 | 34 | def test_kill_neuron(): 35 | # Remember to test if the connection is put on the correct place. 36 | pass 37 | 38 | def test_add_neuron(): 39 | 40 | pass 41 | 42 | def test_connection(): 43 | 44 | pass 45 | 46 | def test_add_connection(): 47 | 48 | pass 49 | 50 | def test_remove_connection(): 51 | pass 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/agent_parts/motorjoint.py: -------------------------------------------------------------------------------- 1 | import pymunk 2 | import pygame 3 | import math 4 | 5 | 6 | class MotorJoint: 7 | def __init__(self, space, body_a, body_b, anchor_a, anchor_b, rate): 8 | """Initialize a motor joint between two limbs.""" 9 | self.pivot = pymunk.PivotJoint(body_a, body_b, anchor_a, anchor_b) 10 | self.motor = pymunk.SimpleMotor(body_a, body_b, rate) 11 | 12 | space.add(self.pivot, self.motor) 13 | 14 | def set_motor_rate(self, rate): 15 | """Set the rotation speed of the motor.""" 16 | self.motor.rate = rate 17 | 18 | def render(self, screen, body_a, body_b): 19 | """Render the motor joint as a small red circle between the two bodies.""" 20 | # Get the positions of the two bodies 21 | pos_a_world = body_a.local_to_world(self.pivot.anchor_a) 22 | # Draw a red circle connecting the two bodies 23 | pygame.draw.circle( 24 | surface=screen, 25 | color=(255, 0, 0), 26 | center=(float(pos_a_world.x), float(pos_a_world.y)), 27 | radius=3, 28 | ) 29 | 30 | def get_angle(self): 31 | """Get the angle of the motor joint.""" 32 | return self.pivot.angle 33 | -------------------------------------------------------------------------------- /docs/Architecture/diagrams/high_level_logic_view.puml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @startuml 5 | 6 | frame "Arrow definitions" { 7 | note "creates: One or more classes in A creates some instance of class in B\nuses: One or more classes in A uses some instance/implementation of class in B\nimplements: One or more classes in A implements some interface(s) in B" as definitionNote 8 | } 9 | 10 | 11 | 12 | frame "core"{ 13 | 14 | package "Agent" { 15 | 16 | } 17 | package "enviroment" { 18 | 19 | } 20 | 21 | package "evolution" { 22 | 23 | } 24 | 25 | package "genetic_algorithm" { 26 | 27 | } 28 | 29 | 30 | 31 | } 32 | 33 | package "agent_storage" { 34 | 35 | } 36 | 37 | package "physics" { 38 | 39 | } 40 | 41 | package "clock" { 42 | 43 | } 44 | 45 | package "pygame" { 46 | 47 | } 48 | 49 | agent --> enviroment 50 | evolution --> agent 51 | evolution --> enviroment 52 | evolution --> genetic_algorithm 53 | enviroment --> clock 54 | enviroment --> pygame 55 | enviroment --> physics 56 | agent_storage --> agent 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | @enduml 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 | -------------------------------------------------------------------------------- /src/agent.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | import pybox2d 3 | from enum import Enum 4 | import tensorflow as tf 5 | 6 | from src.genome import Genome 7 | from src.environment import Environment 8 | from src.render_object import RenderObject 9 | 10 | from src.agent_parts.limb import Limb, LimbType, limb_factory 11 | from src.agent_parts.creature import Creature, creature_factory 12 | 13 | 14 | class Agent(ABC): 15 | """ 16 | 17 | """ 18 | genome: Genome 19 | creature: Creature 20 | 21 | def __init__(self, genome: Genome, creature: Creature) -> None: 22 | self.genome = genome 23 | self.creature = creature 24 | 25 | @abstractmethod 26 | def act(self, env) -> int: 27 | pass 28 | 29 | def save(self, path: str) -> None: 30 | """ 31 | Saves the agent and it's creature to a file. 32 | """ 33 | pass 34 | 35 | def load(self, path: str) -> None: 36 | """ 37 | Loads the agent and it's creature from a file.""" 38 | pass 39 | 40 | def get_genome(self) -> Genome: 41 | """ 42 | Returns the genome of the agent. 43 | """ 44 | return self.genome 45 | 46 | @abstractmethod 47 | def get_enviroment_state(self, env) -> tf.Tensor: 48 | """ 49 | Creates the input tensor based on the agent's enviroment and body. 50 | """ 51 | pass 52 | 53 | -------------------------------------------------------------------------------- /docs/Architecture/stakeholders.md: -------------------------------------------------------------------------------- 1 | # Key Stakeholders 2 | 3 | Stakeholders in the CrawlAI project are individuals and groups with an interest or investment in the development, deployment, and outcome of the system. They play crucial roles in shaping the project, providing resources, and defining requirements and constraints. 4 | 5 | ## Stakeholders and Architectural Concerns for Crawl AI 6 | 7 | | Stakeholder | Role | Architectural Concerns | 8 | | ------------------------ | --------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | 9 | | Developers | Cogito personal involved in building, testing, and maintaining the application. | Deployability, scalability, maintainability/modifiabiity, technology choices. | 10 | | End Users | Students: Primary users for academic support.
Educators: Secondary users for course delivery and content creation. | Usability (accessibility UD, HCI), Performance. | 11 | | Cogito NTNU Management | Leadership responsible for strategic decisions and aligning the project with organizational goals. | Strategic alignment, compliance, financial oversight. | 12 | | Educational Institutions | Potential partners for usage and expansion. | Integration capabilities, scalability. | 13 | -------------------------------------------------------------------------------- /docs/manuals/developer_setup.md: -------------------------------------------------------------------------------- 1 | ### Manual 2 | 3 | #### Virtual Environment (Recommended) 4 | 5 |
6 | 🚀 A better way to set up repositories 7 | 8 | A virtual environment in Python is a self-contained directory that contains a Python installation for a particular version of Python, plus a number of additional packages. Using a virtual environment for your project ensures that the project's dependencies are isolated from the system-wide Python and other Python projects. This is especially useful when working on multiple projects with differing dependencies, as it prevents potential conflicts between packages and allows for easy management of requirements. 9 | 10 | 1. **To set up and use a virtual environment for CrawlAI:** 11 | First, install the virtualenv package using pip. This tool helps create isolated Python environments. 12 | 13 | ```bash 14 | pip install virtualenv 15 | ``` 16 | 17 | 2. **Create virtual environment** 18 | Next, create a new virtual environment in the project directory. This environment is a directory containing a complete Python environment (interpreter and other necessary files). 19 | 20 | ```bash 21 | python -m venv venv 22 | ``` 23 | 24 | 3. **Activate virtual environment** 25 | To activate the environment, run the following command: \* For windows: 26 | `bash 27 | source ./venv/Scripts/activate 28 | ` 29 | 30 | * For Linux / MacOS: 31 | ```bash 32 | source venv/bin/activate 33 | ``` 34 | 35 |
36 | 37 | #### Install dependencies 38 | 39 | With the virtual environment activated, install the project dependencies: 40 | 41 | ```bash 42 | pip install -r requirements.txt 43 | ``` 44 | 45 | The requirements.txt file contains a list of packages necessary to run CrawlAI. Installing them in an activated virtual environment ensures they are available to the project without affecting other Python projects or system settings. 46 | 47 | 48 | 49 | 50 | python -m venv venv 51 | source venv/bin/activate 52 | pip install -r "requirements.txt" 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/agent_parts/vision.py: -------------------------------------------------------------------------------- 1 | import pygame as pg 2 | from src.agent_parts.rectangle import Point 3 | from src.ground import Ground 4 | from src.globals import BLACK, RED 5 | 6 | 7 | class Vision: 8 | eye_position: Point 9 | sight_width = 50 10 | x_offset = 50 11 | near_periphery: Point 12 | far_periphery: Point 13 | 14 | def __init__(self, eye_position: Point): 15 | self.eye_position = eye_position 16 | self.near_periphery = Point(0, 0) 17 | self.far_periphery = Point(0, 0) 18 | 19 | def update(self, eye_position: Point, ground: Ground, scroll_offset: int) -> None: 20 | 21 | self.eye_position = eye_position 22 | x1 = eye_position.x + self.x_offset 23 | x2 = x1 + self.sight_width 24 | try: 25 | y1 = ground.get_y(x1 + scroll_offset) 26 | self.near_periphery = Point(x1, y1) 27 | except: 28 | pass 29 | try: 30 | y2 = ground.get_y(x2 + scroll_offset) 31 | self.far_periphery = Point(x2, y2) 32 | except: 33 | pass 34 | 35 | def render_vision(self, screen): 36 | pg.draw.circle(screen, BLACK, (self.eye_position.x, self.eye_position.y), 5, 2) 37 | pg.draw.line( 38 | screen, 39 | RED, 40 | (self.eye_position.x, self.eye_position.y), 41 | (self.near_periphery.x, self.near_periphery.y), 42 | 2, 43 | ) 44 | pg.draw.line( 45 | screen, 46 | RED, 47 | (self.eye_position.x, self.eye_position.y), 48 | (self.far_periphery.x, self.far_periphery.y), 49 | 2, 50 | ) 51 | 52 | def get_lower_periphery(self): 53 | return self.near_periphery 54 | 55 | def get_upper_periphery(self): 56 | return self.far_periphery 57 | 58 | def get_eye_position(self): 59 | return self.eye_position 60 | 61 | def get_sight_width(self): 62 | return self.sight_width 63 | 64 | def get_near_periphery(self) -> Point: 65 | if self.near_periphery is None: 66 | return Point(0, 0) 67 | return self.near_periphery 68 | 69 | def get_far_periphery(self) -> Point: 70 | if self.far_periphery is None: 71 | return Point(0, 0) 72 | return self.far_periphery 73 | -------------------------------------------------------------------------------- /pymunkJointTest.py: -------------------------------------------------------------------------------- 1 | import sys, random 2 | random.seed(1) # make the simulation the same each time, easier to debug 3 | import pygame 4 | import pymunk 5 | import pymunk.pygame_util 6 | 7 | def add_ball(space): 8 | """Add a ball to the given space at a random position""" 9 | mass = 3 10 | radius = 25 11 | inertia = pymunk.moment_for_circle(mass, 0, radius, (0,0)) 12 | body = pymunk.Body(mass, inertia) 13 | x = random.randint(120,300) 14 | body.position = x, 50 15 | shape = pymunk.Circle(body, radius, (0,0)) 16 | shape.friction = 1 17 | space.add(body, shape) 18 | return shape 19 | 20 | def add_L(space): 21 | """Add a inverted L shape with two joints""" 22 | rotation_center_body = pymunk.Body(body_type = pymunk.Body.STATIC) 23 | rotation_center_body.position = (300,300) 24 | 25 | rotation_limit_body = pymunk.Body(body_type = pymunk.Body.STATIC) 26 | rotation_limit_body.position = (200,300) 27 | 28 | body = pymunk.Body(10, 10000) 29 | body.position = (300,300) 30 | l1 = pymunk.Segment(body, (-150, 0), (255.0, 0.0), 5.0) 31 | l2 = pymunk.Segment(body, (-150.0, 0), (-150.0, -50.0), 5.0) 32 | l1.friction = 1 33 | l2.friction = 1 34 | l1.mass = 8 35 | l2.mass = 1 36 | 37 | rotation_center_joint = pymunk.PinJoint(body, rotation_center_body, (0,0), (0,0)) 38 | joint_limit = 25 39 | rotation_limit_joint = pymunk.SlideJoint(body, rotation_limit_body, (-100,0), (0,0), 0, joint_limit) 40 | 41 | space.add(l1, l2, body, rotation_center_joint, rotation_limit_joint) 42 | return l1,l2 43 | 44 | def main(): 45 | pygame.init() 46 | screen = pygame.display.set_mode((600, 600)) 47 | pygame.display.set_caption("Joints. Just wait and the L will tip over") 48 | clock = pygame.time.Clock() 49 | 50 | space = pymunk.Space() 51 | space.gravity = (0.0, 900.0) 52 | 53 | lines = add_L(space) 54 | balls = [] 55 | draw_options = pymunk.pygame_util.DrawOptions(screen) 56 | 57 | ticks_to_next_ball = 10 58 | while True: 59 | for event in pygame.event.get(): 60 | if event.type == pygame.QUIT: 61 | sys.exit(0) 62 | elif event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE: 63 | sys.exit(0) 64 | 65 | ticks_to_next_ball -= 1 66 | if ticks_to_next_ball <= 0: 67 | ticks_to_next_ball = 25 68 | ball_shape = add_ball(space) 69 | balls.append(ball_shape) 70 | 71 | screen.fill((255,255,255)) 72 | 73 | balls_to_remove = [] 74 | for ball in balls: 75 | if ball.body.position.y > 550: 76 | balls_to_remove.append(ball) 77 | 78 | for ball in balls_to_remove: 79 | space.remove(ball, ball.body) 80 | balls.remove(ball) 81 | 82 | space.debug_draw(draw_options) 83 | 84 | space.step(1/50.0) 85 | 86 | pygame.display.flip() 87 | clock.tick(50) 88 | 89 | if __name__ == '__main__': 90 | main() -------------------------------------------------------------------------------- /src/agent_parts/limb.py: -------------------------------------------------------------------------------- 1 | import pymunk 2 | import pygame 3 | 4 | 5 | class Limb: 6 | def __init__(self, space, width, height, position, mass=3, color=(0, 255, 0)): 7 | """Initialize a limb as a rectangular body.""" 8 | self.width = width 9 | self.height = height 10 | self.color = color 11 | 12 | # Create a dynamic body with mass 13 | self.body = pymunk.Body(mass, pymunk.moment_for_box(mass, (width, height))) 14 | self.body.position = position 15 | 16 | # Create a box shape for the limb 17 | self.shape = pymunk.Poly.create_box(self.body, (width, height)) 18 | 19 | self.shape.friction = 1 20 | self.shape.filter = pymunk.ShapeFilter( 21 | categories=0b1, mask=pymunk.ShapeFilter.ALL_MASKS() ^ 0b1 22 | ) 23 | 24 | space.add(self.body, self.shape) 25 | 26 | def render(self, screen): 27 | """ 28 | Render the limb onto the screen. 29 | Args: 30 | - screen: The pygame surface to render onto. 31 | """ 32 | # Get the position and angle from the pymunk body 33 | pos = self.body.position 34 | angle = self.body.angle 35 | # Calculate the vertices of the rectangle in world coordinates 36 | vertices = self.shape.get_vertices() 37 | vertices = [v.rotated(angle) + pos for v in vertices] 38 | # Convert pymunk Vec2d vertices to pygame coordinates 39 | vertices = [(float(v.x), float(v.y)) for v in vertices] 40 | # Draw the polygon onto the screen 41 | pygame.draw.polygon(surface=screen, color=(0, 255, 0), points=vertices, width=0) 42 | 43 | def contains_point(self, point: tuple[float, float]) -> bool: 44 | """ 45 | Check if a given point is inside the limb. 46 | 47 | Args: 48 | - point: A tuple representing the x and y coordinates of the point. 49 | 50 | Returns: 51 | - True if the point is inside the limb, False otherwise. 52 | """ 53 | x, y = point 54 | point_vec = pymunk.Vec2d(x, y) 55 | 56 | # Perform a point query to check if the point is within the shape 57 | return self.shape.point_query(point_vec).distance <= 0 58 | 59 | def global_to_local( 60 | self, position: tuple[float, float] 61 | ) -> tuple[float, float] | None: 62 | if not isinstance(position, (tuple, list)) or len(position) != 2: 63 | raise ValueError( 64 | "Position must be a tuple or list with two elements: (x, y)" 65 | ) 66 | 67 | # Convert position to Vec2d 68 | global_position = pymunk.Vec2d(position[0], position[1]) 69 | 70 | # Transform from global to local coordinates 71 | local_position = self.body.world_to_local(global_position) 72 | 73 | return float(local_position.x), float(local_position.y) 74 | 75 | def local_to_global( 76 | self, position: tuple[float, float] 77 | ) -> tuple[float, float] | None: 78 | if not isinstance(position, (tuple, list)) or len(position) != 2: 79 | raise ValueError( 80 | "Position must be a tuple or list with two elements: (x, y)" 81 | ) 82 | 83 | # Convert position to Vec2d 84 | local_position = pymunk.Vec2d(position[0], position[1]) 85 | 86 | # Transform from local to global coordinates 87 | global_position = self.body.local_to_world(local_position) 88 | 89 | return float(global_position.x), float(global_position.y) 90 | -------------------------------------------------------------------------------- /src/environment.py: -------------------------------------------------------------------------------- 1 | import pygame as pg 2 | import random 3 | 4 | import itertools 5 | from enum import Enum 6 | 7 | from src.ground import Ground, BasicGround, InterpolationType, PerlinNoise 8 | from src.render_object import RenderObject 9 | from src.agent_parts.rectangle import Point 10 | from src.globals import ( 11 | SCREEN_WIDTH, 12 | SCREEN_HEIGHT, 13 | FONT_SIZE, 14 | SEGMENT_WIDTH, 15 | BLACK, 16 | RED, 17 | ) 18 | 19 | 20 | class GroundType(Enum): 21 | BASIC_GROUND = 1 22 | PERLIN = 2 23 | 24 | 25 | class Environment(RenderObject): 26 | 27 | def __init__(self, screen, space): 28 | self.screen = screen 29 | self.space = space 30 | self.ground_type = GroundType.BASIC_GROUND 31 | self.ground: Ground = self.ground_factory(self.ground_type) 32 | self.starting_xx = 50 33 | self.offset = 0 34 | self.offset_speed = 1 35 | self.death_ray = None 36 | 37 | def activate_death_ray(self): 38 | self.death_ray = DeathRay(20) 39 | 40 | def ground_factory(self, ground_type: GroundType) -> Ground: 41 | 42 | match ground_type: 43 | case GroundType.BASIC_GROUND: 44 | return BasicGround(self.screen, self.space, SEGMENT_WIDTH) 45 | 46 | case GroundType.PERLIN: 47 | seed = random.randint(0, 2**32) 48 | perlinAmplitude = 30 49 | perlinFrequency = 0.1 50 | octaves = 2 51 | interpolation = InterpolationType.COSINE 52 | return PerlinNoise( 53 | seed, perlinAmplitude, perlinFrequency, octaves, interpolation 54 | ) 55 | 56 | def update(self): 57 | self.offset = 0 58 | self.ground.update(self.offset) 59 | # self.ground.move_segments(self.offset/100) 60 | self.starting_xx += 1 61 | if self.death_ray is not None: 62 | self.death_ray.move(0.1) 63 | 64 | def render(self): 65 | self.ground.render() 66 | if self.death_ray is not None: 67 | self.death_ray.render(self.screen) 68 | 69 | def run(self): 70 | 71 | if self.ground_type == GroundType.BASIC_GROUND: 72 | self.ground.generate_floor_segment(0) 73 | 74 | active = True 75 | while active: 76 | clock = pg.time.Clock() 77 | clock.tick(60) 78 | 79 | for event in pg.event.get(): 80 | if event.type == pg.QUIT: 81 | active = False 82 | 83 | self.screen.fill((135, 206, 235)) 84 | 85 | self.ground.update(self.offset) 86 | self.ground.render(self.offset) 87 | self.starting_xx += 1 88 | 89 | match self.ground_type: 90 | case GroundType.BASIC_GROUND: 91 | self.vision.update( 92 | self.screen, 93 | Point(self.starting_xx, 100), 94 | self.ground, 95 | self.offset, 96 | ) 97 | 98 | case GroundType.PERLIN: 99 | self.vision.update( 100 | self.screen, Point(self.starting_xx, 100), self.ground, 0 101 | ) 102 | self.offset += 1 103 | pg.display.flip() 104 | pg.quit() 105 | 106 | def draw_mark(surface, color, coord): 107 | pg.draw.circle(surface, color, coord, 3) 108 | 109 | 110 | class DeathRay: 111 | x: int 112 | 113 | def __init__(self, x: int): 114 | self.x = x 115 | 116 | def update(self, x: int): 117 | self.x = x 118 | 119 | def render(self, screen): 120 | pg.draw.line(screen, RED, (self.x, 0), (self.x, SCREEN_HEIGHT), 2) 121 | 122 | def move(self, offset: int): 123 | self.x += offset 124 | 125 | def get_x(self): 126 | return self.x 127 | -------------------------------------------------------------------------------- /docs/Architecture/architectural_design.md: -------------------------------------------------------------------------------- 1 | # Architectural Design 2 | 3 | ## Quality Attribute Scenarios 4 | 5 | See [Quality Attribute Scenarios](quality_attribute_scenarios.md) 6 | 7 | ## Architectural Drivers / Architecturally Significant Requirements (ASRs) 8 | 9 | See [Architectural Drivers](requirements.md) 10 | 11 | ## Components 12 | 13 | CrawlAI uses the following COTS components: 14 | [COTS Components](cots.md) 15 | 16 | ## Stakeholders and Concerns 17 | 18 | See [Stakeholders and Concerns](stakeholders.md) 19 | 20 | ## Architectural tactics and patterns 21 | 22 | ### Tactics 23 | 24 | | Tactic | Affected Quality Attribute | Reasoning | 25 | | ------------------------ | -------------------------- | ---------------------------------------------------------------------------- | 26 | | Schedule Resources | Performance | Optimizes performance by dynamically allocating resources based on demand. | 27 | | Continuous Deployment | Deployability | Allows for updates without downtime, maintaining high availability. | 28 | | Reduce Size of Modules | Modifiability | Breaks down modules into smaller, more manageable components. | 29 | | Increase Cohesion | Modifiability | Ensures each module has a single, well-defined purpose. | 30 | | Encapsulate | Modifiability | Limits dependencies by encapsulating functionalities. | 31 | | Use an Intermediary | Modifiability | Manages interactions between modules through intermediaries. | 32 | | Restrict Dependencies | Modifiability | Minimizes dependencies between modules. | 33 | | Abstract Common Services | Modifiability | Creates common services that can be reused across the system. | 34 | | Defer Binding | Modifiability | Increases flexibility by delaying the binding of components until needed. | 35 | | Prioritize Events | Modifiability | Manages event priorities to handle critical operations efficiently. | 36 | | Introduce Concurrency | Modifiability | Improves responsiveness by implementing concurrent processing. | 37 | | Heartbeat Monitoring | Availability | Regularly checks the health of the system to detect and respond to failures. | 38 | 39 | | Maintain Task Model | Usability | Continuously updates the model of user tasks to ensure the system remains user-friendly. | 40 | | User Model | Usability | Maintains an understanding of the user base to tailor interactions and improve UX. | 41 | | System Model | Usability | Keeps a model of system interactions to ensure predictable and intuitive behavior. | 42 | 43 | ### Patterns 44 | 45 | Describe the architectural patterns used 46 | ECS? 47 | data access object? 48 | State (design pattern) 49 | 50 | ## Architectural Viewpoints 51 | 52 | ### Physical View 53 | 54 | _The physical view should describe how the software is allocated to 55 | hardware. This includes the client, the server, and network connections you develop 56 | and other services you use (like cloud-based services, etc.). A typical notation for this 57 | view is a deployment diagram._ 58 | 59 | ### Logical View 60 | 61 | _It is recommended to use multiple diagrams for this view, e.g., provide 62 | one high-level diagram and one or more diagrams for more detail. Typical notations 63 | for this view include a UML class diagram, UML package diagram, layers diagram, 64 | ER diagram, and combination of UML class and package diagrams._ 65 | 66 | ### Process View 67 | 68 | _It is also recommended to use multiple diagrams for the view to give a 69 | complete description of the run-time behavior of the system. Typical notations for this 70 | view include a UML activity diagram, a UML state diagram, and a UML sequence 71 | diagram._ 72 | 73 | ### Development View 74 | 75 | _The purpose of the development view is to make it easier to 76 | allocate work to various team members and make it possible to allow development in 77 | parallel. Based on this view, it should be easy to give programming tasks to group 78 | members and make it easy to integrate the results after completion with the rest of the 79 | system_ 80 | 81 | ## Architectural Rationale 82 | -------------------------------------------------------------------------------- /src/NEATnetwork.py: -------------------------------------------------------------------------------- 1 | # src/NEATnetwork.py 2 | 3 | from collections import defaultdict, deque 4 | import numpy as np 5 | from src.genome import Genome, Node, Connection 6 | from typing import List 7 | 8 | 9 | class NEATNetwork: 10 | 11 | def __init__(self, genome: Genome): 12 | 13 | # Store genome information 14 | self.genome = genome 15 | 16 | # Create dictionaries for nodes and connections 17 | self.node_dict = {node.id: node for node in genome.nodes} 18 | self.connection_dict = { 19 | conn.innovation_number: conn for conn in genome.connections if conn.enabled 20 | } 21 | 22 | # Topologically sort nodes based on connections 23 | self.topological_order = self._topological_sort() 24 | 25 | def sigmoid(self, x: np.ndarray) -> np.ndarray: 26 | """Sigmoid activation function.""" 27 | return 1 / (1 + np.exp(-x)) 28 | 29 | def ReLU(self, x): 30 | """ReLU activation function.""" 31 | return np.maximum(0, x) 32 | 33 | def _topological_sort(self) -> List[int]: 34 | """ 35 | Performs topological sorting on the nodes based on their connections. 36 | """ 37 | in_degree = defaultdict(int) 38 | adjacency_list = defaultdict(list) 39 | 40 | # Build graph 41 | for conn in self.genome.connections: 42 | if conn.enabled: 43 | adjacency_list[conn.in_node].append(conn.out_node) 44 | in_degree[conn.out_node] += 1 45 | 46 | # Initialize the queue with input nodes (no incoming connections) 47 | input_nodes = [ 48 | node.id for node in self.genome.nodes if node.node_type == "input" 49 | ] 50 | queue = deque([node_id for node_id in input_nodes if in_degree[node_id] == 0]) 51 | 52 | topological_order = [] 53 | while queue: 54 | node = queue.popleft() 55 | topological_order.append(node) 56 | 57 | # Process the neighbors of this node 58 | for neighbor in adjacency_list[node]: 59 | in_degree[neighbor] -= 1 60 | if in_degree[neighbor] == 0: 61 | queue.append(neighbor) 62 | 63 | return topological_order 64 | 65 | def forward(self, x: np.ndarray) -> np.ndarray: 66 | """ 67 | Perform a forward pass through the network given an input. 68 | Args: 69 | x: Input array to the network 70 | Returns: 71 | Output array from the network 72 | """ 73 | if len(x) == 0: 74 | raise ValueError( 75 | "Input array 'x' is empty. Check if 'num_inputs' is set correctly in the genome." 76 | ) 77 | 78 | # Dictionary to store outputs of each node 79 | node_outputs = {} 80 | 81 | # Assume input nodes are provided in the correct order 82 | input_nodes = [node for node in self.genome.nodes if node.node_type == "input"] 83 | output_nodes = [ 84 | node for node in self.genome.nodes if node.node_type == "output" 85 | ] 86 | 87 | # Assign input values 88 | for i, node in enumerate(input_nodes): 89 | node_outputs[node.id] = x[i] 90 | 91 | # Process nodes in topological order 92 | for node_id in self.topological_order: 93 | # Skip input nodes, their values are already set 94 | if node_id in node_outputs: 95 | continue 96 | 97 | # Compute the value of this node based on incoming connections 98 | incoming_connections = [ 99 | conn 100 | for conn in self.genome.connections 101 | if conn.out_node == node_id and conn.enabled 102 | ] 103 | 104 | node_sum = 0 105 | for conn in incoming_connections: 106 | in_node = conn.in_node 107 | weight = conn.weight 108 | node_sum += node_outputs.get(in_node, 0) * weight 109 | 110 | # Apply activation (ReLU for hidden and output nodes) 111 | node_outputs[node_id] = self.ReLU(node_sum) 112 | 113 | # Collect the outputs for final output nodes 114 | output = np.array([node_outputs.get(node.id, 0) for node in output_nodes]) 115 | return output / 200 # Normalize the output 116 | -------------------------------------------------------------------------------- /src/agent_parts/creature_test.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import pymunk 3 | from creature import Creature 4 | import random 5 | 6 | 7 | def create_ground(space, width, height, position=(0, 0)): 8 | """Create a static ground body to interact with the creature.""" 9 | ground_body = pymunk.Body(body_type=pymunk.Body.STATIC) 10 | ground_body.position = position 11 | ground_shape = pymunk.Segment(ground_body, (0, 0), (width, 0), height) 12 | ground_shape.friction = 1.0 13 | space.add(ground_body, ground_shape) 14 | return ground_shape 15 | 16 | 17 | def setup_pygame(): 18 | """Initialize Pygame screen and clock.""" 19 | pygame.init() 20 | screen = pygame.display.set_mode((800, 600)) 21 | clock = pygame.time.Clock() 22 | return screen, clock 23 | 24 | 25 | def setup_pymunk(): 26 | """Initialize a Pymunk space with Earth-like gravity.""" 27 | space = pymunk.Space() 28 | space.gravity = (0, 981) 29 | return space 30 | 31 | 32 | def create_simulation(space): 33 | """Initialize the creature and its environment in the space.""" 34 | creature = Creature(space) 35 | 36 | # Add limbs to the creature 37 | limb1 = creature.add_limb(100, 20, (300, 300), mass=1) 38 | limb2 = creature.add_limb(100, 20, (350, 300), mass=1) 39 | limb3 = creature.add_limb(80, 40, (400, 300), mass=5) 40 | 41 | # Add motors between limbs 42 | creature.add_motor(limb1, limb2, (25, 0), (-25, 0), rate=2) 43 | creature.add_motor(limb2, limb3, (37, 0), (-23, 0), rate=-2) 44 | 45 | # Create the ground in the space 46 | create_ground(space, width=800, height=10, position=(0, 550)) 47 | return creature 48 | 49 | 50 | def main_loop(screen, clock, space, creature, fps=60): 51 | """Runs the main simulation loop for rendering and updating the creature.""" 52 | running = True 53 | ground_position = (0, 550) 54 | while running: 55 | screen.fill((255, 255, 255)) 56 | 57 | # Update the physics simulation 58 | space.step(1 / fps) 59 | 60 | # Render the ground as a line 61 | pygame.draw.line( 62 | screen, (0, 0, 0), (0, ground_position[1]), (800, ground_position[1]), 10 63 | ) 64 | 65 | current_joint_rates = creature.get_joint_rates() 66 | creature.set_joint_rates([random.uniform(0, 2) for _ in current_joint_rates]) 67 | 68 | creature.render(screen) 69 | 70 | pygame.display.flip() 71 | clock.tick(fps) 72 | 73 | for event in pygame.event.get(): 74 | if event.type == pygame.QUIT: 75 | running = False 76 | 77 | pygame.quit() 78 | 79 | 80 | def run_simulation(simulation_id, frames=600): 81 | """Run a single simulation of the creature for a set number of frames.""" 82 | screen, clock = setup_pygame() 83 | space = setup_pymunk() 84 | 85 | running = True 86 | for _ in range(frames): 87 | creature = create_simulation(space) 88 | for event in pygame.event.get(): 89 | if event.type == pygame.QUIT: 90 | running = False 91 | break # Exit the event loop 92 | 93 | if not running: 94 | break # Exit the main loop 95 | 96 | screen.fill((255, 255, 255)) 97 | space.step(1 / 60.0) # Update physics 98 | creature.set_joint_rates( 99 | [random.uniform(0, 2) for _ in creature.get_joint_rates()] 100 | ) 101 | creature.render(screen) 102 | pygame.display.flip() 103 | clock.tick(60) 104 | 105 | pygame.quit() 106 | return simulation_id 107 | 108 | 109 | def run_multiple_simulations(num_simulations=100, frames_per_simulation=200): 110 | """Run multiple simulations sequentially and collect results.""" 111 | results = [] 112 | for i in range(num_simulations): 113 | result = run_simulation(i, frames_per_simulation) 114 | results.append(result) 115 | return results 116 | 117 | 118 | # Execute simulations 119 | if __name__ == "__main__": 120 | space = setup_pymunk() 121 | screen, clock = setup_pygame() 122 | creature = create_simulation(space) 123 | main_loop(screen, clock, space, creature) 124 | 125 | # Run batch simulations 126 | results = run_multiple_simulations(num_simulations=100, frames_per_simulation=600) 127 | print("All simulations completed:", results) 128 | -------------------------------------------------------------------------------- /src/interface.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | 4 | class Button: 5 | """General button functions""" 6 | 7 | def __init__( 8 | self, 9 | text, 10 | pos, 11 | width, 12 | height, 13 | font, 14 | color, 15 | hover_color, 16 | text_color, 17 | active_color=None, 18 | callback=None, 19 | ): 20 | """Initializes a button with the function name as input""" 21 | self.text = text 22 | self.pos = pos 23 | self.width = width 24 | self.height = height 25 | self.font = font 26 | self.color = color 27 | self.hover_color = hover_color 28 | self.text_color = text_color 29 | self.active_color = active_color if active_color else color 30 | self.rect = pygame.Rect(pos[0], pos[1], width, height) 31 | self.callback = callback 32 | self.toggled = False 33 | 34 | def render(self, screen): 35 | """mouse_pos = pygame.mouse.get_pos() 36 | is_hovered = self.rect.collidepoint(mouse_pos) 37 | color = self.hover_color if is_hovered else self.color""" 38 | current_color = self.active_color if self.toggled else self.color 39 | pygame.draw.rect(screen, current_color, self.rect) 40 | text_surf = self.font.render(self.text, True, self.text_color) 41 | text_rect = text_surf.get_rect(center=self.rect.center) 42 | screen.blit(text_surf, text_rect) 43 | 44 | def is_clicked(self, event) -> bool: 45 | if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: 46 | if self.rect.collidepoint(event.pos): 47 | self.toggled = not self.toggled 48 | if self.callback: # Check if there’s a function to call 49 | self.callback() # Call the button's callback function 50 | return True 51 | return False 52 | 53 | def deactivate(self): 54 | self.toggled = False 55 | 56 | class Interface: 57 | def __init__(self): 58 | """Initializes the elements in the interface""" 59 | 60 | self.buttons: list[Button] = [] #for all buttons in the interface 61 | self.active_button = Button 62 | self.only_one_simultaneously_buttons = [] #buttons that can't be active simultaneously 63 | 64 | 65 | def add_button(self, button: Button) -> Button: 66 | self.buttons.append(button) 67 | return button 68 | 69 | if button in self.buttons: 70 | self.buttons.remove(button) 71 | return button 72 | 73 | def add_only_one_simultaneously_buttons(self, button: Button) -> Button: 74 | self.only_one_simultaneously_buttons.append(button) 75 | self.buttons.append(button) #don't have to append on both lists manually 76 | return button 77 | 78 | def remove_only_one_simultaneously_buttons(self, button: Button) -> Button: 79 | if button in self.only_one_simultaneously_buttons: 80 | self.buttons.remove(button) 81 | return button 82 | 83 | def render(self, screen): 84 | """Render all UI elements.""" 85 | for button in self.buttons: 86 | button.render(screen) 87 | 88 | def handle_events(self, event): 89 | """Handle events for all UI elements.""" 90 | for button in self.buttons: 91 | button.is_clicked(event) 92 | 93 | 94 | def is_any_button_clicked(self, event) -> bool: 95 | for button in self.buttons: 96 | if button.is_clicked(event): # If any button is clicked 97 | return True # Return True immediately 98 | return False 99 | 100 | def handle_only_one_function(self, event) -> Button|None: 101 | """Activates only one of the mutually exclusive buttons.""" 102 | for button in self.only_one_simultaneously_buttons: 103 | if button.is_clicked(event): # If this button was clicked 104 | self.active_button = button 105 | # Deactivate all other mutually exclusive buttons 106 | for other_button in self.only_one_simultaneously_buttons: 107 | if other_button != button: 108 | other_button.deactivate() 109 | return button # Return the clicked (active) button 110 | return None # No button was clicked 111 | 112 | def any_active_only_one_simultaneously_buttons_active(self) -> bool: 113 | return any(button.toggled for button in self.only_one_simultaneously_buttons) 114 | 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | 164 | 165 | 166 | # compiled output 167 | /dist 168 | /tmp 169 | /out-tsc 170 | 171 | # Runtime data 172 | pids 173 | *.pid 174 | *.seed 175 | *.pid.lock 176 | 177 | # Directory for instrumented libs generated by jscoverage/JSCover 178 | lib-cov 179 | 180 | # Coverage directory used by tools like istanbul 181 | coverage 182 | 183 | # nyc test coverage 184 | .nyc_output 185 | 186 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 187 | .grunt 188 | 189 | # Bower dependency directory (https://bower.io/) 190 | bower_components 191 | 192 | # node-waf configuration 193 | .lock-wscript 194 | 195 | # IDEs and editors 196 | .idea 197 | .project 198 | .classpath 199 | .c9/ 200 | *.launch 201 | .settings/ 202 | *.sublime-workspace 203 | 204 | # IDE - VSCode 205 | .vscode/* 206 | !.vscode/settings.json 207 | !.vscode/tasks.json 208 | !.vscode/launch.json 209 | !.vscode/extensions.json 210 | 211 | # misc 212 | .sass-cache 213 | connect.lock 214 | typings 215 | 216 | # Logs 217 | logs 218 | *.log 219 | npm-debug.log* 220 | yarn-debug.log* 221 | yarn-error.log* 222 | 223 | 224 | # Dependency directories 225 | node_modules/ 226 | jspm_packages/ 227 | 228 | # Optional npm cache directory 229 | .npm 230 | 231 | # Optional eslint cache 232 | .eslintcache 233 | 234 | # Optional REPL history 235 | .node_repl_history 236 | 237 | # Output of 'npm pack' 238 | *.tgz 239 | 240 | # Yarn Integrity file 241 | .yarn-integrity 242 | 243 | # dotenv environment variables file 244 | .env 245 | 246 | # next.js build output 247 | .next 248 | 249 | # Lerna 250 | lerna-debug.log 251 | 252 | # System Files 253 | .DS_Store 254 | Thumbs.db -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # CrawlAI 3 | 4 | 5 |
6 | 7 | ![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/CogitoNTNU/CrawlAI/ci.yml) 8 | ![GitHub top language](https://img.shields.io/github/languages/top/CogitoNTNU/CrawlAI) 9 | ![GitHub language count](https://img.shields.io/github/languages/count/CogitoNTNU/CrawlAI) 10 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 11 | [![Project Version](https://img.shields.io/badge/version-0.0.1-blue)](https://img.shields.io/badge/version-0.0.1-blue) 12 | 13 | Cogito Project Logo 14 |
15 | 16 | 17 |
18 | 📋 Table of contents 19 | 20 | - [CrawlAI](#crawlai) 21 | - [Description](#description) 22 | - [Getting started](#getting-started) 23 | - [Prerequisites](#prerequisites) 24 | - [Usage](#usage) 25 | - [Testing](#testing) 26 | - [Documentation](#documentation) 27 | - [Team](#team) 28 | - [License](#license) 29 | 30 |
31 | 32 | ## Description 33 | 34 | 35 | CrawlAI is a project where the developers can get a hands-on experience with genetic algorithms and neural network. The objection is to let agents learn to walk in a 2D enviroment, by evolving. 36 | 37 | 38 | ## Getting started 39 | 41 | 42 | ### Prerequisites 43 | Before you start, make sure the following tools are installed on your system: 44 | - **Git:** Version control system to clone the project repository [Download Git](https://git-scm.com/downloads) 45 | 46 | - **Python:** Programming language used to build the application [Download Python](https://www.python.org/downloads/) 47 | 48 | 49 | 50 | 51 | ## Usage 52 | To run the project, run the following command from the root directory of the project: 53 | ```bash 54 | python main.py 55 | ``` 56 | 57 | 58 | ## Testing 59 | To run the test suite, run the following command from the root directory of the project: 60 | ```bash 61 | pytest 62 | ``` 63 | 64 | ## Documentation 65 | - [Developer Setup](docs/manuals/developer_setup.md) 66 | 67 | 68 | ## Team 69 | This project would not have been possible without the hard work and dedication of all of the contributors. Thank you for the time and effort you have put into making this project a reality. 70 | 71 | 72 | 73 | 79 | 85 | 91 | 97 | 103 | 109 | 115 | 121 | 122 |
74 | 75 | Jonas Korkosh
76 | Jonas Korkosh
77 |
78 |
80 | 81 | AlMinaDO
82 | Mina Al-Dolaimi 83 |
84 |
86 | 87 | Nathaniavm
88 | Nathania Muliawan 89 |
90 |
92 | 93 | Nils Henrik Hoelfeldt Lund
94 | Nils Henrik Hoelfeldt Lund
95 |
96 |
98 | 99 | Parleenb
100 | Parleen Brar
101 |
102 |
104 | 105 | SindreFossdal
106 | Sindre Fossdal 107 |
108 |
110 | 111 | LockedInTheSkage
112 | Skage Reistad 113 |
114 |
116 | 117 | Tobias Fremming
118 | Tobias Fremming 119 |
120 |
123 | 124 | ![Group picture](docs/img/team.png) 125 | 126 | 127 | ### License 128 | ------ 129 | Distributed under the MIT License. See `LICENSE` for more information. 130 | -------------------------------------------------------------------------------- /src/agent_parts/creature.py: -------------------------------------------------------------------------------- 1 | import pymunk 2 | import pygame 3 | from src.agent_parts.limb import Limb 4 | from src.agent_parts.motorjoint import MotorJoint 5 | from src.agent_parts.vision import Vision 6 | from src.ground import Ground 7 | from src.agent_parts.rectangle import Point 8 | 9 | 10 | class Creature: 11 | limbs: list[Limb] 12 | motors: list[MotorJoint] 13 | vision: Vision 14 | 15 | def __init__(self, space, vision: Vision): 16 | """ 17 | Initialize a creature with an empty list of limbs and motors. 18 | 19 | """ 20 | 21 | self.space = space 22 | self.limbs = [] 23 | self.motors = [] 24 | self.relative_vectors = [] 25 | self.vision = vision 26 | 27 | def add_limb( 28 | self, 29 | width: float, 30 | height: float, 31 | position: tuple[float, float], 32 | mass=1, 33 | color=(0, 255, 0), 34 | ) -> Limb: 35 | """ 36 | Add a limb to the creature. 37 | Args: 38 | - width: The width of the limb. 39 | - height: The height of the limb. 40 | - position: The position of the limb. 41 | - mass: The mass of the limb 42 | - color: The color of the limb. 43 | 44 | """ 45 | limb = Limb(self.space, width, height, position, mass, color) 46 | self.limbs.append(limb) 47 | return limb 48 | 49 | def update_vision(self, x: int, y: int): 50 | """ 51 | Update the vision position of the creature 52 | Args: 53 | x (int): x coordinate 54 | y (int): y coordinate 55 | """ 56 | self.vision.update(x, y) 57 | 58 | def start_dragging(self, dragged_limb: Limb): 59 | for limb in self.limbs: 60 | if limb != dragged_limb: 61 | vector = ( 62 | limb.body.position.x - dragged_limb.body.position.x, 63 | limb.body.position.y - dragged_limb.body.position.y, 64 | ) 65 | self.relative_vectors.append((limb, vector)) 66 | 67 | def update_creature_position( 68 | self, dragged_limb: Limb, new_position: tuple[float, float] 69 | ): 70 | dragged_limb.body.position = new_position[0], new_position[1] 71 | for limb, vector in self.relative_vectors: 72 | new_position = ( 73 | dragged_limb.body.position.x + vector[0], 74 | dragged_limb.body.position.y + vector[1], 75 | ) 76 | limb.body.position = new_position 77 | 78 | def add_motor_on_limbs( 79 | self, limb_a: Limb, limb_b: Limb, position: tuple[float, float] 80 | ) -> MotorJoint | None: 81 | if limb_a.contains_point(position) and limb_b.contains_point(position): 82 | anchor1 = limb_a.global_to_local(position) 83 | anchor2 = limb_b.global_to_local(position) 84 | return self.add_motor(limb_a, limb_b, anchor1, anchor2, rate = -10) 85 | else: 86 | return None 87 | 88 | def local_to_global( 89 | self, limb: Limb, anchor: tuple[float, float] 90 | ) -> tuple[float, float]: 91 | """Convert a local anchor point to a global anchor point.""" 92 | return limb.body.local_to_world(anchor) 93 | 94 | def add_motor( 95 | self, 96 | limb_a: Limb, 97 | limb_b: Limb, 98 | anchor_a: tuple[float, float], 99 | anchor_b: tuple[float, float], 100 | rate=0.0, 101 | tolerance=10, 102 | ) -> MotorJoint | None: 103 | """Add a motor connecting two limbs.""" 104 | global_a = self.local_to_global(limb_a, anchor_a) 105 | global_b = self.local_to_global(limb_b, anchor_b) 106 | 107 | # Check if the global points are within the tolerance 108 | if ( 109 | abs(global_a[0] - global_b[0]) < tolerance 110 | and abs(global_a[1] - global_b[1]) < tolerance 111 | ): 112 | motor = MotorJoint( 113 | self.space, limb_a.body, limb_b.body, anchor_a, anchor_b, rate 114 | ) 115 | self.motors.append(motor) 116 | return motor 117 | 118 | def local_to_global( 119 | self, limb: Limb, point: tuple[float, float] 120 | ) -> tuple[float, float] | None: 121 | return limb.local_to_global(point) 122 | 123 | def global_to_local( 124 | self, limb: Limb, point: tuple[float, float] 125 | ) -> tuple[float, float] | None: 126 | return limb.global_to_local(point) 127 | 128 | def render(self, screen: pygame.display): 129 | """Render the entire creature by rendering all limbs.""" 130 | for limb in self.limbs: 131 | limb.render(screen) 132 | for motor in self.motors: 133 | motor.render(screen, motor.pivot.a, motor.pivot.b) # Render motor joints 134 | self.vision.render_vision(screen) 135 | 136 | def get_joint_rates(self) -> list[float]: 137 | """Return the current rates of all motor joints.""" 138 | return [motor.motor.rate for motor in self.motors] 139 | 140 | def get_joint_rotations(self) -> list[float]: 141 | """Return the current rotations of all motor joints.""" 142 | rotations = [] 143 | for motor in self.motors: 144 | rotations.append(motor.get_angle()) 145 | return rotations 146 | 147 | def get_amount_of_joints(self) -> int: 148 | """Return the number of motor joints in the creature.""" 149 | return len(self.motors) 150 | 151 | def get_amount_of_limb(self) -> int: 152 | """Return the number of limbs in the creature.""" 153 | return len(self.limbs) 154 | 155 | def set_joint_rates(self, rates: list[float]): 156 | """Set the rates of all motor joints.""" 157 | for motor, rate in zip(self.motors, rates): 158 | motor.set_motor_rate(rate) 159 | 160 | def get_joint_positions(self) -> list[tuple[float, float]]: 161 | return [(motor.pivot.a, motor.pivot.b) for motor in self.motors] 162 | 163 | def get_limb_positions(self) -> list[tuple[float, float]]: 164 | return [(limb.body.position.x, limb.body.position.y) for limb in self.limbs] 165 | -------------------------------------------------------------------------------- /src/genetic_algorithm.py: -------------------------------------------------------------------------------- 1 | # src/genetic_algorithm.py 2 | 3 | from typing import List 4 | from src.genome import Genome 5 | import random 6 | 7 | 8 | class GeneticAlgorithm: 9 | def __init__( 10 | self, 11 | population_size: int, 12 | initial_creature: "Creature", 13 | speciation_threshold: float = 3.0, 14 | ): 15 | """ 16 | Initialize the Genetic Algorithm with a given population size and initial creature. 17 | 18 | Args: 19 | population_size (int): Number of genomes in the population. 20 | initial_creature (Creature): The initial creature to determine inputs and outputs. 21 | speciation_threshold (float): Threshold for speciation. 22 | """ 23 | self.population_size = population_size 24 | self.initial_creature = initial_creature 25 | self.speciation_threshold = speciation_threshold 26 | self.speciation = {} # species_id -> List[Genome] 27 | self.species_representatives = {} # species_id -> Genome 28 | self.population: List[Genome] = [] 29 | self.innovation = None 30 | self.genome_id_counter = 0 31 | 32 | # Determine number of inputs and outputs based on initial creature 33 | self.num_inputs, self.num_outputs = self.determine_io() 34 | 35 | # Initialize the population 36 | self.population = self.initialize_population() 37 | 38 | # Assign genomes to species 39 | self.assign_species_to_population() 40 | 41 | def determine_io(self) -> (int, int): 42 | """ 43 | Determine the number of inputs and outputs based on the initial creature. 44 | 45 | Returns: 46 | Tuple[int, int]: Number of inputs and outputs. 47 | """ 48 | # Example based on your initial code 49 | # Inputs: 50 | # - Vision data: 4 51 | # - Joint rates: number of joints 52 | # - Limb positions: number of limbs * 2 53 | amount_of_joints = self.initial_creature.get_amount_of_joints() 54 | amount_of_limb = self.initial_creature.get_amount_of_limb() 55 | num_inputs = 4 + amount_of_joints + (amount_of_limb * 2) 56 | num_outputs = amount_of_joints # Assuming each joint has one output 57 | 58 | return num_inputs, num_outputs 59 | 60 | def initialize_population(self) -> List[Genome]: 61 | """Initialize a population of genomes.""" 62 | population = [] 63 | for _ in range(self.population_size): 64 | genome = Genome( 65 | genome_id=self.genome_id_counter, 66 | num_inputs=self.num_inputs, 67 | num_outputs=self.num_outputs, 68 | ) 69 | population.append(genome) 70 | self.genome_id_counter += 1 71 | return population 72 | 73 | def assign_species_to_population(self): 74 | """Assign genomes in the population to species.""" 75 | self.speciation.clear() 76 | self.species_representatives.clear() 77 | for genome in self.population: 78 | self.assign_to_species(genome) 79 | 80 | def assign_to_species(self, genome: Genome): 81 | """Assign a genome to an existing species or create a new species if none match.""" 82 | species_id = self.determine_species(genome) 83 | 84 | # If the species ID is new, initialize its entry 85 | if species_id not in self.speciation: 86 | self.speciation[species_id] = [] 87 | 88 | # Add the genome to the determined species 89 | self.speciation[species_id].append(genome) 90 | genome.species = species_id 91 | 92 | def determine_species(self, genome: Genome) -> int: 93 | """Determine the species ID for a given genome.""" 94 | for species_id, representative in self.species_representatives.items(): 95 | distance = genome.compute_compatibility_distance(representative) 96 | if distance < self.speciation_threshold: 97 | return species_id 98 | 99 | # If no existing species matches, create a new species 100 | new_species_id = len(self.species_representatives) + 1 101 | self.species_representatives[new_species_id] = genome 102 | return new_species_id 103 | 104 | def reassign_species(self): 105 | """Reassign genomes to species after a generation.""" 106 | self.speciation.clear() 107 | self.species_representatives.clear() 108 | for genome in self.population: 109 | self.assign_to_species(genome) 110 | 111 | def evaluate_population(self, evaluate_function) -> float: 112 | """ 113 | Evaluate the entire population's fitness. 114 | 115 | Parameters: 116 | evaluate_function (callable): Function to evaluate fitness of a genome. 117 | 118 | Returns: 119 | float: The average fitness of the population. 120 | """ 121 | total_fitness = 0.0 122 | for genome in self.population: 123 | genome.fitness = evaluate_function(genome) 124 | total_fitness += genome.fitness 125 | average_fitness = total_fitness / len(self.population) if self.population else 0 126 | return average_fitness 127 | 128 | def adjust_fitness(self): 129 | """Adjust the fitness of each genome based on species size.""" 130 | for species_id, members in self.speciation.items(): 131 | species_size = len(members) 132 | for genome in members: 133 | genome.adjusted_fitness = genome.fitness / species_size 134 | 135 | def tournament_selection( 136 | self, members: List[Genome], tournament_size: int = 3 137 | ) -> Genome: 138 | """Select a parent genome using tournament selection.""" 139 | tournament = random.sample(members, min(tournament_size, len(members))) 140 | tournament.sort(key=lambda g: g.fitness, reverse=True) 141 | return tournament[0] 142 | 143 | def reproduce(self): 144 | """Create a new generation through reproduction.""" 145 | new_population = [] 146 | total_adjusted_fitness = sum( 147 | genome.adjusted_fitness for genome in self.population 148 | ) 149 | 150 | if total_adjusted_fitness == 0: 151 | total_adjusted_fitness = 1 # Prevent division by zero 152 | 153 | # Calculate offspring counts for each species 154 | offspring_counts = {} 155 | for species_id, members in self.speciation.items(): 156 | species_adjusted_fitness = sum( 157 | genome.adjusted_fitness for genome in members 158 | ) 159 | offspring_count = int( 160 | (species_adjusted_fitness / total_adjusted_fitness) 161 | * self.population_size 162 | ) 163 | offspring_counts[species_id] = offspring_count 164 | 165 | # Generate offspring for each species 166 | for species_id, members in self.speciation.items(): 167 | offspring_count = offspring_counts.get(species_id, 0) 168 | if offspring_count == 0: 169 | continue 170 | 171 | # Sort members by fitness in descending order 172 | members.sort(key=lambda g: g.fitness, reverse=True) 173 | 174 | # Elitism: Keep the best genome 175 | best_genome = members[0].copy() 176 | best_genome.id = self.genome_id_counter 177 | self.genome_id_counter += 1 178 | new_population.append(best_genome) 179 | 180 | for _ in range(offspring_count - 1): 181 | parent1 = self.tournament_selection(members) 182 | if random.random() < 0.25: 183 | # Mutation without crossover 184 | child = parent1.copy() 185 | child.mutate() 186 | else: 187 | parent2 = self.tournament_selection(members) 188 | # Ensure the more fit parent is parent1 189 | if parent2.fitness > parent1.fitness: 190 | parent1, parent2 = parent2, parent1 191 | 192 | # Perform crossover 193 | child = parent1.crossover(parent2) 194 | child.mutate() 195 | 196 | # Assign a new genome ID 197 | child.id = self.genome_id_counter 198 | self.genome_id_counter += 1 199 | new_population.append(child) 200 | 201 | # If the new population is smaller due to rounding, fill it up 202 | while len(new_population) < self.population_size: 203 | parent = random.choice(self.population) 204 | child = parent.copy() 205 | child.mutate() 206 | child.id = self.genome_id_counter 207 | self.genome_id_counter += 1 208 | new_population.append(child) 209 | 210 | # Update the population 211 | self.population = new_population 212 | 213 | def evolve(self, generations: int, evaluate_function): 214 | """Run the evolution process for a specified number of generations.""" 215 | for generation in range(generations): 216 | print(f"Generation {generation + 1}") 217 | average_fitness = self.evaluate_population(evaluate_function) 218 | print(f"Average Fitness: {average_fitness}") 219 | self.adjust_fitness() 220 | self.reassign_species() 221 | self.reproduce() 222 | -------------------------------------------------------------------------------- /agent_parts_main.py: -------------------------------------------------------------------------------- 1 | from typing import Protocol 2 | from enum import Enum 3 | import numpy 4 | import random 5 | 6 | import pygame 7 | import pymunk 8 | from pygame.locals import * 9 | 10 | from src.genome import Genome 11 | from src.globals import SCREEN_WIDTH, SCREEN_HEIGHT 12 | from src.environment import Environment, GroundType 13 | from src.render_object import RenderObject 14 | from src.interface import Button, Interface 15 | from src.agent_parts.limb import Limb 16 | from src.ground import * 17 | from src.agent_parts.vision import Vision 18 | from src.agent_parts.rectangle import Point 19 | from src.agent_parts.creature import Creature 20 | 21 | #To do : unclick the only_one_simultaneously_buttons when unpausing 22 | 23 | def main(): 24 | # Initialize Pygame and Pymunk 25 | pygame.init() 26 | screen_width, screen_height = SCREEN_WIDTH, SCREEN_HEIGHT 27 | screen = pygame.display.set_mode((screen_width, screen_height)) 28 | pygame.display.set_caption("Pymunk Rectangle Physics") 29 | interface = Interface() 30 | 31 | 32 | # Track whether physics is on or off 33 | physics_on = False 34 | make_limb_mode = False 35 | make_motorjoint_mode = False 36 | 37 | # Track the physics value 38 | physics_value = 0 39 | 40 | def handle_physics(): 41 | nonlocal physics_on 42 | nonlocal physics_value 43 | physics_value = 1/60.0 if physics_value == 0 else 0 44 | physics_on = True if physics_value != 0 else False 45 | print("Physics Enabled" if physics_value != 0 else "Physics Disabled") 46 | 47 | font = pygame.font.Font(None, 20) 48 | 49 | pause_button = Button( 50 | text="Pause", 51 | pos=(10, 10), 52 | width=100, 53 | height=30, 54 | font=font, 55 | color=(70, 130, 180), 56 | hover_color=(100, 149, 237), 57 | text_color=(255, 255, 255), 58 | active_color=(200, 100, 100), 59 | callback=handle_physics 60 | ) 61 | 62 | 63 | def make_limb(): 64 | nonlocal make_limb_mode 65 | make_limb_mode = not make_limb_mode 66 | 67 | limb_button = Button( 68 | text="Add limb", 69 | pos=(10, 50), 70 | width=100, 71 | height=30, 72 | font=font, 73 | color=(70, 130, 180), 74 | hover_color=(100, 149, 237), 75 | text_color=(255, 255, 255), 76 | active_color=(200, 100, 100), 77 | callback=make_limb 78 | ) 79 | 80 | make_motorjoint_mode = False 81 | def add_motorjoint(): 82 | nonlocal make_motorjoint_mode 83 | make_motorjoint_mode = not make_motorjoint_mode 84 | 85 | motorjoint_button = Button( 86 | text="Add joint", 87 | pos=(10, 90), 88 | width=100, 89 | height=30, 90 | font=font, 91 | color=(70, 130, 180), 92 | hover_color=(100, 149, 237), 93 | text_color=(255, 255, 255), 94 | active_color=(200, 100, 100), 95 | callback=add_motorjoint 96 | ) 97 | 98 | interface.add_button(pause_button) 99 | interface.add_only_one_simultaneously_buttons(limb_button) 100 | interface.add_only_one_simultaneously_buttons(motorjoint_button) 101 | 102 | 103 | 104 | # Set up the Pymunk space 105 | space = pymunk.Space() 106 | space.gravity = (0, 981) # Gravity pointing downward 107 | 108 | environment = Environment(screen, space) 109 | environment.ground_type = GroundType.BASIC_GROUND 110 | 111 | vision: Vision = Vision(Point(0,0)) 112 | creature = Creature(space, vision) 113 | 114 | # Add limbs to the creature 115 | limb1 = creature.add_limb(100, 20, (300, 300), mass=1) 116 | limb2 = creature.add_limb(100, 20, (350, 300), mass=3) 117 | limb3 = creature.add_limb(80, 40, (400, 300), mass=5) 118 | 119 | # Add motors between limbs 120 | creature.add_motor_on_limbs(limb1, limb2, (325, 300)) 121 | creature.add_motor_on_limbs(limb2, limb3, (375, 300)) 122 | 123 | #dragging creature properties 124 | dragging = False 125 | dragged_limb = None 126 | drag_offset = [] 127 | 128 | # creating rectangles properties 129 | start_pos = None 130 | end_pos = None 131 | limbs_hovered = [] 132 | 133 | clock = pygame.time.Clock() 134 | 135 | running = True 136 | while running: 137 | for event in pygame.event.get(): 138 | if event.type == QUIT: 139 | running = False 140 | interface.handle_events(event) 141 | 142 | if event.type == pygame.KEYDOWN: 143 | if event.key == pygame.K_LEFT: 144 | print("Left arrow pressed") 145 | if event.key == pygame.K_RIGHT: 146 | print("Right arrow pressed") 147 | if event.key == pygame.K_SPACE: 148 | handle_physics() 149 | elif event.type == pygame.MOUSEBUTTONDOWN: 150 | active_button = interface.handle_only_one_function(event) 151 | 152 | # To make it possible to only click one of the only_one_simultaneously_buttons 153 | if not physics_on and active_button: 154 | if active_button.text == "Add limb": 155 | active_button.is_clicked(event) 156 | print("Limb creation mode activated and motor joint creation mode deactivated") 157 | elif active_button.text == "Add joint": 158 | active_button.is_clicked(event) 159 | print("Motor joint creation mode activated and limb creation mode deactivated") 160 | 161 | elif not active_button and not physics_on: 162 | mouse_x, mouse_y = event.pos 163 | mouse_pos = (mouse_x, mouse_y) 164 | # List of limbs to make motorjoint on 165 | limbs_hovered = [] 166 | # For dragging creature: Check if the mouse is over any limb 167 | if not make_limb_mode and not make_motorjoint_mode: 168 | for limb in creature.limbs: 169 | if limb.contains_point(mouse_pos): 170 | dragging = True 171 | dragged_limb = limb 172 | creature.start_dragging(dragged_limb) 173 | drag_offset = (limb.body.position.x - mouse_x, limb.body.position.y - mouse_y) 174 | #limbs_hovered.append(limb) 175 | break 176 | # For creating rectangles 177 | elif make_limb_mode: 178 | start_pos = mouse_pos 179 | # For creating motorjoint 180 | if make_motorjoint_mode: 181 | for limb in creature.limbs: 182 | if limb.contains_point(mouse_pos) and limb not in limbs_hovered: 183 | limbs_hovered.append(limb) 184 | print(limbs_hovered) 185 | # Ensure exactly two limbs are selected before creating the motor 186 | if len(limbs_hovered) == 2: 187 | limb_1 = limbs_hovered[0] 188 | limb_2 = limbs_hovered[1] 189 | 190 | creature.add_motor_on_limbs(limb_1, limb_2, mouse_pos) 191 | print("Motor joint created between limbs!") 192 | limbs_hovered.clear() 193 | else: 194 | limbs_hovered.clear() 195 | 196 | # If Pause button is clicked and any only_one_simultaneously_buttons are active, deactivate the active button 197 | elif not active_button: 198 | if interface.any_active_only_one_simultaneously_buttons_active(): 199 | interface.active_button.deactivate() 200 | print("Deactivated all mutually exclusive buttons due to Pause") 201 | 202 | elif event.type == MOUSEMOTION and make_limb_mode: 203 | mouse_x, mouse_y = event.pos 204 | mouse_pos = (mouse_x, mouse_y) 205 | end_pos = mouse_pos 206 | 207 | elif event.type == pygame.MOUSEBUTTONUP: 208 | dragging = False 209 | if make_limb_mode and start_pos and end_pos: 210 | width = abs(start_pos[0] - end_pos[0]) 211 | height = abs(start_pos[1] - end_pos[1]) 212 | position = ((start_pos[0] + end_pos[0]) / 2, (start_pos[1] + end_pos[1]) / 2) 213 | limb = creature.add_limb(width, height, position) 214 | 215 | # Reset start and end positions 216 | start_pos = None 217 | end_pos = None 218 | 219 | 220 | space.step(physics_value) 221 | 222 | if dragging and dragged_limb and not make_limb_mode: 223 | mouse_x, mouse_y = pygame.mouse.get_pos() 224 | new_position = (mouse_x + drag_offset[0], mouse_y + drag_offset[1]) 225 | creature.update_creature_position(dragged_limb, new_position) 226 | 227 | screen.fill((135, 206, 235)) 228 | if(physics_on): 229 | environment.update() 230 | environment.render() 231 | 232 | #creature.set_joint_rates([random.random()*2, random.random()*2]) 233 | # Render the creature 234 | creature.render(screen) 235 | interface.render(screen) 236 | 237 | 238 | 239 | clock.tick(60) 240 | 241 | pygame.display.flip() 242 | 243 | pygame.quit() 244 | 245 | 246 | if __name__ == "__main__": 247 | main() -------------------------------------------------------------------------------- /src/runner_display.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pygame 3 | import pymunk 4 | import os 5 | from src.genetic_algorithm import GeneticAlgorithm 6 | from src.agent_parts.vision import Vision 7 | from src.render_object import RenderObject 8 | from src.interface import Button, Interface 9 | from src.agent_parts.limb import Limb 10 | from src.globals import FONT_SIZE, SEGMENT_WIDTH, BLACK, RED 11 | from src.agent_parts.rectangle import Point 12 | from src.environment import Environment, GroundType 13 | from src.agent_parts.creature import Creature 14 | from src.NEATnetwork import NEATNetwork 15 | from src.genome import Genome 16 | from src.interface import Button 17 | from pygame_widgets.dropdown import Dropdown 18 | import pygame_widgets 19 | from src.globals import ( 20 | SCREEN_WIDTH, 21 | SCREEN_HEIGHT, 22 | POPULATION_SIZE, 23 | SPECIATION_THRESHOLD, 24 | NUM_GENERATIONS, 25 | SIMULATION_STEPS, 26 | ) 27 | 28 | 29 | def get_saved_file_paths() -> list[str]: 30 | """ 31 | Returns a list of paths to saved genome files. 32 | """ 33 | return [ 34 | os.path.join("models/", f) for f in os.listdir("models/") if f.endswith(".json") 35 | ] 36 | 37 | 38 | def display_genome_run(genome: Genome): 39 | # Initialize Pygame display for visualization 40 | screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) 41 | pygame.display.set_caption("NEAT Simulation") 42 | clock = pygame.time.Clock() 43 | interface = Interface() 44 | 45 | # Initialize Pymunk space and environment for visualization 46 | space = pymunk.Space() 47 | space.gravity = (0, 981) 48 | environment = Environment(screen, space) 49 | environment.ground_type = GroundType.BASIC_GROUND 50 | font = pygame.font.Font(None, 20) 51 | train_enabled = False 52 | display_dropdown = False 53 | 54 | save_button = Button( 55 | pos=(10, SCREEN_HEIGHT - 100), 56 | width=80, 57 | height=40, 58 | color=(0, 200, 0), 59 | text="Save", 60 | text_color=(255, 255, 255), 61 | hover_color=(0, 255, 0), 62 | active_color=(0, 100, 0), 63 | font=font, 64 | callback=lambda: print("Load button clicked"), 65 | ) 66 | 67 | train_button = Button( 68 | pos=(10, SCREEN_HEIGHT - 150), 69 | width=80, 70 | height=40, 71 | color=(0, 200, 0), 72 | text="Train", 73 | text_color=(255, 255, 255), 74 | hover_color=(0, 255, 0), 75 | active_color=(0, 100, 0), 76 | font=font, 77 | callback=lambda: (train_enabled := True), 78 | ) 79 | interface.add_button(save_button) 80 | interface.add_button(train_button) 81 | 82 | choices = get_saved_file_paths() 83 | dropdown = Dropdown( 84 | screen, 85 | 120, 86 | 10, 87 | 100, 88 | 50, 89 | name="Load Genome", 90 | choices=choices, 91 | borderRadius=3, 92 | colour=pygame.Color("green"), 93 | values=choices, 94 | direction="down", 95 | textHAlign="left", 96 | ) 97 | 98 | if genome: 99 | network = NEATNetwork(genome) 100 | vision = Vision(Point(0, 0)) 101 | creature = Creature(space, vision) 102 | limb1 = creature.add_limb(100, 20, (300, 300), mass=1) 103 | limb2 = creature.add_limb(100, 20, (350, 300), mass=3) 104 | limb3 = creature.add_limb(80, 40, (400, 300), mass=5) 105 | 106 | # Add motors between limbs 107 | creature.add_motor_on_limbs(limb1, limb2, (325, 300)) 108 | creature.add_motor_on_limbs(limb2, limb3, (375, 300)) 109 | 110 | running = True 111 | while running: 112 | events = pygame.event.get() 113 | for event in events: 114 | if event.type == pygame.QUIT: 115 | running = False 116 | interface.handle_events(event) 117 | 118 | if genome: 119 | # Prepare inputs 120 | inputs = [] 121 | inputs.extend( 122 | [ 123 | creature.vision.get_near_periphery().x, 124 | creature.vision.get_near_periphery().y, 125 | creature.vision.get_far_periphery().x, 126 | creature.vision.get_far_periphery().y, 127 | ] 128 | ) 129 | inputs.extend(creature.get_joint_rates()) 130 | for limb in creature.limbs: 131 | inputs.extend([limb.body.position.x, limb.body.position.y]) 132 | 133 | # Ensure inputs match the expected number 134 | inputs = np.array(inputs) 135 | if len(inputs) != genome.num_inputs: 136 | # Handle input size mismatch if necessary 137 | # For simplicity, we'll pad with zeros or truncate 138 | if len(inputs) < genome.num_inputs: 139 | inputs = np.pad( 140 | inputs, (0, genome.num_inputs - len(inputs)), "constant" 141 | ) 142 | else: 143 | inputs = inputs[: genome.num_inputs] 144 | 145 | outputs = network.forward(inputs) 146 | creature.set_joint_rates(outputs) 147 | 148 | creature.vision.update( 149 | Point( 150 | creature.limbs[0].body.position.x, creature.limbs[0].body.position.y 151 | ), 152 | environment.ground, 153 | environment.offset, 154 | ) 155 | 156 | # Step the physics 157 | space.step(1 / 60.0) 158 | 159 | # Move all the bodies in the space as much as the creature has moved 160 | for body in space.bodies: 161 | creature_offset = creature.limbs[0].body.position.x 162 | body.position = (body.position.x - creature_offset / 100, body.position.y) 163 | 164 | # Render everything 165 | screen.fill((135, 206, 235)) 166 | environment.update() 167 | environment.render() 168 | interface.render(screen) 169 | pygame_widgets.update(events) 170 | 171 | if genome: 172 | creature.render(screen) 173 | 174 | network_position = (SCREEN_WIDTH - 350, 50) 175 | network_size = (300, 300) 176 | draw_neural_network( 177 | genome, screen, position=network_position, size=network_size 178 | ) 179 | # Add text with the fitness value and current x position 180 | font = pygame.font.Font(None, FONT_SIZE) 181 | fitness_text = font.render(f"Fitness: {genome.fitness:.2f}", True, BLACK) 182 | x_pos_text = font.render( 183 | f"X Position: {creature.limbs[0].body.position.x:.2f}", True, BLACK 184 | ) 185 | screen.blit(fitness_text, (10, 10)) 186 | screen.blit(x_pos_text, (10, 30)) 187 | 188 | pygame.display.flip() 189 | clock.tick(60) 190 | 191 | pygame.quit() 192 | 193 | 194 | def draw_neural_network(genome: Genome, screen, position=(0, 0), size=(300, 300)): 195 | """ 196 | Draws the neural network represented by the genome onto the Pygame screen. 197 | 198 | :param genome: The Genome object containing nodes and connections. 199 | :param screen: The Pygame surface to draw on. 200 | :param position: The (x, y) position of the top-left corner where to draw the network. 201 | :param size: The (width, height) size of the area to draw the network. 202 | """ 203 | x, y = position 204 | width, height = size 205 | 206 | # Get nodes by type 207 | input_nodes = [node for node in genome.nodes if node.node_type == "input"] 208 | hidden_nodes = [node for node in genome.nodes if node.node_type == "hidden"] 209 | output_nodes = [node for node in genome.nodes if node.node_type == "output"] 210 | 211 | # Assign positions to nodes 212 | node_positions = {} 213 | 214 | # Vertical spacing 215 | layer_nodes = [input_nodes, hidden_nodes, output_nodes] 216 | max_layer_nodes = max(len(layer) for layer in layer_nodes) 217 | node_radius = 10 218 | vertical_spacing = height / (max_layer_nodes + 1) 219 | 220 | # Horizontal positions for layers 221 | num_layers = 3 222 | layer_x_positions = [x + width * i / (num_layers - 1) for i in range(num_layers)] 223 | 224 | # Position nodes in each layer 225 | for layer_idx, nodes in enumerate(layer_nodes): 226 | layer_x = layer_x_positions[layer_idx] 227 | num_nodes = len(nodes) 228 | for idx, node in enumerate(nodes): 229 | # Center nodes vertically 230 | node_y = y + (idx + 1) * height / (num_nodes + 1) 231 | node_positions[node.id] = (layer_x, node_y) 232 | 233 | # Draw connections 234 | for conn in genome.connections: 235 | if conn.enabled: 236 | in_pos = node_positions.get(conn.in_node) 237 | out_pos = node_positions.get(conn.out_node) 238 | if in_pos and out_pos: 239 | weight = conn.weight 240 | # Color code based on weight 241 | color = (0, 0, 255) if weight > 0 else (255, 0, 0) 242 | # Normalize weight for thickness 243 | thickness = max(1, int(abs(weight) * 2)) 244 | pygame.draw.line(screen, color, in_pos, out_pos, thickness) 245 | 246 | # Draw nodes 247 | for node_id, pos in node_positions.items(): 248 | node = next((n for n in genome.nodes if n.id == node_id), None) 249 | if node: 250 | if node.node_type == "input": 251 | color = (0, 255, 0) # Green 252 | elif node.node_type == "output": 253 | color = (255, 165, 0) # Orange 254 | else: 255 | color = (211, 211, 211) # Light Gray 256 | pygame.draw.circle(screen, color, (int(pos[0]), int(pos[1])), node_radius) 257 | pygame.draw.circle( 258 | screen, (0, 0, 0), (int(pos[0]), int(pos[1])), node_radius, 1 259 | ) 260 | -------------------------------------------------------------------------------- /docs/Architecture/requirements.md: -------------------------------------------------------------------------------- 1 | # Architectural Drivers / Architectural Significant Requirements 2 | 3 | This document outlines the architectural drivers and significant requirements for the CrawlAI system. It includes both functional and non-functional (quality) requirements that define the system's behavior, performance, and operational characteristics. The goal is to ensure that TutorAI meets the needs of its users and [Stakeholders](stakeholders.md) while maintaining a high standard of quality. 4 | 5 | ## Functional Requirements 6 | 7 | _Functional requirements define the specific behaviors, actions, and functionalities that the TutorAI system must provide to its users. They describe what the system will do under various conditions, detail the operations and activities the system must be capable of performing, and outline the explicit services it should deliver._ 8 | 9 | The functional requirements are divided into three different priorities: 10 | | Priority | Description | 11 | |----------|-------------| 12 | | High | These requirements are essential for the system to function properly; without these, there will be a significant impact on the system's ability to provide an enjoyable experience. | 13 | | Medium | These requirements are important but not critical for the system to function properly. | 14 | | Low | These requirements are nice-to-have features that would enhance the system but are not essential for its core functionality. | 15 | 16 | All functional requirements are listed below: 17 | 18 | ## GENETIC ALGORITHM 19 | 20 | | ID | Requirement Description | Priority | 21 | | ----- | ------------------------------------------------------------------------------------------------------------------------ | -------- | 22 | | FR1.1 | The system must allow structural mutations to add a link between two neurons | high | 23 | | FR1.2 | The system must allow structural mutations to remove an existing link between two neurons | high | 24 | | FR1.3 | The system must allow structural mutations to add new hidden neuron. | high | 25 | | FR1.4 | The system must allow structural mutations to remove existing hidden neuron | high | 26 | | FR1.5 | A new link/synapse when a new hidden neuron is created must have a weight of 1, so new agent won't terminate prematurely | high | 27 | | FR1.6 | The system must allow non-structural mutations to update values in both neurons and synapses | high | 28 | | FR1.7 | The system must allow interspecies crossover | high | 29 | | FR1.8 | The system must allow backpropagation for machine learning | low | 30 | 31 | ## ENVIROMENT 32 | 33 | | ID | Requirement Description | Priority | 34 | | ----- | ------------------------------------------------------------------------------------------------- | -------- | 35 | | FR2.1 | The enviroment should indicate fintness to the agents depending on how far they are able to move. | high | 36 | | FR2.2 | The enviroment should kill agents that are slow. | high | 37 | | FR2.3 | The killing mechanism must be correctly callibrated in order to allow stable evolution. | medium | 38 | | FR2.4 | The enviroment should contain a hilly ground | medium | 39 | | FR2.5 | The enviroment must make agents compete with each other | high | 40 | | FR2.6 | The enviroment must contain physics | high | 41 | 42 | ## AGENTS 43 | 44 | | ID | Requirement Description | Priority | 45 | | ----- | --------------------------------------------------------------------------------------- | -------- | 46 | | FR3.1 | An agent must be able to see the ground | high | 47 | | FR3.2 | An agent must be able to know the positions of each limb | high | 48 | | FR3.3 | An agent must be able to act upon the enviroment through actuators | high | 49 | | FR3.4 | An agent must have healthpoints | medium | 50 | | FR3.5 | An agent must have vital oragans that can be damaged if agent falls. | medium | 51 | | FR3.6 | An agent must have skin that can be damaged if dragged on the ground too much | medium | 52 | | FR3.7 | The health of an agent plays a role in fitness, and is part of the performence measure. | medium | 53 | | FR3.8 | An agent must be able to be exported and put into another simulation | low | 54 | 55 | ## DISPLAY 56 | 57 | | ID | Requirement Description | Priority | 58 | | ----- | -------------------------------------------------------------------- | -------- | 59 | | FR3.1 | The neural network of a given agent must be displayed | high | 60 | | FR3.2 | A user must be able to see the agents walking for each generation | high | 61 | | FR3.3 | A user must be able to see the family tree of the agents | low | 62 | | FR3.4 | A user must be able to see what agents are killed after a generation | low | 63 | | FR3.5 | A name is displayed for each species | low | 64 | 65 | ## **Quality Attributes** 66 | 67 | _Quality attributes are the system's non-functional requirements that specify the system's operational characteristics. They define the system's behavior, performance, and other qualities that are not directly related to the system's functionality._ 68 | 69 | ### **Usability** 70 | 71 | | ID | Requirement Description | Priority | 72 | | --- | ------------------------------------------------------------------------------------------------------------------------------- | -------- | 73 | | U1 | The system must have an intuitive interface that allows users to understand essential functions within 30 seconds. | High | 74 | | U2 | The system must comply with WCAG 2.1 Level AA guidelines to ensure accessibility for users with disabilities. | Low | 75 | | U3 | The system must enable a user to skip the display of multiple generations. | Medium | 76 | | U4 | The display should show enough of the process to allow a user to understand a bit of what is going on in the genetic algorithm. | Medium | 77 | | U5 | Instruction manual will be accessible, so that a user may understand how to navigate the system. | Low | 78 | 79 | ### **Performance** 80 | 81 | | ID | Requirement Description | Priority | 82 | | --- | -------------------------------------------------------------------------------------------------------------------------------- | -------- | 83 | | P1 | The system should be fast enough so that a user can see the progress of the agents over multiple generations. | High | 84 | | P2 | The system must allow neural networks with a reasonable size the evolve quickly, in less than 5 seconds between each generation. | High | 85 | 86 | ### **Deployment** 87 | 88 | | ID | Requirement Description | Priority | 89 | | --- | --------------------------------------------------------------------------------------------------------------------------------- | -------- | 90 | | D1 | The system must enable straightforward deployment processes for updates and new features that introduce zero downtime or defects. | Low | 91 | | D2 | The system must support automated deployment to streamline the release process. | Low | 92 | 93 | ### **Testability** 94 | 95 | | ID | Requirement Description | Priority | 96 | | --- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | 97 | | T1 | The system must be designed to allow efficient testing of new features and updates to ensure functionality without extensive manual intervention. | High | 98 | | T2 | The system must have a test suite that covers all essential features | High | 99 | | T3 | The system must be able to mock external services for testing | Medium | 100 | 101 | ### **Modifiability** 102 | 103 | | ID | Requirement Description | Priority | 104 | | --- | ------------------------------------------------------------------------------------------------------------------------------- | -------- | 105 | | M1 | The system must be designed to allow for easy modification and extension of features without significant rework or refactoring. | High | 106 | | M2 | The system must be able to change COTS (Commercial Off-The-Shelf) components with only local changes. | High | 107 | | M3 | The system must be able to get new functionalities without much refactoring of existant code. | High | 108 | | M4 | The system must be able to easily change database without any side effects | High | 109 | 110 | ### **Safety** 111 | 112 | | ID | Requirement Description | Priority | 113 | | --- | --------------------------------------------------------------------------------------- | -------- | 114 | | Sa1 | The system should not display any harmfull ideas or language, nor intentionallyu upset creationalists. | Low | 115 | 116 | ## **Business Requirements** 117 | 118 | _Business requirements are the high-level needs of the business that the system must meet to fulfill its purpose. They define the system's strategic goals, objectives, and constraints that guide the system's development and operation._ 119 | 120 | ### **Learning** 121 | 122 | | ID | Requirement Description | Priority | 123 | | ----- | ----------------------------------------------------------------- | -------- | 124 | | BR1.1 | A user should learn about genetic algorithms by using the program | High | 125 | 126 | ### **Compliance and Standards** 127 | 128 | | ID | Requirement Description | Priority | 129 | | ----- | ----------------------------------------- | -------- | 130 | | BR2.1 | The system must compy with copyright laws | High | 131 | 132 | ### **Cost Management** 133 | 134 | | ID | Requirement Description | Priority | 135 | | ----- | ------------------------------------------------------------------------------ | -------- | 136 | | BR3.1 | The system should not cost much to run in case of necessity of cloud computing | High | 137 | -------------------------------------------------------------------------------- /models/best_genome2961_555819068663.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 300, 3 | "fitness": 2961.555819068663, 4 | "adjusted_fitness": 0.0, 5 | "species": 1, 6 | "num_inputs": 12, 7 | "num_outputs": 2, 8 | "nodes": [ 9 | { 10 | "id": 0, 11 | "node_type": "input" 12 | }, 13 | { 14 | "id": 1, 15 | "node_type": "input" 16 | }, 17 | { 18 | "id": 2, 19 | "node_type": "input" 20 | }, 21 | { 22 | "id": 3, 23 | "node_type": "input" 24 | }, 25 | { 26 | "id": 4, 27 | "node_type": "input" 28 | }, 29 | { 30 | "id": 5, 31 | "node_type": "input" 32 | }, 33 | { 34 | "id": 6, 35 | "node_type": "input" 36 | }, 37 | { 38 | "id": 7, 39 | "node_type": "input" 40 | }, 41 | { 42 | "id": 8, 43 | "node_type": "input" 44 | }, 45 | { 46 | "id": 9, 47 | "node_type": "input" 48 | }, 49 | { 50 | "id": 10, 51 | "node_type": "input" 52 | }, 53 | { 54 | "id": 11, 55 | "node_type": "input" 56 | }, 57 | { 58 | "id": 12, 59 | "node_type": "output" 60 | }, 61 | { 62 | "id": 13, 63 | "node_type": "output" 64 | } 65 | ], 66 | "connections": [ 67 | { 68 | "in_node": 0, 69 | "out_node": 12, 70 | "weight": 0.6505565729355962, 71 | "innovation_number": 1, 72 | "enabled": true 73 | }, 74 | { 75 | "in_node": 0, 76 | "out_node": 13, 77 | "weight": -0.6409589342404067, 78 | "innovation_number": 2, 79 | "enabled": true 80 | }, 81 | { 82 | "in_node": 1, 83 | "out_node": 12, 84 | "weight": 0.24786502034770397, 85 | "innovation_number": 3, 86 | "enabled": true 87 | }, 88 | { 89 | "in_node": 1, 90 | "out_node": 13, 91 | "weight": 0.9496299598858728, 92 | "innovation_number": 4, 93 | "enabled": true 94 | }, 95 | { 96 | "in_node": 2, 97 | "out_node": 12, 98 | "weight": 0.8575494491995486, 99 | "innovation_number": 5, 100 | "enabled": true 101 | }, 102 | { 103 | "in_node": 2, 104 | "out_node": 13, 105 | "weight": -0.9541494769741579, 106 | "innovation_number": 6, 107 | "enabled": true 108 | }, 109 | { 110 | "in_node": 3, 111 | "out_node": 12, 112 | "weight": 0.25832724159779397, 113 | "innovation_number": 7, 114 | "enabled": true 115 | }, 116 | { 117 | "in_node": 3, 118 | "out_node": 13, 119 | "weight": 0.32665223127701193, 120 | "innovation_number": 8, 121 | "enabled": true 122 | }, 123 | { 124 | "in_node": 4, 125 | "out_node": 12, 126 | "weight": -0.14602480462281875, 127 | "innovation_number": 9, 128 | "enabled": true 129 | }, 130 | { 131 | "in_node": 4, 132 | "out_node": 13, 133 | "weight": -0.0930237277044812, 134 | "innovation_number": 10, 135 | "enabled": true 136 | }, 137 | { 138 | "in_node": 5, 139 | "out_node": 12, 140 | "weight": -0.15709101577427242, 141 | "innovation_number": 11, 142 | "enabled": true 143 | }, 144 | { 145 | "in_node": 5, 146 | "out_node": 13, 147 | "weight": -0.19043162223479992, 148 | "innovation_number": 12, 149 | "enabled": true 150 | }, 151 | { 152 | "in_node": 6, 153 | "out_node": 12, 154 | "weight": 0.77380331937415, 155 | "innovation_number": 13, 156 | "enabled": true 157 | }, 158 | { 159 | "in_node": 6, 160 | "out_node": 13, 161 | "weight": 0.7134400684555466, 162 | "innovation_number": 14, 163 | "enabled": true 164 | }, 165 | { 166 | "in_node": 7, 167 | "out_node": 12, 168 | "weight": 0.834147287464563, 169 | "innovation_number": 15, 170 | "enabled": true 171 | }, 172 | { 173 | "in_node": 7, 174 | "out_node": 13, 175 | "weight": 0.7612711698349568, 176 | "innovation_number": 16, 177 | "enabled": true 178 | }, 179 | { 180 | "in_node": 8, 181 | "out_node": 12, 182 | "weight": 0.860356334071519, 183 | "innovation_number": 17, 184 | "enabled": true 185 | }, 186 | { 187 | "in_node": 8, 188 | "out_node": 13, 189 | "weight": 0.9857323859275802, 190 | "innovation_number": 18, 191 | "enabled": true 192 | }, 193 | { 194 | "in_node": 9, 195 | "out_node": 12, 196 | "weight": 0.4355401444585201, 197 | "innovation_number": 19, 198 | "enabled": true 199 | }, 200 | { 201 | "in_node": 9, 202 | "out_node": 13, 203 | "weight": -0.8376963203932009, 204 | "innovation_number": 20, 205 | "enabled": true 206 | }, 207 | { 208 | "in_node": 10, 209 | "out_node": 12, 210 | "weight": 0.42316756340284134, 211 | "innovation_number": 21, 212 | "enabled": true 213 | }, 214 | { 215 | "in_node": 10, 216 | "out_node": 13, 217 | "weight": 0.10085813792480325, 218 | "innovation_number": 22, 219 | "enabled": true 220 | }, 221 | { 222 | "in_node": 11, 223 | "out_node": 12, 224 | "weight": -0.9613371530723607, 225 | "innovation_number": 23, 226 | "enabled": true 227 | }, 228 | { 229 | "in_node": 11, 230 | "out_node": 13, 231 | "weight": -0.8908914616358357, 232 | "innovation_number": 24, 233 | "enabled": true 234 | }, 235 | { 236 | "in_node": 0, 237 | "out_node": 12, 238 | "weight": -0.8166283675648227, 239 | "innovation_number": 1, 240 | "enabled": true 241 | }, 242 | { 243 | "in_node": 0, 244 | "out_node": 13, 245 | "weight": -0.3936646884284223, 246 | "innovation_number": 2, 247 | "enabled": true 248 | }, 249 | { 250 | "in_node": 1, 251 | "out_node": 12, 252 | "weight": 0.8009318015188769, 253 | "innovation_number": 3, 254 | "enabled": true 255 | }, 256 | { 257 | "in_node": 1, 258 | "out_node": 13, 259 | "weight": -0.6543132931611231, 260 | "innovation_number": 4, 261 | "enabled": true 262 | }, 263 | { 264 | "in_node": 2, 265 | "out_node": 12, 266 | "weight": 1.1155335151842936, 267 | "innovation_number": 5, 268 | "enabled": true 269 | }, 270 | { 271 | "in_node": 2, 272 | "out_node": 13, 273 | "weight": 0.2188428303841611, 274 | "innovation_number": 6, 275 | "enabled": true 276 | }, 277 | { 278 | "in_node": 3, 279 | "out_node": 12, 280 | "weight": 0.37817094882500846, 281 | "innovation_number": 7, 282 | "enabled": true 283 | }, 284 | { 285 | "in_node": 3, 286 | "out_node": 13, 287 | "weight": -0.5640122555177873, 288 | "innovation_number": 8, 289 | "enabled": true 290 | }, 291 | { 292 | "in_node": 4, 293 | "out_node": 12, 294 | "weight": 0.6787340096063845, 295 | "innovation_number": 9, 296 | "enabled": true 297 | }, 298 | { 299 | "in_node": 4, 300 | "out_node": 13, 301 | "weight": 0.15896158568526148, 302 | "innovation_number": 10, 303 | "enabled": true 304 | }, 305 | { 306 | "in_node": 5, 307 | "out_node": 12, 308 | "weight": 0.5184001132748015, 309 | "innovation_number": 11, 310 | "enabled": true 311 | }, 312 | { 313 | "in_node": 5, 314 | "out_node": 13, 315 | "weight": -0.7013493467798726, 316 | "innovation_number": 12, 317 | "enabled": true 318 | }, 319 | { 320 | "in_node": 6, 321 | "out_node": 12, 322 | "weight": 0.8565433854812168, 323 | "innovation_number": 13, 324 | "enabled": true 325 | }, 326 | { 327 | "in_node": 6, 328 | "out_node": 13, 329 | "weight": -0.3984240948486488, 330 | "innovation_number": 14, 331 | "enabled": true 332 | }, 333 | { 334 | "in_node": 7, 335 | "out_node": 12, 336 | "weight": 0.8101229959672183, 337 | "innovation_number": 15, 338 | "enabled": true 339 | }, 340 | { 341 | "in_node": 7, 342 | "out_node": 13, 343 | "weight": -0.9235015289219627, 344 | "innovation_number": 16, 345 | "enabled": true 346 | }, 347 | { 348 | "in_node": 8, 349 | "out_node": 12, 350 | "weight": 0.6123175315168268, 351 | "innovation_number": 17, 352 | "enabled": true 353 | }, 354 | { 355 | "in_node": 8, 356 | "out_node": 13, 357 | "weight": -0.9272833155344143, 358 | "innovation_number": 18, 359 | "enabled": true 360 | }, 361 | { 362 | "in_node": 9, 363 | "out_node": 12, 364 | "weight": 0.9723202676034364, 365 | "innovation_number": 19, 366 | "enabled": true 367 | }, 368 | { 369 | "in_node": 9, 370 | "out_node": 13, 371 | "weight": -0.8254697886885598, 372 | "innovation_number": 20, 373 | "enabled": true 374 | }, 375 | { 376 | "in_node": 10, 377 | "out_node": 12, 378 | "weight": -0.3957299251514001, 379 | "innovation_number": 21, 380 | "enabled": true 381 | }, 382 | { 383 | "in_node": 10, 384 | "out_node": 13, 385 | "weight": 0.19957218583597164, 386 | "innovation_number": 22, 387 | "enabled": true 388 | }, 389 | { 390 | "in_node": 11, 391 | "out_node": 12, 392 | "weight": 0.9190201773916085, 393 | "innovation_number": 23, 394 | "enabled": true 395 | }, 396 | { 397 | "in_node": 11, 398 | "out_node": 13, 399 | "weight": -0.05350119317696818, 400 | "innovation_number": 24, 401 | "enabled": true 402 | } 403 | ] 404 | } -------------------------------------------------------------------------------- /models/best_genome3159_670865969072.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 300, 3 | "fitness": 3159.670865969072, 4 | "adjusted_fitness": 0.0, 5 | "species": 1, 6 | "num_inputs": 12, 7 | "num_outputs": 2, 8 | "nodes": [ 9 | { 10 | "id": 0, 11 | "node_type": "input" 12 | }, 13 | { 14 | "id": 1, 15 | "node_type": "input" 16 | }, 17 | { 18 | "id": 2, 19 | "node_type": "input" 20 | }, 21 | { 22 | "id": 3, 23 | "node_type": "input" 24 | }, 25 | { 26 | "id": 4, 27 | "node_type": "input" 28 | }, 29 | { 30 | "id": 5, 31 | "node_type": "input" 32 | }, 33 | { 34 | "id": 6, 35 | "node_type": "input" 36 | }, 37 | { 38 | "id": 7, 39 | "node_type": "input" 40 | }, 41 | { 42 | "id": 8, 43 | "node_type": "input" 44 | }, 45 | { 46 | "id": 9, 47 | "node_type": "input" 48 | }, 49 | { 50 | "id": 10, 51 | "node_type": "input" 52 | }, 53 | { 54 | "id": 11, 55 | "node_type": "input" 56 | }, 57 | { 58 | "id": 12, 59 | "node_type": "output" 60 | }, 61 | { 62 | "id": 13, 63 | "node_type": "output" 64 | } 65 | ], 66 | "connections": [ 67 | { 68 | "in_node": 0, 69 | "out_node": 12, 70 | "weight": 0.7943766324171901, 71 | "innovation_number": 1, 72 | "enabled": true 73 | }, 74 | { 75 | "in_node": 0, 76 | "out_node": 13, 77 | "weight": -0.04224633394565447, 78 | "innovation_number": 2, 79 | "enabled": true 80 | }, 81 | { 82 | "in_node": 1, 83 | "out_node": 12, 84 | "weight": 0.3978840066023306, 85 | "innovation_number": 3, 86 | "enabled": true 87 | }, 88 | { 89 | "in_node": 1, 90 | "out_node": 13, 91 | "weight": 0.14631903179148043, 92 | "innovation_number": 4, 93 | "enabled": true 94 | }, 95 | { 96 | "in_node": 2, 97 | "out_node": 12, 98 | "weight": 0.6002932680426663, 99 | "innovation_number": 5, 100 | "enabled": true 101 | }, 102 | { 103 | "in_node": 2, 104 | "out_node": 13, 105 | "weight": 0.16222485671338793, 106 | "innovation_number": 6, 107 | "enabled": true 108 | }, 109 | { 110 | "in_node": 3, 111 | "out_node": 12, 112 | "weight": 0.2776069499523419, 113 | "innovation_number": 7, 114 | "enabled": true 115 | }, 116 | { 117 | "in_node": 3, 118 | "out_node": 13, 119 | "weight": -0.41815058165264785, 120 | "innovation_number": 8, 121 | "enabled": true 122 | }, 123 | { 124 | "in_node": 4, 125 | "out_node": 12, 126 | "weight": -0.36300145606601353, 127 | "innovation_number": 9, 128 | "enabled": true 129 | }, 130 | { 131 | "in_node": 4, 132 | "out_node": 13, 133 | "weight": 0.7864591772599407, 134 | "innovation_number": 10, 135 | "enabled": true 136 | }, 137 | { 138 | "in_node": 5, 139 | "out_node": 12, 140 | "weight": 0.6991959843281608, 141 | "innovation_number": 11, 142 | "enabled": true 143 | }, 144 | { 145 | "in_node": 5, 146 | "out_node": 13, 147 | "weight": -0.37536021012175436, 148 | "innovation_number": 12, 149 | "enabled": true 150 | }, 151 | { 152 | "in_node": 6, 153 | "out_node": 12, 154 | "weight": 0.9397071771504046, 155 | "innovation_number": 13, 156 | "enabled": true 157 | }, 158 | { 159 | "in_node": 6, 160 | "out_node": 13, 161 | "weight": 0.578646875109122, 162 | "innovation_number": 14, 163 | "enabled": true 164 | }, 165 | { 166 | "in_node": 7, 167 | "out_node": 12, 168 | "weight": 0.742472564212002, 169 | "innovation_number": 15, 170 | "enabled": true 171 | }, 172 | { 173 | "in_node": 7, 174 | "out_node": 13, 175 | "weight": -0.0723594014543949, 176 | "innovation_number": 16, 177 | "enabled": true 178 | }, 179 | { 180 | "in_node": 8, 181 | "out_node": 12, 182 | "weight": -0.6945960815446226, 183 | "innovation_number": 17, 184 | "enabled": true 185 | }, 186 | { 187 | "in_node": 8, 188 | "out_node": 13, 189 | "weight": 0.526459994066288, 190 | "innovation_number": 18, 191 | "enabled": true 192 | }, 193 | { 194 | "in_node": 9, 195 | "out_node": 12, 196 | "weight": 0.20854785275743248, 197 | "innovation_number": 19, 198 | "enabled": true 199 | }, 200 | { 201 | "in_node": 9, 202 | "out_node": 13, 203 | "weight": 0.8787484598000457, 204 | "innovation_number": 20, 205 | "enabled": true 206 | }, 207 | { 208 | "in_node": 10, 209 | "out_node": 12, 210 | "weight": 0.6965334312151099, 211 | "innovation_number": 21, 212 | "enabled": true 213 | }, 214 | { 215 | "in_node": 10, 216 | "out_node": 13, 217 | "weight": 0.3787115258828375, 218 | "innovation_number": 22, 219 | "enabled": true 220 | }, 221 | { 222 | "in_node": 11, 223 | "out_node": 12, 224 | "weight": 0.41148251728200425, 225 | "innovation_number": 23, 226 | "enabled": true 227 | }, 228 | { 229 | "in_node": 11, 230 | "out_node": 13, 231 | "weight": -0.7730484231057435, 232 | "innovation_number": 24, 233 | "enabled": true 234 | }, 235 | { 236 | "in_node": 0, 237 | "out_node": 12, 238 | "weight": -0.29212548092165425, 239 | "innovation_number": 1, 240 | "enabled": true 241 | }, 242 | { 243 | "in_node": 0, 244 | "out_node": 13, 245 | "weight": -0.8915303915767279, 246 | "innovation_number": 2, 247 | "enabled": true 248 | }, 249 | { 250 | "in_node": 1, 251 | "out_node": 12, 252 | "weight": -0.46289464393089697, 253 | "innovation_number": 3, 254 | "enabled": true 255 | }, 256 | { 257 | "in_node": 1, 258 | "out_node": 13, 259 | "weight": -0.05379715539571128, 260 | "innovation_number": 4, 261 | "enabled": true 262 | }, 263 | { 264 | "in_node": 2, 265 | "out_node": 12, 266 | "weight": -0.1485672527280213, 267 | "innovation_number": 5, 268 | "enabled": true 269 | }, 270 | { 271 | "in_node": 2, 272 | "out_node": 13, 273 | "weight": 0.38177465483134254, 274 | "innovation_number": 6, 275 | "enabled": true 276 | }, 277 | { 278 | "in_node": 3, 279 | "out_node": 12, 280 | "weight": 0.9927663946738154, 281 | "innovation_number": 7, 282 | "enabled": true 283 | }, 284 | { 285 | "in_node": 3, 286 | "out_node": 13, 287 | "weight": 0.11335090028074535, 288 | "innovation_number": 8, 289 | "enabled": true 290 | }, 291 | { 292 | "in_node": 4, 293 | "out_node": 12, 294 | "weight": -0.843072840693569, 295 | "innovation_number": 9, 296 | "enabled": true 297 | }, 298 | { 299 | "in_node": 4, 300 | "out_node": 13, 301 | "weight": -0.9332937595331414, 302 | "innovation_number": 10, 303 | "enabled": true 304 | }, 305 | { 306 | "in_node": 5, 307 | "out_node": 12, 308 | "weight": 0.028565529696389813, 309 | "innovation_number": 11, 310 | "enabled": true 311 | }, 312 | { 313 | "in_node": 5, 314 | "out_node": 13, 315 | "weight": -0.6205912120846555, 316 | "innovation_number": 12, 317 | "enabled": true 318 | }, 319 | { 320 | "in_node": 6, 321 | "out_node": 12, 322 | "weight": 0.5880345235232711, 323 | "innovation_number": 13, 324 | "enabled": true 325 | }, 326 | { 327 | "in_node": 6, 328 | "out_node": 13, 329 | "weight": 0.8073259569983857, 330 | "innovation_number": 14, 331 | "enabled": true 332 | }, 333 | { 334 | "in_node": 7, 335 | "out_node": 12, 336 | "weight": 0.723497806561078, 337 | "innovation_number": 15, 338 | "enabled": true 339 | }, 340 | { 341 | "in_node": 7, 342 | "out_node": 13, 343 | "weight": -0.27817861636914754, 344 | "innovation_number": 16, 345 | "enabled": true 346 | }, 347 | { 348 | "in_node": 8, 349 | "out_node": 12, 350 | "weight": 0.7809263677774199, 351 | "innovation_number": 17, 352 | "enabled": true 353 | }, 354 | { 355 | "in_node": 8, 356 | "out_node": 13, 357 | "weight": -0.38689582633614883, 358 | "innovation_number": 18, 359 | "enabled": true 360 | }, 361 | { 362 | "in_node": 9, 363 | "out_node": 12, 364 | "weight": -0.44137406005592483, 365 | "innovation_number": 19, 366 | "enabled": true 367 | }, 368 | { 369 | "in_node": 9, 370 | "out_node": 13, 371 | "weight": -0.8755013348177043, 372 | "innovation_number": 20, 373 | "enabled": true 374 | }, 375 | { 376 | "in_node": 10, 377 | "out_node": 12, 378 | "weight": -0.12913086723683875, 379 | "innovation_number": 21, 380 | "enabled": true 381 | }, 382 | { 383 | "in_node": 10, 384 | "out_node": 13, 385 | "weight": -0.3620637635696735, 386 | "innovation_number": 22, 387 | "enabled": true 388 | }, 389 | { 390 | "in_node": 11, 391 | "out_node": 12, 392 | "weight": -0.0955921689090864, 393 | "innovation_number": 23, 394 | "enabled": true 395 | }, 396 | { 397 | "in_node": 11, 398 | "out_node": 13, 399 | "weight": -0.645272778400432, 400 | "innovation_number": 24, 401 | "enabled": true 402 | } 403 | ] 404 | } -------------------------------------------------------------------------------- /src/genome.py: -------------------------------------------------------------------------------- 1 | # src/genome.py 2 | 3 | import random 4 | from dataclasses import dataclass 5 | from typing import List 6 | from copy import deepcopy 7 | 8 | from src.globals import ( 9 | MUTATION_RATE_WEIGHT, 10 | MUTATION_RATE_CONNECTION, 11 | MUTATION_RATE_NODE, 12 | ) 13 | 14 | 15 | class Innovation: 16 | __instance = None 17 | _global_innovation_counter = 0 18 | _innovation_history = {} 19 | 20 | @staticmethod 21 | def get_instance(): 22 | if Innovation.__instance is None: 23 | Innovation() 24 | return Innovation.__instance 25 | 26 | def __init__(self): 27 | if Innovation.__instance is not None: 28 | raise Exception("This is a singleton.") 29 | else: 30 | Innovation.__instance = self 31 | 32 | def get_innovation_number(self, in_node, out_node): 33 | key = (in_node, out_node) 34 | if key in Innovation._innovation_history: 35 | return Innovation._innovation_history[key] 36 | else: 37 | Innovation._global_innovation_counter += 1 38 | Innovation._innovation_history[key] = Innovation._global_innovation_counter 39 | return Innovation._innovation_history[key] 40 | 41 | def to_dict(self): 42 | """Serialize the Innovation singleton.""" 43 | return { 44 | '_global_innovation_counter': self._global_innovation_counter, 45 | '_innovation_history': self._innovation_history 46 | } 47 | 48 | def from_dict(self, data): 49 | """Deserialize the Innovation singleton.""" 50 | self._global_innovation_counter = data['_global_innovation_counter'] 51 | self._innovation_history = data['_innovation_history'] 52 | 53 | 54 | @dataclass 55 | class Node: 56 | id: int 57 | node_type: str # 'input', 'hidden', 'output' 58 | 59 | def __hash__(self) -> int: 60 | return self.id 61 | 62 | 63 | @dataclass 64 | class Connection: 65 | in_node: int 66 | out_node: int 67 | weight: float 68 | innovation_number: int 69 | enabled: bool = True 70 | 71 | def change_enable(self, status: bool): 72 | self.enabled = status 73 | 74 | 75 | class Genome: 76 | def __init__(self, genome_id: int, num_inputs: int = 0, num_outputs: int = 0): 77 | self.id = genome_id 78 | self.fitness: float = 0.0 79 | self.nodes: List[Node] = [] 80 | self.connections: List[Connection] = [] 81 | self.species: int = 0 82 | self.adjusted_fitness: float = 0.0 83 | self.innovation = Innovation.get_instance() 84 | 85 | # Store number of inputs and outputs 86 | self.num_inputs = num_inputs 87 | self.num_outputs = num_outputs 88 | 89 | # Create input nodes 90 | for i in range(num_inputs): 91 | node = Node(id=i, node_type="input") 92 | self.nodes.append(node) 93 | 94 | # Create output nodes 95 | for i in range(num_outputs): 96 | node = Node(id=num_inputs + i, node_type="output") 97 | self.nodes.append(node) 98 | 99 | # Connect each input node to each output node with a random weight 100 | input_nodes = [n for n in self.nodes if n.node_type == "input"] 101 | output_nodes = [n for n in self.nodes if n.node_type == "output"] 102 | for in_node in input_nodes: 103 | for out_node in output_nodes: 104 | innovation_number = self.innovation.get_innovation_number( 105 | in_node.id, out_node.id 106 | ) 107 | connection = Connection( 108 | in_node=in_node.id, 109 | out_node=out_node.id, 110 | weight=random.uniform(-1.0, 1.0), 111 | innovation_number=innovation_number, 112 | ) 113 | self.connections.append(connection) 114 | 115 | def mutate_weights(self, delta: float = 0.1): 116 | """Mutate the weights of the connections.""" 117 | for conn in self.connections: 118 | if random.random() < 0.1: 119 | conn.weight = random.uniform(-1.0, 1.0) 120 | else: 121 | conn.weight += random.gauss(0, delta) 122 | 123 | def mutate_connections(self): 124 | """Add a new connection between two nodes.""" 125 | possible_in_nodes = list(self.nodes) 126 | possible_out_nodes = list(self.nodes) 127 | in_node = random.choice(possible_in_nodes) 128 | out_node = random.choice(possible_out_nodes) 129 | if in_node.id == out_node.id: 130 | return # Avoid self-loops 131 | # Check if connection already exists 132 | for conn in self.connections: 133 | if conn.in_node == in_node.id and conn.out_node == out_node.id: 134 | return 135 | innovation_number = self.innovation.get_innovation_number( 136 | in_node.id, out_node.id 137 | ) 138 | new_conn = Connection( 139 | in_node=in_node.id, 140 | out_node=out_node.id, 141 | weight=random.uniform(-1.0, 1.0), 142 | innovation_number=innovation_number, 143 | ) 144 | self.connections.append(new_conn) 145 | 146 | def mutate_nodes(self): 147 | """Add a new node by splitting an existing connection.""" 148 | if not self.connections: 149 | return 150 | con = random.choice(self.connections) 151 | if not con.enabled: 152 | return 153 | con.enabled = False 154 | new_node_id = max(node.id for node in self.nodes) + 1 155 | new_node = Node(id=new_node_id, node_type="hidden") 156 | self.nodes.append(new_node) 157 | 158 | innovation_number1 = self.innovation.get_innovation_number( 159 | con.in_node, new_node.id 160 | ) 161 | innovation_number2 = self.innovation.get_innovation_number( 162 | new_node.id, con.out_node 163 | ) 164 | 165 | con1 = Connection( 166 | in_node=con.in_node, 167 | out_node=new_node.id, 168 | weight=1.0, 169 | innovation_number=innovation_number1, 170 | ) 171 | con2 = Connection( 172 | in_node=new_node.id, 173 | out_node=con.out_node, 174 | weight=con.weight, 175 | innovation_number=innovation_number2, 176 | ) 177 | self.connections.append(con1) 178 | self.connections.append(con2) 179 | 180 | def mutate(self): 181 | """Apply mutations to the genome.""" 182 | 183 | if random.random() < MUTATION_RATE_WEIGHT: 184 | self.mutate_weights(delta=0.1) 185 | if random.random() < MUTATION_RATE_CONNECTION: 186 | self.mutate_connections() 187 | if random.random() < MUTATION_RATE_NODE: 188 | self.mutate_nodes() 189 | 190 | def compute_compatibility_distance(self, other, c1=1.0, c2=1.0, c3=0.4) -> float: 191 | """Calculate the genetic distance (delta) between two genomes.""" 192 | conn1 = {c.innovation_number: c for c in self.connections} 193 | conn2 = {c.innovation_number: c for c in other.connections} 194 | all_innovations = set(conn1.keys()).union(set(conn2.keys())) 195 | 196 | excess_genes = 0 197 | disjoint_genes = 0 198 | matching_genes = 0 199 | weight_difference_sum = 0 200 | 201 | N = max(len(conn1), len(conn2)) 202 | if N < 20: 203 | N = 1 # Avoid excessive normalization for small genomes 204 | 205 | max_innovation1 = max(conn1.keys(), default=0) 206 | max_innovation2 = max(conn2.keys(), default=0) 207 | 208 | for innovation_number in all_innovations: 209 | if innovation_number in conn1 and innovation_number in conn2: 210 | matching_genes += 1 211 | weight_difference_sum += abs( 212 | conn1[innovation_number].weight - conn2[innovation_number].weight 213 | ) 214 | elif innovation_number in conn1 or innovation_number in conn2: 215 | if innovation_number > max(max_innovation1, max_innovation2): 216 | excess_genes += 1 217 | else: 218 | disjoint_genes += 1 219 | 220 | average_weight_difference = ( 221 | (weight_difference_sum / matching_genes) if matching_genes > 0 else 0 222 | ) 223 | delta = ( 224 | (c1 * excess_genes / N) 225 | + (c2 * disjoint_genes / N) 226 | + (c3 * average_weight_difference) 227 | ) 228 | return delta 229 | 230 | def crossover(self, other): 231 | """Perform crossover between two genomes.""" 232 | # Assume self is the more fit parent 233 | child = Genome( 234 | genome_id=-1, # Temporary ID 235 | num_inputs=self.num_inputs, 236 | num_outputs=self.num_outputs, 237 | ) 238 | child.nodes = deepcopy(self.nodes) 239 | # Ensure all nodes from other are present 240 | for node in other.nodes: 241 | if node.id not in [n.id for n in child.nodes]: 242 | child.nodes.append(deepcopy(node)) 243 | 244 | # Inherit connections 245 | self_conn_dict = {conn.innovation_number: conn for conn in self.connections} 246 | other_conn_dict = {conn.innovation_number: conn for conn in other.connections} 247 | 248 | for innovation_number in set(self_conn_dict.keys()).union( 249 | other_conn_dict.keys() 250 | ): 251 | conn = None 252 | if ( 253 | innovation_number in self_conn_dict 254 | and innovation_number in other_conn_dict 255 | ): 256 | # Matching genes - randomly choose 257 | if random.random() < 0.5: 258 | conn = deepcopy(self_conn_dict[innovation_number]) 259 | else: 260 | conn = deepcopy(other_conn_dict[innovation_number]) 261 | elif innovation_number in self_conn_dict: 262 | # Excess or disjoint genes from the more fit parent 263 | conn = deepcopy(self_conn_dict[innovation_number]) 264 | else: 265 | # Excess or disjoint genes from the other parent 266 | conn = deepcopy(other_conn_dict[innovation_number]) 267 | 268 | if conn: 269 | child.connections.append(conn) 270 | 271 | return child 272 | 273 | def copy(self): 274 | """Create a deep copy of the genome.""" 275 | new_genome = Genome( 276 | genome_id=self.id, num_inputs=self.num_inputs, num_outputs=self.num_outputs 277 | ) 278 | new_genome.nodes = deepcopy(self.nodes) 279 | new_genome.connections = deepcopy(self.connections) 280 | new_genome.fitness = self.fitness 281 | new_genome.adjusted_fitness = self.adjusted_fitness 282 | new_genome.species = self.species 283 | return new_genome 284 | 285 | def __str__(self): 286 | return f"Genome ID: {self.id}, Fitness: {self.fitness}, Species: {self.species}, Adjusted Fitness: {self.adjusted_fitness}" 287 | 288 | def __repr__(self): 289 | return self.__str__() 290 | 291 | def __eq__(self, other): 292 | return self.id == other.id 293 | 294 | def __lt__(self, other): 295 | return self.fitness < other.fitness 296 | 297 | def to_dict(self): 298 | """Serialize the Genome object to a dictionary.""" 299 | return { 300 | 'id': self.id, 301 | 'fitness': self.fitness, 302 | 'adjusted_fitness': self.adjusted_fitness, 303 | 'species': self.species, 304 | 'num_inputs': self.num_inputs, 305 | 'num_outputs': self.num_outputs, 306 | 'nodes': [node.__dict__ for node in self.nodes], 307 | 'connections': [conn.__dict__ for conn in self.connections], 308 | } 309 | 310 | @classmethod 311 | def from_dict(cls, data): 312 | """Deserialize a Genome object from a dictionary.""" 313 | genome = cls( 314 | genome_id=data['id'], 315 | num_inputs=data['num_inputs'], 316 | num_outputs=data['num_outputs'] 317 | ) 318 | genome.fitness = data['fitness'] 319 | genome.adjusted_fitness = data['adjusted_fitness'] 320 | genome.species = data['species'] 321 | 322 | # Reconstruct nodes 323 | genome.nodes = [Node(**node_data) for node_data in data['nodes']] 324 | 325 | # Reconstruct connections 326 | genome.connections = [Connection(**conn_data) for conn_data in data['connections']] 327 | 328 | return genome 329 | -------------------------------------------------------------------------------- /models/best_genome2346_7973381259126.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 10029, 3 | "fitness": 2346.7973381259126, 4 | "adjusted_fitness": 0.0, 5 | "species": 1, 6 | "num_inputs": 12, 7 | "num_outputs": 2, 8 | "nodes": [ 9 | { 10 | "id": 0, 11 | "node_type": "input" 12 | }, 13 | { 14 | "id": 1, 15 | "node_type": "input" 16 | }, 17 | { 18 | "id": 2, 19 | "node_type": "input" 20 | }, 21 | { 22 | "id": 3, 23 | "node_type": "input" 24 | }, 25 | { 26 | "id": 4, 27 | "node_type": "input" 28 | }, 29 | { 30 | "id": 5, 31 | "node_type": "input" 32 | }, 33 | { 34 | "id": 6, 35 | "node_type": "input" 36 | }, 37 | { 38 | "id": 7, 39 | "node_type": "input" 40 | }, 41 | { 42 | "id": 8, 43 | "node_type": "input" 44 | }, 45 | { 46 | "id": 9, 47 | "node_type": "input" 48 | }, 49 | { 50 | "id": 10, 51 | "node_type": "input" 52 | }, 53 | { 54 | "id": 11, 55 | "node_type": "input" 56 | }, 57 | { 58 | "id": 12, 59 | "node_type": "output" 60 | }, 61 | { 62 | "id": 13, 63 | "node_type": "output" 64 | }, 65 | { 66 | "id": 14, 67 | "node_type": "hidden" 68 | } 69 | ], 70 | "connections": [ 71 | { 72 | "in_node": 0, 73 | "out_node": 12, 74 | "weight": 1.0073980846650035, 75 | "innovation_number": 1, 76 | "enabled": true 77 | }, 78 | { 79 | "in_node": 0, 80 | "out_node": 13, 81 | "weight": -0.2188981324096308, 82 | "innovation_number": 2, 83 | "enabled": true 84 | }, 85 | { 86 | "in_node": 1, 87 | "out_node": 12, 88 | "weight": 1.514767426860797, 89 | "innovation_number": 3, 90 | "enabled": true 91 | }, 92 | { 93 | "in_node": 1, 94 | "out_node": 13, 95 | "weight": -0.3168801318982616, 96 | "innovation_number": 4, 97 | "enabled": true 98 | }, 99 | { 100 | "in_node": 2, 101 | "out_node": 12, 102 | "weight": 0.6442569501638267, 103 | "innovation_number": 5, 104 | "enabled": true 105 | }, 106 | { 107 | "in_node": 2, 108 | "out_node": 13, 109 | "weight": -0.26644208926968527, 110 | "innovation_number": 6, 111 | "enabled": true 112 | }, 113 | { 114 | "in_node": 3, 115 | "out_node": 12, 116 | "weight": 0.8271680872646228, 117 | "innovation_number": 7, 118 | "enabled": true 119 | }, 120 | { 121 | "in_node": 3, 122 | "out_node": 13, 123 | "weight": 0.34427919406575486, 124 | "innovation_number": 8, 125 | "enabled": true 126 | }, 127 | { 128 | "in_node": 4, 129 | "out_node": 12, 130 | "weight": -0.07455680088985986, 131 | "innovation_number": 9, 132 | "enabled": true 133 | }, 134 | { 135 | "in_node": 4, 136 | "out_node": 13, 137 | "weight": -0.3207589465708075, 138 | "innovation_number": 10, 139 | "enabled": true 140 | }, 141 | { 142 | "in_node": 5, 143 | "out_node": 12, 144 | "weight": 0.8659258131550592, 145 | "innovation_number": 11, 146 | "enabled": true 147 | }, 148 | { 149 | "in_node": 5, 150 | "out_node": 13, 151 | "weight": 1.091727686574323, 152 | "innovation_number": 12, 153 | "enabled": true 154 | }, 155 | { 156 | "in_node": 6, 157 | "out_node": 12, 158 | "weight": 1.5573275922272385, 159 | "innovation_number": 13, 160 | "enabled": true 161 | }, 162 | { 163 | "in_node": 6, 164 | "out_node": 13, 165 | "weight": 0.666897659542203, 166 | "innovation_number": 14, 167 | "enabled": true 168 | }, 169 | { 170 | "in_node": 7, 171 | "out_node": 12, 172 | "weight": 0.778017044285606, 173 | "innovation_number": 15, 174 | "enabled": true 175 | }, 176 | { 177 | "in_node": 7, 178 | "out_node": 13, 179 | "weight": 0.10495858557241944, 180 | "innovation_number": 16, 181 | "enabled": true 182 | }, 183 | { 184 | "in_node": 8, 185 | "out_node": 12, 186 | "weight": -0.36648238957929047, 187 | "innovation_number": 17, 188 | "enabled": true 189 | }, 190 | { 191 | "in_node": 8, 192 | "out_node": 13, 193 | "weight": 0.7225566449049644, 194 | "innovation_number": 18, 195 | "enabled": true 196 | }, 197 | { 198 | "in_node": 9, 199 | "out_node": 12, 200 | "weight": -0.19816090327281924, 201 | "innovation_number": 19, 202 | "enabled": true 203 | }, 204 | { 205 | "in_node": 9, 206 | "out_node": 13, 207 | "weight": -0.7349077833937139, 208 | "innovation_number": 20, 209 | "enabled": true 210 | }, 211 | { 212 | "in_node": 10, 213 | "out_node": 12, 214 | "weight": 0.47318817925347423, 215 | "innovation_number": 21, 216 | "enabled": true 217 | }, 218 | { 219 | "in_node": 10, 220 | "out_node": 13, 221 | "weight": -0.3851276049130408, 222 | "innovation_number": 22, 223 | "enabled": true 224 | }, 225 | { 226 | "in_node": 11, 227 | "out_node": 12, 228 | "weight": 0.7651000754729474, 229 | "innovation_number": 23, 230 | "enabled": true 231 | }, 232 | { 233 | "in_node": 11, 234 | "out_node": 13, 235 | "weight": -0.4044051674122451, 236 | "innovation_number": 24, 237 | "enabled": true 238 | }, 239 | { 240 | "in_node": 0, 241 | "out_node": 12, 242 | "weight": -0.0795151047527287, 243 | "innovation_number": 1, 244 | "enabled": true 245 | }, 246 | { 247 | "in_node": 0, 248 | "out_node": 13, 249 | "weight": -0.4249407938363876, 250 | "innovation_number": 2, 251 | "enabled": true 252 | }, 253 | { 254 | "in_node": 1, 255 | "out_node": 12, 256 | "weight": 0.5106444023669527, 257 | "innovation_number": 3, 258 | "enabled": true 259 | }, 260 | { 261 | "in_node": 1, 262 | "out_node": 13, 263 | "weight": -0.8624431487458102, 264 | "innovation_number": 4, 265 | "enabled": true 266 | }, 267 | { 268 | "in_node": 2, 269 | "out_node": 12, 270 | "weight": 0.47062625545396863, 271 | "innovation_number": 5, 272 | "enabled": false 273 | }, 274 | { 275 | "in_node": 2, 276 | "out_node": 13, 277 | "weight": 0.24073335650971966, 278 | "innovation_number": 6, 279 | "enabled": true 280 | }, 281 | { 282 | "in_node": 3, 283 | "out_node": 12, 284 | "weight": 0.15011026071018607, 285 | "innovation_number": 7, 286 | "enabled": true 287 | }, 288 | { 289 | "in_node": 3, 290 | "out_node": 13, 291 | "weight": 0.23136014269243027, 292 | "innovation_number": 8, 293 | "enabled": true 294 | }, 295 | { 296 | "in_node": 4, 297 | "out_node": 12, 298 | "weight": -0.5328321074455561, 299 | "innovation_number": 9, 300 | "enabled": true 301 | }, 302 | { 303 | "in_node": 4, 304 | "out_node": 13, 305 | "weight": 0.5902858263775292, 306 | "innovation_number": 10, 307 | "enabled": true 308 | }, 309 | { 310 | "in_node": 5, 311 | "out_node": 12, 312 | "weight": -0.2504673336320768, 313 | "innovation_number": 11, 314 | "enabled": true 315 | }, 316 | { 317 | "in_node": 5, 318 | "out_node": 13, 319 | "weight": -0.1330511419583788, 320 | "innovation_number": 12, 321 | "enabled": true 322 | }, 323 | { 324 | "in_node": 6, 325 | "out_node": 12, 326 | "weight": -0.6364227493353967, 327 | "innovation_number": 13, 328 | "enabled": true 329 | }, 330 | { 331 | "in_node": 6, 332 | "out_node": 13, 333 | "weight": 0.43858935168246616, 334 | "innovation_number": 14, 335 | "enabled": true 336 | }, 337 | { 338 | "in_node": 7, 339 | "out_node": 12, 340 | "weight": -0.32662139597276885, 341 | "innovation_number": 15, 342 | "enabled": true 343 | }, 344 | { 345 | "in_node": 7, 346 | "out_node": 13, 347 | "weight": 0.04053297480520485, 348 | "innovation_number": 16, 349 | "enabled": true 350 | }, 351 | { 352 | "in_node": 8, 353 | "out_node": 12, 354 | "weight": 0.600069037888034, 355 | "innovation_number": 17, 356 | "enabled": true 357 | }, 358 | { 359 | "in_node": 8, 360 | "out_node": 13, 361 | "weight": -0.152886641384371, 362 | "innovation_number": 18, 363 | "enabled": true 364 | }, 365 | { 366 | "in_node": 9, 367 | "out_node": 12, 368 | "weight": -0.2850848390212866, 369 | "innovation_number": 19, 370 | "enabled": true 371 | }, 372 | { 373 | "in_node": 9, 374 | "out_node": 13, 375 | "weight": -0.2983260484808724, 376 | "innovation_number": 20, 377 | "enabled": true 378 | }, 379 | { 380 | "in_node": 10, 381 | "out_node": 12, 382 | "weight": -0.3869287995998474, 383 | "innovation_number": 21, 384 | "enabled": true 385 | }, 386 | { 387 | "in_node": 10, 388 | "out_node": 13, 389 | "weight": 1.2651824782647607, 390 | "innovation_number": 22, 391 | "enabled": true 392 | }, 393 | { 394 | "in_node": 11, 395 | "out_node": 12, 396 | "weight": 0.08692304686330352, 397 | "innovation_number": 23, 398 | "enabled": true 399 | }, 400 | { 401 | "in_node": 11, 402 | "out_node": 13, 403 | "weight": 0.9100600898542504, 404 | "innovation_number": 24, 405 | "enabled": true 406 | }, 407 | { 408 | "in_node": 11, 409 | "out_node": 3, 410 | "weight": 0.8388222820992577, 411 | "innovation_number": 116, 412 | "enabled": true 413 | }, 414 | { 415 | "in_node": 2, 416 | "out_node": 14, 417 | "weight": -0.06624685122299465, 418 | "innovation_number": 26, 419 | "enabled": true 420 | }, 421 | { 422 | "in_node": 14, 423 | "out_node": 12, 424 | "weight": 0.3791766211526823, 425 | "innovation_number": 33, 426 | "enabled": true 427 | }, 428 | { 429 | "in_node": 7, 430 | "out_node": 4, 431 | "weight": 0.04630619979718603, 432 | "innovation_number": 275, 433 | "enabled": true 434 | } 435 | ] 436 | } -------------------------------------------------------------------------------- /models/best_genome5080_920825409008.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 2091, 3 | "fitness": 5080.920825409008, 4 | "adjusted_fitness": 0.0, 5 | "species": 1, 6 | "num_inputs": 12, 7 | "num_outputs": 2, 8 | "nodes": [ 9 | { 10 | "id": 0, 11 | "node_type": "input" 12 | }, 13 | { 14 | "id": 1, 15 | "node_type": "input" 16 | }, 17 | { 18 | "id": 2, 19 | "node_type": "input" 20 | }, 21 | { 22 | "id": 3, 23 | "node_type": "input" 24 | }, 25 | { 26 | "id": 4, 27 | "node_type": "input" 28 | }, 29 | { 30 | "id": 5, 31 | "node_type": "input" 32 | }, 33 | { 34 | "id": 6, 35 | "node_type": "input" 36 | }, 37 | { 38 | "id": 7, 39 | "node_type": "input" 40 | }, 41 | { 42 | "id": 8, 43 | "node_type": "input" 44 | }, 45 | { 46 | "id": 9, 47 | "node_type": "input" 48 | }, 49 | { 50 | "id": 10, 51 | "node_type": "input" 52 | }, 53 | { 54 | "id": 11, 55 | "node_type": "input" 56 | }, 57 | { 58 | "id": 12, 59 | "node_type": "output" 60 | }, 61 | { 62 | "id": 13, 63 | "node_type": "output" 64 | }, 65 | { 66 | "id": 14, 67 | "node_type": "hidden" 68 | }, 69 | { 70 | "id": 15, 71 | "node_type": "hidden" 72 | } 73 | ], 74 | "connections": [ 75 | { 76 | "in_node": 0, 77 | "out_node": 12, 78 | "weight": 0.15237909960531426, 79 | "innovation_number": 1, 80 | "enabled": true 81 | }, 82 | { 83 | "in_node": 0, 84 | "out_node": 13, 85 | "weight": 0.707390893518135, 86 | "innovation_number": 2, 87 | "enabled": true 88 | }, 89 | { 90 | "in_node": 1, 91 | "out_node": 12, 92 | "weight": 0.17343809753451822, 93 | "innovation_number": 3, 94 | "enabled": false 95 | }, 96 | { 97 | "in_node": 1, 98 | "out_node": 13, 99 | "weight": -0.7185754866918524, 100 | "innovation_number": 4, 101 | "enabled": true 102 | }, 103 | { 104 | "in_node": 2, 105 | "out_node": 12, 106 | "weight": 0.5868727324042974, 107 | "innovation_number": 5, 108 | "enabled": true 109 | }, 110 | { 111 | "in_node": 2, 112 | "out_node": 13, 113 | "weight": -1.0012693009459674, 114 | "innovation_number": 6, 115 | "enabled": true 116 | }, 117 | { 118 | "in_node": 3, 119 | "out_node": 12, 120 | "weight": -0.1531541496854131, 121 | "innovation_number": 7, 122 | "enabled": true 123 | }, 124 | { 125 | "in_node": 3, 126 | "out_node": 13, 127 | "weight": -0.9986937022203926, 128 | "innovation_number": 8, 129 | "enabled": true 130 | }, 131 | { 132 | "in_node": 4, 133 | "out_node": 12, 134 | "weight": -0.3510728204546242, 135 | "innovation_number": 9, 136 | "enabled": true 137 | }, 138 | { 139 | "in_node": 4, 140 | "out_node": 13, 141 | "weight": 0.08162983494984727, 142 | "innovation_number": 10, 143 | "enabled": true 144 | }, 145 | { 146 | "in_node": 5, 147 | "out_node": 12, 148 | "weight": 0.3773812624289763, 149 | "innovation_number": 11, 150 | "enabled": true 151 | }, 152 | { 153 | "in_node": 5, 154 | "out_node": 13, 155 | "weight": 0.7186133926561752, 156 | "innovation_number": 12, 157 | "enabled": true 158 | }, 159 | { 160 | "in_node": 6, 161 | "out_node": 12, 162 | "weight": 0.752178837633119, 163 | "innovation_number": 13, 164 | "enabled": true 165 | }, 166 | { 167 | "in_node": 6, 168 | "out_node": 13, 169 | "weight": 0.7052911970446222, 170 | "innovation_number": 14, 171 | "enabled": true 172 | }, 173 | { 174 | "in_node": 7, 175 | "out_node": 12, 176 | "weight": 0.33652472810550127, 177 | "innovation_number": 15, 178 | "enabled": true 179 | }, 180 | { 181 | "in_node": 7, 182 | "out_node": 13, 183 | "weight": 0.8974392786006318, 184 | "innovation_number": 16, 185 | "enabled": true 186 | }, 187 | { 188 | "in_node": 8, 189 | "out_node": 12, 190 | "weight": -0.18531344335288885, 191 | "innovation_number": 17, 192 | "enabled": true 193 | }, 194 | { 195 | "in_node": 8, 196 | "out_node": 13, 197 | "weight": -0.7527510153214283, 198 | "innovation_number": 18, 199 | "enabled": true 200 | }, 201 | { 202 | "in_node": 9, 203 | "out_node": 12, 204 | "weight": 1.0876924240070323, 205 | "innovation_number": 19, 206 | "enabled": true 207 | }, 208 | { 209 | "in_node": 9, 210 | "out_node": 13, 211 | "weight": -0.5595975869544232, 212 | "innovation_number": 20, 213 | "enabled": true 214 | }, 215 | { 216 | "in_node": 10, 217 | "out_node": 12, 218 | "weight": 0.9844037213612887, 219 | "innovation_number": 21, 220 | "enabled": true 221 | }, 222 | { 223 | "in_node": 10, 224 | "out_node": 13, 225 | "weight": -0.2293869627721059, 226 | "innovation_number": 22, 227 | "enabled": true 228 | }, 229 | { 230 | "in_node": 11, 231 | "out_node": 12, 232 | "weight": -0.9016824187748402, 233 | "innovation_number": 23, 234 | "enabled": true 235 | }, 236 | { 237 | "in_node": 11, 238 | "out_node": 13, 239 | "weight": -0.6550239121112809, 240 | "innovation_number": 24, 241 | "enabled": true 242 | }, 243 | { 244 | "in_node": 0, 245 | "out_node": 12, 246 | "weight": -0.8711929373421243, 247 | "innovation_number": 1, 248 | "enabled": true 249 | }, 250 | { 251 | "in_node": 0, 252 | "out_node": 13, 253 | "weight": 0.01784146628611313, 254 | "innovation_number": 2, 255 | "enabled": true 256 | }, 257 | { 258 | "in_node": 1, 259 | "out_node": 12, 260 | "weight": 0.49785442765020727, 261 | "innovation_number": 3, 262 | "enabled": true 263 | }, 264 | { 265 | "in_node": 1, 266 | "out_node": 13, 267 | "weight": -0.1802871285384704, 268 | "innovation_number": 4, 269 | "enabled": true 270 | }, 271 | { 272 | "in_node": 2, 273 | "out_node": 12, 274 | "weight": 0.16168807976422883, 275 | "innovation_number": 5, 276 | "enabled": true 277 | }, 278 | { 279 | "in_node": 2, 280 | "out_node": 13, 281 | "weight": -0.48643569898938904, 282 | "innovation_number": 6, 283 | "enabled": true 284 | }, 285 | { 286 | "in_node": 3, 287 | "out_node": 12, 288 | "weight": 0.4979839065582274, 289 | "innovation_number": 7, 290 | "enabled": true 291 | }, 292 | { 293 | "in_node": 3, 294 | "out_node": 13, 295 | "weight": -0.23219008899543506, 296 | "innovation_number": 8, 297 | "enabled": true 298 | }, 299 | { 300 | "in_node": 4, 301 | "out_node": 12, 302 | "weight": 0.26787022857245946, 303 | "innovation_number": 9, 304 | "enabled": true 305 | }, 306 | { 307 | "in_node": 4, 308 | "out_node": 13, 309 | "weight": -0.4587278772980341, 310 | "innovation_number": 10, 311 | "enabled": true 312 | }, 313 | { 314 | "in_node": 5, 315 | "out_node": 12, 316 | "weight": 0.9417704594005493, 317 | "innovation_number": 11, 318 | "enabled": true 319 | }, 320 | { 321 | "in_node": 5, 322 | "out_node": 13, 323 | "weight": -0.7382336773365692, 324 | "innovation_number": 12, 325 | "enabled": true 326 | }, 327 | { 328 | "in_node": 6, 329 | "out_node": 12, 330 | "weight": 0.5932719921981754, 331 | "innovation_number": 13, 332 | "enabled": true 333 | }, 334 | { 335 | "in_node": 6, 336 | "out_node": 13, 337 | "weight": -0.5737478039132987, 338 | "innovation_number": 14, 339 | "enabled": true 340 | }, 341 | { 342 | "in_node": 7, 343 | "out_node": 12, 344 | "weight": 0.8247528142507573, 345 | "innovation_number": 15, 346 | "enabled": true 347 | }, 348 | { 349 | "in_node": 7, 350 | "out_node": 13, 351 | "weight": -0.533665543105132, 352 | "innovation_number": 16, 353 | "enabled": true 354 | }, 355 | { 356 | "in_node": 8, 357 | "out_node": 12, 358 | "weight": -0.38604036222669563, 359 | "innovation_number": 17, 360 | "enabled": true 361 | }, 362 | { 363 | "in_node": 8, 364 | "out_node": 13, 365 | "weight": 0.9152671445837168, 366 | "innovation_number": 18, 367 | "enabled": true 368 | }, 369 | { 370 | "in_node": 9, 371 | "out_node": 12, 372 | "weight": 0.9312033439600562, 373 | "innovation_number": 19, 374 | "enabled": true 375 | }, 376 | { 377 | "in_node": 9, 378 | "out_node": 13, 379 | "weight": 0.833941173520867, 380 | "innovation_number": 20, 381 | "enabled": true 382 | }, 383 | { 384 | "in_node": 10, 385 | "out_node": 12, 386 | "weight": 0.8724303832175437, 387 | "innovation_number": 21, 388 | "enabled": true 389 | }, 390 | { 391 | "in_node": 10, 392 | "out_node": 13, 393 | "weight": -0.7836895422329733, 394 | "innovation_number": 22, 395 | "enabled": true 396 | }, 397 | { 398 | "in_node": 11, 399 | "out_node": 12, 400 | "weight": 0.7772038740363404, 401 | "innovation_number": 23, 402 | "enabled": true 403 | }, 404 | { 405 | "in_node": 11, 406 | "out_node": 13, 407 | "weight": 0.7330933109599783, 408 | "innovation_number": 24, 409 | "enabled": true 410 | }, 411 | { 412 | "in_node": 14, 413 | "out_node": 12, 414 | "weight": 0.9338311619051411, 415 | "innovation_number": 31, 416 | "enabled": true 417 | }, 418 | { 419 | "in_node": 2, 420 | "out_node": 14, 421 | "weight": 1.2133458462537785, 422 | "innovation_number": 44, 423 | "enabled": true 424 | }, 425 | { 426 | "in_node": 1, 427 | "out_node": 15, 428 | "weight": 0.7741002300940938, 429 | "innovation_number": 50, 430 | "enabled": true 431 | }, 432 | { 433 | "in_node": 15, 434 | "out_node": 12, 435 | "weight": -0.40303742429232836, 436 | "innovation_number": 51, 437 | "enabled": true 438 | } 439 | ] 440 | } -------------------------------------------------------------------------------- /src/ground.py: -------------------------------------------------------------------------------- 1 | from typing import Protocol 2 | import pygame as pg 3 | import random 4 | import math 5 | from enum import Enum 6 | import pymunk 7 | 8 | from src.render_object import RenderObject 9 | from src.globals import ( 10 | SCREEN_WIDTH, 11 | SCREEN_HEIGHT, 12 | PERLIN_SEGMENTS, 13 | RED, 14 | SEGMENT_WIDTH, 15 | FLOOR_HEIGHT, 16 | AMPLITUDE, 17 | FREQUENCY, 18 | ) 19 | 20 | pg.init() 21 | 22 | show_marks = False 23 | 24 | screen = pg.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) 25 | 26 | 27 | class InterpolationType(Enum): 28 | LINEAR = 1 29 | COSINE = 2 30 | CUBIC = 3 31 | 32 | 33 | class Ground(Protocol): 34 | @property 35 | def generate_floor_segment(self, start_x: float) -> list: 36 | pass 37 | 38 | @property 39 | def generate_new_floor_segment(self, scroll_offset: int): 40 | pass 41 | 42 | @property 43 | def remove_old_floor_segment(self, scroll_offset: float): 44 | pass 45 | 46 | @property 47 | def swap_floor_segments(self, scroll_offset: float): 48 | pass 49 | 50 | @property 51 | def get_y(self, x: int) -> int: 52 | pass 53 | 54 | @property 55 | def render(self): 56 | # TODO: removew this later, when we call render from another 57 | # place than environment 58 | pass 59 | 60 | @property 61 | def calculate_y(self, x: int) -> int: 62 | # TODO: remove this later 63 | pass 64 | 65 | @property 66 | def update(self): 67 | pass 68 | 69 | @property 70 | def init_pymunk_polygon(self, space): 71 | pass 72 | 73 | 74 | class BasicSegment: 75 | 76 | def __init__(self, start_x: int, end_x: int, space: pymunk.space) -> None: 77 | self.start_x = start_x 78 | self.end_x = end_x 79 | self.points = [] 80 | self.add_points(start_x, end_x) 81 | 82 | self.body = pymunk.Body(body_type=pymunk.Body.KINEMATIC) 83 | self.poly = pymunk.Poly(self.body, self.points, radius=0.0) 84 | self.poly.friction = 0.5 85 | space.add(self.body, self.poly) 86 | 87 | def add_points(self, start_x: int, end_x: int) -> None: 88 | for x in range(int(self.start_x), int(self.end_x) + 1, 1): 89 | y = int(SCREEN_HEIGHT - FLOOR_HEIGHT + AMPLITUDE * math.sin(FREQUENCY * x)) 90 | self.points.append((x, y)) 91 | 92 | def get_points(self) -> list[tuple[int, int]]: 93 | # Get the points in world coordinates 94 | points = self.poly.get_vertices() 95 | world_points = [self.poly.body.local_to_world(p) for p in points] 96 | points = [(p.x, p.y) for p in world_points] 97 | points.sort(key=lambda x: x[0]) 98 | return points 99 | 100 | def remove_pymunk_polygon(self, space) -> None: 101 | space.remove(self.body, self.poly) 102 | 103 | def render(self, screen: pg.display) -> None: 104 | points = self.get_points().copy() 105 | scroll_offset = -self.poly.body.position[0] 106 | shifted_points = [(x - scroll_offset, y) for (x, y) in points] 107 | shifted_points.append((shifted_points[-1][0], SCREEN_HEIGHT)) 108 | shifted_points.append((shifted_points[0][0], SCREEN_HEIGHT)) 109 | pg.draw.polygon(screen, (34, 139, 34), shifted_points) 110 | 111 | def get_last_shifted_point(self) -> tuple[int, int]: 112 | points = self.get_points() 113 | scroll_offset = self.start_x - self.poly.body.position[0] 114 | return (points[-1][0] - scroll_offset, points[-1][1]) 115 | 116 | def get_first_shifted_point(self) -> tuple[int, int]: 117 | points = self.get_points() 118 | scroll_offset = self.start_x - self.poly.body.position[0] 119 | return (points[0][0] - scroll_offset, points[0][1]) 120 | 121 | def init_pymunk_polygon(self, space) -> None: 122 | body = pymunk.Body(0, 0, 1) 123 | poly = pymunk.Poly(body, self.points, radius=0.0) 124 | space.add(body, poly) 125 | 126 | 127 | class BasicGround(RenderObject, Ground, BasicSegment): 128 | def __init__( 129 | self, screen: pg.display, space: pymunk.space, segment_width: int 130 | ) -> None: 131 | self.screen = screen 132 | self.space = space 133 | self.segment_width = segment_width 134 | self.terrain_segments: BasicSegment = [] 135 | self.scroll_offset = 0.0 136 | 137 | self.terrain_segments.append(self.generate_floor_segment(0)) 138 | self.terrain_segments.append(self.generate_floor_segment(self.segment_width)) 139 | 140 | def get_current_segment(self, x: int) -> int: 141 | """ 142 | Get the index of the segment that 143 | contains the x-coordinate 144 | 145 | Args: 146 | x (int): _description_ The x-coordinate 147 | 148 | Returns: 149 | int: _description_ The index of the segment that contains the 150 | x-coordinate 151 | """ 152 | return x // SEGMENT_WIDTH 153 | 154 | def get_y(self, x: int) -> int: 155 | """_summary_ Get the y-coordinate of the terrain at the x-coordinate 156 | 157 | Args: 158 | x (int): _description_ The x-coordinate 159 | 160 | Returns: 161 | int: _description_ The y-coordinate of the terrain at the 162 | x-coordinate 163 | """ 164 | 165 | segments = self.terrain_segments 166 | 167 | for segment in segments: 168 | if not (segment.get_points()[0][0] <= x <= segment.get_points()[-1][0]): 169 | continue 170 | points = segment.get_points().copy() 171 | for i, _ in enumerate(points): 172 | if points[i][0] <= x <= points[i + 1][0]: 173 | return int((points[i][1] + points[i + 1][1]) / 2) 174 | return None 175 | raise ValueError("The x-coordinate is not in the terrain segments") 176 | 177 | def update(self, scroll_offset: float) -> None: 178 | self.scroll_offset += scroll_offset 179 | self.generate_new_floor_segment() 180 | self.remove_old_floor_segment() 181 | 182 | def generate_floor_segment(self, start_x: int) -> list: 183 | """ 184 | Generates a segment of the floor 185 | 186 | Args: 187 | start_x (float): The x-coordinate of the starting point 188 | of the segment 189 | returns: 190 | list: A list of points representing the floor segment 191 | """ 192 | 193 | segment = BasicSegment(start_x, start_x + self.segment_width, self.space) 194 | return segment 195 | 196 | def generate_new_floor_segment(self) -> None: 197 | """_summary_ Generate a new floor segment 198 | 199 | Args: 200 | scroll_offset (int): _description_ 201 | """ 202 | last_segment = self.terrain_segments[-1] 203 | last_point = last_segment.get_points()[-1] 204 | if last_point[0] - self.scroll_offset < SCREEN_WIDTH: 205 | # Start new segment where the last one ends 206 | last_x = last_point[0] 207 | self.terrain_segments.append(self.generate_floor_segment(last_x)) 208 | 209 | def remove_old_floor_segment(self) -> None: 210 | first_segment = self.terrain_segments[0] 211 | first_point = first_segment.get_points()[0] 212 | # if first_point[0] - self.scroll_offset < -self.segment_width: 213 | # self.terrain_segments.pop(0) 214 | # first_segment.remove_pymunk_polygon(self.space) 215 | 216 | def swap_floor_segments(self, scroll_offset: float) -> None: 217 | """_summary_ Swap the floor segments""" 218 | 219 | if self.terrain_segments[0][-1][0] - self.scroll_offset < -SEGMENT_WIDTH: 220 | # Move the first segment to the right end of the second segment 221 | last_segment_end_x = self.terrain_segments[1][-1][0] 222 | new_start_x = last_segment_end_x + 1 223 | self.terrain_segments[0] = self.generate_floor_segment(new_start_x) 224 | # Swap the segments in the list so they alternate 225 | self.terrain_segments.append(self.terrain_segments.pop(0)) 226 | 227 | def render(self) -> None: 228 | """Render screen objects 229 | 230 | Args: 231 | terrain_segments (_type_): _description_ 232 | scroll_offset (_type_): _description_ 233 | """ 234 | for segment in self.terrain_segments: 235 | points = segment.get_points() 236 | shifted_points = [(x - self.scroll_offset, y) for (x, y) in points] 237 | shifted_points.append((shifted_points[-1][0], SCREEN_HEIGHT)) 238 | shifted_points.append((shifted_points[0][0], SCREEN_HEIGHT)) 239 | pg.draw.polygon(self.screen, (34, 139, 34), shifted_points) 240 | 241 | def move_segments(self, scroll_offset: float) -> None: 242 | for segment in self.terrain_segments: 243 | segment.body.position = ( 244 | segment.body.position[0] - scroll_offset, 245 | segment.body.position[1], 246 | ) 247 | self.space.reindex_shapes_for_body(segment.body) 248 | 249 | def init_pymunk_polygon(self, space) -> None: 250 | for segment in self.terrain_segments: 251 | segment.init_pymunk_polygon(space) 252 | 253 | 254 | class PerlinSegment: 255 | def __init__(self, start_x: int, end_x: int) -> None: 256 | self.start_x = start_x 257 | self.end_x = end_x 258 | self.points = [] 259 | 260 | def add_points(self, start_x: int, end_x: int) -> None: 261 | pass 262 | 263 | def get_points(self) -> list: 264 | return self.points 265 | 266 | def init_pymunk_polygon(self, space) -> None: 267 | body = pymunk.Body(0, 0, 1) 268 | poly = pymunk.Poly(body, self.points, radius=0.0) 269 | space.add(body, poly) 270 | 271 | 272 | class PerlinNoise: 273 | 274 | def __init__( 275 | self, 276 | seed, 277 | amplitude=1, 278 | frequency=0.002, 279 | octaves=1, 280 | interp=InterpolationType.COSINE, 281 | use_fade=False, 282 | ) -> None: 283 | 284 | self.seed = random.Random(seed).random() 285 | self.amplitude = amplitude 286 | self.frequency = frequency 287 | self.octaves = octaves 288 | self.interp = interp 289 | self.use_fade = use_fade 290 | self.coordinates = list() 291 | self.render_points = list() 292 | self.norma = SCREEN_WIDTH / PERLIN_SEGMENTS 293 | self.floor_segments = list() 294 | 295 | self.mem_x = dict() 296 | 297 | def generate_floor_segment(self, offset: int) -> None: 298 | """_summary_ Generate a segment of the floor 299 | 300 | Args: 301 | offset (int): _description_ 302 | """ 303 | pass 304 | 305 | def update(self, offset: int) -> None: 306 | """Update the screen objects 307 | 308 | Args: 309 | offset (int): The current offset for the scrolling terrain 310 | """ 311 | self.offset = offset # Store the offset for use in other methods 312 | points = list() 313 | norma = SCREEN_WIDTH / PERLIN_SEGMENTS 314 | for pix_x in range(SCREEN_WIDTH + SEGMENT_WIDTH): 315 | x = (pix_x + offset) / norma 316 | y = self.generate_y(x) 317 | pix_y = SCREEN_HEIGHT / 2 + y 318 | if show_marks and math.isclose( 319 | x * self.frequency, int(x * self.frequency), rel_tol=0.001 320 | ): 321 | self.draw_mark(screen, RED, (pix_x, pix_y)) 322 | points.append((pix_x, pix_y)) 323 | self.render_points = points 324 | 325 | def render(self, offset) -> None: 326 | """_summary_ Render the screen objects 327 | args: offset is just for matching the function signature 328 | """ 329 | # draw lines and update display 330 | pg.draw.lines(screen, (34, 139, 34), False, self.render_points, 4) 331 | pg.display.flip() 332 | 333 | def __noise(self, x) -> float: 334 | """ 335 | _summary_ Generate a random number for the given x 336 | 337 | Args: 338 | x (_type_): _description_ 339 | 340 | Returns: 341 | _type_: _description_ 342 | """ 343 | # made for improve performance 344 | if x not in self.mem_x: 345 | self.mem_x[x] = random.Random(self.seed + x).uniform(-1, 1) 346 | return self.mem_x[x] 347 | 348 | def draw_mark(surface: pg.Surface, color, coord: tuple) -> None: 349 | """ 350 | _summary_ Draw a mark on the screen 351 | 352 | Args: 353 | surface (pg.Surface): _description_ 354 | color (_type_): _description_ 355 | coord (tuple): _description_ 356 | """ 357 | pg.draw.circle(surface, color, coord, 3) 358 | 359 | def __interpolated_noise(self, x: int) -> float: 360 | """ 361 | _summary_ Interpolate the noise at the given x 362 | 363 | Args: 364 | x (_type_): _description_ 365 | 366 | Returns: 367 | float: result of interpolation 368 | """ 369 | prev_x = int(x) # previous integer 370 | next_x = prev_x + 1 # next integer 371 | frac_x = x - prev_x # fractional of x 372 | 373 | if self.use_fade: 374 | frac_x = self.__fade(frac_x) 375 | 376 | # intepolate x 377 | if self.interp is InterpolationType.LINEAR: 378 | res = self.__linear_interp( 379 | self.__noise(prev_x), self.__noise(next_x), frac_x 380 | ) 381 | elif self.interp is InterpolationType.COSINE: 382 | res = self.__cosine_interp( 383 | self.__noise(prev_x), self.__noise(next_x), frac_x 384 | ) 385 | else: 386 | res = self.__cubic_interp( 387 | self.__noise(prev_x - 1), 388 | self.__noise(prev_x), 389 | self.__noise(next_x), 390 | self.__noise(next_x + 1), 391 | frac_x, 392 | ) 393 | 394 | return res 395 | 396 | def get_y(self, x: int) -> int: 397 | """_summary_ Get the y-coordinate of the terrain at the x-coordinate 398 | 399 | Args: 400 | x (int): _description_ The x-coordinate 401 | 402 | Returns: 403 | int: _description_ The y-coordinate of the terrain at the 404 | x-coordinate 405 | """ 406 | 407 | for i, point in enumerate(self.render_points): 408 | if point[0] <= x <= self.render_points[i + 1][0]: 409 | return int((point[1] + self.render_points[i + 1][1]) / 2) 410 | return None 411 | 412 | def generate_y(self, x: int) -> int: 413 | """_summary_ Calculate the y value of the Perlin noise at the given x 414 | 415 | Args: 416 | x (int): 417 | 418 | Returns: 419 | float: y 420 | """ 421 | frequency = self.frequency 422 | amplitude = self.amplitude 423 | result = 0 424 | for _ in range(self.octaves): 425 | result += self.__interpolated_noise(x * frequency) * amplitude 426 | frequency *= 2 427 | amplitude /= 2 428 | 429 | return result 430 | 431 | def __cosine_interp(self, a, b, x) -> float: 432 | x2 = (1 - math.cos(x * math.pi)) / 2 433 | return a * (1 - x2) + b * x2 -------------------------------------------------------------------------------- /src/agent_parts/rectangle.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pygame 3 | import pymunk 4 | import math 5 | from src.render_object import RenderObject 6 | 7 | 8 | class Point: 9 | x: float 10 | y: float 11 | """ 12 | A class representing a 2D point in space. 13 | 14 | Attributes: 15 | ---------- 16 | x : float 17 | The x-coordinate of the point. 18 | y : float 19 | The y-coordinate of the point. 20 | """ 21 | 22 | def __init__(self, x: float, y: float): 23 | """ 24 | Initializes a Point object. 25 | 26 | Parameters: 27 | ---------- 28 | x : float 29 | The x-coordinate of the point. 30 | y : float 31 | The y-coordinate of the point. 32 | """ 33 | self.x = x 34 | self.y = y 35 | 36 | def __str__(self) -> str: 37 | """ 38 | Returns a string representation of the Point object. 39 | 40 | Returns: 41 | ------- 42 | str: 43 | The string format of the point's coordinates as "X: {x}, Y: {y}". 44 | """ 45 | return f"X: {self.x}, Y: {self.y}" 46 | 47 | 48 | class Rectangle: 49 | 50 | class anchor: 51 | x: float 52 | y: float 53 | 54 | def __init__(self, x: float, y: float): 55 | self.x = x 56 | self.y = y 57 | 58 | """ 59 | A class representing a 2D rectangle using its position, width, and height. 60 | 61 | Attributes: 62 | ---------- 63 | width : float 64 | The width of the rectangle. 65 | height : float 66 | The height of the rectangle. 67 | poPoints : np.ndarray 68 | A np array representing the four corner points of the rectangle. 69 | """ 70 | 71 | def __init__( 72 | self, 73 | point: Point, 74 | width: float, 75 | height: float, 76 | mass: float = 1.0, 77 | body_type="dynamic", 78 | ): 79 | """ 80 | Initializes a Rectangle object and its corresponding Pymunk physics body. 81 | 82 | Parameters: 83 | ---------- 84 | point : Point 85 | The initial position of the rectangle. 86 | width : float 87 | The width of the rectangle. 88 | height : float 89 | The height of the rectangle. 90 | mass : float, optional 91 | The mass of the rectangle for physics simulation. 92 | """ 93 | x, y = point.x, point.y 94 | self.width = width 95 | self.height = height 96 | 97 | if body_type == "static": 98 | self.body = pymunk.Body(body_type=pymunk.Body.STATIC) 99 | else: 100 | moment_of_inertia = pymunk.moment_for_box(mass, (width, height)) 101 | self.body = pymunk.Body(mass, moment_of_inertia) 102 | 103 | # Define rectangle's corner points (used for both rendering and Pymunk poly) 104 | self.poPoints = np.array( 105 | [ 106 | np.array([x, y]), 107 | np.array([x + width, y]), 108 | np.array([x + width, y + height]), 109 | np.array([x, y + height]), 110 | ] 111 | ) 112 | 113 | self.body.position = ( 114 | x + width / 2, 115 | y + height / 2, 116 | ) # Set the center of the rectangle 117 | 118 | # Define the polygon shape 119 | vertices = [ 120 | (-width / 2, -height / 2), 121 | (width / 2, -height / 2), 122 | (width / 2, height / 2), 123 | (-width / 2, height / 2), 124 | ] 125 | self.shape = pymunk.Poly(self.body, vertices) 126 | self.shape.friction = 0.7 # You can adjust friction or other properties 127 | 128 | def update_from_physics(self): 129 | """ 130 | Updates the rectangle's corner points based on the Pymunk body's position and rotation. 131 | """ 132 | cx, cy = self.body.position # Center of the rectangle 133 | angle = self.body.angle # Rotation angle of the rectangle 134 | 135 | # Calculate the rotated points around the center 136 | cos_angle = math.cos(angle) 137 | sin_angle = math.sin(angle) 138 | half_width = self.width / 2 139 | half_height = self.height / 2 140 | 141 | # New corner positions 142 | self.poPoints[0] = np.array( 143 | [ 144 | cx - half_width * cos_angle + half_height * sin_angle, 145 | cy - half_width * sin_angle - half_height * cos_angle, 146 | ] 147 | ) 148 | self.poPoints[1] = np.array( 149 | [ 150 | cx + half_width * cos_angle + half_height * sin_angle, 151 | cy + half_width * sin_angle - half_height * cos_angle, 152 | ] 153 | ) 154 | self.poPoints[2] = np.array( 155 | [ 156 | cx + half_width * cos_angle - half_height * sin_angle, 157 | cy + half_width * sin_angle + half_height * cos_angle, 158 | ] 159 | ) 160 | self.poPoints[3] = np.array( 161 | [ 162 | cx - half_width * cos_angle - half_height * sin_angle, 163 | cy - half_width * sin_angle + half_height * cos_angle, 164 | ] 165 | ) 166 | 167 | def apply_force(self, force, offset=(0, 0)): 168 | """ 169 | Applies a force to the Pymunk body. 170 | 171 | Parameters: 172 | ---------- 173 | force : tuple 174 | The force to apply as (fx, fy). 175 | offset : tuple 176 | The point at which to apply the force, relative to the center of the body. 177 | """ 178 | self.body.apply_force_at_local_point(force, offset) 179 | 180 | def apply_impulse(self, impulse, offset=(0, 0)): 181 | """ 182 | Applies an impulse to the Pymunk body. 183 | 184 | Parameters: 185 | ---------- 186 | impulse : tuple 187 | The impulse to apply as (ix, iy). 188 | offset : tuple 189 | The point at which to apply the impulse, relative to the center of the body. 190 | """ 191 | self.body.apply_impulse_at_local_point(impulse, offset) 192 | 193 | def contains(self, point: Point) -> bool: 194 | """ 195 | Checks if a given point is inside the rectangle using linear algebra. 196 | 197 | Parameters: 198 | ---------- 199 | point : Point 200 | The point to be checked. 201 | 202 | Returns: 203 | ------- 204 | bool: 205 | True if the point is inside the rectangle, False otherwise. 206 | """ 207 | for i in range(4): 208 | x1 = self.poPoints[i][0] 209 | y1 = self.poPoints[i][1] 210 | x2 = self.poPoints[(i + 1) % 4][0] 211 | y2 = self.poPoints[(i + 1) % 4][1] 212 | xp = point.x 213 | yp = point.y 214 | # Cross product 215 | crossProduct = (yp - y1) * (x2 - x1) - (xp - x1) * (y2 - y1) 216 | if crossProduct < 0: 217 | return False 218 | return True 219 | 220 | def intersects(self, other_rect) -> bool: 221 | """ 222 | Checks if this rectangle intersects with another rectangle. 223 | 224 | Parameters: 225 | ---------- 226 | other_rect : Rectangle 227 | The rectangle to check for intersection with. 228 | 229 | Returns: 230 | ------- 231 | bool: 232 | True if the rectangles intersect, False otherwise. 233 | """ 234 | return not ( 235 | other_rect.x > self.poPoints[0][0] + self.width 236 | or other_rect.x + other_rect.width < self.poPoints[0][0] 237 | or other_rect.y > self.poPoints[0][1] + self.height 238 | or other_rect.y + other_rect.height < self.poPoints[0][1] 239 | ) 240 | 241 | def __str__(self) -> str: 242 | """ 243 | Returns a string representation of the Rectangle object. 244 | 245 | Returns: 246 | ------- 247 | str: 248 | The string format of the rectangle's position and size as "X: {x}, 249 | Y: {y}, Width: {width}, Height: {height}". 250 | """ 251 | return ( 252 | f"X: {self.poPoints[0][0]}, Y: {self.poPoints[0][1]}, " 253 | f"Width: {self.width}, Height: {self.height}" 254 | ) 255 | 256 | def render(self, window): 257 | """ 258 | Renders the rectangle on a given window using pygame. 259 | 260 | Parameters: 261 | ---------- 262 | window : any 263 | The graphical window where the rectangle will be drawn. 264 | """ 265 | pygame.draw.polygon( 266 | window, 267 | (255, 255, 255), 268 | [ 269 | (self.poPoints[0][0], self.poPoints[0][1]), 270 | (self.poPoints[1][0], self.poPoints[1][1]), 271 | (self.poPoints[2][0], self.poPoints[2][1]), 272 | (self.poPoints[3][0], self.poPoints[3][1]), 273 | ], 274 | ) 275 | 276 | def updatePosition(self, point: Point): 277 | """ 278 | Updates the position of the rectangle by translating its corner points. 279 | 280 | Parameters: 281 | ---------- 282 | x : float 283 | The amount to translate the rectangle in the x direction. 284 | y : float 285 | The amount to translate the rectangle in the y direction. 286 | """ 287 | x, y = point.x, point.y 288 | 289 | self.poPoints[0][0] += x 290 | self.poPoints[0][1] += y 291 | 292 | self.poPoints[1][0] += x 293 | self.poPoints[1][1] += y 294 | 295 | self.poPoints[2][0] += x 296 | self.poPoints[2][1] += y 297 | 298 | self.poPoints[3][0] += x 299 | self.poPoints[3][1] += y 300 | 301 | """translation = np.array([x, y]) 302 | self.poPoints = self.poPoints + translation""" 303 | 304 | def get_angle(self): 305 | x1, y1 = self.poPoints[0][0], self.poPoints[0][1] 306 | x2, y2 = self.poPoints[1][0], self.poPoints[1][1] 307 | 308 | vector1 = np.array([1, 0]) 309 | vector2 = np.array([x2 - x1, y2 - y1]) 310 | angle, _ = self.angle_between_vectors(vector1, vector2) 311 | if y2 - y1 < 0: 312 | angle = 2 * math.pi - angle 313 | return angle 314 | 315 | def rotateRectangle(self, angle: float): 316 | """ 317 | Returns the coordinates of the edges of the rectangle after rotation 318 | """ 319 | 320 | widthVector = np.array( 321 | [self.width * math.cos(angle), self.width * math.sin(angle)] 322 | ) 323 | heightVector = np.array( 324 | [-self.height * math.sin(angle), self.height * math.cos(angle)] 325 | ) 326 | self.poPoints[1][0], self.poPoints[1][1] = ( 327 | self.poPoints[0][0] + widthVector[0], 328 | self.poPoints[0][1] + widthVector[1], 329 | ) 330 | self.poPoints[2][0], self.poPoints[2][1] = ( 331 | self.poPoints[0][0] + widthVector[0] + heightVector[0], 332 | self.poPoints[0][1] + widthVector[1] + heightVector[1], 333 | ) 334 | self.poPoints[3][0], self.poPoints[3][1] = ( 335 | self.poPoints[0][0] + heightVector[0], 336 | self.poPoints[0][1] + heightVector[1], 337 | ) 338 | 339 | def rotateAnchor(self, angle: float): 340 | """Rotates the special point in the rectangle with a given angle""" 341 | anchorVector = np.array([self.anchor.x - self.x, self.anchor.y - self.y]) 342 | x_rotated = anchorVector[0] * math.cos(angle) - anchorVector[1] * math.sin( 343 | angle 344 | ) 345 | y_rotated = anchorVector[0] * math.sin(angle) - anchorVector[1] * math.cos( 346 | angle 347 | ) 348 | 349 | self.anchor.x = x_rotated 350 | self.anchor.y = y_rotated 351 | 352 | def rotateAroundPoint(self, angle: float, point: Point): 353 | """Rotates the rectangle around a point with a given coordinate""" 354 | x, y = point.x, point.y 355 | for i in range(4): 356 | pointVector = np.array([self.poPoints[i][0] - x, self.poPoints[i][1] - y]) 357 | x_rotated = pointVector[0] * math.cos(angle) - pointVector[1] * math.sin( 358 | angle 359 | ) 360 | y_rotated = pointVector[0] * math.sin(angle) - pointVector[1] * math.cos( 361 | angle 362 | ) 363 | self.poPoints[i][0] = x_rotated 364 | self.poPoints[i][1] = y_rotated 365 | 366 | def rotateRenderObject( 367 | self, angle: float, renderObject: RenderObject, axisPoint: Point 368 | ): 369 | """Rotates the position of the renderObject around the given axisPoint by the specified angle.""" 370 | objectPoint = renderObject.get_position() 371 | 372 | final_x, final_y = self.rotatePointPoint(angle, objectPoint, axisPoint) 373 | # Set the new position 374 | renderObject.set_position(Point(final_x, final_y)) 375 | 376 | def rotatePointPoint(self, angle: float, objectPoint: Point, axisPoint: Point): 377 | o_x = objectPoint.x 378 | o_y = objectPoint.y 379 | 380 | a_x = axisPoint.x 381 | a_y = axisPoint.y 382 | 383 | # Translate point to origin (relative to axisPoint) 384 | translated_x = o_x - a_x 385 | translated_y = o_y - a_y 386 | 387 | obj_angle, _ = self.angle_between_vectors( 388 | np.array([1, 0]), np.array([translated_x, translated_y]) 389 | ) 390 | 391 | r = math.sqrt(translated_x**2 + translated_y**2) 392 | # Apply rotation 393 | # x_rotated = translated_x * math.cos(angle) - translated_y * math.sin(angle) 394 | # y_rotated = translated_x * math.sin(angle) + translated_y * math.cos(angle) 395 | # Translate back to original position 396 | final_x = a_x + r * math.cos(angle + obj_angle) 397 | final_y = a_y + r * math.sin(angle + obj_angle) 398 | 399 | return final_x, final_y 400 | 401 | def get_position(self) -> Point: 402 | x = self.poPoints[0][0] 403 | y = self.poPoints[0][1] 404 | return Point(x, y) 405 | 406 | def angle_between_vectors(self, A, B): 407 | 408 | # Step 1: Compute the dot product 409 | dot_product = np.dot(A, B) 410 | 411 | # Step 2: Calculate the magnitudes (norms) of the vectors 412 | magnitude_A = np.linalg.norm(A) 413 | magnitude_B = np.linalg.norm(B) 414 | 415 | # Step 3: Calculate the cosine of the angle 416 | cos_angle = dot_product / (magnitude_A * magnitude_B) 417 | 418 | # Step 4: Use arccos to find the angle in radians 419 | angle_radians = np.arccos( 420 | np.clip(cos_angle, -1.0, 1.0) 421 | ) # Clip is used to handle floating point errors 422 | 423 | # Step 5: Convert radians to degrees (optional) 424 | angle_degrees = np.degrees(angle_radians) 425 | 426 | return angle_radians, angle_degrees 427 | 428 | 429 | def rectangle_factory( 430 | point: Point, width: float, height: float, mass: float = 1.0, body_type="dynamic" 431 | ): 432 | """ 433 | Factory function for creating a Rectangle object with physics. 434 | 435 | Parameters: 436 | ---------- 437 | point : Point 438 | The initial position of the rectangle. 439 | width : float 440 | The width of the rectangle. 441 | height : float 442 | The height of the rectangle. 443 | mass : float 444 | The mass of the rectangle for physics simulation. 445 | 446 | Returns: 447 | ------- 448 | Rectangle: 449 | A new instance of the Rectangle class. 450 | """ 451 | return Rectangle(point, width, height, mass, body_type) 452 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pygame 3 | import pymunk 4 | import os 5 | import json 6 | 7 | from src.genetic_algorithm import GeneticAlgorithm 8 | from src.agent_parts.vision import Vision 9 | from src.render_object import RenderObject 10 | from src.interface import Button, Interface 11 | from src.agent_parts.limb import Limb 12 | from src.globals import FONT_SIZE, SEGMENT_WIDTH, BLACK, RED 13 | from src.agent_parts.rectangle import Point 14 | from src.environment import Environment, GroundType 15 | from src.agent_parts.creature import Creature 16 | from src.NEATnetwork import NEATNetwork 17 | from src.genome import Genome 18 | from src.genome import Innovation 19 | from src.interface import Button 20 | from pygame_widgets.dropdown import Dropdown 21 | import pygame_widgets 22 | from src.globals import ( 23 | SCREEN_WIDTH, 24 | SCREEN_HEIGHT, 25 | POPULATION_SIZE, 26 | SPECIATION_THRESHOLD, 27 | NUM_GENERATIONS, 28 | SIMULATION_STEPS, 29 | ) 30 | 31 | 32 | def get_saved_file_paths() -> list[str]: 33 | """ 34 | Returns a list of paths to saved genome files. 35 | """ 36 | return [ 37 | os.path.join("models/", f) for f in os.listdir("models/") if f.endswith(".json") 38 | ] 39 | 40 | 41 | def display_genome_run(genome: Genome): 42 | # Initialize Pygame display for visualization 43 | screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) 44 | pygame.display.set_caption("NEAT Simulation") 45 | clock = pygame.time.Clock() 46 | interface = Interface() 47 | 48 | # Initialize Pymunk space and environment for visualization 49 | space = pymunk.Space() 50 | space.gravity = (0, 981) 51 | environment = Environment(screen, space) 52 | environment.ground_type = GroundType.BASIC_GROUND 53 | font = pygame.font.Font(None, 20) 54 | train_enabled = False 55 | display_dropdown = False 56 | 57 | save_enabled = False 58 | 59 | def enable_save(): 60 | nonlocal save_enabled 61 | save_enabled = True 62 | 63 | save_button = Button( 64 | pos=(10, SCREEN_HEIGHT - 100), 65 | width=80, 66 | height=40, 67 | color=(0, 200, 0), 68 | text="Save", 69 | text_color=(255, 255, 255), 70 | hover_color=(0, 255, 0), 71 | active_color=(0, 100, 0), 72 | font=font, 73 | callback=lambda: enable_save(), 74 | ) 75 | 76 | def enable_training(): 77 | nonlocal train_enabled 78 | train_enabled = True 79 | 80 | train_button = Button( 81 | pos=(10, SCREEN_HEIGHT - 50), 82 | width=80, 83 | height=40, 84 | color=(0, 200, 0), 85 | text="Train", 86 | callback=enable_training, 87 | hover_color=(0, 255, 0), 88 | active_color=(0, 100, 0), 89 | font=font, 90 | text_color=(255, 255, 255), 91 | ) 92 | choices = get_saved_file_paths() 93 | dropdown = Dropdown( 94 | screen, 95 | 120, 96 | 10, 97 | 100, 98 | 50, 99 | name="Load Genome", 100 | choices=choices, 101 | borderRadius=3, 102 | colour=pygame.Color("green"), 103 | values=choices, 104 | direction="down", 105 | textHAlign="left", 106 | ) 107 | load_selected = None 108 | 109 | def set_selected(): 110 | nonlocal load_selected 111 | load_selected = dropdown.getSelected() 112 | 113 | display_loaded_button = Button( 114 | pos=(10, SCREEN_HEIGHT - 150), 115 | width=80, 116 | height=40, 117 | color=(0, 200, 0), 118 | text="Display loaded genome", 119 | callback=set_selected, 120 | hover_color=(0, 255, 0), 121 | active_color=(0, 100, 0), 122 | font=font, 123 | text_color=(255, 255, 255), 124 | ) 125 | interface.add_button(save_button) 126 | interface.add_button(train_button) 127 | interface.add_button(display_loaded_button) 128 | 129 | if genome: 130 | network = NEATNetwork(genome) 131 | vision = Vision(Point(0, 0)) 132 | creature = Creature(space, vision) 133 | limb1 = creature.add_limb(100, 20, (300, 300), mass=1) 134 | limb2 = creature.add_limb(100, 20, (350, 300), mass=3) 135 | limb3 = creature.add_limb(80, 40, (400, 300), mass=5) 136 | 137 | # Add motors between limbs 138 | creature.add_motor_on_limbs(limb1, limb2, (325, 300)) 139 | creature.add_motor_on_limbs(limb2, limb3, (375, 300)) 140 | 141 | running = True 142 | while running: 143 | events = pygame.event.get() 144 | for event in events: 145 | if event.type == pygame.QUIT: 146 | running = False 147 | interface.handle_events(event) 148 | 149 | if train_enabled: 150 | print("Training...") 151 | train_enabled = False 152 | genome = train() 153 | display_genome_run(genome) 154 | break 155 | 156 | if load_selected is not None: 157 | print("Loading...") 158 | genome = load_genome(load_selected) 159 | load_selected = None 160 | display_genome_run(genome) 161 | break 162 | 163 | if save_enabled: 164 | print("Saving...") 165 | save_enabled = False 166 | path = save_genome(genome, "best_genome") 167 | print(f"Genome saved to {path}") 168 | 169 | if genome: 170 | # Prepare inputs 171 | inputs = [] 172 | inputs.extend( 173 | [ 174 | creature.vision.get_near_periphery().x, 175 | creature.vision.get_near_periphery().y, 176 | creature.vision.get_far_periphery().x, 177 | creature.vision.get_far_periphery().y, 178 | ] 179 | ) 180 | inputs.extend(creature.get_joint_rates()) 181 | for limb in creature.limbs: 182 | inputs.extend([limb.body.position.x, limb.body.position.y]) 183 | 184 | # Ensure inputs match the expected number 185 | inputs = np.array(inputs) 186 | if len(inputs) != genome.num_inputs: 187 | # Handle input size mismatch if necessary 188 | # For simplicity, we'll pad with zeros or truncate 189 | if len(inputs) < genome.num_inputs: 190 | inputs = np.pad( 191 | inputs, (0, genome.num_inputs - len(inputs)), "constant" 192 | ) 193 | else: 194 | inputs = inputs[: genome.num_inputs] 195 | 196 | outputs = network.forward(inputs) 197 | creature.set_joint_rates(outputs) 198 | 199 | creature.vision.update( 200 | Point( 201 | creature.limbs[0].body.position.x, creature.limbs[0].body.position.y 202 | ), 203 | environment.ground, 204 | environment.offset, 205 | ) 206 | 207 | # Step the physics 208 | space.step(1 / 60.0) 209 | 210 | # Move all the bodies in the space as much as the creature has moved 211 | for body in space.bodies: 212 | creature_offset = creature.limbs[0].body.position.x 213 | body.position = (body.position.x - creature_offset / 100, body.position.y) 214 | 215 | # Render everything 216 | screen.fill((135, 206, 235)) 217 | environment.update() 218 | environment.render() 219 | interface.render(screen) 220 | pygame_widgets.update(events) 221 | 222 | if genome: 223 | creature.render(screen) 224 | 225 | network_position = (SCREEN_WIDTH - 350, 50) 226 | network_size = (300, 300) 227 | draw_neural_network( 228 | genome, screen, position=network_position, size=network_size 229 | ) 230 | # Add text with the fitness value and current x position 231 | font = pygame.font.Font(None, FONT_SIZE) 232 | fitness_text = font.render(f"Fitness: {genome.fitness:.2f}", True, BLACK) 233 | x_pos_text = font.render( 234 | f"X Position: {creature.limbs[0].body.position.x:.2f}", True, BLACK 235 | ) 236 | screen.blit(fitness_text, (10, 10)) 237 | screen.blit(x_pos_text, (10, 30)) 238 | 239 | pygame.display.flip() 240 | clock.tick(60) 241 | 242 | pygame.quit() 243 | 244 | 245 | def draw_neural_network(genome: Genome, screen, position=(0, 0), size=(300, 300)): 246 | """ 247 | Draws the neural network represented by the genome onto the Pygame screen. 248 | 249 | :param genome: The Genome object containing nodes and connections. 250 | :param screen: The Pygame surface to draw on. 251 | :param position: The (x, y) position of the top-left corner where to draw the network. 252 | :param size: The (width, height) size of the area to draw the network. 253 | """ 254 | x, y = position 255 | width, height = size 256 | 257 | # Get nodes by type 258 | input_nodes = [node for node in genome.nodes if node.node_type == "input"] 259 | hidden_nodes = [node for node in genome.nodes if node.node_type == "hidden"] 260 | output_nodes = [node for node in genome.nodes if node.node_type == "output"] 261 | 262 | # Assign positions to nodes 263 | node_positions = {} 264 | 265 | # Vertical spacing 266 | layer_nodes = [input_nodes, hidden_nodes, output_nodes] 267 | max_layer_nodes = max(len(layer) for layer in layer_nodes) 268 | node_radius = 10 269 | vertical_spacing = height / (max_layer_nodes + 1) 270 | 271 | # Horizontal positions for layers 272 | num_layers = 3 273 | layer_x_positions = [x + width * i / (num_layers - 1) for i in range(num_layers)] 274 | 275 | # Position nodes in each layer 276 | for layer_idx, nodes in enumerate(layer_nodes): 277 | layer_x = layer_x_positions[layer_idx] 278 | num_nodes = len(nodes) 279 | for idx, node in enumerate(nodes): 280 | # Center nodes vertically 281 | node_y = y + (idx + 1) * height / (num_nodes + 1) 282 | node_positions[node.id] = (layer_x, node_y) 283 | 284 | # Draw connections 285 | for conn in genome.connections: 286 | if conn.enabled: 287 | in_pos = node_positions.get(conn.in_node) 288 | out_pos = node_positions.get(conn.out_node) 289 | if in_pos and out_pos: 290 | weight = conn.weight 291 | # Color code based on weight 292 | color = (0, 0, 255) if weight > 0 else (255, 0, 0) 293 | # Normalize weight for thickness 294 | thickness = max(1, int(abs(weight) * 2)) 295 | pygame.draw.line(screen, color, in_pos, out_pos, thickness) 296 | 297 | # Draw nodes 298 | for node_id, pos in node_positions.items(): 299 | node = next((n for n in genome.nodes if n.id == node_id), None) 300 | if node: 301 | if node.node_type == "input": 302 | color = (0, 255, 0) # Green 303 | elif node.node_type == "output": 304 | color = (255, 165, 0) # Orange 305 | else: 306 | color = (211, 211, 211) # Light Gray 307 | pygame.draw.circle(screen, color, (int(pos[0]), int(pos[1])), node_radius) 308 | pygame.draw.circle( 309 | screen, (0, 0, 0), (int(pos[0]), int(pos[1])), node_radius, 1 310 | ) 311 | 312 | 313 | MODEL_FILE_PATH = "models/" 314 | 315 | 316 | def save_genome(genome: Genome, filename="saved_genome") -> str: 317 | """Save a genome to a file.""" 318 | 319 | filename += str(genome.fitness) 320 | filename = filename.replace(".", "_") 321 | filename += ".json" 322 | data = genome.to_dict() 323 | path = MODEL_FILE_PATH + filename 324 | print(path) 325 | with open(path, "w") as f: 326 | json.dump(data, f, indent=4) 327 | return path 328 | 329 | 330 | def load_genome(filename: str) -> Genome: 331 | """Load a genome from a file.""" 332 | 333 | with open(filename, "r") as f: 334 | data = json.load(f) 335 | # Ensure the Innovation singleton is updated 336 | genome = Genome.from_dict(data) 337 | Innovation.get_instance().from_dict( 338 | { 339 | "_global_innovation_counter": max( 340 | conn["innovation_number"] for conn in data["connections"] 341 | ), 342 | "_innovation_history": { 343 | (conn["in_node"], conn["out_node"]): conn["innovation_number"] 344 | for conn in data["connections"] 345 | }, 346 | } 347 | ) 348 | return genome 349 | 350 | 351 | def evaluate_genome(genome: Genome) -> float: 352 | """Evaluate a genome by running a simulation and returning its fitness.""" 353 | # Initialize Pymunk space 354 | space = pymunk.Space() 355 | space.gravity = (0, 981) 356 | 357 | # Minimal screen for Pymunk (no rendering during evaluation) 358 | screen = pygame.Surface((1, 1)) 359 | 360 | # Initialize environment 361 | environment = Environment(screen, space) 362 | environment.ground_type = GroundType.BASIC_GROUND 363 | 364 | # Instantiate NEATNetwork and Creature 365 | network = NEATNetwork(genome) 366 | vision = Vision(Point(0, 0)) 367 | creature = Creature(space, vision) 368 | # Initialize creature's limbs and motors 369 | limb1 = creature.add_limb(100, 20, (300, 300), mass=1) 370 | limb2 = creature.add_limb(100, 20, (350, 300), mass=3) 371 | limb3 = creature.add_limb(80, 40, (400, 300), mass=5) 372 | creature.add_motor_on_limbs(limb1, limb2, (325, 300)) 373 | creature.add_motor_on_limbs(limb2, limb3, (375, 300)) 374 | 375 | # Run simulation for a certain number of steps 376 | for _ in range(SIMULATION_STEPS): 377 | inputs = [] 378 | # Prepare inputs 379 | inputs.extend( 380 | [ 381 | creature.vision.get_near_periphery().x, 382 | creature.vision.get_near_periphery().y, 383 | creature.vision.get_far_periphery().x, 384 | creature.vision.get_far_periphery().y, 385 | ] 386 | ) 387 | inputs.extend(creature.get_joint_rates()) 388 | for limb in creature.limbs: 389 | inputs.extend([limb.body.position.x, limb.body.position.y]) 390 | 391 | # Ensure inputs match the expected number 392 | inputs = np.array(inputs) 393 | if len(inputs) != genome.num_inputs: 394 | # Handle input size mismatch if necessary 395 | # For simplicity, we'll pad with zeros or truncate 396 | if len(inputs) < genome.num_inputs: 397 | inputs = np.pad( 398 | inputs, (0, genome.num_inputs - len(inputs)), "constant" 399 | ) 400 | else: 401 | inputs = inputs[: genome.num_inputs] 402 | 403 | outputs = network.forward(inputs) 404 | creature.set_joint_rates(outputs) 405 | 406 | creature.vision.update( 407 | Point(creature.limbs[0].body.position.x, creature.limbs[0].body.position.y), 408 | environment.ground, 409 | environment.offset, 410 | ) 411 | space.step(1 / 60.0) 412 | 413 | # Evaluate fitness (e.g., distance traveled) 414 | fitness = creature.limbs[0].body.position.x 415 | return fitness 416 | 417 | 418 | def train() -> Genome: 419 | 420 | pygame.init() 421 | 422 | # Initialize a temporary creature to determine number of inputs and outputs 423 | temp_space = pymunk.Space() 424 | temp_space.gravity = (0, 981) 425 | temp_screen = pygame.Surface((1, 1)) 426 | temp_environment = Environment(temp_screen, temp_space) 427 | temp_environment.ground_type = GroundType.BASIC_GROUND 428 | 429 | vision = Vision(Point(0, 0)) 430 | temp_creature = Creature(space=temp_space, vision=vision) 431 | limb1 = temp_creature.add_limb(100, 20, (300, 300), mass=1) 432 | limb2 = temp_creature.add_limb(100, 20, (350, 300), mass=3) 433 | limb3 = temp_creature.add_limb(80, 40, (400, 300), mass=5) 434 | temp_creature.add_motor_on_limbs(limb1, limb2, (325, 300)) 435 | temp_creature.add_motor_on_limbs(limb2, limb3, (375, 300)) 436 | 437 | # Determine number of inputs and outputs 438 | amount_of_joints = temp_creature.get_amount_of_joints() 439 | amount_of_limb = temp_creature.get_amount_of_limb() 440 | num_inputs = 4 + amount_of_joints + (amount_of_limb * 2) 441 | num_outputs = amount_of_joints 442 | 443 | # Clean up temporary simulation 444 | del temp_creature 445 | del temp_space 446 | del temp_environment 447 | del temp_screen 448 | 449 | # Initialize a new Creature to pass as initial_creature 450 | # Since GeneticAlgorithm uses initial_creature to determine inputs and outputs, 451 | # we'll create a dummy creature without needing to initialize a full simulation 452 | dummy_space = pymunk.Space() 453 | dummy_space.gravity = (0, 981) 454 | dummy_vision = Vision(Point(0, 0)) 455 | initial_creature = Creature(dummy_space, dummy_vision) 456 | 457 | limb1 = initial_creature.add_limb(100, 20, (300, 300), mass=1) 458 | limb2 = initial_creature.add_limb(100, 20, (350, 300), mass=3) 459 | limb3 = initial_creature.add_limb(80, 40, (400, 300), mass=5) 460 | initial_creature.add_motor_on_limbs(limb1, limb2, (325, 300)) 461 | initial_creature.add_motor_on_limbs(limb2, limb3, (375, 300)) 462 | 463 | # Initialize Genetic Algorithm with population size and initial creature 464 | ga = GeneticAlgorithm( 465 | population_size=POPULATION_SIZE, 466 | initial_creature=initial_creature, 467 | speciation_threshold=SPECIATION_THRESHOLD, 468 | ) 469 | 470 | # Run Evolution 471 | ga.evolve(generations=NUM_GENERATIONS, evaluate_function=evaluate_genome) 472 | 473 | # After evolution, select the best genome 474 | best_genome = max(ga.population, key=lambda g: g.fitness, default=None) 475 | if best_genome: 476 | print("Best Genome:", best_genome) 477 | else: 478 | print("No genomes in population.") 479 | 480 | return best_genome 481 | 482 | 483 | def main(): 484 | 485 | # best_genome = train() 486 | # path = save_genome(best_genome, 'best_genome') 487 | genome = load_genome("models/best_genome3159_670865969072.json") 488 | display_genome_run(genome) 489 | 490 | 491 | if __name__ == "__main__": 492 | main() 493 | --------------------------------------------------------------------------------