Add Makruk, Shako and Shogi + a few fixes
[vchess.git] / client / src / variants / Shogi.js
CommitLineData
cd49e617
BA
1import { ChessRules, PiPo, Move } from "@/base_rules";
2import { ArrayFun } from "@/utils/array";
3
4export 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 color = this.turn;
272 const oneStep = options.oneStep;
273 const forcePromoteOnLastRank = options.force;
274 const promoteInto = options.promote;
275 const lastRanks = (color == 'w' ? [0, 1, 2] : [9, 8, 7]);
276 let moves = [];
277 outerLoop: for (let step of steps) {
278 let i = x + step[0];
279 let j = y + step[1];
280 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
281 if (i != lastRanks[0] || !forcePromoteOnLastRank)
282 moves.push(this.getBasicMove([x, y], [i, j]));
283 if (!!promoteInto && lastRanks.includes(i)) {
284 moves.push(
285 this.getBasicMove(
286 [x, y], [i, j], { c: color, p: promoteInto })
287 );
288 }
289 if (oneStep) continue outerLoop;
290 i += step[0];
291 j += step[1];
292 }
293 if (V.OnBoard(i, j) && this.canTake([x, y], [i, j])) {
294 if (i != lastRanks[0] || !forcePromoteOnLastRank)
295 moves.push(this.getBasicMove([x, y], [i, j]));
296 if (!!promoteInto && lastRanks.includes(i)) {
297 moves.push(
298 this.getBasicMove(
299 [x, y], [i, j], { c: color, p: promoteInto })
300 );
301 }
302 }
303 }
304 return moves;
305 }
306
307 getPotentialGoldMoves(sq) {
308 const forward = (this.turn == 'w' ? -1 : 1);
309 return this.getSlideNJumpMoves(
310 sq,
311 V.steps[V.ROOK].concat([ [forward, 1], [forward, -1] ]),
312 { oneStep: true }
313 );
314 }
315
316 getPotentialPawnMoves(sq) {
317 const forward = (this.turn == 'w' ? -1 : 1);
318 return (
319 this.getSlideNJumpMoves(
320 sq,
321 [[forward, 0]],
322 {
323 oneStep: true,
324 promote: V.P_PAWN,
325 force: true
326 }
327 )
328 );
329 }
330
331 getPotentialSilverMoves(sq) {
332 const forward = (this.turn == 'w' ? -1 : 1);
333 return this.getSlideNJumpMoves(
334 sq,
335 V.steps[V.BISHOP].concat([ [forward, 0] ]),
336 {
337 oneStep: true,
338 promote: V.P_SILVER
339 }
340 );
341 }
342
343 getPotentialKnightMoves(sq) {
344 const forward = (this.turn == 'w' ? -2 : 2);
345 return this.getSlideNJumpMoves(
346 sq,
347 [ [forward, 1], [forward, -1] ],
348 {
349 oneStep: true,
350 promote: V.P_KNIGHT,
351 force: true
352 }
353 );
354 }
355
356 getPotentialRookMoves(sq) {
357 return this.getSlideNJumpMoves(
358 sq, V.steps[V.ROOK], { promote: V.P_ROOK });
359 }
360
361 getPotentialBishopMoves(sq) {
362 return this.getSlideNJumpMoves(
363 sq, V.steps[V.BISHOP], { promote: V.P_BISHOP });
364 }
365
366 getPotentialLancerMoves(sq) {
367 const forward = (this.turn == 'w' ? -1 : 1);
368 return this.getSlideNJumpMoves(
369 sq, [[forward, 0]], { promote: V.P_LANCER });
370 }
371
372 getPotentialDragonMoves(sq) {
373 return (
374 this.getSlideNJumpMoves(sq, V.steps[V.ROOK]).concat(
375 this.getSlideNJumpMoves(sq, V.steps[V.BISHOP], { oneStep: true }))
376 );
377 }
378
379 getPotentialHorseMoves(sq) {
380 return (
381 this.getSlideNJumpMoves(sq, V.steps[V.BISHOP]).concat(
382 this.getSlideNJumpMoves(sq, V.steps[V.ROOK], { oneStep: true }))
383 );
384 }
385
386 getPotentialKingMoves(sq) {
387 return this.getSlideNJumpMoves(
388 sq,
389 V.steps[V.ROOK].concat(V.steps[V.BISHOP]),
390 { oneStep: true }
391 );
392 }
393
394 isAttacked(sq, color) {
395 return (
396 this.isAttackedByPawn(sq, color) ||
397 this.isAttackedByRook(sq, color) ||
398 this.isAttackedByDragon(sq, color) ||
399 this.isAttackedByKnight(sq, color) ||
400 this.isAttackedByBishop(sq, color) ||
401 this.isAttackedByHorse(sq, color) ||
402 this.isAttackedByLancer(sq, color) ||
403 this.isAttackedBySilver(sq, color) ||
404 this.isAttackedByGold(sq, color) ||
405 this.isAttackedByKing(sq, color)
406 );
407 }
408
409 isAttackedByGold([x, y], color) {
410 const shift = (color == 'w' ? 1 : -1);
411 for (let step of V.steps[V.ROOK].concat([[shift, 1], [shift, -1]])) {
412 const [i, j] = [x + step[0], y + step[1]];
413 if (
414 V.OnBoard(i, j) &&
415 this.board[i][j] != V.EMPTY &&
416 this.getColor(i, j) == color &&
417 [V.GOLD_G, V.P_PAWN, V.P_SILVER, V.P_KNIGHT, V.P_LANCER]
418 .includes(this.getPiece(i, j))
419 ) {
420 return true;
421 }
422 }
423 return false;
424 }
425
426 isAttackedBySilver([x, y], color) {
427 const shift = (color == 'w' ? 1 : -1);
428 for (let step of V.steps[V.BISHOP].concat([[shift, 0]])) {
429 const [i, j] = [x + step[0], y + step[1]];
430 if (
431 V.OnBoard(i, j) &&
432 this.board[i][j] != V.EMPTY &&
433 this.getColor(i, j) == color &&
434 this.getPiece(i, j) == V.SILVER_G
435 ) {
436 return true;
437 }
438 }
439 return false;
440 }
441
442 isAttackedByPawn([x, y], color) {
443 const shift = (color == 'w' ? 1 : -1);
444 const [i, j] = [x + shift, y];
445 return (
446 V.OnBoard(i, j) &&
447 this.board[i][j] != V.EMPTY &&
448 this.getColor(i, j) == color &&
449 this.getPiece(i, j) == V.PAWN
450 );
451 }
452
453 isAttackedByKnight(sq, color) {
454 const forward = (color == 'w' ? 2 : -2);
455 return this.isAttackedBySlideNJump(
456 sq, color, V.KNIGHT, [[forward, 1], [forward, -1]], "oneStep");
457 }
458
459 isAttackedByLancer(sq, color) {
460 const forward = (color == 'w' ? 1 : -1);
461 return this.isAttackedBySlideNJump(sq, color, V.LANCER, [[forward, 0]]);
462 }
463
464 isAttackedByDragon(sq, color) {
465 return (
466 this.isAttackedBySlideNJump(sq, color, V.P_ROOK, V.steps[V.ROOK]) ||
467 this.isAttackedBySlideNJump(
468 sq, color, V.DRAGON, V.steps[V.BISHOP], "oneStep")
469 );
470 }
471
472 isAttackedByHorse(sq, color) {
473 return (
474 this.isAttackedBySlideNJump(sq, color, V.P_BISHOP, V.steps[V.BISHOP]) ||
475 this.isAttackedBySlideNJump(
476 sq, color, V.DRAGON, V.steps[V.ROOK], "oneStep")
477 );
478 }
479
480 getAllValidMoves() {
481 let moves = super.getAllPotentialMoves();
482 const color = this.turn;
483 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
484 moves = moves.concat(
485 this.getReserveMoves([V.size.x + (color == "w" ? 0 : 1), i])
486 );
487 }
488 return this.filterValid(moves);
489 }
490
491 atLeastOneMove() {
492 if (!super.atLeastOneMove()) {
493 // Search one reserve move
494 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
495 let moves = this.filterValid(
496 this.getReserveMoves([V.size.x + (this.turn == "w" ? 0 : 1), i])
497 );
498 if (moves.length > 0) return true;
499 }
500 return false;
501 }
502 return true;
503 }
504
505 static get P_CORRESPONDANCES() {
506 return {
507 q: 'p',
508 o: 'n',
509 t: 's',
510 m: 'l',
511 d: 'r',
512 h: 'b'
513 };
514 }
515
516 static MayDecode(piece) {
517 if (Object.keys(V.P_CORRESPONDANCES).includes(piece))
518 return V.P_CORRESPONDANCES[piece];
519 return piece;
520 }
521
522 postPlay(move) {
523 super.postPlay(move);
524 const color = move.appear[0].c;
525 if (move.vanish.length == 0)
526 // Drop unpromoted piece:
527 this.reserve[color][move.appear[0].p]--;
528 else if (move.vanish.length == 2)
529 // May capture a promoted piece:
530 this.reserve[color][V.MayDecode(move.vanish[1].p)]++;
531 }
532
533 postUndo(move) {
534 super.postUndo(move);
535 const color = this.turn;
536 if (move.vanish.length == 0)
537 this.reserve[color][move.appear[0].p]++;
538 else if (move.vanish.length == 2)
539 this.reserve[color][V.MayDecode(move.vanish[1].p)]--;
540 }
541
542 static get SEARCH_DEPTH() {
543 return 2;
544 }
545
546 static get VALUES() {
547 // TODO: very arbitrary and wrong
548 return {
549 p: 1,
550 q: 3,
551 r: 5,
552 d: 6,
553 n: 2,
554 o: 3,
555 b: 3,
556 h: 4,
557 s: 3,
558 t: 3,
559 l: 2,
560 m: 3,
561 g: 3,
562 k: 1000,
563 }
564 }
565
566 evalPosition() {
567 let evaluation = super.evalPosition();
568 // Add reserves:
569 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
570 const p = V.RESERVE_PIECES[i];
571 evaluation += this.reserve["w"][p] * V.VALUES[p];
572 evaluation -= this.reserve["b"][p] * V.VALUES[p];
573 }
574 return evaluation;
575 }
576
577 getNotation(move) {
578 const finalSquare = V.CoordsToSquare(move.end);
579 if (move.vanish.length == 0) {
580 // Rebirth:
581 const piece = move.appear[0].p.toUpperCase();
582 return (piece != 'P' ? piece : "") + "@" + finalSquare;
583 }
584 const piece = move.vanish[0].p.toUpperCase();
585 return (
586 (piece != 'P' || move.vanish.length == 2 ? piece : "") +
587 (move.vanish.length == 2 ? "x" : "") +
588 finalSquare +
589 (
590 move.appear[0].p != move.vanish[0].p
591 ? "=" + move.appear[0].p.toUpperCase()
592 : ""
593 )
594 );
595 }
596};