This commit is contained in:
2026-11-05 09:57:11 -05:00
parent d34388bf2c
commit 89e392cc2e
81 changed files with 12410 additions and 1135 deletions
+318 -87
View File
@@ -54,6 +54,7 @@ local function reset()
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
@@ -67,6 +68,13 @@ local function baseline(k,age)
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)
@@ -116,15 +124,15 @@ test("dry exposed fern dies, while river moisture changes its niche",function()
put(pos(1,0,0),"water_source")
assert(sim.habitat(p).water==1 and sim.habitat(p).moisture>0.2)
end)
test("tree stages require active age, then mature trees become decomposing wood",function()
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:young_tree" and field(p,"succ_age")==0)
metadata(p):set_string("succ_age","86300"); 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")
metadata(p):set_string("succ_age","259100"); advance(100)
assert(name(p)=="hearth:deadwood")
metadata(p):set_string("succ_age","7100"); advance(100)
assert(name(p)=="hearth:litter" and field(pos(0,0,0),"succ_nutrients")==3)
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)
@@ -159,9 +167,9 @@ test("grass spreads locally but never replaces occupied flower or crop cells",fu
end)
test("moist shaded litter seeds fungi, then returns finite nutrients",function()
light=10
baseline("litter",300); advance(100)
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
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)
advance(1)
assert(field(pos(0,0,0),"succ_nutrients")==1)
@@ -174,23 +182,23 @@ test("dung decomposes, fertilizes soil and does not instantly sprout a forest",f
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 consumes exactly one natural grass or fern, not flowers or planted nodes",function()
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)=="air" and name(pos(1))=="hearth:fern")
assert(field(p,"succ_next")==now+300)
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 cooldown prevents immediate seed recolonization",function()
test("grazing retains the perennial and shares seven-day regrowth across animals",function()
baseline("meadow_grass")
assert(sim.forage(p))
advance(100); advance(100)
assert(name(p)=="air")
advance(100)
assert(name(p)=="hearth:meadow_grass")
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)
@@ -274,15 +282,18 @@ test("vegetation reload alone invalidates age even if its supporting block staye
lbm.action(p); advance(100)
assert(name(p)=="hearth:mature_tree" and field(p,"succ_age")==259100)
end)
test("no catchup on long absence, disconnect, unload, restart or clock rewind",function()
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:young_tree")
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)
@@ -294,7 +305,7 @@ test("failed dispersal attempts survive restart without reroll or advancing age"
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+30)
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)
@@ -372,7 +383,7 @@ test("winter mature canopy still sheds litter and decomposition returns nutrient
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)
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()
@@ -410,54 +421,56 @@ local function diverse()
hearth.diversity=dofile(target:gsub("[^/\\]+$","diversity.lua"))(hearth,api)
load()
end
test("unmarked mapgen deadwood is adopted and decays after two active hours with finite capped nutrients",function()
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)
advance(100)
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)
for _=1,18 do advance(100) end
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); advance(100)
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("adopted deadwood never catches up across absence, disconnect, reload, restart or clock rewind",function()
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" and field(p,"succ_age")==7100)
advance(864000); assert(name(p)=="hearth:deadwood")
local born=field(p,"succ_decay_at")
players=false; sim.step(1); players=true; advance(100)
assert(field(p,"succ_age")==7100)
lbm.action(p); advance(100); assert(field(p,"succ_age")==7100)
lbm.action(pos(0,0,0)); advance(100); assert(field(p,"succ_age")==7100)
load(); advance(100); assert(field(p,"succ_age")==7100)
advance(-100); assert(field(p,"succ_age")==7100)
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)
advance(100)
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 birch identity and deadwood records its originating species",function()
diverse(); baseline("birch_seedling",21500); advance(100)
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")
metadata(p):set_string("succ_age","86300"); advance(100)
aged(20*YEAR); advance(100)
assert(name(p)=="hearth:birch_mature")
metadata(p):set_string("succ_age","259100"); advance(100)
assert(name(p)=="hearth:deadwood" and metadata(p):get_string("succ_species")=="birch")
metadata(p):set_string("succ_age","7100"); advance(100)
assert(name(p)=="hearth:litter" and metadata(p):get_string("succ_species")=="birch")
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(); baseline("birch_mature",2000)
diverse(); season="Autumn"; baseline("birch_mature",2000)
metadata(p):set_string("succ_serial","9"); advance(100)
assert(name(pos(-1,1,0))=="hearth:birch_seedling")
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
@@ -555,8 +568,9 @@ test("species seeds respect source, destination and support authority without em
assert(name(seed)~="hearth:birch_seedling",mode)
end
end)
test("species age pauses across winter unload and restart without losing identity",function()
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)
@@ -611,9 +625,10 @@ local function supplied_habitat(h)
return out
end
end
test("ecology80 recruits every real returned candidate, spreads it and completes its finite lifecycle",function()
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)
@@ -621,40 +636,40 @@ test("ecology80 recruits every real returned candidate, spreads it and completes
-- 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)%80~=index-1 or (x*7)%23~=0 do x=x+1; assert(x<1840) end
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); metadata(p):set_string("succ_age","2000"); metadata(p):set_string("succ_serial","9")
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); metadata(p):set_string("succ_age","21600"); advance(100)
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"))
metadata(p):set_string("succ_age","86400"); advance(100)
aged(profile.maturity_days.max*DAY); advance(100)
assert(metadata(p):get_string("succ_tree_stage")=="mature",r.id.." mature")
metadata(p):set_string("succ_age","259200"); advance(100)
assert(name(p)=="hearth:deadwood" and world[key(p)].param2==12,r.id.." fallen")
for id,n in pairs(world) do
if id~=key(p) then assert(metadata((function()
local a,b,c=id:match("([^:]+):([^:]+):([^:]+)"); return pos(tonumber(a),tonumber(b),tonumber(c))
end)()):get_string("succ_component")=="",r.id.." orphan component "..n.name) end
end
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)
assert(hearth.seasons.kind(name(q))==candidate,r.id.." spread got "..name(q))
metadata(p):set_string("succ_age","1000000"); advance(100)
assert(name(p)=="air" or name(p)=="hearth:litter",r.id.." senescence")
if name(p)=="hearth:litter" then
metadata(p):set_string("succ_age","1800"); advance(100); assert(name(p)=="air",r.id.." recycling")
end
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)
@@ -674,15 +689,15 @@ test("real habitat distinguishes bank depth, source columns, pH, host and bark w
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 submerged stand decay never clear or refill water",function()
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")
metadata(p):set_string("succ_age","21600"); advance(100)
assert(name(p)=="air" and 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)
@@ -701,9 +716,9 @@ test("water placement denies flowing, too deep, unloaded, marked, claimed and pr
assert(name(p)=="air" and writes==0,mode)
end
end)
test("real block plans expand atomically, survive restart and collapse only owned cubes",function()
test("real block plans expand atomically, survive restart and never collapse from old age",function()
catalogue(); humidity=45
baseline("birch_seedling",21600); advance(100)
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
@@ -711,19 +726,20 @@ test("real block plans expand atomically, survive restart and collapse only owne
assert(metadata(q):get_string("succ_component")==key(p)); count=count+1
end
assert(count>10)
load(); sim.step(1); metadata(p):set_string("succ_age","86400"); advance(100)
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_age","259200"); advance(100)
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:deadwood" and world[key(p)].param2==12)
assert(name(pos(0,7,0))=="air")
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)
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
@@ -737,14 +753,14 @@ test("every tree destination and its support fail closed before any plan write",
end
end)
test("chopped ecology root cleans matching components but never mapgen or foreign timber",function()
catalogue(); humidity=45; baseline("oak_seedling",21600); advance(100)
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 debit existing soil on faster growth",function()
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)
@@ -752,6 +768,8 @@ test("catalogue nitrogen caps survive restart and root fungi debit existing soil
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")
@@ -762,7 +780,7 @@ test("saprotrophs age existing deadwood without minting nutrients or attacking n
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_age")>0 and field(pos(0,0,0),"succ_nutrients")==0)
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)
@@ -775,8 +793,9 @@ test("flowering, browse and nursery use botanical tags while harvested bushes re
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); advance(100); assert(field(p,"succ_age")==1100 and field(p,"botany_ready_at")>now)
assert(sim.forage(p)); assert(name(p)=="air" and field(p,"botany_ready_at")>now)
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")
@@ -784,6 +803,25 @@ test("flowering, browse and nursery use botanical tags while harvested bushes re
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
@@ -799,7 +837,7 @@ test("one global tree-plan reservation bounds eight concurrent roots and malform
end
for _,root in ipairs(positions) do put(root,"oak_seedling",true) end
sim.step(1)
for _,root in ipairs(positions) do metadata(root):set_string("succ_age","21600") end
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)
@@ -807,7 +845,7 @@ test("one global tree-plan reservation bounds eight concurrent roots and malform
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)
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"}}
@@ -820,14 +858,14 @@ test("one global tree-plan reservation bounds eight concurrent roots and malform
advance(100); assert(name(p)=="hearth:oak_seedling" and writes==0,mode)
end
end)
test("water stands and botanical understory retain loaded-only ages across restart and reload",function()
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")==1100)
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()
@@ -887,7 +925,7 @@ test("alder all living stages and dog lichen share the durable finite nitrogen b
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)
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)
@@ -912,27 +950,220 @@ test("nitrogen never exceeds local four ceiling or writes through source or supp
assert(field(below,"succ_fixed_nitrogen")== (mode=="fraction" and 0.25 or 0),mode)
end
end)
test("multiple host fungi cannot copy nutrients and exhausted soil cannot fund early growth",function()
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")==0)
assert(name(p)==(budget==1 and "hearth:birch_young" or "hearth:birch_seedling"))
advance(100); assert(field(pos(0,0,0),"succ_nutrients")==0)
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); advance(100)
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)
metadata(p):set_string("succ_age","1800"); advance(100)
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)