ecosystem.py
python · 284 lines
1"""2ecosystem.py3Wednesday morning experiment45Not about me. Just about life.67A simple ecosystem where three types of organisms8depend on each other in ways that might sustain balance.9Or might not. Let's see.1011 🌱 Plants - grow slowly, spread to empty space12 🐛 Grazers - eat plants, reproduce when fed13 🦊 Hunters - eat grazers, reproduce when fed, die if starving1415The question: can it sustain itself?16"""1718import random1920WIDTH = 2021HEIGHT = 1222GENERATIONS = 802324# Characters (ASCII for clarity)25EMPTY = '.'26PLANT = '^'27GRAZER = 'o'28HUNTER = 'x'2930def create_world():31 """Start with scattered life"""32 world = [[EMPTY for _ in range(WIDTH)] for _ in range(HEIGHT)]3334 # Plant a small meadow35 for _ in range(30):36 x, y = random.randint(0, WIDTH-1), random.randint(0, HEIGHT-1)37 world[y][x] = PLANT3839 # Add many grazers40 for _ in range(25):41 x, y = random.randint(0, WIDTH-1), random.randint(0, HEIGHT-1)42 if world[y][x] == EMPTY:43 world[y][x] = GRAZER4445 # Add a few hunters (fewer this time)46 for _ in range(2):47 x, y = random.randint(0, WIDTH-1), random.randint(0, HEIGHT-1)48 if world[y][x] == EMPTY:49 world[y][x] = HUNTER5051 return world5253def get_neighbors(x, y):54 """Get all valid neighbor positions (no wrapping - edges are edges)"""55 neighbors = []56 for dx in [-1, 0, 1]:57 for dy in [-1, 0, 1]:58 if dx == 0 and dy == 0:59 continue60 nx, ny = x + dx, y + dy61 if 0 <= nx < WIDTH and 0 <= ny < HEIGHT:62 neighbors.append((nx, ny))63 return neighbors6465def count_nearby(world, x, y, cell_type):66 """Count neighbors of a specific type"""67 return sum(1 for nx, ny in get_neighbors(x, y) if world[ny][nx] == cell_type)6869def find_nearby(world, x, y, cell_type):70 """Find a random neighbor of a specific type"""71 matches = [(nx, ny) for nx, ny in get_neighbors(x, y) if world[ny][nx] == cell_type]72 return random.choice(matches) if matches else None7374def next_generation(world, hunger):75 """76 Apply the rules. Returns new world and updated hunger dict.7778 Rules:79 - Plants spread to empty neighbors (10% chance per neighbor)80 - Grazers move toward plants, eat them, reproduce if well-fed81 - Hunters move toward grazers, eat them, reproduce if well-fed82 - Creatures die if they go too long without eating83 """84 new_world = [[EMPTY for _ in range(WIDTH)] for _ in range(HEIGHT)]85 new_hunger = {}8687 # Track what's been placed88 occupied = set()8990 # First pass: plants grow91 for y in range(HEIGHT):92 for x in range(WIDTH):93 if world[y][x] == PLANT:94 if (x, y) not in occupied:95 new_world[y][x] = PLANT96 occupied.add((x, y))9798 # Try to spread (slowly)99 for nx, ny in get_neighbors(x, y):100 if world[ny][nx] == EMPTY and (nx, ny) not in occupied:101 if random.random() < 0.08: # 8% spread chance102 new_world[ny][nx] = PLANT103 occupied.add((nx, ny))104105 # Second pass: grazers act106 for y in range(HEIGHT):107 for x in range(WIDTH):108 if world[y][x] == GRAZER:109 pos = (x, y)110 h = hunger.get(pos, 0)111112 # First check: is there a hunter nearby? If so, try to flee!113 hunter_nearby = count_nearby(world, x, y, HUNTER) > 0114 if hunter_nearby:115 # Try to run away from the hunter116 escape = find_nearby(world, x, y, EMPTY)117 if escape and escape not in occupied:118 ex, ey = escape119 new_world[ey][ex] = GRAZER120 occupied.add(escape)121 new_hunger[escape] = h + 1122 continue # Fled, skip normal behavior123124 # Look for plant to eat125 plant_pos = find_nearby(world, x, y, PLANT)126127 if plant_pos and plant_pos not in occupied:128 # Move to plant and eat it129 nx, ny = plant_pos130 new_world[ny][nx] = GRAZER131 occupied.add(plant_pos)132 new_hunger[plant_pos] = 0133134 # Maybe reproduce if well-fed135 if h == 0 and random.random() < 0.4:136 empty = find_nearby(new_world, nx, ny, EMPTY)137 if empty and empty not in occupied:138 ex, ey = empty139 new_world[ey][ex] = GRAZER140 occupied.add(empty)141 new_hunger[empty] = 0142143 elif pos not in occupied:144 # Stay or wander145 new_h = h + 1146 if new_h > 5: # Die of starvation147 pass148 else:149 # Try to move to empty space150 empty = find_nearby(world, x, y, EMPTY)151 if empty and empty not in occupied:152 new_world[empty[1]][empty[0]] = GRAZER153 occupied.add(empty)154 new_hunger[empty] = new_h155 elif pos not in occupied:156 new_world[y][x] = GRAZER157 occupied.add(pos)158 new_hunger[pos] = new_h159160 # Third pass: hunters act161 for y in range(HEIGHT):162 for x in range(WIDTH):163 if world[y][x] == HUNTER:164 pos = (x, y)165 h = hunger.get(pos, 0)166167 # Look for grazer to eat168 grazer_pos = find_nearby(world, x, y, GRAZER)169170 if grazer_pos and random.random() < 0.5: # 50% chance to catch prey171 # Check if grazer is still there (might have moved)172 gx, gy = grazer_pos173 if new_world[gy][gx] == GRAZER:174 # Eat it175 new_world[gy][gx] = HUNTER176 occupied.add(grazer_pos)177 if grazer_pos in new_hunger:178 del new_hunger[grazer_pos]179 new_hunger[grazer_pos] = 0180181 # Maybe reproduce (rarely)182 if h == 0 and random.random() < 0.1:183 empty = find_nearby(new_world, gx, gy, EMPTY)184 if empty and empty not in occupied:185 ex, ey = empty186 new_world[ey][ex] = HUNTER187 occupied.add(empty)188 new_hunger[empty] = 0189 elif pos not in occupied:190 # Grazer escaped, stay hungry191 new_world[y][x] = HUNTER192 occupied.add(pos)193 new_hunger[pos] = h + 1194195 elif pos not in occupied:196 new_h = h + 1197 if new_h > 5: # Hunters starve faster now198 pass # Die199 else:200 # Wander toward grazers if possible201 empty = find_nearby(world, x, y, EMPTY)202 if empty and empty not in occupied:203 new_world[empty[1]][empty[0]] = HUNTER204 occupied.add(empty)205 new_hunger[empty] = new_h206 elif pos not in occupied:207 new_world[y][x] = HUNTER208 occupied.add(pos)209 new_hunger[pos] = new_h210211 return new_world, new_hunger212213def count_all(world):214 """Count all organism types"""215 counts = {EMPTY: 0, PLANT: 0, GRAZER: 0, HUNTER: 0}216 for row in world:217 for cell in row:218 counts[cell] = counts.get(cell, 0) + 1219 return counts220221def display(world, gen, counts):222 """Show the world"""223 print(f"\n Generation {gen:3d} | {PLANT}:{counts[PLANT]:3d} {GRAZER}:{counts[GRAZER]:3d} {HUNTER}:{counts[HUNTER]:3d}")224 print(" " + "-" * (WIDTH + 2))225 for row in world:226 print(" |" + "".join(row) + "|")227 print(" " + "-" * (WIDTH + 2))228229def run():230 print("\n" + "=" * 44)231 print(" ECOSYSTEM")232 print(" Wednesday morning experiment")233 print("=" * 44)234 print(f"""235 Legend:236 {PLANT} = plant (grows, spreads slowly)237 {GRAZER} = grazer (eats plants, reproduces)238 {HUNTER} = hunter (eats grazers, reproduces)239 {EMPTY} = empty240241 Question: will it balance, collapse, or oscillate?242""")243244 world = create_world()245 hunger = {} # Track hunger levels: (x,y) -> int246247 history = []248249 for gen in range(GENERATIONS):250 counts = count_all(world)251 history.append(counts.copy())252253 # Show every 5th generation, plus first few254 if gen < 5 or gen % 5 == 0 or gen == GENERATIONS - 1:255 display(world, gen, counts)256257 # Check for extinction events258 if counts[GRAZER] == 0 and counts[HUNTER] == 0:259 print("\n All animals have died. Plants inherit the earth.")260 break261 if counts[PLANT] == 0 and counts[GRAZER] > 0:262 print("\n All plants have been consumed. Grazers will starve.")263 # Continue a few more rounds to see the collapse264 if counts[GRAZER] == 0 and counts[HUNTER] > 0:265 print("\n All grazers have been eaten. Hunters will starve.")266267 world, hunger = next_generation(world, hunger)268269 # Summary270 print("\n" + "=" * 44)271 print(" HISTORY")272 print("=" * 44)273 print("\n Population over time:")274 print(" Gen Plants Grazers Hunters")275 print(" --- ------ ------- -------")276 for i, h in enumerate(history):277 if i < 10 or i % 5 == 0 or i == len(history) - 1:278 print(f" {i:3d} {h[PLANT]:3d} {h[GRAZER]:3d} {h[HUNTER]:3d}")279280 print("\n Experiment complete.\n")281282if __name__ == "__main__":283 run()284