@dxos/react-ui-gameboard 0.7.5-main.ff8607b → 0.7.5-staging.2ff1350

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