Fix Dice chess
[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 const fenParsed = V.ParseFen(fen);
180 // Also init reserves (used by the interface to show landable pieces)
181 this.reserve = {
182 w: {
183 [V.PAWN]: parseInt(fenParsed.reserve[0]),
184 [V.ROOK]: parseInt(fenParsed.reserve[1]),
185 [V.BISHOP]: parseInt(fenParsed.reserve[2]),
186 [V.GOLD_G]: parseInt(fenParsed.reserve[3]),
187 [V.SILVER_G]: parseInt(fenParsed.reserve[4]),
188 [V.KNIGHT]: parseInt(fenParsed.reserve[5]),
189 [V.LANCE]: parseInt(fenParsed.reserve[6])
190 },
191 b: {
192 [V.PAWN]: parseInt(fenParsed.reserve[7]),
193 [V.ROOK]: parseInt(fenParsed.reserve[8]),
194 [V.BISHOP]: parseInt(fenParsed.reserve[9]),
195 [V.GOLD_G]: parseInt(fenParsed.reserve[10]),
196 [V.SILVER_G]: parseInt(fenParsed.reserve[11]),
197 [V.KNIGHT]: parseInt(fenParsed.reserve[12]),
198 [V.LANCE]: parseInt(fenParsed.reserve[13])
199 }
200 };
201 }
202
203 getColor(i, j) {
204 if (i >= V.size.x) return i == V.size.x ? "w" : "b";
205 return this.board[i][j].charAt(0);
206 }
207
208 getPiece(i, j) {
209 if (i >= V.size.x) return V.RESERVE_PIECES[j];
210 return this.board[i][j].charAt(1);
211 }
212
213 static get size() {
214 return { x: 9, y: 9};
215 }
216
217 getReservePpath(index, color, orientation) {
218 return (
219 "Shogi/" + color + V.RESERVE_PIECES[index] +
220 (color != orientation ? 'i' : '')
221 );
222 }
223
224 // Ordering on reserve pieces
225 static get RESERVE_PIECES() {
226 return (
227 [V.PAWN, V.ROOK, V.BISHOP, V.GOLD_G, V.SILVER_G, V.KNIGHT, V.LANCE]
228 );
229 }
230
231 getReserveMoves([x, y]) {
232 const color = this.turn;
233 const p = V.RESERVE_PIECES[y];
234 if (p == V.PAWN) {
235 var oppCol = V.GetOppCol(color);
236 var allowedFiles =
237 [...Array(9).keys()].filter(j =>
238 [...Array(9).keys()].every(i => {
239 return (
240 this.board[i][j] == V.EMPTY ||
241 this.getColor(i, j) != color ||
242 this.getPiece(i, j) != V.PAWN
243 );
244 })
245 )
246 }
247 if (this.reserve[color][p] == 0) return [];
248 let moves = [];
249 const forward = color == 'w' ? -1 : 1;
250 const lastRanks = color == 'w' ? [0, 1] : [8, 7];
251 for (let i = 0; i < V.size.x; i++) {
252 if (
253 (i == lastRanks[0] && [V.PAWN, V.KNIGHT, V.LANCE].includes(p)) ||
254 (i == lastRanks[1] && p == V.KNIGHT)
255 ) {
256 continue;
257 }
258 for (let j = 0; j < V.size.y; j++) {
259 if (
260 this.board[i][j] == V.EMPTY &&
261 (p != V.PAWN || allowedFiles.includes(j))
262 ) {
263 let mv = new Move({
264 appear: [
265 new PiPo({
266 x: i,
267 y: j,
268 c: color,
269 p: p
270 })
271 ],
272 vanish: [],
273 start: { x: x, y: y }, //a bit artificial...
274 end: { x: i, y: j }
275 });
276 if (p == V.PAWN) {
277 // Do not drop on checkmate:
278 this.play(mv);
279 const res = (this.underCheck(oppCol) && !this.atLeastOneMove());
280 this.undo(mv);
281 if (res) continue;
282 }
283 moves.push(mv);
284 }
285 }
286 }
287 return moves;
288 }
289
290 getPotentialMovesFrom([x, y]) {
291 if (x >= V.size.x) {
292 // Reserves, outside of board: x == sizeX(+1)
293 return this.getReserveMoves([x, y]);
294 }
295 switch (this.getPiece(x, y)) {
296 case V.PAWN:
297 return this.getPotentialPawnMoves([x, y]);
298 case V.ROOK:
299 return this.getPotentialRookMoves([x, y]);
300 case V.KNIGHT:
301 return this.getPotentialKnightMoves([x, y]);
302 case V.BISHOP:
303 return this.getPotentialBishopMoves([x, y]);
304 case V.SILVER_G:
305 return this.getPotentialSilverMoves([x, y]);
306 case V.LANCE:
307 return this.getPotentialLanceMoves([x, y]);
308 case V.KING:
309 return this.getPotentialKingMoves([x, y]);
310 case V.P_ROOK:
311 return this.getPotentialDragonMoves([x, y]);
312 case V.P_BISHOP:
313 return this.getPotentialHorseMoves([x, y]);
314 case V.GOLD_G:
315 case V.P_PAWN:
316 case V.P_SILVER:
317 case V.P_KNIGHT:
318 case V.P_LANCE:
319 return this.getPotentialGoldMoves([x, y]);
320 }
321 return []; //never reached
322 }
323
324 // Modified to take promotions into account
325 getSlideNJumpMoves([x, y], steps, options) {
326 options = options || {};
327 const color = this.turn;
328 const oneStep = options.oneStep;
329 const forcePromoteOnLastRank = options.force;
330 const promoteInto = options.promote;
331 const lastRanks = (color == 'w' ? [0, 1, 2] : [9, 8, 7]);
332 let moves = [];
333 outerLoop: for (let step of steps) {
334 let i = x + step[0];
335 let j = y + step[1];
336 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
337 if (i != lastRanks[0] || !forcePromoteOnLastRank)
338 moves.push(this.getBasicMove([x, y], [i, j]));
339 if (!!promoteInto && lastRanks.includes(i)) {
340 moves.push(
341 this.getBasicMove(
342 [x, y], [i, j], { c: color, p: promoteInto })
343 );
344 }
345 if (oneStep) continue outerLoop;
346 i += step[0];
347 j += step[1];
348 }
349 if (V.OnBoard(i, j) && this.canTake([x, y], [i, j])) {
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 }
359 }
360 return moves;
361 }
362
363 getPotentialGoldMoves(sq) {
364 const forward = (this.turn == 'w' ? -1 : 1);
365 return this.getSlideNJumpMoves(
366 sq,
367 V.steps[V.ROOK].concat([ [forward, 1], [forward, -1] ]),
368 { oneStep: true }
369 );
370 }
371
372 getPotentialPawnMoves(sq) {
373 const forward = (this.turn == 'w' ? -1 : 1);
374 return (
375 this.getSlideNJumpMoves(
376 sq,
377 [[forward, 0]],
378 {
379 oneStep: true,
380 promote: V.P_PAWN,
381 force: true
382 }
383 )
384 );
385 }
386
387 getPotentialSilverMoves(sq) {
388 const forward = (this.turn == 'w' ? -1 : 1);
389 return this.getSlideNJumpMoves(
390 sq,
391 V.steps[V.BISHOP].concat([ [forward, 0] ]),
392 {
393 oneStep: true,
394 promote: V.P_SILVER
395 }
396 );
397 }
398
399 getPotentialKnightMoves(sq) {
400 const forward = (this.turn == 'w' ? -2 : 2);
401 return this.getSlideNJumpMoves(
402 sq,
403 [ [forward, 1], [forward, -1] ],
404 {
405 oneStep: true,
406 promote: V.P_KNIGHT,
407 force: true
408 }
409 );
410 }
411
412 getPotentialLanceMoves(sq) {
413 const forward = (this.turn == 'w' ? -1 : 1);
414 return this.getSlideNJumpMoves(
415 sq,
416 [[forward, 0]],
417 {
418 promote: V.P_LANCE,
419 force: true
420 }
421 );
422 }
423
424 getPotentialRookMoves(sq) {
425 return this.getSlideNJumpMoves(
426 sq, V.steps[V.ROOK], { promote: V.P_ROOK });
427 }
428
429 getPotentialBishopMoves(sq) {
430 return this.getSlideNJumpMoves(
431 sq, V.steps[V.BISHOP], { promote: V.P_BISHOP });
432 }
433
434 getPotentialDragonMoves(sq) {
435 return (
436 this.getSlideNJumpMoves(sq, V.steps[V.ROOK]).concat(
437 this.getSlideNJumpMoves(sq, V.steps[V.BISHOP], { oneStep: true }))
438 );
439 }
440
441 getPotentialHorseMoves(sq) {
442 return (
443 this.getSlideNJumpMoves(sq, V.steps[V.BISHOP]).concat(
444 this.getSlideNJumpMoves(sq, V.steps[V.ROOK], { oneStep: true }))
445 );
446 }
447
448 getPotentialKingMoves(sq) {
449 return this.getSlideNJumpMoves(
450 sq,
451 V.steps[V.ROOK].concat(V.steps[V.BISHOP]),
452 { 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() {
554 if (!super.atLeastOneMove()) {
555 // Search one reserve move
556 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
557 let moves = this.filterValid(
558 this.getReserveMoves([V.size.x + (this.turn == "w" ? 0 : 1), i])
559 );
560 if (moves.length > 0) return true;
561 }
562 return false;
563 }
564 return true;
565 }
566
567 static get P_CORRESPONDANCES() {
568 return {
569 q: 'p',
570 o: 'n',
571 t: 's',
572 m: 'l',
573 d: 'r',
574 h: 'b'
575 };
576 }
577
578 static MayDecode(piece) {
579 if (Object.keys(V.P_CORRESPONDANCES).includes(piece))
580 return V.P_CORRESPONDANCES[piece];
581 return piece;
582 }
583
584 postPlay(move) {
585 super.postPlay(move);
586 const color = move.appear[0].c;
587 if (move.vanish.length == 0)
588 // Drop unpromoted piece:
589 this.reserve[color][move.appear[0].p]--;
590 else if (move.vanish.length == 2)
591 // May capture a promoted piece:
592 this.reserve[color][V.MayDecode(move.vanish[1].p)]++;
593 }
594
595 postUndo(move) {
596 super.postUndo(move);
597 const color = this.turn;
598 if (move.vanish.length == 0)
599 this.reserve[color][move.appear[0].p]++;
600 else if (move.vanish.length == 2)
601 this.reserve[color][V.MayDecode(move.vanish[1].p)]--;
602 }
603
604 static get SEARCH_DEPTH() {
605 return 2;
606 }
607
608 static get VALUES() {
609 // TODO: very arbitrary and wrong
610 return {
611 p: 1,
612 q: 3,
613 r: 5,
614 d: 6,
615 n: 2,
616 o: 3,
617 b: 3,
618 h: 4,
619 s: 3,
620 t: 3,
621 l: 2,
622 m: 3,
623 g: 3,
624 k: 1000,
625 }
626 }
627
628 evalPosition() {
629 let evaluation = super.evalPosition();
630 // Add reserves:
631 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
632 const p = V.RESERVE_PIECES[i];
633 evaluation += this.reserve["w"][p] * V.VALUES[p];
634 evaluation -= this.reserve["b"][p] * V.VALUES[p];
635 }
636 return evaluation;
637 }
638
639 getNotation(move) {
640 const finalSquare = V.CoordsToSquare(move.end);
641 if (move.vanish.length == 0) {
642 // Rebirth:
643 const piece = move.appear[0].p.toUpperCase();
644 return (piece != 'P' ? piece : "") + "@" + finalSquare;
645 }
646 const piece = move.vanish[0].p.toUpperCase();
647 return (
648 (piece != 'P' || move.vanish.length == 2 ? piece : "") +
649 (move.vanish.length == 2 ? "x" : "") +
650 finalSquare +
651 (
652 move.appear[0].p != move.vanish[0].p
653 ? "=" + move.appear[0].p.toUpperCase()
654 : ""
655 )
656 );
657 }
658 };