450 lines
18 KiB
Markdown
450 lines
18 KiB
Markdown
# 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)).
|