Electioneer ElectioneerDocumentation
Play
Reference/Overview

Electioneer

Last updated 2026-08-20

A single-player, turn-based election campaign simulator. 20 historical elections across six countries (US, UK, Canada, Germany, France, Australia), 1974 to 2027. Inspired by President Infinity's resource management and New Campaign Trail's branching demographic events, with a clean, data-forward web UI.

You are the campaign manager. Read the map, allocate finite resources each week, answer the moments that matter, and rewrite the result.

Play in the browser at lakesidegames.net/games/electioneer.

Quick start#

npm install
npm run dev        # play at the printed localhost URL
npm test           # engine unit + calibration tests
npm run calibrate  # just the 2020 calibration suite
npm run build      # static SPA → dist/ (Tauri-wrappable as-is)

No backend. The whole game runs client-side; saves go to IndexedDB.

Architecture#

The simulation engine is the core asset and is kept completely isolated: src/engine is pure TypeScript with zero React/DOM/browser dependencies, so it runs in the browser, in a Tauri shell, in tests, or headless for batch calibration.

src/
  engine/        Pure, deterministic, seedable sim. The single source of truth.
    types.ts        Data model (Section 4)
    rng.ts          Seedable RNG (mulberry32), determinism for undo/replay/tests
    setup.ts        createGame() + per-state baseline-margin solver
    voteModel.ts    The scoring model (Section 5): blocs → state → EV → winner
    actions.ts      9 campaign actions, all routed through the vote model
    events.ts       Events engine (applies authored event data)
    ai.ts           Tipping-point AI opponent (easy/normal/hard)
    turn.ts         advanceTurn(state, actions, seed) → newState  (PURE)
    polls.ts        Polls-with-noise + house effects (a blurred view of truth)
  content/       DATA ONLY, designers never touch engine code.
    issues.ts, blocs.ts, candidates.ts, states.ts, events.ts, mapLayout.ts
  persistence/   SyncProvider interface; LocalSyncProvider (Dexie/IndexedDB);
                 RemoteSyncProvider stub (cloud-save seam, not built)
  store/         Zustand store: canonical state, undo ring buffer, autosave
  ui/            React SPA, a pure function of engine state

The vote model (why the map moves is always legible)#

Each state's prior2020BidenShare (the real result) anchors a per-state baseline margin solve: a single scalar is fitted so the turnout-weighted aggregate of that state's demographic blocs reproduces 2020 exactly. Everything the campaign does writes margin deltas into campaignMargin and logs a human-readable cause. A bloc's two-party support is simply:

support_Biden(bloc) = sigmoid(baselineMargin + campaignMargin + momentumTerm)

Because every shift is an additive, logged cause, the "week in review" recap and the end-game post-mortem can trace any poll move back to the ad, rally, event answer, or gaffe that caused it (Pillar C).

Determinism#

advanceTurn(state, actions, seed) is pure: same inputs → same next state. This powers undo (a snapshot ring buffer in the store), reproducible tests, and a future server-authoritative multiplayer promotion with no engine rewrite.

Calibration (the correctness test)#

npm run calibrate asserts that neutral, do-nothing play reproduces history:

Scope#

In: the 2020 general only, two tickets, 51 contests + ME/NE district splits, Electoral College with a defined 269-269 contingent-election ending, weekly turn loop (9 turns Sept 1 → Nov 3), resource allocation, events, AI opponent, polls-with-noise, results + post-mortem, local saves + undo + JSON export/import.

Out (deferred): primaries, downballot, multiplayer, other years, modding, governance. Cloud saves and the Tauri desktop wrapper are designed-for but not built.

License#

PolyForm Noncommercial 1.0.0. The source is available to read, learn from, modify, and run noncommercially. Commercial use, including selling builds or hosting it as a paid service, is not licensed.

Ask