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 1: Line 1:
-- Module:PillSeasonSchedule
-- Module:PillSeasonSchedule
local p = {}
local p = {}
-- Import calendar helpers from BassaridianCalendar module
local date = require('Module:BassaridianCalendar')
local date = require('Module:BassaridianCalendar')


-- 1) TEAM DEFINITIONS
-- 1) TEAM DEFINITIONS
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="{{flagicon image|AllegrosymphonaraB.png|200px}}", championships=4 },
     { code = "Ashguard Pyralis",        fullName = "Ashguard Pyralis",        logo = "{{flagicon image|AshguardpyralisB.png|200px}}", championships = 1 },
     { code="Ashguard Pyralis",        fullName="Ashguard Pyralis",        logo="{{flagicon image|AshguardpyralisB.png|200px}}", championships=1 },
     { code = "Aurelia Auric",          fullName = "Aurelia Auric",          logo = "{{flagicon image|AureliaAuricB.png|200px}}", championships = 2 },
     { code="Aurelia Auric",          fullName="Aurelia Auric",          logo="{{flagicon image|AureliaAuricB.png|200px}}", championships=2 },
     { code = "Brightpath Aureum",      fullName = "Brightpath Aureum",      logo = "{{flagicon image|BrightpathaureumB.png|200px}}", championships = 2 },
     { code="Brightpath Aureum",      fullName="Brightpath Aureum",      logo="{{flagicon image|BrightpathaureumB.png|200px}}", 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="{{flagicon image|CelestialsanctaB.png|200px}}", championships=1 },
     { code = "Delphica Windborne",      fullName = "Delphica Windborne",      logo = "{{flagicon image|DelphicawindborneB.png|200px}}", championships = 1 },
     { code="Delphica Windborne",      fullName="Delphica Windborne",      logo="{{flagicon image|DelphicawindborneB.png|200px}}", championships=1 },
     { code = "Emberstone Vaeringheim",  fullName = "Emberstone Vaeringheim",  logo = "{{flagicon image|EmberstonevaeringheimB.png|200px}}", championships = 3 },
     { code="Emberstone Vaeringheim",  fullName="Emberstone Vaeringheim",  logo="{{flagicon image|EmberstonevaeringheimB.png|200px}}", championships=3 },
     { code = "Flameborne Erythros",    fullName = "Flameborne Erythros",    logo = "{{flagicon image|FlameborneerythrosB.png|200px}}", championships = 1 },
     { code="Flameborne Erythros",    fullName="Flameborne Erythros",    logo="{{flagicon image|FlameborneerythrosB.png|200px}}", championships=1 },
     { code = "Forgeward Nexa",          fullName = "Forgeward Nexa",          logo = "{{flagicon image|ForgewardnexaB.png|200px}}", championships = 0 },
     { code="Forgeward Nexa",          fullName="Forgeward Nexa",          logo="{{flagicon image|ForgewardnexaB.png|200px}}", championships=0 },
     { code = "Hearthkeeper Koinon",    fullName = "Hearthkeeper Koinon",    logo = "{{flagicon image|HearthkeeperkoinonB.png|200px}}", championships = 0 },
     { code="Hearthkeeper Koinon",    fullName="Hearthkeeper Koinon",    logo="{{flagicon image|HearthkeeperkoinonB.png|200px}}", championships=0 },
     { code = "Ironbough Sylvapolis",    fullName = "Ironbough Sylvapolis",    logo = "{{flagicon image|IronboughSylvapolisB.png|200px}}", championships = 1 },
     { code="Ironbough Sylvapolis",    fullName="Ironbough Sylvapolis",    logo="{{flagicon image|IronboughSylvapolisB.png|200px}}", championships=1 },
     { code = "Redcliff Citadel",        fullName = "Redcliff Citadel",        logo = "{{flagicon image|RedcliffCitadel.png|200px}}", championships = 1 },
     { code="Redcliff Citadel",        fullName="Redcliff Citadel",        logo="{{flagicon image|RedcliffCitadel.png|200px}}", championships=1 },
     { code = "Riftwarden Acheron",      fullName = "Riftwarden Acheron",      logo = "{{flagicon image|RiftwardenAcheronB.png|200px}}", championships = 3 },
     { code="Riftwarden Acheron",      fullName="Riftwarden Acheron",      logo="{{flagicon image|RiftwardenAcheronB.png|200px}}", championships=3 },
     { code = "Saluria Skylight",        fullName = "Saluria Skylight",        logo = "{{flagicon image|SaluriaSkylightB.png|200px}}", championships = 0 },
     { code="Saluria Skylight",        fullName="Saluria Skylight",        logo="{{flagicon image|SaluriaSkylightB.png|200px}}", championships=0 },
     { code = "Sandveil Somniumpolis",  fullName = "Sandveil Somniumpolis",  logo = "{{flagicon image|SandVeilSomniumB.png|200px}}", championships = 1 },
     { code="Sandveil Somniumpolis",  fullName="Sandveil Somniumpolis",  logo="{{flagicon image|SandVeilSomniumB.png|200px}}", championships=1 },
     { code = "Serena Tidesong",        fullName = "Serena Tidesong",        logo = "{{flagicon image|Serena TidesongB.png|200px}}", championships = 2 },
     { code="Serena Tidesong",        fullName="Serena Tidesong",        logo="{{flagicon image|Serena TidesongB.png|200px}}", championships=2 },
     { code = "Steppe Luminaria",        fullName = "Steppe Luminaria",        logo = "{{flagicon image|Steppe LuminariaB.png|200px}}", championships = 3 },
     { code="Steppe Luminaria",        fullName="Steppe Luminaria",        logo="{{flagicon image|Steppe LuminariaB.png|200px}}", championships=3 },
     { code = "Strider Myrene",          fullName = "Strider Myrene",          logo = "{{flagicon image|StriderMyreneB.png|200px}}", championships = 1 },
     { code="Strider Myrene",          fullName="Strider Myrene",          logo="{{flagicon image|StriderMyreneB.png|200px}}", championships=1 },
     { code = "Vaeringheim Pillar",      fullName = "Vaeringheim Pillar",      logo = "{{flagicon image|VaeringheimPillarB.png|200px}}", championships = 5 },
     { code="Vaeringheim Pillar",      fullName="Vaeringheim Pillar",      logo="{{flagicon image|VaeringheimPillarB.png|200px}}", championships=5 },
     { code = "Imperial Delphica",      fullName = "Imperial Delphica",      logo = "{{flagicon image|ImperialDelphicaB.png|200px}}", championships = 1 },
     { code="Imperial Delphica",      fullName="Imperial Delphica",      logo="{{flagicon image|ImperialDelphicaB.png|200px}}", championships=1 },
     { code = "Ascendant Aetherium",    fullName = "Ascendant Aetherium",    logo = "{{flagicon image|AscendantAetheriumB.png|200px}}", championships = 2 },
     { code="Ascendant Aetherium",    fullName="Ascendant Aetherium",    logo="{{flagicon image|AscendantAetheriumB.png|200px}}", championships=2 },
     { code = "Sufriya Stormwake",      fullName = "Sufriya Stormwake",      logo = "{{flagicon image|SufriyaStormwake.png|200px}}", championships = 1 },
     { code="Sufriya Stormwake",      fullName="Sufriya Stormwake",      logo="{{flagicon image|SufriyaStormwake.png|200px}}", 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="{{flagicon image|PCSuncliff.png|200px}}", championships=0 },
     { code = "Jogi Regiment",          fullName = "Jogi Regiment",          logo = "{{flagicon image|JogiRegiment.png|200px}}", championships = 0 },
     { code="Jogi Regiment",          fullName="Jogi Regiment",          logo="{{flagicon image|JogiRegiment.png|200px}}", 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="{{flagicon image|VineFleetMylecia.png|200px}}", 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="{{flagicon image|PortBloreHeath.png|200px}}", 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="{{flagicon image|FreeStateAbeis.png|200px}}", championships=0 },
     { code = "Jezeraah City",          fullName = "Jezeraah City",          logo = "{{flagicon image|JezeraahCity.png|200px}}", championships = 0 }
     { code="Jezeraah City",          fullName="Jezeraah City",          logo="{{flagicon image|JezeraahCity.png|200px}}", championships=0 }
}
}


-- 2) DOUBLE ROUND-ROBIN SCHEDULE GENERATOR
-- 2) SCHEDULE GENERATOR
function p.generateSchedule()
local function generateSchedule()
     local n = #teams
     local n = #teams
     assert(n % 2 == 0, "Need an even number of teams")
     assert(n % 2 == 0, "Need even number of teams")
     local pool, rounds = {}, {}
     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)
     for r = 1, n - 1 do
    local rounds = {}
     for r=1,n-1 do
         local pairings = {}
         local pairings = {}
         if r % 2 == 1 then
         if r % 2 == 1 then
Line 51: Line 50:
             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})
Line 62: Line 61:
         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 ROUND NUMBERS TO PSSC DAYS
-- 3) MAP ROUNDS TO DAYS
local START, END = 62, 183
local START, END = 62, 183
local function mapDays()
local function mapDays()
     local totalDays = END - START + 1
     local totalDays = END - START + 1
     local totalRounds = (#teams - 1) * 2
     local totalRounds = (#teams - 1) * 2
     local interval, extra = math.floor(totalDays / totalRounds), totalDays % totalRounds
     local interval = math.floor(totalDays / totalRounds)
     local m, day = {}, START
    local extra = totalDays % totalRounds
     for i = 1, totalRounds do
     local m = {}
    local day = START
     for i=1,totalRounds do
         m[day] = i
         m[day] = i
         day = day + interval + (extra > 0 and 1 or 0)
         day = day + interval + (extra > 0 and 1 or 0)
Line 87: Line 88:
end
end


-- 4) SIMULATE RESULTS FOR ONE DAY
-- 4) SIMULATE RESULTS
local function simulateDayResults(day)
local function simulateDayResults(day)
     local sched = p.generateSchedule()
     local schedule = generateSchedule()
     local dm = mapDays()
     local dm = mapDays()
     local ridx = dm[day]
     local ridx = dm[day]
Line 95: Line 96:
     math.randomseed(day)
     math.randomseed(day)
     local results = {}
     local results = {}
     for _, m in ipairs(sched[ridx]) do
     for _, match in ipairs(schedule[ridx]) do
         local h, a = m[1], m[2]
         local home, away = match[1], match[2]
         local drawProb = 0.1
         local drawProb = 0.1
         local r = math.random()
         local r = math.random()
         local hg, ag
         local hg, ag
         if r < drawProb then
         if r < drawProb then
             local g = math.random(0, 12)
             hg = math.random(0,12)
             hg, ag = g, g
             ag = hg
         else
         else
             local wh, wa = h.championships + 1, a.championships + 1
             local wh = home.championships + 1
             local probHome = wh / (wh + wa)
             local wa = away.championships + 1
             local homeWin = (r - drawProb) / (1 - drawProb) < probHome
             local rem = (r - drawProb) / (1 - drawProb)
             local winG = math.random(1, 12)
            local homeWin = rem < (wh / (wh + wa))
             local loseG = math.random(0, math.min(winG - 1, 12))
             local winG = math.random(1,12)
             if homeWin then hg, ag = winG, loseG else hg, ag = loseG, winG end
             local loseG = math.random(0, math.min(winG-1,12))
             if homeWin then
                hg, ag = winG, loseG
            else
                hg, ag = loseG, winG
            end
         end
         end
         table.insert(results, {home = h, away = a, hg = hg, ag = ag})
         table.insert(results, {home=home, away=away, hg=hg, ag=ag})
     end
     end
     return results
     return results
end
end


-- 5) RENDER A SINGLE ROUND ROW
-- 5) RENDER ONE ROUND
local function renderRound(day, year)
local function renderRound(day, year)
     local dm = mapDays()
     local dm = mapDays()
     if not dm[day] then return "'''No matches scheduled today.'''" end
     if not dm[day] then return "'''No matches scheduled today.'''" end
     local month, dom = date.getDateFromDay(day)
     local month, dom = date.getDateFromDay(day)
     local label = string.format("%d %s %d PSSC", dom, month, year)
     local dateLabel = string.format("%d %s %d PSSC", dom, month, year)
     local lines = {}
     local lines = {}
     for _, res in ipairs(simulateDayResults(day)) do
     for _, res in ipairs(simulateDayResults(day)) do
         local h, a, hg, ag = res.home, res.away, res.hg, res.ag
         local home, away, hg, ag = res.home, res.away, res.hg, res.ag
         local hcol = hg > ag and "green" or hg < ag and "red" or "yellow"
         local hcol = hg>ag and "green" or hg<ag and "red" or "yellow"
         local acol = ag > hg and "green" or ag < hg and "red" or "yellow"
         local acol = ag>hg and "green" or ag<hg and "red" or "yellow"
         table.insert(lines, string.format(
         local homeSpan = string.format("<span style='color:%s;'>'''%d'''</span>", hcol, hg)
            "%s <span style=\"color:%s;\">'''%d'''</span> %s vs %s <span style=\"color:%s;\">'''%d'''</span> %s",
        local awaySpan = string.format("<span style='color:%s;'>'''%d'''</span>", acol, ag)
             h.logo, hcol, hg, h.fullName,
        table.insert(lines, string.format("%s %s %s vs %s %s %s",
             a.logo, acol, ag, a.fullName
             home.logo, homeSpan, home.fullName,
             away.logo, awaySpan, away.fullName
         ))
         ))
     end
     end
     return string.format("|-%n| %d || %s || %s\n", day, label, table.concat(lines, '<br>'))
     return string.format("|-\n| %d || %s || %s\n", day, dateLabel, table.concat(lines,"<br>"))
end
end


-- 6) ENTRY POINT: TODAY’S MATCHDAY SCHEDULE
-- 6) ENTRY: TODAY’S MATCHDAY
function p.renderSchedule(frame)
function p.renderSchedule(frame)
     local curr = date.getCurrentDate()
     local curr = date.getCurrentDate()
     local day = tonumber(string.match(curr, '^(%d+),')) or 0
     local day = tonumber(string.match(curr, "^(%d+),")) or 0
     local year = tonumber(string.match(curr, '.*, (%d+) PSSC')) or 0
     local year = tonumber(string.match(curr, ".*, (%d+) PSSC")) or 0
     if not mapDays()[day] then return "'''No matches scheduled today.'''" end
     local dm = mapDays()
    if not dm[day] then return "'''No matches scheduled today.'''" end
     return table.concat({
     return table.concat({
         '{| class="wikitable sortable"',
         '{| class="wikitable sortable"',
Line 150: Line 158:
end
end


-- 7) ENTRY POINT: CURRENT STANDINGS WITH PF, PA, PD
-- 7) ENTRY: CURRENT STANDINGS
function p.renderStandings(frame)
function p.renderStandings(frame)
     local curr = date.getCurrentDate()
     local curr = date.getCurrentDate()
     local today = tonumber(string.match(curr, '^(%d+),')) or 0
     local today = tonumber(string.match(curr, "^(%d+),")) or 0
     local last = math.min(today, END)
     local last = math.min(today, END)
    -- initialize records
     local records = {}
     local records = {}
     for _, t in ipairs(teams) do
     for _, team in ipairs(teams) do
         records[t.code] = {team = t, W = 0, D = 0, L = 0, Pts = 0, PF = 0, PA = 0}
         records[team.code] = {team=team, W=0, D=0, L=0, Pts=0, PF=0, PA=0}
     end
     end
    -- accumulate
     for d=START,last do
     for day = START, last do
         for _, res in ipairs(simulateDayResults(d)) do
         for _, res in ipairs(simulateDayResults(day)) do
             local hR = records[res.home.code]
             local hR, aR = records[res.home.code], records[res.away.code]
            local aR = records[res.away.code]
             hR.PF = hR.PF + res.hg; hR.PA = hR.PA + res.ag
             hR.PF = hR.PF + res.hg
             aR.PF = aR.PF + res.ag; aR.PA = aR.PA + res.hg
            hR.PA = hR.PA + res.ag
             aR.PF = aR.PF + res.ag
            aR.PA = aR.PA + res.hg
             if res.hg > res.ag then
             if res.hg > res.ag then
                 hR.W = hR.W + 1; hR.Pts = hR.Pts + 3; aR.L = aR.L + 1
                 hR.W = hR.W + 1; hR.Pts = hR.Pts + 3; aR.L = aR.L + 1
Line 176: Line 185:
         end
         end
     end
     end
    -- compute PD & sort
     local list = {}
     local list = {}
     for _, rec in pairs(records) do
     for _, rec in pairs(records) do
Line 182: Line 190:
         table.insert(list, rec)
         table.insert(list, rec)
     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)
    -- render table
     local out = {
     local out = {'{| class="wikitable sortable"', '! Pos !! Team !! W !! D !! L !! Pts !! PF !! PA !! PD'}
        '{| class="wikitable sortable"',
        '! Pos !! Team !! W !! D !! L !! Pts !! PF !! PA !! PD'
    }
     for i, rec in ipairs(list) do
     for i, rec in ipairs(list) do
         table.insert(out, string.format(
         table.insert(out, string.format("|-\n| %d || %s %s || %d || %d || %d || %d || %d || %d || %d",
            "|-%n| %d || %s %s || %d || %d || %d || %d || %d || %d || %d",
             i, rec.team.logo, rec.team.fullName,
             i, i, rec.team.logo, rec.team.fullName,
             rec.W, rec.D, rec.L, rec.Pts,
             rec.W, rec.D, rec.L, rec.Pts, rec.PF, rec.PA, rec.PD
            rec.PF, rec.PA, rec.PD
         ))
         ))
     end
     end
     table.insert(out, '|}')
     table.insert(out, "|}")
     return table.concat(out, "\n")
     return table.concat(out, "\n")
end
end


return p
return p

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

-- 2) SCHEDULE GENERATOR
local function generateSchedule()
    local n = #teams
    assert(n % 2 == 0, "Need even number of teams")
    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 DAYS
local START, END = 62, 183
local function mapDays()
    local totalDays = END - START + 1
    local totalRounds = (#teams - 1) * 2
    local interval = math.floor(totalDays / totalRounds)
    local extra = totalDays % totalRounds
    local m = {}
    local day = START
    for i=1,totalRounds do
        m[day] = i
        day = day + interval + (extra > 0 and 1 or 0)
        if extra > 0 then extra = extra - 1 end
    end
    return m
end

-- 4) SIMULATE RESULTS
local function simulateDayResults(day)
    local schedule = generateSchedule()
    local dm = mapDays()
    local ridx = dm[day]
    if not ridx then return {} end
    math.randomseed(day)
    local results = {}
    for _, match in ipairs(schedule[ridx]) do
        local home, away = match[1], match[2]
        local drawProb = 0.1
        local r = math.random()
        local hg, ag
        if r < drawProb then
            hg = math.random(0,12)
            ag = hg
        else
            local wh = home.championships + 1
            local wa = away.championships + 1
            local rem = (r - drawProb) / (1 - drawProb)
            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(results, {home=home, away=away, hg=hg, ag=ag})
    end
    return results
end

-- 5) RENDER ONE ROUND
local function renderRound(day, year)
    local dm = mapDays()
    if not dm[day] then return "'''No matches scheduled today.'''" end
    local month, dom = date.getDateFromDay(day)
    local dateLabel = string.format("%d %s %d PSSC", dom, month, year)
    local lines = {}
    for _, res in ipairs(simulateDayResults(day)) do
        local home, away, hg, ag = res.home, res.away, res.hg, res.ag
        local hcol = hg>ag and "green" or hg<ag and "red" or "yellow"
        local acol = ag>hg and "green" or ag<hg and "red" or "yellow"
        local homeSpan = string.format("<span style='color:%s;'>'''%d'''</span>", hcol, hg)
        local awaySpan = string.format("<span style='color:%s;'>'''%d'''</span>", acol, ag)
        table.insert(lines, string.format("%s %s %s vs %s %s %s",
            home.logo, homeSpan, home.fullName,
            away.logo, awaySpan, away.fullName
        ))
    end
    return string.format("|-\n| %d || %s || %s\n", day, dateLabel, table.concat(lines,"<br>"))
end

-- 6) ENTRY: TODAY’S MATCHDAY
function p.renderSchedule(frame)
    local curr = date.getCurrentDate()
    local day = tonumber(string.match(curr, "^(%d+),")) or 0
    local year = tonumber(string.match(curr, ".*, (%d+) PSSC")) or 0
    local dm = mapDays()
    if not dm[day] then return "'''No matches scheduled today.'''" end
    return table.concat({
        '{| class="wikitable sortable"',
        '! Day !! Date !! Matches',
        renderRound(day, year),
        '|}'
    }, "\n")
end

-- 7) ENTRY: CURRENT STANDINGS
function p.renderStandings(frame)
    local curr = date.getCurrentDate()
    local today = tonumber(string.match(curr, "^(%d+),")) or 0
    local last = math.min(today, END)
    local records = {}
    for _, team in ipairs(teams) do
        records[team.code] = {team=team, W=0, D=0, L=0, Pts=0, PF=0, PA=0}
    end
    for d=START,last do
        for _, res in ipairs(simulateDayResults(d)) do
            local hR = records[res.home.code]
            local aR = records[res.away.code]
            hR.PF = hR.PF + res.hg
            hR.PA = hR.PA + res.ag
            aR.PF = aR.PF + res.ag
            aR.PA = aR.PA + res.hg
            if res.hg > res.ag then
                hR.W = hR.W + 1; hR.Pts = hR.Pts + 3; aR.L = aR.L + 1
            elseif res.hg < res.ag then
                aR.W = aR.W + 1; aR.Pts = aR.Pts + 3; hR.L = hR.L + 1
            else
                hR.D = hR.D + 1; hR.Pts = hR.Pts + 1
                aR.D = aR.D + 1; aR.Pts = aR.Pts + 1
            end
        end
    end
    local list = {}
    for _, rec in pairs(records) 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)
    local out = {
        '{| class="wikitable sortable"',
        '! Pos !! Team !! W !! D !! L !! Pts !! PF !! PA !! PD'
    }
    for i, rec in ipairs(list) do
        table.insert(out, string.format("|-\n| %d || %s %s || %d || %d || %d || %d || %d || %d || %d",
            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

return p