Regional Governance¶
A Region is a player-owned (or platform-owned) territory of 100–1000 sectors with its own rules, taxes, language pack, and governance model. Regional governance is how the people who live in a region collectively shape it — through elections, policy votes, and treaties with neighboring regions.
This page describes the player-visible governance loop. Subsystem invariants (tally math, lock contracts, scheduler windows) live in SYSTEMS/regional-governance.md.
✅ Shipped — Region, RegionalMembership, RegionalElection, RegionalVote, RegionalPolicy, RegionalTreaty models; service-layer create / vote / configure operations; treaty CRUD.
🚧 Partial — automated election close & winner-determination scheduler, automatic policy enactment when threshold met, treaty-expiry sweeper, voting-power recalc on membership change.
📐 Design-only — coalition / multi-candidate elections beyond plurality, secret ballot vs open ledger toggle per region, recall elections, weighted treaty terms beyond the current free-form terms JSONB.
Governance models¶
A Region's governance_type is one of:
| Code | Friendly name | Decision authority |
|---|---|---|
autocracy |
Autocracy | The owner decides everything; no votes required |
democracy |
Democracy | Citizens vote on policies and elect officials |
council |
Council | Elected council members vote on policies; citizens elect council |
The owner (Region.owner_id) is the human who pays the subscription. In autocracy mode they have unilateral authority; in democracy or council mode they retain veto power and the ability to call elections, but daily policy is voted on.
✅ Shipped (governance_type field). 🚧 Partial — autocracy is fully wired; democracy and council use the same vote machinery but the enactment path differs: autocracy applies changes immediately, democracy/council require a passing vote.
Membership and voting power¶
Players become members of a region when they spend turns in it or set it as their home region. RegionalMembership stores:
| Field | Meaning |
|---|---|
membership_type |
visitor, resident, or citizen |
reputation_score |
−1000 to +1000 (regional, separate from global personal reputation) |
local_rank |
Free-form text rank ("Senator", "Magistrate") set by region |
voting_power |
Decimal 0.0–5.0; default 1.0 |
joined_at, last_visit, total_visits |
Engagement record |
Voting eligibility¶
membership.can_vote = (
membership_type in ("citizen", "resident")
and voting_power > 0
)
Visitors do not vote. Citizens have full rights; residents typically have voting rights but reduced ceremonial standing. ✅ Shipped — RegionalMembership.can_vote property.
Voting power calculation¶
Default voting_power is 1.0. Region owners can adjust (clamped 0.0–5.0) based on:
- Membership tier: residents may default to 1.0, citizens to 1.5 (target spec).
- Local rank: senators get 2.0, magistrates 3.0 etc. (region-defined).
- Reputation: high regional reputation can grant a bonus (target spec — currently a static field).
📐 Design-only — automatic voting-power recalculation when reputation or membership changes.
Quorum¶
A vote is valid only if total voting power cast meets a minimum threshold. Target spec:
quorum = max(0.10 * total_eligible_voting_power, 5)
10% of total possible voting power, with a floor of 5 individual voters. Below quorum, the policy fails regardless of approval %.
📐 Design-only — quorum check in the policy resolution path.
Elections¶
RegionalElection is created via RegionalGovernanceService.start_election:
position = "governor" | "council_member" | "ambassador" | ...
candidates = JSONB list [{player_id, platform}]
voting_opens_at = now()
voting_closes_at = now() + voting_duration_days (default 7)
status = ACTIVE
A region can only have one active election per position at a time — the service rejects creation if an active election already exists.
Voting¶
POST /api/v1/regions/{region_id}/elections/{election_id}/vote
{
"candidate_id": "..."
}
Validations:
- Voter must be a member of the region.
- Voter's can_vote is true.
- Election is ACTIVE and now is within [voting_opens_at, voting_closes_at].
- Voter has not already voted in this election (UniqueConstraint('election_id', 'voter_id')).
A RegionalVote row is created with weight = membership.voting_power. ✅ Shipped (model + uniqueness constraint).
Result determination¶
When voting_closes_at passes, the scheduler:
- Aggregates
sum(weight) per candidate_id. - Picks plurality winner (most weight) by default.
- For high-stakes positions (governor), require supermajority — winner must clear
Region.voting_threshold(default 0.51, range 0.10–0.90) to take the position. If no candidate clears threshold, the election runs off or is voided. - Writes
RegionalElection.resultsJSONB with full tallies. - Sets
status = COMPLETED. - Emits
election_completedevent with winner and tallies.
🚧 Partial — service has start_election and individual vote creation; the close-and-tally scheduler is target spec.
Election duration¶
Min 1 day, max 30 days. Default 7. Set per-election via voting_duration_days. Region's election_frequency_days (default 90, range 30–365) controls how often a recurring office must hold a new election.
Policy proposals¶
RegionalPolicy is the core policy mechanism. Any citizen with sufficient reputation (target threshold: regional reputation ≥ 100) can propose:
POST /api/v1/regions/{region_id}/policies
{
"policy_type": "tax_rate" | "pvp_rules" | "trade_policy" | ...,
"title": "...",
"description": "...",
"proposed_changes": { ... }
"voting_duration_days": 7
}
The proposal enters the VOTING state with voting_closes_at = now + duration.
Voting on policies¶
Citizens cast yes/no votes. Each vote contributes the voter's voting_power to either votes_for or votes_against.
votes_for = Σ voting_power of yes-voters
votes_against = Σ voting_power of no-voters
total_votes = votes_for + votes_against
approval_pct = votes_for / total_votes
is_passing = approval_pct >= region.voting_threshold
✅ Shipped — RegionalPolicy.is_passing property and tally fields.
Enactment¶
When voting_closes_at passes:
- Lock the policy row.
- Verify quorum met. If not, set
status = REJECTED. - If
is_passing, setstatus = PASSED. - Apply
proposed_changesto the region (e.g., changetax_rate, modifytrade_bonuses). - Set
status = IMPLEMENTED. - Emit
policy_enactedevent. - If failed, set
status = REJECTED.
🚧 Partial — voting and tally exist; the automatic enactment that applies proposed_changes to the region row is target spec. Currently an admin or owner manually applies the change.
Policy types¶
| Type | Affects |
|---|---|
tax_rate |
Region.tax_rate (5%–25% range) |
pvp_rules |
Stored in Region.governance JSONB; sectors check it |
trade_policy |
Region.trade_bonuses JSONB |
cultural_identity |
Region.language_pack, Region.aesthetic_theme, Region.traditions |
governance_change |
Region.governance_type (requires supermajority) |
Supermajority¶
Constitutional changes (governance_change, voting_threshold itself) require approval ≥ 0.66 regardless of region default. 📐 Design-only — type-specific threshold table.
Treaties between regions¶
Two regions can sign a treaty via mutual approval. RegionalTreaty:
| Field | Meaning |
|---|---|
region_a_id, region_b_id |
Signatories (must differ) |
treaty_type |
trade_agreement, defense_pact, non_aggression, cultural_exchange |
terms |
JSONB of negotiated terms |
signed_at, expires_at |
Time bounds |
status |
active, expired, cancelled |
Negotiation flow¶
- Region A's owner / governor proposes:
POST /api/v1/regions/{a}/treatieswith target regionb, type, and terms. - Region B's owner / governor receives the proposal in their treaty inbox.
- Region B accepts (creating the row with
status=active) or rejects. - Treaty is bilateral once accepted.
🚧 Partial — service has get_regional_treaties and uniqueness on (region_a, region_b, type). The proposal-and-acceptance flow exists at the API level but the inbox / approval UI is design-stage.
Treaty effects¶
| Type | Effect |
|---|---|
trade_agreement |
Reduced tariffs at each region's stations for citizens of the other region |
defense_pact |
Visiting fleets receive defensive aid in PvP encounters |
non_aggression |
PvP between citizens of the two regions costs +50% turns and is reputation-penalized |
cultural_exchange |
Shared language_pack entries; UI shows partner region's flavor text |
📐 Design-only — most effect application paths.
Expiry¶
Treaties with expires_at set are automatically status=expired once the timestamp passes. A scheduled sweeper handles this.
📐 Design-only — sweeper.
Region statistics¶
The governance UI pulls from get_regional_stats:
{
"total_population": 1247,
"citizen_count": 312,
"resident_count": 488,
"visitor_count": 447,
"average_reputation": 42.3,
"active_elections": 1,
"pending_policies": 3,
"treaties_count": 4
}
✅ Shipped.
UI surface¶
A region's governance page shows: - Population breakdown. - Active elections with candidate platforms and current tallies. - Pending policies with description, vote counts, time remaining. - Active treaties with partner regions. - For the owner: configuration controls (tax rate slider, governance toggle, election scheduler).
🚧 Partial — admin UI is solid; player-facing voting UI is functional but minimal.
Source map¶
| Concern | Path (target) |
|---|---|
| Region model | services/gameserver/src/models/region.py (Region, enums) |
| Membership | same file (RegionalMembership) |
| Election + Vote models | same file (RegionalElection, RegionalVote) |
| Policy model | same file (RegionalPolicy) |
| Treaty model | same file (RegionalTreaty) |
| Service | services/gameserver/src/services/regional_governance_service.py |
| Routes | services/gameserver/src/api/routes/regional_governance.py (target) |
| Scheduler (close elections, enact policies, expire treaties) | services/gameserver/src/services/regional_governance_scheduler.py (target) |
Related¶
OPERATIONS/multi-regional.md— region lifecycle, subscription model, Central Nexus.SYSTEMS/regional-governance.md— invariants, scheduler timing, vote-tally math.factions-and-teams.md— distinguishing regional citizenship from NPC factions.SYSTEMS/genesis-deploy.md— how regions are created.