@connectorvol/chess-widgets 8.0.1 → 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 +74 -70
- package/dist/game-analyzer/gameAnalyzer.svelte.js +8 -6
- package/dist/game-analyzer/types.d.ts +11 -3
- 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 -74
- 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 +24 -11
- package/dist/puzzle-creation/StepPreview.svelte.d.ts +3 -1
- 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 +14 -9
- package/dist/puzzle-creation/puzzleWizardState.d.ts +35 -0
- package/dist/puzzle-creation/puzzleWizardState.js +37 -0
- package/dist/puzzle-creation/types.d.ts +37 -7
- package/package.json +20 -17
|
@@ -23,29 +23,47 @@
|
|
|
23
23
|
|
|
24
24
|
import { INITIAL_FEN } from "@connectorvol/chessops/fen";
|
|
25
25
|
import { PgnOps } from "@connectorvol/chessops/pgnOps.svelte";
|
|
26
|
-
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";
|
|
27
34
|
|
|
28
35
|
import { mergePgnWithSetupFen } from "../puzzle/mergePgnWithSetupFen.js";
|
|
29
|
-
import { createEmptyTreeFromFen } from "../puzzle/puzzleData.js";
|
|
30
|
-
import { PREVIEW_WRONG_NO_VARIANT_MESSAGE } from "../puzzle/puzzlePreviewConstants.js";
|
|
31
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,
|
|
32
45
|
puzzlePreviewClassifySolverMove,
|
|
33
46
|
puzzlePreviewDupSubtreeUnderStudentCursor,
|
|
34
47
|
puzzlePreviewIsSolvedPosition,
|
|
35
48
|
puzzlePreviewNodeAtPath,
|
|
49
|
+
puzzlePreviewRemapPathAfterReorder,
|
|
36
50
|
puzzlePreviewSideToMoveFromFen,
|
|
37
51
|
type TSolverMoveOutcome,
|
|
38
52
|
} from "../puzzle/puzzleStepPreviewSolver.js";
|
|
39
53
|
import PuzzleBoardTreeViewerPane from "./PuzzleBoardTreeViewerPane.svelte";
|
|
40
54
|
import { cn } from "../utils.js";
|
|
41
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";
|
|
42
58
|
|
|
43
59
|
let {
|
|
44
60
|
pgn = "",
|
|
45
61
|
fen: fenProp,
|
|
46
62
|
onOutcome,
|
|
47
63
|
boardTheme,
|
|
48
|
-
boardAppearanceSettings,
|
|
64
|
+
boardAppearanceSettings = DEFAULT_BOARD_APPEARANCE_SETTINGS,
|
|
65
|
+
boardSize,
|
|
66
|
+
onResizeAction,
|
|
49
67
|
class: className,
|
|
50
68
|
wrongFeedback = $bindable<string | null>(null),
|
|
51
69
|
solved = $bindable(false),
|
|
@@ -65,8 +83,28 @@
|
|
|
65
83
|
|
|
66
84
|
let cursorPath = $state<number[]>([]);
|
|
67
85
|
|
|
86
|
+
const OPPONENT_MOVE_ANIMATION_MS = 300;
|
|
87
|
+
|
|
68
88
|
let wrongRevealTimer: ReturnType<typeof setTimeout> | null = null;
|
|
69
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;
|
|
70
108
|
|
|
71
109
|
let previewChess = new PgnOps(INITIAL_FEN, "chess");
|
|
72
110
|
|
|
@@ -78,6 +116,11 @@
|
|
|
78
116
|
* Представляет синхронизацию доступности перетаскивания с очередью хода и состоянием «решено».
|
|
79
117
|
*/
|
|
80
118
|
function syncBoardDragFromChessTurn(): void {
|
|
119
|
+
if (pendingWrongMoveReveal !== null) {
|
|
120
|
+
chessboard.draggable = Draggable.NONE;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
81
124
|
const solver = puzzleSolverColor();
|
|
82
125
|
chessboard.draggable = solved
|
|
83
126
|
? previewChess.turn() === Color.WHITE
|
|
@@ -90,6 +133,137 @@
|
|
|
90
133
|
: Draggable.NONE;
|
|
91
134
|
}
|
|
92
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
|
+
|
|
93
267
|
/**
|
|
94
268
|
* Представляет добавление пути в дерево ученика без очистки уже пройденных линий.
|
|
95
269
|
*/
|
|
@@ -100,22 +274,106 @@
|
|
|
100
274
|
studentPreviewTree.currentNode = studentPreviewTree.rootNode.moves;
|
|
101
275
|
|
|
102
276
|
let src = solution.rootNode.moves;
|
|
277
|
+
const solver = puzzleSolverColor();
|
|
278
|
+
|
|
279
|
+
applyPlayedSolverNagsAlongPath(solution, path, solver);
|
|
280
|
+
|
|
103
281
|
for (const idx of path) {
|
|
104
282
|
const next = src.children[idx];
|
|
105
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
|
+
|
|
106
294
|
studentPreviewTree.addNodeToCurrent({
|
|
107
295
|
id: "",
|
|
108
296
|
children: [],
|
|
109
|
-
data: {
|
|
297
|
+
data: {
|
|
298
|
+
...next.data,
|
|
299
|
+
nags: studentNags,
|
|
300
|
+
},
|
|
110
301
|
});
|
|
111
302
|
src = next;
|
|
112
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;
|
|
113
368
|
}
|
|
114
369
|
|
|
115
370
|
/**
|
|
116
371
|
* Представляет обработку ошибочного хода в превью.
|
|
117
372
|
*/
|
|
118
|
-
function handleWrongOutcome(
|
|
373
|
+
function handleWrongOutcome(
|
|
374
|
+
outcome: TSolverMoveOutcome,
|
|
375
|
+
playedSan: string,
|
|
376
|
+
): void {
|
|
119
377
|
const solution = solutionPreviewTree;
|
|
120
378
|
if (!solution) return;
|
|
121
379
|
|
|
@@ -127,16 +385,40 @@
|
|
|
127
385
|
clearTimeout(advanceLineTimer);
|
|
128
386
|
advanceLineTimer = null;
|
|
129
387
|
}
|
|
388
|
+
if (opponentAutoTimer !== null) {
|
|
389
|
+
clearTimeout(opponentAutoTimer);
|
|
390
|
+
opponentAutoTimer = null;
|
|
391
|
+
}
|
|
130
392
|
|
|
131
393
|
if (outcome.kind === "wrong_marked_variant") {
|
|
132
394
|
wrongFeedback =
|
|
133
395
|
"Неверно. Открываю введённый вариант в дереве, ход отменён.";
|
|
134
396
|
onOutcome?.("failed");
|
|
135
397
|
ensureStudentPreviewTreeHasPath(cursorPath);
|
|
398
|
+
const studentFork = studentPreviewTree.currentNode;
|
|
136
399
|
puzzlePreviewDupSubtreeUnderStudentCursor(
|
|
137
400
|
studentPreviewTree,
|
|
138
401
|
outcome.wrongBranchRoot,
|
|
139
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");
|
|
140
422
|
return;
|
|
141
423
|
}
|
|
142
424
|
|
|
@@ -148,6 +430,8 @@
|
|
|
148
430
|
* Представляет установку позиции и дерева ученика по пути из корня.
|
|
149
431
|
*/
|
|
150
432
|
function setStateFromPath(path: number[]): void {
|
|
433
|
+
dismissPendingWrongMoveReveal();
|
|
434
|
+
|
|
151
435
|
const solution = solutionPreviewTree;
|
|
152
436
|
if (!solution) return;
|
|
153
437
|
|
|
@@ -161,30 +445,123 @@
|
|
|
161
445
|
}
|
|
162
446
|
|
|
163
447
|
cursorPath = path;
|
|
164
|
-
|
|
448
|
+
syncStudentTreeFromSolutionPath(path);
|
|
165
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
|
+
});
|
|
166
456
|
syncBoardDragFromChessTurn();
|
|
167
457
|
}
|
|
168
458
|
|
|
169
459
|
/**
|
|
170
|
-
* Представляет
|
|
460
|
+
* Представляет ответ соперника по главному варианту после хода решателя (без применения к движку).
|
|
171
461
|
*/
|
|
172
|
-
function
|
|
462
|
+
function getOpponentMainLineReply(
|
|
463
|
+
pathAfterSolver: number[],
|
|
464
|
+
): { san: string; path: number[] } | null {
|
|
173
465
|
const solution = solutionPreviewTree;
|
|
174
|
-
if (!solution) return
|
|
466
|
+
if (!solution) return null;
|
|
175
467
|
|
|
176
468
|
if (puzzlePreviewSideToMoveFromFen(previewChess.fen()) === puzzleSolverColor()) {
|
|
177
|
-
return
|
|
469
|
+
return null;
|
|
178
470
|
}
|
|
179
471
|
|
|
180
472
|
const node = puzzlePreviewNodeAtPath(
|
|
181
473
|
solution.rootNode.moves,
|
|
182
474
|
pathAfterSolver,
|
|
183
475
|
);
|
|
184
|
-
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
|
+
}
|
|
185
558
|
|
|
186
|
-
|
|
187
|
-
|
|
559
|
+
const pathAfterSolver = [...cursorPath];
|
|
560
|
+
const delayMs = chessboard.animationTime || OPPONENT_MOVE_ANIMATION_MS;
|
|
561
|
+
opponentAutoTimer = setTimeout(() => {
|
|
562
|
+
playOpponentAutoResponse(pathAfterSolver);
|
|
563
|
+
opponentAutoTimer = null;
|
|
564
|
+
}, delayMs);
|
|
188
565
|
}
|
|
189
566
|
|
|
190
567
|
/**
|
|
@@ -258,6 +635,8 @@
|
|
|
258
635
|
const solution = solutionPreviewTree;
|
|
259
636
|
if (!solution || solved) return undefined;
|
|
260
637
|
|
|
638
|
+
cancelPendingWrongMoveReveal();
|
|
639
|
+
|
|
261
640
|
const cursorNode = puzzlePreviewNodeAtPath(
|
|
262
641
|
solution.rootNode.moves,
|
|
263
642
|
cursorPath,
|
|
@@ -272,34 +651,34 @@
|
|
|
272
651
|
);
|
|
273
652
|
|
|
274
653
|
if (outcome.kind !== "correct") {
|
|
275
|
-
|
|
276
|
-
|
|
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
|
+
}
|
|
277
672
|
}
|
|
278
673
|
|
|
279
674
|
try {
|
|
280
|
-
|
|
675
|
+
const pathAfterSolver = [...cursorPath, outcome.childIndex];
|
|
281
676
|
const moveRecord = previewChess.makeSanMove(san);
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const leaf = puzzlePreviewNodeAtPath(solution.rootNode.moves, cursorPath);
|
|
286
|
-
if (leaf && puzzlePreviewIsSolvedPosition(previewChess, leaf, puzzleSolverColor())) {
|
|
287
|
-
if (advanceLineTimer !== null) {
|
|
288
|
-
clearTimeout(advanceLineTimer);
|
|
289
|
-
advanceLineTimer = null;
|
|
290
|
-
}
|
|
291
|
-
advanceLineTimer = setTimeout(() => {
|
|
292
|
-
const advanced = maybeAdvanceToNextOpponentLine();
|
|
293
|
-
if (!advanced) {
|
|
294
|
-
solved = true;
|
|
295
|
-
onOutcome?.("solved");
|
|
296
|
-
}
|
|
297
|
-
syncBoardDragFromChessTurn();
|
|
298
|
-
advanceLineTimer = null;
|
|
299
|
-
}, 250);
|
|
300
|
-
}
|
|
677
|
+
cursorPath = pathAfterSolver;
|
|
678
|
+
syncStudentTreeFromSolutionPath(pathAfterSolver);
|
|
301
679
|
|
|
302
680
|
wrongFeedback = null;
|
|
681
|
+
lastSolverMoveForBoard = moveRecord;
|
|
303
682
|
syncBoardDragFromChessTurn();
|
|
304
683
|
|
|
305
684
|
return moveRecord;
|
|
@@ -347,8 +726,13 @@
|
|
|
347
726
|
turn: previewChess.turn(),
|
|
348
727
|
};
|
|
349
728
|
},
|
|
350
|
-
afterPieceMoveSan: () => {
|
|
351
|
-
|
|
729
|
+
afterPieceMoveSan: (move) => {
|
|
730
|
+
lastSolverMoveForBoard = null;
|
|
731
|
+
if (solved) {
|
|
732
|
+
syncAnalysisBoardFromCurrentNode();
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
afterSolverMoveOnBoard(move);
|
|
352
736
|
},
|
|
353
737
|
beforePieceMove: (from, to, promotion) => {
|
|
354
738
|
if (solved) {
|
|
@@ -379,42 +763,46 @@
|
|
|
379
763
|
return previewChess.fen();
|
|
380
764
|
},
|
|
381
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
|
+
}
|
|
382
777
|
syncBoardDragFromChessTurn();
|
|
383
778
|
},
|
|
384
779
|
},
|
|
385
780
|
};
|
|
386
781
|
|
|
387
|
-
let chessboard = $derived(
|
|
388
|
-
|
|
782
|
+
let chessboard = $derived.by(() => {
|
|
783
|
+
const merged = buildPuzzleWizardBoardSettings(boardAppearanceSettings);
|
|
784
|
+
return createBoardApi({
|
|
389
785
|
fen: INITIAL_FEN,
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
786
|
+
playSettings:
|
|
787
|
+
boardSize !== undefined
|
|
788
|
+
? { ...merged.play, boardSize }
|
|
789
|
+
: merged.play,
|
|
790
|
+
actions: {
|
|
791
|
+
...actions,
|
|
792
|
+
onResizeAction,
|
|
396
793
|
},
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
const chessboardDesign = $derived({
|
|
798
|
+
...buildPuzzleWizardBoardSettings(boardAppearanceSettings).design,
|
|
799
|
+
theme: boardTheme ?? CHESSBOARD_THEMES.blue,
|
|
800
|
+
});
|
|
401
801
|
|
|
402
802
|
const treeForViewer = $derived(
|
|
403
803
|
solved && solutionPreviewTree ? solutionPreviewTree : studentPreviewTree,
|
|
404
804
|
);
|
|
405
805
|
|
|
406
|
-
/**
|
|
407
|
-
* Представляет синхронизацию выбранного узла в полном дереве решения с пройденной линией при переходе в режим просмотра (`solved`).
|
|
408
|
-
* Без этого после switch с `studentPreviewTree` у `solutionPreviewTree` остаётся `currentNode` у корня и подсветка в TreeViewer неверна.
|
|
409
|
-
*/
|
|
410
|
-
$effect(() => {
|
|
411
|
-
if (!solved || !solutionPreviewTree) return;
|
|
412
|
-
const tree = solutionPreviewTree;
|
|
413
|
-
const path = cursorPath;
|
|
414
|
-
const target = puzzlePreviewNodeAtPath(tree.rootNode.moves, path);
|
|
415
|
-
tree.currentNode = target ?? tree.rootNode.moves;
|
|
416
|
-
});
|
|
417
|
-
|
|
418
806
|
$effect(() => {
|
|
419
807
|
const trimmedPgn = pgn.trim();
|
|
420
808
|
const rootFen = fenProp?.trim() ? fenProp.trim() : INITIAL_FEN;
|
|
@@ -431,6 +819,10 @@
|
|
|
431
819
|
rootFen.trim().split(/\s+/)[1] === "b"
|
|
432
820
|
? Color.BLACK
|
|
433
821
|
: Color.WHITE;
|
|
822
|
+
syncMoveEvaluationNagBadge(chessboard, {
|
|
823
|
+
nags: undefined,
|
|
824
|
+
lastMove: null,
|
|
825
|
+
});
|
|
434
826
|
cursorPath = [];
|
|
435
827
|
solved = false;
|
|
436
828
|
wrongFeedback = null;
|
|
@@ -444,17 +836,28 @@
|
|
|
444
836
|
const { rootNode } = transformPgnToChessNode(fullPgn);
|
|
445
837
|
|
|
446
838
|
layoutInitialFen = rootFen;
|
|
447
|
-
|
|
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;
|
|
448
845
|
|
|
449
846
|
cursorPath = [];
|
|
450
847
|
solved = false;
|
|
451
848
|
wrongFeedback = null;
|
|
452
849
|
previewChess.setFen(rootFen);
|
|
453
|
-
studentPreviewTree.replaceRootTree(
|
|
850
|
+
studentPreviewTree.replaceRootTree(
|
|
851
|
+
createStudentPreviewTreeFromFen(rootFen, rootNode.comments),
|
|
852
|
+
);
|
|
454
853
|
studentPreviewTree.currentNode = studentPreviewTree.rootNode.moves;
|
|
455
854
|
chessboard.fen = rootFen;
|
|
456
855
|
chessboard.orientation =
|
|
457
856
|
rootFen.trim().split(/\s+/)[1] === "b" ? Color.BLACK : Color.WHITE;
|
|
857
|
+
syncMoveEvaluationNagBadge(chessboard, {
|
|
858
|
+
nags: undefined,
|
|
859
|
+
lastMove: null,
|
|
860
|
+
});
|
|
458
861
|
syncBoardDragFromChessTurn();
|
|
459
862
|
});
|
|
460
863
|
|
|
@@ -465,14 +868,15 @@
|
|
|
465
868
|
* Представляет сброс отложенных таймеров превью.
|
|
466
869
|
*/
|
|
467
870
|
function clearTimers(): void {
|
|
468
|
-
|
|
469
|
-
clearTimeout(wrongRevealTimer);
|
|
470
|
-
wrongRevealTimer = null;
|
|
471
|
-
}
|
|
871
|
+
dismissPendingWrongMoveReveal();
|
|
472
872
|
if (advanceLineTimer !== null) {
|
|
473
873
|
clearTimeout(advanceLineTimer);
|
|
474
874
|
advanceLineTimer = null;
|
|
475
875
|
}
|
|
876
|
+
if (opponentAutoTimer !== null) {
|
|
877
|
+
clearTimeout(opponentAutoTimer);
|
|
878
|
+
opponentAutoTimer = null;
|
|
879
|
+
}
|
|
476
880
|
}
|
|
477
881
|
|
|
478
882
|
/**
|
|
@@ -481,11 +885,17 @@
|
|
|
481
885
|
function syncBoardFromSolutionTreeNode(): void {
|
|
482
886
|
const tree = solutionPreviewTree;
|
|
483
887
|
if (!tree || !solved) return;
|
|
484
|
-
|
|
888
|
+
|
|
889
|
+
const node = tree.currentNode;
|
|
890
|
+
|
|
891
|
+
previewChess.setFen(node.data.fen);
|
|
485
892
|
chessboard.fen = previewChess.fen();
|
|
486
|
-
chessboard.addLastMove(
|
|
487
|
-
syncMoveEvaluationNagBadge(
|
|
488
|
-
|
|
893
|
+
chessboard.addLastMove(node.data.lastMove ?? null);
|
|
894
|
+
syncMoveEvaluationNagBadge(
|
|
895
|
+
chessboard,
|
|
896
|
+
nodeDataForAnalysisBoardBadge(node.data),
|
|
897
|
+
);
|
|
898
|
+
chessboard.setNodeMarkers(node.data.parsedFirstComment?.shapes);
|
|
489
899
|
syncBoardDragFromChessTurn();
|
|
490
900
|
}
|
|
491
901
|
|
|
@@ -500,7 +910,7 @@
|
|
|
500
910
|
/**
|
|
501
911
|
* Представляет обновление UI доски после смены узла в дереве решения.
|
|
502
912
|
*/
|
|
503
|
-
function treeSetChessboardFen(_animationTime
|
|
913
|
+
function treeSetChessboardFen(_animationTime: number | undefined): void {
|
|
504
914
|
if (!solved) return;
|
|
505
915
|
syncBoardFromSolutionTreeNode();
|
|
506
916
|
}
|
|
@@ -522,9 +932,10 @@
|
|
|
522
932
|
}
|
|
523
933
|
</script>
|
|
524
934
|
|
|
525
|
-
<div class={
|
|
935
|
+
<div class={cn("flex w-full flex-col gap-2", className)}>
|
|
526
936
|
<PuzzleBoardTreeViewerPane
|
|
527
937
|
{chessboard}
|
|
938
|
+
{chessboardDesign}
|
|
528
939
|
chessTree={treeForViewer}
|
|
529
940
|
onSelectNode={treeOnSelectNode}
|
|
530
941
|
onDeleteVariant={treeOnDeleteVariant}
|