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:
parent
0bc5d76952
commit
fb728bbefe
73 changed files with 364 additions and 194 deletions
33
examples/ecs/hello.ludic
Normal file
33
examples/ecs/hello.ludic
Normal 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()
|
||||
}
|
||||
}
|
||||
27
examples/ecs/world_dyn.ludic
Normal file
27
examples/ecs/world_dyn.ludic
Normal 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
|
||||
}
|
||||
}
|
||||
22
examples/ecs/world_get.ludic
Normal file
22
examples/ecs/world_get.ludic
Normal 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
|
||||
}
|
||||
}
|
||||
17
examples/ecs/world_mixed.ludic
Normal file
17
examples/ecs/world_mixed.ludic
Normal 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
|
||||
}
|
||||
}
|
||||
26
examples/ecs/world_query.ludic
Normal file
26
examples/ecs/world_query.ludic
Normal 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
|
||||
}
|
||||
}
|
||||
30
examples/ecs/world_scan.ludic
Normal file
30
examples/ecs/world_scan.ludic
Normal 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
|
||||
}
|
||||
}
|
||||
20
examples/ecs/world_spawn.ludic
Normal file
20
examples/ecs/world_spawn.ludic
Normal 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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue