@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.
@@ -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
+ }