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:
local p = {}
local p = {}


-- Import date helpers from Module:P
-- Import calendar helpers from Module:P
local date = require('Module:P')
local date = require('Module:P')


Line 36: Line 36:
}
}


-- 2) DOUBLE ROUND-ROBIN SCHEDULE GENERATOR
-- 2) SCHEDULE GENERATOR
function p.generateSchedule()
function p.generateSchedule()
     local n = #teams
     local n, rounds = #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
     for r=1,n-1 do
         local pairings = {}
         local pairings = {}
         if r % 2 == 1 then table.insert(pairings, {fixed, pool[1]}) else table.insert(pairings, {pool[1], fixed}) end
         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
         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 table.insert(pairings, {t1, t2}) else table.insert(pairings, {t2, t1}) end
             if i%2==0 then table.insert(pairings,{t1,t2}) else table.insert(pairings,{t2,t1}) 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 table.insert(rev, {m[2], m[1]}) end
         for _,m in ipairs(rounds[r]) do table.insert(rev,{m[2],m[1]}) end
         rounds[n - 1 + r] = rev
         rounds[n-1+r]=rev
     end
     end
     return rounds
     return rounds
Line 62: Line 62:


-- 3) MAP ROUNDS TO DAYS
-- 3) MAP ROUNDS TO DAYS
local START, END = 62, 183
local START,END=62,183
local TOTAL_DAYS = END - START + 1
local TOTAL_ROUNDS = (#teams - 1) * 2
local function mapDays()
local function mapDays()
     local interval, extra = math.floor(TOTAL_DAYS / TOTAL_ROUNDS), TOTAL_DAYS % TOTAL_ROUNDS
    local total, rounds = END-START+1, (#teams-1)*2
     local interval, extra = math.floor(total/rounds), total%rounds
     local m, day = {}, START
     local m, day = {}, START
     for i = 1, TOTAL_ROUNDS do
     for i=1,rounds 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)
         if extra > 0 then extra = extra - 1 end
         if extra>0 then extra=extra-1 end
     end
     end
     return m
     return m
end
end


-- 4) SIMULATE RESULTS FOR A GIVEN DAY
-- 4) SIMULATE RESULTS FOR DAY
local function simulateDayResults(day)
local function simulateDayResults(day)
     local schedule = p.generateSchedule()
     local schedule, dm = p.generateSchedule(), mapDays()
    local dm = mapDays()
     local ridx = dm[day]
     local ridx = dm[day]
     if not ridx then return {} end
     if not ridx then return {} end
    local pairs = schedule[ridx]
     math.randomseed(day)
     math.randomseed(day)
     local results = {}
     local res={}
     for _, m in ipairs(pairs) do
     for _,m in ipairs(schedule[ridx]) do
         local h, a = m[1], m[2]
         local h,a=m[1],m[2]
         local drawProb = 0.1
         local draw=0.1
         local r = math.random()
         local r=math.random()
         local hg, ag
         local hg,ag
         if r < drawProb then
         if r<draw then local g=math.random(0,12); hg,ag=g,g
            local g = math.random(0, 12)
            hg, ag = g, g
         else
         else
             local wh, wa = h.championships + 1, a.championships + 1
             local wh,wa=h.championships+1,a.championships+1
             local probHome = wh / (wh + wa)
             local probHome=wh/(wh+wa)
             local homeWin = (r - drawProb) / (1 - drawProb) < probHome
             local homeWin=(r-draw)/(1-draw)<probHome
             local winG = math.random(1, 12)
             local winG,loseG=math.random(1,12),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(results, {home = h, away = a, hg = hg, ag = ag})
         table.insert(res,{home=h,away=a,hg=hg,ag=ag})
     end
     end
     return results
     return res
end
end


-- 5) RENDER TODAY'S MATCHDAY
-- 5) RENDER ROUND ROW
local function renderRound(day,year)
    local dm=mapDays()
    if not dm[day] then return "'''No matches scheduled today.'''" end
    local results=simulateDayResults(day)
    local month,dom = date.getDateFromDay(day)
    local label=string.format("%d %s %d PSSC",dom,month,year)
    local lines={}
    for _,m in ipairs(results) do
        local h,a,hg,ag=m.home,m.away,m.hg,m.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"
        table.insert(lines,string.format(
            "%s <span style=\"color:%s;\">'''%d'''</span> %s vs %s <span style=\"color:%s;\">'''%d'''</span> %s",
            h.logo,hcol,hg,h.fullName,a.logo,acol,ag,a.fullName
        ))
    end
    return string.format("|-%n| %d || %s || %s\n",day,label,table.concat(lines,'<br>'))
end
 
-- 6) RENDER SCHEDULE
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 51
     local year=tonumber(string.match(curr,'.*, (%d+) PSSC')) or 51
     if not mapDays()[day] then return "'''No matches scheduled today.'''" end
     local dm=mapDays()
     local header = {'{| class="wikitable sortable"', '! Day !! Date !! Matches'}
    if not dm[day] then return "'''No matches scheduled today.'''" end
    table.insert(header, renderRound(day, year))
     return table.concat({
    table.insert(header, '|}')
        '{| class="wikitable sortable"',
     return table.concat(header, "\n")
        '! Day !! Date !! Matches',
        renderRound(day,year),
        '|}'
     },"\n")
end
end


-- 6) RENDER RANKINGS WITH PF, PA, PD
-- 7) RENDER 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 _, t in ipairs(teams) do
         records[t.code]={team=t,W=0,D=0,L=0,Pts=0,PF=0,PA=0}
         records[t.code] = {team = t, W = 0, D = 0, L = 0, Pts = 0, PF = 0, PA = 0}
     end
     end
    -- accumulate results
     for day=START,last do
     for day = START, last do
         for _,res in ipairs(simulateDayResults(day)) do
         for _, res in ipairs(simulateDayResults(day)) do
             local hR,aR=records[res.home.code],records[res.away.code]
             local hRec = records[res.home.code]
             hR.PF,hR.PA=hR.PF+res.hg,hR.PA+res.ag
            local aRec = records[res.away.code]
             aR.PF,aR.PA=aR.PF+res.ag,aR.PA+res.hg
             -- update PF/PA
             if res.hg>res.ag then
            hRec.PF = hRec.PF + res.hg
                 hR.W,hR.Pts=hR.W+1,hR.Pts+3;aR.L=aR.L+1
            hRec.PA = hRec.PA + res.ag
             elseif res.hg<res.ag then
             aRec.PF = aRec.PF + res.ag
                 aR.W,aR.Pts=aR.W+1,aR.Pts+3;hR.L=hR.L+1
            aRec.PA = aRec.PA + res.hg
            -- update W/D/L and points
             if res.hg > res.ag then
                 hRec.W = hRec.W + 1; hRec.Pts = hRec.Pts + 3
                aRec.L = aRec.L + 1
             elseif res.hg < res.ag then
                 aRec.W = aRec.W + 1; aRec.Pts = aRec.Pts + 3
                hRec.L = hRec.L + 1
             else
             else
                 hRec.D = hRec.D + 1; hRec.Pts = hRec.Pts + 1
                 hR.D,hR.Pts=hR.D+1,hR.Pts+1
                 aRec.D = aRec.D + 1; aRec.Pts = aRec.Pts + 1
                 aR.D,aR.Pts=aR.D+1,aR.Pts+1
             end
             end
         end
         end
     end
     end
    -- compute PD and collect
     local list={}
     local list = {}
     for _,r in pairs(records) do r.PD=r.PF-r.PA;table.insert(list,r) end
     for _, rec in pairs(records) do
     table.sort(list,function(a,b)
        rec.PD = rec.PF - rec.PA
         if a.Pts~=b.Pts then return a.Pts>b.Pts end
        table.insert(list, rec)
         if a.PD~=b.PD   then return a.PD>b.PD   end
    end
         if a.W~=b.W     then return a.W>b.W     end
    -- sort by points, PD, wins, name
         return a.team.fullName<b.team.fullName
     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)
     end)
    -- render
     local out={'{| class="wikitable sortable"','! Pos !! Team !! W !! D !! L !! Pts !! PF !! PA !! PD'}
     local out = {'{| class="wikitable sortable"', '! Pos !! Team !! W !! D !! L !! Pts !! PF !! PA !! PD'}
     for i,r 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, i,
             i,i,r.team.logo,r.team.fullName,r.W,r.D,r.L,r.Pts,r.PF,r.PA,r.PD
            rec.team.logo, rec.team.fullName,
            rec.W, rec.D, rec.L, rec.Pts,
            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:02, 19 April 2025

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

local p = {}

-- Import calendar helpers from Module:P
local date = require('Module:P')

-- 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
function p.generateSchedule()
    local n, rounds = #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)
    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 total, rounds = END-START+1, (#teams-1)*2
    local interval, extra = math.floor(total/rounds), total%rounds
    local m, day = {}, START
    for i=1,rounds 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 FOR DAY
local function simulateDayResults(day)
    local schedule, dm = p.generateSchedule(), mapDays()
    local ridx = dm[day]
    if not ridx then return {} end
    math.randomseed(day)
    local res={}
    for _,m in ipairs(schedule[ridx]) do
        local h,a=m[1],m[2]
        local draw=0.1
        local r=math.random()
        local hg,ag
        if r<draw then local g=math.random(0,12); hg,ag=g,g
        else
            local wh,wa=h.championships+1,a.championships+1
            local probHome=wh/(wh+wa)
            local homeWin=(r-draw)/(1-draw)<probHome
            local winG,loseG=math.random(1,12),math.random(0,math.min(winG-1,12))
            if homeWin then hg,ag=winG,loseG else hg,ag=loseG,winG end
        end
        table.insert(res,{home=h,away=a,hg=hg,ag=ag})
    end
    return res
end

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

-- 6) RENDER SCHEDULE
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 51
    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) RENDER 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 _,t in ipairs(teams) do
        records[t.code]={team=t,W=0,D=0,L=0,Pts=0,PF=0,PA=0}
    end
    for day=START,last do
        for _,res in ipairs(simulateDayResults(day)) do
            local hR,aR=records[res.home.code],records[res.away.code]
            hR.PF,hR.PA=hR.PF+res.hg,hR.PA+res.ag
            aR.PF,aR.PA=aR.PF+res.ag,aR.PA+res.hg
            if res.hg>res.ag then
                hR.W,hR.Pts=hR.W+1,hR.Pts+3;aR.L=aR.L+1
            elseif res.hg<res.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 _,r in pairs(records) do r.PD=r.PF-r.PA;table.insert(list,r) 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,r in ipairs(list) do
        table.insert(out,string.format(
            "|-%n| %d || %s %s || %d || %d || %d || %d || %d || %d || %d",
            i,i,r.team.logo,r.team.fullName,r.W,r.D,r.L,r.Pts,r.PF,r.PA,r.PD
        ))
    end
    table.insert(out,'|}')
    return table.concat(out,"\n")
end

return p