Compare commits
5 Commits
69296cf4b7
...
dc86ef4bd7
| Author | SHA1 | Date | |
|---|---|---|---|
| dc86ef4bd7 | |||
| e2d56ffb76 | |||
| 19b946949d | |||
| 78438ae768 | |||
| de45db4393 |
@ -5,7 +5,8 @@ from .simulation_config import (
|
||||
HeadlessConfig,
|
||||
InteractiveConfig,
|
||||
ExperimentConfig,
|
||||
OutputConfig
|
||||
OutputConfig,
|
||||
EntityConfig
|
||||
)
|
||||
from .config_loader import ConfigLoader
|
||||
|
||||
@ -15,5 +16,6 @@ __all__ = [
|
||||
'InteractiveConfig',
|
||||
'ExperimentConfig',
|
||||
'OutputConfig',
|
||||
'EntityConfig',
|
||||
'ConfigLoader'
|
||||
]
|
||||
@ -101,7 +101,14 @@ class ConfigLoader:
|
||||
|
||||
# Extract simulation config if present
|
||||
sim_data = data.get('simulation', {})
|
||||
simulation_config = SimulationConfig(**sim_data)
|
||||
|
||||
# Extract entities config if present
|
||||
entities_data = sim_data.get('entities', {})
|
||||
entities_config = EntityConfig(**entities_data)
|
||||
|
||||
# Create simulation config with entities
|
||||
sim_data_without_entities = {k: v for k, v in sim_data.items() if k != 'entities'}
|
||||
simulation_config = SimulationConfig(entities=entities_config, **sim_data_without_entities)
|
||||
|
||||
return HeadlessConfig(
|
||||
max_ticks=data.get('max_ticks'),
|
||||
@ -115,7 +122,14 @@ class ConfigLoader:
|
||||
"""Convert dictionary to InteractiveConfig."""
|
||||
# Extract simulation config if present
|
||||
sim_data = data.get('simulation', {})
|
||||
simulation_config = SimulationConfig(**sim_data)
|
||||
|
||||
# Extract entities config if present
|
||||
entities_data = sim_data.get('entities', {})
|
||||
entities_config = EntityConfig(**entities_data)
|
||||
|
||||
# Create simulation config with entities
|
||||
sim_data_without_entities = {k: v for k, v in sim_data.items() if k != 'entities'}
|
||||
simulation_config = SimulationConfig(entities=entities_config, **sim_data_without_entities)
|
||||
|
||||
return InteractiveConfig(
|
||||
window_width=data.get('window_width', 0),
|
||||
@ -190,5 +204,5 @@ class ConfigLoader:
|
||||
print(f"Sample configs created in {output_path}")
|
||||
|
||||
|
||||
# Import OutputConfig for the loader
|
||||
from .simulation_config import OutputConfig, SimulationConfig
|
||||
# Import Config classes for the loader
|
||||
from .simulation_config import OutputConfig, SimulationConfig, EntityConfig
|
||||
@ -13,6 +13,7 @@ WHITE = (255, 255, 255)
|
||||
RED = (255, 0, 0)
|
||||
BLUE = (0, 0, 255)
|
||||
GREEN = (0, 255, 0)
|
||||
ORANGE = (255, 165, 0)
|
||||
LIGHT_BLUE = (52, 134, 235)
|
||||
SELECTION_BLUE = (0, 128, 255)
|
||||
SELECTION_GRAY = (128, 128, 128, 80)
|
||||
|
||||
@ -1,21 +1,50 @@
|
||||
"""Simulation configuration classes for different modes."""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional
|
||||
from config.constants import *
|
||||
from typing import List, Optional, Dict, Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class EntityConfig:
|
||||
"""Configuration for entity hyperparameters."""
|
||||
|
||||
# Global entity settings (apply to all entity types unless overridden)
|
||||
max_acceleration: float = 0.125
|
||||
max_angular_acceleration: float = 0.25
|
||||
max_velocity: float = 1.0
|
||||
max_rotational_velocity: float = 3.0
|
||||
|
||||
# Entity type specific configs (all entity parameters should be defined here)
|
||||
entity_types: Dict[str, Dict[str, Any]] = field(default_factory=lambda: {
|
||||
"default_cell": {
|
||||
"reproduction_energy": 1700,
|
||||
"starting_energy": 1000,
|
||||
"interaction_radius": 50,
|
||||
"drag_coefficient": 0.02,
|
||||
"energy_cost_base": 1.5,
|
||||
"neural_network_complexity_cost": 0.08,
|
||||
"movement_cost": 0.25,
|
||||
"food_energy_value": 140,
|
||||
"max_visual_width": 10,
|
||||
"reproduction_count": 2,
|
||||
"mutation_rate": 0.05,
|
||||
"offspring_offset_range": 10
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@dataclass
|
||||
class SimulationConfig:
|
||||
"""Configuration for simulation setup."""
|
||||
grid_width: int = GRID_WIDTH
|
||||
grid_height: int = GRID_HEIGHT
|
||||
cell_size: int = CELL_SIZE
|
||||
grid_width: int = 50
|
||||
grid_height: int = 50
|
||||
cell_size: int = 20
|
||||
initial_cells: int = 50
|
||||
initial_food: int = FOOD_OBJECTS_COUNT
|
||||
food_spawning: bool = FOOD_SPAWNING
|
||||
random_seed: int = RANDOM_SEED
|
||||
default_tps: float = DEFAULT_TPS
|
||||
initial_food: int = 500
|
||||
food_spawning: bool = True
|
||||
random_seed: int = 0
|
||||
default_tps: float = 40.0
|
||||
entities: EntityConfig = field(default_factory=EntityConfig)
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -44,16 +73,7 @@ class HeadlessConfig:
|
||||
output: OutputConfig = field(default_factory=OutputConfig)
|
||||
|
||||
# Simulation core config
|
||||
simulation: SimulationConfig = field(default_factory=lambda: SimulationConfig(
|
||||
grid_width=GRID_WIDTH,
|
||||
grid_height=GRID_HEIGHT,
|
||||
cell_size=CELL_SIZE,
|
||||
initial_cells=50,
|
||||
initial_food=FOOD_OBJECTS_COUNT,
|
||||
food_spawning=FOOD_SPAWNING,
|
||||
random_seed=RANDOM_SEED,
|
||||
default_tps=DEFAULT_TPS
|
||||
))
|
||||
simulation: SimulationConfig = field(default_factory=SimulationConfig)
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -75,16 +95,7 @@ class InteractiveConfig:
|
||||
console_height: int = 120
|
||||
|
||||
# Simulation core config
|
||||
simulation: SimulationConfig = field(default_factory=lambda: SimulationConfig(
|
||||
grid_width=GRID_WIDTH,
|
||||
grid_height=GRID_HEIGHT,
|
||||
cell_size=CELL_SIZE,
|
||||
initial_cells=50,
|
||||
initial_food=FOOD_OBJECTS_COUNT,
|
||||
food_spawning=FOOD_SPAWNING,
|
||||
random_seed=RANDOM_SEED,
|
||||
default_tps=DEFAULT_TPS
|
||||
))
|
||||
simulation: SimulationConfig = field(default_factory=lambda: SimulationConfig(initial_cells=350))
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
68
configs/experiment_tps_sweep.json
Normal file
68
configs/experiment_tps_sweep.json
Normal file
@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "tps_sweep",
|
||||
"description": "Test different TPS values",
|
||||
"runs": 5,
|
||||
"run_duration": 60.0,
|
||||
"run_ticks": null,
|
||||
"variables": {
|
||||
"simulation.default_tps": [
|
||||
10,
|
||||
20,
|
||||
40,
|
||||
80,
|
||||
160
|
||||
]
|
||||
},
|
||||
"base_config": {
|
||||
"max_ticks": null,
|
||||
"max_duration": null,
|
||||
"output": {
|
||||
"enabled": true,
|
||||
"directory": "simulation_output",
|
||||
"formats": [
|
||||
"json"
|
||||
],
|
||||
"collect_metrics": false,
|
||||
"collect_entities": false,
|
||||
"collect_evolution": false,
|
||||
"metrics_interval": 100,
|
||||
"entities_interval": 1000,
|
||||
"evolution_interval": 1000,
|
||||
"real_time": false
|
||||
},
|
||||
"simulation": {
|
||||
"grid_width": 50,
|
||||
"grid_height": 50,
|
||||
"cell_size": 20,
|
||||
"initial_cells": 50,
|
||||
"initial_food": 500,
|
||||
"food_spawning": true,
|
||||
"random_seed": 0,
|
||||
"default_tps": 40.0,
|
||||
"entities": {
|
||||
"max_acceleration": 0.125,
|
||||
"max_angular_acceleration": 0.25,
|
||||
"max_velocity": 1.0,
|
||||
"max_rotational_velocity": 3.0,
|
||||
"entity_types": {
|
||||
"default_cell": {
|
||||
"reproduction_energy": 1700,
|
||||
"starting_energy": 1000,
|
||||
"interaction_radius": 50,
|
||||
"drag_coefficient": 0.02,
|
||||
"energy_cost_base": 1.5,
|
||||
"neural_network_complexity_cost": 0.08,
|
||||
"movement_cost": 0.25,
|
||||
"food_energy_value": 140,
|
||||
"max_visual_width": 10,
|
||||
"reproduction_count": 2,
|
||||
"mutation_rate": 0.05,
|
||||
"offspring_offset_range": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"aggregate_results": true,
|
||||
"aggregate_format": "csv"
|
||||
}
|
||||
50
configs/headless_default.json
Normal file
50
configs/headless_default.json
Normal file
@ -0,0 +1,50 @@
|
||||
{
|
||||
"max_ticks": null,
|
||||
"max_duration": null,
|
||||
"output": {
|
||||
"enabled": true,
|
||||
"directory": "simulation_output",
|
||||
"formats": [
|
||||
"json"
|
||||
],
|
||||
"collect_metrics": false,
|
||||
"collect_entities": false,
|
||||
"collect_evolution": false,
|
||||
"metrics_interval": 100,
|
||||
"entities_interval": 1000,
|
||||
"evolution_interval": 1000,
|
||||
"real_time": false
|
||||
},
|
||||
"simulation": {
|
||||
"grid_width": 50,
|
||||
"grid_height": 50,
|
||||
"cell_size": 20,
|
||||
"initial_cells": 50,
|
||||
"initial_food": 500,
|
||||
"food_spawning": true,
|
||||
"random_seed": 0,
|
||||
"default_tps": 40.0,
|
||||
"entities": {
|
||||
"max_acceleration": 0.125,
|
||||
"max_angular_acceleration": 0.25,
|
||||
"max_velocity": 1.0,
|
||||
"max_rotational_velocity": 3.0,
|
||||
"entity_types": {
|
||||
"default_cell": {
|
||||
"reproduction_energy": 1700,
|
||||
"starting_energy": 1000,
|
||||
"interaction_radius": 50,
|
||||
"drag_coefficient": 0.02,
|
||||
"energy_cost_base": 1.5,
|
||||
"neural_network_complexity_cost": 0.08,
|
||||
"movement_cost": 0.25,
|
||||
"food_energy_value": 140,
|
||||
"max_visual_width": 10,
|
||||
"reproduction_count": 2,
|
||||
"mutation_rate": 0.05,
|
||||
"offspring_offset_range": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
45
configs/interactive_default.json
Normal file
45
configs/interactive_default.json
Normal file
@ -0,0 +1,45 @@
|
||||
{
|
||||
"window_width": 0,
|
||||
"window_height": 0,
|
||||
"vsync": true,
|
||||
"resizable": true,
|
||||
"show_grid": true,
|
||||
"show_interaction_radius": false,
|
||||
"show_legend": true,
|
||||
"control_bar_height": 48,
|
||||
"inspector_width": 260,
|
||||
"properties_width": 320,
|
||||
"console_height": 120,
|
||||
"simulation": {
|
||||
"grid_width": 50,
|
||||
"grid_height": 50,
|
||||
"cell_size": 20,
|
||||
"initial_cells": 500,
|
||||
"initial_food": 500,
|
||||
"food_spawning": true,
|
||||
"random_seed": 0,
|
||||
"default_tps": 40.0,
|
||||
"entities": {
|
||||
"max_acceleration": 0.125,
|
||||
"max_angular_acceleration": 0.25,
|
||||
"max_velocity": 1.0,
|
||||
"max_rotational_velocity": 3.0,
|
||||
"entity_types": {
|
||||
"default_cell": {
|
||||
"reproduction_energy": 1400,
|
||||
"starting_energy": 500,
|
||||
"interaction_radius": 50,
|
||||
"drag_coefficient": 0.02,
|
||||
"energy_cost_base": 1.5,
|
||||
"neural_network_complexity_cost": 0.05,
|
||||
"movement_cost": 0.25,
|
||||
"food_energy_value": 140,
|
||||
"max_visual_width": 10,
|
||||
"reproduction_count": 2,
|
||||
"mutation_rate": 0.05,
|
||||
"offspring_offset_range": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4,8 +4,7 @@
|
||||
import pygame
|
||||
import math
|
||||
from config.constants import *
|
||||
from world.base.brain import CellBrain
|
||||
from world.objects import DefaultCell
|
||||
from world.objects import DefaultCell, FoodObject
|
||||
|
||||
|
||||
class Renderer:
|
||||
@ -251,3 +250,68 @@ class Renderer:
|
||||
size = camera.get_relative_size(width)
|
||||
rect = pygame.Rect(screen_x - size // 2, screen_y - size // 2, size, size)
|
||||
pygame.draw.rect(self.render_area, SELECTION_BLUE, rect, 1)
|
||||
|
||||
def render_food_connections(self, world, camera, selected_objects):
|
||||
"""Render orange bounding boxes around closest food and lines from selected cells."""
|
||||
if not selected_objects:
|
||||
return
|
||||
|
||||
# Find closest food to each selected cell
|
||||
for selected_cell in selected_objects:
|
||||
# Check if selected cell can have food interactions (DefaultCell)
|
||||
if not isinstance(selected_cell, DefaultCell):
|
||||
continue
|
||||
|
||||
# Find all food objects in world
|
||||
all_objects = world.get_objects()
|
||||
food_objects = [obj for obj in all_objects if isinstance(obj, FoodObject)]
|
||||
|
||||
if not food_objects:
|
||||
continue
|
||||
|
||||
# Get selected cell position
|
||||
cell_x, cell_y = selected_cell.position.get_position()
|
||||
|
||||
# Find closest food object
|
||||
closest_food = None
|
||||
closest_distance = float('inf')
|
||||
|
||||
for food in food_objects:
|
||||
food_x, food_y = food.position.get_position()
|
||||
distance = ((food_x - cell_x) ** 2 + (food_y - cell_y) ** 2) ** 0.5
|
||||
|
||||
if distance < closest_distance:
|
||||
closest_distance = distance
|
||||
closest_food = food
|
||||
|
||||
# Draw bounding box around closest food and connecting line
|
||||
if closest_food and closest_food != selected_cell:
|
||||
food_x, food_y = closest_food.position.get_position()
|
||||
food_width = closest_food.max_visual_width
|
||||
food_size = int(food_width * camera.zoom)
|
||||
|
||||
# Calculate bounding box position (centered on food)
|
||||
box_x, box_y = camera.world_to_screen(food_x, food_y)
|
||||
box_rect = pygame.Rect(
|
||||
box_x - food_size,
|
||||
box_y - food_size,
|
||||
food_size * 2,
|
||||
food_size * 2
|
||||
)
|
||||
|
||||
# Draw orange bounding box (2 pixel width)
|
||||
pygame.draw.rect(self.render_area, ORANGE, box_rect, 2)
|
||||
|
||||
# Draw line from selected cell to closest food
|
||||
screen_x, screen_y = camera.world_to_screen(cell_x, cell_y)
|
||||
|
||||
# Calculate line thickness based on zoom (minimum 1 pixel)
|
||||
line_thickness = max(1, int(2 * camera.zoom))
|
||||
|
||||
pygame.draw.line(
|
||||
self.render_area,
|
||||
ORANGE,
|
||||
(screen_x, screen_y),
|
||||
(box_x, box_y),
|
||||
line_thickness
|
||||
)
|
||||
|
||||
@ -9,7 +9,7 @@ from world.world import World, Position, Rotation
|
||||
from world.simulation_interface import Camera
|
||||
from .event_bus import EventBus, EventType, Event
|
||||
from .timing import TimingController
|
||||
from config.constants import *
|
||||
from config.constants import RENDER_BUFFER
|
||||
from config.simulation_config import SimulationConfig
|
||||
|
||||
|
||||
@ -82,7 +82,8 @@ class SimulationCore:
|
||||
x=random.randint(-half_width // 2, half_width // 2),
|
||||
y=random.randint(-half_height // 2, half_height // 2)
|
||||
),
|
||||
Rotation(angle=0)
|
||||
Rotation(angle=0),
|
||||
entity_config=getattr(self.config, 'entities', None)
|
||||
)
|
||||
# Mutate the initial behavioral model for variety
|
||||
cell.behavioral_model = cell.behavioral_model.mutate(3)
|
||||
|
||||
@ -4,23 +4,22 @@ import sys
|
||||
|
||||
from pygame_gui import UIManager
|
||||
|
||||
from world.simulation_interface import Camera
|
||||
from config.constants import *
|
||||
from config.constants import BLACK, DEFAULT_TPS, MAX_FPS
|
||||
from core.input_handler import InputHandler
|
||||
from core.renderer import Renderer
|
||||
from core.simulation_core import SimulationCore, SimulationConfig
|
||||
from core.simulation_core import SimulationCore
|
||||
from core.event_bus import EventBus
|
||||
from ui.hud import HUD
|
||||
|
||||
import cProfile
|
||||
import pstats
|
||||
|
||||
|
||||
class SimulationEngine:
|
||||
"""Interactive simulation engine with UI (wrapper around SimulationCore)."""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, config=None):
|
||||
pygame.init()
|
||||
self.config = config
|
||||
self.event_bus = EventBus()
|
||||
self._init_window()
|
||||
self._init_simulation()
|
||||
@ -47,11 +46,23 @@ class SimulationEngine:
|
||||
|
||||
def _init_window(self):
|
||||
info = pygame.display.Info()
|
||||
self.window_width = int(info.current_w // 1.5)
|
||||
self.window_height = int(info.current_h // 1.5)
|
||||
|
||||
# Use config or defaults
|
||||
if self.config:
|
||||
self.window_width = self.config.window_width or int(info.current_w // 1.5)
|
||||
self.window_height = self.config.window_height or int(info.current_h // 1.5)
|
||||
vsync = 1 if self.config.vsync else 0
|
||||
resizable = self.config.resizable
|
||||
else:
|
||||
self.window_width = int(info.current_w // 1.5)
|
||||
self.window_height = int(info.current_h // 1.5)
|
||||
vsync = 1
|
||||
resizable = True
|
||||
|
||||
screen_flags = pygame.RESIZABLE if resizable else 0
|
||||
self.screen = pygame.display.set_mode(
|
||||
(self.window_width, self.window_height),
|
||||
pygame.RESIZABLE, vsync=1
|
||||
screen_flags, vsync=vsync
|
||||
)
|
||||
pygame.display.set_caption("Dynamic Abstraction System Testing")
|
||||
self.clock = pygame.time.Clock()
|
||||
@ -67,26 +78,30 @@ class SimulationEngine:
|
||||
# Set HUD reference in input handler after both are created
|
||||
self.input_handler.set_hud(self.hud)
|
||||
|
||||
# Pass config settings to HUD and input handler
|
||||
if self.config:
|
||||
self.input_handler.show_grid = self.config.show_grid
|
||||
self.input_handler.show_interaction_radius = self.config.show_interaction_radius
|
||||
self.input_handler.show_legend = self.config.show_legend
|
||||
|
||||
self._update_simulation_view()
|
||||
|
||||
def _init_simulation(self):
|
||||
# Initialize default sim view rect (will be updated by _init_ui)
|
||||
self.sim_view_width = self.window_width - 400 # Rough estimate for inspector width
|
||||
self.sim_view_height = self.window_height - 200 # Rough estimate for control bar height
|
||||
if self.config:
|
||||
self.sim_view_width = self.window_width - self.config.inspector_width
|
||||
self.sim_view_height = self.window_height - self.config.control_bar_height
|
||||
else:
|
||||
self.sim_view_width = self.window_width - 400 # Rough estimate for inspector width
|
||||
self.sim_view_height = self.window_height - 200 # Rough estimate for control bar height
|
||||
self.sim_view = pygame.Surface((self.sim_view_width, self.sim_view_height))
|
||||
self.sim_view_rect = self.sim_view.get_rect(topleft=(200, 48)) # Rough estimate
|
||||
|
||||
# Create simulation core
|
||||
sim_config = SimulationConfig(
|
||||
grid_width=GRID_WIDTH,
|
||||
grid_height=GRID_HEIGHT,
|
||||
cell_size=CELL_SIZE,
|
||||
initial_cells=50,
|
||||
initial_food=FOOD_OBJECTS_COUNT,
|
||||
food_spawning=FOOD_SPAWNING,
|
||||
random_seed=RANDOM_SEED,
|
||||
default_tps=DEFAULT_TPS
|
||||
)
|
||||
# Create simulation core with config
|
||||
if self.config and self.config.simulation:
|
||||
sim_config = self.config.simulation
|
||||
else:
|
||||
raise(ValueError("Simulation configuration must be provided for SimulationEngine."))
|
||||
|
||||
self.simulation_core = SimulationCore(sim_config, self.event_bus)
|
||||
|
||||
@ -147,7 +162,6 @@ class SimulationEngine:
|
||||
|
||||
def run(self):
|
||||
"""Run the interactive simulation engine."""
|
||||
print(f"World buffer: {self.simulation_core.world.current_buffer}")
|
||||
self.simulation_core.start()
|
||||
|
||||
while self.running:
|
||||
@ -259,6 +273,12 @@ class SimulationEngine:
|
||||
self.input_handler.selected_objects,
|
||||
self.simulation_core.camera
|
||||
)
|
||||
if self.input_handler.show_interaction_radius:
|
||||
self.renderer.render_food_connections(
|
||||
self.simulation_core.world,
|
||||
self.simulation_core.camera,
|
||||
self.input_handler.selected_objects
|
||||
)
|
||||
self.screen.blit(self.sim_view, (self.sim_view_rect.left, self.sim_view_rect.top))
|
||||
|
||||
def _update_and_render_ui(self, deltatime):
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
import time
|
||||
import signal
|
||||
import sys
|
||||
from typing import Dict, Any, Optional, List
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ from pathlib import Path
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from config import ConfigLoader, InteractiveConfig
|
||||
from config import ConfigLoader
|
||||
from core.simulation_engine import SimulationEngine
|
||||
|
||||
|
||||
@ -49,7 +49,7 @@ def main():
|
||||
# Run simulation
|
||||
try:
|
||||
print("Starting interactive simulation...")
|
||||
engine = SimulationEngine()
|
||||
engine = SimulationEngine(config)
|
||||
engine.run()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
|
||||
@ -8,6 +8,7 @@ dependencies = [
|
||||
"numpy>=2.3.0",
|
||||
"pandas>=2.3.3",
|
||||
"pre-commit>=4.2.0",
|
||||
"psutil>=7.0.0",
|
||||
"pydantic>=2.11.5",
|
||||
"pygame>=2.6.1",
|
||||
"pygame-gui>=0.6.14",
|
||||
|
||||
2
uv.lock
generated
2
uv.lock
generated
@ -142,6 +142,7 @@ dependencies = [
|
||||
{ name = "numpy" },
|
||||
{ name = "pandas" },
|
||||
{ name = "pre-commit" },
|
||||
{ name = "psutil" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pygame" },
|
||||
{ name = "pygame-gui" },
|
||||
@ -162,6 +163,7 @@ requires-dist = [
|
||||
{ name = "numpy", specifier = ">=2.3.0" },
|
||||
{ name = "pandas", specifier = ">=2.3.3" },
|
||||
{ name = "pre-commit", specifier = ">=4.2.0" },
|
||||
{ name = "psutil", specifier = ">=7.0.0" },
|
||||
{ name = "pydantic", specifier = ">=2.11.5" },
|
||||
{ name = "pygame", specifier = ">=2.6.1" },
|
||||
{ name = "pygame-gui", specifier = ">=0.6.14" },
|
||||
|
||||
102
world/objects.py
102
world/objects.py
@ -2,17 +2,14 @@ import math
|
||||
import random
|
||||
|
||||
from config.constants import MAX_VELOCITY, MAX_ACCELERATION, MAX_ROTATIONAL_VELOCITY, MAX_ANGULAR_ACCELERATION
|
||||
from typing import List, Any, Union, Optional
|
||||
from world.base.brain import CellBrain
|
||||
from world.behavioral import BehavioralModel
|
||||
from world.world import Position, BaseEntity, Rotation
|
||||
import pygame
|
||||
from typing import Optional, List, Any, Union
|
||||
|
||||
from world.utils import get_distance_between_objects
|
||||
from world.physics import Physics
|
||||
|
||||
from math import atan2, degrees
|
||||
|
||||
|
||||
class DebugRenderObject(BaseEntity):
|
||||
"""
|
||||
@ -93,14 +90,30 @@ class FoodObject(BaseEntity):
|
||||
self.max_visual_width: int = 8
|
||||
self.decay: int = 0
|
||||
self.decay_rate: int = 1
|
||||
self.max_decay = 200
|
||||
self.max_decay = 400
|
||||
self.interaction_radius: int = 50
|
||||
self.neighbors: int = 0
|
||||
self.claimed_this_tick = False # Track if food was claimed this tick
|
||||
self.flags: dict[str, bool] = {
|
||||
"death": False,
|
||||
"can_interact": True,
|
||||
}
|
||||
|
||||
def try_claim(self) -> bool:
|
||||
"""
|
||||
Atomically claims this food for consumption within the current tick.
|
||||
|
||||
:return: True if successfully claimed, False if already claimed this tick
|
||||
"""
|
||||
if not self.claimed_this_tick:
|
||||
self.claimed_this_tick = True
|
||||
return True
|
||||
return False
|
||||
|
||||
def reset_claim(self) -> None:
|
||||
"""Reset the claim for next tick"""
|
||||
self.claimed_this_tick = False
|
||||
|
||||
def tick(self, interactable: Optional[List[BaseEntity]] = None) -> Union["FoodObject", List["FoodObject"]]:
|
||||
"""
|
||||
Updates the food object, increasing decay and flagging for death if decayed.
|
||||
@ -108,6 +121,9 @@ class FoodObject(BaseEntity):
|
||||
:param interactable: List of nearby entities (unused).
|
||||
:return: Self
|
||||
"""
|
||||
# Reset claim at the beginning of each tick
|
||||
self.reset_claim()
|
||||
|
||||
if interactable is None:
|
||||
interactable = []
|
||||
|
||||
@ -235,15 +251,45 @@ class DefaultCell(BaseEntity):
|
||||
"""
|
||||
Cell object
|
||||
"""
|
||||
def __init__(self, starting_position: Position, starting_rotation: Rotation) -> None:
|
||||
def __init__(self, starting_position: Position, starting_rotation: Rotation, entity_config=None) -> None:
|
||||
"""
|
||||
Initializes the cell.
|
||||
|
||||
:param starting_position: The position of the object.
|
||||
:param entity_config: Configuration for entity hyperparameters.
|
||||
"""
|
||||
|
||||
super().__init__(starting_position, starting_rotation)
|
||||
self.drag_coefficient: float = 0.1
|
||||
|
||||
# Use entity config or defaults
|
||||
if entity_config:
|
||||
cell_config = entity_config.entity_types.get("default_cell", {})
|
||||
self.drag_coefficient: float = cell_config.get("drag_coefficient", 0.02)
|
||||
self.energy: int = cell_config.get("starting_energy", 1000)
|
||||
self.max_visual_width: int = cell_config.get("max_visual_width", 10)
|
||||
self.interaction_radius: int = cell_config.get("interaction_radius", 50)
|
||||
self.reproduction_energy: int = cell_config.get("reproduction_energy", 1700)
|
||||
self.food_energy_value: int = cell_config.get("food_energy_value", 140)
|
||||
self.energy_cost_base: float = cell_config.get("energy_cost_base", 1.5)
|
||||
self.neural_network_complexity_cost: float = cell_config.get("neural_network_complexity_cost", 0.08)
|
||||
self.movement_cost: float = cell_config.get("movement_cost", 0.25)
|
||||
self.reproduction_count: int = cell_config.get("reproduction_count", 2)
|
||||
self.mutation_rate: float = cell_config.get("mutation_rate", 0.05)
|
||||
self.offspring_offset_range: int = cell_config.get("offspring_offset_range", 10)
|
||||
else:
|
||||
# Fallback to hardcoded defaults
|
||||
self.drag_coefficient: float = 0.02
|
||||
self.energy: int = 1000
|
||||
self.max_visual_width: int = 10
|
||||
self.interaction_radius: int = 50
|
||||
self.reproduction_energy: int = 1700
|
||||
self.food_energy_value: int = 140
|
||||
self.energy_cost_base: float = 1.5
|
||||
self.neural_network_complexity_cost: float = 0.08
|
||||
self.movement_cost: float = 0.25
|
||||
self.reproduction_count: int = 2
|
||||
self.mutation_rate: float = 0.05
|
||||
self.offspring_offset_range: int = 10
|
||||
|
||||
self.velocity: tuple[int, int] = (0, 0)
|
||||
self.acceleration: tuple[int, int] = (0, 0)
|
||||
@ -251,20 +297,17 @@ class DefaultCell(BaseEntity):
|
||||
self.rotational_velocity: int = 0
|
||||
self.angular_acceleration: int = 0
|
||||
|
||||
self.energy: int = 1000
|
||||
|
||||
self.behavioral_model: CellBrain = CellBrain()
|
||||
|
||||
self.max_visual_width: int = 10
|
||||
self.interaction_radius: int = 50
|
||||
self.flags: dict[str, bool] = {
|
||||
"death": False,
|
||||
"can_interact": True,
|
||||
}
|
||||
|
||||
self.tick_count = 0
|
||||
self.entity_config = entity_config
|
||||
|
||||
self.physics = Physics(0.02, 0.05)
|
||||
self.physics = Physics(self.drag_coefficient, self.drag_coefficient*1.5)
|
||||
|
||||
|
||||
def set_brain(self, behavioral_model: CellBrain) -> None:
|
||||
@ -300,27 +343,30 @@ class DefaultCell(BaseEntity):
|
||||
distance_to_food = get_distance_between_objects(self, food_object)
|
||||
|
||||
if distance_to_food < self.max_visual_width and food_objects:
|
||||
self.energy += 130
|
||||
food_object.flag_for_death()
|
||||
# Use atomic consumption to prevent race conditions
|
||||
if food_object.try_claim():
|
||||
self.energy += self.food_energy_value
|
||||
food_object.flag_for_death()
|
||||
return self
|
||||
|
||||
if self.energy >= 1600:
|
||||
if self.energy >= self.reproduction_energy:
|
||||
# too much energy, split
|
||||
duplicate_x, duplicate_y = self.position.get_position()
|
||||
duplicate_x += random.randint(-self.max_visual_width, self.max_visual_width)
|
||||
duplicate_y += random.randint(-self.max_visual_width, self.max_visual_width)
|
||||
offspring = []
|
||||
|
||||
duplicate_x_2, duplicate_y_2 = self.position.get_position()
|
||||
duplicate_x_2 += random.randint(-self.max_visual_width, self.max_visual_width)
|
||||
duplicate_y_2 += random.randint(-self.max_visual_width, self.max_visual_width)
|
||||
for _ in range(self.reproduction_count):
|
||||
duplicate_x, duplicate_y = self.position.get_position()
|
||||
duplicate_x += random.randint(-self.offspring_offset_range, self.offspring_offset_range)
|
||||
duplicate_y += random.randint(-self.offspring_offset_range, self.offspring_offset_range)
|
||||
|
||||
new_cell = DefaultCell(Position(x=int(duplicate_x), y=int(duplicate_y)), Rotation(angle=random.randint(0, 359)))
|
||||
new_cell.set_brain(self.behavioral_model.mutate(0.05))
|
||||
new_cell = DefaultCell(
|
||||
Position(x=int(duplicate_x), y=int(duplicate_y)),
|
||||
Rotation(angle=random.randint(0, 359)),
|
||||
entity_config=getattr(self, 'entity_config', None)
|
||||
)
|
||||
new_cell.set_brain(self.behavioral_model.mutate(self.mutation_rate))
|
||||
offspring.append(new_cell)
|
||||
|
||||
new_cell_2 = DefaultCell(Position(x=int(duplicate_x_2), y=int(duplicate_y_2)), Rotation(angle=random.randint(0, 359)))
|
||||
new_cell_2.set_brain(self.behavioral_model.mutate(0.05))
|
||||
|
||||
return [new_cell, new_cell_2]
|
||||
return offspring
|
||||
|
||||
input_data = {
|
||||
"distance": distance_to_food,
|
||||
@ -350,7 +396,7 @@ class DefaultCell(BaseEntity):
|
||||
|
||||
movement_cost = abs(output_data["angular_acceleration"]) + abs(output_data["linear_acceleration"])
|
||||
|
||||
self.energy -= (self.behavioral_model.neural_network.network_cost * 0.1) + 1.2 + (0.15 * movement_cost)
|
||||
self.energy -= (self.behavioral_model.neural_network.network_cost * self.neural_network_complexity_cost) + self.energy_cost_base + (self.movement_cost * movement_cost)
|
||||
|
||||
return self
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from collections import defaultdict
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Dict, Tuple, Optional, Any, TypeVar, Union
|
||||
from typing import List, Dict, Tuple, Optional, Any, TypeVar
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
T = TypeVar("T", bound="BaseEntity")
|
||||
@ -170,6 +170,8 @@ class World:
|
||||
next_buffer: int = 1 - self.current_buffer
|
||||
self.buffers[next_buffer].clear()
|
||||
|
||||
# Food claims auto-reset in FoodObject.tick()
|
||||
|
||||
for obj_list in self.buffers[self.current_buffer].values():
|
||||
for obj in obj_list:
|
||||
if obj.flags["death"]:
|
||||
@ -178,14 +180,17 @@ class World:
|
||||
interactable = self.query_objects_within_radius(
|
||||
obj.position.x, obj.position.y, obj.interaction_radius
|
||||
)
|
||||
interactable.remove(obj)
|
||||
# Create defensive copy to prevent shared state corruption
|
||||
interactable = interactable.copy()
|
||||
if obj in interactable:
|
||||
interactable.remove(obj)
|
||||
new_obj = obj.tick(interactable)
|
||||
else:
|
||||
new_obj = obj.tick()
|
||||
if new_obj is None:
|
||||
continue
|
||||
|
||||
# reproduction code
|
||||
# reproduction code - buffer new entities for atomic state transition
|
||||
if isinstance(new_obj, list):
|
||||
for item in new_obj:
|
||||
if isinstance(item, BaseEntity):
|
||||
@ -194,6 +199,8 @@ class World:
|
||||
else:
|
||||
cell = self._hash_position(new_obj.position)
|
||||
self.buffers[next_buffer][cell].append(new_obj)
|
||||
|
||||
# Atomic buffer switch - all state changes become visible simultaneously
|
||||
self.current_buffer = next_buffer
|
||||
|
||||
def add_object(self, new_object: BaseEntity) -> None:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user