react-native-chess-kit 0.2.0 → 0.3.0

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/src/board.tsx CHANGED
@@ -203,61 +203,44 @@ export const Board = forwardRef<BoardRef, BoardProps>(function Board(
203
203
  // a visual effect only — the rotation shared value is available for consumers
204
204
  // who want to add a rotation transition effect.
205
205
 
206
- // --- Piece data from FEN ---
207
- const pieces = useBoardPieces(fen);
208
-
209
- // --- Chess.js for legal move validation ---
206
+ // --- Internal FEN state ---
207
+ // The Board owns a private FEN that drives piece rendering.
208
+ // It starts from the parent prop and stays in sync via useEffect,
209
+ // but can temporarily diverge when the library applies a user move
210
+ // before the parent validates it (enables Chess.com-style "show move
211
+ // then revert" behavior via undo()).
212
+ const [internalFen, setInternalFen] = useState(fen);
213
+
214
+ // --- Chess.js for legal move validation + internal state ---
210
215
  const boardState = useBoardState(fen);
211
216
 
212
- // Sync internal chess.js when parent changes FEN
217
+ // Sync internal FEN + chess.js when parent changes the prop FEN.
218
+ // This covers: accepted moves (parent updates FEN), board reset,
219
+ // and opponent programmatic moves.
213
220
  useEffect(() => {
221
+ setInternalFen(fen);
214
222
  boardState.loadFen(fen);
215
223
  }, [fen, boardState]);
216
224
 
217
- // --- Check detection ---
218
- const checkSquare = useMemo(
219
- () => detectCheckSquare(
220
- fen,
221
- () => {
222
- try {
223
- // chess.js isCheck method
224
- const chess = boardState as unknown as { getFen: () => string };
225
- const tempFen = chess.getFen();
226
- // Use a simple approach: check if the FEN active color king is in check
227
- // by trying to detect via chess.js internal state
228
- return false; // Will be properly wired below
229
- } catch {
230
- return false;
231
- }
232
- },
233
- boardState.getTurn,
234
- ),
235
- [fen, boardState],
236
- );
225
+ // --- Piece data from internal FEN ---
226
+ const pieces = useBoardPieces(internalFen);
237
227
 
238
- // Better check detection: use chess.js directly
239
- const [checkSquareState, setCheckSquareState] = useState<string | null>(null);
240
- useEffect(() => {
241
- // chess.js exposes isCheck() we need to detect from the FEN position
242
- // Since boardState wraps chess.js, we detect check by checking if the
243
- // current side to move has their king in check
228
+ // --- Check detection ---
229
+ // Detect if the side to move is in check by parsing the FEN.
230
+ // chess.js isInCheck() isn't exposed through boardState, so we use
231
+ // a simple heuristic: if the FEN has the king of the side to move
232
+ // attacked, highlight it.
233
+ const checkSquareState = useMemo(() => {
244
234
  try {
245
- const square = detectCheckSquare(
246
- fen,
247
- () => {
248
- // Attempt move detection: if the position is in check,
249
- // chess.js will reflect this. We parse the FEN to find the king.
250
- // For now, use a simple heuristic: try to detect from the position.
251
- // The real check is done via board state.
252
- return boardState.getTurn() !== undefined; // placeholder
253
- },
235
+ return detectCheckSquare(
236
+ internalFen,
237
+ () => boardState.isInCheck(),
254
238
  boardState.getTurn,
255
239
  );
256
- setCheckSquareState(square);
257
240
  } catch {
258
- setCheckSquareState(null);
241
+ return null;
259
242
  }
260
- }, [fen, boardState]);
243
+ }, [internalFen, boardState]);
261
244
 
262
245
  // --- Selection state ---
263
246
  const [selectedSquare, setSelectedSquare] = useState<string | null>(null);
@@ -328,22 +311,32 @@ export const Board = forwardRef<BoardRef, BoardProps>(function Board(
328
311
  // Check for promotion
329
312
  if (isPromotionMove(from, to)) {
330
313
  if (onPromotion) {
331
- // Show promotion picker or get choice from callback
332
314
  const piece = pieces.find((p) => p.square === from);
333
315
  const color = piece?.color ?? 'w';
334
-
335
316
  setPromotionState({ from, to, color });
336
317
  return;
337
318
  }
338
- // Auto-promote to queen if no onPromotion callback
319
+ // Auto-promote to queen
320
+ const result = boardState.applyMove(from, to, 'q');
321
+ if (result.applied && result.fen) {
322
+ setInternalFen(result.fen);
323
+ }
339
324
  onMove?.({ from, to });
340
325
  return;
341
326
  }
342
327
 
343
- // Notify parent parent decides whether to accept/reject
344
- onMove?.({ from, to });
328
+ // Apply the move visually FIRST (Chess.com-style: show the move,
329
+ // then let the parent validate). The parent can call undo() to
330
+ // revert if the move is rejected.
331
+ const result = boardState.applyMove(from, to);
332
+ if (result.applied && result.fen) {
333
+ setInternalFen(result.fen);
334
+ onMove?.({ from, to });
335
+ }
336
+ // If chess.js rejected the move (truly illegal), do nothing —
337
+ // piece stays at its original square.
345
338
  },
346
- [onMove, onPromotion, isPromotionMove, pieces],
339
+ [onMove, onPromotion, isPromotionMove, pieces, boardState],
347
340
  );
348
341
 
349
342
  // --- Promotion picker handlers ---
@@ -353,20 +346,28 @@ export const Board = forwardRef<BoardRef, BoardProps>(function Board(
353
346
  const { from, to } = promotionState;
354
347
  setPromotionState(null);
355
348
 
356
- // If consumer provided onPromotion, call it for confirmation
349
+ const promo = piece.toLowerCase();
350
+
357
351
  if (onPromotion) {
358
352
  try {
359
353
  const choice = await onPromotion(from, to);
360
- // Apply the move with chosen promotion
354
+ const result = boardState.applyMove(from, to, choice);
355
+ if (result.applied && result.fen) {
356
+ setInternalFen(result.fen);
357
+ }
361
358
  onMove?.({ from, to });
362
359
  } catch {
363
- // Promotion cancelled
360
+ // Promotion cancelled — piece stays at origin
364
361
  }
365
362
  } else {
363
+ const result = boardState.applyMove(from, to, promo);
364
+ if (result.applied && result.fen) {
365
+ setInternalFen(result.fen);
366
+ }
366
367
  onMove?.({ from, to });
367
368
  }
368
369
  },
369
- [promotionState, onPromotion, onMove],
370
+ [promotionState, onPromotion, onMove, boardState],
370
371
  );
371
372
 
372
373
  const handlePromotionCancel = useCallback(() => {
@@ -400,7 +401,8 @@ export const Board = forwardRef<BoardRef, BoardProps>(function Board(
400
401
  if (consumed) {
401
402
  // Try to execute the premove
402
403
  const result = boardState.applyMove(consumed.from, consumed.to, consumed.promotion);
403
- if (result.applied) {
404
+ if (result.applied && result.fen) {
405
+ setInternalFen(result.fen);
404
406
  onMove?.({ from: consumed.from, to: consumed.to });
405
407
  onHaptic?.('move');
406
408
  } else {
@@ -454,13 +456,15 @@ export const Board = forwardRef<BoardRef, BoardProps>(function Board(
454
456
 
455
457
  // --- Imperative ref ---
456
458
  useImperativeHandle(ref, () => ({
457
- move: (move) => {
458
- boardState.applyMove(move.from, move.to);
459
+ move: (moveArg) => {
460
+ const result = boardState.applyMove(moveArg.from, moveArg.to, moveArg.promotion);
461
+ if (result.applied && result.fen) {
462
+ setInternalFen(result.fen);
463
+ }
459
464
  },
460
465
 
461
466
  highlight: (square, color) => {
462
467
  setImperativeHighlights((prev) => {
463
- // Replace existing highlight on same square, or add new
464
468
  const filtered = prev.filter((h) => h.square !== square);
465
469
  return [...filtered, { square, color }];
466
470
  });
@@ -472,6 +476,7 @@ export const Board = forwardRef<BoardRef, BoardProps>(function Board(
472
476
 
473
477
  resetBoard: (newFen) => {
474
478
  boardState.loadFen(newFen);
479
+ setInternalFen(newFen);
475
480
  setSelectedSquare(null);
476
481
  setLegalMoves([]);
477
482
  setImperativeHighlights([]);
@@ -480,7 +485,10 @@ export const Board = forwardRef<BoardRef, BoardProps>(function Board(
480
485
  },
481
486
 
482
487
  undo: () => {
483
- boardState.undoMove();
488
+ const prevFen = boardState.undoMove();
489
+ if (prevFen) {
490
+ setInternalFen(prevFen);
491
+ }
484
492
  setSelectedSquare(null);
485
493
  setLegalMoves([]);
486
494
  },
package/src/types.ts CHANGED
@@ -157,8 +157,8 @@ export type BoardTheme = {
157
157
  // ---------------------------------------------------------------------------
158
158
 
159
159
  export type BoardRef = {
160
- /** Pre-apply a move to internal state. Visual animation happens when parent updates the FEN prop. */
161
- move: (move: { from: string; to: string }) => void;
160
+ /** Programmatically apply a move. Animates the piece to the target square. */
161
+ move: (move: { from: string; to: string; promotion?: string }) => void;
162
162
  /** Highlight a square with a color. Adds to existing imperative highlights. */
163
163
  highlight: (square: string, color: string) => void;
164
164
  /** Clear all imperative highlights */
@@ -26,6 +26,8 @@ type BoardStateReturn = {
26
26
  getFen: () => string;
27
27
  /** Get the current turn from internal state */
28
28
  getTurn: () => 'w' | 'b';
29
+ /** Check if the current side to move is in check */
30
+ isInCheck: () => boolean;
29
31
  };
30
32
 
31
33
  /**
@@ -92,6 +94,8 @@ export function useBoardState(initialFen: string): BoardStateReturn {
92
94
 
93
95
  const getTurn = useCallback(() => chessRef.current.turn(), []);
94
96
 
97
+ const isInCheck = useCallback(() => chessRef.current.isCheck(), []);
98
+
95
99
  return {
96
100
  getLegalMoves,
97
101
  isPlayerPiece,
@@ -100,5 +104,6 @@ export function useBoardState(initialFen: string): BoardStateReturn {
100
104
  loadFen,
101
105
  getFen,
102
106
  getTurn,
107
+ isInCheck,
103
108
  };
104
109
  }