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