-- 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")