Merge branch 'lang/play-leaks' into lang/leaks2

This commit is contained in:
Orkun ÇAKILKAYA 2026-09-28 13:33:48 +03:00
commit 67474a5cfe
15 changed files with 153 additions and 11 deletions

View file

@ -0,0 +1,19 @@
# facts.ludic - a fishing fact's record: one no drain holds any more, made fresh (and kept) only when
# every one is held. A cast, a bite, every crossing and run of a fight is a fact, and a record a fact
# is never given back (the ludic.wildlife idiom, facts.ludic there)
function fishing_fact_record(fishing_st: mut FishingState) -> FishingFact {
if fishing_st.fishing_fnext[0] >= len(fishing_st.fishing_ffree) {
q_unheld(fishing_st.fishing_fact_q, fishing_st.fishing_fpool, fishing_st.fishing_ffree)
fishing_st.fishing_fnext[0] = 0
}
if fishing_st.fishing_fnext[0] < len(fishing_st.fishing_ffree) {
let f = fishing_st.fishing_ffree[fishing_st.fishing_fnext[0]]
fishing_st.fishing_fnext[0] += 1
return f
}
let f = new FishingFact
push(fishing_st.fishing_fpool, f)
return f
}
# how many fact records have ever been made: the peak held at once, not the facts said
export function fishing_fact_records(fishing_st: FishingState) -> int { return len(fishing_st.fishing_fpool) }

View file

@ -6,6 +6,7 @@ import "ludic.base"
import "kinds.ludic"
import "ports.ludic"
import "state.ludic"
import "facts.ludic"
import "fight.ludic"
import "verbs.ludic"
import "run.ludic"

View file

@ -25,6 +25,9 @@ export state FishingState {
fishing_run_next: float = 0.0
fishing_dice: Rng = null
fishing_fact_q: Queue<FishingFact> = fishing_fact_q__new()
fishing_fpool: []FishingFact = new []FishingFact # every fact record made, reused once no drain holds it (facts.ludic)
fishing_ffree: []FishingFact = new []FishingFact
fishing_fnext: []int = words(1)
fishing_n: int = 0 # the record: caught, the biggest (cm), a legend landed
fishing_big: int = 0
fishing_leg: bool = false
@ -51,8 +54,8 @@ function fishing_rand(fishing_st: mut FishingState) -> float {
return rng_float(fishing_st.fishing_dice)
}
function fishing_say(fishing_st: FishingState, what: int) -> void {
let f = new FishingFact
function fishing_say(fishing_st: mut FishingState, what: int) -> void {
let f = fishing_fact_record(fishing_st)
f.what = what
f.species = fishing_st.fishing_sp
f.cm = fishing_st.fishing_len

View file

@ -254,4 +254,20 @@ program FishingTest {
expect(fishing_legend_caught(fishing_st))
expect(not fishing_active(fishing_st))
}
test "fifty fish, their facts drained each time, reuse a handful of records" (fishing_st: mut FishingState, fishing_test_st: mut FishingTestState) {
fresh(fishing_st, fishing_test_st)
for k in 0 .. 50 {
fishing_cast(fishing_st, 20.0, 0.0)
facts(fishing_st)
hook(fishing_st)
facts(fishing_st)
play(fishing_st, 12)
facts(fishing_st)
fishing_stop(fishing_st)
facts(fishing_st)
}
print(`fishing: 50 fish, {fishing_fact_records(fishing_st)} fact records made`)
expect(fishing_fact_records(fishing_st) < 30)
}
}

View file

@ -0,0 +1,19 @@
# facts.ludic - a jobs fact's record: one no drain holds any more, made fresh (and kept) only when
# every one is held. A deed that moves a job is a fact, and a record a fact is never given back
# (the ludic.wildlife idiom)
function jobs__fact_record(jobs_st: mut JobsState) -> JobsFact {
if jobs_st.jobs__fnext[0] >= len(jobs_st.jobs__ffree) {
q_unheld(jobs_st.jobs__q, jobs_st.jobs__fpool, jobs_st.jobs__ffree)
jobs_st.jobs__fnext[0] = 0
}
if jobs_st.jobs__fnext[0] < len(jobs_st.jobs__ffree) {
let f = jobs_st.jobs__ffree[jobs_st.jobs__fnext[0]]
jobs_st.jobs__fnext[0] += 1
return f
}
let f = new JobsFact
push(jobs_st.jobs__fpool, f)
return f
}
# how many fact records have ever been made: the peak held at once, not the facts said
export function jobs_fact_records(jobs_st: JobsState) -> int { return len(jobs_st.jobs__fpool) }

View file

@ -6,6 +6,7 @@ import "ludic.base"
import "defs.ludic"
import "ports.ludic"
import "state.ludic"
import "facts.ludic"
import "jobs.ludic"
import "giveup.ludic"
import "count.ludic"

View file

@ -22,6 +22,9 @@ export state JobsState {
jobs__seed: int = 1000
jobs__dice: Rng = rng_new(1000)
jobs__q: Queue<JobsFact> = jobs__q__new()
jobs__fpool: []JobsFact = new []JobsFact # every fact record made, reused once no drain holds it (facts.ludic)
jobs__ffree: []JobsFact = new []JobsFact
jobs__fnext: []int = words(1)
}
# the posts, every board's slots end to end from jobs__off[b]
@ -58,8 +61,8 @@ export function jobs_facts(jobs_st: JobsState) -> Queue<JobsFact> {
return jobs_st.jobs__q
}
function jobs__fact(jobs_st: JobsState, what: int, job: int, b: int, s: int, have: int, need: int) -> void {
let f = new JobsFact
function jobs__fact(jobs_st: mut JobsState, what: int, job: int, b: int, s: int, have: int, need: int) -> void {
let f = jobs__fact_record(jobs_st)
f.what = what
f.job = job
f.board = b

View file

@ -129,4 +129,17 @@ program JobsGiveUpTest {
jobs_test_st.day = 2
expect(jobs_offered(jobs_st, JOB_CATCH))
}
test "two hundred jobs taken and given up, their facts drained each time, reuse a handful of records" (jobs_st: mut JobsState, jobs_test_st: mut JobsTestState) {
fresh(jobs_st, jobs_test_st)
for k in 0 .. 200 {
jobs_take(jobs_st, JOB_CATCH)
count_of(jobs_st, 0)
jobs_give_up(jobs_st, JOB_CATCH)
count_of(jobs_st, 0)
jobs_st.jobs__back[JOB_CATCH] = -1
}
print(`jobs: 200 taken and given up, {jobs_fact_records(jobs_st)} fact records made`)
expect(jobs_fact_records(jobs_st) < 10)
}
}

View file

@ -60,7 +60,7 @@ export port MinimapWorld { # every member has a default
| `minimap_pin_count()`, `minimap_pin_max()`, `minimap_pin_full()`, `minimap_pin(i) -> MinimapPin` | the marks |
| `minimap_pin_set_label/icon/colour(i, v)`, `minimap_pin_last_icon()` | editing one |
| `minimap_pin_open()`, `minimap_pin_select(i)`, `minimap_pin_followed()`, `minimap_pin_follow(i)`, `minimap_pin_follow_toggle()` | the open one and the followed one |
| `minimap_pins_near(x, z, n, within) -> []int` | the nearest marks, not the followed one (for a compass) |
| `minimap_pins_near(x, z, n, within) -> []int` | the nearest marks, not the followed one (for a compass), in a list the state keeps and refills: read it, do not keep it |
| `minimap_config_view(span, zoom_min, zoom_max, focus)`, `minimap_window(x0, y0, size)` | the view's range, and where the window is this frame |
| `minimap_px(x)`, `minimap_py(z)`, `minimap_wx(rx)`, `minimap_wz(ry)`, `minimap_span()`, `minimap_scale()`, `minimap_scale_metres()` | projection, and a scale bar's round distance |
| `minimap_zoom()`, `minimap_zoom_set(z)`, `minimap_zoom_step(dir)`, `minimap_zoom_about(rx, ry, factor)` | zoom |

View file

@ -1,6 +1,7 @@
# grid.ludic - the explored grid: square cells round the origin, and the part of it "explored" is
# measured over (a core, so a bigger grid does not change what a share of it means)
export state MinimapState {
mm_near: []int = new []int # minimap_pins_near's answer, cleared and refilled: a compass asks every frame
mm_land_n: int = 0 # land cells in the core, counted once per origin
mm_land_ox: float = 0.0
mm_land_oz: float = 0.0

View file

@ -71,8 +71,9 @@ export function minimap_pin(minimap_st: MinimapState, i: int) -> MinimapPin {
}
# the nearest n marks within `within` of (x, z), nearest first, leaving out the followed one
export function minimap_pins_near(minimap_st: MinimapState, x: float, z: float, n: int, within: float) -> []int {
let out = new []int
export function minimap_pins_near(minimap_st: mut MinimapState, x: float, z: float, n: int, within: float) -> []int {
let out = minimap_st.mm_near
List.clear(out)
for k in 0 .. n {
var best = -1
var bd = within * within

View file

@ -111,6 +111,20 @@ program MinimapTest {
expect_eq(near[1], 1)
}
test "asking for the nearest marks every frame allocates nothing" (minimap_st: mut MinimapState, minimap_test_st: mut MinimapTestState) {
fresh(minimap_st, minimap_test_st)
minimap_config_pins(minimap_st, 8, 8, 5)
minimap_pin_add(minimap_st, 110.0, 100.0, 0, "near")
minimap_pin_add(minimap_st, 130.0, 100.0, 0, "next")
minimap_pins_near(minimap_st, 100.0, 100.0, 2, 400.0)
let before = Os.heap_bytes()
for k in 0 .. 1000 { minimap_pins_near(minimap_st, 100.0 + float(k % 7), 100.0, 2, 400.0) }
let grew = Os.heap_bytes() - before
print(`minimap: 1000 asks, heap {grew} bytes`)
expect(grew == long(0))
expect_eq(len(minimap_pins_near(minimap_st, 100.0, 100.0, 2, 400.0)), 2)
}
test "the view: the world on the window and back, zoom about a point, a pan, focus and the clamp" (minimap_st: mut MinimapState, minimap_test_st: mut MinimapTestState) {
fresh(minimap_st, minimap_test_st)
minimap_centre(minimap_st, 100.0, 100.0)

View file

@ -16,14 +16,15 @@ export function phys_is_open(physics_st: PhysicsState) -> bool { return physics_
# which world this is: a body, shape or walker id kept from an older one means nothing now
export function phys_generation(physics_st: PhysicsState) -> int { return physics_st.ph_gen }
# everything let go: the world, every body in it and every shape (a world swap, a quit)
# everything let go: the world, every body in it and every shape (a world swap, a quit); the lists
# that held them are emptied and kept, since nothing is given back
export function phys_close(physics_st: mut PhysicsState) -> void {
if physics_st.ph_walkers != null {
for i in 0 .. len(physics_st.ph_walkers) {
if physics_st.ph_walkers[i] != null { jph_char_free(physics_st.ph_walkers[i]) }
}
}
physics_st.ph_walkers = new []pointer
if physics_st.ph_walkers != null { List.clear(physics_st.ph_walkers) }
if physics_st.ph_world != null { jph_world_free(physics_st.ph_world) }
physics_st.ph_world = null
if physics_st.ph_shapes != null {
@ -31,7 +32,7 @@ export function phys_close(physics_st: mut PhysicsState) -> void {
if physics_st.ph_shapes[i] != null { jph_shape_free(physics_st.ph_shapes[i]) }
}
}
physics_st.ph_shapes = new []pointer
if physics_st.ph_shapes != null { List.clear(physics_st.ph_shapes) }
List.clear(physics_st.ph_spare)
List.clear(physics_st.ph_floats)
List.clear(physics_st.ph_float_k)

View file

@ -0,0 +1,42 @@
# reuse_test.ludic - what is taken away is given back: a walker made and removed two hundred times
# holds one place in the list and no more heap, and a world closed and opened again keeps its lists
import "ludic.physics"
import "ludic.base"
program ReuseTest {
numbers float
test "a walker made and removed takes the same place and grows nothing" (physics_st: mut PhysicsState) {
expect(phys_open(physics_st, 64, 1))
let w = phys_walker_add(physics_st, 0.4, 1.8, 0.0, 0.0, 0.0, 50.0, 80.0)
phys_walker_remove(physics_st, w)
let before = Os.heap_bytes()
for k in 0 .. 200 {
let id = phys_walker_add(physics_st, 0.4, 1.8, float(k), 0.0, 0.0, 50.0, 80.0)
expect_eq(id, w)
phys_walker_remove(physics_st, id)
}
let grew = Os.heap_bytes() - before
print(`reuse: 200 walkers made and removed, {len(physics_st.ph_walkers)} place, heap {grew} bytes`)
expect_eq(len(physics_st.ph_walkers), 1)
expect(grew < long(1024))
phys_close(physics_st)
}
test "a world closed and opened again keeps the lists it held" (physics_st: mut PhysicsState) {
expect(phys_open(physics_st, 64, 1))
phys_box(physics_st, 1.0, 1.0, 1.0)
phys_walker_add(physics_st, 0.4, 1.8, 0.0, 0.0, 0.0, 50.0, 80.0)
let shapes = physics_st.ph_shapes
let walkers = physics_st.ph_walkers
for k in 0 .. 20 {
phys_close(physics_st)
expect(phys_open(physics_st, 64, 1))
phys_box(physics_st, 1.0, 1.0, 1.0)
phys_walker_add(physics_st, 0.4, 1.8, 0.0, 0.0, 0.0, 50.0, 80.0)
}
expect(physics_st.ph_shapes == shapes)
expect(physics_st.ph_walkers == walkers)
expect_eq(phys_shapes(physics_st), 1)
phys_close(physics_st)
}
}

View file

@ -27,7 +27,15 @@ function ph_walker(physics_st: PhysicsState, id: int) -> pointer {
export function phys_walker_add(physics_st: mut PhysicsState, r: float, h: float, x: float, y: float, z: float, max_slope: float, mass: float) -> int {
if physics_st.ph_world == null { return -1 }
ph_buffers()
push(physics_st.ph_walkers, jph_char_new(physics_st.ph_world, r, h, x, y, z, max_slope, mass))
let k = jph_char_new(physics_st.ph_world, r, h, x, y, z, max_slope, mass)
# a removed walker's place is taken again, so a horse called and sent away grows nothing
for i in 0 .. len(physics_st.ph_walkers) {
if physics_st.ph_walkers[i] == null {
physics_st.ph_walkers[i] = k
return i
}
}
push(physics_st.ph_walkers, k)
return len(physics_st.ph_walkers) - 1
}