@react-chess-tools/react-chess-game 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/coverage/clover.xml +6 -0
- package/coverage/coverage-final.json +1 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +101 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +196 -0
- package/coverage/lcov.info +0 -0
- package/dist/index.d.mts +32 -2
- package/dist/index.mjs +113 -55
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/ChessGame/parts/Sounds.tsx +8 -2
- package/src/hooks/__tests__/useChessGame.test.tsx +230 -0
- package/src/hooks/useBoardSounds.test.ts +196 -0
- package/src/hooks/useBoardSounds.ts +8 -3
- package/src/hooks/useChessGame.ts +92 -41
- package/src/hooks/useChessGameContext.ts +1 -1
- package/src/hooks/useKeyboardControls.test.tsx +171 -0
- package/src/index.ts +19 -0
- package/src/utils/__tests__/board.test.ts +143 -0
- package/src/utils/__tests__/chess.test.ts +198 -0
- package/src/utils/board.ts +2 -2
- package/src/utils/chess.ts +12 -8
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { Chess } from "chess.js";
|
|
2
|
+
import { getCustomSquareStyles } from "../board";
|
|
3
|
+
import { getGameInfo } from "../chess";
|
|
4
|
+
|
|
5
|
+
describe("Board Utilities", () => {
|
|
6
|
+
describe("getCustomSquareStyles", () => {
|
|
7
|
+
it("should return empty styles for initial position with no active square", () => {
|
|
8
|
+
const game = new Chess();
|
|
9
|
+
const orientation = "w";
|
|
10
|
+
const info = getGameInfo(game, orientation);
|
|
11
|
+
const activeSquare = null;
|
|
12
|
+
|
|
13
|
+
const styles = getCustomSquareStyles(game, info, activeSquare);
|
|
14
|
+
|
|
15
|
+
expect(Object.keys(styles).length).toBe(0);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should highlight last move squares", () => {
|
|
19
|
+
const game = new Chess();
|
|
20
|
+
game.move("e4");
|
|
21
|
+
game.move("e5");
|
|
22
|
+
const orientation = "w";
|
|
23
|
+
const info = getGameInfo(game, orientation);
|
|
24
|
+
const activeSquare = null;
|
|
25
|
+
|
|
26
|
+
const styles = getCustomSquareStyles(game, info, activeSquare);
|
|
27
|
+
|
|
28
|
+
expect(styles).toHaveProperty("e7");
|
|
29
|
+
expect(styles).toHaveProperty("e5");
|
|
30
|
+
expect(styles.e7).toHaveProperty(
|
|
31
|
+
"backgroundColor",
|
|
32
|
+
"rgba(255, 255, 0, 0.5)",
|
|
33
|
+
);
|
|
34
|
+
expect(styles.e5).toHaveProperty(
|
|
35
|
+
"backgroundColor",
|
|
36
|
+
"rgba(255, 255, 0, 0.5)",
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should highlight active square", () => {
|
|
41
|
+
const game = new Chess();
|
|
42
|
+
const orientation = "w";
|
|
43
|
+
const info = getGameInfo(game, orientation);
|
|
44
|
+
const activeSquare = "e2";
|
|
45
|
+
|
|
46
|
+
const styles = getCustomSquareStyles(game, info, activeSquare);
|
|
47
|
+
|
|
48
|
+
expect(styles).toHaveProperty("e2");
|
|
49
|
+
expect(styles.e2).toHaveProperty(
|
|
50
|
+
"backgroundColor",
|
|
51
|
+
"rgba(255, 255, 0, 0.5)",
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should highlight destination squares for active square", () => {
|
|
56
|
+
const game = new Chess();
|
|
57
|
+
const orientation = "w";
|
|
58
|
+
const info = getGameInfo(game, orientation);
|
|
59
|
+
const activeSquare = "e2";
|
|
60
|
+
|
|
61
|
+
const styles = getCustomSquareStyles(game, info, activeSquare);
|
|
62
|
+
|
|
63
|
+
expect(styles).toHaveProperty("e3");
|
|
64
|
+
expect(styles).toHaveProperty("e4");
|
|
65
|
+
expect(styles.e3).toHaveProperty("background");
|
|
66
|
+
expect(styles.e4).toHaveProperty("background");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should highlight destination squares with captures differently", () => {
|
|
70
|
+
const game = new Chess(
|
|
71
|
+
"rnbqkbnr/ppp1pppp/8/3p4/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2",
|
|
72
|
+
);
|
|
73
|
+
const orientation = "w";
|
|
74
|
+
const info = getGameInfo(game, orientation);
|
|
75
|
+
const activeSquare = "e4";
|
|
76
|
+
|
|
77
|
+
const styles = getCustomSquareStyles(game, info, activeSquare);
|
|
78
|
+
|
|
79
|
+
expect(styles).toHaveProperty("d5");
|
|
80
|
+
expect(styles.d5.background).toContain("radial-gradient");
|
|
81
|
+
expect(styles.d5.background).toContain("85%");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("should highlight king in check", () => {
|
|
85
|
+
// Set up a position where the king is in check
|
|
86
|
+
const game = new Chess(
|
|
87
|
+
"rnbqkbnr/ppp2ppp/8/3pp3/4P3/5Q2/PPPP1PPP/RNB1KBNR b KQkq - 1 3",
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// We need to manually set the isCheck flag in the info object
|
|
91
|
+
const orientation = "b";
|
|
92
|
+
const info = getGameInfo(game, orientation);
|
|
93
|
+
const modifiedInfo = { ...info, isCheck: true };
|
|
94
|
+
const activeSquare = null;
|
|
95
|
+
|
|
96
|
+
const styles = getCustomSquareStyles(game, modifiedInfo, activeSquare);
|
|
97
|
+
|
|
98
|
+
// Black king on e8 should be highlighted as in check
|
|
99
|
+
expect(styles).toHaveProperty("e8");
|
|
100
|
+
expect(styles.e8).toHaveProperty(
|
|
101
|
+
"backgroundColor",
|
|
102
|
+
"rgba(255, 0, 0, 0.5)",
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should combine multiple style effects", () => {
|
|
107
|
+
const game = new Chess(
|
|
108
|
+
"rnbqkbnr/ppp2ppp/8/3pp3/4P3/5Q2/PPPP1PPP/RNB1KBNR b KQkq - 1 3",
|
|
109
|
+
);
|
|
110
|
+
game.move("Ke7");
|
|
111
|
+
const orientation = "w";
|
|
112
|
+
const info = getGameInfo(game, orientation);
|
|
113
|
+
const activeSquare = "f3";
|
|
114
|
+
|
|
115
|
+
const styles = getCustomSquareStyles(game, info, activeSquare);
|
|
116
|
+
|
|
117
|
+
expect(styles).toHaveProperty("f3");
|
|
118
|
+
expect(styles.f3).toHaveProperty(
|
|
119
|
+
"backgroundColor",
|
|
120
|
+
"rgba(255, 255, 0, 0.5)",
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
expect(styles).toHaveProperty("e8");
|
|
124
|
+
expect(styles).toHaveProperty("e7");
|
|
125
|
+
|
|
126
|
+
expect(Object.keys(styles).length).toBeGreaterThan(3);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("should highlight empty destination squares differently from capture squares", () => {
|
|
130
|
+
const game = new Chess();
|
|
131
|
+
const orientation = "w";
|
|
132
|
+
const info = getGameInfo(game, orientation);
|
|
133
|
+
const activeSquare = "b1";
|
|
134
|
+
|
|
135
|
+
const styles = getCustomSquareStyles(game, info, activeSquare);
|
|
136
|
+
|
|
137
|
+
expect(styles).toHaveProperty("a3");
|
|
138
|
+
expect(styles).toHaveProperty("c3");
|
|
139
|
+
expect(styles.a3.background).toContain("25%");
|
|
140
|
+
expect(styles.c3.background).toContain("25%");
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { Chess, Color } from "chess.js";
|
|
2
|
+
import {
|
|
3
|
+
cloneGame,
|
|
4
|
+
getGameInfo,
|
|
5
|
+
isLegalMove,
|
|
6
|
+
requiresPromotion,
|
|
7
|
+
getDestinationSquares,
|
|
8
|
+
getCurrentFen,
|
|
9
|
+
} from "../chess";
|
|
10
|
+
|
|
11
|
+
describe("Chess Utilities", () => {
|
|
12
|
+
describe("cloneGame", () => {
|
|
13
|
+
it("should create a clone with the same state", () => {
|
|
14
|
+
const game = new Chess();
|
|
15
|
+
game.move("e4");
|
|
16
|
+
game.move("e5");
|
|
17
|
+
|
|
18
|
+
const clone = cloneGame(game);
|
|
19
|
+
|
|
20
|
+
expect(clone.fen()).toEqual(game.fen());
|
|
21
|
+
expect(clone.history()).toEqual(game.history());
|
|
22
|
+
expect(clone).not.toBe(game); // Different instances
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("getGameInfo", () => {
|
|
27
|
+
it("should return correct game info for initial position", () => {
|
|
28
|
+
const game = new Chess();
|
|
29
|
+
const orientation: Color = "w";
|
|
30
|
+
|
|
31
|
+
const info = getGameInfo(game, orientation);
|
|
32
|
+
|
|
33
|
+
expect(info.turn).toBe("w");
|
|
34
|
+
expect(info.isPlayerTurn).toBe(true);
|
|
35
|
+
expect(info.isOpponentTurn).toBe(false);
|
|
36
|
+
expect(info.moveNumber).toBe(0);
|
|
37
|
+
expect(info.lastMove).toBeUndefined();
|
|
38
|
+
expect(info.isCheck).toBe(false);
|
|
39
|
+
expect(info.isCheckmate).toBe(false);
|
|
40
|
+
expect(info.isDraw).toBe(false);
|
|
41
|
+
expect(info.isGameOver).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should return correct game info after moves", () => {
|
|
45
|
+
const game = new Chess();
|
|
46
|
+
game.move("e4");
|
|
47
|
+
game.move("e5");
|
|
48
|
+
const orientation: Color = "w";
|
|
49
|
+
|
|
50
|
+
const info = getGameInfo(game, orientation);
|
|
51
|
+
|
|
52
|
+
expect(info.turn).toBe("w");
|
|
53
|
+
expect(info.isPlayerTurn).toBe(true);
|
|
54
|
+
expect(info.isOpponentTurn).toBe(false);
|
|
55
|
+
expect(info.moveNumber).toBe(2);
|
|
56
|
+
expect(info.lastMove).toEqual(
|
|
57
|
+
expect.objectContaining({
|
|
58
|
+
from: "e7",
|
|
59
|
+
to: "e5",
|
|
60
|
+
piece: "p",
|
|
61
|
+
color: "b",
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should return correct game info for checkmate position", () => {
|
|
67
|
+
const game = new Chess();
|
|
68
|
+
// Scholar's mate
|
|
69
|
+
game.move("e4");
|
|
70
|
+
game.move("e5");
|
|
71
|
+
game.move("Qh5");
|
|
72
|
+
game.move("Nc6");
|
|
73
|
+
game.move("Bc4");
|
|
74
|
+
game.move("Nf6");
|
|
75
|
+
game.move("Qxf7");
|
|
76
|
+
|
|
77
|
+
const whiteInfo = getGameInfo(game, "w");
|
|
78
|
+
expect(whiteInfo.isCheckmate).toBe(true);
|
|
79
|
+
expect(whiteInfo.isGameOver).toBe(true);
|
|
80
|
+
expect(whiteInfo.hasPlayerWon).toBe(true);
|
|
81
|
+
expect(whiteInfo.hasPlayerLost).toBe(false);
|
|
82
|
+
|
|
83
|
+
const blackInfo = getGameInfo(game, "b");
|
|
84
|
+
expect(blackInfo.hasPlayerWon).toBe(false);
|
|
85
|
+
expect(blackInfo.hasPlayerLost).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("isLegalMove", () => {
|
|
90
|
+
it("should return true for legal moves", () => {
|
|
91
|
+
const game = new Chess();
|
|
92
|
+
|
|
93
|
+
expect(isLegalMove(game, "e2e4")).toBe(true);
|
|
94
|
+
expect(isLegalMove(game, "g1f3")).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("should return false for illegal moves", () => {
|
|
98
|
+
const game = new Chess();
|
|
99
|
+
|
|
100
|
+
expect(isLegalMove(game, "e2e5")).toBe(false);
|
|
101
|
+
expect(isLegalMove(game, "e7e5")).toBe(false); // Black's move, but white to move
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("requiresPromotion", () => {
|
|
106
|
+
it("should return true for moves requiring promotion", () => {
|
|
107
|
+
const game = new Chess("8/P7/7k/8/8/8/8/7K w - - 0 1");
|
|
108
|
+
|
|
109
|
+
expect(requiresPromotion(game, "a7a8")).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should return false for moves not requiring promotion", () => {
|
|
113
|
+
const game = new Chess();
|
|
114
|
+
|
|
115
|
+
expect(requiresPromotion(game, "e2e4")).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("should return false for illegal moves", () => {
|
|
119
|
+
const game = new Chess();
|
|
120
|
+
|
|
121
|
+
expect(requiresPromotion(game, "e2e5")).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("throws if game is not passed", () => {
|
|
125
|
+
expect(() =>
|
|
126
|
+
requiresPromotion(null as unknown as Chess, "e2e4"),
|
|
127
|
+
).toThrow();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("getDestinationSquares", () => {
|
|
132
|
+
it("should return correct destination squares for a piece", () => {
|
|
133
|
+
const game = new Chess();
|
|
134
|
+
|
|
135
|
+
const e2Destinations = getDestinationSquares(game, "e2");
|
|
136
|
+
expect(e2Destinations).toContain("e3");
|
|
137
|
+
expect(e2Destinations).toContain("e4");
|
|
138
|
+
expect(e2Destinations.length).toBe(2);
|
|
139
|
+
|
|
140
|
+
const g1Destinations = getDestinationSquares(game, "g1");
|
|
141
|
+
expect(g1Destinations).toContain("f3");
|
|
142
|
+
expect(g1Destinations).toContain("h3");
|
|
143
|
+
expect(g1Destinations.length).toBe(2);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("should return empty array for squares with no legal moves", () => {
|
|
147
|
+
const game = new Chess();
|
|
148
|
+
|
|
149
|
+
expect(getDestinationSquares(game, "e3")).toEqual([]);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("getCurrentFen", () => {
|
|
154
|
+
it("should return the initial position when moveIndex is -1", () => {
|
|
155
|
+
const game = new Chess();
|
|
156
|
+
game.move("e4");
|
|
157
|
+
|
|
158
|
+
const initialFen =
|
|
159
|
+
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
|
|
160
|
+
expect(getCurrentFen(initialFen, game, -1)).toBe(initialFen);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("should return the FEN after specific moves", () => {
|
|
164
|
+
const game = new Chess();
|
|
165
|
+
const initialFen = game.fen();
|
|
166
|
+
|
|
167
|
+
game.move("e4");
|
|
168
|
+
game.move("e5");
|
|
169
|
+
game.move("Nf3");
|
|
170
|
+
|
|
171
|
+
// After 1 move (e4)
|
|
172
|
+
const fenAfterOneMove = getCurrentFen(initialFen, game, 0);
|
|
173
|
+
expect(fenAfterOneMove).toMatch(
|
|
174
|
+
/rnbqkbnr\/pppppppp\/8\/8\/4P3\/8\/PPPP1PPP\/RNBQKBNR b KQkq/,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// After 2 moves (e4, e5)
|
|
178
|
+
const fenAfterTwoMoves = getCurrentFen(initialFen, game, 1);
|
|
179
|
+
expect(fenAfterTwoMoves).toMatch(
|
|
180
|
+
/rnbqkbnr\/pppp1ppp\/8\/4p3\/4P3\/8\/PPPP1PPP\/RNBQKBNR w KQkq/,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// After 3 moves (e4, e5, Nf3)
|
|
184
|
+
const fenAfterThreeMoves = getCurrentFen(initialFen, game, 2);
|
|
185
|
+
expect(fenAfterThreeMoves).toMatch(
|
|
186
|
+
/rnbqkbnr\/pppp1ppp\/8\/4p3\/4P3\/5N2\/PPPP1PPP\/RNBQKB1R b KQkq/,
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("should use provided FEN as starting position", () => {
|
|
191
|
+
const customFen =
|
|
192
|
+
"1r2r1k1/pp3pbp/1qp3p1/2B5/2BP2b1/Q1n2N2/P4PPP/3R1K1R b - - 0 17";
|
|
193
|
+
const game = new Chess();
|
|
194
|
+
|
|
195
|
+
expect(getCurrentFen(customFen, game, -1)).toBe(customFen);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
});
|
package/src/utils/board.ts
CHANGED
|
@@ -34,8 +34,8 @@ export const getCustomSquareStyles = (
|
|
|
34
34
|
destinationSquares.forEach((square) => {
|
|
35
35
|
customSquareStyles[square] = {
|
|
36
36
|
background:
|
|
37
|
-
game.get(square) && game.get(square)
|
|
38
|
-
? "radial-gradient(circle, rgba(0,0,0
|
|
37
|
+
game.get(square) && game.get(square)?.color !== turn
|
|
38
|
+
? "radial-gradient(circle, rgba(1, 0, 0, 0.1) 85%, transparent 85%)"
|
|
39
39
|
: "radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)",
|
|
40
40
|
};
|
|
41
41
|
});
|
package/src/utils/chess.ts
CHANGED
|
@@ -33,8 +33,8 @@ export const getGameInfo = (game: Chess, orientation: Color) => {
|
|
|
33
33
|
const isThreefoldRepetition = game.isThreefoldRepetition();
|
|
34
34
|
const isInsufficientMaterial = game.isInsufficientMaterial();
|
|
35
35
|
const isGameOver = game.isGameOver();
|
|
36
|
-
const hasPlayerWon =
|
|
37
|
-
const hasPlayerLost =
|
|
36
|
+
const hasPlayerWon = isOpponentTurn && isGameOver && !isDraw;
|
|
37
|
+
const hasPlayerLost = isPlayerTurn && isGameOver && !isDraw;
|
|
38
38
|
const isDrawn = game.isDraw();
|
|
39
39
|
return {
|
|
40
40
|
turn,
|
|
@@ -74,14 +74,17 @@ export const requiresPromotion = (
|
|
|
74
74
|
game: Chess,
|
|
75
75
|
move: Parameters<Chess["move"]>[0],
|
|
76
76
|
) => {
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
try {
|
|
78
|
+
const copy = cloneGame(game);
|
|
79
|
+
const result = copy.move(move);
|
|
79
80
|
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
return result.flags.indexOf("p") !== -1;
|
|
82
|
+
} catch (e) {
|
|
83
|
+
if (e instanceof Error && e.message.includes("Invalid move")) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
throw e;
|
|
82
87
|
}
|
|
83
|
-
|
|
84
|
-
return result.flags.indexOf("p") !== -1;
|
|
85
88
|
};
|
|
86
89
|
|
|
87
90
|
export const getDestinationSquares = (game: Chess, square: Square) => {
|
|
@@ -101,6 +104,7 @@ export const getCurrentFen = (
|
|
|
101
104
|
}
|
|
102
105
|
} else {
|
|
103
106
|
const moves = game.history().slice(0, currentMoveIndex + 1);
|
|
107
|
+
|
|
104
108
|
if (fen) {
|
|
105
109
|
tempGame.load(fen);
|
|
106
110
|
}
|