@connectorvol/chess-widgets 8.0.0 → 9.0.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/dist/constants/default-board-appearance-settings.d.ts +9 -0
- package/dist/constants/default-board-appearance-settings.js +30 -0
- package/dist/constants/editable-board-settings.d.ts +3 -21
- package/dist/constants/editable-board-settings.js +24 -19
- package/dist/game-analyzer/GameAnalyzer.svelte +38 -19
- package/dist/game-analyzer/gameAnalyzer.svelte.js +9 -7
- package/dist/game-analyzer/types.d.ts +10 -2
- package/dist/index.d.ts +9 -1
- package/dist/index.js +6 -0
- package/dist/position-editor/EditPanel.svelte +9 -6
- package/dist/puzzle/puzzleCreatedPayload.d.ts +17 -0
- package/dist/puzzle/puzzleData.d.ts +4 -0
- package/dist/puzzle/puzzleData.js +10 -0
- package/dist/puzzle/puzzlePreviewConstants.d.ts +4 -0
- package/dist/puzzle/puzzlePreviewConstants.js +4 -0
- package/dist/puzzle/puzzlePreviewPathNags.d.ts +6 -0
- package/dist/puzzle/puzzlePreviewPathNags.js +35 -0
- package/dist/puzzle/puzzleSolverForkAnnotations.d.ts +2 -4
- package/dist/puzzle/puzzleSolverForkAnnotations.js +13 -22
- package/dist/puzzle/puzzleStepPreviewSolver.d.ts +13 -1
- package/dist/puzzle/puzzleStepPreviewSolver.js +69 -9
- package/dist/puzzle/syncPuzzleBranchNags.d.ts +14 -0
- package/dist/puzzle/syncPuzzleBranchNags.js +125 -0
- package/dist/puzzle-creation/OpeningTagHoverPreview.svelte +81 -0
- package/dist/puzzle-creation/OpeningTagHoverPreview.svelte.d.ts +11 -0
- package/dist/puzzle-creation/PuzzleBoardTreeViewerPane.svelte +104 -32
- package/dist/puzzle-creation/PuzzleBoardTreeViewerPane.svelte.d.ts +16 -2
- package/dist/puzzle-creation/PuzzleCreationWizard.svelte +192 -202
- package/dist/puzzle-creation/PuzzleCreationWizard.svelte.d.ts +47 -3
- package/dist/puzzle-creation/PuzzlePgnBoardTreeEditor.svelte +485 -76
- package/dist/puzzle-creation/PuzzleWizardTagsStep.svelte +36 -0
- package/dist/puzzle-creation/PuzzleWizardTagsStep.svelte.d.ts +12 -0
- package/dist/puzzle-creation/StepMoves.svelte +38 -18
- package/dist/puzzle-creation/StepMoves.svelte.d.ts +2 -1
- package/dist/puzzle-creation/StepPosition.svelte +15 -9
- package/dist/puzzle-creation/StepPreview.svelte +23 -10
- package/dist/puzzle-creation/StepPreview.svelte.d.ts +2 -0
- package/dist/puzzle-creation/StepTags.svelte +270 -0
- package/dist/puzzle-creation/StepTags.svelte.d.ts +19 -0
- package/dist/puzzle-creation/buildPuzzleWizardBoardSettings.d.ts +9 -0
- package/dist/puzzle-creation/buildPuzzleWizardBoardSettings.js +19 -0
- package/dist/puzzle-creation/createPuzzleLineEditingBoard.d.ts +10 -3
- package/dist/puzzle-creation/createPuzzleLineEditingBoard.js +15 -11
- package/dist/puzzle-creation/puzzleWizardState.d.ts +35 -0
- package/dist/puzzle-creation/puzzleWizardState.js +37 -0
- package/dist/puzzle-creation/types.d.ts +35 -5
- package/package.json +20 -17
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
import {
|
|
13
13
|
CHESSBOARD_THEMES,
|
|
14
14
|
createBoardApi,
|
|
15
|
-
DEFAULT_BOARD_SETTINGS,
|
|
16
15
|
Draggable,
|
|
17
16
|
syncMoveEvaluationNagBadge,
|
|
18
17
|
} from "@connectorvol/chessboard";
|
|
@@ -24,29 +23,47 @@
|
|
|
24
23
|
|
|
25
24
|
import { INITIAL_FEN } from "@connectorvol/chessops/fen";
|
|
26
25
|
import { PgnOps } from "@connectorvol/chessops/pgnOps.svelte";
|
|
27
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
Color,
|
|
28
|
+
calculatePly,
|
|
29
|
+
type Move,
|
|
30
|
+
PUZZLE_BRANCH_CORRECT_NAG_ID,
|
|
31
|
+
PUZZLE_BRANCH_WRONG_NAG_ID,
|
|
32
|
+
isPuzzleBranchNag,
|
|
33
|
+
} from "@connectorvol/shared";
|
|
28
34
|
|
|
29
35
|
import { mergePgnWithSetupFen } from "../puzzle/mergePgnWithSetupFen.js";
|
|
30
|
-
import { createEmptyTreeFromFen } from "../puzzle/puzzleData.js";
|
|
31
|
-
import { PREVIEW_WRONG_NO_VARIANT_MESSAGE } from "../puzzle/puzzlePreviewConstants.js";
|
|
32
36
|
import {
|
|
37
|
+
createEmptyTreeFromFen,
|
|
38
|
+
createStudentPreviewTreeFromFen,
|
|
39
|
+
} from "../puzzle/puzzleData.js";
|
|
40
|
+
import { PREVIEW_WRONG_MOVE_REVEAL_MS, PREVIEW_WRONG_NO_VARIANT_MESSAGE } from "../puzzle/puzzlePreviewConstants.js";
|
|
41
|
+
import { normalizePuzzleTreeMainLine, syncPuzzleBranchNagsOnTree } from "../puzzle/syncPuzzleBranchNags.js";
|
|
42
|
+
import { applyPlayedSolverNagsAlongPath } from "../puzzle/puzzlePreviewPathNags.js";
|
|
43
|
+
import {
|
|
44
|
+
puzzlePreviewAddAdHocWrongMoveToSolutionTree,
|
|
33
45
|
puzzlePreviewClassifySolverMove,
|
|
34
46
|
puzzlePreviewDupSubtreeUnderStudentCursor,
|
|
35
47
|
puzzlePreviewIsSolvedPosition,
|
|
36
48
|
puzzlePreviewNodeAtPath,
|
|
49
|
+
puzzlePreviewRemapPathAfterReorder,
|
|
37
50
|
puzzlePreviewSideToMoveFromFen,
|
|
38
51
|
type TSolverMoveOutcome,
|
|
39
52
|
} from "../puzzle/puzzleStepPreviewSolver.js";
|
|
40
53
|
import PuzzleBoardTreeViewerPane from "./PuzzleBoardTreeViewerPane.svelte";
|
|
41
54
|
import { cn } from "../utils.js";
|
|
42
55
|
import type { TPuzzlePgnBoardTreeEditorProps } from "./types.js";
|
|
56
|
+
import { DEFAULT_BOARD_APPEARANCE_SETTINGS } from "../constants/default-board-appearance-settings.js";
|
|
57
|
+
import { buildPuzzleWizardBoardSettings } from "./buildPuzzleWizardBoardSettings.js";
|
|
43
58
|
|
|
44
59
|
let {
|
|
45
60
|
pgn = "",
|
|
46
61
|
fen: fenProp,
|
|
47
62
|
onOutcome,
|
|
48
63
|
boardTheme,
|
|
49
|
-
boardAppearanceSettings,
|
|
64
|
+
boardAppearanceSettings = DEFAULT_BOARD_APPEARANCE_SETTINGS,
|
|
65
|
+
boardSize,
|
|
66
|
+
onResizeAction,
|
|
50
67
|
class: className,
|
|
51
68
|
wrongFeedback = $bindable<string | null>(null),
|
|
52
69
|
solved = $bindable(false),
|
|
@@ -66,8 +83,28 @@
|
|
|
66
83
|
|
|
67
84
|
let cursorPath = $state<number[]>([]);
|
|
68
85
|
|
|
86
|
+
const OPPONENT_MOVE_ANIMATION_MS = 300;
|
|
87
|
+
|
|
69
88
|
let wrongRevealTimer: ReturnType<typeof setTimeout> | null = null;
|
|
70
89
|
let advanceLineTimer: ReturnType<typeof setTimeout> | null = null;
|
|
90
|
+
let opponentAutoTimer: ReturnType<typeof setTimeout> | null = null;
|
|
91
|
+
|
|
92
|
+
/** Представляет отложенный откат ошибочного хода на доске после показа NAG ✗. */
|
|
93
|
+
type TPendingWrongMoveReveal = {
|
|
94
|
+
/** Возвращает классификацию хода решателя. */
|
|
95
|
+
outcome: TSolverMoveOutcome;
|
|
96
|
+
/** Возвращает сыгранный SAN. */
|
|
97
|
+
playedSan: string;
|
|
98
|
+
/** Возвращает FEN позиции до ошибочного хода. */
|
|
99
|
+
revertFen: string;
|
|
100
|
+
/** Возвращает последний ход до ошибочного хода (для подсветки на доске). */
|
|
101
|
+
revertLastMove: Move | null;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
let pendingWrongMoveReveal = $state<TPendingWrongMoveReveal | null>(null);
|
|
105
|
+
|
|
106
|
+
/** Представляет последний применённый ход решателя до колбэка `afterPieceMove` (drag-and-drop). */
|
|
107
|
+
let lastSolverMoveForBoard: Move | null = null;
|
|
71
108
|
|
|
72
109
|
let previewChess = new PgnOps(INITIAL_FEN, "chess");
|
|
73
110
|
|
|
@@ -79,6 +116,11 @@
|
|
|
79
116
|
* Представляет синхронизацию доступности перетаскивания с очередью хода и состоянием «решено».
|
|
80
117
|
*/
|
|
81
118
|
function syncBoardDragFromChessTurn(): void {
|
|
119
|
+
if (pendingWrongMoveReveal !== null) {
|
|
120
|
+
chessboard.draggable = Draggable.NONE;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
82
124
|
const solver = puzzleSolverColor();
|
|
83
125
|
chessboard.draggable = solved
|
|
84
126
|
? previewChess.turn() === Color.WHITE
|
|
@@ -91,6 +133,137 @@
|
|
|
91
133
|
: Draggable.NONE;
|
|
92
134
|
}
|
|
93
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Представляет отмену отложенного отката ошибочного хода без записи варианта в дерево.
|
|
138
|
+
*/
|
|
139
|
+
function dismissPendingWrongMoveReveal(): void {
|
|
140
|
+
if (wrongRevealTimer !== null) {
|
|
141
|
+
clearTimeout(wrongRevealTimer);
|
|
142
|
+
wrongRevealTimer = null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const pending = pendingWrongMoveReveal;
|
|
146
|
+
if (pending === null) return;
|
|
147
|
+
|
|
148
|
+
pendingWrongMoveReveal = null;
|
|
149
|
+
previewChess.setFen(pending.revertFen);
|
|
150
|
+
chessboard.fen = pending.revertFen;
|
|
151
|
+
chessboard.addLastMove(pending.revertLastMove);
|
|
152
|
+
syncMoveEvaluationNagBadge(chessboard, {
|
|
153
|
+
nags: undefined,
|
|
154
|
+
lastMove: pending.revertLastMove,
|
|
155
|
+
});
|
|
156
|
+
syncBoardDragFromChessTurn();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Представляет досрочное завершение показа ошибочного хода перед следующим ходом решателя.
|
|
161
|
+
*/
|
|
162
|
+
function cancelPendingWrongMoveReveal(): void {
|
|
163
|
+
if (pendingWrongMoveReveal === null) return;
|
|
164
|
+
|
|
165
|
+
if (wrongRevealTimer !== null) {
|
|
166
|
+
clearTimeout(wrongRevealTimer);
|
|
167
|
+
wrongRevealTimer = null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
finishWrongMoveReveal();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Представляет показ NAG ✗ на доске и отложенный откат ошибочного хода.
|
|
175
|
+
*/
|
|
176
|
+
function scheduleWrongMoveReveal(wrongMove: Move): void {
|
|
177
|
+
const pending = pendingWrongMoveReveal;
|
|
178
|
+
if (pending === null) return;
|
|
179
|
+
|
|
180
|
+
syncMoveEvaluationNagBadge(chessboard, {
|
|
181
|
+
nags: [PUZZLE_BRANCH_WRONG_NAG_ID],
|
|
182
|
+
lastMove: wrongMove,
|
|
183
|
+
});
|
|
184
|
+
syncBoardDragFromChessTurn();
|
|
185
|
+
|
|
186
|
+
wrongRevealTimer = setTimeout(() => {
|
|
187
|
+
wrongRevealTimer = null;
|
|
188
|
+
finishWrongMoveReveal();
|
|
189
|
+
}, PREVIEW_WRONG_MOVE_REVEAL_MS);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Представляет завершение показа ошибочного хода: запись варианта в дерево и откат позиции на доске.
|
|
194
|
+
*/
|
|
195
|
+
function finishWrongMoveReveal(): void {
|
|
196
|
+
const pending = pendingWrongMoveReveal;
|
|
197
|
+
pendingWrongMoveReveal = null;
|
|
198
|
+
if (pending === null) return;
|
|
199
|
+
|
|
200
|
+
previewChess.setFen(pending.revertFen);
|
|
201
|
+
handleWrongOutcome(pending.outcome, pending.playedSan);
|
|
202
|
+
|
|
203
|
+
chessboard.fen = pending.revertFen;
|
|
204
|
+
chessboard.addLastMove(pending.revertLastMove);
|
|
205
|
+
syncMoveEvaluationNagBadge(chessboard, {
|
|
206
|
+
nags: undefined,
|
|
207
|
+
lastMove: pending.revertLastMove,
|
|
208
|
+
});
|
|
209
|
+
syncBoardDragFromChessTurn();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Представляет синхронизацию NAG ✓ на доске после верного хода решателя.
|
|
214
|
+
*/
|
|
215
|
+
function syncCorrectSolverMoveNagBadge(move: Move): void {
|
|
216
|
+
syncMoveEvaluationNagBadge(chessboard, {
|
|
217
|
+
nags: [PUZZLE_BRANCH_CORRECT_NAG_ID],
|
|
218
|
+
lastMove: move,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Представляет данные узла для бейджа NAG на доске в режиме свободного анализа (без ✓/✗ задачи).
|
|
224
|
+
*/
|
|
225
|
+
function nodeDataForAnalysisBoardBadge(data: {
|
|
226
|
+
nags?: number[];
|
|
227
|
+
lastMove?: Move | null;
|
|
228
|
+
}): { nags?: number[]; lastMove?: Move | null } {
|
|
229
|
+
const nags = data.nags?.filter((n) => !isPuzzleBranchNag(n));
|
|
230
|
+
return {
|
|
231
|
+
nags: nags && nags.length > 0 ? nags : undefined,
|
|
232
|
+
lastMove: data.lastMove ?? null,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Представляет синхронизацию бейджей NAG и маркеров доски с текущим узлом дерева решения (свободный анализ).
|
|
238
|
+
*/
|
|
239
|
+
function syncAnalysisBoardFromCurrentNode(): void {
|
|
240
|
+
const tree = solutionPreviewTree;
|
|
241
|
+
if (!tree) return;
|
|
242
|
+
|
|
243
|
+
syncMoveEvaluationNagBadge(
|
|
244
|
+
chessboard,
|
|
245
|
+
nodeDataForAnalysisBoardBadge(tree.currentNode.data),
|
|
246
|
+
);
|
|
247
|
+
chessboard.setNodeMarkers(tree.currentNode.data.parsedFirstComment?.shapes);
|
|
248
|
+
syncBoardDragFromChessTurn();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Представляет обработку UI после применения хода решателя на доске.
|
|
253
|
+
*/
|
|
254
|
+
function afterSolverMoveOnBoard(move: Move): void {
|
|
255
|
+
if (pendingWrongMoveReveal !== null) {
|
|
256
|
+
scheduleWrongMoveReveal(move);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
syncCorrectSolverMoveNagBadge(move);
|
|
261
|
+
syncBoardDragFromChessTurn();
|
|
262
|
+
if (!solved) {
|
|
263
|
+
scheduleOpponentAutoResponse();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
94
267
|
/**
|
|
95
268
|
* Представляет добавление пути в дерево ученика без очистки уже пройденных линий.
|
|
96
269
|
*/
|
|
@@ -101,22 +274,106 @@
|
|
|
101
274
|
studentPreviewTree.currentNode = studentPreviewTree.rootNode.moves;
|
|
102
275
|
|
|
103
276
|
let src = solution.rootNode.moves;
|
|
277
|
+
const solver = puzzleSolverColor();
|
|
278
|
+
|
|
279
|
+
applyPlayedSolverNagsAlongPath(solution, path, solver);
|
|
280
|
+
|
|
104
281
|
for (const idx of path) {
|
|
105
282
|
const next = src.children[idx];
|
|
106
283
|
if (!next) break;
|
|
284
|
+
|
|
285
|
+
const studentNags = next.data.nags ? [...next.data.nags] : undefined;
|
|
286
|
+
|
|
287
|
+
const existingChild = studentPreviewTree.currentNode.children.find(
|
|
288
|
+
(c) => c.data.san === next.data.san,
|
|
289
|
+
);
|
|
290
|
+
if (existingChild) {
|
|
291
|
+
existingChild.data.nags = studentNags;
|
|
292
|
+
}
|
|
293
|
+
|
|
107
294
|
studentPreviewTree.addNodeToCurrent({
|
|
108
295
|
id: "",
|
|
109
296
|
children: [],
|
|
110
|
-
data: {
|
|
297
|
+
data: {
|
|
298
|
+
...next.data,
|
|
299
|
+
nags: studentNags,
|
|
300
|
+
},
|
|
111
301
|
});
|
|
112
302
|
src = next;
|
|
113
303
|
}
|
|
304
|
+
|
|
305
|
+
const solverColor = puzzleSolverColor();
|
|
306
|
+
normalizePuzzleTreeMainLine(studentPreviewTree.rootNode.moves, solverColor);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Представляет нормализацию главной линии и NAG только в дереве решения (PGN).
|
|
311
|
+
*/
|
|
312
|
+
function normalizeSolutionTree(): void {
|
|
313
|
+
const solution = solutionPreviewTree;
|
|
314
|
+
if (!solution) return;
|
|
315
|
+
|
|
316
|
+
const solverColor = puzzleSolverColor();
|
|
317
|
+
if (normalizePuzzleTreeMainLine(solution.rootNode.moves, solverColor)) {
|
|
318
|
+
solution.mutationVersion++;
|
|
319
|
+
}
|
|
320
|
+
cursorPath = puzzlePreviewRemapPathAfterReorder(
|
|
321
|
+
solution.rootNode.moves,
|
|
322
|
+
cursorPath,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Представляет синхронизацию дерева ученика с пройденным путём решения (после верного хода).
|
|
328
|
+
*/
|
|
329
|
+
function syncStudentTreeFromSolutionPath(path: number[]): void {
|
|
330
|
+
ensureStudentPreviewTreeHasPath(path);
|
|
331
|
+
studentPreviewTree.forcedNodeId = null;
|
|
332
|
+
studentPreviewTree.mutationVersion++;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Представляет добавление отсутствующего в PGN хода в дерево решения и дерево ученика с NAG ✗.
|
|
337
|
+
*/
|
|
338
|
+
function addAdHocWrongMoveToTrees(playedSan: string): boolean {
|
|
339
|
+
const solution = solutionPreviewTree;
|
|
340
|
+
if (!solution) return false;
|
|
341
|
+
|
|
342
|
+
const addedNode = puzzlePreviewAddAdHocWrongMoveToSolutionTree(
|
|
343
|
+
solution,
|
|
344
|
+
cursorPath,
|
|
345
|
+
previewChess.fen(),
|
|
346
|
+
playedSan,
|
|
347
|
+
);
|
|
348
|
+
if (!addedNode) return false;
|
|
349
|
+
|
|
350
|
+
normalizeSolutionTree();
|
|
351
|
+
|
|
352
|
+
ensureStudentPreviewTreeHasPath(cursorPath);
|
|
353
|
+
const studentFork = studentPreviewTree.currentNode;
|
|
354
|
+
studentPreviewTree.addNodeToCurrent({
|
|
355
|
+
id: "",
|
|
356
|
+
children: [],
|
|
357
|
+
data: { ...addedNode.data },
|
|
358
|
+
});
|
|
359
|
+
studentPreviewTree.forcedNodeId = studentFork.id;
|
|
360
|
+
studentPreviewTree.currentNode = studentFork;
|
|
361
|
+
|
|
362
|
+
const solverColor = puzzleSolverColor();
|
|
363
|
+
normalizePuzzleTreeMainLine(studentPreviewTree.rootNode.moves, solverColor);
|
|
364
|
+
|
|
365
|
+
studentPreviewTree.mutationVersion++;
|
|
366
|
+
|
|
367
|
+
return true;
|
|
114
368
|
}
|
|
115
369
|
|
|
116
370
|
/**
|
|
117
371
|
* Представляет обработку ошибочного хода в превью.
|
|
118
372
|
*/
|
|
119
|
-
function handleWrongOutcome(
|
|
373
|
+
function handleWrongOutcome(
|
|
374
|
+
outcome: TSolverMoveOutcome,
|
|
375
|
+
playedSan: string,
|
|
376
|
+
): void {
|
|
120
377
|
const solution = solutionPreviewTree;
|
|
121
378
|
if (!solution) return;
|
|
122
379
|
|
|
@@ -128,16 +385,40 @@
|
|
|
128
385
|
clearTimeout(advanceLineTimer);
|
|
129
386
|
advanceLineTimer = null;
|
|
130
387
|
}
|
|
388
|
+
if (opponentAutoTimer !== null) {
|
|
389
|
+
clearTimeout(opponentAutoTimer);
|
|
390
|
+
opponentAutoTimer = null;
|
|
391
|
+
}
|
|
131
392
|
|
|
132
393
|
if (outcome.kind === "wrong_marked_variant") {
|
|
133
394
|
wrongFeedback =
|
|
134
395
|
"Неверно. Открываю введённый вариант в дереве, ход отменён.";
|
|
135
396
|
onOutcome?.("failed");
|
|
136
397
|
ensureStudentPreviewTreeHasPath(cursorPath);
|
|
398
|
+
const studentFork = studentPreviewTree.currentNode;
|
|
137
399
|
puzzlePreviewDupSubtreeUnderStudentCursor(
|
|
138
400
|
studentPreviewTree,
|
|
139
401
|
outcome.wrongBranchRoot,
|
|
140
402
|
);
|
|
403
|
+
studentPreviewTree.forcedNodeId = studentFork.id;
|
|
404
|
+
studentPreviewTree.currentNode = studentFork;
|
|
405
|
+
|
|
406
|
+
const solverColor = puzzleSolverColor();
|
|
407
|
+
normalizePuzzleTreeMainLine(studentPreviewTree.rootNode.moves, solverColor);
|
|
408
|
+
|
|
409
|
+
studentPreviewTree.mutationVersion++;
|
|
410
|
+
normalizeSolutionTree();
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (outcome.kind === "no_matching_variant") {
|
|
415
|
+
if (addAdHocWrongMoveToTrees(playedSan)) {
|
|
416
|
+
wrongFeedback =
|
|
417
|
+
"Неверно. Ход добавлен в дерево вариантов, позиция на доске отменена.";
|
|
418
|
+
} else {
|
|
419
|
+
wrongFeedback = PREVIEW_WRONG_NO_VARIANT_MESSAGE;
|
|
420
|
+
}
|
|
421
|
+
onOutcome?.("failed");
|
|
141
422
|
return;
|
|
142
423
|
}
|
|
143
424
|
|
|
@@ -149,6 +430,8 @@
|
|
|
149
430
|
* Представляет установку позиции и дерева ученика по пути из корня.
|
|
150
431
|
*/
|
|
151
432
|
function setStateFromPath(path: number[]): void {
|
|
433
|
+
dismissPendingWrongMoveReveal();
|
|
434
|
+
|
|
152
435
|
const solution = solutionPreviewTree;
|
|
153
436
|
if (!solution) return;
|
|
154
437
|
|
|
@@ -162,30 +445,123 @@
|
|
|
162
445
|
}
|
|
163
446
|
|
|
164
447
|
cursorPath = path;
|
|
165
|
-
|
|
448
|
+
syncStudentTreeFromSolutionPath(path);
|
|
166
449
|
chessboard.fen = previewChess.fen();
|
|
450
|
+
const leaf = puzzlePreviewNodeAtPath(solution.rootNode.moves, path);
|
|
451
|
+
chessboard.addLastMove(leaf?.data.lastMove ?? null);
|
|
452
|
+
syncMoveEvaluationNagBadge(chessboard, {
|
|
453
|
+
nags: undefined,
|
|
454
|
+
lastMove: leaf?.data.lastMove ?? null,
|
|
455
|
+
});
|
|
167
456
|
syncBoardDragFromChessTurn();
|
|
168
457
|
}
|
|
169
458
|
|
|
170
459
|
/**
|
|
171
|
-
* Представляет
|
|
460
|
+
* Представляет ответ соперника по главному варианту после хода решателя (без применения к движку).
|
|
172
461
|
*/
|
|
173
|
-
function
|
|
462
|
+
function getOpponentMainLineReply(
|
|
463
|
+
pathAfterSolver: number[],
|
|
464
|
+
): { san: string; path: number[] } | null {
|
|
174
465
|
const solution = solutionPreviewTree;
|
|
175
|
-
if (!solution) return
|
|
466
|
+
if (!solution) return null;
|
|
176
467
|
|
|
177
468
|
if (puzzlePreviewSideToMoveFromFen(previewChess.fen()) === puzzleSolverColor()) {
|
|
178
|
-
return
|
|
469
|
+
return null;
|
|
179
470
|
}
|
|
180
471
|
|
|
181
472
|
const node = puzzlePreviewNodeAtPath(
|
|
182
473
|
solution.rootNode.moves,
|
|
183
474
|
pathAfterSolver,
|
|
184
475
|
);
|
|
185
|
-
if (!node || node.children.length === 0) return
|
|
476
|
+
if (!node || node.children.length === 0) return null;
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
san: node.children[0]!.data.san,
|
|
480
|
+
path: [...pathAfterSolver, 0],
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Представляет проверку конца линии и переход к следующему варианту ответа соперника.
|
|
486
|
+
*/
|
|
487
|
+
function checkSolvedAndMaybeAdvance(): void {
|
|
488
|
+
const solution = solutionPreviewTree;
|
|
489
|
+
if (!solution) return;
|
|
490
|
+
|
|
491
|
+
const leaf = puzzlePreviewNodeAtPath(solution.rootNode.moves, cursorPath);
|
|
492
|
+
if (
|
|
493
|
+
leaf &&
|
|
494
|
+
puzzlePreviewIsSolvedPosition(previewChess, leaf, puzzleSolverColor())
|
|
495
|
+
) {
|
|
496
|
+
if (advanceLineTimer !== null) {
|
|
497
|
+
clearTimeout(advanceLineTimer);
|
|
498
|
+
advanceLineTimer = null;
|
|
499
|
+
}
|
|
500
|
+
advanceLineTimer = setTimeout(() => {
|
|
501
|
+
const advanced = maybeAdvanceToNextOpponentLine();
|
|
502
|
+
if (!advanced) {
|
|
503
|
+
const target = puzzlePreviewNodeAtPath(
|
|
504
|
+
solution.rootNode.moves,
|
|
505
|
+
cursorPath,
|
|
506
|
+
);
|
|
507
|
+
solution.currentNode = target ?? solution.rootNode.moves;
|
|
508
|
+
solved = true;
|
|
509
|
+
onOutcome?.("solved");
|
|
510
|
+
syncBoardFromSolutionTreeNode();
|
|
511
|
+
}
|
|
512
|
+
syncBoardDragFromChessTurn();
|
|
513
|
+
advanceLineTimer = null;
|
|
514
|
+
}, 250);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Представляет анимированный автоматический ход соперника после хода решателя.
|
|
520
|
+
*/
|
|
521
|
+
function playOpponentAutoResponse(pathAfterSolver: number[]): void {
|
|
522
|
+
const reply = getOpponentMainLineReply(pathAfterSolver);
|
|
523
|
+
if (!reply) {
|
|
524
|
+
checkSolvedAndMaybeAdvance();
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
let opponentMove: Move;
|
|
529
|
+
try {
|
|
530
|
+
opponentMove = previewChess.makeSanMove(reply.san);
|
|
531
|
+
} catch {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
cursorPath = reply.path;
|
|
536
|
+
syncStudentTreeFromSolutionPath(cursorPath);
|
|
537
|
+
|
|
538
|
+
chessboard.animationTime = OPPONENT_MOVE_ANIMATION_MS;
|
|
539
|
+
chessboard.fen = previewChess.fen();
|
|
540
|
+
chessboard.addLastMove(opponentMove);
|
|
541
|
+
syncMoveEvaluationNagBadge(chessboard, {
|
|
542
|
+
nags: undefined,
|
|
543
|
+
lastMove: opponentMove,
|
|
544
|
+
});
|
|
545
|
+
syncBoardDragFromChessTurn();
|
|
546
|
+
|
|
547
|
+
checkSolvedAndMaybeAdvance();
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Представляет отложенный автоматический ответ соперника после анимации хода решателя.
|
|
552
|
+
*/
|
|
553
|
+
function scheduleOpponentAutoResponse(): void {
|
|
554
|
+
if (opponentAutoTimer !== null) {
|
|
555
|
+
clearTimeout(opponentAutoTimer);
|
|
556
|
+
opponentAutoTimer = null;
|
|
557
|
+
}
|
|
186
558
|
|
|
187
|
-
|
|
188
|
-
|
|
559
|
+
const pathAfterSolver = [...cursorPath];
|
|
560
|
+
const delayMs = chessboard.animationTime || OPPONENT_MOVE_ANIMATION_MS;
|
|
561
|
+
opponentAutoTimer = setTimeout(() => {
|
|
562
|
+
playOpponentAutoResponse(pathAfterSolver);
|
|
563
|
+
opponentAutoTimer = null;
|
|
564
|
+
}, delayMs);
|
|
189
565
|
}
|
|
190
566
|
|
|
191
567
|
/**
|
|
@@ -259,6 +635,8 @@
|
|
|
259
635
|
const solution = solutionPreviewTree;
|
|
260
636
|
if (!solution || solved) return undefined;
|
|
261
637
|
|
|
638
|
+
cancelPendingWrongMoveReveal();
|
|
639
|
+
|
|
262
640
|
const cursorNode = puzzlePreviewNodeAtPath(
|
|
263
641
|
solution.rootNode.moves,
|
|
264
642
|
cursorPath,
|
|
@@ -273,34 +651,34 @@
|
|
|
273
651
|
);
|
|
274
652
|
|
|
275
653
|
if (outcome.kind !== "correct") {
|
|
276
|
-
|
|
277
|
-
|
|
654
|
+
const revertFen = previewChess.fen();
|
|
655
|
+
const revertLastMove = cursorNode.data.lastMove ?? null;
|
|
656
|
+
try {
|
|
657
|
+
const wrongMove = previewChess.makeSanMove(san);
|
|
658
|
+
pendingWrongMoveReveal = {
|
|
659
|
+
outcome,
|
|
660
|
+
playedSan: san,
|
|
661
|
+
revertFen,
|
|
662
|
+
revertLastMove,
|
|
663
|
+
};
|
|
664
|
+
lastSolverMoveForBoard = wrongMove;
|
|
665
|
+
return wrongMove;
|
|
666
|
+
} catch {
|
|
667
|
+
pendingWrongMoveReveal = null;
|
|
668
|
+
wrongFeedback = "Ход недопустим в этой позиции.";
|
|
669
|
+
onOutcome?.("failed");
|
|
670
|
+
return undefined;
|
|
671
|
+
}
|
|
278
672
|
}
|
|
279
673
|
|
|
280
674
|
try {
|
|
281
|
-
|
|
675
|
+
const pathAfterSolver = [...cursorPath, outcome.childIndex];
|
|
282
676
|
const moveRecord = previewChess.makeSanMove(san);
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const leaf = puzzlePreviewNodeAtPath(solution.rootNode.moves, cursorPath);
|
|
287
|
-
if (leaf && puzzlePreviewIsSolvedPosition(previewChess, leaf, puzzleSolverColor())) {
|
|
288
|
-
if (advanceLineTimer !== null) {
|
|
289
|
-
clearTimeout(advanceLineTimer);
|
|
290
|
-
advanceLineTimer = null;
|
|
291
|
-
}
|
|
292
|
-
advanceLineTimer = setTimeout(() => {
|
|
293
|
-
const advanced = maybeAdvanceToNextOpponentLine();
|
|
294
|
-
if (!advanced) {
|
|
295
|
-
solved = true;
|
|
296
|
-
onOutcome?.("solved");
|
|
297
|
-
}
|
|
298
|
-
syncBoardDragFromChessTurn();
|
|
299
|
-
advanceLineTimer = null;
|
|
300
|
-
}, 250);
|
|
301
|
-
}
|
|
677
|
+
cursorPath = pathAfterSolver;
|
|
678
|
+
syncStudentTreeFromSolutionPath(pathAfterSolver);
|
|
302
679
|
|
|
303
680
|
wrongFeedback = null;
|
|
681
|
+
lastSolverMoveForBoard = moveRecord;
|
|
304
682
|
syncBoardDragFromChessTurn();
|
|
305
683
|
|
|
306
684
|
return moveRecord;
|
|
@@ -348,8 +726,13 @@
|
|
|
348
726
|
turn: previewChess.turn(),
|
|
349
727
|
};
|
|
350
728
|
},
|
|
351
|
-
afterPieceMoveSan: () => {
|
|
352
|
-
|
|
729
|
+
afterPieceMoveSan: (move) => {
|
|
730
|
+
lastSolverMoveForBoard = null;
|
|
731
|
+
if (solved) {
|
|
732
|
+
syncAnalysisBoardFromCurrentNode();
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
afterSolverMoveOnBoard(move);
|
|
353
736
|
},
|
|
354
737
|
beforePieceMove: (from, to, promotion) => {
|
|
355
738
|
if (solved) {
|
|
@@ -380,43 +763,46 @@
|
|
|
380
763
|
return previewChess.fen();
|
|
381
764
|
},
|
|
382
765
|
afterPieceMove: () => {
|
|
766
|
+
if (solved) {
|
|
767
|
+
lastSolverMoveForBoard = null;
|
|
768
|
+
syncAnalysisBoardFromCurrentNode();
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
const move = lastSolverMoveForBoard;
|
|
772
|
+
lastSolverMoveForBoard = null;
|
|
773
|
+
if (move) {
|
|
774
|
+
afterSolverMoveOnBoard(move);
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
383
777
|
syncBoardDragFromChessTurn();
|
|
384
778
|
},
|
|
385
779
|
},
|
|
386
780
|
};
|
|
387
781
|
|
|
388
|
-
let chessboard = $derived(
|
|
389
|
-
|
|
782
|
+
let chessboard = $derived.by(() => {
|
|
783
|
+
const merged = buildPuzzleWizardBoardSettings(boardAppearanceSettings);
|
|
784
|
+
return createBoardApi({
|
|
390
785
|
fen: INITIAL_FEN,
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
786
|
+
playSettings:
|
|
787
|
+
boardSize !== undefined
|
|
788
|
+
? { ...merged.play, boardSize }
|
|
789
|
+
: merged.play,
|
|
790
|
+
actions: {
|
|
791
|
+
...actions,
|
|
792
|
+
onResizeAction,
|
|
398
793
|
},
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
const chessboardDesign = $derived({
|
|
798
|
+
...buildPuzzleWizardBoardSettings(boardAppearanceSettings).design,
|
|
799
|
+
theme: boardTheme ?? CHESSBOARD_THEMES.blue,
|
|
800
|
+
});
|
|
403
801
|
|
|
404
802
|
const treeForViewer = $derived(
|
|
405
803
|
solved && solutionPreviewTree ? solutionPreviewTree : studentPreviewTree,
|
|
406
804
|
);
|
|
407
805
|
|
|
408
|
-
/**
|
|
409
|
-
* Представляет синхронизацию выбранного узла в полном дереве решения с пройденной линией при переходе в режим просмотра (`solved`).
|
|
410
|
-
* Без этого после switch с `studentPreviewTree` у `solutionPreviewTree` остаётся `currentNode` у корня и подсветка в TreeViewer неверна.
|
|
411
|
-
*/
|
|
412
|
-
$effect(() => {
|
|
413
|
-
if (!solved || !solutionPreviewTree) return;
|
|
414
|
-
const tree = solutionPreviewTree;
|
|
415
|
-
const path = cursorPath;
|
|
416
|
-
const target = puzzlePreviewNodeAtPath(tree.rootNode.moves, path);
|
|
417
|
-
tree.currentNode = target ?? tree.rootNode.moves;
|
|
418
|
-
});
|
|
419
|
-
|
|
420
806
|
$effect(() => {
|
|
421
807
|
const trimmedPgn = pgn.trim();
|
|
422
808
|
const rootFen = fenProp?.trim() ? fenProp.trim() : INITIAL_FEN;
|
|
@@ -433,6 +819,10 @@
|
|
|
433
819
|
rootFen.trim().split(/\s+/)[1] === "b"
|
|
434
820
|
? Color.BLACK
|
|
435
821
|
: Color.WHITE;
|
|
822
|
+
syncMoveEvaluationNagBadge(chessboard, {
|
|
823
|
+
nags: undefined,
|
|
824
|
+
lastMove: null,
|
|
825
|
+
});
|
|
436
826
|
cursorPath = [];
|
|
437
827
|
solved = false;
|
|
438
828
|
wrongFeedback = null;
|
|
@@ -446,17 +836,28 @@
|
|
|
446
836
|
const { rootNode } = transformPgnToChessNode(fullPgn);
|
|
447
837
|
|
|
448
838
|
layoutInitialFen = rootFen;
|
|
449
|
-
|
|
839
|
+
const solutionTree = new ChessTree(rootNode);
|
|
840
|
+
syncPuzzleBranchNagsOnTree(
|
|
841
|
+
solutionTree,
|
|
842
|
+
rootFen.trim().split(/\s+/)[1] === "b" ? Color.BLACK : Color.WHITE,
|
|
843
|
+
);
|
|
844
|
+
solutionPreviewTree = solutionTree;
|
|
450
845
|
|
|
451
846
|
cursorPath = [];
|
|
452
847
|
solved = false;
|
|
453
848
|
wrongFeedback = null;
|
|
454
849
|
previewChess.setFen(rootFen);
|
|
455
|
-
studentPreviewTree.replaceRootTree(
|
|
850
|
+
studentPreviewTree.replaceRootTree(
|
|
851
|
+
createStudentPreviewTreeFromFen(rootFen, rootNode.comments),
|
|
852
|
+
);
|
|
456
853
|
studentPreviewTree.currentNode = studentPreviewTree.rootNode.moves;
|
|
457
854
|
chessboard.fen = rootFen;
|
|
458
855
|
chessboard.orientation =
|
|
459
856
|
rootFen.trim().split(/\s+/)[1] === "b" ? Color.BLACK : Color.WHITE;
|
|
857
|
+
syncMoveEvaluationNagBadge(chessboard, {
|
|
858
|
+
nags: undefined,
|
|
859
|
+
lastMove: null,
|
|
860
|
+
});
|
|
460
861
|
syncBoardDragFromChessTurn();
|
|
461
862
|
});
|
|
462
863
|
|
|
@@ -467,14 +868,15 @@
|
|
|
467
868
|
* Представляет сброс отложенных таймеров превью.
|
|
468
869
|
*/
|
|
469
870
|
function clearTimers(): void {
|
|
470
|
-
|
|
471
|
-
clearTimeout(wrongRevealTimer);
|
|
472
|
-
wrongRevealTimer = null;
|
|
473
|
-
}
|
|
871
|
+
dismissPendingWrongMoveReveal();
|
|
474
872
|
if (advanceLineTimer !== null) {
|
|
475
873
|
clearTimeout(advanceLineTimer);
|
|
476
874
|
advanceLineTimer = null;
|
|
477
875
|
}
|
|
876
|
+
if (opponentAutoTimer !== null) {
|
|
877
|
+
clearTimeout(opponentAutoTimer);
|
|
878
|
+
opponentAutoTimer = null;
|
|
879
|
+
}
|
|
478
880
|
}
|
|
479
881
|
|
|
480
882
|
/**
|
|
@@ -483,11 +885,17 @@
|
|
|
483
885
|
function syncBoardFromSolutionTreeNode(): void {
|
|
484
886
|
const tree = solutionPreviewTree;
|
|
485
887
|
if (!tree || !solved) return;
|
|
486
|
-
|
|
888
|
+
|
|
889
|
+
const node = tree.currentNode;
|
|
890
|
+
|
|
891
|
+
previewChess.setFen(node.data.fen);
|
|
487
892
|
chessboard.fen = previewChess.fen();
|
|
488
|
-
chessboard.addLastMove(
|
|
489
|
-
syncMoveEvaluationNagBadge(
|
|
490
|
-
|
|
893
|
+
chessboard.addLastMove(node.data.lastMove ?? null);
|
|
894
|
+
syncMoveEvaluationNagBadge(
|
|
895
|
+
chessboard,
|
|
896
|
+
nodeDataForAnalysisBoardBadge(node.data),
|
|
897
|
+
);
|
|
898
|
+
chessboard.setNodeMarkers(node.data.parsedFirstComment?.shapes);
|
|
491
899
|
syncBoardDragFromChessTurn();
|
|
492
900
|
}
|
|
493
901
|
|
|
@@ -502,7 +910,7 @@
|
|
|
502
910
|
/**
|
|
503
911
|
* Представляет обновление UI доски после смены узла в дереве решения.
|
|
504
912
|
*/
|
|
505
|
-
function treeSetChessboardFen(_animationTime
|
|
913
|
+
function treeSetChessboardFen(_animationTime: number | undefined): void {
|
|
506
914
|
if (!solved) return;
|
|
507
915
|
syncBoardFromSolutionTreeNode();
|
|
508
916
|
}
|
|
@@ -524,9 +932,10 @@
|
|
|
524
932
|
}
|
|
525
933
|
</script>
|
|
526
934
|
|
|
527
|
-
<div class={
|
|
935
|
+
<div class={cn("flex w-full flex-col gap-2", className)}>
|
|
528
936
|
<PuzzleBoardTreeViewerPane
|
|
529
937
|
{chessboard}
|
|
938
|
+
{chessboardDesign}
|
|
530
939
|
chessTree={treeForViewer}
|
|
531
940
|
onSelectNode={treeOnSelectNode}
|
|
532
941
|
onDeleteVariant={treeOnDeleteVariant}
|