@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.
- package/CHANGELOG.md +128 -0
- package/dist/audio/Channels.d.ts +19 -0
- package/dist/audio/Channels.d.ts.map +1 -1
- package/dist/audio/Channels.js +33 -4
- package/dist/audio/Channels.js.map +1 -1
- package/dist/audio/LoopTags.d.ts +37 -0
- package/dist/audio/LoopTags.d.ts.map +1 -0
- package/dist/audio/LoopTags.js +116 -0
- package/dist/audio/LoopTags.js.map +1 -0
- package/dist/audio/Midi.d.ts +29 -0
- package/dist/audio/Midi.d.ts.map +1 -1
- package/dist/audio/Midi.js +30 -1
- package/dist/audio/Midi.js.map +1 -1
- package/dist/audio/MidiRender.d.ts +3 -2
- package/dist/audio/MidiRender.d.ts.map +1 -1
- package/dist/audio/MidiRender.js +2 -2
- package/dist/audio/MidiRender.js.map +1 -1
- package/dist/audio/humanize/Groove.d.ts +80 -0
- package/dist/audio/humanize/Groove.d.ts.map +1 -0
- package/dist/audio/humanize/Groove.js +102 -0
- package/dist/audio/humanize/Groove.js.map +1 -0
- package/dist/audio/humanize/GrooveConverter.d.ts +49 -0
- package/dist/audio/humanize/GrooveConverter.d.ts.map +1 -0
- package/dist/audio/humanize/GrooveConverter.js +344 -0
- package/dist/audio/humanize/GrooveConverter.js.map +1 -0
- package/dist/audio/humanize/GrooveExtractor.d.ts +49 -0
- package/dist/audio/humanize/GrooveExtractor.d.ts.map +1 -0
- package/dist/audio/humanize/GrooveExtractor.js +179 -0
- package/dist/audio/humanize/GrooveExtractor.js.map +1 -0
- package/dist/audio/humanize/GrooveHumanizer.d.ts +34 -0
- package/dist/audio/humanize/GrooveHumanizer.d.ts.map +1 -0
- package/dist/audio/humanize/GrooveHumanizer.js +37 -0
- package/dist/audio/humanize/GrooveHumanizer.js.map +1 -0
- package/dist/audio/humanize/Humanize.d.ts +54 -0
- package/dist/audio/humanize/Humanize.d.ts.map +1 -0
- package/dist/audio/humanize/Humanize.js +239 -0
- package/dist/audio/humanize/Humanize.js.map +1 -0
- package/dist/audio/humanize/HumanizerFactory.d.ts +68 -0
- package/dist/audio/humanize/HumanizerFactory.d.ts.map +1 -0
- package/dist/audio/humanize/HumanizerFactory.js +84 -0
- package/dist/audio/humanize/HumanizerFactory.js.map +1 -0
- package/dist/audio/humanize/LiteHumanizer.d.ts +30 -0
- package/dist/audio/humanize/LiteHumanizer.d.ts.map +1 -0
- package/dist/audio/humanize/LiteHumanizer.js +35 -0
- package/dist/audio/humanize/LiteHumanizer.js.map +1 -0
- package/dist/audio/humanize/Onsets.d.ts +26 -0
- package/dist/audio/humanize/Onsets.d.ts.map +1 -0
- package/dist/audio/humanize/Onsets.js +164 -0
- package/dist/audio/humanize/Onsets.js.map +1 -0
- package/dist/audio/index.d.ts +14 -1
- package/dist/audio/index.d.ts.map +1 -1
- package/dist/audio/index.js +7 -0
- package/dist/audio/index.js.map +1 -1
- package/dist/board/bitboard.d.ts +132 -0
- package/dist/board/bitboard.d.ts.map +1 -0
- package/dist/board/bitboard.js +495 -0
- package/dist/board/bitboard.js.map +1 -0
- package/dist/board/chess.d.ts +16 -0
- package/dist/board/chess.d.ts.map +1 -1
- package/dist/board/chess.js +43 -0
- package/dist/board/chess.js.map +1 -1
- package/dist/board/index.d.ts +9 -1
- package/dist/board/index.d.ts.map +1 -1
- package/dist/board/index.js +5 -1
- package/dist/board/index.js.map +1 -1
- package/dist/board/openings.d.ts +58 -0
- package/dist/board/openings.d.ts.map +1 -0
- package/dist/board/openings.js +45 -0
- package/dist/board/openings.js.map +1 -0
- package/dist/board/tablebase.d.ts +118 -0
- package/dist/board/tablebase.d.ts.map +1 -0
- package/dist/board/tablebase.js +414 -0
- package/dist/board/tablebase.js.map +1 -0
- package/dist/board/tourney.d.ts +90 -0
- package/dist/board/tourney.d.ts.map +1 -0
- package/dist/board/tourney.js +1013 -0
- package/dist/board/tourney.js.map +1 -0
- package/dist/core/JavaRandom.d.ts +55 -0
- package/dist/core/JavaRandom.d.ts.map +1 -0
- package/dist/core/JavaRandom.js +81 -0
- package/dist/core/JavaRandom.js.map +1 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/mw_games.global.js +123 -116
- package/dist/rpg/EventRunner.d.ts +7 -0
- package/dist/rpg/EventRunner.d.ts.map +1 -1
- package/dist/rpg/EventRunner.js +6 -0
- package/dist/rpg/EventRunner.js.map +1 -1
- package/dist/rpg/MoveRoute.d.ts +35 -4
- package/dist/rpg/MoveRoute.d.ts.map +1 -1
- package/dist/rpg/MoveRoute.js +40 -11
- package/dist/rpg/MoveRoute.js.map +1 -1
- package/dist/two-d/render/FogLayer.d.ts +68 -0
- package/dist/two-d/render/FogLayer.d.ts.map +1 -0
- package/dist/two-d/render/FogLayer.js +83 -0
- package/dist/two-d/render/FogLayer.js.map +1 -0
- package/dist/two-d/render/LiquidLayer.d.ts +62 -0
- package/dist/two-d/render/LiquidLayer.d.ts.map +1 -0
- package/dist/two-d/render/LiquidLayer.js +102 -0
- package/dist/two-d/render/LiquidLayer.js.map +1 -0
- package/dist/two-d/render/RpgmAutotile.d.ts +42 -0
- package/dist/two-d/render/RpgmAutotile.d.ts.map +1 -1
- package/dist/two-d/render/RpgmAutotile.js +71 -4
- package/dist/two-d/render/RpgmAutotile.js.map +1 -1
- package/dist/two-d/render/TileMap.d.ts +50 -2
- package/dist/two-d/render/TileMap.d.ts.map +1 -1
- package/dist/two-d/render/TileMap.js +120 -2
- package/dist/two-d/render/TileMap.js.map +1 -1
- package/dist/two-d/render/index.d.ts +7 -3
- package/dist/two-d/render/index.d.ts.map +1 -1
- package/dist/two-d/render/index.js +3 -1
- package/dist/two-d/render/index.js.map +1 -1
- package/dist/two-d/ui/Label.d.ts.map +1 -1
- package/dist/two-d/ui/Label.js +11 -2
- package/dist/two-d/ui/Label.js.map +1 -1
- package/dist/two-d/ui/ListView.d.ts +24 -0
- package/dist/two-d/ui/ListView.d.ts.map +1 -1
- package/dist/two-d/ui/ListView.js +97 -4
- package/dist/two-d/ui/ListView.js.map +1 -1
- package/dist/two-d/ui/MessageLog.d.ts +62 -0
- package/dist/two-d/ui/MessageLog.d.ts.map +1 -0
- package/dist/two-d/ui/MessageLog.js +114 -0
- package/dist/two-d/ui/MessageLog.js.map +1 -0
- package/dist/two-d/ui/RadioGroup.d.ts +68 -0
- package/dist/two-d/ui/RadioGroup.d.ts.map +1 -0
- package/dist/two-d/ui/RadioGroup.js +177 -0
- package/dist/two-d/ui/RadioGroup.js.map +1 -0
- package/dist/two-d/ui/TextModel.d.ts +26 -1
- package/dist/two-d/ui/TextModel.d.ts.map +1 -1
- package/dist/two-d/ui/TextModel.js +56 -7
- package/dist/two-d/ui/TextModel.js.map +1 -1
- package/dist/two-d/ui/TextSharpness.d.ts +33 -0
- package/dist/two-d/ui/TextSharpness.d.ts.map +1 -0
- package/dist/two-d/ui/TextSharpness.js +56 -0
- package/dist/two-d/ui/TextSharpness.js.map +1 -0
- package/dist/two-d/ui/index.d.ts +5 -0
- package/dist/two-d/ui/index.d.ts.map +1 -1
- package/dist/two-d/ui/index.js +3 -0
- package/dist/two-d/ui/index.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +19 -7
- package/sbom.cdx.json +194 -32
- package/tools/build-tablebases.d.mts +22 -0
- package/tools/build-tablebases.mjs +549 -0
- package/tools/compile-openings.d.mts +20 -0
- package/tools/compile-openings.mjs +130 -0
- package/tools/play-uci.d.mts +26 -0
- package/tools/play-uci.mjs +194 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, writeFileSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { basename, join, relative } from 'node:path';
|
|
4
|
+
import { applyMove, legalMoves, positionKey, sq, squareName, startingChess } from '../src/board/chess.ts';
|
|
5
|
+
import { parseUciMove } from '../src/board/openings.ts';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Compiles opening tables (`eco`, `name`, `pgn` TSV rows, one per line after the header)
|
|
9
|
+
* into a position-keyed book a game checks in beside its own code.
|
|
10
|
+
*
|
|
11
|
+
* Each row's PGN is SAN, resolved against the real move generator and replayed from the
|
|
12
|
+
* starting position; every position along the line records the continuation, and the
|
|
13
|
+
* longest line ending at a position names it (first seen wins ties). The output is plain
|
|
14
|
+
* JSON (`{ version: 1, positions: { [key]: { eco?, name?, moves: [uci...] } } }`), keyed
|
|
15
|
+
* by `positionKey`, which is what `board.probeBook` reads back.
|
|
16
|
+
*
|
|
17
|
+
* Only Node built-ins plus the framework's own board rules are used, so this adds no
|
|
18
|
+
* dependency. Rows are vendored data (`data/openings/`, CC0), never fetched: re-vendoring
|
|
19
|
+
* is copying five files at a new commit, and a row that no longer parses fails the
|
|
20
|
+
* compile instead of silently dropping a line.
|
|
21
|
+
*
|
|
22
|
+
* usage: node tools/compile-openings.mjs <tsv folder> <book.json>
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const PROMOTION_KIND = { q: 'queen', r: 'rook', b: 'bishop', n: 'knight' };
|
|
26
|
+
const PROMOTION_LETTER = { queen: 'q', rook: 'r', bishop: 'b', knight: 'n' };
|
|
27
|
+
|
|
28
|
+
/** SAN tokens of one PGN line, without move numbers or results */
|
|
29
|
+
export function sanTokens(pgn) {
|
|
30
|
+
return pgn
|
|
31
|
+
.split(/\s+/)
|
|
32
|
+
.map((token) => token.replace(/^\d+\.+/, ''))
|
|
33
|
+
.filter((token) => token && !/^(1-0|0-1|1\/2-1\/2|\*)$/.test(token));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolves one SAN token against the position into a UCI move (`e2e4`, `e7e8q`).
|
|
38
|
+
* Castling, disambiguation, captures, promotions and trailing check/mate marks are
|
|
39
|
+
* understood; anything resolving to zero (or several) legal moves throws with the row
|
|
40
|
+
* it came from, because a silent guess would poison every position past it.
|
|
41
|
+
*/
|
|
42
|
+
export function sanToUci(state, san, context) {
|
|
43
|
+
const move = san.replace(/[+#?!]+$/, '');
|
|
44
|
+
if (/^O-O(-O)?$/.test(move)) {
|
|
45
|
+
const home = state.turn === 'white' ? 0 : 56;
|
|
46
|
+
return squareName(home + 4) + squareName(home + (move === 'O-O' ? 6 : 2));
|
|
47
|
+
}
|
|
48
|
+
const match = /^([KQRBN]?)([a-h]?[1-8]?)(x?)([a-h][1-8])(?:=([QRBN]))?$/.exec(move);
|
|
49
|
+
if (!match) throw new Error(`${context}: cannot parse '${san}'`);
|
|
50
|
+
const kind = match[1] === '' ? 'pawn' : { K: 'king', Q: 'queen', R: 'rook', B: 'bishop', N: 'knight' }[match[1]];
|
|
51
|
+
const to = sq(match[4]);
|
|
52
|
+
const promotion = match[5] ? PROMOTION_KIND[match[5].toLowerCase()] : undefined;
|
|
53
|
+
const candidates = legalMoves(state).filter(
|
|
54
|
+
(candidate) =>
|
|
55
|
+
state.board[candidate.from]?.kind === kind &&
|
|
56
|
+
candidate.to === to &&
|
|
57
|
+
(candidate.promotion ?? null) === (promotion ?? null) &&
|
|
58
|
+
[...match[2]].every((c) => squareName(candidate.from).includes(c)),
|
|
59
|
+
);
|
|
60
|
+
if (candidates.length !== 1) throw new Error(`${context}: '${san}' matches ${candidates.length} moves`);
|
|
61
|
+
const chosen = candidates[0];
|
|
62
|
+
return (
|
|
63
|
+
squareName(chosen.from) + squareName(chosen.to) + (chosen.promotion ? PROMOTION_LETTER[chosen.promotion] : '')
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** a whole PGN line to UCI, played from the starting position */
|
|
68
|
+
export function lineToUci(pgn, context) {
|
|
69
|
+
const state = startingChess();
|
|
70
|
+
const uci = [];
|
|
71
|
+
for (const san of sanTokens(pgn)) {
|
|
72
|
+
const move = sanToUci(state, san, context);
|
|
73
|
+
uci.push(move);
|
|
74
|
+
applyMove(state, parseUciMove(move));
|
|
75
|
+
}
|
|
76
|
+
return uci;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Compiles every TSV row in `dir` into a book: each position along each line keeps the
|
|
81
|
+
* continuation there, and the longest line ending at a position names it. Terminal
|
|
82
|
+
* positions keep entries of their own, so a line that stops early still classifies.
|
|
83
|
+
*/
|
|
84
|
+
export function compileBook(dir) {
|
|
85
|
+
const positions = {};
|
|
86
|
+
for (const file of readdirSync(dir)
|
|
87
|
+
.filter((entry) => entry.endsWith('.tsv'))
|
|
88
|
+
.sort()) {
|
|
89
|
+
const rows = readFileSync(join(dir, file), 'utf8').split('\n');
|
|
90
|
+
rows.slice(1).forEach((row, index) => {
|
|
91
|
+
if (!row.trim()) return;
|
|
92
|
+
const [eco, name, pgn] = row.split('\t');
|
|
93
|
+
const context = `${file}:${index + 2} (${eco} ${name})`;
|
|
94
|
+
if (!eco || !name || !pgn) throw new Error(`${context}: malformed row`);
|
|
95
|
+
const uci = lineToUci(pgn, context);
|
|
96
|
+
const state = startingChess();
|
|
97
|
+
const keys = [positionKey(state)];
|
|
98
|
+
for (const move of uci) {
|
|
99
|
+
applyMove(state, parseUciMove(move));
|
|
100
|
+
keys.push(positionKey(state));
|
|
101
|
+
}
|
|
102
|
+
keys.forEach((key, ply) => {
|
|
103
|
+
let entry = positions[key];
|
|
104
|
+
if (!entry) entry = positions[key] = { length: 0, moves: [] };
|
|
105
|
+
if (ply === uci.length && uci.length > entry.length) {
|
|
106
|
+
entry.eco = eco;
|
|
107
|
+
entry.name = name;
|
|
108
|
+
entry.length = uci.length;
|
|
109
|
+
}
|
|
110
|
+
if (ply < uci.length && !entry.moves.includes(uci[ply])) entry.moves.push(uci[ply]);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
for (const key of Object.keys(positions)) delete positions[key].length;
|
|
115
|
+
return { version: 1, positions };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const isMain = process.argv[1] && import.meta.url.endsWith(basename(process.argv[1]));
|
|
119
|
+
|
|
120
|
+
if (isMain) {
|
|
121
|
+
const [dir, out] = process.argv.slice(2);
|
|
122
|
+
if (!dir || !out) {
|
|
123
|
+
console.error('usage: node tools/compile-openings.mjs <tsv folder> <book.json>');
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
const book = compileBook(dir);
|
|
127
|
+
writeFileSync(out, `${JSON.stringify(book)}\n`);
|
|
128
|
+
const count = Object.keys(book.positions).length;
|
|
129
|
+
console.log(` ${count} positions from ${relative(process.cwd(), dir)} -> ${relative(process.cwd(), out)}`);
|
|
130
|
+
}
|
|
@@ -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
|
+
}
|