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