A browser-based historical world map strategy game inspired by Risk — featuring nine playable historical eras (plus custom/community maps), asymmetric factions, a tech tree, an economy system, event cards, secret missions, a 3D globe view, ranked matchmaking, a daily challenge, a campaign mode, game replays, an in-game cosmetics store, and a full JWT authentication system.
Population & Stability: Each territory tracks stability (0–100) and population (1–10). Low stability reduces economy income, limits how many draft units you can stack onto that territory per draft phase (server-enforced cumulative cap, with AI parity), and can trigger rebellion checks. High stability grows population, which boosts production. Captured territories start unstable with reduced population. Factions and garrisoning can accelerate stability recovery. See
docs/PLAYER_GUIDE.mdand in-app How to Play for full detail.
- Project Overview
- Playable Eras
- Tech Stack
- Project Structure
- Prerequisites
- Quick Start
- Environment Variables
- Database Setup
- Running the Application
- Game Mechanics Reference
- Population & Stability System
- Factions & Asymmetric Gameplay
- Technology Tree
- Economy & Buildings
- Event Cards
- Victory Conditions
- Map Editor Guide
- Architecture Overview
- Pages & Features
- Development Notes
Eras of Empire is a full-stack web application where players command armies across historically accurate maps spanning nine distinct eras (plus custom and community maps). Each era features asymmetric factions with unique passive bonuses and once-per-turn abilities, a multi-tier technology tree, an optional territory economy with upgradeable buildings, a shuffled deck of era-specific event cards, and a unique wonder structure. Matches support 2–8 players (human or AI bot), real-time WebSocket gameplay, reconnection recovery, fog of war, and multiple configurable victory conditions.
Beyond live multiplayer, the game includes a ranked matchmaking queue with Glicko-style ratings (μ/φ; see Ratings below), a daily challenge seeded from the date, a linear single-player campaign across six eras, a turn-by-turn replay viewer, a community map hub with ratings and moderation, a custom D3-based map editor, a 3D interactive globe view, an in-game cosmetics store, a friends system with game invites, an interactive tutorial, and guest play without registration.
| Era | Period | Territories | Connections | Regions |
|---|---|---|---|---|
| Ancient World | 200 AD | 28 | 40 | 8 |
| Medieval World | 1200 AD | 29 | 41 | 8 |
| Age of Discovery | 1600 AD | 34 | 51 | 8 |
| World War II | 1939–1945 | 35 | 53 | 8 |
| Cold War | 1947–1991 | 44 | 72 | 8 |
| The Modern Day | Present | 43 | 94 | 8 |
| American Civil War | 1861–1865 | 18 | 37 | 6 |
| Italian Unification | 1859–1871 | 14 | 23 | 6 |
| Space Age | 2100 AD | 55 | 93 | 10 |
Two community maps (14 Nations and Strait of Hormuz) are also included. Additional custom maps can be created with the built-in map editor and published to the community hub.
| Layer | Technology |
|---|---|
| Frontend | React 18 + TypeScript + Vite + TailwindCSS |
| 2D Map Rendering | PixiJS v7 (WebGL canvas) |
| 3D Globe | react-globe.gl (Three.js) |
| Map Editor | D3.js v7 (SVG-based polygon drawing) |
| State Management | Zustand |
| Real-time | Socket.io v4 (WebSockets) |
| Backend API | Node.js 22 + TypeScript + Fastify |
| Authentication | Custom JWT (access + refresh token rotation) |
| Relational DB | PostgreSQL 16 (Drizzle ORM) |
| Document DB | MongoDB 7 (Mongoose — map documents) |
| Cache / Leaderboards | Redis 7 |
| AI Bots | Server-side heuristic Minimax with Alpha-Beta Pruning, timeout-guarded worker |
| Ratings | Glicko-style μ (skill) + φ (uncertainty), per rating type; σ tracked at the schema level for forward compatibility |
| Dev Environment | Docker Compose + VS Code |
eras-of-empire/
├── README.md
├── package.json # Root monorepo workspace
├── .gitignore
│
├── backend/
│ ├── package.json
│ ├── tsconfig.json
│ ├── .env.example # ← Copy to .env and fill in
│ └── src/
│ ├── index.ts # Fastify server entry point
│ ├── config/ # Environment config loader
│ ├── db/
│ │ ├── postgres/ # PostgreSQL connection + migrations
│ │ ├── mongo/ # MongoDB connection + Map model
│ │ └── redis/ # Redis connection + helpers
│ ├── middleware/
│ │ ├── authenticate.ts # JWT auth middleware
│ │ └── rejectGuest.ts # Block guest accounts from mutating routes
│ ├── modules/
│ │ ├── auth/ # Register, login, refresh, logout, guest sessions
│ │ ├── users/ # Profile, achievements, leaderboard, ratings
│ │ ├── games/ # Game CRUD, lobby, join, replay, tutorial
│ │ ├── maps/ # Custom map CRUD, publish, rate, report
│ │ ├── campaign/ # Era campaign start/progress/advance
│ │ ├── daily/ # Daily challenge create/join/leaderboard
│ │ ├── matchmaking/ # Ranked queue (Glicko-style μ/φ, three time buckets)
│ │ └── store/ # Cosmetics catalog, gold-based purchases, loadout
│ ├── sockets/
│ │ └── gameSocket.ts # Socket.io real-time game server
│ └── game-engine/
│ ├── combat/ # Dice resolver, card bonuses, reinforcements
│ ├── state/ # Game state initializer and mutators
│ ├── ai/ # AI bot (minimax + alpha-beta, timeout worker)
│ ├── eras/ # Factions, tech trees, wonders per era
│ ├── events/ # Event card decks + effect applicator
│ ├── victory/ # Secret mission assignment + evaluation
│ ├── achievements/ # Achievement unlock evaluator
│ ├── rating/ # Glicko-style μ/φ update logic
│ ├── tutorial/ # Tutorial game builder
│ └── validation/ # Map graph + connection validator
│
├── frontend/
│ ├── package.json
│ ├── tsconfig.json
│ ├── vite.config.ts
│ ├── tailwind.config.js
│ ├── index.html
│ └── src/
│ ├── main.tsx # React entry point
│ ├── App.tsx # Router + route guards
│ ├── pages/
│ │ ├── LandingPage.tsx # Public marketing page
│ │ ├── LoginPage.tsx # Authentication
│ │ ├── RegisterPage.tsx # Account creation
│ │ ├── LobbyPage.tsx # Game browser + create game
│ │ ├── GamePage.tsx # Main game view (map + HUD)
│ │ ├── ReplayPage.tsx # Turn-by-turn replay viewer
│ │ ├── MapEditorPage.tsx # Custom map creation tool
│ │ ├── MapHubPage.tsx # Community map browser
│ │ ├── ProfilePage.tsx # User stats, achievements, and history
│ │ ├── FriendsPage.tsx # Friends list, pending requests, game invites
│ │ ├── StorePage.tsx # Cosmetics catalog + loadout equip
│ │ ├── CampaignPage.tsx # Single-player era campaign progress
│ │ ├── DailyChallengePage.tsx # Daily seeded challenge + leaderboard
│ │ ├── TutorialPage.tsx # Auto-starts an interactive tutorial game
│ │ ├── HowToPlayPage.tsx # In-app rules reference (collapsible sections)
│ │ ├── PrivacyPage.tsx
│ │ └── NotFoundPage.tsx
│ ├── components/
│ │ └── game/
│ │ ├── GameMap.tsx # PixiJS WebGL 2D map renderer
│ │ ├── GlobeMap.tsx # react-globe.gl 3D globe renderer
│ │ ├── GameHUD.tsx # Phase controls, player list, cards
│ │ ├── TerritoryPanel.tsx # Territory action panel
│ │ ├── BuildingPanel.tsx # Economy: build / upgrade structures
│ │ ├── TechTreeModal.tsx # Research tech nodes
│ │ ├── EventCardModal.tsx # Draw and apply event cards
│ │ ├── ActionModal.tsx # Confirm attack / fortify actions
│ │ ├── BonusesModal.tsx # Region bonuses inspector
│ │ ├── GameChat.tsx # In-game live chat
│ │ ├── EraModifierBadge.tsx # Active era modifier display
│ │ ├── TutorialOverlay.tsx # In-map tutorial guidance slides
│ │ ├── InviteFriendsModal.tsx # Invite friends to open game
│ │ └── AtomBombAnimation.tsx # Cold War / Modern special effect
│ ├── store/
│ │ ├── authStore.ts # Zustand auth state
│ │ └── gameStore.ts # Zustand game state + replay snapshots
│ └── services/
│ ├── api.ts # Axios instance with interceptors
│ └── socket.ts # Socket.io singleton
│
├── database/
│ ├── migrations/ # 12 sequential PostgreSQL migrations
│ ├── seeds/ # Initial achievements, medals, cosmetics
│ ├── maps/ # Era + community map JSON files (10 total)
│ └── seedMaps.ts # MongoDB map seeder
│
└── docker/
├── docker-compose.yml # Local dev: PostgreSQL + MongoDB + Redis
└── docker-compose.prod.yml # Production: nginx + backend + databases
Before running Eras of Empire locally, ensure the following are installed:
| Tool | Version | Install |
|---|---|---|
| Node.js | v22+ | https://nodejs.org |
| pnpm | v9+ | npm install -g pnpm |
| Docker Desktop | Latest | https://www.docker.com/products/docker-desktop |
| VS Code | Latest | https://code.visualstudio.com |
Recommended VS Code Extensions:
- ESLint
- Prettier
- TypeScript and JavaScript Language Features
- Tailwind CSS IntelliSense
- Docker
- REST Client (for testing API endpoints)
Follow these steps in order. Each step must complete successfully before proceeding.
# From the project root directory
pnpm installThis installs dependencies for both backend/ and frontend/ workspaces simultaneously.
# Backend
cp backend/.env.example backend/.env
# Frontend (optional — Vite proxy handles API routing automatically)
cp frontend/.env.example frontend/.envOpen backend/.env and update the following:
JWT_ACCESS_SECRET— Replace with a long random string (min 64 chars)JWT_REFRESH_SECRET— Replace with a different long random string (min 64 chars)
All other values match the Docker Compose defaults and do not need changing for local development.
docker-compose -f docker/docker-compose.yml up -dThis starts PostgreSQL (port 5432), MongoDB (port 27017), and Redis (port 6379) as background services. Verify they are running:
docker-compose -f docker/docker-compose.yml psAll three services should show Up status.
cd backend
pnpm run migrateThis creates all PostgreSQL tables (users, games, game_players, etc.).
cd backend
pnpm run seedThis inserts initial achievements and cosmetic items into PostgreSQL.
# Still in the backend/ directory
pnpm run seed:mapsThis seeds all 8 historical era maps (Ancient, Medieval, Age of Discovery, WWII, Cold War, Modern, American Civil War, Italian Unification) plus 2 community maps into MongoDB. This step is required — without it, no games can be started as the map data will not exist. You should see output like:
✓ INSERTED: Ancient World (200 AD) — 28 territories · 40 connections · 8 regions
✓ INSERTED: Medieval World (1200 AD) — 29 territories · 41 connections · 8 regions
✓ INSERTED: Age of Discovery (1600 AD) — 34 territories · 51 connections · 8 regions
✓ INSERTED: World War II (1939–1945) — 35 territories · 53 connections · 8 regions
✓ INSERTED: Cold War (1947–1991) — 44 territories · 72 connections · 8 regions
✓ INSERTED: The Modern Day — 43 territories · 94 connections · 8 regions
✓ INSERTED: American Civil War (1861–1865) — 18 territories · 37 connections · 6 regions
✓ INSERTED: Italian Unification (1859–1871) — 14 territories · 23 connections · 6 regions
✅ Done.
Re-running this command is safe — it updates existing maps without resetting play counts or ratings.
# From the backend/ directory
pnpm run devThe backend starts on http://localhost:3001. You should see:
🚀 Eras of Empire backend running on http://localhost:3001
Environment: development
Frontend URL: http://localhost:5173
Open a new terminal:
# From the frontend/ directory
pnpm run devThe frontend starts on http://localhost:5173. Open this URL in your browser.
| Variable | Default | Description |
|---|---|---|
NODE_ENV |
development |
Environment mode |
PORT |
3001 |
Backend server port |
FRONTEND_URL |
http://localhost:5173 |
CORS allowed origin |
POSTGRES_HOST |
localhost |
PostgreSQL host |
POSTGRES_PORT |
5432 |
PostgreSQL port |
POSTGRES_DB |
erasofempire |
Database name |
POSTGRES_USER |
chronouser |
Database user |
POSTGRES_PASSWORD |
chronopass |
Database password |
MONGO_URI |
mongodb://... |
MongoDB connection string |
REDIS_HOST |
localhost |
Redis host |
REDIS_PORT |
6379 |
Redis port |
JWT_ACCESS_SECRET |
CHANGE THIS | Access token signing secret |
JWT_REFRESH_SECRET |
CHANGE THIS | Refresh token signing secret |
JWT_ACCESS_EXPIRES_IN |
1h |
Access token lifetime |
JWT_REFRESH_EXPIRES_IN |
7d |
Refresh token lifetime |
BCRYPT_ROUNDS |
12 |
Password hashing cost |
Twelve sequential migrations build the full schema:
| Table | Purpose |
|---|---|
users |
Player accounts, stats, MMR, XP, gold balance, equipped cosmetics, guest flag |
refresh_tokens |
JWT refresh token store (rotation + revocation) |
games |
Game sessions with settings, status, turn timer, ranked flag, join code, async mode |
game_players |
Player slots within each game, including faction assignment |
game_states |
Serialized game state snapshots per turn (used for reconnection and replays) |
achievements |
Achievement definitions |
user_achievements |
Player achievement unlocks |
friendships |
Friend relationships with direction tracking |
game_invites |
Per-game friend invitations (consumed on join) |
cosmetics |
Cosmetic item catalog (banners, frames, unit skins, dice skins, map themes, markers) |
user_cosmetics |
Player cosmetic ownership |
user_ratings |
Glicko-style mu (skill) / phi (uncertainty) per player per rating type; a sigma (volatility) column is provisioned for forward compatibility but the current rating step focuses on μ and φ updates |
ranked_queue |
Active matchmaking queue entries with era, bucket, and socket ID |
daily_challenges |
One row per date — era, map, deterministic seed |
daily_challenge_entries |
Per-player daily entries (won, turn count, territory count) |
gold_transactions |
Audit log for every gold credit/debit |
async_notifications |
Email/in-app turn notifications for async game mode |
map_reports |
Community map moderation reports |
user_campaigns |
Single-player campaign state (current era index, prestige points) |
campaign_entries |
Per-era result rows tied to a campaign |
| Collection | Purpose |
|---|---|
custommaps |
Full map data (territories, polygons, connections, regions, projection bounds) |
| Key Pattern | Purpose |
|---|---|
leaderboard:{era} |
Sorted set of MMR scores per era |
session:{userId} |
Active session metadata |
Older setups used PostgreSQL database chronoconquest and MongoDB chronoconquest_maps. Defaults now use erasofempire and erasofempire_maps.
- Keep existing data without moving files: In
backend/.env,.env.production, and Docker env, setPOSTGRES_DB=chronoconquestand pointMONGO_URIat.../chronoconquest_maps?...so the app connects to your existing databases. - Move to the new names: Use
pg_dump/pg_restoreintoerasofempire, andmongodump/mongorestoreintoerasofempire_maps, then update env vars. Docker-only: you can also start fresh volumes with the new names (loses old data unless you dump first). - Container renames: Compose
container_namevalues were updated for consistency; data stays in named volumes. After changing env, rundocker compose down/upas needed.
Run both servers simultaneously using the root workspace script:
# From project root
pnpm run devOr run them individually in separate terminals as described in Quick Start.
| Location | Command | Description |
|---|---|---|
| Root | pnpm run dev |
Start both frontend and backend concurrently |
| Root | pnpm run lint |
ESLint backend + frontend (same gate as CI-style checks) |
backend/ |
pnpm run dev |
Start backend with hot reload (tsx watch) |
backend/ |
pnpm run build |
Compile TypeScript to dist/ |
backend/ |
pnpm run migrate |
Run all PostgreSQL migrations |
backend/ |
pnpm run seed |
Seed achievements and cosmetics into PostgreSQL |
backend/ |
pnpm run seed:maps |
Seed all era maps into MongoDB (required for gameplay) |
backend/ |
pnpm run test:backend |
Vitest unit tests (combat resolver, missions, map validation) |
frontend/ |
pnpm run dev |
Start Vite dev server |
frontend/ |
pnpm run build |
Build for production |
frontend/ |
pnpm run preview |
Preview production build |
| Root | pnpm run validate:maps |
Validate all database/maps/*.json connection graphs |
See DEPLOYMENT.md for Docker Compose (nginx + API + databases), environment variables, HTTPS, and smoke testing.
Each player's turn consists of three sequential phases:
-
Reinforcement (Draft) — Place new units on owned territories.
- Base units =
max(3, floor(territories_owned / 3)) - Continent bonus added if player controls all territories in a region
- Card set bonus added if player redeems a valid set of 3 cards
- Base units =
-
Attack — Attack adjacent enemy territories any number of times.
- Attacker rolls up to 3 dice (must leave 1 unit behind)
- Defender rolls up to 2 dice
- Highest die compared: higher wins; defender wins ties
- Capturing a territory earns 1 territory card (once per turn)
-
Fortify — Move units along a connected path of owned territories (once per turn, or more with certain tech unlocks).
| Attacker Dice | Defender Dice | Comparisons |
|---|---|---|
| 3 (≥4 units) | 2 (≥2 units) | 2 pairs compared |
| 2 (3 units) | 2 (≥2 units) | 2 pairs compared |
| 1 (2 units) | 2 (≥2 units) | 1 pair compared |
| Any | 1 (1 unit) | 1 pair compared |
All dice rolls use server-side crypto.randomInt() — clients never influence outcomes.
Cards are earned by capturing at least one territory per turn. Valid sets of 3:
- Three of the same symbol (Infantry, Cavalry, or Artillery)
- One of each symbol
- Any two + one Wild card
| Redemption # | Bonus Units |
|---|---|
| 1st | 4 |
| 2nd | 6 |
| 3rd | 8 |
| 4th | 10 |
| 5th | 12 |
| 6th | 15 |
| 7th+ | +5 each |
Each territory now tracks two new stats:
- Stability (0–100%): Low stability reduces production income, caps unit placement, and risks rebellion. High stability enables population growth and maximizes income.
- Population (1–10): Higher population increases production output (up to 1.5× at max population, 0.5× at minimum). Population grows when stability is high, shrinks when stability is low, and is halved (min 1) when a territory is captured.
Key mechanics:
- Deploy cap (draft phase): Low stability limits how many units you can place on a territory per draft phase, tracked cumulatively for that territory until your next draft (prevents bypassing by many small placements). The cap scales with stability tier, turn number, era, and (when Economy is enabled) your Production Points reserve — strict when unstable, more forgiving in long games. ≥50 stability removes the cap for that territory. AI uses the same rules.
- Rebellion: If stability ≤ 10% and the territory has at least one unit, each turn there is a 25% chance the territory loses a unit. If that loss empties the territory it becomes unowned (no garrison → no defenders → no controller). Empty territories never spawn a rebellion on their own; rebellion requires a garrison to revolt against.
- Stability Recovery: Territories recover stability each turn, with bonuses for garrisoned units and select factions.
- Population Growth: If stability ≥ 50%, population may grow (1-in-4 chance per turn); if < 30%, population may shrink (10% chance per turn).
- Faction Bonuses: One faction per era (e.g., Rome, Byzantine, Ming, UK, USA, Western Bloc, Union, Papal States) gains +3 stability recovery per turn.
- Event Cards: New event cards can increase or decrease stability globally or for a single player.
All stability and population effects are visible in the territory panel. The system is fully server-authoritative and persists in replays and campaign.
Each era includes 4–6 selectable factions. Factions are chosen at game creation and give each player a distinct identity with:
- Passive attack or defense bonuses — extra combat dice or modifiers that stack with tech, buildings, wonders, events, and sea connections (see in-game Bonuses and combat feedback)
- Extra reinforcement income — e.g., Han Dynasty (+2/turn), Union Army (+1/turn)
- Once-per-turn special ability — activated during the appropriate phase
Example factions per era:
| Era | Factions |
|---|---|
| Ancient | Roman Republic, Parthian Empire, Han Dynasty, Maurya Empire, Carthaginian Republic, Germanic Tribes |
| American Civil War | Union Army, Confederate Army |
| Italian Unification | Kingdom of Sardinia, Austrian Empire, Papal States, Kingdom of the Two Sicilies |
| Modern Day | Western Bloc, Eastern Coalition, Rogue State, Emerging Economy, Petrostate |
All shipped historical eras have complete faction definitions (where factions are enabled for that era) with passive bonuses and unique special abilities.
Each era has a 4-tier technology tree. Players accumulate tech points each turn and spend them to research nodes. Trees are era-specific and include nodes that grant:
- +attack or +defense dice
- +reinforcement income per turn
- +tech point income per turn
- Building unlocks (requires economy mode)
- Special ability unlocks
- Prerequisite chains across tiers
Each era also has one unique Wonder structure — a globally-unique building that only one player per game can construct, providing a powerful passive effect (e.g., global defense bonus, halved tech costs, or extended influence range).
When economy mode is enabled, players earn production points and can construct buildings on owned territories:
| Category | Tier I | Tier II | Tier III |
|---|---|---|---|
| Production | Camp (+1 unit/turn) | Barracks (+2 units/turn) | Arsenal (+4 units/turn) |
| Defense | Palisade (+1 def die) | Fortress (+2 def die) | Citadel (+3 def die) |
| Tech | Laboratory (+2 TP/turn) | Research Center (+4 TP/turn) | — |
| Naval (coastal) | Port (+1 fleet/turn) | Naval Base (+2 fleets/turn) | — |
Higher tiers require the previous tier and specific researched tech nodes.
Each era has a dedicated shuffled event card deck. At configurable intervals, the active player draws a card and resolves its server-computed effect. Effect types include:
- Units added / removed — bonus reinforcements or attrition losses
- Territory transfer — a neutral or contested territory changes hands
- Temporary modifiers — attack/defense bonuses lasting N turns
- Gold awards — instant gold for store purchases
- Stability change — new event cards can increase or decrease stability globally or for a single player (e.g., Golden Age, Great Famine)
Cards are era-thematic (e.g., plague in Medieval, nuclear tension in Cold War, diplomatic protest in Modern). All effects are resolved server-side.
Games support multiple simultaneous victory modes configured at creation:
| Mode | Win Condition |
|---|---|
| Domination | Control 100% of all territories |
| Threshold | Control a configured percentage of territories |
| Capital Capture | Capture all other players' capital territories |
| Secret Mission | Complete a privately-assigned secret objective |
When enabled, each player is assigned one deterministic (game-id-seeded) objective at game start:
- Capture two specific territories
- Eliminate a specific opponent
- Control a specific region
- Hold a territory count threshold
Missions are hidden from opponents and evaluated server-side each turn.
The Map Editor (/editor) allows you to create fully custom maps:
- Select the Draw tool (pencil icon) from the left toolbar
- Click on the canvas to place polygon vertices for a territory
- Double-click to close and save the territory (minimum 3 points)
- Click a territory with the Select tool to rename it and assign a region
- Select the Connect tool (chain icon) and click two territories to draw a border connection (land or sea)
- Add Regions in the right panel — regions group territories and provide army bonuses
- Save your map — it is saved privately and can be submitted for moderation to publish publicly
Requirements for a valid map:
- Minimum 6 territories
- Minimum 5 connections
- At least 1 region
- All territories must belong to a region
Published maps appear in the Community Map Hub where players rate them and flag inappropriate content. Globe geometry for custom maps is derived from projection_bounds and geo_polygon in the map document.
Browser (React + PixiJS / react-globe.gl)
│
├── HTTP (REST) ──→ Fastify API ──→ PostgreSQL (users, games, rankings, campaign)
│ ──→ MongoDB (maps)
│ ──→ Redis (cache, leaderboards)
│
└── WebSocket ──→ Socket.io Server ──→ In-Memory Game State
──→ AI Bot Engine (worker + timeout guard)
──→ PostgreSQL (snapshots for reconnection + replay)
Key Design Decisions:
- In-memory game state: Active game states are held in server memory for low-latency real-time updates. State is snapshotted to PostgreSQL at the end of each turn for persistence and reconnection recovery.
- JWT rotation: Refresh tokens are rotated on every use and stored as bcrypt hashes, preventing token theft via database compromise.
- Server-authoritative combat: All dice rolls occur on the server using
crypto.randomInt()— clients never control combat outcomes. Event card effects, tech benefits, and faction abilities are all resolved server-side. - Fog of War filtering: When enabled, the server filters the game state before broadcasting to each player, hiding enemy unit counts in non-adjacent territories.
- AI workers: Bot turns are executed in a timeout-guarded worker (
runAiWithTimeout.ts) so a slow AI search never stalls the game loop. - Globe rendering: Territory polygons must follow GeoJSON RFC 7946 winding rules. Canvas coordinates are converted via
projection_boundsandgeo_polygonfrom the MongoDB map document.
| Page | Route | Description |
|---|---|---|
| Landing | / |
Public marketing page with feature overview |
| Register | /register |
Account creation |
| Login | /login |
Email + password login with JWT refresh cookie |
| Lobby | /lobby |
Browse open games, create a game, configure era / settings |
| Game | /game/:id |
Live game: 2D map or 3D globe, HUD, tech tree, buildings, chat |
| Replay | /game/:id/replay |
Step through all saved turn snapshots at variable playback speed |
| Campaign | /campaign |
Single-player linear era progression; earn prestige points per victory |
| Daily Challenge | /daily |
Date-seeded challenge with daily leaderboard |
| Tutorial | /tutorial |
Interactive tutorial with in-map overlay guidance slides |
| Map Editor | /editor |
Create custom maps with D3 polygon drawing tools |
| Map Hub | /maps |
Browse, rate, and report community maps |
| Friends | /friends |
Add friends, manage pending requests, send game invites |
| Store | /store |
Browse cosmetics catalog; purchase with gold; equip loadout |
| Profile | /profile/:id |
Stats, achievements, match history, equipped cosmetics |
| Privacy Policy | /privacy |
Privacy policy |
Three time buckets: Blitz (2 min/turn), Standard (5 min/turn), and Long (20 min/turn). The queue uses Glicko-style ratings — each player has a skill estimate (μ) and an uncertainty (φ), and they are matched within a threshold that widens over wait time. Ratings update after each ranked game completes; the schema reserves a sigma (volatility) field for a future full Glicko-2 update step.
Games can be created in async mode with per-turn deadlines. When it is a player's turn the system dispatches async_notifications (email channel) so participants can play at their own pace.
Players can start games as guests without registering. Guest accounts are fully functional in-game but are blocked from ranked queues, the store (purchases), and the campaign via the rejectGuest middleware.
Every game optionally has an 8-character join code. Share it with friends to bypass the lobby browser. In-game friend invitations are tracked via game_invites and consumed on join.
| Symptom | Things to check |
|---|---|
| API calls fail or CORS errors | FRONTEND_URL and optional CORS_ORIGINS in backend/.env must include your web origin (e.g. http://localhost:5173). |
401 on /api/auth/refresh |
Refresh cookie SameSite/HTTPS: see REFRESH_COOKIE_SAME_SITE in backend/.env.example. Ensure frontend uses the Vite proxy or matching API URL. |
| Socket disconnects or "Game not found" | Socket auth requires a valid access token in the handshake; URL gameId must match a Postgres game row. Rejoin is sent automatically on reconnect (see GamePage.tsx). |
| Map not rendering | Run pnpm run seed:maps from backend/ so MongoDB has map documents. For custom geometry issues on the globe, see docs/GLOBE_2D_CHECKLIST.md. |
| Globe polygons appear black or corrupt | Avoid extremely low polygonCapCurvatureResolution; verify GeoJSON exterior ring winding and that projection_bounds / geo_polygon are correct in the map document. |
From the repository root:
pnpm run test:backend— Vitest unit tests (combat resolver, secret missions, map connection validation).pnpm run validate:maps— validates alldatabase/maps/*.jsonconnection graphs.
Snapshot and restart behavior for ops: docs/OPERATIONS.md.
- Create a map JSON in
database/maps/and runpnpm run seed:maps - Add faction, tech tree, and wonder definitions in
backend/src/game-engine/eras/<era>.tsand export fromindex.ts - Add an event card deck in
backend/src/game-engine/events/decks/<era>.tsand register it ineventCardManager.ts - Register the era ID in
frontend/src/constants/andfrontend/src/pages/LobbyPage.tsx - (Optional) Add era-specific background music and card artwork in
frontend/src/assets/
Edit backend/src/game-engine/ai/aiBot.ts:
- Add the new level to
DIFFICULTY_CONFIG - Adjust
depth(search depth) andrandomFactor(0.0 = deterministic, 1.0 = random)
The game engine is fully decoupled from the socket layer:
combat/combatResolver.ts— Pure functions, no side effects, fully unit testedstate/gameStateManager.ts— Immutable-style state mutationsai/aiBot.ts— Stateless heuristic evaluation; wrapped byrunAiWithTimeout.tsevents/eventCardManager.ts— Deck drawing and server-side effect applicationvictory/missions.ts— Deterministic seeded mission assignment and evaluationeras/index.ts— Barrel export for era factions, tech trees, and wonders
This project is proprietary. All rights reserved.
Eras of Empire — April 2026