Module:PillSeasonSchedule: Difference between revisions
From MicrasWiki
Jump to navigationJump to search
NewZimiaGov (talk | contribs) No edit summary |
NewZimiaGov (talk | contribs) No edit summary |
||
| Line 316: | Line 316: | ||
local DAYS_IN_YEAR = 183 | local DAYS_IN_YEAR = 183 | ||
local function md2day(m, d) return (m - 1) * 61 + d 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 | local function absDay(y, d) return y * DAYS_IN_YEAR + d end | ||
local PO_YEAR = 52 | local PO_YEAR = 52 -- 52 PSSC postseason | ||
local PO_FIRST = md2day( | local PO_FIRST = md2day(1, 1) -- 1 Atosiel 52 PSSC ← starts here | ||
local PO_IV = 3 | local PO_IV = 3 -- gap between rounds (in days) | ||
-- seeds (top-12 of final | -- seeds (top-12 of final standings, already available in teams[]) | ||
local seedTeams = { | local seedTeams = { | ||
teams[7], teams[17], teams[13], teams[19], teams[1], teams[21], | teams[7], teams[17], teams[13], teams[19], teams[1], teams[21], | ||
| Line 329: | Line 329: | ||
} | } | ||
-- Ro12 pairings: 1-12, 2-11 … 6-7 | -- Ro12 pairings: 1-12, 2-11, … , 6-7 | ||
local RO12 = { | local RO12 = { | ||
{ seedTeams[1], seedTeams[12] }, | { seedTeams[1], seedTeams[12] }, | ||
| Line 339: | Line 339: | ||
} | } | ||
-- one-off | -- one-off match simulator (slight bias toward more titles) | ||
local function playScore(A, B, seed) | local function playScore(A, B, seed) | ||
math.randomseed(seed) | math.randomseed(seed) | ||
local p = math.random() | local p = math.random() | ||
if p < 0.10 then local g = math.random(0, 12); return g, g end | 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 wa, wb = A.championships + 1, B.championships + 1 | ||
local home = (p - 0.10) / 0.90 < wa / (wa + wb) | local home = (p - 0.10) / 0.90 < wa / (wa + wb) | ||
| Line 354: | Line 356: | ||
------------------------------------------------------------------------ | ------------------------------------------------------------------------ | ||
-- 9.1 Play-off | -- 9.1 Play-off schedule (blank scores) | ||
------------------------------------------------------------------------ | ------------------------------------------------------------------------ | ||
function p.renderPlayoffSchedule(frame) | function p.renderPlayoffSchedule(frame) | ||
| Line 364: | Line 366: | ||
local function addRow(tag, dayOfYear, H, A) | local function addRow(tag, dayOfYear, H, A) | ||
local mon, dom = getMonthDay(dayOfYear) | local mon, dom = getMonthDay(dayOfYear) | ||
table.insert(out, | table.insert(out, '|-') | ||
table.insert(out, | table.insert(out, | ||
string.format("| %s || %d %s %d PSSC || %s %s vs %s %s", | string.format("| %s || %d %s %d PSSC || %s %s vs %s %s", | ||
tag, dom, mon, PO_YEAR, H.logo, H.fullName, A.logo, A.fullName)) | tag, dom, mon, PO_YEAR, H.logo, H.fullName, A.logo, A.fullName)) | ||
end | end | ||
| Line 372: | Line 374: | ||
-- Round of 12 | -- Round of 12 | ||
for i, pr in ipairs(RO12) do | for i, pr in ipairs(RO12) do | ||
addRow( | addRow('Ro12-' .. i, PO_FIRST, pr[1], pr[2]) | ||
end | end | ||
-- placeholders | -- simple placeholders (no scores) | ||
local dQF = PO_FIRST + PO_IV | local dQF = PO_FIRST + PO_IV | ||
local mon, dom = getMonthDay(dQF) | local mon, dom = getMonthDay(dQF) | ||
table.insert(out, | table.insert(out, '|-') | ||
table.insert(out, | table.insert(out, | ||
"| colspan=\"3\" | '''Quarter-finals:''' " | "| colspan=\"3\" | '''Quarter-finals:''' " | ||
.. dom .. | .. dom .. ' ' .. mon .. ' ' .. PO_YEAR .. ' PSSC') | ||
local dSF = dQF + PO_IV | local dSF = dQF + PO_IV | ||
mon, dom = getMonthDay(dSF) | mon, dom = getMonthDay(dSF) | ||
table.insert(out, | table.insert(out, '|-') | ||
table.insert(out, | table.insert(out, | ||
"| colspan=\"3\" | '''Semi-finals:''' " | "| colspan=\"3\" | '''Semi-finals:''' " | ||
.. dom .. | .. dom .. ' ' .. mon .. ' ' .. PO_YEAR .. ' PSSC') | ||
local dF = dSF + PO_IV | local dF = dSF + PO_IV | ||
mon, dom = getMonthDay(dF) | mon, dom = getMonthDay(dF) | ||
table.insert(out, | table.insert(out, '|-') | ||
table.insert(out, | table.insert(out, | ||
"| colspan=\"3\" | '''Final:''' " | "| colspan=\"3\" | '''Final:''' " | ||
.. dom .. | .. dom .. ' ' .. mon .. ' ' .. PO_YEAR .. ' PSSC') | ||
table.insert(out, | table.insert(out, '|}') | ||
return table.concat(out, | return table.concat(out, '\n') | ||
end | end | ||
------------------------------------------------------------------------ | ------------------------------------------------------------------------ | ||
-- 9.2 Play-off | -- 9.2 Play-off results (fills in once a round’s date has passed) | ||
------------------------------------------------------------------------ | ------------------------------------------------------------------------ | ||
function p.renderPlayoffResults(frame) | function p.renderPlayoffResults(frame) | ||
local | -- current absolute day | ||
local dNow = tonumber( | local nowStr = date.getCurrentDate() | ||
local yNow = tonumber( | local dNow = tonumber(nowStr:match('^(%d+),')) or 0 | ||
local yNow = tonumber(nowStr:match(', (%d+) PSSC')) or 0 | |||
local absNow = absDay(yNow, dNow) | local absNow = absDay(yNow, dNow) | ||
-- helper to | -- helper to format one row | ||
local function row( | local function row(rTag, absKey, H, A, seed) | ||
local hs, as, | local hs, as, winnerText = '—', '—', '—' | ||
if absNow >= absKey and H.fullName ~= | if absNow >= absKey and H.fullName ~= 'TBD' and A.fullName ~= 'TBD' then | ||
hs, as = playScore(H, A, | hs, as = playScore(H, A, seed) | ||
local W = (hs > as) and H or A | local W = (hs > as) and H or A | ||
winnerText = W.logo .. ' ' .. W.fullName | |||
hs | hs = string.format("'''%d-%d'''", hs, as) | ||
end | end | ||
return string.format( | return string.format( | ||
'|-\n| %s || %s %s || %s || %s %s || %s', | |||
rTag, H.logo, H.fullName, hs, A.logo, A.fullName, winnerText) | |||
end | end | ||
local | local tbl = { | ||
'{| class="wikitable sortable"', | '{| class="wikitable sortable"', | ||
'! Round !! Home !! Score !! Away !! Winner' | '! Round !! Home !! Score !! Away !! Winner' | ||
} | } | ||
-- | ------------------------------------------------------------------ Ro12 | ||
local absRo12 = absDay(PO_YEAR, PO_FIRST) | local absRo12 = absDay(PO_YEAR, PO_FIRST) | ||
for i, pr in ipairs(RO12) do | for i, pr in ipairs(RO12) do | ||
-- decide winner if the date has passed | |||
if absNow >= absRo12 then | if absNow >= absRo12 then | ||
local | local g1, g2 = playScore(pr[1], pr[2], absRo12 * 100 + i) | ||
winners.Ro12[i] = (g1 > g2) and pr[1] or pr[2] | |||
winners.Ro12[i] = ( | |||
end | end | ||
table.insert(tbl, row('Ro12-' .. i, absRo12, pr[1], pr[2], absRo12 * 100 + i)) | |||
end | end | ||
-- Quarter-finals | ------------------------------------------------------------------ Quarter-finals | ||
local absQF = absRo12 + PO_IV | local absQF = absRo12 + PO_IV | ||
local qualified, byes = {}, {} | local qualified, byes = {}, {} | ||
for _, w in ipairs(winners.Ro12) do if w then table.insert(qualified, w) end end | for _, w in ipairs(winners.Ro12) do if w then table.insert(qualified, w) end end | ||
-- order by original seed | |||
table.sort(qualified, function(a, b) | table.sort(qualified, function(a, b) | ||
for _, t in ipairs(seedTeams) do | for _, t in ipairs(seedTeams) do | ||
| Line 451: | Line 455: | ||
end | end | ||
end) | end) | ||
if #qualified >= 6 then byes = { qualified[1], qualified[2] } end | if #qualified >= 6 then byes = { qualified[1], qualified[2] } end | ||
if #qualified >= 6 then | if #qualified >= 6 then | ||
table.remove(qualified, 1); table.remove(qualified, 1) | table.remove(qualified, 1); table.remove(qualified, 1) | ||
end | end | ||
local QFpairs = { | local QFpairs = { | ||
{ qualified[1], qualified[#qualified] }, | { qualified[1], qualified[#qualified] }, | ||
| Line 461: | Line 467: | ||
for i = 1, 2 do | for i = 1, 2 do | ||
local H = QFpairs[i] and QFpairs[i][1] or { logo = | local H = QFpairs[i] and QFpairs[i][1] or { logo = '—', fullName = 'TBD' } | ||
local A = QFpairs[i] and QFpairs[i][2] or { logo = | local A = QFpairs[i] and QFpairs[i][2] or { logo = '—', fullName = 'TBD' } | ||
if absNow >= absQF and QFpairs[i] then | if absNow >= absQF and QFpairs[i] then | ||
local | local g1, g2 = playScore(H, A, absQF * 100 + i) | ||
winners.QF[i] = (g1 > g2) and H or A | |||
winners.QF[i] = ( | |||
end | end | ||
table.insert(tbl, row('QF-' .. i, absQF, H, A, absQF * 100 + i)) | |||
end | end | ||
-- Semi-finals | ------------------------------------------------------------------ Semi-finals | ||
local absSF = absQF + PO_IV | local absSF = absQF + PO_IV | ||
local SFpairs = {} | local SFpairs = {} | ||
| Line 483: | Line 488: | ||
for i = 1, 2 do | for i = 1, 2 do | ||
local H = SFpairs[i] and SFpairs[i][1] or { logo = | local H = SFpairs[i] and SFpairs[i][1] or { logo = '—', fullName = 'TBD' } | ||
local A = SFpairs[i] and SFpairs[i][2] or { logo = | local A = SFpairs[i] and SFpairs[i][2] or { logo = '—', fullName = 'TBD' } | ||
if absNow >= absSF and SFpairs[i] then | if absNow >= absSF and SFpairs[i] then | ||
local | local g1, g2 = playScore(H, A, absSF * 100 + i) | ||
winners.SF[i] = (g1 > g2) and H or A | |||
winners.SF[i] = ( | |||
end | end | ||
table.insert(tbl, row('SF-' .. i, absSF, H, A, absSF * 100 + i)) | |||
end | end | ||
-- Final | ------------------------------------------------------------------ Final | ||
local absF = absSF + PO_IV | local absF = absSF + PO_IV | ||
local FH = winners.SF[1] or { logo = | local FH = winners.SF[1] or { logo = '—', fullName = 'TBD' } | ||
local FA = winners.SF[2] or { logo = | local FA = winners.SF[2] or { logo = '—', fullName = 'TBD' } | ||
table.insert( | table.insert(tbl, row('Final', absF, FH, FA, absF * 100 + 1)) | ||
table.insert( | table.insert(tbl, '|}') | ||
return table.concat( | return table.concat(tbl, '\n') | ||
end | end | ||
Revision as of 16:40, 23 August 2025
Documentation for this module may be created at Module:PillSeasonSchedule/doc
-- Module:PillSeasonSchedule
local p = {}
local date = require('Module:BassaridianCalendar')
-- 1) TEAM DEFINITIONS (20px thumbnails)
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=3 },
{ 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 }
}
-- 2) DOUBLE ROUND‑ROBIN GENERATOR
local function generateSchedule()
local n = #teams
assert(n % 2 == 0)
local pool = {}
for i = 1, n do pool[i] = teams[i] end
local fixed = table.remove(pool, 1)
local rounds = {}
for r = 1, n - 1 do
local pairings = {}
if r % 2 == 1 then
table.insert(pairings, { fixed, pool[1] })
else
table.insert(pairings, { pool[1], fixed })
end
for i = 2, n / 2 do
local t1, t2 = pool[i], pool[#pool - (i - 2)]
if i % 2 == 0 then
table.insert(pairings, { t1, t2 })
else
table.insert(pairings, { t2, t1 })
end
end
rounds[r] = pairings
table.insert(pool, 1, table.remove(pool))
end
for r = 1, n - 1 do
local rev = {}
for _, m in ipairs(rounds[r]) do
table.insert(rev, { m[2], m[1] })
end
rounds[n - 1 + r] = rev
end
return rounds
end
-- 3) MAP ROUNDS TO PSSC DAYS
local START, END = 62, 183
local function mapDays()
local total = END - START + 1
local rounds = (#teams - 1) * 2
local iv = math.floor(total / rounds)
local ex = total % rounds
local m, d = {}, START
for i = 1, rounds 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) SIMULATE RESULTS FOR A DAY
local function simulateDayResults(day)
local sched = generateSchedule()
local dm = mapDays()
local idx = dm[day]
if not idx then return {} end
math.randomseed(day)
local out = {}
for _, m in ipairs(sched[idx]) do
local h, a = m[1], m[2]
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
table.insert(out, { home = h, away = a, hg = hg, ag = ag })
end
return out
end
-- helper: convert PSSC day → month/day
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
-- 5) RENDER TODAY or NEXT MATCHDAY
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)
table.insert(rows,
string.format("%s %s %s vs %s %s %s",
h.logo, hs, h.fullName,
a.logo, as, a.fullName
)
)
end
else
local sched = generateSchedule()
local idx = dm[target]
for _, m in ipairs(sched[idx]) do
local h, a = m[1], m[2]
table.insert(rows,
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
-- 6) Invoke: schedule
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
-- 7) CURRENT STANDINGS (with logos and conditional row coloring)
function p.renderStandings(frame)
local curr = date.getCurrentDate()
local today = tonumber(curr:match("^(%d+),")) or 0
local last = math.min(today, END)
local recs = {}
-- initialize records
for _, t in ipairs(teams) do
recs[t.code] = { team = t, W = 0, D = 0, L = 0, Pts = 0, PF = 0, PA = 0 }
end
-- accumulate W/D/L and points
for d = START, last do
for _, r in ipairs(simulateDayResults(d)) do
local hR, aR = recs[r.home.code], recs[r.away.code]
hR.PF, hR.PA = hR.PF + r.hg, hR.PA + r.ag
aR.PF, aR.PA = aR.PF + r.ag, aR.PA + r.hg
if r.hg > r.ag then
hR.W, hR.Pts = hR.W + 1, hR.Pts + 3
aR.L = aR.L + 1
elseif r.hg < r.ag then
aR.W, aR.Pts = aR.W + 1, aR.Pts + 3
hR.L = hR.L + 1
else
hR.D, hR.Pts = hR.D + 1, hR.Pts + 1
aR.D, aR.Pts = aR.D + 1, aR.Pts + 1
end
end
end
-- prepare sorted list
local list = {}
for _, rec in pairs(recs) do
rec.PD = rec.PF - rec.PA
table.insert(list, rec)
end
table.sort(list, function(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)
-- render table with conditional styling
local out = {
"{| class=\"wikitable sortable\"",
"! Pos !! Team !! W !! D !! L !! Pts !! PF !! PA !! PD"
}
for i, rec in ipairs(list) do
-- determine background color
local style = ""
if i <= 8 then
style = ' style="background-color:#ccffcc;"' -- light green
elseif i >= 9 and i <= 12 then
style = ' style="background-color:#cce5ff;"' -- light blue
elseif i >= 24 and i <= 28 then
style = ' style="background-color:#ffcccc;"' -- light red
end
-- insert row
table.insert(out, "|-" .. style)
table.insert(out,
"| " .. i
.. " || " .. rec.team.logo .. " " .. rec.team.fullName
.. " || " .. rec.W
.. " || " .. rec.D
.. " || " .. rec.L
.. " || " .. rec.Pts
.. " || " .. rec.PF
.. " || " .. rec.PA
.. " || " .. rec.PD
)
end
table.insert(out, "|}")
return table.concat(out, "\n")
end
-- 8) PREVIOUS RESULTS: shows the last 5 matchdays
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 table.insert(played, d) end
end
local recent = {}
for i = 1, math.min(5, #played) do
table.insert(recent, played[i])
end
local out = {
"{| class=\"wikitable sortable\"",
"! Date !! Home !! Score !! Away"
}
for _, d in ipairs(recent) do
local mon, dom = getMonthDay(d)
local dateLabel = dom .. " " .. mon .. " " .. year .. " PSSC"
for _, r in ipairs(simulateDayResults(d)) do
local hs = "'''" .. r.hg .. "-" .. r.ag .. "'''"
table.insert(out, "|-")
table.insert(out,
"| " .. dateLabel
.. " || " .. r.home.logo .. " " .. r.home.fullName
.. " || " .. hs
.. " || " .. r.away.logo .. " " .. r.away.fullName
)
end
end
table.insert(out, "|}")
return table.concat(out, "\n")
end
-- return export table
-- 9) 12-TEAM PLAY-OFFS (Ro12 ➜ QF ➜ SF ➜ Final)
------------------------------------------------------------------------
local DAYS_IN_YEAR = 183
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
local PO_YEAR = 52 -- 52 PSSC postseason
local PO_FIRST = md2day(1, 1) -- 1 Atosiel 52 PSSC ← starts here
local PO_IV = 3 -- gap between rounds (in days)
-- seeds (top-12 of final standings, already available in teams[])
local seedTeams = {
teams[7], teams[17], teams[13], teams[19], teams[1], teams[21],
teams[12], teams[4], teams[3], teams[18], teams[16], teams[5]
}
-- Ro12 pairings: 1-12, 2-11, … , 6-7
local RO12 = {
{ seedTeams[1], seedTeams[12] },
{ seedTeams[2], seedTeams[11] },
{ seedTeams[3], seedTeams[10] },
{ seedTeams[4], seedTeams[9] },
{ seedTeams[5], seedTeams[8] },
{ seedTeams[6], seedTeams[7] }
}
-- one-off match simulator (slight bias toward more titles)
local function playScore(A, B, seed)
math.randomseed(seed)
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 winners = { Ro12 = {}, QF = {}, SF = {} }
------------------------------------------------------------------------
-- 9.1 Play-off schedule (blank scores)
------------------------------------------------------------------------
function p.renderPlayoffSchedule(frame)
local out = {
'{| class="wikitable sortable"',
'! Round !! Date !! Match-up'
}
local function addRow(tag, dayOfYear, H, A)
local mon, dom = getMonthDay(dayOfYear)
table.insert(out, '|-')
table.insert(out,
string.format("| %s || %d %s %d PSSC || %s %s vs %s %s",
tag, dom, mon, PO_YEAR, H.logo, H.fullName, A.logo, A.fullName))
end
-- Round of 12
for i, pr in ipairs(RO12) do
addRow('Ro12-' .. i, PO_FIRST, pr[1], pr[2])
end
-- simple placeholders (no scores)
local dQF = PO_FIRST + PO_IV
local mon, dom = getMonthDay(dQF)
table.insert(out, '|-')
table.insert(out,
"| colspan=\"3\" | '''Quarter-finals:''' "
.. dom .. ' ' .. mon .. ' ' .. PO_YEAR .. ' PSSC')
local dSF = dQF + PO_IV
mon, dom = getMonthDay(dSF)
table.insert(out, '|-')
table.insert(out,
"| colspan=\"3\" | '''Semi-finals:''' "
.. dom .. ' ' .. mon .. ' ' .. PO_YEAR .. ' PSSC')
local dF = dSF + PO_IV
mon, dom = getMonthDay(dF)
table.insert(out, '|-')
table.insert(out,
"| colspan=\"3\" | '''Final:''' "
.. dom .. ' ' .. mon .. ' ' .. PO_YEAR .. ' PSSC')
table.insert(out, '|}')
return table.concat(out, '\n')
end
------------------------------------------------------------------------
-- 9.2 Play-off results (fills in once a round’s date has passed)
------------------------------------------------------------------------
function p.renderPlayoffResults(frame)
-- current absolute day
local nowStr = date.getCurrentDate()
local dNow = tonumber(nowStr:match('^(%d+),')) or 0
local yNow = tonumber(nowStr:match(', (%d+) PSSC')) or 0
local absNow = absDay(yNow, dNow)
-- helper to format one row
local function row(rTag, absKey, H, A, seed)
local hs, as, winnerText = '—', '—', '—'
if absNow >= absKey and H.fullName ~= 'TBD' and A.fullName ~= 'TBD' then
hs, as = playScore(H, A, seed)
local W = (hs > as) and H or A
winnerText = W.logo .. ' ' .. W.fullName
hs = string.format("'''%d-%d'''", hs, as)
end
return string.format(
'|-\n| %s || %s %s || %s || %s %s || %s',
rTag, H.logo, H.fullName, hs, A.logo, A.fullName, winnerText)
end
local tbl = {
'{| class="wikitable sortable"',
'! Round !! Home !! Score !! Away !! Winner'
}
------------------------------------------------------------------ Ro12
local absRo12 = absDay(PO_YEAR, PO_FIRST)
for i, pr in ipairs(RO12) do
-- decide winner if the date has passed
if absNow >= absRo12 then
local g1, g2 = playScore(pr[1], pr[2], absRo12 * 100 + i)
winners.Ro12[i] = (g1 > g2) and pr[1] or pr[2]
end
table.insert(tbl, row('Ro12-' .. i, absRo12, pr[1], pr[2], absRo12 * 100 + i))
end
------------------------------------------------------------------ Quarter-finals
local absQF = absRo12 + PO_IV
local qualified, byes = {}, {}
for _, w in ipairs(winners.Ro12) do if w then table.insert(qualified, w) end end
-- order by original seed
table.sort(qualified, function(a, b)
for _, t in ipairs(seedTeams) do
if t == a then return true end
if t == b then return false end
end
end)
if #qualified >= 6 then byes = { qualified[1], qualified[2] } end
if #qualified >= 6 then
table.remove(qualified, 1); table.remove(qualified, 1)
end
local QFpairs = {
{ qualified[1], qualified[#qualified] },
{ qualified[2], qualified[#qualified - 1] }
}
for i = 1, 2 do
local H = QFpairs[i] and QFpairs[i][1] or { logo = '—', fullName = 'TBD' }
local A = QFpairs[i] and QFpairs[i][2] or { logo = '—', fullName = 'TBD' }
if absNow >= absQF and QFpairs[i] then
local g1, g2 = playScore(H, A, absQF * 100 + i)
winners.QF[i] = (g1 > g2) and H or A
end
table.insert(tbl, row('QF-' .. i, absQF, H, A, absQF * 100 + i))
end
------------------------------------------------------------------ Semi-finals
local absSF = absQF + PO_IV
local SFpairs = {}
if byes[1] and winners.QF[1] then
SFpairs = {
{ byes[1], winners.QF[2] },
{ byes[2], winners.QF[1] }
}
end
for i = 1, 2 do
local H = SFpairs[i] and SFpairs[i][1] or { logo = '—', fullName = 'TBD' }
local A = SFpairs[i] and SFpairs[i][2] or { logo = '—', fullName = 'TBD' }
if absNow >= absSF and SFpairs[i] then
local g1, g2 = playScore(H, A, absSF * 100 + i)
winners.SF[i] = (g1 > g2) and H or A
end
table.insert(tbl, row('SF-' .. i, absSF, H, A, absSF * 100 + i))
end
------------------------------------------------------------------ Final
local absF = absSF + PO_IV
local FH = winners.SF[1] or { logo = '—', fullName = 'TBD' }
local FA = winners.SF[2] or { logo = '—', fullName = 'TBD' }
table.insert(tbl, row('Final', absF, FH, FA, absF * 100 + 1))
table.insert(tbl, '|}')
return table.concat(tbl, '\n')
end
-- END OF PLAY-OFF BLOCK
------------------------------------------------------------------------
return p