@react-chess-tools/react-chess-puzzle 0.4.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 +21 -0
- package/README.MD +248 -34
- package/dist/index.d.mts +18 -13
- package/dist/index.mjs +73 -38
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/components/ChessPuzzle/parts/Reset.tsx +3 -3
- package/src/components/ChessPuzzle/parts/Root.tsx +6 -3
- package/src/hooks/__tests__/reducer.test.ts +274 -0
- package/src/hooks/reducer.ts +24 -12
- package/src/hooks/useChessPuzzle.ts +75 -32
- package/src/index.ts +13 -0
- package/src/utils/__tests__/index.test.ts +192 -0
|
@@ -1,13 +1,26 @@
|
|
|
1
|
-
import { useEffect, useReducer } from "react";
|
|
1
|
+
import { useEffect, useReducer, useCallback, useMemo } from "react";
|
|
2
2
|
import { initializePuzzle, reducer } from "./reducer";
|
|
3
|
-
import { getOrientation, type Puzzle } from "../utils";
|
|
3
|
+
import { getOrientation, type Puzzle, type Hint, type Status } from "../utils";
|
|
4
4
|
import { useChessGameContext } from "@react-chess-tools/react-chess-game";
|
|
5
5
|
|
|
6
|
+
export type ChessPuzzleContextType = {
|
|
7
|
+
status: Status;
|
|
8
|
+
changePuzzle: (puzzle: Puzzle) => void;
|
|
9
|
+
puzzle: Puzzle;
|
|
10
|
+
hint: Hint;
|
|
11
|
+
nextMove?: string | null;
|
|
12
|
+
isPlayerTurn: boolean;
|
|
13
|
+
onHint: () => void;
|
|
14
|
+
puzzleState: Status;
|
|
15
|
+
movesPlayed: number;
|
|
16
|
+
totalMoves: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
6
19
|
export const useChessPuzzle = (
|
|
7
20
|
puzzle: Puzzle,
|
|
8
|
-
onSolve?: (
|
|
9
|
-
onFail?: (
|
|
10
|
-
) => {
|
|
21
|
+
onSolve?: (puzzleContext: ChessPuzzleContextType) => void,
|
|
22
|
+
onFail?: (puzzleContext: ChessPuzzleContextType) => void,
|
|
23
|
+
): ChessPuzzleContextType => {
|
|
11
24
|
const gameContext = useChessGameContext();
|
|
12
25
|
|
|
13
26
|
const [state, dispatch] = useReducer(reducer, { puzzle }, initializePuzzle);
|
|
@@ -17,16 +30,17 @@ export const useChessPuzzle = (
|
|
|
17
30
|
methods: { makeMove, setPosition },
|
|
18
31
|
} = gameContext;
|
|
19
32
|
|
|
20
|
-
|
|
21
|
-
|
|
33
|
+
const changePuzzle = useCallback(
|
|
34
|
+
(puzzle: Puzzle) => {
|
|
22
35
|
setPosition(puzzle.fen, getOrientation(puzzle));
|
|
23
|
-
|
|
24
|
-
|
|
36
|
+
dispatch({ type: "INITIALIZE", payload: { puzzle } });
|
|
37
|
+
},
|
|
38
|
+
[setPosition],
|
|
39
|
+
);
|
|
25
40
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
};
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
changePuzzle(puzzle);
|
|
43
|
+
}, [JSON.stringify(puzzle), changePuzzle]);
|
|
30
44
|
|
|
31
45
|
useEffect(() => {
|
|
32
46
|
if (gameContext && game.fen() === puzzle.fen && state.needCpuMove) {
|
|
@@ -46,6 +60,39 @@ export const useChessPuzzle = (
|
|
|
46
60
|
}
|
|
47
61
|
}, [state.cpuMove]);
|
|
48
62
|
|
|
63
|
+
if (!gameContext) {
|
|
64
|
+
throw new Error("useChessPuzzle must be used within a ChessGameContext");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const onHint = useCallback(() => {
|
|
68
|
+
dispatch({ type: "TOGGLE_HINT" });
|
|
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
|
+
);
|
|
95
|
+
|
|
49
96
|
useEffect(() => {
|
|
50
97
|
if (game?.history()?.length <= 0 + (puzzle.makeFirstMove ? 1 : 0)) {
|
|
51
98
|
return;
|
|
@@ -55,9 +102,7 @@ export const useChessPuzzle = (
|
|
|
55
102
|
type: "PLAYER_MOVE",
|
|
56
103
|
payload: {
|
|
57
104
|
move: gameContext?.game?.history({ verbose: true })?.pop() ?? null,
|
|
58
|
-
|
|
59
|
-
onFail,
|
|
60
|
-
changePuzzle,
|
|
105
|
+
puzzleContext,
|
|
61
106
|
game: game,
|
|
62
107
|
},
|
|
63
108
|
});
|
|
@@ -68,21 +113,19 @@ export const useChessPuzzle = (
|
|
|
68
113
|
}
|
|
69
114
|
}, [game?.history()?.length]);
|
|
70
115
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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]);
|
|
74
122
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
hint: state.hint,
|
|
84
|
-
onHint,
|
|
85
|
-
nextMove: state.nextMove,
|
|
86
|
-
isPlayerTurn: state.isPlayerTurn,
|
|
87
|
-
};
|
|
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
|
+
|
|
130
|
+
return puzzleContext;
|
|
88
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
|
+
});
|