@dxos/react-ui-gameboard 0.8.3 → 0.8.4-main.1068cf700f

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.
Files changed (76) hide show
  1. package/dist/lib/browser/index.mjs +764 -825
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +764 -825
  5. package/dist/lib/node-esm/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/components/Chessboard/Chessboard.d.ts +20 -0
  8. package/dist/types/src/components/Chessboard/Chessboard.d.ts.map +1 -0
  9. package/dist/types/src/components/Chessboard/Chessboard.stories.d.ts +30 -0
  10. package/dist/types/src/components/Chessboard/Chessboard.stories.d.ts.map +1 -0
  11. package/dist/types/src/components/Chessboard/chess.d.ts +60 -0
  12. package/dist/types/src/components/Chessboard/chess.d.ts.map +1 -0
  13. package/dist/types/src/components/Chessboard/chess.test.d.ts +2 -0
  14. package/dist/types/src/components/Chessboard/chess.test.d.ts.map +1 -0
  15. package/dist/types/src/components/Chessboard/index.d.ts.map +1 -0
  16. package/dist/types/src/components/Gameboard/Gameboard.d.ts +38 -0
  17. package/dist/types/src/components/Gameboard/Gameboard.d.ts.map +1 -0
  18. package/dist/types/src/{Board → components/Gameboard}/Piece.d.ts +3 -2
  19. package/dist/types/src/components/Gameboard/Piece.d.ts.map +1 -0
  20. package/dist/types/src/components/Gameboard/Square.d.ts.map +1 -0
  21. package/dist/types/src/components/Gameboard/index.d.ts +4 -0
  22. package/dist/types/src/components/Gameboard/index.d.ts.map +1 -0
  23. package/dist/types/src/{Board → components/Gameboard}/types.d.ts +4 -3
  24. package/dist/types/src/components/Gameboard/types.d.ts.map +1 -0
  25. package/dist/types/src/components/Gameboard/util.d.ts.map +1 -0
  26. package/dist/types/src/components/index.d.ts +3 -0
  27. package/dist/types/src/components/index.d.ts.map +1 -0
  28. package/dist/types/src/index.d.ts +1 -2
  29. package/dist/types/src/index.d.ts.map +1 -1
  30. package/dist/types/tsconfig.tsbuildinfo +1 -1
  31. package/package.json +32 -24
  32. package/src/components/Chessboard/Chessboard.stories.tsx +113 -0
  33. package/src/components/Chessboard/Chessboard.tsx +206 -0
  34. package/src/components/Chessboard/chess.test.ts +19 -0
  35. package/src/components/Chessboard/chess.ts +325 -0
  36. package/src/components/Gameboard/Gameboard.tsx +145 -0
  37. package/src/{Board → components/Gameboard}/Piece.tsx +27 -23
  38. package/src/{Board → components/Gameboard}/Square.tsx +8 -6
  39. package/src/components/Gameboard/index.ts +8 -0
  40. package/src/{Board → components/Gameboard}/types.ts +5 -3
  41. package/src/components/index.ts +6 -0
  42. package/src/index.ts +1 -2
  43. package/dist/lib/node/index.cjs +0 -1039
  44. package/dist/lib/node/index.cjs.map +0 -7
  45. package/dist/lib/node/meta.json +0 -1
  46. package/dist/types/src/Board/Board.d.ts +0 -15
  47. package/dist/types/src/Board/Board.d.ts.map +0 -1
  48. package/dist/types/src/Board/Container.d.ts +0 -14
  49. package/dist/types/src/Board/Container.d.ts.map +0 -1
  50. package/dist/types/src/Board/Piece.d.ts.map +0 -1
  51. package/dist/types/src/Board/Square.d.ts.map +0 -1
  52. package/dist/types/src/Board/context.d.ts +0 -10
  53. package/dist/types/src/Board/context.d.ts.map +0 -1
  54. package/dist/types/src/Board/index.d.ts +0 -8
  55. package/dist/types/src/Board/index.d.ts.map +0 -1
  56. package/dist/types/src/Board/types.d.ts.map +0 -1
  57. package/dist/types/src/Board/util.d.ts.map +0 -1
  58. package/dist/types/src/Chessboard/Chessboard.d.ts +0 -14
  59. package/dist/types/src/Chessboard/Chessboard.d.ts.map +0 -1
  60. package/dist/types/src/Chessboard/Chessboard.stories.d.ts +0 -16
  61. package/dist/types/src/Chessboard/Chessboard.stories.d.ts.map +0 -1
  62. package/dist/types/src/Chessboard/chess.d.ts +0 -40
  63. package/dist/types/src/Chessboard/chess.d.ts.map +0 -1
  64. package/dist/types/src/Chessboard/index.d.ts.map +0 -1
  65. package/src/Board/Board.tsx +0 -86
  66. package/src/Board/Container.tsx +0 -25
  67. package/src/Board/context.ts +0 -22
  68. package/src/Board/index.ts +0 -12
  69. package/src/Chessboard/Chessboard.stories.tsx +0 -108
  70. package/src/Chessboard/Chessboard.tsx +0 -190
  71. package/src/Chessboard/chess.ts +0 -213
  72. /package/dist/types/src/{Chessboard → components/Chessboard}/index.d.ts +0 -0
  73. /package/dist/types/src/{Board → components/Gameboard}/Square.d.ts +0 -0
  74. /package/dist/types/src/{Board → components/Gameboard}/util.d.ts +0 -0
  75. /package/src/{Chessboard → components/Chessboard}/index.ts +0 -0
  76. /package/src/{Board → components/Gameboard}/util.ts +0 -0
@@ -1,108 +0,0 @@
1
- //
2
- // Copyright 2024 DXOS.org
3
- //
4
-
5
- import '@dxos-theme';
6
-
7
- import type { Meta, StoryObj } from '@storybook/react';
8
- import React, { useCallback, useEffect, useMemo, useState } from 'react';
9
-
10
- import { log } from '@dxos/log';
11
- import { Button, Toolbar } from '@dxos/react-ui';
12
- import { withLayout, withTheme } from '@dxos/storybook-utils';
13
-
14
- import { Chessboard, type ChessboardProps } from './Chessboard';
15
- import { ChessModel } from './chess';
16
- import { Board, type BoardRootProps, type Player, type Move } from '../Board';
17
-
18
- type RenderProps = Pick<ChessboardProps, 'orientation' | 'showLabels' | 'debug'> & {
19
- fen: string;
20
- };
21
-
22
- const DefaultStory = ({ fen, orientation: _orientation, ...props }: RenderProps) => {
23
- const model = useMemo(() => new ChessModel(fen), [fen]);
24
- const [orientation, setOrientation] = useState<Player | undefined>(_orientation);
25
-
26
- const handleDrop = useCallback<NonNullable<BoardRootProps['onDrop']>>(
27
- (move: Move) => {
28
- log.info('handleDrop', { move });
29
- return model.makeMove(move);
30
- },
31
- [model],
32
- );
33
-
34
- return (
35
- <div className='flex flex-col grow gap-2 overflow-hidden'>
36
- <Toolbar.Root>
37
- <Button onClick={() => model.initialize()}>Reset</Button>
38
- <Button onClick={() => model.makeRandomMove()}>Move</Button>
39
- <div className='grow'></div>
40
- <Button
41
- onClick={() => setOrientation((orientation) => (!orientation || orientation === 'white' ? 'black' : 'white'))}
42
- >
43
- Toggle
44
- </Button>
45
- </Toolbar.Root>
46
- <Board.Root model={model} onDrop={handleDrop}>
47
- <Chessboard orientation={orientation} {...props} />
48
- </Board.Root>
49
- </div>
50
- );
51
- };
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
-
78
- const meta: Meta<typeof DefaultStory> = {
79
- title: 'ui/react-ui-gameboard/Chessboard',
80
- component: Chessboard,
81
- render: DefaultStory,
82
- decorators: [withTheme, withLayout({ fullscreen: true, classNames: '' })],
83
- };
84
-
85
- export default meta;
86
-
87
- type Story = StoryObj<typeof DefaultStory>;
88
-
89
- export const Default: Story = {};
90
-
91
- export const Promotion: Story = {
92
- args: {
93
- fen: '4k3/7P/8/8/8/8/1p6/4K3 w - - 0 1',
94
- },
95
- };
96
-
97
- export const Debug: Story = {
98
- args: {
99
- debug: true,
100
- showLabels: true,
101
- orientation: 'black',
102
- fen: 'q3k1nr/1pp1nQpp/3p4/1P2p3/4P3/B1PP1b2/B5PP/5K2 b k - 0 17',
103
- },
104
- };
105
-
106
- export const Nine: Story = {
107
- render: Grid,
108
- };
@@ -1,190 +0,0 @@
1
- //
2
- // Copyright 2025 DXOS.org
3
- //
4
-
5
- import React, { type PropsWithChildren, useRef, useMemo, useEffect, useState, memo } from 'react';
6
- import { useResizeDetector } from 'react-resize-detector';
7
-
8
- import { useTrackProps } from '@dxos/react-ui';
9
- import { mx } from '@dxos/react-ui-theme';
10
- import { isNotFalsy } from '@dxos/util';
11
-
12
- import { boardStyles, type ChessPiece, ChessPieces, getSquareColor, locationToPos } from './chess';
13
- import {
14
- type DOMRectBounds,
15
- type Location,
16
- type PieceRecord,
17
- type Player,
18
- Piece,
19
- Square,
20
- getRelativeBounds,
21
- locationToString,
22
- } from '../Board';
23
- import { useBoardContext } from '../Board';
24
-
25
- export type ChessboardProps = PropsWithChildren<{
26
- orientation?: Player;
27
- showLabels?: boolean;
28
- debug?: boolean;
29
- rows?: number;
30
- cols?: number;
31
- }>;
32
-
33
- /**
34
- * Chessboard layout.
35
- */
36
- export const Chessboard = memo(({ orientation, showLabels, debug, rows = 8, cols = 8 }: ChessboardProps) => {
37
- useTrackProps({ orientation, showLabels, debug }, Chessboard.displayName, false);
38
- const { ref: containerRef, width, height } = useResizeDetector({ refreshRate: 200 });
39
- const { model, promoting, onPromotion } = useBoardContext();
40
-
41
- const locations = useMemo<Location[]>(() => {
42
- return Array.from({ length: rows }, (_, i) => (orientation === 'black' ? i : rows - 1 - i)).flatMap((row) =>
43
- Array.from({ length: cols }).map((_, col) => [row, col] as Location),
44
- );
45
- }, [orientation, rows, cols]);
46
-
47
- // Use DOM grid layout to position squares.
48
- const layout = useMemo(() => {
49
- return locations.map((location) => {
50
- return (
51
- <div
52
- key={locationToString(location)}
53
- {...{
54
- ['data-location' as const]: locationToString(location),
55
- }}
56
- />
57
- );
58
- });
59
- }, [locations]);
60
-
61
- // Build map of square locations to bounds.
62
- const [grid, setGrid] = useState<Record<string, DOMRectBounds>>({});
63
- const gridRef = useRef<HTMLDivElement>(null);
64
- useEffect(() => {
65
- setGrid(
66
- locations.reduce(
67
- (acc, location) => {
68
- const square = getSquareLocation(gridRef.current!, location)!;
69
- const bounds = getRelativeBounds(gridRef.current!, square);
70
- return { ...acc, [locationToString(location)]: bounds };
71
- },
72
- {} as Record<string, DOMRectBounds>,
73
- ),
74
- );
75
- }, [locations, width, height]);
76
-
77
- // Get the bounds of each square and piece.
78
- const positions = useMemo<{ piece: PieceRecord; bounds: DOMRectBounds }[]>(() => {
79
- if (!gridRef.current) {
80
- return [];
81
- }
82
-
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]);
94
-
95
- return (
96
- <div ref={containerRef} className='relative'>
97
- <div ref={gridRef} className='grid grid-rows-8 grid-cols-8 aspect-square select-none'>
98
- {layout}
99
- </div>
100
- <div>
101
- {locations.map((location) => (
102
- <Square
103
- key={locationToString(location)}
104
- location={location}
105
- label={showLabels ? locationToPos(location) : undefined}
106
- bounds={grid[locationToString(location)]}
107
- classNames={getSquareColor(location)}
108
- />
109
- ))}
110
- </div>
111
- <div className={mx(promoting && 'opacity-50')}>
112
- {positions.map(({ bounds, piece }) => (
113
- <Piece
114
- key={piece.id}
115
- piece={piece}
116
- bounds={bounds}
117
- label={debug ? piece.id : undefined}
118
- orientation={orientation}
119
- Component={ChessPieces[piece.type as ChessPiece]}
120
- />
121
- ))}
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>
139
- </div>
140
- );
141
- });
142
-
143
- Chessboard.displayName = 'Chessboard';
144
-
145
- const getSquareLocation = (container: HTMLElement, location: Location): HTMLElement | null => {
146
- return container.querySelector(`[data-location="${locationToString(location)}"]`);
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
- };
@@ -1,213 +0,0 @@
1
- //
2
- // Copyright 2025 DXOS.org
3
- //
4
-
5
- import { type ReadonlySignal, signal } from '@preact/signals-core';
6
- import { Chess, validateFen } from 'chess.js';
7
- import { type FC, type SVGProps } from 'react';
8
-
9
- import { log } from '@dxos/log';
10
-
11
- import {
12
- type Move,
13
- type Location,
14
- type PieceMap,
15
- locationToString,
16
- type PieceType,
17
- type BoardModel,
18
- type Player,
19
- } from '../Board';
20
- import * as Alpha from '../gen/pieces/chess/alpha';
21
-
22
- export type ChessPiece = 'BK' | 'BQ' | 'BR' | 'BB' | 'BN' | 'BP' | 'WK' | 'WQ' | 'WR' | 'WB' | 'WN' | 'WP';
23
-
24
- export const ChessPieces: Record<ChessPiece, FC<SVGProps<SVGSVGElement>>> = Alpha;
25
-
26
- export const posToLocation = (pos: string): Location => {
27
- const col = pos.charCodeAt(0) - 'a'.charCodeAt(0);
28
- const row = parseInt(pos[1]) - 1;
29
- return [row, col];
30
- };
31
-
32
- export const locationToPos = ([row, col]: Location): string => {
33
- return String.fromCharCode(col + 'a'.charCodeAt(0)) + (row + 1);
34
- };
35
-
36
- const styles = {
37
- neutral: {
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',
46
- },
47
- blue: {
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',
56
- },
57
- };
58
-
59
- export const boardStyles = styles.original;
60
-
61
- export const getSquareColor = ([row, col]: Location) => {
62
- return (col + row) % 2 === 0 ? boardStyles.black : boardStyles.white;
63
- };
64
-
65
- /**
66
- * Attempt move.
67
- */
68
- const makeMove = (game: Chess, move: Move): Chess | null => {
69
- const from = locationToPos(move.from);
70
- const to = locationToPos(move.to);
71
- try {
72
- log('makeMove', { move });
73
- const promotion = move.promotion ? move.promotion[1].toLowerCase() : 'q';
74
- game.move({ from, to, promotion }, { strict: false });
75
- return game;
76
- } catch (err) {
77
- // Ignore.
78
- return null;
79
- }
80
- };
81
-
82
- /**
83
- * Chess model.
84
- */
85
- export class ChessModel implements BoardModel<ChessPiece> {
86
- private _game!: Chess;
87
- private readonly _pieces = signal<PieceMap<ChessPiece>>({});
88
-
89
- constructor(fen?: string) {
90
- this.initialize(fen);
91
- }
92
-
93
- get turn(): Player {
94
- return this._game.turn() === 'w' ? 'white' : 'black';
95
- }
96
-
97
- get pieces(): ReadonlySignal<PieceMap<ChessPiece>> {
98
- return this._pieces;
99
- }
100
-
101
- get game(): Chess {
102
- return this._game;
103
- }
104
-
105
- get fen(): string {
106
- return this._game.fen();
107
- }
108
-
109
- initialize(fen?: string): void {
110
- this._pieces.value = {};
111
- this._game = new Chess(fen ? (validateFen(fen).ok ? fen : undefined) : undefined);
112
- this._update();
113
- }
114
-
115
- isValidMove(move: Move): boolean {
116
- return makeMove(new Chess(this._game.fen()), move) !== null;
117
- }
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
-
125
- makeMove(move: Move): boolean {
126
- const game = makeMove(this._game, move);
127
- if (!game) {
128
- return false;
129
- }
130
-
131
- this._game = game;
132
- this._update();
133
- return true;
134
- }
135
-
136
- makeRandomMove(): boolean {
137
- const moves = this._game.moves();
138
- if (!moves.length) {
139
- return false;
140
- }
141
-
142
- const move = moves[Math.floor(Math.random() * moves.length)];
143
- this._game.move(move);
144
- this._update();
145
- return true;
146
- }
147
-
148
- /**
149
- * Update pieces preserving identity.
150
- */
151
- private _update(): void {
152
- const pieces: PieceMap<ChessPiece> = {};
153
- this._game.board().flatMap((row) =>
154
- row.forEach((record) => {
155
- if (!record) {
156
- return;
157
- }
158
-
159
- const { square, type, color } = record;
160
- const pieceType = `${color.toUpperCase()}${type.toUpperCase()}` as ChessPiece;
161
- const location = posToLocation(square);
162
- pieces[locationToString(location)] = {
163
- id: `${square}-${pieceType}`,
164
- type: pieceType,
165
- side: color === 'w' ? 'white' : 'black',
166
- location,
167
- };
168
- }),
169
- );
170
-
171
- this._pieces.value = mapPieces(this._pieces.value, pieces);
172
- }
173
- }
174
-
175
- /**
176
- * Preserve the original piece objects (and IDs).
177
- */
178
- export const mapPieces = <T extends PieceType>(before: PieceMap<T>, after: PieceMap<T>): PieceMap<T> => {
179
- const difference: { added: PieceMap; removed: PieceMap } = {
180
- removed: {},
181
- added: {},
182
- };
183
-
184
- (Object.keys(before) as Array<keyof typeof before>).forEach((square) => {
185
- if (after[square]?.type !== before[square]?.type) {
186
- difference.removed[square] = before[square];
187
- }
188
- });
189
-
190
- (Object.keys(after) as Array<keyof typeof after>).forEach((square) => {
191
- if (before[square]?.type !== after[square]?.type) {
192
- difference.added[square] = after[square];
193
- } else {
194
- after[square] = before[square];
195
- }
196
- });
197
-
198
- for (const piece of Object.values(difference.added)) {
199
- const previous = Object.values(difference.removed).find((p) => p.type === piece.type);
200
- if (previous) {
201
- piece.id = previous.id;
202
- }
203
- }
204
-
205
- log('delta', {
206
- before: Object.keys(before).length,
207
- after: Object.keys(after).length,
208
- removed: Object.keys(difference.removed).length,
209
- added: Object.keys(difference.added).length,
210
- });
211
-
212
- return after;
213
- };
File without changes