Some fixes. Screen variant computer play is still broken, seemingly
[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
cd49e617
BA
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 }
e2f204ed 51 static get LANCE() {
cd49e617
BA
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 }
e2f204ed 65 static get P_LANCE() {
cd49e617
BA
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,
e2f204ed 84 V.LANCE,
cd49e617
BA
85 V.P_PAWN,
86 V.P_KNIGHT,
87 V.P_SILVER,
e2f204ed 88 V.P_LANCE,
cd49e617
BA
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
ad2494bd
BA
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 }
b4f2488a
BA
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) };
ad2494bd
BA
122 for (let c of ["w", "b"]) {
123 if (c == 'b' && randomness == 1) {
b4f2488a
BA
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);
ad2494bd
BA
128 break;
129 }
b4f2488a
BA
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();
ad2494bd 134 }
cd49e617 135 return (
b4f2488a
BA
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 ) +
ad2494bd 161 " w 0 00000000000000"
cd49e617
BA
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]];
edfb07b1 177 counts[7 + i] = this.reserve["b"][V.RESERVE_PIECES[i]];
cd49e617
BA
178 }
179 return counts.join("");
180 }
181
182 setOtherVariables(fen) {
183 super.setOtherVariables(fen);
cd49e617 184 // Also init reserves (used by the interface to show landable pieces)
e50a8025
BA
185 const reserve =
186 V.ParseFen(fen).reserve.split("").map(x => parseInt(x, 10));
cd49e617
BA
187 this.reserve = {
188 w: {
e50a8025
BA
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]
cd49e617
BA
196 },
197 b: {
e50a8025
BA
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]
cd49e617
BA
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 (
e2f204ed 233 [V.PAWN, V.ROOK, V.BISHOP, V.GOLD_G, V.SILVER_G, V.KNIGHT, V.LANCE]
cd49e617
BA
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 (
e2f204ed 259 (i == lastRanks[0] && [V.PAWN, V.KNIGHT, V.LANCE].includes(p)) ||
cd49e617
BA
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 = (this.underCheck(oppCol) && !this.atLeastOneMove());
286 this.undo(mv);
287 if (res) continue;
288 }
289 moves.push(mv);
290 }
291 }
292 }
293 return moves;
294 }
295
296 getPotentialMovesFrom([x, y]) {
297 if (x >= V.size.x) {
298 // Reserves, outside of board: x == sizeX(+1)
299 return this.getReserveMoves([x, y]);
300 }
301 switch (this.getPiece(x, y)) {
302 case V.PAWN:
303 return this.getPotentialPawnMoves([x, y]);
304 case V.ROOK:
305 return this.getPotentialRookMoves([x, y]);
306 case V.KNIGHT:
307 return this.getPotentialKnightMoves([x, y]);
308 case V.BISHOP:
309 return this.getPotentialBishopMoves([x, y]);
310 case V.SILVER_G:
311 return this.getPotentialSilverMoves([x, y]);
e2f204ed
BA
312 case V.LANCE:
313 return this.getPotentialLanceMoves([x, y]);
cd49e617 314 case V.KING:
6c00a6e5 315 return super.getPotentialKingMoves([x, y]);
cd49e617
BA
316 case V.P_ROOK:
317 return this.getPotentialDragonMoves([x, y]);
318 case V.P_BISHOP:
319 return this.getPotentialHorseMoves([x, y]);
320 case V.GOLD_G:
321 case V.P_PAWN:
322 case V.P_SILVER:
323 case V.P_KNIGHT:
e2f204ed 324 case V.P_LANCE:
cd49e617
BA
325 return this.getPotentialGoldMoves([x, y]);
326 }
327 return []; //never reached
328 }
329
330 // Modified to take promotions into account
331 getSlideNJumpMoves([x, y], steps, options) {
118cff5c 332 options = options || {};
cd49e617
BA
333 const color = this.turn;
334 const oneStep = options.oneStep;
335 const forcePromoteOnLastRank = options.force;
336 const promoteInto = options.promote;
337 const lastRanks = (color == 'w' ? [0, 1, 2] : [9, 8, 7]);
338 let moves = [];
339 outerLoop: for (let step of steps) {
340 let i = x + step[0];
341 let j = y + step[1];
342 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
343 if (i != lastRanks[0] || !forcePromoteOnLastRank)
344 moves.push(this.getBasicMove([x, y], [i, j]));
345 if (!!promoteInto && lastRanks.includes(i)) {
346 moves.push(
347 this.getBasicMove(
348 [x, y], [i, j], { c: color, p: promoteInto })
349 );
350 }
351 if (oneStep) continue outerLoop;
352 i += step[0];
353 j += step[1];
354 }
355 if (V.OnBoard(i, j) && this.canTake([x, y], [i, j])) {
356 if (i != lastRanks[0] || !forcePromoteOnLastRank)
357 moves.push(this.getBasicMove([x, y], [i, j]));
358 if (!!promoteInto && lastRanks.includes(i)) {
359 moves.push(
360 this.getBasicMove(
361 [x, y], [i, j], { c: color, p: promoteInto })
362 );
363 }
364 }
365 }
366 return moves;
367 }
368
369 getPotentialGoldMoves(sq) {
370 const forward = (this.turn == 'w' ? -1 : 1);
371 return this.getSlideNJumpMoves(
372 sq,
373 V.steps[V.ROOK].concat([ [forward, 1], [forward, -1] ]),
374 { oneStep: true }
375 );
376 }
377
378 getPotentialPawnMoves(sq) {
379 const forward = (this.turn == 'w' ? -1 : 1);
380 return (
381 this.getSlideNJumpMoves(
382 sq,
383 [[forward, 0]],
384 {
385 oneStep: true,
386 promote: V.P_PAWN,
387 force: true
388 }
389 )
390 );
391 }
392
393 getPotentialSilverMoves(sq) {
394 const forward = (this.turn == 'w' ? -1 : 1);
395 return this.getSlideNJumpMoves(
396 sq,
397 V.steps[V.BISHOP].concat([ [forward, 0] ]),
398 {
399 oneStep: true,
400 promote: V.P_SILVER
401 }
402 );
403 }
404
405 getPotentialKnightMoves(sq) {
406 const forward = (this.turn == 'w' ? -2 : 2);
407 return this.getSlideNJumpMoves(
408 sq,
409 [ [forward, 1], [forward, -1] ],
410 {
411 oneStep: true,
412 promote: V.P_KNIGHT,
413 force: true
414 }
415 );
416 }
417
6c7cbfed
BA
418 getPotentialLanceMoves(sq) {
419 const forward = (this.turn == 'w' ? -1 : 1);
420 return this.getSlideNJumpMoves(
421 sq,
422 [[forward, 0]],
423 {
424 promote: V.P_LANCE,
425 force: true
426 }
427 );
428 }
429
cd49e617
BA
430 getPotentialRookMoves(sq) {
431 return this.getSlideNJumpMoves(
432 sq, V.steps[V.ROOK], { promote: V.P_ROOK });
433 }
434
435 getPotentialBishopMoves(sq) {
436 return this.getSlideNJumpMoves(
437 sq, V.steps[V.BISHOP], { promote: V.P_BISHOP });
438 }
439
cd49e617
BA
440 getPotentialDragonMoves(sq) {
441 return (
442 this.getSlideNJumpMoves(sq, V.steps[V.ROOK]).concat(
443 this.getSlideNJumpMoves(sq, V.steps[V.BISHOP], { oneStep: true }))
444 );
445 }
446
447 getPotentialHorseMoves(sq) {
448 return (
449 this.getSlideNJumpMoves(sq, V.steps[V.BISHOP]).concat(
450 this.getSlideNJumpMoves(sq, V.steps[V.ROOK], { oneStep: true }))
451 );
452 }
453
cd49e617
BA
454 isAttacked(sq, color) {
455 return (
456 this.isAttackedByPawn(sq, color) ||
457 this.isAttackedByRook(sq, color) ||
458 this.isAttackedByDragon(sq, color) ||
459 this.isAttackedByKnight(sq, color) ||
460 this.isAttackedByBishop(sq, color) ||
461 this.isAttackedByHorse(sq, color) ||
e2f204ed 462 this.isAttackedByLance(sq, color) ||
cd49e617
BA
463 this.isAttackedBySilver(sq, color) ||
464 this.isAttackedByGold(sq, color) ||
465 this.isAttackedByKing(sq, color)
466 );
467 }
468
469 isAttackedByGold([x, y], color) {
470 const shift = (color == 'w' ? 1 : -1);
471 for (let step of V.steps[V.ROOK].concat([[shift, 1], [shift, -1]])) {
472 const [i, j] = [x + step[0], y + step[1]];
473 if (
474 V.OnBoard(i, j) &&
475 this.board[i][j] != V.EMPTY &&
476 this.getColor(i, j) == color &&
e2f204ed 477 [V.GOLD_G, V.P_PAWN, V.P_SILVER, V.P_KNIGHT, V.P_LANCE]
cd49e617
BA
478 .includes(this.getPiece(i, j))
479 ) {
480 return true;
481 }
482 }
483 return false;
484 }
485
486 isAttackedBySilver([x, y], color) {
487 const shift = (color == 'w' ? 1 : -1);
488 for (let step of V.steps[V.BISHOP].concat([[shift, 0]])) {
489 const [i, j] = [x + step[0], y + step[1]];
490 if (
491 V.OnBoard(i, j) &&
492 this.board[i][j] != V.EMPTY &&
493 this.getColor(i, j) == color &&
494 this.getPiece(i, j) == V.SILVER_G
495 ) {
496 return true;
497 }
498 }
499 return false;
500 }
501
502 isAttackedByPawn([x, y], color) {
503 const shift = (color == 'w' ? 1 : -1);
504 const [i, j] = [x + shift, y];
505 return (
506 V.OnBoard(i, j) &&
507 this.board[i][j] != V.EMPTY &&
508 this.getColor(i, j) == color &&
509 this.getPiece(i, j) == V.PAWN
510 );
511 }
512
513 isAttackedByKnight(sq, color) {
514 const forward = (color == 'w' ? 2 : -2);
515 return this.isAttackedBySlideNJump(
516 sq, color, V.KNIGHT, [[forward, 1], [forward, -1]], "oneStep");
517 }
518
e2f204ed 519 isAttackedByLance(sq, color) {
cd49e617 520 const forward = (color == 'w' ? 1 : -1);
e2f204ed 521 return this.isAttackedBySlideNJump(sq, color, V.LANCE, [[forward, 0]]);
cd49e617
BA
522 }
523
524 isAttackedByDragon(sq, color) {
525 return (
526 this.isAttackedBySlideNJump(sq, color, V.P_ROOK, V.steps[V.ROOK]) ||
527 this.isAttackedBySlideNJump(
edfb07b1 528 sq, color, V.P_ROOK, V.steps[V.BISHOP], "oneStep")
cd49e617
BA
529 );
530 }
531
532 isAttackedByHorse(sq, color) {
533 return (
534 this.isAttackedBySlideNJump(sq, color, V.P_BISHOP, V.steps[V.BISHOP]) ||
535 this.isAttackedBySlideNJump(
edfb07b1 536 sq, color, V.P_BISHOP, V.steps[V.ROOK], "oneStep")
cd49e617
BA
537 );
538 }
539
540 getAllValidMoves() {
541 let moves = super.getAllPotentialMoves();
542 const color = this.turn;
543 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
544 moves = moves.concat(
545 this.getReserveMoves([V.size.x + (color == "w" ? 0 : 1), i])
546 );
547 }
548 return this.filterValid(moves);
549 }
550
551 atLeastOneMove() {
552 if (!super.atLeastOneMove()) {
553 // Search one reserve move
554 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
555 let moves = this.filterValid(
556 this.getReserveMoves([V.size.x + (this.turn == "w" ? 0 : 1), i])
557 );
558 if (moves.length > 0) return true;
559 }
560 return false;
561 }
562 return true;
563 }
564
565 static get P_CORRESPONDANCES() {
566 return {
567 q: 'p',
568 o: 'n',
569 t: 's',
570 m: 'l',
571 d: 'r',
572 h: 'b'
573 };
574 }
575
576 static MayDecode(piece) {
577 if (Object.keys(V.P_CORRESPONDANCES).includes(piece))
578 return V.P_CORRESPONDANCES[piece];
579 return piece;
580 }
581
582 postPlay(move) {
583 super.postPlay(move);
584 const color = move.appear[0].c;
585 if (move.vanish.length == 0)
586 // Drop unpromoted piece:
587 this.reserve[color][move.appear[0].p]--;
588 else if (move.vanish.length == 2)
589 // May capture a promoted piece:
590 this.reserve[color][V.MayDecode(move.vanish[1].p)]++;
591 }
592
593 postUndo(move) {
594 super.postUndo(move);
595 const color = this.turn;
596 if (move.vanish.length == 0)
597 this.reserve[color][move.appear[0].p]++;
598 else if (move.vanish.length == 2)
599 this.reserve[color][V.MayDecode(move.vanish[1].p)]--;
600 }
601
602 static get SEARCH_DEPTH() {
603 return 2;
604 }
605
606 static get VALUES() {
607 // TODO: very arbitrary and wrong
608 return {
609 p: 1,
610 q: 3,
611 r: 5,
612 d: 6,
613 n: 2,
614 o: 3,
615 b: 3,
616 h: 4,
617 s: 3,
618 t: 3,
619 l: 2,
620 m: 3,
621 g: 3,
622 k: 1000,
623 }
624 }
625
626 evalPosition() {
627 let evaluation = super.evalPosition();
628 // Add reserves:
629 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
630 const p = V.RESERVE_PIECES[i];
631 evaluation += this.reserve["w"][p] * V.VALUES[p];
632 evaluation -= this.reserve["b"][p] * V.VALUES[p];
633 }
634 return evaluation;
635 }
636
637 getNotation(move) {
638 const finalSquare = V.CoordsToSquare(move.end);
639 if (move.vanish.length == 0) {
640 // Rebirth:
641 const piece = move.appear[0].p.toUpperCase();
642 return (piece != 'P' ? piece : "") + "@" + finalSquare;
643 }
644 const piece = move.vanish[0].p.toUpperCase();
645 return (
646 (piece != 'P' || move.vanish.length == 2 ? piece : "") +
647 (move.vanish.length == 2 ? "x" : "") +
648 finalSquare +
649 (
650 move.appear[0].p != move.vanish[0].p
651 ? "=" + move.appear[0].p.toUpperCase()
652 : ""
653 )
654 );
655 }
7e8a7ea1 656
cd49e617 657};