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

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,20 @@
1
1
  # @react-chess-tools/react-chess-puzzle
2
2
 
3
+ ## 0.5.1
4
+
5
+ ### Patch Changes
6
+
7
+ - ca0a210: add missing properties to ChessPuzzleContextType
8
+ - 755a85d: fix: export missing TypeScript types and component props
9
+ - a9c69b5: test: add unit tests
10
+ - 5ed0baf: fix: fix SSR compatibility and prevent unnecessary sound re-renders
11
+ - c3ac738: fix: fix state management and lifecycle issues in chess components
12
+ - Updated dependencies [755a85d]
13
+ - Updated dependencies [a9c69b5]
14
+ - Updated dependencies [5ed0baf]
15
+ - Updated dependencies [c3ac738]
16
+ - @react-chess-tools/react-chess-game@0.4.1
17
+
3
18
  ## 0.5.0
4
19
 
5
20
  ### 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.changePuzzle(puzzles[currentPuzzle]!);
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
 
@@ -33,6 +33,9 @@ type ChessPuzzleContextType = {
33
33
  nextMove?: string | null;
34
34
  isPlayerTurn: boolean;
35
35
  onHint: () => void;
36
+ puzzleState: Status;
37
+ movesPlayed: number;
38
+ totalMoves: number;
36
39
  };
37
40
 
38
41
  interface RootProps {
@@ -50,4 +53,4 @@ declare const ChessPuzzle: {
50
53
 
51
54
  declare const useChessPuzzleContext: () => ChessPuzzleContextType;
52
55
 
53
- export { ChessPuzzle, useChessPuzzleContext };
56
+ 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,33 @@ 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 puzzleContext = useMemo(
216
+ () => ({
217
+ status: state.status,
218
+ changePuzzle,
219
+ puzzle,
220
+ hint: state.hint,
221
+ onHint,
222
+ nextMove: state.nextMove,
223
+ isPlayerTurn: state.isPlayerTurn,
224
+ puzzleState: state.status,
225
+ movesPlayed: state.currentMoveIndex,
226
+ totalMoves: puzzle.moves.length
227
+ }),
228
+ [
229
+ state.status,
230
+ changePuzzle,
231
+ puzzle,
232
+ state.hint,
233
+ onHint,
234
+ state.nextMove,
235
+ state.isPlayerTurn,
236
+ state.currentMoveIndex
237
+ ]
238
+ );
215
239
  useEffect(() => {
216
240
  var _a2, _b, _c;
217
241
  if (((_a2 = game == null ? void 0 : game.history()) == null ? void 0 : _a2.length) <= 0 + (puzzle.makeFirstMove ? 1 : 0)) {
@@ -222,8 +246,6 @@ var useChessPuzzle = (puzzle, onSolve, onFail) => {
222
246
  type: "PLAYER_MOVE",
223
247
  payload: {
224
248
  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
249
  puzzleContext,
228
250
  game
229
251
  }
@@ -233,6 +255,18 @@ var useChessPuzzle = (puzzle, onSolve, onFail) => {
233
255
  });
234
256
  }
235
257
  }, [(_a = game == null ? void 0 : game.history()) == null ? void 0 : _a.length]);
258
+ useEffect(() => {
259
+ if (state.status === "solved" && !state.onSolveInvoked && onSolve) {
260
+ onSolve(puzzleContext);
261
+ dispatch({ type: "MARK_SOLVE_INVOKED" });
262
+ }
263
+ }, [state.status, state.onSolveInvoked]);
264
+ useEffect(() => {
265
+ if (state.status === "failed" && !state.onFailInvoked && onFail) {
266
+ onFail(puzzleContext);
267
+ dispatch({ type: "MARK_FAIL_INVOKED" });
268
+ }
269
+ }, [state.status, state.onFailInvoked]);
236
270
  return puzzleContext;
237
271
  };
238
272
 
@@ -320,7 +354,7 @@ var Reset = ({
320
354
  const { changePuzzle, status } = puzzleContext;
321
355
  const handleClick = () => {
322
356
  changePuzzle(puzzle || puzzleContext.puzzle);
323
- onReset == null ? void 0 : onReset();
357
+ onReset == null ? void 0 : onReset(puzzleContext);
324
358
  };
325
359
  if (!showOn.includes(status)) {
326
360
  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 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 puzzleContext: ChessPuzzleContextType = useMemo(\n () => ({\n status: state.status,\n changePuzzle,\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 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 \"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, 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.1",
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.1",
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,8 +1,7 @@
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;
@@ -12,6 +11,9 @@ export type ChessPuzzleContextType = {
12
11
  nextMove?: string | null;
13
12
  isPlayerTurn: boolean;
14
13
  onHint: () => void;
14
+ puzzleState: Status;
15
+ movesPlayed: number;
16
+ totalMoves: number;
15
17
  };
16
18
 
17
19
  export const useChessPuzzle = (
@@ -28,16 +30,17 @@ export const useChessPuzzle = (
28
30
  methods: { makeMove, setPosition },
29
31
  } = gameContext;
30
32
 
31
- useEffect(() => {
32
- if (gameContext && game.fen() !== puzzle.fen) {
33
+ const changePuzzle = useCallback(
34
+ (puzzle: Puzzle) => {
33
35
  setPosition(puzzle.fen, getOrientation(puzzle));
34
- }
35
- }, []);
36
+ dispatch({ type: "INITIALIZE", payload: { puzzle } });
37
+ },
38
+ [setPosition],
39
+ );
36
40
 
37
- const changePuzzle = (puzzle: Puzzle) => {
38
- dispatch({ type: "INITIALIZE", payload: { puzzle } });
39
- setPosition(puzzle.fen, getOrientation(puzzle));
40
- };
41
+ useEffect(() => {
42
+ changePuzzle(puzzle);
43
+ }, [JSON.stringify(puzzle), changePuzzle]);
41
44
 
42
45
  useEffect(() => {
43
46
  if (gameContext && game.fen() === puzzle.fen && state.needCpuMove) {
@@ -61,19 +64,34 @@ export const useChessPuzzle = (
61
64
  throw new Error("useChessPuzzle must be used within a ChessGameContext");
62
65
  }
63
66
 
64
- const onHint = () => {
67
+ const onHint = useCallback(() => {
65
68
  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
- };
69
+ }, []);
70
+
71
+ const puzzleContext: ChessPuzzleContextType = useMemo(
72
+ () => ({
73
+ status: state.status,
74
+ changePuzzle,
75
+ puzzle,
76
+ hint: state.hint,
77
+ onHint,
78
+ nextMove: state.nextMove,
79
+ isPlayerTurn: state.isPlayerTurn,
80
+ puzzleState: state.status,
81
+ movesPlayed: state.currentMoveIndex,
82
+ totalMoves: puzzle.moves.length,
83
+ }),
84
+ [
85
+ state.status,
86
+ changePuzzle,
87
+ puzzle,
88
+ state.hint,
89
+ onHint,
90
+ state.nextMove,
91
+ state.isPlayerTurn,
92
+ state.currentMoveIndex,
93
+ ],
94
+ );
77
95
 
78
96
  useEffect(() => {
79
97
  if (game?.history()?.length <= 0 + (puzzle.makeFirstMove ? 1 : 0)) {
@@ -84,8 +102,6 @@ export const useChessPuzzle = (
84
102
  type: "PLAYER_MOVE",
85
103
  payload: {
86
104
  move: gameContext?.game?.history({ verbose: true })?.pop() ?? null,
87
- onSolve,
88
- onFail,
89
105
  puzzleContext,
90
106
  game: game,
91
107
  },
@@ -97,5 +113,19 @@ export const useChessPuzzle = (
97
113
  }
98
114
  }, [game?.history()?.length]);
99
115
 
116
+ useEffect(() => {
117
+ if (state.status === "solved" && !state.onSolveInvoked && onSolve) {
118
+ onSolve(puzzleContext);
119
+ dispatch({ type: "MARK_SOLVE_INVOKED" });
120
+ }
121
+ }, [state.status, state.onSolveInvoked]);
122
+
123
+ useEffect(() => {
124
+ if (state.status === "failed" && !state.onFailInvoked && onFail) {
125
+ onFail(puzzleContext);
126
+ dispatch({ type: "MARK_FAIL_INVOKED" });
127
+ }
128
+ }, [state.status, state.onFailInvoked]);
129
+
100
130
  return puzzleContext;
101
131
  };
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
+ });