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