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