Fix games rendering
[vchess.git] / client / src / variants / Eightpieces.js
1 import { ArrayFun } from "@/utils/array";
2 import { randInt, shuffle } from "@/utils/alea";
3 import { ChessRules, PiPo, Move } from "@/base_rules";
4
5 export const VariantRules = class EightpiecesRules extends ChessRules {
6 static get JAILER() {
7 return "j";
8 }
9 static get SENTRY() {
10 return "s";
11 }
12 static get LANCER() {
13 return "l";
14 }
15
16 static get PIECES() {
17 return ChessRules.PIECES.concat([V.JAILER, V.SENTRY, V.LANCER]);
18 }
19
20 static get LANCER_DIRS() {
21 return {
22 'c': [-1, 0], //north
23 'd': [-1, 1], //N-E
24 'e': [0, 1], //east
25 'f': [1, 1], //S-E
26 'g': [1, 0], //south
27 'h': [1, -1], //S-W
28 'm': [0, -1], //west
29 'o': [-1, -1] //N-W
30 };
31 }
32
33 getPiece(i, j) {
34 const piece = this.board[i][j].charAt(1);
35 // Special lancer case: 8 possible orientations
36 if (Object.keys(V.LANCER_DIRS).includes(piece)) return V.LANCER;
37 return piece;
38 }
39
40 getPpath(b) {
41 if ([V.JAILER, V.SENTRY].concat(Object.keys(V.LANCER_DIRS)).includes(b[1]))
42 return "Eightpieces/" + b;
43 return b;
44 }
45
46 static ParseFen(fen) {
47 const fenParts = fen.split(" ");
48 return Object.assign(ChessRules.ParseFen(fen), {
49 sentrypush: fenParts[5]
50 });
51 }
52
53 getFen() {
54 return super.getFen() + " " + this.getSentrypushFen();
55 }
56
57 getFenForRepeat() {
58 return super.getFenForRepeat() + "_" + this.getSentrypushFen();
59 }
60
61 getSentrypushFen() {
62 const L = this.sentryPush.length;
63 if (!this.sentryPush[L-1]) return "-";
64 let res = "";
65 this.sentryPush[L-1].forEach(coords =>
66 res += V.CoordsToSquare(coords) + ",");
67 return res.slice(0, -1);
68 }
69
70 setOtherVariables(fen) {
71 super.setOtherVariables(fen);
72 // subTurn == 2 only when a sentry moved, and is about to push something
73 this.subTurn = 1;
74 // Stack pieces' forbidden squares after a sentry move at each turn
75 const parsedFen = V.ParseFen(fen);
76 if (parsedFen.sentrypush == "-") this.sentryPush = [null];
77 else {
78 this.sentryPush = [
79 parsedFen.sentrypush.split(",").map(sq => {
80 return V.SquareToCoords(sq);
81 })
82 ];
83 }
84 }
85
86 canTake([x1,y1], [x2, y2]) {
87 if (this.subTurn == 2)
88 // Sentry push: pieces can capture own color (only)
89 return this.getColor(x1, y1) == this.getColor(x2, y2);
90 return super.canTake([x1,y1], [x2, y2]);
91 }
92
93 static GenRandInitFen(randomness) {
94 // TODO: special conditions for 960
95 return "jsfqkbnr/pppppppp/8/8/8/8/PPPPPPPP/JSDQKBNR w 0 1111 - -";
96 }
97
98 // Scan kings, rooks and jailers
99 scanKingsRooks(fen) {
100 this.INIT_COL_KING = { w: -1, b: -1 };
101 this.INIT_COL_ROOK = { w: -1, b: -1 };
102 this.INIT_COL_JAILER = { w: -1, b: -1 };
103 this.kingPos = { w: [-1, -1], b: [-1, -1] };
104 const fenRows = V.ParseFen(fen).position.split("/");
105 const startRow = { 'w': V.size.x - 1, 'b': 0 };
106 for (let i = 0; i < fenRows.length; i++) {
107 let k = 0;
108 for (let j = 0; j < fenRows[i].length; j++) {
109 switch (fenRows[i].charAt(j)) {
110 case "k":
111 this.kingPos["b"] = [i, k];
112 this.INIT_COL_KING["b"] = k;
113 break;
114 case "K":
115 this.kingPos["w"] = [i, k];
116 this.INIT_COL_KING["w"] = k;
117 break;
118 case "r":
119 if (i == startRow['b'] && this.INIT_COL_ROOK["b"] < 0)
120 this.INIT_COL_ROOK["b"] = k;
121 break;
122 case "R":
123 if (i == startRow['w'] && this.INIT_COL_ROOK["w"] < 0)
124 this.INIT_COL_ROOK["w"] = k;
125 break;
126 case "j":
127 if (i == startRow['b'] && this.INIT_COL_JAILER["b"] < 0)
128 this.INIT_COL_JAILER["b"] = k;
129 break;
130 case "J":
131 if (i == startRow['w'] && this.INIT_COL_JAILER["w"] < 0)
132 this.INIT_COL_JAILER["w"] = k;
133 break;
134 default: {
135 const num = parseInt(fenRows[i].charAt(j));
136 if (!isNaN(num)) k += num - 1;
137 }
138 }
139 k++;
140 }
141 }
142 }
143
144 // Is piece on square (x,y) immobilized?
145 isImmobilized([x, y]) {
146 const color = this.getColor(x, y);
147 const oppCol = V.GetOppCol(color);
148 for (let step of V.steps[V.ROOK]) {
149 const [i, j] = [x + step[0], y + step[1]];
150 if (
151 V.OnBoard(i, j) &&
152 this.board[i][j] != V.EMPTY &&
153 this.getColor(i, j) == oppCol
154 ) {
155 const oppPiece = this.getPiece(i, j);
156 if (oppPiece == V.JAILER) return [i, j];
157 }
158 }
159 return null;
160 }
161
162 getPotentialMovesFrom_aux([x, y]) {
163 switch (this.getPiece(x, y)) {
164 case V.JAILER:
165 return this.getPotentialJailerMoves([x, y]);
166 case V.SENTRY:
167 return this.getPotentialSentryMoves([x, y]);
168 case V.LANCER:
169 return this.getPotentialLancerMoves([x, y]);
170 default:
171 return super.getPotentialMovesFrom([x, y]);
172 }
173 }
174
175 getPotentialMovesFrom([x,y]) {
176 if (this.subTurn == 1) {
177 if (!!this.isImmobilized([x, y])) return [];
178 return this.getPotentialMovesFrom_aux([x, y]);
179 }
180 // subTurn == 2: only the piece pushed by the sentry is allowed to move,
181 // as if the sentry didn't exist
182 if (x != this.sentryPos.x && y != this.sentryPos.y) return [];
183 return this.getPotentialMovesFrom_aux([x,y]);
184 }
185
186 getAllValidMoves() {
187 let moves = super.getAllValidMoves().filter(m => {
188 // Remove jailer captures
189 return m.vanish[0].p != V.JAILER || m.vanish.length == 1;
190 });
191 const L = this.sentryPush.length;
192 if (!!this.sentryPush[L-1] && this.subTurn == 1) {
193 // Delete moves walking back on sentry push path
194 moves = moves.filter(m => {
195 if (
196 m.vanish[0].p != V.PAWN &&
197 this.sentryPush[L-1].some(sq => sq.x == m.end.x && sq.y == m.end.y)
198 ) {
199 return false;
200 }
201 return true;
202 });
203 }
204 return moves;
205 }
206
207 filterValid(moves) {
208 // Disable check tests when subTurn == 2, because the move isn't finished
209 if (this.subTurn == 2) return moves;
210 return super.filterValid(moves);
211 }
212
213 getPotentialLancerMoves([x, y]) {
214 // TODO: add all lancer possible orientations same as pawn promotions,
215 // except if just after a push: allow all movements from init square then
216 return [];
217 }
218
219 getPotentialSentryMoves([x, y]) {
220 // The sentry moves a priori like a bishop:
221 let moves = super.getPotentialBishopMoves([x, y]);
222 // ...but captures are replaced by special move
223 // "appear = [], vanish = init square" to let the pushed piece move then.
224 // TODO
225 }
226
227 getPotentialJailerMoves([x, y]) {
228 // Captures are removed afterward:
229 return super.getPotentialRookMoves([x, y]);
230 }
231
232 getPotentialKingMoves([x, y]) {
233 let moves = super.getPotentialKingMoves([x, y]);
234 // Augment with pass move is the king is immobilized:
235 const jsq = this.isImmobilized([x, y]);
236 if (!!jsq) {
237 moves.push(
238 new Move({
239 appear: [],
240 vanish: [],
241 start: { x: x, y: y },
242 end: { x: jsq[0], y: jsq[1] }
243 })
244 );
245 }
246 return moves;
247 }
248
249 // Adapted: castle with jailer possible
250 getCastleMoves([x, y]) {
251 const c = this.getColor(x, y);
252 const firstRank = (c == "w" ? V.size.x - 1 : 0);
253 if (x != firstRank || y != this.INIT_COL_KING[c])
254 return [];
255
256 const oppCol = V.GetOppCol(c);
257 let moves = [];
258 let i = 0;
259 // King, then rook or jailer:
260 const finalSquares = [
261 [2, 3],
262 [V.size.y - 2, V.size.y - 3]
263 ];
264 castlingCheck: for (
265 let castleSide = 0;
266 castleSide < 2;
267 castleSide++
268 ) {
269 if (!this.castleFlags[c][castleSide]) continue;
270 // Rook (or jailer) and king are on initial position
271
272 const finDist = finalSquares[castleSide][0] - y;
273 let step = finDist / Math.max(1, Math.abs(finDist));
274 i = y;
275 do {
276 if (
277 this.isAttacked([x, i], [oppCol]) ||
278 (this.board[x][i] != V.EMPTY &&
279 (this.getColor(x, i) != c ||
280 ![V.KING, V.ROOK].includes(this.getPiece(x, i))))
281 ) {
282 continue castlingCheck;
283 }
284 i += step;
285 } while (i != finalSquares[castleSide][0]);
286
287 step = castleSide == 0 ? -1 : 1;
288 const rookOrJailerPos =
289 castleSide == 0
290 ? Math.min(this.INIT_COL_ROOK[c], this.INIT_COL_JAILER[c])
291 : Math.max(this.INIT_COL_ROOK[c], this.INIT_COL_JAILER[c]);
292 for (i = y + step; i != rookOrJailerPos; i += step)
293 if (this.board[x][i] != V.EMPTY) continue castlingCheck;
294
295 // Nothing on final squares, except maybe king and castling rook or jailer?
296 for (i = 0; i < 2; i++) {
297 if (
298 this.board[x][finalSquares[castleSide][i]] != V.EMPTY &&
299 this.getPiece(x, finalSquares[castleSide][i]) != V.KING &&
300 finalSquares[castleSide][i] != rookOrJailerPos
301 ) {
302 continue castlingCheck;
303 }
304 }
305
306 // If this code is reached, castle is valid
307 const castlingPiece = this.getPiece(firstRank, rookOrJailerPos);
308 moves.push(
309 new Move({
310 appear: [
311 new PiPo({ x: x, y: finalSquares[castleSide][0], p: V.KING, c: c }),
312 new PiPo({ x: x, y: finalSquares[castleSide][1], p: castlingPiece, c: c })
313 ],
314 vanish: [
315 new PiPo({ x: x, y: y, p: V.KING, c: c }),
316 new PiPo({ x: x, y: rookOrJailerPos, p: castlingPiece, c: c })
317 ],
318 end:
319 Math.abs(y - rookOrJailerPos) <= 2
320 ? { x: x, y: rookOrJailerPos }
321 : { x: x, y: y + 2 * (castleSide == 0 ? -1 : 1) }
322 })
323 );
324 }
325
326 return moves;
327 }
328
329 updateVariables(move) {
330 super.updateVariables(move);
331 if (this.subTurn == 2) {
332 // A piece is pushed:
333 // TODO: push array of squares between start and end of move, included
334 // (except if it's a pawn)
335 this.sentryPush.push([]); //TODO
336 this.subTurn = 1;
337 } else {
338 if (move.appear.length == 0 && move.vanish.length == 1) {
339 // Special sentry move: subTurn <- 2, and then move pushed piece
340 this.subTurn = 2;
341 }
342 // Else: normal move.
343 }
344 }
345
346 play(move) {
347 move.flags = JSON.stringify(this.aggregateFlags());
348 this.epSquares.push(this.getEpSquare(move));
349 V.PlayOnBoard(this.board, move);
350 // TODO: turn changes only if not a sentry push or subTurn == 2
351 //this.turn = V.GetOppCol(this.turn);
352 this.movesCount++;
353 this.updateVariables(move);
354 }
355
356 undo(move) {
357 this.epSquares.pop();
358 this.disaggregateFlags(JSON.parse(move.flags));
359 V.UndoOnBoard(this.board, move);
360 // TODO: here too, take care of turn. If undoing when subTurn == 2,
361 // do not change turn (this shouldn't happen anyway).
362 // ==> normal undo() should be ok.
363 //this.turn = V.GetOppCol(this.turn);
364 this.movesCount--;
365 this.unupdateVariables(move);
366 }
367
368 static get VALUES() {
369 return Object.assign(
370 { l: 4.8, s: 2.8, j: 3.8 }, //Jeff K. estimations
371 ChessRules.VALUES
372 );
373 }
374
375 getNotation(move) {
376 // Special case "king takes jailer" is a pass move
377 if (move.appear.length == 0 && move.vanish.length == 0) return "pass";
378 return super.getNotation(move);
379 }
380 };