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