63 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 31cfbc2465 |
feat(rendering): add Screen.camera/clip/blend_mode/oval + Camera.* + Screen.pixel (#23)
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> |
|||
| 12f2dbe958 |
feat(types): add Reflect.* — runtime reflection over the world schema (#20)
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>
|
|||
| b25dc328a2 |
feat(stdlib): add Query.* — ECS spatial queries over the reflection ABI (#42)
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
|
|||
| e4d1e95dcb |
feat(stdlib): add Anim.* + Tween.* — deterministic 2D animation & tweening (#5)
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>
|
|||
| 07e5a20c0e |
feat(stdlib): add Grid.* — tile geometry + A* pathfinding over the tilemap (#24)
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>
|
|||
| b798e3024e |
feat(stdlib): add Regex.* — a linear-time regular-expression engine (#18)
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>
|
|||
| dd4f5a26ad |
feat(stdlib): add Fs.* / Path.* / Mime.* — filesystem, paths, content types (#10)
All checks were successful
docs / build-and-deploy (push) Successful in 2s
A cohesive filesystem & IO library — the foundation for saves, config, mods, and
asset loading — wrapping the bare file_* builtins into one safe, ergonomic API a
non-expert can use without touching a file descriptor or a byte buffer.
Path.* join / dir / base / ext / stem / normalize (pure lexical string ops)
Fs.* exists / is_dir / read_text / write_text / append_text / remove /
size / mkdir / copy / list
Mime.* of (extension table) / sniff (magic bytes: PNG/JPEG/GIF/PDF)
Pure string IR for Path.*; libc (fopen/access/mkdir/rename/opendir…) for Fs.*;
C-free, emitted on demand (g_uses_fsrt). Safety and determinism baked in:
- write_text and copy are atomic (write a temp file, then rename over the target)
so a crash mid-write never corrupts the previous file;
- mkdir creates parents (mkdir -p);
- list is sorted for a stable, reproducible directory walk;
- fallible calls return values (null / false / -1), never crashes — ready for a
first-class try/else when the error-handling work lands.
Complements Os.* (#21): Os supplies per-user locations, Fs the operations. v1
targets the native macOS/BSD filesystem with "/" separators; Windows separators,
a sandboxed wasm virtual FS, recursive directory copy, and richer magic-byte
sniffing are documented follow-ups.
- examples/library/fs.ludic: 32 assertions across pure Path ops (incl. normalize
resolving ./ .. and duplicate slashes), a real create/read/append/copy/list/
remove cycle under build/, and Mime by-extension + by-magic (GIF signature vs a
.bin extension). Wired into `x test` (now 56 passed).
- docs: new Path, Fs, and Mime sections + 18 per-symbol pages; inventory updated;
every fence passes check-docs; site builds via docgen.
- seed regenerated; `x bootstrap-cfree` fixpoint holds.
Closes #10
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|||
| bad6cd1ac9 |
feat(stdlib): add Unicode.* — UTF-8 code points, graphemes, case mapping (#13)
All checks were successful
docs / build-and-deploy (push) Successful in 2s
Make Ludic text correct-by-default over UTF-8, so player names, translated UI, and chat behave for every language instead of counting bytes and splitting characters in half. The byte-oriented Text.* stays for speed; Unicode.* is the layer that understands code points and (approximately) grapheme clusters. - len / byte_len code points vs bytes — the two lengths, kept distinct - is_valid_utf8 strict validation of untrusted input - char_at / chars code-point access by index; chars() -> []int - upper / lower case mapping (ASCII + Latin-1) - truncate first n code points, never a half-character - grapheme_len user-perceived characters (approx UAX#29) Pure integer/byte IR over NUL-terminated buffers; C-free, no data-table blob. Decoding and validation cover the full UTF-8 range (overlong/surrogate/>10FFFF rejected). grapheme_len collapses combining marks, variation selectors, ZWJ sequences (family emoji), and regional-indicator flag pairs. Documented v1 scope: wider-script/locale case rules (Latin-Extended, Greek, Cyrillic, Turkish i, German ß) and NFC normalization are follow-ups. - examples/library/unicode.ludic: asserts the invariants across ASCII, Latin-1 (é round-trips through upper/lower), a decomposed "café" (5 code points, 4 graphemes), a ZWJ family emoji (5 code points, 1 grapheme), and a flag (2 regional indicators, 1 grapheme). Wired into `x test` (now 55 passed). - docs: a new Unicode section + 9 per-symbol pages clarifying byte vs code point vs grapheme; inventory updated; every fence passes check-docs; site builds. - seed regenerated; `x bootstrap-cfree` fixpoint holds. Closes #13 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
|||
| b205dd8dfd |
feat(stdlib): add Os.* — the OS/environment interface (#21)
All checks were successful
docs / build-and-deploy (push) Successful in 2s
An Os.* namespace, Go-flavored and game-scoped, for the environment *around* the game: the command line, environment variables, standard streams, process exit, the host platform, and the per-user known folders a game writes into. Rounds the bare System.* builtins (arg/getenv/exit) into one coherent surface. - args / arg_count / arg the argument vector (args() -> []string) - env / env_or / has_env read env vars (null-safe via env_or) - set_env / unset_env mutate this process's environment - exit(code) terminate with a status code - platform() / arch() host facts (uname sysname/machine) - stdout_write / stderr_write raw writes to the standard streams - save_dir / config_dir / cache_dir / temp_dir per-user known folders Pure libc over NUL-terminated strings; C-free, no new runtime. arg_count/arg/ exit stay light (no prelude) as thin aliases of the existing intrinsics; the rest share one Os runtime prelude emitted on demand (g_uses_osrt). platform() is portable (uname system name is field 0 on every Unix); arch() and the known-folder layout follow the macOS/BSD conventions — the fully supported native target today. Linux/Windows/wasm folder resolution and a target-aware arch() are documented follow-ups. - examples/library/os.ludic: asserts the invariants that hold regardless of host — env round-trip, env_or fallback, unset, args()==arg_count(), non-empty platform/arch and known dirs. Wired into `x test` (now 54 passed). - docs: a new Os section + 17 per-symbol pages; inventory updated; every fence passes check-docs (--fmt) and the site builds via docgen. - seed regenerated; `x bootstrap-cfree` fixpoint holds. Closes #21 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
|||
| 031b138f84 |
feat(stdlib): add Log.* — levelled, structured logging (#15)
All checks were successful
docs / build-and-deploy (push) Successful in 2s
A Log.* namespace: five levels (trace/debug/info/warn/error), a runtime threshold, and structured key=value fields, so games get something better than scattered print calls and release builds can go quiet without touching call sites. - Log.trace/debug/info/warn/error(msg, [k, v]...) -> stderr, "[LEVEL] msg k=v" - Log.set_level(n) show only level >= n (0 = all default, 5 silences all) - Log.level() read the current threshold Fields accept strings, ints, and longs (numbers formatted automatically); the level tag is chosen at compile time so a filtered-out level costs only a comparison. Writes to stderr, never touching the simulation — no effect on determinism/replays. v1 is the console sink; rotating-file and in-engine overlay sinks are noted as follow-ups. - examples/library/logging.ludic: asserts the set_level/level threshold round-trip and that every level (with mixed-type fields) runs without faulting; the stderr gating itself was verified by hand (warn/error emit, lower levels suppressed). Wired into `x test` (now 53 passed). - docs: a new Log section + per-symbol pages; inventory and coverage pass. - seed regenerated; `x bootstrap-cfree` fixpoint holds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
|||
| a4f1494a04 |
feat(stdlib): add Noise.* — deterministic fixed-point procedural noise (#3)
All checks were successful
docs / build-and-deploy (push) Successful in 2s
A Noise.* namespace for procedural generation, implemented entirely in Q16.16 fixed point over an integer permutation hash so a seed reproduces the exact same field on every platform and run (native/headless/wasm) — the determinism edge over float noise that drifts across CPUs. - value2 / perlin2 / simplex2 — value, gradient, and simplex noise -> [-1,1] - fbm2(x,y,seed,octaves) — fractal Brownian motion (octaves of simplex) - cellular2 / cellular2_id — Worley F1 distance + nearest-cell id - unit(n) — remap [-1,1] -> [0,1] Covers issue phases 1–2 fully plus cellular from phase 3; domain warp, ridged/ billow, and sample1/sample3 remain as follow-ups. Pure integer IR, C-free; cellular/fbm reuse the math prelude's fx_sqrt. - examples/library/noise.ludic: asserts the invariants a fixed-point generator must hold (Perlin == 0 at lattice points, every sampler within [-1,1], reproducibility, seed sensitivity, non-negative cellular distance). Wired into `x test` (now 52 passed). - docs: a new Noise section + per-symbol pages; inventory and coverage pass. - seed regenerated; `x bootstrap-cfree` fixpoint holds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
|||
| 2ddf830f0b |
feat(stdlib): finish Crypto (CSPRNG + base64) and add Uuid.* library (#19 #16)
All checks were successful
docs / build-and-deploy (push) Successful in 2s
Crypto (#19): add the OS cryptographically-secure random surface (random_bytes/random_hex/random_u32, reading /dev/urandom) and a standard base64 encoder, completing the library alongside the existing SHA-256/ HMAC-SHA256/verify_hmac/hex/ct_equal. All pure integer IR, C-free. Uuid (#16): a new namespace for stable, collision-free IDs — v4 (random) and v7 (time-ordered) generation, plus parse/is_valid/to_text/equals/nil. UUIDs are canonical lowercase 36-char strings; v4 and v7's random tail draw from the crypto CSPRNG, so both carry the documented determinism caveat (mint at the edges, never inside lockstep simulation). Reuses the crypto prelude's fn_secure_bytes / fn_hex_encode. - examples/library/{crypto,uuid}.ludic: known-answer vectors (SHA-256, HMAC, base64 per RFC 4231/4648) and structural invariants (uuid version/variant bits, parse/equals), wired into `x test` (now 51 passed). - docs: per-symbol pages for every new method + a new Uuid section; inventory and impl-vs-docs coverage check pass. - seed regenerated; `x bootstrap-cfree` fixpoint holds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
|||
| fb728bbefe |
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> |