docs: rewrite README for static-site architecture

Remove all server-side documentation (Express, SQLite, auth, email, uploads,
API routes, migrations, prerequisites for native bindings) and replace it
with accurate documentation for the fully static, buildless frontend:

- Quick start with any static file server
- Deployment to any CDN/object store
- 77-game catalog with categories
- Scene flow and local backend (localStorage, localApi shim)
- Word-game engines running client-side
- Project layout reflecting the no-server structure
- Adding a new game (updated for client-only flow)
- Developer tooling and data files

Reflects the migration from a Node.js/SQLite backend to a pure static site
using native ES modules and Phaser from CDN.
This commit is contained in:
Brian Fertig 2026-06-30 22:31:23 -06:00
parent 376f878a70
commit 9b45f7ba6f
1 changed files with 210 additions and 398 deletions

608
README.md
View File

@ -1,185 +1,99 @@
# Fertig Classic Games
A Phaser 3.90 collection of classic tabletop games (Backgammon, Parchisi, ...)
and casino games (Blackjack, Texas Hold 'Em, ...). Games are single-player
against AI opponents.
A single-page collection of **77 classic games** — board games, card games,
casino games, word games, and logic puzzles — all playable in the browser
against local AI opponents. Built on [Phaser 3.90](https://phaser.io/).
**This is a fully static site — no server, no database, no accounts, no npm
needed to run it.** The frontend uses native browser ES modules (no bundler,
no build step) and loads Phaser from a CDN. All per-player data (a single local
profile, chips, match history, puzzle progress) lives in the browser's
`localStorage`. Word-game dictionaries and AI run client-side from the ported
engines in `src/words/`.
## Running / deployment
The repository root **is** the site. Serve it as static files from any web
server, at the domain root **or any subdirectory** (e.g. `example.com/games/`).
All asset paths are relative and anchored by `<base href="./">`, so no
configuration is needed. The only runtime files are `index.html`, `styles.css`,
and the `assets/`, `data/`, and `src/` directories.
To deploy into a parent site, copy those into the desired subdirectory:
```
cp -r index.html styles.css *.png favicon.ico assets data src /var/www/parent-site/games/
```
For local testing:
```
npm run serve # python3 -m http.server on http://localhost:8000
```
The `tools/` directory holds offline dev tooling — puzzle generators and
`verify*` self-play scripts — run with plain `node` from the repo root
(e.g. `node tools/verifyBookwork.js`). It is not part of the deployed site.
**This is a fully static site.** There is no server, no database, no accounts,
no build step, and no `npm install` required to run it. The frontend uses native
browser ES modules and loads Phaser from a CDN. Everything that used to be a
backend round-trip — the single local player profile, casino chip balance, match
history, puzzle progress, and word-game dictionaries/AI — now runs entirely in
the browser against `localStorage` and the ported engines in `src/words/`.
---
## Table of contents
- [Features](#features)
- [Prerequisites](#prerequisites)
- [Quick start](#quick-start)
- [Configuration (`.env`)](#configuration-env)
- [Running the server](#running-the-server)
- [Deployment / hosting](#deployment--hosting)
- [The game catalog](#the-game-catalog)
- [Project layout](#project-layout)
- [Database schema](#database-schema)
- [REST API](#rest-api)
- [Frontend architecture](#frontend-architecture)
- [Architecture](#architecture)
- [Scene flow](#scene-flow)
- [The local "backend"](#the-local-backend)
- [Word games](#word-games)
- [Assets](#assets)
- [Adding a new game](#adding-a-new-game)
- [Email verification](#email-verification)
- [Profile pictures](#profile-pictures)
- [Troubleshooting](#troubleshooting)
- [Roadmap](#roadmap)
---
## Features
- Account creation with email + username + password (bcrypt hashed)
- Email verification with configurable SMTP — falls back to logging dev links
to the console when SMTP is not set up
- Session cookies backed by SQLite (httpOnly, SameSite=Lax)
- Profile management: display name, bio, avatar upload (PNG / JPEG / WebP)
- Match history (wins / losses / draws) recorded for single-player games
- Pluggable game registry — register tabletop or casino games server-side
- Single-player games against configurable AI opponents
- 1920×1080 canvas that scales to any viewport via `Phaser.Scale.FIT`
- Vector-only placeholder graphics — drop sprites in later without
refactoring scenes
- Mouse + keyboard controls
---
## Prerequisites
- **Node.js 20 or newer** (uses `node --watch`, native fetch, ES modules)
- **npm 9 or newer**
- A C/C++ toolchain for `better-sqlite3` and `bcrypt` to build native bindings:
- **Linux**: `build-essential`, `python3`
- **macOS**: Xcode command line tools (`xcode-select --install`)
- **Windows**: install **Visual Studio Build Tools 2022** with the
"Desktop development with C++" workload and **Python 3.x** (add to PATH).
Do **not** use the deprecated `windows-build-tools` npm package — it is
broken on modern Node.js. See [Troubleshooting](#troubleshooting) for
step-by-step instructions.
No bundler, no Docker, no external database required to get started.
- [Developer tooling (`tools/`)](#developer-tooling-tools)
- [Data files (`data/`)](#data-files-data)
---
## Quick start
No dependencies to install. You just need a static file server, because ES
modules can't be loaded over `file://`.
```bash
git clone <this-repo>
cd fertig-classic-games
cp example.env .env
# Edit .env — at minimum, set SESSION_SECRET to a long random string.
# Generate one with:
# node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"
npm install
npm run migrate
npm run dev
# Any static server works. A couple of one-liners:
npm run serve # python3 -m http.server 8000 → http://localhost:8000
./start_web.sh # python3 -m http.server 3000 → http://localhost:3000
npx serve . # if you prefer Node
```
Open http://localhost:3000 in your browser. Register an account; if SMTP is
not configured (the default), the verification link is printed to the server
console — click it to verify.
Then open the printed URL in a modern browser. That's it — pick a game and play.
> **Requirements:** a current browser with ES-module + importmap support
> (Chrome/Edge/Firefox/Safari, ~2023+). Python 3 (or any static server) for local
> hosting. Node.js 20+ is only needed for the optional dev tooling in `tools/`.
---
## Configuration (`.env`)
## Deployment / hosting
All configuration lives in `.env` at the project root. Use `example.env` as
the template. Fields:
**The repository root _is_ the site.** Serve it as static files from any web
host, CDN, or object store (GitHub Pages, Netlify, Cloudflare Pages, S3, nginx,
Apache, …). No configuration, environment variables, or runtime are needed.
### Server
| Variable | Default | Description |
|-------------|--------------------------|------------------------------------------|
| `NODE_ENV` | `development` | `development` or `production`. |
| `HOST` | `0.0.0.0` | Bind address. |
| `PORT` | `3000` | HTTP port. |
| `BASE_URL` | `http://localhost:3000` | Public URL used in verification emails. |
| `LOG_LEVEL` | `info` | `error`, `warn`, `info`, `debug`. |
### Database
| Variable | Default | Description |
|-----------|-------------------------|-----------------------------------|
| `DB_PATH` | `./data/fertig.sqlite` | SQLite file path. Auto-created. |
### Auth
| Variable | Default | Description |
|------------------------|---------------|--------------------------------------------------------------------------------------------------------------|
| `SESSION_SECRET` | *(required)* | Long random string. **Required in production.** A dev fallback is used if empty in development with a warning. |
| `SESSION_COOKIE_NAME` | `fcg_sid` | Cookie name. |
| `SESSION_TTL_DAYS` | `30` | Session lifetime in days. |
| `BCRYPT_ROUNDS` | `12` | bcrypt cost factor. |
### Uploads (profile pictures)
| Variable | Default | Description |
|------------------------|--------------------------------------|--------------------------------------|
| `UPLOAD_DIR` | `./public/uploads` | Directory for avatar files. |
| `MAX_UPLOAD_SIZE_MB` | `5` | Max image size in MB. |
| `ALLOWED_UPLOAD_MIME` | `image/png,image/jpeg,image/webp` | Comma-separated MIME allowlist. |
### Email
| Variable | Default | Description |
|---------------------------------|---------|------------------------------------------------------------------------|
| `SMTP_HOST` | *(empty)* | If empty, verification links log to the console instead of sending. |
| `SMTP_PORT` | `587` | SMTP port. |
| `SMTP_SECURE` | `false` | `true` for SMTPS (usually port 465). |
| `SMTP_USER` | *(empty)* | SMTP username, if your provider requires auth. |
| `SMTP_PASS` | *(empty)* | SMTP password. |
| `SMTP_FROM` | *(see example.env)* | `From:` header for outgoing mail. |
| `VERIFICATION_TOKEN_TTL_HOURS` | `24` | How long verification links remain valid. |
---
## Running the server
Because every asset path is relative and `index.html` sets `<base href="./">`,
the site works unchanged from a domain root **or any subdirectory**
(e.g. `example.com/games/`). To drop it into an existing site:
```bash
npm run dev # node --watch, auto-restart on file changes
npm start # plain node, production-style
npm run migrate # apply any pending DB migrations
cp -r index.html styles.css *.png favicon.ico assets data src \
/var/www/parent-site/games/
```
The server serves both the API (`/api/*`) and the static frontend (`/`,
`/src/...`, `/uploads/...`) on the same port.
The only files the deployed site needs are `index.html`, `styles.css`, the
favicons/PWA icons in the root, and the `assets/`, `data/`, and `src/`
directories. The `tools/` directory is dev-only and never shipped.
After it starts you should see:
Player state (profile, chips, history, puzzle progress) lives in each visitor's
own browser `localStorage` under the `fcg:` prefix — nothing is stored or synced
server-side, so hosting is entirely stateless.
```
[server] listening on http://0.0.0.0:3000
```
---
## The game catalog
77 games are registered in `src/data/gamesRegistry.js` and grouped into five
menu categories:
| Category | Count | Examples |
|----------|-------|----------|
| **Tabletop** | 21 | Backgammon, Chess, Checkers, Go, Othello, Settlers of Catan, Ticket to Ride, Risk, Monopoly, Blokus, Labyrinth, Mahjong, Stratego, Battleship, Mastermind, Connect 4, Forbidden Island, Azul, Chinese Checkers, Mexican Train, Parchisi |
| **Cards** | 18 | Cribbage, Gin Rummy, Rummikub, Canasta, Hearts, Uno, Phase 10, Skip-Bo, Go Fish, Old Maid, Nerts, Dominion, Splendor, Freecell, Solitaire Tour, Spire Climb, Zahtzee, Farkle |
| **Casino** | 9 | Blackjack, Texas Hold 'Em, Baccarat, Pai Gow Poker, Video Poker, Craps, Roulette, Bingo, Slot Machines |
| **Word** | 15 | Wordle Race, Scrabble, Boggle, Ghost, Word Ladder, Word Search, Hangman, Spelling Bee, Sudoku, Mini Crossword, Tectonic, Bookwork, Kiitos, Tri-Ominoes, Jumble |
| **Logic & Puzzle** | 14 | 2048, Rush Hour, Hexsweeper, Jell-o Monsters, Shift, Mahjong Match, Jewel Quest, Zuma, Bejeweled Blitz, Mini Motorways, Dot Link, Katamino, Genius Square, Block Fighter |
Each game is a self-contained `Phaser.Scene` (plus its own logic/AI/data helper
modules) under `src/games/<slug>/`. Games are single-player against 07
configurable AI opponents depending on the title; several ship a video tutorial.
---
@ -187,291 +101,189 @@ After it starts you should see:
```
fertig-classic-games/
├── example.env Configuration template (commit this)
├── .env Your local configuration (gitignored)
├── package.json
├── README.md
├── index.html Entry point: importmap for Phaser + <base href="./">
├── styles.css Page chrome (canvas container + DOM overlay layer)
├── package.json "serve" script; no runtime deps
├── start_web.sh Convenience local server (python http.server)
├── favicon*/apple-touch/… Favicons and PWA icons
├── server/ Backend (Node.js, ES modules)
│ ├── index.js Express bootstrap
│ ├── config.js Loads & validates .env
│ ├── db/
│ │ ├── index.js better-sqlite3 connection singleton
│ │ ├── migrate.js SQL migration runner
│ │ └── migrations/
│ │ └── 001_init.sql Initial schema
│ ├── auth/
│ │ ├── routes.js /api/auth/* endpoints
│ │ ├── service.js bcrypt, sessions, verification tokens
│ │ └── middleware.js loadUser, requireAuth
│ ├── profile/
│ │ ├── routes.js /api/profile/*, multer upload
│ │ └── service.js
│ ├── history/
│ │ └── routes.js /api/history
│ ├── email/
│ │ └── mailer.js Nodemailer wrapper with console fallback
│ └── games/
│ └── registry.js Game definitions
├── src/ The entire application (native ES modules)
│ ├── main.js Phaser.Game config; registers every scene; DOM-overlay sync
│ ├── config.js Canvas dimensions (1920×1080) and the UI color palette
│ ├── data/
│ │ └── gamesRegistry.js The catalog: registerGame() for all 77 games
│ ├── scenes/ Shell scenes (menu, profile, opponent select, room)
│ ├── services/ api.js → localApi.js → localStore.js (the local backend)
│ ├── ui/ Reusable widgets: Button, Modal, TextInput, Portrait,
│ │ music/speech/tutorial/fullscreen helpers
│ ├── words/ Client-side word-game engines + wordRouter dispatcher
│ └── games/<slug>/ One folder per game (Scene + Logic + AI + Data + tutorial.md)
├── public/ Frontend, served as static files
│ ├── index.html Loads Phaser via importmap
│ ├── styles.css
│ ├── uploads/ Avatars (gitignored)
│ └── src/
│ ├── main.js Phaser.Game + scale config
│ ├── config.js UI colors, dimensions, API base
│ ├── services/
│ │ ├── api.js fetch wrapper
│ │ └── auth.js Client-side auth store
│ ├── ui/
│ │ ├── Button.js
│ │ ├── TextInput.js DOM-overlay input that follows canvas scale
│ │ └── Modal.js
│ ├── scenes/
│ │ ├── BootScene.js
│ │ ├── PreloadScene.js
│ │ ├── LandingScene.js
│ │ ├── LoginScene.js
│ │ ├── RegisterScene.js
│ │ ├── VerifyScene.js
│ │ ├── ProfileScene.js
│ │ ├── GameMenuScene.js
│ │ ├── OpponentSelectScene.js
│ │ └── GameRoomScene.js
│ └── games/ One subdirectory per game (uno/, blackjack/, ...)
├── data/ Static JSON game data + word lists (fetched at runtime)
│ ├── *.json Per-game level banks, artwork maps, opponents, music, etc.
│ ├── crosswords/ Mini-crossword puzzle bank
│ └── wordlists/ enable1.txt (dictionary) + common.txt
└── data/ SQLite database (gitignored)
└── fertig.sqlite
├── assets/ Runtime media
│ ├── images/ Spritesheets, backgrounds, board art (81 files)
│ ├── fonts/ Bitmap/web fonts
│ ├── fx/ Sound effects (71 files)
│ ├── music/ Background music tracks (21 files)
│ ├── speech/ AI opponent voice clips (393 files)
│ ├── videos/ In-game portrait/animation clips (90 files)
│ └── tutorial-videos/ How-to-play videos (17 files)
└── tools/ Offline dev scripts (run with node; NOT deployed)
├── gen*.js Puzzle/level-bank generators
└── verify*.js Headless self-play verification harnesses
```
---
## Database schema
## Architecture
Created by `server/db/migrations/001_init.sql`. Run `npm run migrate` to apply
any pending migrations.
The frontend is deliberately buildless: `index.html` declares an
`<script type="importmap">` that maps the bare specifier `phaser` to a CDN ES
module, and everything else uses relative `import` statements. The Phaser canvas
is a fixed **1920×1080** design surface scaled to any viewport with
`Phaser.Scale.FIT` + `CENTER_BOTH`.
- **`users`** — `id, email, username, password_hash, email_verified,
verification_token, verification_expires_at, created_at`
- **`sessions`** — `id, user_id, expires_at, created_at`
- **`profiles`** — `user_id (PK/FK), display_name, avatar_path, bio,
updated_at`
- **`games`** — `id, slug, name, category ('tabletop'|'casino'),
max_players, supports_multiplayer`
- **`matches`** — `id, game_id, started_at, ended_at, status`
- **`match_players`** — `match_id, user_id, seat, result
('win'|'loss'|'draw'|'abandoned'), score`
### Scene flow
Add new migrations as `server/db/migrations/00N_description.sql` — they are
applied in lexicographic order and tracked in `schema_migrations`.
Scenes live in `src/scenes/` and run in this order:
---
```
Boot → Preload → Landing → (Profile | GameMenu) → OpponentSelect → GameRoom → <ConcreteGame>
```
## REST API
- **BootScene / PreloadScene** — bootstrap and load shared assets.
- **LandingScene** — title screen with menu music and fullscreen toggle.
- **ProfileScene** — edit the single local profile (name, avatar, bio).
- **GameMenuScene** — browse the catalog by category.
- **OpponentSelectScene** — choose opponent count/skill, playfield, card back,
and any per-game options (expansion, difficulty, word length, …).
- **GameRoomScene** — a thin router. Its `slugDispatch` map turns the selected
game's `slug` into a scene key and starts the concrete game scene, forwarding
the chosen setup as scene-init data.
All endpoints are JSON. Session is carried by the `fcg_sid` cookie
automatically; the client uses `credentials: 'same-origin'` in fetch calls.
Every concrete game scene is registered in the `scene` array in `src/main.js`.
### Auth — `/api/auth`
`main.js` also pins Phaser's DOM-overlay container to the real canvas box on
every resize (`syncDomContainer`), so DOM elements like text inputs and video
portraits line up exactly with their in-canvas targets under `Scale.FIT`.
| Method | Path | Auth | Description |
|--------|-------------|------|----------------------------------------------------------------------------------------------|
| POST | `/register` | — | Body: `{ email, username, password }`. Creates user, sends verification, sets session. |
| POST | `/login` | — | Body: `{ identifier, password }` where `identifier` is email or username. |
| POST | `/logout` | — | Destroys session. |
| GET | `/me` | — | Returns `{ user }` or `{ user: null }`. |
| GET | `/verify` | — | `?token=...`. Marks user as verified. Returns HTML so a user clicking the email link sees text. |
### The local "backend"
### Profile — `/api/profile`
There is no HTTP backend. The chokepoint `src/services/api.js` exposes the same
`api.get/post/patch/delete/upload` surface the old server client had, but instead
of `fetch` it calls `src/services/localApi.js`, an in-browser shim that mirrors
the old REST responses (`{ status, data }`). This kept ~135 call sites across the
games working unchanged when the server was removed.
| Method | Path | Auth | Description |
|--------|------------|-----------|-----------------------------------------------------------------------------------|
| GET | `/` | Required | Returns the current user's profile. |
| PATCH | `/` | Required | Body: `{ displayName?, bio? }`. Returns the updated profile. |
| POST | `/avatar` | Required | Multipart with field `avatar`. Stores the file and updates `avatar_path`. |
`localApi.dispatch(method, path, …)` handles the former endpoints against
`localStorage`:
### History — `/api/history`
| Former route | Now backed by |
|--------------|---------------|
| `/auth/*`, `/profile*` | A single implicit local profile in `localStore.js` |
| `/profile/chips*` | Casino chip balance (default 2000; low-balance reset) |
| `/games` | `gamesRegistry.listGames()` |
| `/history*` | Match results array (bounded to last 100) |
| `/puzzles/:slug/{progress,complete,reset}` | Ordered per-game level progress |
| `/words/*` | Delegated to `src/words/wordRouter.js` |
| Method | Path | Auth | Description |
|--------|-------|----------|------------------------------------------------------------|
| GET | `/` | Required | Last 100 matches for the user, plus `{ wins, losses, draws }`. |
`src/services/localStore.js` is the typed `localStorage` wrapper. All keys are
namespaced under `fcg:` (`fcg:profile`, `fcg:history`, `fcg:puzzleProgress`) so
player data is easy to find, export, or clear.
### Misc
### Word games
| Method | Path | Auth | Description |
|--------|----------------|------|------------------------------------------------------|
| GET | `/api/health` | — | `{ ok: true }`. |
| GET | `/api/games` | — | Lists registered games from `games/registry.js`. |
Word games need a dictionary and move-search logic that used to live on the
server. Those engines were ported verbatim into `src/words/` (Scrabble, Ghost,
Word Ladder, Word Search, Sudoku, Tectonic, Boggle, Spelling Bee, Mini Crossword,
Kiitos, …). `wordRouter.js` fetches the dictionaries from `data/wordlists/` once
in the browser and routes `/words/*` calls to the right engine, returning the
same response shapes the old Express routes did.
---
### Assets
## Frontend architecture
- **No bundler.** `public/index.html` uses an `<script type="importmap">` to
resolve `phaser` to a CDN ES module. The rest of the app uses relative ES
imports.
- **Scenes** live in `public/src/scenes/`. The flow is:
`Boot → Preload → Landing → (Login | Register | Verify) → Profile |
GameMenu → OpponentSelect → GameRoom`.
- **`auth` store** (`services/auth.js`) is a tiny pub-sub the scenes
subscribe to so they re-render when the signed-in user changes.
- **`api`** (`services/api.js`) is a thin fetch wrapper that throws on
non-2xx responses with `err.status` and `err.data` attached.
- **DOM-overlay inputs.** Phaser doesn't have a native text input, so
`ui/TextInput.js` positions a real `<input>` element over the canvas and
repositions it on scale-resize. The `#dom-layer` div has
`pointer-events: none` so the canvas stays interactive, and child elements
re-enable pointer events.
Media is loaded from `assets/` (spritesheets, backgrounds, SFX, music, AI voice
clips, tutorial and portrait videos). Per-game data — level banks, artwork maps,
opponent rosters, music playlists — is JSON in `data/`, fetched at runtime. Many
games render primarily with vector/procedural graphics and layer optional
drop-in spritesheets on top, so art can be added without refactoring scenes.
---
## Adding a new game
1. **Register the game on the server.** In `server/games/registry.js`:
1. **Register it** in `src/data/gamesRegistry.js`:
```js
registerGame({
slug: 'cribbage',
name: 'Cribbage',
category: 'tabletop', // or 'casino' or 'cards'
minPlayers: 2,
maxPlayers: 4,
minOpponents: 1,
maxOpponents: 3,
});
```
The game menu picks it up automatically.
2. **Implement the game scene.** Each game is a `Phaser.Scene` that reads its
setup from the data passed by `GameRoomScene`:
```js
// public/src/games/cribbage/CribbageGame.js
import * as Phaser from 'phaser';
export default class CribbageGame extends Phaser.Scene {
constructor() { super('CribbageGame'); }
init(data) {
this.gameDef = data.game;
this.opponents = data.opponents; // selected AI opponents
this.playfield = data.playfield;
this.cardBack = data.cardBack;
}
create() { /* render board, run the game + AI locally */ }
}
```
3. **Register the scene and route to it.** Add the scene to the `scene` array
in `public/src/main.js`, then add its slug → scene-key mapping to the
`slugDispatch` object in `public/src/scenes/GameRoomScene.js`.
4. **Record results (optional).** When a match finishes, `POST` to
`/api/history/single-player` to record the win / loss / draw for history.
---
## Email verification
When `SMTP_HOST` is set, `nodemailer` is used to send a real email. When it's
empty, `server/email/mailer.js` instead prints the verification link to the
server console:
```
[mailer:dev] Verification link for user@example.com:
http://localhost:3000/api/auth/verify?token=...
```
Click that link (or visit it in the browser) to verify the account. The
registration response also includes the dev link as `verification.devLink`,
which the frontend shows on the verify screen.
### Using Gmail / a real provider in development
Add to `.env`:
```
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=you@gmail.com
SMTP_PASS=your-app-password # NOT your normal password, use an app password
SMTP_FROM="Fertig Classic Games <you@gmail.com>"
```
---
## Profile pictures
- Uploaded via `POST /api/profile/avatar` (multipart form, field name
`avatar`).
- Stored on the filesystem under `UPLOAD_DIR` (default `./public/uploads`).
- The DB stores the **public path** (e.g. `/uploads/u1-1715800000-abcd.png`),
not the file contents.
- The `public/uploads/` directory is gitignored. Back it up alongside
`data/fertig.sqlite` if you want to preserve user content.
---
## Troubleshooting
**`SESSION_SECRET must be set in production`** — set a long random
`SESSION_SECRET` in `.env`. In `NODE_ENV=development` the server falls back
to an insecure default with a warning.
**`SqliteError: no such table: ...`** — you haven't run migrations. Run
`npm run migrate`.
**`better-sqlite3` or `bcrypt` build failure on install** — install platform
build tools (see [Prerequisites](#prerequisites)) and re-run `npm install`.
**Windows build failure / `windows-build-tools` error** — the
`windows-build-tools` npm package is deprecated and broken on modern Node.js.
Use one of these approaches instead:
*Option 1 — Re-run the Node.js installer (easiest)*: Download the Node.js
installer from nodejs.org. On the "Tools for Native Modules" step, check the
box to automatically install Chocolatey, Python, and VS Build Tools.
*Option 2 — Manual install*:
1. Download **Visual Studio Build Tools 2022** from Microsoft and install it
with the **"Desktop development with C++"** workload selected.
2. Install **Python 3.x** from python.org, checking "Add to PATH" during
installation.
3. Open an Administrator PowerShell and run:
```powershell
npm install -g node-gyp
```js
registerGame({
slug: 'cribbage',
name: 'Cribbage',
category: 'cards', // tabletop | cards | casino | word | logic
cardGame: true, // uses the shared card-back picker
minPlayers: 2, maxPlayers: 2,
minOpponents: 1, maxOpponents: 1,
defaultOpponents: null, // optional preselected opponent count
hasTutorial: true, // shows the how-to-play video button
iconFrame: 64, // frame index into the game-icons spritesheet
});
```
4. Re-run `npm install` in the project directory.
If you have the broken `windows-build-tools` package installed globally,
uninstall it first (run PowerShell as Administrator):
```powershell
npm uninstall -g windows-build-tools
```
If that fails with a permission error, manually delete
`C:\Users\<you>\AppData\Roaming\npm\node_modules\windows-build-tools`.
The menu, opponent-select screen, and history all pick it up from here.
**Verification email never arrives** — if `SMTP_HOST` is empty, by design no
email is sent; check the server console for the dev link. If SMTP *is* set,
check provider auth (Gmail requires an app password, not your account
password) and `SMTP_PORT` / `SMTP_SECURE` for your provider.
2. **Implement the scene** at `src/games/<slug>/<Name>Game.js`. It's a
`Phaser.Scene` whose `init(data)` receives the chosen setup (`opponents`,
`playfield`, `cardBack`, `difficulty`, expansion/word-length options, …) and
whose `create()` renders the board and runs the game + AI locally. Follow the
established per-game module split (e.g. `<Name>Logic.js`, `<Name>AI.js`,
`<Name>Data.js`) so the scene stays presentation-focused. Add a `tutorial.md`
if applicable.
**Avatar upload fails with `Unsupported image type`** — only the MIME types
in `ALLOWED_UPLOAD_MIME` are accepted. Add more if needed.
3. **Wire it up:**
- import and add the scene class to the `scene` array in `src/main.js`;
- add the `slug → SceneKey` entry to `slugDispatch` in
`src/scenes/GameRoomScene.js`.
**Layout looks wrong / inputs misaligned** — the DOM overlay repositions on
the Phaser `resize` event. If you resize the window very fast, give it a
beat; if it persists, file an issue.
4. **Persist progress (optional):** for level-based puzzles, call
`/puzzles/<slug>/{progress,complete,reset}`; to record win/loss/draw, POST to
`/history/single-player`. Both are served by the local API shim.
---
## Roadmap
## Developer tooling (`tools/`)
- [ ] Smarter AI opponents per game
- [ ] Richer match history views and per-game stats
- [ ] Replace placeholder vector graphics with sprites
Offline Node scripts, not part of the deployed site. Run them with plain `node`
from the repo root (Node.js 20+):
Contributions welcome — start by registering a game in `games/registry.js`
(see [Adding a new game](#adding-a-new-game)).
- **Generators** (`genRushHour.js`, `genZuma.js`, `genKatamino.js`,
`genMiniCrossword.js`, `genJumble.js`, `buildCommonWords.js`, …) produce the
level banks and word lists that live in `data/`.
- **Verifiers** (`verifyCribbage.js`, `verifyRisk.js`, `verifyRummikub.js`, …)
are headless self-play/assertion harnesses that exercise each game's engine and
AI to catch regressions. They exit non-zero on failure:
```bash
node tools/verifyCribbage.js
node tools/verifyRisk.js --games=300
```
---
## Data files (`data/`)
Runtime JSON fetched by the games:
- **Per-game banks & artwork maps** — e.g. `rushhour.json`, `zuma.json`,
`katamino.json`, `dotlink.json`, `jumble.json`, `spireclimb-artwork.json`,
`slots-artwork.json`, `shift-artwork.json`.
- **Shared UI options**`playfields.json`, `colored-playfields.json`,
`card-backs.json`, `opponents.json`, `music.json`.
- **Word data**`wordlists/enable1.txt` (main dictionary),
`wordlists/common.txt` (curated common words), `crosswords/minicrossword.json`.
All are static assets; editing them (or regenerating them via `tools/`) changes
game content without touching code.