7704184f1f22099235cbf82782633d70ecb44003
[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 getCurrentScore() {
139 if (this.subTurn == 2)
140 // Move not over
141 return "*";
142 return super.getCurrentScore();
143 }
144
145 doClick(square) {
146 if (isNaN(square[0])) return null;
147 // If subTurn == 2 && square is empty && !underCheck, then teleport
148 if (this.subTurn == 2 && this.board[square[0]][square[1]] == V.EMPTY) {
149 const L = this.firstMove.length;
150 const color = this.turn;
151 if (
152 this.firstMove[L-1].vanish[1].p == V.PAWN &&
153 square[0] == (color == 'w' ? 0 : 7)
154 ) {
155 // Pawns cannot be teleported on last rank
156 return null;
157 }
158 const tMove = new Move({
159 appear: [
160 new PiPo({
161 x: square[0],
162 y: square[1],
163 c: color,
164 p: this.firstMove[L-1].vanish[1].p
165 })
166 ],
167 vanish: [],
168 start: { x: -1, y: -1 }
169 });
170 this.play(tMove);
171 const moveOk = !this.underCheck(color);
172 this.undo(tMove);
173 if (moveOk) return tMove;
174 }
175 return null;
176 }
177
178 play(move) {
179 move.flags = JSON.stringify(this.aggregateFlags());
180 if (move.vanish.length > 0) {
181 this.epSquares.push(this.getEpSquare(move));
182 this.firstMove.push(move);
183 }
184 V.PlayOnBoard(this.board, move);
185 if (
186 this.subTurn == 2 ||
187 move.vanish.length == 1 ||
188 move.appear.length == 2 ||
189 move.vanish[0].c != move.vanish[1].c
190 ) {
191 this.turn = V.GetOppCol(this.turn);
192 this.subTurn = 1;
193 this.movesCount++;
194 }
195 else this.subTurn = 2;
196 this.postPlay(move);
197 }
198
199 postPlay(move) {
200 if (move.vanish.length == 2 && move.vanish[1].p == V.KING)
201 // A king is moved: temporarily off board
202 this.kingPos[move.vanish[1].c] = [-1, -1];
203 else if (move.appear[0].p == V.KING)
204 this.kingPos[move.appear[0].c] = [move.appear[0].x, move.appear[0].y];
205 this.updateCastleFlags(move);
206 }
207
208 // NOTE: no need to update if castleFlags already off
209 updateCastleFlags(move) {
210 if (move.vanish.length == 0) return;
211 const c = move.vanish[0].c;
212 if (
213 move.vanish.length == 2 &&
214 move.appear.length == 1 &&
215 move.vanish[0].c == move.vanish[1].c
216 ) {
217 // Self-capture: of the king or a rook?
218 if (move.vanish[1].p == V.KING)
219 this.castleFlags[c] = [V.size.y, V.size.y];
220 else if (move.vanish[1].p == V.ROOK) {
221 const firstRank = (c == "w" ? V.size.x - 1 : 0);
222 if (
223 move.end.x == firstRank &&
224 this.castleFlags[c].includes(move.end.y)
225 ) {
226 const flagIdx = (move.end.y == this.castleFlags[c][0] ? 0 : 1);
227 this.castleFlags[c][flagIdx] = V.size.y;
228 }
229 }
230 }
231 else {
232 // Normal move
233 const firstRank = (c == "w" ? V.size.x - 1 : 0);
234 const oppCol = V.GetOppCol(c);
235 const oppFirstRank = V.size.x - 1 - firstRank;
236 if (move.vanish[0].p == V.KING && move.appear.length > 0)
237 this.castleFlags[c] = [V.size.y, V.size.y];
238 else if (
239 move.start.x == firstRank &&
240 this.castleFlags[c].includes(move.start.y)
241 ) {
242 const flagIdx = (move.start.y == this.castleFlags[c][0] ? 0 : 1);
243 this.castleFlags[c][flagIdx] = V.size.y;
244 }
245 if (
246 move.end.x == oppFirstRank &&
247 this.castleFlags[oppCol].includes(move.end.y)
248 ) {
249 const flagIdx = (move.end.y == this.castleFlags[oppCol][0] ? 0 : 1);
250 this.castleFlags[oppCol][flagIdx] = V.size.y;
251 }
252 }
253 }
254
255 undo(move) {
256 this.disaggregateFlags(JSON.parse(move.flags));
257 if (move.vanish.length > 0) {
258 this.epSquares.pop();
259 this.firstMove.pop();
260 }
261 V.UndoOnBoard(this.board, move);
262 if (this.subTurn == 2) this.subTurn = 1;
263 else {
264 this.turn = V.GetOppCol(this.turn);
265 this.movesCount--;
266 this.subTurn = (move.vanish.length > 0 ? 1 : 2);
267 }
268 this.postUndo(move);
269 }
270
271 postUndo(move) {
272 if (move.vanish.length == 0) {
273 if (move.appear[0].p == V.KING)
274 // A king was teleported
275 this.kingPos[move.appear[0].c] = [-1, -1];
276 }
277 else if (move.vanish.length == 2 && move.vanish[1].p == V.KING)
278 // A king was (self-)taken
279 this.kingPos[move.vanish[1].c] = [move.end.x, move.end.y];
280 else super.postUndo(move);
281 }
282
283 getComputerMove() {
284 let moves = this.getAllValidMoves();
285 if (moves.length == 0) return null;
286 // Custom "search" at depth 1 (for now. TODO?)
287 const maxeval = V.INFINITY;
288 const color = this.turn;
289 const initEval = this.evalPosition();
290 moves.forEach(m => {
291 this.play(m);
292 m.eval = (color == "w" ? -1 : 1) * maxeval;
293 if (
294 m.vanish.length == 2 &&
295 m.appear.length == 1 &&
296 m.vanish[0].c == m.vanish[1].c
297 ) {
298 const moves2 = this.getAllValidMoves();
299 m.next = moves2[0];
300 moves2.forEach(m2 => {
301 this.play(m2);
302 const score = this.getCurrentScore();
303 let mvEval = 0;
304 if (["1-0", "0-1"].includes(score))
305 mvEval = (score == "1-0" ? 1 : -1) * maxeval;
306 else if (score == "*")
307 // Add small fluctuations to avoid dropping pieces always on the
308 // first square available.
309 mvEval = initEval + 0.05 - Math.random() / 10;
310 if (
311 (color == 'w' && mvEval > m.eval) ||
312 (color == 'b' && mvEval < m.eval)
313 ) {
314 // TODO: if many second moves have the same eval, only the
315 // first is kept. Could be randomized.
316 m.eval = mvEval;
317 m.next = m2;
318 }
319 this.undo(m2);
320 });
321 }
322 else {
323 const score = this.getCurrentScore();
324 if (score != "1/2") {
325 if (score != "*") m.eval = (score == "1-0" ? 1 : -1) * maxeval;
326 else m.eval = this.evalPosition();
327 }
328 }
329 this.undo(m);
330 });
331 moves.sort((a, b) => {
332 return (color == "w" ? 1 : -1) * (b.eval - a.eval);
333 });
334 let candidates = [0];
335 for (let i = 1; i < moves.length && moves[i].eval == moves[0].eval; i++)
336 candidates.push(i);
337 const mIdx = candidates[randInt(candidates.length)];
338 if (!moves[mIdx].next) return moves[mIdx];
339 const move2 = moves[mIdx].next;
340 delete moves[mIdx]["next"];
341 return [moves[mIdx], move2];
342 }
343
344 getNotation(move) {
345 if (move.vanish.length > 0) return super.getNotation(move);
346 // Teleportation:
347 const piece =
348 move.appear[0].p != V.PAWN ? move.appear[0].p.toUpperCase() : "";
349 return piece + "@" + V.CoordsToSquare(move.end);
350 }
351 };