react-native-chess-kit 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.
Files changed (82) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +168 -168
  3. package/lib/commonjs/board-annotations.js +8 -8
  4. package/lib/commonjs/board-arrows.js +7 -7
  5. package/lib/commonjs/board-background.js +5 -5
  6. package/lib/commonjs/board-coordinates.js +78 -11
  7. package/lib/commonjs/board-coordinates.js.map +1 -1
  8. package/lib/commonjs/board-drag-ghost.js +10 -10
  9. package/lib/commonjs/board-highlights.js +15 -15
  10. package/lib/commonjs/board-legal-dots.js +5 -5
  11. package/lib/commonjs/board-piece.js +25 -25
  12. package/lib/commonjs/board-pieces.js +6 -6
  13. package/lib/commonjs/board.js +76 -35
  14. package/lib/commonjs/board.js.map +1 -1
  15. package/lib/commonjs/constants.js +4 -1
  16. package/lib/commonjs/constants.js.map +1 -1
  17. package/lib/commonjs/index.js.map +1 -1
  18. package/lib/commonjs/promotion-picker.js +8 -8
  19. package/lib/commonjs/static-board.js +51 -16
  20. package/lib/commonjs/static-board.js.map +1 -1
  21. package/lib/commonjs/use-board-gesture.js +52 -33
  22. package/lib/commonjs/use-board-gesture.js.map +1 -1
  23. package/lib/commonjs/use-board-pieces.js +15 -15
  24. package/lib/commonjs/use-board-state.js +8 -8
  25. package/lib/commonjs/use-premove.js +12 -12
  26. package/lib/module/board-annotations.js +8 -8
  27. package/lib/module/board-arrows.js +7 -7
  28. package/lib/module/board-background.js +5 -5
  29. package/lib/module/board-coordinates.js +79 -12
  30. package/lib/module/board-coordinates.js.map +1 -1
  31. package/lib/module/board-drag-ghost.js +10 -10
  32. package/lib/module/board-highlights.js +15 -15
  33. package/lib/module/board-legal-dots.js +5 -5
  34. package/lib/module/board-piece.js +25 -25
  35. package/lib/module/board-pieces.js +6 -6
  36. package/lib/module/board.js +77 -36
  37. package/lib/module/board.js.map +1 -1
  38. package/lib/module/constants.js +3 -0
  39. package/lib/module/constants.js.map +1 -1
  40. package/lib/module/index.js.map +1 -1
  41. package/lib/module/promotion-picker.js +8 -8
  42. package/lib/module/static-board.js +52 -17
  43. package/lib/module/static-board.js.map +1 -1
  44. package/lib/module/use-board-gesture.js +52 -33
  45. package/lib/module/use-board-gesture.js.map +1 -1
  46. package/lib/module/use-board-pieces.js +15 -15
  47. package/lib/module/use-board-state.js +8 -8
  48. package/lib/module/use-premove.js +12 -12
  49. package/lib/typescript/board-coordinates.d.ts +10 -4
  50. package/lib/typescript/board-coordinates.d.ts.map +1 -1
  51. package/lib/typescript/board.d.ts.map +1 -1
  52. package/lib/typescript/constants.d.ts +2 -0
  53. package/lib/typescript/constants.d.ts.map +1 -1
  54. package/lib/typescript/index.d.ts +1 -1
  55. package/lib/typescript/index.d.ts.map +1 -1
  56. package/lib/typescript/static-board.d.ts.map +1 -1
  57. package/lib/typescript/types.d.ts +23 -3
  58. package/lib/typescript/types.d.ts.map +1 -1
  59. package/lib/typescript/use-board-gesture.d.ts.map +1 -1
  60. package/package.json +1 -1
  61. package/src/board-annotations.tsx +147 -147
  62. package/src/board-arrows.tsx +197 -197
  63. package/src/board-background.tsx +46 -46
  64. package/src/board-coordinates.tsx +192 -98
  65. package/src/board-drag-ghost.tsx +132 -132
  66. package/src/board-highlights.tsx +226 -226
  67. package/src/board-legal-dots.tsx +73 -73
  68. package/src/board-piece.tsx +160 -160
  69. package/src/board-pieces.tsx +63 -63
  70. package/src/board.tsx +685 -641
  71. package/src/constants.ts +103 -100
  72. package/src/index.ts +101 -100
  73. package/src/pieces/default-pieces.tsx +383 -383
  74. package/src/pieces/index.ts +1 -1
  75. package/src/promotion-picker.tsx +147 -147
  76. package/src/static-board.tsx +187 -150
  77. package/src/themes.ts +129 -129
  78. package/src/types.ts +373 -352
  79. package/src/use-board-gesture.ts +429 -412
  80. package/src/use-board-pieces.ts +158 -158
  81. package/src/use-board-state.ts +111 -111
  82. package/src/use-premove.ts +59 -59
@@ -1,158 +1,158 @@
1
- import { useMemo, useRef } from 'react';
2
-
3
- import type { ChessColor, BoardPiece, ParsedPiece } from './types';
4
-
5
- const FEN_PIECE_MAP: Record<string, { code: string; color: 'w' | 'b' }> = {
6
- p: { code: 'bp', color: 'b' },
7
- r: { code: 'br', color: 'b' },
8
- n: { code: 'bn', color: 'b' },
9
- b: { code: 'bb', color: 'b' },
10
- q: { code: 'bq', color: 'b' },
11
- k: { code: 'bk', color: 'b' },
12
- P: { code: 'wp', color: 'w' },
13
- R: { code: 'wr', color: 'w' },
14
- N: { code: 'wn', color: 'w' },
15
- B: { code: 'wb', color: 'w' },
16
- Q: { code: 'wq', color: 'w' },
17
- K: { code: 'wk', color: 'w' },
18
- };
19
-
20
- const FILES = 'abcdefgh';
21
-
22
- /**
23
- * Parse the piece-placement part of a FEN string into an array of pieces.
24
- * Pure function — no React dependencies, suitable for worklets if needed.
25
- */
26
- function parseFenPieces(fen: string): ParsedPiece[] {
27
- const placement = fen.split(' ')[0];
28
- const ranks = placement.split('/');
29
- const pieces: ParsedPiece[] = [];
30
-
31
- for (let rankIdx = 0; rankIdx < ranks.length; rankIdx++) {
32
- const rank = ranks[rankIdx];
33
- let fileIdx = 0;
34
-
35
- for (const char of rank) {
36
- if (char >= '1' && char <= '8') {
37
- fileIdx += parseInt(char, 10);
38
- continue;
39
- }
40
-
41
- const mapping = FEN_PIECE_MAP[char];
42
- if (mapping) {
43
- // FEN ranks are from rank 8 (index 0) down to rank 1 (index 7)
44
- const square = `${FILES[fileIdx]}${8 - rankIdx}`;
45
- pieces.push({ code: mapping.code, square, color: mapping.color });
46
- }
47
- fileIdx++;
48
- }
49
- }
50
-
51
- return pieces;
52
- }
53
-
54
- /**
55
- * Convert square notation to pixel coordinates (top-left corner).
56
- * Orientation-aware: flips the board when playing as black.
57
- */
58
- export function squareToXY(
59
- square: string,
60
- squareSize: number,
61
- orientation: ChessColor,
62
- ): { x: number; y: number } {
63
- 'worklet';
64
- const fileIdx = square.charCodeAt(0) - 97; // 'a'=0 .. 'h'=7
65
- const rankIdx = parseInt(square[1], 10) - 1; // '1'=0 .. '8'=7
66
-
67
- const col = orientation === 'white' ? fileIdx : 7 - fileIdx;
68
- const row = orientation === 'white' ? 7 - rankIdx : rankIdx;
69
-
70
- return { x: col * squareSize, y: row * squareSize };
71
- }
72
-
73
- /**
74
- * Convert pixel coordinates to a square notation string.
75
- * Clamps to board bounds. Orientation-aware.
76
- */
77
- export function xyToSquare(
78
- x: number,
79
- y: number,
80
- squareSize: number,
81
- orientation: ChessColor,
82
- ): string {
83
- 'worklet';
84
- const col = Math.max(0, Math.min(7, Math.floor(x / squareSize)));
85
- const row = Math.max(0, Math.min(7, Math.floor(y / squareSize)));
86
-
87
- const fileIdx = orientation === 'white' ? col : 7 - col;
88
- const rankIdx = orientation === 'white' ? 7 - row : row;
89
-
90
- // String.fromCharCode not available in worklets — use lookup
91
- const files = 'abcdefgh';
92
- return `${files[fileIdx]}${rankIdx + 1}`;
93
- }
94
-
95
- /**
96
- * Manages the piece list derived from FEN, with stable IDs for React keys.
97
- *
98
- * Stable IDs prevent unmount/remount cycles when pieces change position.
99
- * A piece keeps its ID as long as it exists on the board — only capture
100
- * (removal) or promotion (code change) creates a new ID.
101
- */
102
- export function useBoardPieces(fen: string): BoardPiece[] {
103
- // Track piece-code counters across renders for stable ID assignment
104
- const idCounterRef = useRef<Record<string, number>>({});
105
- const prevPiecesRef = useRef<BoardPiece[]>([]);
106
-
107
- return useMemo(() => {
108
- const parsed = parseFenPieces(fen);
109
- const prev = prevPiecesRef.current;
110
- const prevBySquare = new Map(prev.map((p) => [p.square, p]));
111
-
112
- // Try to reuse IDs from previous render:
113
- // 1. Same code on same square -> keep ID (piece didn't move)
114
- // 2. Same code moved to a new square -> find unmatched previous piece of same code
115
- const usedPrevIds = new Set<string>();
116
- const result: BoardPiece[] = [];
117
-
118
- // First pass: exact square matches (piece stayed or appeared on same square)
119
- const unmatched: ParsedPiece[] = [];
120
- for (const p of parsed) {
121
- const existing = prevBySquare.get(p.square);
122
- if (existing && existing.code === p.code && !usedPrevIds.has(existing.id)) {
123
- usedPrevIds.add(existing.id);
124
- result.push({ ...p, id: existing.id });
125
- } else {
126
- unmatched.push(p);
127
- }
128
- }
129
-
130
- // Second pass: match unmatched pieces by code to previous pieces that moved
131
- for (const p of unmatched) {
132
- let matchedId: string | null = null;
133
-
134
- for (const prevPiece of prev) {
135
- if (
136
- prevPiece.code === p.code &&
137
- !usedPrevIds.has(prevPiece.id)
138
- ) {
139
- matchedId = prevPiece.id;
140
- usedPrevIds.add(prevPiece.id);
141
- break;
142
- }
143
- }
144
-
145
- if (matchedId) {
146
- result.push({ ...p, id: matchedId });
147
- } else {
148
- // New piece (promotion, or first render) — assign fresh ID
149
- const counter = idCounterRef.current;
150
- counter[p.code] = (counter[p.code] ?? 0) + 1;
151
- result.push({ ...p, id: `${p.code}-${counter[p.code]}` });
152
- }
153
- }
154
-
155
- prevPiecesRef.current = result;
156
- return result;
157
- }, [fen]);
158
- }
1
+ import { useMemo, useRef } from 'react';
2
+
3
+ import type { ChessColor, BoardPiece, ParsedPiece } from './types';
4
+
5
+ const FEN_PIECE_MAP: Record<string, { code: string; color: 'w' | 'b' }> = {
6
+ p: { code: 'bp', color: 'b' },
7
+ r: { code: 'br', color: 'b' },
8
+ n: { code: 'bn', color: 'b' },
9
+ b: { code: 'bb', color: 'b' },
10
+ q: { code: 'bq', color: 'b' },
11
+ k: { code: 'bk', color: 'b' },
12
+ P: { code: 'wp', color: 'w' },
13
+ R: { code: 'wr', color: 'w' },
14
+ N: { code: 'wn', color: 'w' },
15
+ B: { code: 'wb', color: 'w' },
16
+ Q: { code: 'wq', color: 'w' },
17
+ K: { code: 'wk', color: 'w' },
18
+ };
19
+
20
+ const FILES = 'abcdefgh';
21
+
22
+ /**
23
+ * Parse the piece-placement part of a FEN string into an array of pieces.
24
+ * Pure function — no React dependencies, suitable for worklets if needed.
25
+ */
26
+ function parseFenPieces(fen: string): ParsedPiece[] {
27
+ const placement = fen.split(' ')[0];
28
+ const ranks = placement.split('/');
29
+ const pieces: ParsedPiece[] = [];
30
+
31
+ for (let rankIdx = 0; rankIdx < ranks.length; rankIdx++) {
32
+ const rank = ranks[rankIdx];
33
+ let fileIdx = 0;
34
+
35
+ for (const char of rank) {
36
+ if (char >= '1' && char <= '8') {
37
+ fileIdx += parseInt(char, 10);
38
+ continue;
39
+ }
40
+
41
+ const mapping = FEN_PIECE_MAP[char];
42
+ if (mapping) {
43
+ // FEN ranks are from rank 8 (index 0) down to rank 1 (index 7)
44
+ const square = `${FILES[fileIdx]}${8 - rankIdx}`;
45
+ pieces.push({ code: mapping.code, square, color: mapping.color });
46
+ }
47
+ fileIdx++;
48
+ }
49
+ }
50
+
51
+ return pieces;
52
+ }
53
+
54
+ /**
55
+ * Convert square notation to pixel coordinates (top-left corner).
56
+ * Orientation-aware: flips the board when playing as black.
57
+ */
58
+ export function squareToXY(
59
+ square: string,
60
+ squareSize: number,
61
+ orientation: ChessColor,
62
+ ): { x: number; y: number } {
63
+ 'worklet';
64
+ const fileIdx = square.charCodeAt(0) - 97; // 'a'=0 .. 'h'=7
65
+ const rankIdx = parseInt(square[1], 10) - 1; // '1'=0 .. '8'=7
66
+
67
+ const col = orientation === 'white' ? fileIdx : 7 - fileIdx;
68
+ const row = orientation === 'white' ? 7 - rankIdx : rankIdx;
69
+
70
+ return { x: col * squareSize, y: row * squareSize };
71
+ }
72
+
73
+ /**
74
+ * Convert pixel coordinates to a square notation string.
75
+ * Clamps to board bounds. Orientation-aware.
76
+ */
77
+ export function xyToSquare(
78
+ x: number,
79
+ y: number,
80
+ squareSize: number,
81
+ orientation: ChessColor,
82
+ ): string {
83
+ 'worklet';
84
+ const col = Math.max(0, Math.min(7, Math.floor(x / squareSize)));
85
+ const row = Math.max(0, Math.min(7, Math.floor(y / squareSize)));
86
+
87
+ const fileIdx = orientation === 'white' ? col : 7 - col;
88
+ const rankIdx = orientation === 'white' ? 7 - row : row;
89
+
90
+ // String.fromCharCode not available in worklets — use lookup
91
+ const files = 'abcdefgh';
92
+ return `${files[fileIdx]}${rankIdx + 1}`;
93
+ }
94
+
95
+ /**
96
+ * Manages the piece list derived from FEN, with stable IDs for React keys.
97
+ *
98
+ * Stable IDs prevent unmount/remount cycles when pieces change position.
99
+ * A piece keeps its ID as long as it exists on the board — only capture
100
+ * (removal) or promotion (code change) creates a new ID.
101
+ */
102
+ export function useBoardPieces(fen: string): BoardPiece[] {
103
+ // Track piece-code counters across renders for stable ID assignment
104
+ const idCounterRef = useRef<Record<string, number>>({});
105
+ const prevPiecesRef = useRef<BoardPiece[]>([]);
106
+
107
+ return useMemo(() => {
108
+ const parsed = parseFenPieces(fen);
109
+ const prev = prevPiecesRef.current;
110
+ const prevBySquare = new Map(prev.map((p) => [p.square, p]));
111
+
112
+ // Try to reuse IDs from previous render:
113
+ // 1. Same code on same square -> keep ID (piece didn't move)
114
+ // 2. Same code moved to a new square -> find unmatched previous piece of same code
115
+ const usedPrevIds = new Set<string>();
116
+ const result: BoardPiece[] = [];
117
+
118
+ // First pass: exact square matches (piece stayed or appeared on same square)
119
+ const unmatched: ParsedPiece[] = [];
120
+ for (const p of parsed) {
121
+ const existing = prevBySquare.get(p.square);
122
+ if (existing && existing.code === p.code && !usedPrevIds.has(existing.id)) {
123
+ usedPrevIds.add(existing.id);
124
+ result.push({ ...p, id: existing.id });
125
+ } else {
126
+ unmatched.push(p);
127
+ }
128
+ }
129
+
130
+ // Second pass: match unmatched pieces by code to previous pieces that moved
131
+ for (const p of unmatched) {
132
+ let matchedId: string | null = null;
133
+
134
+ for (const prevPiece of prev) {
135
+ if (
136
+ prevPiece.code === p.code &&
137
+ !usedPrevIds.has(prevPiece.id)
138
+ ) {
139
+ matchedId = prevPiece.id;
140
+ usedPrevIds.add(prevPiece.id);
141
+ break;
142
+ }
143
+ }
144
+
145
+ if (matchedId) {
146
+ result.push({ ...p, id: matchedId });
147
+ } else {
148
+ // New piece (promotion, or first render) — assign fresh ID
149
+ const counter = idCounterRef.current;
150
+ counter[p.code] = (counter[p.code] ?? 0) + 1;
151
+ result.push({ ...p, id: `${p.code}-${counter[p.code]}` });
152
+ }
153
+ }
154
+
155
+ prevPiecesRef.current = result;
156
+ return result;
157
+ }, [fen]);
158
+ }
@@ -1,111 +1,111 @@
1
- import { useRef, useCallback, useMemo } from 'react';
2
- import { Chess } from 'chess.js';
3
- import type { Square } from 'chess.js';
4
-
5
- import type { ChessColor, BoardPiece, LegalMoveTarget } from './types';
6
-
7
- type MoveResult = {
8
- /** Whether the move was applied to the internal chess.js instance */
9
- applied: boolean;
10
- /** The new FEN after the move (if applied) */
11
- fen?: string;
12
- };
13
-
14
- type BoardStateReturn = {
15
- /** Get legal moves for a piece on the given square */
16
- getLegalMoves: (square: string) => LegalMoveTarget[];
17
- /** Check if a given square has a piece belonging to the active player */
18
- isPlayerPiece: (square: string, pieces: BoardPiece[], player: ChessColor | 'both') => boolean;
19
- /** Apply a move to the internal chess state. Returns the new FEN if valid. */
20
- applyMove: (from: string, to: string, promotion?: string) => MoveResult;
21
- /** Undo the last move on the internal chess state */
22
- undoMove: () => string | null;
23
- /** Load a new FEN into the internal chess state */
24
- loadFen: (fen: string) => void;
25
- /** Get the current FEN from internal state */
26
- getFen: () => string;
27
- /** Get the current turn from internal state */
28
- getTurn: () => 'w' | 'b';
29
- /** Check if the current side to move is in check */
30
- isInCheck: () => boolean;
31
- };
32
-
33
- /**
34
- * Manages the internal chess.js instance for legal move validation.
35
- *
36
- * This mirrors the visual board state. When the parent passes a new FEN,
37
- * the internal chess.js is synced. Legal move queries and move application
38
- * happen against this instance.
39
- *
40
- * The chess.js instance lives in a ref — no React state, no re-renders.
41
- */
42
- export function useBoardState(initialFen: string): BoardStateReturn {
43
- const chessRef = useRef<Chess>(null!);
44
- if (!chessRef.current) chessRef.current = new Chess(initialFen);
45
-
46
- const getLegalMoves = useCallback((square: string): LegalMoveTarget[] => {
47
- try {
48
- const moves = chessRef.current.moves({ square: square as Square, verbose: true });
49
- return moves.map((m) => ({
50
- square: m.to,
51
- isCapture: m.captured !== undefined,
52
- }));
53
- } catch {
54
- return [];
55
- }
56
- }, []);
57
-
58
- const isPlayerPiece = useCallback(
59
- (square: string, pieces: BoardPiece[], player: ChessColor | 'both'): boolean => {
60
- const piece = pieces.find((p) => p.square === square);
61
- if (!piece) return false;
62
-
63
- if (player === 'both') return true;
64
-
65
- const pieceColor: ChessColor = piece.color === 'w' ? 'white' : 'black';
66
- return pieceColor === player;
67
- },
68
- [],
69
- );
70
-
71
- const applyMove = useCallback((from: string, to: string, promotion?: string): MoveResult => {
72
- try {
73
- chessRef.current.move({
74
- from: from as Square,
75
- to: to as Square,
76
- promotion: promotion as 'q' | 'r' | 'b' | 'n' | undefined,
77
- });
78
- return { applied: true, fen: chessRef.current.fen() };
79
- } catch {
80
- return { applied: false };
81
- }
82
- }, []);
83
-
84
- const undoMove = useCallback((): string | null => {
85
- const result = chessRef.current.undo();
86
- return result ? chessRef.current.fen() : null;
87
- }, []);
88
-
89
- const loadFen = useCallback((fen: string) => {
90
- chessRef.current.load(fen);
91
- }, []);
92
-
93
- const getFen = useCallback(() => chessRef.current.fen(), []);
94
-
95
- const getTurn = useCallback(() => chessRef.current.turn(), []);
96
-
97
- const isInCheck = useCallback(() => chessRef.current.isCheck(), []);
98
-
99
- // Stable reference so Board's useEffect([fen, boardState]) only re-runs
100
- // when the fen prop changes — not on every render.
101
- return useMemo(() => ({
102
- getLegalMoves,
103
- isPlayerPiece,
104
- applyMove,
105
- undoMove,
106
- loadFen,
107
- getFen,
108
- getTurn,
109
- isInCheck,
110
- }), [getLegalMoves, isPlayerPiece, applyMove, undoMove, loadFen, getFen, getTurn, isInCheck]);
111
- }
1
+ import { useRef, useCallback, useMemo } from 'react';
2
+ import { Chess } from 'chess.js';
3
+ import type { Square } from 'chess.js';
4
+
5
+ import type { ChessColor, BoardPiece, LegalMoveTarget } from './types';
6
+
7
+ type MoveResult = {
8
+ /** Whether the move was applied to the internal chess.js instance */
9
+ applied: boolean;
10
+ /** The new FEN after the move (if applied) */
11
+ fen?: string;
12
+ };
13
+
14
+ type BoardStateReturn = {
15
+ /** Get legal moves for a piece on the given square */
16
+ getLegalMoves: (square: string) => LegalMoveTarget[];
17
+ /** Check if a given square has a piece belonging to the active player */
18
+ isPlayerPiece: (square: string, pieces: BoardPiece[], player: ChessColor | 'both') => boolean;
19
+ /** Apply a move to the internal chess state. Returns the new FEN if valid. */
20
+ applyMove: (from: string, to: string, promotion?: string) => MoveResult;
21
+ /** Undo the last move on the internal chess state */
22
+ undoMove: () => string | null;
23
+ /** Load a new FEN into the internal chess state */
24
+ loadFen: (fen: string) => void;
25
+ /** Get the current FEN from internal state */
26
+ getFen: () => string;
27
+ /** Get the current turn from internal state */
28
+ getTurn: () => 'w' | 'b';
29
+ /** Check if the current side to move is in check */
30
+ isInCheck: () => boolean;
31
+ };
32
+
33
+ /**
34
+ * Manages the internal chess.js instance for legal move validation.
35
+ *
36
+ * This mirrors the visual board state. When the parent passes a new FEN,
37
+ * the internal chess.js is synced. Legal move queries and move application
38
+ * happen against this instance.
39
+ *
40
+ * The chess.js instance lives in a ref — no React state, no re-renders.
41
+ */
42
+ export function useBoardState(initialFen: string): BoardStateReturn {
43
+ const chessRef = useRef<Chess>(null!);
44
+ if (!chessRef.current) chessRef.current = new Chess(initialFen);
45
+
46
+ const getLegalMoves = useCallback((square: string): LegalMoveTarget[] => {
47
+ try {
48
+ const moves = chessRef.current.moves({ square: square as Square, verbose: true });
49
+ return moves.map((m) => ({
50
+ square: m.to,
51
+ isCapture: m.captured !== undefined,
52
+ }));
53
+ } catch {
54
+ return [];
55
+ }
56
+ }, []);
57
+
58
+ const isPlayerPiece = useCallback(
59
+ (square: string, pieces: BoardPiece[], player: ChessColor | 'both'): boolean => {
60
+ const piece = pieces.find((p) => p.square === square);
61
+ if (!piece) return false;
62
+
63
+ if (player === 'both') return true;
64
+
65
+ const pieceColor: ChessColor = piece.color === 'w' ? 'white' : 'black';
66
+ return pieceColor === player;
67
+ },
68
+ [],
69
+ );
70
+
71
+ const applyMove = useCallback((from: string, to: string, promotion?: string): MoveResult => {
72
+ try {
73
+ chessRef.current.move({
74
+ from: from as Square,
75
+ to: to as Square,
76
+ promotion: promotion as 'q' | 'r' | 'b' | 'n' | undefined,
77
+ });
78
+ return { applied: true, fen: chessRef.current.fen() };
79
+ } catch {
80
+ return { applied: false };
81
+ }
82
+ }, []);
83
+
84
+ const undoMove = useCallback((): string | null => {
85
+ const result = chessRef.current.undo();
86
+ return result ? chessRef.current.fen() : null;
87
+ }, []);
88
+
89
+ const loadFen = useCallback((fen: string) => {
90
+ chessRef.current.load(fen);
91
+ }, []);
92
+
93
+ const getFen = useCallback(() => chessRef.current.fen(), []);
94
+
95
+ const getTurn = useCallback(() => chessRef.current.turn(), []);
96
+
97
+ const isInCheck = useCallback(() => chessRef.current.isCheck(), []);
98
+
99
+ // Stable reference so Board's useEffect([fen, boardState]) only re-runs
100
+ // when the fen prop changes — not on every render.
101
+ return useMemo(() => ({
102
+ getLegalMoves,
103
+ isPlayerPiece,
104
+ applyMove,
105
+ undoMove,
106
+ loadFen,
107
+ getFen,
108
+ getTurn,
109
+ isInCheck,
110
+ }), [getLegalMoves, isPlayerPiece, applyMove, undoMove, loadFen, getFen, getTurn, isInCheck]);
111
+ }
@@ -1,59 +1,59 @@
1
- import { useState, useCallback } from 'react';
2
-
3
- import type { PremoveData } from './types';
4
-
5
- type UsePremoveReturn = {
6
- /** The currently queued premove (only one at a time, like lichess) */
7
- premove: PremoveData | null;
8
- /** Queue a premove. Replaces any existing premove. */
9
- setPremove: (premove: PremoveData) => void;
10
- /** Clear the queued premove */
11
- clearPremove: () => void;
12
- /**
13
- * Try to execute the queued premove.
14
- * Returns the premove if one was queued (so the caller can apply it),
15
- * then clears the queue. Returns null if no premove was queued.
16
- */
17
- consumePremove: () => PremoveData | null;
18
- };
19
-
20
- /**
21
- * Manages a single premove queue (one premove at a time).
22
- *
23
- * Premoves work like lichess:
24
- * 1. When it's not your turn, you can drag/click a move — it queues as a premove
25
- * 2. The premove squares are highlighted with a distinct color
26
- * 3. When your turn begins (FEN changes and it's now your turn), the premove
27
- * is automatically attempted via the board's internal chess.js
28
- * 4. If the premove is legal, it's applied; if not, it's silently discarded
29
- *
30
- * The board.tsx orchestrates this: it calls consumePremove() in a useEffect
31
- * that watches the FEN (turn change), then attempts the move.
32
- */
33
- export function usePremove(): UsePremoveReturn {
34
- const [premove, setPremoveState] = useState<PremoveData | null>(null);
35
-
36
- const setPremove = useCallback((pm: PremoveData) => {
37
- setPremoveState(pm);
38
- }, []);
39
-
40
- const clearPremove = useCallback(() => {
41
- setPremoveState(null);
42
- }, []);
43
-
44
- const consumePremove = useCallback((): PremoveData | null => {
45
- let consumed: PremoveData | null = null;
46
- setPremoveState((current) => {
47
- consumed = current;
48
- return null;
49
- });
50
- return consumed;
51
- }, []);
52
-
53
- return {
54
- premove,
55
- setPremove,
56
- clearPremove,
57
- consumePremove,
58
- };
59
- }
1
+ import { useState, useCallback } from 'react';
2
+
3
+ import type { PremoveData } from './types';
4
+
5
+ type UsePremoveReturn = {
6
+ /** The currently queued premove (only one at a time, like lichess) */
7
+ premove: PremoveData | null;
8
+ /** Queue a premove. Replaces any existing premove. */
9
+ setPremove: (premove: PremoveData) => void;
10
+ /** Clear the queued premove */
11
+ clearPremove: () => void;
12
+ /**
13
+ * Try to execute the queued premove.
14
+ * Returns the premove if one was queued (so the caller can apply it),
15
+ * then clears the queue. Returns null if no premove was queued.
16
+ */
17
+ consumePremove: () => PremoveData | null;
18
+ };
19
+
20
+ /**
21
+ * Manages a single premove queue (one premove at a time).
22
+ *
23
+ * Premoves work like lichess:
24
+ * 1. When it's not your turn, you can drag/click a move — it queues as a premove
25
+ * 2. The premove squares are highlighted with a distinct color
26
+ * 3. When your turn begins (FEN changes and it's now your turn), the premove
27
+ * is automatically attempted via the board's internal chess.js
28
+ * 4. If the premove is legal, it's applied; if not, it's silently discarded
29
+ *
30
+ * The board.tsx orchestrates this: it calls consumePremove() in a useEffect
31
+ * that watches the FEN (turn change), then attempts the move.
32
+ */
33
+ export function usePremove(): UsePremoveReturn {
34
+ const [premove, setPremoveState] = useState<PremoveData | null>(null);
35
+
36
+ const setPremove = useCallback((pm: PremoveData) => {
37
+ setPremoveState(pm);
38
+ }, []);
39
+
40
+ const clearPremove = useCallback(() => {
41
+ setPremoveState(null);
42
+ }, []);
43
+
44
+ const consumePremove = useCallback((): PremoveData | null => {
45
+ let consumed: PremoveData | null = null;
46
+ setPremoveState((current) => {
47
+ consumed = current;
48
+ return null;
49
+ });
50
+ return consumed;
51
+ }, []);
52
+
53
+ return {
54
+ premove,
55
+ setPremove,
56
+ clearPremove,
57
+ consumePremove,
58
+ };
59
+ }