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