@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 +15 -0
- package/README.MD +19 -15
- package/dist/index.d.mts +5 -2
- package/dist/index.mjs +67 -33
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/components/ChessPuzzle/parts/Reset.tsx +3 -3
- package/src/hooks/__tests__/reducer.test.ts +274 -0
- package/src/hooks/reducer.ts +22 -11
- package/src/hooks/useChessPuzzle.ts +54 -24
- package/src/index.ts +13 -0
- package/src/utils/__tests__/index.test.ts +192 -0
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: "
|
|
234
|
-
moves: ["
|
|
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: "
|
|
239
|
-
moves: ["
|
|
240
|
-
makeFirstMove:
|
|
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
|
-
|
|
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
|
-
|
|
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(`
|
|
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={
|
|
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
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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;
|
package/dist/index.mjs.map
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
+
});
|
package/src/hooks/reducer.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
+
});
|