feat(ludic.base): the vocabulary mechanic packages share - Tick, phases, Queue<T>, rng streams, the save tree, the system runner

A mechanic package depends on ludic.base and nothing else: ports (records of
function values for now) for questions, queues for facts, verbs for changes,
phases for order and its own versioned save section. The runner inits, resets,
saves and loads systems in the order added and ticks them phase by phase; a
missing save section is a reset. Tests for each piece and a worked route
between two toy mechanics live under tests/. The old ludic.core (engine ECS
components) is used by examples/library and stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-25 04:01:16 +03:00
parent ec9a650e65
commit dd6a449921
16 changed files with 928 additions and 0 deletions

View file

@ -0,0 +1,105 @@
# system_test.ludic - the runner: init, reset, save and load in the order added, tick phase by
# phase, each system its own save section, and a missing section is a reset
import "ludic.base"
program SystemTest {
numbers float
var trail: string = ""
var apples: int = 0
var hours: float = 0.0
function draw_tick(t: Tick) -> void { trail = trail + "d" }
function think_tick(t: Tick) -> void { trail = trail + "t" }
function input_tick(t: Tick) -> void { trail = trail + "i" }
function commit_tick(t: Tick) -> void { trail = trail + "c" }
function apples_init() -> void { trail = trail + "A" }
function apples_reset() -> void { apples = 0 }
function apples_tick(t: Tick) -> void { apples += 1 }
function apples_save() -> Val {
let v = Value.object()
sv_put_int(v, "n", apples)
return v
}
function apples_load(v: Val, version: int) -> void { apples = sv_int(v, "n", 0) * version }
function clock_reset() -> void { hours = 6.0 }
function clock_tick(t: Tick) -> void { hours = hours + t.hours }
function clock_save() -> Val {
let v = Value.object()
sv_put_float(v, "h", hours)
return v
}
function clock_load(v: Val, version: int) -> void { hours = sv_float(v, "h", 6.0) }
function sys(key: string, phase: int, tick: fn(Tick) -> void) -> System {
let s = system_new(key, phase)
s.tick = tick
return s
}
function setup() -> void {
core_clear()
trail = ""
core_add(sys("draw", PH_PRESENT, fn draw_tick))
core_add(sys("think", PH_SIMULATE, fn think_tick))
let a = sys("apples", PH_SIMULATE, fn apples_tick)
a.init = fn apples_init
a.reset = fn apples_reset
a.save = fn apples_save
a.load = fn apples_load
a.version = 2
core_add(a)
core_add(sys("input", PH_INPUT, fn input_tick))
core_add(sys("commit", PH_COMMIT, fn commit_tick))
let c = sys("clock", PH_SIMULATE, fn clock_tick)
c.reset = fn clock_reset
c.save = fn clock_save
c.load = fn clock_load
core_add(c)
}
test "tick runs phase by phase, in the order added within a phase" {
setup()
core_tick_all(tick_new(1.0 / 60.0, 0, 0.0))
expect(trail == "itcd")
expect_eq(apples, 1)
}
test "init and reset run in the order added and skip a null verb" {
setup()
core_init_all()
expect(trail == "A")
apples = 9
core_reset_all()
expect_eq(apples, 0)
expect(hours == 6.0)
expect_eq(core_count(), 6)
}
test "save writes each system under its key with its version, and load reads it back" {
setup()
core_reset_all()
apples = 4
core_tick_all(tick_new(0.016, 1, 0.5))
let root = core_save_all()
expect_eq(Value.count(root), 2)
expect_eq(load_section(root, "apples").version, 2)
expect_eq(load_section(root, "clock").version, 1)
let text = save_encode(root)
core_reset_all()
core_load_all(save_decode(text))
expect_eq(apples, 10)
expect(hours == 6.5)
}
test "a system with no section in the save is reset" {
setup()
apples = 3
hours = 11.0
let root = save_tree()
let v = Value.object()
sv_put_float(v, "h", 20.25)
save_section(root, "clock", 1, v)
core_load_all(root)
expect_eq(apples, 0)
expect(hours == 20.25)
}
}