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