Fix Fullcavalry: lancers' initial orientation
[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 baseFen.substring(lastBlackRook + 1, firstWhiteRook) +
108 (firstWhiteRook <= 3 ? 'E' : 'M') +
109 baseFen.substring(firstWhiteRook + 1, lastWhiteRook) +
110 (lastWhiteRook >= 5 ? 'M' : 'E') +
111 baseFen.substring(lastWhiteRook + 1)
112 );
113 }
114
115 // Because of the lancers, getPiece() could be wrong:
116 // use board[x][y][1] instead (always valid).
117 // TODO: base implementation now uses this too (no?)
118 getBasicMove([sx, sy], [ex, ey], tr) {
119 const initColor = this.getColor(sx, sy);
120 const initPiece = this.board[sx][sy].charAt(1);
121 let mv = new Move({
122 appear: [
123 new PiPo({
124 x: ex,
125 y: ey,
126 c: tr ? tr.c : initColor,
127 p: tr ? tr.p : initPiece
128 })
129 ],
130 vanish: [
131 new PiPo({
132 x: sx,
133 y: sy,
134 c: initColor,
135 p: initPiece
136 })
137 ]
138 });
139
140 // The opponent piece disappears if we take it
141 if (this.board[ex][ey] != V.EMPTY) {
142 mv.vanish.push(
143 new PiPo({
144 x: ex,
145 y: ey,
146 c: this.getColor(ex, ey),
147 p: this.board[ex][ey].charAt(1)
148 })
149 );
150 }
151
152 return mv;
153 }
154
155 getPotentialMovesFrom([x, y]) {
156 if (this.getPiece(x, y) == V.LANCER)
157 return this.getPotentialLancerMoves([x, y]);
158 return super.getPotentialMovesFrom([x, y]);
159 }
160
161 getPotentialPawnMoves([x, y]) {
162 const color = this.getColor(x, y);
163 let moves = [];
164 const [sizeX, sizeY] = [V.size.x, V.size.y];
165 let shiftX = (color == "w" ? -1 : 1);
166 const startRank = color == "w" ? sizeX - 2 : 1;
167 const lastRank = color == "w" ? 0 : sizeX - 1;
168
169 let finalPieces = [V.PAWN];
170 if (x + shiftX == lastRank) {
171 // Only allow direction facing inside board:
172 const allowedLancerDirs =
173 lastRank == 0
174 ? ['e', 'f', 'g', 'h', 'm']
175 : ['c', 'd', 'e', 'm', 'o'];
176 finalPieces = allowedLancerDirs.concat([V.KNIGHT, V.BISHOP, V.QUEEN]);
177 }
178 if (this.board[x + shiftX][y] == V.EMPTY) {
179 // One square forward
180 for (let piece of finalPieces) {
181 moves.push(
182 this.getBasicMove([x, y], [x + shiftX, y], {
183 c: color,
184 p: piece
185 })
186 );
187 }
188 if (x == startRank && this.board[x + 2 * shiftX][y] == V.EMPTY)
189 // Two squares jump
190 moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y]));
191 }
192 // Captures
193 for (let shiftY of [-1, 1]) {
194 if (
195 y + shiftY >= 0 &&
196 y + shiftY < sizeY &&
197 this.board[x + shiftX][y + shiftY] != V.EMPTY &&
198 this.canTake([x, y], [x + shiftX, y + shiftY])
199 ) {
200 for (let piece of finalPieces) {
201 moves.push(
202 this.getBasicMove([x, y], [x + shiftX, y + shiftY], {
203 c: color,
204 p: piece
205 })
206 );
207 }
208 }
209 }
210
211 // Add en-passant captures
212 Array.prototype.push.apply(
213 moves,
214 this.getEnpassantCaptures([x, y], shiftX)
215 );
216
217 return moves;
218 }
219
220 // Obtain all lancer moves in "step" direction
221 getPotentialLancerMoves_aux([x, y], step, tr) {
222 let moves = [];
223 // Add all moves to vacant squares until opponent is met:
224 const color = this.getColor(x, y);
225 const oppCol = V.GetOppCol(color)
226 let sq = [x + step[0], y + step[1]];
227 while (V.OnBoard(sq[0], sq[1]) && this.getColor(sq[0], sq[1]) != oppCol) {
228 if (this.board[sq[0]][sq[1]] == V.EMPTY)
229 moves.push(this.getBasicMove([x, y], sq, tr));
230 sq[0] += step[0];
231 sq[1] += step[1];
232 }
233 if (V.OnBoard(sq[0], sq[1]))
234 // Add capturing move
235 moves.push(this.getBasicMove([x, y], sq, tr));
236 return moves;
237 }
238
239 getPotentialLancerMoves([x, y]) {
240 let moves = [];
241 // Add all lancer possible orientations, similar to pawn promotions.
242 const color = this.getColor(x, y);
243 const dirCode = this.board[x][y][1];
244 const curDir = V.LANCER_DIRS[dirCode];
245 const monodirMoves =
246 this.getPotentialLancerMoves_aux([x, y], V.LANCER_DIRS[dirCode]);
247 monodirMoves.forEach(m => {
248 Object.keys(V.LANCER_DIRS).forEach(k => {
249 const newDir = V.LANCER_DIRS[k];
250 // Prevent orientations toward outer board:
251 if (V.OnBoard(m.end.x + newDir[0], m.end.y + newDir[1])) {
252 let mk = JSON.parse(JSON.stringify(m));
253 mk.appear[0].p = k;
254 moves.push(mk);
255 }
256 });
257 });
258 return moves;
259 }
260
261 getCastleMoves([x, y]) {
262 const c = this.getColor(x, y);
263
264 // Castling ?
265 const oppCol = V.GetOppCol(c);
266 let moves = [];
267 let i = 0;
268 // King, then lancer:
269 const finalSquares = [ [2, 3], [V.size.y - 2, V.size.y - 3] ];
270 castlingCheck: for (
271 let castleSide = 0;
272 castleSide < 2;
273 castleSide++ //large, then small
274 ) {
275 if (this.castleFlags[c][castleSide] >= V.size.y) continue;
276 // If this code is reached, lancer and king are on initial position
277
278 const lancerPos = this.castleFlags[c][castleSide];
279 const castlingPiece = this.board[x][lancerPos].charAt(1);
280
281 // Nothing on the path of the king ? (and no checks)
282 const finDist = finalSquares[castleSide][0] - y;
283 let step = finDist / Math.max(1, Math.abs(finDist));
284 i = y;
285 do {
286 if (
287 (this.isAttacked([x, i], oppCol)) ||
288 (
289 this.board[x][i] != V.EMPTY &&
290 // NOTE: next check is enough, because of chessboard constraints
291 (this.getColor(x, i) != c || ![y, lancerPos].includes(i))
292 )
293 ) {
294 continue castlingCheck;
295 }
296 i += step;
297 } while (i != finalSquares[castleSide][0]);
298
299 // Nothing on final squares, except maybe king and castling lancer?
300 for (i = 0; i < 2; i++) {
301 if (
302 finalSquares[castleSide][i] != lancerPos &&
303 this.board[x][finalSquares[castleSide][i]] != V.EMPTY &&
304 (
305 finalSquares[castleSide][i] != y ||
306 this.getColor(x, finalSquares[castleSide][i]) != c
307 )
308 ) {
309 continue castlingCheck;
310 }
311 }
312
313 // If this code is reached, castle is valid
314 let allowedLancerDirs = [castlingPiece];
315 if (finalSquares[castleSide][1] != lancerPos) {
316 // It moved: allow reorientation
317 allowedLancerDirs =
318 x == 0
319 ? ['e', 'f', 'g', 'h', 'm']
320 : ['c', 'd', 'e', 'm', 'o'];
321 }
322 allowedLancerDirs.forEach(dir => {
323 moves.push(
324 new Move({
325 appear: [
326 new PiPo({
327 x: x,
328 y: finalSquares[castleSide][0],
329 p: V.KING,
330 c: c
331 }),
332 new PiPo({
333 x: x,
334 y: finalSquares[castleSide][1],
335 p: dir,
336 c: c
337 })
338 ],
339 vanish: [
340 new PiPo({ x: x, y: y, p: V.KING, c: c }),
341 new PiPo({ x: x, y: lancerPos, p: castlingPiece, c: c })
342 ],
343 end:
344 Math.abs(y - lancerPos) <= 2
345 ? { x: x, y: lancerPos }
346 : { x: x, y: y + 2 * (castleSide == 0 ? -1 : 1) }
347 })
348 );
349 });
350 }
351
352 return moves;
353 }
354
355 isAttacked(sq, color) {
356 return (
357 super.isAttacked(sq, color) ||
358 this.isAttackedByLancer(sq, color)
359 );
360 }
361
362 isAttackedByLancer([x, y], color) {
363 for (let step of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) {
364 // If in this direction there are only enemy pieces and empty squares,
365 // and we meet a lancer: can he reach us?
366 // NOTE: do not stop at first lancer, there might be several!
367 let coord = { x: x + step[0], y: y + step[1] };
368 let lancerPos = [];
369 while (
370 V.OnBoard(coord.x, coord.y) &&
371 (
372 this.board[coord.x][coord.y] == V.EMPTY ||
373 this.getColor(coord.x, coord.y) == color
374 )
375 ) {
376 if (
377 this.getPiece(coord.x, coord.y) == V.LANCER &&
378 !this.isImmobilized([coord.x, coord.y])
379 ) {
380 lancerPos.push({x: coord.x, y: coord.y});
381 }
382 coord.x += step[0];
383 coord.y += step[1];
384 }
385 for (let xy of lancerPos) {
386 const dir = V.LANCER_DIRS[this.board[xy.x][xy.y].charAt(1)];
387 if (dir[0] == -step[0] && dir[1] == -step[1]) return true;
388 }
389 }
390 return false;
391 }
392
393 static get VALUES() {
394 return Object.assign(
395 { l: 4.8 }, //Jeff K. estimation (for Eightpieces)
396 ChessRules.VALUES
397 );
398 }
399
400 // For moves notation:
401 static get LANCER_DIRNAMES() {
402 return {
403 'c': "N",
404 'd': "NE",
405 'e': "E",
406 'f': "SE",
407 'g': "S",
408 'h': "SW",
409 'm': "W",
410 'o': "NW"
411 };
412 }
413
414 filterValid(moves) {
415 // At move 1, forbid captures (in case of...):
416 if (this.movesCount >= 2) return moves;
417 return moves.filter(m => m.vanish.length == 1);
418 }
419
420 getNotation(move) {
421 let notation = super.getNotation(move);
422 if (Object.keys(V.LANCER_DIRNAMES).includes(move.vanish[0].p))
423 // Lancer: add direction info
424 notation += "=" + V.LANCER_DIRNAMES[move.appear[0].p];
425 else if (move.appear.length == 2 && move.vanish[1].p != move.appear[1].p)
426 // Same after castle:
427 notation += "+L:" + V.LANCER_DIRNAMES[move.appear[1].p];
428 else if (
429 move.vanish[0].p == V.PAWN &&
430 Object.keys(V.LANCER_DIRNAMES).includes(move.appear[0].p)
431 ) {
432 // Fix promotions in lancer:
433 notation = notation.slice(0, -1) +
434 "L:" + V.LANCER_DIRNAMES[move.appear[0].p];
435 }
436 return notation;
437 }
438
439 };