wip(ecs): ludic.wildlife on a Table<WildAnimal> - 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 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-27 21:21:29 +03:00
parent ab34f8229b
commit 5ac3d488a4
8 changed files with 142 additions and 15 deletions

View file

@ -61,4 +61,6 @@ export property WildAnimal {
sus_z: float = 0.0 sus_z: float = 0.0
spook: float = 0.0 # game minutes of extra wariness after being flushed spook: float = 0.0 # game minutes of extra wariness after being flushed
seen: float = 0.0 # the alert it has already reacted to seen: float = 0.0 # the alert it has already reacted to
ent: int = -1 # its handle in the table
tab: Table<WildAnimal> = null # the table holding it, so a verb on the animal alone keeps the indexes
} }

View file

@ -3,13 +3,13 @@
export function wildlife_put_away(wildlife_st: mut WildlifeState) -> void { export function wildlife_put_away(wildlife_st: mut WildlifeState) -> void {
if wildlife_st.wl_own != null { return } if wildlife_st.wl_own != null { return }
wildlife_st.wl_own = wildlife_all(wildlife_st) wildlife_st.wl_own = wildlife_st.wl
wildlife_st.wl_list = new []WildAnimal wildlife_st.wl = wl__new()
} }
export function wildlife_bring_back(wildlife_st: mut WildlifeState) -> void { export function wildlife_bring_back(wildlife_st: mut WildlifeState) -> void {
if wildlife_st.wl_own == null { return } 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 wildlife_st.wl_own = null
} }

View file

@ -8,6 +8,7 @@ import "animal.ludic"
import "places.ludic" import "places.ludic"
import "port.ludic" import "port.ludic"
import "facts.ludic" import "facts.ludic"
import "table.ludic"
import "store.ludic" import "store.ludic"
import "guest.ludic" import "guest.ludic"
import "record.ludic" import "record.ludic"

View file

@ -4,6 +4,7 @@ export function wildlife_tick(wildlife_st: mut WildlifeState, dt: float, gh: flo
wl_seed_scan(wildlife_st, dt) wl_seed_scan(wildlife_st, dt)
let all = wildlife_all(wildlife_st) let all = wildlife_all(wildlife_st)
for i in 0 .. len(all) { wl_tick_one(wildlife_st, all[i], dt, gh) } 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 { function wl_tick_one(wildlife_st: mut WildlifeState, a: WildAnimal, dt: float, gh: float) -> void {

View file

@ -34,7 +34,7 @@ export property WildSpecies {
export state WildlifeState { export state WildlifeState {
wl_facts: Queue<WildFact> = wl_facts__new() wl_facts: Queue<WildFact> = 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_px: float = 0.0
wl_pz: float = 0.0 wl_pz: float = 0.0
wl_ask: WildLure = new WildLure wl_ask: WildLure = new WildLure
@ -53,7 +53,7 @@ export state WildlifeState {
wl_fed_any: bool = false wl_fed_any: bool = false
wl_near_i: int = 0 wl_near_i: int = 0
wl_species: []WildSpecies = new []WildSpecies wl_species: []WildSpecies = new []WildSpecies
wl_list: []WildAnimal = new []WildAnimal wl: WildTable = wl__new()
wl_keys: int = 0 wl_keys: int = 0
wl_dice: Rng = rng_new(97) wl_dice: Rng = rng_new(97)
wl_hold: bool = false # staged: nobody thinks, everybody keeps their speed wl_hold: bool = false # staged: nobody thinks, everybody keeps their speed

View file

@ -1,14 +1,20 @@
# store.ludic - the animals, their own dice, and the verbs that make and take one away # store.ludic - the animals, their own dice, and the verbs that make and take one away
const WL_TAU: float = 6.2831853 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 { 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) # 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 { function wl_rng(wildlife_st: WildlifeState) -> Rng {
return wildlife_st.wl_dice 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.scale = scale
a.legend = legend a.legend = legend
a.state = WILD_IDLE a.state = WILD_IDLE
push(wildlife_all(wildlife_st), a) wl_file(wildlife_st.wl, a)
WildlifeWorld.born(a) WildlifeWorld.born(a)
return 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 # gone for good: not drawn, not ticked
export function wildlife_remove(a: WildAnimal) -> void { export function wildlife_remove(a: WildAnimal) -> void {
if a == null { return } if a == null { return }
a.alive = false wildlife_set_alive(a, false)
a.shown = 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_hold(wildlife_st: mut WildlifeState, on: bool) -> void { wildlife_st.wl_hold = on }
export function wildlife_by_nid(wildlife_st: WildlifeState, nid: int) -> WildAnimal { export function wildlife_by_nid(wildlife_st: WildlifeState, nid: int) -> WildAnimal {
let all = wildlife_all(wildlife_st) let tb = wildlife_st.wl.tab
for i in 0 .. len(all) { if all[i].nid == nid { return all[i] } } let r = tb_row(tb, imap_get(wildlife_st.wl.nids, nid, -1))
return null if r < 0 or tb.rec[r].nid != nid { return null }
return tb.rec[r]
} }
# how many of a species are alive, legends apart # how many of a species are alive, legends apart
export function wildlife_count(wildlife_st: WildlifeState, sp: int) -> int { export function wildlife_count(wildlife_st: WildlifeState, sp: int) -> int {
var n = 0 var n = 0
let all = wildlife_all(wildlife_st) let rows = ix_rows(wildlife_st.wl.species, sp)
for i in 0 .. len(all) { if all[i].alive and all[i].sp == sp and not all[i].legend { n += 1 } } for k in 0 .. len(rows) { if not wildlife_st.wl.tab.rec[rows[k]].legend { n += 1 } }
return n return n
} }

View file

@ -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<WildAnimal> = 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<WildAnimal> = 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]
}

View file

@ -230,4 +230,29 @@ program WildlifeTest {
expect_near(r.x, r.wx, 0.001) expect_near(r.x, r.wx, 0.001)
expect(r.x != x0 or r.wx == r.sx) 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)
}
} }