├── __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 | 
8 | 
9 | 
10 | [](https://opensource.org/licenses/MIT)
11 | [](https://img.shields.io/badge/version-0.0.1-blue)
12 |
13 |

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 |
123 |
124 | 
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 |
--------------------------------------------------------------------------------