Almost finished problems logic. TODO: showProblem() part
[vchess.git] / public / javascripts / variants / Ultima.js
1 class UltimaRules extends ChessRules
2 {
3 static getPpath(b)
4 {
5 if (b[1] == "m") //'m' for Immobilizer (I is too similar to 1)
6 return "Ultima/" + b;
7 return b; //usual piece
8 }
9
10 static get PIECES() {
11 return ChessRules.PIECES.concat([V.IMMOBILIZER]);
12 }
13
14 static IsGoodFlags(flags)
15 {
16 return true; //anything is good: no flags
17 }
18
19 initVariables(fen)
20 {
21 this.kingPos = {'w':[-1,-1], 'b':[-1,-1]};
22 const fenParts = fen.split(" ");
23 const position = fenParts[0].split("/");
24 for (let i=0; i<position.length; i++)
25 {
26 let k = 0;
27 for (let j=0; j<position[i].length; j++)
28 {
29 switch (position[i].charAt(j))
30 {
31 case 'k':
32 this.kingPos['b'] = [i,k];
33 break;
34 case 'K':
35 this.kingPos['w'] = [i,k];
36 break;
37 default:
38 let num = parseInt(position[i].charAt(j));
39 if (!isNaN(num))
40 k += (num-1);
41 }
42 k++;
43 }
44 }
45 this.epSquares = []; //no en-passant here
46 }
47
48 setFlags(fen)
49 {
50 // TODO: for compatibility?
51 this.castleFlags = {"w":[false,false], "b":[false,false]};
52 }
53
54 static get IMMOBILIZER() { return 'm'; }
55 // Although other pieces keep their names here for coding simplicity,
56 // keep in mind that:
57 // - a "rook" is a coordinator, capturing by coordinating with the king
58 // - a "knight" is a long-leaper, capturing as in draughts
59 // - a "bishop" is a chameleon, capturing as its prey
60 // - a "queen" is a withdrawer, capturing by moving away from pieces
61
62 // Is piece on square (x,y) immobilized?
63 isImmobilized([x,y])
64 {
65 const piece = this.getPiece(x,y);
66 const color = this.getColor(x,y);
67 const oppCol = this.getOppCol(color);
68 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
69 outerLoop:
70 for (let step of adjacentSteps)
71 {
72 const [i,j] = [x+step[0],y+step[1]];
73 if (V.OnBoard(i,j) && this.board[i][j] != V.EMPTY
74 && this.getColor(i,j) == oppCol)
75 {
76 const oppPiece = this.getPiece(i,j);
77 if (oppPiece == V.IMMOBILIZER)
78 {
79 // Moving is impossible only if this immobilizer is not neutralized
80 for (let step2 of adjacentSteps)
81 {
82 const [i2,j2] = [i+step2[0],j+step2[1]];
83 if (i2 == x && j2 == y)
84 continue; //skip initial piece!
85 if (V.OnBoard(i2,j2) && this.board[i2][j2] != V.EMPTY
86 && this.getColor(i2,j2) == color)
87 {
88 if ([V.BISHOP,V.IMMOBILIZER].includes(this.getPiece(i2,j2)))
89 return false;
90 }
91 }
92 return true; //immobilizer isn't neutralized
93 }
94 // Chameleons can't be immobilized twice, because there is only one immobilizer
95 if (oppPiece == V.BISHOP && piece == V.IMMOBILIZER)
96 return true;
97 }
98 }
99 return false;
100 }
101
102 getPotentialMovesFrom([x,y])
103 {
104 // Pre-check: is thing on this square immobilized?
105 if (this.isImmobilized([x,y]))
106 return [];
107 switch (this.getPiece(x,y))
108 {
109 case V.IMMOBILIZER:
110 return this.getPotentialImmobilizerMoves([x,y]);
111 default:
112 return super.getPotentialMovesFrom([x,y]);
113 }
114 }
115
116 getSlideNJumpMoves([x,y], steps, oneStep)
117 {
118 const color = this.getColor(x,y);
119 const piece = this.getPiece(x,y);
120 let moves = [];
121 outerLoop:
122 for (let step of steps)
123 {
124 let i = x + step[0];
125 let j = y + step[1];
126 while (V.OnBoard(i,j) && this.board[i][j] == V.EMPTY)
127 {
128 moves.push(this.getBasicMove([x,y], [i,j]));
129 if (oneStep !== undefined)
130 continue outerLoop;
131 i += step[0];
132 j += step[1];
133 }
134 // Only king can take on occupied square:
135 if (piece==V.KING && V.OnBoard(i,j) && this.canTake([x,y], [i,j]))
136 moves.push(this.getBasicMove([x,y], [i,j]));
137 }
138 return moves;
139 }
140
141 // Modify capturing moves among listed pawn moves
142 addPawnCaptures(moves, byChameleon)
143 {
144 const steps = V.steps[V.ROOK];
145 const color = this.turn;
146 const oppCol = this.getOppCol(color);
147 moves.forEach(m => {
148 if (!!byChameleon && m.start.x!=m.end.x && m.start.y!=m.end.y)
149 return; //chameleon not moving as pawn
150 // Try capturing in every direction
151 for (let step of steps)
152 {
153 const sq2 = [m.end.x+2*step[0],m.end.y+2*step[1]];
154 if (V.OnBoard(sq2[0],sq2[1]) && this.board[sq2[0]][sq2[1]] != V.EMPTY
155 && this.getColor(sq2[0],sq2[1]) == color)
156 {
157 // Potential capture
158 const sq1 = [m.end.x+step[0],m.end.y+step[1]];
159 if (this.board[sq1[0]][sq1[1]] != V.EMPTY
160 && this.getColor(sq1[0],sq1[1]) == oppCol)
161 {
162 const piece1 = this.getPiece(sq1[0],sq1[1]);
163 if (!byChameleon || piece1 == V.PAWN)
164 {
165 m.vanish.push(new PiPo({
166 x:sq1[0],
167 y:sq1[1],
168 c:oppCol,
169 p:piece1
170 }));
171 }
172 }
173 }
174 }
175 });
176 }
177
178 // "Pincher"
179 getPotentialPawnMoves([x,y])
180 {
181 let moves = super.getPotentialRookMoves([x,y]);
182 this.addPawnCaptures(moves);
183 return moves;
184 }
185
186 addRookCaptures(moves, byChameleon)
187 {
188 const color = this.turn;
189 const oppCol = this.getOppCol(color);
190 const kp = this.kingPos[color];
191 moves.forEach(m => {
192 // Check piece-king rectangle (if any) corners for enemy pieces
193 if (m.end.x == kp[0] || m.end.y == kp[1])
194 return; //"flat rectangle"
195 const corner1 = [m.end.x, kp[1]];
196 const corner2 = [kp[0], m.end.y];
197 for (let [i,j] of [corner1,corner2])
198 {
199 if (this.board[i][j] != V.EMPTY && this.getColor(i,j) == oppCol)
200 {
201 const piece = this.getPiece(i,j);
202 if (!byChameleon || piece == V.ROOK)
203 {
204 m.vanish.push( new PiPo({
205 x:i,
206 y:j,
207 p:piece,
208 c:oppCol
209 }) );
210 }
211 }
212 }
213 });
214 }
215
216 // Coordinator
217 getPotentialRookMoves(sq)
218 {
219 let moves = super.getPotentialQueenMoves(sq);
220 this.addRookCaptures(moves);
221 return moves;
222 }
223
224 // Long-leaper
225 getKnightCaptures(startSquare, byChameleon)
226 {
227 // Look in every direction for captures
228 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
229 const color = this.turn;
230 const oppCol = this.getOppCol(color);
231 let moves = [];
232 const [x,y] = [startSquare[0],startSquare[1]];
233 const piece = this.getPiece(x,y); //might be a chameleon!
234 outerLoop:
235 for (let step of steps)
236 {
237 let [i,j] = [x+step[0], y+step[1]];
238 while (V.OnBoard(i,j) && this.board[i][j]==V.EMPTY)
239 {
240 i += step[0];
241 j += step[1];
242 }
243 if (!V.OnBoard(i,j) || this.getColor(i,j)==color
244 || (!!byChameleon && this.getPiece(i,j)!=V.KNIGHT))
245 {
246 continue;
247 }
248 // last(thing), cur(thing) : stop if "cur" is our color, or beyond board limits,
249 // or if "last" isn't empty and cur neither. Otherwise, if cur is empty then
250 // add move until cur square; if cur is occupied then stop if !!byChameleon and
251 // the square not occupied by a leaper.
252 let last = [i,j];
253 let cur = [i+step[0],j+step[1]];
254 let vanished = [ new PiPo({x:x,y:y,c:color,p:piece}) ];
255 while (V.OnBoard(cur[0],cur[1]))
256 {
257 if (this.board[last[0]][last[1]] != V.EMPTY)
258 {
259 const oppPiece = this.getPiece(last[0],last[1]);
260 if (!!byChameleon && oppPiece != V.KNIGHT)
261 continue outerLoop;
262 // Something to eat:
263 vanished.push( new PiPo({x:last[0],y:last[1],c:oppCol,p:oppPiece}) );
264 }
265 if (this.board[cur[0]][cur[1]] != V.EMPTY)
266 {
267 if (this.getColor(cur[0],cur[1]) == color
268 || this.board[last[0]][last[1]] != V.EMPTY) //TODO: redundant test
269 {
270 continue outerLoop;
271 }
272 }
273 else
274 {
275 moves.push(new Move({
276 appear: [ new PiPo({x:cur[0],y:cur[1],c:color,p:piece}) ],
277 vanish: JSON.parse(JSON.stringify(vanished)), //TODO: required?
278 start: {x:x,y:y},
279 end: {x:cur[0],y:cur[1]}
280 }));
281 }
282 last = [last[0]+step[0],last[1]+step[1]];
283 cur = [cur[0]+step[0],cur[1]+step[1]];
284 }
285 }
286 return moves;
287 }
288
289 // Long-leaper
290 getPotentialKnightMoves(sq)
291 {
292 return super.getPotentialQueenMoves(sq).concat(this.getKnightCaptures(sq));
293 }
294
295 getPotentialBishopMoves([x,y])
296 {
297 let moves = super.getPotentialQueenMoves([x,y])
298 .concat(this.getKnightCaptures([x,y],"asChameleon"));
299 // No "king capture" because king cannot remain under check
300 this.addPawnCaptures(moves, "asChameleon");
301 this.addRookCaptures(moves, "asChameleon");
302 this.addQueenCaptures(moves, "asChameleon");
303 // Post-processing: merge similar moves, concatenating vanish arrays
304 let mergedMoves = {};
305 moves.forEach(m => {
306 const key = m.end.x + V.size.x * m.end.y;
307 if (!mergedMoves[key])
308 mergedMoves[key] = m;
309 else
310 {
311 for (let i=1; i<m.vanish.length; i++)
312 mergedMoves[key].vanish.push(m.vanish[i]);
313 }
314 });
315 // Finally return an array
316 moves = [];
317 Object.keys(mergedMoves).forEach(k => { moves.push(mergedMoves[k]); });
318 return moves;
319 }
320
321 // Withdrawer
322 addQueenCaptures(moves, byChameleon)
323 {
324 if (moves.length == 0)
325 return;
326 const [x,y] = [moves[0].start.x,moves[0].start.y];
327 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
328 let capturingDirections = [];
329 const color = this.turn;
330 const oppCol = this.getOppCol(color);
331 adjacentSteps.forEach(step => {
332 const [i,j] = [x+step[0],y+step[1]];
333 if (V.OnBoard(i,j) && this.board[i][j] != V.EMPTY && this.getColor(i,j) == oppCol
334 && (!byChameleon || this.getPiece(i,j) == V.QUEEN))
335 {
336 capturingDirections.push(step);
337 }
338 });
339 moves.forEach(m => {
340 const step = [
341 m.end.x!=x ? (m.end.x-x)/Math.abs(m.end.x-x) : 0,
342 m.end.y!=y ? (m.end.y-y)/Math.abs(m.end.y-y) : 0
343 ];
344 // NOTE: includes() and even _.isEqual() functions fail...
345 // TODO: this test should be done only once per direction
346 if (capturingDirections.some(dir =>
347 { return (dir[0]==-step[0] && dir[1]==-step[1]); }))
348 {
349 const [i,j] = [x-step[0],y-step[1]];
350 m.vanish.push(new PiPo({
351 x:i,
352 y:j,
353 p:this.getPiece(i,j),
354 c:oppCol
355 }));
356 }
357 });
358 }
359
360 getPotentialQueenMoves(sq)
361 {
362 let moves = super.getPotentialQueenMoves(sq);
363 this.addQueenCaptures(moves);
364 return moves;
365 }
366
367 getPotentialImmobilizerMoves(sq)
368 {
369 // Immobilizer doesn't capture
370 return super.getPotentialQueenMoves(sq);
371 }
372
373 getPotentialKingMoves(sq)
374 {
375 return this.getSlideNJumpMoves(sq,
376 V.steps[V.ROOK].concat(V.steps[V.BISHOP]), "oneStep");
377 }
378
379 // isAttacked() is OK because the immobilizer doesn't take
380
381 isAttackedByPawn([x,y], colors)
382 {
383 // Square (x,y) must be surroundable by two enemy pieces,
384 // and one of them at least should be a pawn (moving).
385 const dirs = [ [1,0],[0,1] ];
386 const steps = V.steps[V.ROOK];
387 for (let dir of dirs)
388 {
389 const [i1,j1] = [x-dir[0],y-dir[1]]; //"before"
390 const [i2,j2] = [x+dir[0],y+dir[1]]; //"after"
391 if (V.OnBoard(i1,j1) && V.OnBoard(i2,j2))
392 {
393 if ((this.board[i1][j1]!=V.EMPTY && colors.includes(this.getColor(i1,j1))
394 && this.board[i2][j2]==V.EMPTY)
395 ||
396 (this.board[i2][j2]!=V.EMPTY && colors.includes(this.getColor(i2,j2))
397 && this.board[i1][j1]==V.EMPTY))
398 {
399 // Search a movable enemy pawn landing on the empty square
400 for (let step of steps)
401 {
402 let [ii,jj] = (this.board[i1][j1]==V.EMPTY ? [i1,j1] : [i2,j2]);
403 let [i3,j3] = [ii+step[0],jj+step[1]];
404 while (V.OnBoard(i3,j3) && this.board[i3][j3]==V.EMPTY)
405 {
406 i3 += step[0];
407 j3 += step[1];
408 }
409 if (V.OnBoard(i3,j3) && colors.includes(this.getColor(i3,j3))
410 && this.getPiece(i3,j3) == V.PAWN && !this.isImmobilized([i3,j3]))
411 {
412 return true;
413 }
414 }
415 }
416 }
417 }
418 return false;
419 }
420
421 isAttackedByRook([x,y], colors)
422 {
423 // King must be on same column or row,
424 // and a rook should be able to reach a capturing square
425 // colors contains only one element, giving the oppCol and thus king position
426 const sameRow = (x == this.kingPos[colors[0]][0]);
427 const sameColumn = (y == this.kingPos[colors[0]][1]);
428 if (sameRow || sameColumn)
429 {
430 // Look for the enemy rook (maximum 1)
431 for (let i=0; i<V.size.x; i++)
432 {
433 for (let j=0; j<V.size.y; j++)
434 {
435 if (this.board[i][j] != V.EMPTY && colors.includes(this.getColor(i,j))
436 && this.getPiece(i,j) == V.ROOK)
437 {
438 if (this.isImmobilized([i,j]))
439 return false; //because only one rook
440 // Can it reach a capturing square?
441 // Easy but quite suboptimal way (TODO): generate all moves (turn is OK)
442 const moves = this.getPotentialMovesFrom([i,j]);
443 for (let move of moves)
444 {
445 if (sameRow && move.end.y == y || sameColumn && move.end.x == x)
446 return true;
447 }
448 }
449 }
450 }
451 }
452 return false;
453 }
454
455 isAttackedByKnight([x,y], colors)
456 {
457 // Square (x,y) must be on same line as a knight,
458 // and there must be empty square(s) behind.
459 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
460 outerLoop:
461 for (let step of steps)
462 {
463 const [i0,j0] = [x+step[0],y+step[1]];
464 if (V.OnBoard(i0,j0) && this.board[i0][j0] == V.EMPTY)
465 {
466 // Try in opposite direction:
467 let [i,j] = [x-step[0],y-step[1]];
468 while (V.OnBoard(i,j))
469 {
470 while (V.OnBoard(i,j) && this.board[i][j] == V.EMPTY)
471 {
472 i -= step[0];
473 j -= step[1];
474 }
475 if (V.OnBoard(i,j))
476 {
477 if (colors.includes(this.getColor(i,j)))
478 {
479 if (this.getPiece(i,j) == V.KNIGHT && !this.isImmobilized([i,j]))
480 return true;
481 continue outerLoop;
482 }
483 // [else] Our color, could be captured *if there was an empty space*
484 if (this.board[i+step[0]][j+step[1]] != V.EMPTY)
485 continue outerLoop;
486 i -= step[0];
487 j -= step[1];
488 }
489 }
490 }
491 }
492 return false;
493 }
494
495 isAttackedByBishop([x,y], colors)
496 {
497 // We cheat a little here: since this function is used exclusively for king,
498 // it's enough to check the immediate surrounding of the square.
499 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
500 for (let step of adjacentSteps)
501 {
502 const [i,j] = [x+step[0],y+step[1]];
503 if (V.OnBoard(i,j) && this.board[i][j]!=V.EMPTY
504 && colors.includes(this.getColor(i,j)) && this.getPiece(i,j) == V.BISHOP)
505 {
506 return true; //bishops are never immobilized
507 }
508 }
509 return false;
510 }
511
512 isAttackedByQueen([x,y], colors)
513 {
514 // Square (x,y) must be adjacent to a queen, and the queen must have
515 // some free space in the opposite direction from (x,y)
516 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
517 for (let step of adjacentSteps)
518 {
519 const sq2 = [x+2*step[0],y+2*step[1]];
520 if (V.OnBoard(sq2[0],sq2[1]) && this.board[sq2[0]][sq2[1]] == V.EMPTY)
521 {
522 const sq1 = [x+step[0],y+step[1]];
523 if (this.board[sq1[0]][sq1[1]] != V.EMPTY
524 && colors.includes(this.getColor(sq1[0],sq1[1]))
525 && this.getPiece(sq1[0],sq1[1]) == V.QUEEN
526 && !this.isImmobilized(sq1))
527 {
528 return true;
529 }
530 }
531 }
532 return false;
533 }
534
535 updateVariables(move)
536 {
537 // Just update king(s) position(s)
538 const piece = this.getPiece(move.start.x,move.start.y);
539 const c = this.getColor(move.start.x,move.start.y);
540 if (piece == V.KING && move.appear.length > 0)
541 {
542 this.kingPos[c][0] = move.appear[0].x;
543 this.kingPos[c][1] = move.appear[0].y;
544 }
545 }
546
547 static get VALUES() { //TODO: totally experimental!
548 return {
549 'p': 1,
550 'r': 2,
551 'n': 5,
552 'b': 3,
553 'q': 3,
554 'm': 5,
555 'k': 1000
556 };
557 }
558
559 static get SEARCH_DEPTH() { return 2; } //TODO?
560
561 static GenRandInitFen()
562 {
563 let pieces = { "w": new Array(8), "b": new Array(8) };
564 // Shuffle pieces on first and last rank
565 for (let c of ["w","b"])
566 {
567 let positions = _.range(8);
568 // Get random squares for every piece, totally freely
569
570 let randIndex = _.random(7);
571 const bishop1Pos = positions[randIndex];
572 positions.splice(randIndex, 1);
573
574 randIndex = _.random(6);
575 const bishop2Pos = positions[randIndex];
576 positions.splice(randIndex, 1);
577
578 randIndex = _.random(5);
579 const knight1Pos = positions[randIndex];
580 positions.splice(randIndex, 1);
581
582 randIndex = _.random(4);
583 const knight2Pos = positions[randIndex];
584 positions.splice(randIndex, 1);
585
586 randIndex = _.random(3);
587 const queenPos = positions[randIndex];
588 positions.splice(randIndex, 1);
589
590 randIndex = _.random(2);
591 const kingPos = positions[randIndex];
592 positions.splice(randIndex, 1);
593
594 randIndex = _.random(1);
595 const rookPos = positions[randIndex];
596 positions.splice(randIndex, 1);
597 const immobilizerPos = positions[0];
598
599 pieces[c][bishop1Pos] = 'b';
600 pieces[c][bishop2Pos] = 'b';
601 pieces[c][knight1Pos] = 'n';
602 pieces[c][knight2Pos] = 'n';
603 pieces[c][queenPos] = 'q';
604 pieces[c][kingPos] = 'k';
605 pieces[c][rookPos] = 'r';
606 pieces[c][immobilizerPos] = 'm';
607 }
608 return pieces["b"].join("") +
609 "/pppppppp/8/8/8/8/PPPPPPPP/" +
610 pieces["w"].join("").toUpperCase() +
611 " 0000"; //TODO: flags?!
612 }
613
614 getFlagsFen()
615 {
616 return "0000"; //TODO: or "-" ?
617 }
618
619 getNotation(move)
620 {
621 const initialSquare =
622 String.fromCharCode(97 + move.start.y) + (V.size.x-move.start.x);
623 const finalSquare = String.fromCharCode(97 + move.end.y) + (V.size.x-move.end.x);
624 let notation = undefined;
625 if (move.appear[0].p == V.PAWN)
626 {
627 // Pawn: generally ambiguous short notation, so we use full description
628 notation = "P" + initialSquare + finalSquare;
629 }
630 else if (move.appear[0].p == V.KING)
631 notation = "K" + (move.vanish.length>1 ? "x" : "") + finalSquare;
632 else
633 notation = move.appear[0].p.toUpperCase() + finalSquare;
634 if (move.vanish.length > 1 && move.appear[0].p != V.KING)
635 notation += "X"; //capture mark (not describing what is captured...)
636 return notation;
637 }
638 }