@dxos/react-ui-gameboard 0.7.5-main.ff8607b → 0.7.5-staging.2ff1350
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/dist/lib/browser/index.mjs +147 -44
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +147 -43
- package/dist/lib/node/index.cjs.map +3 -3
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +147 -44
- package/dist/lib/node-esm/index.mjs.map +3 -3
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/Board/Board.d.ts +2 -2
- package/dist/types/src/Board/Board.d.ts.map +1 -1
- package/dist/types/src/Board/Piece.d.ts +1 -1
- package/dist/types/src/Board/Piece.d.ts.map +1 -1
- package/dist/types/src/Board/Square.d.ts +1 -1
- package/dist/types/src/Board/Square.d.ts.map +1 -1
- package/dist/types/src/Board/context.d.ts +7 -7
- package/dist/types/src/Board/context.d.ts.map +1 -1
- package/dist/types/src/Board/index.d.ts +1 -0
- package/dist/types/src/Board/index.d.ts.map +1 -1
- package/dist/types/src/Board/types.d.ts +6 -2
- package/dist/types/src/Board/types.d.ts.map +1 -1
- package/dist/types/src/Chessboard/Chessboard.d.ts +1 -1
- package/dist/types/src/Chessboard/Chessboard.d.ts.map +1 -1
- package/dist/types/src/Chessboard/Chessboard.stories.d.ts +3 -2
- package/dist/types/src/Chessboard/Chessboard.stories.d.ts.map +1 -1
- package/dist/types/src/Chessboard/chess.d.ts +5 -9
- package/dist/types/src/Chessboard/chess.d.ts.map +1 -1
- package/dist/types/src/gen/pieces/chess/alpha/bB.d.ts +1 -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 +1 -2
- package/dist/types/src/gen/pieces/chess/alpha/bK.d.ts.map +1 -1
- package/dist/types/src/gen/pieces/chess/alpha/bN.d.ts +1 -2
- package/dist/types/src/gen/pieces/chess/alpha/bN.d.ts.map +1 -1
- package/dist/types/src/gen/pieces/chess/alpha/bP.d.ts +1 -2
- package/dist/types/src/gen/pieces/chess/alpha/bP.d.ts.map +1 -1
- package/dist/types/src/gen/pieces/chess/alpha/bQ.d.ts +1 -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 +1 -2
- package/dist/types/src/gen/pieces/chess/alpha/bR.d.ts.map +1 -1
- package/dist/types/src/gen/pieces/chess/alpha/wB.d.ts +1 -2
- package/dist/types/src/gen/pieces/chess/alpha/wB.d.ts.map +1 -1
- package/dist/types/src/gen/pieces/chess/alpha/wK.d.ts +1 -2
- package/dist/types/src/gen/pieces/chess/alpha/wK.d.ts.map +1 -1
- package/dist/types/src/gen/pieces/chess/alpha/wN.d.ts +1 -2
- package/dist/types/src/gen/pieces/chess/alpha/wN.d.ts.map +1 -1
- package/dist/types/src/gen/pieces/chess/alpha/wP.d.ts +1 -2
- package/dist/types/src/gen/pieces/chess/alpha/wP.d.ts.map +1 -1
- package/dist/types/src/gen/pieces/chess/alpha/wQ.d.ts +1 -2
- package/dist/types/src/gen/pieces/chess/alpha/wQ.d.ts.map +1 -1
- package/dist/types/src/gen/pieces/chess/alpha/wR.d.ts +1 -2
- package/dist/types/src/gen/pieces/chess/alpha/wR.d.ts.map +1 -1
- package/dist/types/src/gen/pieces/chess/cburnett/bB.d.ts +1 -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 +1 -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 +1 -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 +1 -2
- package/dist/types/src/gen/pieces/chess/cburnett/bP.d.ts.map +1 -1
- package/dist/types/src/gen/pieces/chess/cburnett/bQ.d.ts +1 -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 +1 -2
- package/dist/types/src/gen/pieces/chess/cburnett/bR.d.ts.map +1 -1
- package/dist/types/src/gen/pieces/chess/cburnett/wB.d.ts +1 -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 +1 -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 +1 -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 +1 -2
- package/dist/types/src/gen/pieces/chess/cburnett/wP.d.ts.map +1 -1
- package/dist/types/src/gen/pieces/chess/cburnett/wQ.d.ts +1 -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 +1 -2
- package/dist/types/src/gen/pieces/chess/cburnett/wR.d.ts.map +1 -1
- package/package.json +12 -11
- package/src/Board/Board.tsx +24 -6
- package/src/Board/Piece.tsx +10 -9
- package/src/Board/Square.tsx +1 -1
- package/src/Board/context.ts +9 -2
- package/src/Board/index.ts +1 -0
- package/src/Board/types.ts +6 -2
- package/src/Chessboard/Chessboard.stories.tsx +45 -3
- package/src/Chessboard/Chessboard.tsx +76 -9
- package/src/Chessboard/chess.ts +31 -10
package/src/Board/types.ts
CHANGED
|
@@ -23,8 +23,8 @@ export type PieceRecord<T extends PieceType = PieceType> = {
|
|
|
23
23
|
export type PieceMap<T extends PieceType = PieceType> = Record<string, PieceRecord<T>>;
|
|
24
24
|
|
|
25
25
|
export type Move = {
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
from: Location;
|
|
27
|
+
to: Location;
|
|
28
28
|
piece: PieceType;
|
|
29
29
|
promotion?: PieceType;
|
|
30
30
|
};
|
|
@@ -47,8 +47,12 @@ export const isLocation = (token: unknown): token is Location =>
|
|
|
47
47
|
|
|
48
48
|
export const isEqualLocation = (l1: Location, l2: Location): boolean => l1[0] === l2[0] && l1[1] === l2[1];
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Generic board model.
|
|
52
|
+
*/
|
|
50
53
|
export interface BoardModel<T extends PieceType = PieceType> {
|
|
51
54
|
turn: Player;
|
|
52
55
|
pieces: ReadonlySignal<PieceMap<T>>;
|
|
53
56
|
isValidMove: (move: Move) => boolean;
|
|
57
|
+
canPromote?: (move: Move) => boolean;
|
|
54
58
|
}
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
import '@dxos-theme';
|
|
6
6
|
|
|
7
7
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
8
|
-
import React, { useCallback, useMemo, useState } from 'react';
|
|
8
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
9
9
|
|
|
10
|
+
import { log } from '@dxos/log';
|
|
10
11
|
import { Button, Toolbar } from '@dxos/react-ui';
|
|
11
12
|
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
|
12
13
|
|
|
@@ -22,7 +23,13 @@ const Render = ({ fen, orientation: _orientation, ...props }: RenderProps) => {
|
|
|
22
23
|
const model = useMemo(() => new ChessModel(fen), [fen]);
|
|
23
24
|
const [orientation, setOrientation] = useState<Player | undefined>(_orientation);
|
|
24
25
|
|
|
25
|
-
const handleDrop = useCallback<NonNullable<BoardRootProps['onDrop']>>(
|
|
26
|
+
const handleDrop = useCallback<NonNullable<BoardRootProps['onDrop']>>(
|
|
27
|
+
(move: Move) => {
|
|
28
|
+
log.info('handleDrop', { move });
|
|
29
|
+
return model.makeMove(move);
|
|
30
|
+
},
|
|
31
|
+
[model],
|
|
32
|
+
);
|
|
26
33
|
|
|
27
34
|
return (
|
|
28
35
|
<div className='flex flex-col grow gap-2 overflow-hidden'>
|
|
@@ -43,11 +50,36 @@ const Render = ({ fen, orientation: _orientation, ...props }: RenderProps) => {
|
|
|
43
50
|
);
|
|
44
51
|
};
|
|
45
52
|
|
|
53
|
+
const Grid = (props: RenderProps) => {
|
|
54
|
+
const models = useMemo(() => Array.from({ length: 9 }).map(() => new ChessModel()), []);
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
const i = setInterval(() => {
|
|
57
|
+
const model = models[Math.floor(Math.random() * models.length)];
|
|
58
|
+
model.makeRandomMove();
|
|
59
|
+
}, 100);
|
|
60
|
+
return () => clearInterval(i);
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className='h-full aspect-square mx-auto'>
|
|
65
|
+
<div className='grid grid-cols-3 gap-2'>
|
|
66
|
+
{models.map((model, i) => (
|
|
67
|
+
<div key={i} className='aspect-square'>
|
|
68
|
+
<Board.Root model={model}>
|
|
69
|
+
<Chessboard />
|
|
70
|
+
</Board.Root>
|
|
71
|
+
</div>
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
46
78
|
const meta: Meta<typeof Render> = {
|
|
47
79
|
title: 'ui/react-ui-gameboard/Chessboard',
|
|
48
80
|
component: Chessboard,
|
|
49
81
|
render: Render,
|
|
50
|
-
decorators: [withTheme, withLayout({ fullscreen: true })],
|
|
82
|
+
decorators: [withTheme, withLayout({ fullscreen: true, classNames: '' })],
|
|
51
83
|
};
|
|
52
84
|
|
|
53
85
|
export default meta;
|
|
@@ -56,6 +88,12 @@ type Story = StoryObj<typeof Render>;
|
|
|
56
88
|
|
|
57
89
|
export const Default: Story = {};
|
|
58
90
|
|
|
91
|
+
export const Promotion: Story = {
|
|
92
|
+
args: {
|
|
93
|
+
fen: '4k3/7P/8/8/8/8/1p6/4K3 w - - 0 1',
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
59
97
|
export const Debug: Story = {
|
|
60
98
|
args: {
|
|
61
99
|
debug: true,
|
|
@@ -64,3 +102,7 @@ export const Debug: Story = {
|
|
|
64
102
|
fen: 'q3k1nr/1pp1nQpp/3p4/1P2p3/4P3/B1PP1b2/B5PP/5K2 b k - 0 17',
|
|
65
103
|
},
|
|
66
104
|
};
|
|
105
|
+
|
|
106
|
+
export const Nine: Story = {
|
|
107
|
+
render: Grid,
|
|
108
|
+
};
|
|
@@ -6,8 +6,10 @@ import React, { type PropsWithChildren, useRef, useMemo, useEffect, useState, me
|
|
|
6
6
|
import { useResizeDetector } from 'react-resize-detector';
|
|
7
7
|
|
|
8
8
|
import { useTrackProps } from '@dxos/react-ui';
|
|
9
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
10
|
+
import { isNotFalsy } from '@dxos/util';
|
|
9
11
|
|
|
10
|
-
import { type ChessPiece, ChessPieces, getSquareColor, locationToPos } from './chess';
|
|
12
|
+
import { boardStyles, type ChessPiece, ChessPieces, getSquareColor, locationToPos } from './chess';
|
|
11
13
|
import {
|
|
12
14
|
type DOMRectBounds,
|
|
13
15
|
type Location,
|
|
@@ -34,7 +36,7 @@ export type ChessboardProps = PropsWithChildren<{
|
|
|
34
36
|
export const Chessboard = memo(({ orientation, showLabels, debug, rows = 8, cols = 8 }: ChessboardProps) => {
|
|
35
37
|
useTrackProps({ orientation, showLabels, debug }, Chessboard.displayName, false);
|
|
36
38
|
const { ref: containerRef, width, height } = useResizeDetector({ refreshRate: 200 });
|
|
37
|
-
const { model } = useBoardContext();
|
|
39
|
+
const { model, promoting, onPromotion } = useBoardContext();
|
|
38
40
|
|
|
39
41
|
const locations = useMemo<Location[]>(() => {
|
|
40
42
|
return Array.from({ length: rows }, (_, i) => (orientation === 'black' ? i : rows - 1 - i)).flatMap((row) =>
|
|
@@ -73,16 +75,22 @@ export const Chessboard = memo(({ orientation, showLabels, debug, rows = 8, cols
|
|
|
73
75
|
}, [locations, width, height]);
|
|
74
76
|
|
|
75
77
|
// Get the bounds of each square and piece.
|
|
76
|
-
const positions = useMemo<{
|
|
78
|
+
const positions = useMemo<{ piece: PieceRecord; bounds: DOMRectBounds }[]>(() => {
|
|
77
79
|
if (!gridRef.current) {
|
|
78
80
|
return [];
|
|
79
81
|
}
|
|
80
82
|
|
|
81
|
-
return Object.values(model?.pieces.value ?? {})
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
return Object.values(model?.pieces.value ?? {})
|
|
84
|
+
.map((piece) => {
|
|
85
|
+
if (piece.id === promoting?.id) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const bounds = grid[locationToString(piece.location)];
|
|
90
|
+
return { piece, bounds };
|
|
91
|
+
})
|
|
92
|
+
.filter(isNotFalsy);
|
|
93
|
+
}, [grid, model?.pieces.value, promoting]);
|
|
86
94
|
|
|
87
95
|
return (
|
|
88
96
|
<div ref={containerRef} className='relative'>
|
|
@@ -100,7 +108,7 @@ export const Chessboard = memo(({ orientation, showLabels, debug, rows = 8, cols
|
|
|
100
108
|
/>
|
|
101
109
|
))}
|
|
102
110
|
</div>
|
|
103
|
-
<div className='
|
|
111
|
+
<div className={mx(promoting && 'opacity-50')}>
|
|
104
112
|
{positions.map(({ bounds, piece }) => (
|
|
105
113
|
<Piece
|
|
106
114
|
key={piece.id}
|
|
@@ -112,6 +120,22 @@ export const Chessboard = memo(({ orientation, showLabels, debug, rows = 8, cols
|
|
|
112
120
|
/>
|
|
113
121
|
))}
|
|
114
122
|
</div>
|
|
123
|
+
<div>
|
|
124
|
+
{promoting && (
|
|
125
|
+
<PromotionSelector
|
|
126
|
+
grid={grid}
|
|
127
|
+
piece={promoting}
|
|
128
|
+
onSelect={(piece) => {
|
|
129
|
+
onPromotion({
|
|
130
|
+
from: Object.values(model!.pieces.value).find((p) => p.id === promoting.id)!.location,
|
|
131
|
+
to: piece.location,
|
|
132
|
+
piece: promoting.type,
|
|
133
|
+
promotion: piece.type,
|
|
134
|
+
});
|
|
135
|
+
}}
|
|
136
|
+
/>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
115
139
|
</div>
|
|
116
140
|
);
|
|
117
141
|
});
|
|
@@ -121,3 +145,46 @@ Chessboard.displayName = 'Chessboard';
|
|
|
121
145
|
const getSquareLocation = (container: HTMLElement, location: Location): HTMLElement | null => {
|
|
122
146
|
return container.querySelector(`[data-location="${locationToString(location)}"]`);
|
|
123
147
|
};
|
|
148
|
+
|
|
149
|
+
const PromotionSelector = ({
|
|
150
|
+
grid,
|
|
151
|
+
piece,
|
|
152
|
+
onSelect,
|
|
153
|
+
}: {
|
|
154
|
+
grid: Record<string, DOMRectBounds>;
|
|
155
|
+
piece: PieceRecord;
|
|
156
|
+
onSelect: (piece: PieceRecord) => void;
|
|
157
|
+
}) => {
|
|
158
|
+
const positions = ['Q', 'N', 'R', 'B'].map((pieceType, i) => {
|
|
159
|
+
const location = [piece.location[0] + (piece.location[0] === 0 ? i : -i), piece.location[1]] as Location;
|
|
160
|
+
return {
|
|
161
|
+
piece: {
|
|
162
|
+
id: `promotion-${pieceType}`,
|
|
163
|
+
type: (piece.side === 'black' ? 'B' : 'W') + pieceType,
|
|
164
|
+
side: piece.side,
|
|
165
|
+
location,
|
|
166
|
+
},
|
|
167
|
+
bounds: grid[locationToString(location)],
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const handleSelect = (selected: PieceRecord) => {
|
|
172
|
+
onSelect({ ...piece, type: selected.type });
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// TODO(burdon): Circle.
|
|
176
|
+
return (
|
|
177
|
+
<div>
|
|
178
|
+
{positions.map(({ piece, bounds }) => (
|
|
179
|
+
<div key={piece.id} style={bounds} onClick={() => handleSelect(piece)}>
|
|
180
|
+
<Piece
|
|
181
|
+
piece={piece}
|
|
182
|
+
bounds={bounds}
|
|
183
|
+
Component={ChessPieces[piece.type as ChessPiece]}
|
|
184
|
+
classNames={mx('border-2 border-neutral-700 rounded-full', boardStyles.promotion)}
|
|
185
|
+
/>
|
|
186
|
+
</div>
|
|
187
|
+
))}
|
|
188
|
+
</div>
|
|
189
|
+
);
|
|
190
|
+
};
|
package/src/Chessboard/chess.ts
CHANGED
|
@@ -33,30 +33,45 @@ export const locationToPos = ([row, col]: Location): string => {
|
|
|
33
33
|
return String.fromCharCode(col + 'a'.charCodeAt(0)) + (row + 1);
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
const styles = {
|
|
37
37
|
neutral: {
|
|
38
|
-
white: 'bg-neutral-200',
|
|
39
38
|
black: 'bg-neutral-50',
|
|
39
|
+
white: 'bg-neutral-200',
|
|
40
|
+
promotion: 'bg-neutral-200 hover:bg-neutral-300 opacity-70 hover:opacity-100',
|
|
41
|
+
},
|
|
42
|
+
original: {
|
|
43
|
+
black: 'bg-[#6C95B9]',
|
|
44
|
+
white: 'bg-[#CCD3DB]',
|
|
45
|
+
promotion: 'duration-500 bg-[#CCD3DB] opacity-70 hover:opacity-100',
|
|
40
46
|
},
|
|
41
47
|
blue: {
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
black: 'bg-[#608BC1]',
|
|
49
|
+
white: 'bg-[#CBDCEB]',
|
|
50
|
+
promotion: 'duration-500 bg-[#CBDCEB] opacity-70 hover:opacity-100',
|
|
51
|
+
},
|
|
52
|
+
green: {
|
|
53
|
+
black: 'bg-[#8EB486]',
|
|
54
|
+
white: 'bg-[#FDF7F4]',
|
|
55
|
+
promotion: 'duration-500 bg-[#FDF7F4] opacity-70 hover:opacity-100',
|
|
44
56
|
},
|
|
45
57
|
};
|
|
46
58
|
|
|
59
|
+
export const boardStyles = styles.original;
|
|
60
|
+
|
|
47
61
|
export const getSquareColor = ([row, col]: Location) => {
|
|
48
|
-
return (col + row) % 2 === 0 ?
|
|
62
|
+
return (col + row) % 2 === 0 ? boardStyles.white : boardStyles.black;
|
|
49
63
|
};
|
|
50
64
|
|
|
51
65
|
/**
|
|
52
66
|
* Attempt move.
|
|
53
67
|
*/
|
|
54
|
-
const makeMove = (game: Chess,
|
|
55
|
-
const
|
|
56
|
-
const
|
|
68
|
+
const makeMove = (game: Chess, move: Move): Chess | null => {
|
|
69
|
+
const from = locationToPos(move.from);
|
|
70
|
+
const to = locationToPos(move.to);
|
|
57
71
|
try {
|
|
58
|
-
log('makeMove', {
|
|
59
|
-
|
|
72
|
+
log('makeMove', { move });
|
|
73
|
+
const promotion = move.promotion ? move.promotion[1].toLowerCase() : 'q';
|
|
74
|
+
game.move({ from, to, promotion }, { strict: false });
|
|
60
75
|
return game;
|
|
61
76
|
} catch (err) {
|
|
62
77
|
// Ignore.
|
|
@@ -101,6 +116,12 @@ export class ChessModel implements BoardModel<ChessPiece> {
|
|
|
101
116
|
return makeMove(new Chess(this._game.fen()), move) !== null;
|
|
102
117
|
}
|
|
103
118
|
|
|
119
|
+
canPromote(move: Move): boolean {
|
|
120
|
+
const isPawnMove = move.piece === 'BP' || move.piece === 'WP';
|
|
121
|
+
const isToLastRank = move.to[0] === 0 || move.to[0] === 7;
|
|
122
|
+
return isPawnMove && isToLastRank;
|
|
123
|
+
}
|
|
124
|
+
|
|
104
125
|
makeMove(move: Move): boolean {
|
|
105
126
|
const game = makeMove(this._game, move);
|
|
106
127
|
if (!game) {
|