Skip to main content

Ecopoesis: The Planetary Simulator — a deterministic terminal-driven planet simulator.

Project description

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

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

Or for development:

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:

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. 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 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.

# 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.

__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.

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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ecopoesis-0.2.0.tar.gz (162.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ecopoesis-0.2.0-py3-none-any.whl (116.9 kB view details)

Uploaded Python 3

File details

Details for the file ecopoesis-0.2.0.tar.gz.

File metadata

  • Download URL: ecopoesis-0.2.0.tar.gz
  • Upload date:
  • Size: 162.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for ecopoesis-0.2.0.tar.gz
Algorithm Hash digest
SHA256 775380f9c3ebc010f3c6836de1ab143aec7d2d6680db840a1731e4135251bc48
MD5 eb05eb880de9d2cb7660982e0d894c6b
BLAKE2b-256 ec97527dd2d1a2e1d476e711116d2704edaaacc99cebb615ed0b4d698929c7dd

See more details on using hashes here.

File details

Details for the file ecopoesis-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: ecopoesis-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 116.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for ecopoesis-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7cf5be8af67ac1192dcfc8ddd274a63ac71e4e6d9a427ebf5572cdd5632b2afa
MD5 74262e48498f9eae2c44ef3620b5eead
BLAKE2b-256 6bb99e7ff4d0f8d9cdec8ecce802282f7a1ee9ae668feb4913eec9e2a6ee4a76

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page