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