Advance on Janggi translation + fix initial setup moves
[vchess.git] / client / src / variants / Janggi.js
1 import { ChessRules, Move, PiPo } from "@/base_rules";
2 import { randInt } from "@/utils/alea";
3
4 export class JanggiRules extends ChessRules {
5
6 static get Monochrome() {
7 return true;
8 }
9
10 static get Notoodark() {
11 return true;
12 }
13
14 static get Lines() {
15 let lines = [];
16 // Draw all inter-squares lines, shifted:
17 for (let i = 0; i < V.size.x; i++)
18 lines.push([[i+0.5, 0.5], [i+0.5, V.size.y-0.5]]);
19 for (let j = 0; j < V.size.y; j++)
20 lines.push([[0.5, j+0.5], [V.size.x-0.5, j+0.5]]);
21 // Add palaces:
22 lines.push([[0.5, 3.5], [2.5, 5.5]]);
23 lines.push([[0.5, 5.5], [2.5, 3.5]]);
24 lines.push([[9.5, 3.5], [7.5, 5.5]]);
25 lines.push([[9.5, 5.5], [7.5, 3.5]]);
26 return lines;
27 }
28
29 // No castle, but flag: bikjang
30 static get HasCastle() {
31 return false;
32 }
33
34 static get HasEnpassant() {
35 return false;
36 }
37
38 static get ELEPHANT() {
39 return "e";
40 }
41
42 static get CANNON() {
43 return "c";
44 }
45
46 static get ADVISOR() {
47 return "a";
48 }
49
50 static get PIECES() {
51 return [V.PAWN, V.ROOK, V.KNIGHT, V.ELEPHANT, V.ADVISOR, V.KING, V.CANNON];
52 }
53
54 getPpath(b) {
55 return "Jiangqi/" + b;
56 }
57
58 static get size() {
59 return { x: 10, y: 9};
60 }
61
62 static IsGoodFlags(flags) {
63 // bikjang status of last move + pass
64 return !!flags.match(/^[0-2]{2,2}$/);
65 }
66
67 aggregateFlags() {
68 return [this.bikjangFlag, this.passFlag];
69 }
70
71 disaggregateFlags(flags) {
72 this.bikjangFlag = flags[0];
73 this.passFlag = flags[1];
74 }
75
76 getFlagsFen() {
77 return this.bikjangFlag.toString() + this.passFlag.toString()
78 }
79
80 setFlags(fenflags) {
81 this.bikjangFlag = parseInt(fenflags.charAt(0), 10);
82 this.passFlag = parseInt(fenflags.charAt(1), 10);
83 }
84
85 setOtherVariables(fen) {
86 super.setOtherVariables(fen);
87 // Sub-turn is useful only at first move...
88 this.subTurn = 1;
89 }
90
91 getPotentialMovesFrom([x, y]) {
92 let moves = [];
93 const c = this.getColor(x, y);
94 const oppCol = V.GetOppCol(c);
95 if (this.kingPos[c][0] == x && this.kingPos[c][1] == y) {
96 // Add pass move (might be impossible if undercheck)
97 moves.push(
98 new Move({
99 appear: [],
100 vanish: [],
101 start: { x: this.kingPos[c][0], y: this.kingPos[c][1] },
102 end: { x: this.kingPos[oppCol][0], y: this.kingPos[oppCol][1] }
103 })
104 );
105 }
106 // TODO: next "if" is mutually exclusive with the block above
107 if (this.movesCount <= 1) {
108 const firstRank = (this.movesCount == 0 ? 9 : 0);
109 const initDestFile = new Map([[1, 2], [7, 6]]);
110 // Only option is knight / elephant swap:
111 if (x == firstRank && !!initDestFile.get(y)) {
112 const destFile = initDestFile.get(y);
113 moves.push(
114 new Move({
115 appear: [
116 new PiPo({
117 x: x,
118 y: destFile,
119 c: c,
120 p: V.KNIGHT
121 }),
122 new PiPo({
123 x: x,
124 y: y,
125 c: c,
126 p: V.ELEPHANT
127 })
128 ],
129 vanish: [
130 new PiPo({
131 x: x,
132 y: y,
133 c: c,
134 p: V.KNIGHT
135 }),
136 new PiPo({
137 x: x,
138 y: destFile,
139 c: c,
140 p: V.ELEPHANT
141 })
142 ],
143 start: { x: x, y: y },
144 end: { x: x, y: destFile }
145 })
146 );
147 }
148 }
149 else {
150 let normalMoves = [];
151 switch (this.getPiece(x, y)) {
152 case V.PAWN:
153 normalMoves = this.getPotentialPawnMoves([x, y]);
154 break;
155 case V.ROOK:
156 normalMoves = this.getPotentialRookMoves([x, y]);
157 break;
158 case V.KNIGHT:
159 normalMoves = this.getPotentialKnightMoves([x, y]);
160 break;
161 case V.ELEPHANT:
162 normalMoves = this.getPotentialElephantMoves([x, y]);
163 break;
164 case V.ADVISOR:
165 normalMoves = this.getPotentialAdvisorMoves([x, y]);
166 break;
167 case V.KING:
168 normalMoves = this.getPotentialKingMoves([x, y]);
169 break;
170 case V.CANNON:
171 normalMoves = this.getPotentialCannonMoves([x, y]);
172 break;
173 }
174 Array.prototype.push.apply(moves, normalMoves);
175 }
176 return moves;
177 }
178
179 getPotentialPawnMoves([x, y]) {
180 const c = this.getColor(x, y);
181 const oppCol = V.GetOppCol(c);
182 const shiftX = (c == 'w' ? -1 : 1);
183 const rank23 = (oppCol == 'w' ? [8, 7] : [1, 2]);
184 let steps = [[shiftX, 0], [0, -1], [0, 1]];
185 // Diagonal moves inside enemy palace:
186 if (y == 4 && x == rank23[0])
187 Array.prototype.push.apply(steps, [[shiftX, 1], [shiftX, -1]]);
188 else if (x == rank23[1]) {
189 if (y == 3) steps.push([shiftX, 1]);
190 else if (y == 5) steps.push([shiftX, -1]);
191 }
192 return super.getSlideNJumpMoves([x, y], steps, "oneStep");
193 }
194
195 knightStepsFromRookStep(step) {
196 if (step[0] == 0) return [ [1, 2*step[1]], [-1, 2*step[1]] ];
197 return [ [2*step[0], 1], [2*step[0], -1] ];
198 }
199
200 getPotentialKnightMoves([x, y]) {
201 let steps = [];
202 for (let rookStep of ChessRules.steps[V.ROOK]) {
203 const [i, j] = [x + rookStep[0], y + rookStep[1]];
204 if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
205 Array.prototype.push.apply(steps,
206 // These moves might be impossible, but need to be checked:
207 this.knightStepsFromRookStep(rookStep));
208 }
209 }
210 return super.getSlideNJumpMoves([x, y], steps, "oneStep");
211 }
212
213 elephantStepsFromRookStep(step) {
214 if (step[0] == 0) return [ [2, 3*step[1]], [-2, 3*step[1]] ];
215 return [ [3*step[0], 2], [3*step[0], -2] ];
216 }
217
218 getPotentialElephantMoves([x, y]) {
219 let steps = [];
220 for (let rookStep of ChessRules.steps[V.ROOK]) {
221 const eSteps = this.elephantStepsFromRookStep(rookStep);
222 const [i, j] = [x + rookStep[0], y + rookStep[1]];
223 if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
224 // Check second crossing:
225 const knightSteps = this.knightStepsFromRookStep(rookStep);
226 for (let k of [0, 1]) {
227 const [ii, jj] = [x + knightSteps[k][0], y + knightSteps[k][1]];
228 if (V.OnBoard(ii, jj) && this.board[ii][jj] == V.EMPTY)
229 steps.push(eSteps[k]); //ok: same ordering
230 }
231 }
232 }
233 return super.getSlideNJumpMoves([x, y], steps, "oneStep");
234 }
235
236 palacePeopleMoves([x, y]) {
237 const c = this.getColor(x, y);
238 let steps = [];
239 // Orthogonal steps:
240 if (x < (c == 'w' ? 9 : 2)) steps.push([1, 0]);
241 if (x > (c == 'w' ? 7 : 0)) steps.push([-1, 0]);
242 if (y > 3) steps.push([0, -1]);
243 if (y < 5) steps.push([0, 1]);
244 // Diagonal steps, if in the middle or corner:
245 if (
246 y != 4 &&
247 (
248 (c == 'w' && x != 8) ||
249 (c == 'b' && x != 1)
250 )
251 ) {
252 // In a corner: maximum one diagonal step available
253 let step = null;
254 const direction = (c == 'w' ? -1 : 1);
255 if ((c == 'w' && x == 9) || (c == 'b' && x == 0)) {
256 // On first line
257 if (y == 3) step = [direction, 1];
258 else step = [direction, -1];
259 }
260 else if ((c == 'w' && x == 7) || (c == 'b' && x == 2)) {
261 // On third line
262 if (y == 3) step = [-direction, 1];
263 else step = [-direction, -1];
264 }
265 steps.push(step);
266 }
267 else if (
268 y == 4 &&
269 (
270 (c == 'w' && x == 8) ||
271 (c == 'b' && x == 1)
272 )
273 ) {
274 // At the middle: all directions available
275 Array.prototype.push.apply(steps, ChessRules.steps[V.BISHOP]);
276 }
277 return super.getSlideNJumpMoves([x, y], steps, "oneStep");
278 }
279
280 getPotentialAdvisorMoves(sq) {
281 return this.palacePeopleMoves(sq);
282 }
283
284 getPotentialKingMoves(sq) {
285 return this.palacePeopleMoves(sq);
286 }
287
288 getPotentialRookMoves([x, y]) {
289 let moves = super.getPotentialRookMoves([x, y]);
290 if ([3, 5].includes(y) && [0, 2, 7, 9].includes(x)) {
291 // In a corner of a palace: move along diagonal
292 const step = [[0, 7].includes(x) ? 1 : -1, 4 - y];
293 const oppCol = V.GetOppCol(this.getColor(x, y));
294 for (let i of [1, 2]) {
295 const [xx, yy] = [x + i * step[0], y + i * step[1]];
296 if (this.board[xx][yy] == V.EMPTY)
297 moves.push(this.getBasicMove([x, y], [xx, yy]));
298 else {
299 if (this.getColor(xx, yy) == oppCol)
300 moves.push(this.getBasicMove([x, y], [xx, yy]));
301 break;
302 }
303 }
304 }
305 else if (y == 4 && [1, 8].includes(x)) {
306 // In the middle of a palace: 4 one-diagonal-step to check
307 Array.prototype.push.apply(
308 moves,
309 super.getSlideNJumpMoves([x, y],
310 ChessRules.steps[V.BISHOP],
311 "oneStep")
312 );
313 }
314 return moves;
315 }
316
317 // NOTE: (mostly) duplicated from Shako (TODO?)
318 getPotentialCannonMoves([x, y]) {
319 const oppCol = V.GetOppCol(this.turn);
320 let moves = [];
321 // Look in every direction until an obstacle (to jump) is met
322 for (const step of V.steps[V.ROOK]) {
323 let i = x + step[0];
324 let j = y + step[1];
325 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
326 i += step[0];
327 j += step[1];
328 }
329 // Then, search for an enemy (if jumped piece isn't a cannon)
330 if (V.OnBoard(i, j) && this.getPiece(i, j) != V.CANNON) {
331 i += step[0];
332 j += step[1];
333 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
334 moves.push(this.getBasicMove([x, y], [i, j]));
335 i += step[0];
336 j += step[1];
337 }
338 if (
339 V.OnBoard(i, j) &&
340 this.getColor(i, j) == oppCol &&
341 this.getPiece(i, j) != V.CANNON
342 ) {
343 moves.push(this.getBasicMove([x, y], [i, j]));
344 }
345 }
346 }
347 if ([3, 5].includes(y) && [0, 2, 7, 9].includes(x)) {
348 // In a corner of a palace: hop over next obstacle if possible
349 const step = [[0, 7].includes(x) ? 1 : -1, 4 - y];
350 const [x1, y1] = [x + step[0], y + step[1]];
351 const [x2, y2] = [x + 2 * step[0], y + 2 * step[1]];
352 if (
353 this.board[x1][y1] != V.EMPTY &&
354 this.getPiece(x1, y1) != V.CANNON &&
355 (
356 this.board[x2][y2] == V.EMPTY ||
357 (
358 this.getColor(x2, y2) == oppCol &&
359 this.getPiece(x2, y2) != V.CANNON
360 )
361 )
362 ) {
363 moves.push(this.getBasicMove([x, y], [x2, y2]));
364 }
365 }
366 return moves;
367 }
368
369 // (King) Never attacked by advisor, since it stays in the palace
370 isAttacked(sq, color) {
371 return (
372 this.isAttackedByPawn(sq, color) ||
373 this.isAttackedByRook(sq, color) ||
374 this.isAttackedByKnight(sq, color) ||
375 this.isAttackedByElephant(sq, color) ||
376 this.isAttackedByCannon(sq, color)
377 );
378 }
379
380 onPalaceDiagonal([x, y]) {
381 return (
382 (y == 4 && [1, 8].includes(x)) ||
383 ([3, 5].includes(y) && [0, 2, 7, 9].includes(x))
384 );
385 }
386
387 isAttackedByPawn([x, y], color) {
388 const shiftX = (color == 'w' ? 1 : -1); //shift from king
389 if (super.isAttackedBySlideNJump(
390 [x, y], color, V.PAWN, [[shiftX, 0], [0, 1], [0, -1]], "oneStep")
391 ) {
392 return true;
393 }
394 if (this.onPalaceDiagonal([x, y])) {
395 for (let yStep of [-1, 1]) {
396 const [xx, yy] = [x + shiftX, y + yStep];
397 if (
398 this.onPalaceDiagonal([xx,yy]) &&
399 this.board[xx][yy] != V.EMPTY &&
400 this.getColor(xx, yy) == color &&
401 this.getPiece(xx, yy) == V.PAWN
402 ) {
403 return true;
404 }
405 }
406 }
407 return false;
408 }
409
410 knightStepsFromBishopStep(step) {
411 return [ [2*step[0], step[1]], [step[0], 2*step[1]] ];
412 }
413
414 isAttackedByKnight([x, y], color) {
415 // Check bishop steps: if empty, look continuation knight step
416 let steps = [];
417 for (let s of ChessRules.steps[V.BISHOP]) {
418 const [i, j] = [x + s[0], y + s[1]];
419 if (
420 V.OnBoard(i, j) &&
421 this.board[i][j] == V.EMPTY
422 ) {
423 Array.prototype.push.apply(steps, this.knightStepsFromBishopStep(s));
424 }
425 }
426 return (
427 super.isAttackedBySlideNJump([x, y], color, V.KNIGHT, steps, "oneStep")
428 );
429 }
430
431 elephantStepsFromBishopStep(step) {
432 return [ [3*step[0], 2*step[1]], [2*step[0], 3*step[1]] ];
433 }
434
435 isAttackedByElephant([x, y], color) {
436 // Check bishop steps: if empty, look continuation elephant step
437 let steps = [];
438 for (let s of ChessRules.steps[V.BISHOP]) {
439 const [i1, j1] = [x + s[0], y + s[1]];
440 const [i2, j2] = [x + 2*s[0], y + 2*s[1]];
441 if (
442 V.OnBoard(i2, j2) && this.board[i2][j2] == V.EMPTY &&
443 V.OnBoard(i1, j1) && this.board[i1][j1] == V.EMPTY
444 ) {
445 Array.prototype.push.apply(steps, this.elephantStepsFromBishopStep(s));
446 }
447 }
448 return (
449 super.isAttackedBySlideNJump([x, y], color, V.ELEPHANT, steps, "oneStep")
450 );
451 }
452
453 isAttackedByRook([x, y], color) {
454 if (super.isAttackedByRook([x, y], color)) return true;
455 // Also check diagonals, if inside palace
456 if (this.onPalaceDiagonal([x, y])) {
457 // TODO: next scan is clearly suboptimal
458 for (let s of ChessRules.steps[V.BISHOP]) {
459 for (let i of [1, 2]) {
460 const [xx, yy] = [x + i * s[0], y + i * s[1]];
461 if (
462 V.OnBoard(xx, yy) &&
463 this.onPalaceDiagonal([xx, yy])
464 ) {
465 if (this.board[xx][yy] != V.EMPTY) {
466 if (
467 this.getColor(xx, yy) == color &&
468 this.getPiece(xx, yy) == V.ROOK
469 ) {
470 return true;
471 }
472 break;
473 }
474 }
475 else continue;
476 }
477 }
478 }
479 return false;
480 }
481
482 // NOTE: (mostly) duplicated from Shako (TODO?)
483 isAttackedByCannon([x, y], color) {
484 // Reversed process: is there an obstacle in line,
485 // and a cannon next in the same line?
486 for (const step of V.steps[V.ROOK]) {
487 let [i, j] = [x+step[0], y+step[1]];
488 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
489 i += step[0];
490 j += step[1];
491 }
492 if (V.OnBoard(i, j) && this.getPiece(i, j) != V.CANNON) {
493 // Keep looking in this direction
494 i += step[0];
495 j += step[1];
496 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
497 i += step[0];
498 j += step[1];
499 }
500 if (
501 V.OnBoard(i, j) &&
502 this.getPiece(i, j) == V.CANNON &&
503 this.getColor(i, j) == color
504 ) {
505 return true;
506 }
507 }
508 }
509 return false;
510 }
511
512 getCurrentScore() {
513 if ([this.bikjangFlag, this.passFlag].includes(2)) return "1/2";
514 const color = this.turn;
515 // super.atLeastOneMove() does not consider passing (OK)
516 if (this.underCheck(color) && !super.atLeastOneMove())
517 return (color == "w" ? "0-1" : "1-0");
518 return "*";
519 }
520
521 static get VALUES() {
522 return {
523 p: 2,
524 r: 13,
525 n: 5,
526 e: 3,
527 a: 3,
528 c: 7,
529 k: 1000
530 };
531 }
532
533 static get SEARCH_DEPTH() {
534 return 2;
535 }
536
537 static GenRandInitFen() {
538 // No randomization here (but initial setup choice)
539 return (
540 "rnea1aenr/4k4/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/4K4/RNEA1AENR w 0 00"
541 );
542 }
543
544 play(move) {
545 move.subTurn = this.subTurn; //much easier
546 if (this.movesCount >= 2 || this.subTurn == 2 || move.vanish.length == 0) {
547 this.turn = V.GetOppCol(this.turn);
548 this.subTurn = 1;
549 this.movesCount++;
550 }
551 else this.subTurn = 2;
552 move.flags = JSON.stringify(this.aggregateFlags());
553 V.PlayOnBoard(this.board, move);
554 this.postPlay(move);
555 }
556
557 postPlay(move) {
558 if (move.vanish.length > 0) super.postPlay(move);
559 else if (this.movesCount > 2) this.passFlag++;
560 // Update bikjang flag
561 if (this.kingPos['w'][1] == this.kingPos['b'][1]) {
562 const y = this.kingPos['w'][1];
563 let bikjang = true;
564 for (let x = this.kingPos['b'][0] + 1; x < this.kingPos['w'][0]; x++) {
565 if (this.board[x][y] != V.EMPTY) {
566 bikjang = false;
567 break;
568 }
569 }
570 if (bikjang) this.bikjangFlag++;
571 else this.bikjangFlag = 0;
572 }
573 else this.bikjangFlag = 0;
574 }
575
576 undo(move) {
577 this.disaggregateFlags(JSON.parse(move.flags));
578 V.UndoOnBoard(this.board, move);
579 this.postUndo(move);
580 if (this.movesCount >= 2 || this.subTurn == 1 || move.vanish.length == 0) {
581 this.turn = V.GetOppCol(this.turn);
582 this.movesCount--;
583 }
584 this.subTurn = move.subTurn;
585 }
586
587 postUndo(move) {
588 if (move.vanish.length > 0) super.postUndo(move);
589 }
590
591 getComputerMove() {
592 if (this.movesCount <= 1) {
593 // Special case: swap and pass at random
594 const moves1 = this.getAllValidMoves();
595 const m1 = moves1[randInt(moves1.length)];
596 this.play(m1);
597 if (m1.vanish.length == 0) {
598 this.undo(m1);
599 return m1;
600 }
601 const moves2 = this.getAllValidMoves();
602 const m2 = moves2[randInt(moves2.length)];
603 this.undo(m1);
604 return [m1, m2];
605 }
606 return super.getComputerMove();
607 }
608
609 getNotation(move) {
610 if (move.vanish.length == 0) return "pass";
611 if (move.appear.length == 2) return "S"; //"swap"
612 let notation = super.getNotation(move);
613 if (move.vanish.length == 2 && move.vanish[0].p == V.PAWN)
614 notation = "P" + notation.substr(1);
615 return notation;
616 }
617
618 };