Fix Shinobi
[vchess.git] / client / src / variants / Shinobi.js
1 import { ChessRules, PiPo, Move } from "@/base_rules";
2
3 export class ShinobiRules extends ChessRules {
4
5 static get LoseOnRepetition() {
6 return true;
7 }
8
9 static get CAPTAIN() {
10 return 'c';
11 }
12 static get NINJA() {
13 return 'j';
14 }
15 static get SAMURAI() {
16 return 's';
17 }
18 static get MONK() {
19 return 'm';
20 }
21 static get HORSE() {
22 return 'h';
23 }
24 static get LANCE() {
25 return 'l';
26 }
27
28 static IsGoodFlags(flags) {
29 // Only black can castle
30 return !!flags.match(/^[a-z]{2,2}$/);
31 }
32
33 static get PIECES() {
34 return (
35 ChessRules.PIECES
36 .concat([V.CAPTAIN, V.NINJA, V.SAMURAI, V.MONK, V.HORSE, V.LANCE])
37 );
38 }
39
40 getPpath(b) {
41 if (b[0] == 'b' && b[1] != 'c') return b;
42 return "Shinobi/" + b;
43 }
44
45 getReservePpath(index, color) {
46 return "Shinobi/" + color + V.RESERVE_PIECES[index];
47 }
48
49 getFlagsFen() {
50 return this.castleFlags['b'].map(V.CoordToColumn).join("");
51 }
52
53 setFlags(fenflags) {
54 this.castleFlags = { 'b': [-1, -1] };
55 for (let i = 0; i < 2; i++)
56 this.castleFlags['b'][i] = V.ColumnToCoord(fenflags.charAt(i));
57 }
58
59 static IsGoodFen(fen) {
60 if (!ChessRules.IsGoodFen(fen)) return false;
61 const fenParsed = V.ParseFen(fen);
62 // 5) Check reserve
63 if (!fenParsed.reserve || !fenParsed.reserve.match(/^[0-9]{5,5}$/))
64 return false;
65 return true;
66 }
67
68 static ParseFen(fen) {
69 const fenParts = fen.split(" ");
70 return Object.assign(
71 ChessRules.ParseFen(fen),
72 { reserve: fenParts[5] }
73 );
74 }
75
76 // In hand initially: ninja, samurai + 2 x monk, horse, lance.
77 static GenRandInitFen(randomness) {
78 const baseFen = ChessRules.GenRandInitFen(Math.min(randomness, 1));
79 return (
80 baseFen.substr(0, 35) + "3CK3 " +
81 "w 0 " + baseFen.substr(48, 2) + " - 11222"
82 );
83 }
84
85 getFen() {
86 return super.getFen() + " " + this.getReserveFen();
87 }
88
89 getFenForRepeat() {
90 return super.getFenForRepeat() + "_" + this.getReserveFen();
91 }
92
93 getReserveFen() {
94 // TODO: can simplify other drops variants with this code:
95 return Object.values(this.reserve['w']).join("");
96 }
97
98 setOtherVariables(fen) {
99 super.setOtherVariables(fen);
100 const reserve =
101 V.ParseFen(fen).reserve.split("").map(x => parseInt(x, 10));
102 this.reserve = {
103 w: {
104 [V.NINJA]: reserve[0],
105 [V.SAMURAI]: reserve[1],
106 [V.MONK]: reserve[2],
107 [V.HORSE]: reserve[3],
108 [V.LANCE]: reserve[4]
109 }
110 };
111 }
112
113 getColor(i, j) {
114 if (i >= V.size.x) return 'w';
115 return this.board[i][j].charAt(0);
116 }
117
118 getPiece(i, j) {
119 if (i >= V.size.x) return V.RESERVE_PIECES[j];
120 return this.board[i][j].charAt(1);
121 }
122
123 static get RESERVE_PIECES() {
124 return [V.NINJA, V.SAMURAI, V.MONK, V.HORSE, V.LANCE];
125 }
126
127 getReserveMoves([x, y]) {
128 // color == 'w', no drops for black.
129 const p = V.RESERVE_PIECES[y];
130 if (this.reserve['w'][p] == 0) return [];
131 let moves = [];
132 for (let i of [4, 5, 6, 7]) {
133 for (let j = 0; j < V.size.y; j++) {
134 if (this.board[i][j] == V.EMPTY) {
135 let mv = new Move({
136 appear: [
137 new PiPo({
138 x: i,
139 y: j,
140 c: 'w',
141 p: p
142 })
143 ],
144 vanish: [],
145 start: { x: x, y: y },
146 end: { x: i, y: j }
147 });
148 moves.push(mv);
149 }
150 }
151 }
152 return moves;
153 }
154
155 static get MapUnpromoted() {
156 return {
157 m: 'b',
158 h: 'n',
159 l: 'r',
160 p: 'c'
161 };
162 }
163
164 getPotentialMovesFrom([x, y]) {
165 if (x >= V.size.x) {
166 // Reserves, outside of board: x == sizeX(+1)
167 if (this.turn == 'b') return [];
168 return this.getReserveMoves([x, y]);
169 }
170 // Standard moves
171 const piece = this.getPiece(x, y);
172 const sq = [x, y];
173 if ([V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN].includes(piece))
174 return super.getPotentialMovesFrom(sq);
175 switch (piece) {
176 case V.KING: return this.getPotentialKingMoves(sq);
177 case V.CAPTAIN: return this.getPotentialCaptainMoves(sq);
178 case V.NINJA: return this.getPotentialNinjaMoves(sq);
179 case V.SAMURAI: return this.getPotentialSamuraiMoves(sq);
180 }
181 let moves = [];
182 switch (piece) {
183 // Unpromoted
184 case V.PAWN:
185 moves = super.getPotentialPawnMoves(sq);
186 break;
187 case V.MONK:
188 moves = this.getPotentialMonkMoves(sq);
189 break;
190 case V.HORSE:
191 moves = this.getPotentialHorseMoves(sq);
192 break;
193 case V.LANCE:
194 moves = this.getPotentialLanceMoves(sq);
195 break;
196 }
197 const promotionZone = (this.turn == 'w' ? [0, 1, 2] : [5, 6, 7]);
198 const promotedForm = V.MapUnpromoted[piece];
199 moves.forEach(m => {
200 if (promotionZone.includes(m.end.x)) m.appear[0].p = promotedForm;
201 });
202 return moves;
203 }
204
205 getPotentialKingMoves([x, y]) {
206 if (this.getColor(x, y) == 'b') return super.getPotentialKingMoves([x, y]);
207 // Clan doesn't castle:
208 return super.getSlideNJumpMoves(
209 [x, y],
210 V.steps[V.ROOK].concat(V.steps[V.BISHOP]),
211 "oneStep"
212 );
213 }
214
215 getPotentialCaptainMoves(sq) {
216 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
217 return super.getSlideNJumpMoves(sq, steps, "oneStep");
218 }
219
220 getPotentialNinjaMoves(sq) {
221 return (
222 super.getSlideNJumpMoves(sq, V.steps[V.BISHOP])
223 .concat(super.getSlideNJumpMoves(sq, V.steps[V.KNIGHT], "oneStep"))
224 );
225 }
226
227 getPotentialSamuraiMoves(sq) {
228 return (
229 super.getSlideNJumpMoves(sq, V.steps[V.ROOK])
230 .concat(super.getSlideNJumpMoves(sq, V.steps[V.KNIGHT], "oneStep"))
231 );
232 }
233
234 getPotentialMonkMoves(sq) {
235 return super.getSlideNJumpMoves(sq, V.steps[V.BISHOP], "oneStep");
236 }
237
238 getPotentialHorseMoves(sq) {
239 return super.getSlideNJumpMoves(sq, [ [-2, 1], [-2, -1] ], "oneStep");
240 }
241
242 getPotentialLanceMoves(sq) {
243 return super.getSlideNJumpMoves(sq, [ [-1, 0] ]);
244 }
245
246 isAttacked(sq, color) {
247 if (color == 'b')
248 return (super.isAttacked(sq, 'b') || this.isAttackedByCaptain(sq, 'b'));
249 // Attacked by white:
250 return (
251 super.isAttackedByKing(sq, 'w') ||
252 super.isAttackedByPawn(sq, 'w') ||
253 this.isAttackedByCaptain(sq, 'w') ||
254 this.isAttackedByNinja(sq, 'w') ||
255 this.isAttackedBySamurai(sq, 'w') ||
256 this.isAttackedByMonk(sq, 'w') ||
257 this.isAttackedByHorse(sq, 'w') ||
258 this.isAttackedByLance(sq, 'w') ||
259 super.isAttackedByBishop(sq, 'w') ||
260 super.isAttackedByKnight(sq, 'w') ||
261 super.isAttackedByRook(sq, 'w')
262 );
263 }
264
265 isAttackedByCaptain(sq, color) {
266 const steps = V.steps[V.BISHOP].concat(V.steps[V.ROOK]);
267 return (
268 super.isAttackedBySlideNJump(sq, color, V.CAPTAIN, steps, "oneStep")
269 );
270 }
271
272 isAttackedByNinja(sq, color) {
273 return (
274 super.isAttackedBySlideNJump(sq, color, V.NINJA, V.steps[V.BISHOP]) ||
275 super.isAttackedBySlideNJump(
276 sq, color, V.NINJA, V.steps[V.KNIGHT], "oneStep")
277 );
278 }
279
280 isAttackedBySamurai(sq, color) {
281 return (
282 super.isAttackedBySlideNJump(sq, color, V.SAMURAI, V.steps[V.ROOK]) ||
283 super.isAttackedBySlideNJump(
284 sq, color, V.SAMURAI, V.steps[V.KNIGHT], "oneStep")
285 );
286 }
287
288 isAttackedByMonk(sq, color) {
289 return (
290 super.isAttackedBySlideNJump(
291 sq, color, V.MONK, V.steps[V.BISHOP], "oneStep")
292 );
293 }
294
295 isAttackedByHorse(sq, color) {
296 return (
297 super.isAttackedBySlideNJump(
298 sq, color, V.HORSE, [ [2, 1], [2, -1] ], "oneStep")
299 );
300 }
301
302 isAttackedByLance(sq, color) {
303 return super.isAttackedBySlideNJump(sq, color, V.LANCE, [ [1, 0] ]);
304 }
305
306 getAllValidMoves() {
307 let moves = super.getAllPotentialMoves();
308 if (this.turn == 'w') {
309 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
310 moves = moves.concat(
311 this.getReserveMoves([V.size.x, i])
312 );
313 }
314 }
315 return this.filterValid(moves);
316 }
317
318 atLeastOneMove() {
319 if (super.atLeastOneMove()) return true;
320 if (this.turn == 'w') {
321 // Search one reserve move
322 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
323 let moves = this.filterValid(
324 this.getReserveMoves([V.size.x, i])
325 );
326 if (moves.length > 0) return true;
327 }
328 }
329 return false;
330 }
331
332 updateCastleFlags(move, piece) {
333 // Only black can castle:
334 const firstRank = 0;
335 if (piece == V.KING && move.appear[0].c == 'b')
336 this.castleFlags['b'] = [8, 8];
337 else if (
338 move.start.x == firstRank &&
339 this.castleFlags['b'].includes(move.start.y)
340 ) {
341 const flagIdx = (move.start.y == this.castleFlags['b'][0] ? 0 : 1);
342 this.castleFlags['b'][flagIdx] = 8;
343 }
344 else if (
345 move.end.x == firstRank &&
346 this.castleFlags['b'].includes(move.end.y)
347 ) {
348 const flagIdx = (move.end.y == this.castleFlags['b'][0] ? 0 : 1);
349 this.castleFlags['b'][flagIdx] = 8;
350 }
351 }
352
353 postPlay(move) {
354 super.postPlay(move);
355 // Skip castle:
356 if (move.vanish.length == 2 && move.appear.length == 2) return;
357 const color = move.appear[0].c;
358 if (move.vanish.length == 0) this.reserve[color][move.appear[0].p]--;
359 }
360
361 postUndo(move) {
362 super.postUndo(move);
363 if (move.vanish.length == 2 && move.appear.length == 2) return;
364 const color = this.turn;
365 if (move.vanish.length == 0) this.reserve[color][move.appear[0].p]++;
366 }
367
368 static get SEARCH_DEPTH() {
369 return 2;
370 }
371
372 getCurrentScore() {
373 const color = this.turn;
374 const nodrawResult = (color == "w" ? "0-1" : "1-0");
375 const oppLastRank = (color == 'w' ? 7 : 0);
376 if (this.kingPos[V.GetOppCol(color)][0] == oppLastRank)
377 return nodrawResult;
378 if (this.atLeastOneMove()) return "*";
379 return nodrawResult;
380 }
381
382 static get VALUES() {
383 return (
384 Object.assign(
385 {
386 c: 4,
387 j: 7,
388 s: 8,
389 m: 2,
390 h: 2,
391 l: 2
392 },
393 ChessRules.VALUES
394 )
395 );
396 }
397
398 evalPosition() {
399 let evaluation = super.evalPosition();
400 // Add reserve:
401 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
402 const p = V.RESERVE_PIECES[i];
403 evaluation += this.reserve["w"][p] * V.VALUES[p];
404 }
405 return evaluation;
406 }
407
408 getNotation(move) {
409 if (move.vanish.length > 0) return super.getNotation(move);
410 // Drop:
411 const piece =
412 move.appear[0].p != V.PAWN ? move.appear[0].p.toUpperCase() : "";
413 return piece + "@" + V.CoordsToSquare(move.end);
414 }
415
416 };