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 04:08, 13 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 str_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

-- -------------------------
-- Group teams within a round
-- -------------------------
local function _matchGroups()
  for j = config.minc, config.c do
    state.matchgroup[j] = {}
    local mgj = state.matchgroup[j]

    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, config.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

-- ----------------------------------------
-- Compute aggregates (score/legs/sets modes)
-- ----------------------------------------
local function _computeAggregate()
  if config.aggregate_mode == 'off' or config.aggregate_mode == 'manual' then return end

  local modeLow = (config.boldwinner_mode == 'low')

  local function numlead(s)
    if not s or s == '' then return nil end
    return tonumber((s):match('^%d+'))  -- leading integer only
  end

  -- Build groups: round j -> groupId -> {team indices}
  local function buildGroupsForRound(j)
    local groups = {}
    local mg = state.matchgroup[j] or {}
    for i = 1, config.r do
      local e = state.entries[j][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)
    local legNums = {}  -- i -> { [l] = number|nil }
    for i = 1, config.r do
      local e = state.entries[j][i]
      if e and e.ctype == 'team' then
        local L = teamLegs(j, i)
        if config.aggregate and L > 1 and (e.score and e.score.agg ~= nil) then
          legNums[i] = {}
          for l = 1, L do
            legNums[i][l] = numlead(e.score[l])
          end
        end
      end
    end
    return legNums
  end

  for j = config.minc, config.c do
    local groups   = buildGroupsForRound(j)
    local legNums  = preparseLegs(j)

    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 sum = 0
              local nums = legNums[i]
              if nums then
                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 index

        -- find number of comparable legs across the group
        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
          -- all teams must have a numeric value for this leg
          local allNumeric = true
          for _, i in ipairs(members) do
            if not (legNums[i] and legNums[i][l] ~= nil) then
              allNumeric = false; break
            end
          end

          if allNumeric then
            local best, bestIndex, tie = nil, nil, false
            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 modeLow  = (config.boldwinner_mode == 'low')
  local aggOnly  = config.boldwinner_aggonly

  local function isWin(mine, theirs)
    if modeLow then return mine < theirs else return mine > theirs end
  end

  local function isAggWin(mine, theirs, l)
    if l == 'agg' and config.aggregate_mode == 'sets' then
      -- Sets/legs won: larger count wins regardless of low/high scoring sport
      return mine > theirs
    else
      return isWin(mine, theirs)
    end
  end

  local function boldScore(j, i, l)
    local e = state.entries[j][i]
    if not e or e.ctype ~= 'team' then return 'normal' end

    local raw = e.score[l] or ''
    local mine = tonumber((raw):match('^%d+'))
    if not mine then return 'normal' end

    local comps = {}
    for oppIndex, groupId in pairs(state.matchgroup[j]) do
      if groupId == state.matchgroup[j][i] and oppIndex ~= i then
        local theirraw = state.entries[j][oppIndex].score[l] or ''
        local theirs = tonumber((theirraw):match('^%d+'))
        if not theirs then return 'normal' end
        t_insert(comps, theirs)
      end
    end

    for _, v in ipairs(comps) do
      if not isAggWin(mine, v, l) then return 'normal' end
    end

    if l ~= 'agg' then
      e.wins = (e.wins or 0) + 1
    else
      e.aggwins = 1
    end
    return 'bold'
  end

  local function boldTeam(j, i, useAggregate)
    local e = state.entries[j][i]
    local winsKey = useAggregate and 'aggwins' or 'wins'
    local legs = teamLegs(j, i)

    if not useAggregate then
      if (e[winsKey] or 0) > legs / 2 then return 'bold' end
      local checkFn = config.autolegs and notempty or function(val) return not isempty(val) end
      for l = 1, legs do
        local sv = e.score[l]
        if not checkFn(sv) or str_find(sv or '', "nbsp") then
          return 'normal'
        end
      end
    end

    for oppIndex, groupId in pairs(state.matchgroup[j]) do
      if groupId == state.matchgroup[j][i] and oppIndex ~= i then
        if (e[winsKey] or 0) <= tonumber(state.entries[j][oppIndex][winsKey] or 0) then
          return 'normal'
        end
      end
    end
    return 'bold'
  end

  -- reset counters
  for j = config.minc, config.c do
    for i = 1, config.r do
      if state.entries[j][i] and state.entries[j][i].ctype == 'team' then
        state.entries[j][i].wins    = 0
        state.entries[j][i].aggwins = 0
      end
    end

    -- per-score bolding
    for i = 1, config.r do
      if state.entries[j][i] and state.entries[j][i].ctype == 'team' then
        local legs = teamLegs(j, i)

        -- legs (skip entirely if agg-only)
        if not aggOnly then
          for l = 1, legs do
            state.entries[j][i].score.weight[l] = boldScore(j, i, l)
          end
        end

        -- aggregate column (if present)
        if config.aggregate and legs > 1 then
          state.entries[j][i].score.weight.agg = boldScore(j, i, 'agg')
        end
      end
    end

    -- whole-team bolding (skip if agg-only so only agg cell bolds)
    if not aggOnly then
      for i = 1, config.r do
        if state.entries[j][i] and state.entries[j][i].ctype == 'team' then
          local useAggregate = config.aggregate and teamLegs(j, i) > 1
          state.entries[j][i].weight = boldTeam(j, i, useAggregate)
        end
      end
    end
  end
end

-- ---------------------
-- Update per-round max legs
-- ---------------------
local function _updateMaxLegs()
  for j = config.minc, config.c do
    state.maxlegs[j] = state.rlegs[j]
    for i = 1, config.r do
      if notempty(state.entries[j][i]) then
        if notempty(state.entries[j][i].legs) then
          state.maxlegs[j] = m_max(state.rlegs[j], state.entries[j][i].legs)
        end
        if config.autolegs then
          local l = 1
          repeat l = l + 1
          until isempty(state.entries[j][i].score) or isempty(state.entries[j][i].score[l])
          state.maxlegs[j] = m_max(state.maxlegs[j], l - 1)
        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