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

101
examples/README.md Normal file
View file

@ -0,0 +1,101 @@
# Examples
A curated tour of Ludic, grouped by intent. Every example here is exercised by
the test suite (`bin/x test` / `bin/x selfhost-test`), so nothing in this
directory silently rots.
Run any program straight from the repository root (so `assets/` resolves):
```sh
bin/x app examples/games/snake.ludic # compile + open a real window
bin/x app examples/games/snake.ludic --headless # render one frame to build/out.ppm
bin/ludic examples/lang/offline_rewards.ludic # compile + run a plain program
```
## `games/` — complete, windowed games
| Example | What it shows |
|---|---|
| [`games/snake.ludic`](games/snake.ludic) | A full game from primitives: input, grid, growth, collision, score. |
| [`games/menu.ludic`](games/menu.ludic) | A retained-UI title screen — the UI declared as data. |
| [`games/chronorift.ludic`](games/chronorift.ludic) | A 2D co-op JRPG: overworld, dungeon, random encounters, turn battle, boss, save. The entry file `import`s the modules under [`games/chronorift/`](games/chronorift/). |
## `rendering/` — drawing primitives
| Example | What it shows |
|---|---|
| [`rendering/draw_prims.ludic`](rendering/draw_prims.ludic) | The extended `Screen.*` primitives — line, circle, triangle, fill, sprite. |
## `ecs/` — the entity-component world
| Example | What it shows |
|---|---|
| [`ecs/hello.ludic`](ecs/hello.ludic) | The smallest program that exercises the ECS pipeline. |
| [`ecs/world_get.ludic`](ecs/world_get.ludic) | Reflection: read/write a component by name. |
| [`ecs/world_scan.ludic`](ecs/world_scan.ludic) | Reflection: scan the world and identify each entity. |
| [`ecs/world_query.ludic`](ecs/world_query.ludic) | Reflection: iterate the world by property. |
| [`ecs/world_spawn.ludic`](ecs/world_spawn.ludic) | Reflection: a mod spawns a fresh entity by model id. |
| [`ecs/world_mixed.ludic`](ecs/world_mixed.ludic) | Reflection: get/set through real struct offsets. |
| [`ecs/world_dyn.ludic`](ecs/world_dyn.ludic) | Reflection: register a brand-new component at runtime. |
## `events/` — the event bus
| Example | What it shows |
|---|---|
| [`events/events.ludic`](events/events.ludic) | EV0: the event-bus core — declare, `emit`, `@On`. |
| [`events/mod_events.ludic`](events/mod_events.ludic) | Two listeners on one event, driven from a mod. |
| [`events/cancel.ludic`](events/cancel.ludic) | EV3: cancellable (decision) events. |
| [`events/recurse.ludic`](events/recurse.ludic) | EV6: re-entrant `emit` is depth-bounded — no runaway cycle. |
| [`events/promote.ludic`](events/promote.ludic) | EV1: `@Public` promotes a lifecycle hook to a public event. |
| [`events/prop_events.ludic`](events/prop_events.ludic) | EV1 for properties: attach/detach events. |
| [`events/scene_events.ludic`](events/scene_events.ludic) | EV1 for scenes: on-enter / on-exit events. |
| [`events/program_events.ludic`](events/program_events.ludic) | EV1 for the program scope: start/quit events. |
| [`events/layer_events.ludic`](events/layer_events.ludic) | EV1 for layers + a `public` layer's show/hide events. |
| [`events/scoped.ludic`](events/scoped.ludic) | A despawned entity drops out of subsequent event work. |
## `networking/` — deterministic multiplayer (N0–N6)
Each maps to a stage of [`NETWORKING-DESIGN.md`](../NETWORKING-DESIGN.md); all run
over the compiler's built-in loopback transport with zero foreign code.
| Example | What it shows |
|---|---|
| [`networking/net_echo.ludic`](networking/net_echo.ludic) | N0: the transport seam. |
| [`networking/net_snapshot.ludic`](networking/net_snapshot.ludic) | N1: whole-world snapshot to a memory buffer. |
| [`networking/net_sync.ludic`](networking/net_sync.ludic) | N2: `@Sync` replication codegen. |
| [`networking/net_owner.ludic`](networking/net_owner.ludic) | N3: entity ownership. |
| [`networking/net_rpc.ludic`](networking/net_rpc.ludic) | N4: remote events / RPCs. |
| [`networking/net_roles.ludic`](networking/net_roles.ludic) | N5: handler roles + the drivable sim. |
| [`networking/net_demo.ludic`](networking/net_demo.ludic) | N6: a networked game end to end, in pure Ludic. |
| [`networking/net_rt.ludic`](networking/net_rt.ludic) | A blessed server-authoritative replication runtime. |
## `lang/` — language & standard-library tour
| Example | What it shows |
|---|---|
| [`lang/annotations.ludic`](lang/annotations.ludic) | The annotation-first style: `@Queries` / `@Computed` / `@OnSpawn` / `@Handles`. |
| [`lang/qdecl.ludic`](lang/qdecl.ludic) | `@Queries` desugaring to the query system. |
| [`lang/lifecycle.ludic`](lang/lifecycle.ludic) | The whole game lifecycle as `@`-hooks, in firing order. |
| [`lang/detach.ludic`](lang/detach.ludic) | Structural attach/detach + `@OnAttach` / `@OnDetach`. |
| [`lang/reason.ludic`](lang/reason.ludic) | Reason-carrying teardown (`@OnDespawn`: Despawned vs Quit). |
| [`lang/toggle.ludic`](lang/toggle.ludic) | enable/disable at the three ECS scopes. |
| [`lang/scenes.ludic`](lang/scenes.ludic) | One active scene at a time, handlers grouped into layers. |
| [`lang/strings.ludic`](lang/strings.ludic) | Strings as values: compare, join, interpolate, slice. |
| [`lang/rng_demo.ludic`](lang/rng_demo.ludic) | The `Random.*` extensions (value/int/sign). |
| [`lang/time_demo.ludic`](lang/time_demo.ludic) | `Time.frame` / `elapsed` / `delta`. |
| [`lang/offline_rewards.ludic`](lang/offline_rewards.ludic) | A worked idle-game example over the Time/Date/Duration stdlib. |
## `library/` — building and linking a shared library
A how-to for compiling a `.ludic` module to a native `.dylib`/`.so`/`.dll` and
linking a second program against it via `extern fn`:
```sh
bin/ludicc examples/library/combat.ludic --shared -o build/libcombat.dylib
bin/ludicc examples/library/arena.ludic -o build/arena -Lbuild -lcombat
```
| Example | What it shows |
|---|---|
| [`library/combat.ludic`](library/combat.ludic) | Exported functions callable across the C ABI. |
| [`library/arena.ludic`](library/arena.ludic) | A program that links against the shared library. |

View file

@ -1,5 +1,5 @@
# world.ludic — the data model: components, entity archetypes, constants.
# Imported by examples/chronorift.ludic. No `game` wrapper (this is a fragment).
# Imported by examples/games/chronorift.ludic. No `game` wrapper (this is a fragment).
property Pos { x: int = 0, y: int = 0 }
property Actor { kind: int = 0 }

View file

@ -1,6 +1,6 @@
# ============================================================================
# snake.ludic — a small, complete game in Ludic. No sprites or assets: every
# pixel is drawn from primitives. Build: bin/x app examples/snake.ludic
# pixel is drawn from primitives. Build: bin/x app examples/games/snake.ludic
#
# It shows the everyday shape of a Ludic game: an ECS for the moving parts (one
# entity per snake segment), named program state for the rest, and a Render

View file

@ -11,7 +11,7 @@
# 25 @OnDetach(Shield): reads the outgoing amount 5, prints 5 + 20
# 0 the Shield is gone — nothing matches
#
# bin/x game-build bin/ludicc examples/detach.ludic /tmp/detach
# bin/x game-build bin/ludicc examples/lang/detach.ludic /tmp/detach
# /tmp/detach </dev/null
program Detach {
property Tag { v: int = 0 }

View file

@ -3,7 +3,7 @@
# the deterministic Clock. Everything is integer seconds, so the result replays
# identically — no wall clock, no floating point.
#
# bin/ludic examples/offline_rewards.ludic # prints 13 / 650 / 2026-08-30 / 0
# bin/ludic examples/lang/offline_rewards.ludic # prints 13 / 650 / 2026-08-30 / 0
program OfflineRewards {
entry {
# A save records when the player last quit. It is hardcoded here so the demo

View file

@ -9,7 +9,7 @@
# 503 Enemy A despawned in-world (Despawned): drop its loot, 3 + 500
# 1009 Enemy B outlived the run; at quit (Quit) it skips loot, 9 + 1000
#
# bin/x game-build bin/ludicc examples/reason.ludic /tmp/reason
# bin/x game-build bin/ludicc examples/lang/reason.ludic /tmp/reason
# /tmp/reason </dev/null
program Reasons {
property Health { hp: int = 0 }

View file

@ -12,7 +12,7 @@
# 201 900 frame 3: Play.World.Step, then Hud.Draw
# 202 900 frame 4: Step reaches 2 -> quit(); Hud.Draw paints the last frame
#
# bin/x game-build bin/ludicc examples/scenes.ludic /tmp/scenes
# bin/x game-build bin/ludicc examples/lang/scenes.ludic /tmp/scenes
# printf 'aaaa' | /tmp/scenes
program SceneDemo {
var counter: int = 0

View file

@ -1,8 +1,8 @@
# ============================================================================
# arena.ludic — a game that links the Ludic shared library next to it.
#
# ludicc examples/lib/combat.ludic --shared -o build/libcombat.dylib
# ludicc examples/lib/arena.ludic -o build/arena -Lbuild -lcombat
# ludicc examples/library/combat.ludic --shared -o build/libcombat.dylib
# ludicc examples/library/arena.ludic -o build/arena -Lbuild -lcombat
#
# `extern fn` binds a name to a symbol resolved at link time. The library the
# symbols come from happens to be written in Ludic, but nothing here depends on

View file

@ -4,7 +4,7 @@
# `module` instead of `game` means: no entry point, no frame loop. ludicc
# compiles this to a real shared object —
#
# ludicc examples/lib/combat.ludic --shared -o build/libcombat.dylib
# ludicc examples/library/combat.ludic --shared -o build/libcombat.dylib
#
# — whose `export fn`s are ordinary C-ABI symbols. Anything that can call a
# .dylib/.so/.dll can call these: another Ludic program via `extern fn`, a game

View file

@ -14,7 +14,7 @@
# 5. rt_receive() — the client reconciles to the authoritative x=5
#
# Prints 5 / 999 / 5. Build & run with the Ludic toolchain only:
# bin/x app examples/net_demo.ludic --headless && ./build/net_demo_headless
# bin/x app examples/networking/net_demo.ludic --headless && ./build/net_demo_headless
import "net_rt.ludic"
program NetDemo {