Fix duplicated moves in Cylinder and Circular chess
[vchess.git] / client / src / variants / Circular.js
1 import { ChessRules, PiPo, Move } from "@/base_rules";
2 import { ArrayFun } from "@/utils/array";
3 import { shuffle } from "@/utils/alea";
4
5 export class CircularRules extends ChessRules {
6 static get HasCastle() {
7 return false;
8 }
9
10 static get HasEnpassant() {
11 return false;
12 }
13
14 static get CanFlip() {
15 return false;
16 }
17
18 setFlags(fenflags) {
19 this.pawnFlags = {
20 w: [...Array(8).fill(true)], //pawns can move 2 squares?
21 b: [...Array(8).fill(true)]
22 };
23 for (let c of ["w", "b"]) {
24 for (let i = 0; i < 8; i++)
25 this.pawnFlags[c][i] = fenflags.charAt((c == "w" ? 0 : 8) + i) == "1";
26 }
27 }
28
29 aggregateFlags() {
30 return this.pawnFlags;
31 }
32
33 disaggregateFlags(flags) {
34 this.pawnFlags = flags;
35 }
36
37 static GenRandInitFen(randomness) {
38 if (randomness == 0)
39 return "8/8/pppppppp/rnbqkbnr/8/8/PPPPPPPP/RNBQKBNR w 0 1111111111111111";
40
41 let pieces = { w: new Array(8), b: new Array(8) };
42 // Shuffle pieces on first and last rank
43 for (let c of ["w", "b"]) {
44 if (c == 'b' && randomness == 1) {
45 pieces['b'] = pieces['w'];
46 break;
47 }
48
49 // Get random squares for every piece, totally freely
50 let positions = shuffle(ArrayFun.range(8));
51 const composition = ['b', 'b', 'r', 'r', 'n', 'n', 'k', 'q'];
52 const rem2 = positions[0] % 2;
53 if (rem2 == positions[1] % 2) {
54 // Fix bishops (on different colors)
55 for (let i=2; i<8; i++) {
56 if (positions[i] % 2 != rem2)
57 [positions[1], positions[i]] = [positions[i], positions[1]];
58 }
59 }
60 for (let i = 0; i < 8; i++) pieces[c][positions[i]] = composition[i];
61 }
62 return (
63 "8/8/pppppppp/" +
64 pieces["b"].join("") +
65 "/8/8/PPPPPPPP/" +
66 pieces["w"].join("").toUpperCase() +
67 // 16 flags: can pawns advance 2 squares?
68 " w 0 1111111111111111"
69 );
70 }
71
72 // Output basically x % 8 (circular board)
73 static ComputeX(x) {
74 let res = x % V.size.x;
75 if (res < 0)
76 res += V.size.x;
77 return res;
78 }
79
80 getSlideNJumpMoves([x, y], steps, oneStep) {
81 let moves = [];
82 // Don't add move twice when running on an infinite file:
83 let infiniteSteps = {};
84 outerLoop: for (let step of steps) {
85 if (!!infiniteSteps[(-step[0]) + "." + (-step[1])]) continue;
86 let i = V.ComputeX(x + step[0]);
87 let j = y + step[1];
88 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
89 moves.push(this.getBasicMove([x, y], [i, j]));
90 if (oneStep !== undefined) continue outerLoop;
91 i = V.ComputeX(i + step[0]);
92 j += step[1];
93 }
94 if (V.OnBoard(i, j)) {
95 if (i == x && j == y)
96 // Looped back onto initial square
97 infiniteSteps[step[0] + "." + step[1]] = true;
98 else if (this.canTake([x, y], [i, j]))
99 moves.push(this.getBasicMove([x, y], [i, j]));
100 }
101 }
102 return moves;
103 }
104
105 getPotentialPawnMoves([x, y]) {
106 const color = this.turn;
107 let moves = [];
108 const [sizeX, sizeY] = [V.size.x, V.size.y];
109 // All pawns go in the same direction!
110 const shiftX = -1;
111 const startRank = color == "w" ? sizeX - 2 : 2;
112
113 // One square forward
114 const nextRow = V.ComputeX(x + shiftX);
115 if (this.board[nextRow][y] == V.EMPTY) {
116 moves.push(this.getBasicMove([x, y], [nextRow, y]));
117 if (
118 x == startRank &&
119 this.pawnFlags[color][y] &&
120 this.board[x + 2 * shiftX][y] == V.EMPTY
121 ) {
122 // Two squares jump
123 moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y]));
124 }
125 }
126 // Captures
127 for (let shiftY of [-1, 1]) {
128 if (
129 y + shiftY >= 0 &&
130 y + shiftY < sizeY &&
131 this.board[nextRow][y + shiftY] != V.EMPTY &&
132 this.canTake([x, y], [nextRow, y + shiftY])
133 ) {
134 moves.push(this.getBasicMove([x, y], [nextRow, y + shiftY]));
135 }
136 }
137
138 return moves;
139 }
140
141 filterValid(moves) {
142 const filteredMoves = super.filterValid(moves);
143 // If at least one full move made, everything is allowed:
144 if (this.movesCount >= 2) return filteredMoves;
145 // Else, forbid check:
146 const oppCol = V.GetOppCol(this.turn);
147 return filteredMoves.filter(m => {
148 this.play(m);
149 const res = !this.underCheck(oppCol);
150 this.undo(m);
151 return res;
152 });
153 }
154
155 isAttackedByPawn([x, y], color) {
156 // pawn shift is always 1 (all pawns go the same way)
157 const attackerRow = V.ComputeX(x + 1);
158 for (let i of [-1, 1]) {
159 if (
160 y + i >= 0 &&
161 y + i < V.size.y &&
162 this.getPiece(attackerRow, y + i) == V.PAWN &&
163 this.getColor(attackerRow, y + i) == color
164 ) {
165 return true;
166 }
167 }
168 return false;
169 }
170
171 isAttackedBySlideNJump([x, y], color, piece, steps, oneStep) {
172 for (let step of steps) {
173 let rx = V.ComputeX(x + step[0]),
174 ry = y + step[1];
175 while (V.OnBoard(rx, ry) && this.board[rx][ry] == V.EMPTY && !oneStep) {
176 rx = V.ComputeX(rx + step[0]);
177 ry += step[1];
178 }
179 if (
180 V.OnBoard(rx, ry) &&
181 this.getPiece(rx, ry) == piece &&
182 this.getColor(rx, ry) == color
183 ) {
184 return true;
185 }
186 }
187 return false;
188 }
189
190 getFlagsFen() {
191 // Return pawns flags
192 let flags = "";
193 for (let c of ["w", "b"]) {
194 for (let i = 0; i < 8; i++) flags += this.pawnFlags[c][i] ? "1" : "0";
195 }
196 return flags;
197 }
198
199 postPlay(move) {
200 super.postPlay(move);
201 const c = move.vanish[0].c;
202 const secondRank = { "w": 6, "b": 2 };
203 if (move.vanish[0].p == V.PAWN && secondRank[c] == move.start.x)
204 // This move turns off a 2-squares pawn flag
205 this.pawnFlags[c][move.start.y] = false;
206 }
207
208 static get VALUES() {
209 return {
210 p: 1,
211 r: 5,
212 n: 3,
213 b: 4,
214 q: 10,
215 k: 1000
216 };
217 }
218
219 static get SEARCH_DEPTH() {
220 return 2;
221 }
222 };