d8e1b1c5c9a08e84ca62690d6cf8f90876870a03
[vchess.git] / client / src / variants / Schess.js
1 import { ChessRules, PiPo } from "@/base_rules";
2
3 export class SchessRules extends ChessRules {
4
5 static get PawnSpecs() {
6 return Object.assign(
7 {},
8 ChessRules.PawnSpecs,
9 {
10 promotions:
11 ChessRules.PawnSpecs.promotions.concat([V.HAWK, V.ELEPHANT])
12 }
13 );
14 }
15
16 static get HAWK() {
17 return 'h';
18 }
19
20 static get ELEPHANT() {
21 return 'e';
22 }
23
24 static get NOTHING() {
25 return 'o';
26 }
27
28 static get PIECES() {
29 return ChessRules.PIECES.concat([V.HAWK, V.ELEPHANT]);
30 }
31
32 getPpath(b) {
33 if ([V.HAWK, V.ELEPHANT, V.NOTHING].includes(b[1])) return "Schess/" + b;
34 return b;
35 }
36
37 static IsGoodFen(fen) {
38 if (!ChessRules.IsGoodFen(fen)) return false;
39 const fenParsed = V.ParseFen(fen);
40 // Check pocket state
41 if (!fenParsed.pocket || !fenParsed.pocket.match(/^[0-1]{4,4}$/))
42 return false;
43 return true;
44 }
45
46 static IsGoodFlags(flags) {
47 // 4 for castle + 16 for generators
48 return !!flags.match(/^[a-z]{4,4}[01]{16,16}$/);
49 }
50
51 setFlags(fenflags) {
52 super.setFlags(fenflags); //castleFlags
53 this.pieceFlags = {
54 w: [...Array(8)], //pieces can generate Hawk or Elephant?
55 b: [...Array(8)]
56 };
57 const flags = fenflags.substr(4); //skip first 4 letters, for castle
58 for (let c of ["w", "b"]) {
59 for (let i = 0; i < 8; i++)
60 this.pieceFlags[c][i] = flags.charAt((c == "w" ? 0 : 8) + i) == "1";
61 }
62 }
63
64 aggregateFlags() {
65 return [this.castleFlags, this.pieceFlags];
66 }
67
68 disaggregateFlags(flags) {
69 this.castleFlags = flags[0];
70 this.pieceFlags = flags[1];
71 }
72
73 static ParseFen(fen) {
74 const fenParts = fen.split(" ");
75 return Object.assign(
76 ChessRules.ParseFen(fen),
77 { pocket: fenParts[5] }
78 );
79 }
80
81 static GenRandInitFen(randomness) {
82 return (
83 ChessRules.GenRandInitFen(randomness).slice(0, -2) +
84 // Add pieceFlags + pocket
85 "1111111111111111 - 1111"
86 );
87 }
88
89 getFen() {
90 return (
91 super.getFen() + " " +
92 this.getPocketFen()
93 );
94 }
95
96 getFenForRepeat() {
97 return (
98 super.getFenForRepeat() + "_" +
99 this.getPocketFen()
100 );
101 }
102
103 getFlagsFen() {
104 let fen = super.getFlagsFen();
105 // Add pieces flags
106 for (let c of ["w", "b"])
107 for (let i = 0; i < 8; i++) fen += (this.pieceFlags[c][i] ? "1" : "0");
108 return fen;
109 }
110
111 getPocketFen() {
112 let res = "";
113 for (let c of ["w", "b"])
114 res += this.pocket[c][V.HAWK] + this.pocket[c][V.ELEPHANT];
115 return res;
116 }
117
118 setOtherVariables(fen) {
119 super.setOtherVariables(fen);
120 const fenParsed = V.ParseFen(fen);
121 this.pocket = {
122 "w": {
123 h: parseInt(fenParsed.pocket[0], 10),
124 e: parseInt(fenParsed.pocket[1], 10)
125 },
126 "b": {
127 h: parseInt(fenParsed.pocket[2], 10),
128 e: parseInt(fenParsed.pocket[3], 10)
129 }
130 };
131 }
132
133 getPotentialMovesFrom([x, y]) {
134 let moves = undefined;
135 switch (this.getPiece(x, y)) {
136 case V.HAWK:
137 moves = this.getPotentialHawkMoves([x, y]);
138 break;
139 case V.ELEPHANT:
140 moves = this.getPotentialElephantMoves([x, y]);
141 break;
142 default:
143 moves = super.getPotentialMovesFrom([x, y]);
144 }
145 // For moves presentation when choices:
146 const unshiftNothing = (m) => {
147 const a = m.appear[0];
148 m.appear.unshift(new PiPo({
149 p: V.NOTHING,
150 c: 'o',
151 x: a.x,
152 y: a.y
153 }));
154 };
155 // Post-processing: add choices for hawk and elephant,
156 // except for moves letting the king under check.
157 const color = this.turn;
158 if (Object.values(this.pocket[color]).some(v => v > 0)) {
159 const firstRank = (color == "w" ? 7 : 0);
160 let validMoves = [];
161 moves.forEach(m => {
162 let inCheckAfter = false;
163 this.play(m);
164 if (this.underCheck(color)) inCheckAfter = true;
165 this.undo(m);
166 if (!inCheckAfter) {
167 for (let pp of ['h', 'e']) {
168 if (this.pocket[color][pp] > 0) {
169 let shift = (m.appear[0].p == V.NOTHING ? 1 : 0);
170 if (
171 m.start.x == firstRank &&
172 this.pieceFlags[color][m.start.y] &&
173 (
174 m.appear.length == shift+1 ||
175 // Special castle case: is initial king square free?
176 ![m.appear[shift].y, m.appear[shift+1].y]
177 .includes(m.vanish[0].y)
178 )
179 ) {
180 let pMove = JSON.parse(JSON.stringify(m));
181 if (shift == 1) pMove.appear.shift();
182 // NOTE: unshift instead of push, for choices presentation
183 pMove.appear.unshift(new PiPo({
184 p: pp,
185 c: color,
186 x: x,
187 y: y
188 }));
189 validMoves.push(pMove);
190 if (shift == 0) unshiftNothing(m);
191 }
192 shift = (m.appear[0].p == V.NOTHING ? 1 : 0);
193 if (
194 m.appear.length >= 2 + shift &&
195 m.vanish.length == 2 &&
196 ![m.appear[shift].y, m.appear[shift+1].y]
197 .includes(m.vanish[1].y)
198 ) {
199 // Special castle case: rook flag was necessarily on
200 let pMove = JSON.parse(JSON.stringify(m));
201 if (shift == 1) pMove.appear.shift();
202 pMove.appear.unshift(new PiPo({
203 p: pp,
204 c: color,
205 x: m.vanish[1].x,
206 y: m.vanish[1].y
207 }));
208 validMoves.push(pMove);
209 if (shift == 0) unshiftNothing(m);
210 }
211 }
212 }
213 // Unshift, to show the empty square on the left:
214 validMoves.unshift(m);
215 }
216 });
217 moves = validMoves;
218 }
219 return moves;
220 }
221
222 getPotentialHawkMoves(sq) {
223 return this.getSlideNJumpMoves(sq, V.steps[V.BISHOP]).concat(
224 this.getSlideNJumpMoves(sq, V.steps[V.KNIGHT], "oneStep")
225 );
226 }
227
228 getPotentialElephantMoves(sq) {
229 return this.getSlideNJumpMoves(sq, V.steps[V.ROOK]).concat(
230 this.getSlideNJumpMoves(sq, V.steps[V.KNIGHT], "oneStep")
231 );
232 }
233
234 isAttacked(sq, color) {
235 return (
236 super.isAttacked(sq, color) ||
237 this.isAttackedByHawk(sq, color) ||
238 this.isAttackedByElephant(sq, color)
239 );
240 }
241
242 isAttackedByHawk(sq, color) {
243 return (
244 this.isAttackedBySlideNJump(sq, color, V.HAWK, V.steps[V.BISHOP]) ||
245 this.isAttackedBySlideNJump(
246 sq,
247 color,
248 V.HAWK,
249 V.steps[V.KNIGHT],
250 "oneStep"
251 )
252 );
253 }
254
255 isAttackedByElephant(sq, color) {
256 return (
257 this.isAttackedBySlideNJump(sq, color, V.ELEPHANT, V.steps[V.ROOK]) ||
258 this.isAttackedBySlideNJump(
259 sq,
260 color,
261 V.ELEPHANT,
262 V.steps[V.KNIGHT],
263 "oneStep"
264 )
265 );
266 }
267
268 filterValid(moves) {
269 if (Object.values(this.pocket[this.turn]).some(v => v > 0))
270 // Undercheck tests done in getPotentialMovesFrom()
271 return moves;
272 return super.filterValid(moves);
273 }
274
275 prePlay(move) {
276 super.prePlay(move);
277 if (move.appear.length >= 2) {
278 if ([V.HAWK, V.ELEPHANT].includes(move.appear[0].p)) {
279 // A pocket piece is used
280 const color = this.turn;
281 this.pocket[color][move.appear[0].p] = 0;
282 }
283 }
284 }
285
286 postPlay(move) {
287 const color = move.vanish[0].c;
288 const piece = move.vanish[0].p;
289 // Update king position + flags
290 if (piece == V.KING) {
291 const shift =
292 ([V.HAWK, V.ELEPHANT, V.NOTHING].includes(move.appear[0].p) ? 1 : 0);
293 this.kingPos[color][0] = move.appear[shift].x;
294 this.kingPos[color][1] = move.appear[shift].y;
295 }
296 this.updateCastleFlags(move, piece);
297
298 const oppCol = this.turn;
299 const firstRank = (color == 'w' ? 7 : 0);
300 const oppFirstRank = 7 - firstRank;
301 // Does this move turn off a piece init square flag?
302 if (move.start.x == firstRank) {
303 if (this.pieceFlags[color][move.start.y])
304 this.pieceFlags[color][move.start.y] = false;
305 // Special castle case:
306 if (move.appear.length >= 2 && move.vanish.length == 2) {
307 const L = move.appear.length;
308 if (move.appear[L-1].p == V.ROOK)
309 this.pieceFlags[color][move.vanish[1].y] = false;
310 }
311 }
312 if (move.end.x == oppFirstRank && this.pieceFlags[oppCol][move.end.y])
313 this.pieceFlags[oppCol][move.end.y] = false;
314 }
315
316 postUndo(move) {
317 super.postUndo(move);
318 if (move.appear.length >= 2) {
319 if ([V.HAWK, V.ELEPHANT].includes(move.appear[0].p)) {
320 // A pocket piece was used
321 const color = this.turn;
322 this.pocket[color][move.appear[0].p] = 1;
323 }
324 }
325 }
326
327 static get SEARCH_DEPTH() {
328 return 2;
329 }
330
331 static get VALUES() {
332 return Object.assign(
333 {},
334 ChessRules.VALUES,
335 { 'h': 5, 'e': 7 }
336 );
337 }
338
339 getNotation(move) {
340 if (move.appear.length >= 2) {
341 const pPieceAppear = [V.HAWK, V.ELEPHANT].includes(move.appear[0].p);
342 const nothingAppear = (move.appear[0].p == V.NOTHING);
343 if (pPieceAppear || nothingAppear) {
344 let suffix = "";
345 if (pPieceAppear) {
346 suffix = "/" + move.appear[0].p.toUpperCase();
347 if (move.appear.length == 3) {
348 // Castling; indicate square
349 suffix +=
350 V.CoordsToSquare({ x: move.appear[0].x, y: move.appear[0].y });
351 }
352 }
353 let cmove = JSON.parse(JSON.stringify(move));
354 cmove.appear.shift();
355 return super.getNotation(cmove) + suffix;
356 }
357 }
358 return super.getNotation(move);
359 }
360
361 };