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) TEAM DEFINITIONS (logos shown only in schedule)
-- 1) TEAM DEFINITIONS (20px thumbnails)
local teams = {
local teams = {
     { code="Allegro Symphonara",      fullName="Allegro Symphonara",      logo="{{flagicon image|AllegrosymphonaraB.png|200px}}", championships=4 },
     { code="Allegro Symphonara",      fullName="Allegro Symphonara",      logo="[[File:AllegrosymphonaraB.png|20px]]", championships=4 },
     { code="Ashguard Pyralis",        fullName="Ashguard Pyralis",        logo="{{flagicon image|AshguardpyralisB.png|200px}}", championships=1 },
     { code="Ashguard Pyralis",        fullName="Ashguard Pyralis",        logo="[[File:AshguardpyralisB.png|20px]]", championships=1 },
     { code="Aurelia Auric",          fullName="Aurelia Auric",          logo="{{flagicon image|AureliaAuricB.png|200px}}", championships=2 },
     { code="Aurelia Auric",          fullName="Aurelia Auric",          logo="[[File:AureliaAuricB.png|20px]]", championships=2 },
     { code="Brightpath Aureum",      fullName="Brightpath Aureum",      logo="{{flagicon image|BrightpathaureumB.png|200px}}", championships=2 },
     { code="Brightpath Aureum",      fullName="Brightpath Aureum",      logo="[[File:BrightpathaureumB.png|20px]]", championships=2 },
     { code="Celestial Sancta Lunaris",fullName="Celestial Sancta Lunaris", logo="{{flagicon image|CelestialsanctaB.png|200px}}", championships=1 },
     { code="Celestial Sancta Lunaris",fullName="Celestial Sancta Lunaris", logo="[[File:CelestialsanctaB.png|20px]]", championships=1 },
     { code="Delphica Windborne",      fullName="Delphica Windborne",      logo="{{flagicon image|DelphicawindborneB.png|200px}}", championships=1 },
     { code="Delphica Windborne",      fullName="Delphica Windborne",      logo="[[File:DelphicawindborneB.png|20px]]", championships=1 },
     { code="Emberstone Vaeringheim",  fullName="Emberstone Vaeringheim",  logo="{{flagicon image|EmberstonevaeringheimB.png|200px}}", championships=3 },
     { code="Emberstone Vaeringheim",  fullName="Emberstone Vaeringheim",  logo="[[File:EmberstonevaeringheimB.png|20px]]", championships=3 },
     { code="Flameborne Erythros",    fullName="Flameborne Erythros",    logo="{{flagicon image|FlameborneerythrosB.png|200px}}", championships=1 },
     { code="Flameborne Erythros",    fullName="Flameborne Erythros",    logo="[[File:FlameborneerythrosB.png|20px]]", championships=1 },
     { code="Forgeward Nexa",          fullName="Forgeward Nexa",          logo="{{flagicon image|ForgewardnexaB.png|200px}}", championships=0 },
     { code="Forgeward Nexa",          fullName="Forgeward Nexa",          logo="[[File:ForgewardnexaB.png|20px]]", championships=0 },
     { code="Hearthkeeper Koinon",    fullName="Hearthkeeper Koinon",    logo="{{flagicon image|HearthkeeperkoinonB.png|200px}}", championships=0 },
     { code="Hearthkeeper Koinon",    fullName="Hearthkeeper Koinon",    logo="[[File:HearthkeeperkoinonB.png|20px]]", championships=0 },
     { code="Ironbough Sylvapolis",    fullName="Ironbough Sylvapolis",    logo="{{flagicon image|IronboughSylvapolisB.png|200px}}", championships=1 },
     { code="Ironbough Sylvapolis",    fullName="Ironbough Sylvapolis",    logo="[[File:IronboughSylvapolisB.png|20px]]", championships=1 },
     { code="Redcliff Citadel",        fullName="Redcliff Citadel",        logo="{{flagicon image|RedcliffCitadel.png|200px}}", championships=1 },
     { code="Redcliff Citadel",        fullName="Redcliff Citadel",        logo="[[File:RedcliffCitadel.png|20px]]", championships=1 },
     { code="Riftwarden Acheron",      fullName="Riftwarden Acheron",      logo="{{flagicon image|RiftwardenAcheronB.png|200px}}", championships=3 },
     { code="Riftwarden Acheron",      fullName="Riftwarden Acheron",      logo="[[File:RiftwardenAcheronB.png|20px]]", championships=3 },
     { code="Saluria Skylight",        fullName="Saluria Skylight",        logo="{{flagicon image|SaluriaSkylightB.png|200px}}", championships=0 },
     { code="Saluria Skylight",        fullName="Saluria Skylight",        logo="[[File:SaluriaSkylightB.png|20px]]", championships=0 },
     { code="Sandveil Somniumpolis",  fullName="Sandveil Somniumpolis",  logo="{{flagicon image|SandVeilSomniumB.png|200px}}", championships=1 },
     { code="Sandveil Somniumpolis",  fullName="Sandveil Somniumpolis",  logo="[[File:SandVeilSomniumB.png|20px]]", championships=1 },
     { code="Serena Tidesong",        fullName="Serena Tidesong",        logo="{{flagicon image|SerenaTidesongB.png|200px}}", championships=2 },
     { code="Serena Tidesong",        fullName="Serena Tidesong",        logo="[[File:SerenaTidesongB.png|20px]]", championships=2 },
     { code="Steppe Luminaria",        fullName="Steppe Luminaria",        logo="{{flagicon image|SteppeLuminariaB.png|200px}}", championships=3 },
     { code="Steppe Luminaria",        fullName="Steppe Luminaria",        logo="[[File:SteppeLuminariaB.png|20px]]", championships=3 },
     { code="Strider Myrene",          fullName="Strider Myrene",          logo="{{flagicon image|StriderMyreneB.png|200px}}", championships=1 },
     { code="Strider Myrene",          fullName="Strider Myrene",          logo="[[File:StriderMyreneB.png|20px]]", championships=1 },
     { code="Vaeringheim Pillar",      fullName="Vaeringheim Pillar",      logo="{{flagicon image|VaeringheimPillarB.png|200px}}", championships=5 },
     { code="Vaeringheim Pillar",      fullName="Vaeringheim Pillar",      logo="[[File:VaeringheimPillarB.png|20px]]", championships=5 },
     { code="Imperial Delphica",      fullName="Imperial Delphica",      logo="{{flagicon image|ImperialDelphicaB.png|200px}}", championships=1 },
     { code="Imperial Delphica",      fullName="Imperial Delphica",      logo="[[File:ImperialDelphicaB.png|20px]]", championships=1 },
     { code="Ascendant Aetherium",    fullName="Ascendant Aetherium",    logo="{{flagicon image|AscendantAetheriumB.png|200px}}", championships=2 },
     { code="Ascendant Aetherium",    fullName="Ascendant Aetherium",    logo="[[File:AscendantAetheriumB.png|20px]]", championships=2 },
     { code="Sufriya Stormwake",      fullName="Sufriya Stormwake",      logo="{{flagicon image|SufriyaStormwake.png|200px}}", championships=1 },
     { code="Sufriya Stormwake",      fullName="Sufriya Stormwake",      logo="[[File:SufriyaStormwake.png|20px]]", championships=1 },
     { code="Pillarion Club Suncliff", fullName="Pillarion Club Suncliff", logo="{{flagicon image|PCSuncliff.png|200px}}", championships=0 },
     { code="Pillarion Club Suncliff", fullName="Pillarion Club Suncliff", logo="[[File:PCSuncliff.png|20px]]", championships=0 },
     { code="Jogi Regiment",          fullName="Jogi Regiment",          logo="{{flagicon image|JogiRegiment.png|200px}}", championships=0 },
     { code="Jogi Regiment",          fullName="Jogi Regiment",          logo="[[File:JogiRegiment.png|20px]]", championships=0 },
     { code="Vine Fleet Mylecia",      fullName="Vine Fleet Mylecia",      logo="{{flagicon image|VineFleetMylecia.png|200px}}", 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="{{flagicon image|PortBloreHeath.png|200px}}", 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="{{flagicon image|FreeStateAbeis.png|200px}}", championships=0 },
     { code="Free State Abeis",        fullName="Free State Abeis",        logo="[[File:FreeStateAbeis.png|20px]]", championships=0 },
     { code="Jezeraah City",          fullName="Jezeraah City",          logo="{{flagicon image|JezeraahCity.png|200px}}", championships=0 }
     { code="Jezeraah City",          fullName="Jezeraah City",          logo="[[File:JezeraahCity.png|20px]]", championships=0 }
}
}


Line 41: Line 41:
     local pool = {}
     local pool = {}
     for i = 1, n do pool[i] = teams[i] end
     for i = 1, n do pool[i] = teams[i] end
     local fixed = table.remove(pool,1)
     local fixed = table.remove(pool, 1)
     local rounds = {}
     local rounds = {}
     for r = 1, n-1 do
     for r = 1, n - 1 do
         local pairings = {}
         local pairings = {}
         if r % 2 == 1 then
         if r % 2 == 1 then
             table.insert(pairings,{fixed, pool[1]})
             table.insert(pairings, { fixed, pool[1] })
         else
         else
             table.insert(pairings,{pool[1], fixed})
             table.insert(pairings, { pool[1], fixed })
         end
         end
         for i = 2, n/2 do
         for i = 2, n / 2 do
             local t1, t2 = pool[i], pool[#pool-(i-2)]
             local t1, t2 = pool[i], pool[#pool - (i - 2)]
             if i % 2 == 0 then
             if i % 2 == 0 then
                 table.insert(pairings,{t1, t2})
                 table.insert(pairings, { t1, t2 })
             else
             else
                 table.insert(pairings,{t2, t1})
                 table.insert(pairings, { t2, t1 })
             end
             end
         end
         end
         rounds[r] = pairings
         rounds[r] = pairings
         table.insert(pool,1,table.remove(pool))
         table.insert(pool, 1, table.remove(pool))
     end
     end
     for r = 1, n-1 do
     for r = 1, n - 1 do
         local rev = {}
         local rev = {}
         for _, m in ipairs(rounds[r]) do
         for _, m in ipairs(rounds[r]) do
             table.insert(rev,{m[2],m[1]})
             table.insert(rev, { m[2], m[1] })
         end
         end
         rounds[n-1+r] = rev
         rounds[n - 1 + r] = rev
     end
     end
     return rounds
     return rounds
end
end


-- 3) MAP ROUNDS → PSSC DAYS
-- 3) MAP ROUNDS TO PSSC DAYS
local START, END = 62, 183
local START, END = 62, 183
local function mapDays()
local function mapDays()
     local total  = END - START + 1
     local total  = END - START + 1
     local rounds = (#teams-1)*2
     local rounds = (#teams - 1) * 2
     local interval= math.floor(total/rounds)
     local iv    = math.floor(total / rounds)
     local extra  = total % rounds
     local ex    = total % rounds
     local m, day  = {}, START
     local m, d  = {}, START
     for i=1,rounds do
     for i = 1, rounds do
         m[day] = i
         m[d] = i
         day = day + interval + (extra>0 and 1 or 0)
         d    = d + iv + (ex > 0 and 1 or 0)
         if extra>0 then extra = extra - 1 end
         if ex > 0 then ex = ex - 1 end
     end
     end
     return m
     return m
Line 91: Line 91:
     local sched = generateSchedule()
     local sched = generateSchedule()
     local dm    = mapDays()
     local dm    = mapDays()
     local ridx  = dm[day]
     local idx  = dm[day]
     if not ridx then return {} end
     if not idx then return {} end
     math.randomseed(day)
     math.randomseed(day)
     local out = {}
     local out = {}
     for _, m in ipairs(sched[ridx]) do
     for _, m in ipairs(sched[idx]) do
         local h,a   = m[1], m[2]
         local h, a = m[1], m[2]
         local draw,p = 0.1, math.random()
         local p     = math.random()
         local hg,ag
         local hg, ag
         if p < draw then
         if p < 0.1 then
             hg = math.random(0,12); ag = hg
             hg = math.random(0, 12); ag = hg
         else
         else
             local wh, wa = h.championships+1, a.championships+1
             local wh, wa = h.championships + 1, a.championships + 1
             local rem     = (p-draw)/(1-draw)
             local rem   = (p - 0.1) / 0.9
             local homeWin= rem < (wh/(wh+wa))
             local homeWin= rem < (wh / (wh + wa))
             local winG  = math.random(1,12)
             local winG  = math.random(1, 12)
             local loseG  = math.random(0,math.min(winG-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
             if homeWin then hg, ag = winG, loseG else hg, ag = loseG, winG end
         end
         end
         table.insert(out,{home=h,away=a,hg=hg,ag=ag})
         table.insert(out, { home = h, away = a, hg = hg, ag = ag })
     end
     end
     return out
     return out
end
end


-- helper: month/day from PSSC day
-- helper: convert PSSC day → month/day
local function getMonthDay(d)
local function getMonthDay(d)
     if d <= 61 then
     if d <= 61 then
         return "Atosiel", d
         return "Atosiel", d
     elseif d <= 122 then
     elseif d <= 122 then
         return "Thalassiel", d-61
         return "Thalassiel", d - 61
     else
     else
         return "Opsitheiel", d-122
         return "Opsitheiel", d - 122
     end
     end
end
end


-- 5) RENDER TODAY or NEXT MATCHDAY
-- 5) RENDER TODAY or NEXT MATCHDAY
local function renderRound(currentDay, year)
local function renderRound(curDay, year)
     local dm         = mapDays()
     local dm     = mapDays()
     local targetDay = currentDay
     local target = curDay
     local upcoming   = false
     local upcoming= false


     if not dm[targetDay] then
     if not dm[target] then
         for d = currentDay+1, END do
         for d = curDay + 1, END do
             if dm[d] then targetDay, upcoming = d, true; break end
             if dm[d] then target = d; upcoming = true; break end
         end
         end
     end
     end
 
     if not dm[target] then
     if not dm[targetDay] then
         return "'''No more matches this season.'''"
         return "'''No more matches this season.'''"
     end
     end


     local prefix = upcoming and "'''Next matchday:''' " or ""
     local prefix = upcoming and "'''Next matchday:''' " or ""
     local mon, dom = getMonthDay(targetDay)
     local mon, dom = getMonthDay(target)
     local label    = dom.." "..mon.." "..year.." PSSC"
     local label    = dom .. " " .. mon .. " " .. year .. " PSSC"
     local rows    = {}
     local rows    = {}


     if not upcoming then
     if not upcoming then
         for _, r in ipairs(simulateDayResults(targetDay)) do
         for _, r in ipairs(simulateDayResults(target)) do
             local h,a,hg,ag = r.home, r.away, r.hg, r.ag
             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 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 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 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)
Line 161: Line 160:
         end
         end
     else
     else
         local sched = generateSchedule()
         local sched = generateSchedule()
         local ridx  = dm[targetDay]
         local idx    = dm[target]
         for _, m in ipairs(sched[ridx]) do
         for _, m in ipairs(sched[idx]) do
             local h,a = m[1], m[2]
             local h, a = m[1], m[2]
             table.insert(rows,
             table.insert(rows,
                 string.format("%s %s vs %s %s",
                 string.format("%s %s vs %s %s",
Line 176: Line 175:
     return string.format(
     return string.format(
         "|-\n| %s%d || %s || %s",
         "|-\n| %s%d || %s || %s",
         prefix, targetDay, label, table.concat(rows, "<br>")
         prefix, target, label, table.concat(rows, "<br>")
     )
     )
end
end


-- 6) =#invoke:PillSeasonSchedule|renderSchedule=
-- 6) Invoke: schedule
function p.renderSchedule(frame)
function p.renderSchedule(frame)
     local curr = date.getCurrentDate()
     local curr = date.getCurrentDate()
Line 193: Line 192:
end
end


-- 7) =#invoke:PillSeasonSchedule|renderStandings=
-- 7) Invoke: standings (no logos)
function p.renderStandings(frame)
function p.renderStandings(frame)
     local curr  = date.getCurrentDate()
     local curr  = date.getCurrentDate()
Line 201: Line 200:


     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 }
     end
     end


Line 210: Line 209:
             aR.PF, aR.PA = aR.PF + r.ag, aR.PA + r.hg
             aR.PF, aR.PA = aR.PF + r.ag, aR.PA + r.hg
             if r.hg > r.ag then
             if r.hg > r.ag then
                 hR.W, hR.Pts = hR.W+1, hR.Pts+3; aR.L = aR.L+1
                 hR.W, hR.Pts = hR.W + 1, hR.Pts + 3; aR.L = aR.L + 1
             elseif r.hg < r.ag then
             elseif r.hg < r.ag then
                 aR.W, aR.Pts = aR.W+1, aR.Pts+3; hR.L = hR.L+1
                 aR.W, aR.Pts = aR.W + 1, aR.Pts + 3; hR.L = hR.L + 1
             else
             else
                 hR.D, hR.Pts = hR.D+1, hR.Pts+1
                 hR.D, hR.Pts = hR.D + 1, hR.Pts + 1
                 aR.D, aR.Pts = aR.D+1, aR.Pts+1
                 aR.D, aR.Pts = aR.D + 1, aR.Pts + 1
             end
             end
         end
         end
Line 225: Line 224:
         table.insert(list, v)
         table.insert(list, v)
     end
     end
     table.sort(list, function(a,b)
     table.sort(list, function(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)



Revision as of 21:12, 19 April 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:SerenaTidesongB.png|20px]]", championships=2 },
    { code="Steppe Luminaria",        fullName="Steppe Luminaria",        logo="[[File:SteppeLuminariaB.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) Invoke: standings (no logos)
function p.renderStandings(frame)
    local curr  = date.getCurrentDate()
    local today = tonumber(curr:match('^(%d+),')) or 0
    local last  = math.min(today, END)
    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 }
    end

    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

    local list = {}
    for _, v in pairs(recs) do
        v.PD = v.PF - v.PA
        table.insert(list, v)
    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)

    local out = {
        '{| class="wikitable sortable"',
        '! Pos !! Team !! W !! D !! L !! Pts !! PF !! PA !! PD'
    }
    for i, v in ipairs(list) do
        table.insert(out, string.format(
            "|-\n| %d || %s || %d || %d || %d || %d || %d || %d || %d",
            i, v.team.fullName, v.W, v.D, v.L, v.Pts, v.PF, v.PA, v.PD
        ))
    end
    table.insert(out, "|}")
    return table.concat(out, "\n")
end

return p