Started code review + some fixes (unfinished)
[vchess.git] / client / src / variants / Marseille.js
CommitLineData
829f6574 1import { ChessRules } from "@/base_rules";
5fde3a01 2import { randInt } from "@/utils/alea";
829f6574 3
6808d7a1
BA
4export const VariantRules = class MarseilleRules extends ChessRules {
5 static IsGoodEnpassant(enpassant) {
6 if (enpassant != "-") {
dac39588 7 const squares = enpassant.split(",");
6808d7a1
BA
8 if (squares.length > 2) return false;
9 for (let sq of squares) {
dac39588 10 const ep = V.SquareToCoords(sq);
6808d7a1 11 if (isNaN(ep.x) || !V.OnBoard(ep)) return false;
dac39588
BA
12 }
13 }
14 return true;
15 }
6e62b1c7 16
6808d7a1 17 getTurnFen() {
dac39588
BA
18 return this.turn + this.subTurn;
19 }
6e62b1c7 20
dac39588 21 // There may be 2 enPassant squares (if 2 pawns jump 2 squares in same turn)
6808d7a1 22 getEnpassantFen() {
dac39588 23 const L = this.epSquares.length;
6808d7a1 24 if (this.epSquares[L - 1].every(epsq => epsq === undefined)) return "-"; //no en-passant
dac39588 25 let res = "";
6808d7a1
BA
26 this.epSquares[L - 1].forEach(epsq => {
27 if (epsq) res += V.CoordsToSquare(epsq) + ",";
dac39588 28 });
6808d7a1 29 return res.slice(0, -1); //remove last comma
dac39588 30 }
6e62b1c7 31
6808d7a1 32 setOtherVariables(fen) {
dac39588
BA
33 const parsedFen = V.ParseFen(fen);
34 this.setFlags(parsedFen.flags);
6808d7a1
BA
35 if (parsedFen.enpassant == "-") this.epSquares = [[undefined]];
36 else {
dac39588
BA
37 let res = [];
38 const squares = parsedFen.enpassant.split(",");
6808d7a1
BA
39 for (let sq of squares) res.push(V.SquareToCoords(sq));
40 this.epSquares = [res];
dac39588
BA
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];
6808d7a1 47 this.subTurn = fullTurn[1] || 0; //"w0" = special code for first move in game
dac39588 48 }
6e62b1c7 49
6808d7a1 50 getPotentialPawnMoves([x, y]) {
dac39588
BA
51 const color = this.turn;
52 let moves = [];
6808d7a1
BA
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];
6e62b1c7 60
dac39588 61 // One square forward
6808d7a1
BA
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 );
dac39588
BA
67 }
68 // Next condition because pawns on 1st rank can generally jump
6808d7a1
BA
69 if (
70 [startRank, firstRank].includes(x) &&
71 this.board[x + 2 * shiftX][y] == V.EMPTY
72 ) {
dac39588 73 // Two squares jump
6808d7a1 74 moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y]));
dac39588
BA
75 }
76 }
77 // Captures
6808d7a1
BA
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 );
dac39588
BA
92 }
93 }
94 }
6e62b1c7 95
dac39588
BA
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;
6808d7a1 100 const epSquares = this.epSquares[Lep - 1]; //always at least one element
dac39588
BA
101 let epSqs = [];
102 epSquares.forEach(sq => {
6808d7a1 103 if (sq) epSqs.push(sq);
dac39588 104 });
6808d7a1 105 if (epSqs.length == 0) return moves;
dac39588 106 const oppCol = V.GetOppCol(color);
6808d7a1
BA
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 &&
dac39588 118 // Add condition "enemy pawn must be present"
6808d7a1
BA
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]);
dac39588
BA
123 epMove.vanish.push({
124 x: x,
125 y: sq.y,
6808d7a1 126 p: "p",
dac39588
BA
127 c: oppCol
128 });
129 moves.push(epMove);
130 }
131 }
132 }
6e62b1c7 133
dac39588
BA
134 return moves;
135 }
6e62b1c7 136
6808d7a1 137 play(move) {
dac39588
BA
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);
6808d7a1
BA
142 if (this.subTurn == 0) {
143 //first move in game
dac39588 144 this.turn = "b";
829f6574 145 this.subTurn = 1;
dac39588
BA
146 this.epSquares.push([epSq]);
147 }
148 // Does this move give check on subturn 1? If yes, skip subturn 2
6808d7a1 149 else if (this.subTurn == 1 && this.underCheck(V.GetOppCol(this.turn))) {
dac39588
BA
150 this.turn = V.GetOppCol(this.turn);
151 this.epSquares.push([epSq]);
152 move.checkOnSubturn1 = true;
6808d7a1
BA
153 } else {
154 if (this.subTurn == 2) {
dac39588 155 this.turn = V.GetOppCol(this.turn);
6808d7a1 156 let lastEpsq = this.epSquares[this.epSquares.length - 1];
dac39588 157 lastEpsq.push(epSq);
6808d7a1 158 } else this.epSquares.push([epSq]);
dac39588
BA
159 this.subTurn = 3 - this.subTurn;
160 }
161 this.updateVariables(move);
162 }
6e62b1c7 163
6808d7a1 164 undo(move) {
dac39588
BA
165 this.disaggregateFlags(JSON.parse(move.flags));
166 V.UndoOnBoard(this.board, move);
6808d7a1 167 if (move.turn[1] == "0" || move.checkOnSubturn1 || this.subTurn == 2)
dac39588 168 this.epSquares.pop();
6808d7a1
BA
169 //this.subTurn == 1
170 else {
171 let lastEpsq = this.epSquares[this.epSquares.length - 1];
dac39588
BA
172 lastEpsq.pop();
173 }
174 this.turn = move.turn[0];
175 this.subTurn = parseInt(move.turn[1]);
176 this.unupdateVariables(move);
177 }
6e62b1c7 178
dac39588
BA
179 // NOTE: GenRandInitFen() is OK,
180 // since at first move turn indicator is just "w"
6e62b1c7 181
6808d7a1 182 static get VALUES() {
dac39588 183 return {
6808d7a1
BA
184 p: 1,
185 r: 5,
186 n: 3,
187 b: 3,
188 q: 7, //slightly less than in orthodox game
189 k: 1000
dac39588
BA
190 };
191 }
0596f5e7 192
dac39588 193 // No alpha-beta here, just adapted min-max at depth 2(+1)
6808d7a1
BA
194 getComputerMove() {
195 if (this.subTurn == 2) return null; //TODO: imperfect interface setup
78bab51e 196
dac39588
BA
197 const maxeval = V.INFINITY;
198 const color = this.turn;
199 const oppCol = V.GetOppCol(this.turn);
51882959 200
dac39588
BA
201 // Search best (half) move for opponent turn
202 const getBestMoveEval = () => {
dac39588 203 let score = this.getCurrentScore();
6808d7a1
BA
204 if (score != "*") {
205 if (score == "1/2") return 0;
dac39588
BA
206 return maxeval * (score == "1-0" ? 1 : -1);
207 }
208 let moves = this.getAllValidMoves();
6808d7a1
BA
209 let res = oppCol == "w" ? -maxeval : maxeval;
210 for (let m of moves) {
dac39588
BA
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
6808d7a1 215 if (score != "*") {
dac39588 216 if (score == "1/2")
6808d7a1
BA
217 res = oppCol == "w" ? Math.max(res, 0) : Math.min(res, 0);
218 else {
dac39588
BA
219 // Found a mate
220 this.undo(m);
221 return maxeval * (score == "1-0" ? 1 : -1);
222 }
223 }
224 const evalPos = this.evalPosition();
6808d7a1 225 res = oppCol == "w" ? Math.max(res, evalPos) : Math.min(res, evalPos);
dac39588
BA
226 this.undo(m);
227 }
228 return res;
229 };
6e62b1c7 230
dac39588
BA
231 let moves11 = this.getAllValidMoves();
232 let doubleMoves = [];
233 // Rank moves using a min-max at depth 2
6808d7a1 234 for (let i = 0; i < moves11.length; i++) {
dac39588 235 this.play(moves11[i]);
6808d7a1 236 if (this.turn != color) {
dac39588 237 // We gave check with last move: search the best opponent move
6808d7a1
BA
238 doubleMoves.push({ moves: [moves11[i]], eval: getBestMoveEval() });
239 } else {
dac39588 240 let moves12 = this.getAllValidMoves();
6808d7a1 241 for (let j = 0; j < moves12.length; j++) {
dac39588
BA
242 this.play(moves12[j]);
243 doubleMoves.push({
6808d7a1
BA
244 moves: [moves11[i], moves12[j]],
245 eval: getBestMoveEval()
246 });
dac39588
BA
247 this.undo(moves12[j]);
248 }
249 }
250 this.undo(moves11[i]);
251 }
6e62b1c7 252
6808d7a1
BA
253 doubleMoves.sort((a, b) => {
254 return (color == "w" ? 1 : -1) * (b.eval - a.eval);
255 });
dac39588 256 let candidates = [0]; //indices of candidates moves
6808d7a1
BA
257 for (
258 let i = 1;
259 i < doubleMoves.length && doubleMoves[i].eval == doubleMoves[0].eval;
260 i++
261 ) {
dac39588
BA
262 candidates.push(i);
263 }
78bab51e 264
dac39588 265 const selected = doubleMoves[randInt(candidates.length)].moves;
6808d7a1 266 if (selected.length == 1) return selected[0];
dac39588
BA
267 return selected;
268 }
6808d7a1 269};