foliot 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 (48) hide show
  1. foliot-0.1.0/CHANGELOG.md +29 -0
  2. foliot-0.1.0/CONTRIBUTING.md +75 -0
  3. foliot-0.1.0/LICENSE +21 -0
  4. foliot-0.1.0/PKG-INFO +196 -0
  5. foliot-0.1.0/README.md +169 -0
  6. foliot-0.1.0/RELEASING.md +49 -0
  7. foliot-0.1.0/docs/actions.md +98 -0
  8. foliot-0.1.0/docs/api.md +42 -0
  9. foliot-0.1.0/docs/architecture.md +97 -0
  10. foliot-0.1.0/docs/determinism.md +58 -0
  11. foliot-0.1.0/docs/drivers.md +37 -0
  12. foliot-0.1.0/docs/events.md +181 -0
  13. foliot-0.1.0/docs/index.md +19 -0
  14. foliot-0.1.0/docs/quickstart.md +104 -0
  15. foliot-0.1.0/docs/stores.md +148 -0
  16. foliot-0.1.0/examples/__init__.py +1 -0
  17. foliot-0.1.0/examples/eventworld/README.md +30 -0
  18. foliot-0.1.0/examples/eventworld/__init__.py +453 -0
  19. foliot-0.1.0/examples/eventworld/__main__.py +31 -0
  20. foliot-0.1.0/examples/tinyworld/README.md +27 -0
  21. foliot-0.1.0/examples/tinyworld/__init__.py +346 -0
  22. foliot-0.1.0/examples/tinyworld/__main__.py +33 -0
  23. foliot-0.1.0/pyproject.toml +118 -0
  24. foliot-0.1.0/pyproject.toml.orig +91 -0
  25. foliot-0.1.0/src/foliot/__init__.py +61 -0
  26. foliot-0.1.0/src/foliot/_event_bridge.py +7 -0
  27. foliot-0.1.0/src/foliot/actions.py +275 -0
  28. foliot-0.1.0/src/foliot/context.py +160 -0
  29. foliot-0.1.0/src/foliot/drivers.py +172 -0
  30. foliot-0.1.0/src/foliot/effects.py +26 -0
  31. foliot-0.1.0/src/foliot/engine.py +565 -0
  32. foliot-0.1.0/src/foliot/events/__init__.py +43 -0
  33. foliot-0.1.0/src/foliot/events/_api.py +675 -0
  34. foliot-0.1.0/src/foliot/events/memory.py +218 -0
  35. foliot-0.1.0/src/foliot/ids.py +26 -0
  36. foliot-0.1.0/src/foliot/py.typed +0 -0
  37. foliot-0.1.0/src/foliot/rng.py +178 -0
  38. foliot-0.1.0/src/foliot/stores/__init__.py +133 -0
  39. foliot-0.1.0/src/foliot/stores/memory.py +493 -0
  40. foliot-0.1.0/tests/test_actions.py +85 -0
  41. foliot-0.1.0/tests/test_drivers.py +166 -0
  42. foliot-0.1.0/tests/test_engine.py +413 -0
  43. foliot-0.1.0/tests/test_events.py +820 -0
  44. foliot-0.1.0/tests/test_eventworld.py +33 -0
  45. foliot-0.1.0/tests/test_memory_store.py +352 -0
  46. foliot-0.1.0/tests/test_rng.py +136 -0
  47. foliot-0.1.0/tests/test_tinyworld.py +28 -0
  48. foliot-0.1.0/uv.lock +138 -0
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ All notable changes to foliot will be documented here.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and releases use [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-09-05
11
+
12
+ ### Added
13
+
14
+ - Mandatory `BaseAction` lifecycle with stable admission identity.
15
+ - Scheduled and recurring actions.
16
+ - Suspension, exact deadline shifting, and the `on_resume` hook.
17
+ - Counter-based deterministic random streams and secure world-seed creation.
18
+ - Structural `Store` and `Txn` protocols.
19
+ - Dependency-free `MemoryStore` reference implementation.
20
+ - Atomic deterministic `Simulation` tick processing.
21
+ - Inclusive `ManualDriver` and drift-resistant `RealtimeDriver`.
22
+ - Optional post-effect `TickFinalizer` and owner-wide action deletion.
23
+ - Optional `foliot.events` layer for simultaneous participant Intents.
24
+ - `EventMemoryStore`, stable Event/entity id templates, and explicit Event
25
+ continuation and ending.
26
+ - Runnable Layer-1 Tinyworld and Layer-2 Eventworld examples.
27
+
28
+ [Unreleased]: https://github.com/wordedword75049/foliot/compare/v0.1.0...HEAD
29
+ [0.1.0]: https://github.com/wordedword75049/foliot/releases/tag/v0.1.0
@@ -0,0 +1,75 @@
1
+ # Contributing to foliot
2
+
3
+ Thank you for helping improve foliot. The project favors small, explicit
4
+ changes that preserve deterministic behavior and keep game concepts outside
5
+ the engine.
6
+
7
+ ## Setup
8
+
9
+ Install [uv](https://docs.astral.sh/uv/), then run:
10
+
11
+ ```console
12
+ uv sync
13
+ ```
14
+
15
+ The project supports Python 3.12 and newer. Runtime dependencies must remain
16
+ empty unless a new dependency has been discussed and justified first.
17
+
18
+ ## Required checks
19
+
20
+ ```console
21
+ uv run ruff format --check src tests examples
22
+ uv run ruff check src tests examples
23
+ uv run basedpyright
24
+ uv run pytest
25
+ ```
26
+
27
+ Use `uv run ruff format` to apply formatting.
28
+
29
+ ## Design rules
30
+
31
+ - Every queued game action inherits `BaseAction`.
32
+ - External ports such as stores, effects, contexts, RNGs, and drivers use
33
+ structural `Protocol` typing.
34
+ - Keep public contexts narrow. Pass `ctx.rng` to a helper instead of passing
35
+ the whole context.
36
+ - Game nouns do not belong in foliot. The engine has no built-in HP, character,
37
+ target, location, or combat model.
38
+ - Use PEP 695 generic syntax; the supported floor is Python 3.12.
39
+ - Do not use `Any` in public signatures.
40
+ - Use exhaustive `match` statements for closed state unions; do not add a
41
+ catch-all branch that disables type-checker exhaustiveness.
42
+ - Use `logging.getLogger(__name__)`; library code must never call
43
+ `logging.basicConfig()` or print.
44
+ - Do not use Python's `random` module outside `rng.py`, `hash()` for persistent
45
+ string identity, or wall-clock time outside a driver.
46
+ - Keep the root and `foliot.events` export lists curated. A public import path
47
+ is a compatibility promise.
48
+
49
+ ## Tests
50
+
51
+ Tests use handwritten fakes rather than `unittest.mock`, `pytest-mock`,
52
+ monkeypatching production objects, or time-freezing libraries.
53
+
54
+ Important changes should prove the architectural property they affect:
55
+
56
+ - replay produces the same output from the same seed;
57
+ - reversing due-action order does not change results;
58
+ - suspension shifts deadlines by the exact pause duration;
59
+ - exceptional transaction exit publishes no foliot-owned state;
60
+ - an incomplete Event round retains exactly one copy of every current child;
61
+ - Layer 1 behaves identically when the optional Event layer is unused.
62
+
63
+ Test game actions directly with a tiny recording context whenever possible.
64
+ Use a full `Simulation` only when testing engine coordination.
65
+
66
+ ## Documentation
67
+
68
+ Public docstrings explain behavior, parameters, return values, and meaningful
69
+ exceptions. Examples should show normal usage. Internal comments should explain
70
+ why a non-obvious mechanism exists rather than narrating the next line of code.
71
+
72
+ Update the relevant guide whenever a public contract changes.
73
+
74
+ Maintainers should follow [RELEASING.md](RELEASING.md) for versioning, artifact
75
+ checks, and trusted publication.
foliot-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nikita Ivanin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
foliot-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,196 @@
1
+ Metadata-Version: 2.4
2
+ Name: foliot
3
+ Version: 0.1.0
4
+ Summary: A deterministic tick-driven simulation core: durable queue, absolute deadlines, per-entity randomness.
5
+ Keywords: simulation,scheduler,tick,deterministic,event-queue
6
+ Author: Nikita Ivanin
7
+ Author-email: Nikita Ivanin <naik.iva@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 2 - Pre-Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Games/Entertainment :: Simulation
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.12
21
+ Project-URL: Homepage, https://github.com/wordedword75049/foliot
22
+ Project-URL: Documentation, https://github.com/wordedword75049/foliot/blob/main/docs/index.md
23
+ Project-URL: Repository, https://github.com/wordedword75049/foliot.git
24
+ Project-URL: Issues, https://github.com/wordedword75049/foliot/issues
25
+ Project-URL: Changelog, https://github.com/wordedword75049/foliot/blob/main/CHANGELOG.md
26
+ Description-Content-Type: text/markdown
27
+
28
+ # foliot
29
+
30
+ `foliot` is a small, deterministic simulation engine for worlds that advance
31
+ in logical ticks.
32
+
33
+ It gives you a durable action queue, scheduled and recurring work, reproducible
34
+ randomness, suspension and resumption, atomic ticks, real-time pacing, and an
35
+ optional layer for simultaneous multi-entity events. It deliberately does not
36
+ define characters, maps, combat, health, inventories, or quests. Those belong
37
+ to your simulation.
38
+
39
+ > **Status:** pre-alpha. The core is tested and usable, but the public API may
40
+ > still change before 1.0.
41
+
42
+ ## Why foliot?
43
+
44
+ - **Reproducible:** the same world seed and inputs produce the same history.
45
+ - **Order-independent:** unrelated actions have independent random streams.
46
+ - **Transactional:** queue changes, effects, journal lines, and the tick commit
47
+ through one store transaction.
48
+ - **Fast-forwardable:** use logical time to run millions of ticks without
49
+ sleeping.
50
+ - **Domain-agnostic:** your game owns its world model and rules.
51
+ - **Storage-agnostic:** implement two small protocols for PostgreSQL, MySQL,
52
+ MariaDB, a file, or another backend.
53
+ - **Dependency-free:** the installed library uses only the Python standard
54
+ library.
55
+
56
+ ## Installation
57
+
58
+ ```console
59
+ pip install foliot
60
+ ```
61
+
62
+ Python 3.12 or newer is required.
63
+
64
+ ## Five-minute example
65
+
66
+ ```python
67
+ from dataclasses import dataclass
68
+ from typing import override
69
+
70
+ from foliot import BaseAction, EntityId, ManualDriver, MemoryStore, Simulation, TickContext
71
+
72
+
73
+ @dataclass(slots=True)
74
+ class World:
75
+ energy: int = 100
76
+
77
+
78
+ @dataclass(frozen=True, slots=True)
79
+ class LoseEnergy:
80
+ amount: int
81
+
82
+ def apply(self, world: World, /) -> None:
83
+ world.energy -= self.amount
84
+
85
+
86
+ class Hunger(BaseAction[World]):
87
+ def __init__(self, entity_id: EntityId) -> None:
88
+ super().__init__(entity_id, suspendable=False)
89
+
90
+ @override
91
+ def process(self, ctx: TickContext[World], /) -> None:
92
+ ctx.emit(LoseEnergy(1))
93
+ ctx.log("Lira grows hungry.")
94
+
95
+
96
+ world = World()
97
+ store = MemoryStore(
98
+ world,
99
+ world_seed=1,
100
+ initial_actions=((Hunger(EntityId("lira")), None),),
101
+ )
102
+
103
+ Simulation(store).run(ManualDriver(until_tick=9))
104
+
105
+ assert world.energy == 90
106
+ assert len(store.logs) == 10
107
+ ```
108
+
109
+ `due_tick=None` makes `Hunger` recurring, so it runs once per logical tick.
110
+ Effects and journal lines are collected first and applied only after every due
111
+ action has made its decision.
112
+
113
+ ## The model
114
+
115
+ An action begins **unbound**. Its first successful store admission assigns one
116
+ permanent sequence number and an active state. Scheduling the same object again
117
+ changes its deadline without changing that identity.
118
+
119
+ | Request | Meaning |
120
+ |---|---|
121
+ | `ctx.schedule(action, 100)` | Run at tick 100. |
122
+ | `ctx.schedule(action, None)` | Run every tick until `ctx.finish()`. |
123
+ | `ctx.emit(effect)` | Apply a game-defined world change after decisions. |
124
+ | `ctx.log(line)` | Append deterministic narrative output. |
125
+ | `ctx.suspend(entity_id, by=handle)` | Pause that entity's suspendable actions. |
126
+ | `ctx.finish()` | Remove the current action. |
127
+
128
+ Concrete deadlines must always be later than the current tick. An action due
129
+ at tick 90 can reschedule itself for tick 100; it keeps the same sequence
130
+ number.
131
+
132
+ ## Optional simultaneous Events
133
+
134
+ Ordinary actions are enough for walking, hunger, poison, construction, growth,
135
+ and most other simulation work. Import `foliot.events` only when several
136
+ entities must decide from the same tick-start state before any result applies.
137
+
138
+ ```python
139
+ from foliot import Simulation
140
+ from foliot.events import EventMemoryStore, Events
141
+
142
+ store = EventMemoryStore(world, world_seed=1)
143
+ simulation = Simulation(store, events=Events(store))
144
+ ```
145
+
146
+ Each `EventAction` returns one game-defined Intent. Once every expected
147
+ participant has answered in the same tick, the concrete `BaseEvent` resolves
148
+ the complete set into either `Outcome.continue_with(...)` or `Outcome.end(...)`.
149
+ Combat formulas and lifecycle rules remain game code.
150
+
151
+ Run the complete example:
152
+
153
+ ```console
154
+ uv run python -m examples.eventworld
155
+ ```
156
+
157
+ ## Storage
158
+
159
+ `MemoryStore` and `EventMemoryStore` are ready-made implementations for tests,
160
+ examples, and temporary simulations. They disappear with the process.
161
+
162
+ A durable application implements `Store` and `Txn` (plus `EventStore` and
163
+ `EventTxn` when Events are enabled). Foliot decides *when* the transaction
164
+ begins and commits; your adapter decides *how* actions and world state are
165
+ encoded in its database.
166
+
167
+ See [Writing a store](https://github.com/wordedword75049/foliot/blob/main/docs/stores.md)
168
+ for the complete contract and a
169
+ PostgreSQL-shaped example.
170
+
171
+ ## Documentation
172
+
173
+ - [Documentation index](https://github.com/wordedword75049/foliot/blob/main/docs/index.md)
174
+ - [Quickstart](https://github.com/wordedword75049/foliot/blob/main/docs/quickstart.md)
175
+ - [Actions and effects](https://github.com/wordedword75049/foliot/blob/main/docs/actions.md)
176
+ - [Simultaneous Events](https://github.com/wordedword75049/foliot/blob/main/docs/events.md)
177
+ - [Writing a durable store](https://github.com/wordedword75049/foliot/blob/main/docs/stores.md)
178
+ - [Determinism and randomness](https://github.com/wordedword75049/foliot/blob/main/docs/determinism.md)
179
+ - [Manual and real-time drivers](https://github.com/wordedword75049/foliot/blob/main/docs/drivers.md)
180
+ - [Architecture](https://github.com/wordedword75049/foliot/blob/main/docs/architecture.md)
181
+ - [Public API map](https://github.com/wordedword75049/foliot/blob/main/docs/api.md)
182
+ - [Contributing](https://github.com/wordedword75049/foliot/blob/main/CONTRIBUTING.md)
183
+
184
+ ## Development
185
+
186
+ ```console
187
+ uv sync
188
+ uv run ruff format --check src tests examples
189
+ uv run ruff check src tests examples
190
+ uv run basedpyright
191
+ uv run pytest
192
+ ```
193
+
194
+ ## License
195
+
196
+ MIT
foliot-0.1.0/README.md ADDED
@@ -0,0 +1,169 @@
1
+ # foliot
2
+
3
+ `foliot` is a small, deterministic simulation engine for worlds that advance
4
+ in logical ticks.
5
+
6
+ It gives you a durable action queue, scheduled and recurring work, reproducible
7
+ randomness, suspension and resumption, atomic ticks, real-time pacing, and an
8
+ optional layer for simultaneous multi-entity events. It deliberately does not
9
+ define characters, maps, combat, health, inventories, or quests. Those belong
10
+ to your simulation.
11
+
12
+ > **Status:** pre-alpha. The core is tested and usable, but the public API may
13
+ > still change before 1.0.
14
+
15
+ ## Why foliot?
16
+
17
+ - **Reproducible:** the same world seed and inputs produce the same history.
18
+ - **Order-independent:** unrelated actions have independent random streams.
19
+ - **Transactional:** queue changes, effects, journal lines, and the tick commit
20
+ through one store transaction.
21
+ - **Fast-forwardable:** use logical time to run millions of ticks without
22
+ sleeping.
23
+ - **Domain-agnostic:** your game owns its world model and rules.
24
+ - **Storage-agnostic:** implement two small protocols for PostgreSQL, MySQL,
25
+ MariaDB, a file, or another backend.
26
+ - **Dependency-free:** the installed library uses only the Python standard
27
+ library.
28
+
29
+ ## Installation
30
+
31
+ ```console
32
+ pip install foliot
33
+ ```
34
+
35
+ Python 3.12 or newer is required.
36
+
37
+ ## Five-minute example
38
+
39
+ ```python
40
+ from dataclasses import dataclass
41
+ from typing import override
42
+
43
+ from foliot import BaseAction, EntityId, ManualDriver, MemoryStore, Simulation, TickContext
44
+
45
+
46
+ @dataclass(slots=True)
47
+ class World:
48
+ energy: int = 100
49
+
50
+
51
+ @dataclass(frozen=True, slots=True)
52
+ class LoseEnergy:
53
+ amount: int
54
+
55
+ def apply(self, world: World, /) -> None:
56
+ world.energy -= self.amount
57
+
58
+
59
+ class Hunger(BaseAction[World]):
60
+ def __init__(self, entity_id: EntityId) -> None:
61
+ super().__init__(entity_id, suspendable=False)
62
+
63
+ @override
64
+ def process(self, ctx: TickContext[World], /) -> None:
65
+ ctx.emit(LoseEnergy(1))
66
+ ctx.log("Lira grows hungry.")
67
+
68
+
69
+ world = World()
70
+ store = MemoryStore(
71
+ world,
72
+ world_seed=1,
73
+ initial_actions=((Hunger(EntityId("lira")), None),),
74
+ )
75
+
76
+ Simulation(store).run(ManualDriver(until_tick=9))
77
+
78
+ assert world.energy == 90
79
+ assert len(store.logs) == 10
80
+ ```
81
+
82
+ `due_tick=None` makes `Hunger` recurring, so it runs once per logical tick.
83
+ Effects and journal lines are collected first and applied only after every due
84
+ action has made its decision.
85
+
86
+ ## The model
87
+
88
+ An action begins **unbound**. Its first successful store admission assigns one
89
+ permanent sequence number and an active state. Scheduling the same object again
90
+ changes its deadline without changing that identity.
91
+
92
+ | Request | Meaning |
93
+ |---|---|
94
+ | `ctx.schedule(action, 100)` | Run at tick 100. |
95
+ | `ctx.schedule(action, None)` | Run every tick until `ctx.finish()`. |
96
+ | `ctx.emit(effect)` | Apply a game-defined world change after decisions. |
97
+ | `ctx.log(line)` | Append deterministic narrative output. |
98
+ | `ctx.suspend(entity_id, by=handle)` | Pause that entity's suspendable actions. |
99
+ | `ctx.finish()` | Remove the current action. |
100
+
101
+ Concrete deadlines must always be later than the current tick. An action due
102
+ at tick 90 can reschedule itself for tick 100; it keeps the same sequence
103
+ number.
104
+
105
+ ## Optional simultaneous Events
106
+
107
+ Ordinary actions are enough for walking, hunger, poison, construction, growth,
108
+ and most other simulation work. Import `foliot.events` only when several
109
+ entities must decide from the same tick-start state before any result applies.
110
+
111
+ ```python
112
+ from foliot import Simulation
113
+ from foliot.events import EventMemoryStore, Events
114
+
115
+ store = EventMemoryStore(world, world_seed=1)
116
+ simulation = Simulation(store, events=Events(store))
117
+ ```
118
+
119
+ Each `EventAction` returns one game-defined Intent. Once every expected
120
+ participant has answered in the same tick, the concrete `BaseEvent` resolves
121
+ the complete set into either `Outcome.continue_with(...)` or `Outcome.end(...)`.
122
+ Combat formulas and lifecycle rules remain game code.
123
+
124
+ Run the complete example:
125
+
126
+ ```console
127
+ uv run python -m examples.eventworld
128
+ ```
129
+
130
+ ## Storage
131
+
132
+ `MemoryStore` and `EventMemoryStore` are ready-made implementations for tests,
133
+ examples, and temporary simulations. They disappear with the process.
134
+
135
+ A durable application implements `Store` and `Txn` (plus `EventStore` and
136
+ `EventTxn` when Events are enabled). Foliot decides *when* the transaction
137
+ begins and commits; your adapter decides *how* actions and world state are
138
+ encoded in its database.
139
+
140
+ See [Writing a store](https://github.com/wordedword75049/foliot/blob/main/docs/stores.md)
141
+ for the complete contract and a
142
+ PostgreSQL-shaped example.
143
+
144
+ ## Documentation
145
+
146
+ - [Documentation index](https://github.com/wordedword75049/foliot/blob/main/docs/index.md)
147
+ - [Quickstart](https://github.com/wordedword75049/foliot/blob/main/docs/quickstart.md)
148
+ - [Actions and effects](https://github.com/wordedword75049/foliot/blob/main/docs/actions.md)
149
+ - [Simultaneous Events](https://github.com/wordedword75049/foliot/blob/main/docs/events.md)
150
+ - [Writing a durable store](https://github.com/wordedword75049/foliot/blob/main/docs/stores.md)
151
+ - [Determinism and randomness](https://github.com/wordedword75049/foliot/blob/main/docs/determinism.md)
152
+ - [Manual and real-time drivers](https://github.com/wordedword75049/foliot/blob/main/docs/drivers.md)
153
+ - [Architecture](https://github.com/wordedword75049/foliot/blob/main/docs/architecture.md)
154
+ - [Public API map](https://github.com/wordedword75049/foliot/blob/main/docs/api.md)
155
+ - [Contributing](https://github.com/wordedword75049/foliot/blob/main/CONTRIBUTING.md)
156
+
157
+ ## Development
158
+
159
+ ```console
160
+ uv sync
161
+ uv run ruff format --check src tests examples
162
+ uv run ruff check src tests examples
163
+ uv run basedpyright
164
+ uv run pytest
165
+ ```
166
+
167
+ ## License
168
+
169
+ MIT
@@ -0,0 +1,49 @@
1
+ # Releasing foliot
2
+
3
+ Publishing is intentionally automated through GitHub Actions and PyPI Trusted
4
+ Publishing. No long-lived PyPI token belongs in the repository.
5
+
6
+ ## One-time PyPI setup
7
+
8
+ Before the first release, create a pending trusted publisher for the future
9
+ `foliot` project with these values:
10
+
11
+ - PyPI project name: `foliot`
12
+ - GitHub owner: `wordedword75049`
13
+ - GitHub repository: `foliot`
14
+ - Workflow filename: `release.yml`
15
+ - Environment name: `pypi`
16
+
17
+ Also create the `pypi` environment in the GitHub repository settings. See
18
+ [PyPI's Trusted Publishers guide](https://docs.pypi.org/trusted-publishers/).
19
+
20
+ ## Prepare a release
21
+
22
+ 1. Choose the version and update `project.version` in `pyproject.toml`.
23
+ 2. Move the release notes out of `Unreleased` in `CHANGELOG.md` and add the
24
+ release date.
25
+ 3. Run the complete checks:
26
+
27
+ ```console
28
+ uv sync --locked
29
+ uv run ruff format --check src tests examples
30
+ uv run ruff check src tests examples
31
+ uv run basedpyright
32
+ uv run pytest
33
+ uv build --no-sources
34
+ ```
35
+
36
+ 4. Inspect both files under `dist/` and install the wheel in a fresh virtual
37
+ environment as a smoke test.
38
+ 5. Commit the release preparation and push it to `main`.
39
+
40
+ ## Publish
41
+
42
+ Create and publish a GitHub release whose tag exactly matches the package
43
+ version with a `v` prefix, such as `v0.1.0`. Publishing the GitHub release
44
+ triggers `.github/workflows/release.yml`, which repeats all checks, builds the
45
+ artifacts, and sends them to PyPI through the trusted publisher.
46
+
47
+ PyPI versions are immutable. If publishing fails after a file was accepted,
48
+ fix the problem under a new version rather than trying to replace the file.
49
+
@@ -0,0 +1,98 @@
1
+ # Actions and effects
2
+
3
+ ## Action ownership
4
+
5
+ Every `BaseAction` has an `entity_id`: the entity that owns the work. Foliot
6
+ uses it for random-stream isolation, suspension, and owner-wide deletion. It
7
+ does not mean “target.” Directed simulations can add a mandatory `target_id`
8
+ in their own shared action base.
9
+
10
+ ## Admission and identity
11
+
12
+ A newly constructed action has `Unbound()` as its complete binding. The first
13
+ successful `Txn.schedule(...)` changes it to `Bound(seq, Active(due_tick))`.
14
+
15
+ `seq` is permanent. Rescheduling, suspending, and resuming replace the state
16
+ without changing the sequence number. A durable adapter must persist and
17
+ restore both the binding and every game-defined subclass field.
18
+
19
+ ## Scheduled and recurring actions
20
+
21
+ - `due_tick=50` means the action becomes due at logical tick 50.
22
+ - `due_tick=None` means the action is due on every tick.
23
+ - A deadline must be strictly later than the tick that schedules it.
24
+
25
+ A scheduled action disappears after it runs unless it reschedules itself. A
26
+ recurring action remains until it calls `ctx.finish()`.
27
+
28
+ ```python
29
+ @override
30
+ def process(self, ctx: TickContext[World], /) -> None:
31
+ ctx.emit(Damage(self.entity_id, 2))
32
+ next_strike = ctx.tick + self.interval
33
+ if next_strike < self.expires_at:
34
+ ctx.schedule(self, next_strike)
35
+ ```
36
+
37
+ The same poison object—and therefore the same `seq` and game payload—moves to
38
+ the new deadline.
39
+
40
+ ## Collect first, apply later
41
+
42
+ Each action receives its own `TickContext`. The context collects effects,
43
+ schedules, suspension requests, completion, and log lines. If the handler
44
+ raises, that context is discarded and the error is written to ordinary Python
45
+ operational logging. Other valid actions may still commit.
46
+
47
+ After all due actions have decided, valid contexts apply in stable sequence
48
+ order. An effect that raises is different: effects execute inside the tick
49
+ transaction, so the exception aborts the entire tick.
50
+
51
+ ## Suspension
52
+
53
+ ```python
54
+ ctx.suspend(lira_id, by=fight_id)
55
+ ```
56
+
57
+ This pauses every suspendable action owned by Lira. Non-suspendable work such
58
+ as hunger or poison continues.
59
+
60
+ A suspended action remembers when it paused, who owes the wake-up, and its
61
+ old deadline. Resuming by the same handle shifts the stored deadline forward
62
+ by the exact pause duration. Override `on_resume(paused_for)` when the action
63
+ also has a game-owned deadline:
64
+
65
+ ```python
66
+ @override
67
+ def on_resume(self, paused_for: int, /) -> None:
68
+ self.arrives_at += paused_for
69
+ ```
70
+
71
+ ## Effects
72
+
73
+ An effect needs only this structural shape:
74
+
75
+ ```python
76
+ class Effect[W](Protocol):
77
+ def apply(self, world: W, /) -> None: ...
78
+ ```
79
+
80
+ Foliot does not know whether the effect represents damage, healing, ownership,
81
+ weather, construction, or economics. Effects should normally carry all data
82
+ needed to apply their mutation.
83
+
84
+ ## Post-effect finalization
85
+
86
+ An optional `TickFinalizer` sees the world after ordinary and Event effects.
87
+ It is the place for game rules such as death:
88
+
89
+ ```python
90
+ class DeathFinalizer:
91
+ def finalize(self, world: World, ctx: FinalizationContext[World], /) -> None:
92
+ for character in world.characters.values():
93
+ if character.hp == 0:
94
+ ctx.delete_owned_by(character.entity_id)
95
+ ```
96
+
97
+ Foliot provides the timing and cleanup tools; it never defines what death
98
+ means.
@@ -0,0 +1,42 @@
1
+ # Public API
2
+
3
+ This page is a map of foliot's supported import surface. The docstrings on
4
+ each object contain the detailed parameter and error contracts.
5
+
6
+ ## Core
7
+
8
+ Import these names directly from `foliot`:
9
+
10
+ - **Actions:** `BaseAction`, `ActionBinding`, `Unbound`, `Bound`, `ActionState`,
11
+ `Active`, and `Suspended`.
12
+ - **Processing:** `TickContext`, `Effect`, `Simulation`, `TickFinalizer`, and
13
+ `FinalizationContext`.
14
+ - **Time:** `Driver`, `ManualDriver`, and `RealtimeDriver`.
15
+ - **Storage:** `Store`, `Txn`, and `MemoryStore`.
16
+ - **Randomness:** `Rng`, `counter_rng`, and `new_world_seed`.
17
+ - **Identifiers:** `Tick`, `EntityId`, and `SuspensionId`.
18
+
19
+ `BaseAction` is the one mandatory base class. The remaining action-state
20
+ classes are useful when a store adapter serializes or inspects queue state.
21
+
22
+ ## Optional Events
23
+
24
+ Import these names from `foliot.events` only when simultaneous decisions are
25
+ needed:
26
+
27
+ - **Actions and intents:** `EventAction` and `IntentRecord`.
28
+ - **Events and outcomes:** `BaseEvent`, `Outcome`, `DecisionContext`, and
29
+ `ResolutionContext`.
30
+ - **Integration:** `Events`, `EventStore`, `EventTxn`, and `EventMemoryStore`.
31
+ - **Commands:** `open_event` and `end_event`.
32
+ - **Stable identifiers:** `EventId`, `EventIdTemplate`, and `EntityIdTemplate`.
33
+ - **Configuration errors:** `EventConfigurationError`.
34
+
35
+ See [Simultaneous Events](events.md) before implementing this layer. Ordinary
36
+ actions, effects, stores, and drivers do not depend on it.
37
+
38
+ ## Import stability
39
+
40
+ Names listed in `foliot.__all__` and `foliot.events.__all__` are the intended
41
+ public API. Modules and names beginning with an underscore are internal and may
42
+ change without notice while the project is pre-alpha.