@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/dist/grid.js
CHANGED
|
@@ -16,6 +16,7 @@ export function validSettings(value) {
|
|
|
16
16
|
&& (s.opening === "safe" || s.opening === "clear")
|
|
17
17
|
&& Number.isInteger(s.seed) && s.seed >= 0 && s.seed <= 0xffffffff;
|
|
18
18
|
}
|
|
19
|
+
/** Reports whether a row-major cell is inside the board's active shape. */
|
|
19
20
|
export function validCell(settings, cell) {
|
|
20
21
|
return Number.isInteger(cell) && cell >= 0 && cell < settings.width * settings.height && activeCell(settings, cell);
|
|
21
22
|
}
|
|
@@ -41,6 +42,7 @@ export function neighbours(settings, cell) {
|
|
|
41
42
|
}
|
|
42
43
|
return [...out].sort((a, b) => a - b);
|
|
43
44
|
}
|
|
45
|
+
/** Precomputes the neighbour list for each row-major cell. */
|
|
44
46
|
export function neighboursOf(settings) {
|
|
45
47
|
return Array.from({ length: settings.width * settings.height }, (_, cell) => neighbours(settings, cell));
|
|
46
48
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,12 @@ export * from "./game.ts";
|
|
|
7
7
|
export * from "./deduce.ts";
|
|
8
8
|
export * from "./keep.ts";
|
|
9
9
|
export { seededRandom } from "./random.ts";
|
|
10
|
-
|
|
10
|
+
/** Package version, kept in step with the release metadata. */
|
|
11
|
+
export declare const VERSION = "0.3.0";
|
|
11
12
|
export { SHAPES, activeCell, activeCells } from "./shape.ts";
|
|
12
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 { levelNamed, levelSettings } from "./levels.ts";
|
|
18
|
+
export type { LevelSize } from "./levels.ts";
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,9 @@ export * from "./game.js";
|
|
|
7
7
|
export * from "./deduce.js";
|
|
8
8
|
export * from "./keep.js";
|
|
9
9
|
export { seededRandom } from "./random.js";
|
|
10
|
-
|
|
10
|
+
/** Package version, kept in step with the release metadata. */
|
|
11
|
+
export const VERSION = "0.3.0";
|
|
11
12
|
export { SHAPES, activeCell, activeCells } from "./shape.js";
|
|
12
13
|
export * from "./orthogonal.js";
|
|
14
|
+
export { measureBoard } from "./measure.js";
|
|
15
|
+
export { levelNamed, levelSettings } from "./levels.js";
|
|
@@ -1,47 +1,66 @@
|
|
|
1
1
|
import type { Grid, Settings } from "./jirai.types.ts";
|
|
2
|
+
/** Names for the supported square, four-neighbour, hex, and wrap grids. */
|
|
2
3
|
export declare const GRIDS: {
|
|
3
4
|
readonly square: "square";
|
|
4
5
|
readonly orthogonal: "orthogonal";
|
|
5
6
|
readonly hex: "hex";
|
|
6
7
|
readonly wrap: "wrap";
|
|
7
8
|
};
|
|
9
|
+
/** Names for the game lifecycle states. */
|
|
8
10
|
export declare const STATUSES: {
|
|
9
11
|
readonly ready: "ready";
|
|
10
12
|
readonly playing: "playing";
|
|
11
13
|
readonly won: "won";
|
|
12
14
|
readonly lost: "lost";
|
|
13
15
|
};
|
|
16
|
+
/** Names for the cell mark cycle. */
|
|
14
17
|
export declare const MARKS: {
|
|
15
18
|
readonly covered: "covered";
|
|
16
19
|
readonly flag: "flag";
|
|
17
20
|
readonly question: "question";
|
|
18
21
|
readonly open: "open";
|
|
19
22
|
};
|
|
23
|
+
/** Names for the moves accepted by the engine. */
|
|
20
24
|
export declare const MOVES: {
|
|
21
25
|
readonly reveal: "reveal";
|
|
22
26
|
readonly mark: "mark";
|
|
23
27
|
readonly chord: "chord";
|
|
24
28
|
};
|
|
25
|
-
/** Hex coordinates are axial:
|
|
29
|
+
/** Neighbour offsets and edge behaviour for each topology. Hex coordinates are axial: each row is displaced half a cell to the right. */
|
|
26
30
|
export declare const GRID_SPECS: Record<Grid, {
|
|
27
31
|
offsets: readonly (readonly [number, number])[];
|
|
28
32
|
wrap: boolean;
|
|
29
33
|
}>;
|
|
34
|
+
/** 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
|
+
/** Common minefield dimensions and mine counts. The levels, their older names, and two shapes of field. */
|
|
30
49
|
export declare const PRESETS: {
|
|
31
50
|
readonly beginner: {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
51
|
+
width: number;
|
|
52
|
+
height: number;
|
|
53
|
+
mines: number;
|
|
35
54
|
};
|
|
36
55
|
readonly intermediate: {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
56
|
+
width: number;
|
|
57
|
+
height: number;
|
|
58
|
+
mines: number;
|
|
40
59
|
};
|
|
41
60
|
readonly expert: {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
61
|
+
width: number;
|
|
62
|
+
height: number;
|
|
63
|
+
mines: number;
|
|
45
64
|
};
|
|
46
65
|
readonly wide: {
|
|
47
66
|
readonly width: 21;
|
|
@@ -53,10 +72,36 @@ export declare const PRESETS: {
|
|
|
53
72
|
readonly height: 21;
|
|
54
73
|
readonly mines: 24;
|
|
55
74
|
};
|
|
75
|
+
readonly easy: {
|
|
76
|
+
width: number;
|
|
77
|
+
height: number;
|
|
78
|
+
mines: number;
|
|
79
|
+
};
|
|
80
|
+
readonly medium: {
|
|
81
|
+
width: number;
|
|
82
|
+
height: number;
|
|
83
|
+
mines: number;
|
|
84
|
+
};
|
|
85
|
+
readonly hard: {
|
|
86
|
+
width: number;
|
|
87
|
+
height: number;
|
|
88
|
+
mines: number;
|
|
89
|
+
};
|
|
90
|
+
readonly "extra-hard": {
|
|
91
|
+
width: number;
|
|
92
|
+
height: number;
|
|
93
|
+
mines: number;
|
|
94
|
+
};
|
|
56
95
|
};
|
|
96
|
+
/** Default easy game, with a verified no-guess board and clear opening. */
|
|
57
97
|
export declare const DEFAULT_SETTINGS: Settings;
|
|
98
|
+
/** Largest width or height accepted by settings validation. */
|
|
58
99
|
export declare const MAX_SIDE = 60;
|
|
100
|
+
/** Largest total cell count accepted by settings validation. */
|
|
59
101
|
export declare const MAX_CELLS = 2400;
|
|
102
|
+
/** Default number of random layouts tried before a no-guess deal falls back to repairing the closest one. */
|
|
60
103
|
export declare const GENERATION_ATTEMPTS = 128;
|
|
104
|
+
/** Largest frontier enumerated for exact deductions. */
|
|
61
105
|
export declare const ENUMERATION_CELLS = 18;
|
|
106
|
+
/** Maximum partial assignments checked in one exact deduction search. */
|
|
62
107
|
export declare const ENUMERATION_NODES = 100000;
|
package/dist/jirai.constants.js
CHANGED
|
@@ -1,24 +1,47 @@
|
|
|
1
|
+
/** Names for the supported square, four-neighbour, hex, and wrap grids. */
|
|
1
2
|
export const GRIDS = { square: "square", orthogonal: "orthogonal", hex: "hex", wrap: "wrap" };
|
|
3
|
+
/** Names for the game lifecycle states. */
|
|
2
4
|
export const STATUSES = { ready: "ready", playing: "playing", won: "won", lost: "lost" };
|
|
5
|
+
/** Names for the cell mark cycle. */
|
|
3
6
|
export const MARKS = { covered: "covered", flag: "flag", question: "question", open: "open" };
|
|
7
|
+
/** Names for the moves accepted by the engine. */
|
|
4
8
|
export const MOVES = { reveal: "reveal", mark: "mark", chord: "chord" };
|
|
5
|
-
/** Hex coordinates are axial:
|
|
9
|
+
/** Neighbour offsets and edge behaviour for each topology. Hex coordinates are axial: each row is displaced half a cell to the right. */
|
|
6
10
|
export const GRID_SPECS = {
|
|
7
11
|
square: { offsets: [[-1, -1], [0, -1], [1, -1], [-1, 0], [1, 0], [-1, 1], [0, 1], [1, 1]], wrap: false },
|
|
8
12
|
orthogonal: { offsets: [[0, -1], [-1, 0], [1, 0], [0, 1]], wrap: false },
|
|
9
13
|
hex: { offsets: [[-1, 0], [1, 0], [0, -1], [1, -1], [-1, 1], [0, 1]], wrap: false },
|
|
10
14
|
wrap: { offsets: [[-1, -1], [0, -1], [1, -1], [-1, 0], [1, 0], [-1, 1], [0, 1], [1, 1]], wrap: true },
|
|
11
15
|
};
|
|
16
|
+
/** 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
|
+
/** Common minefield dimensions and mine counts. The levels, their older names, and two shapes of field. */
|
|
12
28
|
export const PRESETS = {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
29
|
+
...LEVEL_SIZES,
|
|
30
|
+
beginner: LEVEL_SIZES.easy,
|
|
31
|
+
intermediate: LEVEL_SIZES.medium,
|
|
32
|
+
expert: LEVEL_SIZES.hard,
|
|
16
33
|
wide: { width: 21, height: 9, mines: 24 },
|
|
17
34
|
tall: { width: 9, height: 21, mines: 24 },
|
|
18
35
|
};
|
|
19
|
-
|
|
36
|
+
/** Default easy game, with a verified no-guess board and clear opening. */
|
|
37
|
+
export const DEFAULT_SETTINGS = { ...LEVEL_SIZES.easy, grid: GRIDS.square, noGuess: true, opening: "clear", seed: 1 };
|
|
38
|
+
/** Largest width or height accepted by settings validation. */
|
|
20
39
|
export const MAX_SIDE = 60;
|
|
40
|
+
/** Largest total cell count accepted by settings validation. */
|
|
21
41
|
export const MAX_CELLS = 2400;
|
|
42
|
+
/** Default number of random layouts tried before a no-guess deal falls back to repairing the closest one. */
|
|
22
43
|
export const GENERATION_ATTEMPTS = 128;
|
|
44
|
+
/** Largest frontier enumerated for exact deductions. */
|
|
23
45
|
export const ENUMERATION_CELLS = 18;
|
|
46
|
+
/** Maximum partial assignments checked in one exact deduction search. */
|
|
24
47
|
export const ENUMERATION_NODES = 100_000;
|
package/dist/jirai.types.d.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
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";
|
|
7
|
+
/** Current game state, including whether a mine has been hit. */
|
|
3
8
|
export type Status = "ready" | "playing" | "won" | "lost";
|
|
9
|
+
/** Player-facing state of one cell; the answer is never a mark. */
|
|
4
10
|
export type Mark = "covered" | "flag" | "question" | "open";
|
|
11
|
+
/** Board dimensions and rules used to deal and validate a game. */
|
|
5
12
|
export type Settings = {
|
|
6
13
|
/** Shape omits cells; rectangle is the compatible default. Wrap requires rectangle. */
|
|
7
14
|
shape?: "rectangle" | "heart" | "star" | "hexagon";
|
|
@@ -15,15 +22,18 @@ export type Settings = {
|
|
|
15
22
|
opening: "safe" | "clear";
|
|
16
23
|
seed: number;
|
|
17
24
|
};
|
|
25
|
+
/** A reveal, mark-cycle, or numbered-cell chord applied to a game. */
|
|
18
26
|
export type Move = {
|
|
19
27
|
kind: "reveal" | "mark" | "chord";
|
|
20
28
|
cell: number;
|
|
21
29
|
};
|
|
30
|
+
/** A dealt board including its answer; keep it off public clients. */
|
|
22
31
|
export type Board = {
|
|
23
32
|
settings: Settings;
|
|
24
33
|
mines: readonly boolean[];
|
|
25
34
|
clues: readonly number[];
|
|
26
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. */
|
|
27
37
|
attempt: number;
|
|
28
38
|
};
|
|
29
39
|
/** Rules data includes the answer; never send it to a competitive client. Use visibleGame for hints and drawing. */
|
|
@@ -37,6 +47,7 @@ export type Game = {
|
|
|
37
47
|
/** A proved hint has been shown during this run. */
|
|
38
48
|
helped: boolean;
|
|
39
49
|
};
|
|
50
|
+
/** Answer-free state suitable for deductions, hints, and public display. */
|
|
40
51
|
export type VisibleGame = {
|
|
41
52
|
settings: Settings;
|
|
42
53
|
/** Null is hidden, including flags. Only opened cells give a clue. */
|
|
@@ -44,11 +55,13 @@ export type VisibleGame = {
|
|
|
44
55
|
marks: readonly Mark[];
|
|
45
56
|
status: Status;
|
|
46
57
|
};
|
|
58
|
+
/** An exact count over unknown cells, with the clues that supplied it. */
|
|
47
59
|
export type Constraint = {
|
|
48
60
|
cells: readonly number[];
|
|
49
61
|
mines: number;
|
|
50
62
|
sources: readonly number[];
|
|
51
63
|
};
|
|
64
|
+
/** Certain safe cells and mines proved from visible clues. */
|
|
52
65
|
export type Deduction = {
|
|
53
66
|
safe: readonly number[];
|
|
54
67
|
mines: readonly number[];
|
|
@@ -57,10 +70,20 @@ export type Deduction = {
|
|
|
57
70
|
/** A conflicting clue, or a board with no consistent mine placement. */
|
|
58
71
|
contradiction: boolean;
|
|
59
72
|
};
|
|
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
|
+
*/
|
|
60
78
|
export type GenerationOptions = {
|
|
61
79
|
attempts?: number;
|
|
80
|
+
repairs?: number;
|
|
81
|
+
repair?: boolean;
|
|
62
82
|
enumerate?: boolean;
|
|
63
83
|
};
|
|
84
|
+
/** Supported interface copy locales. */
|
|
64
85
|
export type Language = "en" | "ja";
|
|
86
|
+
/** Board colours for drawing and mounted play. */
|
|
65
87
|
export type Material = "ivory" | "wood" | "slate";
|
|
88
|
+
/** Symbols used to mark mines. */
|
|
66
89
|
export type Pieces = "flags" | "stones" | "flowers";
|
package/dist/levels.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
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;
|
package/dist/levels.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { 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
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ProofTally } from "./solve.ts";
|
|
2
|
+
import type { Board } from "./jirai.types.ts";
|
|
3
|
+
/** How demanding a dealt board is to solve by deduction, measured by solving it. */
|
|
4
|
+
export type Measure = {
|
|
5
|
+
/** Mines per playable cell. */
|
|
6
|
+
density: number;
|
|
7
|
+
/** Playable cells. */
|
|
8
|
+
cells: number;
|
|
9
|
+
/** Share of the safe cells the first reveal opens before any reasoning. */
|
|
10
|
+
opening: number;
|
|
11
|
+
/** Cells proved by each kind of reasoning: one clue, two clues together, the mine counter, or every arrangement of a small frontier. */
|
|
12
|
+
proofs: ProofTally;
|
|
13
|
+
/** Share of the playable cells proved by anything beyond one clue's count: the part of the board that needs multi-step reasoning. */
|
|
14
|
+
multiStep: number;
|
|
15
|
+
/** The longest chain of deductions, each resting on the one before. */
|
|
16
|
+
depth: number;
|
|
17
|
+
/** One number for ranking boards: higher is harder. Not a calibrated rating, but monotone in each part above. */
|
|
18
|
+
score: number;
|
|
19
|
+
/** The board can be finished by deduction alone. */
|
|
20
|
+
solvable: boolean;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Solve a dealt board from its opening and report what it took. The score is
|
|
24
|
+
* `100 × density + 100 × multiStep + depth ÷ 4`, so it rises with the crowding of the mines, with the share of the
|
|
25
|
+
* field that needs more than a single clue, and with how long the longest chain of reasoning runs. It is a way to
|
|
26
|
+
* put boards in order, not a rating of how long a person will take.
|
|
27
|
+
*/
|
|
28
|
+
export declare function measureBoard(board: Board): Measure;
|
package/dist/measure.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Solver } from "./solve.js";
|
|
2
|
+
/**
|
|
3
|
+
* Solve a dealt board from its opening and report what it took. The score is
|
|
4
|
+
* `100 × density + 100 × multiStep + depth ÷ 4`, so it rises with the crowding of the mines, with the share of the
|
|
5
|
+
* field that needs more than a single clue, and with how long the longest chain of reasoning runs. It is a way to
|
|
6
|
+
* put boards in order, not a rating of how long a person will take.
|
|
7
|
+
*/
|
|
8
|
+
export function measureBoard(board) {
|
|
9
|
+
const solver = new Solver(board.settings);
|
|
10
|
+
const report = solver.run(Int8Array.from(board.clues), board.first, true);
|
|
11
|
+
const cells = solver.safeTotal + solver.mineTotal;
|
|
12
|
+
const density = solver.mineTotal / cells;
|
|
13
|
+
const { count, overlap, total, enumeration } = report.proved;
|
|
14
|
+
const multiStep = (overlap + total + enumeration) / cells;
|
|
15
|
+
const opening = solver.safeTotal === 0 ? 0 : report.free / solver.safeTotal;
|
|
16
|
+
const score = 100 * density + 100 * multiStep + report.depth / 4;
|
|
17
|
+
return {
|
|
18
|
+
density, cells, opening, proofs: { count, overlap, total, enumeration }, multiStep, depth: report.depth,
|
|
19
|
+
score: Math.round(score * 10) / 10, solvable: report.solved,
|
|
20
|
+
};
|
|
21
|
+
}
|
package/dist/orthogonal.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Board, GenerationOptions, Game, Settings } from "./jirai.types.ts";
|
|
2
2
|
/** Identifies the four-neighbour rules and progress-code format. */
|
|
3
3
|
export declare const ORTHOGONAL_VARIANT: "orthogonal";
|
|
4
|
+
/** Settings accepted by the four-neighbour-specific helpers. */
|
|
4
5
|
export type OrthogonalSettings = Omit<Settings, "grid">;
|
|
5
6
|
/** Starts a game under four-neighbour rules. */
|
|
6
7
|
export declare function newOrthogonalGame(settings: OrthogonalSettings): Game;
|
package/dist/react.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { JiraiProps } from "./react.types.ts";
|
|
2
|
-
/**
|
|
2
|
+
/** Renders the DOM player inside a React-owned host. Options are read on mount; use a new key to load a new board. */
|
|
3
3
|
export declare function JiraiBoard({ settings, progress, material, pieces, language, controls, onChange, onFinish, onError, ...element }: JiraiProps): import("react").JSX.Element;
|
package/dist/react.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef } from "react";
|
|
3
3
|
import { mountJirai } from "./mount.js";
|
|
4
|
-
/**
|
|
4
|
+
/** Renders the DOM player inside a React-owned host. Options are read on mount; use a new key to load a new board. */
|
|
5
5
|
export function JiraiBoard({ settings, progress, material, pieces, language, controls, onChange, onFinish, onError, ...element }) {
|
|
6
6
|
const host = useRef(null);
|
|
7
7
|
const latest = useRef({ onChange, onFinish, onError });
|
package/dist/react.types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { HTMLAttributes } from "react";
|
|
2
2
|
import type { MountOptions } from "./ui.types.ts";
|
|
3
|
+
/** Mount options plus standard div attributes for the React wrapper. */
|
|
3
4
|
export type JiraiProps = MountOptions & Omit<HTMLAttributes<HTMLDivElement>, "onChange" | "onError">;
|
package/dist/repair.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Solver } from "./solve.ts";
|
|
2
|
+
/** What a repair run did: whether it reached a board the solver finishes, how many layouts it tried, and how many safe cells it left covered. */
|
|
3
|
+
export type RepairOutcome = {
|
|
4
|
+
solved: boolean;
|
|
5
|
+
tries: number;
|
|
6
|
+
remaining: number;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Make an unsolvable layout solvable by moving single mines next to the place the solver got stuck, never touching a
|
|
10
|
+
* pinned cell, and keeping a move when it leaves the solver no worse off (and now and then when it does, so that a
|
|
11
|
+
* dead end can be left). The search is counted in layouts tried, never in time, so a seed always gives the same board.
|
|
12
|
+
* `mines` and `clues` are changed in place and hold the answer when the outcome is solved.
|
|
13
|
+
*/
|
|
14
|
+
export declare function repair(solver: Solver, mines: Uint8Array, clues: Int8Array, first: number, pinned: ReadonlySet<number>, random: () => number, budget: number, enumerate: boolean, restart: () => void): RepairOutcome;
|
package/dist/repair.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/** How readily a move that leaves more safe cells covered is kept, to climb out of a dead end. Small: nearly all such moves are refused. */
|
|
2
|
+
const WARMTH = 0.3;
|
|
3
|
+
/** Tries without getting closer after which the search begins again from a new layout, at least. */
|
|
4
|
+
const PATIENCE = 1500;
|
|
5
|
+
/** How far from the stuck cell a mine may be moved from and to. */
|
|
6
|
+
const REACH = 2;
|
|
7
|
+
function pick(items, random) {
|
|
8
|
+
return items.length === 0 ? undefined : items[Math.floor(random() * items.length)];
|
|
9
|
+
}
|
|
10
|
+
/** Move a mine from `from` to `to`, keeping every clue in step. Calling it again with the arguments swapped undoes it. */
|
|
11
|
+
function move(solver, mines, clues, from, to) {
|
|
12
|
+
mines[from] = 0;
|
|
13
|
+
mines[to] = 1;
|
|
14
|
+
for (const cell of [from, to]) {
|
|
15
|
+
const delta = cell === to ? 1 : -1;
|
|
16
|
+
for (const n of solver.adjacent[cell])
|
|
17
|
+
if (!mines[n])
|
|
18
|
+
clues[n] += delta;
|
|
19
|
+
}
|
|
20
|
+
clues[to] = -1;
|
|
21
|
+
let count = 0;
|
|
22
|
+
for (const n of solver.adjacent[from])
|
|
23
|
+
if (mines[n])
|
|
24
|
+
count += 1;
|
|
25
|
+
clues[from] = count;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Make an unsolvable layout solvable by moving single mines next to the place the solver got stuck, never touching a
|
|
29
|
+
* pinned cell, and keeping a move when it leaves the solver no worse off (and now and then when it does, so that a
|
|
30
|
+
* dead end can be left). The search is counted in layouts tried, never in time, so a seed always gives the same board.
|
|
31
|
+
* `mines` and `clues` are changed in place and hold the answer when the outcome is solved.
|
|
32
|
+
*/
|
|
33
|
+
export function repair(solver, mines, clues, first, pinned, random, budget, enumerate, restart) {
|
|
34
|
+
const around = (cell, radius) => {
|
|
35
|
+
const seen = new Set([cell]);
|
|
36
|
+
let ring = [cell];
|
|
37
|
+
for (let step = 0; step < radius; step += 1) {
|
|
38
|
+
const next = [];
|
|
39
|
+
for (const c of ring)
|
|
40
|
+
for (const n of solver.adjacent[c])
|
|
41
|
+
if (!seen.has(n)) {
|
|
42
|
+
seen.add(n);
|
|
43
|
+
next.push(n);
|
|
44
|
+
}
|
|
45
|
+
ring = next;
|
|
46
|
+
}
|
|
47
|
+
return [...seen].filter((c) => !pinned.has(c));
|
|
48
|
+
};
|
|
49
|
+
let report = solver.run(clues, first, enumerate);
|
|
50
|
+
let tries = 0, closest = report.remaining, idle = 0;
|
|
51
|
+
const patience = Math.max(PATIENCE, 2 * solver.cells);
|
|
52
|
+
while (!report.solved && tries < budget) {
|
|
53
|
+
tries += 1;
|
|
54
|
+
const frontier = [], covered = [], cutOff = [];
|
|
55
|
+
for (let cell = 0; cell < solver.cells; cell += 1) {
|
|
56
|
+
if (solver.state[cell] !== 0)
|
|
57
|
+
continue;
|
|
58
|
+
covered.push(cell);
|
|
59
|
+
if (solver.adjacent[cell].some((n) => solver.state[n] === 1))
|
|
60
|
+
frontier.push(cell);
|
|
61
|
+
else if (!mines[cell] && !pinned.has(cell))
|
|
62
|
+
cutOff.push(cell);
|
|
63
|
+
}
|
|
64
|
+
let from, to;
|
|
65
|
+
const lost = cutOff.length > 0 && random() < 0.5 ? pick(cutOff, random) : undefined;
|
|
66
|
+
if (lost !== undefined) {
|
|
67
|
+
// A safe cell no opened clue touches is walled in by mines. Either wall one in more, or take a wall mine away.
|
|
68
|
+
const walls = solver.adjacent[lost].filter((n) => mines[n] && !pinned.has(n));
|
|
69
|
+
if (walls.length > 0 && random() < 0.5) {
|
|
70
|
+
from = pick(walls, random);
|
|
71
|
+
to = pick(around(from, REACH).filter((c) => !mines[c]), random);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
to = lost;
|
|
75
|
+
from = pick(around(lost, REACH + 1).filter((c) => mines[c]), random);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
const focus = pick(frontier.length > 0 ? frontier : covered, random);
|
|
80
|
+
if (focus === undefined)
|
|
81
|
+
break;
|
|
82
|
+
let near = around(focus, REACH);
|
|
83
|
+
from = pick(near.filter((c) => mines[c]), random);
|
|
84
|
+
if (from === undefined) {
|
|
85
|
+
near = around(focus, REACH + 2);
|
|
86
|
+
from = pick(near.filter((c) => mines[c]), random);
|
|
87
|
+
}
|
|
88
|
+
to = pick(near.filter((c) => !mines[c]), random);
|
|
89
|
+
}
|
|
90
|
+
if (from === undefined || to === undefined)
|
|
91
|
+
continue;
|
|
92
|
+
move(solver, mines, clues, from, to);
|
|
93
|
+
const next = solver.run(clues, first, enumerate);
|
|
94
|
+
const worse = next.remaining - report.remaining;
|
|
95
|
+
if (worse < 0 || (worse === 0 && next.unresolved <= report.unresolved) || (worse > 0 && random() < Math.exp(-worse / WARMTH)))
|
|
96
|
+
report = next;
|
|
97
|
+
else
|
|
98
|
+
move(solver, mines, clues, to, from);
|
|
99
|
+
if (report.remaining < closest) {
|
|
100
|
+
closest = report.remaining;
|
|
101
|
+
idle = 0;
|
|
102
|
+
}
|
|
103
|
+
else if (++idle > patience) {
|
|
104
|
+
// Stuck for a long while in one corner of the search: begin again from a new layout.
|
|
105
|
+
restart();
|
|
106
|
+
report = solver.run(clues, first, enumerate);
|
|
107
|
+
closest = report.remaining;
|
|
108
|
+
idle = 0;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return { solved: report.solved, tries, remaining: report.remaining };
|
|
112
|
+
}
|
package/dist/shape.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Settings } from "./jirai.types.ts";
|
|
2
|
+
/** Board silhouettes supported by settings. */
|
|
2
3
|
export declare const SHAPES: readonly ["rectangle", "heart", "star", "hexagon"];
|
|
3
4
|
/** Row-major addresses stay stable; omitted cells have no clue and no neighbours. */
|
|
4
5
|
export declare function activeCell(settings: Settings, cell: number): boolean;
|
|
6
|
+
/** Returns all active cells in stable row-major order. */
|
|
5
7
|
export declare function activeCells(settings: Settings): number[];
|
package/dist/shape.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/** Board silhouettes supported by settings. */
|
|
1
2
|
export const SHAPES = ["rectangle", "heart", "star", "hexagon"];
|
|
2
3
|
/** Row-major addresses stay stable; omitted cells have no clue and no neighbours. */
|
|
3
4
|
export function activeCell(settings, cell) {
|
|
@@ -28,6 +29,7 @@ export function activeCell(settings, cell) {
|
|
|
28
29
|
default: return true;
|
|
29
30
|
}
|
|
30
31
|
}
|
|
32
|
+
/** Returns all active cells in stable row-major order. */
|
|
31
33
|
export function activeCells(settings) {
|
|
32
34
|
return Array.from({ length: settings.width * settings.height }, (_, cell) => cell).filter(cell => activeCell(settings, cell));
|
|
33
35
|
}
|
package/dist/solve.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Settings } from "./jirai.types.ts";
|
|
2
|
+
/** Why a cell was proved: one clue, two overlapping clues, the mine counter, or every arrangement of a small frontier. */
|
|
3
|
+
export type ProofKind = "count" | "overlap" | "total" | "enumeration";
|
|
4
|
+
/** Cells proved by each kind of reasoning. */
|
|
5
|
+
export type ProofTally = Record<ProofKind, number>;
|
|
6
|
+
/** What a run of the deduction solver found. */
|
|
7
|
+
export type SolveReport = {
|
|
8
|
+
/** Every safe cell was opened using only proved deductions. */
|
|
9
|
+
solved: boolean;
|
|
10
|
+
/** Safe cells still covered when the solver stopped; zero when solved. */
|
|
11
|
+
remaining: number;
|
|
12
|
+
/** Safe cells opened, including the free opening. */
|
|
13
|
+
opened: number;
|
|
14
|
+
/** Covered cells, safe or mine, nothing has been proved about. */
|
|
15
|
+
unresolved: number;
|
|
16
|
+
/** Cells opened by the first reveal and its flood, before any deduction. */
|
|
17
|
+
free: number;
|
|
18
|
+
/** Cells proved (safe or mine) by each kind of reasoning. */
|
|
19
|
+
proved: ProofTally;
|
|
20
|
+
/** The longest chain of deductions, each resting on the one before. */
|
|
21
|
+
depth: number;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The same deductions as `deduce` (one clue, two overlapping clues, the mine counter, exact enumeration of a
|
|
25
|
+
* small frontier), run to a fixed point over one board in a single pass instead of one deduction per scan of the
|
|
26
|
+
* whole field. A deduction found here is a deduction `deduce` would find; the order differs, the closure does not.
|
|
27
|
+
* Built once for a set of settings, then `run` as often as needed on different mine layouts.
|
|
28
|
+
*/
|
|
29
|
+
export declare class Solver {
|
|
30
|
+
readonly settings: Settings;
|
|
31
|
+
readonly adjacent: readonly (readonly number[])[];
|
|
32
|
+
readonly cells: number;
|
|
33
|
+
readonly safeTotal: number;
|
|
34
|
+
readonly mineTotal: number;
|
|
35
|
+
readonly state: Uint8Array;
|
|
36
|
+
private readonly start;
|
|
37
|
+
private readonly need;
|
|
38
|
+
private readonly unknown;
|
|
39
|
+
private readonly chain;
|
|
40
|
+
private readonly dirty;
|
|
41
|
+
private readonly searched;
|
|
42
|
+
private readonly marks;
|
|
43
|
+
private readonly seen;
|
|
44
|
+
private readonly visit;
|
|
45
|
+
private readonly queue;
|
|
46
|
+
private readonly stack;
|
|
47
|
+
private clues;
|
|
48
|
+
private stamp;
|
|
49
|
+
private covered;
|
|
50
|
+
private left;
|
|
51
|
+
private opened;
|
|
52
|
+
private free;
|
|
53
|
+
private deepestMine;
|
|
54
|
+
private depth;
|
|
55
|
+
private broken;
|
|
56
|
+
private searching;
|
|
57
|
+
private proved;
|
|
58
|
+
constructor(settings: Settings, adjacent?: readonly (readonly number[])[]);
|
|
59
|
+
/** Clues of a layout: -1 for a mine, -2 outside the shape, otherwise the count of neighbouring mines. */
|
|
60
|
+
clueFor(mines: ArrayLike<number | boolean>, into?: Int8Array): Int8Array;
|
|
61
|
+
/** Play the opening at `first` and every deduction after it. `clues` is read, never changed. */
|
|
62
|
+
run(clues: Int8Array, first: number, enumerate?: boolean): SolveReport;
|
|
63
|
+
private report;
|
|
64
|
+
private touch;
|
|
65
|
+
/** Open a cell known to be safe, and flood through every zero. `kind` null is the free opening. */
|
|
66
|
+
private reveal;
|
|
67
|
+
private flag;
|
|
68
|
+
private settle;
|
|
69
|
+
/** One deduction from the clue at `cell` alone, or from it and an overlapping clue. */
|
|
70
|
+
private step;
|
|
71
|
+
/** The mine counter: nothing left to find, everything left a mine, or a clue that accounts for all that remain. */
|
|
72
|
+
private counter;
|
|
73
|
+
/**
|
|
74
|
+
* Exact enumeration of every frontier small enough, as `enumerateForced` does. Every component is read from the
|
|
75
|
+
* same position and the answers are applied together, exactly as one `deduce` call does, so that this solver and
|
|
76
|
+
* the hint engine finish the same boards: with a cap on the size of a frontier, the order of enumerating matters.
|
|
77
|
+
*/
|
|
78
|
+
private enumerate;
|
|
79
|
+
}
|