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