1 import { ChessRules
, PiPo
, Move
} from "@/base_rules";
2 import { ArrayFun
} from "@/utils/array";
3 import { shuffle
} from "@/utils/alea";
5 export class MaximaRules
extends ChessRules
{
6 static get HasFlags() {
10 static get HasEnpassant() {
15 return ChessRules
.PIECES
.concat([V
.IMMOBILIZER
, V
.MAGE
, V
.GUARD
]);
19 if (b
[0] == 'x') return "Maxima/nothing";
20 if (['m','d','g'].includes(b
[1]))
25 // For space next to the palaces:
26 static get NOTHING() {
31 if (b
[0] == 'x') return 'x';
32 return ChessRules
.board2fen(b
);
36 if (f
== 'x') return V
.NOTHING
;
37 return ChessRules
.fen2board(f
);
40 // TODO: the wall position should be checked too
41 static IsGoodPosition(position
) {
42 if (position
.length
== 0) return false;
43 const rows
= position
.split("/");
44 if (rows
.length
!= V
.size
.x
) return false;
45 let kings
= { "k": 0, "K": 0 };
46 for (let row
of rows
) {
48 for (let i
= 0; i
< row
.length
; i
++) {
49 if (['K','k'].includes(row
[i
])) kings
[row
[i
]]++;
50 if (['x'].concat(V
.PIECES
).includes(row
[i
].toLowerCase())) sumElts
++;
52 const num
= parseInt(row
[i
], 10);
53 if (isNaN(num
)) return false;
57 if (sumElts
!= V
.size
.y
) return false;
59 if (Object
.values(kings
).some(v
=> v
!= 1)) return false;
63 // No castling, but checks, so keep track of kings
64 setOtherVariables(fen
) {
65 this.kingPos
= { w: [-1, -1], b: [-1, -1] };
66 const fenParts
= fen
.split(" ");
67 const position
= fenParts
[0].split("/");
68 for (let i
= 0; i
< position
.length
; i
++) {
70 for (let j
= 0; j
< position
[i
].length
; j
++) {
71 switch (position
[i
].charAt(j
)) {
73 this.kingPos
["b"] = [i
, k
];
76 this.kingPos
["w"] = [i
, k
];
79 const num
= parseInt(position
[i
].charAt(j
), 10);
80 if (!isNaN(num
)) k
+= num
- 1;
89 return { x: 11, y: 8 };
92 static OnBoard(x
, y
) {
94 (x
>= 1 && x
<= 9 && y
>= 0 && y
<= 7) ||
95 ([3, 4].includes(y
) && [0, 10].includes(x
))
99 static get IMMOBILIZER() {
108 // Although other pieces keep their names here for coding simplicity,
109 // keep in mind that:
110 // - a "rook" is a coordinator, capturing by coordinating with the king
111 // - a "knight" is a long-leaper, capturing as in draughts
112 // - a "bishop" is a chameleon, capturing as its prey
113 // - a "queen" is a withdrawer, capturing by moving away from pieces
115 // Is piece on square (x,y) immobilized?
116 isImmobilized([x
, y
]) {
117 const piece
= this.getPiece(x
, y
);
119 // Mages are not immobilized:
121 const oppCol
= V
.GetOppCol(this.getColor(x
, y
));
122 const adjacentSteps
= V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]);
123 for (let step
of adjacentSteps
) {
124 const [i
, j
] = [x
+ step
[0], y
+ step
[1]];
127 this.board
[i
][j
] != V
.EMPTY
&&
128 this.getColor(i
, j
) == oppCol
130 const oppPiece
= this.getPiece(i
, j
);
131 if (oppPiece
== V
.IMMOBILIZER
) return [i
, j
];
132 // Only immobilizers are immobilized by chameleons:
133 if (oppPiece
== V
.BISHOP
&& piece
== V
.IMMOBILIZER
) return [i
, j
];
139 getPotentialMovesFrom([x
, y
]) {
140 // Pre-check: is thing on this square immobilized?
141 const imSq
= this.isImmobilized([x
, y
]);
142 const piece
= this.getPiece(x
, y
);
143 if (!!imSq
&& piece
!= V
.KING
) {
144 // Only option is suicide, if I'm not a king:
147 start: { x: x
, y: y
},
148 end: { x: imSq
[0], y: imSq
[1] },
154 c: this.getColor(x
, y
),
155 p: this.getPiece(x
, y
)
161 let moves
= undefined;
164 moves
= this.getPotentialImmobilizerMoves([x
, y
]);
167 moves
= this.getPotentialGuardMoves([x
, y
]);
170 moves
= this.getPotentialMageMoves([x
, y
]);
173 moves
= super.getPotentialMovesFrom([x
, y
]);
175 const pX
= (this.turn
== 'w' ? 10 : 0);
176 if (this.board
[pX
][3] == V
.EMPTY
&& this.board
[pX
][4] == V
.EMPTY
)
178 // Filter out moves resulting in self palace occupation:
179 // NOTE: cannot invade own palace but still check the king there.
180 const pY
= (this.board
[pX
][3] != V
.EMPTY
? 4 : 3);
181 return moves
.filter(m
=> m
.end
.x
!= pX
|| m
.end
.y
!= pY
);
184 getSlideNJumpMoves([x
, y
], steps
, oneStep
, mageInitSquare
) {
185 const piece
= !mageInitSquare
? this.getPiece(x
, y
) : V
.MAGE
;
186 const initSquare
= mageInitSquare
|| [x
, y
];
188 outerLoop: for (let step
of steps
) {
191 if (piece
== V
.KING
) j
= j
% V
.size
.y
;
192 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
193 moves
.push(this.getBasicMove(initSquare
, [i
, j
]));
194 if (!!oneStep
) continue outerLoop
;
198 // Only king, guard and mage (+ chameleon) can take on occupied square:
201 [V
.KING
, V
.GUARD
, V
.MAGE
].includes(piece
) &&
202 this.canTake(initSquare
, [i
, j
])
204 moves
.push(this.getBasicMove(initSquare
, [i
, j
]));
210 // Modify capturing moves among listed pawn moves
211 addPawnCaptures(moves
, byChameleon
) {
212 const steps
= V
.steps
[V
.ROOK
];
213 const color
= this.turn
;
214 const oppCol
= V
.GetOppCol(color
);
216 if (!!byChameleon
&& m
.start
.x
!= m
.end
.x
&& m
.start
.y
!= m
.end
.y
)
217 // Chameleon not moving as pawn
219 // Try capturing in every direction
220 for (let step
of steps
) {
221 const sq2
= [m
.end
.x
+ 2 * step
[0], m
.end
.y
+ 2 * step
[1]];
223 V
.OnBoard(sq2
[0], sq2
[1]) &&
224 this.board
[sq2
[0]][sq2
[1]] != V
.EMPTY
&&
225 this.getColor(sq2
[0], sq2
[1]) == color
228 const sq1
= [m
.end
.x
+ step
[0], m
.end
.y
+ step
[1]];
230 this.board
[sq1
[0]][sq1
[1]] != V
.EMPTY
&&
231 this.getColor(sq1
[0], sq1
[1]) == oppCol
233 const piece1
= this.getPiece(sq1
[0], sq1
[1]);
234 if (!byChameleon
|| piece1
== V
.PAWN
) {
251 getPotentialPawnMoves([x
, y
]) {
252 let moves
= super.getPotentialRookMoves([x
, y
]);
253 this.addPawnCaptures(moves
);
257 addRookCaptures(moves
, byChameleon
) {
258 const color
= this.turn
;
259 const oppCol
= V
.GetOppCol(color
);
260 const kp
= this.kingPos
[color
];
262 // Check piece-king rectangle (if any) corners for enemy pieces
263 if (m
.end
.x
== kp
[0] || m
.end
.y
== kp
[1]) return; //"flat rectangle"
264 const corner1
= [m
.end
.x
, kp
[1]];
265 const corner2
= [kp
[0], m
.end
.y
];
266 for (let [i
, j
] of [corner1
, corner2
]) {
267 if (this.board
[i
][j
] != V
.EMPTY
&& this.getColor(i
, j
) == oppCol
) {
268 const piece
= this.getPiece(i
, j
);
269 if (!byChameleon
|| piece
== V
.ROOK
) {
285 getPotentialRookMoves(sq
) {
286 let moves
= super.getPotentialQueenMoves(sq
);
287 this.addRookCaptures(moves
);
291 getKnightCaptures(startSquare
, byChameleon
) {
292 // Look in every direction for captures
293 const steps
= V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]);
294 const color
= this.turn
;
295 const oppCol
= V
.GetOppCol(color
);
297 const [x
, y
] = [startSquare
[0], startSquare
[1]];
298 const piece
= this.getPiece(x
, y
); //might be a chameleon!
299 outerLoop: for (let step
of steps
) {
300 let [i
, j
] = [x
+ step
[0], y
+ step
[1]];
301 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
307 this.getColor(i
, j
) == color
||
308 (!!byChameleon
&& this.getPiece(i
, j
) != V
.KNIGHT
)
312 // last(thing), cur(thing) : stop if "cur" is our color,
313 // or beyond board limits, or if "last" isn't empty and cur neither.
314 // Otherwise, if cur is empty then add move until cur square;
315 // if cur is occupied then stop if !!byChameleon and the square not
316 // occupied by a leaper.
318 let cur
= [i
+ step
[0], j
+ step
[1]];
319 let vanished
= [new PiPo({ x: x
, y: y
, c: color
, p: piece
})];
320 while (V
.OnBoard(cur
[0], cur
[1])) {
321 if (this.board
[last
[0]][last
[1]] != V
.EMPTY
) {
322 const oppPiece
= this.getPiece(last
[0], last
[1]);
323 if (!!byChameleon
&& oppPiece
!= V
.KNIGHT
) continue outerLoop
;
326 new PiPo({ x: last
[0], y: last
[1], c: oppCol
, p: oppPiece
})
329 if (this.board
[cur
[0]][cur
[1]] != V
.EMPTY
) {
331 this.getColor(cur
[0], cur
[1]) == color
||
332 this.board
[last
[0]][last
[1]] != V
.EMPTY
334 //TODO: redundant test
340 appear: [new PiPo({ x: cur
[0], y: cur
[1], c: color
, p: piece
})],
341 vanish: JSON
.parse(JSON
.stringify(vanished
)), //TODO: required?
342 start: { x: x
, y: y
},
343 end: { x: cur
[0], y: cur
[1] }
347 last
= [last
[0] + step
[0], last
[1] + step
[1]];
348 cur
= [cur
[0] + step
[0], cur
[1] + step
[1]];
355 getPotentialKnightMoves(sq
) {
356 return super.getPotentialQueenMoves(sq
).concat(this.getKnightCaptures(sq
));
360 getPotentialBishopMoves([x
, y
]) {
362 .getPotentialQueenMoves([x
, y
])
363 .concat(this.getKnightCaptures([x
, y
], "asChameleon"))
364 // No "king capture" because king cannot remain under check
365 this.addPawnCaptures(moves
, "asChameleon");
366 this.addRookCaptures(moves
, "asChameleon");
367 this.addQueenCaptures(moves
, "asChameleon");
368 // Manually add Guard and Mage captures (since cannot move like a Mage)
369 V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]).forEach(step
=> {
370 const [i
, j
] = [x
+ step
[0], y
+ step
[1]];
373 this.board
[i
][j
] != V
.EMPTY
&&
374 this.canTake([x
, y
], [i
, j
]) &&
375 [V
.GUARD
, V
.MAGE
].includes(this.getPiece(i
, j
))
377 moves
.push(this.getBasicMove([x
, y
], [i
, j
]));
380 // Post-processing: merge similar moves, concatenating vanish arrays
381 let mergedMoves
= {};
383 const key
= m
.end
.x
+ V
.size
.x
* m
.end
.y
;
384 if (!mergedMoves
[key
]) mergedMoves
[key
] = m
;
386 for (let i
= 1; i
< m
.vanish
.length
; i
++)
387 mergedMoves
[key
].vanish
.push(m
.vanish
[i
]);
390 return Object
.values(mergedMoves
);
393 addQueenCaptures(moves
, byChameleon
) {
394 if (moves
.length
== 0) return;
395 const [x
, y
] = [moves
[0].start
.x
, moves
[0].start
.y
];
396 const adjacentSteps
= V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]);
397 let capturingDirections
= [];
398 const color
= this.turn
;
399 const oppCol
= V
.GetOppCol(color
);
400 adjacentSteps
.forEach(step
=> {
401 const [i
, j
] = [x
+ step
[0], y
+ step
[1]];
404 this.board
[i
][j
] != V
.EMPTY
&&
405 this.getColor(i
, j
) == oppCol
&&
406 (!byChameleon
|| this.getPiece(i
, j
) == V
.QUEEN
)
408 capturingDirections
.push(step
);
413 m
.end
.x
!= x
? (m
.end
.x
- x
) / Math
.abs(m
.end
.x
- x
) : 0,
414 m
.end
.y
!= y
? (m
.end
.y
- y
) / Math
.abs(m
.end
.y
- y
) : 0
416 // TODO: this test should be done only once per direction
418 capturingDirections
.some(dir
=> {
419 return dir
[0] == -step
[0] && dir
[1] == -step
[1];
422 const [i
, j
] = [x
- step
[0], y
- step
[1]];
427 p: this.getPiece(i
, j
),
436 getPotentialQueenMoves(sq
) {
437 let moves
= super.getPotentialQueenMoves(sq
);
438 this.addQueenCaptures(moves
);
442 getPotentialImmobilizerMoves(sq
) {
443 // Immobilizer doesn't capture
444 return super.getPotentialQueenMoves(sq
);
447 getPotentialKingMoves(sq
) {
448 return this.getSlideNJumpMoves(sq
, V
.steps
[V
.KNIGHT
], "oneStep");
451 getPotentialGuardMoves(sq
) {
453 this.getSlideNJumpMoves(
455 V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]),
462 getNextMageSteps(step
) {
464 if (step
[1] == -1) return [[-1, 0], [0, -1]];
465 return [[-1, 0], [0, 1]];
467 if (step
[1] == -1) return [[1, 0], [0, -1]];
468 return [[1, 0], [0, 1]];
471 getPotentialMageMoves([x
, y
]) {
472 const oppCol
= V
.GetOppCol(this.turn
);
474 for (let step
of V
.steps
[V
.BISHOP
]) {
475 let [i
, j
] = [x
+ step
[0], y
+ step
[1]];
476 if (!V
.OnBoard(i
, j
)) continue;
477 if (this.board
[i
][j
] != V
.EMPTY
) {
478 if (this.getColor(i
, j
) == oppCol
)
480 moves
.push(this.getBasicMove([x
, y
], [i
, j
]));
483 moves
.push(this.getBasicMove([x
, y
], [i
, j
]));
484 // Continue orthogonally:
485 const stepO
= this.getNextMageSteps(step
);
486 Array
.prototype.push
.apply(
488 this.getSlideNJumpMoves([i
, j
], stepO
, null, [x
, y
])
495 isAttacked(sq
, color
) {
497 super.isAttacked(sq
, color
) ||
498 this.isAttackedByGuard(sq
, color
) ||
499 this.isAttackedByMage(sq
, color
)
503 isAttackedByPawn([x
, y
], color
) {
504 // Square (x,y) must be surroundable by two enemy pieces,
505 // and one of them at least should be a pawn (moving).
510 const steps
= V
.steps
[V
.ROOK
];
511 for (let dir
of dirs
) {
512 const [i1
, j1
] = [x
- dir
[0], y
- dir
[1]]; //"before"
513 const [i2
, j2
] = [x
+ dir
[0], y
+ dir
[1]]; //"after"
514 if (V
.OnBoard(i1
, j1
) && V
.OnBoard(i2
, j2
)) {
517 this.board
[i1
][j1
] != V
.EMPTY
&&
518 this.getColor(i1
, j1
) == color
&&
519 this.board
[i2
][j2
] == V
.EMPTY
523 this.board
[i2
][j2
] != V
.EMPTY
&&
524 this.getColor(i2
, j2
) == color
&&
525 this.board
[i1
][j1
] == V
.EMPTY
528 // Search a movable enemy pawn landing on the empty square
529 for (let step
of steps
) {
530 let [ii
, jj
] = this.board
[i1
][j1
] == V
.EMPTY
? [i1
, j1
] : [i2
, j2
];
531 let [i3
, j3
] = [ii
+ step
[0], jj
+ step
[1]];
532 while (V
.OnBoard(i3
, j3
) && this.board
[i3
][j3
] == V
.EMPTY
) {
538 this.getColor(i3
, j3
) == color
&&
539 this.getPiece(i3
, j3
) == V
.PAWN
&&
540 !this.isImmobilized([i3
, j3
])
551 isAttackedByRook([x
, y
], color
) {
552 // King must be on same column or row,
553 // and a rook should be able to reach a capturing square
554 const sameRow
= x
== this.kingPos
[color
][0];
555 const sameColumn
= y
== this.kingPos
[color
][1];
556 if (sameRow
|| sameColumn
) {
557 // Look for the enemy rook (maximum 1)
558 for (let i
= 0; i
< V
.size
.x
; i
++) {
559 for (let j
= 0; j
< V
.size
.y
; j
++) {
561 this.board
[i
][j
] != V
.EMPTY
&&
562 this.getColor(i
, j
) == color
&&
563 this.getPiece(i
, j
) == V
.ROOK
565 if (this.isImmobilized([i
, j
]))
566 // Because only one rook:
568 // Can it reach a capturing square? Easy but quite suboptimal way
569 // (TODO: generate all moves (turn is OK))
570 const moves
= this.getPotentialMovesFrom([i
, j
]);
571 for (let move of moves
) {
573 (sameRow
&& move.end
.y
== y
) ||
574 (sameColumn
&& move.end
.x
== x
)
586 isAttackedByKnight([x
, y
], color
) {
587 // Square (x,y) must be on same line as a knight,
588 // and there must be empty square(s) behind.
589 const steps
= V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]);
590 outerLoop: for (let step
of steps
) {
591 const [i0
, j0
] = [x
+ step
[0], y
+ step
[1]];
592 if (V
.OnBoard(i0
, j0
) && this.board
[i0
][j0
] == V
.EMPTY
) {
593 // Try in opposite direction:
594 let [i
, j
] = [x
- step
[0], y
- step
[1]];
595 while (V
.OnBoard(i
, j
)) {
596 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
600 if (V
.OnBoard(i
, j
)) {
601 if (this.getColor(i
, j
) == color
) {
603 this.getPiece(i
, j
) == V
.KNIGHT
&&
604 !this.isImmobilized([i
, j
])
611 // could be captured *if there was an empty space*
612 if (this.board
[i
+ step
[0]][j
+ step
[1]] != V
.EMPTY
)
623 isAttackedByBishop([x
, y
], color
) {
624 // We cheat a little here: since this function is used exclusively for
625 // the king, it's enough to check the immediate surrounding of the square.
626 const adjacentSteps
= V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]);
627 for (let step
of adjacentSteps
) {
628 const [i
, j
] = [x
+ step
[0], y
+ step
[1]];
631 this.board
[i
][j
] != V
.EMPTY
&&
632 this.getColor(i
, j
) == color
&&
633 this.getPiece(i
, j
) == V
.BISHOP
&&
634 !this.isImmobilized([i
, j
])
642 isAttackedByQueen([x
, y
], color
) {
643 // Square (x,y) must be adjacent to a queen, and the queen must have
644 // some free space in the opposite direction from (x,y)
645 const adjacentSteps
= V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]);
646 for (let step
of adjacentSteps
) {
647 const sq2
= [x
+ 2 * step
[0], y
+ 2 * step
[1]];
648 if (V
.OnBoard(sq2
[0], sq2
[1]) && this.board
[sq2
[0]][sq2
[1]] == V
.EMPTY
) {
649 const sq1
= [x
+ step
[0], y
+ step
[1]];
651 this.board
[sq1
[0]][sq1
[1]] != V
.EMPTY
&&
652 this.getColor(sq1
[0], sq1
[1]) == color
&&
653 this.getPiece(sq1
[0], sq1
[1]) == V
.QUEEN
&&
654 !this.isImmobilized(sq1
)
663 isAttackedByKing([x
, y
], color
) {
664 for (let step
of V
.steps
[V
.KNIGHT
]) {
665 let rx
= x
+ step
[0],
666 // Circular board for king-knight:
667 ry
= (y
+ step
[1]) % V
.size
.y
;
670 this.getPiece(rx
, ry
) === V
.KING
&&
671 this.getColor(rx
, ry
) == color
&&
672 !this.isImmobilized([rx
, ry
])
680 isAttackedByGuard(sq
, color
) {
682 super.isAttackedBySlideNJump(
686 V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]),
692 getNextMageCheck(step
) {
694 if (step
[1] == 1) return [[1, 1], [-1, 1]];
695 return [[-1, -1], [1, -1]];
697 if (step
[0] == -1) return [[-1, -1], [-1, 1]];
698 return [[1, 1], [1, -1]];
701 isAttackedByMage([x
, y
], color
) {
702 for (let step
of V
.steps
[V
.BISHOP
]) {
703 const [i
, j
] = [x
+ step
[0], y
+ step
[1]];
706 this.board
[i
][j
] != V
.EMPTY
&&
707 this.getColor(i
, j
) == color
&&
708 this.getPiece(i
, j
) == V
.MAGE
713 for (let step
of V
.steps
[V
.ROOK
]) {
714 let [i
, j
] = [x
+ step
[0], y
+ step
[1]];
715 const stepM
= this.getNextMageCheck(step
);
716 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
717 for (let s
of stepM
) {
718 const [ii
, jj
] = [i
+ s
[0], j
+ s
[1]];
721 this.board
[ii
][jj
] != V
.EMPTY
&&
722 this.getColor(ii
, jj
) == color
&&
723 this.getPiece(ii
, jj
) == V
.MAGE
736 const color
= this.turn
;
737 const getScoreLost
= () => {
739 return color
== "w" ? "0-1" : "1-0";
741 if (!this.atLeastOneMove()) {
742 // No valid move: I lose or draw
743 if (this.underCheck(color
)) return getScoreLost();
746 // I lose also if no pieces left (except king)
748 outerLoop: for (let i
=0; i
<V
.size
.x
; i
++) {
749 for (let j
=0; j
<V
.size
.y
; j
++) {
751 this.board
[i
][j
] != V
.EMPTY
&&
752 this.getColor(i
, j
) == color
&&
753 this.getPiece(i
,j
) != V
.KING
759 if (piecesLeft
== 0) return getScoreLost();
760 // Check if my palace is invaded:
761 const pX
= (color
== 'w' ? 10 : 0);
762 const oppCol
= V
.GetOppCol(color
);
764 this.board
[pX
][3] != V
.EMPTY
&&
765 this.getColor(pX
, 3) == oppCol
&&
766 this.board
[pX
][4] != V
.EMPTY
&&
767 this.getColor(pX
, 4) == oppCol
769 return getScoreLost();
774 static GenRandInitFen() {
775 // Always deterministic:
777 "xxx2xxx/1g1qk1g1/1bnmrnb1/dppppppd/8/8/8/" +
778 "DPPPPPPD/1BNMRNB1/1G1QK1G1/xxx2xxx w 0"
782 static get VALUES() {
796 static get SEARCH_DEPTH() {
802 for (let i
= 0; i
< V
.size
.x
; i
++) {
803 for (let j
= 0; j
< V
.size
.y
; j
++) {
804 if (![V
.EMPTY
,V
.NOTHING
].includes(this.board
[i
][j
])) {
805 const sign
= this.getColor(i
, j
) == "w" ? 1 : -1;
806 evaluation
+= sign
* V
.VALUES
[this.getPiece(i
, j
)];
814 const initialSquare
= V
.CoordsToSquare(move.start
);
815 const finalSquare
= V
.CoordsToSquare(move.end
);
816 if (move.appear
.length
== 0)
818 return initialSquare
+ "S";
819 let notation
= undefined;
820 if (move.appear
[0].p
== V
.PAWN
) {
821 // Pawn: generally ambiguous short notation, so we use full description
822 notation
= "P" + initialSquare
+ finalSquare
;
823 } else if (move.appear
[0].p
== V
.KING
)
824 notation
= "K" + (move.vanish
.length
> 1 ? "x" : "") + finalSquare
;
825 else notation
= move.appear
[0].p
.toUpperCase() + finalSquare
;
826 // Add a capture mark (not describing what is captured...):
827 if (move.vanish
.length
> 1 && move.appear
[0].p
!= V
.KING
) notation
+= "X";