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