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