@react-chess-tools/react-chess-puzzle 0.5.0 → 0.5.2

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 CHANGED
@@ -1,5 +1,28 @@
1
1
  # @react-chess-tools/react-chess-puzzle
2
2
 
3
+ ## 0.5.2
4
+
5
+ ### Patch Changes
6
+
7
+ - f18ce4b: fix: improve error messages for context hook usage
8
+ - Updated dependencies [f18ce4b]
9
+ - @react-chess-tools/react-chess-game@0.4.2
10
+
11
+ ## 0.5.1
12
+
13
+ ### Patch Changes
14
+
15
+ - ca0a210: add missing properties to ChessPuzzleContextType
16
+ - 755a85d: fix: export missing TypeScript types and component props
17
+ - a9c69b5: test: add unit tests
18
+ - 5ed0baf: fix: fix SSR compatibility and prevent unnecessary sound re-renders
19
+ - c3ac738: fix: fix state management and lifecycle issues in chess components
20
+ - Updated dependencies [755a85d]
21
+ - Updated dependencies [a9c69b5]
22
+ - Updated dependencies [5ed0baf]
23
+ - Updated dependencies [c3ac738]
24
+ - @react-chess-tools/react-chess-game@0.4.1
25
+
3
26
  ## 0.5.0
4
27
 
5
28
  ### Minor Changes
package/README.MD CHANGED
@@ -230,42 +230,47 @@ export const PuzzleSolver = () => {
230
230
  // Example puzzles
231
231
  const puzzles = [
232
232
  {
233
- fen: "r1bqkbnr/pppp1ppp/2n5/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 2 3",
234
- moves: ["d2d4", "e5d4", "f3d4"],
233
+ fen: "4kb1r/p2r1ppp/4qn2/1B2p1B1/4P3/1Q6/PPP2PPP/2KR4 w k - 0 1",
234
+ moves: ["Bxd7+", "Nxd7", "Qb8+", "Nxb8", "Rd8#"],
235
235
  makeFirstMove: false,
236
236
  },
237
237
  {
238
- fen: "r1bqkb1r/pppp1ppp/2n2n2/4p3/2B1P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 4 4",
239
- moves: ["f3g5", "d7d5", "e4d5", "c6a5"],
240
- makeFirstMove: false,
238
+ fen: "6k1/5p1p/p1q1p1p1/1pB1P3/1Pr3Pn/P4P1P/4Q3/3R2K1 b - - 0 31",
239
+ moves: ["h4f3", "e2f3", "c4c5", "d1d8", "g8g7", "f3f6"],
240
+ makeFirstMove: true,
241
241
  },
242
242
  ];
243
243
 
244
244
  const [currentPuzzle, setCurrentPuzzle] = useState(0);
245
245
  const [score, setScore] = useState(0);
246
246
 
247
- // Function to load the next puzzle
248
247
  const nextPuzzle = () => {
249
- setCurrentPuzzle((prev) => (prev + 1) % puzzles.length);
248
+ const nextPuzzle = (currentPuzzle + 1) % puzzles.length;
249
+ setCurrentPuzzle(nextPuzzle);
250
250
  };
251
251
 
252
- const handleSolve = (puzzleContext) => {
252
+ const handleSolve = (puzzleContext: ChessPuzzleContextType) => {
253
253
  setScore((prev) => prev + 10);
254
254
  console.log(`Puzzle solved in ${puzzleContext.movesPlayed} moves`);
255
- // Could automatically progress to next puzzle
256
- // setTimeout(nextPuzzle, 1500);
255
+ nextPuzzle();
257
256
  };
258
257
 
259
- const handleFail = (puzzleContext) => {
258
+ const handleFail = (puzzleContext: ChessPuzzleContextType) => {
260
259
  setScore((prev) => Math.max(0, prev - 5));
261
- console.log(`Failed move: ${puzzleContext.lastMove}`);
260
+ console.log(`Puzzle failed in ${puzzleContext.movesPlayed} moves`);
261
+
262
+ nextPuzzle();
263
+ };
264
+
265
+ const handleReset = (puzzleContext: ChessPuzzleContextType) => {
266
+ puzzleContext.resetPuzzle();
262
267
  };
263
268
 
264
269
  return (
265
270
  <div className="puzzle-container">
266
271
  <div className="score">Score: {score}</div>
267
272
  <ChessPuzzle.Root
268
- puzzle={puzzles[currentPuzzle]}
273
+ puzzle={puzzles[currentPuzzle]!}
269
274
  onSolve={handleSolve}
270
275
  onFail={handleFail}
271
276
  >
@@ -277,13 +282,12 @@ export const PuzzleSolver = () => {
277
282
  </div>
278
283
 
279
284
  <div className="controls">
280
- <PuzzleStatus />
281
285
  <div className="buttons">
282
286
  <ChessPuzzle.Reset>Restart</ChessPuzzle.Reset>
283
287
  <ChessPuzzle.Hint showOn={["in-progress"]}>Hint</ChessPuzzle.Hint>
284
288
  <ChessPuzzle.Reset
285
289
  puzzle={puzzles[(currentPuzzle + 1) % puzzles.length]}
286
- onReset={nextPuzzle}
290
+ onReset={handleReset}
287
291
  >
288
292
  Next Puzzle
289
293
  </ChessPuzzle.Reset>
package/dist/index.d.mts CHANGED
@@ -18,7 +18,7 @@ interface HintProps {
18
18
  interface ResetProps {
19
19
  asChild?: boolean;
20
20
  puzzle?: Puzzle;
21
- onReset?: () => void;
21
+ onReset?: (puzzleContext: ChessPuzzleContextType) => void;
22
22
  showOn?: Status[];
23
23
  }
24
24
 
@@ -28,11 +28,15 @@ interface PuzzleBoardProps extends React__default.ComponentProps<typeof ChessGam
28
28
  type ChessPuzzleContextType = {
29
29
  status: Status;
30
30
  changePuzzle: (puzzle: Puzzle) => void;
31
+ resetPuzzle: () => void;
31
32
  puzzle: Puzzle;
32
33
  hint: Hint;
33
34
  nextMove?: string | null;
34
35
  isPlayerTurn: boolean;
35
36
  onHint: () => void;
37
+ puzzleState: Status;
38
+ movesPlayed: number;
39
+ totalMoves: number;
36
40
  };
37
41
 
38
42
  interface RootProps {
@@ -50,4 +54,4 @@ declare const ChessPuzzle: {
50
54
 
51
55
  declare const useChessPuzzleContext: () => ChessPuzzleContextType;
52
56
 
53
- export { ChessPuzzle, useChessPuzzleContext };
57
+ export { ChessPuzzle, type ChessPuzzleContextType, type Hint, type HintProps, type Puzzle, type PuzzleBoardProps, type ResetProps, type RootProps, type Status, useChessPuzzleContext };
package/dist/index.mjs CHANGED
@@ -68,7 +68,7 @@ var stringToMove = (game, move) => {
68
68
  };
69
69
 
70
70
  // src/hooks/useChessPuzzle.ts
71
- import { useEffect, useReducer } from "react";
71
+ import { useEffect, useReducer, useCallback, useMemo } from "react";
72
72
 
73
73
  // src/hooks/reducer.ts
74
74
  var initializePuzzle = ({ puzzle }) => {
@@ -80,7 +80,9 @@ var initializePuzzle = ({ puzzle }) => {
80
80
  hint: "none",
81
81
  cpuMove: null,
82
82
  needCpuMove: !!puzzle.makeFirstMove,
83
- isPlayerTurn: !puzzle.makeFirstMove
83
+ isPlayerTurn: !puzzle.makeFirstMove,
84
+ onSolveInvoked: false,
85
+ onFailInvoked: false
84
86
  };
85
87
  };
86
88
  var reducer = (state, action) => {
@@ -119,33 +121,29 @@ var reducer = (state, action) => {
119
121
  status: "in-progress"
120
122
  };
121
123
  case "PLAYER_MOVE": {
122
- const { move, onSolve, onFail, puzzleContext } = action.payload;
124
+ const { move } = action.payload;
123
125
  const isMoveRight = [move == null ? void 0 : move.san, move == null ? void 0 : move.lan].includes(
124
126
  (state == null ? void 0 : state.nextMove) || ""
125
127
  );
126
128
  const isPuzzleSolved = state.currentMoveIndex === state.puzzle.moves.length - 1;
127
129
  if (!isMoveRight) {
128
- if (onFail) {
129
- onFail(puzzleContext);
130
- }
131
130
  return {
132
131
  ...state,
133
132
  status: "failed",
134
133
  nextMove: null,
135
134
  hint: "none",
136
- isPlayerTurn: false
135
+ isPlayerTurn: false,
136
+ onFailInvoked: false
137
137
  };
138
138
  }
139
139
  if (isPuzzleSolved) {
140
- if (onSolve) {
141
- onSolve(puzzleContext);
142
- }
143
140
  return {
144
141
  ...state,
145
142
  status: "solved",
146
143
  nextMove: null,
147
144
  hint: "none",
148
- isPlayerTurn: false
145
+ isPlayerTurn: false,
146
+ onSolveInvoked: false
149
147
  };
150
148
  }
151
149
  return {
@@ -158,6 +156,16 @@ var reducer = (state, action) => {
158
156
  isPlayerTurn: false
159
157
  };
160
158
  }
159
+ case "MARK_SOLVE_INVOKED":
160
+ return {
161
+ ...state,
162
+ onSolveInvoked: true
163
+ };
164
+ case "MARK_FAIL_INVOKED":
165
+ return {
166
+ ...state,
167
+ onFailInvoked: true
168
+ };
161
169
  default:
162
170
  return state;
163
171
  }
@@ -173,15 +181,16 @@ var useChessPuzzle = (puzzle, onSolve, onFail) => {
173
181
  game,
174
182
  methods: { makeMove, setPosition }
175
183
  } = gameContext;
184
+ const changePuzzle = useCallback(
185
+ (puzzle2) => {
186
+ setPosition(puzzle2.fen, getOrientation(puzzle2));
187
+ dispatch({ type: "INITIALIZE", payload: { puzzle: puzzle2 } });
188
+ },
189
+ [setPosition]
190
+ );
176
191
  useEffect(() => {
177
- if (gameContext && game.fen() !== puzzle.fen) {
178
- setPosition(puzzle.fen, getOrientation(puzzle));
179
- }
180
- }, []);
181
- const changePuzzle = (puzzle2) => {
182
- dispatch({ type: "INITIALIZE", payload: { puzzle: puzzle2 } });
183
- setPosition(puzzle2.fen, getOrientation(puzzle2));
184
- };
192
+ changePuzzle(puzzle);
193
+ }, [JSON.stringify(puzzle), changePuzzle]);
185
194
  useEffect(() => {
186
195
  if (gameContext && game.fen() === puzzle.fen && state.needCpuMove) {
187
196
  setTimeout(
@@ -200,18 +209,38 @@ var useChessPuzzle = (puzzle, onSolve, onFail) => {
200
209
  if (!gameContext) {
201
210
  throw new Error("useChessPuzzle must be used within a ChessGameContext");
202
211
  }
203
- const onHint = () => {
212
+ const onHint = useCallback(() => {
204
213
  dispatch({ type: "TOGGLE_HINT" });
205
- };
206
- const puzzleContext = {
207
- status: state.status,
208
- changePuzzle,
209
- puzzle,
210
- hint: state.hint,
211
- onHint,
212
- nextMove: state.nextMove,
213
- isPlayerTurn: state.isPlayerTurn
214
- };
214
+ }, []);
215
+ const resetPuzzle = useCallback(() => {
216
+ changePuzzle(puzzle);
217
+ }, [changePuzzle, puzzle]);
218
+ const puzzleContext = useMemo(
219
+ () => ({
220
+ status: state.status,
221
+ changePuzzle,
222
+ resetPuzzle,
223
+ puzzle,
224
+ hint: state.hint,
225
+ onHint,
226
+ nextMove: state.nextMove,
227
+ isPlayerTurn: state.isPlayerTurn,
228
+ puzzleState: state.status,
229
+ movesPlayed: state.currentMoveIndex,
230
+ totalMoves: puzzle.moves.length
231
+ }),
232
+ [
233
+ state.status,
234
+ changePuzzle,
235
+ resetPuzzle,
236
+ puzzle,
237
+ state.hint,
238
+ onHint,
239
+ state.nextMove,
240
+ state.isPlayerTurn,
241
+ state.currentMoveIndex
242
+ ]
243
+ );
215
244
  useEffect(() => {
216
245
  var _a2, _b, _c;
217
246
  if (((_a2 = game == null ? void 0 : game.history()) == null ? void 0 : _a2.length) <= 0 + (puzzle.makeFirstMove ? 1 : 0)) {
@@ -222,8 +251,6 @@ var useChessPuzzle = (puzzle, onSolve, onFail) => {
222
251
  type: "PLAYER_MOVE",
223
252
  payload: {
224
253
  move: ((_c = (_b = gameContext == null ? void 0 : gameContext.game) == null ? void 0 : _b.history({ verbose: true })) == null ? void 0 : _c.pop()) ?? null,
225
- onSolve,
226
- onFail,
227
254
  puzzleContext,
228
255
  game
229
256
  }
@@ -233,6 +260,18 @@ var useChessPuzzle = (puzzle, onSolve, onFail) => {
233
260
  });
234
261
  }
235
262
  }, [(_a = game == null ? void 0 : game.history()) == null ? void 0 : _a.length]);
263
+ useEffect(() => {
264
+ if (state.status === "solved" && !state.onSolveInvoked && onSolve) {
265
+ onSolve(puzzleContext);
266
+ dispatch({ type: "MARK_SOLVE_INVOKED" });
267
+ }
268
+ }, [state.status, state.onSolveInvoked]);
269
+ useEffect(() => {
270
+ if (state.status === "failed" && !state.onFailInvoked && onFail) {
271
+ onFail(puzzleContext);
272
+ dispatch({ type: "MARK_FAIL_INVOKED" });
273
+ }
274
+ }, [state.status, state.onFailInvoked]);
236
275
  return puzzleContext;
237
276
  };
238
277
 
@@ -246,7 +285,7 @@ var useChessPuzzleContext = () => {
246
285
  const context = React2.useContext(ChessPuzzleContext);
247
286
  if (!context) {
248
287
  throw new Error(
249
- "useChessGameContext must be used within a ChessGameProvider"
288
+ `useChessPuzzleContext must be used within a ChessPuzzle component. Make sure your component is wrapped with <ChessPuzzle.Root> or ensure the ChessPuzzle component is properly rendered in the component tree.`
250
289
  );
251
290
  }
252
291
  return context;
@@ -320,7 +359,7 @@ var Reset = ({
320
359
  const { changePuzzle, status } = puzzleContext;
321
360
  const handleClick = () => {
322
361
  changePuzzle(puzzle || puzzleContext.puzzle);
323
- onReset == null ? void 0 : onReset();
362
+ onReset == null ? void 0 : onReset(puzzleContext);
324
363
  };
325
364
  if (!showOn.includes(status)) {
326
365
  return null;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/components/ChessPuzzle/parts/Root.tsx","../src/utils/index.ts","../src/hooks/useChessPuzzle.ts","../src/hooks/reducer.ts","../src/hooks/useChessPuzzleContext.ts","../src/components/ChessPuzzle/parts/PuzzleBoard.tsx","../src/components/ChessPuzzle/parts/Reset.tsx","../src/components/ChessPuzzle/parts/Hint.tsx","../src/components/ChessPuzzle/index.ts"],"sourcesContent":["import React from \"react\";\nimport { Puzzle, getOrientation } from \"../../../utils\";\nimport {\n ChessPuzzleContextType,\n useChessPuzzle,\n} from \"../../../hooks/useChessPuzzle\";\nimport { ChessGame } from \"@react-chess-tools/react-chess-game\";\nimport { ChessPuzzleContext } from \"../../../hooks/useChessPuzzleContext\";\n\nexport interface RootProps {\n puzzle: Puzzle;\n onSolve?: (puzzleContext: ChessPuzzleContextType) => void;\n onFail?: (puzzleContext: ChessPuzzleContextType) => void;\n}\n\nconst PuzzleRoot: React.FC<React.PropsWithChildren<RootProps>> = ({\n puzzle,\n onSolve,\n onFail,\n children,\n}) => {\n const context = useChessPuzzle(puzzle, onSolve, onFail);\n\n return (\n <ChessPuzzleContext.Provider value={context}>\n {children}\n </ChessPuzzleContext.Provider>\n );\n};\n\nexport const Root: React.FC<React.PropsWithChildren<RootProps>> = ({\n puzzle,\n onSolve,\n onFail,\n children,\n}) => {\n return (\n <ChessGame.Root fen={puzzle.fen} orientation={getOrientation(puzzle)}>\n <PuzzleRoot puzzle={puzzle} onSolve={onSolve} onFail={onFail}>\n {children}\n </PuzzleRoot>\n </ChessGame.Root>\n );\n};\n","import { type Color, Chess, Move } from \"chess.js\";\nimport React, { CSSProperties, ReactElement, ReactNode } from \"react\";\nimport _ from \"lodash\";\n\nexport type Status = \"not-started\" | \"in-progress\" | \"solved\" | \"failed\";\n\nexport type Hint = \"none\" | \"piece\" | \"move\";\n\nexport type Puzzle = {\n fen: string;\n moves: string[];\n // if the first move of the puzzle has to be made by the cpu, as in chess.com puzzles\n makeFirstMove?: boolean;\n};\n\nconst FAIL_COLOR = \"rgba(201, 52, 48, 0.5)\";\nconst SUCCESS_COLOR = \"rgba(172, 206, 89, 0.5)\";\nconst HINT_COLOR = \"rgba(27, 172, 166, 0.5)\";\n\nexport const getOrientation = (puzzle: Puzzle): Color => {\n const fen = puzzle.fen;\n const game = new Chess(fen);\n if (puzzle.makeFirstMove) {\n game.move(puzzle.moves[0]);\n }\n return game.turn();\n};\n\ninterface ClickableElement extends ReactElement {\n props: {\n onClick?: () => void;\n };\n}\n\nexport const isClickableElement = (\n element: ReactNode,\n): element is ClickableElement => React.isValidElement(element);\n\nexport const getCustomSquareStyles = (\n status: Status,\n hint: Hint,\n isPlayerTurn: boolean,\n game: Chess,\n nextMove?: Move | null,\n) => {\n const customSquareStyles: Record<string, CSSProperties> = {};\n\n const lastMove = _.last(game.history({ verbose: true }));\n\n if (status === \"failed\" && lastMove) {\n customSquareStyles[lastMove.from] = {\n backgroundColor: FAIL_COLOR,\n };\n customSquareStyles[lastMove.to] = {\n backgroundColor: FAIL_COLOR,\n };\n }\n\n if (\n lastMove &&\n (status === \"solved\" || (status !== \"failed\" && !isPlayerTurn))\n ) {\n customSquareStyles[lastMove.from] = {\n backgroundColor: SUCCESS_COLOR,\n };\n customSquareStyles[lastMove.to] = {\n backgroundColor: SUCCESS_COLOR,\n };\n }\n\n if (hint === \"piece\") {\n if (nextMove) {\n customSquareStyles[nextMove.from] = {\n backgroundColor: HINT_COLOR,\n };\n }\n }\n\n if (hint === \"move\") {\n if (nextMove) {\n customSquareStyles[nextMove.from] = {\n backgroundColor: HINT_COLOR,\n };\n customSquareStyles[nextMove.to] = {\n backgroundColor: HINT_COLOR,\n };\n }\n }\n\n return customSquareStyles;\n};\n\nexport const stringToMove = (game: Chess, move: string | null | undefined) => {\n const copy = new Chess(game.fen());\n if (move === null || move === undefined) {\n return null;\n }\n try {\n return copy.move(move);\n } catch (e) {\n return null;\n }\n};\n","import { useEffect, useReducer } from \"react\";\nimport { initializePuzzle, reducer } from \"./reducer\";\nimport { getOrientation, type Puzzle, type Hint, type Status } from \"../utils\";\nimport { useChessGameContext } from \"@react-chess-tools/react-chess-game\";\nimport { useChessPuzzleContext } from \"./useChessPuzzleContext\";\n\nexport type ChessPuzzleContextType = {\n status: Status;\n changePuzzle: (puzzle: Puzzle) => void;\n puzzle: Puzzle;\n hint: Hint;\n nextMove?: string | null;\n isPlayerTurn: boolean;\n onHint: () => void;\n};\n\nexport const useChessPuzzle = (\n puzzle: Puzzle,\n onSolve?: (puzzleContext: ChessPuzzleContextType) => void,\n onFail?: (puzzleContext: ChessPuzzleContextType) => void,\n): ChessPuzzleContextType => {\n const gameContext = useChessGameContext();\n\n const [state, dispatch] = useReducer(reducer, { puzzle }, initializePuzzle);\n\n const {\n game,\n methods: { makeMove, setPosition },\n } = gameContext;\n\n useEffect(() => {\n if (gameContext && game.fen() !== puzzle.fen) {\n setPosition(puzzle.fen, getOrientation(puzzle));\n }\n }, []);\n\n const changePuzzle = (puzzle: Puzzle) => {\n dispatch({ type: \"INITIALIZE\", payload: { puzzle } });\n setPosition(puzzle.fen, getOrientation(puzzle));\n };\n\n useEffect(() => {\n if (gameContext && game.fen() === puzzle.fen && state.needCpuMove) {\n setTimeout(\n () =>\n dispatch({\n type: \"CPU_MOVE\",\n }),\n 0,\n );\n }\n }, [gameContext, state.needCpuMove]);\n\n useEffect(() => {\n if (state.cpuMove) {\n makeMove(state.cpuMove);\n }\n }, [state.cpuMove]);\n\n if (!gameContext) {\n throw new Error(\"useChessPuzzle must be used within a ChessGameContext\");\n }\n\n const onHint = () => {\n dispatch({ type: \"TOGGLE_HINT\" });\n };\n\n const puzzleContext: ChessPuzzleContextType = {\n status: state.status,\n changePuzzle,\n puzzle,\n hint: state.hint,\n onHint,\n nextMove: state.nextMove,\n isPlayerTurn: state.isPlayerTurn,\n };\n\n useEffect(() => {\n if (game?.history()?.length <= 0 + (puzzle.makeFirstMove ? 1 : 0)) {\n return;\n }\n if (game.history().length % 2 === (puzzle.makeFirstMove ? 0 : 1)) {\n dispatch({\n type: \"PLAYER_MOVE\",\n payload: {\n move: gameContext?.game?.history({ verbose: true })?.pop() ?? null,\n onSolve,\n onFail,\n puzzleContext,\n game: game,\n },\n });\n\n dispatch({\n type: \"CPU_MOVE\",\n });\n }\n }, [game?.history()?.length]);\n\n return puzzleContext;\n};\n","import { Chess, Move } from \"chess.js\";\nimport { type Puzzle, type Hint, type Status } from \"../utils\";\nimport { ChessPuzzleContextType } from \"./useChessPuzzle\";\n\nexport type State = {\n puzzle: Puzzle;\n currentMoveIndex: number;\n status: Status;\n cpuMove?: string | null;\n nextMove?: string | null;\n hint: Hint;\n needCpuMove: boolean;\n isPlayerTurn: boolean;\n};\n\nexport type Action =\n | {\n type: \"INITIALIZE\";\n payload: {\n puzzle: Puzzle;\n };\n }\n | {\n type: \"RESET\";\n }\n | { type: \"TOGGLE_HINT\" }\n | {\n type: \"CPU_MOVE\";\n }\n | {\n type: \"PLAYER_MOVE\";\n payload: {\n move?: Move | null;\n onSolve?: (puzzleContext: ChessPuzzleContextType) => void;\n onFail?: (puzzleContext: ChessPuzzleContextType) => void;\n puzzleContext: ChessPuzzleContextType;\n game: Chess;\n };\n };\n\nexport const initializePuzzle = ({ puzzle }: { puzzle: Puzzle }): State => {\n return {\n puzzle,\n currentMoveIndex: 0,\n status: \"not-started\",\n nextMove: puzzle.moves[0],\n hint: \"none\",\n cpuMove: null,\n needCpuMove: !!puzzle.makeFirstMove,\n isPlayerTurn: !puzzle.makeFirstMove,\n };\n};\n\nexport const reducer = (state: State, action: Action): State => {\n switch (action.type) {\n case \"INITIALIZE\":\n return {\n ...state,\n ...initializePuzzle(action.payload),\n };\n case \"RESET\":\n return {\n ...state,\n ...initializePuzzle({\n puzzle: state.puzzle,\n }),\n };\n case \"TOGGLE_HINT\":\n if (state.hint === \"none\") {\n return { ...state, hint: \"piece\" };\n }\n return { ...state, hint: \"move\" };\n case \"CPU_MOVE\":\n if (state.isPlayerTurn) {\n return state;\n }\n if ([\"solved\", \"failed\"].includes(state.status)) {\n return state;\n }\n\n return {\n ...state,\n currentMoveIndex: state.currentMoveIndex + 1,\n cpuMove: state.puzzle.moves[state.currentMoveIndex],\n nextMove:\n state.currentMoveIndex < state.puzzle.moves.length - 1\n ? state.puzzle.moves[state.currentMoveIndex + 1]\n : null,\n needCpuMove: false,\n isPlayerTurn: true,\n status: \"in-progress\",\n };\n\n case \"PLAYER_MOVE\": {\n const { move, onSolve, onFail, puzzleContext } = action.payload;\n\n const isMoveRight = [move?.san, move?.lan].includes(\n state?.nextMove || \"\",\n );\n const isPuzzleSolved =\n state.currentMoveIndex === state.puzzle.moves.length - 1;\n\n if (!isMoveRight) {\n if (onFail) {\n onFail(puzzleContext);\n }\n return {\n ...state,\n status: \"failed\",\n nextMove: null,\n hint: \"none\",\n isPlayerTurn: false,\n };\n }\n\n if (isPuzzleSolved) {\n if (onSolve) {\n onSolve(puzzleContext);\n }\n\n return {\n ...state,\n status: \"solved\",\n nextMove: null,\n hint: \"none\",\n isPlayerTurn: false,\n };\n }\n\n return {\n ...state,\n hint: \"none\",\n currentMoveIndex: state.currentMoveIndex + 1,\n nextMove: state.puzzle.moves[state.currentMoveIndex + 1],\n status: \"in-progress\",\n needCpuMove: true,\n isPlayerTurn: false,\n };\n }\n\n default:\n return state;\n }\n};\n","import React from \"react\";\nimport { useChessPuzzle } from \"./useChessPuzzle\";\n\nexport const ChessPuzzleContext = React.createContext<ReturnType<\n typeof useChessPuzzle\n> | null>(null);\n\nexport const useChessPuzzleContext = () => {\n const context = React.useContext(ChessPuzzleContext);\n if (!context) {\n throw new Error(\n \"useChessGameContext must be used within a ChessGameProvider\",\n );\n }\n return context;\n};\n","import React from \"react\";\nimport {\n ChessGame,\n useChessGameContext,\n} from \"@react-chess-tools/react-chess-game\";\nimport { getCustomSquareStyles, stringToMove } from \"../../../utils\";\nimport { useChessPuzzleContext } from \"../../..\";\n\nexport interface PuzzleBoardProps\n extends React.ComponentProps<typeof ChessGame.Board> {}\nexport const PuzzleBoard: React.FC<PuzzleBoardProps> = ({ ...rest }) => {\n const puzzleContext = useChessPuzzleContext();\n const gameContext = useChessGameContext();\n\n if (!puzzleContext) {\n throw new Error(\"PuzzleContext not found\");\n }\n if (!gameContext) {\n throw new Error(\"ChessGameContext not found\");\n }\n\n const { game } = gameContext;\n const { status, hint, isPlayerTurn, nextMove } = puzzleContext;\n\n return (\n <ChessGame.Board\n customSquareStyles={getCustomSquareStyles(\n status,\n hint,\n isPlayerTurn,\n game,\n stringToMove(game, nextMove),\n )}\n {...rest}\n />\n );\n};\n","import React from \"react\";\nimport { isClickableElement, type Puzzle, type Status } from \"../../../utils\";\nimport { useChessPuzzleContext } from \"../../..\";\n\nexport interface ResetProps {\n asChild?: boolean;\n puzzle?: Puzzle;\n onReset?: () => void;\n showOn?: Status[];\n}\n\nconst defaultShowOn: Status[] = [\"failed\", \"solved\"];\n\nexport const Reset: React.FC<React.PropsWithChildren<ResetProps>> = ({\n children,\n asChild,\n puzzle,\n onReset,\n showOn = defaultShowOn,\n}) => {\n const puzzleContext = useChessPuzzleContext();\n if (!puzzleContext) {\n throw new Error(\"PuzzleContext not found\");\n }\n const { changePuzzle, status } = puzzleContext;\n const handleClick = () => {\n changePuzzle(puzzle || puzzleContext.puzzle);\n onReset?.();\n };\n\n if (!showOn.includes(status)) {\n return null;\n }\n\n if (asChild) {\n const child = React.Children.only(children);\n if (isClickableElement(child)) {\n return React.cloneElement(child, {\n onClick: handleClick,\n });\n } else {\n throw new Error(\"Change child must be a clickable element\");\n }\n }\n\n return (\n <button type=\"button\" onClick={handleClick}>\n {children}\n </button>\n );\n};\n","import React from \"react\";\nimport { Status, isClickableElement } from \"../../../utils\";\nimport { useChessPuzzleContext } from \"../../..\";\n\nexport interface HintProps {\n asChild?: boolean;\n showOn?: Status[];\n}\n\nconst defaultShowOn: Status[] = [\"not-started\", \"in-progress\"];\n\nexport const Hint: React.FC<React.PropsWithChildren<HintProps>> = ({\n children,\n asChild,\n showOn = defaultShowOn,\n}) => {\n const puzzleContext = useChessPuzzleContext();\n if (!puzzleContext) {\n throw new Error(\"PuzzleContext not found\");\n }\n const { onHint, status } = puzzleContext;\n const handleClick = () => {\n onHint();\n };\n\n if (!showOn.includes(status)) {\n return null;\n }\n\n if (asChild) {\n const child = React.Children.only(children);\n if (isClickableElement(child)) {\n return React.cloneElement(child, {\n onClick: handleClick,\n });\n } else {\n throw new Error(\"Change child must be a clickable element\");\n }\n }\n\n return (\n <button type=\"button\" onClick={handleClick}>\n {children}\n </button>\n );\n};\n","import { Root } from \"./parts/Root\";\nimport { PuzzleBoard } from \"./parts/PuzzleBoard\";\nimport { Reset } from \"./parts/Reset\";\nimport { Hint } from \"./parts/Hint\";\n\nexport const ChessPuzzle = {\n Root,\n Board: PuzzleBoard,\n Reset,\n Hint,\n};\n"],"mappings":"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","names":["React","puzzle","_a","React","React","React","ChessGame","useChessGameContext","useChessGameContext","React","ChessGame","React","React","React","defaultShowOn","React"]}
1
+ {"version":3,"sources":["../src/components/ChessPuzzle/parts/Root.tsx","../src/utils/index.ts","../src/hooks/useChessPuzzle.ts","../src/hooks/reducer.ts","../src/hooks/useChessPuzzleContext.ts","../src/components/ChessPuzzle/parts/PuzzleBoard.tsx","../src/components/ChessPuzzle/parts/Reset.tsx","../src/components/ChessPuzzle/parts/Hint.tsx","../src/components/ChessPuzzle/index.ts"],"sourcesContent":["import React from \"react\";\nimport { Puzzle, getOrientation } from \"../../../utils\";\nimport {\n ChessPuzzleContextType,\n useChessPuzzle,\n} from \"../../../hooks/useChessPuzzle\";\nimport { ChessGame } from \"@react-chess-tools/react-chess-game\";\nimport { ChessPuzzleContext } from \"../../../hooks/useChessPuzzleContext\";\n\nexport interface RootProps {\n puzzle: Puzzle;\n onSolve?: (puzzleContext: ChessPuzzleContextType) => void;\n onFail?: (puzzleContext: ChessPuzzleContextType) => void;\n}\n\nconst PuzzleRoot: React.FC<React.PropsWithChildren<RootProps>> = ({\n puzzle,\n onSolve,\n onFail,\n children,\n}) => {\n const context = useChessPuzzle(puzzle, onSolve, onFail);\n\n return (\n <ChessPuzzleContext.Provider value={context}>\n {children}\n </ChessPuzzleContext.Provider>\n );\n};\n\nexport const Root: React.FC<React.PropsWithChildren<RootProps>> = ({\n puzzle,\n onSolve,\n onFail,\n children,\n}) => {\n return (\n <ChessGame.Root fen={puzzle.fen} orientation={getOrientation(puzzle)}>\n <PuzzleRoot puzzle={puzzle} onSolve={onSolve} onFail={onFail}>\n {children}\n </PuzzleRoot>\n </ChessGame.Root>\n );\n};\n","import { type Color, Chess, Move } from \"chess.js\";\nimport React, { CSSProperties, ReactElement, ReactNode } from \"react\";\nimport _ from \"lodash\";\n\nexport type Status = \"not-started\" | \"in-progress\" | \"solved\" | \"failed\";\n\nexport type Hint = \"none\" | \"piece\" | \"move\";\n\nexport type Puzzle = {\n fen: string;\n moves: string[];\n // if the first move of the puzzle has to be made by the cpu, as in chess.com puzzles\n makeFirstMove?: boolean;\n};\n\nconst FAIL_COLOR = \"rgba(201, 52, 48, 0.5)\";\nconst SUCCESS_COLOR = \"rgba(172, 206, 89, 0.5)\";\nconst HINT_COLOR = \"rgba(27, 172, 166, 0.5)\";\n\nexport const getOrientation = (puzzle: Puzzle): Color => {\n const fen = puzzle.fen;\n const game = new Chess(fen);\n if (puzzle.makeFirstMove) {\n game.move(puzzle.moves[0]);\n }\n return game.turn();\n};\n\ninterface ClickableElement extends ReactElement {\n props: {\n onClick?: () => void;\n };\n}\n\nexport const isClickableElement = (\n element: ReactNode,\n): element is ClickableElement => React.isValidElement(element);\n\nexport const getCustomSquareStyles = (\n status: Status,\n hint: Hint,\n isPlayerTurn: boolean,\n game: Chess,\n nextMove?: Move | null,\n) => {\n const customSquareStyles: Record<string, CSSProperties> = {};\n\n const lastMove = _.last(game.history({ verbose: true }));\n\n if (status === \"failed\" && lastMove) {\n customSquareStyles[lastMove.from] = {\n backgroundColor: FAIL_COLOR,\n };\n customSquareStyles[lastMove.to] = {\n backgroundColor: FAIL_COLOR,\n };\n }\n\n if (\n lastMove &&\n (status === \"solved\" || (status !== \"failed\" && !isPlayerTurn))\n ) {\n customSquareStyles[lastMove.from] = {\n backgroundColor: SUCCESS_COLOR,\n };\n customSquareStyles[lastMove.to] = {\n backgroundColor: SUCCESS_COLOR,\n };\n }\n\n if (hint === \"piece\") {\n if (nextMove) {\n customSquareStyles[nextMove.from] = {\n backgroundColor: HINT_COLOR,\n };\n }\n }\n\n if (hint === \"move\") {\n if (nextMove) {\n customSquareStyles[nextMove.from] = {\n backgroundColor: HINT_COLOR,\n };\n customSquareStyles[nextMove.to] = {\n backgroundColor: HINT_COLOR,\n };\n }\n }\n\n return customSquareStyles;\n};\n\nexport const stringToMove = (game: Chess, move: string | null | undefined) => {\n const copy = new Chess(game.fen());\n if (move === null || move === undefined) {\n return null;\n }\n try {\n return copy.move(move);\n } catch (e) {\n return null;\n }\n};\n","import { useEffect, useReducer, useCallback, useMemo } from \"react\";\nimport { initializePuzzle, reducer } from \"./reducer\";\nimport { getOrientation, type Puzzle, type Hint, type Status } from \"../utils\";\nimport { useChessGameContext } from \"@react-chess-tools/react-chess-game\";\n\nexport type ChessPuzzleContextType = {\n status: Status;\n changePuzzle: (puzzle: Puzzle) => void;\n resetPuzzle: () => void;\n puzzle: Puzzle;\n hint: Hint;\n nextMove?: string | null;\n isPlayerTurn: boolean;\n onHint: () => void;\n puzzleState: Status;\n movesPlayed: number;\n totalMoves: number;\n};\n\nexport const useChessPuzzle = (\n puzzle: Puzzle,\n onSolve?: (puzzleContext: ChessPuzzleContextType) => void,\n onFail?: (puzzleContext: ChessPuzzleContextType) => void,\n): ChessPuzzleContextType => {\n const gameContext = useChessGameContext();\n\n const [state, dispatch] = useReducer(reducer, { puzzle }, initializePuzzle);\n\n const {\n game,\n methods: { makeMove, setPosition },\n } = gameContext;\n\n const changePuzzle = useCallback(\n (puzzle: Puzzle) => {\n setPosition(puzzle.fen, getOrientation(puzzle));\n dispatch({ type: \"INITIALIZE\", payload: { puzzle } });\n },\n [setPosition],\n );\n\n useEffect(() => {\n changePuzzle(puzzle);\n }, [JSON.stringify(puzzle), changePuzzle]);\n\n useEffect(() => {\n if (gameContext && game.fen() === puzzle.fen && state.needCpuMove) {\n setTimeout(\n () =>\n dispatch({\n type: \"CPU_MOVE\",\n }),\n 0,\n );\n }\n }, [gameContext, state.needCpuMove]);\n\n useEffect(() => {\n if (state.cpuMove) {\n makeMove(state.cpuMove);\n }\n }, [state.cpuMove]);\n\n if (!gameContext) {\n throw new Error(\"useChessPuzzle must be used within a ChessGameContext\");\n }\n\n const onHint = useCallback(() => {\n dispatch({ type: \"TOGGLE_HINT\" });\n }, []);\n\n const resetPuzzle = useCallback(() => {\n changePuzzle(puzzle);\n }, [changePuzzle, puzzle]);\n\n const puzzleContext: ChessPuzzleContextType = useMemo(\n () => ({\n status: state.status,\n changePuzzle,\n resetPuzzle,\n puzzle,\n hint: state.hint,\n onHint,\n nextMove: state.nextMove,\n isPlayerTurn: state.isPlayerTurn,\n puzzleState: state.status,\n movesPlayed: state.currentMoveIndex,\n totalMoves: puzzle.moves.length,\n }),\n [\n state.status,\n changePuzzle,\n resetPuzzle,\n puzzle,\n state.hint,\n onHint,\n state.nextMove,\n state.isPlayerTurn,\n state.currentMoveIndex,\n ],\n );\n\n useEffect(() => {\n if (game?.history()?.length <= 0 + (puzzle.makeFirstMove ? 1 : 0)) {\n return;\n }\n if (game.history().length % 2 === (puzzle.makeFirstMove ? 0 : 1)) {\n dispatch({\n type: \"PLAYER_MOVE\",\n payload: {\n move: gameContext?.game?.history({ verbose: true })?.pop() ?? null,\n puzzleContext,\n game: game,\n },\n });\n\n dispatch({\n type: \"CPU_MOVE\",\n });\n }\n }, [game?.history()?.length]);\n\n useEffect(() => {\n if (state.status === \"solved\" && !state.onSolveInvoked && onSolve) {\n onSolve(puzzleContext);\n dispatch({ type: \"MARK_SOLVE_INVOKED\" });\n }\n }, [state.status, state.onSolveInvoked]);\n\n useEffect(() => {\n if (state.status === \"failed\" && !state.onFailInvoked && onFail) {\n onFail(puzzleContext);\n dispatch({ type: \"MARK_FAIL_INVOKED\" });\n }\n }, [state.status, state.onFailInvoked]);\n\n return puzzleContext;\n};\n","import { Chess, Move } from \"chess.js\";\nimport { type Puzzle, type Hint, type Status } from \"../utils\";\nimport { ChessPuzzleContextType } from \"./useChessPuzzle\";\n\nexport type State = {\n puzzle: Puzzle;\n currentMoveIndex: number;\n status: Status;\n cpuMove?: string | null;\n nextMove?: string | null;\n hint: Hint;\n needCpuMove: boolean;\n isPlayerTurn: boolean;\n onSolveInvoked: boolean;\n onFailInvoked: boolean;\n};\n\nexport type Action =\n | {\n type: \"INITIALIZE\";\n payload: {\n puzzle: Puzzle;\n };\n }\n | {\n type: \"RESET\";\n }\n | { type: \"TOGGLE_HINT\" }\n | {\n type: \"CPU_MOVE\";\n }\n | {\n type: \"PLAYER_MOVE\";\n payload: {\n move?: Move | null;\n puzzleContext: ChessPuzzleContextType;\n game: Chess;\n };\n }\n | { type: \"MARK_SOLVE_INVOKED\" }\n | { type: \"MARK_FAIL_INVOKED\" };\n\nexport const initializePuzzle = ({ puzzle }: { puzzle: Puzzle }): State => {\n return {\n puzzle,\n currentMoveIndex: 0,\n status: \"not-started\",\n nextMove: puzzle.moves[0],\n hint: \"none\",\n cpuMove: null,\n needCpuMove: !!puzzle.makeFirstMove,\n isPlayerTurn: !puzzle.makeFirstMove,\n onSolveInvoked: false,\n onFailInvoked: false,\n };\n};\n\nexport const reducer = (state: State, action: Action): State => {\n switch (action.type) {\n case \"INITIALIZE\":\n return {\n ...state,\n ...initializePuzzle(action.payload),\n };\n case \"RESET\":\n return {\n ...state,\n ...initializePuzzle({\n puzzle: state.puzzle,\n }),\n };\n case \"TOGGLE_HINT\":\n if (state.hint === \"none\") {\n return { ...state, hint: \"piece\" };\n }\n return { ...state, hint: \"move\" };\n case \"CPU_MOVE\":\n if (state.isPlayerTurn) {\n return state;\n }\n if ([\"solved\", \"failed\"].includes(state.status)) {\n return state;\n }\n\n return {\n ...state,\n currentMoveIndex: state.currentMoveIndex + 1,\n cpuMove: state.puzzle.moves[state.currentMoveIndex],\n nextMove:\n state.currentMoveIndex < state.puzzle.moves.length - 1\n ? state.puzzle.moves[state.currentMoveIndex + 1]\n : null,\n needCpuMove: false,\n isPlayerTurn: true,\n status: \"in-progress\",\n };\n\n case \"PLAYER_MOVE\": {\n const { move } = action.payload;\n\n const isMoveRight = [move?.san, move?.lan].includes(\n state?.nextMove || \"\",\n );\n const isPuzzleSolved =\n state.currentMoveIndex === state.puzzle.moves.length - 1;\n\n if (!isMoveRight) {\n return {\n ...state,\n status: \"failed\",\n nextMove: null,\n hint: \"none\",\n isPlayerTurn: false,\n onFailInvoked: false,\n };\n }\n\n if (isPuzzleSolved) {\n return {\n ...state,\n status: \"solved\",\n nextMove: null,\n hint: \"none\",\n isPlayerTurn: false,\n onSolveInvoked: false,\n };\n }\n\n return {\n ...state,\n hint: \"none\",\n currentMoveIndex: state.currentMoveIndex + 1,\n nextMove: state.puzzle.moves[state.currentMoveIndex + 1],\n status: \"in-progress\",\n needCpuMove: true,\n isPlayerTurn: false,\n };\n }\n\n case \"MARK_SOLVE_INVOKED\":\n return {\n ...state,\n onSolveInvoked: true,\n };\n\n case \"MARK_FAIL_INVOKED\":\n return {\n ...state,\n onFailInvoked: true,\n };\n\n default:\n return state;\n }\n};\n","import React from \"react\";\nimport { useChessPuzzle } from \"./useChessPuzzle\";\n\nexport const ChessPuzzleContext = React.createContext<ReturnType<\n typeof useChessPuzzle\n> | null>(null);\n\nexport const useChessPuzzleContext = () => {\n const context = React.useContext(ChessPuzzleContext);\n if (!context) {\n throw new Error(\n `useChessPuzzleContext must be used within a ChessPuzzle component. Make sure your component is wrapped with <ChessPuzzle.Root> or ensure the ChessPuzzle component is properly rendered in the component tree.`,\n );\n }\n return context;\n};\n","import React from \"react\";\nimport {\n ChessGame,\n useChessGameContext,\n} from \"@react-chess-tools/react-chess-game\";\nimport { getCustomSquareStyles, stringToMove } from \"../../../utils\";\nimport { useChessPuzzleContext } from \"../../..\";\n\nexport interface PuzzleBoardProps\n extends React.ComponentProps<typeof ChessGame.Board> {}\nexport const PuzzleBoard: React.FC<PuzzleBoardProps> = ({ ...rest }) => {\n const puzzleContext = useChessPuzzleContext();\n const gameContext = useChessGameContext();\n\n if (!puzzleContext) {\n throw new Error(\"PuzzleContext not found\");\n }\n if (!gameContext) {\n throw new Error(\"ChessGameContext not found\");\n }\n\n const { game } = gameContext;\n const { status, hint, isPlayerTurn, nextMove } = puzzleContext;\n\n return (\n <ChessGame.Board\n customSquareStyles={getCustomSquareStyles(\n status,\n hint,\n isPlayerTurn,\n game,\n stringToMove(game, nextMove),\n )}\n {...rest}\n />\n );\n};\n","import React from \"react\";\nimport { isClickableElement, type Puzzle, type Status } from \"../../../utils\";\nimport { useChessPuzzleContext, type ChessPuzzleContextType } from \"../../..\";\n\nexport interface ResetProps {\n asChild?: boolean;\n puzzle?: Puzzle;\n onReset?: (puzzleContext: ChessPuzzleContextType) => void;\n showOn?: Status[];\n}\n\nconst defaultShowOn: Status[] = [\"failed\", \"solved\"];\n\nexport const Reset: React.FC<React.PropsWithChildren<ResetProps>> = ({\n children,\n asChild,\n puzzle,\n onReset,\n showOn = defaultShowOn,\n}) => {\n const puzzleContext = useChessPuzzleContext();\n if (!puzzleContext) {\n throw new Error(\"PuzzleContext not found\");\n }\n const { changePuzzle, status } = puzzleContext;\n const handleClick = () => {\n changePuzzle(puzzle || puzzleContext.puzzle);\n onReset?.(puzzleContext);\n };\n\n if (!showOn.includes(status)) {\n return null;\n }\n\n if (asChild) {\n const child = React.Children.only(children);\n if (isClickableElement(child)) {\n return React.cloneElement(child, {\n onClick: handleClick,\n });\n } else {\n throw new Error(\"Change child must be a clickable element\");\n }\n }\n\n return (\n <button type=\"button\" onClick={handleClick}>\n {children}\n </button>\n );\n};\n","import React from \"react\";\nimport { Status, isClickableElement } from \"../../../utils\";\nimport { useChessPuzzleContext } from \"../../..\";\n\nexport interface HintProps {\n asChild?: boolean;\n showOn?: Status[];\n}\n\nconst defaultShowOn: Status[] = [\"not-started\", \"in-progress\"];\n\nexport const Hint: React.FC<React.PropsWithChildren<HintProps>> = ({\n children,\n asChild,\n showOn = defaultShowOn,\n}) => {\n const puzzleContext = useChessPuzzleContext();\n if (!puzzleContext) {\n throw new Error(\"PuzzleContext not found\");\n }\n const { onHint, status } = puzzleContext;\n const handleClick = () => {\n onHint();\n };\n\n if (!showOn.includes(status)) {\n return null;\n }\n\n if (asChild) {\n const child = React.Children.only(children);\n if (isClickableElement(child)) {\n return React.cloneElement(child, {\n onClick: handleClick,\n });\n } else {\n throw new Error(\"Change child must be a clickable element\");\n }\n }\n\n return (\n <button type=\"button\" onClick={handleClick}>\n {children}\n </button>\n );\n};\n","import { Root } from \"./parts/Root\";\nimport { PuzzleBoard } from \"./parts/PuzzleBoard\";\nimport { Reset } from \"./parts/Reset\";\nimport { Hint } from \"./parts/Hint\";\n\nexport const ChessPuzzle = {\n Root,\n Board: PuzzleBoard,\n Reset,\n Hint,\n};\n"],"mappings":"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","names":["React","puzzle","_a","React","React","React","ChessGame","useChessGameContext","useChessGameContext","React","ChessGame","React","React","React","defaultShowOn","React"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-chess-tools/react-chess-puzzle",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "A lightweight, customizable React component library for rendering and interacting with chess puzzles.",
5
5
  "main": "dist/index.mjs",
6
6
  "module": "dist/index.mjs",
@@ -32,7 +32,7 @@
32
32
  "author": "Daniele Cammareri <daniele.cammareri@gmail.com>",
33
33
  "license": "MIT",
34
34
  "dependencies": {
35
- "@react-chess-tools/react-chess-game": "0.4.0",
35
+ "@react-chess-tools/react-chess-game": "0.4.2",
36
36
  "chess.js": "^1.0.0-beta.8",
37
37
  "lodash": "^4.17.21"
38
38
  },
@@ -1,11 +1,11 @@
1
1
  import React from "react";
2
2
  import { isClickableElement, type Puzzle, type Status } from "../../../utils";
3
- import { useChessPuzzleContext } from "../../..";
3
+ import { useChessPuzzleContext, type ChessPuzzleContextType } from "../../..";
4
4
 
5
5
  export interface ResetProps {
6
6
  asChild?: boolean;
7
7
  puzzle?: Puzzle;
8
- onReset?: () => void;
8
+ onReset?: (puzzleContext: ChessPuzzleContextType) => void;
9
9
  showOn?: Status[];
10
10
  }
11
11
 
@@ -25,7 +25,7 @@ export const Reset: React.FC<React.PropsWithChildren<ResetProps>> = ({
25
25
  const { changePuzzle, status } = puzzleContext;
26
26
  const handleClick = () => {
27
27
  changePuzzle(puzzle || puzzleContext.puzzle);
28
- onReset?.();
28
+ onReset?.(puzzleContext);
29
29
  };
30
30
 
31
31
  if (!showOn.includes(status)) {
@@ -0,0 +1,274 @@
1
+ import { Chess, Move } from "chess.js";
2
+ import { reducer, initializePuzzle, State, Action } from "../reducer";
3
+ import { Puzzle } from "../../utils";
4
+ import { ChessPuzzleContextType } from "../useChessPuzzle";
5
+
6
+ describe("reducer", () => {
7
+ // Mock puzzle data
8
+ const mockPuzzle: Puzzle = {
9
+ fen: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
10
+ moves: ["e4", "e5", "Nf3", "Nc6", "Bb5"],
11
+ makeFirstMove: false,
12
+ };
13
+
14
+ const mockPuzzleWithFirstMove: Puzzle = {
15
+ fen: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
16
+ moves: ["e4", "e5", "Nf3", "Nc6", "Bb5"],
17
+ makeFirstMove: true,
18
+ };
19
+
20
+ // Initial state
21
+ const initialState: State = {
22
+ puzzle: mockPuzzle,
23
+ currentMoveIndex: 0,
24
+ status: "not-started",
25
+ nextMove: "e4",
26
+ hint: "none",
27
+ cpuMove: null,
28
+ needCpuMove: false,
29
+ isPlayerTurn: true,
30
+ onSolveInvoked: false,
31
+ onFailInvoked: false,
32
+ };
33
+
34
+ describe("initializePuzzle", () => {
35
+ it("should initialize a puzzle correctly", () => {
36
+ const state = initializePuzzle({ puzzle: mockPuzzle });
37
+
38
+ expect(state).toEqual({
39
+ puzzle: mockPuzzle,
40
+ currentMoveIndex: 0,
41
+ status: "not-started",
42
+ nextMove: "e4",
43
+ hint: "none",
44
+ cpuMove: null,
45
+ needCpuMove: false,
46
+ isPlayerTurn: true,
47
+ onSolveInvoked: false,
48
+ onFailInvoked: false,
49
+ });
50
+ });
51
+
52
+ it("should set needCpuMove to true if makeFirstMove is true", () => {
53
+ const state = initializePuzzle({ puzzle: mockPuzzleWithFirstMove });
54
+
55
+ expect(state.needCpuMove).toBe(true);
56
+ expect(state.isPlayerTurn).toBe(false);
57
+ });
58
+ });
59
+
60
+ describe("INITIALIZE action", () => {
61
+ it("should initialize with a new puzzle", () => {
62
+ const action: Action = {
63
+ type: "INITIALIZE",
64
+ payload: {
65
+ puzzle: mockPuzzle,
66
+ },
67
+ };
68
+
69
+ const newState = reducer(initialState, action);
70
+
71
+ expect(newState).toEqual(initialState);
72
+ });
73
+ });
74
+
75
+ describe("RESET action", () => {
76
+ it("should reset the puzzle to initial state", () => {
77
+ const modifiedState: State = {
78
+ ...initialState,
79
+ currentMoveIndex: 2,
80
+ status: "in-progress",
81
+ hint: "piece",
82
+ };
83
+
84
+ const action: Action = {
85
+ type: "RESET",
86
+ };
87
+
88
+ const newState = reducer(modifiedState, action);
89
+
90
+ expect(newState).toEqual(initialState);
91
+ });
92
+ });
93
+
94
+ describe("TOGGLE_HINT action", () => {
95
+ it("should change hint from none to piece", () => {
96
+ const action: Action = {
97
+ type: "TOGGLE_HINT",
98
+ };
99
+
100
+ const newState = reducer(initialState, action);
101
+
102
+ expect(newState.hint).toBe("piece");
103
+ });
104
+
105
+ it("should change hint from piece to move", () => {
106
+ const stateWithPieceHint: State = {
107
+ ...initialState,
108
+ hint: "piece",
109
+ };
110
+
111
+ const action: Action = {
112
+ type: "TOGGLE_HINT",
113
+ };
114
+
115
+ const newState = reducer(stateWithPieceHint, action);
116
+
117
+ expect(newState.hint).toBe("move");
118
+ });
119
+ });
120
+
121
+ describe("CPU_MOVE action", () => {
122
+ it("should process CPU move and update state", () => {
123
+ const state: State = {
124
+ ...initialState,
125
+ isPlayerTurn: false,
126
+ needCpuMove: true,
127
+ };
128
+
129
+ const action: Action = {
130
+ type: "CPU_MOVE",
131
+ };
132
+
133
+ const newState = reducer(state, action);
134
+
135
+ expect(newState.currentMoveIndex).toBe(1);
136
+ expect(newState.cpuMove).toBe("e4");
137
+ expect(newState.nextMove).toBe("e5");
138
+ expect(newState.needCpuMove).toBe(false);
139
+ expect(newState.isPlayerTurn).toBe(true);
140
+ expect(newState.status).toBe("in-progress");
141
+ });
142
+
143
+ it("should not change state if it's player's turn", () => {
144
+ const action: Action = {
145
+ type: "CPU_MOVE",
146
+ };
147
+
148
+ const newState = reducer(initialState, action);
149
+
150
+ expect(newState).toBe(initialState);
151
+ });
152
+
153
+ it("should not change state if puzzle is solved or failed", () => {
154
+ const solvedState: State = {
155
+ ...initialState,
156
+ isPlayerTurn: false,
157
+ status: "solved",
158
+ };
159
+
160
+ const action: Action = {
161
+ type: "CPU_MOVE",
162
+ };
163
+
164
+ const newState = reducer(solvedState, action);
165
+
166
+ expect(newState).toBe(solvedState);
167
+ });
168
+
169
+ it("should set nextMove to null when reaching last move", () => {
170
+ const state: State = {
171
+ ...initialState,
172
+ isPlayerTurn: false,
173
+ needCpuMove: true,
174
+ currentMoveIndex: 4, // Last move index
175
+ };
176
+
177
+ const action: Action = {
178
+ type: "CPU_MOVE",
179
+ };
180
+
181
+ const newState = reducer(state, action);
182
+
183
+ expect(newState.nextMove).toBe(null);
184
+ });
185
+ });
186
+
187
+ describe("PLAYER_MOVE action", () => {
188
+ const game = new Chess(mockPuzzle.fen);
189
+ const mockContext = {} as ChessPuzzleContextType; // Mock puzzle context
190
+
191
+ it("should handle correct player move", () => {
192
+ const move = { san: "e4", lan: "e2e4" } as Move;
193
+
194
+ const action: Action = {
195
+ type: "PLAYER_MOVE",
196
+ payload: {
197
+ move,
198
+ puzzleContext: mockContext,
199
+ game,
200
+ },
201
+ };
202
+
203
+ const newState = reducer(initialState, action);
204
+
205
+ expect(newState.currentMoveIndex).toBe(1);
206
+ expect(newState.nextMove).toBe("e5");
207
+ expect(newState.hint).toBe("none");
208
+ expect(newState.needCpuMove).toBe(true);
209
+ expect(newState.isPlayerTurn).toBe(false);
210
+ expect(newState.status).toBe("in-progress");
211
+ });
212
+
213
+ it("should handle incorrect player move", () => {
214
+ const move = { san: "d4", lan: "d2d4" } as Move;
215
+
216
+ const action: Action = {
217
+ type: "PLAYER_MOVE",
218
+ payload: {
219
+ move,
220
+ puzzleContext: mockContext,
221
+ game,
222
+ },
223
+ };
224
+
225
+ const newState = reducer(initialState, action);
226
+
227
+ expect(newState.status).toBe("failed");
228
+ expect(newState.nextMove).toBe(null);
229
+ expect(newState.hint).toBe("none");
230
+ expect(newState.isPlayerTurn).toBe(false);
231
+ });
232
+
233
+ it("should handle solving the puzzle", () => {
234
+ const move = { san: "Bb5", lan: "f1b5" } as Move;
235
+
236
+ const lastMoveState: State = {
237
+ ...initialState,
238
+ currentMoveIndex: 4,
239
+ nextMove: "Bb5",
240
+ };
241
+
242
+ const action: Action = {
243
+ type: "PLAYER_MOVE",
244
+ payload: {
245
+ move,
246
+ puzzleContext: mockContext,
247
+ game,
248
+ },
249
+ };
250
+
251
+ const newState = reducer(lastMoveState, action);
252
+
253
+ expect(newState.status).toBe("solved");
254
+ expect(newState.nextMove).toBe(null);
255
+ expect(newState.hint).toBe("none");
256
+ expect(newState.isPlayerTurn).toBe(false);
257
+ });
258
+
259
+ it("should handle null move", () => {
260
+ const action: Action = {
261
+ type: "PLAYER_MOVE",
262
+ payload: {
263
+ move: null,
264
+ puzzleContext: mockContext,
265
+ game,
266
+ },
267
+ };
268
+
269
+ const newState = reducer(initialState, action);
270
+
271
+ expect(newState.status).toBe("failed");
272
+ });
273
+ });
274
+ });
@@ -11,6 +11,8 @@ export type State = {
11
11
  hint: Hint;
12
12
  needCpuMove: boolean;
13
13
  isPlayerTurn: boolean;
14
+ onSolveInvoked: boolean;
15
+ onFailInvoked: boolean;
14
16
  };
15
17
 
16
18
  export type Action =
@@ -31,12 +33,12 @@ export type Action =
31
33
  type: "PLAYER_MOVE";
32
34
  payload: {
33
35
  move?: Move | null;
34
- onSolve?: (puzzleContext: ChessPuzzleContextType) => void;
35
- onFail?: (puzzleContext: ChessPuzzleContextType) => void;
36
36
  puzzleContext: ChessPuzzleContextType;
37
37
  game: Chess;
38
38
  };
39
- };
39
+ }
40
+ | { type: "MARK_SOLVE_INVOKED" }
41
+ | { type: "MARK_FAIL_INVOKED" };
40
42
 
41
43
  export const initializePuzzle = ({ puzzle }: { puzzle: Puzzle }): State => {
42
44
  return {
@@ -48,6 +50,8 @@ export const initializePuzzle = ({ puzzle }: { puzzle: Puzzle }): State => {
48
50
  cpuMove: null,
49
51
  needCpuMove: !!puzzle.makeFirstMove,
50
52
  isPlayerTurn: !puzzle.makeFirstMove,
53
+ onSolveInvoked: false,
54
+ onFailInvoked: false,
51
55
  };
52
56
  };
53
57
 
@@ -92,7 +96,7 @@ export const reducer = (state: State, action: Action): State => {
92
96
  };
93
97
 
94
98
  case "PLAYER_MOVE": {
95
- const { move, onSolve, onFail, puzzleContext } = action.payload;
99
+ const { move } = action.payload;
96
100
 
97
101
  const isMoveRight = [move?.san, move?.lan].includes(
98
102
  state?.nextMove || "",
@@ -101,29 +105,24 @@ export const reducer = (state: State, action: Action): State => {
101
105
  state.currentMoveIndex === state.puzzle.moves.length - 1;
102
106
 
103
107
  if (!isMoveRight) {
104
- if (onFail) {
105
- onFail(puzzleContext);
106
- }
107
108
  return {
108
109
  ...state,
109
110
  status: "failed",
110
111
  nextMove: null,
111
112
  hint: "none",
112
113
  isPlayerTurn: false,
114
+ onFailInvoked: false,
113
115
  };
114
116
  }
115
117
 
116
118
  if (isPuzzleSolved) {
117
- if (onSolve) {
118
- onSolve(puzzleContext);
119
- }
120
-
121
119
  return {
122
120
  ...state,
123
121
  status: "solved",
124
122
  nextMove: null,
125
123
  hint: "none",
126
124
  isPlayerTurn: false,
125
+ onSolveInvoked: false,
127
126
  };
128
127
  }
129
128
 
@@ -138,6 +137,18 @@ export const reducer = (state: State, action: Action): State => {
138
137
  };
139
138
  }
140
139
 
140
+ case "MARK_SOLVE_INVOKED":
141
+ return {
142
+ ...state,
143
+ onSolveInvoked: true,
144
+ };
145
+
146
+ case "MARK_FAIL_INVOKED":
147
+ return {
148
+ ...state,
149
+ onFailInvoked: true,
150
+ };
151
+
141
152
  default:
142
153
  return state;
143
154
  }
@@ -1,17 +1,20 @@
1
- import { useEffect, useReducer } from "react";
1
+ import { useEffect, useReducer, useCallback, useMemo } from "react";
2
2
  import { initializePuzzle, reducer } from "./reducer";
3
3
  import { getOrientation, type Puzzle, type Hint, type Status } from "../utils";
4
4
  import { useChessGameContext } from "@react-chess-tools/react-chess-game";
5
- import { useChessPuzzleContext } from "./useChessPuzzleContext";
6
5
 
7
6
  export type ChessPuzzleContextType = {
8
7
  status: Status;
9
8
  changePuzzle: (puzzle: Puzzle) => void;
9
+ resetPuzzle: () => void;
10
10
  puzzle: Puzzle;
11
11
  hint: Hint;
12
12
  nextMove?: string | null;
13
13
  isPlayerTurn: boolean;
14
14
  onHint: () => void;
15
+ puzzleState: Status;
16
+ movesPlayed: number;
17
+ totalMoves: number;
15
18
  };
16
19
 
17
20
  export const useChessPuzzle = (
@@ -28,16 +31,17 @@ export const useChessPuzzle = (
28
31
  methods: { makeMove, setPosition },
29
32
  } = gameContext;
30
33
 
31
- useEffect(() => {
32
- if (gameContext && game.fen() !== puzzle.fen) {
34
+ const changePuzzle = useCallback(
35
+ (puzzle: Puzzle) => {
33
36
  setPosition(puzzle.fen, getOrientation(puzzle));
34
- }
35
- }, []);
37
+ dispatch({ type: "INITIALIZE", payload: { puzzle } });
38
+ },
39
+ [setPosition],
40
+ );
36
41
 
37
- const changePuzzle = (puzzle: Puzzle) => {
38
- dispatch({ type: "INITIALIZE", payload: { puzzle } });
39
- setPosition(puzzle.fen, getOrientation(puzzle));
40
- };
42
+ useEffect(() => {
43
+ changePuzzle(puzzle);
44
+ }, [JSON.stringify(puzzle), changePuzzle]);
41
45
 
42
46
  useEffect(() => {
43
47
  if (gameContext && game.fen() === puzzle.fen && state.needCpuMove) {
@@ -61,19 +65,40 @@ export const useChessPuzzle = (
61
65
  throw new Error("useChessPuzzle must be used within a ChessGameContext");
62
66
  }
63
67
 
64
- const onHint = () => {
68
+ const onHint = useCallback(() => {
65
69
  dispatch({ type: "TOGGLE_HINT" });
66
- };
67
-
68
- const puzzleContext: ChessPuzzleContextType = {
69
- status: state.status,
70
- changePuzzle,
71
- puzzle,
72
- hint: state.hint,
73
- onHint,
74
- nextMove: state.nextMove,
75
- isPlayerTurn: state.isPlayerTurn,
76
- };
70
+ }, []);
71
+
72
+ const resetPuzzle = useCallback(() => {
73
+ changePuzzle(puzzle);
74
+ }, [changePuzzle, puzzle]);
75
+
76
+ const puzzleContext: ChessPuzzleContextType = useMemo(
77
+ () => ({
78
+ status: state.status,
79
+ changePuzzle,
80
+ resetPuzzle,
81
+ puzzle,
82
+ hint: state.hint,
83
+ onHint,
84
+ nextMove: state.nextMove,
85
+ isPlayerTurn: state.isPlayerTurn,
86
+ puzzleState: state.status,
87
+ movesPlayed: state.currentMoveIndex,
88
+ totalMoves: puzzle.moves.length,
89
+ }),
90
+ [
91
+ state.status,
92
+ changePuzzle,
93
+ resetPuzzle,
94
+ puzzle,
95
+ state.hint,
96
+ onHint,
97
+ state.nextMove,
98
+ state.isPlayerTurn,
99
+ state.currentMoveIndex,
100
+ ],
101
+ );
77
102
 
78
103
  useEffect(() => {
79
104
  if (game?.history()?.length <= 0 + (puzzle.makeFirstMove ? 1 : 0)) {
@@ -84,8 +109,6 @@ export const useChessPuzzle = (
84
109
  type: "PLAYER_MOVE",
85
110
  payload: {
86
111
  move: gameContext?.game?.history({ verbose: true })?.pop() ?? null,
87
- onSolve,
88
- onFail,
89
112
  puzzleContext,
90
113
  game: game,
91
114
  },
@@ -97,5 +120,19 @@ export const useChessPuzzle = (
97
120
  }
98
121
  }, [game?.history()?.length]);
99
122
 
123
+ useEffect(() => {
124
+ if (state.status === "solved" && !state.onSolveInvoked && onSolve) {
125
+ onSolve(puzzleContext);
126
+ dispatch({ type: "MARK_SOLVE_INVOKED" });
127
+ }
128
+ }, [state.status, state.onSolveInvoked]);
129
+
130
+ useEffect(() => {
131
+ if (state.status === "failed" && !state.onFailInvoked && onFail) {
132
+ onFail(puzzleContext);
133
+ dispatch({ type: "MARK_FAIL_INVOKED" });
134
+ }
135
+ }, [state.status, state.onFailInvoked]);
136
+
100
137
  return puzzleContext;
101
138
  };
@@ -9,7 +9,7 @@ export const useChessPuzzleContext = () => {
9
9
  const context = React.useContext(ChessPuzzleContext);
10
10
  if (!context) {
11
11
  throw new Error(
12
- "useChessGameContext must be used within a ChessGameProvider",
12
+ `useChessPuzzleContext must be used within a ChessPuzzle component. Make sure your component is wrapped with <ChessPuzzle.Root> or ensure the ChessPuzzle component is properly rendered in the component tree.`,
13
13
  );
14
14
  }
15
15
  return context;
package/src/index.ts CHANGED
@@ -1,2 +1,15 @@
1
+ // Components
1
2
  export { ChessPuzzle } from "./components/ChessPuzzle";
3
+
4
+ // Hooks & Context
2
5
  export { useChessPuzzleContext } from "./hooks/useChessPuzzleContext";
6
+ export type { ChessPuzzleContextType } from "./hooks/useChessPuzzle";
7
+
8
+ // Core Types
9
+ export type { Status, Hint, Puzzle } from "./utils";
10
+
11
+ // Component Props
12
+ export type { HintProps } from "./components/ChessPuzzle/parts/Hint";
13
+ export type { ResetProps } from "./components/ChessPuzzle/parts/Reset";
14
+ export type { PuzzleBoardProps } from "./components/ChessPuzzle/parts/PuzzleBoard";
15
+ export type { RootProps } from "./components/ChessPuzzle/parts/Root";
@@ -0,0 +1,192 @@
1
+ import { Chess, Move } from "chess.js";
2
+ import React from "react";
3
+ import {
4
+ getOrientation,
5
+ isClickableElement,
6
+ getCustomSquareStyles,
7
+ stringToMove,
8
+ Puzzle,
9
+ } from "../index";
10
+
11
+ describe("Puzzle Utilities", () => {
12
+ describe("getOrientation", () => {
13
+ it("should return white when it's white's turn", () => {
14
+ const puzzle: Puzzle = {
15
+ fen: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
16
+ moves: ["e4", "e5", "Nf3"],
17
+ };
18
+
19
+ expect(getOrientation(puzzle)).toBe("w");
20
+ });
21
+
22
+ it("should return black when it's black's turn", () => {
23
+ const puzzle: Puzzle = {
24
+ fen: "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1",
25
+ moves: ["e5", "Nf3", "Nc6"],
26
+ };
27
+
28
+ expect(getOrientation(puzzle)).toBe("b");
29
+ });
30
+
31
+ it("should make first move if makeFirstMove is true", () => {
32
+ const puzzle: Puzzle = {
33
+ fen: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
34
+ moves: ["e4", "e5", "Nf3"],
35
+ makeFirstMove: true,
36
+ };
37
+
38
+ // After e4, it should be black's turn
39
+ expect(getOrientation(puzzle)).toBe("b");
40
+ });
41
+ });
42
+
43
+ describe("isClickableElement", () => {
44
+ it("should return true for valid clickable elements", () => {
45
+ const clickableElement = React.createElement("button", {
46
+ onClick: () => {},
47
+ });
48
+ expect(isClickableElement(clickableElement)).toBe(true);
49
+ });
50
+
51
+ it("should return false for non-React elements", () => {
52
+ expect(isClickableElement("not an element")).toBe(false);
53
+ expect(isClickableElement(null)).toBe(false);
54
+ expect(isClickableElement(undefined)).toBe(false);
55
+ });
56
+ });
57
+
58
+ describe("getCustomSquareStyles", () => {
59
+ const game = new Chess();
60
+
61
+ it("should return empty object when no conditions are met", () => {
62
+ const styles = getCustomSquareStyles("not-started", "none", true, game);
63
+ expect(styles).toEqual({});
64
+ });
65
+
66
+ it("should highlight last move with fail color when status is failed", () => {
67
+ const testGame = new Chess();
68
+ testGame.move("e4");
69
+
70
+ const styles = getCustomSquareStyles("failed", "none", true, testGame);
71
+
72
+ expect(styles["e2"]).toHaveProperty(
73
+ "backgroundColor",
74
+ "rgba(201, 52, 48, 0.5)",
75
+ );
76
+ expect(styles["e4"]).toHaveProperty(
77
+ "backgroundColor",
78
+ "rgba(201, 52, 48, 0.5)",
79
+ );
80
+ });
81
+
82
+ it("should highlight last move with success color when status is solved", () => {
83
+ const testGame = new Chess();
84
+ testGame.move("e4");
85
+
86
+ const styles = getCustomSquareStyles("solved", "none", true, testGame);
87
+
88
+ expect(styles["e2"]).toHaveProperty(
89
+ "backgroundColor",
90
+ "rgba(172, 206, 89, 0.5)",
91
+ );
92
+ expect(styles["e4"]).toHaveProperty(
93
+ "backgroundColor",
94
+ "rgba(172, 206, 89, 0.5)",
95
+ );
96
+ });
97
+
98
+ it("should highlight move source square when hint is piece", () => {
99
+ const testGame = new Chess();
100
+ testGame.move("e4");
101
+ const nextMove = testGame.history({ verbose: true })[0] as Move;
102
+
103
+ const styles = getCustomSquareStyles(
104
+ "in-progress",
105
+ "piece",
106
+ true,
107
+ game,
108
+ nextMove,
109
+ );
110
+
111
+ expect(styles["e2"]).toHaveProperty(
112
+ "backgroundColor",
113
+ "rgba(27, 172, 166, 0.5)",
114
+ );
115
+ expect(styles["e4"]).toBeUndefined();
116
+ });
117
+
118
+ it("should highlight move source and destination when hint is move", () => {
119
+ const testGame = new Chess();
120
+ testGame.move("e4");
121
+ const nextMove = testGame.history({ verbose: true })[0] as Move;
122
+
123
+ const styles = getCustomSquareStyles(
124
+ "in-progress",
125
+ "move",
126
+ true,
127
+ game,
128
+ nextMove,
129
+ );
130
+
131
+ expect(styles["e2"]).toHaveProperty(
132
+ "backgroundColor",
133
+ "rgba(27, 172, 166, 0.5)",
134
+ );
135
+ expect(styles["e4"]).toHaveProperty(
136
+ "backgroundColor",
137
+ "rgba(27, 172, 166, 0.5)",
138
+ );
139
+ });
140
+
141
+ it("should highlight with success color when not failed and not player turn", () => {
142
+ const testGame = new Chess();
143
+ testGame.move("e4");
144
+
145
+ const styles = getCustomSquareStyles(
146
+ "in-progress",
147
+ "none",
148
+ false,
149
+ testGame,
150
+ );
151
+
152
+ expect(styles["e2"]).toHaveProperty(
153
+ "backgroundColor",
154
+ "rgba(172, 206, 89, 0.5)",
155
+ );
156
+ expect(styles["e4"]).toHaveProperty(
157
+ "backgroundColor",
158
+ "rgba(172, 206, 89, 0.5)",
159
+ );
160
+ });
161
+ });
162
+
163
+ describe("stringToMove", () => {
164
+ const game = new Chess();
165
+
166
+ it("should return null for null or undefined input", () => {
167
+ expect(stringToMove(game, null)).toBeNull();
168
+ expect(stringToMove(game, undefined)).toBeNull();
169
+ });
170
+
171
+ it("should return a valid move object for legal moves", () => {
172
+ const move = stringToMove(game, "e4");
173
+
174
+ expect(move).not.toBeNull();
175
+ expect(move?.from).toBe("e2");
176
+ expect(move?.to).toBe("e4");
177
+ expect(move?.piece).toBe("p");
178
+ });
179
+
180
+ it("should return null for illegal moves", () => {
181
+ expect(stringToMove(game, "e5")).toBeNull();
182
+ expect(stringToMove(game, "invalid")).toBeNull();
183
+ });
184
+
185
+ it("should not modify the original game", () => {
186
+ const originalFen = game.fen();
187
+ stringToMove(game, "e4");
188
+
189
+ expect(game.fen()).toBe(originalFen);
190
+ });
191
+ });
192
+ });