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