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