@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.
@@ -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
- <div>
52
- <ChessPuzzle.Root {...args} puzzle={puzzle}>
53
- <ChessPuzzle.Board />
54
- <ChessPuzzle.Reset asChild>
55
- <button>restart</button>
56
- </ChessPuzzle.Reset>
57
- <ChessPuzzle.Reset
58
- asChild
59
- puzzle={puzzles[(puzzleIndex + 1) % puzzles.length]}
60
- onReset={() => setPuzzleIndex((puzzleIndex + 1) % puzzles.length)}
61
- >
62
- <button>next</button>
63
- </ChessPuzzle.Reset>
64
- <ChessPuzzle.Hint>hint</ChessPuzzle.Hint>
65
- </ChessPuzzle.Root>
66
- </div>
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
- <div>
79
- <ChessPuzzle.Root {...args} puzzle={puzzle}>
80
- <ChessPuzzle.Board options={{ boardOrientation: "black" }} />
81
- <ChessPuzzle.Reset asChild>
82
- <button>restart</button>
83
- </ChessPuzzle.Reset>
84
- <ChessPuzzle.Reset
85
- asChild
86
- puzzle={puzzle}
87
- onReset={() => setPuzzleIndex((puzzleIndex + 1) % puzzles.length)}
88
- >
89
- <button>next</button>
90
- </ChessPuzzle.Reset>
91
- <ChessPuzzle.Hint>hint</ChessPuzzle.Hint>
92
- </ChessPuzzle.Root>
93
- </div>
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
- <div>
105
- <ChessPuzzle.Root {...args} puzzle={puzzle}>
106
- <ChessPuzzle.Board />
107
- <ChessPuzzle.Reset asChild>
108
- <button>done! Restart</button>
109
- </ChessPuzzle.Reset>
110
- </ChessPuzzle.Root>
111
- </div>
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
- <ChessPuzzle.Board />
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
- <ChessPuzzle.Board />
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 puzzle={puzzle} onSolve={onSolve} onFail={onFail}>
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
  });