@johnmorrisdotca/jirai 0.2.1 → 0.4.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 +14 -0
- package/README.md +62 -4
- package/dist/deduce.d.ts +1 -1
- package/dist/deduce.js +18 -6
- package/dist/generate.d.ts +7 -1
- package/dist/generate.js +74 -43
- package/dist/index.d.ts +6 -1
- package/dist/index.js +3 -1
- package/dist/jirai.constants.d.ts +61 -12
- package/dist/jirai.constants.js +25 -7
- package/dist/jirai.types.d.ts +12 -1
- package/dist/levels.d.ts +32 -0
- package/dist/levels.js +47 -0
- package/dist/measure.d.ts +28 -0
- package/dist/measure.js +21 -0
- package/dist/repair.d.ts +14 -0
- package/dist/repair.js +112 -0
- package/dist/solve.d.ts +79 -0
- package/dist/solve.js +336 -0
- package/package.json +3 -2
- package/src/deduce.ts +22 -15
- package/src/generate.ts +60 -33
- package/src/index.ts +6 -1
- package/src/jirai.constants.ts +25 -7
- package/src/jirai.types.ts +11 -2
- package/src/levels.ts +51 -0
- package/src/measure.ts +44 -0
- package/src/repair.ts +88 -0
- package/src/solve.ts +253 -0
- package/src/version.test.js +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,20 @@ All notable changes to this project are written here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.4.0] - 2026-10-05
|
|
10
|
+
|
|
11
|
+
- **Huge fields.** `hugeSettings(level, { grid, shape, size })` and `HUGE_SIZES` (32×32, 48×24 and 24×48) give each of the four levels on a field of four times the medium level's area, 1,024 to 1,152 squares, with the level's share of mines (a 32×32 has 126, 160, 211 and 256 mines at easy, medium, hard and extra-hard), on every rule set and outline. Every one is dealt and proved to need no guess in a median of 3 to 60 ms (73 ms the slowest of ten 32×32 seeds), on the square, orthogonal, hexagonal and wraparound grids and the heart, star and hexagon outlines, and `src/huge.test.ts` wins a 32×32 at easy and at extra-hard by the explained hints alone. The demo's Level menu offers them (`?level=huge-hard`).
|
|
12
|
+
- **A faster hint.** `deduce` compares a count only with the counts that hold its first square, and `hintFor` builds the neighbours once instead of at every step: on a 32×32 extra-hard field the explained hint took 51 ms on average and up to 400 ms late in the game, now 8 ms and at most 43 ms. The hints it finds are the same ones.
|
|
13
|
+
- Measured, not changed: a reveal or a flag on a 1,024-square board is 4 ms of script, 20 ms with the processor slowed fourfold, and the next frame follows (2,400 squares, the most settings accept: 36 ms), so the board stays plain buttons; the README has the numbers.
|
|
14
|
+
|
|
15
|
+
## [0.3.0] - 2026-10-05
|
|
16
|
+
|
|
17
|
+
- 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.
|
|
18
|
+
- 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).
|
|
19
|
+
- 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.
|
|
20
|
+
- 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.
|
|
21
|
+
- 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`.
|
|
22
|
+
|
|
9
23
|
## [0.2.1] - 2026-10-05
|
|
10
24
|
|
|
11
25
|
- The package no longer ships its unit tests (`src/*.test.ts`) in the npm tarball.
|
package/README.md
CHANGED
|
@@ -49,7 +49,8 @@ const board = makeOrthogonalBoard(settings, 40); // counts only edge-sharing nei
|
|
|
49
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
50
|
- **Board outlines:** rectangles, hearts, stars and hexagon outlines. Shaped boards have cut-outs; wraparound works with rectangles.
|
|
51
51
|
- **A fair first move:** choose a safe first cell or a clear opening with all its neighbours safe.
|
|
52
|
-
- **
|
|
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.
|
|
53
54
|
- **Fixed seeded fields:** after the opening, mines never move. A seed and settings reproduce the same deal.
|
|
54
55
|
- **Familiar play:** reveal, flag, question-mark, chord, flood-open, explained hint, timer, undo by saved replay, and a just-the-board dialog.
|
|
55
56
|
- **Accessible controls:** keyboard navigation, pointer and touch, long press to mark, English and Japanese strings, and board labels read by assistive technology.
|
|
@@ -104,11 +105,49 @@ The optional React entry exports `JiraiBoard` from `@johnmorrisdotca/jirai/react
|
|
|
104
105
|
| `seed` | Integer from 0 through 4,294,967,295. A seed is interpreted together with all other settings and the first cell. |
|
|
105
106
|
| `material`, `pieces`, `language` | `ivory`, `wood`, `slate`; `flags`, `stones`, `flowers`; `en`, `ja`. |
|
|
106
107
|
|
|
107
|
-
|
|
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).
|
|
118
|
+
|
|
119
|
+
### Huge fields
|
|
120
|
+
|
|
121
|
+
`hugeSettings(level, { grid, shape, size })` is the same four levels on a field of four times the area of the medium one: 32×32 (1,024 squares), or 48×24 or 24×48 (1,152; `HUGE_SIZES` lists them) with the level's own share of the mines, so a 32×32 has 126 mines at `easy`, 160 at `medium`, 211 at `hard` and 256 at `extra-hard`, and a heart, star or hexagon outline keeps the share over the squares it has left. They are the same fields as any other: seeded, a safe opening, and every one proved to need no guess, on all four rule sets and all four outlines (`src/huge.test.ts`, and the winning of 32×32 fields at easy and extra-hard by the explained hints alone).
|
|
122
|
+
|
|
123
|
+
Measured on a Mac (20 cores, busy): dealing and proving a 32×32 takes a median of 3 ms at easy, 5 ms at medium, 11 ms at hard and 60 ms at extra-hard (the slowest of ten seeds 73 ms; 48×24 at extra-hard 54 ms median, 91 ms slowest); the orthogonal four-neighbour field, the hardest to finish, took 46 ms median and 87 ms slowest at 25%. The player deals in a worker, so the page is never held. A reveal or a flag on the 1,024-square board is answered in 4 ms of script on the desk and about 20 ms with the processor slowed fourfold, as a phone's is, and the next frame follows at once (36 ms with 2,400 squares, the most a field may have), so the board is plain buttons still and needs no canvas. The explained `hintFor` takes 8 ms on average and at most 43 ms late in a 32×32 extra-hard field (it was 51 and 400 before 0.4.0).
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import { DEFAULT_SETTINGS, hugeSettings, newGame } from "@johnmorrisdotca/jirai";
|
|
127
|
+
|
|
128
|
+
const game = newGame({ ...DEFAULT_SETTINGS, ...hugeSettings("hard"), seed: 7 }); // 32 × 32, 211 mines
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { DEFAULT_SETTINGS, levelSettings, newGame } from "@johnmorrisdotca/jirai";
|
|
133
|
+
|
|
134
|
+
const game = newGame({ ...DEFAULT_SETTINGS, ...levelSettings("extra-hard", { grid: "hex", shape: "star" }), grid: "hex", shape: "star", seed: 7 });
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
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:
|
|
138
|
+
|
|
139
|
+
| Rule | easy | medium | hard | extra-hard |
|
|
140
|
+
| --- | --- | --- | --- | --- |
|
|
141
|
+
| square (8 neighbours) | 26 | 41 | 63 | 85 |
|
|
142
|
+
| orthogonal (4) | 18 | 26 | 38 | 49 |
|
|
143
|
+
| hexagonal (6) | 21 | 31 | 49 | 65 |
|
|
144
|
+
| wraparound (8) | 25 | 41 | 67 | 96 |
|
|
145
|
+
|
|
146
|
+
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.
|
|
108
147
|
|
|
109
148
|
## Engine and saved games
|
|
110
149
|
|
|
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.
|
|
150
|
+
`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.
|
|
112
151
|
|
|
113
152
|
`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.
|
|
114
153
|
|
|
@@ -122,7 +161,26 @@ Materials are `ivory`, `wood` and `slate`. Marker sets are `flags`, `stones` and
|
|
|
122
161
|
|
|
123
162
|
## Limits and browser support
|
|
124
163
|
|
|
125
|
-
The board is capped at 60 cells per side and 2,400 cells overall. Shapes need at least 9×9; wraparound is rectangular.
|
|
164
|
+
The board is capped at 60 cells per side and 2,400 cells overall. Shapes need at least 9×9; wraparound is rectangular.
|
|
165
|
+
|
|
166
|
+
**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.
|
|
167
|
+
|
|
168
|
+
**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.
|
|
169
|
+
|
|
170
|
+
| Level | Median | 95th percentile | Slowest |
|
|
171
|
+
| --- | --- | --- | --- |
|
|
172
|
+
| easy | 0.1–0.5 ms | 0.3–1.1 ms | 3.3 ms |
|
|
173
|
+
| medium | 0.2–1.7 ms | 0.4–3.1 ms | 3.6 ms |
|
|
174
|
+
| hard | 0.6–7.7 ms | 1.2–11.8 ms | 16.7 ms |
|
|
175
|
+
| extra-hard | 3–36 ms | 10–55 ms | 229 ms |
|
|
176
|
+
|
|
177
|
+
**What a field cannot do, and what is done about it.**
|
|
178
|
+
|
|
179
|
+
- 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.
|
|
180
|
+
- 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.
|
|
181
|
+
- The widest fields (30 and 40 columns) scroll sideways inside the board on a narrow screen, as hard always has.
|
|
182
|
+
|
|
183
|
+
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.
|
|
126
184
|
|
|
127
185
|
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.
|
|
128
186
|
|
package/dist/deduce.d.ts
CHANGED
|
@@ -3,6 +3,6 @@ import type { Deduction, VisibleGame } from "./jirai.types.ts";
|
|
|
3
3
|
* What the clues prove, without reading the answer or trusting a player's
|
|
4
4
|
* flags. A flag is a note; treating it as evidence makes a bad note a bad hint.
|
|
5
5
|
*/
|
|
6
|
-
export declare function deduce(game: VisibleGame, knownMines?: ReadonlySet<number>, enumerate?: boolean): Deduction;
|
|
6
|
+
export declare function deduce(game: VisibleGame, knownMines?: ReadonlySet<number>, enumerate?: boolean, adjacent?: readonly (readonly number[])[]): Deduction;
|
|
7
7
|
/** Keep discovering certain mines until there is a safe cell or nothing more is proved. */
|
|
8
8
|
export declare function hintFor(game: VisibleGame): Deduction;
|
package/dist/deduce.js
CHANGED
|
@@ -4,8 +4,7 @@ import { neighboursOf } from "./grid.js";
|
|
|
4
4
|
* What the clues prove, without reading the answer or trusting a player's
|
|
5
5
|
* flags. A flag is a note; treating it as evidence makes a bad note a bad hint.
|
|
6
6
|
*/
|
|
7
|
-
export function deduce(game, knownMines = new Set(), enumerate = true) {
|
|
8
|
-
const adjacent = neighboursOf(game.settings);
|
|
7
|
+
export function deduce(game, knownMines = new Set(), enumerate = true, adjacent = neighboursOf(game.settings)) {
|
|
9
8
|
const constraints = [];
|
|
10
9
|
const empty = { safe: [], mines: [], reason: "none", sources: [], contradiction: false };
|
|
11
10
|
for (let cell = 0; cell < game.clues.length; cell += 1) {
|
|
@@ -43,10 +42,21 @@ export function deduce(game, knownMines = new Set(), enumerate = true) {
|
|
|
43
42
|
}
|
|
44
43
|
// Compare a smaller clue with a larger one, including the mine counter.
|
|
45
44
|
// Their difference is another exact count. Never subtract mere overlaps.
|
|
46
|
-
const sets = [...constraints, total].map((c) => new Set(c.cells));
|
|
47
45
|
const all = [...constraints, total];
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
const sets = all.map((c) => new Set(c.cells));
|
|
47
|
+
// A constraint can only be inside another that holds its first cell, so each is compared with those alone, in the same order as
|
|
48
|
+
// comparing it with every other. On a large field that is a handful of neighbours instead of every clue on the board.
|
|
49
|
+
const holding = new Map();
|
|
50
|
+
all.forEach((c, index) => { for (const cell of c.cells) {
|
|
51
|
+
const list = holding.get(cell);
|
|
52
|
+
if (list === undefined)
|
|
53
|
+
holding.set(cell, [index]);
|
|
54
|
+
else
|
|
55
|
+
list.push(index);
|
|
56
|
+
} });
|
|
57
|
+
for (let i = 0; i < all.length; i += 1) {
|
|
58
|
+
const inside = all[i].cells.length === 0 ? all.map((_, index) => index) : holding.get(all[i].cells[0]);
|
|
59
|
+
for (const j of inside) {
|
|
50
60
|
if (i === j || all[i].cells.length >= all[j].cells.length)
|
|
51
61
|
continue;
|
|
52
62
|
if (!all[i].cells.every((cell) => sets[j].has(cell)))
|
|
@@ -62,6 +72,7 @@ export function deduce(game, knownMines = new Set(), enumerate = true) {
|
|
|
62
72
|
if (result !== null)
|
|
63
73
|
return result;
|
|
64
74
|
}
|
|
75
|
+
}
|
|
65
76
|
// A whole-board constraint would join otherwise independent components.
|
|
66
77
|
// Local enumeration is conservative; the global counter is used above.
|
|
67
78
|
return enumerate ? enumerateForced(constraints) : empty;
|
|
@@ -69,9 +80,10 @@ export function deduce(game, knownMines = new Set(), enumerate = true) {
|
|
|
69
80
|
/** Keep discovering certain mines until there is a safe cell or nothing more is proved. */
|
|
70
81
|
export function hintFor(game) {
|
|
71
82
|
const known = new Set();
|
|
83
|
+
const adjacent = neighboursOf(game.settings);
|
|
72
84
|
let last = { safe: [], mines: [], reason: "none", sources: [], contradiction: false };
|
|
73
85
|
while (known.size <= game.settings.mines) {
|
|
74
|
-
const result = deduce(game, known);
|
|
86
|
+
const result = deduce(game, known, true, adjacent);
|
|
75
87
|
if (result.contradiction || result.safe.length)
|
|
76
88
|
return result;
|
|
77
89
|
const fresh = result.mines.filter((cell) => !known.has(cell));
|
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/index.d.ts
CHANGED
|
@@ -8,6 +8,11 @@ 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.
|
|
11
|
+
export declare const VERSION = "0.4.0";
|
|
12
12
|
export { SHAPES, activeCell, activeCells } from "./shape.ts";
|
|
13
13
|
export * from "./orthogonal.ts";
|
|
14
|
+
export { measureBoard } from "./measure.ts";
|
|
15
|
+
export type { Measure } from "./measure.ts";
|
|
16
|
+
export type { ProofKind, ProofTally } from "./solve.ts";
|
|
17
|
+
export { hugeSettings, levelNamed, levelSettings } from "./levels.ts";
|
|
18
|
+
export type { HugeSize, LevelSize } from "./levels.ts";
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,8 @@ 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.
|
|
11
|
+
export const VERSION = "0.4.0";
|
|
12
12
|
export { SHAPES, activeCell, activeCells } from "./shape.js";
|
|
13
13
|
export * from "./orthogonal.js";
|
|
14
|
+
export { measureBoard } from "./measure.js";
|
|
15
|
+
export { hugeSettings, levelNamed, levelSettings } from "./levels.js";
|
|
@@ -31,22 +31,51 @@ export declare const GRID_SPECS: Record<Grid, {
|
|
|
31
31
|
offsets: readonly (readonly [number, number])[];
|
|
32
32
|
wrap: boolean;
|
|
33
33
|
}>;
|
|
34
|
-
/**
|
|
34
|
+
/** The four difficulty levels, easiest first. Each is a larger and denser field than the one before. */
|
|
35
|
+
export declare const LEVELS: readonly ["easy", "medium", "hard", "extra-hard"];
|
|
36
|
+
/** The 0.1 and 0.2 names for the first three levels. They are still accepted wherever a level is. */
|
|
37
|
+
export declare const LEVEL_ALIASES: {
|
|
38
|
+
readonly beginner: "easy";
|
|
39
|
+
readonly intermediate: "medium";
|
|
40
|
+
readonly expert: "hard";
|
|
41
|
+
};
|
|
42
|
+
/** Size and mine count of each level on a rectangular board. Shaped boards keep the size and the density. */
|
|
43
|
+
export declare const LEVEL_SIZES: Record<(typeof LEVELS)[number], {
|
|
44
|
+
width: number;
|
|
45
|
+
height: number;
|
|
46
|
+
mines: number;
|
|
47
|
+
}>;
|
|
48
|
+
/**
|
|
49
|
+
* The huge fields: four times the area of a 16×16 (the medium level), 1,024 to 1,152 squares, for a long solve. The same
|
|
50
|
+
* share of mines as a level, see `hugeSettings`. They are dealt, proved and drawn like any other field: a no-guess deal takes
|
|
51
|
+
* a few milliseconds to a tenth of a second here, and 2,400 squares is the most `validSettings` accepts.
|
|
52
|
+
*/
|
|
53
|
+
export declare const HUGE_SIZES: readonly [{
|
|
54
|
+
readonly width: 32;
|
|
55
|
+
readonly height: 32;
|
|
56
|
+
}, {
|
|
57
|
+
readonly width: 48;
|
|
58
|
+
readonly height: 24;
|
|
59
|
+
}, {
|
|
60
|
+
readonly width: 24;
|
|
61
|
+
readonly height: 48;
|
|
62
|
+
}];
|
|
63
|
+
/** Common minefield dimensions and mine counts. The levels, their older names, and two shapes of field. */
|
|
35
64
|
export declare const PRESETS: {
|
|
36
65
|
readonly beginner: {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
66
|
+
width: number;
|
|
67
|
+
height: number;
|
|
68
|
+
mines: number;
|
|
40
69
|
};
|
|
41
70
|
readonly intermediate: {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
71
|
+
width: number;
|
|
72
|
+
height: number;
|
|
73
|
+
mines: number;
|
|
45
74
|
};
|
|
46
75
|
readonly expert: {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
76
|
+
width: number;
|
|
77
|
+
height: number;
|
|
78
|
+
mines: number;
|
|
50
79
|
};
|
|
51
80
|
readonly wide: {
|
|
52
81
|
readonly width: 21;
|
|
@@ -58,14 +87,34 @@ export declare const PRESETS: {
|
|
|
58
87
|
readonly height: 21;
|
|
59
88
|
readonly mines: 24;
|
|
60
89
|
};
|
|
90
|
+
readonly easy: {
|
|
91
|
+
width: number;
|
|
92
|
+
height: number;
|
|
93
|
+
mines: number;
|
|
94
|
+
};
|
|
95
|
+
readonly medium: {
|
|
96
|
+
width: number;
|
|
97
|
+
height: number;
|
|
98
|
+
mines: number;
|
|
99
|
+
};
|
|
100
|
+
readonly hard: {
|
|
101
|
+
width: number;
|
|
102
|
+
height: number;
|
|
103
|
+
mines: number;
|
|
104
|
+
};
|
|
105
|
+
readonly "extra-hard": {
|
|
106
|
+
width: number;
|
|
107
|
+
height: number;
|
|
108
|
+
mines: number;
|
|
109
|
+
};
|
|
61
110
|
};
|
|
62
|
-
/** Default
|
|
111
|
+
/** Default easy game, with a verified no-guess board and clear opening. */
|
|
63
112
|
export declare const DEFAULT_SETTINGS: Settings;
|
|
64
113
|
/** Largest width or height accepted by settings validation. */
|
|
65
114
|
export declare const MAX_SIDE = 60;
|
|
66
115
|
/** Largest total cell count accepted by settings validation. */
|
|
67
116
|
export declare const MAX_CELLS = 2400;
|
|
68
|
-
/** Default
|
|
117
|
+
/** Default number of random layouts tried before a no-guess deal falls back to repairing the closest one. */
|
|
69
118
|
export declare const GENERATION_ATTEMPTS = 128;
|
|
70
119
|
/** Largest frontier enumerated for exact deductions. */
|
|
71
120
|
export declare const ENUMERATION_CELLS = 18;
|
package/dist/jirai.constants.js
CHANGED
|
@@ -13,21 +13,39 @@ export const GRID_SPECS = {
|
|
|
13
13
|
hex: { offsets: [[-1, 0], [1, 0], [0, -1], [1, -1], [-1, 1], [0, 1]], wrap: false },
|
|
14
14
|
wrap: { offsets: [[-1, -1], [0, -1], [1, -1], [-1, 0], [1, 0], [-1, 1], [0, 1], [1, 1]], wrap: true },
|
|
15
15
|
};
|
|
16
|
-
/**
|
|
16
|
+
/** The four difficulty levels, easiest first. Each is a larger and denser field than the one before. */
|
|
17
|
+
export const LEVELS = ["easy", "medium", "hard", "extra-hard"];
|
|
18
|
+
/** The 0.1 and 0.2 names for the first three levels. They are still accepted wherever a level is. */
|
|
19
|
+
export const LEVEL_ALIASES = { beginner: "easy", intermediate: "medium", expert: "hard" };
|
|
20
|
+
/** Size and mine count of each level on a rectangular board. Shaped boards keep the size and the density. */
|
|
21
|
+
export const LEVEL_SIZES = {
|
|
22
|
+
easy: { width: 9, height: 9, mines: 10 },
|
|
23
|
+
medium: { width: 16, height: 16, mines: 40 },
|
|
24
|
+
hard: { width: 30, height: 16, mines: 99 },
|
|
25
|
+
"extra-hard": { width: 40, height: 24, mines: 240 },
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* The huge fields: four times the area of a 16×16 (the medium level), 1,024 to 1,152 squares, for a long solve. The same
|
|
29
|
+
* share of mines as a level, see `hugeSettings`. They are dealt, proved and drawn like any other field: a no-guess deal takes
|
|
30
|
+
* a few milliseconds to a tenth of a second here, and 2,400 squares is the most `validSettings` accepts.
|
|
31
|
+
*/
|
|
32
|
+
export const HUGE_SIZES = [{ width: 32, height: 32 }, { width: 48, height: 24 }, { width: 24, height: 48 }];
|
|
33
|
+
/** Common minefield dimensions and mine counts. The levels, their older names, and two shapes of field. */
|
|
17
34
|
export const PRESETS = {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
35
|
+
...LEVEL_SIZES,
|
|
36
|
+
beginner: LEVEL_SIZES.easy,
|
|
37
|
+
intermediate: LEVEL_SIZES.medium,
|
|
38
|
+
expert: LEVEL_SIZES.hard,
|
|
21
39
|
wide: { width: 21, height: 9, mines: 24 },
|
|
22
40
|
tall: { width: 9, height: 21, mines: 24 },
|
|
23
41
|
};
|
|
24
|
-
/** Default
|
|
25
|
-
export const DEFAULT_SETTINGS = { ...
|
|
42
|
+
/** Default easy game, with a verified no-guess board and clear opening. */
|
|
43
|
+
export const DEFAULT_SETTINGS = { ...LEVEL_SIZES.easy, grid: GRIDS.square, noGuess: true, opening: "clear", seed: 1 };
|
|
26
44
|
/** Largest width or height accepted by settings validation. */
|
|
27
45
|
export const MAX_SIDE = 60;
|
|
28
46
|
/** Largest total cell count accepted by settings validation. */
|
|
29
47
|
export const MAX_CELLS = 2400;
|
|
30
|
-
/** Default
|
|
48
|
+
/** Default number of random layouts tried before a no-guess deal falls back to repairing the closest one. */
|
|
31
49
|
export const GENERATION_ATTEMPTS = 128;
|
|
32
50
|
/** Largest frontier enumerated for exact deductions. */
|
|
33
51
|
export const ENUMERATION_CELLS = 18;
|
package/dist/jirai.types.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/** Neighbour topology used to count the clues around each cell. */
|
|
2
2
|
export type Grid = "square" | "orthogonal" | "hex" | "wrap";
|
|
3
|
+
/** A difficulty level: easy, medium, hard or extra-hard. */
|
|
4
|
+
export type Level = "easy" | "medium" | "hard" | "extra-hard";
|
|
5
|
+
/** The 0.1 and 0.2 names for easy, medium and hard, still accepted wherever a level is. */
|
|
6
|
+
export type LevelAlias = "beginner" | "intermediate" | "expert";
|
|
3
7
|
/** Current game state, including whether a mine has been hit. */
|
|
4
8
|
export type Status = "ready" | "playing" | "won" | "lost";
|
|
5
9
|
/** Player-facing state of one cell; the answer is never a mark. */
|
|
@@ -29,6 +33,7 @@ export type Board = {
|
|
|
29
33
|
mines: readonly boolean[];
|
|
30
34
|
clues: readonly number[];
|
|
31
35
|
first: number;
|
|
36
|
+
/** Which candidate this deal was: below the `attempts` limit it is a random layout, at or above it a repaired one. */
|
|
32
37
|
attempt: number;
|
|
33
38
|
};
|
|
34
39
|
/** Rules data includes the answer; never send it to a competitive client. Use visibleGame for hints and drawing. */
|
|
@@ -65,9 +70,15 @@ export type Deduction = {
|
|
|
65
70
|
/** A conflicting clue, or a board with no consistent mine placement. */
|
|
66
71
|
contradiction: boolean;
|
|
67
72
|
};
|
|
68
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* Work limits for seeded board generation and its no-guess check. `attempts` is the number of random layouts tried
|
|
75
|
+
* first; `repairs` is how many single-mine moves the repair stage may try on the closest of them (0, or `repair:
|
|
76
|
+
* false`, keeps the 0.2 behaviour of giving up after the attempts); `enumerate` switches the exact small-frontier check.
|
|
77
|
+
*/
|
|
69
78
|
export type GenerationOptions = {
|
|
70
79
|
attempts?: number;
|
|
80
|
+
repairs?: number;
|
|
81
|
+
repair?: boolean;
|
|
71
82
|
enumerate?: boolean;
|
|
72
83
|
};
|
|
73
84
|
/** Supported interface copy locales. */
|
package/dist/levels.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Grid, Level, LevelAlias, Settings } from "./jirai.types.ts";
|
|
2
|
+
/** Board size and mine count a level asks for. */
|
|
3
|
+
export type LevelSize = Pick<Settings, "width" | "height" | "mines">;
|
|
4
|
+
/**
|
|
5
|
+
* The level a name means, or null. Accepts `easy`, `medium`, `hard` and `extra-hard` (also written `extra hard`,
|
|
6
|
+
* `extra_hard` or `extraHard`, in any case), and the older `beginner`, `intermediate` and `expert`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function levelNamed(name: unknown): Level | null;
|
|
9
|
+
/**
|
|
10
|
+
* The size and mine count of a level. A rectangle gets the level's own numbers; a shaped board keeps the level's
|
|
11
|
+
* width and height and its density of mines over the cells that are left. Throws `RangeError` for a name that is no level.
|
|
12
|
+
*/
|
|
13
|
+
export declare function levelSettings(level: Level | LevelAlias, board?: {
|
|
14
|
+
grid?: Grid;
|
|
15
|
+
shape?: Settings["shape"];
|
|
16
|
+
}): LevelSize;
|
|
17
|
+
/** The outline of a huge field: one of `HUGE_SIZES`' width and height. */
|
|
18
|
+
export type HugeSize = {
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* The size and mine count of a level on a huge field: a field of `HUGE_SIZES` (32 × 32 unless `size` says another of them),
|
|
24
|
+
* with the level's own share of mines over its squares. Easy is 12% mines, medium 16%, hard 21% and extra-hard 25%, as
|
|
25
|
+
* on the level's own field, and a heart, star or hexagon outline keeps that share over the squares it has left. Every
|
|
26
|
+
* field is still dealt and proved to need no guess, on every grid. Throws `RangeError` for a name that is no level or a size that is not huge.
|
|
27
|
+
*/
|
|
28
|
+
export declare function hugeSettings(level: Level | LevelAlias, board?: {
|
|
29
|
+
grid?: Grid;
|
|
30
|
+
shape?: Settings["shape"];
|
|
31
|
+
size?: HugeSize;
|
|
32
|
+
}): LevelSize;
|
package/dist/levels.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { HUGE_SIZES, LEVEL_ALIASES, LEVEL_SIZES, LEVELS } from "./jirai.constants.js";
|
|
2
|
+
import { activeCells } from "./shape.js";
|
|
3
|
+
/**
|
|
4
|
+
* The level a name means, or null. Accepts `easy`, `medium`, `hard` and `extra-hard` (also written `extra hard`,
|
|
5
|
+
* `extra_hard` or `extraHard`, in any case), and the older `beginner`, `intermediate` and `expert`.
|
|
6
|
+
*/
|
|
7
|
+
export function levelNamed(name) {
|
|
8
|
+
if (typeof name !== "string")
|
|
9
|
+
return null;
|
|
10
|
+
const key = name.trim().toLowerCase().replace(/[\s_]+/g, "-").replace(/^extrahard$/, "extra-hard");
|
|
11
|
+
if (Object.hasOwn(LEVEL_ALIASES, key))
|
|
12
|
+
return LEVEL_ALIASES[key];
|
|
13
|
+
return LEVELS.includes(key) ? key : null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The size and mine count of a level. A rectangle gets the level's own numbers; a shaped board keeps the level's
|
|
17
|
+
* width and height and its density of mines over the cells that are left. Throws `RangeError` for a name that is no level.
|
|
18
|
+
*/
|
|
19
|
+
export function levelSettings(level, board = {}) {
|
|
20
|
+
const named = levelNamed(level);
|
|
21
|
+
if (named === null)
|
|
22
|
+
throw new RangeError(`Unknown level: ${String(level)}`);
|
|
23
|
+
const size = LEVEL_SIZES[named];
|
|
24
|
+
if ((board.shape ?? "rectangle") === "rectangle")
|
|
25
|
+
return { ...size };
|
|
26
|
+
const probe = { ...size, grid: board.grid ?? "square", shape: board.shape, noGuess: true, opening: "clear", seed: 1 };
|
|
27
|
+
const cells = activeCells(probe).length;
|
|
28
|
+
return { width: size.width, height: size.height, mines: Math.max(1, Math.min(cells - 10, Math.round(size.mines / (size.width * size.height) * cells))) };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The size and mine count of a level on a huge field: a field of `HUGE_SIZES` (32 × 32 unless `size` says another of them),
|
|
32
|
+
* with the level's own share of mines over its squares. Easy is 12% mines, medium 16%, hard 21% and extra-hard 25%, as
|
|
33
|
+
* on the level's own field, and a heart, star or hexagon outline keeps that share over the squares it has left. Every
|
|
34
|
+
* field is still dealt and proved to need no guess, on every grid. Throws `RangeError` for a name that is no level or a size that is not huge.
|
|
35
|
+
*/
|
|
36
|
+
export function hugeSettings(level, board = {}) {
|
|
37
|
+
const named = levelNamed(level);
|
|
38
|
+
if (named === null)
|
|
39
|
+
throw new RangeError(`Unknown level: ${String(level)}`);
|
|
40
|
+
const size = board.size ?? HUGE_SIZES[0];
|
|
41
|
+
if (!HUGE_SIZES.some((huge) => huge.width === size.width && huge.height === size.height))
|
|
42
|
+
throw new RangeError(`Not a huge size: ${size.width} × ${size.height}`);
|
|
43
|
+
const own = LEVEL_SIZES[named];
|
|
44
|
+
const probe = { width: size.width, height: size.height, mines: 1, grid: board.grid ?? "square", shape: board.shape, noGuess: true, opening: "clear", seed: 1 };
|
|
45
|
+
const cells = activeCells(probe).length;
|
|
46
|
+
return { width: size.width, height: size.height, mines: Math.max(1, Math.min(cells - 10, Math.round(own.mines / (own.width * own.height) * cells))) };
|
|
47
|
+
}
|