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 | | `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 | | `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) | | `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 | | `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 ## A toy mechanic
@ -123,24 +124,26 @@ Both are compiled and run by `tests/route_test.ludic` (the mechanics are `tests/
## 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 ```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 `queue_test`, `rng_test`, `save_test`, `system_test`, `registry_test` and `route_test`. They were
does not do yet shape them: a generic call inside a `test` body is not resolved (so the queue written before a generic call inside a `test` body resolved and before each test had a fresh
cases are functions a test calls), and test blocks share one program's globals (so each case state, so the queue cases are functions a test calls and the runner's cases start with
that uses the runner starts with `core_clear()`); `ludic test <dir>` with a fresh state per `core_clear()`; neither is needed any more.
test will lift the second.
## Later ## Later
- `port FishingWorld { ... }` / `bind` will replace the record of function values, checked at - `port FishingWorld { ... }` / `bind` will replace the record of function values, checked at
compile time instead of a null at run time. 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 - `Systems` is an `open registry`, so a system can be declared instead of added: `def Systems
the game's own `def`s give the order. Function values in a `def` work today (`def Systems a { fishing { phase: PH_SIMULATE, tick: fn fishing_tick }` from any module. The runner starts from
tick: fn a_tick }`); what is missing is a registry a package declares and a game fills, in an the declared systems - the order the compiler gives an open registry: ludic.base's own (none),
order the game controls. Until then the list is built by calls, in one place. 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. - `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 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 var cs_list: []System = null
function cs_all() -> []System { 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 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")
}
}