Skip to content

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:

  1. Aggregates sum(weight) per candidate_id.
  2. Picks plurality winner (most weight) by default.
  3. 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.
  4. Writes RegionalElection.results JSONB with full tallies.
  5. Sets status = COMPLETED.
  6. Emits election_completed event 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:

  1. Lock the policy row.
  2. Verify quorum met. If not, set status = REJECTED.
  3. If is_passing, set status = PASSED.
  4. Apply proposed_changes to the region (e.g., change tax_rate, modify trade_bonuses).
  5. Set status = IMPLEMENTED.
  6. Emit policy_enacted event.
  7. 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

  1. Region A's owner / governor proposes: POST /api/v1/regions/{a}/treaties with target region b, type, and terms.
  2. Region B's owner / governor receives the proposal in their treaty inbox.
  3. Region B accepts (creating the row with status=active) or rejects.
  4. 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)