Adds public/boardRender.js: a layout algorithm that derives every space's position purely from shared/board.js (column = longest-path distance from Start, lane = branch offset that fans out at a choice space and re-centers wherever branches rejoin), so it keeps working unmodified as the board data grows — no hand-placed coordinates to maintain. Renders spaces as styled SVG nodes (color/shape by type), connects them with curved path lines, and animates player tokens between positions. Pending choices glow and are clickable directly on the board, in addition to the existing text buttons. Game view widens the card on desktop for the board and scrolls horizontally on narrow viewports. Also repicks the player color palette (server/index.js) to avoid the board's own semantic colors (green/gold/red), after a token nearly disappeared into the same-colored Start space during visual testing. Verified with a headless-browser run (Playwright, off-screen — not a desktop screenshot): two players through lobby -> live join update -> board -> a fork choice, no console errors, reducer tests still green.
Life Journey — Phase 1
An async multiplayer, Game-of-Life-style board game. Phase 0 proved the deployment path (Docker → Nginx Proxy Manager → HTTPS → WebSocket). Phase 1 adds the actual game: a pure shared reducer, SQLite persistence, and rooms with shareable invite links, so a few people can play together across devices and tab closes.
The server is the sole authority over game state. It generates the only
source of randomness (the dice roll) and applies it through the exact same
reducer (shared/game.js) the browser imports — client and server can never
disagree about the rules.
What's here
lifegame/
├── docker-compose.yml
├── Dockerfile
├── package.json / package-lock.json
├── shared/
│ ├── board.js # the Phase 1 board graph + movement helper
│ ├── game.js # pure reducer: reduce(state, action) -> newState
│ └── game.test.js # node --test coverage of the whole rules engine
├── server/
│ ├── index.js # REST + WebSocket, static hosting
│ ├── rooms.js # in-memory room registry, applies/broadcasts actions
│ ├── db.js # SQLite schema + data access (games/players/tokens)
│ └── ids.js # id/token/join-code generation
└── public/
├── index.html # lobby / waiting room / game UI
├── client.js # REST wrappers + NetworkTransport (WebSocket)
└── boardRender.js # SVG board, laid out from board.js graph data
Running locally
npm install
npm test # reducer unit tests — no server needed
npm run dev # starts on :3000, creates data/lifegame.db on first game
Open two browser tabs at http://localhost:3000. Create a game in one tab,
copy the invite link, open it in the other tab, join, and start the game once
both players are in the lobby.
The board (Phase 1 subset)
The full hand-drawn board (assets/game_board.png) has ~150 spaces across two
thematic passes (Career, Education, Gap Year, Relationship/Family,
Investment, High Risk). Phase 1 encodes a small subset with the same shape —
a Career-vs-Education fork, a Relationship-vs-Investment fork, a
High-Risk-vs-Safe fork, converging to Finish — enough to prove the reducer,
persistence, and rooms all work end to end. More spaces can be inserted into
any branch later without touching the reducer or database schema.
Deploying on the homelab
Same as Phase 0 — see homelab-config.md for the full infrastructure
reference. Set PUBLIC_URL in .env (or the compose environment) to your
public domain so invite links generated by the server are shareable rather
than pointing at an internal address:
cd ~/homelab/lifegame
docker compose up -d --build
docker compose logs -f # expect: "Life Journey (Phase 1) listening on :3000"
Data & backups
The lifegame-data volume (mounted at /app/data) holds lifegame.db —
every game, player, and token. Back it up like your other self-hosted data;
losing it loses every in-progress and finished game.
What's next
- Expand
shared/board.jstoward the full sketched board - Richer board rendering (the current UI is a functional list view, not the illustrated board)
- Tighter reconnection/presence handling (who's online right now, not just who's joined)
- Admin page (
ADMIN_PASSWORD, already stubbed in.env.example)