@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 CHANGED
@@ -1,5 +1,22 @@
1
1
  # @react-chess-tools/react-chess-game
2
2
 
3
+ ## 0.5.1
4
+
5
+ ### Patch Changes
6
+
7
+ - f50ac0f: feat: improve chessboard options merging with deep merge utility
8
+
9
+ ## 0.5.0
10
+
11
+ ### Minor Changes
12
+
13
+ - f58c1ac: chore: upgrade to react-chessboard v5
14
+
15
+ ### Patch Changes
16
+
17
+ - 95cf0c3: docs: update README documentation for react-chess-tools packages
18
+ - a877019: fix: handle sound playback errors gracefully
19
+
3
20
  ## 0.4.2
4
21
 
5
22
  ### Patch Changes
package/README.MD CHANGED
@@ -18,7 +18,7 @@ This project is a React-based chess game that allows users to play chess online.
18
18
  - Square highlighting
19
19
  - Keyboard controls
20
20
 
21
- It is build using an approach similar to the one used in the `radix-ui`, where the `ChessGame` component is built using a `ChessGameContext` that you can use to customize and ehance the component game. Is also provides a set of default components that you can use to build your next chess app.
21
+ It is built using an approach similar to the one used in the `radix-ui`, where the `ChessGame` component is built using a `ChessGameContext` that you can use to customize and enhance the component game. It also provides a set of default components that you can use to build your next chess app.
22
22
 
23
23
  ## Preview
24
24
 
@@ -39,11 +39,11 @@ To use the `react-chess-game` package, you can import the `ChessGame` component
39
39
  ```tsx
40
40
  import { ChessGame } from "@react-chess-tools/react-chess-game";
41
41
 
42
- const App = () => {
42
+ const App = () => (
43
43
  <ChessGame.Root>
44
44
  <ChessGame.Board />
45
- </ChessGame.Root>;
46
- };
45
+ </ChessGame.Root>
46
+ );
47
47
  ```
48
48
 
49
49
  ## Documentation
@@ -58,23 +58,49 @@ The `ChessGame.Root` component is the root component of the `react-chess-game` p
58
58
 
59
59
  The `ChessGame.Root` component accepts the following props:
60
60
 
61
- | Name | Type | Default | Description |
62
- | ----------- | ---------- | ---------------- | ----------------------------------------------- |
63
- | children | React.FC | | The children of the `ChessGame.Root` component. |
64
- | fen | string | initial position | The FEN of the chess game. |
65
- | orientation | "w" \| "b" | "white" | The orientation of the chess game. |
61
+ | Name | Type | Default | Description |
62
+ | ----------- | ---------- | ------- | ----------------------------------------------- |
63
+ | children | React.FC | | The children of the `ChessGame.Root` component. |
64
+ | fen | string | | The initial FEN of the chess game. |
65
+ | orientation | "w" \| "b" | "w" | The orientation of the chess game. |
66
66
 
67
67
  ### ChessGame.Board
68
68
 
69
69
  The `ChessGame.Board` component is the main component of the `react-chess-game` package. It is used to render the chess board and the pieces. It uses the `ChessGameContext` to get the `Chess` instance and the `orientation` of the game.
70
70
 
71
+ This version targets `react-chessboard` v5 and exposes a single `options` prop instead of spreading all board props.
72
+
71
73
  #### Props
72
74
 
73
- All the props from Chessboard.js are supported. You can find the full list of props [here](https://github.com/Clariity/react-chessboard#props).
75
+ Accepts a single prop:
76
+
77
+ | Name | Type | Description |
78
+ | -------- | ------------------- | -------------------------------------------------------------------------------------------------- |
79
+ | options? | `ChessboardOptions` | Forwarded to `react-chessboard` v5 `Chessboard({ options })`. Your values merge with the defaults. |
80
+
81
+ Quick example (custom styles and event handlers):
82
+
83
+ ```tsx
84
+ <ChessGame.Root>
85
+ <ChessGame.Board
86
+ options={{
87
+ squareStyles: { e4: { boxShadow: "inset 0 0 0 2px #4f46e5" } },
88
+ onPieceDrop: ({ sourceSquare, targetSquare }) => {
89
+ // return boolean to accept/reject drop
90
+ return true;
91
+ },
92
+ showNotation: true,
93
+ animationDurationInMs: 300,
94
+ }}
95
+ />
96
+
97
+ {/* Other parts like <ChessGame.Sounds /> or <ChessGame.KeyboardControls /> */}
98
+ </ChessGame.Root>
99
+ ```
74
100
 
75
101
  ### ChessGame.Sounds
76
102
 
77
- The `ChessGame.Sounds` component is used to provide the sounds for the chess game. By default, it uses the sounds from the `react-chess-sounds` package, but you can provide your own sounds. Sounds must be provided as base 64 encoded strings.
103
+ The `ChessGame.Sounds` component is used to provide the sounds for the chess game. By default, it uses built-in sounds, but you can provide your own sounds. Sounds must be provided as base 64 encoded strings.
78
104
 
79
105
  #### Props
80
106
 
@@ -107,27 +133,31 @@ The `useChessGameContext` hook is used to get the `ChessGameContext` from the `C
107
133
 
108
134
  The `useChessGameContext` hook returns the following values:
109
135
 
110
- | Name | Type | Description |
111
- | ----------- | ---------- | -------------------------------------------------------------------------------------- |
112
- | chess | Chess | The `Chess` instance of the chess game. |
113
- | orientation | "w" \| "b" | The orientation of the chess game. |
114
- | methods | Methods | The methods you can use to interact with the chess game. |
115
- | info | Info | The info of the chess game, calculated using the chess instance using chess.js methods |
136
+ | Name | Type | Description |
137
+ | ---------------- | ---------- | -------------------------------------------------------------------------------------- |
138
+ | game | Chess | The underlying `Chess.js` instance. |
139
+ | orientation | "w" \| "b" | The orientation of the chess game. |
140
+ | currentFen | string | The current FEN string representing the board position. |
141
+ | currentPosition | string | The current move in the game history. |
142
+ | currentMoveIndex | number | The index of the current move in the game history. |
143
+ | isLatestMove | boolean | Whether the current position is the latest move in the game. |
144
+ | methods | Methods | The methods you can use to interact with the chess game. |
145
+ | info | Info | The info of the chess game, calculated using the chess instance using chess.js methods |
116
146
 
117
147
  #### Methods
118
148
 
119
- The `useChessGameContextContext` hook returns the following methods:
149
+ The `useChessGameContext` hook also exposes these methods:
120
150
 
121
- | Name | Type | Description |
122
- | ---------------- | --------------------------- | -------------------------------------------- |
123
- | makeMove | (move: string) => void | Makes a move in the chess game. |
124
- | setPostion | (fen: string) => void | Sets the position of the chess game. |
125
- | flipBoard | () => void | Flips the board. |
126
- | goToMove | (moveIndex: number) => void | Goes to a specific move in the game history. |
127
- | goToStart | () => void | Goes to the starting position. |
128
- | goToEnd | () => void | Goes to the latest position. |
129
- | goToPreviousMove | () => void | Goes to the previous move. |
130
- | goToNextMove | () => void | Goes to the next move. |
151
+ | Name | Type | Description |
152
+ | ---------------- | ------------------------------- | --------------------------------------------------------------- | -------------------------------------------------------------------- | --- | ------------------ | ------------------------------------------------------------------ |
153
+ | makeMove | `(move: string | { from: Square; to: Square; promotion?: "q" | "r" | "b" | "n" }) => boolean` | Attempts a move at the latest position; returns `true` if applied. |
154
+ | setPosition | `(fen: string, orientation: "w" | "b") => void` | Sets a new FEN and orientation, resets history navigation to latest. |
155
+ | flipBoard | `() => void` | Flips the board orientation. |
156
+ | goToMove | `(moveIndex: number) => void` | Jumps to a specific move index (`-1` is the starting position). |
157
+ | goToStart | `() => void` | Goes to the starting position. |
158
+ | goToEnd | `() => void` | Goes to the latest move. |
159
+ | goToPreviousMove | `() => void` | Goes to the previous move. |
160
+ | goToNextMove | `() => void` | Goes to the next move. |
131
161
 
132
162
  #### Info
133
163
 
@@ -150,11 +180,6 @@ The `useChessGameContext` hook returns the following info:
150
180
  | isDrawn | boolean | Whether the game is drawn |
151
181
  | hasPlayerWon | boolean | Whether the player (the side specified in the `orientation` prop) has won |
152
182
  | hasPlayerLost | boolean | Whether the player (the side specified in the `orientation` prop) has lost |
153
- | currentFen | string | The current FEN string representing the board position |
154
- | currentPosition | string | The current move in the game history |
155
- | currentMoveIndex | number | The index of the current move in the game history |
156
- | isLatestMove | boolean | Whether the current position is the latest move in the game |
157
- | game | Chess | The underlying Chess.js instance |
158
183
 
159
184
  ## 📝 License
160
185
 
package/dist/index.d.mts CHANGED
@@ -1,6 +1,5 @@
1
- import * as React$1 from 'react';
2
- import React__default from 'react';
3
- import { Chessboard } from 'react-chessboard';
1
+ import { ChessboardOptions } from 'react-chessboard';
2
+ import * as react from 'react';
4
3
  import * as chess_js from 'chess.js';
5
4
  import { Color, Chess } from 'chess.js';
6
5
 
@@ -11,7 +10,8 @@ type SoundsProps = {
11
10
  sounds?: Partial<Record<Sound, string>>;
12
11
  };
13
12
 
14
- interface ChessGameProps extends React__default.ComponentProps<typeof Chessboard> {
13
+ interface ChessGameProps {
14
+ options?: ChessboardOptions;
15
15
  }
16
16
 
17
17
  interface RootProps {
@@ -88,7 +88,7 @@ declare const useChessGameContext: () => {
88
88
  from: string;
89
89
  to: string;
90
90
  promotion?: string | undefined;
91
- }) => boolean;
91
+ } | null) => boolean;
92
92
  setPosition: (fen: string, orientation: chess_js.Color) => void;
93
93
  flipBoard: () => void;
94
94
  goToMove: (moveIndex: number) => void;
@@ -107,10 +107,10 @@ type KeyboardControls = Record<string, (context: ChessGameContextType) => void>;
107
107
  declare const KeyboardControls: React.FC<KeyboardControlsProps>;
108
108
 
109
109
  declare const ChessGame: {
110
- Root: React$1.FC<React$1.PropsWithChildren<RootProps>>;
111
- Board: React$1.FC<ChessGameProps>;
112
- Sounds: React$1.FC<SoundsProps>;
113
- KeyboardControls: React$1.FC<{
110
+ Root: react.FC<react.PropsWithChildren<RootProps>>;
111
+ Board: react.FC<ChessGameProps>;
112
+ Sounds: react.FC<SoundsProps>;
113
+ KeyboardControls: react.FC<{
114
114
  controls?: KeyboardControls | undefined;
115
115
  }>;
116
116
  };
package/dist/index.mjs CHANGED
@@ -224,9 +224,14 @@ var Root = ({
224
224
 
225
225
  // src/components/ChessGame/parts/Board.tsx
226
226
  import React4 from "react";
227
- import { Chessboard } from "react-chessboard";
227
+ import {
228
+ Chessboard,
229
+ defaultPieces,
230
+ chessColumnToColumnIndex
231
+ } from "react-chessboard";
228
232
 
229
233
  // src/utils/board.ts
234
+ import { merge } from "lodash";
230
235
  var LAST_MOVE_COLOR = "rgba(255, 255, 0, 0.5)";
231
236
  var CHECK_COLOR = "rgba(255, 0, 0, 0.5)";
232
237
  var getCustomSquareStyles = (game, info, activeSquare) => {
@@ -267,12 +272,28 @@ var getCustomSquareStyles = (game, info, activeSquare) => {
267
272
  }
268
273
  return customSquareStyles;
269
274
  };
275
+ var deepMergeChessboardOptions = (baseOptions, customOptions) => {
276
+ if (!customOptions) {
277
+ return { ...baseOptions };
278
+ }
279
+ const result = merge({}, baseOptions, customOptions, {
280
+ customizer: (_objValue, srcValue) => {
281
+ if (typeof srcValue === "function") {
282
+ return srcValue;
283
+ }
284
+ if (Array.isArray(srcValue)) {
285
+ return srcValue;
286
+ }
287
+ return void 0;
288
+ }
289
+ });
290
+ delete result.customizer;
291
+ return result;
292
+ };
270
293
 
271
294
  // src/components/ChessGame/parts/Board.tsx
272
- var Board = ({
273
- customSquareStyles,
274
- ...rest
275
- }) => {
295
+ var Board = ({ options = {} }) => {
296
+ var _a, _b, _c;
276
297
  const gameContext = useChessGameContext();
277
298
  if (!gameContext) {
278
299
  throw new Error("ChessGameContext not found");
@@ -282,6 +303,7 @@ var Board = ({
282
303
  currentFen,
283
304
  orientation,
284
305
  info,
306
+ isLatestMove,
285
307
  methods: { makeMove }
286
308
  } = gameContext;
287
309
  const { turn, isGameOver } = info;
@@ -322,45 +344,138 @@ var Board = ({
322
344
  });
323
345
  };
324
346
  const onPromotionPieceSelect = (piece) => {
325
- var _a;
326
- if ((promotionMove == null ? void 0 : promotionMove.from) && (promotionMove == null ? void 0 : promotionMove.to) && piece) {
327
- setPromotionMove(null);
328
- return makeMove({
347
+ if ((promotionMove == null ? void 0 : promotionMove.from) && (promotionMove == null ? void 0 : promotionMove.to)) {
348
+ makeMove({
329
349
  from: promotionMove.from,
330
350
  to: promotionMove.to,
331
- promotion: ((_a = piece == null ? void 0 : piece[1]) == null ? void 0 : _a.toLowerCase()) || "q"
351
+ promotion: piece.toLowerCase()
332
352
  });
353
+ setPromotionMove(null);
333
354
  }
334
- return true;
335
355
  };
336
- return /* @__PURE__ */ React4.createElement(
337
- Chessboard,
338
- {
339
- customSquareStyles: {
340
- ...getCustomSquareStyles(game, info, activeSquare),
341
- ...customSquareStyles
342
- },
343
- boardOrientation: orientation === "b" ? "black" : "white",
344
- position: currentFen,
345
- showPromotionDialog: !!promotionMove,
346
- onPromotionPieceSelect: promotionMove ? onPromotionPieceSelect : void 0,
347
- onPieceDragBegin: (_2, square) => {
356
+ const onSquareRightClick = () => {
357
+ setActiveSquare(null);
358
+ setPromotionMove(null);
359
+ };
360
+ const squareWidth = React4.useMemo(() => {
361
+ var _a2;
362
+ if (typeof document === "undefined") return 80;
363
+ const squareElement = document.querySelector(`[data-square]`);
364
+ return ((_a2 = squareElement == null ? void 0 : squareElement.getBoundingClientRect()) == null ? void 0 : _a2.width) ?? 80;
365
+ }, [promotionMove]);
366
+ const promotionSquareLeft = React4.useMemo(() => {
367
+ var _a2;
368
+ if (!(promotionMove == null ? void 0 : promotionMove.to)) return 0;
369
+ const column = ((_a2 = promotionMove.to.match(/^[a-h]/)) == null ? void 0 : _a2[0]) ?? "a";
370
+ return squareWidth * chessColumnToColumnIndex(
371
+ column,
372
+ 8,
373
+ orientation === "b" ? "black" : "white"
374
+ );
375
+ }, [promotionMove, squareWidth, orientation]);
376
+ const baseOptions = {
377
+ squareStyles: getCustomSquareStyles(game, info, activeSquare),
378
+ boardOrientation: orientation === "b" ? "black" : "white",
379
+ position: currentFen,
380
+ showNotation: true,
381
+ showAnimations: isLatestMove,
382
+ canDragPiece: ({ piece }) => {
383
+ if (isGameOver) return false;
384
+ return piece.pieceType[0] === turn;
385
+ },
386
+ dropSquareStyle: {
387
+ backgroundColor: "rgba(255, 255, 0, 0.4)"
388
+ },
389
+ onPieceDrag: ({ piece, square }) => {
390
+ if (piece.pieceType[0] === turn) {
348
391
  setActiveSquare(square);
349
- },
350
- onPieceDragEnd: () => {
351
- setActiveSquare(null);
352
- },
353
- onPieceDrop: (sourceSquare, targetSquare, piece) => makeMove({
392
+ }
393
+ },
394
+ onPieceDrop: ({ sourceSquare, targetSquare }) => {
395
+ setActiveSquare(null);
396
+ const moveData = {
354
397
  from: sourceSquare,
355
- to: targetSquare,
356
- promotion: (piece == null ? void 0 : piece[1].toLowerCase()) || "q"
357
- }),
358
- onSquareClick,
359
- areArrowsAllowed: true,
360
- animationDuration: game.history().length === 0 ? 0 : 300,
361
- ...rest
398
+ to: targetSquare
399
+ };
400
+ if (requiresPromotion(game, { ...moveData, promotion: "q" })) {
401
+ setPromotionMove(moveData);
402
+ return false;
403
+ }
404
+ return makeMove(moveData);
405
+ },
406
+ onSquareClick: ({ square }) => {
407
+ if (square.match(/^[a-h][1-8]$/)) {
408
+ onSquareClick(square);
409
+ }
410
+ },
411
+ onSquareRightClick,
412
+ allowDrawingArrows: true,
413
+ animationDurationInMs: game.history().length === 0 ? 0 : 300
414
+ };
415
+ const mergedOptions = deepMergeChessboardOptions(baseOptions, options);
416
+ return /* @__PURE__ */ React4.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React4.createElement(Chessboard, { options: mergedOptions }), promotionMove && /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement(
417
+ "div",
418
+ {
419
+ onClick: () => setPromotionMove(null),
420
+ onContextMenu: (e) => {
421
+ e.preventDefault();
422
+ setPromotionMove(null);
423
+ },
424
+ style: {
425
+ position: "absolute",
426
+ top: 0,
427
+ left: 0,
428
+ right: 0,
429
+ bottom: 0,
430
+ backgroundColor: "rgba(0, 0, 0, 0.1)",
431
+ zIndex: 1e3
432
+ }
362
433
  }
363
- );
434
+ ), /* @__PURE__ */ React4.createElement(
435
+ "div",
436
+ {
437
+ style: {
438
+ position: "absolute",
439
+ top: ((_b = (_a = promotionMove.to) == null ? void 0 : _a[1]) == null ? void 0 : _b.includes("8")) ? 0 : "auto",
440
+ bottom: ((_c = promotionMove.to) == null ? void 0 : _c[1].includes("1")) ? 0 : "auto",
441
+ left: promotionSquareLeft,
442
+ backgroundColor: "white",
443
+ width: squareWidth,
444
+ zIndex: 1001,
445
+ display: "flex",
446
+ flexDirection: "column",
447
+ boxShadow: "0 0 10px 0 rgba(0, 0, 0, 0.5)"
448
+ }
449
+ },
450
+ ["q", "r", "n", "b"].map((piece) => /* @__PURE__ */ React4.createElement(
451
+ "button",
452
+ {
453
+ key: piece,
454
+ onClick: () => onPromotionPieceSelect(piece),
455
+ onContextMenu: (e) => {
456
+ e.preventDefault();
457
+ },
458
+ style: {
459
+ width: "100%",
460
+ aspectRatio: "1",
461
+ display: "flex",
462
+ alignItems: "center",
463
+ justifyContent: "center",
464
+ padding: 0,
465
+ border: "none",
466
+ cursor: "pointer",
467
+ backgroundColor: "white"
468
+ },
469
+ onMouseEnter: (e) => {
470
+ e.currentTarget.style.backgroundColor = "#f0f0f0";
471
+ },
472
+ onMouseLeave: (e) => {
473
+ e.currentTarget.style.backgroundColor = "white";
474
+ }
475
+ },
476
+ defaultPieces[`${turn}${piece.toUpperCase()}`]()
477
+ ))
478
+ )));
364
479
  };
365
480
 
366
481
  // src/components/ChessGame/parts/Sounds.tsx
@@ -377,26 +492,31 @@ var defaultSounds = {
377
492
 
378
493
  // src/hooks/useBoardSounds.ts
379
494
  import { useEffect as useEffect2 } from "react";
495
+ var playSound = async (audioElement) => {
496
+ try {
497
+ await audioElement.play();
498
+ } catch (error) {
499
+ console.warn("Failed to play sound:", error.message);
500
+ }
501
+ };
380
502
  var useBoardSounds = (sounds) => {
381
503
  const {
382
504
  info: { lastMove, isCheckmate }
383
505
  } = useChessGameContext();
384
506
  useEffect2(() => {
385
- var _a, _b, _c;
386
507
  if (Object.keys(sounds).length === 0) {
387
508
  return;
388
509
  }
389
- if (isCheckmate) {
390
- (_a = sounds.gameOver) == null ? void 0 : _a.play();
510
+ if (isCheckmate && sounds.gameOver) {
511
+ playSound(sounds.gameOver);
391
512
  return;
392
513
  }
393
- if (lastMove == null ? void 0 : lastMove.captured) {
394
- (_b = sounds.capture) == null ? void 0 : _b.play();
514
+ if ((lastMove == null ? void 0 : lastMove.captured) && sounds.capture) {
515
+ playSound(sounds.capture);
395
516
  return;
396
517
  }
397
- if (lastMove) {
398
- (_c = sounds.move) == null ? void 0 : _c.play();
399
- return;
518
+ if (lastMove && sounds.move) {
519
+ playSound(sounds.move);
400
520
  }
401
521
  }, [lastMove]);
402
522
  };