@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,181 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
2
|
+
import { useChessGameContext } from "../../hooks/useChessGameContext";
|
|
3
|
+
import type { ClockColor } from "@react-chess-tools/react-chess-clock";
|
|
4
|
+
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Server-Controlled Clock Helpers
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
export interface ServerState {
|
|
10
|
+
whiteTime: number;
|
|
11
|
+
blackTime: number;
|
|
12
|
+
activePlayer: ClockColor;
|
|
13
|
+
running: boolean;
|
|
14
|
+
finished: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SimulatedServerReturn {
|
|
18
|
+
serverState: ServerState;
|
|
19
|
+
clientView: ServerState;
|
|
20
|
+
lagMs: number;
|
|
21
|
+
setLagMs: (lag: number) => void;
|
|
22
|
+
serverMove: () => void;
|
|
23
|
+
serverReset: () => void;
|
|
24
|
+
addTime: (player: ClockColor, ms: number) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Simulates a chess server that is authoritative over clock times.
|
|
29
|
+
*
|
|
30
|
+
* Storybook helper - not part of the public API.
|
|
31
|
+
*
|
|
32
|
+
* - The server ticks independently and pushes time updates to the client.
|
|
33
|
+
* - `lagMs` simulates network delay: the client receives stale values, then
|
|
34
|
+
* snaps to the real server value once the "network response" arrives.
|
|
35
|
+
* - `addTime` / `removeTime` let you manipulate server time directly,
|
|
36
|
+
* demonstrating that the client always follows the server.
|
|
37
|
+
*/
|
|
38
|
+
export function useSimulatedServer(
|
|
39
|
+
initialTimeMs: number,
|
|
40
|
+
incrementMs: number,
|
|
41
|
+
): SimulatedServerReturn {
|
|
42
|
+
// True server state — this is the authority
|
|
43
|
+
const serverRef = useRef<ServerState>({
|
|
44
|
+
whiteTime: initialTimeMs,
|
|
45
|
+
blackTime: initialTimeMs,
|
|
46
|
+
activePlayer: "white",
|
|
47
|
+
running: false,
|
|
48
|
+
finished: false,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// What the client sees — may be delayed by simulated lag
|
|
52
|
+
const [clientView, setClientView] = useState<ServerState>(serverRef.current);
|
|
53
|
+
const [lagMs, setLagMs] = useState(0);
|
|
54
|
+
|
|
55
|
+
// Push current server state to the client (with optional lag)
|
|
56
|
+
const pushToClient = useCallback(() => {
|
|
57
|
+
const snapshot = { ...serverRef.current };
|
|
58
|
+
if (lagMs === 0) {
|
|
59
|
+
setClientView(snapshot);
|
|
60
|
+
} else {
|
|
61
|
+
setTimeout(() => setClientView(snapshot), lagMs);
|
|
62
|
+
}
|
|
63
|
+
}, [lagMs]);
|
|
64
|
+
|
|
65
|
+
// Server tick — decrements the active player's time every 100ms
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (!serverRef.current.running || serverRef.current.finished) return;
|
|
68
|
+
|
|
69
|
+
const id = setInterval(() => {
|
|
70
|
+
const s = serverRef.current;
|
|
71
|
+
if (!s.running || s.finished) return;
|
|
72
|
+
|
|
73
|
+
const key = s.activePlayer === "white" ? "whiteTime" : "blackTime";
|
|
74
|
+
const newTime = Math.max(0, s[key] - 100);
|
|
75
|
+
serverRef.current = {
|
|
76
|
+
...s,
|
|
77
|
+
[key]: newTime,
|
|
78
|
+
running: newTime > 0,
|
|
79
|
+
finished: newTime === 0,
|
|
80
|
+
};
|
|
81
|
+
pushToClient();
|
|
82
|
+
}, 100);
|
|
83
|
+
|
|
84
|
+
return () => clearInterval(id);
|
|
85
|
+
}, [clientView.running, clientView.finished, pushToClient]);
|
|
86
|
+
|
|
87
|
+
// Server receives a move — switches active player, applies increment
|
|
88
|
+
const serverMove = useCallback(() => {
|
|
89
|
+
const s = serverRef.current;
|
|
90
|
+
if (s.finished) return;
|
|
91
|
+
const key = s.activePlayer === "white" ? "whiteTime" : "blackTime";
|
|
92
|
+
serverRef.current = {
|
|
93
|
+
...s,
|
|
94
|
+
[key]: s[key] + incrementMs,
|
|
95
|
+
activePlayer: s.activePlayer === "white" ? "black" : "white",
|
|
96
|
+
running: true,
|
|
97
|
+
};
|
|
98
|
+
pushToClient();
|
|
99
|
+
}, [incrementMs, pushToClient]);
|
|
100
|
+
|
|
101
|
+
// Direct server-side time manipulation
|
|
102
|
+
const addTime = useCallback(
|
|
103
|
+
(player: ClockColor, ms: number) => {
|
|
104
|
+
const s = serverRef.current;
|
|
105
|
+
const key = player === "white" ? "whiteTime" : "blackTime";
|
|
106
|
+
serverRef.current = { ...s, [key]: Math.max(0, s[key] + ms) };
|
|
107
|
+
pushToClient();
|
|
108
|
+
},
|
|
109
|
+
[pushToClient],
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const serverReset = useCallback(() => {
|
|
113
|
+
serverRef.current = {
|
|
114
|
+
whiteTime: initialTimeMs,
|
|
115
|
+
blackTime: initialTimeMs,
|
|
116
|
+
activePlayer: "white",
|
|
117
|
+
running: false,
|
|
118
|
+
finished: false,
|
|
119
|
+
};
|
|
120
|
+
pushToClient();
|
|
121
|
+
}, [initialTimeMs, pushToClient]);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
serverState: serverRef.current,
|
|
125
|
+
clientView,
|
|
126
|
+
lagMs,
|
|
127
|
+
setLagMs,
|
|
128
|
+
serverMove,
|
|
129
|
+
serverReset,
|
|
130
|
+
addTime,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Child component that watches for new moves and notifies the simulated server.
|
|
136
|
+
*
|
|
137
|
+
* Storybook helper - not part of the public API.
|
|
138
|
+
*
|
|
139
|
+
* Must be rendered inside ChessGame.Root to access game context.
|
|
140
|
+
*/
|
|
141
|
+
export function ServerMoveDetector({ onMove }: { onMove: () => void }) {
|
|
142
|
+
const { game } = useChessGameContext();
|
|
143
|
+
const moveCount = game.history().length;
|
|
144
|
+
const prevMoveCount = useRef(moveCount);
|
|
145
|
+
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
if (moveCount > prevMoveCount.current) {
|
|
148
|
+
onMove();
|
|
149
|
+
}
|
|
150
|
+
prevMoveCount.current = moveCount;
|
|
151
|
+
}, [moveCount, onMove]);
|
|
152
|
+
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Child component that syncs server times into the clock via setTime().
|
|
158
|
+
*
|
|
159
|
+
* Storybook helper - not part of the public API.
|
|
160
|
+
*/
|
|
161
|
+
export function ServerTimeSync({
|
|
162
|
+
serverTimes,
|
|
163
|
+
}: {
|
|
164
|
+
serverTimes: { white: number; black: number };
|
|
165
|
+
}) {
|
|
166
|
+
const { clock } = useChessGameContext();
|
|
167
|
+
const prevTimes = useRef(serverTimes);
|
|
168
|
+
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
if (!clock) return;
|
|
171
|
+
if (serverTimes.white !== prevTimes.current.white) {
|
|
172
|
+
clock.methods.setTime("white", serverTimes.white);
|
|
173
|
+
}
|
|
174
|
+
if (serverTimes.black !== prevTimes.current.black) {
|
|
175
|
+
clock.methods.setTime("black", serverTimes.black);
|
|
176
|
+
}
|
|
177
|
+
prevTimes.current = serverTimes;
|
|
178
|
+
}, [serverTimes, clock]);
|
|
179
|
+
|
|
180
|
+
return null;
|
|
181
|
+
}
|