Fix Eightpieces
[vchess.git] / client / src / variants / Yote.js
1 import { ChessRules, Move, PiPo } from "@/base_rules";
2 import { randInt } from "@/utils/alea";
3
4 export class YoteRules extends ChessRules {
5
6 static get Options() {
7 return null;
8 }
9
10 static get HasFlags() {
11 return false;
12 }
13
14 static get HasEnpassant() {
15 return false;
16 }
17
18 static get Monochrome() {
19 return true;
20 }
21
22 static get Notoodark() {
23 return true;
24 }
25
26 static get ReverseColors() {
27 return true;
28 }
29
30 static IsGoodPosition(position) {
31 if (position.length == 0) return false;
32 const rows = position.split("/");
33 if (rows.length != V.size.x) return false;
34 for (let row of rows) {
35 let sumElts = 0;
36 for (let i = 0; i < row.length; i++) {
37 if (row[i].toLowerCase() == V.PAWN) sumElts++;
38 else {
39 const num = parseInt(row[i], 10);
40 if (isNaN(num) || num <= 0) return false;
41 sumElts += num;
42 }
43 }
44 if (sumElts != V.size.y) return false;
45 }
46 return true;
47 }
48
49 static IsGoodFen(fen) {
50 if (!ChessRules.IsGoodFen(fen)) return false;
51 const fenParsed = V.ParseFen(fen);
52 // 3) Check reserves
53 if (
54 !fenParsed.reserve ||
55 !fenParsed.reserve.match(/^([0-9]{1,2},?){2,2}$/)
56 ) {
57 return false;
58 }
59 // 4) Check lastMove
60 if (!fenParsed.lastMove) return false;
61 const lmParts = fenParsed.lastMove.split(",");
62 for (lp of lmParts) {
63 if (lp != "-" && !lp.match(/^([a-f][1-5]){2,2}$/)) return false;
64 }
65 return true;
66 }
67
68 static ParseFen(fen) {
69 const fenParts = fen.split(" ");
70 return Object.assign(
71 ChessRules.ParseFen(fen),
72 {
73 reserve: fenParts[3],
74 lastMove: fenParts[4]
75 }
76 );
77 }
78
79 static GenRandInitFen() {
80 return "6/6/6/6/6 w 0 12,12 -,-";
81 }
82
83 getFen() {
84 return (
85 super.getFen() + " " +
86 this.getReserveFen() + " " +
87 this.getLastmoveFen()
88 );
89 }
90
91 getFenForRepeat() {
92 return super.getFenForRepeat() + "_" + this.getReserveFen();
93 }
94
95 getReserveFen() {
96 return (
97 (!this.reserve["w"] ? 0 : this.reserve["w"][V.PAWN]) + "," +
98 (!this.reserve["b"] ? 0 : this.reserve["b"][V.PAWN])
99 );
100 }
101
102 getLastmoveFen() {
103 const L = this.lastMove.length;
104 const lm = this.lastMove[L-1];
105 return (
106 (
107 !lm['w']
108 ? '-'
109 : V.CoordsToSquare(lm['w'].start) + V.CoordsToSquare(lm['w'].end)
110 )
111 + "," +
112 (
113 !lm['b']
114 ? '-'
115 : V.CoordsToSquare(lm['b'].start) + V.CoordsToSquare(lm['b'].end)
116 )
117 );
118 }
119
120 setOtherVariables(fen) {
121 const fenParsed = V.ParseFen(fen);
122 const reserve = fenParsed.reserve.split(",").map(x => parseInt(x, 10));
123 this.reserve = {
124 w: { [V.PAWN]: reserve[0] },
125 b: { [V.PAWN]: reserve[1] }
126 };
127 // And last moves (to avoid undoing your last move)
128 const lmParts = fenParsed.lastMove.split(",");
129 this.lastMove = [{ w: null, b: null }];
130 ['w', 'b'].forEach((c, i) => {
131 if (lmParts[i] != '-') {
132 this.lastMove[0][c] = {
133 start: V.SquareToCoords(lmParts[i].substr(0, 2)),
134 end: V.SquareToCoords(lmParts[i].substr(2))
135 };
136 }
137 });
138 // Local stack to know if (current) last move captured something
139 this.captures = [false];
140 }
141
142 static get size() {
143 return { x: 5, y: 6 };
144 }
145
146 getColor(i, j) {
147 if (i >= V.size.x) return i == V.size.x ? "w" : "b";
148 return this.board[i][j].charAt(0);
149 }
150
151 getPiece() {
152 return V.PAWN;
153 }
154
155 getPpath(b) {
156 return "Yote/" + b;
157 }
158
159 getReservePpath(index, color) {
160 return "Yote/" + color + V.PAWN;
161 }
162
163 static get RESERVE_PIECES() {
164 return [V.PAWN];
165 }
166
167 canIplay(side, [x, y]) {
168 if (this.turn != side) return false;
169 const L = this.captures.length;
170 if (!this.captures[L-1]) return this.getColor(x, y) == side;
171 return (x < V.size.x && this.getColor(x, y) != side);
172 }
173
174 hoverHighlight([x, y], side) {
175 if (!!side && side != this.turn) return false;
176 const L = this.captures.length;
177 if (!this.captures[L-1]) return false;
178 const oppCol = V.GetOppCol(this.turn);
179 return (this.board[x][y] != V.EMPTY && this.getColor(x, y) == oppCol);
180 }
181
182 // TODO: onlyClick() doesn't fulfill exactly its role.
183 // Seems that there is some lag... TOFIX
184 onlyClick([x, y]) {
185 const L = this.captures.length;
186 return (this.captures[L-1] && this.getColor(x, y) != this.turn);
187 }
188
189 // PATCH related to above TO-DO:
190 getPossibleMovesFrom([x, y]) {
191 if (x < V.size.x && this.board[x][y] == V.EMPTY) return [];
192 return super.getPossibleMovesFrom([x, y]);
193 }
194
195 doClick([x, y]) {
196 const L = this.captures.length;
197 if (!this.captures[L-1]) return null;
198 const oppCol = V.GetOppCol(this.turn);
199 if (this.board[x][y] == V.EMPTY || this.getColor(x, y) != oppCol)
200 return null;
201 return new Move({
202 appear: [],
203 vanish: [ new PiPo({ x: x, y: y, c: oppCol, p: V.PAWN }) ],
204 end: { x: x, y: y }
205 });
206 }
207
208 getReserveMoves(x) {
209 const color = this.turn;
210 const L = this.captures.length;
211 if (
212 this.captures[L-1] ||
213 !this.reserve[color] ||
214 this.reserve[color][V.PAWN] == 0
215 ) {
216 return [];
217 }
218 let moves = [];
219 for (let i = 0; i < V.size.x; i++) {
220 for (let j = 0; j < V.size.y; j++) {
221 if (this.board[i][j] == V.EMPTY) {
222 let mv = new Move({
223 appear: [
224 new PiPo({
225 x: i,
226 y: j,
227 c: color,
228 p: V.PAWN
229 })
230 ],
231 vanish: [],
232 start: { x: x, y: 0 }, //a bit artificial...
233 end: { x: i, y: j }
234 });
235 moves.push(mv);
236 }
237 }
238 }
239 return moves;
240 }
241
242 getPotentialMovesFrom([x, y]) {
243 const L = this.captures.length;
244 if (this.captures[L-1]) {
245 if (x >= V.size.x) return [];
246 const mv = this.doClick([x, y]);
247 return (!!mv ? [mv] : []);
248 }
249 if (x >= V.size.x) return this.getReserveMoves(x);
250 return this.getPotentialPawnMoves([x, y]);
251 }
252
253 getPotentialPawnMoves([x, y]) {
254 let moves = [];
255 const color = this.turn;
256 const L = this.lastMove.length;
257 const lm = this.lastMove[L-1];
258 let forbiddenStep = null;
259 if (!!lm[color] && x == lm[color].end.x && y == lm[color].end.y) {
260 forbiddenStep = [
261 lm[color].start.x - lm[color].end.x,
262 lm[color].start.y - lm[color].end.y
263 ];
264 }
265 const oppCol = V.GetOppCol(color);
266 for (let s of V.steps[V.ROOK]) {
267 const [i1, j1] = [x + s[0], y + s[1]];
268 if (V.OnBoard(i1, j1)) {
269 if (this.board[i1][j1] == V.EMPTY) {
270 if (
271 !forbiddenStep ||
272 s[0] != forbiddenStep[0] ||
273 s[1] != forbiddenStep[1]
274 ) {
275 moves.push(super.getBasicMove([x, y], [i1, j1]));
276 }
277 }
278 else if (this.getColor(i1, j1) == oppCol) {
279 const [i2, j2] = [i1 + s[0], j1 + s[1]];
280 if (V.OnBoard(i2, j2) && this.board[i2][j2] == V.EMPTY) {
281 let mv = new Move({
282 appear: [
283 new PiPo({ x: i2, y: j2, c: color, p: V.PAWN })
284 ],
285 vanish: [
286 new PiPo({ x: x, y: y, c: color, p: V.PAWN }),
287 new PiPo({ x: i1, y: j1, c: oppCol, p: V.PAWN })
288 ]
289 });
290 moves.push(mv);
291 }
292 }
293 }
294 }
295 return moves;
296 }
297
298 getAllPotentialMoves() {
299 const L = this.captures.length;
300 const color = (this.captures[L-1] ? V.GetOppCol(this.turn) : this.turn);
301 let potentialMoves = [];
302 for (let i = 0; i < V.size.x; i++) {
303 for (let j = 0; j < V.size.y; j++) {
304 if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) {
305 Array.prototype.push.apply(
306 potentialMoves,
307 this.getPotentialMovesFrom([i, j])
308 );
309 }
310 }
311 }
312 potentialMoves = potentialMoves.concat(
313 this.getReserveMoves(V.size.x + (color == "w" ? 0 : 1)));
314 return potentialMoves;
315 }
316
317 filterValid(moves) {
318 return moves;
319 }
320
321 getCheckSquares() {
322 return [];
323 }
324
325 atLeastOneMove() {
326 if (!super.atLeastOneMove()) {
327 // Search one reserve move
328 const moves =
329 this.getReserveMoves(V.size.x + (this.turn == "w" ? 0 : 1));
330 if (moves.length > 0) return true;
331 return false;
332 }
333 return true;
334 }
335
336 play(move) {
337 const color = this.turn;
338 move.turn = color; //for undo
339 const L = this.lastMove.length;
340 if (color == 'w')
341 this.lastMove.push({ w: null, b: this.lastMove[L-1]['b'] });
342 if (move.appear.length == move.vanish.length) { //== 1
343 // Normal move (non-capturing, non-dropping, non-removal)
344 let lm = this.lastMove[L - (color == 'w' ? 0 : 1)];
345 if (!lm[color]) lm[color] = {};
346 lm[color].start = move.start;
347 lm[color].end = move.end;
348 }
349 const oppCol = V.GetOppCol(color);
350 V.PlayOnBoard(this.board, move);
351 const captureNotEnding = (
352 move.vanish.length == 2 &&
353 this.board.some(b => b.some(cell => cell != "" && cell[0] == oppCol))
354 );
355 this.captures.push(captureNotEnding);
356 // Change turn unless I just captured something,
357 // and an opponent stone can be removed from board.
358 if (!captureNotEnding) {
359 this.turn = oppCol;
360 this.movesCount++;
361 }
362 this.postPlay(move);
363 }
364
365 undo(move) {
366 V.UndoOnBoard(this.board, move);
367 if (this.turn == 'b') this.lastMove.pop();
368 else this.lastMove['b'] = null;
369 this.captures.pop();
370 if (move.turn != this.turn) {
371 this.turn = move.turn;
372 this.movesCount--;
373 }
374 this.postUndo(move);
375 }
376
377 postPlay(move) {
378 if (move.vanish.length == 0) {
379 const color = move.appear[0].c;
380 this.reserve[color][V.PAWN]--;
381 if (this.reserve[color][V.PAWN] == 0) delete this.reserve[color];
382 }
383 }
384
385 postUndo(move) {
386 if (move.vanish.length == 0) {
387 const color = move.appear[0].c;
388 if (!this.reserve[color]) this.reserve[color] = { [V.PAWN]: 0 };
389 this.reserve[color][V.PAWN]++;
390 }
391 }
392
393 getCurrentScore() {
394 if (this.movesCount <= 2) return "*";
395 const color = this.turn;
396 // If no stones on board, or no move available, I lose
397 if (
398 this.board.every(b => {
399 return b.every(cell => {
400 return (cell == "" || cell[0] != color);
401 });
402 })
403 ||
404 !this.atLeastOneMove()
405 ) {
406 return (color == 'w' ? "0-1" : "1-0");
407 }
408 return "*";
409 }
410
411 getComputerMove() {
412 const moves = super.getAllValidMoves();
413 if (moves.length == 0) return null;
414 const color = this.turn;
415 const oppCol = V.GetOppCol(color);
416 // Capture available? If yes, play it
417 const captures = moves.filter(m => m.vanish.length == 2);
418 if (captures.length >= 1) {
419 const m1 = captures[randInt(captures.length)];
420 this.play(m1);
421 const moves2 = super.getAllValidMoves();
422 // Remove a stone which was about to capture one of ours, if possible
423 let candidates = [];
424 for (let m2 of moves2) {
425 const [x, y] = [m2.start.x, m2.start.y];
426 for (let s of V.steps[V.ROOK]) {
427 const [i, j] = [x + 2*s[0], y + 2*s[1]];
428 if (
429 V.OnBoard(i, j) &&
430 this.board[i][j] == V.EMPTY &&
431 this.board[i - s[0], j - s[1]] != V.EMPTY &&
432 this.getColor(i - s[0], j - s[1]) == color
433 ) {
434 candidates.push(m2);
435 break;
436 }
437 }
438 }
439 this.undo(m1);
440 if (candidates.length >= 1)
441 return [m1, candidates[randInt(candidates.length)]];
442 return [m1, moves2[randInt(moves2.length)]];
443 }
444 // Just play a random move, which if possible do not let a capture
445 let candidates = [];
446 for (let m of moves) {
447 this.play(m);
448 const moves2 = super.getAllValidMoves();
449 if (moves2.every(m2 => m2.vanish.length <= 1))
450 candidates.push(m);
451 this.undo(m);
452 }
453 if (candidates.length >= 1) return candidates[randInt(candidates.length)];
454 return moves[randInt(moves.length)];
455 }
456
457 getNotation(move) {
458 if (move.vanish.length == 0)
459 // Placement:
460 return "@" + V.CoordsToSquare(move.end);
461 if (move.appear.length == 0)
462 // Removal after capture:
463 return V.CoordsToSquare(move.start) + "X";
464 return (
465 V.CoordsToSquare(move.start) +
466 (move.vanish.length == 2 ? "x" : "") +
467 V.CoordsToSquare(move.end)
468 );
469 }
470
471 };