diff --git a/Makefile b/Makefile index 7f5356a..5588354 100644 --- a/Makefile +++ b/Makefile @@ -28,16 +28,16 @@ install-dev: # Testing test: - pytest tests/ -v + PYTHONPATH=. pytest tests/ -v test-cov: - pytest tests/ --cov=ai_evo --cov-report=html --cov-report=term + PYTHONPATH=. pytest tests/ --cov=ai_evo --cov-report=html --cov-report=term test-determinism: - pytest tests/test_determinism.py -v + PYTHONPATH=. pytest tests/test_determinism.py -v test-performance: - pytest tests/test_performance.py -v + PYTHONPATH=. pytest tests/test_performance.py -v # Running run: diff --git a/ai_evo/__init__.py b/ai_evo/__init__.py new file mode 100644 index 0000000..b9a49f9 --- /dev/null +++ b/ai_evo/__init__.py @@ -0,0 +1,3 @@ +"""AI Evolution Environment Package.""" + +__version__ = "1.0.0" diff --git a/ai_evo/__pycache__/__init__.cpython-312.pyc b/ai_evo/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..57b1dfe Binary files /dev/null and b/ai_evo/__pycache__/__init__.cpython-312.pyc differ diff --git a/ai_evo/__pycache__/brain.cpython-312.pyc b/ai_evo/__pycache__/brain.cpython-312.pyc new file mode 100644 index 0000000..b004c5c Binary files /dev/null and b/ai_evo/__pycache__/brain.cpython-312.pyc differ diff --git a/ai_evo/__pycache__/config.cpython-312.pyc b/ai_evo/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000..5106ecc Binary files /dev/null and b/ai_evo/__pycache__/config.cpython-312.pyc differ diff --git a/ai_evo/__pycache__/creature.cpython-312.pyc b/ai_evo/__pycache__/creature.cpython-312.pyc new file mode 100644 index 0000000..728bdd4 Binary files /dev/null and b/ai_evo/__pycache__/creature.cpython-312.pyc differ diff --git a/ai_evo/__pycache__/evolution.cpython-312.pyc b/ai_evo/__pycache__/evolution.cpython-312.pyc new file mode 100644 index 0000000..63bb5ca Binary files /dev/null and b/ai_evo/__pycache__/evolution.cpython-312.pyc differ diff --git a/ai_evo/__pycache__/genome.cpython-312.pyc b/ai_evo/__pycache__/genome.cpython-312.pyc new file mode 100644 index 0000000..898646b Binary files /dev/null and b/ai_evo/__pycache__/genome.cpython-312.pyc differ diff --git a/ai_evo/__pycache__/rng.cpython-312.pyc b/ai_evo/__pycache__/rng.cpython-312.pyc new file mode 100644 index 0000000..9ae2c28 Binary files /dev/null and b/ai_evo/__pycache__/rng.cpython-312.pyc differ diff --git a/ai_evo/__pycache__/simulation.cpython-312.pyc b/ai_evo/__pycache__/simulation.cpython-312.pyc new file mode 100644 index 0000000..81cd95c Binary files /dev/null and b/ai_evo/__pycache__/simulation.cpython-312.pyc differ diff --git a/ai_evo/__pycache__/spatial.cpython-312.pyc b/ai_evo/__pycache__/spatial.cpython-312.pyc new file mode 100644 index 0000000..4c9b55e Binary files /dev/null and b/ai_evo/__pycache__/spatial.cpython-312.pyc differ diff --git a/ai_evo/__pycache__/stats.cpython-312.pyc b/ai_evo/__pycache__/stats.cpython-312.pyc new file mode 100644 index 0000000..724be9a Binary files /dev/null and b/ai_evo/__pycache__/stats.cpython-312.pyc differ diff --git a/ai_evo/__pycache__/world.cpython-312.pyc b/ai_evo/__pycache__/world.cpython-312.pyc new file mode 100644 index 0000000..355d988 Binary files /dev/null and b/ai_evo/__pycache__/world.cpython-312.pyc differ diff --git a/ai_evo/_init_.py b/ai_evo/_init_.py deleted file mode 100644 index ebc0d8c..0000000 --- a/ai_evo/_init_.py +++ /dev/null @@ -1,4 +0,0 @@ -numpy==2.0.1 -matplotlib==3.9.0 -streamlit==1.37.0 -pytest==8.2.2 diff --git a/ai_evo/brain.py b/ai_evo/brain.py index fb45f89..7f847ca 100644 --- a/ai_evo/brain.py +++ b/ai_evo/brain.py @@ -1,23 +1,96 @@ -import numpy as np - -class RNG: - """Central deterministic RNG wrapper.""" - def __init__(self, seed: int): - self.seed = seed - self.rs = np.random.RandomState(seed) - - # Basic distributions - def normal(self, loc=0.0, scale=1.0, size=None): - return self.rs.normal(loc, scale, size) +"""Neural network brain for creatures.""" - def rand(self, *shape): - return self.rs.rand(*shape) - - def randint(self, low, high=None, size=None): - return self.rs.randint(low, high, size) +import numpy as np +from typing import Tuple, Optional +from .genome import Genome - def choice(self, a, size=None, replace=True, p=None): - return self.rs.choice(a, size, replace, p) - def random_bool(self, p=0.5, size=None): - return self.rs.rand(*(size if isinstance(size, tuple) else (() if size is None else (size,)))) < p +class CreatureBrain: + """Simple neural network brain for creature decision making.""" + + def __init__(self, genome: Genome, config=None): + """Initialize brain with genome weights.""" + self.genome = genome + self.weights = genome.brain_weights + self.input_size = 8 # Basic sensory inputs + self.output_size = 4 # movement directions + + def process(self, inputs: np.ndarray) -> np.ndarray: + """Process sensory inputs to generate actions.""" + # Simple feedforward network + if len(inputs) != self.input_size: + inputs = np.pad(inputs, (0, max(0, self.input_size - len(inputs))))[:self.input_size] + + if isinstance(self.weights, tuple) and len(self.weights) == 4: + # Handle tuple format (W1, b1, W2, b2) + W1, b1, W2, b2 = self.weights + hidden = np.tanh(np.dot(inputs, W1) + b1) + outputs = np.tanh(np.dot(hidden, W2) + b2) + return outputs + else: + # Handle simple array format - fallback + if hasattr(self.weights, 'shape') and len(self.weights) >= self.input_size * self.output_size: + weight_matrix = self.weights[:self.input_size * self.output_size].reshape(self.input_size, self.output_size) + outputs = np.dot(inputs, weight_matrix) + return np.tanh(outputs) # Activation function + else: + # Fallback for insufficient weights + return np.random.randn(self.output_size) + + def get_movement(self, sensory_data: np.ndarray) -> Tuple[float, float]: + """Get movement direction from sensory input.""" + outputs = self.process(sensory_data) + dx = outputs[0] - outputs[1] # left-right + dy = outputs[2] - outputs[3] # up-down + return dx, dy + + def get_sensory_input(self, creature, environment, nearby_creatures) -> np.ndarray: + """Generate sensory input array for the creature.""" + # Basic sensory inputs + inputs = np.zeros(self.input_size) + + # Position relative to world center + inputs[0] = creature.position[0] / environment.width - 0.5 + inputs[1] = creature.position[1] / environment.height - 0.5 + + # Energy level (normalized) + inputs[2] = creature.energy / 200.0 # Assume max energy is ~200 + + # Food in current location + x, y = int(creature.position[0]), int(creature.position[1]) + x, y = environment.wrap(x, y) + inputs[3] = environment.food[y, x] / 10.0 # Assume max food is ~10 + + # Nearby creature information + if nearby_creatures: + # Count of nearby herbivores and carnivores + herbivore_count = sum(1 for c in nearby_creatures if c.species == "herbivore") + carnivore_count = sum(1 for c in nearby_creatures if c.species == "carnivore") + inputs[4] = min(herbivore_count / 5.0, 1.0) # Normalize + inputs[5] = min(carnivore_count / 5.0, 1.0) # Normalize + + # Distance to nearest threat/prey + if creature.species == "herbivore": + carnivores = [c for c in nearby_creatures if c.species == "carnivore"] + if carnivores: + nearest_distance = min(creature.distance_to(c) for c in carnivores) + inputs[6] = max(0, 1.0 - nearest_distance / creature.genome.perception) + else: # carnivore + herbivores = [c for c in nearby_creatures if c.species == "herbivore"] + if herbivores: + nearest_distance = min(creature.distance_to(c) for c in herbivores) + inputs[7] = max(0, 1.0 - nearest_distance / creature.genome.perception) + + return inputs + + def forward(self, sensory_input: np.ndarray) -> dict: + """Forward pass through the neural network, return action dictionary.""" + outputs = self.process(sensory_input) + + # Convert neural network outputs to action dictionary + return { + "move_x": float(outputs[0]) if len(outputs) > 0 else 0.0, + "move_y": float(outputs[1]) if len(outputs) > 1 else 0.0, + "eat": float(outputs[2]) if len(outputs) > 2 else 0.0, + "reproduce": float(outputs[3]) if len(outputs) > 3 else 0.0 + } diff --git a/ai_evo/config.py b/ai_evo/config.py index fb45f89..e08f28d 100644 --- a/ai_evo/config.py +++ b/ai_evo/config.py @@ -1,5 +1,81 @@ import numpy as np +class Config: + """Configuration class for simulation parameters.""" + + def __init__(self, + seed: int = 42, + width: int = 100, + height: int = 100, + max_steps: int = 1000, + init_herbivores: int = 20, + init_carnivores: int = 10, + mutation_rate: float = 0.1, + mutation_strength: float = 0.1, + plant_growth_rate: float = 0.05, + plant_cap: float = 10.0, + max_energy: float = 200.0, + min_energy: float = 0.0, + herbivore_bite_cap: float = 3.0, + carnivore_gain_eff: float = 0.8, + reproduce_threshold: float = 120.0, + reproduce_cost_frac: float = 0.5, + move_cost_base: float = 0.1, + grid_cell: int = 8, + snapshot_every: int = 100, + # Trait bounds + speed_min: float = 0.1, + speed_max: float = 3.0, + size_min: float = 0.5, + size_max: float = 2.0, + aggression_min: float = 0.0, + aggression_max: float = 1.0, + perception_min: float = 0.5, + perception_max: float = 5.0, + energy_efficiency_min: float = 0.5, + energy_efficiency_max: float = 2.0, + reproduction_threshold_min: float = 60.0, + reproduction_threshold_max: float = 150.0, + lifespan_min: int = 500, + lifespan_max: int = 2000): + """Initialize configuration with default values.""" + self.seed = seed + self.width = width + self.height = height + self.max_steps = max_steps + self.init_herbivores = init_herbivores + self.init_carnivores = init_carnivores + self.mutation_rate = mutation_rate + self.mutation_strength = mutation_strength + self.plant_growth_rate = plant_growth_rate + self.plant_cap = plant_cap + self.max_energy = max_energy + self.min_energy = min_energy + self.herbivore_bite_cap = herbivore_bite_cap + self.carnivore_gain_eff = carnivore_gain_eff + self.reproduce_threshold = reproduce_threshold + self.reproduce_cost_frac = reproduce_cost_frac + self.move_cost_base = move_cost_base + self.grid_cell = grid_cell + self.snapshot_every = snapshot_every + + # Trait bounds + self.speed_min = speed_min + self.speed_max = speed_max + self.size_min = size_min + self.size_max = size_max + self.aggression_min = aggression_min + self.aggression_max = aggression_max + self.perception_min = perception_min + self.perception_max = perception_max + self.energy_efficiency_min = energy_efficiency_min + self.energy_efficiency_max = energy_efficiency_max + self.reproduction_threshold_min = reproduction_threshold_min + self.reproduction_threshold_max = reproduction_threshold_max + self.lifespan_min = lifespan_min + self.lifespan_max = lifespan_max + + class RNG: """Central deterministic RNG wrapper.""" def __init__(self, seed: int): diff --git a/ai_evo/creature.py b/ai_evo/creature.py new file mode 100644 index 0000000..e9adda5 --- /dev/null +++ b/ai_evo/creature.py @@ -0,0 +1,51 @@ +"""Creature class for individual animals in the simulation.""" + +import numpy as np +from typing import Optional, Tuple +from .genome import Genome + + +class Creature: + """Represents an individual creature with genetics, behavior, and state.""" + + def __init__(self, id: str, genome: Genome, species: str, position: np.ndarray, energy: float, + generation: int = 0, parent_ids: Optional[list] = None): + """Initialize a creature with basic properties.""" + self.id = id + self.genome = genome + self.species = species + self.position = position.copy() + self.energy = energy + self.age = 0 + self.generation = generation + self.parent_ids = parent_ids or [] + self.alive = True + + def can_reproduce(self) -> bool: + """Check if creature has enough energy to reproduce.""" + return self.energy >= self.genome.reproduction_threshold + + def is_alive(self) -> bool: + """Check if creature is still alive.""" + return self.alive and self.energy > 0 + + def consume_energy(self, amount: float, min_energy: float, max_energy: float): + """Consume energy, ensuring it stays within bounds.""" + self.energy = max(min_energy, self.energy - amount) + if self.energy <= min_energy: + self.alive = False + + def gain_energy(self, amount: float, max_energy: float): + """Gain energy, ensuring it doesn't exceed maximum.""" + self.energy = min(max_energy, self.energy + amount) + + def update_position(self, dx: float, dy: float, world_width: int, world_height: int): + """Update position with world boundary wrapping.""" + self.position[0] = (self.position[0] + dx) % world_width + self.position[1] = (self.position[1] + dy) % world_height + + def distance_to(self, other: 'Creature') -> float: + """Calculate distance to another creature.""" + dx = self.position[0] - other.position[0] + dy = self.position[1] - other.position[1] + return np.sqrt(dx*dx + dy*dy) diff --git a/ai_evo/creatures.py b/ai_evo/creatures.py deleted file mode 100644 index fb45f89..0000000 --- a/ai_evo/creatures.py +++ /dev/null @@ -1,23 +0,0 @@ -import numpy as np - -class RNG: - """Central deterministic RNG wrapper.""" - def __init__(self, seed: int): - self.seed = seed - self.rs = np.random.RandomState(seed) - - # Basic distributions - def normal(self, loc=0.0, scale=1.0, size=None): - return self.rs.normal(loc, scale, size) - - def rand(self, *shape): - return self.rs.rand(*shape) - - def randint(self, low, high=None, size=None): - return self.rs.randint(low, high, size) - - def choice(self, a, size=None, replace=True, p=None): - return self.rs.choice(a, size, replace, p) - - def random_bool(self, p=0.5, size=None): - return self.rs.rand(*(size if isinstance(size, tuple) else (() if size is None else (size,)))) < p diff --git a/ai_evo/evolution.py b/ai_evo/evolution.py index bc99cb9..d4b91f9 100644 --- a/ai_evo/evolution.py +++ b/ai_evo/evolution.py @@ -8,6 +8,18 @@ class EvolutionEngine: def __init__(self, cfg: Config, rng: RNG): self.cfg, self.rng = cfg, rng + def create_random_genome(self, species: str) -> Genome: + """Create a random genome for a given species.""" + return Genome( + speed=self.rng.rand() * (self.cfg.speed_max - self.cfg.speed_min) + self.cfg.speed_min, + size=self.rng.rand() * (self.cfg.size_max - self.cfg.size_min) + self.cfg.size_min, + aggression=self.rng.rand() * (self.cfg.aggression_max - self.cfg.aggression_min) + self.cfg.aggression_min, + perception=self.rng.rand() * (self.cfg.perception_max - self.cfg.perception_min) + self.cfg.perception_min, + energy_efficiency=self.rng.rand() * (self.cfg.energy_efficiency_max - self.cfg.energy_efficiency_min) + self.cfg.energy_efficiency_min, + reproduction_threshold=self.rng.rand() * (self.cfg.reproduction_threshold_max - self.cfg.reproduction_threshold_min) + self.cfg.reproduction_threshold_min, + lifespan=int(self.rng.rand() * (self.cfg.lifespan_max - self.cfg.lifespan_min) + self.cfg.lifespan_min) + ) + def mutate(self, g: Genome) -> Genome: mr = self.cfg.mutation_rate ms = self.cfg.mutation_strength @@ -19,28 +31,81 @@ def mt(val, lo, hi): new_weights = self._mutate_weights(g.brain_weights, mr, ms) return Genome( - speed=mt(g.speed, 0.1, 3.0), - size=mt(g.size, 0.4, 2.5), - aggression=mt(g.aggression, 0.0, 1.0), - perception=mt(g.perception, 0.5, self.cfg.perception_max), - energy_efficiency=mt(g.energy_efficiency, 0.4, 2.5), - reproduction_threshold=mt(g.reproduction_threshold, 50.0, 160.0), - lifespan=int(mt(g.lifespan, 300, 2400)), + speed=mt(g.speed, self.cfg.speed_min, self.cfg.speed_max), + size=mt(g.size, self.cfg.size_min, self.cfg.size_max), + aggression=mt(g.aggression, self.cfg.aggression_min, self.cfg.aggression_max), + perception=mt(g.perception, self.cfg.perception_min, self.cfg.perception_max), + energy_efficiency=mt(g.energy_efficiency, self.cfg.energy_efficiency_min, self.cfg.energy_efficiency_max), + reproduction_threshold=mt(g.reproduction_threshold, self.cfg.reproduction_threshold_min, self.cfg.reproduction_threshold_max), + lifespan=int(mt(g.lifespan, self.cfg.lifespan_min, self.cfg.lifespan_max)), brain_weights=new_weights ) + def crossover(self, parent1: Genome, parent2: Genome) -> Genome: + """Create offspring through crossover of two parents.""" + # Simple average crossover for most traits + offspring = Genome( + speed=(parent1.speed + parent2.speed) / 2, + size=(parent1.size + parent2.size) / 2, + aggression=(parent1.aggression + parent2.aggression) / 2, + perception=(parent1.perception + parent2.perception) / 2, + energy_efficiency=(parent1.energy_efficiency + parent2.energy_efficiency) / 2, + reproduction_threshold=(parent1.reproduction_threshold + parent2.reproduction_threshold) / 2, + lifespan=int((parent1.lifespan + parent2.lifespan) / 2), + brain_weights=self._crossover_weights(parent1.brain_weights, parent2.brain_weights) + ) + return offspring + + def _crossover_weights(self, weights1, weights2): + """Crossover brain weights between two parents.""" + if weights1 is None or weights2 is None: + return weights1 if weights1 is not None else weights2 + + if isinstance(weights1, tuple) and isinstance(weights2, tuple): + # Handle tuple format (W1, b1, W2, b2) + W1_1, b1_1, W2_1, b2_1 = weights1 + W1_2, b1_2, W2_2, b2_2 = weights2 + + def crossover_array(arr1, arr2): + mask = self.rng.random_bool(0.5, arr1.shape) + result = arr1.copy() + result[mask] = arr2[mask] + return result + + return ( + crossover_array(W1_1, W1_2), + crossover_array(b1_1, b1_2), + crossover_array(W2_1, W2_2), + crossover_array(b2_1, b2_2) + ) + else: + # Handle simple array format + mask = self.rng.random_bool(0.5, weights1.shape) + result = weights1.copy() + result[mask] = weights2[mask] + return result + def _mutate_weights(self, weights, mr, ms): if weights is None: return None - W1, b1, W2, b2 = weights - def mutate_array(arr): - mask = self.rng.random_bool(mr, arr.shape) - arr2 = arr.copy() + + if isinstance(weights, tuple) and len(weights) == 4: + # Handle tuple format (W1, b1, W2, b2) + W1, b1, W2, b2 = weights + def mutate_array(arr): + mask = self.rng.random_bool(mr, arr.shape) + arr2 = arr.copy() + arr2[mask] += self.rng.normal(0, ms, mask.sum()) + return np.clip(arr2, -2.0, 2.0) + return ( + mutate_array(W1), + mutate_array(b1), + mutate_array(W2), + mutate_array(b2) + ) + else: + # Handle simple array format + mask = self.rng.random_bool(mr, weights.shape) + arr2 = weights.copy() arr2[mask] += self.rng.normal(0, ms, mask.sum()) return np.clip(arr2, -2.0, 2.0) - return ( - mutate_array(W1), - mutate_array(b1), - mutate_array(W2), - mutate_array(b2) - ) diff --git a/ai_evo/genome.py b/ai_evo/genome.py index fb45f89..4c5b5f5 100644 --- a/ai_evo/genome.py +++ b/ai_evo/genome.py @@ -1,23 +1,59 @@ -import numpy as np - -class RNG: - """Central deterministic RNG wrapper.""" - def __init__(self, seed: int): - self.seed = seed - self.rs = np.random.RandomState(seed) - - # Basic distributions - def normal(self, loc=0.0, scale=1.0, size=None): - return self.rs.normal(loc, scale, size) +"""Genome class representing creature genetics.""" - def rand(self, *shape): - return self.rs.rand(*shape) - - def randint(self, low, high=None, size=None): - return self.rs.randint(low, high, size) +import numpy as np +from typing import Optional - def choice(self, a, size=None, replace=True, p=None): - return self.rs.choice(a, size, replace, p) - def random_bool(self, p=0.5, size=None): - return self.rs.rand(*(size if isinstance(size, tuple) else (() if size is None else (size,)))) < p +class Genome: + """Represents the genetic information of a creature.""" + + def __init__(self, + speed: float = 1.0, + size: float = 1.0, + aggression: float = 0.5, + perception: float = 2.0, + energy_efficiency: float = 1.0, + reproduction_threshold: float = 90.0, + lifespan: int = 1000, + brain_weights: Optional[np.ndarray] = None): + """Initialize genome with trait values.""" + self.speed = speed + self.size = size + self.aggression = aggression + self.perception = perception + self.energy_efficiency = energy_efficiency + self.reproduction_threshold = reproduction_threshold + self.lifespan = lifespan + + # Initialize brain weights as tuple (W1, b1, W2, b2) or simple array + if brain_weights is None: + # Create default neural network weights + input_size = 8 + hidden_size = 6 + output_size = 4 + self.brain_weights = ( + np.random.randn(input_size, hidden_size) * 0.5, # W1 + np.random.randn(hidden_size) * 0.5, # b1 + np.random.randn(hidden_size, output_size) * 0.5, # W2 + np.random.randn(output_size) * 0.5 # b2 + ) + else: + self.brain_weights = brain_weights + + def copy(self) -> 'Genome': + """Create a copy of this genome.""" + if isinstance(self.brain_weights, tuple): + copied_weights = tuple(w.copy() for w in self.brain_weights) + else: + copied_weights = self.brain_weights.copy() + + return Genome( + speed=self.speed, + size=self.size, + aggression=self.aggression, + perception=self.perception, + energy_efficiency=self.energy_efficiency, + reproduction_threshold=self.reproduction_threshold, + lifespan=self.lifespan, + brain_weights=copied_weights + ) diff --git a/ai_evo/spatial.py b/ai_evo/spatial.py index 53ea600..61ed32c 100644 --- a/ai_evo/spatial.py +++ b/ai_evo/spatial.py @@ -3,8 +3,16 @@ class SpatialHash: """Grid-based spatial hashing for approximate neighbor retrieval.""" - def __init__(self, cell, W, H): - self.cell, self.W, self.H = cell, W, H + def __init__(self, cell_size=None, world_width=None, world_height=None, cell=None, W=None, H=None): + # Support both new and old parameter styles + if cell_size is not None: + self.cell = cell_size + self.W = world_width + self.H = world_height + else: + self.cell = cell + self.W = W + self.H = H self.grid = defaultdict(list) def _key(self, x, y): @@ -18,4 +26,32 @@ def rebuild(self, agents): def neighbors(self, agents, a, radius): cx, cy = self._key(a.position[0], a.position[1]) r = int(math.ceil(radius / self.cell)) - out + result = [] + + for dx in range(-r, r + 1): + for dy in range(-r, r + 1): + key = (cx + dx, cy + dy) + if key in self.grid: + for idx in self.grid[key]: + agent = agents[idx] + dist = math.sqrt((a.position[0] - agent.position[0])**2 + + (a.position[1] - agent.position[1])**2) + if dist <= radius and agent != a: + result.append(agent) + return result + + def get_neighbors(self, agents, query_agent, radius): + """Get neighbors within radius of query agent (alias for neighbors).""" + return self.neighbors(agents, query_agent, radius) + + def get_stats(self) -> dict: + """Get spatial hash statistics.""" + total_cells = len(self.grid) + total_agents = sum(len(agents) for agents in self.grid.values()) + avg_agents_per_cell = total_agents / max(1, total_cells) + + return { + "total_cells": total_cells, + "total_agents": total_agents, + "avg_agents_per_cell": avg_agents_per_cell + } diff --git a/ai_evo/world.py b/ai_evo/world.py index 8ae96ef..f1a960b 100644 --- a/ai_evo/world.py +++ b/ai_evo/world.py @@ -17,3 +17,26 @@ def grow_food(self): def wrap(self, x, y): return x % self.width, y % self.height + + def step(self): + """Advance environment by one time step.""" + self.t += 1 + self.grow_food() + + def consume_food_at(self, x: int, y: int, amount: float) -> float: + """Consume food at given position, return amount actually consumed.""" + x, y = int(x), int(y) # Convert to integers + x, y = self.wrap(x, y) + available = self.food[y, x] + consumed = min(available, amount) + self.food[y, x] -= consumed + return consumed + + def get_statistics(self) -> dict: + """Get environment statistics.""" + return { + "total_food": float(np.sum(self.food)), + "avg_food": float(np.mean(self.food)), + "temperature": self.temperature, + "time_step": self.t + } diff --git a/tests/_init_.py b/tests/__init__.py similarity index 100% rename from tests/_init_.py rename to tests/__init__.py diff --git a/tests/__pycache__/__init__.cpython-312.pyc b/tests/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..7a250e8 Binary files /dev/null and b/tests/__pycache__/__init__.cpython-312.pyc differ diff --git a/tests/__pycache__/test_determinism.cpython-312-pytest-8.2.2.pyc b/tests/__pycache__/test_determinism.cpython-312-pytest-8.2.2.pyc new file mode 100644 index 0000000..1a89454 Binary files /dev/null and b/tests/__pycache__/test_determinism.cpython-312-pytest-8.2.2.pyc differ diff --git a/tests/__pycache__/test_energy.cpython-312-pytest-8.2.2.pyc b/tests/__pycache__/test_energy.cpython-312-pytest-8.2.2.pyc new file mode 100644 index 0000000..00b8f42 Binary files /dev/null and b/tests/__pycache__/test_energy.cpython-312-pytest-8.2.2.pyc differ diff --git a/tests/__pycache__/test_evolution.cpython-312-pytest-8.2.2.pyc b/tests/__pycache__/test_evolution.cpython-312-pytest-8.2.2.pyc new file mode 100644 index 0000000..44cfbc8 Binary files /dev/null and b/tests/__pycache__/test_evolution.cpython-312-pytest-8.2.2.pyc differ diff --git a/tests/__pycache__/test_integration.cpython-312-pytest-8.2.2.pyc b/tests/__pycache__/test_integration.cpython-312-pytest-8.2.2.pyc new file mode 100644 index 0000000..6f18021 Binary files /dev/null and b/tests/__pycache__/test_integration.cpython-312-pytest-8.2.2.pyc differ diff --git a/tests/__pycache__/test_performance.cpython-312-pytest-8.2.2.pyc b/tests/__pycache__/test_performance.cpython-312-pytest-8.2.2.pyc new file mode 100644 index 0000000..d017f97 Binary files /dev/null and b/tests/__pycache__/test_performance.cpython-312-pytest-8.2.2.pyc differ diff --git a/tests/test_energy.py b/tests/test_energy.py index f28c88c..3619d51 100644 --- a/tests/test_energy.py +++ b/tests/test_energy.py @@ -267,7 +267,7 @@ def test_movement_energy_cost(self): slow_energy_loss = 100.0 - slow_creature.energy assert slow_energy_loss < fast_energy_loss - assert both creatures lost some energy + # Both creatures should have lost some energy assert fast_energy_loss > 0 assert slow_energy_loss > 0