Files
2026-11-05 09:57:11 -05:00

254 lines
13 KiB
Lua
Executable File

-- Bounded, deterministic M4 ecology accounting tests.
local target = assert(arg[1], "ecology.lua path required")
local now, nodes, metadata, timers = 100000, {}, {}, {}
local real_time = os.time
os.time = function() return now end
local function key(p) return p.x .. ":" .. p.y .. ":" .. p.z end
local function meta(pos)
local k = key(pos); metadata[k] = metadata[k] or {}
local fields = metadata[k]
return {
get_string = function(_, name) return fields[name] or "" end,
set_string = function(_, name, value) fields[name] = tostring(value) end,
get_int = function(_, name) return tonumber(fields[name]) or 0 end,
set_int = function(_, name, value) fields[name] = math.floor(value) end,
from_table = function(_, value) fields = value.fields or {}; metadata[k] = fields end,
}
end
local api = {
registered_nodes = {air = {buildable_to = true}, ["hearth:grass"] = {}, ["hearth:soil"] = {},
["hearth:crop_1"] = {}, ["hearth:crop_2"] = {}, ["hearth:crop_3"] = {}, ["hearth:crop_4"] = {},
["hearth:berry_bush"] = {}, ["hearth:berry_bush_bare"] = {}, ["hearth:sapling"] = {},
["hearth:log"] = {}, ["hearth:leaves"] = {}},
settings = {get = function() return "512" end},
}
function api.get_node_or_nil(pos) return nodes[key(pos)] or {name="air"} end
function api.get_node(pos) return api.get_node_or_nil(pos) end
function api.set_node(pos, node) nodes[key(pos)] = node; metadata[key(pos)] = {}; timers[key(pos)] = nil end
function api.swap_node(pos, node) nodes[key(pos)] = node end
function api.get_meta(pos) return meta(pos) end
function api.get_node_timer(pos)
local k = key(pos); timers[k] = timers[k] or {}
return {start = function(_, seconds) timers[k].timeout = seconds end,
stop = function() timers[k].timeout = nil end}
end
local permitted, inventory_room, delivered, partial, climate_temperature, season = true, true, 0, false, 0, "Summer"
hearth = {climate_at = function() return {temperature = climate_temperature, season=season} end,
claim_allowed = function() return permitted end,
notify = function(_, message) hearth.last_notice = message end}
local construction = {allowed = function() return permitted end}
hearth.seasons = dofile(target:gsub("[^/\\]+$","seasons.lua"))(hearth,api)
local ecology = dofile(target)(hearth, api, construction)
local function make_stack(text)
local name, count = (text or ""):match("^(%S+)%s*(%d*)$")
return {name = name or "", count = name and (tonumber(count) or 1) or 0,
get_count = function(self) return self.count end,
get_name = function(self) return self.name end,
is_empty = function(self) return self.count == 0 end,
take_item = function(self, amount) self.count = math.max(0, self.count - (amount or 1)) end}
end
ItemStack = make_stack
local inventory = {
room_for_item = function() return inventory_room end,
add_item = function(_, _, item)
local amount = partial and math.max(0, item:get_count() - 1) or item:get_count()
delivered = delivered + amount
return make_stack(amount == item:get_count() and "" or (item:get_name() .. " " .. (item:get_count() - amount)))
end,
}
local player = {is_player = function() return true end, get_player_name = function() return "gardener" end,
get_inventory = function() return inventory end}
local passed = 0
local function test(name, fn)
local ok, err = pcall(fn)
assert(ok, name .. ": " .. tostring(err)); passed = passed + 1
print("ok " .. passed .. " - " .. name)
end
local planted_at = 100000
test("grain needs 120 real growing days and samples the whole interval with bounded work", function()
local cold_days = 4 * 86400
assert(ecology.climate_progress(planted_at, planted_at + cold_days) == 0)
climate_temperature = 15
assert(ecology.crop_stage(planted_at,planted_at+119*86400)<4)
assert(ecology.crop_stage(planted_at,planted_at+120*86400)==4)
assert(ecology.climate_progress(planted_at, planted_at + 90 * 86400)
> ecology.climate_progress(planted_at, planted_at + 30 * 86400))
local calls=0; local climate=hearth.climate_at
hearth.climate_at=function(epoch) calls=calls+1; return climate(epoch) end
assert(ecology.crop_stage(planted_at,planted_at+10000*86400)==4 and calls==366)
hearth.climate_at=climate
assert(ecology.climate_progress("nan", planted_at + 1) == 0)
assert(ecology.climate_progress(planted_at, math.huge) == 0)
end)
test("crop stages derive from a durable planting timestamp independent of update count", function()
local p = {x=0,y=1,z=0}
nodes[key({x=0,y=0,z=0})] = {name="hearth:grass"}
nodes[key(p)] = {name="hearth:crop_1"}
meta(p):set_string("planted_at", tostring(planted_at))
local stage = ecology.crop_stage(planted_at, planted_at + 30 * 86400)
assert(stage >= 2 and stage <= 4)
local same_late_stage = ecology.crop_stage(planted_at, planted_at + 30 * 86400)
assert(stage == same_late_stage)
now = planted_at + 120 * 86400
assert(ecology.update_crop(p) == false and nodes[key(p)].name == "hearth:crop_4")
end)
test("berry regrowth is annual, Summer gated and harvest does not destroy provenance", function()
local p = {x=2,y=1,z=0}
nodes[key(p)] = {name="hearth:berry_bush"}
meta(p):set_string("succ_natural","1")
inventory_room = false
ecology.harvest_berries(p, player)
assert(nodes[key(p)].name == "hearth:berry_bush" and delivered == 0)
inventory_room, partial = true, false
ecology.harvest_berries(p, player)
assert(nodes[key(p)].name == "hearth:berry_bush_bare" and delivered == 3)
local ready = tonumber(meta(p):get_string("ready_at"))
assert(ready == now + 365.2425*86400 and meta(p):get_string("succ_natural")=="1")
ecology.harvest_berries(p, player)
assert(delivered == 3, "bare bush must not yield repeated berries")
assert(ecology.refresh_bush(p) == true and nodes[key(p)].name == "hearth:berry_bush_bare")
now = ready
season="Winter"; assert(ecology.refresh_bush(p)==true)
season="Summer"
assert(ecology.refresh_bush(p) == false and nodes[key(p)].name == "hearth:berry_bush")
end)
test("partial inventory harvest cannot duplicate crops or berries", function()
local crop, bush = {x=4,y=1,z=0}, {x=5,y=1,z=0}
nodes[key(crop)] = {name="hearth:crop_4"}
nodes[key(bush)] = {name="hearth:berry_bush"}
partial, delivered = true, 0
ecology.harvest_crop(crop, player)
assert(nodes[key(crop)].name == "hearth:soil" and delivered == 3)
ecology.harvest_berries(bush, player)
assert(nodes[key(bush)].name == "hearth:berry_bush_bare" and delivered == 5)
partial = false
end)
test("immature or unauthorized crops cannot be harvested", function()
local p = {x=6,y=1,z=0}
nodes[key(p)] = {name="hearth:crop_1"}
local before = delivered
ecology.harvest_crop(p, player)
assert(nodes[key(p)].name == "hearth:crop_1" and delivered == before)
nodes[key(p)] = {name="hearth:crop_4"}
permitted = false
ecology.harvest_crop(p, player)
assert(nodes[key(p)].name == "hearth:crop_4" and delivered == before)
permitted = true
end)
test("generic sapling waits one real year, respects claims, and writes a bounded young tree once", function()
now = 2000000000
local p = {x=8,y=1,z=8}
nodes[key({x=8,y=0,z=8})] = {name="hearth:grass"}
nodes[key(p)] = {name="hearth:sapling"}
meta(p):set_string("planted_at", tostring(now - 3 * 86400))
meta(p):set_string("planter", "gardener")
assert(ecology.grow_sapling(p)==true and nodes[key(p)].name=="hearth:sapling")
now=now+365.2425*86400
permitted = false
assert(ecology.grow_sapling(p) == true and nodes[key(p)].name == "hearth:sapling")
permitted = true
assert(ecology.grow_sapling(p) == false and nodes[key(p)].name == "hearth:log")
assert(nodes[key({x=8,y=4,z=8})].name == "hearth:log")
assert(ecology.grow_sapling(p) == false, "grown tree must not be produced twice")
end)
test("seasonal harvest authorization, empty inventory room and rejected delivery cause no cooldown or yield", function()
local p={x=20,y=1,z=0}
nodes[key(p)]={name="hearth:spring_greens_patch_spring"}
season="Spring"; delivered=0; permitted=false
assert(not ecology.harvest_seasonal(p,player) and delivered==0)
permitted=true; inventory_room=false
assert(not ecology.harvest_seasonal(p,player) and delivered==0)
inventory_room=true; partial=true
assert(not ecology.harvest_seasonal(p,player) and delivered==0)
assert(meta(p):get_string("succ_harvest_ready_at")=="")
partial=false
end)
test("seasonal picking is one food per day without erasing ecological age or provenance", function()
local p={x=21,y=1,z=0}
nodes[key(p)]={name="hearth:spring_greens_patch_spring",param2=2}
meta(p):set_string("succ_age","1200"); meta(p):set_string("succ_natural","1")
meta(p):set_string("owner","gardener")
delivered=0; season="Spring"
assert(ecology.harvest_seasonal(p,player) and delivered==1)
local ready=tonumber(meta(p):get_string("succ_harvest_ready_at"))
assert(ready==now+86400 and not ecology.harvest_seasonal(p,player))
assert(meta(p):get_string("succ_age")=="1200" and meta(p):get_string("succ_natural")=="1")
assert(meta(p):get_string("owner")=="gardener")
season="Winter"; hearth.seasons.reconcile(p)
season="Spring"; hearth.seasons.reconcile(p)
assert(not ecology.harvest_seasonal(p,player) and delivered==1)
now=ready; assert(ecology.harvest_seasonal(p,player) and delivered==2)
assert(tonumber(meta(p):get_string("succ_harvest_ready_at"))==now+86400)
end)
test("forage availability checks the calendar even before appearance has reconciled", function()
local greens,mushrooms={x=22,y=1,z=0},{x=23,y=1,z=0}
nodes[key(greens)]={name="hearth:spring_greens_patch_spring"}
nodes[key(mushrooms)]={name="hearth:autumn_mushrooms_patch"}
delivered=0
for _,s in ipairs({"Summer","Winter","Autumn"}) do
season=s; assert(not ecology.harvest_seasonal(greens,player))
end
season="Spring"; assert(not ecology.harvest_seasonal(mushrooms,player))
season="Autumn"; assert(ecology.harvest_seasonal(mushrooms,player) and delivered==1)
-- New adapter instance models restart; its deadline remains in node metadata.
local reload=dofile(target)(hearth,api,construction)
assert(not reload.harvest_seasonal(mushrooms,player) and delivered==1)
end)
test("berry and seasonal reservations precede reentrant inventory callbacks",function()
local berry,patch={x=30,y=1,z=0},{x=31,y=1,z=0}
nodes[key(berry)]={name="hearth:berry_bush"}
nodes[key(patch)]={name="hearth:spring_greens_patch_spring"}
local original=inventory.add_item
local inside=false
inventory.add_item=function(self,list,item)
assert(not inside,"a nested harvest reached inventory")
inside=true
assert(not ecology.harvest_berries(berry,player))
assert(not ecology.harvest_seasonal(patch,player))
inside=false
return original(self,list,item)
end
season="Spring"
-- Lock the other resource so both recursive calls are unambiguously denied.
meta(patch):set_string("succ_harvest_ready_at",tostring(now+86400))
assert(ecology.harvest_berries(berry,player))
meta(patch):set_string("succ_harvest_ready_at","")
assert(ecology.harvest_seasonal(patch,player))
inventory.add_item=original
end)
test("failed delivery restores readiness and malformed fruit or patch clocks never refill",function()
local p={x=32,y=1,z=0}
local original=inventory.add_item
inventory.add_item=function(_,_,item) return make_stack(item:get_name().." "..item:get_count()) end
nodes[key(p)]={name="hearth:berry_bush"}
assert(not ecology.harvest_berries(p,player) and meta(p):get_string("ready_at")=="")
inventory.add_item=original
for _,bad in ipairs({"broken","-1","inf","nan"}) do
nodes[key(p)]={name="hearth:berry_bush"}; meta(p):set_string("ready_at",bad)
local before=delivered
assert(not ecology.harvest_berries(p,player) and delivered==before)
nodes[key(p)]={name="hearth:berry_bush_bare"}
assert(not ecology.refresh_bush(p) and nodes[key(p)].name=="hearth:berry_bush_bare")
nodes[key(p)]={name="hearth:spring_greens_patch_spring"}
meta(p):set_string("succ_harvest_ready_at",bad); season="Spring"
assert(not ecology.harvest_seasonal(p,player) and delivered==before)
end
end)
test("annual berry lock survives adapter restart, node variants and a one-day return",function()
local p={x=33,y=1,z=0}
nodes[key(p)]={name="hearth:berry_bush"}
assert(ecology.harvest_berries(p,player))
local due=meta(p):get_string("ready_at"); now=now+86400
local reload=dofile(target)(hearth,api,construction)
assert(reload.refresh_bush(p) and meta(p):get_string("ready_at")==due)
local before=delivered; reload.harvest_berries(p,player); assert(delivered==before)
-- Even a mistakenly restored full-looking node cannot reset its receipt.
nodes[key(p)]={name="hearth:berry_bush"}
assert(not reload.harvest_berries(p,player) and delivered==before)
end)
os.time = real_time
print("PASS " .. passed .. " bounded ecology tests")