Compare commits
No commits in common. "master" and "ui-rework" have entirely different histories.
@ -18,9 +18,6 @@ jobs:
|
||||
- name: Set up Python
|
||||
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
|
||||
run: uv sync --locked --all-extras --dev --link-mode=copy
|
||||
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -2,6 +2,4 @@ uv.lock
|
||||
.venv/
|
||||
.idea/
|
||||
.pytest_cache/
|
||||
.ruff_cache/
|
||||
simulation_output/
|
||||
__pycache__/
|
||||
.ruff_cache/
|
||||
@ -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.
|
||||
@ -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'
|
||||
]
|
||||
@ -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
|
||||
@ -13,15 +13,14 @@ WHITE = (255, 255, 255)
|
||||
RED = (255, 0, 0)
|
||||
BLUE = (0, 0, 255)
|
||||
GREEN = (0, 255, 0)
|
||||
ORANGE = (255, 165, 0)
|
||||
LIGHT_BLUE = (52, 134, 235)
|
||||
SELECTION_BLUE = (0, 128, 255)
|
||||
SELECTION_GRAY = (128, 128, 128, 80)
|
||||
SELECTION_BORDER = (80, 80, 90)
|
||||
|
||||
# Grid settings
|
||||
GRID_WIDTH = 50
|
||||
GRID_HEIGHT = 50
|
||||
GRID_WIDTH = 30
|
||||
GRID_HEIGHT = 25
|
||||
CELL_SIZE = 20
|
||||
RENDER_BUFFER = 50
|
||||
|
||||
@ -43,22 +42,6 @@ HUD_MARGIN = 10
|
||||
LINE_HEIGHT = 20
|
||||
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
|
||||
FOOD_SPAWNING = True
|
||||
FOOD_OBJECTS_COUNT = 500
|
||||
|
||||
@ -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"
|
||||
@ -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"
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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'
|
||||
]
|
||||
@ -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()
|
||||
@ -1,53 +1,39 @@
|
||||
# core/input_handler.py
|
||||
"""Handles input events and camera controls - no state management."""
|
||||
"""Handles all input events and camera controls."""
|
||||
|
||||
import pygame
|
||||
from config.constants import *
|
||||
|
||||
|
||||
class InputHandler:
|
||||
"""Pure input handler - processes input without managing simulation state."""
|
||||
|
||||
def __init__(self, camera, world, sim_view_rect):
|
||||
self.camera = camera
|
||||
self.world = world
|
||||
|
||||
# Selection state (input-specific, not simulation state)
|
||||
# Selection state
|
||||
self.selecting = False
|
||||
self.select_start = None
|
||||
self.select_end = None
|
||||
self.selected_objects = []
|
||||
|
||||
# UI display flags (input-controlled visual settings)
|
||||
# UI state flags
|
||||
self.show_grid = True
|
||||
self.show_interaction_radius = False
|
||||
self.show_legend = False
|
||||
self.is_paused = False
|
||||
|
||||
# Simulation state references (synchronized from external source)
|
||||
# Speed control
|
||||
self.tps = DEFAULT_TPS
|
||||
self.default_tps = DEFAULT_TPS
|
||||
self.is_paused = False
|
||||
self.sprint_mode = False
|
||||
self.is_stepping = False
|
||||
self.speed_multiplier = 1.0
|
||||
|
||||
# sim-view rect for mouse position calculations
|
||||
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):
|
||||
"""Update the sim_view rectangle."""
|
||||
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):
|
||||
"""Process all pygame events and return game state."""
|
||||
running = True
|
||||
@ -62,7 +48,7 @@ class InputHandler:
|
||||
elif event.type == pygame.KEYUP:
|
||||
self._handle_keyup(event)
|
||||
elif event.type == pygame.MOUSEWHEEL:
|
||||
self._handle_mouse_wheel(event)
|
||||
self.camera.handle_zoom(event.y)
|
||||
elif event.type == pygame.MOUSEBUTTONDOWN:
|
||||
self._handle_mouse_down(event)
|
||||
elif event.type == pygame.MOUSEBUTTONUP:
|
||||
@ -93,69 +79,31 @@ class InputHandler:
|
||||
elif event.key == pygame.K_l:
|
||||
self.show_legend = not self.show_legend
|
||||
elif event.key == pygame.K_SPACE:
|
||||
self.toggle_pause()
|
||||
self.is_paused = not self.is_paused
|
||||
elif event.key == pygame.K_LSHIFT:
|
||||
# Left Shift for temporary speed boost (turbo mode)
|
||||
self.set_speed_multiplier(2.0)
|
||||
self.tps = self.default_tps * TURBO_MULTIPLIER
|
||||
elif event.key == pygame.K_r:
|
||||
self.camera.reset_position()
|
||||
elif event.key == pygame.K_RSHIFT:
|
||||
self.toggle_sprint_mode() # Right Shift toggles sprint mode
|
||||
elif event.key == pygame.K_s:
|
||||
self.step_forward() # Step forward
|
||||
self.sprint_mode = not self.sprint_mode # Enter sprint mode
|
||||
|
||||
return running
|
||||
|
||||
def _handle_keyup(self, event):
|
||||
"""Handle keyup events."""
|
||||
if event.key == pygame.K_LSHIFT:
|
||||
# Reset speed multiplier when Left Shift is released
|
||||
self.set_speed_multiplier(1.0)
|
||||
self.tps = self.default_tps
|
||||
# if event.key == pygame.K_RSHIFT:
|
||||
# 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):
|
||||
"""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
|
||||
# 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
|
||||
# Only start selection if mouse is in viewport
|
||||
if in_viewport:
|
||||
self.selecting = True
|
||||
self.select_start = event.pos
|
||||
self.select_end = event.pos
|
||||
self.selecting = True
|
||||
self.select_start = event.pos
|
||||
self.select_end = event.pos
|
||||
|
||||
def _handle_mouse_up(self, event):
|
||||
"""Handle mouse button up events."""
|
||||
@ -166,9 +114,7 @@ class InputHandler:
|
||||
|
||||
def _handle_mouse_motion(self, event):
|
||||
"""Handle mouse motion events."""
|
||||
# Only pan if camera was started in viewport (camera will handle this internally)
|
||||
self.camera.pan(event.pos)
|
||||
# Only update selection if we're actively selecting in viewport
|
||||
if self.selecting:
|
||||
self.select_end = event.pos
|
||||
|
||||
@ -234,48 +180,5 @@ class InputHandler:
|
||||
top = min(self.select_start[1], self.select_end[1])
|
||||
width = abs(self.select_end[0] - self.select_start[0])
|
||||
height = abs(self.select_end[1] - self.select_start[1])
|
||||
return left, top, width, height
|
||||
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"
|
||||
return (left, top, width, height)
|
||||
return None
|
||||
@ -4,7 +4,7 @@
|
||||
import pygame
|
||||
import math
|
||||
from config.constants import *
|
||||
from world.objects import DefaultCell, FoodObject
|
||||
from world.base.brain import CellBrain
|
||||
|
||||
|
||||
class Renderer:
|
||||
@ -13,8 +13,11 @@ class Renderer:
|
||||
self.render_height = render_area.get_height()
|
||||
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."""
|
||||
if main_screen:
|
||||
main_screen.fill(BLACK)
|
||||
|
||||
self.render_area.fill(BLACK)
|
||||
|
||||
def draw_grid(self, camera, showing_grid=True):
|
||||
@ -98,9 +101,6 @@ class Renderer:
|
||||
return
|
||||
|
||||
for obj in world.get_objects():
|
||||
if not isinstance(obj, DefaultCell):
|
||||
continue
|
||||
|
||||
obj_x, obj_y = obj.position.get_position()
|
||||
radius = obj.interaction_radius
|
||||
|
||||
@ -250,68 +250,3 @@ class Renderer:
|
||||
size = camera.get_relative_size(width)
|
||||
rect = pygame.Rect(screen_x - size // 2, screen_y - size // 2, size, size)
|
||||
pygame.draw.rect(self.render_area, SELECTION_BLUE, rect, 1)
|
||||
|
||||
def render_food_connections(self, world, camera, selected_objects):
|
||||
"""Render orange bounding boxes around closest food and lines from selected cells."""
|
||||
if not selected_objects:
|
||||
return
|
||||
|
||||
# Find closest food to each selected cell
|
||||
for selected_cell in selected_objects:
|
||||
# Check if selected cell can have food interactions (DefaultCell)
|
||||
if not isinstance(selected_cell, DefaultCell):
|
||||
continue
|
||||
|
||||
# Find all food objects in world
|
||||
all_objects = world.get_objects()
|
||||
food_objects = [obj for obj in all_objects if isinstance(obj, FoodObject)]
|
||||
|
||||
if not food_objects:
|
||||
continue
|
||||
|
||||
# Get selected cell position
|
||||
cell_x, cell_y = selected_cell.position.get_position()
|
||||
|
||||
# Find closest food object
|
||||
closest_food = None
|
||||
closest_distance = float('inf')
|
||||
|
||||
for food in food_objects:
|
||||
food_x, food_y = food.position.get_position()
|
||||
distance = ((food_x - cell_x) ** 2 + (food_y - cell_y) ** 2) ** 0.5
|
||||
|
||||
if distance < closest_distance:
|
||||
closest_distance = distance
|
||||
closest_food = food
|
||||
|
||||
# Draw bounding box around closest food and connecting line
|
||||
if closest_food and closest_food != selected_cell:
|
||||
food_x, food_y = closest_food.position.get_position()
|
||||
food_width = closest_food.max_visual_width
|
||||
food_size = int(food_width * camera.zoom)
|
||||
|
||||
# Calculate bounding box position (centered on food)
|
||||
box_x, box_y = camera.world_to_screen(food_x, food_y)
|
||||
box_rect = pygame.Rect(
|
||||
box_x - food_size,
|
||||
box_y - food_size,
|
||||
food_size * 2,
|
||||
food_size * 2
|
||||
)
|
||||
|
||||
# Draw orange bounding box (2 pixel width)
|
||||
pygame.draw.rect(self.render_area, ORANGE, box_rect, 2)
|
||||
|
||||
# Draw line from selected cell to closest food
|
||||
screen_x, screen_y = camera.world_to_screen(cell_x, cell_y)
|
||||
|
||||
# Calculate line thickness based on zoom (minimum 1 pixel)
|
||||
line_thickness = max(1, int(2 * camera.zoom))
|
||||
|
||||
pygame.draw.line(
|
||||
self.render_area,
|
||||
ORANGE,
|
||||
(screen_x, screen_y),
|
||||
(box_x, box_y),
|
||||
line_thickness
|
||||
)
|
||||
|
||||
@ -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)
|
||||
@ -1,390 +1,193 @@
|
||||
import pygame
|
||||
import time
|
||||
import random
|
||||
import sys
|
||||
|
||||
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.renderer import Renderer
|
||||
from core.simulation_core import SimulationCore
|
||||
from core.event_bus import EventBus
|
||||
from ui.hud import HUD
|
||||
|
||||
import cProfile
|
||||
|
||||
|
||||
class SimulationEngine:
|
||||
"""Interactive simulation engine with UI (wrapper around SimulationCore)."""
|
||||
|
||||
def __init__(self, config=None):
|
||||
def __init__(self):
|
||||
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()
|
||||
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
|
||||
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
|
||||
self.ui_manager = UIManager((self.window_width, self.window_height))
|
||||
|
||||
screen_flags = pygame.RESIZABLE if resizable else 0
|
||||
self.screen = pygame.display.set_mode(
|
||||
(self.window_width, self.window_height),
|
||||
screen_flags, vsync=vsync
|
||||
)
|
||||
self.camera = Camera(SCREEN_WIDTH, SCREEN_HEIGHT, RENDER_BUFFER)
|
||||
self._update_simulation_view()
|
||||
|
||||
# self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT), vsync=1)
|
||||
pygame.display.set_caption("Dynamic Abstraction System Testing")
|
||||
self.clock = pygame.time.Clock()
|
||||
|
||||
def _init_ui(self):
|
||||
self.ui_manager = UIManager((self.window_width, self.window_height))
|
||||
self.last_tick_time = time.perf_counter()
|
||||
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.update_layout(self.window_width, self.window_height)
|
||||
|
||||
# Initialize tree widget with the world
|
||||
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()
|
||||
self.running = True
|
||||
|
||||
def _update_simulation_view(self):
|
||||
viewport_rect = self.hud.get_viewport_rect()
|
||||
self.sim_view_width = viewport_rect.width
|
||||
self.sim_view_height = viewport_rect.height
|
||||
self.sim_view_width = int(self.window_width * 0.75)
|
||||
self.sim_view_height = int(self.window_height * 0.75)
|
||||
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.renderer = Renderer(self.sim_view)
|
||||
|
||||
# Update simulation core camera dimensions
|
||||
self.simulation_core.camera.screen_width = self.sim_view_width
|
||||
self.simulation_core.camera.screen_height = self.sim_view_height
|
||||
# Update camera to match new sim_view size
|
||||
if hasattr(self, 'camera'):
|
||||
self.camera.screen_width = self.sim_view_width
|
||||
self.camera.screen_height = self.sim_view_height
|
||||
|
||||
# Update input handler simulation view rect
|
||||
self.input_handler.update_sim_view_rect(self.sim_view_rect)
|
||||
if hasattr(self, 'input_handler'):
|
||||
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():
|
||||
self.input_handler.set_action_callback(action, callback)
|
||||
@staticmethod
|
||||
def _setup_world():
|
||||
world = World(CELL_SIZE, (CELL_SIZE * GRID_WIDTH, CELL_SIZE * GRID_HEIGHT))
|
||||
random.seed(RANDOM_SEED)
|
||||
|
||||
def _count_cells(self):
|
||||
"""Count cells in the simulation."""
|
||||
# Import locally to avoid circular import
|
||||
from world.objects import DefaultCell
|
||||
return self.simulation_core.count_entities_by_type(DefaultCell)
|
||||
half_width = GRID_WIDTH * CELL_SIZE // 2
|
||||
half_height = GRID_HEIGHT * CELL_SIZE // 2
|
||||
|
||||
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):
|
||||
"""Run the interactive simulation engine."""
|
||||
self.simulation_core.start()
|
||||
|
||||
while self.running:
|
||||
self._handle_frame()
|
||||
|
||||
self.simulation_core.stop()
|
||||
pygame.quit()
|
||||
sys.exit()
|
||||
|
||||
def _handle_frame(self):
|
||||
"""Handle a single frame in the interactive simulation."""
|
||||
deltatime = self.clock.get_time() / 1000.0
|
||||
tick_interval = 1.0 / self.input_handler.tps
|
||||
|
||||
# Handle events
|
||||
events = pygame.event.get()
|
||||
self.running = self.input_handler.handle_events(events, self.hud.manager)
|
||||
|
||||
# Process HUD events and window events
|
||||
for event in events:
|
||||
hud_action = self.hud.process_event(event)
|
||||
self._process_hud_action(hud_action)
|
||||
|
||||
if event.type == pygame.VIDEORESIZE:
|
||||
self._handle_window_resize(event)
|
||||
self.window_width, self.window_height = event.w, event.h
|
||||
self.screen = pygame.display.set_mode((self.window_width, self.window_height),
|
||||
pygame.RESIZABLE)
|
||||
self._update_simulation_view()
|
||||
self.hud.update_layout(self.window_width, self.window_height)
|
||||
|
||||
# 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()
|
||||
# Sprint mode: run as many ticks as possible, skip rendering
|
||||
current_time = time.perf_counter()
|
||||
while True:
|
||||
self.input_handler.update_selected_objects()
|
||||
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
|
||||
|
||||
self.renderer.clear_screen()
|
||||
self.hud.render_sprint_debug(self.screen, self.actual_tps, self.total_ticks)
|
||||
pygame.display.flip()
|
||||
self.clock.tick(MAX_FPS)
|
||||
return
|
||||
|
||||
# Update UI manager every frame
|
||||
self.hud.manager.update(deltatime)
|
||||
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
|
||||
|
||||
# Handle step-forward mode
|
||||
if self.input_handler.is_stepping:
|
||||
self.simulation_core.step()
|
||||
self.input_handler.is_stepping = False
|
||||
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:
|
||||
# Update simulation using core
|
||||
self.simulation_core.update(deltatime)
|
||||
self.last_tick_time = time.perf_counter()
|
||||
self.last_tps_time = time.perf_counter()
|
||||
|
||||
# Update selected objects in input handler
|
||||
self.input_handler.update_selected_objects()
|
||||
self.hud.manager.draw_ui(self.screen)
|
||||
self._update(deltatime)
|
||||
self._render()
|
||||
|
||||
# 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."""
|
||||
def _update(self, deltatime):
|
||||
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()
|
||||
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)
|
||||
|
||||
# Render simulation world
|
||||
self._render_simulation_world()
|
||||
# In core/simulation_engine.py, in _render():
|
||||
self.screen.blit(self.sim_view, (self.sim_view_rect.left, self.sim_view_rect.top))
|
||||
|
||||
# Update and render UI
|
||||
self._update_and_render_ui(deltatime)
|
||||
# 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)
|
||||
|
||||
# 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_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.simulation_core.state.actual_tps)
|
||||
# self.hud.render_selected_objects_info(self.screen, self.input_handler.selected_objects)
|
||||
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.simulation_core.timing.state.is_paused)
|
||||
self.hud.render_pause_indicator(self.screen, self.input_handler.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]
|
||||
)
|
||||
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.screen = pygame.display.set_mode(
|
||||
(self.window_width, self.window_height),
|
||||
pygame.RESIZABLE
|
||||
)
|
||||
self._update_simulation_view()
|
||||
self.hud.update_layout(self.window_width, self.window_height)
|
||||
|
||||
def _process_hud_action(self, action):
|
||||
"""Process a single HUD action using the handler registry."""
|
||||
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()
|
||||
while time.perf_counter() - current_time < 0.05: # 50ms of sprint
|
||||
self.simulation_core.update(0.016) # Update simulation
|
||||
self.input_handler.update_selected_objects()
|
||||
pygame.event.pump() # Prevent event queue overflow
|
||||
|
||||
# Render sprint debug info
|
||||
self.screen.fill(BLACK)
|
||||
self.renderer.clear_screen()
|
||||
cell_count = self._count_cells()
|
||||
self.hud.render_sprint_debug(
|
||||
self.screen,
|
||||
self.simulation_core.state.actual_tps,
|
||||
self.simulation_core.state.total_ticks,
|
||||
cell_count
|
||||
)
|
||||
pygame.display.flip()
|
||||
self.clock.tick(MAX_FPS)
|
||||
|
||||
|
||||
186
core/timing.py
186
core/timing.py
@ -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)
|
||||
@ -1,5 +0,0 @@
|
||||
"""Simulation engines for different modes."""
|
||||
|
||||
from .headless_engine import HeadlessSimulationEngine, HeadlessConfig
|
||||
|
||||
__all__ = ['HeadlessSimulationEngine', 'HeadlessConfig']
|
||||
@ -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()
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
"""Entry points for different simulation modes."""
|
||||
|
||||
__all__ = []
|
||||
@ -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()
|
||||
@ -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()
|
||||
5
main.py
5
main.py
@ -1,8 +1,5 @@
|
||||
from core.simulation_engine import SimulationEngine
|
||||
|
||||
def main():
|
||||
if __name__ == "__main__":
|
||||
engine = SimulationEngine()
|
||||
engine.run()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -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'
|
||||
]
|
||||
@ -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()
|
||||
@ -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'
|
||||
}
|
||||
@ -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'
|
||||
}
|
||||
@ -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'
|
||||
}
|
||||
@ -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
|
||||
@ -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"
|
||||
@ -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"
|
||||
@ -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
|
||||
@ -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
|
||||
@ -4,22 +4,15 @@ version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"matplotlib>=3.10.7",
|
||||
"numpy>=2.3.0",
|
||||
"pandas>=2.3.3",
|
||||
"pre-commit>=4.2.0",
|
||||
"psutil>=7.0.0",
|
||||
"pydantic>=2.11.5",
|
||||
"pygame>=2.6.1",
|
||||
"pygame-gui>=0.6.14",
|
||||
"pytest>=8.3.5",
|
||||
"pyyaml>=6.0.2",
|
||||
"tqdm>=4.67.1",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"psutil>=7.0.0",
|
||||
"ruff>=0.11.12",
|
||||
"snakeviz>=2.2.2",
|
||||
]
|
||||
|
||||
@ -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()
|
||||
@ -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()
|
||||
@ -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,
|
||||
}
|
||||
@ -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)
|
||||
@ -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
|
||||
@ -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"
|
||||
@ -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
|
||||
@ -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
|
||||
@ -1,12 +1,10 @@
|
||||
import pytest
|
||||
from world.world import World, Position, BaseEntity, Rotation
|
||||
from world.world import World, Position, BaseEntity
|
||||
|
||||
|
||||
class DummyEntity(BaseEntity):
|
||||
def __init__(self, position, rotation=None):
|
||||
if rotation is None:
|
||||
rotation = Rotation(angle=0)
|
||||
super().__init__(position, rotation)
|
||||
def __init__(self, position):
|
||||
super().__init__(position)
|
||||
self.ticked = False
|
||||
self.rendered = False
|
||||
|
||||
@ -85,6 +83,9 @@ def test_tick_all_calls_tick(world):
|
||||
|
||||
def test_add_object_out_of_bounds(world):
|
||||
entity = DummyEntity(Position(x=1000, y=1000))
|
||||
|
||||
world.add_object(entity)
|
||||
|
||||
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
611
ui/hud.py
@ -6,562 +6,18 @@ import pygame_gui
|
||||
from config.constants import *
|
||||
from world.base.brain import CellBrain, FlexibleNeuralNetwork
|
||||
from world.objects import DefaultCell
|
||||
from pygame_gui.elements import UIPanel, UIButton, UITextEntryLine, UILabel
|
||||
from ui.tree_widget import TreeWidget
|
||||
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:
|
||||
def __init__(self, ui_manager, screen_width=SCREEN_WIDTH, screen_height=SCREEN_HEIGHT):
|
||||
self.font = pygame.font.Font("freesansbold.ttf", FONT_SIZE)
|
||||
self.legend_font = pygame.font.Font("freesansbold.ttf", LEGEND_FONT_SIZE)
|
||||
|
||||
self.manager = ui_manager
|
||||
self.screen_width = screen_width
|
||||
self.screen_height = screen_height
|
||||
|
||||
# Panel size defaults
|
||||
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
|
||||
)
|
||||
self.manager = ui_manager
|
||||
|
||||
def render_mouse_position(self, screen, camera, sim_view_rect):
|
||||
"""Render mouse position in top left."""
|
||||
@ -584,8 +40,7 @@ class HUD:
|
||||
|
||||
def render_tps(self, screen, actual_tps):
|
||||
"""Render TPS in bottom right."""
|
||||
display_tps = round(actual_tps) # Round to nearest whole number
|
||||
tps_text = self.font.render(f"TPS: {display_tps}", True, WHITE)
|
||||
tps_text = self.font.render(f"TPS: {actual_tps}", True, WHITE)
|
||||
tps_rect = tps_text.get_rect()
|
||||
tps_rect.bottomright = (self.screen_width - HUD_MARGIN, self.screen_height - HUD_MARGIN)
|
||||
screen.blit(tps_text, tps_rect)
|
||||
@ -690,7 +145,6 @@ class HUD:
|
||||
VIZ_WIDTH = 280 # Width 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_BOTTOM_MARGIN = 50 # Distance from the bottom of the screen
|
||||
|
||||
# Background styling constants
|
||||
BACKGROUND_PADDING = 30 # Padding around the visualization background
|
||||
@ -742,10 +196,6 @@ class HUD:
|
||||
TOOLTIP_MARGIN = 10
|
||||
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'):
|
||||
return
|
||||
|
||||
@ -756,9 +206,9 @@ class HUD:
|
||||
|
||||
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_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
|
||||
|
||||
@ -768,8 +218,6 @@ class HUD:
|
||||
pygame.draw.rect(screen, BACKGROUND_COLOR, background_rect)
|
||||
pygame.draw.rect(screen, WHITE, background_rect, BACKGROUND_BORDER_WIDTH)
|
||||
|
||||
info = network.get_structure_info()
|
||||
|
||||
# Title
|
||||
title_text = self.font.render("Neural Network", True, WHITE)
|
||||
title_rect = title_text.get_rect()
|
||||
@ -777,13 +225,6 @@ class HUD:
|
||||
title_rect.top = viz_y - TITLE_TOP_MARGIN
|
||||
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
|
||||
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
|
||||
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 ---
|
||||
mouse_x, mouse_y = pygame.mouse.get_pos()
|
||||
tooltip_text = None
|
||||
@ -1017,36 +474,22 @@ class HUD:
|
||||
screen.blit(surf, (tooltip_rect.left + TOOLTIP_PADDING_X, y))
|
||||
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."""
|
||||
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: {display_tps}", True, (255, 255, 255))
|
||||
tps_text = self.font.render(f"TPS: {actual_tps}", 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))
|
||||
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))
|
||||
cell_rect = cell_text.get_rect(center=(self.screen_width // 2, y + 120))
|
||||
|
||||
screen.blit(header, header_rect)
|
||||
screen.blit(tps_text, tps_rect)
|
||||
screen.blit(ticks_text, ticks_rect)
|
||||
screen.blit(cell_text, cell_rect)
|
||||
|
||||
def update_tree_widget(self, time_delta):
|
||||
"""Update the tree widget."""
|
||||
if self.tree_widget:
|
||||
self.tree_widget.update(time_delta)
|
||||
|
||||
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)
|
||||
def update_layout(self, window_width, window_height):
|
||||
"""Update HUD layout on window resize."""
|
||||
self.screen_width = window_width
|
||||
self.screen_height = window_height
|
||||
|
||||
@ -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
|
||||
@ -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)
|
||||
@ -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
578
uv.lock
generated
@ -1,10 +1,6 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
revision = 2
|
||||
requires-python = ">=3.11"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12'",
|
||||
"python_full_version < '3.12'",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "distlib"
|
||||
version = "0.3.9"
|
||||
@ -138,47 +43,31 @@ name = "dynamicsystemabstraction"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "matplotlib" },
|
||||
{ name = "numpy" },
|
||||
{ name = "pandas" },
|
||||
{ name = "pre-commit" },
|
||||
{ name = "psutil" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pygame" },
|
||||
{ name = "pygame-gui" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "tqdm" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "psutil" },
|
||||
{ name = "ruff" },
|
||||
{ name = "snakeviz" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "matplotlib", specifier = ">=3.10.7" },
|
||||
{ name = "numpy", specifier = ">=2.3.0" },
|
||||
{ name = "pandas", specifier = ">=2.3.3" },
|
||||
{ name = "pre-commit", specifier = ">=4.2.0" },
|
||||
{ name = "psutil", specifier = ">=7.0.0" },
|
||||
{ name = "pydantic", specifier = ">=2.11.5" },
|
||||
{ name = "pygame", specifier = ">=2.6.1" },
|
||||
{ name = "pygame-gui", specifier = ">=0.6.14" },
|
||||
{ name = "pytest", specifier = ">=8.3.5" },
|
||||
{ name = "pyyaml", specifier = ">=6.0.2" },
|
||||
{ name = "tqdm", specifier = ">=4.67.1" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "psutil", specifier = ">=7.0.0" },
|
||||
{ name = "ruff", specifier = ">=0.11.12" },
|
||||
{ name = "snakeviz", specifier = ">=2.2.2" },
|
||||
]
|
||||
dev = [{ name = "ruff", specifier = ">=0.11.12" }]
|
||||
|
||||
[[package]]
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "identify"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "nodeenv"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "platformdirs"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "pydantic"
|
||||
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/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/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]]
|
||||
@ -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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "pytest"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "python-i18n"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "pyyaml"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "typing-extensions"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "virtualenv"
|
||||
version = "20.31.2"
|
||||
|
||||
146
world/objects.py
146
world/objects.py
@ -2,13 +2,15 @@ import math
|
||||
import random
|
||||
|
||||
from config.constants import MAX_VELOCITY, MAX_ACCELERATION, MAX_ROTATIONAL_VELOCITY, MAX_ANGULAR_ACCELERATION
|
||||
from typing import List, Any, Union, Optional
|
||||
from world.base.brain import CellBrain
|
||||
from world.behavioral import BehavioralModel
|
||||
from world.world import Position, BaseEntity, Rotation
|
||||
import pygame
|
||||
from typing import Optional, List, Any, Union
|
||||
|
||||
from world.utils import get_distance_between_objects
|
||||
from world.physics import Physics
|
||||
|
||||
from math import atan2, degrees
|
||||
|
||||
|
||||
class DebugRenderObject(BaseEntity):
|
||||
@ -90,30 +92,14 @@ class FoodObject(BaseEntity):
|
||||
self.max_visual_width: int = 8
|
||||
self.decay: int = 0
|
||||
self.decay_rate: int = 1
|
||||
self.max_decay = 400
|
||||
self.max_decay = 200
|
||||
self.interaction_radius: int = 50
|
||||
self.neighbors: int = 0
|
||||
self.claimed_this_tick = False # Track if food was claimed this tick
|
||||
self.flags: dict[str, bool] = {
|
||||
"death": False,
|
||||
"can_interact": True,
|
||||
}
|
||||
|
||||
def try_claim(self) -> bool:
|
||||
"""
|
||||
Atomically claims this food for consumption within the current tick.
|
||||
|
||||
:return: True if successfully claimed, False if already claimed this tick
|
||||
"""
|
||||
if not self.claimed_this_tick:
|
||||
self.claimed_this_tick = True
|
||||
return True
|
||||
return False
|
||||
|
||||
def reset_claim(self) -> None:
|
||||
"""Reset the claim for next tick"""
|
||||
self.claimed_this_tick = False
|
||||
|
||||
def tick(self, interactable: Optional[List[BaseEntity]] = None) -> Union["FoodObject", List["FoodObject"]]:
|
||||
"""
|
||||
Updates the food object, increasing decay and flagging for death if decayed.
|
||||
@ -121,9 +107,6 @@ class FoodObject(BaseEntity):
|
||||
:param interactable: List of nearby entities (unused).
|
||||
:return: Self
|
||||
"""
|
||||
# Reset claim at the beginning of each tick
|
||||
self.reset_claim()
|
||||
|
||||
if interactable is None:
|
||||
interactable = []
|
||||
|
||||
@ -251,45 +234,15 @@ class DefaultCell(BaseEntity):
|
||||
"""
|
||||
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.
|
||||
|
||||
:param starting_position: The position of the object.
|
||||
:param entity_config: Configuration for entity hyperparameters.
|
||||
"""
|
||||
|
||||
super().__init__(starting_position, starting_rotation)
|
||||
|
||||
# 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.drag_coefficient: float = 0.1
|
||||
|
||||
self.velocity: 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.angular_acceleration: int = 0
|
||||
|
||||
self.energy: int = 1000
|
||||
|
||||
self.behavioral_model: CellBrain = CellBrain()
|
||||
|
||||
self.max_visual_width: int = 10
|
||||
self.interaction_radius: int = 50
|
||||
self.flags: dict[str, bool] = {
|
||||
"death": False,
|
||||
"can_interact": True,
|
||||
}
|
||||
|
||||
self.tick_count = 0
|
||||
self.entity_config = entity_config
|
||||
|
||||
self.physics = Physics(self.drag_coefficient, self.drag_coefficient*1.5)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
if distance_to_food < self.max_visual_width and food_objects:
|
||||
# Use atomic consumption to prevent race conditions
|
||||
if food_object.try_claim():
|
||||
self.energy += self.food_energy_value
|
||||
food_object.flag_for_death()
|
||||
self.energy += 110
|
||||
food_object.flag_for_death()
|
||||
return self
|
||||
|
||||
if self.energy >= self.reproduction_energy:
|
||||
if self.energy >= 1600:
|
||||
# too much energy, split
|
||||
offspring = []
|
||||
duplicate_x, duplicate_y = self.position.get_position()
|
||||
duplicate_x += random.randint(-self.max_visual_width, self.max_visual_width)
|
||||
duplicate_y += random.randint(-self.max_visual_width, self.max_visual_width)
|
||||
|
||||
for _ in range(self.reproduction_count):
|
||||
duplicate_x, duplicate_y = self.position.get_position()
|
||||
duplicate_x += random.randint(-self.offspring_offset_range, self.offspring_offset_range)
|
||||
duplicate_y += random.randint(-self.offspring_offset_range, self.offspring_offset_range)
|
||||
duplicate_x_2, duplicate_y_2 = self.position.get_position()
|
||||
duplicate_x_2 += random.randint(-self.max_visual_width, self.max_visual_width)
|
||||
duplicate_y_2 += random.randint(-self.max_visual_width, self.max_visual_width)
|
||||
|
||||
new_cell = DefaultCell(
|
||||
Position(x=int(duplicate_x), y=int(duplicate_y)),
|
||||
Rotation(angle=random.randint(0, 359)),
|
||||
entity_config=getattr(self, 'entity_config', None)
|
||||
)
|
||||
new_cell.set_brain(self.behavioral_model.mutate(self.mutation_rate))
|
||||
offspring.append(new_cell)
|
||||
new_cell = 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))
|
||||
|
||||
return offspring
|
||||
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 = {
|
||||
"distance": distance_to_food,
|
||||
@ -377,12 +328,44 @@ class DefaultCell(BaseEntity):
|
||||
|
||||
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
|
||||
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"]))
|
||||
|
||||
# request physics data from Physics class
|
||||
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())
|
||||
# 2. Apply drag force
|
||||
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
|
||||
x, y = self.position.get_position()
|
||||
@ -391,12 +374,19 @@ class DefaultCell(BaseEntity):
|
||||
|
||||
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
|
||||
self.rotation.set_rotation(self.rotation.get_rotation() + self.rotational_velocity)
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ -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
|
||||
@ -1,6 +1,6 @@
|
||||
from collections import defaultdict
|
||||
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
|
||||
|
||||
T = TypeVar("T", bound="BaseEntity")
|
||||
@ -153,8 +153,6 @@ class World:
|
||||
|
||||
:param camera: The camera object for coordinate transformation.
|
||||
: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 in obj_list:
|
||||
@ -163,15 +161,10 @@ class World:
|
||||
def tick_all(self) -> None:
|
||||
"""
|
||||
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
|
||||
self.buffers[next_buffer].clear()
|
||||
|
||||
# Food claims auto-reset in FoodObject.tick()
|
||||
|
||||
for obj_list in self.buffers[self.current_buffer].values():
|
||||
for obj in obj_list:
|
||||
if obj.flags["death"]:
|
||||
@ -180,17 +173,14 @@ class World:
|
||||
interactable = self.query_objects_within_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)
|
||||
else:
|
||||
new_obj = obj.tick()
|
||||
if new_obj is None:
|
||||
continue
|
||||
|
||||
# reproduction code - buffer new entities for atomic state transition
|
||||
# reproduction code
|
||||
if isinstance(new_obj, list):
|
||||
for item in new_obj:
|
||||
if isinstance(item, BaseEntity):
|
||||
@ -199,8 +189,6 @@ class World:
|
||||
else:
|
||||
cell = self._hash_position(new_obj.position)
|
||||
self.buffers[next_buffer][cell].append(new_obj)
|
||||
|
||||
# Atomic buffer switch - all state changes become visible simultaneously
|
||||
self.current_buffer = next_buffer
|
||||
|
||||
def add_object(self, new_object: BaseEntity) -> None:
|
||||
@ -220,8 +208,6 @@ class World:
|
||||
:param y: Y coordinate of the center.
|
||||
:param radius: Search 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] = []
|
||||
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 y2: Maximum Y coordinate.
|
||||
: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] = []
|
||||
cell_x1, cell_y1 = (
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user