Module:Sandbox/Vergir: Difference between revisionsGive feedback
Jump to navigation
Jump to search
Blank after experiment (with help from vergir-bot LLM) Tags: Blanking Manual revert |
Add test harness for proposed GameData prop-matching changes (with help from vergir-bot LLM) |
||
| Line 1: | Line 1: | ||
-- Test harness for the proposed Module:GameData prop-matching change. | |||
-- Renders with {{#invoke:Sandbox/Vergir|test}} | |||
-- | |||
-- Holds a copy of both the current and the proposed matching code, runs the | |||
-- proposed one against Data:AbilityData.json, and checks that every existing | |||
-- list in Module:AbilityTable/Lists still selects exactly the same abilities. | |||
local p = {} | |||
local Lists = require("Module:AbilityTable/Lists") | |||
-- ============================================================ | |||
-- Current implementation (unchanged, still used for plain targets) | |||
-- ============================================================ | |||
local function record_matches_prop(record, prop) | |||
for k, v in pairs(record) do | |||
if k == prop then | |||
if type(v) == "table" then | |||
if v["Value"] ~= nil then | |||
v = v["Value"] | |||
else | |||
for _ in pairs(v) do return true end | |||
return false | |||
end | |||
end | |||
if v ~= nil and v ~= 0 and v ~= "" and v ~= "0" and v ~= "0m" then | |||
return true | |||
end | |||
end | |||
if type(v) == "table" and k ~= "DisabledStateMask" then | |||
if record_matches_prop(v, prop) then return true end | |||
elseif type(v) == "string" then | |||
if v == prop then return true end | |||
end | |||
end | |||
return false | |||
end | |||
local function entity_matches_old(record, properties) | |||
for _, prop in ipairs(properties) do | |||
if record_matches_prop(record, prop) then return true end | |||
end | |||
return false | |||
end | |||
-- ============================================================ | |||
-- Proposed additions | |||
-- ============================================================ | |||
local function value_is_meaningful(v) | |||
if type(v) == "table" then | |||
if v["Value"] ~= nil then | |||
v = v["Value"] | |||
else | |||
for _ in pairs(v) do return true end | |||
return false | |||
end | |||
end | |||
return v ~= nil and v ~= 0 and v ~= "" and v ~= "0" and v ~= "0m" | |||
end | |||
local function value_equals(v, expected) | |||
if type(v) == "table" then v = v["Value"] end | |||
if v == nil or type(v) == "table" then return false end | |||
return mw.ustring.lower(tostring(v)) == expected | |||
end | |||
local function follow_path(node, segments, index, value) | |||
if index > #segments then | |||
if value ~= nil then return value_equals(node, value) end | |||
return value_is_meaningful(node) | |||
end | |||
if type(node) ~= "table" then return false end | |||
local segment = segments[index] | |||
local next_node = node[segment] | |||
if next_node == nil then | |||
local num = tonumber(segment) | |||
if num then next_node = node[num] end | |||
end | |||
if next_node ~= nil and follow_path(next_node, segments, index + 1, value) then | |||
return true | |||
end | |||
for _, element in ipairs(node) do | |||
if type(element) == "table" and follow_path(element, segments, index, value) then | |||
return true | |||
end | |||
end | |||
return false | |||
end | |||
local function record_matches_path(record, segments, value) | |||
for k, v in pairs(record) do | |||
if k == segments[1] and follow_path(v, segments, 2, value) then | |||
return true | |||
end | |||
if type(v) == "table" and k ~= "DisabledStateMask" then | |||
if record_matches_path(v, segments, value) then return true end | |||
end | |||
end | |||
return false | |||
end | |||
local function parse_target(prop) | |||
if type(prop) ~= "string" then return nil end | |||
local keys, value = prop, nil | |||
local colon = prop:find(":", 1, true) | |||
if colon then | |||
keys = mw.text.trim(prop:sub(1, colon - 1)) | |||
value = mw.text.trim(prop:sub(colon + 1)) | |||
if keys == "" or value == "" then return nil end | |||
value = mw.ustring.lower(value) | |||
end | |||
local segments = {} | |||
for segment in keys:gmatch("[^%.]+") do table.insert(segments, segment) end | |||
if #segments == 0 then return nil end | |||
if #segments == 1 and value == nil then return nil end | |||
return segments, value | |||
end | |||
local function entity_matches_new(record, properties) | |||
for _, prop in ipairs(properties) do | |||
local segments, value = parse_target(prop) | |||
if segments and record_matches_path(record, segments, value) then | |||
return true | |||
elseif record_matches_prop(record, prop) then | |||
return true | |||
end | |||
end | |||
return false | |||
end | |||
-- ============================================================ | |||
-- Cases | |||
-- ============================================================ | |||
local cases = { | |||
{ target = "Type:spirit", expected = 219, note = "key found at any depth" }, | |||
{ target = "Type:SPIRIT", expected = 219, note = "case-insensitive value" }, | |||
{ target = "Damage:spirit", expected = 0, note = "nested value must not match" }, | |||
{ target = "Damage:90", expected = 5, note = ".Value unwrapped" }, | |||
{ target = "AbilityCharges:2", expected = 18, note = "number compared as string" }, | |||
{ target = "AbilityCooldown.Scale.Type", expected = 300, note = "path, no value" }, | |||
{ target = "AbilityCooldown.Scale.Type:cooldown",expected = 299, note = "Parry scales on parry_cd" }, | |||
{ target = "AbilityCooldown.Scale.Type:spirit", expected = 0, note = "wrong value on a valid path" }, | |||
{ target = "Radius.Scale.Value:1", expected = 67, note = "explicit .Value segment" }, | |||
{ target = "Upgrades.Damage", expected = 63, note = "steps through array entries" }, | |||
{ target = "Upgrades.1.AbilityCooldown", expected = 79, note = "numeric index segment" }, | |||
{ target = "38.1 50.8", expected = 1, note = "dotted value falls back" }, | |||
} | |||
local function select_names(data, properties, matcher) | |||
local names = {} | |||
for key, record in pairs(data) do | |||
if type(record) == "table" and matcher(record, properties) then | |||
table.insert(names, record["Name"] or key) | |||
end | |||
end | |||
table.sort(names) | |||
return names | |||
end | |||
local function ok_cell(pass) | |||
if pass then | |||
return 'style="background:#d5f5d5" | pass' | |||
end | |||
return 'style="background:#f5d5d5" | <b>FAIL</b>' | |||
end | |||
function p.test(frame) | |||
local data = mw.loadJsonData("Data:AbilityData.json") | |||
local out = {} | |||
local failures = 0 | |||
-- --- new target forms --- | |||
table.insert(out, "=== Proposed target forms ===") | |||
table.insert(out, '{| class="wikitable"') | |||
table.insert(out, "! Target !! Expected !! Actual !! Result !! Sample !! Note") | |||
for _, case in ipairs(cases) do | |||
local names = select_names(data, { case.target }, entity_matches_new) | |||
local pass = (#names == case.expected) | |||
if not pass then failures = failures + 1 end | |||
local sample = {} | |||
for i = 1, math.min(4, #names) do table.insert(sample, names[i]) end | |||
local sample_text = #sample > 0 and table.concat(sample, ", ") or "—" | |||
if #names > #sample then sample_text = sample_text .. ", …" end | |||
table.insert(out, "|-") | |||
table.insert(out, "| <code>" .. mw.text.nowiki(case.target) .. "</code>" | |||
.. " || " .. case.expected | |||
.. " || " .. #names | |||
.. " || " .. ok_cell(pass) | |||
.. " || " .. sample_text | |||
.. " || " .. case.note) | |||
end | |||
table.insert(out, "|}") | |||
-- --- regression across every existing list --- | |||
table.insert(out, "=== Existing lists: old vs new ===") | |||
table.insert(out, '{| class="wikitable sortable"') | |||
table.insert(out, "! List !! Old !! New !! Result") | |||
local list_names = {} | |||
for name in pairs(Lists.lists) do table.insert(list_names, name) end | |||
table.sort(list_names) | |||
for _, name in ipairs(list_names) do | |||
local props = Lists.lists[name] | |||
local old_names = select_names(data, props, entity_matches_old) | |||
local new_names = select_names(data, props, entity_matches_new) | |||
local same = (#old_names == #new_names) | |||
if same then | |||
for i = 1, #old_names do | |||
if old_names[i] ~= new_names[i] then same = false break end | |||
end | |||
end | |||
if not same then failures = failures + 1 end | |||
table.insert(out, "|-") | |||
table.insert(out, "| " .. name | |||
.. " || " .. #old_names | |||
.. " || " .. #new_names | |||
.. " || " .. ok_cell(same)) | |||
end | |||
table.insert(out, "|}") | |||
local summary | |||
if failures == 0 then | |||
summary = "'''All checks passed.'''" | |||
else | |||
summary = "'''" .. failures .. " check(s) failed.'''" | |||
end | |||
return summary .. "\n" .. table.concat(out, "\n") | |||
end | |||
return p | |||
Revision as of 00:01, 10 August 2026
Documentation for this module may be created at Module:Sandbox/Vergir/doc
-- Test harness for the proposed Module:GameData prop-matching change.
-- Renders with {{#invoke:Sandbox/Vergir|test}}
--
-- Holds a copy of both the current and the proposed matching code, runs the
-- proposed one against Data:AbilityData.json, and checks that every existing
-- list in Module:AbilityTable/Lists still selects exactly the same abilities.
local p = {}
local Lists = require("Module:AbilityTable/Lists")
-- ============================================================
-- Current implementation (unchanged, still used for plain targets)
-- ============================================================
local function record_matches_prop(record, prop)
for k, v in pairs(record) do
if k == prop then
if type(v) == "table" then
if v["Value"] ~= nil then
v = v["Value"]
else
for _ in pairs(v) do return true end
return false
end
end
if v ~= nil and v ~= 0 and v ~= "" and v ~= "0" and v ~= "0m" then
return true
end
end
if type(v) == "table" and k ~= "DisabledStateMask" then
if record_matches_prop(v, prop) then return true end
elseif type(v) == "string" then
if v == prop then return true end
end
end
return false
end
local function entity_matches_old(record, properties)
for _, prop in ipairs(properties) do
if record_matches_prop(record, prop) then return true end
end
return false
end
-- ============================================================
-- Proposed additions
-- ============================================================
local function value_is_meaningful(v)
if type(v) == "table" then
if v["Value"] ~= nil then
v = v["Value"]
else
for _ in pairs(v) do return true end
return false
end
end
return v ~= nil and v ~= 0 and v ~= "" and v ~= "0" and v ~= "0m"
end
local function value_equals(v, expected)
if type(v) == "table" then v = v["Value"] end
if v == nil or type(v) == "table" then return false end
return mw.ustring.lower(tostring(v)) == expected
end
local function follow_path(node, segments, index, value)
if index > #segments then
if value ~= nil then return value_equals(node, value) end
return value_is_meaningful(node)
end
if type(node) ~= "table" then return false end
local segment = segments[index]
local next_node = node[segment]
if next_node == nil then
local num = tonumber(segment)
if num then next_node = node[num] end
end
if next_node ~= nil and follow_path(next_node, segments, index + 1, value) then
return true
end
for _, element in ipairs(node) do
if type(element) == "table" and follow_path(element, segments, index, value) then
return true
end
end
return false
end
local function record_matches_path(record, segments, value)
for k, v in pairs(record) do
if k == segments[1] and follow_path(v, segments, 2, value) then
return true
end
if type(v) == "table" and k ~= "DisabledStateMask" then
if record_matches_path(v, segments, value) then return true end
end
end
return false
end
local function parse_target(prop)
if type(prop) ~= "string" then return nil end
local keys, value = prop, nil
local colon = prop:find(":", 1, true)
if colon then
keys = mw.text.trim(prop:sub(1, colon - 1))
value = mw.text.trim(prop:sub(colon + 1))
if keys == "" or value == "" then return nil end
value = mw.ustring.lower(value)
end
local segments = {}
for segment in keys:gmatch("[^%.]+") do table.insert(segments, segment) end
if #segments == 0 then return nil end
if #segments == 1 and value == nil then return nil end
return segments, value
end
local function entity_matches_new(record, properties)
for _, prop in ipairs(properties) do
local segments, value = parse_target(prop)
if segments and record_matches_path(record, segments, value) then
return true
elseif record_matches_prop(record, prop) then
return true
end
end
return false
end
-- ============================================================
-- Cases
-- ============================================================
local cases = {
{ target = "Type:spirit", expected = 219, note = "key found at any depth" },
{ target = "Type:SPIRIT", expected = 219, note = "case-insensitive value" },
{ target = "Damage:spirit", expected = 0, note = "nested value must not match" },
{ target = "Damage:90", expected = 5, note = ".Value unwrapped" },
{ target = "AbilityCharges:2", expected = 18, note = "number compared as string" },
{ target = "AbilityCooldown.Scale.Type", expected = 300, note = "path, no value" },
{ target = "AbilityCooldown.Scale.Type:cooldown",expected = 299, note = "Parry scales on parry_cd" },
{ target = "AbilityCooldown.Scale.Type:spirit", expected = 0, note = "wrong value on a valid path" },
{ target = "Radius.Scale.Value:1", expected = 67, note = "explicit .Value segment" },
{ target = "Upgrades.Damage", expected = 63, note = "steps through array entries" },
{ target = "Upgrades.1.AbilityCooldown", expected = 79, note = "numeric index segment" },
{ target = "38.1 50.8", expected = 1, note = "dotted value falls back" },
}
local function select_names(data, properties, matcher)
local names = {}
for key, record in pairs(data) do
if type(record) == "table" and matcher(record, properties) then
table.insert(names, record["Name"] or key)
end
end
table.sort(names)
return names
end
local function ok_cell(pass)
if pass then
return 'style="background:#d5f5d5" | pass'
end
return 'style="background:#f5d5d5" | <b>FAIL</b>'
end
function p.test(frame)
local data = mw.loadJsonData("Data:AbilityData.json")
local out = {}
local failures = 0
-- --- new target forms ---
table.insert(out, "=== Proposed target forms ===")
table.insert(out, '{| class="wikitable"')
table.insert(out, "! Target !! Expected !! Actual !! Result !! Sample !! Note")
for _, case in ipairs(cases) do
local names = select_names(data, { case.target }, entity_matches_new)
local pass = (#names == case.expected)
if not pass then failures = failures + 1 end
local sample = {}
for i = 1, math.min(4, #names) do table.insert(sample, names[i]) end
local sample_text = #sample > 0 and table.concat(sample, ", ") or "—"
if #names > #sample then sample_text = sample_text .. ", …" end
table.insert(out, "|-")
table.insert(out, "| <code>" .. mw.text.nowiki(case.target) .. "</code>"
.. " || " .. case.expected
.. " || " .. #names
.. " || " .. ok_cell(pass)
.. " || " .. sample_text
.. " || " .. case.note)
end
table.insert(out, "|}")
-- --- regression across every existing list ---
table.insert(out, "=== Existing lists: old vs new ===")
table.insert(out, '{| class="wikitable sortable"')
table.insert(out, "! List !! Old !! New !! Result")
local list_names = {}
for name in pairs(Lists.lists) do table.insert(list_names, name) end
table.sort(list_names)
for _, name in ipairs(list_names) do
local props = Lists.lists[name]
local old_names = select_names(data, props, entity_matches_old)
local new_names = select_names(data, props, entity_matches_new)
local same = (#old_names == #new_names)
if same then
for i = 1, #old_names do
if old_names[i] ~= new_names[i] then same = false break end
end
end
if not same then failures = failures + 1 end
table.insert(out, "|-")
table.insert(out, "| " .. name
.. " || " .. #old_names
.. " || " .. #new_names
.. " || " .. ok_cell(same))
end
table.insert(out, "|}")
local summary
if failures == 0 then
summary = "'''All checks passed.'''"
else
summary = "'''" .. failures .. " check(s) failed.'''"
end
return summary .. "\n" .. table.concat(out, "\n")
end
return p