Fix display of Hiddenqueen diags in corr games
[vchess.git] / client / src / variants / Shogi.js
1 import { ChessRules, PiPo, Move } from "@/base_rules";
2 import { ArrayFun } from "@/utils/array";
3 import { sample, shuffle } from "@/utils/alea";
4
5 export class ShogiRules extends ChessRules {
6
7 static get HasFlags() {
8 return false;
9 }
10
11 static get HasEnpassant() {
12 return false;
13 }
14
15 static get Monochrome() {
16 return true;
17 }
18
19 get showFirstTurn() {
20 return true;
21 }
22
23 static get Notoodark() {
24 return true;
25 }
26
27 loseOnRepetition() {
28 // If current side is under check: lost
29 return this.underCheck(this.turn);
30 }
31
32 static IsGoodFen(fen) {
33 if (!ChessRules.IsGoodFen(fen)) return false;
34 const fenParsed = V.ParseFen(fen);
35 // 3) Check reserves
36 if (!fenParsed.reserve || !fenParsed.reserve.match(/^[0-9]{14,14}$/))
37 return false;
38 return true;
39 }
40
41 static ParseFen(fen) {
42 const fenParts = fen.split(" ");
43 return Object.assign(
44 ChessRules.ParseFen(fen),
45 { reserve: fenParts[3] }
46 );
47 }
48
49 // pawns, rooks, knights, bishops and king kept from ChessRules
50 static get GOLD_G() {
51 return "g";
52 }
53 static get SILVER_G() {
54 return "s";
55 }
56 static get LANCE() {
57 return "l";
58 }
59
60 // Promoted pieces:
61 static get P_PAWN() {
62 return 'q';
63 }
64 static get P_KNIGHT() {
65 return 'o';
66 }
67 static get P_SILVER() {
68 return 't';
69 }
70 static get P_LANCE() {
71 return 'm';
72 }
73 static get P_ROOK() {
74 return 'd';
75 }
76 static get P_BISHOP() {
77 return 'h';
78 }
79
80 static get PIECES() {
81 return [
82 ChessRules.PAWN,
83 ChessRules.ROOK,
84 ChessRules.KNIGHT,
85 ChessRules.BISHOP,
86 ChessRules.KING,
87 V.GOLD_G,
88 V.SILVER_G,
89 V.LANCE,
90 V.P_PAWN,
91 V.P_KNIGHT,
92 V.P_SILVER,
93 V.P_LANCE,
94 V.P_ROOK,
95 V.P_BISHOP
96 ];
97 }
98
99 getPpath(b, color, score, orientation) {
100 // 'i' for "inversed":
101 const suffix = (b[0] == orientation ? "" : "i");
102 return "Shogi/" + b + suffix;
103 }
104
105 getPPpath(m, orientation) {
106 return (
107 this.getPpath(
108 m.appear[0].c + m.appear[0].p,
109 null,
110 null,
111 orientation
112 )
113 );
114 }
115
116 static GenRandInitFen(options) {
117 if (options.randomness == 0) {
118 return (
119 "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL " +
120 "w 0 00000000000000"
121 );
122 }
123 // Randomization following these indications:
124 // http://www.shogi.net/shogi-l/Archive/2007/Nmar16-02.txt
125 let pieces1 = { w: new Array(4), b: new Array(4) };
126 let positions2 = { w: new Array(2), b: new Array(2) };
127 for (let c of ["w", "b"]) {
128 if (c == 'b' && options.randomness == 1) {
129 pieces1['b'] = JSON.parse(JSON.stringify(pieces1['w'])).reverse();
130 positions2['b'] =
131 JSON.parse(JSON.stringify(positions2['w'])).reverse()
132 .map(p => 8 - p);
133 break;
134 }
135 let positions = shuffle(ArrayFun.range(4));
136 const composition = ['s', 's', 'g', 'g'];
137 for (let i = 0; i < 4; i++) pieces1[c][positions[i]] = composition[i];
138 positions2[c] = sample(ArrayFun.range(9), 2).sort();
139 }
140 return (
141 (
142 "ln" +
143 pieces1["b"].slice(0, 2).join("") +
144 "k" +
145 pieces1["b"].slice(2, 4).join("") +
146 "nl/"
147 ) +
148 (
149 (positions2['b'][0] || "") + 'r' +
150 (positions2['b'][1] - positions2['b'][0] - 1 || "") + 'b' +
151 (8 - positions2['b'][1] || "")
152 ) +
153 "/ppppppppp/9/9/9/PPPPPPPPP/" +
154 (
155 (positions2['w'][0] || "") + 'B' +
156 (positions2['w'][1] - positions2['w'][0] - 1 || "") + 'R' +
157 (8 - positions2['w'][1] || "")
158 ) +
159 (
160 "/LN" +
161 pieces1["w"].slice(0, 2).join("").toUpperCase() +
162 "K" +
163 pieces1["w"].slice(2, 4).join("").toUpperCase() +
164 "NL"
165 ) +
166 " w 0 00000000000000"
167 );
168 }
169
170 getFen() {
171 return super.getFen() + " " + this.getReserveFen();
172 }
173
174 getFenForRepeat() {
175 return super.getFenForRepeat() + "_" + this.getReserveFen();
176 }
177
178 getReserveFen() {
179 let counts = new Array(14);
180 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
181 counts[i] = this.reserve["w"][V.RESERVE_PIECES[i]];
182 counts[7 + i] = this.reserve["b"][V.RESERVE_PIECES[i]];
183 }
184 return counts.join("");
185 }
186
187 setOtherVariables(fen) {
188 super.setOtherVariables(fen);
189 // Also init reserves (used by the interface to show landable pieces)
190 const reserve =
191 V.ParseFen(fen).reserve.split("").map(x => parseInt(x, 10));
192 this.reserve = {
193 w: {
194 [V.PAWN]: reserve[0],
195 [V.ROOK]: reserve[1],
196 [V.BISHOP]: reserve[2],
197 [V.GOLD_G]: reserve[3],
198 [V.SILVER_G]: reserve[4],
199 [V.KNIGHT]: reserve[5],
200 [V.LANCE]: reserve[6]
201 },
202 b: {
203 [V.PAWN]: reserve[7],
204 [V.ROOK]: reserve[8],
205 [V.BISHOP]: reserve[9],
206 [V.GOLD_G]: reserve[10],
207 [V.SILVER_G]: reserve[11],
208 [V.KNIGHT]: reserve[12],
209 [V.LANCE]: reserve[13]
210 }
211 };
212 }
213
214 getColor(i, j) {
215 if (i >= V.size.x) return i == V.size.x ? "w" : "b";
216 return this.board[i][j].charAt(0);
217 }
218
219 getPiece(i, j) {
220 if (i >= V.size.x) return V.RESERVE_PIECES[j];
221 return this.board[i][j].charAt(1);
222 }
223
224 static get size() {
225 return { x: 9, y: 9};
226 }
227
228 getReservePpath(index, color, orientation) {
229 return (
230 "Shogi/" + color + V.RESERVE_PIECES[index] +
231 (color != orientation ? 'i' : '')
232 );
233 }
234
235 // Ordering on reserve pieces
236 static get RESERVE_PIECES() {
237 return (
238 [V.PAWN, V.ROOK, V.BISHOP, V.GOLD_G, V.SILVER_G, V.KNIGHT, V.LANCE]
239 );
240 }
241
242 getReserveMoves([x, y]) {
243 const color = this.turn;
244 const p = V.RESERVE_PIECES[y];
245 if (p == V.PAWN) {
246 var oppCol = V.GetOppCol(color);
247 var allowedFiles =
248 [...Array(9).keys()].filter(j =>
249 [...Array(9).keys()].every(i => {
250 return (
251 this.board[i][j] == V.EMPTY ||
252 this.getColor(i, j) != color ||
253 this.getPiece(i, j) != V.PAWN
254 );
255 })
256 )
257 }
258 if (this.reserve[color][p] == 0) return [];
259 let moves = [];
260 const forward = color == 'w' ? -1 : 1;
261 const lastRanks = color == 'w' ? [0, 1] : [8, 7];
262 for (let i = 0; i < V.size.x; i++) {
263 if (
264 (i == lastRanks[0] && [V.PAWN, V.KNIGHT, V.LANCE].includes(p)) ||
265 (i == lastRanks[1] && p == V.KNIGHT)
266 ) {
267 continue;
268 }
269 for (let j = 0; j < V.size.y; j++) {
270 if (
271 this.board[i][j] == V.EMPTY &&
272 (p != V.PAWN || allowedFiles.includes(j))
273 ) {
274 let mv = new Move({
275 appear: [
276 new PiPo({
277 x: i,
278 y: j,
279 c: color,
280 p: p
281 })
282 ],
283 vanish: [],
284 start: { x: x, y: y }, //a bit artificial...
285 end: { x: i, y: j }
286 });
287 if (p == V.PAWN) {
288 // Do not drop on checkmate:
289 this.play(mv);
290 const res = (
291 this.underCheck(oppCol) && !this.atLeastOneMove("noReserve")
292 );
293 this.undo(mv);
294 if (res) continue;
295 }
296 moves.push(mv);
297 }
298 }
299 }
300 return moves;
301 }
302
303 getPotentialMovesFrom([x, y]) {
304 if (x >= V.size.x) {
305 // Reserves, outside of board: x == sizeX(+1)
306 return this.getReserveMoves([x, y]);
307 }
308 switch (this.getPiece(x, y)) {
309 case V.PAWN:
310 return this.getPotentialPawnMoves([x, y]);
311 case V.ROOK:
312 return this.getPotentialRookMoves([x, y]);
313 case V.KNIGHT:
314 return this.getPotentialKnightMoves([x, y]);
315 case V.BISHOP:
316 return this.getPotentialBishopMoves([x, y]);
317 case V.SILVER_G:
318 return this.getPotentialSilverMoves([x, y]);
319 case V.LANCE:
320 return this.getPotentialLanceMoves([x, y]);
321 case V.KING:
322 return super.getPotentialKingMoves([x, y]);
323 case V.P_ROOK:
324 return this.getPotentialDragonMoves([x, y]);
325 case V.P_BISHOP:
326 return this.getPotentialHorseMoves([x, y]);
327 case V.GOLD_G:
328 case V.P_PAWN:
329 case V.P_SILVER:
330 case V.P_KNIGHT:
331 case V.P_LANCE:
332 return this.getPotentialGoldMoves([x, y]);
333 }
334 return []; //never reached
335 }
336
337 // Modified to take promotions into account
338 getSlideNJumpMoves([x, y], steps, options) {
339 options = options || {};
340 const color = this.turn;
341 const oneStep = options.oneStep;
342 const forcePromoteOnLastRank = options.force;
343 const promoteInto = options.promote;
344 const lastRanks = (color == 'w' ? [0, 1, 2] : [9, 8, 7]);
345 let moves = [];
346 outerLoop: for (let step of steps) {
347 let i = x + step[0];
348 let j = y + step[1];
349 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
350 if (i != lastRanks[0] || !forcePromoteOnLastRank)
351 moves.push(this.getBasicMove([x, y], [i, j]));
352 if (!!promoteInto && lastRanks.includes(i)) {
353 moves.push(
354 this.getBasicMove(
355 [x, y], [i, j], { c: color, p: promoteInto })
356 );
357 }
358 if (oneStep) continue outerLoop;
359 i += step[0];
360 j += step[1];
361 }
362 if (V.OnBoard(i, j) && this.canTake([x, y], [i, j])) {
363 if (i != lastRanks[0] || !forcePromoteOnLastRank)
364 moves.push(this.getBasicMove([x, y], [i, j]));
365 if (!!promoteInto && lastRanks.includes(i)) {
366 moves.push(
367 this.getBasicMove(
368 [x, y], [i, j], { c: color, p: promoteInto })
369 );
370 }
371 }
372 }
373 return moves;
374 }
375
376 getPotentialGoldMoves(sq) {
377 const forward = (this.turn == 'w' ? -1 : 1);
378 return this.getSlideNJumpMoves(
379 sq,
380 V.steps[V.ROOK].concat([ [forward, 1], [forward, -1] ]),
381 { oneStep: true }
382 );
383 }
384
385 getPotentialPawnMoves(sq) {
386 const forward = (this.turn == 'w' ? -1 : 1);
387 return (
388 this.getSlideNJumpMoves(
389 sq,
390 [[forward, 0]],
391 {
392 oneStep: true,
393 promote: V.P_PAWN,
394 force: true
395 }
396 )
397 );
398 }
399
400 getPotentialSilverMoves(sq) {
401 const forward = (this.turn == 'w' ? -1 : 1);
402 return this.getSlideNJumpMoves(
403 sq,
404 V.steps[V.BISHOP].concat([ [forward, 0] ]),
405 {
406 oneStep: true,
407 promote: V.P_SILVER
408 }
409 );
410 }
411
412 getPotentialKnightMoves(sq) {
413 const forward = (this.turn == 'w' ? -2 : 2);
414 return this.getSlideNJumpMoves(
415 sq,
416 [ [forward, 1], [forward, -1] ],
417 {
418 oneStep: true,
419 promote: V.P_KNIGHT,
420 force: true
421 }
422 );
423 }
424
425 getPotentialLanceMoves(sq) {
426 const forward = (this.turn == 'w' ? -1 : 1);
427 return this.getSlideNJumpMoves(
428 sq,
429 [ [forward, 0] ],
430 {
431 promote: V.P_LANCE,
432 force: true
433 }
434 );
435 }
436
437 getPotentialRookMoves(sq) {
438 return this.getSlideNJumpMoves(
439 sq, V.steps[V.ROOK], { promote: V.P_ROOK });
440 }
441
442 getPotentialBishopMoves(sq) {
443 return this.getSlideNJumpMoves(
444 sq, V.steps[V.BISHOP], { promote: V.P_BISHOP });
445 }
446
447 getPotentialDragonMoves(sq) {
448 return (
449 this.getSlideNJumpMoves(sq, V.steps[V.ROOK]).concat(
450 this.getSlideNJumpMoves(sq, V.steps[V.BISHOP], { oneStep: true }))
451 );
452 }
453
454 getPotentialHorseMoves(sq) {
455 return (
456 this.getSlideNJumpMoves(sq, V.steps[V.BISHOP]).concat(
457 this.getSlideNJumpMoves(sq, V.steps[V.ROOK], { oneStep: true }))
458 );
459 }
460
461 isAttacked(sq, color) {
462 return (
463 this.isAttackedByPawn(sq, color) ||
464 this.isAttackedByRook(sq, color) ||
465 this.isAttackedByDragon(sq, color) ||
466 this.isAttackedByKnight(sq, color) ||
467 this.isAttackedByBishop(sq, color) ||
468 this.isAttackedByHorse(sq, color) ||
469 this.isAttackedByLance(sq, color) ||
470 this.isAttackedBySilver(sq, color) ||
471 this.isAttackedByGold(sq, color) ||
472 this.isAttackedByKing(sq, color)
473 );
474 }
475
476 isAttackedByGold([x, y], color) {
477 const shift = (color == 'w' ? 1 : -1);
478 for (let step of V.steps[V.ROOK].concat([[shift, 1], [shift, -1]])) {
479 const [i, j] = [x + step[0], y + step[1]];
480 if (
481 V.OnBoard(i, j) &&
482 this.board[i][j] != V.EMPTY &&
483 this.getColor(i, j) == color &&
484 [V.GOLD_G, V.P_PAWN, V.P_SILVER, V.P_KNIGHT, V.P_LANCE]
485 .includes(this.getPiece(i, j))
486 ) {
487 return true;
488 }
489 }
490 return false;
491 }
492
493 isAttackedBySilver([x, y], color) {
494 const shift = (color == 'w' ? 1 : -1);
495 return this.isAttackedBySlideNJump(
496 sq, color, V.SILVER, V.steps[V.BISHOP].concat([ [shift, 0] ]), 1);
497 }
498
499 isAttackedByPawn([x, y], color) {
500 const shift = (color == 'w' ? 1 : -1);
501 return this.isAttackedBySlideNJump(sq, color, V.PAWN, [ [shift, 0] ], 1);
502 }
503
504 isAttackedByKnight(sq, color) {
505 const forward = (color == 'w' ? 2 : -2);
506 return this.isAttackedBySlideNJump(
507 sq, color, V.KNIGHT, [ [forward, 1], [forward, -1] ], 1);
508 }
509
510 isAttackedByLance(sq, color) {
511 const forward = (color == 'w' ? 1 : -1);
512 return this.isAttackedBySlideNJump(sq, color, V.LANCE, [[forward, 0]]);
513 }
514
515 isAttackedByDragon(sq, color) {
516 return (
517 this.isAttackedBySlideNJump(sq, color, V.P_ROOK, V.steps[V.ROOK]) ||
518 this.isAttackedBySlideNJump(sq, color, V.P_ROOK, V.steps[V.BISHOP], 1)
519 );
520 }
521
522 isAttackedByHorse(sq, color) {
523 return (
524 this.isAttackedBySlideNJump(sq, color, V.P_BISHOP, V.steps[V.BISHOP]) ||
525 this.isAttackedBySlideNJump(sq, color, V.P_BISHOP, V.steps[V.ROOK], 1)
526 );
527 }
528
529 getAllValidMoves() {
530 let moves = super.getAllPotentialMoves();
531 const color = this.turn;
532 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
533 moves = moves.concat(
534 this.getReserveMoves([V.size.x + (color == "w" ? 0 : 1), i])
535 );
536 }
537 return this.filterValid(moves);
538 }
539
540 atLeastOneMove(noReserve) {
541 if (!super.atLeastOneMove()) {
542 if (!noReserve) {
543 // Search one reserve move
544 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
545 let moves = this.filterValid(
546 this.getReserveMoves([V.size.x + (this.turn == "w" ? 0 : 1), i])
547 );
548 if (moves.length > 0) return true;
549 }
550 }
551 return false;
552 }
553 return true;
554 }
555
556 static get P_CORRESPONDANCES() {
557 return {
558 q: 'p',
559 o: 'n',
560 t: 's',
561 m: 'l',
562 d: 'r',
563 h: 'b'
564 };
565 }
566
567 static MayDecode(piece) {
568 if (Object.keys(V.P_CORRESPONDANCES).includes(piece))
569 return V.P_CORRESPONDANCES[piece];
570 return piece;
571 }
572
573 postPlay(move) {
574 super.postPlay(move);
575 const color = move.appear[0].c;
576 if (move.vanish.length == 0)
577 // Drop unpromoted piece:
578 this.reserve[color][move.appear[0].p]--;
579 else if (move.vanish.length == 2)
580 // May capture a promoted piece:
581 this.reserve[color][V.MayDecode(move.vanish[1].p)]++;
582 }
583
584 postUndo(move) {
585 super.postUndo(move);
586 const color = this.turn;
587 if (move.vanish.length == 0)
588 this.reserve[color][move.appear[0].p]++;
589 else if (move.vanish.length == 2)
590 this.reserve[color][V.MayDecode(move.vanish[1].p)]--;
591 }
592
593 static get SEARCH_DEPTH() {
594 return 2;
595 }
596
597 static get VALUES() {
598 // TODO: very arbitrary and wrong
599 return {
600 p: 1,
601 q: 3,
602 r: 5,
603 d: 6,
604 n: 2,
605 o: 3,
606 b: 3,
607 h: 4,
608 s: 3,
609 t: 3,
610 l: 2,
611 m: 3,
612 g: 3,
613 k: 1000,
614 }
615 }
616
617 evalPosition() {
618 let evaluation = super.evalPosition();
619 // Add reserves:
620 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
621 const p = V.RESERVE_PIECES[i];
622 evaluation += this.reserve["w"][p] * V.VALUES[p];
623 evaluation -= this.reserve["b"][p] * V.VALUES[p];
624 }
625 return evaluation;
626 }
627
628 getNotation(move) {
629 const finalSquare = V.CoordsToSquare(move.end);
630 if (move.vanish.length == 0) {
631 // Rebirth:
632 const piece = move.appear[0].p.toUpperCase();
633 return (piece != 'P' ? piece : "") + "@" + finalSquare;
634 }
635 const piece = move.vanish[0].p.toUpperCase();
636 return (
637 (piece != 'P' || move.vanish.length == 2 ? piece : "") +
638 (move.vanish.length == 2 ? "x" : "") +
639 finalSquare +
640 (
641 move.appear[0].p != move.vanish[0].p
642 ? "=" + move.appear[0].p.toUpperCase()
643 : ""
644 )
645 );
646 }
647
648 };