Compare commits

..

No commits in common. "master" and "ui-rework" have entirely different histories.

51 changed files with 252 additions and 7063 deletions

View File

@ -18,9 +18,6 @@ jobs:
- name: Set up Python - name: Set up Python
run: uv python install run: uv python install
- name: Install system dependencies
run: sudo apt-get update && sudo apt-get install -y libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev
- name: Install the project - name: Install the project
run: uv sync --locked --all-extras --dev --link-mode=copy run: uv sync --locked --all-extras --dev --link-mode=copy

2
.gitignore vendored
View File

@ -3,5 +3,3 @@ uv.lock
.idea/ .idea/
.pytest_cache/ .pytest_cache/
.ruff_cache/ .ruff_cache/
simulation_output/
__pycache__/

View File

@ -1,340 +0,0 @@
# Dynamic Abstraction System - Complete Architecture
## Overview
The Dynamic Abstraction System is a sophisticated 2D cellular simulation platform featuring autonomous entities with evolving neural networks. The system demonstrates headless-first architecture with clean separation of concerns, supporting both interactive development and production-scale batch processing.
## Architecture Principles
1. **Headless-First Design**: Core simulation logic runs independently of any UI dependencies
2. **Separation of Concerns**: Each component has a single, well-defined responsibility
3. **Loose Coupling**: Components communicate through events and interfaces rather than direct dependencies
4. **Configuration-Driven**: Behavior controlled through structured configuration files
5. **Extensible Data Collection**: Comprehensive metrics and evolution tracking capabilities
6. **Dual-Mode Support**: Both interactive debugging and headless batch processing
## Complete Directory Structure
```
DynamicAbstractionSystem/
├── main.py # Legacy entry point (interactive)
├── entry_points/ # Application entry points
│ ├── headless_main.py # Headless simulation entry
│ ├── interactive_main.py # Interactive simulation entry
│ └── __init__.py
├── core/ # Core simulation infrastructure
│ ├── simulation_core.py # Pure simulation logic (no UI deps)
│ ├── timing.py # TPS/timing management
│ ├── event_bus.py # Decoupled communication system
│ ├── simulation_engine.py # Interactive engine (UI wrapper)
│ ├── input_handler.py # Pure input processing
│ ├── renderer.py # World-to-screen rendering
│ └── __init__.py
├── engines/ # Specialized simulation engines
│ ├── headless_engine.py # Production headless engine
│ └── __init__.py
├── world/ # Simulation world and entities
│ ├── __init__.py
│ ├── world.py # Spatial partitioning system
│ ├── objects.py # Entity implementations
│ ├── base/ # Base entity systems
│ │ ├── brain.py # Neural network implementations
│ │ ├── neural.py # Flexible neural network
│ │ └── behavioral.py # Behavioral models
│ └── simulation_interface.py # Camera and spatial interfaces
├── ui/ # User interface components
│ ├── hud.py # Heads-up display and panels
│ └── __init__.py
├── config/ # Configuration management
│ ├── constants.py # Global constants
│ ├── simulation_config.py # Configuration classes
│ ├── config_loader.py # Configuration loading utilities
│ └── __init__.py
├── output/ # Data collection and output
│ ├── __init__.py
│ ├── collectors/ # Data collection modules
│ │ ├── base_collector.py # Collector interface
│ │ ├── metrics_collector.py # Basic metrics collection
│ │ ├── entity_collector.py # Entity state tracking
│ │ └── evolution_collector.py # Evolution tracking
│ ├── formatters/ # Output formatting
│ │ ├── base_formatter.py # Formatter interface
│ │ ├── json_formatter.py # JSON output
│ │ └── csv_formatter.py # CSV output
│ └── writers/ # Output destinations
│ ├── base_writer.py # Writer interface
│ └── file_writer.py # File system output
└── tests/ # Test suite
├── __init__.py
└── [test files]
```
## Core System Architecture
### 1. Simulation Layer
#### SimulationCore (`core/simulation_core.py`)
- **Purpose**: Pure simulation logic with zero UI dependencies
- **Responsibilities**:
- World management and entity lifecycle
- Entity spawning and spatial queries
- Event-driven state management
- Timing coordination
- **Key Features**:
- Event-driven architecture via EventBus
- Configurable simulation parameters
- Complete UI isolation
- Spatial query methods (radius, range, nearest)
#### TimingController (`core/timing.py`)
- **Purpose**: Precise timing management and simulation control
- **Responsibilities**:
- TPS (ticks per second) regulation
- Pause/sprint state management
- Speed multipliers
- Performance timing
- **Key Features**:
- Configurable target TPS
- Sprint mode for accelerated execution
- Pause/resume functionality
- Real-time performance metrics
#### EventBus (`core/event_bus.py`)
- **Purpose**: Decoupled inter-component communication
- **Responsibilities**:
- Event routing and subscription management
- Type-safe event dispatch
- Event history tracking
- **Key Features**:
- Type-safe event system
- Event history for debugging
- Publisher-subscriber pattern
- Loose coupling between components
### 2. Engine Layer
#### SimulationEngine (`core/simulation_engine.py`)
- **Purpose**: Interactive simulation engine with UI integration
- **Responsibilities**:
- Main game loop coordination
- UI event processing
- Rendering pipeline management
- Component orchestration
- **Key Features**:
- Pygame integration
- Real-time input handling
- Rendering coordination
- UI state synchronization
#### HeadlessSimulationEngine (`engines/headless_engine.py`)
- **Purpose**: Production-grade headless simulation execution
- **Responsibilities**:
- Batch simulation execution
- Data collection management
- Output generation
- Performance monitoring
- **Key Features**:
- No UI dependencies
- Maximum speed execution (2000+ TPS)
- Sprint mode integration for unlimited performance
- Precise TPS control when specified
- Comprehensive data collection
- Multiple output formats
- Configurable execution parameters
- Graceful shutdown handling
### 3. World Layer
#### World (`world/world.py`)
- **Purpose**: Spatial partitioning and entity management
- **Responsibilities**:
- Spatial indexing and partitioning
- Entity lifecycle management
- Collision detection support
- Spatial queries
- **Key Features**:
- Grid-based spatial partitioning
- Double buffering for performance
- Efficient spatial queries
- Entity add/remove operations
#### Entity System (`world/objects.py`, `world/base/`)
- **Purpose**: Entity implementations and behavioral systems
- **Key Components**:
- `BaseEntity`: Abstract base class for all entities
- `DefaultCell`: Autonomous cellular entity with neural networks
- `FoodObject`: Energy source entities
- `CellBrain`: Neural network-based decision making
- `FlexibleNeuralNetwork`: Dynamic topology neural networks
### 4. Data Collection System
#### Collectors (`output/collectors/`)
- **BaseCollector**: Abstract base with tick-based collection logic
- **MetricsCollector**: Basic simulation metrics (TPS, entity counts, timing)
- **EntityCollector**: Detailed entity state snapshots
- **EvolutionCollector**: Neural network evolution and generational tracking
- **Tick-Based Intervals**: Uses simulation ticks for accurate collection timing
- **Every-Tick Option**: Configurable for maximum data resolution
#### Formatters (`output/formatters/`)
- **JSONFormatter**: Human-readable JSON output
- **CSVFormatter**: Spreadsheet-compatible CSV output
#### Writers (`output/writers/`)
- **FileWriter**: File system output with batch processing
### 5. Configuration System
#### Configuration Classes (`config/simulation_config.py`)
- **HeadlessConfig**: Headless simulation parameters
- **InteractiveConfig**: Interactive UI configuration
- **ExperimentConfig**: Automated experiment configuration
- **OutputConfig**: Data collection and output settings
#### Configuration Loader (`config/config_loader.py`)
- **Purpose**: Dynamic configuration loading and validation
- **Features**:
- JSON/YAML support
- Configuration validation
- Sample configuration generation
- Lazy loading to avoid circular dependencies
## Entry Points
### Interactive Mode
```bash
# Standard interactive simulation
python entry_points/interactive_main.py
# With custom configuration
python entry_points/interactive_main.py --config configs/interactive.json
# Legacy compatibility
python main.py
```
### Headless Mode
```bash
# Basic headless execution (maximum speed by default)
python entry_points/headless_main.py
# With custom parameters
python entry_points/headless_main.py --max-ticks 10000 --tps 60 --output-dir results
# With data collection on every tick
python entry_points/headless_main.py --max-ticks 1000 --collect-every-tick
# With configuration file
python entry_points/headless_main.py --config configs/experiment.json
```
## Input and Control System
### Keyboard Controls
- **Space**: Toggle pause/play
- **Right Shift**: Toggle sprint mode
- **Left Shift**: Hold for temporary 2x speed boost
- **S**: Step forward one tick
- **R**: Reset camera position
- **G**: Toggle grid display
- **I**: Toggle interaction radius display
- **L**: Toggle legend display
- **WASD**: Move camera
- **Mouse wheel**: Zoom in/out
- **Middle mouse drag**: Pan camera
- **Left click/drag**: Select objects
### Input Handler Architecture
- **Pure Input Processing**: No state management, only input mapping
- **Callback System**: All simulation control actions use callbacks
- **Event-Driven**: Decoupled from simulation state
## Data Collection and Output
### Comprehensive Metrics
- **Performance Metrics**: TPS, timing statistics, execution duration
- **Entity Statistics**: Population counts, energy distributions, age demographics
- **Evolution Data**: Neural network architectures, generational statistics, mutation tracking
- **World State**: Spatial distributions, resource availability
### Output Formats
- **JSON**: Structured, human-readable, web-compatible
- **CSV**: Tabular format for spreadsheet analysis
- **File-based**: Batch output with configurable intervals
## Performance Architecture
### Spatial Optimization
- **Grid-based Spatial Partitioning**: Efficient entity queries
- **Double Buffering**: Prevents frame interference
- **Spatial Indexing**: Fast proximity and range queries
### Timing Optimization
- **Configurable TPS**: Adjustable simulation speed
- **Maximum Speed Mode**: Unlimited execution speed for batch processing (2000+ TPS)
- **Sprint Mode Integration**: Automatic sprint mode for maximum performance
- **Precise Timing**: Frame-independent simulation updates
- **Accurate TPS Control**: Precise timing when TPS is specified
### Memory Management
- **Entity Pooling**: Efficient memory allocation for entities
- **Event System**: Minimal memory overhead for inter-component communication
- **Lazy Loading**: Configuration and components loaded on demand
## Testing Architecture
### Test Coverage
- **Unit Tests**: Core component isolation testing
- **Integration Tests**: Component interaction testing
- **Performance Tests**: Benchmarking and profiling
- **End-to-End Tests**: Complete simulation workflow testing
### Testing Tools
- **cProfile Integration**: Built-in performance profiling
- **pytest Framework**: Structured test execution
- **Mock Components**: Isolated component testing
## Extension Points
### Custom Collectors
- Extensible data collection system
- Plugin architecture for custom metrics
- Configurable collection intervals
### Custom Formatters
- Pluggable output format system
- Custom serialization support
- Multiple output destinations
### Custom Engines
- Specialized simulation engines
- Custom execution patterns
- Integration interfaces
## Deployment Architecture
### Production Deployment
- **Headless Execution**: Server deployment without display dependencies
- **Batch Processing**: Automated experiment execution
- **Configuration-Driven**: Environment-specific configurations
- **Data Pipeline**: Automated data collection and output
### Development Environment
- **Interactive Debugging**: Real-time visualization and control
- **Hot Reloading**: Configuration changes without restart
- **Performance Monitoring**: Real-time TPS and performance metrics
- **Extensive Logging**: Detailed execution tracking
## Architectural Benefits
1. **Maintainability**: Clear separation of concerns and modular design
2. **Testability**: Isolated components enable comprehensive unit testing
3. **Scalability**: Headless mode supports large-scale batch processing
4. **Flexibility**: Configuration-driven behavior adaptation
5. **Performance**: Optimized spatial queries and timing management
6. **Extensibility**: Plugin architecture for custom functionality
7. **Reliability**: Event-driven architecture with error handling
8. **Portability**: Headless operation enables deployment across platforms
This architecture provides a robust foundation for both interactive development and production-scale simulation experiments, with clean separation between simulation logic, user interface, and data collection systems.

View File

@ -1,21 +0,0 @@
"""Configuration system for simulation modes."""
from .simulation_config import (
SimulationConfig,
HeadlessConfig,
InteractiveConfig,
ExperimentConfig,
OutputConfig,
EntityConfig
)
from .config_loader import ConfigLoader
__all__ = [
'SimulationConfig',
'HeadlessConfig',
'InteractiveConfig',
'ExperimentConfig',
'OutputConfig',
'EntityConfig',
'ConfigLoader'
]

View File

@ -1,208 +0,0 @@
"""Configuration loading and management."""
import json
import yaml
from pathlib import Path
from typing import Dict, Any, Union
from dataclasses import asdict
from .simulation_config import HeadlessConfig, InteractiveConfig, ExperimentConfig
class ConfigLoader:
"""Loads configuration from files and creates config objects."""
@staticmethod
def load_headless_config(config_path: Union[str, Path]) -> HeadlessConfig:
"""Load headless configuration from file."""
config_path = Path(config_path)
if not config_path.exists():
raise FileNotFoundError(f"Config file not found: {config_path}")
with open(config_path, 'r') as f:
if config_path.suffix.lower() == '.json':
data = json.load(f)
elif config_path.suffix.lower() in ['.yml', '.yaml']:
data = yaml.safe_load(f)
else:
raise ValueError(f"Unsupported config format: {config_path.suffix}")
return ConfigLoader._dict_to_headless_config(data)
@staticmethod
def load_interactive_config(config_path: Union[str, Path]) -> InteractiveConfig:
"""Load interactive configuration from file."""
config_path = Path(config_path)
if not config_path.exists():
# Return default config if file doesn't exist
return InteractiveConfig()
with open(config_path, 'r') as f:
if config_path.suffix.lower() == '.json':
data = json.load(f)
elif config_path.suffix.lower() in ['.yml', '.yaml']:
data = yaml.safe_load(f)
else:
raise ValueError(f"Unsupported config format: {config_path.suffix}")
return ConfigLoader._dict_to_interactive_config(data)
@staticmethod
def load_experiment_config(config_path: Union[str, Path]) -> ExperimentConfig:
"""Load experiment configuration from file."""
config_path = Path(config_path)
if not config_path.exists():
raise FileNotFoundError(f"Config file not found: {config_path}")
with open(config_path, 'r') as f:
if config_path.suffix.lower() == '.json':
data = json.load(f)
elif config_path.suffix.lower() in ['.yml', '.yaml']:
data = yaml.safe_load(f)
else:
raise ValueError(f"Unsupported config format: {config_path.suffix}")
return ConfigLoader._dict_to_experiment_config(data)
@staticmethod
def save_config(config: Union[HeadlessConfig, InteractiveConfig, ExperimentConfig],
config_path: Union[str, Path]):
"""Save configuration to file."""
config_path = Path(config_path)
config_path.parent.mkdir(parents=True, exist_ok=True)
# Convert config to dictionary
if isinstance(config, HeadlessConfig):
data = asdict(config)
elif isinstance(config, InteractiveConfig):
data = asdict(config)
elif isinstance(config, ExperimentConfig):
data = asdict(config)
else:
raise ValueError(f"Unknown config type: {type(config)}")
with open(config_path, 'w') as f:
if config_path.suffix.lower() == '.json':
json.dump(data, f, indent=2)
elif config_path.suffix.lower() in ['.yml', '.yaml']:
yaml.dump(data, f, default_flow_style=False)
else:
raise ValueError(f"Unsupported config format: {config_path.suffix}")
@staticmethod
def _dict_to_headless_config(data: Dict[str, Any]) -> HeadlessConfig:
"""Convert dictionary to HeadlessConfig."""
# Extract output config if present
output_data = data.get('output', {})
output_config = OutputConfig(**output_data)
# Extract simulation config if present
sim_data = data.get('simulation', {})
# 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'),
max_duration=data.get('max_duration'),
output=output_config,
simulation=simulation_config
)
@staticmethod
def _dict_to_interactive_config(data: Dict[str, Any]) -> InteractiveConfig:
"""Convert dictionary to InteractiveConfig."""
# Extract simulation config if present
sim_data = data.get('simulation', {})
# 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),
window_height=data.get('window_height', 0),
vsync=data.get('vsync', True),
resizable=data.get('resizable', True),
show_grid=data.get('show_grid', True),
show_interaction_radius=data.get('show_interaction_radius', False),
show_legend=data.get('show_legend', True),
control_bar_height=data.get('control_bar_height', 48),
inspector_width=data.get('inspector_width', 260),
properties_width=data.get('properties_width', 320),
console_height=data.get('console_height', 120),
simulation=simulation_config
)
@staticmethod
def _dict_to_experiment_config(data: Dict[str, Any]) -> ExperimentConfig:
"""Convert dictionary to ExperimentConfig."""
# Extract base config if present
base_data = data.get('base_config', {})
base_config = ConfigLoader._dict_to_headless_config(base_data)
return ExperimentConfig(
name=data.get('name', 'experiment'),
description=data.get('description', ''),
runs=data.get('runs', 1),
run_duration=data.get('run_duration'),
run_ticks=data.get('run_ticks'),
variables=data.get('variables', {}),
base_config=base_config,
aggregate_results=data.get('aggregate_results', True),
aggregate_format=data.get('aggregate_format', 'csv')
)
@staticmethod
def get_default_headless_config() -> HeadlessConfig:
"""Get default headless configuration."""
return HeadlessConfig()
@staticmethod
def get_default_interactive_config() -> InteractiveConfig:
"""Get default interactive configuration."""
return InteractiveConfig()
@staticmethod
def create_sample_configs(output_dir: str = "configs"):
"""Create sample configuration files."""
output_path = Path(output_dir)
output_path.mkdir(exist_ok=True)
# Sample headless config
headless_config = ConfigLoader.get_default_headless_config()
ConfigLoader.save_config(headless_config, output_path / "headless_default.json")
# Sample interactive config
interactive_config = ConfigLoader.get_default_interactive_config()
ConfigLoader.save_config(interactive_config, output_path / "interactive_default.json")
# Sample experiment config
experiment_config = ExperimentConfig(
name="tps_sweep",
description="Test different TPS values",
runs=5,
run_duration=60.0, # 1 minute per run
variables={
"simulation.default_tps": [10, 20, 40, 80, 160]
}
)
ConfigLoader.save_config(experiment_config, output_path / "experiment_tps_sweep.json")
print(f"Sample configs created in {output_path}")
# Import Config classes for the loader
from .simulation_config import OutputConfig, SimulationConfig, EntityConfig

View File

@ -13,15 +13,14 @@ WHITE = (255, 255, 255)
RED = (255, 0, 0) RED = (255, 0, 0)
BLUE = (0, 0, 255) BLUE = (0, 0, 255)
GREEN = (0, 255, 0) GREEN = (0, 255, 0)
ORANGE = (255, 165, 0)
LIGHT_BLUE = (52, 134, 235) LIGHT_BLUE = (52, 134, 235)
SELECTION_BLUE = (0, 128, 255) SELECTION_BLUE = (0, 128, 255)
SELECTION_GRAY = (128, 128, 128, 80) SELECTION_GRAY = (128, 128, 128, 80)
SELECTION_BORDER = (80, 80, 90) SELECTION_BORDER = (80, 80, 90)
# Grid settings # Grid settings
GRID_WIDTH = 50 GRID_WIDTH = 30
GRID_HEIGHT = 50 GRID_HEIGHT = 25
CELL_SIZE = 20 CELL_SIZE = 20
RENDER_BUFFER = 50 RENDER_BUFFER = 50
@ -43,22 +42,6 @@ HUD_MARGIN = 10
LINE_HEIGHT = 20 LINE_HEIGHT = 20
SELECTION_THRESHOLD = 3 # pixels SELECTION_THRESHOLD = 3 # pixels
# Unified Panel Styling System (based on tree widget design)
PANEL_BACKGROUND_COLOR = (30, 30, 40) # Dark blue-gray background
PANEL_SELECTED_COLOR = (50, 100, 150) # Blue highlight for selected elements
PANEL_HOVER_COLOR = (60, 60, 80) # Dark blue highlight for interactive elements
PANEL_TEXT_COLOR = (200, 200, 200) # Light gray text
PANEL_ICON_COLOR = (150, 150, 150) # Medium gray icons
PANEL_BORDER_COLOR = (220, 220, 220) # Light gray borders/dividers
# Panel spacing and dimensions
PANEL_DIVIDER_WIDTH = 0 # No divider lines between panels
PANEL_BORDER_WIDTH = 2 # Border width for emphasis elements
PANEL_INTERNAL_PADDING = 8 # Standard padding inside panels
PANEL_TIGHT_SPACING = 4 # Tight spacing between components
PANEL_NODE_HEIGHT = 20 # Height for list/grid items
PANEL_INDENTATION = 20 # Indentation per hierarchy level
# Simulation settings # Simulation settings
FOOD_SPAWNING = True FOOD_SPAWNING = True
FOOD_OBJECTS_COUNT = 500 FOOD_OBJECTS_COUNT = 500

View File

@ -1,121 +0,0 @@
"""Simulation configuration classes for different modes."""
from dataclasses import dataclass, field
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 = 50
grid_height: int = 50
cell_size: int = 20
initial_cells: int = 50
initial_food: int = 500
food_spawning: bool = True
random_seed: int = 0
default_tps: float = 40.0
entities: EntityConfig = field(default_factory=EntityConfig)
@dataclass
class OutputConfig:
"""Configuration for data output."""
enabled: bool = True
directory: str = "simulation_output"
formats: List[str] = field(default_factory=lambda: ['json'])
collect_metrics: bool = False
collect_entities: bool = False
collect_evolution: bool = False
metrics_interval: int = 100
entities_interval: int = 1000
evolution_interval: int = 1000
real_time: bool = False
@dataclass
class HeadlessConfig:
"""Configuration for headless simulation mode."""
# Simulation settings
max_ticks: Optional[int] = None
max_duration: Optional[float] = None # seconds
early_stop: bool = False # Stop when 0 cells remaining
# Output settings
output: OutputConfig = field(default_factory=OutputConfig)
# Simulation core config
simulation: SimulationConfig = field(default_factory=SimulationConfig)
@dataclass
class InteractiveConfig:
"""Configuration for interactive simulation mode with UI."""
# Window settings
window_width: int = 0 # 0 = auto-detect
window_height: int = 0 # 0 = auto-detect
vsync: bool = True
resizable: bool = True
# UI settings
show_grid: bool = True
show_interaction_radius: bool = False
show_legend: bool = True
control_bar_height: int = 48
inspector_width: int = 260
properties_width: int = 320
console_height: int = 120
# Simulation core config
simulation: SimulationConfig = field(default_factory=lambda: SimulationConfig(initial_cells=350))
@dataclass
class ExperimentConfig:
"""Configuration for automated experiments."""
name: str = "experiment"
description: str = ""
# Multiple runs
runs: int = 1
run_duration: Optional[float] = None # seconds per run
run_ticks: Optional[int] = None # ticks per run
# Variables to test (for parameter sweeps)
variables: dict = field(default_factory=dict)
# Base configuration
base_config: HeadlessConfig = field(default_factory=HeadlessConfig)
# Output aggregation
aggregate_results: bool = True
aggregate_format: str = "csv"

View File

@ -1,68 +0,0 @@
{
"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"
}

View File

@ -1,50 +0,0 @@
{
"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": 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.4,
"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
}
}
}
}
}

View File

@ -1,45 +0,0 @@
{
"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
}
}
}
}
}

View File

@ -1,21 +0,0 @@
"""Core simulation infrastructure and engine components."""
from .simulation_core import SimulationCore, SimulationState
from .timing import TimingController, TimingState
from .event_bus import EventBus, EventType, Event
from .simulation_engine import SimulationEngine
from .input_handler import InputHandler
from .renderer import Renderer
__all__ = [
'SimulationCore',
'SimulationState',
'TimingController',
'TimingState',
'EventBus',
'EventType',
'Event',
'SimulationEngine',
'InputHandler',
'Renderer'
]

View File

@ -1,77 +0,0 @@
"""Simple event bus for decoupled component communication."""
from typing import Dict, List, Callable, Any
from dataclasses import dataclass
from enum import Enum
class EventType(Enum):
"""Types of simulation events."""
SIMULATION_STATE_CHANGED = "simulation_state_changed"
WORLD_TICK_COMPLETED = "world_tick_completed"
ENTITY_ADDED = "entity_added"
ENTITY_REMOVED = "entity_removed"
SELECTION_CHANGED = "selection_changed"
TIMING_UPDATE = "timing_update"
@dataclass
class Event:
"""Event data structure."""
type: EventType
data: Dict[str, Any]
timestamp: float = None
def __post_init__(self):
import time
if self.timestamp is None:
self.timestamp = time.time()
class EventBus:
"""Simple event bus for decoupled communication."""
def __init__(self):
self._subscribers: Dict[EventType, List[Callable]] = {}
self._event_history: List[Event] = []
self._max_history = 1000
def subscribe(self, event_type: EventType, callback: Callable[[Event], None]):
"""Subscribe to an event type."""
if event_type not in self._subscribers:
self._subscribers[event_type] = []
self._subscribers[event_type].append(callback)
def unsubscribe(self, event_type: EventType, callback: Callable[[Event], None]):
"""Unsubscribe from an event type."""
if event_type in self._subscribers:
try:
self._subscribers[event_type].remove(callback)
except ValueError:
pass
def publish(self, event: Event):
"""Publish an event to all subscribers."""
# Store in history
self._event_history.append(event)
if len(self._event_history) > self._max_history:
self._event_history.pop(0)
# Notify subscribers
if event.type in self._subscribers:
for callback in self._subscribers[event.type]:
try:
callback(event)
except Exception as e:
print(f"Error in event callback: {e}")
def get_recent_events(self, event_type: EventType = None, count: int = 10) -> List[Event]:
"""Get recent events, optionally filtered by type."""
events = self._event_history
if event_type:
events = [e for e in events if e.type == event_type]
return events[-count:]
def clear_history(self):
"""Clear event history."""
self._event_history.clear()

View File

@ -1,53 +1,39 @@
# core/input_handler.py # core/input_handler.py
"""Handles input events and camera controls - no state management.""" """Handles all input events and camera controls."""
import pygame import pygame
from config.constants import * from config.constants import *
class InputHandler: class InputHandler:
"""Pure input handler - processes input without managing simulation state."""
def __init__(self, camera, world, sim_view_rect): def __init__(self, camera, world, sim_view_rect):
self.camera = camera self.camera = camera
self.world = world self.world = world
# Selection state (input-specific, not simulation state) # Selection state
self.selecting = False self.selecting = False
self.select_start = None self.select_start = None
self.select_end = None self.select_end = None
self.selected_objects = [] self.selected_objects = []
# UI display flags (input-controlled visual settings) # UI state flags
self.show_grid = True self.show_grid = True
self.show_interaction_radius = False self.show_interaction_radius = False
self.show_legend = False self.show_legend = False
self.is_paused = False
# Simulation state references (synchronized from external source) # Speed control
self.tps = DEFAULT_TPS self.tps = DEFAULT_TPS
self.default_tps = DEFAULT_TPS self.default_tps = DEFAULT_TPS
self.is_paused = False
self.sprint_mode = False self.sprint_mode = False
self.is_stepping = False
self.speed_multiplier = 1.0
# sim-view rect for mouse position calculations # sim-view rect for mouse position calculations
self.sim_view_rect = sim_view_rect self.sim_view_rect = sim_view_rect
# HUD reference for viewport/inspector region checking
self.hud = None
# Action callbacks for simulation control
self.action_callbacks = {}
def update_sim_view_rect(self, sim_view_rect): def update_sim_view_rect(self, sim_view_rect):
"""Update the sim_view rectangle.""" """Update the sim_view rectangle."""
self.sim_view_rect = sim_view_rect self.sim_view_rect = sim_view_rect
def set_hud(self, hud):
"""Set HUD reference for viewport/inspector region checking."""
self.hud = hud
def handle_events(self, events, ui_manager): def handle_events(self, events, ui_manager):
"""Process all pygame events and return game state.""" """Process all pygame events and return game state."""
running = True running = True
@ -62,7 +48,7 @@ class InputHandler:
elif event.type == pygame.KEYUP: elif event.type == pygame.KEYUP:
self._handle_keyup(event) self._handle_keyup(event)
elif event.type == pygame.MOUSEWHEEL: elif event.type == pygame.MOUSEWHEEL:
self._handle_mouse_wheel(event) self.camera.handle_zoom(event.y)
elif event.type == pygame.MOUSEBUTTONDOWN: elif event.type == pygame.MOUSEBUTTONDOWN:
self._handle_mouse_down(event) self._handle_mouse_down(event)
elif event.type == pygame.MOUSEBUTTONUP: elif event.type == pygame.MOUSEBUTTONUP:
@ -93,66 +79,28 @@ class InputHandler:
elif event.key == pygame.K_l: elif event.key == pygame.K_l:
self.show_legend = not self.show_legend self.show_legend = not self.show_legend
elif event.key == pygame.K_SPACE: elif event.key == pygame.K_SPACE:
self.toggle_pause() self.is_paused = not self.is_paused
elif event.key == pygame.K_LSHIFT: elif event.key == pygame.K_LSHIFT:
# Left Shift for temporary speed boost (turbo mode) self.tps = self.default_tps * TURBO_MULTIPLIER
self.set_speed_multiplier(2.0)
elif event.key == pygame.K_r: elif event.key == pygame.K_r:
self.camera.reset_position() self.camera.reset_position()
elif event.key == pygame.K_RSHIFT: elif event.key == pygame.K_RSHIFT:
self.toggle_sprint_mode() # Right Shift toggles sprint mode self.sprint_mode = not self.sprint_mode # Enter sprint mode
elif event.key == pygame.K_s:
self.step_forward() # Step forward
return running return running
def _handle_keyup(self, event): def _handle_keyup(self, event):
"""Handle keyup events.""" """Handle keyup events."""
if event.key == pygame.K_LSHIFT: if event.key == pygame.K_LSHIFT:
# Reset speed multiplier when Left Shift is released self.tps = self.default_tps
self.set_speed_multiplier(1.0)
# if event.key == pygame.K_RSHIFT: # if event.key == pygame.K_RSHIFT:
# self.sprint_mode = False # Exit sprint mode # self.sprint_mode = False # Exit sprint mode
def _handle_mouse_wheel(self, event):
"""Handle mouse wheel events."""
mouse_x, mouse_y = pygame.mouse.get_pos()
# Check if mouse is in viewport and HUD is available
if self.hud:
viewport_rect = self.hud.get_viewport_rect()
inspector_rect = self.hud.inspector_panel.rect if self.hud.inspector_panel else None
if viewport_rect.collidepoint(mouse_x, mouse_y):
# Zoom in viewport
self.camera.handle_zoom(event.y)
elif inspector_rect and inspector_rect.collidepoint(mouse_x, mouse_y) and self.hud.tree_widget:
# Scroll tree widget in inspector
if not viewport_rect.collidepoint(mouse_x, mouse_y):
# Convert to local coordinates if needed
if not hasattr(event, 'pos'):
event.pos = (mouse_x - inspector_rect.x, mouse_y - inspector_rect.y)
else:
local_x = mouse_x - inspector_rect.x
local_y = mouse_y - inspector_rect.y
event.pos = (local_x, local_y)
self.hud.tree_widget.handle_event(event)
else:
# Fallback: always zoom if no HUD reference
self.camera.handle_zoom(event.y)
def _handle_mouse_down(self, event): def _handle_mouse_down(self, event):
"""Handle mouse button down events.""" """Handle mouse button down events."""
mouse_x, mouse_y = event.pos
in_viewport = self.hud and self.hud.get_viewport_rect().collidepoint(mouse_x, mouse_y)
if event.button == 2: # Middle mouse button if event.button == 2: # Middle mouse button
# Only start panning if mouse is in viewport
if in_viewport:
self.camera.start_panning(event.pos) self.camera.start_panning(event.pos)
elif event.button == 1: # Left mouse button elif event.button == 1: # Left mouse button
# Only start selection if mouse is in viewport
if in_viewport:
self.selecting = True self.selecting = True
self.select_start = event.pos self.select_start = event.pos
self.select_end = event.pos self.select_end = event.pos
@ -166,9 +114,7 @@ class InputHandler:
def _handle_mouse_motion(self, event): def _handle_mouse_motion(self, event):
"""Handle mouse motion events.""" """Handle mouse motion events."""
# Only pan if camera was started in viewport (camera will handle this internally)
self.camera.pan(event.pos) self.camera.pan(event.pos)
# Only update selection if we're actively selecting in viewport
if self.selecting: if self.selecting:
self.select_end = event.pos self.select_end = event.pos
@ -234,48 +180,5 @@ class InputHandler:
top = min(self.select_start[1], self.select_end[1]) top = min(self.select_start[1], self.select_end[1])
width = abs(self.select_end[0] - self.select_start[0]) width = abs(self.select_end[0] - self.select_start[0])
height = abs(self.select_end[1] - self.select_start[1]) height = abs(self.select_end[1] - self.select_start[1])
return left, top, width, height return (left, top, width, height)
return None return None
def set_action_callback(self, action_name: str, callback):
"""Set callback for simulation control actions."""
self.action_callbacks[action_name] = callback
def toggle_pause(self):
"""Toggle pause state via callback."""
if 'toggle_pause' in self.action_callbacks:
self.action_callbacks['toggle_pause']()
def step_forward(self):
"""Execute single simulation step via callback."""
self.is_stepping = True
if 'step_forward' in self.action_callbacks:
self.action_callbacks['step_forward']()
def set_speed_multiplier(self, multiplier):
"""Set speed multiplier for simulation via callback."""
if 'set_speed' in self.action_callbacks:
self.action_callbacks['set_speed'](multiplier)
def set_custom_tps(self, tps):
"""Set custom TPS value via callback."""
if 'set_custom_tps' in self.action_callbacks:
self.action_callbacks['set_custom_tps'](tps)
def toggle_sprint_mode(self):
"""Toggle sprint mode via callback."""
if 'toggle_sprint' in self.action_callbacks:
self.action_callbacks['toggle_sprint']()
def get_current_speed_display(self):
"""Get current speed display string."""
if self.sprint_mode:
return "Sprint"
elif self.is_paused:
return "Paused"
elif self.speed_multiplier == 1.0:
return "1x"
elif self.speed_multiplier in [0.5, 2.0, 4.0, 8.0]:
return f"{self.speed_multiplier}x"
else:
return f"{self.speed_multiplier:.1f}x"

View File

@ -4,7 +4,7 @@
import pygame import pygame
import math import math
from config.constants import * from config.constants import *
from world.objects import DefaultCell, FoodObject from world.base.brain import CellBrain
class Renderer: class Renderer:
@ -13,8 +13,11 @@ class Renderer:
self.render_height = render_area.get_height() self.render_height = render_area.get_height()
self.render_width = render_area.get_width() self.render_width = render_area.get_width()
def clear_screen(self): def clear_screen(self, main_screen=None):
"""Clear the screen with a black background.""" """Clear the screen with a black background."""
if main_screen:
main_screen.fill(BLACK)
self.render_area.fill(BLACK) self.render_area.fill(BLACK)
def draw_grid(self, camera, showing_grid=True): def draw_grid(self, camera, showing_grid=True):
@ -98,9 +101,6 @@ class Renderer:
return return
for obj in world.get_objects(): for obj in world.get_objects():
if not isinstance(obj, DefaultCell):
continue
obj_x, obj_y = obj.position.get_position() obj_x, obj_y = obj.position.get_position()
radius = obj.interaction_radius radius = obj.interaction_radius
@ -250,68 +250,3 @@ class Renderer:
size = camera.get_relative_size(width) size = camera.get_relative_size(width)
rect = pygame.Rect(screen_x - size // 2, screen_y - size // 2, size, size) rect = pygame.Rect(screen_x - size // 2, screen_y - size // 2, size, size)
pygame.draw.rect(self.render_area, SELECTION_BLUE, rect, 1) 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
)

View File

@ -1,311 +0,0 @@
"""Pure simulation core with no UI dependencies."""
import time
import random
from typing import List, Optional, Dict, Any
from dataclasses import dataclass
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 RENDER_BUFFER
from config.simulation_config import SimulationConfig
@dataclass
class SimulationState:
"""Current simulation state."""
total_ticks: int = 0
actual_tps: float = 0.0
is_running: bool = False
world_buffer: int = 0
class SimulationCore:
"""Pure simulation logic with no UI dependencies."""
def __init__(self, config: SimulationConfig, event_bus: Optional[EventBus] = None):
self.config = config
self.event_bus = event_bus or EventBus()
# Initialize timing controller
self.timing = TimingController(config.default_tps, self.event_bus)
# Initialize world
self.world = self._setup_world()
# Simulation state
self.state = SimulationState()
self.is_running = False
# Camera (for spatial calculations, not rendering)
self.camera = Camera(
config.grid_width * config.cell_size,
config.grid_height * config.cell_size,
RENDER_BUFFER
)
def _setup_world(self) -> World:
"""Setup the simulation world."""
# Import locally to avoid circular imports
from world.objects import DefaultCell, FoodObject
world = World(
self.config.cell_size,
(
self.config.cell_size * self.config.grid_width,
self.config.cell_size * self.config.grid_height
)
)
# Set random seed for reproducibility
random.seed(self.config.random_seed)
# Calculate world bounds
half_width = self.config.grid_width * self.config.cell_size // 2
half_height = self.config.grid_height * self.config.cell_size // 2
# Add initial food
if self.config.food_spawning:
for _ in range(self.config.initial_food):
x = random.randint(-half_width // 2, half_width // 2)
y = random.randint(-half_height // 2, half_height // 2)
food = FoodObject(Position(x=x, y=y))
world.add_object(food)
self._notify_entity_added(food)
# Add initial cells
for _ in range(self.config.initial_cells):
cell = DefaultCell(
Position(
x=random.randint(-half_width // 2, half_width // 2),
y=random.randint(-half_height // 2, half_height // 2)
),
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)
world.add_object(cell)
self._notify_entity_added(cell)
return world
def start(self):
"""Start the simulation."""
self.is_running = True
self.state.is_running = True
self.timing.reset_counters()
def stop(self):
"""Stop the simulation."""
self.is_running = False
self.state.is_running = False
def pause(self):
"""Pause the simulation."""
self.timing.set_pause(True)
def resume(self):
"""Resume the simulation."""
self.timing.set_pause(False)
def step(self):
"""Execute a single simulation step."""
was_paused = self.timing.state.is_paused
self.timing.set_pause(False)
self._tick()
self.timing.set_pause(was_paused)
def update(self, delta_time: float):
"""Update simulation timing and tick if needed."""
if not self.is_running:
return
# Handle sprint mode
if self.timing.state.sprint_mode:
self._handle_sprint_mode()
return
# Handle single step
if hasattr(self, '_stepping') and self._stepping:
self._tick()
self._stepping = False
return
# Normal timing-based ticks
if self.timing.should_tick():
self._tick()
# Update timing
current_time = time.perf_counter()
if current_time - self.timing.last_tps_time >= 1.0:
self.state.actual_tps = self.timing.actual_tps
self.state.total_ticks = self.timing.total_ticks
def _tick(self):
"""Execute a single simulation tick."""
# Update world
self.world.tick_all()
# Update timing
self.timing.update_timing()
# Update state
self.state.total_ticks = self.timing.total_ticks
self.state.actual_tps = self.timing.actual_tps
self.state.world_buffer = self.world.current_buffer
# Notify subscribers
self._notify_world_tick_completed()
def _handle_sprint_mode(self):
"""Handle sprint mode execution."""
if not self.timing.state.sprint_mode:
return
start_time = time.perf_counter()
# Execute multiple ticks in sprint mode
while time.perf_counter() - start_time < 0.05: # 50ms of sprint
self._tick()
def set_tps(self, tps: float):
"""Set target TPS."""
self.timing.set_tps(tps)
def set_speed_multiplier(self, multiplier: float):
"""Set speed multiplier."""
self.timing.set_speed_multiplier(multiplier)
def toggle_pause(self):
"""Toggle pause state."""
self.timing.toggle_pause()
def toggle_sprint_mode(self):
"""Toggle sprint mode."""
self.timing.toggle_sprint_mode()
def get_entities_in_radius(self, position: Position, radius: float) -> List:
"""Get entities within radius of position."""
return self.world.query_objects_within_radius(position.x, position.y, radius)
def get_entities_in_range(self, min_pos: Position, max_pos: Position) -> List:
"""Get entities within rectangular range."""
return self.world.query_objects_in_range(min_pos.x, min_pos.y, max_pos.x, max_pos.y)
def get_closest_entity(self, position: Position, max_distance: float = None) -> Optional:
"""Get closest entity to position."""
if max_distance is not None:
# Filter by distance after getting closest
closest = self.world.query_closest_object(position.x, position.y)
if closest:
dx = closest.position.x - position.x
dy = closest.position.y - position.y
distance = (dx * dx + dy * dy) ** 0.5
if distance <= max_distance:
return closest
return None
else:
return self.world.query_closest_object(position.x, position.y)
def count_entities_by_type(self, entity_type) -> int:
"""Count entities of specific type."""
count = 0
for entity in self.world.get_objects():
if isinstance(entity, entity_type):
count += 1
return count
def get_world_state(self) -> Dict[str, Any]:
"""Get current world state for data collection."""
# Import locally to avoid circular imports
from world.objects import DefaultCell, FoodObject
return {
'tick_count': self.state.total_ticks,
'actual_tps': self.state.actual_tps,
'world_buffer': self.state.world_buffer,
'is_paused': self.timing.state.is_paused,
'sprint_mode': self.timing.state.sprint_mode,
'target_tps': self.timing.state.tps,
'speed_multiplier': self.timing.state.speed_multiplier,
'entity_counts': {
'total': len(list(self.world.get_objects())),
'cells': self.count_entities_by_type(DefaultCell),
'food': self.count_entities_by_type(FoodObject)
}
}
def get_entity_states(self) -> List[Dict[str, Any]]:
"""Get states of all entities for data collection."""
# Import locally to avoid circular imports
from world.objects import DefaultCell, FoodObject
entities = []
for entity in self.world.get_objects():
if isinstance(entity, DefaultCell):
# Get neural network info safely
nn = entity.behavioral_model.neural_network if hasattr(entity.behavioral_model, 'neural_network') else None
layer_sizes = [len(nn.layers)] if nn and hasattr(nn, 'layers') else [4, 6, 2] # Default estimate
entities.append({
'id': id(entity),
'type': 'cell',
'position': {'x': entity.position.x, 'y': entity.position.y},
'rotation': entity.rotation.angle,
'energy': entity.energy,
'age': entity.tick_count, # tick_count represents age
'generation': getattr(entity, 'generation', 0), # Default to 0 if not present
'neural_network': {
'layer_sizes': layer_sizes,
'has_neural_network': nn is not None
}
})
elif isinstance(entity, FoodObject):
entities.append({
'id': id(entity),
'type': 'food',
'position': {'x': entity.position.x, 'y': entity.position.y},
'decay': entity.decay,
'max_decay': entity.max_decay
})
return entities
def _notify_world_tick_completed(self):
"""Notify subscribers that world tick completed."""
if self.event_bus:
event = Event(
type=EventType.WORLD_TICK_COMPLETED,
data={
'tick_count': self.state.total_ticks,
'world_state': self.get_world_state()
}
)
self.event_bus.publish(event)
def _notify_entity_added(self, entity):
"""Notify subscribers of entity addition."""
if self.event_bus:
event = Event(
type=EventType.ENTITY_ADDED,
data={
'entity_id': id(entity),
'entity_type': type(entity).__name__,
'position': {'x': entity.position.x, 'y': entity.position.y}
}
)
self.event_bus.publish(event)
def _notify_entity_removed(self, entity):
"""Notify subscribers of entity removal."""
if self.event_bus:
event = Event(
type=EventType.ENTITY_REMOVED,
data={
'entity_id': id(entity),
'entity_type': type(entity).__name__
}
)
self.event_bus.publish(event)

View File

@ -1,390 +1,193 @@
import pygame import pygame
import time import time
import random
import sys import sys
from pygame_gui import UIManager from pygame_gui import UIManager
from config.constants import BLACK, DEFAULT_TPS, MAX_FPS from world.base.brain import CellBrain, FlexibleNeuralNetwork
from world.world import World, Position, Rotation
from world.objects import FoodObject, DefaultCell
from world.simulation_interface import Camera
from config.constants import *
from core.input_handler import InputHandler from core.input_handler import InputHandler
from core.renderer import Renderer from core.renderer import Renderer
from core.simulation_core import SimulationCore
from core.event_bus import EventBus
from ui.hud import HUD from ui.hud import HUD
import cProfile
class SimulationEngine: class SimulationEngine:
"""Interactive simulation engine with UI (wrapper around SimulationCore).""" def __init__(self):
def __init__(self, config=None):
pygame.init() pygame.init()
self.config = config
self.event_bus = EventBus()
self._init_window()
self._init_simulation()
self._init_ui()
self.running = True
# HUD action handlers registry for extensibility
self._hud_action_handlers = {
'toggle_pause': self.simulation_core.toggle_pause,
'step_forward': self.simulation_core.step,
'toggle_sprint': self.simulation_core.toggle_sprint_mode,
'viewport_resized': self._update_simulation_view,
'set_speed': self.simulation_core.set_speed_multiplier,
'set_custom_tps': self.simulation_core.set_tps,
}
def _profile_single_tick(self):
"""Profile a single tick for performance analysis."""
profiler = cProfile.Profile()
profiler.enable()
self.simulation_core.world.tick_all()
profiler.disable()
profiler.dump_stats('profile_tick.prof') # Save to file
def _init_window(self):
info = pygame.display.Info() info = pygame.display.Info()
self.window_width, self.window_height = info.current_w // 2, info.current_h // 2
self.screen = pygame.display.set_mode((self.window_width, self.window_height),
pygame.RESIZABLE, vsync=1)
# Use config or defaults self.ui_manager = UIManager((self.window_width, self.window_height))
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.camera = Camera(SCREEN_WIDTH, SCREEN_HEIGHT, RENDER_BUFFER)
self.screen = pygame.display.set_mode( self._update_simulation_view()
(self.window_width, self.window_height),
screen_flags, vsync=vsync # self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT), vsync=1)
)
pygame.display.set_caption("Dynamic Abstraction System Testing") pygame.display.set_caption("Dynamic Abstraction System Testing")
self.clock = pygame.time.Clock() self.clock = pygame.time.Clock()
def _init_ui(self): self.last_tick_time = time.perf_counter()
self.ui_manager = UIManager((self.window_width, self.window_height)) self.last_tps_time = time.perf_counter()
self.tick_counter = 0
self.actual_tps = 0
self.total_ticks = 0
self.world = self._setup_world()
self.input_handler = InputHandler(self.camera, self.world, self.sim_view_rect)
self.renderer = Renderer(self.sim_view)
self.hud = HUD(self.ui_manager, self.window_width, self.window_height) self.hud = HUD(self.ui_manager, self.window_width, self.window_height)
self.hud.update_layout(self.window_width, self.window_height) self.hud.update_layout(self.window_width, self.window_height)
# Initialize tree widget with the world self.running = True
self.hud.initialize_tree_widget(self.simulation_core.world)
# 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)
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 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)
# Setup input handler with simulation core world
self.input_handler = InputHandler(
self.simulation_core.camera,
self.simulation_core.world,
self.sim_view_rect
)
self.input_handler.tps = self.simulation_core.timing.state.tps
self.input_handler.default_tps = DEFAULT_TPS
# Set up action callbacks for input handler
self._setup_input_handler_callbacks()
# Setup renderer
self.renderer = Renderer(self.sim_view)
# Profile a single tick for performance analysis
self._profile_single_tick()
def _update_simulation_view(self): def _update_simulation_view(self):
viewport_rect = self.hud.get_viewport_rect() self.sim_view_width = int(self.window_width * 0.75)
self.sim_view_width = viewport_rect.width self.sim_view_height = int(self.window_height * 0.75)
self.sim_view_height = viewport_rect.height
self.sim_view = pygame.Surface((self.sim_view_width, self.sim_view_height)) self.sim_view = pygame.Surface((self.sim_view_width, self.sim_view_height))
self.sim_view_rect = self.sim_view.get_rect(topleft=(viewport_rect.left, viewport_rect.top)) self.sim_view_rect = self.sim_view.get_rect(center=(self.window_width // 2, self.window_height // 2))
self.ui_manager.set_window_resolution((self.window_width, self.window_height)) self.ui_manager.set_window_resolution((self.window_width, self.window_height))
self.renderer = Renderer(self.sim_view) self.renderer = Renderer(self.sim_view)
# Update simulation core camera dimensions # Update camera to match new sim_view size
self.simulation_core.camera.screen_width = self.sim_view_width if hasattr(self, 'camera'):
self.simulation_core.camera.screen_height = self.sim_view_height self.camera.screen_width = self.sim_view_width
self.camera.screen_height = self.sim_view_height
# Update input handler simulation view rect if hasattr(self, 'input_handler'):
self.input_handler.update_sim_view_rect(self.sim_view_rect) self.input_handler.update_sim_view_rect(self.sim_view_rect)
def _setup_input_handler_callbacks(self):
"""Set up action callbacks for input handler."""
callbacks = {
'toggle_pause': self.simulation_core.toggle_pause,
'step_forward': self.simulation_core.step,
'set_speed': self.simulation_core.set_speed_multiplier,
'set_custom_tps': self.simulation_core.set_tps,
'toggle_sprint': self.simulation_core.toggle_sprint_mode,
}
for action, callback in callbacks.items(): @staticmethod
self.input_handler.set_action_callback(action, callback) def _setup_world():
world = World(CELL_SIZE, (CELL_SIZE * GRID_WIDTH, CELL_SIZE * GRID_HEIGHT))
random.seed(RANDOM_SEED)
def _count_cells(self): half_width = GRID_WIDTH * CELL_SIZE // 2
"""Count cells in the simulation.""" half_height = GRID_HEIGHT * CELL_SIZE // 2
# Import locally to avoid circular import
from world.objects import DefaultCell
return self.simulation_core.count_entities_by_type(DefaultCell)
if FOOD_SPAWNING:
for _ in range(FOOD_OBJECTS_COUNT):
x = random.randint(-half_width, half_width)
y = random.randint(-half_height, half_height)
world.add_object(FoodObject(Position(x=x, y=y)))
for _ in range(300):
new_cell = DefaultCell(Position(x=random.randint(-half_width, half_width), y=random.randint(-half_height, half_height)), Rotation(angle=0))
new_cell.behavioral_model = new_cell.behavioral_model.mutate(3)
world.add_object(new_cell)
return world
def run(self): def run(self):
"""Run the interactive simulation engine."""
self.simulation_core.start()
while self.running: while self.running:
self._handle_frame() self._handle_frame()
self.simulation_core.stop()
pygame.quit() pygame.quit()
sys.exit() sys.exit()
def _handle_frame(self): def _handle_frame(self):
"""Handle a single frame in the interactive simulation."""
deltatime = self.clock.get_time() / 1000.0 deltatime = self.clock.get_time() / 1000.0
tick_interval = 1.0 / self.input_handler.tps
# Handle events # Handle events
events = pygame.event.get() events = pygame.event.get()
self.running = self.input_handler.handle_events(events, self.hud.manager) self.running = self.input_handler.handle_events(events, self.hud.manager)
# Process HUD events and window events
for event in events: for event in events:
hud_action = self.hud.process_event(event)
self._process_hud_action(hud_action)
if event.type == pygame.VIDEORESIZE: if event.type == pygame.VIDEORESIZE:
self._handle_window_resize(event)
# Sync input handler state with simulation core timing
self._sync_input_and_timing()
# Handle sprint mode
if self.input_handler.sprint_mode:
self._handle_sprint_mode()
return
# Update UI manager every frame
self.hud.manager.update(deltatime)
# Handle step-forward mode
if self.input_handler.is_stepping:
self.simulation_core.step()
self.input_handler.is_stepping = False
else:
# Update simulation using core
self.simulation_core.update(deltatime)
# Update selected objects in input handler
self.input_handler.update_selected_objects()
# Sync tree selection with world selection
self.hud.update_tree_selection(self.input_handler.selected_objects)
# Render frame
self._update_frame(deltatime)
self._render_frame(deltatime)
def _sync_input_and_timing(self):
"""Synchronize input handler state with simulation core timing."""
timing_state = self.simulation_core.timing.state
# Sync TPS
self.input_handler.tps = timing_state.tps
# Sync pause state
self.input_handler.is_paused = timing_state.is_paused
# Sync sprint mode
self.input_handler.sprint_mode = timing_state.sprint_mode
# Sync speed multiplier
self.input_handler.speed_multiplier = timing_state.speed_multiplier
def _update_frame(self, deltatime):
"""Update camera and input state."""
keys = pygame.key.get_pressed()
self.input_handler.update_camera(keys, deltatime)
def _render_frame(self, deltatime):
"""Render the complete frame."""
self.screen.fill(BLACK)
self.renderer.clear_screen()
# Render simulation world
self._render_simulation_world()
# Update and render UI
self._update_and_render_ui(deltatime)
# Render HUD overlays
self._render_hud_overlays()
pygame.display.flip()
self.clock.tick(MAX_FPS)
def _render_simulation_world(self):
"""Render the simulation world if not dragging splitter."""
if not self.hud.dragging_splitter:
self.renderer.draw_grid(self.simulation_core.camera, self.input_handler.show_grid)
self.renderer.render_world(self.simulation_core.world, self.simulation_core.camera)
self.renderer.render_interaction_radius(
self.simulation_core.world,
self.simulation_core.camera,
self.input_handler.selected_objects,
self.input_handler.show_interaction_radius
)
self.renderer.render_selection_rectangle(
self.input_handler.get_selection_rect(),
self.sim_view_rect
)
self.renderer.render_selected_objects_outline(
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):
"""Update UI elements and render them."""
# Update HUD displays with simulation core state
self.hud.update_simulation_controls(self.simulation_core)
# Update tree widget
self.hud.update_tree_widget(deltatime)
# Draw panel backgrounds first (before pygame_gui UI)
self.hud.render_panel_backgrounds(self.screen)
# Draw UI elements
self.hud.manager.draw_ui(self.screen)
# Render tree widget
self.hud.render_tree_widget(self.screen)
def _render_hud_overlays(self):
"""Render HUD overlay elements."""
self.hud.render_fps(self.screen, self.clock)
self.hud.render_tps(self.screen, self.simulation_core.state.actual_tps)
# self.hud.render_selected_objects_info(self.screen, self.input_handler.selected_objects)
self.hud.render_legend(self.screen, self.input_handler.show_legend)
self.hud.render_pause_indicator(self.screen, self.simulation_core.timing.state.is_paused)
# Render neural network visualization for selected object
if self.input_handler.selected_objects:
self.hud.render_neural_network_visualization(
self.screen,
self.input_handler.selected_objects[0]
)
def _handle_window_resize(self, event):
"""Handle window resize event."""
self.window_width, self.window_height = event.w, event.h self.window_width, self.window_height = event.w, event.h
self.screen = pygame.display.set_mode( self.screen = pygame.display.set_mode((self.window_width, self.window_height),
(self.window_width, self.window_height), pygame.RESIZABLE)
pygame.RESIZABLE
)
self._update_simulation_view() self._update_simulation_view()
self.hud.update_layout(self.window_width, self.window_height) self.hud.update_layout(self.window_width, self.window_height)
def _process_hud_action(self, action): if self.input_handler.sprint_mode:
"""Process a single HUD action using the handler registry.""" # Sprint mode: run as many ticks as possible, skip rendering
if not action:
return
# Handle tree selection changes
if isinstance(action, tuple) and action[0] == 'tree_selection_changed':
selected_entities = action[1]
# Sync world selection with tree selection
self.input_handler.selected_objects = selected_entities
return
# Handle simple actions directly
if action in self._hud_action_handlers:
self._hud_action_handlers[action]()
return
# Handle parameterized actions
if isinstance(action, tuple) and len(action) >= 2:
action_type, param = action[0], action[1]
if action_type == 'set_speed':
self.simulation_core.set_speed_multiplier(param)
elif action_type == 'set_custom_tps':
self.simulation_core.set_tps(param)
elif action_type == 'reset_tps_display':
self._reset_tps_display()
def _reset_tps_display(self):
"""Reset TPS display to current simulation value."""
if self.hud.custom_tps_entry:
current_tps = int(self.simulation_core.timing.state.tps)
self.hud.custom_tps_entry.set_text(str(current_tps))
def register_hud_action(self, action_name: str, handler):
"""Register a new HUD action handler for extensibility.
Args:
action_name: Name of the HUD action
handler: Callable that handles the action
"""
self._hud_action_handlers[action_name] = handler
def _handle_sprint_mode(self):
"""Handle sprint mode by running multiple simulation ticks quickly."""
current_time = time.perf_counter() current_time = time.perf_counter()
while time.perf_counter() - current_time < 0.05: # 50ms of sprint while True:
self.simulation_core.update(0.016) # Update simulation
self.input_handler.update_selected_objects() self.input_handler.update_selected_objects()
pygame.event.pump() # Prevent event queue overflow self.world.tick_all()
self.tick_counter += 1
self.total_ticks += 1
# Optionally break after some time to allow event processing
if time.perf_counter() - current_time > 0.05: # ~50ms per batch
break
# Update TPS every second
if time.perf_counter() - self.last_tps_time >= 1.0:
self.actual_tps = self.tick_counter
self.tick_counter = 0
self.last_tps_time = time.perf_counter()
# No rendering or camera update
# Render sprint debug info
self.screen.fill(BLACK)
self.renderer.clear_screen() self.renderer.clear_screen()
cell_count = self._count_cells() self.hud.render_sprint_debug(self.screen, self.actual_tps, self.total_ticks)
self.hud.render_sprint_debug(
self.screen,
self.simulation_core.state.actual_tps,
self.simulation_core.state.total_ticks,
cell_count
)
pygame.display.flip() pygame.display.flip()
self.clock.tick(MAX_FPS) self.clock.tick(MAX_FPS)
return
if not self.input_handler.is_paused:
current_time = time.perf_counter()
while current_time - self.last_tick_time >= tick_interval:
self.last_tick_time += tick_interval
self.tick_counter += 1
self.total_ticks += 1
self.input_handler.update_selected_objects()
self.world.tick_all()
self.hud.manager.update(deltatime)
if current_time - self.last_tps_time >= 1.0:
self.actual_tps = self.tick_counter
self.tick_counter = 0
self.last_tps_time += 1.0
else:
self.last_tick_time = time.perf_counter()
self.last_tps_time = time.perf_counter()
self.hud.manager.draw_ui(self.screen)
self._update(deltatime)
self._render()
def _update(self, deltatime):
keys = pygame.key.get_pressed()
self.input_handler.update_camera(keys, deltatime)
def _render(self):
self.renderer.clear_screen(self.screen)
self.renderer.draw_grid(self.camera, self.input_handler.show_grid)
self.renderer.render_world(self.world, self.camera)
self.renderer.render_interaction_radius(self.world, self.camera, self.input_handler.selected_objects, self.input_handler.show_interaction_radius)
self.renderer.render_selection_rectangle(self.input_handler.get_selection_rect(), self.sim_view_rect)
self.renderer.render_selected_objects_outline(self.input_handler.selected_objects, self.camera)
# In core/simulation_engine.py, in _render():
self.screen.blit(self.sim_view, (self.sim_view_rect.left, self.sim_view_rect.top))
# Draw border around sim_view
border_color = (255, 255, 255) # White
border_width = 3
pygame.draw.rect(self.screen, border_color, self.sim_view_rect, border_width)
self.hud.render_mouse_position(self.screen, self.camera, self.sim_view_rect)
self.hud.render_fps(self.screen, self.clock)
self.hud.render_tps(self.screen, self.actual_tps)
self.hud.render_tick_count(self.screen, self.total_ticks)
self.hud.render_selected_objects_info(self.screen, self.input_handler.selected_objects)
self.hud.render_legend(self.screen, self.input_handler.show_legend)
self.hud.render_pause_indicator(self.screen, self.input_handler.is_paused)
if self.input_handler.selected_objects:
self.hud.render_neural_network_visualization(self.screen, self.input_handler.selected_objects[0])
pygame.display.flip()
self.clock.tick(MAX_FPS)

View File

@ -1,186 +0,0 @@
"""Timing and TPS management for simulation."""
import time
from dataclasses import dataclass
from typing import Optional
from .event_bus import EventBus, EventType, Event
@dataclass
class TimingState:
"""Current timing state."""
tps: float
is_paused: bool = False
sprint_mode: bool = False
speed_multiplier: float = 1.0
def __post_init__(self):
if self.speed_multiplier == 0:
self.speed_multiplier = 1.0
class TimingController:
"""Manages simulation timing, TPS, and pause states."""
def __init__(self, default_tps: float = 40.0, event_bus: Optional[EventBus] = None):
self.default_tps = default_tps
self.state = TimingState(tps=default_tps)
self.event_bus = event_bus
# Timing variables
self.last_tick_time = time.perf_counter()
self.last_tps_time = time.perf_counter()
self.tick_counter = 0
self.total_ticks = 0
self.actual_tps = 0
# Sprint mode specific
self.sprint_start_time = None
def set_tps(self, tps: float):
"""Set target TPS."""
if tps > 0:
self.state.tps = max(1.0, min(1000.0, tps))
self.state.speed_multiplier = self.state.tps / self.default_tps
self._notify_state_change()
def set_speed_multiplier(self, multiplier: float):
"""Set speed multiplier for simulation."""
if multiplier > 0:
self.state.speed_multiplier = max(0.1, min(10.0, multiplier))
self.state.tps = self.default_tps * self.state.speed_multiplier
self._notify_state_change()
def toggle_pause(self):
"""Toggle pause state."""
self.state.is_paused = not self.state.is_paused
if self.state.is_paused:
# Reset timing when paused
self.last_tick_time = time.perf_counter()
self.last_tps_time = time.perf_counter()
self._notify_state_change()
def set_pause(self, paused: bool):
"""Set pause state directly."""
if self.state.is_paused != paused:
self.toggle_pause()
def toggle_sprint_mode(self):
"""Toggle sprint mode."""
self.state.sprint_mode = not self.state.sprint_mode
if self.state.sprint_mode:
self.sprint_start_time = time.perf_counter()
else:
self.sprint_start_time = None
self._notify_state_change()
def set_sprint_mode(self, enabled: bool):
"""Set sprint mode directly."""
if self.state.sprint_mode != enabled:
self.toggle_sprint_mode()
def should_tick(self) -> bool:
"""Check if simulation should tick based on timing."""
if self.state.is_paused:
return False
if self.state.sprint_mode:
return True
tick_interval = 1.0 / self.state.tps
current_time = time.perf_counter()
return current_time - self.last_tick_time >= tick_interval
def update_timing(self):
"""Update timing variables after a tick."""
current_time = time.perf_counter()
# Update tick counters first
self.tick_counter += 1
self.total_ticks += 1
if self.state.sprint_mode and self.sprint_start_time:
# In sprint mode, calculate TPS based on total sprint duration
sprint_duration = current_time - self.sprint_start_time
if sprint_duration > 0:
self.actual_tps = self.tick_counter / sprint_duration
else:
self.actual_tps = 0
else:
# Normal mode TPS calculation using sliding window for more responsive display
elapsed_time = current_time - self.last_tps_time
# Update TPS more frequently for responsive display
if elapsed_time >= 0.5: # Update every 500ms instead of 1 second
# Calculate actual TPS based on elapsed time
self.actual_tps = self.tick_counter / elapsed_time
# Reset for next measurement period
self.last_tps_time = current_time
self.tick_counter = 0
# Update last tick time for next tick calculation - advance by tick interval
if not self.state.sprint_mode:
tick_interval = 1.0 / self.state.tps
self.last_tick_time += tick_interval
self._notify_timing_update()
def get_display_tps(self) -> int:
"""Get TPS rounded to nearest whole number for display."""
return round(self.actual_tps)
def reset_counters(self):
"""Reset timing counters."""
self.tick_counter = 0
self.total_ticks = 0
self.actual_tps = 0
self.last_tick_time = time.perf_counter()
self.last_tps_time = time.perf_counter()
if self.state.sprint_mode:
self.sprint_start_time = time.perf_counter()
def get_tick_interval(self) -> float:
"""Get current tick interval in seconds."""
return 1.0 / self.state.tps if self.state.tps > 0 else 1.0
def get_current_speed_display(self) -> str:
"""Get current speed display string."""
if self.state.sprint_mode:
return "Sprint"
elif self.state.is_paused:
return "Paused"
elif self.state.speed_multiplier == 1.0:
return "1x"
elif self.state.speed_multiplier in [0.5, 2.0, 4.0, 8.0]:
return f"{self.state.speed_multiplier}x"
else:
return f"{self.state.speed_multiplier:.1f}x"
def _notify_state_change(self):
"""Notify subscribers of state change."""
if self.event_bus:
event = Event(
type=EventType.SIMULATION_STATE_CHANGED,
data={
'timing_state': self.state,
'tps': self.state.tps,
'is_paused': self.state.is_paused,
'sprint_mode': self.state.sprint_mode,
'speed_multiplier': self.state.speed_multiplier
}
)
self.event_bus.publish(event)
def _notify_timing_update(self):
"""Notify subscribers of timing update."""
if self.event_bus:
event = Event(
type=EventType.TIMING_UPDATE,
data={
'actual_tps': self.actual_tps,
'total_ticks': self.total_ticks,
'tick_counter': self.tick_counter
}
)
self.event_bus.publish(event)

View File

@ -1,5 +0,0 @@
"""Simulation engines for different modes."""
from .headless_engine import HeadlessSimulationEngine, HeadlessConfig
__all__ = ['HeadlessSimulationEngine', 'HeadlessConfig']

View File

@ -1,424 +0,0 @@
"""Headless simulation engine for running simulations without UI."""
import time
import signal
from typing import Dict, Any, Optional, List
from dataclasses import dataclass
from core.simulation_core import SimulationCore, SimulationConfig
from core.event_bus import EventBus
from output import MetricsCollector, EntityCollector, EvolutionCollector
from output.formatters.json_formatter import JSONFormatter
from output.formatters.csv_formatter import CSVFormatter
from output.writers.file_writer import FileWriter
try:
from tqdm import tqdm
TQDM_AVAILABLE = True
except ImportError:
TQDM_AVAILABLE = False
@dataclass
class HeadlessConfig:
"""Configuration for headless simulation engine."""
simulation: SimulationConfig
max_ticks: Optional[int] = None
max_duration: Optional[float] = None # seconds
output_dir: str = "simulation_output"
enable_metrics: bool = True
enable_entities: bool = True
enable_evolution: bool = True
metrics_interval: int = 100
entities_interval: int = 1000
evolution_interval: int = 1000
output_formats: List[str] = None # ['json', 'csv']
real_time: bool = False # Whether to run in real-time or as fast as possible
early_stop: bool = False # Stop when 0 cells remaining
def __post_init__(self):
if self.output_formats is None:
self.output_formats = ['json']
class HeadlessSimulationEngine:
"""Headless simulation engine with data collection capabilities."""
def __init__(self, config: HeadlessConfig):
self.config = config
self.event_bus = EventBus()
self.simulation_core = SimulationCore(config.simulation, self.event_bus)
self.file_writer = FileWriter(config.output_dir)
self.formatters = self._create_formatters()
self.collectors = self._create_collectors()
# Runtime state
self.running = False
self.start_time = None
self.tick_data = {}
self.batch_data = {
'metrics': [],
'entities': [],
'evolution': []
}
# Progress tracking
self.files_written = 0
self.last_progress_update = 0
self.progress_update_interval = 1.0 # Update progress every second
self.progress_bar = None
# Setup signal handlers for graceful shutdown
signal.signal(signal.SIGINT, self._signal_handler)
signal.signal(signal.SIGTERM, self._signal_handler)
def _create_formatters(self) -> Dict[str, Any]:
"""Create data formatters."""
formatters = {}
if 'json' in self.config.output_formats:
formatters['json'] = JSONFormatter()
if 'csv' in self.config.output_formats:
formatters['csv'] = CSVFormatter()
return formatters
def _create_collectors(self) -> Dict[str, Any]:
"""Create data collectors."""
collectors = {}
if self.config.enable_metrics:
collectors['metrics'] = MetricsCollector(self.config.metrics_interval)
if self.config.enable_entities:
collectors['entities'] = EntityCollector(
self.config.entities_interval,
include_cells=True,
include_food=False
)
if self.config.enable_evolution:
collectors['evolution'] = EvolutionCollector(self.config.evolution_interval)
return collectors
def _init_progress_bar(self):
"""Initialize progress bar for simulation."""
if not TQDM_AVAILABLE:
return
# Determine progress total based on configuration
if self.config.max_ticks:
total = self.config.max_ticks
unit = 'ticks'
elif self.config.max_duration:
total = int(self.config.max_duration)
unit = 'sec'
else:
# No clear total - create indeterminate progress bar
total = None
unit = 'ticks'
if total:
self.progress_bar = tqdm(
total=total,
unit=unit,
desc="Simulation",
leave=True, # Keep the bar when done
bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}] {postfix}'
)
else:
self.progress_bar = tqdm(
unit='ticks',
desc="Simulation",
leave=True,
bar_format='{l_bar}{bar}| {n_fmt} [{elapsed}, {rate_fmt}] {postfix}'
)
def _update_progress_bar(self):
"""Update progress bar with current status."""
current_time = time.time()
if current_time - self.last_progress_update < self.progress_update_interval:
return
current_tick = self.simulation_core.state.total_ticks
tps = self.simulation_core.state.actual_tps
elapsed = current_time - self.start_time
# Get current cell count
world_state = self.simulation_core.get_world_state()
cell_count = world_state.get('entity_counts', {}).get('cells', 0)
if TQDM_AVAILABLE and self.progress_bar:
# Use tqdm progress bar
if self.config.max_ticks:
# Update based on tick progress
progress = min(current_tick, self.config.max_ticks)
self.progress_bar.n = progress
self.progress_bar.set_postfix({
'TPS': f'{tps:.1f}',
'Cells': cell_count,
'Files': self.files_written
})
elif self.config.max_duration:
# Update based on elapsed time
progress = min(elapsed, self.config.max_duration)
self.progress_bar.n = int(progress)
self.progress_bar.set_postfix({
'TPS': f'{tps:.1f}',
'Cells': cell_count,
'Files': self.files_written,
'Tick': current_tick
})
else:
# Indeterminate progress
self.progress_bar.n = current_tick
self.progress_bar.set_postfix({
'TPS': f'{tps:.1f}',
'Cells': cell_count,
'Files': self.files_written
})
self.progress_bar.refresh()
else:
# Simple text-based progress
eta_text = ""
if self.config.max_ticks and current_tick > 0:
tick_rate = current_tick / elapsed if elapsed > 0 else 0
remaining_ticks = self.config.max_ticks - current_tick
eta_seconds = remaining_ticks / tick_rate if tick_rate > 0 else 0
eta_minutes, eta_seconds = divmod(eta_seconds, 60)
eta_text = f"ETA: {int(eta_minutes)}m{int(eta_seconds)}s"
elif self.config.max_duration:
remaining_seconds = self.config.max_duration - elapsed
eta_minutes, eta_seconds = divmod(remaining_seconds, 60)
eta_text = f"ETA: {int(eta_minutes)}m{int(eta_seconds)}s"
# Calculate progress percentage if we have a limit
progress_pct = ""
if self.config.max_ticks:
pct = (current_tick / self.config.max_ticks) * 100
progress_pct = f"{pct:.1f}%"
elif self.config.max_duration:
pct = (elapsed / self.config.max_duration) * 100
progress_pct = f"{pct:.1f}%"
progress_line = f"[{current_time - self.start_time:.1f}s] "
if progress_pct:
progress_line += f"Progress: {progress_pct} "
progress_line += f"Tick: {current_tick} TPS: {tps:.1f} Files: {self.files_written}"
if eta_text:
progress_line += f" {eta_text}"
# Overwrite the previous line (using carriage return)
print(f"\r{progress_line}", end="", flush=True)
self.last_progress_update = current_time
def _close_progress_bar(self):
"""Close the progress bar."""
if not TQDM_AVAILABLE and self.running:
# Print a newline to clear the text progress line
print()
elif TQDM_AVAILABLE and self.progress_bar:
self.progress_bar.close()
def run(self) -> Dict[str, Any]:
"""Run the headless simulation."""
# Determine if we should run at max speed
max_speed_mode = not self.config.real_time and self.config.simulation.default_tps >= 1000
print(f"Starting headless simulation...")
print(f"Output directory: {self.config.output_dir}")
print(f"Max ticks: {self.config.max_ticks or 'unlimited'}")
print(f"Max duration: {self.config.max_duration or 'unlimited'} seconds")
print(f"Real-time mode: {self.config.real_time}")
print(f"Speed mode: {'Maximum speed' if max_speed_mode else f'{self.config.simulation.default_tps} TPS'}")
print(f"Output formats: {', '.join(self.config.output_formats)}")
print(f"Collectors: {', '.join(self.collectors.keys())}")
print()
self.running = True
self.start_time = time.time()
self.simulation_core.start()
# Initialize progress bar
self._init_progress_bar()
# Enable sprint mode for maximum speed if not real-time mode
if max_speed_mode:
self.simulation_core.timing.set_sprint_mode(True)
last_batch_time = time.time()
batch_interval = 5.0
try:
while self.running:
# Check termination conditions
if self._should_terminate():
break
# Update simulation
if max_speed_mode:
# In max speed mode, update as fast as possible
self.simulation_core.update(0.0)
else:
# Normal timing-based updates
self.simulation_core.update(0.016) # ~60 FPS equivalent
# Collect data
self._collect_data()
# Write batch data periodically
if time.time() - last_batch_time >= batch_interval:
self._write_batch_data()
last_batch_time = time.time()
# Update progress bar
self._update_progress_bar()
# Real-time delay if needed
if self.config.real_time:
time.sleep(0.016) # ~60 FPS
except KeyboardInterrupt:
print("\nSimulation interrupted by user")
except Exception as e:
print(f"Simulation error: {e}")
import traceback
traceback.print_exc()
finally:
self._finalize()
return self._get_summary()
def _should_terminate(self) -> bool:
"""Check if simulation should terminate."""
# Check max ticks
if self.config.max_ticks and self.simulation_core.state.total_ticks >= self.config.max_ticks:
print(f"Reached max ticks: {self.config.max_ticks}")
return True
# Check max duration
if self.config.max_duration:
elapsed = time.time() - self.start_time
if elapsed >= self.config.max_duration:
print(f"Reached max duration: {self.config.max_duration} seconds")
return True
# Check early stopping condition (0 cells remaining)
if self.config.early_stop:
world_state = self.simulation_core.get_world_state()
cell_count = world_state.get('entity_counts', {}).get('cells', 0)
if cell_count == 0:
print(f"Early stopping: 0 cells remaining at tick {self.simulation_core.state.total_ticks}")
return True
return False
def _collect_data(self):
"""Collect data from all collectors."""
for collector_name, collector in self.collectors.items():
data_list = collector.update(self.simulation_core)
for data in data_list:
self.batch_data[collector_name].append(data)
def _write_batch_data(self):
"""Write collected data to files."""
if not self.file_writer.is_ready():
return
for collector_name, data_list in self.batch_data.items():
if not data_list:
continue
for format_name, formatter in self.formatters.items():
# Group data by tick and write one file per tick
data_by_tick = {}
for data in data_list:
tick = data.get('tick_count', data.get('tick', self.simulation_core.state.total_ticks))
if tick not in data_by_tick:
data_by_tick[tick] = []
data_by_tick[tick].append(data)
# Write one file per tick for this collector
for tick, tick_data in data_by_tick.items():
filename = f"{collector_name}_tick{tick}.{formatter.get_file_extension()}"
# If multiple data items for same tick, combine them or write the latest one
combined_data = tick_data[-1] if len(tick_data) == 1 else {
'timestamp': tick_data[0].get('timestamp'),
'tick_count': tick,
'collection_type': collector_name,
'multiple_entries': len(tick_data),
'data': tick_data
}
formatted_data = formatter.format(combined_data)
self.file_writer.write(formatted_data, filename)
self.files_written += 1
# Clear written data
data_list.clear()
def _finalize(self):
"""Finalize simulation and write remaining data."""
# Close progress bar
self._close_progress_bar()
print("Finalizing simulation...")
# Write any remaining data
self._write_batch_data()
# Write final summary
summary = self._get_summary()
if 'json' in self.formatters:
summary_data = self.formatters['json'].format(summary)
self.file_writer.write(summary_data, "simulation_summary.json")
self.files_written += 1
# Stop simulation
self.simulation_core.stop()
self.file_writer.close()
print("Simulation completed")
print(f"Total files written: {self.files_written}")
def _get_summary(self) -> Dict[str, Any]:
"""Get simulation summary."""
duration = time.time() - self.start_time if self.start_time else 0
world_state = self.simulation_core.get_world_state()
return {
'simulation_config': {
'grid_width': self.config.simulation.grid_width,
'grid_height': self.config.simulation.grid_height,
'initial_cells': self.config.simulation.initial_cells,
'initial_food': self.config.simulation.initial_food,
'default_tps': self.config.simulation.default_tps
},
'runtime': {
'duration_seconds': duration,
'total_ticks': self.simulation_core.state.total_ticks,
'average_tps': self.simulation_core.state.total_ticks / duration if duration > 0 else 0,
'final_actual_tps': self.simulation_core.state.actual_tps
},
'final_state': world_state,
'data_collection': {
'collectors_used': list(self.collectors.keys()),
'output_formats': self.config.output_formats,
'output_directory': self.config.output_dir
}
}
def _signal_handler(self, signum, frame):
"""Handle shutdown signals."""
print(f"\nReceived signal {signum}, shutting down gracefully...")
self.running = False
def get_real_time_status(self) -> Dict[str, Any]:
"""Get current simulation status (useful for monitoring)."""
return {
'running': self.running,
'ticks': self.simulation_core.state.total_ticks,
'tps': self.simulation_core.state.actual_tps,
'duration': time.time() - self.start_time if self.start_time else 0,
'world_state': self.simulation_core.get_world_state()
}

View File

@ -1,3 +0,0 @@
"""Entry points for different simulation modes."""
__all__ = []

View File

@ -1,200 +0,0 @@
#!/usr/bin/env python3
"""Headless simulation entry point - runs simulations without UI."""
import sys
import argparse
from pathlib import Path
# Add project root to path
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from config import ConfigLoader, HeadlessConfig
from engines import HeadlessSimulationEngine, HeadlessConfig as EngineHeadlessConfig
def main():
"""Main entry point for headless simulation."""
parser = argparse.ArgumentParser(description="Run headless simulation")
parser.add_argument(
"--config", "-c",
type=str,
help="Path to configuration file (JSON/YAML)",
default=None
)
parser.add_argument(
"--output-dir", "-o",
type=str,
help="Output directory for simulation data",
default="simulation_output"
)
parser.add_argument(
"--max-ticks", "-t",
type=int,
help="Maximum number of ticks to run",
default=None
)
parser.add_argument(
"--max-duration", "-d",
type=float,
help="Maximum duration in seconds",
default=None
)
parser.add_argument(
"--tps",
type=float,
help="Target TPS for simulation (default: unlimited speed)",
default=None
)
parser.add_argument(
"--real-time",
action="store_true",
help="Run in real-time mode (instead of as fast as possible)"
)
parser.add_argument(
"--collect-metrics",
action="store_true",
help="Enable metrics data collection"
)
parser.add_argument(
"--collect-entities",
action="store_true",
help="Enable entity data collection"
)
parser.add_argument(
"--collect-evolution",
action="store_true",
help="Enable evolution data collection"
)
parser.add_argument(
"--collect-all",
action="store_true",
help="Enable all data collection (metrics, entities, evolution)"
)
parser.add_argument(
"--collect-every-tick",
action="store_true",
help="Collect data on every tick instead of at intervals"
)
parser.add_argument(
"--create-sample-configs",
action="store_true",
help="Create sample configuration files and exit"
)
parser.add_argument(
"--early-stop",
action="store_true",
help="Stop simulation when there are 0 cells remaining"
)
args = parser.parse_args()
# Create sample configs if requested
if args.create_sample_configs:
ConfigLoader.create_sample_configs()
return
# Load configuration
try:
if args.config:
headless_config = ConfigLoader.load_headless_config(args.config)
else:
headless_config = ConfigLoader.get_default_headless_config()
# Override config with command line arguments
if args.max_ticks:
headless_config.max_ticks = args.max_ticks
if args.max_duration:
headless_config.max_duration = args.max_duration
if args.tps is not None:
headless_config.simulation.default_tps = args.tps
else:
# No TPS specified - run at maximum speed
headless_config.simulation.default_tps = 999999999.0
if args.output_dir:
headless_config.output.directory = args.output_dir
if args.real_time:
headless_config.output.real_time = True
# Handle data collection arguments - only enable if explicitly requested
# Start with all disabled (default)
headless_config.output.collect_metrics = False
headless_config.output.collect_entities = False
headless_config.output.collect_evolution = False
if args.collect_all:
# Enable all collection
headless_config.output.collect_metrics = True
headless_config.output.collect_entities = True
headless_config.output.collect_evolution = True
else:
# Enable specific collection types
if args.collect_metrics:
headless_config.output.collect_metrics = True
if args.collect_entities:
headless_config.output.collect_entities = True
if args.collect_evolution:
headless_config.output.collect_evolution = True
if args.collect_every_tick:
# Set all collection intervals to 1 for every-tick collection
headless_config.output.metrics_interval = 1
headless_config.output.entities_interval = 1
headless_config.output.evolution_interval = 1
# Also enable all collection if using --collect-every-tick (backward compatibility)
headless_config.output.collect_metrics = True
headless_config.output.collect_entities = True
headless_config.output.collect_evolution = True
# Set early stopping flag
if args.early_stop:
headless_config.early_stop = True
except Exception as e:
print(f"Error loading configuration: {e}")
sys.exit(1)
# Create engine configuration
engine_config = EngineHeadlessConfig(
simulation=headless_config.simulation,
max_ticks=headless_config.max_ticks,
max_duration=headless_config.max_duration,
output_dir=headless_config.output.directory,
enable_metrics=headless_config.output.collect_metrics,
enable_entities=headless_config.output.collect_entities,
enable_evolution=headless_config.output.collect_evolution,
metrics_interval=headless_config.output.metrics_interval,
entities_interval=headless_config.output.entities_interval,
evolution_interval=headless_config.output.evolution_interval,
output_formats=headless_config.output.formats,
real_time=headless_config.output.real_time,
early_stop=headless_config.early_stop
)
# Create and run simulation
try:
print("Starting headless simulation...")
engine = HeadlessSimulationEngine(engine_config)
summary = engine.run()
# Print summary
print("\n" + "="*50)
print("SIMULATION SUMMARY")
print("="*50)
print(f"Total ticks: {summary['runtime']['total_ticks']}")
print(f"Duration: {summary['runtime']['duration_seconds']:.2f} seconds")
print(f"Average TPS: {summary['runtime']['average_tps']:.2f}")
print(f"Final entity counts: {summary['final_state']['entity_counts']}")
print(f"Output directory: {summary['data_collection']['output_directory']}")
except KeyboardInterrupt:
print("\nSimulation interrupted by user")
sys.exit(0)
except Exception as e:
print(f"Simulation error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()

View File

@ -1,66 +0,0 @@
#!/usr/bin/env python3
"""Interactive simulation entry point - runs simulation with UI."""
import sys
import argparse
from pathlib import Path
# Add project root to path
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from config import ConfigLoader
from core.simulation_engine import SimulationEngine
def main():
"""Main entry point for interactive simulation."""
parser = argparse.ArgumentParser(description="Run interactive simulation with UI")
parser.add_argument(
"--config", "-c",
type=str,
help="Path to configuration file (JSON/YAML)",
default=None
)
parser.add_argument(
"--create-sample-configs",
action="store_true",
help="Create sample configuration files and exit"
)
args = parser.parse_args()
# Create sample configs if requested
if args.create_sample_configs:
ConfigLoader.create_sample_configs()
return
# Load configuration
try:
if args.config:
config = ConfigLoader.load_interactive_config(args.config)
else:
config = ConfigLoader.get_default_interactive_config()
except Exception as e:
print(f"Error loading configuration: {e}")
sys.exit(1)
# Run simulation
try:
print("Starting interactive simulation...")
engine = SimulationEngine(config)
engine.run()
except KeyboardInterrupt:
print("\nSimulation interrupted by user")
sys.exit(0)
except Exception as e:
print(f"Simulation error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()

View File

@ -1,8 +1,5 @@
from core.simulation_engine import SimulationEngine from core.simulation_engine import SimulationEngine
def main(): if __name__ == "__main__":
engine = SimulationEngine() engine = SimulationEngine()
engine.run() engine.run()
if __name__ == "__main__":
main()

View File

@ -1,17 +0,0 @@
"""Data collection and output system for headless simulations."""
from .collectors.metrics_collector import MetricsCollector
from .collectors.entity_collector import EntityCollector
from .collectors.evolution_collector import EvolutionCollector
from .formatters.json_formatter import JSONFormatter
from .formatters.csv_formatter import CSVFormatter
from .writers.file_writer import FileWriter
__all__ = [
'MetricsCollector',
'EntityCollector',
'EvolutionCollector',
'JSONFormatter',
'CSVFormatter',
'FileWriter'
]

View File

@ -1,44 +0,0 @@
"""Base data collector interface."""
from abc import ABC, abstractmethod
from typing import Any, Dict, List
from ..formatters.base_formatter import BaseFormatter
from ..writers.base_writer import BaseWriter
class BaseCollector(ABC):
"""Base class for data collectors."""
def __init__(self, collection_interval: int = 100):
self.collection_interval = collection_interval
self.last_collected_tick = -1
self.data_buffer: List[Dict[str, Any]] = []
@abstractmethod
def collect(self, simulation_core) -> Dict[str, Any]:
"""Collect data from simulation core."""
pass
def should_collect(self, current_tick: int) -> bool:
"""Check if data should be collected on current tick."""
return current_tick - self.last_collected_tick >= self.collection_interval
def update(self, simulation_core) -> List[Dict[str, Any]]:
"""Update collector and return collected data if interval reached."""
current_tick = simulation_core.state.total_ticks
if self.should_collect(current_tick):
data = self.collect(simulation_core)
self.data_buffer.append(data)
self.last_collected_tick = current_tick
return [data]
return []
def get_buffered_data(self) -> List[Dict[str, Any]]:
"""Get all buffered data and clear buffer."""
data = self.data_buffer.copy()
self.data_buffer.clear()
return data
def clear_buffer(self):
"""Clear the data buffer."""
self.data_buffer.clear()

View File

@ -1,72 +0,0 @@
"""Entity state collector for detailed entity tracking."""
from typing import Dict, Any, List
from .base_collector import BaseCollector
from world.objects import DefaultCell
class EntityCollector(BaseCollector):
"""Collects detailed entity state information."""
def __init__(self, collection_interval: int = 1000, include_cells: bool = True, include_food: bool = False):
super().__init__(collection_interval)
self.include_cells = include_cells
self.include_food = include_food
def collect(self, simulation_core) -> Dict[str, Any]:
"""Collect entity states from simulation core."""
world_state = simulation_core.get_world_state()
entities = []
for entity_data in simulation_core.get_entity_states():
entity_type = entity_data['type']
# Filter by entity type based on configuration
if entity_type == 'cell' and not self.include_cells:
continue
elif entity_type == 'food' and not self.include_food:
continue
entities.append(entity_data)
# Calculate additional statistics for cells
cell_stats = {}
if self.include_cells:
cells = [e for e in entities if e['type'] == 'cell']
if cells:
energies = [c['energy'] for c in cells]
ages = [c['age'] for c in cells]
generations = [c['generation'] for c in cells]
cell_stats = {
'avg_energy': sum(energies) / len(energies),
'max_energy': max(energies),
'min_energy': min(energies),
'avg_age': sum(ages) / len(ages),
'max_age': max(ages),
'avg_generation': sum(generations) / len(generations),
'max_generation': max(generations)
}
# Calculate food statistics
food_stats = {}
if self.include_food:
foods = [e for e in entities if e['type'] == 'food']
if foods:
decays = [f['decay'] for f in foods]
food_stats = {
'avg_decay': sum(decays) / len(decays),
'max_decay': max(decays),
'min_decay': min(decays),
'fresh_food': len([f for f in foods if f['decay'] < f['max_decay'] * 0.5])
}
return {
'timestamp': simulation_core.timing.last_tick_time,
'tick_count': world_state['tick_count'],
'entity_count': len(entities),
'entities': entities,
'cell_statistics': cell_stats,
'food_statistics': food_stats,
'collection_type': 'entities'
}

View File

@ -1,100 +0,0 @@
"""Evolution data collector for tracking neural network changes."""
from typing import Dict, Any, List
from .base_collector import BaseCollector
from world.objects import DefaultCell
import numpy as np
class EvolutionCollector(BaseCollector):
"""Collects evolution and neural network data."""
def __init__(self, collection_interval: int = 1000):
super().__init__(collection_interval)
def collect(self, simulation_core) -> Dict[str, Any]:
"""Collect evolution data from simulation core."""
world_state = simulation_core.get_world_state()
cells_data = []
network_architectures = {}
total_weights = []
layer_sizes_common = {}
for entity_data in simulation_core.get_entity_states():
if entity_data['type'] == 'cell':
cells_data.append(entity_data)
# Track neural network architecture
nn_data = entity_data['neural_network']
layers_key = str(nn_data['layer_sizes']) # Convert to string for JSON compatibility
if layers_key not in network_architectures:
network_architectures[layers_key] = 0
network_architectures[layers_key] += 1
# Use layer count as a proxy for complexity
total_weights.append(len(nn_data['layer_sizes']))
# Track layer size frequencies
for size in nn_data['layer_sizes']:
if size not in layer_sizes_common:
layer_sizes_common[size] = 0
layer_sizes_common[size] += 1
# Calculate evolution statistics
evolution_stats = {}
if cells_data:
energies = [c['energy'] for c in cells_data]
ages = [c['age'] for c in cells_data]
generations = [c['generation'] for c in cells_data]
evolution_stats = {
'cell_count': len(cells_data),
'energy_distribution': {
'mean': np.mean(energies),
'std': np.std(energies),
'min': min(energies),
'max': max(energies),
'median': np.median(energies)
},
'age_distribution': {
'mean': np.mean(ages),
'std': np.std(ages),
'min': min(ages),
'max': max(ages),
'median': np.median(ages)
},
'generation_distribution': {
'mean': np.mean(generations),
'std': np.std(generations),
'min': min(generations),
'max': max(generations),
'median': np.median(generations)
}
}
# Network architecture statistics
network_stats = {}
if total_weights:
network_stats = {
'architecture_diversity': len(network_architectures),
'most_common_architecture': max(network_architectures.items(), key=lambda x: x[1]) if network_architectures else None,
'complexity_distribution': {
'mean': np.mean(total_weights),
'std': np.std(total_weights),
'min': min(total_weights),
'max': max(total_weights)
},
'layer_size_diversity': len(layer_sizes_common),
'most_common_layer_sizes': sorted(layer_sizes_common.items(), key=lambda x: x[1], reverse=True)[:5]
}
return {
'timestamp': simulation_core.timing.last_tick_time,
'tick_count': world_state['tick_count'],
'evolution_statistics': evolution_stats,
'network_statistics': network_stats,
'network_architectures': dict(network_architectures),
'collection_type': 'evolution'
}

View File

@ -1,28 +0,0 @@
"""Basic metrics collector for simulation statistics."""
from typing import Dict, Any
from .base_collector import BaseCollector
class MetricsCollector(BaseCollector):
"""Collects basic simulation metrics."""
def __init__(self, collection_interval: int = 100):
super().__init__(collection_interval)
def collect(self, simulation_core) -> Dict[str, Any]:
"""Collect basic metrics from simulation core."""
world_state = simulation_core.get_world_state()
return {
'timestamp': simulation_core.timing.last_tick_time,
'tick_count': world_state['tick_count'],
'actual_tps': world_state['actual_tps'],
'target_tps': world_state['target_tps'],
'speed_multiplier': world_state['speed_multiplier'],
'is_paused': world_state['is_paused'],
'sprint_mode': world_state['sprint_mode'],
'world_buffer': world_state['world_buffer'],
'entity_counts': world_state['entity_counts'],
'collection_type': 'metrics'
}

View File

@ -1,18 +0,0 @@
"""Base formatter interface for output data."""
from abc import ABC, abstractmethod
from typing import Any
class BaseFormatter(ABC):
"""Base class for data formatters."""
@abstractmethod
def format(self, data: Any) -> str:
"""Format data for output."""
pass
@abstractmethod
def get_file_extension(self) -> str:
"""Get file extension for this format."""
pass

View File

@ -1,83 +0,0 @@
"""CSV formatter for tabular output data."""
import csv
import io
from typing import Any, List, Dict
from .base_formatter import BaseFormatter
class CSVFormatter(BaseFormatter):
"""Formats data as CSV."""
def __init__(self, flatten_nested: bool = True):
self.flatten_nested = flatten_nested
def format(self, data: Any) -> str:
"""Format data as CSV string."""
if isinstance(data, list):
return self._format_list(data)
elif isinstance(data, dict):
return self._format_dict(data)
else:
# Single value
return str(data)
def _format_list(self, data: List[Dict[str, Any]]) -> str:
"""Format list of dictionaries as CSV."""
if not data:
return ""
# Flatten nested dictionaries if requested
processed_data = []
for item in data:
if self.flatten_nested:
processed_data.append(self._flatten_dict(item))
else:
processed_data.append(item)
# Get all field names from all items
fieldnames = set()
for item in processed_data:
fieldnames.update(item.keys())
fieldnames = sorted(fieldnames)
# Create CSV
output = io.StringIO()
writer = csv.DictWriter(output, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(processed_data)
return output.getvalue()
def _format_dict(self, data: Dict[str, Any]) -> str:
"""Format single dictionary as CSV."""
processed_data = self._flatten_dict(data) if self.flatten_nested else data
fieldnames = sorted(processed_data.keys())
output = io.StringIO()
writer = csv.DictWriter(output, fieldnames=fieldnames)
writer.writeheader()
writer.writerow(processed_data)
return output.getvalue()
def _flatten_dict(self, data: Dict[str, Any], parent_key: str = '', sep: str = '_') -> Dict[str, Any]:
"""Flatten nested dictionaries."""
items = []
for key, value in data.items():
new_key = f"{parent_key}{sep}{key}" if parent_key else key
if isinstance(value, dict):
items.extend(self._flatten_dict(value, new_key, sep).items())
elif isinstance(value, list):
# Convert lists to strings or handle each element
if value and isinstance(value[0], (int, float, str)):
items.append((new_key, ','.join(map(str, value))))
else:
items.append((new_key, str(value)))
else:
items.append((new_key, value))
return dict(items)
def get_file_extension(self) -> str:
"""Get file extension for CSV format."""
return "csv"

View File

@ -1,20 +0,0 @@
"""JSON formatter for output data."""
import json
from typing import Any
from .base_formatter import BaseFormatter
class JSONFormatter(BaseFormatter):
"""Formats data as JSON."""
def __init__(self, indent: int = 2):
self.indent = indent
def format(self, data: Any) -> str:
"""Format data as JSON string."""
return json.dumps(data, indent=self.indent, default=str)
def get_file_extension(self) -> str:
"""Get file extension for JSON format."""
return "json"

View File

@ -1,23 +0,0 @@
"""Base writer interface for output data."""
from abc import ABC, abstractmethod
from typing import Any
class BaseWriter(ABC):
"""Base class for data writers."""
@abstractmethod
def write(self, data: Any) -> bool:
"""Write data to output destination."""
pass
@abstractmethod
def close(self):
"""Close writer and cleanup resources."""
pass
@abstractmethod
def is_ready(self) -> bool:
"""Check if writer is ready for writing."""
pass

View File

@ -1,140 +0,0 @@
"""File writer for output data."""
import os
from pathlib import Path
from typing import Any, List, Optional
from .base_writer import BaseWriter
class FileWriter(BaseWriter):
"""Writes data to files."""
def __init__(self, output_dir: str = "simulation_output", create_dirs: bool = True):
self.output_dir = Path(output_dir)
self.create_dirs = create_dirs
self.file_handles = {}
self.ready = False
if self.create_dirs:
self.output_dir.mkdir(parents=True, exist_ok=True)
self.ready = self.output_dir.exists()
def write(self, data: Any, filename: Optional[str] = None, data_type: str = "data") -> bool:
"""Write data to file."""
if not self.ready:
return False
try:
if filename is None:
# Generate filename based on data type and timestamp
import time
timestamp = int(time.time())
filename = f"{data_type}_{timestamp}.txt"
filepath = self.output_dir / filename
# Handle different data types
if isinstance(data, str):
# Already formatted data
with open(filepath, 'w', encoding='utf-8') as f:
f.write(data)
elif isinstance(data, (list, dict)):
# Convert to string representation
with open(filepath, 'w', encoding='utf-8') as f:
f.write(str(data))
else:
# Single value
with open(filepath, 'w', encoding='utf-8') as f:
f.write(str(data))
return True
except Exception as e:
print(f"Error writing to file {filename}: {e}")
return False
def write_batch(self, data_list: List[Any], filename: str, data_type: str = "batch") -> bool:
"""Write multiple data items to a single file."""
if not self.ready or not data_list:
return False
try:
filepath = self.output_dir / filename
# Combine all data items
if isinstance(data_list[0], str):
# Already formatted strings
content = '\n'.join(data_list)
else:
# Convert to string representation
content = '\n'.join(str(item) for item in data_list)
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
return True
except Exception as e:
print(f"Error writing batch to file {filename}: {e}")
return False
def append(self, data: Any, filename: str) -> bool:
"""Append data to existing file."""
if not self.ready:
return False
try:
filepath = self.output_dir / filename
with open(filepath, 'a', encoding='utf-8') as f:
f.write(str(data))
f.write('\n') # Add newline
return True
except Exception as e:
print(f"Error appending to file {filename}: {e}")
return False
def get_file_path(self, filename: str) -> Path:
"""Get full path for a filename."""
return self.output_dir / filename
def list_files(self, pattern: str = "*") -> List[str]:
"""List files in output directory."""
if not self.ready:
return []
try:
return [f.name for f in self.output_dir.glob(pattern) if f.is_file()]
except Exception:
return []
def delete_file(self, filename: str) -> bool:
"""Delete a file."""
if not self.ready:
return False
try:
filepath = self.output_dir / filename
if filepath.exists():
filepath.unlink()
return True
return False
except Exception as e:
print(f"Error deleting file {filename}: {e}")
return False
def close(self):
"""Close file handles and cleanup."""
for handle in self.file_handles.values():
try:
handle.close()
except:
pass
self.file_handles.clear()
def is_ready(self) -> bool:
"""Check if writer is ready for writing."""
return self.ready

View File

@ -4,22 +4,15 @@ version = "0.1.0"
description = "Add your description here" description = "Add your description here"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [ dependencies = [
"matplotlib>=3.10.7",
"numpy>=2.3.0", "numpy>=2.3.0",
"pandas>=2.3.3",
"pre-commit>=4.2.0", "pre-commit>=4.2.0",
"psutil>=7.0.0",
"pydantic>=2.11.5", "pydantic>=2.11.5",
"pygame>=2.6.1", "pygame>=2.6.1",
"pygame-gui>=0.6.14", "pygame-gui>=0.6.14",
"pytest>=8.3.5", "pytest>=8.3.5",
"pyyaml>=6.0.2",
"tqdm>=4.67.1",
] ]
[dependency-groups] [dependency-groups]
dev = [ dev = [
"psutil>=7.0.0",
"ruff>=0.11.12", "ruff>=0.11.12",
"snakeviz>=2.2.2",
] ]

View File

@ -1,107 +0,0 @@
# python
"""
convert_metrics.py
Usage examples:
python convert_metrics.py --input-dir output_test
python convert_metrics.py --input-dir output_test --out combined_metrics.csv --per-file
"""
import argparse
import json
from pathlib import Path
from typing import Any, Dict, List
import pandas as pd
def flatten(d: Dict[str, Any], parent_key: str = "", sep: str = "_") -> Dict[str, Any]:
"""Flatten nested dict into single-level dict with keys joined by `sep`."""
items: Dict[str, Any] = {}
for k, v in d.items():
new_key = f"{parent_key}{sep}{k}" if parent_key else k
if isinstance(v, dict):
items.update(flatten(v, new_key, sep=sep))
else:
items[new_key] = v
return items
def is_metrics_file(path: Path) -> bool:
"""Quick heuristic: filename contains 'metrics' or JSON has collection_type == metrics."""
if "metrics" in path.name.lower():
return True
try:
data = json.loads(path.read_text())
if isinstance(data, dict) and data.get("collection_type") == "metrics":
return True
if isinstance(data, list) and any(isinstance(item, dict) and item.get("collection_type") == "metrics" for item in data):
return True
except Exception:
pass
return False
def load_metric_records(path: Path) -> List[Dict[str, Any]]:
"""Load JSON file and return list of metric records (handles single dict or list)."""
text = path.read_text()
data = json.loads(text)
if isinstance(data, list):
return data
if isinstance(data, dict):
return [data]
raise ValueError(f"Unsupported JSON structure in {path}")
def main():
parser = argparse.ArgumentParser(description="Convert metric JSON files to CSV")
parser.add_argument("--input-dir", "-i", type=str, default="output_test", help="Directory to search for metric files")
parser.add_argument("--out", "-o", type=str, default="metrics_combined.csv", help="Output CSV path for combined metrics")
parser.add_argument("--recursive", "-r", action="store_true", help="Search recursively")
parser.add_argument("--per-file", action="store_true", help="Also write one CSV per metric file (same folder)")
args = parser.parse_args()
base = Path(args.input_dir)
if not base.exists():
raise SystemExit(f"Input directory does not exist: {base}")
pattern = "**/*.json" if args.recursive else "*.json"
json_files = sorted(base.glob(pattern))
rows: List[Dict[str, Any]] = []
processed = 0
for p in json_files:
if not p.is_file():
continue
if not is_metrics_file(p):
continue
try:
records = load_metric_records(p)
except Exception as e:
print(f"Skipping {p} (failed to parse): {e}")
continue
per_file_rows: List[Dict[str, Any]] = []
for rec in records:
flat = flatten(rec)
# add provenance
flat["_source_file"] = str(p)
per_file_rows.append(flat)
rows.append(flat)
if args.per_file and per_file_rows:
df_pf = pd.DataFrame(per_file_rows)
out_path = p.with_suffix(".csv")
df_pf.to_csv(out_path, index=False)
processed += 1
if not rows:
print("No metric files found.")
return
df = pd.DataFrame(rows)
df.to_csv(args.out, index=False)
print(f"Wrote combined CSV to {args.out} ({len(df)} rows) from {processed} metric files.")
if __name__ == "__main__":
main()

View File

@ -1,118 +0,0 @@
# python
"""
plot_metrics.py
Usage examples:
python plot_metrics.py --csv metrics_combined.csv
python plot_metrics.py --csv metrics_combined.csv --time-col tick --out myplot.png
python plot_metrics.py --csv metrics_combined.csv --cols entity_counts_cells,entity_counts_food
"""
import argparse
from pathlib import Path
import sys
import pandas as pd
import matplotlib.pyplot as plt
COMMON_TIME_COLS = ["tick", "time", "step", "tick_number", "t"]
DEFAULT_PLOT_COLS = ["entity_counts_cells", "entity_counts_food"]
def find_column(df: pd.DataFrame, candidates):
# return the first matching column name from candidates (case-insensitive, substring match)
cols = {c.lower(): c for c in df.columns}
for cand in candidates:
cand_l = cand.lower()
# exact match
if cand_l in cols:
return cols[cand_l]
# substring match
for k, orig in cols.items():
if cand_l in k:
return orig
return None
def main():
p = argparse.ArgumentParser(description="Plot entity counts over time from a metrics CSV")
p.add_argument("--csv", "-c", type=str, default="metrics_combined.csv", help="Path to CSV file")
p.add_argument("--time-col", "-t", type=str, default=None, help="Name of the time column (optional)")
p.add_argument("--cols", type=str, default=None, help="Comma-separated column names to plot (default: entity_counts_cells,entity_counts_food)")
p.add_argument("--out", "-o", type=str, default="metrics_counts_plot.png", help="Output image path")
args = p.parse_args()
csv_path = Path(args.csv)
if not csv_path.exists():
print(f"CSV not found: {csv_path}", file=sys.stderr)
sys.exit(1)
df = pd.read_csv(csv_path)
# detect time column
time_col = None
if args.time_col:
if args.time_col in df.columns:
time_col = args.time_col
else:
print(f"Specified time column `{args.time_col}` not found in CSV columns.", file=sys.stderr)
sys.exit(1)
else:
time_col = find_column(df, COMMON_TIME_COLS)
if time_col is None:
print("Could not auto-detect a time column. Provide one with `--time-col`.", file=sys.stderr)
sys.exit(1)
# determine plot columns
if args.cols:
cols = [c.strip() for c in args.cols.split(",") if c.strip()]
missing = [c for c in cols if c not in df.columns]
if missing:
print(f"Columns not found in CSV: {missing}", file=sys.stderr)
sys.exit(1)
else:
cols = []
for want in DEFAULT_PLOT_COLS:
found = find_column(df, [want])
if found:
cols.append(found)
if not cols:
print(f"Could not find default columns `{DEFAULT_PLOT_COLS}`. Provide `--cols` explicitly.", file=sys.stderr)
sys.exit(1)
# prepare data
df = df[[time_col] + cols].copy()
df[time_col] = pd.to_numeric(df[time_col], errors="coerce")
for c in cols:
df[c] = pd.to_numeric(df[c], errors="coerce")
df = df.dropna(subset=[time_col])
if df.empty:
print("No numeric time values found after cleaning.", file=sys.stderr)
sys.exit(1)
df = df.sort_values(by=time_col)
# plot
plt.figure(figsize=(10, 5))
for c in cols:
plt.plot(df[time_col], df[c], label=c, linewidth=2)
plt.xlabel(time_col)
plt.ylabel("Count")
plt.title("Entity counts over time")
plt.grid(True, linestyle="--", alpha=0.4)
plt.legend()
plt.tight_layout()
out_path = Path(args.out)
plt.savefig(out_path, dpi=150)
print(f"Wrote plot to `{out_path}`")
# also show interactively if running in an environment with a display
try:
plt.show()
except Exception:
pass
if __name__ == "__main__":
main()

View File

@ -1,93 +0,0 @@
import time
import random
import statistics
import hashlib
import pickle
class HeadlessSimulationBenchmark:
def __init__(self, setup_world, random_seed=42):
"""
:param setup_world: Callable that returns a World instance.
:param random_seed: Seed for random number generation.
"""
self.setup_world = setup_world
self.random_seed = random_seed
self.world = None
self.tps_history = []
self._running = False
self.ticks_elapsed_time = None # Track time for designated ticks
def set_random_seed(self, seed):
self.random_seed = seed
random.seed(seed)
def start(self, ticks=100, max_seconds=None):
self.set_random_seed(self.random_seed)
self.world = self.setup_world(self.random_seed)
self.tps_history.clear()
self._running = True
tick_count = 0
start_time = time.perf_counter()
last_time = start_time
# For precise tick timing
tick_timing_start = None
if ticks is not None:
tick_timing_start = time.perf_counter()
while self._running and (ticks is None or tick_count < ticks):
self.world.tick_all()
tick_count += 1
now = time.perf_counter()
elapsed = now - last_time
if elapsed > 0:
self.tps_history.append(1.0 / elapsed)
last_time = now
if max_seconds and (now - start_time) > max_seconds:
break
if ticks is not None:
tick_timing_end = time.perf_counter()
self.ticks_elapsed_time = tick_timing_end - tick_timing_start
else:
self.ticks_elapsed_time = None
self._running = False
def stop(self):
self._running = False
def get_tps_history(self):
return self.tps_history
def get_tps_average(self):
return statistics.mean(self.tps_history) if self.tps_history else 0.0
def get_tps_stddev(self):
return statistics.stdev(self.tps_history) if len(self.tps_history) > 1 else 0.0
def get_simulation_hash(self):
# Serialize the world state and hash it for determinism checks
state = []
for obj in self.world.get_objects():
state.append((
type(obj).__name__,
getattr(obj, "position", None),
getattr(obj, "rotation", None),
getattr(obj, "flags", None),
getattr(obj, "interaction_radius", None),
getattr(obj, "max_visual_width", None),
))
state_bytes = pickle.dumps(state)
return hashlib.sha256(state_bytes).hexdigest()
def get_summary(self):
return {
"tps_avg": self.get_tps_average(),
"tps_stddev": self.get_tps_stddev(),
"ticks": len(self.tps_history),
"simulation_hash": self.get_simulation_hash(),
"ticks_elapsed_time": self.ticks_elapsed_time,
}

View File

@ -1,111 +0,0 @@
"""Tests for configuration system."""
import pytest
import json
import tempfile
import os
from config.simulation_config import SimulationConfig, HeadlessConfig, InteractiveConfig, ExperimentConfig
from config.config_loader import ConfigLoader
class TestSimulationConfig:
"""Test simulation configuration."""
def test_custom_values(self):
"""Test custom configuration values."""
config = SimulationConfig(
grid_width=50,
grid_height=40,
cell_size=15,
initial_cells=100,
initial_food=200,
food_spawning=False,
random_seed=999,
default_tps=120.0
)
assert config.grid_width == 50
assert config.grid_height == 40
assert config.cell_size == 15
assert config.initial_cells == 100
assert config.initial_food == 200
assert config.food_spawning == False
assert config.random_seed == 999
assert config.default_tps == 120.0
class TestConfigLoader:
"""Test configuration loader."""
def test_load_headless_config(self):
"""Test loading headless configuration from file."""
# Create a temporary file
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
temp_file = f.name
json.dump({
"max_ticks": 10000,
"max_duration": 300.0,
"output": {
"enabled": True,
"directory": "test_output",
"formats": ["json", "csv"]
},
"simulation": {
"grid_width": 50,
"grid_height": 40,
"initial_cells": 100,
"default_tps": 60.0
}
}, f)
try:
config = ConfigLoader.load_headless_config(temp_file)
assert config.max_ticks == 10000
assert config.max_duration == 300.0
assert config.output.enabled == True
assert config.output.directory == "test_output"
assert config.output.formats == ["json", "csv"]
assert config.simulation.grid_width == 50
assert config.simulation.grid_height == 40
assert config.simulation.initial_cells == 100
assert config.simulation.default_tps == 60.0
finally:
os.unlink(temp_file)
def test_save_load_json(self):
"""Test saving and loading configuration from JSON file."""
# Create a temporary file
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
temp_file = f.name
try:
# Create test configuration
original_config = HeadlessConfig(
max_ticks=5000,
simulation=SimulationConfig(
grid_width=25,
grid_height=20,
initial_cells=75,
default_tps=90.0
)
)
# Save to JSON
ConfigLoader.save_config(original_config, temp_file)
# Load from JSON
loaded_config = ConfigLoader.load_headless_config(temp_file)
# Verify loaded config matches original
assert loaded_config.max_ticks == original_config.max_ticks
assert loaded_config.simulation.grid_width == original_config.simulation.grid_width
assert loaded_config.simulation.grid_height == original_config.simulation.grid_height
assert loaded_config.simulation.initial_cells == original_config.simulation.initial_cells
assert loaded_config.simulation.default_tps == original_config.simulation.default_tps
finally:
# Clean up temporary file
if os.path.exists(temp_file):
os.unlink(temp_file)

View File

@ -1,390 +0,0 @@
"""Tests for core simulation components."""
import pytest
import time
from unittest.mock import Mock
from core.simulation_core import SimulationCore, SimulationState
from core.timing import TimingController, TimingState
from core.event_bus import EventBus, EventType, Event
from config.simulation_config import SimulationConfig
class TestTimingController:
"""Test timing controller functionality."""
def test_initialization(self):
"""Test timing controller initialization."""
event_bus = Mock()
timing = TimingController(default_tps=60.0, event_bus=event_bus)
assert timing.state.tps == 60.0
assert timing.state.is_paused == False
assert timing.state.sprint_mode == False
assert timing.state.speed_multiplier == 1.0
assert timing.default_tps == 60.0
assert timing.total_ticks == 0
def test_set_tps(self):
"""Test TPS setting."""
timing = TimingController(default_tps=60.0)
timing.set_tps(120.0)
assert timing.state.tps == 120.0
assert timing.state.speed_multiplier == 2.0
# Test bounds
timing.set_tps(2000.0) # Should be capped at 1000
assert timing.state.tps == 1000.0
timing.set_tps(0.5) # Should be capped at 1.0
assert timing.state.tps == 1.0
def test_set_speed_multiplier(self):
"""Test speed multiplier setting."""
timing = TimingController(default_tps=60.0)
timing.set_speed_multiplier(2.0)
assert timing.state.speed_multiplier == 2.0
assert timing.state.tps == 120.0
# Test bounds
timing.set_speed_multiplier(20.0) # Should be capped at 10.0
assert timing.state.speed_multiplier == 10.0
timing.set_speed_multiplier(0.05) # Should be capped at 0.1
assert timing.state.speed_multiplier == 0.1
def test_pause_toggle(self):
"""Test pause functionality."""
timing = TimingController()
assert timing.state.is_paused == False
timing.toggle_pause()
assert timing.state.is_paused == True
timing.set_pause(False) # This will unpause since it's different
assert timing.state.is_paused == False
timing.set_pause(True) # This will pause since it's different
assert timing.state.is_paused == True
timing.toggle_pause()
assert timing.state.is_paused == False
def test_sprint_mode(self):
"""Test sprint mode functionality."""
timing = TimingController()
assert timing.state.sprint_mode == False
timing.toggle_sprint_mode()
assert timing.state.sprint_mode == True
assert timing.sprint_start_time is not None
timing.toggle_sprint_mode()
assert timing.state.sprint_mode == False
assert timing.sprint_start_time is None
def test_should_tick(self):
"""Test tick timing logic."""
timing = TimingController(default_tps=60.0)
# When paused, should not tick
timing.state.is_paused = True
assert timing.should_tick() == False
# When in sprint mode, should always tick
timing.state.is_paused = False
timing.state.sprint_mode = True
assert timing.should_tick() == True
# Normal mode - should tick based on time
timing.state.sprint_mode = False
timing.last_tick_time = time.perf_counter()
time.sleep(0.02) # 20ms, should be enough for 60 TPS (16.67ms interval)
assert timing.should_tick() == True
def test_update_timing(self):
"""Test timing update calculations."""
timing = TimingController(default_tps=60.0)
initial_ticks = timing.total_ticks
timing.update_timing()
assert timing.total_ticks == initial_ticks + 1
assert timing.tick_counter == 1
def test_display_functions(self):
"""Test display utility functions."""
timing = TimingController(default_tps=60.0)
# Test TPS display
timing.actual_tps = 59.7
assert timing.get_display_tps() == 60
# Test speed display
assert timing.get_current_speed_display() == "1x"
timing.state.is_paused = True
assert timing.get_current_speed_display() == "Paused"
timing.state.is_paused = False
timing.state.sprint_mode = True
assert timing.get_current_speed_display() == "Sprint"
timing.state.sprint_mode = False
timing.state.speed_multiplier = 2.0
assert timing.get_current_speed_display() == "2.0x"
class TestEventBus:
"""Test event bus functionality."""
def test_initialization(self):
"""Test event bus initialization."""
bus = EventBus()
assert len(bus._subscribers) == 0
assert len(bus._event_history) == 0
def test_subscribe_unsubscribe(self):
"""Test subscribing and unsubscribing to events."""
bus = EventBus()
callback = Mock()
bus.subscribe(EventType.SIMULATION_STATE_CHANGED, callback)
assert EventType.SIMULATION_STATE_CHANGED in bus._subscribers
assert callback in bus._subscribers[EventType.SIMULATION_STATE_CHANGED]
bus.unsubscribe(EventType.SIMULATION_STATE_CHANGED, callback)
assert callback not in bus._subscribers[EventType.SIMULATION_STATE_CHANGED]
def test_publish_and_receive(self):
"""Test publishing and receiving events."""
bus = EventBus()
callback = Mock()
bus.subscribe(EventType.WORLD_TICK_COMPLETED, callback)
event = Event(type=EventType.WORLD_TICK_COMPLETED, data={'tick_count': 100})
bus.publish(event)
callback.assert_called_once_with(event)
assert len(bus._event_history) == 1
assert bus._event_history[0] == event
def test_multiple_subscribers(self):
"""Test multiple subscribers to same event."""
bus = EventBus()
callback1 = Mock()
callback2 = Mock()
bus.subscribe(EventType.ENTITY_ADDED, callback1)
bus.subscribe(EventType.ENTITY_ADDED, callback2)
event = Event(type=EventType.ENTITY_ADDED, data={'entity_id': 123})
bus.publish(event)
callback1.assert_called_once_with(event)
callback2.assert_called_once_with(event)
def test_event_history(self):
"""Test event history management."""
bus = EventBus()
# Add some events
for i in range(5):
event = Event(type=EventType.TIMING_UPDATE, data={'tick': i})
bus.publish(event)
assert len(bus._event_history) == 5
# Test getting recent events
recent = bus.get_recent_events(count=3)
assert len(recent) == 3
assert recent[0].data['tick'] == 2 # Third from last
assert recent[2].data['tick'] == 4 # Most recent
# Test filtered events
timing_events = bus.get_recent_events(event_type=EventType.TIMING_UPDATE)
assert len(timing_events) == 5
# Clear history
bus.clear_history()
assert len(bus._event_history) == 0
def test_callback_error_handling(self):
"""Test that callback errors don't crash the event bus."""
bus = EventBus()
good_callback = Mock()
def bad_callback(event):
raise ValueError("Test error")
bus.subscribe(EventType.SIMULATION_STATE_CHANGED, good_callback)
bus.subscribe(EventType.SIMULATION_STATE_CHANGED, bad_callback)
# This should not raise an exception despite the bad callback
event = Event(type=EventType.SIMULATION_STATE_CHANGED, data={})
bus.publish(event) # Should not crash
# Good callback should still be called
good_callback.assert_called_once()
class TestSimulationCore:
"""Test simulation core functionality."""
def test_initialization(self):
"""Test simulation core initialization."""
config = SimulationConfig(
grid_width=20,
grid_height=15,
initial_cells=5,
initial_food=10
)
core = SimulationCore(config)
assert core.config == config
assert core.world is not None
assert core.timing is not None
assert core.event_bus is not None
assert core.state.total_ticks == 0
assert core.is_running == False
def test_start_stop(self):
"""Test starting and stopping simulation."""
config = SimulationConfig(initial_cells=0, initial_food=0)
core = SimulationCore(config)
assert core.is_running == False
assert core.state.is_running == False
core.start()
assert core.is_running == True
assert core.state.is_running == True
core.stop()
assert core.is_running == False
assert core.state.is_running == False
def test_pause_resume(self):
"""Test pause and resume functionality."""
config = SimulationConfig(initial_cells=0, initial_food=0)
core = SimulationCore(config)
core.start()
# Pause
core.pause()
assert core.timing.state.is_paused == True
# Resume
core.resume()
assert core.timing.state.is_paused == False
core.stop()
def test_step_execution(self):
"""Test single step execution."""
config = SimulationConfig(initial_cells=0, initial_food=0)
core = SimulationCore(config)
initial_ticks = core.state.total_ticks
core.step()
# Should have advanced by exactly one tick
assert core.state.total_ticks == initial_ticks + 1
def test_tps_control(self):
"""Test TPS control functionality."""
config = SimulationConfig(initial_cells=0, initial_food=0)
core = SimulationCore(config)
core.set_tps(120.0)
assert core.timing.state.tps == 120.0
core.set_speed_multiplier(2.0)
assert core.timing.state.speed_multiplier == 2.0
def test_entity_queries(self):
"""Test entity query methods."""
config = SimulationConfig(
grid_width=10,
grid_height=10,
initial_cells=5,
initial_food=3
)
core = SimulationCore(config)
# Test entity counting
from world.objects import DefaultCell, FoodObject
cell_count = core.count_entities_by_type(DefaultCell)
food_count = core.count_entities_by_type(FoodObject)
assert cell_count == 5
assert food_count == 3
# Test position queries
from world.world import Position
position = Position(x=0, y=0)
entities_in_radius = core.get_entities_in_radius(position, radius=50)
assert len(entities_in_radius) >= 0 # Should find some entities
def test_world_state(self):
"""Test world state collection."""
config = SimulationConfig(initial_cells=2, initial_food=3)
core = SimulationCore(config)
state = core.get_world_state()
assert 'tick_count' in state
assert 'actual_tps' in state
assert 'entity_counts' in state
assert state['entity_counts']['total'] == 5 # 2 cells + 3 food
assert state['entity_counts']['cells'] == 2
assert state['entity_counts']['food'] == 3
def test_entity_states(self):
"""Test entity state collection."""
config = SimulationConfig(initial_cells=2, initial_food=1)
core = SimulationCore(config)
entities = core.get_entity_states()
assert len(entities) == 3 # 2 cells + 1 food
# Check cell entities
cell_entities = [e for e in entities if e['type'] == 'cell']
assert len(cell_entities) == 2
for cell in cell_entities:
assert 'id' in cell
assert 'position' in cell
assert 'energy' in cell
assert 'age' in cell
assert 'neural_network' in cell
# Check food entities
food_entities = [e for e in entities if e['type'] == 'food']
assert len(food_entities) == 1
for food in food_entities:
assert 'id' in food
assert 'position' in food
assert 'decay' in food
def test_sprint_mode(self):
"""Test sprint mode functionality."""
config = SimulationConfig(initial_cells=0, initial_food=0)
core = SimulationCore(config)
assert core.timing.state.sprint_mode == False
core.toggle_sprint_mode()
assert core.timing.state.sprint_mode == True
core.toggle_sprint_mode()
assert core.timing.state.sprint_mode == False

View File

@ -1,57 +0,0 @@
import pytest
import random
from world.world import World, Position, Rotation
from world.objects import FoodObject, DefaultCell
from tests.benchmarking import HeadlessSimulationBenchmark
# Hardcoded simulation parameters (copied from config/constants.py)
CELL_SIZE = 20
GRID_WIDTH = 30
GRID_HEIGHT = 25
FOOD_OBJECTS_COUNT = 500
RANDOM_SEED = 12345
def _setup_world(seed=RANDOM_SEED):
world = World(CELL_SIZE, (CELL_SIZE * GRID_WIDTH, CELL_SIZE * GRID_HEIGHT))
random.seed(seed)
half_width = GRID_WIDTH * CELL_SIZE // 2
half_height = GRID_HEIGHT * CELL_SIZE // 2
for _ in range(FOOD_OBJECTS_COUNT):
x = random.randint(-half_width, half_width)
y = random.randint(-half_height, half_height)
world.add_object(FoodObject(Position(x=x, y=y)))
for _ in range(300):
new_cell = DefaultCell(
Position(x=random.randint(-half_width, half_width), y=random.randint(-half_height, half_height)),
Rotation(angle=0)
)
new_cell.behavioral_model = new_cell.behavioral_model.mutate(3)
world.add_object(new_cell)
return world
def test_simulation_determinism():
bench1 = HeadlessSimulationBenchmark(lambda seed: _setup_world(seed), random_seed=RANDOM_SEED)
bench2 = HeadlessSimulationBenchmark(lambda seed: _setup_world(seed), random_seed=RANDOM_SEED)
bench1.start(ticks=100)
bench2.start(ticks=100)
hash1 = bench1.get_simulation_hash()
hash2 = bench2.get_simulation_hash()
assert hash1 == hash2, f"Simulation hashes differ: {hash1} != {hash2}"
def test_simulation_benchmark():
bench = HeadlessSimulationBenchmark(lambda seed: _setup_world(seed), random_seed=RANDOM_SEED+1)
tick_count = 100
bench.start(ticks=tick_count)
summary = bench.get_summary()
print(f"{tick_count} ticks took {summary.get('ticks_elapsed_time', 0):.4f} seconds, TPS avg: {summary['tps_avg']:.2f}, stddev: {summary['tps_stddev']:.2f}")
assert summary['tps_avg'] > 0, "Average TPS should be greater than zero"
assert summary['ticks_elapsed_time'] > 0, "Elapsed time should be greater than zero"

View File

@ -1,218 +0,0 @@
"""Tests for headless simulation engine."""
import time
from unittest.mock import Mock, patch
from engines.headless_engine import HeadlessSimulationEngine, HeadlessConfig
from config.simulation_config import SimulationConfig
class TestHeadlessConfig:
"""Test headless configuration."""
def test_custom_values(self):
"""Test custom configuration values."""
sim_config = SimulationConfig(initial_cells=50, default_tps=120.0)
config = HeadlessConfig(
simulation=sim_config,
max_ticks=10000,
max_duration=300.0,
output_dir="custom_output",
enable_metrics=False,
enable_entities=True,
enable_evolution=False,
metrics_interval=50,
entities_interval=500,
evolution_interval=2000,
output_formats=['json', 'csv'],
real_time=True
)
assert config.simulation == sim_config
assert config.max_ticks == 10000
assert config.max_duration == 300.0
assert config.output_dir == "custom_output"
assert config.enable_metrics == False
assert config.enable_entities == True
assert config.enable_evolution == False
assert config.metrics_interval == 50
assert config.entities_interval == 500
assert config.evolution_interval == 2000
assert config.output_formats == ['json', 'csv']
assert config.real_time == True
class TestHeadlessSimulationEngine:
"""Test headless simulation engine."""
def test_initialization(self):
"""Test engine initialization."""
sim_config = SimulationConfig(initial_cells=5, initial_food=10)
config = HeadlessConfig(
simulation=sim_config,
max_ticks=1000,
output_formats=['json']
)
engine = HeadlessSimulationEngine(config)
assert engine.config == config
assert engine.event_bus is not None
assert engine.simulation_core is not None
assert engine.file_writer is not None
assert engine.formatters is not None
assert engine.collectors is not None
assert engine.running == False
assert engine.start_time is None
def test_collectors_creation(self):
"""Test collectors are created based on configuration."""
sim_config = SimulationConfig()
config = HeadlessConfig(
simulation=sim_config,
enable_metrics=True,
enable_entities=True,
enable_evolution=False,
metrics_interval=50,
entities_interval=200
)
engine = HeadlessSimulationEngine(config)
assert 'metrics' in engine.collectors
assert 'entities' in engine.collectors
assert 'evolution' not in engine.collectors
assert engine.collectors['metrics'].collection_interval == 50
assert engine.collectors['entities'].collection_interval == 200
def test_formatters_creation(self):
"""Test formatters are created based on configuration."""
sim_config = SimulationConfig()
config = HeadlessConfig(
simulation=sim_config,
output_formats=['json', 'csv']
)
engine = HeadlessSimulationEngine(config)
assert 'json' in engine.formatters
assert 'csv' in engine.formatters
def test_should_terminate_max_ticks(self):
"""Test termination condition for max ticks."""
sim_config = SimulationConfig()
config = HeadlessConfig(simulation=sim_config, max_ticks=100)
engine = HeadlessSimulationEngine(config)
engine.running = True
engine.start_time = time.time()
# Mock the simulation core to report tick count
engine.simulation_core.state.total_ticks = 99
assert engine._should_terminate() == False
engine.simulation_core.state.total_ticks = 100
assert engine._should_terminate() == True
def test_should_terminate_max_duration(self):
"""Test termination condition for max duration."""
sim_config = SimulationConfig()
config = HeadlessConfig(simulation=sim_config, max_duration=1.0)
engine = HeadlessSimulationEngine(config)
engine.running = True
engine.start_time = time.time()
# Should not terminate immediately
assert engine._should_terminate() == False
# Mock time passage
with patch('time.time', return_value=engine.start_time + 1.5):
assert engine._should_terminate() == True
def test_should_terminate_no_limits(self):
"""Test no termination conditions."""
sim_config = SimulationConfig()
config = HeadlessConfig(simulation=sim_config)
engine = HeadlessSimulationEngine(config)
engine.running = True
engine.start_time = time.time()
# Should never terminate without limits
assert engine._should_terminate() == False
def test_collect_data(self):
"""Test data collection from collectors."""
sim_config = SimulationConfig()
config = HeadlessConfig(
simulation=sim_config,
enable_metrics=True,
enable_entities=False,
enable_evolution=False
)
engine = HeadlessSimulationEngine(config)
# Mock simulation core's get_world_state method
engine.simulation_core.get_world_state = Mock(return_value={
'tick_count': 1000,
'actual_tps': 60.0,
'entity_counts': {'total': 25}
})
# Mock collector
mock_collector = Mock()
mock_collector.update.return_value = [
{'tick_count': 1000, 'actual_tps': 60.0, 'collection_type': 'metrics'}
]
engine.collectors['metrics'] = mock_collector
engine._collect_data()
# Verify collector was called
mock_collector.update.assert_called_once()
# Verify data was collected
assert len(engine.batch_data['metrics']) == 1
assert engine.batch_data['metrics'][0]['tick_count'] == 1000
def test_get_real_time_status(self):
"""Test real-time status reporting."""
sim_config = SimulationConfig()
config = HeadlessConfig(simulation=sim_config)
engine = HeadlessSimulationEngine(config)
engine.running = True
engine.start_time = time.time() - 5.0
# Mock simulation state
engine.simulation_core.state.total_ticks = 300
engine.simulation_core.state.actual_tps = 60.0
engine.simulation_core.get_world_state = Mock(return_value={
'tick_count': 300,
'actual_tps': 60.0,
'entity_counts': {'total': 50}
})
status = engine.get_real_time_status()
assert status['running'] == True
assert status['ticks'] == 300
assert status['tps'] == 60.0
assert status['duration'] > 4.0 # Approximately 5 seconds
assert status['world_state']['tick_count'] == 300
def test_signal_handler(self):
"""Test signal handling for graceful shutdown."""
sim_config = SimulationConfig()
config = HeadlessConfig(simulation=sim_config)
engine = HeadlessSimulationEngine(config)
engine.running = True
# Simulate signal handler
engine._signal_handler(2, None) # SIGINT
assert engine.running == False

View File

@ -1,195 +0,0 @@
"""Tests for output collection system."""
import json
import tempfile
import os
from unittest.mock import Mock
from output.collectors.metrics_collector import MetricsCollector
from output.collectors.entity_collector import EntityCollector
from output.formatters.json_formatter import JSONFormatter
from output.formatters.csv_formatter import CSVFormatter
from output.writers.file_writer import FileWriter
class TestMetricsCollector:
"""Test metrics collector functionality."""
def test_collect_data(self):
"""Test metrics data collection."""
collector = MetricsCollector(collection_interval=100)
# Create mock simulation core
mock_sim_core = Mock()
mock_sim_core.get_world_state.return_value = {
'tick_count': 1500,
'actual_tps': 58.5,
'target_tps': 60.0,
'speed_multiplier': 1.0,
'is_paused': False,
'sprint_mode': False,
'world_buffer': 1,
'entity_counts': {
'total': 25,
'cells': 20,
'food': 5
}
}
# Mock timing
mock_sim_core.timing = Mock()
mock_sim_core.timing.last_tick_time = 1234567890.5
data = collector.collect(mock_sim_core)
assert data['tick_count'] == 1500
assert data['actual_tps'] == 58.5
assert data['target_tps'] == 60.0
assert data['speed_multiplier'] == 1.0
assert data['is_paused'] == False
assert data['sprint_mode'] == False
assert data['world_buffer'] == 1
assert data['entity_counts'] == {
'total': 25,
'cells': 20,
'food': 5
}
assert data['collection_type'] == 'metrics'
assert data['timestamp'] == 1234567890.5
class TestEntityCollector:
"""Test entity collector functionality."""
def test_collect_cells_only(self):
"""Test collecting only cell entities."""
collector = EntityCollector(
collection_interval=1000,
include_cells=True,
include_food=False
)
# Create mock simulation core with entity states
mock_sim_core = Mock()
mock_sim_core.get_world_state.return_value = {
'tick_count': 1000
}
mock_sim_core.get_entity_states.return_value = [
{
'id': 1,
'type': 'cell',
'position': {'x': 10, 'y': 20},
'energy': 75.5,
'age': 150,
'generation': 3,
'neural_network': {'layer_sizes': [4, 6, 2]}
},
{
'id': 2,
'type': 'food',
'position': {'x': 30, 'y': 40},
'decay': 50,
'max_decay': 100
}
]
data = collector.collect(mock_sim_core)
assert len(data['entities']) == 1 # Only cell included
assert data['entities'][0]['type'] == 'cell'
assert data['entities'][0]['id'] == 1
assert data['collection_type'] == 'entities'
class TestJSONFormatter:
"""Test JSON formatter functionality."""
def test_format_data(self):
"""Test JSON data formatting."""
formatter = JSONFormatter()
test_data = {
'tick_count': 1000,
'actual_tps': 58.5,
'entity_counts': {
'cells': 20,
'food': 5
}
}
formatted = formatter.format(test_data)
assert isinstance(formatted, str)
# Verify it's valid JSON
parsed = json.loads(formatted)
assert parsed['tick_count'] == 1000
assert parsed['actual_tps'] == 58.5
assert parsed['entity_counts']['cells'] == 20
class TestCSVFormatter:
"""Test CSV formatter functionality."""
def test_format_simple_data(self):
"""Test CSV formatting for simple data."""
formatter = CSVFormatter()
test_data = {
'tick_count': 1000,
'actual_tps': 58.5,
'is_paused': False
}
formatted = formatter.format(test_data)
assert isinstance(formatted, str)
lines = formatted.strip().split('\n')
# Should have header and one data row
assert len(lines) == 2
# Check header
header = lines[0]
assert 'tick_count' in header
assert 'actual_tps' in header
assert 'is_paused' in header
# Check data row
data_row = lines[1]
assert '1000' in data_row
assert '58.5' in data_row
def test_get_file_extension(self):
"""Test file extension."""
formatter = CSVFormatter()
assert formatter.get_file_extension() == 'csv'
class TestFileWriter:
"""Test file writer functionality."""
def test_write_data(self):
"""Test writing data to file."""
# Create temporary directory
with tempfile.TemporaryDirectory() as temp_dir:
writer = FileWriter(temp_dir)
# Should be ready now
assert writer.is_ready() == True
# Write test data
test_data = '{"tick_count": 1000, "actual_tps": 58.5}'
filename = "test_data.json"
writer.write(test_data, filename)
# Verify file was created
file_path = os.path.join(temp_dir, filename)
assert os.path.exists(file_path)
# Verify file content
with open(file_path, 'r') as f:
content = f.read()
assert content == test_data

View File

@ -1,12 +1,10 @@
import pytest import pytest
from world.world import World, Position, BaseEntity, Rotation from world.world import World, Position, BaseEntity
class DummyEntity(BaseEntity): class DummyEntity(BaseEntity):
def __init__(self, position, rotation=None): def __init__(self, position):
if rotation is None: super().__init__(position)
rotation = Rotation(angle=0)
super().__init__(position, rotation)
self.ticked = False self.ticked = False
self.rendered = False self.rendered = False
@ -85,6 +83,9 @@ def test_tick_all_calls_tick(world):
def test_add_object_out_of_bounds(world): def test_add_object_out_of_bounds(world):
entity = DummyEntity(Position(x=1000, y=1000)) entity = DummyEntity(Position(x=1000, y=1000))
world.add_object(entity) world.add_object(entity)
entity = world.get_objects()[0] entity = world.get_objects()[0]
assert entity.position.x == 49 and entity.position.y == 49 assert entity.position.x == 49 and entity.position.y == 49

611
ui/hud.py
View File

@ -6,562 +6,18 @@ import pygame_gui
from config.constants import * from config.constants import *
from world.base.brain import CellBrain, FlexibleNeuralNetwork from world.base.brain import CellBrain, FlexibleNeuralNetwork
from world.objects import DefaultCell from world.objects import DefaultCell
from pygame_gui.elements import UIPanel, UIButton, UITextEntryLine, UILabel
from ui.tree_widget import TreeWidget
import math import math
# Custom HUD colors (preserving existing functionality)
DARK_GRAY = (40, 40, 40)
DARKER_GRAY = (25, 25, 25)
def create_panel_style(manager: pygame_gui.UIManager) -> dict:
"""Create unified styling dictionary for panels."""
return {
'panel_background': PANEL_BACKGROUND_COLOR,
'border_color': PANEL_BORDER_COLOR,
'text_color': PANEL_TEXT_COLOR,
'internal_padding': PANEL_INTERNAL_PADDING,
'border_width': PANEL_BORDER_WIDTH
}
def render_panel_divider(surface: pygame.Surface, rect: pygame.Rect):
"""Render a thin divider line between panels."""
pygame.draw.rect(surface, PANEL_BORDER_COLOR, rect, PANEL_DIVIDER_WIDTH)
# Panel visibility constants
SHOW_CONTROL_BAR = True
SHOW_INSPECTOR_PANEL = True
SHOW_PROPERTIES_PANEL = True
SHOW_CONSOLE_PANEL = False
class HUD: class HUD:
def __init__(self, ui_manager, screen_width=SCREEN_WIDTH, screen_height=SCREEN_HEIGHT): def __init__(self, ui_manager, screen_width=SCREEN_WIDTH, screen_height=SCREEN_HEIGHT):
self.font = pygame.font.Font("freesansbold.ttf", FONT_SIZE) self.font = pygame.font.Font("freesansbold.ttf", FONT_SIZE)
self.legend_font = pygame.font.Font("freesansbold.ttf", LEGEND_FONT_SIZE) self.legend_font = pygame.font.Font("freesansbold.ttf", LEGEND_FONT_SIZE)
self.manager = ui_manager
self.screen_width = screen_width self.screen_width = screen_width
self.screen_height = screen_height self.screen_height = screen_height
# Panel size defaults self.manager = ui_manager
self.control_bar_height = 48
self.inspector_width = 260
self.properties_width = 320
self.console_height = 120
self.splitter_thickness = 6
self.dragging_splitter = None
# Simulation control elements
self.play_pause_button = None
self.step_button = None
self.sprint_button = None
self.speed_buttons = {}
self.custom_tps_entry = None
self.tps_label = None
# Tree widget for inspector
self.tree_widget = None
self.world = None # Will be set when world is available
self._last_tree_selection = None # Track last selection to avoid unnecessary updates
# Initialize unified panel styling
self.panel_style = create_panel_style(self.manager)
self._create_panels()
self._create_simulation_controls()
def _create_panels(self):
self.panels = []
# Top control bar - full width, positioned at top
if SHOW_CONTROL_BAR:
self.control_bar = UIPanel(
relative_rect=pygame.Rect(0, 0, self.screen_width, self.control_bar_height),
manager=self.manager,
object_id="#control_bar",
)
self.panels.append(self.control_bar)
else:
self.control_bar = None
# Calculate vertical position for side panels (edge-to-edge with control bar)
side_panel_top = self.control_bar_height if SHOW_CONTROL_BAR else 0
side_panel_height = self.screen_height - side_panel_top
# Left inspector panel - edge-to-edge with control bar, no gap
if SHOW_INSPECTOR_PANEL:
self.inspector_panel = UIPanel(
relative_rect=pygame.Rect(
0, side_panel_top, # Start right at control bar edge
self.inspector_width,
side_panel_height # Extend to bottom edge
),
manager=self.manager,
object_id="#inspector_panel",
)
self.panels.append(self.inspector_panel)
self.tree_widget = None
else:
self.inspector_panel = None
self.tree_widget = None
# Right properties panel - edge-to-edge with control bar, no gap
if SHOW_PROPERTIES_PANEL:
self.properties_panel = UIPanel(
relative_rect=pygame.Rect(
self.screen_width - self.properties_width, # Precisely at right edge
side_panel_top, # Align with control bar
self.properties_width,
side_panel_height # Extend to bottom edge
),
manager=self.manager,
object_id="#properties_panel",
)
self.panels.append(self.properties_panel)
else:
self.properties_panel = None
# Bottom console panel - edge-to-edge with side panels, no gap
if SHOW_CONSOLE_PANEL:
console_left = self.inspector_width if SHOW_INSPECTOR_PANEL else 0
console_width = self.screen_width - console_left - (self.properties_width if SHOW_PROPERTIES_PANEL else 0)
self.console_panel = UIPanel(
relative_rect=pygame.Rect(
console_left, # Start right at inspector edge
self.screen_height - self.console_height, # Exactly at bottom edge
console_width, # Fill space between side panels
self.console_height
),
manager=self.manager,
object_id="#console_panel",
)
self.panels.append(self.console_panel)
else:
self.console_panel = None
self.dragging_splitter = None
def initialize_tree_widget(self, world):
"""Initialize the tree widget when the world is available."""
self.world = world
if self.inspector_panel and world:
# Create tree widget inside the inspector panel
tree_rect = pygame.Rect(0, 0, self.inspector_width, self.inspector_panel.rect.height)
self.tree_widget = TreeWidget(tree_rect, self.manager, world)
def update_tree_selection(self, selected_objects):
"""Update tree selection based on world selection."""
if self.tree_widget:
# Only update if selection actually changed
current_selection = tuple(selected_objects) # Convert to tuple for comparison
if self._last_tree_selection != current_selection:
self.tree_widget.select_entities(selected_objects)
self._last_tree_selection = current_selection
def render_panel_backgrounds(self, screen: pygame.Surface):
"""Render panel backgrounds with consistent colors before UI elements."""
# Render control bar background to match inspector panel
if SHOW_CONTROL_BAR and self.control_bar:
control_bg_rect = pygame.Rect(
0, 0,
self.screen_width,
self.control_bar_height
)
pygame.draw.rect(screen, PANEL_BACKGROUND_COLOR, control_bg_rect)
# Render console panel background to match inspector panel
if SHOW_CONSOLE_PANEL and self.console_panel:
console_bg_rect = pygame.Rect(
self.console_panel.rect.x,
self.console_panel.rect.y,
self.console_panel.rect.width,
self.console_panel.rect.height
)
pygame.draw.rect(screen, PANEL_BACKGROUND_COLOR, console_bg_rect)
# Render properties panel background to match inspector panel
if SHOW_PROPERTIES_PANEL and self.properties_panel:
properties_bg_rect = pygame.Rect(
self.properties_panel.rect.x,
self.properties_panel.rect.y,
self.properties_panel.rect.width,
self.properties_panel.rect.height
)
pygame.draw.rect(screen, PANEL_BACKGROUND_COLOR, properties_bg_rect)
def render_panel_dividers(self, screen: pygame.Surface):
"""Render consistent panel sliders with proper rendering order."""
# Render consistent panel sliders
self.draw_splitters(screen)
def _create_simulation_controls(self):
"""Create simulation control buttons in the control bar."""
if not self.control_bar:
return
# Button layout constants (using standardized spacing)
button_width = 40
button_height = 32
button_spacing = PANEL_TIGHT_SPACING # Using standardized tight spacing
start_x = PANEL_INTERNAL_PADDING # Using standardized internal padding
start_y = PANEL_TIGHT_SPACING # Using standardized tight spacing
# Play/Pause button
self.play_pause_button = UIButton(
relative_rect=pygame.Rect(start_x, start_y, button_width, button_height),
text='>',
manager=self.manager,
container=self.control_bar,
object_id="#play_pause_button"
)
# Step forward button
step_x = start_x + button_width + button_spacing
self.step_button = UIButton(
relative_rect=pygame.Rect(step_x, start_y, button_width, button_height),
text='>>',
manager=self.manager,
container=self.control_bar,
object_id="#step_button"
)
# Sprint button
sprint_x = step_x + button_width + button_spacing + 5 # Extra spacing
self.sprint_button = UIButton(
relative_rect=pygame.Rect(sprint_x, start_y, button_width + 10, button_height),
text='>>|',
manager=self.manager,
container=self.control_bar,
object_id="#sprint_button"
)
# Speed control buttons
speed_labels = ["0.5x", "1x", "2x", "4x", "8x"]
speed_multipliers = [0.5, 1.0, 2.0, 4.0, 8.0]
speed_x = sprint_x + button_width + 10 + button_spacing + 10 # Extra spacing
for i, (label, multiplier) in enumerate(zip(speed_labels, speed_multipliers)):
button_x = speed_x + i * (button_width - 5 + button_spacing)
button = UIButton(
relative_rect=pygame.Rect(button_x, start_y, button_width - 5, button_height),
text=label,
manager=self.manager,
container=self.control_bar,
object_id=f"#speed_{int(multiplier*10)}x_button"
)
self.speed_buttons[multiplier] = button
# Custom TPS input
tps_x = speed_x + len(speed_labels) * (button_width - 5 + button_spacing) + button_spacing
self.custom_tps_entry = UITextEntryLine(
relative_rect=pygame.Rect(tps_x, start_y + 2, 50, button_height - 4),
manager=self.manager,
container=self.control_bar,
object_id="#custom_tps_entry"
)
self.custom_tps_entry.set_text(str(DEFAULT_TPS))
# TPS display label
tps_label_x = tps_x + 55
self.tps_label = UILabel(
relative_rect=pygame.Rect(tps_label_x, start_y + 4, 80, button_height - 8),
text='TPS: 40',
manager=self.manager,
container=self.control_bar,
object_id="#tps_label"
)
def get_viewport_rect(self):
# Returns the rect for the simulation viewport
inspector_width = self.inspector_width if SHOW_INSPECTOR_PANEL and self.inspector_panel else 0
control_bar_height = self.control_bar_height if SHOW_CONTROL_BAR and self.control_bar else 0
properties_width = self.properties_width if SHOW_PROPERTIES_PANEL and self.properties_panel else 0
console_height = self.console_height if SHOW_CONSOLE_PANEL and self.console_panel else 0
x = inspector_width
y = control_bar_height
w = self.screen_width - inspector_width - properties_width
h = self.screen_height - control_bar_height - console_height
return pygame.Rect(x, y, w, h)
def update_layout(self, window_width, window_height):
self.screen_width = window_width
self.screen_height = window_height
control_bar_height = self.control_bar_height if SHOW_CONTROL_BAR and self.control_bar else 0
# Control bar (top)
if self.control_bar:
self.control_bar.set_relative_position((0, 0))
self.control_bar.set_dimensions((self.screen_width, self.control_bar_height))
# Inspector panel (left)
if self.inspector_panel:
self.inspector_panel.set_relative_position((0, control_bar_height))
self.inspector_panel.set_dimensions((self.inspector_width, self.screen_height - control_bar_height))
# Update tree widget size if it exists
if self.tree_widget:
tree_rect = pygame.Rect(0, 0, self.inspector_width, self.inspector_panel.rect.height)
self.tree_widget.rect = tree_rect
# Properties panel (right)
if self.properties_panel:
self.properties_panel.set_relative_position(
(self.screen_width - self.properties_width, control_bar_height))
self.properties_panel.set_dimensions((self.properties_width, self.screen_height - control_bar_height))
# Console panel (bottom, spans between inspector and properties)
if self.console_panel:
inspector_width = self.inspector_width if SHOW_INSPECTOR_PANEL and self.inspector_panel else 0
properties_width = self.properties_width if SHOW_PROPERTIES_PANEL and self.properties_panel else 0
self.console_panel.set_relative_position((inspector_width, self.screen_height - self.console_height))
self.console_panel.set_dimensions(
(self.screen_width - inspector_width - properties_width, self.console_height))
# Recreate simulation controls after layout change
if hasattr(self, 'play_pause_button'):
self._destroy_simulation_controls()
self._create_simulation_controls()
def _destroy_simulation_controls(self):
"""Destroy simulation control elements."""
if self.play_pause_button:
self.play_pause_button.kill()
if self.step_button:
self.step_button.kill()
if self.sprint_button:
self.sprint_button.kill()
for button in self.speed_buttons.values():
button.kill()
if self.custom_tps_entry:
self.custom_tps_entry.kill()
if self.tps_label:
self.tps_label.kill()
self.play_pause_button = None
self.step_button = None
self.sprint_button = None
self.speed_buttons = {}
self.custom_tps_entry = None
self.tps_label = None
def update_simulation_controls(self, simulation_core):
"""Update simulation control button states and displays based on simulation core state."""
if not self.play_pause_button:
return
timing_state = simulation_core.timing.state
# Update play/pause button
if timing_state.is_paused:
self.play_pause_button.set_text('>')
else:
self.play_pause_button.set_text('||')
# Update speed button highlights
speed_presets = {0.5: "0.5x", 1.0: "1x", 2.0: "2x", 4.0: "4x", 8.0: "8x"}
for multiplier, button in self.speed_buttons.items():
if (timing_state.speed_multiplier == multiplier and
not timing_state.is_paused and
not timing_state.sprint_mode):
# Active speed button - make text more prominent
button.set_text(f"[{speed_presets[multiplier]}]")
else:
# Normal button appearance
button.set_text(speed_presets[multiplier])
# Update sprint button appearance
if timing_state.sprint_mode:
self.sprint_button.set_text('')
else:
self.sprint_button.set_text('')
# Update TPS display
if self.tps_label:
if timing_state.sprint_mode:
self.tps_label.set_text(f"TPS: {timing_state.tps:.0f} (Sprint)")
else:
self.tps_label.set_text(f"TPS: {timing_state.tps:.0f}")
# Update custom TPS entry
if self.custom_tps_entry and not self.custom_tps_entry.is_focused:
self.custom_tps_entry.set_text(str(int(timing_state.tps)))
def process_event(self, event):
# Check for splitter dragging events first (don't let tree widget block them)
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
mx, my = event.pos
# Check if mouse is on a splitter
if abs(mx - self.inspector_width) < self.splitter_thickness:
# Don't handle this event in the tree widget - it's for the splitter
pass
elif self.tree_widget and self.inspector_panel:
# Handle tree widget events
inspector_rect = self.inspector_panel.rect
# The inspector_rect should be in absolute screen coordinates
inspector_abs_x = inspector_rect.x
inspector_abs_y = inspector_rect.y
tree_local_pos = (event.pos[0] - inspector_abs_x, event.pos[1] - inspector_abs_y)
if (0 <= tree_local_pos[0] < self.tree_widget.rect.width and
0 <= tree_local_pos[1] < self.tree_widget.rect.height):
event.pos = tree_local_pos
if self.tree_widget.handle_event(event):
selected_entities = self.tree_widget.get_selected_entities()
return 'tree_selection_changed', selected_entities
elif self.tree_widget and self.inspector_panel:
# Handle other tree widget events (except MOUSEWHEEL - handled by InputHandler)
if event.type == pygame.MOUSEMOTION:
# Only handle if not dragging splitter
if not self.dragging_splitter and hasattr(event, 'pos'):
# For mouse motion, check if mouse is over tree widget
inspector_rect = self.inspector_panel.rect
# Calculate absolute screen position (need to add control bar height)
inspector_abs_x = inspector_rect.x
inspector_abs_y = inspector_rect.y
tree_local_pos = (event.pos[0] - inspector_abs_x, event.pos[1] - inspector_abs_y)
if (0 <= tree_local_pos[0] < self.tree_widget.rect.width and
0 <= tree_local_pos[1] < self.tree_widget.rect.height):
event.pos = tree_local_pos
# This is the handle_event call that is being run even when the mouse is not over the tree widget
if self.tree_widget.handle_event(event):
selected_entities = self.tree_widget.get_selected_entities()
return 'tree_selection_changed', selected_entities
else:
# Mouse left the tree widget area, clear hover
self.tree_widget.clear_hover()
else:
# Handle specific mouse events in tree widget (but not wheel - handled by InputHandler)
if event.type in (pygame.MOUSEBUTTONDOWN, pygame.MOUSEBUTTONUP):
if self.tree_widget.handle_event(event):
selected_entities = self.tree_widget.get_selected_entities()
return 'tree_selection_changed', selected_entities
# Handle simulation control button events using ID matching
if event.type == pygame_gui.UI_BUTTON_START_PRESS:
object_id = str(event.ui_object_id)
if '#play_pause_button' in object_id:
return 'toggle_pause'
elif '#step_button' in object_id:
return 'step_forward'
elif '#sprint_button' in object_id:
return 'toggle_sprint'
elif '#speed_5x_button' in object_id: # 0.5x button
return 'set_speed', 0.5
elif '#speed_10x_button' in object_id: # 1x button
return 'set_speed', 1.0
elif '#speed_20x_button' in object_id: # 2x button
return 'set_speed', 2.0
elif '#speed_40x_button' in object_id: # 4x button
return 'set_speed', 4.0
elif '#speed_80x_button' in object_id: # 8x button
return 'set_speed', 8.0
elif event.type == pygame_gui.UI_TEXT_ENTRY_FINISHED:
object_id = str(event.ui_object_id)
print(f"Text entry finished: {object_id}, text: {event.text}")
if '#custom_tps_entry' in object_id:
try:
tps = float(event.text)
return 'set_custom_tps', tps
except ValueError:
# Invalid TPS value, reset to current TPS
return ('reset_tps_display',)
elif event.type == pygame_gui.UI_TEXT_ENTRY_CHANGED:
object_id = str(event.ui_object_id)
if '#custom_tps_entry' in object_id:
print(f"Text entry changed: {object_id}, text: {event.text}")
# Handle splitter dragging for resizing panels
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
mx, my = event.pos
# Check if mouse is on a splitter (left/right/bottom)
if abs(mx - self.inspector_width) < self.splitter_thickness and self.control_bar_height < my < self.screen_height - self.console_height:
self.dragging_splitter = "inspector"
elif abs(mx - (self.screen_width - self.properties_width)) < self.splitter_thickness and self.control_bar_height < my < self.screen_height - self.console_height:
self.dragging_splitter = "properties"
elif abs(my - (self.screen_height - self.console_height)) < self.splitter_thickness and self.inspector_width < mx < self.screen_width - self.properties_width:
self.dragging_splitter = "console"
self.update_layout(self.screen_width, self.screen_height)
elif event.type == pygame.MOUSEBUTTONUP and event.button == 1:
if self.dragging_splitter is not None:
self.dragging_splitter = None
return 'viewport_resized'
elif event.type == pygame.MOUSEMOTION and self.dragging_splitter:
mx, my = event.pos
if self.dragging_splitter == "inspector":
self.inspector_width = max(100, min(mx, self.screen_width - self.properties_width - 100))
elif self.dragging_splitter == "properties":
self.properties_width = max(100, min(self.screen_width - mx, self.screen_width - self.inspector_width - 100))
elif self.dragging_splitter == "console":
self.console_height = max(60, min(self.screen_height - my, self.screen_height - self.control_bar_height - 60))
self.update_layout(self.screen_width, self.screen_height)
def draw_splitters(self, screen):
# Draw draggable splitters for visual feedback with consistent styling
indicator_color = PANEL_ICON_COLOR # Use standardized icon color for consistency
indicator_size = 6 # Length of indicator line
indicator_gap = 4 # Gap between indicator lines
indicator_count = 3 # Number of indicator lines
inspector_width = self.inspector_width if SHOW_INSPECTOR_PANEL and self.inspector_panel else 0
properties_width = self.properties_width if SHOW_PROPERTIES_PANEL and self.properties_panel else 0
console_height = self.console_height if SHOW_CONSOLE_PANEL and self.console_panel else 0
control_bar_height = self.control_bar_height if SHOW_CONTROL_BAR and self.control_bar else 0
# Vertical splitter (inspector/properties)
# Inspector/properties only if wide enough
if inspector_width > 0:
x = inspector_width - 2
y1 = control_bar_height
y2 = self.screen_height - console_height
# Draw indicator (horizontal lines) in the middle
mid_y = (y1 + y2) // 2
for i in range(indicator_count):
offset = (i - 1) * (indicator_gap + 1)
pygame.draw.line(
screen, indicator_color,
(x - indicator_size // 2, mid_y + offset),
(x + indicator_size // 2, mid_y + offset),
2
)
if properties_width > 0:
x = self.screen_width - properties_width + 2
y1 = control_bar_height
y2 = self.screen_height - console_height
mid_y = (y1 + y2) // 2
for i in range(indicator_count):
offset = (i - 1) * (indicator_gap + 1)
pygame.draw.line(
screen, indicator_color,
(x - indicator_size // 2, mid_y + offset),
(x + indicator_size // 2, mid_y + offset),
2
)
# Horizontal splitter (console)
if console_height > 0:
y = self.screen_height - console_height + 2
x1 = inspector_width
x2 = self.screen_width - properties_width
mid_x = (x1 + x2) // 2
for i in range(indicator_count):
offset = (i - 1) * (indicator_gap + 1)
pygame.draw.line(
screen, indicator_color,
(mid_x + offset, y - indicator_size // 2),
(mid_x + offset, y + indicator_size // 2),
2
)
def render_mouse_position(self, screen, camera, sim_view_rect): def render_mouse_position(self, screen, camera, sim_view_rect):
"""Render mouse position in top left.""" """Render mouse position in top left."""
@ -584,8 +40,7 @@ class HUD:
def render_tps(self, screen, actual_tps): def render_tps(self, screen, actual_tps):
"""Render TPS in bottom right.""" """Render TPS in bottom right."""
display_tps = round(actual_tps) # Round to nearest whole number tps_text = self.font.render(f"TPS: {actual_tps}", True, WHITE)
tps_text = self.font.render(f"TPS: {display_tps}", True, WHITE)
tps_rect = tps_text.get_rect() tps_rect = tps_text.get_rect()
tps_rect.bottomright = (self.screen_width - HUD_MARGIN, self.screen_height - HUD_MARGIN) tps_rect.bottomright = (self.screen_width - HUD_MARGIN, self.screen_height - HUD_MARGIN)
screen.blit(tps_text, tps_rect) screen.blit(tps_text, tps_rect)
@ -690,7 +145,6 @@ class HUD:
VIZ_WIDTH = 280 # Width of the neural network visualization area VIZ_WIDTH = 280 # Width of the neural network visualization area
VIZ_HEIGHT = 300 # Height of the neural network visualization area VIZ_HEIGHT = 300 # Height of the neural network visualization area
VIZ_RIGHT_MARGIN = VIZ_WIDTH + 50 # Distance from right edge of screen to visualization VIZ_RIGHT_MARGIN = VIZ_WIDTH + 50 # Distance from right edge of screen to visualization
VIZ_BOTTOM_MARGIN = 50 # Distance from the bottom of the screen
# Background styling constants # Background styling constants
BACKGROUND_PADDING = 30 # Padding around the visualization background BACKGROUND_PADDING = 30 # Padding around the visualization background
@ -742,10 +196,6 @@ class HUD:
TOOLTIP_MARGIN = 10 TOOLTIP_MARGIN = 10
TOOLTIP_LINE_SPACING = 0 # No extra spacing between lines TOOLTIP_LINE_SPACING = 0 # No extra spacing between lines
if self.properties_width < VIZ_RIGHT_MARGIN + 50:
self.properties_width = VIZ_RIGHT_MARGIN + 50 # Ensure properties panel is wide enough for tooltip
self.update_layout(self.screen_width, self.screen_height) # Immediately update layout
if not hasattr(cell, 'behavioral_model'): if not hasattr(cell, 'behavioral_model'):
return return
@ -756,9 +206,9 @@ class HUD:
network: FlexibleNeuralNetwork = cell_brain.neural_network network: FlexibleNeuralNetwork = cell_brain.neural_network
# Calculate visualization position (bottom right) # Calculate visualization position
viz_x = self.screen_width - VIZ_RIGHT_MARGIN # Right side of screen viz_x = self.screen_width - VIZ_RIGHT_MARGIN # Right side of screen
viz_y = self.screen_height - VIZ_HEIGHT - VIZ_BOTTOM_MARGIN # Above the bottom margin viz_y = (self.screen_height // 2) - (VIZ_HEIGHT // 2) # Centered vertically
layer_spacing = VIZ_WIDTH // max(1, len(network.layers) - 1) if len(network.layers) > 1 else VIZ_WIDTH layer_spacing = VIZ_WIDTH // max(1, len(network.layers) - 1) if len(network.layers) > 1 else VIZ_WIDTH
@ -768,8 +218,6 @@ class HUD:
pygame.draw.rect(screen, BACKGROUND_COLOR, background_rect) pygame.draw.rect(screen, BACKGROUND_COLOR, background_rect)
pygame.draw.rect(screen, WHITE, background_rect, BACKGROUND_BORDER_WIDTH) pygame.draw.rect(screen, WHITE, background_rect, BACKGROUND_BORDER_WIDTH)
info = network.get_structure_info()
# Title # Title
title_text = self.font.render("Neural Network", True, WHITE) title_text = self.font.render("Neural Network", True, WHITE)
title_rect = title_text.get_rect() title_rect = title_text.get_rect()
@ -777,13 +225,6 @@ class HUD:
title_rect.top = viz_y - TITLE_TOP_MARGIN title_rect.top = viz_y - TITLE_TOP_MARGIN
screen.blit(title_text, title_rect) screen.blit(title_text, title_rect)
# Render network cost under the title
cost_text = self.font.render(f"Cost: {info['network_cost']}", True, WHITE)
cost_rect = cost_text.get_rect()
cost_rect.centerx = title_rect.centerx
cost_rect.top = title_rect.bottom + 4 # Small gap below the title
screen.blit(cost_text, cost_rect)
# Get current activations by running a forward pass with current inputs # Get current activations by running a forward pass with current inputs
input_values = [cell_brain.inputs[key] for key in cell_brain.input_keys] input_values = [cell_brain.inputs[key] for key in cell_brain.input_keys]
@ -942,6 +383,22 @@ class HUD:
label_rect.bottom = viz_y + VIZ_HEIGHT + LAYER_LABEL_BOTTOM_MARGIN label_rect.bottom = viz_y + VIZ_HEIGHT + LAYER_LABEL_BOTTOM_MARGIN
screen.blit(label_text, label_rect) screen.blit(label_text, label_rect)
# Draw network info
info = network.get_structure_info()
info_lines = [
f"Layers: {info['total_layers']}",
f"Neurons: {info['total_neurons']}",
f"Connections: {info['total_connections']}",
f"Network Cost: {info['network_cost']}",
]
for i, line in enumerate(info_lines):
info_text = self.legend_font.render(line, True, WHITE)
info_rect = info_text.get_rect()
info_rect.left = viz_x
info_rect.top = viz_y + VIZ_HEIGHT + INFO_TEXT_TOP_MARGIN + i * INFO_TEXT_LINE_SPACING
screen.blit(info_text, info_rect)
# --- Tooltip logic for neuron hover --- # --- Tooltip logic for neuron hover ---
mouse_x, mouse_y = pygame.mouse.get_pos() mouse_x, mouse_y = pygame.mouse.get_pos()
tooltip_text = None tooltip_text = None
@ -1017,36 +474,22 @@ class HUD:
screen.blit(surf, (tooltip_rect.left + TOOLTIP_PADDING_X, y)) screen.blit(surf, (tooltip_rect.left + TOOLTIP_PADDING_X, y))
y += surf.get_height() + TOOLTIP_LINE_SPACING y += surf.get_height() + TOOLTIP_LINE_SPACING
def render_sprint_debug(self, screen, actual_tps, total_ticks, cell_count=None): def render_sprint_debug(self, screen, actual_tps, total_ticks):
"""Render sprint debug info: header, TPS, and tick count.""" """Render sprint debug info: header, TPS, and tick count."""
header = self.font.render("Sprinting...", True, (255, 200, 0)) header = self.font.render("Sprinting...", True, (255, 200, 0))
display_tps = round(actual_tps) # Round to nearest whole number tps_text = self.font.render(f"TPS: {actual_tps}", True, (255, 255, 255))
tps_text = self.font.render(f"TPS: {display_tps}", True, (255, 255, 255))
ticks_text = self.font.render(f"Ticks: {total_ticks}", True, (255, 255, 255)) ticks_text = self.font.render(f"Ticks: {total_ticks}", True, (255, 255, 255))
cell_text = self.font.render(f"Cells: {cell_count}" if cell_count is not None else "Cells: N/A", True, (255, 255, 255))
y = self.screen_height // 2 - 80 y = self.screen_height // 2 - 40
header_rect = header.get_rect(center=(self.screen_width // 2, y)) header_rect = header.get_rect(center=(self.screen_width // 2, y))
tps_rect = tps_text.get_rect(center=(self.screen_width // 2, y + 40)) tps_rect = tps_text.get_rect(center=(self.screen_width // 2, y + 40))
ticks_rect = ticks_text.get_rect(center=(self.screen_width // 2, y + 80)) ticks_rect = ticks_text.get_rect(center=(self.screen_width // 2, y + 80))
cell_rect = cell_text.get_rect(center=(self.screen_width // 2, y + 120))
screen.blit(header, header_rect) screen.blit(header, header_rect)
screen.blit(tps_text, tps_rect) screen.blit(tps_text, tps_rect)
screen.blit(ticks_text, ticks_rect) screen.blit(ticks_text, ticks_rect)
screen.blit(cell_text, cell_rect)
def update_tree_widget(self, time_delta): def update_layout(self, window_width, window_height):
"""Update the tree widget.""" """Update HUD layout on window resize."""
if self.tree_widget: self.screen_width = window_width
self.tree_widget.update(time_delta) self.screen_height = window_height
def render_tree_widget(self, screen):
"""Render the tree widget."""
if self.tree_widget and self.inspector_panel:
# Create a surface for the tree widget area
tree_surface = screen.subsurface(self.inspector_panel.rect)
self.tree_widget.draw(tree_surface)
# Render panel dividers for visual consistency
self.render_panel_dividers(screen)

View File

@ -1,270 +0,0 @@
"""
Tree node classes for the hierarchical inspector view.
Provides extensible tree structure for entity inspection.
"""
from abc import ABC, abstractmethod
from typing import List, Optional, Dict, Any
import pygame
from config.constants import PANEL_NODE_HEIGHT, PANEL_INDENTATION
class TreeNode(ABC):
"""Base class for tree nodes in the inspector."""
def __init__(self, label: str, parent: Optional['TreeNode'] = None):
self.label = label
self.parent = parent
self.children: List['TreeNode'] = []
self.is_expanded = False
self.is_selected = False
self.depth = 0 if parent is None else parent.depth + 1
self.rect = pygame.Rect(0, 0, 0, PANEL_NODE_HEIGHT) # Will be updated during layout
def add_child(self, child: 'TreeNode') -> None:
"""Add a child node to this node."""
child.parent = self
child.depth = self.depth + 1
self.children.append(child)
def remove_child(self, child: 'TreeNode') -> None:
"""Remove a child node from this node."""
if child in self.children:
self.children.remove(child)
child.parent = None
def toggle_expand(self) -> None:
"""Toggle the expanded state of this node."""
if self.children:
self.is_expanded = not self.is_expanded
def expand(self) -> None:
"""Expand this node."""
if self.children:
self.is_expanded = True
def collapse(self) -> None:
"""Collapse this node."""
self.is_expanded = False
def get_visible_children(self) -> List['TreeNode']:
"""Get children that are currently visible."""
if not self.is_expanded:
return []
return self.children
def get_all_visible_descendants(self) -> List['TreeNode']:
"""Get all visible descendants of this node."""
visible = []
if self.is_expanded:
for child in self.children:
visible.append(child)
visible.extend(child.get_all_visible_descendants())
return visible
def is_leaf(self) -> bool:
"""Check if this node has no children."""
return len(self.children) == 0
@abstractmethod
def get_display_text(self) -> str:
"""Get the display text for this node."""
pass
@abstractmethod
def can_expand(self) -> bool:
"""Check if this node can be expanded."""
pass
def get_indent(self) -> int:
"""Get the indentation width for this node."""
return self.depth * PANEL_INDENTATION
class SimulationNode(TreeNode):
"""Root node representing the entire simulation."""
def __init__(self, world):
super().__init__("Simulation")
self.world = world
self.is_expanded = True # Always expanded
def get_display_text(self) -> str:
return f"Simulation"
def can_expand(self) -> bool:
return True
def update_entity_counts(self) -> None:
"""Update entity type nodes with current world state."""
entity_types = {}
# Count entities by type
for entity in self.world.get_objects():
entity_type = type(entity).__name__
if entity_type not in entity_types:
entity_types[entity_type] = []
entity_types[entity_type].append(entity)
# Update children to match current entity types
existing_types = {child.label: child for child in self.children}
# Remove types that no longer exist
for type_name in existing_types:
if type_name not in entity_types:
self.remove_child(existing_types[type_name])
# Add or update type nodes
for type_name, entities in entity_types.items():
if type_name in existing_types:
# Update existing node
type_node = existing_types[type_name]
type_node.update_entities(entities)
else:
# Create new type node
type_node = EntityTypeNode(type_name, entities)
self.add_child(type_node)
# Now update children after this node has correct depth
type_node._update_children()
class EntityTypeNode(TreeNode):
"""Node representing a category of entities."""
def __init__(self, entity_type: str, entities: List[Any]):
super().__init__(entity_type)
self.entities = entities
# Don't call _update_children() here - parent will call after adding this node
def _update_children(self) -> None:
"""Update child entity nodes to match current entities."""
existing_ids = {child.entity_id: child for child in self.children}
current_ids = {id(entity): entity for entity in self.entities}
# Remove entities that no longer exist
for entity_id in existing_ids:
if entity_id not in current_ids:
self.remove_child(existing_ids[entity_id])
# Add or update entity nodes
for entity_id, entity in current_ids.items():
if entity_id in existing_ids:
# Update existing node
entity_node = existing_ids[entity_id]
entity_node.entity = entity
else:
# Create new entity node
entity_node = EntityNode(entity)
self.add_child(entity_node)
def update_entities(self, entities: List[Any]) -> None:
"""Update the entities for this type."""
self.entities = entities
self._update_children()
def get_display_text(self) -> str:
count = len(self.entities)
return f"{self.label} ({count})"
def can_expand(self) -> bool:
return len(self.entities) > 0
class EntityNode(TreeNode):
"""Node representing an individual entity."""
def __init__(self, entity: Any):
# Use entity ID as the initial label, will be updated in get_display_text
super().__init__("", None)
self.entity = entity
self.entity_id = id(entity)
self.is_leaf = True # Entity nodes can't have children
def get_display_text(self) -> str:
# Start with just ID, designed for extensibility
return f"Entity {self.entity_id}"
def can_expand(self) -> bool:
return False
def is_leaf(self) -> bool:
return True
def get_entity_info(self) -> Dict[str, Any]:
"""Get entity information for display purposes."""
# Base implementation - can be extended later
info = {
'id': self.entity_id,
'type': type(self.entity).__name__
}
# Add position if available
if hasattr(self.entity, 'position'):
info['position'] = {
'x': getattr(self.entity.position, 'x', 0),
'y': getattr(self.entity.position, 'y', 0)
}
return info
class TreeSelectionManager:
"""Manages selection state for tree nodes."""
def __init__(self):
self.selected_nodes: List[TreeNode] = []
self.last_selected_node: Optional[TreeNode] = None
def select_node(self, node: TreeNode, multi_select: bool = False) -> None:
"""Select a node, optionally with multi-select."""
if not multi_select:
# Clear existing selection
for selected_node in self.selected_nodes:
selected_node.is_selected = False
self.selected_nodes.clear()
# Always select the node (remove toggle behavior for better UX)
if not node.is_selected:
node.is_selected = True
self.selected_nodes.append(node)
self.last_selected_node = node
def select_range(self, from_node: TreeNode, to_node: TreeNode, all_nodes: List[TreeNode]) -> None:
"""Select a range of nodes between from_node and to_node."""
if from_node not in all_nodes or to_node not in all_nodes:
return
# Clear existing selection
for selected_node in self.selected_nodes:
selected_node.is_selected = False
self.selected_nodes.clear()
# Find indices
from_index = all_nodes.index(from_node)
to_index = all_nodes.index(to_node)
# Select range
start = min(from_index, to_index)
end = max(from_index, to_index)
for i in range(start, end + 1):
node = all_nodes[i]
node.is_selected = True
self.selected_nodes.append(node)
self.last_selected_node = to_node
def clear_selection(self) -> None:
"""Clear all selections."""
for node in self.selected_nodes:
node.is_selected = False
self.selected_nodes.clear()
self.last_selected_node = None
def get_selected_entities(self) -> List[Any]:
"""Get the actual entities for selected entity nodes."""
entities = []
for node in self.selected_nodes:
if isinstance(node, EntityNode):
entities.append(node.entity)
return entities

View File

@ -1,597 +0,0 @@
"""Layout management system with constraint-based positioning and event-driven updates."""
from typing import Dict, List, Optional, Tuple, Callable, Any
from dataclasses import dataclass, field
from enum import Enum
import math
import time
from core.event_bus import EventBus, EventType, Event, PanelEvent, LayoutEvent
class LayoutType(Enum):
"""Layout types for containers."""
HORIZONTAL = "horizontal"
VERTICAL = "vertical"
GRID = "grid"
ABSOLUTE = "absolute"
class ResizePolicy(Enum):
"""Resize policies for panels."""
FIXED = "fixed"
FLEXIBLE = "flexible"
PROPORTIONAL = "proportional"
AUTO = "auto"
@dataclass
class LayoutConstraints:
"""Layout constraints for a panel."""
min_width: float = 0
min_height: float = 0
max_width: float = float('inf')
max_height: float = float('inf')
preferred_width: float = 0
preferred_height: float = 0
flex_width: float = 1.0
flex_height: float = 1.0
resize_policy: ResizePolicy = ResizePolicy.FLEXIBLE
def apply_constraints(self, width: float, height: float) -> Tuple[float, float]:
"""Apply size constraints to a given dimension."""
width = max(self.min_width, min(self.max_width, width))
height = max(self.min_height, min(self.max_height, height))
return width, height
@dataclass
class PanelState:
"""State information for a panel."""
panel_id: str
x: float = 0
y: float = 0
width: float = 100
height: float = 100
visible: bool = True
focused: bool = False
constraints: LayoutConstraints = field(default_factory=LayoutConstraints)
parent: Optional[str] = None
children: List[str] = field(default_factory=list)
def get_rect(self) -> Tuple[float, float, float, float]:
"""Get panel rectangle as (x, y, width, height)."""
return self.x, self.y, self.width, self.height
def contains_point(self, x: float, y: float) -> bool:
"""Check if a point is inside the panel."""
return (self.x <= x <= self.x + self.width and
self.y <= y <= self.y + self.height)
@dataclass
class ContainerState:
"""State information for a layout container."""
container_id: str
layout_type: LayoutType = LayoutType.HORIZONTAL
x: float = 0
y: float = 0
width: float = 100
height: float = 100
padding: float = 0
spacing: float = 0
panels: List[str] = field(default_factory=list)
parent: Optional[str] = None
def get_rect(self) -> Tuple[float, float, float, float]:
"""Get container rectangle as (x, y, width, height)."""
return self.x, self.y, self.width, self.height
@dataclass
class AnimationState:
"""Animation state for smooth transitions."""
target_x: float = 0
target_y: float = 0
target_width: float = 0
target_height: float = 0
start_x: float = 0
start_y: float = 0
start_width: float = 0
start_height: float = 0
progress: float = 0
duration: float = 0.2
easing: str = "ease_out_quad"
active: bool = False
class LayoutManager:
"""Enhanced layout manager with constraint-based positioning and animations."""
def __init__(self, event_bus: Optional[EventBus] = None):
self.event_bus = event_bus
self.panels: Dict[str, PanelState] = {}
self.containers: Dict[str, ContainerState] = {}
self.animations: Dict[str, AnimationState] = {}
# Root container
self.root_container = ContainerState(
container_id="root",
layout_type=LayoutType.VERTICAL
)
self.containers["root"] = self.root_container
# Layout preferences
self.animation_enabled = True
self.auto_layout = True
# Performance tracking
self.last_layout_time = 0
self.layout_count = 0
# Subscribe to events if available
if self.event_bus:
self._setup_event_subscriptions()
def _setup_event_subscriptions(self):
"""Setup event subscriptions for layout management."""
self.event_bus.subscribe(EventType.PANEL_RESIZE, self._on_panel_resize)
self.event_bus.subscribe(EventType.PANEL_FOCUS, self._on_panel_focus)
self.event_bus.subscribe(EventType.LAYOUT_CHANGE, self._on_layout_change)
self.event_bus.subscribe(EventType.VIEWPORT_UPDATE, self._on_viewport_update)
def add_panel(self, panel_id: str, x: float = 0, y: float = 0,
width: float = 100, height: float = 100,
constraints: Optional[LayoutConstraints] = None,
container_id: Optional[str] = None) -> PanelState:
"""Add a new panel to the layout system."""
panel = PanelState(
panel_id=panel_id,
x=x, y=y, width=width, height=height,
constraints=constraints or LayoutConstraints()
)
self.panels[panel_id] = panel
# Add to container if specified
if container_id and container_id in self.containers:
self.containers[container_id].panels.append(panel_id)
panel.parent = container_id
else:
# Add to root by default
self.root_container.panels.append(panel_id)
panel.parent = "root"
# Publish event
if self.event_bus:
self.event_bus.create_panel_event(
EventType.PANEL_RESIZE, panel_id, x, y, width, height,
source="LayoutManager.add_panel"
)
return panel
def add_container(self, container_id: str, layout_type: LayoutType,
x: float = 0, y: float = 0, width: float = 100, height: float = 100,
parent_container: Optional[str] = None) -> ContainerState:
"""Add a new layout container."""
container = ContainerState(
container_id=container_id,
layout_type=layout_type,
x=x, y=y, width=width, height=height
)
self.containers[container_id] = container
# Set parent relationship
if parent_container and parent_container in self.containers:
self.containers[parent_container].panels.append(container_id)
container.parent = parent_container
return container
def resize_panel(self, panel_id: str, width: float, height: float,
animate: bool = True) -> bool:
"""Resize a panel with optional animation."""
if panel_id not in self.panels:
return False
panel = self.panels[panel_id]
# Apply constraints
width, height = panel.constraints.apply_constraints(width, height)
if animate and self.animation_enabled:
# Start animation
if panel_id not in self.animations:
self.animations[panel_id] = AnimationState()
anim = self.animations[panel_id]
anim.start_x, anim.start_y = panel.x, panel.y
anim.start_width, anim.start_height = panel.width, panel.height
anim.target_x, anim.target_y = panel.x, panel.y
anim.target_width, anim.target_height = width, height
anim.progress = 0
anim.active = True
else:
# Apply immediately
panel.width, panel.height = width, height
panel.x, panel.y = panel.x, panel.y # Keep position
# Publish event
if self.event_bus:
self.event_bus.create_panel_event(
EventType.PANEL_RESIZE, panel_id,
panel.x, panel.y, panel.width, panel.height,
source="LayoutManager.resize_panel"
)
return True
def move_panel(self, panel_id: str, x: float, y: float,
animate: bool = True) -> bool:
"""Move a panel with optional animation."""
if panel_id not in self.panels:
return False
panel = self.panels[panel_id]
if animate and self.animation_enabled:
# Start animation
if panel_id not in self.animations:
self.animations[panel_id] = AnimationState()
anim = self.animations[panel_id]
anim.start_x, anim.start_y = panel.x, panel.y
anim.start_width, anim.start_height = panel.width, panel.height
anim.target_x, anim.target_y = x, y
anim.target_width, anim.target_height = panel.width, panel.height
anim.progress = 0
anim.active = True
else:
# Apply immediately
panel.x, panel.y = x, y
# Publish event
if self.event_bus:
self.event_bus.create_panel_event(
EventType.PANEL_RESIZE, panel_id,
panel.x, panel.y, panel.width, panel.height,
source="LayoutManager.move_panel"
)
return True
def set_panel_constraints(self, panel_id: str, constraints: LayoutConstraints) -> bool:
"""Set layout constraints for a panel."""
if panel_id not in self.panels:
return False
self.panels[panel_id].constraints = constraints
return True
def update_layout(self, container_id: Optional[str] = None) -> bool:
"""Update layout calculations for a container or all containers."""
start_time = time.time()
if container_id:
containers_to_update = [container_id] if container_id in self.containers else []
else:
containers_to_update = list(self.containers.keys())
for container_id in containers_to_update:
self._update_container_layout(container_id)
# Update animations
self._update_animations()
# Track performance
self.last_layout_time = time.time() - start_time
self.layout_count += 1
return True
def _update_container_layout(self, container_id: str):
"""Update layout for a specific container."""
if container_id not in self.containers:
return
container = self.containers[container_id]
if container.layout_type == LayoutType.HORIZONTAL:
self._layout_horizontal(container)
elif container.layout_type == LayoutType.VERTICAL:
self._layout_vertical(container)
elif container.layout_type == LayoutType.GRID:
self._layout_grid(container)
# ABSOLUTE layout doesn't need automatic positioning
def _layout_horizontal(self, container: ContainerState):
"""Layout panels horizontally within a container."""
available_width = container.width - (2 * container.padding)
total_flex = 0
fixed_width_total = 0
# Calculate flex and fixed widths
panel_ids = [p for p in container.panels if p in self.panels]
for panel_id in panel_ids:
panel = self.panels[panel_id]
if not panel.visible:
continue
if panel.constraints.resize_policy == ResizePolicy.FIXED:
fixed_width_total += panel.constraints.preferred_width
else:
total_flex += panel.constraints.flex_width
# Add spacing to fixed total
if len(panel_ids) > 1:
fixed_width_total += (len(panel_ids) - 1) * container.spacing
available_flex_width = max(0, available_width - fixed_width_total)
# Position panels
current_x = container.x + container.padding
for panel_id in panel_ids:
panel = self.panels[panel_id]
if not panel.visible:
continue
if panel.constraints.resize_policy == ResizePolicy.FIXED:
width = panel.constraints.preferred_width
else:
if total_flex > 0:
width = available_flex_width * (panel.constraints.flex_width / total_flex)
else:
width = panel.width
# Apply constraints
width, height = panel.constraints.apply_constraints(width, container.height)
panel.x = current_x
panel.y = container.y + container.padding
panel.width = width
panel.height = height
current_x += width + container.spacing
def _layout_vertical(self, container: ContainerState):
"""Layout panels vertically within a container."""
available_height = container.height - (2 * container.padding)
total_flex = 0
fixed_height_total = 0
# Calculate flex and fixed heights
panel_ids = [p for p in container.panels if p in self.panels]
for panel_id in panel_ids:
panel = self.panels[panel_id]
if not panel.visible:
continue
if panel.constraints.resize_policy == ResizePolicy.FIXED:
fixed_height_total += panel.constraints.preferred_height
else:
total_flex += panel.constraints.flex_height
# Add spacing to fixed total
if len(panel_ids) > 1:
fixed_height_total += (len(panel_ids) - 1) * container.spacing
available_flex_height = max(0, available_height - fixed_height_total)
# Position panels
current_y = container.y + container.padding
for panel_id in panel_ids:
panel = self.panels[panel_id]
if not panel.visible:
continue
if panel.constraints.resize_policy == ResizePolicy.FIXED:
height = panel.constraints.preferred_height
else:
if total_flex > 0:
height = available_flex_height * (panel.constraints.flex_height / total_flex)
else:
height = panel.height
# Apply constraints
width, height = panel.constraints.apply_constraints(container.width, height)
panel.x = container.x + container.padding
panel.y = current_y
panel.width = width
panel.height = height
current_y += height + container.spacing
def _layout_grid(self, container: ContainerState):
"""Layout panels in a grid within a container."""
# Simple grid layout - can be enhanced with more sophisticated grid logic
panel_ids = [p for p in container.panels if p in self.panels]
if not panel_ids:
return
# Calculate grid dimensions (for now, use a simple 2xN grid)
cols = min(2, len(panel_ids))
rows = math.ceil(len(panel_ids) / cols)
cell_width = (container.width - (2 * container.padding) - (cols - 1) * container.spacing) / cols
cell_height = (container.height - (2 * container.padding) - (rows - 1) * container.spacing) / rows
for i, panel_id in enumerate(panel_ids):
panel = self.panels[panel_id]
if not panel.visible:
continue
col = i % cols
row = i // cols
panel.x = container.x + container.padding + col * (cell_width + container.spacing)
panel.y = container.y + container.padding + row * (cell_height + container.spacing)
panel.width, panel.height = panel.constraints.apply_constraints(cell_width, cell_height)
def _update_animations(self):
"""Update active animations."""
if not self.animation_enabled:
return
dt = 0.016 # Assume 60 FPS for now
completed_animations = []
for panel_id, anim in self.animations.items():
if not anim.active or panel_id not in self.panels:
continue
anim.progress += dt / anim.duration
if anim.progress >= 1.0:
anim.progress = 1.0
anim.active = False
completed_animations.append(panel_id)
# Apply easing
t = self._apply_easing(anim.progress, anim.easing)
# Interpolate position and size
panel = self.panels[panel_id]
panel.x = anim.start_x + (anim.target_x - anim.start_x) * t
panel.y = anim.start_y + (anim.target_y - anim.start_y) * t
panel.width = anim.start_width + (anim.target_width - anim.start_width) * t
panel.height = anim.start_height + (anim.target_height - anim.start_height) * t
# Publish update event
if self.event_bus:
self.event_bus.create_panel_event(
EventType.PANEL_RESIZE, panel_id,
panel.x, panel.y, panel.width, panel.height,
source="LayoutManager.animation"
)
# Clean up completed animations
for panel_id in completed_animations:
del self.animations[panel_id]
def _apply_easing(self, t: float, easing_type: str) -> float:
"""Apply easing function to animation progress."""
if easing_type == "linear":
return t
elif easing_type == "ease_in_quad":
return t * t
elif easing_type == "ease_out_quad":
return 1 - (1 - t) * (1 - t)
elif easing_type == "ease_in_out_quad":
return 2 * t * t if t < 0.5 else 1 - pow(-2 * t + 2, 2) / 2
else:
return t # Default to linear
def get_panel_at_position(self, x: float, y: float) -> Optional[str]:
"""Get the panel at a given position."""
# Check panels in reverse order (top to bottom)
for panel_id, panel in reversed(list(self.panels.items())):
if panel.visible and panel.contains_point(x, y):
return panel_id
return None
def get_panel_rect(self, panel_id: str) -> Optional[Tuple[float, float, float, float]]:
"""Get the rectangle of a panel."""
if panel_id in self.panels:
return self.panels[panel_id].get_rect()
return None
def remove_panel(self, panel_id: str) -> bool:
"""Remove a panel from the layout system."""
if panel_id not in self.panels:
return False
panel = self.panels[panel_id]
# Remove from parent container
if panel.parent and panel.parent in self.containers:
container = self.containers[panel.parent]
if panel_id in container.panels:
container.panels.remove(panel_id)
# Remove panel and any animations
del self.panels[panel_id]
if panel_id in self.animations:
del self.animations[panel_id]
return True
def set_viewport_size(self, width: float, height: float):
"""Update the root container size."""
self.root_container.width = width
self.root_container.height = height
if self.event_bus:
self.event_bus.create_event(
EventType.VIEWPORT_UPDATE,
{"width": width, "height": height},
source="LayoutManager.set_viewport_size"
)
if self.auto_layout:
self.update_layout()
def focus_panel(self, panel_id: str) -> bool:
"""Set focus to a panel."""
if panel_id not in self.panels:
return False
# Clear focus from all panels
for panel in self.panels.values():
panel.focused = False
# Set focus to target panel
self.panels[panel_id].focused = True
if self.event_bus:
self.event_bus.create_event(
EventType.PANEL_FOCUS,
{"panel_id": panel_id, "focused": True},
source="LayoutManager.focus_panel"
)
return True
def get_layout_stats(self) -> Dict[str, Any]:
"""Get layout performance statistics."""
return {
"panel_count": len(self.panels),
"container_count": len(self.containers),
"active_animations": len([a for a in self.animations.values() if a.active]),
"last_layout_time": self.last_layout_time,
"total_layouts": self.layout_count,
"auto_layout_enabled": self.auto_layout,
"animation_enabled": self.animation_enabled
}
# Event handlers
def _on_panel_resize(self, event: PanelEvent):
"""Handle panel resize events."""
if event.panel_id in self.panels:
panel = self.panels[event.panel_id]
if not (hasattr(self, '_internal_update') and self._internal_update):
panel.x, panel.y = event.x, event.y
panel.width, panel.height = event.width, event.height
def _on_panel_focus(self, event: Event):
"""Handle panel focus events."""
panel_id = event.get("panel_id")
if panel_id:
self.focus_panel(panel_id)
def _on_layout_change(self, event: LayoutEvent):
"""Handle layout change events."""
if event.container_id in self.containers:
container = self.containers[event.container_id]
if event.layout_type:
container.layout_type = LayoutType(event.layout_type)
self.update_layout(event.container_id)
def _on_viewport_update(self, event: Event):
"""Handle viewport update events."""
width = event.get("width")
height = event.get("height")
if width is not None and height is not None:
self.set_viewport_size(width, height)

View File

@ -1,335 +0,0 @@
"""
Tree widget for displaying hierarchical inspector view.
Handles rendering, interaction, and navigation of tree nodes.
"""
import pygame
import pygame_gui
from pygame_gui.core import UIElement
from typing import List, Optional, Tuple, Any
from ui.inspector_tree import TreeNode, SimulationNode, TreeSelectionManager
from config.constants import (
PANEL_BACKGROUND_COLOR, PANEL_SELECTED_COLOR, PANEL_HOVER_COLOR,
PANEL_TEXT_COLOR, PANEL_ICON_COLOR, PANEL_NODE_HEIGHT, PANEL_INDENTATION
)
class TreeWidget(UIElement):
"""Interactive tree widget for hierarchical entity inspection."""
def __init__(self, relative_rect: pygame.Rect, manager: pygame_gui.UIManager, world: Any):
super().__init__(relative_rect, manager, container=None,
starting_height=1, layer_thickness=1)
self.world = world
self.font = pygame.font.Font(None, 16)
self.small_font = pygame.font.Font(None, 12)
# Tree structure
self.root_node = SimulationNode(world)
self.visible_nodes: List[TreeNode] = []
self.all_nodes: List[TreeNode] = []
# Selection management
self.selection_manager = TreeSelectionManager()
# Visual properties (using standardized panel styling)
self.node_height = PANEL_NODE_HEIGHT
self.expand_collapse_width = PANEL_INDENTATION
self.icon_size = 8
self.text_color = PANEL_TEXT_COLOR
self.selected_color = PANEL_SELECTED_COLOR
self.hover_color = PANEL_HOVER_COLOR
self.expand_icon_color = PANEL_ICON_COLOR
# Interaction state
self.hovered_node: Optional[TreeNode] = None
self.drag_start_node: Optional[TreeNode] = None
self.is_dragging = False
self.last_mouse_pos: Optional[Tuple[int, int]] = None
# Scrolling
self.scroll_offset = 0
self.total_height = 0
# Initialize tree structure
self._update_tree_structure()
self._update_visible_nodes()
def _update_tree_structure(self) -> None:
"""Update the tree structure based on current world state."""
self.root_node.update_entity_counts()
self.all_nodes = self._build_all_nodes_list(self.root_node)
def _build_all_nodes_list(self, node: TreeNode) -> List[TreeNode]:
"""Build a flat list of all nodes in the tree."""
nodes = [node]
for child in node.children:
nodes.extend(self._build_all_nodes_list(child))
return nodes
def _update_visible_nodes(self) -> None:
"""Update the list of visible nodes based on expanded state."""
self.visible_nodes = [self.root_node]
self.visible_nodes.extend(self.root_node.get_all_visible_descendants())
self._update_node_layout()
self.total_height = len(self.visible_nodes) * self.node_height
def _update_node_layout(self) -> None:
"""Update the layout rectangles for all visible nodes."""
y_offset = 0
for node in self.visible_nodes:
node.rect = pygame.Rect(
0,
y_offset - self.scroll_offset,
self.rect.width,
self.node_height
)
y_offset += self.node_height
def _get_node_at_position_local(self, local_pos: Tuple[int, int]) -> Optional[TreeNode]:
"""Get the node at the given local position (already converted)."""
for node in self.visible_nodes:
if node.rect.collidepoint(local_pos):
return node
return None
def _get_node_at_position(self, pos: Tuple[int, int]) -> Optional[TreeNode]:
"""Get the node at the given screen position."""
# Convert screen coordinates to local tree widget coordinates
local_pos = (pos[0] - self.rect.x, pos[1] - self.rect.y)
return self._get_node_at_position_local(local_pos)
def _get_expand_collapse_rect(self, node: TreeNode) -> pygame.Rect:
"""Get the rectangle for the expand/collapse icon."""
indent = node.get_indent()
return pygame.Rect(
indent + 5,
node.rect.y + (self.node_height - self.icon_size) // 2,
self.icon_size,
self.icon_size
)
def _is_click_on_expand_collapse_local(self, node: TreeNode, local_pos: Tuple[int, int]) -> bool:
"""Check if a click is on the expand/collapse icon (local coordinates)."""
if not node.can_expand():
return False
expand_rect = self._get_expand_collapse_rect(node)
return expand_rect.collidepoint(local_pos)
def _is_click_on_expand_collapse(self, node: TreeNode, pos: Tuple[int, int]) -> bool:
"""Check if a click is on the expand/collapse icon."""
if not node.can_expand():
return False
local_pos = (pos[0] - self.rect.x, pos[1] - self.rect.y)
expand_rect = self._get_expand_collapse_rect(node)
return expand_rect.collidepoint(local_pos)
def _render_node(self, surface: pygame.Surface, node: TreeNode) -> None:
"""Render a single tree node."""
if not node.rect.colliderect(surface.get_rect()):
return
# Background
if node.is_selected:
pygame.draw.rect(surface, self.selected_color, node.rect)
elif node == self.hovered_node:
pygame.draw.rect(surface, self.hover_color, node.rect)
# Expand/collapse icon
if node.can_expand():
icon_rect = self._get_expand_collapse_rect(node)
icon_color = self.expand_icon_color
# Draw + or - icon
center_x = icon_rect.centerx
center_y = icon_rect.centery
size = 3
# Horizontal line
pygame.draw.line(surface, icon_color,
(center_x - size, center_y),
(center_x + size, center_y), 1)
# Vertical line (only for collapsed nodes)
if not node.is_expanded:
pygame.draw.line(surface, icon_color,
(center_x, center_y - size),
(center_x, center_y + size), 1)
# Node text
indent = node.get_indent()
text_x = indent + (self.expand_collapse_width if node.can_expand() else 10)
text_surface = self.font.render(node.get_display_text(), True, self.text_color)
text_rect = pygame.Rect(
text_x,
node.rect.y + (self.node_height - text_surface.get_height()) // 2,
text_surface.get_width(),
text_surface.get_height()
)
# Clip text if it's too wide
max_text_width = self.rect.width - text_x - 10
if text_rect.width > max_text_width:
# Truncate text
text = node.get_display_text()
while text and text_surface.get_width() > max_text_width - 20:
text = text[:-1]
text_surface = self.font.render(text + "...", True, self.text_color)
surface.blit(text_surface, text_rect)
def handle_event(self, event: pygame.event.Event) -> bool:
"""Handle input events for the tree widget."""
if event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1: # Left click
# Event position is already converted to local coordinates by HUD
self.last_mouse_pos = event.pos
node = self._get_node_at_position_local(event.pos)
if node:
# Check for expand/collapse click
if self._is_click_on_expand_collapse_local(node, event.pos):
node.toggle_expand()
self._update_visible_nodes()
else:
# Selection click
multi_select = pygame.key.get_pressed()[pygame.K_LSHIFT]
self.selection_manager.select_node(node, multi_select)
return True
elif event.type == pygame.MOUSEMOTION:
# Event position is already converted to local coordinates by HUD
self.last_mouse_pos = event.pos
self.hovered_node = self._get_node_at_position_local(event.pos)
return True
elif event.type == pygame.MOUSEWHEEL:
# Handle scrolling
if event.y > 0:
self.scroll_offset = max(0, self.scroll_offset - self.node_height)
else:
max_scroll = max(0, self.total_height - self.rect.height)
self.scroll_offset = min(max_scroll, self.scroll_offset + self.node_height)
self._update_node_layout()
return True
return False
def update(self, time_delta: float) -> None:
"""Update the tree widget with smart performance optimizations."""
# Smart update system: only update tree structure when necessary
if self._should_update_tree():
self._update_tree_structure()
self._update_visible_nodes()
# Recalculate hover state based on last mouse position after structure changes
if self.last_mouse_pos:
self.hovered_node = self._get_node_at_position_local(self.last_mouse_pos)
def _should_update_tree(self) -> bool:
"""Determine if the tree structure needs updating."""
if not hasattr(self, '_last_entity_count'):
self._last_entity_count = len(self.world.get_objects())
return True
current_count = len(self.world.get_objects())
if current_count != self._last_entity_count:
self._last_entity_count = current_count
return True
# Also check if any previously selected entities have been removed
if self.selection_manager.selected_nodes:
for node in self.selection_manager.selected_nodes[:]:
if hasattr(node, 'entity'):
try:
# Check if entity still exists in world
if node.entity not in self.world.get_objects():
return True
except:
return True
return False
def draw(self, surface: pygame.Surface) -> None:
"""Draw the tree widget."""
# Create a clipping surface for the tree area
tree_surface = pygame.Surface((self.rect.width, self.rect.height))
tree_surface.fill(PANEL_BACKGROUND_COLOR) # Using standardized background color
# Set up clipping rect to prevent rendering outside bounds
clip_rect = pygame.Rect(0, 0, self.rect.width, self.rect.height)
tree_surface.set_clip(clip_rect)
# Render visible nodes (only those that should be visible after scrolling)
for node in self.visible_nodes:
# Only render if node is within the visible tree area
if node.rect.bottom > 0 and node.rect.top < self.rect.height:
self._render_node(tree_surface, node)
# Blit to main surface at position (0,0) since this is already positioned
surface.blit(tree_surface, (0, 0))
def expand_all(self) -> None:
"""Expand all nodes in the tree."""
for node in self.all_nodes:
if node.can_expand():
node.expand()
self._update_visible_nodes()
def collapse_all(self) -> None:
"""Collapse all nodes except the root."""
for node in self.all_nodes[1:]: # Skip root node
if node.can_expand():
node.collapse()
self._update_visible_nodes()
def get_selected_entities(self) -> List[Any]:
"""Get the entities currently selected in the tree."""
return self.selection_manager.get_selected_entities()
def select_entities(self, entities: List[Any]) -> None:
"""Select specific entities in the tree."""
self.selection_manager.clear_selection()
for entity in entities:
# Find the entity node for this entity
for node in self.all_nodes:
if hasattr(node, 'entity') and node.entity == entity:
self.selection_manager.select_node(node, multi_select=True)
break
# Expand parent nodes to show selected entities
for node in self.selection_manager.selected_nodes:
parent = node.parent
while parent:
if parent.can_expand():
parent.expand()
parent = parent.parent
self._update_visible_nodes()
def scroll_to_node(self, node: TreeNode) -> None:
"""Scroll the tree to show the given node."""
if node not in self.visible_nodes:
return
node_top = node.rect.y
node_bottom = node.rect.y + node.rect.height
if node_top < 0:
self.scroll_offset = max(0, self.scroll_offset + node_top)
elif node_bottom > self.rect.height:
self.scroll_offset = min(
self.total_height - self.rect.height,
self.scroll_offset + (node_bottom - self.rect.height)
)
self._update_node_layout()
def clear_hover(self) -> None:
"""Clear the hover state (called when mouse leaves tree widget)."""
self.hovered_node = None
self.last_mouse_pos = None

578
uv.lock generated
View File

@ -1,10 +1,6 @@
version = 1 version = 1
revision = 3 revision = 2
requires-python = ">=3.11" requires-python = ">=3.11"
resolution-markers = [
"python_full_version >= '3.12'",
"python_full_version < '3.12'",
]
[[package]] [[package]]
name = "annotated-types" name = "annotated-types"
@ -33,97 +29,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
] ]
[[package]]
name = "contourpy"
version = "1.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" },
{ url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" },
{ url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" },
{ url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" },
{ url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" },
{ url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" },
{ url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" },
{ url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" },
{ url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" },
{ url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" },
{ url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" },
{ url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" },
{ url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" },
{ url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" },
{ url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" },
{ url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" },
{ url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" },
{ url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" },
{ url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" },
{ url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" },
{ url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" },
{ url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" },
{ url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" },
{ url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" },
{ url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" },
{ url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" },
{ url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" },
{ url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" },
{ url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" },
{ url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" },
{ url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" },
{ url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" },
{ url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" },
{ url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" },
{ url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" },
{ url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" },
{ url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" },
{ url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" },
{ url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" },
{ url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" },
{ url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" },
{ url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" },
{ url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" },
{ url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" },
{ url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" },
{ url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" },
{ url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" },
{ url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" },
{ url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" },
{ url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" },
{ url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" },
{ url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" },
{ url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" },
{ url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" },
{ url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" },
{ url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" },
{ url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" },
{ url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" },
{ url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" },
{ url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" },
{ url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" },
{ url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" },
{ url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" },
{ url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" },
{ url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" },
{ url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" },
{ url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" },
{ url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" },
{ url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" },
{ url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" },
{ url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" },
]
[[package]]
name = "cycler"
version = "0.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
]
[[package]] [[package]]
name = "distlib" name = "distlib"
version = "0.3.9" version = "0.3.9"
@ -138,47 +43,31 @@ name = "dynamicsystemabstraction"
version = "0.1.0" version = "0.1.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "matplotlib" },
{ name = "numpy" }, { name = "numpy" },
{ name = "pandas" },
{ name = "pre-commit" }, { name = "pre-commit" },
{ name = "psutil" },
{ name = "pydantic" }, { name = "pydantic" },
{ name = "pygame" }, { name = "pygame" },
{ name = "pygame-gui" }, { name = "pygame-gui" },
{ name = "pytest" }, { name = "pytest" },
{ name = "pyyaml" },
{ name = "tqdm" },
] ]
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
{ name = "psutil" },
{ name = "ruff" }, { name = "ruff" },
{ name = "snakeviz" },
] ]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "matplotlib", specifier = ">=3.10.7" },
{ name = "numpy", specifier = ">=2.3.0" }, { name = "numpy", specifier = ">=2.3.0" },
{ name = "pandas", specifier = ">=2.3.3" },
{ name = "pre-commit", specifier = ">=4.2.0" }, { name = "pre-commit", specifier = ">=4.2.0" },
{ name = "psutil", specifier = ">=7.0.0" },
{ name = "pydantic", specifier = ">=2.11.5" }, { name = "pydantic", specifier = ">=2.11.5" },
{ name = "pygame", specifier = ">=2.6.1" }, { name = "pygame", specifier = ">=2.6.1" },
{ name = "pygame-gui", specifier = ">=0.6.14" }, { name = "pygame-gui", specifier = ">=0.6.14" },
{ name = "pytest", specifier = ">=8.3.5" }, { name = "pytest", specifier = ">=8.3.5" },
{ name = "pyyaml", specifier = ">=6.0.2" },
{ name = "tqdm", specifier = ">=4.67.1" },
] ]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [{ name = "ruff", specifier = ">=0.11.12" }]
{ name = "psutil", specifier = ">=7.0.0" },
{ name = "ruff", specifier = ">=0.11.12" },
{ name = "snakeviz", specifier = ">=2.2.2" },
]
[[package]] [[package]]
name = "filelock" name = "filelock"
@ -189,55 +78,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" },
] ]
[[package]]
name = "fonttools"
version = "4.60.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4b/42/97a13e47a1e51a5a7142475bbcf5107fe3a68fc34aef331c897d5fb98ad0/fonttools-4.60.1.tar.gz", hash = "sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9", size = 3559823, upload-time = "2025-09-29T21:13:27.129Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ea/85/639aa9bface1537e0fb0f643690672dde0695a5bbbc90736bc571b0b1941/fonttools-4.60.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f", size = 2831872, upload-time = "2025-09-29T21:11:20.329Z" },
{ url = "https://files.pythonhosted.org/packages/6b/47/3c63158459c95093be9618794acb1067b3f4d30dcc5c3e8114b70e67a092/fonttools-4.60.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2", size = 2356990, upload-time = "2025-09-29T21:11:22.754Z" },
{ url = "https://files.pythonhosted.org/packages/94/dd/1934b537c86fcf99f9761823f1fc37a98fbd54568e8e613f29a90fed95a9/fonttools-4.60.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914", size = 5042189, upload-time = "2025-09-29T21:11:25.061Z" },
{ url = "https://files.pythonhosted.org/packages/d2/d2/9f4e4c4374dd1daa8367784e1bd910f18ba886db1d6b825b12edf6db3edc/fonttools-4.60.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1", size = 4978683, upload-time = "2025-09-29T21:11:27.693Z" },
{ url = "https://files.pythonhosted.org/packages/cc/c4/0fb2dfd1ecbe9a07954cc13414713ed1eab17b1c0214ef07fc93df234a47/fonttools-4.60.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d", size = 5021372, upload-time = "2025-09-29T21:11:30.257Z" },
{ url = "https://files.pythonhosted.org/packages/0c/d5/495fc7ae2fab20223cc87179a8f50f40f9a6f821f271ba8301ae12bb580f/fonttools-4.60.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa", size = 5132562, upload-time = "2025-09-29T21:11:32.737Z" },
{ url = "https://files.pythonhosted.org/packages/bc/fa/021dab618526323c744e0206b3f5c8596a2e7ae9aa38db5948a131123e83/fonttools-4.60.1-cp311-cp311-win32.whl", hash = "sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258", size = 2230288, upload-time = "2025-09-29T21:11:35.015Z" },
{ url = "https://files.pythonhosted.org/packages/bb/78/0e1a6d22b427579ea5c8273e1c07def2f325b977faaf60bb7ddc01456cb1/fonttools-4.60.1-cp311-cp311-win_amd64.whl", hash = "sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf", size = 2278184, upload-time = "2025-09-29T21:11:37.434Z" },
{ url = "https://files.pythonhosted.org/packages/e3/f7/a10b101b7a6f8836a5adb47f2791f2075d044a6ca123f35985c42edc82d8/fonttools-4.60.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc", size = 2832953, upload-time = "2025-09-29T21:11:39.616Z" },
{ url = "https://files.pythonhosted.org/packages/ed/fe/7bd094b59c926acf2304d2151354ddbeb74b94812f3dc943c231db09cb41/fonttools-4.60.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877", size = 2352706, upload-time = "2025-09-29T21:11:41.826Z" },
{ url = "https://files.pythonhosted.org/packages/c0/ca/4bb48a26ed95a1e7eba175535fe5805887682140ee0a0d10a88e1de84208/fonttools-4.60.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c", size = 4923716, upload-time = "2025-09-29T21:11:43.893Z" },
{ url = "https://files.pythonhosted.org/packages/b8/9f/2cb82999f686c1d1ddf06f6ae1a9117a880adbec113611cc9d22b2fdd465/fonttools-4.60.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:996a4d1834524adbb423385d5a629b868ef9d774670856c63c9a0408a3063401", size = 4968175, upload-time = "2025-09-29T21:11:46.439Z" },
{ url = "https://files.pythonhosted.org/packages/18/79/be569699e37d166b78e6218f2cde8c550204f2505038cdd83b42edc469b9/fonttools-4.60.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a46b2f450bc79e06ef3b6394f0c68660529ed51692606ad7f953fc2e448bc903", size = 4911031, upload-time = "2025-09-29T21:11:48.977Z" },
{ url = "https://files.pythonhosted.org/packages/cc/9f/89411cc116effaec5260ad519162f64f9c150e5522a27cbb05eb62d0c05b/fonttools-4.60.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6ec722ee589e89a89f5b7574f5c45604030aa6ae24cb2c751e2707193b466fed", size = 5062966, upload-time = "2025-09-29T21:11:54.344Z" },
{ url = "https://files.pythonhosted.org/packages/62/a1/f888221934b5731d46cb9991c7a71f30cb1f97c0ef5fcf37f8da8fce6c8e/fonttools-4.60.1-cp312-cp312-win32.whl", hash = "sha256:b2cf105cee600d2de04ca3cfa1f74f1127f8455b71dbad02b9da6ec266e116d6", size = 2218750, upload-time = "2025-09-29T21:11:56.601Z" },
{ url = "https://files.pythonhosted.org/packages/88/8f/a55b5550cd33cd1028601df41acd057d4be20efa5c958f417b0c0613924d/fonttools-4.60.1-cp312-cp312-win_amd64.whl", hash = "sha256:992775c9fbe2cf794786fa0ffca7f09f564ba3499b8fe9f2f80bd7197db60383", size = 2267026, upload-time = "2025-09-29T21:11:58.852Z" },
{ url = "https://files.pythonhosted.org/packages/7c/5b/cdd2c612277b7ac7ec8c0c9bc41812c43dc7b2d5f2b0897e15fdf5a1f915/fonttools-4.60.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb", size = 2825777, upload-time = "2025-09-29T21:12:01.22Z" },
{ url = "https://files.pythonhosted.org/packages/d6/8a/de9cc0540f542963ba5e8f3a1f6ad48fa211badc3177783b9d5cadf79b5d/fonttools-4.60.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eedacb5c5d22b7097482fa834bda0dafa3d914a4e829ec83cdea2a01f8c813c4", size = 2348080, upload-time = "2025-09-29T21:12:03.785Z" },
{ url = "https://files.pythonhosted.org/packages/2d/8b/371ab3cec97ee3fe1126b3406b7abd60c8fec8975fd79a3c75cdea0c3d83/fonttools-4.60.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b33a7884fabd72bdf5f910d0cf46be50dce86a0362a65cfc746a4168c67eb96c", size = 4903082, upload-time = "2025-09-29T21:12:06.382Z" },
{ url = "https://files.pythonhosted.org/packages/04/05/06b1455e4bc653fcb2117ac3ef5fa3a8a14919b93c60742d04440605d058/fonttools-4.60.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77", size = 4960125, upload-time = "2025-09-29T21:12:09.314Z" },
{ url = "https://files.pythonhosted.org/packages/8e/37/f3b840fcb2666f6cb97038793606bdd83488dca2d0b0fc542ccc20afa668/fonttools-4.60.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c8651e0d4b3bdeda6602b85fdc2abbefc1b41e573ecb37b6779c4ca50753a199", size = 4901454, upload-time = "2025-09-29T21:12:11.931Z" },
{ url = "https://files.pythonhosted.org/packages/fd/9e/eb76f77e82f8d4a46420aadff12cec6237751b0fb9ef1de373186dcffb5f/fonttools-4.60.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:145daa14bf24824b677b9357c5e44fd8895c2a8f53596e1b9ea3496081dc692c", size = 5044495, upload-time = "2025-09-29T21:12:15.241Z" },
{ url = "https://files.pythonhosted.org/packages/f8/b3/cede8f8235d42ff7ae891bae8d619d02c8ac9fd0cfc450c5927a6200c70d/fonttools-4.60.1-cp313-cp313-win32.whl", hash = "sha256:2299df884c11162617a66b7c316957d74a18e3758c0274762d2cc87df7bc0272", size = 2217028, upload-time = "2025-09-29T21:12:17.96Z" },
{ url = "https://files.pythonhosted.org/packages/75/4d/b022c1577807ce8b31ffe055306ec13a866f2337ecee96e75b24b9b753ea/fonttools-4.60.1-cp313-cp313-win_amd64.whl", hash = "sha256:a3db56f153bd4c5c2b619ab02c5db5192e222150ce5a1bc10f16164714bc39ac", size = 2266200, upload-time = "2025-09-29T21:12:20.14Z" },
{ url = "https://files.pythonhosted.org/packages/9a/83/752ca11c1aa9a899b793a130f2e466b79ea0cf7279c8d79c178fc954a07b/fonttools-4.60.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a884aef09d45ba1206712c7dbda5829562d3fea7726935d3289d343232ecb0d3", size = 2822830, upload-time = "2025-09-29T21:12:24.406Z" },
{ url = "https://files.pythonhosted.org/packages/57/17/bbeab391100331950a96ce55cfbbff27d781c1b85ebafb4167eae50d9fe3/fonttools-4.60.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8a44788d9d91df72d1a5eac49b31aeb887a5f4aab761b4cffc4196c74907ea85", size = 2345524, upload-time = "2025-09-29T21:12:26.819Z" },
{ url = "https://files.pythonhosted.org/packages/3d/2e/d4831caa96d85a84dd0da1d9f90d81cec081f551e0ea216df684092c6c97/fonttools-4.60.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e852d9dda9f93ad3651ae1e3bb770eac544ec93c3807888798eccddf84596537", size = 4843490, upload-time = "2025-09-29T21:12:29.123Z" },
{ url = "https://files.pythonhosted.org/packages/49/13/5e2ea7c7a101b6fc3941be65307ef8df92cbbfa6ec4804032baf1893b434/fonttools-4.60.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:154cb6ee417e417bf5f7c42fe25858c9140c26f647c7347c06f0cc2d47eff003", size = 4944184, upload-time = "2025-09-29T21:12:31.414Z" },
{ url = "https://files.pythonhosted.org/packages/0c/2b/cf9603551c525b73fc47c52ee0b82a891579a93d9651ed694e4e2cd08bb8/fonttools-4.60.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5664fd1a9ea7f244487ac8f10340c4e37664675e8667d6fee420766e0fb3cf08", size = 4890218, upload-time = "2025-09-29T21:12:33.936Z" },
{ url = "https://files.pythonhosted.org/packages/fd/2f/933d2352422e25f2376aae74f79eaa882a50fb3bfef3c0d4f50501267101/fonttools-4.60.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:583b7f8e3c49486e4d489ad1deacfb8d5be54a8ef34d6df824f6a171f8511d99", size = 4999324, upload-time = "2025-09-29T21:12:36.637Z" },
{ url = "https://files.pythonhosted.org/packages/38/99/234594c0391221f66216bc2c886923513b3399a148defaccf81dc3be6560/fonttools-4.60.1-cp314-cp314-win32.whl", hash = "sha256:66929e2ea2810c6533a5184f938502cfdaea4bc3efb7130d8cc02e1c1b4108d6", size = 2220861, upload-time = "2025-09-29T21:12:39.108Z" },
{ url = "https://files.pythonhosted.org/packages/3e/1d/edb5b23726dde50fc4068e1493e4fc7658eeefcaf75d4c5ffce067d07ae5/fonttools-4.60.1-cp314-cp314-win_amd64.whl", hash = "sha256:f3d5be054c461d6a2268831f04091dc82753176f6ea06dc6047a5e168265a987", size = 2270934, upload-time = "2025-09-29T21:12:41.339Z" },
{ url = "https://files.pythonhosted.org/packages/fb/da/1392aaa2170adc7071fe7f9cfd181a5684a7afcde605aebddf1fb4d76df5/fonttools-4.60.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:b6379e7546ba4ae4b18f8ae2b9bc5960936007a1c0e30b342f662577e8bc3299", size = 2894340, upload-time = "2025-09-29T21:12:43.774Z" },
{ url = "https://files.pythonhosted.org/packages/bf/a7/3b9f16e010d536ce567058b931a20b590d8f3177b2eda09edd92e392375d/fonttools-4.60.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9d0ced62b59e0430b3690dbc5373df1c2aa7585e9a8ce38eff87f0fd993c5b01", size = 2375073, upload-time = "2025-09-29T21:12:46.437Z" },
{ url = "https://files.pythonhosted.org/packages/9b/b5/e9bcf51980f98e59bb5bb7c382a63c6f6cac0eec5f67de6d8f2322382065/fonttools-4.60.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:875cb7764708b3132637f6c5fb385b16eeba0f7ac9fa45a69d35e09b47045801", size = 4849758, upload-time = "2025-09-29T21:12:48.694Z" },
{ url = "https://files.pythonhosted.org/packages/e3/dc/1d2cf7d1cba82264b2f8385db3f5960e3d8ce756b4dc65b700d2c496f7e9/fonttools-4.60.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a184b2ea57b13680ab6d5fbde99ccef152c95c06746cb7718c583abd8f945ccc", size = 5085598, upload-time = "2025-09-29T21:12:51.081Z" },
{ url = "https://files.pythonhosted.org/packages/5d/4d/279e28ba87fb20e0c69baf72b60bbf1c4d873af1476806a7b5f2b7fac1ff/fonttools-4.60.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:026290e4ec76583881763fac284aca67365e0be9f13a7fb137257096114cb3bc", size = 4957603, upload-time = "2025-09-29T21:12:53.423Z" },
{ url = "https://files.pythonhosted.org/packages/78/d4/ff19976305e0c05aa3340c805475abb00224c954d3c65e82c0a69633d55d/fonttools-4.60.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f0e8817c7d1a0c2eedebf57ef9a9896f3ea23324769a9a2061a80fe8852705ed", size = 4974184, upload-time = "2025-09-29T21:12:55.962Z" },
{ url = "https://files.pythonhosted.org/packages/63/22/8553ff6166f5cd21cfaa115aaacaa0dc73b91c079a8cfd54a482cbc0f4f5/fonttools-4.60.1-cp314-cp314t-win32.whl", hash = "sha256:1410155d0e764a4615774e5c2c6fc516259fe3eca5882f034eb9bfdbee056259", size = 2282241, upload-time = "2025-09-29T21:12:58.179Z" },
{ url = "https://files.pythonhosted.org/packages/8a/cb/fa7b4d148e11d5a72761a22e595344133e83a9507a4c231df972e657579b/fonttools-4.60.1-cp314-cp314t-win_amd64.whl", hash = "sha256:022beaea4b73a70295b688f817ddc24ed3e3418b5036ffcd5658141184ef0d0c", size = 2345760, upload-time = "2025-09-29T21:13:00.375Z" },
{ url = "https://files.pythonhosted.org/packages/c7/93/0dd45cd283c32dea1545151d8c3637b4b8c53cdb3a625aeb2885b184d74d/fonttools-4.60.1-py3-none-any.whl", hash = "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb", size = 1143175, upload-time = "2025-09-29T21:13:24.134Z" },
]
[[package]] [[package]]
name = "identify" name = "identify"
version = "2.6.12" version = "2.6.12"
@ -256,160 +96,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
] ]
[[package]]
name = "kiwisolver"
version = "1.4.9"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" },
{ url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" },
{ url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" },
{ url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" },
{ url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" },
{ url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" },
{ url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" },
{ url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" },
{ url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" },
{ url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" },
{ url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" },
{ url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" },
{ url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" },
{ url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" },
{ url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" },
{ url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" },
{ url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" },
{ url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" },
{ url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" },
{ url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" },
{ url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" },
{ url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" },
{ url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" },
{ url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" },
{ url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" },
{ url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" },
{ url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" },
{ url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" },
{ url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" },
{ url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" },
{ url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" },
{ url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" },
{ url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" },
{ url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" },
{ url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" },
{ url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" },
{ url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" },
{ url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" },
{ url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" },
{ url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" },
{ url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" },
{ url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" },
{ url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" },
{ url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" },
{ url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" },
{ url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" },
{ url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" },
{ url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" },
{ url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" },
{ url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" },
{ url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" },
{ url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" },
{ url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" },
{ url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" },
{ url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414, upload-time = "2025-08-10T21:27:05.437Z" },
{ url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272, upload-time = "2025-08-10T21:27:07.063Z" },
{ url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578, upload-time = "2025-08-10T21:27:08.452Z" },
{ url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607, upload-time = "2025-08-10T21:27:10.125Z" },
{ url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150, upload-time = "2025-08-10T21:27:11.484Z" },
{ url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" },
{ url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" },
{ url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" },
{ url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" },
{ url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" },
{ url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" },
{ url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" },
{ url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" },
{ url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197, upload-time = "2025-08-10T21:27:22.604Z" },
{ url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125, upload-time = "2025-08-10T21:27:24.036Z" },
{ url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612, upload-time = "2025-08-10T21:27:25.773Z" },
{ url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990, upload-time = "2025-08-10T21:27:27.089Z" },
{ url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601, upload-time = "2025-08-10T21:27:29.343Z" },
{ url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" },
{ url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" },
{ url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" },
{ url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" },
{ url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" },
{ url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" },
{ url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" },
{ url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" },
{ url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" },
{ url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" },
]
[[package]]
name = "matplotlib"
version = "3.10.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "contourpy" },
{ name = "cycler" },
{ name = "fonttools" },
{ name = "kiwisolver" },
{ name = "numpy" },
{ name = "packaging" },
{ name = "pillow" },
{ name = "pyparsing" },
{ name = "python-dateutil" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ae/e2/d2d5295be2f44c678ebaf3544ba32d20c1f9ef08c49fe47f496180e1db15/matplotlib-3.10.7.tar.gz", hash = "sha256:a06ba7e2a2ef9131c79c49e63dad355d2d878413a0376c1727c8b9335ff731c7", size = 34804865, upload-time = "2025-10-09T00:28:00.669Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fc/bc/0fb489005669127ec13f51be0c6adc074d7cf191075dab1da9fe3b7a3cfc/matplotlib-3.10.7-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:53b492410a6cd66c7a471de6c924f6ede976e963c0f3097a3b7abfadddc67d0a", size = 8257507, upload-time = "2025-10-09T00:26:19.073Z" },
{ url = "https://files.pythonhosted.org/packages/e2/6a/d42588ad895279ff6708924645b5d2ed54a7fb2dc045c8a804e955aeace1/matplotlib-3.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d9749313deb729f08207718d29c86246beb2ea3fdba753595b55901dee5d2fd6", size = 8119565, upload-time = "2025-10-09T00:26:21.023Z" },
{ url = "https://files.pythonhosted.org/packages/10/b7/4aa196155b4d846bd749cf82aa5a4c300cf55a8b5e0dfa5b722a63c0f8a0/matplotlib-3.10.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2222c7ba2cbde7fe63032769f6eb7e83ab3227f47d997a8453377709b7fe3a5a", size = 8692668, upload-time = "2025-10-09T00:26:22.967Z" },
{ url = "https://files.pythonhosted.org/packages/e6/e7/664d2b97016f46683a02d854d730cfcf54ff92c1dafa424beebef50f831d/matplotlib-3.10.7-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e91f61a064c92c307c5a9dc8c05dc9f8a68f0a3be199d9a002a0622e13f874a1", size = 9521051, upload-time = "2025-10-09T00:26:25.041Z" },
{ url = "https://files.pythonhosted.org/packages/a8/a3/37aef1404efa615f49b5758a5e0261c16dd88f389bc1861e722620e4a754/matplotlib-3.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6f1851eab59ca082c95df5a500106bad73672645625e04538b3ad0f69471ffcc", size = 9576878, upload-time = "2025-10-09T00:26:27.478Z" },
{ url = "https://files.pythonhosted.org/packages/33/cd/b145f9797126f3f809d177ca378de57c45413c5099c5990de2658760594a/matplotlib-3.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:6516ce375109c60ceec579e699524e9d504cd7578506f01150f7a6bc174a775e", size = 8115142, upload-time = "2025-10-09T00:26:29.774Z" },
{ url = "https://files.pythonhosted.org/packages/2e/39/63bca9d2b78455ed497fcf51a9c71df200a11048f48249038f06447fa947/matplotlib-3.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:b172db79759f5f9bc13ef1c3ef8b9ee7b37b0247f987fbbbdaa15e4f87fd46a9", size = 7992439, upload-time = "2025-10-09T00:26:40.32Z" },
{ url = "https://files.pythonhosted.org/packages/be/b3/09eb0f7796932826ec20c25b517d568627754f6c6462fca19e12c02f2e12/matplotlib-3.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a0edb7209e21840e8361e91ea84ea676658aa93edd5f8762793dec77a4a6748", size = 8272389, upload-time = "2025-10-09T00:26:42.474Z" },
{ url = "https://files.pythonhosted.org/packages/11/0b/1ae80ddafb8652fd8046cb5c8460ecc8d4afccb89e2c6d6bec61e04e1eaf/matplotlib-3.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c380371d3c23e0eadf8ebff114445b9f970aff2010198d498d4ab4c3b41eea4f", size = 8128247, upload-time = "2025-10-09T00:26:44.77Z" },
{ url = "https://files.pythonhosted.org/packages/7d/18/95ae2e242d4a5c98bd6e90e36e128d71cf1c7e39b0874feaed3ef782e789/matplotlib-3.10.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d5f256d49fea31f40f166a5e3131235a5d2f4b7f44520b1cf0baf1ce568ccff0", size = 8696996, upload-time = "2025-10-09T00:26:46.792Z" },
{ url = "https://files.pythonhosted.org/packages/7e/3d/5b559efc800bd05cb2033aa85f7e13af51958136a48327f7c261801ff90a/matplotlib-3.10.7-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11ae579ac83cdf3fb72573bb89f70e0534de05266728740d478f0f818983c695", size = 9530153, upload-time = "2025-10-09T00:26:49.07Z" },
{ url = "https://files.pythonhosted.org/packages/88/57/eab4a719fd110312d3c220595d63a3c85ec2a39723f0f4e7fa7e6e3f74ba/matplotlib-3.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4c14b6acd16cddc3569a2d515cfdd81c7a68ac5639b76548cfc1a9e48b20eb65", size = 9593093, upload-time = "2025-10-09T00:26:51.067Z" },
{ url = "https://files.pythonhosted.org/packages/31/3c/80816f027b3a4a28cd2a0a6ef7f89a2db22310e945cd886ec25bfb399221/matplotlib-3.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:0d8c32b7ea6fb80b1aeff5a2ceb3fb9778e2759e899d9beff75584714afcc5ee", size = 8122771, upload-time = "2025-10-09T00:26:53.296Z" },
{ url = "https://files.pythonhosted.org/packages/de/77/ef1fc78bfe99999b2675435cc52120887191c566b25017d78beaabef7f2d/matplotlib-3.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:5f3f6d315dcc176ba7ca6e74c7768fb7e4cf566c49cb143f6bc257b62e634ed8", size = 7992812, upload-time = "2025-10-09T00:26:54.882Z" },
{ url = "https://files.pythonhosted.org/packages/02/9c/207547916a02c78f6bdd83448d9b21afbc42f6379ed887ecf610984f3b4e/matplotlib-3.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1d9d3713a237970569156cfb4de7533b7c4eacdd61789726f444f96a0d28f57f", size = 8273212, upload-time = "2025-10-09T00:26:56.752Z" },
{ url = "https://files.pythonhosted.org/packages/bc/d0/b3d3338d467d3fc937f0bb7f256711395cae6f78e22cef0656159950adf0/matplotlib-3.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37a1fea41153dd6ee061d21ab69c9cf2cf543160b1b85d89cd3d2e2a7902ca4c", size = 8128713, upload-time = "2025-10-09T00:26:59.001Z" },
{ url = "https://files.pythonhosted.org/packages/22/ff/6425bf5c20d79aa5b959d1ce9e65f599632345391381c9a104133fe0b171/matplotlib-3.10.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b3c4ea4948d93c9c29dc01c0c23eef66f2101bf75158c291b88de6525c55c3d1", size = 8698527, upload-time = "2025-10-09T00:27:00.69Z" },
{ url = "https://files.pythonhosted.org/packages/d0/7f/ccdca06f4c2e6c7989270ed7829b8679466682f4cfc0f8c9986241c023b6/matplotlib-3.10.7-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22df30ffaa89f6643206cf13877191c63a50e8f800b038bc39bee9d2d4957632", size = 9529690, upload-time = "2025-10-09T00:27:02.664Z" },
{ url = "https://files.pythonhosted.org/packages/b8/95/b80fc2c1f269f21ff3d193ca697358e24408c33ce2b106a7438a45407b63/matplotlib-3.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b69676845a0a66f9da30e87f48be36734d6748024b525ec4710be40194282c84", size = 9593732, upload-time = "2025-10-09T00:27:04.653Z" },
{ url = "https://files.pythonhosted.org/packages/e1/b6/23064a96308b9aeceeffa65e96bcde459a2ea4934d311dee20afde7407a0/matplotlib-3.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:744991e0cc863dd669c8dc9136ca4e6e0082be2070b9d793cbd64bec872a6815", size = 8122727, upload-time = "2025-10-09T00:27:06.814Z" },
{ url = "https://files.pythonhosted.org/packages/b3/a6/2faaf48133b82cf3607759027f82b5c702aa99cdfcefb7f93d6ccf26a424/matplotlib-3.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:fba2974df0bf8ce3c995fa84b79cde38326e0f7b5409e7a3a481c1141340bcf7", size = 7992958, upload-time = "2025-10-09T00:27:08.567Z" },
{ url = "https://files.pythonhosted.org/packages/4a/f0/b018fed0b599bd48d84c08794cb242227fe3341952da102ee9d9682db574/matplotlib-3.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:932c55d1fa7af4423422cb6a492a31cbcbdbe68fd1a9a3f545aa5e7a143b5355", size = 8316849, upload-time = "2025-10-09T00:27:10.254Z" },
{ url = "https://files.pythonhosted.org/packages/b0/b7/bb4f23856197659f275e11a2a164e36e65e9b48ea3e93c4ec25b4f163198/matplotlib-3.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e38c2d581d62ee729a6e144c47a71b3f42fb4187508dbbf4fe71d5612c3433b", size = 8178225, upload-time = "2025-10-09T00:27:12.241Z" },
{ url = "https://files.pythonhosted.org/packages/62/56/0600609893ff277e6f3ab3c0cef4eafa6e61006c058e84286c467223d4d5/matplotlib-3.10.7-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:786656bb13c237bbcebcd402f65f44dd61ead60ee3deb045af429d889c8dbc67", size = 8711708, upload-time = "2025-10-09T00:27:13.879Z" },
{ url = "https://files.pythonhosted.org/packages/d8/1a/6bfecb0cafe94d6658f2f1af22c43b76cf7a1c2f0dc34ef84cbb6809617e/matplotlib-3.10.7-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09d7945a70ea43bf9248f4b6582734c2fe726723204a76eca233f24cffc7ef67", size = 9541409, upload-time = "2025-10-09T00:27:15.684Z" },
{ url = "https://files.pythonhosted.org/packages/08/50/95122a407d7f2e446fd865e2388a232a23f2b81934960ea802f3171518e4/matplotlib-3.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d0b181e9fa8daf1d9f2d4c547527b167cb8838fc587deabca7b5c01f97199e84", size = 9594054, upload-time = "2025-10-09T00:27:17.547Z" },
{ url = "https://files.pythonhosted.org/packages/13/76/75b194a43b81583478a81e78a07da8d9ca6ddf50dd0a2ccabf258059481d/matplotlib-3.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:31963603041634ce1a96053047b40961f7a29eb8f9a62e80cc2c0427aa1d22a2", size = 8200100, upload-time = "2025-10-09T00:27:20.039Z" },
{ url = "https://files.pythonhosted.org/packages/f5/9e/6aefebdc9f8235c12bdeeda44cc0383d89c1e41da2c400caf3ee2073a3ce/matplotlib-3.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:aebed7b50aa6ac698c90f60f854b47e48cd2252b30510e7a1feddaf5a3f72cbf", size = 8042131, upload-time = "2025-10-09T00:27:21.608Z" },
{ url = "https://files.pythonhosted.org/packages/0d/4b/e5bc2c321b6a7e3a75638d937d19ea267c34bd5a90e12bee76c4d7c7a0d9/matplotlib-3.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d883460c43e8c6b173fef244a2341f7f7c0e9725c7fe68306e8e44ed9c8fb100", size = 8273787, upload-time = "2025-10-09T00:27:23.27Z" },
{ url = "https://files.pythonhosted.org/packages/86/ad/6efae459c56c2fbc404da154e13e3a6039129f3c942b0152624f1c621f05/matplotlib-3.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07124afcf7a6504eafcb8ce94091c5898bbdd351519a1beb5c45f7a38c67e77f", size = 8131348, upload-time = "2025-10-09T00:27:24.926Z" },
{ url = "https://files.pythonhosted.org/packages/a6/5a/a4284d2958dee4116359cc05d7e19c057e64ece1b4ac986ab0f2f4d52d5a/matplotlib-3.10.7-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c17398b709a6cce3d9fdb1595c33e356d91c098cd9486cb2cc21ea2ea418e715", size = 9533949, upload-time = "2025-10-09T00:27:26.704Z" },
{ url = "https://files.pythonhosted.org/packages/de/ff/f3781b5057fa3786623ad8976fc9f7b0d02b2f28534751fd5a44240de4cf/matplotlib-3.10.7-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7146d64f561498764561e9cd0ed64fcf582e570fc519e6f521e2d0cfd43365e1", size = 9804247, upload-time = "2025-10-09T00:27:28.514Z" },
{ url = "https://files.pythonhosted.org/packages/47/5a/993a59facb8444efb0e197bf55f545ee449902dcee86a4dfc580c3b61314/matplotlib-3.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90ad854c0a435da3104c01e2c6f0028d7e719b690998a2333d7218db80950722", size = 9595497, upload-time = "2025-10-09T00:27:30.418Z" },
{ url = "https://files.pythonhosted.org/packages/0d/a5/77c95aaa9bb32c345cbb49626ad8eb15550cba2e6d4c88081a6c2ac7b08d/matplotlib-3.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:4645fc5d9d20ffa3a39361fcdbcec731382763b623b72627806bf251b6388866", size = 8252732, upload-time = "2025-10-09T00:27:32.332Z" },
{ url = "https://files.pythonhosted.org/packages/74/04/45d269b4268d222390d7817dae77b159651909669a34ee9fdee336db5883/matplotlib-3.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:9257be2f2a03415f9105c486d304a321168e61ad450f6153d77c69504ad764bb", size = 8124240, upload-time = "2025-10-09T00:27:33.94Z" },
{ url = "https://files.pythonhosted.org/packages/4b/c7/ca01c607bb827158b439208c153d6f14ddb9fb640768f06f7ca3488ae67b/matplotlib-3.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1e4bbad66c177a8fdfa53972e5ef8be72a5f27e6a607cec0d8579abd0f3102b1", size = 8316938, upload-time = "2025-10-09T00:27:35.534Z" },
{ url = "https://files.pythonhosted.org/packages/84/d2/5539e66e9f56d2fdec94bb8436f5e449683b4e199bcc897c44fbe3c99e28/matplotlib-3.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d8eb7194b084b12feb19142262165832fc6ee879b945491d1c3d4660748020c4", size = 8178245, upload-time = "2025-10-09T00:27:37.334Z" },
{ url = "https://files.pythonhosted.org/packages/77/b5/e6ca22901fd3e4fe433a82e583436dd872f6c966fca7e63cf806b40356f8/matplotlib-3.10.7-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d41379b05528091f00e1728004f9a8d7191260f3862178b88e8fd770206318", size = 9541411, upload-time = "2025-10-09T00:27:39.387Z" },
{ url = "https://files.pythonhosted.org/packages/9e/99/a4524db57cad8fee54b7237239a8f8360bfcfa3170d37c9e71c090c0f409/matplotlib-3.10.7-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a74f79fafb2e177f240579bc83f0b60f82cc47d2f1d260f422a0627207008ca", size = 9803664, upload-time = "2025-10-09T00:27:41.492Z" },
{ url = "https://files.pythonhosted.org/packages/e6/a5/85e2edf76ea0ad4288d174926d9454ea85f3ce5390cc4e6fab196cbf250b/matplotlib-3.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:702590829c30aada1e8cef0568ddbffa77ca747b4d6e36c6d173f66e301f89cc", size = 9594066, upload-time = "2025-10-09T00:27:43.694Z" },
{ url = "https://files.pythonhosted.org/packages/39/69/9684368a314f6d83fe5c5ad2a4121a3a8e03723d2e5c8ea17b66c1bad0e7/matplotlib-3.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:f79d5de970fc90cd5591f60053aecfce1fcd736e0303d9f0bf86be649fa68fb8", size = 8342832, upload-time = "2025-10-09T00:27:45.543Z" },
{ url = "https://files.pythonhosted.org/packages/04/5f/e22e08da14bc1a0894184640d47819d2338b792732e20d292bf86e5ab785/matplotlib-3.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:cb783436e47fcf82064baca52ce748af71725d0352e1d31564cbe9c95df92b9c", size = 8172585, upload-time = "2025-10-09T00:27:47.185Z" },
{ url = "https://files.pythonhosted.org/packages/58/8f/76d5dc21ac64a49e5498d7f0472c0781dae442dd266a67458baec38288ec/matplotlib-3.10.7-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:15112bcbaef211bd663fa935ec33313b948e214454d949b723998a43357b17b0", size = 8252283, upload-time = "2025-10-09T00:27:54.739Z" },
{ url = "https://files.pythonhosted.org/packages/27/0d/9c5d4c2317feb31d819e38c9f947c942f42ebd4eb935fc6fd3518a11eaa7/matplotlib-3.10.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d2a959c640cdeecdd2ec3136e8ea0441da59bcaf58d67e9c590740addba2cb68", size = 8116733, upload-time = "2025-10-09T00:27:56.406Z" },
{ url = "https://files.pythonhosted.org/packages/9a/cc/3fe688ff1355010937713164caacf9ed443675ac48a997bab6ed23b3f7c0/matplotlib-3.10.7-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3886e47f64611046bc1db523a09dd0a0a6bed6081e6f90e13806dd1d1d1b5e91", size = 8693919, upload-time = "2025-10-09T00:27:58.41Z" },
]
[[package]] [[package]]
name = "nodeenv" name = "nodeenv"
version = "1.9.1" version = "1.9.1"
@ -486,147 +172,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
] ]
[[package]]
name = "pandas"
version = "2.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
{ name = "python-dateutil" },
{ name = "pytz" },
{ name = "tzdata" },
]
sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" },
{ url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" },
{ url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" },
{ url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" },
{ url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" },
{ url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" },
{ url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" },
{ url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" },
{ url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" },
{ url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" },
{ url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" },
{ url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" },
{ url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" },
{ url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" },
{ url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" },
{ url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" },
{ url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" },
{ url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" },
{ url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" },
{ url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" },
{ url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" },
{ url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" },
{ url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" },
{ url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" },
{ url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" },
{ url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" },
{ url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" },
{ url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" },
{ url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" },
{ url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" },
{ url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" },
{ url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" },
{ url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" },
{ url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" },
{ url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" },
{ url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" },
{ url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" },
{ url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" },
{ url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" },
{ url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" },
]
[[package]]
name = "pillow"
version = "12.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/5a/a2f6773b64edb921a756eb0729068acad9fc5208a53f4a349396e9436721/pillow-12.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc", size = 5289798, upload-time = "2025-10-15T18:21:47.763Z" },
{ url = "https://files.pythonhosted.org/packages/2e/05/069b1f8a2e4b5a37493da6c5868531c3f77b85e716ad7a590ef87d58730d/pillow-12.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257", size = 4650589, upload-time = "2025-10-15T18:21:49.515Z" },
{ url = "https://files.pythonhosted.org/packages/61/e3/2c820d6e9a36432503ead175ae294f96861b07600a7156154a086ba7111a/pillow-12.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642", size = 6230472, upload-time = "2025-10-15T18:21:51.052Z" },
{ url = "https://files.pythonhosted.org/packages/4f/89/63427f51c64209c5e23d4d52071c8d0f21024d3a8a487737caaf614a5795/pillow-12.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3", size = 8033887, upload-time = "2025-10-15T18:21:52.604Z" },
{ url = "https://files.pythonhosted.org/packages/f6/1b/c9711318d4901093c15840f268ad649459cd81984c9ec9887756cca049a5/pillow-12.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c", size = 6343964, upload-time = "2025-10-15T18:21:54.619Z" },
{ url = "https://files.pythonhosted.org/packages/41/1e/db9470f2d030b4995083044cd8738cdd1bf773106819f6d8ba12597d5352/pillow-12.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227", size = 7034756, upload-time = "2025-10-15T18:21:56.151Z" },
{ url = "https://files.pythonhosted.org/packages/cc/b0/6177a8bdd5ee4ed87cba2de5a3cc1db55ffbbec6176784ce5bb75aa96798/pillow-12.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b", size = 6458075, upload-time = "2025-10-15T18:21:57.759Z" },
{ url = "https://files.pythonhosted.org/packages/bc/5e/61537aa6fa977922c6a03253a0e727e6e4a72381a80d63ad8eec350684f2/pillow-12.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e", size = 7125955, upload-time = "2025-10-15T18:21:59.372Z" },
{ url = "https://files.pythonhosted.org/packages/1f/3d/d5033539344ee3cbd9a4d69e12e63ca3a44a739eb2d4c8da350a3d38edd7/pillow-12.0.0-cp311-cp311-win32.whl", hash = "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739", size = 6298440, upload-time = "2025-10-15T18:22:00.982Z" },
{ url = "https://files.pythonhosted.org/packages/4d/42/aaca386de5cc8bd8a0254516957c1f265e3521c91515b16e286c662854c4/pillow-12.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e", size = 6999256, upload-time = "2025-10-15T18:22:02.617Z" },
{ url = "https://files.pythonhosted.org/packages/ba/f1/9197c9c2d5708b785f631a6dfbfa8eb3fb9672837cb92ae9af812c13b4ed/pillow-12.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d", size = 2436025, upload-time = "2025-10-15T18:22:04.598Z" },
{ url = "https://files.pythonhosted.org/packages/2c/90/4fcce2c22caf044e660a198d740e7fbc14395619e3cb1abad12192c0826c/pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371", size = 5249377, upload-time = "2025-10-15T18:22:05.993Z" },
{ url = "https://files.pythonhosted.org/packages/fd/e0/ed960067543d080691d47d6938ebccbf3976a931c9567ab2fbfab983a5dd/pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082", size = 4650343, upload-time = "2025-10-15T18:22:07.718Z" },
{ url = "https://files.pythonhosted.org/packages/e7/a1/f81fdeddcb99c044bf7d6faa47e12850f13cee0849537a7d27eeab5534d4/pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f", size = 6232981, upload-time = "2025-10-15T18:22:09.287Z" },
{ url = "https://files.pythonhosted.org/packages/88/e1/9098d3ce341a8750b55b0e00c03f1630d6178f38ac191c81c97a3b047b44/pillow-12.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d", size = 8041399, upload-time = "2025-10-15T18:22:10.872Z" },
{ url = "https://files.pythonhosted.org/packages/a7/62/a22e8d3b602ae8cc01446d0c57a54e982737f44b6f2e1e019a925143771d/pillow-12.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953", size = 6347740, upload-time = "2025-10-15T18:22:12.769Z" },
{ url = "https://files.pythonhosted.org/packages/4f/87/424511bdcd02c8d7acf9f65caa09f291a519b16bd83c3fb3374b3d4ae951/pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8", size = 7040201, upload-time = "2025-10-15T18:22:14.813Z" },
{ url = "https://files.pythonhosted.org/packages/dc/4d/435c8ac688c54d11755aedfdd9f29c9eeddf68d150fe42d1d3dbd2365149/pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79", size = 6462334, upload-time = "2025-10-15T18:22:16.375Z" },
{ url = "https://files.pythonhosted.org/packages/2b/f2/ad34167a8059a59b8ad10bc5c72d4d9b35acc6b7c0877af8ac885b5f2044/pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba", size = 7134162, upload-time = "2025-10-15T18:22:17.996Z" },
{ url = "https://files.pythonhosted.org/packages/0c/b1/a7391df6adacf0a5c2cf6ac1cf1fcc1369e7d439d28f637a847f8803beb3/pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0", size = 6298769, upload-time = "2025-10-15T18:22:19.923Z" },
{ url = "https://files.pythonhosted.org/packages/a2/0b/d87733741526541c909bbf159e338dcace4f982daac6e5a8d6be225ca32d/pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a", size = 7001107, upload-time = "2025-10-15T18:22:21.644Z" },
{ url = "https://files.pythonhosted.org/packages/bc/96/aaa61ce33cc98421fb6088af2a03be4157b1e7e0e87087c888e2370a7f45/pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad", size = 2436012, upload-time = "2025-10-15T18:22:23.621Z" },
{ url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" },
{ url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" },
{ url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" },
{ url = "https://files.pythonhosted.org/packages/a4/a4/a0a31467e3f83b94d37568294b01d22b43ae3c5d85f2811769b9c66389dd/pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", size = 5249132, upload-time = "2025-10-15T18:22:30.641Z" },
{ url = "https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", size = 4650099, upload-time = "2025-10-15T18:22:32.73Z" },
{ url = "https://files.pythonhosted.org/packages/fc/bd/69ed99fd46a8dba7c1887156d3572fe4484e3f031405fcc5a92e31c04035/pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", size = 6230808, upload-time = "2025-10-15T18:22:34.337Z" },
{ url = "https://files.pythonhosted.org/packages/ea/94/8fad659bcdbf86ed70099cb60ae40be6acca434bbc8c4c0d4ef356d7e0de/pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", size = 8037804, upload-time = "2025-10-15T18:22:36.402Z" },
{ url = "https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", size = 6345553, upload-time = "2025-10-15T18:22:38.066Z" },
{ url = "https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", size = 7037729, upload-time = "2025-10-15T18:22:39.769Z" },
{ url = "https://files.pythonhosted.org/packages/ca/b6/7e94f4c41d238615674d06ed677c14883103dce1c52e4af16f000338cfd7/pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", size = 6459789, upload-time = "2025-10-15T18:22:41.437Z" },
{ url = "https://files.pythonhosted.org/packages/9c/14/4448bb0b5e0f22dd865290536d20ec8a23b64e2d04280b89139f09a36bb6/pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", size = 7130917, upload-time = "2025-10-15T18:22:43.152Z" },
{ url = "https://files.pythonhosted.org/packages/dd/ca/16c6926cc1c015845745d5c16c9358e24282f1e588237a4c36d2b30f182f/pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", size = 6302391, upload-time = "2025-10-15T18:22:44.753Z" },
{ url = "https://files.pythonhosted.org/packages/6d/2a/dd43dcfd6dae9b6a49ee28a8eedb98c7d5ff2de94a5d834565164667b97b/pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", size = 7007477, upload-time = "2025-10-15T18:22:46.838Z" },
{ url = "https://files.pythonhosted.org/packages/77/f0/72ea067f4b5ae5ead653053212af05ce3705807906ba3f3e8f58ddf617e6/pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", size = 2435918, upload-time = "2025-10-15T18:22:48.399Z" },
{ url = "https://files.pythonhosted.org/packages/f5/5e/9046b423735c21f0487ea6cb5b10f89ea8f8dfbe32576fe052b5ba9d4e5b/pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", size = 5251406, upload-time = "2025-10-15T18:22:49.905Z" },
{ url = "https://files.pythonhosted.org/packages/12/66/982ceebcdb13c97270ef7a56c3969635b4ee7cd45227fa707c94719229c5/pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", size = 4653218, upload-time = "2025-10-15T18:22:51.587Z" },
{ url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" },
{ url = "https://files.pythonhosted.org/packages/98/59/dfb38f2a41240d2408096e1a76c671d0a105a4a8471b1871c6902719450c/pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", size = 8069260, upload-time = "2025-10-15T18:22:54.933Z" },
{ url = "https://files.pythonhosted.org/packages/dc/3d/378dbea5cd1874b94c312425ca77b0f47776c78e0df2df751b820c8c1d6c/pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", size = 6379248, upload-time = "2025-10-15T18:22:56.605Z" },
{ url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f1621510327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" },
{ url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" },
{ url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" },
{ url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" },
{ url = "https://files.pythonhosted.org/packages/c0/3d/2afaf4e840b2df71344ababf2f8edd75a705ce500e5dc1e7227808312ae1/pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", size = 7013165, upload-time = "2025-10-15T18:23:06.46Z" },
{ url = "https://files.pythonhosted.org/packages/6f/75/3fa09aa5cf6ed04bee3fa575798ddf1ce0bace8edb47249c798077a81f7f/pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", size = 2437834, upload-time = "2025-10-15T18:23:08.194Z" },
{ url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" },
{ url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" },
{ url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" },
{ url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" },
{ url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" },
{ url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" },
{ url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" },
{ url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" },
{ url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" },
{ url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" },
{ url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" },
{ url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" },
{ url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" },
{ url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" },
{ url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" },
{ url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" },
{ url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" },
{ url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" },
{ url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" },
{ url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" },
{ url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" },
{ url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" },
{ url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" },
{ url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" },
{ url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" },
{ url = "https://files.pythonhosted.org/packages/1d/b3/582327e6c9f86d037b63beebe981425d6811104cb443e8193824ef1a2f27/pillow-12.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8", size = 5215068, upload-time = "2025-10-15T18:23:59.594Z" },
{ url = "https://files.pythonhosted.org/packages/fd/d6/67748211d119f3b6540baf90f92fae73ae51d5217b171b0e8b5f7e5d558f/pillow-12.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a", size = 4614994, upload-time = "2025-10-15T18:24:01.669Z" },
{ url = "https://files.pythonhosted.org/packages/2d/e1/f8281e5d844c41872b273b9f2c34a4bf64ca08905668c8ae730eedc7c9fa/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197", size = 5246639, upload-time = "2025-10-15T18:24:03.403Z" },
{ url = "https://files.pythonhosted.org/packages/94/5a/0d8ab8ffe8a102ff5df60d0de5af309015163bf710c7bb3e8311dd3b3ad0/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c", size = 6986839, upload-time = "2025-10-15T18:24:05.344Z" },
{ url = "https://files.pythonhosted.org/packages/20/2e/3434380e8110b76cd9eb00a363c484b050f949b4bbe84ba770bb8508a02c/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e", size = 5313505, upload-time = "2025-10-15T18:24:07.137Z" },
{ url = "https://files.pythonhosted.org/packages/57/ca/5a9d38900d9d74785141d6580950fe705de68af735ff6e727cb911b64740/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76", size = 5963654, upload-time = "2025-10-15T18:24:09.579Z" },
{ url = "https://files.pythonhosted.org/packages/95/7e/f896623c3c635a90537ac093c6a618ebe1a90d87206e42309cb5d98a1b9e/pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5", size = 6997850, upload-time = "2025-10-15T18:24:11.495Z" },
]
[[package]] [[package]]
name = "platformdirs" name = "platformdirs"
version = "4.3.8" version = "4.3.8"
@ -661,21 +206,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" }, { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" },
] ]
[[package]]
name = "psutil"
version = "7.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" },
{ url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" },
{ url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" },
{ url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" },
{ url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" },
{ url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" },
{ url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" },
]
[[package]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.11.5" version = "2.11.5"
@ -812,19 +342,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/19/7f9a37b7ff55dc34a8f727b86b89800a1fdb4b1436e601dea66f89764472/pygame_ce-2.5.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e37bfd4545fb057ce24d06e13d1262989ca0ece3b12010c585130607e3c2bbf8", size = 12463330, upload-time = "2025-06-07T07:32:10.136Z" }, { url = "https://files.pythonhosted.org/packages/d7/19/7f9a37b7ff55dc34a8f727b86b89800a1fdb4b1436e601dea66f89764472/pygame_ce-2.5.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e37bfd4545fb057ce24d06e13d1262989ca0ece3b12010c585130607e3c2bbf8", size = 12463330, upload-time = "2025-06-07T07:32:10.136Z" },
{ url = "https://files.pythonhosted.org/packages/fa/f5/5ad50c34f042bbc135312cd75d86d156bf18f54b72ae8947498acbda8cbd/pygame_ce-2.5.5-cp313-cp313-win32.whl", hash = "sha256:476a1b56b19f5023ddd0512716f11c413c3587b93dfd4aebd40869f261d3b8b7", size = 9799763, upload-time = "2025-06-07T07:32:12.332Z" }, { url = "https://files.pythonhosted.org/packages/fa/f5/5ad50c34f042bbc135312cd75d86d156bf18f54b72ae8947498acbda8cbd/pygame_ce-2.5.5-cp313-cp313-win32.whl", hash = "sha256:476a1b56b19f5023ddd0512716f11c413c3587b93dfd4aebd40869f261d3b8b7", size = 9799763, upload-time = "2025-06-07T07:32:12.332Z" },
{ url = "https://files.pythonhosted.org/packages/a8/88/89cfcaf55c8ccab5a2d59f206bf7b7d4336c4b27d9b63531a0e274cac817/pygame_ce-2.5.5-cp313-cp313-win_amd64.whl", hash = "sha256:8568fab6d43e23ca209fb860f7d387f2f89bd4047a4fa617ed0951fd9739109c", size = 10376053, upload-time = "2025-06-07T07:32:14.526Z" }, { url = "https://files.pythonhosted.org/packages/a8/88/89cfcaf55c8ccab5a2d59f206bf7b7d4336c4b27d9b63531a0e274cac817/pygame_ce-2.5.5-cp313-cp313-win_amd64.whl", hash = "sha256:8568fab6d43e23ca209fb860f7d387f2f89bd4047a4fa617ed0951fd9739109c", size = 10376053, upload-time = "2025-06-07T07:32:14.526Z" },
{ url = "https://files.pythonhosted.org/packages/09/54/351e6cc0b389cfd6ae97c999bf1beee23269823e5c4f2d700a7b507642aa/pygame_ce-2.5.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:5fdaba786e3fd77315dcdf240d1d24119c51ce803c31e8d1362cc938cea75570", size = 12374606, upload-time = "2025-07-28T07:48:04.566Z" },
{ url = "https://files.pythonhosted.org/packages/e7/e5/ba180f96353e0394ac14b6d8549b44cc2bb509b0e71dc42d1800fc605843/pygame_ce-2.5.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:518768ce311ae56ac43059307f18a07768a106e8fbed365f747e2558dba7a4b4", size = 11624585, upload-time = "2025-07-28T07:48:14.375Z" },
{ url = "https://files.pythonhosted.org/packages/0c/49/449d7fb46a2fe8cea8d0863c4aec0dc1ad6a38a97e2ae1001c0a0c51dbee/pygame_ce-2.5.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18b683f1a14d2d537f1c3e7749c156ead905521a3089b4106a7ffb1e5c8f7922", size = 12267397, upload-time = "2025-07-28T07:48:24.171Z" },
{ url = "https://files.pythonhosted.org/packages/df/df/6d67658f79b9127d6f5750cbaa521372c0f62554e4cf6791ecffd10a1f1e/pygame_ce-2.5.5-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:33a6e579061bd74ce31d23148b631b00cd0ed71eeea06c0a852c8a29552315db", size = 12793784, upload-time = "2025-07-28T07:48:34.4Z" },
{ url = "https://files.pythonhosted.org/packages/f0/40/e7c47b0e296a181900e2ab24ef87e2e922ec08f141ab35d0433917f7c5b6/pygame_ce-2.5.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6f50a43ca19e2e060e79ab53f8a9db9c7e3388b39c0015e56e30f17feab6c37f", size = 12436752, upload-time = "2025-07-28T07:48:44.357Z" },
{ url = "https://files.pythonhosted.org/packages/b6/48/036295f5340261d725e75ccf3851d22d18dcedf9627f8e77856e8ba14ad7/pygame_ce-2.5.5-cp314-cp314-win32.whl", hash = "sha256:2515a5d07a856b61871bc47f5c88d9cbb50d81013de865d7cc3a1d3648b4fef9", size = 9904668, upload-time = "2025-07-28T07:48:52.776Z" },
{ url = "https://files.pythonhosted.org/packages/fc/51/908716125a12272494953e4c34abd178b54870611e015078b0238531b90c/pygame_ce-2.5.5-cp314-cp314-win_amd64.whl", hash = "sha256:7f3bafbc6007c96d42f305e2ed7b2ad899ed6aa97546b1457bd50c39fd764dbf", size = 10516298, upload-time = "2025-07-28T07:49:00.866Z" },
{ url = "https://files.pythonhosted.org/packages/e1/82/e90170b3598168f89ae0a8336f8e65a79c21d1f66562662884e036191148/pygame_ce-2.5.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:68c57dc55a597c19d7321225aedf888597859947f0cdc90acf601acce5152178", size = 12333826, upload-time = "2025-07-28T07:49:10.406Z" },
{ url = "https://files.pythonhosted.org/packages/8d/a8/88da9df964890d01dc9f3349f3871a1174007b71fc1baf82394b8d527d7a/pygame_ce-2.5.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b901cf9359d77b924bc2885867fe426672f57223f284f79cf69b0c959245d66e", size = 11593010, upload-time = "2025-07-28T07:49:20.14Z" },
{ url = "https://files.pythonhosted.org/packages/ef/7c/aaa32c00d28106545b0160fd38ca4e4dbfeb926fda65d6310c54bbff2c59/pygame_ce-2.5.5-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0d84df032875bd6a0d040e3cb9273db6121093db9ef2ab4b09747b7000dd2c79", size = 12227010, upload-time = "2025-07-28T07:49:29.599Z" },
{ url = "https://files.pythonhosted.org/packages/1c/45/f7de742f0892fb67fcd550b3a19cdd084ee11e5bdc9314bdefc058d96f12/pygame_ce-2.5.5-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:5f6c99999bd2e5c7f33e472b1154e6e72b340458ce4d2a61d39d4bb180224bf9", size = 12744365, upload-time = "2025-07-28T07:49:39.758Z" },
{ url = "https://files.pythonhosted.org/packages/48/4f/587f55265edb1994e3a03aa29f261d3e57daf4b4b826b8734bf0ceb1ddab/pygame_ce-2.5.5-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15bae81bff1750dfe0f087f1ba6a4395d9154bcb962118c87cc35da74d116b0a", size = 12392705, upload-time = "2025-07-28T07:49:49.53Z" },
{ url = "https://files.pythonhosted.org/packages/39/42/919e9acaf40b5828edd5f039becb9c0594e2d0846a0100bcb26fd96f72b7/pygame_ce-2.5.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3f106be1d9daaf0f44c9b588a01efb6e520fe52de3da7332bc97eaa950e751ff", size = 10311535, upload-time = "2025-07-28T07:49:57.804Z" },
] ]
[[package]] [[package]]
@ -839,15 +356,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/29/bf/d2589b06e39a6588480e6313ab530a6073a4ac6734d0f48a1883a5c46236/pygame_gui-0.6.14-py2.py3-none-any.whl", hash = "sha256:e351e88fab01756af6338d071c3cf6ce832a90c3b9f7db4fcb7b5216d5634482", size = 30896869, upload-time = "2025-05-30T18:25:53.938Z" }, { url = "https://files.pythonhosted.org/packages/29/bf/d2589b06e39a6588480e6313ab530a6073a4ac6734d0f48a1883a5c46236/pygame_gui-0.6.14-py2.py3-none-any.whl", hash = "sha256:e351e88fab01756af6338d071c3cf6ce832a90c3b9f7db4fcb7b5216d5634482", size = 30896869, upload-time = "2025-05-30T18:25:53.938Z" },
] ]
[[package]]
name = "pyparsing"
version = "3.2.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/181488fc2b9d093e3972d2a472855aae8a03f000592dbfce716a512b3359/pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6", size = 1099274, upload-time = "2025-09-21T04:11:06.277Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" },
]
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "8.3.5" version = "8.3.5"
@ -863,18 +371,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" },
] ]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
]
[[package]] [[package]]
name = "python-i18n" name = "python-i18n"
version = "0.3.9" version = "0.3.9"
@ -884,15 +380,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/73/9a0b2974dd9a3d50788d235f10c4d73c2efcd22926036309645fc2f0db0c/python_i18n-0.3.9-py3-none-any.whl", hash = "sha256:bda5b8d889ebd51973e22e53746417bd32783c9bd6780fd27cadbb733915651d", size = 13750, upload-time = "2020-08-26T14:31:26.266Z" }, { url = "https://files.pythonhosted.org/packages/5a/73/9a0b2974dd9a3d50788d235f10c4d73c2efcd22926036309645fc2f0db0c/python_i18n-0.3.9-py3-none-any.whl", hash = "sha256:bda5b8d889ebd51973e22e53746417bd32783c9bd6780fd27cadbb733915651d", size = 13750, upload-time = "2020-08-26T14:31:26.266Z" },
] ]
[[package]]
name = "pytz"
version = "2025.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
]
[[package]] [[package]]
name = "pyyaml" name = "pyyaml"
version = "6.0.2" version = "6.0.2"
@ -953,58 +440,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/44/42/d58086ec20f52d2b0140752ae54b355ea2be2ed46f914231136dd1effcc7/ruff-0.11.12-py3-none-win_arm64.whl", hash = "sha256:65194e37853158d368e333ba282217941029a28ea90913c67e558c611d04daa5", size = 10697770, upload-time = "2025-05-29T13:31:38.009Z" }, { url = "https://files.pythonhosted.org/packages/44/42/d58086ec20f52d2b0140752ae54b355ea2be2ed46f914231136dd1effcc7/ruff-0.11.12-py3-none-win_arm64.whl", hash = "sha256:65194e37853158d368e333ba282217941029a28ea90913c67e558c611d04daa5", size = 10697770, upload-time = "2025-05-29T13:31:38.009Z" },
] ]
[[package]]
name = "six"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
[[package]]
name = "snakeviz"
version = "2.2.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "tornado" },
]
sdist = { url = "https://files.pythonhosted.org/packages/04/06/82f56563b16d33c2586ac2615a3034a83a4ff1969b84c8d79339e5d07d73/snakeviz-2.2.2.tar.gz", hash = "sha256:08028c6f8e34a032ff14757a38424770abb8662fb2818985aeea0d9bc13a7d83", size = 182039, upload-time = "2024-11-09T22:03:58.99Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cd/f7/83b00cdf4f114f10750a18b64c27dc34636d0ac990ccac98282f5c0fbb43/snakeviz-2.2.2-py3-none-any.whl", hash = "sha256:77e7b9c82f6152edc330040319b97612351cd9b48c706434c535c2df31d10ac5", size = 183477, upload-time = "2024-11-09T22:03:57.049Z" },
]
[[package]]
name = "tornado"
version = "6.5.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/51/89/c72771c81d25d53fe33e3dca61c233b665b2780f21820ba6fd2c6793c12b/tornado-6.5.1.tar.gz", hash = "sha256:84ceece391e8eb9b2b95578db65e920d2a61070260594819589609ba9bc6308c", size = 509934, upload-time = "2025-05-22T18:15:38.788Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/77/89/f4532dee6843c9e0ebc4e28d4be04c67f54f60813e4bf73d595fe7567452/tornado-6.5.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d50065ba7fd11d3bd41bcad0825227cc9a95154bad83239357094c36708001f7", size = 441948, upload-time = "2025-05-22T18:15:20.862Z" },
{ url = "https://files.pythonhosted.org/packages/15/9a/557406b62cffa395d18772e0cdcf03bed2fff03b374677348eef9f6a3792/tornado-6.5.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9e9ca370f717997cb85606d074b0e5b247282cf5e2e1611568b8821afe0342d6", size = 440112, upload-time = "2025-05-22T18:15:22.591Z" },
{ url = "https://files.pythonhosted.org/packages/55/82/7721b7319013a3cf881f4dffa4f60ceff07b31b394e459984e7a36dc99ec/tornado-6.5.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b77e9dfa7ed69754a54c89d82ef746398be82f749df69c4d3abe75c4d1ff4888", size = 443672, upload-time = "2025-05-22T18:15:24.027Z" },
{ url = "https://files.pythonhosted.org/packages/7d/42/d11c4376e7d101171b94e03cef0cbce43e823ed6567ceda571f54cf6e3ce/tornado-6.5.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:253b76040ee3bab8bcf7ba9feb136436a3787208717a1fb9f2c16b744fba7331", size = 443019, upload-time = "2025-05-22T18:15:25.735Z" },
{ url = "https://files.pythonhosted.org/packages/7d/f7/0c48ba992d875521ac761e6e04b0a1750f8150ae42ea26df1852d6a98942/tornado-6.5.1-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:308473f4cc5a76227157cdf904de33ac268af770b2c5f05ca6c1161d82fdd95e", size = 443252, upload-time = "2025-05-22T18:15:27.499Z" },
{ url = "https://files.pythonhosted.org/packages/89/46/d8d7413d11987e316df4ad42e16023cd62666a3c0dfa1518ffa30b8df06c/tornado-6.5.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:caec6314ce8a81cf69bd89909f4b633b9f523834dc1a352021775d45e51d9401", size = 443930, upload-time = "2025-05-22T18:15:29.299Z" },
{ url = "https://files.pythonhosted.org/packages/78/b2/f8049221c96a06df89bed68260e8ca94beca5ea532ffc63b1175ad31f9cc/tornado-6.5.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:13ce6e3396c24e2808774741331638ee6c2f50b114b97a55c5b442df65fd9692", size = 443351, upload-time = "2025-05-22T18:15:31.038Z" },
{ url = "https://files.pythonhosted.org/packages/76/ff/6a0079e65b326cc222a54720a748e04a4db246870c4da54ece4577bfa702/tornado-6.5.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5cae6145f4cdf5ab24744526cc0f55a17d76f02c98f4cff9daa08ae9a217448a", size = 443328, upload-time = "2025-05-22T18:15:32.426Z" },
{ url = "https://files.pythonhosted.org/packages/49/18/e3f902a1d21f14035b5bc6246a8c0f51e0eef562ace3a2cea403c1fb7021/tornado-6.5.1-cp39-abi3-win32.whl", hash = "sha256:e0a36e1bc684dca10b1aa75a31df8bdfed656831489bc1e6a6ebed05dc1ec365", size = 444396, upload-time = "2025-05-22T18:15:34.205Z" },
{ url = "https://files.pythonhosted.org/packages/7b/09/6526e32bf1049ee7de3bebba81572673b19a2a8541f795d887e92af1a8bc/tornado-6.5.1-cp39-abi3-win_amd64.whl", hash = "sha256:908e7d64567cecd4c2b458075589a775063453aeb1d2a1853eedb806922f568b", size = 444840, upload-time = "2025-05-22T18:15:36.1Z" },
{ url = "https://files.pythonhosted.org/packages/55/a7/535c44c7bea4578e48281d83c615219f3ab19e6abc67625ef637c73987be/tornado-6.5.1-cp39-abi3-win_arm64.whl", hash = "sha256:02420a0eb7bf617257b9935e2b754d1b63897525d8a289c9d65690d580b4dcf7", size = 443596, upload-time = "2025-05-22T18:15:37.433Z" },
]
[[package]]
name = "tqdm"
version = "4.67.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.14.0" version = "4.14.0"
@ -1026,15 +461,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" },
] ]
[[package]]
name = "tzdata"
version = "2025.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
]
[[package]] [[package]]
name = "virtualenv" name = "virtualenv"
version = "20.31.2" version = "20.31.2"

View File

@ -2,13 +2,15 @@ import math
import random import random
from config.constants import MAX_VELOCITY, MAX_ACCELERATION, MAX_ROTATIONAL_VELOCITY, MAX_ANGULAR_ACCELERATION 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.base.brain import CellBrain
from world.behavioral import BehavioralModel
from world.world import Position, BaseEntity, Rotation from world.world import Position, BaseEntity, Rotation
import pygame import pygame
from typing import Optional, List, Any, Union
from world.utils import get_distance_between_objects from world.utils import get_distance_between_objects
from world.physics import Physics
from math import atan2, degrees
class DebugRenderObject(BaseEntity): class DebugRenderObject(BaseEntity):
@ -90,30 +92,14 @@ class FoodObject(BaseEntity):
self.max_visual_width: int = 8 self.max_visual_width: int = 8
self.decay: int = 0 self.decay: int = 0
self.decay_rate: int = 1 self.decay_rate: int = 1
self.max_decay = 400 self.max_decay = 200
self.interaction_radius: int = 50 self.interaction_radius: int = 50
self.neighbors: int = 0 self.neighbors: int = 0
self.claimed_this_tick = False # Track if food was claimed this tick
self.flags: dict[str, bool] = { self.flags: dict[str, bool] = {
"death": False, "death": False,
"can_interact": True, "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"]]: def tick(self, interactable: Optional[List[BaseEntity]] = None) -> Union["FoodObject", List["FoodObject"]]:
""" """
Updates the food object, increasing decay and flagging for death if decayed. Updates the food object, increasing decay and flagging for death if decayed.
@ -121,9 +107,6 @@ class FoodObject(BaseEntity):
:param interactable: List of nearby entities (unused). :param interactable: List of nearby entities (unused).
:return: Self :return: Self
""" """
# Reset claim at the beginning of each tick
self.reset_claim()
if interactable is None: if interactable is None:
interactable = [] interactable = []
@ -251,45 +234,15 @@ class DefaultCell(BaseEntity):
""" """
Cell object Cell object
""" """
def __init__(self, starting_position: Position, starting_rotation: Rotation, entity_config=None) -> None: def __init__(self, starting_position: Position, starting_rotation: Rotation) -> None:
""" """
Initializes the cell. Initializes the cell.
:param starting_position: The position of the object. :param starting_position: The position of the object.
:param entity_config: Configuration for entity hyperparameters.
""" """
super().__init__(starting_position, starting_rotation) 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.velocity: tuple[int, int] = (0, 0)
self.acceleration: tuple[int, int] = (0, 0) self.acceleration: tuple[int, int] = (0, 0)
@ -297,17 +250,18 @@ class DefaultCell(BaseEntity):
self.rotational_velocity: int = 0 self.rotational_velocity: int = 0
self.angular_acceleration: int = 0 self.angular_acceleration: int = 0
self.energy: int = 1000
self.behavioral_model: CellBrain = CellBrain() self.behavioral_model: CellBrain = CellBrain()
self.max_visual_width: int = 10
self.interaction_radius: int = 50
self.flags: dict[str, bool] = { self.flags: dict[str, bool] = {
"death": False, "death": False,
"can_interact": True, "can_interact": True,
} }
self.tick_count = 0 self.tick_count = 0
self.entity_config = entity_config
self.physics = Physics(self.drag_coefficient, self.drag_coefficient*1.5)
def set_brain(self, behavioral_model: CellBrain) -> None: def set_brain(self, behavioral_model: CellBrain) -> None:
@ -343,30 +297,27 @@ class DefaultCell(BaseEntity):
distance_to_food = get_distance_between_objects(self, food_object) distance_to_food = get_distance_between_objects(self, food_object)
if distance_to_food < self.max_visual_width and food_objects: if distance_to_food < self.max_visual_width and food_objects:
# Use atomic consumption to prevent race conditions self.energy += 110
if food_object.try_claim():
self.energy += self.food_energy_value
food_object.flag_for_death() food_object.flag_for_death()
return self return self
if self.energy >= self.reproduction_energy: if self.energy >= 1600:
# too much energy, split # too much energy, split
offspring = []
for _ in range(self.reproduction_count):
duplicate_x, duplicate_y = self.position.get_position() duplicate_x, duplicate_y = self.position.get_position()
duplicate_x += random.randint(-self.offspring_offset_range, self.offspring_offset_range) duplicate_x += random.randint(-self.max_visual_width, self.max_visual_width)
duplicate_y += random.randint(-self.offspring_offset_range, self.offspring_offset_range) duplicate_y += random.randint(-self.max_visual_width, self.max_visual_width)
new_cell = DefaultCell( duplicate_x_2, duplicate_y_2 = self.position.get_position()
Position(x=int(duplicate_x), y=int(duplicate_y)), duplicate_x_2 += random.randint(-self.max_visual_width, self.max_visual_width)
Rotation(angle=random.randint(0, 359)), duplicate_y_2 += random.randint(-self.max_visual_width, self.max_visual_width)
entity_config=getattr(self, 'entity_config', None)
)
new_cell.set_brain(self.behavioral_model.mutate(self.mutation_rate))
offspring.append(new_cell)
return offspring 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.4))
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.4))
return [new_cell, new_cell_2]
input_data = { input_data = {
"distance": distance_to_food, "distance": distance_to_food,
@ -377,12 +328,44 @@ class DefaultCell(BaseEntity):
output_data = self.behavioral_model.tick(input_data) output_data = self.behavioral_model.tick(input_data)
# everything below this point is physics simulation and needs to be extracted to a separate class
# clamp accelerations # clamp accelerations
output_data["linear_acceleration"] = max(-MAX_ACCELERATION, min(MAX_ACCELERATION, output_data["linear_acceleration"])) output_data["linear_acceleration"] = max(-MAX_ACCELERATION, min(MAX_ACCELERATION, output_data["linear_acceleration"]))
output_data["angular_acceleration"] = max(-MAX_ANGULAR_ACCELERATION, min(MAX_ANGULAR_ACCELERATION, output_data["angular_acceleration"])) output_data["angular_acceleration"] = max(-MAX_ANGULAR_ACCELERATION, min(MAX_ANGULAR_ACCELERATION, output_data["angular_acceleration"]))
# request physics data from Physics class # 2. Apply drag force
self.velocity, self.acceleration, self.rotational_velocity, self.angular_acceleration = self.physics.move(output_data["linear_acceleration"], output_data["angular_acceleration"], self.rotation.get_rotation()) drag_coefficient = 0.02
drag_x = -self.velocity[0] * drag_coefficient
drag_y = -self.velocity[1] * drag_coefficient
# 3. Combine all forces
total_linear_accel = output_data["linear_acceleration"]
total_linear_accel = max(-0.1, min(0.1, total_linear_accel))
# 4. Convert to world coordinates
x_component = total_linear_accel * math.cos(math.radians(self.rotation.get_rotation()))
y_component = total_linear_accel * math.sin(math.radians(self.rotation.get_rotation()))
# 5. Add drag to total acceleration
total_accel_x = x_component + drag_x
total_accel_y = y_component + drag_y
self.acceleration = (total_accel_x, total_accel_y)
rotational_drag = 0.05
self.angular_acceleration = output_data["angular_acceleration"] - self.rotational_velocity * rotational_drag
# tick acceleration
velocity_x = self.velocity[0] + self.acceleration[0]
velocity_y = self.velocity[1] + self.acceleration[1]
self.velocity = (velocity_x, velocity_y)
# # clamp velocity
speed = math.sqrt(self.velocity[0] ** 2 + self.velocity[1] ** 2)
if speed > MAX_VELOCITY:
scale = MAX_VELOCITY / speed
self.velocity = (self.velocity[0] * scale, self.velocity[1] * scale)
# tick velocity # tick velocity
x, y = self.position.get_position() x, y = self.position.get_position()
@ -391,12 +374,19 @@ class DefaultCell(BaseEntity):
self.position.set_position(x, y) self.position.set_position(x, y)
# tick rotational acceleration
self.angular_acceleration = output_data["angular_acceleration"]
self.rotational_velocity += self.angular_acceleration
# clamp rotational velocity
self.rotational_velocity = max(-MAX_ROTATIONAL_VELOCITY, min(MAX_ROTATIONAL_VELOCITY, self.rotational_velocity))
# tick rotational velocity # tick rotational velocity
self.rotation.set_rotation(self.rotation.get_rotation() + self.rotational_velocity) self.rotation.set_rotation(self.rotation.get_rotation() + self.rotational_velocity)
movement_cost = abs(output_data["angular_acceleration"]) + abs(output_data["linear_acceleration"]) movement_cost = abs(output_data["angular_acceleration"]) + abs(output_data["linear_acceleration"])
self.energy -= (self.behavioral_model.neural_network.network_cost * self.neural_network_complexity_cost) + self.energy_cost_base + (self.movement_cost * movement_cost) self.energy -= (self.behavioral_model.neural_network.network_cost * 0.01) + 1 + (0.5 * movement_cost)
return self return self

View File

@ -1,82 +0,0 @@
import math
from config.constants import MAX_VELOCITY, MAX_ROTATIONAL_VELOCITY
class Physics:
"""
Simulates basic 2D physics for an object, including linear and rotational motion
with drag effects.
"""
def __init__(self, drag_coefficient: float, rotational_drag: float):
"""
Initialize the Physics object.
Args:
drag_coefficient (float): Linear drag coefficient.
rotational_drag (float): Rotational drag coefficient.
"""
self.drag_coefficient: float = drag_coefficient
self.rotational_drag: float = rotational_drag
self.velocity: tuple[int, int] = (0, 0)
self.acceleration: tuple[int, int] = (0, 0)
self.rotational_velocity: int = 0
self.angular_acceleration: int = 0
def move(self, linear_acceleration: float, angular_acceleration: int, rotational_position):
"""
Update the object's velocity and acceleration based on input forces and drag.
Args:
linear_acceleration (float): The applied linear acceleration.
angular_acceleration (int): The applied angular acceleration.
rotational_position: The current rotational position in degrees.
Returns:
tuple: Updated (velocity, acceleration, rotational_velocity, angular_acceleration).
"""
# Apply drag force
drag_coefficient = self.drag_coefficient
drag_x = -self.velocity[0] * drag_coefficient
drag_y = -self.velocity[1] * drag_coefficient
# Combine all forces
total_linear_accel = linear_acceleration
total_linear_accel = max(-0.1, min(0.1, total_linear_accel))
# Convert to world coordinates
x_component = total_linear_accel * math.cos(math.radians(rotational_position))
y_component = total_linear_accel * math.sin(math.radians(rotational_position))
# Add drag to total acceleration
total_accel_x = x_component + drag_x
total_accel_y = y_component + drag_y
self.acceleration = (total_accel_x, total_accel_y)
# Apply drag force to angular acceleration
rotational_drag = self.rotational_drag
self.angular_acceleration = angular_acceleration - self.rotational_velocity * rotational_drag
# tick acceleration
velocity_x = self.velocity[0] + self.acceleration[0]
velocity_y = self.velocity[1] + self.acceleration[1]
self.velocity = (velocity_x, velocity_y)
# clamp velocity
speed = math.sqrt(self.velocity[0] ** 2 + self.velocity[1] ** 2)
if speed > MAX_VELOCITY:
scale = MAX_VELOCITY / speed
self.velocity = (self.velocity[0] * scale, self.velocity[1] * scale)
self.angular_acceleration = angular_acceleration
self.rotational_velocity += self.angular_acceleration
# clamp rotational velocity
self.rotational_velocity = max(-MAX_ROTATIONAL_VELOCITY, min(MAX_ROTATIONAL_VELOCITY, self.rotational_velocity))
return self.velocity, self.acceleration, self.rotational_velocity, self.angular_acceleration

View File

@ -1,6 +1,6 @@
from collections import defaultdict from collections import defaultdict
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import List, Dict, Tuple, Optional, Any, TypeVar from typing import List, Dict, Tuple, Optional, Any, TypeVar, Union
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
T = TypeVar("T", bound="BaseEntity") T = TypeVar("T", bound="BaseEntity")
@ -153,8 +153,6 @@ class World:
:param camera: The camera object for coordinate transformation. :param camera: The camera object for coordinate transformation.
:param screen: The Pygame screen surface. :param screen: The Pygame screen surface.
Time complexity: O(n), where n is the number of objects in the current buffer.
""" """
for obj_list in self.buffers[self.current_buffer].values(): for obj_list in self.buffers[self.current_buffer].values():
for obj in obj_list: for obj in obj_list:
@ -163,15 +161,10 @@ class World:
def tick_all(self) -> None: def tick_all(self) -> None:
""" """
Advances all objects in the world by one tick, updating their state and handling interactions. Advances all objects in the world by one tick, updating their state and handling interactions.
Time complexity: O(N + K) / O(N*M), where N is the number of objects in the current buffer,
K is the number of objects that can interact with each object, and M is number of objects in checked cells where C is the number of cells checked within the interaction radius.
""" """
next_buffer: int = 1 - self.current_buffer next_buffer: int = 1 - self.current_buffer
self.buffers[next_buffer].clear() self.buffers[next_buffer].clear()
# Food claims auto-reset in FoodObject.tick()
for obj_list in self.buffers[self.current_buffer].values(): for obj_list in self.buffers[self.current_buffer].values():
for obj in obj_list: for obj in obj_list:
if obj.flags["death"]: if obj.flags["death"]:
@ -180,9 +173,6 @@ class World:
interactable = self.query_objects_within_radius( interactable = self.query_objects_within_radius(
obj.position.x, obj.position.y, obj.interaction_radius obj.position.x, obj.position.y, obj.interaction_radius
) )
# Create defensive copy to prevent shared state corruption
interactable = interactable.copy()
if obj in interactable:
interactable.remove(obj) interactable.remove(obj)
new_obj = obj.tick(interactable) new_obj = obj.tick(interactable)
else: else:
@ -190,7 +180,7 @@ class World:
if new_obj is None: if new_obj is None:
continue continue
# reproduction code - buffer new entities for atomic state transition # reproduction code
if isinstance(new_obj, list): if isinstance(new_obj, list):
for item in new_obj: for item in new_obj:
if isinstance(item, BaseEntity): if isinstance(item, BaseEntity):
@ -199,8 +189,6 @@ class World:
else: else:
cell = self._hash_position(new_obj.position) cell = self._hash_position(new_obj.position)
self.buffers[next_buffer][cell].append(new_obj) self.buffers[next_buffer][cell].append(new_obj)
# Atomic buffer switch - all state changes become visible simultaneously
self.current_buffer = next_buffer self.current_buffer = next_buffer
def add_object(self, new_object: BaseEntity) -> None: def add_object(self, new_object: BaseEntity) -> None:
@ -220,8 +208,6 @@ class World:
:param y: Y coordinate of the center. :param y: Y coordinate of the center.
:param radius: Search radius. :param radius: Search radius.
:return: List of objects within the radius. :return: List of objects within the radius.
Time complexity: O(C * M) / O(N), where C is the number of cells checked within the radius and M is the number of objects in those cells.
""" """
result: List[BaseEntity] = [] result: List[BaseEntity] = []
cell_x, cell_y = int(x // self.partition_size), int(y // self.partition_size) cell_x, cell_y = int(x // self.partition_size), int(y // self.partition_size)
@ -248,8 +234,6 @@ class World:
:param x2: Maximum X coordinate. :param x2: Maximum X coordinate.
:param y2: Maximum Y coordinate. :param y2: Maximum Y coordinate.
:return: List of objects within the rectangle. :return: List of objects within the rectangle.
Time complexity: O(C * M) / O(N), where C is the number of cells checked within the rectangle and M is the number of objects in those cells.
""" """
result: List[BaseEntity] = [] result: List[BaseEntity] = []
cell_x1, cell_y1 = ( cell_x1, cell_y1 = (