Add Convert variant
[vchess.git] / client / src / variants / Convert.js
1 import { ChessRules, PiPo, Move } from "@/base_rules";
2 import { randInt } from "@/utils/alea";
3
4 export class ConvertRules extends ChessRules {
5
6 setOtherVariables(fen) {
7 super.setOtherVariables(fen);
8 // Stack of "last move" only for intermediate chaining
9 this.lastMoveEnd = [null];
10 }
11
12 getBasicMove([sx, sy], [ex, ey], tr) {
13 const L = this.lastMoveEnd.length;
14 const lm = this.lastMoveEnd[L-1];
15 const piece = (!!lm ? lm.p : null);
16 const c = this.turn;
17 if (this.board[ex][ey] == V.EMPTY) {
18 if (!!piece && !tr) tr = { c: c, p: piece }
19 let mv = super.getBasicMove([sx, sy], [ex, ey], tr);
20 if (!!piece) mv.vanish.pop();
21 return mv;
22 }
23 // Capture: initial, or inside a chain
24 const initPiece = (piece || this.getPiece(sx, sy));
25 const oppCol = V.GetOppCol(c);
26 const oppPiece = this.getPiece(ex, ey);
27 let mv = new Move({
28 start: { x: sx, y: sy },
29 end: { x: ex, y: ey },
30 appear: [
31 new PiPo({
32 x: ex,
33 y: ey,
34 c: c,
35 p: (!!tr ? tr.p : initPiece)
36 })
37 ],
38 vanish: [
39 new PiPo({
40 x: ex,
41 y: ey,
42 c: oppCol,
43 p: oppPiece
44 })
45 ]
46 });
47 if (!piece) {
48 // Initial capture
49 mv.vanish.unshift(
50 new PiPo({
51 x: sx,
52 y: sy,
53 c: c,
54 p: initPiece
55 })
56 );
57 }
58 // TODO: This "converted" indication isn't needed in fact,
59 // because it can be deduced from the move itself.
60 mv.end.converted = oppPiece;
61 return mv;
62 }
63
64 getPotentialMovesFrom([x, y], asA) {
65 const L = this.lastMoveEnd.length;
66 if (!!this.lastMoveEnd[L-1]) {
67 if (x != this.lastMoveEnd[L-1].x || y != this.lastMoveEnd[L-1].y)
68 // A capture was played: wrong square
69 return [];
70 asA = this.lastMoveEnd[L-1].p;
71 }
72 switch (asA || this.getPiece(x, y)) {
73 case V.PAWN: return super.getPotentialPawnMoves([x, y]);
74 case V.ROOK: return super.getPotentialRookMoves([x, y]);
75 case V.KNIGHT: return super.getPotentialKnightMoves([x, y]);
76 case V.BISHOP: return super.getPotentialBishopMoves([x, y]);
77 case V.QUEEN: return super.getPotentialQueenMoves([x, y]);
78 case V.KING: return super.getPotentialKingMoves([x, y]);
79 }
80 return [];
81 }
82
83 getPossibleMovesFrom(sq) {
84 const L = this.lastMoveEnd.length;
85 let asA = undefined;
86 if (!!this.lastMoveEnd[L-1]) {
87 if (
88 sq[0] != this.lastMoveEnd[L-1].x ||
89 sq[1] != this.lastMoveEnd[L-1].y
90 ) {
91 return [];
92 }
93 asA = this.lastMoveEnd[L-1].p;
94 }
95 return this.filterValid(this.getPotentialMovesFrom(sq, asA));
96 }
97
98 isAttacked_aux([x, y], color, explored) {
99 if (explored.some(sq => sq[0] == x && sq[1] == y))
100 // Start of an infinite loop: exit
101 return false;
102 explored.push([x, y]);
103 if (super.isAttacked([x, y], color)) return true;
104 // Maybe indirect "chaining" attack:
105 const myColor = this.turn
106 let res = false;
107 let toCheck = []; //check all but king (no need)
108 // Pawns:
109 const shiftToPawn = (myColor == 'w' ? -1 : 1);
110 for (let yShift of [-1, 1]) {
111 const [i, j] = [x + shiftToPawn, y + yShift];
112 if (
113 V.OnBoard(i, j) &&
114 this.board[i][j] != V.EMPTY &&
115 // NOTE: no need to check color (no enemy pawn can take directly)
116 this.getPiece(i, j) == V.PAWN
117 ) {
118 toCheck.push([i, j]);
119 }
120 }
121 // Knights:
122 V.steps[V.KNIGHT].forEach(s => {
123 const [i, j] = [x + s[0], y + s[1]];
124 if (
125 V.OnBoard(i, j) &&
126 this.board[i][j] != V.EMPTY &&
127 this.getPiece(i, j) == V.KNIGHT
128 ) {
129 toCheck.push([i, j]);
130 }
131 });
132 // Sliders:
133 V.steps[V.ROOK].concat(V.steps[V.BISHOP]).forEach(s => {
134 let [i, j] = [x + s[0], y + s[1]];
135 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
136 i += s[0];
137 j += s[1];
138 }
139 if (!V.OnBoard(i, j)) return;
140 const piece = this.getPiece(i, j);
141 if (
142 piece == V.QUEEN ||
143 (piece == V.ROOK && (s[0] == 0 || s[1] == 0)) ||
144 (piece == V.BISHOP && (s[0] != 0 && s[1] != 0))
145 ) {
146 toCheck.push([i, j]);
147 }
148 });
149 for (let ij of toCheck) {
150 if (this.isAttacked_aux(ij, color, explored)) return true;
151 }
152 return false;
153 }
154
155 isAttacked([x, y], color) {
156 let explored = [];
157 return this.isAttacked_aux([x, y], color, explored);
158 }
159
160 filterValid(moves) {
161 // No "checks" (except to forbid castle)
162 return moves;
163 }
164
165 getCheckSquares() {
166 return [];
167 }
168
169 play(move) {
170 move.flags = JSON.stringify(this.aggregateFlags());
171 this.epSquares.push(this.getEpSquare(move));
172 V.PlayOnBoard(this.board, move);
173 if (!move.end.converted) {
174 // Not a capture: change turn
175 this.turn = V.GetOppCol(this.turn);
176 this.movesCount++;
177 this.lastMoveEnd.push(null);
178 }
179 else {
180 this.lastMoveEnd.push(
181 Object.assign({}, move.end, { p: move.end.converted })
182 );
183 }
184 this.postPlay(move);
185 }
186
187 postPlay(move) {
188 const c = (!move.end.converted ? V.GetOppCol(this.turn) : this.turn);
189 const piece = move.appear[0].p;
190 if (piece == V.KING) {
191 this.kingPos[c][0] = move.appear[0].x;
192 this.kingPos[c][1] = move.appear[0].y;
193 }
194 super.updateCastleFlags(move, piece, c);
195 }
196
197 undo(move) {
198 this.disaggregateFlags(JSON.parse(move.flags));
199 this.epSquares.pop();
200 this.lastMoveEnd.pop();
201 V.UndoOnBoard(this.board, move);
202 if (!move.end.converted) {
203 this.turn = V.GetOppCol(this.turn);
204 this.movesCount--;
205 }
206 super.postUndo(move);
207 }
208
209 getCurrentScore() {
210 const color = this.turn;
211 const kp = this.kingPos[color];
212 if (this.getColor(kp[0], kp[1]) != color)
213 return (color == "w" ? "0-1" : "1-0");
214 if (!super.atLeastOneMove()) return "1/2";
215 return "*";
216 }
217
218 getComputerMove() {
219 let initMoves = this.getAllValidMoves();
220 if (initMoves.length == 0) return null;
221 // Loop until valid move is found (no blocked pawn conversion...)
222 while (true) {
223 let moves = JSON.parse(JSON.stringify(initMoves));
224 let mvArray = [];
225 let mv = null;
226 // Just play random moves (for now at least. TODO?)
227 while (moves.length > 0) {
228 mv = moves[randInt(moves.length)];
229 mvArray.push(mv);
230 this.play(mv);
231 if (!!mv.end.converted)
232 // A piece was just converted
233 moves = this.getPotentialMovesFrom([mv.end.x, mv.end.y]);
234 else break;
235 }
236 for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]);
237 if (!mv.end.released) return (mvArray.length > 1 ? mvArray : mvArray[0]);
238 }
239 return null; //never reached
240 }
241
242 getNotation(move) {
243 if (move.appear.length == 2 && move.appear[0].p == V.KING)
244 return (move.end.y < move.start.y ? "0-0-0" : "0-0");
245 const c = this.turn;
246 const L = this.lastMoveEnd.length;
247 const lm = this.lastMoveEnd[L-1];
248 const piece = (!lm ? move.appear[0].p : lm.p);
249 // Basic move notation:
250 let notation = piece.toUpperCase();
251 if (
252 this.board[move.end.x][move.end.y] != V.EMPTY ||
253 (piece == V.PAWN && move.start.y != move.end.y)
254 ) {
255 notation += "x";
256 }
257 const finalSquare = V.CoordsToSquare(move.end);
258 notation += finalSquare;
259
260 // Add potential promotion indications:
261 const firstLastRank = (c == 'w' ? [7, 0] : [0, 7]);
262 if (move.end.x == firstLastRank[1] && piece == V.PAWN)
263 notation += "=" + move.appear[0].p.toUpperCase();
264 return notation;
265 }
266
267 };