@johnmorrisdotca/kazu 1.2.0 → 1.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.
Files changed (113) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +69 -27
  3. package/dist/akari-entry.d.ts +1 -0
  4. package/dist/akari-entry.js +1 -0
  5. package/dist/akari.constants.d.ts +12 -0
  6. package/dist/akari.constants.js +12 -0
  7. package/dist/akari.types.d.ts +19 -0
  8. package/dist/akariGenerate.d.ts +10 -5
  9. package/dist/akariGenerate.js +101 -65
  10. package/dist/akariLogic.d.ts +10 -0
  11. package/dist/akariLogic.js +84 -0
  12. package/dist/akariRate.d.ts +6 -0
  13. package/dist/akariRate.js +34 -0
  14. package/dist/akariSolve.d.ts +1 -1
  15. package/dist/akariSolve.js +8 -96
  16. package/dist/akariTemplate.d.ts +7 -0
  17. package/dist/akariTemplate.js +78 -0
  18. package/dist/cells.d.ts +4 -4
  19. package/dist/cells.js +6 -6
  20. package/dist/csp.d.ts +63 -0
  21. package/dist/csp.js +162 -0
  22. package/dist/fillomino-entry.d.ts +1 -0
  23. package/dist/fillomino-entry.js +1 -0
  24. package/dist/fillomino.constants.d.ts +6 -2
  25. package/dist/fillomino.constants.js +6 -2
  26. package/dist/fillomino.types.d.ts +18 -1
  27. package/dist/fillominoBuild.d.ts +10 -0
  28. package/dist/fillominoBuild.js +78 -0
  29. package/dist/fillominoGenerate.d.ts +7 -1
  30. package/dist/fillominoGenerate.js +75 -107
  31. package/dist/fillominoLogic.d.ts +14 -0
  32. package/dist/fillominoLogic.js +177 -0
  33. package/dist/fillominoMount.js +4 -1
  34. package/dist/fillominoRate.d.ts +3 -0
  35. package/dist/fillominoRate.js +57 -0
  36. package/dist/fillominoSolve.js +15 -88
  37. package/dist/groupSolve.d.ts +9 -0
  38. package/dist/groupSolve.js +66 -0
  39. package/dist/hitori-entry.d.ts +1 -0
  40. package/dist/hitori-entry.js +1 -0
  41. package/dist/hitori.constants.d.ts +7 -1
  42. package/dist/hitori.constants.js +7 -1
  43. package/dist/hitori.types.d.ts +18 -1
  44. package/dist/hitoriBoard.js +2 -1
  45. package/dist/hitoriBuild.d.ts +12 -0
  46. package/dist/hitoriBuild.js +117 -0
  47. package/dist/hitoriGenerate.d.ts +10 -3
  48. package/dist/hitoriGenerate.js +59 -157
  49. package/dist/hitoriLogic.d.ts +12 -0
  50. package/dist/hitoriLogic.js +189 -0
  51. package/dist/hitoriRate.d.ts +3 -0
  52. package/dist/hitoriRate.js +32 -0
  53. package/dist/hitoriSolve.d.ts +4 -1
  54. package/dist/hitoriSolve.js +11 -62
  55. package/dist/kakuro-entry.d.ts +1 -0
  56. package/dist/kakuro-entry.js +1 -0
  57. package/dist/kakuro.constants.d.ts +6 -1
  58. package/dist/kakuro.constants.js +6 -1
  59. package/dist/kakuro.types.d.ts +19 -0
  60. package/dist/kakuroBuild.d.ts +30 -0
  61. package/dist/kakuroBuild.js +337 -0
  62. package/dist/kakuroGenerate.d.ts +10 -3
  63. package/dist/kakuroGenerate.js +54 -122
  64. package/dist/kakuroLogic.d.ts +13 -0
  65. package/dist/kakuroLogic.js +153 -0
  66. package/dist/kakuroRate.d.ts +3 -0
  67. package/dist/kakuroRate.js +40 -0
  68. package/dist/kakuroSolve.js +17 -69
  69. package/dist/kakuroTemplate.d.ts +3 -0
  70. package/dist/kakuroTemplate.js +130 -0
  71. package/dist/kinds.js +1 -1
  72. package/dist/layout.js +1 -0
  73. package/dist/mount.d.ts +0 -18
  74. package/dist/mount.js +24 -3
  75. package/dist/names.js +3 -3
  76. package/dist/numberPlace.d.ts +1 -1
  77. package/dist/numberPlace.js +21 -8
  78. package/dist/shikaku-entry.d.ts +1 -0
  79. package/dist/shikaku-entry.js +1 -0
  80. package/dist/shikaku.constants.d.ts +5 -2
  81. package/dist/shikaku.constants.js +5 -2
  82. package/dist/shikaku.types.d.ts +18 -1
  83. package/dist/shikakuBuild.d.ts +21 -0
  84. package/dist/shikakuBuild.js +135 -0
  85. package/dist/shikakuGenerate.d.ts +6 -1
  86. package/dist/shikakuGenerate.js +50 -35
  87. package/dist/shikakuLogic.d.ts +13 -0
  88. package/dist/shikakuLogic.js +84 -0
  89. package/dist/shikakuRate.d.ts +3 -0
  90. package/dist/shikakuRate.js +27 -0
  91. package/dist/shikakuSolve.d.ts +1 -1
  92. package/dist/shikakuSolve.js +23 -44
  93. package/dist/shikakuTemplate.d.ts +3 -0
  94. package/dist/shikakuTemplate.js +43 -0
  95. package/dist/slitherlink-entry.d.ts +1 -0
  96. package/dist/slitherlink-entry.js +1 -0
  97. package/dist/slitherlink.constants.d.ts +5 -0
  98. package/dist/slitherlink.constants.js +5 -0
  99. package/dist/slitherlink.types.d.ts +18 -0
  100. package/dist/slitherlinkGenerate.d.ts +10 -3
  101. package/dist/slitherlinkGenerate.js +161 -80
  102. package/dist/slitherlinkLogic.d.ts +10 -0
  103. package/dist/slitherlinkLogic.js +267 -0
  104. package/dist/slitherlinkRate.d.ts +3 -0
  105. package/dist/slitherlinkRate.js +26 -0
  106. package/dist/slitherlinkSolve.d.ts +1 -1
  107. package/dist/slitherlinkSolve.js +9 -117
  108. package/dist/slitherlinkTemplate.d.ts +3 -0
  109. package/dist/slitherlinkTemplate.js +101 -0
  110. package/dist/strings.js +2 -0
  111. package/dist/version.d.ts +1 -1
  112. package/dist/version.js +1 -1
  113. package/package.json +2 -2
@@ -0,0 +1,177 @@
1
+ import { fillominoNeighbours } from "./fillominoBoard.js";
2
+ /** The most any square can be: a region holding a given or an entry is that number, and one with none lies among the squares with neither. */
3
+ export function fillominoMostValue(board, fixed) {
4
+ let most = 1;
5
+ for (const value of fixed)
6
+ if (value > most)
7
+ most = value;
8
+ const seen = new Uint8Array(fixed.length);
9
+ for (let start = 0; start < fixed.length; start += 1) {
10
+ if (fixed[start] || seen[start])
11
+ continue;
12
+ let size = 0;
13
+ const stack = [start];
14
+ seen[start] = 1;
15
+ while (stack.length) {
16
+ const cell = stack.pop();
17
+ size += 1;
18
+ for (const next of fillominoNeighbours(board, cell))
19
+ if (!fixed[next] && !seen[next]) {
20
+ seen[next] = 1;
21
+ stack.push(next);
22
+ }
23
+ }
24
+ if (size > most)
25
+ most = size;
26
+ }
27
+ return most;
28
+ }
29
+ /**
30
+ * Fillomino as variables: one per square, a slot for each number it can be (1 up to `most`). A finished group of
31
+ * equal numbers keeps its neighbours off that number; a group still short must be able to grow, to squares that can
32
+ * still be that number, until it is the right size, and settles on them when there is exactly room; and a square
33
+ * can only be a number if the squares that can still be it, joined up, make a group big enough.
34
+ */
35
+ export function fillominoModel(board, fixed, most = fillominoMostValue(board, fixed)) {
36
+ const cells = board.width * board.height, values = most;
37
+ const nb = new Int32Array(cells * 4).fill(-1);
38
+ for (let cell = 0; cell < cells; cell += 1)
39
+ fillominoNeighbours(board, cell).forEach((next, k) => { nb[cell * 4 + k] = next; });
40
+ const starts = Int32Array.from({ length: cells + 1 }, (_, i) => i * values);
41
+ const only = new Int32Array(cells), label = new Int32Array(cells), size = new Int32Array(cells + 1), stack = new Int32Array(cells + 1), members = new Int32Array(cells), seen = new Int32Array(cells);
42
+ const dirty = new Uint8Array(values + 1);
43
+ let stamp = 0;
44
+ const propagate = (alive) => {
45
+ let changed = true;
46
+ dirty.fill(1);
47
+ while (changed) {
48
+ changed = false;
49
+ for (let cell = 0; cell < cells; cell += 1) {
50
+ let left = 0, last = 0;
51
+ const base = cell * values;
52
+ for (let v = 0; v < values; v += 1)
53
+ if (alive[base + v]) {
54
+ left += 1;
55
+ last = v + 1;
56
+ }
57
+ if (!left)
58
+ return false;
59
+ only[cell] = left === 1 ? last : 0;
60
+ }
61
+ // Groups of squares already settled on the same number.
62
+ label.fill(0);
63
+ for (let start = 0; start < cells; start += 1) {
64
+ const v = only[start];
65
+ if (!v || label[start])
66
+ continue;
67
+ let count = 0, top = 0;
68
+ stack[top++] = start;
69
+ label[start] = 1;
70
+ while (top) {
71
+ const cell = stack[--top];
72
+ members[count++] = cell;
73
+ for (let k = 0; k < 4; k += 1) {
74
+ const next = nb[cell * 4 + k];
75
+ if (next >= 0 && only[next] === v && !label[next]) {
76
+ label[next] = 1;
77
+ stack[top++] = next;
78
+ }
79
+ }
80
+ }
81
+ if (count > v)
82
+ return false;
83
+ if (count === v) {
84
+ for (let m = 0; m < count; m += 1)
85
+ for (let k = 0; k < 4; k += 1) {
86
+ const next = nb[members[m] * 4 + k];
87
+ if (next >= 0 && only[next] !== v && alive[next * values + v - 1]) {
88
+ alive[next * values + v - 1] = 0;
89
+ dirty[v] = 1;
90
+ changed = true;
91
+ }
92
+ }
93
+ continue;
94
+ }
95
+ // Short of its size: grow through squares that can still be v.
96
+ stamp += 1;
97
+ let reached = count, frontier = 0, lastFrontier = -1;
98
+ for (let m = 0; m < count; m += 1)
99
+ seen[members[m]] = stamp;
100
+ for (let at = 0; at < reached; at += 1) {
101
+ const cell = members[at];
102
+ for (let k = 0; k < 4; k += 1) {
103
+ const next = nb[cell * 4 + k];
104
+ if (next < 0 || seen[next] === stamp || !alive[next * values + v - 1])
105
+ continue;
106
+ seen[next] = stamp;
107
+ members[reached++] = next;
108
+ if (at < count) {
109
+ frontier += 1;
110
+ lastFrontier = next;
111
+ }
112
+ }
113
+ }
114
+ if (reached < v)
115
+ return false;
116
+ if (reached === v) {
117
+ for (let m = 0; m < reached; m += 1) {
118
+ const base = members[m] * values;
119
+ for (let other = 0; other < values; other += 1)
120
+ if (other !== v - 1 && alive[base + other]) {
121
+ alive[base + other] = 0;
122
+ dirty[other + 1] = 1;
123
+ changed = true;
124
+ }
125
+ }
126
+ }
127
+ else if (frontier === 1 && !only[lastFrontier]) {
128
+ const base = lastFrontier * values;
129
+ for (let other = 0; other < values; other += 1)
130
+ if (other !== v - 1 && alive[base + other]) {
131
+ alive[base + other] = 0;
132
+ dirty[other + 1] = 1;
133
+ changed = true;
134
+ }
135
+ }
136
+ }
137
+ // A number needs room: the squares that can be it, joined up, must make a group at least that big.
138
+ for (let v = 2; v <= values; v += 1) {
139
+ if (!dirty[v])
140
+ continue;
141
+ dirty[v] = 0;
142
+ label.fill(0);
143
+ let components = 0;
144
+ const slot = v - 1;
145
+ for (let start = 0; start < cells; start += 1) {
146
+ if (label[start] || !alive[start * values + slot])
147
+ continue;
148
+ components += 1;
149
+ let count = 0, top = 0;
150
+ stack[top++] = start;
151
+ label[start] = components;
152
+ while (top) {
153
+ const cell = stack[--top];
154
+ count += 1;
155
+ for (let k = 0; k < 4; k += 1) {
156
+ const next = nb[cell * 4 + k];
157
+ if (next >= 0 && !label[next] && alive[next * values + slot]) {
158
+ label[next] = components;
159
+ stack[top++] = next;
160
+ }
161
+ }
162
+ }
163
+ size[components] = count;
164
+ }
165
+ for (let cell = 0; cell < cells; cell += 1) {
166
+ if (label[cell] && size[label[cell]] < v) {
167
+ alive[cell * values + slot] = 0;
168
+ dirty[v] = 1;
169
+ changed = true;
170
+ }
171
+ }
172
+ }
173
+ }
174
+ return true;
175
+ };
176
+ return { csp: { starts, propagate }, most: values };
177
+ }
@@ -1,4 +1,5 @@
1
1
  import { checkFillomino } from "./fillominoBoard.js";
2
+ import { fillominoMostValue } from "./fillominoLogic.js";
2
3
  import { drawFillomino } from "./fillominoDraw.js";
3
4
  import { decodeFillomino, encodeFillomino, hintFillomino, newFillomino, restartFillomino, setFillominoCell, undoFillomino } from "./fillominoGame.js";
4
5
  import { FILLOMINO_PLAY_STYLE } from "./fillominoStyle.js";
@@ -98,7 +99,9 @@ export function mountFillomino(host, initial) {
98
99
  const number = document.createElement("select");
99
100
  number.setAttribute("aria-label", words.selectNumber);
100
101
  number.dataset.number = "true";
101
- for (let value = 1; value <= game.entries.length; value += 1) {
102
+ // Only numbers an answer can hold: more than the biggest given only fits among squares that have no given.
103
+ const most = Math.max(fillominoMostValue(game.board, game.board.givens), ...game.entries);
104
+ for (let value = 1; value <= most; value += 1) {
102
105
  const option = document.createElement("option");
103
106
  option.value = String(value);
104
107
  option.textContent = String(value);
@@ -0,0 +1,3 @@
1
+ import type { FillominoBoard, FillominoRating } from "./fillomino.types.ts";
2
+ /** Rates a board with one answer by solving it as a person would: with the rules, then by supposing one number at a time. */
3
+ export declare function rateFillomino(board: FillominoBoard): FillominoRating;
@@ -0,0 +1,57 @@
1
+ import { isFillominoBoard } from "./fillominoBoard.js";
2
+ import { fillominoModel } from "./fillominoLogic.js";
3
+ import { decide, logicCsp, openSlots, solveCsp } from "./csp.js";
4
+ /** Rates a board with one answer by solving it as a person would: with the rules, then by supposing one number at a time. */
5
+ export function rateFillomino(board) {
6
+ if (!isFillominoBoard(board))
7
+ throw new RangeError("Invalid Fillomino board");
8
+ const { csp, most } = fillominoModel(board, board.givens);
9
+ const start = openSlots(csp);
10
+ board.givens.forEach((value, cell) => { if (value)
11
+ decide(csp, start, cell, cell * most + value - 1); });
12
+ const proof = solveCsp(csp, start, 2, 200000);
13
+ if (proof.count !== 1 || proof.exhausted)
14
+ throw new RangeError("Fillomino rating needs a board with one answer");
15
+ const plain = logicCsp(csp, start, 0);
16
+ const probing = plain.solved ? plain : logicCsp(csp, start, 1);
17
+ const answer = Array.from({ length: board.givens.length }, (_, cell) => { for (let v = 0; v < most; v += 1)
18
+ if (proof.solution[cell * most + v])
19
+ return v + 1; return 0; });
20
+ const regions = new Map();
21
+ const label = Array(answer.length).fill(-1);
22
+ let count = 0, largest = 0, unnamed = 0;
23
+ for (let first = 0; first < answer.length; first += 1) {
24
+ if (label[first] >= 0)
25
+ continue;
26
+ const members = [first];
27
+ label[first] = count;
28
+ for (let at = 0; at < members.length; at += 1) {
29
+ const cell = members[at], x = cell % board.width, y = Math.floor(cell / board.width);
30
+ for (const [nx, ny] of [[x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]]) {
31
+ if (nx < 0 || ny < 0 || nx >= board.width || ny >= board.height)
32
+ continue;
33
+ const next = ny * board.width + nx;
34
+ if (label[next] < 0 && answer[next] === answer[first]) {
35
+ label[next] = count;
36
+ members.push(next);
37
+ }
38
+ }
39
+ }
40
+ largest = Math.max(largest, members.length);
41
+ if (!members.some(cell => board.givens[cell]))
42
+ unnamed += 1;
43
+ regions.set(String(count), members.length);
44
+ count += 1;
45
+ }
46
+ const givens = board.givens.filter(Boolean).length;
47
+ return {
48
+ depth: plain.solved ? 0 : probing.solved ? 1 : 2,
49
+ probes: plain.solved ? 0 : probing.probes,
50
+ givens,
51
+ givenShare: givens / board.givens.length,
52
+ regions: count,
53
+ unnamed,
54
+ largest,
55
+ meanRegion: board.givens.length / count,
56
+ };
57
+ }
@@ -1,5 +1,7 @@
1
1
  import { FILLOMINO_MOST_NODES } from "./fillomino.constants.js";
2
- import { fillominoNeighbours, isFillominoBoard } from "./fillominoBoard.js";
2
+ import { isFillominoBoard } from "./fillominoBoard.js";
3
+ import { fillominoModel } from "./fillominoLogic.js";
4
+ import { decide, openSlots, solveCsp } from "./csp.js";
3
5
  /** Counts completed partitions. `complete` is false when a bound, including the answer limit, stops search. */
4
6
  export function solveFillomino(board, entries = board.givens, options = {}) {
5
7
  if (!isFillominoBoard(board))
@@ -21,91 +23,16 @@ export function solveFillomino(board, entries = board.givens, options = {}) {
21
23
  });
22
24
  if (fixed.some(value => value < 0))
23
25
  return { count: 0, solution: null, complete: true, nodes: 0 };
24
- const resolved = Array(entries.length).fill(0);
25
- const state = { nodes: 0, stopped: false };
26
- let count = 0;
27
- let solution = null;
28
- const visit = () => {
29
- if (++state.nodes > budget) {
30
- state.stopped = true;
31
- return;
32
- }
33
- let anchor = -1;
34
- for (let cell = 0; cell < resolved.length; cell += 1) {
35
- if (resolved[cell] === 0) {
36
- anchor = cell;
37
- break;
38
- }
39
- }
40
- if (anchor < 0) {
41
- count += 1;
42
- solution ?? (solution = [...resolved]);
43
- if (count >= limit)
44
- state.stopped = true;
45
- return;
46
- }
47
- const choices = fixed[anchor] ? [fixed[anchor]] : Array.from({ length: entries.length }, (_, index) => index + 1);
48
- for (const value of choices) {
49
- const shapes = regionShapes(board, anchor, value, resolved, fixed, state, budget);
50
- if (state.stopped)
51
- return;
52
- for (const shape of shapes) {
53
- for (const cell of shape)
54
- resolved[cell] = value;
55
- visit();
56
- for (const cell of shape)
57
- resolved[cell] = 0;
58
- if (state.stopped)
59
- return;
60
- }
61
- }
62
- };
63
- visit();
64
- const complete = !state.stopped;
65
- return { count, solution, complete, nodes: Math.min(state.nodes, budget) };
66
- }
67
- function regionShapes(board, anchor, area, resolved, fixed, state, budget) {
68
- if (fixed[anchor] !== 0 && fixed[anchor] !== area)
69
- return [];
70
- const shapes = [];
71
- const seen = new Set();
72
- const grow = (cells) => {
73
- if (state.stopped)
74
- return;
75
- if (++state.nodes > budget) {
76
- state.stopped = true;
77
- return;
78
- }
79
- const key = [...cells].sort((a, b) => a - b).join(",");
80
- if (seen.has(key))
81
- return;
82
- seen.add(key);
83
- if (cells.length === area) {
84
- const members = new Set(cells);
85
- for (const cell of cells) {
86
- for (const next of fillominoNeighbours(board, cell)) {
87
- if (members.has(next))
88
- continue;
89
- if (resolved[next] === area || fixed[next] === area)
90
- return;
91
- }
92
- }
93
- shapes.push([...cells]);
94
- return;
95
- }
96
- const frontier = new Set();
97
- for (const cell of cells) {
98
- for (const next of fillominoNeighbours(board, cell)) {
99
- if (cells.includes(next) || resolved[next] !== 0)
100
- continue;
101
- if (fixed[next] !== 0 && fixed[next] !== area)
102
- continue;
103
- frontier.add(next);
104
- }
105
- }
106
- for (const next of frontier)
107
- grow([...cells, next]);
108
- };
109
- grow([anchor]);
110
- return shapes;
26
+ const { csp, most } = fillominoModel(board, fixed);
27
+ const start = openSlots(csp);
28
+ fixed.forEach((value, cell) => { if (value)
29
+ decide(csp, start, cell, cell * most + value - 1); });
30
+ const found = solveCsp(csp, start, limit, budget);
31
+ const solution = found.solution ? Array.from({ length: fixed.length }, (_, cell) => {
32
+ for (let v = 0; v < most; v += 1)
33
+ if (found.solution[cell * most + v])
34
+ return v + 1;
35
+ return 0;
36
+ }) : null;
37
+ return { count: found.count, solution, complete: !found.exhausted && !found.stopped, nodes: found.nodes };
111
38
  }
@@ -60,6 +60,15 @@ export declare function applySingles(grid: Grid, layout: Layout): SinglesResult;
60
60
  * has no answer. Meant for a grid already known to have exactly one.
61
61
  */
62
62
  export declare function guessDepth(grid: Grid, layout: Layout): number;
63
+ /**
64
+ * What reasoning with at most `guesses` guesses proves about a grid: 0 when it proves there is no answer, 1
65
+ * when it proves there is exactly one, 2 when it proves neither (it could not finish, or there are several).
66
+ * A guess is made at the most constrained empty cell and every value is tried, each followed by everything
67
+ * singles find; the grid has one answer when exactly one value leads to one and every other leads to none.
68
+ * This is a person's proof, and it stops where theirs would, so it costs a bounded few hundred passes of
69
+ * singles however sparse and large the grid is: counting every answer of a sparse 25×25 grid does not.
70
+ */
71
+ export declare function provedByGuessing(grid: Grid, layout: Layout, guesses: number): 0 | 1 | 2;
63
72
  /**
64
73
  * A full grid for this layout, drawn at random, or null when the search runs
65
74
  * past `budget` steps — which for a jigsaw means these regions are a poor
@@ -121,6 +121,8 @@ export function countSolutions(grid, layout, limit = 2) {
121
121
  * rather than keeping a browser waiting.
122
122
  */
123
123
  export function countSolutionsWithin(grid, layout, limit, budget, first) {
124
+ if (layout.size > LARGEST_SEARCHED_BARE)
125
+ return countByReasoning(grid, layout, limit, budget, first);
124
126
  const work = [...grid];
125
127
  const taken = used(work, layout);
126
128
  let found = 0;
@@ -150,6 +152,41 @@ export function countSolutionsWithin(grid, layout, limit, budget, first) {
150
152
  step();
151
153
  return steps > budget && found < limit ? null : found;
152
154
  }
155
+ /**
156
+ * Up to this side the search guesses at the most constrained cell and nothing else, which is how every
157
+ * 4×4 to 16×16 grid has always been counted. Past it (the 25×25) a sparse grid thrashes that way for
158
+ * minutes, so a search there lets singles finish whatever they can before each guess.
159
+ */
160
+ const LARGEST_SEARCHED_BARE = 16;
161
+ /** The same count for a large grid: at every step all the singles are filled in first, and only what they leave is guessed at. */
162
+ function countByReasoning(grid, layout, limit, budget, first) {
163
+ let found = 0;
164
+ let steps = 0;
165
+ const step = (from) => {
166
+ if (found >= limit || steps > budget)
167
+ return;
168
+ steps += 1;
169
+ const singles = applySingles(from, layout);
170
+ if (singles.contradiction)
171
+ return;
172
+ if (singles.solved) {
173
+ if (found === 0)
174
+ first?.([...singles.grid]);
175
+ found += 1;
176
+ return;
177
+ }
178
+ const { index, mask } = mostConstrained(singles.grid, layout, used(singles.grid, layout));
179
+ for (let left = mask; left !== 0; left &= left - 1) {
180
+ const next = [...singles.grid];
181
+ next[index] = lowestBit(left);
182
+ step(next);
183
+ if (found >= limit || steps > budget)
184
+ return;
185
+ }
186
+ };
187
+ step(grid);
188
+ return steps > budget && found < limit ? null : found;
189
+ }
153
190
  /**
154
191
  * THE ANSWER, WORKED OUT FROM THE GIVENS: a finished puzzle kept before its
155
192
  * grid was, drawn solved rather than as dealt. Every puzzle made here has
@@ -245,6 +282,35 @@ export function guessDepth(grid, layout) {
245
282
  }
246
283
  return deepest === Infinity ? Infinity : deepest + 1;
247
284
  }
285
+ /**
286
+ * What reasoning with at most `guesses` guesses proves about a grid: 0 when it proves there is no answer, 1
287
+ * when it proves there is exactly one, 2 when it proves neither (it could not finish, or there are several).
288
+ * A guess is made at the most constrained empty cell and every value is tried, each followed by everything
289
+ * singles find; the grid has one answer when exactly one value leads to one and every other leads to none.
290
+ * This is a person's proof, and it stops where theirs would, so it costs a bounded few hundred passes of
291
+ * singles however sparse and large the grid is: counting every answer of a sparse 25×25 grid does not.
292
+ */
293
+ export function provedByGuessing(grid, layout, guesses) {
294
+ const singles = applySingles(grid, layout);
295
+ if (singles.contradiction)
296
+ return 0;
297
+ if (singles.solved)
298
+ return 1;
299
+ if (guesses < 1)
300
+ return 2;
301
+ const work = singles.grid;
302
+ const { index, mask } = mostConstrained(work, layout, used(work, layout));
303
+ let ones = 0;
304
+ for (let left = mask; left !== 0; left &= left - 1) {
305
+ const next = [...work];
306
+ next[index] = lowestBit(left);
307
+ const outcome = provedByGuessing(next, layout, guesses - 1);
308
+ if (outcome === 2)
309
+ return 2;
310
+ ones += outcome;
311
+ }
312
+ return ones === 0 ? 0 : ones === 1 ? 1 : 2;
313
+ }
248
314
  /**
249
315
  * A full grid for this layout, drawn at random, or null when the search runs
250
316
  * past `budget` steps — which for a jigsaw means these regions are a poor
@@ -3,4 +3,5 @@ export * from "./hitori.constants.ts";
3
3
  export * from "./hitoriBoard.ts";
4
4
  export * from "./hitoriSolve.ts";
5
5
  export * from "./hitoriGenerate.ts";
6
+ export * from "./hitoriRate.ts";
6
7
  export * from "./hitoriGame.ts";
@@ -3,4 +3,5 @@ export * from "./hitori.constants.js";
3
3
  export * from "./hitoriBoard.js";
4
4
  export * from "./hitoriSolve.js";
5
5
  export * from "./hitoriGenerate.js";
6
+ export * from "./hitoriRate.js";
6
7
  export * from "./hitoriGame.js";
@@ -1,2 +1,8 @@
1
1
  export declare const HITORI_MAX_NODES = 300000;
2
- export declare const HITORI_SIZES: readonly [5, 7];
2
+ /** The sides on offer; any side from 4 to `HITORI_MOST_SIDE` is a board `isHitoriBoard` takes. */
3
+ export declare const HITORI_SIZES: readonly [5, 6, 7, 8, 9, 10, 12];
4
+ export declare const HITORI_MOST_SIDE = 12;
5
+ export declare const HITORI_LEAST_SIDE = 4;
6
+ /** How hard a puzzle is made, by what a person must do to solve it; see `rateHitori`. */
7
+ export declare const HITORI_LEVELS: readonly ["easy", "medium", "hard", "extra-hard"];
8
+ export declare const HITORI_MOST_ATTEMPTS = 600;
@@ -1,2 +1,8 @@
1
1
  export const HITORI_MAX_NODES = 300000;
2
- export const HITORI_SIZES = [5, 7];
2
+ /** The sides on offer; any side from 4 to `HITORI_MOST_SIDE` is a board `isHitoriBoard` takes. */
3
+ export const HITORI_SIZES = [5, 6, 7, 8, 9, 10, 12];
4
+ export const HITORI_MOST_SIDE = 12;
5
+ export const HITORI_LEAST_SIDE = 4;
6
+ /** How hard a puzzle is made, by what a person must do to solve it; see `rateHitori`. */
7
+ export const HITORI_LEVELS = ["easy", "medium", "hard", "extra-hard"];
8
+ export const HITORI_MOST_ATTEMPTS = 600;
@@ -1,11 +1,28 @@
1
1
  export type HitoriBoard = {
2
- size: 5 | 7;
2
+ size: number;
3
3
  numbers: readonly number[];
4
4
  };
5
+ export type HitoriLevel = "easy" | "medium" | "hard" | "extra-hard";
5
6
  export type HitoriPuzzle = HitoriBoard & {
6
7
  seed: number;
8
+ level: HitoriLevel;
7
9
  solution: readonly boolean[];
8
10
  };
11
+ /**
12
+ * How hard a board is, measured by solving it. `depth` 0 means the rules solve it, 1 that somebody has to suppose a
13
+ * square shaded or white and watch it break, 2 that more than that is needed. `reach` says the rule that the white
14
+ * squares stay in one piece was needed at depth 0. `probes` is the suppositions depth 1 needed.
15
+ */
16
+ export type HitoriRating = {
17
+ depth: 0 | 1 | 2;
18
+ reach: boolean;
19
+ probes: number;
20
+ shaded: number;
21
+ /** Shaded squares as a share of the board. */
22
+ shadedShare: number;
23
+ /** Squares whose number repeats in their row or column, as a share of the board. */
24
+ repeatShare: number;
25
+ };
9
26
  export type HitoriSolve = {
10
27
  count: number;
11
28
  solution: readonly boolean[] | null;
@@ -1,5 +1,6 @@
1
+ import { HITORI_LEAST_SIDE, HITORI_MOST_SIDE } from "./hitori.constants.js";
1
2
  export function isHitoriBoard(board) {
2
- return !!board && (board.size === 5 || board.size === 7)
3
+ return !!board && Number.isInteger(board.size) && board.size >= HITORI_LEAST_SIDE && board.size <= HITORI_MOST_SIDE
3
4
  && Array.isArray(board.numbers)
4
5
  && board.numbers.length === board.size * board.size
5
6
  && board.numbers.every(value => Number.isInteger(value) && value >= 1 && value <= board.size);
@@ -0,0 +1,12 @@
1
+ import type { HitoriBoard, HitoriLevel } from "./hitori.types.ts";
2
+ import type { Random } from "./random.ts";
3
+ export declare const SHARE: Record<HitoriLevel, number>;
4
+ /** A board with one answer, or null when the random choices did not lead to one. How the numbers are chosen leans on the level. */
5
+ export declare function candidateHitori(size: number, level: HitoriLevel, random: Random, base?: readonly number[]): {
6
+ board: HitoriBoard;
7
+ shaded: readonly boolean[];
8
+ } | null;
9
+ /** A random Latin square: every number once in each row and column, built row by row with backtracking. */
10
+ export declare function latinSquare(size: number, random: Random): number[] | null;
11
+ /** The same square with its rows, columns and numbers renamed at random: still one of each in every line. */
12
+ export declare function isotope(square: readonly number[], size: number, random: Random): number[];