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