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 312: Line 312:


-- return export table
-- return export table
-- 9)  12-TEAM PLAY-OFFS  (Ro12 ▸ QF ▸ SF ▸ Final)
-- 9)  12-TEAM PLAY-OFFS  (Ro12 ➜ QF ➜ SF ➜ Final)
------------------------------------------------------------------------
------------------------------------------------------------------------


-- 9.1 Seeding --------------------------------------------------------
local DAYS_IN_YEAR = 183
local function md2day(m, d) return (m - 1) * 61 + d end
local function absDay(y, d) return y * DAYS_IN_YEAR + d end


local PO_YEAR  = 52                -- 52 PSSC
local PO_FIRST = md2day(3, 1)      -- 1 Opsitheiel = day 123
local PO_IV    = 3                  -- 3-day gaps
-- seeds (top-12 of final table, already in teams[])
local seedTeams = {
local seedTeams = {
   teams[7],   -- 1 Emberstone Vaeringheim
   teams[7],  teams[17], teams[13], teams[19], teams[1],  teams[21],
  teams[17], -- 2  Steppe Luminaria
   teams[12], teams[4],  teams[3],  teams[18], teams[16], teams[5]
  teams[13], -- 3  Riftwarden Acheron
  teams[19], -- 4  Vaeringheim Pillar
  teams[1],   -- 5 Allegro Symphonara
  teams[21], -- 6  Ascendant Aetherium
   teams[12], -- 7  Redcliff Citadel
  teams[4],   -- 8 Brightpath Aureum
  teams[3],   -- 9 Aurelia Auric
  teams[18], -- 10 Strider Myrene
  teams[16], -- 11 Serena Tidesong
  teams[5]   -- 12 Celestial Sancta Lunaris
}
}


-- helpers
-- Ro12 pairings: 1-12, 2-11 … 6-7
local function md2day(m, d)  return (m-1)*61 + d end
local PO_FIRST, PO_IV = md2day(3,1), 3    -- start 1 Opsitheiel 52, interval 3 days
 
-- Ro12 pairings (higher seed hosts)
local RO12 = {
local RO12 = {
   {seedTeams[1], seedTeams[12]},
   { seedTeams[1], seedTeams[12] },
   {seedTeams[2], seedTeams[11]},
   { seedTeams[2], seedTeams[11] },
   {seedTeams[3], seedTeams[10]},
   { seedTeams[3], seedTeams[10] },
   {seedTeams[4], seedTeams[9]},
   { seedTeams[4], seedTeams[9] },
   {seedTeams[5], seedTeams[8]},
   { seedTeams[5], seedTeams[8] },
   {seedTeams[6], seedTeams[7]}
   { seedTeams[6], seedTeams[7] }
}
}


-- simple single-game score maker (same bias as season)
-- one-off score with small seed bias
local function playoffScore(A,B,seed)
local function playScore(A, B, seed)
   math.randomseed(seed)
   math.randomseed(seed)
   local p = math.random()
   local p = math.random()
   if p < 0.1 then local g = math.random(0,12); return g,g end
   if p < 0.10 then local g = math.random(0, 12); return g, g end
   local wA, wB = A.championships+1, B.championships+1
   local wa, wb = A.championships + 1, B.championships + 1
   local homeWin = (p-0.1)/0.9 < wA/(wA+wB)
   local home  = (p - 0.10) / 0.90 < wa / (wa + wb)
   local big = math.random(1,12); local sml = math.random(0, math.min(big-1,12))
   local hi    = math.random(1, 12)
   return homeWin and big or sml, homeWin and sml or big
  local lo    = math.random(0, math.min(hi - 1, 12))
   return home and hi or lo, home and lo or hi
end
end


-- store winners round-by-round keyed by day index
local winners = { Ro12 = {}, QF = {}, SF = {} }
local winners = { Ro12={}, QF={}, SF={} }


------------------------------------------------------------------------
------------------------------------------------------------------------
-- 9.2 Schedule table
-- 9.1 Play-off Schedule (blank scores)
------------------------------------------------------------------------
------------------------------------------------------------------------
function p.renderPlayoffSchedule()
function p.renderPlayoffSchedule(frame)
   local out = {
   local out = {
     "{| class=\"wikitable sortable\"",
     '{| class="wikitable sortable"',
     "! Round !! Date !! Match-up"
     '! Round !! Date !! Match-up'
   }
   }


   local function addRow(rnd, day, A, B, idx)
   local function addRow(tag, dayOfYear, H, A)
     local mon, dom = getMonthDay(day)
     local mon, dom = getMonthDay(dayOfYear)
     table.insert(out, "|-")
     table.insert(out, "|-")
     table.insert(out, "| "..rnd.." "..idx
     table.insert(out,
       .." || "..dom.." "..mon.." 52 PSSC"
       string.format("| %s || %d %s %d PSSC || %s %s vs %s %s",
      .." || "..A.logo.." "..A.fullName.." vs "..B.logo.." "..B.fullName)
        tag, dom, mon, PO_YEAR, H.logo, H.fullName, A.logo, A.fullName))
   end
   end


   -- Ro12 (day 123)
   -- Round of 12
  local dRo12 = PO_FIRST
   for i, pr in ipairs(RO12) do
   for i, pair in ipairs(RO12) do addRow("Ro12", dRo12, pair[1], pair[2], i) end
    addRow("Ro12-" .. i, PO_FIRST, pr[1], pr[2])
  end


   -- QF date
   -- placeholders
   local dQF = dRo12 + PO_IV
   local dQF = PO_FIRST + PO_IV
   local mon,dom = getMonthDay(dQF)
   local mon, dom = getMonthDay(dQF)
   table.insert(out, "|-")
   table.insert(out, "|-")
   table.insert(out, "| colspan=\"3\" | '''Quarter-finals:''' "..dom.." "..mon.." 52 PSSC")
   table.insert(out,
    "| colspan=\"3\" | '''Quarter-finals:''' "
      .. dom .. " " .. mon .. " " .. PO_YEAR .. " PSSC")


  -- SF date
   local dSF = dQF + PO_IV
   local dSF = dQF + PO_IV
   mon,dom = getMonthDay(dSF)
   mon, dom = getMonthDay(dSF)
   table.insert(out, "|-")
   table.insert(out, "|-")
   table.insert(out, "| colspan=\"3\" | '''Semi-finals:''' "..dom.." "..mon.." 52 PSSC")
   table.insert(out,
    "| colspan=\"3\" | '''Semi-finals:''' "
      .. dom .. " " .. mon .. " " .. PO_YEAR .. " PSSC")


  -- Final date
   local dF = dSF + PO_IV
   local dF = dSF + PO_IV
   mon,dom = getMonthDay(dF)
   mon, dom = getMonthDay(dF)
   table.insert(out, "|-")
   table.insert(out, "|-")
   table.insert(out, "| colspan=\"3\" | '''Final:''' "..dom.." "..mon.." 52 PSSC")
   table.insert(out,
    "| colspan=\"3\" | '''Final:''' "
      .. dom .. " " .. mon .. " " .. PO_YEAR .. " PSSC")


   table.insert(out, "|}")
   table.insert(out, "|}")
Line 404: Line 402:


------------------------------------------------------------------------
------------------------------------------------------------------------
-- 9.3 Results table – fills as days pass
-- 9.2 Play-off Results (update as dates pass)
------------------------------------------------------------------------
------------------------------------------------------------------------
function p.renderPlayoffResults()
function p.renderPlayoffResults(frame)
   local today = tonumber(date.getCurrentDate():match("^(%d+),")) or 0
   local cur      = date.getCurrentDate()
   local rows  = {
  local dNow    = tonumber(cur:match("^(%d+),")) or 0
      "{| class=\"wikitable sortable\"",
   local yNow    = tonumber(cur:match(", (%d+) PSSC")) or 0
      "! Round !! Home !! Score !! Away !! Winner"
   local absNow  = absDay(yNow, dNow)
   }


   ------------------------------------------------------------------ Ro12
   -- helper to build one row
   local dRo12 = PO_FIRST
   local function row(rnd, absKey, H, A, idx)
  for i, pair in ipairs(RO12) do
    local hs, as, winTxt = "—", "—", "—"
      local H,A = pair[1], pair[2]
    if absNow >= absKey and H.fullName ~= "TBD" and A.fullName ~= "TBD" then
      local hs,as,win = "—","—","—"
      hs, as = playScore(H, A, absKey * 100 + idx)
 
      local W = (hs > as) and H or A
      if today >= dRo12 then
      winTxt  = W.logo .. " " .. W.fullName
          hs,as = playoffScore(H,A,dRo12*100+i)
      hs      = string.format("'''%d-%d'''", hs, as)
          win  = (hs>as) and H or A
    end
          winners.Ro12[i] = win
    return string.format(
          hs = "'''"..hs.."-"..as.."'''"
       "|-\n| %s || %s %s || %s || %s %s || %s",
          win = win.logo.." "..win.fullName
      rnd, H.logo, H.fullName, hs, A.logo, A.fullName, winTxt)
      end
 
      table.insert(rows,"|-")
       table.insert(rows,"| Ro12-"..i
        .." || "..H.logo.." "..H.fullName
        .." || "..hs
        .." || "..A.logo.." "..A.fullName
        .." || "..win)
   end
   end


  ------------------------------------------------------------------ QF
   local rows = {
   local dQF = dRo12 + PO_IV
    '{| class="wikitable sortable"',
  local qfPairs, seedsLeft = {}, {}
    '! Round !! Home !! Score !! Away !! Winner'
  -- collect Ro12 winners with their original seed rank
   }
  for idx, w in ipairs(winners.Ro12) do
      if w then                                          -- played
          local seed = nil
          for s, t in ipairs(seedTeams) do if t == w then seed = s break end end
          table.insert(seedsLeft,{seed=seed, team=w})
      else
          table.insert(rows,"|-");                      -- placeholder
          table.insert(rows,"| QF-"..idx.." || — || — || — || —")
      end
   end
  -- sort ascending by seed, take top2 bye, rest form QFs
  table.sort(seedsLeft,function(a,b) return a.seed<b.seed end)
  local bye1, bye2 = seedsLeft[1] and seedsLeft[1].team, seedsLeft[2] and seedsLeft[2].team
  local qTeams = {seedsLeft[3] and seedsLeft[3].team, seedsLeft[4] and seedsLeft[4].team,
                  seedsLeft[5] and seedsLeft[5].team, seedsLeft[6] and seedsLeft[6].team}


   if today >= dQF and qTeams[1] then
   -- Round of 12
       -- pair highest remaining vs lowest remaining, etc.
  local absRo12 = absDay(PO_YEAR, PO_FIRST)
       qfPairs = {
  for i, pr in ipairs(RO12) do
        {qTeams[1], qTeams[4]},
    local line = row("Ro12-" .. i, absRo12, pr[1], pr[2], i)
        {qTeams[2], qTeams[3]}
    table.insert(rows, line)
      }
    if absNow >= absRo12 then
       local hG = tonumber(line:match("'''(%d+)%-%d+'''"))
       local aG = tonumber(line:match("%-(%d+)'''"))
      winners.Ro12[i] = (hG and aG and hG > aG) and pr[1] or pr[2]
    end
   end
   end


   for i=1,2 do
   -- Quarter-finals
      local H,A = qfPairs[i] and qfPairs[i][1] or {logo="—",fullName="TBD"},
  local absQF = absRo12 + PO_IV
                  qfPairs[i] and qfPairs[i][2] or {logo="—",fullName="TBD"}
  local qualified, byes = {}, {}
      local hs,as,win="—","—","—"
  for _, w in ipairs(winners.Ro12) do if w then table.insert(qualified, w) end end
       if today >= dQF and qfPairs[i] then
  table.sort(qualified, function(a, b)
          hs,as = playoffScore(H,A,dQF*100+i)
    for _, t in ipairs(seedTeams) do
          win   = (hs>as) and H or A
       if t == a then return true end
          winners.QF[i] = win
      if t == b then return false end
          hs="'''"..hs.."-"..as.."'''"
    end
          win = win.logo.." "..win.fullName
  end)
      end
   if #qualified >= 6 then byes = { qualified[1], qualified[2] } end
      table.insert(rows,"|-")
  if #qualified >= 6 then
      table.insert(rows,"| QF-"..i
    table.remove(qualified, 1); table.remove(qualified, 1)
        .." || "..H.logo.." "..H.fullName
        .." || "..hs
        .." || "..A.logo.." "..A.fullName
        .." || "..win)
   end
   end
  local QFpairs = {
    { qualified[1], qualified[#qualified] },
    { qualified[2], qualified[#qualified - 1] }
  }


   ------------------------------------------------------------------ SF
   for i = 1, 2 do
  local dSF = dQF + PO_IV
    local H = QFpairs[i] and QFpairs[i][1] or { logo = "—", fullName = "TBD" }
  local sfPairs = {}
    local A = QFpairs[i] and QFpairs[i][2] or { logo = "—", fullName = "TBD" }
  if today >= dSF and bye1 and bye2 and winners.QF[1] and winners.QF[2] then
    local line = row("QF-" .. i, absQF, H, A, i)
      -- highest seed hosts lowest remaining
    table.insert(rows, line)
      local function seedOf(t)
    if absNow >= absQF and QFpairs[i] then
          for s,tm in ipairs(seedTeams) do if tm==t then return s end end
       local hG = tonumber(line:match("'''(%d+)%-%d+'''"))
          return 99
       local aG = tonumber(line:match("%-(%d+)'''"))
       end
       winners.QF[i] = (hG and aG and hG > aG) and H or A
       local pool = {bye1, bye2, winners.QF[1], winners.QF[2]}
    end
      table.sort(pool,function(a,b) return seedOf(a)<seedOf(b) end)
       sfPairs = {
        {pool[1], pool[4]},
        {pool[2], pool[3]}
      }
   end
   end


   for i=1,2 do
   -- Semi-finals
      local H,A = sfPairs[i] and sfPairs[i][1] or {logo="—",fullName="TBD"},
  local absSF = absQF + PO_IV
                  sfPairs[i] and sfPairs[i][2] or {logo="—",fullName="TBD"}
  local SFpairs = {}
      local hs,as,win="—","—","—"
  if byes[1] and winners.QF[1] then
       if today >= dSF and sfPairs[i] then
    SFpairs = {
          hs,as = playoffScore(H,A,dSF*100+i)
      { byes[1], winners.QF[2] },
          win  = (hs>as) and H or A
       { byes[2], winners.QF[1] }
          winners.SF[i] = win
    }
          hs="'''"..hs.."-"..as.."'''"
          win = win.logo.." "..win.fullName
      end
      table.insert(rows,"|-")
      table.insert(rows,"| SF-"..i
        .." || "..H.logo.." "..H.fullName
        .." || "..hs
        .." || "..A.logo.." "..A.fullName
        .." || "..win)
   end
   end


   ------------------------------------------------------------------ Final
   for i = 1, 2 do
  local dF = dSF + PO_IV
    local H = SFpairs[i] and SFpairs[i][1] or { logo = "—", fullName = "TBD" }
  local FH,FA = winners.SF[1] or {logo="—",fullName="TBD"}, winners.SF[2] or {logo="—",fullName="TBD"}
    local A = SFpairs[i] and SFpairs[i][2] or { logo = "—", fullName = "TBD" }
  local fScore, champ = "—" , "—"
    local line = row("SF-" .. i, absSF, H, A, i)
 
    table.insert(rows, line)
  if today >= dF and FH.fullName~="TBD" and FA.fullName~="TBD" then
    if absNow >= absSF and SFpairs[i] then
       local hs,as = playoffScore(FH,FA,dF*100+1)
       local hG = tonumber(line:match("'''(%d+)%-%d+'''"))
      champ      = (hs>as) and FH or FA
       local aG = tonumber(line:match("%-(%d+)'''"))
      fScore      = "'''"..hs.."-"..as.."'''"
      winners.SF[i] = (hG and aG and hG > aG) and H or A
       champ      = champ.logo.." "..champ.fullName
    end
   end
   end


   table.insert(rows, "|-")
   -- Final
   local monF,domF = getMonthDay(dF)
   local absF = absSF + PO_IV
   table.insert(rows, "| Final ("..domF.." "..monF..")"
   local FH = winners.SF[1] or { logo = "—", fullName = "TBD" }
    .." || "..FH.logo.." "..FH.fullName
  local FA = winners.SF[2] or { logo = "—", fullName = "TBD" }
    .." || "..fScore
  table.insert(rows, row("Final", absF, FH, FA, 1))
    .." || "..FA.logo.." "..FA.fullName
    .." || "..champ)


   table.insert(rows, "|}")
   table.insert(rows, "|}")
   return table.concat(rows, "\n")
   return table.concat(rows, "\n")
end
end
-- END OF PLAY-OFF BLOCK
------------------------------------------------------------------------
return p
return p

Revision as of 16:07, 23 August 2025

Documentation for this module may be created at Module:PillSeasonSchedule/doc

-- Module:PillSeasonSchedule
local p = {}
local date = require('Module:BassaridianCalendar')

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

-- 2) DOUBLE ROUND‑ROBIN GENERATOR
local function generateSchedule()
    local n = #teams
    assert(n % 2 == 0)
    local pool = {}
    for i = 1, n do pool[i] = teams[i] end
    local fixed = table.remove(pool, 1)
    local rounds = {}
    for r = 1, n - 1 do
        local pairings = {}
        if r % 2 == 1 then
            table.insert(pairings, { fixed, pool[1] })
        else
            table.insert(pairings, { pool[1], fixed })
        end
        for i = 2, n / 2 do
            local t1, t2 = pool[i], pool[#pool - (i - 2)]
            if i % 2 == 0 then
                table.insert(pairings, { t1, t2 })
            else
                table.insert(pairings, { t2, t1 })
            end
        end
        rounds[r] = pairings
        table.insert(pool, 1, table.remove(pool))
    end
    for r = 1, n - 1 do
        local rev = {}
        for _, m in ipairs(rounds[r]) do
            table.insert(rev, { m[2], m[1] })
        end
        rounds[n - 1 + r] = rev
    end
    return rounds
end

-- 3) MAP ROUNDS TO PSSC DAYS
local START, END = 62, 183
local function mapDays()
    local total  = END - START + 1
    local rounds = (#teams - 1) * 2
    local iv     = math.floor(total / rounds)
    local ex     = total % rounds
    local m, d   = {}, START
    for i = 1, rounds do
        m[d] = i
        d    = d + iv + (ex > 0 and 1 or 0)
        if ex > 0 then ex = ex - 1 end
    end
    return m
end

-- 4) SIMULATE RESULTS FOR A DAY
local function simulateDayResults(day)
    local sched = generateSchedule()
    local dm    = mapDays()
    local idx   = dm[day]
    if not idx then return {} end
    math.randomseed(day)
    local out = {}
    for _, m in ipairs(sched[idx]) do
        local h, a = m[1], m[2]
        local p     = math.random()
        local hg, ag
        if p < 0.1 then
            hg = math.random(0, 12); ag = hg
        else
            local wh, wa = h.championships + 1, a.championships + 1
            local rem    = (p - 0.1) / 0.9
            local homeWin= rem < (wh / (wh + wa))
            local winG   = math.random(1, 12)
            local loseG  = math.random(0, math.min(winG - 1, 12))
            if homeWin then hg, ag = winG, loseG else hg, ag = loseG, winG end
        end
        table.insert(out, { home = h, away = a, hg = hg, ag = ag })
    end
    return out
end

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

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

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

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

    if not upcoming then
        for _, r in ipairs(simulateDayResults(target)) do
            local h, a, hg, ag = r.home, r.away, r.hg, r.ag
            local hc = (hg > ag and "green") or (hg < ag and "red") or "yellow"
            local ac = (ag > hg and "green") or (ag < hg and "red") or "yellow"
            local hs = string.format("<span style='color:%s;'>'''%d'''</span>", hc, hg)
            local as = string.format("<span style='color:%s;'>'''%d'''</span>", ac, ag)
            table.insert(rows,
                string.format("%s %s %s vs %s %s %s",
                    h.logo, hs, h.fullName,
                    a.logo, as, a.fullName
                )
            )
        end
    else
        local sched  = generateSchedule()
        local idx    = dm[target]
        for _, m in ipairs(sched[idx]) do
            local h, a = m[1], m[2]
            table.insert(rows,
                string.format("%s %s vs %s %s",
                    h.logo, h.fullName,
                    a.logo, a.fullName
                )
            )
        end
    end

    return string.format(
        "|-\n| %s%d || %s || %s",
        prefix, target, label, table.concat(rows, "<br>")
    )
end

-- 6) Invoke: schedule
function p.renderSchedule(frame)
    local curr = date.getCurrentDate()
    local day  = tonumber(curr:match('^(%d+),')) or 0
    local year = tonumber(curr:match(', (%d+) PSSC')) or 0
    return table.concat({
        '{| class="wikitable sortable"',
        '! Day !! Date !! Matches',
        renderRound(day, year),
        '|}'
    }, "\n")
end

-- 7) CURRENT STANDINGS (with logos and conditional row coloring)
function p.renderStandings(frame)
    local curr   = date.getCurrentDate()
    local today  = tonumber(curr:match("^(%d+),")) or 0
    local last   = math.min(today, END)
    local recs   = {}

    -- initialize records
    for _, t in ipairs(teams) do
        recs[t.code] = { team = t, W = 0, D = 0, L = 0, Pts = 0, PF = 0, PA = 0 }
    end

    -- accumulate W/D/L and points
    for d = START, last do
        for _, r in ipairs(simulateDayResults(d)) do
            local hR, aR = recs[r.home.code], recs[r.away.code]
            hR.PF, hR.PA = hR.PF + r.hg, hR.PA + r.ag
            aR.PF, aR.PA = aR.PF + r.ag, aR.PA + r.hg
            if r.hg > r.ag then
                hR.W, hR.Pts = hR.W + 1, hR.Pts + 3
                aR.L         = aR.L + 1
            elseif r.hg < r.ag then
                aR.W, aR.Pts = aR.W + 1, aR.Pts + 3
                hR.L         = hR.L + 1
            else
                hR.D, hR.Pts = hR.D + 1, hR.Pts + 1
                aR.D, aR.Pts = aR.D + 1, aR.Pts + 1
            end
        end
    end

    -- prepare sorted list
    local list = {}
    for _, rec in pairs(recs) do
        rec.PD = rec.PF - rec.PA
        table.insert(list, rec)
    end
    table.sort(list, function(a, b)
        if a.Pts ~= b.Pts then return a.Pts > b.Pts end
        if a.PD  ~= b.PD  then return a.PD  > b.PD  end
        if a.W   ~= b.W   then return a.W   > b.W   end
        return a.team.fullName < b.team.fullName
    end)

    -- render table with conditional styling
    local out = {
        "{| class=\"wikitable sortable\"",
        "! Pos !! Team !! W !! D !! L !! Pts !! PF !! PA !! PD"
    }
    for i, rec in ipairs(list) do
        -- determine background color
        local style = ""
        if i <= 8 then
            style = ' style="background-color:#ccffcc;"'    -- light green
        elseif i >= 9 and i <= 12 then
            style = ' style="background-color:#cce5ff;"'    -- light blue
        elseif i >= 24 and i <= 28 then
            style = ' style="background-color:#ffcccc;"'    -- light red
        end

        -- insert row
        table.insert(out, "|-" .. style)
        table.insert(out,
            "| " .. i
            .. " || " .. rec.team.logo .. " " .. rec.team.fullName
            .. " || " .. rec.W
            .. " || " .. rec.D
            .. " || " .. rec.L
            .. " || " .. rec.Pts
            .. " || " .. rec.PF
            .. " || " .. rec.PA
            .. " || " .. rec.PD
        )
    end

    table.insert(out, "|}")
    return table.concat(out, "\n")
end


-- 8) PREVIOUS RESULTS: shows the last 5 matchdays
function p.renderPreviousResults(frame)
    local curr   = date.getCurrentDate()
    local today  = tonumber(curr:match("^(%d+),")) or 0
    local year   = tonumber(curr:match(", (%d+) PSSC")) or 0
    local dm     = mapDays()
    local played = {}

    for d = today - 1, START, -1 do
        if dm[d] then table.insert(played, d) end
    end

    local recent = {}
    for i = 1, math.min(5, #played) do
        table.insert(recent, played[i])
    end

    local out = {
        "{| class=\"wikitable sortable\"",
        "! Date !! Home !! Score !! Away"
    }
    for _, d in ipairs(recent) do
        local mon, dom  = getMonthDay(d)
        local dateLabel = dom .. " " .. mon .. " " .. year .. " PSSC"
        for _, r in ipairs(simulateDayResults(d)) do
            local hs = "'''" .. r.hg .. "-" .. r.ag .. "'''"
            table.insert(out, "|-")
            table.insert(out,
                "| " .. dateLabel
                .. " || " .. r.home.logo .. " " .. r.home.fullName
                .. " || " .. hs
                .. " || " .. r.away.logo .. " " .. r.away.fullName
            )
        end
    end
    table.insert(out, "|}")
    return table.concat(out, "\n")
end

-- return export table
-- 9)  12-TEAM PLAY-OFFS  (Ro12 ➜ QF ➜ SF ➜ Final)
------------------------------------------------------------------------

local DAYS_IN_YEAR = 183
local function md2day(m, d) return (m - 1) * 61 + d end
local function absDay(y, d) return y * DAYS_IN_YEAR + d end

local PO_YEAR  = 52                 -- 52 PSSC
local PO_FIRST = md2day(3, 1)       -- 1 Opsitheiel = day 123
local PO_IV    = 3                  -- 3-day gaps

-- seeds (top-12 of final table, already in teams[])
local seedTeams = {
  teams[7],  teams[17], teams[13], teams[19], teams[1],  teams[21],
  teams[12], teams[4],  teams[3],  teams[18], teams[16], teams[5]
}

-- Ro12 pairings: 1-12, 2-11 … 6-7
local RO12 = {
  { seedTeams[1],  seedTeams[12] },
  { seedTeams[2],  seedTeams[11] },
  { seedTeams[3],  seedTeams[10] },
  { seedTeams[4],  seedTeams[9]  },
  { seedTeams[5],  seedTeams[8]  },
  { seedTeams[6],  seedTeams[7]  }
}

-- one-off score with small seed bias
local function playScore(A, B, seed)
  math.randomseed(seed)
  local p = math.random()
  if p < 0.10 then local g = math.random(0, 12); return g, g end
  local wa, wb = A.championships + 1, B.championships + 1
  local home   = (p - 0.10) / 0.90 < wa / (wa + wb)
  local hi     = math.random(1, 12)
  local lo     = math.random(0, math.min(hi - 1, 12))
  return home and hi or lo, home and lo or hi
end

local winners = { Ro12 = {}, QF = {}, SF = {} }

------------------------------------------------------------------------
-- 9.1  Play-off Schedule  (blank scores)
------------------------------------------------------------------------
function p.renderPlayoffSchedule(frame)
  local out = {
    '{| class="wikitable sortable"',
    '! Round !! Date !! Match-up'
  }

  local function addRow(tag, dayOfYear, H, A)
    local mon, dom = getMonthDay(dayOfYear)
    table.insert(out, "|-")
    table.insert(out,
      string.format("| %s || %d %s %d PSSC || %s %s vs %s %s",
        tag, dom, mon, PO_YEAR, H.logo, H.fullName, A.logo, A.fullName))
  end

  -- Round of 12
  for i, pr in ipairs(RO12) do
    addRow("Ro12-" .. i, PO_FIRST, pr[1], pr[2])
  end

  -- placeholders
  local dQF = PO_FIRST + PO_IV
  local mon, dom = getMonthDay(dQF)
  table.insert(out, "|-")
  table.insert(out,
    "| colspan=\"3\" | '''Quarter-finals:''' "
      .. dom .. " " .. mon .. " " .. PO_YEAR .. " PSSC")

  local dSF = dQF + PO_IV
  mon, dom = getMonthDay(dSF)
  table.insert(out, "|-")
  table.insert(out,
    "| colspan=\"3\" | '''Semi-finals:''' "
      .. dom .. " " .. mon .. " " .. PO_YEAR .. " PSSC")

  local dF = dSF + PO_IV
  mon, dom = getMonthDay(dF)
  table.insert(out, "|-")
  table.insert(out,
    "| colspan=\"3\" | '''Final:''' "
      .. dom .. " " .. mon .. " " .. PO_YEAR .. " PSSC")

  table.insert(out, "|}")
  return table.concat(out, "\n")
end

------------------------------------------------------------------------
-- 9.2  Play-off Results  (update as dates pass)
------------------------------------------------------------------------
function p.renderPlayoffResults(frame)
  local cur      = date.getCurrentDate()
  local dNow     = tonumber(cur:match("^(%d+),")) or 0
  local yNow     = tonumber(cur:match(", (%d+) PSSC")) or 0
  local absNow   = absDay(yNow, dNow)

  -- helper to build one row
  local function row(rnd, absKey, H, A, idx)
    local hs, as, winTxt = "—", "—", "—"
    if absNow >= absKey and H.fullName ~= "TBD" and A.fullName ~= "TBD" then
      hs, as = playScore(H, A, absKey * 100 + idx)
      local W = (hs > as) and H or A
      winTxt  = W.logo .. " " .. W.fullName
      hs      = string.format("'''%d-%d'''", hs, as)
    end
    return string.format(
      "|-\n| %s || %s %s || %s || %s %s || %s",
      rnd, H.logo, H.fullName, hs, A.logo, A.fullName, winTxt)
  end

  local rows = {
    '{| class="wikitable sortable"',
    '! Round !! Home !! Score !! Away !! Winner'
  }

  -- Round of 12
  local absRo12 = absDay(PO_YEAR, PO_FIRST)
  for i, pr in ipairs(RO12) do
    local line = row("Ro12-" .. i, absRo12, pr[1], pr[2], i)
    table.insert(rows, line)
    if absNow >= absRo12 then
      local hG = tonumber(line:match("'''(%d+)%-%d+'''"))
      local aG = tonumber(line:match("%-(%d+)'''"))
      winners.Ro12[i] = (hG and aG and hG > aG) and pr[1] or pr[2]
    end
  end

  -- Quarter-finals
  local absQF = absRo12 + PO_IV
  local qualified, byes = {}, {}
  for _, w in ipairs(winners.Ro12) do if w then table.insert(qualified, w) end end
  table.sort(qualified, function(a, b)
    for _, t in ipairs(seedTeams) do
      if t == a then return true end
      if t == b then return false end
    end
  end)
  if #qualified >= 6 then byes = { qualified[1], qualified[2] } end
  if #qualified >= 6 then
    table.remove(qualified, 1); table.remove(qualified, 1)
  end
  local QFpairs = {
    { qualified[1], qualified[#qualified] },
    { qualified[2], qualified[#qualified - 1] }
  }

  for i = 1, 2 do
    local H = QFpairs[i] and QFpairs[i][1] or { logo = "—", fullName = "TBD" }
    local A = QFpairs[i] and QFpairs[i][2] or { logo = "—", fullName = "TBD" }
    local line = row("QF-" .. i, absQF, H, A, i)
    table.insert(rows, line)
    if absNow >= absQF and QFpairs[i] then
      local hG = tonumber(line:match("'''(%d+)%-%d+'''"))
      local aG = tonumber(line:match("%-(%d+)'''"))
      winners.QF[i] = (hG and aG and hG > aG) and H or A
    end
  end

  -- Semi-finals
  local absSF = absQF + PO_IV
  local SFpairs = {}
  if byes[1] and winners.QF[1] then
    SFpairs = {
      { byes[1], winners.QF[2] },
      { byes[2], winners.QF[1] }
    }
  end

  for i = 1, 2 do
    local H = SFpairs[i] and SFpairs[i][1] or { logo = "—", fullName = "TBD" }
    local A = SFpairs[i] and SFpairs[i][2] or { logo = "—", fullName = "TBD" }
    local line = row("SF-" .. i, absSF, H, A, i)
    table.insert(rows, line)
    if absNow >= absSF and SFpairs[i] then
      local hG = tonumber(line:match("'''(%d+)%-%d+'''"))
      local aG = tonumber(line:match("%-(%d+)'''"))
      winners.SF[i] = (hG and aG and hG > aG) and H or A
    end
  end

  -- Final
  local absF = absSF + PO_IV
  local FH = winners.SF[1] or { logo = "—", fullName = "TBD" }
  local FA = winners.SF[2] or { logo = "—", fullName = "TBD" }
  table.insert(rows, row("Final", absF, FH, FA, 1))

  table.insert(rows, "|}")
  return table.concat(rows, "\n")
end

-- END OF PLAY-OFF BLOCK
------------------------------------------------------------------------
return p