fix: Change in README.md
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Star Wars – Wild Space
|
||||
|
||||
Exploitable ring only (outer Ø100, inner Ø60). Planet positions are deterministic from the **server world seed**; **stats stay hidden** until you click a tile. Reveals are **persisted in PostgreSQL**.
|
||||
A persistent, real-time team strategy game set in the Star Wars universe. Two teams (`blue` / `red`) compete to explore and capture planets in the Wild Space ring. Planet positions are **deterministic from the server world seed** — the same coordinates always yield the same planet, on every epoch. Cell stats are hidden until a team reveals them; visibility is **per-team** and **private**. All game state is persisted in PostgreSQL.
|
||||
|
||||
> **Desktop only** — the galaxy map is not playable on phones or narrow screens (< 768 px wide).
|
||||
|
||||
@@ -16,9 +16,14 @@ Exploitable ring only (outer Ø100, inner Ø60). Planet positions are determinis
|
||||
6. [Local development (without Docker)](#local-development-without-docker)
|
||||
7. [UI layout](#ui-layout)
|
||||
8. [Game mechanics](#game-mechanics)
|
||||
9. [API reference](#api-reference)
|
||||
10. [Docker image notes](#docker-image-notes)
|
||||
11. [Database persistence](#database-persistence)
|
||||
9. [WebSocket real-time layer](#websocket-real-time-layer)
|
||||
10. [Deterministic planet generation](#deterministic-planet-generation)
|
||||
11. [Economy engine](#economy-engine)
|
||||
12. [Military system](#military-system)
|
||||
13. [API reference](#api-reference)
|
||||
14. [Database schema](#database-schema)
|
||||
15. [Docker image notes](#docker-image-notes)
|
||||
16. [Database persistence](#database-persistence)
|
||||
|
||||
---
|
||||
|
||||
@@ -27,14 +32,30 @@ Exploitable ring only (outer Ø100, inner Ø60). Planet positions are determinis
|
||||
| Component | Technology |
|
||||
|-----------|-----------|
|
||||
| Web server / API | Node.js 20 + Express |
|
||||
| Real-time push | WebSocket (`ws` library) — upgrade on the same HTTP port |
|
||||
| Game database | PostgreSQL 16 (`star_wars_grid`) |
|
||||
| Users database | PostgreSQL 16 (`star_wars_users`) |
|
||||
| Auth | JWT (7-day tokens, signed with `JWT_SECRET`) |
|
||||
| Config hot-reload | File mtime polling (`game.settings.json`) |
|
||||
| Economy engine | Server-side tick loop (every 5 s) |
|
||||
| Auth | JWT (7-day tokens, signed with `JWT_SECRET`, bcrypt password hashing with cost 12) |
|
||||
| Config hot-reload | File `mtime` polling (`game.settings.json`) — no restart needed |
|
||||
| Economy engine | Server-side tick loop (every 5 s), runs headlessly |
|
||||
| Container runtime | Docker / Docker Compose |
|
||||
|
||||
The app runs as a single Node process that serves both the static frontend (`public/`) and the REST API (`/api/*`). Two separate PostgreSQL instances hold game state and user accounts respectively.
|
||||
The app runs as a **single Node.js process** that serves the static frontend (`public/`), the REST API (`/api/*`), and the WebSocket endpoint (`/ws`), all on the same port. Two separate PostgreSQL instances isolate game state from user accounts.
|
||||
|
||||
```
|
||||
Browser ──HTTP──► Express (/api/*, static)
|
||||
──WS───► WebSocket hub (/ws)
|
||||
│
|
||||
┌───────────┼───────────────┐
|
||||
▼ ▼ ▼
|
||||
econTick configPoll route handlers
|
||||
(5 s) (variable) (reveal/capture/attack)
|
||||
│ │ │
|
||||
└───────────┴───────────────┘
|
||||
│ broadcast()
|
||||
▼
|
||||
Connected clients
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -72,16 +93,11 @@ Open `.env` and set every value — see [Environment variables](#environment-var
|
||||
node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"
|
||||
```
|
||||
|
||||
You may use alternatives for Linux-based distros, like this OpenSSL one:
|
||||
Alternatives for Linux-based distros:
|
||||
```bash
|
||||
openssl rand -hex 48
|
||||
```
|
||||
|
||||
Or even lower-level tools from Linux/UNIX entropy pool:
|
||||
```bash
|
||||
# or from the entropy pool:
|
||||
head -c 48 /dev/urandom | xxd -p -c 48
|
||||
# or
|
||||
head -c 48 /dev/urandom | hexdump -e '48/1 "%02x"'
|
||||
```
|
||||
- `POSTGRES_PASSWORD`
|
||||
- `POSTGRES_USERS_PASSWORD`
|
||||
@@ -104,7 +120,7 @@ mkdir -p data/postgres data/postgres_users
|
||||
docker compose up --build -d
|
||||
```
|
||||
|
||||
The `app` service waits for both databases to pass their health checks before starting (pg_isready polling every 3 s, up to 15 retries). On first boot the server automatically creates all required tables.
|
||||
The `app` service waits for both databases to pass their health checks before starting (`pg_isready` polling every 3 s, up to 15 retries). On first boot the server automatically creates all required tables via idempotent `CREATE TABLE IF NOT EXISTS` and `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` migrations.
|
||||
|
||||
### 6 — Verify the deployment
|
||||
|
||||
@@ -123,7 +139,7 @@ Open `http://<server-ip>:8080` in a browser.
|
||||
|
||||
### 7 — (Optional) Reverse proxy
|
||||
|
||||
To expose the app on port 80/443, put Nginx or Caddy in front and proxy to `http://localhost:8080`. Set `CORS_ORIGIN` in `.env` to your public domain.
|
||||
To expose the app on port 80/443, put Nginx or Caddy in front and proxy to `http://localhost:8080`. WebSocket (`/ws`) proxying requires setting appropriate `Upgrade` and `Connection` headers. Set `CORS_ORIGIN` in `.env` to your public domain.
|
||||
|
||||
### Stopping / restarting
|
||||
|
||||
@@ -149,30 +165,39 @@ docker compose restart app # restart only the app container
|
||||
| `POSTGRES_USERS_USER` | `users` | Users-DB username |
|
||||
| `POSTGRES_USERS_PASSWORD` | *(required)* | Users-DB password |
|
||||
| `POSTGRES_USERS_DB` | `star_wars_users` | Users-DB name |
|
||||
| `CORS_ORIGIN` | `*` | Value of the `Access-Control-Allow-Origin` header — set to your public domain in production |
|
||||
| `CORS_ORIGIN` | `*` | `Access-Control-Allow-Origin` header — set to your public domain in production |
|
||||
| `CONFIG_FILE_PATH` | `config/game.settings.json` | Override path to the config file (set by Docker Compose) |
|
||||
|
||||
---
|
||||
|
||||
## Runtime config (`game.settings.json`)
|
||||
|
||||
Edit **`config/game.settings.json`** on the host (bind-mounted into the container at `/app/config/game.settings.json`). The server checks the file's **mtime** on a schedule; when it changes the new values are applied without a restart, within at most `configReloadIntervalSeconds` seconds (minimum 5 s).
|
||||
Edit **`config/game.settings.json`** on the host (bind-mounted into the container at `/app/config/game.settings.json`). The server checks the file's **mtime** on a schedule and applies new values without a restart; the change propagates to all connected clients via a `config-updated` WebSocket broadcast within at most `configReloadIntervalSeconds` seconds.
|
||||
|
||||
### Core fields
|
||||
|
||||
| Field | Default | Meaning |
|
||||
|-------|---------|---------|
|
||||
| `clickCooldownSeconds` | `10` | Per-user cooldown in seconds between new tile reveals. `0` disables it. |
|
||||
| `databaseWipeoutIntervalSeconds` | `21600` | World period length in seconds (6 h). The world seed is `swg-<slot>` where `slot = floor(UTC unix seconds / this value)`. When the slot advances, `grid_cells` is truncated and a new era begins. Minimum `60`. |
|
||||
| `configReloadIntervalSeconds` | `30` | How often the server polls the config file; also used by the client to poll `/api/config`. Minimum `5`. |
|
||||
| `dailyActionQuota` | `100` | Per-user action budget, refilled on the schedule below. Each new cell reveal costs 1 action. |
|
||||
| `teamActionQuota` | `100` | Shared team action pool, used for planet captures. Refilled on the same schedule. |
|
||||
| `actionsResetIntervalHours` | `24` | How often both quotas reset; aligned to UTC boundaries. Must be a divisor of 24: `1, 2, 3, 4, 6, 8, 12, 24`. |
|
||||
| `databaseWipeoutIntervalSeconds` | `604800` | World period length in seconds (default 7 d). The world seed is `swg-<slot>` where `slot = floor(UTC unix seconds / this value)`. When the slot advances, `grid_cells` is truncated and a new era begins. Minimum `60`. |
|
||||
| `configReloadIntervalSeconds` | `30` | How often the server polls the config file. Minimum `5`. |
|
||||
|
||||
### Economy fields
|
||||
|
||||
| Field | Meaning |
|
||||
|-------|---------|
|
||||
| `elementWorth` | Per-element multipliers (`common`, `food`, `petrol`, `goods`, `industry`, `medic`, `money`, `science`). |
|
||||
| `resourceWorth.common` / `resourceWorth.rare` | Base income per resource type for common and rare planet tiers. Drives the server-side economy tick (every 5 s). |
|
||||
| `elementWorth` | Per-element bonus multipliers (`common`, `food`, `petrol`, `goods`, `industry`, `medic`, `money`, `science`). The sum of `(productionShare% / 100) × worth` across all owned planets gives the team's element bonus (%). This bonus scales the effective `dailyActionQuota`. |
|
||||
| `resourceWorth.common` / `resourceWorth.rare` | Credit-per-second weight per resource type for common and rare planet tiers. Drives income in the server-side economy tick. |
|
||||
|
||||
`GET /api/config` returns the core fields plus **`worldSeed`**, **`seedPeriodStartsAtUtc`**, **`seedPeriodEndsAtUtc`**, and economy values.
|
||||
### Military fields
|
||||
|
||||
| Field | Meaning |
|
||||
|-------|---------|
|
||||
| `militaryPower.humans` / `.near` / `.aliens` | Percentage of a planet's population (in billions) that contributes to the team's military power. E.g. a planet with 5 billion humans and `humans: 10` contributes 0.5 billion military. |
|
||||
|
||||
`GET /api/config` returns the current values of all fields above plus **`worldSeed`**, **`seedPeriodStartsAtUtc`**, **`seedPeriodEndsAtUtc`**, and (if called with a valid Bearer token + `?team=`) **`actionsRemaining`** and **`teamActionsRemaining`**.
|
||||
|
||||
---
|
||||
|
||||
@@ -193,13 +218,13 @@ $env:JWT_SECRET = "dev_secret_change_me"
|
||||
npm start
|
||||
```
|
||||
|
||||
The server creates tables automatically on first run. Ensure `config/game.settings.json` exists (the file is included in the repository).
|
||||
The server creates all tables automatically on first run. Ensure `config/game.settings.json` exists (included in the repository).
|
||||
|
||||
### Ports (local)
|
||||
|
||||
| Service | Port |
|
||||
|---------|------|
|
||||
| App | `8080` (or `PORT` env var) |
|
||||
| App (HTTP + WS) | `8080` (or `PORT` env var) |
|
||||
| Game Postgres | `5432` |
|
||||
| Users Postgres | `5433` |
|
||||
|
||||
@@ -209,88 +234,271 @@ The server creates tables automatically on first run. Ensure `config/game.settin
|
||||
|
||||
| Pane | Content |
|
||||
|------|---------|
|
||||
| **Left column** | Title, team scores, player info, cooldown timer, world seed & period countdown, Refresh button, selection details |
|
||||
| **Left column** | Title, team tile & econ scores, player info, action quotas, world seed & period countdown, active players, military power, victory points, selection details |
|
||||
| **Right side** | Galaxy map — a perfect square anchored to the right edge of the viewport |
|
||||
|
||||
---
|
||||
|
||||
## Game mechanics
|
||||
|
||||
### Teams (Résistance / Premier Ordre)
|
||||
### Teams
|
||||
|
||||
- Users register with a team (`Résistance` or `Premier Ordre`) that is locked to their account and embedded in their JWT.
|
||||
- **Your** discovered tiles use your team colour. Unclaimed ring tiles use the neutral idle tint.
|
||||
- Tiles discovered by the **other** team appear grey, show no planet, and are not clickable. The first reveal owns the tile (`discovered_by`).
|
||||
Users register as either **`blue`** or **`red`**. The team is locked to the account and embedded in the JWT so the server always trusts the token, never the client.
|
||||
|
||||
### Cooldown
|
||||
### Per-team cell visibility
|
||||
|
||||
After revealing a **new** tile, a per-user cooldown starts. During cooldown you cannot reveal additional unseen tiles, but you can still click tiles your team has already discovered to view their stats. The effective cooldown is reduced by the team's element bonus:
|
||||
Each team has its own **private** visibility layer (`team_cell_visibility`). Revealing a cell marks it as visible only for your team: you see the planet data, the other team does not. Both teams can independently scout the same tile. Ownership (`discovered_by`) is entirely separate from visibility.
|
||||
|
||||
```
|
||||
effectiveCooldown = clickCooldownSeconds / (1 + elementBonus / 100)
|
||||
```
|
||||
### Action quotas
|
||||
|
||||
Two quota systems gate how fast teams can act:
|
||||
|
||||
| Quota | Scope | Refills |
|
||||
|-------|-------|---------|
|
||||
| `dailyActionQuota` | Per user | Every `actionsResetIntervalHours` UTC |
|
||||
| `teamActionQuota` | Shared per team | Same schedule |
|
||||
|
||||
- Revealing a **new** (previously unseen by your team) cell costs **1 user action**.
|
||||
- Re-clicking a cell your team has already revealed is free (no action consumed).
|
||||
- The team's **element bonus** boosts the effective user quota: `floor(dailyActionQuota × (1 + elementBonus / 100))`.
|
||||
- Military power can increase the team quota: `teamActionQuota + floor(max(0, netMilitaryPower) / 10_000)` extra actions.
|
||||
|
||||
### Reveal flow
|
||||
|
||||
`POST /api/cell/reveal`:
|
||||
1. Checks the seed is current (`410` if stale).
|
||||
2. If already visible for this team → free re-view, quota untouched, returns current cell data.
|
||||
3. If new → deduct 1 user action (`429` if exhausted), mark `team_cell_visibility`, insert into `grid_cells` (deterministically computed from seed+coords), broadcast `cell-updated` to your team over WebSocket.
|
||||
|
||||
### Planet capture
|
||||
|
||||
`POST /api/cell/capture` transfers ownership (`discovered_by`) of a planet to your team:
|
||||
|
||||
- Cost in **team actions**: `max(1, ceil(population_billions / 10))`.
|
||||
- If the tile is currently **enemy-controlled**, cost is **doubled**.
|
||||
- A successful capture broadcasts `cell-updated` to your team, and to the opposing team **only if they had already revealed that cell**.
|
||||
- A `team-quota-updated` broadcast is sent to your own team after deduction.
|
||||
|
||||
### World seed & wipe
|
||||
|
||||
The world seed changes automatically when the UTC period slot advances. On a seed change the server truncates `grid_cells` and starts a fresh era. Data in `users`, cooldown tables, econ scores, and victory points is kept across wipes.
|
||||
The world seed is `swg-<slot>` where `slot = floor(utcUnixSeconds / databaseWipeoutIntervalSeconds)`. When the slot advances:
|
||||
|
||||
### Economy
|
||||
1. The team with the highest economic score for the expiring epoch receives a **victory point**.
|
||||
2. `grid_cells` and all per-epoch tables (`team_cell_visibility`, cooldowns, econ scores, element bonus, military deductions, attack log) are truncated or cleaned.
|
||||
3. Both team and user quotas reset to server defaults.
|
||||
4. A `seed-changed` WebSocket broadcast triggers all clients to resync.
|
||||
|
||||
A server-side tick runs every 5 seconds. It reads the current grid from the database, computes income and element bonus for each team from their discovered planets, and persists the deltas. No browser needs to be connected for the economy to progress.
|
||||
User accounts, victory points table, and the `db_metadata` row survive all wipes.
|
||||
|
||||
---
|
||||
|
||||
## WebSocket real-time layer
|
||||
|
||||
The WebSocket server is co-hosted with Express on the same HTTP port, upgraded at path `/ws` (`ws://host/ws` or `wss://host/ws` over TLS). There is no separate port or process.
|
||||
|
||||
### Connection lifecycle
|
||||
|
||||
```
|
||||
Client connects
|
||||
→ Server sends: { type: "welcome", authenticated: false, team: null, timestamp: … }
|
||||
|
||||
Client sends: { type: "auth", token: "<JWT>" }
|
||||
→ Server sends: { type: "auth-state", authenticated: true|false, team: "blue"|"red"|null, timestamp: … }
|
||||
|
||||
(ongoing)
|
||||
← Server sends periodic snapshots, cell updates, etc.
|
||||
|
||||
Heartbeat: server pings every 30 s; clients that don't pong are terminated and removed.
|
||||
```
|
||||
|
||||
### Server → client message types
|
||||
|
||||
| Type | Trigger | Payload highlights |
|
||||
|------|---------|-------------------|
|
||||
| `welcome` | On connect | `{ authenticated: false, team: null }` |
|
||||
| `auth-state` | After client sends `auth` | `{ authenticated, team }` |
|
||||
| `snapshot` | Every economy tick (5 s) | `{ worldSeed, scores, elementBonus, militaryDeductions, activePlayers, playerNames, victoryPoints, incomePerSecond, militaryPowerGross }` |
|
||||
| `cell-updated` | Cell revealed / captured / attacked | `{ worldSeed, cell: { x, y, exploitable, hasPlanet, planet, discoveredBy, capturedBy } }` — **targeted to team(s)** |
|
||||
| `team-quota-updated` | After a capture | `{ team, actionsRemaining }` — **targeted to the acting team** |
|
||||
| `military-deductions-updated` | After a military attack | `{ worldSeed, deductions: { blue, red } }` — **broadcast to all** |
|
||||
| `config-updated` | Config file changed | `{ worldSeed, config: { … } }` — broadcast to all |
|
||||
| `seed-changed` | Epoch rolls over | `{ worldSeed }` — broadcast to all; clients resync from REST API |
|
||||
|
||||
### Client reconnection & fallback
|
||||
|
||||
The frontend (`public/src/realtime.js`) implements **exponential back-off reconnection** (1 s → 2 s → 4 s … capped at 10 s). While disconnected, it automatically falls back to HTTP polling every `configReloadIntervalSeconds` for config/scores and every 5 s for economy data. Polling stops the moment a WebSocket connection is re-established (`onOpen` callback).
|
||||
|
||||
The `snapshot` message carries the same data as calling several REST endpoints in parallel, making it the primary update path when connected.
|
||||
|
||||
---
|
||||
|
||||
## Deterministic planet generation
|
||||
|
||||
Planet positions and stats are computed **purely from the world seed and coordinates** — they are never stored until a team reveals a cell. The same (seed, x, y) triple always produces the same planet.
|
||||
|
||||
### RNG pipeline (`public/src/rng.js`, shared server-side)
|
||||
|
||||
```
|
||||
seedString ──FNV-1a 32──► seedU32
|
||||
(x, y, seedU32) ──hash2u32──► presenceU32 → planet present if presenceU32 / 2³² < 0.10 (10% chance)
|
||||
(x, y, seedU32 ^ 0xa5a5a5a5) ──hash2u32──► statsU32 ──mulberry32──► per-planet PRNG stream
|
||||
```
|
||||
|
||||
| Function | Algorithm | Purpose |
|
||||
|----------|-----------|---------|
|
||||
| `fnv1a32(str)` | FNV-1a 32-bit | Converts the seed string to a stable uint32 |
|
||||
| `hash2u32(a, b, seed)` | Integer avalanche mix | Maps (x, y) to a deterministic uint32 for planet presence and stats seeding |
|
||||
| `mulberry32(seedU32)` | Mulberry32 PRNG | Fast, high-quality stateless PRNG stream for planet attribute generation |
|
||||
|
||||
The `hash2u32` / `mulberry32` functions are in `public/src/rng.js` and are imported directly by `server/helpers/cell.js`, ensuring client and server always agree on cell contents without any network round-trip.
|
||||
|
||||
### Galaxy ring geometry
|
||||
|
||||
The exploitable zone is an annular ring centered on the 100×100 grid:
|
||||
|
||||
- **Outer radius**: 50 cells (Ø100)
|
||||
- **Inner radius**: 30 cells (Ø60)
|
||||
- Planet density: ~10% of exploitable ring cells
|
||||
|
||||
---
|
||||
|
||||
## Economy engine
|
||||
|
||||
`server/econTick.js` runs a `setInterval` every **5 seconds**, independent of connected clients:
|
||||
|
||||
1. Calls `ensureSeedEpoch()` — detects and handles epoch rollovers.
|
||||
2. Queries all `grid_cells` rows for the current seed.
|
||||
3. Computes **income per second** for each team: iterates owned planets, sums `(resourceShare% / 100) × resourceWorth × populationFactor`.
|
||||
4. **Population factor** applies a small-population efficiency boost: `(billions / 10) × boost` where boost is `10× (< 10 B)`, `5× (< 100 B)`, `2.5× (< 1 000 B)`, `1× (≥ 1 000 B)`.
|
||||
5. Adds `income × 5` credits to each team's `team_econ_scores`.
|
||||
6. **Overwrites** `team_element_bonus` with the current bonus computed from production percentages across all owned planets.
|
||||
7. Builds a **realtime snapshot** (via `buildRealtimeSnapshot`) containing econ scores, element bonus, military state, active player counts & names, victory points, and income/s — then broadcasts it as a `snapshot` message to all connected WebSocket clients.
|
||||
|
||||
---
|
||||
|
||||
## Military system
|
||||
|
||||
Military power represents a team's capacity for aggressive tile takeovers.
|
||||
|
||||
### Power computation
|
||||
|
||||
For each team-owned planet:
|
||||
`militaryContribution = population.billions × militaryPower[species] / 100`
|
||||
|
||||
Species → config key mapping: `Humains` → `humans`, `Presque'humains` → `near`, `Aliens` → `aliens`.
|
||||
|
||||
The gross military power is shown in the UI. The **net** military power accounts for deductions from attacks spent: `netPower = grossPower − teamMilitaryDeductions`.
|
||||
|
||||
### Military attack (`POST /api/military/attack`)
|
||||
|
||||
- Target must be an enemy-owned tile with a planet.
|
||||
- Costs exactly **1.0 billion** military power (added to `team_military_deductions`).
|
||||
- Transfers tile ownership to the attacker immediately.
|
||||
- Records the event in `cell_attack_log`.
|
||||
- Broadcasts `military-deductions-updated` to **all** clients and `cell-updated` to the relevant team(s).
|
||||
|
||||
### Team action quota bonus
|
||||
|
||||
Net military power also feeds into capture capacity: every 10 000 units of net military above 0 grants 1 extra team action on quota refresh.
|
||||
|
||||
---
|
||||
|
||||
## API reference
|
||||
|
||||
All endpoints are relative to the app root.
|
||||
All endpoints are relative to the app root. All responses are JSON. Cache-Control is `no-store` on every route.
|
||||
|
||||
### Public
|
||||
### Public (no auth required)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `GET` | `/api/config` | Game config plus current world seed and period timestamps. Accepts optional `?team=blue|red` and Bearer token to compute per-user cooldown remaining. |
|
||||
| `GET` | `/api/grid/:seed` | All revealed cells for `seed`. Returns `410` if `seed` is not the current world seed. |
|
||||
| `GET` | `/api/scores` | `{ blue: N, red: N }` — tile counts for the current period. |
|
||||
| `GET` | `/api/econ-scores` | Economic score totals for the current period. |
|
||||
| `GET` | `/api/element-bonus` | Current element bonus percentages per team. |
|
||||
| `GET` | `/api/victory-points` | All awarded victory points across eras. |
|
||||
| `GET` | `/api/db-info` | Database creation timestamp. |
|
||||
| `GET` | `/api/config` | Game config + world seed + period timestamps. Accepts `?team=blue\|red` and a Bearer token to also return per-user and team `actionsRemaining`. |
|
||||
| `GET` | `/api/grid/:seed` | All cells visible to the requesting team for `seed` (auth-aware). Returns `410` if `seed` ≠ current world seed. Requires a valid Bearer token; returns empty grid if token is absent or invalid. |
|
||||
| `GET` | `/api/scores` | `{ blue: N, red: N }` — captured tile counts for the current period. |
|
||||
| `GET` | `/api/econ-scores` | `{ blue: N, red: N }` — cumulative economic scores for the current period. |
|
||||
| `GET` | `/api/element-bonus` | `{ blue: N, red: N }` — element bonus percentages. |
|
||||
| `GET` | `/api/victory-points` | Array of all awarded victory points across all eras. |
|
||||
| `GET` | `/api/db-info` | `{ createdAt }` — database creation timestamp. |
|
||||
| `GET` | `/api/active-players` | `{ blue: N, red: N }` — players active in the current epoch. |
|
||||
| `GET` | `/api/active-players/names` | `{ blue: [...], red: [...] }` — usernames of active players. |
|
||||
| `GET` | `/api/team-quota?team=blue\|red` | `{ team, actionsRemaining }` — current shared team action pool. |
|
||||
| `GET` | `/api/military-deductions` | `{ blue: N, red: N }` — cumulative military power spent this epoch. |
|
||||
| `GET` | `/api/cell/attacks?x=&y=` | `{ x, y, attackCount }` — number of attacks on a specific cell this epoch. |
|
||||
|
||||
### Auth (no token required)
|
||||
|
||||
| Method | Path | Body | Response |
|
||||
|--------|------|------|----------|
|
||||
| `POST` | `/api/auth/register` | `{ username, email, password, team }` | `{ token, user }` |
|
||||
| `POST` | `/api/auth/register` | `{ username, password, team }` | `{ token, user }` |
|
||||
| `POST` | `/api/auth/login` | `{ username, password }` | `{ token, user }` |
|
||||
| `GET` | `/api/auth/player-counts` | — | `{ blue: N, red: N }` — total registered players per team |
|
||||
|
||||
### Auth (Bearer token required)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `GET` | `/api/auth/me` | Returns refreshed `{ token, user }`. |
|
||||
| `POST` | `/api/cell/reveal` | Body `{ seed, x, y }`. Team is taken from the JWT. First reveal wins; **`409`** if the other team already owns the tile; **`410`** if seed is stale; **`429`** if on cooldown. |
|
||||
| `POST` | `/api/cell/reveal` | Body `{ seed, x, y }`. Marks cell as visible for your team. Costs 1 user action if not yet seen. Returns `{ x, y, exploitable, hasPlanet, planet, discoveredBy, capturedBy }`. `429` if quota exhausted; `410` if seed stale. |
|
||||
| `POST` | `/api/cell/capture` | Body `{ seed, x, y }`. Transfers planet ownership. Costs team actions based on population. `409` if already owned; `429` if quota exhausted; `410` if seed stale. |
|
||||
| `POST` | `/api/military/attack` | Body `{ seed, x, y }`. Attacks an enemy tile. Costs 1 billion military power. `409` if targeting own/neutral tile. |
|
||||
|
||||
### Auth field rules
|
||||
### Registration field rules
|
||||
|
||||
| Field | Rules |
|
||||
|-------|-------|
|
||||
| `username` | 2–32 characters |
|
||||
| `password` | Minimum 6 characters |
|
||||
| `team` | `"Résistance"` or `"Premier Ordre"` |
|
||||
| `email` | Must be unique |
|
||||
| `team` | `"blue"` or `"red"` |
|
||||
|
||||
Tokens are valid for **7 days**. Call `GET /api/auth/me` to silently renew.
|
||||
|
||||
### Error codes
|
||||
|
||||
| HTTP | `error` value | Meaning |
|
||||
|------|---------------|---------|
|
||||
| 400 | `invalid_body` / `missing_fields` | Malformed request |
|
||||
| 401 | `unauthorized` / `invalid_token` | Missing or invalid JWT |
|
||||
| 404 | `cell_not_found` | Cell does not exist in the DB yet |
|
||||
| 409 | `already_owned` / `no_planet` / `cannot_attack_own_or_neutral_tile` | Business logic conflict |
|
||||
| 410 | `seed_expired` | Client is using a stale world seed — resync from `/api/config` |
|
||||
| 429 | `quota_exhausted` / `team_quota_exhausted` | Action budget depleted |
|
||||
| 500 | `database_error` / `config_error` | Server-side failure |
|
||||
|
||||
---
|
||||
|
||||
## Database schema
|
||||
|
||||
### `star_wars_grid` (game DB)
|
||||
|
||||
| Table | Key columns | Notes |
|
||||
|-------|-------------|-------|
|
||||
| `grid_cells` | `(world_seed, x, y)` UNIQUE | Cells inserted on first reveal. `discovered_by` is `NULL` (neutral), `'blue'`, or `'red'`. `captured_by` stores the capturer's username. |
|
||||
| `team_cell_visibility` | `(world_seed, team, x, y)` PK | Per-team visibility — independent of ownership. Truncated on epoch change. |
|
||||
| `team_econ_scores` | `(world_seed, team)` PK | Cumulative credits per epoch. |
|
||||
| `team_element_bonus` | `(world_seed, team)` PK | Overwritten every 5 s tick. |
|
||||
| `team_military_deductions` | `(world_seed, team)` PK | Cumulative military power spent. |
|
||||
| `team_action_quota` | `team` PK | Shared team action pool, persists across epochs. |
|
||||
| `cell_attack_log` | `id` PK, index on `(world_seed, x, y)` | One row per military attack event. |
|
||||
| `victory_points` | `id` PK, index on `team` | One row per awarded VP; survives all wipes. |
|
||||
| `db_metadata` | `id` PK | Single row with DB creation timestamp. |
|
||||
|
||||
### `star_wars_users` (users DB)
|
||||
|
||||
| Table | Key columns | Notes |
|
||||
|-------|-------------|-------|
|
||||
| `users` | `id` PK, `username` UNIQUE | `team` constrained to `'blue'` or `'red'`; `password_hash` is bcrypt cost-12. |
|
||||
| `user_action_quota` | `user_id` PK → `users.id` | Per-user daily action budget with `quota_reset_at` timestamp. |
|
||||
|
||||
---
|
||||
|
||||
## Docker image notes
|
||||
|
||||
- Build uses a **two-stage** Dockerfile:
|
||||
- Stage 1 (`node:20-alpine`): installs production dependencies via `npm ci`.
|
||||
- Stage 2 (`gcr.io/distroless/nodejs20-debian12:nonroot`): minimal runtime with no shell or package manager; runs as UID 65532.
|
||||
- `NODE_ENV=production` is set inside the image.
|
||||
- Built-in health check: calls `server/healthcheck.js` via the Node binary every 15 s (no `wget`/`curl` — distroless has none).
|
||||
- Secrets are **never baked into the image** — always passed via `.env` / Compose environment.
|
||||
- Stage 1 (`node:20-alpine`): installs production dependencies via `npm ci --omit=dev`.
|
||||
- Stage 2 (`gcr.io/distroless/nodejs20-debian12:nonroot`): minimal runtime with no shell or package manager; runs as UID 65532 (non-root).
|
||||
- `NODE_ENV=production` is baked into the image.
|
||||
- Built-in health check: calls `server/healthcheck.js` via the Node binary every 15 s (`GET /api/config`, exits 0 on HTTP 200). No `wget`/`curl` needed — distroless has none.
|
||||
- Secrets are **never baked into the image** — always injected at runtime via `.env` / Compose environment.
|
||||
- `config/` is a **volume mount**, not baked in, enabling hot-reload without rebuilding the image.
|
||||
|
||||
---
|
||||
|
||||
@@ -303,7 +511,7 @@ PostgreSQL data lives in bind-mount directories on the host:
|
||||
| `./data/postgres` | Game DB (`star_wars_grid`) |
|
||||
| `./data/postgres_users` | Users DB (`star_wars_users`) |
|
||||
|
||||
A **world wipe** only truncates the `grid_cells` table when the UTC period slot changes; it does not touch the host data directories. Users, cooldowns, econ scores, and victory points survive wipes.
|
||||
A **world wipe** only cleans epoch-scoped tables when the UTC period slot changes; it does not touch the host data directories. User accounts, `user_action_quota`, `team_action_quota`, `victory_points`, and `db_metadata` survive all wipes.
|
||||
|
||||
To fully reset the game state (including all user accounts), stop the stack and delete both data directories, then start again:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user