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