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