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

582
README.md
View File

@ -1,185 +1,99 @@
# Fertig Classic Games # Fertig Classic Games
A Phaser 3.90 collection of classic tabletop games (Backgammon, Parchisi, ...) A single-page collection of **77 classic games** — board games, card games,
and casino games (Blackjack, Texas Hold 'Em, ...). Games are single-player casino games, word games, and logic puzzles — all playable in the browser
against AI opponents. 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 **This is a fully static site.** There is no server, no database, no accounts,
needed to run it.** The frontend uses native browser ES modules (no bundler, no build step, and no `npm install` required to run it. The frontend uses native
no build step) and loads Phaser from a CDN. All per-player data (a single local browser ES modules and loads Phaser from a CDN. Everything that used to be a
profile, chips, match history, puzzle progress) lives in the browser's backend round-trip — the single local player profile, casino chip balance, match
`localStorage`. Word-game dictionaries and AI run client-side from the ported history, puzzle progress, and word-game dictionaries/AI — now runs entirely in
engines in `src/words/`. the browser against `localStorage` and 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.
--- ---
## Table of contents ## Table of contents
- [Features](#features)
- [Prerequisites](#prerequisites)
- [Quick start](#quick-start) - [Quick start](#quick-start)
- [Configuration (`.env`)](#configuration-env) - [Deployment / hosting](#deployment--hosting)
- [Running the server](#running-the-server) - [The game catalog](#the-game-catalog)
- [Project layout](#project-layout) - [Project layout](#project-layout)
- [Database schema](#database-schema) - [Architecture](#architecture)
- [REST API](#rest-api) - [Scene flow](#scene-flow)
- [Frontend architecture](#frontend-architecture) - [The local "backend"](#the-local-backend)
- [Word games](#word-games)
- [Assets](#assets)
- [Adding a new game](#adding-a-new-game) - [Adding a new game](#adding-a-new-game)
- [Email verification](#email-verification) - [Developer tooling (`tools/`)](#developer-tooling-tools)
- [Profile pictures](#profile-pictures) - [Data files (`data/`)](#data-files-data)
- [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.
--- ---
## Quick start ## Quick start
No dependencies to install. You just need a static file server, because ES
modules can't be loaded over `file://`.
```bash ```bash
git clone <this-repo> git clone <this-repo>
cd fertig-classic-games cd fertig-classic-games
cp example.env .env # Any static server works. A couple of one-liners:
# Edit .env — at minimum, set SESSION_SECRET to a long random string. npm run serve # python3 -m http.server 8000 → http://localhost:8000
# Generate one with: ./start_web.sh # python3 -m http.server 3000 → http://localhost:3000
# node -e "console.log(require('crypto').randomBytes(48).toString('hex'))" npx serve . # if you prefer Node
npm install
npm run migrate
npm run dev
``` ```
Open http://localhost:3000 in your browser. Register an account; if SMTP is Then open the printed URL in a modern browser. That's it — pick a game and play.
not configured (the default), the verification link is printed to the server
console — click it to verify. > **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 repository root _is_ the site.** Serve it as static files from any web
the template. Fields: host, CDN, or object store (GitHub Pages, Netlify, Cloudflare Pages, S3, nginx,
Apache, …). No configuration, environment variables, or runtime are needed.
### Server Because every asset path is relative and `index.html` sets `<base href="./">`,
the site works unchanged from a domain root **or any subdirectory**
| Variable | Default | Description | (e.g. `example.com/games/`). To drop it into an existing site:
|-------------|--------------------------|------------------------------------------|
| `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
```bash ```bash
npm run dev # node --watch, auto-restart on file changes cp -r index.html styles.css *.png favicon.ico assets data src \
npm start # plain node, production-style /var/www/parent-site/games/
npm run migrate # apply any pending DB migrations
``` ```
The server serves both the API (`/api/*`) and the static frontend (`/`, The only files the deployed site needs are `index.html`, `styles.css`, the
`/src/...`, `/uploads/...`) on the same port. 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/ fertig-classic-games/
├── example.env Configuration template (commit this) ├── index.html Entry point: importmap for Phaser + <base href="./">
├── .env Your local configuration (gitignored) ├── styles.css Page chrome (canvas container + DOM overlay layer)
├── package.json ├── package.json "serve" script; no runtime deps
├── README.md ├── start_web.sh Convenience local server (python http.server)
├── favicon*/apple-touch/… Favicons and PWA icons
├── server/ Backend (Node.js, ES modules) ├── src/ The entire application (native ES modules)
│ ├── index.js Express bootstrap │ ├── main.js Phaser.Game config; registers every scene; DOM-overlay sync
│ ├── config.js Loads & validates .env │ ├── config.js Canvas dimensions (1920×1080) and the UI color palette
│ ├── db/ │ ├── data/
│ │ ├── index.js better-sqlite3 connection singleton │ │ └── gamesRegistry.js The catalog: registerGame() for all 77 games
│ │ ├── migrate.js SQL migration runner │ ├── scenes/ Shell scenes (menu, profile, opponent select, room)
│ │ └── migrations/ │ ├── services/ api.js → localApi.js → localStore.js (the local backend)
│ │ └── 001_init.sql Initial schema │ ├── ui/ Reusable widgets: Button, Modal, TextInput, Portrait,
│ ├── auth/ │ │ music/speech/tutorial/fullscreen helpers
│ │ ├── routes.js /api/auth/* endpoints │ ├── words/ Client-side word-game engines + wordRouter dispatcher
│ │ ├── service.js bcrypt, sessions, verification tokens │ └── games/<slug>/ One folder per game (Scene + Logic + AI + Data + tutorial.md)
│ │ └── 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
├── public/ Frontend, served as static files ├── data/ Static JSON game data + word lists (fetched at runtime)
│ ├── index.html Loads Phaser via importmap │ ├── *.json Per-game level banks, artwork maps, opponents, music, etc.
│ ├── styles.css │ ├── crosswords/ Mini-crossword puzzle bank
│ ├── uploads/ Avatars (gitignored) │ └── wordlists/ enable1.txt (dictionary) + common.txt
│ └── 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/ SQLite database (gitignored) ├── assets/ Runtime media
└── fertig.sqlite │ ├── 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 The frontend is deliberately buildless: `index.html` declares an
any pending migrations. `<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, ### Scene flow
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`
Add new migrations as `server/db/migrations/00N_description.sql` — they are Scenes live in `src/scenes/` and run in this order:
applied in lexicographic order and tracked in `schema_migrations`.
--- ```
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 Every concrete game scene is registered in the `scene` array in `src/main.js`.
automatically; the client uses `credentials: 'same-origin'` in fetch calls.
### 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 | ### The local "backend"
|--------|-------------|------|----------------------------------------------------------------------------------------------|
| 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. |
### 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 | `localApi.dispatch(method, path, …)` handles the former endpoints against
|--------|------------|-----------|-----------------------------------------------------------------------------------| `localStorage`:
| 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`. |
### 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 | `src/services/localStore.js` is the typed `localStorage` wrapper. All keys are
|--------|-------|----------|------------------------------------------------------------| namespaced under `fcg:` (`fcg:profile`, `fcg:history`, `fcg:puzzleProgress`) so
| GET | `/` | Required | Last 100 matches for the user, plus `{ wins, losses, draws }`. | player data is easy to find, export, or clear.
### Misc ### Word games
| Method | Path | Auth | Description | 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,
| GET | `/api/health` | — | `{ ok: true }`. | Word Ladder, Word Search, Sudoku, Tectonic, Boggle, Spelling Bee, Mini Crossword,
| GET | `/api/games` | — | Lists registered games from `games/registry.js`. | 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 Media is loaded from `assets/` (spritesheets, backgrounds, SFX, music, AI voice
clips, tutorial and portrait videos). Per-game data — level banks, artwork maps,
- **No bundler.** `public/index.html` uses an `<script type="importmap">` to opponent rosters, music playlists — is JSON in `data/`, fetched at runtime. Many
resolve `phaser` to a CDN ES module. The rest of the app uses relative ES games render primarily with vector/procedural graphics and layer optional
imports. drop-in spritesheets on top, so art can be added without refactoring scenes.
- **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.
--- ---
## Adding a new game ## 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 ```js
registerGame({ registerGame({
slug: 'cribbage', slug: 'cribbage',
name: 'Cribbage', name: 'Cribbage',
category: 'tabletop', // or 'casino' or 'cards' category: 'cards', // tabletop | cards | casino | word | logic
minPlayers: 2, cardGame: true, // uses the shared card-back picker
maxPlayers: 4, minPlayers: 2, maxPlayers: 2,
minOpponents: 1, minOpponents: 1, maxOpponents: 1,
maxOpponents: 3, defaultOpponents: null, // optional preselected opponent count
hasTutorial: true, // shows the how-to-play video button
iconFrame: 64, // frame index into the game-icons spritesheet
}); });
``` ```
The game menu picks it up automatically. The menu, opponent-select screen, and history all pick it up from here.
2. **Implement the game scene.** Each game is a `Phaser.Scene` that reads its 2. **Implement the scene** at `src/games/<slug>/<Name>Game.js`. It's a
setup from the data passed by `GameRoomScene`: `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.
```js 3. **Wire it up:**
// public/src/games/cribbage/CribbageGame.js - import and add the scene class to the `scene` array in `src/main.js`;
import * as Phaser from 'phaser'; - add the `slug → SceneKey` entry to `slugDispatch` in
`src/scenes/GameRoomScene.js`.
export default class CribbageGame extends Phaser.Scene { 4. **Persist progress (optional):** for level-based puzzles, call
constructor() { super('CribbageGame'); } `/puzzles/<slug>/{progress,complete,reset}`; to record win/loss/draw, POST to
init(data) { `/history/single-player`. Both are served by the local API shim.
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 ## Developer tooling (`tools/`)
When `SMTP_HOST` is set, `nodemailer` is used to send a real email. When it's Offline Node scripts, not part of the deployed site. Run them with plain `node`
empty, `server/email/mailer.js` instead prints the verification link to the from the repo root (Node.js 20+):
server console:
``` - **Generators** (`genRushHour.js`, `genZuma.js`, `genKatamino.js`,
[mailer:dev] Verification link for user@example.com: `genMiniCrossword.js`, `genJumble.js`, `buildCommonWords.js`, …) produce the
http://localhost:3000/api/auth/verify?token=... 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:
Click that link (or visit it in the browser) to verify the account. The ```bash
registration response also includes the dev link as `verification.devLink`, node tools/verifyCribbage.js
which the frontend shows on the verify screen. node tools/verifyRisk.js --games=300
### 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 ## Data files (`data/`)
- Uploaded via `POST /api/profile/avatar` (multipart form, field name Runtime JSON fetched by the games:
`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.
--- - **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`.
## Troubleshooting All are static assets; editing them (or regenerating them via `tools/`) changes
game content without touching code.
**`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
```
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`.
**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.
**Avatar upload fails with `Unsupported image type`** — only the MIME types
in `ALLOWED_UPLOAD_MIME` are accepted. Add more if needed.
**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.
---
## Roadmap
- [ ] Smarter AI opponents per game
- [ ] Richer match history views and per-game stats
- [ ] Replace placeholder vector graphics with sprites
Contributions welcome — start by registering a game in `games/registry.js`
(see [Adding a new game](#adding-a-new-game)).