@connectorvol/chess-widgets 9.0.0 → 9.0.1
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/(components)/BoardSettingsTrigger.svelte +10 -0
- package/dist/(components)/BoardSettingsTrigger.svelte.d.ts +18 -0
- package/dist/game-analyzer/GameAnalyzer.svelte +88 -74
- package/dist/game-analyzer/gameAnalyzer.svelte.js +2 -7
- package/dist/game-analyzer/types.d.ts +3 -6
- package/dist/index.d.ts +1 -1
- package/dist/puzzle/puzzlePreviewPathNags.js +1 -1
- package/dist/puzzle-creation/PuzzleBoardTreeViewerPane.svelte +199 -171
- package/dist/puzzle-creation/PuzzleBoardTreeViewerPane.svelte.d.ts +7 -3
- package/dist/puzzle-creation/PuzzleCreationWizard.svelte +184 -180
- package/dist/puzzle-creation/PuzzlePgnBoardTreeEditor.svelte +906 -870
- package/dist/puzzle-creation/StepMoves.svelte +209 -207
- package/dist/puzzle-creation/StepMoves.svelte.d.ts +1 -2
- package/dist/puzzle-creation/StepPosition.svelte +22 -2
- package/dist/puzzle-creation/puzzleWizardState.js +1 -1
- package/dist/puzzle-creation/types.d.ts +0 -4
- package/package.json +12 -10
|
@@ -1,955 +1,991 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
export type {
|
|
3
|
+
TPuzzlePgnBoardTreeEditorOutcome,
|
|
4
|
+
TPuzzlePgnBoardTreeEditorProps,
|
|
5
|
+
} from "./types.js";
|
|
6
6
|
</script>
|
|
7
7
|
|
|
8
8
|
<script lang="ts">
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
import {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
let cursorPath = $state<number[]>([]);
|
|
85
|
-
|
|
86
|
-
const OPPONENT_MOVE_ANIMATION_MS = 300;
|
|
87
|
-
|
|
88
|
-
let wrongRevealTimer: ReturnType<typeof setTimeout> | null = null;
|
|
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;
|
|
108
|
-
|
|
109
|
-
let previewChess = new PgnOps(INITIAL_FEN, "chess");
|
|
110
|
-
|
|
111
|
-
const studentPreviewTree = new ChessTree(createEmptyTreeFromFen(INITIAL_FEN));
|
|
112
|
-
|
|
113
|
-
let solutionPreviewTree = $state<ChessTree | null>(null);
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Представляет синхронизацию доступности перетаскивания с очередью хода и состоянием «решено».
|
|
117
|
-
*/
|
|
118
|
-
function syncBoardDragFromChessTurn(): void {
|
|
119
|
-
if (pendingWrongMoveReveal !== null) {
|
|
120
|
-
chessboard.draggable = Draggable.NONE;
|
|
121
|
-
return;
|
|
9
|
+
import type { IChessBoardActions } from "@connectorvol/chessboard";
|
|
10
|
+
import type { Square } from "@connectorvol/shared";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
createBoardApi,
|
|
14
|
+
Draggable,
|
|
15
|
+
syncMoveEvaluationNagBadge,
|
|
16
|
+
} from "@connectorvol/chessboard";
|
|
17
|
+
import { ChessTree, transformPgnToChessNode } from "@connectorvol/tree";
|
|
18
|
+
import { untrack } from "svelte";
|
|
19
|
+
|
|
20
|
+
import { INITIAL_FEN } from "@connectorvol/chessops/fen";
|
|
21
|
+
import { PgnOps } from "@connectorvol/chessops/pgnOps.svelte";
|
|
22
|
+
import {
|
|
23
|
+
Color,
|
|
24
|
+
calculatePly,
|
|
25
|
+
type Move,
|
|
26
|
+
PUZZLE_BRANCH_CORRECT_NAG_ID,
|
|
27
|
+
PUZZLE_BRANCH_WRONG_NAG_ID,
|
|
28
|
+
isPuzzleBranchNag,
|
|
29
|
+
} from "@connectorvol/shared";
|
|
30
|
+
|
|
31
|
+
import { mergePgnWithSetupFen } from "../puzzle/mergePgnWithSetupFen.js";
|
|
32
|
+
import {
|
|
33
|
+
createEmptyTreeFromFen,
|
|
34
|
+
createStudentPreviewTreeFromFen,
|
|
35
|
+
} from "../puzzle/puzzleData.js";
|
|
36
|
+
import {
|
|
37
|
+
PREVIEW_WRONG_MOVE_REVEAL_MS,
|
|
38
|
+
PREVIEW_WRONG_NO_VARIANT_MESSAGE,
|
|
39
|
+
} from "../puzzle/puzzlePreviewConstants.js";
|
|
40
|
+
import {
|
|
41
|
+
normalizePuzzleTreeMainLine,
|
|
42
|
+
syncPuzzleBranchNagsOnTree,
|
|
43
|
+
} from "../puzzle/syncPuzzleBranchNags.js";
|
|
44
|
+
import { applyPlayedSolverNagsAlongPath } from "../puzzle/puzzlePreviewPathNags.js";
|
|
45
|
+
import {
|
|
46
|
+
puzzlePreviewAddAdHocWrongMoveToSolutionTree,
|
|
47
|
+
puzzlePreviewClassifySolverMove,
|
|
48
|
+
puzzlePreviewDupSubtreeUnderStudentCursor,
|
|
49
|
+
puzzlePreviewIsSolvedPosition,
|
|
50
|
+
puzzlePreviewNodeAtPath,
|
|
51
|
+
puzzlePreviewRemapPathAfterReorder,
|
|
52
|
+
puzzlePreviewSideToMoveFromFen,
|
|
53
|
+
type TSolverMoveOutcome,
|
|
54
|
+
} from "../puzzle/puzzleStepPreviewSolver.js";
|
|
55
|
+
import PuzzleBoardTreeViewerPane from "./PuzzleBoardTreeViewerPane.svelte";
|
|
56
|
+
import { cn } from "../utils.js";
|
|
57
|
+
import type { TPuzzlePgnBoardTreeEditorProps } from "./types.js";
|
|
58
|
+
import { DEFAULT_BOARD_APPEARANCE_SETTINGS } from "../constants/default-board-appearance-settings.js";
|
|
59
|
+
import { buildPuzzleWizardBoardSettings } from "./buildPuzzleWizardBoardSettings.js";
|
|
60
|
+
|
|
61
|
+
let {
|
|
62
|
+
pgn = "",
|
|
63
|
+
fen: fenProp,
|
|
64
|
+
onOutcome,
|
|
65
|
+
boardAppearanceSettings = DEFAULT_BOARD_APPEARANCE_SETTINGS,
|
|
66
|
+
boardSize,
|
|
67
|
+
onResizeAction,
|
|
68
|
+
class: className,
|
|
69
|
+
wrongFeedback = $bindable<string | null>(null),
|
|
70
|
+
solved = $bindable(false),
|
|
71
|
+
}: TPuzzlePgnBoardTreeEditorProps = $props();
|
|
72
|
+
|
|
73
|
+
/** Корень превью: синхронизируется с эффектом смены PGN/FEN. */
|
|
74
|
+
let layoutInitialFen = $state(INITIAL_FEN);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Представляет цвет стороны решателя по текущему корню превью (`layoutInitialFen`).
|
|
78
|
+
*/
|
|
79
|
+
function puzzleSolverColor(): Color {
|
|
80
|
+
return layoutInitialFen.trim().split(/\s+/)[1] === "b"
|
|
81
|
+
? Color.BLACK
|
|
82
|
+
: Color.WHITE;
|
|
122
83
|
}
|
|
123
84
|
|
|
124
|
-
|
|
125
|
-
chessboard.draggable = solved
|
|
126
|
-
? previewChess.turn() === Color.WHITE
|
|
127
|
-
? Draggable.WHITE
|
|
128
|
-
: Draggable.BLACK
|
|
129
|
-
: previewChess.turn() === solver
|
|
130
|
-
? solver === Color.WHITE
|
|
131
|
-
? Draggable.WHITE
|
|
132
|
-
: Draggable.BLACK
|
|
133
|
-
: Draggable.NONE;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Представляет отмену отложенного отката ошибочного хода без записи варианта в дерево.
|
|
138
|
-
*/
|
|
139
|
-
function dismissPendingWrongMoveReveal(): void {
|
|
140
|
-
if (wrongRevealTimer !== null) {
|
|
141
|
-
clearTimeout(wrongRevealTimer);
|
|
142
|
-
wrongRevealTimer = null;
|
|
143
|
-
}
|
|
85
|
+
let cursorPath = $state<number[]>([]);
|
|
144
86
|
|
|
145
|
-
const
|
|
146
|
-
if (pending === null) return;
|
|
87
|
+
const OPPONENT_MOVE_ANIMATION_MS = 300;
|
|
147
88
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
}
|
|
89
|
+
let wrongRevealTimer: ReturnType<typeof setTimeout> | null = null;
|
|
90
|
+
let advanceLineTimer: ReturnType<typeof setTimeout> | null = null;
|
|
91
|
+
let opponentAutoTimer: ReturnType<typeof setTimeout> | null = null;
|
|
169
92
|
|
|
170
|
-
|
|
171
|
-
|
|
93
|
+
/** Представляет отложенный откат ошибочного хода на доске после показа NAG ✗. */
|
|
94
|
+
type TPendingWrongMoveReveal = {
|
|
95
|
+
/** Возвращает классификацию хода решателя. */
|
|
96
|
+
outcome: TSolverMoveOutcome;
|
|
97
|
+
/** Возвращает сыгранный SAN. */
|
|
98
|
+
playedSan: string;
|
|
99
|
+
/** Возвращает FEN позиции до ошибочного хода. */
|
|
100
|
+
revertFen: string;
|
|
101
|
+
/** Возвращает последний ход до ошибочного хода (для подсветки на доске). */
|
|
102
|
+
revertLastMove: Move | null;
|
|
103
|
+
};
|
|
172
104
|
|
|
173
|
-
|
|
174
|
-
* Представляет показ NAG ✗ на доске и отложенный откат ошибочного хода.
|
|
175
|
-
*/
|
|
176
|
-
function scheduleWrongMoveReveal(wrongMove: Move): void {
|
|
177
|
-
const pending = pendingWrongMoveReveal;
|
|
178
|
-
if (pending === null) return;
|
|
105
|
+
let pendingWrongMoveReveal = $state<TPendingWrongMoveReveal | null>(null);
|
|
179
106
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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),
|
|
107
|
+
/** Представляет последний применённый ход решателя до колбэка `afterPieceMove` (drag-and-drop). */
|
|
108
|
+
let lastSolverMoveForBoard: Move | null = null;
|
|
109
|
+
|
|
110
|
+
let previewChess = new PgnOps(INITIAL_FEN, "chess");
|
|
111
|
+
|
|
112
|
+
const studentPreviewTree = new ChessTree(
|
|
113
|
+
createEmptyTreeFromFen(INITIAL_FEN),
|
|
246
114
|
);
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
115
|
+
|
|
116
|
+
let solutionPreviewTree = $state<ChessTree | null>(null);
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Представляет синхронизацию доступности перетаскивания с очередью хода и состоянием «решено».
|
|
120
|
+
*/
|
|
121
|
+
function syncBoardDragFromChessTurn(): void {
|
|
122
|
+
if (pendingWrongMoveReveal !== null) {
|
|
123
|
+
chessboard.draggable = Draggable.NONE;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const solver = puzzleSolverColor();
|
|
128
|
+
chessboard.draggable = solved
|
|
129
|
+
? previewChess.turn() === Color.WHITE
|
|
130
|
+
? Draggable.WHITE
|
|
131
|
+
: Draggable.BLACK
|
|
132
|
+
: previewChess.turn() === solver
|
|
133
|
+
? solver === Color.WHITE
|
|
134
|
+
? Draggable.WHITE
|
|
135
|
+
: Draggable.BLACK
|
|
136
|
+
: Draggable.NONE;
|
|
258
137
|
}
|
|
259
138
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Представляет отмену отложенного отката ошибочного хода без записи варианта в дерево.
|
|
141
|
+
*/
|
|
142
|
+
function dismissPendingWrongMoveReveal(): void {
|
|
143
|
+
if (wrongRevealTimer !== null) {
|
|
144
|
+
clearTimeout(wrongRevealTimer);
|
|
145
|
+
wrongRevealTimer = null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const pending = pendingWrongMoveReveal;
|
|
149
|
+
if (pending === null) return;
|
|
150
|
+
|
|
151
|
+
pendingWrongMoveReveal = null;
|
|
152
|
+
previewChess.setFen(pending.revertFen);
|
|
153
|
+
chessboard.fen = pending.revertFen;
|
|
154
|
+
chessboard.addLastMove(pending.revertLastMove);
|
|
155
|
+
syncMoveEvaluationNagBadge(chessboard, {
|
|
156
|
+
nags: undefined,
|
|
157
|
+
lastMove: pending.revertLastMove,
|
|
158
|
+
});
|
|
159
|
+
syncBoardDragFromChessTurn();
|
|
264
160
|
}
|
|
265
|
-
}
|
|
266
161
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if (!solution) return;
|
|
162
|
+
/**
|
|
163
|
+
* Представляет досрочное завершение показа ошибочного хода перед следующим ходом решателя.
|
|
164
|
+
*/
|
|
165
|
+
function cancelPendingWrongMoveReveal(): void {
|
|
166
|
+
if (pendingWrongMoveReveal === null) return;
|
|
273
167
|
|
|
274
|
-
|
|
168
|
+
if (wrongRevealTimer !== null) {
|
|
169
|
+
clearTimeout(wrongRevealTimer);
|
|
170
|
+
wrongRevealTimer = null;
|
|
171
|
+
}
|
|
275
172
|
|
|
276
|
-
|
|
277
|
-
|
|
173
|
+
finishWrongMoveReveal();
|
|
174
|
+
}
|
|
278
175
|
|
|
279
|
-
|
|
176
|
+
/**
|
|
177
|
+
* Представляет показ NAG ✗ на доске и отложенный откат ошибочного хода.
|
|
178
|
+
*/
|
|
179
|
+
function scheduleWrongMoveReveal(wrongMove: Move): void {
|
|
180
|
+
const pending = pendingWrongMoveReveal;
|
|
181
|
+
if (pending === null) return;
|
|
280
182
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
183
|
+
syncMoveEvaluationNagBadge(chessboard, {
|
|
184
|
+
nags: [PUZZLE_BRANCH_WRONG_NAG_ID],
|
|
185
|
+
lastMove: wrongMove,
|
|
186
|
+
});
|
|
187
|
+
syncBoardDragFromChessTurn();
|
|
284
188
|
|
|
285
|
-
|
|
189
|
+
wrongRevealTimer = setTimeout(() => {
|
|
190
|
+
wrongRevealTimer = null;
|
|
191
|
+
finishWrongMoveReveal();
|
|
192
|
+
}, PREVIEW_WRONG_MOVE_REVEAL_MS);
|
|
193
|
+
}
|
|
286
194
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
195
|
+
/**
|
|
196
|
+
* Представляет завершение показа ошибочного хода: запись варианта в дерево и откат позиции на доске.
|
|
197
|
+
*/
|
|
198
|
+
function finishWrongMoveReveal(): void {
|
|
199
|
+
const pending = pendingWrongMoveReveal;
|
|
200
|
+
pendingWrongMoveReveal = null;
|
|
201
|
+
if (pending === null) return;
|
|
293
202
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
203
|
+
previewChess.setFen(pending.revertFen);
|
|
204
|
+
handleWrongOutcome(pending.outcome, pending.playedSan);
|
|
205
|
+
|
|
206
|
+
chessboard.fen = pending.revertFen;
|
|
207
|
+
chessboard.addLastMove(pending.revertLastMove);
|
|
208
|
+
syncMoveEvaluationNagBadge(chessboard, {
|
|
209
|
+
nags: undefined,
|
|
210
|
+
lastMove: pending.revertLastMove,
|
|
211
|
+
});
|
|
212
|
+
syncBoardDragFromChessTurn();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Представляет синхронизацию NAG ✓ на доске после верного хода решателя.
|
|
217
|
+
*/
|
|
218
|
+
function syncCorrectSolverMoveNagBadge(move: Move): void {
|
|
219
|
+
syncMoveEvaluationNagBadge(chessboard, {
|
|
220
|
+
nags: [PUZZLE_BRANCH_CORRECT_NAG_ID],
|
|
221
|
+
lastMove: move,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Представляет данные узла для бейджа NAG на доске в режиме свободного анализа (без ✓/✗ задачи).
|
|
227
|
+
*/
|
|
228
|
+
function nodeDataForAnalysisBoardBadge(data: {
|
|
229
|
+
nags?: number[];
|
|
230
|
+
lastMove?: Move | null;
|
|
231
|
+
}): { nags?: number[]; lastMove?: Move | null } {
|
|
232
|
+
const nags = data.nags?.filter((n) => !isPuzzleBranchNag(n));
|
|
233
|
+
return {
|
|
234
|
+
nags: nags && nags.length > 0 ? nags : undefined,
|
|
235
|
+
lastMove: data.lastMove ?? null,
|
|
236
|
+
};
|
|
303
237
|
}
|
|
304
238
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
239
|
+
/**
|
|
240
|
+
* Представляет синхронизацию бейджей NAG и маркеров доски с текущим узлом дерева решения (свободный анализ).
|
|
241
|
+
*/
|
|
242
|
+
function syncAnalysisBoardFromCurrentNode(): void {
|
|
243
|
+
const tree = solutionPreviewTree;
|
|
244
|
+
if (!tree) return;
|
|
245
|
+
|
|
246
|
+
syncMoveEvaluationNagBadge(
|
|
247
|
+
chessboard,
|
|
248
|
+
nodeDataForAnalysisBoardBadge(tree.currentNode.data),
|
|
249
|
+
);
|
|
250
|
+
chessboard.setNodeMarkers(
|
|
251
|
+
tree.currentNode.data.parsedFirstComment?.shapes,
|
|
252
|
+
);
|
|
253
|
+
syncBoardDragFromChessTurn();
|
|
254
|
+
}
|
|
308
255
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
256
|
+
/**
|
|
257
|
+
* Представляет обработку UI после применения хода решателя на доске.
|
|
258
|
+
*/
|
|
259
|
+
function afterSolverMoveOnBoard(move: Move): void {
|
|
260
|
+
if (pendingWrongMoveReveal !== null) {
|
|
261
|
+
scheduleWrongMoveReveal(move);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
315
264
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
265
|
+
syncCorrectSolverMoveNagBadge(move);
|
|
266
|
+
syncBoardDragFromChessTurn();
|
|
267
|
+
if (!solved) {
|
|
268
|
+
scheduleOpponentAutoResponse();
|
|
269
|
+
}
|
|
319
270
|
}
|
|
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
271
|
|
|
350
|
-
|
|
272
|
+
/**
|
|
273
|
+
* Представляет добавление пути в дерево ученика без очистки уже пройденных линий.
|
|
274
|
+
*/
|
|
275
|
+
function ensureStudentPreviewTreeHasPath(path: number[]): void {
|
|
276
|
+
const solution = solutionPreviewTree;
|
|
277
|
+
if (!solution) return;
|
|
351
278
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
279
|
+
studentPreviewTree.currentNode = studentPreviewTree.rootNode.moves;
|
|
280
|
+
|
|
281
|
+
let src = solution.rootNode.moves;
|
|
282
|
+
const solver = puzzleSolverColor();
|
|
283
|
+
|
|
284
|
+
applyPlayedSolverNagsAlongPath(solution, path, solver);
|
|
285
|
+
|
|
286
|
+
for (const idx of path) {
|
|
287
|
+
const next = src.children[idx];
|
|
288
|
+
if (!next) break;
|
|
289
|
+
|
|
290
|
+
const studentNags = next.data.nags
|
|
291
|
+
? [...next.data.nags]
|
|
292
|
+
: undefined;
|
|
293
|
+
|
|
294
|
+
const existingChild = studentPreviewTree.currentNode.children.find(
|
|
295
|
+
(c) => c.data.san === next.data.san,
|
|
296
|
+
);
|
|
297
|
+
if (existingChild) {
|
|
298
|
+
existingChild.data.nags = studentNags;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
studentPreviewTree.addNodeToCurrent({
|
|
302
|
+
id: "",
|
|
303
|
+
children: [],
|
|
304
|
+
data: {
|
|
305
|
+
...next.data,
|
|
306
|
+
nags: studentNags,
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
src = next;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const solverColor = puzzleSolverColor();
|
|
313
|
+
normalizePuzzleTreeMainLine(
|
|
314
|
+
studentPreviewTree.rootNode.moves,
|
|
315
|
+
solverColor,
|
|
316
|
+
);
|
|
383
317
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Представляет нормализацию главной линии и NAG только в дереве решения (PGN).
|
|
321
|
+
*/
|
|
322
|
+
function normalizeSolutionTree(): void {
|
|
323
|
+
const solution = solutionPreviewTree;
|
|
324
|
+
if (!solution) return;
|
|
325
|
+
|
|
326
|
+
const solverColor = puzzleSolverColor();
|
|
327
|
+
if (normalizePuzzleTreeMainLine(solution.rootNode.moves, solverColor)) {
|
|
328
|
+
solution.mutationVersion++;
|
|
329
|
+
}
|
|
330
|
+
cursorPath = puzzlePreviewRemapPathAfterReorder(
|
|
331
|
+
solution.rootNode.moves,
|
|
332
|
+
cursorPath,
|
|
333
|
+
);
|
|
387
334
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Представляет синхронизацию дерева ученика с пройденным путём решения (после верного хода).
|
|
338
|
+
*/
|
|
339
|
+
function syncStudentTreeFromSolutionPath(path: number[]): void {
|
|
340
|
+
ensureStudentPreviewTreeHasPath(path);
|
|
341
|
+
studentPreviewTree.forcedNodeId = null;
|
|
342
|
+
studentPreviewTree.mutationVersion++;
|
|
391
343
|
}
|
|
392
344
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
345
|
+
/**
|
|
346
|
+
* Представляет добавление отсутствующего в PGN хода в дерево решения и дерево ученика с NAG ✗.
|
|
347
|
+
*/
|
|
348
|
+
function addAdHocWrongMoveToTrees(playedSan: string): boolean {
|
|
349
|
+
const solution = solutionPreviewTree;
|
|
350
|
+
if (!solution) return false;
|
|
351
|
+
|
|
352
|
+
const addedNode = puzzlePreviewAddAdHocWrongMoveToSolutionTree(
|
|
353
|
+
solution,
|
|
354
|
+
cursorPath,
|
|
355
|
+
previewChess.fen(),
|
|
356
|
+
playedSan,
|
|
357
|
+
);
|
|
358
|
+
if (!addedNode) return false;
|
|
359
|
+
|
|
360
|
+
normalizeSolutionTree();
|
|
361
|
+
|
|
362
|
+
ensureStudentPreviewTreeHasPath(cursorPath);
|
|
363
|
+
const studentFork = studentPreviewTree.currentNode;
|
|
364
|
+
studentPreviewTree.addNodeToCurrent({
|
|
365
|
+
id: "",
|
|
366
|
+
children: [],
|
|
367
|
+
data: { ...addedNode.data },
|
|
368
|
+
});
|
|
369
|
+
studentPreviewTree.forcedNodeId = studentFork.id;
|
|
370
|
+
studentPreviewTree.currentNode = studentFork;
|
|
371
|
+
|
|
372
|
+
const solverColor = puzzleSolverColor();
|
|
373
|
+
normalizePuzzleTreeMainLine(
|
|
374
|
+
studentPreviewTree.rootNode.moves,
|
|
375
|
+
solverColor,
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
studentPreviewTree.mutationVersion++;
|
|
379
|
+
|
|
380
|
+
return true;
|
|
412
381
|
}
|
|
413
382
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
383
|
+
/**
|
|
384
|
+
* Представляет обработку ошибочного хода в превью.
|
|
385
|
+
*/
|
|
386
|
+
function handleWrongOutcome(
|
|
387
|
+
outcome: TSolverMoveOutcome,
|
|
388
|
+
playedSan: string,
|
|
389
|
+
): void {
|
|
390
|
+
const solution = solutionPreviewTree;
|
|
391
|
+
if (!solution) return;
|
|
392
|
+
|
|
393
|
+
if (wrongRevealTimer !== null) {
|
|
394
|
+
clearTimeout(wrongRevealTimer);
|
|
395
|
+
wrongRevealTimer = null;
|
|
396
|
+
}
|
|
397
|
+
if (advanceLineTimer !== null) {
|
|
398
|
+
clearTimeout(advanceLineTimer);
|
|
399
|
+
advanceLineTimer = null;
|
|
400
|
+
}
|
|
401
|
+
if (opponentAutoTimer !== null) {
|
|
402
|
+
clearTimeout(opponentAutoTimer);
|
|
403
|
+
opponentAutoTimer = null;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (outcome.kind === "wrong_marked_variant") {
|
|
407
|
+
wrongFeedback =
|
|
408
|
+
"Неверно. Открываю введённый вариант в дереве, ход отменён.";
|
|
409
|
+
onOutcome?.("failed");
|
|
410
|
+
ensureStudentPreviewTreeHasPath(cursorPath);
|
|
411
|
+
const studentFork = studentPreviewTree.currentNode;
|
|
412
|
+
puzzlePreviewDupSubtreeUnderStudentCursor(
|
|
413
|
+
studentPreviewTree,
|
|
414
|
+
outcome.wrongBranchRoot,
|
|
415
|
+
);
|
|
416
|
+
studentPreviewTree.forcedNodeId = studentFork.id;
|
|
417
|
+
studentPreviewTree.currentNode = studentFork;
|
|
418
|
+
|
|
419
|
+
const solverColor = puzzleSolverColor();
|
|
420
|
+
normalizePuzzleTreeMainLine(
|
|
421
|
+
studentPreviewTree.rootNode.moves,
|
|
422
|
+
solverColor,
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
studentPreviewTree.mutationVersion++;
|
|
426
|
+
normalizeSolutionTree();
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (outcome.kind === "no_matching_variant") {
|
|
431
|
+
if (addAdHocWrongMoveToTrees(playedSan)) {
|
|
432
|
+
wrongFeedback =
|
|
433
|
+
"Неверно. Ход добавлен в дерево вариантов, позиция на доске отменена.";
|
|
434
|
+
} else {
|
|
435
|
+
wrongFeedback = PREVIEW_WRONG_NO_VARIANT_MESSAGE;
|
|
436
|
+
}
|
|
437
|
+
onOutcome?.("failed");
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
419
441
|
wrongFeedback = PREVIEW_WRONG_NO_VARIANT_MESSAGE;
|
|
420
|
-
|
|
421
|
-
onOutcome?.("failed");
|
|
422
|
-
return;
|
|
442
|
+
onOutcome?.("failed");
|
|
423
443
|
}
|
|
424
444
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
+
/**
|
|
446
|
+
* Представляет установку позиции и дерева ученика по пути из корня.
|
|
447
|
+
*/
|
|
448
|
+
function setStateFromPath(path: number[]): void {
|
|
449
|
+
dismissPendingWrongMoveReveal();
|
|
450
|
+
|
|
451
|
+
const solution = solutionPreviewTree;
|
|
452
|
+
if (!solution) return;
|
|
453
|
+
|
|
454
|
+
previewChess.setFen(layoutInitialFen);
|
|
455
|
+
let node = solution.rootNode.moves;
|
|
456
|
+
for (const idx of path) {
|
|
457
|
+
const child = node.children[idx];
|
|
458
|
+
if (!child) break;
|
|
459
|
+
previewChess.makeSanMove(child.data.san);
|
|
460
|
+
node = child;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
cursorPath = path;
|
|
464
|
+
syncStudentTreeFromSolutionPath(path);
|
|
465
|
+
chessboard.fen = previewChess.fen();
|
|
466
|
+
const leaf = puzzlePreviewNodeAtPath(solution.rootNode.moves, path);
|
|
467
|
+
chessboard.addLastMove(leaf?.data.lastMove ?? null);
|
|
468
|
+
syncMoveEvaluationNagBadge(chessboard, {
|
|
469
|
+
nags: undefined,
|
|
470
|
+
lastMove: leaf?.data.lastMove ?? null,
|
|
471
|
+
});
|
|
472
|
+
syncBoardDragFromChessTurn();
|
|
445
473
|
}
|
|
446
474
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
475
|
+
/**
|
|
476
|
+
* Представляет ответ соперника по главному варианту после хода решателя (без применения к движку).
|
|
477
|
+
*/
|
|
478
|
+
function getOpponentMainLineReply(
|
|
479
|
+
pathAfterSolver: number[],
|
|
480
|
+
): { san: string; path: number[] } | null {
|
|
481
|
+
const solution = solutionPreviewTree;
|
|
482
|
+
if (!solution) return null;
|
|
483
|
+
|
|
484
|
+
if (
|
|
485
|
+
puzzlePreviewSideToMoveFromFen(previewChess.fen()) ===
|
|
486
|
+
puzzleSolverColor()
|
|
487
|
+
) {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const node = puzzlePreviewNodeAtPath(
|
|
492
|
+
solution.rootNode.moves,
|
|
493
|
+
pathAfterSolver,
|
|
494
|
+
);
|
|
495
|
+
if (!node || node.children.length === 0) return null;
|
|
496
|
+
|
|
497
|
+
return {
|
|
498
|
+
san: node.children[0]!.data.san,
|
|
499
|
+
path: [...pathAfterSolver, 0],
|
|
500
|
+
};
|
|
470
501
|
}
|
|
471
502
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
)
|
|
476
|
-
|
|
503
|
+
/**
|
|
504
|
+
* Представляет проверку конца линии и переход к следующему варианту ответа соперника.
|
|
505
|
+
*/
|
|
506
|
+
function checkSolvedAndMaybeAdvance(): void {
|
|
507
|
+
const solution = solutionPreviewTree;
|
|
508
|
+
if (!solution) return;
|
|
477
509
|
|
|
478
|
-
|
|
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(
|
|
510
|
+
const leaf = puzzlePreviewNodeAtPath(
|
|
504
511
|
solution.rootNode.moves,
|
|
505
512
|
cursorPath,
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
513
|
+
);
|
|
514
|
+
if (
|
|
515
|
+
leaf &&
|
|
516
|
+
puzzlePreviewIsSolvedPosition(
|
|
517
|
+
previewChess,
|
|
518
|
+
leaf,
|
|
519
|
+
puzzleSolverColor(),
|
|
520
|
+
)
|
|
521
|
+
) {
|
|
522
|
+
if (advanceLineTimer !== null) {
|
|
523
|
+
clearTimeout(advanceLineTimer);
|
|
524
|
+
advanceLineTimer = null;
|
|
525
|
+
}
|
|
526
|
+
advanceLineTimer = setTimeout(() => {
|
|
527
|
+
const advanced = maybeAdvanceToNextOpponentLine();
|
|
528
|
+
if (!advanced) {
|
|
529
|
+
const target = puzzlePreviewNodeAtPath(
|
|
530
|
+
solution.rootNode.moves,
|
|
531
|
+
cursorPath,
|
|
532
|
+
);
|
|
533
|
+
solution.currentNode = target ?? solution.rootNode.moves;
|
|
534
|
+
solved = true;
|
|
535
|
+
onOutcome?.("solved");
|
|
536
|
+
syncBoardFromSolutionTreeNode();
|
|
537
|
+
}
|
|
538
|
+
syncBoardDragFromChessTurn();
|
|
539
|
+
advanceLineTimer = null;
|
|
540
|
+
}, 250);
|
|
511
541
|
}
|
|
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
542
|
}
|
|
527
543
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
544
|
+
/**
|
|
545
|
+
* Представляет анимированный автоматический ход соперника после хода решателя.
|
|
546
|
+
*/
|
|
547
|
+
function playOpponentAutoResponse(pathAfterSolver: number[]): void {
|
|
548
|
+
const reply = getOpponentMainLineReply(pathAfterSolver);
|
|
549
|
+
if (!reply) {
|
|
550
|
+
checkSolvedAndMaybeAdvance();
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
534
553
|
|
|
535
|
-
|
|
536
|
-
|
|
554
|
+
let opponentMove: Move;
|
|
555
|
+
try {
|
|
556
|
+
opponentMove = previewChess.makeSanMove(reply.san);
|
|
557
|
+
} catch {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
537
560
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
* Представляет отложенный автоматический ответ соперника после анимации хода решателя.
|
|
552
|
-
*/
|
|
553
|
-
function scheduleOpponentAutoResponse(): void {
|
|
554
|
-
if (opponentAutoTimer !== null) {
|
|
555
|
-
clearTimeout(opponentAutoTimer);
|
|
556
|
-
opponentAutoTimer = null;
|
|
561
|
+
cursorPath = reply.path;
|
|
562
|
+
syncStudentTreeFromSolutionPath(cursorPath);
|
|
563
|
+
|
|
564
|
+
chessboard.animationTime = OPPONENT_MOVE_ANIMATION_MS;
|
|
565
|
+
chessboard.fen = previewChess.fen();
|
|
566
|
+
chessboard.addLastMove(opponentMove);
|
|
567
|
+
syncMoveEvaluationNagBadge(chessboard, {
|
|
568
|
+
nags: undefined,
|
|
569
|
+
lastMove: opponentMove,
|
|
570
|
+
});
|
|
571
|
+
syncBoardDragFromChessTurn();
|
|
572
|
+
|
|
573
|
+
checkSolvedAndMaybeAdvance();
|
|
557
574
|
}
|
|
558
575
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
/**
|
|
568
|
-
* Представляет поиск следующей линии по развилкам соперника (DFS: чем глубже развилка — тем раньше возврат).
|
|
569
|
-
*/
|
|
570
|
-
function findNextOpponentLine(path: number[]): number[] | null {
|
|
571
|
-
const solution = solutionPreviewTree;
|
|
572
|
-
if (!solution) return null;
|
|
573
|
-
|
|
574
|
-
const solver = puzzleSolverColor();
|
|
575
|
-
|
|
576
|
-
let node = solution.rootNode.moves;
|
|
577
|
-
for (let depth = 0; depth <= path.length; depth++) {
|
|
578
|
-
const sideToMove = puzzlePreviewSideToMoveFromFen(node.data.fen);
|
|
579
|
-
const isOpponentToMove = sideToMove !== solver;
|
|
580
|
-
const isFork = node.children.length > 1;
|
|
581
|
-
|
|
582
|
-
if (isOpponentToMove && isFork && depth < path.length) {
|
|
583
|
-
const chosenIdx = path[depth]!;
|
|
584
|
-
if (chosenIdx < node.children.length - 1) {
|
|
585
|
-
/* кандидат на «следующую линию» с этого форка — ниже второй проход */
|
|
576
|
+
/**
|
|
577
|
+
* Представляет отложенный автоматический ответ соперника после анимации хода решателя.
|
|
578
|
+
*/
|
|
579
|
+
function scheduleOpponentAutoResponse(): void {
|
|
580
|
+
if (opponentAutoTimer !== null) {
|
|
581
|
+
clearTimeout(opponentAutoTimer);
|
|
582
|
+
opponentAutoTimer = null;
|
|
586
583
|
}
|
|
587
|
-
}
|
|
588
584
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
585
|
+
const pathAfterSolver = [...cursorPath];
|
|
586
|
+
const delayMs = chessboard.animationTime || OPPONENT_MOVE_ANIMATION_MS;
|
|
587
|
+
opponentAutoTimer = setTimeout(() => {
|
|
588
|
+
playOpponentAutoResponse(pathAfterSolver);
|
|
589
|
+
opponentAutoTimer = null;
|
|
590
|
+
}, delayMs);
|
|
593
591
|
}
|
|
594
592
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
593
|
+
/**
|
|
594
|
+
* Представляет поиск следующей линии по развилкам соперника (DFS: чем глубже развилка — тем раньше возврат).
|
|
595
|
+
*/
|
|
596
|
+
function findNextOpponentLine(path: number[]): number[] | null {
|
|
597
|
+
const solution = solutionPreviewTree;
|
|
598
|
+
if (!solution) return null;
|
|
599
|
+
|
|
600
|
+
const solver = puzzleSolverColor();
|
|
601
|
+
|
|
602
|
+
let node = solution.rootNode.moves;
|
|
603
|
+
for (let depth = 0; depth <= path.length; depth++) {
|
|
604
|
+
const sideToMove = puzzlePreviewSideToMoveFromFen(node.data.fen);
|
|
605
|
+
const isOpponentToMove = sideToMove !== solver;
|
|
606
|
+
const isFork = node.children.length > 1;
|
|
607
|
+
|
|
608
|
+
if (isOpponentToMove && isFork && depth < path.length) {
|
|
609
|
+
const chosenIdx = path[depth]!;
|
|
610
|
+
if (chosenIdx < node.children.length - 1) {
|
|
611
|
+
/* кандидат на «следующую линию» с этого форка — ниже второй проход */
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (depth === path.length) break;
|
|
616
|
+
const child = node.children[path[depth]!];
|
|
617
|
+
if (!child) break;
|
|
618
|
+
node = child;
|
|
606
619
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
620
|
+
|
|
621
|
+
node = solution.rootNode.moves;
|
|
622
|
+
let bestForkDepth: number | null = null;
|
|
623
|
+
let bestNextIdx = 0;
|
|
624
|
+
for (let depth = 0; depth < path.length; depth++) {
|
|
625
|
+
const sideToMove = puzzlePreviewSideToMoveFromFen(node.data.fen);
|
|
626
|
+
const isOpponentToMove = sideToMove !== solver;
|
|
627
|
+
if (isOpponentToMove && node.children.length > 1) {
|
|
628
|
+
const chosenIdx = path[depth]!;
|
|
629
|
+
if (chosenIdx < node.children.length - 1) {
|
|
630
|
+
bestForkDepth = depth;
|
|
631
|
+
bestNextIdx = chosenIdx + 1;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
const child = node.children[path[depth]!];
|
|
635
|
+
if (!child) break;
|
|
636
|
+
node = child;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (bestForkDepth === null) return null;
|
|
640
|
+
return [...path.slice(0, bestForkDepth), bestNextIdx];
|
|
611
641
|
}
|
|
612
642
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
643
|
+
/**
|
|
644
|
+
* Представляет переход на следующую линию ответов соперника, если возможно.
|
|
645
|
+
*/
|
|
646
|
+
function maybeAdvanceToNextOpponentLine(): boolean {
|
|
647
|
+
const nextBase = findNextOpponentLine(cursorPath);
|
|
648
|
+
if (!nextBase) return false;
|
|
616
649
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
*/
|
|
620
|
-
function maybeAdvanceToNextOpponentLine(): boolean {
|
|
621
|
-
const nextBase = findNextOpponentLine(cursorPath);
|
|
622
|
-
if (!nextBase) return false;
|
|
650
|
+
solved = false;
|
|
651
|
+
wrongFeedback = null;
|
|
623
652
|
|
|
624
|
-
|
|
625
|
-
|
|
653
|
+
setStateFromPath(nextBase);
|
|
654
|
+
return true;
|
|
655
|
+
}
|
|
626
656
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
657
|
+
/**
|
|
658
|
+
* Представляет попытку применить SAN хода решателя с автоответами соперника.
|
|
659
|
+
*/
|
|
660
|
+
function tryApplySolverSan(san: string): Move | undefined {
|
|
661
|
+
const solution = solutionPreviewTree;
|
|
662
|
+
if (!solution || solved) return undefined;
|
|
630
663
|
|
|
631
|
-
|
|
632
|
-
* Представляет попытку применить SAN хода решателя с автоответами соперника.
|
|
633
|
-
*/
|
|
634
|
-
function tryApplySolverSan(san: string): Move | undefined {
|
|
635
|
-
const solution = solutionPreviewTree;
|
|
636
|
-
if (!solution || solved) return undefined;
|
|
664
|
+
cancelPendingWrongMoveReveal();
|
|
637
665
|
|
|
638
|
-
|
|
666
|
+
const cursorNode = puzzlePreviewNodeAtPath(
|
|
667
|
+
solution.rootNode.moves,
|
|
668
|
+
cursorPath,
|
|
669
|
+
);
|
|
670
|
+
if (!cursorNode) return undefined;
|
|
671
|
+
|
|
672
|
+
const outcome = puzzlePreviewClassifySolverMove(
|
|
673
|
+
cursorNode,
|
|
674
|
+
previewChess.fen(),
|
|
675
|
+
san,
|
|
676
|
+
puzzleSolverColor(),
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
if (outcome.kind !== "correct") {
|
|
680
|
+
const revertFen = previewChess.fen();
|
|
681
|
+
const revertLastMove = cursorNode.data.lastMove ?? null;
|
|
682
|
+
try {
|
|
683
|
+
const wrongMove = previewChess.makeSanMove(san);
|
|
684
|
+
pendingWrongMoveReveal = {
|
|
685
|
+
outcome,
|
|
686
|
+
playedSan: san,
|
|
687
|
+
revertFen,
|
|
688
|
+
revertLastMove,
|
|
689
|
+
};
|
|
690
|
+
lastSolverMoveForBoard = wrongMove;
|
|
691
|
+
return wrongMove;
|
|
692
|
+
} catch {
|
|
693
|
+
pendingWrongMoveReveal = null;
|
|
694
|
+
wrongFeedback = "Ход недопустим в этой позиции.";
|
|
695
|
+
onOutcome?.("failed");
|
|
696
|
+
return undefined;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
639
699
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
700
|
+
try {
|
|
701
|
+
const pathAfterSolver = [...cursorPath, outcome.childIndex];
|
|
702
|
+
const moveRecord = previewChess.makeSanMove(san);
|
|
703
|
+
cursorPath = pathAfterSolver;
|
|
704
|
+
syncStudentTreeFromSolutionPath(pathAfterSolver);
|
|
645
705
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
san,
|
|
650
|
-
puzzleSolverColor(),
|
|
651
|
-
);
|
|
706
|
+
wrongFeedback = null;
|
|
707
|
+
lastSolverMoveForBoard = moveRecord;
|
|
708
|
+
syncBoardDragFromChessTurn();
|
|
652
709
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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
|
-
}
|
|
710
|
+
return moveRecord;
|
|
711
|
+
} catch {
|
|
712
|
+
wrongFeedback = "Ход недопустим в этой позиции.";
|
|
713
|
+
onOutcome?.("failed");
|
|
714
|
+
return undefined;
|
|
715
|
+
}
|
|
672
716
|
}
|
|
673
717
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
718
|
+
const actions: IChessBoardActions = {
|
|
719
|
+
game: {
|
|
720
|
+
possibleMovesOnSquare: (square: Square) =>
|
|
721
|
+
previewChess.moves(square),
|
|
722
|
+
beforePieceMoveSan(san: string) {
|
|
723
|
+
if (solved) {
|
|
724
|
+
try {
|
|
725
|
+
const move = previewChess.makeSanMove(san);
|
|
726
|
+
const fen = previewChess.fen();
|
|
727
|
+
const { halfMoves, fullMoves } = calculatePly(fen);
|
|
728
|
+
solutionPreviewTree?.addNodeToCurrent({
|
|
729
|
+
id: "",
|
|
730
|
+
children: [],
|
|
731
|
+
data: {
|
|
732
|
+
fen,
|
|
733
|
+
san,
|
|
734
|
+
ply: halfMoves,
|
|
735
|
+
fullMoves,
|
|
736
|
+
lastMove: move,
|
|
737
|
+
},
|
|
738
|
+
});
|
|
739
|
+
return {
|
|
740
|
+
move,
|
|
741
|
+
fen,
|
|
742
|
+
turn: previewChess.turn(),
|
|
743
|
+
};
|
|
744
|
+
} catch {
|
|
745
|
+
return undefined;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
const move = tryApplySolverSan(san);
|
|
749
|
+
if (!move) return undefined;
|
|
750
|
+
return {
|
|
751
|
+
move,
|
|
752
|
+
fen: previewChess.fen(),
|
|
753
|
+
turn: previewChess.turn(),
|
|
754
|
+
};
|
|
755
|
+
},
|
|
756
|
+
afterPieceMoveSan: (move) => {
|
|
757
|
+
lastSolverMoveForBoard = null;
|
|
758
|
+
if (solved) {
|
|
759
|
+
syncAnalysisBoardFromCurrentNode();
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
afterSolverMoveOnBoard(move);
|
|
763
|
+
},
|
|
764
|
+
beforePieceMove: (from, to, promotion) => {
|
|
765
|
+
if (solved) {
|
|
766
|
+
try {
|
|
767
|
+
const san = previewChess.getSanForMove({
|
|
768
|
+
from,
|
|
769
|
+
to,
|
|
770
|
+
promotion,
|
|
771
|
+
});
|
|
772
|
+
const move = previewChess.makeSanMove(san);
|
|
773
|
+
const fen = previewChess.fen();
|
|
774
|
+
const { halfMoves, fullMoves } = calculatePly(fen);
|
|
775
|
+
solutionPreviewTree?.addNodeToCurrent({
|
|
776
|
+
id: "",
|
|
777
|
+
children: [],
|
|
778
|
+
data: {
|
|
779
|
+
fen,
|
|
780
|
+
san,
|
|
781
|
+
ply: halfMoves,
|
|
782
|
+
fullMoves,
|
|
783
|
+
lastMove: move,
|
|
784
|
+
},
|
|
785
|
+
});
|
|
786
|
+
return fen;
|
|
787
|
+
} catch {
|
|
788
|
+
return "";
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
const san = previewChess.getSanForMove({ from, to, promotion });
|
|
792
|
+
const move = tryApplySolverSan(san);
|
|
793
|
+
if (!move) return "";
|
|
794
|
+
return previewChess.fen();
|
|
795
|
+
},
|
|
796
|
+
afterPieceMove: () => {
|
|
797
|
+
if (solved) {
|
|
798
|
+
lastSolverMoveForBoard = null;
|
|
799
|
+
syncAnalysisBoardFromCurrentNode();
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
const move = lastSolverMoveForBoard;
|
|
803
|
+
lastSolverMoveForBoard = null;
|
|
804
|
+
if (move) {
|
|
805
|
+
afterSolverMoveOnBoard(move);
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
syncBoardDragFromChessTurn();
|
|
809
|
+
},
|
|
810
|
+
},
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
let chessboard = $derived.by(() => {
|
|
814
|
+
const merged = buildPuzzleWizardBoardSettings(boardAppearanceSettings);
|
|
815
|
+
return createBoardApi({
|
|
816
|
+
fen: INITIAL_FEN,
|
|
817
|
+
playSettings:
|
|
818
|
+
boardSize !== undefined
|
|
819
|
+
? { ...merged.play, boardSize }
|
|
820
|
+
: merged.play,
|
|
821
|
+
actions: {
|
|
822
|
+
...actions,
|
|
823
|
+
onResizeAction,
|
|
824
|
+
},
|
|
825
|
+
});
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
const treeForViewer = $derived(
|
|
829
|
+
solved && solutionPreviewTree
|
|
830
|
+
? solutionPreviewTree
|
|
831
|
+
: studentPreviewTree,
|
|
832
|
+
);
|
|
833
|
+
|
|
834
|
+
$effect(() => {
|
|
835
|
+
const trimmedPgn = pgn.trim();
|
|
836
|
+
const rootFen = fenProp?.trim() ? fenProp.trim() : INITIAL_FEN;
|
|
837
|
+
|
|
838
|
+
if (!trimmedPgn) {
|
|
839
|
+
untrack(() => {
|
|
840
|
+
layoutInitialFen = rootFen;
|
|
841
|
+
solutionPreviewTree = null;
|
|
842
|
+
previewChess.setFen(rootFen);
|
|
843
|
+
studentPreviewTree.replaceRootTree(
|
|
844
|
+
createEmptyTreeFromFen(rootFen),
|
|
845
|
+
);
|
|
846
|
+
studentPreviewTree.currentNode =
|
|
847
|
+
studentPreviewTree.rootNode.moves;
|
|
848
|
+
chessboard.fen = rootFen;
|
|
849
|
+
chessboard.orientation =
|
|
850
|
+
rootFen.trim().split(/\s+/)[1] === "b"
|
|
851
|
+
? Color.BLACK
|
|
852
|
+
: Color.WHITE;
|
|
853
|
+
syncMoveEvaluationNagBadge(chessboard, {
|
|
854
|
+
nags: undefined,
|
|
855
|
+
lastMove: null,
|
|
856
|
+
});
|
|
857
|
+
cursorPath = [];
|
|
858
|
+
solved = false;
|
|
859
|
+
wrongFeedback = null;
|
|
860
|
+
syncBoardDragFromChessTurn();
|
|
711
861
|
});
|
|
712
|
-
return
|
|
713
|
-
move,
|
|
714
|
-
fen,
|
|
715
|
-
turn: previewChess.turn(),
|
|
716
|
-
};
|
|
717
|
-
} catch {
|
|
718
|
-
return undefined;
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
const move = tryApplySolverSan(san);
|
|
722
|
-
if (!move) return undefined;
|
|
723
|
-
return {
|
|
724
|
-
move,
|
|
725
|
-
fen: previewChess.fen(),
|
|
726
|
-
turn: previewChess.turn(),
|
|
727
|
-
};
|
|
728
|
-
},
|
|
729
|
-
afterPieceMoveSan: (move) => {
|
|
730
|
-
lastSolverMoveForBoard = null;
|
|
731
|
-
if (solved) {
|
|
732
|
-
syncAnalysisBoardFromCurrentNode();
|
|
733
|
-
return;
|
|
862
|
+
return () => clearTimers();
|
|
734
863
|
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
864
|
+
|
|
865
|
+
untrack(() => {
|
|
866
|
+
const fullPgn = mergePgnWithSetupFen(trimmedPgn, rootFen);
|
|
867
|
+
const { rootNode } = transformPgnToChessNode(fullPgn);
|
|
868
|
+
|
|
869
|
+
layoutInitialFen = rootFen;
|
|
870
|
+
const solutionTree = new ChessTree(rootNode);
|
|
871
|
+
syncPuzzleBranchNagsOnTree(
|
|
872
|
+
solutionTree,
|
|
873
|
+
rootFen.trim().split(/\s+/)[1] === "b"
|
|
874
|
+
? Color.BLACK
|
|
875
|
+
: Color.WHITE,
|
|
876
|
+
);
|
|
877
|
+
solutionPreviewTree = solutionTree;
|
|
878
|
+
|
|
879
|
+
cursorPath = [];
|
|
880
|
+
solved = false;
|
|
881
|
+
wrongFeedback = null;
|
|
882
|
+
previewChess.setFen(rootFen);
|
|
883
|
+
studentPreviewTree.replaceRootTree(
|
|
884
|
+
createStudentPreviewTreeFromFen(rootFen, rootNode.comments),
|
|
885
|
+
);
|
|
886
|
+
studentPreviewTree.currentNode = studentPreviewTree.rootNode.moves;
|
|
887
|
+
chessboard.fen = rootFen;
|
|
888
|
+
chessboard.orientation =
|
|
889
|
+
rootFen.trim().split(/\s+/)[1] === "b"
|
|
890
|
+
? Color.BLACK
|
|
891
|
+
: Color.WHITE;
|
|
892
|
+
syncMoveEvaluationNagBadge(chessboard, {
|
|
893
|
+
nags: undefined,
|
|
894
|
+
lastMove: null,
|
|
754
895
|
});
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
if (
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
return;
|
|
896
|
+
syncBoardDragFromChessTurn();
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
return () => clearTimers();
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Представляет сброс отложенных таймеров превью.
|
|
904
|
+
*/
|
|
905
|
+
function clearTimers(): void {
|
|
906
|
+
dismissPendingWrongMoveReveal();
|
|
907
|
+
if (advanceLineTimer !== null) {
|
|
908
|
+
clearTimeout(advanceLineTimer);
|
|
909
|
+
advanceLineTimer = null;
|
|
770
910
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
afterSolverMoveOnBoard(move);
|
|
775
|
-
return;
|
|
911
|
+
if (opponentAutoTimer !== null) {
|
|
912
|
+
clearTimeout(opponentAutoTimer);
|
|
913
|
+
opponentAutoTimer = null;
|
|
776
914
|
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Представляет синхронизацию доски с текущим узлом полного дерева решения (после `solved`).
|
|
919
|
+
*/
|
|
920
|
+
function syncBoardFromSolutionTreeNode(): void {
|
|
921
|
+
const tree = solutionPreviewTree;
|
|
922
|
+
if (!tree || !solved) return;
|
|
923
|
+
|
|
924
|
+
const node = tree.currentNode;
|
|
925
|
+
|
|
926
|
+
previewChess.setFen(node.data.fen);
|
|
927
|
+
chessboard.fen = previewChess.fen();
|
|
928
|
+
chessboard.addLastMove(node.data.lastMove ?? null);
|
|
929
|
+
syncMoveEvaluationNagBadge(
|
|
930
|
+
chessboard,
|
|
931
|
+
nodeDataForAnalysisBoardBadge(node.data),
|
|
932
|
+
);
|
|
933
|
+
chessboard.setNodeMarkers(node.data.parsedFirstComment?.shapes);
|
|
777
934
|
syncBoardDragFromChessTurn();
|
|
778
|
-
},
|
|
779
|
-
},
|
|
780
|
-
};
|
|
781
|
-
|
|
782
|
-
let chessboard = $derived.by(() => {
|
|
783
|
-
const merged = buildPuzzleWizardBoardSettings(boardAppearanceSettings);
|
|
784
|
-
return createBoardApi({
|
|
785
|
-
fen: INITIAL_FEN,
|
|
786
|
-
playSettings:
|
|
787
|
-
boardSize !== undefined
|
|
788
|
-
? { ...merged.play, boardSize }
|
|
789
|
-
: merged.play,
|
|
790
|
-
actions: {
|
|
791
|
-
...actions,
|
|
792
|
-
onResizeAction,
|
|
793
|
-
},
|
|
794
|
-
});
|
|
795
|
-
});
|
|
796
|
-
|
|
797
|
-
const chessboardDesign = $derived({
|
|
798
|
-
...buildPuzzleWizardBoardSettings(boardAppearanceSettings).design,
|
|
799
|
-
theme: boardTheme ?? CHESSBOARD_THEMES.blue,
|
|
800
|
-
});
|
|
801
|
-
|
|
802
|
-
const treeForViewer = $derived(
|
|
803
|
-
solved && solutionPreviewTree ? solutionPreviewTree : studentPreviewTree,
|
|
804
|
-
);
|
|
805
|
-
|
|
806
|
-
$effect(() => {
|
|
807
|
-
const trimmedPgn = pgn.trim();
|
|
808
|
-
const rootFen = fenProp?.trim() ? fenProp.trim() : INITIAL_FEN;
|
|
809
|
-
|
|
810
|
-
if (!trimmedPgn) {
|
|
811
|
-
untrack(() => {
|
|
812
|
-
layoutInitialFen = rootFen;
|
|
813
|
-
solutionPreviewTree = null;
|
|
814
|
-
previewChess.setFen(rootFen);
|
|
815
|
-
studentPreviewTree.replaceRootTree(createEmptyTreeFromFen(rootFen));
|
|
816
|
-
studentPreviewTree.currentNode = studentPreviewTree.rootNode.moves;
|
|
817
|
-
chessboard.fen = rootFen;
|
|
818
|
-
chessboard.orientation =
|
|
819
|
-
rootFen.trim().split(/\s+/)[1] === "b"
|
|
820
|
-
? Color.BLACK
|
|
821
|
-
: Color.WHITE;
|
|
822
|
-
syncMoveEvaluationNagBadge(chessboard, {
|
|
823
|
-
nags: undefined,
|
|
824
|
-
lastMove: null,
|
|
825
|
-
});
|
|
826
|
-
cursorPath = [];
|
|
827
|
-
solved = false;
|
|
828
|
-
wrongFeedback = null;
|
|
829
|
-
syncBoardDragFromChessTurn();
|
|
830
|
-
});
|
|
831
|
-
return () => clearTimers();
|
|
832
935
|
}
|
|
833
936
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
solutionTree,
|
|
842
|
-
rootFen.trim().split(/\s+/)[1] === "b" ? Color.BLACK : Color.WHITE,
|
|
843
|
-
);
|
|
844
|
-
solutionPreviewTree = solutionTree;
|
|
845
|
-
|
|
846
|
-
cursorPath = [];
|
|
847
|
-
solved = false;
|
|
848
|
-
wrongFeedback = null;
|
|
849
|
-
previewChess.setFen(rootFen);
|
|
850
|
-
studentPreviewTree.replaceRootTree(
|
|
851
|
-
createStudentPreviewTreeFromFen(rootFen, rootNode.comments),
|
|
852
|
-
);
|
|
853
|
-
studentPreviewTree.currentNode = studentPreviewTree.rootNode.moves;
|
|
854
|
-
chessboard.fen = rootFen;
|
|
855
|
-
chessboard.orientation =
|
|
856
|
-
rootFen.trim().split(/\s+/)[1] === "b" ? Color.BLACK : Color.WHITE;
|
|
857
|
-
syncMoveEvaluationNagBadge(chessboard, {
|
|
858
|
-
nags: undefined,
|
|
859
|
-
lastMove: null,
|
|
860
|
-
});
|
|
861
|
-
syncBoardDragFromChessTurn();
|
|
862
|
-
});
|
|
937
|
+
/**
|
|
938
|
+
* Представляет установку FEN движка при навигации по дереву (только после полного решения превью).
|
|
939
|
+
*/
|
|
940
|
+
function treeSetChessFen(fen: string): void {
|
|
941
|
+
if (!solved) return;
|
|
942
|
+
previewChess.setFen(fen);
|
|
943
|
+
}
|
|
863
944
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
function clearTimers(): void {
|
|
871
|
-
dismissPendingWrongMoveReveal();
|
|
872
|
-
if (advanceLineTimer !== null) {
|
|
873
|
-
clearTimeout(advanceLineTimer);
|
|
874
|
-
advanceLineTimer = null;
|
|
945
|
+
/**
|
|
946
|
+
* Представляет обновление UI доски после смены узла в дереве решения.
|
|
947
|
+
*/
|
|
948
|
+
function treeSetChessboardFen(_animationTime: number | undefined): void {
|
|
949
|
+
if (!solved) return;
|
|
950
|
+
syncBoardFromSolutionTreeNode();
|
|
875
951
|
}
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Представляет реакцию на выбор узла в дереве (клик по ходу после решения).
|
|
955
|
+
*/
|
|
956
|
+
function treeOnSelectNode(): void {
|
|
957
|
+
if (!solved) return;
|
|
958
|
+
syncBoardFromSolutionTreeNode();
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* Представляет синхронизацию доски после операций с вариантами (в режиме просмотра без правки не используется).
|
|
963
|
+
*/
|
|
964
|
+
function treeOnDeleteVariant(): void {
|
|
965
|
+
if (!solved) return;
|
|
966
|
+
syncBoardFromSolutionTreeNode();
|
|
879
967
|
}
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
/**
|
|
883
|
-
* Представляет синхронизацию доски с текущим узлом полного дерева решения (после `solved`).
|
|
884
|
-
*/
|
|
885
|
-
function syncBoardFromSolutionTreeNode(): void {
|
|
886
|
-
const tree = solutionPreviewTree;
|
|
887
|
-
if (!tree || !solved) return;
|
|
888
|
-
|
|
889
|
-
const node = tree.currentNode;
|
|
890
|
-
|
|
891
|
-
previewChess.setFen(node.data.fen);
|
|
892
|
-
chessboard.fen = previewChess.fen();
|
|
893
|
-
chessboard.addLastMove(node.data.lastMove ?? null);
|
|
894
|
-
syncMoveEvaluationNagBadge(
|
|
895
|
-
chessboard,
|
|
896
|
-
nodeDataForAnalysisBoardBadge(node.data),
|
|
897
|
-
);
|
|
898
|
-
chessboard.setNodeMarkers(node.data.parsedFirstComment?.shapes);
|
|
899
|
-
syncBoardDragFromChessTurn();
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
/**
|
|
903
|
-
* Представляет установку FEN движка при навигации по дереву (только после полного решения превью).
|
|
904
|
-
*/
|
|
905
|
-
function treeSetChessFen(fen: string): void {
|
|
906
|
-
if (!solved) return;
|
|
907
|
-
previewChess.setFen(fen);
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
/**
|
|
911
|
-
* Представляет обновление UI доски после смены узла в дереве решения.
|
|
912
|
-
*/
|
|
913
|
-
function treeSetChessboardFen(_animationTime: number | undefined): void {
|
|
914
|
-
if (!solved) return;
|
|
915
|
-
syncBoardFromSolutionTreeNode();
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
/**
|
|
919
|
-
* Представляет реакцию на выбор узла в дереве (клик по ходу после решения).
|
|
920
|
-
*/
|
|
921
|
-
function treeOnSelectNode(): void {
|
|
922
|
-
if (!solved) return;
|
|
923
|
-
syncBoardFromSolutionTreeNode();
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
/**
|
|
927
|
-
* Представляет синхронизацию доски после операций с вариантами (в режиме просмотра без правки не используется).
|
|
928
|
-
*/
|
|
929
|
-
function treeOnDeleteVariant(): void {
|
|
930
|
-
if (!solved) return;
|
|
931
|
-
syncBoardFromSolutionTreeNode();
|
|
932
|
-
}
|
|
933
968
|
</script>
|
|
934
969
|
|
|
935
970
|
<div class={cn("flex w-full flex-col gap-2", className)}>
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
971
|
+
<PuzzleBoardTreeViewerPane
|
|
972
|
+
{chessboard}
|
|
973
|
+
chessTree={treeForViewer}
|
|
974
|
+
onSelectNode={treeOnSelectNode}
|
|
975
|
+
onDeleteVariant={treeOnDeleteVariant}
|
|
976
|
+
setChessFen={treeSetChessFen}
|
|
977
|
+
setChessboardFen={treeSetChessboardFen}
|
|
978
|
+
editMode={false}
|
|
979
|
+
selectable={solved}
|
|
980
|
+
>
|
|
981
|
+
{#snippet belowChessboard()}
|
|
982
|
+
{#if wrongFeedback && wrongFeedback !== PREVIEW_WRONG_NO_VARIANT_MESSAGE}
|
|
983
|
+
<p
|
|
984
|
+
class="text-sm font-medium text-amber-600 dark:text-amber-400"
|
|
985
|
+
>
|
|
986
|
+
{wrongFeedback}
|
|
987
|
+
</p>
|
|
988
|
+
{/if}
|
|
989
|
+
{/snippet}
|
|
990
|
+
</PuzzleBoardTreeViewerPane>
|
|
955
991
|
</div>
|