254 lines
13 KiB
Lua
Executable File
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")
|