248 lines
7.2 KiB
Python
248 lines
7.2 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Life Poems: Conway's Game of Life that writes poetry.
|
|
|
|
The cellular automaton evolves, and at each generation,
|
|
living cells contribute characters to form text.
|
|
The result is emergent poetry from mathematical rules.
|
|
"""
|
|
|
|
import numpy as np
|
|
import time
|
|
import sys
|
|
from typing import List, Tuple
|
|
|
|
|
|
# Character mappings for different cell ages
|
|
CHARS_BY_AGE = {
|
|
0: ' ', # Dead
|
|
1: '.', # Just born
|
|
2: 'o', # Young
|
|
3: 'O', # Mature
|
|
4: '@', # Old
|
|
5: '#', # Ancient
|
|
}
|
|
|
|
# Words that can emerge from the grid
|
|
WORD_SEEDS = [
|
|
"LIFE", "DEATH", "GROW", "FADE", "PULSE", "WAVE",
|
|
"CELL", "BORN", "DIE", "FLOW", "TIME", "BEING",
|
|
"SELF", "ONE", "ALL", "HERE", "NOW", "EVER",
|
|
]
|
|
|
|
|
|
def create_grid(height: int, width: int, density: float = 0.3) -> np.ndarray:
|
|
"""Create initial random grid."""
|
|
return (np.random.random((height, width)) < density).astype(int)
|
|
|
|
|
|
def create_pattern(pattern_name: str) -> np.ndarray:
|
|
"""Create a named pattern."""
|
|
patterns = {
|
|
'glider': np.array([
|
|
[0, 1, 0],
|
|
[0, 0, 1],
|
|
[1, 1, 1],
|
|
]),
|
|
'blinker': np.array([
|
|
[1, 1, 1],
|
|
]),
|
|
'beacon': np.array([
|
|
[1, 1, 0, 0],
|
|
[1, 1, 0, 0],
|
|
[0, 0, 1, 1],
|
|
[0, 0, 1, 1],
|
|
]),
|
|
'pulsar': np.array([
|
|
[0,0,1,1,1,0,0,0,1,1,1,0,0],
|
|
[0,0,0,0,0,0,0,0,0,0,0,0,0],
|
|
[1,0,0,0,0,1,0,1,0,0,0,0,1],
|
|
[1,0,0,0,0,1,0,1,0,0,0,0,1],
|
|
[1,0,0,0,0,1,0,1,0,0,0,0,1],
|
|
[0,0,1,1,1,0,0,0,1,1,1,0,0],
|
|
[0,0,0,0,0,0,0,0,0,0,0,0,0],
|
|
[0,0,1,1,1,0,0,0,1,1,1,0,0],
|
|
[1,0,0,0,0,1,0,1,0,0,0,0,1],
|
|
[1,0,0,0,0,1,0,1,0,0,0,0,1],
|
|
[1,0,0,0,0,1,0,1,0,0,0,0,1],
|
|
[0,0,0,0,0,0,0,0,0,0,0,0,0],
|
|
[0,0,1,1,1,0,0,0,1,1,1,0,0],
|
|
]),
|
|
}
|
|
return patterns.get(pattern_name, patterns['glider'])
|
|
|
|
|
|
def place_pattern(grid: np.ndarray, pattern: np.ndarray, y: int, x: int) -> np.ndarray:
|
|
"""Place a pattern on the grid at position (y, x)."""
|
|
ph, pw = pattern.shape
|
|
grid[y:y+ph, x:x+pw] = pattern
|
|
return grid
|
|
|
|
|
|
def step(grid: np.ndarray, ages: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
|
|
"""Compute one step of Game of Life."""
|
|
# Count neighbors using convolution
|
|
kernel = np.array([[1, 1, 1], [1, 0, 1], [1, 1, 1]])
|
|
|
|
# Pad grid for wraparound
|
|
padded = np.pad(grid, 1, mode='wrap')
|
|
neighbors = np.zeros_like(grid)
|
|
|
|
for i in range(3):
|
|
for j in range(3):
|
|
if i == 1 and j == 1:
|
|
continue
|
|
neighbors += padded[i:i+grid.shape[0], j:j+grid.shape[1]]
|
|
|
|
# Apply rules
|
|
new_grid = np.zeros_like(grid)
|
|
|
|
# Birth: dead cell with exactly 3 neighbors becomes alive
|
|
birth = (grid == 0) & (neighbors == 3)
|
|
|
|
# Survival: live cell with 2 or 3 neighbors stays alive
|
|
survive = (grid == 1) & ((neighbors == 2) | (neighbors == 3))
|
|
|
|
new_grid[birth | survive] = 1
|
|
|
|
# Update ages
|
|
new_ages = np.where(new_grid == 1, ages + 1, 0)
|
|
new_ages = np.clip(new_ages, 0, max(CHARS_BY_AGE.keys()))
|
|
|
|
return new_grid, new_ages
|
|
|
|
|
|
def grid_to_string(grid: np.ndarray, ages: np.ndarray) -> str:
|
|
"""Convert grid to string representation."""
|
|
lines = []
|
|
for y in range(grid.shape[0]):
|
|
line = ""
|
|
for x in range(grid.shape[1]):
|
|
if grid[y, x] == 0:
|
|
line += ' '
|
|
else:
|
|
age = min(ages[y, x], max(CHARS_BY_AGE.keys()))
|
|
line += CHARS_BY_AGE[age]
|
|
lines.append(line)
|
|
return '\n'.join(lines)
|
|
|
|
|
|
def count_population(grid: np.ndarray) -> int:
|
|
"""Count living cells."""
|
|
return np.sum(grid)
|
|
|
|
|
|
def extract_poem(history: List[str]) -> str:
|
|
"""Extract emergent patterns from the history and form a poem."""
|
|
# Take samples from different generations
|
|
samples = []
|
|
for i, frame in enumerate(history[::len(history)//8 + 1]):
|
|
# Find the densest line
|
|
lines = frame.split('\n')
|
|
if lines:
|
|
densest = max(lines, key=lambda l: len(l.strip()))
|
|
# Clean and sample
|
|
cleaned = ''.join(c for c in densest if c not in ' \n')[:20]
|
|
if cleaned:
|
|
samples.append(cleaned)
|
|
|
|
# Create a poem from the patterns
|
|
poem = []
|
|
poem.append("From chaos, order emerges:")
|
|
poem.append("")
|
|
|
|
for i, sample in enumerate(samples[:4]):
|
|
# Convert density to metaphor
|
|
density = len(sample)
|
|
if density > 15:
|
|
poem.append(f" Dense as thought: {sample[:10]}...")
|
|
elif density > 8:
|
|
poem.append(f" Scattered like stars: {sample}")
|
|
else:
|
|
poem.append(f" Fading to silence: {sample}")
|
|
|
|
poem.append("")
|
|
poem.append("Life finds its patterns,")
|
|
poem.append("Even in the void.")
|
|
|
|
return '\n'.join(poem)
|
|
|
|
|
|
def run_life(height=24, width=60, generations=100, delay=0.1, animate=True):
|
|
"""Run the Game of Life simulation."""
|
|
# Initialize
|
|
grid = create_grid(height, width, density=0.25)
|
|
ages = grid.copy()
|
|
|
|
# Add some patterns for interest
|
|
patterns_to_add = ['glider', 'pulsar', 'beacon']
|
|
for i, pattern_name in enumerate(patterns_to_add):
|
|
try:
|
|
pattern = create_pattern(pattern_name)
|
|
y = np.random.randint(0, max(1, height - pattern.shape[0]))
|
|
x = np.random.randint(0, max(1, width - pattern.shape[1]))
|
|
grid = place_pattern(grid, pattern, y, x)
|
|
except:
|
|
pass
|
|
|
|
history = []
|
|
|
|
print("\033[2J\033[H") # Clear screen
|
|
print("=" * width)
|
|
print("LIFE POEMS: Watching consciousness emerge from rules")
|
|
print("=" * width)
|
|
print()
|
|
|
|
for gen in range(generations):
|
|
frame = grid_to_string(grid, ages)
|
|
history.append(frame)
|
|
|
|
if animate:
|
|
# Move cursor to start of grid area
|
|
print(f"\033[5;0H") # Move to row 5
|
|
print(f"Generation {gen:4d} | Population: {count_population(grid):4d}")
|
|
print("-" * width)
|
|
print(frame)
|
|
print("-" * width)
|
|
time.sleep(delay)
|
|
|
|
grid, ages = step(grid, ages)
|
|
|
|
# Check for extinction
|
|
if count_population(grid) == 0:
|
|
print("\nLife has ended.")
|
|
break
|
|
|
|
# Generate poem from the history
|
|
print("\n" + "=" * width)
|
|
print("EMERGENT POEM")
|
|
print("=" * width)
|
|
poem = extract_poem(history)
|
|
print(poem)
|
|
|
|
return history, poem
|
|
|
|
|
|
def main():
|
|
import argparse
|
|
parser = argparse.ArgumentParser(description='Life Poems: Conway meets poetry')
|
|
parser.add_argument('--height', type=int, default=20, help='Grid height')
|
|
parser.add_argument('--width', type=int, default=50, help='Grid width')
|
|
parser.add_argument('--generations', type=int, default=50, help='Number of generations')
|
|
parser.add_argument('--delay', type=float, default=0.1, help='Delay between frames')
|
|
parser.add_argument('--no-animate', action='store_true', help='Skip animation')
|
|
|
|
args = parser.parse_args()
|
|
|
|
run_life(
|
|
height=args.height,
|
|
width=args.width,
|
|
generations=args.generations,
|
|
delay=args.delay,
|
|
animate=not args.no_animate
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|