@dxos/react-ui-gameboard 0.8.4-main.c1de068 → 0.8.4-main.c85a9c8dae
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/README.md +2 -0
- package/dist/lib/browser/index.mjs +736 -788
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +736 -788
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/Chessboard/Chessboard.d.ts +20 -0
- package/dist/types/src/components/Chessboard/Chessboard.d.ts.map +1 -0
- package/dist/types/src/components/Chessboard/Chessboard.stories.d.ts +30 -0
- package/dist/types/src/components/Chessboard/Chessboard.stories.d.ts.map +1 -0
- package/dist/types/src/components/Chessboard/chess.d.ts +60 -0
- package/dist/types/src/components/Chessboard/chess.d.ts.map +1 -0
- package/dist/types/src/components/Chessboard/chess.test.d.ts +2 -0
- package/dist/types/src/components/Chessboard/chess.test.d.ts.map +1 -0
- package/dist/types/src/components/Chessboard/index.d.ts.map +1 -0
- package/dist/types/src/components/Gameboard/Gameboard.d.ts +37 -0
- package/dist/types/src/components/Gameboard/Gameboard.d.ts.map +1 -0
- package/dist/types/src/{Gameboard → components/Gameboard}/Piece.d.ts +3 -2
- package/dist/types/src/components/Gameboard/Piece.d.ts.map +1 -0
- package/dist/types/src/components/Gameboard/Square.d.ts.map +1 -0
- package/dist/types/src/components/Gameboard/index.d.ts +4 -0
- package/dist/types/src/components/Gameboard/index.d.ts.map +1 -0
- package/dist/types/src/{Gameboard → components/Gameboard}/types.d.ts +3 -2
- package/dist/types/src/components/Gameboard/types.d.ts.map +1 -0
- package/dist/types/src/components/Gameboard/util.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +3 -0
- package/dist/types/src/components/index.d.ts.map +1 -0
- package/dist/types/src/gen/pieces/chess/alpha/bB.d.ts +2 -2
- package/dist/types/src/gen/pieces/chess/alpha/bB.d.ts.map +1 -1
- package/dist/types/src/gen/pieces/chess/alpha/bK.d.ts +2 -2
- package/dist/types/src/gen/pieces/chess/alpha/bN.d.ts +2 -2
- package/dist/types/src/gen/pieces/chess/alpha/bP.d.ts +2 -2
- package/dist/types/src/gen/pieces/chess/alpha/bQ.d.ts +2 -2
- package/dist/types/src/gen/pieces/chess/alpha/bQ.d.ts.map +1 -1
- package/dist/types/src/gen/pieces/chess/alpha/bR.d.ts +2 -2
- package/dist/types/src/gen/pieces/chess/alpha/index.d.ts +12 -12
- package/dist/types/src/gen/pieces/chess/alpha/wB.d.ts +2 -2
- package/dist/types/src/gen/pieces/chess/alpha/wK.d.ts +2 -2
- package/dist/types/src/gen/pieces/chess/alpha/wN.d.ts +2 -2
- package/dist/types/src/gen/pieces/chess/alpha/wP.d.ts +2 -2
- package/dist/types/src/gen/pieces/chess/alpha/wQ.d.ts +2 -2
- package/dist/types/src/gen/pieces/chess/alpha/wR.d.ts +2 -2
- package/dist/types/src/gen/pieces/chess/cburnett/bB.d.ts +2 -2
- package/dist/types/src/gen/pieces/chess/cburnett/bB.d.ts.map +1 -1
- package/dist/types/src/gen/pieces/chess/cburnett/bK.d.ts +2 -2
- package/dist/types/src/gen/pieces/chess/cburnett/bK.d.ts.map +1 -1
- package/dist/types/src/gen/pieces/chess/cburnett/bN.d.ts +2 -2
- package/dist/types/src/gen/pieces/chess/cburnett/bN.d.ts.map +1 -1
- package/dist/types/src/gen/pieces/chess/cburnett/bP.d.ts +2 -2
- package/dist/types/src/gen/pieces/chess/cburnett/bQ.d.ts +2 -2
- package/dist/types/src/gen/pieces/chess/cburnett/bQ.d.ts.map +1 -1
- package/dist/types/src/gen/pieces/chess/cburnett/bR.d.ts +2 -2
- package/dist/types/src/gen/pieces/chess/cburnett/bR.d.ts.map +1 -1
- package/dist/types/src/gen/pieces/chess/cburnett/index.d.ts +12 -12
- package/dist/types/src/gen/pieces/chess/cburnett/wB.d.ts +2 -2
- package/dist/types/src/gen/pieces/chess/cburnett/wB.d.ts.map +1 -1
- package/dist/types/src/gen/pieces/chess/cburnett/wK.d.ts +2 -2
- package/dist/types/src/gen/pieces/chess/cburnett/wK.d.ts.map +1 -1
- package/dist/types/src/gen/pieces/chess/cburnett/wN.d.ts +2 -2
- package/dist/types/src/gen/pieces/chess/cburnett/wN.d.ts.map +1 -1
- package/dist/types/src/gen/pieces/chess/cburnett/wP.d.ts +2 -2
- package/dist/types/src/gen/pieces/chess/cburnett/wQ.d.ts +2 -2
- package/dist/types/src/gen/pieces/chess/cburnett/wQ.d.ts.map +1 -1
- package/dist/types/src/gen/pieces/chess/cburnett/wR.d.ts +2 -2
- package/dist/types/src/gen/pieces/chess/cburnett/wR.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -2
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +27 -22
- package/src/{Chessboard → components/Chessboard}/Chessboard.stories.tsx +32 -29
- package/src/{Chessboard → components/Chessboard}/Chessboard.tsx +72 -60
- package/src/components/Chessboard/chess.test.ts +19 -0
- package/src/components/Chessboard/chess.ts +325 -0
- package/src/components/Gameboard/Gameboard.tsx +139 -0
- package/src/{Gameboard → components/Gameboard}/Piece.tsx +27 -23
- package/src/{Gameboard → components/Gameboard}/Square.tsx +8 -6
- package/src/{Gameboard → components/Gameboard}/index.ts +0 -3
- package/src/{Gameboard → components/Gameboard}/types.ts +4 -2
- package/src/components/index.ts +6 -0
- package/src/gen/pieces/chess/alpha/bB.tsx +9 -6
- package/src/gen/pieces/chess/alpha/bK.tsx +7 -7
- package/src/gen/pieces/chess/alpha/bN.tsx +7 -7
- package/src/gen/pieces/chess/alpha/bP.tsx +5 -5
- package/src/gen/pieces/chess/alpha/bQ.tsx +9 -6
- package/src/gen/pieces/chess/alpha/bR.tsx +6 -6
- package/src/gen/pieces/chess/alpha/index.ts +12 -12
- package/src/gen/pieces/chess/alpha/wB.tsx +7 -7
- package/src/gen/pieces/chess/alpha/wK.tsx +7 -7
- package/src/gen/pieces/chess/alpha/wN.tsx +7 -7
- package/src/gen/pieces/chess/alpha/wP.tsx +7 -7
- package/src/gen/pieces/chess/alpha/wQ.tsx +7 -7
- package/src/gen/pieces/chess/alpha/wR.tsx +7 -7
- package/src/gen/pieces/chess/cburnett/bB.tsx +20 -9
- package/src/gen/pieces/chess/cburnett/bK.tsx +25 -15
- package/src/gen/pieces/chess/cburnett/bN.tsx +20 -13
- package/src/gen/pieces/chess/cburnett/bP.tsx +6 -6
- package/src/gen/pieces/chess/cburnett/bQ.tsx +19 -13
- package/src/gen/pieces/chess/cburnett/bR.tsx +27 -11
- package/src/gen/pieces/chess/cburnett/index.ts +12 -12
- package/src/gen/pieces/chess/cburnett/wB.tsx +19 -9
- package/src/gen/pieces/chess/cburnett/wK.tsx +19 -12
- package/src/gen/pieces/chess/cburnett/wN.tsx +16 -9
- package/src/gen/pieces/chess/cburnett/wP.tsx +7 -7
- package/src/gen/pieces/chess/cburnett/wQ.tsx +19 -9
- package/src/gen/pieces/chess/cburnett/wR.tsx +19 -9
- package/src/index.ts +1 -2
- package/dist/types/src/Chessboard/Chessboard.d.ts +0 -15
- package/dist/types/src/Chessboard/Chessboard.d.ts.map +0 -1
- package/dist/types/src/Chessboard/Chessboard.stories.d.ts +0 -16
- package/dist/types/src/Chessboard/Chessboard.stories.d.ts.map +0 -1
- package/dist/types/src/Chessboard/chess.d.ts +0 -40
- package/dist/types/src/Chessboard/chess.d.ts.map +0 -1
- package/dist/types/src/Chessboard/index.d.ts.map +0 -1
- package/dist/types/src/Gameboard/Gameboard.d.ts +0 -23
- package/dist/types/src/Gameboard/Gameboard.d.ts.map +0 -1
- package/dist/types/src/Gameboard/Piece.d.ts.map +0 -1
- package/dist/types/src/Gameboard/Square.d.ts.map +0 -1
- package/dist/types/src/Gameboard/context.d.ts +0 -10
- package/dist/types/src/Gameboard/context.d.ts.map +0 -1
- package/dist/types/src/Gameboard/index.d.ts +0 -7
- package/dist/types/src/Gameboard/index.d.ts.map +0 -1
- package/dist/types/src/Gameboard/types.d.ts.map +0 -1
- package/dist/types/src/Gameboard/util.d.ts.map +0 -1
- package/src/Chessboard/chess.ts +0 -213
- package/src/Gameboard/Gameboard.tsx +0 -103
- package/src/Gameboard/context.ts +0 -22
- /package/dist/types/src/{Chessboard → components/Chessboard}/index.d.ts +0 -0
- /package/dist/types/src/{Gameboard → components/Gameboard}/Square.d.ts +0 -0
- /package/dist/types/src/{Gameboard → components/Gameboard}/util.d.ts +0 -0
- /package/src/{Chessboard → components/Chessboard}/index.ts +0 -0
- /package/src/{Gameboard → components/Gameboard}/util.ts +0 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Atom, type Registry } from '@effect-atom/atom-react';
|
|
6
|
+
import { Chess as ChessJS } from 'chess.js';
|
|
7
|
+
import { type FC, type SVGProps } from 'react';
|
|
8
|
+
|
|
9
|
+
import { invariant } from '@dxos/invariant';
|
|
10
|
+
import { log } from '@dxos/log';
|
|
11
|
+
|
|
12
|
+
import * as Alpha from '../../gen/pieces/chess/alpha';
|
|
13
|
+
import {
|
|
14
|
+
type GameboardModel,
|
|
15
|
+
type Location,
|
|
16
|
+
type Move,
|
|
17
|
+
type PieceMap,
|
|
18
|
+
type PieceType,
|
|
19
|
+
type Player,
|
|
20
|
+
locationToString,
|
|
21
|
+
} from '../Gameboard';
|
|
22
|
+
|
|
23
|
+
export type ChessPiece = 'BK' | 'BQ' | 'BR' | 'BB' | 'BN' | 'BP' | 'WK' | 'WQ' | 'WR' | 'WB' | 'WN' | 'WP';
|
|
24
|
+
|
|
25
|
+
export const ChessPieces: Record<ChessPiece, FC<SVGProps<SVGSVGElement>>> = Alpha;
|
|
26
|
+
|
|
27
|
+
export const posToLocation = (pos: string): Location => {
|
|
28
|
+
const col = pos.charCodeAt(0) - 'a'.charCodeAt(0);
|
|
29
|
+
const row = parseInt(pos[1]) - 1;
|
|
30
|
+
return [row, col];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const locationToPos = ([row, col]: Location): string => {
|
|
34
|
+
return String.fromCharCode(col + 'a'.charCodeAt(0)) + (row + 1);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const getRawPgn = (pgn: string) => {
|
|
38
|
+
return pgn.replace(/\[.*?\]/g, '').trim();
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const styles = {
|
|
42
|
+
neutral: {
|
|
43
|
+
black: 'bg-neutral-50',
|
|
44
|
+
white: 'bg-neutral-200',
|
|
45
|
+
promotion: 'bg-neutral-200 hover:bg-neutral-300 opacity-70 hover:opacity-100',
|
|
46
|
+
},
|
|
47
|
+
original: {
|
|
48
|
+
black: 'bg-[#6C95B9]',
|
|
49
|
+
white: 'bg-[#CCD3DB]',
|
|
50
|
+
promotion: 'duration-500 bg-[#CCD3DB] opacity-70 hover:opacity-100',
|
|
51
|
+
},
|
|
52
|
+
blue: {
|
|
53
|
+
black: 'bg-[#608BC1]',
|
|
54
|
+
white: 'bg-[#CBDCEB]',
|
|
55
|
+
promotion: 'duration-500 bg-[#CBDCEB] opacity-70 hover:opacity-100',
|
|
56
|
+
},
|
|
57
|
+
green: {
|
|
58
|
+
black: 'bg-[#8EB486]',
|
|
59
|
+
white: 'bg-[#FDF7F4]',
|
|
60
|
+
promotion: 'duration-500 bg-[#FDF7F4] opacity-70 hover:opacity-100',
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const boardStyles = styles.original;
|
|
65
|
+
|
|
66
|
+
export const getSquareColor = ([row, col]: Location) => {
|
|
67
|
+
return (col + row) % 2 === 0 ? boardStyles.black : boardStyles.white;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const createChess = (pgn?: string): ChessJS => {
|
|
71
|
+
const chess = new ChessJS();
|
|
72
|
+
if (pgn) {
|
|
73
|
+
try {
|
|
74
|
+
chess.loadPgn(pgn);
|
|
75
|
+
} catch {
|
|
76
|
+
log.warn(pgn);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return chess;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Chess model.
|
|
85
|
+
*/
|
|
86
|
+
export class ChessModel implements GameboardModel<ChessPiece> {
|
|
87
|
+
private readonly _chess = new ChessJS();
|
|
88
|
+
private readonly _pieces = Atom.make<PieceMap<ChessPiece>>({});
|
|
89
|
+
private readonly _moveIndex = Atom.make(0);
|
|
90
|
+
|
|
91
|
+
constructor(
|
|
92
|
+
private readonly _registry: Registry.Registry,
|
|
93
|
+
pgn?: string,
|
|
94
|
+
) {
|
|
95
|
+
this.update(pgn);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
get readonly(): boolean {
|
|
99
|
+
return this._registry.get(this._moveIndex) !== this._chess.history().length;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
get turn(): Player {
|
|
103
|
+
return this._chess.turn() === 'w' ? 'white' : 'black';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
get game(): ChessJS {
|
|
107
|
+
return this._chess;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
get pieces(): Atom.Atom<PieceMap<ChessPiece>> {
|
|
111
|
+
return this._pieces;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
get moveIndex(): Atom.Atom<number> {
|
|
115
|
+
return this._moveIndex;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
get fen() {
|
|
119
|
+
return this._chess.fen();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* PGN with headers.
|
|
124
|
+
*
|
|
125
|
+
* [Event "?"]
|
|
126
|
+
* [Site "?"]
|
|
127
|
+
* [Date "2025.08.05"]
|
|
128
|
+
* [Round "?"]
|
|
129
|
+
* [White "?"]
|
|
130
|
+
* [Black "?"]
|
|
131
|
+
* [Result "*"]
|
|
132
|
+
*/
|
|
133
|
+
// TODO(burdon): Update headers.
|
|
134
|
+
get pgn(): string {
|
|
135
|
+
return getRawPgn(this._chess.pgn());
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
setMoveIndex(index: number) {
|
|
139
|
+
const temp = new ChessJS();
|
|
140
|
+
const history = this._chess.history({ verbose: true });
|
|
141
|
+
for (let i = 0; i < index && i < history.length; i++) {
|
|
142
|
+
temp.move(history[i]);
|
|
143
|
+
}
|
|
144
|
+
this._updateBoard(temp);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
update(pgn = ''): void {
|
|
148
|
+
const previous = this._chess.history();
|
|
149
|
+
try {
|
|
150
|
+
this._chess.loadPgn(pgn);
|
|
151
|
+
// TODO(burdon): Get from TS.
|
|
152
|
+
// TODO(burdon): Update if not set.
|
|
153
|
+
this._chess.setHeader('Date', createDate());
|
|
154
|
+
this._chess.setHeader('Site', 'dxos.org');
|
|
155
|
+
// TODO(burdon): Update player keys.
|
|
156
|
+
// this._chess.setHeader('White', 'White');
|
|
157
|
+
// this._chess.setHeader('Black', 'Black');
|
|
158
|
+
} catch {
|
|
159
|
+
// Ignore.
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const current = this._chess.history();
|
|
163
|
+
if (!isValidNextMove(previous, current)) {
|
|
164
|
+
this._registry.set(this._pieces, {});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
this._updateBoard(this._chess);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
isValidMove(move: Move): boolean {
|
|
171
|
+
return tryMove(new ChessJS(this._chess.fen()), move) !== null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
canPromote(move: Move): boolean {
|
|
175
|
+
const isPawnMove = move.piece === 'BP' || move.piece === 'WP';
|
|
176
|
+
const isToLastRank = move.to[0] === 0 || move.to[0] === 7;
|
|
177
|
+
return isPawnMove && isToLastRank;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
makeMove(move: Move): boolean {
|
|
181
|
+
const game = tryMove(this._chess, move);
|
|
182
|
+
if (!game) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
this._updateBoard(this._chess);
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
makeRandomMove(): boolean {
|
|
191
|
+
const moves = this._chess.moves();
|
|
192
|
+
if (!moves.length) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const move = moves[Math.floor(Math.random() * moves.length)];
|
|
197
|
+
this._chess.move(move);
|
|
198
|
+
|
|
199
|
+
this._updateBoard(this._chess);
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Update pieces preserving identity.
|
|
205
|
+
*/
|
|
206
|
+
private _updateBoard(chess: ChessJS): void {
|
|
207
|
+
this._registry.set(this._pieces, createPieceMap(chess));
|
|
208
|
+
this._registry.set(this._moveIndex, chess.history().length);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Attempt move.
|
|
214
|
+
*/
|
|
215
|
+
const tryMove = (chess: ChessJS, move: Move): ChessJS | null => {
|
|
216
|
+
const from = locationToPos(move.from);
|
|
217
|
+
const to = locationToPos(move.to);
|
|
218
|
+
try {
|
|
219
|
+
const promotion = move.promotion ? move.promotion[1].toLowerCase() : 'q';
|
|
220
|
+
chess.move({ from, to, promotion }, { strict: false });
|
|
221
|
+
return chess;
|
|
222
|
+
} catch {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const isValidNextMove = (previous: string[], current: string[]) => {
|
|
228
|
+
if (current.length > previous.length + 1) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
for (let i = 0; i < previous.length; i++) {
|
|
233
|
+
if (previous[i] !== current[i]) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return true;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Starting from a new game, assign piece IDs based on their starting position.
|
|
243
|
+
* Then iterate through the history of the provided game and update the piece map.
|
|
244
|
+
*/
|
|
245
|
+
export const createPieceMap = (chess: ChessJS): PieceMap<ChessPiece> => {
|
|
246
|
+
const temp = new ChessJS();
|
|
247
|
+
let pieces = _createPieceMap(temp);
|
|
248
|
+
const history = chess.history({ verbose: true });
|
|
249
|
+
for (let i = 0; i < history.length; i++) {
|
|
250
|
+
const move = history[i];
|
|
251
|
+
temp.move(move);
|
|
252
|
+
pieces = _diffPieces(pieces, _createPieceMap(temp));
|
|
253
|
+
const test = new Set();
|
|
254
|
+
Object.values(pieces).forEach((piece) => {
|
|
255
|
+
invariant(!test.has(piece.id), 'Duplicate: ' + piece.id);
|
|
256
|
+
test.add(piece.id);
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return pieces;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Create a map of pieces from the board positions; assign each piece the ID of the current square.
|
|
265
|
+
*/
|
|
266
|
+
const _createPieceMap = (chess: ChessJS): PieceMap<ChessPiece> => {
|
|
267
|
+
const pieces: PieceMap<ChessPiece> = {};
|
|
268
|
+
chess.board().flatMap((row) =>
|
|
269
|
+
row.forEach((record) => {
|
|
270
|
+
if (!record) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const { square, type, color } = record;
|
|
275
|
+
const pieceType = `${color.toUpperCase()}${type.toUpperCase()}` as ChessPiece;
|
|
276
|
+
const location = posToLocation(square);
|
|
277
|
+
pieces[locationToString(location)] = {
|
|
278
|
+
id: `${square}-${pieceType}`,
|
|
279
|
+
type: pieceType,
|
|
280
|
+
side: color === 'w' ? 'white' : 'black',
|
|
281
|
+
location,
|
|
282
|
+
};
|
|
283
|
+
}),
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
return pieces;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Preserve the original piece objects (and IDs).
|
|
291
|
+
*/
|
|
292
|
+
const _diffPieces = <T extends PieceType>(before: PieceMap<T>, after: PieceMap<T>): PieceMap<T> => {
|
|
293
|
+
const difference: { added: PieceMap; removed: PieceMap } = {
|
|
294
|
+
removed: {},
|
|
295
|
+
added: {},
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// Removed.
|
|
299
|
+
(Object.keys(before) as Array<keyof typeof before>).forEach((square) => {
|
|
300
|
+
if (after[square]?.type !== before[square]?.type) {
|
|
301
|
+
difference.removed[square] = before[square];
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Added.
|
|
306
|
+
(Object.keys(after) as Array<keyof typeof after>).forEach((square) => {
|
|
307
|
+
if (before[square]?.type !== after[square]?.type) {
|
|
308
|
+
difference.added[square] = after[square];
|
|
309
|
+
} else {
|
|
310
|
+
after[square] = before[square];
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Preserve IDs.
|
|
315
|
+
for (const piece of Object.values(difference.added)) {
|
|
316
|
+
const previous = Object.values(difference.removed).find((p) => p.type === piece.type);
|
|
317
|
+
if (previous) {
|
|
318
|
+
piece.id = previous.id;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return after;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const createDate = (date = new Date()) => date.toISOString().slice(0, 10).replace(/-/g, '.'); // e.g., "2025.08.05"
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
|
6
|
+
import { createContext } from '@radix-ui/react-context';
|
|
7
|
+
import React, { type PropsWithChildren, forwardRef, useCallback, useEffect, useState } from 'react';
|
|
8
|
+
|
|
9
|
+
import { log } from '@dxos/log';
|
|
10
|
+
import { type ThemedClassName } from '@dxos/react-ui';
|
|
11
|
+
import { mx } from '@dxos/ui-theme';
|
|
12
|
+
|
|
13
|
+
import { Piece, type PieceProps } from './Piece';
|
|
14
|
+
import { Square, type SquareProps } from './Square';
|
|
15
|
+
import { type GameboardModel, type Move, type PieceRecord, isLocation, isPiece } from './types';
|
|
16
|
+
|
|
17
|
+
export type GameboardContextValue<M extends GameboardModel<any>> = {
|
|
18
|
+
model: M;
|
|
19
|
+
dragging?: boolean; // TODO(burdon): Change to PieceRecord.
|
|
20
|
+
promoting?: PieceRecord;
|
|
21
|
+
onPromotion: (move: Move) => void;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const [GameboardContextProvider, useRadixGameboardContext] = createContext<GameboardContextValue<any>>('Gameboard');
|
|
25
|
+
|
|
26
|
+
const useGameboardContext = <M extends GameboardModel<any>>(consumerName: string): GameboardContextValue<M> => {
|
|
27
|
+
return useRadixGameboardContext(consumerName);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
//
|
|
31
|
+
// Root
|
|
32
|
+
//
|
|
33
|
+
|
|
34
|
+
type GameboardRootProps<M extends GameboardModel<any>> = PropsWithChildren<{
|
|
35
|
+
model?: M;
|
|
36
|
+
onDrop?: (move: Move) => boolean;
|
|
37
|
+
}>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Generic board container.
|
|
41
|
+
*/
|
|
42
|
+
const GameboardRoot = <M extends GameboardModel<any>>({ children, model, onDrop }: GameboardRootProps<M>) => {
|
|
43
|
+
const [dragging, setDragging] = useState(false);
|
|
44
|
+
const [promoting, setPromoting] = useState<PieceRecord | undefined>();
|
|
45
|
+
|
|
46
|
+
const handlePromotion = useCallback<GameboardContextValue<GameboardModel<any>>['onPromotion']>((move) => {
|
|
47
|
+
log('onPromotion', { move });
|
|
48
|
+
setPromoting(undefined);
|
|
49
|
+
onDrop?.(move);
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (!model) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// TODO(burdon): Should target specific container.
|
|
58
|
+
return monitorForElements({
|
|
59
|
+
onDragStart: ({ source }) => {
|
|
60
|
+
log('onDragStart', { source });
|
|
61
|
+
setDragging(true);
|
|
62
|
+
},
|
|
63
|
+
onDrop: ({ source, location }) => {
|
|
64
|
+
log('onDrop', { source, location });
|
|
65
|
+
const target = location.current.dropTargets[0];
|
|
66
|
+
if (!target) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const targetLocation = target.data.location;
|
|
71
|
+
const piece = source.data.piece;
|
|
72
|
+
if (!isLocation(targetLocation) || !isPiece(piece)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const move: Move = { from: piece.location, to: targetLocation, piece: piece.type };
|
|
77
|
+
if (model.isValidMove(move)) {
|
|
78
|
+
if (model.canPromote?.(move)) {
|
|
79
|
+
setPromoting({ ...piece, location: targetLocation });
|
|
80
|
+
} else {
|
|
81
|
+
onDrop?.(move);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
setDragging(false);
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}, [model]);
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<GameboardContextProvider model={model} dragging={dragging} promoting={promoting} onPromotion={handlePromotion}>
|
|
92
|
+
{children}
|
|
93
|
+
</GameboardContextProvider>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
GameboardRoot.displayName = 'Gameboard.Root';
|
|
98
|
+
|
|
99
|
+
//
|
|
100
|
+
// Content
|
|
101
|
+
//
|
|
102
|
+
|
|
103
|
+
type GameboardContentProps = ThemedClassName<PropsWithChildren<{ grow?: boolean; contain?: boolean }>>;
|
|
104
|
+
|
|
105
|
+
const GameboardContent = forwardRef<HTMLDivElement, GameboardContentProps>(
|
|
106
|
+
({ children, classNames, grow, contain }, forwardedRef) => {
|
|
107
|
+
return (
|
|
108
|
+
<div
|
|
109
|
+
role='none'
|
|
110
|
+
className={mx(grow && 'grid w-full h-full dx-size-container place-content-center', classNames)}
|
|
111
|
+
ref={forwardedRef}
|
|
112
|
+
>
|
|
113
|
+
{contain ? <div className='w-[min(100cqw,100cqh)] h-[min(100cqw,100cqh)]'>{children}</div> : children}
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
GameboardContent.displayName = 'Gameboard.Content';
|
|
120
|
+
|
|
121
|
+
//
|
|
122
|
+
// Gameboard
|
|
123
|
+
//
|
|
124
|
+
|
|
125
|
+
export const Gameboard = {
|
|
126
|
+
Root: GameboardRoot,
|
|
127
|
+
Content: GameboardContent,
|
|
128
|
+
Piece,
|
|
129
|
+
Square,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export { useGameboardContext };
|
|
133
|
+
|
|
134
|
+
export type {
|
|
135
|
+
GameboardRootProps,
|
|
136
|
+
GameboardContentProps,
|
|
137
|
+
PieceProps as GameboardPieceProps,
|
|
138
|
+
SquareProps as GameboardSquareProps,
|
|
139
|
+
};
|
|
@@ -3,34 +3,33 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
|
6
|
-
// import { preserveOffsetOnSource } from '@atlaskit/pragmatic-drag-and-drop/element/preserve-offset-on-source';
|
|
7
6
|
import { centerUnderPointer } from '@atlaskit/pragmatic-drag-and-drop/element/center-under-pointer';
|
|
8
7
|
import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview';
|
|
9
|
-
import React, {
|
|
8
|
+
import React, { type FC, type SVGProps, memo, useEffect, useRef, useState } from 'react';
|
|
10
9
|
import { createPortal } from 'react-dom';
|
|
11
10
|
|
|
12
11
|
import { invariant } from '@dxos/invariant';
|
|
13
12
|
import { log } from '@dxos/log';
|
|
14
|
-
import {
|
|
15
|
-
import { mx } from '@dxos/
|
|
13
|
+
import { type ThemedClassName, useDynamicRef } from '@dxos/react-ui';
|
|
14
|
+
import { mx } from '@dxos/ui-theme';
|
|
16
15
|
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
16
|
+
import { useGameboardContext } from './Gameboard';
|
|
17
|
+
import { type Location, type PieceRecord, type Player, isEqualLocation, isLocation } from './types';
|
|
19
18
|
import { type DOMRectBounds } from './util';
|
|
20
19
|
|
|
21
20
|
export type PieceProps = ThemedClassName<{
|
|
21
|
+
Component: FC<SVGProps<SVGSVGElement>>;
|
|
22
22
|
piece: PieceRecord;
|
|
23
23
|
bounds: DOMRectBounds;
|
|
24
24
|
label?: string;
|
|
25
25
|
orientation?: Player;
|
|
26
|
-
|
|
26
|
+
onClick?: () => void;
|
|
27
27
|
}>;
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
useTrackProps({ classNames, piece, orientation, bounds, label, Component }, Piece.displayName, false);
|
|
31
|
-
const { model } = useBoardContext();
|
|
29
|
+
const PIECE_NAME = 'Piece';
|
|
32
30
|
|
|
33
|
-
|
|
31
|
+
export const Piece = memo(({ classNames, Component, piece, bounds, label, onClick }: PieceProps) => {
|
|
32
|
+
const { model, dragging: isDragging, promoting } = useGameboardContext(PIECE_NAME);
|
|
34
33
|
const promotingRef = useDynamicRef(promoting);
|
|
35
34
|
const [dragging, setDragging] = useState(false);
|
|
36
35
|
const [preview, setPreview] = useState<HTMLElement>();
|
|
@@ -40,20 +39,20 @@ export const Piece = memo(({ classNames, piece, orientation, bounds, label, Comp
|
|
|
40
39
|
|
|
41
40
|
const ref = useRef<HTMLDivElement>(null);
|
|
42
41
|
useEffect(() => {
|
|
42
|
+
if (!model) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
43
46
|
const el = ref.current;
|
|
44
47
|
invariant(el);
|
|
45
48
|
|
|
46
49
|
return draggable({
|
|
47
50
|
element: el,
|
|
48
51
|
getInitialData: () => ({ piece }),
|
|
49
|
-
onGenerateDragPreview: ({ nativeSetDragImage,
|
|
52
|
+
onGenerateDragPreview: ({ nativeSetDragImage, source }) => {
|
|
50
53
|
log('onGenerateDragPreview', { source: source.data });
|
|
51
54
|
setCustomNativeDragPreview({
|
|
52
55
|
getOffset: centerUnderPointer,
|
|
53
|
-
// getOffset: preserveOffsetOnSource({
|
|
54
|
-
// element: source.element,
|
|
55
|
-
// input: location.current.input,
|
|
56
|
-
// }),
|
|
57
56
|
render: ({ container }) => {
|
|
58
57
|
setPreview(container);
|
|
59
58
|
const { width, height } = el.getBoundingClientRect();
|
|
@@ -66,12 +65,17 @@ export const Piece = memo(({ classNames, piece, orientation, bounds, label, Comp
|
|
|
66
65
|
nativeSetDragImage,
|
|
67
66
|
});
|
|
68
67
|
},
|
|
69
|
-
canDrag: () => !promotingRef.current && model
|
|
68
|
+
canDrag: () => !promotingRef.current && !model.readonly && model.turn === piece.side,
|
|
70
69
|
onDragStart: () => setDragging(true),
|
|
71
70
|
onDrop: ({ location: { current } }) => {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
// TODO(burdon): Create wrapper function to catch errors.
|
|
72
|
+
try {
|
|
73
|
+
const location = current.dropTargets[0]?.data.location;
|
|
74
|
+
if (isLocation(location)) {
|
|
75
|
+
setCurrent((current) => ({ ...current, location }));
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
// Ignore.
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
setDragging(false);
|
|
@@ -88,7 +92,7 @@ export const Piece = memo(({ classNames, piece, orientation, bounds, label, Comp
|
|
|
88
92
|
|
|
89
93
|
// Check if piece moved.
|
|
90
94
|
if (!current.location || !isEqualLocation(current.location, piece.location)) {
|
|
91
|
-
ref.current.style.transition = 'top
|
|
95
|
+
ref.current.style.transition = 'top 250ms ease-out, left 250ms ease-out';
|
|
92
96
|
ref.current.style.top = bounds.top + 'px';
|
|
93
97
|
ref.current.style.left = bounds.left + 'px';
|
|
94
98
|
setCurrent({ location: piece.location, bounds });
|
|
@@ -112,10 +116,10 @@ export const Piece = memo(({ classNames, piece, orientation, bounds, label, Comp
|
|
|
112
116
|
className={mx(
|
|
113
117
|
'absolute',
|
|
114
118
|
classNames,
|
|
115
|
-
// orientation === 'black' && '_rotate-180',
|
|
116
119
|
dragging && 'opacity-20', // Must not unmount component while dragging.
|
|
117
120
|
isDragging && 'pointer-events-none', // Don't block the square's drop target.
|
|
118
121
|
)}
|
|
122
|
+
onClick={onClick}
|
|
119
123
|
>
|
|
120
124
|
<Component className='grow' />
|
|
121
125
|
{label && <div className='absolute inset-1 text-xs text-black'>{label}</div>}
|
|
@@ -132,4 +136,4 @@ export const Piece = memo(({ classNames, piece, orientation, bounds, label, Comp
|
|
|
132
136
|
);
|
|
133
137
|
});
|
|
134
138
|
|
|
135
|
-
Piece.displayName =
|
|
139
|
+
Piece.displayName = PIECE_NAME;
|
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
|
6
|
-
import React, {
|
|
6
|
+
import React, { memo, useEffect, useRef, useState } from 'react';
|
|
7
7
|
|
|
8
8
|
import { invariant } from '@dxos/invariant';
|
|
9
9
|
import { log } from '@dxos/log';
|
|
10
10
|
import { type ThemedClassName } from '@dxos/react-ui';
|
|
11
|
-
import { mx } from '@dxos/
|
|
11
|
+
import { mx } from '@dxos/ui-theme';
|
|
12
12
|
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
13
|
+
import { useGameboardContext } from './Gameboard';
|
|
14
|
+
import { type Location, isPiece } from './types';
|
|
15
15
|
import { type DOMRectBounds } from './util';
|
|
16
16
|
|
|
17
17
|
type HoveredState = 'idle' | 'validMove' | 'invalidMove';
|
|
@@ -22,10 +22,12 @@ export type SquareProps = ThemedClassName<{
|
|
|
22
22
|
label?: string;
|
|
23
23
|
}>;
|
|
24
24
|
|
|
25
|
+
const SQUARE_NAME = 'Square';
|
|
26
|
+
|
|
25
27
|
export const Square = memo(({ location, bounds, label, classNames }: SquareProps) => {
|
|
26
28
|
const ref = useRef<HTMLDivElement>(null);
|
|
27
29
|
const [state, setState] = useState<HoveredState>('idle');
|
|
28
|
-
const { model } =
|
|
30
|
+
const { model } = useGameboardContext(SQUARE_NAME);
|
|
29
31
|
|
|
30
32
|
useEffect(() => {
|
|
31
33
|
const el = ref.current;
|
|
@@ -71,4 +73,4 @@ export const Square = memo(({ location, bounds, label, classNames }: SquareProps
|
|
|
71
73
|
);
|
|
72
74
|
});
|
|
73
75
|
|
|
74
|
-
Square.displayName =
|
|
76
|
+
Square.displayName = SQUARE_NAME;
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { type
|
|
5
|
+
import { type Atom } from '@effect-atom/atom-react';
|
|
6
6
|
|
|
7
|
+
// TODO(burdon): Don't make this assumption.
|
|
7
8
|
export type Player = 'black' | 'white';
|
|
8
9
|
|
|
9
10
|
export type Location = [number, number];
|
|
@@ -51,8 +52,9 @@ export const isEqualLocation = (l1: Location, l2: Location): boolean => l1[0] ==
|
|
|
51
52
|
* Generic board model.
|
|
52
53
|
*/
|
|
53
54
|
export interface GameboardModel<T extends PieceType = PieceType> {
|
|
55
|
+
readonly: boolean;
|
|
54
56
|
turn: Player;
|
|
55
|
-
pieces:
|
|
57
|
+
pieces: Atom.Atom<PieceMap<T>>;
|
|
56
58
|
isValidMove: (move: Move) => boolean;
|
|
57
59
|
canPromote?: (move: Move) => boolean;
|
|
58
60
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import * as React from
|
|
2
|
-
import type { SVGProps } from
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { SVGProps } from "react";
|
|
3
3
|
const SvgBB = (props: SVGProps<SVGSVGElement>) => (
|
|
4
|
-
<svg xmlns=
|
|
5
|
-
<path fill='#f9f9f9' d='M732 1290 628 993l386-417 421 375-132 380 49 152-658-9z' />
|
|
4
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048" {...props}>
|
|
6
5
|
<path
|
|
7
|
-
fill=
|
|
8
|
-
d=
|
|
6
|
+
fill="#f9f9f9"
|
|
7
|
+
d="M732 1290 628 993l386-417 421 375-132 380 49 152-658-9z"
|
|
8
|
+
/>
|
|
9
|
+
<path
|
|
10
|
+
fill="#101010"
|
|
11
|
+
d="M768 1365q-5 39-26 82h564q-18-36-26-82zm495-73 46-73q-142-49-285-47-144-2-285 47l46 73q118-40 239-38 120-2 239 38m-432 227H624q67-116 72-229-114-119-162-223t-6-224q33-96 118-189t312-247q-17-11-46-36t-29-79q0-58 41-96t100-38q58 0 100 38t41 96q0 54-29 79t-46 36q226 153 311 247t119 189q42 119-6 224t-162 223q4 113 72 229h-207l10 16q33 53 70 60t89 7h250q76 0 142 62t65 179h-495q-123 0-223-84t-101-199q0 114-101 199t-223 84H205q0-117 65-179t142-62h250q51 0 88-7t71-60zm146-701h-95v89h95v165h94V907h95v-89h-95V714h-94z"
|
|
9
12
|
/>
|
|
10
13
|
</svg>
|
|
11
14
|
);
|