@dxos/react-ui-gameboard 0.8.4-main.c1de068 → 0.8.4-main.fd6878d
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 +309 -274
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +309 -274
- 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.map +1 -0
- package/dist/types/src/components/Chessboard/Chessboard.stories.d.ts +28 -0
- package/dist/types/src/components/Chessboard/Chessboard.stories.d.ts.map +1 -0
- package/dist/types/src/{Chessboard → components/Chessboard}/chess.d.ts +19 -6
- package/dist/types/src/components/Chessboard/chess.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 +1 -0
- 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/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 +14 -12
- package/src/{Chessboard → components/Chessboard}/Chessboard.stories.tsx +26 -25
- package/src/{Chessboard → components/Chessboard}/Chessboard.tsx +32 -35
- package/src/{Chessboard → components/Chessboard}/chess.ts +86 -26
- package/src/components/Gameboard/Gameboard.tsx +139 -0
- package/src/{Gameboard → components/Gameboard}/Piece.tsx +19 -20
- package/src/{Gameboard → components/Gameboard}/Square.tsx +4 -4
- package/src/{Gameboard → components/Gameboard}/index.ts +0 -3
- package/src/{Gameboard → components/Gameboard}/types.ts +2 -0
- package/src/components/index.ts +6 -0
- package/src/index.ts +1 -2
- 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.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/Gameboard/Gameboard.tsx +0 -103
- package/src/Gameboard/context.ts +0 -22
- /package/dist/types/src/{Chessboard → components/Chessboard}/Chessboard.d.ts +0 -0
- /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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-gameboard",
|
|
3
|
-
"version": "0.8.4-main.
|
|
3
|
+
"version": "0.8.4-main.fd6878d",
|
|
4
4
|
"description": "Game board.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"type": "module",
|
|
10
10
|
"exports": {
|
|
11
11
|
".": {
|
|
12
|
+
"source": "./src/index.ts",
|
|
12
13
|
"types": "./dist/types/src/index.d.ts",
|
|
13
14
|
"browser": "./dist/lib/browser/index.mjs",
|
|
14
15
|
"node": "./dist/lib/node-esm/index.mjs"
|
|
@@ -27,13 +28,14 @@
|
|
|
27
28
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
|
|
28
29
|
"@preact-signals/safe-react": "^0.9.0",
|
|
29
30
|
"@preact/signals-core": "^1.9.0",
|
|
30
|
-
"
|
|
31
|
+
"@radix-ui/react-context": "1.1.1",
|
|
32
|
+
"chess.js": "^1.4.0",
|
|
31
33
|
"react-resize-detector": "^11.0.1",
|
|
32
|
-
"@dxos/
|
|
33
|
-
"@dxos/
|
|
34
|
-
"@dxos/log": "0.8.4-main.
|
|
35
|
-
"@dxos/node-std": "0.8.4-main.
|
|
36
|
-
"@dxos/util": "0.8.4-main.
|
|
34
|
+
"@dxos/invariant": "0.8.4-main.fd6878d",
|
|
35
|
+
"@dxos/debug": "0.8.4-main.fd6878d",
|
|
36
|
+
"@dxos/log": "0.8.4-main.fd6878d",
|
|
37
|
+
"@dxos/node-std": "0.8.4-main.fd6878d",
|
|
38
|
+
"@dxos/util": "0.8.4-main.fd6878d"
|
|
37
39
|
},
|
|
38
40
|
"devDependencies": {
|
|
39
41
|
"@svgr/cli": "^8.1.0",
|
|
@@ -44,15 +46,15 @@
|
|
|
44
46
|
"react": "~18.2.0",
|
|
45
47
|
"react-dom": "~18.2.0",
|
|
46
48
|
"vite": "5.4.7",
|
|
47
|
-
"@dxos/react-ui": "0.8.4-main.
|
|
48
|
-
"@dxos/react-ui-theme": "0.8.4-main.
|
|
49
|
-
"@dxos/storybook-utils": "0.8.4-main.
|
|
49
|
+
"@dxos/react-ui": "0.8.4-main.fd6878d",
|
|
50
|
+
"@dxos/react-ui-theme": "0.8.4-main.fd6878d",
|
|
51
|
+
"@dxos/storybook-utils": "0.8.4-main.fd6878d"
|
|
50
52
|
},
|
|
51
53
|
"peerDependencies": {
|
|
52
54
|
"react": "~18.2.0",
|
|
53
55
|
"react-dom": "~18.2.0",
|
|
54
|
-
"@dxos/react-ui": "0.8.4-main.
|
|
55
|
-
"@dxos/react-ui-theme": "0.8.4-main.
|
|
56
|
+
"@dxos/react-ui": "0.8.4-main.fd6878d",
|
|
57
|
+
"@dxos/react-ui-theme": "0.8.4-main.fd6878d"
|
|
56
58
|
},
|
|
57
59
|
"publishConfig": {
|
|
58
60
|
"access": "public"
|
|
@@ -7,26 +7,27 @@ import '@dxos-theme';
|
|
|
7
7
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
8
8
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
9
9
|
|
|
10
|
-
import { log } from '@dxos/log';
|
|
11
10
|
import { Button, Toolbar } from '@dxos/react-ui';
|
|
12
11
|
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
|
13
12
|
|
|
14
|
-
import {
|
|
13
|
+
import { Gameboard, type GameboardRootProps, type Move, type Player } from '../Gameboard';
|
|
14
|
+
|
|
15
15
|
import { ChessModel } from './chess';
|
|
16
|
-
import {
|
|
16
|
+
import { Chessboard, type ChessboardProps } from './Chessboard';
|
|
17
17
|
|
|
18
|
-
type
|
|
19
|
-
|
|
18
|
+
type DefaultStoryProps = Pick<ChessboardProps, 'orientation' | 'showLabels' | 'debug'> & {
|
|
19
|
+
pgn?: string;
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
const DefaultStory = ({
|
|
23
|
-
const model = useMemo(() => new ChessModel(
|
|
22
|
+
const DefaultStory = ({ orientation: _orientation, pgn, ...props }: DefaultStoryProps) => {
|
|
23
|
+
const model = useMemo(() => new ChessModel(pgn), [pgn]);
|
|
24
24
|
const [orientation, setOrientation] = useState<Player | undefined>(_orientation);
|
|
25
25
|
|
|
26
|
-
const handleDrop = useCallback<NonNullable<GameboardRootProps['onDrop']>>(
|
|
26
|
+
const handleDrop = useCallback<NonNullable<GameboardRootProps<ChessModel>['onDrop']>>(
|
|
27
27
|
(move: Move) => {
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
const result = model.makeMove(move);
|
|
29
|
+
console.log(model.pgn);
|
|
30
|
+
return result;
|
|
30
31
|
},
|
|
31
32
|
[model],
|
|
32
33
|
);
|
|
@@ -34,7 +35,7 @@ const DefaultStory = ({ fen, orientation: _orientation, ...props }: RenderProps)
|
|
|
34
35
|
return (
|
|
35
36
|
<div className='flex flex-col grow gap-2 overflow-hidden'>
|
|
36
37
|
<Toolbar.Root>
|
|
37
|
-
<Button onClick={() => model.
|
|
38
|
+
<Button onClick={() => model.update()}>Reset</Button>
|
|
38
39
|
<Button onClick={() => model.makeRandomMove()}>Move</Button>
|
|
39
40
|
<div className='grow'></div>
|
|
40
41
|
<Button
|
|
@@ -44,7 +45,7 @@ const DefaultStory = ({ fen, orientation: _orientation, ...props }: RenderProps)
|
|
|
44
45
|
</Button>
|
|
45
46
|
</Toolbar.Root>
|
|
46
47
|
<Gameboard.Root model={model} onDrop={handleDrop}>
|
|
47
|
-
<Gameboard.Content>
|
|
48
|
+
<Gameboard.Content grow contain>
|
|
48
49
|
<Chessboard orientation={orientation} {...props} />
|
|
49
50
|
</Gameboard.Content>
|
|
50
51
|
</Gameboard.Root>
|
|
@@ -52,7 +53,7 @@ const DefaultStory = ({ fen, orientation: _orientation, ...props }: RenderProps)
|
|
|
52
53
|
);
|
|
53
54
|
};
|
|
54
55
|
|
|
55
|
-
const
|
|
56
|
+
const GridStory = () => {
|
|
56
57
|
const models = useMemo(() => Array.from({ length: 9 }).map(() => new ChessModel()), []);
|
|
57
58
|
useEffect(() => {
|
|
58
59
|
const i = setInterval(() => {
|
|
@@ -88,23 +89,23 @@ export default meta;
|
|
|
88
89
|
|
|
89
90
|
type Story = StoryObj<typeof DefaultStory>;
|
|
90
91
|
|
|
91
|
-
export const Default
|
|
92
|
+
export const Default = {} satisfies Story;
|
|
92
93
|
|
|
93
|
-
export const Promotion
|
|
94
|
+
export const Promotion = {
|
|
94
95
|
args: {
|
|
95
|
-
|
|
96
|
+
pgn: '1. e4 e5 2. Nf3 Nc6 3. Bc4 Bc5 4. c3 Nf6 5. d4 exd4 6. cxd4 Bb4+ 7. Nc3 d5 8. exd5 Nxd5 9. O-O Be6 10. Qb3 Na5 11. Qa4+ c6 12. Bxd5 Bxc3 13. Bxe6 fxe6 14. d5 Qg5 15. dxe6 Kf8 16. e7+ Kg8 *',
|
|
96
97
|
},
|
|
97
|
-
};
|
|
98
|
+
} satisfies Story;
|
|
98
99
|
|
|
99
|
-
export const Debug
|
|
100
|
+
export const Debug = {
|
|
100
101
|
args: {
|
|
101
|
-
|
|
102
|
-
showLabels: true,
|
|
102
|
+
pgn: '1. e4 e5 2. Nf3 Nc6 3. Bc4 Bc5 4. c3 Nf6 5. d4 exd4 6. cxd4 Bb4+ 7. Nc3 d5 8. exd5 Nxd5 9. O-O Be6 10. Qb3 Na5 11. Qa4+ c6 12. Bxd5 Bxc3 13. Bxe6 fxe6 *',
|
|
103
103
|
orientation: 'black',
|
|
104
|
-
|
|
104
|
+
showLabels: true,
|
|
105
|
+
debug: true,
|
|
105
106
|
},
|
|
106
|
-
};
|
|
107
|
+
} satisfies Story;
|
|
107
108
|
|
|
108
|
-
export const
|
|
109
|
-
render:
|
|
110
|
-
};
|
|
109
|
+
export const Grid = {
|
|
110
|
+
render: GridStory,
|
|
111
|
+
} satisfies Story;
|
|
@@ -2,26 +2,26 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import React, { type PropsWithChildren,
|
|
5
|
+
import React, { Fragment, type PropsWithChildren, memo, useEffect, useMemo, useRef, useState } from 'react';
|
|
6
6
|
import { useResizeDetector } from 'react-resize-detector';
|
|
7
7
|
|
|
8
8
|
import { type ThemedClassName, useTrackProps } from '@dxos/react-ui';
|
|
9
9
|
import { mx } from '@dxos/react-ui-theme';
|
|
10
10
|
import { isNotFalsy } from '@dxos/util';
|
|
11
11
|
|
|
12
|
-
import { boardStyles, type ChessPiece, ChessPieces, getSquareColor, locationToPos } from './chess';
|
|
13
12
|
import {
|
|
14
13
|
type DOMRectBounds,
|
|
14
|
+
Gameboard,
|
|
15
15
|
type Location,
|
|
16
16
|
type PieceRecord,
|
|
17
17
|
type Player,
|
|
18
|
-
Piece,
|
|
19
|
-
Square,
|
|
20
18
|
getRelativeBounds,
|
|
21
19
|
locationToString,
|
|
22
|
-
|
|
20
|
+
useGameboardContext,
|
|
23
21
|
} from '../Gameboard';
|
|
24
22
|
|
|
23
|
+
import { type ChessModel, type ChessPiece, ChessPieces, boardStyles, getSquareColor, locationToPos } from './chess';
|
|
24
|
+
|
|
25
25
|
export type ChessboardProps = ThemedClassName<
|
|
26
26
|
PropsWithChildren<{
|
|
27
27
|
orientation?: Player;
|
|
@@ -39,7 +39,7 @@ export const Chessboard = memo(
|
|
|
39
39
|
({ orientation, showLabels, debug, rows = 8, cols = 8, classNames }: ChessboardProps) => {
|
|
40
40
|
useTrackProps({ orientation, showLabels, debug }, Chessboard.displayName, false);
|
|
41
41
|
const { ref: containerRef, width, height } = useResizeDetector({ refreshRate: 200 });
|
|
42
|
-
const { model, promoting, onPromotion } =
|
|
42
|
+
const { model, promoting, onPromotion } = useGameboardContext<ChessModel>(Chessboard.displayName!);
|
|
43
43
|
|
|
44
44
|
const locations = useMemo<Location[]>(() => {
|
|
45
45
|
return Array.from({ length: rows }, (_, i) => (orientation === 'black' ? i : rows - 1 - i)).flatMap((row) =>
|
|
@@ -102,7 +102,7 @@ export const Chessboard = memo(
|
|
|
102
102
|
</div>
|
|
103
103
|
<div>
|
|
104
104
|
{locations.map((location) => (
|
|
105
|
-
<Square
|
|
105
|
+
<Gameboard.Square
|
|
106
106
|
key={locationToString(location)}
|
|
107
107
|
location={location}
|
|
108
108
|
label={showLabels ? locationToPos(location) : undefined}
|
|
@@ -113,7 +113,7 @@ export const Chessboard = memo(
|
|
|
113
113
|
</div>
|
|
114
114
|
<div className={mx(promoting && 'opacity-50')}>
|
|
115
115
|
{positions.map(({ bounds, piece }) => (
|
|
116
|
-
<Piece
|
|
116
|
+
<Gameboard.Piece
|
|
117
117
|
key={piece.id}
|
|
118
118
|
piece={piece}
|
|
119
119
|
bounds={bounds}
|
|
@@ -123,22 +123,20 @@ export const Chessboard = memo(
|
|
|
123
123
|
/>
|
|
124
124
|
))}
|
|
125
125
|
</div>
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
)}
|
|
141
|
-
</div>
|
|
126
|
+
{promoting && (
|
|
127
|
+
<PromotionSelector
|
|
128
|
+
grid={grid}
|
|
129
|
+
piece={promoting}
|
|
130
|
+
onSelect={(piece) => {
|
|
131
|
+
onPromotion({
|
|
132
|
+
from: Object.values(model!.pieces.value).find((p) => p.id === promoting.id)!.location,
|
|
133
|
+
to: piece.location,
|
|
134
|
+
piece: promoting.type,
|
|
135
|
+
promotion: piece.type,
|
|
136
|
+
});
|
|
137
|
+
}}
|
|
138
|
+
/>
|
|
139
|
+
)}
|
|
142
140
|
</div>
|
|
143
141
|
);
|
|
144
142
|
},
|
|
@@ -176,19 +174,18 @@ const PromotionSelector = ({
|
|
|
176
174
|
onSelect({ ...piece, type: selected.type });
|
|
177
175
|
};
|
|
178
176
|
|
|
179
|
-
// TODO(burdon): Circle.
|
|
180
177
|
return (
|
|
181
|
-
|
|
178
|
+
<>
|
|
182
179
|
{positions.map(({ piece, bounds }) => (
|
|
183
|
-
<
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
180
|
+
<Gameboard.Piece
|
|
181
|
+
key={piece.id}
|
|
182
|
+
piece={piece}
|
|
183
|
+
bounds={bounds}
|
|
184
|
+
Component={ChessPieces[piece.type as ChessPiece]}
|
|
185
|
+
classNames={mx('border-2 border-neutral-700 rounded-full', boardStyles.promotion)}
|
|
186
|
+
onClick={() => handleSelect(piece)}
|
|
187
|
+
/>
|
|
191
188
|
))}
|
|
192
|
-
|
|
189
|
+
</>
|
|
193
190
|
);
|
|
194
191
|
};
|
|
@@ -3,21 +3,21 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { type ReadonlySignal, signal } from '@preact/signals-core';
|
|
6
|
-
import { Chess
|
|
6
|
+
import { Chess as ChessJS } from 'chess.js';
|
|
7
7
|
import { type FC, type SVGProps } from 'react';
|
|
8
8
|
|
|
9
9
|
import { log } from '@dxos/log';
|
|
10
10
|
|
|
11
|
+
import * as Alpha from '../../gen/pieces/chess/alpha';
|
|
11
12
|
import {
|
|
12
|
-
type
|
|
13
|
+
type GameboardModel,
|
|
13
14
|
type Location,
|
|
15
|
+
type Move,
|
|
14
16
|
type PieceMap,
|
|
15
|
-
locationToString,
|
|
16
17
|
type PieceType,
|
|
17
|
-
type GameboardModel,
|
|
18
18
|
type Player,
|
|
19
|
+
locationToString,
|
|
19
20
|
} from '../Gameboard';
|
|
20
|
-
import * as Alpha from '../gen/pieces/chess/alpha';
|
|
21
21
|
|
|
22
22
|
export type ChessPiece = 'BK' | 'BQ' | 'BR' | 'BB' | 'BN' | 'BP' | 'WK' | 'WQ' | 'WR' | 'WB' | 'WN' | 'WP';
|
|
23
23
|
|
|
@@ -62,18 +62,30 @@ export const getSquareColor = ([row, col]: Location) => {
|
|
|
62
62
|
return (col + row) % 2 === 0 ? boardStyles.black : boardStyles.white;
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
+
export const createChess = (pgn?: string) => {
|
|
66
|
+
const chess = new ChessJS();
|
|
67
|
+
if (pgn) {
|
|
68
|
+
try {
|
|
69
|
+
chess.loadPgn(pgn);
|
|
70
|
+
} catch {
|
|
71
|
+
log.warn(pgn);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return chess;
|
|
76
|
+
};
|
|
77
|
+
|
|
65
78
|
/**
|
|
66
79
|
* Attempt move.
|
|
67
80
|
*/
|
|
68
|
-
const
|
|
81
|
+
const tryMove = (chess: ChessJS, move: Move): ChessJS | null => {
|
|
69
82
|
const from = locationToPos(move.from);
|
|
70
83
|
const to = locationToPos(move.to);
|
|
71
84
|
try {
|
|
72
|
-
log('makeMove', { move });
|
|
73
85
|
const promotion = move.promotion ? move.promotion[1].toLowerCase() : 'q';
|
|
74
|
-
|
|
75
|
-
return
|
|
76
|
-
} catch
|
|
86
|
+
chess.move({ from, to, promotion }, { strict: false });
|
|
87
|
+
return chess;
|
|
88
|
+
} catch {
|
|
77
89
|
// Ignore.
|
|
78
90
|
return null;
|
|
79
91
|
}
|
|
@@ -83,37 +95,70 @@ const makeMove = (game: Chess, move: Move): Chess | null => {
|
|
|
83
95
|
* Chess model.
|
|
84
96
|
*/
|
|
85
97
|
export class ChessModel implements GameboardModel<ChessPiece> {
|
|
86
|
-
private
|
|
98
|
+
private readonly _chess = new ChessJS();
|
|
87
99
|
private readonly _pieces = signal<PieceMap<ChessPiece>>({});
|
|
88
100
|
|
|
89
|
-
constructor(
|
|
90
|
-
this.
|
|
101
|
+
constructor(pgn?: string) {
|
|
102
|
+
this.update(pgn);
|
|
91
103
|
}
|
|
92
104
|
|
|
93
105
|
get turn(): Player {
|
|
94
|
-
return this.
|
|
106
|
+
return this._chess.turn() === 'w' ? 'white' : 'black';
|
|
95
107
|
}
|
|
96
108
|
|
|
97
109
|
get pieces(): ReadonlySignal<PieceMap<ChessPiece>> {
|
|
98
110
|
return this._pieces;
|
|
99
111
|
}
|
|
100
112
|
|
|
101
|
-
get game():
|
|
102
|
-
return this.
|
|
113
|
+
get game(): ChessJS {
|
|
114
|
+
return this._chess;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* PGN with headers.
|
|
119
|
+
*
|
|
120
|
+
* [Event "?"]
|
|
121
|
+
* [Site "?"]
|
|
122
|
+
* [Date "2025.08.05"]
|
|
123
|
+
* [Round "?"]
|
|
124
|
+
* [White "?"]
|
|
125
|
+
* [Black "?"]
|
|
126
|
+
* [Result "*"]
|
|
127
|
+
*/
|
|
128
|
+
// TODO(burdon): Update headers.
|
|
129
|
+
get pgn(): string {
|
|
130
|
+
return this._chess.pgn();
|
|
103
131
|
}
|
|
104
132
|
|
|
105
133
|
get fen(): string {
|
|
106
|
-
return this.
|
|
134
|
+
return this._chess.fen();
|
|
107
135
|
}
|
|
108
136
|
|
|
109
|
-
|
|
110
|
-
this.
|
|
111
|
-
|
|
137
|
+
update(pgn = ''): void {
|
|
138
|
+
const previous = this._chess.history();
|
|
139
|
+
try {
|
|
140
|
+
this._chess.loadPgn(pgn);
|
|
141
|
+
// TODO(burdon): Get from TS.
|
|
142
|
+
// TODO(burdon): Update if not set.
|
|
143
|
+
this._chess.setHeader('Date', createDate());
|
|
144
|
+
this._chess.setHeader('Site', 'dxos.org');
|
|
145
|
+
// TODO(burdon): Update player keys.
|
|
146
|
+
// this._chess.setHeader('White', 'White');
|
|
147
|
+
// this._chess.setHeader('Black', 'Black');
|
|
148
|
+
} catch {
|
|
149
|
+
// Ignore.
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const current = this._chess.history();
|
|
153
|
+
if (!isValidNextMove(previous, current)) {
|
|
154
|
+
this._pieces.value = {};
|
|
155
|
+
}
|
|
156
|
+
|
|
112
157
|
this._update();
|
|
113
158
|
}
|
|
114
159
|
|
|
115
160
|
isValidMove(move: Move): boolean {
|
|
116
|
-
return
|
|
161
|
+
return tryMove(new ChessJS(this._chess.fen()), move) !== null;
|
|
117
162
|
}
|
|
118
163
|
|
|
119
164
|
canPromote(move: Move): boolean {
|
|
@@ -123,24 +168,23 @@ export class ChessModel implements GameboardModel<ChessPiece> {
|
|
|
123
168
|
}
|
|
124
169
|
|
|
125
170
|
makeMove(move: Move): boolean {
|
|
126
|
-
const game =
|
|
171
|
+
const game = tryMove(this._chess, move);
|
|
127
172
|
if (!game) {
|
|
128
173
|
return false;
|
|
129
174
|
}
|
|
130
175
|
|
|
131
|
-
this._game = game;
|
|
132
176
|
this._update();
|
|
133
177
|
return true;
|
|
134
178
|
}
|
|
135
179
|
|
|
136
180
|
makeRandomMove(): boolean {
|
|
137
|
-
const moves = this.
|
|
181
|
+
const moves = this._chess.moves();
|
|
138
182
|
if (!moves.length) {
|
|
139
183
|
return false;
|
|
140
184
|
}
|
|
141
185
|
|
|
142
186
|
const move = moves[Math.floor(Math.random() * moves.length)];
|
|
143
|
-
this.
|
|
187
|
+
this._chess.move(move);
|
|
144
188
|
this._update();
|
|
145
189
|
return true;
|
|
146
190
|
}
|
|
@@ -150,7 +194,7 @@ export class ChessModel implements GameboardModel<ChessPiece> {
|
|
|
150
194
|
*/
|
|
151
195
|
private _update(): void {
|
|
152
196
|
const pieces: PieceMap<ChessPiece> = {};
|
|
153
|
-
this.
|
|
197
|
+
this._chess.board().flatMap((row) =>
|
|
154
198
|
row.forEach((record) => {
|
|
155
199
|
if (!record) {
|
|
156
200
|
return;
|
|
@@ -172,6 +216,20 @@ export class ChessModel implements GameboardModel<ChessPiece> {
|
|
|
172
216
|
}
|
|
173
217
|
}
|
|
174
218
|
|
|
219
|
+
const isValidNextMove = (previous: string[], current: string[]) => {
|
|
220
|
+
if (current.length > previous.length + 1) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
for (let i = 0; i < previous.length; i++) {
|
|
225
|
+
if (previous[i] !== current[i]) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return true;
|
|
231
|
+
};
|
|
232
|
+
|
|
175
233
|
/**
|
|
176
234
|
* Preserve the original piece objects (and IDs).
|
|
177
235
|
*/
|
|
@@ -211,3 +269,5 @@ export const mapPieces = <T extends PieceType>(before: PieceMap<T>, after: Piece
|
|
|
211
269
|
|
|
212
270
|
return after;
|
|
213
271
|
};
|
|
272
|
+
|
|
273
|
+
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/react-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> = {
|
|
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>(consumerName: string): GameboardContextValue<M> => {
|
|
27
|
+
return useRadixGameboardContext(consumerName);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
//
|
|
31
|
+
// Root
|
|
32
|
+
//
|
|
33
|
+
|
|
34
|
+
type GameboardRootProps<M extends GameboardModel> = PropsWithChildren<{
|
|
35
|
+
model?: M;
|
|
36
|
+
onDrop?: (move: Move) => boolean;
|
|
37
|
+
}>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Generic board container.
|
|
41
|
+
*/
|
|
42
|
+
const GameboardRoot = <M extends GameboardModel>({ children, model, onDrop }: GameboardRootProps<M>) => {
|
|
43
|
+
const [dragging, setDragging] = useState(false);
|
|
44
|
+
const [promoting, setPromoting] = useState<PieceRecord | undefined>();
|
|
45
|
+
|
|
46
|
+
const handlePromotion = useCallback<GameboardContextValue<M>['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 is-full bs-full size-container place-content-center', classNames)}
|
|
111
|
+
ref={forwardedRef}
|
|
112
|
+
>
|
|
113
|
+
{contain ? <div className='is-[min(100cqw,100cqh)] bs-[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
|
+
};
|