@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,180 +1,186 @@
1
1
  <script lang="ts">
2
- import type { BoardApi } from "@connectorvol/chessboard";
3
- import type { Castling, Fen } from "./fen.svelte.js";
4
- import { cn } from "../utils.js";
2
+ import type { BoardApi } from "@connectorvol/chessboard";
3
+ import type { Castling, Fen } from "./fen.svelte.js";
4
+ import { cn } from "../utils.js";
5
5
 
6
- let isPossibleShortCastleWhite = $state(true);
7
- let isPossibleShortCastleBlack = $state(true);
8
- let isPossibleLongCastleWhite = $state(true);
9
- let isPossibleLongCastleBlack = $state(true);
10
-
11
- interface Props {
12
- fen: Fen;
13
- api: BoardApi;
14
- }
6
+ interface Props {
7
+ fen: Fen;
8
+ api: BoardApi;
9
+ }
15
10
 
16
- const { fen, api: _api }: Props = $props();
11
+ const { fen, api: _api }: Props = $props();
17
12
 
18
- $effect(() => {
19
- isPossibleShortCastleWhite = fen.castling.includes("K");
20
- isPossibleShortCastleBlack = fen.castling.includes("k");
21
- isPossibleLongCastleWhite = fen.castling.includes("Q");
22
- isPossibleLongCastleBlack = fen.castling.includes("q");
23
- });
13
+ const isPossibleShortCastleWhite = $derived(fen.castling.includes("K"));
14
+ const isPossibleShortCastleBlack = $derived(fen.castling.includes("k"));
15
+ const isPossibleLongCastleWhite = $derived(fen.castling.includes("Q"));
16
+ const isPossibleLongCastleBlack = $derived(fen.castling.includes("q"));
24
17
 
25
- $effect(() => {
26
- const castling = [
27
- isPossibleShortCastleWhite,
28
- isPossibleLongCastleWhite,
29
- isPossibleShortCastleBlack,
30
- isPossibleLongCastleBlack,
31
- ]
32
- .map((c, i) => (c ? "KQkq"[i] : ""))
33
- .join("");
34
- if (!castling) {
35
- fen.castling = "-";
36
- } else {
37
- fen.castling = castling as Castling;
18
+ function buildCastling(
19
+ shortWhite: boolean,
20
+ longWhite: boolean,
21
+ shortBlack: boolean,
22
+ longBlack: boolean,
23
+ ): Castling {
24
+ const castling = [shortWhite, longWhite, shortBlack, longBlack]
25
+ .map((c, i) => (c ? "KQkq"[i] : ""))
26
+ .join("");
27
+ return (castling || "-") as Castling;
38
28
  }
39
- });
40
29
 
41
- const enPassantMoves = $derived(fen.genPossibleEnPassantMoves());
42
- $effect(() => {
43
- if (enPassantMoves.length === 0) {
44
- fen.enPassant = "-";
30
+ function toggleCastling(flagIndex: number, value: boolean): void {
31
+ const flags = [
32
+ isPossibleShortCastleWhite,
33
+ isPossibleLongCastleWhite,
34
+ isPossibleShortCastleBlack,
35
+ isPossibleLongCastleBlack,
36
+ ];
37
+ flags[flagIndex] = value;
38
+ fen.castling = buildCastling(
39
+ flags[0]!,
40
+ flags[1]!,
41
+ flags[2]!,
42
+ flags[3]!,
43
+ );
45
44
  }
46
- });
45
+
46
+ const enPassantMoves = $derived(fen.genPossibleEnPassantMoves());
47
47
  </script>
48
48
 
49
49
  <div class="space-y-3 md:space-y-4">
50
- <div class="space-y-2">
51
- <label
52
- for="move-select"
53
- class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
54
- >
55
- Установить ход
56
- </label>
57
- <select
58
- id="move-select"
59
- bind:value={fen.move}
60
- class={cn(
61
- "border-input bg-background ring-offset-background shadow-xs flex h-9 w-full rounded-md border px-3 py-1 text-base outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
62
- "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
63
- )}
64
- >
65
- <option value="w">w</option>
66
- <option value="b">b</option>
67
- </select>
68
- </div>
69
-
70
- <div class="space-y-2">
71
- <label
72
- for="enpassant-select"
73
- class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
74
- >
75
- Enpassant
76
- </label>
77
- <select
78
- id="enpassant-select"
79
- bind:value={fen.enPassant}
80
- class={cn(
81
- "border-input bg-background ring-offset-background shadow-xs flex h-9 w-full rounded-md border px-3 py-1 text-base outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
82
- "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
83
- )}
84
- >
85
- <option value="-">-</option>
86
- {#each enPassantMoves as move}
87
- <option value={move}>{move}</option>
88
- {/each}
89
- </select>
90
- </div>
91
-
92
- <div class="space-y-2 md:space-y-3">
93
- <div class="text-sm font-medium leading-none">Возможность рокировки</div>
94
- <div class="grid grid-cols-1 sm:grid-cols-2 gap-2 sm:gap-4">
95
- <div class="flex items-center space-x-2">
96
- <input
97
- type="checkbox"
98
- id="isPossibleShortCastleWhite"
99
- bind:checked={isPossibleShortCastleWhite}
100
- class={cn(
101
- "peer h-4 w-4 shrink-0 rounded-sm border border-input ring-offset-background transition-colors",
102
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
103
- "disabled:cursor-not-allowed disabled:opacity-50",
104
- "data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
105
- "accent-primary",
106
- )}
107
- />
50
+ <div class="space-y-2">
108
51
  <label
109
- for="isPossibleShortCastleWhite"
110
- class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
52
+ for="move-select"
53
+ class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
111
54
  >
112
- Белые 0-0
55
+ Установить ход
113
56
  </label>
114
- </div>
115
-
116
- <div class="flex items-center space-x-2">
117
- <input
118
- type="checkbox"
119
- id="isPossibleShortCastleBlack"
120
- bind:checked={isPossibleShortCastleBlack}
121
- class={cn(
122
- "peer h-4 w-4 shrink-0 rounded-sm border border-input ring-offset-background transition-colors",
123
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
124
- "disabled:cursor-not-allowed disabled:opacity-50",
125
- "data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
126
- "accent-primary",
127
- )}
128
- />
129
- <label
130
- for="isPossibleShortCastleBlack"
131
- class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
57
+ <select
58
+ id="move-select"
59
+ bind:value={fen.move}
60
+ class={cn(
61
+ "border-input bg-background ring-offset-background shadow-xs flex h-9 w-full rounded-md border px-3 py-1 text-base outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
62
+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
63
+ )}
132
64
  >
133
- Черные 0-0
134
- </label>
135
- </div>
65
+ <option value="w">w</option>
66
+ <option value="b">b</option>
67
+ </select>
68
+ </div>
136
69
 
137
- <div class="flex items-center space-x-2">
138
- <input
139
- type="checkbox"
140
- id="isPossibleLongCastleWhite"
141
- bind:checked={isPossibleLongCastleWhite}
142
- class={cn(
143
- "peer h-4 w-4 shrink-0 rounded-sm border border-input ring-offset-background transition-colors",
144
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
145
- "disabled:cursor-not-allowed disabled:opacity-50",
146
- "data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
147
- "accent-primary",
148
- )}
149
- />
70
+ <div class="space-y-2">
150
71
  <label
151
- for="isPossibleLongCastleWhite"
152
- class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
72
+ for="enpassant-select"
73
+ class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
153
74
  >
154
- Белые 0-0-0
75
+ Enpassant
155
76
  </label>
156
- </div>
157
-
158
- <div class="flex items-center space-x-2">
159
- <input
160
- type="checkbox"
161
- id="isPossibleLongCastleBlack"
162
- bind:checked={isPossibleLongCastleBlack}
163
- class={cn(
164
- "peer h-4 w-4 shrink-0 rounded-sm border border-input ring-offset-background transition-colors",
165
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
166
- "disabled:cursor-not-allowed disabled:opacity-50",
167
- "data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
168
- "accent-primary",
169
- )}
170
- />
171
- <label
172
- for="isPossibleLongCastleBlack"
173
- class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
77
+ <select
78
+ id="enpassant-select"
79
+ bind:value={fen.enPassant}
80
+ class={cn(
81
+ "border-input bg-background ring-offset-background shadow-xs flex h-9 w-full rounded-md border px-3 py-1 text-base outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
82
+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
83
+ )}
174
84
  >
175
- Черные 0-0-0
176
- </label>
177
- </div>
85
+ <option value="-">-</option>
86
+ {#each enPassantMoves as move}
87
+ <option value={move}>{move}</option>
88
+ {/each}
89
+ </select>
90
+ </div>
91
+
92
+ <div class="space-y-2 md:space-y-3">
93
+ <div class="text-sm font-medium leading-none">
94
+ Возможность рокировки
95
+ </div>
96
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-2 sm:gap-4">
97
+ <div class="flex items-center space-x-2">
98
+ <input
99
+ type="checkbox"
100
+ id="isPossibleShortCastleWhite"
101
+ checked={isPossibleShortCastleWhite}
102
+ onchange={(e) => toggleCastling(0, e.currentTarget.checked)}
103
+ class={cn(
104
+ "peer h-4 w-4 shrink-0 rounded-sm border border-input ring-offset-background transition-colors",
105
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
106
+ "disabled:cursor-not-allowed disabled:opacity-50",
107
+ "data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
108
+ "accent-primary",
109
+ )}
110
+ />
111
+ <label
112
+ for="isPossibleShortCastleWhite"
113
+ class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
114
+ >
115
+ Белые 0-0
116
+ </label>
117
+ </div>
118
+
119
+ <div class="flex items-center space-x-2">
120
+ <input
121
+ type="checkbox"
122
+ id="isPossibleShortCastleBlack"
123
+ checked={isPossibleShortCastleBlack}
124
+ onchange={(e) => toggleCastling(2, e.currentTarget.checked)}
125
+ class={cn(
126
+ "peer h-4 w-4 shrink-0 rounded-sm border border-input ring-offset-background transition-colors",
127
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
128
+ "disabled:cursor-not-allowed disabled:opacity-50",
129
+ "data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
130
+ "accent-primary",
131
+ )}
132
+ />
133
+ <label
134
+ for="isPossibleShortCastleBlack"
135
+ class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
136
+ >
137
+ Черные 0-0
138
+ </label>
139
+ </div>
140
+
141
+ <div class="flex items-center space-x-2">
142
+ <input
143
+ type="checkbox"
144
+ id="isPossibleLongCastleWhite"
145
+ checked={isPossibleLongCastleWhite}
146
+ onchange={(e) => toggleCastling(1, e.currentTarget.checked)}
147
+ class={cn(
148
+ "peer h-4 w-4 shrink-0 rounded-sm border border-input ring-offset-background transition-colors",
149
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
150
+ "disabled:cursor-not-allowed disabled:opacity-50",
151
+ "data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
152
+ "accent-primary",
153
+ )}
154
+ />
155
+ <label
156
+ for="isPossibleLongCastleWhite"
157
+ class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
158
+ >
159
+ Белые 0-0-0
160
+ </label>
161
+ </div>
162
+
163
+ <div class="flex items-center space-x-2">
164
+ <input
165
+ type="checkbox"
166
+ id="isPossibleLongCastleBlack"
167
+ checked={isPossibleLongCastleBlack}
168
+ onchange={(e) => toggleCastling(3, e.currentTarget.checked)}
169
+ class={cn(
170
+ "peer h-4 w-4 shrink-0 rounded-sm border border-input ring-offset-background transition-colors",
171
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
172
+ "disabled:cursor-not-allowed disabled:opacity-50",
173
+ "data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
174
+ "accent-primary",
175
+ )}
176
+ />
177
+ <label
178
+ for="isPossibleLongCastleBlack"
179
+ class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
180
+ >
181
+ Черные 0-0-0
182
+ </label>
183
+ </div>
184
+ </div>
178
185
  </div>
179
- </div>
180
186
  </div>
@@ -20,7 +20,7 @@ export declare function puzzlePreviewSideToMoveFromFen(fullFen: string): Color;
20
20
  /**
21
21
  * Представляет получение узла дерева по пути индексов детей от корня партии.
22
22
  */
23
- export declare function puzzlePreviewNodeAtPath(rootMoves: ChessTreeNode, path: number[]): ChessTreeNode;
23
+ export declare function puzzlePreviewNodeAtPath(rootMoves: ChessTreeNode, path: number[]): ChessTreeNode | null;
24
24
  /**
25
25
  * Представляет поиск индекса ребёнка, чей SAN даёт ту же результирующую позицию, что и сыгранный SAN.
26
26
  */
@@ -13,7 +13,10 @@ export function puzzlePreviewSideToMoveFromFen(fullFen) {
13
13
  export function puzzlePreviewNodeAtPath(rootMoves, path) {
14
14
  let node = rootMoves;
15
15
  for (const idx of path) {
16
- node = node.children[idx];
16
+ const child = node.children[idx];
17
+ if (!child)
18
+ return null;
19
+ node = child;
17
20
  }
18
21
  return node;
19
22
  }