game.test.js: rewritten for the new {type, rolls} action shape and 5-stat
player model. The full-playthrough test now walks the real 212-space board
turn by turn (deterministic dummy rolls — always the low end of each die's
range) until finish, asserting every stat stays numeric and every one of
the 9 real tile types actually gets resolved along the way. board-shape
assertions updated to match (212 spaces, no `choices` populated yet).
tileEffects.test.js (new): unit tests per tile type in isolation, including
exact deterministic assertions against real placeholder-table content (roll
a known value, assert the exact banded effect) and a multi-table
roll_table_ref cross-check computed independently from the raw table data —
no seeded-rng machinery anywhere, same explicit-action-value pattern the
reducer tests already used. Also a sweep resolving every real tile across
the full range of its die to catch any type/die combination that throws.
package.json: test script now runs shared/*.test.js instead of naming one
file.
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
shared/board.js (~107 spaces) is transcribed from the hand-drawn sketch at
assets/game_board.png, organized into three fork points with the same
shape the sketch uses: Career / Education / Gap Year at the start,
Relationship / Investment after a shared "quarter-life crisis" chain,
then High Risk / Safe before a long shared retirement tail to Finish.
The sketch reuses several space names across its zones (Start A Business,
Family Reunion, Market Crash, ...) — that's kept as intentional recurring
flavor rather than deduplicated. More spaces can be inserted into any
branch's chain([...]) array in board.js without touching the reducer,
the SVG renderer, or the database schema — none of them know or care how
many spaces exist.
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)