@mirasen/chessboard 1.0.7 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +82 -84
- package/dist/adapters/chessjs/index.d.ts +1 -0
- package/dist/adapters/chessjs/index.js +1 -0
- package/dist/adapters/chessjs/main.d.ts +19 -0
- package/dist/adapters/chessjs/main.js +134 -0
- package/dist/animation/planner.js +5 -7
- package/dist/extensions/factory/runtime.js +4 -1
- package/dist/extensions/first-party/board-events/factory.js +11 -2
- package/dist/extensions/first-party/legal-moves/types.js +4 -4
- package/dist/runtime/mutation/last-move.js +1 -1
- package/dist/state/board/check.d.ts +4 -3
- package/dist/state/board/check.js +6 -1
- package/dist/state/board/factory.js +3 -5
- package/dist/state/board/fen/main.d.ts +3 -3
- package/dist/state/board/fen/main.js +1 -1
- package/dist/state/board/position.d.ts +2 -2
- package/dist/state/board/reducers.d.ts +3 -5
- package/dist/state/board/reducers.js +13 -6
- package/dist/state/board/types/internal.d.ts +6 -4
- package/package.json +14 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @mirasen/chessboard
|
|
2
2
|
|
|
3
|
+
## 1.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 8b3781c: dependabot: directory '/', update @ktarmyshov/assert
|
|
8
|
+
- 8b3781c: dependabot: directory '/', update es-toolkit
|
|
9
|
+
- 8b3781c: dependabot: directory '/', update typescript-eslint
|
|
10
|
+
- c877d35: dependabot: directory '/', update jsdom
|
|
11
|
+
|
|
12
|
+
## 1.1.0
|
|
13
|
+
|
|
14
|
+
### Minor Changes
|
|
15
|
+
|
|
16
|
+
- 3956358: Add a stable chess.js adapter with pure move conversion helpers.
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- 3956358: Fix lifted-drag animation suppression for compound moves.
|
|
21
|
+
- 3956358: Fix `onUIMove` so it only fires for committed UI moves.
|
|
22
|
+
|
|
3
23
|
## 1.0.7
|
|
4
24
|
|
|
5
25
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
|
|
9
9
|
A framework-agnostic TypeScript chessboard platform with real chess interaction built in and an extension-driven architecture.
|
|
10
10
|
|
|
11
|
+
## Try it live
|
|
12
|
+
|
|
13
|
+
- [Play a chess.js game](https://mirasen.io/chessboard/examples/chessjs.html)
|
|
14
|
+
- [Try the promotion flow](https://mirasen.io/chessboard/examples/promotion.html)
|
|
15
|
+
- [Try the minimal example](https://mirasen.io/chessboard/examples/minimal.html)
|
|
16
|
+
|
|
11
17
|
## Why this exists
|
|
12
18
|
|
|
13
19
|
Most chessboards fall into one of two buckets:
|
|
@@ -37,16 +43,22 @@ The default first-party extension baseline includes rendering, events, selection
|
|
|
37
43
|
- `promotion`
|
|
38
44
|
- `autoPromote`
|
|
39
45
|
|
|
46
|
+
The package also includes `chess.js` adapter helpers for connecting legal destinations, UI moves, external moves, promotion, castling, and en passant without writing special-move glue code by hand.
|
|
47
|
+
|
|
40
48
|
That means the board already covers a meaningful baseline chess UX instead of stopping at “draw squares and pieces”.
|
|
41
49
|
|
|
42
50
|
### Built-in chessboard behavior
|
|
43
51
|
|
|
44
|
-
Mirasen Chessboard provides a complete modern interaction and visual baseline out of the box: selection, deselection, reselection, drag-to-move, release targeting, legal destinations, promotion, auto-promotion, and state-diff animation.
|
|
52
|
+
Mirasen Chessboard provides a complete modern interaction and visual baseline out of the box: selection, deselection, reselection, drag-to-move, release targeting, legal destinations, promotion, auto-promotion, and state-diff animation.
|
|
53
|
+
|
|
54
|
+
Moves are resolved on release rather than committed immediately on press, giving users a more forgiving interaction flow. Because animation is driven by board-state transitions, pieces can move, appear, disappear, promote, castle, or return smoothly without app-level rendering glue code.
|
|
55
|
+
|
|
56
|
+
Rules and legality still belong to your game layer. Use the built-in `chess.js` adapter when you want a ready-made bridge from `chess.js` legal moves and move results into the board.
|
|
45
57
|
|
|
46
58
|
## Highlights
|
|
47
59
|
|
|
48
60
|
- Real built-in chess interaction
|
|
49
|
-
- Easy game/rules
|
|
61
|
+
- **Easy game/rules integration** — connect your own game model, or use the built-in `chess.js` adapter
|
|
50
62
|
- Works on desktop and mobile out of the box, with pointer-based mouse and touch interaction
|
|
51
63
|
- State-diff animation beyond simple move events
|
|
52
64
|
- Extension-driven architecture
|
|
@@ -62,15 +74,6 @@ Mirasen Chessboard provides a complete modern interaction and visual baseline ou
|
|
|
62
74
|
npm install @mirasen/chessboard
|
|
63
75
|
```
|
|
64
76
|
|
|
65
|
-
## Live examples
|
|
66
|
-
|
|
67
|
-
Interactive examples are available on the Mirasen website:
|
|
68
|
-
|
|
69
|
-
- [Examples overview](https://mirasen.io/chessboard/examples/)
|
|
70
|
-
- [Minimal interactive example](https://mirasen.io/chessboard/examples/minimal.html)
|
|
71
|
-
- [Legal moves example](https://mirasen.io/chessboard/examples/legal-moves.html)
|
|
72
|
-
- [Promotion example](https://mirasen.io/chessboard/examples/promotion.html)
|
|
73
|
-
|
|
74
77
|
## Usage
|
|
75
78
|
|
|
76
79
|
### Minimal example
|
|
@@ -95,40 +98,25 @@ board.setMovability({
|
|
|
95
98
|
});
|
|
96
99
|
```
|
|
97
100
|
|
|
98
|
-
### Connect to
|
|
101
|
+
### Connect to chess.js
|
|
99
102
|
|
|
100
|
-
`@mirasen/chessboard`
|
|
103
|
+
`@mirasen/chessboard` owns board interaction, rendering, promotion UI, and animation.
|
|
104
|
+
Your game/rules layer owns legality and game state.
|
|
101
105
|
|
|
102
|
-
|
|
106
|
+
For `chess.js`, use the built-in adapter:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
npm install chess.js
|
|
110
|
+
```
|
|
103
111
|
|
|
104
112
|
```ts
|
|
113
|
+
import { createBoard } from '@mirasen/chessboard';
|
|
105
114
|
import {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
} from '
|
|
111
|
-
|
|
112
|
-
type GameMove = {
|
|
113
|
-
from: string;
|
|
114
|
-
to: string;
|
|
115
|
-
capturedSquare?: string;
|
|
116
|
-
secondary?: {
|
|
117
|
-
from: string;
|
|
118
|
-
to: string;
|
|
119
|
-
};
|
|
120
|
-
promotedTo?: 'queen' | 'rook' | 'bishop' | 'knight';
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
type GameDestination = {
|
|
124
|
-
to: string;
|
|
125
|
-
capturedSquare?: string;
|
|
126
|
-
secondary?: {
|
|
127
|
-
from: string;
|
|
128
|
-
to: string;
|
|
129
|
-
};
|
|
130
|
-
promotedTo?: ('queen' | 'rook' | 'bishop' | 'knight')[];
|
|
131
|
-
};
|
|
115
|
+
toBoardMove,
|
|
116
|
+
toBoardMoveDestinations,
|
|
117
|
+
toGameMove
|
|
118
|
+
} from '@mirasen/chessboard/adapters/chessjs';
|
|
119
|
+
import { Chess } from 'chess.js';
|
|
132
120
|
|
|
133
121
|
const element = document.getElementById('board');
|
|
134
122
|
|
|
@@ -136,70 +124,76 @@ if (!element) {
|
|
|
136
124
|
throw new Error('Missing board element');
|
|
137
125
|
}
|
|
138
126
|
|
|
139
|
-
const
|
|
127
|
+
const chess = new Chess();
|
|
140
128
|
const board = createBoard({
|
|
141
129
|
element
|
|
142
130
|
});
|
|
143
131
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
function toBoardDestination(gameDestination: GameDestination): MoveDestinationInput {
|
|
147
|
-
return {
|
|
148
|
-
to: gameDestination.to,
|
|
149
|
-
capturedSquare: gameDestination.capturedSquare,
|
|
150
|
-
secondary: gameDestination.secondary, // e.g. castling.
|
|
151
|
-
promotedTo: gameDestination.promotedTo
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function toBoardMoveRequest(gameMove: GameMove): MoveRequestInput {
|
|
156
|
-
return {
|
|
157
|
-
from: gameMove.from,
|
|
158
|
-
to: gameMove.to,
|
|
159
|
-
capturedSquare: gameMove.capturedSquare,
|
|
160
|
-
secondary: gameMove.secondary,
|
|
161
|
-
promotedTo: gameMove.promotedTo
|
|
162
|
-
};
|
|
163
|
-
}
|
|
132
|
+
// Let the user play white in this example.
|
|
133
|
+
const playerColor = 'w';
|
|
164
134
|
|
|
165
|
-
|
|
166
|
-
return {
|
|
167
|
-
from: move.from,
|
|
168
|
-
to: move.to,
|
|
169
|
-
capturedSquare: move.capturedSquare,
|
|
170
|
-
secondary: move.secondary,
|
|
171
|
-
promotedTo: move.promotedTo
|
|
172
|
-
};
|
|
173
|
-
}
|
|
135
|
+
board.setPosition(chess.fen());
|
|
174
136
|
|
|
175
137
|
board.setMovability({
|
|
176
138
|
mode: 'strict',
|
|
177
|
-
destinations: (source)
|
|
178
|
-
|
|
139
|
+
destinations: (source) => {
|
|
140
|
+
// During the computer's turn, do not allow user moves.
|
|
141
|
+
if (chess.turn() !== playerColor) {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
179
144
|
|
|
180
|
-
|
|
145
|
+
const moves = chess.moves({ square: source, verbose: true });
|
|
146
|
+
const destinations = toBoardMoveDestinations(moves);
|
|
147
|
+
|
|
148
|
+
return destinations.length > 0 ? destinations : undefined;
|
|
181
149
|
}
|
|
182
150
|
});
|
|
183
151
|
|
|
184
152
|
board.extensions.events.setOnUIMove((move) => {
|
|
185
|
-
// The board accepted and applied this UI move.
|
|
186
|
-
// Keep
|
|
187
|
-
|
|
153
|
+
// The board has already accepted and applied this UI move.
|
|
154
|
+
// Keep chess.js in sync.
|
|
155
|
+
chess.move(toGameMove(move));
|
|
156
|
+
|
|
157
|
+
if (!chess.isGameOver()) {
|
|
158
|
+
makeRandomComputerMove();
|
|
159
|
+
}
|
|
188
160
|
});
|
|
189
161
|
|
|
190
|
-
function
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
162
|
+
function makeRandomComputerMove() {
|
|
163
|
+
const moves = chess.moves({ verbose: true });
|
|
164
|
+
const randomMove = moves[Math.floor(Math.random() * moves.length)];
|
|
165
|
+
|
|
166
|
+
if (!randomMove) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Apply the move to chess.js first.
|
|
171
|
+
const appliedMove = chess.move(randomMove);
|
|
194
172
|
|
|
195
|
-
// Then
|
|
196
|
-
board.move(
|
|
173
|
+
// Then apply and animate the same move on the board.
|
|
174
|
+
board.move(toBoardMove(appliedMove));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function resetGame() {
|
|
178
|
+
chess.reset();
|
|
179
|
+
board.setPosition(chess.fen());
|
|
197
180
|
}
|
|
198
181
|
```
|
|
199
182
|
|
|
200
|
-
|
|
183
|
+
The adapter exposes three small conversion helpers:
|
|
184
|
+
|
|
185
|
+
- `toGameMove` converts a board UI move into a move accepted by `chess.move(...)`.
|
|
186
|
+
- `toBoardMove` converts a `chess.js` move result into a board move request for external or computer moves.
|
|
187
|
+
- `toBoardMoveDestinations` converts verbose legal `chess.js` moves into strict board destinations.
|
|
188
|
+
|
|
189
|
+
This keeps the boundary explicit:
|
|
201
190
|
|
|
202
|
-
|
|
191
|
+
- `chess.js` decides which moves are legal.
|
|
192
|
+
- The board handles interaction, legal targets, promotion UI, castling/en-passant board updates, and animation.
|
|
193
|
+
- User moves flow from the board into `chess.js`.
|
|
194
|
+
- External moves flow from `chess.js` back into the board.
|
|
195
|
+
|
|
196
|
+
`toBoardMoveDestinations` preserves special move information such as promotion choices, en passant capture squares, and castling secondary rook movement, so those flows work through the normal board pipeline.
|
|
203
197
|
|
|
204
198
|
### Promotion and auto-promotion
|
|
205
199
|
|
|
@@ -300,12 +294,16 @@ A board that only renders pieces and leaves everything else to app code often fo
|
|
|
300
294
|
|
|
301
295
|
A board that handles everything internally without clean boundaries becomes difficult to evolve, customize, or extend.
|
|
302
296
|
|
|
297
|
+
### Opt out, not build up
|
|
298
|
+
|
|
303
299
|
`@mirasen/chessboard` aims for a middle path:
|
|
304
300
|
|
|
305
301
|
- strong built-in baseline interaction
|
|
306
302
|
- clean internal architecture
|
|
307
303
|
- extension-driven feature growth
|
|
308
304
|
|
|
305
|
+
Common chessboard behavior is included by default: rendering, interaction, selection, legal target feedback, promotion, and animation. You start from a working interaction baseline and remove or replace extensions only when you want less or need something different.
|
|
306
|
+
|
|
309
307
|
## Project direction
|
|
310
308
|
|
|
311
309
|
This platform is being built as a foundation for richer chess learning and interaction workflows, but the board itself is designed to stand as a serious open-source UI platform in its own right.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { toBoardMove, toBoardMoveDestinations, toGameMove } from './main.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { toBoardMove, toBoardMoveDestinations, toGameMove } from './main.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Chess, Move } from 'chess.js';
|
|
2
|
+
import type { MoveRequestInput } from '../../state/board/types/input.js';
|
|
3
|
+
import type { MoveOutput } from '../../state/board/types/output.js';
|
|
4
|
+
import type { MoveDestinationInput } from '../../state/interaction/types/input.js';
|
|
5
|
+
type ChessJsMoveInput = Exclude<Parameters<Chess['move']>[0], string | null>;
|
|
6
|
+
/**
|
|
7
|
+
* Convert a board MoveOutput into the object notation accepted by chess.move().
|
|
8
|
+
*/
|
|
9
|
+
export declare function toGameMove(move: MoveOutput): ChessJsMoveInput;
|
|
10
|
+
/**
|
|
11
|
+
* Convert a chess.js Move object into MoveRequestInput.
|
|
12
|
+
*/
|
|
13
|
+
export declare function toBoardMove(move: Move): MoveRequestInput;
|
|
14
|
+
/**
|
|
15
|
+
* Convert readonly chess.js verbose moves into readonly MoveDestinationInput[].
|
|
16
|
+
* Merges promotion variants for the same from/to/special-move identity into one destination.
|
|
17
|
+
*/
|
|
18
|
+
export declare function toBoardMoveDestinations(moves: readonly Move[]): MoveDestinationInput[];
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const BOARD_TO_CHESSJS = {
|
|
2
|
+
Q: 'q',
|
|
3
|
+
R: 'r',
|
|
4
|
+
B: 'b',
|
|
5
|
+
N: 'n'
|
|
6
|
+
};
|
|
7
|
+
const CHESSJS_TO_BOARD = {
|
|
8
|
+
q: 'Q',
|
|
9
|
+
r: 'R',
|
|
10
|
+
b: 'B',
|
|
11
|
+
n: 'N'
|
|
12
|
+
};
|
|
13
|
+
function toBoardPromotion(p) {
|
|
14
|
+
const result = CHESSJS_TO_BOARD[p];
|
|
15
|
+
if (!result) {
|
|
16
|
+
throw new Error(`Unsupported chess.js promotion symbol: "${p}"`);
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
function toChessJsPromotion(p) {
|
|
21
|
+
const result = BOARD_TO_CHESSJS[p];
|
|
22
|
+
if (!result) {
|
|
23
|
+
throw new Error(`Unsupported board promotion symbol: "${p}"`);
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Convert a board MoveOutput into the object notation accepted by chess.move().
|
|
29
|
+
*/
|
|
30
|
+
export function toGameMove(move) {
|
|
31
|
+
const result = {
|
|
32
|
+
from: move.from,
|
|
33
|
+
to: move.to
|
|
34
|
+
};
|
|
35
|
+
if (move.promotedTo) {
|
|
36
|
+
result.promotion = toChessJsPromotion(move.promotedTo);
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Convert a chess.js Move object into MoveRequestInput.
|
|
42
|
+
*/
|
|
43
|
+
export function toBoardMove(move) {
|
|
44
|
+
const result = {
|
|
45
|
+
from: move.from,
|
|
46
|
+
to: move.to
|
|
47
|
+
};
|
|
48
|
+
if (move.isPromotion()) {
|
|
49
|
+
if (!move.promotion) {
|
|
50
|
+
throw new Error('Promotion move missing promotion piece');
|
|
51
|
+
}
|
|
52
|
+
result.promotedTo = toBoardPromotion(move.promotion);
|
|
53
|
+
}
|
|
54
|
+
if (move.isEnPassant()) {
|
|
55
|
+
const file = move.to[0];
|
|
56
|
+
const toRank = parseInt(move.to[1], 10);
|
|
57
|
+
const capturedRank = move.color === 'w' ? toRank - 1 : toRank + 1;
|
|
58
|
+
result.capturedSquare = `${file}${capturedRank}`;
|
|
59
|
+
}
|
|
60
|
+
if (move.isKingsideCastle()) {
|
|
61
|
+
if (move.from === 'e1' && move.to === 'g1') {
|
|
62
|
+
result.secondary = { from: 'h1', to: 'f1' };
|
|
63
|
+
}
|
|
64
|
+
else if (move.from === 'e8' && move.to === 'g8') {
|
|
65
|
+
result.secondary = { from: 'h8', to: 'f8' };
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
throw new Error(`Unsupported chess.js kingside castling move: ${move.from}-${move.to}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else if (move.isQueensideCastle()) {
|
|
72
|
+
if (move.from === 'e1' && move.to === 'c1') {
|
|
73
|
+
result.secondary = { from: 'a1', to: 'd1' };
|
|
74
|
+
}
|
|
75
|
+
else if (move.from === 'e8' && move.to === 'c8') {
|
|
76
|
+
result.secondary = { from: 'a8', to: 'd8' };
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
throw new Error(`Unsupported chess.js queenside castling move: ${move.from}-${move.to}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Convert readonly chess.js verbose moves into readonly MoveDestinationInput[].
|
|
86
|
+
* Merges promotion variants for the same from/to/special-move identity into one destination.
|
|
87
|
+
*/
|
|
88
|
+
export function toBoardMoveDestinations(moves) {
|
|
89
|
+
if (moves.length === 0)
|
|
90
|
+
return [];
|
|
91
|
+
const destinations = [];
|
|
92
|
+
const keyToIndex = new Map();
|
|
93
|
+
for (const move of moves) {
|
|
94
|
+
const boardMove = toBoardMove(move);
|
|
95
|
+
// Build identity key: from + to + capturedSquare + secondary
|
|
96
|
+
const key = [
|
|
97
|
+
boardMove.from,
|
|
98
|
+
boardMove.to,
|
|
99
|
+
boardMove.capturedSquare ?? '',
|
|
100
|
+
boardMove.secondary ? `${boardMove.secondary.from}-${boardMove.secondary.to}` : ''
|
|
101
|
+
].join('|');
|
|
102
|
+
const existingIdx = keyToIndex.get(key);
|
|
103
|
+
if (existingIdx !== undefined) {
|
|
104
|
+
// Merge promotion into existing destination
|
|
105
|
+
const existing = destinations[existingIdx];
|
|
106
|
+
if (boardMove.promotedTo) {
|
|
107
|
+
if (!existing.promotedTo) {
|
|
108
|
+
existing.promotedTo = [];
|
|
109
|
+
}
|
|
110
|
+
if (!existing.promotedTo.includes(boardMove.promotedTo)) {
|
|
111
|
+
existing.promotedTo.push(boardMove.promotedTo);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
// Create new destination
|
|
117
|
+
const dest = {
|
|
118
|
+
to: boardMove.to
|
|
119
|
+
};
|
|
120
|
+
if (boardMove.capturedSquare) {
|
|
121
|
+
dest.capturedSquare = boardMove.capturedSquare;
|
|
122
|
+
}
|
|
123
|
+
if (boardMove.secondary) {
|
|
124
|
+
dest.secondary = boardMove.secondary;
|
|
125
|
+
}
|
|
126
|
+
if (boardMove.promotedTo) {
|
|
127
|
+
dest.promotedTo = [boardMove.promotedTo];
|
|
128
|
+
}
|
|
129
|
+
keyToIndex.set(key, destinations.length);
|
|
130
|
+
destinations.push(dest);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return destinations;
|
|
134
|
+
}
|
|
@@ -239,11 +239,9 @@ export function calculateAnimationPlan(input) {
|
|
|
239
239
|
const tracks = calculateTracks(effectivePrevious.board, planningCurrent.board, exclude);
|
|
240
240
|
// 5) Build suppressedSquares: track-derived + excluded drop endpoints
|
|
241
241
|
const suppressed = collectSuppressedSquaresFromTracks(tracks);
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
return { tracks, suppressedSquares: suppressed };
|
|
242
|
+
const result = {
|
|
243
|
+
tracks,
|
|
244
|
+
suppressedSquares: suppressed
|
|
245
|
+
};
|
|
246
|
+
return result;
|
|
249
247
|
}
|
|
@@ -45,8 +45,11 @@ function createExtensionRuntimeSurfaceEvents(getInternalState, runtimeSurfaceEve
|
|
|
45
45
|
subscribers = new Set();
|
|
46
46
|
internalState.eventSubscribers.set(type, subscribers);
|
|
47
47
|
}
|
|
48
|
+
const sizeBeforeAdd = subscribers.size;
|
|
48
49
|
subscribers.add(extensionDef.id);
|
|
49
|
-
|
|
50
|
+
if (sizeBeforeAdd === 0) {
|
|
51
|
+
runtimeSurfaceEvents.subscribeEvent(type);
|
|
52
|
+
}
|
|
50
53
|
},
|
|
51
54
|
unsubscribeEvent(type) {
|
|
52
55
|
const internalState = getInternalState();
|
|
@@ -40,8 +40,17 @@ function createBoardEventsInstance() {
|
|
|
40
40
|
currentFrame: context.currentFrame
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
const lastMoveChanged = context.mutation.hasMutation({
|
|
44
|
+
causes: ['state.change.setLastMove']
|
|
45
|
+
});
|
|
46
|
+
const interactionCompleted = context.mutation.hasMutation({
|
|
47
|
+
causes: [
|
|
48
|
+
'runtime.interaction.completeCoreDragTo',
|
|
49
|
+
'runtime.interaction.completeExtensionDragTo',
|
|
50
|
+
'runtime.interaction.resolveDeferredUIMoveRequest'
|
|
51
|
+
]
|
|
52
|
+
});
|
|
53
|
+
if (interactionCompleted && lastMoveChanged && context.currentFrame.state.change.lastMove) {
|
|
45
54
|
internalState.onUIMove?.(denormalizeMove(context.currentFrame.state.change.lastMove));
|
|
46
55
|
}
|
|
47
56
|
},
|
|
@@ -6,14 +6,14 @@ export const DEFAULT_CONFIG = {
|
|
|
6
6
|
color: 'rgb(0, 0, 0)',
|
|
7
7
|
opacity: 0.14
|
|
8
8
|
},
|
|
9
|
-
radiusRatio: 0.
|
|
10
|
-
},
|
|
9
|
+
radiusRatio: 0.165
|
|
10
|
+
},
|
|
11
11
|
captureTarget: {
|
|
12
12
|
color: {
|
|
13
13
|
color: 'rgb(0, 0, 0)',
|
|
14
14
|
opacity: 0.14
|
|
15
15
|
},
|
|
16
|
-
radiusRatio: 0.
|
|
17
|
-
strokeWidthRatio: 0.
|
|
16
|
+
radiusRatio: 0.445,
|
|
17
|
+
strokeWidthRatio: 0.11
|
|
18
18
|
}
|
|
19
19
|
};
|
|
@@ -11,7 +11,7 @@ export const updateLastMoveOnBoardMove = (context, mutationSession) => {
|
|
|
11
11
|
export const clearLastMoveOnBoardSetPosition = (context, mutationSession) => {
|
|
12
12
|
const { current } = context;
|
|
13
13
|
if (!mutationSession.hasMutation({
|
|
14
|
-
causes: ['state.board.setPosition', 'state.board.setPiecePosition']
|
|
14
|
+
causes: ['state.board.setPosition', 'state.board.setPiecePosition', 'state.board.setTurn']
|
|
15
15
|
})) {
|
|
16
16
|
return;
|
|
17
17
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { Color, ColorShort, MoveRequestInput, Piece, PiecePositionInput, PiecePositionRecord, PiecePositionRecordShort, PiecePositionRecordString, PieceShort, PieceString, Role, RoleShort, SquareString } from './types/input.js';
|
|
2
|
-
import { EmptyPieceCode, MoveRequest, NonEmptyPieceCode, PieceCode, RolePromotionCode, Square, SquareFile, SquareRank } from './types/internal.js';
|
|
3
|
-
import { BoardStateSnapshot } from './types/main.js';
|
|
2
|
+
import { EmptyPieceCode, MoveRequest, NonEmptyPieceCode, PieceCode, PiecePositionSnapshot, PositionSnapshot, RolePromotionCode, Square, SquareFile, SquareRank } from './types/internal.js';
|
|
3
|
+
import type { BoardStateSnapshot } from './types/main.js';
|
|
4
4
|
export declare function isWhitePieceCode(code: NonEmptyPieceCode): boolean;
|
|
5
5
|
export declare function isBlackPieceCode(code: NonEmptyPieceCode): boolean;
|
|
6
6
|
export declare function isOccupied(board: BoardStateSnapshot, sq: Square): boolean;
|
|
7
7
|
export declare function isEmptyPieceCode(code: PieceCode): code is EmptyPieceCode;
|
|
8
8
|
export declare function isNonEmptyPieceCode(code: PieceCode): code is NonEmptyPieceCode;
|
|
9
|
-
export declare function piecePositionsEqual(pos1:
|
|
9
|
+
export declare function piecePositionsEqual(pos1: PiecePositionSnapshot, pos2: PiecePositionSnapshot): boolean;
|
|
10
|
+
export declare function positionsEqual(pos1: PositionSnapshot, pos2: PositionSnapshot): boolean;
|
|
10
11
|
export declare function isRank(n: number): n is SquareRank;
|
|
11
12
|
export declare function isFile(n: number): n is SquareFile;
|
|
12
13
|
export declare function isValidSquare(n: number): n is Square;
|
|
@@ -15,7 +15,7 @@ export function isNonEmptyPieceCode(code) {
|
|
|
15
15
|
return code !== 0 /* PieceCode.Empty */;
|
|
16
16
|
}
|
|
17
17
|
export function piecePositionsEqual(pos1, pos2) {
|
|
18
|
-
if (pos1.
|
|
18
|
+
if (pos1.pieces.length !== pos2.pieces.length)
|
|
19
19
|
return false;
|
|
20
20
|
for (let i = 0; i < pos1.pieces.length; i++) {
|
|
21
21
|
if (pos1.pieces[i] !== pos2.pieces[i])
|
|
@@ -23,6 +23,11 @@ export function piecePositionsEqual(pos1, pos2) {
|
|
|
23
23
|
}
|
|
24
24
|
return true;
|
|
25
25
|
}
|
|
26
|
+
export function positionsEqual(pos1, pos2) {
|
|
27
|
+
if (pos1.turn !== pos2.turn)
|
|
28
|
+
return false;
|
|
29
|
+
return piecePositionsEqual(pos1, pos2);
|
|
30
|
+
}
|
|
26
31
|
export function isRank(n) {
|
|
27
32
|
return Number.isInteger(n) && n >= 0 && n < 8;
|
|
28
33
|
}
|
|
@@ -2,7 +2,7 @@ import { cloneDeep } from 'es-toolkit/object';
|
|
|
2
2
|
import { isNormalizedMoveRequest } from './check.js';
|
|
3
3
|
import { normalizeColor, normalizeMoveRequest } from './normalize.js';
|
|
4
4
|
import { boardParsePiecePositionInput, boardParsePosition } from './position.js';
|
|
5
|
-
import { boardMove, boardSetPosition, boardSetTurn } from './reducers.js';
|
|
5
|
+
import { boardMove, boardSetPiecePosition, boardSetPosition, boardSetTurn } from './reducers.js';
|
|
6
6
|
function createBoardStateInternal(position) {
|
|
7
7
|
const parsedPosition = boardParsePosition(position);
|
|
8
8
|
return {
|
|
@@ -16,13 +16,11 @@ export function createBoardState(position) {
|
|
|
16
16
|
return {
|
|
17
17
|
setPosition(input, mutationSession) {
|
|
18
18
|
const position = boardParsePosition(input);
|
|
19
|
-
|
|
20
|
-
changed = boardSetTurn(internalState, position.turn) || changed;
|
|
21
|
-
return mutationSession.addMutation('state.board.setPosition', changed);
|
|
19
|
+
return mutationSession.addMutation('state.board.setPosition', boardSetPosition(internalState, position));
|
|
22
20
|
},
|
|
23
21
|
setPiecePosition(input, mutationSession) {
|
|
24
22
|
const pieces = boardParsePiecePositionInput(input);
|
|
25
|
-
return mutationSession.addMutation('state.board.setPiecePosition',
|
|
23
|
+
return mutationSession.addMutation('state.board.setPiecePosition', boardSetPiecePosition(internalState, pieces));
|
|
26
24
|
},
|
|
27
25
|
setTurn(turn, mutationSession) {
|
|
28
26
|
const normalizedTurn = normalizeColor(turn);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { FenString } from '../types/input.js';
|
|
2
|
-
import {
|
|
2
|
+
import { PositionSnapshot } from '../types/internal.js';
|
|
3
3
|
/**
|
|
4
|
-
* Parse a FEN string into a {@link
|
|
4
|
+
* Parse a FEN string into a {@link PositionSnapshot}.
|
|
5
5
|
*
|
|
6
6
|
* Only the first two FEN fields (piece placement + active color) are required
|
|
7
7
|
* for a board-only representation. The remaining fields (castling, en-passant,
|
|
8
8
|
* half-move clock, full-move number) are accepted but ignored.
|
|
9
9
|
*/
|
|
10
|
-
export declare function parseFen(fen: FenString):
|
|
10
|
+
export declare function parseFen(fen: FenString): PositionSnapshot;
|
|
@@ -74,7 +74,7 @@ function parseTurn(turn) {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
/**
|
|
77
|
-
* Parse a FEN string into a {@link
|
|
77
|
+
* Parse a FEN string into a {@link PositionSnapshot}.
|
|
78
78
|
*
|
|
79
79
|
* Only the first two FEN fields (piece placement + active color) are required
|
|
80
80
|
* for a board-only representation. The remaining fields (castling, en-passant,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { PiecePositionInput, PositionInput } from './types/input.js';
|
|
2
|
-
import {
|
|
2
|
+
import { PositionSnapshot } from './types/internal.js';
|
|
3
3
|
export declare function boardParsePiecePositionInput(input: PiecePositionInput): Uint8Array;
|
|
4
|
-
export declare function boardParsePosition(input: PositionInput):
|
|
4
|
+
export declare function boardParsePosition(input: PositionInput): PositionSnapshot;
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { ColorCode, Move, MoveRequest } from './types/internal.js';
|
|
1
|
+
import { ColorCode, Move, MoveRequest, PositionSnapshot } from './types/internal.js';
|
|
2
2
|
import { BoardStateInternal } from './types/main.js';
|
|
3
|
-
export declare function
|
|
4
|
-
/**
|
|
5
|
-
* Set active color turn.
|
|
6
|
-
*/
|
|
3
|
+
export declare function boardSetPiecePosition(state: BoardStateInternal, pieces: Uint8Array): boolean;
|
|
7
4
|
export declare function boardSetTurn(state: BoardStateInternal, turn: ColorCode): boolean;
|
|
5
|
+
export declare function boardSetPosition(state: BoardStateInternal, position: PositionSnapshot): boolean;
|
|
8
6
|
export declare function boardMove(state: BoardStateInternal, request: MoveRequest): Move;
|
|
@@ -1,19 +1,26 @@
|
|
|
1
|
-
import { isEmptyPieceCode } from './check.js';
|
|
1
|
+
import { isEmptyPieceCode, piecePositionsEqual, positionsEqual } from './check.js';
|
|
2
2
|
import { denormalizeSquare } from './denormalize.js';
|
|
3
3
|
import { fromPieceCode, toPieceCode } from './piece.js';
|
|
4
|
-
export function
|
|
4
|
+
export function boardSetPiecePosition(state, pieces) {
|
|
5
|
+
if (piecePositionsEqual(state, { pieces }))
|
|
6
|
+
return false; // no-op
|
|
5
7
|
state.pieces = new Uint8Array(pieces);
|
|
6
|
-
// Increment position epoch to prevent false animation across position resets
|
|
7
8
|
state.positionEpoch++;
|
|
8
9
|
return true;
|
|
9
10
|
}
|
|
10
|
-
/**
|
|
11
|
-
* Set active color turn.
|
|
12
|
-
*/
|
|
13
11
|
export function boardSetTurn(state, turn) {
|
|
14
12
|
if (state.turn === turn)
|
|
15
13
|
return false; // no-op
|
|
16
14
|
state.turn = turn;
|
|
15
|
+
state.positionEpoch++;
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
export function boardSetPosition(state, position) {
|
|
19
|
+
if (positionsEqual(state, position))
|
|
20
|
+
return false; // no-op
|
|
21
|
+
state.pieces = new Uint8Array(position.pieces);
|
|
22
|
+
state.turn = position.turn;
|
|
23
|
+
state.positionEpoch++;
|
|
17
24
|
return true;
|
|
18
25
|
}
|
|
19
26
|
function buildMoveBase(state, request) {
|
|
@@ -48,7 +48,9 @@ export type MoveBaseSnapshot = ReadonlyDeep<MoveBase>;
|
|
|
48
48
|
export type MoveSnapshot = ReadonlyDeep<Move>;
|
|
49
49
|
export declare const FILE_START: number;
|
|
50
50
|
export declare const RANK_START: number;
|
|
51
|
-
export
|
|
52
|
-
pieces: Uint8Array
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
export interface PiecePositionSnapshot {
|
|
52
|
+
readonly pieces: Readonly<Uint8Array>;
|
|
53
|
+
}
|
|
54
|
+
export interface PositionSnapshot extends PiecePositionSnapshot {
|
|
55
|
+
readonly turn: ColorCode;
|
|
56
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mirasen/chessboard",
|
|
3
3
|
"description": "Framework-agnostic TypeScript chessboard platform with real chess interaction built in.",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.1.1",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Mirasen",
|
|
@@ -42,6 +42,10 @@
|
|
|
42
42
|
"types": "./dist/extensions/build/index.d.ts",
|
|
43
43
|
"import": "./dist/extensions/build/index.js"
|
|
44
44
|
},
|
|
45
|
+
"./adapters/chessjs": {
|
|
46
|
+
"types": "./dist/adapters/chessjs/index.d.ts",
|
|
47
|
+
"import": "./dist/adapters/chessjs/index.js"
|
|
48
|
+
},
|
|
45
49
|
"./unstable/*": {
|
|
46
50
|
"types": "./dist/*.d.ts",
|
|
47
51
|
"import": "./dist/*.js"
|
|
@@ -57,7 +61,7 @@
|
|
|
57
61
|
"check": "tsc --skipLibCheck --noEmit",
|
|
58
62
|
"check:test": "tsc --project tsconfig-test.json --skipLibCheck --noEmit",
|
|
59
63
|
"test": "npm run check:test && vitest run --run",
|
|
60
|
-
"coverage": "vitest run --coverage",
|
|
64
|
+
"coverage": "npm run check:test && vitest run --coverage",
|
|
61
65
|
"changeset:version": "changeset version && npm install && npm run format && git add --all",
|
|
62
66
|
"changeset:publish": "changeset publish"
|
|
63
67
|
},
|
|
@@ -100,5 +104,13 @@
|
|
|
100
104
|
"@ktarmyshov/assert": "^0.1.9",
|
|
101
105
|
"es-toolkit": "^1.45.1",
|
|
102
106
|
"type-fest": "^5.5.0"
|
|
107
|
+
},
|
|
108
|
+
"peerDependencies": {
|
|
109
|
+
"chess.js": "^1.4.0"
|
|
110
|
+
},
|
|
111
|
+
"peerDependenciesMeta": {
|
|
112
|
+
"chess.js": {
|
|
113
|
+
"optional": true
|
|
114
|
+
}
|
|
103
115
|
}
|
|
104
116
|
}
|