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