@dxos/react-ui-gameboard 0.8.4-main.fd6878d → 0.8.4-staging.60fe92afc8
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/LICENSE +102 -5
- package/README.md +2 -0
- package/dist/lib/browser/index.mjs +588 -735
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +588 -735
- 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 +9 -4
- package/dist/types/src/components/Chessboard/Chessboard.d.ts.map +1 -1
- package/dist/types/src/components/Chessboard/Chessboard.stories.d.ts +21 -18
- package/dist/types/src/components/Chessboard/Chessboard.stories.d.ts.map +1 -1
- package/dist/types/src/components/Chessboard/chess.d.ts +15 -8
- package/dist/types/src/components/Chessboard/chess.d.ts.map +1 -1
- 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/Gameboard/Gameboard.d.ts +11 -7
- package/dist/types/src/components/Gameboard/Gameboard.d.ts.map +1 -1
- package/dist/types/src/components/Gameboard/Piece.d.ts +1 -1
- package/dist/types/src/components/Gameboard/Piece.d.ts.map +1 -1
- package/dist/types/src/components/Gameboard/Square.d.ts.map +1 -1
- package/dist/types/src/components/Gameboard/types.d.ts +3 -3
- package/dist/types/src/components/Gameboard/types.d.ts.map +1 -1
- package/dist/types/src/components/Gameboard/util.d.ts.map +1 -1
- 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.map +1 -1
- 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.map +1 -1
- 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.map +1 -1
- 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.map +1 -1
- 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.map +1 -1
- 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.map +1 -1
- 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.map +1 -1
- 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.map +1 -1
- 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.map +1 -1
- 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.map +1 -1
- 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.map +1 -1
- 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.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +27 -27
- package/src/components/Chessboard/Chessboard.stories.tsx +20 -18
- package/src/components/Chessboard/Chessboard.tsx +44 -30
- package/src/components/Chessboard/chess.test.ts +19 -0
- package/src/components/Chessboard/chess.ts +111 -59
- package/src/components/Gameboard/Gameboard.tsx +8 -9
- package/src/components/Gameboard/Piece.tsx +13 -8
- package/src/components/Gameboard/Square.tsx +5 -3
- package/src/components/Gameboard/types.ts +3 -3
package/package.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-gameboard",
|
|
3
|
-
"version": "0.8.4-
|
|
3
|
+
"version": "0.8.4-staging.60fe92afc8",
|
|
4
4
|
"description": "Game board.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
7
|
-
"
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/dxos/dxos"
|
|
10
|
+
},
|
|
11
|
+
"license": "FSL-1.1-Apache-2.0",
|
|
8
12
|
"author": "DXOS.org",
|
|
9
13
|
"type": "module",
|
|
10
14
|
"exports": {
|
|
@@ -16,45 +20,41 @@
|
|
|
16
20
|
}
|
|
17
21
|
},
|
|
18
22
|
"types": "dist/types/src/index.d.ts",
|
|
19
|
-
"typesVersions": {
|
|
20
|
-
"*": {}
|
|
21
|
-
},
|
|
22
23
|
"files": [
|
|
23
24
|
"dist",
|
|
24
25
|
"src"
|
|
25
26
|
],
|
|
26
27
|
"dependencies": {
|
|
27
|
-
"@atlaskit/pragmatic-drag-and-drop": "
|
|
28
|
-
"@atlaskit/pragmatic-drag-and-drop-hitbox": "
|
|
29
|
-
"@
|
|
30
|
-
"@preact/signals-core": "^1.9.0",
|
|
28
|
+
"@atlaskit/pragmatic-drag-and-drop": "1.7.7",
|
|
29
|
+
"@atlaskit/pragmatic-drag-and-drop-hitbox": "1.1.0",
|
|
30
|
+
"@effect-atom/atom-react": "^0.5.0",
|
|
31
31
|
"@radix-ui/react-context": "1.1.1",
|
|
32
|
-
"chess.js": "^1.
|
|
32
|
+
"chess.js": "^1.0.0",
|
|
33
33
|
"react-resize-detector": "^11.0.1",
|
|
34
|
-
"@dxos/invariant": "0.8.4-
|
|
35
|
-
"@dxos/debug": "0.8.4-
|
|
36
|
-
"@dxos/
|
|
37
|
-
"@dxos/node-std": "0.8.4-
|
|
38
|
-
"@dxos/
|
|
34
|
+
"@dxos/invariant": "0.8.4-staging.60fe92afc8",
|
|
35
|
+
"@dxos/debug": "0.8.4-staging.60fe92afc8",
|
|
36
|
+
"@dxos/util": "0.8.4-staging.60fe92afc8",
|
|
37
|
+
"@dxos/node-std": "0.8.4-staging.60fe92afc8",
|
|
38
|
+
"@dxos/log": "0.8.4-staging.60fe92afc8"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@svgr/cli": "^8.1.0",
|
|
42
42
|
"@types/lodash.defaultsdeep": "^4.6.6",
|
|
43
|
-
"@types/react": "~
|
|
44
|
-
"@types/react-dom": "~
|
|
43
|
+
"@types/react": "~19.2.7",
|
|
44
|
+
"@types/react-dom": "~19.2.3",
|
|
45
45
|
"lodash.defaultsdeep": "^4.6.1",
|
|
46
|
-
"react": "~
|
|
47
|
-
"react-dom": "~
|
|
48
|
-
"vite": "
|
|
49
|
-
"@dxos/react-ui": "0.8.4-
|
|
50
|
-
"@dxos/
|
|
51
|
-
"@dxos/
|
|
46
|
+
"react": "~19.2.3",
|
|
47
|
+
"react-dom": "~19.2.3",
|
|
48
|
+
"vite": "^8.0.16",
|
|
49
|
+
"@dxos/react-ui": "0.8.4-staging.60fe92afc8",
|
|
50
|
+
"@dxos/storybook-utils": "0.8.4-staging.60fe92afc8",
|
|
51
|
+
"@dxos/ui-theme": "0.8.4-staging.60fe92afc8"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"react": "~
|
|
55
|
-
"react-dom": "~
|
|
56
|
-
"@dxos/
|
|
57
|
-
"@dxos/react-ui
|
|
54
|
+
"react": "~19.2.3",
|
|
55
|
+
"react-dom": "~19.2.3",
|
|
56
|
+
"@dxos/ui-theme": "0.8.4-staging.60fe92afc8",
|
|
57
|
+
"@dxos/react-ui": "0.8.4-staging.60fe92afc8"
|
|
58
58
|
},
|
|
59
59
|
"publishConfig": {
|
|
60
60
|
"access": "public"
|
|
@@ -2,16 +2,15 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import '@
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
5
|
+
import { RegistryContext } from '@effect-atom/atom-react';
|
|
6
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
7
|
+
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
|
9
8
|
|
|
10
9
|
import { Button, Toolbar } from '@dxos/react-ui';
|
|
11
|
-
import { withLayout, withTheme } from '@dxos/
|
|
10
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
11
|
+
import { withRegistry } from '@dxos/storybook-utils';
|
|
12
12
|
|
|
13
13
|
import { Gameboard, type GameboardRootProps, type Move, type Player } from '../Gameboard';
|
|
14
|
-
|
|
15
14
|
import { ChessModel } from './chess';
|
|
16
15
|
import { Chessboard, type ChessboardProps } from './Chessboard';
|
|
17
16
|
|
|
@@ -20,7 +19,8 @@ type DefaultStoryProps = Pick<ChessboardProps, 'orientation' | 'showLabels' | 'd
|
|
|
20
19
|
};
|
|
21
20
|
|
|
22
21
|
const DefaultStory = ({ orientation: _orientation, pgn, ...props }: DefaultStoryProps) => {
|
|
23
|
-
const
|
|
22
|
+
const registry = useContext(RegistryContext);
|
|
23
|
+
const model = useMemo(() => new ChessModel(registry, pgn), [registry, pgn]);
|
|
24
24
|
const [orientation, setOrientation] = useState<Player | undefined>(_orientation);
|
|
25
25
|
|
|
26
26
|
const handleDrop = useCallback<NonNullable<GameboardRootProps<ChessModel>['onDrop']>>(
|
|
@@ -54,7 +54,8 @@ const DefaultStory = ({ orientation: _orientation, pgn, ...props }: DefaultStory
|
|
|
54
54
|
};
|
|
55
55
|
|
|
56
56
|
const GridStory = () => {
|
|
57
|
-
const
|
|
57
|
+
const registry = useContext(RegistryContext);
|
|
58
|
+
const models = useMemo(() => Array.from({ length: 9 }).map(() => new ChessModel(registry)), [registry]);
|
|
58
59
|
useEffect(() => {
|
|
59
60
|
const i = setInterval(() => {
|
|
60
61
|
const model = models[Math.floor(Math.random() * models.length)];
|
|
@@ -78,34 +79,35 @@ const GridStory = () => {
|
|
|
78
79
|
);
|
|
79
80
|
};
|
|
80
81
|
|
|
81
|
-
const meta
|
|
82
|
+
const meta = {
|
|
82
83
|
title: 'ui/react-ui-gameboard/Chessboard',
|
|
83
84
|
component: Chessboard,
|
|
84
85
|
render: DefaultStory,
|
|
85
|
-
decorators: [withTheme, withLayout({
|
|
86
|
-
}
|
|
86
|
+
decorators: [withRegistry, withTheme(), withLayout({ layout: 'column' })],
|
|
87
|
+
} satisfies Meta<typeof Chessboard>;
|
|
87
88
|
|
|
88
89
|
export default meta;
|
|
89
90
|
|
|
90
|
-
type Story = StoryObj<typeof
|
|
91
|
+
type Story = StoryObj<typeof meta>;
|
|
91
92
|
|
|
92
|
-
export const Default = {}
|
|
93
|
+
export const Default: Story = {};
|
|
93
94
|
|
|
94
|
-
export const Promotion = {
|
|
95
|
+
export const Promotion: Story = {
|
|
95
96
|
args: {
|
|
96
97
|
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 *',
|
|
97
98
|
},
|
|
98
|
-
}
|
|
99
|
+
};
|
|
99
100
|
|
|
100
|
-
export const Debug = {
|
|
101
|
+
export const Debug: Story = {
|
|
101
102
|
args: {
|
|
102
103
|
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
104
|
orientation: 'black',
|
|
104
105
|
showLabels: true,
|
|
105
106
|
debug: true,
|
|
106
107
|
},
|
|
107
|
-
}
|
|
108
|
+
};
|
|
108
109
|
|
|
109
110
|
export const Grid = {
|
|
111
|
+
decorators: [withRegistry, withTheme(), withLayout({ layout: 'fullscreen' })],
|
|
110
112
|
render: GridStory,
|
|
111
|
-
}
|
|
113
|
+
};
|
|
@@ -2,26 +2,35 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import { Atom, useAtomValue } from '@effect-atom/atom-react';
|
|
6
|
+
import React, { type PropsWithChildren, forwardRef, memo, useEffect, useMemo, useRef, useState } from 'react';
|
|
6
7
|
import { useResizeDetector } from 'react-resize-detector';
|
|
7
8
|
|
|
8
|
-
import { type ThemedClassName,
|
|
9
|
-
import { mx } from '@dxos/
|
|
10
|
-
import {
|
|
9
|
+
import { type ThemedClassName, useForwardedRef } from '@dxos/react-ui';
|
|
10
|
+
import { mx } from '@dxos/ui-theme';
|
|
11
|
+
import { isNonNullable } from '@dxos/util';
|
|
11
12
|
|
|
12
13
|
import {
|
|
13
14
|
type DOMRectBounds,
|
|
14
15
|
Gameboard,
|
|
15
16
|
type Location,
|
|
17
|
+
type PieceMap,
|
|
16
18
|
type PieceRecord,
|
|
17
19
|
type Player,
|
|
18
20
|
getRelativeBounds,
|
|
19
21
|
locationToString,
|
|
20
22
|
useGameboardContext,
|
|
21
23
|
} from '../Gameboard';
|
|
22
|
-
|
|
23
24
|
import { type ChessModel, type ChessPiece, ChessPieces, boardStyles, getSquareColor, locationToPos } from './chess';
|
|
24
25
|
|
|
26
|
+
/** Fallback atom for when model is undefined. */
|
|
27
|
+
const EMPTY_PIECES_ATOM = Atom.make<PieceMap<ChessPiece>>({});
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Chessboard layout.
|
|
31
|
+
*/
|
|
32
|
+
const CHESSBOARD_NAME = 'Chessboard';
|
|
33
|
+
|
|
25
34
|
export type ChessboardProps = ThemedClassName<
|
|
26
35
|
PropsWithChildren<{
|
|
27
36
|
orientation?: Player;
|
|
@@ -32,16 +41,15 @@ export type ChessboardProps = ThemedClassName<
|
|
|
32
41
|
}>
|
|
33
42
|
>;
|
|
34
43
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const { ref: containerRef, width, height } = useResizeDetector({ refreshRate: 200 });
|
|
42
|
-
const { model, promoting, onPromotion } = useGameboardContext<ChessModel>(Chessboard.displayName!);
|
|
44
|
+
const ChessboardComponent = forwardRef<HTMLDivElement, ChessboardProps>(
|
|
45
|
+
({ classNames, orientation, showLabels, debug, rows = 8, cols = 8 }, forwardedRef) => {
|
|
46
|
+
const targetRef = useForwardedRef(forwardedRef);
|
|
47
|
+
const { width, height } = useResizeDetector({ targetRef, refreshRate: 200 });
|
|
48
|
+
const { model, promoting, onPromotion } = useGameboardContext<ChessModel>(CHESSBOARD_NAME);
|
|
49
|
+
const pieces = useAtomValue(model?.pieces ?? EMPTY_PIECES_ATOM);
|
|
43
50
|
|
|
44
|
-
|
|
51
|
+
// Board squares.
|
|
52
|
+
const squares = useMemo<Location[]>(() => {
|
|
45
53
|
return Array.from({ length: rows }, (_, i) => (orientation === 'black' ? i : rows - 1 - i)).flatMap((row) =>
|
|
46
54
|
Array.from({ length: cols }).map((_, col) => [row, col] as Location),
|
|
47
55
|
);
|
|
@@ -49,24 +57,24 @@ export const Chessboard = memo(
|
|
|
49
57
|
|
|
50
58
|
// Use DOM grid layout to position squares.
|
|
51
59
|
const layout = useMemo(() => {
|
|
52
|
-
return
|
|
60
|
+
return squares.map((location) => {
|
|
53
61
|
return (
|
|
54
62
|
<div
|
|
55
63
|
key={locationToString(location)}
|
|
56
64
|
{...{
|
|
57
|
-
|
|
65
|
+
'data-location': locationToString(location),
|
|
58
66
|
}}
|
|
59
67
|
/>
|
|
60
68
|
);
|
|
61
69
|
});
|
|
62
|
-
}, [
|
|
70
|
+
}, [squares]);
|
|
63
71
|
|
|
64
72
|
// Build map of square locations to bounds.
|
|
65
73
|
const [grid, setGrid] = useState<Record<string, DOMRectBounds>>({});
|
|
66
74
|
const gridRef = useRef<HTMLDivElement>(null);
|
|
67
75
|
useEffect(() => {
|
|
68
76
|
setGrid(
|
|
69
|
-
|
|
77
|
+
squares.reduce(
|
|
70
78
|
(acc, location) => {
|
|
71
79
|
const square = getSquareLocation(gridRef.current!, location)!;
|
|
72
80
|
const bounds = getRelativeBounds(gridRef.current!, square);
|
|
@@ -75,7 +83,7 @@ export const Chessboard = memo(
|
|
|
75
83
|
{} as Record<string, DOMRectBounds>,
|
|
76
84
|
),
|
|
77
85
|
);
|
|
78
|
-
}, [
|
|
86
|
+
}, [squares, width, height]);
|
|
79
87
|
|
|
80
88
|
// Get the bounds of each square and piece.
|
|
81
89
|
const positions = useMemo<{ piece: PieceRecord; bounds: DOMRectBounds }[]>(() => {
|
|
@@ -83,7 +91,7 @@ export const Chessboard = memo(
|
|
|
83
91
|
return [];
|
|
84
92
|
}
|
|
85
93
|
|
|
86
|
-
return Object.values(
|
|
94
|
+
return Object.values(pieces)
|
|
87
95
|
.map((piece) => {
|
|
88
96
|
if (piece.id === promoting?.id) {
|
|
89
97
|
return null;
|
|
@@ -92,16 +100,18 @@ export const Chessboard = memo(
|
|
|
92
100
|
const bounds = grid[locationToString(piece.location)];
|
|
93
101
|
return { piece, bounds };
|
|
94
102
|
})
|
|
95
|
-
.filter(
|
|
96
|
-
}, [grid,
|
|
103
|
+
.filter(isNonNullable);
|
|
104
|
+
}, [grid, pieces, promoting]);
|
|
97
105
|
|
|
98
106
|
return (
|
|
99
|
-
<div ref={
|
|
107
|
+
<div ref={targetRef} tabIndex={0} className={mx('dx-expander relative outline-hidden', classNames)}>
|
|
108
|
+
{/* DOM Layout. */}
|
|
100
109
|
<div ref={gridRef} className='grid grid-rows-8 grid-cols-8 aspect-square select-none'>
|
|
101
110
|
{layout}
|
|
102
111
|
</div>
|
|
112
|
+
{/* Squares. */}
|
|
103
113
|
<div>
|
|
104
|
-
{
|
|
114
|
+
{squares.map((location) => (
|
|
105
115
|
<Gameboard.Square
|
|
106
116
|
key={locationToString(location)}
|
|
107
117
|
location={location}
|
|
@@ -111,6 +121,7 @@ export const Chessboard = memo(
|
|
|
111
121
|
/>
|
|
112
122
|
))}
|
|
113
123
|
</div>
|
|
124
|
+
{/* Pieces. */}
|
|
114
125
|
<div className={mx(promoting && 'opacity-50')}>
|
|
115
126
|
{positions.map(({ bounds, piece }) => (
|
|
116
127
|
<Gameboard.Piece
|
|
@@ -123,13 +134,14 @@ export const Chessboard = memo(
|
|
|
123
134
|
/>
|
|
124
135
|
))}
|
|
125
136
|
</div>
|
|
137
|
+
{/* Promotion selector. */}
|
|
126
138
|
{promoting && (
|
|
127
139
|
<PromotionSelector
|
|
128
140
|
grid={grid}
|
|
129
141
|
piece={promoting}
|
|
130
142
|
onSelect={(piece) => {
|
|
131
143
|
onPromotion({
|
|
132
|
-
from: Object.values(
|
|
144
|
+
from: Object.values(pieces).find((p) => p.id === promoting.id)!.location,
|
|
133
145
|
to: piece.location,
|
|
134
146
|
piece: promoting.type,
|
|
135
147
|
promotion: piece.type,
|
|
@@ -142,11 +154,9 @@ export const Chessboard = memo(
|
|
|
142
154
|
},
|
|
143
155
|
);
|
|
144
156
|
|
|
145
|
-
|
|
157
|
+
ChessboardComponent.displayName = CHESSBOARD_NAME;
|
|
146
158
|
|
|
147
|
-
const
|
|
148
|
-
return container.querySelector(`[data-location="${locationToString(location)}"]`);
|
|
149
|
-
};
|
|
159
|
+
export const Chessboard = memo(ChessboardComponent);
|
|
150
160
|
|
|
151
161
|
const PromotionSelector = ({
|
|
152
162
|
grid,
|
|
@@ -179,13 +189,17 @@ const PromotionSelector = ({
|
|
|
179
189
|
{positions.map(({ piece, bounds }) => (
|
|
180
190
|
<Gameboard.Piece
|
|
181
191
|
key={piece.id}
|
|
192
|
+
classNames={mx('border-2 border-neutral-700 rounded-full', boardStyles.promotion)}
|
|
182
193
|
piece={piece}
|
|
183
194
|
bounds={bounds}
|
|
184
195
|
Component={ChessPieces[piece.type as ChessPiece]}
|
|
185
|
-
classNames={mx('border-2 border-neutral-700 rounded-full', boardStyles.promotion)}
|
|
186
196
|
onClick={() => handleSelect(piece)}
|
|
187
197
|
/>
|
|
188
198
|
))}
|
|
189
199
|
</>
|
|
190
200
|
);
|
|
191
201
|
};
|
|
202
|
+
|
|
203
|
+
const getSquareLocation = (container: HTMLElement, location: Location): HTMLElement | null => {
|
|
204
|
+
return container.querySelector(`[data-location="${locationToString(location)}"]`);
|
|
205
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Chess as ChessJS } from 'chess.js';
|
|
6
|
+
import { describe, it } from 'vitest';
|
|
7
|
+
|
|
8
|
+
import { createPieceMap } from './chess';
|
|
9
|
+
|
|
10
|
+
describe('ChessModel', () => {
|
|
11
|
+
it('should update pieces', ({ expect }) => {
|
|
12
|
+
const chess = new ChessJS();
|
|
13
|
+
chess.loadPgn(
|
|
14
|
+
'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 *',
|
|
15
|
+
);
|
|
16
|
+
const pieces = createPieceMap(chess);
|
|
17
|
+
expect(pieces).to.exist;
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { type
|
|
5
|
+
import { Atom, type Registry } from '@effect-atom/atom-react';
|
|
6
6
|
import { Chess as ChessJS } from 'chess.js';
|
|
7
7
|
import { type FC, type SVGProps } from 'react';
|
|
8
8
|
|
|
9
|
+
import { invariant } from '@dxos/invariant';
|
|
9
10
|
import { log } from '@dxos/log';
|
|
10
11
|
|
|
11
12
|
import * as Alpha from '../../gen/pieces/chess/alpha';
|
|
@@ -33,6 +34,10 @@ export const locationToPos = ([row, col]: Location): string => {
|
|
|
33
34
|
return String.fromCharCode(col + 'a'.charCodeAt(0)) + (row + 1);
|
|
34
35
|
};
|
|
35
36
|
|
|
37
|
+
export const getRawPgn = (pgn: string) => {
|
|
38
|
+
return pgn.replace(/\[.*?\]/g, '').trim();
|
|
39
|
+
};
|
|
40
|
+
|
|
36
41
|
const styles = {
|
|
37
42
|
neutral: {
|
|
38
43
|
black: 'bg-neutral-50',
|
|
@@ -62,7 +67,7 @@ export const getSquareColor = ([row, col]: Location) => {
|
|
|
62
67
|
return (col + row) % 2 === 0 ? boardStyles.black : boardStyles.white;
|
|
63
68
|
};
|
|
64
69
|
|
|
65
|
-
export const createChess = (pgn?: string) => {
|
|
70
|
+
export const createChess = (pgn?: string): ChessJS => {
|
|
66
71
|
const chess = new ChessJS();
|
|
67
72
|
if (pgn) {
|
|
68
73
|
try {
|
|
@@ -75,43 +80,43 @@ export const createChess = (pgn?: string) => {
|
|
|
75
80
|
return chess;
|
|
76
81
|
};
|
|
77
82
|
|
|
78
|
-
/**
|
|
79
|
-
* Attempt move.
|
|
80
|
-
*/
|
|
81
|
-
const tryMove = (chess: ChessJS, move: Move): ChessJS | null => {
|
|
82
|
-
const from = locationToPos(move.from);
|
|
83
|
-
const to = locationToPos(move.to);
|
|
84
|
-
try {
|
|
85
|
-
const promotion = move.promotion ? move.promotion[1].toLowerCase() : 'q';
|
|
86
|
-
chess.move({ from, to, promotion }, { strict: false });
|
|
87
|
-
return chess;
|
|
88
|
-
} catch {
|
|
89
|
-
// Ignore.
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
|
|
94
83
|
/**
|
|
95
84
|
* Chess model.
|
|
96
85
|
*/
|
|
97
86
|
export class ChessModel implements GameboardModel<ChessPiece> {
|
|
98
87
|
private readonly _chess = new ChessJS();
|
|
99
|
-
private readonly _pieces =
|
|
88
|
+
private readonly _pieces = Atom.make<PieceMap<ChessPiece>>({});
|
|
89
|
+
private readonly _moveIndex = Atom.make(0);
|
|
100
90
|
|
|
101
|
-
constructor(
|
|
91
|
+
constructor(
|
|
92
|
+
private readonly _registry: Registry.Registry,
|
|
93
|
+
pgn?: string,
|
|
94
|
+
) {
|
|
102
95
|
this.update(pgn);
|
|
103
96
|
}
|
|
104
97
|
|
|
98
|
+
get readonly(): boolean {
|
|
99
|
+
return this._registry.get(this._moveIndex) !== this._chess.history().length;
|
|
100
|
+
}
|
|
101
|
+
|
|
105
102
|
get turn(): Player {
|
|
106
103
|
return this._chess.turn() === 'w' ? 'white' : 'black';
|
|
107
104
|
}
|
|
108
105
|
|
|
109
|
-
get
|
|
106
|
+
get game(): ChessJS {
|
|
107
|
+
return this._chess;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
get pieces(): Atom.Atom<PieceMap<ChessPiece>> {
|
|
110
111
|
return this._pieces;
|
|
111
112
|
}
|
|
112
113
|
|
|
113
|
-
get
|
|
114
|
-
return this.
|
|
114
|
+
get moveIndex(): Atom.Atom<number> {
|
|
115
|
+
return this._moveIndex;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
get fen() {
|
|
119
|
+
return this._chess.fen();
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
/**
|
|
@@ -127,11 +132,16 @@ export class ChessModel implements GameboardModel<ChessPiece> {
|
|
|
127
132
|
*/
|
|
128
133
|
// TODO(burdon): Update headers.
|
|
129
134
|
get pgn(): string {
|
|
130
|
-
return this._chess.pgn();
|
|
135
|
+
return getRawPgn(this._chess.pgn());
|
|
131
136
|
}
|
|
132
137
|
|
|
133
|
-
|
|
134
|
-
|
|
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);
|
|
135
145
|
}
|
|
136
146
|
|
|
137
147
|
update(pgn = ''): void {
|
|
@@ -151,10 +161,10 @@ export class ChessModel implements GameboardModel<ChessPiece> {
|
|
|
151
161
|
|
|
152
162
|
const current = this._chess.history();
|
|
153
163
|
if (!isValidNextMove(previous, current)) {
|
|
154
|
-
this._pieces
|
|
164
|
+
this._registry.set(this._pieces, {});
|
|
155
165
|
}
|
|
156
166
|
|
|
157
|
-
this.
|
|
167
|
+
this._updateBoard(this._chess);
|
|
158
168
|
}
|
|
159
169
|
|
|
160
170
|
isValidMove(move: Move): boolean {
|
|
@@ -173,7 +183,7 @@ export class ChessModel implements GameboardModel<ChessPiece> {
|
|
|
173
183
|
return false;
|
|
174
184
|
}
|
|
175
185
|
|
|
176
|
-
this.
|
|
186
|
+
this._updateBoard(this._chess);
|
|
177
187
|
return true;
|
|
178
188
|
}
|
|
179
189
|
|
|
@@ -185,37 +195,35 @@ export class ChessModel implements GameboardModel<ChessPiece> {
|
|
|
185
195
|
|
|
186
196
|
const move = moves[Math.floor(Math.random() * moves.length)];
|
|
187
197
|
this._chess.move(move);
|
|
188
|
-
|
|
198
|
+
|
|
199
|
+
this._updateBoard(this._chess);
|
|
189
200
|
return true;
|
|
190
201
|
}
|
|
191
202
|
|
|
192
203
|
/**
|
|
193
204
|
* Update pieces preserving identity.
|
|
194
205
|
*/
|
|
195
|
-
private
|
|
196
|
-
|
|
197
|
-
this.
|
|
198
|
-
row.forEach((record) => {
|
|
199
|
-
if (!record) {
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const { square, type, color } = record;
|
|
204
|
-
const pieceType = `${color.toUpperCase()}${type.toUpperCase()}` as ChessPiece;
|
|
205
|
-
const location = posToLocation(square);
|
|
206
|
-
pieces[locationToString(location)] = {
|
|
207
|
-
id: `${square}-${pieceType}`,
|
|
208
|
-
type: pieceType,
|
|
209
|
-
side: color === 'w' ? 'white' : 'black',
|
|
210
|
-
location,
|
|
211
|
-
};
|
|
212
|
-
}),
|
|
213
|
-
);
|
|
214
|
-
|
|
215
|
-
this._pieces.value = mapPieces(this._pieces.value, pieces);
|
|
206
|
+
private _updateBoard(chess: ChessJS): void {
|
|
207
|
+
this._registry.set(this._pieces, createPieceMap(chess));
|
|
208
|
+
this._registry.set(this._moveIndex, chess.history().length);
|
|
216
209
|
}
|
|
217
210
|
}
|
|
218
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
|
+
|
|
219
227
|
const isValidNextMove = (previous: string[], current: string[]) => {
|
|
220
228
|
if (current.length > previous.length + 1) {
|
|
221
229
|
return false;
|
|
@@ -230,21 +238,71 @@ const isValidNextMove = (previous: string[], current: string[]) => {
|
|
|
230
238
|
return true;
|
|
231
239
|
};
|
|
232
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
|
+
|
|
233
289
|
/**
|
|
234
290
|
* Preserve the original piece objects (and IDs).
|
|
235
291
|
*/
|
|
236
|
-
|
|
292
|
+
const _diffPieces = <T extends PieceType>(before: PieceMap<T>, after: PieceMap<T>): PieceMap<T> => {
|
|
237
293
|
const difference: { added: PieceMap; removed: PieceMap } = {
|
|
238
294
|
removed: {},
|
|
239
295
|
added: {},
|
|
240
296
|
};
|
|
241
297
|
|
|
298
|
+
// Removed.
|
|
242
299
|
(Object.keys(before) as Array<keyof typeof before>).forEach((square) => {
|
|
243
300
|
if (after[square]?.type !== before[square]?.type) {
|
|
244
301
|
difference.removed[square] = before[square];
|
|
245
302
|
}
|
|
246
303
|
});
|
|
247
304
|
|
|
305
|
+
// Added.
|
|
248
306
|
(Object.keys(after) as Array<keyof typeof after>).forEach((square) => {
|
|
249
307
|
if (before[square]?.type !== after[square]?.type) {
|
|
250
308
|
difference.added[square] = after[square];
|
|
@@ -253,6 +311,7 @@ export const mapPieces = <T extends PieceType>(before: PieceMap<T>, after: Piece
|
|
|
253
311
|
}
|
|
254
312
|
});
|
|
255
313
|
|
|
314
|
+
// Preserve IDs.
|
|
256
315
|
for (const piece of Object.values(difference.added)) {
|
|
257
316
|
const previous = Object.values(difference.removed).find((p) => p.type === piece.type);
|
|
258
317
|
if (previous) {
|
|
@@ -260,13 +319,6 @@ export const mapPieces = <T extends PieceType>(before: PieceMap<T>, after: Piece
|
|
|
260
319
|
}
|
|
261
320
|
}
|
|
262
321
|
|
|
263
|
-
log('delta', {
|
|
264
|
-
before: Object.keys(before).length,
|
|
265
|
-
after: Object.keys(after).length,
|
|
266
|
-
removed: Object.keys(difference.removed).length,
|
|
267
|
-
added: Object.keys(difference.added).length,
|
|
268
|
-
});
|
|
269
|
-
|
|
270
322
|
return after;
|
|
271
323
|
};
|
|
272
324
|
|