299 lines
15 KiB
Lua
Executable File
299 lines
15 KiB
Lua
Executable File
-- Standalone Lua 5.1/LuaJIT (also Lua 5.3) fauna contract tests.
|
|
local target=assert(arg[1],"wildlife.lua path required")
|
|
local passed=0
|
|
local function test(name,fn)
|
|
local ok,err=pcall(fn)
|
|
assert(ok,name..": "..tostring(err)); passed=passed+1
|
|
print("ok "..passed.." - "..name)
|
|
end
|
|
local function fixture()
|
|
local h={defs={},objects={},players={},steps={},habitat=true,plants=true,natural=true,forages=0,deposits=0,
|
|
blocked=function() return false end,terrain=function(p) return p.y<=3 and "hearth:grass" or "air" end}
|
|
local api={}
|
|
function api.register_entity(n,d) h.defs[n]=d end
|
|
function api.register_globalstep(fn) h.steps[#h.steps+1]=fn end
|
|
function api.get_connected_players() return h.players end
|
|
function api.get_node_or_nil(p)
|
|
if h.unloaded and h.unloaded(p) then return nil end
|
|
local n=h.terrain(p)
|
|
if n=="air" and p.y==4 and h.plants then n="hearth:meadow_grass" end
|
|
return {name=n}
|
|
end
|
|
function api.get_meta() return {get_string=function(_,key) return key=="succ_natural" and h.natural and "1" or "" end} end
|
|
function api.is_protected(p) return h.blocked(p) end
|
|
function api.add_item() error("wildlife must never drop items") end
|
|
function h.player(x,z)
|
|
local p={pos={x=x or 0,y=4,z=z or 0}}
|
|
function p:get_pos() return self.pos end
|
|
function p:punch() error("animals must never punch players") end
|
|
h.players[#h.players+1]=p; return p
|
|
end
|
|
function h.add(kind,x,z,payload,restore)
|
|
local def=assert(h.defs["hearth:"..kind])
|
|
local e={name="hearth:"..kind}
|
|
local obj={pos={x=x or 12,y=3.5,z=z or 0},entity=e}
|
|
e.object=obj
|
|
function obj:get_pos() return not self.removed and self.pos or nil end
|
|
function obj:get_luaentity() return self.entity end
|
|
function obj:set_pos(p) self.pos=p; self.moves=(self.moves or 0)+1 end
|
|
function obj:set_velocity(v) self.velocity=v end
|
|
function obj:set_acceleration(v) self.acceleration=v end
|
|
function obj:set_armor_groups(v) self.armor=v end
|
|
function obj:set_yaw(y) self.yaw=y end
|
|
function obj:remove() self.removed=true; def.on_deactivate(e) end
|
|
h.objects[#h.objects+1]=obj
|
|
def.on_activate(e,payload or "",restore or 0)
|
|
return e,obj
|
|
end
|
|
function api.add_entity(p,name)
|
|
local _,obj=h.add(name:match(":(.+)$"),p.x,p.z)
|
|
return obj
|
|
end
|
|
local hearth={claim_owner_at=function(p)
|
|
if h.ambiguous and h.ambiguous(p) then return nil,true end
|
|
if h.claim and h.claim(p) then return "owner",false end
|
|
return nil,false
|
|
end,claim_allowed=function() error("empty-name claim_allowed must never be used") end}
|
|
hearth.succession={habitat=function(p)
|
|
local available=h.habitat and (not h.habitat_at or h.habitat_at(p))
|
|
return {loaded=not h.habitat_unloaded,grass=available and 8 or 0,cover=0}
|
|
end,
|
|
forage=function(p)
|
|
assert(not h.blocked(p)); h.forages=h.forages+1
|
|
return h.feed_ok~=false
|
|
end,
|
|
deposit=function(p) assert(not h.blocked(p)); h.deposits=h.deposits+1; return true end}
|
|
h.hearth=hearth; h.api=api; h.result=dofile(target)(hearth,api)
|
|
function h.tick(n,dt)
|
|
for _=1,n do
|
|
local snapshot={}
|
|
for _,o in ipairs(h.objects) do snapshot[#snapshot+1]=o end
|
|
for _,o in ipairs(snapshot) do if not o.removed then h.defs[o.entity.name].on_step(o.entity,dt or 1) end end
|
|
for _,fn in ipairs(h.steps) do fn(dt or 1) end
|
|
end
|
|
end
|
|
function h.stats() return h.result.status() end
|
|
return h
|
|
end
|
|
|
|
test("three original grounded meshes, body collision, gravity, immortal and nonpersistent",function()
|
|
local h=fixture(); h.player()
|
|
for _,kind in ipairs({"hare","deer","fox"}) do
|
|
local d=h.defs["hearth:"..kind]; local p=d.initial_properties
|
|
assert(p.visual=="mesh" and p.mesh=="hearth_"..kind..".obj" and p.textures[1]=="hearth_"..kind..".png")
|
|
assert(p.visual_size.x==1 and p.visual_size.y==1 and p.collisionbox[2]==0)
|
|
assert(p.physical and p.collide_with_objects and p.static_save==false and p.pointable==false)
|
|
local e,o=h.add(kind,12)
|
|
assert(not o.removed and o.acceleration.y<0 and o.armor.immortal==1)
|
|
assert(d.on_punch(e,h.players[1])==true and not o.removed)
|
|
end
|
|
end)
|
|
test("activation rejects payloads, restores, invalid positions and duplicate accounting",function()
|
|
local h=fixture(); h.player()
|
|
local _,a=h.add("hare",12,0,"forged"); assert(a.removed)
|
|
local _,b=h.add("hare",12,0,"",1); assert(b.removed)
|
|
local _,c=h.add("hare",0/0); assert(c.removed)
|
|
local e=h.add("hare",12); h.defs[e.name].on_activate(e,"")
|
|
assert(h.stats().total==1)
|
|
end)
|
|
test("synchronous native activation enforces player and local caps",function()
|
|
local h=fixture(); h.player()
|
|
for _=1,20 do h.add("hare",12) end
|
|
assert(h.stats().total==6)
|
|
-- No player nearby is not an unlimited admission path.
|
|
local _,o=h.add("deer",1000); assert(o.removed)
|
|
local snap=h.stats(); snap.hare=999; assert(h.stats().hare==6)
|
|
local j=fixture(); j.player(-30); j.player(30)
|
|
for _=1,10 do j.add("deer",0) end
|
|
assert(j.stats().deer==6,"local cap spans overlapping player neighborhoods")
|
|
end)
|
|
test("global and species caps include distant active creatures until deactivation",function()
|
|
local h=fixture()
|
|
for i=1,12 do h.player(i*120) end
|
|
for i=1,30 do h.add("hare",math.ceil(i/3)*120+12) end
|
|
assert(h.stats().hare==16)
|
|
for i=1,20 do h.add("deer",math.ceil(i/3)*120+12) end
|
|
assert(h.stats().deer==10)
|
|
for i=1,12 do h.add("fox",(i+5)*120+12) end
|
|
assert(h.stats().total==32 and h.stats().fox==6)
|
|
for _,o in ipairs(h.objects) do if not o.removed then o.pos.x=o.pos.x+10000 end end
|
|
h.add("fox",132); assert(h.stats().total==32)
|
|
local e=h.objects[1].entity
|
|
h.defs[e.name].on_deactivate(e); h.defs[e.name].on_deactivate(e)
|
|
assert(h.stats().total==31)
|
|
end)
|
|
test("habitat recruitment is slow, empty or unloaded habitat admits nothing",function()
|
|
local h=fixture(); h.player(); h.tick(29); assert(h.stats().total==0)
|
|
h.tick(1); assert(h.stats().hare==1)
|
|
local j=fixture(); j.player(); j.habitat=false; j.tick(120); assert(j.stats().total==0)
|
|
j.habitat=true; j.habitat_unloaded=true; j.tick(60); assert(j.stats().total==0)
|
|
local k=fixture(); k.player(); k.plants=false
|
|
k.hearth.succession.habitat=function() return {loaded=true,grass=0,cover=8} end
|
|
k.tick(60); assert(k.stats().total==0,"tree cover without edible plants must not recruit")
|
|
end)
|
|
test("natural grazing earns food then delayed dung; planted lookalikes never graze",function()
|
|
local h=fixture(); h.player(); local e=h.add("deer",12)
|
|
h.tick(7); assert(h.forages==0 and h.deposits==0)
|
|
h.tick(1); assert(h.forages==1 and e.food==1 and e.hunger<15)
|
|
h.tick(7); assert(h.deposits==0)
|
|
h.tick(1); assert(h.deposits==1 and h.stats().dung==1)
|
|
local j=fixture(); j.player(); j.add("hare",12); j.natural=false; j.tick(20)
|
|
assert(j.forages==0 and j.deposits==0)
|
|
end)
|
|
test("two fed mature animals breed once, consume both parents' food, and cool down",function()
|
|
local h=fixture(); h.player(); local a=h.add("deer",12); local b=h.add("deer",13)
|
|
-- Time and meals are established explicitly; no instant birth on activation.
|
|
h.tick(2); assert(h.stats().births==0)
|
|
for _,e in ipairs({a,b}) do e.age=60; e.food=3; e.hunger=0; e.breed_at=0 end
|
|
h.tick(2); assert(h.stats().births==1 and h.stats().deer==3)
|
|
assert(a.food==0 and b.food==0)
|
|
h.tick(2); assert(h.stats().births==1)
|
|
end)
|
|
test("empty habitat and unsuccessful forage cannot fund breeding",function()
|
|
local h=fixture(); h.player(); local a=h.add("hare",12); local b=h.add("hare",13)
|
|
for _,e in ipairs({a,b}) do e.age=60;e.food=3;e.hunger=0;e.breed_at=0 end
|
|
h.habitat=false; h.tick(10); assert(h.stats().births==0 and h.forages==0)
|
|
local j=fixture(); j.player(); j.feed_ok=false
|
|
local c=j.add("deer",12); local d=j.add("deer",13)
|
|
c.age=60;d.age=60; j.tick(25)
|
|
assert(c.food==0 and d.food==0 and j.stats().births==0 and j.deposits==0)
|
|
end)
|
|
test("predation consumes finite registered hares once and has a meal cooldown",function()
|
|
local h=fixture(); h.player(); local fox=h.add("fox",12); local prey,o=h.add("hare",12.4)
|
|
-- Hold prey until the predator's turn to isolate accounting from flight.
|
|
prey.next_ai=999; fox.feed_at=0
|
|
h.tick(2); assert(o.removed and h.stats().meals==1 and h.stats().hare==0 and fox.food==1)
|
|
h.defs[prey.name].on_deactivate(prey); assert(h.stats().hare==0)
|
|
h.tick(10); assert(h.stats().meals==1 and h.stats().births==0)
|
|
local prey2,o2=h.add("hare",fox.object.pos.x+0.4,fox.object.pos.z); prey2.next_ai=999
|
|
fox.hunger=30; h.tick(2); assert(not o2.removed and h.stats().meals==1)
|
|
end)
|
|
test("predator reproduction requires two meals from each mature parent",function()
|
|
local h=fixture(); h.player(); local a=h.add("fox",12); local b=h.add("fox",13)
|
|
for _,e in ipairs({a,b}) do e.age=60;e.breed_at=0;e.hunger=0 end
|
|
h.tick(2); assert(h.stats().births==0)
|
|
a.food=2;b.food=1; h.tick(2); assert(h.stats().births==0)
|
|
b.food=2; h.tick(2); assert(h.stats().births==1 and a.food==0 and b.food==0)
|
|
end)
|
|
test("players and predators trigger safe flight with correct +Z mesh yaw",function()
|
|
local h=fixture(); h.player(10); local _,o=h.add("hare",12)
|
|
h.tick(2); assert(o.pos.x>12 and math.abs(o.yaw+math.pi/2)<0.001)
|
|
assert(o.velocity.x==0 and o.velocity.y==0)
|
|
local j=fixture(); j.player(); local _,p=j.add("deer",12); j.add("fox",10)
|
|
j.tick(2); assert(p.pos.x>12)
|
|
end)
|
|
test("all callback neighbors deny claims, ambiguous overlaps and external protection",function()
|
|
for _,mode in ipairs({"claim","ambiguous","blocked"}) do
|
|
local h=fixture(); h.player(); local e=h.add("hare",12)
|
|
h[mode]=function(p) return p.x>=13 end
|
|
e.feed_at=0; e.dung_at=0; h.tick(2)
|
|
assert(h.forages==0 and h.deposits==0,"callback crossed "..mode)
|
|
local _,o=h.add("fox",14); assert(o.removed)
|
|
end
|
|
local h=fixture(); h.player(); h.hearth.external_protected=function() return false end
|
|
h.api.is_protected=function() error("must prefer external protection over wrapped claim API") end
|
|
local _,o=h.add("hare",12); assert(not o.removed)
|
|
end)
|
|
test("water, cliffs, buildings, unloaded cells and newly claimed sources stop movement",function()
|
|
for _,mode in ipairs({"water","cliff","building","unloaded"}) do
|
|
local h=fixture(); h.player(10); local _,o=h.add("hare",12)
|
|
local original=h.terrain
|
|
if mode=="unloaded" then h.unloaded=function(p) return p.x>=13 end
|
|
else h.terrain=function(p)
|
|
if p.x>=13 then
|
|
if mode=="water" then return "hearth:water_source" end
|
|
if mode=="cliff" then return "air" end
|
|
if mode=="building" and p.y>=4 then return "hearth:wall" end
|
|
end
|
|
return original(p)
|
|
end end
|
|
h.tick(4); assert(o.pos.x<12.5,"unsafe crossing: "..mode)
|
|
end
|
|
local h=fixture(); h.player(); local _,o=h.add("deer",12)
|
|
h.claim=function() return true end; h.tick(1); assert(o.removed and h.forages==0)
|
|
end)
|
|
test("protected prey cannot be consumed through a boundary",function()
|
|
local h=fixture(); h.player(); local fox=h.add("fox",12); local prey,o=h.add("hare",13)
|
|
prey.next_ai=999; fox.feed_at=0; h.claim=function(p) return p.x>=13 end
|
|
-- Predator AI only: the prey's own guard would despawn it, not feed the fox.
|
|
for _=1,2 do h.steps[1](1) end
|
|
assert(not o.removed and h.stats().meals==0 and fox.food==0)
|
|
end)
|
|
test("loaded lifetime, gradual hunger and deactivation release slots without drops",function()
|
|
local h=fixture(); h.player(); local e,o=h.add("hare",12)
|
|
for _,dt in ipairs({-1,0/0,math.huge,"bad"}) do h.defs[e.name].on_step(e,dt) end
|
|
assert(e.age==0 and e.hunger==20)
|
|
h.defs[e.name].on_step(e,100000); assert(e.age==1 and not o.removed)
|
|
e.age=899; h.defs[e.name].on_step(e,1); assert(o.removed and h.stats().total==0)
|
|
local a,b=h.add("deer",12); a.hunger=99
|
|
h.defs[a.name].on_step(a,1); assert(not b.removed)
|
|
h.defs[a.name].on_step(a,1); assert(b.removed and h.stats().total==0)
|
|
end)
|
|
test("shared AI plus recruitment never exceeds 12 actions per second or catches up",function()
|
|
local h=fixture()
|
|
for i=1,8 do h.player(i*120) end
|
|
for i=1,16 do h.add("hare",math.ceil(i/3)*120+12) end
|
|
for i=1,10 do h.add("deer",math.ceil(i/3)*120+12) end
|
|
for _=1,10 do
|
|
local before=h.stats().actions; h.tick(1); assert(h.stats().actions-before<=12)
|
|
end
|
|
local before=h.stats().actions
|
|
h.steps[1](0/0); h.steps[1](math.huge); h.steps[1](-2)
|
|
assert(h.stats().actions==before)
|
|
h.steps[1](100000); assert(h.stats().actions-before<=12)
|
|
assert(h.stats().actions>20,"shared scheduler must make progress")
|
|
end)
|
|
test("earned forage and four finite prey meals fund births rather than free reproduction",function()
|
|
local h=fixture(); h.player(); local a=h.add("hare",12); local b=h.add("hare",13)
|
|
-- Keep the mating pair in one patch; the AI still earns every meal itself.
|
|
for _,e in ipairs({a,b}) do
|
|
e.object.set_pos=function() end
|
|
end
|
|
h.tick(65)
|
|
assert(h.stats().grazed>=6 and h.stats().births==1 and h.stats().hare>=3)
|
|
assert(a.food==0 and b.food==0,"both parents must pay from earned forage")
|
|
local j=fixture(); j.player(); local f=j.add("fox",12); local g=j.add("fox",13)
|
|
for _,e in ipairs({f,g}) do e.age=60;e.breed_at=0;e.next_ai=999 end
|
|
for _,hunter in ipairs({f,f,g,g}) do
|
|
local p=hunter.object.pos
|
|
local prey=j.add("hare",p.x+0.2,p.z); prey.next_ai=999
|
|
hunter.next_ai=0; hunter.feed_at=0; hunter.hunger=30
|
|
j.tick(2); hunter.next_ai=999
|
|
end
|
|
assert(j.stats().meals==4 and j.stats().hare==0 and f.food==2 and g.food==2)
|
|
f.next_ai=0; f.hunger=0; g.hunger=0; j.tick(2)
|
|
assert(j.stats().births==1 and j.stats().fox==3 and f.food==0 and g.food==0)
|
|
end)
|
|
test("wandering selects loaded habitat and refused capped births conserve parent food",function()
|
|
local h=fixture(); h.player(); local e,o=h.add("deer",12)
|
|
h.habitat_at=function(p) return p.z>=1 end
|
|
h.tick(2); assert(o.pos.z>0 and o.moves==1)
|
|
local j=fixture(); j.player(); local a=j.add("hare",12); local b=j.add("hare",13)
|
|
for _=1,4 do j.add("deer",15) end
|
|
for _,parent in ipairs({a,b}) do parent.age=60;parent.food=3;parent.hunger=0;parent.breed_at=0 end
|
|
j.tick(2)
|
|
assert(j.stats().births==0 and j.stats().total==6 and a.food==3 and b.food==3)
|
|
end)
|
|
test("canonical species ecosystem herbs feed on expanded ground but not in dormant winter",function()
|
|
for _,ground in ipairs({"forest_floor","podzol","alluvium","peat","alpine_turf"}) do
|
|
local h=fixture(); local season="Summer"
|
|
h.hearth.climate_at=function() return {season=season} end
|
|
h.hearth.seasons=dofile(target:gsub("[^/\\]+$","seasons.lua"))(h.hearth,h.api)
|
|
h.hearth.diversity=dofile(target:gsub("[^/\\]+$","diversity.lua"))(h.hearth,h.api)
|
|
h.terrain=function(p) return p.y<=3 and "hearth:"..ground or (p.y==4 and "hearth:clover" or "air") end
|
|
h.hearth.succession.habitat=function() return {loaded=true,grass=0,cover=8,edible=8} end
|
|
h.player(); local _,o=h.add("hare",12); assert(not o.removed,ground)
|
|
h.tick(8); assert(h.forages==1,ground)
|
|
season="Winter"
|
|
h.terrain=function(p) return p.y<=3 and "hearth:"..ground or (p.y==4 and "hearth:clover_winter" or "air") end
|
|
h.tick(20); assert(h.forages==1,"dormant clover grazed on "..ground)
|
|
end
|
|
end)
|
|
test("winter canonical grass cannot be admitted as a free meal or breeding habitat",function()
|
|
local h=fixture(); h.hearth.climate_at=function() return {season="Winter"} end
|
|
h.hearth.seasons=dofile(target:gsub("[^/\\]+$","seasons.lua"))(h.hearth,h.api)
|
|
h.player(); local _,o=h.add("hare",12); assert(o.removed)
|
|
h.tick(60); assert(h.forages==0 and h.stats().total==0)
|
|
end)
|
|
print("PASS "..passed.." bounded wildlife tests")
|