feat(ecs): ludic.vehicles on a Table<Vehicle>

The vehicles are rows of a ludic.base Table with kind and owner as columns, every player's own by
an owner index and a nid map: vehicle_of walks only that player's rows, vehicle_by_nid is a map
lookup, and a player who leaves has their rows removed instead of the whole list rebuilt. ve_add
takes the owner, so the owner is filed once and never assigned around the table. Positions stay
on the record: a boat moves every frame and there are at most two a player. vehicles_test holds
the table (9 tests); the game builds against it and lab/unit passes (88 tests).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-27 21:50:23 +03:00
parent 52270ab5d1
commit 045bf22e10
8 changed files with 87 additions and 18 deletions

View file

@ -1,10 +1,11 @@
# call.ludic - a player's boat or horse brought to a place: made there the first time, fetched there # call.ludic - a player's boat or horse brought to a place: made there the first time, fetched there
# after; gone when its player leaves # after; gone when its player leaves
function ve_add(vehicles_st: VehiclesState, kind: int, x: float, z: float, yaw: float) -> Vehicle { function ve_add(vehicles_st: VehiclesState, kind: int, owner: int, x: float, z: float, yaw: float) -> Vehicle {
if kind != VEHICLE_HORSE and kind != VEHICLE_BOAT { return null } if kind != VEHICLE_HORSE and kind != VEHICLE_BOAT { return null }
let v = new Vehicle let v = new Vehicle
v.nid = VehicleWorld.next_id() v.nid = VehicleWorld.next_id()
v.kind = kind v.kind = kind
v.owner = owner
v.yaw = yaw v.yaw = yaw
if kind == VEHICLE_HORSE { if kind == VEHICLE_HORSE {
v.seat_h = 1.35 v.seat_h = 1.35
@ -14,7 +15,7 @@ function ve_add(vehicles_st: VehiclesState, kind: int, x: float, z: float, yaw:
v.seat_h = 0.30 v.seat_h = 0.30
} }
ve_put(v, x, z) ve_put(v, x, z)
push(ve_all(vehicles_st), v) ve_file(vehicles_st.ve, v)
ve_settle(v) ve_settle(v)
VehicleWorld.made(v) VehicleWorld.made(v)
return v return v
@ -51,9 +52,8 @@ export function vehicle_call_for(vehicles_st: VehiclesState, pid: int, kind: int
var yaw = vehicles_st.ve_dock_yaw var yaw = vehicles_st.ve_dock_yaw
if kind == VEHICLE_HORSE { yaw = vehicles_st.ve_rail_yaw } if kind == VEHICLE_HORSE { yaw = vehicles_st.ve_rail_yaw }
if v == null { if v == null {
let made = ve_add(vehicles_st, kind, vehicle_place_x(vehicles_st, kind, k), vehicle_place_z(vehicles_st, kind, k), yaw) let made = ve_add(vehicles_st, kind, pid, vehicle_place_x(vehicles_st, kind, k), vehicle_place_z(vehicles_st, kind, k), yaw)
if made == null { return false } if made == null { return false }
made.owner = pid
if pid == 0 { ve_apply_kept(vehicles_st, made) } if pid == 0 { ve_apply_kept(vehicles_st, made) }
return true return true
} }
@ -68,10 +68,10 @@ export function vehicle_call_for(vehicles_st: VehiclesState, pid: int, kind: int
# a player left: their boat and horse go with them, and their places are free # a player left: their boat and horse go with them, and their places are free
export function vehicles_release(vehicles_st: mut VehiclesState, pid: int) -> void { export function vehicles_release(vehicles_st: mut VehiclesState, pid: int) -> void {
if pid == 0 { return } if pid == 0 { return }
let keep = new []Vehicle let rows = ix_rows(vehicles_st.ve.owners, pid)
let l = ve_all(vehicles_st) while len(rows) > 0 {
for i in 0 .. len(l) { let v = ve_all(vehicles_st)[rows[len(rows) - 1]]
if l[i].owner != pid { push(keep, l[i]) } else { ve_gone(l[i]) } ve_gone(v)
ve_unfile(vehicles_st.ve, v)
} }
vehicles_st.ve_list = keep
} }

View file

@ -5,6 +5,7 @@ module ludic_vehicles uses ludic_base
numbers float numbers float
import "ludic.base" import "ludic.base"
import "kinds.ludic" import "kinds.ludic"
import "table.ludic"
import "ports.ludic" import "ports.ludic"
import "state.ludic" import "state.ludic"
import "places.ludic" import "places.ludic"

View file

@ -19,6 +19,8 @@ export property Vehicle {
rider: int = -1 # the player on it, -1 nobody rider: int = -1 # the player on it, -1 nobody
home_x: float = 0.0 # a horse is tied where its home is home_x: float = 0.0 # a horse is tied where its home is
home_z: float = 0.0 home_z: float = 0.0
ent: int = -1 # its handle in the table
tab: Table<Vehicle> = null
} }
export const VEHICLE_MOUNTED: int = 0 export const VEHICLE_MOUNTED: int = 0

View file

@ -45,7 +45,7 @@ export state VehiclesState {
ve_ids: int = 0 ve_ids: int = 0
ve_sx: float = 0.0 ve_sx: float = 0.0
ve_sz: float = 0.0 ve_sz: float = 0.0
ve_list: []Vehicle = new []Vehicle ve: VehicleTable = ve__new()
ve_cur: Vehicle = null ve_cur: Vehicle = null
ve_dock_x: float = 0.0 ve_dock_x: float = 0.0
ve_dock_z: float = 0.0 ve_dock_z: float = 0.0

View file

@ -18,7 +18,7 @@ function ve_say(vehicles_st: VehiclesState, what: int, v: Vehicle, n: float) ->
q_push(vehicle_facts(vehicles_st), f) q_push(vehicle_facts(vehicles_st), f)
} }
function ve_all(vehicles_st: VehiclesState) -> []Vehicle { return vehicles_st.ve_list } function ve_all(vehicles_st: VehiclesState) -> []Vehicle { return vehicles_st.ve.tab.rec }
# every vehicle in the valley, to draw and to aim at (change one only through the verbs) # every vehicle in the valley, to draw and to aim at (change one only through the verbs)
export function vehicles_all(vehicles_st: VehiclesState) -> []Vehicle { return ve_all(vehicles_st) } export function vehicles_all(vehicles_st: VehiclesState) -> []Vehicle { return ve_all(vehicles_st) }
@ -32,8 +32,9 @@ export function vehicle_ride_kind(vehicles_st: VehiclesState) -> int {
export function vehicle_current(vehicles_st: VehiclesState) -> Vehicle { return vehicles_st.ve_cur } export function vehicle_current(vehicles_st: VehiclesState) -> Vehicle { return vehicles_st.ve_cur }
export function vehicle_of(vehicles_st: VehiclesState, pid: int, kind: int) -> Vehicle { export function vehicle_of(vehicles_st: VehiclesState, pid: int, kind: int) -> Vehicle {
let rows = ix_rows(vehicles_st.ve.owners, pid)
let l = ve_all(vehicles_st) let l = ve_all(vehicles_st)
for i in 0 .. len(l) { if l[i].kind == kind and l[i].owner == pid { return l[i] } } for k in 0 .. len(rows) { if l[rows[k]].kind == kind { return l[rows[k]] } }
return null return null
} }

View file

@ -2,13 +2,14 @@
# and the horse's hay); where they lie is not saved - they are called again. The boats' hulls are # and the horse's hay); where they lie is not saved - they are called again. The boats' hulls are
# read back and pushed by the wind in vehicles_tick, which the game calls where the world is run # read back and pushed by the wind in vehicles_tick, which the game calls where the world is run
export function vehicles_reset(vehicles_st: mut VehiclesState) -> void { export function vehicles_reset(vehicles_st: mut VehiclesState) -> void {
if vehicles_st.ve_list != null { let l = ve_all(vehicles_st)
for i in 0 .. len(vehicles_st.ve_list) { for i in 0 .. len(l) {
let v = vehicles_st.ve_list[i] let v = l[i]
if v.kind == VEHICLE_BOAT { VehicleHull.gone(v.nid) } else { VehicleLegs.gone(v.nid) } if v.kind == VEHICLE_BOAT { VehicleHull.gone(v.nid) } else { VehicleLegs.gone(v.nid) }
} v.ent = -1
} }
vehicles_st.ve_list = new []Vehicle tb_clear(vehicles_st.ve.tab)
imap_clear(vehicles_st.ve.nids)
vehicles_st.ve_cur = null vehicles_st.ve_cur = null
vehicles_st.ve_have_dock = false vehicles_st.ve_have_dock = false
vehicles_st.ve_swell_said = 0.0 vehicles_st.ve_swell_said = 0.0

View file

@ -0,0 +1,45 @@
# table.ludic - the vehicles, rows of a ludic.base Table: kind and owner as columns, each player's
# own by index, and a nid to its handle. Where one is stays on its record: a boat moves every frame
# and there are never more than two a player to search.
export const VE_KIND: int = 0 # int columns
export const VE_OWNER: int = 1
export property VehicleTable {
tab: Table<Vehicle> = null
owners: IntIndex = null # every vehicle by the player it belongs to
nids: IntMap = null
}
function ve__new() -> VehicleTable {
let w = new VehicleTable
let tb: Table<Vehicle> = table_new(0, 2)
w.tab = tb
w.owners = tb_index(tb, VE_OWNER, -1)
w.nids = imap_new()
return w
}
# a new row for v, its kind and owner from its record
function ve_file(w: VehicleTable, v: Vehicle) -> void {
v.tab = w.tab
v.ent = tb_add(w.tab, v)
let r = tb_row(w.tab, v.ent)
w.tab.i[VE_KIND][r] = v.kind
w.tab.i[VE_OWNER][r] = v.owner
tb_refile(w.tab, r)
imap_put(w.nids, v.nid, v.ent)
}
function ve_unfile(w: VehicleTable, v: Vehicle) -> void {
imap_del(w.nids, v.nid)
tb_remove(w.tab, v.ent)
v.ent = -1
}
# the vehicle a message names, or null
export function vehicle_by_nid(vehicles_st: VehiclesState, nid: int) -> Vehicle {
let tb = vehicles_st.ve.tab
let r = tb_row(tb, imap_get(vehicles_st.ve.nids, nid, -1))
if r < 0 or tb.rec[r].nid != nid { return null }
return tb.rec[r]
}

View file

@ -344,4 +344,23 @@ program VehiclesTest {
expect_near(vehicle_find(vehicles_st, VEHICLE_HORSE).food, 77.0, 0.01) expect_near(vehicle_find(vehicles_st, VEHICLE_HORSE).food, 77.0, 0.01)
expect(not vehicle_find(vehicles_st, VEHICLE_BOAT).owned) expect(not vehicle_find(vehicles_st, VEHICLE_BOAT).owned)
} }
test "the table: a nid finds its vehicle, each player's own by the owner index, and a release takes both" (vehicles_st: mut VehiclesState, vehicles_test_st: mut VehiclesTestState) {
fresh(vehicles_st, vehicles_test_st)
vehicle_call_for(vehicles_st, 5, VEHICLE_HORSE, 0)
vehicle_call_for(vehicles_st, 5, VEHICLE_BOAT, 0)
vehicle_call_for(vehicles_st, 6, VEHICLE_BOAT, 1)
let b = vehicle_of(vehicles_st, 6, VEHICLE_BOAT)
expect(b != null)
expect(vehicle_by_nid(vehicles_st, b.nid) == b)
expect(vehicle_of(vehicles_st, 6, VEHICLE_HORSE) == null)
let h = vehicle_of(vehicles_st, 5, VEHICLE_HORSE)
vehicles_release(vehicles_st, 5)
expect_eq(len(vehicles_all(vehicles_st)), 1)
expect(vehicle_by_nid(vehicles_st, h.nid) == null)
expect(vehicle_of(vehicles_st, 6, VEHICLE_BOAT) == b)
vehicles_reset(vehicles_st)
expect_eq(len(vehicles_all(vehicles_st)), 0)
expect(vehicle_by_nid(vehicles_st, b.nid) == null)
}
} }