@datamoc/mw_games 0.19.0 → 0.22.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 (141) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/dist/ai/index.d.ts +2 -2
  3. package/dist/ai/index.d.ts.map +1 -1
  4. package/dist/ai/index.js +1 -1
  5. package/dist/ai/index.js.map +1 -1
  6. package/dist/ai/search.d.ts +49 -0
  7. package/dist/ai/search.d.ts.map +1 -1
  8. package/dist/ai/search.js +48 -0
  9. package/dist/ai/search.js.map +1 -1
  10. package/dist/audio/Channels.d.ts +19 -0
  11. package/dist/audio/Channels.d.ts.map +1 -1
  12. package/dist/audio/Channels.js +33 -4
  13. package/dist/audio/Channels.js.map +1 -1
  14. package/dist/audio/LoopTags.d.ts +37 -0
  15. package/dist/audio/LoopTags.d.ts.map +1 -0
  16. package/dist/audio/LoopTags.js +116 -0
  17. package/dist/audio/LoopTags.js.map +1 -0
  18. package/dist/audio/Midi.d.ts +5 -0
  19. package/dist/audio/Midi.d.ts.map +1 -1
  20. package/dist/audio/Midi.js +9 -1
  21. package/dist/audio/Midi.js.map +1 -1
  22. package/dist/audio/MidiRender.d.ts +42 -4
  23. package/dist/audio/MidiRender.d.ts.map +1 -1
  24. package/dist/audio/MidiRender.js +286 -199
  25. package/dist/audio/MidiRender.js.map +1 -1
  26. package/dist/audio/index.d.ts +5 -3
  27. package/dist/audio/index.d.ts.map +1 -1
  28. package/dist/audio/index.js +2 -1
  29. package/dist/audio/index.js.map +1 -1
  30. package/dist/board/Engine.d.ts +33 -0
  31. package/dist/board/Engine.d.ts.map +1 -1
  32. package/dist/board/Engine.js +448 -1
  33. package/dist/board/Engine.js.map +1 -1
  34. package/dist/board/bitboard.d.ts +132 -0
  35. package/dist/board/bitboard.d.ts.map +1 -0
  36. package/dist/board/bitboard.js +495 -0
  37. package/dist/board/bitboard.js.map +1 -0
  38. package/dist/board/chess.d.ts +16 -0
  39. package/dist/board/chess.d.ts.map +1 -1
  40. package/dist/board/chess.js +43 -0
  41. package/dist/board/chess.js.map +1 -1
  42. package/dist/board/index.d.ts +11 -3
  43. package/dist/board/index.d.ts.map +1 -1
  44. package/dist/board/index.js +6 -2
  45. package/dist/board/index.js.map +1 -1
  46. package/dist/board/openings.d.ts +58 -0
  47. package/dist/board/openings.d.ts.map +1 -0
  48. package/dist/board/openings.js +45 -0
  49. package/dist/board/openings.js.map +1 -0
  50. package/dist/board/tablebase.d.ts +118 -0
  51. package/dist/board/tablebase.d.ts.map +1 -0
  52. package/dist/board/tablebase.js +414 -0
  53. package/dist/board/tablebase.js.map +1 -0
  54. package/dist/board/tourney.d.ts +90 -0
  55. package/dist/board/tourney.d.ts.map +1 -0
  56. package/dist/board/tourney.js +1013 -0
  57. package/dist/board/tourney.js.map +1 -0
  58. package/dist/core/JavaRandom.d.ts +55 -0
  59. package/dist/core/JavaRandom.d.ts.map +1 -0
  60. package/dist/core/JavaRandom.js +81 -0
  61. package/dist/core/JavaRandom.js.map +1 -0
  62. package/dist/core/index.d.ts +2 -0
  63. package/dist/core/index.d.ts.map +1 -1
  64. package/dist/core/index.js +1 -0
  65. package/dist/core/index.js.map +1 -1
  66. package/dist/mw_games.global.js +123 -116
  67. package/dist/rpg/EventRunner.d.ts +7 -0
  68. package/dist/rpg/EventRunner.d.ts.map +1 -1
  69. package/dist/rpg/EventRunner.js +6 -0
  70. package/dist/rpg/EventRunner.js.map +1 -1
  71. package/dist/threads/index.d.ts +3 -0
  72. package/dist/threads/index.d.ts.map +1 -0
  73. package/dist/threads/index.js +2 -0
  74. package/dist/threads/index.js.map +1 -0
  75. package/dist/threads/spawn.d.ts +41 -0
  76. package/dist/threads/spawn.d.ts.map +1 -0
  77. package/dist/threads/spawn.js +143 -0
  78. package/dist/threads/spawn.js.map +1 -0
  79. package/dist/threads/worker.d.ts +51 -0
  80. package/dist/threads/worker.d.ts.map +1 -0
  81. package/dist/threads/worker.js +84 -0
  82. package/dist/threads/worker.js.map +1 -0
  83. package/dist/two-d/render/FogLayer.d.ts +68 -0
  84. package/dist/two-d/render/FogLayer.d.ts.map +1 -0
  85. package/dist/two-d/render/FogLayer.js +83 -0
  86. package/dist/two-d/render/FogLayer.js.map +1 -0
  87. package/dist/two-d/render/LiquidLayer.d.ts +62 -0
  88. package/dist/two-d/render/LiquidLayer.d.ts.map +1 -0
  89. package/dist/two-d/render/LiquidLayer.js +102 -0
  90. package/dist/two-d/render/LiquidLayer.js.map +1 -0
  91. package/dist/two-d/render/RpgmAutotile.d.ts +5 -0
  92. package/dist/two-d/render/RpgmAutotile.d.ts.map +1 -1
  93. package/dist/two-d/render/RpgmAutotile.js +4 -3
  94. package/dist/two-d/render/RpgmAutotile.js.map +1 -1
  95. package/dist/two-d/render/TileMap.d.ts +14 -2
  96. package/dist/two-d/render/TileMap.d.ts.map +1 -1
  97. package/dist/two-d/render/TileMap.js +32 -2
  98. package/dist/two-d/render/TileMap.js.map +1 -1
  99. package/dist/two-d/render/index.d.ts +4 -0
  100. package/dist/two-d/render/index.d.ts.map +1 -1
  101. package/dist/two-d/render/index.js +2 -0
  102. package/dist/two-d/render/index.js.map +1 -1
  103. package/dist/two-d/ui/Label.d.ts.map +1 -1
  104. package/dist/two-d/ui/Label.js +11 -2
  105. package/dist/two-d/ui/Label.js.map +1 -1
  106. package/dist/two-d/ui/ListView.d.ts +24 -0
  107. package/dist/two-d/ui/ListView.d.ts.map +1 -1
  108. package/dist/two-d/ui/ListView.js +97 -4
  109. package/dist/two-d/ui/ListView.js.map +1 -1
  110. package/dist/two-d/ui/MessageLog.d.ts +62 -0
  111. package/dist/two-d/ui/MessageLog.d.ts.map +1 -0
  112. package/dist/two-d/ui/MessageLog.js +114 -0
  113. package/dist/two-d/ui/MessageLog.js.map +1 -0
  114. package/dist/two-d/ui/RadioGroup.d.ts +68 -0
  115. package/dist/two-d/ui/RadioGroup.d.ts.map +1 -0
  116. package/dist/two-d/ui/RadioGroup.js +177 -0
  117. package/dist/two-d/ui/RadioGroup.js.map +1 -0
  118. package/dist/two-d/ui/TextModel.d.ts +26 -1
  119. package/dist/two-d/ui/TextModel.d.ts.map +1 -1
  120. package/dist/two-d/ui/TextModel.js +56 -7
  121. package/dist/two-d/ui/TextModel.js.map +1 -1
  122. package/dist/two-d/ui/TextSharpness.d.ts +33 -0
  123. package/dist/two-d/ui/TextSharpness.d.ts.map +1 -0
  124. package/dist/two-d/ui/TextSharpness.js +56 -0
  125. package/dist/two-d/ui/TextSharpness.js.map +1 -0
  126. package/dist/two-d/ui/index.d.ts +5 -0
  127. package/dist/two-d/ui/index.d.ts.map +1 -1
  128. package/dist/two-d/ui/index.js +3 -0
  129. package/dist/two-d/ui/index.js.map +1 -1
  130. package/dist/version.d.ts +1 -1
  131. package/dist/version.js +1 -1
  132. package/package.json +19 -2
  133. package/sbom.cdx.json +54 -28
  134. package/tools/build-tablebases.d.mts +22 -0
  135. package/tools/build-tablebases.mjs +549 -0
  136. package/tools/compile-openings.d.mts +20 -0
  137. package/tools/compile-openings.mjs +130 -0
  138. package/tools/compress-dist.d.mts +8 -0
  139. package/tools/compress-dist.mjs +76 -30
  140. package/tools/play-uci.d.mts +26 -0
  141. package/tools/play-uci.mjs +194 -0
@@ -6,6 +6,8 @@ export interface CompressOptions {
6
6
  brotli?: boolean;
7
7
  /** Also write `.xz` siblings where smaller (default false, needs the system `xz`). */
8
8
  xz?: boolean;
9
+ /** Files to compress at once (default `compressJobs()`). */
10
+ jobs?: number;
9
11
  }
10
12
 
11
13
  /**
@@ -25,6 +27,12 @@ export interface CompressRow {
25
27
  /** True when the system `xz` binary (LZMA2) is available for the opt-in archive pass. */
26
28
  export function hasXz(): boolean;
27
29
 
30
+ /**
31
+ * How many files `compressDist` works on at once: `MWG_COMPRESS_JOBS` when it names
32
+ * a positive integer, else the machine's own thread count, else 4.
33
+ */
34
+ export function compressJobs(): number;
35
+
28
36
  /**
29
37
  * Writes compressed siblings for the large files in `dir`, *into that same folder*: the
30
38
  * originals are left untouched, and each `<file>` gains `<file>.gz`, `<file>.br` and, when
@@ -1,6 +1,8 @@
1
1
  import { readFile, writeFile } from 'node:fs/promises';
2
2
  import { relative, extname, basename } from 'node:path';
3
- import { gzipSync, brotliCompressSync, constants } from 'node:zlib';
3
+ import { gzip, brotliCompress, constants } from 'node:zlib';
4
+ import { promisify } from 'node:util';
5
+ import { availableParallelism } from 'node:os';
4
6
  import { spawnSync } from 'node:child_process';
5
7
  import { walk } from './compile-resources.mjs';
6
8
 
@@ -21,6 +23,11 @@ import { walk } from './compile-resources.mjs';
21
23
  * a server can negotiate: `gz` stays the fallback.
22
24
  *
23
25
  * usage: node tools/compress-dist.mjs <dist folder> [--xz] [--no-gzip] [--no-brotli]
26
+ *
27
+ * Files compress in parallel over async `zlib` (the libuv threadpool, not worker
28
+ * threads): one chain per job over a shared queue, defaulting to MWG_COMPRESS_JOBS
29
+ * when it names a positive integer, else the machine's thread count, else 4. Rows
30
+ * land by index, so the report keeps walk order no matter who finishes first.
24
31
  */
25
32
 
26
33
  const TEXT_EXTENSIONS = new Set(['.js', '.html', '.css', '.svg', '.json', '.txt', '.map']);
@@ -41,6 +48,26 @@ const ALREADY_COMPRESSED = new Set([
41
48
  ]);
42
49
  const MIN_BYTES = 1024;
43
50
 
51
+ const gzipAsync = promisify(gzip);
52
+ const brotliAsync = promisify(brotliCompress);
53
+
54
+ /**
55
+ * How many files `compressDist` works on at once: `MWG_COMPRESS_JOBS` when it names
56
+ * a positive integer, else the machine's own thread count, else 4. An env knob rather
57
+ * than a constant so a loaded CI host can turn one build down without editing anything.
58
+ */
59
+ export function compressJobs() {
60
+ const raw = Number(process.env.MWG_COMPRESS_JOBS);
61
+ if (Number.isInteger(raw) && raw > 0) return raw;
62
+ try {
63
+ const parallel = availableParallelism();
64
+ if (Number.isInteger(parallel) && parallel > 0) return parallel;
65
+ } catch {
66
+ //a build tool must never fail for lack of a hint about its own host
67
+ }
68
+ return 4;
69
+ }
70
+
44
71
  /** true when the system `xz` binary (LZMA2) is available for the opt-in archive pass */
45
72
  export function hasXz() {
46
73
  try {
@@ -67,44 +94,63 @@ function compressXz(data) {
67
94
  * @param options.gzip write `.gz` siblings (default true)
68
95
  * @param options.brotli write `.br` siblings (default true)
69
96
  * @param options.xz also write `.xz` siblings where smaller (default false, needs `xz`)
70
- * @returns one row per file considered, with the sizes actually written
97
+ * @param options.jobs files to compress at once (default `compressJobs()`)
98
+ * @returns one row per file considered, with the sizes actually written, in walk order
71
99
  */
72
- export async function compressDist(dir, { gzip = true, brotli = true, xz = false } = {}) {
73
- const rows = [];
74
- const wantXz = xz && hasXz();
100
+ export async function compressDist(dir, { gzip = true, brotli = true, xz = false, jobs = compressJobs() } = {}) {
101
+ const candidates = [];
75
102
  for await (const file of walk(dir)) {
76
103
  const ext = extname(file).toLowerCase();
77
104
  if (!TEXT_EXTENSIONS.has(ext) || ALREADY_COMPRESSED.has(ext)) continue;
78
- const data = await readFile(file);
79
- if (data.length < MIN_BYTES) continue;
80
- const name = relative(dir, file);
81
- const row = { file: name, raw: data.length, gzip: 0, brotli: 0, xz: 0 };
82
- if (gzip) {
83
- const zipped = gzipSync(data, { level: 9 });
84
- if (zipped.length < data.length) {
85
- await writeFile(file + '.gz', zipped);
86
- row.gzip = zipped.length;
105
+ candidates.push(file);
106
+ }
107
+ const wantXz = xz && hasXz();
108
+ const lanes = Math.max(1, Math.min(Math.floor(jobs) || 1, Math.max(candidates.length, 1)));
109
+ const rows = new Array(candidates.length);
110
+ let next = 0;
111
+ await Promise.all(
112
+ Array.from({ length: lanes }, async () => {
113
+ for (;;) {
114
+ const index = next++;
115
+ if (index >= candidates.length) return;
116
+ rows[index] = await compressOne(dir, candidates[index], { gzip, brotli, wantXz });
87
117
  }
118
+ }),
119
+ );
120
+ return rows.filter((row) => row !== undefined);
121
+ }
122
+
123
+ /** gzip and brotli of one file, or undefined when it is too small to bother with */
124
+ async function compressOne(dir, file, { gzip, brotli, wantXz }) {
125
+ const data = await readFile(file);
126
+ if (data.length < MIN_BYTES) return undefined;
127
+ const row = { file: relative(dir, file), raw: data.length, gzip: 0, brotli: 0, xz: 0 };
128
+ if (gzip) {
129
+ const zipped = await gzipAsync(data, { level: 9 });
130
+ if (zipped.length < data.length) {
131
+ await writeFile(file + '.gz', zipped);
132
+ row.gzip = zipped.length;
88
133
  }
89
- if (brotli) {
90
- const squeezed = brotliCompressSync(data, {
91
- params: { [constants.BROTLI_PARAM_QUALITY]: 11 },
92
- });
93
- if (squeezed.length < data.length) {
94
- await writeFile(file + '.br', squeezed);
95
- row.brotli = squeezed.length;
96
- }
134
+ }
135
+ if (brotli) {
136
+ const squeezed = await brotliAsync(data, {
137
+ params: { [constants.BROTLI_PARAM_QUALITY]: 11 },
138
+ });
139
+ if (squeezed.length < data.length) {
140
+ await writeFile(file + '.br', squeezed);
141
+ row.brotli = squeezed.length;
97
142
  }
98
- if (wantXz) {
99
- const archived = compressXz(data);
100
- if (archived && archived.length < data.length) {
101
- await writeFile(file + '.xz', archived);
102
- row.xz = archived.length;
103
- }
143
+ }
144
+ if (wantXz) {
145
+ //the opt-in archive pass stays synchronous: xz is a child process either way, and
146
+ //it sits outside the measured gzip/brotli path this parallelizes
147
+ const archived = compressXz(data);
148
+ if (archived && archived.length < data.length) {
149
+ await writeFile(file + '.xz', archived);
150
+ row.xz = archived.length;
104
151
  }
105
- rows.push(row);
106
152
  }
107
- return rows;
153
+ return row;
108
154
  }
109
155
 
110
156
  const isMain = process.argv[1] && import.meta.url.endsWith(basename(process.argv[1]));
@@ -0,0 +1,26 @@
1
+ import type { ChessMove } from '../src/board/chess.ts';
2
+ import type { OpeningBook } from '../src/board/openings.ts';
3
+ import type { LoadedEnding } from '../src/board/tablebase.ts';
4
+
5
+ export interface UciSessionOptions {
6
+ /** plies when `go` names no depth */
7
+ depth?: number;
8
+ /** compiled opening book, probed before anything thinks */
9
+ book?: OpeningBook | null;
10
+ /** loaded tablebase endings, probed before the search */
11
+ tables?: Record<string, LoadedEnding>;
12
+ }
13
+
14
+ export interface UciSession {
15
+ /** one input line in, zero or more output lines out; never throws */
16
+ input(line: string): string[];
17
+ }
18
+
19
+ /** a move in UCI coordinates (`e2e4`, `e7e8q`). */
20
+ export function uciString(move: ChessMove): string;
21
+
22
+ /**
23
+ * One stateful UCI conversation over the tournament engine: book, then
24
+ * tablebase, then `searchTourney`. Feed it input lines, read back output lines.
25
+ */
26
+ export function createUciSession(options?: UciSessionOptions): UciSession;
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, readdirSync } from 'node:fs';
3
+ import { join, basename, extname } from 'node:path';
4
+ import { createInterface } from 'node:readline';
5
+ import { applyMove, cloneChess, legalMoves, parseFen, squareName, startingChess } from '../src/board/chess.ts';
6
+ import { parseUciMove, probeBook } from '../src/board/openings.ts';
7
+ import { searchTourney } from '../src/board/tourney.ts';
8
+ import { loadTablebaseEnding, probeTablebase } from '../src/board/tablebase.ts';
9
+
10
+ /**
11
+ * A UCI adapter over the tournament engine: `position`/`go` on stdin, `bestmove`
12
+ * on stdout, the way a GUI expects. Move choice runs book first, then tablebase,
13
+ * then `searchTourney`, so each layer only pays when the earlier ones miss.
14
+ *
15
+ * This is a Node tool, never part of the browser bundle: it reads files and
16
+ * stdio, which a `file://` page cannot do. `info score` is centipawns from the
17
+ * side to move's view (documented because GUIs vary); `bestmove 0000` answers
18
+ * a position with no legal moves, per the UCI convention.
19
+ *
20
+ * usage: node tools/play-uci.mjs [--book <book.json>] [--tables <dir>] [--depth N]
21
+ */
22
+
23
+ const PROMOTION_LETTER = { queen: 'q', rook: 'r', bishop: 'b', knight: 'n' };
24
+
25
+ export function uciString(move) {
26
+ return squareName(move.from) + squareName(move.to) + (move.promotion ? PROMOTION_LETTER[move.promotion] : '');
27
+ }
28
+
29
+ /**
30
+ * One stateful conversation: feed it input lines, read back output lines. Pure
31
+ * except for the injected search, book and tables, so tests drive transcripts
32
+ * without spawning anything.
33
+ */
34
+ export function createUciSession(options = {}) {
35
+ const book = options.book ?? null;
36
+ const tables = options.tables ?? {};
37
+ const fallbackDepth = options.depth ?? 4;
38
+ let state = startingChess();
39
+ let halfmove = 0;
40
+
41
+ const findMove = (token) => {
42
+ let parsed;
43
+ try {
44
+ parsed = parseUciMove(token);
45
+ } catch {
46
+ return null;
47
+ }
48
+ return (
49
+ legalMoves(state).find(
50
+ (move) =>
51
+ move.from === parsed.from &&
52
+ move.to === parsed.to &&
53
+ (parsed.promotion === undefined
54
+ ? move.promotion === undefined
55
+ : move.promotion === parsed.promotion),
56
+ ) ?? null
57
+ );
58
+ };
59
+
60
+ const play = (move) => {
61
+ const target = move.to === state.enPassant && state.board[move.from]?.kind === 'pawn';
62
+ const capture = target || state.board[move.to] !== null;
63
+ const pawn = state.board[move.from]?.kind === 'pawn';
64
+ applyMove(state, move);
65
+ halfmove = pawn || capture ? 0 : halfmove + 1;
66
+ };
67
+
68
+ const tablebest = () => {
69
+ const ids = Object.keys(tables);
70
+ if (!ids.length) return null;
71
+ let best = null;
72
+ let complete = true;
73
+ for (const move of legalMoves(state)) {
74
+ const next = cloneChess(state);
75
+ applyMove(next, move);
76
+ const probe = probeTablebase(tables, next, halfmove);
77
+ if (!probe) {
78
+ complete = false;
79
+ continue;
80
+ }
81
+ const score =
82
+ probe.outcome === 'loss' ? 100000 - probe.dtm : probe.outcome === 'draw' ? 0 : -(100000 - probe.dtm);
83
+ if (best === null || score > best.score) best = { move, score };
84
+ }
85
+ //a forced mate beats any unprobed capture line; otherwise every successor
86
+ //must probe, or the search (which sees captures) takes the move instead
87
+ if (best !== null && (best.score > 90000 || complete)) return best;
88
+ return null;
89
+ };
90
+
91
+ const go = (args) => {
92
+ let depth = fallbackDepth;
93
+ let maxNodes;
94
+ let timeMs;
95
+ for (let i = 0; i < args.length; i++) {
96
+ if (args[i] === 'depth' && args[i + 1] !== undefined) depth = Math.max(1, Math.floor(Number(args[i + 1])));
97
+ if (args[i] === 'nodes' && args[i + 1] !== undefined)
98
+ maxNodes = Math.max(1, Math.floor(Number(args[i + 1])));
99
+ if (args[i] === 'movetime' && args[i + 1] !== undefined)
100
+ timeMs = Math.max(1, Math.floor(Number(args[i + 1])));
101
+ }
102
+ if (!Number.isInteger(depth)) depth = fallbackDepth;
103
+ const moves = legalMoves(state);
104
+ if (!moves.length) return ['bestmove 0000'];
105
+ const entry = book ? probeBook(book, state) : null;
106
+ if (entry?.moves.length) return ['info string book', `bestmove ${entry.moves[0]}`];
107
+ const tabled = tablebest();
108
+ if (tabled) return ['info string tablebase', `bestmove ${uciString(tabled.move)}`];
109
+ const result = searchTourney(state, { depth, maxNodes, timeMs });
110
+ if (!result.move) return ['bestmove 0000'];
111
+ return [
112
+ `info depth ${result.depth} score cp ${result.score} nodes ${result.nodes}`,
113
+ `bestmove ${uciString(result.move)}`,
114
+ ];
115
+ };
116
+
117
+ const position = (args) => {
118
+ const movesAt = args.indexOf('moves');
119
+ const head = movesAt === -1 ? args : args.slice(0, movesAt);
120
+ const tail = movesAt === -1 ? [] : args.slice(movesAt + 1);
121
+ if (head[0] === 'startpos') {
122
+ state = startingChess();
123
+ halfmove = 0;
124
+ } else if (head[0] === 'fen') {
125
+ try {
126
+ const fen = head.slice(1).join(' ');
127
+ state = parseFen(fen);
128
+ const parts = fen.split(' ');
129
+ halfmove = parts[4] !== undefined && /^\d+$/.test(parts[4]) ? Number(parts[4]) : 0;
130
+ } catch {
131
+ return [`info string bad fen: ${head.slice(1).join(' ')}`];
132
+ }
133
+ } else {
134
+ return [`info string unknown position: ${head[0] ?? ''}`];
135
+ }
136
+ for (const token of tail) {
137
+ const move = findMove(token);
138
+ if (!move) return [`info string illegal move: ${token}`];
139
+ play(move);
140
+ }
141
+ return [];
142
+ };
143
+
144
+ return {
145
+ input(line) {
146
+ const parts = line
147
+ .trim()
148
+ .split(/\s+/)
149
+ .filter((part) => part.length);
150
+ if (!parts.length) return [];
151
+ const [command, ...args] = parts;
152
+ if (command === 'uci') return ['id name mwg tourney', 'uciok'];
153
+ if (command === 'isready') return ['readyok'];
154
+ if (command === 'ucinewgame') {
155
+ state = startingChess();
156
+ halfmove = 0;
157
+ return [];
158
+ }
159
+ if (command === 'position') return position(args);
160
+ if (command === 'go') return go(args);
161
+ if (command === 'quit') return [];
162
+ return [`info string unknown command: ${command}`];
163
+ },
164
+ };
165
+ }
166
+
167
+ const isMain = process.argv[1] && import.meta.url.endsWith(basename(process.argv[1]));
168
+
169
+ if (isMain) {
170
+ const args = process.argv.slice(2);
171
+ const flag = (name) => {
172
+ const at = args.indexOf(name);
173
+ return at === -1 ? undefined : args[at + 1];
174
+ };
175
+ const bookPath = flag('--book');
176
+ const tablesDir = flag('--tables');
177
+ const depth = flag('--depth') === undefined ? 4 : Math.max(1, Math.floor(Number(flag('--depth'))));
178
+ const book = bookPath ? JSON.parse(readFileSync(bookPath, 'utf8')) : null;
179
+ if (book && book.version !== 1) throw new Error(`${bookPath}: unknown opening-book version`);
180
+ const tables = {};
181
+ if (tablesDir) {
182
+ for (const file of readdirSync(tablesDir)) {
183
+ if (extname(file) !== '.json') continue;
184
+ const ending = loadTablebaseEnding(JSON.parse(readFileSync(join(tablesDir, file), 'utf8')));
185
+ tables[ending.id] = ending;
186
+ }
187
+ }
188
+ const session = createUciSession({ book, tables, depth });
189
+ const lines = createInterface({ input: process.stdin, output: process.stdout, terminal: false });
190
+ for await (const line of lines) {
191
+ if (line.trim() === 'quit') break;
192
+ for (const out of session.input(line)) console.log(out);
193
+ }
194
+ }