@react-chess-tools/react-chess-game 0.4.2 → 0.5.1
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 +17 -0
- package/README.MD +58 -33
- package/dist/index.d.mts +9 -9
- package/dist/index.mjs +164 -44
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/components/ChessGame/parts/Board.tsx +160 -43
- package/src/hooks/useBoardSounds.ts +16 -7
- package/src/utils/__tests__/board.test.ts +256 -1
- package/src/utils/board.ts +49 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Chess } from "chess.js";
|
|
2
|
-
import { getCustomSquareStyles } from "../board";
|
|
2
|
+
import { getCustomSquareStyles, deepMergeChessboardOptions } from "../board";
|
|
3
3
|
import { getGameInfo } from "../chess";
|
|
4
4
|
|
|
5
5
|
describe("Board Utilities", () => {
|
|
@@ -140,4 +140,259 @@ describe("Board Utilities", () => {
|
|
|
140
140
|
expect(styles.c3.background).toContain("25%");
|
|
141
141
|
});
|
|
142
142
|
});
|
|
143
|
+
|
|
144
|
+
describe("deepMergeChessboardOptions", () => {
|
|
145
|
+
it("should deeply merge nested object options without overriding computed values", () => {
|
|
146
|
+
const baseOptions = {
|
|
147
|
+
squareStyles: {
|
|
148
|
+
e4: {
|
|
149
|
+
backgroundColor: "rgba(255, 255, 0, 0.5)", // Computed move highlighting
|
|
150
|
+
background:
|
|
151
|
+
"radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)", // Move dot
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
dropSquareStyle: {
|
|
155
|
+
backgroundColor: "rgba(255, 255, 0, 0.4)",
|
|
156
|
+
border: "2px dashed yellow",
|
|
157
|
+
},
|
|
158
|
+
showNotation: true,
|
|
159
|
+
allowDrawingArrows: true,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const customOptions = {
|
|
163
|
+
squareStyles: {
|
|
164
|
+
e4: {
|
|
165
|
+
border: "2px solid red", // Should be added without removing background
|
|
166
|
+
},
|
|
167
|
+
a1: {
|
|
168
|
+
backgroundColor: "blue", // New square style
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
dropSquareStyle: {
|
|
172
|
+
backgroundColor: "rgba(0, 255, 0, 0.6)", // Should override backgroundColor but preserve border
|
|
173
|
+
},
|
|
174
|
+
showNotation: false, // Should override primitive
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const result = deepMergeChessboardOptions(baseOptions, customOptions);
|
|
178
|
+
|
|
179
|
+
// squareStyles should deep merge
|
|
180
|
+
expect(result.squareStyles?.e4).toEqual({
|
|
181
|
+
backgroundColor: "rgba(255, 255, 0, 0.5)",
|
|
182
|
+
background:
|
|
183
|
+
"radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)",
|
|
184
|
+
border: "2px solid red",
|
|
185
|
+
});
|
|
186
|
+
expect(result.squareStyles?.a1).toEqual({
|
|
187
|
+
backgroundColor: "blue",
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// dropSquareStyle should deep merge
|
|
191
|
+
expect(result.dropSquareStyle).toEqual({
|
|
192
|
+
backgroundColor: "rgba(0, 255, 0, 0.6)",
|
|
193
|
+
border: "2px dashed yellow",
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Primitives should override
|
|
197
|
+
expect(result.showNotation).toBe(false);
|
|
198
|
+
expect(result.allowDrawingArrows).toBe(true);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("should handle function properties by overwriting them", () => {
|
|
202
|
+
const baseOnSquareClick = jest.fn();
|
|
203
|
+
const baseCanDragPiece = jest.fn();
|
|
204
|
+
const customOnSquareClick = jest.fn();
|
|
205
|
+
|
|
206
|
+
const baseOptions = {
|
|
207
|
+
onSquareClick: baseOnSquareClick,
|
|
208
|
+
canDragPiece: baseCanDragPiece,
|
|
209
|
+
showNotation: true,
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const customOptions = {
|
|
213
|
+
onSquareClick: customOnSquareClick,
|
|
214
|
+
// canDragPiece not provided - should keep base function
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const result = deepMergeChessboardOptions(baseOptions, customOptions);
|
|
218
|
+
|
|
219
|
+
// Custom function should replace base function
|
|
220
|
+
expect(result.onSquareClick).toBe(customOnSquareClick);
|
|
221
|
+
expect(result.onSquareClick).not.toBe(baseOnSquareClick);
|
|
222
|
+
|
|
223
|
+
// Base function should be preserved when not overridden
|
|
224
|
+
expect(result.canDragPiece).toBe(baseCanDragPiece);
|
|
225
|
+
|
|
226
|
+
// Other properties should merge normally
|
|
227
|
+
expect(result.showNotation).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("should handle nested object properties by deep merging them", () => {
|
|
231
|
+
const baseOptions = {
|
|
232
|
+
darkSquareStyle: {
|
|
233
|
+
backgroundColor: "#8B4513",
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const customOptions = {
|
|
238
|
+
darkSquareStyle: {
|
|
239
|
+
backgroundColor: "#654321",
|
|
240
|
+
border: "1px solid black",
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const result = deepMergeChessboardOptions(baseOptions, customOptions);
|
|
245
|
+
|
|
246
|
+
expect(result.darkSquareStyle).toEqual({
|
|
247
|
+
backgroundColor: "#654321",
|
|
248
|
+
border: "1px solid black",
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("should handle undefined custom options gracefully", () => {
|
|
253
|
+
const baseOptions = {
|
|
254
|
+
squareStyles: {
|
|
255
|
+
e4: { backgroundColor: "yellow" },
|
|
256
|
+
},
|
|
257
|
+
showNotation: true,
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const result = deepMergeChessboardOptions(baseOptions, undefined);
|
|
261
|
+
|
|
262
|
+
expect(result).toEqual(baseOptions);
|
|
263
|
+
expect(result).not.toBe(baseOptions); // Should be a new object
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("should handle empty custom options", () => {
|
|
267
|
+
const baseOptions = {
|
|
268
|
+
squareStyles: {
|
|
269
|
+
e4: { backgroundColor: "yellow" },
|
|
270
|
+
},
|
|
271
|
+
showNotation: true,
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const result = deepMergeChessboardOptions(baseOptions, {});
|
|
275
|
+
|
|
276
|
+
expect(result).toEqual(baseOptions);
|
|
277
|
+
expect(result).not.toBe(baseOptions); // Should be a new object
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("should preserve complex nested object structures", () => {
|
|
281
|
+
const baseOptions = {
|
|
282
|
+
squareStyles: {
|
|
283
|
+
e4: {
|
|
284
|
+
backgroundColor: "rgba(255, 255, 0, 0.5)",
|
|
285
|
+
background:
|
|
286
|
+
"radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)",
|
|
287
|
+
border: "1px solid black",
|
|
288
|
+
},
|
|
289
|
+
d5: {
|
|
290
|
+
backgroundColor: "rgba(0, 255, 0, 0.3)",
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const customOptions = {
|
|
296
|
+
squareStyles: {
|
|
297
|
+
e4: {
|
|
298
|
+
borderRadius: "4px", // Add new property
|
|
299
|
+
backgroundColor: "rgba(255, 0, 0, 0.5)", // Override existing
|
|
300
|
+
// background should be preserved from base
|
|
301
|
+
},
|
|
302
|
+
f6: {
|
|
303
|
+
backgroundColor: "blue", // New square
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const result = deepMergeChessboardOptions(baseOptions, customOptions);
|
|
309
|
+
|
|
310
|
+
expect(result.squareStyles?.e4).toEqual({
|
|
311
|
+
backgroundColor: "rgba(255, 0, 0, 0.5)", // Overridden
|
|
312
|
+
background:
|
|
313
|
+
"radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)", // Preserved
|
|
314
|
+
border: "1px solid black", // Preserved
|
|
315
|
+
borderRadius: "4px", // Added
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
expect(result.squareStyles?.d5).toEqual({
|
|
319
|
+
backgroundColor: "rgba(0, 255, 0, 0.3)", // Preserved from base
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
expect(result.squareStyles?.f6).toEqual({
|
|
323
|
+
backgroundColor: "blue", // Added from custom
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("should handle real-world Board component use case", () => {
|
|
328
|
+
// Simulate the actual use case in Board component
|
|
329
|
+
const computedSquareStyles = {
|
|
330
|
+
e2: {
|
|
331
|
+
backgroundColor: "rgba(255, 255, 0, 0.5)", // Active square highlight
|
|
332
|
+
},
|
|
333
|
+
e3: {
|
|
334
|
+
background:
|
|
335
|
+
"radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)", // Move dot
|
|
336
|
+
},
|
|
337
|
+
e4: {
|
|
338
|
+
background:
|
|
339
|
+
"radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)", // Move dot
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const baseOptions = {
|
|
344
|
+
squareStyles: computedSquareStyles,
|
|
345
|
+
boardOrientation: "white" as const,
|
|
346
|
+
position: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
|
|
347
|
+
showNotation: true,
|
|
348
|
+
onSquareClick: jest.fn(),
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const userCustomOptions = {
|
|
352
|
+
squareStyles: {
|
|
353
|
+
e4: {
|
|
354
|
+
border: "2px solid red", // User wants to add border to a square that has move dots
|
|
355
|
+
},
|
|
356
|
+
a1: {
|
|
357
|
+
backgroundColor: "lightblue", // User wants to highlight corner square
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
showNotation: false, // User wants to hide notation
|
|
361
|
+
onSquareClick: jest.fn(), // User provides custom click handler
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const result = deepMergeChessboardOptions(baseOptions, userCustomOptions);
|
|
365
|
+
|
|
366
|
+
// Move highlighting should be preserved while adding user's border
|
|
367
|
+
expect(result.squareStyles?.e4).toEqual({
|
|
368
|
+
background:
|
|
369
|
+
"radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)",
|
|
370
|
+
border: "2px solid red",
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// User's new square style should be added
|
|
374
|
+
expect(result.squareStyles?.a1).toEqual({
|
|
375
|
+
backgroundColor: "lightblue",
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// Other computed styles should be preserved
|
|
379
|
+
expect(result.squareStyles?.e2).toEqual({
|
|
380
|
+
backgroundColor: "rgba(255, 255, 0, 0.5)",
|
|
381
|
+
});
|
|
382
|
+
expect(result.squareStyles?.e3).toEqual({
|
|
383
|
+
background:
|
|
384
|
+
"radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)",
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Primitives should be overridden
|
|
388
|
+
expect(result.showNotation).toBe(false);
|
|
389
|
+
expect(result.position).toBe(
|
|
390
|
+
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
// Functions should be replaced
|
|
394
|
+
expect(result.onSquareClick).toBe(userCustomOptions.onSquareClick);
|
|
395
|
+
expect(result.onSquareClick).not.toBe(baseOptions.onSquareClick);
|
|
396
|
+
});
|
|
397
|
+
});
|
|
143
398
|
});
|
package/src/utils/board.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type Chess, type Square } from "chess.js";
|
|
2
2
|
import { type CSSProperties } from "react";
|
|
3
|
+
import { merge } from "lodash";
|
|
4
|
+
import type { ChessboardOptions } from "react-chessboard";
|
|
3
5
|
import { getDestinationSquares, type GameInfo } from "./chess";
|
|
4
6
|
|
|
5
7
|
const LAST_MOVE_COLOR = "rgba(255, 255, 0, 0.5)";
|
|
@@ -54,3 +56,50 @@ export const getCustomSquareStyles = (
|
|
|
54
56
|
}
|
|
55
57
|
return customSquareStyles;
|
|
56
58
|
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Smart deep merge for ChessboardOptions that handles different property types appropriately:
|
|
62
|
+
* - Functions: Overwrite (custom functions replace base functions)
|
|
63
|
+
* - Objects: Deep merge (nested objects merge recursively)
|
|
64
|
+
* - Primitives: Overwrite (custom values replace base values)
|
|
65
|
+
*
|
|
66
|
+
* This ensures that computed options (like squareStyles with move highlighting) are preserved
|
|
67
|
+
* while allowing custom options to extend or override them intelligently.
|
|
68
|
+
*
|
|
69
|
+
* @param baseOptions - The computed base options (e.g., computed squareStyles, event handlers)
|
|
70
|
+
* @param customOptions - Custom options provided by the user
|
|
71
|
+
* @returns Intelligently merged ChessboardOptions
|
|
72
|
+
*/
|
|
73
|
+
export const deepMergeChessboardOptions = (
|
|
74
|
+
baseOptions: ChessboardOptions,
|
|
75
|
+
customOptions?: Partial<ChessboardOptions>,
|
|
76
|
+
): ChessboardOptions => {
|
|
77
|
+
if (!customOptions) {
|
|
78
|
+
return { ...baseOptions }; // Return a new object even when no custom options
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const result = merge({}, baseOptions, customOptions, {
|
|
82
|
+
customizer: (_objValue: unknown, srcValue: unknown) => {
|
|
83
|
+
// Functions should always overwrite (not merge)
|
|
84
|
+
// This is important for event handlers like onSquareClick, onPieceDrop, etc.
|
|
85
|
+
if (typeof srcValue === "function") {
|
|
86
|
+
return srcValue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// For arrays, we typically want to overwrite rather than merge
|
|
90
|
+
// This avoids unexpected behavior with array concatenation
|
|
91
|
+
if (Array.isArray(srcValue)) {
|
|
92
|
+
return srcValue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Let lodash handle objects with default deep merge behavior
|
|
96
|
+
// This will properly merge nested objects like squareStyles, dropSquareStyle, etc.
|
|
97
|
+
return undefined; // Use default merge behavior
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Clean up any unwanted properties that lodash might add
|
|
102
|
+
delete (result as Record<string, unknown>).customizer;
|
|
103
|
+
|
|
104
|
+
return result;
|
|
105
|
+
};
|