Moduł:Build bracket/Params
Wygląd
local Params = {}
-- =========================
-- 1) MODULE BINDINGS
-- =========================
-- Upvalues bound per call
local state, config, Helpers, StateChecks
-- Stdlib aliases
local str_format = string.format
local t_insert = table.insert
local t_sort = table.sort
-- Locals filled on bind (with safe fallbacks)
local isempty, notempty, bargs, getPArg, getFArg, toChar, split
local function _toCharFallback(n)
n = tonumber(n or 0) or 0
if n >= 1 and n <= 26 then
return string.char(96 + n) -- 'a'..'z'
end
return tostring(n)
end
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
bargs = Helpers and Helpers.bargs
getPArg = Helpers and Helpers.getPArg
getFArg = Helpers and Helpers.getFArg
toChar = (Helpers and Helpers.toChar) or _toCharFallback
split = Helpers and Helpers.split
end
-- Try both RDj[a]-X and RDj[A]-X; falls back to "" if not present
local function readPerHeaderArg(j, k, suffix)
local ch = toChar(k) -- e.g. 'a' with Helpers.toChar
local key1 = "RD" .. j .. ch .. suffix
local v = bargs(key1)
if v ~= nil and v ~= "" then
return v
end
local key2 = "RD" .. j .. string.upper(ch) .. suffix
v = bargs(key2)
if v ~= nil and v ~= "" then
return v
end
return ""
end
-- =========================
-- 2) ARG HELPERS
-- =========================
-- Trim + split a comma list frame arg (returns {} on blank)
local function readCsvArg(name)
local raw = (getFArg(name) or ""):gsub("%s+", "")
return split(raw, {","}, true)
end
-- ========================================
-- 3) BUILD SKELETON (formerly getCells)
-- ========================================
local function buildSkeleton()
local DEFAULT_TPM = 2
local maxrow = 1
local colentry = {}
local hasNoHeaders = true
-- ensure containers
state.entries = state.entries or {}
state.shift = state.shift or {}
state.teamsPerMatch = state.teamsPerMatch or {}
state.maxtpm = state.maxtpm or 0
local Cmin, Cmax = config.minc, config.c
-- Phase 1: Determine header presence and teamsPerMatch
for j = Cmin, Cmax do
if notempty(getFArg("col" .. j .. "-headers")) then
hasNoHeaders = false
end
local tpm =
tonumber(getFArg("RD" .. j .. "-teams-per-match")) or tonumber(getFArg("col" .. j .. "-teams-per-match")) or
tonumber(getFArg("teams-per-match")) or
DEFAULT_TPM
state.teamsPerMatch[j] = tpm
if tpm > state.maxtpm then
state.maxtpm = tpm
end
end
-- Phase 2: Build colentry for each column
for j = Cmin, Cmax do
state.entries[j] = {}
state.shift[j] = tonumber(bargs("RD" .. j .. "-shift")) or tonumber(bargs("shift")) or 0
colentry[j] = {
readCsvArg("col" .. j .. "-headers"),
readCsvArg("col" .. j .. "-matches"),
readCsvArg("col" .. j .. "-lines"),
readCsvArg("col" .. j .. "-text"),
readCsvArg("col" .. j .. "-groups") -- reserved for user-specified groups
}
-- inject a default header if none were specified anywhere (unless noheaders=y/yes)
local noheaders = (getFArg("noheaders") or ""):lower()
if hasNoHeaders and (noheaders ~= "y" and noheaders ~= "yes") then
t_insert(colentry[j][1], 1)
end
end
-- Ctype mapping for colentry positions
local CTYPE_MAP = {"header", "team", "line", "text", "group"}
-- Helpers to populate entries (preserve legacy shapes)
local function populateTeam(j, rowIndex, n)
local TPM = state.teamsPerMatch[j]
-- scaffold a text row above when needed (legacy behavior)
if state.entries[j][rowIndex - 1] == nil and state.entries[j][rowIndex - 2] == nil then
state.entries[j][rowIndex - 2] = {ctype = "text", index = n}
state.entries[j][rowIndex - 1] = {ctype = "blank"}
end
-- first team (top)
state.entries[j][rowIndex] = {ctype = "team", index = TPM * n - (TPM - 1), position = "top"}
state.entries[j][rowIndex + 1] = {ctype = "blank"}
-- remaining teams in the match (every 2 rows)
for m = 2, TPM do
local idx = TPM * n - (TPM - m)
local r = rowIndex + 2 * (m - 1)
state.entries[j][r] = {ctype = "team", index = idx}
state.entries[j][r + 1] = {ctype = "blank"}
end
end
local function populateText(j, rowIndex, index)
state.entries[j][rowIndex] = {ctype = "text", index = index}
state.entries[j][rowIndex + 1] = {ctype = "blank"}
end
local function populateLine(j, rowIndex)
-- first segment draws its bottom edge
state.entries[j][rowIndex] = {ctype = "line", border = "bottom"}
state.entries[j][rowIndex + 1] = {ctype = "blank"}
-- second segment draws its top edge
state.entries[j][rowIndex + 2] = {ctype = "line", border = "top"}
state.entries[j][rowIndex + 3] = {ctype = "blank"}
end
local function populateGroup(j, rowIndex, n)
state.entries[j][rowIndex] = {ctype = "group", index = n}
state.entries[j][rowIndex + 1] = {ctype = "blank"}
end
local function populateDefault(j, rowIndex, n)
state.entries[j][rowIndex] = {ctype = "header", index = n, position = "top"}
state.entries[j][rowIndex + 1] = {ctype = "blank"}
end
-- Phase 3: Populate entries for each column
for j = Cmin, Cmax do
local textindex = 0
local TPM = state.teamsPerMatch[j]
local shiftJ = state.shift[j]
for k, positions in ipairs(colentry[j]) do
t_sort(positions)
local ctype = CTYPE_MAP[k]
for n = 1, #positions do
if shiftJ ~= 0 and positions[n] > 1 then
positions[n] = positions[n] + shiftJ
end
local rowIndex = 2 * positions[n] - 1
local lastRow = rowIndex + 2 * TPM - 1
if lastRow > maxrow then
maxrow = lastRow
end
if ctype == "team" then
populateTeam(j, rowIndex, n)
textindex = n
elseif ctype == "text" then
populateText(j, rowIndex, textindex + n)
elseif ctype == "line" then
populateLine(j, rowIndex)
elseif ctype == "group" then
populateGroup(j, rowIndex, n)
else
populateDefault(j, rowIndex, n)
end
end
end
end
if isempty(config.r) then
config.r = maxrow
end
end
-- ========================================
-- 4) NAME RESOLUTION HELPERS
-- ========================================
local function paramNames(cname, j, i, l)
local function getArg(key)
return bargs(key) or ""
end
local function getP(key)
return getPArg(key) or ""
end
local e = state.entries[j][i]
local RD = "RD" .. j
local hidx = e.headerindex
local hchar = toChar(hidx)
local RDh = RD .. hchar
local SYNONYMS = {legs = {"legs", "sets"}}
-- Try a list of names against base+index(+suffix); returns first non-empty
local function tryAny(base, names, idx, suffix)
suffix = suffix or ""
for _, nm in ipairs(names) do
local a = bargs(base .. "-" .. nm .. idx .. suffix) or ""
if isempty(a) then
a = bargs(base .. "-" .. nm .. string.format("%02d", idx) .. suffix) or ""
end
if notempty(a) then
return a
end
end
return ""
end
local function tryBoth(base, name, idx, suffix)
suffix = suffix or ""
local a = getArg(base .. "-" .. name .. idx .. suffix)
if isempty(a) then
a = getArg(base .. "-" .. name .. str_format("%02d", idx) .. suffix)
end
return a
end
-- Round names (prefer altname when present)
local RDlabel = getArg(RD .. "-altname") or RD
local RDhlabel = getArg(RDh .. "-altname") or RDh
local rname = {{RD, RDlabel}, {RDh, RDhlabel}}
local name = {cname, getArg(cname .. "-altname") or cname}
local nameKeys = SYNONYMS[cname] or {name[1]}
local index = {e.index, e.altindex}
local result = {}
if cname == "header" then
if hidx == 1 then
for _, base in ipairs({rname[1], rname[2]}) do
for k = 2, 1, -1 do
result[#result + 1] = getArg(base[k])
end
end
else
for k = 2, 1, -1 do
result[#result + 1] = getArg(rname[2][k])
end
end
elseif cname == "pheader" then
if hidx == 1 then
for _, base in ipairs({rname[1], rname[2]}) do
for k = 2, 1, -1 do
result[#result + 1] = getP(base[k])
end
end
else
for k = 2, 1, -1 do
result[#result + 1] = getP(rname[2][k])
end
end
elseif cname == "score" then
local bases = {rname[2][2], rname[2][1], rname[1][2], rname[1][1]}
local idxs = {index[2], index[2], index[1], index[1]}
for n = 1, 4 do
if l == 1 then
result[#result + 1] = tryAny(bases[n], nameKeys, idxs[n])
end
result[#result + 1] = tryAny(bases[n], nameKeys, idxs[n], "-" .. l)
end
elseif cname == "shade" then
for k = 2, 1, -1 do
local base = (hidx == 1) and rname[1][k] or rname[2][k]
result[#result + 1] = getArg(base .. "-" .. name[1])
end
result[#result + 1] = getArg("RD-shade")
result[#result + 1] = (config.COLORS and config.COLORS.cell_bg_dark) or "#eaecf0"
elseif cname == "text" then
local bases = {rname[2][2], rname[2][1], rname[1][2], rname[1][1]}
local idxs = {index[2], index[2], index[1], index[1]}
local names = {name[2], name[1]}
for ni = 1, 2 do
for n = 1, 4 do
result[#result + 1] = tryBoth(bases[n], names[ni], idxs[n])
end
end
else
local bases = {rname[2][2], rname[2][1], rname[1][2], rname[1][1]}
local idxs = {index[2], index[2], index[1], index[1]}
for n = 1, 4 do
result[#result + 1] = tryAny(bases[n], nameKeys, idxs[n])
end
end
for _, val in ipairs(result) do
if notempty(val) then
return val
end
end
return ""
end
-- ========================================
-- 5) NUMBERED PARAM MODE
-- ========================================
local masterindex = 1
local function numberedParams(j)
local row = state.entries[j]
if not row then
return
end
local function nextArg()
local v = bargs(tostring(masterindex)) or ""
masterindex = masterindex + 1
return v
end
local R = config.r
for i = 1, R do
local e = row[i]
if e then
local ct = e.ctype
if ct == "team" then
local legs = state.rlegs[j]
if config.forceseeds then
e.seed = nextArg()
end
e.team = nextArg()
e.legs = paramNames("legs", j, i)
e.score = {weight = {}}
e.weight = "normal"
if notempty(e.legs) then
legs = tonumber(e.legs) or legs
end
for l = 1, legs do
e.score[l] = nextArg()
e.score.weight[l] = "normal"
end
if config.aggregate and legs > 1 then
e.score.agg = nextArg()
e.score.weight.agg = "normal"
end
elseif ct == "header" then
e.header = paramNames("header", j, i)
e.pheader = paramNames("pheader", j, i)
e.shade = paramNames("shade", j, i)
elseif ct == "text" then
e.text = nextArg()
elseif ct == "group" then
e.group = nextArg()
elseif ct == "line" and e.hastext == true then
e.text = nextArg()
end
end
end
end
-- ========================================
-- 6) NAMED MODE ASSIGNERS (per-ctype)
-- ========================================
local function cellHasMeaningfulContent(e)
if not e then
return false
end
if e.ctype == "team" then
return notempty(e.team)
end
if e.ctype == "text" then
return notempty(e.text)
end
if e.ctype == "group" then
return notempty(e.group)
end
if e.ctype == "line" and e.hastext == true then
return notempty(e.text)
end
return false
end
local function enforceContentUnhide(j)
if not (state.hide and state.hide[j]) then
return
end
local explicit = (state._hideExplicit and state._hideExplicit[j]) or {}
local R = config.r
-- If master hid a header, but we later discover content in that header,
-- flip it visible unless there was an EXPLICIT per-header hide.
for i = 1, R do
local e = state.entries[j][i]
if e and e.headerindex then
local h = e.headerindex
if state.hide[j][h] and cellHasMeaningfulContent(e) then
state.hide[j][h] = false
end
end
end
end
local function assignTeamParams(j, i)
local legs = state.rlegs[j]
local e = state.entries[j][i]
e.seed = paramNames("seed", j, i)
e.team = paramNames("team", j, i)
e.legs = paramNames("legs", j, i)
e.score = {weight = {}}
e.weight = "normal"
if notempty(e.legs) then
legs = tonumber(e.legs) or legs
end
if config.autolegs then
local l = 1
repeat
e.score[l] = paramNames("score", j, i, l)
e.score.weight[l] = "normal"
l = l + 1
until isempty(paramNames("score", j, i, l))
legs = l - 1
else
for l = 1, legs do
e.score[l] = paramNames("score", j, i, l)
e.score.weight[l] = "normal"
end
end
if config.aggregate and legs > 1 then
e.score.agg = paramNames("score", j, i, "agg")
e.score.weight.agg = "normal"
end
end
local function assignHeaderParams(j, i)
local e = state.entries[j][i]
e.header = paramNames("header", j, i)
e.pheader = paramNames("pheader", j, i)
e.shade = paramNames("shade", j, i)
-- Did shade originate from an RD*-shade param?
local hchar = toChar(e.headerindex)
local rdNames = {
"RD" .. j .. "-shade",
"RD" .. j .. hchar .. "-shade",
"RD-shade"
}
e.shade_is_rd = false
for _, pname in ipairs(rdNames) do
local v = bargs(pname)
if notempty(v) and e.shade == v then
e.shade_is_rd = true
break
end
end
end
local function assignTextParams(j, i)
state.entries[j][i].text = paramNames("text", j, i)
end
local function assignGroupParams(j, i)
state.entries[j][i].group = paramNames("group", j, i)
end
local function assignLineTextParams(j, i)
state.entries[j][i].text = paramNames("text", j, i)
end
-- ========================================
-- 7) TABLE-WIDE ASSIGNMENT PASS
-- ========================================
local function getScalarRoundParam(j, bases) -- e.g. {"legs","sets"}
-- prefer per-round keys, then global
for _, base in ipairs(bases) do
local v = bargs("RD" .. j .. "-" .. base)
if notempty(v) then
return v
end
end
for _, base in ipairs(bases) do
local v = bargs(base)
if notempty(v) then
return v
end
end
return ""
end
local function assignParams()
masterindex = 1
local maxcol = 1
local Cmin, Cmax, R = config.minc, config.c, config.r
for j = Cmin, Cmax do
-- prepare per-round containers
state.hide[j] = state.hide[j] or {}
state.byes[j] = state.byes[j] or {}
-- Set legs for this column
local vlegs = getScalarRoundParam(j, {"legs", "sets"})
state.rlegs[j] = tonumber(vlegs) or 1
if notempty(vlegs) then
config.autolegs = false
end
-- assign params
if config.paramstyle == "numbered" then
numberedParams(j)
else
local col = state.entries[j]
for i = 1, R do
local cell = col[i]
if cell ~= nil then
local ct = cell.ctype
if ct == "team" then
assignTeamParams(j, i)
elseif ct == "header" then
assignHeaderParams(j, i)
elseif ct == "text" then
assignTextParams(j, i)
elseif ct == "group" then
assignGroupParams(j, i)
elseif ct == "line" and cell.hastext == true then
assignLineTextParams(j, i)
end
end
if config.autocol and not StateChecks.isBlankEntry(j, i) and j > maxcol then
maxcol = j
end
end
end
enforceContentUnhide(j)
-- parent header text forces visible
for i = 1, R do
local e = state.entries[j][i]
if e and e.ctype == "header" then
local hidx = e.headerindex
if (Helpers.notempty and Helpers.notempty(e.pheader)) then
state.hide[j][hidx] = false
end
end
end
end
if config.autocol then
config.c = maxcol
end
end
-- ========================================
-- 8) STRUCTURE DISCOVERY (hide/byes/indices)
-- ========================================
local function getHide(j)
state.hide[j] = {}
state._hideExplicit = state._hideExplicit or {}
state._hideExplicit[j] = {}
-- master round-level hide flag: RD{j}-hide
local masterRaw = bargs("RD" .. j .. "-hide") or ""
local masterOn = (Helpers and Helpers.yes and Helpers.yes(masterRaw)) or false
for k = 1, state.headerindex[j] do
state.hide[j][k] = masterOn
-- per-header override
local rh = readPerHeaderArg(j, k, "-hide")
if rh ~= "" then
if Helpers and Helpers.yes and Helpers.yes(rh) then
state.hide[j][k] = true
state._hideExplicit[j][k] = true
elseif Helpers and Helpers.no and Helpers.no(rh) then
state.hide[j][k] = false
state._hideExplicit[j][k] = true
end
end
end
end
local function getByes(j)
state.byes[j] = {}
for k = 1, state.headerindex[j] do
-- global byes
local byes = (bargs("byes") or ""):lower()
if (Helpers.yes and Helpers.yes(byes)) then
state.byes[j][k] = true
elseif tonumber(byes) then
state.byes[j][k] = (j <= tonumber(byes))
else
state.byes[j][k] = false
end
-- per-round byes
local r = (bargs("RD" .. j .. "-byes") or ""):lower()
if (Helpers.yes and Helpers.yes(r)) then
state.byes[j][k] = true
elseif r == "no" or r == "n" then
state.byes[j][k] = false
end
-- per-header byes
local rh = (readPerHeaderArg(j, k, "-byes") or ""):lower()
if (Helpers.yes and Helpers.yes(rh)) then
state.byes[j][k] = true
elseif rh == "no" or rh == "n" then
state.byes[j][k] = false
end
end
end
local function getAltIndices()
local Cmin, Cmax, R = config.minc, config.c, config.r
for j = Cmin, Cmax do
state.headerindex[j] = 0
-- per-round counters
local teamindex, textindex, groupindex = 1, 1, 1
local row = state.entries[j]
-- if the very first cell is nil, bump headerindex once (legacy quirk)
if row and row[1] == nil then
state.headerindex[j] = state.headerindex[j] + 1
end
-- walk rows in the round
for i = 1, R do
local e = row and row[i] or nil
if e then
local ct = e.ctype
if ct == "header" then
e.altindex = state.headerindex[j]
teamindex, textindex = 1, 1
state.headerindex[j] = state.headerindex[j] + 1
elseif ct == "team" then
e.altindex = teamindex
teamindex = teamindex + 1
elseif ct == "text" or (ct == "line" and e.hastext == true) then
e.altindex = textindex
textindex = textindex + 1
elseif ct == "group" then
e.altindex = groupindex
groupindex = groupindex + 1
end
e.headerindex = state.headerindex[j]
end
end
getByes(j)
getHide(j)
end
end
-- ========================================
-- 9) PUBLIC API
-- ========================================
function Params.buildSkeleton(_state, _config, _Helpers, _StateChecks)
bind(_state, _config, _Helpers, _StateChecks)
buildSkeleton()
end
function Params.scanStructure(_state, _config, _Helpers, _StateChecks)
bind(_state, _config, _Helpers, _StateChecks)
getAltIndices()
end
function Params.assign(_state, _config, _Helpers, _StateChecks)
bind(_state, _config, _Helpers, _StateChecks)
assignParams()
end
-- ========================================
-- 10) SLICING FOR MIN ROUND (base offset)
-- ========================================
local function shiftCols(tbl, base, c)
if not tbl then
return {}
end
local out = {}
for j = base + 1, c do
out[j - base] = tbl[j]
end
return out
end
function Params.sliceForMinround(_state, _config)
local base = _config.base or 0
if base <= 0 then
return
end
local oldC = _config.c
local newC = oldC - base
if newC < 1 then
newC = 1
end
-- Shift all column-indexed tables
_state.entries = shiftCols(_state.entries, base, oldC)
_state.headerindex = shiftCols(_state.headerindex, base, oldC)
_state.rlegs = shiftCols(_state.rlegs, base, oldC)
_state.maxlegs = {} -- recompute later
_state.hascross = {} -- rebuild later
_state.crossCell = {} -- rebuild later
_state.pathCell = {} -- rebuild later
_state.skipPath = {} -- rebuild later
_state.hide = shiftCols(_state.hide, base, oldC)
_state.byes = shiftCols(_state.byes, base, oldC)
_state.teamsPerMatch = shiftCols(_state.teamsPerMatch, base, oldC)
_state.matchgroup = {} -- recompute
-- Update view range: now we render 1..newC
_config.c = newC
_config.minc = 1
end
return Params