laid out and done like most of the ground work

to reorganize the Physics Engine
so that it'll be friendly to others to read.
This commit is contained in:
Stan44 2024-12-31 01:50:07 -06:00
parent c6c047fdb7
commit c225632aac
16 changed files with 1059 additions and 55 deletions

View File

@ -7,25 +7,25 @@ the code is not finished yet, but i will update it as i go.
## Main Features
- Particle Physics
- Gravity and wind effects
- Gravity and wind effects (maybe on the wind zones)
- Temperature dynamics
- State transitions (melting, freezing, evaporation)
- Particle Interactions
- Collision detection
- Chemical reactions (e.g., water + sand = mud)
- Heat transfer between particles
- Chemical reactions (e.g., water + sand = wet sand, Lava + lower temperature = molten rock = rock)
- Heat transfer between particles (e.g., Things seem to cool off as for heating up that's a different thing)
- Special Effects
- Fire propagation sorta
- Smoke generation
- Liquid spreading
- Liquid spreading (Could be improved )
- Optimization Features
- Spatial partitioning grid
- Dormant particle tracking
- Batch processing
- Static User Interface
- Spatial partitioning grid (to reduce calculations)
- Dormant particle tracking (to reduce unnecessary calculations)
- Batch processing (to reduce unnecessary calculations)
- Static User Interface (to reduce unnecessary calculations)
### **Current Features**

75
physics/REORGANIZATION.md Normal file
View File

@ -0,0 +1,75 @@
# Physics Module Reorganization Plan
## 1. particle.py
Contains core particle functionality:
- Full Particle class definition
- particle_properties handling
- create_particle()
- create_particle_circle()
- transform_particle()
- get_particle_state()
## 2. forces.py
Physics and movement systems:
- calculate_forces()
- _apply_neighbor_forces()
- apply_gravity()
- apply_physics()
- add_wind_zone()
- handle_gas_movement()
## 3. interactions.py
Particle interaction handling:
- handle_particle_interactions()
- process_interaction()
- mix_liquids()
- create_mud()
- handle_particle_damage()
- _get_quick_neighbors()
## 4. temperature.py
Temperature and state management:
- handle_temperature()
- handle_phase_transitions()
- burning()
- spread_fire()
- ignite_particle()
- handle_special_particles()
## 5. grid.py
Spatial management systems:
- update_spatial_grid()
- get_cell_key()
- add_to_spatial_grid()
- remove_from_spatial_grid()
- _get_neighbors_from_grid()
- _wake_neighbors()
- clear_particles_circle()
## 6. simulation.py
Core simulation coordination:
- Simulation class (main orchestrator)
- simulate_step()
- track_tps()
- reset_particle_count()
- get_accurate_particle_count()
## Migration Steps
1. Create new files
2. Move related code sections
3. Update imports
4. Test each component
5. Update main simulation.py references

View File

@ -22,7 +22,7 @@ import time
import numpy as np
engine_settings = {
'pause_sim': False,
'pause_sim': True,
'enable_cursor': True,
'enable_glow': False,
'enable_gas_effect': True,
@ -76,5 +76,5 @@ def load_particle_properties():
# Load particle properties once when module is imported
particle_properties = load_particle_properties()
__all__ = ['pygame', 'np', 'random', 'time', 'engine_settings', 'particle_properties', 'cProfile', 'pstats', 'sys', 'os', 'jit']
__all__ = ['pygame', 'np', 'random', 'time', 'engine_settings', 'particle_properties', 'cProfile', 'pstats', 'sys', 'os']

0
src/debug/__init__.py Normal file
View File

View File

@ -60,8 +60,10 @@
"velocity": 0.5,
"conductivity": 1,
"heat_capacity": 1,
"color": [139, 69, 19, 255],
"color": [125, 45, 55, 255],
"mass": 0.5,
"melt": "dirt",
"melt_temperature": 100,
"flamability": 0,
"temperature": 20,
"explosive": false,

View File

@ -2,15 +2,13 @@
"plasma": {
"name": "Plasma",
"size": 1,
"hardness": 0.001,
"velocity": 0.0,
"conductivity": 0,
"heat_capacity": 10,
"hardness": 0.0,
"velocity": 0.8,
"conductivity": 1,
"heat_capacity": 1,
"color": [255, 100, 200, 255],
"mass": 0.001,
"temperature": 3600,
"friction": 0.0,
"viscosity": 0.0,
"mass": 0.01,
"temperature": 3400,
"liquid": false,
"solid": false,
"is_gas": true,
@ -24,6 +22,7 @@
"hardness": 0.1,
"velocity": 0.8,
"conductivity": 1,
"heat_capacity": 1,
"color": [255, 255, 0, 255],
"mass": 0.01,
"temperature": 900,

69
src/physics/particle.py Normal file
View File

@ -0,0 +1,69 @@
# Load particle properties from json so we know what particles we got and how they should be simulated.
class Particle:
def __init__(self, simulation, 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
self.sim = simulation
# 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)
self.durability = properties.get("durability", 100.0)
# 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])
self.explosion_force = properties.get("explosion_force", 0)
self.explosion_duration = properties.get("explosion_duration", 0)
# Pressure properties
self.pressure_resistance = properties.get("pressure_resistance", 0)
self.pressure_tolerance = properties.get("pressure_tolerance", 0)
self.pressure_threshold = properties.get("pressure_threshold", 0)
self.pressure_threshold_duration = properties.get("pressure_threshold_duration", 0)
# Burning properties
self.burning = properties.get("burning", False)
self.burn_temperature = properties.get("burn_temperature", 0)
self.burn_duration = properties.get("burn_duration", 0)
self.burn_color = properties.get("burn_color", [255, 0, 0])
self.burn_rate = properties.get("burn_rate", 0)
self.burn_intensity = properties.get("burn_intensity", 0)
self.burn_rate_multiplier = properties.get("burn_rate_multiplier", 1.0)
@classmethod
def from_type(cls, simulation, position, particle_type, properties):
default_velocity = [0, 0]
default_mass = properties.get("mass", 1.0)
return cls(simulation, position, default_velocity, default_mass, particle_type, properties)

View File

@ -20,15 +20,17 @@ Key Components:
#Load the imports.
from config.settings import np, time, particle_properties
from physics.particle import Particle
# 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):
def __init__(self, simulation, 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
self.sim = simulation
# Core properties
self.size = properties.get("size", 1)
@ -83,12 +85,12 @@ class Particle:
self.burn_rate_multiplier = properties.get("burn_rate_multiplier", 1.0)
@classmethod
def from_type(cls, position, particle_type, properties):
def from_type(cls, simulation, 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)
return cls(simulation, position, default_velocity, default_mass, particle_type, properties)
class Simulation:
# the main class of the simulation.
@ -200,8 +202,8 @@ class Simulation:
if particle.particle_type == 'lava':
if particle.temperature < particle.solidify_temperature:
state_changed = self.transform_particle(x, y, particle.solidify)
return state_changed
return state_changed
# Check evaporation
if hasattr(particle, 'evaporate_temperature') and particle.evaporate_temperature is not None:
if particle.temperature >= particle.evaporate_temperature and particle.evaporate:
@ -294,13 +296,20 @@ class Simulation:
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
# Water + Sand = Wet Sand
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.create_mud(x1, y1, 'wsand') # Pass wsand type
self.particles[x2][y2] = None
self.active_particles.discard((x2, y2))
# Water + Dirt = Mud
if (particle1.particle_type == 'water' and particle2.particle_type == 'dirt' or
particle2.particle_type == 'water' and particle1.particle_type == 'dirt'):
self.create_mud(x1, y1, 'mud') # Pass mud type
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
@ -315,21 +324,30 @@ class Simulation:
if np.random.random() < 0.3: # 30% chance to ignite
self.transform_particle(target_x, target_y, 'fire')
# Add plasma effects
if particle1.particle_type == 'plasma' or particle2.particle_type == 'plasma':
target = particle2 if particle1.particle_type == 'plasma' else particle1
target_x, target_y = (x2, y2) if particle1.particle_type == 'plasma' else (x1, y1)
# Transfer high temperature to target
if hasattr(target, 'temperature'):
target.temperature += 100 # Rapid temperature increase
def create_mud(self, x, y): # this is where we create the mud. probably should be moved to handle_particle_interactions or process_interaction.
if 'mud' in self.particle_properties:
properties = self.particle_properties['mud']
new_particle = Particle.from_type((x, y), 'mud', properties)
def create_mud(self, x, y, mud_type):
"""Create either wet sand or mud based on the specified type"""
if mud_type in self.particle_properties:
properties = self.particle_properties[mud_type]
new_particle = Particle.from_type(self, (x, y), mud_type, 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.
def transform_particle(self, x, y, new_type):
"""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)
new_particle = Particle.from_type(self, (x, y), new_type, properties)
self.particles[x][y] = new_particle
self.active_particles.add((x, y))
@ -495,12 +513,12 @@ class Simulation:
continue
if particle.temperature > 1700:
if particle.temperature > 30000:
# Transition to gas
particle.is_gas = True
particle.temperature = 1700
particle.temperature = 30000
particle.velocity = [np.random.uniform(-1, 1), np.random.uniform(-1, 1)]
particle.temperature < 1400
particle.temperature < 30000
particle.is_gas = False
# Temperature spread to neighbors
@ -511,7 +529,7 @@ class Simulation:
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
heat_transfer = temp_diff * 0.3 * dt
particle.temperature -= heat_transfer
neighbor.temperature += heat_transfer
@ -543,6 +561,7 @@ class Simulation:
properties = self.particle_properties[particle_type]
position = (grid_x, grid_y)
new_particle = Particle(
simulation=self,
position=position,
velocity=[0, 0],
mass=properties.get('mass', 1.0),
@ -670,6 +689,7 @@ class Simulation:
if np.random.random() < 0.25 and new_y > 0:
properties = self.particle_properties['smoke']
new_smoke = Particle(
simulation=self,
position=(new_x, new_y-1),
velocity=[np.random.uniform(-0.5, 0.5), -1],
mass=properties.get('mass', 0.1),

72
src/physics/tests/base.py Normal file
View File

@ -0,0 +1,72 @@
from config.settings import np, time, engine_settings
from src.physics.particle import Particle, particle_properties
class SimulationBase:
def __init__(self, width: int, height: int, x: int = 0, y: int = 0):
self.dormant_particles = set()
self.particle_movement_counter = {}
self.DORMANT_THRESHOLD = 10
self.width = width
self.height = height
self.x = x
self.y = y
self.new_x = 0
self.new_y = 0
self.particle_size = 3
self.particles = [[None for _ in range(height)] for _ in range(width)]
print(f"Base init - particles type: {type(self.particles)}")
print(f"After initialization - particles type: {type(self.particles)}")
self.active_particles = set()
self.particle_count = 0
self.brush_size = 1
self.max_brush_size = 20
self.particle_properties = particle_properties
self.particle_types = list(self.particle_properties.keys())
self.current_particle_type = self.particle_types[0] if self.particle_types else 'sand'
self.gravity = 9.8
self.wind_zones = []
self.wind = [0.0, 0.0]
def create_particle(self, x, y): # this is where we create the particle.
if engine_settings ["enable_debug"]:
print(f"Before particle creation - particles type: {type(self.particles)}")
"""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 and
self.particles[grid_x][grid_y] is None):
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
)
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):
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
grid_x = (center_x + dx * self.particle_size) // self.particle_size
grid_y = (center_y + dy * self.particle_size) // self.particle_size
self.create_particle(grid_x, grid_y)

296
src/physics/tests/forces.py Normal file
View File

@ -0,0 +1,296 @@
"""
Physics force calculations and movement systems for Sandpypi
Handles particle movement, forces, and physics calculations
"""
from config.settings import np
from src.physics.particle import Particle
class ForceSystem:
def __init__(self, simulation):
self.sim = simulation
self.wind_zones = []
def calculate_forces(self, particle, x, y):
"""Calculate net forces acting on a particle."""
# Initialize forces as numpy array for vectorized operations
forces = np.zeros(2, dtype=np.float32) # [fx, fy]
# Vectorized wind zone calculations
if self.wind_zones:
positions = np.array([[zone['x'], zone['y']] for zone in self.wind_zones])
directions = np.array([zone['direction'] for zone in self.wind_zones])
strengths = np.array([zone['strength'] for zone in self.wind_zones])
radii = np.array([zone['radius'] for zone in self.wind_zones])
# Calculate distances vectorized
dx = x - positions[:, 0]
dy = y - positions[:, 1]
distances = np.sqrt(dx**2 + dy**2)
# Apply wind zone forces where distance <= radius
mask = distances <= radii
scale_factors = np.where(mask, (1 - distances/radii) * strengths[:, np.newaxis], 0)
forces += np.sum(directions * scale_factors[:, np.newaxis], axis=0)
# Apply global wind with vectorized operation
wind_factor = 0.5 if particle.is_gas else 1.0
forces += np.array(self.wind) * wind_factor
# Apply drag force vectorized
drag = particle.viscosity * -1
forces += drag * np.array(particle.velocity)
# Neighbor forces using numpy arrays
neighbors = np.array(self._get_neighbors_from_grid(x, y))
if len(neighbors):
valid_mask = (
(neighbors[:, 0] >= 0) &
(neighbors[:, 0] < self.width) &
(neighbors[:, 1] >= 0) &
(neighbors[:, 1] < self.height)
)
neighbors = neighbors[valid_mask]
for nx, ny in neighbors:
if (nx, ny) != (x, y):
neighbor = self.sim.particles[nx][ny]
if neighbor:
self._apply_neighbor_forces(particle, neighbor, forces[0], forces[1])
return forces[0], forces[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 apply_gravity(self, dt):
"""Handle only gravity and basic particle movement"""
self.sim.spatial_grid.spatial_grid.clear()
for x, y in list(self.sim.active_particles):
particle = self.sim.particles[x][y]
if not particle or particle.particle_type in ['wall', 'stone', 'wood']:
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.sim.particles[x][new_y] is None:
new_x, new_y = x, y + 1
else:
# Define diagonal directions first
diagonal_dirs = [(-1, 1), (1, 1)]
diagonal_dirs = np.array(diagonal_dirs)
# Randomize movement directions
diagonal_dirs = np.random.permutation(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.sim.particles[test_x][test_y] is None):
if np.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.sim.particles[x][new_y] is None:
new_x = x
new_y = y + 1
else:
spread_directions = np.array([(-1, 0), (1, 0)])
np.random.shuffle(spread_directions)
for dx, _ in spread_directions:
test_x = x + dx
if (0 <= test_x < self.width and
self.sim.particles[test_x][y] is None):
new_x = test_x
new_y = y
break
# Move particle if destination is empty
if self.sim.particles[new_x][new_y] is None:
self.sim.particles[x][y] = None
self.sim.particles[new_x][new_y] = particle
self.sim.particles.add((new_x, new_y))
self.sim.particles.discard((x, y))
particle.position = (new_x, new_y)
def apply_physics(self, dt, engine_settings):
"""Handle all physics effects"""
new_active_particles = set()
updates = []
for x, y in list(self.sim.active_particles):
particle = self.sim.particles[x][y]
if not particle:
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.sim.particles[x][y] = None
self.sim.particles.discard((x, y))
# Handle fire movement using numpy random
dx = np.random.uniform(-0.5, 0.5)
dy = np.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.sim.particles[new_x][new_y] is None:
self.sim.particles[new_x][new_y] = particle
new_active_particles.add((new_x, new_y))
# Generate smoke above with numpy random
if np.random.random() < 0.25 and new_y > 0:
properties = self.sim.particle_properties['smoke']
new_smoke = Particle(
position=(new_x, new_y-1),
velocity=[np.random.uniform(-0.5, 0.5), -1],
mass=properties.get('mass', 0.1),
particle_type='smoke',
properties=properties
)
if self.sim.particles[new_x][new_y-1] is None:
self.sim.particles[new_x][new_y-1] = new_smoke
new_active_particles.add((new_x, new_y-1))
# Dissipation chance using numpy random
if np.random.random() < 0.02:
continue
continue
# Air handling
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 movement with numpy random
dx = np.random.uniform(2, -1)
dy = np.random.uniform(-2, 0) # Bias upward
new_x = int(x + dx)
new_y = int(y + dy)
self.sim.particles[x][y] = None
self.sim.particles.discard((x, y))
if 0 <= new_x < self.width and 0 <= new_y < self.height:
if self.sim.particles[new_x][new_y] is None:
self.sim.particles[x][y] = None
self.sim.particles[new_x][new_y] = particle
new_active_particles.add((new_x, new_y))
self.sim.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 with numpy random
if np.random.random() < 0.5:
dx = np.random.choice([-1, 1])
if (0 <= x + dx < self.width and
self.sim.particles[x + dx][y] is None):
new_x = x + dx
new_y = y
self.sim.particles[x][y] = None
self.sim.particles[new_x][new_y] = particle
new_active_particles.add((new_x, new_y))
continue
new_x = int(x + particle.velocity[0] * dt)
new_y = int(y + particle.velocity[1] * dt)
# Update position
if 0 <= new_x < self.width and 0 <= new_y < self.height:
if self.sim.particles[new_x][new_y] is None:
updates.append((x, y, new_x, new_y, particle))
new_active_particles.add((new_x, new_y))
self._wake_neighbors(new_x, new_y)
else:
new_active_particles.add((x, y))
# Apply updates
for old_x, old_y, new_x, new_y, particle in updates:
self.sim.particles[old_x][old_y] = None
self.sim.particles[new_x][new_y] = particle
particle.position = (new_x, new_y)
# Handle boundaries
if engine_settings['outerwall']:
if x <= 0 or x >= self.width-1 or y <= 0 or y >= self.height-1:
if self.sim.particles[x][y] is None:
properties = self.sim.particle_properties['wall']
wall = Particle.from_type((x, y), 'wall', properties)
self.sim.particles[x][y] = wall
new_active_particles.add((x, y))
self.particle_count += 1
continue
else:
if x <= 0 or x >= self.width-1 or y <= 0 or y >= self.height-1:
if self.sim.particles[x][y] is not None:
self.sim.particles[x][y] = None
self.sim.particles.discard((x, y))
self.sim.particle_count -= 1
continue
self.sim.particles = new_active_particles
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 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 = np.random.uniform(-1, 1)
dy = np.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.sim.particles[new_x][new_y] is None:
self.sim.particles[x][y] = None
self.sim.particles[new_x][new_y] = particle
self.sim.particles.add((new_x, new_y))
self.sim.particles.discard((x, y))

87
src/physics/tests/grid.py Normal file
View File

@ -0,0 +1,87 @@
"""
Spatial grid management systems for Sandpypi
Handles particle positioning and neighbor calculations
"""
from physics.base import SimulationBase
class SpatialGrid:
def __init__(self, simulation):
self.sim = simulation
self.width = simulation.width
self.height = simulation.height
self.cell_size = 32
self.spatial_grid = {}
def update_spatial_grid(self):
"""Enhanced spatial grid update"""
if len(self.sim.active_particles) > 100:
self.spatial_grid = {}
cell_lists = {}
# Track temperature-sensitive particles
temp_sensitive_cells = set()
for x, y in self.sim.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))
# Mark cells with temperature-sensitive particles
particle = self.sim.particles[x][y]
if hasattr(particle, 'solidify_temperature') or hasattr(particle, 'melt_temperature'):
temp_sensitive_cells.add(cell_key)
self.spatial_grid = {k: set(v) for k, v in cell_lists.items()}
return temp_sensitive_cells
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 _get_neighbors_from_grid(self, x, y):
"""Get neighbors using spatial grid"""
cell_key = self.get_cell_key(x, y)
neighbors = []
# Check current and adjacent cells
for dx in [-1, 0, 1]:
for dy in [-1, 0, 1]:
check_key = (cell_key[0] + dx, cell_key[1] + dy)
if check_key in self.spatial_grid:
neighbors.extend(self.spatial_grid[check_key])
return neighbors
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 _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

View File

@ -0,0 +1,118 @@
"""
Particle interaction handling systems for Sandpypi
Manages how different particle types interact with each other
"""
from config.settings import np
from src.physics.particle import Particle
from physics.base import SimulationBase
class ParticleInteractions:
def __init__(self, simulation):
self.sim = simulation
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.sim.active_particles):
particle = self.sim.particles[x][y]
if not particle:
continue
# Handle damage for any particle with durability
self.handle_particle_damage(particle, x, y)
# 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 = Wet Sand
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, 'wsand') # Pass wsand type
self.particles[x2][y2] = None
self.active_particles.discard((x2, y2))
# Water + Dirt = Mud
if (particle1.particle_type == 'water' and particle2.particle_type == 'dirt' or
particle2.particle_type == 'water' and particle1.particle_type == 'dirt'):
self.create_mud(x1, y1, 'mud') # Pass mud type
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 np.random.random() < 0.3: # 30% chance to ignite
self.transform_particle(target_x, target_y, 'fire')
def mix_liquids(self, liquid1, liquid2): # not implemented
"""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 create_mud(self, x, y, mud_type):
"""Create either wet sand or mud based on the specified type"""
if mud_type in self.particle_properties:
properties = self.particle_properties[mud_type]
new_particle = Particle.from_type((x, y), mud_type, properties)
self.particles[x][y] = new_particle
self.active_particles.add((x, y))
def handle_particle_damage(self, particle, x, y):
"""Handle damage calculations for particles with durability"""
if not hasattr(particle, 'durability'):
return
# Pressure damage
fx, fy = self.calculate_forces(particle, x, y)
pressure = (fx*fx + fy*fy)**0.5 # Calculate magnitude of force vector
if hasattr(particle, 'pressure_threshold') and pressure > particle.pressure_threshold:
particle.durability -= 0.1
# Impact damage
neighbors = self._get_neighbors_from_grid(x, y)
for nx, ny in neighbors:
neighbor = self.particles[nx][ny]
if neighbor and neighbor.velocity[1] > 5.0:
particle.durability -= 0.2
# Heat damage
if particle.temperature > 900:
particle.durability -= 1
# Check if particle should break
if particle.durability <= 0 and hasattr(particle, 'broken'):
self.transform_particle(x, y, particle.broken)
return
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)]]

View File

@ -0,0 +1,73 @@
from physics.base import SimulationBase, time, np
from physics.grid import SpatialGrid as sg
from physics.forces import ForceSystem as fs
from physics.temperature import TemperatureSystem as ts
from physics.interactions import ParticleInteractions as pi
from src.physics.particle import Particle, particle_properties
from physics.sim import Simulation
class Simulation(SimulationBase):
def __init__(self, width, height, x=0, y=0):
print(f"Simulation init - before base init - particles type: {type(getattr(self, 'particles', None))}")
SimulationBase.__init__(self, width, height, x, y)
print(f"After base init - particles type: {type(self.particles)}")
self.dormant_particles = set()
self.particle_movement_counter = {}
self.DORMANT_THRESHOLD = 10
self.sim = Simulation(width, height)
self.spatial_grid = sg(self)
self.force_system = fs(self)
self.temperature_system = ts(self)
self.particle_interactions = pi(self)
def simulate_step(self, dt, engine_settings):
"""Run simulation step with spatial grid updates"""
self.spatial_grid.update_spatial_grid()
# Update particle positions and physics
self.force_system.apply_gravity(dt)
self.sim.Simulation.apply_physics(dt, engine_settings)
# Handle state changes and interactions
self.temperature_system.handle_temperature(dt)
self.particle_interactions.handle_particle_interactions(dt)
self.temperature_system.burning()
self.temperature_system.spread_fire()
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 reset_particle_count(self):
"""Reset and recalculate accurate particle count"""
active_count = np.sum([1 for x, y in self.active_particles if self.particles[x][y] is not None])
self.particle_count = int(active_count)
def get_accurate_particle_count(self):
"""Get current accurate particle count using numpy"""
particle_mask = np.array([[self.particles[x][y] is not None
for y in range(self.height)]
for x in range(self.width)])
return np.sum(particle_mask)

View File

@ -0,0 +1,162 @@
"""
Temperature and state management systems for Sandpypi
Handles temperature changes, phase transitions, and burning mechanics
"""
from config.settings import np
class TemperatureSystem:
def __init__(self, simulation):
self.sim = simulation
def handle_temperature(self, dt): # this is where we handle the temperature.
"""Handle temperature changes and state transitions"""
for x, y in list(self.sim.active_particles):
particle = self.sim.particles[x][y]
if particle and particle.is_gas:
particle.temperature -= 0.5 * dt
if not particle:
continue
if particle.temperature > 1700:
# Transition to gas
particle.is_gas = True
particle.temperature = 1700
particle.velocity = [np.random.uniform(-1, 1), np.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.sim.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 handle_phase_transitions(self, particle, x, y): # this is where we handle all the phase transitions.
"""Handle all phase transitions for a particle"""
state_changed = False
if particle.particle_type == 'lava':
if particle.temperature < particle.solidify_temperature:
state_changed = self.transform_particle(x, y, particle.solidify)
return state_changed
# 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)
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.sim.particle:
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.sim.particle_properties:
self.transform_particle(x, y, new_type)
return new_type
if particle.particle_type == 'steam':
# Steam should condense when it cools
if particle.temperature <= particle.solidify_temperature:
self.transform_particle(x, y, new_type)
return new_type
# Check durability property from JSON
if (hasattr(particle, 'durability') and hasattr(particle, 'brk')
and particle.brk is not None):
if particle.durability <= 0:
self.transform_particle(x, y, particle.broken)
return new_type
def burning(self): # this is where we handle the burning.
"""Handle burning of particles."""
for x, y in list(self.sim.active_particles):
particle = self.sim.particles[x][y]
if particle and hasattr(particle, 'burning') and particle.burning:
particle.temperature += 10
if particle.temperature > 1000:
self.sim.particles[x][y] = None
self.sim.active_particles.remove((x, y))
self.spatial_grid.pop((x, y), None)
def spread_fire(self): # this is where we spread the fire.
"""Spread fire to neighboring particles."""
for x, y in list(self.sim.active_particles):
particle = self.sim.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.sim.particles[nx][ny]
if neighbor and hasattr(neighbor, 'flamability'):
if neighbor.particle_type == 'wood':
# Higher chance to ignite wood
if np.random.random() < 0.3: # 30% chance to spread
self.ignite_particle(neighbor)
elif neighbor.flamability > 0:
if np.random.random() < 0.1: # 10% chance for other materials
self.ignite_particle(neighbor)
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 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 np.random.random() < 0.6: # % chance
self.sim.particles[x][y] = None
self.sim.active_particles.discard((x, y))
if particle.particle_type in ['fire', 'flame', 'lava']:
# Create smoke above with proper physics
if np.random.random() < 0.65 and y > 0: # % chance for smoke
properties = self.sim.particle_properties['smoke']
new_smoke = self.sim.Particle.from_type((x, y-1), 'smoke', properties)
if self.sim.particles[x][y-1] is None:
self.sim.particles[x][y-1] = new_smoke
self.sim.active_particles.add((x, y-1))
else:
# Handle collision with water
if particle.particle_type == 'water':
self.sim.particles[x][y-1] = None
self.sim.active_particles.discard((x, y-1))
self.sim.particles[x][y] = None
self.sim.active_particles.discard((x, y))
self.sim.particles[x][y] = self.sim.Particle.from_type((x, y), 'water', self.sim.particle_properties['water'])
self.sim.active_particles.add((x, y))

View File

@ -23,7 +23,6 @@ The `clear_screen` function is used to reset the simulation grid and clear the d
from config.settings import pygame, random, particle_properties, engine_settings
from debug.debugger_system import DebuggerSystem
class Rendering:
@ -49,7 +48,6 @@ class Rendering:
'zoom_window': pygame.font.SysFont(None, 20),
'zoom_window_text': pygame.font.SysFont(None, 20)
}
self.debug = DebuggerSystem()
# Pre-render static UI elements
self.button_surfaces = {}
@ -286,13 +284,52 @@ class Rendering:
def draw_debug_overlay(self, fps, sim):
"""Draws debug overlay on the screen."""
if engine_settings['enable_fps'] or engine_settings['enable_debug']:
self.debug.screen = self.screen # Pass screen reference
self.debug.cached_fonts = self.cached_fonts # Pass font references
self.debug.track_fps()
self.debug.update_performance_metrics(sim)
self.debug.draw_debug_overlay(self.screen, sim)
if not engine_settings['enable_fps'] and not engine_settings['enable_debug']:
return
# Create static debug surface if not exists
if not hasattr(self, 'debug_surface'):
self.debug_surface = pygame.Surface((300, 150), pygame.SRCALPHA)
# Only update when values change significantly
mouse_x, mouse_y = pygame.mouse.get_pos()
grid_x, grid_y = mouse_x // 3, mouse_y // 3
current_info = (
int(fps),
int(sim.track_tps()),
mouse_x // 10, # Reduced update frequency
mouse_y // 10,
sim.particle_count
)
if not hasattr(self, '_last_debug_info') or current_info != self._last_debug_info:
self._last_debug_info = current_info
self.debug_surface.fill((0, 0, 0, 0))
font = self.cached_fonts['debug']
y_offset = 10
if engine_settings['enable_fps']:
fps_surf = font.render(f"FPS: {fps:.1f} | TPS: {sim.track_tps():.1f}", True, (255, 255, 255))
self.debug_surface.blit(fps_surf, (10, y_offset))
y_offset += 25
if engine_settings['enable_debug']:
debug_lines = [
f"Mouse: ({mouse_x}, {mouse_y})",
f"Grid: ({grid_x}, {grid_y})",
f"Particles: {sim.particle_count}"
]
for line in debug_lines:
text_surf = font.render(line, True, (255, 255, 255))
self.debug_surface.blit(text_surf, (10, y_offset))
y_offset += 25
# Single blit of cached surface
self.screen.blit(self.debug_surface, (0, 0))
def draw_buttons(self): # this is the function that draws the buttons
self.buttons = {}

View File

@ -20,12 +20,6 @@ from config.settings import pygame, engine_settings, cProfile, pstats, time
from rendering.rendering import Rendering
from physics.sim import Simulation
"""
This is for the future physics engine until i figure out a better method used for testing right now.
#import os
#import sys
#sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
"""
def handle_input(event, sim, rendering, settings_visible, zoom_active, zoom_locked, zoom_pos):