Module:Build bracket/Logic
Appearance
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