Module:PillSeasonSchedule
From MicrasWiki
Documentation for this module may be created at Module:PillSeasonSchedule/doc
-- Module:PillSeasonSchedule
local p = {}
local date = require('Module:BassaridianCalendar')
------------------------------------------------------------------------
-- 0) CONFIG / CALENDAR
------------------------------------------------------------------------
local START, END = 62, 183 -- season window within the PSSC year
local DAYS_IN_YEAR = 183
local function getMonthDay(d)
if d <= 61 then
return "Atosiel", d
elseif d <= 122 then
return "Thalassiel", d - 61
else
return "Opsitheiel", d - 122
end
end
local function md2day(m,d) return (m-1)*61 + d end -- 61-day “months”
local function absDay(y,d) return y*DAYS_IN_YEAR + d end
------------------------------------------------------------------------
-- 1) TEAMS (keep your logos/titles/championships exactly)
------------------------------------------------------------------------
local teams = {
{ code="Allegro Symphonara", fullName="Allegro Symphonara", logo="[[File:AllegrosymphonaraB.png|20px]]", championships=4 },
{ code="Ashguard Pyralis", fullName="Ashguard Pyralis", logo="[[File:AshguardpyralisB.png|20px]]", championships=1 },
{ code="Aurelia Auric", fullName="Aurelia Auric", logo="[[File:AureliaAuricB.png|20px]]", championships=2 },
{ code="Brightpath Aureum", fullName="Brightpath Aureum", logo="[[File:BrightpathaureumB.png|20px]]", championships=2 },
{ code="Celestial Sancta Lunaris",fullName="Celestial Sancta Lunaris", logo="[[File:CelestialsanctaB.png|20px]]", championships=1 },
{ code="Delphica Windborne", fullName="Delphica Windborne", logo="[[File:DelphicawindborneB.png|20px]]", championships=1 },
{ code="Emberstone Vaeringheim", fullName="Emberstone Vaeringheim", logo="[[File:EmberstonevaeringheimB.png|20px]]", championships=3 },
{ code="Flameborne Erythros", fullName="Flameborne Erythros", logo="[[File:FlameborneerythrosB.png|20px]]", championships=1 },
{ code="Forgeward Nexa", fullName="Forgeward Nexa", logo="[[File:ForgewardnexaB.png|20px]]", championships=0 },
{ code="Hearthkeeper Koinon", fullName="Hearthkeeper Koinon", logo="[[File:HearthkeeperkoinonB.png|20px]]", championships=0 },
{ code="Ironbough Sylvapolis", fullName="Ironbough Sylvapolis", logo="[[File:IronboughSylvapolisB.png|20px]]", championships=1 },
{ code="Redcliff Citadel", fullName="Redcliff Citadel", logo="[[File:RedcliffCitadel.png|20px]]", championships=1 },
{ code="Riftwarden Acheron", fullName="Riftwarden Acheron", logo="[[File:RiftwardenAcheronB.png|20px]]", championships=3 },
{ code="Saluria Skylight", fullName="Saluria Skylight", logo="[[File:SaluriaSkylightB.png|20px]]", championships=0 },
{ code="Sandveil Somniumpolis", fullName="Sandveil Somniumpolis", logo="[[File:SandVeilSomniumB.png|20px]]", championships=1 },
{ code="Serena Tidesong", fullName="Serena Tidesong", logo="[[File:Serena TidesongB.png|20px]]", championships=2 },
{ code="Steppe Luminaria", fullName="Steppe Luminaria", logo="[[File:Steppe LuminariaB.png|20px]]", championships=4 },
{ code="Strider Myrene", fullName="Strider Myrene", logo="[[File:StriderMyreneB.png|20px]]", championships=1 },
{ code="Vaeringheim Pillar", fullName="Vaeringheim Pillar", logo="[[File:VaeringheimPillarB.png|20px]]", championships=5 },
{ code="Imperial Delphica", fullName="Imperial Delphica", logo="[[File:ImperialDelphicaB.png|20px]]", championships=1 },
{ code="Ascendant Aetherium", fullName="Ascendant Aetherium", logo="[[File:AscendantAetheriumB.png|20px]]", championships=2 },
{ code="Sufriya Stormwake", fullName="Sufriya Stormwake", logo="[[File:SufriyaStormwake.png|20px]]", championships=1 },
{ code="Pillarion Club Suncliff", fullName="Pillarion Club Suncliff", logo="[[File:PCSuncliff.png|20px]]", championships=0 },
{ code="Jogi Regiment", fullName="Jogi Regiment", logo="[[File:JogiRegiment.png|20px]]", championships=0 },
{ code="Vine Fleet Mylecia", fullName="Vine Fleet Mylecia", logo="[[File:VineFleetMylecia.png|20px]]", championships=0 },
{ code="Port of Blore Heath", fullName="Port of Blore Heath", logo="[[File:PortBloreHeath.png|20px]]", championships=0 },
{ code="Free State Abeis", fullName="Free State Abeis", logo="[[File:FreeStateAbeis.png|20px]]", championships=0 },
{ code="Jezeraah City", fullName="Jezeraah City", logo="[[File:JezeraahCity.png|20px]]", championships=0 },
{ code="Ourid Pegasi", fullName="Ourid Pegasi", logo="[[File:OuridPegasiLogo.png|20px]]", championships=0 },
{ code="Pillarion Club Caspazani",fullName="Pillarion Club Caspazani",logo="[[File:PCCaspazani.png|20px]]", championships=0 },
{ code="Amberwatch Slevik", fullName="Amberwatch Slevik", logo="[[File:AmberwatchSlevik.png|20px]]", championships=0 },
{ code="Fanghorn Rein", fullName="Fanghorn Rein", logo="[[File:FanghornRein.png|20px]]", championships=0 },
}
local TEAM_BY_CODE = {}
for _,t in ipairs(teams) do TEAM_BY_CODE[t.code] = t end
------------------------------------------------------------------------
-- 2) DIVISIONS (your final four quads of eight)
------------------------------------------------------------------------
local DIVISIONS = {
["Morovian Division"] = {
"Emberstone Vaeringheim","Vaeringheim Pillar","Imperial Delphica","Delphica Windborne",
"Allegro Symphonara","Steppe Luminaria","Jezeraah City","Saluria Skylight"
},
["Southern Strait Division"] = {
"Sufriya Stormwake","Vine Fleet Mylecia","Port of Blore Heath","Jogi Regiment",
"Free State Abeis","Sandveil Somniumpolis","Brightpath Aureum","Flameborne Erythros"
},
["Western Highlands Division"] = {
"Ashguard Pyralis","Aurelia Auric","Celestial Sancta Lunaris","Forgeward Nexa",
"Hearthkeeper Koinon","Ironbough Sylvapolis","Riftwarden Acheron","Serena Tidesong"
},
["Normarkian Division"] = {
"Redcliff Citadel","Strider Myrene","Ascendant Aetherium","Pillarion Club Suncliff",
"Ourid Pegasi","Pillarion Club Caspazani","Amberwatch Slevik","Fanghorn Rein"
}
}
local DIV_OF = {}
for div, list in pairs(DIVISIONS) do
for _, code in ipairs(list) do DIV_OF[code] = div end
end
------------------------------------------------------------------------
-- 3) DIVISION-WEIGHTED SCHEDULER
-- • In-division: each of 7 rivals twice (H/A) -> 14 matches
-- • Cross-division: the other 24 once -> 24 matches
-- = 38 total per club
------------------------------------------------------------------------
local function homeFirst(codeA, codeB)
-- deterministic “hash” to pick a home side for cross-division fixtures
local sA, sB = 0, 0
for i=1,#codeA do sA = (sA + string.byte(codeA,i)) % 9973 end
for i=1,#codeB do sB = (sB + string.byte(codeB,i)) % 9973 end
return sA > sB
end
local function buildPairings()
local intra, inter = {}, {}
-- in-division double round robin (H/A)
for _, list in pairs(DIVISIONS) do
for i=1,#list-1 do
for j=i+1,#list do
local A, B = TEAM_BY_CODE[list[i]], TEAM_BY_CODE[list[j]]
table.insert(intra, {home=A, away=B})
table.insert(intra, {home=B, away=A})
end
end
end
-- cross-division single round robin
local allCodes = {}
for _,t in ipairs(teams) do table.insert(allCodes, t.code) end
table.sort(allCodes) -- stable order
for ai=1,#allCodes-1 do
for bi=ai+1,#allCodes do
local ca, cb = allCodes[ai], allCodes[bi]
if DIV_OF[ca] ~= DIV_OF[cb] then
local a, b = TEAM_BY_CODE[ca], TEAM_BY_CODE[cb]
local hFirst = homeFirst(ca, cb)
table.insert(inter, {home = hFirst and a or b, away = hFirst and b or a})
end
end
end
-- sort (deterministic grouping for nicer pacing)
table.sort(intra, function(a,b)
if DIV_OF[a.home.code] ~= DIV_OF[b.home.code] then
return DIV_OF[a.home.code] < DIV_OF[b.home.code]
end
return a.home.code .. a.away.code < b.home.code .. b.away.code
end)
table.sort(inter, function(a,b)
local ka = DIV_OF[a.home.code] .. "|" .. DIV_OF[a.away.code]
local kb = DIV_OF[b.home.code] .. "|" .. DIV_OF[b.away.code]
if ka ~= kb then return ka < kb end
return a.home.code .. a.away.code < b.home.code .. b.away.code
end)
return intra, inter
end
local function packRounds(matches)
-- greedy packer so no club appears twice in a round
local rounds, used = {}, {}
local function canPlace(r, m)
return not (used[r] and (used[r][m.home.code] or used[r][m.away.code]))
end
for _, m in ipairs(matches) do
local placed = false
for r=1,#rounds do
if canPlace(r, m) then
rounds[r][#rounds[r]+1] = { m.home, m.away }
used[r] = used[r] or {}
used[r][m.home.code] = true
used[r][m.away.code] = true
placed = true
break
end
end
if not placed then
rounds[#rounds+1] = { { m.home, m.away } }
used[#rounds] = {}
used[#rounds][m.home.code] = true
used[#rounds][m.away.code] = true
end
end
return rounds
end
-- Build the master schedule once (deterministic)
local SCHED = (function()
local intra, inter = buildPairings()
-- simple interleave: one intra block, one inter block, repeat
local function chunk(arr, size)
local out = {}
for i=1,#arr,size do
local b = {}
for j=i, math.min(i+size-1, #arr) do b[#b+1] = arr[j] end
out[#out+1] = b
end
return out
end
-- block sizes chosen to avoid very long runs of a single type
local intraBlocks = chunk(intra, 64)
local interBlocks = chunk(inter, 128)
local merged = {}
local ia, ib = 1, 1
while ia <= #intraBlocks or ib <= #interBlocks do
if ia <= #intraBlocks then
for _,m in ipairs(intraBlocks[ia]) do merged[#merged+1] = m end
ia = ia + 1
end
if ib <= #interBlocks then
for _,m in ipairs(interBlocks[ib]) do merged[#merged+1] = m end
ib = ib + 1
end
end
return packRounds(merged)
end)()
local function generateSchedule() return SCHED end
-- Map rounds to PSSC dates across the season window
local function mapDays()
local totalDays = END - START + 1
local totalRnds = #SCHED
local iv = math.floor(totalDays / totalRnds)
local ex = totalDays % totalRnds
local m, d = {}, START
for i=1,totalRnds do
m[d] = i
d = d + iv + (ex > 0 and 1 or 0)
if ex > 0 then ex = ex - 1 end
end
return m
end
------------------------------------------------------------------------
-- 4) MATCH SIMULATION (deterministic by day & pairing)
------------------------------------------------------------------------
local function simScoreFor(h, a, seed)
math.randomseed(seed)
local p = math.random()
local hg, ag
if p < 0.1 then
hg = math.random(0, 12); ag = hg
else
local wh, wa = h.championships + 1, a.championships + 1
local rem = (p - 0.1) / 0.9
local homeWin= rem < (wh / (wh + wa))
local winG = math.random(1, 12)
local loseG = math.random(0, math.min(winG - 1, 12))
if homeWin then hg, ag = winG, loseG else hg, ag = loseG, winG end
end
return hg, ag
end
local function simulateDayResults(day)
local dm = mapDays()
local idx = dm[day]
if not idx then return {} end
local out = {}
for k, m in ipairs(SCHED[idx]) do
local h, a = m[1], m[2]
local salt = day*1000 + k + string.byte(h.code,1) + string.byte(a.code,1)
local hg, ag = simScoreFor(h, a, salt)
out[#out+1] = { home=h, away=a, hg=hg, ag=ag }
end
return out
end
------------------------------------------------------------------------
-- 5) TODAY / NEXT MATCHDAY RENDER
------------------------------------------------------------------------
local function renderRound(curDay, year)
local dm = mapDays()
local target = curDay
local upcoming= false
if not dm[target] then
for d = curDay + 1, END do
if dm[d] then target = d; upcoming = true; break end
end
end
if not dm[target] then
return "'''No more matches this season.'''"
end
local prefix = upcoming and "'''Next matchday:''' " or ""
local mon, dom = getMonthDay(target)
local label = dom .. " " .. mon .. " " .. year .. " PSSC"
local rows = {}
if not upcoming then
for _, r in ipairs(simulateDayResults(target)) do
local h, a, hg, ag = r.home, r.away, r.hg, r.ag
local hc = (hg > ag and "green") or (hg < ag and "red") or "yellow"
local ac = (ag > hg and "green") or (ag < hg and "red") or "yellow"
local hs = string.format("<span style='color:%s;'>'''%d'''</span>", hc, hg)
local as = string.format("<span style='color:%s;'>'''%d'''</span>", ac, ag)
rows[#rows+1] =
string.format("%s %s %s vs %s %s %s",
h.logo, hs, h.fullName,
a.logo, as, a.fullName
)
end
else
local idx = dm[target]
for _, m in ipairs(SCHED[idx]) do
local h, a = m[1], m[2]
rows[#rows+1] =
string.format("%s %s vs %s %s",
h.logo, h.fullName,
a.logo, a.fullName
)
end
end
return string.format(
"|-\n| %s%d || %s || %s",
prefix, target, label, table.concat(rows, "<br>")
)
end
function p.renderSchedule(frame)
local curr = date.getCurrentDate()
local day = tonumber(curr:match('^(%d+),')) or 0
local year = tonumber(curr:match(', (%d+) PSSC')) or 0
return table.concat({
'{| class="wikitable sortable"',
'! Day !! Date !! Matches',
renderRound(day, year),
'|}'
}, "\n")
end
------------------------------------------------------------------------
-- 6) DIVISION STANDINGS
------------------------------------------------------------------------
function p.renderStandings(frame)
local curr = date.getCurrentDate()
local today = tonumber(curr:match("^(%d+),")) or 0
local last = math.min(today, END)
-- init records
local recs = {}
for _, t in ipairs(teams) do
recs[t.code] = { team=t, W=0, D=0, L=0, Pts=0, PF=0, PA=0, PD=0 }
end
-- accumulate results up to 'last'
local dm = mapDays()
for d = START, last do
local idx = dm[d]
if idx then
for k, m in ipairs(SCHED[idx]) do
local h, a = m[1], m[2]
local salt = d*1000 + k + string.byte(h.code,1) + string.byte(a.code,1)
local hg, ag = simScoreFor(h, a, salt)
local H, A = recs[h.code], recs[a.code]
H.PF, H.PA = H.PF + hg, H.PA + ag
A.PF, A.PA = A.PF + ag, A.PA + hg
if hg > ag then
H.W, H.Pts = H.W + 1, H.Pts + 3
A.L = A.L + 1
elseif hg < ag then
A.W, A.Pts = A.W + 1, A.Pts + 3
H.L = H.L + 1
else
H.D, H.Pts = H.D + 1, H.Pts + 1
A.D, A.Pts = A.D + 1, A.Pts + 1
end
end
end
end
-- bucket by division
local buckets = {}
for div,_ in pairs(DIVISIONS) do buckets[div] = {} end
for code, r in pairs(recs) do
r.PD = r.PF - r.PA
local div = DIV_OF[code] or "—"
buckets[div] = buckets[div] or {}
table.insert(buckets[div], r)
end
local function cmp(a,b)
if a.Pts ~= b.Pts then return a.Pts > b.Pts end
if a.PD ~= b.PD then return a.PD > b.PD end
if a.W ~= b.W then return a.W > b.W end
return a.team.fullName < b.team.fullName
end
for div, list in pairs(buckets) do table.sort(list, cmp) end
-- render four separate tables
local order = {
"Morovian Division","Southern Strait Division",
"Western Highlands Division","Normarkian Division"
}
local out = {}
for _, div in ipairs(order) do
local list = buckets[div] or {}
out[#out+1] = "== " .. div .. " Standings =="
out[#out+1] = '{| class="wikitable sortable"'
out[#out+1] = "! Pos !! Team !! W !! D !! L !! Pts !! PF !! PA !! PD"
for i, rec in ipairs(list) do
out[#out+1] = "|-"
out[#out+1] =
"| " .. i
.. " || " .. rec.team.logo .. " " .. rec.team.fullName
.. " || " .. rec.W
.. " || " .. rec.D
.. " || " .. rec.L
.. " || " .. rec.Pts
.. " || " .. rec.PF
.. " || " .. rec.PA
.. " || " .. rec.PD
end
out[#out+1] = "|}"
out[#out+1] = ""
end
return table.concat(out, "\n")
end
------------------------------------------------------------------------
-- 7) PREVIOUS RESULTS (last 5 matchdays that actually exist)
------------------------------------------------------------------------
function p.renderPreviousResults(frame)
local curr = date.getCurrentDate()
local today = tonumber(curr:match("^(%d+),")) or 0
local year = tonumber(curr:match(", (%d+) PSSC")) or 0
local dm = mapDays()
local played = {}
for d = today - 1, START, -1 do
if dm[d] then played[#played+1] = d end
if #played == 5 then break end
end
local out = {
'{| class="wikitable sortable"',
'! Date !! Home !! Score !! Away'
}
for _, d in ipairs(played) do
local mon, dom = getMonthDay(d)
local dateLabel = dom .. " " .. mon .. " " .. year .. " PSSC"
local idx = dm[d]
for k, m in ipairs(SCHED[idx]) do
local h, a = m[1], m[2]
local salt = d*1000 + k + string.byte(h.code,1) + string.byte(a.code,1)
local hg, ag = simScoreFor(h, a, salt)
local hs = "'''" .. hg .. "-" .. ag .. "'''"
out[#out+1] = "|-"
out[#out+1] =
"| " .. dateLabel
.. " || " .. h.logo .. " " .. h.fullName
.. " || " .. hs
.. " || " .. a.logo .. " " .. a.fullName
end
end
out[#out+1] = "|}"
return table.concat(out, "\n")
end
------------------------------------------------------------------------
-- 8) PLAYOFFS (kept from your original, lightly wired)
-- • Still uses a static seed list; can be upgraded to pull top-12 per division.
------------------------------------------------------------------------
local PO_YEAR = 52 -- post-season year
local PO_FIRST = md2day(1,4) -- 4 Atosiel 52 PSSC (Play-in day)
local PO_IV = 3 -- days between rounds
-- seeding list (top-12 of final table) — unchanged
local seedTeams = {
teams[7], teams[17], teams[13], teams[19], teams[1], teams[21], -- 1-6
teams[12],teams[4], teams[3], teams[18], teams[16], teams[5] -- 7-12
}
-- play-in (9 v 12, 10 v 11)
local PLAYIN = {
{ seedTeams[9], seedTeams[12] },
{ seedTeams[10], seedTeams[11] }
}
local function playScore(A,B,salt)
math.randomseed(salt)
local p = math.random()
if p < 0.10 then local g=math.random(0,12); return g,g end
local wa,wb = A.championships+1,B.championships+1
local home = (p-0.10)/0.90 < wa/(wa+wb)
local hi = math.random(1,12)
local lo = math.random(0, math.min(hi-1,12))
return home and hi or lo, home and lo or hi
end
local W = { PI={}, R8={}, R4={}, SF={} }
local function mkRow(tag, absDate, H, A, salt, absNow)
local hs, as, win = '—','—','—'
if absNow >= absDate and H.fullName~='TBD' and A.fullName~='TBD' then
local h,a = playScore(H,A,salt)
hs,as = string.format("'''%d-%d'''",h,a), nil
local Wn = (h>a) and H or A
win = Wn.logo .. ' ' .. Wn.fullName
as = '' -- keep the combined cell already set above
hs = string.format("'''%d-%d'''",h,a)
end
return string.format(
"|-\n| %s || %s %s || %s || %s %s || %s",
tag, H.logo,H.fullName, (hs or '—'), A.logo,A.fullName, win)
end
function p.renderPlayoffSchedule(frame)
local out = {
'{| class="wikitable sortable"',
'! Round !! Date !! Match-up'
}
local function dateCell(day)
local mon,dom = getMonthDay(day)
return dom .. ' ' .. mon .. ' ' .. PO_YEAR .. ' PSSC'
end
local function add(tag, day, H, A)
out[#out+1] = '|-'
out[#out+1] = string.format(
'| %s || %s || %s %s vs %s %s',
tag, dateCell(day), H.logo,H.fullName, A.logo,A.fullName)
end
-- Play-in
for i,pair in ipairs(PLAYIN) do add('Play-in-'..i, PO_FIRST, pair[1], pair[2]) end
-- banners for remaining rounds
local banners = {
{ 'Round-of-8', 1 },
{ 'Round-of-4', 2 },
{ 'Semi-finals (Seeds 1-2 BYE)', 3 },
{ 'Final', 4 }
}
for _,b in ipairs(banners) do
local d = PO_FIRST + PO_IV * b[2]
out[#out+1] = '|-'
out[#out+1] = '| colspan="3" | ' .. "'''" .. b[1] .. ":'''" ..
' ' .. dateCell(d)
end
out[#out+1] = '|}'
return table.concat(out, '\n')
end
function p.renderPlayoffResults(frame)
local now = date.getCurrentDate()
local dNow = tonumber(now:match('^(%d+),')) or 0
local yNow = tonumber(now:match(', (%d+) PSSC')) or 0
local absNow = absDay(yNow,dNow)
local rows = {
'{| class="wikitable sortable"',
'! Round !! Home !! Score !! Away !! Winner'
}
-- Play-in
local absPI = absDay(PO_YEAR, PO_FIRST)
for i,pair in ipairs(PLAYIN) do
if absNow >= absPI then
local g1,g2 = playScore(pair[1],pair[2],absPI*100+i)
W.PI[i] = (g1>g2) and pair[1] or pair[2]
end
rows[#rows+1] = (function()
local hs, as, win = '—','—','—'
if absNow >= absPI then
local g1,g2 = playScore(pair[1],pair[2],absPI*100+i)
local Wn = (g1>g2) and pair[1] or pair[2]
win = Wn.logo .. ' ' .. Wn.fullName
hs = string.format("'''%d-%d'''", g1, g2)
end
return string.format("|-\n| %s || %s %s || %s || %s %s || %s",
'PI-'..i, pair[1].logo, pair[1].fullName, hs, pair[2].logo, pair[2].fullName, win)
end)()
end
-- Round-of-8
local absR8 = absPI + PO_IV
local pool = { seedTeams[3],seedTeams[4],seedTeams[5],seedTeams[6],
seedTeams[7],seedTeams[8],
W.PI[1] or {logo='—',fullName='TBD'},
W.PI[2] or {logo='—',fullName='TBD'} }
table.sort(pool, function(a,b)
for _,t in ipairs(seedTeams) do
if t==a then return true elseif t==b then return false end
end
end)
local R8 = {}
for i=1,4 do R8[i] = { pool[i], pool[#pool - (i-1)] } end
for i,pair in ipairs(R8) do
local H,A = pair[1], pair[2]
local hs, win = '—', '—'
if absNow >= absR8 and A.fullName~='TBD' then
local g1,g2 = playScore(H,A,absR8*100+i)
W.R8[i] = (g1>g2) and H or A
hs = string.format("'''%d-%d'''", g1, g2)
win = (g1>g2 and H or A).logo .. ' ' .. (g1>g2 and H or A).fullName
end
rows[#rows+1] = string.format("|-\n| %s || %s %s || %s || %s %s || %s",
'R-8-'..i, H.logo,H.fullName, hs, A.logo,A.fullName, win)
end
-- Round-of-4 (re-seed)
local absR4 = absR8 + PO_IV
local R4 = {}
if W.R8[1] and W.R8[4] then
local w = { W.R8[1],W.R8[2],W.R8[3],W.R8[4] }
table.sort(w, function(a,b)
for _,t in ipairs(seedTeams) do
if t==a then return true elseif t==b then return false end
end
end)
R4 = { {w[1],w[4]}, {w[2],w[3]} }
end
for i=1,2 do
local H = R4[i] and R4[i][1] or {logo='—',fullName='TBD'}
local A = R4[i] and R4[i][2] or {logo='—',fullName='TBD'}
local hs, win = '—', '—'
if absNow >= absR4 and R4[i] then
local g1,g2 = playScore(H,A,absR4*100+i)
W.R4[i] = (g1>g2) and H or A
hs = string.format("'''%d-%d'''", g1, g2)
win = (g1>g2 and H or A).logo .. ' ' .. (g1>g2 and H or A).fullName
end
rows[#rows+1] = string.format("|-\n| %s || %s %s || %s || %s %s || %s",
'R-4-'..i, H.logo,H.fullName, hs, A.logo,A.fullName, win)
end
-- Semi-finals (Seeds 1–2 BYE)
local absSF = absR4 + PO_IV
local SF = {
{ seedTeams[1], W.R4[2] or {logo='—',fullName='TBD'} },
{ seedTeams[2], W.R4[1] or {logo='—',fullName='TBD'} }
}
for i=1,2 do
local H,A = SF[i][1], SF[i][2]
local hs, win = '—', '—'
if absNow >= absSF and A.fullName~='TBD' then
local g1,g2 = playScore(H,A,absSF*100+i)
W.SF[i] = (g1>g2) and H or A
hs = string.format("'''%d-%d'''", g1, g2)
win = (g1>g2 and H or A).logo .. ' ' .. (g1>g2 and H or A).fullName
end
rows[#rows+1] = string.format("|-\n| %s || %s %s || %s || %s %s || %s",
'SF-'..i, H.logo,H.fullName, hs, A.logo,A.fullName, win)
end
-- Final
local absF = absSF + PO_IV
local FH = W.SF[1] or {logo='—',fullName='TBD'}
local FA = W.SF[2] or {logo='—',fullName='TBD'}
local hs, win = '—', '—'
if FH.fullName~='TBD' and FA.fullName~='TBD' and absNow >= absF then
local g1,g2 = playScore(FH,FA,absF*100+1)
hs = string.format("'''%d-%d'''", g1, g2)
win = (g1>g2 and FH or FA).logo .. ' ' .. (g1>g2 and FH or FA).fullName
end
rows[#rows+1] = string.format("|-\n| %s || %s %s || %s || %s %s || %s",
'Final', FH.logo,FH.fullName, hs, FA.logo,FA.fullName, win)
rows[#rows+1] = '|}'
return table.concat(rows, '\n')
end
------------------------------------------------------------------------
-- EXPORT
------------------------------------------------------------------------
return p