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') | ||
-- TEAM DEFINITIONS | -- 1) TEAM DEFINITIONS | ||
local teams = { | local teams = { | ||
{ code="Allegro Symphonara", fullName="Allegro Symphonara", | { code="Allegro Symphonara", fullName="Allegro Symphonara", championships=4 }, | ||
{ code="Ashguard Pyralis", fullName="Ashguard Pyralis", | { code="Ashguard Pyralis", fullName="Ashguard Pyralis", championships=1 }, | ||
{ code="Aurelia Auric", fullName="Aurelia Auric", | { code="Aurelia Auric", fullName="Aurelia Auric", championships=2 }, | ||
{ code="Brightpath Aureum", fullName="Brightpath Aureum", | { code="Brightpath Aureum", fullName="Brightpath Aureum", championships=2 }, | ||
{ code="Celestial Sancta Lunaris",fullName="Celestial Sancta Lunaris | { code="Celestial Sancta Lunaris",fullName="Celestial Sancta Lunaris", championships=1 }, | ||
{ code="Delphica Windborne", fullName="Delphica Windborne", | { code="Delphica Windborne", fullName="Delphica Windborne", championships=1 }, | ||
{ code="Emberstone Vaeringheim", fullName="Emberstone Vaeringheim", | { code="Emberstone Vaeringheim", fullName="Emberstone Vaeringheim", championships=3 }, | ||
{ code="Flameborne Erythros", fullName="Flameborne Erythros", | { code="Flameborne Erythros", fullName="Flameborne Erythros", championships=1 }, | ||
{ code="Forgeward Nexa", fullName="Forgeward Nexa", | { code="Forgeward Nexa", fullName="Forgeward Nexa", championships=0 }, | ||
{ code="Hearthkeeper Koinon", fullName="Hearthkeeper Koinon", | { code="Hearthkeeper Koinon", fullName="Hearthkeeper Koinon", championships=0 }, | ||
{ code="Ironbough Sylvapolis", fullName="Ironbough Sylvapolis", | { code="Ironbough Sylvapolis", fullName="Ironbough Sylvapolis", championships=1 }, | ||
{ code="Redcliff Citadel", fullName="Redcliff Citadel", | { code="Redcliff Citadel", fullName="Redcliff Citadel", championships=1 }, | ||
{ code="Riftwarden Acheron", fullName="Riftwarden Acheron", | { code="Riftwarden Acheron", fullName="Riftwarden Acheron", championships=3 }, | ||
{ code="Saluria Skylight", fullName="Saluria Skylight", | { code="Saluria Skylight", fullName="Saluria Skylight", championships=0 }, | ||
{ code="Sandveil Somniumpolis", fullName="Sandveil Somniumpolis", | { code="Sandveil Somniumpolis", fullName="Sandveil Somniumpolis", championships=1 }, | ||
{ code="Serena Tidesong", fullName="Serena Tidesong", | { code="Serena Tidesong", fullName="Serena Tidesong", championships=2 }, | ||
{ code="Steppe Luminaria", fullName="Steppe Luminaria", | { code="Steppe Luminaria", fullName="Steppe Luminaria", championships=3 }, | ||
{ code="Strider Myrene", fullName="Strider Myrene", | { code="Strider Myrene", fullName="Strider Myrene", championships=1 }, | ||
{ code="Vaeringheim Pillar", fullName="Vaeringheim Pillar", | { code="Vaeringheim Pillar", fullName="Vaeringheim Pillar", championships=5 }, | ||
{ code="Imperial Delphica", fullName="Imperial Delphica", | { code="Imperial Delphica", fullName="Imperial Delphica", championships=1 }, | ||
{ code="Ascendant Aetherium", fullName="Ascendant Aetherium", | { code="Ascendant Aetherium", fullName="Ascendant Aetherium", championships=2 }, | ||
{ code="Sufriya Stormwake", fullName="Sufriya Stormwake", | { code="Sufriya Stormwake", fullName="Sufriya Stormwake", championships=1 }, | ||
{ code="Pillarion Club Suncliff", fullName="Pillarion Club Suncliff | { code="Pillarion Club Suncliff", fullName="Pillarion Club Suncliff", championships=0 }, | ||
{ code="Jogi Regiment", fullName="Jogi Regiment", | { code="Jogi Regiment", fullName="Jogi Regiment", championships=0 }, | ||
{ code="Vine Fleet Mylecia", fullName="Vine Fleet Mylecia", | { code="Vine Fleet Mylecia", fullName="Vine Fleet Mylecia", championships=0 }, | ||
{ code="Port of Blore Heath", fullName="Port of Blore Heath", | { code="Port of Blore Heath", fullName="Port of Blore Heath", championships=0 }, | ||
{ code="Free State Abeis", fullName="Free State Abeis", | { code="Free State Abeis", fullName="Free State Abeis", championships=0 }, | ||
{ code="Jezeraah City", fullName="Jezeraah City", | { code="Jezeraah City", fullName="Jezeraah City", championships=0 } | ||
} | } | ||
-- 2) SCHEDULE GENERATOR | -- 2) SCHEDULE GENERATOR | ||
local function generateSchedule() | local function generateSchedule() | ||
local n=#teams | local n = #teams | ||
assert(n%2==0) | assert(n % 2 == 0) | ||
local pool={} for i=1,n do pool[i]=teams[i] end | local pool = {} | ||
local fixed=table.remove(pool,1) | for i = 1, n do pool[i] = teams[i] end | ||
local rounds={} | local fixed = table.remove(pool, 1) | ||
for r=1,n-1 do | local rounds = {} | ||
local pairings={} | for r = 1, n - 1 do | ||
if r%2==1 then table.insert(pairings,{fixed,pool[1]}) else table.insert(pairings,{pool[1],fixed}) | local pairings = {} | ||
if r % 2 == 1 then | |||
table.insert(pairings, {fixed, pool[1]}) | |||
else | |||
table.insert(pairings, {pool[1], fixed}) | |||
end | end | ||
rounds[r]=pairings | for i = 2, n/2 do | ||
table.insert(pool,1,table.remove(pool)) | 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 | 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 | ||
rounds[n-1+r]=rev | table.insert(rev, {m[2], m[1]}) | ||
end | |||
rounds[n - 1 + r] = rev | |||
end | end | ||
return rounds | return rounds | ||
| Line 61: | Line 72: | ||
-- 3) MAP ROUNDS TO DAYS | -- 3) MAP ROUNDS TO DAYS | ||
local START,END=62,183 | local START, END = 62, 183 | ||
local function mapDays() | local function mapDays() | ||
local | local totalDays = END - START + 1 | ||
local | local totalRounds = (#teams - 1) * 2 | ||
local interval | local interval = math.floor(totalDays / totalRounds) | ||
local m | local extra = totalDays % totalRounds | ||
for i=1, | local m = {} | ||
m[day]=i | local day = START | ||
day=day+interval+(extra>0 and 1 or 0) | for i = 1, totalRounds do | ||
if extra>0 then extra=extra-1 end | m[day] = i | ||
day = day + interval + (extra > 0 and 1 or 0) | |||
if extra > 0 then extra = extra - 1 end | |||
end | end | ||
return m | return m | ||
| Line 77: | Line 90: | ||
-- 4) SIMULATE RESULTS | -- 4) SIMULATE RESULTS | ||
local function simulateDayResults(day) | local function simulateDayResults(day) | ||
local | local schedule = generateSchedule() | ||
local dm=mapDays() | local dm = mapDays() | ||
local ridx=dm[day] if not ridx then return {} end | local ridx = dm[day] | ||
if not ridx then return {} end | |||
math.randomseed(day) | math.randomseed(day) | ||
local | local results = {} | ||
for _, | for _, match in ipairs(schedule[ridx]) do | ||
local | local home, away = match[1], match[2] | ||
local hg,ag | local drawProb = 0.1 | ||
if r< | local r = math.random() | ||
else local wh | local hg, ag | ||
local rem=(r- | if r < drawProb then | ||
local winG | hg = math.random(0, 12) | ||
if | 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 | end | ||
table.insert( | table.insert(results, {home = home, away = away, hg = hg, ag = ag}) | ||
end | end | ||
return | return results | ||
end | end | ||
-- 5) RENDER ROUND | -- 5) RENDER 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=dom.." "..month.." "..year.." PSSC" | local label = dom .. " " .. month .. " " .. year .. " PSSC" | ||
local rows = {} | |||
local rows= | for _, res in ipairs(simulateDayResults(day)) do | ||
for _, | local h, a, hg, ag = res.home, res.away, res.hg, res.ag | ||
local h,a,hg,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" | local homeSpan = string.format("<span style='color:%s;'>'''%d'''</span>", hcol, hg) | ||
local homeSpan="<span style='color: | local awaySpan = string.format("<span style='color:%s;'>'''%d'''</span>", acol, ag) | ||
local awaySpan="<span style='color: | rows[#rows+1] = string.format( | ||
rows= | "%s %s %s vs %s %s %s", | ||
h.logo, homeSpan, h.fullName, | |||
a.logo, awaySpan, a.fullName | |||
) | |||
end | end | ||
local content = table.concat(rows, "<br>") | |||
return string.format("|-%s| %d || %s || %s", "\n", day, label, content) | |||
return "|- | |||
end | end | ||
-- 6) TODAY'S MATCHDAY | -- 6) TODAY'S MATCHDAY | ||
function p.renderSchedule(frame) | function p.renderSchedule(frame) | ||
local curr=date.getCurrentDate() | local curr = date.getCurrentDate() | ||
local day=tonumber(curr:match('^(%d+),')) or 0 | local day = tonumber(curr:match('^(%d+),')) or 0 | ||
local year=tonumber(curr:match('.*, (%d+) PSSC')) or 0 | local year = tonumber(curr:match('.*, (%d+) PSSC')) or 0 | ||
if not mapDays()[day] then return "'''No matches scheduled today'''" end | if not mapDays()[day] then return "'''No matches scheduled today'''" end | ||
return '{| class="wikitable sortable" | return table.concat({ | ||
'{| class="wikitable sortable"', | |||
'! Day !! Date !! Matches', | |||
renderRound(day, year), | |||
'|}' | |||
}, "\n") | |||
end | end | ||
-- 7) CURRENT STANDINGS | -- 7) CURRENT STANDINGS WITHOUT LOGOS | ||
function p.renderStandings(frame) | function p.renderStandings(frame) | ||
local curr=date.getCurrentDate() | local curr = date.getCurrentDate() | ||
local today=tonumber(curr:match('^(%d+),')) or 0 | local today = tonumber(curr:match('^(%d+),')) or 0 | ||
local last=math.min(today,END) | local last = math.min(today, END) | ||
local records={} | 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 _, t in ipairs(teams) do | ||
for d=START,last do for _,res in ipairs(simulateDayResults(d)) do | records[t.code] = {team = t, W = 0, D = 0, L = 0, Pts = 0, PF = 0, PA = 0} | ||
local hR,aR=records[res.home.code],records[res.away.code] | end | ||
hR.PF, hR.PA = hR.PF+res.hg, hR.PA+res.ag | for d = START, last do | ||
aR.PF, aR.PA = aR.PF+res.ag, aR.PA+res.hg | for _, res in ipairs(simulateDayResults(d)) do | ||
if res.hg>res.ag then hR.W,hR.Pts=hR.W+1,hR.Pts+3 | local hR, aR = records[res.home.code], records[res.away.code] | ||
elseif res.hg<res.ag then aR.W,aR.Pts=aR.W+1,aR.Pts+3 | hR.PF, hR.PA = hR.PF + res.hg, hR.PA + res.ag | ||
else hR.D,hR.Pts=hR.D+1,hR.Pts+1 | aR.PF, aR.PA = aR.PF + res.ag, aR.PA + res.hg | ||
if res.hg > res.ag then | |||
local list={} | hR.W, hR.Pts = hR.W + 1, hR.Pts + 3 | ||
for _,rec in pairs(records) do rec.PD=rec.PF-rec.PA | aR.L = aR.L + 1 | ||
table.sort(list,function(a,b) | elseif res.hg < res.ag then | ||
if a.Pts~=b.Pts then return a.Pts>b.Pts end | aR.W, aR.Pts = aR.W + 1, aR.Pts + 3 | ||
if a.PD~=b.PD then return a.PD>b.PD end | hR.L = hR.L + 1 | ||
if a.W~=b.W then return a.W>b.W end | else | ||
return a.team.fullName<b.team.fullName | 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 _, 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) | end) | ||
local out='{| class="wikitable sortable" | local out = {'{| 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 | ||
out | table.insert(out, string.format( | ||
'|-\n| %d || %s || %d || %d || %d || %d || %d || %d || %d', | |||
i, rec.team.fullName, | |||
rec.W, rec.D, rec.L, rec.Pts, | |||
rec.PF, rec.PA, rec.PD | |||
)) | |||
end | end | ||
out | table.insert(out, '|}') | ||
return out | return table.concat(out, '\n') | ||
end | end | ||
return p | return p | ||
Revision as of 19:21, 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", championships=4 },
{ code="Ashguard Pyralis", fullName="Ashguard Pyralis", championships=1 },
{ code="Aurelia Auric", fullName="Aurelia Auric", championships=2 },
{ code="Brightpath Aureum", fullName="Brightpath Aureum", championships=2 },
{ code="Celestial Sancta Lunaris",fullName="Celestial Sancta Lunaris", championships=1 },
{ code="Delphica Windborne", fullName="Delphica Windborne", championships=1 },
{ code="Emberstone Vaeringheim", fullName="Emberstone Vaeringheim", championships=3 },
{ code="Flameborne Erythros", fullName="Flameborne Erythros", championships=1 },
{ code="Forgeward Nexa", fullName="Forgeward Nexa", championships=0 },
{ code="Hearthkeeper Koinon", fullName="Hearthkeeper Koinon", championships=0 },
{ code="Ironbough Sylvapolis", fullName="Ironbough Sylvapolis", championships=1 },
{ code="Redcliff Citadel", fullName="Redcliff Citadel", championships=1 },
{ code="Riftwarden Acheron", fullName="Riftwarden Acheron", championships=3 },
{ code="Saluria Skylight", fullName="Saluria Skylight", championships=0 },
{ code="Sandveil Somniumpolis", fullName="Sandveil Somniumpolis", championships=1 },
{ code="Serena Tidesong", fullName="Serena Tidesong", championships=2 },
{ code="Steppe Luminaria", fullName="Steppe Luminaria", championships=3 },
{ code="Strider Myrene", fullName="Strider Myrene", championships=1 },
{ code="Vaeringheim Pillar", fullName="Vaeringheim Pillar", championships=5 },
{ code="Imperial Delphica", fullName="Imperial Delphica", championships=1 },
{ code="Ascendant Aetherium", fullName="Ascendant Aetherium", championships=2 },
{ code="Sufriya Stormwake", fullName="Sufriya Stormwake", championships=1 },
{ code="Pillarion Club Suncliff", fullName="Pillarion Club Suncliff", championships=0 },
{ code="Jogi Regiment", fullName="Jogi Regiment", championships=0 },
{ code="Vine Fleet Mylecia", fullName="Vine Fleet Mylecia", championships=0 },
{ code="Port of Blore Heath", fullName="Port of Blore Heath", championships=0 },
{ code="Free State Abeis", fullName="Free State Abeis", championships=0 },
{ code="Jezeraah City", fullName="Jezeraah City", championships=0 }
}
-- 2) SCHEDULE 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 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 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 label = dom .. " " .. month .. " " .. year .. " PSSC"
local rows = {}
for _, res in ipairs(simulateDayResults(day)) do
local h, a, 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)
rows[#rows+1] = string.format(
"%s %s %s vs %s %s %s",
h.logo, homeSpan, h.fullName,
a.logo, awaySpan, a.fullName
)
end
local content = table.concat(rows, "<br>")
return string.format("|-%s| %d || %s || %s", "\n", day, label, content)
end
-- 6) TODAY'S MATCHDAY
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
if not mapDays()[day] then return "'''No matches scheduled today'''" end
return table.concat({
'{| class="wikitable sortable"',
'! Day !! Date !! Matches',
renderRound(day, year),
'|}'
}, "\n")
end
-- 7) CURRENT STANDINGS WITHOUT LOGOS
function p.renderStandings(frame)
local curr = date.getCurrentDate()
local today = tonumber(curr:match('^(%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 d = START, last do
for _, res in ipairs(simulateDayResults(d)) 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 _, 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 || %d || %d || %d || %d || %d || %d || %d',
i, 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