@react-chess-tools/react-chess-puzzle 1.0.1 → 1.0.3
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/CHANGELOG.md +13 -0
- package/README.md +9 -7
- package/dist/index.cjs +27 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +27 -5
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/ChessPuzzle/ChessPuzzle.stories.tsx +409 -51
- package/src/components/ChessPuzzle/parts/Root.tsx +12 -2
- package/src/hooks/__tests__/reducer.test.ts +134 -0
- package/src/hooks/reducer.ts +13 -1
- package/src/hooks/useChessPuzzle.ts +2 -0
- package/src/utils/index.ts +1 -1
|
@@ -18,6 +18,105 @@ const puzzles = [
|
|
|
18
18
|
},
|
|
19
19
|
];
|
|
20
20
|
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Shared Story Styles
|
|
23
|
+
// ============================================================================
|
|
24
|
+
const storyStyles = {
|
|
25
|
+
container: {
|
|
26
|
+
display: "flex",
|
|
27
|
+
flexDirection: "column" as const,
|
|
28
|
+
alignItems: "center",
|
|
29
|
+
gap: "20px",
|
|
30
|
+
padding: "24px",
|
|
31
|
+
backgroundColor: "#f8f9fa",
|
|
32
|
+
borderRadius: "12px",
|
|
33
|
+
maxWidth: "500px",
|
|
34
|
+
margin: "0 auto",
|
|
35
|
+
},
|
|
36
|
+
header: {
|
|
37
|
+
display: "flex",
|
|
38
|
+
flexDirection: "column" as const,
|
|
39
|
+
alignItems: "center",
|
|
40
|
+
gap: "8px",
|
|
41
|
+
},
|
|
42
|
+
title: {
|
|
43
|
+
fontSize: "22px",
|
|
44
|
+
fontWeight: 700,
|
|
45
|
+
color: "#2c3e50",
|
|
46
|
+
margin: 0,
|
|
47
|
+
textAlign: "center" as const,
|
|
48
|
+
},
|
|
49
|
+
subtitle: {
|
|
50
|
+
fontSize: "14px",
|
|
51
|
+
color: "#6c757d",
|
|
52
|
+
margin: 0,
|
|
53
|
+
textAlign: "center" as const,
|
|
54
|
+
},
|
|
55
|
+
boardWrapper: {
|
|
56
|
+
backgroundColor: "#fff",
|
|
57
|
+
padding: "16px",
|
|
58
|
+
borderRadius: "10px",
|
|
59
|
+
boxShadow: "0 2px 12px rgba(0,0,0,0.08)",
|
|
60
|
+
},
|
|
61
|
+
controlsSection: {
|
|
62
|
+
display: "flex",
|
|
63
|
+
gap: "10px",
|
|
64
|
+
justifyContent: "center",
|
|
65
|
+
flexWrap: "wrap" as const,
|
|
66
|
+
},
|
|
67
|
+
button: {
|
|
68
|
+
padding: "10px 20px",
|
|
69
|
+
fontSize: "14px",
|
|
70
|
+
fontWeight: 600,
|
|
71
|
+
cursor: "pointer",
|
|
72
|
+
border: "none",
|
|
73
|
+
borderRadius: "8px",
|
|
74
|
+
backgroundColor: "#fff",
|
|
75
|
+
boxShadow: "0 2px 6px rgba(0,0,0,0.08)",
|
|
76
|
+
color: "#495057",
|
|
77
|
+
transition: "all 0.2s ease",
|
|
78
|
+
} as React.CSSProperties,
|
|
79
|
+
buttonPrimary: {
|
|
80
|
+
backgroundColor: "#4dabf7",
|
|
81
|
+
color: "#fff",
|
|
82
|
+
boxShadow: "0 2px 6px rgba(77, 171, 247, 0.3)",
|
|
83
|
+
} as React.CSSProperties,
|
|
84
|
+
buttonSuccess: {
|
|
85
|
+
backgroundColor: "#51cf66",
|
|
86
|
+
color: "#fff",
|
|
87
|
+
boxShadow: "0 2px 6px rgba(81, 207, 102, 0.3)",
|
|
88
|
+
} as React.CSSProperties,
|
|
89
|
+
hintButton: {
|
|
90
|
+
padding: "8px 16px",
|
|
91
|
+
fontSize: "13px",
|
|
92
|
+
fontWeight: 500,
|
|
93
|
+
cursor: "pointer",
|
|
94
|
+
border: "1px dashed #adb5bd",
|
|
95
|
+
borderRadius: "8px",
|
|
96
|
+
backgroundColor: "transparent",
|
|
97
|
+
color: "#868e96",
|
|
98
|
+
} as React.CSSProperties,
|
|
99
|
+
infoBox: {
|
|
100
|
+
padding: "12px 16px",
|
|
101
|
+
backgroundColor: "#e7f5ff",
|
|
102
|
+
borderRadius: "8px",
|
|
103
|
+
fontSize: "13px",
|
|
104
|
+
color: "#1864ab",
|
|
105
|
+
textAlign: "center" as const,
|
|
106
|
+
},
|
|
107
|
+
statusBadge: {
|
|
108
|
+
display: "inline-flex",
|
|
109
|
+
alignItems: "center",
|
|
110
|
+
gap: "6px",
|
|
111
|
+
padding: "6px 14px",
|
|
112
|
+
fontSize: "12px",
|
|
113
|
+
fontWeight: 600,
|
|
114
|
+
backgroundColor: "#e9ecef",
|
|
115
|
+
borderRadius: "20px",
|
|
116
|
+
color: "#495057",
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
21
120
|
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
|
|
22
121
|
const meta = {
|
|
23
122
|
title: "react-chess-puzzle/Components/Puzzle",
|
|
@@ -29,15 +128,8 @@ const meta = {
|
|
|
29
128
|
},
|
|
30
129
|
parameters: {
|
|
31
130
|
actions: { argTypesRegex: "^_on.*" },
|
|
131
|
+
layout: "centered",
|
|
32
132
|
},
|
|
33
|
-
decorators: [
|
|
34
|
-
(Story) => (
|
|
35
|
-
<div style={{ width: "400px" }}>
|
|
36
|
-
{/* 👇 Decorators in Storybook also accept a function. Replace <Story/> with Story() to enable it */}
|
|
37
|
-
<Story />
|
|
38
|
-
</div>
|
|
39
|
-
),
|
|
40
|
-
],
|
|
41
133
|
} satisfies Meta<typeof ChessPuzzle.Root>;
|
|
42
134
|
|
|
43
135
|
export default meta;
|
|
@@ -48,49 +140,70 @@ export const Example = (args: RootProps) => {
|
|
|
48
140
|
const [puzzleIndex, setPuzzleIndex] = React.useState(0);
|
|
49
141
|
const puzzle = puzzles[puzzleIndex];
|
|
50
142
|
return (
|
|
51
|
-
<
|
|
52
|
-
<
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
143
|
+
<ChessPuzzle.Root {...args} puzzle={puzzle}>
|
|
144
|
+
<div style={storyStyles.container}>
|
|
145
|
+
<div style={storyStyles.header}>
|
|
146
|
+
<h3 style={storyStyles.title}>Chess Puzzle</h3>
|
|
147
|
+
<p style={storyStyles.subtitle}>Find the best move sequence</p>
|
|
148
|
+
<span style={storyStyles.statusBadge}>
|
|
149
|
+
Puzzle {puzzleIndex + 1} of {puzzles.length}
|
|
150
|
+
</span>
|
|
151
|
+
</div>
|
|
152
|
+
<div style={storyStyles.boardWrapper}>
|
|
153
|
+
<ChessPuzzle.Board />
|
|
154
|
+
</div>
|
|
155
|
+
<div style={storyStyles.controlsSection}>
|
|
156
|
+
<ChessPuzzle.Reset asChild>
|
|
157
|
+
<button style={storyStyles.button}>Restart</button>
|
|
158
|
+
</ChessPuzzle.Reset>
|
|
159
|
+
<ChessPuzzle.Reset
|
|
160
|
+
asChild
|
|
161
|
+
puzzle={puzzles[(puzzleIndex + 1) % puzzles.length]}
|
|
162
|
+
onReset={() => setPuzzleIndex((puzzleIndex + 1) % puzzles.length)}
|
|
163
|
+
>
|
|
164
|
+
<button
|
|
165
|
+
style={{ ...storyStyles.button, ...storyStyles.buttonPrimary }}
|
|
166
|
+
>
|
|
167
|
+
Next Puzzle
|
|
168
|
+
</button>
|
|
169
|
+
</ChessPuzzle.Reset>
|
|
170
|
+
<ChessPuzzle.Hint style={storyStyles.hintButton}>
|
|
171
|
+
💡 Hint
|
|
172
|
+
</ChessPuzzle.Hint>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</ChessPuzzle.Root>
|
|
67
176
|
);
|
|
68
177
|
};
|
|
69
178
|
|
|
70
179
|
export const WithOrientation = (args: RootProps) => {
|
|
71
|
-
const [puzzleIndex, setPuzzleIndex] = React.useState(0);
|
|
72
180
|
const puzzle = {
|
|
73
181
|
fen: "4kbnr/2p1pp1p/pp4p1/5b2/8/2NB1N2/PP3PPP/RKB4R b k - 0 1",
|
|
74
182
|
makeFirstMove: false,
|
|
75
183
|
moves: ["Bxd3"],
|
|
76
184
|
};
|
|
77
185
|
return (
|
|
78
|
-
<
|
|
79
|
-
<
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
>
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
186
|
+
<ChessPuzzle.Root {...args} puzzle={puzzle}>
|
|
187
|
+
<div style={storyStyles.container}>
|
|
188
|
+
<div style={storyStyles.header}>
|
|
189
|
+
<h3 style={storyStyles.title}>Black to Move</h3>
|
|
190
|
+
<p style={storyStyles.subtitle}>
|
|
191
|
+
Board oriented from Black's perspective
|
|
192
|
+
</p>
|
|
193
|
+
</div>
|
|
194
|
+
<div style={storyStyles.boardWrapper}>
|
|
195
|
+
<ChessPuzzle.Board options={{ boardOrientation: "black" }} />
|
|
196
|
+
</div>
|
|
197
|
+
<div style={storyStyles.controlsSection}>
|
|
198
|
+
<ChessPuzzle.Reset asChild>
|
|
199
|
+
<button style={storyStyles.button}>Restart</button>
|
|
200
|
+
</ChessPuzzle.Reset>
|
|
201
|
+
<ChessPuzzle.Hint style={storyStyles.hintButton}>
|
|
202
|
+
💡 Hint
|
|
203
|
+
</ChessPuzzle.Hint>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</ChessPuzzle.Root>
|
|
94
207
|
);
|
|
95
208
|
};
|
|
96
209
|
|
|
@@ -101,14 +214,31 @@ export const Underpromotion = (args: RootProps) => {
|
|
|
101
214
|
makeFirstMove: true,
|
|
102
215
|
};
|
|
103
216
|
return (
|
|
104
|
-
<
|
|
105
|
-
<
|
|
106
|
-
<
|
|
107
|
-
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
217
|
+
<ChessPuzzle.Root {...args} puzzle={puzzle}>
|
|
218
|
+
<div style={storyStyles.container}>
|
|
219
|
+
<div style={storyStyles.header}>
|
|
220
|
+
<h3 style={storyStyles.title}>Underpromotion Challenge</h3>
|
|
221
|
+
<p style={storyStyles.subtitle}>
|
|
222
|
+
Promote to a knight instead of a queen
|
|
223
|
+
</p>
|
|
224
|
+
</div>
|
|
225
|
+
<div style={storyStyles.boardWrapper}>
|
|
226
|
+
<ChessPuzzle.Board />
|
|
227
|
+
</div>
|
|
228
|
+
<div style={storyStyles.controlsSection}>
|
|
229
|
+
<ChessPuzzle.Reset asChild>
|
|
230
|
+
<button
|
|
231
|
+
style={{ ...storyStyles.button, ...storyStyles.buttonSuccess }}
|
|
232
|
+
>
|
|
233
|
+
✓ Solved! Restart
|
|
234
|
+
</button>
|
|
235
|
+
</ChessPuzzle.Reset>
|
|
236
|
+
</div>
|
|
237
|
+
<div style={storyStyles.infoBox}>
|
|
238
|
+
Sometimes promoting to a knight is better than a queen!
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
</ChessPuzzle.Root>
|
|
112
242
|
);
|
|
113
243
|
};
|
|
114
244
|
|
|
@@ -116,7 +246,18 @@ export const WithSounds = (args: RootProps) => {
|
|
|
116
246
|
return (
|
|
117
247
|
<ChessPuzzle.Root {...args} puzzle={puzzles[0]}>
|
|
118
248
|
<ChessGame.Sounds />
|
|
119
|
-
<
|
|
249
|
+
<div style={storyStyles.container}>
|
|
250
|
+
<div style={storyStyles.header}>
|
|
251
|
+
<h3 style={storyStyles.title}>Puzzle with Sound</h3>
|
|
252
|
+
<p style={storyStyles.subtitle}>Audio feedback on every move</p>
|
|
253
|
+
</div>
|
|
254
|
+
<div style={storyStyles.boardWrapper}>
|
|
255
|
+
<ChessPuzzle.Board />
|
|
256
|
+
</div>
|
|
257
|
+
<p style={{ fontSize: "12px", color: "#868e96", textAlign: "center" }}>
|
|
258
|
+
Move pieces to hear different sounds
|
|
259
|
+
</p>
|
|
260
|
+
</div>
|
|
120
261
|
</ChessPuzzle.Root>
|
|
121
262
|
);
|
|
122
263
|
};
|
|
@@ -133,7 +274,224 @@ export const WithKeyboardControls = (args: RootProps) => {
|
|
|
133
274
|
d: (context) => context.methods.goToNextMove(),
|
|
134
275
|
}}
|
|
135
276
|
/>
|
|
136
|
-
<
|
|
277
|
+
<div style={storyStyles.container}>
|
|
278
|
+
<div style={storyStyles.header}>
|
|
279
|
+
<h3 style={storyStyles.title}>Keyboard Navigation</h3>
|
|
280
|
+
<p style={storyStyles.subtitle}>Use keyboard shortcuts to navigate</p>
|
|
281
|
+
</div>
|
|
282
|
+
<div style={storyStyles.boardWrapper}>
|
|
283
|
+
<ChessPuzzle.Board />
|
|
284
|
+
</div>
|
|
285
|
+
<div
|
|
286
|
+
style={{
|
|
287
|
+
display: "grid",
|
|
288
|
+
gridTemplateColumns: "repeat(3, auto)",
|
|
289
|
+
gap: "8px",
|
|
290
|
+
justifyContent: "center",
|
|
291
|
+
marginTop: "12px",
|
|
292
|
+
}}
|
|
293
|
+
>
|
|
294
|
+
<div
|
|
295
|
+
style={{
|
|
296
|
+
display: "flex",
|
|
297
|
+
alignItems: "center",
|
|
298
|
+
gap: "6px",
|
|
299
|
+
fontSize: "12px",
|
|
300
|
+
color: "#495057",
|
|
301
|
+
}}
|
|
302
|
+
>
|
|
303
|
+
<kbd
|
|
304
|
+
style={{
|
|
305
|
+
padding: "2px 8px",
|
|
306
|
+
backgroundColor: "#e9ecef",
|
|
307
|
+
border: "1px solid #ced4da",
|
|
308
|
+
borderRadius: "4px",
|
|
309
|
+
fontFamily: "monospace",
|
|
310
|
+
fontSize: "11px",
|
|
311
|
+
fontWeight: 600,
|
|
312
|
+
}}
|
|
313
|
+
>
|
|
314
|
+
W
|
|
315
|
+
</kbd>{" "}
|
|
316
|
+
Start
|
|
317
|
+
</div>
|
|
318
|
+
<div
|
|
319
|
+
style={{
|
|
320
|
+
display: "flex",
|
|
321
|
+
alignItems: "center",
|
|
322
|
+
gap: "6px",
|
|
323
|
+
fontSize: "12px",
|
|
324
|
+
color: "#495057",
|
|
325
|
+
}}
|
|
326
|
+
>
|
|
327
|
+
<kbd
|
|
328
|
+
style={{
|
|
329
|
+
padding: "2px 8px",
|
|
330
|
+
backgroundColor: "#e9ecef",
|
|
331
|
+
border: "1px solid #ced4da",
|
|
332
|
+
borderRadius: "4px",
|
|
333
|
+
fontFamily: "monospace",
|
|
334
|
+
fontSize: "11px",
|
|
335
|
+
fontWeight: 600,
|
|
336
|
+
}}
|
|
337
|
+
>
|
|
338
|
+
A
|
|
339
|
+
</kbd>{" "}
|
|
340
|
+
Previous
|
|
341
|
+
</div>
|
|
342
|
+
<div
|
|
343
|
+
style={{
|
|
344
|
+
display: "flex",
|
|
345
|
+
alignItems: "center",
|
|
346
|
+
gap: "6px",
|
|
347
|
+
fontSize: "12px",
|
|
348
|
+
color: "#495057",
|
|
349
|
+
}}
|
|
350
|
+
>
|
|
351
|
+
<kbd
|
|
352
|
+
style={{
|
|
353
|
+
padding: "2px 8px",
|
|
354
|
+
backgroundColor: "#e9ecef",
|
|
355
|
+
border: "1px solid #ced4da",
|
|
356
|
+
borderRadius: "4px",
|
|
357
|
+
fontFamily: "monospace",
|
|
358
|
+
fontSize: "11px",
|
|
359
|
+
fontWeight: 600,
|
|
360
|
+
}}
|
|
361
|
+
>
|
|
362
|
+
F
|
|
363
|
+
</kbd>{" "}
|
|
364
|
+
Flip
|
|
365
|
+
</div>
|
|
366
|
+
<div
|
|
367
|
+
style={{
|
|
368
|
+
display: "flex",
|
|
369
|
+
alignItems: "center",
|
|
370
|
+
gap: "6px",
|
|
371
|
+
fontSize: "12px",
|
|
372
|
+
color: "#495057",
|
|
373
|
+
}}
|
|
374
|
+
>
|
|
375
|
+
<kbd
|
|
376
|
+
style={{
|
|
377
|
+
padding: "2px 8px",
|
|
378
|
+
backgroundColor: "#e9ecef",
|
|
379
|
+
border: "1px solid #ced4da",
|
|
380
|
+
borderRadius: "4px",
|
|
381
|
+
fontFamily: "monospace",
|
|
382
|
+
fontSize: "11px",
|
|
383
|
+
fontWeight: 600,
|
|
384
|
+
}}
|
|
385
|
+
>
|
|
386
|
+
S
|
|
387
|
+
</kbd>{" "}
|
|
388
|
+
End
|
|
389
|
+
</div>
|
|
390
|
+
<div
|
|
391
|
+
style={{
|
|
392
|
+
display: "flex",
|
|
393
|
+
alignItems: "center",
|
|
394
|
+
gap: "6px",
|
|
395
|
+
fontSize: "12px",
|
|
396
|
+
color: "#495057",
|
|
397
|
+
}}
|
|
398
|
+
>
|
|
399
|
+
<kbd
|
|
400
|
+
style={{
|
|
401
|
+
padding: "2px 8px",
|
|
402
|
+
backgroundColor: "#e9ecef",
|
|
403
|
+
border: "1px solid #ced4da",
|
|
404
|
+
borderRadius: "4px",
|
|
405
|
+
fontFamily: "monospace",
|
|
406
|
+
fontSize: "11px",
|
|
407
|
+
fontWeight: 600,
|
|
408
|
+
}}
|
|
409
|
+
>
|
|
410
|
+
D
|
|
411
|
+
</kbd>{" "}
|
|
412
|
+
Next
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
</ChessPuzzle.Root>
|
|
417
|
+
);
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
// Puzzle with multiple checkmate solutions for testing solveOnCheckmate prop
|
|
421
|
+
const multiMatePuzzle = {
|
|
422
|
+
fen: "7k/R7/1R6/2Q5/4Q3/8/8/7K w - - 0 1",
|
|
423
|
+
moves: ["a7a8"], // Canonical solution
|
|
424
|
+
makeFirstMove: false,
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
export const MultiMatePuzzle = (args: RootProps) => {
|
|
428
|
+
return (
|
|
429
|
+
<ChessPuzzle.Root {...args} puzzle={multiMatePuzzle}>
|
|
430
|
+
<div style={storyStyles.container}>
|
|
431
|
+
<div style={storyStyles.header}>
|
|
432
|
+
<h3 style={storyStyles.title}>Flexible Checkmate</h3>
|
|
433
|
+
<p style={storyStyles.subtitle}>
|
|
434
|
+
Any checkmate move solves the puzzle
|
|
435
|
+
</p>
|
|
436
|
+
</div>
|
|
437
|
+
<div
|
|
438
|
+
style={{
|
|
439
|
+
...storyStyles.infoBox,
|
|
440
|
+
backgroundColor: "#d3f9d8",
|
|
441
|
+
color: "#2b8a3e",
|
|
442
|
+
}}
|
|
443
|
+
>
|
|
444
|
+
<strong>solveOnCheckmate=true (default)</strong>
|
|
445
|
+
<br />
|
|
446
|
+
Try Qc8#, Qf8#, Rb8#, or the canonical Ra8#
|
|
447
|
+
</div>
|
|
448
|
+
<div style={storyStyles.boardWrapper}>
|
|
449
|
+
<ChessPuzzle.Board />
|
|
450
|
+
</div>
|
|
451
|
+
<div style={storyStyles.controlsSection}>
|
|
452
|
+
<ChessPuzzle.Reset asChild>
|
|
453
|
+
<button style={storyStyles.button}>Restart</button>
|
|
454
|
+
</ChessPuzzle.Reset>
|
|
455
|
+
</div>
|
|
456
|
+
</div>
|
|
457
|
+
</ChessPuzzle.Root>
|
|
458
|
+
);
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
export const MultiMatePuzzleStrict = (args: RootProps) => {
|
|
462
|
+
return (
|
|
463
|
+
<ChessPuzzle.Root
|
|
464
|
+
{...args}
|
|
465
|
+
puzzle={multiMatePuzzle}
|
|
466
|
+
solveOnCheckmate={false}
|
|
467
|
+
>
|
|
468
|
+
<div style={storyStyles.container}>
|
|
469
|
+
<div style={storyStyles.header}>
|
|
470
|
+
<h3 style={storyStyles.title}>Strict Checkmate</h3>
|
|
471
|
+
<p style={storyStyles.subtitle}>
|
|
472
|
+
Only the canonical solution is accepted
|
|
473
|
+
</p>
|
|
474
|
+
</div>
|
|
475
|
+
<div
|
|
476
|
+
style={{
|
|
477
|
+
...storyStyles.infoBox,
|
|
478
|
+
backgroundColor: "#ffe3e3",
|
|
479
|
+
color: "#c92a2a",
|
|
480
|
+
}}
|
|
481
|
+
>
|
|
482
|
+
<strong>solveOnCheckmate=false</strong>
|
|
483
|
+
<br />
|
|
484
|
+
Only Ra8# is accepted. Alternative mates like Qc8# will fail!
|
|
485
|
+
</div>
|
|
486
|
+
<div style={storyStyles.boardWrapper}>
|
|
487
|
+
<ChessPuzzle.Board />
|
|
488
|
+
</div>
|
|
489
|
+
<div style={storyStyles.controlsSection}>
|
|
490
|
+
<ChessPuzzle.Reset asChild>
|
|
491
|
+
<button style={storyStyles.button}>Restart</button>
|
|
492
|
+
</ChessPuzzle.Reset>
|
|
493
|
+
</div>
|
|
494
|
+
</div>
|
|
137
495
|
</ChessPuzzle.Root>
|
|
138
496
|
);
|
|
139
497
|
};
|
|
@@ -16,12 +16,15 @@ export interface RootProps {
|
|
|
16
16
|
onFail?: (puzzleContext: ChessPuzzleContextType) => void;
|
|
17
17
|
/** Optional theme configuration. Supports partial themes - only override the colors you need. */
|
|
18
18
|
theme?: PartialChessPuzzleTheme;
|
|
19
|
+
/** When true, any checkmate move solves the puzzle (not just the canonical solution). Defaults to true. */
|
|
20
|
+
solveOnCheckmate?: boolean;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
interface PuzzleRootInnerProps {
|
|
22
24
|
puzzle: Puzzle;
|
|
23
25
|
onSolve?: (puzzleContext: ChessPuzzleContextType) => void;
|
|
24
26
|
onFail?: (puzzleContext: ChessPuzzleContextType) => void;
|
|
27
|
+
solveOnCheckmate: boolean;
|
|
25
28
|
children: React.ReactNode;
|
|
26
29
|
}
|
|
27
30
|
|
|
@@ -29,9 +32,10 @@ const PuzzleRootInner: React.FC<PuzzleRootInnerProps> = ({
|
|
|
29
32
|
puzzle,
|
|
30
33
|
onSolve,
|
|
31
34
|
onFail,
|
|
35
|
+
solveOnCheckmate,
|
|
32
36
|
children,
|
|
33
37
|
}) => {
|
|
34
|
-
const context = useChessPuzzle(puzzle, onSolve, onFail);
|
|
38
|
+
const context = useChessPuzzle(puzzle, onSolve, onFail, solveOnCheckmate);
|
|
35
39
|
|
|
36
40
|
return (
|
|
37
41
|
<ChessPuzzleContext.Provider value={context}>
|
|
@@ -45,6 +49,7 @@ export const Root: React.FC<React.PropsWithChildren<RootProps>> = ({
|
|
|
45
49
|
onSolve,
|
|
46
50
|
onFail,
|
|
47
51
|
theme,
|
|
52
|
+
solveOnCheckmate = true,
|
|
48
53
|
children,
|
|
49
54
|
}) => {
|
|
50
55
|
// Merge partial theme with defaults
|
|
@@ -57,7 +62,12 @@ export const Root: React.FC<React.PropsWithChildren<RootProps>> = ({
|
|
|
57
62
|
theme={mergedTheme}
|
|
58
63
|
>
|
|
59
64
|
<PuzzleThemeProvider theme={mergedTheme}>
|
|
60
|
-
<PuzzleRootInner
|
|
65
|
+
<PuzzleRootInner
|
|
66
|
+
puzzle={puzzle}
|
|
67
|
+
onSolve={onSolve}
|
|
68
|
+
onFail={onFail}
|
|
69
|
+
solveOnCheckmate={solveOnCheckmate}
|
|
70
|
+
>
|
|
61
71
|
{children}
|
|
62
72
|
</PuzzleRootInner>
|
|
63
73
|
</PuzzleThemeProvider>
|
|
@@ -271,4 +271,138 @@ describe("reducer", () => {
|
|
|
271
271
|
expect(newState.status).toBe("failed");
|
|
272
272
|
});
|
|
273
273
|
});
|
|
274
|
+
|
|
275
|
+
describe("solveOnCheckmate feature", () => {
|
|
276
|
+
// Puzzle with multiple checkmate solutions
|
|
277
|
+
const multiMatePuzzle: Puzzle = {
|
|
278
|
+
fen: "7k/R7/1R6/2Q5/4Q3/8/8/7K w - - 0 1",
|
|
279
|
+
moves: ["a7a8"],
|
|
280
|
+
makeFirstMove: false,
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const multiMateInitialState: State = {
|
|
284
|
+
puzzle: multiMatePuzzle,
|
|
285
|
+
currentMoveIndex: 0,
|
|
286
|
+
status: "not-started",
|
|
287
|
+
nextMove: "a7a8",
|
|
288
|
+
hint: "none",
|
|
289
|
+
cpuMove: null,
|
|
290
|
+
needCpuMove: false,
|
|
291
|
+
isPlayerTurn: true,
|
|
292
|
+
onSolveInvoked: false,
|
|
293
|
+
onFailInvoked: false,
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
it("should solve puzzle when alternative checkmate move is made and solveOnCheckmate=true", () => {
|
|
297
|
+
const game = new Chess(multiMatePuzzle.fen);
|
|
298
|
+
// Alternative mate: Qc8# (queen mate from c5 to c8) instead of canonical Ra8#
|
|
299
|
+
game.move("Qc8");
|
|
300
|
+
const alternativeMateMove = game.history({ verbose: true }).pop() as Move;
|
|
301
|
+
|
|
302
|
+
const action: Action = {
|
|
303
|
+
type: "PLAYER_MOVE",
|
|
304
|
+
payload: {
|
|
305
|
+
move: alternativeMateMove,
|
|
306
|
+
puzzleContext: {} as ChessPuzzleContextType,
|
|
307
|
+
game,
|
|
308
|
+
solveOnCheckmate: true,
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const newState = reducer(multiMateInitialState, action);
|
|
313
|
+
|
|
314
|
+
expect(newState.status).toBe("solved");
|
|
315
|
+
expect(newState.nextMove).toBe(null);
|
|
316
|
+
expect(newState.isPlayerTurn).toBe(false);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("should fail puzzle when alternative checkmate move is made and solveOnCheckmate=false", () => {
|
|
320
|
+
const game = new Chess(multiMatePuzzle.fen);
|
|
321
|
+
// Alternative mate: Qf8# (queen mate from c5 to f8) instead of canonical Ra8#
|
|
322
|
+
game.move("Qf8");
|
|
323
|
+
const alternativeMateMove = game.history({ verbose: true }).pop() as Move;
|
|
324
|
+
|
|
325
|
+
const action: Action = {
|
|
326
|
+
type: "PLAYER_MOVE",
|
|
327
|
+
payload: {
|
|
328
|
+
move: alternativeMateMove,
|
|
329
|
+
puzzleContext: {} as ChessPuzzleContextType,
|
|
330
|
+
game,
|
|
331
|
+
solveOnCheckmate: false,
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const newState = reducer(multiMateInitialState, action);
|
|
336
|
+
|
|
337
|
+
expect(newState.status).toBe("failed");
|
|
338
|
+
expect(newState.nextMove).toBe(null);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("should still solve puzzle when canonical solution move is made with solveOnCheckmate=true", () => {
|
|
342
|
+
const game = new Chess(multiMatePuzzle.fen);
|
|
343
|
+
// Make the canonical checkmate move on the game so isCheckmate() returns true
|
|
344
|
+
game.move("a7a8");
|
|
345
|
+
// Canonical move: Ra8# (rook mate from b6 to b8)
|
|
346
|
+
const canonicalMove = game.history({ verbose: true }).pop() as Move;
|
|
347
|
+
|
|
348
|
+
const action: Action = {
|
|
349
|
+
type: "PLAYER_MOVE",
|
|
350
|
+
payload: {
|
|
351
|
+
move: canonicalMove,
|
|
352
|
+
puzzleContext: {} as ChessPuzzleContextType,
|
|
353
|
+
game,
|
|
354
|
+
solveOnCheckmate: true,
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const newState = reducer(multiMateInitialState, action);
|
|
359
|
+
|
|
360
|
+
expect(newState.status).toBe("solved");
|
|
361
|
+
expect(newState.nextMove).toBe(null);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("should fail puzzle when non-mate incorrect move is made with solveOnCheckmate=true", () => {
|
|
365
|
+
const game = new Chess(multiMatePuzzle.fen);
|
|
366
|
+
// Make a non-mate incorrect move (Qa3 checks but doesn't mate)
|
|
367
|
+
const incorrectResult = game.move("Qa3");
|
|
368
|
+
const incorrectMove = incorrectResult as Move;
|
|
369
|
+
|
|
370
|
+
const action: Action = {
|
|
371
|
+
type: "PLAYER_MOVE",
|
|
372
|
+
payload: {
|
|
373
|
+
move: incorrectMove,
|
|
374
|
+
puzzleContext: {} as ChessPuzzleContextType,
|
|
375
|
+
game,
|
|
376
|
+
solveOnCheckmate: true,
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const newState = reducer(multiMateInitialState, action);
|
|
381
|
+
|
|
382
|
+
// Not a checkmate (Qa3 is not mate), so should fail as incorrect move
|
|
383
|
+
expect(newState.status).toBe("failed");
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it("should enable checkmate detection when solveOnCheckmate is undefined in payload", () => {
|
|
387
|
+
const game = new Chess(multiMatePuzzle.fen);
|
|
388
|
+
// Make an alternative checkmate move on the game so isCheckmate() returns true
|
|
389
|
+
game.move("Qe8");
|
|
390
|
+
// Alternative mate: Qe8# (queen mate from e4 to e8)
|
|
391
|
+
const alternativeMateMove = game.history({ verbose: true }).pop() as Move;
|
|
392
|
+
|
|
393
|
+
const action: Action = {
|
|
394
|
+
type: "PLAYER_MOVE",
|
|
395
|
+
payload: {
|
|
396
|
+
move: alternativeMateMove,
|
|
397
|
+
puzzleContext: {} as ChessPuzzleContextType,
|
|
398
|
+
game,
|
|
399
|
+
// solveOnCheckmate not provided - should default to true
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const newState = reducer(multiMateInitialState, action);
|
|
404
|
+
|
|
405
|
+
expect(newState.status).toBe("solved");
|
|
406
|
+
});
|
|
407
|
+
});
|
|
274
408
|
});
|