privatepanda.co

projects · flagship

Meddler in development

solo · pure Python engine · WebSocket bridge · browser client

A world simulator where the useful question is not only what happened, but why—and what changes if one cause is replaced.

How it got here

The repository is older than the current project. It began in 2023 under the name Veritasim, attached to a much larger multiplayer-world idea. That version was mostly ambition: a hosted world, many users, and far too many systems before there was a clear reason to watch any of them. It sat dormant.

The restart in July 2026 was a scope reset. Multiplayer, hosted deployment, a database, and generated prose were made explicit non-goals. The thing worth keeping was the world itself; the part that made it legible was causality. A country should not merely enter a recession. A shortage should reduce output, which should empty a treasury, which should cancel an infrastructure project, which might change a war. Each step needed to be inspectable.

The first restart still imagined a terminal UI. That lasted about one proposal revision. A causal graph, parallel timelines, a globe, inspectors, and direct interventions needed more spatial freedom, so the interface moved to a browser and the engine stayed separate behind a small WebSocket contract. The proposal then kept expanding: fixed countries became generated countries, one economic number became six commodities, instant trade became shipments, and infrastructure started affecting what could actually move through the world.

What it became

interventionexpectedcaused

The engine advances in daily ticks. Countries produce and consume commodities, trade across distance, move shipments, maintain assets, elect or depose leaders, and fight. The browser turns the resulting event log into a newspaper feed, a globe, dossiers, and a causal trace. Rewind reconstructs state from snapshots and recorded deltas. Forking copies the parent at a chosen tick; an intervention then changes one branch while the untouched future remains available for comparison.

The separation is deliberate but not elaborate: meddler.engine owns deterministic simulation, meddler.bridge translates state and commands across the frontend contract, and web/ owns presentation. Import-linter enforces that direction so browser concerns cannot leak back into the simulation.

The parts that kept changing

Determinism became a feature, not test plumbing

At first, reproducibility was necessary so debugging was possible. Fork comparison made it part of the product. A zero-intervention fork must follow its parent exactly, so a fork copies the parent RNG state instead of being reseeded. Stable substreams use SHA-256 rather than Python’s process-salted hash(). A golden event log pins a 1,000-tick run.

The economy stopped being one function

Production led to inventories; inventories led to bilateral trade; trade led to routes, in-flight shipments, arrivals, and losses; those led to ports, rail, power, communications, and other assets that could constrain the flow. Most of the recent work has been this kind of addition: a new feature forcing an older shortcut to become an explicit system.

Events had to own every mutation

Silent state changes make a causal UI dishonest. Money moves through transfer, mint, and burn operations in integer minor units. Other changes are recorded as stat deltas owned by events. Event kinds moved from a closed switch to a declarative registry because interventions and cascades were growing faster than the loop should.

The frontend briefly had two engines

The browser still contains a mock engine used while the real bridge was being built. It helped the UI move independently, but it also created a point where two implementations could disagree. The real Python engine is now authoritative; the remaining work is making the long-running real-engine path boring enough to trust.

Current map

Where it is now

The tests are less about individual output values than invariants: identical seeds produce identical logs; an untouched fork has zero diff; ledgers conserve each currency unless an event explicitly mints or burns; bridge messages survive a real WebSocket round trip; and the layer boundary remains intact. Unit, property, golden, and bridge tests cover the engine from different directions.

The v1 plan is implemented and the suite passes, but that is not the same as a beta. Not every planned system exists, and some that do are thinner than the interface suggests. Five of the god-mode palette interventions (war, peace, alliance, embargo, secession) currently apply stat pressure to the targeted countries rather than changing the world’s structure; making them structural is open work. Adopting a fork and then scrubbing back past the branch point is still broken, because a fork does not yet inherit its parent’s earlier snapshots. Long runs show stability collapsing and countries being lost after a few thousand ticks, and the 30-seed acceptance run from the spec has not been done.

What remains before a beta is mostly unglamorous: write down the beta criteria, run and record a multi-hour soak, fix or pull the stat-only interventions, do a full browser pass, and record a real demo.

Python · deterministic event log · WebSockets · HTML/JS · pytest · Hypothesis · mypy · import-linter

back to selected projects →