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