98549db49de1229d6ad97bca42da561fb304f916
[vchess.git] / client / src / variants / Fullcavalry.js
1 import { ArrayFun } from "@/utils/array";
2 import { randInt } from "@/utils/alea";
3 import { ChessRules, PiPo, Move } from "@/base_rules";
4
5 export class FullcavalryRules extends ChessRules {
6
7 static get LANCER() {
8 return "l";
9 }
10
11 static get IMAGE_EXTENSION() {
12 // Temporarily, for the time SVG pieces are being designed:
13 return ".png";
14 }
15
16 // Lancer directions *from white perspective*
17 static get LANCER_DIRS() {
18 return {
19 'c': [-1, 0], //north
20 'd': [-1, 1], //N-E
21 'e': [0, 1], //east
22 'f': [1, 1], //S-E
23 'g': [1, 0], //south
24 'h': [1, -1], //S-W
25 'm': [0, -1], //west
26 'o': [-1, -1] //N-W
27 };
28 }
29
30 static get PIECES() {
31 return ChessRules.PIECES.concat(Object.keys(V.LANCER_DIRS));
32 }
33
34 getPiece(i, j) {
35 const piece = this.board[i][j].charAt(1);
36 // Special lancer case: 8 possible orientations
37 if (Object.keys(V.LANCER_DIRS).includes(piece)) return V.LANCER;
38 return piece;
39 }
40
41 getPpath(b, color, score, orientation) {
42 if (Object.keys(V.LANCER_DIRS).includes(b[1])) {
43 if (orientation == 'w') return "Eightpieces/tmp_png/" + b;
44 // Find opposite direction for adequate display:
45 let oppDir = '';
46 switch (b[1]) {
47 case 'c':
48 oppDir = 'g';
49 break;
50 case 'g':
51 oppDir = 'c';
52 break;
53 case 'd':
54 oppDir = 'h';
55 break;
56 case 'h':
57 oppDir = 'd';
58 break;
59 case 'e':
60 oppDir = 'm';
61 break;
62 case 'm':
63 oppDir = 'e';
64 break;
65 case 'f':
66 oppDir = 'o';
67 break;
68 case 'o':
69 oppDir = 'f';
70 break;
71 }
72 return "Eightpieces/tmp_png/" + b[0] + oppDir;
73 }
74 // TODO: after we have SVG pieces, remove the folder and next prefix:
75 return "Eightpieces/tmp_png/" + b;
76 }
77
78 getPPpath(m, orientation) {
79 // If castle, show choices on m.appear[1]:
80 const index = (m.appear.length == 2 ? 1 : 0);
81 return (
82 this.getPpath(
83 m.appear[index].c + m.appear[index].p,
84 null,
85 null,
86 orientation
87 )
88 );
89 }
90
91 static GenRandInitFen(randomness) {
92 if (randomness == 0)
93 // Deterministic:
94 return "efbqkbnm/pppppppp/8/8/8/8/PPPPPPPP/EDBQKBNM w 0 ahah -";
95
96 const baseFen = ChessRules.GenRandInitFen(randomness);
97 // Replace rooks by lancers with expected orientation:
98 const firstBlackRook = baseFen.indexOf('r'),
99 lastBlackRook = baseFen.lastIndexOf('r'),
100 firstWhiteRook = baseFen.indexOf('R'),
101 lastWhiteRook = baseFen.lastIndexOf('R');
102 return (
103 baseFen.substring(0, firstBlackRook) +
104 (firstBlackRook <= 3 ? 'e' : 'm') +
105 baseFen.substring(firstBlackRook + 1, lastBlackRook) +
106 (lastBlackRook >= 5 ? 'm' : 'e') +
107 // Subtract 35 = total number of characters before last FEN row:
108 // 8x3 (full rows) + 4 (empty rows) + 7 (separators)
109 baseFen.substring(lastBlackRook + 1, firstWhiteRook) +
110 (firstWhiteRook - 35 <= 3 ? 'E' : 'M') +
111 baseFen.substring(firstWhiteRook + 1, lastWhiteRook) +
112 (lastWhiteRook - 35 >= 5 ? 'M' : 'E') +
113 baseFen.substring(lastWhiteRook + 1)
114 );
115 }
116
117 // Because of the lancers, getPiece() could be wrong:
118 // use board[x][y][1] instead (always valid).
119 // TODO: base implementation now uses this too (no?)
120 getBasicMove([sx, sy], [ex, ey], tr) {
121 const initColor = this.getColor(sx, sy);
122 const initPiece = this.board[sx][sy].charAt(1);
123 let mv = new Move({
124 appear: [
125 new PiPo({
126 x: ex,
127 y: ey,
128 c: tr ? tr.c : initColor,
129 p: tr ? tr.p : initPiece
130 })
131 ],
132 vanish: [
133 new PiPo({
134 x: sx,
135 y: sy,
136 c: initColor,
137 p: initPiece
138 })
139 ]
140 });
141
142 // The opponent piece disappears if we take it
143 if (this.board[ex][ey] != V.EMPTY) {
144 mv.vanish.push(
145 new PiPo({
146 x: ex,
147 y: ey,
148 c: this.getColor(ex, ey),
149 p: this.board[ex][ey].charAt(1)
150 })
151 );
152 }
153
154 return mv;
155 }
156
157 getPotentialMovesFrom([x, y]) {
158 if (this.getPiece(x, y) == V.LANCER)
159 return this.getPotentialLancerMoves([x, y]);
160 return super.getPotentialMovesFrom([x, y]);
161 }
162
163 getPotentialPawnMoves([x, y]) {
164 const color = this.getColor(x, y);
165 let moves = [];
166 const [sizeX, sizeY] = [V.size.x, V.size.y];
167 let shiftX = (color == "w" ? -1 : 1);
168 const startRank = color == "w" ? sizeX - 2 : 1;
169 const lastRank = color == "w" ? 0 : sizeX - 1;
170
171 let finalPieces = [V.PAWN];
172 if (x + shiftX == lastRank) {
173 // Only allow direction facing inside board:
174 const allowedLancerDirs =
175 lastRank == 0
176 ? ['e', 'f', 'g', 'h', 'm']
177 : ['c', 'd', 'e', 'm', 'o'];
178 finalPieces = allowedLancerDirs.concat([V.KNIGHT, V.BISHOP, V.QUEEN]);
179 }
180 if (this.board[x + shiftX][y] == V.EMPTY) {
181 // One square forward
182 for (let piece of finalPieces) {
183 moves.push(
184 this.getBasicMove([x, y], [x + shiftX, y], {
185 c: color,
186 p: piece
187 })
188 );
189 }
190 if (x == startRank && this.board[x + 2 * shiftX][y] == V.EMPTY)
191 // Two squares jump
192 moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y]));
193 }
194 // Captures
195 for (let shiftY of [-1, 1]) {
196 if (
197 y + shiftY >= 0 &&
198 y + shiftY < sizeY &&
199 this.board[x + shiftX][y + shiftY] != V.EMPTY &&
200 this.canTake([x, y], [x + shiftX, y + shiftY])
201 ) {
202 for (let piece of finalPieces) {
203 moves.push(
204 this.getBasicMove([x, y], [x + shiftX, y + shiftY], {
205 c: color,
206 p: piece
207 })
208 );
209 }
210 }
211 }
212
213 // Add en-passant captures
214 Array.prototype.push.apply(
215 moves,
216 this.getEnpassantCaptures([x, y], shiftX)
217 );
218
219 return moves;
220 }
221
222 // Obtain all lancer moves in "step" direction
223 getPotentialLancerMoves_aux([x, y], step, tr) {
224 let moves = [];
225 // Add all moves to vacant squares until opponent is met:
226 const color = this.getColor(x, y);
227 const oppCol = V.GetOppCol(color)
228 let sq = [x + step[0], y + step[1]];
229 while (V.OnBoard(sq[0], sq[1]) && this.getColor(sq[0], sq[1]) != oppCol) {
230 if (this.board[sq[0]][sq[1]] == V.EMPTY)
231 moves.push(this.getBasicMove([x, y], sq, tr));
232 sq[0] += step[0];
233 sq[1] += step[1];
234 }
235 if (V.OnBoard(sq[0], sq[1]))
236 // Add capturing move
237 moves.push(this.getBasicMove([x, y], sq, tr));
238 return moves;
239 }
240
241 getPotentialLancerMoves([x, y]) {
242 let moves = [];
243 // Add all lancer possible orientations, similar to pawn promotions.
244 const color = this.getColor(x, y);
245 const dirCode = this.board[x][y][1];
246 const curDir = V.LANCER_DIRS[dirCode];
247 const monodirMoves =
248 this.getPotentialLancerMoves_aux([x, y], V.LANCER_DIRS[dirCode]);
249 monodirMoves.forEach(m => {
250 Object.keys(V.LANCER_DIRS).forEach(k => {
251 const newDir = V.LANCER_DIRS[k];
252 // Prevent orientations toward outer board:
253 if (V.OnBoard(m.end.x + newDir[0], m.end.y + newDir[1])) {
254 let mk = JSON.parse(JSON.stringify(m));
255 mk.appear[0].p = k;
256 moves.push(mk);
257 }
258 });
259 });
260 return moves;
261 }
262
263 getCastleMoves([x, y]) {
264 const c = this.getColor(x, y);
265
266 // Castling ?
267 const oppCol = V.GetOppCol(c);
268 let moves = [];
269 let i = 0;
270 // King, then lancer:
271 const finalSquares = [ [2, 3], [V.size.y - 2, V.size.y - 3] ];
272 castlingCheck: for (
273 let castleSide = 0;
274 castleSide < 2;
275 castleSide++ //large, then small
276 ) {
277 if (this.castleFlags[c][castleSide] >= V.size.y) continue;
278 // If this code is reached, lancer and king are on initial position
279
280 const lancerPos = this.castleFlags[c][castleSide];
281 const castlingPiece = this.board[x][lancerPos].charAt(1);
282
283 // Nothing on the path of the king ? (and no checks)
284 const finDist = finalSquares[castleSide][0] - y;
285 let step = finDist / Math.max(1, Math.abs(finDist));
286 i = y;
287 do {
288 if (
289 (this.isAttacked([x, i], oppCol)) ||
290 (
291 this.board[x][i] != V.EMPTY &&
292 // NOTE: next check is enough, because of chessboard constraints
293 (this.getColor(x, i) != c || ![y, lancerPos].includes(i))
294 )
295 ) {
296 continue castlingCheck;
297 }
298 i += step;
299 } while (i != finalSquares[castleSide][0]);
300
301 // Nothing on final squares, except maybe king and castling lancer?
302 for (i = 0; i < 2; i++) {
303 if (
304 finalSquares[castleSide][i] != lancerPos &&
305 this.board[x][finalSquares[castleSide][i]] != V.EMPTY &&
306 (
307 finalSquares[castleSide][i] != y ||
308 this.getColor(x, finalSquares[castleSide][i]) != c
309 )
310 ) {
311 continue castlingCheck;
312 }
313 }
314
315 // If this code is reached, castle is valid
316 let allowedLancerDirs = [castlingPiece];
317 if (finalSquares[castleSide][1] != lancerPos) {
318 // It moved: allow reorientation
319 allowedLancerDirs =
320 x == 0
321 ? ['e', 'f', 'g', 'h', 'm']
322 : ['c', 'd', 'e', 'm', 'o'];
323 }
324 allowedLancerDirs.forEach(dir => {
325 moves.push(
326 new Move({
327 appear: [
328 new PiPo({
329 x: x,
330 y: finalSquares[castleSide][0],
331 p: V.KING,
332 c: c
333 }),
334 new PiPo({
335 x: x,
336 y: finalSquares[castleSide][1],
337 p: dir,
338 c: c
339 })
340 ],
341 vanish: [
342 new PiPo({ x: x, y: y, p: V.KING, c: c }),
343 new PiPo({ x: x, y: lancerPos, p: castlingPiece, c: c })
344 ],
345 end:
346 Math.abs(y - lancerPos) <= 2
347 ? { x: x, y: lancerPos }
348 : { x: x, y: y + 2 * (castleSide == 0 ? -1 : 1) }
349 })
350 );
351 });
352 }
353
354 return moves;
355 }
356
357 isAttacked(sq, color) {
358 return (
359 super.isAttacked(sq, color) ||
360 this.isAttackedByLancer(sq, color)
361 );
362 }
363
364 isAttackedByLancer([x, y], color) {
365 for (let step of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) {
366 // If in this direction there are only enemy pieces and empty squares,
367 // and we meet a lancer: can he reach us?
368 // NOTE: do not stop at first lancer, there might be several!
369 let coord = { x: x + step[0], y: y + step[1] };
370 let lancerPos = [];
371 while (
372 V.OnBoard(coord.x, coord.y) &&
373 (
374 this.board[coord.x][coord.y] == V.EMPTY ||
375 this.getColor(coord.x, coord.y) == color
376 )
377 ) {
378 if (
379 this.getPiece(coord.x, coord.y) == V.LANCER &&
380 !this.isImmobilized([coord.x, coord.y])
381 ) {
382 lancerPos.push({x: coord.x, y: coord.y});
383 }
384 coord.x += step[0];
385 coord.y += step[1];
386 }
387 for (let xy of lancerPos) {
388 const dir = V.LANCER_DIRS[this.board[xy.x][xy.y].charAt(1)];
389 if (dir[0] == -step[0] && dir[1] == -step[1]) return true;
390 }
391 }
392 return false;
393 }
394
395 static get VALUES() {
396 return Object.assign(
397 { l: 4.8 }, //Jeff K. estimation (for Eightpieces)
398 ChessRules.VALUES
399 );
400 }
401
402 // For moves notation:
403 static get LANCER_DIRNAMES() {
404 return {
405 'c': "N",
406 'd': "NE",
407 'e': "E",
408 'f': "SE",
409 'g': "S",
410 'h': "SW",
411 'm': "W",
412 'o': "NW"
413 };
414 }
415
416 filterValid(moves) {
417 // At move 1, forbid captures (in case of...):
418 if (this.movesCount >= 2) return moves;
419 return moves.filter(m => m.vanish.length == 1);
420 }
421
422 getNotation(move) {
423 let notation = super.getNotation(move);
424 if (Object.keys(V.LANCER_DIRNAMES).includes(move.vanish[0].p))
425 // Lancer: add direction info
426 notation += "=" + V.LANCER_DIRNAMES[move.appear[0].p];
427 else if (move.appear.length == 2 && move.vanish[1].p != move.appear[1].p)
428 // Same after castle:
429 notation += "+L:" + V.LANCER_DIRNAMES[move.appear[1].p];
430 else if (
431 move.vanish[0].p == V.PAWN &&
432 Object.keys(V.LANCER_DIRNAMES).includes(move.appear[0].p)
433 ) {
434 // Fix promotions in lancer:
435 notation = notation.slice(0, -1) +
436 "L:" + V.LANCER_DIRNAMES[move.appear[0].p];
437 }
438 return notation;
439 }
440
441 };