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