@johnmorrisdotca/jirai 0.3.0 → 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 CHANGED
@@ -6,6 +6,12 @@ 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
+
9
15
  ## [0.3.0] - 2026-10-05
10
16
 
11
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.
package/README.md CHANGED
@@ -116,6 +116,18 @@ The optional React entry exports `JiraiBoard` from `@johnmorrisdotca/jirai/react
116
116
 
117
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
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
+
119
131
  ```ts
120
132
  import { DEFAULT_SETTINGS, levelSettings, newGame } from "@johnmorrisdotca/jirai";
121
133
 
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
- for (let i = 0; i < all.length; i += 1)
49
- for (let j = 0; j < all.length; j += 1) {
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/index.d.ts CHANGED
@@ -8,11 +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.3.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
14
  export { measureBoard } from "./measure.ts";
15
15
  export type { Measure } from "./measure.ts";
16
16
  export type { ProofKind, ProofTally } from "./solve.ts";
17
- export { levelNamed, levelSettings } from "./levels.ts";
18
- export type { LevelSize } from "./levels.ts";
17
+ export { hugeSettings, levelNamed, levelSettings } from "./levels.ts";
18
+ export type { HugeSize, LevelSize } from "./levels.ts";
package/dist/index.js CHANGED
@@ -8,8 +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.3.0";
11
+ export const VERSION = "0.4.0";
12
12
  export { SHAPES, activeCell, activeCells } from "./shape.js";
13
13
  export * from "./orthogonal.js";
14
14
  export { measureBoard } from "./measure.js";
15
- export { levelNamed, levelSettings } from "./levels.js";
15
+ export { hugeSettings, levelNamed, levelSettings } from "./levels.js";
@@ -45,6 +45,21 @@ export declare const LEVEL_SIZES: Record<(typeof LEVELS)[number], {
45
45
  height: number;
46
46
  mines: number;
47
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
+ }];
48
63
  /** Common minefield dimensions and mine counts. The levels, their older names, and two shapes of field. */
49
64
  export declare const PRESETS: {
50
65
  readonly beginner: {
@@ -24,6 +24,12 @@ export const LEVEL_SIZES = {
24
24
  hard: { width: 30, height: 16, mines: 99 },
25
25
  "extra-hard": { width: 40, height: 24, mines: 240 },
26
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 }];
27
33
  /** Common minefield dimensions and mine counts. The levels, their older names, and two shapes of field. */
28
34
  export const PRESETS = {
29
35
  ...LEVEL_SIZES,
package/dist/levels.d.ts CHANGED
@@ -14,3 +14,19 @@ export declare function levelSettings(level: Level | LevelAlias, board?: {
14
14
  grid?: Grid;
15
15
  shape?: Settings["shape"];
16
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 CHANGED
@@ -1,4 +1,4 @@
1
- import { LEVEL_ALIASES, LEVEL_SIZES, LEVELS } from "./jirai.constants.js";
1
+ import { HUGE_SIZES, LEVEL_ALIASES, LEVEL_SIZES, LEVELS } from "./jirai.constants.js";
2
2
  import { activeCells } from "./shape.js";
3
3
  /**
4
4
  * The level a name means, or null. Accepts `easy`, `medium`, `hard` and `extra-hard` (also written `extra hard`,
@@ -27,3 +27,21 @@ export function levelSettings(level, board = {}) {
27
27
  const cells = activeCells(probe).length;
28
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
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnmorrisdotca/jirai",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Minesweeper on square, orthogonal, hexagonal and wraparound boards: four levels up to extra-hard, seeded games, a safe opening, verified no-guess boards, explained hints, configurable materials and markers, and a playable board for any page. Zero runtime dependencies.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/deduce.ts CHANGED
@@ -6,8 +6,7 @@ import type { Constraint, Deduction, VisibleGame } from "./jirai.types.ts";
6
6
  * What the clues prove, without reading the answer or trusting a player's
7
7
  * flags. A flag is a note; treating it as evidence makes a bad note a bad hint.
8
8
  */
9
- export function deduce(game: VisibleGame, knownMines: ReadonlySet<number> = new Set(), enumerate = true): Deduction {
10
- const adjacent = neighboursOf(game.settings);
9
+ export function deduce(game: VisibleGame, knownMines: ReadonlySet<number> = new Set(), enumerate = true, adjacent: readonly (readonly number[])[] = neighboursOf(game.settings)): Deduction {
11
10
  const constraints: Constraint[] = [];
12
11
  const empty: Deduction = { safe: [], mines: [], reason: "none", sources: [], contradiction: false };
13
12
  for (let cell = 0; cell < game.clues.length; cell += 1) {
@@ -31,19 +30,26 @@ export function deduce(game: VisibleGame, knownMines: ReadonlySet<number> = new
31
30
  if (unknown.length) { const result = forced(total, "total"); if (result !== null) return result; }
32
31
  // Compare a smaller clue with a larger one, including the mine counter.
33
32
  // Their difference is another exact count. Never subtract mere overlaps.
34
- const sets = [...constraints, total].map((c) => new Set(c.cells));
35
33
  const all = [...constraints, total];
36
- for (let i = 0; i < all.length; i += 1) for (let j = 0; j < all.length; j += 1) {
37
- if (i === j || all[i]!.cells.length >= all[j]!.cells.length) continue;
38
- if (!all[i]!.cells.every((cell) => sets[j]!.has(cell))) continue;
39
- const c: Constraint = {
40
- cells: all[j]!.cells.filter((cell) => !sets[i]!.has(cell)),
41
- mines: all[j]!.mines - all[i]!.mines,
42
- sources: [...new Set([...all[i]!.sources, ...all[j]!.sources])],
43
- };
44
- if (c.mines < 0 || c.mines > c.cells.length) return { ...empty, contradiction: true };
45
- const result = forced(c, j === all.length - 1 ? "total" : "overlap");
46
- if (result !== null) return result;
34
+ const sets = all.map((c) => new Set(c.cells));
35
+ // A constraint can only be inside another that holds its first cell, so each is compared with those alone, in the same order as
36
+ // comparing it with every other. On a large field that is a handful of neighbours instead of every clue on the board.
37
+ const holding = new Map<number, number[]>();
38
+ all.forEach((c, index) => { for (const cell of c.cells) { const list = holding.get(cell); if (list === undefined) holding.set(cell, [index]); else list.push(index); } });
39
+ for (let i = 0; i < all.length; i += 1) {
40
+ const inside = all[i]!.cells.length === 0 ? all.map((_, index) => index) : holding.get(all[i]!.cells[0]!)!;
41
+ for (const j of inside) {
42
+ if (i === j || all[i]!.cells.length >= all[j]!.cells.length) continue;
43
+ if (!all[i]!.cells.every((cell) => sets[j]!.has(cell))) continue;
44
+ const c: Constraint = {
45
+ cells: all[j]!.cells.filter((cell) => !sets[i]!.has(cell)),
46
+ mines: all[j]!.mines - all[i]!.mines,
47
+ sources: [...new Set([...all[i]!.sources, ...all[j]!.sources])],
48
+ };
49
+ if (c.mines < 0 || c.mines > c.cells.length) return { ...empty, contradiction: true };
50
+ const result = forced(c, j === all.length - 1 ? "total" : "overlap");
51
+ if (result !== null) return result;
52
+ }
47
53
  }
48
54
  // A whole-board constraint would join otherwise independent components.
49
55
  // Local enumeration is conservative; the global counter is used above.
@@ -53,9 +59,10 @@ export function deduce(game: VisibleGame, knownMines: ReadonlySet<number> = new
53
59
  /** Keep discovering certain mines until there is a safe cell or nothing more is proved. */
54
60
  export function hintFor(game: VisibleGame): Deduction {
55
61
  const known = new Set<number>();
62
+ const adjacent = neighboursOf(game.settings);
56
63
  let last: Deduction = { safe: [], mines: [], reason: "none", sources: [], contradiction: false };
57
64
  while (known.size <= game.settings.mines) {
58
- const result = deduce(game, known);
65
+ const result = deduce(game, known, true, adjacent);
59
66
  if (result.contradiction || result.safe.length) return result;
60
67
  const fresh = result.mines.filter((cell) => !known.has(cell));
61
68
  if (!fresh.length) return last;
package/src/index.ts CHANGED
@@ -8,7 +8,7 @@ export * from "./deduce.ts";
8
8
  export * from "./keep.ts";
9
9
  export { seededRandom } from "./random.ts";
10
10
  /** Package version, kept in step with the release metadata. */
11
- export const VERSION = "0.3.0";
11
+ export const VERSION = "0.4.0";
12
12
 
13
13
  export { SHAPES, activeCell, activeCells } from "./shape.ts";
14
14
 
@@ -16,5 +16,5 @@ export * from "./orthogonal.ts";
16
16
  export { measureBoard } from "./measure.ts";
17
17
  export type { Measure } from "./measure.ts";
18
18
  export type { ProofKind, ProofTally } from "./solve.ts";
19
- export { levelNamed, levelSettings } from "./levels.ts";
20
- export type { LevelSize } from "./levels.ts";
19
+ export { hugeSettings, levelNamed, levelSettings } from "./levels.ts";
20
+ export type { HugeSize, LevelSize } from "./levels.ts";
@@ -26,6 +26,12 @@ export const LEVEL_SIZES: Record<(typeof LEVELS)[number], { width: number; heigh
26
26
  hard: { width: 30, height: 16, mines: 99 },
27
27
  "extra-hard": { width: 40, height: 24, mines: 240 },
28
28
  };
29
+ /**
30
+ * 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
31
+ * share of mines as a level, see `hugeSettings`. They are dealt, proved and drawn like any other field: a no-guess deal takes
32
+ * a few milliseconds to a tenth of a second here, and 2,400 squares is the most `validSettings` accepts.
33
+ */
34
+ export const HUGE_SIZES = [{ width: 32, height: 32 }, { width: 48, height: 24 }, { width: 24, height: 48 }] as const satisfies readonly { width: number; height: number }[];
29
35
  /** Common minefield dimensions and mine counts. The levels, their older names, and two shapes of field. */
30
36
  export const PRESETS = {
31
37
  ...LEVEL_SIZES,
package/src/levels.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { LEVEL_ALIASES, LEVEL_SIZES, LEVELS } from "./jirai.constants.ts";
1
+ import { HUGE_SIZES, LEVEL_ALIASES, LEVEL_SIZES, LEVELS } from "./jirai.constants.ts";
2
2
  import { activeCells } from "./shape.ts";
3
3
  import type { Grid, Level, LevelAlias, Settings } from "./jirai.types.ts";
4
4
 
@@ -29,3 +29,23 @@ export function levelSettings(level: Level | LevelAlias, board: { grid?: Grid; s
29
29
  const cells = activeCells(probe).length;
30
30
  return { width: size.width, height: size.height, mines: Math.max(1, Math.min(cells - 10, Math.round(size.mines / (size.width * size.height) * cells))) };
31
31
  }
32
+
33
+ /** The outline of a huge field: one of `HUGE_SIZES`' width and height. */
34
+ export type HugeSize = { width: number; height: number };
35
+
36
+ /**
37
+ * 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),
38
+ * with the level's own share of mines over its squares. Easy is 12% mines, medium 16%, hard 21% and extra-hard 25%, as
39
+ * on the level's own field, and a heart, star or hexagon outline keeps that share over the squares it has left. Every
40
+ * 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.
41
+ */
42
+ export function hugeSettings(level: Level | LevelAlias, board: { grid?: Grid; shape?: Settings["shape"]; size?: HugeSize } = {}): LevelSize {
43
+ const named = levelNamed(level);
44
+ if (named === null) throw new RangeError(`Unknown level: ${String(level)}`);
45
+ const size = board.size ?? HUGE_SIZES[0];
46
+ if (!HUGE_SIZES.some((huge) => huge.width === size.width && huge.height === size.height)) throw new RangeError(`Not a huge size: ${size.width} × ${size.height}`);
47
+ const own = LEVEL_SIZES[named];
48
+ const probe = { width: size.width, height: size.height, mines: 1, grid: board.grid ?? "square", shape: board.shape, noGuess: true, opening: "clear", seed: 1 } as const;
49
+ const cells = activeCells(probe).length;
50
+ return { width: size.width, height: size.height, mines: Math.max(1, Math.min(cells - 10, Math.round(own.mines / (own.width * own.height) * cells))) };
51
+ }