Add Makpong, Hoppelpoppel, and Jangqi (rules unwritten yet)
[vchess.git] / client / src / variants / Xiangqi.js
1 import { ChessRules } from "@/base_rules";
2
3 export class XiangqiRules extends ChessRules {
4
5 // NOTE (TODO?) scanKings() could be more efficient (in Jangqi too)
6
7 static get Monochrome() {
8 return true;
9 }
10
11 static get Notoodark() {
12 return true;
13 }
14
15 static get Lines() {
16 let lines = [];
17 // Draw all inter-squares lines, shifted:
18 for (let i = 0; i < V.size.x; i++)
19 lines.push([[i+0.5, 0.5], [i+0.5, V.size.y-0.5]]);
20 for (let j = 0; j < V.size.y; j++)
21 lines.push([[0.5, j+0.5], [V.size.x-0.5, j+0.5]]);
22 // Add palaces:
23 lines.push([[0.5, 3.5], [2.5, 5.5]]);
24 lines.push([[0.5, 5.5], [2.5, 3.5]]);
25 lines.push([[9.5, 3.5], [7.5, 5.5]]);
26 lines.push([[9.5, 5.5], [7.5, 3.5]]);
27 // Show river:
28 lines.push([[4.5, 0.5], [5.5, 8.5]]);
29 lines.push([[5.5, 0.5], [4.5, 8.5]]);
30 return lines;
31 }
32
33 static get HasFlags() {
34 return false;
35 }
36
37 static get HasEnpassant() {
38 return false;
39 }
40
41 static get LoseOnRepetition() {
42 return true;
43 }
44
45 static get ELEPHANT() {
46 return "e";
47 }
48
49 static get CANNON() {
50 return "c";
51 }
52
53 static get ADVISOR() {
54 return "a";
55 }
56
57 static get PIECES() {
58 return [V.PAWN, V.ROOK, V.KNIGHT, V.ELEPHANT, V.ADVISOR, V.KING, V.CANNON];
59 }
60
61 getPpath(b) {
62 return "Xiangqi/" + b;
63 }
64
65 static get size() {
66 return { x: 10, y: 9};
67 }
68
69 getPotentialMovesFrom(sq) {
70 let moves = [];
71 const piece = this.getPiece(sq[0], sq[1]);
72 switch (piece) {
73 case V.PAWN:
74 moves = this.getPotentialPawnMoves(sq);
75 break;
76 case V.ROOK:
77 moves = super.getPotentialRookMoves(sq);
78 break;
79 case V.KNIGHT:
80 moves = this.getPotentialKnightMoves(sq);
81 break;
82 case V.ELEPHANT:
83 moves = this.getPotentialElephantMoves(sq);
84 break;
85 case V.ADVISOR:
86 moves = this.getPotentialAdvisorMoves(sq);
87 break;
88 case V.KING:
89 moves = this.getPotentialKingMoves(sq);
90 break;
91 case V.CANNON:
92 moves = this.getPotentialCannonMoves(sq);
93 break;
94 }
95 if (piece != V.KING && this.kingPos['w'][1] != this.kingPos['b'][1])
96 return moves;
97 if (this.kingPos['w'][1] == this.kingPos['b'][1]) {
98 const colKing = this.kingPos['w'][1];
99 let intercept = 0; //count intercepting pieces
100 for (let i = this.kingPos['b'][0] + 1; i < this.kingPos['w'][0]; i++) {
101 if (this.board[i][colKing] != V.EMPTY) intercept++;
102 }
103 if (intercept >= 2) return moves;
104 // intercept == 1 (0 is impossible):
105 // Any move not removing intercept is OK
106 return moves.filter(m => {
107 return (
108 // From another column?
109 m.start.y != colKing ||
110 // From behind a king? (including kings themselves!)
111 m.start.x <= this.kingPos['b'][0] ||
112 m.start.x >= this.kingPos['w'][0] ||
113 // Intercept piece moving: must remain in-between
114 (
115 m.end.y == colKing &&
116 m.end.x > this.kingPos['b'][0] &&
117 m.end.x < this.kingPos['w'][0]
118 )
119 );
120 });
121 }
122 // piece == king: check only if move.end.y == enemy king column
123 const color = this.getColor(sq[0], sq[1]);
124 const oppCol = V.GetOppCol(color);
125 // colCheck == -1 if unchecked, 1 if checked and occupied,
126 // 0 if checked and clear
127 let colCheck = -1;
128 return moves.filter(m => {
129 if (m.end.y != this.kingPos[oppCol][1]) return true;
130 if (colCheck < 0) {
131 // Do the check:
132 colCheck = 0;
133 for (let i = this.kingPos['b'][0] + 1; i < this.kingPos['w'][0]; i++) {
134 if (this.board[i][m.end.y] != V.EMPTY) {
135 colCheck++;
136 break;
137 }
138 }
139 return colCheck == 1;
140 }
141 // Check already done:
142 return colCheck == 1;
143 });
144 }
145
146 getPotentialPawnMoves([x, y]) {
147 const c = this.getColor(x, y);
148 const shiftX = (c == 'w' ? -1 : 1);
149 const crossedRiver = (c == 'w' && x <= 4 || c == 'b' && x >= 5);
150 const lastRank = (c == 'w' && x == 0 || c == 'b' && x == 9);
151 let steps = [];
152 if (!lastRank) steps.push([shiftX, 0]);
153 if (crossedRiver) {
154 if (y > 0) steps.push([0, -1]);
155 if (y < 9) steps.push([0, 1]);
156 }
157 return super.getSlideNJumpMoves([x, y], steps, "oneStep");
158 }
159
160 knightStepsFromRookStep(step) {
161 if (step[0] == 0) return [ [1, 2*step[1]], [-1, 2*step[1]] ];
162 return [ [2*step[0], 1], [2*step[0], -1] ];
163 }
164
165 getPotentialKnightMoves([x, y]) {
166 let steps = [];
167 for (let rookStep of ChessRules.steps[V.ROOK]) {
168 const [i, j] = [x + rookStep[0], y + rookStep[1]];
169 if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
170 Array.prototype.push.apply(steps,
171 // These moves might be impossible, but need to be checked:
172 this.knightStepsFromRookStep(rookStep));
173 }
174 }
175 return super.getSlideNJumpMoves([x, y], steps, "oneStep");
176 }
177
178 getPotentialElephantMoves([x, y]) {
179 let steps = [];
180 const c = this.getColor(x, y);
181 for (let bishopStep of ChessRules.steps[V.BISHOP]) {
182 const [i, j] = [x + bishopStep[0], y + bishopStep[1]];
183 if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
184 const [newX, newY] = [x + 2*bishopStep[0], y + 2*bishopStep[1]];
185 if ((c == 'w' && newX >= 5) || (c == 'b' && newX <= 4))
186 // A priori valid (elephant don't cross the river)
187 steps.push(bishopStep.map(s => 2*s));
188 // "out of board" checks delayed to next method
189 }
190 }
191 return super.getSlideNJumpMoves([x, y], steps, "oneStep");
192 }
193
194 getPotentialAdvisorMoves([x, y]) {
195 // Diagonal steps inside palace
196 const c = this.getColor(x, y);
197 if (
198 y != 4 ||
199 (c == 'w' && x != V.size.x - 2) ||
200 (c == 'b' && x != 1)
201 ) {
202 // In a corner: only one step available
203 let step = null;
204 const direction = (c == 'w' ? -1 : 1);
205 if ((c == 'w' && x == V.size.x - 1) || (c == 'b' && x == 0)) {
206 // On first line
207 if (y == 3) step = [direction, 1];
208 else step = [direction, -1];
209 }
210 else {
211 // On third line
212 if (y == 3) step = [-direction, 1];
213 else step = [-direction, -1];
214 }
215 return super.getSlideNJumpMoves([x, y], [step], "oneStep");
216 }
217 // In the middle of the palace:
218 return (
219 super.getSlideNJumpMoves([x, y], ChessRules.steps[V.BISHOP], "oneStep")
220 );
221 }
222
223 getPotentialKingMoves([x, y]) {
224 // Orthogonal steps inside palace
225 const c = this.getColor(x, y);
226 if (
227 y != 4 ||
228 (c == 'w' && x != V.size.x - 2) ||
229 (c == 'b' && x != 1)
230 ) {
231 // On the edge: only two steps available
232 let steps = [];
233 if (x < (c == 'w' ? V.size.x - 1 : 2)) steps.push([1, 0]);
234 if (x > (c == 'w' ? V.size.x - 3 : 0)) steps.push([-1, 0]);
235 if (y > 3) steps.push([0, -1]);
236 if (y < 5) steps.push([0, 1]);
237 return super.getSlideNJumpMoves([x, y], steps, "oneStep");
238 }
239 // In the middle of the palace:
240 return (
241 super.getSlideNJumpMoves([x, y], ChessRules.steps[V.ROOK], "oneStep")
242 );
243 }
244
245 // NOTE: duplicated from Shako (TODO?)
246 getPotentialCannonMoves([x, y]) {
247 const oppCol = V.GetOppCol(this.turn);
248 let moves = [];
249 // Look in every direction until an obstacle (to jump) is met
250 for (const step of V.steps[V.ROOK]) {
251 let i = x + step[0];
252 let j = y + step[1];
253 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
254 moves.push(this.getBasicMove([x, y], [i, j]));
255 i += step[0];
256 j += step[1];
257 }
258 // Then, search for an enemy
259 i += step[0];
260 j += step[1];
261 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
262 i += step[0];
263 j += step[1];
264 }
265 if (V.OnBoard(i, j) && this.getColor(i, j) == oppCol)
266 moves.push(this.getBasicMove([x, y], [i, j]));
267 }
268 return moves;
269 }
270
271 // (King) Never attacked by advisor, since it stays in the palace
272 // Also, never attacked by elephants since they don't cross the river.
273 isAttacked(sq, color) {
274 return (
275 this.isAttackedByPawn(sq, color) ||
276 super.isAttackedByRook(sq, color) ||
277 this.isAttackedByKnight(sq, color) ||
278 this.isAttackedByCannon(sq, color)
279 );
280 }
281
282 isAttackedByPawn([x, y], color) {
283 // The pawn necessarily crossed the river (attack on king)
284 const shiftX = (color == 'w' ? 1 : -1); //shift from king
285 return super.isAttackedBySlideNJump(
286 [x, y], color, V.PAWN, [[shiftX, 0], [0, 1], [0, -1]], "oneStep");
287 }
288
289 knightStepsFromBishopStep(step) {
290 return [ [2*step[0], step[1]], [step[0], 2*step[1]] ];
291 }
292
293 isAttackedByKnight([x, y], color) {
294 // Check bishop steps: if empty, look continuation knight step
295 let steps = [];
296 for (let s of ChessRules.steps[V.BISHOP]) {
297 const [i, j] = [x + s[0], y + s[1]];
298 if (
299 V.OnBoard(i, j) &&
300 this.board[i][j] == V.EMPTY
301 ) {
302 Array.prototype.push.apply(steps, this.knightStepsFromBishopStep(s));
303 }
304 }
305 return (
306 super.isAttackedBySlideNJump([x, y], color, V.KNIGHT, steps, "oneStep")
307 );
308 }
309
310 // NOTE: duplicated from Shako (TODO?)
311 isAttackedByCannon([x, y], color) {
312 // Reversed process: is there an obstacle in line,
313 // and a cannon next in the same line?
314 for (const step of V.steps[V.ROOK]) {
315 let [i, j] = [x+step[0], y+step[1]];
316 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
317 i += step[0];
318 j += step[1];
319 }
320 if (V.OnBoard(i, j)) {
321 // Keep looking in this direction
322 i += step[0];
323 j += step[1];
324 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
325 i += step[0];
326 j += step[1];
327 }
328 if (
329 V.OnBoard(i, j) &&
330 this.getPiece(i, j) == V.CANNON &&
331 this.getColor(i, j) == color
332 ) {
333 return true;
334 }
335 }
336 }
337 return false;
338 }
339
340 getCurrentScore() {
341 if (this.atLeastOneMove()) return "*";
342 // Game over
343 const color = this.turn;
344 // No valid move: I lose!
345 return (color == "w" ? "0-1" : "1-0");
346 }
347
348 static get VALUES() {
349 return {
350 p: 1,
351 r: 9,
352 n: 4,
353 e: 2.5,
354 a: 2,
355 c: 4.5,
356 k: 1000
357 };
358 }
359
360 evalPosition() {
361 let evaluation = 0;
362 for (let i = 0; i < V.size.x; i++) {
363 for (let j = 0; j < V.size.y; j++) {
364 if (this.board[i][j] != V.EMPTY) {
365 const c = this.getColor(i, j);
366 const sign = (c == 'w' ? 1 : -1);
367 const piece = this.getPiece(i, j);
368 let pieceEval = V.VALUES[this.getPiece(i, j)];
369 if (
370 piece == V.PAWN &&
371 (
372 (c == 'w' && i <= 4) ||
373 (c == 'b' && i >= 5)
374 )
375 ) {
376 // Pawn crossed the river: higher value
377 pieceEval++;
378 }
379 evaluation += sign * pieceEval;
380 }
381 }
382 }
383 return evaluation;
384 }
385
386 static get SEARCH_DEPTH() {
387 return 2;
388 }
389
390 static GenRandInitFen() {
391 // No randomization here (TODO?)
392 return "rneakaenr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNEAKAENR w 0";
393 }
394
395 getNotation(move) {
396 let notation = super.getNotation(move);
397 if (move.vanish.length == 2 && move.vanish[0].p == V.PAWN)
398 notation = "P" + substr(notation, 1);
399 return notation;
400 }
401
402 };