From 3ae9799d4dac582acc4332971dd0406b5d28c9bb Mon Sep 17 00:00:00 2001 From: Calic Date: Sat, 13 Jun 2026 17:33:18 +0200 Subject: [PATCH] =?UTF-8?q?initial:=20inventory-list-test=20v0.1.0=20?= =?UTF-8?q?=E2=80=94=2012=20assertions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Covers engine spike (multi-slot children), add/remove/contains/count round-trips, Loud-Error paths (no container block, no stack_mode, stack_mode=stack), renderable-tag toggle via set_tag, and slot-naming reconstruction from get_children (simulated post-reload state). --- LICENSE | 24 ++++ README.md | 26 +++++ init.lua | 304 ++++++++++++++++++++++++++++++++++++++++++++++++++ manifest.core | 6 + manifest.lib | 10 ++ 5 files changed, 370 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 init.lua create mode 100644 manifest.core create mode 100644 manifest.lib diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..81c9d65 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2026 Calic. All rights reserved. + +This software is part of the Sporel platform — **Tier 1 (Official / +Proprietary)** content per the Three-Tier Licensing Model documented in +`meta/docs/archive/design/vision.md §Licensing Model` (current source; +migration to `meta/docs/architecture/licensing-model.md` pending). + +⚠ **WIP — Legal review required before public launch.** The terms below +reflect design intent only; the formalized license framework will be +finalized through legal counsel before the first public release. Until +then, this notice serves as a placeholder defending the platform owner's +rights against unintentional re-licensing. + +No license is granted to copy, modify, distribute, sublicense, or otherwise +use this software in any form without prior written permission from the +copyright holder. + +References: +- Tier 1 (this file): all rights reserved, proprietary, sold/distributed + via official channels (Steam, etc.) +- Tier 2 (Semi-Commercial Co-Development): bilateral contracts, revenue- + share — see vision.md §Licensing Model +- Tier 3 (Community Content): CC BY-NC-SA 4.0 + asymmetric CLA — applies + to community-uploaded libs/modules/assets, not this repo diff --git a/README.md b/README.md new file mode 100644 index 0000000..33efa0b --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# lib-core.inventory-list-test + +Test suite for `lib-core.inventory-list` v0.1.0. + +**12 assertions:** 1 engine spike + 11 spec assertions. + +| # | Description | +|---|---| +| spike | `engine.entity` supports 3 children on distinct slot names | +| 1 | `add` → `contains` true | +| 2 | after `add`: `count == 1`, `contents` contains item | +| 3 | two `add`s → `count == 2`, both in `contents`, distinct slot names | +| 4 | `remove` → `contains` false, `count == 0` | +| 5 | `add` to entity without container block → Loud-Error | +| 6 | `add` entity without `stack_mode` → Loud-Error | +| 7 | `add` item with `stack_mode="stack"` → Loud-Error | +| 8 | `add` to non-container-template entity → Loud-Error | +| 9 | `add` removes item from renderable index | +| 10 | `remove` restores item to renderable index | +| 11 | slot-naming reconstruction from `get_children` (simulated reload) | + +**Run:** +```bash +SPOREL_LIBS_DIR=~/Projects/Sporel/sporel-libs SPOREL_CI=1 SPOREL_LOG_STDERR=1 \ + ./build/Sporel.exe --test-libs=lib-core.inventory-list-test 2>&1 | tail -30 +``` diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..5314bb2 --- /dev/null +++ b/init.lua @@ -0,0 +1,304 @@ +-- 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 = {} + +-- ---------------------------------------------------------------- +-- 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. 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 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 it == sword3 then found3_s = true end + if 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 to container with kind != "list" → Loud-Error + -- (Template-level catch already fires, but runtime check is the + -- double-belt tested here via a raw entity.create() bypass.) + -- We test the double-belt by attempting to add to a composition + -- entity whose template has a deferred constraint — this cannot + -- be constructed via define_template (it loud-errors), so we test + -- the "no container block" path which is the closest available + -- runtime-only path not gated by template-load. + -- The template-level gate for kind!="list" is covered in + -- composition-test (assertion 6 there). Here we verify inventory + -- itself also loud-errors on a non-container-template entity. + -- ================================================================ + + local plain8 = composition.create{ template = "inv_test.plain_entity" } + local sword8 = composition.create{ template = "inv_test.sword" } + local ok8 = pcall(inventory.add, plain8, sword8) + T.assert(not ok8, + "8. add to entity with no container block (non-container) → 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 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 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 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. children") + + local contents11 = inventory.contents(bag11) + T.assert(#contents11 == 2 and + contents11[1] == item11a and + 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 starts renderable; set_tag will be called by add. + -- But item11a and item11b were attached directly (bypass add), + -- so we must declare stack_mode; they have it via template. + -- Detach item11c from any parent first (it has none here). + 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 diff --git a/manifest.core b/manifest.core new file mode 100644 index 0000000..777f4ef --- /dev/null +++ b/manifest.core @@ -0,0 +1,6 @@ +{ + "id": "lib-core.inventory-list-test.core", + "kind": "lib", + "version": "0.1.0", + "license": "Proprietary" +} diff --git a/manifest.lib b/manifest.lib new file mode 100644 index 0000000..6dea8fe --- /dev/null +++ b/manifest.lib @@ -0,0 +1,10 @@ +{ + "id": "lib-core.inventory-list-test", + "role": "test", + "version": "0.1.0", + "api_min": "0.1", + "deps": [ + {"id": "lib-core.inventory-list", "version": "0.1.0"}, + {"id": "lib-core.composition", "version": "0.2.0"} + ] +}