@react-chess-tools/react-chess-game 1.0.0 → 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 +14 -0
- package/README.md +569 -0
- package/dist/index.cjs +265 -184
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +48 -4
- package/dist/index.d.ts +48 -4
- package/dist/index.js +271 -185
- package/dist/index.js.map +1 -1
- package/package.json +3 -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/Theme.stories.tsx +2 -2
- package/src/components/ChessGame/index.ts +2 -0
- package/src/components/ChessGame/parts/Board.tsx +214 -205
- package/src/components/ChessGame/parts/KeyboardControls.tsx +9 -0
- package/src/components/ChessGame/parts/Root.tsx +15 -1
- package/src/components/ChessGame/parts/Sounds.tsx +9 -0
- package/src/components/ChessGame/parts/__tests__/Board.test.tsx +122 -0
- package/src/components/ChessGame/parts/__tests__/KeyboardControls.test.tsx +34 -0
- package/src/components/ChessGame/parts/__tests__/Root.test.tsx +50 -0
- package/src/components/ChessGame/parts/__tests__/Sounds.test.tsx +22 -0
- package/src/hooks/useChessGame.ts +50 -12
- package/src/index.ts +10 -0
- package/src/theme/__tests__/context.test.tsx +0 -15
- package/README.MD +0 -190
|
@@ -2,8 +2,197 @@ import type { Meta } from "@storybook/react";
|
|
|
2
2
|
|
|
3
3
|
import React from "react";
|
|
4
4
|
import { ChessGame } from "./index";
|
|
5
|
+
import {
|
|
6
|
+
useSimulatedServer,
|
|
7
|
+
ServerMoveDetector,
|
|
8
|
+
ServerTimeSync,
|
|
9
|
+
} from "./ChessGame.stories.helpers";
|
|
5
10
|
|
|
6
|
-
//
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Design Tokens
|
|
13
|
+
// ============================================================================
|
|
14
|
+
const color = {
|
|
15
|
+
bg: "#f5f5f0",
|
|
16
|
+
surface: "#ffffff",
|
|
17
|
+
border: "#e2e0db",
|
|
18
|
+
text: "#2d2d2d",
|
|
19
|
+
textSecondary: "#7a7a72",
|
|
20
|
+
textMuted: "#a5a59c",
|
|
21
|
+
accent: "#5b8a3c",
|
|
22
|
+
accentLight: "#eef4e8",
|
|
23
|
+
dark: "#1c1c1a",
|
|
24
|
+
danger: "#c44",
|
|
25
|
+
dangerLight: "#fef2f2",
|
|
26
|
+
dangerBorder: "#e8b4b4",
|
|
27
|
+
warn: "#b58a1b",
|
|
28
|
+
warnLight: "#fdf8ec",
|
|
29
|
+
warnBorder: "#e0d3a8",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const font = {
|
|
33
|
+
sans: "'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
|
|
34
|
+
mono: "'JetBrains Mono', 'SF Mono', 'Fira Code', monospace",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Shared Styles
|
|
39
|
+
// ============================================================================
|
|
40
|
+
const s = {
|
|
41
|
+
container: {
|
|
42
|
+
display: "flex",
|
|
43
|
+
flexDirection: "column" as const,
|
|
44
|
+
alignItems: "center",
|
|
45
|
+
gap: "16px",
|
|
46
|
+
padding: "24px",
|
|
47
|
+
fontFamily: font.sans,
|
|
48
|
+
maxWidth: "560px",
|
|
49
|
+
margin: "0 auto",
|
|
50
|
+
},
|
|
51
|
+
header: {
|
|
52
|
+
textAlign: "center" as const,
|
|
53
|
+
},
|
|
54
|
+
title: {
|
|
55
|
+
fontSize: "15px",
|
|
56
|
+
fontWeight: 600,
|
|
57
|
+
color: color.text,
|
|
58
|
+
margin: "0 0 4px",
|
|
59
|
+
letterSpacing: "-0.01em",
|
|
60
|
+
},
|
|
61
|
+
subtitle: {
|
|
62
|
+
fontSize: "13px",
|
|
63
|
+
color: color.textSecondary,
|
|
64
|
+
margin: 0,
|
|
65
|
+
lineHeight: 1.4,
|
|
66
|
+
},
|
|
67
|
+
board: {
|
|
68
|
+
borderRadius: "6px",
|
|
69
|
+
overflow: "hidden",
|
|
70
|
+
boxShadow: "0 1px 3px rgba(0,0,0,0.06), 0 0 0 1px rgba(0,0,0,0.04)",
|
|
71
|
+
},
|
|
72
|
+
hint: {
|
|
73
|
+
fontSize: "12px",
|
|
74
|
+
color: color.textMuted,
|
|
75
|
+
textAlign: "center" as const,
|
|
76
|
+
margin: 0,
|
|
77
|
+
lineHeight: 1.5,
|
|
78
|
+
},
|
|
79
|
+
btn: {
|
|
80
|
+
padding: "7px 14px",
|
|
81
|
+
fontSize: "13px",
|
|
82
|
+
fontWeight: 500,
|
|
83
|
+
fontFamily: font.sans,
|
|
84
|
+
cursor: "pointer",
|
|
85
|
+
border: `1px solid ${color.border}`,
|
|
86
|
+
borderRadius: "4px",
|
|
87
|
+
backgroundColor: color.surface,
|
|
88
|
+
color: color.text,
|
|
89
|
+
} as React.CSSProperties,
|
|
90
|
+
btnPrimary: {
|
|
91
|
+
backgroundColor: color.accent,
|
|
92
|
+
borderColor: color.accent,
|
|
93
|
+
color: "#fff",
|
|
94
|
+
} as React.CSSProperties,
|
|
95
|
+
controls: {
|
|
96
|
+
display: "flex",
|
|
97
|
+
gap: "8px",
|
|
98
|
+
justifyContent: "center",
|
|
99
|
+
flexWrap: "wrap" as const,
|
|
100
|
+
},
|
|
101
|
+
divider: {
|
|
102
|
+
width: "100%",
|
|
103
|
+
height: "1px",
|
|
104
|
+
backgroundColor: color.border,
|
|
105
|
+
margin: "4px 0",
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// ============================================================================
|
|
110
|
+
// Clock Styles
|
|
111
|
+
// ============================================================================
|
|
112
|
+
const clock = {
|
|
113
|
+
row: {
|
|
114
|
+
display: "flex",
|
|
115
|
+
gap: "12px",
|
|
116
|
+
justifyContent: "center",
|
|
117
|
+
alignItems: "center",
|
|
118
|
+
},
|
|
119
|
+
cell: {
|
|
120
|
+
display: "flex",
|
|
121
|
+
flexDirection: "column" as const,
|
|
122
|
+
alignItems: "center",
|
|
123
|
+
gap: "4px",
|
|
124
|
+
},
|
|
125
|
+
label: {
|
|
126
|
+
fontSize: "11px",
|
|
127
|
+
fontWeight: 600,
|
|
128
|
+
color: color.textMuted,
|
|
129
|
+
textTransform: "uppercase" as const,
|
|
130
|
+
letterSpacing: "0.06em",
|
|
131
|
+
},
|
|
132
|
+
white: {
|
|
133
|
+
padding: "10px 20px",
|
|
134
|
+
fontSize: "24px",
|
|
135
|
+
fontWeight: 600,
|
|
136
|
+
fontFamily: font.mono,
|
|
137
|
+
borderRadius: "5px",
|
|
138
|
+
textAlign: "center" as const,
|
|
139
|
+
minWidth: "100px",
|
|
140
|
+
backgroundColor: color.surface,
|
|
141
|
+
border: `2px solid ${color.text}`,
|
|
142
|
+
color: color.text,
|
|
143
|
+
},
|
|
144
|
+
black: {
|
|
145
|
+
padding: "10px 20px",
|
|
146
|
+
fontSize: "24px",
|
|
147
|
+
fontWeight: 600,
|
|
148
|
+
fontFamily: font.mono,
|
|
149
|
+
borderRadius: "5px",
|
|
150
|
+
textAlign: "center" as const,
|
|
151
|
+
minWidth: "100px",
|
|
152
|
+
backgroundColor: color.dark,
|
|
153
|
+
border: `2px solid ${color.dark}`,
|
|
154
|
+
color: "#f0f0ec",
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// ============================================================================
|
|
159
|
+
// Keyboard Hint Styles
|
|
160
|
+
// ============================================================================
|
|
161
|
+
const kbd = {
|
|
162
|
+
grid: {
|
|
163
|
+
display: "flex",
|
|
164
|
+
gap: "6px",
|
|
165
|
+
justifyContent: "center",
|
|
166
|
+
flexWrap: "wrap" as const,
|
|
167
|
+
},
|
|
168
|
+
item: {
|
|
169
|
+
display: "inline-flex",
|
|
170
|
+
alignItems: "center",
|
|
171
|
+
gap: "4px",
|
|
172
|
+
fontSize: "11px",
|
|
173
|
+
color: color.textSecondary,
|
|
174
|
+
},
|
|
175
|
+
key: {
|
|
176
|
+
display: "inline-flex",
|
|
177
|
+
alignItems: "center",
|
|
178
|
+
justifyContent: "center",
|
|
179
|
+
minWidth: "22px",
|
|
180
|
+
height: "22px",
|
|
181
|
+
padding: "0 5px",
|
|
182
|
+
backgroundColor: color.bg,
|
|
183
|
+
border: `1px solid ${color.border}`,
|
|
184
|
+
borderRadius: "3px",
|
|
185
|
+
fontFamily: font.mono,
|
|
186
|
+
fontSize: "11px",
|
|
187
|
+
fontWeight: 600,
|
|
188
|
+
color: color.text,
|
|
189
|
+
lineHeight: 1,
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// ============================================================================
|
|
194
|
+
// Meta
|
|
195
|
+
// ============================================================================
|
|
7
196
|
const meta = {
|
|
8
197
|
title: "react-chess-game/Components/ChessGame",
|
|
9
198
|
component: ChessGame.Root,
|
|
@@ -11,52 +200,402 @@ const meta = {
|
|
|
11
200
|
argTypes: {},
|
|
12
201
|
parameters: {
|
|
13
202
|
actions: { argTypesRegex: "^_on.*" },
|
|
203
|
+
layout: "centered",
|
|
14
204
|
},
|
|
15
|
-
decorators: [
|
|
16
|
-
(Story) => (
|
|
17
|
-
<div style={{ width: "600px" }}>
|
|
18
|
-
{/* 👇 Decorators in Storybook also accept a function. Replace <Story/> with Story() to enable it */}
|
|
19
|
-
<Story />
|
|
20
|
-
</div>
|
|
21
|
-
),
|
|
22
|
-
],
|
|
23
205
|
} satisfies Meta<typeof ChessGame.Root>;
|
|
24
206
|
|
|
25
207
|
export default meta;
|
|
26
208
|
|
|
27
|
-
//
|
|
209
|
+
// ============================================================================
|
|
210
|
+
// Stories
|
|
211
|
+
// ============================================================================
|
|
28
212
|
|
|
29
|
-
export const Default = () =>
|
|
30
|
-
|
|
31
|
-
<
|
|
32
|
-
<
|
|
33
|
-
<
|
|
34
|
-
</
|
|
35
|
-
|
|
36
|
-
|
|
213
|
+
export const Default = () => (
|
|
214
|
+
<div style={s.container}>
|
|
215
|
+
<div style={s.header}>
|
|
216
|
+
<h3 style={s.title}>Standard Game</h3>
|
|
217
|
+
<p style={s.subtitle}>Interactive chess board with default settings</p>
|
|
218
|
+
</div>
|
|
219
|
+
<div style={s.board}>
|
|
220
|
+
<ChessGame.Root>
|
|
221
|
+
<ChessGame.KeyboardControls />
|
|
222
|
+
<ChessGame.Board />
|
|
223
|
+
</ChessGame.Root>
|
|
224
|
+
</div>
|
|
225
|
+
<p style={s.hint}>Arrow keys to navigate moves · Press F to flip</p>
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
37
228
|
|
|
38
|
-
export const WithSounds = () =>
|
|
39
|
-
|
|
40
|
-
<
|
|
41
|
-
<
|
|
42
|
-
<
|
|
43
|
-
</
|
|
44
|
-
|
|
229
|
+
export const WithSounds = () => (
|
|
230
|
+
<div style={s.container}>
|
|
231
|
+
<div style={s.header}>
|
|
232
|
+
<h3 style={s.title}>Sound Effects</h3>
|
|
233
|
+
<p style={s.subtitle}>Audio feedback on every move</p>
|
|
234
|
+
</div>
|
|
235
|
+
<div style={s.board}>
|
|
236
|
+
<ChessGame.Root>
|
|
237
|
+
<ChessGame.Sounds />
|
|
238
|
+
<ChessGame.Board />
|
|
239
|
+
</ChessGame.Root>
|
|
240
|
+
</div>
|
|
241
|
+
<p style={s.hint}>Move pieces to hear sounds for each piece type</p>
|
|
242
|
+
</div>
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
export const WithKeyboardControls = () => (
|
|
246
|
+
<div style={s.container}>
|
|
247
|
+
<div style={s.header}>
|
|
248
|
+
<h3 style={s.title}>Keyboard Navigation</h3>
|
|
249
|
+
<p style={s.subtitle}>Custom keyboard shortcuts for game control</p>
|
|
250
|
+
</div>
|
|
251
|
+
<div style={s.board}>
|
|
252
|
+
<ChessGame.Root>
|
|
253
|
+
<ChessGame.KeyboardControls
|
|
254
|
+
controls={{
|
|
255
|
+
f: (ctx) => ctx.methods.flipBoard(),
|
|
256
|
+
w: (ctx) => ctx.methods.goToStart(),
|
|
257
|
+
s: (ctx) => ctx.methods.goToEnd(),
|
|
258
|
+
a: (ctx) => ctx.methods.goToPreviousMove(),
|
|
259
|
+
d: (ctx) => ctx.methods.goToNextMove(),
|
|
260
|
+
}}
|
|
261
|
+
/>
|
|
262
|
+
<ChessGame.Board />
|
|
263
|
+
</ChessGame.Root>
|
|
264
|
+
</div>
|
|
265
|
+
<div style={kbd.grid}>
|
|
266
|
+
<span style={kbd.item}>
|
|
267
|
+
<kbd style={kbd.key}>W</kbd> Start
|
|
268
|
+
</span>
|
|
269
|
+
<span style={kbd.item}>
|
|
270
|
+
<kbd style={kbd.key}>A</kbd> Prev
|
|
271
|
+
</span>
|
|
272
|
+
<span style={kbd.item}>
|
|
273
|
+
<kbd style={kbd.key}>D</kbd> Next
|
|
274
|
+
</span>
|
|
275
|
+
<span style={kbd.item}>
|
|
276
|
+
<kbd style={kbd.key}>S</kbd> End
|
|
277
|
+
</span>
|
|
278
|
+
<span style={kbd.item}>
|
|
279
|
+
<kbd style={kbd.key}>F</kbd> Flip
|
|
280
|
+
</span>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// ============================================================================
|
|
286
|
+
// Clock Stories
|
|
287
|
+
// ============================================================================
|
|
288
|
+
|
|
289
|
+
const ClockDisplay = ({
|
|
290
|
+
label,
|
|
291
|
+
color: side,
|
|
292
|
+
...props
|
|
293
|
+
}: { label: string; color: "white" | "black" } & Record<string, unknown>) => (
|
|
294
|
+
<div style={clock.cell}>
|
|
295
|
+
<span style={clock.label}>{label}</span>
|
|
296
|
+
<ChessGame.Clock.Display
|
|
297
|
+
color={side}
|
|
298
|
+
style={side === "white" ? clock.white : clock.black}
|
|
299
|
+
{...props}
|
|
300
|
+
/>
|
|
301
|
+
</div>
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
export const WithClockBlitz = () => (
|
|
305
|
+
<ChessGame.Root timeControl={{ time: "5+3", clockStart: "immediate" }}>
|
|
306
|
+
<div style={s.container}>
|
|
307
|
+
<div style={s.header}>
|
|
308
|
+
<h3 style={s.title}>Blitz · 5+3</h3>
|
|
309
|
+
<p style={s.subtitle}>5 minutes with 3-second increment</p>
|
|
310
|
+
</div>
|
|
311
|
+
<div style={clock.row}>
|
|
312
|
+
<ClockDisplay label="White" color="white" />
|
|
313
|
+
<ClockDisplay label="Black" color="black" />
|
|
314
|
+
</div>
|
|
315
|
+
<div style={s.board}>
|
|
316
|
+
<ChessGame.Board />
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
</ChessGame.Root>
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
export const WithClockBullet = () => (
|
|
323
|
+
<ChessGame.Root timeControl={{ time: "1+0", clockStart: "immediate" }}>
|
|
324
|
+
<div style={s.container}>
|
|
325
|
+
<div style={s.header}>
|
|
326
|
+
<h3 style={s.title}>Bullet · 1+0</h3>
|
|
327
|
+
<p style={s.subtitle}>1 minute, no increment</p>
|
|
328
|
+
</div>
|
|
329
|
+
<div style={clock.row}>
|
|
330
|
+
<ClockDisplay label="White" color="white" format="ss.d" />
|
|
331
|
+
<ClockDisplay label="Black" color="black" format="ss.d" />
|
|
332
|
+
</div>
|
|
333
|
+
<div style={s.board}>
|
|
334
|
+
<ChessGame.Board />
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
</ChessGame.Root>
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
export const WithClockControls = () => (
|
|
341
|
+
<ChessGame.Root
|
|
342
|
+
timeControl={{ time: "3+2", clockStart: "immediate" }}
|
|
343
|
+
autoSwitchOnMove={false}
|
|
344
|
+
>
|
|
345
|
+
<div style={s.container}>
|
|
346
|
+
<div style={s.header}>
|
|
347
|
+
<h3 style={s.title}>Rapid · 3+2</h3>
|
|
348
|
+
<p style={s.subtitle}>Manual clock controls enabled</p>
|
|
349
|
+
</div>
|
|
350
|
+
<div style={clock.row}>
|
|
351
|
+
<ClockDisplay label="White" color="white" />
|
|
352
|
+
<ClockDisplay label="Black" color="black" />
|
|
353
|
+
</div>
|
|
354
|
+
<div style={s.controls}>
|
|
355
|
+
<ChessGame.Clock.PlayPause style={{ ...s.btn, ...s.btnPrimary }}>
|
|
356
|
+
Play / Pause
|
|
357
|
+
</ChessGame.Clock.PlayPause>
|
|
358
|
+
<ChessGame.Clock.Switch style={s.btn}>Switch</ChessGame.Clock.Switch>
|
|
359
|
+
<ChessGame.Clock.Reset style={s.btn}>Reset</ChessGame.Clock.Reset>
|
|
360
|
+
</div>
|
|
361
|
+
<div style={s.board}>
|
|
362
|
+
<ChessGame.Board />
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
</ChessGame.Root>
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
// ============================================================================
|
|
369
|
+
// Server-Controlled Clock
|
|
370
|
+
// ============================================================================
|
|
371
|
+
|
|
372
|
+
const srv = {
|
|
373
|
+
panel: {
|
|
374
|
+
width: "100%",
|
|
375
|
+
padding: "14px 16px",
|
|
376
|
+
backgroundColor: color.warnLight,
|
|
377
|
+
border: `1px solid ${color.warnBorder}`,
|
|
378
|
+
borderRadius: "6px",
|
|
379
|
+
display: "flex",
|
|
380
|
+
flexDirection: "column" as const,
|
|
381
|
+
gap: "10px",
|
|
382
|
+
fontFamily: font.sans,
|
|
383
|
+
},
|
|
384
|
+
panelTitle: {
|
|
385
|
+
fontSize: "11px",
|
|
386
|
+
fontWeight: 700,
|
|
387
|
+
color: color.warn,
|
|
388
|
+
textTransform: "uppercase" as const,
|
|
389
|
+
letterSpacing: "0.06em",
|
|
390
|
+
margin: 0,
|
|
391
|
+
},
|
|
392
|
+
row: {
|
|
393
|
+
display: "flex",
|
|
394
|
+
gap: "8px",
|
|
395
|
+
alignItems: "center",
|
|
396
|
+
fontSize: "12px",
|
|
397
|
+
color: "#5a4e1a",
|
|
398
|
+
fontFamily: font.mono,
|
|
399
|
+
flexWrap: "wrap" as const,
|
|
400
|
+
},
|
|
401
|
+
badge: {
|
|
402
|
+
padding: "2px 8px",
|
|
403
|
+
borderRadius: "3px",
|
|
404
|
+
backgroundColor: "#f5edcf",
|
|
405
|
+
fontWeight: 600,
|
|
406
|
+
fontSize: "11px",
|
|
407
|
+
fontFamily: font.sans,
|
|
408
|
+
},
|
|
409
|
+
smallBtn: {
|
|
410
|
+
padding: "3px 8px",
|
|
411
|
+
fontSize: "11px",
|
|
412
|
+
fontWeight: 600,
|
|
413
|
+
cursor: "pointer",
|
|
414
|
+
border: `1px solid ${color.warnBorder}`,
|
|
415
|
+
borderRadius: "3px",
|
|
416
|
+
backgroundColor: "#f5edcf",
|
|
417
|
+
color: "#5a4e1a",
|
|
418
|
+
fontFamily: font.sans,
|
|
419
|
+
} as React.CSSProperties,
|
|
420
|
+
dangerBtn: {
|
|
421
|
+
padding: "3px 8px",
|
|
422
|
+
fontSize: "11px",
|
|
423
|
+
fontWeight: 600,
|
|
424
|
+
cursor: "pointer",
|
|
425
|
+
border: `1px solid ${color.dangerBorder}`,
|
|
426
|
+
borderRadius: "3px",
|
|
427
|
+
backgroundColor: color.dangerLight,
|
|
428
|
+
color: color.danger,
|
|
429
|
+
fontFamily: font.sans,
|
|
430
|
+
} as React.CSSProperties,
|
|
431
|
+
slider: {
|
|
432
|
+
display: "flex",
|
|
433
|
+
alignItems: "center",
|
|
434
|
+
gap: "8px",
|
|
435
|
+
fontSize: "12px",
|
|
436
|
+
color: "#5a4e1a",
|
|
437
|
+
fontFamily: font.sans,
|
|
438
|
+
},
|
|
45
439
|
};
|
|
46
440
|
|
|
47
|
-
export const
|
|
441
|
+
export const WithServerControlledClock = () => {
|
|
442
|
+
const INITIAL_TIME = 30 * 1000;
|
|
443
|
+
const INCREMENT = 2 * 1000;
|
|
444
|
+
const { clientView, lagMs, setLagMs, serverMove, serverReset, addTime } =
|
|
445
|
+
useSimulatedServer(INITIAL_TIME, INCREMENT);
|
|
446
|
+
|
|
48
447
|
return (
|
|
49
|
-
<ChessGame.Root
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
448
|
+
<ChessGame.Root
|
|
449
|
+
timeControl={{
|
|
450
|
+
time: { baseTime: 30, increment: 2 },
|
|
451
|
+
clockStart: "immediate",
|
|
452
|
+
onTimeout: (loser) => console.log(`Server: ${loser} flagged`),
|
|
453
|
+
}}
|
|
454
|
+
autoSwitchOnMove={false}
|
|
455
|
+
>
|
|
456
|
+
<ServerTimeSync
|
|
457
|
+
serverTimes={{
|
|
458
|
+
white: clientView.whiteTime,
|
|
459
|
+
black: clientView.blackTime,
|
|
57
460
|
}}
|
|
58
461
|
/>
|
|
59
|
-
<
|
|
462
|
+
<div style={s.container}>
|
|
463
|
+
<div style={s.header}>
|
|
464
|
+
<h3 style={s.title}>Server-Synced Clock</h3>
|
|
465
|
+
<p style={s.subtitle}>
|
|
466
|
+
Server-authoritative times synced via setTime()
|
|
467
|
+
</p>
|
|
468
|
+
</div>
|
|
469
|
+
|
|
470
|
+
{/* Server control panel */}
|
|
471
|
+
<div style={srv.panel}>
|
|
472
|
+
<p style={srv.panelTitle}>Server Controls</p>
|
|
473
|
+
|
|
474
|
+
<div style={srv.row}>
|
|
475
|
+
<span>
|
|
476
|
+
W: <b>{(clientView.whiteTime / 1000).toFixed(1)}s</b>
|
|
477
|
+
</span>
|
|
478
|
+
<span>
|
|
479
|
+
B: <b>{(clientView.blackTime / 1000).toFixed(1)}s</b>
|
|
480
|
+
</span>
|
|
481
|
+
<span style={srv.badge}>
|
|
482
|
+
{clientView.finished
|
|
483
|
+
? "finished"
|
|
484
|
+
: clientView.running
|
|
485
|
+
? `${clientView.activePlayer} to move`
|
|
486
|
+
: "waiting"}
|
|
487
|
+
</span>
|
|
488
|
+
</div>
|
|
489
|
+
|
|
490
|
+
<div style={srv.row}>
|
|
491
|
+
<span style={{ fontWeight: 600, fontFamily: font.sans }}>W:</span>
|
|
492
|
+
<button
|
|
493
|
+
style={srv.smallBtn}
|
|
494
|
+
onClick={() => addTime("white", 15000)}
|
|
495
|
+
>
|
|
496
|
+
+15s
|
|
497
|
+
</button>
|
|
498
|
+
<button
|
|
499
|
+
style={srv.dangerBtn}
|
|
500
|
+
onClick={() => addTime("white", -15000)}
|
|
501
|
+
>
|
|
502
|
+
-15s
|
|
503
|
+
</button>
|
|
504
|
+
<span
|
|
505
|
+
style={{
|
|
506
|
+
fontWeight: 600,
|
|
507
|
+
fontFamily: font.sans,
|
|
508
|
+
marginLeft: "4px",
|
|
509
|
+
}}
|
|
510
|
+
>
|
|
511
|
+
B:
|
|
512
|
+
</span>
|
|
513
|
+
<button
|
|
514
|
+
style={srv.smallBtn}
|
|
515
|
+
onClick={() => addTime("black", 15000)}
|
|
516
|
+
>
|
|
517
|
+
+15s
|
|
518
|
+
</button>
|
|
519
|
+
<button
|
|
520
|
+
style={srv.dangerBtn}
|
|
521
|
+
onClick={() => addTime("black", -15000)}
|
|
522
|
+
>
|
|
523
|
+
-15s
|
|
524
|
+
</button>
|
|
525
|
+
</div>
|
|
526
|
+
|
|
527
|
+
<div style={srv.slider}>
|
|
528
|
+
<span style={{ fontWeight: 600 }}>Lag</span>
|
|
529
|
+
<input
|
|
530
|
+
type="range"
|
|
531
|
+
min={0}
|
|
532
|
+
max={3000}
|
|
533
|
+
step={100}
|
|
534
|
+
value={lagMs}
|
|
535
|
+
onChange={(e) => setLagMs(Number(e.target.value))}
|
|
536
|
+
style={{ flex: 1 }}
|
|
537
|
+
/>
|
|
538
|
+
<span
|
|
539
|
+
style={{
|
|
540
|
+
fontFamily: font.mono,
|
|
541
|
+
fontSize: "11px",
|
|
542
|
+
fontWeight: 600,
|
|
543
|
+
minWidth: "40px",
|
|
544
|
+
textAlign: "right" as const,
|
|
545
|
+
}}
|
|
546
|
+
>
|
|
547
|
+
{lagMs}ms
|
|
548
|
+
</span>
|
|
549
|
+
</div>
|
|
550
|
+
</div>
|
|
551
|
+
|
|
552
|
+
<div style={clock.row}>
|
|
553
|
+
<ClockDisplay label="White" color="white" />
|
|
554
|
+
<ClockDisplay label="Black" color="black" />
|
|
555
|
+
</div>
|
|
556
|
+
|
|
557
|
+
<ServerMoveDetector onMove={serverMove} />
|
|
558
|
+
<div style={s.board}>
|
|
559
|
+
<ChessGame.Board />
|
|
560
|
+
</div>
|
|
561
|
+
|
|
562
|
+
<div style={s.controls}>
|
|
563
|
+
<button style={s.btn} onClick={serverReset}>
|
|
564
|
+
Reset Server
|
|
565
|
+
</button>
|
|
566
|
+
</div>
|
|
567
|
+
|
|
568
|
+
<p style={s.hint}>
|
|
569
|
+
Use +/- to change server time. Set lag > 0 and make moves to see
|
|
570
|
+
client interpolation with delayed server corrections.
|
|
571
|
+
</p>
|
|
572
|
+
</div>
|
|
60
573
|
</ChessGame.Root>
|
|
61
574
|
);
|
|
62
575
|
};
|
|
576
|
+
|
|
577
|
+
export const WithClockMultiPeriod = () => (
|
|
578
|
+
<ChessGame.Root
|
|
579
|
+
timeControl={{
|
|
580
|
+
time: [
|
|
581
|
+
{ baseTime: 120, increment: 0, moves: 5 },
|
|
582
|
+
{ baseTime: 60, increment: 0 },
|
|
583
|
+
],
|
|
584
|
+
clockStart: "immediate",
|
|
585
|
+
}}
|
|
586
|
+
>
|
|
587
|
+
<div style={s.container}>
|
|
588
|
+
<div style={s.header}>
|
|
589
|
+
<h3 style={s.title}>Multi-Period Tournament</h3>
|
|
590
|
+
<p style={s.subtitle}>2 min (5 moves) then 1 min sudden death</p>
|
|
591
|
+
</div>
|
|
592
|
+
<div style={clock.row}>
|
|
593
|
+
<ClockDisplay label="White" color="white" format="mm:ss" />
|
|
594
|
+
<ClockDisplay label="Black" color="black" format="mm:ss" />
|
|
595
|
+
</div>
|
|
596
|
+
<div style={s.board}>
|
|
597
|
+
<ChessGame.Board />
|
|
598
|
+
</div>
|
|
599
|
+
</div>
|
|
600
|
+
</ChessGame.Root>
|
|
601
|
+
);
|