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