From 5ac3d488a451bb031a9801556485c537353605a7 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Sun, 27 Sep 2026 21:21:29 +0300 Subject: [PATCH] wip(ecs): ludic.wildlife on a Table - UNBUILT Written while builds are held (agents share 16 GB; no build or run until the user lifts it), so it has not been compiled or tested. The animals are rows of a ludic.base Table: x, z, species and alive as columns, a 32 m grid over the living, the living by species, and a nid map. wildlife_by_nid is a map lookup instead of a scan of every animal ever made, wildlife_count walks only its species, wildlife_nearest asks the grid, wildlife_set_alive writes the flag and the row together, and wildlife_refile files the tick's moves once after it (wildlife_verify holds the columns to the records). A guest puts its whole table away and brings it back. A test case covers them; to run once builds are allowed: ludic test packages/ludic.wildlife. Co-Authored-By: Claude Opus 5.5 --- packages/ludic.wildlife/animal.ludic | 2 + packages/ludic.wildlife/guest.ludic | 6 +- packages/ludic.wildlife/index.ludic | 1 + packages/ludic.wildlife/run.ludic | 1 + packages/ludic.wildlife/species.ludic | 4 +- packages/ludic.wildlife/store.ludic | 27 ++++-- packages/ludic.wildlife/table.ludic | 91 +++++++++++++++++++ .../ludic.wildlife/tests/wildlife_test.ludic | 25 +++++ 8 files changed, 142 insertions(+), 15 deletions(-) create mode 100644 packages/ludic.wildlife/table.ludic diff --git a/packages/ludic.wildlife/animal.ludic b/packages/ludic.wildlife/animal.ludic index a5f3ef4c..eb3f52e4 100644 --- a/packages/ludic.wildlife/animal.ludic +++ b/packages/ludic.wildlife/animal.ludic @@ -61,4 +61,6 @@ export property WildAnimal { sus_z: float = 0.0 spook: float = 0.0 # game minutes of extra wariness after being flushed seen: float = 0.0 # the alert it has already reacted to + ent: int = -1 # its handle in the table + tab: Table = null # the table holding it, so a verb on the animal alone keeps the indexes } diff --git a/packages/ludic.wildlife/guest.ludic b/packages/ludic.wildlife/guest.ludic index 1fdf2f1a..e1664892 100644 --- a/packages/ludic.wildlife/guest.ludic +++ b/packages/ludic.wildlife/guest.ludic @@ -3,13 +3,13 @@ export function wildlife_put_away(wildlife_st: mut WildlifeState) -> void { if wildlife_st.wl_own != null { return } - wildlife_st.wl_own = wildlife_all(wildlife_st) - wildlife_st.wl_list = new []WildAnimal + wildlife_st.wl_own = wildlife_st.wl + wildlife_st.wl = wl__new() } export function wildlife_bring_back(wildlife_st: mut WildlifeState) -> void { if wildlife_st.wl_own == null { return } - wildlife_st.wl_list = wildlife_st.wl_own + wildlife_st.wl = wildlife_st.wl_own wildlife_st.wl_own = null } diff --git a/packages/ludic.wildlife/index.ludic b/packages/ludic.wildlife/index.ludic index 832aa192..a7499956 100644 --- a/packages/ludic.wildlife/index.ludic +++ b/packages/ludic.wildlife/index.ludic @@ -8,6 +8,7 @@ import "animal.ludic" import "places.ludic" import "port.ludic" import "facts.ludic" +import "table.ludic" import "store.ludic" import "guest.ludic" import "record.ludic" diff --git a/packages/ludic.wildlife/run.ludic b/packages/ludic.wildlife/run.ludic index 0229aee1..248333e5 100644 --- a/packages/ludic.wildlife/run.ludic +++ b/packages/ludic.wildlife/run.ludic @@ -4,6 +4,7 @@ export function wildlife_tick(wildlife_st: mut WildlifeState, dt: float, gh: flo wl_seed_scan(wildlife_st, dt) let all = wildlife_all(wildlife_st) for i in 0 .. len(all) { wl_tick_one(wildlife_st, all[i], dt, gh) } + wildlife_refile(wildlife_st) } function wl_tick_one(wildlife_st: mut WildlifeState, a: WildAnimal, dt: float, gh: float) -> void { diff --git a/packages/ludic.wildlife/species.ludic b/packages/ludic.wildlife/species.ludic index df213e6b..db87a1e1 100644 --- a/packages/ludic.wildlife/species.ludic +++ b/packages/ludic.wildlife/species.ludic @@ -34,7 +34,7 @@ export property WildSpecies { export state WildlifeState { wl_facts: Queue = wl_facts__new() - wl_own: []WildAnimal = null + wl_own: WildTable = null # a guest's own animals, put away whole while it draws the host's wl_px: float = 0.0 wl_pz: float = 0.0 wl_ask: WildLure = new WildLure @@ -53,7 +53,7 @@ export state WildlifeState { wl_fed_any: bool = false wl_near_i: int = 0 wl_species: []WildSpecies = new []WildSpecies - wl_list: []WildAnimal = new []WildAnimal + wl: WildTable = wl__new() wl_keys: int = 0 wl_dice: Rng = rng_new(97) wl_hold: bool = false # staged: nobody thinks, everybody keeps their speed diff --git a/packages/ludic.wildlife/store.ludic b/packages/ludic.wildlife/store.ludic index 04a9640e..c6fc6e7d 100644 --- a/packages/ludic.wildlife/store.ludic +++ b/packages/ludic.wildlife/store.ludic @@ -1,14 +1,20 @@ # store.ludic - the animals, their own dice, and the verbs that make and take one away const WL_TAU: float = 6.2831853 +# every animal, one per row - the table's own list: read it, never push to it export function wildlife_all(wildlife_st: WildlifeState) -> []WildAnimal { - return wildlife_st.wl_list + return wildlife_st.wl.tab.rec } -export function wildlife_count_all(wildlife_st: WildlifeState) -> int { return len(wildlife_all(wildlife_st)) } +export function wildlife_count_all(wildlife_st: WildlifeState) -> int { return tb_len(wildlife_st.wl.tab) } # a new valley: every animal forgotten, quietly (the game has already let its drawings go) -export function wildlife_clear(wildlife_st: mut WildlifeState) -> void { wildlife_st.wl_list = new []WildAnimal } +export function wildlife_clear(wildlife_st: mut WildlifeState) -> void { + let tb = wildlife_st.wl.tab + for r in 0 .. tb_len(tb) { tb.rec[r].ent = -1 } + tb_clear(tb) + imap_clear(wildlife_st.wl.nids) +} function wl_rng(wildlife_st: WildlifeState) -> Rng { return wildlife_st.wl_dice @@ -40,7 +46,7 @@ export function wildlife_make(wildlife_st: mut WildlifeState, sp: int, x: float, a.scale = scale a.legend = legend a.state = WILD_IDLE - push(wildlife_all(wildlife_st), a) + wl_file(wildlife_st.wl, a) WildlifeWorld.born(a) return a } @@ -67,7 +73,7 @@ export function wildlife_new(wildlife_st: mut WildlifeState, sp: int, x: float, # gone for good: not drawn, not ticked export function wildlife_remove(a: WildAnimal) -> void { if a == null { return } - a.alive = false + wildlife_set_alive(a, false) a.shown = false } @@ -75,15 +81,16 @@ export function wildlife_remove(a: WildAnimal) -> void { export function wildlife_hold(wildlife_st: mut WildlifeState, on: bool) -> void { wildlife_st.wl_hold = on } export function wildlife_by_nid(wildlife_st: WildlifeState, nid: int) -> WildAnimal { - let all = wildlife_all(wildlife_st) - for i in 0 .. len(all) { if all[i].nid == nid { return all[i] } } - return null + let tb = wildlife_st.wl.tab + let r = tb_row(tb, imap_get(wildlife_st.wl.nids, nid, -1)) + if r < 0 or tb.rec[r].nid != nid { return null } + return tb.rec[r] } # how many of a species are alive, legends apart export function wildlife_count(wildlife_st: WildlifeState, sp: int) -> int { var n = 0 - let all = wildlife_all(wildlife_st) - for i in 0 .. len(all) { if all[i].alive and all[i].sp == sp and not all[i].legend { n += 1 } } + let rows = ix_rows(wildlife_st.wl.species, sp) + for k in 0 .. len(rows) { if not wildlife_st.wl.tab.rec[rows[k]].legend { n += 1 } } return n } diff --git a/packages/ludic.wildlife/table.ludic b/packages/ludic.wildlife/table.ludic new file mode 100644 index 00000000..46782ddb --- /dev/null +++ b/packages/ludic.wildlife/table.ludic @@ -0,0 +1,91 @@ +# table.ludic - the animals, rows of a ludic.base Table: where each stands, its species and whether +# it lives are columns, with a grid over the living, the living by species, and a nid to its handle. +# The AI moves an animal by its record; wildlife_refile files every move once, after the tick. +export const WA_X: int = 0 # float columns +export const WA_Z: int = 1 +export const WA_SP: int = 0 # int columns +export const WA_ALIVE: int = 1 # 1 while alive +const WA_CELL: float = 32.0 + +export property WildTable { + tab: Table = null + grid: Grid = null + species: IntIndex = null # the living, by species + nids: IntMap = null +} + +function wl__new() -> WildTable { + let w = new WildTable + let tb: Table = table_new(2, 2) + w.tab = tb + w.grid = tb_grid(tb, WA_X, WA_Z, WA_ALIVE, WA_CELL) + w.species = tb_index(tb, WA_SP, WA_ALIVE) + w.nids = imap_new() + return w +} + +function wl_row(a: WildAnimal) -> int { + if a == null or a.tab == null { return -1 } + return tb_row(a.tab, a.ent) +} + +# a new row for `a`, its columns from its record +function wl_file(w: WildTable, a: WildAnimal) -> void { + a.tab = w.tab + a.ent = tb_add(w.tab, a) + let r = wl_row(a) + let tb = w.tab + tb.f[WA_X][r] = a.x + tb.f[WA_Z][r] = a.z + tb.i[WA_SP][r] = a.sp + var on = 0 + if a.alive { on = 1 } + tb.i[WA_ALIVE][r] = on + tb_refile(tb, r) + if a.nid != 0 { imap_put(w.nids, a.nid, a.ent) } +} + +# alive or not, the record and the row together (a copy the game is handed says so too) +export function wildlife_set_alive(a: WildAnimal, on: bool) -> void { + if a == null { return } + a.alive = on + let r = wl_row(a) + if r < 0 { return } + var v = 0 + if on { v = 1 } + tb_set_i(a.tab, WA_ALIVE, r, v) +} + +# every row whose record moved since it was filed, refiled: once after a tick that moves them all, +# and by the game after it moves the copies a guest is handed +export function wildlife_refile(wildlife_st: mut WildlifeState) -> void { + let tb = wildlife_st.wl.tab + let xs = tb.f[WA_X] + let zs = tb.f[WA_Z] + for r in 0 .. tb_len(tb) { + let a = tb.rec[r] + if xs[r] != a.x or zs[r] != a.z { tb_set_xz(tb, WA_X, WA_Z, r, a.x, a.z) } + } +} + +# after a refile, the columns say what the records say; the count of rows that do not +export function wildlife_verify(wildlife_st: WildlifeState) -> int { + let tb = wildlife_st.wl.tab + var bad = 0 + for r in 0 .. tb_len(tb) { + let a = tb.rec[r] + var on = 0 + if a.alive { on = 1 } + if tb.f[WA_X][r] != a.x or tb.f[WA_Z][r] != a.z or tb.i[WA_SP][r] != a.sp or tb.i[WA_ALIVE][r] != on or a.ent != tb.ent[r] { bad += 1 } + } + return bad +} + +# the nearest living animal of a species (below 0: any) within maxr (0: any distance), or null +export function wildlife_nearest(wildlife_st: WildlifeState, sp: int, x: float, z: float, maxr: float) -> WildAnimal { + let w = wildlife_st.wl + var r = -1 + if sp < 0 { r = tb_nearest(w.tab, w.grid, x, z, maxr, -1, 0) } else { r = tb_nearest_of(w.tab, w.grid, w.species, x, z, maxr, sp) } + if r < 0 { return null } + return w.tab.rec[r] +} diff --git a/packages/ludic.wildlife/tests/wildlife_test.ludic b/packages/ludic.wildlife/tests/wildlife_test.ludic index b3174d04..6c393fae 100644 --- a/packages/ludic.wildlife/tests/wildlife_test.ludic +++ b/packages/ludic.wildlife/tests/wildlife_test.ludic @@ -230,4 +230,29 @@ program WildlifeTest { expect_near(r.x, r.wx, 0.001) expect(r.x != x0 or r.wx == r.sx) } + + test "the table: a nid finds its animal, the living count by species, the nearest, and a refile" (wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState) { + fk_setup(wildlife_st, wildlife_tests_fake_st) + wildlife_clear(wildlife_st) + let a = wildlife_make(wildlife_st, 0, 10.0, 0.0, false, 0.0, 0, 1.0, 41) + let b = wildlife_make(wildlife_st, 0, 90.0, 0.0, false, 0.0, 0, 1.0, 42) + let c = wildlife_make(wildlife_st, 1, 30.0, 0.0, true, 0.0, 0, 1.0, 43) + expect(wildlife_by_nid(wildlife_st, 42) == b) + expect(wildlife_by_nid(wildlife_st, 7) == null) + expect_eq(wildlife_count(wildlife_st, 0), 2) + expect_eq(wildlife_count(wildlife_st, 1), 0) + expect(wildlife_nearest(wildlife_st, -1, 0.0, 0.0, 0.0) == a) + expect(wildlife_nearest(wildlife_st, 1, 0.0, 0.0, 0.0) == c) + wildlife_remove(a) + expect_eq(wildlife_count(wildlife_st, 0), 1) + expect(wildlife_nearest(wildlife_st, 0, 0.0, 0.0, 0.0) == b) + b.x = -5.0 + wildlife_refile(wildlife_st) + expect(wildlife_nearest(wildlife_st, -1, 0.0, 0.0, 20.0) == b) + expect_eq(wildlife_verify(wildlife_st), 0) + expect_eq(wildlife_count_all(wildlife_st), 3) + wildlife_clear(wildlife_st) + expect_eq(wildlife_count_all(wildlife_st), 0) + expect(wildlife_by_nid(wildlife_st, 42) == null) + } }