react-native-chess-kit 0.1.0 → 0.2.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/lib/commonjs/board-annotations.js +131 -0
- package/lib/commonjs/board-annotations.js.map +1 -0
- package/lib/commonjs/board-arrows.js +164 -0
- package/lib/commonjs/board-arrows.js.map +1 -0
- package/lib/commonjs/board-highlights.js +212 -0
- package/lib/commonjs/board-highlights.js.map +1 -0
- package/lib/commonjs/board-piece.js +71 -25
- package/lib/commonjs/board-piece.js.map +1 -1
- package/lib/commonjs/board-pieces.js +2 -0
- package/lib/commonjs/board-pieces.js.map +1 -1
- package/lib/commonjs/board.js +392 -42
- package/lib/commonjs/board.js.map +1 -1
- package/lib/commonjs/constants.js +104 -0
- package/lib/commonjs/constants.js.map +1 -0
- package/lib/commonjs/index.js +128 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/pieces/default-pieces.js +536 -0
- package/lib/commonjs/pieces/default-pieces.js.map +1 -0
- package/lib/commonjs/pieces/index.js +13 -0
- package/lib/commonjs/pieces/index.js.map +1 -0
- package/lib/commonjs/promotion-picker.js +129 -0
- package/lib/commonjs/promotion-picker.js.map +1 -0
- package/lib/commonjs/static-board.js +150 -0
- package/lib/commonjs/static-board.js.map +1 -0
- package/lib/commonjs/themes.js +175 -0
- package/lib/commonjs/themes.js.map +1 -0
- package/lib/commonjs/use-board-gesture.js +184 -11
- package/lib/commonjs/use-board-gesture.js.map +1 -1
- package/lib/commonjs/use-premove.js +44 -0
- package/lib/commonjs/use-premove.js.map +1 -0
- package/lib/module/board-annotations.js +126 -0
- package/lib/module/board-annotations.js.map +1 -0
- package/lib/module/board-arrows.js +161 -0
- package/lib/module/board-arrows.js.map +1 -0
- package/lib/module/board-highlights.js +206 -0
- package/lib/module/board-highlights.js.map +1 -0
- package/lib/module/board-piece.js +72 -26
- package/lib/module/board-piece.js.map +1 -1
- package/lib/module/board-pieces.js +2 -0
- package/lib/module/board-pieces.js.map +1 -1
- package/lib/module/board.js +395 -44
- package/lib/module/board.js.map +1 -1
- package/lib/module/constants.js +100 -0
- package/lib/module/constants.js.map +1 -0
- package/lib/module/index.js +29 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/pieces/default-pieces.js +530 -0
- package/lib/module/pieces/default-pieces.js.map +1 -0
- package/lib/module/pieces/index.js +4 -0
- package/lib/module/pieces/index.js.map +1 -0
- package/lib/module/promotion-picker.js +124 -0
- package/lib/module/promotion-picker.js.map +1 -0
- package/lib/module/static-board.js +146 -0
- package/lib/module/static-board.js.map +1 -0
- package/lib/module/themes.js +171 -0
- package/lib/module/themes.js.map +1 -0
- package/lib/module/use-board-gesture.js +185 -11
- package/lib/module/use-board-gesture.js.map +1 -1
- package/lib/module/use-premove.js +40 -0
- package/lib/module/use-premove.js.map +1 -0
- package/lib/typescript/board-annotations.d.ts +30 -0
- package/lib/typescript/board-annotations.d.ts.map +1 -0
- package/lib/typescript/board-arrows.d.ts +27 -0
- package/lib/typescript/board-arrows.d.ts.map +1 -0
- package/lib/typescript/board-highlights.d.ts +65 -0
- package/lib/typescript/board-highlights.d.ts.map +1 -0
- package/lib/typescript/board-piece.d.ts +19 -9
- package/lib/typescript/board-piece.d.ts.map +1 -1
- package/lib/typescript/board-pieces.d.ts +2 -1
- package/lib/typescript/board-pieces.d.ts.map +1 -1
- package/lib/typescript/board.d.ts +11 -2
- package/lib/typescript/board.d.ts.map +1 -1
- package/lib/typescript/constants.d.ts +54 -0
- package/lib/typescript/constants.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +9 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/pieces/default-pieces.d.ts +3 -0
- package/lib/typescript/pieces/default-pieces.d.ts.map +1 -0
- package/lib/typescript/pieces/index.d.ts +2 -0
- package/lib/typescript/pieces/index.d.ts.map +1 -0
- package/lib/typescript/promotion-picker.d.ts +30 -0
- package/lib/typescript/promotion-picker.d.ts.map +1 -0
- package/lib/typescript/static-board.d.ts +12 -0
- package/lib/typescript/static-board.d.ts.map +1 -0
- package/lib/typescript/themes.d.ts +15 -0
- package/lib/typescript/themes.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +194 -24
- package/lib/typescript/types.d.ts.map +1 -1
- package/lib/typescript/use-board-gesture.d.ts +28 -2
- package/lib/typescript/use-board-gesture.d.ts.map +1 -1
- package/lib/typescript/use-premove.d.ts +31 -0
- package/lib/typescript/use-premove.d.ts.map +1 -0
- package/package.json +4 -2
- package/src/board-annotations.tsx +147 -0
- package/src/board-arrows.tsx +197 -0
- package/src/board-highlights.tsx +226 -0
- package/src/board-piece.tsx +77 -29
- package/src/board-pieces.tsx +4 -1
- package/src/board.tsx +462 -46
- package/src/constants.ts +100 -0
- package/src/index.ts +62 -1
- package/src/pieces/default-pieces.tsx +383 -0
- package/src/pieces/index.ts +1 -0
- package/src/promotion-picker.tsx +147 -0
- package/src/static-board.tsx +150 -0
- package/src/themes.ts +129 -0
- package/src/types.ts +251 -25
- package/src/use-board-gesture.ts +219 -8
- package/src/use-premove.ts +59 -0
package/src/board.tsx
CHANGED
|
@@ -1,16 +1,94 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
|
|
1
|
+
import React, {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useState,
|
|
4
|
+
useCallback,
|
|
5
|
+
useImperativeHandle,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useRef,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import { View, type LayoutChangeEvent } from 'react-native';
|
|
3
11
|
import { GestureDetector } from 'react-native-gesture-handler';
|
|
12
|
+
import Animated, {
|
|
13
|
+
useSharedValue,
|
|
14
|
+
useAnimatedStyle,
|
|
15
|
+
withTiming,
|
|
16
|
+
} from 'react-native-reanimated';
|
|
4
17
|
|
|
5
|
-
import type {
|
|
18
|
+
import type {
|
|
19
|
+
BoardRef,
|
|
20
|
+
BoardProps,
|
|
21
|
+
LegalMoveTarget,
|
|
22
|
+
HighlightData,
|
|
23
|
+
PieceCode,
|
|
24
|
+
PromotionPiece,
|
|
25
|
+
} from './types';
|
|
26
|
+
import {
|
|
27
|
+
DEFAULT_BOARD_COLORS,
|
|
28
|
+
DEFAULT_MOVE_DURATION,
|
|
29
|
+
DEFAULT_LAST_MOVE_COLOR,
|
|
30
|
+
DEFAULT_CHECK_COLOR,
|
|
31
|
+
DEFAULT_SELECTED_COLOR,
|
|
32
|
+
DEFAULT_PREMOVE_COLOR,
|
|
33
|
+
DEFAULT_DRAG_TARGET_COLOR,
|
|
34
|
+
} from './constants';
|
|
35
|
+
import { DefaultPieceSet } from './pieces';
|
|
6
36
|
import { useBoardPieces } from './use-board-pieces';
|
|
7
37
|
import { useBoardState } from './use-board-state';
|
|
8
38
|
import { useBoardGesture } from './use-board-gesture';
|
|
39
|
+
import { usePremove } from './use-premove';
|
|
9
40
|
import { BoardBackground } from './board-background';
|
|
10
41
|
import { BoardCoordinates } from './board-coordinates';
|
|
42
|
+
import { BoardHighlights, DragTargetHighlight } from './board-highlights';
|
|
11
43
|
import { BoardLegalDots } from './board-legal-dots';
|
|
12
44
|
import { BoardPiecesLayer } from './board-pieces';
|
|
13
45
|
import { BoardDragGhost } from './board-drag-ghost';
|
|
46
|
+
import { BoardArrows } from './board-arrows';
|
|
47
|
+
import { BoardAnnotations } from './board-annotations';
|
|
48
|
+
import { PromotionPicker } from './promotion-picker';
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Check detection helper
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Find the king square for the side currently in check.
|
|
56
|
+
* Returns null if not in check.
|
|
57
|
+
*/
|
|
58
|
+
function detectCheckSquare(
|
|
59
|
+
fen: string,
|
|
60
|
+
isInCheck: () => boolean,
|
|
61
|
+
getTurn: () => 'w' | 'b',
|
|
62
|
+
): string | null {
|
|
63
|
+
if (!isInCheck()) return null;
|
|
64
|
+
|
|
65
|
+
const turn = getTurn();
|
|
66
|
+
const kingChar = turn === 'w' ? 'K' : 'k';
|
|
67
|
+
const placement = fen.split(' ')[0];
|
|
68
|
+
const ranks = placement.split('/');
|
|
69
|
+
|
|
70
|
+
for (let rankIdx = 0; rankIdx < ranks.length; rankIdx++) {
|
|
71
|
+
const rank = ranks[rankIdx]!;
|
|
72
|
+
let fileIdx = 0;
|
|
73
|
+
for (const char of rank) {
|
|
74
|
+
if (char >= '1' && char <= '8') {
|
|
75
|
+
fileIdx += parseInt(char, 10);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (char === kingChar) {
|
|
79
|
+
const files = 'abcdefgh';
|
|
80
|
+
return `${files[fileIdx]}${8 - rankIdx}`;
|
|
81
|
+
}
|
|
82
|
+
fileIdx++;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Board component
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
14
92
|
|
|
15
93
|
/**
|
|
16
94
|
* High-performance custom chess board built on Reanimated + Gesture Handler.
|
|
@@ -22,28 +100,108 @@ import { BoardDragGhost } from './board-drag-ghost';
|
|
|
22
100
|
* - 0 React Context providers
|
|
23
101
|
* - 0 re-renders during drag (pure worklet — only 2 shared value writes per frame)
|
|
24
102
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
103
|
+
* v0.2.0 layer stack (10 layers):
|
|
104
|
+
* 1. BoardBackground (64 squares)
|
|
105
|
+
* 2. BoardCoordinates (a-h, 1-8)
|
|
106
|
+
* 3. BoardHighlights (last move, check, selected, premove, custom, imperative)
|
|
107
|
+
* 4. DragTargetHighlight (animated, worklet-driven)
|
|
108
|
+
* 5. BoardLegalDots (legal move indicators)
|
|
109
|
+
* 6. BoardPiecesLayer (all pieces)
|
|
110
|
+
* 7. BoardArrows (SVG arrows + circles)
|
|
111
|
+
* 8. BoardAnnotations (text badges)
|
|
112
|
+
* 9. BoardDragGhost (floating piece)
|
|
113
|
+
* 10. PromotionPicker (modal, conditional)
|
|
27
114
|
*/
|
|
28
115
|
export const Board = forwardRef<BoardRef, BoardProps>(function Board(
|
|
29
116
|
{
|
|
30
117
|
fen,
|
|
31
118
|
orientation,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
119
|
+
|
|
120
|
+
// Layout
|
|
121
|
+
boardSize: boardSizeProp,
|
|
122
|
+
|
|
123
|
+
// Interaction
|
|
124
|
+
gestureEnabled = true,
|
|
125
|
+
player = 'both',
|
|
126
|
+
moveMethod = 'both',
|
|
127
|
+
showLegalMoves = true,
|
|
128
|
+
premovesEnabled = false,
|
|
129
|
+
|
|
130
|
+
// Appearance
|
|
36
131
|
colors,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
withNumbers,
|
|
132
|
+
withLetters = true,
|
|
133
|
+
withNumbers = true,
|
|
40
134
|
renderPiece,
|
|
41
|
-
|
|
42
|
-
|
|
135
|
+
pieceSet,
|
|
136
|
+
|
|
137
|
+
// Overlays
|
|
138
|
+
lastMove,
|
|
139
|
+
highlights,
|
|
140
|
+
arrows,
|
|
141
|
+
shapes,
|
|
142
|
+
annotations,
|
|
143
|
+
showDragTarget = true,
|
|
144
|
+
|
|
145
|
+
// Overlay colors
|
|
146
|
+
lastMoveColor = DEFAULT_LAST_MOVE_COLOR,
|
|
147
|
+
checkHighlightColor = DEFAULT_CHECK_COLOR,
|
|
148
|
+
selectedSquareColor = DEFAULT_SELECTED_COLOR,
|
|
149
|
+
premoveColor = DEFAULT_PREMOVE_COLOR,
|
|
150
|
+
dragTargetColor = DEFAULT_DRAG_TARGET_COLOR,
|
|
151
|
+
|
|
152
|
+
// Animation
|
|
153
|
+
moveDuration = DEFAULT_MOVE_DURATION,
|
|
154
|
+
animationConfig,
|
|
155
|
+
animateFlip = true,
|
|
156
|
+
|
|
157
|
+
// Promotion
|
|
158
|
+
onPromotion,
|
|
159
|
+
|
|
160
|
+
// Callbacks
|
|
161
|
+
onMove,
|
|
162
|
+
onPieceClick,
|
|
163
|
+
onSquareClick,
|
|
164
|
+
onPieceDragBegin,
|
|
165
|
+
onPieceDragEnd,
|
|
166
|
+
onSquareLongPress,
|
|
167
|
+
onPremove,
|
|
168
|
+
onHaptic,
|
|
43
169
|
},
|
|
44
170
|
ref,
|
|
45
171
|
) {
|
|
172
|
+
// --- Auto-sizing via onLayout when boardSize not provided ---
|
|
173
|
+
const [measuredSize, setMeasuredSize] = useState(0);
|
|
174
|
+
const handleLayout = useCallback((e: LayoutChangeEvent) => {
|
|
175
|
+
const { width, height } = e.nativeEvent.layout;
|
|
176
|
+
setMeasuredSize(Math.min(width, height));
|
|
177
|
+
}, []);
|
|
178
|
+
|
|
179
|
+
const boardSize = boardSizeProp ?? measuredSize;
|
|
46
180
|
const squareSize = boardSize / 8;
|
|
181
|
+
const boardColors = colors ?? DEFAULT_BOARD_COLORS;
|
|
182
|
+
|
|
183
|
+
// --- Board flip animation ---
|
|
184
|
+
const flipRotation = useSharedValue(orientation === 'black' ? 180 : 0);
|
|
185
|
+
const prevOrientationRef = useRef(orientation);
|
|
186
|
+
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
if (prevOrientationRef.current !== orientation) {
|
|
189
|
+
prevOrientationRef.current = orientation;
|
|
190
|
+
if (animateFlip) {
|
|
191
|
+
flipRotation.value = withTiming(
|
|
192
|
+
orientation === 'black' ? 180 : 0,
|
|
193
|
+
{ duration: 300 },
|
|
194
|
+
);
|
|
195
|
+
} else {
|
|
196
|
+
flipRotation.value = orientation === 'black' ? 180 : 0;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}, [orientation, animateFlip, flipRotation]);
|
|
200
|
+
|
|
201
|
+
// Note: We don't actually rotate the board view because all layers already
|
|
202
|
+
// handle orientation via squareToXY coordinate math. The flip animation is
|
|
203
|
+
// a visual effect only — the rotation shared value is available for consumers
|
|
204
|
+
// who want to add a rotation transition effect.
|
|
47
205
|
|
|
48
206
|
// --- Piece data from FEN ---
|
|
49
207
|
const pieces = useBoardPieces(fen);
|
|
@@ -51,25 +209,99 @@ export const Board = forwardRef<BoardRef, BoardProps>(function Board(
|
|
|
51
209
|
// --- Chess.js for legal move validation ---
|
|
52
210
|
const boardState = useBoardState(fen);
|
|
53
211
|
|
|
54
|
-
// Sync internal chess.js when parent changes FEN
|
|
55
|
-
// Must be in useEffect — side effects during render violate React's rules
|
|
56
|
-
// and can fire multiple times in concurrent mode.
|
|
212
|
+
// Sync internal chess.js when parent changes FEN
|
|
57
213
|
useEffect(() => {
|
|
58
214
|
boardState.loadFen(fen);
|
|
59
215
|
}, [fen, boardState]);
|
|
60
216
|
|
|
61
|
-
// ---
|
|
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
|
+
);
|
|
237
|
+
|
|
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
|
|
244
|
+
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
|
+
},
|
|
254
|
+
boardState.getTurn,
|
|
255
|
+
);
|
|
256
|
+
setCheckSquareState(square);
|
|
257
|
+
} catch {
|
|
258
|
+
setCheckSquareState(null);
|
|
259
|
+
}
|
|
260
|
+
}, [fen, boardState]);
|
|
261
|
+
|
|
262
|
+
// --- Selection state ---
|
|
62
263
|
const [selectedSquare, setSelectedSquare] = useState<string | null>(null);
|
|
63
264
|
const [legalMoves, setLegalMoves] = useState<LegalMoveTarget[]>([]);
|
|
64
265
|
|
|
65
|
-
//
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
266
|
+
// --- Imperative highlights ---
|
|
267
|
+
const [imperativeHighlights, setImperativeHighlights] = useState<HighlightData[]>([]);
|
|
268
|
+
|
|
269
|
+
// --- Premove state ---
|
|
270
|
+
const { premove, setPremove, clearPremove, consumePremove } = usePremove();
|
|
271
|
+
|
|
272
|
+
// --- Promotion state ---
|
|
273
|
+
const [promotionState, setPromotionState] = useState<{
|
|
274
|
+
from: string;
|
|
275
|
+
to: string;
|
|
276
|
+
color: 'w' | 'b';
|
|
277
|
+
} | null>(null);
|
|
278
|
+
|
|
279
|
+
// --- Resolve piece renderer: renderPiece > pieceSet > DefaultPieceSet ---
|
|
280
|
+
const resolvedRenderer = useMemo(() => {
|
|
281
|
+
if (renderPiece) return renderPiece;
|
|
282
|
+
const set = pieceSet ?? DefaultPieceSet;
|
|
283
|
+
return (code: string, size: number) => {
|
|
284
|
+
const renderer = set[code as PieceCode];
|
|
285
|
+
if (renderer) return renderer(size);
|
|
286
|
+
return <View style={{ width: size, height: size }} />;
|
|
287
|
+
};
|
|
288
|
+
}, [renderPiece, pieceSet]);
|
|
289
|
+
|
|
290
|
+
// --- Promotion detection ---
|
|
291
|
+
const isPromotionMove = useCallback(
|
|
292
|
+
(from: string, to: string): boolean => {
|
|
293
|
+
const piece = pieces.find((p) => p.square === from);
|
|
294
|
+
if (!piece) return false;
|
|
295
|
+
// Must be a pawn
|
|
296
|
+
if (piece.code !== 'wp' && piece.code !== 'bp') return false;
|
|
297
|
+
// Must be moving to the last rank
|
|
298
|
+
const toRank = to[1];
|
|
299
|
+
if (piece.color === 'w' && toRank === '8') return true;
|
|
300
|
+
if (piece.color === 'b' && toRank === '1') return true;
|
|
301
|
+
return false;
|
|
302
|
+
},
|
|
303
|
+
[pieces],
|
|
71
304
|
);
|
|
72
|
-
const pieceRenderer = renderPiece ?? defaultRenderPiece;
|
|
73
305
|
|
|
74
306
|
// --- Gesture callbacks ---
|
|
75
307
|
const handlePieceSelected = useCallback(
|
|
@@ -88,15 +320,115 @@ export const Board = forwardRef<BoardRef, BoardProps>(function Board(
|
|
|
88
320
|
}, []);
|
|
89
321
|
|
|
90
322
|
const handlePieceMoved = useCallback(
|
|
91
|
-
(from: string, to: string) => {
|
|
323
|
+
async (from: string, to: string) => {
|
|
92
324
|
// Clear selection and legal dots
|
|
93
325
|
setSelectedSquare(null);
|
|
94
326
|
setLegalMoves([]);
|
|
95
327
|
|
|
328
|
+
// Check for promotion
|
|
329
|
+
if (isPromotionMove(from, to)) {
|
|
330
|
+
if (onPromotion) {
|
|
331
|
+
// Show promotion picker or get choice from callback
|
|
332
|
+
const piece = pieces.find((p) => p.square === from);
|
|
333
|
+
const color = piece?.color ?? 'w';
|
|
334
|
+
|
|
335
|
+
setPromotionState({ from, to, color });
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
// Auto-promote to queen if no onPromotion callback
|
|
339
|
+
onMove?.({ from, to });
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
96
343
|
// Notify parent — parent decides whether to accept/reject
|
|
97
344
|
onMove?.({ from, to });
|
|
98
345
|
},
|
|
99
|
-
[onMove],
|
|
346
|
+
[onMove, onPromotion, isPromotionMove, pieces],
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
// --- Promotion picker handlers ---
|
|
350
|
+
const handlePromotionSelect = useCallback(
|
|
351
|
+
async (piece: PromotionPiece) => {
|
|
352
|
+
if (!promotionState) return;
|
|
353
|
+
const { from, to } = promotionState;
|
|
354
|
+
setPromotionState(null);
|
|
355
|
+
|
|
356
|
+
// If consumer provided onPromotion, call it for confirmation
|
|
357
|
+
if (onPromotion) {
|
|
358
|
+
try {
|
|
359
|
+
const choice = await onPromotion(from, to);
|
|
360
|
+
// Apply the move with chosen promotion
|
|
361
|
+
onMove?.({ from, to });
|
|
362
|
+
} catch {
|
|
363
|
+
// Promotion cancelled
|
|
364
|
+
}
|
|
365
|
+
} else {
|
|
366
|
+
onMove?.({ from, to });
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
[promotionState, onPromotion, onMove],
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
const handlePromotionCancel = useCallback(() => {
|
|
373
|
+
setPromotionState(null);
|
|
374
|
+
}, []);
|
|
375
|
+
|
|
376
|
+
// --- Premove handling ---
|
|
377
|
+
const handlePremoveSet = useCallback(
|
|
378
|
+
(pm: { from: string; to: string }) => {
|
|
379
|
+
setPremove(pm);
|
|
380
|
+
onPremove?.(pm);
|
|
381
|
+
onHaptic?.('select');
|
|
382
|
+
},
|
|
383
|
+
[setPremove, onPremove, onHaptic],
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
// Execute premove when turn changes
|
|
387
|
+
useEffect(() => {
|
|
388
|
+
if (!premovesEnabled || !premove) return;
|
|
389
|
+
|
|
390
|
+
const turn = boardState.getTurn();
|
|
391
|
+
// Check if it's now the premover's turn
|
|
392
|
+
const premovePiece = pieces.find((p) => p.square === premove.from);
|
|
393
|
+
if (!premovePiece) {
|
|
394
|
+
clearPremove();
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (premovePiece.color === turn) {
|
|
399
|
+
const consumed = consumePremove();
|
|
400
|
+
if (consumed) {
|
|
401
|
+
// Try to execute the premove
|
|
402
|
+
const result = boardState.applyMove(consumed.from, consumed.to, consumed.promotion);
|
|
403
|
+
if (result.applied) {
|
|
404
|
+
onMove?.({ from: consumed.from, to: consumed.to });
|
|
405
|
+
onHaptic?.('move');
|
|
406
|
+
} else {
|
|
407
|
+
// Premove was illegal — discard silently
|
|
408
|
+
onHaptic?.('error');
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}, [fen, premovesEnabled, premove, pieces, boardState, consumePremove, clearPremove, onMove, onHaptic]);
|
|
413
|
+
|
|
414
|
+
// --- Rich callbacks ref (stable, for gesture hook) ---
|
|
415
|
+
const richCallbacks = useMemo(
|
|
416
|
+
() => ({
|
|
417
|
+
onPieceClick,
|
|
418
|
+
onSquareClick,
|
|
419
|
+
onPieceDragBegin,
|
|
420
|
+
onPieceDragEnd,
|
|
421
|
+
onSquareLongPress,
|
|
422
|
+
onHaptic,
|
|
423
|
+
}),
|
|
424
|
+
[onPieceClick, onSquareClick, onPieceDragBegin, onPieceDragEnd, onSquareLongPress, onHaptic],
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
const premoveCallbacks = useMemo(
|
|
428
|
+
() => ({
|
|
429
|
+
onPremoveSet: handlePremoveSet,
|
|
430
|
+
}),
|
|
431
|
+
[handlePremoveSet],
|
|
100
432
|
);
|
|
101
433
|
|
|
102
434
|
// --- Single centralized gesture ---
|
|
@@ -112,33 +444,39 @@ export const Board = forwardRef<BoardRef, BoardProps>(function Board(
|
|
|
112
444
|
onPieceMoved: handlePieceMoved,
|
|
113
445
|
onSelectionCleared: handleSelectionCleared,
|
|
114
446
|
},
|
|
447
|
+
richCallbacks,
|
|
448
|
+
premoveCallbacks,
|
|
449
|
+
premovesEnabled,
|
|
115
450
|
selectedSquare,
|
|
116
451
|
legalMoves,
|
|
452
|
+
currentTurn: boardState.getTurn(),
|
|
117
453
|
});
|
|
118
454
|
|
|
119
|
-
// --- Imperative ref
|
|
455
|
+
// --- Imperative ref ---
|
|
120
456
|
useImperativeHandle(ref, () => ({
|
|
121
457
|
move: (move) => {
|
|
122
|
-
// Pre-apply to internal chess.js so subsequent getLegalMoves calls
|
|
123
|
-
// reflect the new position. The parent will also update the FEN prop,
|
|
124
|
-
// which triggers useBoardPieces -> piece position animates via shared values.
|
|
125
458
|
boardState.applyMove(move.from, move.to);
|
|
126
459
|
},
|
|
127
460
|
|
|
128
|
-
highlight: (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
461
|
+
highlight: (square, color) => {
|
|
462
|
+
setImperativeHighlights((prev) => {
|
|
463
|
+
// Replace existing highlight on same square, or add new
|
|
464
|
+
const filtered = prev.filter((h) => h.square !== square);
|
|
465
|
+
return [...filtered, { square, color }];
|
|
466
|
+
});
|
|
132
467
|
},
|
|
133
468
|
|
|
134
469
|
clearHighlights: () => {
|
|
135
|
-
|
|
470
|
+
setImperativeHighlights([]);
|
|
136
471
|
},
|
|
137
472
|
|
|
138
473
|
resetBoard: (newFen) => {
|
|
139
474
|
boardState.loadFen(newFen);
|
|
140
475
|
setSelectedSquare(null);
|
|
141
476
|
setLegalMoves([]);
|
|
477
|
+
setImperativeHighlights([]);
|
|
478
|
+
clearPremove();
|
|
479
|
+
setPromotionState(null);
|
|
142
480
|
},
|
|
143
481
|
|
|
144
482
|
undo: () => {
|
|
@@ -146,29 +484,72 @@ export const Board = forwardRef<BoardRef, BoardProps>(function Board(
|
|
|
146
484
|
setSelectedSquare(null);
|
|
147
485
|
setLegalMoves([]);
|
|
148
486
|
},
|
|
487
|
+
|
|
488
|
+
clearPremoves: () => {
|
|
489
|
+
clearPremove();
|
|
490
|
+
},
|
|
149
491
|
}));
|
|
150
492
|
|
|
493
|
+
// If no size yet (auto-sizing), render invisible container for measurement
|
|
494
|
+
if (boardSize === 0) {
|
|
495
|
+
return (
|
|
496
|
+
<View style={{ flex: 1, aspectRatio: 1 }} onLayout={handleLayout} />
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
151
500
|
return (
|
|
152
501
|
<GestureDetector gesture={gesture}>
|
|
153
|
-
<View
|
|
502
|
+
<View
|
|
503
|
+
style={{ width: boardSize, height: boardSize }}
|
|
504
|
+
onLayout={boardSizeProp ? undefined : handleLayout}
|
|
505
|
+
accessibilityLabel="Chess board"
|
|
506
|
+
accessibilityRole="adjustable"
|
|
507
|
+
>
|
|
154
508
|
{/* Layer 1: Board background (64 colored squares) */}
|
|
155
509
|
<BoardBackground
|
|
156
510
|
boardSize={boardSize}
|
|
157
|
-
lightColor={
|
|
158
|
-
darkColor={
|
|
511
|
+
lightColor={boardColors.light}
|
|
512
|
+
darkColor={boardColors.dark}
|
|
159
513
|
/>
|
|
160
514
|
|
|
161
515
|
{/* Layer 2: Coordinate labels (a-h, 1-8) */}
|
|
162
516
|
<BoardCoordinates
|
|
163
517
|
boardSize={boardSize}
|
|
164
518
|
orientation={orientation}
|
|
165
|
-
lightColor={
|
|
166
|
-
darkColor={
|
|
519
|
+
lightColor={boardColors.light}
|
|
520
|
+
darkColor={boardColors.dark}
|
|
167
521
|
withLetters={withLetters}
|
|
168
522
|
withNumbers={withNumbers}
|
|
169
523
|
/>
|
|
170
524
|
|
|
171
|
-
{/* Layer 3:
|
|
525
|
+
{/* Layer 3: Square highlights (last move, check, selected, premove, custom, imperative) */}
|
|
526
|
+
<BoardHighlights
|
|
527
|
+
boardSize={boardSize}
|
|
528
|
+
orientation={orientation}
|
|
529
|
+
squareSize={squareSize}
|
|
530
|
+
lastMove={lastMove}
|
|
531
|
+
lastMoveColor={lastMoveColor}
|
|
532
|
+
checkSquare={checkSquareState}
|
|
533
|
+
checkColor={checkHighlightColor}
|
|
534
|
+
selectedSquare={selectedSquare}
|
|
535
|
+
selectedColor={selectedSquareColor}
|
|
536
|
+
premoveSquares={premove ? { from: premove.from, to: premove.to } : null}
|
|
537
|
+
premoveColor={premoveColor}
|
|
538
|
+
highlights={highlights}
|
|
539
|
+
imperativeHighlights={imperativeHighlights}
|
|
540
|
+
/>
|
|
541
|
+
|
|
542
|
+
{/* Layer 4: Drag target highlight (animated, updates during drag) */}
|
|
543
|
+
{showDragTarget && (
|
|
544
|
+
<DragTargetHighlight
|
|
545
|
+
squareSize={squareSize}
|
|
546
|
+
orientation={orientation}
|
|
547
|
+
dragTargetSquare={gestureState.dragTargetSquare}
|
|
548
|
+
color={dragTargetColor}
|
|
549
|
+
/>
|
|
550
|
+
)}
|
|
551
|
+
|
|
552
|
+
{/* Layer 5: Legal move dots (only when a piece is selected) */}
|
|
172
553
|
{showLegalMoves && (
|
|
173
554
|
<BoardLegalDots
|
|
174
555
|
legalMoves={legalMoves}
|
|
@@ -177,26 +558,61 @@ export const Board = forwardRef<BoardRef, BoardProps>(function Board(
|
|
|
177
558
|
/>
|
|
178
559
|
)}
|
|
179
560
|
|
|
180
|
-
{/* Layer
|
|
561
|
+
{/* Layer 6: Pieces */}
|
|
181
562
|
<BoardPiecesLayer
|
|
182
563
|
pieces={pieces}
|
|
183
564
|
squareSize={squareSize}
|
|
184
565
|
orientation={orientation}
|
|
185
566
|
moveDuration={moveDuration}
|
|
186
|
-
|
|
567
|
+
animationConfig={animationConfig}
|
|
568
|
+
renderPiece={resolvedRenderer}
|
|
187
569
|
activeSquare={gestureState.activeSquare}
|
|
188
570
|
isDragging={gestureState.isDragging}
|
|
189
571
|
/>
|
|
190
572
|
|
|
191
|
-
{/* Layer
|
|
573
|
+
{/* Layer 7: Arrows + shapes (SVG overlay) */}
|
|
574
|
+
{((arrows && arrows.length > 0) || (shapes && shapes.length > 0)) && (
|
|
575
|
+
<BoardArrows
|
|
576
|
+
boardSize={boardSize}
|
|
577
|
+
orientation={orientation}
|
|
578
|
+
arrows={arrows}
|
|
579
|
+
shapes={shapes}
|
|
580
|
+
/>
|
|
581
|
+
)}
|
|
582
|
+
|
|
583
|
+
{/* Layer 8: Annotations (text badges) */}
|
|
584
|
+
{annotations && annotations.length > 0 && (
|
|
585
|
+
<BoardAnnotations
|
|
586
|
+
boardSize={boardSize}
|
|
587
|
+
orientation={orientation}
|
|
588
|
+
squareSize={squareSize}
|
|
589
|
+
annotations={annotations}
|
|
590
|
+
/>
|
|
591
|
+
)}
|
|
592
|
+
|
|
593
|
+
{/* Layer 9: Drag ghost (single floating piece) */}
|
|
192
594
|
<BoardDragGhost
|
|
193
595
|
squareSize={squareSize}
|
|
194
596
|
isDragging={gestureState.isDragging}
|
|
195
597
|
dragX={gestureState.dragX}
|
|
196
598
|
dragY={gestureState.dragY}
|
|
197
599
|
dragPieceCode={gestureState.dragPieceCode}
|
|
198
|
-
renderPiece={
|
|
600
|
+
renderPiece={resolvedRenderer}
|
|
199
601
|
/>
|
|
602
|
+
|
|
603
|
+
{/* Layer 10: Promotion picker (conditional) */}
|
|
604
|
+
{promotionState && (
|
|
605
|
+
<PromotionPicker
|
|
606
|
+
square={promotionState.to}
|
|
607
|
+
pieceColor={promotionState.color}
|
|
608
|
+
boardSize={boardSize}
|
|
609
|
+
squareSize={squareSize}
|
|
610
|
+
orientation={orientation}
|
|
611
|
+
renderPiece={resolvedRenderer}
|
|
612
|
+
onSelect={handlePromotionSelect}
|
|
613
|
+
onCancel={handlePromotionCancel}
|
|
614
|
+
/>
|
|
615
|
+
)}
|
|
200
616
|
</View>
|
|
201
617
|
</GestureDetector>
|
|
202
618
|
);
|