Add `animateDrawTile()` to tween a face-down triangle from the pool area to the drawing player's portrait over 1.2 seconds. Integrates the animation into both the human draw flow (`onPoolClick`) and the AI draw loop (`runAITurn`), ensuring visual feedback before the game state updates and the hand refreshes. |
||
|---|---|---|
| .vscode | ||
| public | ||
| server | ||
| .gitignore | ||
| README.md | ||
| example.env | ||
| game.md | ||
| package-lock.json | ||
| package.json | ||
README.md
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
- Prerequisites
- Quick start
- Configuration (
.env) - Running the server
- Project layout
- Database schema
- REST API
- Frontend architecture
- Adding a new game
- Email verification
- Profile pictures
- Troubleshooting
- 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-sqlite3andbcryptto 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-toolsnpm package — it is broken on modern Node.js. See Troubleshooting for step-by-step instructions.
- Linux:
No bundler, no Docker, no external database required to get started.
Quick start
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. |
| 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
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_atsessions—id, user_id, expires_at, created_atprofiles—user_id (PK/FK), display_name, avatar_path, bio, updated_atgames—id, slug, name, category ('tabletop'|'casino'), max_players, supports_multiplayermatches—id, game_id, started_at, ended_at, statusmatch_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.htmluses an<script type="importmap">to resolvephaserto 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. authstore (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 witherr.statusanderr.dataattached.- DOM-overlay inputs. Phaser doesn't have a native text input, so
ui/TextInput.jspositions a real<input>element over the canvas and repositions it on scale-resize. The#dom-layerdiv haspointer-events: noneso the canvas stays interactive, and child elements re-enable pointer events.
Adding a new game
-
Register the game on the server. In
server/games/registry.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.
-
Implement the game scene. Each game is a
Phaser.Scenethat reads its setup from the data passed byGameRoomScene:// 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 */ } } -
Register the scene and route to it. Add the scene to the
scenearray inpublic/src/main.js, then add its slug → scene-key mapping to theslugDispatchobject inpublic/src/scenes/GameRoomScene.js. -
Record results (optional). When a match finishes,
POSTto/api/history/single-playerto 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 nameavatar). - 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 alongsidedata/fertig.sqliteif 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) 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:
- Download Visual Studio Build Tools 2022 from Microsoft and install it with the "Desktop development with C++" workload selected.
- Install Python 3.x from python.org, checking "Add to PATH" during installation.
- Open an Administrator PowerShell and run:
npm install -g node-gyp - Re-run
npm installin the project directory.
If you have the broken windows-build-tools package installed globally,
uninstall it first (run PowerShell as Administrator):
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).