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>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-28 20:03:46 +03:00
parent 5be2422c84
commit d9612ba2a6
31 changed files with 215 additions and 25 deletions

View file

@ -12,6 +12,7 @@ export property HandlePool {
}
# everything the pool will ever hold, made now: nothing after this allocates
@alloc_ok("a pool made at start: its owner keeps it")
export function hd_pool_new(name: string, cap: int) -> HandlePool {
let p = new HandlePool
p.name = name

View file

@ -6,6 +6,7 @@ import "tick.ludic"
import "kept.ludic"
import "handle.ludic"
import "queue.ludic"
import "queue_ring.ludic"
import "text_buf.ludic"
import "text_intern.ludic"
import "text_ring.ludic"

View file

@ -5,6 +5,8 @@ export property Queue<T> {
tag: QueueTag = null
none: []T = null # what draining an empty queue hands back, never pushed to
spare: []T = null # the list the last drain handed out: cleared and reused by the next
ring: []T = null # the facts' records, used again once no list holds them (queue_ring.ludic)
ring_at: int = 0
}
# what a queue says about itself, whatever it holds: its name and how many facts wait in it
export property QueueTag {
@ -26,6 +28,7 @@ export function queue_new<T>(name: string) -> Queue<T> {
q.tag.name = name
q.none = new []T
q.spare = new []T
q.ring = new []T
return q
}

View file

@ -0,0 +1,21 @@
# ludic.base/queue_ring.ludic - a fact's record made again rather than new: Ludic frees nothing, and a
# mechanic reports all day. q_rec hands out a record neither waiting in the queue nor handed out by
# its last drain, or null when every one is: the caller makes one more and q_ring_add keeps it, so
# the ring grows to the busiest frame and stays there. The caller sets every field it reads
export function q_rec<T>(q: Queue<T>) -> T {
let n = len(q.ring)
for t in 0 .. n {
let r = q.ring[q.ring_at]
q.ring_at = (q.ring_at + 1) % n
if not q_holds(q, r) { return r }
}
return null
}
export function q_ring_add<T>(q: Queue<T>, r: T) -> void { kept_push(q.ring, r) }
function q_holds<T>(q: Queue<T>, r: T) -> bool {
for i in 0 .. len(q.items) { if q.items[i] == r { return true } }
for i in 0 .. len(q.spare) { if q.spare[i] == r { return true } }
return false
}

View file

@ -134,6 +134,32 @@ program QueueTest {
test "a queue holds records" () { record_case() }
test "clear drops what is waiting" () { clear_case() }
test "a pool's records are free once no drain holds them" () { unheld_case() }
function rec_of(q: Queue<Caught>) -> Caught {
var c = q_rec(q)
if c == null {
c = new Caught
q_ring_add(q, c)
}
return c
}
function ring_case() -> void {
let q: Queue<Caught> = queue_new("caught")
let a = rec_of(q)
q_push(q, a)
let b = rec_of(q)
expect(a != b)
q_push(q, b)
q_drain(q)
expect_eq(len(q.ring), 2)
let c = rec_of(q)
expect(c != a and c != b)
q_push(q, c)
q_drain(q)
let d = rec_of(q)
expect(d == a or d == b)
expect_eq(len(q.ring), 3)
}
test "a fact's record comes round again once no list holds it" () { ring_case() }
test "a queue's element type is told by the queue alone" () { infer_case() }
test "the undrained queues are named, and a drain clears the report" () { undrained_case() }
}

View file

@ -13,7 +13,12 @@ export function clock_set_hours(clock_st: mut ClockState, h: float) -> void {
function ck_turn(clock_st: mut ClockState, how: int) -> void {
clock_st.ck_day += 1
clock_st.ck_moon = (clock_st.ck_moon + 1.0 / clock_st.ck_moon_days) % 1.0
let d = new DayTurned
var d = q_rec(clock_days(clock_st))
if d == null {
@alloc_ok("past the ring: more of these in a frame than its records, then kept in it")
d = new DayTurned
q_ring_add(clock_days(clock_st), d)
}
d.day = clock_st.ck_day
d.how = how
q_push(clock_days(clock_st), d)
@ -35,7 +40,10 @@ export function clock_advance(clock_st: mut ClockState, gh: float) -> void {
# a new trip: day one at the start hour, the moon somewhere in its month
export function clock_reset(clock_st: mut ClockState) -> void {
if clock_st.ck_dice == null {
@alloc_ok("once: the clock's dice, seeded again on every new trip after")
clock_st.ck_dice = rng_new(ClockWorld.seed())
} else { rng_seed(clock_st.ck_dice, ClockWorld.seed()) }
clock_st.ck_day = 1
clock_st.ck_hours = clock_st.ck_start_hours
clock_st.ck_moon = rng_span(clock_st.ck_dice, clock_st.ck_moon_lo, clock_st.ck_moon_hi) % 1.0

View file

@ -51,7 +51,12 @@ export function craft_refund(i: int) -> void {
function craft_fact_push(crafting_st: CraftingState, i: int, queued: bool) -> void {
let r = CraftRecipes[i]
let c = new Crafted
var c = q_rec(craft_facts(crafting_st))
if c == null {
@alloc_ok("past the ring: more of these in a frame than its records, then kept in it")
c = new Crafted
q_ring_add(craft_facts(crafting_st), c)
}
c.recipe = i
c.item = r.out
c.n = r.n

View file

@ -2,7 +2,12 @@
# itself; a bonus past the cap pushes out the oldest bonus; a full ring drops the nearest end.
function ef_end(effects_st: mut EffectsState, i: int, why: int) -> void {
let all = ef_all(effects_st)
let e = new EffectEnded
var e = q_rec(effects_ended(effects_st))
if e == null {
@alloc_ok("past the ring: more of these in a frame than its records, then kept in it")
e = new EffectEnded
q_ring_add(effects_ended(effects_st), e)
}
e.kind = all[i].kind
e.label = all[i].label
e.why = why

View file

@ -48,7 +48,12 @@ export function fire_facts(fire_st: FireState) -> Queue<FireFact> {
}
function fr_fact(fire_st: FireState, what: int) -> void {
let f = new FireFact
var f = q_rec(fire_facts(fire_st))
if f == null {
@alloc_ok("past the ring: more of these in a frame than its records, then kept in it")
f = new FireFact
q_ring_add(fire_facts(fire_st), f)
}
f.what = what
f.fuel = fire_st.fr_fuel
q_push(fire_facts(fire_st), f)

View file

@ -27,11 +27,18 @@ function gear__offs() -> []bool {
export function gear_facts(gear_st: GearState) -> Queue<GearFact> { return gear_st.gear__q }
function gear__fact(gear_st: GearState, what: int, k: int, c: int, cost: int) -> void {
let f = new GearFact
var f = q_rec(gear_facts(gear_st))
if f == null {
@alloc_ok("past the ring: more of these in a frame than its records, then kept in it")
f = new GearFact
q_ring_add(gear_facts(gear_st), f)
}
f.what = what
f.cost = cost
f.kind = k
f.charge = c
f.level = 0
f.item = -1
if k >= 0 {
f.level = gear_st.gear__lv[k]
f.item = GearKinds[k].item

View file

@ -18,7 +18,12 @@ export function hints_facts(hints_st: HintsState) -> Queue<HintsFact> {
}
function hn_fact(hints_st: HintsState, what: int, card: int) -> void {
let f = new HintsFact
var f = q_rec(hints_facts(hints_st))
if f == null {
@alloc_ok("past the ring: more of these in a frame than its records, then kept in it")
f = new HintsFact
q_ring_add(hints_facts(hints_st), f)
}
f.what = what
f.card = card
q_push(hints_facts(hints_st), f)

View file

@ -7,7 +7,10 @@ function iv_change(inventory_st: mut InventoryState, it: int, after: int) -> voi
if len(inventory_st.iv_pool) > 0 {
c = inventory_st.iv_pool[inventory_st.iv_at]
inventory_st.iv_at = (inventory_st.iv_at + 1) % len(inventory_st.iv_pool)
} else { c = new ItemChanged }
} else {
@alloc_ok("never after inv_config: the ring is made there")
c = new ItemChanged
}
c.item = it
c.before = before
c.after = after

View file

@ -11,7 +11,9 @@ function jobs__fact_record(jobs_st: mut JobsState) -> JobsFact {
jobs_st.jobs__fnext[0] += 1
return f
}
@alloc_ok("a pool miss: past the most facts held at once, then kept in the pool")
let f = new JobsFact
@alloc_ok("as above")
push(jobs_st.jobs__fpool, f)
return f
}

View file

@ -16,6 +16,7 @@ function jobs__reset_scope(jobs_st: mut JobsState, sc: int) -> void {
jobs_st.jobs__primed = false
}
@alloc_ok("on a save: the jobs written out")
function jobs__save_scope(jobs_st: JobsState, sc: int) -> Val {
let v = Value.object()
let js = Value.object()

View file

@ -75,6 +75,7 @@ function mm_land_count(minimap_st: mut MinimapState) -> void {
export function minimap_land_changed(minimap_st: mut MinimapState) -> void { minimap_st.mm_land_n = 0 }
# the grid as one string of 0 / 1, row by row, and back (a shorter string leaves the rest unseen)
@alloc_ok("on a save: the explored grid written out")
export function minimap_seen_text(minimap_st: mut MinimapState) -> string {
mm_grid(minimap_st)
var s = ""

View file

@ -45,6 +45,7 @@ export function minimap_config(minimap_st: mut MinimapState, cells: int, cell_m:
minimap_st.mm_cell_m = Math.max(cell_m, 0.001)
minimap_st.mm_core0 = 0
minimap_st.mm_core_n = minimap_st.mm_cells
@alloc_ok("once per map configured: its explored grid")
minimap_st.mm_seen = new []int
for i in 0 .. minimap_st.mm_cells * minimap_st.mm_cells { push(minimap_st.mm_seen, 0) }
minimap_st.mm_land_n = 0

View file

@ -2,9 +2,9 @@
# and which followed, and whether the map is waiting for a place to drop the next
export function minimap_pin_remove(minimap_st: mut MinimapState, i: int) -> void {
if i < 0 or i >= minimap_pin_count(minimap_st) { return }
let rest = new []MinimapPin
for k in 0 .. len(minimap_st.mm_pins) { if k != i { push(rest, minimap_st.mm_pins[k]) } }
minimap_st.mm_pins = rest
let l = minimap_st.mm_pins
for k in i .. len(l) - 1 { l[k] = l[k + 1] }
List.pop(l)
if minimap_st.mm_open == i { minimap_st.mm_open = -1 } else if minimap_st.mm_open > i { minimap_st.mm_open -= 1 }
if minimap_st.mm_followed == i { minimap_st.mm_followed = -1 } else if minimap_st.mm_followed > i { minimap_st.mm_followed -= 1 }
}

View file

@ -19,7 +19,10 @@ export function minimap_config_pins(minimap_st: mut MinimapState, max: int, symb
}
export function minimap_pins_clear(minimap_st: mut MinimapState) -> void {
if minimap_st.mm_pins == null {
@alloc_ok("once: the marks' list, cleared after")
minimap_st.mm_pins = new []MinimapPin
} else { List.clear(minimap_st.mm_pins) }
minimap_st.mm_open = -1
minimap_st.mm_followed = -1
minimap_st.mm_last = 0
@ -38,6 +41,7 @@ export function minimap_pin_full(minimap_st: MinimapState) -> bool { return mini
export function minimap_pin_add(minimap_st: mut MinimapState, x: float, z: float, icon: int, label: string) -> int {
if minimap_st.mm_pins == null { minimap_pins_clear(minimap_st) }
if minimap_pin_full(minimap_st) { return -1 }
@alloc_ok("a mark put down: kept, and the map holds at most its most")
let p = new MinimapPin
p.x = x
p.z = z
@ -53,6 +57,7 @@ export function minimap_pin_add(minimap_st: mut MinimapState, x: float, z: float
export function minimap_pin_put(minimap_st: mut MinimapState, x: float, z: float, icon: int, colour: int, day: int, label: string) -> int {
if minimap_st.mm_pins == null { minimap_pins_clear(minimap_st) }
if minimap_pin_full(minimap_st) { return -1 }
@alloc_ok("a mark put down: kept, and the map holds at most its most")
let p = new MinimapPin
p.x = x
p.z = z

View file

@ -40,8 +40,10 @@ export function minimap_wz(minimap_st: MinimapState, ry: float) -> float { retur
# a round distance whose bar is no wider than a quarter of the window
export function minimap_scale_metres(minimap_st: MinimapState) -> int {
let steps = [2000, 1000, 500, 250, 100, 50, 25]
for k in 0 .. len(steps) { if float(steps[k]) * minimap_scale(minimap_st) <= minimap_st.mm_size / 4.0 { return steps[k] } }
for k in 0 .. 7 {
let m = mm_scale_step(k)
if float(m) * minimap_scale(minimap_st) <= minimap_st.mm_size / 4.0 { return m }
}
return 25
}
@ -68,3 +70,14 @@ export function minimap_focus(minimap_st: mut MinimapState, on_you: bool) -> voi
minimap_recentre(minimap_st, on_you)
if minimap_st.mm_zoom < minimap_st.mm_zoom_focus { minimap_st.mm_zoom = Math.clamp(minimap_st.mm_zoom_focus, minimap_st.mm_zoom_min, minimap_st.mm_zoom_max) }
}
# the round distances a scale bar can show, largest first
function mm_scale_step(k: int) -> int {
if k == 0 { return 2000 }
if k == 1 { return 1000 }
if k == 2 { return 500 }
if k == 3 { return 250 }
if k == 4 { return 100 }
if k == 5 { return 50 }
return 25
}

View file

@ -83,7 +83,12 @@ export function needs_facts(needs_st: NeedsState) -> Queue<NeedFact> {
}
function nd_fact(needs_st: NeedsState, what: int, need: int) -> void {
let f = new NeedFact
var f = q_rec(needs_facts(needs_st))
if f == null {
@alloc_ok("past the ring: more of these in a frame than its records, then kept in it")
f = new NeedFact
q_ring_add(needs_facts(needs_st), f)
}
f.what = what
f.need = need
q_push(needs_facts(needs_st), f)

View file

@ -2,6 +2,7 @@
# from outside: the router's address and the port it mapped
function net__stun_send(net_st: mut NetState) -> void {
@alloc_ok("once: the transport's dice, seeded by its config the first time a STUN request goes")
if net_st.net__dice == null { net_st.net__dice = rng_new(net__conf(net_st).seed) }
if net_st.net__stun_ip == 0 {
let c = net__conf(net_st)

View file

@ -64,9 +64,23 @@ export function photo_facts(photo_st: PhotoState) -> Queue<PhotoFact> {
}
function pht_fact(photo_st: PhotoState, what: int, i: int) -> PhotoFact {
let f = new PhotoFact
var f = q_rec(photo_facts(photo_st))
if f == null {
@alloc_ok("past the ring: more of these in a frame than its records, then kept in it")
f = new PhotoFact
q_ring_add(photo_facts(photo_st), f)
}
f.what = what
f.index = i
f.mask = 0
f.legend = 0
f.firsts = 0
f.grade = 0
f.sign = -1
f.stars = false
f.file = ""
f.tags = ""
f.v = 0
q_push(photo_facts(photo_st), f)
return f
}

View file

@ -28,6 +28,7 @@ export function ply_join(session_st: mut SessionState, pid: int) -> int {
if pid <= 0 { return -1 } # 0 is this machine's player
for p in 1 .. PLY_MAX {
if session_st.session__slot[p].on { continue }
@alloc_ok("once per player joining: their slot's record")
let r = new PartyMember
r.on = true
r.pid = pid

View file

@ -83,11 +83,21 @@ export function ses_facts(session_st: SessionState) -> Queue<SessionFact> {
}
function session__fact(session_st: SessionState, what: int, slot: int, pid: int) -> SessionFact {
let f = new SessionFact
var f = q_rec(ses_facts(session_st))
if f == null {
@alloc_ok("past the ring: more of these in a frame than its records, then kept in it")
f = new SessionFact
q_ring_add(ses_facts(session_st), f)
}
f.what = what
f.slot = slot
f.pid = pid
f.role = session_st.session__role
f.id = 0
f.kind = 0
f.param = 0
f.yes = false
f.text = ""
q_push(ses_facts(session_st), f)
return f
}

View file

@ -46,7 +46,12 @@ export function settings_get_bool(settings_st: SettingsState, id: int) -> bool {
export function settings_get_text(settings_st: SettingsState, id: int) -> string { return settings_st.sg_text[id] }
function sg_changed(settings_st: SettingsState, id: int) -> void {
let c = new SettingChanged
var c = q_rec(settings_changes(settings_st))
if c == null {
@alloc_ok("past the ring: more of these in a frame than its records, then kept in it")
c = new SettingChanged
q_ring_add(settings_changes(settings_st), c)
}
c.id = id
c.key = settings_st.sg_defs[id].key
q_push(settings_changes(settings_st), c)

View file

@ -27,7 +27,12 @@ export function shop_sellable(shop_st: ShopState, v: int) -> []int {
}
function shop_trade_fact(shop_st: ShopState, what: int, it: int, n: int, price: int) -> void {
let t = new ShopTrade
var t = q_rec(shop_trades(shop_st))
if t == null {
@alloc_ok("past the ring: more of these in a frame than its records, then kept in it")
t = new ShopTrade
q_ring_add(shop_trades(shop_st), t)
}
t.what = what
t.vendor = shop_st.shop_at
t.item = it

View file

@ -64,7 +64,12 @@ export function steps_facts(steps_st: StepsState) -> Queue<StepsFact> {
}
function steps__fact(steps_st: StepsState, what: int, ch: int, i: int) -> void {
let f = new StepsFact
var f = q_rec(steps_facts(steps_st))
if f == null {
@alloc_ok("past the ring: more of these in a frame than its records, then kept in it")
f = new StepsFact
q_ring_add(steps_facts(steps_st), f)
}
f.what = what
f.chapter = ch
f.step = i

View file

@ -15,7 +15,7 @@ function tk_push(tracks_st: mut TracksState, f: TrackRead) -> void {
export function tracks_read(tracks_st: mut TracksState, slot: int) -> void {
if slot < 0 or tracks_st.tk_ring == null or slot >= len(tracks_st.tk_ring) { return }
let t = tracks_st.tk_ring[slot]
let f = new TrackRead
let f = tk_rec(tracks_st)
f.sp = t.sp
f.source = TRACK_PRINT
f.kind = slot / tracks_st.tk_cap
@ -35,7 +35,7 @@ export function tracks_read(tracks_st: mut TracksState, slot: int) -> void {
# a sign that is not a print (droppings, a legend's mark): the caller knows whether it is the
# first reading of it
export function tracks_note(tracks_st: mut TracksState, sp: int, source: int, legend: bool, yaw: float, first: bool) -> void {
let f = new TrackRead
let f = tk_rec(tracks_st)
f.sp = sp
f.source = source
f.legend = legend
@ -53,3 +53,19 @@ export function tracks_note_bearing(tracks_st: mut TracksState, yaw: float) -> v
}
export function tracks_set_read_count(tracks_st: mut TracksState, n: int) -> void { tracks_st.tk_read_n = n }
# a reading's record from the queue's ring (ludic.base's q_rec), every field back to its default
function tk_rec(tracks_st: mut TracksState) -> TrackRead {
var f = q_rec(tracks_reads(tracks_st))
if f == null {
@alloc_ok("past the ring: more readings in a frame than its records, then kept in it")
f = new TrackRead
q_ring_add(tracks_reads(tracks_st), f)
}
f.kind = 0
f.age = 0.0
f.fresh = 0
f.named = true
f.bearing = ""
return f
}

View file

@ -91,7 +91,12 @@ export function update_facts(update_st: UpdateState) -> Queue<UpdateFact> {
}
function update__fact(update_st: UpdateState, what: int) -> void {
let f = new UpdateFact
var f = q_rec(update_facts(update_st))
if f == null {
@alloc_ok("past the ring: more of these in a frame than its records, then kept in it")
f = new UpdateFact
q_ring_add(update_facts(update_st), f)
}
f.what = what
f.error = update_st.update__err
f.target = update_st.update__target

View file

@ -19,7 +19,12 @@ export function wallet_can(wallet_st: WalletState, n: int) -> bool { return wall
# the purse holds exactly n (a starting purse, a correction, the network's word)
export function wallet_set(wallet_st: mut WalletState, n: int) -> void {
if n == wallet_st.wl_money { return }
let c = new MoneyChanged
var c = q_rec(wallet_changes(wallet_st))
if c == null {
@alloc_ok("past the ring: more of these in a frame than its records, then kept in it")
c = new MoneyChanged
q_ring_add(wallet_changes(wallet_st), c)
}
c.before = wallet_st.wl_money
c.after = n
wallet_st.wl_money = n

View file

@ -83,7 +83,12 @@ export function weather_facts(weather_st: WeatherState) -> Queue<WeatherFact> {
}
function wx_fact(weather_st: WeatherState, what: int, kind: int, from: int) -> void {
let f = new WeatherFact
var f = q_rec(weather_facts(weather_st))
if f == null {
@alloc_ok("past the ring: more of these in a frame than its records, then kept in it")
f = new WeatherFact
q_ring_add(weather_facts(weather_st), f)
}
f.what = what
f.kind = kind
f.from = from