Przejdź do zawartości

Moduł:Build bracket/Logic

Z Wikipedii, wolnej encyklopedii
To jest stara wersja tej strony, edytowana przez Quri.inka (dyskusja | edycje) o 15:52, 18 sie 2025. Może się ona znacząco różnić od aktualnej wersji.
 Dokumentacja modułu[stwórz] • [odśwież]
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

    -- 1) strip leading spaces and wiki bold/italic quotes ('' or ''')
    s = s:match("^%s*'*%s*(.*)") or s

    -- 2) strip *leading* simple wrappers (tags/templates/links), repeatedly, but only if they occur before the first digit.
    local advanced = true -- set false if you want the minimal version
    if advanced then
        local changed = true
        while changed do
            changed = false
            local s2 = s:gsub("^%s*<[^>]->%s*", "", 1)      -- leading HTML tag
            if s2 ~= s then s, changed = s2, true end
            s2 = s:gsub("^%s*{{.-}}%s*", "", 1)             -- leading template
            if s2 ~= s then s, changed = s2, true end
            s2 = s:gsub("^%s*%[%[[^%]]-%]%]%s*", "", 1)     -- leading wikilink
            if s2 ~= s then s, changed = s2, true end
        end
    end

    -- 3) capture leading digits only (stops at first non-digit)
    local n = s:match("^(%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()
	local function isWin(mine, theirs)
	    return modeLow and (mine < theirs) or (not modeLow and mine > theirs)
	end
	
	local function isAggWin(mine, theirs, colKey)
	    -- For aggregate counts (legs/sets won), higher is always better
	    if colKey == "agg" and (config.aggregate_mode ~= "score") then
	        return mine > theirs
	    end
	    return isWin(mine, theirs)
	end
	
    if not config or config.boldwinner_mode == "off" then
        -- Normalize all weights (defensive; avoids leftovers across calls)
        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"
                    if e.score and e.score.weight then
                        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
    local modeLow = (config.boldwinner_mode == "low")
    local aggOnly = config.boldwinner_aggonly

    local function isWin(mine, theirs)
        return modeLow and (mine < theirs) or (not modeLow and mine > theirs)
    end

    local function isAggWin(mine, theirs, colKey)
        if colKey == "agg" and config.aggregate_mode == "sets" then
            -- Sets/legs won: larger count wins regardless of low/high sport
            return mine > theirs
        end
        return isWin(mine, theirs)
    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  -- use alias
	            return false
	        end
	    end
	    return true
	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 for this group (works for 1+ legs)
            local perLegNum = {}  -- row -> { l -> number|nil }
            local aggNum    = {}  -- row -> number|nil

            for _, i in ipairs(members) do
                local e = state.entries[j][i]
                local legs = teamLegs(j, i)
                local arr = {}
                for l = 1, legs do
                    arr[l] = numlead(e.score and e.score[l])
                end
                perLegNum[i] = arr
                aggNum[i] = numlead(e.score and e.score.agg)
            end

            -- Per-score bolding (skip entirely if agg-only)
			if not aggOnly then
			    -- iterate to the max leg count in the group
			    local maxL = 0
			    for _, i in ipairs(members) do
			        local L = teamLegs(j, i)
			        if L > maxL then maxL = L end
			    end
			
			    for l = 1, maxL do
			        local allNumeric = true
			        local best, bestIdx, tie
			        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 (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 allNumeric and not tie and bestIdx then
			            -- set the winner bold + increment wins
			            local e = state.entries[j][bestIdx]
			            e.score.weight[l] = "bold"
			            e.wins = (e.wins or 0) + 1
			            -- normalize others on this leg
			            for _, i in ipairs(members) do
			                if i ~= bestIdx then
			                    state.entries[j][i].score.weight[l] = "normal"
			                end
			            end
			        else
			            -- no unique winner: normalize everyone for this leg
			            for _, i in ipairs(members) do
			                state.entries[j][i].score.weight[l] = "normal"
			            end
			        end
			    end
			end

            -- Aggregate column (if configured & multi-leg)
			do
			    local needAgg = false
			    for _, i in ipairs(members) do
			        if config.aggregate and teamLegs(j, i) > 1 then
			            needAgg = true; break
			        end
			    end
			
			    if needAgg then
			        local allNumeric = true
			        local best, bestIdx, tie
			
			        -- comparator: for legs/sets counts, higher always wins
			        local function betterAgg(a, b)
			            if config.aggregate_mode ~= "score" then
			                return a > b
			            else
			                return (modeLow and a < b) or (not modeLow and a > b)
			            end
			        end
			
			        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
			    end
			end


            -- Whole-team bolding (skip if agg-only so only agg cell bolds)
            if not aggOnly then
                for _, i in ipairs(members) do
                    local e = state.entries[j][i]
                    local legs = teamLegs(j, i)
                    local useAggregate = config.aggregate and legs > 1
                    local winsKey = useAggregate and "aggwins" or "wins"

                    if not useAggregate then
                        if (e[winsKey] or 0) > legs / 2 then
                            e.weight = "bold"
                        else
                            e.weight = hasAllScores(e, legs) and "bold" or "normal"
                        end
                    end

                    -- Must strictly beat any opponent on winsKey
                    for _, oi in ipairs(members) do
                        if oi ~= i then
                            local opp = state.entries[j][oi]
                            if (e[winsKey] or 0) <= tonumber(opp[winsKey] or 0) then
                                e.weight = "normal"
                                break
                            end
                        end
                    end

                    if useAggregate then
                        -- when using aggregate, team weight follows aggwins comparison
                        e.weight = ((e[winsKey] or 0) > 0) and "bold" or "normal"
                        for _, oi in ipairs(members) do
                            if oi ~= i then
                                local opp = state.entries[j][oi]
                                if (e[winsKey] or 0) <= tonumber(opp[winsKey] or 0) then
                                    e.weight = "normal"
                                    break
                                end
                            end
                        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