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:
Simulationclass 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 viaLife.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_statestill load; new saves includepopulations: 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 SPECIESplacement, save/load round-trip, and anargparse --helppage - 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.tomlexposesecopoesis = "ecopoesis.cli:main"sopip install -e .installs an entry point;ecopoesis/__init__.pyre-exportsmainfor library callers - Persistence gap closed:
SimulationState.to_dict()now emits the"life"envelope wheneverlife_ticks > 0(not only when populations exist), so save → load round-trips preservelife_tickscorrectly. Backward-compat is preserved — old Slice-2/3 saves without"life"still load withLifeState()defaults.
Slice 6 (resource layer + environment controls):
- Resources:
ResourceStatewith per-cell int arrays for minerals, water, energy, plusresource_ticks.Resourcesengine seeded deterministically atseed_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)withapply(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_dictRNG-skip block now importsBOUNDSfrom.resourcesso 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 emitsminerals=X water=Y energy=Zand 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:
GeologyStatetrackstectonic_events_count,eruption_events_count, and the liverng_state.GeologyEngineis seeded deterministically atseed_int + 4; per tick it draws two unconditional Bernoulli values againsttectonic_freq/eruption_freqplus 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
+10minerals at a random cell, clamped to[0, MAX_RESOURCE]. - RNG state in snapshot:
SimulationStatecarriesgeology_state.rng_state(the engine'srandom.Randomgetstate()tuple), sofrom_dictrestores viasetstaterather 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()appendstectonic_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 withGeologyState()defaults; new saves round-trip the live RNG state so save → load → advance is byte-identical to no-save advance.
Key Features
Core Components
-
src/ecopoesis/- Main package with:Simulationclass for deterministic simulation (with terrain and climate layers)SimulationStatedataclass with optional terrain/climate state fieldsTerrain/Climateengine classes (seeded PRNG, tick-based evolution)save_simulation()/load_simulation()file persistence helpers- Deterministic random number generation based on seed+tick
-
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:
- Simulation initialization with seed
- Tick advancement functionality
- Deterministic behavior for same seed + same operations
- Different behavior for different seeds
- Serialization and deserialization of state
- Round-trip save/load operations
- File-based save/load persistence and version handling
- Terrain elevation bounds [0, 255] after many ticks
- Climate temperature [-500, 500] and precipitation [0, 255] invariants
- 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/.simfile in--diraspath | size | tick | seed, sorted by path. An empty directory prints(no saves found)and exits0.info— prints the file's metadata header (schema, format, version, seed, tick, present layers, and per-layerrng_statepresence) without instantiating aSimulation. Useful as a cheap sanity check before loading.delete— refuses to run without--yes; with--yesit 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 theprotobufruntime is missing).
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
775380f9c3ebc010f3c6836de1ab143aec7d2d6680db840a1731e4135251bc48
|
|
| MD5 |
eb05eb880de9d2cb7660982e0d894c6b
|
|
| BLAKE2b-256 |
ec97527dd2d1a2e1d476e711116d2704edaaacc99cebb615ed0b4d698929c7dd
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7cf5be8af67ac1192dcfc8ddd274a63ac71e4e6d9a427ebf5572cdd5632b2afa
|
|
| MD5 |
74262e48498f9eae2c44ef3620b5eead
|
|
| BLAKE2b-256 |
6bb99e7ff4d0f8d9cdec8ecce802282f7a1ee9ae668feb4913eec9e2a6ee4a76
|