Go to file
Brian Fertig 97edebab55 feat(videopoker): add 8-bit sound effects and update audio playback
- Load new 8-bit style sound assets (card deal, card show, casino win/lose)
- Replace SFX enum-based sound calls with direct sound key playback
- Use 8-bit sounds for card interactions and win/lose feedback in VideoPokerGame
2026-06-06 18:13:06 -06:00
public feat(videopoker): add 8-bit sound effects and update audio playback 2026-06-06 18:13:06 -06:00
server feat: add Farkel game and update Video Poker CRT theme 2026-06-06 18:01:50 -06:00
.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

  • 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 for step-by-step instructions.

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.

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

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.

  • usersid, email, username, password_hash, email_verified, verification_token, verification_expires_at, created_at
  • sessionsid, user_id, expires_at, created_at
  • profilesuser_id (PK/FK), display_name, avatar_path, bio, updated_at
  • gamesid, slug, name, category ('tabletop'|'casino'), max_players, supports_multiplayer
  • matchesid, game_id, started_at, ended_at, status
  • match_playersmatch_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:

    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:

    // 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) 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:
    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):

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).