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