# 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.
**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 ``, 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
- [Features](#features)
- [Prerequisites](#prerequisites)
- [Quick start](#quick-start)
- [Configuration (`.env`)](#configuration-env)
- [Running the server](#running-the-server)
- [Project layout](#project-layout)
- [Database schema](#database-schema)
- [REST API](#rest-api)
- [Frontend architecture](#frontend-architecture)
- [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.
---
## Quick start
```bash
git clone
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
```
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.
---
## Configuration (`.env`)
All configuration lives in `.env` at the project root. Use `example.env` as
the template. Fields:
### 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
```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
```
The server serves both the API (`/api/*`) and the static frontend (`/`,
`/src/...`, `/uploads/...`) on the same port.
After it starts you should see:
```
[server] listening on http://0.0.0.0:3000
```
---
## Project layout
```
fertig-classic-games/
├── example.env Configuration template (commit this)
├── .env Your local configuration (gitignored)
├── package.json
├── README.md
│
├── 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
│
├── 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/ SQLite database (gitignored)
└── fertig.sqlite
```
---
## Database schema
Created by `server/db/migrations/001_init.sql`. Run `npm run migrate` to apply
any pending migrations.
- **`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`
Add new migrations as `server/db/migrations/00N_description.sql` — they are
applied in lexicographic order and tracked in `schema_migrations`.
---
## REST API
All endpoints are JSON. Session is carried by the `fcg_sid` cookie
automatically; the client uses `credentials: 'same-origin'` in fetch calls.
### Auth — `/api/auth`
| 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. |
### Profile — `/api/profile`
| 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`. |
### History — `/api/history`
| Method | Path | Auth | Description |
|--------|-------|----------|------------------------------------------------------------|
| GET | `/` | Required | Last 100 matches for the user, plus `{ wins, losses, draws }`. |
### Misc
| Method | Path | Auth | Description |
|--------|----------------|------|------------------------------------------------------|
| GET | `/api/health` | — | `{ ok: true }`. |
| GET | `/api/games` | — | Lists registered games from `games/registry.js`. |
---
## Frontend architecture
- **No bundler.** `public/index.html` uses an `