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