@react-chess-tools/react-chess-game 1.0.1 → 1.0.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 +8 -0
- package/README.md +188 -18
- package/dist/index.cjs +85 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +31 -1
- package/dist/index.d.ts +31 -1
- package/dist/index.js +91 -15
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/components/ChessGame/ChessGame.stories.helpers.tsx +181 -0
- package/src/components/ChessGame/ChessGame.stories.tsx +574 -35
- package/src/components/ChessGame/Clock/index.tsx +174 -0
- package/src/components/ChessGame/index.ts +2 -0
- package/src/components/ChessGame/parts/Root.tsx +13 -1
- package/src/hooks/useChessGame.ts +50 -12
- package/src/index.ts +10 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useChessGameContext } from "../../../hooks/useChessGameContext";
|
|
3
|
+
import {
|
|
4
|
+
Display as ClockDisplay,
|
|
5
|
+
Switch as ClockSwitch,
|
|
6
|
+
PlayPause as ClockPlayPause,
|
|
7
|
+
Reset as ClockReset,
|
|
8
|
+
ChessClockContext,
|
|
9
|
+
} from "@react-chess-tools/react-chess-clock";
|
|
10
|
+
import type {
|
|
11
|
+
ChessClockDisplayProps,
|
|
12
|
+
ChessClockControlProps,
|
|
13
|
+
ChessClockPlayPauseProps,
|
|
14
|
+
ChessClockResetProps,
|
|
15
|
+
ClockColor,
|
|
16
|
+
} from "@react-chess-tools/react-chess-clock";
|
|
17
|
+
|
|
18
|
+
export type {
|
|
19
|
+
ChessClockDisplayProps,
|
|
20
|
+
ChessClockControlProps,
|
|
21
|
+
ChessClockPlayPauseProps,
|
|
22
|
+
ChessClockResetProps,
|
|
23
|
+
ClockColor,
|
|
24
|
+
} from "@react-chess-tools/react-chess-clock";
|
|
25
|
+
|
|
26
|
+
export interface ClockDisplayProps extends Omit<
|
|
27
|
+
ChessClockDisplayProps,
|
|
28
|
+
"color"
|
|
29
|
+
> {
|
|
30
|
+
color: ClockColor;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* ChessGame.Clock.Display - Autonomous display component
|
|
35
|
+
*
|
|
36
|
+
* Wraps react-chess-clock's Display component, providing clock state from ChessGame.Root context.
|
|
37
|
+
* No need to pass timeControl - it's inherited from the root.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```tsx
|
|
41
|
+
* <ChessGame.Root timeControl={{ time: "5+3" }}>
|
|
42
|
+
* <ChessGame.Clock.Display color="white" />
|
|
43
|
+
* <ChessGame.Board />
|
|
44
|
+
* <ChessGame.Clock.Display color="black" />
|
|
45
|
+
* </ChessGame.Root>
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export const Display = React.forwardRef<HTMLDivElement, ClockDisplayProps>(
|
|
49
|
+
({ color, ...rest }, ref) => {
|
|
50
|
+
const { clock } = useChessGameContext();
|
|
51
|
+
|
|
52
|
+
if (!clock) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<ChessClockContext.Provider value={clock}>
|
|
58
|
+
<ClockDisplay ref={ref} color={color} {...rest} />
|
|
59
|
+
</ChessClockContext.Provider>
|
|
60
|
+
);
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
Display.displayName = "ChessGame.Clock.Display";
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* ChessGame.Clock.Switch - Manual switch control
|
|
68
|
+
*
|
|
69
|
+
* Wraps react-chess-clock's Switch component, providing clock state from ChessGame.Root context.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```tsx
|
|
73
|
+
* <ChessGame.Root timeControl={{ time: "5+3" }}>
|
|
74
|
+
* <ChessGame.Clock.Switch>Switch Clock</ChessGame.Clock.Switch>
|
|
75
|
+
* </ChessGame.Root>
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export const Switch = React.forwardRef<
|
|
79
|
+
HTMLElement,
|
|
80
|
+
React.PropsWithChildren<ChessClockControlProps>
|
|
81
|
+
>(({ children, ...rest }, ref) => {
|
|
82
|
+
const { clock } = useChessGameContext();
|
|
83
|
+
|
|
84
|
+
if (!clock) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<ChessClockContext.Provider value={clock}>
|
|
90
|
+
<ClockSwitch ref={ref} {...rest}>
|
|
91
|
+
{children}
|
|
92
|
+
</ClockSwitch>
|
|
93
|
+
</ChessClockContext.Provider>
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
Switch.displayName = "ChessGame.Clock.Switch";
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* ChessGame.Clock.PlayPause - Play/pause control
|
|
101
|
+
*
|
|
102
|
+
* Wraps react-chess-clock's PlayPause component, providing clock state from ChessGame.Root context.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```tsx
|
|
106
|
+
* <ChessGame.Root timeControl={{ time: "5+3" }}>
|
|
107
|
+
* <ChessGame.Clock.PlayPause
|
|
108
|
+
* startContent="Start"
|
|
109
|
+
* pauseContent="Pause"
|
|
110
|
+
* resumeContent="Resume"
|
|
111
|
+
* />
|
|
112
|
+
* </ChessGame.Root>
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
export const PlayPause = React.forwardRef<
|
|
116
|
+
HTMLElement,
|
|
117
|
+
React.PropsWithChildren<ChessClockPlayPauseProps>
|
|
118
|
+
>(({ children, ...rest }, ref) => {
|
|
119
|
+
const { clock } = useChessGameContext();
|
|
120
|
+
|
|
121
|
+
if (!clock) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<ChessClockContext.Provider value={clock}>
|
|
127
|
+
<ClockPlayPause ref={ref} {...rest}>
|
|
128
|
+
{children}
|
|
129
|
+
</ClockPlayPause>
|
|
130
|
+
</ChessClockContext.Provider>
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
PlayPause.displayName = "ChessGame.Clock.PlayPause";
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* ChessGame.Clock.Reset - Reset control
|
|
138
|
+
*
|
|
139
|
+
* Wraps react-chess-clock's Reset component, providing clock state from ChessGame.Root context.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```tsx
|
|
143
|
+
* <ChessGame.Root timeControl={{ time: "5+3" }}>
|
|
144
|
+
* <ChessGame.Clock.Reset>Reset</ChessGame.Clock.Reset>
|
|
145
|
+
* </ChessGame.Root>
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
export const Reset = React.forwardRef<
|
|
149
|
+
HTMLElement,
|
|
150
|
+
React.PropsWithChildren<ChessClockResetProps>
|
|
151
|
+
>(({ children, ...rest }, ref) => {
|
|
152
|
+
const { clock } = useChessGameContext();
|
|
153
|
+
|
|
154
|
+
if (!clock) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<ChessClockContext.Provider value={clock}>
|
|
160
|
+
<ClockReset ref={ref} {...rest}>
|
|
161
|
+
{children}
|
|
162
|
+
</ClockReset>
|
|
163
|
+
</ChessClockContext.Provider>
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
Reset.displayName = "ChessGame.Clock.Reset";
|
|
168
|
+
|
|
169
|
+
export const Clock = {
|
|
170
|
+
Display,
|
|
171
|
+
Switch,
|
|
172
|
+
PlayPause,
|
|
173
|
+
Reset,
|
|
174
|
+
};
|
|
@@ -2,10 +2,12 @@ import { Root } from "./parts/Root";
|
|
|
2
2
|
import { Board } from "./parts/Board";
|
|
3
3
|
import { Sounds } from "./parts/Sounds";
|
|
4
4
|
import { KeyboardControls } from "./parts/KeyboardControls";
|
|
5
|
+
import { Clock } from "./Clock";
|
|
5
6
|
|
|
6
7
|
export const ChessGame = {
|
|
7
8
|
Root,
|
|
8
9
|
Board,
|
|
9
10
|
Sounds,
|
|
10
11
|
KeyboardControls,
|
|
12
|
+
Clock,
|
|
11
13
|
};
|
|
@@ -5,21 +5,33 @@ import { ChessGameContext } from "../../../hooks/useChessGameContext";
|
|
|
5
5
|
import { ThemeProvider } from "../../../theme/context";
|
|
6
6
|
import { mergeTheme } from "../../../theme/utils";
|
|
7
7
|
import type { PartialChessGameTheme } from "../../../theme/types";
|
|
8
|
+
import type { TimeControlConfig } from "@react-chess-tools/react-chess-clock";
|
|
8
9
|
|
|
9
10
|
export interface RootProps {
|
|
10
11
|
fen?: string;
|
|
11
12
|
orientation?: Color;
|
|
12
13
|
/** Optional theme configuration. Supports partial themes - only override the colors you need. */
|
|
13
14
|
theme?: PartialChessGameTheme;
|
|
15
|
+
/** Optional clock configuration to enable chess clock functionality */
|
|
16
|
+
timeControl?: TimeControlConfig;
|
|
17
|
+
/** Auto-switch clock on move (default: true) */
|
|
18
|
+
autoSwitchOnMove?: boolean;
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
export const Root: React.FC<React.PropsWithChildren<RootProps>> = ({
|
|
17
22
|
fen,
|
|
18
23
|
orientation,
|
|
19
24
|
theme,
|
|
25
|
+
timeControl,
|
|
26
|
+
autoSwitchOnMove,
|
|
20
27
|
children,
|
|
21
28
|
}) => {
|
|
22
|
-
const context = useChessGame({
|
|
29
|
+
const context = useChessGame({
|
|
30
|
+
fen,
|
|
31
|
+
orientation,
|
|
32
|
+
timeControl,
|
|
33
|
+
autoSwitchOnMove,
|
|
34
|
+
});
|
|
23
35
|
|
|
24
36
|
// Merge partial theme with defaults
|
|
25
37
|
const mergedTheme = React.useMemo(() => mergeTheme(theme), [theme]);
|
|
@@ -1,22 +1,31 @@
|
|
|
1
|
-
import React, { useEffect } from "react";
|
|
1
|
+
import React, { useEffect, useRef } from "react";
|
|
2
2
|
import { Chess, Color } from "chess.js";
|
|
3
3
|
import { cloneGame, getCurrentFen, getGameInfo } from "../utils/chess";
|
|
4
|
+
import { useOptionalChessClock } from "@react-chess-tools/react-chess-clock";
|
|
5
|
+
import type { TimeControlConfig } from "@react-chess-tools/react-chess-clock";
|
|
4
6
|
|
|
5
7
|
export type useChessGameProps = {
|
|
6
8
|
fen?: string;
|
|
7
9
|
orientation?: Color;
|
|
10
|
+
/** Optional clock configuration to enable chess clock functionality */
|
|
11
|
+
timeControl?: TimeControlConfig;
|
|
12
|
+
/** Automatically switch the clock after each move (default: true).
|
|
13
|
+
* Set to false to let players manually press the clock, mimicking real-life over-the-board play. */
|
|
14
|
+
autoSwitchOnMove?: boolean;
|
|
8
15
|
};
|
|
9
16
|
|
|
10
17
|
export const useChessGame = ({
|
|
11
18
|
fen,
|
|
12
19
|
orientation: initialOrientation,
|
|
20
|
+
timeControl,
|
|
21
|
+
autoSwitchOnMove = true,
|
|
13
22
|
}: useChessGameProps = {}) => {
|
|
14
23
|
const [game, setGame] = React.useState(() => {
|
|
15
24
|
try {
|
|
16
25
|
return new Chess(fen);
|
|
17
26
|
} catch (e) {
|
|
18
27
|
console.error("Invalid FEN:", fen, e);
|
|
19
|
-
return new Chess();
|
|
28
|
+
return new Chess();
|
|
20
29
|
}
|
|
21
30
|
});
|
|
22
31
|
|
|
@@ -35,10 +44,8 @@ export const useChessGame = ({
|
|
|
35
44
|
const [currentMoveIndex, setCurrentMoveIndex] = React.useState(-1);
|
|
36
45
|
|
|
37
46
|
const history = React.useMemo(() => game.history(), [game]);
|
|
38
|
-
const isLatestMove =
|
|
39
|
-
|
|
40
|
-
[currentMoveIndex, history.length],
|
|
41
|
-
);
|
|
47
|
+
const isLatestMove =
|
|
48
|
+
currentMoveIndex === history.length - 1 || currentMoveIndex === -1;
|
|
42
49
|
|
|
43
50
|
const info = React.useMemo(
|
|
44
51
|
() => getGameInfo(game, orientation),
|
|
@@ -47,13 +54,18 @@ export const useChessGame = ({
|
|
|
47
54
|
|
|
48
55
|
const currentFen = React.useMemo(
|
|
49
56
|
() => getCurrentFen(fen, game, currentMoveIndex),
|
|
50
|
-
[game, currentMoveIndex],
|
|
57
|
+
[fen, game, currentMoveIndex],
|
|
51
58
|
);
|
|
52
59
|
|
|
53
|
-
const currentPosition =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
60
|
+
const currentPosition = game.history()[currentMoveIndex];
|
|
61
|
+
|
|
62
|
+
const clockState = useOptionalChessClock(timeControl);
|
|
63
|
+
|
|
64
|
+
// Keep clockState in a ref to avoid re-creating makeMove on every clock tick.
|
|
65
|
+
// The clock state object is recreated on every render (especially during active
|
|
66
|
+
// ticking), which would defeat the useCallback memoization.
|
|
67
|
+
const clockStateRef = useRef(clockState);
|
|
68
|
+
clockStateRef.current = clockState;
|
|
57
69
|
|
|
58
70
|
const setPosition = React.useCallback((fen: string, orientation: Color) => {
|
|
59
71
|
try {
|
|
@@ -74,17 +86,42 @@ export const useChessGame = ({
|
|
|
74
86
|
return false;
|
|
75
87
|
}
|
|
76
88
|
|
|
89
|
+
// Access clock state via ref to avoid stale closures while keeping
|
|
90
|
+
// the callback stable (not re-created on every clock tick)
|
|
91
|
+
const clock = clockStateRef.current;
|
|
92
|
+
|
|
93
|
+
// Don't allow moves after clock timeout
|
|
94
|
+
if (clock && clock.timeout !== null) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
77
98
|
try {
|
|
78
99
|
const copy = cloneGame(game);
|
|
79
100
|
copy.move(move);
|
|
80
101
|
setGame(copy);
|
|
81
102
|
setCurrentMoveIndex(copy.history().length - 1);
|
|
103
|
+
|
|
104
|
+
// Auto-start clock on first move
|
|
105
|
+
if (clock && clock.status === "idle") {
|
|
106
|
+
clock.methods.start();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Pause clock on game over (checked immediately after move)
|
|
110
|
+
if (clock && clock.status === "running" && copy.isGameOver()) {
|
|
111
|
+
clock.methods.pause();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Auto-switch clock after a move is made if enabled
|
|
115
|
+
if (autoSwitchOnMove && clock && clock.status !== "finished") {
|
|
116
|
+
clock.methods.switch();
|
|
117
|
+
}
|
|
118
|
+
|
|
82
119
|
return true;
|
|
83
120
|
} catch (e) {
|
|
84
121
|
return false;
|
|
85
122
|
}
|
|
86
123
|
},
|
|
87
|
-
[isLatestMove, game],
|
|
124
|
+
[isLatestMove, game, autoSwitchOnMove],
|
|
88
125
|
);
|
|
89
126
|
|
|
90
127
|
const flipBoard = React.useCallback(() => {
|
|
@@ -145,5 +182,6 @@ export const useChessGame = ({
|
|
|
145
182
|
isLatestMove,
|
|
146
183
|
info,
|
|
147
184
|
methods,
|
|
185
|
+
clock: clockState,
|
|
148
186
|
};
|
|
149
187
|
};
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
// Components
|
|
2
2
|
export { ChessGame } from "./components/ChessGame";
|
|
3
3
|
|
|
4
|
+
// Clock - Re-export from react-chess-clock for convenience
|
|
5
|
+
export { ChessClock } from "@react-chess-tools/react-chess-clock";
|
|
6
|
+
export type {
|
|
7
|
+
TimeControlConfig,
|
|
8
|
+
TimeControlInput,
|
|
9
|
+
TimingMethod,
|
|
10
|
+
ClockStartMode,
|
|
11
|
+
UseChessClockReturn,
|
|
12
|
+
} from "@react-chess-tools/react-chess-clock";
|
|
13
|
+
|
|
4
14
|
// Hooks & Context
|
|
5
15
|
export { useChessGameContext } from "./hooks/useChessGameContext";
|
|
6
16
|
export { useChessGame } from "./hooks/useChessGame";
|