@johnmorrisdotca/jirai 0.2.0 → 0.2.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
@@ -1,10 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ All notable changes to this project are written here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.2.1] - 2026-10-05
10
+
11
+ - The package no longer ships its unit tests (`src/*.test.ts`) in the npm tarball.
12
+ - Each of the demo's settings now has a real line of help under it when the Help switch is on, in English and Japanese.
13
+ - The demo, its README family list and its tests are the family's own: the shared header, footer and list of twenty-two, written from one template.
14
+ - The package check runs on Windows too, where npm is a .cmd file.
15
+ - CI runs on a push to main and on a pull request, not twice per pull request, and the release's notes are the changelog's section.
16
+ - Complete package presentation: desktop and phone screenshots, badges, demo/API links, targeted keywords and linked MIT licence.
17
+ - Source-derived API reference and public API comments, contribution/security files, and a package presentation gate.
18
+
3
19
  ## [0.2.0] - 2026-10-05
4
20
 
5
21
  - Orthogonal fields: four-neighbour clues, independent hints, proof-backed generation, drawing, saved progress and bilingual controls. Existing square, hexagonal and wraparound modes remain available.
6
22
 
7
- ## 0.1.0 — release candidate
23
+ ## [0.1.0] - 2026-10-04
8
24
 
9
25
  - Square, hexagonal and wraparound fields, seeded and configurable.
10
26
  - Safe and clear openings, verified no-guess generation and explained deductions.
package/README.md CHANGED
@@ -1,138 +1,185 @@
1
- # Jirai 地雷
1
+ <h1 align="center">Jirai <sub>地雷</sub></h1>
2
2
 
3
- **Every number is a clue.** Minesweeper on four-neighbour orthogonal and eight-neighbour square grids, hexagons, or a board whose opposite edges join. Clear the ground, mark the mines, and finish a field made from a seed.
3
+ <p align="center"><strong>Every number is a clue.</strong><br>
4
+ Minesweeper across four-neighbour, eight-neighbour, hexagonal and wraparound grids. Choose a shape, lay a seeded field, and clear it with deductions you can trust.</p>
4
5
 
5
- Plain TypeScript rules, no runtime dependencies. The rules work without a browser; the playable board works in any page. A React wrapper and a custom element are separate imports.
6
+ <p align="center">
7
+ <a href="https://github.com/johnmorrisdotca/jirai/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/johnmorrisdotca/jirai/actions/workflows/ci.yml/badge.svg"></a>
8
+ <a href="https://www.npmjs.com/package/@johnmorrisdotca/jirai"><img alt="npm" src="https://img.shields.io/npm/v/@johnmorrisdotca/jirai?color=2f5d4a"></a>
9
+ <a href="./LICENSE"><img alt="MIT licence" src="https://img.shields.io/badge/licence-MIT-2f5d4a"></a>
10
+ <img alt="No runtime dependencies" src="https://img.shields.io/badge/runtime%20dependencies-0-2f5d4a">
11
+ <img alt="TypeScript" src="https://img.shields.io/badge/types-TypeScript-3178c6">
12
+ </p>
6
13
 
7
- ## What it does
14
+ <p align="center"><a href="https://johnmorrisdotca.github.io/jirai/"><strong>Play Jirai →</strong></a> · <a href="https://johnmorrisdotca.github.io/jirai/api.html">API reference</a></p>
8
15
 
9
- - Orthogonal four-neighbour, square eight-neighbour, hexagonal and wraparound boards, with width, height and mine count of your own.
10
- - Rectangle, heart, star and hexagon outlines; wide and tall presets. Cut-outs are outside the playable field.
11
- - A safe first cell, or a clear opening with all its neighbours safe.
12
- - Verified no-guess fields. A bounded generator throws `GenerationError` if it cannot prove a board; it never substitutes an ordinary field.
13
- - Fixed mines after the opening, seeded deals, replayable progress and daily seeds.
14
- - Flood opening, flags and question marks, chording, explained hints, a clock and a just-the-board dialog.
15
- - Wood, ivory or slate; flags, stones or flowers. Host CSS may replace the palette.
16
- - English and Japanese, mouse, touch, long press, and keyboard navigation.
17
- - Browser generation runs in a module worker, so searching for a no-guess field does not block input.
16
+ <p align="center">
17
+ <img src="docs/desktop.jpg" alt="Jirai on a desktop: the Minesweeper board and its shape, grid, size, mine-count and material controls in the shared family demo style" width="680">
18
+ <img src="docs/phone.jpg" alt="Jirai on a phone: a square minefield with touch controls and clear number clues" width="220">
19
+ </p>
18
20
 
19
- ## Start a game
21
+ Jirai is a Minesweeper rules engine and player for TypeScript and JavaScript. The core is plain functions; drawing, browser controls, a custom element and an optional React wrapper are separate imports. The package has no runtime dependencies and needs Node 22 or a modern browser.
20
22
 
21
- Once published:
23
+ ## In 30 seconds
22
24
 
23
25
  ```sh
24
26
  npm install @johnmorrisdotca/jirai
25
27
  ```
26
28
 
27
- The present checkout is a release candidate; its repository URL and package name are intended destinations, not a claim that it has been published.
28
-
29
29
  ```ts
30
- import { DEFAULT_SETTINGS, newGame, play, visibleGame, hintFor } from "@johnmorrisdotca/jirai";
30
+ import { DEFAULT_SETTINGS, hintFor, newGame, play, visibleGame } from "@johnmorrisdotca/jirai";
31
31
 
32
- let game = newGame({ ...DEFAULT_SETTINGS, width: 16, height: 16, mines: 40, seed: 42 });
33
- game = play(game, { kind: "reveal", cell: 136 });
34
- game = play(game, { kind: "mark", cell: 0 });
35
- const hint = hintFor(visibleGame(game));
32
+ let game = newGame({ ...DEFAULT_SETTINGS, width: 9, height: 9, mines: 10, seed: 42 });
33
+ game = play(game, { kind: "reveal", cell: 40 }); // the first reveal deals the seeded board
34
+ const hint = hintFor(visibleGame(game)); // certain safe cells/mines from clues only
36
35
  ```
37
36
 
38
- Every move returns a new game and leaves its input untouched. A move which is unavailable returns the game given. The first reveal deals the board. A mark made before it does not influence the deal.
37
+ For a dedicated four-neighbour variant, its entry fixes the topology for you:
38
+
39
+ ```ts
40
+ import { makeOrthogonalBoard, newOrthogonalGame } from "@johnmorrisdotca/jirai/orthogonal";
41
+
42
+ const settings = { width: 9, height: 9, mines: 10, noGuess: true, opening: "clear", seed: 42 };
43
+ const game = newOrthogonalGame(settings);
44
+ const board = makeOrthogonalBoard(settings, 40); // counts only edge-sharing neighbours
45
+ ```
39
46
 
40
- `Settings.shape` may be `rectangle` (the default), `heart`, `star` or `hexagon`. Shaped fields require at least 9 cells on each side and work on square or hexagonal grids; wraparound requires a rectangle. `activeCells(settings)` returns playable row-major cells and `activeCell(settings, cell)` tests membership. Outside cells have clue −2, never contain mines and are excluded from neighbours, solving, drawing and winning. Mine count must be less than the playable cell count.
47
+ ## What it does
41
48
 
42
- The dedicated `@johnmorrisdotca/jirai/orthogonal` entry provides `newOrthogonalGame`, `makeOrthogonalBoard`, `orthogonalNeighbours`, `orthogonalHint`, `encodeOrthogonalGame` and `decodeOrthogonalGame`. It applies four-neighbour rules explicitly, so callers do not need to thread a topology string through setup.
49
+ - **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
+ - **Board outlines:** rectangles, hearts, stars and hexagon outlines. Shaped boards have cut-outs; wraparound works with rectangles.
51
+ - **A fair first move:** choose a safe first cell or a clear opening with all its neighbours safe.
52
+ - **Verified no-guess deals:** optional deduction-only dealing accepts a board only when the solver proves every safe cell from the opening. It throws `GenerationError` when the bounded search cannot prove one.
53
+ - **Fixed seeded fields:** after the opening, mines never move. A seed and settings reproduce the same deal.
54
+ - **Familiar play:** reveal, flag, question-mark, chord, flood-open, explained hint, timer, undo by saved replay, and a just-the-board dialog.
55
+ - **Accessible controls:** keyboard navigation, pointer and touch, long press to mark, English and Japanese strings, and board labels read by assistive technology.
56
+ - **Materials and markers:** ivory, wood or slate; flags, stones or flowers. Host CSS can replace the palette.
43
57
 
44
- The demo uses the family’s original shared stylesheet and header template, with its five table-cloth choices and bilingual Help controls.
58
+ ## Use it in a page
45
59
 
46
- Cells are row-major numbers, zero first: `cell = row * width + column`. Orthogonal clues count only the four edge-sharing cells. Square boards count eight neighbours. Hex boards use axial coordinates: neighbours are left, right, above, above-right, below-left and below. A wrap board joins both opposite edges of a square grid.
60
+ 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.
47
61
 
48
62
  ```ts
49
- import { mountJirai } from "@johnmorrisdotca/jirai/play";
63
+ import { DEFAULT_SETTINGS, mountJirai } from "@johnmorrisdotca/jirai/play";
50
64
 
51
- const board = mountJirai(document.querySelector<HTMLElement>("#board")!, {
52
- settings: { ...DEFAULT_SETTINGS, grid: "hex", seed: 7 },
65
+ const board = mountJirai(document.querySelector<HTMLElement>("#game")!, {
66
+ settings: { ...DEFAULT_SETTINGS, grid: "orthogonal", seed: 7 },
53
67
  material: "wood",
54
68
  pieces: "stones",
55
69
  language: "en",
56
- onChange(game) { /* save gameProgress(game) */ },
57
- onFinish(game) { /* game.status is won or lost */ },
58
- onError(error) { /* show a generation or worker-loading error */ },
70
+ onChange(game) { localStorage.setItem("jirai", board.progress()); },
71
+ onFinish(game) { console.log(game.status, game.helped); },
72
+ onError(error) { console.error(error); },
59
73
  });
60
-
61
74
  board.set({ material: "slate", language: "ja" });
62
75
  board.restart();
63
76
  board.destroy();
64
77
  ```
65
78
 
66
- The handle also offers `game()`, `progress()`, `load(settings, progress?)`, `play(cell, mark?)`, and `hint()`. `controls: false` gives a bare board for a host which supplies its own controls, status and timer. Events `jirai-change` and `jirai-finish` are dispatched on the host.
79
+ The handle also provides `game()`, `progress()`, `play(cell, mark?)`, `hint()`, and `load(settings, progress?)`. Set `controls: false` when your page supplies its own controls and status. Change options with `set`; call `destroy` when the host is removed.
67
80
 
68
- The engine's `Game` contains the answer. Keep that in the process doing the checking. `visibleGame` removes all hidden clues and is the only input the hint solver reads; a player's flags never count as evidence. This is a local puzzle, not a secure multiplayer protocol.
69
-
70
- ## In a framework
71
-
72
- ```tsx
73
- import { JiraiBoard } from "@johnmorrisdotca/jirai/react";
74
- <JiraiBoard key={seed} settings={{ ...DEFAULT_SETTINGS, seed }} material="wood" />
75
- ```
76
-
77
- Options are read when mounted; a new key starts a different board. `onChange`, `onFinish` and `onError` stay current.
78
-
79
- Vue, Svelte and Angular can call `mountJirai` on their element in the mount lifecycle and call `destroy` when it leaves. Or use the tag:
81
+ Use the custom element without a mount call:
80
82
 
81
83
  ```html
82
84
  <script type="module">
83
85
  import "@johnmorrisdotca/jirai/element/define";
84
86
  </script>
85
- <jirai-board grid="hex" width="9" height="9" mines="10" seed="7"
86
- no-guess="true" material="wood" pieces="stones" lang="ja"></jirai-board>
87
+ <jirai-board width="9" height="9" mines="10" seed="42"
88
+ grid="orthogonal" opening="clear" no-guess="true"
89
+ material="wood" pieces="stones" lang="ja"></jirai-board>
87
90
  ```
88
91
 
89
- Use your bundler or an import map to resolve the package name. The worker is `dist/worker.js`, resolved beside the mounting module; serve the built files over HTTP and allow module workers in your CSP. It is included in the npm tarball. A deployment that rewrites module paths must keep this worker URL working too.
92
+ The optional React entry exports `JiraiBoard` from `@johnmorrisdotca/jirai/react`; React is an optional peer dependency. Its options are read when mounted. Use a new React `key` to start with a different settings object.
90
93
 
91
- ## Keep a game
94
+ ## Rules and settings
92
95
 
93
- ```ts
94
- import { gameProgress, gameFromProgress, dailySeed } from "@johnmorrisdotca/jirai";
95
- const saved = gameProgress(game);
96
- const restored = gameFromProgress(saved); // null for an invalid record
97
- const seed = dailySeed("2026-10-04", "hex");
98
- ```
96
+ | Setting | Values and limits |
97
+ | --- | --- |
98
+ | `grid` | `square` (eight neighbours), `orthogonal` (four), `hex` (six), or `wrap` (eight; opposite edges join). |
99
+ | `shape` | `rectangle`, `heart`, `star` or `hexagon`; non-rectangles require at least 9 rows and columns. Wraparound requires `rectangle`. |
100
+ | `width`, `height` | 3–60 each, with at most 2,400 total cells. |
101
+ | `mines` | At least one, fewer than active cells, and low enough to leave the selected opening. |
102
+ | `noGuess` | If true, reject any deal the bounded deduction solver cannot finish from the opening. This proves the deal under this solver’s deductions, not a unique solution to every custom board. |
103
+ | `opening` | `safe` protects the first cell; `clear` also protects its neighbours. |
104
+ | `seed` | Integer from 0 through 4,294,967,295. A seed is interpreted together with all other settings and the first cell. |
105
+ | `material`, `pieces`, `language` | `ivory`, `wood`, `slate`; `flags`, `stones`, `flowers`; `en`, `ja`. |
99
106
 
100
- Ordinary grids keep version 1 progress records. Orthogonal records use version 2 and an explicit `variant: "orthogonal"` marker; `decodeOrthogonalGame` accepts only those records. Existing square, hex and wraparound records keep their format and meaning. A seed is only the start of a board's identity: width, height, mine count, grid, no-guess setting, opening policy and first cell matter too. Daily play uses UTC, beginner settings, and the centre opening. The demo's share link includes the first cell after a field has been dealt. Its local save restores the moves, but does not restore elapsed time.
107
+ The built-in presets are beginner (9×9, 10 mines), intermediate (16×16, 40), expert (30×16, 99), wide (21×9, 24), and tall (9×21, 24). They are starting points, not calibrated difficulty ratings.
101
108
 
102
- ## The no-guess promise
109
+ ## Engine and saved games
103
110
 
104
- The generator deals candidates and plays them using deductions made from visible clues. It uses local counts, differences between contained clue sets, the total mine count, and complete enumeration of small connected frontiers. Enumeration stops at a fixed work budget and discards incomplete results. A candidate is accepted only when all safe cells were reached by proved moves.
111
+ `newGame(settings)` returns an immutable ready game with no dealt mines. `play(game, move)` returns a new state; a move that cannot be made returns the original state. `visibleGame(game)` strips hidden mine locations. `hintFor(visibleGame)` reads only opened clues: flags are marks, never evidence. `makeBoard(settings, first, options?)` deals directly, and `isSolvable(board)` independently checks whether its deduction solver can finish.
105
112
 
106
- This is deliberately conservative. Failing to prove a board does not mean no human could solve it. Very dense custom fields or large expert boards may exceed the search budget. Call `makeBoard(settings, first, { attempts })` to set the candidate budget (1–10000; default 128). Server generation is synchronous; run it in a worker when serving large boards. The mounted browser board already does so.
113
+ `gameProgress(game)` saves the settings and move history, not an unchecked answer. `gameFromProgress(code)` replays and validates the moves, returning `null` for invalid data. The original square, hex and wraparound games use version 1 records. Orthogonal games use version 2 with `variant: "orthogonal"`; `decodeOrthogonalGame` accepts only those records. Keep this distinction when storing old games.
107
114
 
108
- Hints identify a certain safe cell or mine and say which deduction proved it. They do not offer approximate probabilities. Chording still requires correctly placed flags: a matched count of incorrect flags can expose a mine.
115
+ 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.
109
116
 
110
- ## Build and check
117
+ ## Drawing and theming
111
118
 
112
- ```sh
113
- pnpm install
114
- pnpm check
115
- pnpm test:package
116
- pnpm exec playwright install chromium
117
- pnpm test:demo
118
- pnpm site
119
- node scripts/serve.mjs
120
- ```
119
+ `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.
121
120
 
122
- The generated standalone page is in `docs/`; the preview serves only on `127.0.0.1:6713`. The tests include an exhaustive independent small-board oracle for solver soundness, deterministic dealing, flood/chord rules, immutable moves, replay, and phone/desktop browser flows. The package check installs the actual tarball into an empty temporary project and imports it in Node.
121
+ 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.
123
122
 
124
- ## Architecture
123
+ ## Limits and browser support
125
124
 
126
- Rules: `jirai.types.ts`, `jirai.constants.ts`, `grid.ts`, `random.ts`, `generate.ts`, `flood.ts`, `game.ts`, `deduce.ts`, `enumerate.ts`, `keep.ts`, `orthogonal.ts`.
125
+ The board is capped at 60 cells per side and 2,400 cells overall. Shapes need at least 9×9; wraparound is rectangular. The no-guess generator tries at most 128 candidates by default. `makeBoard(settings, first, { attempts })` accepts 1–10,000 attempts and can enable or disable local enumeration with `enumerate`; failure raises `GenerationError` rather than silently returning a guessing field. Very dense or shaped boards may exhaust that budget. For synchronous server use, consider running difficult custom settings in a worker.
127
126
 
128
- Drawing and play: `draw.ts`, `style.ts`, `strings.ts`, `mount.ts`, `worker.ts`, `ui.types.ts`. Framework and tag wrappers are separate entries.
127
+ 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.
129
128
 
130
- Types and domain constants are kept beside their modules. A new grid is a topology row, not a second copy of the rules. A theme changes drawing, never the answer.
129
+ ## The name
131
130
 
132
- ## References
131
+ *Jirai* (地雷) is Japanese for a land mine, read じらい, said in three beats, *ji-ra-i*. It is made of 地 (*ji*,
132
+ ground) and 雷 (*rai*, thunder): a mine is thunder buried in the ground. Every number on the board is a clue to
133
+ where it lies. ([Wiktionary: 地雷](https://en.wiktionary.org/wiki/地雷).)
133
134
 
134
- [Simon Tatham's Mines](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/doc/mines.html) is the reference for no-guess play, alternate tilings and wrapping. [David Hill's JSMinesweeper](https://github.com/DavidNHill/JSMinesweeper) is the reference for analysis, opening choices and chording. This implementation does not incorporate their source code.
135
+ ## Development
135
136
 
136
- The package follows the pure rules / separate draw / separate play structure of [Suido](https://github.com/johnmorrisdotca/suido), and the optional React wrapper pattern used by Tenka. MIT © John Morris.
137
+ ```sh
138
+ pnpm install --frozen-lockfile
139
+ pnpm check # lint, types and tests
140
+ pnpm test:package # build and import the actual npm tarball
141
+ pnpm test:demo # browser flows against the built page
142
+ pnpm site # build the standalone page into docs/
143
+ ```
137
144
 
138
- A run has `helped: true` after a proved hint is shown. This flag survives saved progress and lets a host distinguish assisted finishes. It is client-side state, not a competitive score verification mechanism.
145
+ 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.
146
+
147
+ ## Licence
148
+
149
+ [MIT](LICENSE) © John Morris. No third-party puzzle boards or artwork are included.
150
+
151
+ ## The family
152
+
153
+ <!-- family:start (made by scripts/family-readme.mjs from scripts/family-template.mjs; change those, not this) -->
154
+ Jirai is one of twenty-two packages, each made for the same site, each at
155
+ [github.com/johnmorrisdotca](https://github.com/johnmorrisdotca). The code of every one is MIT.
156
+
157
+ - [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/).
158
+ - [Kyuubu](https://github.com/johnmorrisdotca/kyuubu) (キューブ): a turning cube for the browser, 2×2 to 7×7, with record solves to replay. [Demo](https://johnmorrisdotca.github.io/kyuubu/).
159
+ - [Hitotsu](https://github.com/johnmorrisdotca/hitotsu) (一つ): a colour-card shedding game for two to eight, with the house rules people play. [Demo](https://johnmorrisdotca.github.io/hitotsu/).
160
+ - [Toranpu](https://github.com/johnmorrisdotca/toranpu) (トランプ): a deck of playing cards, card games with computer players, and solitaires. [Demo](https://johnmorrisdotca.github.io/toranpu/).
161
+ - [Tane](https://github.com/johnmorrisdotca/tane) (種): seeded random numbers and daily seeds, the same in every browser and on every server. [Demo](https://johnmorrisdotca.github.io/tane/).
162
+ - [Narabe](https://github.com/johnmorrisdotca/narabe) (並べ): one rules engine for abstract board games, from gomoku and Reversi to Go and checkers. [Demo](https://johnmorrisdotca.github.io/narabe/).
163
+ - [Tenka](https://github.com/johnmorrisdotca/tenka) (天下): world conquest for two to six, on a map of the real world. [Demo](https://johnmorrisdotca.github.io/tenka/).
164
+ - [Kumimoji](https://github.com/johnmorrisdotca/kumimoji) (組み文字): a crossword tile race, in English and Japanese kana. [Demo](https://johnmorrisdotca.github.io/kumimoji/).
165
+ - [Tsunagi](https://github.com/johnmorrisdotca/tsunagi) (繋ぎ): a line-joining logic puzzle whose every level has exactly one answer. [Demo](https://johnmorrisdotca.github.io/tsunagi/).
166
+ - [Jarajara](https://github.com/johnmorrisdotca/jarajara) (ジャラジャラ): mahjong tiles drawn as SVG, stacked layouts, and the matching solitaire Awase. [Demo](https://johnmorrisdotca.github.io/jarajara/).
167
+ - [Suido](https://github.com/johnmorrisdotca/suido) (水道): a pipe puzzle: turn the pieces until the water reaches every drain. [Demo](https://johnmorrisdotca.github.io/suido/).
168
+ - [Domino](https://github.com/johnmorrisdotca/domino) (ドミノ): dominoes and Mexican Train. [Demo](https://johnmorrisdotca.github.io/domino/).
169
+ - [Kotoba](https://github.com/johnmorrisdotca/kotoba) (言葉): word lists and word-game rules in English, French, German and Japanese. [Demo](https://johnmorrisdotca.github.io/kotoba/).
170
+ - [Sugoroku](https://github.com/johnmorrisdotca/sugoroku) (双六): backgammon and its variants, with the doubling cube and match play. [Demo](https://johnmorrisdotca.github.io/sugoroku/).
171
+ - [Kazu](https://github.com/johnmorrisdotca/kazu) (数): grid number puzzles: Sudoku and its variants, Futoshiki and Skyscrapers. [Demo](https://johnmorrisdotca.github.io/kazu/).
172
+ - [Meikyuu](https://github.com/johnmorrisdotca/meikyuu) (迷宮): mazes on squares, hexagons, triangles and circles, made from a seed and drawn through with a finger or the mouse. [Demo](https://johnmorrisdotca.github.io/meikyuu/).
173
+ - [Hikidashi](https://github.com/johnmorrisdotca/hikidashi) (引き出し): a drawer of small Japanese text tools: era dates, kanji numerals, readings and sentence difficulty. [Demo](https://johnmorrisdotca.github.io/hikidashi/).
174
+ - [Chizu](https://github.com/johnmorrisdotca/chizu) (地図): maps of the world and of countries' regions, in English and Japanese, with a quiz and callouts. [Demo](https://johnmorrisdotca.github.io/chizu/).
175
+ - [Bushu](https://github.com/johnmorrisdotca/bushu) (部首): find a kanji by the parts it is made of. [Demo](https://johnmorrisdotca.github.io/bushu/).
176
+ - [Tobiishi](https://github.com/johnmorrisdotca/tobiishi) (飛び石): peg solitaire with nine boards and seeded solvable challenges. [Demo](https://johnmorrisdotca.github.io/tobiishi/).
177
+ - [Jirai](https://github.com/johnmorrisdotca/jirai) (地雷): minesweeper on shaped grids with verified no-guess boards. [Demo](https://johnmorrisdotca.github.io/jirai/).
178
+ - [Gunjin](https://github.com/johnmorrisdotca/gunjin) (軍人): five hidden-rank strategy games with pass-the-device play. [Demo](https://johnmorrisdotca.github.io/gunjin/).
179
+
180
+ **This package is Jirai.** The demos of all twenty-two share one header and footer, so each links the rest.
181
+ <!-- family:end -->
182
+
183
+ ## Contributing and security
184
+
185
+ See [Contributing](CONTRIBUTING.md), the [Code of Conduct](CODE_OF_CONDUCT.md) and the [Security policy](SECURITY.md).
package/dist/element.d.ts CHANGED
@@ -2,6 +2,7 @@ declare const ElementBase: {
2
2
  new (): HTMLElement;
3
3
  prototype: HTMLElement;
4
4
  };
5
+ /** Configurable `<jirai-board>` element that owns its mounted game. */
5
6
  export declare class JiraiElement extends ElementBase {
6
7
  private mounted;
7
8
  static observedAttributes: string[];
@@ -10,5 +11,6 @@ export declare class JiraiElement extends ElementBase {
10
11
  attributeChangedCallback(): void;
11
12
  private mount;
12
13
  }
14
+ /** Registers the `<jirai-board>` custom element once in a registry. */
13
15
  export declare function defineJirai(registry?: CustomElementRegistry): void;
14
16
  export {};
package/dist/element.js CHANGED
@@ -3,6 +3,7 @@ import { mountJirai } from "./mount.js";
3
3
  // Importing this file on a server is harmless; only defineJirai registers the tag.
4
4
  const ElementBase = typeof HTMLElement === "undefined" ? class {
5
5
  } : HTMLElement;
6
+ /** Configurable `<jirai-board>` element that owns its mounted game. */
6
7
  export class JiraiElement extends ElementBase {
7
8
  mounted = null;
8
9
  static observedAttributes = ["width", "height", "mines", "seed", "grid", "shape", "no-guess", "material", "pieces", "lang"];
@@ -24,5 +25,6 @@ export class JiraiElement extends ElementBase {
24
25
  }
25
26
  }
26
27
  }
28
+ /** Registers the `<jirai-board>` custom element once in a registry. */
27
29
  export function defineJirai(registry = customElements) { if (!registry.get("jirai-board"))
28
30
  registry.define("jirai-board", JiraiElement); }
package/dist/game.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import type { Board, Game, Move, Settings, VisibleGame } from "./jirai.types.ts";
2
+ /** Creates an unstarted game with covered cells and no dealt answer. */
2
3
  export declare function newGame(settings?: Settings): Game;
3
4
  /** The clue-only view used by the solver. No hidden mine, including under a flag, survives it. */
4
5
  export declare function visibleGame(game: Game): VisibleGame;
5
- /** Accept a worker's dealt board without changing any moves made before the opening. */
6
+ /** Attaches a worker-dealt board that matches the game's settings, preserving existing moves. */
6
7
  export declare function withBoard(game: Game, board: Board): Game;
7
8
  /** A legal move returns a new game; an unavailable move returns the same one. */
8
9
  export declare function play(game: Game, move: Move): Game;
package/dist/game.js CHANGED
@@ -3,6 +3,7 @@ import { flood } from "./flood.js";
3
3
  import { makeBoard } from "./generate.js";
4
4
  import { DEFAULT_SETTINGS, MARKS, MOVES, STATUSES } from "./jirai.constants.js";
5
5
  import { neighbours, neighboursOf, validCell, validSettings } from "./grid.js";
6
+ /** Creates an unstarted game with covered cells and no dealt answer. */
6
7
  export function newGame(settings = DEFAULT_SETTINGS) {
7
8
  if (!validSettings(settings))
8
9
  throw new RangeError("Invalid Minesweeper settings.");
@@ -13,7 +14,7 @@ export function visibleGame(game) {
13
14
  return { settings: { ...game.settings }, marks: [...game.marks], status: game.status,
14
15
  clues: game.marks.map((mark, cell) => !activeCell(game.settings, cell) ? -2 : mark === MARKS.open ? game.board?.clues[cell] ?? null : null) };
15
16
  }
16
- /** Accept a worker's dealt board without changing any moves made before the opening. */
17
+ /** Attaches a worker-dealt board that matches the game's settings, preserving existing moves. */
17
18
  export function withBoard(game, board) {
18
19
  if (game.board !== null || (game.settings.shape ?? "rectangle") !== (board.settings.shape ?? "rectangle") || Object.keys(DEFAULT_SETTINGS).some(key => game.settings[key] !== board.settings[key]))
19
20
  throw new Error("The board does not belong to this game.");
package/dist/grid.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import type { Settings } from "./jirai.types.ts";
2
2
  /** Reject a setting before it reaches a board allocation or a shuffle. */
3
3
  export declare function validSettings(value: unknown): value is Settings;
4
+ /** Reports whether a row-major cell is inside the board's active shape. */
4
5
  export declare function validCell(settings: Settings, cell: number): boolean;
5
6
  /** Every neighbour once. Wrap joins both pairs of opposite edges. */
6
7
  export declare function neighbours(settings: Settings, cell: number): number[];
8
+ /** Precomputes the neighbour list for each row-major cell. */
7
9
  export declare function neighboursOf(settings: Settings): number[][];
package/dist/grid.js CHANGED
@@ -16,6 +16,7 @@ export function validSettings(value) {
16
16
  && (s.opening === "safe" || s.opening === "clear")
17
17
  && Number.isInteger(s.seed) && s.seed >= 0 && s.seed <= 0xffffffff;
18
18
  }
19
+ /** Reports whether a row-major cell is inside the board's active shape. */
19
20
  export function validCell(settings, cell) {
20
21
  return Number.isInteger(cell) && cell >= 0 && cell < settings.width * settings.height && activeCell(settings, cell);
21
22
  }
@@ -41,6 +42,7 @@ export function neighbours(settings, cell) {
41
42
  }
42
43
  return [...out].sort((a, b) => a - b);
43
44
  }
45
+ /** Precomputes the neighbour list for each row-major cell. */
44
46
  export function neighboursOf(settings) {
45
47
  return Array.from({ length: settings.width * settings.height }, (_, cell) => neighbours(settings, cell));
46
48
  }
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ export * from "./game.ts";
7
7
  export * from "./deduce.ts";
8
8
  export * from "./keep.ts";
9
9
  export { seededRandom } from "./random.ts";
10
- export declare const VERSION = "0.2.0";
10
+ /** Package version, kept in step with the release metadata. */
11
+ export declare const VERSION = "0.2.1";
11
12
  export { SHAPES, activeCell, activeCells } from "./shape.ts";
12
13
  export * from "./orthogonal.ts";
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ export * from "./game.js";
7
7
  export * from "./deduce.js";
8
8
  export * from "./keep.js";
9
9
  export { seededRandom } from "./random.js";
10
- export const VERSION = "0.2.0";
10
+ /** Package version, kept in step with the release metadata. */
11
+ export const VERSION = "0.2.1";
11
12
  export { SHAPES, activeCell, activeCells } from "./shape.js";
12
13
  export * from "./orthogonal.js";
@@ -1,32 +1,37 @@
1
1
  import type { Grid, Settings } from "./jirai.types.ts";
2
+ /** Names for the supported square, four-neighbour, hex, and wrap grids. */
2
3
  export declare const GRIDS: {
3
4
  readonly square: "square";
4
5
  readonly orthogonal: "orthogonal";
5
6
  readonly hex: "hex";
6
7
  readonly wrap: "wrap";
7
8
  };
9
+ /** Names for the game lifecycle states. */
8
10
  export declare const STATUSES: {
9
11
  readonly ready: "ready";
10
12
  readonly playing: "playing";
11
13
  readonly won: "won";
12
14
  readonly lost: "lost";
13
15
  };
16
+ /** Names for the cell mark cycle. */
14
17
  export declare const MARKS: {
15
18
  readonly covered: "covered";
16
19
  readonly flag: "flag";
17
20
  readonly question: "question";
18
21
  readonly open: "open";
19
22
  };
23
+ /** Names for the moves accepted by the engine. */
20
24
  export declare const MOVES: {
21
25
  readonly reveal: "reveal";
22
26
  readonly mark: "mark";
23
27
  readonly chord: "chord";
24
28
  };
25
- /** Hex coordinates are axial: a row is displaced half a cell to the right. */
29
+ /** Neighbour offsets and edge behaviour for each topology. Hex coordinates are axial: each row is displaced half a cell to the right. */
26
30
  export declare const GRID_SPECS: Record<Grid, {
27
31
  offsets: readonly (readonly [number, number])[];
28
32
  wrap: boolean;
29
33
  }>;
34
+ /** Common minefield dimensions and mine counts. */
30
35
  export declare const PRESETS: {
31
36
  readonly beginner: {
32
37
  readonly width: 9;
@@ -54,9 +59,15 @@ export declare const PRESETS: {
54
59
  readonly mines: 24;
55
60
  };
56
61
  };
62
+ /** Default beginner game, with a verified no-guess board and clear opening. */
57
63
  export declare const DEFAULT_SETTINGS: Settings;
64
+ /** Largest width or height accepted by settings validation. */
58
65
  export declare const MAX_SIDE = 60;
66
+ /** Largest total cell count accepted by settings validation. */
59
67
  export declare const MAX_CELLS = 2400;
68
+ /** Default candidate-board budget for verified generation. */
60
69
  export declare const GENERATION_ATTEMPTS = 128;
70
+ /** Largest frontier enumerated for exact deductions. */
61
71
  export declare const ENUMERATION_CELLS = 18;
72
+ /** Maximum partial assignments checked in one exact deduction search. */
62
73
  export declare const ENUMERATION_NODES = 100000;
@@ -1,14 +1,19 @@
1
+ /** Names for the supported square, four-neighbour, hex, and wrap grids. */
1
2
  export const GRIDS = { square: "square", orthogonal: "orthogonal", hex: "hex", wrap: "wrap" };
3
+ /** Names for the game lifecycle states. */
2
4
  export const STATUSES = { ready: "ready", playing: "playing", won: "won", lost: "lost" };
5
+ /** Names for the cell mark cycle. */
3
6
  export const MARKS = { covered: "covered", flag: "flag", question: "question", open: "open" };
7
+ /** Names for the moves accepted by the engine. */
4
8
  export const MOVES = { reveal: "reveal", mark: "mark", chord: "chord" };
5
- /** Hex coordinates are axial: a row is displaced half a cell to the right. */
9
+ /** Neighbour offsets and edge behaviour for each topology. Hex coordinates are axial: each row is displaced half a cell to the right. */
6
10
  export const GRID_SPECS = {
7
11
  square: { offsets: [[-1, -1], [0, -1], [1, -1], [-1, 0], [1, 0], [-1, 1], [0, 1], [1, 1]], wrap: false },
8
12
  orthogonal: { offsets: [[0, -1], [-1, 0], [1, 0], [0, 1]], wrap: false },
9
13
  hex: { offsets: [[-1, 0], [1, 0], [0, -1], [1, -1], [-1, 1], [0, 1]], wrap: false },
10
14
  wrap: { offsets: [[-1, -1], [0, -1], [1, -1], [-1, 0], [1, 0], [-1, 1], [0, 1], [1, 1]], wrap: true },
11
15
  };
16
+ /** Common minefield dimensions and mine counts. */
12
17
  export const PRESETS = {
13
18
  beginner: { width: 9, height: 9, mines: 10 },
14
19
  intermediate: { width: 16, height: 16, mines: 40 },
@@ -16,9 +21,15 @@ export const PRESETS = {
16
21
  wide: { width: 21, height: 9, mines: 24 },
17
22
  tall: { width: 9, height: 21, mines: 24 },
18
23
  };
24
+ /** Default beginner game, with a verified no-guess board and clear opening. */
19
25
  export const DEFAULT_SETTINGS = { ...PRESETS.beginner, grid: GRIDS.square, noGuess: true, opening: "clear", seed: 1 };
26
+ /** Largest width or height accepted by settings validation. */
20
27
  export const MAX_SIDE = 60;
28
+ /** Largest total cell count accepted by settings validation. */
21
29
  export const MAX_CELLS = 2400;
30
+ /** Default candidate-board budget for verified generation. */
22
31
  export const GENERATION_ATTEMPTS = 128;
32
+ /** Largest frontier enumerated for exact deductions. */
23
33
  export const ENUMERATION_CELLS = 18;
34
+ /** Maximum partial assignments checked in one exact deduction search. */
24
35
  export const ENUMERATION_NODES = 100_000;
@@ -1,7 +1,10 @@
1
- /** A cell's number is its row-major place on the board, starting at zero. */
1
+ /** Neighbour topology used to count the clues around each cell. */
2
2
  export type Grid = "square" | "orthogonal" | "hex" | "wrap";
3
+ /** Current game state, including whether a mine has been hit. */
3
4
  export type Status = "ready" | "playing" | "won" | "lost";
5
+ /** Player-facing state of one cell; the answer is never a mark. */
4
6
  export type Mark = "covered" | "flag" | "question" | "open";
7
+ /** Board dimensions and rules used to deal and validate a game. */
5
8
  export type Settings = {
6
9
  /** Shape omits cells; rectangle is the compatible default. Wrap requires rectangle. */
7
10
  shape?: "rectangle" | "heart" | "star" | "hexagon";
@@ -15,10 +18,12 @@ export type Settings = {
15
18
  opening: "safe" | "clear";
16
19
  seed: number;
17
20
  };
21
+ /** A reveal, mark-cycle, or numbered-cell chord applied to a game. */
18
22
  export type Move = {
19
23
  kind: "reveal" | "mark" | "chord";
20
24
  cell: number;
21
25
  };
26
+ /** A dealt board including its answer; keep it off public clients. */
22
27
  export type Board = {
23
28
  settings: Settings;
24
29
  mines: readonly boolean[];
@@ -37,6 +42,7 @@ export type Game = {
37
42
  /** A proved hint has been shown during this run. */
38
43
  helped: boolean;
39
44
  };
45
+ /** Answer-free state suitable for deductions, hints, and public display. */
40
46
  export type VisibleGame = {
41
47
  settings: Settings;
42
48
  /** Null is hidden, including flags. Only opened cells give a clue. */
@@ -44,11 +50,13 @@ export type VisibleGame = {
44
50
  marks: readonly Mark[];
45
51
  status: Status;
46
52
  };
53
+ /** An exact count over unknown cells, with the clues that supplied it. */
47
54
  export type Constraint = {
48
55
  cells: readonly number[];
49
56
  mines: number;
50
57
  sources: readonly number[];
51
58
  };
59
+ /** Certain safe cells and mines proved from visible clues. */
52
60
  export type Deduction = {
53
61
  safe: readonly number[];
54
62
  mines: readonly number[];
@@ -57,10 +65,14 @@ export type Deduction = {
57
65
  /** A conflicting clue, or a board with no consistent mine placement. */
58
66
  contradiction: boolean;
59
67
  };
68
+ /** Work limits for seeded board generation and its no-guess check. */
60
69
  export type GenerationOptions = {
61
70
  attempts?: number;
62
71
  enumerate?: boolean;
63
72
  };
73
+ /** Supported interface copy locales. */
64
74
  export type Language = "en" | "ja";
75
+ /** Board colours for drawing and mounted play. */
65
76
  export type Material = "ivory" | "wood" | "slate";
77
+ /** Symbols used to mark mines. */
66
78
  export type Pieces = "flags" | "stones" | "flowers";
@@ -1,6 +1,7 @@
1
1
  import type { Board, GenerationOptions, Game, Settings } from "./jirai.types.ts";
2
2
  /** Identifies the four-neighbour rules and progress-code format. */
3
3
  export declare const ORTHOGONAL_VARIANT: "orthogonal";
4
+ /** Settings accepted by the four-neighbour-specific helpers. */
4
5
  export type OrthogonalSettings = Omit<Settings, "grid">;
5
6
  /** Starts a game under four-neighbour rules. */
6
7
  export declare function newOrthogonalGame(settings: OrthogonalSettings): Game;
package/dist/react.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  import type { JiraiProps } from "./react.types.ts";
2
- /** The plain-DOM game as a React component. Options are read on mount; give it a new key to load a new board. */
2
+ /** Renders the DOM player inside a React-owned host. Options are read on mount; use a new key to load a new board. */
3
3
  export declare function JiraiBoard({ settings, progress, material, pieces, language, controls, onChange, onFinish, onError, ...element }: JiraiProps): import("react").JSX.Element;
package/dist/react.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useEffect, useRef } from "react";
3
3
  import { mountJirai } from "./mount.js";
4
- /** The plain-DOM game as a React component. Options are read on mount; give it a new key to load a new board. */
4
+ /** Renders the DOM player inside a React-owned host. Options are read on mount; use a new key to load a new board. */
5
5
  export function JiraiBoard({ settings, progress, material, pieces, language, controls, onChange, onFinish, onError, ...element }) {
6
6
  const host = useRef(null);
7
7
  const latest = useRef({ onChange, onFinish, onError });