diff --git a/src/config/settings.py b/src/config/settings.py index 3e7a043..d33cb39 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -22,7 +22,7 @@ import time import numpy as np engine_settings = { - 'pause_sim': True, + 'pause_sim': False, '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'] +__all__ = ['pygame', 'np', 'random', 'time', 'engine_settings', 'particle_properties', 'cProfile', 'pstats', 'sys', 'os', 'jit'] diff --git a/src/debug/debugger_system.py b/src/debug/debugger_system.py new file mode 100644 index 0000000..2cadd65 --- /dev/null +++ b/src/debug/debugger_system.py @@ -0,0 +1,66 @@ +from config.settings import engine_settings, pygame +import time + +class DebuggerSystem: + def __init__(self): + self.debug_surface = pygame.Surface((300, 150), pygame.SRCALPHA) + self.debug_font = pygame.font.SysFont(None, 24) + self.fps_counter = 0 + self.fps_timer = time.time() + self.current_fps = 0 + self.debug_text = [] + self.performance_metrics = { + 'particle_updates': 0, + 'physics_time': 0, + 'render_time': 0 + } + + def track_fps(self): + """Track FPS with high precision""" + self.fps_counter += 1 + current_time = time.time() + elapsed = current_time - self.fps_timer + + if elapsed >= 1.0: + self.current_fps = self.fps_counter / elapsed + self.fps_counter = 0 + self.fps_timer = current_time + return self.current_fps + + def update_performance_metrics(self, sim): + """Track simulation performance metrics""" + self.performance_metrics['particle_updates'] = len(sim.active_particles) + return self.performance_metrics + + def draw_debug_overlay(self, screen, sim): + if not engine_settings['enable_fps'] and not engine_settings['enable_debug']: + return + + self.debug_surface.fill((0, 0, 0, 128)) + y_offset = 10 + + if engine_settings['enable_fps']: + fps_text = f"FPS: {self.current_fps:.1f} | TPS: {sim.track_tps():.1f}" + fps_surf = self.debug_font.render(fps_text, True, (255, 255, 255)) + self.debug_surface.blit(fps_surf, (10, y_offset)) + y_offset += 25 + + if engine_settings['enable_debug']: + debug_info = [ + f"Active Particles: {len(sim.active_particles)}", + f"Total Particles: {sim.particle_count}", + f"Current Type: {sim.current_particle_type}", + f"Brush Size: {sim.brush_size}" + ] + + for line in debug_info: + text_surf = self.debug_font.render(line, True, (255, 255, 255)) + self.debug_surface.blit(text_surf, (10, y_offset)) + y_offset += 25 + + screen.blit(self.debug_surface, (0, 0)) + + def log_error(self, error_msg): + """Log errors for debugging""" + with open('debug_log.txt', 'a') as f: + f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - {error_msg}\n") diff --git a/src/physics/sim.py b/src/physics/sim.py index 839f25c..876b344 100644 --- a/src/physics/sim.py +++ b/src/physics/sim.py @@ -19,7 +19,7 @@ Key Components: """ #Load the imports. -from config.settings import random, time, particle_properties +from config.settings import np, time, particle_properties # Load particle properties from json so we know what particles we got and how they should be simulated. @@ -112,15 +112,26 @@ class Simulation: 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 # 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 + """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) + + 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 @@ -238,7 +249,9 @@ class Simulation: return # Pressure damage - pressure = self.calculate_forces(particle, x, y) + 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 @@ -299,7 +312,7 @@ class Simulation: # Wood to Fire elif target.particle_type == 'wood': - if random.random() < 0.3: # 30% chance to ignite + if np.random.random() < 0.3: # 30% chance to ignite self.transform_particle(target_x, target_y, 'fire') @@ -324,8 +337,8 @@ class Simulation: 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 + 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) @@ -349,43 +362,55 @@ class Simulation: self.wind_zones.append(wind_zone) - def calculate_forces(self, particle, x, y): # this is where we calculate the forces. + + def calculate_forces(self, particle, x, y): """Calculate net forces acting on a particle.""" - fx, fy = 0.0, 0.0 # Initialize forces + # Initialize forces as numpy array for vectorized operations + forces = np.zeros(2, dtype=np.float32) # [fx, fy] - # 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']) + # 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 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 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 + # Apply drag force vectorized drag = particle.viscosity * -1 - fx += drag * particle.velocity[0] - fy += drag * particle.velocity[1] + forces += drag * np.array(particle.velocity) - # Check neighboring particles - neighbors = self._get_neighbors_from_grid(x, y) - for nx, ny in neighbors: - if(nx, ny) != (x, y): - if 0 <= nx < self.width and 0 <= ny < self.height: + # 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.particles[nx][ny] if neighbor: - self._apply_neighbor_forces(particle, neighbor, fx, fy) - + self._apply_neighbor_forces(particle, neighbor, forces[0], forces[1]) - return fx, fy + return forces[0], forces[1] def _get_quick_neighbors(self, x, y): @@ -426,23 +451,23 @@ class Simulation: 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 + if np.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 + if np.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 + if np.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 + if np.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: @@ -474,7 +499,7 @@ class Simulation: # Transition to gas particle.is_gas = True particle.temperature = 1700 - particle.velocity = [random.uniform(-1, 1), random.uniform(-1, 1)] + particle.velocity = [np.random.uniform(-1, 1), np.random.uniform(-1, 1)] particle.temperature < 1400 particle.is_gas = False @@ -511,7 +536,10 @@ class Simulation: grid_x = x // self.particle_size grid_y = y // self.particle_size - if 0 <= grid_x < self.width and 0 <= grid_y < self.height: + 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( @@ -521,11 +549,10 @@ class Simulation: 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 + + 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. @@ -544,10 +571,10 @@ class Simulation: return None - def apply_gravity(self, dt): # this is where we apply gravity. + def apply_gravity(self, dt): """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 in ['wall', 'stone', 'wood']: @@ -566,15 +593,18 @@ class Simulation: if self.particles[x][new_y] is None: new_x, new_y = x, y + 1 else: - # Try diagonal movement with randomization + # Define diagonal directions first diagonal_dirs = [(-1, 1), (1, 1)] - random.shuffle(diagonal_dirs) + 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.particles[test_x][test_y] is None): - if random.random() < 0.8: # 80% chance to move diagonally + if np.random.random() < 0.8: # 80% chance to move diagonally new_x = test_x new_y = test_y break @@ -585,8 +615,8 @@ class Simulation: new_x = x new_y = y + 1 else: - spread_directions = [(-1, 0), (1, 0)] - random.shuffle(spread_directions) + 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 @@ -604,8 +634,7 @@ class Simulation: particle.position = (new_x, new_y) - - def apply_physics(self, dt, engine_settings): # this is where we apply physics. + def apply_physics(self, dt, engine_settings): """Handle all physics effects""" new_active_particles = set() updates = [] @@ -615,8 +644,6 @@ class Simulation: if not particle: continue - - # Skip wall physics - walls are immutable if particle.particle_type == 'wall': new_active_particles.add((x, y)) @@ -628,9 +655,9 @@ class Simulation: 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 + # 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) @@ -639,12 +666,12 @@ class Simulation: 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: + # Generate smoke above with numpy random + if np.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], + velocity=[np.random.uniform(-0.5, 0.5), -1], mass=properties.get('mass', 0.1), particle_type='smoke', properties=properties @@ -653,13 +680,12 @@ class Simulation: 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: + # Dissipation chance using numpy random + if np.random.random() < 0.02: continue - continue - # Air handling - particles can pass through should implement proper air instead of a particle + # Air handling if particle.particle_type == 'air': continue @@ -669,17 +695,17 @@ class Simulation: # Calculate forces fx, fy = self.calculate_forces(particle, x, y) - # handle gas particles + # Handle gas particles if particle.is_gas: - # Gas-specific movement - dx = random.uniform(2, -1) - dy = random.uniform(-2, 0) # Bias upward + # 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.particles[x][y] = None self.active_particles.discard((x, y)) - # Check if the new position is within bounds + 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 @@ -694,10 +720,9 @@ class Simulation: 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]) + # 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.particles[x + dx][y] is None): new_x = x + dx @@ -715,33 +740,29 @@ class Simulation: if self.particles[new_x][new_y] is None: updates.append((x, y, new_x, new_y, particle)) new_active_particles.add((new_x, new_y)) - # Wake up neighboring dormant particles self._wake_neighbors(new_x, new_y) else: new_active_particles.add((x, y)) - # Apply updates and return new active set + # Apply updates 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) - # Handle boundaries based on settings - + # Handle boundaries 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 + self.particle_count += 1 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: + if self.particles[x][y] is not None: self.particles[x][y] = None self.active_particles.discard((x, y)) self.particle_count -= 1 diff --git a/src/rendering/rendering.py b/src/rendering/rendering.py index 05f7b89..034e549 100644 --- a/src/rendering/rendering.py +++ b/src/rendering/rendering.py @@ -23,6 +23,7 @@ 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: @@ -48,6 +49,7 @@ 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 = {} @@ -284,52 +286,13 @@ class Rendering: def draw_debug_overlay(self, fps, 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)) - + """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) def draw_buttons(self): # this is the function that draws the buttons self.buttons = {} diff --git a/src/sandpypi.py b/src/sandpypi.py index 5e8d16a..910626a 100644 --- a/src/sandpypi.py +++ b/src/sandpypi.py @@ -15,7 +15,7 @@ The main loop runs at a target frame rate of 60 FPS (this fps varies on my mood """ # Import Require files for the Engine. -from config.settings import pygame, engine_settings, cProfile, pstats +from config.settings import pygame, engine_settings, cProfile, pstats, time from rendering.rendering import Rendering from physics.sim import Simulation @@ -274,9 +274,14 @@ def main(): if mouse_down_left and not over_button: x, y = mouse_pos if sim.current_particle_type not in ['wind', 'air']: + current_time = time.time() + if not hasattr(main, 'last_particle_time'): + main.last_particle_time = 0 + + # Limit particle creation to every 16ms (approximately 60 FPS) + if current_time - main.last_particle_time >= 0.016: sim.create_particle_circle(x, y) - else: - sim.add_wind_zone(x, y) + main.last_particle_time = current_time if mouse_down_right: x, y = mouse_pos