Fix Shogi.js
[vchess.git] / client / src / variants / Shogi.js
CommitLineData
cd49e617
BA
1import { ChessRules, PiPo, Move } from "@/base_rules";
2import { ArrayFun } from "@/utils/array";
b4f2488a 3import { sample, shuffle } from "@/utils/alea";
cd49e617
BA
4
5export class ShogiRules extends ChessRules {
7e8a7ea1 6
cd49e617
BA
7 static get HasFlags() {
8 return false;
9 }
10
11 static get HasEnpassant() {
12 return false;
13 }
14
107dc1bd
BA
15 static get Monochrome() {
16 return true;
17 }
18
ded43c88
BA
19 get showFirstTurn() {
20 return true;
21 }
22
b4f2488a
BA
23 static get Notoodark() {
24 return true;
25 }
26
809ab1a8
BA
27 loseOnRepetition() {
28 // If current side is under check: lost
29 return this.underCheck(this.turn);
30 }
31
cd49e617
BA
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 }
e2f204ed 56 static get LANCE() {
cd49e617
BA
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 }
e2f204ed 70 static get P_LANCE() {
cd49e617
BA
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,
e2f204ed 89 V.LANCE,
cd49e617
BA
90 V.P_PAWN,
91 V.P_KNIGHT,
92 V.P_SILVER,
e2f204ed 93 V.P_LANCE,
cd49e617
BA
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
4313762d
BA
116 static GenRandInitFen(options) {
117 if (options.randomness == 0) {
ad2494bd
BA
118 return (
119 "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL " +
120 "w 0 00000000000000"
121 );
122 }
b4f2488a
BA
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) };
ad2494bd 127 for (let c of ["w", "b"]) {
4313762d 128 if (c == 'b' && options.randomness == 1) {
b4f2488a
BA
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);
ad2494bd
BA
133 break;
134 }
b4f2488a
BA
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();
ad2494bd 139 }
cd49e617 140 return (
b4f2488a
BA
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 ) +
ad2494bd 166 " w 0 00000000000000"
cd49e617
BA
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]];
edfb07b1 182 counts[7 + i] = this.reserve["b"][V.RESERVE_PIECES[i]];
cd49e617
BA
183 }
184 return counts.join("");
185 }
186
187 setOtherVariables(fen) {
188 super.setOtherVariables(fen);
cd49e617 189 // Also init reserves (used by the interface to show landable pieces)
e50a8025
BA
190 const reserve =
191 V.ParseFen(fen).reserve.split("").map(x => parseInt(x, 10));
cd49e617
BA
192 this.reserve = {
193 w: {
e50a8025
BA
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]
cd49e617
BA
201 },
202 b: {
e50a8025
BA
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]
cd49e617
BA
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 (
e2f204ed 238 [V.PAWN, V.ROOK, V.BISHOP, V.GOLD_G, V.SILVER_G, V.KNIGHT, V.LANCE]
cd49e617
BA
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 (
e2f204ed 264 (i == lastRanks[0] && [V.PAWN, V.KNIGHT, V.LANCE].includes(p)) ||
cd49e617
BA
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);
723262f9
BA
290 const res = (
291 this.underCheck(oppCol) && !this.atLeastOneMove("noReserve")
292 );
cd49e617
BA
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]);
e2f204ed
BA
319 case V.LANCE:
320 return this.getPotentialLanceMoves([x, y]);
cd49e617 321 case V.KING:
6c00a6e5 322 return super.getPotentialKingMoves([x, y]);
cd49e617
BA
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:
e2f204ed 331 case V.P_LANCE:
cd49e617
BA
332 return this.getPotentialGoldMoves([x, y]);
333 }
334 return []; //never reached
335 }
336
337 // Modified to take promotions into account
3972f787 338 getSlideNJumpMoves_opt([x, y], steps, options) {
118cff5c 339 options = options || {};
cd49e617
BA
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);
3972f787 378 return this.getSlideNJumpMoves_opt(
cd49e617
BA
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 (
3972f787 388 this.getSlideNJumpMoves_opt(
cd49e617
BA
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);
3972f787 402 return this.getSlideNJumpMoves_opt(
cd49e617
BA
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);
3972f787 414 return this.getSlideNJumpMoves_opt(
cd49e617
BA
415 sq,
416 [ [forward, 1], [forward, -1] ],
417 {
418 oneStep: true,
419 promote: V.P_KNIGHT,
420 force: true
421 }
422 );
423 }
424
6c7cbfed
BA
425 getPotentialLanceMoves(sq) {
426 const forward = (this.turn == 'w' ? -1 : 1);
3972f787 427 return this.getSlideNJumpMoves_opt(
6c7cbfed 428 sq,
4313762d 429 [ [forward, 0] ],
6c7cbfed
BA
430 {
431 promote: V.P_LANCE,
432 force: true
433 }
434 );
435 }
436
cd49e617 437 getPotentialRookMoves(sq) {
3972f787 438 return this.getSlideNJumpMoves_opt(
cd49e617
BA
439 sq, V.steps[V.ROOK], { promote: V.P_ROOK });
440 }
441
442 getPotentialBishopMoves(sq) {
3972f787 443 return this.getSlideNJumpMoves_opt(
cd49e617
BA
444 sq, V.steps[V.BISHOP], { promote: V.P_BISHOP });
445 }
446
cd49e617
BA
447 getPotentialDragonMoves(sq) {
448 return (
3972f787
BA
449 this.getSlideNJumpMoves_opt(sq, V.steps[V.ROOK]).concat(
450 this.getSlideNJumpMoves_opt(sq, V.steps[V.BISHOP], { oneStep: true }))
cd49e617
BA
451 );
452 }
453
454 getPotentialHorseMoves(sq) {
455 return (
3972f787
BA
456 this.getSlideNJumpMoves_opt(sq, V.steps[V.BISHOP]).concat(
457 this.getSlideNJumpMoves_opt(sq, V.steps[V.ROOK], { oneStep: true }))
cd49e617
BA
458 );
459 }
460
cd49e617
BA
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) ||
e2f204ed 469 this.isAttackedByLance(sq, color) ||
cd49e617
BA
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 &&
e2f204ed 484 [V.GOLD_G, V.P_PAWN, V.P_SILVER, V.P_KNIGHT, V.P_LANCE]
cd49e617
BA
485 .includes(this.getPiece(i, j))
486 ) {
487 return true;
488 }
489 }
490 return false;
491 }
492
45731a7c 493 isAttackedBySilver(sq, color) {
cd49e617 494 const shift = (color == 'w' ? 1 : -1);
4313762d
BA
495 return this.isAttackedBySlideNJump(
496 sq, color, V.SILVER, V.steps[V.BISHOP].concat([ [shift, 0] ]), 1);
cd49e617
BA
497 }
498
45731a7c 499 isAttackedByPawn(sq, color) {
cd49e617 500 const shift = (color == 'w' ? 1 : -1);
4313762d 501 return this.isAttackedBySlideNJump(sq, color, V.PAWN, [ [shift, 0] ], 1);
cd49e617
BA
502 }
503
504 isAttackedByKnight(sq, color) {
505 const forward = (color == 'w' ? 2 : -2);
506 return this.isAttackedBySlideNJump(
4313762d 507 sq, color, V.KNIGHT, [ [forward, 1], [forward, -1] ], 1);
cd49e617
BA
508 }
509
e2f204ed 510 isAttackedByLance(sq, color) {
cd49e617 511 const forward = (color == 'w' ? 1 : -1);
e2f204ed 512 return this.isAttackedBySlideNJump(sq, color, V.LANCE, [[forward, 0]]);
cd49e617
BA
513 }
514
515 isAttackedByDragon(sq, color) {
516 return (
517 this.isAttackedBySlideNJump(sq, color, V.P_ROOK, V.steps[V.ROOK]) ||
4313762d 518 this.isAttackedBySlideNJump(sq, color, V.P_ROOK, V.steps[V.BISHOP], 1)
cd49e617
BA
519 );
520 }
521
522 isAttackedByHorse(sq, color) {
523 return (
524 this.isAttackedBySlideNJump(sq, color, V.P_BISHOP, V.steps[V.BISHOP]) ||
4313762d 525 this.isAttackedBySlideNJump(sq, color, V.P_BISHOP, V.steps[V.ROOK], 1)
cd49e617
BA
526 );
527 }
528
3972f787
BA
529 filterValid(moves) {
530 if (moves.length == 0) return [];
531 const color = this.turn;
532 const lastRanks = (color == 'w' ? [0, 1] : [8, 7]);
533 return moves.filter(m => {
534 if (
535 (m.appear[0].p == V.KNIGHT && lastRanks.includes(m.end.x)) ||
536 ([V.PAWN, V.LANCE].includes(m.appear[0].p) && lastRanks[0] == m.end.x)
537 ) {
538 // Forbid moves resulting in a blocked piece
539 return false;
540 }
541 this.play(m);
542 const res = !this.underCheck(color);
543 this.undo(m);
544 return res;
545 });
546 }
547
cd49e617
BA
548 getAllValidMoves() {
549 let moves = super.getAllPotentialMoves();
550 const color = this.turn;
551 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
552 moves = moves.concat(
553 this.getReserveMoves([V.size.x + (color == "w" ? 0 : 1), i])
554 );
555 }
556 return this.filterValid(moves);
557 }
558
723262f9 559 atLeastOneMove(noReserve) {
cd49e617 560 if (!super.atLeastOneMove()) {
723262f9
BA
561 if (!noReserve) {
562 // Search one reserve move
563 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
564 let moves = this.filterValid(
565 this.getReserveMoves([V.size.x + (this.turn == "w" ? 0 : 1), i])
566 );
567 if (moves.length > 0) return true;
568 }
cd49e617
BA
569 }
570 return false;
571 }
572 return true;
573 }
574
575 static get P_CORRESPONDANCES() {
576 return {
577 q: 'p',
578 o: 'n',
579 t: 's',
580 m: 'l',
581 d: 'r',
582 h: 'b'
583 };
584 }
585
586 static MayDecode(piece) {
587 if (Object.keys(V.P_CORRESPONDANCES).includes(piece))
588 return V.P_CORRESPONDANCES[piece];
589 return piece;
590 }
591
592 postPlay(move) {
593 super.postPlay(move);
594 const color = move.appear[0].c;
595 if (move.vanish.length == 0)
596 // Drop unpromoted piece:
597 this.reserve[color][move.appear[0].p]--;
598 else if (move.vanish.length == 2)
599 // May capture a promoted piece:
600 this.reserve[color][V.MayDecode(move.vanish[1].p)]++;
601 }
602
603 postUndo(move) {
604 super.postUndo(move);
605 const color = this.turn;
606 if (move.vanish.length == 0)
607 this.reserve[color][move.appear[0].p]++;
608 else if (move.vanish.length == 2)
609 this.reserve[color][V.MayDecode(move.vanish[1].p)]--;
610 }
611
612 static get SEARCH_DEPTH() {
613 return 2;
614 }
615
616 static get VALUES() {
617 // TODO: very arbitrary and wrong
618 return {
619 p: 1,
620 q: 3,
621 r: 5,
622 d: 6,
623 n: 2,
624 o: 3,
625 b: 3,
626 h: 4,
627 s: 3,
628 t: 3,
629 l: 2,
630 m: 3,
631 g: 3,
632 k: 1000,
633 }
634 }
635
636 evalPosition() {
637 let evaluation = super.evalPosition();
638 // Add reserves:
639 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
640 const p = V.RESERVE_PIECES[i];
641 evaluation += this.reserve["w"][p] * V.VALUES[p];
642 evaluation -= this.reserve["b"][p] * V.VALUES[p];
643 }
644 return evaluation;
645 }
646
647 getNotation(move) {
648 const finalSquare = V.CoordsToSquare(move.end);
649 if (move.vanish.length == 0) {
650 // Rebirth:
651 const piece = move.appear[0].p.toUpperCase();
652 return (piece != 'P' ? piece : "") + "@" + finalSquare;
653 }
654 const piece = move.vanish[0].p.toUpperCase();
655 return (
656 (piece != 'P' || move.vanish.length == 2 ? piece : "") +
657 (move.vanish.length == 2 ? "x" : "") +
658 finalSquare +
659 (
660 move.appear[0].p != move.vanish[0].p
661 ? "=" + move.appear[0].p.toUpperCase()
662 : ""
663 )
664 );
665 }
7e8a7ea1 666
cd49e617 667};