Thanks for your interest in NoFi Games! This document explains how to set up the project locally, the standards we hold contributions to, and how to get a change merged.
Be kind, be constructive, assume good intent. We follow the spirit of the Contributor Covenant. Harassment, personal attacks, and discriminatory language are not tolerated.
NoFi Games is an offline-first casual games collection built with:
- Vite + TypeScript (strict mode)
- HTML5 Canvas 2D (custom lightweight engine, no game framework)
- IndexedDB via
idb-keyvalfor offline storage - Web Audio API for procedural sound (no audio files)
- Vitest + jsdom for tests
- Capacitor (iOS/Android) and Tauri v2 (desktop) for native builds
For deeper architectural notes, read CLAUDE.md.
git clone https://github.com/IvoryHeart/nofi-games.git
cd nofi-games
npm install
npm run dev # http://localhost:5173Other useful scripts:
npm run build # Production build (tsc + vite)
npm run preview # Preview the built bundle
npm test # Run the full test suite once
npm run test:watch # Vitest in watch mode- Fork the repo and create a topic branch off
main:git checkout -b feat/my-new-game
- Make your change with tests (see below).
- Push the branch and open a PR against
main. - CI runs
tsc --noEmit,npm test, andnpm run buildon every PR. All checks must be green before review. - Vercel automatically creates a preview deployment for your PR — please test the live preview before requesting review.
Branch name suggestions: feat/..., fix/..., docs/..., refactor/..., test/....
Every change must include tests. This is a hard rule.
Coverage targets:
| Area | Target |
|---|---|
src/engine/, src/storage/, src/games/registry.ts, src/utils/audio.ts, src/utils/haptics.ts |
100% |
src/app.ts, src/main.ts |
90%+ |
Individual games in src/games/<name>/ |
80%+ |
Run coverage locally:
npx vitest run --coverageTest layout:
tests/unit/— pure logic in isolation. Mockidb-keyval, canvas, etc.tests/integration/— game registration, instantiation at all four difficulties, cross-module behavior.tests/functional/— app-level flows: navigation, settings, offline persistence, build artifacts.
The canvas mock for jsdom is in tests/setup.ts and is auto-loaded by vitest.config.ts.
- Create
src/games/<name>/<Name>.ts. - Extend
GameEngineand implementinit(),update(dt),render(). - Call
registerGame({...})at module level (self-registration pattern). - Add a dynamic import to
loadAllGames()insrc/games/registry.ts. - Use
this.width/this.heightfor all coordinates — never hardcode pixel positions. - Map
this.difficulty(0–3) to meaningful gameplay changes (grid sizes, speeds, mechanics). - Trigger sound via
this.playSound('name')for key game events. - Write tests across all four difficulty levels in
tests/integration/.
- TypeScript strict mode is on. Fix the type, don't reach for
any. - No infinite loops — every
whilemust have a guaranteed termination condition or iteration cap. - Guard against NaN/zero in canvas math:
Math.max(value, minimum). - Warm color palette only — see
src/styles/theme.css. Never introduce cool blue/gray. - Time-based animations — use
dt(delta seconds), never frame counts. - Clean up listeners via
GameEngine.destroy(). Don't add listeners outside the engine. - Comment only where logic isn't self-evident. Don't narrate obvious code.
Keep them short, imperative, and meaningful:
Add diagonal-swap mechanic to Gem Swap
Fix Bubble Pop infinite loop in renderAimLine
Bump test coverage for Sudoku to 98%
We don't enforce a strict format (Conventional Commits is welcome but not required).
Open an issue with:
- What you expected to happen
- What actually happened
- Steps to reproduce (which game, difficulty, browser/platform)
- Console errors if any
Open an issue tagged enhancement describing the feature and why it fits NoFi's offline-first, lightweight philosophy. Bundle size and dependency count are taken seriously — we keep the app under 200KB for a reason.
Open a discussion or issue on GitHub. Thanks for contributing!