cm-chess 3.6.3 → 4.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/.claude/settings.local.json +11 -0
- package/README.md +235 -21
- package/package.json +5 -5
- package/test/TestChess.js +30 -1
- package/test/TestChess960.js +11 -0
package/README.md
CHANGED
|
@@ -1,38 +1,252 @@
|
|
|
1
|
-
# cm-chess
|
|
1
|
+
# cm-chess
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
A chess validation library inspired by [chess.js](https://github.com/jhlywa/chess.js), with first-class support for:
|
|
4
|
+
|
|
5
|
+
- **Variations** — full tree-structured move history (not just a linear list)
|
|
6
|
+
- **PGN import/export** — header tags, NAGs, annotations and comments
|
|
7
|
+
- **Chess960 (Fischer Random)** — including proper O-O / O-O-O castling
|
|
8
|
+
- **Sloppy SAN** — lenient parsing of common notation mistakes
|
|
9
|
+
- **Observers** — subscribe to legal/illegal/undo events
|
|
10
|
+
|
|
11
|
+
It powers the online chess server [chessmail.de](https://www.chessmail.de).
|
|
6
12
|
|
|
7
13
|
## Install
|
|
8
14
|
|
|
9
|
-
|
|
15
|
+
```bash
|
|
16
|
+
npm install cm-chess
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
import { Chess, COLOR, GAME_VARIANT } from "cm-chess"
|
|
23
|
+
|
|
24
|
+
const chess = new Chess()
|
|
25
|
+
chess.move("e4")
|
|
26
|
+
chess.move("e5")
|
|
27
|
+
chess.move("Nf3")
|
|
28
|
+
|
|
29
|
+
console.log(chess.fen()) // position after last move
|
|
30
|
+
console.log(chess.turn()) // "b"
|
|
31
|
+
console.log(chess.history()) // array of move nodes
|
|
32
|
+
console.log(chess.renderPgn()) // PGN string
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Run the tests
|
|
36
|
+
|
|
37
|
+
A hosted runner is available at **<https://shaack.com/projekte/cm-chess/test/>**.
|
|
38
|
+
Locally, open [`test/index.html`](test/index.html) in a browser.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Constructor
|
|
43
|
+
|
|
44
|
+
`new Chess(props)` — `props` is an optional object:
|
|
45
|
+
|
|
46
|
+
| Option | Type | Default | Description |
|
|
47
|
+
|---------------|---------------------------|---------------|----------------------------------------------------------------|
|
|
48
|
+
| `fen` | `string` | start FEN | Load from a FEN. |
|
|
49
|
+
| `pgn` | `string` | — | Load from a PGN (overrides `fen`). |
|
|
50
|
+
| `gameVariant` | `"standard"` / `"chess960"` | `"standard"` | Variant. Required for Chess960 castling to work. |
|
|
51
|
+
| `sloppy` | `boolean` | `true` | Lenient SAN parsing (e.g. `Rc8c7`, `Rc8-c7`). |
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
new Chess()
|
|
55
|
+
new Chess({ fen: "4k3/pppppppp/8/8/8/8/PPPPPPPP/4K3 w - - 0 1" })
|
|
56
|
+
new Chess({ pgn: "1. e4 e5 2. Nf3" })
|
|
57
|
+
new Chess({ gameVariant: GAME_VARIANT.chess960 })
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Making moves
|
|
63
|
+
|
|
64
|
+
### `move(san, previousMove?, sloppy?)`
|
|
65
|
+
|
|
66
|
+
Apply a move in SAN. Returns the created move node, or `null` if illegal.
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
chess.move("e4") // simple move
|
|
70
|
+
chess.move("gxh8=Q") // promotion
|
|
71
|
+
chess.move("dxc6") // en passant (flags include 'e')
|
|
72
|
+
chess.move("O-O") // castling, letter O (not zero)
|
|
73
|
+
chess.move("Nbd7", previousMove) // add a variation branching from previousMove
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### `validateMove(san, previousMove?, sloppy?)`
|
|
77
|
+
|
|
78
|
+
Check whether a move would be legal without making it. Returns a move object or `null`.
|
|
79
|
+
|
|
80
|
+
### `moves(options?, move?)`
|
|
81
|
+
|
|
82
|
+
Return all legal moves from a given position (defaults to the last move).
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
chess.moves() // all legal SAN strings
|
|
86
|
+
chess.moves({ square: "e2" }) // only moves from e2
|
|
87
|
+
chess.moves({ piece: "n" }) // only knight moves
|
|
88
|
+
chess.moves({ verbose: true }) // detailed move objects
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### `undo(move?)`
|
|
92
|
+
|
|
93
|
+
Remove a move and everything after it from its variation. Defaults to the last move.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Inspecting state
|
|
98
|
+
|
|
99
|
+
| Method | Returns |
|
|
100
|
+
|---------------------------|---------------------------------------------------------------|
|
|
101
|
+
| `fen(move?)` | FEN of the given move, or the last move |
|
|
102
|
+
| `setUpFen()` | Starting FEN (from PGN header or default start) |
|
|
103
|
+
| `turn(move?)` | `"w"` or `"b"` |
|
|
104
|
+
| `piece(square, move?)` | `{ type, color }` or `null` |
|
|
105
|
+
| `pieces(type?, color?, move?)` | array of pieces with `square`, `type`, `color` |
|
|
106
|
+
| `history()` | array of move nodes (main line) |
|
|
107
|
+
| `lastMove()` | last move node or `null` |
|
|
108
|
+
| `plyCount()` | number of plies played |
|
|
109
|
+
| `fenOfPly(n)` | FEN after the n-th ply (`fenOfPly(0)` returns the setup FEN) |
|
|
110
|
+
| `header()` | map of PGN header tags |
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Game status
|
|
10
115
|
|
|
11
|
-
|
|
116
|
+
All accept an optional `move` argument and default to the last move.
|
|
12
117
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
118
|
+
| Method | True when… |
|
|
119
|
+
|------------------------------|------------------------------------------------|
|
|
120
|
+
| `gameOver(move?)` | the game has ended |
|
|
121
|
+
| `inCheck(move?)` | the side to move is in check |
|
|
122
|
+
| `inCheckmate(move?)` | the side to move is checkmated |
|
|
123
|
+
| `inStalemate(move?)` | the position is stalemate |
|
|
124
|
+
| `inDraw(move?)` | the game is drawn (any reason) |
|
|
125
|
+
| `insufficientMaterial(move?)`| insufficient material |
|
|
126
|
+
| `inThreefoldRepetition(move?)`| threefold repetition |
|
|
17
127
|
|
|
18
|
-
|
|
128
|
+
---
|
|
19
129
|
|
|
20
|
-
|
|
130
|
+
## Loading
|
|
21
131
|
|
|
22
|
-
|
|
23
|
-
header and history, therefore you have also the full API of **cm-pgn**.
|
|
132
|
+
### `load(fen)`
|
|
24
133
|
|
|
25
|
-
|
|
134
|
+
Replace the game state with the given FEN. Throws on an invalid FEN.
|
|
135
|
+
|
|
136
|
+
### `loadPgn(pgn, sloppy?)`
|
|
137
|
+
|
|
138
|
+
Replace the game state from a PGN string, including header, variations, NAGs and comments.
|
|
139
|
+
If the PGN's `[Variant "Chess960"]` tag is present, `gameVariant` is set automatically.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Exporting PGN
|
|
144
|
+
|
|
145
|
+
### `renderPgn(renderHeader = true, renderComments = true, renderNags = true)`
|
|
146
|
+
|
|
147
|
+
Return a PGN string for the current game.
|
|
148
|
+
|
|
149
|
+
```js
|
|
150
|
+
chess.renderPgn() // full PGN
|
|
151
|
+
chess.renderPgn(false) // no header
|
|
152
|
+
chess.renderPgn(true, false) // no comments
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Variations
|
|
158
|
+
|
|
159
|
+
Move history is a tree. Each move node has `next`, `previous`, `variations` and `variation`
|
|
160
|
+
references, so you can walk the main line and any side lines.
|
|
161
|
+
|
|
162
|
+
```js
|
|
163
|
+
const first = chess.history()[0]
|
|
164
|
+
console.log(first.san) // "e4"
|
|
165
|
+
console.log(first.next.san) // "e5"
|
|
166
|
+
|
|
167
|
+
// Start a side variation from `first`:
|
|
168
|
+
chess.move("e5", first) // becomes the main continuation of `first`
|
|
169
|
+
chess.move("c5", first) // creates a Sicilian variation off `first`
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Each move node also carries computed fields: `fen`, `uci`, `ply`, `color`, `flags`,
|
|
173
|
+
`piece`, `captured`, `promotion`, `inCheck`, `inCheckmate`, `inStalemate`,
|
|
174
|
+
`inDraw`, `insufficientMaterial`, `inThreefoldRepetition`, `gameOver`.
|
|
175
|
+
|
|
176
|
+
See [cm-pgn](https://github.com/shaack/cm-pgn) for the full history/header API.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Events
|
|
181
|
+
|
|
182
|
+
Subscribe to moves with `addObserver(callback)`:
|
|
183
|
+
|
|
184
|
+
```js
|
|
185
|
+
import { EVENT_TYPE } from "cm-chess"
|
|
186
|
+
|
|
187
|
+
chess.addObserver((event) => {
|
|
188
|
+
switch (event.type) {
|
|
189
|
+
case EVENT_TYPE.initialized: /* fen/pgn loaded */; break
|
|
190
|
+
case EVENT_TYPE.legalMove: console.log("played", event.move.san); break
|
|
191
|
+
case EVENT_TYPE.illegalMove: console.warn("rejected", event.move); break
|
|
192
|
+
case EVENT_TYPE.undoMove: console.log("undid", event.move.san); break
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Chess960 (Fischer Random)
|
|
200
|
+
|
|
201
|
+
Pass `gameVariant: GAME_VARIANT.chess960` to enable 960-style castling.
|
|
202
|
+
Use `O-O` / `O-O-O` (letter O, not zero) — cm-chess resolves them to the
|
|
203
|
+
correct king/rook destinations for the given FEN.
|
|
204
|
+
|
|
205
|
+
```js
|
|
206
|
+
import { Chess, GAME_VARIANT } from "cm-chess"
|
|
207
|
+
|
|
208
|
+
const chess = new Chess({ gameVariant: GAME_VARIANT.chess960 })
|
|
209
|
+
chess.load("nrkbrnbq/pppppppp/8/8/8/8/PPPPPPPP/NRKBRNBQ w KQkq - 0 1")
|
|
210
|
+
chess.move("f4")
|
|
211
|
+
// …
|
|
212
|
+
chess.move("O-O-O")
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
When loading a PGN containing `[Variant "Chess960"]`, the variant is detected
|
|
216
|
+
automatically.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Exports
|
|
221
|
+
|
|
222
|
+
```js
|
|
223
|
+
import {
|
|
224
|
+
Chess,
|
|
225
|
+
COLOR, // { white: "w", black: "b" }
|
|
226
|
+
PIECES, // { p, n, b, r, q, k } with { name, value }
|
|
227
|
+
FEN, // { empty, start }
|
|
228
|
+
EVENT_TYPE, // { initialized, legalMove, illegalMove, undoMove }
|
|
229
|
+
GAME_VARIANT // { standard, chess960 }
|
|
230
|
+
} from "cm-chess"
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
26
234
|
|
|
27
235
|
## Examples
|
|
28
236
|
|
|
29
|
-
|
|
30
|
-
|
|
237
|
+
- [`test/TestChess.js`](test/TestChess.js) — core API, loading, variations, events
|
|
238
|
+
- [`test/TestChess960.js`](test/TestChess960.js) — Chess960 castling and PGN
|
|
239
|
+
- [`test/TestFen.js`](test/TestFen.js) — FEN handling
|
|
240
|
+
- Tests of [cm-pgn](https://github.com/shaack/cm-pgn/tree/master/test) for the underlying history/header API
|
|
241
|
+
|
|
242
|
+
---
|
|
31
243
|
|
|
32
|
-
|
|
33
|
-
https://github.com/shaack/cm-pgn/tree/master/test
|
|
244
|
+
## Related
|
|
34
245
|
|
|
35
|
-
|
|
246
|
+
- [cm-pgn](https://github.com/shaack/cm-pgn) — PGN parser and history tree (used internally)
|
|
247
|
+
- [chess.mjs](https://www.npmjs.com/package/chess.mjs) — ES-module fork of chess.js (used for move validation)
|
|
248
|
+
- [cm-chessboard](https://github.com/shaack/cm-chessboard) — companion chessboard component
|
|
36
249
|
|
|
37
|
-
|
|
250
|
+
## License
|
|
38
251
|
|
|
252
|
+
MIT — see [LICENSE](LICENSE).
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cm-chess",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"description": "It's like chess.js, but in ES6 and can handle variations (tree-structured move history)",
|
|
5
5
|
"main": "./src/Chess.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"test": "
|
|
8
|
+
"test": "teevi"
|
|
9
9
|
},
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
},
|
|
25
25
|
"homepage": "https://github.com/shaack/cm-chess#readme",
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"chess.mjs": "^2.
|
|
28
|
-
"cm-pgn": "
|
|
27
|
+
"chess.mjs": "^2.4.1",
|
|
28
|
+
"cm-pgn": "^5.0.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"teevi": "^2.
|
|
31
|
+
"teevi": "^2.7.0"
|
|
32
32
|
}
|
|
33
33
|
}
|
package/test/TestChess.js
CHANGED
|
@@ -67,7 +67,9 @@ describe("Chess", function () {
|
|
|
67
67
|
assert.equal(firstMove.color, "w")
|
|
68
68
|
assert.equal(firstMove.san, "e4")
|
|
69
69
|
assert.equal(firstMove.fen, "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1")
|
|
70
|
-
|
|
70
|
+
// cm-pgn 5: comments after a move are an array in `comments`
|
|
71
|
+
assert.equal(chess.history()[19].comments.length, 1)
|
|
72
|
+
assert.equal(chess.history()[19].comments[0], "Kasparov schüttelt kurz den Kopf")
|
|
71
73
|
})
|
|
72
74
|
|
|
73
75
|
it("should load a pgn with SetUp", () => {
|
|
@@ -289,6 +291,33 @@ Kc4 7. Qb4+ Kd5 8. Qc5#) 5. Bc5# 1-0`)
|
|
|
289
291
|
assert.true(result != null)
|
|
290
292
|
})
|
|
291
293
|
|
|
294
|
+
it("should promote a pawn via SAN", function () {
|
|
295
|
+
const chess = new Chess({fen: "rnbqkbnr/ppppppPp/8/8/8/8/PPPPPP1P/RNBQKBNR w KQkq - 0 1"})
|
|
296
|
+
const move = chess.move("gxh8=Q")
|
|
297
|
+
assert.true(move !== null)
|
|
298
|
+
assert.equal(move.promotion, "q")
|
|
299
|
+
assert.true(move.flags.includes("p"))
|
|
300
|
+
assert.equal(chess.piece("h8").type, "q")
|
|
301
|
+
assert.equal(chess.piece("h8").color, "w")
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it("should promote to a knight (underpromotion)", function () {
|
|
305
|
+
const chess = new Chess({fen: "4k3/P7/8/8/8/8/8/4K3 w - - 0 1"})
|
|
306
|
+
const move = chess.move("a8=N")
|
|
307
|
+
assert.true(move !== null)
|
|
308
|
+
assert.equal(move.promotion, "n")
|
|
309
|
+
assert.equal(chess.piece("a8").type, "n")
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it("should capture en passant", function () {
|
|
313
|
+
const chess = new Chess({fen: "rnbqkbnr/pp1ppppp/8/2pP4/8/8/PPP1PPPP/RNBQKBNR w KQkq c6 0 1"})
|
|
314
|
+
const move = chess.move("dxc6")
|
|
315
|
+
assert.true(move !== null)
|
|
316
|
+
assert.true(move.flags.includes("e"))
|
|
317
|
+
assert.equal(move.fen, "rnbqkbnr/pp1ppppp/2P5/8/8/8/PPP1PPPP/RNBQKBNR b KQkq - 0 1")
|
|
318
|
+
assert.equal(chess.piece("c5"), null)
|
|
319
|
+
})
|
|
320
|
+
|
|
292
321
|
it("should load a sloppy pgn", function () {
|
|
293
322
|
const pgn = `pgn [SetUp "1"]
|
|
294
323
|
[FEN "r1R2r1k/1R6/1P2B2p/4pPp1/4N1P1/7P/5P2/2R3K1 w - - 1 44"]
|
package/test/TestChess960.js
CHANGED
|
@@ -33,6 +33,17 @@ describe("Testing Chess 960", function () {
|
|
|
33
33
|
|
|
34
34
|
})
|
|
35
35
|
|
|
36
|
+
it("should do kingside castling (O-O) in chess960", function () {
|
|
37
|
+
const fen = "rnbqkbnr/pppppppp/8/8/8/3PP1BN/PPP2PPP/RNBQ1K1R w KQkq - 0 1"
|
|
38
|
+
const chess = new Chess({gameVariant: GAME_VARIANT.chess960})
|
|
39
|
+
chess.load(fen)
|
|
40
|
+
const move = chess.move("O-O")
|
|
41
|
+
assert.true(move !== null)
|
|
42
|
+
assert.equal(move.san, "O-O")
|
|
43
|
+
assert.equal(chess.piece("g1").type, "k")
|
|
44
|
+
assert.equal(chess.piece("f1").type, "r")
|
|
45
|
+
})
|
|
46
|
+
|
|
36
47
|
it("should load a chess960 game from a valid chess960 pgn", function () {
|
|
37
48
|
const pgn = `[Event "Freestyle Weissenhaus KO"]
|
|
38
49
|
[Site "Wangels GER"]
|