@johnmorrisdotca/jirai 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,23 @@ All notable changes to this project are written here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.4.1] - 2026-10-05
10
+
11
+ Nothing that was exported has changed.
12
+
13
+ ### Added
14
+
15
+ - A test holds every `@johnmorrisdotca/jirai@N` version pin in the README to this package's major version.
16
+
17
+ ### Changed
18
+
19
+ - The family's list, in the README and in the demo's footer, names all twenty-four packages, Karakuri and Houseki included.
20
+ - The npm description is one sentence of 250 characters or fewer, so npm and its search show it whole; it is also the repository's About text. `homepage` is the demo site and `author` is `"John Morris"`, the same in every package.
21
+ - The GitHub Actions workflows use the current versions of the actions (checkout 7, setup-node 7, pnpm/action-setup 6; configure-pages 6, upload-pages-artifact 5 and deploy-pages 5 for Pages), which clears GitHub's Node 20 deprecation warning.
22
+ - Every entry has an `import` condition beside `default`.
23
+ - The README has the family's sections in the family's order (Who it is for, Features, Use it in your project, API, Theming, Limits, Browser support, Languages, Roadmap, Architecture, Where it comes from, Changes), the family list sits under "Where it comes from", and a test holds it to them.
24
+ - The development tools are the family's: Vitest 5 and Playwright 1.63, as in the other packages.
25
+
9
26
  ## [0.4.0] - 2026-10-05
10
27
 
11
28
  - **Huge fields.** `hugeSettings(level, { grid, shape, size })` and `HUGE_SIZES` (32×32, 48×24 and 24×48) give each of the four levels on a field of four times the medium level's area, 1,024 to 1,152 squares, with the level's share of mines (a 32×32 has 126, 160, 211 and 256 mines at easy, medium, hard and extra-hard), on every rule set and outline. Every one is dealt and proved to need no guess in a median of 3 to 60 ms (73 ms the slowest of ten 32×32 seeds), on the square, orthogonal, hexagonal and wraparound grids and the heart, star and hexagon outlines, and `src/huge.test.ts` wins a 32×32 at easy and at extra-hard by the explained hints alone. The demo's Level menu offers them (`?level=huge-hard`).
package/README.md CHANGED
@@ -44,7 +44,13 @@ const game = newOrthogonalGame(settings);
44
44
  const board = makeOrthogonalBoard(settings, 40); // counts only edge-sharing neighbours
45
45
  ```
46
46
 
47
- ## What it does
47
+ ## Who it is for
48
+
49
+ - **Puzzle and game sites** that want Minesweeper with boards a player can trust: seeded fields that deal the same everywhere, a safe opening, verified no-guess deals, and the words in English and Japanese.
50
+ - **Developers of other front ends** who want the rules, the dealer and the deduction solver as plain functions, with no DOM, and their own drawing on top.
51
+ - **Players and teachers** who want to learn why a cell is safe: the hints explain the deduction that proves it, and a level steps up a measured difficulty.
52
+
53
+ ## Features
48
54
 
49
55
  - **Four rule sets:** square grids count eight neighbours, orthogonal grids count four, hex grids count six axial neighbours, and wraparound grids join opposite square edges.
50
56
  - **Board outlines:** rectangles, hearts, stars and hexagon outlines. Shaped boards have cut-outs; wraparound works with rectangles.
@@ -56,7 +62,7 @@ const board = makeOrthogonalBoard(settings, 40); // counts only edge-sharing nei
56
62
  - **Accessible controls:** keyboard navigation, pointer and touch, long press to mark, English and Japanese strings, and board labels read by assistive technology.
57
63
  - **Materials and markers:** ivory, wood or slate; flags, stones or flowers. Host CSS can replace the palette.
58
64
 
59
- ## Use it in a page
65
+ ## Use it in your project
60
66
 
61
67
  Mount a player into any element. The first reveal asks the module worker to deal the board, so serve the package over HTTP and allow same-origin module workers in your content security policy.
62
68
 
@@ -153,13 +159,17 @@ Orthogonal clues carry less information, so its numbers are smaller at every lev
153
159
 
154
160
  The full engine entry is `@johnmorrisdotca/jirai`; four-neighbour helpers are in `@johnmorrisdotca/jirai/orthogonal`. Drawing is `@johnmorrisdotca/jirai/draw`, browser play is `/play`, and the custom element is `/element` or `/element/define`. The [API guide](docs/API.md) lists the entries and public calls.
155
161
 
156
- ## Drawing and theming
162
+ ## API
163
+
164
+ Every export of every entry point is in the [API reference](https://johnmorrisdotca.github.io/jirai/api.html), made from the source when the demo is built, and the [API guide](docs/API.md) lists the entries and public calls in prose.
165
+
166
+ ## Theming
157
167
 
158
168
  `boardModel(game, options)` returns row-major labelled cells without touching the DOM. The draw entry exports `JIRAI_STYLE` and the board model. The player uses the same theme variables as its SVG and HTML controls; set `material`, `pieces`, and `language` on mount or override the CSS custom properties in your host.
159
169
 
160
170
  Materials are `ivory`, `wood` and `slate`. Marker sets are `flags`, `stones` and `flowers`. They change appearance only; the engine still stores covered, flag, question or open states.
161
171
 
162
- ## Limits and browser support
172
+ ## Limits
163
173
 
164
174
  The board is capped at 60 cells per side and 2,400 cells overall. Shapes need at least 9×9; wraparound is rectangular.
165
175
 
@@ -182,34 +192,67 @@ The board is capped at 60 cells per side and 2,400 cells overall. Shapes need at
182
192
 
183
193
  For synchronous server use, consider running a deal in a worker; a browser deals extra-hard in the board's own worker without a pause.
184
194
 
195
+ ## Browser support
196
+
185
197
  The browser player uses ES modules, SVG, custom elements, dialogs and module workers. Serve built files over HTTP; `file:` pages cannot load its worker. The engine and drawing functions do not need DOM globals. Development and tests require Node 22 or later.
186
198
 
199
+ ## Languages
200
+
201
+ The player's words are English and Japanese, chosen with the `language` option or the `lang` attribute: the buttons, the status lines, the hints and the labels read by assistive technology. Corrections to the Japanese are welcome as issues.
202
+
203
+ ## Roadmap
204
+
205
+ The engine, the dealer, the hints and the player are in. Nothing else is promised for a date; ideas are welcome in the [issues](https://github.com/johnmorrisdotca/jirai/issues).
206
+
207
+ ## Architecture
208
+
209
+ ```text
210
+ src/
211
+ ├── deduce.ts
212
+ ├── draw-entry.ts
213
+ ├── draw.ts
214
+ ├── element-define.ts
215
+ ├── element.ts
216
+ ├── enumerate.ts
217
+ ├── flood.ts
218
+ ├── game.ts
219
+ ├── generate.ts
220
+ ├── grid.ts
221
+ ├── index.ts
222
+ ├── jirai.constants.ts
223
+ ├── jirai.types.ts
224
+ ├── keep.ts
225
+ ├── levels.ts
226
+ ├── measure.ts
227
+ ├── mount.ts
228
+ ├── orthogonal.ts
229
+ ├── play-entry.ts
230
+ ├── random.ts
231
+ ├── react.tsx
232
+ ├── react.types.ts
233
+ ├── repair.ts
234
+ ├── shape.ts
235
+ ├── solve.ts
236
+ ├── strings.ts
237
+ ├── style.ts
238
+ ├── ui.types.ts
239
+ └── worker.ts
240
+ ```
241
+
187
242
  ## The name
188
243
 
189
244
  *Jirai* (地雷) is Japanese for a land mine, read じらい, said in three beats, *ji-ra-i*. It is made of 地 (*ji*,
190
245
  ground) and 雷 (*rai*, thunder): a mine is thunder buried in the ground. Every number on the board is a clue to
191
246
  where it lies. ([Wiktionary: 地雷](https://en.wiktionary.org/wiki/地雷).)
192
247
 
193
- ## Development
248
+ ## Where it comes from
194
249
 
195
- ```sh
196
- pnpm install --frozen-lockfile
197
- pnpm check # lint, types and tests
198
- pnpm test:package # build and import the actual npm tarball
199
- pnpm test:demo # browser flows against the built page
200
- pnpm site # build the standalone page into docs/
201
- ```
250
+ Minesweeper's rules are common property. Everything here, the rules, the dealer, the solver, the hints, the pictures and the words, is written for the package, and no third-party puzzle boards or artwork are included.
202
251
 
203
- The standalone game is `docs/index.html`. The preview binds to `127.0.0.1:6713`; see [CONTRIBUTING.md](CONTRIBUTING.md) before changing the engine or player.
204
-
205
- ## Licence
206
-
207
- [MIT](LICENSE) © John Morris. No third-party puzzle boards or artwork are included.
208
-
209
- ## The family
252
+ ### The family
210
253
 
211
254
  <!-- family:start (made by scripts/family-readme.mjs from scripts/family-template.mjs; change those, not this) -->
212
- Jirai is one of twenty-two packages, each made for the same site, each at
255
+ Jirai is one of twenty-four packages, each made for the same site, each at
213
256
  [github.com/johnmorrisdotca](https://github.com/johnmorrisdotca). The code of every one is MIT.
214
257
 
215
258
  - [Korokoro](https://github.com/johnmorrisdotca/korokoro) (コロコロ): dice, with notation, exact odds, real sounds and the dice of many games. [Demo](https://johnmorrisdotca.github.io/korokoro/).
@@ -234,10 +277,32 @@ Jirai is one of twenty-two packages, each made for the same site, each at
234
277
  - [Tobiishi](https://github.com/johnmorrisdotca/tobiishi) (飛び石): peg solitaire with nine boards and seeded solvable challenges. [Demo](https://johnmorrisdotca.github.io/tobiishi/).
235
278
  - [Jirai](https://github.com/johnmorrisdotca/jirai) (地雷): minesweeper on shaped grids with verified no-guess boards. [Demo](https://johnmorrisdotca.github.io/jirai/).
236
279
  - [Gunjin](https://github.com/johnmorrisdotca/gunjin) (軍人): five hidden-rank strategy games with pass-the-device play. [Demo](https://johnmorrisdotca.github.io/gunjin/).
280
+ - [Karakuri](https://github.com/johnmorrisdotca/karakuri) (からくり): eight hyper-casual puzzle games, some of them physics: draw a shield, pull pins, cut ropes, slide blocks, pour tubes. [Demo](https://johnmorrisdotca.github.io/karakuri/).
281
+ - [Houseki](https://github.com/johnmorrisdotca/houseki) (宝石): gem and stone matching puzzles: falling triplets, stone collapse, colour chains and gem swap. [Demo](https://johnmorrisdotca.github.io/houseki/).
237
282
 
238
- **This package is Jirai.** The demos of all twenty-two share one header and footer, so each links the rest.
283
+ **This package is Jirai.** The demos of all twenty-four share one header and footer, so each links the rest.
239
284
  <!-- family:end -->
240
285
 
241
- ## Contributing and security
286
+ ## Development
287
+
288
+ ```sh
289
+ pnpm install --frozen-lockfile
290
+ pnpm check # lint, types, tests and the presentation checks
291
+ pnpm test:package # build and import the actual npm tarball
292
+ pnpm test:demo # browser flows against the built page
293
+ pnpm site # build the standalone page into docs/
294
+ ```
295
+
296
+ The standalone game is `docs/index.html`. The preview binds to `127.0.0.1:6713`; see [CONTRIBUTING.md](CONTRIBUTING.md) before changing the engine or player.
297
+
298
+ ## Contributing
299
+
300
+ Bug reports and pull requests are welcome in the [issues](https://github.com/johnmorrisdotca/jirai/issues). See [CONTRIBUTING.md](CONTRIBUTING.md), the [Code of Conduct](CODE_OF_CONDUCT.md) and the [Security policy](SECURITY.md).
301
+
302
+ ## Changes
242
303
 
243
- See [Contributing](CONTRIBUTING.md), the [Code of Conduct](CODE_OF_CONDUCT.md) and the [Security policy](SECURITY.md).
304
+ Every release is written up in [CHANGELOG.md](./CHANGELOG.md).
305
+
306
+ ## Licence
307
+
308
+ [MIT](LICENSE) © John Morris. No third-party puzzle boards or artwork are included.
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@ export * from "./deduce.ts";
8
8
  export * from "./keep.ts";
9
9
  export { seededRandom } from "./random.ts";
10
10
  /** Package version, kept in step with the release metadata. */
11
- export declare const VERSION = "0.4.0";
11
+ export declare const VERSION = "0.4.1";
12
12
  export { SHAPES, activeCell, activeCells } from "./shape.ts";
13
13
  export * from "./orthogonal.ts";
14
14
  export { measureBoard } from "./measure.ts";
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ export * from "./deduce.js";
8
8
  export * from "./keep.js";
9
9
  export { seededRandom } from "./random.js";
10
10
  /** Package version, kept in step with the release metadata. */
11
- export const VERSION = "0.4.0";
11
+ export const VERSION = "0.4.1";
12
12
  export { SHAPES, activeCell, activeCells } from "./shape.js";
13
13
  export * from "./orthogonal.js";
14
14
  export { measureBoard } from "./measure.js";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@johnmorrisdotca/jirai",
3
- "version": "0.4.0",
4
- "description": "Minesweeper on square, orthogonal, hexagonal and wraparound boards: four levels up to extra-hard, seeded games, a safe opening, verified no-guess boards, explained hints, configurable materials and markers, and a playable board for any page. Zero runtime dependencies.",
3
+ "version": "0.4.1",
4
+ "description": "Minesweeper on square, orthogonal, hexagonal and wraparound boards: four levels up to extra-hard, seeded games, a safe opening, verified no-guess boards, explained hints and a playable board for any page. Zero runtime dependencies.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "John Morris",
@@ -54,30 +54,37 @@
54
54
  "exports": {
55
55
  ".": {
56
56
  "types": "./dist/index.d.ts",
57
+ "import": "./dist/index.js",
57
58
  "default": "./dist/index.js"
58
59
  },
59
60
  "./orthogonal": {
60
61
  "types": "./dist/orthogonal.d.ts",
62
+ "import": "./dist/orthogonal.js",
61
63
  "default": "./dist/orthogonal.js"
62
64
  },
63
65
  "./play": {
64
66
  "types": "./dist/play-entry.d.ts",
67
+ "import": "./dist/play-entry.js",
65
68
  "default": "./dist/play-entry.js"
66
69
  },
67
70
  "./draw": {
68
71
  "types": "./dist/draw-entry.d.ts",
72
+ "import": "./dist/draw-entry.js",
69
73
  "default": "./dist/draw-entry.js"
70
74
  },
71
75
  "./element": {
72
76
  "types": "./dist/element.d.ts",
77
+ "import": "./dist/element.js",
73
78
  "default": "./dist/element.js"
74
79
  },
75
80
  "./element/define": {
76
81
  "types": "./dist/element-define.d.ts",
82
+ "import": "./dist/element-define.js",
77
83
  "default": "./dist/element-define.js"
78
84
  },
79
85
  "./react": {
80
86
  "types": "./dist/react.d.ts",
87
+ "import": "./dist/react.js",
81
88
  "default": "./dist/react.js"
82
89
  }
83
90
  },
@@ -121,8 +128,8 @@
121
128
  "eslint": "^9.0.0",
122
129
  "typescript-eslint": "^8.0.0",
123
130
  "typescript": "^5.9.0",
124
- "vitest": "^4.0.0",
125
- "@playwright/test": "^1.58.0",
131
+ "vitest": "^5.0.0",
132
+ "@playwright/test": "^1.63.0",
126
133
  "@types/react": "^19.0.0",
127
134
  "react": "^19.0.0"
128
135
  },
@@ -15,7 +15,9 @@ const id = pkg.name.replace(/^@[^/]+\//, "");
15
15
 
16
16
  // The recorded hashes. The template's is the one that says every demo's header and footer, and every README's
17
17
  // list of the family, are the same text.
18
- const TEMPLATE = { version: "2026-10-05", sha256: "061b5ed89c345dccb6e029d5091dff0a5bbc4a9b57812fbcd0bd619038bdb7f1" };
18
+ // The template of 2026-10-05 lists twenty-four packages, Karakuri and Houseki included. The family's list is swept again, in every
19
+ // repository at once, when a package is added to it, and this hash is then the new one.
20
+ const TEMPLATE = { version: "2026-10-05", sha256: "a2dc81808be980438bdef8b91f5c0bbff920a739bc50930cd4632cb017c8fa48" };
19
21
  const FILES = {
20
22
  "scripts/family-readme.mjs": "3c9d5b2cbf17a92d31bced98edac7f544616edb0dff90bf2d141722a9d4516c5",
21
23
  "scripts/release-notes.mjs": "efab0fb78ad05973a8885624c0d2ce3b458b55799c11eabaa5176603ce8cd1e9",
@@ -32,7 +34,7 @@ describe("the family template", () => {
32
34
  it("lists every package of the family, in order, each with its Japanese name and a line on it", () => {
33
35
  expect(FAMILY.map((one) => one.id)).toEqual([
34
36
  "korokoro", "kyuubu", "hitotsu", "toranpu", "tane", "narabe", "tenka", "kumimoji", "tsunagi", "jarajara",
35
- "suido", "domino", "kotoba", "sugoroku", "kazu", "meikyuu", "hikidashi", "chizu", "bushu", "tobiishi", "jirai", "gunjin",
37
+ "suido", "domino", "kotoba", "sugoroku", "kazu", "meikyuu", "hikidashi", "chizu", "bushu", "tobiishi", "jirai", "gunjin", "karakuri", "houseki",
36
38
  ]);
37
39
  for (const one of FAMILY) {
38
40
  expect(one.name, one.id).toBe(one.id[0].toUpperCase() + one.id.slice(1));
@@ -75,13 +77,27 @@ describe("the README's family", () => {
75
77
  });
76
78
  });
77
79
 
80
+ describe("the README's version pins", () => {
81
+ it("name this package's major version, never an older one: a CDN address says @2 once the package is 2.x", () => {
82
+ const major = pkg.version.split(".")[0];
83
+ const pins = read("README.md")
84
+ .split(`${pkg.name}@`)
85
+ .slice(1)
86
+ .map((rest) => /^\d+/.exec(rest)?.[0])
87
+ .filter((pin) => pin !== undefined);
88
+ for (const pin of pins) expect(pin, `${pkg.name}@${pin} in README.md`).toBe(major);
89
+ });
90
+ });
91
+
78
92
  describe("the release notes", () => {
79
93
  it("are the changelog's section for the version, which the Release workflow puts on the GitHub release", () => {
80
94
  const log = "# Changelog\n\n## [Unreleased]\n\n## [1.2.0] - 2026-01-02\n\n### Added\n\n- A thing.\n\n## [1.1.0] - 2026-01-01\n\n- Older.\n\n[Unreleased]: https://example.test\n";
81
95
  expect(releaseNotes(log, "1.2.0")).toBe("### Added\n\n- A thing.");
82
96
  expect(releaseNotes(log, "1.1.0")).toBe("- Older.");
83
97
  expect(releaseNotes(log, "9.9.9")).toBeNull();
84
- expect(releaseNotes(read("CHANGELOG.md"), pkg.version)?.length, `CHANGELOG.md has nothing under ## [${pkg.version}]`).toBeGreaterThan(40);
98
+ // Until a version is published its notes are under [Unreleased]; the release takes the heading with the version and the date.
99
+ const notes = releaseNotes(read("CHANGELOG.md"), pkg.version) ?? releaseNotes(read("CHANGELOG.md"), "Unreleased");
100
+ expect(notes?.length, `CHANGELOG.md has nothing under ## [${pkg.version}] or ## [Unreleased]`).toBeGreaterThan(40);
85
101
  const workflow = read(".github/workflows/release.yml");
86
102
  expect(workflow).toContain("scripts/release-notes.mjs");
87
103
  expect(workflow).not.toContain("See CHANGELOG.md.");
@@ -90,7 +106,8 @@ describe("the release notes", () => {
90
106
  it("come from a changelog in Keep a Changelog form: an Unreleased heading, then each version in brackets with its date", () => {
91
107
  const log = read("CHANGELOG.md");
92
108
  expect(log).toContain("\n## [Unreleased]\n");
93
- expect(log).toMatch(/^## \[\d+\.\d+\.\d+\] - \d{4}-\d{2}-\d{2}$/m);
109
+ // Before the first release there is no versioned heading yet, only Unreleased.
110
+ if (/^## \[\d/m.test(log)) expect(log).toMatch(/^## \[\d+\.\d+\.\d+\] - \d{4}-\d{2}-\d{2}$/m);
94
111
  expect(log).not.toMatch(/^## \d/m);
95
112
  });
96
113
  });
package/src/index.ts CHANGED
@@ -8,7 +8,7 @@ export * from "./deduce.ts";
8
8
  export * from "./keep.ts";
9
9
  export { seededRandom } from "./random.ts";
10
10
  /** Package version, kept in step with the release metadata. */
11
- export const VERSION = "0.4.0";
11
+ export const VERSION = "0.4.1";
12
12
 
13
13
  export { SHAPES, activeCell, activeCells } from "./shape.ts";
14
14
 
@@ -0,0 +1,34 @@
1
+ // readme.test.js: the README has the sections every package of the family has, in the family's order, each with something in it.
2
+ import { readFileSync } from "node:fs";
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ const readme = readFileSync("README.md", "utf8").replace(/\r\n/g, "\n");
6
+ const HOUSE = [
7
+ "In 30 seconds", "Who it is for", "Features", "Use it in your project", "API", "Theming", "Limits", "Browser support",
8
+ "Languages", "Roadmap", "Architecture", "The name", "Where it comes from", "Development", "Contributing", "Changes", "Licence",
9
+ ];
10
+
11
+ describe("the README's shape", () => {
12
+ it("has the family's sections, in the family's order, each with something in it", () => {
13
+ const headings = [...readme.matchAll(/^## (.+)$/gm)].map((match) => match[1]);
14
+ let from = 0;
15
+ for (const heading of HOUSE) {
16
+ const at = headings.indexOf(heading, from);
17
+ expect(at, `README.md has no "## ${heading}" after the section before it`).toBeGreaterThanOrEqual(from);
18
+ from = at + 1;
19
+ const start = readme.indexOf(`\n## ${heading}\n`) + 1;
20
+ const next = readme.indexOf("\n## ", start + 4);
21
+ const body = readme.slice(start + heading.length + 4, next < 0 ? undefined : next).trim();
22
+ expect(body.length, `"## ${heading}" is empty`).toBeGreaterThan(20);
23
+ }
24
+ });
25
+
26
+ it("puts the family under \"Where it comes from\", one level down", () => {
27
+ const from = readme.indexOf("\n## Where it comes from\n");
28
+ const family = readme.indexOf("\n### The family\n");
29
+ const next = readme.indexOf("\n## Development\n");
30
+ expect(from).toBeGreaterThan(0);
31
+ expect(family).toBeGreaterThan(from);
32
+ expect(family).toBeLessThan(next);
33
+ });
34
+ });