Some debug, plan several short + long term TODOs
[vchess.git] / public / javascripts / variants / Marseille.js
1 class MarseilleRules extends ChessRules
2 {
3 static IsGoodEnpassant(enpassant)
4 {
5 if (enpassant != "-")
6 {
7 const squares = enpassant.split(",");
8 if (squares.length > 2)
9 return false;
10 for (let sq of squares)
11 {
12 const ep = V.SquareToCoords(sq);
13 if (isNaN(ep.x) || !V.OnBoard(ep))
14 return false;
15 }
16 }
17 return true;
18 }
19
20 getTurnFen()
21 {
22 if (this.startAtFirstMove && this.moves.length==0)
23 return "w";
24 return this.turn + this.subTurn;
25 }
26
27 // There may be 2 enPassant squares (if 2 pawns jump 2 squares in same turn)
28 getEnpassantFen()
29 {
30 const L = this.epSquares.length;
31 if (this.epSquares[L-1].every(epsq => epsq === undefined))
32 return "-"; //no en-passant
33 let res = "";
34 this.epSquares[L-1].forEach(epsq => {
35 if (!!epsq)
36 res += V.CoordsToSquare(epsq) + ",";
37 });
38 return res.slice(0,-1); //remove last comma
39 }
40
41 setOtherVariables(fen)
42 {
43 const parsedFen = V.ParseFen(fen);
44 this.setFlags(parsedFen.flags);
45 if (parsedFen.enpassant == "-")
46 this.epSquares = [ [undefined] ];
47 else
48 {
49 let res = [];
50 const squares = parsedFen.enpassant.split(",");
51 for (let sq of squares)
52 res.push(V.SquareToCoords(sq));
53 this.epSquares = [ res ];
54 }
55 this.scanKingsRooks(fen);
56 // Extract subTurn from turn indicator: "w" (first move), or
57 // "w1" or "w2" white subturn 1 or 2, and same for black
58 const fullTurn = V.ParseFen(fen).turn;
59 this.startAtFirstMove = (fullTurn == "w");
60 this.turn = fullTurn[0];
61 this.subTurn = (fullTurn[1] || 1);
62 }
63
64 getPotentialPawnMoves([x,y])
65 {
66 const color = this.turn;
67 let moves = [];
68 const [sizeX,sizeY] = [V.size.x,V.size.y];
69 const shiftX = (color == "w" ? -1 : 1);
70 const firstRank = (color == 'w' ? sizeX-1 : 0);
71 const startRank = (color == "w" ? sizeX-2 : 1);
72 const lastRank = (color == "w" ? 0 : sizeX-1);
73 const finalPieces = x + shiftX == lastRank
74 ? [V.ROOK,V.KNIGHT,V.BISHOP,V.QUEEN]
75 : [V.PAWN];
76
77 // One square forward
78 if (this.board[x+shiftX][y] == V.EMPTY)
79 {
80 for (let piece of finalPieces)
81 {
82 moves.push(this.getBasicMove([x,y], [x+shiftX,y],
83 {c:color,p:piece}));
84 }
85 // Next condition because pawns on 1st rank can generally jump
86 if ([startRank,firstRank].includes(x)
87 && this.board[x+2*shiftX][y] == V.EMPTY)
88 {
89 // Two squares jump
90 moves.push(this.getBasicMove([x,y], [x+2*shiftX,y]));
91 }
92 }
93 // Captures
94 for (let shiftY of [-1,1])
95 {
96 if (y + shiftY >= 0 && y + shiftY < sizeY
97 && this.board[x+shiftX][y+shiftY] != V.EMPTY
98 && this.canTake([x,y], [x+shiftX,y+shiftY]))
99 {
100 for (let piece of finalPieces)
101 {
102 moves.push(this.getBasicMove([x,y], [x+shiftX,y+shiftY],
103 {c:color,p:piece}));
104 }
105 }
106 }
107
108 // En passant: always OK if subturn 1,
109 // OK on subturn 2 only if enPassant was played at subturn 1
110 // (and if there are two e.p. squares available).
111 const Lep = this.epSquares.length;
112 const epSquares = this.epSquares[Lep-1]; //always at least one element
113 let epSqs = [];
114 epSquares.forEach(sq => {
115 if (!!sq)
116 epSqs.push(sq);
117 });
118 if (epSqs.length == 0)
119 return moves;
120 const oppCol = this.getOppCol(color);
121 for (let sq of epSqs)
122 {
123 if (this.subTurn == 1 || (epSqs.length == 2 &&
124 // Was this en-passant capture already played at subturn 1 ?
125 // (Or maybe the opponent filled the en-passant square with a piece)
126 this.board[epSqs[0].x][epSqs[0].y] != V.EMPTY))
127 {
128 if (sq.x == x+shiftX && Math.abs(sq.y - y) == 1
129 // Add condition "enemy pawn must be present"
130 && this.getPiece(x,sq.y) == V.PAWN && this.getColor(x,sq.y) == oppCol)
131 {
132 let epMove = this.getBasicMove([x,y], [sq.x,sq.y]);
133 epMove.vanish.push({
134 x: x,
135 y: sq.y,
136 p: 'p',
137 c: oppCol
138 });
139 moves.push(epMove);
140 }
141 }
142 }
143
144 return moves;
145 }
146
147 play(move, ingame)
148 {
149 if (!!ingame)
150 {
151 move.notation = [this.getNotation(move), this.getLongNotation(move)];
152 // In this special case, we also need the "move color":
153 move.color = this.turn;
154 }
155 move.flags = JSON.stringify(this.aggregateFlags());
156 V.PlayOnBoard(this.board, move);
157 const epSq = this.getEpSquare(move);
158 if (this.startAtFirstMove && this.moves.length == 0)
159 {
160 this.turn = "b";
161 this.epSquares.push([epSq]);
162 }
163 // Does this move give check on subturn 1? If yes, skip subturn 2
164 else if (this.subTurn==1 && this.underCheck(this.getOppCol(this.turn)))
165 {
166 this.turn = this.getOppCol(this.turn);
167 this.epSquares.push([epSq]);
168 move.checkOnSubturn1 = true;
169 }
170 else
171 {
172 if (this.subTurn == 2)
173 {
174 this.turn = this.getOppCol(this.turn);
175 let lastEpsq = this.epSquares[this.epSquares.length-1];
176 lastEpsq.push(epSq);
177 }
178 else
179 this.epSquares.push([epSq]);
180 this.subTurn = 3 - this.subTurn;
181 }
182 this.moves.push(move);
183 this.updateVariables(move);
184 if (!!ingame)
185 move.hash = hex_md5(this.getFen());
186 }
187
188 undo(move)
189 {
190 this.disaggregateFlags(JSON.parse(move.flags));
191 V.UndoOnBoard(this.board, move);
192 if (this.startAtFirstMove && this.moves.length == 1)
193 {
194 this.turn = "w";
195 this.epSquares.pop();
196 }
197 else if (move.checkOnSubturn1)
198 {
199 this.turn = this.getOppCol(this.turn);
200 this.subTurn = 1;
201 this.epSquares.pop();
202 }
203 else
204 {
205 if (this.subTurn == 1)
206 {
207 this.turn = this.getOppCol(this.turn);
208 let lastEpsq = this.epSquares[this.epSquares.length-1];
209 lastEpsq.pop();
210 }
211 else
212 this.epSquares.pop();
213 this.subTurn = 3 - this.subTurn;
214 }
215 this.moves.pop();
216 this.unupdateVariables(move);
217 }
218
219 // NOTE: GenRandInitFen() is OK,
220 // since at first move turn indicator is just "w"
221
222 static get VALUES()
223 {
224 return {
225 'p': 1,
226 'r': 5,
227 'n': 3,
228 'b': 3,
229 'q': 7, //slightly less than in orthodox game
230 'k': 1000
231 };
232 }
233
234 // No alpha-beta here, just adapted min-max at depth 2(+1)
235 getComputerMove()
236 {
237 if (this.subTurn == 2)
238 return null; //TODO: imperfect interface setup
239
240 const maxeval = V.INFINITY;
241 const color = this.turn;
242 const oppCol = this.getOppCol(this.turn);
243
244 // Search best (half) move for opponent turn
245 const getBestMoveEval = () => {
246 const turnBefore = this.turn + this.subTurn;
247 let moves = this.getAllValidMoves();
248 if (moves.length == 0)
249 {
250 const score = this.checkGameEnd();
251 if (score == "1/2")
252 return 0;
253 return maxeval * (score == "1-0" ? 1 : -1);
254 }
255 let res = (oppCol == "w" ? -maxeval : maxeval);
256 for (let m of moves)
257 {
258 this.play(m);
259 // Now turn is oppCol,2 if m doesn't give check
260 // Otherwise it's color,1. In both cases the next test makes sense
261 if (!this.atLeastOneMove())
262 {
263 const score = this.checkGameEnd();
264 if (score == "1/2")
265 res = (oppCol == "w" ? Math.max(res, 0) : Math.min(res, 0));
266 else
267 {
268 // Found a mate
269 this.undo(m);
270 return maxeval * (score == "1-0" ? 1 : -1);
271 }
272 }
273 const evalPos = this.evalPosition();
274 res = (oppCol == "w" ? Math.max(res, evalPos) : Math.min(res, evalPos));
275 this.undo(m);
276 }
277 return res;
278 };
279
280 let moves11 = this.getAllValidMoves();
281 let doubleMoves = [];
282 // Rank moves using a min-max at depth 2
283 for (let i=0; i<moves11.length; i++)
284 {
285 this.play(moves11[i]);
286 if (this.turn != color)
287 {
288 // We gave check with last move: search the best opponent move
289 doubleMoves.push({moves:[moves11[i]], eval:getBestMoveEval()});
290 }
291 else
292 {
293 let moves12 = this.getAllValidMoves();
294 for (let j=0; j<moves12.length; j++)
295 {
296 this.play(moves12[j]);
297 doubleMoves.push({
298 moves:[moves11[i],moves12[j]],
299 eval:getBestMoveEval()});
300 this.undo(moves12[j]);
301 }
302 }
303 this.undo(moves11[i]);
304 }
305
306 doubleMoves.sort( (a,b) => {
307 return (color=="w" ? 1 : -1) * (b.eval - a.eval); });
308 let candidates = [0]; //indices of candidates moves
309 for (let i=1;
310 i<doubleMoves.length && doubleMoves[i].eval == doubleMoves[0].eval;
311 i++)
312 {
313 candidates.push(i);
314 }
315
316 const selected = doubleMoves[_.sample(candidates, 1)].moves;
317 if (selected.length == 1)
318 return selected[0];
319 return selected;
320 }
321
322 getPGN(mycolor, score, fenStart, mode)
323 {
324 let pgn = "";
325 pgn += '[Site "vchess.club"]\n';
326 const opponent = mode=="human" ? "Anonymous" : "Computer";
327 pgn += '[Variant "' + variant + '"]\n';
328 pgn += '[Date "' + getDate(new Date()) + '"]\n';
329 const whiteName = ["human","computer"].includes(mode)
330 ? (mycolor=='w'?'Myself':opponent)
331 : "analyze";
332 const blackName = ["human","computer"].includes(mode)
333 ? (mycolor=='b'?'Myself':opponent)
334 : "analyze";
335 pgn += '[White "' + whiteName + '"]\n';
336 pgn += '[Black "' + blackName + '"]\n';
337 pgn += '[FenStart "' + fenStart + '"]\n';
338 pgn += '[Fen "' + this.getFen() + '"]\n';
339 pgn += '[Result "' + score + '"]\n\n';
340
341 let counter = 1;
342 let i = 0;
343 while (i < this.moves.length)
344 {
345 pgn += (counter++) + ".";
346 for (let color of ["w","b"])
347 {
348 let move = "";
349 while (i < this.moves.length && this.moves[i].color == color)
350 move += this.moves[i++].notation[0] + ",";
351 move = move.slice(0,-1); //remove last comma
352 pgn += move + (i < this.moves.length-1 ? " " : "");
353 }
354 }
355 pgn += "\n\n";
356
357 // "Complete moves" PGN (helping in ambiguous cases)
358 counter = 1;
359 i = 0;
360 while (i < this.moves.length)
361 {
362 pgn += (counter++) + ".";
363 for (let color of ["w","b"])
364 {
365 let move = "";
366 while (i < this.moves.length && this.moves[i].color == color)
367 move += this.moves[i++].notation[1] + ",";
368 move = move.slice(0,-1); //remove last comma
369 pgn += move + (i < this.moves.length-1 ? " " : "");
370 }
371 }
372
373 return pgn + "\n";
374 }
375 }
376
377 const VariantRules = MarseilleRules;