Module:AlpazkigzSimulator: Difference between revisions

From MicrasWiki
Jump to navigationJump to search
NewZimiaGov (talk | contribs)
No edit summary
NewZimiaGov (talk | contribs)
No edit summary
 
(One intermediate revision by the same user not shown)
Line 1: Line 1:
-- Module:AlpazkigzSimulator
-- Module:AlpazkigzSimulator
-- An illustrative, date-aware simulation of the mobile Alperkin city.
-- Calendar-guided civic and geographic account of the mobile Alperkin city.
-- Designed for MediaWiki Scribunto (Lua 5.1); no external packages or writes.
-- Designed for MediaWiki Scribunto (Lua 5.1); no external packages or writes.
-- CANON: festival dates and calendar conversion are sourced from
-- CANON: festival dates and calendar conversion are sourced from
-- Module:BassaridianCalendar; city customs from Alperkin/city articles.
-- Module:BassaridianCalendar; city customs from Alperkin/city articles.
-- MODEL: proposed site localities, exact annual assembly sites, daily indices, phase sub-boundaries
-- Editorial note: specific site descriptions and the default annual route are new
-- and administrative scenarios are invented for gameplay, not historical data.
-- in-universe geographic material developed for this article, not independently
-- attested year-by-year positions in older source pages. Adjust the itinerary when
-- subsequent city history establishes a particular year's actual assembly ground.
-- Internal weighting factors organize seasonal prose; they are not public statistics.
-- Version 2.1: corrected generated MediaWiki table row boundaries and itinerary columns.
-- Version 2: explicit six-site geographic gazetteer / intra-year travel sectors.
-- Version 2: explicit six-site geographic gazetteer / intra-year travel sectors.
-- Keep existing 13 public functions and add geography, gazetteer, waypoints.
-- Keep existing 13 public functions and add geography, gazetteer, waypoints.
Line 14: Line 18:
local YEAR_LENGTH = 183
local YEAR_LENGTH = 183


-- GEOGRAPHIC MODEL / EDITOR CONFIGURATION ---------------------------------
-- GEOGRAPHIC ITINERARY / EDITOR CONFIGURATION ---------------------------------
-- Three established traditional landscapes: the Coastal Woodlands of
-- Three established traditional landscapes: the Coastal Woodlands of
-- southeastern Alperkin (NOT the Coral Woodlands and Plains of Melusine),
-- southeastern Alperkin (NOT the Coral Woodlands and Plains of Melusine),
-- Gloom Forest of Perpetual Autumn, and Alperkin Northern/Western Highlands.
-- Gloom Forest of Perpetual Autumn, and Alperkin Northern/Western Highlands.
-- The site names below denote PROPOSED gathering-ground scenarios within
-- The following designated assembly grounds are part of the city's newly
-- known geographic belts; they are NOT attested campsites or GPS coordinates.
-- developed in-universe geography; they are not GPS addresses.
-- See: Geography of Bassaridia Vaeringheim; Alperkin; Gloom Forest of
-- See: Geography of Bassaridia Vaeringheim; Alperkin; Gloom Forest of
-- Perpetual Autumn; List of cities in Bassaridia Vaeringheim.
-- Perpetual Autumn; List of cities in Bassaridia Vaeringheim.
-- Add a verified year below if a canonical year-to-site assignment is made:
-- Enter a distinct annual assignment here when the city selects another ground:
-- [54] = {site='gloom', note='On-wiki published source / decision'},
-- [54] = {site='gloom', note='On-wiki published source / decision'},
-- String entries [54]='gloom' also work, but are merely editor supplied.
-- String entries [54]='gloom' also work, but are merely editor supplied.
local RECORDED_ITINERARY = {
local RECORDED_ITINERARY = {
   -- No annual campsite has been verified in the consulted articles.
   -- In the absence of an annual exception the six-ground itinerary below applies.
}
}


Line 50: Line 54:
     climate='Frequent ground mist and damp leaf litter; waterlogged hollows, fog and short-range changes in footing.',
     climate='Frequent ground mist and damp leaf litter; waterlogged hollows, fog and short-range changes in footing.',
     access='Authorised forest-edge hostels, raised approaches, signposted shrine roads and supervised forest corridors only.',
     access='Authorised forest-edge hostels, raised approaches, signposted shrine roads and supervised forest corridors only.',
     restriction='No modelled public shortcut through tar-pits, the Amanitian Lowlands, sacred groves or closed funerary grounds.',
     restriction='Public passage avoids the tar-pits, the Amanitian Lowlands, sacred groves and closed funerary grounds.',
     economy='Controlled ritual, festival, forest-craft and guided-corridor services through authorised receiving points.',
     economy='Controlled ritual, festival, forest-craft and guided-corridor services through authorised receiving points.',
     link='[[Gloom Forest of Perpetual Autumn]]',
     link='[[Gloom Forest of Perpetual Autumn]]',
Line 73: Line 77:
     short='Coastal Woodlands — Vaeringheim-facing oak margin', region='Coastal Woodlands of Alperkin',
     short='Coastal Woodlands — Vaeringheim-facing oak margin', region='Coastal Woodlands of Alperkin',
     regionKey='coastal', code='C-1', sector='Southeastern Alperkin / Vaeringheim-facing historic boundary',
     regionKey='coastal', code='C-1', sector='Southeastern Alperkin / Vaeringheim-facing historic boundary',
     ground='Proposed firm oak-margin clearing on an established customary track; no canonically named clearing is attested.',
     ground='Firm oak-margin gathering ground on the southeastern Alperkin customary track.',
     landmark='Vaeringheim-side boundary of the Alperkin Coastal Woodlands (regional reference, not a city-center location)',
     landmark='Vaeringheim-side boundary of the Alperkin Coastal Woodlands (regional reference, not a city-center location)',
     nearest='[[Vaeringheim (Bassaridia Vaeringheim)|Vaeringheim]] is the wider exchange reference, not the asserted nearest measured town.',
     nearest='[[Vaeringheim (Bassaridia Vaeringheim)|Vaeringheim]] is the wider exchange reference, not the asserted nearest measured town.',
Line 87: Line 91:
     short='Coastal Woodlands — upper oak-rise', region='Coastal Woodlands of Alperkin',
     short='Coastal Woodlands — upper oak-rise', region='Coastal Woodlands of Alperkin',
     regionKey='coastal', code='C-2', sector='Southeastern Alperkin / firmer inland rise above low woodland approaches',
     regionKey='coastal', code='C-2', sector='Southeastern Alperkin / firmer inland rise above low woodland approaches',
     ground='Proposed customary terrace above wetter oak-margin lanes, not an established named or mapped campsite.',
     ground='Upper oak-rise gathering terrace above the lower woodland lanes.',
     landmark='Transition between Alperkin woodland and the Vaeringheim-facing lake-plain approach',
     landmark='Transition between Alperkin woodland and the Vaeringheim-facing lake-plain approach',
     nearest='[[Vaeringheim (Bassaridia Vaeringheim)|Vaeringheim]] and its plain-side depots are external commercial references.',
     nearest='[[Vaeringheim (Bassaridia Vaeringheim)|Vaeringheim]] and its plain-side depots are external commercial references.',
     anchor='An illustrative firmer rise within southeastern Alperkin Coastal Woodlands, set back from the dampest lakeside approaches.',
     anchor='The upper oak-rise within the southeastern Alperkin Coastal Woodlands, set back from the dampest lakeside approaches.',
     setting='Oak-studded raised ground with lower wet lanes and open clan receiving patches.',
     setting='Oak-studded raised ground with lower wet lanes and open clan receiving patches.',
     links={'Raised customary oak-rise track','Vaeringheim-facing fixed receiving ground','General Port onward forwarding'},
     links={'Raised customary oak-rise track','Vaeringheim-facing fixed receiving ground','General Port onward forwarding'},
     advantages='A drier hypothetical assembly foundation than a low woodland clearing, subject to ground survey.',
     advantages='The raised terrace provides firmer footing than the low woodland approaches; stewards inspect the ground before assembly.',
     sensitivities='Do not extend roadwork into protected roots, sacred paths or erosion-prone slopes.',
     sensitivities='Do not extend roadwork into protected roots, sacred paths or erosion-prone slopes.',
     port=6, route=5, water=-5, stewardship=1, weather='Wind and runoff on upper ground; water hauling is more demanding than at lower woodland margins.'
     port=6, route=5, water=-5, stewardship=1, weather='Wind and runoff on upper ground; water hauling is more demanding than at lower woodland margins.'
Line 101: Line 105:
     short='Gloom — Erythros Outer Approaches', region='Gloom Forest of Perpetual Autumn',
     short='Gloom — Erythros Outer Approaches', region='Gloom Forest of Perpetual Autumn',
     regionKey='gloom', code='G-1', sector='Serviced outer forest approach / Erythros-facing threshold',
     regionKey='gloom', code='G-1', sector='Serviced outer forest approach / Erythros-facing threshold',
     ground='Proposed authorised clearing beside a serviced outer approach, outside urban Erythros and restricted tar precincts.',
     ground='Authorised gathering ground beside the serviced Outer Approaches, beyond urban Erythros and the restricted tar precincts.',
     landmark='[[Erythros]] forest-edge threshold and its maintained road, hostel and shrine approaches',
     landmark='[[Erythros]] forest-edge threshold and its maintained road, hostel and shrine approaches',
     nearest='[[Erythros]] is a corridor reference; no distance or precise clearing is documented.',
     nearest='[[Erythros]] is a corridor reference; no distance or precise clearing is documented.',
Line 115: Line 119:
     short='Gloom — Acheron-facing northern corridor', region='Gloom Forest of Perpetual Autumn',
     short='Gloom — Acheron-facing northern corridor', region='Gloom Forest of Perpetual Autumn',
     regionKey='gloom', code='G-2', sector='Northern Gloom edge / upland and canyonland transition',
     regionKey='gloom', code='G-2', sector='Northern Gloom edge / upland and canyonland transition',
     ground='Proposed northern forest-edge gathering rise on a maintained route; not within Acheron itself.',
     ground='Northern forest-edge gathering rise along the maintained Acheron-facing corridor, outside Acheron itself.',
     landmark='[[Acheron]] canyonland threshold at the northern Gloom-to-highland interface',
     landmark='[[Acheron]] canyonland threshold at the northern Gloom-to-highland interface',
     nearest='[[Acheron]] is the named canyonland approach, not an attested adjacent village.',
     nearest='[[Acheron]] is the named canyonland approach, not an attested adjacent village.',
Line 121: Line 125:
     setting='Oak forest turning to rocky slopes, shaded ravines and restricted side valleys.',
     setting='Oak forest turning to rocky slopes, shaded ravines and restricted side valleys.',
     links={'Maintained northern Gloom corridor','Approved Acheron-facing canyon approaches','Protected highland clan paths'},
     links={'Maintained northern Gloom corridor','Approved Acheron-facing canyon approaches','Protected highland clan paths'},
     advantages='Modelled meeting ground for northern forest households and upland caravans.',
     advantages='Meeting ground for northern forest households and upland caravans.',
     sensitivities='Flash-flood ravines, slippery rock, canyon choke points and protected forest interiors.',
     sensitivities='Flash-flood ravines, slippery rock, canyon choke points and protected forest interiors.',
     port=-7, route=-7, water=-1, stewardship=-5, weather='Canyon runoff can disrupt lower tracks even when local skies are clear; check slopes and crossings.'
     port=-7, route=-7, water=-1, stewardship=-5, weather='Canyon runoff can disrupt lower tracks even when local skies are clear; check slopes and crossings.'
Line 129: Line 133:
     short='Highlands — Acheron-facing foothills', region='Northern and Western Morovian Highlands',
     short='Highlands — Acheron-facing foothills', region='Northern and Western Morovian Highlands',
     regionKey='highlands', code='H-1', sector='Northern Alperkin forest-to-canyon foothill transition',
     regionKey='highlands', code='H-1', sector='Northern Alperkin forest-to-canyon foothill transition',
     ground='Proposed customary highland terrace, outside Acheron urban ground and unverified as an annual campsite.',
     ground='Customary foothill gathering terrace outside Acheron urban ground.',
     landmark='[[Acheron]] northern canyonlands where Gloom woodland grades into rugged upland ravines',
     landmark='[[Acheron]] northern canyonlands where Gloom woodland grades into rugged upland ravines',
     nearest='[[Acheron]] is the relevant named threshold settlement; the exact campsite is unspecified.',
     nearest='[[Acheron]] is the relevant named threshold settlement; the exact campsite is unspecified.',
Line 143: Line 147:
     short='Highlands — western inland boundary', region='Northern and Western Morovian Highlands',
     short='Highlands — western inland boundary', region='Northern and Western Morovian Highlands',
     regionKey='highlands', code='H-2', sector='Western inland border of Alperkin / hill-and-pass country',
     regionKey='highlands', code='H-2', sector='Western inland border of Alperkin / hill-and-pass country',
     ground='Proposed upper-clan receiving ground on a permitted firm slope, not a surveyed or canonically named settlement.',
     ground='Upper-clan receiving ground on a permitted firm slope of the western Alperkin highlands.',
     landmark='Western Alperkin highland boundary and the customary upland paths toward the inland Keltian hills',
     landmark='Western Alperkin highland boundary and the customary upland paths toward the inland Keltian hills',
     nearest='No nearest fixed city or measured distance has been established for this proposed ground.',
     nearest='The western clan tracks connect with upland receiving depots; no permanent town forms part of this gathering ground.',
     anchor='Western Alperkin upland clan country, inside its inland highland boundary and away from closed passes.',
     anchor='Western Alperkin upland clan country, inside its inland highland boundary and away from closed passes.',
     setting='Open upland pasture, juniper slopes, stone outcrops and exposed caravan staging terrain.',
     setting='Open upland pasture, juniper slopes, stone outcrops and exposed caravan staging terrain.',
     links={'Western upland clan tracks','Permitted mountain receiving depots','Forest-bound forwarding through recognised pass windows'},
     links={'Western upland clan tracks','Permitted mountain receiving depots','Forest-bound forwarding through recognised pass windows'},
     advantages='Provides a modelled western-clan assembly option and access to upland pasture and repair goods.',
     advantages='Provides access for western clans, upland pasture and repair goods.',
     sensitivities='Ground anchoring, scarcity of shelter, weather exposure and seasonal restrictions on steep passes.',
     sensitivities='Ground anchoring, scarcity of shelter, weather exposure and seasonal restrictions on steep passes.',
     port=-10, route=-10, water=-8, stewardship=2, weather='Upland wind, locally limited surface water and sudden cold; hold caravans at depots during closures.'
     port=-10, route=-10, water=-8, stewardship=2, weather='Upland wind, locally limited surface water and sudden cold; hold caravans at depots during closures.'
Line 155: Line 159:
}
}
-- The first three retain the original short site= syntax. Defaults now rotate
-- The first three retain the original short site= syntax. Defaults now rotate
-- between six geographically more explicit, but still hypothetical, grounds.
-- among six geographically distinct gathering grounds.
local SITE_ROTATION = {'coastal','gloom','highlands','coastal_terrace','gloom_north','highlands_west'}
local SITE_ROTATION = {'coastal','gloom','highlands','coastal_terrace','gloom_north','highlands_west'}
local SITE_ORDER = {'coastal','coastal_terrace','gloom','gloom_north','highlands','highlands_west'}
local SITE_ORDER = {'coastal','coastal_terrace','gloom','gloom_north','highlands','highlands_west'}
Line 336: Line 340:
   if key and SITES[key] then
   if key and SITES[key] then
     local note=type(picked)=='table' and (picked.note or '') or ''
     local note=type(picked)=='table' and (picked.note or '') or ''
     return key,'Editor-supplied annual itinerary'..(note~='' and ': '..note or ' (source not specified)')
     return key,'Annual gathering assignment'..(note~='' and ': '..note or '')
   end
   end
   -- Stable, SIX-year scenario cycle. The source articles do not date the
   -- Established city itinerary for this article; annual exceptions override it.
  -- city's actual annual campsite; never present this as historical record.
   return SITE_ROTATION[(year%#SITE_ROTATION)+1],'Regular six-ground itinerary'
   return SITE_ROTATION[(year%#SITE_ROTATION)+1],'Illustrative six-ground rotation (not a recorded location)'
end
end
local function passageSector(fromKey,toKey,step)
local function passageSector(fromKey,toKey,step)
Line 375: Line 378:
   if override~='' then
   if override~='' then
     if not SITES[override] then return nil,'Unknown site='..override..'. Choose coastal, coastal_terrace, gloom, gloom_north, highlands or highlands_west.' end
     if not SITES[override] then return nil,'Unknown site='..override..'. Choose coastal, coastal_terrace, gloom, gloom_north, highlands or highlands_west.' end
     site=override; provenance='Reader-selected scenario (not a recorded location)'
     site=override; provenance='Specified gathering ground'
   end
   end
   local targetOverride=tidy(args.nextsite or ''):lower()
   local targetOverride=tidy(args.nextsite or ''):lower()
   if targetOverride~='' then
   if targetOverride~='' then
     if not SITES[targetOverride] then return nil,'Unknown nextsite='..targetOverride..'. Choose coastal, coastal_terrace, gloom, gloom_north, highlands or highlands_west.' end
     if not SITES[targetOverride] then return nil,'Unknown nextsite='..targetOverride..'. Choose coastal, coastal_terrace, gloom, gloom_north, highlands or highlands_west.' end
     destination=targetOverride; nextProvenance='Reader-selected scenario (not a recorded location)'
     destination=targetOverride; nextProvenance='Specified receiving ground'
   end
   end
   local doy=date.doy
   local doy=date.doy
   local loc={origin=site, destination=destination, originSource=provenance,
   local loc={origin=site, destination=destination, originSource=provenance,
     destinationSource=nextProvenance, site=site, assembled=(doy<=145),
     destinationSource=nextProvenance, site=site, assembled=(doy<=145),
     status='assembled', source='Geographic belts are sourced; exact gathering ground and annual choice remain illustrative.',
     status='assembled', source='Customary gathering ground and protected migration corridor.',
     travelStep=0, travelSector='', travelStart=site, travelEnd=destination}
     travelStep=0, travelSector='', travelStart=site, travelEnd=destination}
   local current=SITES[site]
   local current=SITES[site]
Line 400: Line 403:
   if not loc.site then
   if not loc.site then
     loc.title='In transit: '..current.short..' to '..SITES[destination].short
     loc.title='In transit: '..current.short..' to '..SITES[destination].short
     loc.description='Illustrative migration sector: '..loc.travelSector
     loc.description='Travelling procession: '..loc.travelSector
     loc.region='Between '..REGIONS[SITES[site].regionKey].name..' and '..REGIONS[SITES[destination].regionKey].name
     loc.region='Between '..REGIONS[SITES[site].regionKey].name..' and '..REGIONS[SITES[destination].regionKey].name
     loc.sector=loc.travelSector
     loc.sector=loc.travelSector
     loc.ground='No assembled city and no fixed street address during transit.'
     loc.ground='The city travels with its households and custodians; its customary offices remain in operation.'
     loc.nearest='Moving caravan; consult registered custodians and fixed transfer depots.'
     loc.nearest='Travelling caravan; clan representatives and established receiving depots provide contact.'
   else
   else
     loc.title=current.name..(doy>=159 and ' (winter receiving landscape; Great Yurt not yet assembled)' or '')
     loc.title=current.name..(doy>=159 and ' (winter receiving landscape; Great Yurt not yet assembled)' or '')
Line 507: Line 510:
   if phase=='relocation' then out[#out+1]='The assembled city\'s address is suspended; use custodians and registered transfer grounds.' end
   if phase=='relocation' then out[#out+1]='The assembled city\'s address is suspended; use custodians and registered transfer grounds.' end
   if phase=='winter-camp' then out[#out+1]='The receiving ground is not a fully assembled urban settlement; keep public functions distributed.' end
   if phase=='winter-camp' then out[#out+1]='The receiving ground is not a fully assembled urban settlement; keep public functions distributed.' end
   if loc.origin==loc.destination then out[#out+1]='Origin and destination coincide in this scenario: relocation is between recognised grounds within the same landscape, not necessarily the same clearing.' end
   if loc.origin==loc.destination then out[#out+1]='The next assembly lies within the same regional landscape; the procession moves between recognised grounds rather than remaining in the same clearing.' end
   if #out==0 then out[1]='Routine civic, sacred-land and public-health inspections remain in effect.' end
   if #out==0 then out[1]='Routine civic, sacred-land and public-health inspections remain in effect.' end
   return out
   return out
Line 560: Line 563:
   local arr=scripts[phase]
   local arr=scripts[phase]
   local event=arr[((date.doy+date.year)%#arr)+1]
   local event=arr[((date.doy+date.year)%#arr)+1]
   local detail='Illustrative daily civic activity at '..loc.title..'.'
   local detail='Civic and household activity at '..loc.title..'.'
   if m.water<66 then detail=detail..' Water and sanitation are flagged for attention.' end
   if m.water<66 then detail=detail..' Water and sanitation are flagged for attention.' end
   return event,detail
   return event,detail
Line 587: Line 590:


local function row(k,v) return '|-\n! style="text-align:left;vertical-align:top" | '..k..'\n| '..v..'\n' end
local function row(k,v) return '|-\n! style="text-align:left;vertical-align:top" | '..k..'\n| '..v..'\n' end
local function heading(text) return '\n== '..text..' ==\n' end
local function heading(text) return '\n==== '..text..' ====\n' end
local function renderTable(title,rows)
local function renderTable(title,rows)
   return '{| class="wikitable" style="width:100%;"\n|+ '..title..'\n'..table.concat(rows)..'|}\n'
   return '{| class="wikitable" style="width:100%;"\n|+ '..title..'\n'..table.concat(rows)..'|}\n'
Line 596: Line 599:
     row('PSSC date',d.long),
     row('PSSC date',d.long),
     row('Civic / construction phase',escape(state.phaseDescription)),
     row('Civic / construction phase',escape(state.phaseDescription)),
     row('Simulated present location',escape(l.title)),
     row('Current location',escape(l.title)),
     row('Geographic region',escape(l.region)),
     row('Geographic region',escape(l.region)),
     row('Subregion / travel sector',escape(l.sector)),
     row('Subregion / travel sector',escape(l.sector)),
Line 603: Line 606:
     row('Great Yurt',l.assembled and 'Assembled / in reconstruction' or 'Not fully assembled'),
     row('Great Yurt',l.assembled and 'Assembled / in reconstruction' or 'Not fully assembled'),
     row('Next festival',state.nextFestival.name..' — '..dateFromDoy(state.nextFestival.doy,state.nextFestivalYear).label..' ('..state.daysUntilNext..' days)'),
     row('Next festival',state.nextFestival.name..' — '..dateFromDoy(state.nextFestival.doy,state.nextFestivalYear).label..' ('..state.daysUntilNext..' days)'),
     row('Itinerary evidence',escape(l.originSource)..'; '..escape(l.source))
     row('Seasonal route',escape(SITES[l.origin].short)..' → '..escape(SITES[l.destination].short))
   }
   }
   return renderTable('Alpazkigz geographic and civic status — '..d.label,rows)
   return renderTable('Alpazkigz geographic and civic status — '..d.label,rows)
Line 612: Line 615:
   local d=state.date
   local d=state.date
   local lines={}
   local lines={}
   lines[#lines+1]="The city is a seasonal institution, not a fixed point on a map. In this '''date-dependent geographic scenario''', year "..d.year.." assembles at '''"..SITES[state.location.origin].short.."''', while the next Cricket-Rise is planned for '''"..SITES[state.location.destination].short.."'''."
   lines[#lines+1]="The Alpazkigz follows an annual circuit of its customary gathering grounds. In "..d.year.." PSSC, the city gathers at '''"..SITES[state.location.origin].short.."'''; at the next Cricket-Rise its gathering ground will be '''"..SITES[state.location.destination].short.."'''."
   lines[#lines+1]="'''Geographic reference:''' "..escape(state.location.region)..' / '..escape(state.location.sector)..'. '..escape(state.location.description)
   lines[#lines+1]="'''Geographic reference:''' "..escape(state.location.region)..' / '..escape(state.location.sector)..'. '..escape(state.location.description)
   lines[#lines+1]="'''On this date:''' "..state.phaseDescription
   lines[#lines+1]="'''On this date:''' "..state.phaseDescription
Line 619: Line 622:
end
end
local function renderMetrics(state)
local function renderMetrics(state)
   local m=state.metrics
   local phase=state.phase
   local displays={
   local moving=(phase=='relocation' or phase=='winter-camp')
    {'Attendance index','attendance','Relative concentration of households and visitors, not headcount.'},
  local preparing=(phase=='rebuilding' or phase=='opening')
    {'Great Yurt completion','assembly','Progress of assembly; a modelled progress measure.'},
  local closing=(phase=='cricket-fall' or phase=='closing' or phase=='dismantling')
    {'Market activity','market','Activity of ordinary and festival markets.'},
  return renderTable('The work of the city',{
     {'Port-linked trade','trade','Ability to dispatch and receive goods through fixed depots.'},
     row('Households and visitors',moving and 'Families live along the receiving grounds or travel with their clan processions; their representatives continue civic business.' or
    {'Route accessibility','routes','Readiness of caravans, receiving grounds and emergency roads.'},
      (preparing and 'Returning households receive their places beneath the Great Yurt as the assembly opens.' or
     {'Water and sanitation','water','Readiness of drinking water and waste systems.'},
      (closing and 'Families arrange departure, settle obligations and safeguard their household possessions.' or
    {'Sacred-land stewardship','stewardship','Capacity to protect sensitive gathering lands.'},
      'Households, visiting clans, merchants and pilgrims sustain the city throughout the gathering season.'))),
     {'Fire and structural readiness','fire','Readiness of fire response, egress and shelter inspections.'},
     row('The Great Yurt',state.location.assembled and (preparing and 'The enclosing structure is being raised and inspected.' or (closing and 'The enclosing structure is being prepared for dismantling.' or 'The Great Yurt shelters the assembled city.')) or 'The enclosing structure is packed or awaiting its next raising; its custodians retain its fittings and records.'),
    {'Archives and continuity','archives','Care of the portable yurt-story and civic records.'},
     row('Markets',moving and 'Travelling merchants serve dispersed households; fixed receiving depots maintain outside trade.' or
     {'Public health and care','health','Capacity for routine public health and household support.'},
      (closing and 'Traders close their stalls, settle accounts and dispatch goods ahead of the procession.' or
     {'Ceremonial provision','ritual','Readiness of ritual spaces and customary observances.'},
      'The market quarters receive goods, household orders and visiting caravans.')),
     {'Household supply','supply','Availability of routine stores and distribution services.'},
     row('Water and household services','Custodians maintain drinking-water points, washing grounds and waste collection appropriate to '..escape(SITES[state.location.site or state.location.origin].short)..'.'),
     {'Safe passage','security','Visitor screening and safety on customary routes.'},
     row('Sacred lands','The Saint Mothers and local stewards supervise the permitted approaches, ancestral grounds and protected vegetation.'),
     {'Civic continuity','civic','Ability to hear petitions and maintain authorities when dispersed.'}
     row('Public safety','Gatekeepers and service crews keep passageways open, inspect hearths and fittings, and maintain access for medical and emergency personnel.'),
  }
     row('Public records','The Heavenlet’s officers and designated archivists maintain petitions, land permissions and the portable yurt-stories.'),
  local rows={}
     row('Civic administration',moving and 'Court officers and clan representatives continue their duties while dispersed.' or 'The civic offices receive petitions, organise public works and coordinate the visiting clans.')
  for _,x in ipairs(displays) do
   })
    rows[#rows+1]=row(x[1],bar(m[x[2]])..' — '..x[3])
   end
  return renderTable('Illustrative operational indicators (0–100; not census or measured statistics)',rows)
end
end
local function renderDistricts(state)
local function renderDistricts(state)
Line 664: Line 664:
     row('Clan chieftains','Household representation, allotment and coordination between migrating or assembled clans.'),
     row('Clan chieftains','Household representation, allotment and coordination between migrating or assembled clans.'),
     row('Constitutional setting','Customary institutions and cultural migration routes operate within the national constitutional framework, including protected sacred land and applicable national law.'),
     row('Constitutional setting','Customary institutions and cultural migration routes operate within the national constitutional framework, including protected sacred land and applicable national law.'),
     row('Visitor and Nonkin protections','The simulation assumes no exemption from public safety, lawful access and the protection of residents or visitors; historic abuses are not presented as permissible present-day policy.'),
     row('Residents and visitors','Customary authority operates alongside the legal protections applicable to residents and visitors, including Nonkin communities.'),
     row('Civic service index',bar(state.metrics.civic))
     row('Public business','The Heavenlet’s officers receive petitions, record decisions and coordinate necessary services through the city’s seasonal offices.')
   }
   }
   return renderTable('Government, customary authority and continuity',rows)
   return renderTable('Government, customary authority and continuity',rows)
Line 683: Line 683:
     row('Graji Canïfaha','The sanctuary of Tar, whose ritual access is restricted by Alperkin custom. Physical status: '..districtStatus(state.phase,'sanctuary')..'.'),
     row('Graji Canïfaha','The sanctuary of Tar, whose ritual access is restricted by Alperkin custom. Physical status: '..districtStatus(state.phase,'sanctuary')..'.'),
     row('Yurt-stories','Portable family and civic histories remain part of the city even when its buildings are in transit.'),
     row('Yurt-stories','Portable family and civic histories remain part of the city even when its buildings are in transit.'),
     row('Calendar caveat','The official module assigns fixed festival days. Traditional Alperkin accounts also link observances to insect song, the solstices and lunar events; no astronomical ephemeris is computed here.'),
     row('Observance','The principal PSSC festival dates provide a common civic calendar, while local custom also attends to insect song, the solstices and lunar observances.'),
     row('Ceremonial provision',bar(state.metrics.ritual))
     row('Ceremonial preparations',(state.phase=='relocation' or state.phase=='winter-camp') and 'The sanctuary custodians carry their sacred objects and keep the customary observances with the travelling households.' or 'The Graji Canïfaha and its appointed custodians maintain the observances of the gathering season.')
   })
   })
end
end
Line 693: Line 693:
   local r=REGIONS[site.regionKey]
   local r=REGIONS[site.regionKey]
   local rows={
   local rows={
     row('Modelled gathering ground',escape(site.name)..' ('..site.code..')'),
     row('Gathering ground',escape(site.name)),
     row('Actual position on selected date',escape(loc.title)),
     row('Location on this date',escape(loc.title)),
     row('Geographic region',r.link..' — '..escape(r.parent)),
     row('Geographic region',r.link..' — '..escape(r.parent)),
     row('Subregion / sector',escape(loc.sector)),
     row('Subregion / sector',escape(loc.sector)),
     row('Precise locality status',escape(site.ground)),
     row('Local gathering place',escape(site.ground)),
     row('Named settlement / landscape reference',site.nearest),
     row('Named settlement / landscape reference',site.nearest),
     row('Topography and habitat',escape(r.geology)..' '..escape(site.setting)),
     row('Topography and habitat',escape(r.geology)..' '..escape(site.setting)),
Line 706: Line 706:
     row('Geographic distinction',escape(r.distinction)),
     row('Geographic distinction',escape(r.distinction)),
     row('Current civic consequence',escape(loc.description)),
     row('Current civic consequence',escape(loc.description)),
     row('Geographic provenance','Broad regions and settlement references derive from [[Geography of Bassaridia Vaeringheim]], [[Alperkin]] and [[Gloom Forest of Perpetual Autumn]]. Exact clearing, assembly-year choice and migration sectors are hypothetical model parameters.')
     row('Regional setting','The gathering grounds lie within the Alperkin seasonal circuit; neighbouring permanent settlements remain independent of the Great Yurt.')
   }
   }
   if state.phase=='relocation' then
   if state.phase=='relocation' then
     rows[#rows+1]=row('Migration progress','Day '..loc.travelStep..' of the modelled seven-day Silence journey; sector: '..escape(loc.travelSector))
     rows[#rows+1]=row('Migration progress','Silence procession, day '..loc.travelStep..'; passage: '..escape(loc.travelSector))
   end
   end
   return renderTable('Geographic dossier: '..escape(site.short),rows)
   return renderTable('Geographic dossier: '..escape(site.short),rows)
end
end
local function renderGazetteer(state)
local function renderGazetteer(state)
   local out={'{| class="wikitable sortable" style="width:100%;"','! Site code !! Proposed receiving ground !! Canonical geographic belt !! Reference / limitations'}
   local out={'{| class="wikitable sortable" style="width:100%;"','! Ground !! Place in the annual circuit !! Geographic landscape !! Regional access and considerations'}
   for _,key in ipairs(SITE_ORDER) do
   for _,key in ipairs(SITE_ORDER) do
     local site=SITES[key]
     local site=SITES[key]
     local flag=(state.location.site==key) and "CURRENT SCENARIO — " or (state.location.destination==key and 'Next modelled assembly — ' or '')
     local flag=(state.location.site==key) and "CURRENT GATHERING — " or (state.location.destination==key and 'Next assembly — ' or '')
     out[#out+1]='|-\n| '..site.code..' / <code>'..key..'</code>\n| '..flag..escape(site.name)..'\n| '..REGIONS[site.regionKey].link..'\n| '..site.nearest..'<br />'..escape(site.ground)
     out[#out+1]='|-\n| '..site.code..' / <code>'..key..'</code> — '..flag..escape(site.name)..'\n| '..REGIONS[site.regionKey].link..'\n| '..site.nearest..'<br />'..escape(site.ground)..'\n'
   end
   end
   out[#out+1]='|}'
   out[#out+1]='|}'
   out[#out+1]="''Only the regional landscapes and named settlement references are established lore; the six receiving-ground options are deliberately proposed, not attested named sites.''"
   out[#out+1]="''The Great Yurt occupies the gathering ground indicated for the relevant season; Erythros, Acheron and Vaeringheim remain neighbouring permanent cities and points of onward travel.''"
   return table.concat(out,'\n')..'\n'
   return table.concat(out,'\n')..'\n'
end
end
local function renderWaypoints(state)
local function renderWaypoints(state)
   local a,b=state.location.origin,state.location.destination
   local a,b=state.location.origin,state.location.destination
   local out={'{| class="wikitable" style="width:100%;"','! PSSC date !! Modelled travel phase !! Regional position (NOT an exact road/coordinate)'}
   local out={'{| class="wikitable" style="width:100%;"','! PSSC date !! Stage of the procession !! Region and passage'}
   for doy=146,159 do
   for doy=146,159 do
     local date=dateFromDoy(doy,state.date.year)
     local date=dateFromDoy(doy,state.date.year)
     local loc=currentLocation(date,{site=a,nextsite=b})
     local loc=currentLocation(date,{site=a,nextsite=b})
     local label=(doy<=151 and 'Dismantling at departure ground' or doy<=158 and 'Silence movement; travelling civic custody' or 'Receiving landscape / dispersed winter camp')
     local label=(doy<=151 and 'Dismantling at departure ground' or doy<=158 and 'Silence movement; travelling civic custody' or 'Receiving landscape / dispersed winter camp')
     out[#out+1]='|-\n| '..date.label..'\n| '..label..'\n| '..escape(loc.description)
     out[#out+1]='|-\n| '..date.label..'\n| '..label..'\n| '..escape(loc.description)..'\n'
   end
   end
   out[#out+1]='|}'
   out[#out+1]='|}'
   out[#out+1]="''This is a scheduled SECTOR illustration, not a mapped route; actual stops, distances, permissions, weather holds and arrival may differ.''"
   out[#out+1]="''Travel follows permitted clan roads and receiving grounds; passage through closed sacred lands is not part of the city’s itinerary.''"
   return table.concat(out,'\n')..'\n'
   return table.concat(out,'\n')..'\n'
end
end
Line 753: Line 753:
     row('Principal outbound goods','Portable textiles and craftwork, household products, permitted botanicals and authorised religious objects.'),
     row('Principal outbound goods','Portable textiles and craftwork, household products, permitted botanicals and authorised religious objects.'),
     row('Principal inbound goods','Food staples, hardware, medical supplies, shelter materials and equipment not readily produced by travelling households.'),
     row('Principal inbound goods','Food staples, hardware, medical supplies, shelter materials and equipment not readily produced by travelling households.'),
     row('National trade interface','[[General Port of Lake Morovia]] and the Alpazkigz Regional Investor; this module does not claim to reproduce their live financial ledgers.'),
     row('National trade connections','The [[General Port of Lake Morovia]] and the Alpazkigz Regional Investor serve commercial exchange and civic distribution.'),
     row('Ordinary market',bar(m.market)),row('Port-linked trade',bar(m.trade)),row('Household supply',bar(m.supply)),
     row('Ordinary market',state.phase=='relocation' and 'The enclosed market is closed during the procession; mobile traders continue essential exchange.' or 'The market quarter receives merchants, household orders and visiting caravans.'),
     row('Data status','All displayed indices are scenario outputs, not recorded Poli totals, prices, tonnage, population or trade statistics.')
    row('Port-linked trade','Registered forwarding depots carry goods between the city, permanent lake-plain markets and the General Port.'),
     row('Household supplies','Food, water and necessary stores are distributed through clan stewards and the city’s receiving depots.')
   })
   })
end
end
Line 762: Line 763:
   local routeIntro
   local routeIntro
   if state.phase=='relocation' then
   if state.phase=='relocation' then
     routeIntro='Custodians and households are modelled as moving between '..SITES[loc.origin].short..' and '..SITES[loc.destination].short..'. A precise track is not asserted.'
     routeIntro='Custodians and households travel between '..SITES[loc.origin].short..' and '..SITES[loc.destination].short..'. The procession follows approved clan roads and receiving grounds.'
   elseif state.phase=='winter-camp' then
   elseif state.phase=='winter-camp' then
     routeIntro='Households have reached the receiving landscape at '..SITES[loc.destination].short..', but the Great Yurt remains unassembled.'
     routeIntro='Households have reached the receiving landscape at '..SITES[loc.destination].short..', but the Great Yurt remains unassembled.'
Line 777: Line 778:
   end
   end
   out[#out+1]='|}'
   out[#out+1]='|}'
   out[#out+1]="\n'''Route readiness".."''' — "..bar(state.metrics.routes)..'\n'
   out[#out+1]="\n'''Road and caravan arrangements:''' Stewards inspect crossings, rest grounds and emergency access along the permitted route.\n"
   return table.concat(out,'\n')
   return table.concat(out,'\n')
end
end
Line 790: Line 791:
     row('Access restrictions',REGIONS[site.regionKey].restriction),
     row('Access restrictions',REGIONS[site.regionKey].restriction),
     row('Protection priorities',site.sensitivities),
     row('Protection priorities',site.sensitivities),
     row('Water and sanitation',bar(state.metrics.water)),
     row('Water and sanitation','Water stewards inspect household drinking-water points, washing places and refuse routes for the current gathering ground.'),
     row('Fire readiness',bar(state.metrics.fire)),
     row('Fire precautions','Gatekeepers keep textile shelters, kitchens and emergency passages under regular inspection.'),
     row('Land stewardship',bar(state.metrics.stewardship)),
     row('Land stewardship','Local custodians safeguard protected roots, sacred routes and receiving grounds throughout the city’s residence.'),
     row('Health and household care',bar(state.metrics.health)),
     row('Health and household care','Healers, clan carers and visiting medical personnel serve households and maintain arrangements for urgent transport.')
    row('Operational caveat','These are illustrative readiness indices, not real-time weather, measured health outcomes, or an environmental-impact survey.')
   })
   })
end
end
Line 808: Line 808:
     if year>=0 then
     if year>=0 then
       local key,source=siteForYear(year)
       local key,source=siteForYear(year)
       if year==state.date.year and state.location.originSource:find('Reader',1,true) then
       if year==state.date.year and state.location.originSource:find('Specified',1,true) then
         key=state.location.origin; source=state.location.originSource
         key=state.location.origin; source=state.location.originSource
       elseif year==state.date.year+1 and state.location.destinationSource:find('Reader',1,true) then
       elseif year==state.date.year+1 and state.location.destinationSource:find('Specified',1,true) then
         key=state.location.destination; source=state.location.destinationSource
         key=state.location.destination; source=state.location.destinationSource
       end
       end
       rows[#rows+1]='|-\n| '..year..' PSSC\n| '..SITES[key].short..'\n| '..escape(source)..'\n| Next assembly on 1/1/'..year..' PSSC; route preparations at the preceding Silence.\n'
       rows[#rows+1]='|-\n| '..year..' PSSC\n| '..escape(SITES[key].short)..'\n| '..escape(SITES[key].sector)..'\n| '..escape(source)..'\n| Next assembly on 1/1/'..year..' PSSC; route preparations at the preceding Silence.\n'
     end
     end
   end
   end
   local out={'{| class="wikitable" style="width:100%;"','! Assembly year !! Selected ground !! Subregion !! Evidentiary status !! Annual transition',table.concat(rows),'|}'}
   local out={'{| class="wikitable" style="width:100%;"','! PSSC year !! Gathering ground !! Regional position !! Annual circuit !! Seasonal change',table.concat(rows),'|}'}
   out[#out+1]="\n'''Important".."''': The default repeating itinerary is a playable model, not proof of the city’s position in a historical year. Use the editor-maintained RECORDED_ITINERARY (with published source notes) or site= / nextsite= to select hypothetical grounds."
   out[#out+1]="\n''The city follows its established six-ground circuit unless a different annual gathering ground is entered in the civic itinerary.''"
   return table.concat(out,'\n')..'\n'
   return table.concat(out,'\n')..'\n'
end
end
Line 828: Line 828:
     local festival=festivalToday(date)
     local festival=festivalToday(date)
     local label=festival and festival.name or description
     local label=festival and festival.name or description
     local loc=currentLocation(date,{site=state.location.originSource:find('Reader',1,true) and state.location.origin or nil,
     local loc=currentLocation(date,{site=state.location.originSource:find('Specified',1,true) and state.location.origin or nil,
       nextsite=state.location.destinationSource:find('Reader',1,true) and state.location.destination or nil})
       nextsite=state.location.destinationSource:find('Specified',1,true) and state.location.destination or nil})
     local active=(doy==state.date.doy) and "'''CURRENT SELECTED DAY''' — " or ''
     local active=(doy==state.date.doy) and "'''CURRENT SELECTED DAY''' — " or ''
     rows[#rows+1]='|-\n| '..active..date.label..'\n| '..escape(loc.title)..'\n| '..escape(phase)..'\n| '..escape(label)..'\n'
     rows[#rows+1]='|-\n| '..active..date.label..'\n| '..escape(loc.title)..'\n| '..escape(phase)..'\n| '..escape(label)..'\n'
   end
   end
   return '{| class="wikitable sortable" style="width:100%;"\n! Date !! Modeled location !! Phase !! Holiday or civic activity\n'..table.concat(rows)..'|}\n'
   return '{| class="wikitable sortable" style="width:100%;"\n! Date !! Location !! Civic season !! Festival or civic activity\n'..table.concat(rows)..'|}\n'
end
end
local function renderDispatches(state,n)
local function renderDispatches(state,n)
Line 839: Line 839:
   local rows={}
   local rows={}
   local current=state.date
   local current=state.date
   local siteArgs={site=state.location.originSource:find('Reader',1,true) and state.location.origin or nil,
   local siteArgs={site=state.location.originSource:find('Specified',1,true) and state.location.origin or nil,
     nextsite=state.location.destinationSource:find('Reader',1,true) and state.location.destination or nil,policy=state.policy}
     nextsite=state.location.destinationSource:find('Specified',1,true) and state.location.destination or nil,policy=state.policy}
   for offset=0,n-1 do
   for offset=0,n-1 do
     local doy=current.doy+offset
     local doy=current.doy+offset
Line 856: Line 856:
   return '{| class="wikitable sortable" style="width:100%;"\n! Date !! Location !! Civic dispatch !! Summary\n'..table.concat(rows)..'|}\n'
   return '{| class="wikitable sortable" style="width:100%;"\n! Date !! Location !! Civic dispatch !! Summary\n'..table.concat(rows)..'|}\n'
end
end
-- Every generated table row MUST terminate with a newline before the next |- or |}.
local function renderMap(state)
local function renderMap(state)
   local loc=state.location
   local loc=state.location
Line 861: Line 862:
   for _,key in ipairs(SITE_ORDER) do
   for _,key in ipairs(SITE_ORDER) do
     local s=SITES[key]
     local s=SITES[key]
     local here=(loc.site==key and '● Modelled present landscape' or
     local here=(loc.site==key and '● Current gathering ground' or
       (state.phase=='relocation' and loc.origin==key and '◌ Modelled departure ground' or
       (state.phase=='relocation' and loc.origin==key and '◌ Departure ground' or
       (state.phase=='relocation' and loc.destination==key and '➜ Modelled receiving ground' or
       (state.phase=='relocation' and loc.destination==key and '➜ Receiving ground' or
       (loc.destination==key and '○ Modelled next assembly' or '—'))))
       (loc.destination==key and '○ Next assembly ground' or '—'))))
     rows[#rows+1]='|-\n| '..s.code..': '..escape(s.short)..'\n| '..REGIONS[s.regionKey].link..'\n| '..here..'\n| '..escape(s.sector)..'\n| '..s.nearest
     rows[#rows+1]='|-\n| '..s.code..': '..escape(s.short)..'\n| '..REGIONS[s.regionKey].link..'\n| '..here..'\n| '..escape(s.sector)..'\n| '..s.nearest..'\n'
   end
   end
   return '{| class="wikitable" style="width:100%;"\n! Proposed ground !! Established geographic belt !! Modelled status !! Subregion !! Named regional reference\n'..table.concat(rows)..'|}\n'..
   return '{| class="wikitable" style="width:100%;"\n! Gathering ground !! Geographic landscape !! Place in the annual circuit !! Subregion !! Neighbouring settlement or route\n'..table.concat(rows)..'|}\n'..
     "''Schematic geographic gazetteer; no GPS location, precise historical clearing, street address, or verifiable annual itinerary is implied.''\n"
     "''The named permanent cities provide regional points of reference; the Great Yurt stands on its own customary gathering grounds.''\n"
end
end


Line 892: Line 893:
   local args=argumentTable(frame)
   local args=argumentTable(frame)
   local state,err=buildState(args)
   local state,err=buildState(args)
   if not state then return '<strong class="error">AlpazkigzSimulator: '..escape(err)..'</strong>' end
   if not state then return '<strong class="error">Alpazkigz: '..escape(err)..'</strong>' end
   return render(state,args)
   return render(state,args)
end
end
Line 900: Line 901:
   return withState(frame,function(state,args)
   return withState(frame,function(state,args)
     local out={
     local out={
       "'''ALPAZKIGZ / SEASONAL CIVIC SIMULATION'''",
       "'''THE ALPAZKIGZ — CITY LIFE AND SEASONAL AFFAIRS'''",
       "\n''A repeatable, fictional scenario informed by Alperkin lore. Live location and indices are not canonically recorded observations.''\n",
       "\n''The city's current place in its annual journey, the work of its households, and its customary and civic affairs.''\n",
       renderSmall(state),
       renderSmall(state),
       renderHistory(state),
       renderHistory(state),
       heading('Gathering landscape and planned itinerary'),renderGeography(state),renderMap(state),renderGazetteer(state),renderItinerary(state),renderWaypoints(state),
       heading('Gathering landscape and planned itinerary'),renderGeography(state),renderMap(state),renderGazetteer(state),renderItinerary(state),renderWaypoints(state),
       heading('Civic activity and operating indicators'),renderMetrics(state),
       heading('Households and daily affairs'),renderMetrics(state),
       heading('Customary government and law'),renderGovernance(state),
       heading('Customary government and law'),renderGovernance(state),
       heading('Sacred calendar and ceremonial life'),renderSacred(state),
       heading('Sacred calendar and ceremonial life'),renderSacred(state),
Line 912: Line 913:
       heading('Caravan routes, rail interfaces and travel'),renderRoutes(state),
       heading('Caravan routes, rail interfaces and travel'),renderRoutes(state),
       heading('Ecology, public health and emergency preparedness'),renderEnvironment(state),
       heading('Ecology, public health and emergency preparedness'),renderEnvironment(state),
       heading('Daily advisories'),renderAdvisories(state),
       heading('Notices to households and travellers'),renderAdvisories(state),
       heading('Upcoming PSSC milestones'),renderCalendar(state)
       heading('Festival days and the annual circuit'),renderCalendar(state)
     }
     }
     if tidy(args.chronicle or '')~='' then
     if tidy(args.chronicle or '')~='' then
       out[#out+1]=heading('Forward daily dispatches')
       out[#out+1]=heading('Civic chronicle')
       out[#out+1]=renderDispatches(state,args.chronicle)
       out[#out+1]=renderDispatches(state,args.chronicle)
     end
     end
     out[#out+1]="\n''Calendar source: [[Module:BassaridianCalendar]]; lore: [[Alperkin]], [[List of cities in Bassaridia Vaeringheim#The Alpazkigz|The Alpazkigz]] and [[General Port of Lake Morovia]].''"
     out[#out+1]="\n''See also [[Alperkin]], [[List of cities in Bassaridia Vaeringheim#The Alpazkigz|The Alpazkigz]] and [[General Port of Lake Morovia]].''"
     return table.concat(out,'\n')
     return table.concat(out,'\n')
   end)
   end)

Latest revision as of 21:27, 19 September 2026

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

-- Module:AlpazkigzSimulator
-- Calendar-guided civic and geographic account of the mobile Alperkin city.
-- Designed for MediaWiki Scribunto (Lua 5.1); no external packages or writes.
-- CANON: festival dates and calendar conversion are sourced from
-- Module:BassaridianCalendar; city customs from Alperkin/city articles.
-- Editorial note: specific site descriptions and the default annual route are new
-- in-universe geographic material developed for this article, not independently
-- attested year-by-year positions in older source pages. Adjust the itinerary when
-- subsequent city history establishes a particular year's actual assembly ground.
-- Internal weighting factors organize seasonal prose; they are not public statistics.
-- Version 2.1: corrected generated MediaWiki table row boundaries and itinerary columns.
-- Version 2: explicit six-site geographic gazetteer / intra-year travel sectors.
-- Keep existing 13 public functions and add geography, gazetteer, waypoints.

local p = {}
local MONTHS = { 'Atosiel', 'Thalassiel', 'Opsitheiel' }
local MONTH_NUM = { atosiel=1, thalassiel=2, opsitheiel=3, ['1']=1, ['2']=2, ['3']=3 }
local YEAR_LENGTH = 183

-- GEOGRAPHIC ITINERARY / EDITOR CONFIGURATION ---------------------------------
-- Three established traditional landscapes: the Coastal Woodlands of
-- southeastern Alperkin (NOT the Coral Woodlands and Plains of Melusine),
-- Gloom Forest of Perpetual Autumn, and Alperkin Northern/Western Highlands.
-- The following designated assembly grounds are part of the city's newly
-- developed in-universe geography; they are not GPS addresses.
-- See: Geography of Bassaridia Vaeringheim; Alperkin; Gloom Forest of
-- Perpetual Autumn; List of cities in Bassaridia Vaeringheim.
-- Enter a distinct annual assignment here when the city selects another ground:
-- [54] = {site='gloom', note='On-wiki published source / decision'},
-- String entries [54]='gloom' also work, but are merely editor supplied.
local RECORDED_ITINERARY = {
  -- In the absence of an annual exception the six-ground itinerary below applies.
}

local REGIONS = {
  coastal = {
    name='Coastal Woodlands of Alperkin',
    parent='Alpazkigz Alperkin; southeastern fringe of the Alperkin homeland',
    geology='Sparse oak woodlands and lakeward woodland margins near the historic Vaeringheim-Alperkin boundary.',
    distinction='NOT the Coral Woodlands and Plains of Melusine, which belong to the southern strait and Haifan belt.',
    neighbors='Vaeringheim-facing Alperkin edge; lakeward exchange approaches and the Plains of Vaeringheim beyond the cultural boundary.',
    climate='Woodland and basin-edge moisture; locally saturated ground and lake fog; seasonally exposed woodland lanes.',
    access='Customary clan paths, permitted oak-margin lanes and fixed transfer yards beyond the mobile enclosure.',
    restriction='Protect household grounds, traditional crossings and any designated sacred oak or culturally significant land.',
    economy='Comparatively convenient onward exchange toward the Vaeringheim lake plain and General Port.',
    link='[[Alperkin#Coastal Woodlands|Coastal Woodlands]]',
  },
  gloom = {
    name='Gloom Forest of Perpetual Autumn',
    parent='Alpazkigz Alperkin; western/interior Lake Morovia basin',
    geology='Cool oak-dominant canopy, raised woodland corridors, misted wetland pockets and tar-associated interior lands.',
    distinction='Serviced Outer Approaches and Corridor Belt are not the restricted Tar-Lands, Amanitian Lowlands or funerary precincts.',
    neighbors='[[Erythros]] forest threshold; northern [[Acheron]] canyonlands and highland approaches; lakeward roads eastward.',
    climate='Frequent ground mist and damp leaf litter; waterlogged hollows, fog and short-range changes in footing.',
    access='Authorised forest-edge hostels, raised approaches, signposted shrine roads and supervised forest corridors only.',
    restriction='Public passage avoids the tar-pits, the Amanitian Lowlands, sacred groves and closed funerary grounds.',
    economy='Controlled ritual, festival, forest-craft and guided-corridor services through authorised receiving points.',
    link='[[Gloom Forest of Perpetual Autumn]]',
  },
  highlands = {
    name='Northern and Western Morovian Highlands (Alperkin uplands)',
    parent='Northern and western inland boundary of Alpazkigz Alperkin',
    geology='Forest-to-upland gradients, ridges, ravines, hilly pasture, canyon approaches and guarded passes.',
    distinction='The Alperkin uplands are not the remote Eastern Caledonian Highlands or Bassaridian Normark.',
    neighbors='[[Acheron]] northern canyonland approach and highland-facing forest routes; remote upland clan tracks.',
    climate='Cool exposed nights, wind and highland runoff; pass conditions may vary independently from lakeshore weather.',
    access='Approved caravan/pack-animal approaches, suitable firm-ground terraces, established ridge roads and pass guides.',
    restriction='No automatic access across closed passes, avalanche-prone ground, sacred highland routes or unstable ravines.',
    economy='Upland clans, wool, hides, weavers, pack service, stonework and limited-volume pass-linked shipments.',
    link='[[Northern and Western Morovian Highlands]]',
  }
}

local SITES = {
  coastal = {
    name='Southeastern Alperkin woodland-border gathering ground',
    short='Coastal Woodlands — Vaeringheim-facing oak margin', region='Coastal Woodlands of Alperkin',
    regionKey='coastal', code='C-1', sector='Southeastern Alperkin / Vaeringheim-facing historic boundary',
    ground='Firm oak-margin gathering ground on the southeastern Alperkin customary track.',
    landmark='Vaeringheim-side boundary of the Alperkin Coastal Woodlands (regional reference, not a city-center location)',
    nearest='[[Vaeringheim (Bassaridia Vaeringheim)|Vaeringheim]] is the wider exchange reference, not the asserted nearest measured town.',
    anchor='Southeastern Alperkin Coastal Woodlands, at the woodland-to-lake-plain transition along the historic Vaeringheim-facing edge.',
    setting='Sparse oaks, gently rising ground, woodland tracks and damp low ground near the basin margin.',
    links={'Customary southeast-Alperkin oak lanes','Permitted fixed depots on the Vaeringheim-facing side','Onward registered lake-plain/General Port freight'},
    advantages='Lake-plain markets and established port-facing transfer services are more accessible than from the interior Gloom.',
    sensitivities='Protect old-growth oaks, seasonal runoff routes, household clearings and cultural boundary markers.',
    port=9, route=7, water=-2, stewardship=-3, weather='Lake fog, oak-root disturbance and low-ground saturation; check firmness before driving structural carts.'
  },
  coastal_terrace = {
    name='Upper oak-rise receiving terrace of the Coastal Woodlands',
    short='Coastal Woodlands — upper oak-rise', region='Coastal Woodlands of Alperkin',
    regionKey='coastal', code='C-2', sector='Southeastern Alperkin / firmer inland rise above low woodland approaches',
    ground='Upper oak-rise gathering terrace above the lower woodland lanes.',
    landmark='Transition between Alperkin woodland and the Vaeringheim-facing lake-plain approach',
    nearest='[[Vaeringheim (Bassaridia Vaeringheim)|Vaeringheim]] and its plain-side depots are external commercial references.',
    anchor='The upper oak-rise within the southeastern Alperkin Coastal Woodlands, set back from the dampest lakeside approaches.',
    setting='Oak-studded raised ground with lower wet lanes and open clan receiving patches.',
    links={'Raised customary oak-rise track','Vaeringheim-facing fixed receiving ground','General Port onward forwarding'},
    advantages='The raised terrace provides firmer footing than the low woodland approaches; stewards inspect the ground before assembly.',
    sensitivities='Do not extend roadwork into protected roots, sacred paths or erosion-prone slopes.',
    port=6, route=5, water=-5, stewardship=1, weather='Wind and runoff on upper ground; water hauling is more demanding than at lower woodland margins.'
  },
  gloom = {
    name='Gloom Outer Approaches gathering ground on the Erythros corridor',
    short='Gloom — Erythros Outer Approaches', region='Gloom Forest of Perpetual Autumn',
    regionKey='gloom', code='G-1', sector='Serviced outer forest approach / Erythros-facing threshold',
    ground='Authorised gathering ground beside the serviced Outer Approaches, beyond urban Erythros and the restricted tar precincts.',
    landmark='[[Erythros]] forest-edge threshold and its maintained road, hostel and shrine approaches',
    nearest='[[Erythros]] is a corridor reference; no distance or precise clearing is documented.',
    anchor='The maintained Outer Approaches of the Gloom Forest, referenced to the Erythros forest-edge corridor.',
    setting='Misted oak canopy, raised tracks, firmer woodland rises, and swampy off-road hollows.',
    links={'Erythros serviced forest threshold','Approved forest-edge hostels and shrine roads','Registered lakeward cargo transfer via permanent depots'},
    advantages='Accessible forest-edge worship and craft circuits without placing the city in the forbidden interior.',
    sensitivities='Remain on permitted approaches; no access to the Tar-Lands, Amanitian Lowlands or funerary grounds.',
    port=-4, route=-4, water=2, stewardship=-7, weather='Mist and softened organic soils; raised-road crossings must be inspected every travel day.'
  },
  gloom_north = {
    name='Northern Gloom corridor gathering ground below the Acheron canyon threshold',
    short='Gloom — Acheron-facing northern corridor', region='Gloom Forest of Perpetual Autumn',
    regionKey='gloom', code='G-2', sector='Northern Gloom edge / upland and canyonland transition',
    ground='Northern forest-edge gathering rise along the maintained Acheron-facing corridor, outside Acheron itself.',
    landmark='[[Acheron]] canyonland threshold at the northern Gloom-to-highland interface',
    nearest='[[Acheron]] is the named canyonland approach, not an attested adjacent village.',
    anchor='The northern Gloom Forest corridor, on the woodland-facing side of the Acheron canyonland transition.',
    setting='Oak forest turning to rocky slopes, shaded ravines and restricted side valleys.',
    links={'Maintained northern Gloom corridor','Approved Acheron-facing canyon approaches','Protected highland clan paths'},
    advantages='Meeting ground for northern forest households and upland caravans.',
    sensitivities='Flash-flood ravines, slippery rock, canyon choke points and protected forest interiors.',
    port=-7, route=-7, water=-1, stewardship=-5, weather='Canyon runoff can disrupt lower tracks even when local skies are clear; check slopes and crossings.'
  },
  highlands = {
    name='Acheron-facing northern Alperkin foothill gathering terrace',
    short='Highlands — Acheron-facing foothills', region='Northern and Western Morovian Highlands',
    regionKey='highlands', code='H-1', sector='Northern Alperkin forest-to-canyon foothill transition',
    ground='Customary foothill gathering terrace outside Acheron urban ground.',
    landmark='[[Acheron]] northern canyonlands where Gloom woodland grades into rugged upland ravines',
    nearest='[[Acheron]] is the relevant named threshold settlement; the exact campsite is unspecified.',
    anchor='Firm-ground foothill terrace in northern Alperkin uplands, referenced to the Acheron canyonland transition.',
    setting='Juniper and oak at the woodland fringe, uneven stone, exposed terraces and rocky runoff channels.',
    links={'Approved Acheron-facing foothill tracks','Northern Alperkin clan pack routes','Transfer via stable highland depots and forest-edge roads'},
    advantages='Proximity to northern highland clans and caravan services without imposing the Great Yurt on a narrow pass.',
    sensitivities='Terrace bearing capacity, steep pack routes, flash-flood channels and sacred ridge approaches.',
    port=-7, route=-8, water=-6, stewardship=1, weather='Cool windy nights and abrupt canyon runoff; suspend heavy transport during pass advisories.'
  },
  highlands_west = {
    name='Western Alperkin inland-boundary upland receiving ground',
    short='Highlands — western inland boundary', region='Northern and Western Morovian Highlands',
    regionKey='highlands', code='H-2', sector='Western inland border of Alperkin / hill-and-pass country',
    ground='Upper-clan receiving ground on a permitted firm slope of the western Alperkin highlands.',
    landmark='Western Alperkin highland boundary and the customary upland paths toward the inland Keltian hills',
    nearest='The western clan tracks connect with upland receiving depots; no permanent town forms part of this gathering ground.',
    anchor='Western Alperkin upland clan country, inside its inland highland boundary and away from closed passes.',
    setting='Open upland pasture, juniper slopes, stone outcrops and exposed caravan staging terrain.',
    links={'Western upland clan tracks','Permitted mountain receiving depots','Forest-bound forwarding through recognised pass windows'},
    advantages='Provides access for western clans, upland pasture and repair goods.',
    sensitivities='Ground anchoring, scarcity of shelter, weather exposure and seasonal restrictions on steep passes.',
    port=-10, route=-10, water=-8, stewardship=2, weather='Upland wind, locally limited surface water and sudden cold; hold caravans at depots during closures.'
  }
}
-- The first three retain the original short site= syntax. Defaults now rotate
-- among six geographically distinct gathering grounds.
local SITE_ROTATION = {'coastal','gloom','highlands','coastal_terrace','gloom_north','highlands_west'}
local SITE_ORDER = {'coastal','coastal_terrace','gloom','gloom_north','highlands','highlands_west'}
-- Non-operative geographic references: useful as trade links, NOT valid urban
-- gathering sites. The city is not automatically placed in these settlements.
local CONNECTIONS = {
  ['Vaeringheim']='Plain-side port and registered-market interface; no claim the mobile city is inside Vaeringheim.',
  ['Erythros']='Gloom forest-edge threshold, supervised pilgrim and freight approaches.',
  ['Acheron']='Northern canyon and foothill threshold; weather may close the upland passage.',
  ['Odiferia']='Distinct permanent Alperkin city within the Southern Odiferae Wetlands.',
  ['General Port of Lake Morovia']='Fixed national commodity clearing and freight interface; not a movable quay.',
  ['Coral Woodlands and Plains of Melusine']='Different southern-strait region; NOT the Alpazkigz Coastal Woodlands.',
  ['Eastern Caledonian Highlands']='Remote dependency highlands; NOT the Alperkin Northern/Western Highlands.'
}


local FESTIVALS = {
  {doy=1, name='Cricket-Rise Festival', short='Cricket-Rise', description='The standardized opening of the Winter-to-Spring season and the Great Yurt reconstruction cycle.'},
  {doy=62, name='Cicada Song Festival', short='Cicada Song', description='Principal Alperkin gathering associated with the migration of the wider tribes.'},
  {doy=123, name='Cricket-Fall Festival', short='Cricket-Fall', description='Remembrance rites and the opening of the city\'s closing season.'},
  {doy=152, name='The Silence', short='The Silence', description='A solemn observance associated here with the city\'s relocation and dispersed civic life.'}
}

local POLICIES = {
  balanced={name='Balanced stewardship', note='Customary routine; no additional policy bias.', mod={}},
  sanctuary={name='Sanctuary and ceremonies', note='More ritual access and protected time, less market throughput.', mod={ritual=12, archives=5, trade=-8, market=-7, routes=-3}},
  trade={name='Caravan and market priority', note='Additional controlled trading hours increase commerce and infrastructure pressure.', mod={trade=13, market=14, supply=2, ritual=-4, stewardship=-8, fire=-5}},
  mobility={name='Migration readiness', note='Crews prioritise packing, route checks and vehicle staging over ordinary business.', mod={routes=14, archives=7, civic=3, market=-10, trade=-9, assembly=-3}},
  welfare={name='Households and public care', note='Extra staff are assigned to food, health, accessible routes and water distribution.', mod={health=13, water=10, supply=6, market=-5, trade=-3}},
  stewardship={name='Forest and sacred-land protection', note='Ground inspection and restricted access reduce ecological strain and commerce.', mod={stewardship=16, ritual=5, trade=-9, market=-9, routes=-3}},
  security={name='Perimeter and safe passage', note='Resources shift to emergency access, visitors and routes, limiting trade capacity.', mod={security=15, fire=8, routes=7, market=-9, ritual=-3}}
}

local DISTRICTS = {
  {name='Graji Canïfaha Precinct', kind='sanctuary',
   role='Tar-associated sanctuary, protected ceremonial ground and restricted religious custody.',
   office='Saint Mothers and temple custodians',
   tasks='Guard sanctified thresholds; inspect ritual coverings; coordinate authorised ceremonial access.'},
  {name='Court of the Heavenlet', kind='civic',
   role='Seat of the Heavenlet, reception of clan petitions and diplomatic audiences.',
   office='Heavenlet and clan chieftains',
   tasks='Receive petitions; preserve the civic yurt-story; record rulings and formal audiences.'},
  {name="Mothers' Enclosures", kind='sanctuary',
   role='Protected meeting courts, inter-clan mediation and custody of sacred records.',
   office='Saint Mothers',
   tasks='Mediate disputes; safeguard protected records; settle sanctuary-access questions.'},
  {name='Circle of the Clan Yurts', kind='household',
   role='Residential and kinship clusters arranged around customary clan gathering spaces.',
   office='Clan chieftains and household councils',
   tasks='Allocate household spaces; assist returning families; inspect paths and shared hearths.'},
  {name='The Yurt-Story Walk', kind='archives',
   role='Scribes, textile conservators and the recording and repair of portable family histories.',
   office='Scribes and hereditary custodians',
   tasks='Copy genealogies; protect memorial panels; prepare archives for safe transport.'},
  {name='Cricket Market', kind='trade',
   role='Food, textiles, tools and everyday household exchange; a larger fair at festivals.',
   office='Merchant stewards and clan market representatives',
   tasks='Arrange stalls; check food storage; record merchant deposits and return spaces.'},
  {name='Noctic Exchange', kind='trade',
   role='Culturally important, controlled trade in Noctic-Rabrev and ritual botanical goods.',
   office='Customary market stewards and relevant lawful inspectors',
   tasks='Keep restricted goods separate from food stalls; check provenance and safe handling.'},
  {name='Caravan Gateyards', kind='route',
   role='Receiving courts for people, freight, pack animals and outbound port-linked goods.',
   office='Gate stewards and transport coordinators',
   tasks='Reserve emergency routes; manage arrivals and departures; match shipments to depots.'},
  {name="The Weavers' Rows", kind='craft',
   role='Construction, repair and replacement of the Great Yurt\'s textile and structural components.',
   office='Weavers, rope-makers and construction crews',
   tasks='Check panels, lashings and frames; organise reusable materials and repair teams.'},
  {name='Outer Hearths and Service Grounds', kind='services',
   role='Water, kitchens, sanitation, household services and emergency staging inside or beyond the gates.',
   office='Service stewards and local response crews',
   tasks='Protect drinking water; clean collection points; inspect emergency access and storage.'}
}

local ROUTES = {
  {name='Lakeward exchange', destination='General Port of Lake Morovia',
   direction='Coastal woodland transfer yards → fixed depots → lake and port network',
   goods='Textiles, portable crafts, permitted botanicals and religious objects outbound; food, hardware and household supplies inbound.',
   note='The mobile city is not itself a deep-water port; freight uses permanent receiving and transfer points.'},
  {name='Forest-edge circuit', destination='Erythros and Acheron',
   direction='Recognised Gloom Forest perimeter corridors and established outside settlements',
   goods='Cloth, forest crafts, archives, household supplies and authorised ceremonial consignments.',
   note='Not all forest interior paths are public; sensitive and sacred areas remain restricted.'},
  {name='Northern highland circuit', destination='Northern and Western Highlands',
   direction='Approved upland clan paths and mountain transfer stations',
   goods='Wool, skins, repair stock, pack-animal services and family goods.',
   note='Steep routes limit bulky freight and may require rescheduling.'},
  {name='Southern Alperkin connection', destination='Odiferia and the Southern Odiferae Wetlands',
   direction='Registered southward caravans, lakeward markets and authorised inter-clan passages',
   goods='Textiles, prepared goods and permitted ceremonial supplies.',
   note='Odiferia remains a distinct settled Alperkin city with its own traditions and institutions.'},
  {name='National railway interface', destination='Trans-Morovian Express and regional fixed depots',
   direction='Road or caravan transfer between the current gathering ground and established rail facilities',
   goods='Passengers, construction materials, durable stores and forwarding consignments.',
   note='This does not imply a railway terminus moving with the Great Yurt.'}
}

local function clamp(x, min, max)
  if x < min then return min end
  if x > max then return max end
  return x
end
local function round(x) return math.floor(x + 0.5) end
local function tidy(x) return tostring(x or ''):match('^%s*(.-)%s*$') end
local function escape(x)
  local str=tostring(x or '')
  return (str:gsub('&','&amp;'):gsub('<','&lt;'):gsub('>','&gt;')
    :gsub('%[','&#91;'):gsub('%]','&#93;'):gsub('|','&#124;')
    :gsub('{','&#123;'):gsub('}','&#125;'))
end
local function bar(n)
  n=clamp(round(n),0,100)
  local blocks=math.floor(n/10+0.5)
  return string.rep('■',blocks)..string.rep('·',10-blocks)..' '..n..'/100'
end
local function dateFromDoy(doy, year)
  local m=math.floor((doy-1)/61)+1
  return {year=year, doy=doy, month=m, day=doy-(m-1)*61,
    monthName=MONTHS[m], label=(doy-(m-1)*61)..'/'..m..'/'..year..' PSSC',
    long=(doy-(m-1)*61)..' '..MONTHS[m]..', '..year..' PSSC'}
end
local function validInteger(x,min,max)
  local n=tonumber(x)
  if not n or n~=math.floor(n) or n<min or n>max then return nil end
  return n
end
local function resolveCurrent()
  -- Calendar module's first number is DAY OF YEAR, not day in the month.
  -- Example: '152, Opsitheiel (Stygian), 54 PSSC'.
  local ok, mod=pcall(require,'Module:BassaridianCalendar')
  if not ok or type(mod)~='table' or type(mod.getCurrentDateNoProverb)~='function' then
    return nil, 'Module:BassaridianCalendar is unavailable.'
  end
  local ran, output=pcall(mod.getCurrentDateNoProverb)
  if not ran or type(output)~='string' then
    return nil, 'The calendar module did not return a readable date.'
  end
  local doy, year=output:match('^%s*(%d+)%s*,.-,%s*(%-?%d+)%s+PSSC')
  doy=validInteger(doy,1,183)
  year=validInteger(year,0,9999)
  if not doy or not year then
    return nil, 'Could not parse the current PSSC date: '..output
  end
  return dateFromDoy(doy,year)
end
local function inputDate(args)
  local base,err=resolveCurrent()
  if not base then
    -- Explicit dates still work even when the live calendar module is offline.
    if not (args.date or args.year or args.doy or args.month or args.day) then return nil,err end
  end
  local year=validInteger(args.year or (base and base.year) or nil,0,9999)
  local doy
  if args.date and tidy(args.date)~='' then
    local d,m,y=tidy(args.date):match('^(%d+)%s*/%s*(%d+)%s*/%s*(%d+)%s*$')
    if not d then return nil,'Use date=day/month/year, for example date=30/3/54.' end
    d,m,y=validInteger(d,1,61),validInteger(m,1,3),validInteger(y,0,9999)
    if not d or not m or not y then return nil,'Invalid PSSC date: day must be 1–61, month 1–3.' end
    year=y
    doy=(m-1)*61+d
  elseif args.doy and tidy(args.doy)~='' then
    doy=validInteger(args.doy,1,YEAR_LENGTH)
    if not doy then return nil,'doy must be a whole number from 1 to 183.' end
  elseif (args.day and tidy(args.day)~='') or (args.month and tidy(args.month)~='') then
    local m=MONTH_NUM[tostring(args.month or (base and base.month) or ''):lower()]
    local d=validInteger(args.day or (base and base.day),1,61)
    if not m or not d then return nil,'Use month=1, 2, 3 (or month name) and day=1–61.' end
    doy=(m-1)*61+d
  else
    doy=base and base.doy
  end
  if not year or not doy then return nil,'A year and valid PSSC date are required.' end
  return dateFromDoy(doy,year)
end
local function siteForYear(year)
  local picked=RECORDED_ITINERARY[year]
  local key=type(picked)=='table' and picked.site or picked
  if key and SITES[key] then
    local note=type(picked)=='table' and (picked.note or '') or ''
    return key,'Annual gathering assignment'..(note~='' and ': '..note or '')
  end
  -- Established city itinerary for this article; annual exceptions override it.
  return SITE_ROTATION[(year%#SITE_ROTATION)+1],'Regular six-ground itinerary'
end
local function passageSector(fromKey,toKey,step)
  local a,b=SITES[fromKey],SITES[toKey]
  local ra,rb=a.regionKey,b.regionKey
  if ra==rb then
    local sectors={
      a.ground,
      REGIONS[ra].access,
      'Managed intra-regional reception and customary waystations',
      'Approach to '..b.ground
    }
    return sectors[math.min(step,#sectors)]
  end
  -- A staged REGIONAL itinerary, not a claim that a particular road connects
  -- two points. No path is routed through sacred/restricted forest interiors.
  local sectors={
    [1]='Departure from '..a.sector..'; portable sacred objects and records remain under travelling custody.',
    [2]='Approved outbound tracks in '..REGIONS[ra].name..'; protected approach checks and convoy staging.',
    [3]=(ra=='coastal' and 'Lake-plain to forest-edge transfer interfaces, via fixed permitted receiving points.'
         or ra=='gloom' and 'Maintained Gloom Outer Approaches; avoid Tar-Lands and Amanitian Lowlands.'
         or 'Authorised foothill approach, guarded passes and stable road transfer.'),
    [4]=(rb=='coastal' and 'Approaching southeastern Alperkin oak-margin receiving lanes from registered fixed depots.'
         or rb=='gloom' and 'Approaching serviced Gloom Corridor Belt and Outer Approaches, outside restricted interiors.'
         or 'Approaching approved northern/western Alperkin foothill routes, subject to pass status.'),
    [5]='Customary reception and ground inspection at '..b.sector,
  }
  return sectors[math.min(step,#sectors)]
end
local function currentLocation(date, args)
  local site, provenance=siteForYear(date.year)
  local destination, nextProvenance=siteForYear(date.year+1)
  local override=tidy(args.site or ''):lower()
  if override~='' then
    if not SITES[override] then return nil,'Unknown site='..override..'. Choose coastal, coastal_terrace, gloom, gloom_north, highlands or highlands_west.' end
    site=override; provenance='Specified gathering ground'
  end
  local targetOverride=tidy(args.nextsite or ''):lower()
  if targetOverride~='' then
    if not SITES[targetOverride] then return nil,'Unknown nextsite='..targetOverride..'. Choose coastal, coastal_terrace, gloom, gloom_north, highlands or highlands_west.' end
    destination=targetOverride; nextProvenance='Specified receiving ground'
  end
  local doy=date.doy
  local loc={origin=site, destination=destination, originSource=provenance,
    destinationSource=nextProvenance, site=site, assembled=(doy<=145),
    status='assembled', source='Customary gathering ground and protected migration corridor.',
    travelStep=0, travelSector='', travelStart=site, travelEnd=destination}
  local current=SITES[site]
  if doy>=146 and doy<=151 then
    loc.assembled=false; loc.status='partly dismantled'
  elseif doy>=152 and doy<=158 then
    loc.assembled=false; loc.status='on the migration route'; loc.site=nil
    loc.travelStep=doy-151
    loc.travelSector=passageSector(site,destination,math.max(1,math.min(5,loc.travelStep-1)))
  elseif doy>=159 then
    loc.assembled=false; loc.status='dispersed winter gathering'; loc.site=destination
    current=SITES[destination]
  end
  if not loc.site then
    loc.title='In transit: '..current.short..' to '..SITES[destination].short
    loc.description='Travelling procession: '..loc.travelSector
    loc.region='Between '..REGIONS[SITES[site].regionKey].name..' and '..REGIONS[SITES[destination].regionKey].name
    loc.sector=loc.travelSector
    loc.ground='The city travels with its households and custodians; its customary offices remain in operation.'
    loc.nearest='Travelling caravan; clan representatives and established receiving depots provide contact.'
  else
    loc.title=current.name..(doy>=159 and ' (winter receiving landscape; Great Yurt not yet assembled)' or '')
    loc.description=current.anchor
    loc.region=REGIONS[current.regionKey].name
    loc.sector=current.sector
    loc.ground=current.ground
    loc.nearest=current.nearest
  end
  return loc
end


local function phaseFor(date)
  local d=date.doy
  if d<=6 then return 'rebuilding','The Great Yurt is being raised. Advance parties, families and the principal offices return.' end
  if d<=12 then return 'opening','Households settle, public services are inspected and the markets begin opening.' end
  if d<=61 then return 'inhabited','The mobile city is fully assembled and everyday civic life continues.' end
  if d<=71 then return 'cicada-gathering','Cicada Song draws wider tribal attendance, ceremonies, trading and inter-clan meetings.' end
  if d<=122 then return 'inhabited','The city is assembled; its households, courts, shrines and markets operate.' end
  if d<=136 then return 'cricket-fall','The city holds autumnal remembrance rites and begins its closing preparations.' end
  if d<=145 then return 'closing','Commercial accounts and petitions are being settled before the Great Yurt is packed.' end
  if d<=151 then return 'dismantling','The canopy, market stalls and residential structures are packed in stages.' end
  if d<=158 then return 'relocation','The Silence accompanies movement of households, sacred custody and civic archives.' end
  return 'winter-camp','Dispersed households and custodians occupy the receiving landscape; the full city awaits Cricket-Rise.'
end
local function festivalToday(date)
  for _,event in ipairs(FESTIVALS) do
    if event.doy==date.doy then return event end
  end
  return nil
end
local function nextFestival(date)
  for _,event in ipairs(FESTIVALS) do
    if event.doy>date.doy then return event,date.year,event.doy-date.doy end
  end
  return FESTIVALS[1],date.year+1,YEAR_LENGTH-date.doy+1
end
local function noise(y,d,s)
  local q=math.sin((y+31)*12.9898+(d+1)*78.233+(s+3)*37.719)*43758.5453
  return (q-math.floor(q))*2-1
end
local function metrics(date, phase, loc, policy)
  local load={rebuilding=37, opening=64, inhabited=76, ['cicada-gathering']=96,
    ['cricket-fall']=87, closing=68, dismantling=42, relocation=23, ['winter-camp']=29}
  local market={rebuilding=19, opening=54, inhabited=78, ['cicada-gathering']=95,
    ['cricket-fall']=76, closing=45, dismantling=12, relocation=7, ['winter-camp']=24}
  local assembled={rebuilding=round((date.doy/7)*100),opening=100,inhabited=100,
    ['cicada-gathering']=100,['cricket-fall']=100,closing=95,
    dismantling=round((152-date.doy)/7*100),relocation=0,['winter-camp']=0}
  local ctx=SITES[loc.site or loc.origin]
  local shifts=POLICIES[policy].mod
  local mods={attendance=0, assembly=0, market=0, trade=0, routes=ctx.route,
    water=ctx.water, stewardship=ctx.stewardship, fire=0, archives=0,
    health=0, ritual=0, supply=0, security=0, civic=0}
  for k,v in pairs(shifts) do mods[k]=(mods[k] or 0)+v end
  local m={}
  local base={
    attendance=load[phase], assembly=assembled[phase], market=market[phase],
    trade=market[phase]+ctx.port-5, routes=74, water=73,
    stewardship=82, fire=76, archives=82, health=75, ritual=74,
    supply=76, security=75, civic=77
  }
  if phase=='rebuilding' or phase=='dismantling' then
    base.routes=67; base.fire=62; base.supply=70; base.civic=63; base.archives=70
  elseif phase=='relocation' then
    base.routes=58; base.water=66; base.fire=67; base.archives=69; base.civic=66
  elseif phase=='winter-camp' then
    base.routes=64; base.water=69; base.fire=72; base.archives=84
  elseif phase=='cicada-gathering' then
    base.water=62; base.fire=66; base.stewardship=70; base.supply=68; base.health=70
  end
  -- Region-specific operating constraints are scenario pressures, not live weather.
  local r=ctx.regionKey
  if r=='gloom' then
    base.routes=base.routes-5; base.stewardship=base.stewardship-5
    if date.doy>=123 then base.water=base.water-3 end
  elseif r=='highlands' then
    base.routes=base.routes-5; base.supply=base.supply-5
    if date.doy>=123 then base.routes=base.routes-5; base.water=base.water-4 end
  elseif r=='coastal' then
    base.trade=base.trade+3
    if date.doy>=62 and date.doy<=122 then base.water=base.water-4 end
  end
  if phase=='relocation' then base.routes=base.routes-7; base.supply=base.supply-4 end
  local order={'attendance','assembly','market','trade','routes','water','stewardship','fire','archives','health','ritual','supply','security','civic'}
  for i,key in ipairs(order) do
    local jitter=round(5*noise(date.year,date.doy,i))
    if key=='assembly' then jitter=0 end
    m[key]=clamp(round(base[key]+jitter+(mods[key] or 0)),0,100)
  end
  -- This is a modelled attendance *index*, not an invented census.
  return m
end
local function concerns(m,phase,loc)
  local out={}
  if m.water<70 then out[#out+1]='Water distribution requires inspection before additional households or visitors are admitted.' end
  if m.fire<70 then out[#out+1]='Keep emergency corridors unobstructed and check textile, hearth and ventilation arrangements.' end
  if m.stewardship<73 then out[#out+1]='Inspect ground disturbance and restrict traffic near protected woodland and sacred approaches.' end
  if m.routes<65 then out[#out+1]='Review caravan timing, animal rest points and access for emergency personnel.' end
  if m.archives<75 then out[#out+1]='Transfer priority to the yurt-story panels and portable civic records.' end
  if phase=='relocation' then out[#out+1]='The assembled city\'s address is suspended; use custodians and registered transfer grounds.' end
  if phase=='winter-camp' then out[#out+1]='The receiving ground is not a fully assembled urban settlement; keep public functions distributed.' end
  if loc.origin==loc.destination then out[#out+1]='The next assembly lies within the same regional landscape; the procession moves between recognised grounds rather than remaining in the same clearing.' end
  if #out==0 then out[1]='Routine civic, sacred-land and public-health inspections remain in effect.' end
  return out
end
local function districtStatus(phase, kind)
  if phase=='relocation' then
    if kind=='archives' or kind=='civic' or kind=='sanctuary' then return 'Travelling custody' end
    if kind=='services' or kind=='route' then return 'Mobile support' end
    return 'Packed / dispersed'
  end
  if phase=='winter-camp' then
    if kind=='archives' or kind=='civic' or kind=='sanctuary' then return 'Distributed winter office' end
    if kind=='services' or kind=='route' then return 'Receiving-ground service' end
    return 'No permanent city precinct'
  end
  if phase=='dismantling' then
    if kind=='archives' or kind=='sanctuary' or kind=='civic' then return 'Packing under custody' end
    if kind=='trade' then return 'Settling and closing' end
    return 'Being dismantled'
  end
  if phase=='rebuilding' then
    if kind=='services' or kind=='route' or kind=='craft' then return 'Priority construction / receiving' end
    if kind=='sanctuary' then return 'Ceremonial re-establishment' end
    return 'Opening progressively'
  end
  if phase=='closing' then
    if kind=='trade' then return 'Final market / accounts' end
    if kind=='archives' or kind=='craft' then return 'Packing preparations' end
    return 'Open; closing preparations'
  end
  if kind=='sanctuary' then return 'Active; authorised access only' end
  if phase=='cicada-gathering' then return 'Active; festival demand' end
  if phase=='cricket-fall' then return 'Active; autumn rites' end
  return 'Active'
end
local function dailyEvent(date, phase, loc, m)
  local festival=festivalToday(date)
  if festival then return festival.name,festival.description end
  local scripts={
    rebuilding={
      'Receiving-ground survey','Textile and lashings inspection','Returning households',
      'Sacred archive transfer','Water and hearth certification','Gate and market opening'},
    opening={'Household allotment review','Opening of the Cricket Market','Clan registration','Public sanitation checks'},
    inhabited={'Clan petition day','Yurt-story preservation','Caravan receiving','Perimeter and water inspection','Weavers\' repair day'},
    ['cicada-gathering']={'Inter-clan audience','Cicada fair arrangements','Festival shelter inspection','Visitor access review'},
    ['cricket-fall']={'Remembrance gathering','Autumn stores audit','Cemetery and tar-land access review'},
    closing={'Final merchant settlement','Clan departure scheduling','Packing of non-essential goods','Winter route review'},
    dismantling={'Canopy panel inventory','Archive packing and sealing','Emergency route clearance','Caravan load inspection'},
    relocation={'Protected caravan passage','Waystation inspection','Transfer of the civic yurt-story','Silence observances'},
    ['winter-camp']={'Household winter correspondence','Receiving-ground stewardship','Repair of portable structures','Planning the next Cricket-Rise'}
  }
  local arr=scripts[phase]
  local event=arr[((date.doy+date.year)%#arr)+1]
  local detail='Civic and household activity at '..loc.title..'.'
  if m.water<66 then detail=detail..' Water and sanitation are flagged for attention.' end
  return event,detail
end
local function buildState(args)
  args=args or {}
  local date,err=inputDate(args)
  if not date then return nil,err end
  local policy=tidy(args.policy or 'balanced'):lower()
  if policy=='' then policy='balanced' end
  if not POLICIES[policy] then return nil,'Unknown policy. Choose balanced, sanctuary, trade, mobility, welfare, stewardship or security.' end
  local loc,lerr=currentLocation(date,args)
  if not loc then return nil,lerr end
  local phase,description=phaseFor(date)
  local m=metrics(date,phase,loc,policy)
  local event,nextEventYear,daysLeft=nextFestival(date)
  local dailyTitle,dailyDescription=dailyEvent(date,phase,loc,m)
  return {
    date=date, phase=phase, phaseDescription=description,
    location=loc, metrics=m, policy=policy,
    today=festivalToday(date), dailyTitle=dailyTitle, dailyDescription=dailyDescription,
    nextFestival=event, nextFestivalYear=nextEventYear, daysUntilNext=daysLeft,
    concerns=concerns(m,phase,loc)
  }
end

local function row(k,v) return '|-\n! style="text-align:left;vertical-align:top" | '..k..'\n| '..v..'\n' end
local function heading(text) return '\n==== '..text..' ====\n' end
local function renderTable(title,rows)
  return '{| class="wikitable" style="width:100%;"\n|+ '..title..'\n'..table.concat(rows)..'|}\n'
end
local function renderSmall(state)
  local d,l=state.date,state.location
  local rows={
    row('PSSC date',d.long),
    row('Civic / construction phase',escape(state.phaseDescription)),
    row('Current location',escape(l.title)),
    row('Geographic region',escape(l.region)),
    row('Subregion / travel sector',escape(l.sector)),
    row('Gathering ground or transit condition',escape(l.ground)),
    row('Nearby settlement / fixed interface',l.nearest),
    row('Great Yurt',l.assembled and 'Assembled / in reconstruction' or 'Not fully assembled'),
    row('Next festival',state.nextFestival.name..' — '..dateFromDoy(state.nextFestival.doy,state.nextFestivalYear).label..' ('..state.daysUntilNext..' days)'),
    row('Seasonal route',escape(SITES[l.origin].short)..' → '..escape(SITES[l.destination].short))
  }
  return renderTable('Alpazkigz geographic and civic status — '..d.label,rows)
end


local function renderHistory(state)
  local d=state.date
  local lines={}
  lines[#lines+1]="The Alpazkigz follows an annual circuit of its customary gathering grounds. In "..d.year.." PSSC, the city gathers at '''"..SITES[state.location.origin].short.."'''; at the next Cricket-Rise its gathering ground will be '''"..SITES[state.location.destination].short.."'''."
  lines[#lines+1]="'''Geographic reference:''' "..escape(state.location.region)..' / '..escape(state.location.sector)..'. '..escape(state.location.description)
  lines[#lines+1]="'''On this date:''' "..state.phaseDescription
  lines[#lines+1]="'''Daily dispatch:''' "..escape(state.dailyTitle)..' — '..escape(state.dailyDescription)
  return table.concat(lines,'\n\n')..'\n'
end
local function renderMetrics(state)
  local phase=state.phase
  local moving=(phase=='relocation' or phase=='winter-camp')
  local preparing=(phase=='rebuilding' or phase=='opening')
  local closing=(phase=='cricket-fall' or phase=='closing' or phase=='dismantling')
  return renderTable('The work of the city',{
    row('Households and visitors',moving and 'Families live along the receiving grounds or travel with their clan processions; their representatives continue civic business.' or
      (preparing and 'Returning households receive their places beneath the Great Yurt as the assembly opens.' or
      (closing and 'Families arrange departure, settle obligations and safeguard their household possessions.' or
      'Households, visiting clans, merchants and pilgrims sustain the city throughout the gathering season.'))),
    row('The Great Yurt',state.location.assembled and (preparing and 'The enclosing structure is being raised and inspected.' or (closing and 'The enclosing structure is being prepared for dismantling.' or 'The Great Yurt shelters the assembled city.')) or 'The enclosing structure is packed or awaiting its next raising; its custodians retain its fittings and records.'),
    row('Markets',moving and 'Travelling merchants serve dispersed households; fixed receiving depots maintain outside trade.' or
      (closing and 'Traders close their stalls, settle accounts and dispatch goods ahead of the procession.' or
      'The market quarters receive goods, household orders and visiting caravans.')),
    row('Water and household services','Custodians maintain drinking-water points, washing grounds and waste collection appropriate to '..escape(SITES[state.location.site or state.location.origin].short)..'.'),
    row('Sacred lands','The Saint Mothers and local stewards supervise the permitted approaches, ancestral grounds and protected vegetation.'),
    row('Public safety','Gatekeepers and service crews keep passageways open, inspect hearths and fittings, and maintain access for medical and emergency personnel.'),
    row('Public records','The Heavenlet’s officers and designated archivists maintain petitions, land permissions and the portable yurt-stories.'),
    row('Civic administration',moving and 'Court officers and clan representatives continue their duties while dispersed.' or 'The civic offices receive petitions, organise public works and coordinate the visiting clans.')
  })
end
local function renderDistricts(state)
  local rows={}
  for _,d in ipairs(DISTRICTS) do
    rows[#rows+1]="|-\n| '''"..d.name.."'''\n| "..districtStatus(state.phase,d.kind)..'\n| '..d.role..'\n| '..d.office..'\n| '..d.tasks..'\n'
  end
  return '{| class="wikitable sortable" style="width:100%;"\n! Precinct !! Status on this date !! Role !! Stewardship !! Today\n'..table.concat(rows)..'|}\n'
end
local function renderGovernance(state)
  local phase=state.phase
  local mode='The Heavenlet receives petitions at the Court; clan chieftains coordinate households; Saint Mothers preside over customary and religious matters.'
  if phase=='relocation' or phase=='winter-camp' then
    mode='The Heavenlet, Saint Mothers and clan chieftains continue their functions through travelling or dispersed custodians; the absence of an assembled court does not dissolve their offices.'
  elseif phase=='dismantling' or phase=='closing' then
    mode='Priority is placed on finishing petitions, recording decisions, protecting sacred archives and arranging urgent hearings during travel.'
  elseif phase=='cicada-gathering' then
    mode='Expanded inter-clan audiences and consultations accompany the Cicada Song gathering; visitors do not acquire access to restricted sanctuaries.'
  end
  local rows={
    row('Heavenlet',mode),
    row('Saint Mothers','Customary mediation, religious stewardship and protected ceremonial thresholds. No computed score replaces their decisions.'),
    row('Clan chieftains','Household representation, allotment and coordination between migrating or assembled clans.'),
    row('Constitutional setting','Customary institutions and cultural migration routes operate within the national constitutional framework, including protected sacred land and applicable national law.'),
    row('Residents and visitors','Customary authority operates alongside the legal protections applicable to residents and visitors, including Nonkin communities.'),
    row('Public business','The Heavenlet’s officers receive petitions, record decisions and coordinate necessary services through the city’s seasonal offices.')
  }
  return renderTable('Government, customary authority and continuity',rows)
end
local function renderSacred(state)
  local d=state.date
  local season,plant
  if d.doy<=61 then season='Winter-to-Spring'; plant='Green growth is associated with the opening season.'
  elseif d.doy<=122 then season='Spring-to-Summer'; plant='The season is associated with the cicada song and growing Alperkin movement.'
  elseif d.doy<=151 then season='Summer-to-Fall'; plant='Autumn remembrance and the closing of the assembled city dominate public observance.'
  else season='Fall-to-Winter'; plant='The Silence marks contemplation and the dispersed condition of the city.' end
  local today=state.today and (state.today.name..' — '..state.today.description) or 'No principal standardized Alperkin festival falls on this date.'
  return renderTable('Sacred calendar and religious life',{
    row('Liturgical season',season),
    row('Today',today),
    row('Seasonal environment',plant),
    row('Graji Canïfaha','The sanctuary of Tar, whose ritual access is restricted by Alperkin custom. Physical status: '..districtStatus(state.phase,'sanctuary')..'.'),
    row('Yurt-stories','Portable family and civic histories remain part of the city even when its buildings are in transit.'),
    row('Observance','The principal PSSC festival dates provide a common civic calendar, while local custom also attends to insect song, the solstices and lunar observances.'),
    row('Ceremonial preparations',(state.phase=='relocation' or state.phase=='winter-camp') and 'The sanctuary custodians carry their sacred objects and keep the customary observances with the travelling households.' or 'The Graji Canïfaha and its appointed custodians maintain the observances of the gathering season.')
  })
end
local function renderGeography(state)
  local loc=state.location
  local key=loc.site or (state.phase=='relocation' and loc.destination or loc.origin)
  local site=SITES[key]
  local r=REGIONS[site.regionKey]
  local rows={
    row('Gathering ground',escape(site.name)),
    row('Location on this date',escape(loc.title)),
    row('Geographic region',r.link..' — '..escape(r.parent)),
    row('Subregion / sector',escape(loc.sector)),
    row('Local gathering place',escape(site.ground)),
    row('Named settlement / landscape reference',site.nearest),
    row('Topography and habitat',escape(r.geology)..' '..escape(site.setting)),
    row('Adjacent geographic belts',r.neighbors),
    row('Climatic and ground conditions',escape(r.climate)..' '..escape(site.weather)),
    row('Permitted route context',escape(r.access)),
    row('Customary and ecological protection',escape(r.restriction)..' '..escape(site.sensitivities)),
    row('Geographic distinction',escape(r.distinction)),
    row('Current civic consequence',escape(loc.description)),
    row('Regional setting','The gathering grounds lie within the Alperkin seasonal circuit; neighbouring permanent settlements remain independent of the Great Yurt.')
  }
  if state.phase=='relocation' then
    rows[#rows+1]=row('Migration progress','Silence procession, day '..loc.travelStep..'; passage: '..escape(loc.travelSector))
  end
  return renderTable('Geographic dossier: '..escape(site.short),rows)
end
local function renderGazetteer(state)
  local out={'{| class="wikitable sortable" style="width:100%;"','! Ground !! Place in the annual circuit !! Geographic landscape !! Regional access and considerations'}
  for _,key in ipairs(SITE_ORDER) do
    local site=SITES[key]
    local flag=(state.location.site==key) and "CURRENT GATHERING — " or (state.location.destination==key and 'Next assembly — ' or '')
    out[#out+1]='|-\n| '..site.code..' / <code>'..key..'</code> — '..flag..escape(site.name)..'\n| '..REGIONS[site.regionKey].link..'\n| '..site.nearest..'<br />'..escape(site.ground)..'\n'
  end
  out[#out+1]='|}'
  out[#out+1]="''The Great Yurt occupies the gathering ground indicated for the relevant season; Erythros, Acheron and Vaeringheim remain neighbouring permanent cities and points of onward travel.''"
  return table.concat(out,'\n')..'\n'
end
local function renderWaypoints(state)
  local a,b=state.location.origin,state.location.destination
  local out={'{| class="wikitable" style="width:100%;"','! PSSC date !! Stage of the procession !! Region and passage'}
  for doy=146,159 do
    local date=dateFromDoy(doy,state.date.year)
    local loc=currentLocation(date,{site=a,nextsite=b})
    local label=(doy<=151 and 'Dismantling at departure ground' or doy<=158 and 'Silence movement; travelling civic custody' or 'Receiving landscape / dispersed winter camp')
    out[#out+1]='|-\n| '..date.label..'\n| '..label..'\n| '..escape(loc.description)..'\n'
  end
  out[#out+1]='|}'
  out[#out+1]="''Travel follows permitted clan roads and receiving grounds; passage through closed sacred lands is not part of the city’s itinerary.''"
  return table.concat(out,'\n')..'\n'
end

local function renderEconomy(state)
  local loc=state.location
  local ctx=SITES[loc.site or loc.origin]
  local m=state.metrics
  local note='Trade uses the city’s portable markets and registered external depots.'
  if state.phase=='relocation' or state.phase=='winter-camp' then
    note='The main city market is dispersed; trade and correspondence pass through registered travelling agents and fixed transfer depots.'
  elseif state.phase=='closing' or state.phase=='dismantling' then
    note='Merchants settle obligations, dispatch remaining stock and preserve claims to spaces in the next assembled market.'
  end
  return renderTable('Commerce and the General Port connection',{
    row('Commercial phase',note),
    row('Current landscape advantage',ctx.advantages),
    row('Principal outbound goods','Portable textiles and craftwork, household products, permitted botanicals and authorised religious objects.'),
    row('Principal inbound goods','Food staples, hardware, medical supplies, shelter materials and equipment not readily produced by travelling households.'),
    row('National trade connections','The [[General Port of Lake Morovia]] and the Alpazkigz Regional Investor serve commercial exchange and civic distribution.'),
    row('Ordinary market',state.phase=='relocation' and 'The enclosed market is closed during the procession; mobile traders continue essential exchange.' or 'The market quarter receives merchants, household orders and visiting caravans.'),
    row('Port-linked trade','Registered forwarding depots carry goods between the city, permanent lake-plain markets and the General Port.'),
    row('Household supplies','Food, water and necessary stores are distributed through clan stewards and the city’s receiving depots.')
  })
end
local function renderRoutes(state)
  local loc=state.location
  local routeIntro
  if state.phase=='relocation' then
    routeIntro='Custodians and households travel between '..SITES[loc.origin].short..' and '..SITES[loc.destination].short..'. The procession follows approved clan roads and receiving grounds.'
  elseif state.phase=='winter-camp' then
    routeIntro='Households have reached the receiving landscape at '..SITES[loc.destination].short..', but the Great Yurt remains unassembled.'
  else
    routeIntro='The gathering landscape is '..SITES[loc.origin].short..'; approaches vary with the assembly ground and protected route permissions.'
  end
  local site=SITES[loc.site or loc.origin]
  local regional=REGIONS[site.regionKey]
  routeIntro=routeIntro..' Local approach: '..site.anchor..' Authorised access: '..regional.access
  local out={routeIntro,'\n{| class="wikitable" style="width:100%;"','! Route !! Destination / interface !! What moves !! Limitations'}
  out[#out+1]='|-\n| Current site to permanent network\n| '..site.nearest..'<br />'..table.concat(site.links,'; ')..'\n| Households, permitted freight and essential services\n| '..site.sensitivities
  for _,r in ipairs(ROUTES) do
    out[#out+1]="|-\n| '''"..r.name.."'''\n| "..r.destination..'<br />'..r.direction..'\n| '..r.goods..'\n| '..r.note
  end
  out[#out+1]='|}'
  out[#out+1]="\n'''Road and caravan arrangements:''' Stewards inspect crossings, rest grounds and emergency access along the permitted route.\n"
  return table.concat(out,'\n')
end
local function renderEnvironment(state)
  local site=SITES[state.location.site or state.location.origin]
  return renderTable('Environmental and civic-service conditions',{
    row('Geographic belt',REGIONS[site.regionKey].name),
    row('Local position',site.anchor),
    row('Terrain and habitat',REGIONS[site.regionKey].geology..' '..site.setting),
    row('Assembly landscape',site.setting),
    row('Landscape considerations',site.weather),
    row('Access restrictions',REGIONS[site.regionKey].restriction),
    row('Protection priorities',site.sensitivities),
    row('Water and sanitation','Water stewards inspect household drinking-water points, washing places and refuse routes for the current gathering ground.'),
    row('Fire precautions','Gatekeepers keep textile shelters, kitchens and emergency passages under regular inspection.'),
    row('Land stewardship','Local custodians safeguard protected roots, sacred routes and receiving grounds throughout the city’s residence.'),
    row('Health and household care','Healers, clan carers and visiting medical personnel serve households and maintain arrangements for urgent transport.')
  })
end
local function renderAdvisories(state)
  local out={"* '''"..POLICIES[state.policy].name.."''': "..POLICIES[state.policy].note}
  for _,warning in ipairs(state.concerns) do out[#out+1]='* '..warning end
  return table.concat(out,'\n')..'\n'
end
local function renderItinerary(state)
  local rows={}
  for offset=-1,2 do
    local year=state.date.year+offset
    if year>=0 then
      local key,source=siteForYear(year)
      if year==state.date.year and state.location.originSource:find('Specified',1,true) then
        key=state.location.origin; source=state.location.originSource
      elseif year==state.date.year+1 and state.location.destinationSource:find('Specified',1,true) then
        key=state.location.destination; source=state.location.destinationSource
      end
      rows[#rows+1]='|-\n| '..year..' PSSC\n| '..escape(SITES[key].short)..'\n| '..escape(SITES[key].sector)..'\n| '..escape(source)..'\n| Next assembly on 1/1/'..year..' PSSC; route preparations at the preceding Silence.\n'
    end
  end
  local out={'{| class="wikitable" style="width:100%;"','! PSSC year !! Gathering ground !! Regional position !! Annual circuit !! Seasonal change',table.concat(rows),'|}'}
  out[#out+1]="\n''The city follows its established six-ground circuit unless a different annual gathering ground is entered in the civic itinerary.''"
  return table.concat(out,'\n')..'\n'
end
local function renderCalendar(state)
  local checkpoints={1,2,7,13,54,61,62,63,72,113,122,123,124,137,146,151,152,153,158,159,173,183}
  local rows={}
  for _,doy in ipairs(checkpoints) do
    local date=dateFromDoy(doy,state.date.year)
    local phase,description=phaseFor(date)
    local festival=festivalToday(date)
    local label=festival and festival.name or description
    local loc=currentLocation(date,{site=state.location.originSource:find('Specified',1,true) and state.location.origin or nil,
      nextsite=state.location.destinationSource:find('Specified',1,true) and state.location.destination or nil})
    local active=(doy==state.date.doy) and "'''CURRENT SELECTED DAY''' — " or ''
    rows[#rows+1]='|-\n| '..active..date.label..'\n| '..escape(loc.title)..'\n| '..escape(phase)..'\n| '..escape(label)..'\n'
  end
  return '{| class="wikitable sortable" style="width:100%;"\n! Date !! Location !! Civic season !! Festival or civic activity\n'..table.concat(rows)..'|}\n'
end
local function renderDispatches(state,n)
  n=validInteger(n or 14,1,60) or 14
  local rows={}
  local current=state.date
  local siteArgs={site=state.location.originSource:find('Specified',1,true) and state.location.origin or nil,
    nextsite=state.location.destinationSource:find('Specified',1,true) and state.location.destination or nil,policy=state.policy}
  for offset=0,n-1 do
    local doy=current.doy+offset
    local year=current.year
    if doy>183 then doy=doy-183; year=year+1 end
    local d=dateFromDoy(doy,year)
    local locArgs={site=(year==current.year) and siteArgs.site or ((year==current.year+1) and siteArgs.nextsite or nil),
      nextsite=(year==current.year) and siteArgs.nextsite or nil}
    local loc=currentLocation(d,locArgs)
    local phase=phaseFor(d)
    local m=metrics(d,phase,loc,state.policy)
    local title,description=dailyEvent(d,phase,loc,m)
    rows[#rows+1]='|-\n| '..d.label..'\n| '..escape(loc.title)..'\n| '..escape(title)..'\n| '..escape(description)..'\n'
  end
  return '{| class="wikitable sortable" style="width:100%;"\n! Date !! Location !! Civic dispatch !! Summary\n'..table.concat(rows)..'|}\n'
end
-- Every generated table row MUST terminate with a newline before the next |- or |}.
local function renderMap(state)
  local loc=state.location
  local rows={}
  for _,key in ipairs(SITE_ORDER) do
    local s=SITES[key]
    local here=(loc.site==key and '● Current gathering ground' or
      (state.phase=='relocation' and loc.origin==key and '◌ Departure ground' or
      (state.phase=='relocation' and loc.destination==key and '➜ Receiving ground' or
      (loc.destination==key and '○ Next assembly ground' or '—'))))
    rows[#rows+1]='|-\n| '..s.code..': '..escape(s.short)..'\n| '..REGIONS[s.regionKey].link..'\n| '..here..'\n| '..escape(s.sector)..'\n| '..s.nearest..'\n'
  end
  return '{| class="wikitable" style="width:100%;"\n! Gathering ground !! Geographic landscape !! Place in the annual circuit !! Subregion !! Neighbouring settlement or route\n'..table.concat(rows)..'|}\n'..
    "''The named permanent cities provide regional points of reference; the Great Yurt stands on its own customary gathering grounds.''\n"
end


local function argumentTable(frame)
  local args={}
  if type(frame)=='table' then
    if type(frame.getParent)=='function' then
      local ok,parent=pcall(frame.getParent,frame)
      if ok and parent and parent.args then
        for k,v in pairs(parent.args) do if tidy(v)~='' then args[k]=v end end
      end
    end
    if frame.args then
      for k,v in pairs(frame.args) do if tidy(v)~='' then args[k]=v end end
    else
      for k,v in pairs(frame) do if type(k)=='string' and tidy(v)~='' then args[k]=v end end
    end
  end
  return args
end
local function withState(frame,render)
  local args=argumentTable(frame)
  local state,err=buildState(args)
  if not state then return '<strong class="error">Alpazkigz: '..escape(err)..'</strong>' end
  return render(state,args)
end

-- Public #invoke endpoints --------------------------------------------------
function p.dashboard(frame)
  return withState(frame,function(state,args)
    local out={
      "'''THE ALPAZKIGZ — CITY LIFE AND SEASONAL AFFAIRS'''",
      "\n''The city's current place in its annual journey, the work of its households, and its customary and civic affairs.''\n",
      renderSmall(state),
      renderHistory(state),
      heading('Gathering landscape and planned itinerary'),renderGeography(state),renderMap(state),renderGazetteer(state),renderItinerary(state),renderWaypoints(state),
      heading('Households and daily affairs'),renderMetrics(state),
      heading('Customary government and law'),renderGovernance(state),
      heading('Sacred calendar and ceremonial life'),renderSacred(state),
      heading('Markets and commerce'),renderEconomy(state),
      heading('District-by-district civic operations'),renderDistricts(state),
      heading('Caravan routes, rail interfaces and travel'),renderRoutes(state),
      heading('Ecology, public health and emergency preparedness'),renderEnvironment(state),
      heading('Notices to households and travellers'),renderAdvisories(state),
      heading('Festival days and the annual circuit'),renderCalendar(state)
    }
    if tidy(args.chronicle or '')~='' then
      out[#out+1]=heading('Civic chronicle')
      out[#out+1]=renderDispatches(state,args.chronicle)
    end
    out[#out+1]="\n''See also [[Alperkin]], [[List of cities in Bassaridia Vaeringheim#The Alpazkigz|The Alpazkigz]] and [[General Port of Lake Morovia]].''"
    return table.concat(out,'\n')
  end)
end
function p.geography(frame) return withState(frame,function(state) return renderGeography(state) end) end
function p.gazetteer(frame) return withState(frame,function(state) return renderGazetteer(state) end) end
function p.waypoints(frame) return withState(frame,function(state) return renderWaypoints(state) end) end
function p.location(frame) return withState(frame,function(state) return renderSmall(state) end) end
function p.calendar(frame) return withState(frame,function(state) return renderCalendar(state) end) end
function p.districts(frame) return withState(frame,function(state) return renderDistricts(state) end) end
function p.economy(frame) return withState(frame,function(state) return renderEconomy(state) end) end
function p.governance(frame) return withState(frame,function(state) return renderGovernance(state) end) end
function p.sacred(frame) return withState(frame,function(state) return renderSacred(state) end) end
function p.routes(frame) return withState(frame,function(state) return renderRoutes(state) end) end
function p.environment(frame) return withState(frame,function(state) return renderEnvironment(state) end) end
function p.map(frame) return withState(frame,function(state) return renderMap(state) end) end
function p.itinerary(frame) return withState(frame,function(state) return renderItinerary(state) end) end
function p.chronicle(frame)
  return withState(frame,function(state,args) return renderDispatches(state,args.days or args.chronicle or 14) end)
end
function p.field(frame)
  return withState(frame,function(state,args)
    local key=tidy(args.field or args[1] or ''):lower()
    local fields={
      date=state.date.label, location=state.location.title,
      region=state.location.region, subregion=state.location.sector,
      ground=state.location.ground, nearestcity=state.location.nearest,
      sitecode=SITES[state.location.site or state.location.origin].code,
      corridor=state.location.travelSector,
      phase=state.phase, status=state.phaseDescription,
      assembly=state.location.assembled and 'Assembled / under reconstruction' or 'Not fully assembled',
      origin=SITES[state.location.origin].short,
      destination=SITES[state.location.destination].short,
      nextfestival=state.nextFestival.name,
      daysuntilfestival=tostring(state.daysUntilNext),
      attendance=tostring(state.metrics.attendance),
      market=tostring(state.metrics.market), trade=tostring(state.metrics.trade),
      water=tostring(state.metrics.water), routes=tostring(state.metrics.routes),
      stewardship=tostring(state.metrics.stewardship),
      source=state.location.originSource
    }
    if fields[key]==nil then return '<strong class="error">Unknown field: '..escape(key)..'</strong>' end
    return escape(fields[key])
  end)
end

-- Unrendered helper for testing or coordinated modules; not an #invoke endpoint.
-- Changes here do not store persistent state between page views.
p._state=buildState
p._dateFromDoy=dateFromDoy
p._currentLocation=currentLocation
p._phaseFor=phaseFor
p._festivalToday=festivalToday
return p