Normalise three collision sources into the byte tilemap esys_move / Grid.* /
Path.* read, in the design's priority order (§3.5):
- per-tile <objectgroup> hitboxes (Tiled.tile_shapes) — the precise source;
- the solid/oneway/trigger bool property convention (Tiled.collision_kind);
- the designated collision layer — any non-zero GID solid (Tiled.project),
the fallback source, applied automatically on load.
Tiled.collide drives collision from a visual layer's per-tile metadata alone
(a tile with no collision metadata stays passable). tmap_collision_kind
classifies a GID (0 none / 1 solid / 2 one-way / 3 trigger) from its metadata;
the projection adds the collision-layer fallback.
Proven by library/tiled_p2.ludic (14 assertions): kind classification for each
source, the property convention and collision-layer fallback producing an
identical Solids feed, a per-tile-<objectgroup> floor blocking an esys_move
mover, and A* over a loaded map matching the hand-authored baseline. x test:
93 passed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The heart of Tiled support: load a map and draw it.
- rt_tmap model (Tmap/TmLayer/TmTileset in tiled.ludic): map header + ordered
layers (dense int32 GID arrays, heap-allocated to w*h, lifting the 96x64 cap)
+ tilesets. Built from the intermediate Value tree, so the TMX and TMJ paths
both feed it.
- GID resolver (Tiled.resolve): gid -> (tileset, localId, flipH/V/D); the three
flip flags masked off before the local-id lookup, returned alongside. gid==0
is empty.
- Image-backed render (Tiled.draw): every visible tile layer in file order,
blitting each tile from its tileset image with flips applied at draw.
- Compatibility projection (Tiled.project / auto on load): a designated
collision layer projects to the legacy byte tilemap ('#' solid, '=' one-way
via the oneway property, ' ' empty) so Grid.*/Path.*/esys_move are unchanged.
- Tiled.load resolves external tilesets + images relative to the map file and
auto-projects a collision/solids/walls layer.
- The grid and physics_tiles demos now run off a loaded map (grid_maze.tmx /
physics_map.tmx) instead of hand-authored Map.row strings, byte-identically.
Two compiler fixes fell out of this (see the changeset):
- emit_index_addr set g_addr_ty before evaluating the index, so slice[obj.field]
came back mis-typed; set it last, like the raw-pointer branches.
- @strcmp was declared by both the world table and the fs prelude; centralise
it in the head prelude so a game that uses Fs/Path links.
Proven by library/tiled_p1.ludic (14 assertions: loads+renders Kenney map
identically from .tmx and .tmj, flip mirroring) + the converted grid/
physics_tiles demos. x test: 92 passed; self-host bootstrap fixpoint intact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tiled.read / Tiled.read_tsx (runtime/native/tiled.ludic): map the native
XML formats (TMX/TSX/TX) onto the SAME intermediate the JSON path produces
— a Value tree in Tiled's JSON schema — so one format-independent core
(P1) consumes either reader.
- tmx_to_value walks a <map> into {orientation, width/height, tilewidth/
height, tilesets[], layers[]}; every tile layer's <data> is decoded to a
dense GID int list (CSV split, or base64 -> zlib/gzip inflate ->
little-endian u32s), so a CSV .tmx and a base64 .tmj of the same map read
structurally identically.
- External tilesets keep the {firstgid, source} reference (as TMJ does);
embedded tilesets and standalone .tsx (tsx_to_value) inline full geometry
+ per-tile metadata (animation frames, collision objectgroup, class,
properties) for later phases.
- Object layers, shapes (rect/ellipse/point/polygon/polyline/text),
image/group layers and custom properties are parsed into the tree now so
P2/P4/P5 just read it.
- tmj_normalize decodes base64 `data` strings in a parsed .tmj so the JSON
path matches the XML path.
Proven by library/tiled_p05.ludic (30 assertions) incl. the in-repo Kenney
sampleMap.tmx + external sampleSheet.tsx and TMX-vs-TMJ structural
identity. Docs + inventory added; x test: 91 passed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The three parsing primitives the TMX path needs that were not already in
the tree (zlib inflate + the JSON reader already shipped):
- Xml.* — a minimal, deterministic pure-Ludic XML reader for the
element/attribute/CDATA subset TMX/TSX/TX use, spliced on demand. Handles
nested elements, single/double-quoted attributes, text + <![CDATA[…]]>,
comments, the <?xml?> prolog and <!DOCTYPE>, the five predefined entities
and numeric character references.
- Base64.* — standard base64 (RFC 4648) decode + a matching pure-Ludic
encoder; the decoder ignores the whitespace Tiled wraps into <data>.
Splicing Base64.* also pulls in inflate.ludic so a plain tool can run the
full base64 -> zlib/gzip decode chain.
- z_gunzip — gzip framing (RFC 1952) over the existing DEFLATE inflater:
skip the 10-byte header + optional fields, inflate the body, ignore the
CRC32/ISIZE trailer.
Golden corpus vendored under assets/tiled-fixtures/ (mapeditor/tiled
examples, attributed, + hand-authored multi-encoding fixtures). New
example library/tiled_p0.ludic proves all three (21 assertions); docs
pages + inventory added so check-impl stays green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 0 of the gameplay-controller work (#57): turn the static Collision.*
overlap tests into a real physics-lite moving-body-with-collision layer that
the platformer / shooter / NPC-AI / RPG families build on.
New engine-owned system esys_move (runtime/native/systems_move.ludic), spliced
and Update-phase-registered when a game declares `Body` (parse.ludic), standing
entirely on the reflection ABI like the SpriteAnim / Motion / Light2D systems:
- Body { vx, vy, gravity, max_fall, rx, ry, policy, on_ground, hit_wall,
hit_ceiling } — Q16.16 velocity + engine-owned sub-pixel accumulators.
- Collider { w, h, offx, offy, is_trigger, one_way, layer, mask, hit, entered,
exited } — AABB shape off Position, layer/mask filtering, one-way + triggers.
- Solids { tile, wall, oneway } — optional config entity enabling the tile-grid
broadphase over the Map.* tilemap.
esys_move integrates velocity + gravity, then resolves per-axis swept AABB
against both solid Collider entities and the tile grid (no tunneling), handles
one-way platforms (block only a downward landing), reports trigger/sensor
overlaps without resolving, and sets on_ground / hit_wall / hit_ceiling for a
controller to poll. No float, no hidden singletons, no runtime dispatch —
integer + deterministic, so replay / lockstep / world_save hold. Contact is
surfaced as polled flags (the SpriteAnim.event_fired shape), so a game raises
its own CollisionResolved / TriggerEntered events with no engine coupling.
Two self-asserting examples (entity + tile broadphase) wired into `x test`;
docs added to the collision section. A build with no Body compiles byte-for-byte
the same (bootstrap fixpoint + all golden renders unchanged).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per-annotation pages for the three package-registration annotations (with
inventory entries), a "Package-declarable namespaces and engine systems" section
in docs/PACKAGES.md, and a changeset. All doc gates green (check-docs 675
fences, docs-check 726 symbols).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Registry-izes the two hooks that made stdlib namespaces and engine systems
compiler-hardcoded, so a package registers them with no compiler edit — the
Phase-1 prerequisite for shipping the controller libraries (#58–#61) as real
packages rather than in-repo stdlib.
- Engine systems are a data-driven registry (component, esys-fn, phase). The
core three (SpriteAnim/Motion — Update, Light2D — Render) are seeded in that
exact order, so uses_engine_systems / emit_engine_systems_for_phase are now
registry-driven with byte-identical output (verified: anim_ecs, light_ecs,
snake IR unchanged; 87/0 golden renders; C-free fixpoint holds). A package
appends with `@EngineSystem(Component, Phase)` on its esys function.
- Namespaces are a registry too: a package marks a provider with
`@Namespace(Foo)`, and emit_ns_call aliases an otherwise-unknown Foo.method to
the bare foo_method (the same generic path the core namespaces use) — after
every hardcoded core block, so core dispatch is untouched.
- Both annotations are keyword-free (like #64's @System), so no vocabulary /
grammar churn.
Proven end-to-end (hermetic, source path, runs everywhere): a package registers
Score + esys_score via @EngineSystem and coach_bonus via @Namespace; a consumer
game imports it and prints "4 99" — the engine system ran each Update and
Coach.bonus() dispatched, with no compiler edit for the package. Package suite
18/0.
Core stdlib namespaces stay on their optimized hardcoded blocks by design
(byte-identity + determinism); the generic path is proven to carry a namespace
and packages ride it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The package-manager half of prebuilt binary packages, on top of the compiler
foundation (dynamic system registration + --emit-module).
- x build-lib [module.ludic]: compile a package's module to a per-target native
dylib under lib/<target>/, with an @rpath install name so a consumer resolves
it from the content-addressed store.
- x link-flags: print the clang flags (the dylib, an rpath to its store dir,
-export_dynamic) so any build system links a project's prebuilt module dylibs;
x app splices them automatically for in-repo builds.
- kind prebuilt is resolved + linked like any dependency; a missing build target
stays a hard error.
Proven hermetically (macOS-gated, since dylibs are native): a module exporting a
component + an @System(Update) + a function is built with x build-lib, fetched
as a prebuilt dep, and linked into a consumer game that never saw its source —
the module's system mutates the shared world and its function is callable
("3 42"). Package suite 17/0; full suite 87/0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The runtime + compiler foundation for prebuilt binary packages, over the
existing reflection C-ABI (EV0–EV8 already gives dynamic components via
ludic_register_prop).
- ludic_register_system(fn, phase) + a registry the frame loop dispatches after
each phase's own handlers — the systems analogue of ludic_register_prop,
mirroring the EV6 foreign event-listener array. Emitted for every ECS program;
a zero-length registry means a non-hosting game is output-identical.
- --emit-module: compile a package to a position-independent module — no main,
no world table — that declares the host reflection ABI it calls and carries a
load-time constructor which registers its @System(Phase) functions and runs
module_init (where it registers its dynamic components). Built as a dylib with
-undefined dynamic_lookup, it binds ludic_* back to the host image at load.
- @System(Phase) annotation marks a module function as a runtime-registered
system; the compiler supplies its address (Ludic source cannot take one).
- The reflection ABI (world table) is now emitted for every ECS program, so any
game can host binary modules with no flag; unused defs dead-strip at -O2, so
golden renders stay byte-identical and the C-free bootstrap fixpoint holds.
Proven end-to-end: a module dylib registers a component + an Update system; a
host game that never saw its source links it and the system mutates the shared
world each frame. Full suite 87/0, test-tools 30/0, fixpoint intact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Locks in the source-package guarantee: because Ludic is AOT, a package's
property (component), model, @OnSpawn handler (system) and plain function all
compile into the consumer's compile-time ECS with no ABI seam. The new case
fetches such a package and runs a game that spawns the imported model, reads the
imported component via reflection, relies on the imported system, and calls the
imported function — asserting "50 7".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The content-addressed store keys an entry by its hash-named directory, so
`x get` trusts one that already exists and will not silently overwrite it.
The tamper check now drops the entry before re-fetching and asserts `x verify`
goes green again, rather than relying on a no-op restore.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements the v1 direction decided in the RFC as a set of `x` subcommands
plus a small, contained compiler change.
* URL-as-identity, no registry — a dependency is named by its git import
path and a `git tag vX.Y.Z` publishes a version.
* Minimum Version Selection — a `require` is a minimum; the resolver picks
the greatest required minimum per module, then the reachable closure at
those versions. Deterministic, no SAT solver (tools/x/pkg.ludic).
* Content-addressed global store + per-project links — packages live once in
~/.ludic/store keyed by a content hash; each project links them under
ludic_modules/. package.ludic (manifest) + package.lock.ludic (lock).
* Namespace registration for source packages via a module-root import
fallback in the compiler: do_import resolves a non-local, non-absolute
import under $LUDIC_MODULES (default ludic_modules/), so a fetched
package's Ludic compiles into the consumer the way the built-in stdlib
does. Collisions and missing prebuilt targets are hard errors.
Commands: x add / x get / x update / x verify / x vendor. New hermetic suite
`x test-pkg` (stands up throwaway git repos, offline) is gated inside `x test`.
Existing programs compile byte-for-byte identically (the import fallback only
fires when the local path is absent); the C-free bootstrap fixpoint holds and
the seed is regenerated. Full suite: 87 passed, package suite: 12 passed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A layered concurrency library, safe by default. The recommended tier is
Job.* / Promise.*: a Job is a future — Job.run(kind, arg) starts a
cooperative background compute that advances each Job.pump(budget) and
finishes after enough frames (heavy work spreads out instead of hitching),
or Job.defer + Job.fulfill/fail/cancel drives one by hand. Poll with
done/ok/failed/cancelled, read result/error, count outstanding work with
Job.pending. Promise.all/race combine handle lists into a group job resolved
on the main thread; Promise.count_done/all_done power a loading bar.
The advanced, opt-in Sync.* tier (mutex/atomic/channel + cpu_count) is the
"here be dragons" surface for engine-level message passing.
The whole thing is a deterministic cooperative scheduler: results are
collected on the main thread and a Job never touches the ECS world, so
lockstep and replays stay bit-exact — same jobs + same budget reproduce
byte-for-byte on every target, and a preemptive OS-thread backend can slot
behind this same API later. Ludic has no closures, so a Job carries a
compute kind + int arg (or a hand-driven defer) rather than fn()->…, and
Promise progress is polled rather than chained through then.
Written in Ludic and spliced on demand (like Regex/Dict/Numeric): a program
that never mentions Job.*/Promise.*/Sync.* compiles byte-identically and the
C-free bootstrap fixpoint is untouched. New: runtime/native/jobs.ludic,
emit_ns_call dispatch, parse-time splice, examples/library/jobs.ludic (31
self-asserting checks), 33 docs pages + inventory, changeset.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extend `enum` from named int constants to a tagged union: a variant may
carry a payload (`enum Tile { Empty, Wall, Door(int), Portal(int, int) }`).
Such enums box to a heap record (an i32 tag at offset 0, then one 8-byte
slot per payload position); an all-bare enum keeps its zero-cost compile-
time-ordinal representation, byte-for-byte unchanged (every golden render
and the bootstrap fixpoint still hold).
- Parser: variant payload declarations, stored as N_PARAM kids on the
variant node.
- Construction: by name — `Door(3)`, `Portal(x, y)`, bare `Empty` — resolved
ahead of the function-call fallback and boxed with the payloads coerced to
their declared types.
- match: destructures a tagged scrutinee, switching on the tag and binding
each arm's payload names in a scoped local frame.
- Checking pass: a tagged `match` must be exhaustive (cover every variant or
end in `_`), and constructor/pattern arities and binding forms are checked
— all reported where the scrutinee's type is known.
Adds selfhost/tests/enums.ludic to the regression suite and documents the
feature in LANGUAGE.md and the enum/match pages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A const reference lowered to `val(itoa(g.a.ival), "int")` in emit_call.ludic —
the initializer's raw integer bits, hardcoded as `int`. For a fixed const like
`const X: fixed = 10.0` that yielded the Q16.16 bits (655360) typed as int, so
every fixed comparison/arithmetic against it silently broke (it caused an
infinite loop in runtime/native/numeric.ludic, previously worked around with
inline literals).
Fix: a const reference now emits its initializer expression via emit_expr(g.a),
which carries the initializer's real type (E_FLOAT->fixed, E_BOOL->bool,
E_STR->string) and even handles computed initializers. Every existing const is
an int literal, for which this is byte-identical to the old immediate — the
C-free bootstrap fixpoint and all golden renders are unchanged.
- selfhost/tests/const.ludic + sh_case pin fixed/int/bool const behaviour.
- runtime/native/numeric.ludic restored to named fixed consts (HUGE_TEN etc.),
which the workaround had inlined; the numeric example (20 assertions) still
passes, validating the fix under runtime splice.
All suites green: x selfhost-test 31/31 (fixpoint holds, goldens byte-identical),
x test 85/85, x test-tools 30/30.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Types phase 3 — the option/result safety pair. result/ok/err/try shipped in
#46; this adds its companion option:
- some(v) -> option (present value; any i32-width scalar)
- none() -> option (empty; no magic -1 sentinel)
- is_some / is_none -> bool
- unwrap_or(o, fallback) -> int
A heap %Option = { i32 present, i32 value }, bare builtins guarded by find_fn
(a user fn of the same name still wins), gated by g_uses_option so unused
programs compile byte-identically — same idiom as result.
Wired: emit_call codegen + %Option decl (emit_decl) + g_uses_option (emit_core),
reseeded seed, vocabulary sync (header/JetBrains/TextMate), builtin docs +
inventory, and a self-asserting example (examples/library/optionresult.ludic +
feat_case). All suites green incl. golden renders byte-identical and the
bootstrap fixpoint.
Tagged-union enums (variant payloads + binding match + exhaustiveness) are the
deep type-system feature, split out to #56.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Types phase 5 (polish). A splice-on-demand numeric runtime
(runtime/native/numeric.ludic, built on the inline Math.* trig) behind three
namespaces:
- Huge.* — idle big numbers (normalized mantissa x 10^exponent): from/add/
sub/mul/neg/cmp/sign/mantissa/exp/str (scientific 1.23e45). Display-scale,
not lockstep-exact (BigInt/Decimal for exactness).
- Angle.* — auto-wrapping radians: from_degrees/to_degrees/wrap/sin/cos/add/
diff (shortest signed rotation)/lerp (shortest arc).
- Percent.* — clamped [0,1]: clamp/of/lerp/apply.
Remaining phase-5 items are already covered (duration=Duration.*,
rune=Unicode.*, i64=long) or need a type-checking pass (handle, typed name,
other sized ints) — tracked for later.
Wired: parser splice trigger (g_uses_numeric), emit_call dispatch, reseeded
seed, a self-asserting example (examples/library/numeric.ludic + feat_case),
per-symbol docs + inventory. All suites green incl. golden renders byte-
identical and the bootstrap fixpoint.
NOTE: fixed `const`s lower to raw-int-typed values (emit_call N_CONST), which
breaks fixed comparisons — the runtime uses inline fixed literals instead.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Types phase 4 — containers. A splice-on-demand open-addressing hash table
(runtime/native/dict.ludic, FNV-1a, linear probing, tombstones, grow at 0.7)
behind two namespaces:
- Dict.* — string -> int map: new/set/get/get_or/has/remove/size/clear/keys.
Resource counts, id/name registries. O(1) average vs a linear list scan.
- Set.* — set of strings: new/add/has/remove/size/clear/members. Tags,
unlocked achievements, visited tiles. Shares the same table.
Values are int (also an entity handle / small id); Value.* covers richer
maps. [T; N] inline fixed arrays remain future work — typed buffers and []T
slices already cover heap-backed arrays.
Wired: parser splice trigger (g_uses_dict), emit_call dispatch, reseeded seed,
a self-asserting example (examples/library/containers.ludic + feat_case), and
per-symbol docs + inventory. All suites green incl. golden renders byte-
identical and the bootstrap fixpoint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Types phase 2 — the "money problem". A splice-on-demand bignum engine
(runtime/native/bignum.ludic, self-contained, only intrinsics) exposed as
two namespaces:
- BigInt.* — arbitrary-precision integer (sign-magnitude, base-1e9 limbs):
from/parse, add/sub/mul/pow, div/mod (by int), cmp/eq/is_zero, to_int, str.
For idle counters and exact huge currencies that overflow a 32/64-bit int.
- Decimal.* — exact base-10 fixed point (BigInt mantissa + decimal scale):
from/parse, exact add/sub/mul, cmp/eq, scale/rescale (truncate), str.
So 0.10 + 0.20 is exactly 0.30 — no binary rounding.
Both exact => deterministic; no f32/f64. Wired: parser splice trigger
(g_uses_bignum), emit_call dispatch, reseeded seed, a self-asserting example
(examples/library/bignum.ludic + feat_case), and per-symbol docs + inventory.
All suites green incl. golden renders byte-identical and the bootstrap fixpoint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 1 of the fuller type-system proposal: two by-value spatial types
that lower to packed integers (no heap, copy like scalars).
- IVec2 — integer 2D vector, a pair of int packed into one i64, for tile
and grid coordinates: make/zero/x/y/add/sub/scale/dot, the grid distance
manhattan, equal, and to_vector (widen into the fixed-point Vector).
- Rect — axis-aligned rectangle, four Q16.16 fixed components packed into
one i128, for HUD boxes and hitboxes: make/x/y/w/h, the derived
right/bottom/center, and the contains (point) / intersects (overlap) tests.
Both are exact and deterministic, bit-identical on every platform. Vector
and Color already cover phase 1's other 2D primitives.
Wired end to end: emit_core llty (IVec2->i64, Rect->i128), emit_call
dispatch, the FRAGS list + reseeded seed, a selfhost test (types2d),
per-symbol docs + type pages + inventory, and the vocabulary/editor sync
(header, JetBrains, TextMate, LSP).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the Http.* namespace and its transport, the HTTP client from #6. The JSON
companion the proposal called for already shipped as Json.* (#44).
- runtime/native/http.ll: the macOS transport — NSURLConnection driven through
the objc runtime C ABI (no ObjC/C source), run on a detached pthread so the
frame never blocks; TLS is the system's, on by default. A fixed slot pool holds
each in-flight request; the worker publishes status/body/response behind an
atomic done flag (release/acquire). Spliced + Foundation linked only when a
program uses Http.*.
- runtime/native/http.ludic: the Http.* runtime — get/post/request, the
open/set/body/send builder, poll/status/ok/text/body_len/header/free, plus a
pure-Ludic response parser (Http.parse + case-insensitive header lookup) that
is transport-independent and portable.
- compiler: Http.* dispatch, g_uses_http splice, hs_* transport intrinsics +
declarations, the conditional Foundation link, and a new \r string/char escape
the protocol needs.
- docs: a full docs/language/http section (16 pages); check-impl/check-docs green.
- test: examples/library/http.ludic self-asserts the parser offline (Darwin-gated
build via the canonical path, since it links Foundation).
Verified end to end against real endpoints: HTTPS GET (200 + headers + body) and
POST (body + custom header). HTTP is out-of-band and never feeds the
deterministic sim. Reseeded; suites green (81 + 29).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the Audio.* namespace and its platform backend, the audio subsystem #22 was
blocked on.
- runtime/native/audio.ll: the macOS backend, AVAudioPlayer driven through the
objc runtime C ABI (no ObjC/C source), same style as cocoa.ll — snd_load /
play / stop / playing / set_volume / set_rate. Spliced and linked with
AVFoundation only when a windowed build actually uses Audio.* (needed_framework,
since AVAudioPlayer is reached by name).
- runtime/native/audio.ludic: the Audio.* runtime — a handle table, master
volume/pitch, a single music channel. load/play/play_sound/play_music/stop/
stop_music/stop_all/volume/pitch/is_playing. Every native call is
is_windowed()-guarded, so a headless build carries the API as no-ops (load
returns 0, is_playing false) and needs no audio device.
- compiler: Audio.* namespace dispatch, g_uses_audio splice, snd_* intrinsics +
declarations, and the conditional AVFoundation link in both the canonical
(main.ludic) and dev-runner (x app) paths.
- docs: a full docs/language/audio section (10 method pages); check-impl green.
- test: examples/library/audio.ludic self-asserts the headless no-op path.
Playback is out-of-band and never feeds the deterministic sim, but triggers are
frame-driven so replays fire the same sounds. Reseeded; suites green (80 + 29).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire the macOS platform side of the #50 device layer, feeding the same state
buffers the read APIs consume — no API changes, purely OS glue.
- mouse: cocoa.ll reads the live cursor via mouseLocationOutsideOfEventStream,
converted to framebuffer pixels and y-flipped, so windowed games get
Input.mouse_x/y without injection (W_mx/W_my were never written before).
- gamepad: win_pad polls GCController.controllers each frame, packing extended-
gamepad buttons (SDL_GameControllerButton order) and thumbsticks (16.16 fixed,
Y negated for SDL convention) into in_pad_*. Windowed builds now load
GameController via -needed_framework (its classes are reached by name, so a
plain -framework link dead-strips it); DCE'd in headless builds.
- touch: the view's NSTouch phase handlers snapshot the touching set into
in_touch_* (normalizedPosition -> framebuffer pixels).
Web platform.js gains zero-fill stubs for win_held/mouse/pad/touch so a windowed
wasm build resolves the device-layer imports. New test asserts the windowed link
loads GameController. Reseeded; full + selfhost suites green (79 + 29).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The raw device layer the input proposal sketched, over the action maps +
record/replay of #7. Beyond one key per frame, gameplay can read:
- Multiple simultaneous held keys: Input.key_down / key_pressed / key_released,
with clean rising/falling edges (hold left AND jump).
- Analog from keys: Input.axis(neg, pos) and a normalized Input.vector(l,r,u,d)
(diagonals scaled by 1/sqrt(2)), plus Input.strength(action).
- Mouse: Input.mouse_x/y, mouse_dx/dy (per-frame delta), mouse_down(btn), wheel.
- Gamepads: Input.pad_connected/pad_button/pad_axis (SDL-order buttons, -1..1
sticks); touch: Input.touch_count/touch_x/touch_y.
The held set is fed by the platform when windowed — cocoa.ll now tracks
keyDown/keyUp into a 256-bit held-key bitset (win_held) and the mouse
buttons/wheel (win_mouse), gated so headless builds DCE the native calls — and
by the Input.press / Input.set_mouse / Input.set_pad / Input.set_touch injection
on every target (Godot-style action injection: replays, AI, network-fed input).
Input.record / replay now snapshot the full per-frame device state (held set +
mouse), extending #7's single-key tape.
Everything is integer and deterministic, so the same inputs reproduce the same
frame on every run and headless. The gamepad/touch native hardware bindings
(GameController.framework / NSTouch) feed the same injected state and are the one
remaining platform-glue follow-up; the software layer, semantics and replay are
complete and driven deterministically today.
Worked example + regression: examples/library/input_device.ludic
(1 1 0 1 0 1 71 -71 5 1 3 1 2 1 0 0 1, injection-driven headless). 23 new
docs/language/input pages. Full suite 78 passed, self-host C-free fixpoint
intact, no golden drift; cocoa.ll assembles and a windowed build links.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ergonomic layer over the engine-owned SpriteAnim/Motion systems (#43):
- Named clips: Anim.clip("run", frames, fps, mode) registers a clip by name and
Anim.play(entity, "run") plays it; Anim.play(entity, fps, frames, mode) sets
the clip directly. A name-keyed registry in systems.ludic.
- Frame events: Anim.on_frame(entity, frame) arms optional SpriteAnim
event_frame/event_fired fields; the engine flags the tick the clip first lands
on that frame, and Anim.fired(entity) reads it — the game reacts, so it stays
inside the no-runtime-dispatch event model.
- Motion.to(entity, from, to, dur, ease) starts a value tween over the Motion
component in one call (reflection-ABI writes, resetting the timer).
- Fluent Tween handles (runtime/native/tween.ludic): Tween.to / Tween.chain /
Tween.delay build a sequenced, disposable handle advanced by a new engine-owned
system (esys_tween, run each Update tick); Tween.value / Tween.done /
Tween.parallel / Tween.stop read and control it. The 1-arg Tween.done(handle)
is disambiguated from the 2-arg pure Tween.done(timer, dur).
Splicing: g_uses_anim_rt pulls in systems.ludic; g_uses_tween_rt pulls in
tween.ludic and inserts esys_tween into the Update phase. All integer and
deterministic, so animation and motion reproduce exactly under replay/lockstep.
Worked example + regression: examples/library/anim_sugar.ludic
(4 8 2 1 0 100 100 0 0 1 20 20 30 0 1). Twelve new docs pages (Anim, the new
Motion namespace, Tween handles). Full suite 77 passed, self-host C-free fixpoint
intact, no golden drift.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The remaining lighting tiers from the original proposal, all extending the
deterministic accumulation core (light.ludic) — no new ECS plumbing:
- Light.spot: cone / flashlight lights (direction + spread degrees), with a
self-contained integer atan2-in-degrees and a feathered edge.
- Light.falloff: a brightness-ramp exponent (1 linear, 2 quadratic, …) via
repeated fixed multiply.
- Light.soft: soft shadows — an area-sampled light so an occluder edge fades
through a penumbra instead of a hard cut.
- Light.gel + Light.clear_gel: colour cookies — a light gels from its centre
colour to a rim colour.
- Light.normal + Light.clear_normals + Light.height: a normal G-buffer so
surfaces shade by facing (N·L), not distance alone (tier 3).
- Light.time_of_day: a day/night ambient ramp from a single 0..1 value.
The engine lighting system (systems_light.ludic) consumes matching optional
Light2D fields — direction/spread/falloff/softness/gel — each defaulting off so
an older five-field Light2D lights exactly as before. Every tier is integer +
Q16.16 fixed, so scenes light identically on every run and headless.
Worked example + regression: examples/library/light_tiers.ludic (1 1 1 1 1 1 1 1 1).
Nine new docs/language/light pages. Full suite 76 passed, self-host C-free
fixpoint intact, no golden drift.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The two ideas the input revamp leads with, built in Ludic over the
single-key poll every target already provides:
- Action maps: gameplay reads named actions, not physical keys, so keys
are rebindable and a scheme is data. Input.bind(action, key),
Input.down/pressed(action), Input.rebind(action, from, to).
- Deterministic record/replay: Input.poll() is the one per-frame input
read; Input.record() captures the key each frame and Input.replay()
feeds the tape back, so a run reproduces exactly — the seed of lockstep
netcode. "Read input" and "read a recorded snapshot" are the same call.
runtime/native/input.ludic (spliced when the new Input.* methods are used;
pulls in core.ludic for rt_poll). emit_ns_call routes the methods to the
@fn_input_* runtime; parse.ludic gates the splice. Seven docs/language
pages; worked example + regression examples/library/input_actions.ludic
(1 0 1 1 0 1 0). Full suite 75 passed, self-host fixpoint intact, no golden
drift. The device layer (multi-key held, gamepads, touch, analog) needs a
platform key-state backend and is tracked separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ECS-native shape the 2D lighting follow-up asked for, built on the
engine-owned-system hook from #43. A torch is just an entity carrying
Light2D, a wall an entity carrying Occluder, and one Ambient entity sets
the night tint — the engine runs the whole deterministic light pass at the
end of the Render phase (ambient modulate -> carve occluder shadows ->
accumulate additive radial lights) and presents. No Light.* calls wired.
- runtime/native/systems_light.ludic: esys_light2d, consuming the components
through the by-name reflection ABI and reusing the #4 accumulation core
(light_ambient / light_occlude / light_point). Reads position from a
Position component when present, else the light's own x/y.
- Split from systems.ludic so a SpriteAnim/Motion-only game never links the
light pass; spliced (with light.ludic) only when Light2D/Occluder declared.
- emit_main now boots rt_init like the auto-loop/test runner, so an
entry-driven game that renders has its framebuffer allocated (headless:
allocate only, no window, byte-identical stdout for non-rendering games).
Worked example + regression: examples/library/light_ecs.ludic (32 1 32 1).
Full suite 74 passed, self-host C-free fixpoint intact, no golden drift.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the ECS hook issues #43 and #47 named as their real dependency: a
system the *engine* owns, inserted into the frame loop over a component a
game merely declares and carries — no `handler` wired.
- runtime/native/systems.ludic: esys_spriteanim (SpriteAnim frame advance:
loop/once/pingpong) and esys_motion (Motion value tween: linear/in/out/
in-out), both on the by-name reflection ABI, integer + deterministic.
- backend: emit_engine_systems_for_phase inserts the calls after every user
handler in a phase (auto-loop and the drivable tick helpers alike);
uses_engine_systems() drives the systems.ludic splice, the world-table
force-emit, and makes a component-only game count as a systems game.
- A game that declares neither component is byte-for-byte unchanged.
Worked example + regression: examples/library/anim_ecs.ludic. Full suite
73 passed, self-host C-free bootstrap fixpoint intact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A self-describing Value node (null/int/fixed/bool/str/list/object) with
constructors, builders (Value.add/put) and accessors (get/at/count/kind/
as_int/as_str/…). Reflect.serialize(entity) walks an entity's whole component
set into a value tree — one member per component, each a sub-object of its
fields — and Reflect.apply(entity, value) writes one back; a fixed field
becomes a fixed node, everything else an int node, so the round-trip is
bit-exact, with the model id under "@kind". Json.encode/parse bridge the tree
to and from compact, stable, diffable text, with fixed written as an exact
terminating decimal that parses back bit-for-bit (verified across the raw
Q16.16 range). Together: a one-call, bit-exact save/load for entities.
Written in Ludic and spliced on demand (runtime/native/value.ludic +
reflect_io.ludic, like Query/Light), so a program that doesn't touch
Value.*/Json.*/Reflect.serialize compiles byte-identically and the C-free
bootstrap fixpoint holds (verified). The general tagged-union/any language type
stays tracked in #1; this ships the concrete value tree the serializer needs.
Adds 21 namespace-method docs pages + Value/Json sections,
examples/library/serialize.ludic, and a regression case. Whole CI set green:
x test 72/72, x test-tools 30/30, check-impl/vocabulary/docs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A fallible function returns a `result` value, built with ok(payload) on success
or err(message) on failure. The caller recovers a value with `try EXPR else {
… }`: on ok the whole expression is the payload; on err the else block runs —
with the failure message bound to `error` — and its trailing expression supplies
the fallback. It is a plain branch on the result's tag: no exceptions, no hidden
control flow, nothing unwinds. is_ok(r) / is_err(r) classify without unwrapping.
Payloads are any i32-width scalar (int/fixed/bool/entity). The feature is
additive and only kicks in when ok/err/try are used, so untouched programs
compile byte-identically (verified) and the C-free bootstrap fixpoint holds.
Complements panic/assert from #8 (the unrecoverable half). The optional
top-level frame `recover` stays deferred (needs a frame-abort mechanism); the
full tagged-union/any generalization is tracked in #1.
Adds the `try` keyword and ok/err/is_ok/is_err builtins across the compiler,
the vocabulary header, JetBrains + TextMate/VSCode grammars, the docs inventory
and pages, examples/library/recover.ludic, and a regression case.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Instrument each emitted statement with a per-source-line hit counter, gated
behind a new --coverage flag (default off) so ordinary builds — and the
compiler's own self-compile — stay byte-identical and the C-free bootstrap
fixpoint is untouched. A static line table plus a parallel hit-counter array
are dumped at exit through an atexit hook to $LUDIC_COVERAGE (default
ludic.cov) as a `FILE <name>` header and `<line> <hits>` rows.
bin/x test --coverage compiles the test specs with instrumentation, runs them
into per-file dumps, and aggregates a clean per-file line-coverage report that
names the unreached lines. Adds examples/library/coverage.ludic (a spec whose
tests deliberately miss one branch) and docs. Closes the last open acceptance
item of the testing framework (#12).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
RFC decision (the split the issue recommended): programmer bugs abort loud and
located; recoverable failures become values. This ships the first half.
panic(msg) prints `file:line: panic: <msg>` to stderr and aborts the process with
exit code 1 — a clear, located error instead of a segfault or a silent wrong
result. assert(cond, msg) is the guarded form: it aborts with `file:line:
assertion failed: <msg>` only when cond is false, otherwise execution continues.
The location is baked in at compile time (the call node carries its source line,
g_src_name carries the file); the message is any string.
Both lower in emit_call to an fprintf-to-stderr + exit(1) + unreachable tail
(assert branches on the condition first). @fprintf and the format constant are
declared on demand (g_uses_panic), so a program that never panics is unchanged —
and the compiler's own source uses neither, so the C-free bootstrap fixpoint holds.
- panic/assert registered as builtins across the vocabulary (ludic_syntax.h, the
JetBrains lexer, the TextMate grammar) and documented (docs/language/builtins/)
- examples/library/errors.ludic covers the success path (asserts hold, program
runs to the end); a panic_case in the suite covers the failure path (non-zero
exit + the located stderr message). x test is now 69 checks.
Deferred: recoverable failures as `try`/`else` values (needs the tagged-union
type system, #1) and a top-level `recover` for the dev game loop.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A `test "name" { ... }` top-level block is discovered automatically and run by a
synthetic runner @main — no `entry` to write, nothing to register. Inside a test,
expect(cond) / expect_eq(a, b) / expect_near(a, b, tol) assert; on failure they
print `file:line: <what> failed (got G, want W)` and set a per-test fail flag but
keep going, so one run reports every failure. The runner prints `ok - name` /
`FAIL - name` per test, a `== N passed, M failed ==` summary, and exits non-zero
if any test failed — so `ludic spec_test.ludic` drops straight into bin/x and CI.
expect_near carries the tolerance fixed-point / accumulated-integer game math need.
Frontend: new `test` keyword (parse_test -> N_TEST, collected in g_tests) and the
call node now carries its source line for the file:line messages. Backend:
emit_test_runner synthesises @fn__test_i bodies + the runner @main; the expect*
builtins lower to a branch-print-flag tail (emit_expect_fail). g_src_name (set in
main from the input path) supplies the filename. The compiler's own source has no
`test` blocks, so its self-compiled IR is unchanged and the C-free fixpoint holds.
- `test` wired into the vocabulary (ludic_syntax.h, JetBrains lexer, TextMate
grammar) and documented (docs/language/testing/)
- examples/library/testing.ludic: a passing spec, guarded by a new spec_case in
the regression suite (build, run, require exit 0 + the expected summary)
Coverage instrumentation (the biggest lift in #12) is left as a follow-up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A software light pass over the framebuffer, run in a render phase after drawing
the scene: Light.ambient multiplies the scene toward a tint (night/cave mood),
Light.point additively accumulates a radial glow with linear falloff clamped per
channel, and Light.occlude / Light.clear_occluders cast hard shadows by blocking
a light's rays against rectangular occluders. Integer + Q16.16 fixed throughout,
so a scene lights identically every run and in a headless render (diffable).
Engine in runtime/native/light.ludic, spliced on demand (g_uses_light) like the
regex/query runtimes; namespace wired in emit_call.ludic. Ships issue #4 tiers 1
(ambient + additive radial lights) and 2 (hard shadows). Normal-mapped sprites,
soft shadows, a day/night directional light, and auto-consuming Light2D/Occluder
components are follow-ups (the auto-system hook is tracked by #43).
- runtime/native/light.ludic: the light-accumulation engine (isqrt falloff,
segment/occluder shadow test, ambient modulate)
- examples/library/lighting.ludic: 14 pixel-readback assertions
- docs/language/light/: Light.ambient/point/occlude/clear_occluders
- tools/x/test.ludic: lighting.ludic in the regression suite
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the transform/state-based rendering #23 tracked as blocked on new
renderer state. All of it threads through the two framebuffer chokepoints every
draw primitive already funnels through (rt_put_px / rt_fill_rect), so one place
gives the whole draw API a camera, a clip rect, and a blend mode. Defaults are
neutral — camera (0,0), clip = full screen, blend = replace — so every existing
golden render is byte-identical (the 60+ render tests still pass unchanged).
New renderer state (runtime/native/core.ludic):
- Screen.camera(x, y) / Camera.set(x, y) world-space draw offset; a world
point draws at (wx-x, wy-y). Moves
everything — reset to (0,0) for a HUD.
- Camera.follow(x, y, lerp) ease the offset toward centring a
target (fixed lerp 0..1)
- Camera.shake(amount) +/- amount jitter from the seeded RNG
(replay shakes identically); 0 clears
- Screen.clip(x,y,w,h) / clip_reset() screen-space clip rectangle
- Screen.blend_mode(m) 0 = replace, 1 = additive (clamped)
New primitives:
- Screen.oval(x, y, rx, ry, color) axis-aligned ellipse outline (midpoint)
- Screen.measure_text(text) -> int advance width in the 5x7 font
- Screen.pixel(x, y) -> int read a framebuffer pixel (0x00RRGGBB)
Everything stays integer and deterministic (the camera, shake, and blend all
reproduce exactly under identical inputs), so headless renders remain diffable.
Camera.follow interpolates in the fixed domain (fixed*fixed then floor) to avoid
the int*fixed coercion trap.
Screen.pixel makes the whole surface testable by reading rendered pixels back:
examples/library/render.ludic asserts 18 cases — pixel round-trip, camera and
Camera.set/follow offsets, clip in/out + reset, additive blend with 255 clamp,
oval extremes vs hollow centre, and text measurement — all verified against the
actual framebuffer, not just that the call compiled. Wired into x test (now 65
passed). Docs: 7 new Screen pages + a Camera section with 3 pages,
inventory/coverage green. Seed reseeded; the C-free bootstrap fixpoint holds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Runtime type reflection: enumerate properties and fields by index, resolve ids
by name, read a field's type, and get/set/has an entity's fields generically —
the foundation the issue calls out for auto-serialization, data-driven tools,
and debug/inspector overlays. Built on the existing EV2 reflection ABI plus a
new EV8 metadata-enumeration layer, all generated at compile time (a table walk,
no heavy runtime introspection), so a binary that never reflects pays nothing.
Surface (Reflect.*, aliased in emit_call.ludic over the world_* reflection ABI):
- Reflect.prop(name) / field(prop,name) resolve ids by name (-1 = none)
- Reflect.prop_count() / prop_name(i) enumerate properties
- Reflect.field_count(prop) / field_name(prop,i) / field_type(prop,i)
enumerate a component's fields
- Reflect.get / set / has (entity, prop, ...) read/write/test a field by id
- Reflect.kind(entity) / model(name) an entity's model, by id/name
New codegen (emit_world.ludic, EV8): ludic_prop_count / prop_name /
field_count / field_name / field_type, generated the same way as ludic_prop_id
— a switch over the compile-time property/field metadata, falling through to the
mod-registered (dynamic) registries. Field names/types come straight from the
AST, so field_type reports the declared type ("int"/"fixed"/…). A program that
uses Reflect.* force-emits the reflection ABI (g_uses_reflect) so it needs no
@events of its own, exactly like Query.* (#42).
examples/library/reflect.ludic asserts 20 cases including a generic inspector
that sums every field of every component an entity has while naming none of them
— the auto-save / debug-overlay pattern end to end. Wired into x test (now 64
passed). Docs: a Reflect section + 12 per-symbol pages (positioned as an
advanced/tooling surface), inventory/coverage green. Seed reseeded; the C-free
bootstrap fixpoint holds.
Scope: this lands the reflection core and a real consumer (the generic
inspector). The generic value-tree `serialize` the proposal also sketches wants
a tagged-union/any value type from the #1 type-system work, so it is tracked as
a follow-up rather than forced in here.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the half of #24 that was explicitly deferred as blocked: entity-space
queries to sit alongside the grid-space Grid.*/pathfinding that shipped in
07e5a20. Query.* answers questions about the live entities that carry a
property, built directly on the EV2 reflection ABI (world_query_next/world_get):
- Query.count(prop) -> int how many live entities carry prop
- Query.first(prop) -> int the lowest-id bearer, or -1
- Query.nearest(prop, pos, xf, yf, x, y) the bearer closest to (x,y), or -1
- Query.within(prop, pos, x, y, r, xf, yf) -> []int every bearer within r
prop is a property id (World.prop_id); the spatial forms read a position from a
coordinate property `pos` at two int field ids (World.field_id), so `prop` can be
a discriminating tag distinct from the position component ("nearest Enemy"), or
the same id to query the coordinate component itself. Distances are exact squared
integers (no sqrt), ties break to the lower entity id, and `within` returns
entities in ascending id order — so every answer is deterministic and replay-safe.
The engine (runtime/native/query.ludic, ~55 lines of Ludic, C-free) is a linear
scan over the entity table — ample for the entity counts Ludic targets, the same
reasoning as the grid pathfinder's open set; a bucketed/quadtree index is a
future optimisation, not a correctness need. It is spliced on demand when the
parser sees Query.* (g_uses_query), which also force-emits the reflection ABI so
a Query program needs no @events of its own (previously the ABI required them).
examples/library/query.ludic asserts 18 cases over five entities at known
positions (count/first with a component filter, nearest with a separate tag vs
position property, within radii incl. r=0 and the empty-property case), wired
into x test (now 63 passed). Docs: a Query section + 4 per-symbol pages,
inventory/coverage green. Seed reseeded; the C-free bootstrap fixpoint holds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two ECS-native, deterministic namespaces for 2D motion, driven off the fixed
frame clock so replays and lockstep netcode reproduce every frame and every
eased value exactly. Both are pure computed-inline Q16.16 / integer math (no new
runtime, no heap) — the game stores a timer on a component and calls these each
frame, exactly the way Collision.* / Grid.* are used.
Anim.* — spritesheet frame animation:
- Anim.frame(timer,fps,count) -> int looping frame index
- Anim.once(timer,fps,count) -> int one-shot, clamps on the last frame
- Anim.pingpong(timer,fps,count) -> int bounce 0..count-1..0
- Anim.finished(timer,fps,count) -> bool has a one-shot run past its end?
- Anim.duration(fps,count) -> fixed seconds for one cycle
- Anim.cell_x/cell_y(frame,cols,cell) -> int source rect on a grid sheet
Tween.* — value interpolation over a timeline:
- Tween.progress/loop/yoyo(timer,duration) -> fixed normalized amount
- Tween.done(timer,duration) -> bool
- Tween.ease(t, mode) -> fixed shape by a literal curve 0..6,
the same curves as Ease.* (now
factored into a shared ease_eval)
- Tween.number/round/point/tint(from,to,t) blend a fixed / int / Vector / color
The typed blends reuse the existing fixed / Vector / color helpers, and
Tween.ease shares Ease.*'s exact formulas via the new ease_eval(mode,t) — one
source of truth for every easing curve in the engine.
examples/library/anim.ludic asserts 34 cases (frame math, clamping, ping-pong,
cell geometry, timeline clamp/loop/yoyo, rounding, color/vector blends, and
Ease.in == Tween.ease(.,1)); wired into x test (now 62 passed). Docs: Anim +
Tween sections with 16 per-symbol pages, inventory/coverage green. Seed
reseeded; the C-free bootstrap fixpoint holds.
The stateful sugar the proposal sketches (named clips, Anim.play, fluent
Tween.chain/parallel handles, and an auto-injected advance system) is deliberately
left as a follow-up — this lands the deterministic math core both halves stand on.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Grid.* operates on the Map tilemap (Map.size/Map.row): a cell is passable unless
it is out of bounds or holds the caller's `wall` tile (a char code, e.g. '#'), so
any impassable glyph works. Everything is integer and deterministic.
- Grid.line(x0,y0,x1,y1) -> []Cell Bresenham line cells (LOS/raycast base)
- Grid.blocked(x,y,wall) -> bool the shared passability test
- Grid.line_of_sight(x0,y0,x1,y1,wall) unobstructed straight line?
- Grid.flood(x,y,wall) -> []Cell 4-connected reachable region (BFS)
- Grid.a_star(x0,y0,x1,y1,wall) -> []Cell shortest 4-connected path (A*,
Manhattan heuristic), empty if unreachable
The engine (runtime/native/grid.ludic, ~150 lines of Ludic, C-free) is spliced
into a game via core.ludic since it reads the tilemap runtime; returned Cell
slices are ordinary Ludic slices (`len` / `[i]`; each cell has `.x` `.y`).
Pathfinding lives under Grid rather than a `Path` namespace — that name is
already the filesystem-paths library (#10).
Verified against Python references: a 1500-case fuzzer over random maps agrees
exactly on A* path length (optimal, == BFS), flood-fill count, and line-of-sight.
examples/library/grid.ludic asserts the behaviour and is wired into `x test`
(now 61 passed); docs: a Grid section + 5 per-symbol pages, inventory/coverage
green. Seed reseeded; the C-free bootstrap fixpoint holds.
Scope: this lands the Grid.*/pathfinding half of #24. The ECS Query.* helpers
(count/first, and nearest/within which want a runtime spatial index) remain the
tracked follow-up the issue calls out as blocked.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A regular-expression library with PCRE/PECL-compatible syntax, implemented as a
Thompson NFA / Pike VM so a bad pattern from a modder can NEVER cause
catastrophic backtracking — matching is O(n·m), never exponential. `(a+)+$` on
40 non-matching chars, `(a*)*b`, `(.*a){20}b` all run in microseconds; a 50 KB
input scans in ~7 ms.
The engine (runtime/native/regex.ludic + regex_vm.ludic, ~700 lines of Ludic, no
C) parses a pattern to a small bytecode program — an unanchored lazy `.*?` prefix
makes a plain search match anywhere — and the VM runs every alive thread in
lockstep per input byte, deduped by program counter and carrying capture slots
(save/restore, leftmost-greedy priority). Supported: literals, `.`, classes
`[...]` (ranges, negation, `\d \w \s` and their negations), anchors `^ $`,
alternation `|`, capturing and `(?:…)` groups, and `* + ? {n} {n,} {n,m}` in
greedy or lazy form, plus the common escapes; numbered capture groups. Errors are
values — an invalid pattern compiles to null, never a crash. Backreferences and
look-around are out of scope for a linear engine, and on the degenerate case of a
nullable subpattern under an unbounded quantifier positions may differ from a
backtracking engine (the price of the linear-time guarantee) — documented.
Surface (Regex.*, aliased in emit_call.ludic to the regex_* functions):
compile / valid / matches / test / find / exec / next / replace / group /
group_count / start / end / ok.
The runtime is spliced on demand: the parser sets a flag when it sees `Regex.`
and maybe_splice_runtime imports the engine — so it costs nothing in a program
that doesn't use it and works in a plain tool (not just an ECS game).
Verified against Python's `re` as an oracle: a 20k-case grammar fuzzer agrees
100% on realistic patterns (0 / 15000 with capture groups) and 99.8% on group-0
spans across the full pathological grammar, the residual being the documented
nullable-quantifier case. examples/library/regex.ludic asserts the behaviour
(wired into `x test`, now 60 passed); docs: a Regex section + 13 per-symbol
pages, inventory + coverage green. Seed reseeded; the C-free bootstrap fixpoint
holds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to #31: the doc/lint/grammar checks moved to Ludic there; this ports
the remaining docgen piece (gen.py / check.py / palette.py) so nothing in the
documentation pipeline is Python any more.
Three new `x` subcommands, all in Ludic and compiled by Ludic:
- x docs-gen [--out DIR] the static-site generator: parses docs/language/**
front-matter + bodies (fences, Parameters:), builds the section/symbol
model, reads the asset templates, and emits every page + ns/color/api pages
+ the landing page + ludic-highlight.js + symbols.json + .nojekyll.
- x docs-check [DIR] the coverage / integrity guard (required files, a
page per inventory.json symbol, duplicate-token and one-dir-per-namespace
guards, highlighter link targets).
- x docs-palette the named-colour source of truth: the palette table
moved into tools/x/docgen.ludic, emitting emit_color.ludic (pointer, not
ptr) + palette.json.
Verified against the Python oracle: `x docs-gen` reproduces all 466 output files
BYTE-FOR-BYTE (a Ludic json.dumps/html.escape/front-matter port — ordered dicts,
indent=2 vs compact, ensure_ascii \uXXXX, codepoint-aware truncation), and
`x docs-check` matches check.py's pass/fail output. Wired into `x test` as a
gate (docs-gen -> docs-check on a fresh site; docs-palette stays byte-identical).
CI swap: ci.yml and docs.yml call the Ludic generator; docs.yml drops the
python:3.12 container and bootstraps the toolchain from the IR seed instead.
tools/docgen/{gen,check,palette}.py deleted; only assets/ + inventory.json
remain. Completes #31's criterion 3.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
palette.py generates selfhost/backend/stdlib/emit_color.ludic but its
template wrote `function color_lookup(name: ptr)`, while the committed,
correct source (and the rest of the compiler) uses `pointer` — so running
the generator rewrote the file to a drifted version. Emit `pointer`;
`python3 tools/docgen/palette.py` now leaves emit_color.ludic byte-identical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`new` accepted only a bare `new T` (every field its declared default) or
`new []T`, but the docs (kw-new) document `new Record { field: value, … }`
as the way to construct a record with non-default fields — a documented,
intended form the parser never accepted (`let o = new Point { x: 3 }` failed
with "expected newline or ';'").
Parse an optional `{ … }` override record after the type in a `new`
expression (reusing the existing `record()` parser that `spawn` uses), and
seed each field in emit_new_struct from that record when present, else from
the field's declared default. `new []T` and bare `new T` are unchanged.
Also mark the illustrative kw-import fence `# doc-check: skip` (its imports
are example paths that can't resolve in isolation), which makes `x check-docs`
fully green (398 fences, 0 drifted) — so it is now wired as a gate in
`x test-tools` and CI, guarding against future doc/compiler drift.
Reseed is a clean fixpoint (x bootstrap-cfree holds); x test (56),
x selfhost-test (29, golden renders unchanged) and x test-tools (30) green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the Python doc/lint/vocabulary guards with Ludic equivalents that
run through the `x` task runner, so the checks need no Python interpreter:
x check-docs every ```ludic doc fence parses (or is marked)
x check-impl every implemented feature has a docs/language page
x check-vocabulary vocabulary in sync across grammar / lexer / header / parser
x lint-asset <file> validate one editor .json / .xml asset
New fragments: tools/x/json.ludic (a small JSON reader — objects/arrays/
strings with \uXXXX + surrogates/numbers/literals, used by the vocabulary
check's grammar navigation and the asset validator) and tools/x/checks.ludic
(the checks + string helpers + a minimal XML well-formedness validator).
`x test-tools` now runs the vocabulary + docs-coverage checks and the
JSON/XML asset validation through Ludic instead of python3; ci.yml's
docs-coverage step calls `x check-impl` / `x check-vocabulary`. Each port was
verified against its former Python script for exact verdict parity on the
clean tree and on injected drift (a removed keyword, a broken grammar
alternation, an undocumented method).
Deletes the superseded scripts: tools/check-vocabulary.py, tools/check-docs.py,
tools/docgen/check-impl.py, tools/docgen/validate.py. The docgen site
generator (gen.py/check.py/palette.py) and the LSP protocol driver
(test-lsp.py) remain and are tracked separately.
Toolchain unchanged (seed byte-identical); `x test` (56) and `x test-tools`
(29) stay green.
Part of #31
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The repository root carried 11 large Markdown files (~330 KB); most were
long-lived design records rather than things a newcomer needs on first
contact, which buried the README and mixed "how to use Ludic" with "how we
decided to build it."
Move the design/roadmap docs to the Forgejo wiki (now enabled and
populated): Events, Networking, Scenes, Lifecycle, Mobile and
Syntax-redesign design records, the Bootstrap deep-dive and the Luanti
roadmap, under a Home index + sidebar. Each page had its selfhost/ source
links corrected for the #29 reorg and every repo-relative link rewritten to
an absolute URL on main so it resolves from the wiki.
All eight were current, actively-maintained records, so none were dropped.
The root now holds README.md plus the two user-facing references,
LANGUAGE.md and COMPILING.md; the README links to the wiki, and the
remaining references in LANGUAGE.md / COMPILING.md / examples/README.md and
the emit_net.ludic header comment point at the wiki pages. The emit_net.ludic
change is a comment only — the seed stays byte-identical and bootstrap-cfree
+ the full suite (56) stay green.
Closes#26
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Split the flat 38-file selfhost/ into concern-based subdirectories:
frontend/ lex, parse, parse_game, ast
support/ str, buf, io
backend/ core IR + expression/statement lowering
backend/game/ ECS/scene/event/world lowering
backend/stdlib/ the namespaced Math.*/Text.*/Crypto.*/… intrinsics
and split the three oversized emitters at responsibility boundaries so
no file mixes concerns:
emit_game.ludic -> + emit_world.ludic (reflection world table,
tick helpers, @main synthesis)
emit_expr.ludic -> + emit_call.ludic (namespaced builtins, call
lowering, expr dispatch)
emit_text.ludic -> + emit_text_prelude.ludic (emitted string-builder runtime)
FRAGS in tools/x/selfhost.ludic is updated to the new paths with the link
order preserved, and the Python doc/vocabulary tooling is updated to walk
the new layout. Because the build is a plain in-order concatenation and
every split lands on a blank-line boundary, the regenerated seed is
byte-identical: `x reseed` leaves selfhost/ludicc.seed.ll unchanged,
`x bootstrap-cfree` still reaches its fixed point, and both `x test` (56)
and `x selfhost-test` (29, incl. golden renders) stay green.
Closes#29
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>