ecopoesis 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. ecopoesis-0.1.0/PKG-INFO +266 -0
  2. ecopoesis-0.1.0/README.md +239 -0
  3. ecopoesis-0.1.0/pyproject.toml +78 -0
  4. ecopoesis-0.1.0/setup.cfg +4 -0
  5. ecopoesis-0.1.0/src/ecopoesis/__init__.py +71 -0
  6. ecopoesis-0.1.0/src/ecopoesis/__version__.py +9 -0
  7. ecopoesis-0.1.0/src/ecopoesis/_gen_pb2.py +37 -0
  8. ecopoesis-0.1.0/src/ecopoesis/actions.py +547 -0
  9. ecopoesis-0.1.0/src/ecopoesis/cli.py +1338 -0
  10. ecopoesis-0.1.0/src/ecopoesis/climate.py +310 -0
  11. ecopoesis-0.1.0/src/ecopoesis/controls.py +102 -0
  12. ecopoesis-0.1.0/src/ecopoesis/geology.py +376 -0
  13. ecopoesis-0.1.0/src/ecopoesis/life.py +572 -0
  14. ecopoesis-0.1.0/src/ecopoesis/persistence.py +667 -0
  15. ecopoesis-0.1.0/src/ecopoesis/render.py +971 -0
  16. ecopoesis-0.1.0/src/ecopoesis/resources.py +316 -0
  17. ecopoesis-0.1.0/src/ecopoesis/simulation.py +484 -0
  18. ecopoesis-0.1.0/src/ecopoesis/simulation_pb2.py +36 -0
  19. ecopoesis-0.1.0/src/ecopoesis/state.py +194 -0
  20. ecopoesis-0.1.0/src/ecopoesis/terrain.py +209 -0
  21. ecopoesis-0.1.0/src/ecopoesis.egg-info/PKG-INFO +266 -0
  22. ecopoesis-0.1.0/src/ecopoesis.egg-info/SOURCES.txt +43 -0
  23. ecopoesis-0.1.0/src/ecopoesis.egg-info/dependency_links.txt +1 -0
  24. ecopoesis-0.1.0/src/ecopoesis.egg-info/entry_points.txt +2 -0
  25. ecopoesis-0.1.0/src/ecopoesis.egg-info/requires.txt +1 -0
  26. ecopoesis-0.1.0/src/ecopoesis.egg-info/top_level.txt +1 -0
  27. ecopoesis-0.1.0/tests/test_actions.py +205 -0
  28. ecopoesis-0.1.0/tests/test_cli_smoke.py +201 -0
  29. ecopoesis-0.1.0/tests/test_controls.py +139 -0
  30. ecopoesis-0.1.0/tests/test_diff.py +258 -0
  31. ecopoesis-0.1.0/tests/test_geology.py +279 -0
  32. ecopoesis-0.1.0/tests/test_install.py +42 -0
  33. ecopoesis-0.1.0/tests/test_life.py +362 -0
  34. ecopoesis-0.1.0/tests/test_performance.py +95 -0
  35. ecopoesis-0.1.0/tests/test_persistence.py +690 -0
  36. ecopoesis-0.1.0/tests/test_population_determinism.py +220 -0
  37. ecopoesis-0.1.0/tests/test_property_roundtrips.py +193 -0
  38. ecopoesis-0.1.0/tests/test_render.py +709 -0
  39. ecopoesis-0.1.0/tests/test_resources.py +153 -0
  40. ecopoesis-0.1.0/tests/test_save_load.py +235 -0
  41. ecopoesis-0.1.0/tests/test_save_management.py +330 -0
  42. ecopoesis-0.1.0/tests/test_simulation.py +82 -0
  43. ecopoesis-0.1.0/tests/test_simulation_tick.py +60 -0
  44. ecopoesis-0.1.0/tests/test_terrain_climate.py +303 -0
  45. ecopoesis-0.1.0/tests/test_watch.py +462 -0
@@ -0,0 +1,266 @@
1
+ Metadata-Version: 2.4
2
+ Name: ecopoesis
3
+ Version: 0.1.0
4
+ Summary: Ecopoesis: The Planetary Simulator — a deterministic terminal-driven planet simulator.
5
+ Author-email: Ecopoesis Engineering <engineering@ecopoesis.dev>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/ecopoesis/ecopoesis
8
+ Project-URL: Repository, https://github.com/ecopoesis/ecopoesis.git
9
+ Project-URL: Issues, https://github.com/ecopoesis/ecopoesis/issues
10
+ Project-URL: Bug Tracker, https://github.com/ecopoesis/ecopoesis/issues
11
+ Keywords: ecopoesis,simulation,terraforming,procedural,deterministic,game,planetary,earth
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: Implementation :: CPython
21
+ Classifier: Topic :: Games/Entertainment :: Simulation
22
+ Classifier: Topic :: Scientific/Engineering :: Hydrology
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.11
25
+ Description-Content-Type: text/markdown
26
+ Requires-Dist: protobuf<7,>=6.33.5
27
+
28
+ # Ecopoesis
29
+
30
+ Project workspace for the modern Ecopoesis remake.
31
+
32
+ ## Status
33
+ 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.
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install ecopoesis
39
+ ecopoesis run --seed demo --ticks 5
40
+ ```
41
+
42
+ Or for development:
43
+
44
+ ```bash
45
+ git clone https://github.com/ecopoesis/ecopoesis.git
46
+ cd ecopoesis
47
+ uv sync
48
+ uv run ecopoesis run --seed demo --ticks 5
49
+ ```
50
+
51
+ 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.
52
+
53
+ ## Visualising the world
54
+
55
+ Run the renderer against any save file to see the simulation as a grid of ASCII glyphs. Example:
56
+
57
+ ```bash
58
+ python -m ecopoesis.cli run --seed demo --ticks 50 --out save.json
59
+ python -m ecopoesis.cli render --in save.json --layer terrain
60
+ ```
61
+
62
+ 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`.
63
+
64
+ ## Slice 7 Refactor: Unified RNG-state pattern
65
+
66
+ All four RNG-driven layers (Climate, Life, Resources, Geology) now share the same
67
+ save/load pattern: each engine's `get_state()` captures its live `rng_state`
68
+ (via `random.Random.getstate()`) into the dataclass, and `restore_state()`
69
+ restores via `rng.setstate(...)`. This eliminates the count-based replay
70
+ pattern that caused the pre-Slice-7 climate RNG bug (5 determinism tests were
71
+ skipped; they now pass with the refactor in place). Legacy saves without
72
+ `rng_state` still load — the engines just start fresh from their constructed
73
+ RNG.
74
+
75
+ ## What's Implemented (Slice 1-4)
76
+
77
+ Slices 1-2 (core simulation shell):
78
+ - **Project Scaffold**: Complete Python package structure
79
+ - **Deterministic Simulation**: `Simulation` class initialized from a seed
80
+ - **Fixed Tick Progression**: Simulation advances ticks deterministically
81
+ - **Serializable State**: Simulation state can be saved and loaded as JSON
82
+ - **File-based Persistence**: Save/load helpers write and read versioned save files
83
+ - **Determinism Verification**: Tests showing same seed + same operations produce identical results
84
+ - **Different Seed Behavior**: Different seeds produce different initial states
85
+
86
+ Slice 3 (terrain + climate):
87
+ - **Terrain Layer**: Heightmap grid (default 8x8, elevation values 0-255) with deterministic generation from seed and per-tick diffusion-based erosion
88
+ - **Climate Layer**: Temperature and precipitation grids (same dimensions), with per-tick thermal diffusion plus bounded random perturbation
89
+ - **Fixed Update Order**: Core LCG → Terrain erosion → Climate diffusion → Snapshot
90
+ - **Backward-Compatible Serialization**: Existing saves without terrain/climate data still load; new saves include the grid data
91
+
92
+ Slice 4 (life + population dynamics):
93
+ - **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)
94
+ - **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
95
+ - **Carrying Capacity**: per-cell K derived from local terrain + climate; clamp to `>= 0` (no negative populations, verified across 500 adversarial ticks)
96
+ - **Wired into Simulation**: life step runs after climate in the fixed tick order (core LCG → terrain erosion → climate diffusion → life step → snapshot)
97
+ - **Backward-Compatible Saves**: Slice-3 saves without `life_state` still load; new saves include `populations: dict[str, list[int]]` + `life_ticks`
98
+
99
+ Slice 5 (CLI front-end):
100
+ - **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
101
+ - **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
102
+ - **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
103
+ - **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
104
+ - **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.
105
+
106
+ Slice 6 (resource layer + environment controls):
107
+ - **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.
108
+ - **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.
109
+ - **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.
110
+ - **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.
111
+ - **Backward-compatible saves**: Slice-2/3/4 saves without `"resources"` or `"controls"` keys still load with defaults; new saves round-trip both layers faithfully.
112
+
113
+ Slice 7 (geology: tectonic shifts + volcanic eruptions):
114
+ - **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).
115
+ - **Tectonic shifts**: when triggered, picks a random cell and applies a small signed int delta, clamped to terrain bounds `[0, 255]`.
116
+ - **Volcanic eruptions**: when triggered, deposits `+10` minerals at a random cell, clamped to `[0, MAX_RESOURCE]`.
117
+ - **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.
118
+ - **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.
119
+ - **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.
120
+
121
+ ## Key Features
122
+
123
+ ### Core Components
124
+ 1. `src/ecopoesis/` - Main package with:
125
+ - `Simulation` class for deterministic simulation (with terrain and climate layers)
126
+ - `SimulationState` dataclass with optional terrain/climate state fields
127
+ - `Terrain` / `Climate` engine classes (seeded PRNG, tick-based evolution)
128
+ - `save_simulation()` / `load_simulation()` file persistence helpers
129
+ - Deterministic random number generation based on seed+tick
130
+
131
+ 2. `tests/` - Test suite with:
132
+ - Basic tick functionality tests
133
+ - Terrain and climate determinism (same-seed identity, different-seed divergence)
134
+ - Tick order equivalence (`advance(N)` == `advance(1)` * N times)
135
+ - Bounds/invariant checks after many ticks
136
+ - Serialization round-trip including terrain/climate data
137
+ - Backward compatibility for saves without terrain/climate fields
138
+
139
+ ### Determinism Guarantees
140
+ - Same seeds + same tick sequences = identical state at all times (including grids)
141
+ - Different seeds = different initial deterministic behavior
142
+ - State can be saved and loaded without loss of determinism
143
+ - Fixed tick progression ensures consistent updates
144
+ - Terrain elevation values bounded to [0, 255], temperature to [-500, 500], precipitation to [0, 255]
145
+
146
+ ## Verification
147
+
148
+ The implementation passes:
149
+ 1. Simulation initialization with seed
150
+ 2. Tick advancement functionality
151
+ 3. Deterministic behavior for same seed + same operations
152
+ 4. Different behavior for different seeds
153
+ 5. Serialization and deserialization of state
154
+ 6. Round-trip save/load operations
155
+ 7. File-based save/load persistence and version handling
156
+ 8. Terrain elevation bounds [0, 255] after many ticks
157
+ 9. Climate temperature [-500, 500] and precipitation [0, 255] invariants
158
+ 10. Terrain/climate determinism across independent Simulation instances
159
+
160
+ ## Getting Started
161
+
162
+ The canonical 60-second walkthrough lives in
163
+ [`docs/GETTING_STARTED.md`](docs/GETTING_STARTED.md). It captures the
164
+ full seed → run → save → render → info → saves → load → replay → info
165
+ loop in a byte-equal transcript, and the companion
166
+ [`scripts/demo.py`](scripts/demo.py) reproduces it on any host
167
+ (`uv run python scripts/demo.py`).
168
+
169
+ If you `pip install -e .` from the repo root, a `ecopoesis` console
170
+ script is exposed (via the `[project.scripts]` entry in
171
+ `pyproject.toml`), so you can run `ecopoesis run --seed demo --ticks 5`
172
+ directly. The CLI also supports the `python -m ecopoesis.cli …` form.
173
+
174
+ The CLI has 9 subcommands: `run`, `save`, `load`, `replay`, `summary`,
175
+ `info`, `saves`, `delete`, `export`, and `render`. Each exits `0` on
176
+ success and prints a deterministic multi-line summary or ASCII grid
177
+ suitable for diffing.
178
+
179
+ ```bash
180
+ # Install in development mode
181
+ uv sync
182
+
183
+ # Run all tests
184
+ uv run python -m unittest discover -s tests
185
+
186
+ # Or run specific suites:
187
+ uv run python -m unittest tests.test_terrain_climate
188
+ uv run python -m unittest tests.test_simulation_tick
189
+
190
+ # Reproduce the canonical walkthrough
191
+ uv run python scripts/demo.py
192
+ ```
193
+
194
+ ## Public API
195
+
196
+ The complete public surface is `ecopoesis.__all__`. Anything not listed
197
+ here is internal and may change without notice.
198
+
199
+ <!-- generated from ecopoesis/__init__.py -->
200
+ ```
201
+ __version__
202
+ Simulation
203
+ SimulationState
204
+ SUPPORTED_VERSIONS
205
+ Terrain
206
+ TerrainState
207
+ ELEVATION_MIN
208
+ ELEVATION_MAX
209
+ Climate
210
+ ClimateState
211
+ TEMP_MIN
212
+ TEMP_MAX
213
+ PRECIP_MIN
214
+ PRECIP_MAX
215
+ Life
216
+ LifeState
217
+ Species
218
+ is_habitable
219
+ Resources
220
+ ResourceState
221
+ MAX_RESOURCE
222
+ EnvironmentControls
223
+ GeologyEngine
224
+ GeologyState
225
+ Format
226
+ CURRENT_SCHEMA
227
+ save_simulation
228
+ load_simulation
229
+ save_simulation_json
230
+ load_simulation_json
231
+ save_simulation_protobuf
232
+ load_simulation_protobuf
233
+ save_simulation_v1
234
+ load_simulation_v1
235
+ ```
236
+
237
+ ## Managing saves
238
+
239
+ Slice 8 adds four save-management subcommands so you can list, inspect,
240
+ delete, and re-export save files from the terminal without writing a
241
+ script. All four respect the same deterministic, traceback-free error
242
+ policy as the rest of the CLI.
243
+
244
+ ```bash
245
+ python -m ecopoesis.cli saves --dir ./out
246
+ python -m ecopoesis.cli info --in save.json
247
+ python -m ecopoesis.cli delete --in save.json --yes
248
+ python -m ecopoesis.cli export --in save.json --format json --out save.min.json
249
+ ```
250
+
251
+ * ``saves`` — lists every ``.json`` / ``.sim`` file in ``--dir`` as
252
+ ``path | size | tick | seed``, sorted by path. An empty directory
253
+ prints ``(no saves found)`` and exits ``0``.
254
+ * ``info`` — prints the file's metadata header (schema, format, version,
255
+ seed, tick, present layers, and per-layer ``rng_state`` presence)
256
+ without instantiating a ``Simulation``. Useful as a cheap sanity
257
+ check before loading.
258
+ * ``delete`` — refuses to run without ``--yes``; with ``--yes`` it
259
+ removes the file. Files outside the allow-listed extensions
260
+ (``.json`` / ``.sim``) are never touched.
261
+ * ``export`` — re-emits a save in ``--format json`` (minified, no
262
+ whitespace) or ``--format protobuf`` (feature-gated: prints a
263
+ friendly stderr error and exits non-zero when the ``protobuf``
264
+ runtime is missing).
265
+
266
+
@@ -0,0 +1,239 @@
1
+ # Ecopoesis
2
+
3
+ Project workspace for the modern Ecopoesis remake.
4
+
5
+ ## Status
6
+ 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.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ pip install ecopoesis
12
+ ecopoesis run --seed demo --ticks 5
13
+ ```
14
+
15
+ Or for development:
16
+
17
+ ```bash
18
+ git clone https://github.com/ecopoesis/ecopoesis.git
19
+ cd ecopoesis
20
+ uv sync
21
+ uv run ecopoesis run --seed demo --ticks 5
22
+ ```
23
+
24
+ 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.
25
+
26
+ ## Visualising the world
27
+
28
+ Run the renderer against any save file to see the simulation as a grid of ASCII glyphs. Example:
29
+
30
+ ```bash
31
+ python -m ecopoesis.cli run --seed demo --ticks 50 --out save.json
32
+ python -m ecopoesis.cli render --in save.json --layer terrain
33
+ ```
34
+
35
+ 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`.
36
+
37
+ ## Slice 7 Refactor: Unified RNG-state pattern
38
+
39
+ All four RNG-driven layers (Climate, Life, Resources, Geology) now share the same
40
+ save/load pattern: each engine's `get_state()` captures its live `rng_state`
41
+ (via `random.Random.getstate()`) into the dataclass, and `restore_state()`
42
+ restores via `rng.setstate(...)`. This eliminates the count-based replay
43
+ pattern that caused the pre-Slice-7 climate RNG bug (5 determinism tests were
44
+ skipped; they now pass with the refactor in place). Legacy saves without
45
+ `rng_state` still load — the engines just start fresh from their constructed
46
+ RNG.
47
+
48
+ ## What's Implemented (Slice 1-4)
49
+
50
+ Slices 1-2 (core simulation shell):
51
+ - **Project Scaffold**: Complete Python package structure
52
+ - **Deterministic Simulation**: `Simulation` class initialized from a seed
53
+ - **Fixed Tick Progression**: Simulation advances ticks deterministically
54
+ - **Serializable State**: Simulation state can be saved and loaded as JSON
55
+ - **File-based Persistence**: Save/load helpers write and read versioned save files
56
+ - **Determinism Verification**: Tests showing same seed + same operations produce identical results
57
+ - **Different Seed Behavior**: Different seeds produce different initial states
58
+
59
+ Slice 3 (terrain + climate):
60
+ - **Terrain Layer**: Heightmap grid (default 8x8, elevation values 0-255) with deterministic generation from seed and per-tick diffusion-based erosion
61
+ - **Climate Layer**: Temperature and precipitation grids (same dimensions), with per-tick thermal diffusion plus bounded random perturbation
62
+ - **Fixed Update Order**: Core LCG → Terrain erosion → Climate diffusion → Snapshot
63
+ - **Backward-Compatible Serialization**: Existing saves without terrain/climate data still load; new saves include the grid data
64
+
65
+ Slice 4 (life + population dynamics):
66
+ - **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)
67
+ - **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
68
+ - **Carrying Capacity**: per-cell K derived from local terrain + climate; clamp to `>= 0` (no negative populations, verified across 500 adversarial ticks)
69
+ - **Wired into Simulation**: life step runs after climate in the fixed tick order (core LCG → terrain erosion → climate diffusion → life step → snapshot)
70
+ - **Backward-Compatible Saves**: Slice-3 saves without `life_state` still load; new saves include `populations: dict[str, list[int]]` + `life_ticks`
71
+
72
+ Slice 5 (CLI front-end):
73
+ - **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
74
+ - **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
75
+ - **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
76
+ - **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
77
+ - **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.
78
+
79
+ Slice 6 (resource layer + environment controls):
80
+ - **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.
81
+ - **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.
82
+ - **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.
83
+ - **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.
84
+ - **Backward-compatible saves**: Slice-2/3/4 saves without `"resources"` or `"controls"` keys still load with defaults; new saves round-trip both layers faithfully.
85
+
86
+ Slice 7 (geology: tectonic shifts + volcanic eruptions):
87
+ - **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).
88
+ - **Tectonic shifts**: when triggered, picks a random cell and applies a small signed int delta, clamped to terrain bounds `[0, 255]`.
89
+ - **Volcanic eruptions**: when triggered, deposits `+10` minerals at a random cell, clamped to `[0, MAX_RESOURCE]`.
90
+ - **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.
91
+ - **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.
92
+ - **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.
93
+
94
+ ## Key Features
95
+
96
+ ### Core Components
97
+ 1. `src/ecopoesis/` - Main package with:
98
+ - `Simulation` class for deterministic simulation (with terrain and climate layers)
99
+ - `SimulationState` dataclass with optional terrain/climate state fields
100
+ - `Terrain` / `Climate` engine classes (seeded PRNG, tick-based evolution)
101
+ - `save_simulation()` / `load_simulation()` file persistence helpers
102
+ - Deterministic random number generation based on seed+tick
103
+
104
+ 2. `tests/` - Test suite with:
105
+ - Basic tick functionality tests
106
+ - Terrain and climate determinism (same-seed identity, different-seed divergence)
107
+ - Tick order equivalence (`advance(N)` == `advance(1)` * N times)
108
+ - Bounds/invariant checks after many ticks
109
+ - Serialization round-trip including terrain/climate data
110
+ - Backward compatibility for saves without terrain/climate fields
111
+
112
+ ### Determinism Guarantees
113
+ - Same seeds + same tick sequences = identical state at all times (including grids)
114
+ - Different seeds = different initial deterministic behavior
115
+ - State can be saved and loaded without loss of determinism
116
+ - Fixed tick progression ensures consistent updates
117
+ - Terrain elevation values bounded to [0, 255], temperature to [-500, 500], precipitation to [0, 255]
118
+
119
+ ## Verification
120
+
121
+ The implementation passes:
122
+ 1. Simulation initialization with seed
123
+ 2. Tick advancement functionality
124
+ 3. Deterministic behavior for same seed + same operations
125
+ 4. Different behavior for different seeds
126
+ 5. Serialization and deserialization of state
127
+ 6. Round-trip save/load operations
128
+ 7. File-based save/load persistence and version handling
129
+ 8. Terrain elevation bounds [0, 255] after many ticks
130
+ 9. Climate temperature [-500, 500] and precipitation [0, 255] invariants
131
+ 10. Terrain/climate determinism across independent Simulation instances
132
+
133
+ ## Getting Started
134
+
135
+ The canonical 60-second walkthrough lives in
136
+ [`docs/GETTING_STARTED.md`](docs/GETTING_STARTED.md). It captures the
137
+ full seed → run → save → render → info → saves → load → replay → info
138
+ loop in a byte-equal transcript, and the companion
139
+ [`scripts/demo.py`](scripts/demo.py) reproduces it on any host
140
+ (`uv run python scripts/demo.py`).
141
+
142
+ If you `pip install -e .` from the repo root, a `ecopoesis` console
143
+ script is exposed (via the `[project.scripts]` entry in
144
+ `pyproject.toml`), so you can run `ecopoesis run --seed demo --ticks 5`
145
+ directly. The CLI also supports the `python -m ecopoesis.cli …` form.
146
+
147
+ The CLI has 9 subcommands: `run`, `save`, `load`, `replay`, `summary`,
148
+ `info`, `saves`, `delete`, `export`, and `render`. Each exits `0` on
149
+ success and prints a deterministic multi-line summary or ASCII grid
150
+ suitable for diffing.
151
+
152
+ ```bash
153
+ # Install in development mode
154
+ uv sync
155
+
156
+ # Run all tests
157
+ uv run python -m unittest discover -s tests
158
+
159
+ # Or run specific suites:
160
+ uv run python -m unittest tests.test_terrain_climate
161
+ uv run python -m unittest tests.test_simulation_tick
162
+
163
+ # Reproduce the canonical walkthrough
164
+ uv run python scripts/demo.py
165
+ ```
166
+
167
+ ## Public API
168
+
169
+ The complete public surface is `ecopoesis.__all__`. Anything not listed
170
+ here is internal and may change without notice.
171
+
172
+ <!-- generated from ecopoesis/__init__.py -->
173
+ ```
174
+ __version__
175
+ Simulation
176
+ SimulationState
177
+ SUPPORTED_VERSIONS
178
+ Terrain
179
+ TerrainState
180
+ ELEVATION_MIN
181
+ ELEVATION_MAX
182
+ Climate
183
+ ClimateState
184
+ TEMP_MIN
185
+ TEMP_MAX
186
+ PRECIP_MIN
187
+ PRECIP_MAX
188
+ Life
189
+ LifeState
190
+ Species
191
+ is_habitable
192
+ Resources
193
+ ResourceState
194
+ MAX_RESOURCE
195
+ EnvironmentControls
196
+ GeologyEngine
197
+ GeologyState
198
+ Format
199
+ CURRENT_SCHEMA
200
+ save_simulation
201
+ load_simulation
202
+ save_simulation_json
203
+ load_simulation_json
204
+ save_simulation_protobuf
205
+ load_simulation_protobuf
206
+ save_simulation_v1
207
+ load_simulation_v1
208
+ ```
209
+
210
+ ## Managing saves
211
+
212
+ Slice 8 adds four save-management subcommands so you can list, inspect,
213
+ delete, and re-export save files from the terminal without writing a
214
+ script. All four respect the same deterministic, traceback-free error
215
+ policy as the rest of the CLI.
216
+
217
+ ```bash
218
+ python -m ecopoesis.cli saves --dir ./out
219
+ python -m ecopoesis.cli info --in save.json
220
+ python -m ecopoesis.cli delete --in save.json --yes
221
+ python -m ecopoesis.cli export --in save.json --format json --out save.min.json
222
+ ```
223
+
224
+ * ``saves`` — lists every ``.json`` / ``.sim`` file in ``--dir`` as
225
+ ``path | size | tick | seed``, sorted by path. An empty directory
226
+ prints ``(no saves found)`` and exits ``0``.
227
+ * ``info`` — prints the file's metadata header (schema, format, version,
228
+ seed, tick, present layers, and per-layer ``rng_state`` presence)
229
+ without instantiating a ``Simulation``. Useful as a cheap sanity
230
+ check before loading.
231
+ * ``delete`` — refuses to run without ``--yes``; with ``--yes`` it
232
+ removes the file. Files outside the allow-listed extensions
233
+ (``.json`` / ``.sim``) are never touched.
234
+ * ``export`` — re-emits a save in ``--format json`` (minified, no
235
+ whitespace) or ``--format protobuf`` (feature-gated: prints a
236
+ friendly stderr error and exits non-zero when the ``protobuf``
237
+ runtime is missing).
238
+
239
+
@@ -0,0 +1,78 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ecopoesis"
7
+ version = "0.1.0"
8
+ description = "Ecopoesis: The Planetary Simulator — a deterministic terminal-driven planet simulator."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.11"
12
+ authors = [
13
+ { name = "Ecopoesis Engineering", email = "engineering@ecopoesis.dev" },
14
+ ]
15
+ keywords = [
16
+ "ecopoesis",
17
+ "simulation",
18
+ "terraforming",
19
+ "procedural",
20
+ "deterministic",
21
+ "game",
22
+ "planetary",
23
+ "earth",
24
+ ]
25
+ classifiers = [
26
+ "Development Status :: 3 - Alpha",
27
+ "Intended Audience :: Developers",
28
+ "Intended Audience :: Science/Research",
29
+ "Operating System :: OS Independent",
30
+ "Programming Language :: Python :: 3",
31
+ "Programming Language :: Python :: 3.11",
32
+ "Programming Language :: Python :: 3.12",
33
+ "Programming Language :: Python :: 3.13",
34
+ "Programming Language :: Python :: Implementation :: CPython",
35
+ "Topic :: Games/Entertainment :: Simulation",
36
+ "Topic :: Scientific/Engineering :: Hydrology",
37
+ "Typing :: Typed",
38
+ ]
39
+ # Pinned runtime dependencies.
40
+ # `protobuf` is required for save_simulation_protobuf / load_simulation_protobuf
41
+ # (lazy-imported; >=6.33.5 needed for the generated simulation_pb2 module —
42
+ # the runtime MUST match or exceed the gencode version per protobuf's
43
+ # cross-version runtime guarantee).
44
+ # The generated simulation_pb2.py was built with gencode 6.33.5. Pin to
45
+ # the 6.x line to keep gencode/runtime in lock-step. If the generated
46
+ # module is regenerated, update the lower bound accordingly.
47
+ #
48
+ # The protobuf path serialises the FULL SimulationState.to_dict() payload
49
+ # (including per-layer rng_state snapshots) inside a ``state_json``
50
+ # ``bytes`` field, so protobuf round-trip is byte-identical to a no-save
51
+ # baseline. The legacy terrain_* / climate_* fields are kept populated
52
+ # for back-compat with saves produced before the state_json field was
53
+ # added; old saves load via legacy field reconstruction (without
54
+ # rng_state, no replay guarantee).
55
+ dependencies = [
56
+ "protobuf>=6.33.5,<7",
57
+ ]
58
+
59
+ [project.urls]
60
+ Homepage = "https://github.com/ecopoesis/ecopoesis"
61
+ Repository = "https://github.com/ecopoesis/ecopoesis.git"
62
+ Issues = "https://github.com/ecopoesis/ecopoesis/issues"
63
+ "Bug Tracker" = "https://github.com/ecopoesis/ecopoesis/issues"
64
+
65
+ [project.scripts]
66
+ ecopoesis = "ecopoesis.cli:main"
67
+
68
+ [tool.setuptools.package-dir]
69
+ "" = "src"
70
+
71
+ [tool.setuptools.packages.find]
72
+ where = ["src"]
73
+
74
+ [dependency-groups]
75
+ dev = [
76
+ "grpcio-tools>=1.81.1",
77
+ "hypothesis>=6.0",
78
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,71 @@
1
+ """SimEarth package exports."""
2
+
3
+ from .__version__ import __version__ # noqa: F401
4
+ from .cli import main # noqa: F401
5
+ from .controls import EnvironmentControls
6
+ from .geology import GeologyEngine, GeologyState
7
+ from .life import Life, LifeState, Species, is_habitable
8
+ from .resources import MAX_RESOURCE, ResourceState, Resources
9
+ from .simulation import Simulation
10
+ from .state import SUPPORTED_VERSIONS, SimulationState
11
+ from .terrain import Terrain, TerrainState, ELEVATION_MIN, ELEVATION_MAX
12
+ from .climate import Climate, ClimateState, TEMP_MIN, TEMP_MAX, PRECIP_MIN, PRECIP_MAX
13
+ from .persistence import (
14
+ Format,
15
+ CURRENT_SCHEMA,
16
+ load_simulation,
17
+ load_simulation_json,
18
+ load_simulation_protobuf,
19
+ load_simulation_v1,
20
+ save_simulation,
21
+ save_simulation_json,
22
+ save_simulation_protobuf,
23
+ save_simulation_v1,
24
+ )
25
+
26
+ __all__ = [
27
+ # Package metadata (Slice 11)
28
+ "__version__",
29
+ # Simulation core
30
+ "Simulation",
31
+ "SimulationState",
32
+ "SUPPORTED_VERSIONS",
33
+ # Terrain layer (Slice 3)
34
+ "Terrain",
35
+ "TerrainState",
36
+ "ELEVATION_MIN",
37
+ "ELEVATION_MAX",
38
+ # Climate layer (Slice 3)
39
+ "Climate",
40
+ "ClimateState",
41
+ "TEMP_MIN",
42
+ "TEMP_MAX",
43
+ "PRECIP_MIN",
44
+ "PRECIP_MAX",
45
+ # Life layer (Slice 4)
46
+ "Life",
47
+ "LifeState",
48
+ "Species",
49
+ "is_habitable",
50
+ # Resources layer (Slice 6)
51
+ "Resources",
52
+ "ResourceState",
53
+ "MAX_RESOURCE",
54
+ # Environment controls (Slice 6)
55
+ "EnvironmentControls",
56
+ # Geology layer (Slice 7)
57
+ "GeologyEngine",
58
+ "GeologyState",
59
+ # Persistence
60
+ "Format",
61
+ "CURRENT_SCHEMA",
62
+ "save_simulation",
63
+ "load_simulation",
64
+ "save_simulation_json",
65
+ "load_simulation_json",
66
+ "save_simulation_protobuf",
67
+ "load_simulation_protobuf",
68
+ "save_simulation_v1",
69
+ "load_simulation_v1",
70
+ ]
71
+
@@ -0,0 +1,9 @@
1
+ """Single-source-of-truth version for the ecopoesis package.
2
+
3
+ This file is intentionally minimal so it can be imported from any context
4
+ (including build scripts and runtime) without pulling in heavier dependencies
5
+ like ``ecopoesis.simulation_pb2``. Both ``pyproject.toml`` and the public
6
+ ``ecopoesis.__version__`` re-export should stay in lock-step with this string.
7
+ """
8
+
9
+ __version__ = "0.1.0"