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 3: | Line 3: | ||
local date = require('Module:BassaridianCalendar') | local date = require('Module:BassaridianCalendar') | ||
-- 1) | ------------------------------------------------------------------------ | ||
-- 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 = { | local teams = { | ||
{ code="Allegro Symphonara", fullName="Allegro Symphonara", logo="[[File:AllegrosymphonaraB.png|20px]]", championships=4 }, | { code="Allegro Symphonara", fullName="Allegro Symphonara", logo="[[File:AllegrosymphonaraB.png|20px]]", championships=4 }, | ||
| Line 36: | Line 57: | ||
{ code="Pillarion Club Caspazani",fullName="Pillarion Club Caspazani",logo="[[File:PCCaspazani.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="Amberwatch Slevik", fullName="Amberwatch Slevik", logo="[[File:AmberwatchSlevik.png|20px]]", championships=0 }, | ||
{ code="Fanghorn Rein", fullName="Fanghorn Rein", | { 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 | |||
for | |||
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 | end | ||
return | 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 | end | ||
-- | local function packRounds(matches) | ||
local | -- greedy packer so no club appears twice in a round | ||
local function | local rounds, used = {}, {} | ||
local function canPlace(r, m) | |||
return not (used[r] and (used[r][m.home.code] or used[r][m.away.code])) | |||
local | end | ||
for _, m in ipairs(matches) do | |||
local placed = false | |||
for r=1,#rounds do | |||
m | if canPlace(r, m) then | ||
rounds[r][#rounds[r]+1] = { m.home, m.away } | |||
if | 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 | ||
end | |||
return rounds | |||
end | end | ||
-- | -- Build the master schedule once (deterministic) | ||
local function | local SCHED = (function() | ||
local intra, inter = buildPairings() | |||
-- simple interleave: one intra block, one inter block, repeat | |||
local function chunk(arr, size) | |||
local out = {} | local out = {} | ||
for | 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 | end | ||
return out | 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 | end | ||
-- | ------------------------------------------------------------------------ | ||
local function | -- 4) MATCH SIMULATION (deterministic by day & pairing) | ||
------------------------------------------------------------------------ | |||
local function simScoreFor(h, a, seed) | |||
math.randomseed(seed) | |||
local p = math.random() | |||
else | local hg, ag | ||
if p < 0.1 then | |||
end | 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 | end | ||
-- 5) | ------------------------------------------------------------------------ | ||
-- 5) TODAY / NEXT MATCHDAY RENDER | |||
------------------------------------------------------------------------ | |||
local function renderRound(curDay, year) | local function renderRound(curDay, year) | ||
local dm = mapDays() | local dm = mapDays() | ||
| Line 156: | Line 292: | ||
local hs = string.format("<span style='color:%s;'>'''%d'''</span>", hc, hg) | local hs = string.format("<span style='color:%s;'>'''%d'''</span>", hc, hg) | ||
local as = string.format("<span style='color:%s;'>'''%d'''</span>", ac, ag) | local as = string.format("<span style='color:%s;'>'''%d'''</span>", ac, ag) | ||
rows[#rows+1] = | |||
string.format("%s %s %s vs %s %s %s", | string.format("%s %s %s vs %s %s %s", | ||
h.logo, hs, h.fullName, | h.logo, hs, h.fullName, | ||
a.logo, as, a.fullName | a.logo, as, a.fullName | ||
) | ) | ||
end | end | ||
else | else | ||
local idx = dm[target] | |||
local idx | for _, m in ipairs(SCHED[idx]) do | ||
for _, m in ipairs( | |||
local h, a = m[1], m[2] | local h, a = m[1], m[2] | ||
rows[#rows+1] = | |||
string.format("%s %s vs %s %s", | string.format("%s %s vs %s %s", | ||
h.logo, h.fullName, | h.logo, h.fullName, | ||
a.logo, a.fullName | a.logo, a.fullName | ||
) | ) | ||
end | end | ||
end | end | ||
| Line 183: | Line 316: | ||
end | end | ||
function p.renderSchedule(frame) | function p.renderSchedule(frame) | ||
local curr = date.getCurrentDate() | local curr = date.getCurrentDate() | ||
| Line 196: | Line 328: | ||
end | end | ||
-- | ------------------------------------------------------------------------ | ||
-- 6) DIVISION STANDINGS | |||
------------------------------------------------------------------------ | |||
function p.renderStandings(frame) | function p.renderStandings(frame) | ||
local curr = date.getCurrentDate() | local curr = date.getCurrentDate() | ||
local today = tonumber(curr:match("^(%d+),")) or 0 | local today = tonumber(curr:match("^(%d+),")) or 0 | ||
local last = math.min(today, END) | local last = math.min(today, END) | ||
-- init records | |||
local recs = {} | local recs = {} | ||
for _, t in ipairs(teams) do | for _, t in ipairs(teams) do | ||
recs[t.code] = { team = t, W = 0, D = 0, L = 0, Pts = 0, PF = 0, PA = 0 } | recs[t.code] = { team=t, W=0, D=0, L=0, Pts=0, PF=0, PA=0, PD=0 } | ||
end | end | ||
-- accumulate | -- accumulate results up to 'last' | ||
local dm = mapDays() | |||
for d = START, last do | for d = START, last do | ||
for | 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 | end | ||
end | end | ||
-- | -- bucket by division | ||
local | local buckets = {} | ||
for _, | for div,_ in pairs(DIVISIONS) do buckets[div] = {} end | ||
for code, r in pairs(recs) do | |||
table.insert( | r.PD = r.PF - r.PA | ||
local div = DIV_OF[code] or "—" | |||
buckets[div] = buckets[div] or {} | |||
table.insert(buckets[div], r) | |||
end | end | ||
local function cmp(a,b) | |||
if a.Pts ~= b.Pts then return a.Pts > b.Pts end | 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.PD ~= b.PD then return a.PD > b.PD end | ||
if a.W ~= b.W then return a.W > b.W end | if a.W ~= b.W then return a.W > b.W end | ||
return a.team.fullName < b.team.fullName | return a.team.fullName < b.team.fullName | ||
end) | end | ||
for div, list in pairs(buckets) do table.sort(list, cmp) end | |||
-- render | -- render four separate tables | ||
local | local order = { | ||
" | "Morovian Division","Southern Strait Division", | ||
" | "Western Highlands Division","Normarkian Division" | ||
} | } | ||
for | local out = {} | ||
for _, div in ipairs(order) do | |||
local | 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 | end | ||
out[#out+1] = "|}" | |||
out[#out+1] = "" | |||
end | end | ||
return table.concat(out, "\n") | return table.concat(out, "\n") | ||
end | end | ||
------------------------------------------------------------------------ | |||
-- | -- 7) PREVIOUS RESULTS (last 5 matchdays that actually exist) | ||
------------------------------------------------------------------------ | |||
function p.renderPreviousResults(frame) | function p.renderPreviousResults(frame) | ||
local curr = date.getCurrentDate() | local curr = date.getCurrentDate() | ||
| Line 287: | Line 429: | ||
for d = today - 1, START, -1 do | for d = today - 1, START, -1 do | ||
if dm[d] then | if dm[d] then played[#played+1] = d end | ||
if #played == 5 then break end | |||
end | end | ||
local out = { | local out = { | ||
'{| class="wikitable sortable"', | |||
'! Date !! Home !! Score !! Away' | |||
} | } | ||
for _, d in ipairs( | for _, d in ipairs(played) do | ||
local mon, dom = getMonthDay(d) | local mon, dom = getMonthDay(d) | ||
local dateLabel = dom .. " " .. mon .. " " .. year .. " PSSC" | local dateLabel = dom .. " " .. mon .. " " .. year .. " PSSC" | ||
for | local idx = dm[d] | ||
local hs = "'''" . | 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 | "| " .. dateLabel | ||
.. " || " .. | .. " || " .. h.logo .. " " .. h.fullName | ||
.. " || " .. hs | .. " || " .. hs | ||
.. " || " .. | .. " || " .. a.logo .. " " .. a.fullName | ||
end | end | ||
end | end | ||
out[#out+1] = "|}" | |||
return table.concat(out, "\n") | return table.concat(out, "\n") | ||
end | 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_YEAR = 52 -- post-season year | ||
local PO_FIRST = md2day(1,4) -- 4 Atosiel 52 PSSC (Play-in day) | local PO_FIRST = md2day(1,4) -- 4 Atosiel 52 PSSC (Play-in day) | ||
local PO_IV = 3 -- days between rounds | local PO_IV = 3 -- days between rounds | ||
-- seeding list (top-12 of final table) | -- seeding list (top-12 of final table) — unchanged | ||
local seedTeams = { | local seedTeams = { | ||
teams[7], teams[17], teams[13], teams[19], teams[1], teams[21], -- 1-6 | teams[7], teams[17], teams[13], teams[19], teams[1], teams[21], -- 1-6 | ||
| Line 343: | Line 472: | ||
} | } | ||
-- play-in | -- play-in (9 v 12, 10 v 11) | ||
local PLAYIN = { | local PLAYIN = { | ||
{ seedTeams[9], seedTeams[12] }, | { seedTeams[9], seedTeams[12] }, | ||
| Line 349: | Line 478: | ||
} | } | ||
local function playScore(A,B,salt) | local function playScore(A,B,salt) | ||
math.randomseed(salt) | math.randomseed(salt) | ||
| Line 361: | Line 489: | ||
end | end | ||
local W = { PI={}, R8={}, R4={}, SF={} } | local W = { PI={}, R8={}, R4={}, SF={} } | ||
local function mkRow(tag, absDate, H, A, salt, absNow) | local function mkRow(tag, absDate, H, A, salt, absNow) | ||
local hs, as, win = '—','—','—' | local hs, as, win = '—','—','—' | ||
if absNow >= absDate and H.fullName~='TBD' and A.fullName~='TBD' then | if absNow >= absDate and H.fullName~='TBD' and A.fullName~='TBD' then | ||
local h,a = playScore(H,A,salt) | |||
local Wn = ( | hs,as = string.format("'''%d-%d'''",h,a), nil | ||
local Wn = (h>a) and H or A | |||
win = Wn.logo .. ' ' .. Wn.fullName | win = Wn.logo .. ' ' .. Wn.fullName | ||
hs = string.format("'''%d-%d'''", | as = '' -- keep the combined cell already set above | ||
hs = string.format("'''%d-%d'''",h,a) | |||
end | end | ||
return string.format( | return string.format( | ||
"|-\n| %s || %s %s || %s || %s %s || %s", | "|-\n| %s || %s %s || %s || %s %s || %s", | ||
tag, H.logo,H.fullName, hs, A.logo,A.fullName, win) | tag, H.logo,H.fullName, (hs or '—'), A.logo,A.fullName, win) | ||
end | end | ||
function p.renderPlayoffSchedule(frame) | function p.renderPlayoffSchedule(frame) | ||
local out = { | local out = { | ||
| Line 386: | Line 511: | ||
'! Round !! Date !! Match-up' | '! Round !! Date !! Match-up' | ||
} | } | ||
local function dateCell(day) | local function dateCell(day) | ||
local mon,dom = getMonthDay(day) | local mon,dom = getMonthDay(day) | ||
| Line 399: | Line 523: | ||
-- Play-in | -- Play-in | ||
for i, | for i,pair in ipairs(PLAYIN) do add('Play-in-'..i, PO_FIRST, pair[1], pair[2]) end | ||
-- banners for remaining rounds | -- banners for remaining rounds | ||
| Line 419: | Line 543: | ||
end | end | ||
function p.renderPlayoffResults(frame) | function p.renderPlayoffResults(frame) | ||
local now = date.getCurrentDate() | local now = date.getCurrentDate() | ||
| Line 433: | Line 554: | ||
} | } | ||
-- Play-in | -- Play-in | ||
local absPI = absDay(PO_YEAR, PO_FIRST) | local absPI = absDay(PO_YEAR, PO_FIRST) | ||
for i, | for i,pair in ipairs(PLAYIN) do | ||
if absNow >= absPI then | if absNow >= absPI then | ||
local g1,g2 = playScore( | local g1,g2 = playScore(pair[1],pair[2],absPI*100+i) | ||
W.PI[i] = (g1>g2) and | W.PI[i] = (g1>g2) and pair[1] or pair[2] | ||
end | end | ||
rows[#rows+1] = | 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 | end | ||
-- Round-of-8 | -- Round-of-8 | ||
local absR8 = absPI + PO_IV | local absR8 = absPI + PO_IV | ||
local pool = { seedTeams[3],seedTeams[4],seedTeams[5],seedTeams[6], | local pool = { seedTeams[3],seedTeams[4],seedTeams[5],seedTeams[6], | ||
| Line 456: | Line 587: | ||
local R8 = {} | local R8 = {} | ||
for i=1,4 do R8[i] = { pool[i], pool[#pool - (i-1)] } end | for i=1,4 do R8[i] = { pool[i], pool[#pool - (i-1)] } end | ||
for i, | for i,pair in ipairs(R8) do | ||
if absNow >= absR8 and | local H,A = pair[1], pair[2] | ||
local g1,g2 = playScore( | local hs, win = '—', '—' | ||
W.R8[i] = (g1>g2) and | 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 | end | ||
rows[#rows+1] = | 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 | end | ||
-- Round-of-4 - | -- Round-of-4 (re-seed) | ||
local absR4 = absR8 + PO_IV | local absR4 = absR8 + PO_IV | ||
local R4 = {} | local R4 = {} | ||
| Line 479: | Line 615: | ||
local H = R4[i] and R4[i][1] or {logo='—',fullName='TBD'} | 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 A = R4[i] and R4[i][2] or {logo='—',fullName='TBD'} | ||
local hs, win = '—', '—' | |||
if absNow >= absR4 and R4[i] then | if absNow >= absR4 and R4[i] then | ||
local g1,g2 = playScore(H,A,absR4*100+i) | local g1,g2 = playScore(H,A,absR4*100+i) | ||
W.R4[i] = (g1>g2) and H or A | 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 | end | ||
rows[#rows+1] = | 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 | end | ||
-- Semi-finals (Seeds | -- Semi-finals (Seeds 1–2 BYE) | ||
local absSF = absR4 + PO_IV | local absSF = absR4 + PO_IV | ||
local SF = { | local SF = { | ||
| Line 494: | Line 634: | ||
for i=1,2 do | for i=1,2 do | ||
local H,A = SF[i][1], SF[i][2] | local H,A = SF[i][1], SF[i][2] | ||
local hs, win = '—', '—' | |||
if absNow >= absSF and A.fullName~='TBD' then | if absNow >= absSF and A.fullName~='TBD' then | ||
local g1,g2 = playScore(H,A,absSF*100+i) | local g1,g2 = playScore(H,A,absSF*100+i) | ||
W.SF[i] = (g1>g2) and H or A | 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 | end | ||
rows[#rows+1] = | 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 | end | ||
-- Final | -- Final | ||
local absF = absSF + PO_IV | local absF = absSF + PO_IV | ||
local FH = W.SF[1] or {logo='—',fullName='TBD'} | local FH = W.SF[1] or {logo='—',fullName='TBD'} | ||
local FA = W.SF[2] or {logo='—',fullName='TBD'} | local FA = W.SF[2] or {logo='—',fullName='TBD'} | ||
rows[#rows+1] = | 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] = '|}' | rows[#rows+1] = '|}' | ||
| Line 511: | Line 662: | ||
end | end | ||
-- | ------------------------------------------------------------------------ | ||
-- EXPORT | |||
------------------------------------------------------------------------ | ------------------------------------------------------------------------ | ||
return p | return p | ||
Revision as of 00:18, 20 October 2025
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