Private
Public Access
1
0
2026-03-31 23:35:08 +02:00

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.

Desktop only — the galaxy map is not playable on phones or narrow screens (< 768 px wide).


Table of contents

  1. Architecture
  2. Prerequisites
  3. Deploy on a new server
  4. Environment variables
  5. Runtime config (game.settings.json)
  6. Local development (without Docker)
  7. UI layout
  8. Game mechanics
  9. API reference
  10. Docker image notes
  11. Database persistence

Architecture

Component Technology
Web server / API Node.js 20 + Express
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)
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.


Prerequisites

Requirement Minimum version
Docker Engine 24+
Docker Compose plugin v2 (docker compose)
Git any recent

No Node.js or PostgreSQL installation is needed on the host — everything runs inside Docker.


Deploy on a new server

1 — Clone the repository

git clone <repo-url> star-wars-wild-space
cd star-wars-wild-space

2 — Create the environment file

cp .env.example .env

Open .env and set every value — see Environment variables for details. At a minimum you must change:

  • JWT_SECRET — generate a strong random string:

    node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"
    

    You may use alternatives for Linux-based distros, like this OpenSSL one:

    openssl rand -hex 48
    

    Or even lower-level tools from Linux/UNIX 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

  • ADMIN_PASSWORD

3 — Review the game config

config/game.settings.json is mounted into the container and hot-reloaded at runtime. Adjust values before first boot if needed — see Runtime config.

4 — Create the data directories

Docker bind-mounts the PostgreSQL data directories from the host. Create them before the first run so that Docker does not create them as root:

mkdir -p data/postgres data/postgres_users

5 — Build and start

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.

6 — Verify the deployment

# Check all three containers are running
docker compose ps

# Tail logs
docker compose logs -f app

# Health check (should return JSON)
curl http://localhost:8080/api/config

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.

Stopping / restarting

docker compose down          # stop (data preserved)
docker compose down -v       # stop and remove named volumes (data lost)
docker compose restart app   # restart only the app container

Environment variables

Never commit secrets. All credentials live in .env (git-ignored). Copy from .env.example before the first run.

Variable Default Description
PORT 8080 HTTP port exposed by the app
JWT_SECRET (required) Secret used to sign JWT tokens — use a long random string in production
POSTGRES_USER game Game-DB username
POSTGRES_PASSWORD (required) Game-DB password
POSTGRES_DB star_wars_grid Game-DB name
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

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).

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.

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).

GET /api/config returns the core fields plus worldSeed, seedPeriodStartsAtUtc, seedPeriodEndsAtUtc, and economy values.


Local development (without Docker)

Requires two running PostgreSQL 16 instances, then:

cp .env.example .env   # fill in all values

npm install

# Set connection strings (PowerShell example)
$env:DATABASE_URL       = "postgres://game:game@localhost:5432/star_wars_grid"
$env:USERS_DATABASE_URL = "postgres://users:users@localhost:5433/star_wars_users"
$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).

Ports (local)

Service Port
App 8080 (or PORT env var)
Game Postgres 5432
Users Postgres 5433

UI layout

Pane Content
Left column Title, team scores, player info, cooldown timer, world seed & period countdown, Refresh button, selection details
Right side Galaxy map — a perfect square anchored to the right edge of the viewport

Game mechanics

Teams (Résistance / Premier Ordre)

  • 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).

Cooldown

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:

effectiveCooldown = clickCooldownSeconds / (1 + elementBonus / 100)

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.

Economy

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.


API reference

All endpoints are relative to the app root.

Public

Method Path Description
GET /api/config Game config plus current world seed and period timestamps. Accepts optional `?team=blue
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.

Auth (no token required)

Method Path Body Response
POST /api/auth/register { username, email, password, team } { token, user }
POST /api/auth/login { username, password } { token, user }

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.

Auth field rules

Field Rules
username 232 characters
password Minimum 6 characters
team "Résistance" or "Premier Ordre"
email Must be unique

Tokens are valid for 7 days. Call GET /api/auth/me to silently renew.


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.

Database persistence

PostgreSQL data lives in bind-mount directories on the host:

Directory Database
./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.

To fully reset the game state (including all user accounts), stop the stack and delete both data directories, then start again:

docker compose down
rm -rf data/postgres data/postgres_users
mkdir -p data/postgres data/postgres_users
docker compose up -d
Description
No description provided
Readme 7.7 MiB
Languages
JavaScript 75.8%
CSS 14.3%
HTML 9.4%
Dockerfile 0.5%