Several small improvements + integrate options + first working draft of Cwda
[vchess.git] / client / src / variants / Spartan.js
1 import { ChessRules } from "@/base_rules";
2
3 export class SpartanRules extends ChessRules {
4
5 static get HasEnpassant() {
6 return false;
7 }
8
9 static IsGoodFlags(flags) {
10 // Only white can castle
11 return !!flags.match(/^[a-z]{2,2}$/);
12 }
13
14 getPpath(b) {
15 if (b[0] == 'b' && b[1] != 'k') return "Spartan/" + b;
16 return b;
17 }
18
19 static GenRandInitFen(options) {
20 if (options.randomness == 0)
21 return "lgkcckwl/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w 0 ah";
22
23 // Mapping white --> black (first knight --> general; TODO):
24 const piecesMap = {
25 'r': 'c',
26 'n': 'w',
27 'b': 'l',
28 'q': 'k',
29 'k': 'k',
30 'g': 'g'
31 };
32
33 const baseFen = ChessRules.GenRandInitFen(options).replace('n', 'g');
34 return (
35 baseFen.substr(0, 8).split('').map(p => piecesMap[p]).join('') +
36 baseFen.substr(8)
37 );
38 }
39
40 getFlagsFen() {
41 return this.castleFlags['w'].map(V.CoordToColumn).join("");
42 }
43
44 setFlags(fenflags) {
45 this.castleFlags = { 'w': [-1, -1] };
46 for (let i = 0; i < 2; i++)
47 this.castleFlags['w'][i] = V.ColumnToCoord(fenflags.charAt(i));
48 }
49
50 static IsGoodPosition(position) {
51 if (position.length == 0) return false;
52 const rows = position.split("/");
53 if (rows.length != V.size.x) return false;
54 let kings = { "k": 0, "K": 0 };
55 for (let row of rows) {
56 let sumElts = 0;
57 for (let i = 0; i < row.length; i++) {
58 if (['K','k'].includes(row[i])) kings[row[i]]++;
59 if (V.PIECES.includes(row[i].toLowerCase())) sumElts++;
60 else {
61 const num = parseInt(row[i], 10);
62 if (isNaN(num) || num <= 0) return false;
63 sumElts += num;
64 }
65 }
66 if (sumElts != V.size.y) return false;
67 }
68 // Both kings should be on board. One for white, 1 or 2 for black.
69 if (kings['K'] != 1 || ![1, 2].includes(kings['k'])) return false;
70 return true;
71 }
72
73 scanKings(fen) {
74 // Scan white king only:
75 this.kingPos = { w: [-1, -1] };
76 const fenRows = V.ParseFen(fen).position.split("/");
77 for (let i = 0; i < fenRows.length; i++) {
78 let k = 0;
79 for (let j = 0; j < fenRows[i].length; j++) {
80 switch (fenRows[i].charAt(j)) {
81 case "K":
82 this.kingPos["w"] = [i, k];
83 break;
84 default: {
85 const num = parseInt(fenRows[i].charAt(j), 10);
86 if (!isNaN(num)) k += num - 1;
87 }
88 }
89 k++;
90 }
91 }
92 }
93
94 static get LIEUTENANT() {
95 return 'l';
96 }
97 static get GENERAL() {
98 return 'g';
99 }
100 static get CAPTAIN() {
101 return 'c';
102 }
103 static get WARLORD() {
104 return 'w';
105 }
106
107 static get PIECES() {
108 return (
109 ChessRules.PIECES.concat([V.LIEUTENANT, V.GENERAL, V.CAPTAIN, V.WARLORD])
110 );
111 }
112
113 getPotentialMovesFrom([x, y]) {
114 if (this.getColor(x, y) == 'w') return super.getPotentialMovesFrom([x, y]);
115 switch (this.getPiece(x, y)) {
116 case V.PAWN: {
117 const kings = this.getKingsPos();
118 const moves = this.getPotentialHopliteMoves([x, y]);
119 if (kings.length == 1) return moves;
120 return moves.filter(m => m.appear[0].p != V.KING);
121 }
122 case V.KING: return this.getPotentialSpartanKingMoves([x, y]);
123 case V.LIEUTENANT: return this.getPotentialLieutenantMoves([x, y]);
124 case V.GENERAL: return this.getPotentialGeneralMoves([x, y]);
125 case V.CAPTAIN: return this.getPotentialCaptainMoves([x, y]);
126 case V.WARLORD: return this.getPotentialWarlordMoves([x, y]);
127 }
128 return [];
129 }
130
131 static get steps() {
132 return Object.assign(
133 {},
134 ChessRules.steps,
135 {
136 // Dabbabah
137 'd': [
138 [-2, 0],
139 [0, -2],
140 [2, 0],
141 [0, 2]
142 ],
143 // Alfil
144 'a': [
145 [2, 2],
146 [2, -2],
147 [-2, 2],
148 [-2, -2]
149 ]
150 }
151 );
152 }
153
154 getPotentialSpartanKingMoves(sq) {
155 // No castle:
156 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
157 return super.getSlideNJumpMoves(sq, steps, 1);
158 }
159
160 getPotentialHopliteMoves([x, y]) {
161 // Berolina pawn, with initial jumping option
162 let moves = [];
163 if (x == 6) {
164 const finalPieces =
165 [V.LIEUTENANT, V.GENERAL, V.CAPTAIN, V.KING, V.WARLORD];
166 for (let shiftY of [-1, 0, 1]) {
167 const [i, j] = [7, y + shiftY];
168 if (
169 V.OnBoard(i, j) &&
170 (
171 (shiftY != 0 && this.board[i][j] == V.EMPTY) ||
172 (shiftY == 0 && this.getColor(i, j) == 'w')
173 )
174 ) {
175 for (let p of finalPieces)
176 moves.push(this.getBasicMove([x, y], [i, j], { c: 'b', p: p }));
177 }
178 }
179 }
180 else {
181 for (let shiftY of [-1, 0, 1]) {
182 const [i, j] = [x + 1, y + shiftY];
183 if (
184 V.OnBoard(i, j) &&
185 (
186 (shiftY != 0 && this.board[i][j] == V.EMPTY) ||
187 (shiftY == 0 && this.getColor(i, j) == 'w')
188 )
189 ) {
190 moves.push(this.getBasicMove([x, y], [i, j]));
191 }
192 }
193 // Add initial 2 squares jumps:
194 if (x == 1) {
195 for (let shiftY of [-2, 2]) {
196 const [i, j] = [3, y + shiftY];
197 if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY)
198 moves.push(this.getBasicMove([x, y], [i, j]));
199 }
200 }
201 }
202 return moves;
203 }
204
205 getPotentialLieutenantMoves([x, y]) {
206 let moves = [];
207 for (let shiftY of [-1, 1]) {
208 const [i, j] = [x, y + shiftY];
209 if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY)
210 moves.push(this.getBasicMove([x, y], [i, j]));
211 }
212 const steps = V.steps[V.BISHOP].concat(V.steps['a']);
213 Array.prototype.push.apply(
214 moves,
215 super.getSlideNJumpMoves([x, y], steps, 1)
216 );
217 return moves;
218 }
219
220 getPotentialCaptainMoves([x, y]) {
221 const steps = V.steps[V.ROOK].concat(V.steps['d']);
222 return super.getSlideNJumpMoves([x, y], steps, 1)
223 }
224
225 getPotentialGeneralMoves([x, y]) {
226 return (
227 super.getSlideNJumpMoves([x, y], V.steps[V.BISHOP], 1)
228 .concat(super.getSlideNJumpMoves([x, y], V.steps[V.ROOK]))
229 );
230 }
231
232 getPotentialWarlordMoves([x, y]) {
233 return (
234 super.getSlideNJumpMoves([x, y], V.steps[V.KNIGHT], 1)
235 .concat(super.getSlideNJumpMoves([x, y], V.steps[V.BISHOP]))
236 );
237 }
238
239 isAttacked(sq, color) {
240 if (color == 'w') return super.isAttacked(sq, 'w');
241 return (
242 this.isAttackedByHoplite(sq) ||
243 super.isAttackedByKing(sq, 'b') ||
244 this.isAttackedByLieutenant(sq) ||
245 this.isAttackedByGeneral(sq) ||
246 this.isAttackedByCaptain(sq) ||
247 this.isAttackedByWarlord(sq)
248 );
249 }
250
251 isAttackedByHoplite(sq) {
252 return super.isAttackedBySlideNJump(sq, 'b', V.PAWN, [[-1,0]], 1);
253 }
254
255 isAttackedByLieutenant(sq) {
256 const steps = V.steps[V.BISHOP].concat(V.steps['a']);
257 return (
258 super.isAttackedBySlideNJump(sq, 'b', V.LIEUTENANT, steps, 1)
259 );
260 }
261
262 isAttackedByCaptain(sq) {
263 const steps = V.steps[V.ROOK].concat(V.steps['d']);
264 return super.isAttackedBySlideNJump(sq, 'b', V.CAPTAIN, steps, 1);
265 }
266
267 isAttackedByGeneral(sq) {
268 return (
269 super.isAttackedBySlideNJump(
270 sq, 'b', V.GENERAL, V.steps[V.BISHOP], 1) ||
271 super.isAttackedBySlideNJump(sq, 'b', V.GENERAL, V.steps[V.ROOK])
272 );
273 }
274
275 isAttackedByWarlord(sq) {
276 return (
277 super.isAttackedBySlideNJump(
278 sq, 'b', V.WARLORD, V.steps[V.KNIGHT], 1) ||
279 super.isAttackedBySlideNJump(sq, 'b', V.WARLORD, V.steps[V.BISHOP])
280 );
281 }
282
283 updateCastleFlags(move, piece) {
284 // Only white can castle:
285 const firstRank = 7;
286 if (piece == V.KING && move.appear[0].c == 'w')
287 this.castleFlags['w'] = [8, 8];
288 else if (
289 move.start.x == firstRank &&
290 this.castleFlags['w'].includes(move.start.y)
291 ) {
292 const flagIdx = (move.start.y == this.castleFlags['w'][0] ? 0 : 1);
293 this.castleFlags['w'][flagIdx] = 8;
294 }
295 else if (
296 move.end.x == firstRank &&
297 this.castleFlags['w'].includes(move.end.y)
298 ) {
299 const flagIdx = (move.end.y == this.castleFlags['w'][0] ? 0 : 1);
300 this.castleFlags['w'][flagIdx] = 8;
301 }
302 }
303
304 postPlay(move) {
305 if (move.vanish[0].c == 'w') super.postPlay(move);
306 }
307
308 postUndo(move) {
309 if (move.vanish[0].c == 'w') super.postUndo(move);
310 }
311
312 getKingsPos() {
313 let kings = [];
314 for (let i=0; i<8; i++) {
315 for (let j=0; j<8; j++) {
316 if (
317 this.board[i][j] != V.EMPTY &&
318 this.getColor(i, j) == 'b' &&
319 this.getPiece(i, j) == V.KING
320 ) {
321 kings.push({ x: i, y: j });
322 }
323 }
324 }
325 return kings;
326 }
327
328 getCheckSquares() {
329 if (this.turn == 'w') return super.getCheckSquares();
330 const kings = this.getKingsPos();
331 let res = [];
332 for (let i of [0, 1]) {
333 if (
334 kings.length >= i+1 &&
335 super.isAttacked([kings[i].x, kings[i].y], 'w')
336 ) {
337 res.push([kings[i].x, kings[i].y]);
338 }
339 }
340 return res;
341 }
342
343 filterValid(moves) {
344 if (moves.length == 0) return [];
345 const color = moves[0].vanish[0].c;
346 if (color == 'w') return super.filterValid(moves);
347 // Black moves: check if both kings under attack
348 // If yes, moves must remove at least one attack.
349 const kings = this.getKingsPos();
350 return moves.filter(m => {
351 this.play(m);
352 let attacks = 0;
353 for (let k of kings) {
354 const curKingPos =
355 this.board[k.x][k.y] == V.EMPTY
356 ? [m.appear[0].x, m.appear[0].y] //king moved
357 : [k.x, k.y]
358 if (super.isAttacked(curKingPos, 'w')) attacks++;
359 else break; //no need to check further
360 }
361 this.undo(m);
362 return (
363 (kings.length == 2 && attacks <= 1) ||
364 (kings.length == 1 && attacks == 0)
365 );
366 });
367 }
368
369 getCurrentScore() {
370 if (this.turn == 'w') return super.getCurrentScore();
371 if (super.atLeastOneMove()) return "*";
372 // Count kings on board
373 const kings = this.getKingsPos();
374 if (
375 super.isAttacked([kings[0].x, kings[0].y], 'w') ||
376 (kings.length == 2 && super.isAttacked([kings[1].x, kings[1].y], 'w'))
377 ) {
378 return "1-0";
379 }
380 return "1/2"; //stalemate
381 }
382
383 static get VALUES() {
384 return Object.assign(
385 {},
386 ChessRules.VALUES,
387 {
388 l: 3,
389 g: 7,
390 c: 3,
391 w: 7
392 }
393 );
394 }
395
396 static get SEARCH_DEPTH() {
397 return 2;
398 }
399
400 getNotation(move) {
401 const piece = this.getPiece(move.start.x, move.start.y);
402 if (piece == V.PAWN) {
403 // Pawn move
404 const finalSquare = V.CoordsToSquare(move.end);
405 let notation = "";
406 if (move.vanish.length == 2)
407 // Capture
408 notation = "Px" + finalSquare;
409 else {
410 // No capture: indicate the initial square for potential ambiguity
411 const startSquare = V.CoordsToSquare(move.start);
412 notation = startSquare + finalSquare;
413 }
414 if (move.appear[0].p != V.PAWN)
415 // Promotion
416 notation += "=" + move.appear[0].p.toUpperCase();
417 return notation;
418 }
419 return super.getNotation(move); //OK for all other pieces
420 }
421
422 };