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