@datamoc/mw_games 0.20.0 → 0.23.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 (151) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/dist/audio/Channels.d.ts +19 -0
  3. package/dist/audio/Channels.d.ts.map +1 -1
  4. package/dist/audio/Channels.js +33 -4
  5. package/dist/audio/Channels.js.map +1 -1
  6. package/dist/audio/LoopTags.d.ts +37 -0
  7. package/dist/audio/LoopTags.d.ts.map +1 -0
  8. package/dist/audio/LoopTags.js +116 -0
  9. package/dist/audio/LoopTags.js.map +1 -0
  10. package/dist/audio/Midi.d.ts +29 -0
  11. package/dist/audio/Midi.d.ts.map +1 -1
  12. package/dist/audio/Midi.js +30 -1
  13. package/dist/audio/Midi.js.map +1 -1
  14. package/dist/audio/MidiRender.d.ts +3 -2
  15. package/dist/audio/MidiRender.d.ts.map +1 -1
  16. package/dist/audio/MidiRender.js +2 -2
  17. package/dist/audio/MidiRender.js.map +1 -1
  18. package/dist/audio/humanize/Groove.d.ts +80 -0
  19. package/dist/audio/humanize/Groove.d.ts.map +1 -0
  20. package/dist/audio/humanize/Groove.js +102 -0
  21. package/dist/audio/humanize/Groove.js.map +1 -0
  22. package/dist/audio/humanize/GrooveConverter.d.ts +49 -0
  23. package/dist/audio/humanize/GrooveConverter.d.ts.map +1 -0
  24. package/dist/audio/humanize/GrooveConverter.js +344 -0
  25. package/dist/audio/humanize/GrooveConverter.js.map +1 -0
  26. package/dist/audio/humanize/GrooveExtractor.d.ts +49 -0
  27. package/dist/audio/humanize/GrooveExtractor.d.ts.map +1 -0
  28. package/dist/audio/humanize/GrooveExtractor.js +179 -0
  29. package/dist/audio/humanize/GrooveExtractor.js.map +1 -0
  30. package/dist/audio/humanize/GrooveHumanizer.d.ts +34 -0
  31. package/dist/audio/humanize/GrooveHumanizer.d.ts.map +1 -0
  32. package/dist/audio/humanize/GrooveHumanizer.js +37 -0
  33. package/dist/audio/humanize/GrooveHumanizer.js.map +1 -0
  34. package/dist/audio/humanize/Humanize.d.ts +54 -0
  35. package/dist/audio/humanize/Humanize.d.ts.map +1 -0
  36. package/dist/audio/humanize/Humanize.js +239 -0
  37. package/dist/audio/humanize/Humanize.js.map +1 -0
  38. package/dist/audio/humanize/HumanizerFactory.d.ts +68 -0
  39. package/dist/audio/humanize/HumanizerFactory.d.ts.map +1 -0
  40. package/dist/audio/humanize/HumanizerFactory.js +84 -0
  41. package/dist/audio/humanize/HumanizerFactory.js.map +1 -0
  42. package/dist/audio/humanize/LiteHumanizer.d.ts +30 -0
  43. package/dist/audio/humanize/LiteHumanizer.d.ts.map +1 -0
  44. package/dist/audio/humanize/LiteHumanizer.js +35 -0
  45. package/dist/audio/humanize/LiteHumanizer.js.map +1 -0
  46. package/dist/audio/humanize/Onsets.d.ts +26 -0
  47. package/dist/audio/humanize/Onsets.d.ts.map +1 -0
  48. package/dist/audio/humanize/Onsets.js +164 -0
  49. package/dist/audio/humanize/Onsets.js.map +1 -0
  50. package/dist/audio/index.d.ts +14 -1
  51. package/dist/audio/index.d.ts.map +1 -1
  52. package/dist/audio/index.js +7 -0
  53. package/dist/audio/index.js.map +1 -1
  54. package/dist/board/bitboard.d.ts +132 -0
  55. package/dist/board/bitboard.d.ts.map +1 -0
  56. package/dist/board/bitboard.js +495 -0
  57. package/dist/board/bitboard.js.map +1 -0
  58. package/dist/board/chess.d.ts +16 -0
  59. package/dist/board/chess.d.ts.map +1 -1
  60. package/dist/board/chess.js +43 -0
  61. package/dist/board/chess.js.map +1 -1
  62. package/dist/board/index.d.ts +9 -1
  63. package/dist/board/index.d.ts.map +1 -1
  64. package/dist/board/index.js +5 -1
  65. package/dist/board/index.js.map +1 -1
  66. package/dist/board/openings.d.ts +58 -0
  67. package/dist/board/openings.d.ts.map +1 -0
  68. package/dist/board/openings.js +45 -0
  69. package/dist/board/openings.js.map +1 -0
  70. package/dist/board/tablebase.d.ts +118 -0
  71. package/dist/board/tablebase.d.ts.map +1 -0
  72. package/dist/board/tablebase.js +414 -0
  73. package/dist/board/tablebase.js.map +1 -0
  74. package/dist/board/tourney.d.ts +90 -0
  75. package/dist/board/tourney.d.ts.map +1 -0
  76. package/dist/board/tourney.js +1013 -0
  77. package/dist/board/tourney.js.map +1 -0
  78. package/dist/core/JavaRandom.d.ts +55 -0
  79. package/dist/core/JavaRandom.d.ts.map +1 -0
  80. package/dist/core/JavaRandom.js +81 -0
  81. package/dist/core/JavaRandom.js.map +1 -0
  82. package/dist/core/index.d.ts +2 -0
  83. package/dist/core/index.d.ts.map +1 -1
  84. package/dist/core/index.js +1 -0
  85. package/dist/core/index.js.map +1 -1
  86. package/dist/mw_games.global.js +123 -116
  87. package/dist/rpg/EventRunner.d.ts +7 -0
  88. package/dist/rpg/EventRunner.d.ts.map +1 -1
  89. package/dist/rpg/EventRunner.js +6 -0
  90. package/dist/rpg/EventRunner.js.map +1 -1
  91. package/dist/rpg/MoveRoute.d.ts +35 -4
  92. package/dist/rpg/MoveRoute.d.ts.map +1 -1
  93. package/dist/rpg/MoveRoute.js +40 -11
  94. package/dist/rpg/MoveRoute.js.map +1 -1
  95. package/dist/two-d/render/FogLayer.d.ts +68 -0
  96. package/dist/two-d/render/FogLayer.d.ts.map +1 -0
  97. package/dist/two-d/render/FogLayer.js +83 -0
  98. package/dist/two-d/render/FogLayer.js.map +1 -0
  99. package/dist/two-d/render/LiquidLayer.d.ts +62 -0
  100. package/dist/two-d/render/LiquidLayer.d.ts.map +1 -0
  101. package/dist/two-d/render/LiquidLayer.js +102 -0
  102. package/dist/two-d/render/LiquidLayer.js.map +1 -0
  103. package/dist/two-d/render/RpgmAutotile.d.ts +42 -0
  104. package/dist/two-d/render/RpgmAutotile.d.ts.map +1 -1
  105. package/dist/two-d/render/RpgmAutotile.js +71 -4
  106. package/dist/two-d/render/RpgmAutotile.js.map +1 -1
  107. package/dist/two-d/render/TileMap.d.ts +50 -2
  108. package/dist/two-d/render/TileMap.d.ts.map +1 -1
  109. package/dist/two-d/render/TileMap.js +120 -2
  110. package/dist/two-d/render/TileMap.js.map +1 -1
  111. package/dist/two-d/render/index.d.ts +7 -3
  112. package/dist/two-d/render/index.d.ts.map +1 -1
  113. package/dist/two-d/render/index.js +3 -1
  114. package/dist/two-d/render/index.js.map +1 -1
  115. package/dist/two-d/ui/Label.d.ts.map +1 -1
  116. package/dist/two-d/ui/Label.js +11 -2
  117. package/dist/two-d/ui/Label.js.map +1 -1
  118. package/dist/two-d/ui/ListView.d.ts +24 -0
  119. package/dist/two-d/ui/ListView.d.ts.map +1 -1
  120. package/dist/two-d/ui/ListView.js +97 -4
  121. package/dist/two-d/ui/ListView.js.map +1 -1
  122. package/dist/two-d/ui/MessageLog.d.ts +62 -0
  123. package/dist/two-d/ui/MessageLog.d.ts.map +1 -0
  124. package/dist/two-d/ui/MessageLog.js +114 -0
  125. package/dist/two-d/ui/MessageLog.js.map +1 -0
  126. package/dist/two-d/ui/RadioGroup.d.ts +68 -0
  127. package/dist/two-d/ui/RadioGroup.d.ts.map +1 -0
  128. package/dist/two-d/ui/RadioGroup.js +177 -0
  129. package/dist/two-d/ui/RadioGroup.js.map +1 -0
  130. package/dist/two-d/ui/TextModel.d.ts +26 -1
  131. package/dist/two-d/ui/TextModel.d.ts.map +1 -1
  132. package/dist/two-d/ui/TextModel.js +56 -7
  133. package/dist/two-d/ui/TextModel.js.map +1 -1
  134. package/dist/two-d/ui/TextSharpness.d.ts +33 -0
  135. package/dist/two-d/ui/TextSharpness.d.ts.map +1 -0
  136. package/dist/two-d/ui/TextSharpness.js +56 -0
  137. package/dist/two-d/ui/TextSharpness.js.map +1 -0
  138. package/dist/two-d/ui/index.d.ts +5 -0
  139. package/dist/two-d/ui/index.d.ts.map +1 -1
  140. package/dist/two-d/ui/index.js +3 -0
  141. package/dist/two-d/ui/index.js.map +1 -1
  142. package/dist/version.d.ts +1 -1
  143. package/dist/version.js +1 -1
  144. package/package.json +19 -7
  145. package/sbom.cdx.json +194 -32
  146. package/tools/build-tablebases.d.mts +22 -0
  147. package/tools/build-tablebases.mjs +549 -0
  148. package/tools/compile-openings.d.mts +20 -0
  149. package/tools/compile-openings.mjs +130 -0
  150. package/tools/play-uci.d.mts +26 -0
  151. package/tools/play-uci.mjs +194 -0
@@ -0,0 +1,22 @@
1
+ export interface TablebaseSpec {
2
+ white: Array<'queen' | 'rook' | 'bishop' | 'knight'>;
3
+ }
4
+
5
+ export interface TablebaseSolution {
6
+ wdl: number[];
7
+ dtm: number[];
8
+ positions: number;
9
+ }
10
+
11
+ /**
12
+ * Solves one white-extras-against-lone-king ending by retrograde analysis.
13
+ * Self-contained for `threads.spawn`: everything it touches is nested inside.
14
+ * `lower` carries the smaller tables captures convert into, null for 3-piece.
15
+ */
16
+ export function tablebaseTask(
17
+ spec: TablebaseSpec,
18
+ lower: Record<string, { wdl: number[]; dtm: number[] }> | null,
19
+ ): TablebaseSolution;
20
+
21
+ /** `KQBK` from an ending id's letters, throwing on anything unshaped. */
22
+ export function specOf(id: string): TablebaseSpec;
@@ -0,0 +1,549 @@
1
+ #!/usr/bin/env node
2
+ import { mkdirSync, writeFileSync } from 'node:fs';
3
+ import { join, relative, basename } from 'node:path';
4
+ import { huffEncode } from '../src/board/tablebase.ts';
5
+ import { spawn } from '../src/threads/spawn.ts';
6
+
7
+ /**
8
+ * Generates pawnless endgame tablebases (WDL plus distance-to-mate) and ships
9
+ * them compiled as JSON a game checks in beside its own code.
10
+ *
11
+ * Each ending is white extras against a lone black king (`KQK`, `KBNK`, ...): a
12
+ * retrograde solve over every placement, verified against the known results
13
+ * (queens and rooks win, lone bishops and knights draw). Captures convert into
14
+ * the already-generated smaller tables, so 3-piece endings generate first and
15
+ * ride along as the 4-piece tasks' arguments.
16
+ *
17
+ * Only Node built-ins plus the framework's own rule shape are used, so this
18
+ * adds no dependency. Generation is the parallel half of item 390's data story
19
+ * beside the in-page search: one `threads.spawn` task per ending over a capped
20
+ * lane queue (each big ending holds hundreds of megabytes of arrays, so lanes
21
+ * default to 4, not the search's 8), the same tools/compress-dist.mjs shape the
22
+ * async search uses. A row that no longer parses fails the compile instead of
23
+ * silently dropping a line; a corrupt artifact fails the load, never misprobes.
24
+ *
25
+ * usage: node tools/build-tablebases.mjs <outDir> [ending ...] [--jobs=N]
26
+ * endings default to the shipped set: KQK KRK KBK KNK KBNK KBBK
27
+ */
28
+
29
+ /**
30
+ * Solves one ending by retrograde analysis: every placement with every side to
31
+ * move, wins/losses propagated from mates and stalemates until nothing changes,
32
+ * the rest draws. **Self-contained by construction:** `spawn` stringifies this
33
+ * function into a worker, so everything it touches is nested inside it; the
34
+ * async-equals-sync probe tests are what keep it honest against `tablebase.ts`.
35
+ *
36
+ * `spec` is `{ white: ['queen', ...] }`; `lower` carries the smaller tables a
37
+ * capture converts into (`{ [id]: { wdl: number[], dtm: number[] } }`, null for
38
+ * 3-piece endings where every capture leaves bare kings). Returns plain arrays
39
+ * so the result structured-clones straight into the writer below.
40
+ */
41
+ export function tablebaseTask(spec, lower) {
42
+ const extras = spec.white.map((kind) => ({ queen: 4, rook: 3, bishop: 2, knight: 1 })[kind]).sort((a, b) => b - a);
43
+ const kinds = [5, ...extras, 5];
44
+ const colors = [0, ...extras.map(() => 0), 1];
45
+ const count = kinds.length;
46
+ let positions = 1;
47
+ for (let i = 0; i < count; i++) positions *= 64;
48
+ positions *= 2;
49
+
50
+ const indexOf = (squares, side) => {
51
+ let index = squares[0];
52
+ for (let i = 1; i < squares.length; i++) index = index * 64 + squares[i];
53
+ return index * 2 + side;
54
+ };
55
+
56
+ const adjacent = (a, b) => Math.max(Math.abs((a & 7) - (b & 7)), Math.abs((a >> 3) - (b >> 3))) <= 1;
57
+
58
+ const KNIGHT_STEPS = [
59
+ [1, 2],
60
+ [2, 1],
61
+ [2, -1],
62
+ [1, -2],
63
+ [-1, -2],
64
+ [-2, -1],
65
+ [-2, 1],
66
+ [-1, 2],
67
+ ];
68
+ const KING_STEPS = [
69
+ [1, 0],
70
+ [1, 1],
71
+ [0, 1],
72
+ [-1, 1],
73
+ [-1, 0],
74
+ [-1, -1],
75
+ [0, -1],
76
+ [1, -1],
77
+ ];
78
+ const DIAGONALS = [
79
+ [1, 1],
80
+ [-1, 1],
81
+ [-1, -1],
82
+ [1, -1],
83
+ ];
84
+ const STRAIGHTS = [
85
+ [1, 0],
86
+ [0, 1],
87
+ [-1, 0],
88
+ [0, -1],
89
+ ];
90
+
91
+ const squaresOf = (index) => {
92
+ const squares = new Array(count);
93
+ let rest = Math.floor(index / 2);
94
+ for (let i = count - 1; i >= 0; i--) {
95
+ squares[i] = rest % 64;
96
+ rest = Math.floor(rest / 64);
97
+ }
98
+ return { squares, side: index % 2 };
99
+ };
100
+
101
+ const occupiedSet = (squares, skip) => {
102
+ const set = new Set();
103
+ for (let i = 0; i < squares.length; i++) {
104
+ if (i !== skip && squares[i] >= 0) set.add(squares[i]);
105
+ }
106
+ return set;
107
+ };
108
+
109
+ /** side `by` (0 white, 1 black) attacks `square` on this occupancy */
110
+ const attacked = (squares, square, by) => {
111
+ const file = square & 7;
112
+ const rank = square >> 3;
113
+ for (let i = 0; i < count; i++) {
114
+ if (colors[i] !== by || squares[i] < 0) continue;
115
+ const kind = kinds[i];
116
+ const f = squares[i] & 7;
117
+ const r = squares[i] >> 3;
118
+ if (kind === 1) {
119
+ for (const [df, dr] of KNIGHT_STEPS) {
120
+ if (f + df === file && r + dr === rank) return true;
121
+ }
122
+ } else if (kind === 5) {
123
+ if (Math.max(Math.abs(f - file), Math.abs(r - rank)) === 1) return true;
124
+ } else {
125
+ const rays = kind === 2 ? DIAGONALS : kind === 3 ? STRAIGHTS : [...DIAGONALS, ...STRAIGHTS];
126
+ for (const [df, dr] of rays) {
127
+ let a = f + df;
128
+ let b = r + dr;
129
+ while (a >= 0 && a < 8 && b >= 0 && b < 8) {
130
+ if (a === file && b === rank) return true;
131
+ let blocked = false;
132
+ for (let j = 0; j < count; j++) {
133
+ if ((squares[j] & 7) === a && squares[j] >> 3 === b) {
134
+ blocked = true;
135
+ break;
136
+ }
137
+ }
138
+ if (blocked) break;
139
+ a += df;
140
+ b += dr;
141
+ }
142
+ }
143
+ }
144
+ }
145
+ return false;
146
+ };
147
+
148
+ const kingOf = (squares, side) => squares[side === 0 ? 0 : count - 1];
149
+
150
+ /** every move for `side`: `{ from: pieceIndex, to, capture: capturedIndex | -1 }` */
151
+ const genMoves = (squares, side) => {
152
+ const occupied = occupiedSet(squares, -1);
153
+ const out = [];
154
+ for (let i = 0; i < count; i++) {
155
+ if (colors[i] !== side) continue;
156
+ const kind = kinds[i];
157
+ const f = squares[i] & 7;
158
+ const r = squares[i] >> 3;
159
+ const slide = (steps) => {
160
+ for (const [df, dr] of steps) {
161
+ let a = f + df;
162
+ let b = r + dr;
163
+ while (a >= 0 && a < 8 && b >= 0 && b < 8) {
164
+ const to = b * 8 + a;
165
+ if (!occupied.has(to)) out.push({ from: i, to, capture: -1 });
166
+ else {
167
+ for (let j = 0; j < count; j++) {
168
+ if (squares[j] === to && colors[j] !== side && kinds[j] !== 5) {
169
+ out.push({ from: i, to, capture: j });
170
+ break;
171
+ }
172
+ }
173
+ break;
174
+ }
175
+ a += df;
176
+ b += dr;
177
+ }
178
+ }
179
+ };
180
+ if (kind === 1) {
181
+ for (const [df, dr] of KNIGHT_STEPS) {
182
+ const a = f + df;
183
+ const b = r + dr;
184
+ if (a < 0 || a > 7 || b < 0 || b > 7) continue;
185
+ const to = b * 8 + a;
186
+ const capture = captureAt(squares, side, to);
187
+ if (capture === -1 && occupied.has(to)) continue;
188
+ out.push({ from: i, to, capture });
189
+ }
190
+ } else if (kind === 5) {
191
+ for (const [df, dr] of KING_STEPS) {
192
+ const a = f + df;
193
+ const b = r + dr;
194
+ if (a < 0 || a > 7 || b < 0 || b > 7) continue;
195
+ const to = b * 8 + a;
196
+ const capture = captureAt(squares, side, to);
197
+ if (capture === -1 && occupied.has(to)) continue;
198
+ out.push({ from: i, to, capture });
199
+ }
200
+ } else if (kind === 2) slide(DIAGONALS);
201
+ else if (kind === 3) slide(STRAIGHTS);
202
+ else slide([...DIAGONALS, ...STRAIGHTS]);
203
+ }
204
+ return out;
205
+ };
206
+
207
+ /** captured piece index on `to`, -1 when empty, -2 when it holds a king (never generated) */
208
+ const captureAt = (squares, side, to) => {
209
+ for (let j = 0; j < count; j++) {
210
+ if (squares[j] === to) {
211
+ if (colors[j] === side) return -1;
212
+ return kinds[j] === 5 ? -2 : j;
213
+ }
214
+ }
215
+ return -1;
216
+ };
217
+
218
+ /** legal moves: captures verified like quiets, kings never adjacent afterwards */
219
+ const legalMoves = (squares, side) => {
220
+ const out = [];
221
+ for (const move of genMoves(squares, side)) {
222
+ if (move.capture === -2) continue;
223
+ const next = squares.slice();
224
+ next[move.from] = move.to;
225
+ if (move.capture >= 0) next[move.capture] = -1;
226
+ if (adjacent(next[0], next[count - 1])) continue;
227
+ if (attacked(next, kingOf(next, side), 1 - side)) continue;
228
+ out.push(move);
229
+ }
230
+ return out;
231
+ };
232
+
233
+ /** reduced ending id after piece `gone` is captured, or null for bare kings */
234
+ const reducedId = (gone) => {
235
+ const rest = kinds.filter((kind, i) => i !== 0 && i !== count - 1 && i !== gone && kind !== 5);
236
+ if (!rest.length) return null;
237
+ const letter = { 4: 'Q', 3: 'R', 2: 'B', 1: 'N' };
238
+ return `K${rest
239
+ .slice()
240
+ .sort((a, b) => b - a)
241
+ .map((kind) => letter[kind])
242
+ .join('')}K`;
243
+ };
244
+
245
+ /** index of these squares in the reduced ending's order */
246
+ const reducedIndex = (squares, side, gone) => {
247
+ const order = [0];
248
+ const rest = [];
249
+ for (let i = 1; i < count - 1; i++) {
250
+ if (i !== gone) rest.push(i);
251
+ }
252
+ rest.sort((a, b) => kinds[b] - kinds[a]);
253
+ const seq = [squares[0], ...rest.map((i) => squares[i]), squares[count - 1]];
254
+ let index = seq[0];
255
+ for (let i = 1; i < seq.length; i++) index = index * 64 + seq[i];
256
+ return index * 2 + (1 - side);
257
+ };
258
+
259
+ /** conversion value from the mover's view: 1 loss, 2 draw, 3 win, with distance */
260
+ const convert = (squares, side, gone) => {
261
+ const id = reducedId(gone);
262
+ if (id === null) return { value: 2, dtm: 0 };
263
+ const table = lower?.[id];
264
+ if (!table) throw new Error(`tablebaseTask needs ${id} to convert a capture`);
265
+ const index = reducedIndex(squares, side, gone);
266
+ const opponent = table.wdl[index];
267
+ const dtm = table.dtm[index] + 1;
268
+ if (opponent === 1) return { value: 3, dtm };
269
+ if (opponent === 3) return { value: 1, dtm };
270
+ return { value: 2, dtm: 0 };
271
+ };
272
+
273
+ const wdl = new Uint8Array(positions);
274
+ const dtm = new Uint16Array(positions);
275
+ const remaining = new Uint16Array(positions);
276
+ const queue = [];
277
+
278
+ /** predecessors: quiet un-moves of one enemy piece back onto an empty square */
279
+ const predecessors = (squares, side) => {
280
+ const out = [];
281
+ const enemy = 1 - side;
282
+ for (let i = 0; i < count; i++) {
283
+ if (colors[i] !== enemy) continue;
284
+ const kind = kinds[i];
285
+ const t = squares[i];
286
+ const rays =
287
+ kind === 2 ? DIAGONALS : kind === 3 ? STRAIGHTS : kind === 4 ? [...DIAGONALS, ...STRAIGHTS] : null;
288
+ const consume = (f) => {
289
+ if (squares.includes(f)) return;
290
+ //forward legality lives in the post-move occupancy, which is exactly
291
+ //this position: the destination is safe for a king, and the mover's
292
+ //own king stands safe (moving out of check is legal, so the
293
+ //pre-move square is never tested)
294
+ const ownKing = kind === 5 ? t : kingOf(squares, enemy);
295
+ if (attacked(squares, ownKing, side)) return;
296
+ const prev = squares.slice();
297
+ prev[i] = f;
298
+ if (adjacent(prev[0], prev[count - 1])) return;
299
+ out.push({ squares: prev, side: enemy });
300
+ };
301
+ if (rays) {
302
+ const f = t & 7;
303
+ const r = t >> 3;
304
+ for (const [df, dr] of rays) {
305
+ let a = f + df;
306
+ let b = r + dr;
307
+ while (a >= 0 && a < 8 && b >= 0 && b < 8) {
308
+ const from = b * 8 + a;
309
+ if (squares.includes(from)) break;
310
+ consume(from);
311
+ a += df;
312
+ b += dr;
313
+ }
314
+ }
315
+ } else {
316
+ const steps = kind === 1 ? KNIGHT_STEPS : KING_STEPS;
317
+ const f = t & 7;
318
+ const r = t >> 3;
319
+ for (const [df, dr] of steps) {
320
+ const a = f + df;
321
+ const b = r + dr;
322
+ if (a < 0 || a > 7 || b < 0 || b > 7) continue;
323
+ consume(b * 8 + a);
324
+ }
325
+ }
326
+ }
327
+ return out;
328
+ };
329
+
330
+ for (let index = 0; index < positions; index++) {
331
+ const { squares, side } = squaresOf(index);
332
+ const seen = new Set(squares);
333
+ if (seen.size !== count || adjacent(squares[0], squares[count - 1])) {
334
+ wdl[index] = 2;
335
+ continue;
336
+ }
337
+ const moves = legalMoves(squares, side);
338
+ let quiet = 0;
339
+ let draws = 0;
340
+ let won = false;
341
+ let wonDtm = 0;
342
+ let badDelay = 0;
343
+ for (const move of moves) {
344
+ if (move.capture === -1) {
345
+ quiet++;
346
+ continue;
347
+ }
348
+ const { value, dtm: distance } = convert(squares, side, move.capture);
349
+ if (value === 3) {
350
+ won = true;
351
+ wonDtm = distance;
352
+ break;
353
+ }
354
+ if (value === 2) draws++;
355
+ else if (distance > badDelay) badDelay = distance;
356
+ }
357
+ if (won) {
358
+ wdl[index] = 3;
359
+ dtm[index] = Math.min(65535, wonDtm);
360
+ queue.push(index);
361
+ } else if (moves.length === 0) {
362
+ if (attacked(squares, kingOf(squares, side), 1 - side)) {
363
+ wdl[index] = 1;
364
+ dtm[index] = 0;
365
+ } else {
366
+ wdl[index] = 2;
367
+ }
368
+ queue.push(index);
369
+ } else if (quiet === 0) {
370
+ if (draws > 0) {
371
+ wdl[index] = 2;
372
+ } else {
373
+ wdl[index] = 1;
374
+ dtm[index] = Math.min(65535, badDelay);
375
+ }
376
+ queue.push(index);
377
+ } else {
378
+ remaining[index] = quiet;
379
+ }
380
+ }
381
+
382
+ for (let head = 0; head < queue.length; head++) {
383
+ const index = queue[head];
384
+ const { squares, side } = squaresOf(index);
385
+ const value = wdl[index];
386
+ for (const pred of predecessors(squares, side)) {
387
+ const at = indexOf(pred.squares, pred.side);
388
+ if (wdl[at] !== 0) continue;
389
+ if (value === 1) {
390
+ wdl[at] = 3;
391
+ dtm[at] = Math.min(65535, dtm[index] + 1);
392
+ queue.push(at);
393
+ } else if (value === 3) {
394
+ remaining[at]--;
395
+ if (remaining[at] === 0) {
396
+ const { squares: ps, side: pside } = squaresOf(at);
397
+ const moves = legalMoves(ps, pside);
398
+ let delay = 0;
399
+ let drawn = false;
400
+ for (const move of moves) {
401
+ if (move.capture === -1) {
402
+ const next = ps.slice();
403
+ next[move.from] = move.to;
404
+ const child = indexOf(next, 1 - pside);
405
+ if (wdl[child] === 3 && dtm[child] + 1 > delay) delay = dtm[child] + 1;
406
+ } else {
407
+ const { value: converted } = convert(ps, pside, move.capture);
408
+ if (converted === 2) drawn = true;
409
+ }
410
+ }
411
+ if (drawn) {
412
+ wdl[at] = 2;
413
+ } else {
414
+ wdl[at] = 1;
415
+ dtm[at] = Math.min(65535, delay);
416
+ }
417
+ queue.push(at);
418
+ }
419
+ }
420
+ }
421
+ }
422
+
423
+ /** anything unreachable stays unknown: no forced result, so a draw */
424
+ for (let index = 0; index < positions; index++) {
425
+ if (wdl[index] === 0) wdl[index] = 2;
426
+ }
427
+ return { wdl: Array.from(wdl), dtm: Array.from(dtm), positions };
428
+ }
429
+
430
+ const SHIPPED = ['KQK', 'KRK', 'KBK', 'KNK', 'KBNK', 'KBBK'];
431
+ const LETTER_KIND = { Q: 'queen', R: 'rook', B: 'bishop', N: 'knight' };
432
+
433
+ export function specOf(id) {
434
+ const kinds = id
435
+ .slice(1, -1)
436
+ .split('')
437
+ .map((letter) => LETTER_KIND[letter]);
438
+ if (!kinds.length || kinds.some((kind) => !kind)) throw new Error(`not a tablebase ending: "${id}"`);
439
+ return { white: kinds };
440
+ }
441
+
442
+ function defaultJobs() {
443
+ try {
444
+ const parallelism = globalThis.process?.getBuiltinModule?.('node:os')?.availableParallelism?.();
445
+ if (typeof parallelism === 'number' && Number.isInteger(parallelism) && parallelism > 0) {
446
+ return Math.min(parallelism, 4);
447
+ }
448
+ } catch {
449
+ //generation must never fail for lack of a hint about its own host
450
+ }
451
+ return 2;
452
+ }
453
+
454
+ const isMain = process.argv[1] && import.meta.url.endsWith(basename(process.argv[1]));
455
+
456
+ if (isMain) {
457
+ const args = process.argv.slice(2);
458
+ const outDir = args.find((arg) => !arg.startsWith('--'));
459
+ const jobsArg = args.find((arg) => arg.startsWith('--jobs='));
460
+ const jobs = jobsArg ? Math.max(1, Math.floor(Number(jobsArg.slice(7)))) : defaultJobs();
461
+ if (!Number.isInteger(jobs)) {
462
+ console.error('jobs must be a positive integer, e.g. --jobs=4');
463
+ process.exit(1);
464
+ }
465
+ const wanted = args.filter((arg) => !arg.startsWith('--') && arg !== outDir);
466
+ if (!outDir) {
467
+ console.error('usage: node tools/build-tablebases.mjs <outDir> [ending ...] [--jobs=N]');
468
+ process.exit(1);
469
+ }
470
+ const ids = (wanted.length ? wanted : SHIPPED).map((id) => {
471
+ if (!/^K[QRBN]+K$/.test(id)) throw new Error(`not a tablebase ending: "${id}"`);
472
+ const strength = { Q: 0, R: 1, B: 2, N: 3 };
473
+ const canonical = `K${id
474
+ .slice(1, -1)
475
+ .split('')
476
+ .sort((a, b) => strength[a] - strength[b])
477
+ .join('')}K`;
478
+ if (id !== canonical) throw new Error(`not a canonical ending id (want "${canonical}"): "${id}"`);
479
+ return id;
480
+ });
481
+ //a capture converts into the ending without the taken piece, so requested
482
+ //4-piece endings pull in the 3-piece reductions they convert to
483
+ const withDeps = new Set(ids);
484
+ for (const id of ids) {
485
+ if (id.length !== 4) continue;
486
+ const extras = id.slice(1, -1).split('');
487
+ for (let gone = 0; gone < extras.length; gone++) {
488
+ withDeps.add(`K${extras.filter((_, i) => i !== gone).join('')}K`);
489
+ }
490
+ }
491
+ const three = [...withDeps].filter((id) => id.length === 3);
492
+ const four = [...withDeps].filter((id) => id.length === 4);
493
+ if (ids.some((id) => id.length > 4))
494
+ throw new Error('only 3-to-4-piece endings generate here; 5-piece stays out (see the roadmap)');
495
+ mkdirSync(outDir, { recursive: true });
496
+
497
+ const writeEnding = (id, result, seconds) => {
498
+ const wdl = huffEncode(Uint8Array.from(result.wdl));
499
+ const raw = Uint16Array.from(result.dtm);
500
+ const bytes = new Uint8Array(raw.length * 2);
501
+ for (let i = 0; i < raw.length; i++) {
502
+ bytes[2 * i] = raw[i] & 255;
503
+ bytes[2 * i + 1] = raw[i] >> 8;
504
+ }
505
+ const dtm = huffEncode(bytes);
506
+ const file = { version: 1, id, white: specOf(id).white, positions: result.positions, wdl, dtm };
507
+ writeFileSync(join(outDir, `${id}.json`), `${JSON.stringify(file, null, 2)}\n`);
508
+ console.log(
509
+ ` ${id}: ${result.positions} positions in ${seconds}s -> ${relative(process.cwd(), join(outDir, `${id}.json`))}`,
510
+ );
511
+ };
512
+ //4-piece endings hold hundreds of megabytes of arrays each, so their lanes
513
+ //stay narrow whatever --jobs says; 3-piece endings are megabytes and share wide
514
+ const lanes = async (items, solve, cap = jobs) => {
515
+ let next = 0;
516
+ const width = Math.max(1, Math.min(cap, items.length));
517
+ await Promise.all(
518
+ Array.from({ length: width }, async () => {
519
+ for (;;) {
520
+ const index = next++;
521
+ if (index >= items.length) return;
522
+ await solve(items[index]);
523
+ }
524
+ }),
525
+ );
526
+ };
527
+
528
+ //3-piece endings solve with no smaller table (every capture leaves bare
529
+ //kings); 4-piece tasks then receive them for their conversions
530
+ const lower = {};
531
+ await lanes(three, async (id) => {
532
+ const started = Date.now();
533
+ const result = await spawn(tablebaseTask, [specOf(id), null]);
534
+ lower[id] = { wdl: result.wdl, dtm: result.dtm };
535
+ writeEnding(id, result, ((Date.now() - started) / 1000).toFixed(1));
536
+ });
537
+ await lanes(
538
+ four,
539
+ async (id) => {
540
+ const started = Date.now();
541
+ const result = await spawn(tablebaseTask, [specOf(id), lower]);
542
+ writeEnding(id, result, ((Date.now() - started) / 1000).toFixed(1));
543
+ },
544
+ Math.min(jobs, 2),
545
+ );
546
+ console.log(
547
+ `\ntablebases in ${relative(process.cwd(), outDir)}. Load the endings a game needs with board.loadTablebaseEnding.`,
548
+ );
549
+ }
@@ -0,0 +1,20 @@
1
+ import type { OpeningBook } from '../src/board/openings.ts';
2
+
3
+ /** SAN tokens of one PGN line, without move numbers or results. */
4
+ export function sanTokens(pgn: string): string[];
5
+
6
+ /**
7
+ * Resolves one SAN token against the position into a UCI move (`e2e4`, `e7e8q`).
8
+ * Anything resolving to zero (or several) legal moves throws with the row it came
9
+ * from, because a silent guess would poison every position past it.
10
+ */
11
+ export function sanToUci(state: import('../src/board/chess.ts').ChessState, san: string, context: string): string;
12
+
13
+ /** A whole PGN line to UCI, played from the starting position. */
14
+ export function lineToUci(pgn: string, context: string): string[];
15
+
16
+ /**
17
+ * Compiles every TSV row in `dir` into a book: each position along each line keeps the
18
+ * continuation there, and the longest line through the position names it.
19
+ */
20
+ export function compileBook(dir: string): OpeningBook;