Fix Monochrome update
[vchess.git] / client / src / variants / Rococo.js
CommitLineData
2f8dce6a
BA
1import { ChessRules, PiPo, Move } from "@/base_rules";
2import { ArrayFun } from "@/utils/array";
3import { shuffle } from "@/utils/alea";
4
5export class RococoRules extends ChessRules {
6 static get HasFlags() {
7 return false;
8 }
9
10 static get HasEnpassant() {
11 return false;
12 }
13
14 static get PIECES() {
15 return ChessRules.PIECES.concat([V.IMMOBILIZER]);
16 }
17
107dc1bd
BA
18 static get Lines() {
19 return [
20 [[1, 1], [1, 9]],
21 [[1, 9], [9, 9]],
22 [[9, 9], [9, 1]],
23 [[9, 1], [1, 1]]
24 ];
25 }
26
2f8dce6a
BA
27 getPpath(b) {
28 if (b[1] == "m")
29 //'m' for Immobilizer (I is too similar to 1)
30 return "Rococo/" + b;
31 return b; //usual piece
32 }
33
34 getPPpath(m) {
35 // The only "choice" case is between a swap and a mutual destruction:
36 // show empty square in case of mutual destruction.
37 if (m.appear.length == 0) return "Rococo/empty";
38 return m.appear[0].c + m.appear[0].p;
39 }
40
41 setOtherVariables(fen) {
42 // No castling, but checks, so keep track of kings
43 this.kingPos = { w: [-1, -1], b: [-1, -1] };
44 const fenParts = fen.split(" ");
45 const position = fenParts[0].split("/");
46 for (let i = 0; i < position.length; i++) {
47 let k = 0;
48 for (let j = 0; j < position[i].length; j++) {
49 switch (position[i].charAt(j)) {
50 case "k":
51 this.kingPos["b"] = [i, k];
52 break;
53 case "K":
54 this.kingPos["w"] = [i, k];
55 break;
56 default: {
e50a8025 57 const num = parseInt(position[i].charAt(j), 10);
2f8dce6a
BA
58 if (!isNaN(num)) k += num - 1;
59 }
60 }
61 k++;
62 }
63 }
64 // Local stack of swaps:
65 this.smoves = [];
66 const smove = V.ParseFen(fen).smove;
67 if (smove == "-") this.smoves.push(null);
68 else {
69 this.smoves.push({
70 start: ChessRules.SquareToCoords(smove.substr(0, 2)),
71 end: ChessRules.SquareToCoords(smove.substr(2))
72 });
73 }
74 }
75
76 static ParseFen(fen) {
77 return Object.assign(
78 ChessRules.ParseFen(fen),
79 { smove: fen.split(" ")[3] }
80 );
81 }
82
83 static IsGoodFen(fen) {
84 if (!ChessRules.IsGoodFen(fen)) return false;
85 const fenParts = fen.split(" ");
86 if (fenParts.length != 4) return false;
87 if (fenParts[3] != "-" && !fenParts[3].match(/^([a-h][1-8]){2}$/))
88 return false;
89 return true;
90 }
91
92 getSmove(move) {
93 if (move.appear.length == 2)
94 return { start: move.start, end: move.end };
95 return null;
96 }
97
98 static get size() {
99 // Add the "capturing edge"
100 return { x: 10, y: 10 };
101 }
102
103 static get IMMOBILIZER() {
104 return "m";
105 }
106 // Although other pieces keep their names here for coding simplicity,
107 // keep in mind that:
108 // - a "rook" is a swapper, exchanging positions and "capturing" by
109 // mutual destruction only.
110 // - a "knight" is a long-leaper, capturing as in draughts
111 // - a "bishop" is a chameleon, capturing as its prey
112 // - a "queen" is a withdrawer+advancer, capturing by moving away from
113 // pieces or advancing in front of them.
114
115 // Is piece on square (x,y) immobilized?
116 isImmobilized([x, y]) {
117 const piece = this.getPiece(x, y);
118 const oppCol = V.GetOppCol(this.getColor(x, y));
119 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
120 for (let step of adjacentSteps) {
121 const [i, j] = [x + step[0], y + step[1]];
122 if (
123 V.OnBoard(i, j) &&
124 this.board[i][j] != V.EMPTY &&
125 this.getColor(i, j) == oppCol
126 ) {
127 const oppPiece = this.getPiece(i, j);
128 if (oppPiece == V.IMMOBILIZER) return [i, j];
129 // Only immobilizers are immobilized by chameleons:
130 if (oppPiece == V.BISHOP && piece == V.IMMOBILIZER) return [i, j];
131 }
132 }
133 return null;
134 }
135
136 static OnEdge(x, y) {
137 return x == 0 || y == 0 || x == V.size.x - 1 || y == V.size.y - 1;
138 }
139
140 getPotentialMovesFrom([x, y]) {
141 // Pre-check: is thing on this square immobilized?
142 const imSq = this.isImmobilized([x, y]);
e90bafa8
BA
143 const piece = this.getPiece(x, y);
144 if (!!imSq && piece != V.KING) {
145 // Only option is suicide, if I'm not a king:
2f8dce6a
BA
146 return [
147 new Move({
148 start: { x: x, y: y },
149 end: { x: imSq[0], y: imSq[1] },
150 appear: [],
151 vanish: [
152 new PiPo({
153 x: x,
154 y: y,
155 c: this.getColor(x, y),
156 p: this.getPiece(x, y)
157 })
158 ]
159 })
160 ];
161 }
162 let moves = [];
e90bafa8 163 switch (piece) {
2f8dce6a
BA
164 case V.IMMOBILIZER:
165 moves = this.getPotentialImmobilizerMoves([x, y]);
166 break;
167 default:
168 moves = super.getPotentialMovesFrom([x, y]);
169 }
170 // Post-processing: prune redundant non-minimal capturing moves,
171 // and non-capturing moves ending on the edge:
172 moves.forEach(m => {
173 // Useful precomputation
174 m.dist = Math.abs(m.end.x - m.start.x) + Math.abs(m.end.y - m.start.y);
175 });
176 return moves.filter(m => {
177 if (!V.OnEdge(m.end.x, m.end.y)) return true;
178 // End on the edge:
179 if (m.vanish.length == 1) return false;
180 // Capture or swap: only captures get filtered
181 if (m.appear.length == 2) return true;
182 // Can we find other moves with a shorter path to achieve the same
183 // capture? Apply to queens and knights.
184 if (
185 moves.some(mv => {
186 return (
187 mv.dist < m.dist &&
188 mv.vanish.length == m.vanish.length &&
189 mv.vanish.every(v => {
190 return m.vanish.some(vv => {
191 return (
192 vv.x == v.x && vv.y == v.y && vv.c == v.c && vv.p == v.p
193 );
194 });
195 })
196 );
197 })
198 ) {
199 return false;
200 }
201 return true;
202 });
203 // NOTE: not removing "dist" field; shouldn't matter much...
204 }
205
206 getSlideNJumpMoves([x, y], steps, oneStep) {
207 const piece = this.getPiece(x, y);
208 let moves = [];
209 outerLoop: for (let step of steps) {
210 let i = x + step[0];
211 let j = y + step[1];
212 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
213 moves.push(this.getBasicMove([x, y], [i, j]));
214 if (oneStep !== undefined) continue outerLoop;
215 i += step[0];
216 j += step[1];
217 }
218 // Only king can take on occupied square:
219 if (piece == V.KING && V.OnBoard(i, j) && this.canTake([x, y], [i, j]))
220 moves.push(this.getBasicMove([x, y], [i, j]));
221 }
222 return moves;
223 }
224
225 // "Cannon/grasshopper pawn"
226 getPotentialPawnMoves([x, y]) {
227 const oppCol = V.GetOppCol(this.turn);
228 let moves = [];
229 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
230 adjacentSteps.forEach(step => {
231 const [i, j] = [x + step[0], y + step[1]];
232 if (V.OnBoard(i, j)) {
233 if (this.board[i][j] == V.EMPTY)
234 moves.push(this.getBasicMove([x, y], [i, j]));
235 else {
236 // Try to leap over:
237 const [ii, jj] = [i + step[0], j + step[1]];
238 if (V.OnBoard(ii, jj) && this.getColor(ii, jj) == oppCol)
239 moves.push(this.getBasicMove([x, y], [ii, jj]));
240 }
241 }
242 });
243 return moves;
244 }
245
246 // NOTE: not really captures, but let's keep the name
247 getRookCaptures([x, y], byChameleon) {
248 let moves = [];
249 const oppCol = V.GetOppCol(this.turn);
250 // Simple: if something is visible, we can swap
251 V.steps[V.ROOK].concat(V.steps[V.BISHOP]).forEach(step => {
252 let [i, j] = [x + step[0], y + step[1]];
253 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
254 i += step[0];
255 j += step[1];
256 }
257 if (V.OnBoard(i, j) && this.getColor(i, j) == oppCol) {
258 const oppPiece = this.getPiece(i, j);
259 if (!byChameleon || oppPiece == V.ROOK) {
260 let m = this.getBasicMove([x, y], [i, j]);
261 m.appear.push(
262 new PiPo({
263 x: x,
264 y: y,
265 c: oppCol,
266 p: this.getPiece(i, j)
267 })
268 );
269 moves.push(m);
270 if (i == x + step[0] && j == y + step[1]) {
271 // Add mutual destruction option:
272 m = new Move({
273 start: { x: x, y: y},
274 end: { x: i, y: j },
275 appear: [],
276 // TODO: is copying necessary here?
277 vanish: JSON.parse(JSON.stringify(m.vanish))
278 });
279 moves.push(m);
280 }
281 }
282 }
283 });
284 return moves;
285 }
286
287 // Swapper
288 getPotentialRookMoves(sq) {
289 return super.getPotentialQueenMoves(sq).concat(this.getRookCaptures(sq));
290 }
291
292 getKnightCaptures(startSquare, byChameleon) {
293 // Look in every direction for captures
294 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
295 const color = this.turn;
296 const oppCol = V.GetOppCol(color);
297 let moves = [];
298 const [x, y] = [startSquare[0], startSquare[1]];
299 const piece = this.getPiece(x, y); //might be a chameleon!
300 outerLoop: for (let step of steps) {
301 let [i, j] = [x + step[0], y + step[1]];
302 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
303 i += step[0];
304 j += step[1];
305 }
306 if (
307 !V.OnBoard(i, j) ||
308 this.getColor(i, j) == color ||
309 (!!byChameleon && this.getPiece(i, j) != V.KNIGHT)
310 ) {
311 continue;
312 }
313 // last(thing), cur(thing) : stop if "cur" is our color,
314 // or beyond board limits, or if "last" isn't empty and cur neither.
315 // Otherwise, if cur is empty then add move until cur square;
316 // if cur is occupied then stop if !!byChameleon and the square not
317 // occupied by a leaper.
318 let last = [i, j];
319 let cur = [i + step[0], j + step[1]];
320 let vanished = [new PiPo({ x: x, y: y, c: color, p: piece })];
321 while (V.OnBoard(cur[0], cur[1])) {
322 if (this.board[last[0]][last[1]] != V.EMPTY) {
323 const oppPiece = this.getPiece(last[0], last[1]);
324 if (!!byChameleon && oppPiece != V.KNIGHT) continue outerLoop;
325 // Something to eat:
326 vanished.push(
327 new PiPo({ x: last[0], y: last[1], c: oppCol, p: oppPiece })
328 );
329 }
330 if (this.board[cur[0]][cur[1]] != V.EMPTY) {
331 if (
332 this.getColor(cur[0], cur[1]) == color ||
333 this.board[last[0]][last[1]] != V.EMPTY
334 ) {
335 //TODO: redundant test
336 continue outerLoop;
337 }
338 } else {
339 moves.push(
340 new Move({
341 appear: [new PiPo({ x: cur[0], y: cur[1], c: color, p: piece })],
342 vanish: JSON.parse(JSON.stringify(vanished)), //TODO: required?
343 start: { x: x, y: y },
344 end: { x: cur[0], y: cur[1] }
345 })
346 );
347 }
348 last = [last[0] + step[0], last[1] + step[1]];
349 cur = [cur[0] + step[0], cur[1] + step[1]];
350 }
351 }
352 return moves;
353 }
354
355 // Long-leaper
356 getPotentialKnightMoves(sq) {
357 return super.getPotentialQueenMoves(sq).concat(this.getKnightCaptures(sq));
358 }
359
360 // Chameleon
361 getPotentialBishopMoves([x, y]) {
362 const oppCol = V.GetOppCol(this.turn);
363 let moves = super
364 .getPotentialQueenMoves([x, y])
365 .concat(this.getKnightCaptures([x, y], "asChameleon"))
366 .concat(this.getRookCaptures([x, y], "asChameleon"));
367 // No "king capture" because king cannot remain under check
368 this.addQueenCaptures(moves, "asChameleon");
369 // Also add pawn captures (as a pawn):
370 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
371 adjacentSteps.forEach(step => {
372 const [i, j] = [x + step[0], y + step[1]];
373 const [ii, jj] = [i + step[0], j + step[1]];
374 // Try to leap over (i,j):
375 if (
376 V.OnBoard(ii, jj) &&
377 this.board[i][j] != V.EMPTY &&
378 this.board[ii][jj] != V.EMPTY &&
379 this.getColor(ii, jj) == oppCol &&
380 this.getPiece(ii, jj) == V.PAWN
381 ) {
382 moves.push(this.getBasicMove([x, y], [ii, jj]));
383 }
384 });
385 // Post-processing: merge similar moves, concatenating vanish arrays
386 let mergedMoves = {};
387 moves.forEach(m => {
388 const key = m.end.x + V.size.x * m.end.y;
389 if (!mergedMoves[key]) mergedMoves[key] = m;
390 else {
391 for (let i = 1; i < m.vanish.length; i++)
392 mergedMoves[key].vanish.push(m.vanish[i]);
393 }
394 });
395 return Object.values(mergedMoves);
396 }
397
398 addQueenCaptures(moves, byChameleon) {
399 if (moves.length == 0) return;
400 const [x, y] = [moves[0].start.x, moves[0].start.y];
401 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
402 let capturingDirStart = {};
403 const oppCol = V.GetOppCol(this.turn);
404 // Useful precomputation:
405 adjacentSteps.forEach(step => {
406 const [i, j] = [x + step[0], y + step[1]];
407 if (
408 V.OnBoard(i, j) &&
409 this.board[i][j] != V.EMPTY &&
410 this.getColor(i, j) == oppCol &&
411 (!byChameleon || this.getPiece(i, j) == V.QUEEN)
412 ) {
413 capturingDirStart[step[0] + "_" + step[1]] = this.getPiece(i, j);
414 }
415 });
416 moves.forEach(m => {
417 const step = [
418 m.end.x != x ? (m.end.x - x) / Math.abs(m.end.x - x) : 0,
419 m.end.y != y ? (m.end.y - y) / Math.abs(m.end.y - y) : 0
420 ];
421 // TODO: this test should be done only once per direction
422 const capture = capturingDirStart[(-step[0]) + "_" + (-step[1])];
423 if (!!capture) {
424 const [i, j] = [x - step[0], y - step[1]];
425 m.vanish.push(
426 new PiPo({
427 x: i,
428 y: j,
429 p: capture,
430 c: oppCol
431 })
432 );
433 }
434 // Also test the end (advancer effect)
435 const [i, j] = [m.end.x + step[0], m.end.y + step[1]];
436 if (
437 V.OnBoard(i, j) &&
438 this.board[i][j] != V.EMPTY &&
439 this.getColor(i, j) == oppCol &&
440 (!byChameleon || this.getPiece(i, j) == V.QUEEN)
441 ) {
442 m.vanish.push(
443 new PiPo({
444 x: i,
445 y: j,
446 p: this.getPiece(i, j),
447 c: oppCol
448 })
449 );
450 }
451 });
452 }
453
454 // Withdrawer + advancer: "pushme-pullyu"
455 getPotentialQueenMoves(sq) {
456 let moves = super.getPotentialQueenMoves(sq);
457 this.addQueenCaptures(moves);
458 return moves;
459 }
460
461 getPotentialImmobilizerMoves(sq) {
462 // Immobilizer doesn't capture
463 return super.getPotentialQueenMoves(sq);
464 }
465
466 // Does m2 un-do m1 ? (to disallow undoing swaps)
467 oppositeMoves(m1, m2) {
468 return (
469 !!m1 &&
470 m2.appear.length == 2 &&
471 m1.start.x == m2.start.x &&
472 m1.end.x == m2.end.x &&
473 m1.start.y == m2.start.y &&
474 m1.end.y == m2.end.y
475 );
476 }
477
478 filterValid(moves) {
479 if (moves.length == 0) return [];
480 const color = this.turn;
481 return (
482 super.filterValid(
483 moves.filter(m => {
484 const L = this.smoves.length; //at least 1: init from FEN
485 return !this.oppositeMoves(this.smoves[L - 1], m);
486 })
487 )
488 );
489 }
490
491 // isAttacked() is OK because the immobilizer doesn't take
492
493 isAttackedByPawn([x, y], color) {
494 // Attacked if an enemy pawn stands just behind an immediate obstacle:
495 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
496 for (let step of adjacentSteps) {
497 const [i, j] = [x + step[0], y + step[1]];
498 const [ii, jj] = [i + step[0], j + step[1]];
499 if (
500 V.OnBoard(ii, jj) &&
501 this.board[i][j] != V.EMPTY &&
502 this.board[ii][jj] != V.EMPTY &&
503 this.getColor(ii, jj) == color &&
504 this.getPiece(ii, jj) == V.PAWN &&
505 !this.isImmobilized([ii, jj])
506 ) {
507 return true;
508 }
509 }
510 return false;
511 }
512
513 isAttackedByRook([x, y], color) {
514 // The only way a swapper can take is by mutual destruction when the
515 // enemy piece stands just next:
516 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
517 for (let step of adjacentSteps) {
518 const [i, j] = [x + step[0], y + step[1]];
519 if (
520 V.OnBoard(i, j) &&
521 this.board[i][j] != V.EMPTY &&
522 this.getColor(i, j) == color &&
523 this.getPiece(i, j) == V.ROOK &&
524 !this.isImmobilized([i, j])
525 ) {
526 return true;
527 }
528 }
529 return false;
530 }
531
532 isAttackedByKnight([x, y], color) {
533 // Square (x,y) must be on same line as a knight,
534 // and there must be empty square(s) behind.
535 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
536 outerLoop: for (let step of steps) {
537 const [i0, j0] = [x + step[0], y + step[1]];
538 if (V.OnBoard(i0, j0) && this.board[i0][j0] == V.EMPTY) {
539 // Try in opposite direction:
540 let [i, j] = [x - step[0], y - step[1]];
541 while (V.OnBoard(i, j)) {
542 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
543 i -= step[0];
544 j -= step[1];
545 }
546 if (V.OnBoard(i, j)) {
547 if (this.getColor(i, j) == color) {
548 if (
549 this.getPiece(i, j) == V.KNIGHT &&
550 !this.isImmobilized([i, j])
551 )
552 return true;
553 continue outerLoop;
554 }
555 // [else] Our color,
556 // could be captured *if there was an empty space*
557 if (this.board[i + step[0]][j + step[1]] != V.EMPTY)
558 continue outerLoop;
559 i -= step[0];
560 j -= step[1];
561 }
562 }
563 }
564 }
565 return false;
566 }
567
568 isAttackedByBishop([x, y], color) {
569 // We cheat a little here: since this function is used exclusively for
570 // the king, it's enough to check the immediate surrounding of the square.
571 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
572 for (let step of adjacentSteps) {
573 const [i, j] = [x + step[0], y + step[1]];
574 if (
575 V.OnBoard(i, j) &&
576 this.board[i][j] != V.EMPTY &&
577 this.getColor(i, j) == color &&
578 this.getPiece(i, j) == V.BISHOP &&
579 !this.isImmobilized([i, j])
580 ) {
581 return true;
582 }
583 }
584 return false;
585 }
586
587 isAttackedByQueen([x, y], color) {
588 // Is there a queen in view?
589 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
590 for (let step of adjacentSteps) {
591 let [i, j] = [x + step[0], y + step[1]];
592 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
593 i += step[0];
594 j += step[1];
595 }
596 if (
597 V.OnBoard(i, j) &&
598 this.getColor(i, j) == color &&
599 this.getPiece(i, j) == V.QUEEN
600 ) {
601 // Two cases: the queen is at 2 steps at least, or just close
602 // but maybe with enough space behind to withdraw.
603 let attacked = false;
604 if (i == x + step[0] && j == y + step[1]) {
605 const [ii, jj] = [i + step[0], j + step[1]];
606 if (V.OnBoard(ii, jj) && this.board[ii][jj] == V.EMPTY)
607 attacked = true;
608 }
609 else attacked = true;
610 if (attacked && !this.isImmobilized([i, j])) return true;
611 }
612 }
613 return false;
614 }
615
616 isAttackedByKing([x, y], color) {
617 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
618 for (let step of steps) {
619 let rx = x + step[0],
620 ry = y + step[1];
621 if (
622 V.OnBoard(rx, ry) &&
623 this.getPiece(rx, ry) === V.KING &&
624 this.getColor(rx, ry) == color &&
625 !this.isImmobilized([rx, ry])
626 ) {
627 return true;
628 }
629 }
630 return false;
631 }
632
633 static GenRandInitFen(randomness) {
634 if (randomness == 0) {
635 return (
93ce6119 636 "91/1rqnbknqm1/1pppppppp1/91/91/91/91/1PPPPPPPP1/1MQNBKNQR1/91 w 0 -"
2f8dce6a
BA
637 );
638 }
639
640 let pieces = { w: new Array(8), b: new Array(8) };
641 // Shuffle pieces on first and last rank
642 for (let c of ["w", "b"]) {
643 if (c == 'b' && randomness == 1) {
644 pieces['b'] = pieces['w'];
645 break;
646 }
647
648 // Get random squares for every piece, totally freely
649 let positions = shuffle(ArrayFun.range(8));
650 const composition = ['r', 'm', 'n', 'n', 'q', 'q', 'b', 'k'];
651 for (let i = 0; i < 8; i++) pieces[c][positions[i]] = composition[i];
652 }
653 return (
654 "91/1" + pieces["b"].join("") +
655 "1/1pppppppp1/91/91/91/91/1PPPPPPPP1/1" +
656 pieces["w"].join("").toUpperCase() + "1/91 w 0 -"
657 );
658 }
659
660 getSmoveFen() {
661 const L = this.smoves.length;
662 return (
663 !this.smoves[L - 1]
664 ? "-"
665 : ChessRules.CoordsToSquare(this.smoves[L - 1].start) +
666 ChessRules.CoordsToSquare(this.smoves[L - 1].end)
667 );
668 }
669
670 getFen() {
671 return super.getFen() + " " + this.getSmoveFen();
672 }
673
674 getFenForRepeat() {
675 return super.getFenForRepeat() + "_" + this.getSmoveFen();
676 }
677
678 postPlay(move) {
679 super.postPlay(move);
680 this.smoves.push(this.getSmove(move));
681 }
682
683 postUndo(move) {
684 super.postUndo(move);
685 this.smoves.pop();
686 }
687
688 static get VALUES() {
689 return {
690 p: 1,
691 r: 2,
692 n: 5,
693 b: 3,
694 q: 5,
695 m: 5,
696 k: 1000
697 };
698 }
699
700 static get SEARCH_DEPTH() {
701 return 2;
702 }
703
704 getNotation(move) {
705 const initialSquare = V.CoordsToSquare(move.start);
706 const finalSquare = V.CoordsToSquare(move.end);
707 if (move.appear.length == 0) {
708 // Suicide 'S' or mutual destruction 'D':
709 return (
710 initialSquare + (move.vanish.length == 1 ? "S" : "D" + finalSquare)
711 );
712 }
713 let notation = undefined;
714 if (move.appear[0].p == V.PAWN) {
715 // Pawn: generally ambiguous short notation, so we use full description
716 notation = "P" + initialSquare + finalSquare;
717 } else if (move.appear[0].p == V.KING)
718 notation = "K" + (move.vanish.length > 1 ? "x" : "") + finalSquare;
719 else notation = move.appear[0].p.toUpperCase() + finalSquare;
720 // Add a capture mark (not describing what is captured...):
721 if (move.vanish.length > 1 && move.appear[0].p != V.KING) notation += "X";
722 return notation;
723 }
724};