- Create a red "X" overlay that animates onto the portrait of a skipped opponent - Implement `showSkipOverlay` to render the effect using Phaser DOM objects - Trigger the overlay when a player is skipped to provide clearer visual feedback |
||
|---|---|---|
| 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, match history, and multiplayer lobbies.
The frontend uses native browser ES modules — no bundler, no build step. The backend is Node.js + Express + Socket.IO with SQLite for persistence.
Table of contents
- Features
- Prerequisites
- Quick start
- Configuration (
.env) - Running the server
- Project layout
- Database schema
- REST API
- Socket.IO events
- 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) ready to be populated by games
- Multiplayer lobby system over Socket.IO with rooms, presence, and broadcast
- Pluggable game registry — register tabletop or casino games server-side
- Base classes (
TabletopGame,CasinoGame) that handle the turn loop / betting loop scaffolding so new games only implement rules - 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. |
Multiplayer
| Variable | Default | Description |
|---|---|---|
SOCKET_IO_CORS_ORIGIN |
http://localhost:3000 |
CORS origin for the Socket.IO server. Use * only in dev. |
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/*), static frontend (/, /src/...,
/uploads/...), and the Socket.IO endpoint (/socket.io) 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 + Socket.IO 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
│ └── multiplayer/
│ ├── index.js Socket.IO server + auth handshake
│ ├── lobby.js Room manager (create/join/leave)
│ └── gameRegistry.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
│ │ └── socket.js socket.io-client
│ ├── 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
│ │ ├── LobbyScene.js
│ │ └── GameRoomScene.js
│ └── games/
│ ├── BaseGame.js
│ ├── tabletop/TabletopGame.js
│ └── casino/CasinoGame.js
│
└── 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 gameRegistry.js. |
Socket.IO events
Clients connect to the same origin as the page. The server reads the session cookie during the handshake and rejects unauthenticated connections.
Server → client
| Event | Payload | When |
|---|---|---|
hello |
{ user, games } |
On successful connection. |
lobby:update |
Room[] |
After a room is created, joined, or left. |
room:update |
Room |
When the room's players list changes. |
room:message |
{ from: { id, username }, payload, at } |
Chat / game messages routed within a room. |
Client → server
| Event | Payload | Ack | Notes |
|---|---|---|---|
lobby:subscribe |
gameSlug |
— | Joins the lobby for that game slug. |
lobby:unsubscribe |
gameSlug |
— | |
room:create |
{ gameSlug, name? } |
{ ok, room? , error? } |
Creates a room; you join automatically. |
room:join |
{ roomId } |
{ ok, room?, error? } |
|
room:leave |
{ roomId } |
{ ok } |
|
room:message |
{ roomId, payload } |
— | Broadcast to other room members. |
Room shape:
{
id: string,
gameSlug: string,
name: string,
hostId: number,
players: [{ id: number, username: string, seat: number }],
maxPlayers: number,
status: 'waiting' | 'in_progress' | 'completed'
}
Frontend architecture
- No bundler.
public/index.htmluses an<script type="importmap">to resolvephaserandsocket.io-clientto CDN ES modules. 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 → Lobby → 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.socket(services/socket.js) is a lazily-connected singleton; scenes callconnectSocket()when entering a multiplayer flow.- 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/multiplayer/gameRegistry.js:registerGame({ slug: 'cribbage', name: 'Cribbage', category: 'tabletop', // or 'casino' minPlayers: 2, maxPlayers: 4, supportsMultiplayer: true, });The lobby and game menu will pick it up automatically.
-
Add a row in the
gamestable if you want to record matches in history. Either insert in a new migration or via a one-off statement. -
Implement the game scene. Extend
TabletopGameorCasinoGame:// public/src/games/tabletop/CribbageGame.js import TabletopGame from './TabletopGame.js'; export default class CribbageGame extends TabletopGame { constructor() { super('CribbageGame'); } createBoard() { /* render felt, pegs, cards (placeholder shapes ok) */ } applyBoardState(state) { /* reconcile pieces & turn */ } } -
Route to it from
GameRoomScene. Either switch ongame.slugand start the right scene, or load the module dynamically:const mod = await import(`../games/${category}/${ClassName}.js`); this.scene.add(ClassName, mod.default, true, { game, room, socket, user }); -
Send/receive game state. Use
sendAction({ type, ... })from the base class. The server should listen forgame:<slug>:actionand respond withgame:<slug>:state(public) andgame:<slug>:private(per-seat hidden info, for casino games).
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.
Socket connection rejected with Not authenticated — sign in first; the
Socket.IO handshake reads the session cookie. Make sure
SOCKET_IO_CORS_ORIGIN matches the origin you're browsing from.
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
The framework is intentionally game-agnostic. Concrete games come next:
- Backgammon — 2-player turn-based on
TabletopGame - Parchisi — up to 4 players on
TabletopGame - Blackjack — 1–6 players on
CasinoGame - Texas Hold 'Em — 2–8 players on
CasinoGame - Server-authoritative game state, per-seat private state for casino games
- Match recording into
matches/match_playersfor history - Friends list + invites
- Spectator mode
Contributions welcome — start by registering a game in gameRegistry.js and
extending one of the base classes.