@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 +17 -1
- package/README.md +128 -81
- package/dist/element.d.ts +2 -0
- package/dist/element.js +2 -0
- package/dist/game.d.ts +2 -1
- package/dist/game.js +2 -1
- package/dist/grid.d.ts +2 -0
- package/dist/grid.js +2 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/jirai.constants.d.ts +12 -1
- package/dist/jirai.constants.js +12 -1
- package/dist/jirai.types.d.ts +13 -1
- package/dist/orthogonal.d.ts +1 -0
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/react.types.d.ts +1 -0
- package/dist/shape.d.ts +2 -0
- package/dist/shape.js +2 -0
- package/dist/strings.d.ts +2 -0
- package/dist/strings.js +2 -0
- package/dist/ui.types.d.ts +5 -0
- package/package.json +35 -8
- package/src/element.ts +2 -0
- package/src/family.test.js +105 -0
- package/src/game.ts +2 -1
- package/src/grid.ts +2 -0
- package/src/index.ts +2 -1
- package/src/jirai.constants.ts +12 -1
- package/src/jirai.types.ts +13 -1
- package/src/orthogonal.ts +1 -0
- package/src/react.tsx +1 -1
- package/src/react.types.ts +1 -0
- package/src/shape.ts +2 -0
- package/src/strings.ts +2 -0
- package/src/ui.types.ts +5 -0
- package/src/deduce.test.ts +0 -47
- package/src/game.test.ts +0 -172
- package/src/shape.test.ts +0 -56
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
|
|
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
|
-
|
|
1
|
+
<h1 align="center">Jirai <sub>地雷</sub></h1>
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
30
|
+
import { DEFAULT_SETTINGS, hintFor, newGame, play, visibleGame } from "@johnmorrisdotca/jirai";
|
|
31
31
|
|
|
32
|
-
let game = newGame({ ...DEFAULT_SETTINGS, width:
|
|
33
|
-
game = play(game, { kind: "reveal", cell:
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
+
## What it does
|
|
41
48
|
|
|
42
|
-
|
|
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
|
-
|
|
58
|
+
## Use it in a page
|
|
45
59
|
|
|
46
|
-
|
|
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>("#
|
|
52
|
-
settings: { ...DEFAULT_SETTINGS, grid: "
|
|
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) {
|
|
57
|
-
onFinish(game) {
|
|
58
|
-
onError(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
|
|
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
|
-
|
|
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
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
94
|
+
## Rules and settings
|
|
92
95
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
109
|
+
## Engine and saved games
|
|
103
110
|
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
117
|
+
## Drawing and theming
|
|
111
118
|
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
123
|
+
## Limits and browser support
|
|
125
124
|
|
|
126
|
-
|
|
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
|
-
|
|
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
|
-
|
|
129
|
+
## The name
|
|
131
130
|
|
|
132
|
-
|
|
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
|
-
|
|
135
|
+
## Development
|
|
135
136
|
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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;
|
package/dist/jirai.constants.js
CHANGED
|
@@ -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:
|
|
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;
|
package/dist/jirai.types.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
/**
|
|
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";
|
package/dist/orthogonal.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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 });
|