Get rid of livereload dependency. Draft Baroque (still some issues)
[xogo.git] / variants / Baroque / class.js
1 import ChessRules from "/base_rules.js";
2 import GiveawayRules from "/variants/Giveaway/class.js";
3 import {Random} from "/utils/alea.js";
4 import PiPo from "/utils/PiPo.js";
5 import Move from "/utils/Move.js";
6
7 export default class BaroqueRules extends ChessRules {
8
9 static get Options() {
10 return {
11 select: C.Options.Select,
12 input: [
13 {
14 label: "Capture king",
15 variable: "taking",
16 type: "checkbox",
17 defaut: false
18 }
19 ],
20 styles: [
21 "balance",
22 "capture",
23 "crazyhouse",
24 "cylinder",
25 "doublemove",
26 "progressive",
27 "recycle",
28 "teleport"
29 ]
30 };
31 }
32
33 get hasFlags() {
34 return false;
35 }
36 get hasEnpassant() {
37 return false;
38 }
39
40 genRandInitBaseFen() {
41 if (this.options["randomness"] == 0)
42 return "rnbkqbnm/pppppppp/8/8/8/8/PPPPPPPP/MNBQKBNR";
43 const options = Object.assign({mode: "suicide"}, this.options);
44 const gr = new GiveawayRules({options: options, genFenOnly: true});
45 let res = gr.genRandInitBaseFen();
46 let immPos = {};
47 for (let c of ['w', 'b']) {
48 const rookChar = (c == 'w' ? 'R' : 'r');
49 switch (Random.randInt(2)) {
50 case 0:
51 immPos[c] = res.fen.indexOf(rookChar);
52 break;
53 case 1:
54 immPos[c] = res.fen.lastIndexOf(rookChar);
55 break;
56 }
57 }
58 res.fen = res.fen.substring(0, immPos['b']) + 'i' +
59 res.fen.substring(immPos['b'] + 1, immPos['w']) + 'I' +
60 res.fen.substring(immPos['w'] + 1);
61 return res;
62 }
63
64 // Although other pieces keep their names here for coding simplicity,
65 // keep in mind that:
66 // - a "rook" is a coordinator, capturing by coordinating with the king
67 // - a "knight" is a long-leaper, capturing as in draughts
68 // - a "bishop" is a chameleon, capturing as its prey
69 // - a "queen" is a withdrawer, capturing by moving away from pieces
70
71 pieces() {
72 return Object.assign({},
73 super.pieces(),
74 {
75 'p': {
76 "class": "pawn",
77 moves: [
78 {steps: [[0, 1], [0, -1], [1, 0], [-1, 0]]}
79 ]
80 },
81 'r': {
82 "class": "rook",
83 moves: [
84 {
85 steps: [
86 [1, 0], [0, 1], [-1, 0], [0, -1],
87 [1, 1], [1, -1], [-1, 1], [-1, -1]
88 ]
89 }
90 ]
91 },
92 'n': {
93 "class": "knight",
94 moveas: 'r'
95 },
96 'b': {
97 "class": "bishop",
98 moveas: 'r'
99 },
100 'q': {
101 "class": "queen",
102 moveas: 'r'
103 },
104 'i': {
105 "class": "immobilizer",
106 moveas: 'q'
107 }
108 }
109 );
110 }
111
112 // Is piece on square (x,y) immobilized?
113 isImmobilized([x, y]) {
114 const piece = this.getPiece(x, y);
115 const color = this.getColor(x, y);
116 const oppCol = C.GetOppCol(color);
117 const adjacentSteps = this.pieces()['k'].moves[0].steps;
118 for (let step of adjacentSteps) {
119 const [i, j] = [x + step[0], this.getY(y + step[1])];
120 if (
121 this.onBoard(i, j) &&
122 this.board[i][j] != "" &&
123 this.getColor(i, j) == oppCol
124 ) {
125 const oppPiece = this.getPiece(i, j);
126 if (oppPiece == 'i') {
127 // Moving is possible only if this immobilizer is neutralized
128 for (let step2 of adjacentSteps) {
129 const [i2, j2] = [i + step2[0], this.getY(j + step2[1])];
130 if (i2 == x && j2 == y)
131 continue; //skip initial piece!
132 if (
133 this.onBoard(i2, j2) &&
134 this.board[i2][j2] != "" &&
135 this.getColor(i2, j2) == color
136 ) {
137 if (['b', 'i'].includes(this.getPiece(i2, j2)))
138 return false;
139 }
140 }
141 return true; //immobilizer isn't neutralized
142 }
143 // Chameleons can't be immobilized twice,
144 // because there is only one immobilizer
145 if (oppPiece == 'b' && piece == 'i')
146 return true;
147 }
148 }
149 return false;
150 }
151
152 canTake([x1, y1], [x2, y2]) {
153 // Deactivate standard captures, except for king:
154 return (
155 this.getPiece(x1, y1) == 'k' &&
156 this.getColor(x1, y1) != this.getColor(x2, y2)
157 );
158 }
159
160 postProcessPotentialMoves(moves) {
161 if (moves.length == 0)
162 return [];
163 switch (moves[0].vanish[0].p) {
164 case 'p':
165 this.addPawnCaptures(moves);
166 break;
167 case 'r':
168 this.addRookCaptures(moves);
169 break;
170 case 'n':
171 const [x, y] = [moves[0].start.x, moves[0].start.y];
172 moves = moves.concat(this.getKnightCaptures([x, y]));
173 break;
174 case 'b':
175 moves = this.getBishopCaptures(moves);
176 break;
177 case 'q':
178 this.addPawnCaptures(moves);
179 break;
180 }
181 return moves;
182 }
183
184 // Modify capturing moves among listed pawn moves
185 addPawnCaptures(moves, byChameleon) {
186 const steps = this.pieces()['p'].moves[0].steps;
187 const color = this.turn;
188 const oppCol = C.GetOppCol(color);
189 moves.forEach(m => {
190 if (byChameleon && m.start.x != m.end.x && m.start.y != m.end.y)
191 // Chameleon not moving as pawn
192 return;
193 // Try capturing in every direction
194 for (let step of steps) {
195 const sq2 = [m.end.x + 2 * step[0], this.getY(m.end.y + 2 * step[1])];
196 if (
197 this.onBoard(sq2[0], sq2[1]) &&
198 this.board[sq2[0]][sq2[1]] != "" &&
199 this.getColor(sq2[0], sq2[1]) == color
200 ) {
201 // Potential capture
202 const sq1 = [m.end.x + step[0], this.getY(m.end.y + step[1])];
203 if (
204 this.board[sq1[0]][sq1[1]] != "" &&
205 this.getColor(sq1[0], sq1[1]) == oppCol
206 ) {
207 const piece1 = this.getPiece(sq1[0], sq1[1]);
208 if (!byChameleon || piece1 == 'p') {
209 m.vanish.push(
210 new PiPo({
211 x: sq1[0],
212 y: sq1[1],
213 c: oppCol,
214 p: piece1
215 })
216 );
217 }
218 }
219 }
220 }
221 });
222 }
223
224 addRookCaptures(moves, byChameleon) {
225 const color = this.turn;
226 const oppCol = V.GetOppCol(color);
227 const kp = this.searchKingPos(color)[0];
228 moves.forEach(m => {
229 // Check piece-king rectangle (if any) corners for enemy pieces
230 if (m.end.x == kp[0] || m.end.y == kp[1])
231 return; //"flat rectangle"
232 const corner1 = [m.end.x, kp[1]];
233 const corner2 = [kp[0], m.end.y];
234 for (let [i, j] of [corner1, corner2]) {
235 if (this.board[i][j] != "" && this.getColor(i, j) == oppCol) {
236 const piece = this.getPiece(i, j);
237 if (!byChameleon || piece == 'r') {
238 m.vanish.push(
239 new PiPo({
240 x: i,
241 y: j,
242 p: piece,
243 c: oppCol
244 })
245 );
246 }
247 }
248 }
249 });
250 }
251
252 getKnightCaptures(startSquare, byChameleon) {
253 // Look in every direction for captures
254 const steps = this.pieces()['r'].moves[0].steps;
255 const color = this.turn;
256 const oppCol = C.GetOppCol(color);
257 let moves = [];
258 const [x, y] = [startSquare[0], startSquare[1]];
259 const piece = this.getPiece(x, y); //might be a chameleon!
260 outerLoop: for (let step of steps) {
261 let [i, j] = [x + step[0], this.getY(y + step[1])];
262 while (this.onBoard(i, j) && this.board[i][j] == "")
263 [i, j] = [i + step[0], this.getY(j + step[1])];
264 if (
265 !this.onBoard(i, j) ||
266 this.getColor(i, j) == color ||
267 (byChameleon && this.getPiece(i, j) != 'n')
268 ) {
269 continue;
270 }
271 // last(thing), cur(thing) : stop if "cur" is our color,
272 // or beyond board limits, or if "last" isn't empty and cur neither.
273 // Otherwise, if cur is empty then add move until cur square;
274 // if cur is occupied then stop if !!byChameleon and the square not
275 // occupied by a leaper.
276 let last = [i, j];
277 let cur = [i + step[0], this.getY(j + step[1])];
278 let vanished = [new PiPo({x: x, y: y, c: color, p: piece})];
279 while (this.onBoard(cur[0], cur[1])) {
280 if (this.board[last[0]][last[1]] != "") {
281 const oppPiece = this.getPiece(last[0], last[1]);
282 if (!!byChameleon && oppPiece != 'n')
283 continue outerLoop;
284 // Something to eat:
285 vanished.push(
286 new PiPo({x: last[0], y: last[1], c: oppCol, p: oppPiece})
287 );
288 }
289 if (this.board[cur[0]][cur[1]] != "") {
290 if (
291 this.getColor(cur[0], cur[1]) == color ||
292 this.board[last[0]][last[1]] != ""
293 ) {
294 //TODO: redundant test
295 continue outerLoop;
296 }
297 }
298 else {
299 moves.push(
300 new Move({
301 appear: [new PiPo({x: cur[0], y: cur[1], c: color, p: piece})],
302 vanish: JSON.parse(JSON.stringify(vanished)), //TODO: required?
303 start: {x: x, y: y},
304 end: {x: cur[0], y: cur[1]}
305 })
306 );
307 }
308 last = [last[0] + step[0], this.getY(last[1] + step[1])];
309 cur = [cur[0] + step[0], this.getY(cur[1] + step[1])];
310 }
311 }
312 return moves;
313 }
314
315 // Chameleon
316 getBishopCaptures(moves) {
317 const [x, y] = [moves[0].start.x, moves[0].start.y];
318 moves = moves.concat(this.getKnightCaptures([x, y], "asChameleon"));
319 // No "king capture" because king cannot remain under check
320 this.addPawnCaptures(moves, "asChameleon");
321 this.addRookCaptures(moves, "asChameleon");
322 this.addQueenCaptures(moves, "asChameleon");
323 // Post-processing: merge similar moves, concatenating vanish arrays
324 let mergedMoves = {};
325 moves.forEach(m => {
326 const key = m.end.x + this.size.x * m.end.y;
327 if (!mergedMoves[key])
328 mergedMoves[key] = m;
329 else {
330 for (let i = 1; i < m.vanish.length; i++)
331 mergedMoves[key].vanish.push(m.vanish[i]);
332 }
333 });
334 return Object.values(mergedMoves);
335 }
336
337 addQueenCaptures(moves, byChameleon) {
338 if (moves.length == 0) return;
339 const [x, y] = [moves[0].start.x, moves[0].start.y];
340 const adjacentSteps = this.pieces()['r'].moves[0].steps;
341 let capturingDirections = [];
342 const color = this.turn;
343 const oppCol = C.GetOppCol(color);
344 adjacentSteps.forEach(step => {
345 const [i, j] = [x + step[0], this.getY(y + step[1])];
346 if (
347 this.onBoard(i, j) &&
348 this.board[i][j] != "" &&
349 this.getColor(i, j) == oppCol &&
350 (!byChameleon || this.getPiece(i, j) == 'q')
351 ) {
352 capturingDirections.push(step);
353 }
354 });
355 moves.forEach(m => {
356 const step = [
357 m.end.x != x ? (m.end.x - x) / Math.abs(m.end.x - x) : 0,
358 m.end.y != y ? (m.end.y - y) / Math.abs(m.end.y - y) : 0
359 ];
360 // NOTE: includes() and even _.isEqual() functions fail...
361 // TODO: this test should be done only once per direction
362 if (
363 capturingDirections.some(dir => {
364 return dir[0] == -step[0] && dir[1] == -step[1];
365 })
366 ) {
367 const [i, j] = [x - step[0], this.getY(y - step[1])];
368 m.vanish.push(
369 new PiPo({
370 x: i,
371 y: j,
372 p: this.getPiece(i, j),
373 c: oppCol
374 })
375 );
376 }
377 });
378 }
379
380 underAttack([x, y], oppCol) {
381 // Generate all potential opponent moves, check if king captured.
382 // TODO: do it more efficiently.
383 const color = this.getColor(x, y);
384 for (let i = 0; i < this.size.x; i++) {
385 for (let j = 0; j < this.size.y; j++) {
386 if (
387 this.board[i][j] != "" && this.getColor(i, j) == oppCol &&
388 this.getPotentialMovesFrom([i, j]).some(m => {
389 return (
390 m.vanish.length >= 2 &&
391 [1, m.vanish.length - 1].some(k => m.vanish[k].p == 'k')
392 );
393 })
394 ) {
395 return true;
396 }
397 }
398 }
399 return false;
400 }
401
402 };