Jump to content

Module:Build bracket/Logic

Permanently protected module
From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Pbrks (talk | contribs) at 22:05, 16 August 2025. The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
local Logic = {}

-- =======================
-- 1) STDLIB ALIASES
-- =======================
local m_max  = math.max
local m_ceil = math.ceil
local s_match = string.match
local s_find  = string.find

-- =======================
-- 2) MODULE UPVALUES
-- =======================
local state, config, Helpers, StateChecks
local isempty, notempty, teamLegs

local function bind(_state, _config, _Helpers, _StateChecks)
    state, config, Helpers, StateChecks = _state, _config, _Helpers, _StateChecks
    isempty   = _Helpers and _Helpers.isempty   or function(v) return v == nil or v == "" end
    notempty  = _Helpers and _Helpers.notempty  or function(v) return v ~= nil and v ~= "" end
    teamLegs  = _StateChecks and _StateChecks.teamLegs
end

-- =======================
-- 3) SMALL UTILITIES
-- =======================
-- Leading integer from a value; supports string or number; nil if none.
local function numlead(v)
    if v == nil then return nil end
    if type(v) == "number" then return v end
    local s = tostring(v)
    if s == "" then return nil end
    local n = s_match(s, "^%d+")
    return n and tonumber(n) or nil
end

-- ===========================
-- 4) MATCH GROUPING (PER RD)
-- ===========================
local function _matchGroups()
	state.matchgroup = state.matchgroup or {}
    local MINC, C, R = config.minc, config.c, config.r
    for j = MINC, C do
        local mgj = {}
        state.matchgroup[j] = mgj

        local tpm = tonumber(state.teamsPerMatch[j]) or 2
        if tpm < 1 then tpm = 2 end

        local col = state.entries[j]
        if col then
            for i = 1, R do
                local e = col[i]
                if e and e.ctype == "team" then
                    local idx = tonumber(e.index) or tonumber(e.altindex) or i
                    local g = m_ceil(idx / tpm)
                    mgj[i] = g
                    e.group = g
                end
            end
        end
    end
end

-- Build ordered lists once per round: gid -> {row indices}
local function buildGroupsForRound(j, R)
	local groups = {}
    local mg = (state.matchgroup and state.matchgroup[j]) or {}
    local col = state.entries[j]
    if not col then return groups end

    for i = 1, R do
        local e = col[i]
        if e and e.ctype == "team" then
            local gid = mg[i]
            if gid ~= nil then
                local t = groups[gid]; if not t then t = {}; groups[gid] = t end
                t[#t + 1] = i
            end
        end
    end
    return groups
end

-- Pre-parse leg numbers for each team (per round), only when agg & >1 legs.
-- Returns: i -> { [l] = number|nil }
local function preparseLegs(j, R)
    local legNums = {}
    local col = state.entries[j]
    if not col then return legNums end

    for i = 1, R do
        local e = col[i]
        if e and e.ctype == "team" then
            local L = teamLegs(j, i)
            if config.aggregate and L > 1 and e.score then
                local t = {}
                for l = 1, L do
                    t[l] = numlead(e.score[l])
                end
                legNums[i] = t
            end
        end
    end
    return legNums
end

-- ==========================================
-- 5) COMPUTE AGGREGATES (score/legs/sets)
-- ==========================================
local function _computeAggregate()
    if config.aggregate_mode == "off" or config.aggregate_mode == "manual" then
        return
    end

    local MINC, C, R = config.minc, config.c, config.r
    local modeLow = (config.boldwinner_mode == "low")

    for j = MINC, C do
        local groups  = buildGroupsForRound(j, R)
        local legNums = preparseLegs(j, R)

        if config.aggregate_mode == "score" then
            -- Sum per-leg scores; operate directly on parsed legNums.
            for i, nums in pairs(legNums) do
                local e = state.entries[j][i]
                if e and e.ctype == "team" and config.aggregate and teamLegs(j, i) > 1 then
                    local sc = e.score; if sc and isempty(sc.agg) then
                        local sum = 0
                        for _, v in ipairs(nums) do if v then sum = sum + v end end
                        sc.agg = tostring(sum)
                    end
                end
            end
        else
            -- 'sets'/'legs': count wins per-leg using high/low rule; ties yield no win.
            for _, members in pairs(groups) do
                local wins = {} -- row index -> wins

                -- Comparable leg count across the group = min(#numeric arrays)
                local commonLegs = math.huge
                for _, i in ipairs(members) do
                    local nums = legNums[i]
                    local L = (nums and #nums) or 0
                    if L == 0 then commonLegs = 0; break end
                    if L < commonLegs then commonLegs = L end
                end

                for l = 1, commonLegs do
                    local allNumeric = true
                    for _, i in ipairs(members) do
                        local v = legNums[i] and legNums[i][l]
                        if v == nil then allNumeric = false; break end
                    end
                    if allNumeric then
                        local best, bestIdx, tie
                        for _, i in ipairs(members) do
                            local v = legNums[i][l]
                            if best == nil then
                                best, bestIdx, tie = v, i, false
                            else
                                if (modeLow and v < best) or (not modeLow and v > best) then
                                    best, bestIdx, tie = v, i, false
                                elseif v == best then
                                    tie = true
                                end
                            end
                        end
                        if not tie and bestIdx then
                            wins[bestIdx] = (wins[bestIdx] or 0) + 1
                        end
                    end
                end

                -- Write aggregates if still empty
                for _, i in ipairs(members) do
                    local e = state.entries[j][i]
                    if e and e.ctype == "team" and config.aggregate and teamLegs(j, i) > 1 then
                        local sc = e.score
                        if sc and isempty(sc.agg) then
                            sc.agg = tostring(wins[i] or 0)
                        end
                    end
                end
            end
        end
    end
end

-- ==========================================
-- 6) BOLD WINNERS (per cells & whole rows)
-- ==========================================
local function _boldWinner()
    -- short-circuit: explicit OFF clears all bolding and returns
    if not config or (config.boldwinner_mode == "off") then
        for j = config.minc, config.c do
            for i = 1, config.r do
                local e = state.entries[j] and state.entries[j][i]
                if e and e.ctype == "team" then
                    e.weight = "normal"
                    -- team seed (both shapes, be defensive)
                    if e.seed and type(e.seed) == "table" then e.seed.weight = "normal" end
                    e.seed_weight = "normal"

                    if e.score then
                        e.score.weight = e.score.weight or {}
                        -- normalize all legs and agg we know about
                        for k,_ in pairs(e.score.weight) do
                            e.score.weight[k] = "normal"
                        end
                    end
                end
            end
        end
        return
    end

    local MINC, C, R = config.minc, config.c, config.r

    -- modes
    local mode = tostring(config.boldwinner_mode or ""):lower()
    local modeLow = (mode == "low" or mode == "aggregate-low")
    -- aggregate* modes suppress per-leg score bolding but still bold seed/team/agg
    local aggOnly = (mode == "aggregate" or mode == "aggregate-low") or (config.boldwinner_aggonly == true)

    -- helpers
    local function isWin(a, b)
        return modeLow and (a < b) or (not modeLow and a > b)
    end

    -- For aggregate counts (sets/legs won), higher always wins unless aggregate_mode=="score"
    local function betterAgg(a, b)
        if config.aggregate_mode ~= "score" then
            return a > b
        else
            return isWin(a, b)
        end
    end

    local function hasAllScores(e, legs)
        local sc = e.score
        for l = 1, legs do
            local sv = sc and sc[l]
            if isempty(sv) or s_find(sv or "", "nbsp") then
                return false
            end
        end
        return true
    end

    local function boldSeedAndTeam(e, on)
        local w = on and "bold" or "normal"
        e.weight = w
        if e.seed and type(e.seed) == "table" then e.seed.weight = w end
        e.seed_weight = w -- harmless if renderer ignores it
    end

    for j = MINC, C do
        local groups = buildGroupsForRound(j, R)

        -- Reset counters & ensure score/weight tables exist
        for i = 1, R do
            local e = state.entries[j] and state.entries[j][i]
            if e and e.ctype == "team" then
                e.wins, e.aggwins = 0, 0
                e.score = e.score or {}
                e.score.weight = e.score.weight or {}
            end
        end

        for _, members in pairs(groups) do
            -- Parse per-leg and aggregate numbers once
            local perLegNum, aggNum = {}, {}
            local maxL = 0
            for _, i in ipairs(members) do
                local e = state.entries[j][i]
                local L = teamLegs(j, i); if L > maxL then maxL = L end
                local arr = {}
                for l = 1, L do arr[l] = numlead(e.score and e.score[l]) end
                perLegNum[i] = arr
                aggNum[i]    = numlead(e.score and e.score.agg)
            end

            -- If we're in aggregate* modes, proactively clear per-leg bolds
            if aggOnly then
                for _, i in ipairs(members) do
                    local e = state.entries[j][i]
                    local L = teamLegs(j, i)
                    for l = 1, L do
                        e.score.weight[l] = "normal"
                    end
                end
            end

            -- Per-leg bolding (skip if aggOnly)
            if not aggOnly then
                for l = 1, maxL do
                    local allNumeric, best, bestIdx, tie = true, nil, nil, false
                    for _, i in ipairs(members) do
                        local v = perLegNum[i] and perLegNum[i][l]
                        if v == nil then allNumeric = false; break end
                        if best == nil then best, bestIdx, tie = v, i, false
                        else
                            if isWin(v, best) then best, bestIdx, tie = v, i, false
                            elseif v == best then tie = true end
                        end
                    end
                    if allNumeric and not tie and bestIdx then
                        local e = state.entries[j][bestIdx]
                        e.score.weight[l] = "bold"
                        e.wins = (e.wins or 0) + 1
                        for _, i in ipairs(members) do
                            if i ~= bestIdx then state.entries[j][i].score.weight[l] = "normal" end
                        end
                    else
                        for _, i in ipairs(members) do
                            state.entries[j][i].score.weight[l] = "normal"
                        end
                    end
                end
            end

            -- Aggregate column (if configured & multi-leg)
            local anyMultiLeg = false
            if config.aggregate then
                for _, i in ipairs(members) do
                    if teamLegs(j, i) > 1 then anyMultiLeg = true; break end
                end
            end

            if anyMultiLeg then
                local allNumeric, best, bestIdx, tie = true, nil, nil, false
                for _, i in ipairs(members) do
                    local v = aggNum[i]
                    if v == nil then allNumeric = false; break end
                    if best == nil then best, bestIdx, tie = v, i, false
                    else
                        if betterAgg(v, best) then best, bestIdx, tie = v, i, false
                        elseif v == best then tie = true end
                    end
                end
                if allNumeric and not tie and bestIdx then
                    local e = state.entries[j][bestIdx]
                    e.score.weight.agg = "bold"
                    e.aggwins = 1
                    for _, i in ipairs(members) do
                        if i ~= bestIdx then
                            local o = state.entries[j][i]
                            o.score.weight.agg = "normal"
                            o.aggwins = 0
                        end
                    end
                else
                    for _, i in ipairs(members) do
                        local e = state.entries[j][i]
                        if e.score then e.score.weight.agg = "normal" end
                        e.aggwins = 0
                    end
                end
            else
                -- no aggregate column in this group
                for _, i in ipairs(members) do
                    local e = state.entries[j][i]
                    if e.score then e.score.weight.agg = "normal" end
                    e.aggwins = 0
                end
            end

            -- Team/Seed bolding (ALWAYS evaluate, even in aggOnly modes)
            for _, i in ipairs(members) do
                local e = state.entries[j][i]
                local L = teamLegs(j, i)
                local usingAggregate = config.aggregate and L > 1
                local winsKey = usingAggregate and "aggwins" or "wins"

                -- default assumption
                boldSeedAndTeam(e, false)

                if usingAggregate then
                    -- aggregate winner decided above (aggwins ∈ {0,1})
                    if (e[winsKey] or 0) > 0 then
                        -- must strictly beat every opponent on aggwins
                        local lead = true
                        for _, oi in ipairs(members) do
                            if oi ~= i then
                                local opp = state.entries[j][oi]
                                if tonumber(e[winsKey] or 0) <= tonumber(opp[winsKey] or 0) then
                                    lead = false; break
                                end
                            end
                        end
                        boldSeedAndTeam(e, lead)
                    end
                else
                    -- single-leg or multi-leg without aggregate: majority of per-leg wins
                    local need = L / 2
                    if (e[winsKey] or 0) > need then
                        -- verify it’s strictly greater than any opponent’s wins
                        local lead = true
                        for _, oi in ipairs(members) do
                            if oi ~= i then
                                local opp = state.entries[j][oi]
                                if tonumber(e[winsKey] or 0) <= tonumber(opp[winsKey] or 0) then
                                    lead = false; break
                                end
                            end
                        end
                        boldSeedAndTeam(e, lead)
                    else
                        -- If not a clear majority, bold only when all scores are present
                        -- and you strictly beat every opponent by score on the deciding leg set
                        if hasAllScores(e, L) then
                            local lead = true
                            for _, oi in ipairs(members) do
                                if oi ~= i then
                                    local opp = state.entries[j][oi]
                                    if tonumber(e[winsKey] or 0) <= tonumber(opp[winsKey] or 0) then
                                        lead = false; break
                                    end
                                end
                            end
                            boldSeedAndTeam(e, lead)
                        end
                    end
                end
            end
        end
    end
end

-- ==============================
-- 7) UPDATE PER-ROUND MAX LEGS
-- ==============================
local function _updateMaxLegs()
    local MINC, C, R = config.minc, config.c, config.r
    for j = MINC, C do
	    local col = state.entries[j]
	    local rj = state.rlegs[j]
	    local mj = rj
	    if col then
	        for i = 1, R do
	            local e = col[i]
	            if e then
	                if notempty(e.legs) then
	                    mj = m_max(rj, e.legs)
	                end
	                if config.autolegs and e.score then
	                    local l = 1
	                    while e.score[l] and not isempty(e.score[l]) do
	                        l = l + 1
	                    end
	                    mj = m_max(mj, l - 1)
	                end
	            end
	        end
	    end
	    state.maxlegs[j] = mj
	end
end

-- ==============
-- 8) PUBLIC API
-- ==============
function Logic.matchGroups(_state, _config)
    bind(_state, _config)
    _matchGroups()
end

function Logic.computeAggregate(_state, _config, _Helpers, _StateChecks)
    bind(_state, _config, _Helpers, _StateChecks)
    _computeAggregate()
end

function Logic.boldWinner(_state, _config, _Helpers, _StateChecks)
    bind(_state, _config, _Helpers, _StateChecks)
    _boldWinner()
end

function Logic.updateMaxLegs(_state, _config, _Helpers)
    bind(_state, _config, _Helpers)
    _updateMaxLegs()
end

return Logic