Module:Sandbox/Vergir

From The Deadlock Wiki
Revision as of 00:02, 10 August 2026 by Vergir (talk | contribs) (Add diag entry point for count discrepancies (with help from vergir-bot LLM))
Jump to navigation Jump to search

Documentation for this module may be created at Module:Sandbox/Vergir/doc

-- (diag entry point is defined at the bottom of this page)
-- 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