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