Commit graph

9 commits

Author SHA1 Message Date
d9612ba2a6 ludic.base: a queue's fact records come round again (q_rec / q_ring_add), used by effects, clock, wallet, weather, tracks, needs, fire, settings, update, steps, crafting, shop, gear, hints, session and photo instead of a new record per fact; the minimap's marks cleared and rubbed out in place, its scale steps without a list; the clock's dice seeded again, not made again; once-per-join, per-save and pool-miss paths declared
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 20:03:46 +03:00
2d1d06899e ludic.base: tb_reserve and imap_reserve size a table up front; HandlePool (a slot and a generation, nothing made after hd_pool_new, past its capacity a panic naming it); kept_push is the one declared grow, and every growth path left says its bound in @alloc_ok
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:58:11 +03:00
61a6f3c827 ludic.base: TextRing - a fixed ring of text buffers written in place, for a line made per event
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 13:35:34 +03:00
a26cc2eea7 ludic.base: StrBuf and StrTable - a line written into a kept buffer and interned, one string per distinct text
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 11:04:07 +03:00
f3fec88888 feat(ecs): chunks as a grouping - IntIndex takes sparse values; ludic.things' chunk column and things_drop_chunk
A streamed world loads and lets go of a chunk's entities together, so a chunk is a first-class
group: chunk_of(x, z, size) packs a chunk's cell into an int, tb_remove_all(tb, ix, v) removes every
row an index files under one value (the observers told of each). IntIndex kept one list per value
up to the largest, which a packed chunk number (hundreds of millions) turned into hundreds of
millions of empty lists and a 12 GB test run; a value past 1024 now gets its slot through an IntMap,
so a sparse value costs one list.

ludic.things files every Thing under its 256 m chunk (TH_CHUNK), kept by every move;
things_chunk_count and things_drop_chunk (the port and the facts told of each removal, as
thing_remove tells them) are what the memory budget's streamed chunks ask. The moves are their own
file (moves.ludic). Tests: ludic.base 35, ludic.things 8, all under a 2 GB cap (24 MB peak).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 22:07:26 +03:00
52270ab5d1 feat(ecs): observers and block change stamps on a Table
tb_on_add / tb_on_remove take a fn(int) (its states supplied, like a system's) told each handle as
its row is made and before it goes (ecs_hooks.ludic), so what follows a table - a drawing, a
message - does so without a scan. Every row's tick now also stamps its 64-row block, and
tb_changed_since / tb_added_since skip the blocks nobody wrote since: a delta over a large table
costs the blocks that moved. A row moved into a removal's gap keeps its own tick (it was not
written); a removal is told by the hook. ecs_test holds both. The ludic.wildlife table
(5ac3d48, written while builds were held) now builds and passes its 19 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:47:06 +03:00
f802bdd3ec feat(ecs): ludic.base Table<T> - dense rows of hot columns, generational handles, a spatial grid, kind indexes and an id map kept current by the setters; ludic.things on it
A mechanic that keeps many of something keeps them as rows of a Table<T> rather than a list it
scans. Removal swaps the last row in; a handle (22-bit slot, 9-bit generation) goes stale when its
entity is removed. tb_set_f / tb_set_xz / tb_set_i stamp a change tick and refile the row in every
index over that column in O(1): tb_grid (a doubly linked spatial hash, rings outward for nearest,
rehashing as it grows), tb_index (a cached query: the rows of each value of a kind column, gated by
an active column), tb_nearest_of / tb_within_of (a rare kind from its own list), tb_nearest_where
(a predicate on the record), tb_within_recs (into the caller's list), tb_changed_since /
tb_added_since, IntMap. No question allocates or writes: Ludic frees nothing, and the old lists'
per-call copies leaked every frame.

ludic.things keeps its Things as a Table<Thing> with x, z, kind and active as columns; every verb
writes the record and the row together (a Thing carries its handle and table, so thing_hide(t)
still needs no state), thing_set_on / thing_set_xz / thing_place_at are the silent forms the game
used to do by assignment, and things_verify holds the columns against the records.
things_near(_of) fill a caller's list, thing_of_kind walks a kind, things_count_of counts one, and
things_tick visits only the kinds that tick. thing_find answers the first-placed by uid.

Against a []Record scanned (M4 Pro): nearest 0.37 / 1.5 / 7.2 us at 10k / 100k / 1M (list 30 /
307 / 3075), by id 0.09 us at 100k (list 17), a move refiled in 11-44 ns. ecs_fuzz_test holds the
grid, the kind index and the record queries against a scan through 9000 random changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:52:51 +03:00
63e29f024c refactor(packages): the mechanic packages use ludic_base only, and ask through ports
ludic.clock, .effects, .inventory, .wallet, .weather and .tracks say
'uses ludic_base' (ludic.base uses nothing). ClockWorld, PackRules,
WeatherWorld and TracksWorld are export ports whose defaults are the old
fallbacks; clock_bind, inv_bind, weather_bind and tracks_bind are gone,
and the tests bind their fakes. The base README's toy does the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:36:11 +03:00
dd6a449921 feat(ludic.base): the vocabulary mechanic packages share - Tick, phases, Queue<T>, rng streams, the save tree, the system runner
A mechanic package depends on ludic.base and nothing else: ports (records of
function values for now) for questions, queues for facts, verbs for changes,
phases for order and its own versioned save section. The runner inits, resets,
saves and loads systems in the order added and ticks them phase by phase; a
missing save section is a reset. Tests for each piece and a worked route
between two toy mechanics live under tests/. The old ludic.core (engine ECS
components) is used by examples/library and stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 04:01:16 +03:00