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