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