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