Go to file
Brian Fertig 7713089466 feat: add per-game soundtracks for multiple games
Introduce game-specific music overrides using the soundtrack service.
Adds arcadedark and hacker music packs, and wires them to Balatro,
Colorado Defense, Tempest, Mastermind, Hexsweeper, Dot Link, and 2048.
Games now lazy-load their soundtrack assets on entry instead of sharing
a single generic music cache.
2026-07-18 17:21:32 -06:00
.vscode
assets feat: add per-game soundtracks for multiple games 2026-07-18 17:21:32 -06:00
data feat: add per-game soundtracks for multiple games 2026-07-18 17:21:32 -06:00
src feat: add per-game soundtracks for multiple games 2026-07-18 17:21:32 -06:00
tools
.gitignore
README.md
android-chrome-192x192.png
android-chrome-512x512.png
apple-touch-icon.png
favicon-16x16.png
favicon-32x32.png
favicon.ico
index.html
package.json
start_web.sh
styles.css

README.md

Fertig Classic Games

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.

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


Quick start

No dependencies to install. You just need a static file server, because ES modules can't be loaded over file://.

git clone <this-repo>
cd fertig-classic-games

# 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

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


Deployment / hosting

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.

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:

cp -r index.html styles.css *.png favicon.ico assets data src \
      /var/www/parent-site/games/

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.

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.


The game catalog

82 games are registered in src/data/gamesRegistry.js and grouped into six 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
Arcade, Console & PC 2 Colorado Defense, Star Control

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 07 configurable AI opponents depending on the title; several ship a video tutorial.


Project layout

fertig-classic-games/
├── 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
│
├── 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)
│
├── 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
│
├── 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

Architecture

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.

Scene flow

Scenes live in src/scenes/ and run in this order:

Boot → Preload → Landing → (Profile | GameMenu) → OpponentSelect → GameRoom → <ConcreteGame>
  • 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.

Every concrete game scene is registered in the scene array in src/main.js.

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.

The local "backend"

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.

localApi.dispatch(method, path, …) handles the former endpoints against localStorage:

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

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.

Word games

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

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 it in src/data/gamesRegistry.js:

    registerGame({
      slug: 'cribbage',
      name: 'Cribbage',
      category: 'cards',        // tabletop | cards | casino | word | logic | arcade-console-pc
      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 menu, opponent-select screen, and history all pick it up from here.

  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.

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


Developer tooling (tools/)

Offline Node scripts, not part of the deployed site. Run them with plain node from the repo root (Node.js 20+):

  • 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:

    node tools/verifyCribbage.js
    node tools/verifyRisk.js --games=300
    

Data files (data/)

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 optionsplayfields.json, colored-playfields.json, card-backs.json, opponents.json, music.json.
  • Word datawordlists/enable1.txt (main dictionary), wordlists/common.txt (curated common words), crosswords/minicrossword.json.

All are static assets; editing them (or regenerating them via tools/) changes game content without touching code.