Almost added TitanChess + EvolutionChess
[vchess.git] / client / src / variants / Hidden.js
1 import { ChessRules, PiPo, Move } from "@/base_rules";
2 import { ArrayFun } from "@/utils/array";
3 import { randInt } from "@/utils/alea";
4
5 export class HiddenRules extends ChessRules {
6
7 static get HasFlags() {
8 return false;
9 }
10
11 static get HasEnpassant() {
12 return false;
13 }
14
15 static get SomeHiddenMoves() {
16 return true;
17 }
18
19 // Analyse in Hidden mode makes no sense
20 static get CanAnalyze() {
21 return false;
22 }
23
24 // Moves are revealed only when game ends, but are highlighted on board
25 static get ShowMoves() {
26 return "highlight";
27 }
28
29 static get HIDDEN_DECODE() {
30 return {
31 s: "p",
32 t: "q",
33 u: "r",
34 c: "b",
35 o: "n",
36 l: "k"
37 };
38 }
39 static get HIDDEN_CODE() {
40 return {
41 p: "s",
42 q: "t",
43 r: "u",
44 b: "c",
45 n: "o",
46 k: "l"
47 };
48 }
49
50 // Turn a hidden piece or revealed piece into revealed piece:
51 static Decode(p) {
52 if (Object.keys(V.HIDDEN_DECODE).includes(p))
53 return V.HIDDEN_DECODE[p];
54 return p;
55 }
56
57 static get PIECES() {
58 return ChessRules.PIECES.concat(Object.keys(V.HIDDEN_DECODE));
59 }
60
61 // Pieces can be hidden :)
62 getPiece(i, j) {
63 const piece = this.board[i][j].charAt(1);
64 if (Object.keys(V.HIDDEN_DECODE).includes(piece))
65 return V.HIDDEN_DECODE[piece];
66 return piece;
67 }
68
69 getPpath(b, color, score) {
70 if (Object.keys(V.HIDDEN_DECODE).includes(b[1])) {
71 // Supposed to be hidden.
72 if (score == "*" && (!color || color != b[0]))
73 return "Hidden/" + b[0] + "p";
74 // Else: condition OK to show the piece
75 return b[0] + V.HIDDEN_DECODE[b[1]];
76 }
77 // The piece is already not supposed to be hidden:
78 return b;
79 }
80
81 // Scan board for kings positions (no castling)
82 scanKings(fen) {
83 this.kingPos = { w: [-1, -1], b: [-1, -1] };
84 const fenRows = V.ParseFen(fen).position.split("/");
85 for (let i = 0; i < fenRows.length; i++) {
86 let k = 0; //column index on board
87 for (let j = 0; j < fenRows[i].length; j++) {
88 switch (fenRows[i].charAt(j)) {
89 case "k":
90 case "l":
91 this.kingPos["b"] = [i, k];
92 break;
93 case "K":
94 case "L":
95 this.kingPos["w"] = [i, k];
96 break;
97 default: {
98 const num = parseInt(fenRows[i].charAt(j), 10);
99 if (!isNaN(num)) k += num - 1;
100 }
101 }
102 k++;
103 }
104 }
105 }
106
107 getBasicMove([sx, sy], [ex, ey], tr) {
108 if (
109 tr &&
110 Object.keys(V.HIDDEN_DECODE).includes(this.board[sx][sy].charAt(1))
111 ) {
112 // The transformed piece is a priori hidden
113 tr.p = V.HIDDEN_CODE[tr.p];
114 }
115 let mv = new Move({
116 appear: [
117 new PiPo({
118 x: ex,
119 y: ey,
120 c: tr ? tr.c : this.getColor(sx, sy),
121 p: tr ? tr.p : this.board[sx][sy].charAt(1)
122 })
123 ],
124 vanish: [
125 new PiPo({
126 x: sx,
127 y: sy,
128 c: this.getColor(sx, sy),
129 p: this.board[sx][sy].charAt(1)
130 })
131 ]
132 });
133
134 // The opponent piece disappears if we take it
135 if (this.board[ex][ey] != V.EMPTY) {
136 mv.vanish.push(
137 new PiPo({
138 x: ex,
139 y: ey,
140 c: this.getColor(ex, ey),
141 p: this.board[ex][ey].charAt(1)
142 })
143 );
144 // Pieces are revealed when they capture
145 mv.appear[0].p = V.Decode(mv.appear[0].p);
146 }
147
148 return mv;
149 }
150
151 filterValid(moves) {
152 return moves;
153 }
154
155 // Ignore randomness here: placement is always random asymmetric
156 static GenRandInitFen() {
157 let pieces = { w: new Array(8), b: new Array(8) };
158 // Shuffle pieces + pawns on two first ranks
159 for (let c of ["w", "b"]) {
160 let positions = ArrayFun.range(16);
161
162 // Get random squares for bishops
163 let randIndex = 2 * randInt(8);
164 const bishop1Pos = positions[randIndex];
165 // The second bishop must be on a square of different color
166 let randIndex_tmp = 2 * randInt(8) + 1;
167 const bishop2Pos = positions[randIndex_tmp];
168 // Remove chosen squares
169 positions.splice(Math.max(randIndex, randIndex_tmp), 1);
170 positions.splice(Math.min(randIndex, randIndex_tmp), 1);
171
172 // Get random squares for knights
173 randIndex = randInt(14);
174 const knight1Pos = positions[randIndex];
175 positions.splice(randIndex, 1);
176 randIndex = randInt(13);
177 const knight2Pos = positions[randIndex];
178 positions.splice(randIndex, 1);
179
180 // Get random squares for rooks
181 randIndex = randInt(12);
182 const rook1Pos = positions[randIndex];
183 positions.splice(randIndex, 1);
184 randIndex = randInt(11);
185 const rook2Pos = positions[randIndex];
186 positions.splice(randIndex, 1);
187
188 // Get random square for queen
189 randIndex = randInt(10);
190 const queenPos = positions[randIndex];
191 positions.splice(randIndex, 1);
192
193 // Get random square for king
194 randIndex = randInt(9);
195 const kingPos = positions[randIndex];
196 positions.splice(randIndex, 1);
197
198 // Pawns position are all remaining slots:
199 for (let p of positions)
200 pieces[c][p] = "s";
201
202 // Finally put the shuffled pieces in the board array
203 pieces[c][rook1Pos] = "u";
204 pieces[c][knight1Pos] = "o";
205 pieces[c][bishop1Pos] = "c";
206 pieces[c][queenPos] = "t";
207 pieces[c][kingPos] = "l";
208 pieces[c][bishop2Pos] = "c";
209 pieces[c][knight2Pos] = "o";
210 pieces[c][rook2Pos] = "u";
211 }
212 let upFen = pieces["b"].join("");
213 upFen = upFen.substr(0,8) + "/" +
214 upFen.substr(8).split("").reverse().join("");
215 let downFen = pieces["b"].join("").toUpperCase();
216 downFen = downFen.substr(0,8) + "/" +
217 downFen.substr(8).split("").reverse().join("");
218 return upFen + "/8/8/8/8/" + downFen + " w 0";
219 }
220
221 getCheckSquares() {
222 return [];
223 }
224
225 postPlay(move) {
226 super.postPlay(move);
227 if (
228 move.vanish.length >= 2 &&
229 [V.KING,V.HIDDEN_CODE[V.KING]].includes(move.vanish[1].p)
230 ) {
231 // We took opponent king
232 this.kingPos[this.turn] = [-1, -1];
233 }
234 }
235
236 postUndo(move) {
237 super.postUndo(move);
238 const c = move.vanish[0].c;
239 const oppCol = V.GetOppCol(c);
240 if (this.kingPos[oppCol][0] < 0)
241 // Last move took opponent's king:
242 this.kingPos[oppCol] = [move.vanish[1].x, move.vanish[1].y];
243 }
244
245 getCurrentScore() {
246 const color = this.turn;
247 const kp = this.kingPos[color];
248 if (kp[0] < 0)
249 // King disappeared
250 return color == "w" ? "0-1" : "1-0";
251 // Assume that stalemate is impossible:
252 return "*";
253 }
254
255 getComputerMove() {
256 const color = this.turn;
257 let moves = this.getAllValidMoves();
258 for (let move of moves) {
259 move.eval = 0; //a priori...
260
261 // Can I take something ? If yes, do it with some probability
262 if (move.vanish.length == 2 && move.vanish[1].c != color) {
263 // OK this isn't a castling move
264 const myPieceVal = V.VALUES[move.appear[0].p];
265 const hisPieceVal =
266 Object.keys(V.HIDDEN_DECODE).includes(move.vanish[1].p)
267 ? undefined
268 : V.VALUES[move.vanish[1].p];
269 if (!hisPieceVal) {
270 // Opponent's piece is unknown: do not take too much risk
271 move.eval = -myPieceVal + 1.5; //so that pawns always take
272 }
273 // Favor captures
274 else if (myPieceVal <= hisPieceVal)
275 move.eval = hisPieceVal - myPieceVal + 1;
276 else {
277 // Taking a pawn with minor piece,
278 // or minor piece or pawn with a rook,
279 // or anything but a queen with a queen,
280 // or anything with a king.
281 move.eval = hisPieceVal - myPieceVal;
282 }
283 } else {
284 // If no capture, favor small step moves,
285 // but sometimes move the knight anyway
286 const penalty = V.Decode(move.vanish[0].p) != V.KNIGHT
287 ? Math.abs(move.end.x - move.start.x) +
288 Math.abs(move.end.y - move.start.y)
289 : (Math.random() < 0.5 ? 3 : 1);
290 move.eval -= penalty / (V.size.x + V.size.y - 1);
291 }
292
293 // TODO: also favor movements toward the center?
294 }
295
296 moves.sort((a, b) => b.eval - a.eval);
297 let candidates = [0];
298 for (let j = 1; j < moves.length && moves[j].eval == moves[0].eval; j++)
299 candidates.push(j);
300 return moves[candidates[randInt(candidates.length)]];
301 }
302
303 getNotation(move) {
304 // Translate final square
305 const finalSquare = V.CoordsToSquare(move.end);
306
307 const piece = this.getPiece(move.start.x, move.start.y);
308 if (piece == V.PAWN) {
309 // Pawn move
310 let notation = "";
311 if (move.vanish.length > move.appear.length) {
312 // Capture
313 const startColumn = V.CoordToColumn(move.start.y);
314 notation = startColumn + "x" + finalSquare;
315 }
316 else notation = finalSquare;
317 if (move.appear.length > 0 && !["p","s"].includes(move.appear[0].p)) {
318 // Promotion
319 const appearPiece = V.Decode(move.appear[0].p);
320 notation += "=" + appearPiece.toUpperCase();
321 }
322 return notation;
323 }
324 // Piece movement
325 return (
326 piece.toUpperCase() +
327 (move.vanish.length > move.appear.length ? "x" : "") +
328 finalSquare
329 );
330 }
331
332 };