sandpypi/sim.py
stan44 3e6796220d revert 6a0de3601989df8820bbdf9986aed8f04869b71e
There appears to be some differences between master and my local machines so this didn't work as expected.

revert Updated sim.py

updated sim.py with performance patch from the dev branch.
2024-12-29 12:25:21 -06:00

790 lines
34 KiB
Python

"""
#File Name: sim.py
Particle-based Physics Simulation System
======================================
This module implements a 2D particle simulation with physics, interactions, and state changes.
Key Components:
--------------
1. Particle Class
- Handles individual particle properties and behaviors
- Supports multiple particle types (solid, liquid, gas)
- Manages temperature and state transitions
2. Simulation Class
- Core simulation engine
- Manages particle creation, movement and interactions
- Handles physics calculations and spatial partitioning
"""
#Load the imports. Pygame is what makes this even work and so simple may consider other engines for performance depends on learning curve.
from settings import random, time, particle_properties
# Load particle properties from json so we know what particles we got and how they should be simulated.
class Particle:
def __init__(self, position, velocity, mass, particle_type, properties, temperature=20):
self.position = position # (x, y)
self.velocity = velocity # (vx, vy)
self.mass = mass
self.particle_type = particle_type
# Core properties
self.size = properties.get("size", 1)
self.hardness = properties.get("hardness", 0.5)
self.color = properties.get("color", [255, 255, 255, 255])
self.temperature = properties.get("temperature", temperature)
# Physics properties
self.conductivity = properties.get("conductivity", 0)
self.heat_capacity = properties.get("heat_capacity", 1)
self.flamability = properties.get("flamability", 0.0)
self.friction = properties.get("friction", 0.5)
self.viscosity = properties.get("viscosity", 1.0)
self.pressure = properties.get("pressure", 0)
# State properties
self.liquid = properties.get("liquid", False)
self.solid = properties.get("solid", True)
self.is_gas = properties.get("is_gas", False)
# Temperature transition properties
self.melt = properties.get("melt", None)
self.melt_temperature = properties.get("melt_temperature", None)
self.solidify = properties.get("solidify", None)
self.solidify_temperature = properties.get("solidify_temperature", None)
self.evaporate = properties.get("evaporate", None)
self.evaporate_temperature = properties.get("evaporate_temperature", None)
self.freeze = properties.get("freeze", None)
self.freeze_temperature = properties.get("freeze_temperature", None)
# Special properties
self.explosive = properties.get("explosive", False)
self.explosion_radius = properties.get("explosion_radius", 0)
self.explosion_color = properties.get("explosion_color", [0, 0, 0])
@classmethod
def from_type(cls, position, particle_type, properties):
default_velocity = [0, 0]
default_mass = properties.get("mass", 1.0)
return cls(position, default_velocity, default_mass, particle_type, properties)
class Simulation:
# the main class of the simulation.
def __init__(self, width, height, x=0, y=0):
self.dormant_particles = set()
self.particle_movement_counter = {}
self.DORMANT_THRESHOLD = 10
self.x = x
self.y = y
self.new_x = 0
self.new_y = 0
self.width = width
self.height = height
self.particle_size = 3
self.particles = [[None for _ in range(height)] for _ in range(width)]
self.particle_count = 0
self.active_particles = set()
self.cell_size = 32
self.spatial_grid = {}
self.brush_size = 1
self.max_brush_size = 20
self.particle_properties = particle_properties
self.current_particle_type = 'sand'
self.gravity = 9.8 # m/s^2, adjustable based on the scale of simulation
self.wind_zones = []
self.wind = [0.0, 0.0] # Global wind vector (x, y)
def reset_particle_count(self):
self.particle_count = 0
def get_cell_key(self, x, y): # this is where we get the cell key.
# Convert coordinates to grid cell
cell_x = x // self.cell_size
cell_y = y // self.cell_size
return (cell_x, cell_y)
def add_to_spatial_grid(self, particle, x, y): # this is where we add to the spatial grid.
cell_key = self.get_cell_key(x, y)
if cell_key not in self.spatial_grid:
self.spatial_grid[cell_key] = set()
self.spatial_grid[cell_key].add((x, y))
return cell_key
def remove_from_spatial_grid(self, x, y): # this is where we remove from the spatial grid.
cell_key = self.get_cell_key(x, y)
if cell_key in self.spatial_grid:
self.spatial_grid[cell_key].discard((x, y))
return cell_key
def update_spatial_grid(self): # this is where we update the spatial grid.
"""Update spatial grid for optimized collision detection"""
if len(self.active_particles) > 100:
self.spatial_grid = {}
cell_lists = {}
for x, y in self.active_particles:
cell_key = (x // self.cell_size, y // self.cell_size)
if cell_key not in cell_lists:
cell_lists[cell_key] = []
cell_lists[cell_key].append((x, y))
self.spatial_grid = {k: set(v) for k, v in cell_lists.items()}
def _check_dormant_state(self, x, y, particle):
key = (x, y)
if particle.particle_type == 'wall':
self.dormant_particles.add(key)
return True
if not hasattr(particle, 'last_position'):
particle.last_position = (x, y)
self.particle_movement_counter[key] = 0
return False
if particle.last_position == (x, y):
self.particle_movement_counter[key] = self.particle_movement_counter.get(key, 0) + 1
if self.particle_movement_counter[key] >= self.DORMANT_THRESHOLD:
self.dormant_particles.add(key)
return True
else:
particle.last_position = (x, y)
self.particle_movement_counter[key] = 0
self.dormant_particles.discard(key)
return False
def handle_phase_transitions(self, particle, x, y): # this is where we handle all the phase transitions.
"""Handle all phase transitions for a particle"""
# Check evaporation
if hasattr(particle, 'evaporate_temperature') and particle.evaporate_temperature is not None:
if particle.temperature >= particle.evaporate_temperature and particle.evaporate:
self.transform_particle(x, y, particle.evaporate)
# Check freezing
if hasattr(particle, 'freeze_temperature') and particle.freeze_temperature is not None:
if particle.temperature <= particle.freeze_temperature and particle.freeze:
self.transform_particle(x, y, particle.freeze)
# Check for melting with proper attribute validation
if (hasattr(particle, 'melt') and hasattr(particle, 'melt_temperature')
and particle.melt_temperature is not None):
if particle.temperature >= particle.melt_temperature:
new_type = particle.melt
if new_type in self.particle_properties:
self.transform_particle(x, y, new_type)
# Check for solidification with proper attribute validation
if (hasattr(particle, 'solidify') and hasattr(particle, 'solidify_temperature')
and particle.solidify_temperature is not None):
if particle.temperature <= particle.solidify_temperature:
new_type = particle.solidify
if new_type in self.particle_properties:
self.transform_particle(x, y, new_type)
def handle_particle_interactions(self, dt): # this is where we handle all the particle interactions.
"""Handle interactions between different particle types"""
for x, y in list(self.active_particles):
particle = self.particles[x][y]
if not particle:
continue
# Check neighboring particles
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1), (-1, -1), (1, -1), (-1, 1), (1, 1)]:
nx, ny = x + dx, y + dy
if 0 <= nx < self.width and 0 <= ny < self.height:
neighbor = self.particles[nx][ny]
if neighbor:
self.process_interaction(particle, neighbor, x, y, nx, ny)
def process_interaction(self, particle1, particle2, x1, y1, x2, y2): # this function is part 2 of handle_particle_interactions.
"""Process specific interactions between two particles"""
# Water + Sand = Mud
if (particle1.particle_type == 'water' and particle2.particle_type == 'sand' or
particle2.particle_type == 'water' and particle1.particle_type == 'sand'):
self.create_mud(x1, y1)
self.particles[x2][y2] = None
self.active_particles.discard((x2, y2))
# Lava/Fire effects
if particle1.particle_type in ['lava', 'fire', 'flame'] or particle2.particle_type in ['lava', 'fire', 'flame']:
target = particle2 if particle1.particle_type in ['lava', 'fire', 'flame'] else particle1
target_x, target_y = (x2, y2) if particle1.particle_type in ['lava', 'fire', 'flame'] else (x1, y1)
# Water to Steam
if target.particle_type == 'water':
self.transform_particle(target_x, target_y, 'steam')
# Wood to Fire
elif target.particle_type == 'wood':
if random.random() < 0.3: # 30% chance to ignite
self.transform_particle(target_x, target_y, 'fire')
def create_mud(self, x, y): # this is where we create the mud. probably should be moved to handle_particle_interactions or process_interaction.
"""Create mud particle from water and sand interaction"""
if 'mud' in self.particle_properties:
properties = self.particle_properties['mud']
new_particle = Particle.from_type((x, y), 'mud', properties)
self.particles[x][y] = new_particle
self.active_particles.add((x, y))
def transform_particle(self, x, y, new_type): # this is where we transform the particle.
"""Transform a particle into a different type"""
if new_type in self.particle_properties:
properties = self.particle_properties[new_type]
new_particle = Particle.from_type((x, y), new_type, properties)
self.particles[x][y] = new_particle
self.active_particles.add((x, y))
def handle_gas_movement(self, particle, x, y): # this is where we handle the gas movement. this function sucks. wip
"""Handle gas particle movement"""
if particle.is_gas:
dx = random.uniform(-1, 1)
dy = random.uniform(-2, 0) # Bias upward movement
new_x = int(x + dx)
new_y = int(y + dy)
if 0 <= new_x < self.width and 0 <= new_y < self.height:
if self.particles[new_x][new_y] is None:
self.particles[x][y] = None
self.particles[new_x][new_y] = particle
self.active_particles.add((new_x, new_y))
self.active_particles.discard((x, y))
def add_wind_zone(self, x, y):
# Instead of creating particles, store wind zone data
wind_zone = {
'x': x,
'y': y,
'radius': 50,
'strength': 2.0,
'direction': [1, 0]
}
self.wind_zones.append(wind_zone)
def calculate_forces(self, particle, x, y): # this is where we calculate the forces.
"""Calculate net forces acting on a particle."""
fx, fy = 0.0, 0.0 # Initialize forces
# Check wind zones
for zone in self.wind_zones:
dx = x - zone['x']
dy = y - zone['y']
distance = (dx*dx + dy*dy) ** 0.5
if distance <= zone['radius']:
fx += zone['direction'][0] * zone['strength'] * (1 - distance/zone['radius'])
fy += zone['direction'][1] * zone['strength'] * (1 - distance/zone['radius'])
# Apply wind force
if particle.is_gas:
fx += self.wind[0] * 0.5
fy += self.wind[1] * 0.5
else:
fx += self.wind[0]
fy += self.wind[1]
# Apply drag force
drag = particle.viscosity * -1
fx += drag * particle.velocity[0]
fy += drag * particle.velocity[1]
# Check neighboring particles
neighbors = self._get_quick_neighbors(x, y)
for nx, ny in neighbors:
if 0 <= nx < self.width and 0 <= ny < self.height:
neighbor = self.particles[nx][ny]
if neighbor:
self._apply_neighbor_forces(particle, neighbor, fx, fy)
return fx, fy
def _process_particle_batch(self, batch, dt):
updates = []
new_active = set()
# Filter out dormant particles from the batch
active_batch = [pos for pos in batch if pos not in self.dormant_particles]
for x, y in active_batch:
particle = self.particles[x][y]
if not particle:
continue
if particle.particle_type == 'wall':
new_active.add((x, y))
continue
# Check if particle should become dormant
if self._check_dormant_state(x, y, particle):
new_active.add((x, y))
continue
# physics calculations
fx, fy = self.calculate_forces(particle, x, y)
# Use max() to ensure mass is never zero
mass = max(particle.mass, 0.001)
particle.velocity[0] += (fx / mass) * dt
particle.velocity[1] += (fy / mass) * dt
new_x = int(x + particle.velocity[0] * dt)
new_y = int(y + particle.velocity[1] * dt)
if 0 <= new_x < self.width and 0 <= new_y < self.height:
if self.particles[new_x][new_y] is None:
updates.append((x, y, new_x, new_y, particle))
new_active.add((new_x, new_y))
# Wake up neighboring dormant particles
self._wake_neighbors(new_x, new_y)
else:
new_active.add((x, y))
# Apply updates and return new active set
for old_x, old_y, new_x, new_y, particle in updates:
self.particles[old_x][old_y] = None
self.particles[new_x][new_y] = particle
particle.position = (new_x, new_y)
return new_active
def _get_quick_neighbors(self, x, y):
"""Quick neighbor lookup without full spatial grid"""
return [(x+dx, y+dy) for dx, dy in [(-1,0), (1,0), (0,-1), (0,1)]]
def _apply_neighbor_forces(self, particle, neighbor, fx, fy):
"""Optimized neighbor force calculation"""
if hasattr(neighbor, 'temperature') and hasattr(particle, 'temperature'):
temp_diff = neighbor.temperature - particle.temperature
fy += temp_diff * 0.05
def ignite_particle(self, particle): # this is where we ignite the particle.
"""Handle ignition and burning of flammable particles."""
if hasattr(particle, 'flamability') and particle.flamability > 0.5:
if hasattr(particle, 'temperature') and particle.temperature > 150:
particle.type = 'fire'
particle.temperature += 200
# Add burning effect for wood
if particle.type == 'wood':
particle.burning = True
particle.burn_time = 100 # burn time
def spread_fire(self): # this is where we spread the fire.
"""Spread fire to neighboring particles."""
for x, y in list(self.active_particles):
particle = self.particles[x][y]
if particle and (particle.particle_type == 'fire' or getattr(particle, 'burning', False)):
# Check all neighboring cells including diagonals
for dx in [-1, 0, 1]:
for dy in [-1, 0, 1]:
nx, ny = x + dx, y + dy
if 0 <= nx < self.width and 0 <= ny < self.height:
neighbor = self.particles[nx][ny]
if neighbor and hasattr(neighbor, 'flamability'):
if neighbor.particle_type == 'wood':
# Higher chance to ignite wood
if random.random() < 0.3: # 30% chance to spread
self.ignite_particle(neighbor)
elif neighbor.flamability > 0:
if random.random() < 0.1: # 10% chance for other materials
self.ignite_particle(neighbor)
def handle_special_particles(self, particle, x, y): # this is where we handle special particles.
"""Handle special particle behaviors"""
if particle.particle_type in ['fire', 'flame', 'smoke']:
if random.random() < 0.6: # % chance
self.particles[x][y] = None
self.active_particles.discard((x, y))
if particle.particle_type in ['fire', 'flame', 'lava']:
# Create smoke above with proper physics
if random.random() < 0.65 and y > 0: # % chance for smoke
properties = self.particle_properties['smoke']
new_smoke = Particle.from_type((x, y-1), 'smoke', properties)
if self.particles[x][y-1] is None:
self.particles[x][y-1] = new_smoke
self.active_particles.add((x, y-1))
else:
# Handle collision with water
if particle.particle_type == 'water':
self.particles[x][y-1] = None
self.active_particles.discard((x, y-1))
self.particles[x][y] = None
self.active_particles.discard((x, y))
self.particles[x][y] = Particle.from_type((x, y), 'water', self.particle_properties['water'])
self.active_particles.add((x, y))
def handle_temperature(self, dt): # this is where we handle the temperature.
"""Handle temperature changes and state transitions"""
for x, y in list(self.active_particles):
particle = self.particles[x][y]
if not particle:
continue
if particle.temperature > 1700:
# Transition to gas
particle.is_gas = True
particle.temperature = 1700
particle.velocity = [random.uniform(-1, 1), random.uniform(-1, 1)]
particle.temperature < 1400
particle.is_gas = False
# Temperature spread to neighbors
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
nx, ny = x + dx, y + dy
if 0 <= nx < self.width and 0 <= ny < self.height:
neighbor = self.particles[nx][ny]
if (neighbor and hasattr(neighbor, 'temperature')
and neighbor.temperature is not None):
temp_diff = particle.temperature - neighbor.temperature
heat_transfer = temp_diff * 0.1 * dt
particle.temperature -= heat_transfer
neighbor.temperature += heat_transfer
def burning(self): # this is where we handle the burning.
"""Handle burning of particles."""
for x, y in list(self.active_particles):
particle = self.particles[x][y]
if particle and hasattr(particle, 'burning') and particle.burning:
particle.temperature += 10
if particle.temperature > 1000:
self.particles[x][y] = None
self.active_particles.remove((x, y))
self.spatial_grid.pop((x, y), None)
def create_particle(self, x, y): # this is where we create the particle.
"""Create a new particle with full property support"""
particle_type = self.current_particle_type.lower()
# Check if the particle is within the grid boundaries
if particle_type in self.particle_properties:
grid_x = x // self.particle_size
grid_y = y // self.particle_size
if 0 <= grid_x < self.width and 0 <= grid_y < self.height:
properties = self.particle_properties[particle_type]
position = (grid_x, grid_y)
new_particle = Particle(
position=position,
velocity=[0, 0],
mass=properties.get('mass', 1.0),
particle_type=particle_type,
properties=properties
)
# Add to the grid
if 0 <= grid_x < len(self.particles) and 0 <= grid_y < len(self.particles[0]):
self.particles[grid_x][grid_y] = new_particle
self.active_particles.add((grid_x, grid_y))
self.particle_count += 1
def create_particle_circle(self, center_x, center_y): # this is where we create the particle circle.
brush_size = int(self.brush_size)
for dx in range(-brush_size, brush_size + 1):
for dy in range(-brush_size, brush_size + 1):
if dx*dx + dy*dy <= brush_size*brush_size: # Circle check
self.create_particle(center_x + dx * self.particle_size, center_y + dy * self.particle_size)
def get_particle_state(self, x, y): # this is where we get the particle state.
"""Get the state of a particle at a given position"""
particle = self.particles[x][y]
if particle:
return particle.particle_type
return None
def apply_gravity(self, dt): # this is where we apply gravity.
"""Handle only gravity and basic particle movement"""
self.spatial_grid.clear()
for x, y in list(self.active_particles):
particle = self.particles[x][y]
if not particle or particle.particle_type == 'wall':
continue
# Apply gravity
new_y = y + 1
new_x = x
# Check boundaries
if not (0 <= new_x < self.width and 0 <= new_y < self.height):
continue
# Handle granular materials (sand, dirt)
if particle.particle_type in ['sand', 'dirt', 'snow', 'ice']:
if self.particles[x][new_y] is None:
new_x, new_y = x, y + 1
else:
# Try diagonal movement with randomization
diagonal_dirs = [(-1, 1), (1, 1)]
random.shuffle(diagonal_dirs)
for dx, dy in diagonal_dirs:
test_x = x + dx
test_y = y + dy
if (0 <= test_x < self.width and 0 <= test_y < self.height and
self.particles[test_x][test_y] is None):
if random.random() < 0.8: # 80% chance to move diagonally
new_x = test_x
new_y = test_y
break
# Handle liquid movement (water, lava)
elif particle.liquid:
if self.particles[x][new_y] is None:
new_x = x
new_y = y + 1
else:
spread_directions = [(-1, 0), (1, 0)]
random.shuffle(spread_directions)
for dx, _ in spread_directions:
test_x = x + dx
if (0 <= test_x < self.width and
self.particles[test_x][y] is None):
new_x = test_x
new_y = y
break
# Move particle if destination is empty
if self.particles[new_x][new_y] is None:
self.particles[x][y] = None
self.particles[new_x][new_y] = particle
self.active_particles.add((new_x, new_y))
self.active_particles.discard((x, y))
particle.position = (new_x, new_y)
def apply_physics(self, dt, engine_settings): # this is where we apply physics.
"""Handle all physics effects"""
new_active_particles = set()
for x, y in list(self.active_particles):
particle = self.particles[x][y]
if not particle:
continue
# Handle boundaries based on settings
if engine_settings['outerwall']:
if x <= 0 or x >= self.width-1 or y <= 0 or y >= self.height-1:
# Create wall particle at boundary if none exists
if self.particles[x][y] is None:
properties = self.particle_properties['wall']
wall = Particle.from_type((x, y), 'wall', properties)
self.particles[x][y] = wall
new_active_particles.add((x, y))
self.particle_count += 1 # Track new wall particle
continue
else:
# Delete particles that go out of bounds
if x <= 0 or x >= self.width-1 or y <= 0 or y >= self.height-1:
if self.particles[x][y] is not None:
self.particles[x][y] = None
self.active_particles.discard((x, y))
self.particle_count -= 1
continue
# Skip wall physics - walls are immutable
if particle.particle_type == 'wall':
new_active_particles.add((x, y))
continue
# Handle dissipating particles
if particle.particle_type in ['fire', 'flame']:
# Clear current position first
self.particles[x][y] = None
self.active_particles.discard((x, y))
# Handle fire movement
dx = random.uniform(-0.5, 0.5)
dy = random.uniform(-1.5, -0.5) # Upward drift
new_x = int(x + dx)
new_y = int(y + dy)
if 0 <= new_x < self.width and 0 <= new_y < self.height:
if self.particles[new_x][new_y] is None:
self.particles[new_x][new_y] = particle
new_active_particles.add((new_x, new_y))
# Generate smoke above the new fire position
if random.random() < 0.25 and new_y > 0:
properties = self.particle_properties['smoke']
new_smoke = Particle(
position=(new_x, new_y-1),
velocity=[random.uniform(-0.5, 0.5), -1],
mass=properties.get('mass', 0.1),
particle_type='smoke',
properties=properties
)
if self.particles[new_x][new_y-1] is None:
self.particles[new_x][new_y-1] = new_smoke
new_active_particles.add((new_x, new_y-1))
# Dissipation chance
if random.random() < 0.02:
continue
continue
# Air handling - particles can pass through should implement proper air instead of a particle
if particle.particle_type == 'air':
continue
# Handle phase transitions
self.handle_phase_transitions(particle, x, y)
# Calculate forces
fx, fy = self.calculate_forces(particle, x, y)
# handle gas particles
if particle.is_gas:
# Gas-specific movement
dx = random.uniform(2, -1)
dy = random.uniform(-2, 0) # Bias upward
new_x = int(x + dx)
new_y = int(y + dy)
self.particles[x][y] = None
self.active_particles.discard((x, y))
if 0 <= new_x < self.width and 0 <= new_y < self.height:
if self.particles[new_x][new_y] is None:
self.particles[x][y] = None
self.particles[new_x][new_y] = particle
new_active_particles.add((new_x, new_y))
self.active_particles.discard((x, y))
continue
else:
# Regular particle physics
mass = max(particle.mass, 0.001)
particle.velocity[0] += (fx / mass) * dt
particle.velocity[1] += (fy / mass) * dt
if particle.liquid:
# Enhanced liquid spreading
spread_chance = 0.5
if random.random() < spread_chance:
dx = random.choice([-1, 1])
if (0 <= x + dx < self.width and
self.particles[x + dx][y] is None):
new_x = x + dx
new_y = y
self.particles[x][y] = None
self.particles[new_x][new_y] = particle
new_active_particles.add((new_x, new_y))
continue
# Update position for non-liquid particles
new_x = int(x + particle.velocity[0] * dt)
new_y = int(y + particle.velocity[1] * dt)
if 0 <= new_x < self.width and 0 <= new_y < self.height:
if self.particles[new_x][new_y] is None:
self.particles[x][y] = None
self.particles[new_x][new_y] = particle
else:
new_active_particles.add((x, y))
self.active_particles = new_active_particles
def clear_particles_circle(self, center_x, center_y): # this is for the brush tool
"""Clear particles in a circle around the given point based on brush size"""
brush_size = int(self.brush_size)
particles_cleared = 0 # Track how many particles we clear
for dx in range(-brush_size, brush_size + 1):
for dy in range(-brush_size, brush_size + 1):
if dx*dx + dy*dy <= brush_size*brush_size: # Circle check
grid_x = (center_x + dx * self.particle_size) // self.particle_size
grid_y = (center_y + dy * self.particle_size) // self.particle_size
if 0 <= grid_x < self.width and 0 <= grid_y < self.height:
if self.particles[grid_x][grid_y]:
self.particles[grid_x][grid_y] = None
self.active_particles.discard((grid_x, grid_y))
self.remove_from_spatial_grid(grid_x, grid_y)
particles_cleared += 1
self.particle_count = max(0, self.particle_count - particles_cleared) # Update count, ensure it doesn't go negative
def mix_liquids(self, liquid1, liquid2): # this is for the mix tool
"""Handle liquid mixing interactions"""
if liquid1.temperature != liquid2.temperature:
avg_temp = (liquid1.temperature + liquid2.temperature) / 2
liquid1.temperature = avg_temp
liquid2.temperature = avg_temp
liquid1.density = self.calculate_density(liquid1.temperature)
liquid2.density = self.calculate_density(liquid2.temperature)
liquid1.viscosity = self.calculate_viscosity(liquid1.temperature)
liquid2.viscosity = self.calculate_viscosity(liquid2.temperature)
liquid1.color = self.calculate_color(liquid1.temperature)
liquid2.color = self.calculate_color(liquid2.temperature)
def _wake_neighbors(self, x, y):
for dx in [-1, 0, 1]:
for dy in [-1, 0, 1]:
nx, ny = x + dx, y + dy
key = (nx, ny)
if key in self.dormant_particles:
self.dormant_particles.discard(key)
self.particle_movement_counter[key] = 0
def track_tps(self):
"""Track Ticks Per Second for simulation performance monitoring"""
if not hasattr(self, '_tps_counter'):
self._tps_counter = 0
self._tps_timer = time.time()
self._current_tps = 0
self._tps_counter += 1
current_time = time.time()
elapsed = current_time - self._tps_timer
# Update TPS count every second
if elapsed >= 1.0:
self._current_tps = self._tps_counter / elapsed
self._tps_counter = 0
self._tps_timer = current_time
return self._current_tps
def simulate_step(self, dt, engine_settings):
"""Run a single step of the simulation"""
active_list = list(self.active_particles)
batch_size = 1000
for i in range(0, len(active_list), batch_size):
batch = active_list[i:i + batch_size]
self._process_particle_batch(batch, dt)
# Update spatial grid only when needed
if len(self.active_particles) > 100:
self.update_spatial_grid()
# Update particle positions and physics
self.apply_gravity(dt)
self.apply_physics(dt, engine_settings)
# Handle state changes and interactions
self.handle_temperature(dt)
self.handle_particle_interactions(dt)
self.burning()
self.spread_fire()