Fix Pocketknight computer play
[vchess.git] / client / src / variants / Pocketknight.js
1 import { ChessRules, Move, PiPo } from "@/base_rules";
2 import { randInt } from "@/utils/alea";
3
4 export class PocketknightRules 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 static IsGoodFlags(flags) {
12 // 4 for castle + 2 for knights
13 return !!flags.match(/^[a-z]{4,4}[01]{2,2}$/);
14 }
15
16 setFlags(fenflags) {
17 super.setFlags(fenflags); //castleFlags
18 this.knightFlags = fenflags.substr(4).split("").map(e => e == "1");
19 }
20
21 aggregateFlags() {
22 return [this.castleFlags, this.knightFlags];
23 }
24
25 disaggregateFlags(flags) {
26 this.castleFlags = flags[0];
27 this.knightFlags = flags[1];
28 }
29
30 setOtherVariables(fen) {
31 super.setOtherVariables(fen);
32 this.subTurn = 1;
33 }
34
35 static GenRandInitFen(randomness) {
36 // Add 2 knight flags
37 return ChessRules.GenRandInitFen(randomness)
38 .slice(0, -2) + "11 -";
39 }
40
41 getFlagsFen() {
42 return (
43 super.getFlagsFen() + this.knightFlags.map(e => e ? "1" : "0").join("")
44 );
45 }
46
47 getPotentialMovesFrom([x, y]) {
48 if (this.subTurn == 1) {
49 let moves = super.getPotentialMovesFrom([x, y]);
50 // If flag allow it, add "king capture"
51 if (
52 this.knightFlags[this.turn == 'w' ? 0 : 1] &&
53 this.getPiece(x, y) == V.KING
54 ) {
55 const kp = this.kingPos[V.GetOppCol(this.turn)];
56 moves.push(
57 new Move({
58 appear: [],
59 vanish: [],
60 start: { x: x, y: y },
61 end: { x: kp[0], y: kp[1] }
62 })
63 );
64 }
65 return moves;
66 }
67 // subTurn == 2: a move is a click, not handled here
68 return [];
69 }
70
71 filterValid(moves) {
72 if (this.subTurn == 2) return super.filterValid(moves);
73 const color = this.turn;
74 return moves.filter(m => {
75 this.play(m);
76 let res = false;
77 if (m.appear.length > 0)
78 // Standard check:
79 res = !this.underCheck(color);
80 else {
81 // "Capture king": find landing square not resulting in check
82 outerLoop: for (let i=0; i<8; i++) {
83 for (let j=0; j<8; j++) {
84 if (this.board[i][j] == V.EMPTY) {
85 const tMove = new Move({
86 appear: [
87 new PiPo({
88 x: i,
89 y: j,
90 c: color,
91 p: V.KNIGHT
92 })
93 ],
94 vanish: [],
95 start: { x: -1, y: -1 }
96 });
97 this.play(tMove);
98 const moveOk = !this.underCheck(color);
99 this.undo(tMove);
100 if (moveOk) {
101 res = true;
102 break outerLoop;
103 }
104 }
105 }
106 }
107 }
108 this.undo(m);
109 return res;
110 });
111 }
112
113 getAllValidMoves() {
114 if (this.subTurn == 1) return super.getAllValidMoves();
115 // Subturn == 2: only knight landings
116 let moves = [];
117 const color = this.turn;
118 for (let i=0; i<8; i++) {
119 for (let j=0; j<8; j++) {
120 if (this.board[i][j] == V.EMPTY) {
121 const tMove = new Move({
122 appear: [
123 new PiPo({
124 x: i,
125 y: j,
126 c: color,
127 p: V.KNIGHT
128 })
129 ],
130 vanish: [],
131 start: { x: -1, y: -1 }
132 });
133 this.play(tMove);
134 const moveOk = !this.underCheck(color);
135 this.undo(tMove);
136 if (moveOk) moves.push(tMove);
137 }
138 }
139 }
140 return moves;
141 }
142
143 doClick(square) {
144 if (isNaN(square[0])) return null;
145 // If subTurn == 2 && square is empty && !underCheck, then drop
146 if (this.subTurn == 2 && this.board[square[0]][square[1]] == V.EMPTY) {
147 const color = this.turn;
148 const tMove = new Move({
149 appear: [
150 new PiPo({
151 x: square[0],
152 y: square[1],
153 c: color,
154 p: V.KNIGHT
155 })
156 ],
157 vanish: [],
158 start: { x: -1, y: -1 }
159 });
160 this.play(tMove);
161 const moveOk = !this.underCheck(color);
162 this.undo(tMove);
163 if (moveOk) return tMove;
164 }
165 return null;
166 }
167
168 play(move) {
169 move.flags = JSON.stringify(this.aggregateFlags());
170 if (move.appear.length > 0) {
171 // Usual case or knight landing
172 if (move.vanish.length > 0) this.epSquares.push(this.getEpSquare(move));
173 else this.subTurn = 1;
174 this.turn = V.GetOppCol(this.turn);
175 this.movesCount++;
176 V.PlayOnBoard(this.board, move);
177 if (move.vanish.length > 0) this.postPlay(move);
178 }
179 else {
180 // "king capture"
181 this.subTurn = 2;
182 this.knightFlags[this.turn == 'w' ? 0 : 1] = false;
183 }
184 }
185
186 undo(move) {
187 this.disaggregateFlags(JSON.parse(move.flags));
188 if (move.appear.length > 0) {
189 if (move.vanish.length > 0) this.epSquares.pop();
190 else this.subTurn = 2;
191 this.turn = V.GetOppCol(this.turn);
192 this.movesCount--;
193 V.UndoOnBoard(this.board, move);
194 if (move.vanish.length > 0) this.postUndo(move);
195 }
196 else this.subTurn = 1;
197 }
198
199 getComputerMove() {
200 let moves = this.getAllValidMoves();
201 if (moves.length == 0) return null;
202 const maxeval = V.INFINITY;
203 const color = this.turn;
204 const oppCol = V.GetOppCol(color);
205 const getOppEval = () => {
206 let evalOpp = this.evalPosition();
207 this.getAllValidMoves().forEach(m => {
208 // Do not consider knight landings here
209 if (m.appear.length > 0) {
210 this.play(m);
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 == "*") mvEval = this.evalPosition();
216 if (
217 (oppCol == 'w' && mvEval > evalOpp) ||
218 (oppCol == 'b' && mvEval < evalOpp)
219 ) {
220 evalOpp = mvEval;
221 }
222 this.undo(m);
223 }
224 });
225 return evalOpp;
226 };
227 // Custom "search" at depth 2
228 moves.forEach(m => {
229 this.play(m);
230 m.eval = (color == "w" ? -1 : 1) * maxeval;
231 if (m.appear.length == 0) {
232 const moves2 = this.getAllValidMoves();
233 m.next = moves2[0];
234 moves2.forEach(m2 => {
235 this.play(m2);
236 const score = this.getCurrentScore();
237 let mvEval = 0;
238 if (["1-0", "0-1"].includes(score))
239 mvEval = (score == "1-0" ? 1 : -1) * maxeval;
240 else if (score == "*")
241 // Add small fluctuations to avoid dropping pieces always on the
242 // first available square.
243 mvEval = getOppEval() + 0.05 - Math.random() / 10;
244 if (
245 (color == 'w' && mvEval > m.eval) ||
246 (color == 'b' && mvEval < m.eval)
247 ) {
248 m.eval = mvEval;
249 m.next = m2;
250 }
251 this.undo(m2);
252 });
253 }
254 else {
255 const score = this.getCurrentScore();
256 if (score != "1/2") {
257 if (score != "*") m.eval = (score == "1-0" ? 1 : -1) * maxeval;
258 else m.eval = getOppEval();
259 }
260 }
261 this.undo(m);
262 });
263 moves.sort((a, b) => {
264 return (color == "w" ? 1 : -1) * (b.eval - a.eval);
265 });
266 let candidates = [0];
267 for (let i = 1; i < moves.length && moves[i].eval == moves[0].eval; i++)
268 candidates.push(i);
269 const mIdx = candidates[randInt(candidates.length)];
270 if (!moves[mIdx].next) return moves[mIdx];
271 const move2 = moves[mIdx].next;
272 delete moves[mIdx]["next"];
273 return [moves[mIdx], move2];
274 }
275
276 getNotation(move) {
277 if (move.vanish.length > 0)
278 return super.getNotation(move);
279 if (move.appear.length == 0)
280 // "king capture"
281 return "-";
282 // Knight landing:
283 return "N@" + V.CoordsToSquare(move.end);
284 }
285 };