Several small improvements + integrate options + first working draft of Cwda
[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(options) {
92 if (options.randomness == 0)
93 // Deterministic:
94 return "enbqkbnm/pppppppp/8/8/8/8/PPPPPPPP/ENBQKBNM w 0 ahah -";
95
96 const baseFen = ChessRules.GenRandInitFen(options);
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 getPotentialMovesFrom([x, y]) {
118 if (this.getPiece(x, y) == V.LANCER)
119 return this.getPotentialLancerMoves([x, y]);
120 return super.getPotentialMovesFrom([x, y]);
121 }
122
123 getPotentialPawnMoves([x, y]) {
124 const color = this.getColor(x, y);
125 let shiftX = (color == "w" ? -1 : 1);
126 const lastRank = (color == "w" ? 0 : 7);
127 let finalPieces = [V.PAWN];
128 if (x + shiftX == lastRank) {
129 // Only allow direction facing inside board:
130 const allowedLancerDirs =
131 lastRank == 0
132 ? ['e', 'f', 'g', 'h', 'm']
133 : ['c', 'd', 'e', 'm', 'o'];
134 finalPieces = allowedLancerDirs.concat([V.KNIGHT, V.BISHOP, V.QUEEN]);
135 }
136 return super.getPotentialPawnMoves([x, y], finalPieces);
137 }
138
139 // Obtain all lancer moves in "step" direction
140 getPotentialLancerMoves_aux([x, y], step, tr) {
141 let moves = [];
142 // Add all moves to vacant squares until opponent is met:
143 const color = this.getColor(x, y);
144 const oppCol = V.GetOppCol(color)
145 let sq = [x + step[0], y + step[1]];
146 while (V.OnBoard(sq[0], sq[1]) && this.getColor(sq[0], sq[1]) != oppCol) {
147 if (this.board[sq[0]][sq[1]] == V.EMPTY)
148 moves.push(this.getBasicMove([x, y], sq, tr));
149 sq[0] += step[0];
150 sq[1] += step[1];
151 }
152 if (V.OnBoard(sq[0], sq[1]))
153 // Add capturing move
154 moves.push(this.getBasicMove([x, y], sq, tr));
155 return moves;
156 }
157
158 getPotentialLancerMoves([x, y]) {
159 let moves = [];
160 // Add all lancer possible orientations, similar to pawn promotions.
161 const color = this.getColor(x, y);
162 const dirCode = this.board[x][y][1];
163 const curDir = V.LANCER_DIRS[dirCode];
164 const monodirMoves =
165 this.getPotentialLancerMoves_aux([x, y], V.LANCER_DIRS[dirCode]);
166 monodirMoves.forEach(m => {
167 Object.keys(V.LANCER_DIRS).forEach(k => {
168 const newDir = V.LANCER_DIRS[k];
169 // Prevent orientations toward outer board:
170 if (V.OnBoard(m.end.x + newDir[0], m.end.y + newDir[1])) {
171 let mk = JSON.parse(JSON.stringify(m));
172 mk.appear[0].p = k;
173 moves.push(mk);
174 }
175 });
176 });
177 return moves;
178 }
179
180 getCastleMoves([x, y]) {
181 const c = this.getColor(x, y);
182
183 // Castling ?
184 const oppCol = V.GetOppCol(c);
185 let moves = [];
186 let i = 0;
187 // King, then lancer:
188 const finalSquares = [ [2, 3], [V.size.y - 2, V.size.y - 3] ];
189 castlingCheck: for (
190 let castleSide = 0;
191 castleSide < 2;
192 castleSide++ //large, then small
193 ) {
194 if (this.castleFlags[c][castleSide] >= V.size.y) continue;
195 // If this code is reached, lancer and king are on initial position
196
197 const lancerPos = this.castleFlags[c][castleSide];
198 const castlingPiece = this.board[x][lancerPos].charAt(1);
199
200 // Nothing on the path of the king ? (and no checks)
201 const finDist = finalSquares[castleSide][0] - y;
202 let step = finDist / Math.max(1, Math.abs(finDist));
203 i = y;
204 do {
205 if (
206 (this.isAttacked([x, i], oppCol)) ||
207 (
208 this.board[x][i] != V.EMPTY &&
209 // NOTE: next check is enough, because of chessboard constraints
210 (this.getColor(x, i) != c || ![y, lancerPos].includes(i))
211 )
212 ) {
213 continue castlingCheck;
214 }
215 i += step;
216 } while (i != finalSquares[castleSide][0]);
217
218 // Nothing on final squares, except maybe king and castling lancer?
219 for (i = 0; i < 2; i++) {
220 if (
221 finalSquares[castleSide][i] != lancerPos &&
222 this.board[x][finalSquares[castleSide][i]] != V.EMPTY &&
223 (
224 finalSquares[castleSide][i] != y ||
225 this.getColor(x, finalSquares[castleSide][i]) != c
226 )
227 ) {
228 continue castlingCheck;
229 }
230 }
231
232 // If this code is reached, castle is valid
233 let allowedLancerDirs = [castlingPiece];
234 if (finalSquares[castleSide][1] != lancerPos) {
235 // It moved: allow reorientation
236 allowedLancerDirs =
237 x == 0
238 ? ['e', 'f', 'g', 'h', 'm']
239 : ['c', 'd', 'e', 'm', 'o'];
240 }
241 allowedLancerDirs.forEach(dir => {
242 moves.push(
243 new Move({
244 appear: [
245 new PiPo({
246 x: x,
247 y: finalSquares[castleSide][0],
248 p: V.KING,
249 c: c
250 }),
251 new PiPo({
252 x: x,
253 y: finalSquares[castleSide][1],
254 p: dir,
255 c: c
256 })
257 ],
258 vanish: [
259 new PiPo({ x: x, y: y, p: V.KING, c: c }),
260 new PiPo({ x: x, y: lancerPos, p: castlingPiece, c: c })
261 ],
262 end:
263 Math.abs(y - lancerPos) <= 2
264 ? { x: x, y: lancerPos }
265 : { x: x, y: y + 2 * (castleSide == 0 ? -1 : 1) }
266 })
267 );
268 });
269 }
270
271 return moves;
272 }
273
274 isAttacked(sq, color) {
275 return (
276 super.isAttacked(sq, color) ||
277 this.isAttackedByLancer(sq, color)
278 );
279 }
280
281 isAttackedByLancer([x, y], color) {
282 for (let step of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) {
283 // If in this direction there are only enemy pieces and empty squares,
284 // and we meet a lancer: can he reach us?
285 // NOTE: do not stop at first lancer, there might be several!
286 let coord = { x: x + step[0], y: y + step[1] };
287 let lancerPos = [];
288 while (
289 V.OnBoard(coord.x, coord.y) &&
290 (
291 this.board[coord.x][coord.y] == V.EMPTY ||
292 this.getColor(coord.x, coord.y) == color
293 )
294 ) {
295 if (this.getPiece(coord.x, coord.y) == V.LANCER)
296 lancerPos.push({x: coord.x, y: coord.y});
297 coord.x += step[0];
298 coord.y += step[1];
299 }
300 for (let xy of lancerPos) {
301 const dir = V.LANCER_DIRS[this.board[xy.x][xy.y].charAt(1)];
302 if (dir[0] == -step[0] && dir[1] == -step[1]) return true;
303 }
304 }
305 return false;
306 }
307
308 static get VALUES() {
309 return Object.assign(
310 { l: 4.8 }, //Jeff K. estimation (for Eightpieces)
311 ChessRules.VALUES
312 );
313 }
314
315 // For moves notation:
316 static get LANCER_DIRNAMES() {
317 return {
318 'c': "N",
319 'd': "NE",
320 'e': "E",
321 'f': "SE",
322 'g': "S",
323 'h': "SW",
324 'm': "W",
325 'o': "NW"
326 };
327 }
328
329 filterValid(moves) {
330 // At move 1, forbid captures (in case of...):
331 if (this.movesCount >= 2) return super.filterValid(moves);
332 return moves.filter(m => m.vanish.length == 1);
333 }
334
335 static get SEARCH_DEPTH() {
336 return 2;
337 }
338
339 getNotation(move) {
340 let notation = super.getNotation(move);
341 if (Object.keys(V.LANCER_DIRNAMES).includes(move.vanish[0].p))
342 // Lancer: add direction info
343 notation += "=" + V.LANCER_DIRNAMES[move.appear[0].p];
344 else if (move.appear.length == 2 && move.vanish[1].p != move.appear[1].p)
345 // Same after castle:
346 notation += "+L:" + V.LANCER_DIRNAMES[move.appear[1].p];
347 else if (
348 move.vanish[0].p == V.PAWN &&
349 Object.keys(V.LANCER_DIRNAMES).includes(move.appear[0].p)
350 ) {
351 // Fix promotions in lancer:
352 notation = notation.slice(0, -1) +
353 "L:" + V.LANCER_DIRNAMES[move.appear[0].p];
354 }
355 return notation;
356 }
357
358 };