chore: restructure to new cozy-games monorepo

Reviewed-on: https://git.ayo.run/ayo/cozy-games/pulls/1
Co-authored-by: Ayo <ayo@ayco.io>
Co-committed-by: Ayo <ayo@ayco.io>
This commit is contained in:
ayo 2026-07-03 13:53:13 +00:00 committed by ayo
parent 5f6e389258
commit 969ebba067
60 changed files with 169 additions and 180 deletions

6
.gitignore vendored
View file

@ -1,14 +1,14 @@
node_modules/ node_modules/
dist/ dist/
# generated at publish time from the root README.md (see scripts/publish-lib.js) # generated at publish time from apps/mnswpr/README.md (see scripts/publish-lib.js)
lib/README.md packages/mnswpr/README.md
.claude .claude
# Production / local Firebase config values are not committed — set them as # 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 # 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.production
.env.local .env.local
.env.*.local .env.*.local

View file

@ -10,9 +10,9 @@ Classic Minesweeper as a vanilla web game — no framework, no TypeScript (JSDoc
```bash ```bash
pnpm i # install (pnpm is required — this is a pnpm workspace) pnpm i # install (pnpm is required — this is a pnpm workspace)
pnpm dev # run the website dev server (vite app) — most common pnpm dev # run the website dev server (vite apps/mnswpr) — most common
pnpm build # build the website -> app/dist pnpm build # build the website -> apps/mnswpr/dist
pnpm build:lib # build the publishable library -> lib/dist pnpm build:lib # build the publishable library -> packages/mnswpr/dist
pnpm lint # eslint . (JS + CSS); runs automatically on pre-commit pnpm lint # eslint . (JS + CSS); runs automatically on pre-commit
pnpm lint:fix # eslint --fix pnpm lint:fix # eslint --fix
pnpm build:preview # build the app and serve the production preview 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 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/*`. 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/`. - **`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/<name>/`.
- **`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. - **`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`.
- **`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`. - **`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 ## 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: **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 `<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.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. - `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. **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`. `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 ## 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`). - **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) ## 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. - **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.

135
README.md
View file

@ -1,123 +1,40 @@
> [!IMPORTANT] # Cozy Games
> **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)**.
# Play Minesweeper Online for Free A monorepo for **Cozy Games** — a growing collection of small browser
[![Netlify Status](https://api.netlify.com/api/v1/badges/172478bd-afc5-4e47-95ba-d9ab814248fb/deploy-status)](https://app.netlify.com/sites/mnswpr/deploys) 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) ```
cozy-games/
## How to Play ├── apps/ Playable games (each deploys independently)
│ └── mnswpr/ Minesweeper — mnswpr.com (@ayo-run/mnswpr engine + Firebase leaderboard)
The goal is to reveal every safe cell without detonating a mine. Your **first click is always safe**. ├── packages/ Shared, publishable libraries
│ ├── mnswpr/ @ayo-run/mnswpr — the vanilla Minesweeper game engine
- **Left click** — reveal a cell │ ├── leaderboard/ @cozy-games/leaderboard — backend-agnostic, time-windowed leaderboard
- **Right click** — flag / unflag a suspected mine │ └── utils/ @cozy-games/utils — shared browser utilities (storage, timer, …)
- **Left + right click together** (chording) — reveal the neighbors of a satisfied number └── sites/ Docs (Astro Starlight) and UI demos — placeholders for now
- **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()
``` ```
## Tooling Each app owns its own backend config (e.g. mnswpr's Firestore rules live in
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! `apps/mnswpr/`); the shared packages stay backend-agnostic.
As of now the tooling I use are: ## Getting started
- [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
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. This is a [pnpm](https://pnpm.io) workspace (pnpm is required).
## 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:
```bash ```bash
pnpm test # run the Vitest suite pnpm i # install
pnpm lint # ESLint (JS + CSS) pnpm dev # run the mnswpr app (Vite dev server)
pnpm lint:fix # ESLint with autofix pnpm test # run all package tests (vitest)
pnpm build # build the website pnpm lint # eslint
pnpm build:lib # build the publishable library pnpm build # build the mnswpr app -> apps/mnswpr/dist
pnpm build:lib # build the engine package -> packages/mnswpr/dist
``` ```
### Leaderboard (local Firestore emulator) See [apps/mnswpr/README.md](apps/mnswpr/README.md) for the game itself, and each package's
README for library usage.
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) ✨
## License ## License
[BSD-2-Clause](./LICENSE) BSD-2-Clause © Ayo Ayco
---
_Just keep building._<br>
_A project by [Ayo](https://ayo.ayco.io)_

View file

@ -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). Play it here: [mnswpr.com](https://mnswpr.com). This is the classic game **Minesweeper** built with vanilla web technologies (i.e., no framework dependency).
<!-- TODO: replace with the actual screenshot/GIF once supplied (confirm final path/filename) --> ![mnswpr gameplay](https://git.ayo.run/ayo/mnswpr/raw/branch/main/screenshot.png)
![mnswpr gameplay](app/public/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: 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() 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 ## 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! 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. 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 ## 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: 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` 1. Install dependencies: `pnpm i`
2. Start the dev server: `pnpm run dev` 2. Start the dev server: `pnpm run dev`
@ -63,6 +64,22 @@ pnpm build # build the website
pnpm build:lib # build the publishable library 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 ## Contributing
Contributions are welcome! See [`AGENTS.md`](./AGENTS.md) for the architecture, conventions, and release workflow before opening a pull request. Contributions are welcome! See [`AGENTS.md`](./AGENTS.md) for the architecture, conventions, and release workflow before opening a pull request.

View file

@ -10,7 +10,6 @@
<link rel="shortcut icon" type="image/png" href="/favicon.ico" /> <link rel="shortcut icon" type="image/png" href="/favicon.ico" />
<link rel="stylesheet" href="./main.css" /> <link rel="stylesheet" href="./main.css" />
<link rel="stylesheet" href="../utils/loading/loading.css" />
<style> <style>
:host, :root{ :host, :root{
--mnswpr-transition: 10s ease-in-out; --mnswpr-transition: 10s ease-in-out;

View file

@ -1,5 +1,6 @@
import mnswpr from '@ayo-run/mnswpr/mnswpr.js' import mnswpr from '@ayo-run/mnswpr/mnswpr.js'
import '@ayo-run/mnswpr/mnswpr.css' import '@ayo-run/mnswpr/mnswpr.css'
import '@cozy-games/utils/loading/loading.css'
import * as pkg from '@ayo-run/mnswpr/package.json' import * as pkg from '@ayo-run/mnswpr/package.json'
import { configureLeaderboard } from '@cozy-games/leaderboard/leaderboard-element.js' import { configureLeaderboard } from '@cozy-games/leaderboard/leaderboard-element.js'
import { FirebaseAdapter } from '@cozy-games/leaderboard/adapters/firebase.js' import { FirebaseAdapter } from '@cozy-games/leaderboard/adapters/firebase.js'

View file

@ -1,4 +1,4 @@
import { StorageService } from '../../../utils/index.js' import { StorageService } from '@cozy-games/utils'
const NICK_KEY = 'nickname' const NICK_KEY = 'nickname'
const MAX_LENGTH = 24 const MAX_LENGTH = 24

View file

@ -12,6 +12,7 @@
"devDependencies": { "devDependencies": {
"@ayo-run/mnswpr": "workspace:*", "@ayo-run/mnswpr": "workspace:*",
"@cozy-games/leaderboard": "workspace:*", "@cozy-games/leaderboard": "workspace:*",
"@cozy-games/utils": "workspace:*",
"firebase": "^12.11.0", "firebase": "^12.11.0",
"web-component-base": "^4.1.2" "web-component-base": "^4.1.2"
}, },

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -1,12 +1,12 @@
/** /**
* One-time generator: snapshot the all-time leaders from the legacy * One-time generator: snapshot the all-time leaders from the legacy
* `mw-leaders/{level}/games` collection and write them into app/legends.html as * `mw-leaders/{level}/games` collection and write them into legends.html as
* a FULLY-RENDERED, static page no runtime JS, no Firebase at page load. * a FULLY-RENDERED, static page no runtime JS, no Firebase at page load.
* *
* The data never changes, so the page is a frozen artifact. Re-run only if you * The data never changes, so the page is a frozen artifact. Re-run only if you
* ever need to regenerate it. Because `firebase` is an app-workspace dependency, * ever need to regenerate it. Because `firebase` is an app-workspace dependency,
* run it so Node can resolve it, e.g. from the app directory: * run it so Node can resolve it, e.g. from the app directory:
* (cd app && node ../scripts/export-legends.js) * (cd apps/mnswpr && node scripts/export-legends.js)
*/ */
import { writeFileSync } from 'node:fs' import { writeFileSync } from 'node:fs'
import { resolve, dirname } from 'node:path' import { resolve, dirname } from 'node:path'
@ -17,9 +17,9 @@ import {
getFirestore, getDocs, collection, query, orderBy, limit getFirestore, getDocs, collection, query, orderBy, limit
} from 'firebase/firestore/lite' } from 'firebase/firestore/lite'
import { levels } from '../lib/levels.js' import { levels } from '@ayo-run/mnswpr/levels.js'
// Mirror of TimerService.pretty() (utils/timer/timer.js) — inlined so this // Mirror of TimerService.pretty() (@cozy-games/utils timer) — inlined so this
// generator has no cross-module import chain to resolve under raw Node. // generator has no cross-module import chain to resolve under raw Node.
const clean = (str, separator) => (str === '00' ? '' : `${str}${separator}`) const clean = (str, separator) => (str === '00' ? '' : `${str}${separator}`)
const pretty = duration => { const pretty = duration => {
@ -131,6 +131,6 @@ ${sections.join('\n')}
` `
const __dirname = dirname(fileURLToPath(import.meta.url)) const __dirname = dirname(fileURLToPath(import.meta.url))
const out = resolve(__dirname, '../app/legends.html') const out = resolve(__dirname, '../legends.html')
writeFileSync(out, html) writeFileSync(out, html)
console.log(`\nWrote ${out}`) console.log(`\nWrote ${out}`)

View file

@ -13,7 +13,7 @@
* firestore.rules to be deployed first (they permit writes to * firestore.rules to be deployed first (they permit writes to
* `mw-test-scores`), else every write returns permission-denied: * `mw-test-scores`), else every write returns permission-denied:
* npx firebase deploy --only firestore:rules --project dev * npx firebase deploy --only firestore:rules --project dev
* (cd app && node ../scripts/seed-dev-scores.js) * (cd apps/mnswpr && node scripts/seed-dev-scores.js)
* *
* `firebase` is an app-workspace dependency, so run it from the app directory so * `firebase` is an app-workspace dependency, so run it from the app directory so
* Node can resolve it. Optional namespace override: LB_NAMESPACE=mw-test. * Node can resolve it. Optional namespace override: LB_NAMESPACE=mw-test.
@ -21,7 +21,7 @@
import { initializeApp } from 'firebase/app' import { initializeApp } from 'firebase/app'
import { getFirestore, connectFirestoreEmulator, doc, collection, setDoc } from 'firebase/firestore/lite' import { getFirestore, connectFirestoreEmulator, doc, collection, setDoc } from 'firebase/firestore/lite'
import { buckets } from '../utils/date-bucket/date-bucket.js' import { buckets } from '@cozy-games/utils/date-bucket/date-bucket.js'
const firebaseConfig = { const firebaseConfig = {
apiKey: 'AIzaSyCTi_5Sm5dHFNf0d_Gn0MNWmlGheFBf6MQ', apiKey: 'AIzaSyCTi_5Sm5dHFNf0d_Gn0MNWmlGheFBf6MQ',

View file

@ -50,7 +50,7 @@ export default defineConfig([
} }
}, },
{ {
files: ['scripts/**/*.js'], files: ['**/scripts/**/*.js'],
languageOptions: { languageOptions: {
globals: globals.node globals: globals.node
} }

View file

@ -1,8 +1,8 @@
{ {
"name": "monorepo", "name": "cozy-games",
"version": "0.0.1", "version": "0.0.1",
"private": true, "private": true,
"description": "Classic Minesweeper browser game", "description": "Cozy Games monorepo — apps, shared packages, and sites",
"author": "Ayo Ayco", "author": "Ayo Ayco",
"type": "module", "type": "module",
"repository": { "repository": {
@ -13,12 +13,12 @@
"scripts": { "scripts": {
"test": "vitest run", "test": "vitest run",
"test:watch": "vitest", "test:watch": "vitest",
"dev": "vite app", "dev": "vite apps/mnswpr",
"start": "vite app", "start": "vite apps/mnswpr",
"emulators": "npx -y firebase-tools emulators:start --only firestore", "emulators": "cd apps/mnswpr && npx -y firebase-tools emulators:start --only firestore",
"seed:emulator": "cd app && FIRESTORE_EMULATOR_HOST=127.0.0.1:8080 node ../scripts/seed-dev-scores.js", "seed:emulator": "cd apps/mnswpr && FIRESTORE_EMULATOR_HOST=127.0.0.1:8080 node scripts/seed-dev-scores.js",
"build": "vite build app", "build": "vite build apps/mnswpr",
"build:lib": "vite build lib", "build:lib": "vite build packages/mnswpr",
"publish:lib": "node scripts/publish-lib.js", "publish:lib": "node scripts/publish-lib.js",
"release": "pnpm build:lib && pnpm -F @ayo-run/mnswpr run release && pnpm publish:lib", "release": "pnpm build:lib && pnpm -F @ayo-run/mnswpr run release && pnpm publish:lib",
"build:preview": "pnpm -F app run build:preview", "build:preview": "pnpm -F app run build:preview",

View file

@ -1,4 +1,4 @@
import { buckets } from '../utils/date-bucket/date-bucket.js' import { buckets } from '@cozy-games/utils/date-bucket/date-bucket.js'
const DAY_MS = 24 * 60 * 60 * 1000 const DAY_MS = 24 * 60 * 60 * 1000

View file

@ -27,6 +27,9 @@
"dependencies": { "dependencies": {
"web-component-base": "^4.1.2" "web-component-base": "^4.1.2"
}, },
"devDependencies": {
"@cozy-games/utils": "workspace:*"
},
"peerDependencies": { "peerDependencies": {
"firebase": "^12.11.0" "firebase": "^12.11.0"
}, },

View file

@ -9,7 +9,7 @@ import {
LoggerService, LoggerService,
StorageService, StorageService,
TimerService TimerService
} from '../utils/index.js' } from '@cozy-games/utils'
import { levels } from './levels.js' import { levels } from './levels.js'
const TEST_MODE = false // set to true if you want to test the game with visual hints const TEST_MODE = false // set to true if you want to test the game with visual hints

View file

@ -10,9 +10,12 @@
}, },
"homepage": "https://mnswpr.com", "homepage": "https://mnswpr.com",
"scripts": { "scripts": {
"release": "bumpp && node ../scripts/release.js" "release": "bumpp && node ../../scripts/release.js"
}, },
"main": "mnswpr.js", "main": "mnswpr.js",
"devDependencies": {
"@cozy-games/utils": "workspace:*"
},
"exports": { "exports": {
".": { ".": {
"default": "./dist/mnswpr.js" "default": "./dist/mnswpr.js"

View file

@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest' import { describe, it, expect } from 'vitest'
import { levels } from '../lib/levels.js' import { levels } from '../levels.js'
describe('levels', () => { describe('levels', () => {
it('defines the four expected difficulty presets', () => { it('defines the four expected difficulty presets', () => {

View file

@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import Minesweeper from '../lib/mnswpr.js' import Minesweeper from '../mnswpr.js'
// Build a fresh board mounted on #app and return its grid <table>. // Build a fresh board mounted on #app and return its grid <table>.
function mountGame() { function mountGame() {

View file

@ -0,0 +1,22 @@
{
"name": "@cozy-games/utils",
"version": "0.0.1",
"description": "Shared, dependency-free browser utilities for Cozy Games (storage, timer, logger, loading, date buckets)",
"author": "Ayo Ayco",
"private": true,
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/ayo-run/mnswpr"
},
"main": "index.js",
"exports": {
".": {
"default": "./index.js"
},
"./*": {
"default": "./*"
}
},
"license": "BSD-2-Clause"
}

View file

@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest' import { describe, it, expect } from 'vitest'
import { dayKey, weekKey, monthKey, buckets } from '../utils/date-bucket/date-bucket.js' import { dayKey, weekKey, monthKey, buckets } from '@cozy-games/utils/date-bucket/date-bucket.js'
describe('date-bucket', () => { describe('date-bucket', () => {
it('builds a zero-padded UTC day key', () => { it('builds a zero-padded UTC day key', () => {

View file

@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach } from 'vitest' import { describe, it, expect, beforeEach } from 'vitest'
import { StorageService } from '../utils/storage/storage.js' import { StorageService } from '@cozy-games/utils/storage/storage.js'
describe('StorageService', () => { describe('StorageService', () => {
let storage let storage

View file

@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { TimerService } from '../utils/timer/timer.js' import { TimerService } from '@cozy-games/utils/timer/timer.js'
describe('TimerService.pretty', () => { describe('TimerService.pretty', () => {
const timer = new TimerService() const timer = new TimerService()

View file

@ -42,14 +42,17 @@ importers:
specifier: ^4.1.9 specifier: ^4.1.9
version: 4.1.9(@types/node@25.5.2)(jsdom@29.1.1)(vite@8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.5.2)(jiti@2.6.1)(yaml@2.8.3)) version: 4.1.9(@types/node@25.5.2)(jsdom@29.1.1)(vite@8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.5.2)(jiti@2.6.1)(yaml@2.8.3))
app: apps/mnswpr:
devDependencies: devDependencies:
'@ayo-run/mnswpr': '@ayo-run/mnswpr':
specifier: workspace:* specifier: workspace:*
version: link:../lib version: link:../../packages/mnswpr
'@cozy-games/leaderboard': '@cozy-games/leaderboard':
specifier: workspace:* specifier: workspace:*
version: link:../leaderboard version: link:../../packages/leaderboard
'@cozy-games/utils':
specifier: workspace:*
version: link:../../packages/utils
firebase: firebase:
specifier: ^12.11.0 specifier: ^12.11.0
version: 12.11.0 version: 12.11.0
@ -57,7 +60,7 @@ importers:
specifier: ^4.1.2 specifier: ^4.1.2
version: 4.1.2 version: 4.1.2
leaderboard: packages/leaderboard:
dependencies: dependencies:
firebase: firebase:
specifier: ^12.11.0 specifier: ^12.11.0
@ -65,8 +68,18 @@ importers:
web-component-base: web-component-base:
specifier: ^4.1.2 specifier: ^4.1.2
version: 4.1.2 version: 4.1.2
devDependencies:
'@cozy-games/utils':
specifier: workspace:*
version: link:../utils
lib: {} packages/mnswpr:
devDependencies:
'@cozy-games/utils':
specifier: workspace:*
version: link:../utils
packages/utils: {}
packages: packages:

View file

@ -1,7 +1,7 @@
packages: packages:
- "lib" - "apps/*"
- "app" - "packages/*"
- "leaderboard" - "sites/*"
allowBuilds: allowBuilds:
'@firebase/util': false '@firebase/util': false
protobufjs: false protobufjs: false

View file

@ -1,17 +1,18 @@
// Publishes @ayo-run/mnswpr to npm using the root project README.md as the // Publishes @ayo-run/mnswpr to npm using the mnswpr app README.md as the
// package's README (what npmjs.com displays). // package's README (what npmjs.com displays).
// //
// lib/README.md is a generated copy (gitignored) — the source of truth is the // packages/mnswpr/README.md is a generated copy (gitignored) — the source of
// root README.md. The library's own guide lives in lib/TUTORIAL.md. // truth is apps/mnswpr/README.md. The library's own guide lives in
// packages/mnswpr/TUTORIAL.md.
import { execSync } from 'node:child_process' import { execSync } from 'node:child_process'
import { copyFileSync } from 'node:fs' import { copyFileSync } from 'node:fs'
import { resolve } from 'node:path' import { resolve } from 'node:path'
const root = resolve(import.meta.dirname, '..') const root = resolve(import.meta.dirname, '..')
const libDir = resolve(root, 'lib') const libDir = resolve(root, 'packages/mnswpr')
console.log('Copying root README.md into lib/ for the published package') console.log('Copying apps/mnswpr/README.md into packages/mnswpr/ for the published package')
copyFileSync(resolve(root, 'README.md'), resolve(libDir, 'README.md')) copyFileSync(resolve(root, 'apps/mnswpr/README.md'), resolve(libDir, 'README.md'))
execSync('npm login') execSync('npm login')
execSync('npm publish', { cwd: libDir, stdio: 'inherit' }) execSync('npm publish', { cwd: libDir, stdio: 'inherit' })

5
sites/demos/README.md Normal file
View file

@ -0,0 +1,5 @@
# Cozy Games — UI Demos (placeholder)
Interactive UI/component demos will live here. Not yet scaffolded.
Tracked in the monorepo transition plan as a later pass.

5
sites/docs/README.md Normal file
View file

@ -0,0 +1,5 @@
# Cozy Games — Docs (placeholder)
The documentation site (Astro Starlight) will live here. Not yet scaffolded.
Tracked in the monorepo transition plan as a later pass.

View file

@ -3,6 +3,6 @@ import { defineConfig } from 'vitest/config'
export default defineConfig({ export default defineConfig({
test: { test: {
environment: 'jsdom', environment: 'jsdom',
include: ['test/**/*.test.js'] include: ['packages/**/test/**/*.test.js', 'apps/**/test/**/*.test.js']
} }
}) })