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