A few fixes (for updateCastleFlags()) + add Madhouse and Pocketknight variants
[vchess.git] / client / src / variants / Madhouse.js
1 import { ChessRules, Move, PiPo } from "@/base_rules";
2 import { randInt } from "@/utils/alea";
3
4 export class MadhouseRules extends ChessRules {
5 hoverHighlight(x, y) {
6 // Testing move validity results in an infinite update loop.
7 // TODO: find a way to test validity anyway.
8 return (this.subTurn == 2 && this.board[x][y] == V.EMPTY);
9 }
10
11 setOtherVariables(fen) {
12 super.setOtherVariables(fen);
13 this.subTurn = 1;
14 this.firstMove = [];
15 }
16
17 getPotentialMovesFrom([x, y]) {
18 if (this.subTurn == 1) return super.getPotentialMovesFrom([x, y]);
19 // subTurn == 2: a move is a click, not handled here
20 return [];
21 }
22
23 filterValid(moves) {
24 if (this.subTurn == 2) return super.filterValid(moves);
25 const color = this.turn;
26 return moves.filter(m => {
27 this.play(m);
28 let res = false;
29 if (m.vanish.length == 1 || m.appear.length == 2)
30 // Standard check:
31 res = !this.underCheck(color);
32 else {
33 // Capture: find landing square not resulting in check
34 const boundary = (m.vanish[1].p != V.PAWN ? [0, 7] : [1, 6]);
35 const sqColor =
36 m.vanish[1].p == V.BISHOP
37 ? (m.vanish[1].x + m.vanish[1].y) % 2
38 : null;
39 outerLoop: for (let i = boundary[0]; i <= boundary[1]; i++) {
40 for (let j=0; j<8; j++) {
41 if (
42 this.board[i][j] == V.EMPTY &&
43 (!sqColor || (i + j) % 2 == sqColor)
44 ) {
45 const tMove = new Move({
46 appear: [
47 new PiPo({
48 x: i,
49 y: j,
50 c: m.vanish[1].c,
51 p: m.vanish[1].p
52 })
53 ],
54 vanish: [],
55 start: { x: -1, y: -1 }
56 });
57 this.play(tMove);
58 const moveOk = !this.underCheck(color);
59 this.undo(tMove);
60 if (moveOk) {
61 res = true;
62 break outerLoop;
63 }
64 }
65 }
66 }
67 }
68 this.undo(m);
69 return res;
70 });
71 }
72
73 getAllValidMoves() {
74 if (this.subTurn == 1) return super.getAllValidMoves();
75 // Subturn == 2: only replacements
76 let moves = [];
77 const L = this.firstMove.length;
78 const fm = this.firstMove[L - 1];
79 const color = this.turn;
80 const oppCol = V.GetOppCol(color);
81 const boundary = (fm.vanish[1].p != V.PAWN ? [0, 7] : [1, 6]);
82 const sqColor =
83 fm.vanish[1].p == V.BISHOP
84 ? (fm.vanish[1].x + fm.vanish[1].y) % 2
85 : null;
86 for (let i = boundary[0]; i < boundary[1]; i++) {
87 for (let j=0; j<8; j++) {
88 if (
89 this.board[i][j] == V.EMPTY &&
90 (!sqColor || (i + j) % 2 == sqColor)
91 ) {
92 const tMove = new Move({
93 appear: [
94 new PiPo({
95 x: i,
96 y: j,
97 c: oppCol,
98 p: fm.vanish[1].p
99 })
100 ],
101 vanish: [],
102 start: { x: -1, y: -1 }
103 });
104 this.play(tMove);
105 const moveOk = !this.underCheck(color);
106 this.undo(tMove);
107 if (moveOk) moves.push(tMove);
108 }
109 }
110 }
111 return moves;
112 }
113
114 doClick(square) {
115 if (isNaN(square[0])) return null;
116 // If subTurn == 2 && square is empty && !underCheck, then replacement
117 if (this.subTurn == 2 && this.board[square[0]][square[1]] == V.EMPTY) {
118 const L = this.firstMove.length;
119 const fm = this.firstMove[L - 1];
120 const color = this.turn;
121 const oppCol = V.GetOppCol(color);
122 if (
123 (fm.vanish[1].p == V.PAWN && [0, 7].includes(square[0])) ||
124 (
125 fm.vanish[1].p == V.BISHOP &&
126 (square[0] + square[1] + fm.vanish[1].x + fm.vanish[1].y) % 2 != 0
127 )
128 ) {
129 // Pawns cannot be replaced on first or last rank,
130 // bishops must be replaced on same square color.
131 return null;
132 }
133 const tMove = new Move({
134 appear: [
135 new PiPo({
136 x: square[0],
137 y: square[1],
138 c: oppCol,
139 p: fm.vanish[1].p
140 })
141 ],
142 vanish: [],
143 start: { x: -1, y: -1 }
144 });
145 this.play(tMove);
146 const moveOk = !this.underCheck(color);
147 this.undo(tMove);
148 if (moveOk) return tMove;
149 }
150 return null;
151 }
152
153 play(move) {
154 move.flags = JSON.stringify(this.aggregateFlags());
155 if (move.vanish.length > 0) {
156 this.epSquares.push(this.getEpSquare(move));
157 this.firstMove.push(move);
158 }
159 V.PlayOnBoard(this.board, move);
160 if (
161 this.subTurn == 2 ||
162 move.vanish.length == 1 ||
163 move.appear.length == 2
164 ) {
165 this.turn = V.GetOppCol(this.turn);
166 this.subTurn = 1;
167 this.movesCount++;
168 }
169 else this.subTurn = 2;
170 if (move.vanish.length > 0) this.postPlay(move);
171 }
172
173 postPlay(move) {
174 if (move.appear[0].p == V.KING)
175 this.kingPos[move.appear[0].c] = [move.appear[0].x, move.appear[0].y];
176 this.updateCastleFlags(move, move.appear[0].p, move.appear[0].c);
177 }
178
179 undo(move) {
180 this.disaggregateFlags(JSON.parse(move.flags));
181 if (move.vanish.length > 0) {
182 this.epSquares.pop();
183 this.firstMove.pop();
184 }
185 V.UndoOnBoard(this.board, move);
186 if (this.subTurn == 2) this.subTurn = 1;
187 else {
188 this.turn = V.GetOppCol(this.turn);
189 this.movesCount--;
190 this.subTurn = (move.vanish.length > 0 ? 1 : 2);
191 }
192 if (move.vanish.length > 0) super.postUndo(move);
193 }
194
195 getComputerMove() {
196 let moves = this.getAllValidMoves();
197 if (moves.length == 0) return null;
198 // Custom "search" at depth 1 (for now. TODO?)
199 const maxeval = V.INFINITY;
200 const color = this.turn;
201 const initEval = this.evalPosition();
202 moves.forEach(m => {
203 this.play(m);
204 m.eval = (color == "w" ? -1 : 1) * maxeval;
205 if (m.vanish.length == 2 && m.appear.length == 1) {
206 const moves2 = this.getAllValidMoves();
207 m.next = moves2[0];
208 moves2.forEach(m2 => {
209 this.play(m2);
210 const score = this.getCurrentScore();
211 let mvEval = 0;
212 if (["1-0", "0-1"].includes(score))
213 mvEval = (score == "1-0" ? 1 : -1) * maxeval;
214 else if (score == "*")
215 // Add small fluctuations to avoid dropping pieces always on the
216 // first square available.
217 mvEval = initEval + 0.05 - Math.random() / 10;
218 if (
219 (color == 'w' && mvEval > m.eval) ||
220 (color == 'b' && mvEval < m.eval)
221 ) {
222 m.eval = mvEval;
223 m.next = m2;
224 }
225 this.undo(m2);
226 });
227 }
228 else {
229 const score = this.getCurrentScore();
230 if (score != "1/2") {
231 if (score != "*") m.eval = (score == "1-0" ? 1 : -1) * maxeval;
232 else m.eval = this.evalPosition();
233 }
234 }
235 this.undo(m);
236 });
237 moves.sort((a, b) => {
238 return (color == "w" ? 1 : -1) * (b.eval - a.eval);
239 });
240 let candidates = [0];
241 for (let i = 1; i < moves.length && moves[i].eval == moves[0].eval; i++)
242 candidates.push(i);
243 const mIdx = candidates[randInt(candidates.length)];
244 if (!moves[mIdx].next) return moves[mIdx];
245 const move2 = moves[mIdx].next;
246 delete moves[mIdx]["next"];
247 return [moves[mIdx], move2];
248 }
249
250 getNotation(move) {
251 if (move.vanish.length > 0) return super.getNotation(move);
252 // Replacement:
253 const piece =
254 move.appear[0].p != V.PAWN ? move.appear[0].p.toUpperCase() : "";
255 return piece + "@" + V.CoordsToSquare(move.end);
256 }
257 };