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¶
Region.election_frequency_daysdefines the base cadence (default 90 days for player regions).- The scheduler tick runs hourly. For every region with no active election whose previous election ended ≥
election_frequency_daysago, it callsstart_election(region_id, position, voting_duration_days, candidates). - A region can have at most one ACTIVE election per
positionsimultaneously (governor, council seat, etc.). Duplicate start is a no-op. - Election rows carry
voting_opens_atandvoting_closes_at. Default voting window: 7 days.
Candidate registration¶
- During SCHEDULED or early ACTIVE phase (configurable cutoff), citizens can register:
POST .../elections/{id}/candidates. - Eligibility:
RegionalMembership.membership_type = "citizen",reputation_score ≥ MIN_CANDIDACY_REP(default 0). - Candidate names appended to
RegionalElection.candidatesJSONB array. Mutation flagged for SQLAlchemy. - Cutoff: candidates lock when state advances to ACTIVE (no late entries).
Voting¶
cast_vote(election_id, voter_id, candidate):- Validate election is ACTIVE.
- Validate voter has
RegionalMembership.membership_type ∈ {citizen, resident}(residents may vote with reduced weight, design-dependent). - Reject duplicate votes (
UNIQUE(voter_id, election_id)constraint). - Insert
RegionalVoterow carryingvoter_id,candidate,voting_power = membership.voting_power. - Vote weight defaults to 1.0; reputation- or stake-weighted variants override.
Tally¶
- When
voting_closes_atpasses (scheduler tick) or the owner manually closes: - Aggregate
RegionalVoterows bycandidate, summingvoting_power. - Winner = max sum; ties broken by earliest registration.
- If no votes cast → election declared
INCONCLUSIVE; new election scheduled at the next tick. - Persist winner to
RegionalElection.winnerand the appropriateRegion.{position}_idfield. - State → COMPLETED. Emit
governance_eventwithkind = "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¶
create_policy_proposal(region_id, proposer_id, policy_data):- Required:
policy_type,title,proposed_changes(JSONB). - Optional:
description,voting_duration_days(default 7). - Computes
voting_closes_at; persists with statusVOTING. - Voting: same vote model as elections (membership-gated, weighted, deduplicated).
- Tally:
- Pass condition:
yes_weight / (yes_weight + no_weight) ≥ Region.voting_threshold(default 0.5; supermajority configs allowed). - Quorum check:
total_voting_weight ≥ MIN_QUORUM(default 0.1 of citizen population). - Fail →
REJECTED; pass →ENACTED. - On
ENACTED: - Apply
proposed_changesto the relevantRegionfields (e.g.tax_rate,trade_bonuses). - Persist a
policy_change_logentry for audit. - Emit
governance_eventwithkind = "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
- Region A's owner (or council) calls
propose_treaty(region_a_id, region_b_id, treaty_type, terms, expires_at). - Insert
RegionalTreatyrow withstatus = PROPOSED. - Region B receives a
governance_eventnotification. - Region B accepts →
status = ACTIVE,signed_at = now. Reject →REJECTED. - ACTIVE treaties can be terminated unilaterally if the treaty terms allow; otherwise mutual.
- The scheduler tick auto-EXPIRES treaties past
expires_at.
Membership lifecycle (relevant context)¶
- A
Playerjoining a region is enrolled asmembership_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¶
- At most one
ACTIVEelection per(region_id, position). - At most one vote per
(voter_id, election_id)and per(voter_id, policy_id). - Votes recorded only while parent record is
ACTIVE/VOTING. - State transitions are monotonic — no return from
COMPLETEDtoACTIVE. - Policy enactment changes are atomic with the policy state transition (single transaction).
- Treaty
region_a_id ≠ region_b_idand a region cannot have two ACTIVE treaties of the sametreaty_typewith the same partner. - The scheduler is idempotent: re-running the tick over already-processed elections / policies is a no-op.
- 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) |
Related¶
- DATA_MODELS: regional governance entities in
../DATA_MODELS/admin.md,../DATA_MODELS/entities.md. - FEATURES:
../FEATURES/gameplay/factions-and-teams.md. - ARCHITECTURE:
../ARCHITECTURE/multi-regional.md. - OPERATIONS:
../OPERATIONS/multi-regional.md. - SYSTEMS: galaxy-generation.md, realtime-bus.md.
- API: regional governance routes.