Kevin db82b531e0 Update client rendering for the real board and 5 stats
boardRender.js: TYPE_STYLE now covers the 9 real tile types instead of the
old 5 (start/finish/choice/money/event) — payday/cash_bonus/action_space/
dice_space/roll_table_ref/inline_table each get their own icon, color-coded
by mechanic (green=income, blue=rolls-against-a-real-table, dashed
light-blue=rolls-against-a-synthesized-placeholder, red=decision point).
The old per-space cash badge is gone (tiles no longer carry a static cash
amount — effects resolve dynamically via tables) and replaced with a die
badge ("D20") wherever a tile involves a roll. Also fixes a real bug the
headless verification run caught: computeLayout assumed every 'choice'-type
space has a populated `choices` array, which none do yet post-rebuild
(`for (const t of space.choices)` on undefined) — now falls back to `next`
like everything else, and the lane fan-out is keyed off "does this space
have more than one outgoing edge" generically rather than off `type`, so it
keeps working whenever choices/stop tiles do get real branches later.

index.html: player list shows all five stats (cash/love/education/wealth/
age) instead of just cash; the log feed shows each tile's actual resolution
description (already human-readable from tileEffects.js) with a small ⚠️
marker on placeholder/invented resolutions, instead of a bare cash delta.
computeLayout's colsPerRow bumped to 20 for the much larger (212-space)
board.
2026-07-28 10:07:32 -07:00
2026-07-21 20:48:28 +00:00
2026-07-21 14:16:59 -07:00
2026-07-21 16:17:12 -07:00

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.js toward 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)
S
Description
Mairins Version of The game of life
Readme 22 MiB
Languages
JavaScript 92%
HTML 7.7%
Dockerfile 0.3%