feat(ludic.base): Systems is an open registry - def a system from any module

The runner's list starts from the declared systems, in the order the
compiler gives an open registry, and core_add appends after them.
registry_test covers it; the README says ludic test runs the package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-25 05:15:56 +03:00
parent 73129b59d5
commit cac8c740fa
3 changed files with 43 additions and 12 deletions

View file

@ -43,6 +43,7 @@ import "ludic.base"
| `sv_put_int`, `sv_put_float`, `sv_put_bool`, `sv_put_str`, `sv_put_ints` | the matching writes |
| `System { key, phase, version, init, reset, tick, save, load }`, `system_new(key, phase)` | a system; a null function is a verb it does not have |
| `core_add(s)`, `core_clear()`, `core_count()` | the game's system list (a key may appear once) |
| `def Systems key { ... }`, `SYS_<KEY>`, `SYS_COUNT` | a system declared from any module; the list starts from these |
| `core_init_all()`, `core_reset_all()`, `core_tick_all(t)`, `core_save_all() -> Val`, `core_load_all(v)` | the runner: in the order added, tick phase by phase |
## A toy mechanic
@ -123,24 +124,26 @@ Both are compiled and run by `tests/route_test.ludic` (the mechanics are `tests/
## Tests
Each piece has a program under `tests/`, built and run directly:
Each piece has a program under `tests/`, and `ludic test` runs them all, every test block in a
process of its own:
```bash
ludic build packages/ludic.base/tests/queue_test.ludic --headless -o /tmp/queue_test && /tmp/queue_test
ludic test packages/ludic.base
```
`queue_test`, `rng_test`, `save_test`, `system_test` and `route_test`. Two things the language
does not do yet shape them: a generic call inside a `test` body is not resolved (so the queue
cases are functions a test calls), and test blocks share one program's globals (so each case
that uses the runner starts with `core_clear()`); `ludic test <dir>` with a fresh state per
test will lift the second.
`queue_test`, `rng_test`, `save_test`, `system_test`, `registry_test` and `route_test`. They were
written before a generic call inside a `test` body resolved and before each test had a fresh
state, so the queue cases are functions a test calls and the runner's cases start with
`core_clear()`; neither is needed any more.
## Later
- `port FishingWorld { ... }` / `bind` will replace the record of function values, checked at
compile time instead of a null at run time.
- `open registry Systems of System` could replace `core_add`: each mechanic `def`s its system and
the game's own `def`s give the order. Function values in a `def` work today (`def Systems a {
tick: fn a_tick }`); what is missing is a registry a package declares and a game fills, in an
order the game controls. Until then the list is built by calls, in one place.
- `Systems` is an `open registry`, so a system can be declared instead of added: `def Systems
fishing { phase: PH_SIMULATE, tick: fn fishing_tick }` from any module. The runner starts from
the declared systems - the order the compiler gives an open registry: ludic.base's own (none),
then each other module's by module name, each module's in the order it is read - and `core_add`
appends after them. A game that wants to decide the order itself writes the `def`s in its own
files, or keeps calling `core_add` in one place.
- `module toy_fishing uses ludic_base` will make rule 1 a compile error rather than a review.

View file

@ -12,10 +12,17 @@ export property System {
load: fn(Val, int) -> void = null
}
# the declared list: a mechanic or the game writes `def Systems fishing { phase: PH_ACT, tick: fn
# fishing_tick }` from its own module; the runner starts from these, then core_add appends
export open registry Systems of System as SYS
var cs_list: []System = null
function cs_all() -> []System {
if cs_list == null { cs_list = new []System }
if cs_list == null {
cs_list = new []System
for i in 0 .. SYS_COUNT { push(cs_list, Systems[i]) }
}
return cs_list
}

View file

@ -0,0 +1,21 @@
# registry_test.ludic - systems declared with `def Systems ...` from outside the package: the
# runner starts from them in the registry's order, and core_add still appends after them
import "ludic.base"
program RegistryTest {
numbers float
var trail: string = ""
function late_tick(t: Tick) -> void { trail = trail + "l" }
function early_tick(t: Tick) -> void { trail = trail + "e" }
function added_tick(t: Tick) -> void { trail = trail + "a" }
def Systems late { phase: PH_RESOLVE, tick: fn late_tick }
def Systems early { phase: PH_INPUT, tick: fn early_tick }
test "the declared systems run, phase by phase" {
expect_eq(core_count(), 2)
expect(Systems[SYS_LATE].key == "late")
let s = system_new("added", PH_INPUT)
s.tick = fn added_tick
core_add(s)
core_tick_all(tick_new(0.016, 1, 0.0))
expect(trail == "eal")
}
}