Fix some bugs
[xogo.git] / variants / Apocalypse / class.js
1 import ChessRules from "/base_rules.js";
2 import {ArrayFun} from "/utils/array.js";
3
4 export default class ApocalypseRules extends ChessRules {
5
6 static get Options() {
7 return {};
8 }
9
10 get hasFlags() {
11 return false;
12 }
13 get hasEnpassant() {
14 return false;
15 }
16 get hideMoves() {
17 return true;
18 }
19
20 get pawnPromotions() {
21 return ['n', 'p'];
22 }
23
24 get size() {
25 return {x: 5, y: 5};
26 }
27
28 setOtherVariables(fenParsed) {
29 super.setOtherVariables(fenParsed);
30 // Often a simple move, but sometimes an array (pawn relocation)
31 this.whiteMove = fenParsed.whiteMove != "-"
32 ? JSON.parse(fenParsed.whiteMove)
33 : [];
34 this.firstMove = null; //used if black turn pawn relocation
35 this.penalties = ArrayFun.toObject(
36 ['w', 'b'],
37 [0, 1].map(i => parseInt(fenParsed.penalties.charAt(i), 10))
38 );
39 }
40
41 genRandInitBaseFen() {
42 return {
43 fen: "npppn/p3p/5/P3P/NPPPN",
44 o: {}
45 };
46 }
47
48 getPartFen(o) {
49 return {
50 whiteMove: (o.init || !this.whiteMove) ? "-" : this.whiteMove,
51 penalties: o.init ? "00" : Object.values(this.penalties).join("")
52 };
53 }
54
55 getWhitemoveFen() {
56 if (this.whiteMove.length == 0)
57 return "-";
58 if (this.whiteMove.length == 1)
59 return JSON.stringify(this.whiteMove[0]);
60 return JSON.stringify(this.whiteMove); //pawn relocation
61 }
62
63 // Allow pawns to move diagonally and capture vertically,
64 // because some of these moves might be valid a posteriori.
65 // They will be flagged as 'illegal' in a first time, however.
66 pieces(color, x, y) {
67 const pawnShift = (color == "w" ? -1 : 1);
68 return {
69 'p': {
70 "class": "pawn",
71 moves: [
72 {
73 steps: [[pawnShift, 0], [pawnShift, -1], [pawnShift, 1]],
74 range: 1
75 }
76 ],
77 },
78 'n': super.pieces(color, x, y)['n']
79 };
80 }
81
82 // Allow self-captures, because they might be valid
83 // if opponent takes on the same square (luck...)
84 canTake() {
85 return true;
86 }
87
88 getPotentialMovesFrom([x, y]) {
89 let moves = [];
90 if (this.subTurn == 2) {
91 const start = this.firstMove.end;
92 if (x == start.x && y == start.y) {
93 // Move the pawn to any empty square not on last rank (== x)
94 for (let i=0; i<this.size.x; i++) {
95 if (i == x)
96 continue;
97 for (let j=0; j<this.size.y; j++) {
98 if (this.board[i][j] == "")
99 moves.push(this.getBasicMove([x, y], [i, j]));
100 }
101 }
102 }
103 }
104 else {
105 const oppCol = C.GetOppCol(this.getColor(x, y));
106 moves = super.getPotentialMovesFrom([x, y]).filter(m => {
107 // Remove pawn push toward own color (absurd)
108 return (
109 m.vanish[0].p != 'p' ||
110 m.end.y != m.start.y ||
111 m.vanish.length == 1 ||
112 m.vanish[1].c == oppCol
113 );
114 });
115 // Flag a priori illegal moves
116 moves.forEach(m => {
117 if (
118 // Self-capture test:
119 (m.vanish.length == 2 && m.vanish[1].c == m.vanish[0].c) ||
120 // Pawn going diagonaly to empty square, or vertically to occupied
121 (
122 m.vanish[0].p == 'p' &&
123 (
124 (m.end.y == m.start.y && m.vanish.length == 2) ||
125 (m.end.y != m.start.y && m.vanish.length == 1)
126 )
127 )
128 ) {
129 m.illegal = true;
130 }
131 });
132 }
133 return moves;
134 }
135
136 pawnPostProcess(moves, color, oppCol) {
137 let knightCount = 0;
138 for (let i=0; i<this.size.x; i++) {
139 for (let j=0; j<this.size.y; j++) {
140 if (
141 this.board[i][j] != "" &&
142 this.getColor(i, j) == color &&
143 this.getPiece(i, j) == 'n'
144 ) {
145 knightCount++;
146 }
147 }
148 }
149 return super.pawnPostProcess(moves, color, oppCol).filter(m => {
150 if (
151 m.vanish[0].p == 'p' &&
152 (
153 (color == 'w' && m.end.x == 0) ||
154 (color == 'b' && m.end.x == this.size.x - 1)
155 )
156 ) {
157 // Pawn promotion
158 if (knightCount <= 1 && m.appear[0].p == 'p')
159 return false; //knight promotion mandatory
160 if (knightCount == 2 && m.appear[0].p == 'n')
161 m.illegal = true; //will be legal only if one knight is captured
162 }
163 return true;
164 });
165 }
166
167 filterValid(moves) {
168 // No checks:
169 return moves;
170 }
171
172 // White and black (partial) moves were played: merge
173 resolveSynchroneMove(move) {
174 const condensate = (mArr) => {
175 const illegal = (mArr.length == 1 && mArr[0].illegal) ||
176 (!mArr[0] && mArr[1].illegal);
177 if (mArr.length == 1)
178 return Object.assign({illegal: illegal}, mArr[0]);
179 if (!mArr[0])
180 return Object.assign({illegal: illegal}, mArr[1]);
181 // Pawn relocation
182 return {
183 start: mArr[0].start,
184 end: mArr[1].end,
185 vanish: mArr[0].vanish,
186 appear: mArr[1].appear,
187 segments: [
188 [[mArr[0].start.x, mArr[0].start.y], [mArr[0].end.x, mArr[0].end.y]],
189 [[mArr[1].start.x, mArr[1].start.y], [mArr[1].end.x, mArr[1].end.y]]
190 ]
191 };
192 };
193 const compatible = (m1, m2) => {
194 if (m2.illegal)
195 return false;
196 // Knight promotion?
197 if (m1.appear[0].p != m1.vanish[0].p)
198 return m2.vanish.length == 2 && m2.vanish[1].p == 'n';
199 if (
200 // Self-capture attempt?
201 (m1.vanish.length == 2 && m1.vanish[1].c == m1.vanish[0].c) ||
202 // Pawn captures something by anticipation?
203 (
204 m1.vanish[0].p == 'p' &&
205 m1.vanish.length == 1 &&
206 m1.start.y != m1.end.y
207 )
208 ) {
209 return m2.end.x == m1.end.x && m2.end.y == m1.end.y;
210 }
211 // Pawn push toward an enemy piece?
212 if (
213 m1.vanish[0].p == 'p' &&
214 m1.vanish.length == 2 &&
215 m1.start.y == m1.end.y
216 ) {
217 return m2.start.x == m1.end.x && m2.start.y == m1.end.y;
218 }
219 return true;
220 };
221 const adjust = (res) => {
222 if (!res.wm || !res.bm)
223 return;
224 for (let c of ['w', 'b']) {
225 const myMove = res[c + 'm'], oppMove = res[C.GetOppCol(c) + 'm'];
226 if (
227 // More general test than checking moves ends,
228 // because of potential pawn relocation
229 myMove.vanish.length == 2 &&
230 myMove.vanish[1].x == oppMove.start.x &&
231 myMove.vanish[1].y == oppMove.start.y
232 ) {
233 // Whatever was supposed to vanish, finally doesn't vanish
234 myMove.vanish.pop();
235 }
236 }
237 if (res.wm.end.y == res.bm.end.y && res.wm.end.x == res.bm.end.x) {
238 // Collision (necessarily on empty square)
239 if (!res.wm.illegal && !res.bm.illegal) {
240 if (res.wm.vanish[0].p != res.bm.vanish[0].p) {
241 const vanishColor = (res.wm.vanish[0].p == 'n' ? 'b' : 'w');
242 res[vanishColor + 'm'].appear.shift();
243 }
244 else {
245 // Collision of two pieces of same nature: both disappear
246 res.wm.appear.shift();
247 res.bm.appear.shift();
248 }
249 }
250 else {
251 const c = (!res.wm.illegal ? 'w' : 'b');
252 // Illegal move wins:
253 res[c + 'm'].appear.shift();
254 }
255 }
256 };
257 // Clone moves to avoid altering them:
258 let whiteMove = JSON.parse(JSON.stringify(this.whiteMove)),
259 blackMove = JSON.parse(JSON.stringify([this.firstMove, move]));
260 [whiteMove, blackMove] = [condensate(whiteMove), condensate(blackMove)];
261 let res = {
262 wm: (
263 (!whiteMove.illegal || compatible(whiteMove, blackMove))
264 ? whiteMove
265 : null
266 ),
267 bm: (
268 (!blackMove.illegal || compatible(blackMove, whiteMove))
269 ? blackMove
270 : null
271 )
272 };
273 adjust(res);
274 return res;
275 }
276
277 play(move, callback) {
278 const color = this.turn;
279 if (color == 'w')
280 this.whiteMove.push(move);
281 if (
282 move.vanish[0].p == 'p' && move.appear[0].p == 'p' &&
283 (
284 (color == 'w' && move.end.x == 0) ||
285 (color == 'b' && move.end.x == this.size.x - 1)
286 )
287 ) {
288 // Pawn on last rank : will relocate
289 this.subTurn = 2;
290 this.firstMove = move;
291 if (color == this.playerColor) {
292 this.playOnBoard(move);
293 this.playVisual(move);
294 }
295 callback();
296 return;
297 }
298 if (color == this.playerColor && this.firstMove) {
299 // The move was played on board: undo it
300 this.undoOnBoard(this.firstMove);
301 const revFirstMove = {
302 start: this.firstMove.end,
303 end: this.firstMove.start,
304 appear: this.firstMove.vanish,
305 vanish: this.firstMove.appear
306 };
307 this.playVisual(revFirstMove);
308 }
309 this.turn = C.GetOppCol(color);
310 this.movesCount++;
311 this.subTurn = 1;
312 this.firstMove = null;
313 if (color == 'b') {
314 // A full turn just ended
315 const res = this.resolveSynchroneMove(move);
316 const afterAnimate = () => {
317 // start + end don't matter for playOnBoard() and playVisual().
318 // Merging is necessary because moves may overlap.
319 let toPlay = {appear: [], vanish: []};
320 for (let c of ['w', 'b']) {
321 if (res[c + 'm']) {
322 Array.prototype.push.apply(toPlay.vanish, res[c + 'm'].vanish);
323 Array.prototype.push.apply(toPlay.appear, res[c + 'm'].appear);
324 }
325 }
326 this.playOnBoard(toPlay);
327 this.playVisual(toPlay);
328 callback();
329 };
330 if (res.wm)
331 this.animate(res.wm, () => {if (!res.bm) afterAnimate();});
332 if (res.bm)
333 this.animate(res.bm, afterAnimate);
334 if (!res.wm && !res.bm) {
335 this.displayIllegalInfo("both illegal");
336 ['w', 'b'].forEach(c => this.penalties[c]++);
337 }
338 else if (!res.wm) {
339 this.displayIllegalInfo("white illegal");
340 this.penalties['w']++;
341 }
342 else if (!res.bm) {
343 this.displayIllegalInfo("black illegal");
344 this.penalties['b']++;
345 }
346 this.whiteMove = [];
347 }
348 else
349 callback();
350 }
351
352 displayIllegalInfo(msg) {
353 super.displayMessage(null, msg, "illegal-text", 2000);
354 }
355
356 atLeastOneLegalMove(color) {
357 for (let i=0; i<this.size.x; i++) {
358 for (let j=0; j<this.size.y; j++) {
359 if (
360 this.board[i][j] != "" &&
361 this.getColor(i, j) == color &&
362 this.getPotentialMovesFrom([i, j]).some(m => !m.illegal)
363 ) {
364 return true;
365 }
366 }
367 }
368 return false;
369 }
370
371 getCurrentScore() {
372 if (this.turn == 'b') {
373 // Turn (white + black) not over yet.
374 // Could be stalemate if black cannot move (legally):
375 if (!this.atLeastOneLegalMove('b'))
376 return "1/2";
377 return "*";
378 }
379 // Count footmen: if a side has none, it loses
380 let fmCount = {w: 0, b: 0};
381 for (let i=0; i<this.size.x; i++) {
382 for (let j=0; j<this.size.y; j++) {
383 if (this.board[i][j] != "" && this.getPiece(i, j) == 'p')
384 fmCount[this.getColor(i, j)]++;
385 }
386 }
387 if (Object.values(fmCount).some(v => v == 0)) {
388 if (fmCount['w'] == 0 && fmCount['b'] == 0)
389 // Everyone died
390 return "1/2";
391 if (fmCount['w'] == 0) return "0-1";
392 return "1-0"; //fmCount['b'] == 0
393 }
394 // Check penaltyFlags: if a side has 2 or more, it loses
395 if (Object.values(this.penalties).every(v => v == 2)) return "1/2";
396 if (this.penalties['w'] == 2) return "0-1";
397 if (this.penalties['b'] == 2) return "1-0";
398 if (!this.atLeastOneLegalMove('w') || !this.atLeastOneLegalMove('b'))
399 // Stalemate (should be very rare)
400 return "1/2";
401 return "*";
402 }
403
404 };