@ankhorage/zora 21.1.4 → 22.0.1
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/CHANGELOG.md +12 -0
- package/README.md +4 -1
- package/dist/features/authoring/index.d.ts +0 -4
- package/dist/features/authoring/index.d.ts.map +1 -1
- package/dist/features/authoring/index.js +0 -3
- package/dist/features/authoring/index.js.map +1 -1
- package/dist/features/chess/adapters/inbound/ChessBoard.d.ts +1 -1
- package/dist/features/chess/adapters/inbound/ChessBoard.d.ts.map +1 -1
- package/dist/features/chess/adapters/inbound/ChessBoard.js +23 -7
- package/dist/features/chess/adapters/inbound/ChessBoard.js.map +1 -1
- package/dist/features/chess/meta.d.ts +68 -0
- package/dist/features/chess/meta.d.ts.map +1 -1
- package/dist/features/chess/meta.js +36 -1
- package/dist/features/chess/meta.js.map +1 -1
- package/dist/features/chess/public.d.ts +3 -1
- package/dist/features/chess/public.d.ts.map +1 -1
- package/dist/features/chess/public.js +2 -0
- package/dist/features/chess/public.js.map +1 -1
- package/dist/features/chess/utils/chessEngine.d.ts +8 -0
- package/dist/features/chess/utils/chessEngine.d.ts.map +1 -0
- package/dist/features/chess/utils/chessEngine.js +71 -0
- package/dist/features/chess/utils/chessEngine.js.map +1 -0
- package/dist/features/tabletop/meta.d.ts +32 -0
- package/dist/features/tabletop/meta.d.ts.map +1 -1
- package/dist/features/tabletop/meta.js +24 -0
- package/dist/features/tabletop/meta.js.map +1 -1
- package/dist/features/tabletop/public.d.ts +1 -0
- package/dist/features/tabletop/public.d.ts.map +1 -1
- package/dist/features/tabletop/public.js +1 -0
- package/dist/features/tabletop/public.js.map +1 -1
- package/dist/index.d.ts +4 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -5
- package/dist/index.js.map +1 -1
- package/dist/types/chess.d.ts +10 -0
- package/dist/types/chess.d.ts.map +1 -1
- package/dist/types/chess.js.map +1 -1
- package/package.json +5 -4
- package/src/features/authoring/featureManifest.test.ts +46 -10
- package/src/features/authoring/index.ts +0 -8
- package/src/features/chess/adapters/inbound/ChessBoard.tsx +34 -11
- package/src/features/chess/meta.ts +36 -1
- package/src/features/chess/public.test.tsx +83 -0
- package/src/features/chess/public.ts +3 -0
- package/src/features/chess/utils/chessEngine.test.ts +35 -0
- package/src/features/chess/utils/chessEngine.ts +83 -0
- package/src/features/chessTabletopOwnership.test.ts +8 -4
- package/src/features/tabletop/meta.ts +24 -0
- package/src/features/tabletop/public.test.tsx +12 -0
- package/src/features/tabletop/public.ts +1 -0
- package/src/index.ts +7 -15
- package/src/structureOwnership.test.ts +2 -7
- package/src/types/chess.ts +11 -0
- package/web-dist/chunks/Accordion-784f23tq.js +3813 -0
- package/web-dist/components/card-back/CardBack.js +1 -1
- package/web-dist/components/card-hand/CardHand.js +1 -1
- package/web-dist/components/chess-board/ChessBoard.js +1 -1
- package/web-dist/components/opening-book/OpeningBook.js +1 -1
- package/web-dist/components/playing-card/PlayingCard.js +1 -1
- package/web-dist/components/poker-training-table/PokerTrainingTable.js +1 -1
- package/web-dist/components/tabletop-table/TabletopTable.js +1 -1
- package/web-dist/manifest.json +12 -24
- package/dist/features/plugin/ZORA_CORE_PLUGIN.d.ts +0 -4
- package/dist/features/plugin/ZORA_CORE_PLUGIN.d.ts.map +0 -1
- package/dist/features/plugin/ZORA_CORE_PLUGIN.js +0 -8
- package/dist/features/plugin/ZORA_CORE_PLUGIN.js.map +0 -1
- package/dist/features/plugin/ZORA_CORE_PLUGIN_METADATA.d.ts +0 -3
- package/dist/features/plugin/ZORA_CORE_PLUGIN_METADATA.d.ts.map +0 -1
- package/dist/features/plugin/ZORA_CORE_PLUGIN_METADATA.js +0 -36
- package/dist/features/plugin/ZORA_CORE_PLUGIN_METADATA.js.map +0 -1
- package/dist/features/plugin/application/use-cases/composeZoraPluginMetadata.d.ts +0 -4
- package/dist/features/plugin/application/use-cases/composeZoraPluginMetadata.d.ts.map +0 -1
- package/dist/features/plugin/application/use-cases/composeZoraPluginMetadata.js +0 -106
- package/dist/features/plugin/application/use-cases/composeZoraPluginMetadata.js.map +0 -1
- package/dist/features/plugin/application/use-cases/composeZoraPlugins.d.ts +0 -4
- package/dist/features/plugin/application/use-cases/composeZoraPlugins.d.ts.map +0 -1
- package/dist/features/plugin/application/use-cases/composeZoraPlugins.js +0 -41
- package/dist/features/plugin/application/use-cases/composeZoraPlugins.js.map +0 -1
- package/dist/features/plugin/domain/ZoraPluginCompositionError.d.ts +0 -14
- package/dist/features/plugin/domain/ZoraPluginCompositionError.d.ts.map +0 -1
- package/dist/features/plugin/domain/ZoraPluginCompositionError.js +0 -14
- package/dist/features/plugin/domain/ZoraPluginCompositionError.js.map +0 -1
- package/dist/features/plugin/public.d.ts +0 -4
- package/dist/features/plugin/public.d.ts.map +0 -1
- package/dist/features/plugin/public.js +0 -4
- package/dist/features/plugin/public.js.map +0 -1
- package/dist/features/plugin/runtime.d.ts +0 -3
- package/dist/features/plugin/runtime.d.ts.map +0 -1
- package/dist/features/plugin/runtime.js +0 -3
- package/dist/features/plugin/runtime.js.map +0 -1
- package/dist/types/plugin.d.ts +0 -29
- package/dist/types/plugin.d.ts.map +0 -1
- package/dist/types/plugin.js +0 -2
- package/dist/types/plugin.js.map +0 -1
- package/src/features/plugin/ZORA_CORE_PLUGIN.ts +0 -9
- package/src/features/plugin/ZORA_CORE_PLUGIN_METADATA.ts +0 -49
- package/src/features/plugin/application/use-cases/composeZoraPluginMetadata.ts +0 -166
- package/src/features/plugin/application/use-cases/composeZoraPlugins.ts +0 -76
- package/src/features/plugin/domain/ZoraPluginCompositionError.ts +0 -21
- package/src/features/plugin/pluginComposition.test.ts +0 -139
- package/src/features/plugin/public.ts +0 -3
- package/src/features/plugin/runtime.ts +0 -2
- package/src/types/plugin.ts +0 -43
- package/web-dist/chunks/Accordion-86z2tvvz.js +0 -321
- package/web-dist/components/zora-plugin-composition-error/ZoraPluginCompositionError.d.ts +0 -3
- package/web-dist/components/zora-plugin-composition-error/ZoraPluginCompositionError.js +0 -17
- package/web-dist/components/zora-plugin-composition-error/index.d.ts +0 -1
- package/web-dist/components/zora-plugin-composition-error/index.js +0 -2
- /package/web-dist/chunks/{Accordion-57mn1j9e.js → Accordion-z585n0mr.js} +0 -0
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
|
3
3
|
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
ChessBoardProps,
|
|
6
|
+
ChessMoveAttempt,
|
|
7
|
+
ChessPieceState,
|
|
8
|
+
ChessSquareId,
|
|
9
|
+
} from '../../../../types/chess';
|
|
5
10
|
import { withZoraThemeScope } from '../../../theme/adapters/inbound/withZoraThemeScope';
|
|
6
11
|
import { useZoraTheme } from '../../../theme/composition/useZoraTheme';
|
|
12
|
+
import { getLegalTargets, readChessPieces, tryMove } from '../../utils/chessEngine';
|
|
7
13
|
import { createBoardSquares } from '../../utils/createBoardSquares';
|
|
8
14
|
import { createChessBoardColorScheme } from '../../utils/createChessBoardColorScheme';
|
|
9
15
|
import { isLightSquare } from '../../utils/isLightSquare';
|
|
@@ -23,21 +29,25 @@ const symbols = new Map<string, string>([
|
|
|
23
29
|
['black:k', '♚'],
|
|
24
30
|
]);
|
|
25
31
|
|
|
26
|
-
/*** Renders caller-owned chess
|
|
32
|
+
/*** Renders caller-owned or FEN-backed chess state with optional legal-move validation. */
|
|
27
33
|
export const ChessBoard = withZoraThemeScope(ChessBoardInner);
|
|
28
34
|
|
|
29
|
-
/*** Renders the current presentation state
|
|
35
|
+
/*** Renders the current presentation state and delegates FEN validation to the core chess engine. */
|
|
30
36
|
function ChessBoardInner({
|
|
31
|
-
|
|
37
|
+
fen,
|
|
38
|
+
pieces,
|
|
32
39
|
orientation = 'white',
|
|
33
40
|
selectedSquare = null,
|
|
34
|
-
legalTargets
|
|
41
|
+
legalTargets,
|
|
35
42
|
lastMove = null,
|
|
36
43
|
disabled = false,
|
|
37
44
|
showCoordinates = false,
|
|
45
|
+
validateMoves = true,
|
|
38
46
|
colorScheme,
|
|
39
47
|
onSquarePress,
|
|
40
48
|
onMoveAttempt,
|
|
49
|
+
onLegalMove,
|
|
50
|
+
onInvalidMove,
|
|
41
51
|
renderPiece,
|
|
42
52
|
interactionPolicy,
|
|
43
53
|
testID,
|
|
@@ -45,15 +55,28 @@ function ChessBoardInner({
|
|
|
45
55
|
const { theme } = useZoraTheme();
|
|
46
56
|
const colors = createChessBoardColorScheme(theme, colorScheme);
|
|
47
57
|
const squares = createBoardSquares(orientation);
|
|
48
|
-
const piecesBySquare =
|
|
49
|
-
pieces
|
|
50
|
-
|
|
51
|
-
|
|
58
|
+
const piecesBySquare =
|
|
59
|
+
pieces === undefined
|
|
60
|
+
? fen === undefined
|
|
61
|
+
? new Map<ChessSquareId, ChessPieceState>()
|
|
62
|
+
: new Map(readChessPieces(fen))
|
|
63
|
+
: new Map<ChessSquareId, ChessPieceState>(pieces.map((piece) => [piece.square, piece]));
|
|
64
|
+
const resolvedLegalTargets =
|
|
65
|
+
legalTargets ??
|
|
66
|
+
(fen !== undefined && selectedSquare !== null ? getLegalTargets(fen, selectedSquare) : []);
|
|
67
|
+
const legalTargetSet = new Set(resolvedLegalTargets);
|
|
52
68
|
const pressSquare = (square: ChessSquareId) => {
|
|
53
69
|
if (disabled || interactionPolicy === 'passive') return;
|
|
54
70
|
onSquarePress?.(square);
|
|
55
|
-
if (selectedSquare
|
|
56
|
-
|
|
71
|
+
if (selectedSquare === null || selectedSquare === square) return;
|
|
72
|
+
|
|
73
|
+
const attempt: ChessMoveAttempt = { from: selectedSquare, to: square };
|
|
74
|
+
onMoveAttempt?.(attempt);
|
|
75
|
+
if (!validateMoves || fen === undefined) return;
|
|
76
|
+
|
|
77
|
+
const result = tryMove(fen, attempt);
|
|
78
|
+
if (result) onLegalMove?.(result);
|
|
79
|
+
else onInvalidMove?.(attempt);
|
|
57
80
|
};
|
|
58
81
|
return (
|
|
59
82
|
<View
|
|
@@ -9,7 +9,7 @@ export const chessBoardMeta = {
|
|
|
9
9
|
allowedChildren: [],
|
|
10
10
|
blueprint: {
|
|
11
11
|
label: 'Chess board',
|
|
12
|
-
defaultProps: { pieces: [], orientation: 'white', showCoordinates: true },
|
|
12
|
+
defaultProps: { pieces: [], orientation: 'white', showCoordinates: true, validateMoves: true },
|
|
13
13
|
},
|
|
14
14
|
events: {
|
|
15
15
|
squarePress: {
|
|
@@ -23,6 +23,28 @@ export const chessBoardMeta = {
|
|
|
23
23
|
payloadFields: [
|
|
24
24
|
{ path: 'from', type: 'string', label: 'From' },
|
|
25
25
|
{ path: 'to', type: 'string', label: 'To' },
|
|
26
|
+
{ path: 'promotion', type: 'string', label: 'Promotion' },
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
legalMove: {
|
|
30
|
+
label: 'Legal move',
|
|
31
|
+
eventType: 'chess.legalMove',
|
|
32
|
+
payloadFields: [
|
|
33
|
+
{ path: 'from', type: 'string', label: 'From' },
|
|
34
|
+
{ path: 'to', type: 'string', label: 'To' },
|
|
35
|
+
{ path: 'fen', type: 'string', label: 'Position' },
|
|
36
|
+
{ path: 'san', type: 'string', label: 'SAN' },
|
|
37
|
+
{ path: 'lan', type: 'string', label: 'LAN' },
|
|
38
|
+
{ path: 'promotion', type: 'string', label: 'Promotion' },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
invalidMove: {
|
|
42
|
+
label: 'Invalid move',
|
|
43
|
+
eventType: 'chess.invalidMove',
|
|
44
|
+
payloadFields: [
|
|
45
|
+
{ path: 'from', type: 'string', label: 'From' },
|
|
46
|
+
{ path: 'to', type: 'string', label: 'To' },
|
|
47
|
+
{ path: 'promotion', type: 'string', label: 'Promotion' },
|
|
26
48
|
],
|
|
27
49
|
},
|
|
28
50
|
},
|
|
@@ -43,6 +65,12 @@ export const chessBoardMeta = {
|
|
|
43
65
|
},
|
|
44
66
|
},
|
|
45
67
|
props: {
|
|
68
|
+
fen: {
|
|
69
|
+
type: 'string',
|
|
70
|
+
category: 'Position',
|
|
71
|
+
label: 'FEN',
|
|
72
|
+
authoring: { authority: 'instance' },
|
|
73
|
+
},
|
|
46
74
|
pieces: {
|
|
47
75
|
type: 'array',
|
|
48
76
|
category: 'Board',
|
|
@@ -78,6 +106,13 @@ export const chessBoardMeta = {
|
|
|
78
106
|
default: false,
|
|
79
107
|
authoring: { authority: 'instance' },
|
|
80
108
|
},
|
|
109
|
+
validateMoves: {
|
|
110
|
+
type: 'boolean',
|
|
111
|
+
category: 'Behavior',
|
|
112
|
+
label: 'Validate moves',
|
|
113
|
+
default: true,
|
|
114
|
+
authoring: { authority: 'instance' },
|
|
115
|
+
},
|
|
81
116
|
disabled: {
|
|
82
117
|
type: 'boolean',
|
|
83
118
|
category: 'State',
|
|
@@ -7,6 +7,7 @@ import React, { act } from 'react';
|
|
|
7
7
|
import { createRoot } from 'react-dom/client';
|
|
8
8
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
9
9
|
|
|
10
|
+
import { chessBoardMeta } from './meta';
|
|
10
11
|
import type {
|
|
11
12
|
ChessBoard as ChessBoardComponent,
|
|
12
13
|
OpeningBook as OpeningBookComponent,
|
|
@@ -30,6 +31,24 @@ describe('Chess presentation', () => {
|
|
|
30
31
|
expect(typeof createChessBoardColorScheme).toBe('function');
|
|
31
32
|
});
|
|
32
33
|
|
|
34
|
+
test('preserves FEN and validated-move authoring metadata', () => {
|
|
35
|
+
expect(chessBoardMeta.props.fen.type).toBe('string');
|
|
36
|
+
expect(chessBoardMeta.props.validateMoves.default).toBe(true);
|
|
37
|
+
expect(chessBoardMeta.events.legalMove.payloadFields.map((field) => field.path)).toEqual([
|
|
38
|
+
'from',
|
|
39
|
+
'to',
|
|
40
|
+
'fen',
|
|
41
|
+
'san',
|
|
42
|
+
'lan',
|
|
43
|
+
'promotion',
|
|
44
|
+
]);
|
|
45
|
+
expect(chessBoardMeta.events.invalidMove.payloadFields.map((field) => field.path)).toEqual([
|
|
46
|
+
'from',
|
|
47
|
+
'to',
|
|
48
|
+
'promotion',
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
51
|
+
|
|
33
52
|
test('renders custom React nodes, square IDs, and opening metadata through independent artifacts', async () => {
|
|
34
53
|
const { ChessBoard } = (await load('components/chess-board/index.js')) as {
|
|
35
54
|
ChessBoard: typeof ChessBoardComponent;
|
|
@@ -88,6 +107,70 @@ describe('Chess presentation', () => {
|
|
|
88
107
|
browser.close();
|
|
89
108
|
});
|
|
90
109
|
|
|
110
|
+
test('preserves FEN-backed pieces, legal targets, and legal/invalid move callbacks', async () => {
|
|
111
|
+
const { ChessBoard } = (await load('components/chess-board/index.js')) as {
|
|
112
|
+
ChessBoard: typeof ChessBoardComponent;
|
|
113
|
+
};
|
|
114
|
+
const browser = new Window();
|
|
115
|
+
const keys = ['window', 'document', 'Node', 'navigator', 'IS_REACT_ACT_ENVIRONMENT'] as const;
|
|
116
|
+
const previous = keys.map(
|
|
117
|
+
(key) => [key, Object.getOwnPropertyDescriptor(globalThis, key)] as const,
|
|
118
|
+
);
|
|
119
|
+
Object.assign(globalThis, {
|
|
120
|
+
window: browser,
|
|
121
|
+
document: browser.document,
|
|
122
|
+
Node: browser.Node,
|
|
123
|
+
navigator: browser.navigator,
|
|
124
|
+
IS_REACT_ACT_ENVIRONMENT: true,
|
|
125
|
+
});
|
|
126
|
+
const host = document.createElement('div');
|
|
127
|
+
document.body.appendChild(host);
|
|
128
|
+
const legal: string[] = [];
|
|
129
|
+
const invalid: string[] = [];
|
|
130
|
+
const root = createRoot(host);
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
act(() =>
|
|
134
|
+
root.render(
|
|
135
|
+
<ChessBoard
|
|
136
|
+
fen="rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
|
|
137
|
+
onInvalidMove={(move) => invalid.push(`${move.from}-${move.to}`)}
|
|
138
|
+
onLegalMove={(move) => legal.push(`${move.lan}:${move.san}`)}
|
|
139
|
+
selectedSquare="e2"
|
|
140
|
+
showCoordinates
|
|
141
|
+
testID="fen-board"
|
|
142
|
+
/>,
|
|
143
|
+
),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
expect(host.querySelector('[data-testid="fen-board-square-e1"]')?.textContent).toContain('♔');
|
|
147
|
+
expect(
|
|
148
|
+
host.querySelector('[data-testid="fen-board-square-e4"]')?.getAttribute('style'),
|
|
149
|
+
).toContain('background-color');
|
|
150
|
+
|
|
151
|
+
act(() => {
|
|
152
|
+
host
|
|
153
|
+
.querySelector('[data-testid="fen-board-square-e4"]')
|
|
154
|
+
?.dispatchEvent(new browser.MouseEvent('click', { bubbles: true }));
|
|
155
|
+
});
|
|
156
|
+
expect(legal).toEqual(['e2e4:e4']);
|
|
157
|
+
|
|
158
|
+
act(() => {
|
|
159
|
+
host
|
|
160
|
+
.querySelector('[data-testid="fen-board-square-e5"]')
|
|
161
|
+
?.dispatchEvent(new browser.MouseEvent('click', { bubbles: true }));
|
|
162
|
+
});
|
|
163
|
+
expect(invalid).toEqual(['e2-e5']);
|
|
164
|
+
} finally {
|
|
165
|
+
act(() => root.unmount());
|
|
166
|
+
browser.close();
|
|
167
|
+
for (const [key, descriptor] of previous) {
|
|
168
|
+
if (descriptor) Object.defineProperty(globalThis, key, descriptor);
|
|
169
|
+
else Reflect.deleteProperty(globalThis, key);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
91
174
|
test('emits only caller-owned intents in active mode and suppresses them in passive mode', async () => {
|
|
92
175
|
const { ChessBoard } = (await load('components/chess-board/index.js')) as {
|
|
93
176
|
ChessBoard: typeof ChessBoardComponent;
|
|
@@ -4,6 +4,7 @@ export type {
|
|
|
4
4
|
ChessBoardOrientation,
|
|
5
5
|
ChessBoardProps,
|
|
6
6
|
ChessMoveAttempt,
|
|
7
|
+
ChessMoveResult,
|
|
7
8
|
ChessPieceCode,
|
|
8
9
|
ChessPieceRenderContext,
|
|
9
10
|
ChessPieceRenderer,
|
|
@@ -17,6 +18,8 @@ export type {
|
|
|
17
18
|
} from '../../types/chess';
|
|
18
19
|
export { ChessBoard } from './adapters/inbound/ChessBoard';
|
|
19
20
|
export { OpeningBook } from './adapters/inbound/OpeningBook';
|
|
21
|
+
export { chessBoardMeta, openingBookMeta } from './meta';
|
|
22
|
+
export { getLegalTargets, readChessPieces, tryMove } from './utils/chessEngine';
|
|
20
23
|
export { chessSquares } from './utils/chessSquares';
|
|
21
24
|
export { createBoardSquares } from './utils/createBoardSquares';
|
|
22
25
|
export type { ChessColorThemeShape } from './utils/createChessBoardColorScheme';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { getLegalTargets, readChessPieces, tryMove } from './chessEngine';
|
|
4
|
+
|
|
5
|
+
const START_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';
|
|
6
|
+
|
|
7
|
+
describe('core chess engine', () => {
|
|
8
|
+
it('reads pieces from FEN and preserves piece codes', () => {
|
|
9
|
+
const pieces = readChessPieces(START_FEN);
|
|
10
|
+
|
|
11
|
+
expect(pieces.size).toBe(32);
|
|
12
|
+
expect(pieces.get('e1')?.piece).toBe('K');
|
|
13
|
+
expect(pieces.get('e8')?.piece).toBe('k');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('returns an empty map for invalid FEN', () => {
|
|
17
|
+
expect(readChessPieces('invalid').size).toBe(0);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('returns legal target squares', () => {
|
|
21
|
+
expect(getLegalTargets(START_FEN, 'e2')).toEqual(['e3', 'e4']);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('returns resulting move details for legal moves', () => {
|
|
25
|
+
const result = tryMove(START_FEN, { from: 'e2', to: 'e4' });
|
|
26
|
+
|
|
27
|
+
expect(result?.san).toBe('e4');
|
|
28
|
+
expect(result?.lan).toBe('e2e4');
|
|
29
|
+
expect(result?.fen).toContain(' b ');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('returns null for invalid moves', () => {
|
|
33
|
+
expect(tryMove(START_FEN, { from: 'e2', to: 'e5' })).toBeNull();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Chess, type Move } from 'chess.js';
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
ChessMoveAttempt,
|
|
5
|
+
ChessMoveResult,
|
|
6
|
+
ChessPieceCode,
|
|
7
|
+
ChessPieceState,
|
|
8
|
+
ChessPromotionPiece,
|
|
9
|
+
ChessSquareId,
|
|
10
|
+
} from '../../../types/chess';
|
|
11
|
+
|
|
12
|
+
/*** Create a chess.js position from FEN without leaking invalid-position exceptions. */
|
|
13
|
+
function createChess(fen: string): Chess | null {
|
|
14
|
+
try {
|
|
15
|
+
return new Chess(fen);
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/*** Convert a chess.js piece into the public ZORA piece code. */
|
|
22
|
+
function toPieceCode(piece: { readonly color: 'b' | 'w'; readonly type: string }): ChessPieceCode {
|
|
23
|
+
return piece.color === 'w' ? piece.type.toUpperCase() : piece.type.toLowerCase();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/*** Normalize a chess.js promotion marker into the public promotion union. */
|
|
27
|
+
function toPromotionPiece(piece: string | undefined): ChessPromotionPiece | undefined {
|
|
28
|
+
return piece === 'q' || piece === 'r' || piece === 'b' || piece === 'n' ? piece : undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/*** Convert one successful chess.js move into the public result contract. */
|
|
32
|
+
function toMoveResult(move: Move, fen: string): ChessMoveResult {
|
|
33
|
+
return {
|
|
34
|
+
fen,
|
|
35
|
+
from: move.from,
|
|
36
|
+
lan: move.lan,
|
|
37
|
+
promotion: toPromotionPiece(move.promotion),
|
|
38
|
+
san: move.san,
|
|
39
|
+
to: move.to,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/*** Read all pieces from a FEN-backed position. Invalid FEN produces an empty position. */
|
|
44
|
+
export function readChessPieces(fen: string): ReadonlyMap<ChessSquareId, ChessPieceState> {
|
|
45
|
+
const chess = createChess(fen);
|
|
46
|
+
const pieces = new Map<ChessSquareId, ChessPieceState>();
|
|
47
|
+
if (!chess) return pieces;
|
|
48
|
+
|
|
49
|
+
for (const rank of chess.board()) {
|
|
50
|
+
for (const piece of rank) {
|
|
51
|
+
if (!piece) continue;
|
|
52
|
+
pieces.set(piece.square, {
|
|
53
|
+
color: piece.color === 'w' ? 'white' : 'black',
|
|
54
|
+
piece: toPieceCode(piece),
|
|
55
|
+
square: piece.square,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return pieces;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/*** Resolve legal destination squares from one FEN position and source square. */
|
|
63
|
+
export function getLegalTargets(fen: string, from: ChessSquareId): readonly ChessSquareId[] {
|
|
64
|
+
const chess = createChess(fen);
|
|
65
|
+
return chess ? chess.moves({ square: from, verbose: true }).map((move) => move.to) : [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/*** Attempt one legal chess move against FEN and return the resulting position/move notation. */
|
|
69
|
+
export function tryMove(fen: string, attempt: ChessMoveAttempt): ChessMoveResult | null {
|
|
70
|
+
const chess = createChess(fen);
|
|
71
|
+
if (!chess) return null;
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const move = chess.move({
|
|
75
|
+
from: attempt.from,
|
|
76
|
+
promotion: attempt.promotion,
|
|
77
|
+
to: attempt.to,
|
|
78
|
+
});
|
|
79
|
+
return toMoveResult(move, chess.fen());
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -10,7 +10,7 @@ test('Chess and Tabletop implementation modules own one matching runtime export
|
|
|
10
10
|
for (const featureRoot of featureRoots) {
|
|
11
11
|
for (const directory of [join(featureRoot, 'adapters/inbound'), join(featureRoot, 'utils')]) {
|
|
12
12
|
for (const name of readdirSync(directory)) {
|
|
13
|
-
if (!['.ts', '.tsx'].includes(extname(name))) continue;
|
|
13
|
+
if (!['.ts', '.tsx'].includes(extname(name)) || name.includes('.test.')) continue;
|
|
14
14
|
const source = ts.createSourceFile(
|
|
15
15
|
name,
|
|
16
16
|
readFileSync(join(directory, name), 'utf8'),
|
|
@@ -32,13 +32,17 @@ test('Chess and Tabletop implementation modules own one matching runtime export
|
|
|
32
32
|
);
|
|
33
33
|
return [];
|
|
34
34
|
});
|
|
35
|
-
|
|
35
|
+
const expectedExports =
|
|
36
|
+
name === 'chessEngine.ts'
|
|
37
|
+
? ['getLegalTargets', 'readChessPieces', 'tryMove']
|
|
38
|
+
: [parse(name).name];
|
|
39
|
+
expect(runtimeExports.sort(), join(directory, name)).toEqual(expectedExports.sort());
|
|
36
40
|
}
|
|
37
41
|
}
|
|
38
42
|
}
|
|
39
43
|
});
|
|
40
44
|
|
|
41
|
-
test('Chess and Tabletop source graphs
|
|
45
|
+
test('Chess and Tabletop source graphs depend only on their intentional runtime owners', () => {
|
|
42
46
|
const sourcePaths = featureRoots.flatMap((featureRoot) => {
|
|
43
47
|
const visit = (directory: string): string[] =>
|
|
44
48
|
readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
@@ -63,7 +67,7 @@ test('Chess and Tabletop source graphs contain no standalone package or game-ser
|
|
|
63
67
|
for (const specifier of imports) {
|
|
64
68
|
if (specifier.startsWith('.')) continue;
|
|
65
69
|
expect(
|
|
66
|
-
['react', 'react-native', '@ankhorage/color-theory'],
|
|
70
|
+
['react', 'react-native', 'chess.js', '@ankhorage/color-theory'],
|
|
67
71
|
`${path}: ${specifier}`,
|
|
68
72
|
).toContain(specifier);
|
|
69
73
|
}
|
|
@@ -116,6 +116,24 @@ export const tabletopTableMeta = {
|
|
|
116
116
|
acceptsFallback: true,
|
|
117
117
|
acceptsTransforms: true,
|
|
118
118
|
},
|
|
119
|
+
centerLabel: {
|
|
120
|
+
label: 'Center label',
|
|
121
|
+
value: { type: 'string' },
|
|
122
|
+
acceptsFallback: true,
|
|
123
|
+
acceptsTransforms: true,
|
|
124
|
+
},
|
|
125
|
+
centerSublabel: {
|
|
126
|
+
label: 'Center sublabel',
|
|
127
|
+
value: { type: 'string' },
|
|
128
|
+
acceptsFallback: true,
|
|
129
|
+
acceptsTransforms: true,
|
|
130
|
+
},
|
|
131
|
+
disabled: {
|
|
132
|
+
label: 'Disabled',
|
|
133
|
+
value: { type: 'boolean' },
|
|
134
|
+
acceptsFallback: true,
|
|
135
|
+
acceptsTransforms: true,
|
|
136
|
+
},
|
|
119
137
|
},
|
|
120
138
|
},
|
|
121
139
|
props: {
|
|
@@ -211,6 +229,12 @@ export const pokerTrainingTableMeta = {
|
|
|
211
229
|
},
|
|
212
230
|
acceptsFallback: true,
|
|
213
231
|
},
|
|
232
|
+
disabled: {
|
|
233
|
+
label: 'Disabled',
|
|
234
|
+
value: { type: 'boolean' },
|
|
235
|
+
acceptsFallback: true,
|
|
236
|
+
acceptsTransforms: true,
|
|
237
|
+
},
|
|
214
238
|
},
|
|
215
239
|
},
|
|
216
240
|
props: {
|
|
@@ -6,6 +6,7 @@ import { Window } from 'happy-dom';
|
|
|
6
6
|
import React from 'react';
|
|
7
7
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
8
8
|
|
|
9
|
+
import { pokerTrainingTableMeta, tabletopTableMeta } from './meta';
|
|
9
10
|
import type {
|
|
10
11
|
CardHand as CardHandComponent,
|
|
11
12
|
PokerTrainingTable as PokerTrainingTableComponent,
|
|
@@ -65,6 +66,17 @@ describe('Tabletop presentation', () => {
|
|
|
65
66
|
]);
|
|
66
67
|
});
|
|
67
68
|
|
|
69
|
+
test('preserves the standalone Tabletop authoring bindings in core metadata', () => {
|
|
70
|
+
expect(Object.keys(tabletopTableMeta.bindings.props)).toEqual([
|
|
71
|
+
'seats',
|
|
72
|
+
'centerCards',
|
|
73
|
+
'centerLabel',
|
|
74
|
+
'centerSublabel',
|
|
75
|
+
'disabled',
|
|
76
|
+
]);
|
|
77
|
+
expect(pokerTrainingTableMeta.bindings.props.disabled.value.type).toBe('boolean');
|
|
78
|
+
});
|
|
79
|
+
|
|
68
80
|
test('renders card sizing, per-card IDs, seat accessibility, and table shapes', async () => {
|
|
69
81
|
const { CardHand } = (await load('components/card-hand/index.js')) as {
|
|
70
82
|
CardHand: typeof CardHandComponent;
|
|
@@ -27,6 +27,7 @@ export { CardHand } from './adapters/inbound/CardHand';
|
|
|
27
27
|
export { PlayingCard } from './adapters/inbound/PlayingCard';
|
|
28
28
|
export { PokerTrainingTable } from './adapters/inbound/PokerTrainingTable';
|
|
29
29
|
export { TabletopTable } from './adapters/inbound/TabletopTable';
|
|
30
|
+
export { pokerTrainingTableMeta, tabletopTableMeta } from './meta';
|
|
30
31
|
export { createPokerTrainingTableState } from './utils/createPokerTrainingTableState';
|
|
31
32
|
export { createTabletopColorScheme } from './utils/createTabletopColorScheme';
|
|
32
33
|
export { createTabletopGameSeats } from './utils/createTabletopGameSeats';
|
package/src/index.ts
CHANGED
|
@@ -131,6 +131,7 @@ export type {
|
|
|
131
131
|
ChessBoardOrientation,
|
|
132
132
|
ChessBoardProps,
|
|
133
133
|
ChessMoveAttempt,
|
|
134
|
+
ChessMoveResult,
|
|
134
135
|
ChessPieceCode,
|
|
135
136
|
ChessPieceRenderContext,
|
|
136
137
|
ChessPieceRenderer,
|
|
@@ -143,15 +144,18 @@ export type {
|
|
|
143
144
|
OpeningBookProps,
|
|
144
145
|
} from './features/chess/public';
|
|
145
146
|
export type { ChessColorThemeShape } from './features/chess/public';
|
|
146
|
-
export { ChessBoard, OpeningBook } from './features/chess/public';
|
|
147
|
+
export { ChessBoard, chessBoardMeta, OpeningBook, openingBookMeta } from './features/chess/public';
|
|
147
148
|
export {
|
|
148
149
|
chessSquares,
|
|
149
150
|
createBoardSquares,
|
|
150
151
|
createChessBoardColorScheme,
|
|
151
152
|
createOpeningBookColorScheme,
|
|
153
|
+
getLegalTargets,
|
|
152
154
|
getSquareFile,
|
|
153
155
|
getSquareRank,
|
|
154
156
|
isLightSquare,
|
|
157
|
+
readChessPieces,
|
|
158
|
+
tryMove,
|
|
155
159
|
} from './features/chess/public';
|
|
156
160
|
export type { ChipProps } from './features/chip/public';
|
|
157
161
|
export type { ChipGroupItem, ChipGroupProps } from './features/chip/public';
|
|
@@ -319,12 +323,6 @@ export { MissingElement, type MissingElementProps } from './features/missing-ele
|
|
|
319
323
|
export { Pagination, type PaginationProps } from './features/pagination/public';
|
|
320
324
|
export type { PaletteItemProps } from './features/palette-item/public';
|
|
321
325
|
export { PaletteItem } from './features/palette-item/public';
|
|
322
|
-
export {
|
|
323
|
-
composeZoraPluginMetadata,
|
|
324
|
-
ZORA_CORE_PLUGIN_METADATA,
|
|
325
|
-
ZoraPluginCompositionError,
|
|
326
|
-
} from './features/plugin/public';
|
|
327
|
-
export { composeZoraPlugins, ZORA_CORE_PLUGIN } from './features/plugin/runtime';
|
|
328
326
|
export {
|
|
329
327
|
PopoverMenu,
|
|
330
328
|
type PopoverMenuAction,
|
|
@@ -423,7 +421,9 @@ export {
|
|
|
423
421
|
createTabletopGameSeats,
|
|
424
422
|
PlayingCard,
|
|
425
423
|
PokerTrainingTable,
|
|
424
|
+
pokerTrainingTableMeta,
|
|
426
425
|
TabletopTable,
|
|
426
|
+
tabletopTableMeta,
|
|
427
427
|
} from './features/tabletop/public';
|
|
428
428
|
export {
|
|
429
429
|
Tab,
|
|
@@ -495,12 +495,4 @@ export type {
|
|
|
495
495
|
ValidateUploadAssetInput,
|
|
496
496
|
} from './features/uploader/public';
|
|
497
497
|
export { Uploader, validateUploadAsset } from './features/uploader/public';
|
|
498
|
-
export type {
|
|
499
|
-
ComposedZoraPluginCatalog,
|
|
500
|
-
ComposedZoraPluginMetadataCatalog,
|
|
501
|
-
ZoraPluginCompositionErrorCode,
|
|
502
|
-
ZoraPluginDescriptor,
|
|
503
|
-
ZoraPluginMetadata,
|
|
504
|
-
ZoraPluginPlacement,
|
|
505
|
-
} from './types/plugin';
|
|
506
498
|
export type { ZoraComponentRegistry } from './types/registry';
|
|
@@ -26,10 +26,9 @@ describe('src ownership', () => {
|
|
|
26
26
|
}
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
test('keeps reusable authoring,
|
|
29
|
+
test('keeps reusable authoring, registry, and theme types centralized', () => {
|
|
30
30
|
for (const path of [
|
|
31
31
|
'src/types/authoring.ts',
|
|
32
|
-
'src/types/plugin.ts',
|
|
33
32
|
'src/types/registry.ts',
|
|
34
33
|
'src/types/theme.ts',
|
|
35
34
|
'src/types/theme-recipe.ts',
|
|
@@ -49,8 +48,6 @@ describe('src ownership', () => {
|
|
|
49
48
|
|
|
50
49
|
test('keeps deliberate feature facades instead of legacy catch-all implementation files', () => {
|
|
51
50
|
for (const path of [
|
|
52
|
-
'src/features/plugin/public.ts',
|
|
53
|
-
'src/features/plugin/runtime.ts',
|
|
54
51
|
'src/features/registry/public.ts',
|
|
55
52
|
'src/features/theme/public.ts',
|
|
56
53
|
'src/features/theme/runtime.ts',
|
|
@@ -59,9 +56,7 @@ describe('src ownership', () => {
|
|
|
59
56
|
}
|
|
60
57
|
|
|
61
58
|
for (const path of [
|
|
62
|
-
'src/features/plugin
|
|
63
|
-
'src/features/plugin/corePlugin.ts',
|
|
64
|
-
'src/features/plugin/corePluginMetadata.ts',
|
|
59
|
+
'src/features/plugin',
|
|
65
60
|
'src/features/registry/registry.ts',
|
|
66
61
|
'src/features/theme/index.ts',
|
|
67
62
|
]) {
|
package/src/types/chess.ts
CHANGED
|
@@ -14,6 +14,13 @@ export interface ChessMoveAttempt {
|
|
|
14
14
|
readonly promotion?: ChessPromotionPiece;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export interface ChessMoveResult extends Omit<ChessMoveAttempt, 'promotion'> {
|
|
18
|
+
readonly fen: string;
|
|
19
|
+
readonly lan: string;
|
|
20
|
+
readonly promotion?: ChessPromotionPiece;
|
|
21
|
+
readonly san: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
export interface ChessPieceState {
|
|
18
25
|
readonly color: 'black' | 'white';
|
|
19
26
|
readonly piece: ChessPieceCode;
|
|
@@ -58,6 +65,7 @@ export interface OpeningBookColorScheme {
|
|
|
58
65
|
export type OpeningBookColorOverrides = Partial<OpeningBookColorScheme>;
|
|
59
66
|
|
|
60
67
|
export interface ChessBoardProps extends ZoraBaseProps {
|
|
68
|
+
readonly fen?: string;
|
|
61
69
|
readonly pieces?: readonly ChessPieceState[];
|
|
62
70
|
readonly orientation?: ChessBoardOrientation;
|
|
63
71
|
readonly selectedSquare?: ChessSquareId | null;
|
|
@@ -65,9 +73,12 @@ export interface ChessBoardProps extends ZoraBaseProps {
|
|
|
65
73
|
readonly lastMove?: ChessMoveAttempt | null;
|
|
66
74
|
readonly disabled?: boolean;
|
|
67
75
|
readonly showCoordinates?: boolean;
|
|
76
|
+
readonly validateMoves?: boolean;
|
|
68
77
|
readonly colorScheme?: ChessBoardColorOverrides;
|
|
69
78
|
readonly onSquarePress?: (square: ChessSquareId) => void;
|
|
70
79
|
readonly onMoveAttempt?: (move: ChessMoveAttempt) => void;
|
|
80
|
+
readonly onLegalMove?: (move: ChessMoveResult) => void;
|
|
81
|
+
readonly onInvalidMove?: (move: ChessMoveAttempt) => void;
|
|
71
82
|
readonly renderPiece?: ChessPieceRenderer;
|
|
72
83
|
}
|
|
73
84
|
|