@dxos/react-ui-gameboard 0.8.4-main.fd6878d → 0.8.4-main.fffef41
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 +133 -98
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +133 -98
- package/dist/lib/node-esm/index.mjs.map +3 -3
- 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 +20 -18
- package/dist/types/src/components/Chessboard/Chessboard.stories.d.ts.map +1 -1
- package/dist/types/src/components/Chessboard/chess.d.ts +12 -6
- 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 +3 -2
- 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/types.d.ts +1 -0
- package/dist/types/src/components/Gameboard/types.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +19 -19
- package/src/components/Chessboard/Chessboard.stories.tsx +13 -15
- package/src/components/Chessboard/Chessboard.tsx +28 -21
- package/src/components/Chessboard/chess.test.ts +19 -0
- package/src/components/Chessboard/chess.ts +103 -54
- package/src/components/Gameboard/Gameboard.tsx +2 -1
- package/src/components/Gameboard/Piece.tsx +8 -5
- package/src/components/Gameboard/types.ts +1 -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.fffef41",
|
|
4
4
|
"description": "Game board.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -27,34 +27,34 @@
|
|
|
27
27
|
"@atlaskit/pragmatic-drag-and-drop": "^1.4.0",
|
|
28
28
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
|
|
29
29
|
"@preact-signals/safe-react": "^0.9.0",
|
|
30
|
-
"@preact/signals-core": "^1.
|
|
30
|
+
"@preact/signals-core": "^1.12.1",
|
|
31
31
|
"@radix-ui/react-context": "1.1.1",
|
|
32
32
|
"chess.js": "^1.4.0",
|
|
33
33
|
"react-resize-detector": "^11.0.1",
|
|
34
|
-
"@dxos/invariant": "0.8.4-main.
|
|
35
|
-
"@dxos/debug": "0.8.4-main.
|
|
36
|
-
"@dxos/log": "0.8.4-main.
|
|
37
|
-
"@dxos/node-std": "0.8.4-main.
|
|
38
|
-
"@dxos/util": "0.8.4-main.
|
|
34
|
+
"@dxos/invariant": "0.8.4-main.fffef41",
|
|
35
|
+
"@dxos/debug": "0.8.4-main.fffef41",
|
|
36
|
+
"@dxos/log": "0.8.4-main.fffef41",
|
|
37
|
+
"@dxos/node-std": "0.8.4-main.fffef41",
|
|
38
|
+
"@dxos/util": "0.8.4-main.fffef41"
|
|
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.2",
|
|
44
|
+
"@types/react-dom": "~19.2.2",
|
|
45
45
|
"lodash.defaultsdeep": "^4.6.1",
|
|
46
|
-
"react": "~
|
|
47
|
-
"react-dom": "~
|
|
48
|
-
"vite": "
|
|
49
|
-
"@dxos/react-ui": "0.8.4-main.
|
|
50
|
-
"@dxos/react-ui
|
|
51
|
-
"@dxos/storybook-utils": "0.8.4-main.
|
|
46
|
+
"react": "~19.2.0",
|
|
47
|
+
"react-dom": "~19.2.0",
|
|
48
|
+
"vite": "7.1.9",
|
|
49
|
+
"@dxos/react-ui-theme": "0.8.4-main.fffef41",
|
|
50
|
+
"@dxos/react-ui": "0.8.4-main.fffef41",
|
|
51
|
+
"@dxos/storybook-utils": "0.8.4-main.fffef41"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"react": "
|
|
55
|
-
"react-dom": "
|
|
56
|
-
"@dxos/react-ui": "0.8.4-main.
|
|
57
|
-
"@dxos/react-ui-theme": "0.8.4-main.
|
|
54
|
+
"react": "^19.0.0",
|
|
55
|
+
"react-dom": "^19.0.0",
|
|
56
|
+
"@dxos/react-ui": "0.8.4-main.fffef41",
|
|
57
|
+
"@dxos/react-ui-theme": "0.8.4-main.fffef41"
|
|
58
58
|
},
|
|
59
59
|
"publishConfig": {
|
|
60
60
|
"access": "public"
|
|
@@ -2,13 +2,11 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import '@
|
|
6
|
-
|
|
7
|
-
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
8
6
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
9
7
|
|
|
10
8
|
import { Button, Toolbar } from '@dxos/react-ui';
|
|
11
|
-
import { withLayout, withTheme } from '@dxos/
|
|
9
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
12
10
|
|
|
13
11
|
import { Gameboard, type GameboardRootProps, type Move, type Player } from '../Gameboard';
|
|
14
12
|
|
|
@@ -64,7 +62,7 @@ const GridStory = () => {
|
|
|
64
62
|
}, []);
|
|
65
63
|
|
|
66
64
|
return (
|
|
67
|
-
<div className='
|
|
65
|
+
<div className='bs-full aspect-square mx-auto'>
|
|
68
66
|
<div className='grid grid-cols-3 gap-2'>
|
|
69
67
|
{models.map((model, i) => (
|
|
70
68
|
<div key={i} className='aspect-square'>
|
|
@@ -78,34 +76,34 @@ const GridStory = () => {
|
|
|
78
76
|
);
|
|
79
77
|
};
|
|
80
78
|
|
|
81
|
-
const meta
|
|
79
|
+
const meta = {
|
|
82
80
|
title: 'ui/react-ui-gameboard/Chessboard',
|
|
83
81
|
component: Chessboard,
|
|
84
82
|
render: DefaultStory,
|
|
85
|
-
decorators: [withTheme, withLayout({
|
|
86
|
-
}
|
|
83
|
+
decorators: [withTheme, withLayout({ container: 'column' })],
|
|
84
|
+
} satisfies Meta<typeof Chessboard>;
|
|
87
85
|
|
|
88
86
|
export default meta;
|
|
89
87
|
|
|
90
|
-
type Story = StoryObj<typeof
|
|
88
|
+
type Story = StoryObj<typeof meta>;
|
|
91
89
|
|
|
92
|
-
export const Default = {}
|
|
90
|
+
export const Default: Story = {};
|
|
93
91
|
|
|
94
|
-
export const Promotion = {
|
|
92
|
+
export const Promotion: Story = {
|
|
95
93
|
args: {
|
|
96
94
|
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
95
|
},
|
|
98
|
-
}
|
|
96
|
+
};
|
|
99
97
|
|
|
100
|
-
export const Debug = {
|
|
98
|
+
export const Debug: Story = {
|
|
101
99
|
args: {
|
|
102
100
|
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
101
|
orientation: 'black',
|
|
104
102
|
showLabels: true,
|
|
105
103
|
debug: true,
|
|
106
104
|
},
|
|
107
|
-
}
|
|
105
|
+
};
|
|
108
106
|
|
|
109
107
|
export const Grid = {
|
|
110
108
|
render: GridStory,
|
|
111
|
-
}
|
|
109
|
+
};
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import React, {
|
|
5
|
+
import React, { type PropsWithChildren, forwardRef, memo, useEffect, useMemo, useRef, useState } from 'react';
|
|
6
6
|
import { useResizeDetector } from 'react-resize-detector';
|
|
7
7
|
|
|
8
|
-
import { type ThemedClassName,
|
|
8
|
+
import { type ThemedClassName, useForwardedRef } from '@dxos/react-ui';
|
|
9
9
|
import { mx } from '@dxos/react-ui-theme';
|
|
10
|
-
import {
|
|
10
|
+
import { isNonNullable } from '@dxos/util';
|
|
11
11
|
|
|
12
12
|
import {
|
|
13
13
|
type DOMRectBounds,
|
|
@@ -35,13 +35,14 @@ export type ChessboardProps = ThemedClassName<
|
|
|
35
35
|
/**
|
|
36
36
|
* Chessboard layout.
|
|
37
37
|
*/
|
|
38
|
-
|
|
39
|
-
({ orientation, showLabels, debug, rows = 8, cols = 8
|
|
40
|
-
|
|
41
|
-
const {
|
|
38
|
+
const ChessboardComponent = forwardRef<HTMLDivElement, ChessboardProps>(
|
|
39
|
+
({ classNames, orientation, showLabels, debug, rows = 8, cols = 8 }, forwardedRef) => {
|
|
40
|
+
const targetRef = useForwardedRef(forwardedRef);
|
|
41
|
+
const { width, height } = useResizeDetector({ targetRef, refreshRate: 200 });
|
|
42
42
|
const { model, promoting, onPromotion } = useGameboardContext<ChessModel>(Chessboard.displayName!);
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
// Board squares.
|
|
45
|
+
const squares = useMemo<Location[]>(() => {
|
|
45
46
|
return Array.from({ length: rows }, (_, i) => (orientation === 'black' ? i : rows - 1 - i)).flatMap((row) =>
|
|
46
47
|
Array.from({ length: cols }).map((_, col) => [row, col] as Location),
|
|
47
48
|
);
|
|
@@ -49,7 +50,7 @@ export const Chessboard = memo(
|
|
|
49
50
|
|
|
50
51
|
// Use DOM grid layout to position squares.
|
|
51
52
|
const layout = useMemo(() => {
|
|
52
|
-
return
|
|
53
|
+
return squares.map((location) => {
|
|
53
54
|
return (
|
|
54
55
|
<div
|
|
55
56
|
key={locationToString(location)}
|
|
@@ -59,14 +60,14 @@ export const Chessboard = memo(
|
|
|
59
60
|
/>
|
|
60
61
|
);
|
|
61
62
|
});
|
|
62
|
-
}, [
|
|
63
|
+
}, [squares]);
|
|
63
64
|
|
|
64
65
|
// Build map of square locations to bounds.
|
|
65
66
|
const [grid, setGrid] = useState<Record<string, DOMRectBounds>>({});
|
|
66
67
|
const gridRef = useRef<HTMLDivElement>(null);
|
|
67
68
|
useEffect(() => {
|
|
68
69
|
setGrid(
|
|
69
|
-
|
|
70
|
+
squares.reduce(
|
|
70
71
|
(acc, location) => {
|
|
71
72
|
const square = getSquareLocation(gridRef.current!, location)!;
|
|
72
73
|
const bounds = getRelativeBounds(gridRef.current!, square);
|
|
@@ -75,7 +76,7 @@ export const Chessboard = memo(
|
|
|
75
76
|
{} as Record<string, DOMRectBounds>,
|
|
76
77
|
),
|
|
77
78
|
);
|
|
78
|
-
}, [
|
|
79
|
+
}, [squares, width, height]);
|
|
79
80
|
|
|
80
81
|
// Get the bounds of each square and piece.
|
|
81
82
|
const positions = useMemo<{ piece: PieceRecord; bounds: DOMRectBounds }[]>(() => {
|
|
@@ -92,16 +93,18 @@ export const Chessboard = memo(
|
|
|
92
93
|
const bounds = grid[locationToString(piece.location)];
|
|
93
94
|
return { piece, bounds };
|
|
94
95
|
})
|
|
95
|
-
.filter(
|
|
96
|
+
.filter(isNonNullable);
|
|
96
97
|
}, [grid, model?.pieces.value, promoting]);
|
|
97
98
|
|
|
98
99
|
return (
|
|
99
|
-
<div ref={
|
|
100
|
+
<div ref={targetRef} tabIndex={0} className={mx('relative outline-none', classNames)}>
|
|
101
|
+
{/* DOM Layout. */}
|
|
100
102
|
<div ref={gridRef} className='grid grid-rows-8 grid-cols-8 aspect-square select-none'>
|
|
101
103
|
{layout}
|
|
102
104
|
</div>
|
|
105
|
+
{/* Squares. */}
|
|
103
106
|
<div>
|
|
104
|
-
{
|
|
107
|
+
{squares.map((location) => (
|
|
105
108
|
<Gameboard.Square
|
|
106
109
|
key={locationToString(location)}
|
|
107
110
|
location={location}
|
|
@@ -111,6 +114,7 @@ export const Chessboard = memo(
|
|
|
111
114
|
/>
|
|
112
115
|
))}
|
|
113
116
|
</div>
|
|
117
|
+
{/* Pieces. */}
|
|
114
118
|
<div className={mx(promoting && 'opacity-50')}>
|
|
115
119
|
{positions.map(({ bounds, piece }) => (
|
|
116
120
|
<Gameboard.Piece
|
|
@@ -123,13 +127,14 @@ export const Chessboard = memo(
|
|
|
123
127
|
/>
|
|
124
128
|
))}
|
|
125
129
|
</div>
|
|
130
|
+
{/* Promotion selector. */}
|
|
126
131
|
{promoting && (
|
|
127
132
|
<PromotionSelector
|
|
128
133
|
grid={grid}
|
|
129
134
|
piece={promoting}
|
|
130
135
|
onSelect={(piece) => {
|
|
131
136
|
onPromotion({
|
|
132
|
-
from: Object.values(model
|
|
137
|
+
from: Object.values(model.pieces.value).find((p) => p.id === promoting.id)!.location,
|
|
133
138
|
to: piece.location,
|
|
134
139
|
piece: promoting.type,
|
|
135
140
|
promotion: piece.type,
|
|
@@ -142,11 +147,9 @@ export const Chessboard = memo(
|
|
|
142
147
|
},
|
|
143
148
|
);
|
|
144
149
|
|
|
145
|
-
|
|
150
|
+
ChessboardComponent.displayName = 'Chessboard';
|
|
146
151
|
|
|
147
|
-
const
|
|
148
|
-
return container.querySelector(`[data-location="${locationToString(location)}"]`);
|
|
149
|
-
};
|
|
152
|
+
export const Chessboard = memo(ChessboardComponent);
|
|
150
153
|
|
|
151
154
|
const PromotionSelector = ({
|
|
152
155
|
grid,
|
|
@@ -179,13 +182,17 @@ const PromotionSelector = ({
|
|
|
179
182
|
{positions.map(({ piece, bounds }) => (
|
|
180
183
|
<Gameboard.Piece
|
|
181
184
|
key={piece.id}
|
|
185
|
+
classNames={mx('border-2 border-neutral-700 rounded-full', boardStyles.promotion)}
|
|
182
186
|
piece={piece}
|
|
183
187
|
bounds={bounds}
|
|
184
188
|
Component={ChessPieces[piece.type as ChessPiece]}
|
|
185
|
-
classNames={mx('border-2 border-neutral-700 rounded-full', boardStyles.promotion)}
|
|
186
189
|
onClick={() => handleSelect(piece)}
|
|
187
190
|
/>
|
|
188
191
|
))}
|
|
189
192
|
</>
|
|
190
193
|
);
|
|
191
194
|
};
|
|
195
|
+
|
|
196
|
+
const getSquareLocation = (container: HTMLElement, location: Location): HTMLElement | null => {
|
|
197
|
+
return container.querySelector(`[data-location="${locationToString(location)}"]`);
|
|
198
|
+
};
|
|
@@ -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
|
+
});
|
|
@@ -6,6 +6,7 @@ import { type ReadonlySignal, signal } from '@preact/signals-core';
|
|
|
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,40 @@ 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
88
|
private readonly _pieces = signal<PieceMap<ChessPiece>>({});
|
|
89
|
+
private readonly _moveIndex = signal(0);
|
|
100
90
|
|
|
101
91
|
constructor(pgn?: string) {
|
|
102
92
|
this.update(pgn);
|
|
103
93
|
}
|
|
104
94
|
|
|
95
|
+
get readonly(): boolean {
|
|
96
|
+
return this._moveIndex.value !== this._chess.history().length;
|
|
97
|
+
}
|
|
98
|
+
|
|
105
99
|
get turn(): Player {
|
|
106
100
|
return this._chess.turn() === 'w' ? 'white' : 'black';
|
|
107
101
|
}
|
|
108
102
|
|
|
103
|
+
get game(): ChessJS {
|
|
104
|
+
return this._chess;
|
|
105
|
+
}
|
|
106
|
+
|
|
109
107
|
get pieces(): ReadonlySignal<PieceMap<ChessPiece>> {
|
|
110
108
|
return this._pieces;
|
|
111
109
|
}
|
|
112
110
|
|
|
113
|
-
get
|
|
114
|
-
return this.
|
|
111
|
+
get moveIndex(): ReadonlySignal<number> {
|
|
112
|
+
return this._moveIndex;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
get fen() {
|
|
116
|
+
return this._chess.fen();
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
/**
|
|
@@ -127,11 +129,16 @@ export class ChessModel implements GameboardModel<ChessPiece> {
|
|
|
127
129
|
*/
|
|
128
130
|
// TODO(burdon): Update headers.
|
|
129
131
|
get pgn(): string {
|
|
130
|
-
return this._chess.pgn();
|
|
132
|
+
return getRawPgn(this._chess.pgn());
|
|
131
133
|
}
|
|
132
134
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
+
setMoveIndex(index: number) {
|
|
136
|
+
const temp = new ChessJS();
|
|
137
|
+
const history = this._chess.history({ verbose: true });
|
|
138
|
+
for (let i = 0; i < index && i < history.length; i++) {
|
|
139
|
+
temp.move(history[i]);
|
|
140
|
+
}
|
|
141
|
+
this._updateBoard(temp);
|
|
135
142
|
}
|
|
136
143
|
|
|
137
144
|
update(pgn = ''): void {
|
|
@@ -154,7 +161,7 @@ export class ChessModel implements GameboardModel<ChessPiece> {
|
|
|
154
161
|
this._pieces.value = {};
|
|
155
162
|
}
|
|
156
163
|
|
|
157
|
-
this.
|
|
164
|
+
this._updateBoard(this._chess);
|
|
158
165
|
}
|
|
159
166
|
|
|
160
167
|
isValidMove(move: Move): boolean {
|
|
@@ -173,7 +180,7 @@ export class ChessModel implements GameboardModel<ChessPiece> {
|
|
|
173
180
|
return false;
|
|
174
181
|
}
|
|
175
182
|
|
|
176
|
-
this.
|
|
183
|
+
this._updateBoard(this._chess);
|
|
177
184
|
return true;
|
|
178
185
|
}
|
|
179
186
|
|
|
@@ -185,37 +192,35 @@ export class ChessModel implements GameboardModel<ChessPiece> {
|
|
|
185
192
|
|
|
186
193
|
const move = moves[Math.floor(Math.random() * moves.length)];
|
|
187
194
|
this._chess.move(move);
|
|
188
|
-
|
|
195
|
+
|
|
196
|
+
this._updateBoard(this._chess);
|
|
189
197
|
return true;
|
|
190
198
|
}
|
|
191
199
|
|
|
192
200
|
/**
|
|
193
201
|
* Update pieces preserving identity.
|
|
194
202
|
*/
|
|
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);
|
|
203
|
+
private _updateBoard(chess: ChessJS): void {
|
|
204
|
+
this._pieces.value = createPieceMap(chess);
|
|
205
|
+
this._moveIndex.value = chess.history().length;
|
|
216
206
|
}
|
|
217
207
|
}
|
|
218
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Attempt move.
|
|
211
|
+
*/
|
|
212
|
+
const tryMove = (chess: ChessJS, move: Move): ChessJS | null => {
|
|
213
|
+
const from = locationToPos(move.from);
|
|
214
|
+
const to = locationToPos(move.to);
|
|
215
|
+
try {
|
|
216
|
+
const promotion = move.promotion ? move.promotion[1].toLowerCase() : 'q';
|
|
217
|
+
chess.move({ from, to, promotion }, { strict: false });
|
|
218
|
+
return chess;
|
|
219
|
+
} catch {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
219
224
|
const isValidNextMove = (previous: string[], current: string[]) => {
|
|
220
225
|
if (current.length > previous.length + 1) {
|
|
221
226
|
return false;
|
|
@@ -230,21 +235,71 @@ const isValidNextMove = (previous: string[], current: string[]) => {
|
|
|
230
235
|
return true;
|
|
231
236
|
};
|
|
232
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Starting from a new game, assign piece IDs based on their starting position.
|
|
240
|
+
* Then iterate through the history of the provided game and update the piece map.
|
|
241
|
+
*/
|
|
242
|
+
export const createPieceMap = (chess: ChessJS): PieceMap<ChessPiece> => {
|
|
243
|
+
const temp = new ChessJS();
|
|
244
|
+
let pieces = _createPieceMap(temp);
|
|
245
|
+
const history = chess.history({ verbose: true });
|
|
246
|
+
for (let i = 0; i < history.length; i++) {
|
|
247
|
+
const move = history[i];
|
|
248
|
+
temp.move(move);
|
|
249
|
+
pieces = _diffPieces(pieces, _createPieceMap(temp));
|
|
250
|
+
const test = new Set();
|
|
251
|
+
Object.values(pieces).forEach((piece) => {
|
|
252
|
+
invariant(!test.has(piece.id), 'Duplicate: ' + piece.id);
|
|
253
|
+
test.add(piece.id);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return pieces;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Create a map of pieces from the board positions; assign each piece the ID of the current square.
|
|
262
|
+
*/
|
|
263
|
+
const _createPieceMap = (chess: ChessJS): PieceMap<ChessPiece> => {
|
|
264
|
+
const pieces: PieceMap<ChessPiece> = {};
|
|
265
|
+
chess.board().flatMap((row) =>
|
|
266
|
+
row.forEach((record) => {
|
|
267
|
+
if (!record) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const { square, type, color } = record;
|
|
272
|
+
const pieceType = `${color.toUpperCase()}${type.toUpperCase()}` as ChessPiece;
|
|
273
|
+
const location = posToLocation(square);
|
|
274
|
+
pieces[locationToString(location)] = {
|
|
275
|
+
id: `${square}-${pieceType}`,
|
|
276
|
+
type: pieceType,
|
|
277
|
+
side: color === 'w' ? 'white' : 'black',
|
|
278
|
+
location,
|
|
279
|
+
};
|
|
280
|
+
}),
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
return pieces;
|
|
284
|
+
};
|
|
285
|
+
|
|
233
286
|
/**
|
|
234
287
|
* Preserve the original piece objects (and IDs).
|
|
235
288
|
*/
|
|
236
|
-
|
|
289
|
+
const _diffPieces = <T extends PieceType>(before: PieceMap<T>, after: PieceMap<T>): PieceMap<T> => {
|
|
237
290
|
const difference: { added: PieceMap; removed: PieceMap } = {
|
|
238
291
|
removed: {},
|
|
239
292
|
added: {},
|
|
240
293
|
};
|
|
241
294
|
|
|
295
|
+
// Removed.
|
|
242
296
|
(Object.keys(before) as Array<keyof typeof before>).forEach((square) => {
|
|
243
297
|
if (after[square]?.type !== before[square]?.type) {
|
|
244
298
|
difference.removed[square] = before[square];
|
|
245
299
|
}
|
|
246
300
|
});
|
|
247
301
|
|
|
302
|
+
// Added.
|
|
248
303
|
(Object.keys(after) as Array<keyof typeof after>).forEach((square) => {
|
|
249
304
|
if (before[square]?.type !== after[square]?.type) {
|
|
250
305
|
difference.added[square] = after[square];
|
|
@@ -253,6 +308,7 @@ export const mapPieces = <T extends PieceType>(before: PieceMap<T>, after: Piece
|
|
|
253
308
|
}
|
|
254
309
|
});
|
|
255
310
|
|
|
311
|
+
// Preserve IDs.
|
|
256
312
|
for (const piece of Object.values(difference.added)) {
|
|
257
313
|
const previous = Object.values(difference.removed).find((p) => p.type === piece.type);
|
|
258
314
|
if (previous) {
|
|
@@ -260,13 +316,6 @@ export const mapPieces = <T extends PieceType>(before: PieceMap<T>, after: Piece
|
|
|
260
316
|
}
|
|
261
317
|
}
|
|
262
318
|
|
|
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
319
|
return after;
|
|
271
320
|
};
|
|
272
321
|
|
|
@@ -33,13 +33,14 @@ const useGameboardContext = <M extends GameboardModel>(consumerName: string): Ga
|
|
|
33
33
|
|
|
34
34
|
type GameboardRootProps<M extends GameboardModel> = PropsWithChildren<{
|
|
35
35
|
model?: M;
|
|
36
|
+
moveNumber?: number;
|
|
36
37
|
onDrop?: (move: Move) => boolean;
|
|
37
38
|
}>;
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
41
|
* Generic board container.
|
|
41
42
|
*/
|
|
42
|
-
const GameboardRoot = <M extends GameboardModel>({ children, model, onDrop }: GameboardRootProps<M>) => {
|
|
43
|
+
const GameboardRoot = <M extends GameboardModel>({ children, model, moveNumber, onDrop }: GameboardRootProps<M>) => {
|
|
43
44
|
const [dragging, setDragging] = useState(false);
|
|
44
45
|
const [promoting, setPromoting] = useState<PieceRecord | undefined>();
|
|
45
46
|
|
|
@@ -10,7 +10,7 @@ import { createPortal } from 'react-dom';
|
|
|
10
10
|
|
|
11
11
|
import { invariant } from '@dxos/invariant';
|
|
12
12
|
import { log } from '@dxos/log';
|
|
13
|
-
import { type ThemedClassName, useDynamicRef
|
|
13
|
+
import { type ThemedClassName, useDynamicRef } from '@dxos/react-ui';
|
|
14
14
|
import { mx } from '@dxos/react-ui-theme';
|
|
15
15
|
|
|
16
16
|
import { useGameboardContext } from './Gameboard';
|
|
@@ -26,8 +26,7 @@ export type PieceProps = ThemedClassName<{
|
|
|
26
26
|
onClick?: () => void;
|
|
27
27
|
}>;
|
|
28
28
|
|
|
29
|
-
export const Piece = memo(({ classNames, Component, piece,
|
|
30
|
-
useTrackProps({ classNames, Component, piece, orientation, bounds, label }, Piece.displayName, false);
|
|
29
|
+
export const Piece = memo(({ classNames, Component, piece, bounds, label, onClick }: PieceProps) => {
|
|
31
30
|
const { model, dragging: isDragging, promoting } = useGameboardContext(Piece.displayName!);
|
|
32
31
|
const promotingRef = useDynamicRef(promoting);
|
|
33
32
|
const [dragging, setDragging] = useState(false);
|
|
@@ -38,6 +37,10 @@ export const Piece = memo(({ classNames, Component, piece, orientation, bounds,
|
|
|
38
37
|
|
|
39
38
|
const ref = useRef<HTMLDivElement>(null);
|
|
40
39
|
useEffect(() => {
|
|
40
|
+
if (!model) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
41
44
|
const el = ref.current;
|
|
42
45
|
invariant(el);
|
|
43
46
|
|
|
@@ -60,7 +63,7 @@ export const Piece = memo(({ classNames, Component, piece, orientation, bounds,
|
|
|
60
63
|
nativeSetDragImage,
|
|
61
64
|
});
|
|
62
65
|
},
|
|
63
|
-
canDrag: () => !promotingRef.current && model
|
|
66
|
+
canDrag: () => !promotingRef.current && !model.readonly && model.turn === piece.side,
|
|
64
67
|
onDragStart: () => setDragging(true),
|
|
65
68
|
onDrop: ({ location: { current } }) => {
|
|
66
69
|
// TODO(burdon): Create wrapper function to catch errors.
|
|
@@ -87,7 +90,7 @@ export const Piece = memo(({ classNames, Component, piece, orientation, bounds,
|
|
|
87
90
|
|
|
88
91
|
// Check if piece moved.
|
|
89
92
|
if (!current.location || !isEqualLocation(current.location, piece.location)) {
|
|
90
|
-
ref.current.style.transition = 'top
|
|
93
|
+
ref.current.style.transition = 'top 250ms ease-out, left 250ms ease-out';
|
|
91
94
|
ref.current.style.top = bounds.top + 'px';
|
|
92
95
|
ref.current.style.left = bounds.left + 'px';
|
|
93
96
|
setCurrent({ location: piece.location, bounds });
|
|
@@ -52,6 +52,7 @@ export const isEqualLocation = (l1: Location, l2: Location): boolean => l1[0] ==
|
|
|
52
52
|
* Generic board model.
|
|
53
53
|
*/
|
|
54
54
|
export interface GameboardModel<T extends PieceType = PieceType> {
|
|
55
|
+
readonly: boolean;
|
|
55
56
|
turn: Player;
|
|
56
57
|
/** @reactive */
|
|
57
58
|
pieces: ReadonlySignal<PieceMap<T>>;
|