@johnmorrisdotca/jirai 0.2.0 → 0.3.0
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 +25 -1
- package/README.md +172 -79
- 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/generate.d.ts +7 -1
- package/dist/generate.js +74 -43
- package/dist/grid.d.ts +2 -0
- package/dist/grid.js +2 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +4 -1
- package/dist/jirai.constants.d.ts +55 -10
- package/dist/jirai.constants.js +28 -5
- package/dist/jirai.types.d.ts +24 -1
- package/dist/levels.d.ts +16 -0
- package/dist/levels.js +29 -0
- package/dist/measure.d.ts +28 -0
- package/dist/measure.js +21 -0
- 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/repair.d.ts +14 -0
- package/dist/repair.js +112 -0
- package/dist/shape.d.ts +2 -0
- package/dist/shape.js +2 -0
- package/dist/solve.d.ts +79 -0
- package/dist/solve.js +336 -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 +37 -9
- package/src/element.ts +2 -0
- package/src/family.test.js +105 -0
- package/src/game.ts +2 -1
- package/src/generate.ts +60 -33
- package/src/grid.ts +2 -0
- package/src/index.ts +7 -1
- package/src/jirai.constants.ts +28 -5
- package/src/jirai.types.ts +23 -2
- package/src/levels.ts +31 -0
- package/src/measure.ts +44 -0
- package/src/orthogonal.ts +1 -0
- package/src/react.tsx +1 -1
- package/src/react.types.ts +1 -0
- package/src/repair.ts +88 -0
- package/src/shape.ts +2 -0
- package/src/solve.ts +253 -0
- package/src/strings.ts +2 -0
- package/src/ui.types.ts +5 -0
- package/src/version.test.js +9 -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,34 @@
|
|
|
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.3.0] - 2026-10-05
|
|
10
|
+
|
|
11
|
+
- An extra-hard level, 40×24 with 240 mines (25%), joins easy, medium and hard on every rule and outline, and every field is still proved to need no guess. `beginner`, `intermediate` and `expert` stay as names for easy, medium and hard, and `PRESETS` keeps them. New: `LEVELS`, `LEVEL_SIZES`, `LEVEL_ALIASES`, `levelNamed`, `levelSettings` (shaped boards keep a level's size and share of mines) and `measureBoard`, which scores density, multi-step share and depth of deduction so the levels can be shown to step up.
|
|
12
|
+
- No-guess dealing is much faster and no longer fails on dense fields: a one-pass solver that agrees with the hint engine replaces the repeated scans (a 30×16 expert square field took over half a second, now about 2 ms), and when no random layout can be finished the closest is repaired by moving single mines next to where the solver stopped. Orthogonal expert fields, which failed five times in six, are now dealt every time. New options `repairs` and `repair: false` (the 0.2 behaviour of giving up after the random layouts).
|
|
13
|
+
- Every field 0.2.1 dealt is dealt again, identically, for the same seed and settings (pinned by tests), and saved games from 0.2.1 replay unchanged. Only settings that used to throw `GenerationError` deal differently, because they now succeed.
|
|
14
|
+
- Narrowed: a cell with no neighbours under the rules (a tip of an orthogonal or hexagonal star) cannot be opened from without a guess, so an opening there now throws `GenerationError` with code `"opening"`, and unreachable cells are made mines. Nothing else is narrowed: extra-hard is offered on square, orthogonal, hexagonal and wraparound grids and on rectangle, heart, star and hexagon outlines.
|
|
15
|
+
- The demo's Size menu is now Level (easy, medium, hard, extra-hard, wide, tall), in English and Japanese, follows the grid and outline, and reads `?level=` (old names too). The Playwright port can be set with `TEST_PORT`.
|
|
16
|
+
|
|
17
|
+
## [0.2.1] - 2026-10-05
|
|
18
|
+
|
|
19
|
+
- The package no longer ships its unit tests (`src/*.test.ts`) in the npm tarball.
|
|
20
|
+
- 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.
|
|
21
|
+
- 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.
|
|
22
|
+
- The package check runs on Windows too, where npm is a .cmd file.
|
|
23
|
+
- 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.
|
|
24
|
+
- Complete package presentation: desktop and phone screenshots, badges, demo/API links, targeted keywords and linked MIT licence.
|
|
25
|
+
- Source-derived API reference and public API comments, contribution/security files, and a package presentation gate.
|
|
26
|
+
|
|
3
27
|
## [0.2.0] - 2026-10-05
|
|
4
28
|
|
|
5
29
|
- 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
30
|
|
|
7
|
-
## 0.1.0
|
|
31
|
+
## [0.1.0] - 2026-10-04
|
|
8
32
|
|
|
9
33
|
- Square, hexagonal and wraparound fields, seeded and configurable.
|
|
10
34
|
- Safe and clear openings, verified no-guess generation and explained deductions.
|
package/README.md
CHANGED
|
@@ -1,138 +1,231 @@
|
|
|
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
|
+
- **Four levels:** easy (9×9, 10 mines), medium (16×16, 40), hard (30×16, 99) and extra-hard (40×24, 240), on every rule and outline. The old names `beginner`, `intermediate` and `expert` still work and mean easy, medium and hard.
|
|
53
|
+
- **Verified no-guess deals:** optional deduction-only dealing accepts a board only when the solver proves every safe cell from the opening, even at extra-hard's 25% mines. It throws `GenerationError` when the bounded search cannot prove one.
|
|
54
|
+
- **Fixed seeded fields:** after the opening, mines never move. A seed and settings reproduce the same deal.
|
|
55
|
+
- **Familiar play:** reveal, flag, question-mark, chord, flood-open, explained hint, timer, undo by saved replay, and a just-the-board dialog.
|
|
56
|
+
- **Accessible controls:** keyboard navigation, pointer and touch, long press to mark, English and Japanese strings, and board labels read by assistive technology.
|
|
57
|
+
- **Materials and markers:** ivory, wood or slate; flags, stones or flowers. Host CSS can replace the palette.
|
|
43
58
|
|
|
44
|
-
|
|
59
|
+
## Use it in a page
|
|
45
60
|
|
|
46
|
-
|
|
61
|
+
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
62
|
|
|
48
63
|
```ts
|
|
49
|
-
import { mountJirai } from "@johnmorrisdotca/jirai/play";
|
|
64
|
+
import { DEFAULT_SETTINGS, mountJirai } from "@johnmorrisdotca/jirai/play";
|
|
50
65
|
|
|
51
|
-
const board = mountJirai(document.querySelector<HTMLElement>("#
|
|
52
|
-
settings: { ...DEFAULT_SETTINGS, grid: "
|
|
66
|
+
const board = mountJirai(document.querySelector<HTMLElement>("#game")!, {
|
|
67
|
+
settings: { ...DEFAULT_SETTINGS, grid: "orthogonal", seed: 7 },
|
|
53
68
|
material: "wood",
|
|
54
69
|
pieces: "stones",
|
|
55
70
|
language: "en",
|
|
56
|
-
onChange(game) {
|
|
57
|
-
onFinish(game) {
|
|
58
|
-
onError(error) {
|
|
71
|
+
onChange(game) { localStorage.setItem("jirai", board.progress()); },
|
|
72
|
+
onFinish(game) { console.log(game.status, game.helped); },
|
|
73
|
+
onError(error) { console.error(error); },
|
|
59
74
|
});
|
|
60
|
-
|
|
61
75
|
board.set({ material: "slate", language: "ja" });
|
|
62
76
|
board.restart();
|
|
63
77
|
board.destroy();
|
|
64
78
|
```
|
|
65
79
|
|
|
66
|
-
The handle also
|
|
80
|
+
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
81
|
|
|
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:
|
|
82
|
+
Use the custom element without a mount call:
|
|
80
83
|
|
|
81
84
|
```html
|
|
82
85
|
<script type="module">
|
|
83
86
|
import "@johnmorrisdotca/jirai/element/define";
|
|
84
87
|
</script>
|
|
85
|
-
<jirai-board
|
|
86
|
-
|
|
88
|
+
<jirai-board width="9" height="9" mines="10" seed="42"
|
|
89
|
+
grid="orthogonal" opening="clear" no-guess="true"
|
|
90
|
+
material="wood" pieces="stones" lang="ja"></jirai-board>
|
|
87
91
|
```
|
|
88
92
|
|
|
89
|
-
|
|
93
|
+
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.
|
|
94
|
+
|
|
95
|
+
## Rules and settings
|
|
96
|
+
|
|
97
|
+
| Setting | Values and limits |
|
|
98
|
+
| --- | --- |
|
|
99
|
+
| `grid` | `square` (eight neighbours), `orthogonal` (four), `hex` (six), or `wrap` (eight; opposite edges join). |
|
|
100
|
+
| `shape` | `rectangle`, `heart`, `star` or `hexagon`; non-rectangles require at least 9 rows and columns. Wraparound requires `rectangle`. |
|
|
101
|
+
| `width`, `height` | 3–60 each, with at most 2,400 total cells. |
|
|
102
|
+
| `mines` | At least one, fewer than active cells, and low enough to leave the selected opening. |
|
|
103
|
+
| `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. |
|
|
104
|
+
| `opening` | `safe` protects the first cell; `clear` also protects its neighbours. |
|
|
105
|
+
| `seed` | Integer from 0 through 4,294,967,295. A seed is interpreted together with all other settings and the first cell. |
|
|
106
|
+
| `material`, `pieces`, `language` | `ivory`, `wood`, `slate`; `flags`, `stones`, `flowers`; `en`, `ja`. |
|
|
90
107
|
|
|
91
|
-
##
|
|
108
|
+
## Levels
|
|
109
|
+
|
|
110
|
+
| Level | Size | Mines | Mines per cell | Old name |
|
|
111
|
+
| --- | --- | --- | --- | --- |
|
|
112
|
+
| `easy` | 9×9 | 10 | 12% | `beginner` |
|
|
113
|
+
| `medium` | 16×16 | 40 | 16% | `intermediate` |
|
|
114
|
+
| `hard` | 30×16 | 99 | 21% | `expert` |
|
|
115
|
+
| `extra-hard` | 40×24 | 240 | 25% | |
|
|
116
|
+
|
|
117
|
+
`levelNamed(name)` returns the level a name means (`extra-hard` may also be written `extra hard`, `extra_hard` or `extraHard`) or `null`. `levelSettings(level, { grid, shape })` returns `{ width, height, mines }`: a rectangle gets the numbers above, and a heart, star or hexagon outline keeps the level's width and height and its share of mines over the cells that are left. `PRESETS` holds the four levels, the three old names, and `wide` (21×9, 24) and `tall` (9×21, 24).
|
|
92
118
|
|
|
93
119
|
```ts
|
|
94
|
-
import {
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
const seed = dailySeed("2026-10-04", "hex");
|
|
120
|
+
import { DEFAULT_SETTINGS, levelSettings, newGame } from "@johnmorrisdotca/jirai";
|
|
121
|
+
|
|
122
|
+
const game = newGame({ ...DEFAULT_SETTINGS, ...levelSettings("extra-hard", { grid: "hex", shape: "star" }), grid: "hex", shape: "star", seed: 7 });
|
|
98
123
|
```
|
|
99
124
|
|
|
100
|
-
|
|
125
|
+
The levels step up a measured difficulty. `measureBoard(board)` solves a dealt board with the same deductions the hints use and reports its `density`, the share of cells proved by anything beyond one clue's count (`multiStep`), the longest chain of deductions (`depth`), the cells proved by each kind of reasoning, and a `score` (`100 × density + 100 × multiStep + depth ÷ 4`) that puts boards in order. It is not a prediction of how long a person takes. Mean score of 30 seeds, rectangles, opening in the middle:
|
|
101
126
|
|
|
102
|
-
|
|
127
|
+
| Rule | easy | medium | hard | extra-hard |
|
|
128
|
+
| --- | --- | --- | --- | --- |
|
|
129
|
+
| square (8 neighbours) | 26 | 41 | 63 | 85 |
|
|
130
|
+
| orthogonal (4) | 18 | 26 | 38 | 49 |
|
|
131
|
+
| hexagonal (6) | 21 | 31 | 49 | 65 |
|
|
132
|
+
| wraparound (8) | 25 | 41 | 67 | 96 |
|
|
103
133
|
|
|
104
|
-
|
|
134
|
+
Orthogonal clues carry less information, so its numbers are smaller at every level; each level is still at least a quarter harder than the one before it on every rule.
|
|
105
135
|
|
|
106
|
-
|
|
136
|
+
## Engine and saved games
|
|
107
137
|
|
|
108
|
-
|
|
138
|
+
`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. The solver runs the hint engine's deductions to a fixed point in one pass, and is tested to agree with `deduce` on every board.
|
|
109
139
|
|
|
110
|
-
|
|
140
|
+
`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.
|
|
111
141
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
node scripts/serve.mjs
|
|
120
|
-
```
|
|
142
|
+
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.
|
|
143
|
+
|
|
144
|
+
## Drawing and theming
|
|
145
|
+
|
|
146
|
+
`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.
|
|
147
|
+
|
|
148
|
+
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.
|
|
121
149
|
|
|
122
|
-
|
|
150
|
+
## Limits and browser support
|
|
123
151
|
|
|
124
|
-
|
|
152
|
+
The board is capped at 60 cells per side and 2,400 cells overall. Shapes need at least 9×9; wraparound is rectangular.
|
|
125
153
|
|
|
126
|
-
|
|
154
|
+
**How a no-guess field is dealt.** First the generator draws up to 128 random layouts (`attempts`), exactly as 0.2 did, so every field those versions dealt is dealt again, unchanged, for the same seed. If none of them can be finished by deduction it takes the closest and repairs it: single mines are moved next to the place the solver stopped, a move is kept when it leaves the solver no worse off, and the search starts over from a new layout when it stalls. The work is counted in layouts tried (`repairs`, 12 per cell by default), never in time, so a seed always gives the same field. Every field returned is proved by the same deductions either way; `board.attempt` at or above `attempts` says the field was repaired. `makeBoard(settings, first, { attempts, repairs, repair, enumerate })` accepts 1–10,000 attempts, 0–1,000,000 repairs, `repair: false` for the 0.2 behaviour of giving up after the random layouts, and `enumerate` to switch the exact small-frontier check. Failure raises `GenerationError` rather than returning a guessing field.
|
|
127
155
|
|
|
128
|
-
|
|
156
|
+
**Measured generation** (200 seeds for each rule, outline and level, with the opening on a random cell; Node 24 on one core of a shared laptop, so read the milliseconds as an order of magnitude). Every level on every combination dealt a verified field every time, except for the one case below. The slowest single deal in the whole run was 230 ms; the median at extra-hard, the largest and densest level, was 36 ms for a square rectangle (the slowest median), 25 ms orthogonal, 22 ms hexagonal and 3 ms wraparound; its 95th percentile was at most 55 ms. Before 0.3, orthogonal `expert` boards failed five times in six and square ones took over half a second.
|
|
129
157
|
|
|
130
|
-
|
|
158
|
+
| Level | Median | 95th percentile | Slowest |
|
|
159
|
+
| --- | --- | --- | --- |
|
|
160
|
+
| easy | 0.1–0.5 ms | 0.3–1.1 ms | 3.3 ms |
|
|
161
|
+
| medium | 0.2–1.7 ms | 0.4–3.1 ms | 3.6 ms |
|
|
162
|
+
| hard | 0.6–7.7 ms | 1.2–11.8 ms | 16.7 ms |
|
|
163
|
+
| extra-hard | 3–36 ms | 10–55 ms | 229 ms |
|
|
131
164
|
|
|
132
|
-
|
|
165
|
+
**What a field cannot do, and what is done about it.**
|
|
133
166
|
|
|
134
|
-
|
|
167
|
+
- A cell with no neighbours under the rules (a tip of an orthogonal or hexagonal star) can never be told by a clue. `makeBoard` refuses an opening on one with `GenerationError` code `"opening"` (open another cell), and a field whose opening can reach only part of the outline makes the cut-off cells mines, since nobody has to find a mine to win. This is the only case in which an extra-hard deal fails: 2 of 340 cells on an orthogonal star, 1 on a hexagonal star, 0 on any other outline.
|
|
168
|
+
- Extra-hard is not narrowed for any rule or outline: all of them reach 25% reliably. A custom field much denser than that is still not promised; it raises `GenerationError` when the work budget runs out.
|
|
169
|
+
- The widest fields (30 and 40 columns) scroll sideways inside the board on a narrow screen, as hard always has.
|
|
135
170
|
|
|
136
|
-
|
|
171
|
+
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.
|
|
172
|
+
|
|
173
|
+
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.
|
|
174
|
+
|
|
175
|
+
## The name
|
|
176
|
+
|
|
177
|
+
*Jirai* (地雷) is Japanese for a land mine, read じらい, said in three beats, *ji-ra-i*. It is made of 地 (*ji*,
|
|
178
|
+
ground) and 雷 (*rai*, thunder): a mine is thunder buried in the ground. Every number on the board is a clue to
|
|
179
|
+
where it lies. ([Wiktionary: 地雷](https://en.wiktionary.org/wiki/地雷).)
|
|
180
|
+
|
|
181
|
+
## Development
|
|
182
|
+
|
|
183
|
+
```sh
|
|
184
|
+
pnpm install --frozen-lockfile
|
|
185
|
+
pnpm check # lint, types and tests
|
|
186
|
+
pnpm test:package # build and import the actual npm tarball
|
|
187
|
+
pnpm test:demo # browser flows against the built page
|
|
188
|
+
pnpm site # build the standalone page into docs/
|
|
189
|
+
```
|
|
137
190
|
|
|
138
|
-
|
|
191
|
+
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.
|
|
192
|
+
|
|
193
|
+
## Licence
|
|
194
|
+
|
|
195
|
+
[MIT](LICENSE) © John Morris. No third-party puzzle boards or artwork are included.
|
|
196
|
+
|
|
197
|
+
## The family
|
|
198
|
+
|
|
199
|
+
<!-- family:start (made by scripts/family-readme.mjs from scripts/family-template.mjs; change those, not this) -->
|
|
200
|
+
Jirai is one of twenty-two packages, each made for the same site, each at
|
|
201
|
+
[github.com/johnmorrisdotca](https://github.com/johnmorrisdotca). The code of every one is MIT.
|
|
202
|
+
|
|
203
|
+
- [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/).
|
|
204
|
+
- [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/).
|
|
205
|
+
- [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/).
|
|
206
|
+
- [Toranpu](https://github.com/johnmorrisdotca/toranpu) (トランプ): a deck of playing cards, card games with computer players, and solitaires. [Demo](https://johnmorrisdotca.github.io/toranpu/).
|
|
207
|
+
- [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/).
|
|
208
|
+
- [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/).
|
|
209
|
+
- [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/).
|
|
210
|
+
- [Kumimoji](https://github.com/johnmorrisdotca/kumimoji) (組み文字): a crossword tile race, in English and Japanese kana. [Demo](https://johnmorrisdotca.github.io/kumimoji/).
|
|
211
|
+
- [Tsunagi](https://github.com/johnmorrisdotca/tsunagi) (繋ぎ): a line-joining logic puzzle whose every level has exactly one answer. [Demo](https://johnmorrisdotca.github.io/tsunagi/).
|
|
212
|
+
- [Jarajara](https://github.com/johnmorrisdotca/jarajara) (ジャラジャラ): mahjong tiles drawn as SVG, stacked layouts, and the matching solitaire Awase. [Demo](https://johnmorrisdotca.github.io/jarajara/).
|
|
213
|
+
- [Suido](https://github.com/johnmorrisdotca/suido) (水道): a pipe puzzle: turn the pieces until the water reaches every drain. [Demo](https://johnmorrisdotca.github.io/suido/).
|
|
214
|
+
- [Domino](https://github.com/johnmorrisdotca/domino) (ドミノ): dominoes and Mexican Train. [Demo](https://johnmorrisdotca.github.io/domino/).
|
|
215
|
+
- [Kotoba](https://github.com/johnmorrisdotca/kotoba) (言葉): word lists and word-game rules in English, French, German and Japanese. [Demo](https://johnmorrisdotca.github.io/kotoba/).
|
|
216
|
+
- [Sugoroku](https://github.com/johnmorrisdotca/sugoroku) (双六): backgammon and its variants, with the doubling cube and match play. [Demo](https://johnmorrisdotca.github.io/sugoroku/).
|
|
217
|
+
- [Kazu](https://github.com/johnmorrisdotca/kazu) (数): grid number puzzles: Sudoku and its variants, Futoshiki and Skyscrapers. [Demo](https://johnmorrisdotca.github.io/kazu/).
|
|
218
|
+
- [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/).
|
|
219
|
+
- [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/).
|
|
220
|
+
- [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/).
|
|
221
|
+
- [Bushu](https://github.com/johnmorrisdotca/bushu) (部首): find a kanji by the parts it is made of. [Demo](https://johnmorrisdotca.github.io/bushu/).
|
|
222
|
+
- [Tobiishi](https://github.com/johnmorrisdotca/tobiishi) (飛び石): peg solitaire with nine boards and seeded solvable challenges. [Demo](https://johnmorrisdotca.github.io/tobiishi/).
|
|
223
|
+
- [Jirai](https://github.com/johnmorrisdotca/jirai) (地雷): minesweeper on shaped grids with verified no-guess boards. [Demo](https://johnmorrisdotca.github.io/jirai/).
|
|
224
|
+
- [Gunjin](https://github.com/johnmorrisdotca/gunjin) (軍人): five hidden-rank strategy games with pass-the-device play. [Demo](https://johnmorrisdotca.github.io/gunjin/).
|
|
225
|
+
|
|
226
|
+
**This package is Jirai.** The demos of all twenty-two share one header and footer, so each links the rest.
|
|
227
|
+
<!-- family:end -->
|
|
228
|
+
|
|
229
|
+
## Contributing and security
|
|
230
|
+
|
|
231
|
+
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/generate.d.ts
CHANGED
|
@@ -6,5 +6,11 @@ export declare class GenerationError extends Error {
|
|
|
6
6
|
}
|
|
7
7
|
/** Verify a board from one opening, using only clues that have been uncovered. */
|
|
8
8
|
export declare function isSolvable(board: Board, enumerate?: boolean): boolean;
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Deal after the first reveal. A seed fixes the whole candidate stream and the accepted board.
|
|
11
|
+
*
|
|
12
|
+
* A no-guess deal first draws random layouts, as 0.1 and 0.2 did, so every board those versions dealt is still dealt.
|
|
13
|
+
* If none of them can be finished by deduction it takes the closest and repairs it: single mines are moved near the
|
|
14
|
+
* place the solver stopped until the solver finishes the whole field. The result is proved the same way either route.
|
|
15
|
+
*/
|
|
10
16
|
export declare function makeBoard(settings: Settings, first: number, options?: GenerationOptions): Board;
|
package/dist/generate.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { flood } from "./flood.js";
|
|
4
|
-
import { GENERATION_ATTEMPTS, MARKS, STATUSES } from "./jirai.constants.js";
|
|
1
|
+
import { activeCells } from "./shape.js";
|
|
2
|
+
import { GENERATION_ATTEMPTS } from "./jirai.constants.js";
|
|
5
3
|
import { neighbours, neighboursOf, validCell, validSettings } from "./grid.js";
|
|
4
|
+
import { repair } from "./repair.js";
|
|
6
5
|
import { seededRandom, shuffled } from "./random.js";
|
|
6
|
+
import { Solver } from "./solve.js";
|
|
7
7
|
/** A bounded generator must say it failed, never quietly return a guessing board. */
|
|
8
8
|
export class GenerationError extends Error {
|
|
9
9
|
code;
|
|
@@ -15,58 +15,89 @@ export class GenerationError extends Error {
|
|
|
15
15
|
}
|
|
16
16
|
/** Verify a board from one opening, using only clues that have been uncovered. */
|
|
17
17
|
export function isSolvable(board, enumerate = true) {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
const known = new Set();
|
|
21
|
-
flood(board.clues, adjacent, open, [board.first]);
|
|
22
|
-
while (open.filter(Boolean).length < activeCells(board.settings).length - board.settings.mines) {
|
|
23
|
-
const visible = {
|
|
24
|
-
settings: board.settings, clues: board.clues.map((n, cell) => activeCell(board.settings, cell) ? open[cell] ? n : null : -2),
|
|
25
|
-
marks: open.map((yes) => yes ? MARKS.open : MARKS.covered), status: STATUSES.playing,
|
|
26
|
-
};
|
|
27
|
-
const result = deduce(visible, known, enumerate);
|
|
28
|
-
if (result.contradiction)
|
|
29
|
-
return false;
|
|
30
|
-
let changed = false;
|
|
31
|
-
for (const cell of result.mines) {
|
|
32
|
-
if (!board.mines[cell])
|
|
33
|
-
return false;
|
|
34
|
-
if (!known.has(cell)) {
|
|
35
|
-
known.add(cell);
|
|
36
|
-
changed = true;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
if (result.safe.some((cell) => board.mines[cell]))
|
|
40
|
-
return false;
|
|
41
|
-
if (result.safe.some((cell) => !open[cell])) {
|
|
42
|
-
flood(board.clues, adjacent, open, result.safe);
|
|
43
|
-
changed = true;
|
|
44
|
-
}
|
|
45
|
-
if (!changed)
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
return true;
|
|
18
|
+
const solver = new Solver(board.settings);
|
|
19
|
+
return solver.run(Int8Array.from(board.clues), board.first, enumerate).solved;
|
|
49
20
|
}
|
|
50
|
-
/**
|
|
21
|
+
/** Layouts the repair stage may try, per cell of the field. */
|
|
22
|
+
const REPAIRS_PER_CELL = 12;
|
|
23
|
+
/**
|
|
24
|
+
* Deal after the first reveal. A seed fixes the whole candidate stream and the accepted board.
|
|
25
|
+
*
|
|
26
|
+
* A no-guess deal first draws random layouts, as 0.1 and 0.2 did, so every board those versions dealt is still dealt.
|
|
27
|
+
* If none of them can be finished by deduction it takes the closest and repairs it: single mines are moved near the
|
|
28
|
+
* place the solver stopped until the solver finishes the whole field. The result is proved the same way either route.
|
|
29
|
+
*/
|
|
51
30
|
export function makeBoard(settings, first, options = {}) {
|
|
52
31
|
if (!validSettings(settings) || !validCell(settings, first))
|
|
53
32
|
throw new GenerationError("settings", "Invalid board settings or opening cell.");
|
|
54
33
|
const protectedCells = new Set([first, ...(settings.opening === "clear" ? neighbours(settings, first) : [])]);
|
|
55
|
-
const available =
|
|
34
|
+
const available = activeCells(settings).filter((cell) => !protectedCells.has(cell));
|
|
56
35
|
if (available.length < settings.mines)
|
|
57
36
|
throw new GenerationError("opening", "Too many mines for a safe opening at this cell.");
|
|
37
|
+
if (settings.noGuess && neighbours(settings, first).length === 0 && available.length > settings.mines)
|
|
38
|
+
throw new GenerationError("opening", "The opening cell touches no other cell, so no clue can follow from it. Open another cell.");
|
|
58
39
|
const attempts = options.attempts ?? GENERATION_ATTEMPTS;
|
|
59
40
|
if (!Number.isInteger(attempts) || attempts < 1 || attempts > 10_000)
|
|
60
41
|
throw new GenerationError("settings", "Attempts must be between 1 and 10000.");
|
|
42
|
+
const repairs = options.repairs ?? (options.repair === false ? 0 : REPAIRS_PER_CELL * settings.width * settings.height);
|
|
43
|
+
if (!Number.isInteger(repairs) || repairs < 0 || repairs > 1_000_000)
|
|
44
|
+
throw new GenerationError("settings", "Repairs must be between 0 and 1000000.");
|
|
45
|
+
const enumerate = options.enumerate ?? true;
|
|
61
46
|
const random = seededRandom(settings.seed);
|
|
62
47
|
const adjacent = neighboursOf(settings);
|
|
48
|
+
const solver = new Solver(settings, adjacent);
|
|
49
|
+
const cells = settings.width * settings.height;
|
|
50
|
+
const layout = new Uint8Array(cells), clues = new Int8Array(cells);
|
|
51
|
+
let best = null, fewest = Infinity;
|
|
52
|
+
const board = (attempt) => ({
|
|
53
|
+
settings: { ...settings }, mines: Array.from(layout, Boolean), clues: Array.from(clues), first, attempt,
|
|
54
|
+
});
|
|
63
55
|
for (let attempt = 0; attempt < (settings.noGuess ? attempts : 1); attempt += 1) {
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (!settings.noGuess
|
|
69
|
-
return board;
|
|
56
|
+
layout.fill(0);
|
|
57
|
+
for (const cell of shuffled(available, random).slice(0, settings.mines))
|
|
58
|
+
layout[cell] = 1;
|
|
59
|
+
solver.clueFor(layout, clues);
|
|
60
|
+
if (!settings.noGuess)
|
|
61
|
+
return board(attempt);
|
|
62
|
+
const report = solver.run(clues, first, enumerate);
|
|
63
|
+
if (report.solved)
|
|
64
|
+
return board(attempt);
|
|
65
|
+
if (report.remaining < fewest) {
|
|
66
|
+
fewest = report.remaining;
|
|
67
|
+
best = layout.slice();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (best !== null && repairs > 0) {
|
|
71
|
+
// A cell no chain of neighbours reaches from the opening can never be told by a clue. Make each a mine and leave it
|
|
72
|
+
// be: nobody has to find a mine to win, and nobody could find that one. Fewer mines than such cells: do not try.
|
|
73
|
+
const reached = new Set([first]);
|
|
74
|
+
for (const cell of reached)
|
|
75
|
+
for (const n of adjacent[cell])
|
|
76
|
+
reached.add(n);
|
|
77
|
+
const stranded = available.filter((cell) => !reached.has(cell));
|
|
78
|
+
if (stranded.length <= settings.mines) {
|
|
79
|
+
const reachable = available.filter((cell) => reached.has(cell));
|
|
80
|
+
const place = () => {
|
|
81
|
+
layout.fill(0);
|
|
82
|
+
for (const cell of stranded)
|
|
83
|
+
layout[cell] = 1;
|
|
84
|
+
for (const cell of shuffled(reachable, random).slice(0, settings.mines - stranded.length))
|
|
85
|
+
layout[cell] = 1;
|
|
86
|
+
solver.clueFor(layout, clues);
|
|
87
|
+
};
|
|
88
|
+
// Begin from the closest of the random layouts, with the stranded cells made mines at the cost of other mines.
|
|
89
|
+
layout.set(best);
|
|
90
|
+
const movable = reachable.filter((cell) => layout[cell]);
|
|
91
|
+
for (const cell of shuffled(stranded.filter((c) => !layout[c]), random)) {
|
|
92
|
+
const from = movable.pop();
|
|
93
|
+
layout[from] = 0;
|
|
94
|
+
layout[cell] = 1;
|
|
95
|
+
}
|
|
96
|
+
solver.clueFor(layout, clues);
|
|
97
|
+
const outcome = repair(solver, layout, clues, first, new Set([...protectedCells, ...stranded]), random, repairs, enumerate, place);
|
|
98
|
+
if (outcome.solved)
|
|
99
|
+
return board(attempts + outcome.tries);
|
|
100
|
+
}
|
|
70
101
|
}
|
|
71
102
|
throw new GenerationError("exhausted", "No verified board found within the work budget. Try another seed, fewer mines, or a different opening.");
|
|
72
103
|
}
|
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[][];
|