docs: rewrite README for static-site architecture
Remove all server-side documentation (Express, SQLite, auth, email, uploads, API routes, migrations, prerequisites for native bindings) and replace it with accurate documentation for the fully static, buildless frontend: - Quick start with any static file server - Deployment to any CDN/object store - 77-game catalog with categories - Scene flow and local backend (localStorage, localApi shim) - Word-game engines running client-side - Project layout reflecting the no-server structure - Adding a new game (updated for client-only flow) - Developer tooling and data files Reflects the migration from a Node.js/SQLite backend to a pure static site using native ES modules and Phaser from CDN.
This commit is contained in:
parent
376f878a70
commit
9b45f7ba6f
582
README.md
582
README.md
|
|
@ -1,185 +1,99 @@
|
|||
# Fertig Classic Games
|
||||
|
||||
A Phaser 3.90 collection of classic tabletop games (Backgammon, Parchisi, ...)
|
||||
and casino games (Blackjack, Texas Hold 'Em, ...). Games are single-player
|
||||
against AI opponents.
|
||||
A single-page collection of **77 classic games** — board games, card games,
|
||||
casino games, word games, and logic puzzles — all playable in the browser
|
||||
against local AI opponents. Built on [Phaser 3.90](https://phaser.io/).
|
||||
|
||||
**This is a fully static site — no server, no database, no accounts, no npm
|
||||
needed to run it.** The frontend uses native browser ES modules (no bundler,
|
||||
no build step) and loads Phaser from a CDN. All per-player data (a single local
|
||||
profile, chips, match history, puzzle progress) lives in the browser's
|
||||
`localStorage`. Word-game dictionaries and AI run client-side from the ported
|
||||
engines in `src/words/`.
|
||||
|
||||
## Running / deployment
|
||||
|
||||
The repository root **is** the site. Serve it as static files from any web
|
||||
server, at the domain root **or any subdirectory** (e.g. `example.com/games/`).
|
||||
All asset paths are relative and anchored by `<base href="./">`, so no
|
||||
configuration is needed. The only runtime files are `index.html`, `styles.css`,
|
||||
and the `assets/`, `data/`, and `src/` directories.
|
||||
|
||||
To deploy into a parent site, copy those into the desired subdirectory:
|
||||
|
||||
```
|
||||
cp -r index.html styles.css *.png favicon.ico assets data src /var/www/parent-site/games/
|
||||
```
|
||||
|
||||
For local testing:
|
||||
|
||||
```
|
||||
npm run serve # python3 -m http.server on http://localhost:8000
|
||||
```
|
||||
|
||||
The `tools/` directory holds offline dev tooling — puzzle generators and
|
||||
`verify*` self-play scripts — run with plain `node` from the repo root
|
||||
(e.g. `node tools/verifyBookwork.js`). It is not part of the deployed site.
|
||||
**This is a fully static site.** There is no server, no database, no accounts,
|
||||
no build step, and no `npm install` required to run it. The frontend uses native
|
||||
browser ES modules and loads Phaser from a CDN. Everything that used to be a
|
||||
backend round-trip — the single local player profile, casino chip balance, match
|
||||
history, puzzle progress, and word-game dictionaries/AI — now runs entirely in
|
||||
the browser against `localStorage` and the ported engines in `src/words/`.
|
||||
|
||||
---
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Features](#features)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Quick start](#quick-start)
|
||||
- [Configuration (`.env`)](#configuration-env)
|
||||
- [Running the server](#running-the-server)
|
||||
- [Deployment / hosting](#deployment--hosting)
|
||||
- [The game catalog](#the-game-catalog)
|
||||
- [Project layout](#project-layout)
|
||||
- [Database schema](#database-schema)
|
||||
- [REST API](#rest-api)
|
||||
- [Frontend architecture](#frontend-architecture)
|
||||
- [Architecture](#architecture)
|
||||
- [Scene flow](#scene-flow)
|
||||
- [The local "backend"](#the-local-backend)
|
||||
- [Word games](#word-games)
|
||||
- [Assets](#assets)
|
||||
- [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.
|
||||
- [Developer tooling (`tools/`)](#developer-tooling-tools)
|
||||
- [Data files (`data/`)](#data-files-data)
|
||||
|
||||
---
|
||||
|
||||
## Quick start
|
||||
|
||||
No dependencies to install. You just need a static file server, because ES
|
||||
modules can't be loaded over `file://`.
|
||||
|
||||
```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
|
||||
# Any static server works. A couple of one-liners:
|
||||
npm run serve # python3 -m http.server 8000 → http://localhost:8000
|
||||
./start_web.sh # python3 -m http.server 3000 → http://localhost:3000
|
||||
npx serve . # if you prefer Node
|
||||
```
|
||||
|
||||
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.
|
||||
Then open the printed URL in a modern browser. That's it — pick a game and play.
|
||||
|
||||
> **Requirements:** a current browser with ES-module + importmap support
|
||||
> (Chrome/Edge/Firefox/Safari, ~2023+). Python 3 (or any static server) for local
|
||||
> hosting. Node.js 20+ is only needed for the optional dev tooling in `tools/`.
|
||||
|
||||
---
|
||||
|
||||
## Configuration (`.env`)
|
||||
## Deployment / hosting
|
||||
|
||||
All configuration lives in `.env` at the project root. Use `example.env` as
|
||||
the template. Fields:
|
||||
**The repository root _is_ the site.** Serve it as static files from any web
|
||||
host, CDN, or object store (GitHub Pages, Netlify, Cloudflare Pages, S3, nginx,
|
||||
Apache, …). No configuration, environment variables, or runtime are needed.
|
||||
|
||||
### 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
|
||||
Because every asset path is relative and `index.html` sets `<base href="./">`,
|
||||
the site works unchanged from a domain root **or any subdirectory**
|
||||
(e.g. `example.com/games/`). To drop it into an existing site:
|
||||
|
||||
```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
|
||||
cp -r index.html styles.css *.png favicon.ico assets data src \
|
||||
/var/www/parent-site/games/
|
||||
```
|
||||
|
||||
The server serves both the API (`/api/*`) and the static frontend (`/`,
|
||||
`/src/...`, `/uploads/...`) on the same port.
|
||||
The only files the deployed site needs are `index.html`, `styles.css`, the
|
||||
favicons/PWA icons in the root, and the `assets/`, `data/`, and `src/`
|
||||
directories. The `tools/` directory is dev-only and never shipped.
|
||||
|
||||
After it starts you should see:
|
||||
Player state (profile, chips, history, puzzle progress) lives in each visitor's
|
||||
own browser `localStorage` under the `fcg:` prefix — nothing is stored or synced
|
||||
server-side, so hosting is entirely stateless.
|
||||
|
||||
```
|
||||
[server] listening on http://0.0.0.0:3000
|
||||
```
|
||||
---
|
||||
|
||||
## The game catalog
|
||||
|
||||
77 games are registered in `src/data/gamesRegistry.js` and grouped into five
|
||||
menu categories:
|
||||
|
||||
| Category | Count | Examples |
|
||||
|----------|-------|----------|
|
||||
| **Tabletop** | 21 | Backgammon, Chess, Checkers, Go, Othello, Settlers of Catan, Ticket to Ride, Risk, Monopoly, Blokus, Labyrinth, Mahjong, Stratego, Battleship, Mastermind, Connect 4, Forbidden Island, Azul, Chinese Checkers, Mexican Train, Parchisi |
|
||||
| **Cards** | 18 | Cribbage, Gin Rummy, Rummikub, Canasta, Hearts, Uno, Phase 10, Skip-Bo, Go Fish, Old Maid, Nerts, Dominion, Splendor, Freecell, Solitaire Tour, Spire Climb, Zahtzee, Farkle |
|
||||
| **Casino** | 9 | Blackjack, Texas Hold 'Em, Baccarat, Pai Gow Poker, Video Poker, Craps, Roulette, Bingo, Slot Machines |
|
||||
| **Word** | 15 | Wordle Race, Scrabble, Boggle, Ghost, Word Ladder, Word Search, Hangman, Spelling Bee, Sudoku, Mini Crossword, Tectonic, Bookwork, Kiitos, Tri-Ominoes, Jumble |
|
||||
| **Logic & Puzzle** | 14 | 2048, Rush Hour, Hexsweeper, Jell-o Monsters, Shift, Mahjong Match, Jewel Quest, Zuma, Bejeweled Blitz, Mini Motorways, Dot Link, Katamino, Genius Square, Block Fighter |
|
||||
|
||||
Each game is a self-contained `Phaser.Scene` (plus its own logic/AI/data helper
|
||||
modules) under `src/games/<slug>/`. Games are single-player against 0–7
|
||||
configurable AI opponents depending on the title; several ship a video tutorial.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -187,291 +101,189 @@ After it starts you should see:
|
|||
|
||||
```
|
||||
fertig-classic-games/
|
||||
├── example.env Configuration template (commit this)
|
||||
├── .env Your local configuration (gitignored)
|
||||
├── package.json
|
||||
├── README.md
|
||||
├── index.html Entry point: importmap for Phaser + <base href="./">
|
||||
├── styles.css Page chrome (canvas container + DOM overlay layer)
|
||||
├── package.json "serve" script; no runtime deps
|
||||
├── start_web.sh Convenience local server (python http.server)
|
||||
├── favicon*/apple-touch/… Favicons and PWA icons
|
||||
│
|
||||
├── 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
|
||||
├── src/ The entire application (native ES modules)
|
||||
│ ├── main.js Phaser.Game config; registers every scene; DOM-overlay sync
|
||||
│ ├── config.js Canvas dimensions (1920×1080) and the UI color palette
|
||||
│ ├── data/
|
||||
│ │ └── gamesRegistry.js The catalog: registerGame() for all 77 games
|
||||
│ ├── scenes/ Shell scenes (menu, profile, opponent select, room)
|
||||
│ ├── services/ api.js → localApi.js → localStore.js (the local backend)
|
||||
│ ├── ui/ Reusable widgets: Button, Modal, TextInput, Portrait,
|
||||
│ │ music/speech/tutorial/fullscreen helpers
|
||||
│ ├── words/ Client-side word-game engines + wordRouter dispatcher
|
||||
│ └── games/<slug>/ One folder per game (Scene + Logic + AI + Data + tutorial.md)
|
||||
│
|
||||
├── 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/ Static JSON game data + word lists (fetched at runtime)
|
||||
│ ├── *.json Per-game level banks, artwork maps, opponents, music, etc.
|
||||
│ ├── crosswords/ Mini-crossword puzzle bank
|
||||
│ └── wordlists/ enable1.txt (dictionary) + common.txt
|
||||
│
|
||||
└── data/ SQLite database (gitignored)
|
||||
└── fertig.sqlite
|
||||
├── assets/ Runtime media
|
||||
│ ├── images/ Spritesheets, backgrounds, board art (81 files)
|
||||
│ ├── fonts/ Bitmap/web fonts
|
||||
│ ├── fx/ Sound effects (71 files)
|
||||
│ ├── music/ Background music tracks (21 files)
|
||||
│ ├── speech/ AI opponent voice clips (393 files)
|
||||
│ ├── videos/ In-game portrait/animation clips (90 files)
|
||||
│ └── tutorial-videos/ How-to-play videos (17 files)
|
||||
│
|
||||
└── tools/ Offline dev scripts (run with node; NOT deployed)
|
||||
├── gen*.js Puzzle/level-bank generators
|
||||
└── verify*.js Headless self-play verification harnesses
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database schema
|
||||
## Architecture
|
||||
|
||||
Created by `server/db/migrations/001_init.sql`. Run `npm run migrate` to apply
|
||||
any pending migrations.
|
||||
The frontend is deliberately buildless: `index.html` declares an
|
||||
`<script type="importmap">` that maps the bare specifier `phaser` to a CDN ES
|
||||
module, and everything else uses relative `import` statements. The Phaser canvas
|
||||
is a fixed **1920×1080** design surface scaled to any viewport with
|
||||
`Phaser.Scale.FIT` + `CENTER_BOTH`.
|
||||
|
||||
- **`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`
|
||||
### Scene flow
|
||||
|
||||
Add new migrations as `server/db/migrations/00N_description.sql` — they are
|
||||
applied in lexicographic order and tracked in `schema_migrations`.
|
||||
Scenes live in `src/scenes/` and run in this order:
|
||||
|
||||
---
|
||||
```
|
||||
Boot → Preload → Landing → (Profile | GameMenu) → OpponentSelect → GameRoom → <ConcreteGame>
|
||||
```
|
||||
|
||||
## REST API
|
||||
- **BootScene / PreloadScene** — bootstrap and load shared assets.
|
||||
- **LandingScene** — title screen with menu music and fullscreen toggle.
|
||||
- **ProfileScene** — edit the single local profile (name, avatar, bio).
|
||||
- **GameMenuScene** — browse the catalog by category.
|
||||
- **OpponentSelectScene** — choose opponent count/skill, playfield, card back,
|
||||
and any per-game options (expansion, difficulty, word length, …).
|
||||
- **GameRoomScene** — a thin router. Its `slugDispatch` map turns the selected
|
||||
game's `slug` into a scene key and starts the concrete game scene, forwarding
|
||||
the chosen setup as scene-init data.
|
||||
|
||||
All endpoints are JSON. Session is carried by the `fcg_sid` cookie
|
||||
automatically; the client uses `credentials: 'same-origin'` in fetch calls.
|
||||
Every concrete game scene is registered in the `scene` array in `src/main.js`.
|
||||
|
||||
### Auth — `/api/auth`
|
||||
`main.js` also pins Phaser's DOM-overlay container to the real canvas box on
|
||||
every resize (`syncDomContainer`), so DOM elements like text inputs and video
|
||||
portraits line up exactly with their in-canvas targets under `Scale.FIT`.
|
||||
|
||||
| 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. |
|
||||
### The local "backend"
|
||||
|
||||
### Profile — `/api/profile`
|
||||
There is no HTTP backend. The chokepoint `src/services/api.js` exposes the same
|
||||
`api.get/post/patch/delete/upload` surface the old server client had, but instead
|
||||
of `fetch` it calls `src/services/localApi.js`, an in-browser shim that mirrors
|
||||
the old REST responses (`{ status, data }`). This kept ~135 call sites across the
|
||||
games working unchanged when the server was removed.
|
||||
|
||||
| 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`. |
|
||||
`localApi.dispatch(method, path, …)` handles the former endpoints against
|
||||
`localStorage`:
|
||||
|
||||
### History — `/api/history`
|
||||
| Former route | Now backed by |
|
||||
|--------------|---------------|
|
||||
| `/auth/*`, `/profile*` | A single implicit local profile in `localStore.js` |
|
||||
| `/profile/chips*` | Casino chip balance (default 2000; low-balance reset) |
|
||||
| `/games` | `gamesRegistry.listGames()` |
|
||||
| `/history*` | Match results array (bounded to last 100) |
|
||||
| `/puzzles/:slug/{progress,complete,reset}` | Ordered per-game level progress |
|
||||
| `/words/*` | Delegated to `src/words/wordRouter.js` |
|
||||
|
||||
| Method | Path | Auth | Description |
|
||||
|--------|-------|----------|------------------------------------------------------------|
|
||||
| GET | `/` | Required | Last 100 matches for the user, plus `{ wins, losses, draws }`. |
|
||||
`src/services/localStore.js` is the typed `localStorage` wrapper. All keys are
|
||||
namespaced under `fcg:` (`fcg:profile`, `fcg:history`, `fcg:puzzleProgress`) so
|
||||
player data is easy to find, export, or clear.
|
||||
|
||||
### Misc
|
||||
### Word games
|
||||
|
||||
| Method | Path | Auth | Description |
|
||||
|--------|----------------|------|------------------------------------------------------|
|
||||
| GET | `/api/health` | — | `{ ok: true }`. |
|
||||
| GET | `/api/games` | — | Lists registered games from `games/registry.js`. |
|
||||
Word games need a dictionary and move-search logic that used to live on the
|
||||
server. Those engines were ported verbatim into `src/words/` (Scrabble, Ghost,
|
||||
Word Ladder, Word Search, Sudoku, Tectonic, Boggle, Spelling Bee, Mini Crossword,
|
||||
Kiitos, …). `wordRouter.js` fetches the dictionaries from `data/wordlists/` once
|
||||
in the browser and routes `/words/*` calls to the right engine, returning the
|
||||
same response shapes the old Express routes did.
|
||||
|
||||
---
|
||||
### Assets
|
||||
|
||||
## 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.
|
||||
Media is loaded from `assets/` (spritesheets, backgrounds, SFX, music, AI voice
|
||||
clips, tutorial and portrait videos). Per-game data — level banks, artwork maps,
|
||||
opponent rosters, music playlists — is JSON in `data/`, fetched at runtime. Many
|
||||
games render primarily with vector/procedural graphics and layer optional
|
||||
drop-in spritesheets on top, so art can be added without refactoring scenes.
|
||||
|
||||
---
|
||||
|
||||
## Adding a new game
|
||||
|
||||
1. **Register the game on the server.** In `server/games/registry.js`:
|
||||
1. **Register it** in `src/data/gamesRegistry.js`:
|
||||
|
||||
```js
|
||||
registerGame({
|
||||
slug: 'cribbage',
|
||||
name: 'Cribbage',
|
||||
category: 'tabletop', // or 'casino' or 'cards'
|
||||
minPlayers: 2,
|
||||
maxPlayers: 4,
|
||||
minOpponents: 1,
|
||||
maxOpponents: 3,
|
||||
category: 'cards', // tabletop | cards | casino | word | logic
|
||||
cardGame: true, // uses the shared card-back picker
|
||||
minPlayers: 2, maxPlayers: 2,
|
||||
minOpponents: 1, maxOpponents: 1,
|
||||
defaultOpponents: null, // optional preselected opponent count
|
||||
hasTutorial: true, // shows the how-to-play video button
|
||||
iconFrame: 64, // frame index into the game-icons spritesheet
|
||||
});
|
||||
```
|
||||
|
||||
The game menu picks it up automatically.
|
||||
The menu, opponent-select screen, and history all pick it up from here.
|
||||
|
||||
2. **Implement the game scene.** Each game is a `Phaser.Scene` that reads its
|
||||
setup from the data passed by `GameRoomScene`:
|
||||
2. **Implement the scene** at `src/games/<slug>/<Name>Game.js`. It's a
|
||||
`Phaser.Scene` whose `init(data)` receives the chosen setup (`opponents`,
|
||||
`playfield`, `cardBack`, `difficulty`, expansion/word-length options, …) and
|
||||
whose `create()` renders the board and runs the game + AI locally. Follow the
|
||||
established per-game module split (e.g. `<Name>Logic.js`, `<Name>AI.js`,
|
||||
`<Name>Data.js`) so the scene stays presentation-focused. Add a `tutorial.md`
|
||||
if applicable.
|
||||
|
||||
```js
|
||||
// public/src/games/cribbage/CribbageGame.js
|
||||
import * as Phaser from 'phaser';
|
||||
3. **Wire it up:**
|
||||
- import and add the scene class to the `scene` array in `src/main.js`;
|
||||
- add the `slug → SceneKey` entry to `slugDispatch` in
|
||||
`src/scenes/GameRoomScene.js`.
|
||||
|
||||
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.
|
||||
4. **Persist progress (optional):** for level-based puzzles, call
|
||||
`/puzzles/<slug>/{progress,complete,reset}`; to record win/loss/draw, POST to
|
||||
`/history/single-player`. Both are served by the local API shim.
|
||||
|
||||
---
|
||||
|
||||
## Email verification
|
||||
## Developer tooling (`tools/`)
|
||||
|
||||
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:
|
||||
Offline Node scripts, not part of the deployed site. Run them with plain `node`
|
||||
from the repo root (Node.js 20+):
|
||||
|
||||
```
|
||||
[mailer:dev] Verification link for user@example.com:
|
||||
http://localhost:3000/api/auth/verify?token=...
|
||||
```
|
||||
- **Generators** (`genRushHour.js`, `genZuma.js`, `genKatamino.js`,
|
||||
`genMiniCrossword.js`, `genJumble.js`, `buildCommonWords.js`, …) produce the
|
||||
level banks and word lists that live in `data/`.
|
||||
- **Verifiers** (`verifyCribbage.js`, `verifyRisk.js`, `verifyRummikub.js`, …)
|
||||
are headless self-play/assertion harnesses that exercise each game's engine and
|
||||
AI to catch regressions. They exit non-zero on failure:
|
||||
|
||||
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>"
|
||||
```bash
|
||||
node tools/verifyCribbage.js
|
||||
node tools/verifyRisk.js --games=300
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Profile pictures
|
||||
## Data files (`data/`)
|
||||
|
||||
- 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.
|
||||
Runtime JSON fetched by the games:
|
||||
|
||||
---
|
||||
- **Per-game banks & artwork maps** — e.g. `rushhour.json`, `zuma.json`,
|
||||
`katamino.json`, `dotlink.json`, `jumble.json`, `spireclimb-artwork.json`,
|
||||
`slots-artwork.json`, `shift-artwork.json`.
|
||||
- **Shared UI options** — `playfields.json`, `colored-playfields.json`,
|
||||
`card-backs.json`, `opponents.json`, `music.json`.
|
||||
- **Word data** — `wordlists/enable1.txt` (main dictionary),
|
||||
`wordlists/common.txt` (curated common words), `crosswords/minicrossword.json`.
|
||||
|
||||
## 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)).
|
||||
All are static assets; editing them (or regenerating them via `tools/`) changes
|
||||
game content without touching code.
|
||||
|
|
|
|||
Loading…
Reference in New Issue