fertig-classic-games/README.md

450 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Fertig Classic Games
A Phaser 3.90 framework for classic tabletop games (Backgammon, Parchisi, ...)
and casino games (Blackjack, Texas Hold 'Em, ...), with accounts, profiles,
and match history. Games are single-player against AI opponents.
The frontend uses **native browser ES modules** — no bundler, no build step.
The backend is Node.js + Express with SQLite for persistence.
---
## 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 <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
```
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 `<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.
---
## Adding a new game
1. **Register the game on the server.** In `server/games/registry.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
```
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)).