chore(repo): DX cleanup — categorise examples, text-diffable golden, build/ output (#27 #28 #30)

Repository-cleanup / DX pass folding three tracker items into one coherent
change, verified green end to end (`bin/x test` 49/0, `bin/x selfhost-test`
29/0, `bin/x test-tools` 29/0).

#28 — curate & categorise examples/
- 42 flat entries regrouped into intent-revealing subdirs: games/, rendering/,
  ecs/, events/, networking/, lang/, library/ (was lib/).
- chronorift dir-vs-file duplication resolved: the entry file and its import
  modules now live together under games/chronorift(.ludic).
- Every path reference updated repo-wide (test runner, editor-tool drivers,
  docs/site, design docs).
- New examples/README.md indexes the whole set with run commands.
- Showcase examples without a self-asserting entry (hello, events, net_rt) now
  get a compile-only rot guard in `bin/x test`, so nothing here rots silently.

#30 — text-diffable golden baseline
- The 4 binary selfhost/golden/*.ppm blobs are replaced by a single
  selfhost/golden/renders.sha256 manifest (SHA-256 per render). Hashes are
  byte-identical to the old PPMs, so the baseline is unchanged — only its form.
- game_case now compares framebuffer hashes; a regression shows as a changed
  hex line in review, not "binary files differ".
- New `bin/x golden` regenerates the manifest deliberately (review with
  `git diff selfhost/golden/renders.sha256`).

#27 — PPM & asset handling
- Headless renders now write build/out.ppm, never the repo root; `x app`,
  `x clean`, messaging and .gitignore updated to match. Nothing is written to
  the working root any more.
- Redundant local Kenney .zip archives removed (the art ships extracted;
  .gitignore already excludes *.zip). CC0 License.txt files retained.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-30 18:54:21 +03:00
parent 0bc5d76952
commit fb728bbefe
73 changed files with 364 additions and 194 deletions

33
examples/ecs/hello.ludic Normal file
View file

@ -0,0 +1,33 @@
# hello.ludic — smallest program that exercises the ECS pipeline
program Hello {
property Pos { x: int = 0, y: int = 0 }
property Vel { dx: int = 0, dy: int = 0 }
handler Boot phase Start {
spawn Mob {
Pos { x: 3, y: 4 }
Vel { dx: 1, dy: 0 }
}
spawn Mob {
Pos { x: 10, y: 2 }
Vel { dx: 0, dy: 1 }
}
}
# a handler declares the entities it operates on with @Queries: the body then
# runs once per match, each property bound by name and self() giving that entity.
@Queries(these: [Pos, Vel])
handler Move phase FixedUpdate {
Pos.x = Pos.x + Vel.dx
Pos.y = Pos.y + Vel.dy
}
handler Report phase Update {
for (p) in query [Pos] {
print(p.x)
print(p.y)
}
quit()
}
}

View file

@ -0,0 +1,27 @@
# world_dyn.ludic — EV7 reflection: register a brand-new component at runtime
# (replaces tests/mod_c/world_dyn.c). world_register_prop declares a component the
# game never wrote; world_attach_dyn adds it to an entity; get/set/has then treat
# it exactly like a built-in, with per-entity isolation. Prints 0 1 30 100 1 30.
program WorldDyn {
property Health { hp: int = 0, max: int = 0 }
model Unit { Health }
@Public @OnSpawn(Unit) handler Init { }
entry {
let Um = world_model_id("Unit")
let e = world_spawn(Um)
let e2 = world_spawn(Um)
let Mana = world_register_prop("Mana", 2) # a component never declared in source
print(world_has(e, Mana)) # 0 — not attached yet
world_attach_dyn(e, Mana)
print(world_has(e, Mana)) # 1
world_set(e, Mana, 0, 30) # field 0 (current)
world_set(e, Mana, 1, 100) # field 1 (max)
print(world_get(e, Mana, 0)) # 30
print(world_get(e, Mana, 1)) # 100
if world_prop_id("Mana") == Mana { print(1) } else { print(0) } # 1 — name resolves
world_attach_dyn(e2, Mana)
world_set(e2, Mana, 0, 7)
print(world_get(e, Mana, 0)) # 30 — per-entity isolation
}
}

View file

@ -0,0 +1,22 @@
# world_get.ludic — EV2 reflection: read and write a component by NAME through the
# world table, driven from Ludic (replaces tests/mod_c/world_mod.c). world_prop_id
# / world_field_id resolve names to ids; world_get / world_set / world_has read and
# write by those ids — the same reflection ABI a foreign mod binds, now callable
# from Ludic. Spawns a Unit (Init sets hp=max=50), then reads/tests/writes hp.
# Prints 50 / 1 / 7.
program WorldGet {
property Health { hp: int = 0, max: int = 0 }
model Unit { Health }
@Public @OnSpawn(Unit) handler Init { Health.hp = Health.max } # -> emits the world table
entry {
spawn Unit { Health { max: 50 } }
let H = world_prop_id("Health")
let hp = world_field_id(H, "hp")
let e = world_query_next(H, 0)
print(world_get(e, H, hp)) # 50 — read by name
print(world_has(e, H)) # 1 — entity has Health
world_set(e, H, hp, 7) # write by name
print(world_get(e, H, hp)) # 7 — the write stuck
}
}

View file

@ -0,0 +1,17 @@
# world_mixed.ludic — EV2b reflection: get/set use real struct offsets, not a
# field*4 assumption (replaces tests/mod_c/world_mixed.c). Slot's `qty` sits after
# a `ptr` field, so its byte offset is 8, not 4; writing qty=99 by name and reading
# it back proves the world table addresses fields by their true layout. Prints 99.
program WorldMixed {
property Slot { ref: pointer = null, qty: int = 0 }
model Item { Slot }
@Public @OnSpawn(Item) handler Born { } # makes it a modding program
entry {
let e = world_spawn(world_model_id("Item"))
let S = world_prop_id("Slot")
let qty = world_field_id(S, "qty") # field index 1, after the ptr
world_set(e, S, qty, 99)
print(world_get(e, S, qty)) # 99 iff the offset is 8, not 4
}
}

View file

@ -0,0 +1,26 @@
# world_query.ludic — EV2b reflection: iterate the world by property (replaces
# tests/mod_c/world_query.c). world_query_next(prop, from) returns the next live
# entity (>= from) that has the property, or -1 — so a mod walks all bearers of a
# component without knowing the models. Two Units (hp 50, 60) → count 2, sum 110.
program WorldQuery {
property Health { hp: int = 0, max: int = 0 }
model Unit { Health }
@Public @OnSpawn(Unit) handler Init { Health.hp = Health.max }
entry {
spawn Unit { Health { max: 50 } }
spawn Unit { Health { max: 60 } }
let H = world_prop_id("Health")
let hp = world_field_id(H, "hp")
var count = 0
var sum = 0
var e = world_query_next(H, 0)
while e >= 0 {
count = count + 1
sum = sum + world_get(e, H, hp)
e = world_query_next(H, e + 1)
}
print(count) # 2
print(sum) # 110
}
}

View file

@ -0,0 +1,30 @@
# world_scan.ludic — EV2b reflection: scan the world and identify each entity by
# MODEL, by name (replaces tests/mod_c/world_scan.c). world_count / world_kind /
# world_model_id let a mod walk every entity and pick out a model's instances, then
# read their fields with world_get. Two Units (hp 50, 60) → count 2, sum 110.
program WorldScan {
property Health { hp: int = 0, max: int = 0 }
model Unit { Health }
@Public @OnSpawn(Unit) handler Init { Health.hp = Health.max }
entry {
spawn Unit { Health { max: 50 } }
spawn Unit { Health { max: 60 } }
let H = world_prop_id("Health")
let hp = world_field_id(H, "hp")
let Um = world_model_id("Unit")
let n = world_count()
var count = 0
var sum = 0
var e = 0
while e < n {
if world_kind(e) == Um {
count = count + 1
sum = sum + world_get(e, H, hp)
}
e = e + 1
}
print(count) # 2
print(sum) # 110
}
}

View file

@ -0,0 +1,20 @@
# world_spawn.ludic — EV2b reflection: a mod creates a fresh entity by model id
# (replaces tests/mod_c/world_spawn.c). world_spawn reuses the compiler's own
# spawn lowering (alloc, kind, component defaults, @OnSpawn), so a mod-spawned
# entity is indistinguishable from one born in source. Prints 1 / 42 / 1.
program WorldSpawn {
property Health { hp: int = 0, max: int = 0 }
model Unit { Health }
@Public @OnSpawn(Unit) handler Init { } # world table; leave defaults
entry {
let Um = world_model_id("Unit")
let e = world_spawn(Um) # the mod creates an entity
let H = world_prop_id("Health")
let hp = world_field_id(H, "hp")
print(world_has(e, H)) # 1 — spawn attached Health with defaults
world_set(e, H, hp, 42)
print(world_get(e, H, hp)) # 42
print(world_count()) # 1 — it is really in the world
}
}