@connectorvol/chess-widgets 8.0.1 → 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/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 +28 -10
- package/dist/game-analyzer/gameAnalyzer.svelte.js +6 -9
- package/dist/game-analyzer/types.d.ts +13 -8
- 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 +198 -98
- package/dist/puzzle-creation/PuzzleBoardTreeViewerPane.svelte.d.ts +20 -2
- package/dist/puzzle-creation/PuzzleCreationWizard.svelte +100 -106
- package/dist/puzzle-creation/PuzzleCreationWizard.svelte.d.ts +47 -3
- package/dist/puzzle-creation/PuzzlePgnBoardTreeEditor.svelte +945 -498
- package/dist/puzzle-creation/PuzzleWizardTagsStep.svelte +36 -0
- package/dist/puzzle-creation/PuzzleWizardTagsStep.svelte.d.ts +12 -0
- package/dist/puzzle-creation/StepMoves.svelte +210 -188
- package/dist/puzzle-creation/StepPosition.svelte +35 -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 +36 -10
- package/package.json +22 -17
|
@@ -1,544 +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
|
-
|
|
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
|
-
? previewChess.turn() === Color.WHITE
|
|
84
|
-
? Draggable.WHITE
|
|
85
|
-
: Draggable.BLACK
|
|
86
|
-
: previewChess.turn() === solver
|
|
87
|
-
? solver === Color.WHITE
|
|
88
|
-
? Draggable.WHITE
|
|
89
|
-
: Draggable.BLACK
|
|
90
|
-
: Draggable.NONE;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Представляет добавление пути в дерево ученика без очистки уже пройденных линий.
|
|
95
|
-
*/
|
|
96
|
-
function ensureStudentPreviewTreeHasPath(path: number[]): void {
|
|
97
|
-
const solution = solutionPreviewTree;
|
|
98
|
-
if (!solution) return;
|
|
99
|
-
|
|
100
|
-
studentPreviewTree.currentNode = studentPreviewTree.rootNode.moves;
|
|
101
|
-
|
|
102
|
-
let src = solution.rootNode.moves;
|
|
103
|
-
for (const idx of path) {
|
|
104
|
-
const next = src.children[idx];
|
|
105
|
-
if (!next) break;
|
|
106
|
-
studentPreviewTree.addNodeToCurrent({
|
|
107
|
-
id: "",
|
|
108
|
-
children: [],
|
|
109
|
-
data: { ...next.data },
|
|
110
|
-
});
|
|
111
|
-
src = next;
|
|
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;
|
|
112
83
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
84
|
+
|
|
85
|
+
let cursorPath = $state<number[]>([]);
|
|
86
|
+
|
|
87
|
+
const OPPONENT_MOVE_ANIMATION_MS = 300;
|
|
88
|
+
|
|
89
|
+
let wrongRevealTimer: ReturnType<typeof setTimeout> | null = null;
|
|
90
|
+
let advanceLineTimer: ReturnType<typeof setTimeout> | null = null;
|
|
91
|
+
let opponentAutoTimer: ReturnType<typeof setTimeout> | null = null;
|
|
92
|
+
|
|
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
|
+
};
|
|
104
|
+
|
|
105
|
+
let pendingWrongMoveReveal = $state<TPendingWrongMoveReveal | null>(null);
|
|
106
|
+
|
|
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),
|
|
114
|
+
);
|
|
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;
|
|
125
137
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
138
|
+
|
|
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();
|
|
129
160
|
}
|
|
130
161
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Представляет досрочное завершение показа ошибочного хода перед следующим ходом решателя.
|
|
164
|
+
*/
|
|
165
|
+
function cancelPendingWrongMoveReveal(): void {
|
|
166
|
+
if (pendingWrongMoveReveal === null) return;
|
|
167
|
+
|
|
168
|
+
if (wrongRevealTimer !== null) {
|
|
169
|
+
clearTimeout(wrongRevealTimer);
|
|
170
|
+
wrongRevealTimer = null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
finishWrongMoveReveal();
|
|
141
174
|
}
|
|
142
175
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
node = child;
|
|
176
|
+
/**
|
|
177
|
+
* Представляет показ NAG ✗ на доске и отложенный откат ошибочного хода.
|
|
178
|
+
*/
|
|
179
|
+
function scheduleWrongMoveReveal(wrongMove: Move): void {
|
|
180
|
+
const pending = pendingWrongMoveReveal;
|
|
181
|
+
if (pending === null) return;
|
|
182
|
+
|
|
183
|
+
syncMoveEvaluationNagBadge(chessboard, {
|
|
184
|
+
nags: [PUZZLE_BRANCH_WRONG_NAG_ID],
|
|
185
|
+
lastMove: wrongMove,
|
|
186
|
+
});
|
|
187
|
+
syncBoardDragFromChessTurn();
|
|
188
|
+
|
|
189
|
+
wrongRevealTimer = setTimeout(() => {
|
|
190
|
+
wrongRevealTimer = null;
|
|
191
|
+
finishWrongMoveReveal();
|
|
192
|
+
}, PREVIEW_WRONG_MOVE_REVEAL_MS);
|
|
161
193
|
}
|
|
162
194
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
195
|
+
/**
|
|
196
|
+
* Представляет завершение показа ошибочного хода: запись варианта в дерево и откат позиции на доске.
|
|
197
|
+
*/
|
|
198
|
+
function finishWrongMoveReveal(): void {
|
|
199
|
+
const pending = pendingWrongMoveReveal;
|
|
200
|
+
pendingWrongMoveReveal = null;
|
|
201
|
+
if (pending === null) return;
|
|
202
|
+
|
|
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();
|
|
178
213
|
}
|
|
179
214
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
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
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Представляет обработку UI после применения хода решателя на доске.
|
|
258
|
+
*/
|
|
259
|
+
function afterSolverMoveOnBoard(move: Move): void {
|
|
260
|
+
if (pendingWrongMoveReveal !== null) {
|
|
261
|
+
scheduleWrongMoveReveal(move);
|
|
262
|
+
return;
|
|
209
263
|
}
|
|
210
|
-
}
|
|
211
264
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
265
|
+
syncCorrectSolverMoveNagBadge(move);
|
|
266
|
+
syncBoardDragFromChessTurn();
|
|
267
|
+
if (!solved) {
|
|
268
|
+
scheduleOpponentAutoResponse();
|
|
269
|
+
}
|
|
216
270
|
}
|
|
217
271
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
272
|
+
/**
|
|
273
|
+
* Представляет добавление пути в дерево ученика без очистки уже пройденных линий.
|
|
274
|
+
*/
|
|
275
|
+
function ensureStudentPreviewTreeHasPath(path: number[]): void {
|
|
276
|
+
const solution = solutionPreviewTree;
|
|
277
|
+
if (!solution) return;
|
|
278
|
+
|
|
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;
|
|
229
310
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
311
|
+
|
|
312
|
+
const solverColor = puzzleSolverColor();
|
|
313
|
+
normalizePuzzleTreeMainLine(
|
|
314
|
+
studentPreviewTree.rootNode.moves,
|
|
315
|
+
solverColor,
|
|
316
|
+
);
|
|
234
317
|
}
|
|
235
318
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
*/
|
|
243
|
-
function maybeAdvanceToNextOpponentLine(): boolean {
|
|
244
|
-
const nextBase = findNextOpponentLine(cursorPath);
|
|
245
|
-
if (!nextBase) return false;
|
|
246
|
-
|
|
247
|
-
solved = false;
|
|
248
|
-
wrongFeedback = null;
|
|
249
|
-
|
|
250
|
-
setStateFromPath(nextBase);
|
|
251
|
-
return true;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Представляет попытку применить SAN хода решателя с автоответами соперника.
|
|
256
|
-
*/
|
|
257
|
-
function tryApplySolverSan(san: string): Move | undefined {
|
|
258
|
-
const solution = solutionPreviewTree;
|
|
259
|
-
if (!solution || solved) return undefined;
|
|
260
|
-
|
|
261
|
-
const cursorNode = puzzlePreviewNodeAtPath(
|
|
262
|
-
solution.rootNode.moves,
|
|
263
|
-
cursorPath,
|
|
264
|
-
);
|
|
265
|
-
if (!cursorNode) return undefined;
|
|
319
|
+
/**
|
|
320
|
+
* Представляет нормализацию главной линии и NAG только в дереве решения (PGN).
|
|
321
|
+
*/
|
|
322
|
+
function normalizeSolutionTree(): void {
|
|
323
|
+
const solution = solutionPreviewTree;
|
|
324
|
+
if (!solution) return;
|
|
266
325
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
+
);
|
|
334
|
+
}
|
|
273
335
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
336
|
+
/**
|
|
337
|
+
* Представляет синхронизацию дерева ученика с пройденным путём решения (после верного хода).
|
|
338
|
+
*/
|
|
339
|
+
function syncStudentTreeFromSolutionPath(path: number[]): void {
|
|
340
|
+
ensureStudentPreviewTreeHasPath(path);
|
|
341
|
+
studentPreviewTree.forcedNodeId = null;
|
|
342
|
+
studentPreviewTree.mutationVersion++;
|
|
277
343
|
}
|
|
278
344
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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;
|
|
381
|
+
}
|
|
284
382
|
|
|
285
|
-
|
|
286
|
-
|
|
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
|
+
}
|
|
287
397
|
if (advanceLineTimer !== null) {
|
|
288
|
-
|
|
289
|
-
|
|
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;
|
|
290
428
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
return moveRecord;
|
|
306
|
-
} catch {
|
|
307
|
-
wrongFeedback = "Ход недопустим в этой позиции.";
|
|
308
|
-
onOutcome?.("failed");
|
|
309
|
-
return undefined;
|
|
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
|
+
|
|
441
|
+
wrongFeedback = PREVIEW_WRONG_NO_VARIANT_MESSAGE;
|
|
442
|
+
onOutcome?.("failed");
|
|
310
443
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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();
|
|
473
|
+
}
|
|
474
|
+
|
|
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;
|
|
341
489
|
}
|
|
342
|
-
|
|
343
|
-
|
|
490
|
+
|
|
491
|
+
const node = puzzlePreviewNodeAtPath(
|
|
492
|
+
solution.rootNode.moves,
|
|
493
|
+
pathAfterSolver,
|
|
494
|
+
);
|
|
495
|
+
if (!node || node.children.length === 0) return null;
|
|
496
|
+
|
|
344
497
|
return {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
turn: previewChess.turn(),
|
|
498
|
+
san: node.children[0]!.data.san,
|
|
499
|
+
path: [...pathAfterSolver, 0],
|
|
348
500
|
};
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Представляет проверку конца линии и переход к следующему варианту ответа соперника.
|
|
505
|
+
*/
|
|
506
|
+
function checkSolvedAndMaybeAdvance(): void {
|
|
507
|
+
const solution = solutionPreviewTree;
|
|
508
|
+
if (!solution) return;
|
|
509
|
+
|
|
510
|
+
const leaf = puzzlePreviewNodeAtPath(
|
|
511
|
+
solution.rootNode.moves,
|
|
512
|
+
cursorPath,
|
|
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);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Представляет анимированный автоматический ход соперника после хода решателя.
|
|
546
|
+
*/
|
|
547
|
+
function playOpponentAutoResponse(pathAfterSolver: number[]): void {
|
|
548
|
+
const reply = getOpponentMainLineReply(pathAfterSolver);
|
|
549
|
+
if (!reply) {
|
|
550
|
+
checkSolvedAndMaybeAdvance();
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
let opponentMove: Move;
|
|
555
|
+
try {
|
|
556
|
+
opponentMove = previewChess.makeSanMove(reply.san);
|
|
557
|
+
} catch {
|
|
558
|
+
return;
|
|
375
559
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
560
|
+
|
|
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
|
+
});
|
|
382
571
|
syncBoardDragFromChessTurn();
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
572
|
+
|
|
573
|
+
checkSolvedAndMaybeAdvance();
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Представляет отложенный автоматический ответ соперника после анимации хода решателя.
|
|
578
|
+
*/
|
|
579
|
+
function scheduleOpponentAutoResponse(): void {
|
|
580
|
+
if (opponentAutoTimer !== null) {
|
|
581
|
+
clearTimeout(opponentAutoTimer);
|
|
582
|
+
opponentAutoTimer = null;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const pathAfterSolver = [...cursorPath];
|
|
586
|
+
const delayMs = chessboard.animationTime || OPPONENT_MOVE_ANIMATION_MS;
|
|
587
|
+
opponentAutoTimer = setTimeout(() => {
|
|
588
|
+
playOpponentAutoResponse(pathAfterSolver);
|
|
589
|
+
opponentAutoTimer = null;
|
|
590
|
+
}, delayMs);
|
|
591
|
+
}
|
|
592
|
+
|
|
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;
|
|
619
|
+
}
|
|
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];
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Представляет переход на следующую линию ответов соперника, если возможно.
|
|
645
|
+
*/
|
|
646
|
+
function maybeAdvanceToNextOpponentLine(): boolean {
|
|
647
|
+
const nextBase = findNextOpponentLine(cursorPath);
|
|
648
|
+
if (!nextBase) return false;
|
|
649
|
+
|
|
435
650
|
solved = false;
|
|
436
651
|
wrongFeedback = null;
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
652
|
+
|
|
653
|
+
setStateFromPath(nextBase);
|
|
654
|
+
return true;
|
|
440
655
|
}
|
|
441
656
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
657
|
+
/**
|
|
658
|
+
* Представляет попытку применить SAN хода решателя с автоответами соперника.
|
|
659
|
+
*/
|
|
660
|
+
function tryApplySolverSan(san: string): Move | undefined {
|
|
661
|
+
const solution = solutionPreviewTree;
|
|
662
|
+
if (!solution || solved) return undefined;
|
|
663
|
+
|
|
664
|
+
cancelPendingWrongMoveReveal();
|
|
665
|
+
|
|
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
|
+
}
|
|
699
|
+
|
|
700
|
+
try {
|
|
701
|
+
const pathAfterSolver = [...cursorPath, outcome.childIndex];
|
|
702
|
+
const moveRecord = previewChess.makeSanMove(san);
|
|
703
|
+
cursorPath = pathAfterSolver;
|
|
704
|
+
syncStudentTreeFromSolutionPath(pathAfterSolver);
|
|
705
|
+
|
|
706
|
+
wrongFeedback = null;
|
|
707
|
+
lastSolverMoveForBoard = moveRecord;
|
|
708
|
+
syncBoardDragFromChessTurn();
|
|
709
|
+
|
|
710
|
+
return moveRecord;
|
|
711
|
+
} catch {
|
|
712
|
+
wrongFeedback = "Ход недопустим в этой позиции.";
|
|
713
|
+
onOutcome?.("failed");
|
|
714
|
+
return undefined;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
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();
|
|
861
|
+
});
|
|
862
|
+
return () => clearTimers();
|
|
863
|
+
}
|
|
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,
|
|
895
|
+
});
|
|
896
|
+
syncBoardDragFromChessTurn();
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
return () => clearTimers();
|
|
459
900
|
});
|
|
460
901
|
|
|
461
|
-
|
|
462
|
-
|
|
902
|
+
/**
|
|
903
|
+
* Представляет сброс отложенных таймеров превью.
|
|
904
|
+
*/
|
|
905
|
+
function clearTimers(): void {
|
|
906
|
+
dismissPendingWrongMoveReveal();
|
|
907
|
+
if (advanceLineTimer !== null) {
|
|
908
|
+
clearTimeout(advanceLineTimer);
|
|
909
|
+
advanceLineTimer = null;
|
|
910
|
+
}
|
|
911
|
+
if (opponentAutoTimer !== null) {
|
|
912
|
+
clearTimeout(opponentAutoTimer);
|
|
913
|
+
opponentAutoTimer = null;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
463
916
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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);
|
|
934
|
+
syncBoardDragFromChessTurn();
|
|
471
935
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* Представляет установку FEN движка при навигации по дереву (только после полного решения превью).
|
|
939
|
+
*/
|
|
940
|
+
function treeSetChessFen(fen: string): void {
|
|
941
|
+
if (!solved) return;
|
|
942
|
+
previewChess.setFen(fen);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Представляет обновление UI доски после смены узла в дереве решения.
|
|
947
|
+
*/
|
|
948
|
+
function treeSetChessboardFen(_animationTime: number | undefined): void {
|
|
949
|
+
if (!solved) return;
|
|
950
|
+
syncBoardFromSolutionTreeNode();
|
|
951
|
+
}
|
|
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();
|
|
475
967
|
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
/**
|
|
479
|
-
* Представляет синхронизацию доски с текущим узлом полного дерева решения (после `solved`).
|
|
480
|
-
*/
|
|
481
|
-
function syncBoardFromSolutionTreeNode(): void {
|
|
482
|
-
const tree = solutionPreviewTree;
|
|
483
|
-
if (!tree || !solved) return;
|
|
484
|
-
previewChess.setFen(tree.currentNode.data.fen);
|
|
485
|
-
chessboard.fen = previewChess.fen();
|
|
486
|
-
chessboard.addLastMove(tree.currentNode.data.lastMove ?? null);
|
|
487
|
-
syncMoveEvaluationNagBadge(chessboard, tree.currentNode.data);
|
|
488
|
-
chessboard.setNodeMarkers(tree.currentNode.data.parsedFirstComment?.shapes);
|
|
489
|
-
syncBoardDragFromChessTurn();
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
/**
|
|
493
|
-
* Представляет установку FEN движка при навигации по дереву (только после полного решения превью).
|
|
494
|
-
*/
|
|
495
|
-
function treeSetChessFen(fen: string): void {
|
|
496
|
-
if (!solved) return;
|
|
497
|
-
previewChess.setFen(fen);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
/**
|
|
501
|
-
* Представляет обновление UI доски после смены узла в дереве решения.
|
|
502
|
-
*/
|
|
503
|
-
function treeSetChessboardFen(_animationTime?: number): void {
|
|
504
|
-
if (!solved) return;
|
|
505
|
-
syncBoardFromSolutionTreeNode();
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
/**
|
|
509
|
-
* Представляет реакцию на выбор узла в дереве (клик по ходу после решения).
|
|
510
|
-
*/
|
|
511
|
-
function treeOnSelectNode(): void {
|
|
512
|
-
if (!solved) return;
|
|
513
|
-
syncBoardFromSolutionTreeNode();
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
/**
|
|
517
|
-
* Представляет синхронизацию доски после операций с вариантами (в режиме просмотра без правки не используется).
|
|
518
|
-
*/
|
|
519
|
-
function treeOnDeleteVariant(): void {
|
|
520
|
-
if (!solved) return;
|
|
521
|
-
syncBoardFromSolutionTreeNode();
|
|
522
|
-
}
|
|
523
968
|
</script>
|
|
524
969
|
|
|
525
|
-
<div class={
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
970
|
+
<div class={cn("flex w-full flex-col gap-2", className)}>
|
|
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>
|
|
544
991
|
</div>
|