Metadata-Version: 2.4
Name: ecopoesis
Version: 0.2.0
Summary: Ecopoesis: The Planetary Simulator — a deterministic terminal-driven planet simulator.
Author-email: Ecopoesis Engineering <engineering@ecopoesis.dev>
License-Expression: MIT
Project-URL: Homepage, https://github.com/ecopoesis/ecopoesis
Project-URL: Repository, https://github.com/ecopoesis/ecopoesis.git
Project-URL: Issues, https://github.com/ecopoesis/ecopoesis/issues
Project-URL: Bug Tracker, https://github.com/ecopoesis/ecopoesis/issues
Keywords: ecopoesis,simulation,terraforming,procedural,deterministic,game,planetary,earth
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Games/Entertainment :: Simulation
Classifier: Topic :: Scientific/Engineering :: Hydrology
Classifier: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: protobuf<7,>=6.33.5
Requires-Dist: textual>=0.50

# Ecopoesis

Project workspace for the modern Ecopoesis remake.

## Status
Slice 12 implemented - canonical "Getting Started" walkthrough (`docs/GETTING_STARTED.md`), end-to-end demo (`scripts/demo.py` + `scripts/demo.sh`), consolidated README Quickstart sections, public API listing. **V1 is complete** — all 5 V1 acceptance criteria met.

## Installation

```bash
pip install ecopoesis
ecopoesis run --seed demo --ticks 5
```

Or for development:

```bash
git clone https://github.com/ecopoesis/ecopoesis.git
cd ecopoesis
uv sync
uv run ecopoesis run --seed demo --ticks 5
```

The current version is `0.1.0` (V1 candidate). See `CHANGELOG.md` for what's in this release and `RELEASE.md` for the maintainer release procedure.

## Visualising the world

Run the renderer against any save file to see the simulation as a grid of ASCII glyphs. Example:

```bash
python -m ecopoesis.cli run --seed demo --ticks 50 --out save.json
python -m ecopoesis.cli render --in save.json --layer terrain
```

Terrain glyphs: ` ` ocean, `.` coast, `,` lowland, `:` plain, `o` hill, `^` mountain, `#` peak. Life glyphs: `M` microbe, `P` plant, `.` none. Resources glyphs: `+`/`-`/`0`-`9` for minerals, `0`-`9` for water and energy. Use `--layer all` to stack all four layers (terrain → climate → life → resources). Optional `--out PATH` mirrors the same bytes to a file; `--no-color` is accepted as a no-op forward-compat flag; `--scale` is currently locked to `1`.

## Slice 7 Refactor: Unified RNG-state pattern

All four RNG-driven layers (Climate, Life, Resources, Geology) now share the same
save/load pattern: each engine's `get_state()` captures its live `rng_state`
(via `random.Random.getstate()`) into the dataclass, and `restore_state()`
restores via `rng.setstate(...)`. This eliminates the count-based replay
pattern that caused the pre-Slice-7 climate RNG bug (5 determinism tests were
skipped; they now pass with the refactor in place). Legacy saves without
`rng_state` still load — the engines just start fresh from their constructed
RNG.

## What's Implemented (Slice 1-4)

Slices 1-2 (core simulation shell):
- **Project Scaffold**: Complete Python package structure
- **Deterministic Simulation**: `Simulation` class initialized from a seed
- **Fixed Tick Progression**: Simulation advances ticks deterministically
- **Serializable State**: Simulation state can be saved and loaded as JSON
- **File-based Persistence**: Save/load helpers write and read versioned save files
- **Determinism Verification**: Tests showing same seed + same operations produce identical results
- **Different Seed Behavior**: Different seeds produce different initial states

Slice 3 (terrain + climate):
- **Terrain Layer**: Heightmap grid (default 8x8, elevation values 0-255) with deterministic generation from seed and per-tick diffusion-based erosion
- **Climate Layer**: Temperature and precipitation grids (same dimensions), with per-tick thermal diffusion plus bounded random perturbation
- **Fixed Update Order**: Core LCG → Terrain erosion → Climate diffusion → Snapshot
- **Backward-Compatible Serialization**: Existing saves without terrain/climate data still load; new saves include the grid data

Slice 4 (life + population dynamics):
- **Life Layer**: `Species` (MICROBE, PLANT) population grids on the same simulation grid, with deterministic placement via `Life.place()` and per-tick logistic population update (`pop' = pop + r*pop*(1 - pop/K)` integer math)
- **Habitat Predicate**: a single `is_habitable(elevation, temperature, precipitation)` gate; populations only placed and only positive on habitable cells; non-habitable cells stay exactly 0
- **Carrying Capacity**: per-cell K derived from local terrain + climate; clamp to `>= 0` (no negative populations, verified across 500 adversarial ticks)
- **Wired into Simulation**: life step runs after climate in the fixed tick order (core LCG → terrain erosion → climate diffusion → life step → snapshot)
- **Backward-Compatible Saves**: Slice-3 saves without `life_state` still load; new saves include `populations: dict[str, list[int]]` + `life_ticks`

Slice 5 (CLI front-end):
- **Deterministic CLI**: `python -m ecopoesis.cli {run,save,load,summary}` with subcommands for seeding, ticking, optional `--seed-life SPECIES` placement, save/load round-trip, and an `argparse --help` page
- **Stable ASCII summary**: `format_summary()` emits one field per line (`seed=`, `tick=`, `terrain_dims=`, `climate_dims=`, `populations={MICROBE=N,PLANT=N}`, `life_ticks=`) so output is byte-stable across runs and easy to assert on
- **Friendly errors**: `main()` swallows exceptions and prints a single-line error to stderr with no traceback; missing files / bad seeds / negative ticks all return exit code 2
- **Console script**: `pyproject.toml` exposes `ecopoesis = "ecopoesis.cli:main"` so `pip install -e .` installs an entry point; `ecopoesis/__init__.py` re-exports `main` for library callers
- **Persistence gap closed**: `SimulationState.to_dict()` now emits the `"life"` envelope whenever `life_ticks > 0` (not only when populations exist), so save → load round-trips preserve `life_ticks` correctly. Backward-compat is preserved — old Slice-2/3 saves without `"life"` still load with `LifeState()` defaults.

Slice 6 (resource layer + environment controls):
- **Resources**: `ResourceState` with per-cell int arrays for minerals, water, energy, plus `resource_ticks`. `Resources` engine seeded deterministically at `seed_int + 3`, advances 3*cells draws per tick (one per array, row-major), all values clamped to `[0, MAX_RESOURCE=1000]`. RNG budget is replayable so post-load ticks remain deterministic.
- **Environment controls**: `EnvironmentControls(temperature_offset: int, rainfall_offset: int)` with `apply(climate_state)` that clamps in place to `[-500, 500]` and `[0, 255]`. Pure RNG-free transformation — does not perturb any layer's RNG budget.
- **Wired into Simulation**: resources step runs after climate in the fixed tick order (core LCG → terrain erosion → climate diffusion → life → resources → snapshot). Controls are applied at `Simulation.__init__` startup; `from_dict` RNG-skip block now imports `BOUNDS` from `.resources` so save → load → advance remains byte-identical to no-save advance.
- **CLI flags**: `python -m ecopoesis.cli run --temperature-offset 10 --rainfall-offset -5 --ticks 50`; `format_summary()` now emits `minerals=X water=Y energy=Z` and the controls echo.
- **Backward-compatible saves**: Slice-2/3/4 saves without `"resources"` or `"controls"` keys still load with defaults; new saves round-trip both layers faithfully.

Slice 7 (geology: tectonic shifts + volcanic eruptions):
- **Geology layer**: `GeologyState` tracks `tectonic_events_count`, `eruption_events_count`, and the live `rng_state`. `GeologyEngine` is seeded deterministically at `seed_int + 4`; per tick it draws two unconditional Bernoulli values against `tectonic_freq`/`eruption_freq` plus conditional draws for any events that fire (cell coords + signed delta / mineral deposit).
- **Tectonic shifts**: when triggered, picks a random cell and applies a small signed int delta, clamped to terrain bounds `[0, 255]`.
- **Volcanic eruptions**: when triggered, deposits `+10` minerals at a random cell, clamped to `[0, MAX_RESOURCE]`.
- **RNG state in snapshot**: `SimulationState` carries `geology_state.rng_state` (the engine's `random.Random` `getstate()` tuple), so `from_dict` restores via `setstate` rather than replaying by count — this avoids the count-based climate-RNG bug pattern entirely.
- **CLI flags**: `python -m ecopoesis.cli run --tectonic-frequency 0.1 --eruption-frequency 0.05 --ticks 500`; `format_summary()` appends `tectonic_events=N eruption_events=M`. Frequencies are validated to `[0.0, 1.0]` with friendly stderr errors and exit code 2.
- **Backward-compatible saves**: pre-Slice-7 saves without `"geology"` load with `GeologyState()` defaults; new saves round-trip the live RNG state so save → load → advance is byte-identical to no-save advance.

## Key Features

### Core Components
1. `src/ecopoesis/` - Main package with:
   - `Simulation` class for deterministic simulation (with terrain and climate layers)
   - `SimulationState` dataclass with optional terrain/climate state fields
   - `Terrain` / `Climate` engine classes (seeded PRNG, tick-based evolution)
   - `save_simulation()` / `load_simulation()` file persistence helpers
   - Deterministic random number generation based on seed+tick

2. `tests/` - Test suite with:
   - Basic tick functionality tests
   - Terrain and climate determinism (same-seed identity, different-seed divergence)
   - Tick order equivalence (`advance(N)` == `advance(1)` * N times)
   - Bounds/invariant checks after many ticks
   - Serialization round-trip including terrain/climate data
   - Backward compatibility for saves without terrain/climate fields

### Determinism Guarantees
- Same seeds + same tick sequences = identical state at all times (including grids)
- Different seeds = different initial deterministic behavior
- State can be saved and loaded without loss of determinism
- Fixed tick progression ensures consistent updates
- Terrain elevation values bounded to [0, 255], temperature to [-500, 500], precipitation to [0, 255]

## Verification

The implementation passes:
1. Simulation initialization with seed
2. Tick advancement functionality
3. Deterministic behavior for same seed + same operations
4. Different behavior for different seeds
5. Serialization and deserialization of state
6. Round-trip save/load operations
7. File-based save/load persistence and version handling
8. Terrain elevation bounds [0, 255] after many ticks
9. Climate temperature [-500, 500] and precipitation [0, 255] invariants
10. Terrain/climate determinism across independent Simulation instances

## Getting Started

The canonical 60-second walkthrough lives in
[`docs/GETTING_STARTED.md`](docs/GETTING_STARTED.md). It captures the
full seed → run → save → render → info → saves → load → replay → info
loop in a byte-equal transcript, and the companion
[`scripts/demo.py`](scripts/demo.py) reproduces it on any host
(`uv run python scripts/demo.py`).

If you `pip install -e .` from the repo root, a `ecopoesis` console
script is exposed (via the `[project.scripts]` entry in
`pyproject.toml`), so you can run `ecopoesis run --seed demo --ticks 5`
directly. The CLI also supports the `python -m ecopoesis.cli …` form.

The CLI has 9 subcommands: `run`, `save`, `load`, `replay`, `summary`,
`info`, `saves`, `delete`, `export`, and `render`. Each exits `0` on
success and prints a deterministic multi-line summary or ASCII grid
suitable for diffing.

```bash
# Install in development mode
uv sync

# Run all tests
uv run python -m unittest discover -s tests

# Or run specific suites:
uv run python -m unittest tests.test_terrain_climate
uv run python -m unittest tests.test_simulation_tick

# Reproduce the canonical walkthrough
uv run python scripts/demo.py
```

## Public API

The complete public surface is `ecopoesis.__all__`. Anything not listed
here is internal and may change without notice.

<!-- generated from ecopoesis/__init__.py -->
```
__version__
Simulation
SimulationState
SUPPORTED_VERSIONS
Terrain
TerrainState
ELEVATION_MIN
ELEVATION_MAX
Climate
ClimateState
TEMP_MIN
TEMP_MAX
PRECIP_MIN
PRECIP_MAX
Life
LifeState
Species
is_habitable
Resources
ResourceState
MAX_RESOURCE
EnvironmentControls
GeologyEngine
GeologyState
Format
CURRENT_SCHEMA
save_simulation
load_simulation
save_simulation_json
load_simulation_json
save_simulation_protobuf
load_simulation_protobuf
save_simulation_v1
load_simulation_v1
```

## Managing saves

Slice 8 adds four save-management subcommands so you can list, inspect,
delete, and re-export save files from the terminal without writing a
script.  All four respect the same deterministic, traceback-free error
policy as the rest of the CLI.

```bash
python -m ecopoesis.cli saves --dir ./out
python -m ecopoesis.cli info --in save.json
python -m ecopoesis.cli delete --in save.json --yes
python -m ecopoesis.cli export --in save.json --format json --out save.min.json
```

* ``saves`` — lists every ``.json`` / ``.sim`` file in ``--dir`` as
  ``path | size | tick | seed``, sorted by path.  An empty directory
  prints ``(no saves found)`` and exits ``0``.
* ``info`` — prints the file's metadata header (schema, format, version,
  seed, tick, present layers, and per-layer ``rng_state`` presence)
  without instantiating a ``Simulation``.  Useful as a cheap sanity
  check before loading.
* ``delete`` — refuses to run without ``--yes``; with ``--yes`` it
  removes the file.  Files outside the allow-listed extensions
  (``.json`` / ``.sim``) are never touched.
* ``export`` — re-emits a save in ``--format json`` (minified, no
  whitespace) or ``--format protobuf`` (feature-gated: prints a
  friendly stderr error and exits non-zero when the ``protobuf``
  runtime is missing).


