Module:Sandbox/VergirGive feedback
Jump to navigation
Jump to search
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}}
-- Diagnostics with {{#invoke:Sandbox/Vergir|diag|TARGET|TARGET|...}}
--
-- 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 = 215, note = "key found at any depth" },
{ target = "Type:SPIRIT", expected = 215, note = "case-insensitive value" },
{ target = "Damage:spirit", expected = 0, note = "nested value must not match" },
{ target = "Damage:90", expected = 6, note = ".Value unwrapped" },
{ target = "AbilityCharges:2", expected = 19, 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
-- Lists every ability matched by each given target, plus the dataset size.
-- Used to tell a logic fault apart from the dataset having moved on.
function p.diag(frame)
local data = mw.loadJsonData("Data:AbilityData.json")
local total = 0
for _ in pairs(data) do total = total + 1 end
local out = { "Records in Data:AbilityData.json: '''" .. total .. "'''" }
for i = 1, 5 do
local target = frame.args[i]
if target and target ~= "" then
local names = select_names(data, { target }, entity_matches_new)
table.insert(out, "* <code>" .. mw.text.nowiki(target) .. "</code> ("
.. #names .. "): " .. table.concat(names, ", "))
end
end
return table.concat(out, "\n")
end
return p