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