1170 lines
59 KiB
Lua
Executable File
1170 lines
59 KiB
Lua
Executable File
-- Standalone Lua 5.1+; deterministic loaded-world fixture, no engine required.
|
|
local target = assert(arg[1], "succession.lua path required")
|
|
local original_time, passed = os.time, 0
|
|
local now, world, fields, protected, claims, missing, light, humidity, temperature, season
|
|
local api, hearth, sim, probes, writes, reads, players, lbm, real_positions
|
|
local p = {x=0,y=1,z=0}
|
|
local function key(q) return q.x .. ":" .. q.y .. ":" .. q.z end
|
|
local function pos(x,y,z) return {x=x or 0,y=y or 1,z=z or 0} end
|
|
local function metadata(q)
|
|
local k=key(q); fields[k]=fields[k] or {}
|
|
local f=fields[k]
|
|
return {get_string=function(_,name) return f[name] or "" end,
|
|
set_string=function(_,name,value) f[name]=tostring(value) end,
|
|
to_table=function() return {fields=f} end}
|
|
end
|
|
local function put(q,k,natural)
|
|
world[key(q)]={name=k=="air" and k or "hearth:"..k}
|
|
if natural then metadata(q):set_string("succ_natural","1") end
|
|
end
|
|
local function name(q) return api.get_node_or_nil(q).name end
|
|
local function load()
|
|
probes=0
|
|
sim=dofile(target)(hearth,api)
|
|
end
|
|
local function reset()
|
|
p=pos(0,1,0)
|
|
now,world,fields,protected,claims,missing=1000000,{},{},{},{},{}
|
|
light,humidity,temperature,season=15,60,15,"Summer"
|
|
writes,reads,players,real_positions=0,0,true,false
|
|
api={}
|
|
function api.get_meta(q) return metadata(q) end
|
|
function api.get_node_or_nil(q)
|
|
reads=reads+1
|
|
if missing[key(q)] then return nil end
|
|
return world[key(q)] or {name=q.y==0 and "hearth:grass" or "air"}
|
|
end
|
|
function api.swap_node(q,n) writes=writes+1; world[key(q)]=n end
|
|
function api.get_node_light() return light end
|
|
function api.get_biome_data() return {humidity=humidity} end
|
|
function api.register_globalstep(fn) api.step=fn end
|
|
function api.register_lbm(def) lbm=def end
|
|
function api.get_connected_players()
|
|
if not players then return {} end
|
|
return {{get_pos=function()
|
|
-- Follow the deterministic scan so its first column is our test plot.
|
|
-- Subsequent probes of the same node at the same timestamp earn no time.
|
|
local index=probes%625; probes=probes+1
|
|
if real_positions then return pos(0,1,0) end
|
|
return pos(p.x+12-index%25,p.y,p.z+12-math.floor(index/25))
|
|
end}}
|
|
end
|
|
hearth={claim_owner_at=function(q) return claims[key(q)] and "owner" end,
|
|
claim_allowed=function(q, who) return who ~= "" and not claims[key(q)] end,
|
|
external_protected=function(q) return protected[key(q)] end,
|
|
climate_at=function() return {temperature=temperature,season=season} end}
|
|
hearth.seasons=dofile(target:gsub("[^/\\]+$","seasons.lua"))(hearth,api)
|
|
hearth.lifecycle=dofile(target:gsub("[^/\\]+$","lifecycle.lua"))
|
|
os.time=function() return now end
|
|
load()
|
|
end
|
|
local function advance(dt)
|
|
now=now+(dt or 60)
|
|
assert(sim.step(1)==sim.BUDGET)
|
|
end
|
|
local function baseline(k,age)
|
|
if k then put(p,k,true) end
|
|
sim.step(1)
|
|
if age then metadata(p):set_string("succ_age",age) end
|
|
end
|
|
local function field(q,f) return tonumber(metadata(q):get_string(f)) or 0 end
|
|
local DAY,YEAR=86400,365.2425*86400
|
|
local function aged(seconds,q)
|
|
q=q or p
|
|
metadata(q):set_string("succ_initial_age_seconds",tostring(seconds))
|
|
metadata(q):set_string("succ_birth_at",tostring(now))
|
|
metadata(q):set_string("succ_last_observed_at",tostring(now))
|
|
end
|
|
local function test(label,fn)
|
|
reset()
|
|
local ok,err=pcall(fn)
|
|
assert(ok,label..": "..tostring(err)); passed=passed+1
|
|
print("ok "..passed.." - "..label)
|
|
end
|
|
|
|
test("existing natural ground recruits native grass without regeneration",function()
|
|
baseline()
|
|
advance(60); advance(60)
|
|
assert(name(p)=="air")
|
|
advance(60)
|
|
assert(name(p)=="hearth:meadow_grass")
|
|
assert(metadata(p):get_string("succ_natural")=="1")
|
|
end)
|
|
test("nutrients accelerate establishment and are consumed rather than copied",function()
|
|
metadata(pos(0,0,0)):set_string("succ_nutrients","4")
|
|
baseline(); advance(100)
|
|
assert(name(p)=="hearth:meadow_grass")
|
|
assert(field(pos(0,0,0),"succ_nutrients")==3)
|
|
end)
|
|
test("sunny mixed meadow recruits wildflowers, dense grass excludes them",function()
|
|
put(pos(1),"meadow_grass",true); put(pos(-1),"meadow_grass",true)
|
|
baseline(); advance(100); advance(100)
|
|
assert(name(p)=="hearth:wildflowers")
|
|
for x=-2,2 do for z=-2,2 do
|
|
if x~=0 or z~=0 then put(pos(x,1,z),"meadow_grass",true) end
|
|
end end
|
|
for _=1,6 do advance(100) end
|
|
assert(name(p)=="hearth:litter")
|
|
end)
|
|
test("mature canopy shades out grasses and favors moist ferns",function()
|
|
put(pos(1),"mature_tree",true); put(pos(-1),"mature_tree",true); put(pos(0,1,1),"mature_tree",true)
|
|
baseline("meadow_grass")
|
|
assert(sim.habitat(p).light==6)
|
|
for _=1,6 do advance(100) end
|
|
assert(name(p)=="hearth:litter")
|
|
put(p,"air"); metadata(p):set_string("succ_age","180")
|
|
advance(100)
|
|
assert(name(p)=="hearth:fern")
|
|
end)
|
|
test("dry exposed fern dies, while river moisture changes its niche",function()
|
|
light,humidity=14,20
|
|
baseline("fern")
|
|
for _=1,6 do advance(100) end
|
|
assert(name(p)=="hearth:litter")
|
|
put(pos(1,0,0),"water_source")
|
|
assert(sim.habitat(p).water==1 and sim.habitat(p).moisture>0.2)
|
|
end)
|
|
test("generic tree needs a real year to establish and ten to mature, never dies at a short deadline",function()
|
|
baseline("tree_seedling",21500); advance(100)
|
|
assert(name(p)=="hearth:tree_seedling" and field(p,"succ_age")==21500)
|
|
aged(YEAR); advance(100)
|
|
assert(name(p)=="hearth:young_tree" and field(p,"succ_age")==21500)
|
|
aged(10*YEAR); advance(100)
|
|
assert(name(p)=="hearth:mature_tree")
|
|
aged(10000*YEAR); advance(100)
|
|
assert(name(p)=="hearth:mature_tree" and field(pos(0,0,0),"succ_nutrients")==0)
|
|
end)
|
|
test("established sparse meadow eventually recruits a native tree",function()
|
|
baseline("meadow_grass",7100); advance(100)
|
|
assert(name(p)=="hearth:tree_seedling")
|
|
end)
|
|
test("dense meadow delays rather than permanently preventing woodland recruitment",function()
|
|
for x=-2,2 do for z=-2,2 do put(pos(x,1,z),"meadow_grass",true) end end
|
|
baseline("meadow_grass",7100); advance(100)
|
|
assert(name(p)=="hearth:meadow_grass")
|
|
metadata(p):set_string("succ_age","14300"); advance(100)
|
|
assert(name(p)=="hearth:tree_seedling")
|
|
end)
|
|
test("mature tree disperses one seed on a durable attempt schedule",function()
|
|
baseline("mature_tree",2000)
|
|
metadata(p):set_string("succ_serial","9")
|
|
advance(100)
|
|
local seed=pos(-1,1,0) -- serial 10 selects offset 3
|
|
assert(name(seed)=="hearth:tree_seedling")
|
|
local before=writes
|
|
sim.step(99999)
|
|
assert(writes==before and field(p,"succ_serial")==10)
|
|
end)
|
|
test("grass spreads locally but never replaces occupied flower or crop cells",function()
|
|
baseline("meadow_grass",1000)
|
|
metadata(p):set_string("succ_serial","2")
|
|
put(pos(0,1,-1),"wildflowers",true) -- serial 3 selects offset 4
|
|
advance(100)
|
|
assert(name(pos(0,1,-1))=="hearth:wildflowers")
|
|
metadata(p):set_string("succ_serial","2")
|
|
put(pos(0,1,-1),"air"); advance(100)
|
|
assert(name(pos(0,1,-1))=="hearth:meadow_grass")
|
|
end)
|
|
test("moist shaded litter seeds fungi, then returns finite nutrients",function()
|
|
light=10
|
|
baseline("litter",300); metadata(p):set_string("succ_decay_initial","300"); advance(100)
|
|
assert(name(pos(0,1,1))=="hearth:fungi") -- first attempt selects offset 2
|
|
now=now+90*DAY; advance(100); advance(100)
|
|
assert(name(p)=="air" and field(pos(0,0,0),"succ_nutrients")==1)
|
|
advance(1)
|
|
assert(field(pos(0,0,0),"succ_nutrients")==1)
|
|
end)
|
|
test("dung decomposes, fertilizes soil and does not instantly sprout a forest",function()
|
|
assert(sim.deposit(p))
|
|
local patch=pos(-1,1,-1)
|
|
assert(name(patch)=="hearth:dung")
|
|
-- Move the patch to the scheduler's plot without resetting its natural marker.
|
|
put(p,"dung",true); baseline(nil,500); advance(100)
|
|
assert(name(p)=="air" and field(pos(0,0,0),"succ_nutrients")==4)
|
|
end)
|
|
test("grazing debits one partial canopy ration, not flowers or planted nodes",function()
|
|
put(p,"meadow_grass",true); put(pos(1),"fern",true)
|
|
assert(sim.forage(p))
|
|
assert(name(p)=="hearth:meadow_grass" and name(pos(1))=="hearth:fern")
|
|
assert(field(p,"succ_browse_ready_at")==now+7*DAY)
|
|
put(pos(1),"wildflowers",true)
|
|
assert(not sim.forage(p))
|
|
put(p,"meadow_grass",false); metadata(p):set_string("succ_natural","")
|
|
assert(not sim.forage(p))
|
|
end)
|
|
test("grazing retains the perennial and shares seven-day regrowth across animals",function()
|
|
baseline("meadow_grass")
|
|
assert(sim.forage(p))
|
|
assert(not sim.forage(p))
|
|
load(); assert(not sim.forage(p))
|
|
now=now+7*DAY
|
|
assert(sim.forage(p) and name(p)=="hearth:meadow_grass")
|
|
end)
|
|
test("source claims and external protection block all simulation writes",function()
|
|
baseline("mature_tree",259100)
|
|
protected[key(p)]=true; advance(100)
|
|
assert(name(p)=="hearth:mature_tree" and writes==0)
|
|
protected={}; claims[key(p)]=true; advance(100)
|
|
assert(writes==0 and not sim.deposit(p) and not sim.forage(p))
|
|
end)
|
|
test("support protection and markers block source decay and grazing",function()
|
|
baseline("meadow_grass",21600)
|
|
protected[key(pos(0,0,0))]=true; advance(100)
|
|
assert(writes==0 and not sim.forage(p))
|
|
protected={}; metadata(pos(0,0,0)):set_string("placed_by","builder"); advance(100)
|
|
assert(writes==0 and not sim.forage(p))
|
|
end)
|
|
test("seed dispersal respects destination AND destination-support protection",function()
|
|
baseline("mature_tree",2000)
|
|
local seed=pos(-1,1,0)
|
|
protected[key(seed)]=true
|
|
metadata(p):set_string("succ_serial","9"); advance(100)
|
|
assert(name(seed)=="air")
|
|
protected={}; claims[key(pos(-1,0,0))]=true
|
|
metadata(p):set_string("succ_serial","9"); advance(100)
|
|
assert(name(seed)=="air")
|
|
end)
|
|
test("any supported placement marker opts the plot out of nature",function()
|
|
for _,fieldname in ipairs({"owner","planter","placed_by","player_placed","hearth_placed","hearth_player_placed","planted_at","third_party:hand_placed"}) do
|
|
fields={}; metadata(p):set_string(fieldname,"1")
|
|
baseline(); advance(100); advance(100)
|
|
assert(name(p)=="air",fieldname)
|
|
end
|
|
end)
|
|
test("builds, farms, water and nonnatural plants are never overwritten",function()
|
|
for _,k in ipairs({"wall","crop_1","water_source","sapling","berry_bush"}) do
|
|
world,fields={},{}
|
|
put(p,k); baseline(); advance(100); advance(100)
|
|
assert(name(p)=="hearth:"..k,k)
|
|
end
|
|
world,fields={},{}; put(pos(1),"crop_4"); baseline(); advance(100); advance(100)
|
|
assert(name(p)=="air", "adjacent farm buffer")
|
|
end)
|
|
test("unloaded source, support, or destination never cause writes or force loading",function()
|
|
baseline("mature_tree",2000)
|
|
missing[key(p)]=true; advance(100); assert(writes==0)
|
|
missing={}; missing[key(pos(0,0,0))]=true; advance(100); assert(writes==0)
|
|
missing={}; missing[key(pos(-1,1,0))]=true
|
|
metadata(p):set_string("succ_serial","9"); advance(100)
|
|
assert(writes==0)
|
|
end)
|
|
test("work and node reads remain bounded after enormous frame stalls",function()
|
|
real_positions=true
|
|
reads=0; local work=sim.step(1000000000)
|
|
assert(work==8 and probes==8)
|
|
assert(reads<3000,"bounded stencil reads: "..reads)
|
|
players=false; reads=0
|
|
assert(sim.step(999999)==0 and reads==0)
|
|
end)
|
|
test("fixed total budget visits every column fairly across multiple players",function()
|
|
local columns, counts, current, first = {{},{}}, {0,0}, 0, false
|
|
api.get_connected_players=function()
|
|
local result={}
|
|
for i=1,2 do
|
|
local who=i
|
|
result[i]={get_pos=function() current=who; first=true; counts[who]=counts[who]+1; return pos(who*100,1,0) end}
|
|
end
|
|
return result
|
|
end
|
|
api.get_node_or_nil=function(q)
|
|
if first then columns[current][key(q)]=true; first=false end
|
|
return nil
|
|
end
|
|
for _=1,157 do assert(sim.step(1)==8) end
|
|
for i=1,2 do
|
|
local count=0; for _ in pairs(columns[i]) do count=count+1 end
|
|
assert(count==625 and counts[i]==628)
|
|
end
|
|
assert(sim.CACHE_LIMIT>=625*10 and sim.MAX_GAP>=625*10/sim.BUDGET)
|
|
end)
|
|
test("vegetation reload alone invalidates age even if its supporting block stayed loaded",function()
|
|
baseline("mature_tree",259100)
|
|
lbm.action(p); advance(100)
|
|
assert(name(p)=="hearth:mature_tree" and field(p,"succ_age")==259100)
|
|
end)
|
|
test("calendar anchor survives absence, disconnect, unload and restart; rollback pauses at high water",function()
|
|
baseline("tree_seedling",21500)
|
|
local born=field(p,"succ_birth_at")
|
|
advance(864000); assert(name(p)=="hearth:tree_seedling" and field(p,"succ_age")==21500)
|
|
players=false; sim.step(1); players=true
|
|
advance(100); assert(field(p,"succ_age")==21500)
|
|
lbm.action(pos(0,0,0)); advance(100); assert(field(p,"succ_age")==21500)
|
|
load(); advance(100); assert(field(p,"succ_age")==21500)
|
|
advance(-100); assert(field(p,"succ_age")==21500)
|
|
advance(100); assert(name(p)=="hearth:tree_seedling")
|
|
assert(field(p,"succ_birth_at")==born and field(p,"succ_last_observed_at")==now)
|
|
advance(YEAR); advance(100); assert(name(p)=="hearth:young_tree")
|
|
end)
|
|
test("failed dispersal attempts survive restart without reroll or advancing age",function()
|
|
baseline("mature_tree",2000)
|
|
metadata(p):set_string("succ_serial","9")
|
|
protected[key(pos(-1,1,0))]=true; advance(100)
|
|
assert(field(p,"succ_serial")==10)
|
|
local age,due=field(p,"succ_age"),field(p,"succ_next")
|
|
protected={}; load(); sim.step(1)
|
|
assert(field(p,"succ_serial")==10 and field(p,"succ_next")==due)
|
|
advance(30)
|
|
assert(field(p,"succ_serial")==10 and name(pos(-1,1,0))=="air")
|
|
assert(field(p,"succ_age")==age and field(p,"succ_last_observed_at")==now)
|
|
end)
|
|
test("cold weather pauses recruitment and stage growth without catchup",function()
|
|
temperature=-5; baseline(); advance(100); advance(100)
|
|
assert(name(p)=="air")
|
|
put(p,"tree_seedling",true); metadata(p):set_string("succ_age","21600"); advance(100)
|
|
assert(name(p)=="hearth:tree_seedling")
|
|
end)
|
|
test("habitat is read only and malformed API coordinates fail closed",function()
|
|
put(p,"meadow_grass",true); put(pos(1),"wildflowers",true); put(pos(-1),"fern",true)
|
|
local h=sim.habitat(p)
|
|
assert(h.grass==1 and h.flowers==1 and h.cover==1 and h.loaded)
|
|
assert(writes==0 and metadata(p):get_string("succ_age")=="")
|
|
for _,invalid in ipairs({{}, {x=0/0,y=1,z=0}, {x=math.huge,y=1,z=0}, {x=999999,y=1,z=0}}) do
|
|
assert(not sim.forage(invalid) and not sim.deposit(invalid) and not sim.habitat(invalid).loaded)
|
|
end
|
|
end)
|
|
|
|
test("season swaps preserve natural identity and active age; winter pauses perennial senescence",function()
|
|
baseline("meadow_grass",1000)
|
|
season,temperature="Winter",-5
|
|
advance(100)
|
|
assert(name(p)=="hearth:meadow_grass_winter" and field(p,"succ_age")==1000)
|
|
assert(metadata(p):get_string("succ_natural")=="1" and sim.habitat(p).grass==1)
|
|
for _=1,7 do advance(100) end
|
|
assert(name(p)=="hearth:meadow_grass_winter" and field(p,"succ_age")==1000)
|
|
season,temperature="Spring",15; advance(100)
|
|
assert(name(p)=="hearth:meadow_grass" and field(p,"succ_age")==1100)
|
|
end)
|
|
test("spring greens recruit moist sunny gaps, mushrooms autumn shade; dormant plants are occupied",function()
|
|
season="Spring"; baseline(); advance(100); advance(100)
|
|
assert(name(p)=="hearth:spring_greens_patch_spring")
|
|
metadata(p):set_string("succ_harvest_ready_at",tostring(now+86400))
|
|
season="Summer"; advance(100)
|
|
assert(name(p)=="hearth:spring_greens_patch" and field(p,"succ_harvest_ready_at")>now)
|
|
assert(not sim.deposit(p) or name(p)=="hearth:spring_greens_patch")
|
|
reset(); season,light,humidity="Autumn",8,70
|
|
baseline(); advance(100); advance(100)
|
|
assert(name(p)=="hearth:autumn_mushrooms_patch_autumn")
|
|
reset(); season,light,humidity="Autumn",15,70
|
|
baseline(); advance(100); advance(100)
|
|
assert(name(p)=="hearth:meadow_grass")
|
|
end)
|
|
test("forage dispersal respects niches, occupation and durable harvest metadata",function()
|
|
season="Spring"; baseline("spring_greens_patch_spring",1000)
|
|
metadata(p):set_string("succ_harvest_ready_at",tostring(now+86400))
|
|
metadata(p):set_string("succ_serial","2")
|
|
advance(100)
|
|
assert(name(pos(0,1,-1))=="hearth:spring_greens_patch_spring")
|
|
assert(field(p,"succ_harvest_ready_at")>now)
|
|
metadata(p):set_string("succ_serial","2"); advance(100)
|
|
assert(name(pos(0,1,-1))=="hearth:spring_greens_patch_spring")
|
|
end)
|
|
test("tree mesh growth denies foreign, marked, claimed and unloaded target volume",function()
|
|
for _, obstacle in ipairs({"foreign","placed","claimed","unloaded"}) do
|
|
reset(); baseline("young_tree",86300)
|
|
local q=pos(2,4,0)
|
|
if obstacle=="foreign" then put(q,"wall") end
|
|
if obstacle=="placed" then metadata(q):set_string("player_placed","1") end
|
|
if obstacle=="claimed" then claims[key(q)]=true end
|
|
if obstacle=="unloaded" then missing[key(q)]=true end
|
|
advance(100)
|
|
assert(name(p)=="hearth:young_tree",obstacle)
|
|
end
|
|
end)
|
|
test("leafless winter canopy admits more light than summer foliage",function()
|
|
put(p,"mature_tree",true)
|
|
local summer=sim.habitat(p)
|
|
season="Winter"
|
|
local winter=sim.habitat(p)
|
|
assert(winter.canopy<summer.canopy and winter.light>summer.light)
|
|
assert(math.abs(winter.canopy-0.15)<0.0001)
|
|
end)
|
|
test("winter mature canopy still sheds litter and decomposition returns nutrients",function()
|
|
season,temperature="Winter",-5; baseline("mature_tree",2000)
|
|
metadata(p):set_string("succ_serial","4"); advance(100)
|
|
assert(name(pos(-2,1,-1))=="hearth:litter")
|
|
put(p,"litter",true); metadata(p):set_string("succ_age","1700")
|
|
advance(100); now=now+90*DAY; advance(100); advance(100)
|
|
assert(name(p)=="air" and field(pos(0,0,0),"succ_nutrients")==1)
|
|
end)
|
|
test("seasonal probes reconcile old canopy and handplaced plants within twelve heights",function()
|
|
season="Autumn"
|
|
put(pos(0,5,0),"leaves"); put(p,"wildflowers")
|
|
metadata(p):set_string("player_placed","1")
|
|
metadata(p):set_string("succ_age","912")
|
|
sim.step(1)
|
|
assert(name(pos(0,5,0))=="hearth:leaves_autumn")
|
|
assert(name(p)=="hearth:wildflowers_autumn" and field(p,"succ_age")==912)
|
|
assert(metadata(p):get_string("player_placed")=="1")
|
|
end)
|
|
test("season appearance has a fixed 96 swap ceiling after a stalled frame",function()
|
|
season="Winter"; real_positions=true
|
|
api.get_node_or_nil=function(q)
|
|
reads=reads+1
|
|
return world[key(q)] or {name="hearth:leaves"}
|
|
end
|
|
assert(sim.step(999999)==8)
|
|
assert(writes==96 and reads<400)
|
|
end)
|
|
test("incomplete habitat and protected source write no ecological metadata",function()
|
|
baseline("tree_seedling",1000)
|
|
local age,serial,due=field(p,"succ_age"),field(p,"succ_serial"),field(p,"succ_next")
|
|
missing[key(pos(2,4,2))]=true
|
|
advance(100)
|
|
assert(field(p,"succ_age")==age and field(p,"succ_serial")==serial and field(p,"succ_next")==due)
|
|
assert(writes==0)
|
|
missing={}; protected[key(p)]=true; advance(100)
|
|
assert(field(p,"succ_age")==age and field(p,"succ_serial")==serial and field(p,"succ_next")==due)
|
|
assert(writes==0)
|
|
end)
|
|
|
|
local function diverse()
|
|
hearth.diversity=dofile(target:gsub("[^/\\]+$","diversity.lua"))(hearth,api)
|
|
load()
|
|
end
|
|
test("unmarked mapgen deadwood decays after a calendar year with finite capped nutrients",function()
|
|
diverse(); humidity=20; put(p,"deadwood"); baseline()
|
|
assert(metadata(p):get_string("succ_natural")=="1" and field(p,"succ_age")==0)
|
|
for _=1,71 do advance(100) end
|
|
assert(name(p)=="hearth:deadwood" and field(p,"succ_age")==7100)
|
|
assert(field(pos(0,0,0),"succ_nutrients")==0)
|
|
now=now+YEAR; advance(100); advance(100)
|
|
assert(name(p)=="hearth:litter" and field(p,"succ_age")==0)
|
|
assert(field(pos(0,0,0),"succ_nutrients")==3)
|
|
local before=writes; sim.step(999999)
|
|
assert(writes==before and field(pos(0,0,0),"succ_nutrients")==3)
|
|
now=now+90*DAY; advance(100); advance(100)
|
|
assert(name(p)=="air" and field(pos(0,0,0),"succ_nutrients")==4)
|
|
advance(1); assert(field(pos(0,0,0),"succ_nutrients")==4)
|
|
reset(); diverse(); humidity=20
|
|
metadata(pos(0,0,0)):set_string("succ_nutrients","7")
|
|
put(p,"deadwood"); baseline(nil,7100); now=now+YEAR; advance(100); advance(100)
|
|
assert(name(p)=="hearth:litter" and field(pos(0,0,0),"succ_nutrients")==8)
|
|
end)
|
|
test("deadwood calendar survives absence, disconnect, reload and restart; rollback pauses",function()
|
|
diverse(); humidity=20; put(p,"deadwood"); baseline(nil,7100)
|
|
advance(864000); assert(name(p)=="hearth:deadwood")
|
|
local born=field(p,"succ_decay_at")
|
|
players=false; sim.step(1); players=true; advance(100)
|
|
lbm.action(p); advance(100)
|
|
lbm.action(pos(0,0,0)); advance(100)
|
|
load(); advance(100)
|
|
local high=field(p,"succ_decay_seen_at")
|
|
advance(-100); assert(field(p,"succ_decay_seen_at")==high)
|
|
assert(field(p,"succ_decay_at")==born)
|
|
assert(field(pos(0,0,0),"succ_nutrients")==0)
|
|
now=now+YEAR; advance(100); advance(100)
|
|
assert(name(p)=="hearth:litter" and field(pos(0,0,0),"succ_nutrients")==3)
|
|
end)
|
|
test("species stages keep one birth anchor, real profile thresholds and never die at lifespan endpoints",function()
|
|
diverse(); baseline("birch_seedling",21500); aged(5*YEAR); advance(100)
|
|
assert(name(p)=="hearth:birch_young" and metadata(p):get_string("succ_species")=="birch")
|
|
local born=field(p,"succ_birth_at")
|
|
metadata(p):set_string("succ_age","30000"); advance(100)
|
|
assert(name(p)=="hearth:birch_young","young trees must not take herb senescence")
|
|
aged(20*YEAR); advance(100)
|
|
assert(name(p)=="hearth:birch_mature")
|
|
aged(1000*YEAR); advance(100)
|
|
assert(name(p)=="hearth:birch_mature" and metadata(p):get_string("succ_species")=="birch")
|
|
assert(born>0 and metadata(p):get_string("succ_lifecycle_origin")=="legacy_established")
|
|
end)
|
|
test("species seeds retain parent identity and reject an incompatible destination soil",function()
|
|
diverse(); season="Autumn"; baseline("birch_mature",2000)
|
|
metadata(p):set_string("succ_serial","9"); advance(100)
|
|
assert(hearth.seasons.kind(name(pos(-1,1,0)))=="birch_seedling")
|
|
reset(); diverse(); put(pos(0,0,0),"podzol"); put(pos(-1,0,0),"peat")
|
|
baseline("pine_mature",2000); metadata(p):set_string("succ_serial","9"); advance(100)
|
|
assert(name(pos(-1,1,0))~="hearth:pine_seedling") -- litter may share the attempt
|
|
end)
|
|
test("pine is evergreen while species deciduous canopies lose winter shade",function()
|
|
diverse(); put(p,"pine_mature",true); put(pos(1,2,0),"pine_leaves")
|
|
local summer=sim.habitat(p); season="Winter"
|
|
local winter=sim.habitat(p); assert(winter.canopy==summer.canopy)
|
|
put(p,"oak_mature",true); put(pos(1,2,0),"oak_leaves_winter")
|
|
assert(math.abs(sim.habitat(p).canopy-0.1725)<0.0001)
|
|
end)
|
|
test("species growth checks the full five-by-five-by-six target volume",function()
|
|
for _,obstacle in ipairs({"foreign","placed","claimed","unloaded","external"}) do
|
|
reset(); diverse(); baseline("birch_young",86300)
|
|
local q=pos(2,6,0)
|
|
if obstacle=="foreign" then put(q,"wall") end
|
|
if obstacle=="placed" then metadata(q):set_string("player_placed","1") end
|
|
if obstacle=="claimed" then claims[key(q)]=true end
|
|
if obstacle=="unloaded" then missing[key(q)]=true end
|
|
if obstacle=="external" then protected[key(q)]=true end
|
|
advance(100); assert(name(p)=="hearth:birch_young",obstacle)
|
|
end
|
|
end)
|
|
test("forest, acidic, alpine and saturated soils recruit live understory without hours of waiting",function()
|
|
local cases={{"podzol",15,30,"heather"},{"alluvium",15,90,"sedge"},
|
|
{"peat",15,90,"sedge"},{"forest_floor",8,70,"moss"},{"alpine_turf",15,30,"heather"},
|
|
{"granite",15,10,"lichen"}}
|
|
for _,c in ipairs(cases) do
|
|
reset(); diverse(); put(pos(0,0,0),c[1]); light,humidity=c[2],c[3]
|
|
baseline(); advance(100); advance(100)
|
|
assert(name(p)=="hearth:"..c[4],c[1].." got "..name(p))
|
|
assert(name(pos(0,0,0))=="hearth:"..c[1],"never rewrite supporting terrain")
|
|
end
|
|
end)
|
|
test("wet understory recruits willow and alder instead of silently turning into oak",function()
|
|
for _,c in ipairs({{"alluvium","willow"},{"peat","alder"},{"podzol","pine"}}) do
|
|
reset(); diverse(); put(pos(0,0,0),c[1]); humidity=c[1]=="podzol" and 30 or 90
|
|
baseline(c[1]=="podzol" and "heather" or "sedge",7100); advance(100)
|
|
assert(name(p)=="hearth:"..c[2].."_seedling",name(p))
|
|
end
|
|
end)
|
|
test("scheduler adopts worldgen herbs but never player blocks or placed species trees",function()
|
|
diverse(); put(p,"clover"); baseline(); advance(100)
|
|
assert(metadata(p):get_string("succ_natural")=="1" and field(p,"succ_age")==100)
|
|
for _,k in ipairs({"birch_log","birch_leaves","birch_mature"}) do
|
|
reset(); diverse(); put(p,k)
|
|
if k=="birch_mature" then metadata(p):set_string("player_placed","1") end
|
|
baseline(); advance(100)
|
|
assert(metadata(p):get_string("succ_natural")=="" and field(p,"succ_age")==0,k)
|
|
end
|
|
end)
|
|
test("clover fixes only two finite nitrogen units per support across restart and recolonization",function()
|
|
diverse(); baseline("clover",900)
|
|
for _=1,5 do advance(100) end
|
|
local support=pos(0,0,0)
|
|
assert(field(support,"succ_nutrients")==2 and field(support,"succ_fixed_nitrogen")==2)
|
|
metadata(support):set_string("succ_nutrients","0")
|
|
load(); sim.step(1); advance(100)
|
|
assert(field(support,"succ_nutrients")==0)
|
|
metadata(p):set_string("succ_natural",""); put(p,"clover"); advance(100)
|
|
assert(field(support,"succ_nutrients")==0 and field(support,"succ_fixed_nitrogen")==2)
|
|
end)
|
|
test("natural clover and sedge feed wildlife but dormant variants, flowers and player plants do not",function()
|
|
diverse(); put(p,"clover",true); assert(sim.forage(p))
|
|
put(pos(1,0,0),"alluvium"); put(pos(1),"sedge",true); assert(sim.forage(p))
|
|
season="Winter"; put(p,"clover_winter",true); put(pos(1),"sedge_winter",true)
|
|
assert(not sim.forage(p))
|
|
season="Summer"; put(p,"bluebell",true); put(pos(1),"clover")
|
|
metadata(pos(1)):set_string("succ_natural",""); assert(not sim.forage(p))
|
|
end)
|
|
test("habitat exposes bounded nutrients and pollination without changing metadata",function()
|
|
diverse(); put(p,"clover",true); put(pos(1),"wildflowers",true)
|
|
metadata(pos(0,0,0)):set_string("succ_nutrients","999999")
|
|
local h=sim.habitat(p)
|
|
assert(h.nutrients==8 and h.pollination==2 and h.edible==1 and h.ground=="grass")
|
|
assert(field(pos(0,0,0),"succ_nutrients")==999999 and writes==0)
|
|
end)
|
|
test("species foliage respects the same total eight-column budget and ninety-six appearance swaps",function()
|
|
diverse(); season="Autumn"; real_positions=true
|
|
api.get_node_or_nil=function(q) reads=reads+1; return world[key(q)] or {name="hearth:alder_leaves"} end
|
|
assert(sim.step(999999)==8 and writes==96)
|
|
end)
|
|
test("species seeds respect source, destination and support authority without empty-name claim access",function()
|
|
for _,mode in ipairs({"source","destination","support","external","marker","ambiguous"}) do
|
|
reset(); diverse(); baseline("birch_mature",2000)
|
|
hearth.claim_allowed=function() error("forbidden empty-name player access") end
|
|
local seed=pos(-1,1,0)
|
|
if mode=="source" then claims[key(p)]=true end
|
|
if mode=="destination" then claims[key(seed)]=true end
|
|
if mode=="support" then claims[key(pos(-1,0,0))]=true end
|
|
if mode=="external" then protected[key(seed)]=true end
|
|
if mode=="marker" then metadata(seed):set_string("third_party:placed","1") end
|
|
if mode=="ambiguous" then hearth.claim_owner_at=function(q) return nil,key(q)==key(seed) end end
|
|
metadata(p):set_string("succ_serial","9"); advance(100)
|
|
assert(name(seed)~="hearth:birch_seedling",mode)
|
|
end
|
|
end)
|
|
test("calendar age continues through winter while visible stage growth waits for spring",function()
|
|
diverse(); baseline("oak_seedling",21500)
|
|
aged(5*YEAR)
|
|
season,temperature="Winter",-5; advance(100)
|
|
assert(name(p)=="hearth:oak_seedling_winter" and field(p,"succ_age")==21500)
|
|
load(); advance(100); assert(field(p,"succ_age")==21500)
|
|
lbm.action(p); season,temperature="Spring",15; advance(100)
|
|
assert(name(p)=="hearth:oak_seedling_spring" and field(p,"succ_age")==21500)
|
|
advance(100)
|
|
assert(name(p)=="hearth:oak_young_spring" and metadata(p):get_string("succ_species")=="oak")
|
|
end)
|
|
|
|
local function catalogue()
|
|
hearth.diversity=dofile(target:gsub("[^/\\]+$","diversity.lua"))(hearth,api)
|
|
hearth.botany=dofile(target:gsub("[^/\\]+$","botany.lua"))(hearth,api)
|
|
hearth.species_effects=dofile(target:gsub("[^/\\]+$","species_effects.lua"))(hearth,api)
|
|
hearth.biomes=dofile(target:gsub("[^/\\]+$","biomes.lua"))(hearth,api,function() end,{})
|
|
-- Exercise one plot per frame; the other seven still consume the global
|
|
-- budget but have no valid player position. Separate tests cover fairness.
|
|
api.get_connected_players=function()
|
|
return {{get_pos=function()
|
|
local index=probes%625; local visit=probes%8==0; probes=probes+1
|
|
if not visit then return nil end
|
|
return pos(p.x+12-index%25,p.y,p.z+12-math.floor(index/25))
|
|
end}}
|
|
end
|
|
load()
|
|
end
|
|
local function vector(r)
|
|
local n=r.niche
|
|
local soil=next(n.substrates)
|
|
if n.aquatic=="surface" or n.aquatic=="submerged" then soil="alluvium" end
|
|
return {loaded=true,ground=soil,substrate=r.id=="hornwort" and "water" or soil,
|
|
light=(n.light[1]+n.light[2])/2,moisture=(n.moisture[1]+n.moisture[2])/2,
|
|
ph=(n.ph[1]+n.ph[2])/2,water=n.water[1],aquatic=n.aquatic,freshwater=true,
|
|
nutrients=2,host=n.hosts and next(n.hosts) or nil,canopy=0,grass=0,flowers=0,nursery=0,root_fungi={}}
|
|
end
|
|
local function support(q,h)
|
|
local b=pos(q.x,q.y-1,q.z)
|
|
if h.water>0 then
|
|
for i=1,h.water do put(pos(q.x,q.y-i,q.z),"water_source") end
|
|
put(pos(q.x,q.y-h.water-1,q.z),h.ground)
|
|
elseif h.substrate=="bark" then
|
|
put(b,(h.host or "oak").."_young",true)
|
|
elseif h.substrate=="deadwood" or h.substrate=="litter" then
|
|
put(b,h.substrate,true); metadata(b):set_string("succ_species",h.host or "oak")
|
|
else put(b,h.ground) end
|
|
end
|
|
local function supplied_habitat(h)
|
|
-- Controlled niche vectors isolate the integration handoff from the
|
|
-- independently tested real bounded habitat sampler. Selection is REAL.
|
|
sim.habitat=function(q)
|
|
local out={}; for k,v in pairs(h) do out[k]=v end
|
|
if q.y~=p.y then out.loaded=false end
|
|
return out
|
|
end
|
|
end
|
|
test("ecology100 recruits every exact candidate with calendar anchors and no automatic death",function()
|
|
catalogue()
|
|
local records=hearth.botany.list()
|
|
assert(#records==100)
|
|
for index,r in ipairs(records) do
|
|
reset(); catalogue()
|
|
local h=vector(r)
|
|
season=next(r.seasons)
|
|
-- Exact position-hash start at this record, also meeting the old
|
|
-- woody-gap gate. No fake choose(), random rerolls or ID aliases.
|
|
local x=0
|
|
while (x*17+7)%100~=index-1 or (x*7)%23~=0 do x=x+1; assert(x<2300) end
|
|
p=pos(x,1,0); support(p,h); supplied_habitat(h)
|
|
baseline(nil,7100); advance(100)
|
|
local candidate=r.group=="trees" and r.id.."_seedling" or r.id
|
|
assert(hearth.seasons.kind(name(p))==candidate,r.id.." recruit got "..name(p))
|
|
assert(metadata(p):get_string("succ_natural")=="1",r.id)
|
|
assert(metadata(p):get_string("succ_lifecycle_version")=="1" and field(p,"succ_birth_at")==now,r.id)
|
|
local profile=hearth.lifecycle.plant(r.id)
|
|
local q=pos(p.x,p.y,p.z-1); support(q,h)
|
|
metadata(p):set_string("succ_age","1000"); metadata(p):set_string("succ_serial","2")
|
|
if r.group=="trees" then
|
|
-- A mature parent disperses the same species seed, not a
|
|
-- generic oak. The subsequent expansion uses real block plans.
|
|
put(p,r.id.."_mature",true); aged(profile.maturity_days.max*DAY); metadata(p):set_string("succ_serial","9")
|
|
season="Autumn"
|
|
q=pos(p.x-1,p.y,p.z); support(q,h); advance(100)
|
|
assert(hearth.seasons.kind(name(q))==candidate,r.id.." tree spread")
|
|
put(q,"air"); metadata(q):set_string("succ_natural","")
|
|
put(p,candidate,true); aged(profile.juvenile_days.max*DAY); advance(100)
|
|
assert(metadata(p):get_string("succ_tree_stage")=="young",r.id.." young got "..name(p).." age "..field(p,"succ_age").." stress "..field(p,"succ_stress"))
|
|
aged(profile.maturity_days.max*DAY); advance(100)
|
|
assert(metadata(p):get_string("succ_tree_stage")=="mature",r.id.." mature")
|
|
aged(10000*YEAR); advance(100)
|
|
assert(hearth.seasons.kind(name(p))==r.id.."_mature",r.id.." must not die")
|
|
else
|
|
season=r.seeding.Autumn and "Autumn" or "Summer"
|
|
if not r.seasons[season] then season=next(r.seasons) end
|
|
aged(profile.maturity_days and profile.maturity_days.max*DAY or 10000*YEAR)
|
|
advance(100)
|
|
if profile.maturity_days and r.seeding[season] then
|
|
assert(hearth.seasons.kind(name(q))==candidate,r.id.." spread got "..name(q))
|
|
else assert(name(q)=="air",r.id.." unknown maturity must not spawn") end
|
|
aged(10000*YEAR); advance(100)
|
|
assert(hearth.seasons.kind(name(p))==candidate,r.id.." no guessed senescence")
|
|
end
|
|
end
|
|
end)
|
|
test("real habitat distinguishes bank depth, source columns, pH, host and bark without writes",function()
|
|
catalogue()
|
|
put(pos(1,0,0),"water_source"); put(pos(0,0,0),"alluvium"); humidity=90
|
|
local h=sim.habitat(p)
|
|
assert(h.water==0 and h.near_water==1 and h.aquatic=="bank" and h.ph==6.5)
|
|
put(pos(0,0,0),"water_source"); put(pos(0,-1,0),"water_source"); put(pos(0,-2,0),"alluvium")
|
|
h=sim.habitat(p)
|
|
assert(h.water==2 and h.ground=="alluvium" and h.substrate=="water" and h.freshwater and h.moisture==1)
|
|
put(p,"hornwort",true); assert(sim.habitat(p).aquatic=="submerged")
|
|
put(pos(0,0,0),"deadwood",true); metadata(pos(0,0,0)):set_string("succ_species","birch")
|
|
h=sim.habitat(p); assert(h.substrate=="deadwood" and h.host=="birch")
|
|
put(pos(0,0,0),"oak_young",true)
|
|
h=sim.habitat(p); assert(h.substrate=="bark" and h.host=="oak" and h.water==0)
|
|
metadata(pos(0,0,0)):set_string("player_placed","1")
|
|
assert(not sim.habitat(p).loaded and writes==0)
|
|
end)
|
|
test("real source-water recruitment and very old submerged stands never clear or refill water",function()
|
|
for _,mode in ipairs({"surface","submerged"}) do
|
|
reset(); catalogue(); p=pos(mode=="surface" and 0 or 1,1,0)
|
|
put(pos(p.x,0,0),"water_source"); put(pos(p.x,-1,0),"alluvium")
|
|
baseline(); advance(100); advance(100)
|
|
local r=hearth.botany.get(name(p)); assert(r and r.group=="aquatic" and r.niche.aquatic==mode)
|
|
assert(name(pos(p.x,0,0))=="hearth:water_source")
|
|
aged(10000*YEAR); advance(100)
|
|
assert(hearth.botany.kind(name(p))==r.id and name(pos(p.x,0,0))=="hearth:water_source")
|
|
assert(field(pos(p.x,-1,0),"succ_nutrients")==0)
|
|
end
|
|
end)
|
|
test("water placement denies flowing, too deep, unloaded, marked, claimed and protected columns",function()
|
|
for _,mode in ipairs({"flowing","deep","unloaded","marked","claimed","external","bottom"}) do
|
|
reset(); catalogue()
|
|
put(pos(0,0,0),"water_source"); put(pos(0,-1,0),"alluvium")
|
|
if mode=="flowing" then put(pos(0,0,0),"water_flowing") end
|
|
if mode=="deep" then for y=0,-6,-1 do put(pos(0,y,0),"water_source") end end
|
|
if mode=="unloaded" then missing[key(pos(0,-1,0))]=true end
|
|
if mode=="marked" then metadata(pos(0,0,0)):set_string("placed_by","builder") end
|
|
if mode=="claimed" then claims[key(pos(0,0,0))]=true end
|
|
if mode=="external" then protected[key(pos(0,0,0))]=true end
|
|
if mode=="bottom" then claims[key(pos(0,-1,0))]=true end
|
|
baseline(); advance(100); advance(100)
|
|
assert(name(p)=="air" and writes==0,mode)
|
|
end
|
|
end)
|
|
test("real block plans expand atomically, survive restart and never collapse from old age",function()
|
|
catalogue(); humidity=45
|
|
baseline("birch_seedling",21600); aged(5*YEAR); advance(100)
|
|
assert(metadata(p):get_string("succ_tree_stage")=="young")
|
|
local count=0
|
|
for _,c in ipairs(hearth.diversity.tree_plan("birch","young")) do
|
|
local q=pos(p.x+c.x,p.y+c.y,p.z+c.z)
|
|
assert(metadata(q):get_string("succ_component")==key(p)); count=count+1
|
|
end
|
|
assert(count>10)
|
|
load(); sim.step(1); aged(20*YEAR); advance(100)
|
|
assert(metadata(p):get_string("succ_tree_stage")=="mature")
|
|
local before=writes
|
|
local q=pos(0,4,0); metadata(q):set_string("owner","builder")
|
|
metadata(p):set_string("succ_seed_epoch",tostring(now+YEAR))
|
|
metadata(p):set_string("succ_litter_epoch",tostring(now+YEAR))
|
|
aged(1000*YEAR); advance(100)
|
|
assert(name(p)=="hearth:birch_mature" and writes==before)
|
|
metadata(q):set_string("owner",""); advance(100)
|
|
assert(name(p)=="hearth:birch_mature" and name(pos(0,7,0))~="air")
|
|
end)
|
|
test("every tree destination and its support fail closed before any plan write",function()
|
|
for _,mode in ipairs({"foreign","natural_plant","marker","claim","external","unloaded","support"}) do
|
|
reset(); catalogue(); humidity=45; baseline("oak_seedling",21600); aged(5*YEAR)
|
|
local q=pos(0,4,0)
|
|
if mode=="foreign" then put(q,"wall") end
|
|
if mode=="natural_plant" then put(q,"yarrow",true) end
|
|
if mode=="marker" then metadata(q):set_string("player_placed","1") end
|
|
if mode=="claim" then claims[key(q)]=true end
|
|
if mode=="external" then protected[key(q)]=true end
|
|
if mode=="unloaded" then missing[key(q)]=true end
|
|
if mode=="support" then claims[key(pos(0,3,0))]=true end
|
|
advance(100)
|
|
assert(name(p)=="hearth:oak_seedling" and writes==0,mode)
|
|
end
|
|
end)
|
|
test("chopped ecology root cleans matching components but never mapgen or foreign timber",function()
|
|
catalogue(); humidity=45; baseline("oak_seedling",21600); aged(5*YEAR); advance(100)
|
|
local old={name=name(p)}; put(p,"air"); now=now+1
|
|
assert(sim.tree_removed(p,old)); assert(name(pos(0,3,0))=="air")
|
|
put(pos(0,3,0),"oak_log"); now=now+1
|
|
assert(not sim.tree_removed(p,{name="hearth:oak_young"}))
|
|
assert(name(pos(0,3,0))=="hearth:oak_log")
|
|
end)
|
|
test("catalogue nitrogen caps survive restart and root fungi never accelerate calendar growth",function()
|
|
catalogue(); humidity=45; baseline("clover",1000)
|
|
for _=1,5 do advance(100) end
|
|
assert(field(pos(0,0,0),"succ_fixed_nitrogen")==2 and field(pos(0,0,0),"succ_nutrients")==2)
|
|
load(); sim.step(1); advance(100); assert(field(pos(0,0,0),"succ_nutrients")==2)
|
|
reset(); catalogue(); humidity=45; put(pos(0,0,0),"forest_floor")
|
|
put(pos(1),"birch_bolete",true); metadata(pos(0,0,0)):set_string("succ_nutrients","1")
|
|
baseline("birch_seedling",16100); advance(100)
|
|
assert(name(p)=="hearth:birch_seedling" and field(pos(0,0,0),"succ_nutrients")==1)
|
|
aged(5*YEAR); advance(100)
|
|
assert(name(p)=="hearth:birch_young" and field(pos(0,0,0),"succ_nutrients")==0)
|
|
reset(); catalogue(); humidity=45; put(pos(1),"birch_bolete",true)
|
|
metadata(pos(0,0,0)):set_string("succ_nutrients","1")
|
|
baseline("oak_seedling",16100); advance(100)
|
|
assert(name(p)=="hearth:oak_seedling" and field(pos(0,0,0),"succ_nutrients")==1)
|
|
end)
|
|
test("saprotrophs age existing deadwood without minting nutrients or attacking natural or player trees",function()
|
|
catalogue(); humidity=70; light=10
|
|
put(pos(0,0,0),"deadwood",true); metadata(pos(0,0,0)):set_string("succ_species","birch")
|
|
baseline("birch_polypore",1000); advance(100)
|
|
assert(field(pos(0,0,0),"succ_decay_bonus")>0 and field(pos(0,0,0),"succ_nutrients")==0)
|
|
assert(field(p,"succ_stress")==0)
|
|
metadata(pos(0,0,0)):set_string("owner","builder"); local age=field(pos(0,0,0),"succ_age")
|
|
advance(100); assert(field(pos(0,0,0),"succ_age")==age)
|
|
reset(); catalogue(); humidity=70; light=10
|
|
put(pos(0,0,0),"deadwood",true); metadata(pos(0,0,0)):set_string("succ_species","oak")
|
|
baseline("birch_polypore",1000); advance(100)
|
|
assert(field(pos(0,0,0),"succ_age")==0,"birch-only fungus must not accelerate oak decay")
|
|
end)
|
|
test("flowering, browse and nursery use botanical tags while harvested bushes retain cooldowns",function()
|
|
catalogue(); humidity=45; put(p,"blackberry",true); put(pos(1),"yarrow",true)
|
|
local h=sim.habitat(p); assert(h.pollination==2 and h.edible==1 and h.nursery==1)
|
|
metadata(p):set_string("botany_ready_at",tostring(now+86400))
|
|
baseline(nil,1000); metadata(p):set_string("succ_seed_epoch",tostring(now+YEAR))
|
|
advance(100); assert(field(p,"succ_age")==1000 and field(p,"botany_ready_at")>now)
|
|
assert(sim.forage(p)); assert(name(p)=="hearth:blackberry" and field(p,"botany_ready_at")>now)
|
|
put(p,"foxglove",true); assert(not sim.forage(p))
|
|
season="Spring"; put(p,"rowan_mature",true)
|
|
metadata(p):set_string("succ_component",key(p)); metadata(p):set_string("succ_tree_stage","mature")
|
|
put(pos(1),"rowan_leaves",true); metadata(pos(1)):set_string("succ_component",key(p))
|
|
h=sim.habitat(p)
|
|
assert(h.pollination==1 and h.nursery==1 and h.edible==1,"one managed root, not a benefit per leaf")
|
|
end)
|
|
test("autumn and winter browse debit a shared partial ration without human fruiting",function()
|
|
catalogue(); season="Winter"; put(p,"blackberry",true)
|
|
assert(not hearth.botany.edible("blackberry","Winter"))
|
|
local ready=now+7*86400
|
|
assert(sim.forage(p,"winter_browse"))
|
|
assert(name(p)=="hearth:blackberry" and field(p,"succ_browse_ready_at")==ready)
|
|
assert(not sim.forage(p,"winter_browse"),"one resident cannot eat the same plant twice during its rest")
|
|
season="Summer"; now=ready
|
|
assert(sim.forage(p,"herbivore"),"the same plant becomes browseable after its shared rest window")
|
|
assert(field(p,"succ_browse_ready_at")==now+7*86400)
|
|
end)
|
|
test("animal seasonal browse can never fell a mature tree or feed from toxic plants",function()
|
|
catalogue(); season="Winter"; put(p,"oak_mature",true)
|
|
local before=name(p); local age=field(p,"succ_age"); local cooldown=field(p,"succ_browse_ready_at")
|
|
assert(not sim.forage(p,"winter_browse"))
|
|
assert(name(p)==before and field(p,"succ_age")==age and field(p,"succ_browse_ready_at")==cooldown)
|
|
put(p,"foxglove",true); before=name(p)
|
|
assert(not sim.forage(p,"winter_browse") and name(p)==before)
|
|
end)
|
|
|
|
test("one global tree-plan reservation bounds eight concurrent roots and malformed geometry writes nothing",function()
|
|
catalogue(); humidity=45
|
|
local positions={pos(0),pos(20),pos(40),pos(60),pos(80),pos(100),pos(120),pos(140)}
|
|
local i,visits=0,0
|
|
api.get_connected_players=function()
|
|
return {{get_pos=function()
|
|
i=i%8+1
|
|
local index=visits%625; visits=visits+1
|
|
local root=positions[i]
|
|
return pos(root.x+12-index%25,1,root.z+12-math.floor(index/25))
|
|
end}}
|
|
end
|
|
for _,root in ipairs(positions) do put(root,"oak_seedling",true) end
|
|
sim.step(1)
|
|
for _,root in ipairs(positions) do aged(5*YEAR,root) end
|
|
local calls=0; local original=hearth.diversity.tree_plan
|
|
hearth.diversity.tree_plan=function(...) calls=calls+1; return original(...) end
|
|
advance(100); assert(calls==1)
|
|
local grown=0
|
|
for _,root in ipairs(positions) do if metadata(root):get_string("succ_tree_stage")=="young" then grown=grown+1 end end
|
|
assert(grown==1 and writes<=sim.MAX_TREE_CELLS)
|
|
for _,mode in ipairs({"missing","oversize","foreign","duplicate","offset"}) do
|
|
reset(); catalogue(); humidity=45; baseline("oak_seedling",21600); aged(5*YEAR)
|
|
hearth.diversity.tree_plan=function()
|
|
if mode=="missing" then return nil end
|
|
local plan={{x=0,y=0,z=0,name="hearth:oak_young"}}
|
|
if mode=="oversize" then for _=1,sim.MAX_TREE_CELLS do plan[#plan+1]=plan[1] end end
|
|
if mode=="foreign" then plan[1].name="foreign:log" end
|
|
if mode=="duplicate" then plan[2]=plan[1] end
|
|
if mode=="offset" then plan[1].x=100 end
|
|
return plan
|
|
end
|
|
advance(100); assert(name(p)=="hearth:oak_seedling" and writes==0,mode)
|
|
end
|
|
end)
|
|
test("water stands and botanical understory retain calendar anchors across restart and reload",function()
|
|
for _,plant in ipairs({"duckweed","yarrow"}) do
|
|
reset(); catalogue(); humidity=45
|
|
if plant=="duckweed" then put(pos(0,0,0),"water_source"); put(pos(0,-1,0),"alluvium") end
|
|
baseline(plant,1000); advance(864000); assert(field(p,"succ_age")==1000)
|
|
load(); advance(100); assert(field(p,"succ_age")==1000)
|
|
lbm.action(p); advance(100); assert(field(p,"succ_age")==1000)
|
|
advance(100); assert(field(p,"succ_age")==1000 and field(p,"succ_last_observed_at")==now)
|
|
end
|
|
end)
|
|
test("real moss mat sampler changes local moisture and pH once without map or metadata writes",function()
|
|
catalogue(); humidity=80; light=10
|
|
assert(hearth.botany.suitable("bracken",sim.habitat(p),"Summer"))
|
|
put(pos(1,0,0),"alluvium"); put(pos(1),"sphagnum",true)
|
|
local before=sim.habitat(p)
|
|
assert(before.species_counts.sphagnum==1)
|
|
assert(not hearth.botany.suitable("bracken",before,"Summer"),"retention changes a real neighbour niche")
|
|
assert(math.abs(before.moisture-before.source_moisture-0.1)<0.000001)
|
|
assert(math.abs(before.ph-6.3)<0.000001 and before.source_ph==6.5)
|
|
put(pos(-1,0,0),"peat"); put(pos(-1),"sphagnum",true)
|
|
put(pos(0,0,1),"peat"); put(pos(0,1,1),"sphagnum",true)
|
|
local function snapshot()
|
|
local out={}; for id,f in pairs(fields) do for k,v in pairs(f) do out[#out+1]=id..k..v end end
|
|
table.sort(out); return table.concat(out,"|")
|
|
end
|
|
local h=sim.habitat(p); local saved=snapshot()
|
|
for _=1,5 do
|
|
h=sim.habitat(p)
|
|
assert(h.species_counts.sphagnum==3 and math.abs(h.ph-6.1)<0.000001)
|
|
assert(math.abs(h.moisture-h.source_moisture-0.2)<0.000001)
|
|
end
|
|
assert(snapshot()==saved and writes==0 and name(pos(1,0,0))=="hearth:alluvium")
|
|
end)
|
|
test("moss effects exclude planted protected claimed unsupported and unloaded sources",function()
|
|
for _,mode in ipairs({"planted","protected","claimed","support","unsupported","unloaded"}) do
|
|
reset(); catalogue(); light=10
|
|
local q=pos(1); put(pos(1,0,0),"peat"); put(q,"sphagnum",true)
|
|
if mode=="planted" then metadata(q):set_string("player_placed","1") end
|
|
if mode=="protected" then protected[key(q)]=true end
|
|
if mode=="claimed" then claims[key(q)]=true end
|
|
if mode=="support" then claims[key(pos(1,0,0))]=true end
|
|
if mode=="unsupported" then put(pos(1,0,0),"air") end
|
|
if mode=="unloaded" then missing[key(q)]=true end
|
|
local h=sim.habitat(p)
|
|
assert(not h.species_counts.sphagnum and h.ph==6.5 and writes==0,mode)
|
|
end
|
|
end)
|
|
test("real floating duckweed shades submerged neighbours but not itself or land and preserves water",function()
|
|
catalogue(); light=12
|
|
put(pos(0,0,0),"water_source"); put(pos(0,-1,0),"alluvium"); put(p,"hornwort",true)
|
|
for _,x in ipairs({-1,1,2}) do
|
|
put(pos(x,0,0),"water_source"); put(pos(x,-1,0),"alluvium"); put(pos(x),"duckweed",true)
|
|
end
|
|
local h=sim.habitat(p)
|
|
assert(h.aquatic=="submerged" and h.species_counts.duckweed==3 and h.light==7.5 and h.source_light==12)
|
|
assert(sim.habitat(pos(1)).light==12)
|
|
assert(sim.habitat(pos(0,1,1)).light==12)
|
|
season="Winter"; assert(sim.habitat(p).light==12)
|
|
season="Summer"; protected[key(pos(1))]=true
|
|
assert(sim.habitat(p).light==9)
|
|
for _,x in ipairs({-1,0,1,2}) do assert(name(pos(x,0,0))=="hearth:water_source") end
|
|
assert(writes==0)
|
|
end)
|
|
test("alder all living stages and dog lichen share the durable finite nitrogen budget",function()
|
|
for _,id in ipairs({"alder_seedling","alder_young","alder_mature","dog_lichen"}) do
|
|
reset(); catalogue()
|
|
local r=hearth.botany.get(id); local h=vector(r)
|
|
support(p,h); supplied_habitat(h); baseline(id,1000); aged(90*DAY)
|
|
for _=1,5 do advance(100) end
|
|
local below=pos(0,0,0)
|
|
assert(field(below,"succ_fixed_nitrogen")==2 and field(below,"succ_nutrients")==2,id)
|
|
metadata(below):set_string("succ_nutrients","0")
|
|
load(); supplied_habitat(h); sim.step(1); advance(100)
|
|
assert(field(below,"succ_nutrients")==0,id.." reload")
|
|
put(p,"clover",true); supplied_habitat(vector(hearth.botany.get("clover")))
|
|
advance(100); assert(field(below,"succ_nutrients")==0,id.." shared cap")
|
|
end
|
|
end)
|
|
test("nitrogen never exceeds local four ceiling or writes through source or support protection",function()
|
|
for _,mode in ipairs({"fraction","full","source","support"}) do
|
|
reset(); catalogue(); humidity=45
|
|
local below=pos(0,0,0)
|
|
metadata(below):set_string("succ_nutrients",mode=="fraction" and "3.75" or (mode=="full" and "8" or "0"))
|
|
baseline("clover",1000)
|
|
if mode=="source" then protected[key(p)]=true end
|
|
if mode=="support" then claims[key(below)]=true end
|
|
advance(100)
|
|
local expected=mode=="fraction" and 4 or (mode=="full" and 8 or 0)
|
|
assert(field(below,"succ_nutrients")==expected,mode)
|
|
assert(field(below,"succ_fixed_nitrogen")== (mode=="fraction" and 0.25 or 0),mode)
|
|
end
|
|
end)
|
|
test("multiple host fungi cannot copy nutrients or accelerate growth even with a resource budget",function()
|
|
for _,budget in ipairs({0,1}) do
|
|
reset(); catalogue(); humidity=45
|
|
put(pos(0,0,0),"forest_floor"); metadata(pos(0,0,0)):set_string("succ_nutrients",budget)
|
|
put(pos(1),"fly_agaric",true); put(pos(-1),"birch_bolete",true)
|
|
baseline("birch_seedling",16100); advance(100)
|
|
assert(field(pos(0,0,0),"succ_nutrients")==budget)
|
|
assert(name(p)=="hearth:birch_seedling")
|
|
aged(5*YEAR); advance(100)
|
|
assert(field(pos(0,0,0),"succ_nutrients")==0 and name(p)=="hearth:birch_young")
|
|
end
|
|
end)
|
|
test("birch wood fungus exhausts supporting deadwood then litter with exactly one conserved payout each",function()
|
|
catalogue(); humidity=70; light=10
|
|
put(p,"deadwood",true); metadata(p):set_string("succ_species","birch")
|
|
put(pos(0,2,0),"birch_polypore",true)
|
|
baseline(nil,7100); now=now+YEAR; advance(100); advance(100)
|
|
assert(name(p)=="hearth:litter" and name(pos(0,2,0))=="air")
|
|
assert(field(pos(0,0,0),"succ_nutrients")==3)
|
|
now=now+90*DAY; advance(100); advance(100)
|
|
assert(name(p)=="air" and field(pos(0,0,0),"succ_nutrients")==4)
|
|
advance(60); assert(field(pos(0,0,0),"succ_nutrients")==4)
|
|
end)
|
|
test("new seedlings never reinterpret legacy seconds and stage swaps retain one calendar birthday",function()
|
|
catalogue(); humidity=45
|
|
baseline("crab_apple_seedling",999999)
|
|
local born=field(p,"succ_birth_at")
|
|
assert(metadata(p):get_string("succ_lifecycle_origin")=="legacy_established")
|
|
assert(field(p,"succ_age")==999999 and born==now)
|
|
aged(5*YEAR); advance(100)
|
|
local birthday=field(p,"succ_birth_at")
|
|
assert(hearth.seasons.kind(name(p))=="crab_apple_young")
|
|
advance(100); assert(field(p,"succ_birth_at")==birthday and field(p,"succ_age")==999999)
|
|
end)
|
|
test("seed-age biennials cannot supply flowers before calendar maturity",function()
|
|
catalogue(); season="Spring"; humidity=60; light=8
|
|
baseline("garlic_mustard",1000)
|
|
aged(100*DAY)
|
|
assert(sim.habitat(p).pollination==0)
|
|
aged(730*DAY)
|
|
assert(sim.habitat(p).pollination==1)
|
|
season="Winter"; assert(sim.habitat(p).pollination==0)
|
|
end)
|
|
test("calendar metadata corruption versions and rollback fail closed without losing the high water",function()
|
|
for _,fieldname in ipairs({"succ_birth_at","succ_initial_age_seconds","succ_last_observed_at","succ_lifecycle_seed","succ_seed_epoch"}) do
|
|
reset(); catalogue(); baseline("oak_seedling",30000)
|
|
metadata(p):set_string(fieldname,"broken")
|
|
local before=writes; advance(100)
|
|
assert(writes==before and field(p,"succ_age")==30000,fieldname)
|
|
end
|
|
reset(); catalogue(); baseline("oak_seedling",30000)
|
|
metadata(p):set_string("succ_lifecycle_version","2"); advance(100)
|
|
assert(metadata(p):get_string("succ_lifecycle_version")=="2" and field(p,"succ_age")==30000)
|
|
reset(); catalogue(); baseline("oak_seedling",30000); advance(100)
|
|
local high=field(p,"succ_last_observed_at"); advance(-50)
|
|
assert(field(p,"succ_last_observed_at")==high)
|
|
advance(100); assert(field(p,"succ_last_observed_at")==now)
|
|
end)
|
|
test("worldgen botanical flora and full block roots adopt established provenance without replacing canopies",function()
|
|
for _,id in ipairs({"dandelion","garlic_mustard","holly_fern","crab_apple_log","wild_cherry_log","field_maple_log","log"}) do
|
|
reset(); catalogue(); humidity=45; put(p,id)
|
|
baseline(); advance(100)
|
|
assert(name(p)=="hearth:"..id,id)
|
|
assert(metadata(p):get_string("succ_lifecycle_origin")=="worldgen_established",id)
|
|
assert(field(p,"succ_birth_at")==now-100 and metadata(p):get_string("succ_lifecycle_version")=="1",id)
|
|
assert(field(p,"succ_initial_age_seconds")>0,id)
|
|
end
|
|
end)
|
|
local function snapshot()
|
|
local out={}
|
|
for id,f in pairs(fields) do for k,v in pairs(f) do out[#out+1]=id..":"..k.."="..v end end
|
|
table.sort(out); return table.concat(out,"|").."/writes="..writes
|
|
end
|
|
test("nectar is a shared hourly debit and pollen is one expiring boolean, never a seed or fruit grant",function()
|
|
catalogue(); put(p,"blackberry",true)
|
|
assert(sim.pollinate(p))
|
|
assert(field(p,"succ_nectar_ready_at")==now+3600 and field(p,"succ_pollinated_until")==now+DAY)
|
|
local saved=snapshot()
|
|
for _=1,20 do assert(not sim.pollinate(p)) end
|
|
assert(snapshot()==saved and field(p,"botany_ready_at")==0 and writes==0)
|
|
load(); assert(not sim.pollinate(p))
|
|
now=now+3600; assert(sim.pollinate(p))
|
|
assert(field(p,"succ_pollinated_until")==now-3600+DAY,"no additive/extended pollen credit")
|
|
now=now+DAY; assert(sim.pollinate(p) and field(p,"succ_pollinated_until")==now+DAY)
|
|
assert(not sim.disperse_seed(pos(1),"oak"))
|
|
end)
|
|
test("nectar and fruit deny immature plants, out-of-season food, and malformed shared timestamps",function()
|
|
catalogue(); baseline("blackberry"); aged(DAY)
|
|
local saved=snapshot()
|
|
assert(not sim.pollinate(p) and not sim.frugivore(p) and snapshot()==saved)
|
|
aged(10*YEAR); season="Winter"; saved=snapshot()
|
|
assert(not sim.pollinate(p) and not sim.frugivore(p) and snapshot()==saved)
|
|
season="Summer"
|
|
for _,bad in ipairs({"broken","-1","inf","nan"}) do
|
|
metadata(p):set_string("succ_nectar_ready_at",bad)
|
|
metadata(p):set_string("botany_ready_at",bad)
|
|
saved=snapshot(); assert(not sim.pollinate(p) and not sim.frugivore(p) and snapshot()==saved,bad)
|
|
end
|
|
end)
|
|
test("frugivore spends one whole existing botanical fruit batch and shares its human lock across restart",function()
|
|
catalogue(); put(p,"blackberry",true)
|
|
assert(not sim.frugivore(p,"raspberry"))
|
|
assert(sim.frugivore(p,"blackberry") and field(p,"botany_ready_at")==now+YEAR)
|
|
assert(name(p)=="hearth:blackberry" and writes==0)
|
|
local saved=snapshot()
|
|
for _=1,20 do assert(not sim.frugivore(p)) end
|
|
load(); assert(not sim.frugivore(p) and snapshot()==saved)
|
|
now=now+DAY; assert(not sim.frugivore(p),"no daily fruit refill")
|
|
local B=hearth.botany
|
|
hearth.construction={allowed=function() return true end}
|
|
local human={is_player=function() return true end,get_inventory=function() error("spent fruit cannot reach inventory") end}
|
|
assert(not B.harvest(p,human))
|
|
now=now+YEAR; season="Winter"; assert(not sim.frugivore(p))
|
|
season="Summer"; assert(sim.frugivore(p))
|
|
end)
|
|
test("all resource hooks reject source and support claims protection markers and missing nodes without metadata writes",function()
|
|
for _,hook in ipairs({"pollinate","frugivore","granivore","detritivore","forage"}) do
|
|
for _,mode in ipairs({"source_claim","support_claim","source_protect","support_protect","source_mark","support_mark","source_missing","support_missing","ambiguous"}) do
|
|
reset(); catalogue()
|
|
put(p,hook=="granivore" and "oak_mature" or (hook=="detritivore" and "litter" or "blackberry"),true)
|
|
if hook=="granivore" then season="Autumn" end
|
|
local q=mode:match("^support") and pos(0,0,0) or p
|
|
if mode:find("claim") then claims[key(q)]=true end
|
|
if mode:find("protect") then protected[key(q)]=true end
|
|
if mode:find("mark") then metadata(q):set_string("third_party:placed","1") end
|
|
if mode:find("missing") then missing[key(q)]=true end
|
|
if mode=="ambiguous" then hearth.claim_owner_at=function(q) return nil,key(q)==key(p) end end
|
|
local saved=snapshot(); assert(not sim[hook](p) and snapshot()==saved,hook.." "..mode)
|
|
end
|
|
end
|
|
end)
|
|
test("two annual nuts fund one meal and a separate one-use local cache; species IDs alone authorize nothing",function()
|
|
catalogue(); season="Autumn"; put(p,"oak_mature",true)
|
|
assert(not sim.disperse_seed(pos(1),"oak"))
|
|
local fed,cache=sim.granivore(p,"oak")
|
|
assert(fed and cache=="oak" and field(p,"succ_nuts_remaining")==0)
|
|
assert(sim.disperse_seed(pos(1),cache))
|
|
assert(hearth.seasons.kind(name(pos(1)))=="oak_seedling")
|
|
assert(metadata(pos(1)):get_string("succ_lifecycle_origin")=="natural_seed")
|
|
assert(not sim.disperse_seed(pos(-1),cache) and not sim.granivore(p))
|
|
load(); assert(not sim.granivore(p) and not sim.disperse_seed(pos(-1),"oak"))
|
|
now=now+YEAR; local fed2,cache2=sim.granivore(p); assert(fed2 and cache2=="oak")
|
|
end)
|
|
test("one remaining nut grants a meal but no cache and immature or wrong-season roots have no seed budget",function()
|
|
catalogue(); season="Autumn"; put(p,"hazel_mature",true)
|
|
metadata(p):set_string("succ_nuts_year",os.date("!*t",now).year)
|
|
metadata(p):set_string("succ_nuts_remaining","1")
|
|
local fed,cache=sim.granivore(p); assert(fed and cache==nil and not sim.disperse_seed(pos(1),"hazel"))
|
|
reset(); catalogue(); season="Autumn"; baseline("oak_seedling"); aged(YEAR)
|
|
local saved=snapshot(); assert(not sim.granivore(p) and snapshot()==saved)
|
|
put(p,"oak_mature",true); aged(40*YEAR); season="Summer"
|
|
saved=snapshot(); assert(not sim.granivore(p) and snapshot()==saved)
|
|
end)
|
|
test("failed or delayed caching burns paid authority; donors and destinations cannot be cloned",function()
|
|
for _,mode in ipairs({"claim","support","marker","occupied","unloaded","donor","delay","wrong","far"}) do
|
|
reset(); catalogue(); season="Autumn"; put(p,"hazel_mature",true)
|
|
assert(sim.granivore(p)); local q=pos(1)
|
|
if mode=="claim" then claims[key(q)]=true end
|
|
if mode=="support" then claims[key(pos(1,0,0))]=true end
|
|
if mode=="marker" then metadata(q):set_string("owner","builder") end
|
|
if mode=="occupied" then put(q,"meadow_grass",true) end
|
|
if mode=="unloaded" then missing[key(q)]=true end
|
|
if mode=="donor" then metadata(p):set_string("player_placed","1") end
|
|
if mode=="delay" then now=now+1 end
|
|
if mode=="far" then q=pos(200) end
|
|
local saved=snapshot()
|
|
assert(not sim.disperse_seed(q,mode=="wrong" and "oak" or "hazel") and snapshot()==saved,mode)
|
|
assert(not sim.disperse_seed(pos(-1),"hazel") and not sim.granivore(p),mode)
|
|
end
|
|
reset(); catalogue(); season="Autumn"; put(p,"oak_mature",true); assert(sim.granivore(p))
|
|
load(); assert(not sim.disperse_seed(pos(1),"oak"),"restart discards authority, not debits")
|
|
end)
|
|
test("litter transfers ten finite tenths, retains its patch until exhausted and cannot pay again after restart",function()
|
|
put(p,"litter",true)
|
|
for i=1,10 do
|
|
assert(sim.detritivore(p),i)
|
|
assert(math.abs(field(pos(0,0,0),"succ_nutrients")-i/10)<0.000001)
|
|
if i<10 then assert(name(p)=="hearth:litter"); load() end
|
|
end
|
|
assert(name(p)=="air" and field(p,"succ_litter_remaining")==0)
|
|
local saved=snapshot(); assert(not sim.detritivore(p) and snapshot()==saved)
|
|
load(); assert(not sim.detritivore(p) and field(pos(0,0,0),"succ_nutrients")<1.000001)
|
|
end)
|
|
test("partial litter feeding and normal calendar decay share exactly one mineral payout",function()
|
|
put(p,"litter",true); assert(sim.detritivore(p)); baseline()
|
|
now=now+90*DAY; advance(100); advance(100)
|
|
assert(name(p)=="air" and math.abs(field(pos(0,0,0),"succ_nutrients")-1)<0.000001)
|
|
end)
|
|
test("last litter unit refuses a protected dependent fungus or full soil without a debit",function()
|
|
catalogue(); put(p,"litter",true); metadata(p):set_string("succ_litter_remaining","0.1")
|
|
put(pos(0,2,0),"wood_blewit",true); protected[key(pos(0,2,0))]=true
|
|
local saved=snapshot(); assert(not sim.detritivore(p) and snapshot()==saved)
|
|
protected={}; metadata(pos(0,0,0)):set_string("succ_nutrients","8")
|
|
saved=snapshot(); assert(not sim.detritivore(p) and snapshot()==saved)
|
|
end)
|
|
test("resource APIs fail closed on malformed positions clocks receipts and nonroot tree components",function()
|
|
catalogue(); put(p,"blackberry",true)
|
|
for _,hook in ipairs({"pollinate","frugivore","granivore","detritivore","forage","disperse_seed"}) do
|
|
for _,bad in ipairs({{}, {x=0/0,y=1,z=0}, {x=math.huge,y=1,z=0}, {x=40000,y=1,z=0}}) do
|
|
local saved=snapshot(); assert(not sim[hook](bad,"oak") and snapshot()==saved,hook)
|
|
end
|
|
end
|
|
api.now=function() return 0/0 end
|
|
for _,hook in ipairs({"pollinate","frugivore","granivore","detritivore","forage"}) do
|
|
local saved=snapshot(); assert(not sim[hook](p) and snapshot()==saved,hook)
|
|
end
|
|
api.now=nil; season="Autumn"; baseline("oak_mature"); aged(40*YEAR)
|
|
put(p,"oak_planks",true)
|
|
local saved=snapshot(); assert(not sim.granivore(p) and snapshot()==saved)
|
|
put(p,"oak_mature",true)
|
|
for _,bad in ipairs({"broken","-1","0.5","3"}) do
|
|
metadata(p):set_string("succ_nuts_remaining",bad)
|
|
saved=snapshot(); assert(not sim.granivore(p) and snapshot()==saved,bad)
|
|
end
|
|
end)
|
|
os.time=original_time
|
|
print("LAST_HEARTH_SUCCESSION_PASS "..passed)
|