Some fixes + first draft of Chakart (just thoughts for now)
[vchess.git] / client / src / variants / Shogi.js
CommitLineData
cd49e617
BA
1import { ChessRules, PiPo, Move } from "@/base_rules";
2import { ArrayFun } from "@/utils/array";
ad2494bd 3import { shuffle } from "@/utils/alea";
cd49e617
BA
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 }
e2f204ed 38 static get LANCE() {
cd49e617
BA
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 }
e2f204ed 52 static get P_LANCE() {
cd49e617
BA
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,
e2f204ed 71 V.LANCE,
cd49e617
BA
72 V.P_PAWN,
73 V.P_KNIGHT,
74 V.P_SILVER,
e2f204ed 75 V.P_LANCE,
cd49e617
BA
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
ad2494bd
BA
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 }
cd49e617 115 return (
ad2494bd
BA
116 pieces["b"].join("") +
117 "/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/" +
118 pieces["w"].join("").toUpperCase() +
119 " w 0 00000000000000"
cd49e617
BA
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]];
edfb07b1 135 counts[7 + i] = this.reserve["b"][V.RESERVE_PIECES[i]];
cd49e617
BA
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]),
e2f204ed 152 [V.LANCE]: parseInt(fenParsed.reserve[6])
cd49e617
BA
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]),
e2f204ed 161 [V.LANCE]: parseInt(fenParsed.reserve[13])
cd49e617
BA
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 (
e2f204ed 190 [V.PAWN, V.ROOK, V.BISHOP, V.GOLD_G, V.SILVER_G, V.KNIGHT, V.LANCE]
cd49e617
BA
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 (
e2f204ed 216 (i == lastRanks[0] && [V.PAWN, V.KNIGHT, V.LANCE].includes(p)) ||
cd49e617
BA
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]);
e2f204ed
BA
269 case V.LANCE:
270 return this.getPotentialLanceMoves([x, y]);
cd49e617
BA
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:
e2f204ed 281 case V.P_LANCE:
cd49e617
BA
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) {
118cff5c 289 options = options || {};
cd49e617
BA
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
6c7cbfed
BA
375 getPotentialLanceMoves(sq) {
376 const forward = (this.turn == 'w' ? -1 : 1);
377 return this.getSlideNJumpMoves(
378 sq,
379 [[forward, 0]],
380 {
381 promote: V.P_LANCE,
382 force: true
383 }
384 );
385 }
386
cd49e617
BA
387 getPotentialRookMoves(sq) {
388 return this.getSlideNJumpMoves(
389 sq, V.steps[V.ROOK], { promote: V.P_ROOK });
390 }
391
392 getPotentialBishopMoves(sq) {
393 return this.getSlideNJumpMoves(
394 sq, V.steps[V.BISHOP], { promote: V.P_BISHOP });
395 }
396
cd49e617
BA
397 getPotentialDragonMoves(sq) {
398 return (
399 this.getSlideNJumpMoves(sq, V.steps[V.ROOK]).concat(
400 this.getSlideNJumpMoves(sq, V.steps[V.BISHOP], { oneStep: true }))
401 );
402 }
403
404 getPotentialHorseMoves(sq) {
405 return (
406 this.getSlideNJumpMoves(sq, V.steps[V.BISHOP]).concat(
407 this.getSlideNJumpMoves(sq, V.steps[V.ROOK], { oneStep: true }))
408 );
409 }
410
411 getPotentialKingMoves(sq) {
412 return this.getSlideNJumpMoves(
413 sq,
414 V.steps[V.ROOK].concat(V.steps[V.BISHOP]),
415 { oneStep: true }
416 );
417 }
418
419 isAttacked(sq, color) {
420 return (
421 this.isAttackedByPawn(sq, color) ||
422 this.isAttackedByRook(sq, color) ||
423 this.isAttackedByDragon(sq, color) ||
424 this.isAttackedByKnight(sq, color) ||
425 this.isAttackedByBishop(sq, color) ||
426 this.isAttackedByHorse(sq, color) ||
e2f204ed 427 this.isAttackedByLance(sq, color) ||
cd49e617
BA
428 this.isAttackedBySilver(sq, color) ||
429 this.isAttackedByGold(sq, color) ||
430 this.isAttackedByKing(sq, color)
431 );
432 }
433
434 isAttackedByGold([x, y], color) {
435 const shift = (color == 'w' ? 1 : -1);
436 for (let step of V.steps[V.ROOK].concat([[shift, 1], [shift, -1]])) {
437 const [i, j] = [x + step[0], y + step[1]];
438 if (
439 V.OnBoard(i, j) &&
440 this.board[i][j] != V.EMPTY &&
441 this.getColor(i, j) == color &&
e2f204ed 442 [V.GOLD_G, V.P_PAWN, V.P_SILVER, V.P_KNIGHT, V.P_LANCE]
cd49e617
BA
443 .includes(this.getPiece(i, j))
444 ) {
445 return true;
446 }
447 }
448 return false;
449 }
450
451 isAttackedBySilver([x, y], color) {
452 const shift = (color == 'w' ? 1 : -1);
453 for (let step of V.steps[V.BISHOP].concat([[shift, 0]])) {
454 const [i, j] = [x + step[0], y + step[1]];
455 if (
456 V.OnBoard(i, j) &&
457 this.board[i][j] != V.EMPTY &&
458 this.getColor(i, j) == color &&
459 this.getPiece(i, j) == V.SILVER_G
460 ) {
461 return true;
462 }
463 }
464 return false;
465 }
466
467 isAttackedByPawn([x, y], color) {
468 const shift = (color == 'w' ? 1 : -1);
469 const [i, j] = [x + shift, y];
470 return (
471 V.OnBoard(i, j) &&
472 this.board[i][j] != V.EMPTY &&
473 this.getColor(i, j) == color &&
474 this.getPiece(i, j) == V.PAWN
475 );
476 }
477
478 isAttackedByKnight(sq, color) {
479 const forward = (color == 'w' ? 2 : -2);
480 return this.isAttackedBySlideNJump(
481 sq, color, V.KNIGHT, [[forward, 1], [forward, -1]], "oneStep");
482 }
483
e2f204ed 484 isAttackedByLance(sq, color) {
cd49e617 485 const forward = (color == 'w' ? 1 : -1);
e2f204ed 486 return this.isAttackedBySlideNJump(sq, color, V.LANCE, [[forward, 0]]);
cd49e617
BA
487 }
488
489 isAttackedByDragon(sq, color) {
490 return (
491 this.isAttackedBySlideNJump(sq, color, V.P_ROOK, V.steps[V.ROOK]) ||
492 this.isAttackedBySlideNJump(
edfb07b1 493 sq, color, V.P_ROOK, V.steps[V.BISHOP], "oneStep")
cd49e617
BA
494 );
495 }
496
497 isAttackedByHorse(sq, color) {
498 return (
499 this.isAttackedBySlideNJump(sq, color, V.P_BISHOP, V.steps[V.BISHOP]) ||
500 this.isAttackedBySlideNJump(
edfb07b1 501 sq, color, V.P_BISHOP, V.steps[V.ROOK], "oneStep")
cd49e617
BA
502 );
503 }
504
505 getAllValidMoves() {
506 let moves = super.getAllPotentialMoves();
507 const color = this.turn;
508 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
509 moves = moves.concat(
510 this.getReserveMoves([V.size.x + (color == "w" ? 0 : 1), i])
511 );
512 }
513 return this.filterValid(moves);
514 }
515
516 atLeastOneMove() {
517 if (!super.atLeastOneMove()) {
518 // Search one reserve move
519 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
520 let moves = this.filterValid(
521 this.getReserveMoves([V.size.x + (this.turn == "w" ? 0 : 1), i])
522 );
523 if (moves.length > 0) return true;
524 }
525 return false;
526 }
527 return true;
528 }
529
530 static get P_CORRESPONDANCES() {
531 return {
532 q: 'p',
533 o: 'n',
534 t: 's',
535 m: 'l',
536 d: 'r',
537 h: 'b'
538 };
539 }
540
541 static MayDecode(piece) {
542 if (Object.keys(V.P_CORRESPONDANCES).includes(piece))
543 return V.P_CORRESPONDANCES[piece];
544 return piece;
545 }
546
547 postPlay(move) {
548 super.postPlay(move);
549 const color = move.appear[0].c;
550 if (move.vanish.length == 0)
551 // Drop unpromoted piece:
552 this.reserve[color][move.appear[0].p]--;
553 else if (move.vanish.length == 2)
554 // May capture a promoted piece:
555 this.reserve[color][V.MayDecode(move.vanish[1].p)]++;
556 }
557
558 postUndo(move) {
559 super.postUndo(move);
560 const color = this.turn;
561 if (move.vanish.length == 0)
562 this.reserve[color][move.appear[0].p]++;
563 else if (move.vanish.length == 2)
564 this.reserve[color][V.MayDecode(move.vanish[1].p)]--;
565 }
566
567 static get SEARCH_DEPTH() {
568 return 2;
569 }
570
571 static get VALUES() {
572 // TODO: very arbitrary and wrong
573 return {
574 p: 1,
575 q: 3,
576 r: 5,
577 d: 6,
578 n: 2,
579 o: 3,
580 b: 3,
581 h: 4,
582 s: 3,
583 t: 3,
584 l: 2,
585 m: 3,
586 g: 3,
587 k: 1000,
588 }
589 }
590
591 evalPosition() {
592 let evaluation = super.evalPosition();
593 // Add reserves:
594 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
595 const p = V.RESERVE_PIECES[i];
596 evaluation += this.reserve["w"][p] * V.VALUES[p];
597 evaluation -= this.reserve["b"][p] * V.VALUES[p];
598 }
599 return evaluation;
600 }
601
602 getNotation(move) {
603 const finalSquare = V.CoordsToSquare(move.end);
604 if (move.vanish.length == 0) {
605 // Rebirth:
606 const piece = move.appear[0].p.toUpperCase();
607 return (piece != 'P' ? piece : "") + "@" + finalSquare;
608 }
609 const piece = move.vanish[0].p.toUpperCase();
610 return (
611 (piece != 'P' || move.vanish.length == 2 ? piece : "") +
612 (move.vanish.length == 2 ? "x" : "") +
613 finalSquare +
614 (
615 move.appear[0].p != move.vanish[0].p
616 ? "=" + move.appear[0].p.toUpperCase()
617 : ""
618 )
619 );
620 }
621};