• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

brozeph / node-chess / 17656510336

11 Sep 2025 08:28PM UTC coverage: 95.659% (+0.4%) from 95.286%
17656510336

push

github

web-flow
Merge pull request #102 from brozeph/v1.3.1

V1.3.1

462 of 491 branches covered (94.09%)

52 of 54 new or added lines in 2 files covered. (96.3%)

2 existing lines in 2 files now uncovered.

1939 of 2027 relevant lines covered (95.66%)

42655.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.73
/src/algebraicGameClient.js
1
/* eslint sort-imports: 0 */
6✔
2
import { EventEmitter } from 'events';
3✔
3
import { Board } from './board.js';
3✔
4
import { Game } from './game.js';
3✔
5
import { GameValidation } from './gameValidation.js';
3✔
6
import { Piece } from './piece.js';
3✔
7
import { PieceType } from './piece.js';
3✔
8
import { SideType } from './piece.js';
3✔
9

3✔
10
// private methods
6✔
11
function getNotationPrefix (src, dest, movesForPiece) {
15,894✔
12
        let
15,894✔
13
                containsDest = (squares) => {
15,894✔
14
                        let n = 0;
31,788✔
15

15,894✔
16
                        for (; n < squares.length; n++) {
31,788✔
17
                                if (squares[n] === dest) {
107,784✔
18
                                        return true;
17,262✔
19
                                }
17,262✔
20
                        }
107,784✔
21

7,263✔
22
                        return false;
14,526✔
23
                },
15,894✔
24
                file = '',
15,894✔
25
                fileHash = {},
15,894✔
26
                i = 0,
15,894✔
27
                prefix = src.piece.notation,
15,894✔
28
                rank = 0,
15,894✔
29
                rankHash = {};
15,894✔
30

7,947✔
31
        for (; i < movesForPiece.length; i++) {
15,894✔
32
                if (containsDest(movesForPiece[i].squares)) {
31,788✔
33
                        file = movesForPiece[i].src.file;
17,262✔
34
                        rank = movesForPiece[i].src.rank;
17,262✔
35

8,631✔
36
                        fileHash[file] = (typeof fileHash[file] !== 'undefined' ? fileHash[file] + 1 : 1);
17,262✔
37
                        rankHash[rank] = (typeof rankHash[rank] !== 'undefined' ? rankHash[rank] + 1 : 1);
17,262✔
38
                }
17,262✔
39
        }
31,788✔
40

7,947✔
41
        if (Object.keys(fileHash).length > 1) {
15,894✔
42
                prefix += src.file;
1,344✔
43
        }
1,344✔
44

7,947✔
45
        if (Object.keys(rankHash).length > Object.keys(fileHash).length) {
15,894✔
46
                prefix += src.rank;
24✔
47
        }
24✔
48

7,947✔
49
        return prefix;
15,894✔
50
}
15,894✔
51

3✔
52
function getValidMovesByPieceType (pieceType, validMoves) {
28,668✔
53
        let
28,668✔
54
                byPiece = [],
28,668✔
55
                i = 0;
28,668✔
56

14,334✔
57
        for (; i < validMoves.length; i++) {
28,668✔
58
                if (validMoves[i].src.piece.type === pieceType) {
315,432✔
59
                        byPiece.push(validMoves[i]);
44,562✔
60
                }
44,562✔
61
        }
315,432✔
62

14,334✔
63
        return byPiece;
28,668✔
64
}
28,668✔
65

3✔
66
function notate (validMoves, gameClient) {
1,908✔
67
        let
1,908✔
68
                algebraicNotation = {},
1,908✔
69
                i = 0,
1,908✔
70
                isPromotion = false,
1,908✔
71
                movesForPiece = [],
1,908✔
72
                n = 0,
1,908✔
73
                p = null,
1,908✔
74
                prefix = '',
1,908✔
75
                sq = null,
1,908✔
76
                src = null,
1,908✔
77
                suffix = '';
1,908✔
78

954✔
79
        // iterate through each starting squares valid moves
1,908✔
80
        for (; i < validMoves.length; i++) {
1,908✔
81
                src = validMoves[i].src;
19,836✔
82
                p = src.piece;
19,836✔
83

9,918✔
84
                // iterate each potential move and build prefix and suffix for notation
19,836✔
85
                for (n = 0; n < validMoves[i].squares.length; n++) {
19,836✔
86
                        prefix = '';
52,884✔
87
                        sq = validMoves[i].squares[n];
52,884✔
88

26,442✔
89
                        // set suffix for notation
52,884✔
90
                        suffix = (sq.piece ? 'x' : '') + sq.file + sq.rank;
52,884✔
91

26,442✔
92
                        // check for potential promotion
52,884✔
93
                        /* eslint no-magic-numbers: 0 */
52,884✔
94
                        isPromotion =
52,884✔
95
                                (sq.rank === 8 || sq.rank === 1) &&
52,884✔
96
                                p.type === PieceType.Pawn;
52,884✔
97

26,442✔
98
                        // squares with pawns
52,884✔
99
                        if (sq.piece && p.type === PieceType.Pawn) {
52,884✔
100
                                prefix = src.file;
432✔
101
                        }
432✔
102

26,442✔
103
                        // en passant
52,884✔
104
                        // fix for #53
52,884✔
105
                        if (p.type === PieceType.Pawn &&
52,884✔
106
                                src.file !== sq.file &&
52,884✔
107
                                !sq.piece) {
52,884✔
108
                                prefix = [src.file, 'x'].join('');
18✔
109
                        }
18✔
110

26,442✔
111
                        // squares with Bishop, Knight, Queen or Rook pieces
52,884✔
112
                        if (p.type === PieceType.Bishop ||
52,884✔
113
                                p.type === PieceType.Knight ||
52,884✔
114
                                p.type === PieceType.Queen ||
52,884✔
115
                                p.type === PieceType.Rook) {
52,884✔
116
                                // if there is more than 1 of the specified piece on the board,
28,668✔
117
                                // can more than 1 land on the specified square?
28,668✔
118
                                movesForPiece = getValidMovesByPieceType(p.type, validMoves);
28,668✔
119
                                if (movesForPiece.length > 1) {
28,668✔
120
                                        prefix = getNotationPrefix(src, sq, movesForPiece);
15,894✔
121
                                } else {
28,668✔
122
                                        prefix = src.piece.notation;
12,774✔
123
                                }
12,774✔
124
                        }
28,668✔
125

26,442✔
126
                        // squares with a King piece
52,884✔
127
                        if (p.type === PieceType.King) {
52,884✔
128
                                // look for castle left and castle right
2,472✔
129
                                if (src.file === 'e' && sq.file === 'g') {
2,472✔
130
                                        // fix for issue #13 - if PGN is specified should be letters, not numbers
180✔
131
                                        prefix = gameClient.PGN ? 'O-O' : '0-0';
180✔
132
                                        suffix = '';
180✔
133
                                } else if (src.file === 'e' && sq.file === 'c') {
2,472✔
134
                                        // fix for issue #13 - if PGN is specified should be letters, not numbers
90✔
135
                                        prefix = gameClient.PGN ? 'O-O-O' : '0-0-0';
90✔
136
                                        suffix = '';
90✔
137
                                } else {
2,292✔
138
                                        prefix = src.piece.notation;
2,202✔
139
                                }
2,202✔
140
                        }
2,472✔
141

26,442✔
142
                        // set the notation
52,884✔
143
                        if (isPromotion) {
52,884✔
144
                                // Rook promotion
84✔
145
                                algebraicNotation[prefix + suffix + 'R'] = {
84✔
146
                                        dest : sq,
84✔
147
                                        src
84✔
148
                                };
84✔
149

42✔
150
                                // Knight promotion
84✔
151
                                algebraicNotation[prefix + suffix + 'N'] = {
84✔
152
                                        dest : sq,
84✔
153
                                        src
84✔
154
                                };
84✔
155

42✔
156
                                // Bishop promotion
84✔
157
                                algebraicNotation[prefix + suffix + 'B'] = {
84✔
158
                                        dest : sq,
84✔
159
                                        src
84✔
160
                                };
84✔
161

42✔
162
                                // Queen promotion
84✔
163
                                algebraicNotation[prefix + suffix + 'Q'] = {
84✔
164
                                        dest : sq,
84✔
165
                                        src
84✔
166
                                };
84✔
167
                        } else {
52,884✔
168
                                algebraicNotation[prefix + suffix] = {
52,800✔
169
                                        dest : sq,
52,800✔
170
                                        src
52,800✔
171
                                };
52,800✔
172
                        }
52,800✔
173
                }
52,884✔
174
        }
19,836✔
175

954✔
176
        return algebraicNotation;
1,908✔
177
}
1,908✔
178

3✔
179
function parseNotation (notation) {
24✔
180
        let
24✔
181
                captureRegex = /^[a-h]x[a-h][1-8]$/,
24✔
182
                parseDest = '';
24✔
183

12✔
184
        // try and parse the notation
24✔
185
        parseDest = notation.substring(notation.length - 2);
24✔
186

12✔
187
        if (notation.length > 2) {
24✔
188
                // check for preceding pawn capture style notation (i.e. a-h x)
12✔
189
                if (captureRegex.test(notation)) {
12!
190
                        return parseDest;
×
191
                }
×
192

6✔
193
                return notation.charAt(0) + parseDest;
12✔
194
        }
12✔
195

6✔
196
        return '';
12✔
197
}
12✔
198

3✔
199
function updateGameClient (gameClient) {
1,908✔
200
        gameClient.validation.start((err, result) => {
1,908✔
201
                if (err) {
1,908!
202
                        throw new Error(err);
×
203
                }
×
204

954✔
205
                gameClient.isCheck = result.isCheck;
1,908✔
206
                gameClient.isCheckmate = result.isCheckmate;
1,908✔
207
                gameClient.isRepetition = result.isRepetition;
1,908✔
208
                gameClient.isStalemate = result.isStalemate;
1,908✔
209
                gameClient.notatedMoves = notate(result.validMoves, gameClient);
1,908✔
210
                gameClient.validMoves = result.validMoves;
1,908✔
211
        });
1,908✔
212
}
1,908✔
213

3✔
214
export class AlgebraicGameClient extends EventEmitter {
6✔
215
        constructor (game, opts) {
6✔
216
                super();
246✔
217

123✔
218
                this.game = game;
246✔
219
                this.isCheck = false;
246✔
220
                this.isCheckmate = false;
246✔
221
                this.isRepetition = false;
246✔
222
                this.isStalemate = false;
246✔
223
                this.notatedMoves = {};
246✔
224
                // for issue #13, adding options allowing consumers to specify
246✔
225
                // PGN (Portable Game Notation)... essentially, this makes castle moves
246✔
226
                // appear as capital letter O rather than the number 0
246✔
227
                this.PGN = (opts && typeof opts.PGN === 'boolean') ? opts.PGN : false;
246✔
228
                this.validMoves = [];
246✔
229
                this.validation = GameValidation.create(this.game);
246✔
230

123✔
231
                // bubble the game and board events
246✔
232
                ['check', 'checkmate'].forEach((ev) => {
246✔
233
                        this.game.on(ev, (data) => this.emit(ev, data));
492✔
234
                });
246✔
235

123✔
236
                ['capture', 'castle', 'enPassant', 'move', 'promote', 'undo'].forEach((ev) => {
246✔
237
                        this.game.board.on(ev, (data) => this.emit(ev, data));
1,476✔
238
                });
246✔
239

123✔
240
                let self = this;
246✔
241
                this.on('undo', () => {
246✔
242
                        // force an update
12✔
243
                        self.getStatus(true);
12✔
244
                });
246✔
245
        }
246✔
246

3✔
247
        static create (opts) {
6✔
248
                let
240✔
249
                        game = Game.create(),
240✔
250
                        gameClient = new AlgebraicGameClient(game, opts);
240✔
251

120✔
252
                updateGameClient(gameClient);
240✔
253

120✔
254
                return gameClient;
240✔
255
        }
240✔
256

3✔
257
        static fromFEN (fen, opts) {
6✔
258
                if (!fen || typeof fen !== 'string') {
6!
NEW
259
                        throw new Error('FEN must be a non-empty string');
×
NEW
260
                }
×
261

3✔
262
                // create a standard game so listeners/history are wired
6✔
263
                let 
6✔
264
                        game = Game.create(),
6✔
265
                        loadedBoard = Board.load(fen);
6✔
266

3✔
267
                // copy piece placement from loaded board to preserve board indexing and listeners
6✔
268
                for (let i = 0; i < game.board.squares.length; i++) {
6✔
269
                        game.board.squares[i].piece = null;
384✔
270
                }
384✔
271

3✔
272
                for (let i = 0; i < loadedBoard.squares.length; i++) {
6✔
273
                        let sq = loadedBoard.squares[i];
384✔
274
                        if (sq.piece) {
384✔
275
                                let target = game.board.getSquare(sq.file, sq.rank);
192✔
276
                                target.piece = sq.piece;
192✔
277
                        }
192✔
278
                }
384✔
279

3✔
280
                game.board.lastMovedPiece = null;
6✔
281

3✔
282
                // derive side to move from FEN (default to White if missing)
6✔
283
                let parts = fen.split(' ');
6✔
284
                let active = parts[1] || 'w';
6!
285
                let baseSide = active === 'b' ? SideType.Black : SideType.White;
6!
286

3✔
287
                // override getCurrentSide to honor FEN and alternate thereafter
6✔
288
                let whiteFirst = baseSide === SideType.White;
6✔
289

3✔
290
                /* eslint no-param-reassign: 0 */
6✔
291
                game.getCurrentSide = function getCurrentSideAfterFENLoad () {
6✔
292
                        return (this.moveHistory.length % 2 === 0) ?
54✔
293
                                (whiteFirst ? SideType.White : SideType.Black) :
54!
294
                                (whiteFirst ? SideType.Black : SideType.White);
54!
295
                };
6✔
296

3✔
297
                const gameClient = new AlgebraicGameClient(game, opts);
6✔
298
                updateGameClient(gameClient);
6✔
299

3✔
300
                return gameClient;
6✔
301
        }
6✔
302

3✔
303
        getStatus (forceUpdate) {
6✔
304
                if (forceUpdate) {
210✔
305
                        updateGameClient(this);
114✔
306
                }
114✔
307

105✔
308
                return {
210✔
309
                        board : this.game.board,
210✔
310
                        isCheck : this.isCheck,
210✔
311
                        isCheckmate : this.isCheckmate,
210✔
312
                        isRepetition : this.isRepetition,
210✔
313
                        isStalemate : this.isStalemate,
210✔
314
                        notatedMoves : this.notatedMoves
210✔
315
                };
210✔
316
        }
210✔
317

3✔
318
        getFen () {
6✔
319
                return this.game.board.getFen();
12✔
320
        }
12✔
321

3✔
322
        move (notation, isFuzzy) {
6✔
323
                let
1,596✔
324
                        move = null,
1,596✔
325
                        notationRegex = /^[BKQNR]?[a-h]?[1-8]?[x-]?[a-h][1-8][+#]?$/,
1,596✔
326
                        p = null,
1,596✔
327
                        promo = '',
1,596✔
328
                        side = this.game.getCurrentSide();
1,596✔
329

798✔
330
                if (notation && typeof notation === 'string') {
1,596✔
331
                        // clean notation of extra or alternate chars
1,584✔
332
                        notation = notation
1,584✔
333
                                .replace(/\!/g, '')
1,584✔
334
                                .replace(/\+/g, '')
1,584✔
335
                                .replace(/\#/g, '')
1,584✔
336
                                .replace(/\=/g, '')
1,584✔
337
                                .replace(/\\/g, '');
1,584✔
338

792✔
339
                        // fix for issue #13 - if PGN is specified, should be letters not numbers
1,584✔
340
                        if (this.PGN) {
1,584✔
341
                                notation = notation.replace(/0/g, 'O');
24✔
342
                        } else {
1,584✔
343
                                notation = notation.replace(/O/g, '0');
1,560✔
344
                        }
1,560✔
345

792✔
346
                        // check for pawn promotion
1,584✔
347
                        if (notation.charAt(notation.length - 1).match(/[BNQR]/)) {
1,584✔
348
                                promo = notation.charAt(notation.length - 1);
18✔
349
                        }
18✔
350

792✔
351
                        // use it directly or attempt to parse it if not found
1,584✔
352
                        if (this.notatedMoves[notation]) {
1,584✔
353
                                move = this.game.board.move(
1,548✔
354
                                        this.notatedMoves[notation].src,
1,548✔
355
                                        this.notatedMoves[notation].dest,
1,548✔
356
                                        notation);
1,548✔
357
                        } else if (notation.match(notationRegex) && notation.length > 1 && !isFuzzy) {
1,584✔
358
                                return this.move(parseNotation(notation), true);
24✔
359
                        } else if (isFuzzy) {
36✔
360
                                throw new Error(`Invalid move (${notation})`);
6✔
361
                        }
6✔
362

777✔
363
                        if (move) {
1,584✔
364
                                // apply pawn promotion
1,548✔
365
                                if (promo) {
1,548✔
366
                                        switch (promo) {
18✔
367
                                                case 'B':
18!
368
                                                        p = Piece.createBishop(side);
×
369
                                                        break;
×
370
                                                case 'N':
18!
371
                                                        p = Piece.createKnight(side);
×
372
                                                        break;
×
373
                                                case 'Q':
18!
374
                                                        p = Piece.createQueen(side);
×
375
                                                        break;
×
376
                                                case 'R':
18✔
377
                                                        p = Piece.createRook(side);
18✔
378
                                                        break;
18✔
379
                                                default:
18!
380
                                                        p = Piece.createPawn(side);
×
381
                                        }
18✔
382

9✔
383
                                        if (p) {
18✔
384
                                                this.game.board.promote(move.move.postSquare, p);
18✔
385
                                        }
18✔
386
                                }
18✔
387

774✔
388
                                updateGameClient(this);
1,548✔
389

774✔
390
                                return move;
1,548✔
391
                        }
1,548✔
392
                }
1,584✔
393

9✔
394
                throw new Error(`Notation is invalid (${notation})`);
18✔
395
        }
18✔
396
}
6✔
397

3✔
398
export default { AlgebraicGameClient };
6✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc