foliot
foliot is a small, deterministic, transactional tick engine for persistent
simulations.
It gives you a durable action queue, scheduled and recurring work, reproducible randomness, external action admission, suspension and resumption, atomic ticks, real-time pacing, and an optional layer for simultaneous multi-entity events. It deliberately does not define a domain model or its rules. Applications can use it for games, agent-based models, virtual worlds, economies, ecosystems, logistics, and other stateful simulations while owning all domain state and behavior.
Status: pre-alpha. The core is tested and usable, but the public API may still change before 1.0.
Why foliot?
- Reproducible: the same world seed and inputs produce the same history.
- Order-independent: unrelated actions have independent random streams.
- Transactional: queue changes, effects, journal lines, and the tick commit through one store transaction.
- Fast-forwardable: use logical time to run millions of ticks without sleeping.
- Domain-agnostic: your application owns its state model and rules.
- Storage-agnostic: implement two small protocols for PostgreSQL, MySQL, MariaDB, a file, or another backend.
- Dependency-free: the installed library uses only the Python standard library.
Installation
pip install foliot
Python 3.12 or newer is required.
Five-minute example
from dataclasses import dataclass
from typing import override
from foliot import BaseAction, EntityId, ManualDriver, MemoryStore, Simulation, TickContext
@dataclass(slots=True)
class World:
energy: int = 100
@dataclass(frozen=True, slots=True)
class LoseEnergy:
amount: int
def apply(self, world: World, /) -> None:
world.energy -= self.amount
class Hunger(BaseAction[World]):
def __init__(self, entity_id: EntityId) -> None:
super().__init__(entity_id, suspendable=False)
@override
def process(self, ctx: TickContext[World], /) -> None:
ctx.emit(LoseEnergy(1))
ctx.log("Lira grows hungry.")
world = World()
store = MemoryStore(
world,
world_seed=1,
initial_actions=((Hunger(EntityId("lira")), None),),
)
simulation = Simulation(store)
simulation.run(ManualDriver(until_tick=9))
assert world.energy == 90
assert len(store.logs) == 10
due_tick=None makes Hunger recurring, so it runs once per logical tick.
Effects and journal lines are collected first and applied only after every due
action has made its decision.
You can also admit an action from outside tick processing. expected_tick
names the state boundary from which the application made its decision:
observed_tick = simulation.tick
new_action = Hunger(EntityId("visitor"))
receipt = simulation.submit(new_action, observed_tick, expected_tick=observed_tick)
assert receipt.seq == new_action.seq
The submission does not advance time. If the world has advanced since
observed_tick, it raises StaleSubmissionError without binding the action.
The model
An action begins unbound. Its first successful store admission assigns one permanent sequence number and an active state. Scheduling the same object again changes its deadline without changing that identity.
| Request | Meaning |
|---|---|
ctx.schedule(action, 100) |
Run at tick 100. |
ctx.schedule(action, None) |
Run every tick until ctx.finish(). |
ctx.emit(effect) |
Apply an application-defined state change after decisions. |
ctx.log(line) |
Append one deterministic journal line. |
ctx.suspend(entity_id, by=handle) |
Pause that entity's suspendable actions. |
ctx.finish() |
Remove the current action. |
Concrete deadlines must always be later than the current tick. An action due at tick 90 can reschedule itself for tick 100; it keeps the same sequence number. External submission may be due at the current unfinished tick.
Optional simultaneous Events
Ordinary actions are enough for walking, hunger, poison, construction, growth,
and most other simulation work. Import foliot.events only when several
entities must decide from the same tick-start state before any result applies.
from foliot import Simulation
from foliot.events import EventMemoryStore, Events
store = EventMemoryStore(world, world_seed=1)
simulation = Simulation(store, events=Events(store))
Each EventAction returns one application-defined Intent. Once every expected
participant has answered in the same tick, the concrete BaseEvent resolves
the complete set into either Outcome.continue_with(...) or Outcome.end(...).
Domain formulas and lifecycle rules remain application code.
Run the complete example:
uv run python -m examples.eventworld
Storage
MemoryStore and EventMemoryStore are ready-made implementations for tests,
examples, and temporary simulations. They disappear with the process.
A durable application implements Store and Txn (plus EventStore and
EventTxn when Events are enabled). Foliot decides when the transaction
begins and commits; your adapter decides how actions and world state are
encoded in its database.
See Writing a store for the complete contract and a PostgreSQL-shaped example.
Documentation
- Documentation index
- Quickstart
- Actions and effects
- Simultaneous Events
- Writing a durable store
- Determinism and randomness
- Manual and real-time drivers
- Architecture
- Public API map
- Contributing
Development
uv sync
uv run ruff format --check src tests examples
uv run ruff check src tests examples
uv run basedpyright
uv run pytest
License
MIT
Release files for foliot 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| foliot-0.2.0.tar.gz | 69.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| foliot-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 107.9 kB
Release files / foliot-0.2.0.tar.gz
| Download URL | foliot-0.2.0.tar.gz |
|---|---|
| Size | 69.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
141374f97e44e6b24063f2c0214783fd58d99861bd3ac6a931c2fc9cb7c760a7
|
|
BLAKE2b-256 checksum How to use checksums |
85d4e7e1ac862ea2c82099fa082a5577800f05431de47a7f998aa16c09020cf5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.
Transparency logRelease files / foliot-0.2.0-py3-none-any.whl
| Download URL | foliot-0.2.0-py3-none-any.whl |
|---|---|
| Size | 38.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e2d143dd0329236b8d977f419c9069a7fcca4cf36a1045ba890b2463c6196d67
|
|
BLAKE2b-256 checksum How to use checksums |
0dc2968062b1d1f78af177f8897fd5b9925e446788e821cef55faa6cbad57bbd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.
Transparency log