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