Fix Koopa promotions with captures, and Balakhlava: pawns move forward
[vchess.git] / client / src / variants / Teleport.js
1 import { ChessRules, Move, PiPo } from "@/base_rules";
2 import { randInt } from "@/utils/alea";
3
4 export class TeleportRules extends ChessRules {
5 hoverHighlight(x, y) {
6 // Testing move validity results in an infinite update loop.
7 // TODO: find a way to test validity anyway.
8 return (this.subTurn == 2 && this.board[x][y] == V.EMPTY);
9 }
10
11 setOtherVariables(fen) {
12 super.setOtherVariables(fen);
13 this.subTurn = 1;
14 this.firstMove = [];
15 }
16
17 canTake([x1, y1], [x2, y2]) {
18 return this.subTurn == 1;
19 }
20
21 getPPpath(m) {
22 if (
23 m.vanish.length == 2 &&
24 m.appear.length == 1 &&
25 m.vanish[0].c == m.vanish[1].c &&
26 m.appear[0].p == V.KING
27 ) {
28 // Rook teleportation with the king
29 return this.getPpath(m.vanish[1].c + m.vanish[1].p);
30 }
31 return this.getPpath(m.appear[0].c + m.appear[0].p);
32 }
33
34 getPotentialMovesFrom([x, y]) {
35 if (this.subTurn == 1) return super.getPotentialMovesFrom([x, y]);
36 // subTurn == 2: a move is a click, not handled here
37 return [];
38 }
39
40 filterValid(moves) {
41 if (this.subTurn == 2) return super.filterValid(moves);
42 const color = this.turn;
43 return moves.filter(m => {
44 this.play(m);
45 let res = false;
46 if (
47 m.vanish.length == 1 ||
48 m.appear.length == 2 ||
49 m.vanish[0].c != m.vanish[1].c
50 ) {
51 // Standard check:
52 res = !this.underCheck(color);
53 }
54 else {
55 // Self-capture: find landing square not resulting in check
56 outerLoop: for (let i=0; i<8; i++) {
57 for (let j=0; j<8; j++) {
58 if (
59 this.board[i][j] == V.EMPTY &&
60 (
61 m.vanish[1].p != V.PAWN ||
62 i != (color == 'w' ? 0 : 7)
63 )
64 ) {
65 const tMove = new Move({
66 appear: [
67 new PiPo({
68 x: i,
69 y: j,
70 c: color,
71 // The dropped piece nature has no importance:
72 p: V.KNIGHT
73 })
74 ],
75 vanish: [],
76 start: { x: -1, y: -1 }
77 });
78 this.play(tMove);
79 const moveOk = !this.underCheck(color);
80 this.undo(tMove);
81 if (moveOk) {
82 res = true;
83 break outerLoop;
84 }
85 }
86 }
87 }
88 }
89 this.undo(m);
90 return res;
91 });
92 }
93
94 getAllValidMoves() {
95 if (this.subTurn == 1) return super.getAllValidMoves();
96 // Subturn == 2: only teleportations
97 let moves = [];
98 const L = this.firstMove.length;
99 const color = this.turn;
100 for (let i=0; i<8; i++) {
101 for (let j=0; j<8; j++) {
102 if (
103 this.board[i][j] == V.EMPTY &&
104 (
105 this.firstMove[L-1].vanish[1].p != V.PAWN ||
106 i != (color == 'w' ? 0 : 7)
107 )
108 ) {
109 const tMove = new Move({
110 appear: [
111 new PiPo({
112 x: i,
113 y: j,
114 c: color,
115 p: this.firstMove[L-1].vanish[1].p
116 })
117 ],
118 vanish: [],
119 start: { x: -1, y: -1 }
120 });
121 this.play(tMove);
122 const moveOk = !this.underCheck(color);
123 this.undo(tMove);
124 if (moveOk) moves.push(tMove);
125 }
126 }
127 }
128 return moves;
129 }
130
131 underCheck(color) {
132 if (this.kingPos[color][0] < 0)
133 // King is being moved:
134 return false;
135 return super.underCheck(color);
136 }
137
138 doClick(square) {
139 if (isNaN(square[0])) return null;
140 // If subTurn == 2 && square is empty && !underCheck, then teleport
141 if (this.subTurn == 2 && this.board[square[0]][square[1]] == V.EMPTY) {
142 const L = this.firstMove.length;
143 const color = this.turn;
144 if (
145 this.firstMove[L-1].vanish[1].p == V.PAWN &&
146 square[0] == (color == 'w' ? 0 : 7)
147 ) {
148 // Pawns cannot be teleported on last rank
149 return null;
150 }
151 const tMove = new Move({
152 appear: [
153 new PiPo({
154 x: square[0],
155 y: square[1],
156 c: color,
157 p: this.firstMove[L-1].vanish[1].p
158 })
159 ],
160 vanish: [],
161 start: { x: -1, y: -1 }
162 });
163 this.play(tMove);
164 const moveOk = !this.underCheck(color);
165 this.undo(tMove);
166 if (moveOk) return tMove;
167 }
168 return null;
169 }
170
171 play(move) {
172 move.flags = JSON.stringify(this.aggregateFlags());
173 if (move.vanish.length > 0) {
174 this.epSquares.push(this.getEpSquare(move));
175 this.firstMove.push(move);
176 }
177 V.PlayOnBoard(this.board, move);
178 if (
179 this.subTurn == 2 ||
180 move.vanish.length == 1 ||
181 move.appear.length == 2 ||
182 move.vanish[0].c != move.vanish[1].c
183 ) {
184 this.turn = V.GetOppCol(this.turn);
185 this.subTurn = 1;
186 this.movesCount++;
187 }
188 else this.subTurn = 2;
189 this.postPlay(move);
190 }
191
192 postPlay(move) {
193 if (move.vanish.length == 2 && move.vanish[1].p == V.KING)
194 // A king is moved: temporarily off board
195 this.kingPos[move.vanish[1].c] = [-1, -1];
196 else if (move.appear[0].p == V.KING)
197 this.kingPos[move.appear[0].c] = [move.appear[0].x, move.appear[0].y];
198 this.updateCastleFlags(move);
199 }
200
201 // NOTE: no need to update if castleFlags already off
202 updateCastleFlags(move) {
203 if (move.vanish.length == 0) return;
204 const c = move.vanish[0].c;
205 if (
206 move.vanish.length == 2 &&
207 move.appear.length == 1 &&
208 move.vanish[0].c == move.vanish[1].c
209 ) {
210 // Self-capture: of the king or a rook?
211 if (move.vanish[1].p == V.KING)
212 this.castleFlags[c] = [V.size.y, V.size.y];
213 else if (move.vanish[1].p == V.ROOK) {
214 const firstRank = (c == "w" ? V.size.x - 1 : 0);
215 if (
216 move.end.x == firstRank &&
217 this.castleFlags[c].includes(move.end.y)
218 ) {
219 const flagIdx = (move.end.y == this.castleFlags[c][0] ? 0 : 1);
220 this.castleFlags[c][flagIdx] = V.size.y;
221 }
222 }
223 }
224 // Normal check:
225 super.updateCastleFlags(move, move.vanish[0].p, c);
226 }
227
228 undo(move) {
229 this.disaggregateFlags(JSON.parse(move.flags));
230 if (move.vanish.length > 0) {
231 this.epSquares.pop();
232 this.firstMove.pop();
233 }
234 V.UndoOnBoard(this.board, move);
235 if (this.subTurn == 2) this.subTurn = 1;
236 else {
237 this.turn = V.GetOppCol(this.turn);
238 this.movesCount--;
239 this.subTurn = (move.vanish.length > 0 ? 1 : 2);
240 }
241 this.postUndo(move);
242 }
243
244 postUndo(move) {
245 if (move.vanish.length == 0) {
246 if (move.appear[0].p == V.KING)
247 // A king was teleported
248 this.kingPos[move.appear[0].c] = [-1, -1];
249 }
250 else if (move.vanish.length == 2 && move.vanish[1].p == V.KING)
251 // A king was (self-)taken
252 this.kingPos[move.vanish[1].c] = [move.end.x, move.end.y];
253 else super.postUndo(move);
254 }
255
256 getComputerMove() {
257 let moves = this.getAllValidMoves();
258 if (moves.length == 0) return null;
259 // Custom "search" at depth 1 (for now. TODO?)
260 const maxeval = V.INFINITY;
261 const color = this.turn;
262 const initEval = this.evalPosition();
263 moves.forEach(m => {
264 this.play(m);
265 m.eval = (color == "w" ? -1 : 1) * maxeval;
266 if (
267 m.vanish.length == 2 &&
268 m.appear.length == 1 &&
269 m.vanish[0].c == m.vanish[1].c
270 ) {
271 const moves2 = this.getAllValidMoves();
272 m.next = moves2[0];
273 moves2.forEach(m2 => {
274 this.play(m2);
275 const score = this.getCurrentScore();
276 let mvEval = 0;
277 if (["1-0", "0-1"].includes(score))
278 mvEval = (score == "1-0" ? 1 : -1) * maxeval;
279 else if (score == "*")
280 // Add small fluctuations to avoid dropping pieces always on the
281 // first square available.
282 mvEval = initEval + 0.05 - Math.random() / 10;
283 if (
284 (color == 'w' && mvEval > m.eval) ||
285 (color == 'b' && mvEval < m.eval)
286 ) {
287 m.eval = mvEval;
288 m.next = m2;
289 }
290 this.undo(m2);
291 });
292 }
293 else {
294 const score = this.getCurrentScore();
295 if (score != "1/2") {
296 if (score != "*") m.eval = (score == "1-0" ? 1 : -1) * maxeval;
297 else m.eval = this.evalPosition();
298 }
299 }
300 this.undo(m);
301 });
302 moves.sort((a, b) => {
303 return (color == "w" ? 1 : -1) * (b.eval - a.eval);
304 });
305 let candidates = [0];
306 for (let i = 1; i < moves.length && moves[i].eval == moves[0].eval; i++)
307 candidates.push(i);
308 const mIdx = candidates[randInt(candidates.length)];
309 if (!moves[mIdx].next) return moves[mIdx];
310 const move2 = moves[mIdx].next;
311 delete moves[mIdx]["next"];
312 return [moves[mIdx], move2];
313 }
314
315 getNotation(move) {
316 if (move.vanish.length > 0) return super.getNotation(move);
317 // Teleportation:
318 const piece =
319 move.appear[0].p != V.PAWN ? move.appear[0].p.toUpperCase() : "";
320 return piece + "@" + V.CoordsToSquare(move.end);
321 }
322 };