A few bug fixes
[vchess.git] / client / src / variants / Swap.js
1 import { ChessRules, PiPo } from "@/base_rules";
2 import { randInt } from "@/utils/alea";
3
4 export class SwapRules extends ChessRules {
5
6 setOtherVariables(fen) {
7 super.setOtherVariables(fen);
8 // Local stack of swaps
9 this.swaps = [];
10 const smove = V.ParseFen(fen).smove;
11 if (smove == "-") this.swaps.push(null);
12 else {
13 this.swaps.push({
14 start: ChessRules.SquareToCoords(smove.substr(0, 2)),
15 end: ChessRules.SquareToCoords(smove.substr(2))
16 });
17 }
18 this.subTurn = 1;
19 }
20
21 static ParseFen(fen) {
22 return Object.assign(
23 ChessRules.ParseFen(fen),
24 { smove: fen.split(" ")[5] }
25 );
26 }
27
28 static IsGoodFen(fen) {
29 if (!ChessRules.IsGoodFen(fen)) return false;
30 const fenParts = fen.split(" ");
31 if (fenParts.length != 6) return false;
32 if (fenParts[5] != "-" && !fenParts[5].match(/^([a-h][1-8]){2}$/))
33 return false;
34 return true;
35 }
36
37 getPPpath(m) {
38 if (m.appear.length == 1) return super.getPPpath(m);
39 // Swap promotion:
40 return m.appear[1].c + m.appear[1].p;
41 }
42
43 getSwapMoves([x1, y1], [x2, y2]) {
44 let move = super.getBasicMove([x1, y1], [x2, y2]);
45 move.appear.push(
46 new PiPo({
47 x: x1,
48 y: y1,
49 c: this.getColor(x2, y2),
50 p: this.getPiece(x2, y2)
51 })
52 );
53 const lastRank = (move.appear[1].c == 'w' ? 0 : 7);
54 if (move.appear[1].p == V.PAWN && move.appear[1].x == lastRank) {
55 // Promotion:
56 move.appear[1].p = V.ROOK;
57 let moves = [move];
58 [V.KNIGHT, V.BISHOP, V.QUEEN].forEach(p => {
59 let m = JSON.parse(JSON.stringify(move));
60 m.appear[1].p = p;
61 moves.push(m);
62 });
63 return moves;
64 }
65 return [move];
66 }
67
68 getPotentialMovesFrom([x, y]) {
69 if (this.subTurn == 1) return super.getPotentialMovesFrom([x, y]);
70 // SubTurn == 2: only swaps
71 let moves = [];
72 const color = this.turn;
73 const piece = this.getPiece(x, y);
74 const addSmoves = (i, j) => {
75 if (this.getPiece(i, j) != piece || this.getColor(i, j) != color)
76 Array.prototype.push.apply(moves, this.getSwapMoves([x, y], [i, j]));
77 };
78 switch (piece) {
79 case V.PAWN: {
80 const forward = (color == 'w' ? -1 : 1);
81 const startRank = (color == 'w' ? [6, 7] : [0, 1]);
82 if (
83 x == startRank &&
84 this.board[x + forward][y] == V.EMPTY &&
85 this.board[x + 2 * forward][y] != V.EMPTY
86 ) {
87 // Swap using 2 squares move
88 addSmoves(x + 2 * forward, y);
89 }
90 for (let shift of [-1, 0, 1]) {
91 const [i, j] = [x + forward, y + shift];
92 if (V.OnBoard(i, j) && this.board[i][j] != V.EMPTY) addSmoves(i, j);
93 }
94 break;
95 }
96 case V.KNIGHT:
97 V.steps[V.KNIGHT].forEach(s => {
98 const [i, j] = [x + s[0], y + s[1]];
99 if (V.OnBoard(i, j) && this.board[i][j] != V.EMPTY) addSmoves(i, j);
100 });
101 break;
102 case V.KING:
103 V.steps[V.ROOK].concat(V.steps[V.BISHOP]).forEach(s => {
104 const [i, j] = [x + s[0], y + s[1]];
105 if (V.OnBoard(i, j) && this.board[i][j] != V.EMPTY) addSmoves(i, j);
106 });
107 break;
108 case V.ROOK:
109 case V.BISHOP:
110 case V.QUEEN: {
111 const steps =
112 piece != V.QUEEN
113 ? V.steps[piece]
114 : V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
115 steps.forEach(s => {
116 let [i, j] = [x + s[0], y + s[1]];
117 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
118 i += s[0];
119 j += s[1];
120 }
121 if (V.OnBoard(i, j) && this.board[i][j] != V.EMPTY) addSmoves(i, j);
122 });
123 break;
124 }
125 }
126 return moves;
127 }
128
129 // Does m2 un-do m1 ? (to disallow undoing swaps)
130 oppositeSwaps(m1, m2) {
131 return (
132 !!m1 &&
133 m1.start.x == m2.start.x &&
134 m1.end.x == m2.end.x &&
135 m1.start.y == m2.start.y &&
136 m1.end.y == m2.end.y
137 );
138 }
139
140 filterValid(moves) {
141 const fmoves = super.filterValid(moves);
142 if (this.subTurn == 1) return fmoves;
143 return fmoves.filter(m => {
144 const L = this.swaps.length; //at least 1: init from FEN
145 return !this.oppositeSwaps(this.swaps[L - 1], m);
146 });
147 }
148
149 static GenRandInitFen(options) {
150 // Add empty smove:
151 return ChessRules.GenRandInitFen(options) + " -";
152 }
153
154 getSmoveFen() {
155 const L = this.swaps.length;
156 return (
157 !this.swaps[L - 1]
158 ? "-"
159 : ChessRules.CoordsToSquare(this.swaps[L - 1].start) +
160 ChessRules.CoordsToSquare(this.swaps[L - 1].end)
161 );
162 }
163
164 getFen() {
165 return super.getFen() + " " + this.getSmoveFen();
166 }
167
168 getFenForRepeat() {
169 return super.getFenForRepeat() + "_" + this.getSmoveFen();
170 }
171
172 getCurrentScore() {
173 const L = this.swaps.length;
174 if (this.movesCount >= 2 && !this.swaps[L-1])
175 // Opponent had no swap moves: I win
176 return (this.turn == "w" ? "1-0" : "0-1");
177 if (this.atLeastOneMove()) return "*";
178 // No valid move: I lose
179 return (this.turn == "w" ? "0-1" : "1-0");
180 }
181
182 noSwapAfter(move) {
183 this.subTurn = 2;
184 const res = !this.atLeastOneMove();
185 this.subTurn = 1;
186 return res;
187 }
188
189 play(move) {
190 move.flags = JSON.stringify(this.aggregateFlags());
191 move.turn = [this.turn, this.subTurn];
192 V.PlayOnBoard(this.board, move);
193 let epSq = undefined;
194 if (this.subTurn == 1) epSq = this.getEpSquare(move);
195 if (this.movesCount == 0) {
196 // First move in game
197 this.turn = "b";
198 this.epSquares.push(epSq);
199 this.movesCount = 1;
200 }
201 // Any swap available after move? If no, skip subturn 2
202 else if (this.subTurn == 1 && this.noSwapAfter(move)) {
203 this.turn = V.GetOppCol(this.turn);
204 this.epSquares.push(epSq);
205 move.noSwap = true;
206 this.movesCount++;
207 }
208 else {
209 if (this.subTurn == 2) {
210 this.turn = V.GetOppCol(this.turn);
211 this.swaps.push({ start: move.start, end: move.end });
212 }
213 else {
214 this.epSquares.push(epSq);
215 this.movesCount++;
216 }
217 this.subTurn = 3 - this.subTurn;
218 }
219 this.postPlay(move);
220 }
221
222 postPlay(move) {
223 const firstRank = { 7: 'w', 0: 'b' };
224 // Did some king move?
225 move.appear.forEach(a => {
226 if (a.p == V.KING) {
227 this.kingPos[a.c] = [a.x, a.y];
228 this.castleFlags[a.c] = [V.size.y, V.size.y];
229 }
230 });
231 for (let coords of [move.start, move.end]) {
232 if (
233 Object.keys(firstRank).includes(coords.x) &&
234 this.castleFlags[firstRank[coords.x]].includes(coords.y)
235 ) {
236 const c = firstRank[coords.x];
237 const flagIdx = (coords.y == this.castleFlags[c][0] ? 0 : 1);
238 this.castleFlags[c][flagIdx] = V.size.y;
239 }
240 }
241 }
242
243 undo(move) {
244 this.disaggregateFlags(JSON.parse(move.flags));
245 V.UndoOnBoard(this.board, move);
246 if (move.turn[1] == 1) {
247 // The move may not be full, but is fully undone:
248 this.epSquares.pop();
249 // Moves counter was just incremented:
250 this.movesCount--;
251 }
252 else
253 // Undo the second half of a move
254 this.swaps.pop();
255 this.turn = move.turn[0];
256 this.subTurn = move.turn[1];
257 this.postUndo(move);
258 }
259
260 postUndo(move) {
261 // Did some king move?
262 move.vanish.forEach(v => {
263 if (v.p == V.KING) this.kingPos[v.c] = [v.x, v.y];
264 });
265 }
266
267 // No alpha-beta here, just adapted min-max at depth 2(+1)
268 getComputerMove() {
269 const maxeval = V.INFINITY;
270 const color = this.turn;
271 const oppCol = V.GetOppCol(this.turn);
272
273 // NOTE: searching best (half) move for opponent turn is a bit too slow.
274 // => Only 2 half moves depth here.
275
276 const moves11 = this.getAllValidMoves();
277 if (this.movesCount == 0)
278 // Just play first move at random:
279 return moves11[randInt(moves11.length)];
280 let bestMove = undefined;
281 // Rank moves using a min-max at depth 2
282 for (let i = 0; i < moves11.length; i++) {
283 this.play(moves11[i]);
284 if (!!moves11[i].noSwap) {
285 // I lose
286 if (!bestMove) bestMove = {
287 moves: moves11[i],
288 eval: (color == 'w' ? -maxeval : maxeval)
289 };
290 }
291 else {
292 let moves12 = this.getAllValidMoves();
293 for (let j = 0; j < moves12.length; j++) {
294 this.play(moves12[j]);
295 const evalMove = this.evalPosition() + 0.05 - Math.random() / 10;
296 if (
297 !bestMove ||
298 (color == 'w' && evalMove > bestMove.eval) ||
299 (color == 'b' && evalMove < bestMove.eval)
300 ) {
301 bestMove = {
302 moves: [moves11[i], moves12[j]],
303 eval: evalMove
304 };
305 }
306 this.undo(moves12[j]);
307 }
308 }
309 this.undo(moves11[i]);
310 }
311 return bestMove.moves;
312 }
313
314 getNotation(move) {
315 if (move.appear.length == 1)
316 // Normal move
317 return super.getNotation(move);
318 if (move.appear[0].p == V.KING && move.appear[1].p == V.ROOK)
319 // Castle
320 return (move.end.y < move.start.y ? "0-0-0" : "0-0");
321 // Swap
322 return "S" + V.CoordsToSquare(move.start) + V.CoordsToSquare(move.end);
323 }
324
325 };