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

brozeph / node-chess / 17655824768

11 Sep 2025 07:59PM UTC coverage: 95.304% (+0.02%) from 95.286%
17655824768

Pull #102

github

web-flow
Merge a4a32dc5d into ad520e6eb
Pull Request #102: V1.3.1

448 of 475 branches covered (94.32%)

49 of 51 new or added lines in 1 file covered. (96.08%)

1928 of 2023 relevant lines covered (95.3%)

47008.99 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';
6✔
3
import { Board } from './board.js';
6✔
4
import { Game } from './game.js';
6✔
5
import { GameValidation } from './gameValidation.js';
6✔
6
import { Piece } from './piece.js';
6✔
7
import { PieceType } from './piece.js';
6✔
8
import { SideType } from './piece.js';
6✔
9

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

31,692✔
16
                        for (; n < squares.length; n++) {
31,692✔
17
                                if (squares[n] === dest) {
107,616✔
18
                                        return true;
17,214✔
19
                                }
17,214✔
20
                        }
107,616✔
21

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

15,846✔
31
        for (; i < movesForPiece.length; i++) {
15,846✔
32
                if (containsDest(movesForPiece[i].squares)) {
31,692✔
33
                        file = movesForPiece[i].src.file;
17,214✔
34
                        rank = movesForPiece[i].src.rank;
17,214✔
35

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

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

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

15,846✔
49
        return prefix;
15,846✔
50
}
15,846✔
51

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

28,620✔
57
        for (; i < validMoves.length; i++) {
28,620✔
58
                if (validMoves[i].src.piece.type === pieceType) {
314,952✔
59
                        byPiece.push(validMoves[i]);
44,466✔
60
                }
44,466✔
61
        }
314,952✔
62

28,620✔
63
        return byPiece;
28,620✔
64
}
28,620✔
65

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

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

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

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

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

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

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

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

52,644✔
126
                        // squares with a King piece
52,644✔
127
                        if (p.type === PieceType.King) {
52,644✔
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

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

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

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

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

1,896✔
176
        return algebraicNotation;
1,896✔
177
}
1,896✔
178

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

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

24✔
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

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

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

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

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

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

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

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

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

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

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

228✔
252
                updateGameClient(gameClient);
228✔
253

228✔
254
                return gameClient;
228✔
255
        }
228✔
256

6✔
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

6✔
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

6✔
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

6✔
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

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

6✔
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

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

6✔
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

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

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

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

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

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

6✔
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

1,596✔
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

1,584✔
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

1,584✔
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

1,584✔
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

1,554✔
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

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

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

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

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

6✔
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