@connectorvol/chess-widgets 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,214 +1,212 @@
1
1
  <script lang="ts" module>
2
- export type { TPuzzleCreationWizardProps } from "./types.js";
2
+ export type { TPuzzleCreationWizardProps } from "./types.js";
3
3
  </script>
4
4
 
5
5
  <script lang="ts">
6
- import { untrack } from "svelte";
7
-
8
- import StepMoves from "./StepMoves.svelte";
9
- import StepPosition from "./StepPosition.svelte";
10
- import StepPreview from "./StepPreview.svelte";
11
- import type { TPuzzleCreationWizardProps } from "./types.js";
12
- import {
13
- createEmptyTreeFromFen,
14
- createInitialPuzzleData,
15
- type PuzzleData,
16
- } from "../puzzle/puzzleData.js";
17
- import { PgnOps } from "@connectorvol/chessops/pgnOps.svelte";
18
- import { ChessTree, createPgnFromTree } from "@connectorvol/tree";
19
- import { CHESSBOARD_THEMES, Draggable, type IChessBoardActions } from "@connectorvol/chessboard";
20
- import { calculatePly, Color, Square } from "@connectorvol/shared";
21
-
22
- import { createPuzzleLineEditingBoardApi } from "./createPuzzleLineEditingBoard.js";
23
-
24
- const {
25
- onPuzzleCreated,
26
- fen: fenProp,
27
- boardTheme,
28
- boardAppearanceSettings,
29
- }: TPuzzleCreationWizardProps = $props();
30
-
31
- /**
32
- * Представляет нормализацию входного FEN из пропса (пустая строка трактуется как отсутствие значения).
33
- */
34
- function wizardSeedFen(prop: string | undefined): string | undefined {
35
- const t = prop?.trim();
36
- return t ? t : undefined;
37
- }
38
-
39
- let step = $state<1 | 2 | 3>(1);
40
- let puzzleData = $derived<PuzzleData>(
41
- createInitialPuzzleData(wizardSeedFen(fenProp)),
42
- );
43
-
44
- /** Возвращает PGN дерева решения для превью на шаге 3. */
45
- let solutionPgnForPreview = $state("");
46
-
47
- function handleMovesBack() {
48
- step = 1;
49
- }
50
-
51
- function handleMovesNext(moves: string[]) {
52
- puzzleData.moves = moves;
53
- solutionPgnForPreview = createPgnFromTree(tree.rootNode);
54
- step = 3;
55
- }
56
-
57
- function handlePreviewBack() {
58
- step = 2;
59
- }
60
-
61
- let chess = $derived(new PgnOps(puzzleData.initialFen, "chess"));
62
- let tree = $derived(
63
- new ChessTree(createEmptyTreeFromFen(puzzleData.initialFen)),
64
- );
65
- const addMoveToTree = (san: string) => {
66
- const move = chess.makeSanMove(san);
67
- if (!move) return;
68
- const fen = chess.fen();
69
- const { halfMoves, fullMoves } = calculatePly(fen);
70
- tree.addNodeToCurrent({
71
- id: "",
72
- children: [],
73
- data: {
74
- fen,
75
- san,
76
- ply: halfMoves,
77
- fullMoves,
78
- },
79
- });
80
- return { move, fen, turn: chess.turn() };
81
- };
82
-
83
- const actions: IChessBoardActions = {
84
- game: {
85
- possibleMovesOnSquare: (square: Square) => chess.moves(square),
86
- beforePieceMoveSan(san: string) {
87
- const result = addMoveToTree(san);
88
- return result;
89
- },
90
- afterPieceMoveSan: () => {},
91
- beforePieceMove: (from, to, promotion) => {
92
- const san = chess.getSanForMove({ from, to, promotion });
93
- chess.makeMove({ from, to, promotion });
6
+ import { untrack } from "svelte";
7
+
8
+ import StepMoves from "./StepMoves.svelte";
9
+ import StepPosition from "./StepPosition.svelte";
10
+ import StepPreview from "./StepPreview.svelte";
11
+ import type { TPuzzleCreationWizardProps } from "./types.js";
12
+ import {
13
+ createEmptyTreeFromFen,
14
+ createInitialPuzzleData,
15
+ type PuzzleData,
16
+ } from "../puzzle/puzzleData.js";
17
+ import { PgnOps } from "@connectorvol/chessops/pgnOps.svelte";
18
+ import { ChessTree, createPgnFromTree } from "@connectorvol/tree";
19
+ import {
20
+ CHESSBOARD_THEMES,
21
+ Draggable,
22
+ type IChessBoardActions,
23
+ } from "@connectorvol/chessboard";
24
+ import { calculatePly, Color, Square } from "@connectorvol/shared";
25
+
26
+ import { createPuzzleLineEditingBoardApi } from "./createPuzzleLineEditingBoard.js";
27
+
28
+ const {
29
+ onPuzzleCreated,
30
+ fen: fenProp,
31
+ boardTheme,
32
+ boardAppearanceSettings,
33
+ }: TPuzzleCreationWizardProps = $props();
34
+
35
+ /**
36
+ * Представляет нормализацию входного FEN из пропса (пустая строка трактуется как отсутствие значения).
37
+ */
38
+ function wizardSeedFen(prop: string | undefined): string | undefined {
39
+ const t = prop?.trim();
40
+ return t ? t : undefined;
41
+ }
42
+
43
+ let step = $state<1 | 2 | 3>(1);
44
+ let puzzleData = $state<PuzzleData>(
45
+ createInitialPuzzleData(wizardSeedFen(fenProp)),
46
+ );
47
+
48
+ /** Возвращает PGN дерева решения для превью на шаге 3. */
49
+ let solutionPgnForPreview = $state("");
50
+
51
+ function handleMovesBack() {
52
+ step = 1;
53
+ }
54
+
55
+ function handleMovesNext(moves: string[]) {
56
+ puzzleData.moves = moves;
57
+ solutionPgnForPreview = createPgnFromTree(tree.rootNode);
58
+ step = 3;
59
+ }
60
+
61
+ function handlePreviewBack() {
62
+ step = 2;
63
+ }
64
+
65
+ let chess = $derived(new PgnOps(puzzleData.initialFen, "chess"));
66
+ let tree = $derived(
67
+ new ChessTree(createEmptyTreeFromFen(puzzleData.initialFen)),
68
+ );
69
+ const addMoveToTree = (san: string) => {
70
+ const move = chess.makeSanMove(san);
71
+ if (!move) return;
94
72
  const fen = chess.fen();
95
73
  const { halfMoves, fullMoves } = calculatePly(fen);
96
74
  tree.addNodeToCurrent({
97
- id: "",
98
- children: [],
99
- data: {
100
- fen,
101
- san,
102
- ply: halfMoves,
103
- fullMoves,
104
- },
75
+ id: "",
76
+ children: [],
77
+ data: {
78
+ fen,
79
+ san,
80
+ ply: halfMoves,
81
+ fullMoves,
82
+ },
83
+ });
84
+ return { move, fen, turn: chess.turn() };
85
+ };
86
+
87
+ const actions: IChessBoardActions = {
88
+ game: {
89
+ possibleMovesOnSquare: (square: Square) => chess.moves(square),
90
+ beforePieceMoveSan(san: string) {
91
+ const result = addMoveToTree(san);
92
+ return result;
93
+ },
94
+ afterPieceMoveSan: () => {},
95
+ beforePieceMove: (from, to, promotion) => {
96
+ const san = chess.getSanForMove({ from, to, promotion });
97
+ chess.makeMove({ from, to, promotion });
98
+ const fen = chess.fen();
99
+ const { halfMoves, fullMoves } = calculatePly(fen);
100
+ tree.addNodeToCurrent({
101
+ id: "",
102
+ children: [],
103
+ data: {
104
+ fen,
105
+ san,
106
+ ply: halfMoves,
107
+ fullMoves,
108
+ },
109
+ });
110
+ return fen;
111
+ },
112
+ afterPieceMove: () => {
113
+ const side = chess.turn();
114
+ chessboard.draggable =
115
+ side === Color.WHITE ? Draggable.WHITE : Draggable.BLACK;
116
+ },
117
+ },
118
+ };
119
+
120
+ /**
121
+ * Представляет сборку доски для шага построения линии: позиция и ориентация по стороне хода в FEN.
122
+ */
123
+ function createMainChessboard(fullFen: string) {
124
+ return createPuzzleLineEditingBoardApi(fullFen, actions, {
125
+ boardTheme: boardTheme ?? CHESSBOARD_THEMES.blue,
126
+ boardAppearanceSettings,
127
+ });
128
+ }
129
+
130
+ let chessboard = $derived(createMainChessboard(puzzleData.initialFen));
131
+
132
+ /**
133
+ * Представляет сброс мастера при смене входного FEN извне (шаг 1 и черновик ходов обнуляются).
134
+ */
135
+ $effect(() => {
136
+ const seed = wizardSeedFen(fenProp);
137
+ const next = createInitialPuzzleData(seed);
138
+ untrack(() => {
139
+ puzzleData = next;
140
+ step = 1;
141
+ solutionPgnForPreview = "";
105
142
  });
106
- return fen;
107
- },
108
- afterPieceMove: () => {
109
- const side = chess.turn();
110
- chessboard.draggable =
111
- side === Color.WHITE ? Draggable.WHITE : Draggable.BLACK;
112
- },
113
- },
114
- };
115
-
116
- /**
117
- * Представляет сборку доски для шага построения линии: позиция и ориентация по стороне хода в FEN.
118
- */
119
- function createMainChessboard(fullFen: string) {
120
- return createPuzzleLineEditingBoardApi(fullFen, actions, {
121
- boardTheme: boardTheme ?? CHESSBOARD_THEMES.blue,
122
- boardAppearanceSettings,
123
- });
124
- }
125
-
126
- let chessboard = $derived(createMainChessboard(puzzleData.initialFen));
127
-
128
- /**
129
- * Представляет сброс мастера при смене входного FEN извне (шаг 1 и черновик ходов обнуляются).
130
- */
131
- $effect(() => {
132
- const seed = wizardSeedFen(fenProp);
133
- const next = createInitialPuzzleData(seed);
134
- untrack(() => {
135
- puzzleData = next;
136
- chess = new PgnOps(next.initialFen, "chess");
137
- tree = new ChessTree(createEmptyTreeFromFen(next.initialFen));
138
- chessboard = createMainChessboard(next.initialFen);
139
- step = 1;
140
- solutionPgnForPreview = "";
141
143
  });
142
- });
143
-
144
- function handlePositionNext(fen: string) {
145
- puzzleData.initialFen = fen;
146
- chess = new PgnOps(fen, "chess");
147
- tree = new ChessTree(createEmptyTreeFromFen(fen));
148
- chessboard = createMainChessboard(fen);
149
- step = 2;
150
- }
144
+
145
+ function handlePositionNext(fen: string) {
146
+ puzzleData.initialFen = fen;
147
+ step = 2;
148
+ }
151
149
  </script>
152
150
 
153
151
  <div class="mx-4 lg:container lg:mx-auto">
154
- <div class="flex flex-col gap-4 pt-4 pb-0">
155
- <div class="flex items-center gap-2">
156
- <span
157
- class="flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium"
158
- class:bg-primary={step >= 1}
159
- class:bg-muted={step < 1}
160
- class:text-primary-foreground={step >= 1}
161
- >
162
- 1
163
- </span>
164
- <span class="text-sm">Позиция</span>
165
- <span class="h-px flex-1 bg-border"></span>
166
- <span
167
- class="flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium"
168
- class:bg-primary={step >= 2}
169
- class:bg-muted={step < 2}
170
- class:text-primary-foreground={step >= 2}
171
- >
172
- 2
173
- </span>
174
- <span class="text-sm">Ходы</span>
175
- <span class="h-px flex-1 bg-border"></span>
176
- <span
177
- class="flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium"
178
- class:bg-primary={step >= 3}
179
- class:bg-muted={step < 3}
180
- class:text-primary-foreground={step >= 3}
181
- >
182
- 3
183
- </span>
184
- <span class="text-sm">Превью</span>
152
+ <div class="flex flex-col gap-4 pt-4 pb-0">
153
+ <div class="flex items-center gap-2">
154
+ <span
155
+ class="flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium"
156
+ class:bg-primary={step >= 1}
157
+ class:bg-muted={step < 1}
158
+ class:text-primary-foreground={step >= 1}
159
+ >
160
+ 1
161
+ </span>
162
+ <span class="text-sm">Позиция</span>
163
+ <span class="h-px flex-1 bg-border"></span>
164
+ <span
165
+ class="flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium"
166
+ class:bg-primary={step >= 2}
167
+ class:bg-muted={step < 2}
168
+ class:text-primary-foreground={step >= 2}
169
+ >
170
+ 2
171
+ </span>
172
+ <span class="text-sm">Ходы</span>
173
+ <span class="h-px flex-1 bg-border"></span>
174
+ <span
175
+ class="flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium"
176
+ class:bg-primary={step >= 3}
177
+ class:bg-muted={step < 3}
178
+ class:text-primary-foreground={step >= 3}
179
+ >
180
+ 3
181
+ </span>
182
+ <span class="text-sm">Превью</span>
183
+ </div>
184
+
185
+ {#if step === 1}
186
+ <StepPosition
187
+ initialFen={puzzleData.initialFen}
188
+ onNext={handlePositionNext}
189
+ {boardTheme}
190
+ {boardAppearanceSettings}
191
+ />
192
+ {:else if step === 2}
193
+ <StepMoves
194
+ {puzzleData}
195
+ {chess}
196
+ {tree}
197
+ {chessboard}
198
+ onBack={handleMovesBack}
199
+ onNext={handleMovesNext}
200
+ />
201
+ {:else}
202
+ <StepPreview
203
+ {puzzleData}
204
+ solutionPgn={solutionPgnForPreview}
205
+ onBack={handlePreviewBack}
206
+ {onPuzzleCreated}
207
+ {boardTheme}
208
+ {boardAppearanceSettings}
209
+ />
210
+ {/if}
185
211
  </div>
186
-
187
- {#if step === 1}
188
- <StepPosition
189
- initialFen={puzzleData.initialFen}
190
- onNext={handlePositionNext}
191
- {boardTheme}
192
- boardAppearanceSettings={boardAppearanceSettings}
193
- />
194
- {:else if step === 2}
195
- <StepMoves
196
- {puzzleData}
197
- {chess}
198
- {tree}
199
- {chessboard}
200
- onBack={handleMovesBack}
201
- onNext={handleMovesNext}
202
- />
203
- {:else}
204
- <StepPreview
205
- {puzzleData}
206
- solutionPgn={solutionPgnForPreview}
207
- onBack={handlePreviewBack}
208
- {onPuzzleCreated}
209
- {boardTheme}
210
- boardAppearanceSettings={boardAppearanceSettings}
211
- />
212
- {/if}
213
- </div>
214
212
  </div>
@@ -24,7 +24,7 @@
24
24
 
25
25
  import { INITIAL_FEN } from "@connectorvol/chessops/fen";
26
26
  import { PgnOps } from "@connectorvol/chessops/pgnOps.svelte";
27
- import { Color, type Move } from "@connectorvol/shared";
27
+ import { Color, calculatePly, type Move } from "@connectorvol/shared";
28
28
 
29
29
  import { mergePgnWithSetupFen } from "../puzzle/mergePgnWithSetupFen.js";
30
30
  import { createEmptyTreeFromFen } from "../puzzle/puzzleData.js";
@@ -81,7 +81,9 @@
81
81
  function syncBoardDragFromChessTurn(): void {
82
82
  const solver = puzzleSolverColor();
83
83
  chessboard.draggable = solved
84
- ? Draggable.NONE
84
+ ? previewChess.turn() === Color.WHITE
85
+ ? Draggable.WHITE
86
+ : Draggable.BLACK
85
87
  : previewChess.turn() === solver
86
88
  ? solver === Color.WHITE
87
89
  ? Draggable.WHITE
@@ -101,6 +103,7 @@
101
103
  let src = solution.rootNode.moves;
102
104
  for (const idx of path) {
103
105
  const next = src.children[idx];
106
+ if (!next) break;
104
107
  studentPreviewTree.addNodeToCurrent({
105
108
  id: "",
106
109
  children: [],
@@ -152,7 +155,8 @@
152
155
  previewChess.setFen(layoutInitialFen);
153
156
  let node = solution.rootNode.moves;
154
157
  for (const idx of path) {
155
- const child = node.children[idx]!;
158
+ const child = node.children[idx];
159
+ if (!child) break;
156
160
  previewChess.makeSanMove(child.data.san);
157
161
  node = child;
158
162
  }
@@ -178,7 +182,7 @@
178
182
  solution.rootNode.moves,
179
183
  pathAfterSolver,
180
184
  );
181
- if (node.children.length === 0) return pathAfterSolver;
185
+ if (!node || node.children.length === 0) return pathAfterSolver;
182
186
 
183
187
  previewChess.makeSanMove(node.children[0]!.data.san);
184
188
  return [...pathAfterSolver, 0];
@@ -207,7 +211,9 @@
207
211
  }
208
212
 
209
213
  if (depth === path.length) break;
210
- node = node.children[path[depth]!]!;
214
+ const child = node.children[path[depth]!];
215
+ if (!child) break;
216
+ node = child;
211
217
  }
212
218
 
213
219
  node = solution.rootNode.moves;
@@ -223,7 +229,9 @@
223
229
  bestNextIdx = chosenIdx + 1;
224
230
  }
225
231
  }
226
- node = node.children[path[depth]!]!;
232
+ const child = node.children[path[depth]!];
233
+ if (!child) break;
234
+ node = child;
227
235
  }
228
236
 
229
237
  if (bestForkDepth === null) return null;
@@ -255,6 +263,7 @@
255
263
  solution.rootNode.moves,
256
264
  cursorPath,
257
265
  );
266
+ if (!cursorNode) return undefined;
258
267
 
259
268
  const outcome = puzzlePreviewClassifySolverMove(
260
269
  cursorNode,
@@ -275,7 +284,7 @@
275
284
  setStateFromPath(path);
276
285
 
277
286
  const leaf = puzzlePreviewNodeAtPath(solution.rootNode.moves, cursorPath);
278
- if (puzzlePreviewIsSolvedPosition(previewChess, leaf, puzzleSolverColor())) {
287
+ if (leaf && puzzlePreviewIsSolvedPosition(previewChess, leaf, puzzleSolverColor())) {
279
288
  if (advanceLineTimer !== null) {
280
289
  clearTimeout(advanceLineTimer);
281
290
  advanceLineTimer = null;
@@ -306,6 +315,31 @@
306
315
  game: {
307
316
  possibleMovesOnSquare: (square: Square) => previewChess.moves(square),
308
317
  beforePieceMoveSan(san: string) {
318
+ if (solved) {
319
+ try {
320
+ const move = previewChess.makeSanMove(san);
321
+ const fen = previewChess.fen();
322
+ const { halfMoves, fullMoves } = calculatePly(fen);
323
+ solutionPreviewTree?.addNodeToCurrent({
324
+ id: "",
325
+ children: [],
326
+ data: {
327
+ fen,
328
+ san,
329
+ ply: halfMoves,
330
+ fullMoves,
331
+ lastMove: move,
332
+ },
333
+ });
334
+ return {
335
+ move,
336
+ fen,
337
+ turn: previewChess.turn(),
338
+ };
339
+ } catch {
340
+ return undefined;
341
+ }
342
+ }
309
343
  const move = tryApplySolverSan(san);
310
344
  if (!move) return undefined;
311
345
  return {
@@ -318,6 +352,28 @@
318
352
  syncBoardDragFromChessTurn();
319
353
  },
320
354
  beforePieceMove: (from, to, promotion) => {
355
+ if (solved) {
356
+ try {
357
+ const san = previewChess.getSanForMove({ from, to, promotion });
358
+ const move = previewChess.makeSanMove(san);
359
+ const fen = previewChess.fen();
360
+ const { halfMoves, fullMoves } = calculatePly(fen);
361
+ solutionPreviewTree?.addNodeToCurrent({
362
+ id: "",
363
+ children: [],
364
+ data: {
365
+ fen,
366
+ san,
367
+ ply: halfMoves,
368
+ fullMoves,
369
+ lastMove: move,
370
+ },
371
+ });
372
+ return fen;
373
+ } catch {
374
+ return "";
375
+ }
376
+ }
321
377
  const san = previewChess.getSanForMove({ from, to, promotion });
322
378
  const move = tryApplySolverSan(san);
323
379
  if (!move) return "";
@@ -357,7 +413,8 @@
357
413
  if (!solved || !solutionPreviewTree) return;
358
414
  const tree = solutionPreviewTree;
359
415
  const path = cursorPath;
360
- tree.currentNode = puzzlePreviewNodeAtPath(tree.rootNode.moves, path);
416
+ const target = puzzlePreviewNodeAtPath(tree.rootNode.moves, path);
417
+ tree.currentNode = target ?? tree.rootNode.moves;
361
418
  });
362
419
 
363
420
  $effect(() => {
@@ -430,6 +487,7 @@
430
487
  chessboard.fen = previewChess.fen();
431
488
  chessboard.addLastMove(tree.currentNode.data.lastMove ?? null);
432
489
  syncMoveEvaluationNagBadge(chessboard, tree.currentNode.data);
490
+ syncBoardDragFromChessTurn();
433
491
  }
434
492
 
435
493
  /**