diff --git a/.gitignore b/.gitignore index 9d8e475..4bbfe89 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,14 @@ node_modules/ dist/ -# generated at publish time from the root README.md (see scripts/publish-lib.js) -lib/README.md +# generated at publish time from apps/mnswpr/README.md (see scripts/publish-lib.js) +packages/mnswpr/README.md .claude # Production / local Firebase config values are not committed — set them as # Netlify env vars, or keep them in a local, gitignored env file. Dev values -# live in the committed app/.env.development (public, non-secret keys). +# live in the committed apps/mnswpr/.env.development (public, non-secret keys). .env.production .env.local .env.*.local diff --git a/AGENTS.md b/AGENTS.md index dee5dd2..42b6468 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,9 +10,9 @@ Classic Minesweeper as a vanilla web game — no framework, no TypeScript (JSDoc ```bash 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 dev # run the website dev server (vite apps/mnswpr) — most common +pnpm build # build the website -> apps/mnswpr/dist +pnpm build:lib # build the publishable library -> packages/mnswpr/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 @@ -20,21 +20,23 @@ pnpm test # run the Vitest suite once (jsdom) pnpm test:watch # run Vitest in watch mode ``` -Tests live in `test/` and run under **Vitest** with a jsdom environment (config in `vitest.config.js`). They cover the shared utils and drive the engine through real DOM events (mount `#app`, dispatch mouse events, assert on cell/grid attributes). For anything visual or input-timing related, also verify by running `pnpm dev` and playing. +Tests are co-located with the package they exercise (`packages/utils/test/`, `packages/mnswpr/test/`) and run under **Vitest** with a jsdom environment (root config in `vitest.config.js`). They cover the shared utils and drive the engine through real DOM events (mount `#app`, dispatch mouse events, assert on cell/grid attributes). For anything visual or input-timing related, also verify by running `pnpm dev` and playing. Node version: `.nvmrc` pins `lts/*`. -## Repository layout (pnpm workspace) +## Repository layout (Cozy Games monorepo, 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`. +This is the **Cozy Games** monorepo. Workspaces are declared in `pnpm-workspace.yaml` as `apps/*`, `packages/*`, and `sites/*`. `utils/` is now a real workspace package (`@cozy-games/utils`), imported by name — no more `../utils` relative paths. -- **`lib/`** — `@ayo-run/mnswpr`, the standalone, framework-free game engine. This is what gets published to npm. `lib/mnswpr.js` is the whole engine; `lib/levels.js` defines the four difficulty presets. Depends only on `utils/`. -- **`app/`** — the mnswpr.com website. Consumes the library via `workspace:*` (`import mnswpr from '@ayo-run/mnswpr/mnswpr.js'`) and adds the Firebase leaderboard. `app/main.js` wires the engine to the leaderboard through the engine's hooks. -- **`utils/`** — shared services with no dependencies, re-exported from `utils/index.js`: `StorageService` (localStorage/sessionStorage JSON wrapper), `TimerService` (game clock + `pretty()` time formatting used by both engine and leaderboard), `LoggerService`, `LoadingService`. +- **`apps/mnswpr/`** — `@ayo-run/mnswpr`'s host, the mnswpr.com website. Consumes the engine and leaderboard via `workspace:*` (`import mnswpr from '@ayo-run/mnswpr/mnswpr.js'`) and wires them together in `apps/mnswpr/main.js`. Owns its Firebase config (`firebase.json`, `firestore.rules`, `.firebaserc`) and app-specific scripts (`apps/mnswpr/scripts/`). A future app (e.g. sudoku) gets its own `apps//`. +- **`packages/mnswpr/`** — `@ayo-run/mnswpr`, the standalone, framework-free game engine published to npm. `packages/mnswpr/mnswpr.js` is the whole engine; `levels.js` defines the four difficulty presets. Depends only on `@cozy-games/utils`. +- **`packages/leaderboard/`** — `@cozy-games/leaderboard`, a backend-agnostic, time-windowed leaderboard (adapter-injected storage). +- **`packages/utils/`** — `@cozy-games/utils`, shared services with no dependencies, re-exported from `index.js`: `StorageService`, `TimerService` (`pretty()` time formatting used by both engine and leaderboard), `LoggerService`, `LoadingService`, and date-bucket helpers. +- **`sites/`** — docs (Astro Starlight) and UI demos. Placeholders for now. ## 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 `` grid in the DOM. It knows nothing about Firebase or leaderboards. The app injects behavior through: +**The engine is decoupled from the app via two hooks.** `Minesweeper(appId, version, hooks)` (`packages/mnswpr/mnswpr.js`) is a classic constructor function that imperatively builds a `
` 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 a `game` object (`time`, `status`, `level`, `time_stamp`, `isMobile`); the app uses this to submit the score. @@ -47,9 +49,9 @@ When adding engine features that the website needs to react to, prefer adding a **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. +**Test mode:** set `TEST_MODE = true` at the top of `packages/mnswpr/mnswpr.js` to render mine positions as visual hints and enable debug logging. -## Leaderboard / Firebase (`app/modules/`) +## Leaderboard / Firebase (`apps/mnswpr/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`. @@ -60,9 +62,9 @@ The **Firebase config in `leader-board.js` is intentionally public and committed ## 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]`. Run `pnpm lint:fix` before committing. Both `**/*.js` and `**/*.css` are linted (CSS via `@eslint/css`). -- The engine uses **plain functions and `var`/`let` closures**, not classes; `utils/` and `app/modules/` use ES classes. Match the surrounding style of the file you edit. +- The engine uses **plain functions and `var`/`let` closures**, not classes; `packages/utils/` and `apps/mnswpr/modules/` use ES classes. Match the surrounding style of the file you edit. ## Release & git hooks (maintainer workflow) - **Husky hooks:** `pre-commit` runs `pnpm lint`; `post-commit` auto-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, runs `bumpp` (version bump + tag) in `lib/`, then `scripts/release.js` force-pushes a `release` branch and tags to remotes `gh`/`sh`. Pushing a `v*` tag triggers `.github/workflows/release.yml` (`changelogithub`) to publish GitHub release notes. Only run this when explicitly releasing. +- **Releasing** (`pnpm release`) builds the lib, runs `bumpp` (version bump + tag) in `packages/mnswpr/`, then `scripts/release.js` force-pushes a `release` branch and tags to remotes `gh`/`sh`. Pushing a `v*` tag triggers `.github/workflows/release.yml` (`changelogithub`) to publish GitHub release notes. Only run this when explicitly releasing. diff --git a/README.md b/README.md index eaac9df..5b48426 100644 --- a/README.md +++ b/README.md @@ -1,123 +1,40 @@ -> [!IMPORTANT] -> **mnswpr has a new home.** Development now happens in the **Cozy Games** monorepo: -> **[git.ayo.run/ayo/cozy-games](https://git.ayo.run/ayo/cozy-games)**. +# Cozy Games -# Play Minesweeper Online for Free -[![Netlify Status](https://api.netlify.com/api/v1/badges/172478bd-afc5-4e47-95ba-d9ab814248fb/deploy-status)](https://app.netlify.com/sites/mnswpr/deploys) +A monorepo for **Cozy Games** — a growing collection of small browser +games and the shared, reusable packages that power them. -Play it here: [mnswpr.com](https://mnswpr.com). This is the classic game **Minesweeper** built with vanilla web technologies (i.e., no framework dependency). +## Layout -![mnswpr gameplay](https://git.ayo.run/ayo/mnswpr/raw/branch/main/screenshot.png) - -## How to Play - -The goal is to reveal every safe cell without detonating a mine. Your **first click is always safe**. - -- **Left click** — reveal a cell -- **Right click** — flag / unflag a suspected mine -- **Left + right click together** (chording) — reveal the neighbors of a satisfied number -- **Touch** — tap to reveal, long-press to flag - - -## Ways to Use - -The web is a wonderful, free, and open platform to create and distribute value. You can use **mnswpr** in different ways: - -- as a deployed [web app](https://mnswpr.com) -- as a [library](https://npmx.dev/package/@ayo-run/mnswpr) with `npm i @ayo-run/mnswpr` -- as a `web component` (coming soon). - -Using it as a library takes only a few lines — mount it onto any element by `id`: - -```js -import '@ayo-run/mnswpr/mnswpr.css' -import mnswpr from '@ayo-run/mnswpr' - -const game = new mnswpr('app') -game.initialize() +``` +cozy-games/ +├── apps/ Playable games (each deploys independently) +│ └── mnswpr/ Minesweeper — mnswpr.com (@ayo-run/mnswpr engine + Firebase leaderboard) +├── packages/ Shared, publishable libraries +│ ├── mnswpr/ @ayo-run/mnswpr — the vanilla Minesweeper game engine +│ ├── leaderboard/ @cozy-games/leaderboard — backend-agnostic, time-windowed leaderboard +│ └── utils/ @cozy-games/utils — shared browser utilities (storage, timer, …) +└── sites/ Docs (Astro Starlight) and UI demos — placeholders for now ``` -## Tooling -The project has gone through years of existence. It started from 2019 when tooling was massively different. I have [modernized it](https://elk.zone/social.ayco.io/@ayo/116333804543330938) since and have witnessed how much easier and faster it is to build now - even without web frameworks or LLMs! +Each app owns its own backend config (e.g. mnswpr's Firestore rules live in +`apps/mnswpr/`); the shared packages stay backend-agnostic. -As of now the tooling I use are: -- [Vite](https://vite.dev/) for bundling and development server -- [Eslint](https://eslint.org) for JS linting & [CSS linting](https://eslint.org/blog/2025/02/eslint-css-support/) -- [ESLint Stylistic](https://eslint.style) for JS formatting -- [Husky](https://typicode.github.io/husky/) for git hooks -- [PNPM](https://pnpm.io/installation) for dependency & workspace management -- and a bunch of automation using scripts and Continuous Integration actions +## Getting started -Because a big part of this project's purpose is to track how the software development industry evolves — and because it has come a long way in modernizing along the way — I now also use it as a **playground for coding agents**. It's a small, framework-free, well-scoped codebase, which makes it a great sandbox to see how AI agents read, reason about, and change real code. To help them get their bearings quickly, the repo ships an [`AGENTS.md`](./AGENTS.md) describing the architecture and conventions. - -## Development - -Technology Stack: HTML, JS, and CSS; [Google Firebase](https://firebase.google.com) for leader board store; [Netlify](https://netlify.com) for hosting - -To start development, you need [`node`](https://nodejs.org/en/download). I highly recommend [`pnpm`](https://pnpm.io/installation) to be used as well. Once you know you have this, you can do the following: -1. Install dependencies: `pnpm i` -2. Start the dev server: `pnpm run dev` - -The rest of the everyday commands: +This is a [pnpm](https://pnpm.io) workspace (pnpm is required). ```bash -pnpm test # run the Vitest suite -pnpm lint # ESLint (JS + CSS) -pnpm lint:fix # ESLint with autofix -pnpm build # build the website -pnpm build:lib # build the publishable library +pnpm i # install +pnpm dev # run the mnswpr app (Vite dev server) +pnpm test # run all package tests (vitest) +pnpm lint # eslint +pnpm build # build the mnswpr app -> apps/mnswpr/dist +pnpm build:lib # build the engine package -> packages/mnswpr/dist ``` -### Leaderboard (local Firestore emulator) - -The leader board is backed by [Google Firestore](https://firebase.google.com). For local development the app talks to the **Firestore emulator** by default — fully local, no cloud, no deploy. The flag `VITE_FIRESTORE_EMULATOR=1` is already set in `app/.env.development`. - -You need a **JDK 21+** installed (the emulator runs on Java; `firebase-tools` itself is fetched on demand via `npx`). Then, in two terminals: - -```bash -pnpm emulators # terminal 1 — start the Firestore emulator on :8080 (+ UI) -pnpm seed:emulator # terminal 2, once — fill it with sample scores -pnpm dev # terminal 2 — run the app against the emulator -``` - -If the emulator isn't running, the board simply shows *"unavailable"* (a refused connection). To skip the emulator — for quick UI-only work, or if you don't have a JDK — set `VITE_FIRESTORE_EMULATOR=` (empty) in a local, gitignored `app/.env.local`; the app then uses the cloud `mw-test` namespace instead. - -See [`docs/firebase-leaderboards.md`](./docs/firebase-leaderboards.md) for the full data model, security rules, environments, and deployment. - -## Contributing - -Contributions are welcome! See [`AGENTS.md`](./AGENTS.md) for the architecture, conventions, and release workflow before opening a pull request. - -## You just want to play? - -*👉 The live site is here: [mnswpr.com](https://mnswpr.com)* - -## Background -One day, while working in my home office, I heard loud and fast mouse clicks coming from our bedroom. It's my wife, playing her favorite game (Minesweeper) on a crappy website full of advertisements. - -I can't allow this, it's a security issue. 🤣 - -But it is also an opportunity. - -I wanted to give her the same game, with a similar leader board she can dominate. And this is also a chance for me to dig deeper into vanilla JS. - -Can I make a page with complex interactions (more on this later) without any library dependency? - -## What I have learned: -1. JS is awesome ✨ -1. We don't always *need* JS frameworks (or TS) ✨ -1. Even subtle UI changes *can improve* user gameplay experience ✨ -1. There's more ways to break your app than you are initially aware of ✨ -1. Competition motivates users to use your app more ✨ -1. Hash in bundled filenames helps avoid issues with browser caching (when shipping versions fast) ✨ - - +See [apps/mnswpr/README.md](apps/mnswpr/README.md) for the game itself, and each package's +README for library usage. ## License -[BSD-2-Clause](./LICENSE) - ---- - -_Just keep building._
-_A project by [Ayo](https://ayo.ayco.io)_ +BSD-2-Clause © Ayo Ayco diff --git a/app/.env.development b/apps/mnswpr/.env.development similarity index 100% rename from app/.env.development rename to apps/mnswpr/.env.development diff --git a/app/.env.example b/apps/mnswpr/.env.example similarity index 100% rename from app/.env.example rename to apps/mnswpr/.env.example diff --git a/.firebaserc b/apps/mnswpr/.firebaserc similarity index 100% rename from .firebaserc rename to apps/mnswpr/.firebaserc diff --git a/lib/README.md b/apps/mnswpr/README.md similarity index 78% rename from lib/README.md rename to apps/mnswpr/README.md index 88781d8..8af5c50 100644 --- a/lib/README.md +++ b/apps/mnswpr/README.md @@ -3,12 +3,19 @@ Play it here: [mnswpr.com](https://mnswpr.com). This is the classic game **Minesweeper** built with vanilla web technologies (i.e., no framework dependency). - -![mnswpr gameplay](app/public/screenshot.png) +![mnswpr gameplay](https://git.ayo.run/ayo/mnswpr/raw/branch/main/screenshot.png) -Technology Stack: HTML, JS, and CSS; [Google Firebase](https://firebase.google.com) for leader board store; [Netlify](https://netlify.com) for hosting +## How to Play -## Usage +The goal is to reveal every safe cell without detonating a mine. Your **first click is always safe**. + +- **Left click** — reveal a cell +- **Right click** — flag / unflag a suspected mine +- **Left + right click together** (chording) — reveal the neighbors of a satisfied number +- **Touch** — tap to reveal, long-press to flag + + +## Ways to Use The web is a wonderful, free, and open platform to create and distribute value. You can use **mnswpr** in different ways: @@ -26,15 +33,6 @@ const game = new mnswpr('app') game.initialize() ``` -## How to Play - -The goal is to reveal every safe cell without detonating a mine. Your **first click is always safe**. - -- **Left click** — reveal a cell -- **Right click** — flag / unflag a suspected mine -- **Left + right click together** (chording) — reveal the neighbors of a satisfied number -- **Touch** — tap to reveal, long-press to flag - ## Tooling The project has gone through years of existence. It started from 2019 when tooling was massively different. I have [modernized it](https://elk.zone/social.ayco.io/@ayo/116333804543330938) since and have witnessed how much easier and faster it is to build now - even without web frameworks or LLMs! @@ -49,6 +47,9 @@ As of now the tooling I use are: Because a big part of this project's purpose is to track how the software development industry evolves — and because it has come a long way in modernizing along the way — I now also use it as a **playground for coding agents**. It's a small, framework-free, well-scoped codebase, which makes it a great sandbox to see how AI agents read, reason about, and change real code. To help them get their bearings quickly, the repo ships an [`AGENTS.md`](./AGENTS.md) describing the architecture and conventions. ## Development + +Technology Stack: HTML, JS, and CSS; [Google Firebase](https://firebase.google.com) for leader board store; [Netlify](https://netlify.com) for hosting + To start development, you need [`node`](https://nodejs.org/en/download). I highly recommend [`pnpm`](https://pnpm.io/installation) to be used as well. Once you know you have this, you can do the following: 1. Install dependencies: `pnpm i` 2. Start the dev server: `pnpm run dev` @@ -63,6 +64,22 @@ pnpm build # build the website pnpm build:lib # build the publishable library ``` +### Leaderboard (local Firestore emulator) + +The leader board is backed by [Google Firestore](https://firebase.google.com). For local development the app talks to the **Firestore emulator** by default — fully local, no cloud, no deploy. The flag `VITE_FIRESTORE_EMULATOR=1` is already set in `app/.env.development`. + +You need a **JDK 21+** installed (the emulator runs on Java; `firebase-tools` itself is fetched on demand via `npx`). Then, in two terminals: + +```bash +pnpm emulators # terminal 1 — start the Firestore emulator on :8080 (+ UI) +pnpm seed:emulator # terminal 2, once — fill it with sample scores +pnpm dev # terminal 2 — run the app against the emulator +``` + +If the emulator isn't running, the board simply shows *"unavailable"* (a refused connection). To skip the emulator — for quick UI-only work, or if you don't have a JDK — set `VITE_FIRESTORE_EMULATOR=` (empty) in a local, gitignored `app/.env.local`; the app then uses the cloud `mw-test` namespace instead. + +See [`docs/firebase-leaderboards.md`](./docs/firebase-leaderboards.md) for the full data model, security rules, environments, and deployment. + ## Contributing Contributions are welcome! See [`AGENTS.md`](./AGENTS.md) for the architecture, conventions, and release workflow before opening a pull request. diff --git a/docs/firebase-leaderboards.md b/apps/mnswpr/docs/firebase-leaderboards.md similarity index 100% rename from docs/firebase-leaderboards.md rename to apps/mnswpr/docs/firebase-leaderboards.md diff --git a/docs/leaderboard-env-migration.md b/apps/mnswpr/docs/leaderboard-env-migration.md similarity index 100% rename from docs/leaderboard-env-migration.md rename to apps/mnswpr/docs/leaderboard-env-migration.md diff --git a/firebase.json b/apps/mnswpr/firebase.json similarity index 100% rename from firebase.json rename to apps/mnswpr/firebase.json diff --git a/firestore.indexes.json b/apps/mnswpr/firestore.indexes.json similarity index 100% rename from firestore.indexes.json rename to apps/mnswpr/firestore.indexes.json diff --git a/firestore.rules b/apps/mnswpr/firestore.rules similarity index 100% rename from firestore.rules rename to apps/mnswpr/firestore.rules diff --git a/app/index.html b/apps/mnswpr/index.html similarity index 95% rename from app/index.html rename to apps/mnswpr/index.html index 31189b9..44502c3 100644 --- a/app/index.html +++ b/apps/mnswpr/index.html @@ -10,7 +10,6 @@ -