Player Random Events
The Player Random Events (PREE) system is an optional gameplay layer that offers event-driven choices to players each turn. The system is gated behind a single feature flag — when disabled, no new events are offered, but pending instances still sweep (expire and resolve).
Feature Flag
The master gate is isPlayerRandomEventsEnabled() in src/lib/events/featureFlag.ts. It reads gameState.playerRandomEventsEnabled from the database and returns a boolean.
| State | Behavior |
|---|---|
false or absent | PREE skips new offers. Pending instances still sweep (expire and resolve). |
true | The per-turn driver offers one event per eligible character. |
Because the flag is a single boolean on the gameState document, admins can flip the system on or off at any time. When off, no new events are offered, but any already-pending events continue to exist until they expire or are resolved.
Per-Turn Driver
The PREE driver (src/lib/events/pree/driver.ts) runs each turn and orchestrates the full event-offer lifecycle:
- Sweep expired instances — resolve any pending event whose
expiresAtRealtimeMshas passed, notifying the owning player - Check the feature flag — if
isPlayerRandomEventsEnabled()is false, return early withoffered: 0 - Process election debates (if RPG stats are enabled) — roll debate challenges and resolve overdue debates before offering normal events. Debate participants are excluded from the standard event offer loop (the "supersede" rule).
- Load approved definitions — fetch all
EventDefinitiondocuments withstatus: "approved"from the database - Walk every player character in chunks of 200 — for each character:
- Skip if the character has a pending event already
- Skip if the character is on cooldown (per-kind and global cooldowns tracked in the event cooldown ledger)
- Filter definitions by eligibility tags
- Roll a weighted pick among eligible templates
- Offer the event: create a pending
EventInstanceand notify the player
The driver returns a PlayerRandomEventsTurnResult with counts of swept, offered, and skipped events.
Eligibility Checks
Not every character receives every event. The system uses a two-layer eligibility model:
Character Event Context
Each character is classified into a context at offer time:
| Field | How determined |
|---|---|
characterId | From the character document |
countryId | From the character document |
isPolitician | True if the character holds office OR is in an active election |
isCeo | True if the character is the CEO of a corporation |
isInElection | True if the character has an active candidacy |
Eligibility Tags
Each event definition declares an eligibility array of tags. A character matches if:
- The tag
"all"is present (matches everyone), OR - Any of these tags match the character's context:
"politician"— matches ifisPoliticianis true"ceo"— matches ifisCeois true"inElection"— matches ifisInElectionis true
This means an event tagged ["ceo"] only goes to characters running a corporation. An event tagged ["politician"] only goes to officeholders and active candidates. An event tagged ["all"] goes to everyone.
Cooldowns
Each character has an event cooldown ledger that tracks when they are next eligible for an offer, both globally and per-kind:
| Cooldown | Scope | Effect |
|---|---|---|
| Global | Per character | Blocks all event offers until the cooldown expires |
| Per-kind | Per character × event kind | Blocks offers of a specific event kind until the cooldown expires |
The cooldown ledger is checked before eligibility filtering — a character on global cooldown receives no offers regardless of how many eligible templates exist.
Event Definitions and Handlers
Each event kind has two components:
- EventDefinition (database document) — display copy, eligibility tags, default option, version. Definitions are seeded and admin-approved before they can be offered.
- EventHandler (registered in code) — outcome tables (positive/negative ranges per option), payload builder, default option ID.
The seed definitions live in src/lib/events/pree/countryDefinitions.ts and the handlers in src/lib/events/pree/handlers/. A barrel file (index.ts) side-effect-imports all handlers to register them at startup. A test verifies that every seed definition matches its registered handler and that option IDs haven't drifted.
Example Event Kinds
| Kind | Eligibility | Description |
|---|---|---|
| Lottery Win | all | A windfall event — lottery annuity paid out over time |
| Staffer Scandal | politician | A staffer's scandal damages the politician's reputation |
| Campaign Viral | politician | A campaign moment goes viral, boosting favorability |
| Corrupt Donor | politician | A donor offers tainted money — accept or refuse |
| CEO Whistleblower | ceo | A whistleblower threatens a corporation |
| Tax Audit | ceo | A tax audit targets a corporation |
| Old Friend Venture | all | An old friend pitches a business venture |
| Memoir Offer | politician | A publisher offers a memoir deal |
Each event presents the player with a choice (typically 2–4 options) and resolves with an outcome drawn from the handler's outcome tables, modified by a seeded RNG roll.
Active Event Tracking
A character can have at most one pending event at a time. The driver checks hasPendingEvent(db, "character", character._id) before offering — if a pending event exists, the character is skipped.
This prevents event spam: a character who hasn't resolved their current event won't receive another until they do. If the event expires before the player responds, it auto-resolves (typically with a default or negative outcome) and the character becomes eligible again on the next cooldown cycle.
Integration with RPG Stats
The PREE driver integrates with the RPG Stats system. When both flags are enabled:
- The driver calls
isRpgStatsEnabled()at the start of the turn - If true, it processes election debates before offering normal events
- Characters locked into a debate this pass are excluded from the standard event offer loop — a debate supersedes a standard event
- If RPG stats are off, the debate step is skipped and all eligible characters receive normal event offers
This coupling means debates and random events share the same "one event per character per turn" slot.
Strategic Implications
| Decision | Impact |
|---|---|
| PREE off | No random events — characters only experience crises and election cycles. Predictable, less dynamic. |
| PREE on | Characters face periodic choices with real consequences — windfalls, scandals, opportunities. Adds texture and unpredictability to the game loop. |
The flag is a game-admin decision, not a per-player choice. Once on, it applies to all player characters in the game.
Related Systems
- RPG Stats & Debates — Debates supersede normal PREE events when both flags are on
- Crisis Interaction — Separate feature flag, independent of PREE
- Core Systems — Turn structure, action economy