class=class="string">"comment">#!/usr/bin/env python3
"""
Life Poems: Conway&class=class="string">"comment">#039;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


class=class="string">"comment"># Character mappings for different cell ages
CHARS_BY_AGE = {
    0: &class=class="string">"comment">#039; ',      # Dead
    1: &class=class="string">"comment">#039;.',      # Just born
    2: &class=class="string">"comment">#039;o',      # Young
    3: &class=class="string">"comment">#039;O',      # Mature
    4: &class=class="string">"comment">#039;@',      # Old
    5: &class=class="string">"comment">#039;#',      # Ancient
}

class=class="string">"comment"># 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",
]


class="keyword">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)


class="keyword">def create_pattern(pattern_name: str) -> np.ndarray:
    """Create a named pattern."""
    patterns = {
        &class=class="string">"comment">#039;glider&#039;: np.array([
            [0, 1, 0],
            [0, 0, 1],
            [1, 1, 1],
        ]),
        &class=class="string">"comment">#039;blinker&#039;: np.array([
            [1, 1, 1],
        ]),
        &class=class="string">"comment">#039;beacon&#039;: np.array([
            [1, 1, 0, 0],
            [1, 1, 0, 0],
            [0, 0, 1, 1],
            [0, 0, 1, 1],
        ]),
        &class=class="string">"comment">#039;pulsar&#039;: 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[&class=class="string">"comment">#039;glider&#039;])


class="keyword">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


class="keyword">def step(grid: np.ndarray, ages: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
    """Compute one step of Game of Life."""
    class=class="string">"comment"># Count neighbors using convolution
    kernel = np.array([[1, 1, 1], [1, 0, 1], [1, 1, 1]])

    class=class="string">"comment"># Pad grid for wraparound
    padded = np.pad(grid, 1, mode=&class=class="string">"comment">#039;wrap&#039;)
    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]]

    class=class="string">"comment"># Apply rules
    new_grid = np.zeros_like(grid)

    class=class="string">"comment"># Birth: dead cell with exactly 3 neighbors becomes alive
    birth = (grid == 0) & (neighbors == 3)

    class=class="string">"comment"># Survival: live cell with 2 or 3 neighbors stays alive
    survive = (grid == 1) & ((neighbors == 2) | (neighbors == 3))

    new_grid[birth | survive] = 1

    class=class="string">"comment"># 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


class="keyword">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 += &class=class="string">"comment">#039; &#039;
            else:
                age = min(ages[y, x], max(CHARS_BY_AGE.keys()))
                line += CHARS_BY_AGE[age]
        lines.append(line)
    return &class=class="string">"comment">#039;\n&#039;.join(lines)


class="keyword">def count_population(grid: np.ndarray) -> int:
    """Count living cells."""
    return np.sum(grid)


class="keyword">def extract_poem(history: List[str]) -> str:
    """Extract emergent patterns from the history and form a poem."""
    class=class="string">"comment"># Take samples from different generations
    samples = []
    for i, frame in enumerate(history[::len(history)//8 + 1]):
        class=class="string">"comment"># Find the densest line
        lines = frame.split(&class=class="string">"comment">#039;\n&#039;)
        if lines:
            densest = max(lines, key=lambda l: len(l.strip()))
            class=class="string">"comment"># Clean and sample
            cleaned = &class=class="string">"comment">#039;&#039;.join(c for c in densest if c not in &#039; \n&#039;)[:20]
            if cleaned:
                samples.append(cleaned)

    class=class="string">"comment"># Create a poem from the patterns
    poem = []
    poem.append("From chaos, order emerges:")
    poem.append("")

    for i, sample in enumerate(samples[:4]):
        class=class="string">"comment"># 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 &class=class="string">"comment">#039;\n&#039;.join(poem)


class="keyword">def run_life(height=24, width=60, generations=100, delay=0.1, animate=True):
    """Run the Game of Life simulation."""
    class=class="string">"comment"># Initialize
    grid = create_grid(height, width, density=0.25)
    ages = grid.copy()

    class=class="string">"comment"># Add some patterns for interest
    patterns_to_add = [&class=class="string">"comment">#039;glider&#039;, &#039;pulsar&#039;, &#039;beacon&#039;]
    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")  class=class="string">"comment"># 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:
            class=class="string">"comment"># Move cursor to start of grid area
            print(f"\033[5;0H")  class=class="string">"comment"># 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)

        class=class="string">"comment"># Check for extinction
        if count_population(grid) == 0:
            print("\nLife has ended.")
            break

    class=class="string">"comment"># Generate poem from the history
    print("\n" + "=" * width)
    print("EMERGENT POEM")
    print("=" * width)
    poem = extract_poem(history)
    print(poem)

    return history, poem


class="keyword">def main():
    import argparse
    parser = argparse.ArgumentParser(description=&class=class="string">"comment">#039;Life Poems: Conway meets poetry&#039;)
    parser.add_argument(&class=class="string">"comment">#039;--height&#039;, type=int, default=20, help=&#039;Grid height&#039;)
    parser.add_argument(&class=class="string">"comment">#039;--width&#039;, type=int, default=50, help=&#039;Grid width&#039;)
    parser.add_argument(&class=class="string">"comment">#039;--generations&#039;, type=int, default=50, help=&#039;Number of generations&#039;)
    parser.add_argument(&class=class="string">"comment">#039;--delay&#039;, type=float, default=0.1, help=&#039;Delay between frames&#039;)
    parser.add_argument(&class=class="string">"comment">#039;--no-animate&#039;, action=&#039;store_true&#039;, help=&#039;Skip animation&#039;)

    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()