- gravity and physics are the same thing now.

- __init__.py added to all folders
- preparing docs
- .gitignore updated
- .vscode/settings.json updated
- mypy.ini updated
- pyproject.toml updated
- plansandideas/Coding optimization plan.md added
- plansandideas/REORGANIZATION.md added
- scripts/lint.py added
- scripts/setup_dev.py added
- requirements-dev.txt added
- and more.
This commit is contained in:
Stan44 2025-01-05 03:35:17 -06:00
parent c225632aac
commit d05e64839f
32 changed files with 1613 additions and 1748 deletions

62
.gitea/workflows/ci.yml Normal file
View File

@ -0,0 +1,62 @@
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13.1"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .[dev]
- name: Run linters
run: |
black . --check
isort . --check
# flake8 .
- name: Run tests
run: |
pytest tests/ --cov=src --cov-report=xml
# Deployment job - uncomment and configure when PyPI uploads are needed
# This job will build and upload your package to PyPI when merging to main
#
# deploy:
# needs: test
# runs-on: ubuntu-latest
# if: github.event_name == 'push' && github.ref == 'refs/heads/main'
#
# steps:
# - uses: actions/checkout@v3
#
# - name: Set up Python
# uses: actions/setup-python@v3
# with:
# python-version: "3.8"
#
# - name: Build and publish
# env:
# TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
# TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
# run: |
# pip install build twine
# python -m build
# twine upload dist/*

178
.gitignore vendored
View File

@ -1,12 +1,10 @@
# ---> Python
# Byte-compiled / optimized / DLL files # Python
__pycache__/
.mypy_cache/
*.py[cod] *.py[cod]
*$py.class *$py.class
__pycache__/
# C extensions
*.so *.so
# Distribution / packaging
.Python .Python
build/ build/
develop-eggs/ develop-eggs/
@ -20,158 +18,36 @@ parts/
sdist/ sdist/
var/ var/
wheels/ wheels/
share/python-wheels/
*.egg-info/ *.egg-info/
.installed.cfg .installed.cfg
*.egg *.egg
MANIFEST
# PyInstaller # Virtual Environment
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/ venv/
env/
ENV/ ENV/
env.bak/ .venv/
venv.bak/ .*_venv/
.env
# Spyder project settings # IDE
.spyderproject .idea/
.spyproject .vscode/
*.swp
*.swo
# Rope project settings # Testing
.ropeproject .coverage
htmlcov/
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Custom Ignores
current_stations.html
forecast_data.json
openapi.json
__pycache__/sim.cpython-312.pyc
__pycache__/sim.cpython-312.pyc
__pycache__/rendering.cpython-312.pyc
sandpypi.dist/
sandpypi.build/
sandpypi.onefile-build/
sandpypi.exe
sandpypi.7z
unittest/ unittest/
.7z .pytest_cache/
.zip
# Distribution
dist/
build/
*.exe
# Logs
logs/
*.log
livenotes.txt livenotes.txt

View File

@ -0,0 +1,30 @@
#repos:
#- repo: https://github.com/psf/black
# rev: 23.12.1
# hooks:
# - id: black
# language_version: python3
# args: ["--line-length", "79"]
#- repo: https://github.com/pycqa/isort
# rev: 5.13.2
# hooks:
# - id: isort
# args: ["--profile", "black", "--filter-files"]
#- repo: https://github.com/pre-commit/pre-commit-hooks
# rev: v4.5.0
# hooks:
# - id: trailing-whitespace
# - id: end-of-file-fixer
# - id: check-yaml
# - id: check-added-large-files
#- repo: local
# hooks:
# - id: pytest
# name: pytest
# entry: pytest
# language: system
# pass_filenames: false
# always_run: false

View File

@ -1,3 +1,7 @@
{ {
"python.linting.pylintArgs": [
"--disable=E1101",
"--ignored-modules=pygame"
],
"cody.agentic.context.experimentalShell": true "cody.agentic.context.experimentalShell": true
} }

View File

@ -1,16 +0,0 @@
from src.config.settings import cProfile, pstats
from src.sandpypi import main
if __name__ == "__main__":
# Profile the application
profiler = cProfile.Profile()
profiler.enable()
main()
profiler.disable()
# Write profiling results to file
with open('profile_results.log', 'w') as f:
stats = pstats.Stats(profiler, stream=f)
stats.sort_stats('cumulative')
stats.print_stats()

33
__init__.py Normal file
View File

@ -0,0 +1,33 @@
"""
Sandpypi - A falling sand simulation package in Python.
This package provides modules for particle physics simulation and rendering:
- config.settings: Configuration and particle properties
- physics.sim: Physics engine and particle behavior
- rendering.rendering: Display and visualization components
"""
from src.config.settings import (
cProfile,
engine_settings,
particle_properties,
pstats,
time,
)
from src.physics.sim import Simulation
from src.rendering.rendering import Rendering
__all__ = [
"cProfile",
"pstats",
"time",
"engine_settings",
"particle_properties",
"Simulation",
"Rendering",
]
# src\rendering\rendering.py"
# src\physics\sim.py
# src\config\settings.py
# src\physics\particle.py
# src\debug\debugger_system.py

22
docs/conf.py Normal file
View File

@ -0,0 +1,22 @@
""" Configuration file for Sphinx documentation"""
PROJECT = "sandpypi"
COPYRIGHT = "2024, stan44"
AUTHOR = "stan44"
VERSION = "0.1.0"
RELEASE = "0.1.0"
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.napoleon",
"sphinx.ext.viewcode",
"sphinx.ext.githubpages",
"sphinx_rtd_theme",
]
templates_path = ["_templates"]
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
HTML_THEME = "sphinx_rtd_theme"
html_static_path = ["_static"]
# html_logo = "_static/logo.png" # Add your project's logo

29
docs/index.rst Normal file
View File

@ -0,0 +1,29 @@
Welcome to Sandpypi Documentation
===============================
.. toctree::
:maxdepth: 2
:caption: Contents:
guides/getting_started
guides/user_guide
architecture/overview
api/modules
Development Standards
------------------
This project follows these development standards:
* Black code formatting (88 characters line length)
* Type hints for Python functions
* Google style docstrings
* Particle simulation physics
* Optimized rendering with Pygame
Quick Links
----------
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

4
mypy.ini Normal file
View File

@ -0,0 +1,4 @@
[mypy]
namespace_packages = True
explicit_package_bases = True
python_version = 3.13

View File

@ -0,0 +1,46 @@
targeted performance improvements while maintaining core plan:
1. Spatial Grid Optimizations:
```python
def update_spatial_grid(self):
# Use direct array operations instead of dict
# Pre-allocate grid arrays for particle positions
# Batch update active cells
```
2. Force Calculations:
```python
def calculate_forces(self, particle, x, y):
# Vectorize neighbor force calculations
# Cache common force values
# Use direct array math instead of loops
```
3. Temperature System:
```python
def handle_temperature(self, dt):
# Batch temperature updates
# Use array operations for heat transfer
# Pre-calculate temperature thresholds
```
4. Particle Creation:
```python
def create_particle_circle(self):
# Pre-calculate circle bounds
# Batch particle creation
# Use array operations for position checks
```
5. Phase Transitions:
```python
def handle_phase_transitions(self):
# Group similar transitions
# Cache property lookups
# Batch state changes
```

59
pyproject.toml Normal file
View File

@ -0,0 +1,59 @@
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "sandpypi"
version = "0.1.0"
authors = [
{name = "Stan44", email = "stan44@example.com"},
]
description = "Falling Sand Simulation in Python"
readme = "README.md"
requires-python = ">=3.8"
dependencies = [
"numpy>=1.21.0",
"pygame>=2.1.0"
]
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"pytest-cov>=4.0",
"black>=22.0",
"flake8>=4.0",
"isort>=5.0",
"mypy>=0.9",
"pylint>=2.10",
"sphinx>=4.0",
"sphinx-rtd-theme>=1.0"
]
[tool.black]
line-length = 79
target-version = ['py38']
[tool.isort]
profile = "black"
multi_line_output = 3
line_length = 79
force_grid_wrap = 0
include_trailing_comma = true
use_parentheses = true
ensure_newline_before_comments = true
[tool.flake8]
max-line-length = 79
[tool.mypy]
python_version = "3.8"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
check_untyped_defs = true
strict_optional = true
[tool.pylint]
max-line-length = 79
ignored-modules = ["pygame"]
disable = ["E1101"]

7
requirements-dev.txt Normal file
View File

@ -0,0 +1,7 @@
-r requirements.txt
pytest>=7.0
pytest-cov>=4.0
black>=22.0
isort>=5.0
mypy>=0.9

View File

@ -3,32 +3,40 @@
# Sandpypi by Stanton. # Sandpypi by Stanton.
# Project name is a placeholder. # Project name is a placeholder.
# This has been a multimonth or year project i have time blindness sorta. # This has been a multimonth or year project i have time blindness sorta.
# This is my most functional system for falling sand in python yet i took some things i learned in JS. # This is my most functional system for falling sand in python yet i took
# some things i learned in JS.
# This needs further optimizations to core performance sections. # This needs further optimizations to core performance sections.
The main function to run the Sandpypi program. The main function to run the Sandpypi program.
This function initializes the Pygame environment, creates the simulation and rendering objects, and enters the main event loop. This function initializes the Pygame environment, creates the simulation and
It handles user input events such as mouse clicks, mouse wheel scrolling, and keyboard presses. rendering objects, and enters the main event loop.
It also updates the simulation, draws the particles, buttons, and other UI elements, and manages the settings menu. It also updates the simulation, draws the particles, buttons, and other UI
The main loop runs at a target frame rate of 60 FPS (this fps varies on my mood and the testing), with the actual frame rate displayed in the debug overlay. elements, and manages the settings menu.
The main loop runs at a target frame rate of 60 FPS
(this fps varies on my mood and the testing), with the actual frame rate
displayed in the debug overlay.
""" """
# Import Require files for the Engine. # Import Require files for the Engine.
from config.settings import pygame, engine_settings, cProfile, pstats, time import pygame
from rendering.rendering import Rendering from src.config.settings import cProfile, engine_settings, pstats, time
from physics.sim import Simulation from src.physics.sim import Simulation
from src.rendering.rendering import Rendering
def handle_input(
def handle_input(event, sim, rendering, settings_visible, zoom_active, zoom_locked, zoom_pos): event, sim, rendering, settings_visible, zoom_active, zoom_locked, zoom_pos
):
"""Handle all input events""" """Handle all input events"""
if event.type == pygame.MOUSEBUTTONDOWN: if event.type == pygame.MOUSEBUTTONDOWN:
return handle_mouse_down(event, sim, rendering, settings_visible, zoom_active) return handle_mouse_down(
elif event.type == pygame.MOUSEBUTTONUP: event, sim, rendering, settings_visible, zoom_active
)
if event.type == pygame.MOUSEBUTTONUP:
return handle_mouse_up(event) return handle_mouse_up(event)
elif event.type == pygame.KEYDOWN: if event.type == pygame.KEYDOWN:
return handle_key_press(event, rendering, sim) return handle_key_press(event, rendering, sim)
return None return None
@ -47,44 +55,57 @@ def handle_mouse_down(event, sim, rendering, settings_visible, zoom_active):
elif event.button == 5: # Mouse wheel down elif event.button == 5: # Mouse wheel down
sim.brush_size = max(sim.brush_size - 1, 1) sim.brush_size = max(sim.brush_size - 1, 1)
elif event.button == 1: # Left click elif event.button == 1: # Left click
return handle_left_click(mouse_pos, sim, rendering, settings_visible, in_settings_area, zoom_active) return handle_left_click(
mouse_pos,
sim,
rendering,
settings_visible,
in_settings_area,
zoom_active,
)
elif event.button == 3: # Right click elif event.button == 3: # Right click
return {'mouse_down_right': True} return {"mouse_down_right": True}
elif event.button == 2: # Middle click elif event.button == 2: # Middle click
return {'mouse_down_middle': True} return {"mouse_down_middle": True}
return {} return {}
def handle_left_click(mouse_pos, sim, rendering, settings_visible, in_settings_area, zoom_active): def handle_left_click(
mouse_pos, sim, rendering, settings_visible, in_settings_area, zoom_active
):
"""Handle left click interactions""" """Handle left click interactions"""
result = {'mouse_down_left': False, 'settings_visible': settings_visible, 'over_button': False} result = {
"mouse_down_left": False,
"settings_visible": settings_visible,
"over_button": False,
}
if rendering.settings_button.collidepoint(mouse_pos): if rendering.settings_button.collidepoint(mouse_pos):
result['settings_visible'] = not settings_visible result["settings_visible"] = not settings_visible
result['over_button'] = True result["over_button"] = True
return result return result
if zoom_active: if zoom_active:
result['zoom_locked'] = not result.get('zoom_locked', False) result["zoom_locked"] = not result.get("zoom_locked", False)
if result['zoom_locked']: if result["zoom_locked"]:
result['zoom_pos'] = mouse_pos result["zoom_pos"] = mouse_pos
return result return result
if settings_visible and in_settings_area: if settings_visible and in_settings_area:
handle_settings_click(mouse_pos, sim) handle_settings_click(mouse_pos)
result['over_button'] = True result["over_button"] = True
return result return result
if not in_settings_area: if not in_settings_area:
if handle_ui_click(mouse_pos, sim, rendering): if handle_ui_click(mouse_pos, sim, rendering):
result['over_button'] = True result["over_button"] = True
else: else:
result['mouse_down_left'] = True result["mouse_down_left"] = True
return result return result
def handle_settings_click(mouse_pos, sim): def handle_settings_click(mouse_pos):
"""Handle clicks in settings menu""" """Handle clicks in settings menu"""
settings_menu_y = 100 settings_menu_y = 100
relative_y = mouse_pos[1] - settings_menu_y relative_y = mouse_pos[1] - settings_menu_y
@ -116,11 +137,11 @@ def handle_ui_click(mouse_pos, sim, rendering):
def handle_mouse_up(event): def handle_mouse_up(event):
"""Handle mouse button up events""" """Handle mouse button up events"""
if event.button == 1: if event.button == 1:
return {'mouse_down_left': False} return {"mouse_down_left": False}
elif event.button == 3: elif event.button == 3:
return {'mouse_down_right': False} return {"mouse_down_right": False}
elif event.button == 2: elif event.button == 2:
return {'mouse_down_middle': False} return {"mouse_down_middle": False}
return {} return {}
@ -129,23 +150,30 @@ def handle_key_press(event, rendering, sim):
if event.key == pygame.K_ESCAPE: if event.key == pygame.K_ESCAPE:
print("Escape button pressed") print("Escape button pressed")
print(f"Exiting Program {__file__}") print(f"Exiting Program {__file__}")
return {'running': False} return {"running": False}
elif event.key == pygame.K_SPACE: elif event.key == pygame.K_SPACE:
engine_settings['pause_sim'] = not engine_settings['pause_sim'] engine_settings["pause_sim"] = not engine_settings["pause_sim"]
elif event.key == pygame.K_c: elif event.key == pygame.K_c:
rendering.clear_screen(sim) rendering.clear_screen(sim)
sim.reset_particle_count() sim.reset_particle_count()
elif event.key == pygame.K_z: elif event.key == pygame.K_z:
return {'zoom_active': True, 'zoom_locked': False, 'zoom_pos': pygame.mouse.get_pos()} return {
"zoom_active": True,
"zoom_locked": False,
"zoom_pos": pygame.mouse.get_pos(),
}
return {} return {}
def main(): def main():
"""Main function to run the simulation"""
pygame.init() pygame.init()
clock = pygame.time.Clock() clock = pygame.time.Clock()
width = 1024 width = 1024
height = 768 height = 768
screen = pygame.display.set_mode((width, height), pygame.HWSURFACE | pygame.DOUBLEBUF) screen = pygame.display.set_mode(
(width, height), pygame.HWSURFACE | pygame.DOUBLEBUF
)
sim = Simulation(width, height) sim = Simulation(width, height)
rendering = Rendering(width, height) rendering = Rendering(width, height)
@ -159,6 +187,7 @@ def main():
zoom_pos = None zoom_pos = None
settings_visible = False settings_visible = False
running = True running = True
last_particle_time = 0
while running: while running:
# Clear screen at start of frame # Clear screen at start of frame
@ -181,11 +210,15 @@ def main():
in_settings_area = False in_settings_area = False
if settings_visible: if settings_visible:
settings_rect = pygame.Rect(rendering.width - 320, 100, 300, 400) settings_rect = pygame.Rect(
rendering.width - 320, 100, 300, 400
)
in_settings_area = settings_rect.collidepoint(mouse_pos) in_settings_area = settings_rect.collidepoint(mouse_pos)
if event.button == 4: # Mouse wheel up if event.button == 4: # Mouse wheel up
sim.brush_size = min(sim.brush_size + 1, sim.max_brush_size) sim.brush_size = min(
sim.brush_size + 1, sim.max_brush_size
)
elif event.button == 5: # Mouse wheel down elif event.button == 5: # Mouse wheel down
sim.brush_size = max(sim.brush_size - 1, 1) sim.brush_size = max(sim.brush_size - 1, 1)
elif event.button == 1: # Left click elif event.button == 1: # Left click
@ -206,13 +239,20 @@ def main():
relative_y = mouse_pos[1] - 100 # settings_menu_y relative_y = mouse_pos[1] - 100 # settings_menu_y
setting_index = relative_y // 30 setting_index = relative_y // 30
if 0 <= setting_index < len(engine_settings): if 0 <= setting_index < len(engine_settings):
setting_name = list(engine_settings.keys())[setting_index] setting_name = list(engine_settings.keys())[
engine_settings[setting_name] = not engine_settings[setting_name] setting_index
]
engine_settings[
setting_name
] = not engine_settings[setting_name]
over_button = True over_button = True
elif not in_settings_area: elif not in_settings_area:
# Check category buttons # Check category buttons
for category, button in rendering.category_buttons.items(): for (
category,
button,
) in rendering.category_buttons.items():
if button.collidepoint(mouse_pos): if button.collidepoint(mouse_pos):
rendering.current_category = category rendering.current_category = category
over_button = True over_button = True
@ -220,16 +260,21 @@ def main():
# Check particle buttons # Check particle buttons
if not over_button: if not over_button:
for particle_type, button in rendering.buttons.items(): for (
particle_type,
button,
) in rendering.buttons.items():
if button.collidepoint(mouse_pos): if button.collidepoint(mouse_pos):
sim.current_particle_type = particle_type sim.current_particle_type = particle_type
over_button = True over_button = True
break break
# Check clear screen button # Check clear screen button
if rendering.clear_screen_button.collidepoint(mouse_pos): if rendering.clear_screen_button.collidepoint(
mouse_pos
):
rendering.clear_screen(sim) rendering.clear_screen(sim)
particle_count = 0 # particle_count = 0
over_button = True over_button = True
if not over_button and not in_settings_area: if not over_button and not in_settings_area:
@ -252,7 +297,9 @@ def main():
if event.key == pygame.K_ESCAPE: if event.key == pygame.K_ESCAPE:
running = False running = False
elif event.key == pygame.K_SPACE: elif event.key == pygame.K_SPACE:
engine_settings['pause_sim'] = not engine_settings['pause_sim'] engine_settings["pause_sim"] = not engine_settings[
"pause_sim"
]
elif event.key == pygame.K_c: elif event.key == pygame.K_c:
rendering.clear_screen(sim) rendering.clear_screen(sim)
elif event.key == pygame.K_z: elif event.key == pygame.K_z:
@ -261,21 +308,18 @@ def main():
zoom_pos = pygame.mouse.get_pos() zoom_pos = pygame.mouse.get_pos()
# Update simulation if not paused # Update simulation if not paused
if not engine_settings['pause_sim']: if not engine_settings["pause_sim"]:
sim.simulate_step(dt, engine_settings) sim.simulate_step(dt, engine_settings)
# Handle continuous mouse input # Handle continuous mouse input
if mouse_down_left and not over_button: if mouse_down_left and not over_button:
x, y = mouse_pos x, y = mouse_pos
if sim.current_particle_type not in ['wind', 'air']:
current_time = time.time() current_time = time.time()
if not hasattr(main, 'last_particle_time'): if sim.current_particle_type not in ["wind", "air"]:
main.last_particle_time = 0
# Limit particle creation to every 16ms (approximately 60 FPS) # Limit particle creation to every 16ms (approximately 60 FPS)
if current_time - main.last_particle_time >= 0.016: if current_time - last_particle_time >= 0.016:
sim.create_particle_circle(x, y) sim.create_particle_circle(x, y)
main.last_particle_time = current_time last_particle_time = current_time
if mouse_down_right: if mouse_down_right:
x, y = mouse_pos x, y = mouse_pos
@ -288,19 +332,28 @@ def main():
# Handle zoom window # Handle zoom window
if zoom_active or zoom_locked: if zoom_active or zoom_locked:
current_zoom_pos = zoom_pos if zoom_locked else mouse_pos current_zoom_pos = zoom_pos if zoom_locked else mouse_pos
zoom_surface = rendering.draw_zoom_window(sim.particles, sim.particle_size, zoom_surface = rendering.draw_zoom_window(
rendering.particle_colors, current_zoom_pos) sim.particles,
sim.particle_size,
rendering.particle_colors,
current_zoom_pos,
)
zoom_x = 80 if current_zoom_pos[0] > width / 2 else width - 110 zoom_x = 80 if current_zoom_pos[0] > width / 2 else width - 110
zoom_y = 80 if current_zoom_pos[1] > height / 2 else height - 110 zoom_y = 80 if current_zoom_pos[1] > height / 2 else height - 110
screen.blit(zoom_surface, (zoom_x, zoom_y)) screen.blit(zoom_surface, (zoom_x, zoom_y))
# Draw everything in correct order # Draw everything in correct order
rendering.draw_particles(sim.particles, sim.active_particles, sim.particle_size, rendering.particle_colors) rendering.draw_particles(
sim.particles,
sim.active_particles,
sim.particle_size,
rendering.particle_colors,
)
rendering.draw_buttons() rendering.draw_buttons()
rendering.draw_brush_size_slider(sim.brush_size) rendering.draw_brush_size_slider(sim.brush_size)
rendering.render_brush_cursor(mouse_pos[0], mouse_pos[1], sim.brush_size * sim.particle_size) rendering.render_brush_cursor(
mouse_pos[0], mouse_pos[1], sim.brush_size * sim.particle_size
)
# Draw settings and debug overlay last # Draw settings and debug overlay last
if settings_visible: if settings_visible:
@ -309,9 +362,9 @@ def main():
rendering.draw_debug_overlay(fps, sim) rendering.draw_debug_overlay(fps, sim)
pygame.display.flip() pygame.display.flip()
pygame.quit() pygame.quit()
if __name__ == "__main__": if __name__ == "__main__":
# Profile the application # Profile the application
profiler = cProfile.Profile() profiler = cProfile.Profile()
@ -321,7 +374,7 @@ if __name__ == "__main__":
profiler.disable() profiler.disable()
# Write profiling results to file # Write profiling results to file
with open('profile_results.log', 'w') as f: with open("profile_results.log", "w", encoding="utf-8") as f:
stats = pstats.Stats(profiler, stream=f) stats = pstats.Stats(profiler, stream=f)
stats.sort_stats('cumulative') stats.sort_stats("cumulative")
stats.print_stats() stats.print_stats()

153
scripts/lint.py Normal file
View File

@ -0,0 +1,153 @@
#!/usr/bin/env python3
"""Rich TUI linting interface for Sandpypi."""
import logging
import subprocess
import time
from datetime import datetime
from pathlib import Path
from rich.console import Console
from rich.layout import Layout
from rich.live import Live
from rich.panel import Panel
from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn
class LinterTUI:
"""Main classs for the linting TUI."""
def __init__(self):
self.console = Console()
self.layout = Layout()
self.layout.split_column(
Layout(name="output", ratio=8), Layout(name="progress", ratio=2)
)
self.setup_logging()
def setup_logging(self):
"""Logging"""
self.log_dir = Path("logs")
self.log_dir.mkdir(exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
self.log_file = f"lint_{timestamp}.log"
logging.basicConfig(
filename=self.log_dir / self.log_file,
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
)
def get_target_path(self):
"""Get thee target path for linting."""
paths = input(
"\nEnter paths to lint (comma-separated NO SPACES)\
or press Enter for default [src,tests]: "
).strip()
target_paths = paths.split(",") if paths else ["src", "tests"]
# Convert to absolute paths and include subdirectories
abs_paths = []
for path in target_paths:
base_path = Path(path).absolute()
if base_path.is_dir():
abs_paths.extend(str(p) for p in base_path.rglob("*.py"))
else:
abs_paths.append(str(base_path))
return abs_paths
def run_linters(self, selected_linters, paths):
"""Run the linters on the selected paths."""
progress = Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
BarColumn(),
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
expand=True,
)
self.layout["progress"].update(progress)
with Live(self.layout, refresh_per_second=5, screen=True):
task = progress.add_task(
"[cyan]Linting...", total=len(selected_linters)
)
for linter in selected_linters:
progress.update(task, description=f"[cyan]Running {linter}...")
cmd = [linter] + paths
with subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
bufsize=1,
) as process:
current_output = []
linter_name = linter.split()[0]
while True:
output = process.stdout.readline()
if output == "" and process.poll() is not None:
break
if output:
current_output.append(output.strip())
# Log each line of output
logging.info("Running %s on %s", linter, paths)
logging.info("Output: %s", output.strip())
self.layout["output"].update(
Panel(
"\n".join(current_output[-20:]),
title=f"[bold green]{linter}\
{linter_name}",
border_style="green",
)
)
time.sleep(0.1)
# Log any errors
if not current_output:
stderr_output = process.stderr.read()
output_text = stderr_output or "No output"
self.layout["output"].update(
Panel(
output_text,
title=f"[bold green]{linter} Output",
border_style="green",
)
)
if stderr_output:
logging.error("Errors: %s", stderr_output)
progress.advance(task)
final_message = Panel(
"[bold green]✨ Linting Complete! ✨\n"
"[cyan]Check the logs for detailed results.\n"
f"[green]Logs saved to: {self.log_dir / self.log_file}\n"
"[yellow]Press Enter to exit...",
title="[bold blue]Sandpypi Linter",
border_style="green",
padding=(2, 4),
)
self.layout["output"].update(final_message)
self.layout["progress"].update(
Panel("[bold green]100% Complete!", border_style="green")
)
input()
def main():
"""Runner for the Linting TUI."""
tui = LinterTUI()
linters = ["black", "isort", "mypy", "flake8", "pylint"]
paths = tui.get_target_path()
tui.run_linters(linters, paths)
if __name__ == "__main__":
main()
# scripts\run_tests.py
# !/usr/bin/env python3

108
scripts/setup_dev.py Normal file
View File

@ -0,0 +1,108 @@
#!/usr/bin/env python3
"""Development environment setup script."""
import os
import platform
import shutil
import subprocess
import sys
from pathlib import Path
def check_python_version():
"""Ensure Python version meets minimum requirements."""
min_version = (3, 8)
current = sys.version_info[:2]
if current < min_version:
raise SystemError(
f"Python {min_version[0]}.{min_version[1]} or higher required"
)
def clean_existing_venv():
"""Remove existing virtual environment if present."""
venv_path = Path(".venv")
if venv_path.exists():
shutil.rmtree(venv_path)
def initialize_environment():
"""Create and initialize development environment."""
# Allow configurable venv path
venv_path = os.getenv("VENV_PATH", ".sandpypi_venv")
# Create and activate virtual environment
subprocess.run([sys.executable, "-m", "venv", venv_path], check=True)
# Get the correct paths based on OS
is_windows = platform.system() == "Windows"
scripts_path = "Scripts" if is_windows else "bin"
# Setup commands using virtual environment paths
pip_cmd = os.path.join(venv_path, scripts_path, "pip")
# Update pip and install dependencies
subprocess.run([pip_cmd, "install", "--upgrade", "pip"], check=True)
subprocess.run([pip_cmd, "install", "pre-commit"], check=True)
subprocess.run([pip_cmd, "install", "-e", ".[dev]"], check=True)
# Install and configure pre-commit hooks
precommit_cmd = os.path.join(venv_path, scripts_path, "pre-commit")
subprocess.run([precommit_cmd, "install"], check=True)
subprocess.run(
[precommit_cmd, "install", "--hook-type", "pre-push"], check=True
)
def setup_docs():
"""Initialize documentation structure."""
docs_path = Path("docs")
if not docs_path.exists():
subprocess.run(
[
os.path.join(
".venv",
"Scripts" if platform.system() == "Windows" else "bin",
"sphinx-quickstart",
),
"docs",
"--sep",
"--project=Sandpypi",
"--author=Stan44",
],
check=True,
)
def main():
"""Execute the complete development environment setup."""
try:
check_python_version()
clean_existing_venv()
# setup_git_hooks()
initialize_environment()
setup_docs()
print("\n✅ Development environment setup complete!")
print("\nNext steps:")
print("1. Activate your virtual environment:")
activate_cmd = (
"source .venv/Scripts/activate"
if platform.system() == "Windows"
else "source .venv/bin/activate"
)
print(activate_cmd)
print("2. Run tests: pytest")
print("3. Build docs: cd docs && make html")
except subprocess.CalledProcessError as e:
print(f"\n❌ Setup failed: {e}")
sys.exit(1)
except (OSError, IOError) as e:
print(f"\n❌ File system error: {e}")
sys.exit(1)
except ImportError as e:
print(f"\n❌ Dependency error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

8
setup.py Normal file
View File

@ -0,0 +1,8 @@
"""setup.py"""
from setuptools import find_packages, setup # type: ignore
setup(
name="sandpypi",
packages=find_packages(),
)

View File

@ -0,0 +1,14 @@
"""
Sandpypi - A falling sand simulation package in Python.
This package provides modules for particle physics simulation and rendering:
- config.settings: Configuration and particle properties
- physics.sim: Physics engine and particle behavior
- rendering.rendering: Display and visualization components
"""
# src\rendering\rendering.py
# src\physics\sim.py
# src\config\settings.py
# src\physics\particle.py
# src\debug\debugger_system.py

0
src/config/py.typed Normal file
View File

View File

@ -3,57 +3,65 @@
Global settings and imports for the project. Global settings and imports for the project.
This module defines various settings for the game engine, such as enabling or disabling the cursor, glow effect, gas effect, debug mode, and FPS display. It also provides a function to load particle properties from a JSON file. This module defines various settings for the game engine, such as
enabling or disabling the cursor, glow effect, gas effect, debug mode,
and FPS display.
It also provides a function to load particle properties from a JSON file.
The `engine_settings` dictionary contains the configurable settings for the game engine. These settings can be used to customize the behavior of the game. The `engine_settings` dictionary contains the configurable settings
for the game engine. These settings can be used to
customize the behavior of the game.
The `load_particle_properties()` function attempts to load particle properties from a 'particles.json' file. If the file is not found or the JSON data is invalid, it returns an empty dictionary. The `load_particle_properties()` function attempts to load particle properties
from a 'particles.json' file. If the file is not found or the
JSON data is invalid, it returns an empty dictionary.
The `particle_properties` variable is initialized by calling `load_particle_properties()` when the module is imported. The `particle_properties` variable is initialized by calling
`load_particle_properties()` when the module is imported.
""" """
import cProfile import cProfile
import pstats
import sys
import os
import pygame
import json import json
import os
import pstats
import random import random
import sys
import time import time
import numpy as np import numpy as np
import pygame
engine_settings = { engine_settings = {
'pause_sim': True, "pause_sim": True,
'enable_cursor': True, "enable_cursor": True,
'enable_glow': False, "enable_glow": False,
'enable_gas_effect': True, "enable_gas_effect": True,
'enable_debug': True, "enable_debug": True,
'enable_fps': True, "enable_fps": True,
'enable_WVisuals': False, "enable_WVisuals": False,
'enable_PVisuals': False, "enable_PVisuals": False,
'enable_TempVisuals': False, "enable_TempVisuals": False,
'outerwall': False, "outerwall": False,
# 'settings': True/False # 'settings': True/False
} }
def load_particle_properties(): def load_particle_properties():
"""Loads particle properties from a JSON file."""
particle_data = {} particle_data = {}
base_path = os.path.join(os.path.dirname(__file__), '..') base_path = os.path.join(os.path.dirname(__file__), "..")
core_path = os.path.join(base_path, 'part', 'coreparts') core_path = os.path.join(base_path, "part", "coreparts")
mods_path = os.path.join(base_path, 'part', 'mods') mods_path = os.path.join(base_path, "part", "mods")
def load_json_files(directory): def load_json_files(directory):
for root, _, files in os.walk(directory): for root, _, files in os.walk(directory):
for file in files: for file in files:
if file.endswith('.json'): if file.endswith(".json"):
file_path = os.path.join(root, file) file_path = os.path.join(root, file)
try: try:
with open(file_path, 'r') as f: with open(file_path, "r", encoding="utf-8") as f:
data = json.load(f) data = json.load(f)
"""# Debug: Print specific temperature-related properties
for particle_name, props in data.items():
if 'temperature' in props:
print(f"Loaded {particle_name} with temp: {props['temperature']}")
if 'melt_temperature' in props:
print(f"Loaded {particle_name} with melt_temp: {props['melt_temperature']}")"""
particle_data.update(data) particle_data.update(data)
except (FileNotFoundError, json.JSONDecodeError) as e: except (FileNotFoundError, json.JSONDecodeError) as e:
print(f"Error loading {file}: {e}") print(f"Error loading {file}: {e}")
@ -65,16 +73,21 @@ def load_particle_properties():
else: else:
print(f"Directory not found: {path}") print(f"Directory not found: {path}")
"""# Final verification of critical properties
print("\nVerifying critical properties:")
for name, props in particle_data.items():
if 'lava' in name.lower():
print(f"Lava properties: {props}")"""
return particle_data return particle_data
# Load particle properties once when module is imported # Load particle properties once when module is imported
particle_properties = load_particle_properties() 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",
]

View File

@ -1,7 +1,17 @@
from config.settings import engine_settings, pygame """# DebuggerSystem.py"""
import time import time
import psutil
from src.config.settings import engine_settings, pygame # typing: ignore
process = psutil.Process()
class DebuggerSystem: class DebuggerSystem:
"""DebuggerSystem class for tracking and displaying performance metrics."""
def __init__(self): def __init__(self):
self.debug_surface = pygame.Surface((300, 150), pygame.SRCALPHA) self.debug_surface = pygame.Surface((300, 150), pygame.SRCALPHA)
self.debug_font = pygame.font.SysFont(None, 24) self.debug_font = pygame.font.SysFont(None, 24)
@ -10,9 +20,15 @@ class DebuggerSystem:
self.current_fps = 0 self.current_fps = 0
self.debug_text = [] self.debug_text = []
self.performance_metrics = { self.performance_metrics = {
'particle_updates': 0, "particle_updates": 0,
'physics_time': 0, "physics_time": 0,
'render_time': 0 "render_time": 0,
"spatial_grid_updates": 0,
"active_particles_near_mouse": 0,
"render_regions": 0,
"memory_usage": 0,
"cpu_usage": 0,
"mouse_movement_delta": 0,
} }
def track_fps(self): def track_fps(self):
@ -27,30 +43,60 @@ class DebuggerSystem:
self.fps_timer = current_time self.fps_timer = current_time
return self.current_fps return self.current_fps
def update_performance_metrics(self, sim): def track_spatial_activity(self, sim, mouse_pos):
"""Track simulation performance metrics""" """Track spatial grid updates near mouse position"""
self.performance_metrics['particle_updates'] = len(sim.active_particles) x, y = mouse_pos
return self.performance_metrics cell_key = sim.get_cell_key(x, y)
radius = 3 # Track 3x3 grid around mouse
active_count = 0
for dx in range(-radius, radius + 1):
for dy in range(-radius, radius + 1):
check_key = (cell_key[0] + dx, cell_key[1] + dy)
if check_key in sim.spatial_grid:
active_count += len(sim.spatial_grid[check_key])
self.performance_metrics["active_particles_near_mouse"] = active_count
self.performance_metrics["spatial_grid_updates"] += 1
def track_render_performance(self, rendering):
"""Track rendering system metrics"""
self.performance_metrics["render_regions"] = len(
rendering.active_particles
)
def update_system_metrics(self):
"""Track system resource usage"""
self.performance_metrics["memory_usage"] = (
process.memory_info().rss / 1024 / 1024
) # MB
self.performance_metrics["cpu_usage"] = process.cpu_percent()
def draw_debug_overlay(self, screen, sim): def draw_debug_overlay(self, screen, sim):
if not engine_settings['enable_fps'] and not engine_settings['enable_debug']: """Draws the debug overlay on the screen."""
if (
not engine_settings["enable_fps"]
and not engine_settings["enable_debug"]
):
return return
self.debug_surface.fill((0, 0, 0, 128)) self.debug_surface.fill((0, 0, 0, 128))
y_offset = 10 y_offset = 10
if engine_settings['enable_fps']: if engine_settings["enable_fps"]:
fps_text = f"FPS: {self.current_fps:.1f} | TPS: {sim.track_tps():.1f}" 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)) fps_surf = self.debug_font.render(fps_text, True, (255, 255, 255))
self.debug_surface.blit(fps_surf, (10, y_offset)) self.debug_surface.blit(fps_surf, (10, y_offset))
y_offset += 25 y_offset += 25
if engine_settings['enable_debug']: if engine_settings["enable_debug"]:
debug_info = [ debug_info = [
f"Active Particles: {len(sim.active_particles)}", f"Active Particles: {len(sim.active_particles)}",
f"Total Particles: {sim.particle_count}", f"Total Particles: {sim.particle_count}",
f"Current Type: {sim.current_particle_type}", f"Current Type: {sim.current_particle_type}",
f"Brush Size: {sim.brush_size}" f"Brush Size: {sim.brush_size}",
] ]
for line in debug_info: for line in debug_info:
@ -62,5 +108,5 @@ class DebuggerSystem:
def log_error(self, error_msg): def log_error(self, error_msg):
"""Log errors for debugging""" """Log errors for debugging"""
with open('debug_log.txt', 'a') as f: with open("debug_log.txt", "a", encoding="utf-8") as f:
f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - {error_msg}\n") f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - {error_msg}\n")

View File

@ -1,7 +1,21 @@
"""Load particle properties from json so we know what particles
we got and how they should be simulated.
"""
# Load particle properties from json so we know what particles we got and how they should be simulated.
class Particle: class Particle:
def __init__(self, simulation, position, velocity, mass, particle_type, properties, temperature=20): """Set up a particle with the given properties."""
def __init__(
self,
simulation,
position,
velocity,
mass,
particle_type,
properties,
temperature=20,
):
self.position = position # (x, y) self.position = position # (x, y)
self.velocity = velocity # (vx, vy) self.velocity = velocity # (vx, vy)
self.mass = mass self.mass = mass
@ -32,9 +46,13 @@ class Particle:
self.melt = properties.get("melt", None) self.melt = properties.get("melt", None)
self.melt_temperature = properties.get("melt_temperature", None) self.melt_temperature = properties.get("melt_temperature", None)
self.solidify = properties.get("solidify", None) self.solidify = properties.get("solidify", None)
self.solidify_temperature = properties.get("solidify_temperature", None) self.solidify_temperature = properties.get(
"solidify_temperature", None
)
self.evaporate = properties.get("evaporate", None) self.evaporate = properties.get("evaporate", None)
self.evaporate_temperature = properties.get("evaporate_temperature", None) self.evaporate_temperature = properties.get(
"evaporate_temperature", None
)
self.freeze = properties.get("freeze", None) self.freeze = properties.get("freeze", None)
self.freeze_temperature = properties.get("freeze_temperature", None) self.freeze_temperature = properties.get("freeze_temperature", None)
@ -49,7 +67,9 @@ class Particle:
self.pressure_resistance = properties.get("pressure_resistance", 0) self.pressure_resistance = properties.get("pressure_resistance", 0)
self.pressure_tolerance = properties.get("pressure_tolerance", 0) self.pressure_tolerance = properties.get("pressure_tolerance", 0)
self.pressure_threshold = properties.get("pressure_threshold", 0) self.pressure_threshold = properties.get("pressure_threshold", 0)
self.pressure_threshold_duration = properties.get("pressure_threshold_duration", 0) self.pressure_threshold_duration = properties.get(
"pressure_threshold_duration", 0
)
# Burning properties # Burning properties
self.burning = properties.get("burning", False) self.burning = properties.get("burning", False)
@ -62,8 +82,14 @@ class Particle:
@classmethod @classmethod
def from_type(cls, simulation, position, particle_type, properties): def from_type(cls, simulation, position, particle_type, properties):
"""Pre-initialize a particle with default values based on its type."""
default_velocity = [0, 0] default_velocity = [0, 0]
default_mass = properties.get("mass", 1.0) default_mass = properties.get("mass", 1.0)
return cls(simulation, position, default_velocity, default_mass, particle_type, properties) return cls(
simulation,
position,
default_velocity,
default_mass,
particle_type,
properties,
)

0
src/physics/py.typed Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@ -1,72 +0,0 @@
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)

View File

@ -1,296 +0,0 @@
"""
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))

View File

@ -1,87 +0,0 @@
"""
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

@ -1,118 +0,0 @@
"""
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

@ -1,73 +0,0 @@
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

@ -1,162 +0,0 @@
"""
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))

0
src/rendering/py.typed Normal file
View File

View File

@ -2,30 +2,49 @@
#File Name: rendering.py #File Name: rendering.py
Rendering class for the particle simulation. Rendering class for the particle simulation.
This class is responsible for rendering the particles, UI elements, and debug information on the screen. It handles the setup of the display, pre-rendering of static UI elements, and the drawing of particles, buttons, and debug overlays. This class is responsible for rendering the particles, UI elements, and debug
information on the screen.
It handles the setup of the display, pre-rendering
of static UI elements,
and the drawing of particles, buttons, and debug overlays.
The `draw_particles` function is the main method for rendering the particles on the screen. It takes the particle data, active particles, particle size, and particle colors as input, and renders the particles on the `particle_surface`. The `particle_surface` is then blitted onto the main screen. The `draw_particles` function is the main method for rendering the particles
on the screen. It takes the particle data, active particles, particle size,
and particle colors as input,
and renders the particles on the `particle_surface`. The `particle_surface` is
then blitted onto the main screen.
The `draw_zoom_window` function is used to render a zoomed-in view of the particles around the mouse cursor. It creates a separate surface for the zoomed-in view and returns it. The `draw_zoom_window` function is used to render a zoomed-in view of the
particles around the mouse cursor.
It creates a separate surface for the zoomed-in view and returns it.
The `draw_debug_overlay` function is responsible for rendering the debug information, such as FPS, mouse position, and particle information, on the screen. The `draw_debug_overlay` responsible for rendering the debug information,
such as FPS, mouse position, and particle information, on the screen.
The `draw_buttons` function handles the rendering of the category buttons, particle buttons, and other UI elements like the clear screen and settings buttons. The `draw_buttons` function handles the rendering of the category buttons,
particle buttons, and other UI elements
like the clear screen and settings buttons.
The `render_brush_cursor` function is used to draw a visual indicator for the current brush size. The `render_brush_cursor` draw a visual indicator for the current brush size.
The `draw_brush_size_slider` function renders a slider for adjusting the brush size. The `draw_brush_size_slider` renders a slider for adjusting the brush size.
The `draw_settings_menu` function creates a settings menu surface that can be displayed on the screen. The `draw_settings_menu` settings menu that can be displayed on the screen.
The `clear_screen` function is used to reset the simulation grid and clear the display surfaces. The `clear_screen` reset the simulation grid and clear the display surfaces.
The `setup_gpu_rendering` placeholder for GPU-based rendering setup.
""" """
from ..config.settings import (
from config.settings import pygame, random, particle_properties, engine_settings engine_settings,
particle_properties,
pygame,
random,
)
class Rendering: class Rendering:
"""Main rendering system"""
def __init__(self, width, height): def __init__(self, width, height):
# self.setup_gpu_rendering() # self.setup_gpu_rendering()
@ -34,63 +53,85 @@ class Rendering:
self.background.fill((0, 0, 0)) self.background.fill((0, 0, 0))
self.width = width self.width = width
self.height = height self.height = height
self.particle_count = 0
self.particle_colors = {} self.particle_colors = {}
self.particle_properties = particle_properties self.particle_properties = particle_properties
self.particle_surface = pygame.Surface((width, height), pygame.SRCALPHA) self.particle_surface = pygame.Surface(
self.debug_surface = pygame.Surface((300, 150), pygame.SRCALPHA) (width, height), pygame.SRCALPHA
)
self.zoom_window_surface = pygame.Surface((200, 200), pygame.SRCALPHA)
# self.debug_surface = pygame.Surface((300, 150), pygame.SRCALPHA)
self.particle_surface = pygame.Surface(
(self.width, self.height), pygame.SRCALPHA
)
self.settings_menu_surface = pygame.Surface(
(300, 300), pygame.SRCALPHA
)
self.brush_cursor_surface = pygame.Surface((10, 10), pygame.SRCALPHA)
self._last_debug_info = None
self.buttons = {}
self.button_height = 30
self.button_width = 100
self.clear_screen_button = pygame.Rect(10, 10, 100, 30)
self.settings_button = pygame.Rect(10, 50, 100, 30)
self.cached_fonts = { self.cached_fonts = {
'debug': pygame.font.SysFont(None, 24), "debug": pygame.font.SysFont(None, 24),
'button': pygame.font.SysFont(None, 20), "button": pygame.font.SysFont(None, 20),
'slider': pygame.font.SysFont(None, 20), "slider": pygame.font.SysFont(None, 20),
'settings': pygame.font.SysFont(None, 20), "settings": pygame.font.SysFont(None, 20),
'zoom': pygame.font.SysFont(None, 20), "zoom": pygame.font.SysFont(None, 20),
'brush_size': pygame.font.SysFont(None, 20), "brush_size": pygame.font.SysFont(None, 20),
'zoom_window': pygame.font.SysFont(None, 20), "zoom_window": pygame.font.SysFont(None, 20),
'zoom_window_text': pygame.font.SysFont(None, 20) "zoom_window_text": pygame.font.SysFont(None, 20),
} }
# Pre-render static UI elements # Pre-render static UI elements
self.button_surfaces = {} self.button_surfaces = {}
# Initialize categories # Initialize categories
for name, properties in particle_properties.items(): for name, properties in particle_properties.items():
if 'color' in properties: if "color" in properties:
self.particle_colors[name.lower()] = properties['color'] self.particle_colors[name.lower()] = properties["color"]
self.categories = {'Solids': [], 'Liquids': [], 'Gases': [], 'Special': []} self.categories = {
"Solids": [],
"Liquids": [],
"Gases": [],
"Special": [],
}
for particle_name, properties in self.particle_properties.items(): for particle_name, properties in self.particle_properties.items():
if properties.get('is_gas'): if properties.get("is_gas"):
self.categories['Gases'].append(particle_name) self.categories["Gases"].append(particle_name)
elif properties.get('liquid'): elif properties.get("liquid"):
self.categories['Liquids'].append(particle_name) self.categories["Liquids"].append(particle_name)
elif properties.get('solid'): elif properties.get("solid"):
self.categories['Solids'].append(particle_name) self.categories["Solids"].append(particle_name)
else: else:
self.categories['Special'].append(particle_name) self.categories["Special"].append(particle_name)
self.current_category = 'Solids' self.current_category = "Solids"
self.category_buttons = {} self.category_buttons = {}
self.setup_category_menu() self.setup_category_menu()
self.setup_static_ui() self.setup_static_ui()
def setup_gpu_rendering(self): def setup_gpu_rendering(self):
# Initialize OpenGL context """Initialize OpenGL context"""
pygame.display.gl_set_attribute(pygame.GL_ACCELERATED_VISUAL, 1) pygame.display.gl_set_attribute(pygame.GL_ACCELERATED_VISUAL, 1)
self.screen = pygame.display.set_mode((self.width, self.height), self.screen = pygame.display.set_mode(
pygame.OPENGL | pygame.DOUBLEBUF) (self.width, self.height), pygame.OPENGL | pygame.DOUBLEBUF
)
def setup_static_ui(self): def setup_static_ui(self):
"""Setup static UI elements"""
for category in self.categories: for category in self.categories:
surf = pygame.Surface((80, 25)) surf = pygame.Surface((80, 25))
surf.fill((150, 150, 150)) surf.fill((150, 150, 150))
text = self.cached_fonts['button'].render(category, True, (0, 0, 0)) text = self.cached_fonts["button"].render(
category, True, (0, 0, 0)
)
surf.blit(text, (5, 5)) surf.blit(text, (5, 5))
self.button_surfaces[category] = surf self.button_surfaces[category] = surf
def setup_category_menu(self): def setup_category_menu(self):
# Category buttons at the top """Setup category menu"""
x_offset = self.width - 350 x_offset = self.width - 350
y_offset = 10 y_offset = 10
for category in self.categories: for category in self.categories:
@ -98,20 +139,25 @@ class Rendering:
self.category_buttons[category] = button_rect self.category_buttons[category] = button_rect
x_offset += 90 x_offset += 90
def load_buttons(self): def load_buttons(self):
"""Load buttons"""
x_offset = 10 x_offset = 10
y_offset = 10 y_offset = 10
for particle_type, properties in self.particle_properties.items(): for particle_type, properties in self.particle_properties.items():
if 'color' in properties: if "color" in properties:
button_rect = pygame.Rect(x_offset, y_offset, self.button_width, self.button_height) button_rect = pygame.Rect(
x_offset, y_offset, self.button_width, self.button_height
)
self.buttons[particle_type.lower()] = button_rect self.buttons[particle_type.lower()] = button_rect
x_offset += self.button_width + 10 # Add spacing between buttons x_offset += (
self.button_width + 10
) # Add spacing between buttons
def draw_particles(
def draw_particles(self, particles, active_particles, particle_size, particle_colors): # this is the function that draws the particles self, particles, active_particles, particle_size, particle_colors
#self.particle_surface = pygame.Surface((self.width, self.height), pygame.SRCALPHA) ):
"""Draw particles"""
self.particle_surface.fill((0, 0, 0, 0)) self.particle_surface.fill((0, 0, 0, 0))
particle_batches = {} particle_batches = {}
@ -124,23 +170,40 @@ class Rendering:
if p_type not in particle_batches: if p_type not in particle_batches:
particle_batches[p_type] = [] particle_batches[p_type] = []
rect = pygame.Rect(x * particle_size, y * particle_size, rect = pygame.Rect(
particle_size, particle_size) x * particle_size,
y * particle_size,
particle_size,
particle_size,
)
particle_batches[p_type].append(rect) particle_batches[p_type].append(rect)
if particle.particle_type in particle_colors: if particle.particle_type in particle_colors:
color = particle_colors[particle.particle_type] color = particle_colors[particle.particle_type]
else: else:
color = (255, 255, 255) color = (255, 255, 255)
if engine_settings['enable_glow']: if engine_settings["enable_glow"]:
glow_color = (255, 255, 255) glow_color = (255, 255, 255)
glow_radius = 0.5 * particle_size glow_radius = 0.5 * particle_size
glow_surface = pygame.Surface((glow_radius * 2, glow_radius * 2), pygame.SRCALPHA) glow_surface = pygame.Surface(
pygame.draw.circle(glow_surface, glow_color, (glow_radius, glow_radius), glow_radius) (glow_radius * 2, glow_radius * 2), pygame.SRCALPHA
)
pygame.draw.circle(
glow_surface,
glow_color,
(glow_radius, glow_radius),
glow_radius,
)
glow_surface.set_alpha(85) glow_surface.set_alpha(85)
self.particle_surface.blit(glow_surface, (x * particle_size - glow_radius, y * particle_size - glow_radius)) self.particle_surface.blit(
glow_surface,
(
x * particle_size - glow_radius,
y * particle_size - glow_radius,
),
)
if engine_settings['enable_gas_effect']: if engine_settings["enable_gas_effect"]:
if particle.is_gas: if particle.is_gas:
# Enhanced gas visibility # Enhanced gas visibility
alpha = random.randint(128, 200) alpha = random.randint(128, 200)
@ -153,15 +216,22 @@ class Rendering:
# Add subtle movement effect # Add subtle movement effect
offset_x = random.randint(-1, 1) offset_x = random.randint(-1, 1)
offset_y = random.randint(-1, 1) offset_y = random.randint(-1, 1)
rect = (x * particle_size + offset_x, y * particle_size + offset_y, rect = (
particle_size, particle_size) x * particle_size + offset_x,
y * particle_size + offset_y,
particle_size,
particle_size,
)
else: else:
rect = (x * particle_size, y * particle_size, rect = (
particle_size, particle_size) x * particle_size,
y * particle_size,
particle_size,
particle_size,
)
pygame.draw.rect(self.particle_surface, color, rect) pygame.draw.rect(self.particle_surface, color, rect)
self.screen.blit(self.background, (0, 0)) self.screen.blit(self.background, (0, 0))
self.screen.blit(self.particle_surface, (0, 0)) self.screen.blit(self.particle_surface, (0, 0))
"""#Potentially for future """#Potentially for future
@ -177,8 +247,16 @@ class Rendering:
self.screen.blit(self.background, (0, 0)) self.screen.blit(self.background, (0, 0))
self.screen.blit(self.particle_surface, (0, 0))""" self.screen.blit(self.particle_surface, (0, 0))"""
def draw_zoom_window(self, particles, particle_size, particle_colors, mouse_pos, zoom_factor=4): # this is the function that draws the zoom window def draw_zoom_window(
print(f"Drawing zoom window.") self,
particles,
particle_size,
particle_colors,
mouse_pos,
zoom_factor=4,
):
"""Draw zoom window"""
print(f"Drawing zoom window.{mouse_pos}")
zoom_size = 100 # Size of zoom window zoom_size = 100 # Size of zoom window
zoom_surface = pygame.Surface((zoom_size, zoom_size)) zoom_surface = pygame.Surface((zoom_size, zoom_size))
zoom_surface.fill((0, 0, 0)) zoom_surface.fill((0, 0, 0))
@ -195,37 +273,53 @@ class Rendering:
if 0 <= x < len(particles) and 0 <= y < len(particles[0]): if 0 <= x < len(particles) and 0 <= y < len(particles[0]):
particle = particles[x][y] particle = particles[x][y]
if particle: if particle:
color = particle_colors.get(particle.particle_type, (255, 255, 255)) color = particle_colors.get(
rect = ((x - view_x) * zoom_factor, particle.particle_type, (255, 255, 255)
)
rect = (
(x - view_x) * zoom_factor,
(y - view_y) * zoom_factor, (y - view_y) * zoom_factor,
particle_size * zoom_factor, particle_size * zoom_factor,
particle_size * zoom_factor) particle_size * zoom_factor,
)
pygame.draw.rect(zoom_surface, color, rect) pygame.draw.rect(zoom_surface, color, rect)
print(f"Drawing zoom window at {mouse_pos}{zoom_surface}") print(f"Drawing zoom window at {mouse_pos}{zoom_surface}")
return zoom_surface return zoom_surface
def _get_particle_info(self, particles, x, y): def _get_particle_info(self, particles, x, y):
if 0 <= x < len(particles) and 0 <= y < len(particles[0]): if 0 <= x < len(particles) and 0 <= y < len(particles[0]):
particle = particles[x][y] particle = particles[x][y]
if particle: if particle:
attrs = ['temperature', 'liquid', 'is_gas', 'solid', 'mass', 'velocity', 'friction'] attrs = [
"temperature",
"liquid",
"is_gas",
"solid",
"mass",
"velocity",
"friction",
]
info = [f"Type: {particle.particle_type}"] info = [f"Type: {particle.particle_type}"]
info.extend(f"{attr}: {getattr(particle, attr)}" for attr in attrs info.extend(
if hasattr(particle, attr)) f"{attr}: {getattr(particle, attr)}"
for attr in attrs
if hasattr(particle, attr)
)
return " | ".join(info) return " | ".join(info)
return "None" return "None"
"""
def draw_wind_overlay(self, wind_zones): def draw_wind_overlay(self, wind_zones):
wind_surface = pygame.Surface((self.width, self.height), pygame.SRCALPHA) wind_surface = pygame.Surface(
(self.width, self.height), pygame.SRCALPHA
)
for zone in wind_zones: for zone in wind_zones:
# Draw wind direction arrows # Draw wind direction arrows
x, y = zone['x'], zone['y'] x, y = zone["x"], zone["y"]
radius = zone['radius'] # radius = zone["radius"]
strength = zone['strength'] strength = zone["strength"]
direction = zone['direction'] direction = zone["direction"]
# Draw wind field visualization # Draw wind field visualization
arrow_color = (0, 150, 255, 100) # Light blue, semi-transparent arrow_color = (0, 150, 255, 100) # Light blue, semi-transparent
@ -234,61 +328,106 @@ class Rendering:
# Draw main direction arrow # Draw main direction arrow
end_x = x + direction[0] * arrow_length end_x = x + direction[0] * arrow_length
end_y = y + direction[1] * arrow_length end_y = y + direction[1] * arrow_length
pygame.draw.line(wind_surface, arrow_color, (x, y), (end_x, end_y), 2) pygame.draw.line(
wind_surface, arrow_color, (x, y), (end_x, end_y), 2
)
# Draw arrow head # Draw arrow head
pygame.draw.circle(wind_surface, arrow_color, (int(x), int(y)), 5) pygame.draw.circle(wind_surface, arrow_color, (int(x), int(y)), 5)
self.screen.blit(wind_surface, (0, 0)) self.screen.blit(wind_surface, (0, 0))"""
"""
def draw_pressure_overlay(self, particles, active_particles): def draw_pressure_overlay(self, particles, active_particles):
pressure_surface = pygame.Surface((self.width, self.height), pygame.SRCALPHA) pressure_surface = pygame.Surface(
(self.width, self.height), pygame.SRCALPHA
)
# Create pressure map # Create pressure map
for x, y in active_particles: for x, y in active_particles:
particle = particles[x][y] particle = particles[x][y]
if particle and hasattr(particle, 'pressure'): if particle and hasattr(particle, "pressure"):
# Color gradient based on pressure # Color gradient based on pressure
pressure = particle.pressure pressure = particle.pressure
if pressure > 0: if pressure > 0:
color = (255, 0, 0, int(min(pressure * 50, 255))) # Red for high pressure color = (
255,
0,
0,
int(min(pressure * 50, 255)),
) # Red for high pressure
else: else:
color = (0, 0, 255, int(min(-pressure * 50, 255))) # Blue for low pressure color = (
0,
0,
255,
int(min(-pressure * 50, 255)),
) # Blue for low pressure
pygame.draw.rect(pressure_surface, color, pygame.draw.rect(
(x * self.particle_size, y * self.particle_size, pressure_surface,
self.particle_size, self.particle_size)) color,
(
self.screen.blit(pressure_surface, (0, 0)) x * self.particle_size,
y * self.particle_size,
self.particle_size,
self.particle_size,
),
)
self.screen.blit(pressure_surface, (0, 0))"""
"""
def draw_temperature_overlay(self, particles, active_particles): def draw_temperature_overlay(self, particles, active_particles):
temperature_surface = pygame.Surface((self.width, self.height), pygame.SRCALPHA) temperature_surface = pygame.Surface(
(self.width, self.height), pygame.SRCALPHA
)
# Create temperature map # Create temperature map
for x, y in active_particles: for x, y in active_particles:
particle = particles[x][y] particle = particles[x][y]
if particle and hasattr(particle, 'temperature'): if particle and hasattr(particle, "temperature"):
# Color gradient based on temperature # Color gradient based on temperature
temperature = particle.temperature temperature = particle.temperature
if temperature > 0: if temperature > 0:
color = (255, 0, 0, int(min(temperature * 50, 255))) # Red for high temperature color = (
255,
0,
0,
int(min(temperature * 50, 255)),
) # Red for high temperature
else: else:
color = (0, 0, 255, int(min(-temperature * 50, 255))) # Blue for low temperature color = (
0,
0,
255,
int(min(-temperature * 50, 255)),
) # Blue for low temperature
pygame.draw.rect(temperature_surface, color, pygame.draw.rect(
(x * self.particle_size, y * self.particle_size, temperature_surface,
self.particle_size, self.particle_size)) color,
(
self.screen.blit(temperature_surface, (0, 0)) x * self.particle_size,
y * self.particle_size,
self.particle_size,
self.particle_size,
),
)
self.screen.blit(temperature_surface, (0, 0))"""
def draw_debug_overlay(self, fps, sim): def draw_debug_overlay(self, fps, sim):
if not engine_settings['enable_fps'] and not engine_settings['enable_debug']: """Debugger moving to debugger_system.py"""
if (
not engine_settings["enable_fps"]
and not engine_settings["enable_debug"]
):
return return
# Create static debug surface if not exists # Create static debug surface if not exists
if not hasattr(self, 'debug_surface'): if not hasattr(self, "debug_surface"):
self.debug_surface = pygame.Surface((300, 150), pygame.SRCALPHA) self.debug_surface = pygame.Surface((300, 150), pygame.SRCALPHA)
# Only update when values change significantly # Only update when values change significantly
@ -300,26 +439,32 @@ class Rendering:
int(sim.track_tps()), int(sim.track_tps()),
mouse_x // 10, # Reduced update frequency mouse_x // 10, # Reduced update frequency
mouse_y // 10, mouse_y // 10,
sim.particle_count sim.particle_count,
) )
if not hasattr(self, '_last_debug_info') or current_info != self._last_debug_info: if (
not hasattr(self, "_last_debug_info")
or current_info != self._last_debug_info
):
self._last_debug_info = current_info self._last_debug_info = current_info
self.debug_surface.fill((0, 0, 0, 0)) self.debug_surface.fill((0, 0, 0, 0))
font = self.cached_fonts["debug"]
font = self.cached_fonts['debug']
y_offset = 10 y_offset = 10
if engine_settings['enable_fps']: if engine_settings["enable_fps"]:
fps_surf = font.render(f"FPS: {fps:.1f} | TPS: {sim.track_tps():.1f}", True, (255, 255, 255)) 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)) self.debug_surface.blit(fps_surf, (10, y_offset))
y_offset += 25 y_offset += 25
if engine_settings['enable_debug']: if engine_settings["enable_debug"]:
debug_lines = [ debug_lines = [
f"Mouse: ({mouse_x}, {mouse_y})", f"Mouse: ({mouse_x}, {mouse_y})",
f"Grid: ({grid_x}, {grid_y})", f"Grid: ({grid_x}, {grid_y})",
f"Particles: {sim.particle_count}" f"Particles: {sim.particle_count}",
] ]
for line in debug_lines: for line in debug_lines:
@ -330,8 +475,8 @@ class Rendering:
# Single blit of cached surface # Single blit of cached surface
self.screen.blit(self.debug_surface, (0, 0)) self.screen.blit(self.debug_surface, (0, 0))
def draw_buttons(self):
def draw_buttons(self): # this is the function that draws the buttons """Draws the buttons on the screen."""
self.buttons = {} self.buttons = {}
# Draw category buttons on right # Draw category buttons on right
@ -356,13 +501,17 @@ class Rendering:
# Use cached button surface # Use cached button surface
if particle_type in self.button_surfaces: if particle_type in self.button_surfaces:
self.screen.blit(self.button_surfaces[particle_type], button_rect) self.screen.blit(
self.button_surfaces[particle_type], button_rect
)
else: else:
# Create and cache button surface if not exists # Create and cache button surface if not exists
button_surface = pygame.Surface((80, 25)) button_surface = pygame.Surface((80, 25))
color = self.particle_properties[particle_type].get('color', (255, 255, 255)) color = self.particle_properties[particle_type].get(
"color", (255, 255, 255)
)
button_surface.fill(color) button_surface.fill(color)
font = self.cached_fonts.get('button') font = self.cached_fonts.get("button")
label = font.render(particle_type, True, (0, 0, 0)) label = font.render(particle_type, True, (0, 0, 0))
button_surface.blit(label, (5, 5)) button_surface.blit(label, (5, 5))
self.button_surfaces[particle_type] = button_surface self.button_surfaces[particle_type] = button_surface
@ -370,81 +519,99 @@ class Rendering:
y_offset += 30 # Stack buttons vertically y_offset += 30 # Stack buttons vertically
final_y_offset = y_offset + 10
# Draw clear screen button # Draw clear screen button
if 'clear' not in self.button_surfaces: if "clear" not in self.button_surfaces:
clear_surface = pygame.Surface((80, 25)) clear_surface = pygame.Surface((80, 25))
clear_surface.fill((255, 0, 0)) clear_surface.fill((255, 0, 0))
font = self.cached_fonts.get('button') font = self.cached_fonts.get("button")
label = font.render("Clear", True, (255, 255, 255)) label = font.render("Clear", True, (255, 255, 255))
clear_surface.blit(label, (5, 5)) clear_surface.blit(label, (5, 5))
self.button_surfaces['clear'] = clear_surface self.button_surfaces["clear"] = clear_surface
self.clear_screen_button = pygame.Rect(x_offset, y_offset + 10, 80, 25) self.clear_screen_button = pygame.Rect(x_offset, y_offset + 10, 80, 25)
self.screen.blit(self.button_surfaces['clear'], self.clear_screen_button) self.screen.blit(
self.button_surfaces["clear"], self.clear_screen_button
)
# Draw Settings menu button # Draw Settings menu button
if 'settings' not in self.button_surfaces: if "settings" not in self.button_surfaces:
settings_surface = pygame.Surface((80, 25)) settings_surface = pygame.Surface((80, 25))
settings_surface.fill((255, 255, 255)) settings_surface.fill((255, 255, 255))
font = self.cached_fonts.get('button') font = self.cached_fonts.get("button")
label = font.render("Settings", True, (0, 0, 0)) label = font.render("Settings", True, (0, 0, 0))
settings_surface.blit(label, (5, 5)) settings_surface.blit(label, (5, 5))
self.button_surfaces['settings'] = settings_surface self.button_surfaces["settings"] = settings_surface
self.settings_button = pygame.Rect(x_offset, y_offset + 50, 80, 25) self.settings_button = pygame.Rect(x_offset, y_offset + 50, 80, 25)
self.screen.blit(self.button_surfaces['settings'], self.settings_button) self.screen.blit(
self.button_surfaces["settings"], self.settings_button
)
def render_brush_cursor(self, x, y, radius): def render_brush_cursor(self, x, y, radius):
if engine_settings ['enable_cursor']: """Draw outline circle"""
# Draw outline circle if engine_settings["enable_cursor"]:
pygame.draw.circle(self.screen, (255, 255, 255), (x, y), radius, 1) pygame.draw.circle(self.screen, (255, 255, 255), (x, y), radius, 1)
# Draw slightly transparent fill # Draw slightly transparent fill
cursor_surface = pygame.Surface((radius*2, radius*2), pygame.SRCALPHA) cursor_surface = pygame.Surface(
pygame.draw.circle(cursor_surface, (255, 255, 255, 55), (radius, radius), radius) (radius * 2, radius * 2), pygame.SRCALPHA
)
pygame.draw.circle(
cursor_surface, (255, 255, 255, 55), (radius, radius), radius
)
self.screen.blit(cursor_surface, (x - radius, y - radius)) self.screen.blit(cursor_surface, (x - radius, y - radius))
def draw_brush_size_slider(self, brush_size):
def draw_brush_size_slider(self, brush_size): # this is the function that draws the brush size slider """Draw the slider for brush size"""
# Draw the slider for brush size
pygame.draw.rect(self.screen, (255, 255, 255), (500, 10, 100, 20)) pygame.draw.rect(self.screen, (255, 255, 255), (500, 10, 100, 20))
pygame.draw.rect(self.screen, (0, 0, 0), (500, 10, 100, 20), 2) pygame.draw.rect(self.screen, (0, 0, 0), (500, 10, 100, 20), 2)
pygame.draw.rect(self.screen, (255, 0, 0), (500 + brush_size, 10, 100 - brush_size * 2, 20)) pygame.draw.rect(
label = self.cached_fonts.get('brush_size').render(f"Brush Size: {brush_size}", True, (255, 255, 255)) self.screen,
(255, 0, 0),
(500 + brush_size, 10, 100 - brush_size * 2, 20),
)
label = self.cached_fonts.get("brush_size").render(
f"Brush Size: {brush_size}", True, (255, 255, 255)
)
self.screen.blit(label, (500, 10)) self.screen.blit(label, (500, 10))
def draw_settings_menu(self): def draw_settings_menu(self):
"""Draw the settings menu"""
settings_surface = pygame.Surface((300, 400)) settings_surface = pygame.Surface((300, 400))
settings_surface.fill((50, 50, 50)) settings_surface.fill((50, 50, 50))
y_offset = 10 y_offset = 10
font = self.cached_fonts.get('settings') font = self.cached_fonts.get("settings")
for setting, value in engine_settings.items(): for setting, value in engine_settings.items():
# Create toggle button # Create toggle button
button_rect = pygame.Rect(10, y_offset, 20, 20) button_rect = pygame.Rect(10, y_offset, 20, 20)
pygame.draw.rect(settings_surface, (0, 255, 0) if value else (255, 0, 0), button_rect) pygame.draw.rect(
settings_surface,
(0, 255, 0) if value else (255, 0, 0),
button_rect,
)
# Draw setting name # Draw setting name
label = font.render(setting.replace('_', ' ').title(), True, (255, 255, 255)) label = font.render(
setting.replace("_", " ").title(), True, (255, 255, 255)
)
settings_surface.blit(label, (40, y_offset)) settings_surface.blit(label, (40, y_offset))
y_offset += 30 y_offset += 30
return settings_surface return settings_surface
def clear_screen(self, sim):
def clear_screen(self, sim): # this is the function that clears the screen """Store current particle type"""
# Store current particle type
current_type = sim.current_particle_type current_type = sim.current_particle_type
self.particle_count = 0 self.particle_count = 0
# Reset simulation grid while preserving particle type # Reset simulation grid while preserving particle type
sim.particles = [[None for _ in range(sim.height)] for _ in range(sim.width)] sim.particles = [
[None for _ in range(sim.height)] for _ in range(sim.width)
]
sim.active_particles.clear() sim.active_particles.clear()
sim.current_particle_type = current_type sim.current_particle_type = current_type
sim.reset_particle_count() sim.reset_particle_count()
# Clear display surfaces # Clear display surfaces
self.background.fill((0, 0, 0)) self.background.fill((0, 0, 0))
self.particle_surface.fill((0, 0, 0, 0)) self.particle_surface.fill((0, 0, 0, 0))