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 01:56, 15 August 2025. The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
local Logic = {}

-- stdlib aliases
local m_max    = math.max
local m_ceil   = math.ceil
local t_insert = table.insert
local s_match  = string.match
local s_find   = string.find

-- upvalues bound per call
local state, config, Helpers, StateChecks
local isempty, notempty, teamLegs

-- internal binder used by the public fns below
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

-- small helper: leading integer (or nil)
local function numlead(s)
  if not s or s == '' then return nil end
  local n = s_match(s, '^%d+')
  return n and tonumber(n) or nil
end

-- -------------------------
-- Group teams within a round
-- -------------------------
local function _matchGroups()
  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 groups as ordered lists once per round: gid -> { row indices }
local function buildGroupsForRound(j, R)
  local groups, mg = {}, 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)
local function preparseLegs(j, R)
  local legNums = {}  -- i -> { [l] = number|nil }, only for teams with >1 legs
  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

-- ----------------------------------------
-- Compute aggregates (score/legs/sets modes)
-- ----------------------------------------
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
      for _, members in pairs(groups) do
        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
            if isempty(e.score.agg) then
              local nums = legNums[i]
              if nums then
                local sum = 0
                for _, v in ipairs(nums) do if v then sum = sum + v end end
                e.score.agg = tostring(sum)
              end
            end
          end
        end
      end

    else
      -- 'sets'/'legs' → count leg wins using high/low rule; ties = no win; non-numeric leg => skip
      for _, members in pairs(groups) do
        local wins = {}  -- per team row index

        -- comparable legs 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
          -- must be numeric for everyone at this leg
          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, bestIndex, tie
            for _, i in ipairs(members) do
              local v = legNums[i][l]
              if best == nil then
                best, bestIndex, tie = v, i, false
              else
                if (modeLow and v < best) or (not modeLow and v > best) then
                  best, bestIndex, tie = v, i, false
                elseif v == best then
                  tie = true
                end
              end
            end
            if not tie and bestIndex then
              wins[bestIndex] = (wins[bestIndex] 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
            if isempty(e.score.agg) then
              e.score.agg = tostring(wins[i] or 0)
            end
          end
        end
      end
    end
  end
end

-- ---------------------
-- Bold winners (cells & rows)
-- ---------------------
local function _boldWinner()
  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

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

    -- reset counters
    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
      end
    end

    -- per-score bolding
    for gid, members in pairs(groups) do
      -- quick index set for group membership checks if needed
      for _, i in ipairs(members) do
        local e = state.entries[j][i]
        if e and e.ctype == 'team' then
          local legs = teamLegs(j, i)

          -- legs (skip entirely if agg-only)
          if not aggOnly then
            for l = 1, legs do
              local mine = numlead(e.score and e.score[l])
              local bold = (mine ~= nil)
              if bold then
                for _, oi in ipairs(members) do
                  if oi ~= i then
                    local theirs = numlead(state.entries[j][oi].score and state.entries[j][oi].score[l])
                    if theirs == nil or not isAggWin(mine, theirs, l) then
                      bold = false; break
                    end
                  end
                end
              end
              e.score.weight[l] = bold and 'bold' or 'normal'
              if bold then e.wins = (e.wins or 0) + 1 end
            end
          end

          -- aggregate column (if present)
          if config.aggregate and legs > 1 then
            local mineAgg = numlead(e.score and e.score.agg)
            local boldAgg = (mineAgg ~= nil)
            if boldAgg then
              for _, oi in ipairs(members) do
                if oi ~= i then
                  local theirsAgg = numlead(state.entries[j][oi].score and state.entries[j][oi].score.agg)
                  if theirsAgg == nil or not isAggWin(mineAgg, theirsAgg, 'agg') then
                    boldAgg = false; break
                  end
                end
              end
            end
            e.score.weight.agg = boldAgg and 'bold' or 'normal'
            if boldAgg then e.aggwins = 1 end
          end
        end
      end
    end

    -- whole-team bolding (skip if agg-only so only agg cell bolds)
    if not aggOnly then
      for gid, members in pairs(groups) do
        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'

          -- majority check for legs-mode rows
          if not useAggregate then
            if (e[winsKey] or 0) > legs / 2 then
              e.weight = 'bold'
            else
              -- If any score cell is missing, keep normal (mirrors previous behavior)
              local ok = true
              local checkFn = config.autolegs and notempty or function(val) return not isempty(val) end
              for l = 1, legs do
                local sv = e.score and e.score[l]
                if not checkFn(sv) or s_find(sv or '', 'nbsp') then ok = false; break end
              end
              e.weight = ok and 'bold' or 'normal'
            end
          end

          -- must strictly beat opponents 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

-- ---------------------
-- 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]
    state.maxlegs[j] = state.rlegs[j]
    if col then
      for i = 1, R do
        local e = col[i]
        if e then
          if notempty(e.legs) then
            state.maxlegs[j] = m_max(state.rlegs[j], 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
            state.maxlegs[j] = m_max(state.maxlegs[j], l - 1)
          end
        end
      end
    end
  end
end

-- -------------
-- 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