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