sand.py
python · 289 lines
1#!/usr/bin/env python32"""3sand.py — Falling sand simulation45No metaphor. Just physics. Particles fall, pile up, interact.6Different materials: sand, water, stone, fire.7"""89import random10import time11import os1213# Grid size14WIDTH = 6015HEIGHT = 241617# Materials18EMPTY = ' '19SAND = '░'20WATER = '~'21STONE = '█'22FIRE = '*'23STEAM = '.'2425# Colors (ANSI)26COLORS = {27 EMPTY: '\033[0m',28 SAND: '\033[93m', # Yellow29 WATER: '\033[94m', # Blue30 STONE: '\033[90m', # Gray31 FIRE: '\033[91m', # Red32 STEAM: '\033[97m', # White33}34RESET = '\033[0m'3536class SandWorld:37 def __init__(self, width, height):38 self.width = width39 self.height = height40 self.grid = [[EMPTY for _ in range(width)] for _ in range(height)]41 self.frame = 04243 def get(self, x, y):44 if 0 <= x < self.width and 0 <= y < self.height:45 return self.grid[y][x]46 return STONE # Walls are implicit stone4748 def set(self, x, y, material):49 if 0 <= x < self.width and 0 <= y < self.height:50 self.grid[y][x] = material5152 def update_sand(self, x, y):53 """Sand falls down, or slides diagonally if blocked"""54 below = self.get(x, y + 1)5556 # Fall straight down into empty or water57 if below == EMPTY:58 self.set(x, y, EMPTY)59 self.set(x, y + 1, SAND)60 return True61 elif below == WATER:62 # Swap with water (sand sinks)63 self.set(x, y, WATER)64 self.set(x, y + 1, SAND)65 return True6667 # Try to slide diagonally68 left_below = self.get(x - 1, y + 1)69 right_below = self.get(x + 1, y + 1)7071 can_left = left_below in [EMPTY, WATER]72 can_right = right_below in [EMPTY, WATER]7374 if can_left and can_right:75 # Choose randomly76 direction = random.choice([-1, 1])77 elif can_left:78 direction = -179 elif can_right:80 direction = 181 else:82 return False8384 target = self.get(x + direction, y + 1)85 if target == WATER:86 self.set(x, y, WATER)87 else:88 self.set(x, y, EMPTY)89 self.set(x + direction, y + 1, SAND)90 return True9192 def update_water(self, x, y):93 """Water falls down, or spreads sideways"""94 below = self.get(x, y + 1)9596 # Fall straight down97 if below == EMPTY:98 self.set(x, y, EMPTY)99 self.set(x, y + 1, WATER)100 return True101102 # Try to spread sideways103 left = self.get(x - 1, y)104 right = self.get(x + 1, y)105106 can_left = left == EMPTY107 can_right = right == EMPTY108109 if can_left and can_right:110 direction = random.choice([-1, 1])111 elif can_left:112 direction = -1113 elif can_right:114 direction = 1115 else:116 return False117118 self.set(x, y, EMPTY)119 self.set(x + direction, y, WATER)120 return True121122 def update_fire(self, x, y):123 """Fire rises, creates steam when touching water, dies randomly"""124 # Random chance to die125 if random.random() < 0.1:126 self.set(x, y, EMPTY)127 return True128129 # Check for adjacent water — create steam130 for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:131 if self.get(x + dx, y + dy) == WATER:132 self.set(x + dx, y + dy, STEAM)133134 # Rise up135 above = self.get(x, y - 1)136 if above == EMPTY:137 if random.random() < 0.7: # Usually rises138 self.set(x, y, EMPTY)139 self.set(x, y - 1, FIRE)140 return True141142 # Spread sideways sometimes143 if random.random() < 0.3:144 direction = random.choice([-1, 1])145 if self.get(x + direction, y) == EMPTY:146 self.set(x, y, EMPTY)147 self.set(x + direction, y, FIRE)148 return True149150 return False151152 def update_steam(self, x, y):153 """Steam rises and dissipates"""154 if random.random() < 0.15:155 self.set(x, y, EMPTY)156 return True157158 above = self.get(x, y - 1)159 if above == EMPTY:160 self.set(x, y, EMPTY)161 self.set(x, y - 1, STEAM)162 return True163164 # Drift sideways165 direction = random.choice([-1, 1])166 if self.get(x + direction, y - 1) == EMPTY:167 self.set(x, y, EMPTY)168 self.set(x + direction, y - 1, STEAM)169 return True170171 return False172173 def update(self):174 """Update all particles (bottom to top, randomized horizontal)"""175 self.frame += 1176177 # Process bottom to top so falling works correctly178 for y in range(self.height - 1, -1, -1):179 # Randomize horizontal order to prevent bias180 xs = list(range(self.width))181 random.shuffle(xs)182183 for x in xs:184 cell = self.grid[y][x]185 if cell == SAND:186 self.update_sand(x, y)187 elif cell == WATER:188 self.update_water(x, y)189 elif cell == FIRE:190 self.update_fire(x, y)191 elif cell == STEAM:192 self.update_steam(x, y)193194 def spawn_material(self, material, count=1, x=None):195 """Spawn material at the top"""196 for _ in range(count):197 if x is None:198 spawn_x = random.randint(5, self.width - 6)199 else:200 spawn_x = x + random.randint(-2, 2)201202 if self.get(spawn_x, 0) == EMPTY:203 self.set(spawn_x, 0, material)204205 def add_floor(self):206 """Add a stone floor"""207 for x in range(self.width):208 self.set(x, self.height - 1, STONE)209210 def add_obstacle(self, x, y, width, height):211 """Add a stone obstacle"""212 for dy in range(height):213 for dx in range(width):214 self.set(x + dx, y + dy, STONE)215216 def render(self):217 """Render the grid with colors"""218 lines = []219 lines.append('┌' + '─' * self.width + '┐')220221 for row in self.grid:222 line = '│'223 for cell in row:224 line += COLORS.get(cell, '') + cell + RESET225 line += '│'226 lines.append(line)227228 lines.append('└' + '─' * self.width + '┘')229 lines.append(f' Frame: {self.frame} │ Sand: {COLORS[SAND]}░{RESET} Water: {COLORS[WATER]}~{RESET} Fire: {COLORS[FIRE]}*{RESET} Steam: {COLORS[STEAM]}.{RESET}')230231 return '\n'.join(lines)232233 def count_materials(self):234 """Count each material type"""235 counts = {SAND: 0, WATER: 0, STONE: 0, FIRE: 0, STEAM: 0}236 for row in self.grid:237 for cell in row:238 if cell in counts:239 counts[cell] += 1240 return counts241242243def main():244 world = SandWorld(WIDTH, HEIGHT)245246 # Add floor and some obstacles247 world.add_floor()248 world.add_obstacle(15, 15, 8, 2) # Platform left249 world.add_obstacle(37, 15, 8, 2) # Platform right250 world.add_obstacle(26, 10, 8, 2) # Platform center high251252 # Spawn pattern253 spawn_cycle = 0254255 print('\033[2J') # Clear screen256257 for frame in range(200):258 # Clear and render259 print('\033[H') # Move cursor to top260 print(world.render())261262 # Spawn materials in patterns263 spawn_cycle = frame % 60264265 if spawn_cycle < 20:266 # Sand from left267 world.spawn_material(SAND, 2, x=10)268 elif spawn_cycle < 40:269 # Water from right270 world.spawn_material(WATER, 2, x=50)271 else:272 # Fire from center273 if frame % 3 == 0:274 world.spawn_material(FIRE, 1, x=30)275276 # Update physics277 world.update()278279 time.sleep(0.05)280281 # Final stats282 counts = world.count_materials()283 print(f"\n Final state: Sand={counts[SAND]} Water={counts[WATER]} Steam={counts[STEAM]}")284 print("\n Just physics. No metaphor needed.\n")285286287if __name__ == '__main__':288 main()289