@mirasen/chessboard 1.0.7 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @mirasen/chessboard
2
2
 
3
+ ## 1.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 3956358: Add a stable chess.js adapter with pure move conversion helpers.
8
+
9
+ ### Patch Changes
10
+
11
+ - 3956358: Fix lifted-drag animation suppression for compound moves.
12
+ - 3956358: Fix `onUIMove` so it only fires for committed UI moves.
13
+
3
14
  ## 1.0.7
4
15
 
5
16
  ### Patch Changes
package/README.md CHANGED
@@ -37,16 +37,22 @@ The default first-party extension baseline includes rendering, events, selection
37
37
  - `promotion`
38
38
  - `autoPromote`
39
39
 
40
+ 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.
41
+
40
42
  That means the board already covers a meaningful baseline chess UX instead of stopping at “draw squares and pieces”.
41
43
 
42
44
  ### Built-in chessboard behavior
43
45
 
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. 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, or return smoothly without app-level glue code.
46
+ 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.
47
+
48
+ 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.
49
+
50
+ 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
51
 
46
52
  ## Highlights
47
53
 
48
54
  - Real built-in chess interaction
49
- - Easy game/rules layer integration
55
+ - **Easy game/rules integration** — connect your own game model, or use the built-in `chess.js` adapter
50
56
  - Works on desktop and mobile out of the box, with pointer-based mouse and touch interaction
51
57
  - State-diff animation beyond simple move events
52
58
  - Extension-driven architecture
@@ -68,8 +74,8 @@ Interactive examples are available on the Mirasen website:
68
74
 
69
75
  - [Examples overview](https://mirasen.io/chessboard/examples/)
70
76
  - [Minimal interactive example](https://mirasen.io/chessboard/examples/minimal.html)
71
- - [Legal moves example](https://mirasen.io/chessboard/examples/legal-moves.html)
72
77
  - [Promotion example](https://mirasen.io/chessboard/examples/promotion.html)
78
+ - [chess.js game example](https://mirasen.io/chessboard/examples/chessjs.html)
73
79
 
74
80
  ## Usage
75
81
 
@@ -95,40 +101,25 @@ board.setMovability({
95
101
  });
96
102
  ```
97
103
 
98
- ### Connect to a game/rules layer
104
+ ### Connect to chess.js
99
105
 
100
- `@mirasen/chessboard` handles board interaction and move intent. Your application can keep chess rules and game state in its own game/rules layer.
106
+ `@mirasen/chessboard` owns board interaction, rendering, promotion UI, and animation.
107
+ Your game/rules layer owns legality and game state.
101
108
 
102
- A typical integration uses a small adapter between your game move format and the board move format. The `game` object below represents your own chess rules layer, such as chess.js, a server-backed game model, or a custom engine.
109
+ For `chess.js`, use the built-in adapter:
110
+
111
+ ```bash
112
+ npm install chess.js
113
+ ```
103
114
 
104
115
  ```ts
116
+ import { createBoard } from '@mirasen/chessboard';
105
117
  import {
106
- createBoard,
107
- type MoveDestinationInput,
108
- type MoveOutput,
109
- type MoveRequestInput
110
- } from '@mirasen/chessboard';
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
- };
118
+ toBoardMove,
119
+ toBoardMoveDestinations,
120
+ toGameMove
121
+ } from '@mirasen/chessboard/adapters/chessjs';
122
+ import { Chess } from 'chess.js';
132
123
 
133
124
  const element = document.getElementById('board');
134
125
 
@@ -136,70 +127,76 @@ if (!element) {
136
127
  throw new Error('Missing board element');
137
128
  }
138
129
 
139
- const game = createYourGameEngine(); // Your app/game layer.
130
+ const chess = new Chess();
140
131
  const board = createBoard({
141
132
  element
142
133
  });
143
134
 
144
- board.setPosition(game.getPosition());
135
+ // Let the user play white in this example.
136
+ const playerColor = 'w';
145
137
 
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
- }
164
-
165
- function toGameMove(move: MoveOutput): GameMove {
166
- return {
167
- from: move.from,
168
- to: move.to,
169
- capturedSquare: move.capturedSquare,
170
- secondary: move.secondary,
171
- promotedTo: move.promotedTo
172
- };
173
- }
138
+ board.setPosition(chess.fen());
174
139
 
175
140
  board.setMovability({
176
141
  mode: 'strict',
177
- destinations: (source): MoveDestinationInput[] | undefined => {
178
- const gameDestinations = game.getDestinationsFor(source);
142
+ destinations: (source) => {
143
+ // During the computer's turn, do not allow user moves.
144
+ if (chess.turn() !== playerColor) {
145
+ return undefined;
146
+ }
147
+
148
+ const moves = chess.moves({ square: source, verbose: true });
149
+ const destinations = toBoardMoveDestinations(moves);
179
150
 
180
- return gameDestinations.length > 0 ? gameDestinations.map(toBoardDestination) : undefined;
151
+ return destinations.length > 0 ? destinations : undefined;
181
152
  }
182
153
  });
183
154
 
184
155
  board.extensions.events.setOnUIMove((move) => {
185
- // The board accepted and applied this UI move.
186
- // Keep your game/rules layer in sync.
187
- game.move(toGameMove(move));
156
+ // The board has already accepted and applied this UI move.
157
+ // Keep chess.js in sync.
158
+ chess.move(toGameMove(move));
159
+
160
+ if (!chess.isGameOver()) {
161
+ makeRandomComputerMove();
162
+ }
188
163
  });
189
164
 
190
- function applyExternalMove(gameMove: GameMove) {
191
- // Apply moves that come from outside the board UI:
192
- // server, engine, opponent, imported game, replay, etc.
193
- game.move(gameMove);
165
+ function makeRandomComputerMove() {
166
+ const moves = chess.moves({ verbose: true });
167
+ const randomMove = moves[Math.floor(Math.random() * moves.length)];
168
+
169
+ if (!randomMove) {
170
+ return;
171
+ }
172
+
173
+ // Apply the move to chess.js first.
174
+ const appliedMove = chess.move(randomMove);
194
175
 
195
- // Then animate/apply the same move on the board.
196
- board.move(toBoardMoveRequest(gameMove));
176
+ // Then apply and animate the same move on the board.
177
+ board.move(toBoardMove(appliedMove));
178
+ }
179
+
180
+ function resetGame() {
181
+ chess.reset();
182
+ board.setPosition(chess.fen());
197
183
  }
198
184
  ```
199
185
 
200
- `MoveDestinationInput` can describe more than a target square. For example, `secondary` can represent paired piece movement (e.g. castling), `capturedSquare` can represent captures away from the destination square (e.g. en passant), and `promotedTo` can represent promotion choices.
186
+ The adapter exposes three small conversion helpers:
187
+
188
+ - `toGameMove` converts a board UI move into a move accepted by `chess.move(...)`.
189
+ - `toBoardMove` converts a `chess.js` move result into a board move request for external or computer moves.
190
+ - `toBoardMoveDestinations` converts verbose legal `chess.js` moves into strict board destinations.
201
191
 
202
- This lets your game/rules layer own chess legality and game state while the board owns interaction, selection, targeting, visual feedback, promotion UI, animation, and its own displayed position.
192
+ This keeps the boundary explicit:
193
+
194
+ - `chess.js` decides which moves are legal.
195
+ - The board handles interaction, legal targets, promotion UI, castling/en-passant board updates, and animation.
196
+ - User moves flow from the board into `chess.js`.
197
+ - External moves flow from `chess.js` back into the board.
198
+
199
+ `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
200
 
204
201
  ### Promotion and auto-promotion
205
202
 
@@ -326,3 +323,7 @@ For details, see:
326
323
 
327
324
  - [./assets/pieces/chessnut/ATTRIBUTION.md](./assets/pieces/chessnut/ATTRIBUTION.md)
328
325
  - [./assets/pieces/chessnut/LICENSE.txt](./assets/pieces/chessnut/LICENSE.txt)
326
+
327
+ ```
328
+
329
+ ```
@@ -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
- if (droppedMove !== null) {
243
- // The dropped move was intentionally suppressed (no move track emitted),
244
- // but the base render must still hide both endpoints.
245
- suppressed.add(droppedMove.from);
246
- suppressed.add(droppedMove.to);
247
- }
248
- return { tracks, suppressedSquares: suppressed };
242
+ const result = {
243
+ tracks,
244
+ suppressedSquares: suppressed
245
+ };
246
+ return result;
249
247
  }
@@ -40,8 +40,17 @@ function createBoardEventsInstance() {
40
40
  currentFrame: context.currentFrame
41
41
  });
42
42
  }
43
- if (context.mutation.hasMutation({ causes: ['state.change.setLastMove'] }) &&
44
- context.currentFrame.state.change.lastMove) {
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
  },
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.0.7",
4
+ "version": "1.1.0",
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"
@@ -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
  }