Debug Knightrelay variant
[vchess.git] / client / src / variants / Marseille.js
1 import { ChessRules } from "@/base_rules";
2 import { randInt } from "@/utils/alea";
3
4 export const VariantRules = class MarseilleRules extends ChessRules {
5 static IsGoodEnpassant(enpassant) {
6 if (enpassant != "-") {
7 const squares = enpassant.split(",");
8 if (squares.length > 2) return false;
9 for (let sq of squares) {
10 const ep = V.SquareToCoords(sq);
11 if (isNaN(ep.x) || !V.OnBoard(ep)) return false;
12 }
13 }
14 return true;
15 }
16
17 getTurnFen() {
18 return this.turn + this.subTurn;
19 }
20
21 // There may be 2 enPassant squares (if 2 pawns jump 2 squares in same turn)
22 getEnpassantFen() {
23 const L = this.epSquares.length;
24 if (this.epSquares[L - 1].every(epsq => epsq === undefined)) return "-"; //no en-passant
25 let res = "";
26 this.epSquares[L - 1].forEach(epsq => {
27 if (epsq) res += V.CoordsToSquare(epsq) + ",";
28 });
29 return res.slice(0, -1); //remove last comma
30 }
31
32 setOtherVariables(fen) {
33 const parsedFen = V.ParseFen(fen);
34 this.setFlags(parsedFen.flags);
35 if (parsedFen.enpassant == "-") this.epSquares = [[undefined]];
36 else {
37 let res = [];
38 const squares = parsedFen.enpassant.split(",");
39 for (let sq of squares) res.push(V.SquareToCoords(sq));
40 this.epSquares = [res];
41 }
42 this.scanKingsRooks(fen);
43 // Extract subTurn from turn indicator: "w" (first move), or
44 // "w1" or "w2" white subturn 1 or 2, and same for black
45 const fullTurn = V.ParseFen(fen).turn;
46 this.turn = fullTurn[0];
47 this.subTurn = fullTurn[1] || 0; //"w0" = special code for first move in game
48 }
49
50 getPotentialPawnMoves([x, y]) {
51 const color = this.turn;
52 let moves = [];
53 const [sizeX, sizeY] = [V.size.x, V.size.y];
54 const shiftX = color == "w" ? -1 : 1;
55 const firstRank = color == "w" ? sizeX - 1 : 0;
56 const startRank = color == "w" ? sizeX - 2 : 1;
57 const lastRank = color == "w" ? 0 : sizeX - 1;
58 const finalPieces =
59 x + shiftX == lastRank ? [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN] : [V.PAWN];
60
61 // One square forward
62 if (this.board[x + shiftX][y] == V.EMPTY) {
63 for (let piece of finalPieces) {
64 moves.push(
65 this.getBasicMove([x, y], [x + shiftX, y], { c: color, p: piece })
66 );
67 }
68 // Next condition because pawns on 1st rank can generally jump
69 if (
70 [startRank, firstRank].includes(x) &&
71 this.board[x + 2 * shiftX][y] == V.EMPTY
72 ) {
73 // Two squares jump
74 moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y]));
75 }
76 }
77 // Captures
78 for (let shiftY of [-1, 1]) {
79 if (
80 y + shiftY >= 0 &&
81 y + shiftY < sizeY &&
82 this.board[x + shiftX][y + shiftY] != V.EMPTY &&
83 this.canTake([x, y], [x + shiftX, y + shiftY])
84 ) {
85 for (let piece of finalPieces) {
86 moves.push(
87 this.getBasicMove([x, y], [x + shiftX, y + shiftY], {
88 c: color,
89 p: piece
90 })
91 );
92 }
93 }
94 }
95
96 // En passant: always OK if subturn 1,
97 // OK on subturn 2 only if enPassant was played at subturn 1
98 // (and if there are two e.p. squares available).
99 const Lep = this.epSquares.length;
100 const epSquares = this.epSquares[Lep - 1]; //always at least one element
101 let epSqs = [];
102 epSquares.forEach(sq => {
103 if (sq) epSqs.push(sq);
104 });
105 if (epSqs.length == 0) return moves;
106 const oppCol = V.GetOppCol(color);
107 for (let sq of epSqs) {
108 if (
109 this.subTurn == 1 ||
110 (epSqs.length == 2 &&
111 // Was this en-passant capture already played at subturn 1 ?
112 // (Or maybe the opponent filled the en-passant square with a piece)
113 this.board[epSqs[0].x][epSqs[0].y] != V.EMPTY)
114 ) {
115 if (
116 sq.x == x + shiftX &&
117 Math.abs(sq.y - y) == 1 &&
118 // Add condition "enemy pawn must be present"
119 this.getPiece(x, sq.y) == V.PAWN &&
120 this.getColor(x, sq.y) == oppCol
121 ) {
122 let epMove = this.getBasicMove([x, y], [sq.x, sq.y]);
123 epMove.vanish.push({
124 x: x,
125 y: sq.y,
126 p: "p",
127 c: oppCol
128 });
129 moves.push(epMove);
130 }
131 }
132 }
133
134 return moves;
135 }
136
137 play(move) {
138 move.flags = JSON.stringify(this.aggregateFlags());
139 move.turn = this.turn + this.subTurn;
140 V.PlayOnBoard(this.board, move);
141 const epSq = this.getEpSquare(move);
142 if (this.subTurn == 0) {
143 //first move in game
144 this.turn = "b";
145 this.subTurn = 1;
146 this.epSquares.push([epSq]);
147 }
148 // Does this move give check on subturn 1? If yes, skip subturn 2
149 else if (this.subTurn == 1 && this.underCheck(V.GetOppCol(this.turn))) {
150 this.turn = V.GetOppCol(this.turn);
151 this.epSquares.push([epSq]);
152 move.checkOnSubturn1 = true;
153 } else {
154 if (this.subTurn == 2) {
155 this.turn = V.GetOppCol(this.turn);
156 let lastEpsq = this.epSquares[this.epSquares.length - 1];
157 lastEpsq.push(epSq);
158 } else this.epSquares.push([epSq]);
159 this.subTurn = 3 - this.subTurn;
160 }
161 this.updateVariables(move);
162 }
163
164 undo(move) {
165 this.disaggregateFlags(JSON.parse(move.flags));
166 V.UndoOnBoard(this.board, move);
167 if (move.turn[1] == "0" || move.checkOnSubturn1 || this.subTurn == 2)
168 this.epSquares.pop();
169 //this.subTurn == 1
170 else {
171 let lastEpsq = this.epSquares[this.epSquares.length - 1];
172 lastEpsq.pop();
173 }
174 this.turn = move.turn[0];
175 this.subTurn = parseInt(move.turn[1]);
176 this.unupdateVariables(move);
177 }
178
179 // NOTE: GenRandInitFen() is OK,
180 // since at first move turn indicator is just "w"
181
182 static get VALUES() {
183 return {
184 p: 1,
185 r: 5,
186 n: 3,
187 b: 3,
188 q: 7, //slightly less than in orthodox game
189 k: 1000
190 };
191 }
192
193 // No alpha-beta here, just adapted min-max at depth 2(+1)
194 getComputerMove() {
195 if (this.subTurn == 2) return null; //TODO: imperfect interface setup
196
197 const maxeval = V.INFINITY;
198 const color = this.turn;
199 const oppCol = V.GetOppCol(this.turn);
200
201 // Search best (half) move for opponent turn
202 const getBestMoveEval = () => {
203 let score = this.getCurrentScore();
204 if (score != "*") {
205 if (score == "1/2") return 0;
206 return maxeval * (score == "1-0" ? 1 : -1);
207 }
208 let moves = this.getAllValidMoves();
209 let res = oppCol == "w" ? -maxeval : maxeval;
210 for (let m of moves) {
211 this.play(m);
212 score = this.getCurrentScore();
213 // Now turn is oppCol,2 if m doesn't give check
214 // Otherwise it's color,1. In both cases the next test makes sense
215 if (score != "*") {
216 if (score == "1/2")
217 res = oppCol == "w" ? Math.max(res, 0) : Math.min(res, 0);
218 else {
219 // Found a mate
220 this.undo(m);
221 return maxeval * (score == "1-0" ? 1 : -1);
222 }
223 }
224 const evalPos = this.evalPosition();
225 res = oppCol == "w" ? Math.max(res, evalPos) : Math.min(res, evalPos);
226 this.undo(m);
227 }
228 return res;
229 };
230
231 let moves11 = this.getAllValidMoves();
232 let doubleMoves = [];
233 // Rank moves using a min-max at depth 2
234 for (let i = 0; i < moves11.length; i++) {
235 this.play(moves11[i]);
236 if (this.turn != color) {
237 // We gave check with last move: search the best opponent move
238 doubleMoves.push({ moves: [moves11[i]], eval: getBestMoveEval() });
239 } else {
240 let moves12 = this.getAllValidMoves();
241 for (let j = 0; j < moves12.length; j++) {
242 this.play(moves12[j]);
243 doubleMoves.push({
244 moves: [moves11[i], moves12[j]],
245 eval: getBestMoveEval()
246 });
247 this.undo(moves12[j]);
248 }
249 }
250 this.undo(moves11[i]);
251 }
252
253 doubleMoves.sort((a, b) => {
254 return (color == "w" ? 1 : -1) * (b.eval - a.eval);
255 });
256 let candidates = [0]; //indices of candidates moves
257 for (
258 let i = 1;
259 i < doubleMoves.length && doubleMoves[i].eval == doubleMoves[0].eval;
260 i++
261 ) {
262 candidates.push(i);
263 }
264
265 const selected = doubleMoves[randInt(candidates.length)].moves;
266 if (selected.length == 1) return selected[0];
267 return selected;
268 }
269 };