Skip to content

Regional Governance

Purpose

Player-owned regions have governance: elections, policies, treaties. The governance subsystem is a set of cooperating state machines (election, policy, treaty) that schedule themselves on time and on member action. The owner sets the governance type (autocracy, council, democracy), but the lifecycle of any individual proposal or election runs identically. All state changes are persisted and propagated through the realtime bus to the region room.

Inputs

The system reads: - The Region row — governance_type, voting_threshold, election_frequency_days, tax_rate, treasury, constitutional_text. - RegionalMembership rows — membership_type (citizen/resident/visitor), voting_power, reputation_score. - RegionalElection, RegionalVote, RegionalPolicy, RegionalTreaty rows. - The proposing/voting Player (and User) row. - Wall-clock time for window enforcement.

The system fires on: - A scheduled tick (cron / background task) — opens elections that are due, closes elections whose voting_closes_at has passed, finalizes policies past their voting window. - POST /api/v1/regions/{id}/elections — manual election start. - POST /api/v1/regions/{id}/policies — policy proposal. - POST /api/v1/regions/{id}/elections/{eid}/vote — cast vote. - POST /api/v1/regions/{id}/policies/{pid}/vote — vote on a policy. - POST /api/v1/regions/{id}/treaties — propose / accept treaty.

Process

Election state machine

        ┌────────────────┐
        │  SCHEDULED     │  (auto-created by tick when prior term ends)
        └───────┬────────┘
                │ voting_opens_at reached
                ▼
        ┌────────────────┐
        │  ACTIVE        │  voting open, candidates locked
        └───────┬────────┘
                │ voting_closes_at reached  OR  manual close
                ▼
        ┌────────────────┐
        │  TALLYING      │  vote tally, winner determined
        └───────┬────────┘
                │ tally complete
                ▼
        ┌────────────────┐
        │  COMPLETED     │  winner persisted, term begins
        └────────────────┘

Election scheduling

  1. Region.election_frequency_days defines the base cadence (default 90 days for player regions).
  2. The scheduler tick runs hourly. For every region with no active election whose previous election ended ≥ election_frequency_days ago, it calls start_election(region_id, position, voting_duration_days, candidates).
  3. A region can have at most one ACTIVE election per position simultaneously (governor, council seat, etc.). Duplicate start is a no-op.
  4. Election rows carry voting_opens_at and voting_closes_at. Default voting window: 7 days.

Candidate registration

  1. During SCHEDULED or early ACTIVE phase (configurable cutoff), citizens can register: POST .../elections/{id}/candidates.
  2. Eligibility: RegionalMembership.membership_type = "citizen", reputation_score ≥ MIN_CANDIDACY_REP (default 0).
  3. Candidate names appended to RegionalElection.candidates JSONB array. Mutation flagged for SQLAlchemy.
  4. Cutoff: candidates lock when state advances to ACTIVE (no late entries).

Voting

  1. cast_vote(election_id, voter_id, candidate):
  2. Validate election is ACTIVE.
  3. Validate voter has RegionalMembership.membership_type ∈ {citizen, resident} (residents may vote with reduced weight, design-dependent).
  4. Reject duplicate votes (UNIQUE(voter_id, election_id) constraint).
  5. Insert RegionalVote row carrying voter_id, candidate, voting_power = membership.voting_power.
  6. Vote weight defaults to 1.0; reputation- or stake-weighted variants override.

Tally

  1. When voting_closes_at passes (scheduler tick) or the owner manually closes:
  2. Aggregate RegionalVote rows by candidate, summing voting_power.
  3. Winner = max sum; ties broken by earliest registration.
  4. If no votes cast → election declared INCONCLUSIVE; new election scheduled at the next tick.
  5. Persist winner to RegionalElection.winner and the appropriate Region.{position}_id field.
  6. State → COMPLETED. Emit governance_event with kind = "election_completed".

Policy state machine

        ┌──────────────┐
        │ DRAFT        │  proposer composing
        └──────┬───────┘
               │ submit
               ▼
        ┌──────────────┐
        │ VOTING       │  voting_closes_at = now + duration
        └──────┬───────┘
               │ window closes
               ▼
        ┌──────────────┐
        │ TALLYING     │
        └──┬────────┬──┘
   passes  │        │  fails
           ▼        ▼
   ┌────────────┐ ┌────────────┐
   │ ENACTED    │ │ REJECTED   │
   └────────────┘ └────────────┘

Policy proposal → enactment

  1. create_policy_proposal(region_id, proposer_id, policy_data):
  2. Required: policy_type, title, proposed_changes (JSONB).
  3. Optional: description, voting_duration_days (default 7).
  4. Computes voting_closes_at; persists with status VOTING.
  5. Voting: same vote model as elections (membership-gated, weighted, deduplicated).
  6. Tally:
  7. Pass condition: yes_weight / (yes_weight + no_weight) ≥ Region.voting_threshold (default 0.5; supermajority configs allowed).
  8. Quorum check: total_voting_weight ≥ MIN_QUORUM (default 0.1 of citizen population).
  9. Fail → REJECTED; pass → ENACTED.
  10. On ENACTED:
  11. Apply proposed_changes to the relevant Region fields (e.g. tax_rate, trade_bonuses).
  12. Persist a policy_change_log entry for audit.
  13. Emit governance_event with kind = "policy_enacted".

Treaty negotiation

   region A proposes ─► PROPOSED ─► region B accepts ─► ACTIVE
                              │
                              └► region B rejects ─► REJECTED
                              └► no response in N days ─► EXPIRED

   ACTIVE ─► withdrawal by either side ─► TERMINATED
   ACTIVE ─► expires_at reached ─► EXPIRED
  1. Region A's owner (or council) calls propose_treaty(region_a_id, region_b_id, treaty_type, terms, expires_at).
  2. Insert RegionalTreaty row with status = PROPOSED.
  3. Region B receives a governance_event notification.
  4. Region B accepts → status = ACTIVE, signed_at = now. Reject → REJECTED.
  5. ACTIVE treaties can be terminated unilaterally if the treaty terms allow; otherwise mutual.
  6. The scheduler tick auto-EXPIRES treaties past expires_at.

Membership lifecycle (relevant context)

  • A Player joining a region is enrolled as membership_type = visitor.
  • Citizenship is granted by policy / owner / time-in-region threshold.
  • Voting power is set on the membership row.

Outputs / state changes

Per election cycle: - RegionalElection rows traverse SCHEDULED → ACTIVE → TALLYING → COMPLETED. - RegionalVote rows accumulate during ACTIVE. - Region.{position}_id updated on COMPLETED.

Per policy: - RegionalPolicy rows traverse DRAFT → VOTING → TALLYING → {ENACTED, REJECTED}. - On ENACTED, the matching Region columns are mutated (tax, bonuses, configs).

Per treaty: - RegionalTreaty rows traverse PROPOSED → ACTIVE → {EXPIRED, TERMINATED}.

Events emitted (region room): - governance_event with kind: - election_scheduled, election_started, election_completed - policy_proposed, policy_voting_closed, policy_enacted, policy_rejected - treaty_proposed, treaty_signed, treaty_terminated, treaty_expired - notification (personal) to relevant participants (proposers, candidates, voters).

Invariants

  1. At most one ACTIVE election per (region_id, position).
  2. At most one vote per (voter_id, election_id) and per (voter_id, policy_id).
  3. Votes recorded only while parent record is ACTIVE / VOTING.
  4. State transitions are monotonic — no return from COMPLETED to ACTIVE.
  5. Policy enactment changes are atomic with the policy state transition (single transaction).
  6. Treaty region_a_id ≠ region_b_id and a region cannot have two ACTIVE treaties of the same treaty_type with the same partner.
  7. The scheduler is idempotent: re-running the tick over already-processed elections / policies is a no-op.
  8. Voting power is read at vote-cast time (not at tally) — citizenship changes after voting do not change recorded weight.

Failure modes

Mode Target handling
Scheduler tick missed (downtime) Next tick processes everything past its window; no events lost.
Quorum not met Policy → REJECTED with reason = "no_quorum"; election → INCONCLUSIVE.
Policy proposed_changes references unknown field Validator catches at proposal time (400); ENACTED policies guaranteed schema-correct.
Tied election Tie-breaker: earliest candidate registration wins; logged.
Region owner override Autocracy / council governance types let the owner bypass voting; policy goes straight to ENACTED, audit-logged with actor = owner.
Voter no longer a citizen at tally Vote already counted at cast time; not retroactively invalidated.
Treaty proposed to a deleted region Validator rejects at proposal time.
Concurrent vote (race) Unique constraint rejects the second insert.
Region currently in transfer of ownership Governance actions blocked for the duration; surfaced as 423 Locked.

Source map

Concern Path (target)
Governance service services/gameserver/src/services/regional_governance_service.py
Region / governance models services/gameserver/src/models/region.py
Regional auth (citizen / resident gating) services/gameserver/src/services/regional_auth_service.py
Scheduler tick services/gameserver/src/services/regional_governance_service.py:tick_governance (target)
API routes services/gameserver/src/api/routes/regional_governance.py
Admin governance tools services/gameserver/src/api/routes/admin_comprehensive.py
Realtime broadcast services/gameserver/src/services/websocket_service.py (region room)