cm-chessboard 8.10.0 → 8.11.0
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/.claude/settings.local.json +9 -0
- package/CLAUDE.md +207 -0
- package/README.md +3 -2
- package/examples/validate-moves-chess960.html +121 -0
- package/package.json +1 -1
- package/src/view/VisualMoveInput.js +14 -10
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
cm-chessboard is a lightweight, dependency-free JavaScript chessboard library. It's ES6 module-based, responsive, SVG-rendered, and used in production on chessmail.eu/chessmail.de.
|
|
8
|
+
|
|
9
|
+
**Key Philosophy:** The core is intentionally minimal. Functionality is extended through an extension system rather than adding features to the core.
|
|
10
|
+
|
|
11
|
+
## Development Commands
|
|
12
|
+
|
|
13
|
+
### Running Tests
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Tests use the Teevi framework and must be run in a browser
|
|
17
|
+
# Open test/index.html directly to run the test suite
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### No Build Step
|
|
21
|
+
This project has no build process. It uses native ES6 modules that run directly in modern browsers.
|
|
22
|
+
|
|
23
|
+
## Architecture
|
|
24
|
+
|
|
25
|
+
### Core Components (MVC Pattern)
|
|
26
|
+
|
|
27
|
+
The codebase follows a clean MVC architecture:
|
|
28
|
+
|
|
29
|
+
1. **Model Layer** (`src/model/`)
|
|
30
|
+
- `Position.js` - Board state representation using FEN notation. Contains 64-square array and FEN parsing/serialization
|
|
31
|
+
- `ChessboardState.js` - Manages application state (orientation, input flags, extension points)
|
|
32
|
+
- `Extension.js` - Base class for all extensions. Defines EXTENSION_POINT constants
|
|
33
|
+
|
|
34
|
+
2. **View Layer** (`src/view/`)
|
|
35
|
+
- `ChessboardView.js` - SVG rendering, board drawing, piece rendering, coordinate display
|
|
36
|
+
- `VisualMoveInput.js` - Handles user interaction (drag & drop, click to move)
|
|
37
|
+
- `PositionAnimationsQueue.js` - Queues and executes piece animations
|
|
38
|
+
|
|
39
|
+
3. **Controller** (`src/Chessboard.js`)
|
|
40
|
+
- Main API class that coordinates model and view
|
|
41
|
+
- Entry point for all public methods
|
|
42
|
+
- Instantiates extensions from props
|
|
43
|
+
|
|
44
|
+
4. **Utilities** (`src/lib/`)
|
|
45
|
+
- `Svg.js` - SVG DOM manipulation helpers
|
|
46
|
+
- `Utils.js` - General utilities (URL handling, object merging)
|
|
47
|
+
|
|
48
|
+
### Data Flow
|
|
49
|
+
|
|
50
|
+
1. User calls API method on `Chessboard` instance
|
|
51
|
+
2. `Chessboard` updates `Position` in `ChessboardState`
|
|
52
|
+
3. Extension points are invoked via `state.invokeExtensionPoints()`
|
|
53
|
+
4. Animation is queued in `PositionAnimationsQueue`
|
|
54
|
+
5. `ChessboardView` renders the changes to SVG
|
|
55
|
+
|
|
56
|
+
### Extension System
|
|
57
|
+
|
|
58
|
+
Extensions are the primary way to add functionality. They:
|
|
59
|
+
- Extend the `Extension` base class
|
|
60
|
+
- Register callbacks at extension points using `registerExtensionPoint(EXTENSION_POINT.*, callback)`
|
|
61
|
+
- Can add methods directly to the chessboard instance (e.g., `chessboard.addMarker = this.addMarker.bind(this)`)
|
|
62
|
+
|
|
63
|
+
**Available Extension Points** (defined in `Extension.js`):
|
|
64
|
+
- `positionChanged` - Piece positions changed
|
|
65
|
+
- `boardChanged` - Board orientation changed
|
|
66
|
+
- `boardResized` - Board was resized
|
|
67
|
+
- `moveInputToggled` - Move input enabled/disabled
|
|
68
|
+
- `moveInput` - Move events (started, validating, canceled, finished)
|
|
69
|
+
- `beforeRedrawBoard` / `afterRedrawBoard` - Board redraw lifecycle
|
|
70
|
+
- `animation` - Animation lifecycle hooks
|
|
71
|
+
- `destroy` - Cleanup before board destruction
|
|
72
|
+
|
|
73
|
+
**Included Extensions** (`src/extensions/`):
|
|
74
|
+
- `markers/` - Visual markers on squares (frames, circles, dots)
|
|
75
|
+
- `arrows/` - Drawing arrows between squares
|
|
76
|
+
- `right-click-annotator/` - Right-click UI for annotations (uses markers + arrows)
|
|
77
|
+
- `accessibility/` - Screen reader support, braille notation, keyboard input
|
|
78
|
+
- `promotion-dialog/` - UI dialog for pawn promotion
|
|
79
|
+
- `persistence/` - Save/restore board state to localStorage
|
|
80
|
+
- `html-layer/` - Overlay HTML content on the board
|
|
81
|
+
- `auto-border-none/` - Utility extension
|
|
82
|
+
|
|
83
|
+
## Important Implementation Details
|
|
84
|
+
|
|
85
|
+
### Coordinate System
|
|
86
|
+
- Squares are represented as strings: "a1", "h8", etc.
|
|
87
|
+
- Internally uses 0-63 index (Position.squareToIndex / indexToSquare)
|
|
88
|
+
- Orientation affects rendering but not internal representation
|
|
89
|
+
|
|
90
|
+
### FEN Handling
|
|
91
|
+
- Position class only stores the piece placement (first part of full FEN)
|
|
92
|
+
- Full FEN format is supported in setPosition() but only piece positions are stored
|
|
93
|
+
- Use `FEN.start` and `FEN.empty` constants
|
|
94
|
+
|
|
95
|
+
### Piece Naming
|
|
96
|
+
- Format: `{color}{type}` - e.g., "wp" (white pawn), "bk" (black king)
|
|
97
|
+
- Color: "w" or "b"
|
|
98
|
+
- Type: "p", "n", "b", "r", "q", "k"
|
|
99
|
+
|
|
100
|
+
### SVG Rendering
|
|
101
|
+
- Pieces are rendered from SVG sprites (40x40px tiles)
|
|
102
|
+
- Sprite can be cached in a hidden div for performance
|
|
103
|
+
- Board uses multiple SVG groups as layers (board, coordinates, markers, pieces, etc.)
|
|
104
|
+
- Responsive sizing via ResizeObserver or window resize events
|
|
105
|
+
|
|
106
|
+
### Move Input System
|
|
107
|
+
- Implemented in `VisualMoveInput.js`
|
|
108
|
+
- Supports both drag-and-drop and click-to-move
|
|
109
|
+
- Callback receives event objects with types from `INPUT_EVENT_TYPE`
|
|
110
|
+
- Returns true/false to validate moves
|
|
111
|
+
- Move input can be enabled per color (white/black/both)
|
|
112
|
+
|
|
113
|
+
### Animation Queue
|
|
114
|
+
- All position changes are queued to prevent conflicts
|
|
115
|
+
- Animations return Promises that resolve when complete
|
|
116
|
+
- Queue ensures sequential execution even with rapid API calls
|
|
117
|
+
|
|
118
|
+
## Writing Extensions
|
|
119
|
+
|
|
120
|
+
Template for a new extension:
|
|
121
|
+
|
|
122
|
+
```javascript
|
|
123
|
+
import {Extension, EXTENSION_POINT} from "../../model/Extension.js"
|
|
124
|
+
|
|
125
|
+
export class MyExtension extends Extension {
|
|
126
|
+
constructor(chessboard, props = {}) {
|
|
127
|
+
super(chessboard)
|
|
128
|
+
|
|
129
|
+
// Set default props
|
|
130
|
+
this.props = {
|
|
131
|
+
myOption: true
|
|
132
|
+
}
|
|
133
|
+
Object.assign(this.props, props)
|
|
134
|
+
|
|
135
|
+
// Register extension points
|
|
136
|
+
this.registerExtensionPoint(EXTENSION_POINT.positionChanged, (data) => {
|
|
137
|
+
// React to position changes
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
// Add methods to chessboard instance
|
|
141
|
+
chessboard.myMethod = this.myMethod.bind(this)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
myMethod() {
|
|
145
|
+
// Implementation
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Enable extension in chessboard props:
|
|
151
|
+
```javascript
|
|
152
|
+
extensions: [{class: MyExtension, props: {myOption: false}}]
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Common Patterns
|
|
156
|
+
|
|
157
|
+
### Adding a piece
|
|
158
|
+
```javascript
|
|
159
|
+
board.setPiece("e4", "wn", true) // animated
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Moving pieces
|
|
163
|
+
```javascript
|
|
164
|
+
board.movePiece("e2", "e4", true) // animated
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Setting entire position
|
|
168
|
+
```javascript
|
|
169
|
+
board.setPosition(FEN.start, true) // animated
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Enabling move input
|
|
173
|
+
```javascript
|
|
174
|
+
board.enableMoveInput((event) => {
|
|
175
|
+
if (event.type === INPUT_EVENT_TYPE.validateMoveInput) {
|
|
176
|
+
// Validate the move
|
|
177
|
+
return isLegalMove(event.squareFrom, event.squareTo)
|
|
178
|
+
}
|
|
179
|
+
}, COLOR.white) // Optional: restrict to one side
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Creating markers (requires Markers extension)
|
|
183
|
+
```javascript
|
|
184
|
+
board.addMarker(MARKER_TYPE.dot, "e4")
|
|
185
|
+
board.removeMarkers(MARKER_TYPE.frame) // Remove by type
|
|
186
|
+
board.removeMarkers() // Remove all
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Assets Structure
|
|
190
|
+
|
|
191
|
+
- `assets/pieces/` - SVG sprite files for piece sets (standard.svg, staunty.svg)
|
|
192
|
+
- `assets/extensions/` - Assets needed by extensions (markers SVG, arrow SVG, etc.)
|
|
193
|
+
- `assets/chessboard.css` - Core styles
|
|
194
|
+
- Extension-specific CSS must be included separately when using extensions
|
|
195
|
+
|
|
196
|
+
## Examples
|
|
197
|
+
|
|
198
|
+
The `examples/` directory contains working examples demonstrating:
|
|
199
|
+
- Simple board creation
|
|
200
|
+
- Move input and validation
|
|
201
|
+
- Different visual styles
|
|
202
|
+
- All included extensions
|
|
203
|
+
- Animation examples
|
|
204
|
+
- Responsive boards
|
|
205
|
+
- Multiple boards on one page
|
|
206
|
+
|
|
207
|
+
Reference examples when implementing new features or debugging issues.
|
package/README.md
CHANGED
|
@@ -15,12 +15,13 @@ in [chess-console](https://shaack.com/projekte/chess-console/examples/load-pgn.h
|
|
|
15
15
|
- Uses SVG for rendering
|
|
16
16
|
- [Allows adding extensions to extend the
|
|
17
17
|
functionality](https://shaack.com/projekte/cm-chessboard/examples/extensions/arrows-extension.html)
|
|
18
|
+
- [Supports chess960 (Freestyle) castling input](https://shaack.com/projekte/cm-chessboard/examples/validate-moves-chess960.html)
|
|
18
19
|
|
|
19
20
|
## Extensions
|
|
20
21
|
|
|
21
|
-
The core of cm-chessboard is small, fast and reduced to the essentials. You can
|
|
22
|
+
The core of cm-chessboard is small, fast and reduced to the essentials. You can extend its functionality with extensions.
|
|
22
23
|
|
|
23
|
-
- [RightClickAnnotator](https://shaack.com/projekte/cm-chessboard/examples/extensions/right-click-annotator.html) ⇨ Uses [Markers Extension](https://shaack.com/projekte/cm-chessboard/examples/extensions/markers-extension.html) and [Arrows Extension](https://shaack.com/projekte/cm-chessboard/examples/extensions/arrows-extension.html)
|
|
24
|
+
- [RightClickAnnotator](https://shaack.com/projekte/cm-chessboard/examples/extensions/right-click-annotator.html) ⇨ Uses [Markers Extension](https://shaack.com/projekte/cm-chessboard/examples/extensions/markers-extension.html) and [Arrows Extension](https://shaack.com/projekte/cm-chessboard/examples/extensions/arrows-extension.html). Adds the handling of mouse events to draw them on the board.
|
|
24
25
|
- [Markers Extension](https://shaack.com/projekte/cm-chessboard/examples/extensions/markers-extension.html) ⇨ create markers on specific squares
|
|
25
26
|
- [Arrows Extension](https://shaack.com/projekte/cm-chessboard/examples/extensions/arrows-extension.html) ⇨ renders arrows on the chessboard
|
|
26
27
|
- [Accessibility Extension](https://shaack.com/projekte/cm-chessboard/examples/extensions/accessibility-extension.html) ⇨ makes the chessboard more accessible
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>cm-chessboard</title>
|
|
6
|
+
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1.0"/>
|
|
7
|
+
<link rel="stylesheet" href="styles/examples.css"/>
|
|
8
|
+
<link rel="stylesheet" href="../assets/chessboard.css"/>
|
|
9
|
+
<link rel="stylesheet" href="../assets/extensions/markers/markers.css"/>
|
|
10
|
+
<link rel="stylesheet" href="../assets/extensions/arrows/arrows.css"/>
|
|
11
|
+
<link rel="stylesheet" href="../assets/extensions/promotion-dialog/promotion-dialog.css"/>
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<h1><a href="../">cm-chessboard</a></h1>
|
|
15
|
+
<h2>Example: Input enabled with move validation and promotion dialog</h2>
|
|
16
|
+
<p>Input enabled for white. <a href="https://github.com/jhlywa/chess.js">chess.js</a> does the validation and answers
|
|
17
|
+
with random moves.</p>
|
|
18
|
+
<div class="board board-large" id="board"></div>
|
|
19
|
+
<div id="output"></div>
|
|
20
|
+
|
|
21
|
+
<script type="module">
|
|
22
|
+
import {INPUT_EVENT_TYPE, COLOR, Chessboard, BORDER_TYPE} from "../src/Chessboard.js"
|
|
23
|
+
import {MARKER_TYPE, Markers} from "../src/extensions/markers/Markers.js"
|
|
24
|
+
import {PROMOTION_DIALOG_RESULT_TYPE, PromotionDialog} from "../src/extensions/promotion-dialog/PromotionDialog.js"
|
|
25
|
+
import {Accessibility} from "../src/extensions/accessibility/Accessibility.js"
|
|
26
|
+
import {Chess} from "https://cdn.jsdelivr.net/npm/chess.mjs@2/src/Chess.js"
|
|
27
|
+
import {RightClickAnnotator} from "../src/extensions/right-click-annotator/RightClickAnnotator.js"
|
|
28
|
+
|
|
29
|
+
const chess = new Chess("bnrqnkrb/pppppppp/8/8/8/8/PPPPPPPP/BNRQNKRB w KQkq - 0 1", {chess960: true})
|
|
30
|
+
|
|
31
|
+
let seed = 71
|
|
32
|
+
|
|
33
|
+
function random() {
|
|
34
|
+
const x = Math.sin(seed++) * 10000
|
|
35
|
+
return x - Math.floor(x)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function makeEngineMove(chessboard) {
|
|
39
|
+
const possibleMoves = chess.moves({verbose: true})
|
|
40
|
+
if (possibleMoves.length > 0) {
|
|
41
|
+
const randomIndex = Math.floor(random() * possibleMoves.length)
|
|
42
|
+
const randomMove = possibleMoves[randomIndex]
|
|
43
|
+
setTimeout(() => { // smoother with 500ms delay
|
|
44
|
+
chess.move({from: randomMove.from, to: randomMove.to})
|
|
45
|
+
chessboard.setPosition(chess.fen(), true)
|
|
46
|
+
chessboard.enableMoveInput(inputHandler, COLOR.white)
|
|
47
|
+
}, 500)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function inputHandler(event) {
|
|
52
|
+
console.log("inputHandler", event)
|
|
53
|
+
if (event.type === INPUT_EVENT_TYPE.movingOverSquare) {
|
|
54
|
+
return // ignore this event
|
|
55
|
+
}
|
|
56
|
+
if (event.type !== INPUT_EVENT_TYPE.moveInputFinished) {
|
|
57
|
+
event.chessboard.removeLegalMovesMarkers()
|
|
58
|
+
}
|
|
59
|
+
if (event.type === INPUT_EVENT_TYPE.moveInputStarted) {
|
|
60
|
+
// mark legal moves
|
|
61
|
+
const moves = chess.moves({square: event.squareFrom, verbose: true})
|
|
62
|
+
event.chessboard.addLegalMovesMarkers(moves)
|
|
63
|
+
return moves.length > 0
|
|
64
|
+
} else if (event.type === INPUT_EVENT_TYPE.validateMoveInput) {
|
|
65
|
+
const move = {from: event.squareFrom, to: event.squareTo, promotion: event.promotion}
|
|
66
|
+
const result = chess.move(move)
|
|
67
|
+
if (result) {
|
|
68
|
+
event.chessboard.state.moveInputProcess.then(() => { // wait for the move input process has finished
|
|
69
|
+
event.chessboard.setPosition(chess.fen(), true).then(() => { // update position, maybe castled and wait for animation has finished
|
|
70
|
+
makeEngineMove(event.chessboard)
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
} else {
|
|
74
|
+
// promotion?
|
|
75
|
+
let possibleMoves = chess.moves({square: event.squareFrom, verbose: true})
|
|
76
|
+
for (const possibleMove of possibleMoves) {
|
|
77
|
+
if (possibleMove.promotion && possibleMove.to === event.squareTo) {
|
|
78
|
+
event.chessboard.showPromotionDialog(event.squareTo, COLOR.white, (result) => {
|
|
79
|
+
console.log("promotion result", result)
|
|
80
|
+
if (result.type === PROMOTION_DIALOG_RESULT_TYPE.pieceSelected) {
|
|
81
|
+
chess.move({
|
|
82
|
+
from: event.squareFrom,
|
|
83
|
+
to: event.squareTo,
|
|
84
|
+
promotion: result.piece.charAt(1)
|
|
85
|
+
})
|
|
86
|
+
event.chessboard.setPosition(chess.fen(), true)
|
|
87
|
+
makeEngineMove(event.chessboard)
|
|
88
|
+
} else {
|
|
89
|
+
// promotion canceled
|
|
90
|
+
event.chessboard.enableMoveInput(inputHandler, COLOR.white)
|
|
91
|
+
event.chessboard.setPosition(chess.fen(), true)
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
return true
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return result
|
|
99
|
+
} else if (event.type === INPUT_EVENT_TYPE.moveInputFinished) {
|
|
100
|
+
if (event.legalMove) {
|
|
101
|
+
event.chessboard.disableMoveInput()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const board = new Chessboard(document.getElementById("board"), {
|
|
107
|
+
position: chess.fen(),
|
|
108
|
+
assetsUrl: "../assets/",
|
|
109
|
+
style: {borderType: BORDER_TYPE.none, pieces: {file: "pieces/staunty.svg"}, animationDuration: 300},
|
|
110
|
+
orientation: COLOR.white,
|
|
111
|
+
extensions: [
|
|
112
|
+
{class: Markers, props: {autoMarkers: MARKER_TYPE.square}},
|
|
113
|
+
{class: RightClickAnnotator},
|
|
114
|
+
{class: PromotionDialog},
|
|
115
|
+
{class: Accessibility, props: {visuallyHidden: true}}
|
|
116
|
+
]
|
|
117
|
+
})
|
|
118
|
+
board.enableMoveInput(inputHandler, COLOR.white)
|
|
119
|
+
</script>
|
|
120
|
+
</body>
|
|
121
|
+
</html>
|
package/package.json
CHANGED
|
@@ -286,16 +286,20 @@ export class VisualMoveInput {
|
|
|
286
286
|
const startPieceName = this.chessboard.getPiece(this.fromSquare)
|
|
287
287
|
const startPieceColor = startPieceName ? startPieceName.substring(0, 1) : null
|
|
288
288
|
if (color && startPieceColor === pieceColor) {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
289
|
+
// added to allow chess960 castling
|
|
290
|
+
const result = this.validateMoveInputCallback(this.fromSquare, square)
|
|
291
|
+
if(!result) {
|
|
292
|
+
this.moveInputCanceledCallback(this.fromSquare, square, MOVE_CANCELED_REASON.clickedAnotherPiece)
|
|
293
|
+
if (this.moveInputStartedCallback(square)) {
|
|
294
|
+
this.setMoveInputState(MOVE_INPUT_STATE.pieceClickedThreshold, {
|
|
295
|
+
square: square,
|
|
296
|
+
piece: pieceName,
|
|
297
|
+
point: point,
|
|
298
|
+
type: e.type
|
|
299
|
+
})
|
|
300
|
+
} else {
|
|
301
|
+
this.setMoveInputState(MOVE_INPUT_STATE.reset)
|
|
302
|
+
}
|
|
299
303
|
}
|
|
300
304
|
} else {
|
|
301
305
|
this.setMoveInputState(MOVE_INPUT_STATE.moveDone, {square: square})
|