get_children() returns fresh userdata wrappers on each call; without an __eq metamethod, Lua pointer equality fails between two wrappers of the same engine entity. Use composition.reg_id (a stable property on all composition entities) for same_entity() comparisons in assertions 2b, 3b, 9, 10, 11b.
311 lines
12 KiB
Lua
311 lines
12 KiB
Lua
-- lib-core.inventory-list-test — v0.1.0
|
|
-- 12 assertions: 1 engine spike + 11 spec assertions (B.2 test-spec).
|
|
-- Pattern follows P.2.4 Test-Module-Pattern; TAP output via engine.test.*
|
|
|
|
local composition = require("lib-core.composition")
|
|
local inventory = require("lib-core.inventory-list")
|
|
local T = engine.test
|
|
|
|
local M = {}
|
|
|
|
-- entity.get_children() returns fresh userdata wrappers on each call;
|
|
-- there is no __eq on entity userdata, so == is Lua-pointer equality
|
|
-- (always false for different wrappers of the same engine entity).
|
|
-- Compare by composition.reg_id (stable property on all composition
|
|
-- entities) instead.
|
|
local function same_entity(a, b)
|
|
local ra = a and a:get_property("composition.reg_id")
|
|
local rb = b and b:get_property("composition.reg_id")
|
|
if ra and ra ~= 0 and rb and rb ~= 0 then
|
|
return ra == rb
|
|
end
|
|
-- Fallback for raw entities: pointer equality (may be unreliable).
|
|
return a == b
|
|
end
|
|
|
|
-- ----------------------------------------------------------------
|
|
-- Template definitions (shared across tests)
|
|
-- All template names are unique within this test run.
|
|
-- ----------------------------------------------------------------
|
|
|
|
-- Container template: kind="list", no constraints.
|
|
composition.define_template{
|
|
id = "inv_test.container",
|
|
properties = {
|
|
sprite_path = "sprites/bag.png",
|
|
position = {x = 0, y = 0},
|
|
},
|
|
tags = {"renderable"},
|
|
container = { kind = "list" },
|
|
}
|
|
|
|
-- Item template: stack_mode="individual", has "item" tag + "renderable" tag.
|
|
composition.define_template{
|
|
id = "inv_test.sword",
|
|
properties = {
|
|
name = "Iron Sword",
|
|
stack_mode = "individual",
|
|
sprite_path = "sprites/sword.png",
|
|
position = {x = 0, y = 0},
|
|
},
|
|
tags = {"renderable", "item"},
|
|
}
|
|
|
|
-- A second item template for multi-item tests.
|
|
composition.define_template{
|
|
id = "inv_test.potion",
|
|
properties = {
|
|
name = "Health Potion",
|
|
stack_mode = "individual",
|
|
sprite_path = "sprites/potion.png",
|
|
position = {x = 0, y = 0},
|
|
},
|
|
tags = {"renderable", "item"},
|
|
}
|
|
|
|
-- Non-item template (no stack_mode property).
|
|
composition.define_template{
|
|
id = "inv_test.sign",
|
|
properties = {
|
|
text = "a sign",
|
|
sprite_path = "sprites/sign.png",
|
|
position = {x = 0, y = 0},
|
|
},
|
|
tags = {"renderable"},
|
|
}
|
|
|
|
-- Stack-item template (stack_mode="stack" — not supported in v0.1).
|
|
composition.define_template{
|
|
id = "inv_test.gold_coin",
|
|
properties = {
|
|
name = "Gold Coin",
|
|
stack_mode = "stack",
|
|
sprite_path = "sprites/coin.png",
|
|
position = {x = 0, y = 0},
|
|
},
|
|
tags = {"renderable", "item"},
|
|
}
|
|
|
|
-- Non-container template (no container block).
|
|
composition.define_template{
|
|
id = "inv_test.plain_entity",
|
|
properties = {
|
|
name = "plain",
|
|
},
|
|
}
|
|
|
|
function M.run_tests(ctx)
|
|
-- ================================================================
|
|
-- Spike: engine.entity supports multiple children on distinct slots
|
|
-- (locks the invariant that synthetic item.<n> naming is safe)
|
|
-- ================================================================
|
|
|
|
local spike_parent = entity.create()
|
|
local spike_a = entity.create()
|
|
local spike_b = entity.create()
|
|
local spike_c = entity.create()
|
|
entity.attach(spike_parent, "a", spike_a)
|
|
entity.attach(spike_parent, "b", spike_b)
|
|
entity.attach(spike_parent, "c", spike_c)
|
|
local spike_children = spike_parent:get_children()
|
|
local spike_n = 0
|
|
for _ in pairs(spike_children) do spike_n = spike_n + 1 end
|
|
T.assert(spike_n == 3 and
|
|
spike_children.a ~= nil and
|
|
spike_children.b ~= nil and
|
|
spike_children.c ~= nil,
|
|
"spike: engine.entity supports 3 children on distinct slot names")
|
|
|
|
-- ================================================================
|
|
-- Assertion 1: add(container, item) → contains returns true
|
|
-- ================================================================
|
|
|
|
local bag1 = composition.create{ template = "inv_test.container" }
|
|
local sword1 = composition.create{ template = "inv_test.sword" }
|
|
inventory.add(bag1, sword1)
|
|
T.assert(inventory.contains(bag1, sword1),
|
|
"1. add(container, item) → contains(container, item) is true")
|
|
|
|
-- ================================================================
|
|
-- Assertion 2: after add, count == 1 and contents contains item
|
|
-- ================================================================
|
|
|
|
T.assert(inventory.count(bag1) == 1,
|
|
"2a. after add: count(container) == 1")
|
|
local contents2 = inventory.contents(bag1)
|
|
local found_sword1 = false
|
|
for _, it in ipairs(contents2) do
|
|
if same_entity(it, sword1) then found_sword1 = true; break end
|
|
end
|
|
T.assert(found_sword1,
|
|
"2b. after add: contents(container) contains item")
|
|
|
|
-- ================================================================
|
|
-- Assertion 3: two adds of different items → count == 2,
|
|
-- both in contents, different engine slot names
|
|
-- ================================================================
|
|
|
|
local bag3 = composition.create{ template = "inv_test.container" }
|
|
local sword3 = composition.create{ template = "inv_test.sword" }
|
|
local potion3 = composition.create{ template = "inv_test.potion" }
|
|
inventory.add(bag3, sword3)
|
|
inventory.add(bag3, potion3)
|
|
|
|
T.assert(inventory.count(bag3) == 2,
|
|
"3a. two adds → count == 2")
|
|
|
|
local contents3 = inventory.contents(bag3)
|
|
local found3_s = false
|
|
local found3_p = false
|
|
for _, it in ipairs(contents3) do
|
|
if same_entity(it, sword3) then found3_s = true end
|
|
if same_entity(it, potion3) then found3_p = true end
|
|
end
|
|
T.assert(found3_s and found3_p,
|
|
"3b. two adds → both items in contents")
|
|
|
|
-- Verify no slot collision (different slot names).
|
|
local children3 = bag3:get_children()
|
|
local slots3 = {}
|
|
for slot, _ in pairs(children3) do table.insert(slots3, slot) end
|
|
T.assert(#slots3 == 2 and slots3[1] ~= slots3[2],
|
|
"3c. two adds → different engine slot names (no collision)")
|
|
|
|
-- ================================================================
|
|
-- Assertion 4: remove → contains false, count 0
|
|
-- ================================================================
|
|
|
|
local bag4 = composition.create{ template = "inv_test.container" }
|
|
local sword4 = composition.create{ template = "inv_test.sword" }
|
|
inventory.add(bag4, sword4)
|
|
inventory.remove(bag4, sword4)
|
|
T.assert(not inventory.contains(bag4, sword4),
|
|
"4a. remove → contains(container, item) is false")
|
|
T.assert(inventory.count(bag4) == 0,
|
|
"4b. remove → count == 0")
|
|
|
|
-- ================================================================
|
|
-- Assertion 5: add to entity without container-block → Loud-Error
|
|
-- ================================================================
|
|
|
|
local plain5 = composition.create{ template = "inv_test.plain_entity" }
|
|
local sword5 = composition.create{ template = "inv_test.sword" }
|
|
local ok5 = pcall(inventory.add, plain5, sword5)
|
|
T.assert(not ok5,
|
|
"5. add to entity without container block → Loud-Error")
|
|
|
|
-- ================================================================
|
|
-- Assertion 6: add of entity without stack_mode → Loud-Error
|
|
-- ================================================================
|
|
|
|
local bag6 = composition.create{ template = "inv_test.container" }
|
|
local sign6 = composition.create{ template = "inv_test.sign" }
|
|
local ok6 = pcall(inventory.add, bag6, sign6)
|
|
T.assert(not ok6,
|
|
"6. add entity without stack_mode (not an item) → Loud-Error")
|
|
|
|
-- ================================================================
|
|
-- Assertion 7: add item with stack_mode="stack" → Loud-Error
|
|
-- ================================================================
|
|
|
|
local bag7 = composition.create{ template = "inv_test.container" }
|
|
local coin7 = composition.create{ template = "inv_test.gold_coin" }
|
|
local ok7 = pcall(inventory.add, bag7, coin7)
|
|
T.assert(not ok7,
|
|
"7. add item with stack_mode='stack' → Loud-Error")
|
|
|
|
-- ================================================================
|
|
-- Assertion 8: add on non-composition entity (no reg_id) → Loud-Error
|
|
-- Uses raw entity.create() (not composition.create) so the entity
|
|
-- has no composition.reg_id. composition.get_container returns nil,
|
|
-- which propagates through validate_container as a Loud-Error.
|
|
-- Distinct from assertion 5 (composition entity, no container block).
|
|
-- ================================================================
|
|
|
|
local raw8 = entity.create()
|
|
local sword8 = composition.create{ template = "inv_test.sword" }
|
|
local ok8 = pcall(inventory.add, raw8, sword8)
|
|
T.assert(not ok8,
|
|
"8. add on non-composition entity (no reg_id) → Loud-Error")
|
|
|
|
-- ================================================================
|
|
-- Assertion 9: add sets renderable tag off on item
|
|
-- ================================================================
|
|
|
|
local bag9 = composition.create{ template = "inv_test.container" }
|
|
local sword9 = composition.create{ template = "inv_test.sword" }
|
|
|
|
-- Confirm sword is renderable before add.
|
|
local renderable_before9 = composition.list_by_tag("renderable")
|
|
local in_renderable_before9 = false
|
|
for _, e in ipairs(renderable_before9) do
|
|
if same_entity(e, sword9) then in_renderable_before9 = true; break end
|
|
end
|
|
T.assert(in_renderable_before9,
|
|
"9a. item is in renderable index before add")
|
|
|
|
inventory.add(bag9, sword9)
|
|
|
|
local renderable_after9 = composition.list_by_tag("renderable")
|
|
local in_renderable_after9 = false
|
|
for _, e in ipairs(renderable_after9) do
|
|
if same_entity(e, sword9) then in_renderable_after9 = true; break end
|
|
end
|
|
T.assert(not in_renderable_after9,
|
|
"9b. add: item removed from renderable index via composition.set_tag")
|
|
|
|
-- ================================================================
|
|
-- Assertion 10: remove sets renderable tag back on item
|
|
-- ================================================================
|
|
|
|
inventory.remove(bag9, sword9)
|
|
|
|
local renderable_after_remove10 = composition.list_by_tag("renderable")
|
|
local restored10 = false
|
|
for _, e in ipairs(renderable_after_remove10) do
|
|
if same_entity(e, sword9) then restored10 = true; break end
|
|
end
|
|
T.assert(restored10,
|
|
"10. remove: item restored to renderable index via composition.set_tag")
|
|
|
|
-- ================================================================
|
|
-- Assertion 11: slot-naming reconstruction
|
|
-- A container already has 2 children from a prior session (simulated
|
|
-- by directly attaching raw entities under item.3 and item.7 slots),
|
|
-- then inventory.contents and inventory.count derive correct results
|
|
-- from get_children, and next add lands on item.8 (max+1).
|
|
-- ================================================================
|
|
|
|
local bag11 = composition.create{ template = "inv_test.container" }
|
|
|
|
-- Simulate pre-existing items (as if loaded from persistence).
|
|
-- We create composition items so set_tag works, then directly attach
|
|
-- them under specific slot names to mimic a reloaded state.
|
|
local item11a = composition.create{ template = "inv_test.sword" }
|
|
local item11b = composition.create{ template = "inv_test.potion" }
|
|
|
|
-- Attach directly at known slot indices (simulating a reload state).
|
|
entity.attach(bag11, "item.3", item11a)
|
|
entity.attach(bag11, "item.7", item11b)
|
|
|
|
-- contents/count must work purely from get_children.
|
|
T.assert(inventory.count(bag11) == 2,
|
|
"11a. reconstruction: count derives 2 from existing item.<n> children")
|
|
|
|
local contents11 = inventory.contents(bag11)
|
|
T.assert(#contents11 == 2 and
|
|
same_entity(contents11[1], item11a) and
|
|
same_entity(contents11[2], item11b),
|
|
"11b. reconstruction: contents ordered by slot-n, items correct")
|
|
|
|
-- Next add must land on item.8 (max=7, so 7+1=8), not item.1.
|
|
local item11c = composition.create{ template = "inv_test.sword" }
|
|
-- item11c has no prior parent; set_tag is invoked by add.
|
|
inventory.add(bag11, item11c)
|
|
local children11 = bag11:get_children()
|
|
T.assert(children11["item.8"] ~= nil,
|
|
"11c. reconstruction: next synthetic slot after item.7 is item.8")
|
|
end
|
|
|
|
return M
|