react-chess-core 0.1.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/README.md +46 -0
- package/dist/features/analysis/analysisBoardHighlightColors.d.ts +12 -0
- package/dist/features/analysis/analysisUtils.d.ts +4 -0
- package/dist/features/analysis/core/AnalysisBoardCore.d.ts +28 -0
- package/dist/features/analysis/core/AnalysisChessboardView.d.ts +5 -0
- package/dist/features/analysis/core/AnalysisErrorBoundary.d.ts +14 -0
- package/dist/features/analysis/core/AnalysisPosition.d.ts +54 -0
- package/dist/features/analysis/core/analysisLayoutConfig.d.ts +6 -0
- package/dist/features/analysis/core/index.d.ts +7 -0
- package/dist/features/analysis/core/renderProps.d.ts +39 -0
- package/dist/features/analysis/core/useAnalysisBoardModel.d.ts +36 -0
- package/dist/features/analysis/defaults/AnalysisBoard.d.ts +16 -0
- package/dist/features/analysis/defaults/AnalysisBoardLayout.d.ts +14 -0
- package/dist/features/analysis/defaults/DefaultAnalysisContainer.d.ts +4 -0
- package/dist/features/analysis/defaults/DefaultAnalysisSidebar.d.ts +3 -0
- package/dist/features/analysis/defaults/EngineEvaluationPanel.d.ts +8 -0
- package/dist/features/analysis/defaults/analysisLayout.d.ts +3 -0
- package/dist/features/analysis/defaults/analysisModalStyles.d.ts +7 -0
- package/dist/features/analysis/defaults/analysisSidebarColors.d.ts +37 -0
- package/dist/features/analysis/defaults/analysisSidebarRowStyle.d.ts +8 -0
- package/dist/features/analysis/defaults/index.d.ts +9 -0
- package/dist/features/analysis/index.d.ts +5 -0
- package/dist/features/analysis/types.d.ts +9 -0
- package/dist/features/chessboard/HighlightChessboard.d.ts +7 -0
- package/dist/features/chessboard/boardSquareHighlightColors.d.ts +9 -0
- package/dist/features/chessboard/chessboardTheme.d.ts +27 -0
- package/dist/features/chessboard/index.d.ts +3 -0
- package/dist/features/engine/StockfishBrowserEngine.d.ts +32 -0
- package/dist/features/engine/formatEvaluation.d.ts +18 -0
- package/dist/features/engine/index.d.ts +7 -0
- package/dist/features/engine/isAnalyzableFen.d.ts +2 -0
- package/dist/features/engine/parseUciInfo.d.ts +2 -0
- package/dist/features/engine/stockfishUrls.d.ts +8 -0
- package/dist/features/engine/types.d.ts +27 -0
- package/dist/features/engine/useAnalysisEngine.d.ts +2 -0
- package/dist/features/navigation/DefaultPlyNavigation.d.ts +4 -0
- package/dist/features/navigation/PlyNavigation.d.ts +6 -0
- package/dist/features/navigation/index.d.ts +4 -0
- package/dist/features/navigation/plyNavigationStyles.d.ts +12 -0
- package/dist/features/navigation/types.d.ts +36 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.esm.js +1495 -0
- package/dist/index.js +1547 -0
- package/dist/stories/Chessboard.stories.d.ts +7 -0
- package/dist/stories/withThemeProvider.d.ts +7 -0
- package/package.json +65 -0
|
@@ -0,0 +1,1495 @@
|
|
|
1
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
|
+
import { createContext, useContext, useState, useRef, useEffect, useLayoutEffect, useMemo, Component } from 'react';
|
|
3
|
+
import { Chessboard, ChessboardDnDProvider } from 'react-chessboard';
|
|
4
|
+
import { Chess } from 'chess.js';
|
|
5
|
+
import { createPortal } from 'react-dom';
|
|
6
|
+
|
|
7
|
+
const ChessboardThemeContext = createContext(undefined);
|
|
8
|
+
const useChessboardTheme = () => {
|
|
9
|
+
const context = useContext(ChessboardThemeContext);
|
|
10
|
+
if (!context) {
|
|
11
|
+
throw new Error('useChessboardTheme must be used within a ThemeProvider');
|
|
12
|
+
}
|
|
13
|
+
return context;
|
|
14
|
+
};
|
|
15
|
+
/** @deprecated Use {@link useChessboardTheme}. */
|
|
16
|
+
const useTheme = useChessboardTheme;
|
|
17
|
+
const getStylesForTheme = (theme) => {
|
|
18
|
+
if (theme === 'dark') {
|
|
19
|
+
return {
|
|
20
|
+
customDarkSquareStyle: { backgroundColor: '#838387' },
|
|
21
|
+
customLightSquareStyle: { backgroundColor: '#e1e1e3' },
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
customDarkSquareStyle: { backgroundColor: '#b58863' },
|
|
26
|
+
customLightSquareStyle: { backgroundColor: '#f0d9b5' },
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
const ThemeProvider = ({ children, theme }) => {
|
|
30
|
+
const { customDarkSquareStyle, customLightSquareStyle } = getStylesForTheme(theme);
|
|
31
|
+
return (jsx(ChessboardThemeContext.Provider, { value: { customDarkSquareStyle, customLightSquareStyle }, children: children }));
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/******************************************************************************
|
|
35
|
+
Copyright (c) Microsoft Corporation.
|
|
36
|
+
|
|
37
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
38
|
+
purpose with or without fee is hereby granted.
|
|
39
|
+
|
|
40
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
41
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
42
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
43
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
44
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
45
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
46
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
47
|
+
***************************************************************************** */
|
|
48
|
+
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
function __rest(s, e) {
|
|
52
|
+
var t = {};
|
|
53
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
54
|
+
t[p] = s[p];
|
|
55
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
56
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
57
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
58
|
+
t[p[i]] = s[p[i]];
|
|
59
|
+
}
|
|
60
|
+
return t;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function __awaiter(thisArg, _arguments, P, generator) {
|
|
64
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
65
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
66
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
67
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
68
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
69
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
74
|
+
var e = new Error(message);
|
|
75
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Square overlay colors for puzzle boards (check, hint, incorrect).
|
|
80
|
+
*/
|
|
81
|
+
const boardSquareHighlightColors = {
|
|
82
|
+
check: 'rgba(255, 127, 127, 0.8)',
|
|
83
|
+
hint: 'rgba(119, 177, 212, 0.75)',
|
|
84
|
+
/** Muted red — softer than the in-check highlight. */
|
|
85
|
+
incorrect: 'rgba(140, 38, 38, 0.82)',
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const getCheckHighlighting = (checkSquare) => {
|
|
89
|
+
const styles = {};
|
|
90
|
+
styles[checkSquare] = { backgroundColor: boardSquareHighlightColors.check };
|
|
91
|
+
return styles;
|
|
92
|
+
};
|
|
93
|
+
const getFeedbackHighlighting = (hintSquare, incorrectMoveSquare) => {
|
|
94
|
+
const styles = {};
|
|
95
|
+
if (hintSquare) {
|
|
96
|
+
styles[hintSquare] = { backgroundColor: boardSquareHighlightColors.hint };
|
|
97
|
+
}
|
|
98
|
+
if (incorrectMoveSquare) {
|
|
99
|
+
styles[incorrectMoveSquare] = {
|
|
100
|
+
backgroundColor: boardSquareHighlightColors.incorrect,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return styles;
|
|
104
|
+
};
|
|
105
|
+
const HighlightChessboard = (_a) => {
|
|
106
|
+
var { checkSquare, hintSquare, incorrectMoveSquare, customSquareStyles: extraSquareStyles } = _a, props = __rest(_a, ["checkSquare", "hintSquare", "incorrectMoveSquare", "customSquareStyles"]);
|
|
107
|
+
const { customDarkSquareStyle, customLightSquareStyle } = useChessboardTheme();
|
|
108
|
+
const checkStyles = getCheckHighlighting(checkSquare);
|
|
109
|
+
const feedbackStyles = getFeedbackHighlighting(hintSquare, incorrectMoveSquare);
|
|
110
|
+
const customSquareStyles = Object.assign(Object.assign(Object.assign({}, checkStyles), feedbackStyles), extraSquareStyles);
|
|
111
|
+
return (jsx(Chessboard, Object.assign({ customDarkSquareStyle: customDarkSquareStyle, customLightSquareStyle: customLightSquareStyle, customSquareStyles: customSquareStyles }, props)));
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const emptyEngineEvaluation = () => ({
|
|
115
|
+
status: 'idle',
|
|
116
|
+
depth: 0,
|
|
117
|
+
lines: [],
|
|
118
|
+
});
|
|
119
|
+
const DEFAULT_STOCKFISH_SCRIPT_URL = '/stockfish/stockfish-18-lite-single.js';
|
|
120
|
+
|
|
121
|
+
/** Resolve a public asset path against the current page (honors CRA `PUBLIC_URL`). */
|
|
122
|
+
const resolveStockfishScriptUrl = (scriptUrl, baseHref = typeof window !== 'undefined' ? window.location.href : '') => new URL(scriptUrl, baseHref).href;
|
|
123
|
+
const resolveStockfishWasmUrl = (scriptUrl, baseHref) => resolveStockfishScriptUrl(scriptUrl, baseHref).replace(/\.js(\?.*)?$/i, '.wasm$1');
|
|
124
|
+
/**
|
|
125
|
+
* Worker URL for stockfish.js (see examples/loadEngine.js in the stockfish package).
|
|
126
|
+
* Stockfish derives the sibling `.wasm` URL from the worker script pathname.
|
|
127
|
+
*/
|
|
128
|
+
const resolveStockfishWorkerUrl = (scriptUrl, baseHref) => resolveStockfishScriptUrl(scriptUrl, baseHref);
|
|
129
|
+
|
|
130
|
+
/** Parse a UCI token (e2e4, e7e8q) into a chess.js move object. */
|
|
131
|
+
const parseUciMove = (uci) => {
|
|
132
|
+
const token = uci.trim().toLowerCase();
|
|
133
|
+
if (token.length < 4) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const from = token.slice(0, 2);
|
|
137
|
+
const to = token.slice(2, 4);
|
|
138
|
+
const promotion = token.length > 4 ? token[4] : undefined;
|
|
139
|
+
return promotion ? { from, to, promotion } : { from, to };
|
|
140
|
+
};
|
|
141
|
+
/** Coerce engine PV data to UCI move tokens (handles array or space-separated string). */
|
|
142
|
+
const normalizePvMoves = (pv) => {
|
|
143
|
+
if (Array.isArray(pv)) {
|
|
144
|
+
return pv.filter((move) => typeof move === 'string' && move.length > 0);
|
|
145
|
+
}
|
|
146
|
+
if (typeof pv === 'string' && pv.trim().length > 0) {
|
|
147
|
+
return pv.trim().split(/\s+/);
|
|
148
|
+
}
|
|
149
|
+
return [];
|
|
150
|
+
};
|
|
151
|
+
/** Apply UCI moves from a FEN and return SAN for each legal move in order. */
|
|
152
|
+
const uciPvToSan = (fen, pv) => {
|
|
153
|
+
const moves = normalizePvMoves(pv);
|
|
154
|
+
const chess = new Chess(fen);
|
|
155
|
+
const sans = [];
|
|
156
|
+
for (const uci of moves) {
|
|
157
|
+
const uciMove = parseUciMove(uci);
|
|
158
|
+
if (!uciMove) {
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
const move = chess.move(uciMove);
|
|
163
|
+
sans.push(move.san);
|
|
164
|
+
}
|
|
165
|
+
catch (_a) {
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return sans;
|
|
170
|
+
};
|
|
171
|
+
/** Normalize UCI eval (side to move) to White's perspective. */
|
|
172
|
+
const normalizeEvalForWhite = (fen, centipawns, mate) => {
|
|
173
|
+
const turn = fen.split(' ')[1];
|
|
174
|
+
if (turn === 'w') {
|
|
175
|
+
return { centipawns, mate };
|
|
176
|
+
}
|
|
177
|
+
if (centipawns !== null) {
|
|
178
|
+
return { centipawns: -centipawns, mate: null };
|
|
179
|
+
}
|
|
180
|
+
if (mate !== null) {
|
|
181
|
+
return { centipawns: null, mate: -mate };
|
|
182
|
+
}
|
|
183
|
+
return { centipawns: null, mate: null };
|
|
184
|
+
};
|
|
185
|
+
const formatEvaluation = (centipawns, mate) => {
|
|
186
|
+
if (mate !== null) {
|
|
187
|
+
return mate > 0 ? `#+${mate}` : `#${mate}`;
|
|
188
|
+
}
|
|
189
|
+
if (centipawns === null) {
|
|
190
|
+
return '—';
|
|
191
|
+
}
|
|
192
|
+
const pawns = centipawns / 100;
|
|
193
|
+
const sign = pawns > 0 ? '+' : '';
|
|
194
|
+
return `${sign}${pawns.toFixed(2)}`;
|
|
195
|
+
};
|
|
196
|
+
/** Principal variation as space-separated SAN (from the given FEN). */
|
|
197
|
+
const formatPvPreview = (fen, pv, maxMoves = 6) => {
|
|
198
|
+
const moves = normalizePvMoves(pv);
|
|
199
|
+
if (moves.length === 0) {
|
|
200
|
+
return '';
|
|
201
|
+
}
|
|
202
|
+
const limit = typeof maxMoves === 'number' && Number.isFinite(maxMoves) ? maxMoves : 6;
|
|
203
|
+
return uciPvToSan(fen, moves.slice(0, limit)).join(' ');
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const parseUciInfoLine = (line) => {
|
|
207
|
+
if (!line.startsWith('info ')) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
const tokens = line.split(' ');
|
|
211
|
+
let depth = 0;
|
|
212
|
+
let multipv = 1;
|
|
213
|
+
let centipawns = null;
|
|
214
|
+
let mate = null;
|
|
215
|
+
let pv = [];
|
|
216
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
217
|
+
const token = tokens[i];
|
|
218
|
+
if (token === 'depth') {
|
|
219
|
+
depth = Number.parseInt(tokens[++i], 10);
|
|
220
|
+
}
|
|
221
|
+
else if (token === 'multipv') {
|
|
222
|
+
multipv = Number.parseInt(tokens[++i], 10);
|
|
223
|
+
}
|
|
224
|
+
else if (token === 'score') {
|
|
225
|
+
if (tokens[i + 1] === 'cp') {
|
|
226
|
+
centipawns = Number.parseInt(tokens[i + 2], 10);
|
|
227
|
+
}
|
|
228
|
+
else if (tokens[i + 1] === 'mate') {
|
|
229
|
+
mate = Number.parseInt(tokens[i + 2], 10);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
else if (token === 'pv') {
|
|
233
|
+
pv = tokens.slice(i + 1);
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (depth === 0 && centipawns === null && mate === null && pv.length === 0) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
multipv,
|
|
242
|
+
depth,
|
|
243
|
+
centipawns,
|
|
244
|
+
mate,
|
|
245
|
+
pv,
|
|
246
|
+
};
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
/** Stockfish can trap on finished positions; skip analysis when the game is over. */
|
|
250
|
+
const isAnalyzableFen = (fen) => {
|
|
251
|
+
if (!fen.trim()) {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
const chess = new Chess(fen);
|
|
256
|
+
return !chess.isGameOver();
|
|
257
|
+
}
|
|
258
|
+
catch (_a) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const INIT_TIMEOUT_MS = 45000;
|
|
264
|
+
const splitWorkerLines = (data) => String(data)
|
|
265
|
+
.split(/\r?\n/)
|
|
266
|
+
.map((line) => line.trim())
|
|
267
|
+
.filter(Boolean);
|
|
268
|
+
const firstToken = (line) => { var _a; return (_a = line.split(/\s+/)[0]) !== null && _a !== void 0 ? _a : ''; };
|
|
269
|
+
class StockfishBrowserEngine {
|
|
270
|
+
constructor(scriptUrl) {
|
|
271
|
+
this.scriptUrl = scriptUrl;
|
|
272
|
+
this.worker = null;
|
|
273
|
+
this.ready = false;
|
|
274
|
+
this.disposed = false;
|
|
275
|
+
this.lifecycleGeneration = 0;
|
|
276
|
+
this.evaluation = emptyEngineEvaluation();
|
|
277
|
+
this.lineMap = new Map();
|
|
278
|
+
this.listeners = new Set();
|
|
279
|
+
this.analysisFen = '';
|
|
280
|
+
this.analysisGeneration = 0;
|
|
281
|
+
this.pendingSearch = null;
|
|
282
|
+
this.dispatchedSearchGeneration = 0;
|
|
283
|
+
this.searching = false;
|
|
284
|
+
}
|
|
285
|
+
subscribe(listener) {
|
|
286
|
+
this.listeners.add(listener);
|
|
287
|
+
listener(this.evaluation);
|
|
288
|
+
return () => this.listeners.delete(listener);
|
|
289
|
+
}
|
|
290
|
+
getEvaluation() {
|
|
291
|
+
return this.evaluation;
|
|
292
|
+
}
|
|
293
|
+
assertWasmReachable() {
|
|
294
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
295
|
+
var _a, _b;
|
|
296
|
+
if (typeof window === 'undefined') {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const wasmUrl = resolveStockfishWasmUrl(this.scriptUrl);
|
|
300
|
+
let response;
|
|
301
|
+
try {
|
|
302
|
+
response = yield fetch(wasmUrl, { method: 'HEAD' });
|
|
303
|
+
if (response.status === 405 || response.status === 501) {
|
|
304
|
+
response = yield fetch(wasmUrl, { method: 'GET', headers: { Range: 'bytes=0-0' } });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
catch (_c) {
|
|
308
|
+
throw new Error(`Could not fetch Stockfish WASM at ${wasmUrl}. Run copy:stockfish and restart the dev server.`);
|
|
309
|
+
}
|
|
310
|
+
if (!response.ok) {
|
|
311
|
+
throw new Error(`Stockfish WASM missing at ${wasmUrl} (HTTP ${response.status}). Run: npm run copy:stockfish`);
|
|
312
|
+
}
|
|
313
|
+
const contentType = (_b = (_a = response.headers.get('content-type')) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== null && _b !== void 0 ? _b : '';
|
|
314
|
+
if (contentType.includes('text/html')) {
|
|
315
|
+
throw new Error(`Stockfish WASM at ${wasmUrl} returned HTML (dev-server fallback). Run: npm run copy:stockfish and restart the dev server.`);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
init() {
|
|
320
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
321
|
+
var _a;
|
|
322
|
+
if (typeof Worker === 'undefined') {
|
|
323
|
+
throw new Error('Web Workers are not available in this environment');
|
|
324
|
+
}
|
|
325
|
+
const generation = ++this.lifecycleGeneration;
|
|
326
|
+
this.disposed = false;
|
|
327
|
+
this.setEvaluation(Object.assign(Object.assign({}, emptyEngineEvaluation()), { status: 'loading' }));
|
|
328
|
+
yield this.assertWasmReachable();
|
|
329
|
+
if (this.disposed || generation !== this.lifecycleGeneration) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const workerUrl = resolveStockfishWorkerUrl(this.scriptUrl);
|
|
333
|
+
let worker;
|
|
334
|
+
try {
|
|
335
|
+
worker = new Worker(workerUrl, { type: 'classic' });
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
const message = error instanceof Error ? error.message : 'Failed to create Stockfish worker';
|
|
339
|
+
throw new Error(`${message}. Browser extensions (e.g. MetaMask) can block Web Workers on some pages.`);
|
|
340
|
+
}
|
|
341
|
+
this.worker = worker;
|
|
342
|
+
this.worker.onerror = (event) => {
|
|
343
|
+
var _a;
|
|
344
|
+
const detail = ((_a = event.message) === null || _a === void 0 ? void 0 : _a.trim()) ||
|
|
345
|
+
'Worker crashed while loading or analyzing (see browser console).';
|
|
346
|
+
this.handleWorkerFailure(`Stockfish worker failed (${this.scriptUrl}): ${detail} If WASM is missing, run copy:stockfish.`);
|
|
347
|
+
};
|
|
348
|
+
this.worker.onmessageerror = () => {
|
|
349
|
+
this.handleWorkerFailure(`Stockfish worker message error (${this.scriptUrl}).`);
|
|
350
|
+
};
|
|
351
|
+
try {
|
|
352
|
+
yield this.handshake(workerUrl, generation);
|
|
353
|
+
if (this.disposed || generation !== this.lifecycleGeneration || !this.worker) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
this.worker.onmessage = (event) => {
|
|
357
|
+
for (const line of splitWorkerLines(event.data)) {
|
|
358
|
+
this.handleLine(line);
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
this.ready = true;
|
|
362
|
+
this.setEvaluation(Object.assign(Object.assign({}, emptyEngineEvaluation()), { status: 'idle' }));
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
(_a = this.worker) === null || _a === void 0 ? void 0 : _a.terminate();
|
|
366
|
+
this.worker = null;
|
|
367
|
+
if (!this.disposed && generation === this.lifecycleGeneration) {
|
|
368
|
+
throw error;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
handleWorkerFailure(message) {
|
|
374
|
+
var _a;
|
|
375
|
+
if (this.disposed) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
this.ready = false;
|
|
379
|
+
this.setEvaluation(Object.assign(Object.assign({}, emptyEngineEvaluation()), { status: 'error', error: message }));
|
|
380
|
+
(_a = this.worker) === null || _a === void 0 ? void 0 : _a.terminate();
|
|
381
|
+
this.worker = null;
|
|
382
|
+
}
|
|
383
|
+
handshake(workerUrl, generation) {
|
|
384
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
385
|
+
if (!this.worker) {
|
|
386
|
+
throw new Error('Stockfish worker was not created');
|
|
387
|
+
}
|
|
388
|
+
yield this.waitForLine(this.worker, workerUrl, generation, (line) => firstToken(line) === 'uciok', () => {
|
|
389
|
+
var _a;
|
|
390
|
+
(_a = this.worker) === null || _a === void 0 ? void 0 : _a.postMessage('uci');
|
|
391
|
+
});
|
|
392
|
+
if (this.disposed || generation !== this.lifecycleGeneration || !this.worker) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
this.worker.postMessage('setoption name UCI_AnalyseMode value true');
|
|
396
|
+
yield this.waitForLine(this.worker, workerUrl, generation, (line) => firstToken(line) === 'readyok', () => {
|
|
397
|
+
var _a;
|
|
398
|
+
(_a = this.worker) === null || _a === void 0 ? void 0 : _a.postMessage('isready');
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
waitForLine(worker, workerUrl, generation, match, sendCommand, timeoutMs = INIT_TIMEOUT_MS) {
|
|
403
|
+
return new Promise((resolve, reject) => {
|
|
404
|
+
const cleanup = () => {
|
|
405
|
+
clearTimeout(timeoutId);
|
|
406
|
+
worker.removeEventListener('message', onMessage);
|
|
407
|
+
worker.removeEventListener('error', onError);
|
|
408
|
+
worker.removeEventListener('messageerror', onMessageError);
|
|
409
|
+
};
|
|
410
|
+
const timeoutId = setTimeout(() => {
|
|
411
|
+
cleanup();
|
|
412
|
+
reject(new Error(`Stockfish engine timed out (${workerUrl}). Run copy:stockfish, confirm public/stockfish/*.wasm is served, and check the browser console.`));
|
|
413
|
+
}, timeoutMs);
|
|
414
|
+
const onError = () => {
|
|
415
|
+
cleanup();
|
|
416
|
+
reject(new Error(`Stockfish worker failed while loading ${this.scriptUrl}. Check the browser console and WASM MIME type.`));
|
|
417
|
+
};
|
|
418
|
+
const onMessageError = () => {
|
|
419
|
+
cleanup();
|
|
420
|
+
reject(new Error(`Stockfish worker message error while loading ${this.scriptUrl}.`));
|
|
421
|
+
};
|
|
422
|
+
const onMessage = (event) => {
|
|
423
|
+
if (this.disposed || generation !== this.lifecycleGeneration) {
|
|
424
|
+
cleanup();
|
|
425
|
+
resolve();
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
for (const line of splitWorkerLines(event.data)) {
|
|
429
|
+
if (match(line)) {
|
|
430
|
+
cleanup();
|
|
431
|
+
resolve();
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
worker.addEventListener('message', onMessage);
|
|
437
|
+
worker.addEventListener('error', onError);
|
|
438
|
+
worker.addEventListener('messageerror', onMessageError);
|
|
439
|
+
sendCommand();
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
analyze(fen, depth, multiPv) {
|
|
443
|
+
if (!this.worker || !this.ready || this.disposed) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const generation = ++this.analysisGeneration;
|
|
447
|
+
if (!isAnalyzableFen(fen)) {
|
|
448
|
+
this.pendingSearch = null;
|
|
449
|
+
this.searching = false;
|
|
450
|
+
this.dispatchedSearchGeneration = generation;
|
|
451
|
+
this.analysisFen = fen;
|
|
452
|
+
this.lineMap.clear();
|
|
453
|
+
this.setEvaluation({
|
|
454
|
+
status: 'idle',
|
|
455
|
+
depth: 0,
|
|
456
|
+
lines: [],
|
|
457
|
+
fen,
|
|
458
|
+
}, generation);
|
|
459
|
+
this.worker.postMessage('stop');
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
this.pendingSearch = { fen, depth, multiPv, generation };
|
|
463
|
+
this.analysisFen = fen;
|
|
464
|
+
this.lineMap.clear();
|
|
465
|
+
this.setEvaluation({
|
|
466
|
+
status: 'analyzing',
|
|
467
|
+
depth: 0,
|
|
468
|
+
lines: [],
|
|
469
|
+
}, generation);
|
|
470
|
+
if (!this.searching) {
|
|
471
|
+
this.dispatchPendingSearch();
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
this.worker.postMessage('stop');
|
|
475
|
+
}
|
|
476
|
+
dispatchPendingSearch() {
|
|
477
|
+
const pending = this.pendingSearch;
|
|
478
|
+
if (!pending ||
|
|
479
|
+
!this.worker ||
|
|
480
|
+
!this.ready ||
|
|
481
|
+
this.disposed ||
|
|
482
|
+
pending.generation !== this.analysisGeneration ||
|
|
483
|
+
pending.generation === this.dispatchedSearchGeneration ||
|
|
484
|
+
!isAnalyzableFen(pending.fen)) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
this.dispatchedSearchGeneration = pending.generation;
|
|
488
|
+
this.searching = true;
|
|
489
|
+
this.worker.postMessage(`setoption name MultiPV value ${pending.multiPv}`);
|
|
490
|
+
this.worker.postMessage(`position fen ${pending.fen}`);
|
|
491
|
+
this.worker.postMessage(`go depth ${pending.depth}`);
|
|
492
|
+
}
|
|
493
|
+
stop() {
|
|
494
|
+
if (!this.worker || !this.ready) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
this.pendingSearch = null;
|
|
498
|
+
this.searching = false;
|
|
499
|
+
this.dispatchedSearchGeneration = this.analysisGeneration;
|
|
500
|
+
this.worker.postMessage('stop');
|
|
501
|
+
this.setEvaluation(Object.assign(Object.assign({}, this.evaluation), { status: 'idle' }));
|
|
502
|
+
}
|
|
503
|
+
dispose() {
|
|
504
|
+
this.disposed = true;
|
|
505
|
+
this.lifecycleGeneration += 1;
|
|
506
|
+
this.ready = false;
|
|
507
|
+
this.pendingSearch = null;
|
|
508
|
+
this.searching = false;
|
|
509
|
+
this.dispatchedSearchGeneration = 0;
|
|
510
|
+
this.stop();
|
|
511
|
+
if (this.worker) {
|
|
512
|
+
this.worker.onmessage = null;
|
|
513
|
+
this.worker.onerror = null;
|
|
514
|
+
this.worker.onmessageerror = null;
|
|
515
|
+
this.worker.terminate();
|
|
516
|
+
this.worker = null;
|
|
517
|
+
}
|
|
518
|
+
this.listeners.clear();
|
|
519
|
+
}
|
|
520
|
+
handleLine(line) {
|
|
521
|
+
const generation = this.analysisGeneration;
|
|
522
|
+
if (line.startsWith('info ')) {
|
|
523
|
+
const parsed = parseUciInfoLine(line);
|
|
524
|
+
if (!parsed) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
this.lineMap.set(parsed.multipv, parsed);
|
|
528
|
+
const lines = [...this.lineMap.values()].sort((a, b) => a.multipv - b.multipv);
|
|
529
|
+
const maxDepth = lines.reduce((max, entry) => Math.max(max, entry.depth), 0);
|
|
530
|
+
this.setEvaluation({
|
|
531
|
+
status: 'analyzing',
|
|
532
|
+
depth: maxDepth,
|
|
533
|
+
lines,
|
|
534
|
+
}, generation);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (line.startsWith('bestmove')) {
|
|
538
|
+
this.searching = false;
|
|
539
|
+
if (this.pendingSearch &&
|
|
540
|
+
this.pendingSearch.generation === this.analysisGeneration &&
|
|
541
|
+
this.pendingSearch.generation !== this.dispatchedSearchGeneration) {
|
|
542
|
+
this.dispatchPendingSearch();
|
|
543
|
+
if (this.searching) {
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
this.setEvaluation(Object.assign(Object.assign({}, this.evaluation), { status: 'idle' }), generation);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
setEvaluation(evaluation, generation = this.analysisGeneration) {
|
|
551
|
+
if (generation !== this.analysisGeneration) {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
const withFen = this.analysisFen.length > 0
|
|
555
|
+
? Object.assign(Object.assign({}, evaluation), { fen: this.analysisFen }) : evaluation;
|
|
556
|
+
this.evaluation = withFen;
|
|
557
|
+
this.listeners.forEach((listener) => listener(withFen));
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const useAnalysisEngine = (fen, options = {}) => {
|
|
562
|
+
const { enabled = true, depth = 16, multiPv = 2, scriptUrl = DEFAULT_STOCKFISH_SCRIPT_URL, } = options;
|
|
563
|
+
const [evaluation, setEvaluation] = useState(emptyEngineEvaluation());
|
|
564
|
+
const [engineReady, setEngineReady] = useState(false);
|
|
565
|
+
const engineRef = useRef(null);
|
|
566
|
+
const mountGenerationRef = useRef(0);
|
|
567
|
+
useEffect(() => {
|
|
568
|
+
if (!enabled || typeof Worker === 'undefined') {
|
|
569
|
+
setEvaluation(emptyEngineEvaluation());
|
|
570
|
+
setEngineReady(false);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
const mountGeneration = ++mountGenerationRef.current;
|
|
574
|
+
const engine = new StockfishBrowserEngine(scriptUrl);
|
|
575
|
+
engineRef.current = engine;
|
|
576
|
+
let cancelled = false;
|
|
577
|
+
const unsubscribe = engine.subscribe((next) => {
|
|
578
|
+
if (!cancelled && mountGeneration === mountGenerationRef.current) {
|
|
579
|
+
setEvaluation(next);
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
engine
|
|
583
|
+
.init()
|
|
584
|
+
.then(() => {
|
|
585
|
+
if (!cancelled &&
|
|
586
|
+
mountGeneration === mountGenerationRef.current) {
|
|
587
|
+
setEngineReady(true);
|
|
588
|
+
}
|
|
589
|
+
})
|
|
590
|
+
.catch((error) => {
|
|
591
|
+
if (cancelled || mountGeneration !== mountGenerationRef.current) {
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
const message = error instanceof Error ? error.message : 'Failed to start engine';
|
|
595
|
+
setEvaluation(Object.assign(Object.assign({}, emptyEngineEvaluation()), { status: 'error', error: message }));
|
|
596
|
+
});
|
|
597
|
+
return () => {
|
|
598
|
+
cancelled = true;
|
|
599
|
+
setEngineReady(false);
|
|
600
|
+
unsubscribe();
|
|
601
|
+
engine.dispose();
|
|
602
|
+
if (engineRef.current === engine) {
|
|
603
|
+
engineRef.current = null;
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
}, [enabled, scriptUrl]);
|
|
607
|
+
useLayoutEffect(() => {
|
|
608
|
+
if (!enabled || !engineReady || !engineRef.current) {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
const engine = engineRef.current;
|
|
612
|
+
const timer = window.setTimeout(() => {
|
|
613
|
+
engine.analyze(fen, depth, multiPv);
|
|
614
|
+
}, 75);
|
|
615
|
+
return () => {
|
|
616
|
+
window.clearTimeout(timer);
|
|
617
|
+
};
|
|
618
|
+
}, [enabled, engineReady, fen, depth, multiPv]);
|
|
619
|
+
return useMemo(() => {
|
|
620
|
+
if (evaluation.fen !== fen) {
|
|
621
|
+
return Object.assign(Object.assign({}, emptyEngineEvaluation()), { status: evaluation.status === 'error'
|
|
622
|
+
? 'error'
|
|
623
|
+
: evaluation.status === 'loading'
|
|
624
|
+
? 'loading'
|
|
625
|
+
: 'analyzing', error: evaluation.error, fen });
|
|
626
|
+
}
|
|
627
|
+
return evaluation;
|
|
628
|
+
}, [evaluation, fen]);
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Square overlay colors for analysis board replay (last move highlight).
|
|
633
|
+
*/
|
|
634
|
+
const analysisBoardHighlightColors = {
|
|
635
|
+
lastMove: {
|
|
636
|
+
light: 'rgba(253, 216, 53, 0.55)',
|
|
637
|
+
dark: 'rgba(144, 202, 249, 0.5)',
|
|
638
|
+
},
|
|
639
|
+
};
|
|
640
|
+
const getLastMoveSquareStyles = (from, to, theme) => {
|
|
641
|
+
const backgroundColor = analysisBoardHighlightColors.lastMove[theme];
|
|
642
|
+
return {
|
|
643
|
+
[from]: { backgroundColor },
|
|
644
|
+
[to]: { backgroundColor },
|
|
645
|
+
};
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
/** Apply a UCI move (e.g. `e7e8q`) without throwing. */
|
|
649
|
+
function applyUciMove(chess, uci) {
|
|
650
|
+
if (!uci || uci.length < 4) {
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
const from = uci.slice(0, 2);
|
|
654
|
+
const to = uci.slice(2, 4);
|
|
655
|
+
const promotion = uci.length > 4 ? uci[4] : undefined;
|
|
656
|
+
try {
|
|
657
|
+
return chess.move({ from, to, promotion }) !== null;
|
|
658
|
+
}
|
|
659
|
+
catch (_a) {
|
|
660
|
+
try {
|
|
661
|
+
chess.move(uci);
|
|
662
|
+
return true;
|
|
663
|
+
}
|
|
664
|
+
catch (_b) {
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
function getCheckSquareFromChess(chess) {
|
|
670
|
+
if (!chess.inCheck()) {
|
|
671
|
+
return '';
|
|
672
|
+
}
|
|
673
|
+
const turn = chess.turn();
|
|
674
|
+
const board = chess.board();
|
|
675
|
+
for (let rowIndex = 0; rowIndex < 8; rowIndex++) {
|
|
676
|
+
for (let colIndex = 0; colIndex < 8; colIndex++) {
|
|
677
|
+
const piece = board[rowIndex][colIndex];
|
|
678
|
+
if ((piece === null || piece === void 0 ? void 0 : piece.type) === 'k' && piece.color === turn) {
|
|
679
|
+
return String.fromCharCode(97 + colIndex) + (8 - rowIndex);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return '';
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
class AnalysisErrorBoundary extends Component {
|
|
687
|
+
constructor() {
|
|
688
|
+
super(...arguments);
|
|
689
|
+
this.state = { error: null };
|
|
690
|
+
}
|
|
691
|
+
static getDerivedStateFromError(error) {
|
|
692
|
+
return { error };
|
|
693
|
+
}
|
|
694
|
+
render() {
|
|
695
|
+
if (this.state.error) {
|
|
696
|
+
return (jsxs("div", { style: { padding: 16, maxWidth: 480 }, children: [jsx("p", { style: { margin: '0 0 8px', fontWeight: 600 }, children: "Analysis could not be opened." }), jsx("p", { style: { margin: 0, fontSize: 14, color: '#666' }, children: this.state.error.message ||
|
|
697
|
+
'An unexpected error occurred. If you use a wallet browser extension, try disabling it for this site.' }), this.props.onClose ? (jsx("button", { type: "button", onClick: this.props.onClose, style: { marginTop: 12, cursor: 'pointer' }, children: "Close" })) : null] }));
|
|
698
|
+
}
|
|
699
|
+
return this.props.children;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/** Draggable analysis board (no surrounding layout chrome). */
|
|
704
|
+
const AnalysisChessboardView = ({ model }) => {
|
|
705
|
+
var _a;
|
|
706
|
+
return (jsx(ChessboardDnDProvider, { children: jsx(HighlightChessboard, { checkSquare: (_a = model.checkSquare) !== null && _a !== void 0 ? _a : '', hintSquare: null, incorrectMoveSquare: null, position: model.fen, boardOrientation: model.boardOrientation, boardWidth: model.boardWidth, arePiecesDraggable: true, onPieceDrop: model.onPieceDrop, promotionDialogVariant: "modal", customSquareStyles: model.lastMove
|
|
707
|
+
? getLastMoveSquareStyles(model.lastMove.from, model.lastMove.to, model.theme)
|
|
708
|
+
: {} }) }));
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
class AnalysisPosition {
|
|
712
|
+
constructor(context) {
|
|
713
|
+
this.variation = null;
|
|
714
|
+
this.variationCursor = 0;
|
|
715
|
+
this.initialFen = context.initialFen;
|
|
716
|
+
this.solutionMoves = context.solutionMoves;
|
|
717
|
+
this.solutionSans = AnalysisPosition.buildSolutionSans(context.initialFen, context.solutionMoves);
|
|
718
|
+
this.mainPly = context.currentPly;
|
|
719
|
+
this.chess = new Chess(this.initialFen);
|
|
720
|
+
this.rebuildChess();
|
|
721
|
+
}
|
|
722
|
+
static buildSolutionSans(initialFen, solutionMoves) {
|
|
723
|
+
const chess = new Chess(initialFen);
|
|
724
|
+
return solutionMoves.map((uci, index) => {
|
|
725
|
+
let san = uci;
|
|
726
|
+
const before = chess.fen();
|
|
727
|
+
if (applyUciMove(chess, uci)) {
|
|
728
|
+
const move = chess.history({ verbose: true }).at(-1);
|
|
729
|
+
if (move === null || move === void 0 ? void 0 : move.san) {
|
|
730
|
+
san = move.san;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
chess.load(before);
|
|
735
|
+
}
|
|
736
|
+
return {
|
|
737
|
+
ply: index + 1,
|
|
738
|
+
uci,
|
|
739
|
+
san,
|
|
740
|
+
};
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
static applySolutionMove(chess, uci) {
|
|
744
|
+
return applyUciMove(chess, uci);
|
|
745
|
+
}
|
|
746
|
+
fenAtMainPly(ply) {
|
|
747
|
+
const chess = new Chess(this.initialFen);
|
|
748
|
+
for (let i = 0; i < ply; i++) {
|
|
749
|
+
if (!AnalysisPosition.applySolutionMove(chess, this.solutionMoves[i])) {
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return chess.fen();
|
|
754
|
+
}
|
|
755
|
+
rebuildChess() {
|
|
756
|
+
this.chess.load(this.initialFen);
|
|
757
|
+
for (let i = 0; i < this.mainPly; i++) {
|
|
758
|
+
if (!AnalysisPosition.applySolutionMove(this.chess, this.solutionMoves[i])) {
|
|
759
|
+
break;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
if (this.variation && this.variationCursor > 0) {
|
|
763
|
+
for (let i = 0; i < this.variationCursor; i++) {
|
|
764
|
+
this.chess.move(this.variation.moves[i]);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
findLegalMove(sourceSquare, targetSquare, piece) {
|
|
769
|
+
var _a;
|
|
770
|
+
const pieceType = (_a = piece[1]) === null || _a === void 0 ? void 0 : _a.toLowerCase();
|
|
771
|
+
return this.chess
|
|
772
|
+
.moves({ square: sourceSquare, verbose: true })
|
|
773
|
+
.find((move) => move.to === targetSquare &&
|
|
774
|
+
(!move.promotion || move.promotion === pieceType));
|
|
775
|
+
}
|
|
776
|
+
uciFromVerboseMove(move) {
|
|
777
|
+
var _a;
|
|
778
|
+
return `${move.from}${move.to}${(_a = move.promotion) !== null && _a !== void 0 ? _a : ''}`;
|
|
779
|
+
}
|
|
780
|
+
matchesMainMove(mainIndex, uci) {
|
|
781
|
+
const expected = this.solutionMoves[mainIndex];
|
|
782
|
+
if (expected === uci) {
|
|
783
|
+
return true;
|
|
784
|
+
}
|
|
785
|
+
if (expected.length === 5 && uci.length === 4) {
|
|
786
|
+
return expected.slice(0, 4) === uci;
|
|
787
|
+
}
|
|
788
|
+
return false;
|
|
789
|
+
}
|
|
790
|
+
getNavPly() {
|
|
791
|
+
if (this.variation && this.variationCursor > 0) {
|
|
792
|
+
return this.variation.branchPly + this.variationCursor;
|
|
793
|
+
}
|
|
794
|
+
return this.mainPly;
|
|
795
|
+
}
|
|
796
|
+
getMaxNavPly() {
|
|
797
|
+
if (this.variation && this.variation.moves.length > 0) {
|
|
798
|
+
return this.variation.branchPly + this.variation.moves.length;
|
|
799
|
+
}
|
|
800
|
+
return this.solutionMoves.length;
|
|
801
|
+
}
|
|
802
|
+
/** @deprecated Use getNavPly */
|
|
803
|
+
getPly() {
|
|
804
|
+
return this.getNavPly();
|
|
805
|
+
}
|
|
806
|
+
/** @deprecated Use getMaxNavPly */
|
|
807
|
+
getMaxPly() {
|
|
808
|
+
return this.getMaxNavPly();
|
|
809
|
+
}
|
|
810
|
+
getSolutionSans() {
|
|
811
|
+
return this.solutionSans;
|
|
812
|
+
}
|
|
813
|
+
getHistoryRows() {
|
|
814
|
+
var _a, _b;
|
|
815
|
+
const rows = [
|
|
816
|
+
{
|
|
817
|
+
key: 'start',
|
|
818
|
+
label: 'Start',
|
|
819
|
+
indent: 0,
|
|
820
|
+
kind: 'start',
|
|
821
|
+
mainPly: 0,
|
|
822
|
+
variationIndex: 0,
|
|
823
|
+
},
|
|
824
|
+
];
|
|
825
|
+
if (((_a = this.variation) === null || _a === void 0 ? void 0 : _a.branchPly) === 0) {
|
|
826
|
+
this.variation.sans.forEach((san, index) => {
|
|
827
|
+
const variationIndex = index + 1;
|
|
828
|
+
rows.push({
|
|
829
|
+
key: `var-0-${variationIndex}`,
|
|
830
|
+
label: `${variationIndex}. ${san}`,
|
|
831
|
+
indent: 1,
|
|
832
|
+
kind: 'variation',
|
|
833
|
+
mainPly: 0,
|
|
834
|
+
variationIndex,
|
|
835
|
+
});
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
for (const move of this.solutionSans) {
|
|
839
|
+
rows.push({
|
|
840
|
+
key: `main-${move.ply}`,
|
|
841
|
+
label: `${move.ply}. ${move.san}`,
|
|
842
|
+
indent: 0,
|
|
843
|
+
kind: 'main',
|
|
844
|
+
mainPly: move.ply,
|
|
845
|
+
variationIndex: 0,
|
|
846
|
+
});
|
|
847
|
+
if (((_b = this.variation) === null || _b === void 0 ? void 0 : _b.branchPly) === move.ply) {
|
|
848
|
+
this.variation.sans.forEach((san, index) => {
|
|
849
|
+
const variationIndex = index + 1;
|
|
850
|
+
rows.push({
|
|
851
|
+
key: `var-${move.ply}-${variationIndex}`,
|
|
852
|
+
label: `${move.ply + variationIndex}. ${san}`,
|
|
853
|
+
indent: 1,
|
|
854
|
+
kind: 'variation',
|
|
855
|
+
mainPly: move.ply,
|
|
856
|
+
variationIndex,
|
|
857
|
+
});
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return rows;
|
|
862
|
+
}
|
|
863
|
+
isHistoryRowSelected(row) {
|
|
864
|
+
var _a;
|
|
865
|
+
if (row.kind === 'start') {
|
|
866
|
+
return this.getNavPly() === 0;
|
|
867
|
+
}
|
|
868
|
+
if (row.kind === 'main') {
|
|
869
|
+
return ((!this.variation || this.variationCursor === 0) &&
|
|
870
|
+
this.mainPly === row.mainPly);
|
|
871
|
+
}
|
|
872
|
+
return (((_a = this.variation) === null || _a === void 0 ? void 0 : _a.branchPly) === row.mainPly &&
|
|
873
|
+
this.variationCursor === row.variationIndex);
|
|
874
|
+
}
|
|
875
|
+
selectHistoryRow(row) {
|
|
876
|
+
if (row.kind === 'start' || row.kind === 'main') {
|
|
877
|
+
this.selectMainLine(row.mainPly);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
if (!this.variation || this.variation.branchPly !== row.mainPly) {
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
this.mainPly = row.mainPly;
|
|
884
|
+
this.variationCursor = row.variationIndex;
|
|
885
|
+
this.rebuildChess();
|
|
886
|
+
}
|
|
887
|
+
selectMainLine(ply) {
|
|
888
|
+
this.mainPly = Math.max(0, Math.min(ply, this.solutionMoves.length));
|
|
889
|
+
this.variation = null;
|
|
890
|
+
this.variationCursor = 0;
|
|
891
|
+
this.rebuildChess();
|
|
892
|
+
}
|
|
893
|
+
/** @deprecated Use selectMainLine or goToNavPly */
|
|
894
|
+
goToPly(ply) {
|
|
895
|
+
this.goToNavPly(ply);
|
|
896
|
+
}
|
|
897
|
+
goToNavPly(navPly) {
|
|
898
|
+
const clamped = Math.max(0, Math.min(navPly, this.getMaxNavPly()));
|
|
899
|
+
if (clamped === 0) {
|
|
900
|
+
this.selectMainLine(0);
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
if (this.variation &&
|
|
904
|
+
clamped > this.variation.branchPly &&
|
|
905
|
+
clamped <= this.variation.branchPly + this.variation.moves.length) {
|
|
906
|
+
this.mainPly = this.variation.branchPly;
|
|
907
|
+
this.variationCursor = clamped - this.variation.branchPly;
|
|
908
|
+
this.rebuildChess();
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
this.selectMainLine(clamped);
|
|
912
|
+
}
|
|
913
|
+
tryPlayMove(sourceSquare, targetSquare, piece) {
|
|
914
|
+
var _a, _b;
|
|
915
|
+
const legal = this.findLegalMove(sourceSquare, targetSquare, piece);
|
|
916
|
+
if (!legal) {
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
const uci = this.uciFromVerboseMove(legal);
|
|
920
|
+
if (!this.variation && this.mainPly < this.solutionMoves.length) {
|
|
921
|
+
if (this.matchesMainMove(this.mainPly, uci)) {
|
|
922
|
+
this.mainPly++;
|
|
923
|
+
this.rebuildChess();
|
|
924
|
+
return true;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
const branchPly = (_b = (_a = this.variation) === null || _a === void 0 ? void 0 : _a.branchPly) !== null && _b !== void 0 ? _b : this.mainPly;
|
|
928
|
+
const fenAtBranch = this.fenAtMainPly(branchPly);
|
|
929
|
+
let nextMoves;
|
|
930
|
+
if (this.variation && this.variation.branchPly === branchPly) {
|
|
931
|
+
nextMoves = [
|
|
932
|
+
...this.variation.moves.slice(0, this.variationCursor),
|
|
933
|
+
uci,
|
|
934
|
+
];
|
|
935
|
+
}
|
|
936
|
+
else {
|
|
937
|
+
nextMoves = [uci];
|
|
938
|
+
}
|
|
939
|
+
this.variation = {
|
|
940
|
+
branchPly,
|
|
941
|
+
moves: nextMoves,
|
|
942
|
+
sans: uciPvToSan(fenAtBranch, nextMoves),
|
|
943
|
+
};
|
|
944
|
+
this.mainPly = branchPly;
|
|
945
|
+
this.variationCursor = nextMoves.length;
|
|
946
|
+
this.rebuildChess();
|
|
947
|
+
return true;
|
|
948
|
+
}
|
|
949
|
+
next() {
|
|
950
|
+
if (this.getNavPly() >= this.getMaxNavPly()) {
|
|
951
|
+
return false;
|
|
952
|
+
}
|
|
953
|
+
this.goToNavPly(this.getNavPly() + 1);
|
|
954
|
+
return true;
|
|
955
|
+
}
|
|
956
|
+
prev() {
|
|
957
|
+
if (this.getNavPly() <= 0) {
|
|
958
|
+
return false;
|
|
959
|
+
}
|
|
960
|
+
this.goToNavPly(this.getNavPly() - 1);
|
|
961
|
+
return true;
|
|
962
|
+
}
|
|
963
|
+
getLastMoveSquares() {
|
|
964
|
+
const navPly = this.getNavPly();
|
|
965
|
+
if (navPly === 0) {
|
|
966
|
+
return null;
|
|
967
|
+
}
|
|
968
|
+
let uci;
|
|
969
|
+
if (this.variation && this.variationCursor > 0) {
|
|
970
|
+
uci = this.variation.moves[this.variationCursor - 1];
|
|
971
|
+
}
|
|
972
|
+
else {
|
|
973
|
+
uci = this.solutionMoves[this.mainPly - 1];
|
|
974
|
+
}
|
|
975
|
+
return {
|
|
976
|
+
from: uci.slice(0, 2),
|
|
977
|
+
to: uci.slice(2, 4),
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
fen() {
|
|
981
|
+
return this.chess.fen();
|
|
982
|
+
}
|
|
983
|
+
getCheckSquare() {
|
|
984
|
+
return getCheckSquareFromChess(this.chess);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
const useAnalysisBoardModel = ({ analysisContext, onClose, theme, boardWidth, engine, }) => {
|
|
989
|
+
var _a, _b, _c, _d;
|
|
990
|
+
const skipBackdropCloseRef = useRef(true);
|
|
991
|
+
const analysisPosition = useMemo(() => new AnalysisPosition(analysisContext), [analysisContext]);
|
|
992
|
+
const [navRevision, setNavRevision] = useState(0);
|
|
993
|
+
const bumpNav = () => setNavRevision((value) => value + 1);
|
|
994
|
+
useEffect(() => {
|
|
995
|
+
analysisPosition.goToNavPly(analysisContext.currentPly);
|
|
996
|
+
bumpNav();
|
|
997
|
+
skipBackdropCloseRef.current = true;
|
|
998
|
+
const frameId = requestAnimationFrame(() => {
|
|
999
|
+
skipBackdropCloseRef.current = false;
|
|
1000
|
+
});
|
|
1001
|
+
return () => cancelAnimationFrame(frameId);
|
|
1002
|
+
}, [analysisContext, analysisPosition]);
|
|
1003
|
+
const fen = analysisPosition.fen();
|
|
1004
|
+
const engineEnabled = (_a = engine === null || engine === void 0 ? void 0 : engine.enabled) !== null && _a !== void 0 ? _a : true;
|
|
1005
|
+
const engineEvaluation = useAnalysisEngine(fen, {
|
|
1006
|
+
enabled: engineEnabled,
|
|
1007
|
+
depth: (_b = engine === null || engine === void 0 ? void 0 : engine.depth) !== null && _b !== void 0 ? _b : 16,
|
|
1008
|
+
multiPv: (_c = engine === null || engine === void 0 ? void 0 : engine.multiPv) !== null && _c !== void 0 ? _c : 2,
|
|
1009
|
+
scriptUrl: (_d = engine === null || engine === void 0 ? void 0 : engine.scriptUrl) !== null && _d !== void 0 ? _d : DEFAULT_STOCKFISH_SCRIPT_URL,
|
|
1010
|
+
});
|
|
1011
|
+
return {
|
|
1012
|
+
theme,
|
|
1013
|
+
boardWidth,
|
|
1014
|
+
analysisContext,
|
|
1015
|
+
fen,
|
|
1016
|
+
ply: analysisPosition.getNavPly(),
|
|
1017
|
+
maxPly: analysisPosition.getMaxNavPly(),
|
|
1018
|
+
historyRows: analysisPosition.getHistoryRows(),
|
|
1019
|
+
solutionSans: analysisPosition.getSolutionSans(),
|
|
1020
|
+
boardOrientation: analysisContext.boardOrientation,
|
|
1021
|
+
engineEvaluation,
|
|
1022
|
+
engineEnabled,
|
|
1023
|
+
lastMove: analysisPosition.getLastMoveSquares(),
|
|
1024
|
+
checkSquare: analysisPosition.getCheckSquare(),
|
|
1025
|
+
onSelectPly: (ply) => {
|
|
1026
|
+
analysisPosition.goToNavPly(ply);
|
|
1027
|
+
bumpNav();
|
|
1028
|
+
},
|
|
1029
|
+
onSelectHistoryRow: (row) => {
|
|
1030
|
+
analysisPosition.selectHistoryRow(row);
|
|
1031
|
+
bumpNav();
|
|
1032
|
+
},
|
|
1033
|
+
isHistoryRowSelected: (row) => analysisPosition.isHistoryRowSelected(row),
|
|
1034
|
+
onPieceDrop: (sourceSquare, targetSquare, piece) => {
|
|
1035
|
+
if (!analysisPosition.tryPlayMove(sourceSquare, targetSquare, piece)) {
|
|
1036
|
+
return false;
|
|
1037
|
+
}
|
|
1038
|
+
bumpNav();
|
|
1039
|
+
return true;
|
|
1040
|
+
},
|
|
1041
|
+
onBackdropMouseDown: () => {
|
|
1042
|
+
if (skipBackdropCloseRef.current) {
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
onClose();
|
|
1046
|
+
},
|
|
1047
|
+
onClose,
|
|
1048
|
+
};
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* Analysis logic + composition only: hook, board node, sidebar/engine slots.
|
|
1053
|
+
* No layout divs — use {@link renderMain} (e.g. `AnalysisBoardLayout` from `analysis/defaults` or a host layout).
|
|
1054
|
+
*/
|
|
1055
|
+
const AnalysisBoardCore = (_a) => {
|
|
1056
|
+
var { renderContainer, renderMain, renderSidebar, renderEngineEvaluation } = _a, modelArgs = __rest(_a, ["renderContainer", "renderMain", "renderSidebar", "renderEngineEvaluation"]);
|
|
1057
|
+
const model = useAnalysisBoardModel(modelArgs);
|
|
1058
|
+
return (jsx(AnalysisBoardCoreView, { model: model, renderContainer: renderContainer, renderMain: renderMain, renderSidebar: renderSidebar, renderEngineEvaluation: renderEngineEvaluation }));
|
|
1059
|
+
};
|
|
1060
|
+
/** Pure composition (no layout styles) for testing and reuse. */
|
|
1061
|
+
const AnalysisBoardCoreView = ({ model, renderContainer, renderMain, renderSidebar, renderEngineEvaluation, }) => {
|
|
1062
|
+
const board = jsx(AnalysisChessboardView, { model: model });
|
|
1063
|
+
const engineEvaluationPanel = model.engineEnabled
|
|
1064
|
+
? renderEngineEvaluation({
|
|
1065
|
+
fen: model.fen,
|
|
1066
|
+
evaluation: model.engineEvaluation,
|
|
1067
|
+
theme: model.theme,
|
|
1068
|
+
})
|
|
1069
|
+
: null;
|
|
1070
|
+
const sidebar = renderSidebar({
|
|
1071
|
+
moves: model.solutionSans,
|
|
1072
|
+
historyRows: model.historyRows,
|
|
1073
|
+
isHistoryRowSelected: model.isHistoryRowSelected,
|
|
1074
|
+
onSelectHistoryRow: model.onSelectHistoryRow,
|
|
1075
|
+
ply: model.ply,
|
|
1076
|
+
maxPly: model.maxPly,
|
|
1077
|
+
onSelectPly: model.onSelectPly,
|
|
1078
|
+
theme: model.theme,
|
|
1079
|
+
engineEvaluationPanel,
|
|
1080
|
+
});
|
|
1081
|
+
const main = renderMain({ model, board, sidebar });
|
|
1082
|
+
return renderContainer({
|
|
1083
|
+
theme: model.theme,
|
|
1084
|
+
onClose: model.onClose,
|
|
1085
|
+
children: main,
|
|
1086
|
+
onBackdropMouseDown: model.onBackdropMouseDown,
|
|
1087
|
+
});
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
const getAnalysisModalStyles = (theme) => {
|
|
1091
|
+
if (theme === 'dark') {
|
|
1092
|
+
return {
|
|
1093
|
+
panel: {
|
|
1094
|
+
backgroundColor: '#1e1e1e',
|
|
1095
|
+
color: '#e0e0e0',
|
|
1096
|
+
border: '1px solid #424242',
|
|
1097
|
+
},
|
|
1098
|
+
title: { color: '#fff' },
|
|
1099
|
+
closeButton: {
|
|
1100
|
+
color: '#e0e0e0',
|
|
1101
|
+
backgroundColor: '#2d2d2d',
|
|
1102
|
+
border: '1px solid #555',
|
|
1103
|
+
},
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
return {
|
|
1107
|
+
panel: {
|
|
1108
|
+
backgroundColor: '#fff',
|
|
1109
|
+
color: '#1a1a1a',
|
|
1110
|
+
border: '1px solid #e0e0e0',
|
|
1111
|
+
},
|
|
1112
|
+
title: { color: '#1a1a1a' },
|
|
1113
|
+
closeButton: {
|
|
1114
|
+
color: '#1a1a1a',
|
|
1115
|
+
backgroundColor: '#f5f5f5',
|
|
1116
|
+
border: '1px solid #ccc',
|
|
1117
|
+
},
|
|
1118
|
+
};
|
|
1119
|
+
};
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* Move-list colors for the analysis board sidebar.
|
|
1123
|
+
* Tweak values here when iterating on history row striping.
|
|
1124
|
+
*/
|
|
1125
|
+
const analysisSidebarColors = {
|
|
1126
|
+
/** Selected move row */
|
|
1127
|
+
activeMove: {
|
|
1128
|
+
light: 'rgba(119, 177, 212, 1)', // #77b1d4
|
|
1129
|
+
dark: 'rgba(90, 159, 190, 1)', // #5a9fbe
|
|
1130
|
+
},
|
|
1131
|
+
/** Start row */
|
|
1132
|
+
start: {
|
|
1133
|
+
light: '#e8e8e8',
|
|
1134
|
+
dark: '#262626',
|
|
1135
|
+
},
|
|
1136
|
+
/** Alternating main-line rows (index 0 = darker, 1 = lighter). */
|
|
1137
|
+
mainStripe: [
|
|
1138
|
+
{ light: '#c9c9c9', dark: '#1c1c1c' },
|
|
1139
|
+
{ light: '#f2f2f2', dark: '#383838' },
|
|
1140
|
+
],
|
|
1141
|
+
/** Alternating user-variation rows (high-contrast striping like puzzle UI). */
|
|
1142
|
+
variationStripe: [
|
|
1143
|
+
{ light: 'rgba(216, 216, 216, 1)', dark: 'rgba(200, 200, 200, 1)' },
|
|
1144
|
+
{ light: 'rgba(255, 255, 255, 1)', dark: 'rgba(255, 255, 255, 1)' },
|
|
1145
|
+
],
|
|
1146
|
+
/** Text on variation rows (light backgrounds need dark type). */
|
|
1147
|
+
variationText: {
|
|
1148
|
+
light: 'rgba(0, 0, 0, 0.87)',
|
|
1149
|
+
dark: 'rgba(0, 0, 0, 0.87)',
|
|
1150
|
+
},
|
|
1151
|
+
};
|
|
1152
|
+
|
|
1153
|
+
const createSidebarRowBandCounters = () => ({
|
|
1154
|
+
main: 0,
|
|
1155
|
+
variation: 0,
|
|
1156
|
+
});
|
|
1157
|
+
/** Background for one analysis history row; mutates `bands` while iterating rows in order. */
|
|
1158
|
+
const getSidebarRowBackground = (theme, row, bands) => {
|
|
1159
|
+
if (row.kind === 'start') {
|
|
1160
|
+
return analysisSidebarColors.start[theme];
|
|
1161
|
+
}
|
|
1162
|
+
if (row.kind === 'main') {
|
|
1163
|
+
bands.variation = 0;
|
|
1164
|
+
const stripe = bands.main % 2;
|
|
1165
|
+
bands.main += 1;
|
|
1166
|
+
return analysisSidebarColors.mainStripe[stripe][theme];
|
|
1167
|
+
}
|
|
1168
|
+
const stripe = bands.variation % 2;
|
|
1169
|
+
bands.variation += 1;
|
|
1170
|
+
return analysisSidebarColors.variationStripe[stripe][theme];
|
|
1171
|
+
};
|
|
1172
|
+
|
|
1173
|
+
/** Library default grid; hosts should pass their own {@link AnalysisLayoutConfig}. */
|
|
1174
|
+
const DEFAULT_ANALYSIS_LAYOUT = {
|
|
1175
|
+
boardWidth: 480,
|
|
1176
|
+
sidebarWidth: 500,
|
|
1177
|
+
columnGap: 16,
|
|
1178
|
+
};
|
|
1179
|
+
|
|
1180
|
+
const EngineEvaluationPanel = ({ fen, evaluation, theme, }) => {
|
|
1181
|
+
var _a;
|
|
1182
|
+
const isDark = theme === 'dark';
|
|
1183
|
+
const safePv = (pv, maxMoves) => {
|
|
1184
|
+
try {
|
|
1185
|
+
const label = formatPvPreview(fen, pv, maxMoves);
|
|
1186
|
+
const title = formatPvPreview(fen, pv, Array.isArray(pv) ? pv.length : maxMoves !== null && maxMoves !== void 0 ? maxMoves : 6);
|
|
1187
|
+
return { label, title };
|
|
1188
|
+
}
|
|
1189
|
+
catch (_a) {
|
|
1190
|
+
return { label: '', title: '' };
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
if (evaluation.status === 'loading') {
|
|
1194
|
+
return (jsx("p", { style: captionStyle(isDark), children: "Starting engine\u2026" }));
|
|
1195
|
+
}
|
|
1196
|
+
if (evaluation.status === 'error') {
|
|
1197
|
+
return (jsx("p", { style: Object.assign(Object.assign({}, captionStyle(isDark)), { color: '#e57373' }), children: (_a = evaluation.error) !== null && _a !== void 0 ? _a : 'Engine unavailable' }));
|
|
1198
|
+
}
|
|
1199
|
+
if (evaluation.lines.length === 0) {
|
|
1200
|
+
return (jsx("p", { style: captionStyle(isDark), children: evaluation.status === 'analyzing' ? 'Analyzing…' : 'No evaluation yet' }));
|
|
1201
|
+
}
|
|
1202
|
+
return (jsxs("div", { style: panelStyle$1, children: [jsxs("p", { style: headerStyle$1(isDark), children: ["Engine", evaluation.depth > 0 ? ` · depth ${evaluation.depth}` : ''] }), jsx("ul", { style: listStyle, children: evaluation.lines.map((line) => {
|
|
1203
|
+
const normalized = normalizeEvalForWhite(fen, line.centipawns, line.mate);
|
|
1204
|
+
const evalLabel = formatEvaluation(normalized.centipawns, normalized.mate);
|
|
1205
|
+
const { label: pvLabel, title: pvTitle } = safePv(line.pv, Array.isArray(line.pv) ? line.pv.length : 6);
|
|
1206
|
+
return (jsxs("li", { style: lineStyle(isDark), children: [jsx("span", { style: evalStyle(isDark), children: evalLabel }), pvLabel ? (jsx("span", { style: pvStyle(isDark), title: pvTitle || undefined, children: pvLabel })) : null] }, line.multipv));
|
|
1207
|
+
}) })] }));
|
|
1208
|
+
};
|
|
1209
|
+
const panelStyle$1 = {
|
|
1210
|
+
display: 'flex',
|
|
1211
|
+
flexDirection: 'column',
|
|
1212
|
+
gap: 6,
|
|
1213
|
+
};
|
|
1214
|
+
const listStyle = {
|
|
1215
|
+
listStyle: 'none',
|
|
1216
|
+
margin: 0,
|
|
1217
|
+
padding: 0,
|
|
1218
|
+
display: 'flex',
|
|
1219
|
+
flexDirection: 'column',
|
|
1220
|
+
gap: 4,
|
|
1221
|
+
};
|
|
1222
|
+
const lineStyle = (dark) => ({
|
|
1223
|
+
display: 'flex',
|
|
1224
|
+
flexDirection: 'column',
|
|
1225
|
+
gap: 2,
|
|
1226
|
+
padding: '6px 8px',
|
|
1227
|
+
borderRadius: 4,
|
|
1228
|
+
backgroundColor: dark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)',
|
|
1229
|
+
fontSize: 12,
|
|
1230
|
+
});
|
|
1231
|
+
const evalStyle = (dark) => ({
|
|
1232
|
+
fontWeight: 700,
|
|
1233
|
+
fontFamily: 'monospace',
|
|
1234
|
+
color: dark ? '#ce93d8' : '#6a1b9a',
|
|
1235
|
+
});
|
|
1236
|
+
const pvStyle = (dark) => ({
|
|
1237
|
+
fontFamily: 'monospace',
|
|
1238
|
+
fontSize: 11,
|
|
1239
|
+
color: dark ? '#b0b0b0' : '#555',
|
|
1240
|
+
wordBreak: 'break-word',
|
|
1241
|
+
});
|
|
1242
|
+
const headerStyle$1 = (dark) => ({
|
|
1243
|
+
margin: 0,
|
|
1244
|
+
fontSize: 12,
|
|
1245
|
+
fontWeight: 600,
|
|
1246
|
+
color: dark ? '#e0e0e0' : '#333',
|
|
1247
|
+
});
|
|
1248
|
+
const captionStyle = (dark) => ({
|
|
1249
|
+
margin: 0,
|
|
1250
|
+
fontSize: 12,
|
|
1251
|
+
color: dark ? '#9e9e9e' : '#666',
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
/**
|
|
1255
|
+
* Optional grid helper: board column + sidebar column.
|
|
1256
|
+
* Hosts may use this in `renderMain` or supply their own layout.
|
|
1257
|
+
*/
|
|
1258
|
+
const AnalysisBoardLayout = ({ layout, model, board, sidebar, }) => {
|
|
1259
|
+
const { boardWidth, sidebarWidth, columnGap } = layout;
|
|
1260
|
+
const totalWidth = boardWidth + sidebarWidth + columnGap;
|
|
1261
|
+
return (jsx(ThemeProvider, { theme: model.theme, children: jsxs("div", { style: {
|
|
1262
|
+
display: 'grid',
|
|
1263
|
+
gridTemplateColumns: `${boardWidth}px ${sidebarWidth}px`,
|
|
1264
|
+
columnGap,
|
|
1265
|
+
alignItems: 'start',
|
|
1266
|
+
width: totalWidth,
|
|
1267
|
+
maxWidth: '100%',
|
|
1268
|
+
boxSizing: 'border-box',
|
|
1269
|
+
}, children: [jsx("div", { style: {
|
|
1270
|
+
gridColumn: 1,
|
|
1271
|
+
gridRow: 1,
|
|
1272
|
+
minWidth: 0,
|
|
1273
|
+
width: boardWidth,
|
|
1274
|
+
maxWidth: boardWidth,
|
|
1275
|
+
overflow: 'hidden',
|
|
1276
|
+
}, children: jsx("div", { style: { width: boardWidth, maxWidth: '100%' }, children: board }) }), jsx("div", { style: {
|
|
1277
|
+
gridColumn: 2,
|
|
1278
|
+
gridRow: 1,
|
|
1279
|
+
minWidth: 0,
|
|
1280
|
+
width: sidebarWidth,
|
|
1281
|
+
maxWidth: sidebarWidth,
|
|
1282
|
+
}, children: sidebar })] }) }));
|
|
1283
|
+
};
|
|
1284
|
+
|
|
1285
|
+
/** Default full-screen modal shell for analysis (library preset UI). */
|
|
1286
|
+
const DefaultAnalysisContainer = ({ theme, onClose, onBackdropMouseDown, children, }) => {
|
|
1287
|
+
const modalTheme = getAnalysisModalStyles(theme);
|
|
1288
|
+
const modal = (jsx("div", { style: overlayStyle, onMouseDown: onBackdropMouseDown, children: jsxs("div", { style: Object.assign(Object.assign({}, panelStyle), modalTheme.panel), onMouseDown: (event) => event.stopPropagation(), role: "dialog", "aria-modal": "true", "aria-label": "Analysis", "data-rcwc-theme": theme, children: [jsxs("div", { style: headerStyle, children: [jsx("h2", { style: Object.assign(Object.assign({}, titleStyle), modalTheme.title), children: "Analysis" }), jsx("button", { type: "button", onClick: onClose, style: Object.assign(Object.assign({}, closeButtonStyle), modalTheme.closeButton), children: "Close" })] }), children] }) }));
|
|
1289
|
+
if (typeof document === 'undefined') {
|
|
1290
|
+
return modal;
|
|
1291
|
+
}
|
|
1292
|
+
return createPortal(modal, document.body);
|
|
1293
|
+
};
|
|
1294
|
+
const overlayStyle = {
|
|
1295
|
+
position: 'fixed',
|
|
1296
|
+
inset: 0,
|
|
1297
|
+
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
|
1298
|
+
display: 'flex',
|
|
1299
|
+
alignItems: 'center',
|
|
1300
|
+
justifyContent: 'center',
|
|
1301
|
+
zIndex: 9999,
|
|
1302
|
+
};
|
|
1303
|
+
const panelStyle = {
|
|
1304
|
+
borderRadius: 8,
|
|
1305
|
+
padding: 16,
|
|
1306
|
+
width: 'max-content',
|
|
1307
|
+
maxWidth: '95vw',
|
|
1308
|
+
maxHeight: '95vh',
|
|
1309
|
+
overflow: 'auto',
|
|
1310
|
+
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3)',
|
|
1311
|
+
};
|
|
1312
|
+
const headerStyle = {
|
|
1313
|
+
display: 'flex',
|
|
1314
|
+
alignItems: 'center',
|
|
1315
|
+
justifyContent: 'space-between',
|
|
1316
|
+
marginBottom: 12,
|
|
1317
|
+
};
|
|
1318
|
+
const titleStyle = {
|
|
1319
|
+
margin: 0,
|
|
1320
|
+
fontSize: 20,
|
|
1321
|
+
};
|
|
1322
|
+
const closeButtonStyle = {
|
|
1323
|
+
cursor: 'pointer',
|
|
1324
|
+
borderRadius: 4,
|
|
1325
|
+
padding: '4px 12px',
|
|
1326
|
+
fontSize: 14,
|
|
1327
|
+
};
|
|
1328
|
+
|
|
1329
|
+
const navRowStyle = {
|
|
1330
|
+
display: 'flex',
|
|
1331
|
+
alignItems: 'center',
|
|
1332
|
+
gap: 6,
|
|
1333
|
+
};
|
|
1334
|
+
const scrubberInputStyle = {
|
|
1335
|
+
flex: 1,
|
|
1336
|
+
};
|
|
1337
|
+
const plyLabelStyle = {
|
|
1338
|
+
minWidth: 56,
|
|
1339
|
+
textAlign: 'center',
|
|
1340
|
+
fontSize: 14,
|
|
1341
|
+
};
|
|
1342
|
+
function palette(theme) {
|
|
1343
|
+
return {
|
|
1344
|
+
text: theme === 'dark' ? '#e8e8e8' : '#1a1a1a',
|
|
1345
|
+
border: theme === 'dark' ? '#3a3a3a' : '#d0d0d0',
|
|
1346
|
+
surface: theme === 'dark' ? '#262626' : '#f5f5f5',
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
function navButtonStyle(colors) {
|
|
1350
|
+
return {
|
|
1351
|
+
padding: '4px 10px',
|
|
1352
|
+
borderRadius: 6,
|
|
1353
|
+
cursor: 'pointer',
|
|
1354
|
+
fontSize: 14,
|
|
1355
|
+
fontWeight: 600,
|
|
1356
|
+
border: `1px solid ${colors.border}`,
|
|
1357
|
+
background: colors.surface,
|
|
1358
|
+
color: colors.text,
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/** Library-default ply navigation (inline styles). */
|
|
1363
|
+
const DefaultPlyNavigation = ({ plyIndex, totalPly, canPrev, canNext, onGoFirst, onGoPrev, onGoNext, onGoLast, onGoTo, theme, showScrubber, showPlyLabel, }) => {
|
|
1364
|
+
const colors = palette(theme);
|
|
1365
|
+
const buttonStyle = navButtonStyle(colors);
|
|
1366
|
+
return (jsxs("div", { style: navRowStyle, children: [jsx("button", { type: "button", onClick: onGoFirst, disabled: !canPrev, style: buttonStyle, "aria-label": "First move", children: "\u23EE" }), jsx("button", { type: "button", onClick: onGoPrev, disabled: !canPrev, style: buttonStyle, "aria-label": "Previous move", children: "\u25C0" }), showScrubber ? (jsx("input", { type: "range", min: 0, max: totalPly, value: plyIndex, onChange: (e) => onGoTo(Number(e.target.value)), style: scrubberInputStyle, "aria-label": "Scrub through game" })) : showPlyLabel ? (jsxs("span", { style: Object.assign(Object.assign({}, plyLabelStyle), { color: colors.text }), children: [plyIndex, " / ", totalPly] })) : null, jsx("button", { type: "button", onClick: onGoNext, disabled: !canNext, style: buttonStyle, "aria-label": "Next move", children: "\u25B6" }), jsx("button", { type: "button", onClick: onGoLast, disabled: !canNext, style: buttonStyle, "aria-label": "Last move", children: "\u23ED" })] }));
|
|
1367
|
+
};
|
|
1368
|
+
const defaultRenderPlyNavigation = (props) => (jsx(DefaultPlyNavigation, Object.assign({}, props)));
|
|
1369
|
+
|
|
1370
|
+
/**
|
|
1371
|
+
* Step through a fixed move list. Omit {@link PlyNavigationProps.renderPlyNavigation}
|
|
1372
|
+
* for the default inline-styled UI, or pass a custom renderer (e.g. MUI controls).
|
|
1373
|
+
*/
|
|
1374
|
+
const PlyNavigation = ({ plyIndex, totalPly, canPrev, canNext, onGoFirst, onGoPrev, onGoNext, onGoLast, onGoTo, theme = 'dark', showScrubber = true, showPlyLabel, renderPlyNavigation = defaultRenderPlyNavigation, }) => renderPlyNavigation({
|
|
1375
|
+
plyIndex,
|
|
1376
|
+
totalPly,
|
|
1377
|
+
canPrev,
|
|
1378
|
+
canNext,
|
|
1379
|
+
onGoFirst,
|
|
1380
|
+
onGoPrev,
|
|
1381
|
+
onGoNext,
|
|
1382
|
+
onGoLast,
|
|
1383
|
+
onGoTo,
|
|
1384
|
+
theme,
|
|
1385
|
+
showScrubber,
|
|
1386
|
+
showPlyLabel: showPlyLabel !== null && showPlyLabel !== void 0 ? showPlyLabel : !showScrubber,
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
const DefaultAnalysisSidebar = ({ historyRows, isHistoryRowSelected, onSelectHistoryRow, ply, maxPly, onSelectPly, theme, engineEvaluationPanel, }) => {
|
|
1390
|
+
const rowBands = createSidebarRowBandCounters();
|
|
1391
|
+
const baseChipStyle = {
|
|
1392
|
+
cursor: 'pointer',
|
|
1393
|
+
padding: '4px 8px',
|
|
1394
|
+
borderRadius: 4,
|
|
1395
|
+
};
|
|
1396
|
+
return (jsxs("div", { style: sidebarStyle, children: [jsxs("div", { style: navBlockStyle, children: [jsx(PlyNavigation, { plyIndex: ply, totalPly: maxPly, canPrev: ply > 0, canNext: ply < maxPly, onGoFirst: () => onSelectPly(0), onGoPrev: () => onSelectPly(ply - 1), onGoNext: () => onSelectPly(ply + 1), onGoLast: () => onSelectPly(maxPly), onGoTo: onSelectPly, theme: theme, showScrubber: false }), jsx("p", { style: sectionTitleStyle, children: "Move history" })] }), jsxs("div", { style: contentRowStyle, children: [jsx("ol", { style: moveListStyle, children: historyRows.length === 0 ? (jsx("li", { style: emptyRowStyle, children: "No moves played yet." })) : (historyRows.map((row) => {
|
|
1397
|
+
const isSelected = isHistoryRowSelected(row);
|
|
1398
|
+
const isVariation = row.kind === 'variation';
|
|
1399
|
+
const backgroundColor = isSelected
|
|
1400
|
+
? analysisSidebarColors.activeMove[theme]
|
|
1401
|
+
: getSidebarRowBackground(theme, row, rowBands);
|
|
1402
|
+
return (jsx("li", { style: Object.assign(Object.assign({}, baseChipStyle), { backgroundColor, marginLeft: row.indent * 16 }), onClick: () => onSelectHistoryRow(row), children: jsx("span", { style: {
|
|
1403
|
+
fontWeight: isSelected ? 600 : undefined,
|
|
1404
|
+
fontStyle: isVariation ? 'italic' : 'normal',
|
|
1405
|
+
fontFamily: isVariation
|
|
1406
|
+
? 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
|
|
1407
|
+
: 'inherit',
|
|
1408
|
+
fontSize: isVariation ? 13 : 14,
|
|
1409
|
+
color: isSelected
|
|
1410
|
+
? '#fff'
|
|
1411
|
+
: isVariation
|
|
1412
|
+
? analysisSidebarColors.variationText[theme]
|
|
1413
|
+
: undefined,
|
|
1414
|
+
}, children: row.label }) }, row.key));
|
|
1415
|
+
})) }), engineEvaluationPanel ? (jsx("div", { style: enginePanelStyle, children: engineEvaluationPanel })) : null] }), jsx("p", { style: footerStyle, children: "Drag pieces to explore lines. Select a main-line move to return to the game." })] }));
|
|
1416
|
+
};
|
|
1417
|
+
const contentRowStyle = {
|
|
1418
|
+
display: 'flex',
|
|
1419
|
+
flex: 1,
|
|
1420
|
+
minHeight: 0,
|
|
1421
|
+
gap: 12,
|
|
1422
|
+
alignItems: 'stretch',
|
|
1423
|
+
padding: '0 8px 8px',
|
|
1424
|
+
};
|
|
1425
|
+
const enginePanelStyle = {
|
|
1426
|
+
flexShrink: 0,
|
|
1427
|
+
width: 220,
|
|
1428
|
+
minWidth: 220,
|
|
1429
|
+
paddingLeft: 12,
|
|
1430
|
+
borderLeft: '1px solid rgba(128, 128, 128, 0.35)',
|
|
1431
|
+
overflowY: 'auto',
|
|
1432
|
+
maxHeight: 400,
|
|
1433
|
+
};
|
|
1434
|
+
const sidebarStyle = {
|
|
1435
|
+
width: '100%',
|
|
1436
|
+
height: '100%',
|
|
1437
|
+
display: 'flex',
|
|
1438
|
+
flexDirection: 'column',
|
|
1439
|
+
minHeight: 0,
|
|
1440
|
+
maxHeight: 480,
|
|
1441
|
+
overflow: 'hidden',
|
|
1442
|
+
border: '1px solid rgba(128, 128, 128, 0.35)',
|
|
1443
|
+
borderRadius: 4,
|
|
1444
|
+
boxSizing: 'border-box',
|
|
1445
|
+
};
|
|
1446
|
+
const navBlockStyle = {
|
|
1447
|
+
flexShrink: 0,
|
|
1448
|
+
padding: '12px 12px 8px',
|
|
1449
|
+
};
|
|
1450
|
+
const sectionTitleStyle = {
|
|
1451
|
+
margin: '8px 0 0',
|
|
1452
|
+
fontSize: 14,
|
|
1453
|
+
fontWeight: 600,
|
|
1454
|
+
};
|
|
1455
|
+
const emptyRowStyle = {
|
|
1456
|
+
listStyle: 'none',
|
|
1457
|
+
padding: '4px 8px',
|
|
1458
|
+
fontSize: 14,
|
|
1459
|
+
color: '#666',
|
|
1460
|
+
};
|
|
1461
|
+
const footerStyle = {
|
|
1462
|
+
flexShrink: 0,
|
|
1463
|
+
margin: 0,
|
|
1464
|
+
padding: '0 12px 12px',
|
|
1465
|
+
fontSize: 12,
|
|
1466
|
+
color: '#888',
|
|
1467
|
+
};
|
|
1468
|
+
const moveListStyle = {
|
|
1469
|
+
listStyle: 'none',
|
|
1470
|
+
margin: 0,
|
|
1471
|
+
padding: 0,
|
|
1472
|
+
flex: 1,
|
|
1473
|
+
minWidth: 0,
|
|
1474
|
+
display: 'flex',
|
|
1475
|
+
flexDirection: 'column',
|
|
1476
|
+
gap: 4,
|
|
1477
|
+
overflowY: 'auto',
|
|
1478
|
+
maxHeight: 400,
|
|
1479
|
+
};
|
|
1480
|
+
|
|
1481
|
+
const createDefaultRenderMain = (layout) => (props) => (jsx(AnalysisBoardLayout, { layout: layout, model: props.model, board: props.board, sidebar: props.sidebar }));
|
|
1482
|
+
/**
|
|
1483
|
+
* Full analysis UI with library defaults: modal shell, grid layout, sidebar, engine panel.
|
|
1484
|
+
* For host-owned UI only, use {@link AnalysisBoardCore} and pass all render props.
|
|
1485
|
+
*/
|
|
1486
|
+
const AnalysisBoard = (_a) => {
|
|
1487
|
+
var _b;
|
|
1488
|
+
var { layout = DEFAULT_ANALYSIS_LAYOUT, renderContainer, renderMain, renderSidebar, renderEngineEvaluation, engine } = _a, modelArgs = __rest(_a, ["layout", "renderContainer", "renderMain", "renderSidebar", "renderEngineEvaluation", "engine"]);
|
|
1489
|
+
const engineEnabled = (_b = engine === null || engine === void 0 ? void 0 : engine.enabled) !== null && _b !== void 0 ? _b : true;
|
|
1490
|
+
return (jsx(AnalysisBoardCore, Object.assign({}, modelArgs, { boardWidth: layout.boardWidth, engine: engine, renderContainer: renderContainer !== null && renderContainer !== void 0 ? renderContainer : DefaultAnalysisContainer, renderMain: renderMain !== null && renderMain !== void 0 ? renderMain : createDefaultRenderMain(layout), renderSidebar: renderSidebar !== null && renderSidebar !== void 0 ? renderSidebar : DefaultAnalysisSidebar, renderEngineEvaluation: renderEngineEvaluation !== null && renderEngineEvaluation !== void 0 ? renderEngineEvaluation : (engineEnabled
|
|
1491
|
+
? (props) => jsx(EngineEvaluationPanel, Object.assign({}, props))
|
|
1492
|
+
: () => null) })));
|
|
1493
|
+
};
|
|
1494
|
+
|
|
1495
|
+
export { AnalysisBoard, AnalysisBoardCore, AnalysisBoardCoreView, AnalysisBoardLayout, AnalysisChessboardView, AnalysisErrorBoundary, AnalysisPosition, ChessboardThemeContext, DEFAULT_ANALYSIS_LAYOUT, DEFAULT_STOCKFISH_SCRIPT_URL, DefaultAnalysisContainer, DefaultAnalysisSidebar, DefaultPlyNavigation, EngineEvaluationPanel, HighlightChessboard, PlyNavigation, StockfishBrowserEngine, ThemeProvider, analysisBoardHighlightColors, analysisSidebarColors, applyUciMove, boardSquareHighlightColors, createSidebarRowBandCounters, defaultRenderPlyNavigation, emptyEngineEvaluation, formatEvaluation, formatPvPreview, getAnalysisModalStyles, getCheckSquareFromChess, getLastMoveSquareStyles, getSidebarRowBackground, getStylesForTheme, isAnalyzableFen, navButtonStyle, navRowStyle, normalizeEvalForWhite, normalizePvMoves, parseUciInfoLine, parseUciMove, plyLabelStyle, palette as plyNavigationPalette, resolveStockfishScriptUrl, resolveStockfishWasmUrl, resolveStockfishWorkerUrl, scrubberInputStyle, splitWorkerLines, uciPvToSan, useAnalysisBoardModel, useAnalysisEngine, useChessboardTheme, useTheme };
|