Almost completed Empire Chess. Next is Synochess (+ rules translations)
[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 {
7e8a7ea1 5
90df90bc 6 hoverHighlight(x, y) {
596e24d0
BA
7 // Testing move validity results in an infinite update loop.
8 // TODO: find a way to test validity anyway.
9 return (this.subTurn == 2 && this.board[x][y] == V.EMPTY);
90df90bc
BA
10 }
11
107dc1bd
BA
12 setOtherVariables(fen) {
13 super.setOtherVariables(fen);
14 this.subTurn = 1;
15 this.firstMove = [];
16 }
17
18 canTake([x1, y1], [x2, y2]) {
19 return this.subTurn == 1;
20 }
21
22 getPPpath(m) {
23 if (
24 m.vanish.length == 2 &&
25 m.appear.length == 1 &&
26 m.vanish[0].c == m.vanish[1].c &&
27 m.appear[0].p == V.KING
28 ) {
29 // Rook teleportation with the king
30 return this.getPpath(m.vanish[1].c + m.vanish[1].p);
31 }
32 return this.getPpath(m.appear[0].c + m.appear[0].p);
33 }
34
35 getPotentialMovesFrom([x, y]) {
36 if (this.subTurn == 1) return super.getPotentialMovesFrom([x, y]);
37 // subTurn == 2: a move is a click, not handled here
38 return [];
39 }
40
41 filterValid(moves) {
42 if (this.subTurn == 2) return super.filterValid(moves);
43 const color = this.turn;
44 return moves.filter(m => {
45 this.play(m);
46 let res = false;
47 if (
48 m.vanish.length == 1 ||
49 m.appear.length == 2 ||
50 m.vanish[0].c != m.vanish[1].c
51 ) {
52 // Standard check:
53 res = !this.underCheck(color);
54 }
55 else {
56 // Self-capture: find landing square not resulting in check
57 outerLoop: for (let i=0; i<8; i++) {
58 for (let j=0; j<8; j++) {
59 if (
60 this.board[i][j] == V.EMPTY &&
61 (
62 m.vanish[1].p != V.PAWN ||
63 i != (color == 'w' ? 0 : 7)
64 )
65 ) {
66 const tMove = new Move({
67 appear: [
68 new PiPo({
69 x: i,
70 y: j,
71 c: color,
72 // The dropped piece nature has no importance:
73 p: V.KNIGHT
74 })
75 ],
76 vanish: [],
77 start: { x: -1, y: -1 }
78 });
79 this.play(tMove);
80 const moveOk = !this.underCheck(color);
81 this.undo(tMove);
82 if (moveOk) {
83 res = true;
84 break outerLoop;
85 }
86 }
87 }
88 }
89 }
90 this.undo(m);
91 return res;
92 });
93 }
94
95 getAllValidMoves() {
96 if (this.subTurn == 1) return super.getAllValidMoves();
97 // Subturn == 2: only teleportations
98 let moves = [];
99 const L = this.firstMove.length;
100 const color = this.turn;
101 for (let i=0; i<8; i++) {
102 for (let j=0; j<8; j++) {
103 if (
104 this.board[i][j] == V.EMPTY &&
105 (
106 this.firstMove[L-1].vanish[1].p != V.PAWN ||
107 i != (color == 'w' ? 0 : 7)
108 )
109 ) {
110 const tMove = new Move({
111 appear: [
112 new PiPo({
113 x: i,
114 y: j,
115 c: color,
116 p: this.firstMove[L-1].vanish[1].p
117 })
118 ],
119 vanish: [],
120 start: { x: -1, y: -1 }
121 });
122 this.play(tMove);
123 const moveOk = !this.underCheck(color);
124 this.undo(tMove);
125 if (moveOk) moves.push(tMove);
126 }
127 }
128 }
129 return moves;
130 }
131
132 underCheck(color) {
133 if (this.kingPos[color][0] < 0)
134 // King is being moved:
135 return false;
136 return super.underCheck(color);
137 }
138
107dc1bd
BA
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 }
a9e1202b
BA
225 // Normal check:
226 super.updateCastleFlags(move, move.vanish[0].p, c);
107dc1bd
BA
227 }
228
229 undo(move) {
230 this.disaggregateFlags(JSON.parse(move.flags));
231 if (move.vanish.length > 0) {
232 this.epSquares.pop();
233 this.firstMove.pop();
234 }
235 V.UndoOnBoard(this.board, move);
236 if (this.subTurn == 2) this.subTurn = 1;
237 else {
238 this.turn = V.GetOppCol(this.turn);
239 this.movesCount--;
240 this.subTurn = (move.vanish.length > 0 ? 1 : 2);
241 }
242 this.postUndo(move);
243 }
244
245 postUndo(move) {
246 if (move.vanish.length == 0) {
247 if (move.appear[0].p == V.KING)
248 // A king was teleported
249 this.kingPos[move.appear[0].c] = [-1, -1];
250 }
251 else if (move.vanish.length == 2 && move.vanish[1].p == V.KING)
252 // A king was (self-)taken
253 this.kingPos[move.vanish[1].c] = [move.end.x, move.end.y];
254 else super.postUndo(move);
255 }
256
257 getComputerMove() {
258 let moves = this.getAllValidMoves();
259 if (moves.length == 0) return null;
260 // Custom "search" at depth 1 (for now. TODO?)
261 const maxeval = V.INFINITY;
262 const color = this.turn;
263 const initEval = this.evalPosition();
264 moves.forEach(m => {
265 this.play(m);
266 m.eval = (color == "w" ? -1 : 1) * maxeval;
267 if (
268 m.vanish.length == 2 &&
269 m.appear.length == 1 &&
270 m.vanish[0].c == m.vanish[1].c
271 ) {
272 const moves2 = this.getAllValidMoves();
273 m.next = moves2[0];
274 moves2.forEach(m2 => {
275 this.play(m2);
276 const score = this.getCurrentScore();
ff0150d1
BA
277 let mvEval = 0;
278 if (["1-0", "0-1"].includes(score))
279 mvEval = (score == "1-0" ? 1 : -1) * maxeval;
280 else if (score == "*")
281 // Add small fluctuations to avoid dropping pieces always on the
282 // first square available.
283 mvEval = initEval + 0.05 - Math.random() / 10;
107dc1bd
BA
284 if (
285 (color == 'w' && mvEval > m.eval) ||
286 (color == 'b' && mvEval < m.eval)
287 ) {
107dc1bd
BA
288 m.eval = mvEval;
289 m.next = m2;
290 }
291 this.undo(m2);
292 });
293 }
294 else {
295 const score = this.getCurrentScore();
296 if (score != "1/2") {
297 if (score != "*") m.eval = (score == "1-0" ? 1 : -1) * maxeval;
298 else m.eval = this.evalPosition();
299 }
300 }
301 this.undo(m);
302 });
303 moves.sort((a, b) => {
304 return (color == "w" ? 1 : -1) * (b.eval - a.eval);
305 });
306 let candidates = [0];
307 for (let i = 1; i < moves.length && moves[i].eval == moves[0].eval; i++)
308 candidates.push(i);
309 const mIdx = candidates[randInt(candidates.length)];
310 if (!moves[mIdx].next) return moves[mIdx];
311 const move2 = moves[mIdx].next;
312 delete moves[mIdx]["next"];
313 return [moves[mIdx], move2];
314 }
315
316 getNotation(move) {
317 if (move.vanish.length > 0) return super.getNotation(move);
318 // Teleportation:
319 const piece =
320 move.appear[0].p != V.PAWN ? move.appear[0].p.toUpperCase() : "";
321 return piece + "@" + V.CoordsToSquare(move.end);
322 }
7e8a7ea1 323
107dc1bd 324};