1 import { ChessRules
, Move
, PiPo
} from "@/base_rules";
2 import { randInt
} from "@/utils/alea";
4 export class JanggiRules
extends ChessRules
{
6 static get Monochrome() {
10 static get Notoodark() {
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]]);
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]]);
29 // No castle, but flag: bikjang
30 static get HasCastle() {
34 static get HasEnpassant() {
38 static get LoseOnRepetition() {
42 static get ELEPHANT() {
50 static get ADVISOR() {
55 return [V
.PAWN
, V
.ROOK
, V
.KNIGHT
, V
.ELEPHANT
, V
.ADVISOR
, V
.KING
, V
.CANNON
];
59 return "Jiangqi/" + b
;
63 return { x: 10, y: 9};
66 static IsGoodFlags(flags
) {
67 // bikjang status of last move + pass
68 return !!flags
.match(/^[0-2]{2,2}$/);
72 return [this.bikjangFlag
, this.passFlag
];
75 disaggregateFlags(flags
) {
76 this.bikjangFlag
= flags
[0];
77 this.passFlag
= flags
[1];
81 return this.bikjangFlag
.toString() + this.passFlag
.toString()
85 this.bikjangFlag
= parseInt(fenflags
.charAt(0), 10);
86 this.passFlag
= parseInt(fenflags
.charAt(1), 10);
89 setOtherVariables(fen
) {
90 super.setOtherVariables(fen
);
91 // Sub-turn is useful only at first move...
95 getPotentialMovesFrom([x
, y
]) {
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)
105 start: { x: this.kingPos
[c
][0], y: this.kingPos
[c
][1] },
106 end: { x: this.kingPos
[oppCol
][0], y: this.kingPos
[oppCol
][1] }
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
) {
146 start: { x: x
, y: y
},
147 end: { x: x
, y: destFile
}
153 let normalMoves
= [];
154 switch (this.getPiece(x
, y
)) {
156 normalMoves
= this.getPotentialPawnMoves([x
, y
]);
159 normalMoves
= this.getPotentialRookMoves([x
, y
]);
162 normalMoves
= this.getPotentialKnightMoves([x
, y
]);
165 normalMoves
= this.getPotentialElephantMoves([x
, y
]);
168 normalMoves
= this.getPotentialAdvisorMoves([x
, y
]);
171 normalMoves
= this.getPotentialKingMoves([x
, y
]);
174 normalMoves
= this.getPotentialCannonMoves([x
, y
]);
177 Array
.prototype.push
.apply(moves
, normalMoves
);
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]);
195 return super.getSlideNJumpMoves([x
, y
], steps
, "oneStep");
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] ];
203 getPotentialKnightMoves([x
, y
]) {
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
));
213 return super.getSlideNJumpMoves([x
, y
], steps
, "oneStep");
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] ];
221 getPotentialElephantMoves([x
, y
]) {
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
236 return super.getSlideNJumpMoves([x
, y
], steps
, "oneStep");
239 palacePeopleMoves([x
, y
]) {
240 const c
= this.getColor(x
, y
);
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:
251 (c
== 'w' && x
!= 8) ||
255 // In a corner: maximum one diagonal step available
257 const direction
= (c
== 'w' ? -1 : 1);
258 if ((c
== 'w' && x
== 9) || (c
== 'b' && x
== 0)) {
260 if (y
== 3) step
= [direction
, 1];
261 else step
= [direction
, -1];
263 else if ((c
== 'w' && x
== 7) || (c
== 'b' && x
== 2)) {
265 if (y
== 3) step
= [-direction
, 1];
266 else step
= [-direction
, -1];
273 (c
== 'w' && x
== 8) ||
277 // At the middle: all directions available
278 Array
.prototype.push
.apply(steps
, ChessRules
.steps
[V
.BISHOP
]);
280 return super.getSlideNJumpMoves([x
, y
], steps
, "oneStep");
283 getPotentialAdvisorMoves(sq
) {
284 return this.palacePeopleMoves(sq
);
287 getPotentialKingMoves(sq
) {
288 return this.palacePeopleMoves(sq
);
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
]));
302 if (this.getColor(xx
, yy
) == oppCol
)
303 moves
.push(this.getBasicMove([x
, y
], [xx
, yy
]));
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(
312 super.getSlideNJumpMoves([x
, y
],
313 ChessRules
.steps
[V
.BISHOP
],
320 // NOTE: (mostly) duplicated from Shako (TODO?)
321 getPotentialCannonMoves([x
, y
]) {
322 const oppCol
= V
.GetOppCol(this.turn
);
324 // Look in every direction until an obstacle (to jump) is met
325 for (const step
of V
.steps
[V
.ROOK
]) {
328 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
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
) {
336 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
337 moves
.push(this.getBasicMove([x
, y
], [i
, j
]));
343 this.getColor(i
, j
) == oppCol
&&
344 this.getPiece(i
, j
) != V
.CANNON
346 moves
.push(this.getBasicMove([x
, y
], [i
, j
]));
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]];
356 this.board
[x1
][y1
] != V
.EMPTY
&&
357 this.getPiece(x1
, y1
) != V
.CANNON
&&
359 this.board
[x2
][y2
] == V
.EMPTY
||
361 this.getColor(x2
, y2
) == oppCol
&&
362 this.getPiece(x2
, y2
) != V
.CANNON
366 moves
.push(this.getBasicMove([x
, y
], [x2
, y2
]));
372 // (King) Never attacked by advisor, since it stays in the palace
373 isAttacked(sq
, color
) {
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
)
383 onPalaceDiagonal([x
, y
]) {
385 (y
== 4 && [1, 8].includes(x
)) ||
386 ([3, 5].includes(y
) && [0, 2, 7, 9].includes(x
))
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")
397 if (this.onPalaceDiagonal([x
, y
])) {
398 for (let yStep
of [-1, 1]) {
399 const [xx
, yy
] = [x
+ shiftX
, y
+ yStep
];
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
413 knightStepsFromBishopStep(step
) {
414 return [ [2*step
[0], step
[1]], [step
[0], 2*step
[1]] ];
417 isAttackedByKnight([x
, y
], color
) {
418 // Check bishop steps: if empty, look continuation knight step
420 for (let s
of ChessRules
.steps
[V
.BISHOP
]) {
421 const [i
, j
] = [x
+ s
[0], y
+ s
[1]];
424 this.board
[i
][j
] == V
.EMPTY
426 Array
.prototype.push
.apply(steps
, this.knightStepsFromBishopStep(s
));
430 super.isAttackedBySlideNJump([x
, y
], color
, V
.KNIGHT
, steps
, "oneStep")
434 elephantStepsFromBishopStep(step
) {
435 return [ [3*step
[0], 2*step
[1]], [2*step
[0], 3*step
[1]] ];
438 isAttackedByElephant([x
, y
], color
) {
439 // Check bishop steps: if empty, look continuation elephant step
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]];
445 V
.OnBoard(i2
, j2
) && this.board
[i2
][j2
] == V
.EMPTY
&&
446 V
.OnBoard(i1
, j1
) && this.board
[i1
][j1
] == V
.EMPTY
448 Array
.prototype.push
.apply(steps
, this.elephantStepsFromBishopStep(s
));
452 super.isAttackedBySlideNJump([x
, y
], color
, V
.ELEPHANT
, steps
, "oneStep")
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]];
466 this.onPalaceDiagonal([xx
, yy
])
468 if (this.board
[xx
][yy
] != V
.EMPTY
) {
470 this.getColor(xx
, yy
) == color
&&
471 this.getPiece(xx
, yy
) == V
.ROOK
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
) {
495 if (V
.OnBoard(i
, j
) && this.getPiece(i
, j
) != V
.CANNON
) {
496 // Keep looking in this direction
499 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
505 this.getPiece(i
, j
) == V
.CANNON
&&
506 this.getColor(i
, j
) == color
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");
524 static get VALUES() {
536 static get SEARCH_DEPTH() {
540 static GenRandInitFen() {
541 // No randomization here (but initial setup choice)
543 "rnea1aenr/4k4/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/4K4/RNEA1AENR w 0 00"
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
);
554 else this.subTurn
= 2;
555 move.flags
= JSON
.stringify(this.aggregateFlags());
556 V
.PlayOnBoard(this.board
, 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];
567 for (let x
= this.kingPos
['b'][0] + 1; x
< this.kingPos
['w'][0]; x
++) {
568 if (this.board
[x
][y
] != V
.EMPTY
) {
573 if (bikjang
) this.bikjangFlag
++;
574 else this.bikjangFlag
= 0;
576 else this.bikjangFlag
= 0;
580 this.disaggregateFlags(JSON
.parse(move.flags
));
581 V
.UndoOnBoard(this.board
, move);
583 if (this.movesCount
>= 2 || this.subTurn
== 1 || move.vanish
.length
== 0) {
584 this.turn
= V
.GetOppCol(this.turn
);
587 this.subTurn
= move.subTurn
;
591 if (move.vanish
.length
> 0) super.postUndo(move);
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
)];
600 if (m1
.vanish
.length
== 0) {
604 const moves2
= this.getAllValidMoves();
605 const m2
= moves2
[randInt(moves2
.length
)];
609 return super.getComputerMove();
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);