Module:BassaridianElectionSim
From MicrasWiki
Documentation for this module may be created at Module:BassaridianElectionSim/doc
-- Module:BassaridianElectionSim
-- Simulated multi-party elections for Bassaridia Vaeringheim,
-- using the same PSSC calendar structure as PillSeasonSchedule.
local p = {}
local date = require('Module:BassaridianCalendar')
------------------------------------------------------------
-- 0) CONFIG / CALENDAR
------------------------------------------------------------
local DAYS_IN_YEAR = 183
local FIRST_ELECTION_YEAR = 50 -- first election anchor (adjust as needed)
local ELECTION_DAY = 122 -- election held on 122nd day of year
-- 61-day "months" matching PillSeasonSchedule logic
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
local function absDay(y, d)
return y * DAYS_IN_YEAR + d
end
------------------------------------------------------------
-- 1) PARTIES
------------------------------------------------------------
local parties = {
{
id = "CUF",
name = "Councilist Unity Front",
short = "CUF",
logo = "[[File:CUF logo.png|20px]]",
base = { city = 1.30, tribe = 0.90 },
},
{
id = "MSC",
name = "Morovian Social Cooperative",
short = "MSC",
logo = "[[File:MSC logo.png.png|20px]]",
base = { city = 1.10, tribe = 1.00 },
},
{
id = "PRP",
name = "Path of Renewal Party",
short = "PRP",
logo = "[[File:PRP logo.png|20px]]",
base = { city = 1.00, tribe = 1.10 },
},
{
id = "WLG",
name = "War League Guardians Bloc",
short = "WLG",
logo = "[[File:WLG logo.png|20px]]",
base = { city = 0.95, tribe = 1.30 },
},
{
id = "AESL",
name = "Alperkin Earth & Sky League",
short = "AESL",
logo = "[[File:AESL logo.png|20px]]",
base = { city = 0.60, tribe = 1.50 },
},
{
id = "DBM",
name = "Diaspora Bridges Movement",
short = "DBM",
logo = "[[File:DBM logo.png|20px]]",
base = { city = 0.90, tribe = 0.50 },
},
{
id = "FIML",
name = "Free Investors & Merchants League",
short = "FIML",
logo = "[[File:FIML logo.png|20px]]",
base = { city = 1.10, tribe = 0.40 },
},
{
id = "CRC",
name = "Civic Reform Covenant",
short = "CRC",
logo = "[[File:CRC logo.png.png|20px]]",
base = { city = 1.00, tribe = 0.80 },
},
}
local PARTY_BY_ID = {}
for _, pty in ipairs(parties) do
PARTY_BY_ID[pty.id] = pty
end
------------------------------------------------------------
-- 2) DISTRICTS
-- 3-seat city districts for ALL non-dependency cities
-- + 1-seat Alperkin tribal constituencies.
-- Each district also has:
-- • name – wikilinked display name
-- • key – plain key for lookups (companies etc.)
-- • profile– geographic / political profile
------------------------------------------------------------
local districts = {
-- Major Cities in Bassaridia Vaeringheim
{ key="Vaeringheim", name="[[List of cities in Bassaridia Vaeringheim#Vaeringheim|Vaeringheim]]", type="city", seats=3, profile="port_core" },
{ key="Luminaria", name="[[List of cities in Bassaridia Vaeringheim#Luminaria|Luminaria]]", type="city", seats=3, profile="shrine_mystic" },
{ key="Serena", name="[[List of cities in Bassaridia Vaeringheim#Serena|Serena]]", type="city", seats=3, profile="port_periphery" },
{ key="Pyralis", name="[[List of cities in Bassaridia Vaeringheim#Pyralis|Pyralis]]", type="city", seats=3, profile="war_frontier" },
{ key="Symphonara", name="[[List of cities in Bassaridia Vaeringheim#Symphonara|Symphonara]]", type="city", seats=3, profile="shrine_mystic" },
{ key="Aurelia", name="[[List of cities in Bassaridia Vaeringheim#Aurelia|Aurelia]]", type="city", seats=3, profile="agrarian" },
{ key="Somniumpolis", name="[[List of cities in Bassaridia Vaeringheim#Somniumpolis|Somniumpolis]]", type="city", seats=3, profile="port_periphery" },
{ key="Nexa", name="[[List of cities in Bassaridia Vaeringheim#Nexa|Nexa]]", type="city", seats=3, profile="war_frontier" },
{ key="LunalisSancta", name="[[List of cities in Bassaridia Vaeringheim#Lunalis Sancta|Lunalis Sancta]]", type="city", seats=3, profile="shrine_mystic" },
{ key="Sylvapolis", name="[[List of cities in Bassaridia Vaeringheim#Sylvapolis|Sylvapolis]]", type="city", seats=3, profile="agrarian" },
-- Minor Cities in Bassaridia Vaeringheim
{ key="Saluria", name="[[List of cities in Bassaridia Vaeringheim#Saluria|Saluria]]", type="city", seats=3, profile="port_periphery" },
{ key="Aetherium", name="[[List of cities in Bassaridia Vaeringheim#Aetherium|Aetherium]]", type="city", seats=3, profile="shrine_mystic" },
{ key="FerrumCitadel", name="[[List of cities in Bassaridia Vaeringheim#Ferrum Citadel|Ferrum Citadel]]", type="city", seats=3, profile="war_frontier" },
{ key="Acheron", name="[[List of cities in Bassaridia Vaeringheim#Acheron|Acheron]]", type="city", seats=3, profile="war_frontier" },
{ key="Erythros", name="[[List of cities in Bassaridia Vaeringheim#Erythros|Erythros]]", type="city", seats=3, profile="war_frontier" },
{ key="CatonisAtrium", name="[[List of cities in Bassaridia Vaeringheim#Catonis Atrium|Catonis Atrium]]", type="city", seats=3, profile="agrarian" },
{ key="Delphica", name="[[List of cities in Bassaridia Vaeringheim#Delphica|Delphica]]", type="city", seats=3, profile="port_core" },
{ key="Koinonia", name="[[List of cities in Bassaridia Vaeringheim#Koinonía|Koinonía]]", type="city", seats=3, profile="agrarian" },
{ key="Aureum", name="[[List of cities in Bassaridia Vaeringheim#Aureum|Aureum]]", type="city", seats=3, profile="port_periphery" },
-- Alperkin cities
{ key="TheAlpazkigz", name="[[List of cities in Bassaridia Vaeringheim#The Alpazkigz|The Alpazkigz]]", type="city", seats=3, profile="alperkin_city" },
{ key="Odiferia", name="[[List of cities in Bassaridia Vaeringheim#Odiferia|Odiferia]]", type="city", seats=3, profile="alperkin_city" },
-- Major Cities in New South Jangsong
{ key="Skyrophos", name="[[List of cities in Bassaridia Vaeringheim#Skýrophos|Skýrophos]]", type="city", seats=3, profile="war_frontier" },
{ key="Bjornopolis", name="[[List of cities in Bassaridia Vaeringheim#Bjornopolis|Bjornopolis]]", type="city", seats=3, profile="shrine_mystic" },
{ key="Aegirheim", name="[[List of cities in Bassaridia Vaeringheim#Aegirheim|Aegirheim]]", type="city", seats=3, profile="port_periphery" },
{ key="Norsolyra", name="[[List of cities in Bassaridia Vaeringheim#Norsolyra|Norsolyra]]", type="city", seats=3, profile="agrarian" },
{ key="Thorsalon", name="[[List of cities in Bassaridia Vaeringheim#Thorsalon|Thorsalon]]", type="city", seats=3, profile="agrarian" },
-- Minor Cities in New South Jangsong
{ key="Pelagia", name="[[List of cities in Bassaridia Vaeringheim#Pelagia|Pelagia]]", type="city", seats=3, profile="port_periphery" },
{ key="Myrene", name="[[List of cities in Bassaridia Vaeringheim#Myrene|Myrene]]", type="city", seats=3, profile="agrarian" },
{ key="Thyrea", name="[[List of cities in Bassaridia Vaeringheim#Thyrea|Thyrea]]", type="city", seats=3, profile="agrarian" },
{ key="Ephyra", name="[[List of cities in Bassaridia Vaeringheim#Ephyra|Ephyra]]", type="city", seats=3, profile="agrarian" },
{ key="Halicarn", name="[[List of cities in Bassaridia Vaeringheim#Halicarn|Halicarn]]", type="city", seats=3, profile="port_periphery" },
-- Major Cities in Haifan Bassaridia
{ key="KeybirAviv", name="[[List of cities in Bassaridia Vaeringheim#Keybir-Aviv|Keybir-Aviv]]", type="city", seats=3, profile="haifan_urban" },
{ key="TelAmin", name="[[List of cities in Bassaridia Vaeringheim#Tel-Amin|Tel-Amin]]", type="city", seats=3, profile="haifan_urban" },
{ key="Diamandis", name="[[List of cities in Bassaridia Vaeringheim#Diamandis|Diamandis]]", type="city", seats=3, profile="haifan_resort" },
{ key="Jogi", name="[[List of cities in Bassaridia Vaeringheim#Jogi|Jogi]]", type="city", seats=3, profile="haifan_resort" },
{ key="Lewisburg", name="[[List of cities in Bassaridia Vaeringheim#Lewisburg|Lewisburg]]", type="city", seats=3, profile="haifan_urban" },
-- Minor Cities in Haifan Bassaridia
{ key="Thermosalem", name="[[List of cities in Bassaridia Vaeringheim#Thermosalem|Thermosalem]]", type="city", seats=3, profile="haifan_resort" },
{ key="Akrostadium", name="[[List of cities in Bassaridia Vaeringheim#Akróstadium|Akróstadium]]", type="city", seats=3, profile="haifan_resort" },
{ key="Sufriya", name="[[List of cities in Bassaridia Vaeringheim#Sufriya|Sufriya]]", type="city", seats=3, profile="port_periphery" },
{ key="Lykopolis", name="[[List of cities in Bassaridia Vaeringheim#Lykopolis|Lykopolis]]", type="city", seats=3, profile="war_frontier" },
-- Bassaridian Normark
{ key="Ardclach", name="[[List of cities in Bassaridia Vaeringheim#Ardclach|Ardclach]]", type="city", seats=3, profile="normark_trade" },
{ key="Riddersborg", name="[[List of cities in Bassaridia Vaeringheim#Riddersborg|Riddersborg]]", type="city", seats=3, profile="normark_trade" },
-- Ouriana Province (major cities)
{ key="Bashkim", name="[[List of cities in Bassaridia Vaeringheim#Bashkim|Bashkim]]", type="city", seats=3, profile="agrarian" },
{ key="Ourid", name="[[List of cities in Bassaridia Vaeringheim#Ourid|Ourid]]", type="city", seats=3, profile="agrarian" },
-- Alperkin tribal constituencies (non-city, 1-seat)
{ key="WesternSteppesTribe", name="Western Steppes Alperkin Tribe", type="tribe", seats=1, profile="alperkin_tribe" },
{ key="NorthernCanyonTribe", name="Northern Canyon Alperkin Tribe", type="tribe", seats=1, profile="alperkin_tribe" },
{ key="MorovianShoreTribe", name="Morovian Shoreline Alperkin Tribe", type="tribe", seats=1, profile="alperkin_tribe" },
}
------------------------------------------------------------
-- 3) PROFILE-BASED BIAS
-- + cult-based influence (Reformed Stripping Path)
-- + company/economic influence (Port-linked firms)
------------------------------------------------------------
-- Geographical / structural profile bias
local profileBias = {
-- Core Port / capital
port_core = {
CUF = 1.15,
FIML = 1.20,
CRC = 1.05,
DBM = 1.05,
},
-- Secondary/coastal trade cities
port_periphery = {
CUF = 1.05,
MSC = 1.05,
FIML = 1.10,
DBM = 1.05,
},
-- Shrine-heavy / mystic centers
shrine_mystic = {
PRP = 1.20,
CRC = 1.10,
CUF = 1.05,
},
-- War frontier / militarized zones
war_frontier = {
WLG = 1.20,
CUF = 1.05,
MSC = 1.00,
},
-- Agrarian / rural valleys
agrarian = {
MSC = 1.15,
PRP = 1.05,
CUF = 0.95,
FIML = 0.95,
},
-- Normark/Bassarid northern trade
normark_trade = {
CUF = 1.10,
CRC = 1.10,
FIML = 1.05,
},
-- Alperkin cities
alperkin_city = {
AESL = 1.30,
MSC = 1.05,
PRP = 1.05,
},
-- Haifan resort/port cities
haifan_resort = {
MSC = 1.10,
PRP = 1.05,
CUF = 1.05,
DBM = 1.10,
},
-- Haifan dense urban centers
haifan_urban = {
MSC = 1.10,
CUF = 1.10,
CRC = 1.05,
DBM = 1.10,
},
-- Pure tribal constituencies
alperkin_tribe = {
AESL = 1.40,
WLG = 1.10,
MSC = 1.05,
PRP = 1.05,
},
}
------------------------------------------------------------
-- Cult influence
------------------------------------------------------------
local cultInfluence = {
Aurora = { PRP = 1.20, CRC = 1.05, CUF = 1.05 }, -- Order of Aurora Mystica
Azure = { CUF = 1.15, WLG = 1.10, FIML = 1.05 }, -- Azure Sentinel Sect
Harmony = { CUF = 1.10, CRC = 1.10, PRP = 1.05 }, -- Harmony Sanctum
Ignis = { PRP = 1.20, WLG = 1.10 }, -- Ignis Aeternum
Celestial = { PRP = 1.15, DBM = 1.05, CRC = 1.05 }, -- Celestial Harmony Sect
Golden = { FIML = 1.20, CUF = 1.10, WLG = 1.05 }, -- Guild of Golden Shadows
Reverie = { MSC = 1.15, DBM = 1.10 }, -- Reverie Nebulous
Eon = { CRC = 1.15, FIML = 1.05 }, -- Eon Fellowship
Umbral = { PRP = 1.10, MSC = 1.05, WLG = 1.05 }, -- Order of the Umbral Oracle
Verdant = { AESL = 1.20, MSC = 1.10 }, -- Verdant Embrace
Vitalis = { AESL = 1.10, MSC = 1.10, PRP = 1.05 }, -- Sanctum Vitalis
Alabaster = { FIML = 1.15, CUF = 1.05 }, -- Temple Alabaster
Ironclad = { WLG = 1.20, CUF = 1.05 }, -- Court of the Ironclad
Concordia = { CRC = 1.15, CUF = 1.05 }, -- Accord Concordia
Stygian = { WLG = 1.10, PRP = 1.05 }, -- Styx cults
Sylvan = { AESL = 1.20, MSC = 1.05 }, -- Sylvan Fellowship
RedMirth = { MSC = 1.10, PRP = 1.05 }, -- Mystery of Red Mirth
Rex = { FIML = 1.10, CUF = 1.10 }, -- Rex Catonis
Delphica = { MSC = 1.05, PRP = 1.05, CRC = 1.05 }, -- Sanctum Delphica
Amicitia = { MSC = 1.10, CRC = 1.10 }, -- Ordo Amicitia
Illuminata= { DBM = 1.15, CRC = 1.05 }, -- Temple Illuminata
Conclave = { PRP = 1.10, DBM = 1.10, CUF = 1.05 }, -- Conclave Illuminara
}
-- which cult constellations dominate each profile
local profileCults = {
port_core = { "Azure", "Golden", "Harmony" },
port_periphery = { "Azure", "Reverie" },
shrine_mystic = { "Aurora", "Celestial", "Harmony" },
war_frontier = { "Ironclad", "Stygian" },
agrarian = { "Verdant", "Sylvan", "Vitalis" },
normark_trade = { "Golden", "Eon" },
alperkin_city = { "Sylvan", "Verdant" },
haifan_resort = { "Conclave", "Reverie" },
haifan_urban = { "Conclave", "Harmony" },
alperkin_tribe = { "Sylvan", "Verdant" },
}
------------------------------------------------------------
-- Economic / company influence
-- Tags are informed by the General Port company table:
-- • port_finance – Temple Bank, Temple University of Delphica, Eosphorus, etc.
-- • agricoop – Gladeseed, Plains of Jogi, Ouriana Lowlands, Blood Vineyards…
-- • arms_heavy – Somniant Stock Fund, Baratar Corp, Thesean Iron, heavy industry
-- • fishery – Suncliff Fisheries, Caledonj Bay, coastal fleets
-- • diaspora_port – ports & corridors strongly tied to diaspora and foreign investors
-- • cult_services – cities where cultic/temple “service economy” is heavy
-- • war_religious – shrines closely tied to security campaigns
------------------------------------------------------------
local econInfluence = {
port_finance = {
FIML = 1.25,
CUF = 1.10,
CRC = 1.05,
DBM = 1.05,
},
agricoop = {
MSC = 1.20,
AESL = 1.05,
PRP = 1.05,
},
arms_heavy = {
WLG = 1.20,
CUF = 1.05,
FIML = 1.05,
},
fishery = {
MSC = 1.10,
DBM = 1.05,
},
diaspora_port = {
DBM = 1.20,
CUF = 1.05,
FIML = 1.05,
},
cult_services = {
PRP = 1.10,
CRC = 1.10,
},
war_religious = {
WLG = 1.15,
PRP = 1.05,
CUF = 1.05,
},
}
-- Which econ tags apply to which city (by key)
local econTagsByCity = {
Vaeringheim = { "port_finance", "diaspora_port" },
Luminaria = { "cult_services" },
Serena = { "port_finance" },
Pyralis = { "arms_heavy" },
Symphonara = { "cult_services" },
Aurelia = { "agricoop" },
Somniumpolis = { "arms_heavy", "diaspora_port" },
Nexa = { "arms_heavy" },
LunalisSancta = { "port_finance", "cult_services" },
Sylvapolis = { "agricoop" },
Saluria = { "agricoop", "cult_services" },
Aetherium = { "cult_services" },
FerrumCitadel = { "arms_heavy" },
Acheron = { "arms_heavy" },
Erythros = { "arms_heavy" },
CatonisAtrium = { "agricoop" },
Delphica = { "port_finance", "cult_services" },
Koinonia = { "agricoop" },
Aureum = { "port_finance" },
TheAlpazkigz = { "agricoop" },
Odiferia = { "agricoop" },
Skyrophos = { "arms_heavy" },
Bjornopolis = { "cult_services" },
Aegirheim = { "arms_heavy", "port_finance" },
Norsolyra = { "agricoop" },
Thorsalon = { "agricoop", "diaspora_port" },
Pelagia = { "war_religious", "diaspora_port" },
Myrene = { "agricoop" },
Thyrea = { "agricoop" },
Ephyra = { "agricoop", "fishery" },
Halicarn = { "port_finance" },
KeybirAviv = { "port_finance", "diaspora_port" },
TelAmin = { "agricoop", "diaspora_port" },
Diamandis = { "diaspora_port", "cult_services" },
Jogi = { "arms_heavy", "agricoop", "diaspora_port" },
Lewisburg = { "port_finance", "diaspora_port" },
Thermosalem = { "diaspora_port", "cult_services" },
Akrostadium = { "diaspora_port" },
Sufriya = { "port_finance", "diaspora_port" },
Lykopolis = { "war_religious" },
Ardclach = { "port_finance" },
Riddersborg = { "port_finance" },
Bashkim = { "agricoop" },
Ourid = { "agricoop" },
-- tribes intentionally left blank (their economy comes via alperkin_tribe profile)
}
------------------------------------------------------------
-- 4) RANDOMNESS (DETERMINISTIC BY YEAR/DISTRICT/PARTY)
------------------------------------------------------------
local function partyScore(year, dIndex, pIndex, dType, profile, cityKey)
local party = parties[pIndex]
local base = (party.base[dType] or 1.0)
-- deterministic seed: year + district index + party index
local seed = year * 100000 + dIndex * 1000 + pIndex
math.randomseed(seed)
-- jitter in roughly [-0.20, +0.20]
local jitter = (math.random() - 0.5) * 0.4
local score = base * (1 + jitter)
-- (a) profile-based bias
if profile then
local pb = profileBias[profile]
if pb and pb[party.id] then
score = score * pb[party.id]
end
end
-- (b) cult-based influence for this profile
local cults = profile and profileCults[profile] or nil
if cults then
for _, cid in ipairs(cults) do
local cf = cultInfluence[cid]
if cf and cf[party.id] then
score = score * cf[party.id]
end
end
end
-- (c) company / economic influence for this city (if any)
local tags = cityKey and econTagsByCity[cityKey] or nil
if tags then
for _, tag in ipairs(tags) do
local ef = econInfluence[tag]
if ef and ef[party.id] then
score = score * ef[party.id]
end
end
end
if score < 0 then score = 0 end
return score
end
------------------------------------------------------------
-- 5) SEAT ALLOCATION (LARGEST REMAINDER / HARE QUOTA)
------------------------------------------------------------
local function allocateSeats(votes, seats)
local total = 0
for _, v in pairs(votes) do total = total + v end
local alloc = {}
local remainders = {}
if total <= 0 or seats <= 0 then
for id,_ in pairs(votes) do
alloc[id] = 0
remainders[id] = 0
end
return alloc, total
end
local quota = total / seats
local assigned = 0
for id, v in pairs(votes) do
local exact = v / quota
local base = math.floor(exact)
alloc[id] = base
assigned = assigned + base
remainders[#remainders+1] = { id = id, rem = exact - base }
end
local remaining = seats - assigned
table.sort(remainders, function(a, b) return a.rem > b.rem end)
for i = 1, remaining do
local r = remainders[i]
if r then
alloc[r.id] = (alloc[r.id] or 0) + 1
end
end
return alloc, total
end
local function normalizePercentages(votes)
local total = 0
for _, v in pairs(votes) do total = total + v end
local pct = {}
if total <= 0 then
for id,_ in pairs(votes) do pct[id] = 0 end
return pct, total
end
for id, v in pairs(votes) do
pct[id] = (v / total) * 100
end
return pct, total
end
local function sortedPartyList(votes, seatsAlloc)
local list = {}
for _, party in ipairs(parties) do
local id = party.id
list[#list+1] = {
party = party,
votes = votes[id] or 0,
seats = seatsAlloc[id] or 0,
}
end
table.sort(list, function(a, b)
if a.seats ~= b.seats then return a.seats > b.seats end
if a.votes ~= b.votes then return a.votes > b.votes end
return a.party.name < b.party.name
end)
return list
end
------------------------------------------------------------
-- 6) CORE SIMULATION
------------------------------------------------------------
local function simulateDistrict(year, dIndex)
local d = districts[dIndex]
local votes = {}
for pIndex, party in ipairs(parties) do
votes[party.id] = partyScore(
year, dIndex, pIndex, d.type, d.profile, d.key
)
end
local seatsAlloc, totalVotes = allocateSeats(votes, d.seats)
local pct, _ = normalizePercentages(votes)
local sorted = sortedPartyList(votes, seatsAlloc)
return {
district = d,
votes = votes,
percentages = pct,
seats = seatsAlloc,
totalVotes = totalVotes,
sorted = sorted,
}
end
local function simulateNation(year)
local results = {}
local seatTotal = {}
local voteTotal = {}
for _, party in ipairs(parties) do
seatTotal[party.id] = 0
voteTotal[party.id] = 0
end
for i = 1, #districts do
local r = simulateDistrict(year, i)
results[i] = r
for id, s in pairs(r.seats) do
seatTotal[id] = seatTotal[id] + (s or 0)
end
for id, v in pairs(r.votes) do
voteTotal[id] = voteTotal[id] + (v or 0)
end
end
local pctNation, totalNation = normalizePercentages(voteTotal)
return {
districts = results,
seatTotal = seatTotal,
voteTotal = voteTotal,
pctNation = pctNation,
totalNation = totalNation,
}
end
------------------------------------------------------------
-- 7) ELECTION TIMING (BASED ON BASSARIDIAN CALENDAR)
------------------------------------------------------------
local function getCurrentDayYear()
local curr = date.getCurrentDate() -- e.g. "67, 52 PSSC"
local day = tonumber(curr:match("^(%d+),")) or 0
local year = tonumber(curr:match(", (%d+) PSSC")) or 0
return day, year
end
-- Elections every year from FIRST_ELECTION_YEAR onward
local function isElectionYear(year)
return year >= FIRST_ELECTION_YEAR
end
------------------------------------------------------------
-- 8) RENDER FUNCTIONS
------------------------------------------------------------
-- District-by-district results
-- Usage:
-- {{#invoke:BassaridianElectionSim|renderElectionResults}}
-- {{#invoke:BassaridianElectionSim|renderElectionResults|year=52|day=183}}
function p.renderElectionResults(frame)
local args = frame.args or {}
local dayNow, yearNow = getCurrentDayYear()
local year = tonumber(args.year or "") or yearNow
local day = tonumber(args.day or "") or dayNow
local showZero = (args.show_zero and args.show_zero ~= "0") or false
if not isElectionYear(year) then
return string.format(
"'''No general election in %d PSSC.'''",
year
)
end
if day < ELECTION_DAY then
local mon, dom = getMonthDay(ELECTION_DAY)
return string.format(
"'''General election scheduled for %d %s %d PSSC.'''",
dom, mon, year
)
end
local nation = simulateNation(year)
local mon, dom = getMonthDay(ELECTION_DAY)
local headerDate = string.format("%d %s %d PSSC", dom, mon, year)
local out = {}
out[#out+1] = '{| class="wikitable sortable"'
out[#out+1] = '|+ General Election Results — ' .. headerDate
out[#out+1] = '! District !! Seats !! Party !! Seats Won !! Vote %'
for _, r in ipairs(nation.districts) do
local d = r.district
-- Build the list of party rows we actually want to render
local rows = {}
for _, entry in ipairs(r.sorted) do
local seatsWon = r.seats[entry.party.id] or 0
local votePct = r.percentages[entry.party.id] or 0
if seatsWon > 0 or showZero then
rows[#rows+1] = {
party = entry.party,
seats = seatsWon,
pct = votePct,
}
end
end
if #rows > 0 then
-- First row for this district, with rowspans on District + Seats
local first = rows[1]
out[#out+1] = "|-"
out[#out+1] = string.format(
'| rowspan="%d" | %s || rowspan="%d" | %d || %s %s || %d || %.1f',
#rows,
d.name,
#rows,
d.seats,
first.party.logo or "",
first.party.name,
first.seats,
first.pct
)
-- Subsequent rows: Party, Seats Won, Vote %
for i = 2, #rows do
local row = rows[i]
out[#out+1] = "|-"
out[#out+1] = string.format(
'| %s %s || %d || %.1f',
row.party.logo or "",
row.party.name,
row.seats,
row.pct
)
end
end
end
out[#out+1] = "|}"
return table.concat(out, "\n")
end
-- National summary: total seats + national vote share
-- Usage:
-- {{#invoke:BassaridianElectionSim|renderNationalSummary}}
function p.renderNationalSummary(frame)
local args = frame.args or {}
local dayNow, yearNow = getCurrentDayYear()
local year = tonumber(args.year or "") or yearNow
local day = tonumber(args.day or "") or dayNow
if not isElectionYear(year) or day < ELECTION_DAY then
return "'''National summary available only once the general election has been held.'''"
end
local nation = simulateNation(year)
local out = {}
out[#out+1] = '{| class="wikitable sortable"'
out[#out+1] = '! Party !! Seats (total) !! National Vote %'
for _, party in ipairs(parties) do
local seats = nation.seatTotal[party.id] or 0
local pct = nation.pctNation[party.id] or 0
out[#out+1] = "|-"
out[#out+1] = string.format(
"| %s %s || %d || %.1f",
party.logo or "",
party.name,
seats,
pct
)
end
out[#out+1] = "|}"
return table.concat(out, "\n")
end
------------------------------------------------------------
-- EXPORT
------------------------------------------------------------
return p