Module:StraitsDaily: Difference between revisions

From MicrasWiki
Jump to navigationJump to search
NewZimiaGov (talk | contribs)
No edit summary
NewZimiaGov (talk | contribs)
No edit summary
Line 42: Line 42:
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
local sectors = {
local sectors = {
     "Strait South Inbound","Strait North Outbound","Mid-Strait Separation Zone",
     "Strait South Inbound", "Strait South Outbound", "Strait North Inbound","Strait North Outbound", "Archipelago Zone",
     "Sacred Buffer Approaches","Environmental Buffer","Northern Approach TSS","Southern Approach TSS"
     "Sacred Buffer Approaches","Environmental Buffer","Northern Approach TSS","Southern Approach TSS"
}
}

Revision as of 00:02, 6 September 2025

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

-------------------------------------------------------------------------------
-- Module: StraitsDaily
-- Purpose: Daily, in-world report of Strait of Haifa enforcement
-- Changes in this version:
--  • No mention of Florian cargo or the Imperial Federation.
--  • Explicit inclusion of Moorland and Bassaridian-flag vessels.
--  • Frequency smoothing + per-flag caps to avoid over-mentioning any registry.
--  • Non-escalatory language, outcome caps, and balance guardrails retained.
-------------------------------------------------------------------------------

local p = {}

-------------------------------------------------------------------------------
-- (A) PSSC DATE
-------------------------------------------------------------------------------
local function getPSSCDate()
    local startDate = os.time({year = 1999, month = 8, day = 6})
    local secondsInDay = 86400
    local daysPerYear  = 183
    local now = os.time()
    local totalDaysElapsed = math.floor((now - startDate) / secondsInDay)

    local yearFraction = totalDaysElapsed / daysPerYear
    local psscYear     = math.floor(yearFraction)
    local dayOfYear    = math.floor((yearFraction - psscYear) * daysPerYear) + 1

    local month, dayOfMonth
    if dayOfYear <= 61 then
        month = 1; dayOfMonth = dayOfYear
    elseif dayOfYear <= 122 then
        month = 2; dayOfMonth = dayOfYear - 61
    else
        month = 3; dayOfMonth = dayOfYear - 122
    end

    local dateString = string.format("%d/%d/%d PSSC", dayOfMonth, month, psscYear)
    return { text = dateString, year = psscYear, dayOfYear = dayOfYear }
end

-------------------------------------------------------------------------------
-- (B) STATIC DATA
-------------------------------------------------------------------------------
local sectors = {
    "Strait South Inbound", "Strait South Outbound", "Strait North Inbound","Strait North Outbound", "Archipelago Zone",
    "Sacred Buffer Approaches","Environmental Buffer","Northern Approach TSS","Southern Approach TSS"
}

-- Registries / flags:
--  - Explicit: Moorland, Bassaridian-flag
--  - Generic/neutral: Neutral Merchant, Distant Partner (Euranidom), Distant Partner (Corumia), Distant Partner (Southern Keltiania), Consortium Registry, Other Coastal State
-- Per-flag 'risky' toggles are modest, to stay non-escalatory.
local flags = {
    {name="Bassaridian-flag",           weight=6, risky=false},
    {name="Moorland",                   weight=1, risky=false},
    {name="Imperial Federation",        weight=5, risky=true},
    {name="Neutral Merchant",           weight=4, risky=false},
    {name="Distant Partner",            weight=3, risky=true},
    {name="Consortium Registry",        weight=2, risky=true},
    {name="Other Coastal State",        weight=7, risky=false},
}

local vessels = {
    "Handymax bulker","Product tanker","General cargo ship","Container feeder",
    "Ro-Ro ferry","Research craft","Declared submarine","Undeclared submarine"
}

local clauses = {
    {key="Pilotage & Traffic Scheme", cite="SC §A2–A4"},
    {key="Tonnage & Warship Limits",  cite="SC §B1–B4"},
    {key="Submarine Surface/Declare", cite="SC §C2–C3"},
    {key="Environmental Buffer",      cite="SC §D1–D3"},
    {key="Sacred Site Stand-off",     cite="SC §E1–E2"},
    {key="Emergency/Leviathan",       cite="SC §L1–L4"},
    {key="Right-of-Visit (Suspicion)",cite="SC §R1–R3"}
}

local actions = {
    {name="Documents & pilotage verification",      weight=5, basis="Pilotage & Traffic Scheme"},
    {name="Tonnage/armament declaration check",     weight=4, basis="Tonnage & Warship Limits"},
    {name="Environmental reroute order",            weight=3, basis="Environmental Buffer"},
    {name="Sacred buffer stand-off adjustment",     weight=3, basis="Sacred Site Stand-off"},
    {name="Submarine declare signal issued",        weight=2, basis="Submarine Surface/Declare"},
    {name="Right-of-visit boarding (lawful suspicion)", weight=2, basis="Right-of-Visit (Suspicion)"},
    {name="SAR escort under Leviathan Protocol",    weight=2, basis="Emergency/Leviathan"}
}

local outcomes = {
    "Compliant; cleared",
    "Minor non-compliance; corrected; cleared",
    "Rerouted; passage maintained",
    "Inspection completed; cargo intact",
    "Administrative notice issued; monitoring",
    "Escorted to anchorage for formal review",
    "Denied entry pending documents",
    "SAR/aid rendered; voyage resumed",
    "No reports"
}

-------------------------------------------------------------------------------
-- (C) UTILITIES
-------------------------------------------------------------------------------
local function pickWeighted(list)
    local total=0
    for _,item in ipairs(list) do total=total+(item.weight or 1) end
    local roll,cumul=math.random(total),0
    for _,item in ipairs(list) do
        cumul=cumul+(item.weight or 1)
        if roll<=cumul then return item end
    end
    return list[#list]
end
local function pick(list) return list[math.random(#list)] end
local clauseIndex = (function()
    local t={} for _,c in ipairs(clauses) do t[c.key]=c.cite end; return t
end)()

-------------------------------------------------------------------------------
-- (D) ENFORCEMENT PROBABILITY & FLAG SMOOTHING
-------------------------------------------------------------------------------
local function baseProb(flag, sector)
    local p = 0.30
    if flag.risky then p = p + 0.10 end         -- mild, non-accusatory bias
    if sector=="Sacred Buffer Approaches" or sector=="Environmental Buffer" then
        p = p + 0.10
    end
    if p>0.60 then p=0.60 end
    return p
end

-- To avoid over-mentioning, impose per-flag cap and discourage repeats.
local function pickFlagWithCaps(flagCounts, maxPerFlag, lastFlagName)
    local tries = 0
    while tries < 20 do
        local f = pickWeighted(flags)
        if (flagCounts[f.name] or 0) < maxPerFlag then
            -- discourage immediate repeats 50% of the time
            if lastFlagName and f.name == lastFlagName and math.random() < 0.5 then
                tries = tries + 1
            else
                return f
            end
        else
            tries = tries + 1
        end
    end
    -- fallback to any with room
    for _,f in ipairs(flags) do
        if (flagCounts[f.name] or 0) < maxPerFlag then return f end
    end
    return flags[#flags]
end

-------------------------------------------------------------------------------
-- (E) EVENT GENERATION
-------------------------------------------------------------------------------
local function makeEvent(dateText, state)
    local sector = pick(sectors)
    local flag   = pickFlagWithCaps(state.flagCounts, state.maxPerFlag, state.lastFlag)
    local vessel = pick(vessels)
    state.flagCounts[flag.name] = (state.flagCounts[flag.name] or 0) + 1
    state.lastFlag = flag.name

    local pEnf = baseProb(flag, sector)
    if math.random() > pEnf then
        return {
            date=dateText, sector=sector,
            vessel=vessel.." ("..flag.name..")",
            action="Routine transit", legal="N/A",
            outcome="No reports", notes="Compliant passage"
        }
    end

    local act   = pickWeighted(actions)
    local cite  = clauseIndex[act.basis] or "SC §—"
    local out   = pick(outcomes)

    -- Guardrail: cap strong outcomes (denied/anchorage review) total per day
    if (out=="Denied entry pending documents" or out=="Escorted to anchorage for formal review")
       and state.strongCount >= state.strongCap then
        out = "Administrative notice issued; monitoring"
    elseif out=="Denied entry pending documents" or out=="Escorted to anchorage for formal review" then
        state.strongCount = state.strongCount + 1
    end

    -- Sub rules & notes
    local notes = "—"
    if vessel=="Undeclared submarine" then
        act  = {name="Submarine declare signal issued", basis="Submarine Surface/Declare"}
        cite = clauseIndex[act.basis]
        notes= "Undeclared contact directed to surface and declare per procedure"
        if out=="No reports" then out="Administrative notice issued; monitoring" end
    elseif vessel=="Declared submarine" then
        notes = "Declared sub transited in daylight per SC §C2–C3"
    elseif sector=="Sacred Buffer Approaches" then
        notes = "Stand-off distance verified for sacred sites"
    elseif sector=="Environmental Buffer" then
        notes = "Sensitive habitat protection enforced during passage"
    end

    -- Count enforcement share for later balancing
    if out~="No reports" then state.enfCount = state.enfCount + 1 end

    return {
        date=dateText, sector=sector,
        vessel=vessel.." ("..flag.name..")",
        action=act.name, legal=(act.basis.." — "..cite),
        outcome=out, notes=notes
    }
end

-------------------------------------------------------------------------------
-- (F) PUBLIC: makeTable
-------------------------------------------------------------------------------
function p.makeTable(frame)
    local dateInfo = getPSSCDate()
    math.randomseed(dateInfo.year*1000 + dateInfo.dayOfYear)

    local ROWS = tonumber(frame and frame.args and frame.args.rows) or 18
    local out = {}
    table.insert(out, '{| class="wikitable sortable"')
    table.insert(out, '! Date !! Sector !! Vessel/Flag !! Action !! Legal Basis !! Outcome !! Notes')

    -- Per-flag cap: ~35% of rows per registry max (rounded up)
    local maxPerFlag = math.max(1, math.ceil(ROWS * 0.35))

    local state = {
        strongCount = 0,           -- denied/anchorage combined
        strongCap   = 3,           -- daily cap
        flagCounts  = {},
        lastFlag    = nil,
        enfCount    = 0,
        maxPerFlag  = maxPerFlag   -- << ADD THIS LINE
    }

    local events = {}
    for i=1, ROWS do
        local ev = makeEvent(dateInfo.text, state)
        table.insert(events, ev)
    end

    -- Balance guardrail: keep total enforcement ≤ 40% of rows
    local maxEnf = math.floor(ROWS * 0.40)
    if state.enfCount > maxEnf then
        local toReduce = state.enfCount - maxEnf
        for _,ev in ipairs(events) do
            if toReduce <= 0 then break end
            if ev.outcome ~= "No reports" and ev.outcome ~= "Compliant; cleared" then
                ev.outcome = "Compliant; cleared"
                ev.notes   = (ev.notes=="—" and "Auto-balanced for daily transparency") or (ev.notes.."; auto-balanced")
                toReduce = toReduce - 1
            end
        end
    end

    for _,ev in ipairs(events) do
        table.insert(out, "|-")
        table.insert(out, "| "..ev.date.." || "..ev.sector.." || "..ev.vessel.." || "..ev.action.." || "..ev.legal.." || "..ev.outcome.." || "..ev.notes)
    end

    table.insert(out, '|}')
    table.insert(out, '<!-- StraitsDaily v3.0 (no explicit IF/Floria; Moorland+Bassaridian included; per-flag cap & smoothing) -->')
    return table.concat(out, "\n")
end

return p