Experimental loss on repetition for Shogi and Pandemonium. Simplify Crazyhouse, with...
[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
ad2494bd
BA
116 static GenRandInitFen(randomness) {
117 if (randomness == 0) {
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
BA
127 for (let c of ["w", "b"]) {
128 if (c == 'b' && 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
338 getSlideNJumpMoves([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);
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
6c7cbfed
BA
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
cd49e617
BA
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
cd49e617
BA
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
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
493 isAttackedBySilver([x, y], color) {
494 const shift = (color == 'w' ? 1 : -1);
495 for (let step of V.steps[V.BISHOP].concat([[shift, 0]])) {
496 const [i, j] = [x + step[0], y + step[1]];
497 if (
498 V.OnBoard(i, j) &&
499 this.board[i][j] != V.EMPTY &&
500 this.getColor(i, j) == color &&
501 this.getPiece(i, j) == V.SILVER_G
502 ) {
503 return true;
504 }
505 }
506 return false;
507 }
508
509 isAttackedByPawn([x, y], color) {
510 const shift = (color == 'w' ? 1 : -1);
511 const [i, j] = [x + shift, y];
512 return (
513 V.OnBoard(i, j) &&
514 this.board[i][j] != V.EMPTY &&
515 this.getColor(i, j) == color &&
516 this.getPiece(i, j) == V.PAWN
517 );
518 }
519
520 isAttackedByKnight(sq, color) {
521 const forward = (color == 'w' ? 2 : -2);
522 return this.isAttackedBySlideNJump(
523 sq, color, V.KNIGHT, [[forward, 1], [forward, -1]], "oneStep");
524 }
525
e2f204ed 526 isAttackedByLance(sq, color) {
cd49e617 527 const forward = (color == 'w' ? 1 : -1);
e2f204ed 528 return this.isAttackedBySlideNJump(sq, color, V.LANCE, [[forward, 0]]);
cd49e617
BA
529 }
530
531 isAttackedByDragon(sq, color) {
532 return (
533 this.isAttackedBySlideNJump(sq, color, V.P_ROOK, V.steps[V.ROOK]) ||
534 this.isAttackedBySlideNJump(
edfb07b1 535 sq, color, V.P_ROOK, V.steps[V.BISHOP], "oneStep")
cd49e617
BA
536 );
537 }
538
539 isAttackedByHorse(sq, color) {
540 return (
541 this.isAttackedBySlideNJump(sq, color, V.P_BISHOP, V.steps[V.BISHOP]) ||
542 this.isAttackedBySlideNJump(
edfb07b1 543 sq, color, V.P_BISHOP, V.steps[V.ROOK], "oneStep")
cd49e617
BA
544 );
545 }
546
547 getAllValidMoves() {
548 let moves = super.getAllPotentialMoves();
549 const color = this.turn;
550 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
551 moves = moves.concat(
552 this.getReserveMoves([V.size.x + (color == "w" ? 0 : 1), i])
553 );
554 }
555 return this.filterValid(moves);
556 }
557
723262f9 558 atLeastOneMove(noReserve) {
cd49e617 559 if (!super.atLeastOneMove()) {
723262f9
BA
560 if (!noReserve) {
561 // Search one reserve move
562 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
563 let moves = this.filterValid(
564 this.getReserveMoves([V.size.x + (this.turn == "w" ? 0 : 1), i])
565 );
566 if (moves.length > 0) return true;
567 }
cd49e617
BA
568 }
569 return false;
570 }
571 return true;
572 }
573
574 static get P_CORRESPONDANCES() {
575 return {
576 q: 'p',
577 o: 'n',
578 t: 's',
579 m: 'l',
580 d: 'r',
581 h: 'b'
582 };
583 }
584
585 static MayDecode(piece) {
586 if (Object.keys(V.P_CORRESPONDANCES).includes(piece))
587 return V.P_CORRESPONDANCES[piece];
588 return piece;
589 }
590
591 postPlay(move) {
592 super.postPlay(move);
593 const color = move.appear[0].c;
594 if (move.vanish.length == 0)
595 // Drop unpromoted piece:
596 this.reserve[color][move.appear[0].p]--;
597 else if (move.vanish.length == 2)
598 // May capture a promoted piece:
599 this.reserve[color][V.MayDecode(move.vanish[1].p)]++;
600 }
601
602 postUndo(move) {
603 super.postUndo(move);
604 const color = this.turn;
605 if (move.vanish.length == 0)
606 this.reserve[color][move.appear[0].p]++;
607 else if (move.vanish.length == 2)
608 this.reserve[color][V.MayDecode(move.vanish[1].p)]--;
609 }
610
611 static get SEARCH_DEPTH() {
612 return 2;
613 }
614
615 static get VALUES() {
616 // TODO: very arbitrary and wrong
617 return {
618 p: 1,
619 q: 3,
620 r: 5,
621 d: 6,
622 n: 2,
623 o: 3,
624 b: 3,
625 h: 4,
626 s: 3,
627 t: 3,
628 l: 2,
629 m: 3,
630 g: 3,
631 k: 1000,
632 }
633 }
634
635 evalPosition() {
636 let evaluation = super.evalPosition();
637 // Add reserves:
638 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
639 const p = V.RESERVE_PIECES[i];
640 evaluation += this.reserve["w"][p] * V.VALUES[p];
641 evaluation -= this.reserve["b"][p] * V.VALUES[p];
642 }
643 return evaluation;
644 }
645
646 getNotation(move) {
647 const finalSquare = V.CoordsToSquare(move.end);
648 if (move.vanish.length == 0) {
649 // Rebirth:
650 const piece = move.appear[0].p.toUpperCase();
651 return (piece != 'P' ? piece : "") + "@" + finalSquare;
652 }
653 const piece = move.vanish[0].p.toUpperCase();
654 return (
655 (piece != 'P' || move.vanish.length == 2 ? piece : "") +
656 (move.vanish.length == 2 ? "x" : "") +
657 finalSquare +
658 (
659 move.appear[0].p != move.vanish[0].p
660 ? "=" + move.appear[0].p.toUpperCase()
661 : ""
662 )
663 );
664 }
7e8a7ea1 665
cd49e617 666};