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.
- package/LICENSE +21 -21
- package/README.md +168 -168
- package/lib/commonjs/board-annotations.js +8 -8
- package/lib/commonjs/board-arrows.js +7 -7
- package/lib/commonjs/board-background.js +5 -5
- package/lib/commonjs/board-coordinates.js +78 -11
- package/lib/commonjs/board-coordinates.js.map +1 -1
- package/lib/commonjs/board-drag-ghost.js +10 -10
- package/lib/commonjs/board-highlights.js +15 -15
- package/lib/commonjs/board-legal-dots.js +5 -5
- package/lib/commonjs/board-piece.js +25 -25
- package/lib/commonjs/board-pieces.js +6 -6
- package/lib/commonjs/board.js +76 -35
- package/lib/commonjs/board.js.map +1 -1
- package/lib/commonjs/constants.js +4 -1
- package/lib/commonjs/constants.js.map +1 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/promotion-picker.js +8 -8
- package/lib/commonjs/static-board.js +51 -16
- package/lib/commonjs/static-board.js.map +1 -1
- package/lib/commonjs/use-board-gesture.js +52 -33
- package/lib/commonjs/use-board-gesture.js.map +1 -1
- package/lib/commonjs/use-board-pieces.js +15 -15
- package/lib/commonjs/use-board-state.js +8 -8
- package/lib/commonjs/use-premove.js +12 -12
- package/lib/module/board-annotations.js +8 -8
- package/lib/module/board-arrows.js +7 -7
- package/lib/module/board-background.js +5 -5
- package/lib/module/board-coordinates.js +79 -12
- package/lib/module/board-coordinates.js.map +1 -1
- package/lib/module/board-drag-ghost.js +10 -10
- package/lib/module/board-highlights.js +15 -15
- package/lib/module/board-legal-dots.js +5 -5
- package/lib/module/board-piece.js +25 -25
- package/lib/module/board-pieces.js +6 -6
- package/lib/module/board.js +77 -36
- package/lib/module/board.js.map +1 -1
- package/lib/module/constants.js +3 -0
- package/lib/module/constants.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/promotion-picker.js +8 -8
- package/lib/module/static-board.js +52 -17
- package/lib/module/static-board.js.map +1 -1
- package/lib/module/use-board-gesture.js +52 -33
- package/lib/module/use-board-gesture.js.map +1 -1
- package/lib/module/use-board-pieces.js +15 -15
- package/lib/module/use-board-state.js +8 -8
- package/lib/module/use-premove.js +12 -12
- package/lib/typescript/board-coordinates.d.ts +10 -4
- package/lib/typescript/board-coordinates.d.ts.map +1 -1
- package/lib/typescript/board.d.ts.map +1 -1
- package/lib/typescript/constants.d.ts +2 -0
- package/lib/typescript/constants.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +1 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/static-board.d.ts.map +1 -1
- package/lib/typescript/types.d.ts +23 -3
- package/lib/typescript/types.d.ts.map +1 -1
- package/lib/typescript/use-board-gesture.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/board-annotations.tsx +147 -147
- package/src/board-arrows.tsx +197 -197
- package/src/board-background.tsx +46 -46
- package/src/board-coordinates.tsx +192 -98
- package/src/board-drag-ghost.tsx +132 -132
- package/src/board-highlights.tsx +226 -226
- package/src/board-legal-dots.tsx +73 -73
- package/src/board-piece.tsx +160 -160
- package/src/board-pieces.tsx +63 -63
- package/src/board.tsx +685 -641
- package/src/constants.ts +103 -100
- package/src/index.ts +101 -100
- package/src/pieces/default-pieces.tsx +383 -383
- package/src/pieces/index.ts +1 -1
- package/src/promotion-picker.tsx +147 -147
- package/src/static-board.tsx +187 -150
- package/src/themes.ts +129 -129
- package/src/types.ts +373 -352
- package/src/use-board-gesture.ts +429 -412
- package/src/use-board-pieces.ts +158 -158
- package/src/use-board-state.ts +111 -111
- package/src/use-premove.ts +59 -59
package/src/use-board-pieces.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/use-board-state.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/use-premove.ts
CHANGED
|
@@ -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
|
+
}
|