566feb5ed1235d0231b0688141f1a077f9da2773
[xogo.git] / variants / Convert / class.js
1 import ChessRules from "/base_rules.js";
2 import PiPo from "/utils/PiPo.js";
3 import Move from "/utils/Move.js";
4
5 export default class ConvertRules extends ChessRules {
6
7 // TODO: options ? (balance progressive ok it seems?)
8 static get Options() {
9 return {
10 select: C.Options.select,
11 input: C.Options.input,
12 styles: [
13 "atomic", "cannibal", "capture", "cylinder",
14 "dark", "madrasi", "rifle", "teleport"
15 ]
16 };
17 }
18
19 get hasEnpassant() {
20 return false;
21 }
22
23 setOtherVariables(fenParsed, pieceArray) {
24 super.setOtherVariables(fenParsed, pieceArray);
25 // Stack of "last move" only for intermediate chaining
26 this.lastMoveEnd = [];
27 }
28
29 genRandInitBaseFen() {
30 const baseFen = super.genRandInitBaseFen();
31 return {
32 fen: baseFen.fen.replace("pppppppp/8", "8/pppppppp")
33 .replace("8/PPPPPPPP", "PPPPPPPP/8"),
34 o: baseFen.o
35 };
36 }
37
38 getBasicMove([sx, sy], [ex, ey], tr) {
39 const L = this.lastMoveEnd.length;
40 const lm = this.lastMoveEnd[L-1];
41 const piece = (!!lm ? lm.p : null);
42 const c = this.turn;
43 if (this.board[ex][ey] == "") {
44 if (piece && !tr)
45 tr = {c: c, p: piece};
46 let mv = super.getBasicMove([sx, sy], [ex, ey], tr);
47 if (piece)
48 mv.vanish.pop();
49 return mv;
50 }
51 // Capture: initial, or inside a chain
52 const initPiece = (piece || this.getPiece(sx, sy));
53 const oppCol = C.GetOppTurn(c);
54 const oppPiece = this.getPiece(ex, ey);
55 let mv = new Move({
56 start: {x: sx, y: sy},
57 end: {x: ex, y: ey},
58 appear: [
59 new PiPo({
60 x: ex,
61 y: ey,
62 c: c,
63 p: (!!tr ? tr.p : initPiece)
64 })
65 ],
66 vanish: [
67 new PiPo({
68 x: ex,
69 y: ey,
70 c: oppCol,
71 p: oppPiece
72 })
73 ]
74 });
75 if (!piece) {
76 // Initial capture
77 mv.vanish.unshift(
78 new PiPo({
79 x: sx,
80 y: sy,
81 c: c,
82 p: initPiece
83 })
84 );
85 }
86 return mv;
87 }
88
89 getPiece(x, y) {
90 const L = this.lastMoveEnd.length;
91 if (L >= 1 && this.lastMoveEnd[L-1].x == x && this.lastMoveEnd[L-1].y == y)
92 return this.lastMoveEnd[L-1].p;
93 return super.getPiece(x, y);
94 }
95
96 getPotentialMovesFrom([x, y], color) {
97 const L = this.lastMoveEnd.length;
98 if (
99 L >= 1 &&
100 (x != this.lastMoveEnd[L-1].x || y != this.lastMoveEnd[L-1].y)
101 ) {
102 // A capture was played: wrong square
103 return [];
104 }
105 return super.getPotentialMovesFrom([x, y], color);
106 }
107
108 underAttack_aux([x, y], color, explored) {
109 if (explored.some(sq => sq[0] == x && sq[1] == y))
110 // Start of an infinite loop: exit
111 return false;
112 explored.push([x, y]);
113 if (super.underAttack([x, y], [color]))
114 return true;
115 // Maybe indirect "chaining" attack:
116 const myColor = this.turn;
117 let res = false;
118 let toCheck = []; //check all but king (no need)
119 // Pawns:
120 const shiftToPawn = (myColor == 'w' ? -1 : 1);
121 for (let yShift of [-1, 1]) {
122 const [i, j] = [x + shiftToPawn, y + yShift];
123 if (
124 this.onBoard(i, j) &&
125 this.board[i][j] != "" &&
126 // NOTE: no need to check color (no enemy pawn can take directly)
127 this.getPiece(i, j) == 'p'
128 ) {
129 toCheck.push([i, j]);
130 }
131 }
132 // Knights:
133 this.pieces()['n'].both[0].steps.forEach(s => {
134 const [i, j] = [x + s[0], y + s[1]];
135 if (
136 this.onBoard(i, j) &&
137 this.board[i][j] != "" &&
138 this.getPiece(i, j) == 'n'
139 ) {
140 toCheck.push([i, j]);
141 }
142 });
143 // Sliders:
144 this.pieces()['q'].both[0].steps.forEach(s => {
145 let [i, j] = [x + s[0], y + s[1]];
146 while (this.onBoard(i, j) && this.board[i][j] == "") {
147 i += s[0];
148 j += s[1];
149 }
150 if (!this.onBoard(i, j))
151 return;
152 const piece = this.getPiece(i, j);
153 if (
154 piece == 'q' ||
155 (piece == 'r' && (s[0] == 0 || s[1] == 0)) ||
156 (piece == 'b' && (s[0] != 0 && s[1] != 0))
157 ) {
158 toCheck.push([i, j]);
159 }
160 });
161 for (let ij of toCheck) {
162 if (this.underAttack_aux(ij, color, explored))
163 return true;
164 }
165 return false;
166 }
167
168 underAttack([x, y], color) {
169 let explored = [];
170 return this.underAttack_aux([x, y], color, explored);
171 }
172
173 filterValid(moves) {
174 // No "checks" (except to forbid castle)
175 return moves;
176 }
177
178 isLastMove(move) {
179 return (
180 super.isLastMove(move) ||
181 move.vanish.length <= 1 ||
182 move.vanish[1].c != move.vanish[0].c ||
183 move.appear.length == 2 //castle!
184 );
185 }
186
187 postPlay(move) {
188 super.postPlay(move);
189 if (!this.isLastMove(move)) {
190 this.lastMoveEnd.push({
191 x: move.end.x,
192 y: move.end.y,
193 p: move.vanish[1].p //TODO: check this
194 });
195 }
196 }
197
198 };
199
200 // TODO: wrong rules! mismatch Convert (taking opponent pieces) and chaining (tend to be this) taking own units (with normal initial position).
201 // Initial Convert:
202
203 import { ChessRules, PiPo, Move } from "@/base_rules";
204 import { randInt } from "@/utils/alea";
205
206 export class ConvertRules extends ChessRules {
207
208 static get HasEnpassant() {
209 return false;
210 }
211
212 setOtherVariables(fen) {
213 super.setOtherVariables(fen);
214 // Stack of "last move" only for intermediate chaining
215 this.lastMoveEnd = [null];
216 }
217
218 static GenRandInitFen(options) {
219 const baseFen = ChessRules.GenRandInitFen(options);
220 return (
221 baseFen.substr(0, 8) +
222 "/8/pppppppp/8/8/PPPPPPPP/8/" +
223 baseFen.substr(35, 17)
224 );
225 }
226
227 getBasicMove([sx, sy], [ex, ey], tr) {
228 const L = this.lastMoveEnd.length;
229 const lm = this.lastMoveEnd[L-1];
230 const piece = (!!lm ? lm.p : null);
231 const c = this.turn;
232 if (this.board[ex][ey] == V.EMPTY) {
233 if (!!piece && !tr) tr = { c: c, p: piece }
234 let mv = super.getBasicMove([sx, sy], [ex, ey], tr);
235 if (!!piece) mv.vanish.pop();
236 return mv;
237 }
238 // Capture: initial, or inside a chain
239 const initPiece = (piece || this.getPiece(sx, sy));
240 const oppCol = V.GetOppCol(c);
241 const oppPiece = this.getPiece(ex, ey);
242 let mv = new Move({
243 start: { x: sx, y: sy },
244 end: { x: ex, y: ey },
245 appear: [
246 new PiPo({
247 x: ex,
248 y: ey,
249 c: c,
250 p: (!!tr ? tr.p : initPiece)
251 })
252 ],
253 vanish: [
254 new PiPo({
255 x: ex,
256 y: ey,
257 c: oppCol,
258 p: oppPiece
259 })
260 ]
261 });
262 if (!piece) {
263 // Initial capture
264 mv.vanish.unshift(
265 new PiPo({
266 x: sx,
267 y: sy,
268 c: c,
269 p: initPiece
270 })
271 );
272 }
273 // TODO: This "converted" indication isn't needed in fact,
274 // because it can be deduced from the move itself.
275 mv.end.converted = oppPiece;
276 return mv;
277 }
278
279 getPotentialMovesFrom([x, y], asA) {
280 const L = this.lastMoveEnd.length;
281 if (!!this.lastMoveEnd[L-1]) {
282 if (x != this.lastMoveEnd[L-1].x || y != this.lastMoveEnd[L-1].y)
283 // A capture was played: wrong square
284 return [];
285 asA = this.lastMoveEnd[L-1].p;
286 }
287 switch (asA || this.getPiece(x, y)) {
288 case V.PAWN: return super.getPotentialPawnMoves([x, y]);
289 case V.ROOK: return super.getPotentialRookMoves([x, y]);
290 case V.KNIGHT: return super.getPotentialKnightMoves([x, y]);
291 case V.BISHOP: return super.getPotentialBishopMoves([x, y]);
292 case V.QUEEN: return super.getPotentialQueenMoves([x, y]);
293 case V.KING: return super.getPotentialKingMoves([x, y]);
294 }
295 return [];
296 }
297
298 getPossibleMovesFrom(sq) {
299 const L = this.lastMoveEnd.length;
300 let asA = undefined;
301 if (!!this.lastMoveEnd[L-1]) {
302 if (
303 sq[0] != this.lastMoveEnd[L-1].x ||
304 sq[1] != this.lastMoveEnd[L-1].y
305 ) {
306 return [];
307 }
308 asA = this.lastMoveEnd[L-1].p;
309 }
310 return this.filterValid(this.getPotentialMovesFrom(sq, asA));
311 }
312
313 isAttacked_aux([x, y], color, explored) {
314 if (explored.some(sq => sq[0] == x && sq[1] == y))
315 // Start of an infinite loop: exit
316 return false;
317 explored.push([x, y]);
318 if (super.isAttacked([x, y], color)) return true;
319 // Maybe indirect "chaining" attack:
320 const myColor = this.turn
321 let res = false;
322 let toCheck = []; //check all but king (no need)
323 // Pawns:
324 const shiftToPawn = (myColor == 'w' ? -1 : 1);
325 for (let yShift of [-1, 1]) {
326 const [i, j] = [x + shiftToPawn, y + yShift];
327 if (
328 V.OnBoard(i, j) &&
329 this.board[i][j] != V.EMPTY &&
330 // NOTE: no need to check color (no enemy pawn can take directly)
331 this.getPiece(i, j) == V.PAWN
332 ) {
333 toCheck.push([i, j]);
334 }
335 }
336 // Knights:
337 V.steps[V.KNIGHT].forEach(s => {
338 const [i, j] = [x + s[0], y + s[1]];
339 if (
340 V.OnBoard(i, j) &&
341 this.board[i][j] != V.EMPTY &&
342 this.getPiece(i, j) == V.KNIGHT
343 ) {
344 toCheck.push([i, j]);
345 }
346 });
347 // Sliders:
348 V.steps[V.ROOK].concat(V.steps[V.BISHOP]).forEach(s => {
349 let [i, j] = [x + s[0], y + s[1]];
350 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
351 i += s[0];
352 j += s[1];
353 }
354 if (!V.OnBoard(i, j)) return;
355 const piece = this.getPiece(i, j);
356 if (
357 piece == V.QUEEN ||
358 (piece == V.ROOK && (s[0] == 0 || s[1] == 0)) ||
359 (piece == V.BISHOP && (s[0] != 0 && s[1] != 0))
360 ) {
361 toCheck.push([i, j]);
362 }
363 });
364 for (let ij of toCheck) {
365 if (this.isAttacked_aux(ij, color, explored)) return true;
366 }
367 return false;
368 }
369
370 isAttacked([x, y], color) {
371 let explored = [];
372 return this.isAttacked_aux([x, y], color, explored);
373 }
374
375 filterValid(moves) {
376 // No "checks" (except to forbid castle)
377 return moves;
378 }
379
380 getCheckSquares() {
381 return [];
382 }
383
384 prePlay(move) {
385 const c = this.turn;
386 // Extra conditions to avoid tracking converted kings:
387 if (
388 move.appear[0].p == V.KING &&
389 move.vanish.length >= 1 &&
390 move.vanish[0].p == V.KING
391 ) {
392 this.kingPos[c][0] = move.appear[0].x;
393 this.kingPos[c][1] = move.appear[0].y;
394 }
395 }
396
397 play(move) {
398 this.prePlay(move);
399 const c = this.turn;
400 move.flags = JSON.stringify(this.aggregateFlags());
401 V.PlayOnBoard(this.board, move);
402 if (!move.end.converted) {
403 // Not a capture: change turn
404 this.turn = V.GetOppCol(this.turn);
405 this.movesCount++;
406 this.lastMoveEnd.push(null);
407 }
408 else {
409 this.lastMoveEnd.push(
410 Object.assign({}, move.end, { p: move.end.converted })
411 );
412 }
413 super.updateCastleFlags(move, move.appear[0].p, c);
414 }
415
416 undo(move) {
417 this.disaggregateFlags(JSON.parse(move.flags));
418 this.lastMoveEnd.pop();
419 V.UndoOnBoard(this.board, move);
420 if (!move.end.converted) {
421 this.turn = V.GetOppCol(this.turn);
422 this.movesCount--;
423 }
424 this.postUndo(move);
425 }
426
427 postUndo(move) {
428 const c = this.getColor(move.start.x, move.start.y);
429 if (
430 move.appear[0].p == V.KING &&
431 move.vanish.length >= 1 &&
432 move.vanish[0].p == V.KING
433 ) {
434 this.kingPos[c] = [move.start.x, move.start.y];
435 }
436 }
437
438 getCurrentScore() {
439 const color = this.turn;
440 const kp = this.kingPos[color];
441 if (this.getColor(kp[0], kp[1]) != color)
442 return (color == "w" ? "0-1" : "1-0");
443 if (!super.atLeastOneMove()) return "1/2";
444 return "*";
445 }
446
447 getComputerMove() {
448 let initMoves = this.getAllValidMoves();
449 if (initMoves.length == 0) return null;
450 // Loop until valid move is found (no blocked pawn conversion...)
451 while (true) {
452 let moves = JSON.parse(JSON.stringify(initMoves));
453 let mvArray = [];
454 let mv = null;
455 // Just play random moves (for now at least. TODO?)
456 while (moves.length > 0) {
457 mv = moves[randInt(moves.length)];
458 mvArray.push(mv);
459 this.play(mv);
460 if (!!mv.end.converted)
461 // A piece was just converted
462 moves = this.getPotentialMovesFrom([mv.end.x, mv.end.y]);
463 else break;
464 }
465 for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]);
466 if (!mv.end.converted) return (mvArray.length > 1 ? mvArray : mvArray[0]);
467 }
468 return null; //never reached
469 }
470
471 getNotation(move) {
472 if (move.appear.length == 2 && move.appear[0].p == V.KING)
473 return (move.end.y < move.start.y ? "0-0-0" : "0-0");
474 const c = this.turn;
475 const L = this.lastMoveEnd.length;
476 const lm = this.lastMoveEnd[L-1];
477 const piece = (!lm ? move.appear[0].p : lm.p);
478 // Basic move notation:
479 let notation = piece.toUpperCase();
480 if (
481 this.board[move.end.x][move.end.y] != V.EMPTY ||
482 (piece == V.PAWN && move.start.y != move.end.y)
483 ) {
484 notation += "x";
485 }
486 const finalSquare = V.CoordsToSquare(move.end);
487 notation += finalSquare;
488
489 // Add potential promotion indications:
490 const firstLastRank = (c == 'w' ? [7, 0] : [0, 7]);
491 if (move.end.x == firstLastRank[1] && piece == V.PAWN)
492 notation += "=" + move.appear[0].p.toUpperCase();
493 return notation;
494 }
495
496 };