6 KiB
AGENTS.md
Guidance for AI coding agents working in this repository.
What this is
Classic Minesweeper as a vanilla web game — no framework, no TypeScript (JSDoc + // @ts-check only). Deployed at mnswpr.com (Netlify) and published to npm as @ayo-run/mnswpr. The game engine has zero runtime dependencies; only the website adds Firebase.
Commands
pnpm i # install (pnpm is required — this is a pnpm workspace)
pnpm dev # run the website dev server (vite app) — most common
pnpm build # build the website -> app/dist
pnpm build:lib # build the publishable library -> lib/dist
pnpm lint # eslint . (JS + CSS); runs automatically on pre-commit
pnpm lint:fix # eslint --fix
pnpm build:preview # build the app and serve the production preview
There is no test suite (pnpm test is a no-op placeholder). Verify changes by running pnpm dev and playing.
Node version: .nvmrc pins lts/*.
Repository layout (pnpm workspace)
Workspaces are declared in pnpm-workspace.yaml as lib and app. Note that utils/ is not a workspace package — it's a plain shared folder imported by relative path from both lib and app.
lib/—@ayo-run/mnswpr, the standalone, framework-free game engine. This is what gets published to npm.lib/mnswpr.jsis the whole engine;lib/levels.jsdefines the four difficulty presets. Depends only onutils/.app/— the mnswpr.com website. Consumes the library viaworkspace:*(import mnswpr from '@ayo-run/mnswpr/mnswpr.js') and adds the Firebase leaderboard.app/main.jswires the engine to the leaderboard through the engine's hooks.utils/— shared services with no dependencies, re-exported fromutils/index.js:StorageService(localStorage/sessionStorage JSON wrapper),TimerService(game clock +pretty()time formatting used by both engine and leaderboard),LoggerService,LoadingService.
Architecture
The engine is decoupled from the app via two hooks. Minesweeper(appId, version, hooks) (lib/mnswpr.js) is a classic constructor function that imperatively builds a <table> grid in the DOM. It knows nothing about Firebase or leaderboards. The app injects behavior through:
hooks.levelChanged(setting)— fired when the difficulty level changes; the app uses this to re-fetch and render the leaderboard for that level.hooks.gameDone(game)— fired when a game ends (win or loss) with agameobject (time,status,level,time_stamp,isMobile); the app uses this to submit the score.
When adding engine features that the website needs to react to, prefer adding a new hook over reaching into the app — that separation is what keeps the library publishable on its own. (There are already TODO markers in the engine for an afterGridGenerated hook.)
Game state lives in DOM attributes, not a JS model. The grid's overall state is the game-status attribute on the <table> (inactive → active → over/win → done). Each cell carries data-status (default, highlighted, flagged, clicked, empty) and data-value (adjacent mine count). Mine positions are the one exception: kept in minesArray as [row, col] pairs. When changing game logic, read/write these attributes consistently — helpers like getStatus/setStatus, isMine, isFlagged are the intended accessors.
First-click safety: the first clicked cell is never a mine — if it is, transferMine() relocates it to a non-neighboring empty cell before revealing.
Input handling is intricate. Mouse (left/right/middle, plus simultaneous left+right "chording") and touch (long-press to flag) are handled through a state machine of flags (isLeft, isRight, pressed, bothPressed, skip, isBusy) in initializeEventHandlers/initializeTouchEventHandlers. isBusy debounces input (MOBILE_BUSY_DELAY/PC_BUSY_DELAY). Tread carefully here — small changes easily break chording or mobile flagging.
Test mode: set TEST_MODE = true at the top of lib/mnswpr.js to render mine positions as visual hints and enable debug logging.
Leaderboard / Firebase (app/modules/)
LeaderBoardService (leader-board.js) reads/writes Firestore (firebase/firestore/lite). Structure: top-10 per level in mw-leaders/{level}/games, all sessions in mw-all/{browserId}/games, and remote runtime configuration in mw-config. A score is only offered to the leaderboard when it beats the current 10th place and matches the server-side passingStatus.
The Firebase config in leader-board.js is intentionally public and committed — for a client-only Firebase app the API key is not a secret (access is governed by Firestore security rules), so don't treat it as a leaked credential or try to move it to env vars.
UserService (user.js) derives a non-cryptographic browserId fingerprint from navigator/screen properties to attribute scores without accounts.
Conventions
- Code style is enforced by ESLint Stylistic, not Prettier: 2-space indent, single quotes, no semicolons, no trailing commas, spaces inside
{ braces }but not[brackets]. Runpnpm lint:fixbefore committing. Both**/*.jsand**/*.cssare linted (CSS via@eslint/css). - The engine uses plain functions and
var/letclosures, not classes;utils/andapp/modules/use ES classes. Match the surrounding style of the file you edit.
Release & git hooks (maintainer workflow)
- Husky hooks:
pre-commitrunspnpm lint;post-commitauto-pushes to two extra remotes (git push gh,git push sh). If those remotes aren't configured locally, expect post-commit failures — that's environmental, not a code problem. - Releasing (
pnpm release) builds the lib, runsbumpp(version bump + tag) inlib/, thenscripts/release.jsforce-pushes areleasebranch and tags to remotesgh/sh. Pushing av*tag triggers.github/workflows/release.yml(changelogithub) to publish GitHub release notes. Only run this when explicitly releasing.