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