Files
last-hearth/tests/biomes_spec.lua
T
2026-11-05 09:57:11 -05:00

364 lines
19 KiB
Lua
Executable File

-- Standalone Lua 5.1+; registration contract, no world or engine is launched.
local target = assert(arg[1], "biomes.lua path required")
local factory = dofile(target)
local passed = 0
local function contains(list, value)
for _, entry in ipairs(list or {}) do if entry == value then return true end end
return false
end
local function count(t) local n=0; for _ in pairs(t) do n=n+1 end; return n end
local function fixture(existing_alias)
local nodes, items, biomes, ores, decos, crafts = {}, {}, {}, {}, {}, {}
local aliases = {external_alias="external:stone"}
if existing_alias then aliases.mapgen_stone = "external:stone" end
for _, name in ipairs({"stone", "soil", "grass", "sand", "water_source", "water_flowing",
"log", "leaves", "planks", "sticks", "loose_stone", "berry_bush", "reeds", "grain_patch",
"clay_bank", "iron_ore_vein", "weir_stone", "meadow_grass", "wildflowers", "fern", "litter",
"fungi", "deadwood", "spring_greens_patch", "autumn_mushrooms_patch"}) do
nodes["hearth:"..name] = {sentinel=true}; items["hearth:"..name] = nodes["hearth:"..name]
end
for _, name in ipairs({"iron_ore", "charcoal", "charcoal_charge", "clay", "foundation"}) do
items["hearth:"..name] = {sentinel=true}
end
local external = {sentinel=true}
biomes.external = external
local placed, owned = function() end, function() end
local construction = {place_building=placed, owner=owned}
local api = {registered_nodes=nodes, registered_items=items, registered_aliases=aliases}
function api.register_alias(name, value)
assert(aliases[name] == nil, "alias overwritten"); aliases[name] = value
end
function api.register_craftitem(name, def)
assert(not items[name]); items[name] = def
end
function api.register_biome(def) assert(not biomes[def.name]); biomes[def.name] = def end
function api.register_ore(def) ores[#ores+1] = def end
function api.register_decoration(def)
assert(not decos[def.name]); decos[def.name] = def; decos[#decos+1] = def
end
function api.register_craft(def) crafts[#crafts+1] = def end
-- Any attempt to clear registries, force aliases, change settings, install
-- globalsteps or write world nodes is a test failure, not a permissive noop.
setmetatable(api, {__index=function(_, key) error("unapproved engine API: " .. key) end})
local registrations = 0
local function private_register(name, def)
assert(not name:find(":"), "private helper takes short names")
local full="hearth:"..name
assert(not nodes[full], "base node overwritten")
registrations=registrations+1; nodes[full]=def; items[full]=def
end
local hearth = {sentinel=external}
local m = factory(hearth, api, private_register, construction)
return {m=m, nodes=nodes, items=items, biomes=biomes, ores=ores, decos=decos, crafts=crafts,
aliases=aliases, api=api, hearth=hearth, construction=construction, external=external,
registrations=function() return registrations end}
end
local function test(label, fn)
local ok, err=pcall(fn)
assert(ok, label .. ": " .. tostring(err)); passed=passed+1
print("ok " .. passed .. " - " .. label)
end
local function loaded(existing) local f=fixture(existing); assert(f.m.register()==f.m); return f end
local function recipe(f, input, output)
local inputs = type(input) == "table" and input or {input}
local matches = 0
for _, craft in ipairs(f.crafts) do
if craft.output == output and craft.type == "shapeless" and #craft.recipe == #inputs then
local same = true
for i, value in ipairs(inputs) do if craft.recipe[i] ~= value then same = false end end
if same then matches = matches + 1 end
end
end
return matches == 1
end
local function cell(s, x,y,z) return s.data[1+x+s.size.x*(y+s.size.y*z)] end
test("factory is lazy and does not publish globals or mutate hearth", function()
local f=fixture()
assert(f.registrations()==0 and #f.ores==0 and #f.decos==0 and #f.crafts==0)
assert(count(f.hearth)==1 and f.hearth.sentinel==f.external)
assert(type(f.m.register)=="function" and type(f.m.surface)=="function")
end)
test("registration is idempotent and preserves external registries", function()
local f=loaded(); local n, ores, decos, crafts=f.registrations(), #f.ores, #f.decos, #f.crafts
assert(f.m.register()==f.m)
assert(n==f.registrations() and ores==#f.ores and decos==#f.decos and crafts==#f.crafts)
assert(f.biomes.external==f.external and f.nodes["hearth:grass"].sentinel)
assert(f.aliases.external_alias=="external:stone")
end)
test("mapgen aliases are non-forcing and keep canonical stone identity", function()
local f=loaded()
assert(f.aliases.mapgen_stone=="hearth:stone")
assert(f.aliases.mapgen_water_source=="hearth:water_source")
assert(f.aliases.mapgen_river_water_source=="hearth:water_source")
assert(loaded(true).aliases.mapgen_stone=="external:stone")
end)
test("alias with a registered item is not overwritten", function()
local f=fixture(); f.items.mapgen_stone={sentinel=true}; f.m.register()
assert(not f.aliases.mapgen_stone and f.items.mapgen_stone.sentinel)
end)
test("terrain node collision fails rather than overwriting", function()
local f=fixture(); local old={sentinel=true}; f.nodes["hearth:granite"]=old
assert(not pcall(f.m.register)); assert(f.nodes["hearth:granite"]==old)
end)
test("ten biomes preserve meadow and river names and resolve all materials", function()
local f=loaded(); assert(count(f.biomes)==11)
assert(f.biomes.hearth_temperate.node_top=="hearth:grass")
assert(f.biomes.hearth_riverbank.node_top=="hearth:sand")
for name, b in pairs(f.biomes) do if name~="external" then
assert(b.y_min<=b.y_max and b.heat_point>=0 and b.heat_point<=100)
assert(b.humidity_point>=0 and b.humidity_point<=100)
for key, value in pairs(b) do if key:match("^node_") then assert(f.nodes[value], value) end end
end end
end)
test("altitude coverage has no missing climate-independent fallback band", function()
local f=loaded()
for y=-512,512 do
local coverage=false
for _, b in ipairs(f.m.biome_profiles) do if y>=b.min and y<=b.max then coverage=true end end
assert(coverage, "altitude "..y)
end
local peak=f.biomes.hearth_rocky_peaks
assert(peak.y_min==211 and peak.node_top=="hearth:snow" and peak.vertical_blend==0)
assert(f.biomes.hearth_floodplain.y_max==8)
end)
test("habitat helper resolves qualified nodes and rejects unknown surfaces", function()
local m=loaded().m
assert(m.surface("hearth:peat").moisture > m.surface("podzol").moisture)
assert(m.surface({name="hearth:alluvium"}).fertility==1)
assert(not m.surface("other:grass") and not m.surface(false) and not m.surface({}))
assert(m.supports_species("willow", "hearth:alluvium"))
assert(not m.supports_species("willow", "hearth:podzol"))
assert(not m.supports_species("unknown", "grass"))
assert(not m.supports_species("oak", false))
end)
test("thirteen exact catalogue species have real full-block trunks, leaves, and timber", function()
local f=loaded(); assert(table.concat(f.m.species_order,",")=="oak,birch,pine,willow,alder,beech,aspen,rowan,hazel,spruce,crab_apple,wild_cherry,field_maple")
for _, species in ipairs(f.m.species_order) do
local log=f.nodes["hearth:"..species.."_log"]
local leaf=f.nodes["hearth:"..species.."_leaves"]
assert(log.groups.tree==1 and log.groups.choppy==2 and log.drawtype==nil)
assert(log.tiles[1]=="hearth_"..species.."_log_top.png")
assert(log.tiles[3]=="hearth_"..species.."_log_side.png")
assert(leaf.drawtype=="allfaces_optional" and leaf.groups.leaves==1)
assert(f.nodes["hearth:"..species.."_planks"].groups.wood==1)
assert(not f.nodes["hearth:"..species.."_seedling"])
end
end)
test("species timber converts losslessly into canonical progression ingredients", function()
local f=loaded()
for _, species in ipairs(f.m.species_order) do
local log="hearth:"..species.."_log"
assert(recipe(f,{log,log},"hearth:log 2"))
assert(not recipe(f,log,"hearth:log") and not recipe(f,log,"hearth:log 2"))
assert(recipe(f,"hearth:"..species.."_log","hearth:"..species.."_planks 4"))
assert(recipe(f,"hearth:"..species.."_planks","hearth:planks"))
end
-- Existing recipes accept these exact names for charcoal, walls and sticks.
assert(f.items["hearth:log"] and f.items["hearth:planks"] and f.items["hearth:clay"])
end)
test("geological stone remains useful masonry and canonical foundations", function()
local f=loaded()
for _, rock in ipairs(f.m.rocks) do
local raw, brick="hearth:"..rock, "hearth:"..rock.."_bricks"
assert(f.nodes[raw].groups.cracky==3 and f.nodes[raw].drop==nil)
assert(recipe(f,raw,brick.." 4") and recipe(f,{raw,raw},"hearth:stone 2"))
assert(not recipe(f,raw,"hearth:stone") and not recipe(f,raw,"hearth:stone 2"))
assert(f.nodes[brick].groups.hearth_building==1)
assert(f.nodes[brick].on_place==f.construction.place_building)
assert(f.nodes[brick].after_place_node==f.construction.owner)
end
end)
test("all 49 terrain recipes have unique shapeless inputs including multiplicity", function()
local f=loaded(); assert(#f.crafts==49)
local seen={}
for _, craft in ipairs(f.crafts) do
assert(craft.type=="shapeless")
local inputs={}
for i, input in ipairs(craft.recipe) do inputs[i]=input end
table.sort(inputs)
local key=table.concat(inputs,",")
assert(not seen[key], "colliding terrain recipe: "..craft.output.." and "..tostring(seen[key]))
seen[key]=craft.output
end
end)
test("all registered material drops and crafting outputs resolve", function()
local f=loaded()
local function resolved(stack)
local name=stack:match("^([^ ]+)"); assert(f.items[name], "unresolved "..stack)
end
for _, def in pairs(f.nodes) do if type(def.drop)=="string" and def.drop~="" then resolved(def.drop) end end
for _, craft in ipairs(f.crafts) do
resolved(craft.output); for _, input in ipairs(craft.recipe) do resolved(input) end
end
end)
test("resource items and ore nodes use precise original art names", function()
local f=loaded()
for item, vein in pairs({quartz="quartz_vein", copper_ore="copper_ore_vein", coal="coal_ore_vein"}) do
assert(f.items["hearth:"..item].inventory_image=="hearth_item_"..item..".png")
assert(f.nodes["hearth:"..vein].tiles[1]=="hearth_"..vein..".png")
assert(f.nodes["hearth:"..vein].drop=="hearth:"..item.." 2")
end
assert(f.nodes["hearth:iron_ore_vein"].sentinel)
end)
test("geology runs before nested restricted scatter ore registration", function()
local f=loaded(); assert(#f.ores==9)
assert(f.ores[4].ore_type=="blob" and f.ores[4].ore=="hearth:basalt")
assert(f.ores[4].noise_params and f.ores[4].y_max<0)
local generated={}
for _, ore in ipairs(f.ores) do
assert(f.nodes[ore.ore] and ore.clust_scarcity>0 and ore.y_min<=ore.y_max)
for _, host in ipairs(ore.wherein) do
assert(f.nodes[host] and f.nodes[host].groups and f.nodes[host].groups.stone==1 or host=="hearth:stone")
assert(not contains({"hearth:soil", "hearth:grass", "air"},host))
end
if ore.ore_type=="scatter" then
assert(ore.clust_num_ores<=ore.clust_size^3)
generated[ore.ore]=true
end
end
for _, name in ipairs({"quartz_vein", "copper_ore_vein", "coal_ore_vein", "iron_ore_vein"}) do assert(generated["hearth:"..name]) end
assert(contains(f.ores[5].wherein,"hearth:granite") and not contains(f.ores[5].wherein,"hearth:sandstone"))
assert(contains(f.ores[6].wherein,"hearth:basalt"))
assert(contains(f.ores[7].wherein,"hearth:sandstone") and not contains(f.ores[7].wherein,"hearth:granite"))
end)
test("v7 underground riverbank stone receives ordered resource-bearing beds", function()
local f=loaded()
for i=1,3 do assert(f.ores[i].clust_size==0 and f.ores[i].clust_scarcity==1) end
local function host_at(y)
local host=f.biomes.hearth_riverbank.node_stone
for _, bed in ipairs(f.ores) do
if bed.ore_type=="stratum" and y>=bed.y_min and y<=bed.y_max and contains(bed.wherein,host) then host=bed.ore end
end
return host
end
assert(host_at(-150)=="hearth:granite" and host_at(-80)=="hearth:granite")
assert(host_at(-79)=="hearth:slate" and host_at(-24)=="hearth:slate")
assert(host_at(-23)=="hearth:limestone" and host_at(-8)=="hearth:limestone")
assert(host_at(-7)=="hearth:sandstone")
for _, ore in ipairs(f.ores) do if ore.ore_type=="scatter" and ore.y_min==-31000 then
local found=false
for y=-150,-8 do if contains(ore.wherein,host_at(y)) and y<=ore.y_max then found=true end end
assert(found,"unreachable ore host "..ore.ore)
end end
end)
test("schematic dimensions, probabilities and trunk anchoring are valid", function()
local f=loaded()
for _, species in ipairs(f.m.species_order) do
local s=f.m.tree_schematic(species); local c=math.floor(s.size.x/2)
assert(s.size.y>=5 and s.size.y<=10 and s.size.x<=9 and #s.data==s.size.x*s.size.y*s.size.z)
for y=0,s.size.y-2 do assert(cell(s,c,y,c).name=="hearth:"..species.."_log") end
local leaves, air=0,0
for _, n in ipairs(s.data) do
assert(n.param2==0 and not n.force_place)
if n.name=="air" then assert(n.prob==0); air=air+1
else assert(f.nodes[n.name] and n.prob==255); if n.name:match("_leaves$") then leaves=leaves+1 end end
end
assert(leaves>10 and air>10)
end
assert(not pcall(f.m.tree_schematic,"unknown"))
end)
test("all ten silhouettes are geometrically distinct and young canopies stay bounded", function()
local m=loaded().m; local shapes={}
for _,species in ipairs(m.species_order) do
local s=m.tree_schematic(species); local shape={s.size.x..":"..s.size.y..":"..s.size.z}
for _,n in ipairs(s.data) do shape[#shape+1]=n.name=="air" and "." or (n.name:match("_log$") and "T" or "L") end
local signature=table.concat(shape,",")
assert(not shapes[signature],species.." duplicates "..tostring(shapes[signature])); shapes[signature]=species
local young=m.tree_schematic(species,false,"young"); local leaves=0
assert(young.size.x==3 and young.size.y<=5 and #young.data<=45)
for _,n in ipairs(young.data) do
assert(not n.force_place)
if n.name:match("_leaves$") then leaves=leaves+1 end
end
assert(leaves>=5)
end
assert(not pcall(m.tree_schematic,"oak",false,"invalid"))
end)
test("fallen logs are three full cubes with horizontal facedir and non-forcing mapgen only", function()
local f=loaded(); local d=f.decos["hearth:woodland_deadwood"]
assert(d.deco_type=="schematic" and d.rotation=="random" and d.place_offset_y==1)
assert(not d.flags:find("force_placement",1,true) and not d.force_placement)
assert(d.schematic.size.x==3 and d.schematic.size.y==1 and d.schematic.size.z==1)
assert(#d.schematic.data==3 and d.y_min==2 and d.y_max==160)
for _,n in ipairs(d.schematic.data) do
assert(n.name=="hearth:deadwood" and n.prob==255 and n.param2==12 and not n.force_place)
-- Luanti facedir / 4 = axis direction; axis 3 is +X, axis 1 is +Z.
assert(math.floor(n.param2/4)==3)
end
for _,ground in ipairs(d.place_on) do assert(ground~="hearth:water_source") end
end)
test("species silhouettes are distinct broad slender conical drooping riparian", function()
local m=loaded().m
local oak, birch, pine, willow, alder=m.tree_schematic("oak"),m.tree_schematic("birch"),m.tree_schematic("pine"),m.tree_schematic("willow"),m.tree_schematic("alder")
assert(oak.size.x>birch.size.x and birch.size.y>oak.size.y)
assert(cell(oak,6,4,3).name=="hearth:oak_leaves")
assert(cell(pine,0,2,3).name=="hearth:pine_leaves")
assert(cell(pine,0,7,3).name=="air" and cell(pine,3,8,3).name=="hearth:pine_leaves")
assert(cell(willow,7,1,7).name=="hearth:willow_leaves")
assert(cell(willow,4,1,4).name=="hearth:willow_log")
assert(alder.size.y==6 and m.profiles.alder.nitrogen_fixer)
end)
test("tree decorations are habitat-restricted and cannot erase terrain", function()
local f=loaded()
for _, species in ipairs(f.m.species_order) do
local d=f.decos["hearth:"..species.."_trees"]
assert(d.deco_type=="schematic" and #d.biomes>=1 and #d.place_on>=1)
assert(d.place_offset_y==1 and d.rotation=="random")
assert(not d.flags:find("force_placement",1,true))
if species=="willow" or species=="alder" then
assert(d.num_spawn_by==1 and contains(d.spawn_by,"hearth:water_source"))
end
end
end)
test("all canopy registrations precede ground flora", function()
local f=loaded(); local first_plant=math.huge
for i,d in ipairs(f.decos) do if d.deco_type=="simple" then first_plant=math.min(first_plant,i) end end
for i,d in ipairs(f.decos) do if d.name:match("_trees$") then assert(i<first_plant) end end
end)
test("canonical meadow timber and all-surface gathering remain available", function()
local f=loaded(); local d=f.decos["hearth:temperate_trees"]
assert(contains(d.biomes,"hearth_temperate")); local log=false
for _, n in ipairs(d.schematic.data) do if n.name=="hearth:log" then log=true end end
assert(log)
for name in pairs(f.m.surfaces) do
assert(contains(f.decos["hearth:loose_stone"].place_on,"hearth:"..name))
assert(contains(f.decos["hearth:sticks"].place_on,"hearth:"..name))
end
end)
test("riparian food and clay require water and upland springs are limited", function()
local f=loaded()
for _, id in ipairs({"reeds", "clay_bank", "old_river_weir"}) do
local d=f.decos["hearth:"..id]; assert(d.num_spawn_by==1 and d.check_offset==-1)
assert(contains(d.spawn_by,"hearth:water_source"))
end
assert(f.decos["hearth:clay_bank"].y_max==8)
assert(f.decos["hearth:seep_springs"].y_max==150)
assert(not contains(f.decos["hearth:seep_springs"].biomes,"hearth_dry_scrub"))
end)
test("flora communities and seasonal patches resolve without new growth nodes", function()
local f=loaded()
for _, d in ipairs(f.decos) do
for _, ground in ipairs(d.place_on) do assert(f.nodes[ground],ground) end
for _, biome in ipairs(d.biomes or {}) do assert(f.biomes[biome],biome) end
if d.decoration then assert(f.nodes[d.decoration],d.decoration) end
end
assert(contains(f.decos["hearth:woodland_ferns"].biomes,"hearth_wet_forest"))
assert(not contains(f.decos["hearth:woodland_ferns"].biomes,"hearth_dry_scrub"))
assert(f.decos["hearth:spring_foraging"].decoration=="hearth:spring_greens_patch")
assert(f.decos["hearth:autumn_foraging"].decoration=="hearth:autumn_mushrooms_patch")
end)
test("world texture contract is explicit for every new node", function()
local f=loaded(); local textures={}
for name, def in pairs(f.nodes) do if not def.sentinel then
assert(def.tiles and #def.tiles>=1,name)
for _, image in ipairs(def.tiles) do
assert(image:match("^hearth_[a-z_]+%.png$"),image); textures[image]=true
end
end end
-- Thirteen woods each have their own end grain, bark, foliage and planks.
assert(count(textures)==74, "unexpected world tile count "..count(textures))
assert(f.registrations()==60, "unexpected private node count "..f.registrations())
end)
print("PASS " .. passed .. " biome/geology checks")