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

brozeph / node-chess / 17660353757

12 Sep 2025 12:00AM UTC coverage: 95.183% (-0.5%) from 95.659%
17660353757

push

github

web-flow
Merge pull request #103 from brozeph/v1.4.0

added support for UCI - addressing #78

506 of 546 branches covered (92.67%)

216 of 237 new or added lines in 3 files covered. (91.14%)

2154 of 2263 relevant lines covered (95.18%)

39260.14 hits per line

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

90.99
/src/uciGameClient.js
1
/* eslint sort-imports: 0 */
6✔
2
import { EventEmitter } from 'events';
3✔
3
import { Game } from './game.js';
3✔
4
import { GameValidation } from './gameValidation.js';
3✔
5
import { Piece } from './piece.js';
3✔
6
import { PieceType } from './piece.js';
3✔
7

3✔
8
// private helpers
6✔
9

3✔
10
function parseUCI(uci) {
54✔
11
  if (typeof uci !== 'string') {
54!
NEW
12
    return null;
×
NEW
13
  }
×
14

27✔
15
  // UCI format: e2e4, e7e8q (promotion), case-insensitive for promo
54✔
16
  let
54✔
17
    formatRegex = /^([a-h][1-8])([a-h][1-8])([qrbnQRBN])?$/,
54✔
18
    uciMove = uci.trim().match(formatRegex);
54✔
19

27✔
20
  if (!uciMove) {
54✔
21
    return null;
12✔
22
  }
12✔
23

21✔
24
  let
42✔
25
    dest = { file: uciMove[2][0], rank: Number(uciMove[2][1]) },
42✔
26
    promo = uciMove[3] ? uciMove[3].toUpperCase() : '',
54✔
27
    src = { file: uciMove[1][0], rank: Number(uciMove[1][1]) };
54✔
28

27✔
29
  return { dest, promo, src };
54✔
30
}
54✔
31

3✔
32
function updateGameClient(gameClient) {
90✔
33
  return gameClient.validation.start((err, result) => {
90✔
34
    if (err) {
90!
NEW
35
      throw new Error(err);
×
NEW
36
    }
×
37

45✔
38
    gameClient.isCheck = result.isCheck;
90✔
39
    gameClient.isCheckmate = result.isCheckmate;
90✔
40
    gameClient.isRepetition = result.isRepetition;
90✔
41
    gameClient.isStalemate = result.isStalemate;
90✔
42
    gameClient.validMoves = result.validMoves;
90✔
43
    gameClient.uciMoves = notateUCI(result.validMoves);
90✔
44
  });
90✔
45
}
90✔
46

3✔
47
function notateUCI(validMoves) {
90✔
48
  let 
90✔
49
    i = 0,
90✔
50
    isPromotion = false,
90✔
51
    notation = {};
90✔
52

45✔
53
  // iterate through all valid moves and create UCI notation
90✔
54
  for (; i < validMoves.length; i++) {
90✔
55
    let 
888✔
56
      p = validMoves[i].src.piece,
888✔
57
      src = validMoves[i].src;
888✔
58

444✔
59
    // reset inner index for each piece's move list
888✔
60
    for (let n = 0; n < validMoves[i].squares.length; n++) {
888✔
61
      // get the destination square for this move
1,842✔
62
      let sq = validMoves[i].squares[n];
1,842✔
63

921✔
64
      // base notation
1,842✔
65
      let base = `${src.file}${src.rank}${sq.file}${sq.rank}`;
1,842✔
66

921✔
67
      // check for potential promotion
1,842✔
68
      /* eslint no-magic-numbers: 0 */
1,842✔
69
      isPromotion = 
1,842✔
70
        (sq.rank === 8 || sq.rank === 1) && 
1,842✔
71
        p.type === PieceType.Pawn;
1,842✔
72
      
1,842✔
73
      if (isPromotion) {
1,842✔
74
        // add all promotion options
6✔
75
        ['q', 'r', 'b', 'n'].forEach((promo) => {
6✔
76
          notation[`${base}${promo}`] = {
24✔
77
            dest: sq,
24✔
78
            src
24✔
79
          };
24✔
80
        });
6✔
81

3✔
82
        continue
6✔
83
      }
6✔
84

918✔
85
      // regular move
1,836✔
86
      notation[base] = {
1,836✔
87
        dest: sq,
1,836✔
88
        src
1,836✔
89
      };
1,836✔
90
    }
1,836✔
91
  }
1,842✔
92

45✔
93
  return notation;
90✔
94
}
90✔
95

3✔
96
export class UCIGameClient extends EventEmitter {
6✔
97
  constructor(game) {
6✔
98
    super();
36✔
99

18✔
100
    this.game = game;
36✔
101
    this.isCheck = false;
36✔
102
    this.isCheckmate = false;
36✔
103
    this.isRepetition = false;
36✔
104
    this.isStalemate = false;
36✔
105
    this.uciMoves = {};
36✔
106
    this.validMoves = [];
36✔
107
    this.validation = GameValidation.create(this.game);
36✔
108

18✔
109
    // bubble the game and board events
36✔
110
    ['check', 'checkmate'].forEach((ev) => {
36✔
111
      this.game.on(ev, (data) => this.emit(ev, data));
72✔
112
    });
36✔
113

18✔
114
    ['capture', 'castle', 'enPassant', 'move', 'promote', 'undo'].forEach((ev) => {
36✔
115
      this.game.board.on(ev, (data) => this.emit(ev, data));
216✔
116
    });
36✔
117

18✔
118
    const self = this;
36✔
119
    this.on('undo', () => {
36✔
NEW
120
      // force an update
×
NEW
121
      self.getStatus(true);
×
122
    });
36✔
123
  }
36✔
124

3✔
125
  static create() {
6✔
126
    let 
36✔
127
      game = Game.create(),
36✔
128
      gameClient = new UCIGameClient(game);
36✔
129

18✔
130
    updateGameClient(gameClient);
36✔
131

18✔
132
    return gameClient;
36✔
133
  }
36✔
134

3✔
135
  getStatus(forceUpdate) {
6✔
136
    if (forceUpdate) {
24✔
137
      updateGameClient(this);
12✔
138
    }
12✔
139

12✔
140
    return {
24✔
141
      board: this.game.board,
24✔
142
      isCheck: this.isCheck,
24✔
143
      isCheckmate: this.isCheckmate,
24✔
144
      isRepetition: this.isRepetition,
24✔
145
      isStalemate: this.isStalemate,
24✔
146
      uciMoves: this.uciMoves
24✔
147
    };
24✔
148
  }
24✔
149

3✔
150
  move(uci) {
6✔
151
    let 
54✔
152
      canonical = null,
54✔
153
      dest = null,
54✔
154
      move = null,
54✔
155
      parsed = parseUCI(uci),
54✔
156
      promo = null,
54✔
157
      requiresPromotion = false,
54✔
158
      side = null,
54✔
159
      src = null, 
54✔
160
      srcSquare = null;
54✔
161

27✔
162
    if (!parsed) {
54✔
163
      throw new Error(`UCI is invalid (${uci})`);
12✔
164
    }
12✔
165

21✔
166
    // destructure the parsed UCI move
42✔
167
    ({ src, dest, promo } = parsed);
42✔
168

21✔
169
    // normalize UCI key to compare with generated map
42✔
170
    canonical = promo
42✔
171
      ? `${src.file}${src.rank}${dest.file}${dest.rank}${promo.toLowerCase()}`
54✔
172
      : `${src.file}${src.rank}${dest.file}${dest.rank}`;
54✔
173

27✔
174
    // ensure move exactly matches a generated UCI move
54✔
175
    if (!this.uciMoves || !this.uciMoves[canonical]) {
54!
NEW
176
      throw new Error(`Move is invalid (${uci})`);
×
NEW
177
    }
✔
178

21✔
179
    // determine the current side
42✔
180
    side = this.game.getCurrentSide();
42✔
181

21✔
182
    // additional safety: enforce promotion semantics
42✔
183
    srcSquare = this.game.board.getSquare(src.file, src.rank);
42✔
184
    requiresPromotion =
42✔
185
      srcSquare && srcSquare.piece && srcSquare.piece.type === PieceType.Pawn &&
54✔
186
      (dest.rank === 8 || dest.rank === 1);
54✔
187

27✔
188
    if (requiresPromotion && !promo) {
54!
NEW
189
      throw new Error(`Promotion required for move (${uci})`);
×
NEW
190
    }
✔
191

21✔
192
    if (promo && !requiresPromotion) {
54!
NEW
193
      throw new Error(`Promotion flag not allowed for move (${uci})`);
×
NEW
194
    }
✔
195

21✔
196
    // make the move
42✔
197
    move = this.game.board.move(`${src.file}${src.rank}`, `${dest.file}${dest.rank}`);
42✔
198
    if (move) {
42✔
199
      // apply pawn promotion if applicable (already validated above)
42✔
200
      if (promo) {
42✔
201
        let piece;
6✔
202
        switch (promo) {
6✔
203
          case 'B':
6!
NEW
204
            piece = Piece.createBishop(side);
×
NEW
205
            break;
×
206
          case 'N':
6!
NEW
207
            piece = Piece.createKnight(side);
×
NEW
208
            break;
×
209
          case 'Q':
6✔
210
            piece = Piece.createQueen(side);
6✔
211
            break;
6✔
212
          case 'R':
6!
NEW
213
            piece = Piece.createRook(side);
×
NEW
214
            break;
×
215
          default:
6!
NEW
216
            piece = null;
×
NEW
217
            break;
×
218
        }
6✔
219

3✔
220
        if (piece) {
6✔
221
          this.game.board.promote(move.move.postSquare, piece);
6✔
222
        }
6✔
223
      }
6✔
224

21✔
225
      updateGameClient(this);
42✔
226
      
42✔
227
      return move;
42✔
228
    }
42!
229

×
NEW
230
    throw new Error(`Move is invalid (${uci})`);
×
231
  }
54✔
232
}
6✔
233

3✔
234
export default { UCIGameClient };
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