First working draft of Apocalypse
[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 w 0",
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 myMove.end.x == oppMove.start.x &&
228 myMove.end.y == oppMove.start.y
229 ) {
230 // Whatever was supposed to vanish, finally doesn't vanish
231 myMove.vanish.pop();
232 }
233 }
234 if (res.wm.end.y == res.bm.end.y && res.wm.end.x == res.bm.end.x) {
235 // Collision (necessarily on empty square)
236 if (!res.wm.illegal && !res.bm.illegal) {
237 if (res.wm.vanish[0].p != res.bm.vanish[0].p) {
238 const c = (res.wm.vanish[0].p == 'n' ? 'w' : 'b');
239 res[c + 'm'].vanish.push(res[C.GetOppCol(c) + 'm'].appear.shift());
240 }
241 else {
242 // Collision of two pieces of same nature: both disappear
243 res.wm.appear.shift();
244 res.bm.appear.shift();
245 }
246 }
247 else {
248 const c = (!res.wm.illegal ? 'w' : 'b');
249 // Illegal move wins:
250 res[c + 'm'].appear.shift();
251 }
252 }
253 };
254 // Clone moves to avoid altering them:
255 let whiteMove = JSON.parse(JSON.stringify(this.whiteMove)),
256 blackMove = JSON.parse(JSON.stringify([this.firstMove, move]));
257 [whiteMove, blackMove] = [condensate(whiteMove), condensate(blackMove)];
258 let res = {
259 wm: (
260 (!whiteMove.illegal || compatible(whiteMove, blackMove))
261 ? whiteMove
262 : null
263 ),
264 bm: (
265 (!blackMove.illegal || compatible(blackMove, whiteMove))
266 ? blackMove
267 : null
268 )
269 };
270 adjust(res);
271 return res;
272 }
273
274 play(move) {
275 const color = this.turn;
276 if (color == 'w')
277 this.whiteMove.push(move);
278 if (
279 move.vanish[0].p == 'p' && move.appear[0].p == 'p' &&
280 (
281 (color == 'w' && move.end.x == 0) ||
282 (color == 'b' && move.end.x == this.size.x - 1)
283 )
284 ) {
285 // Pawn on last rank : will relocate
286 this.subTurn = 2;
287 this.firstMove = move;
288 if (color == this.playerColor) {
289 this.playOnBoard(move);
290 this.playVisual(move);
291 }
292 return;
293 }
294 if (color == this.playerColor && this.firstMove) {
295 // The move was played on board: undo it
296 this.undoOnBoard(this.firstMove);
297 const revFirstMove = {
298 start: this.firstMove.end,
299 end: this.firstMove.start,
300 appear: this.firstMove.vanish,
301 vanish: this.firstMove.appear
302 };
303 this.playVisual(revFirstMove);
304 }
305 this.turn = C.GetOppCol(color);
306 this.movesCount++;
307 this.subTurn = 1;
308 this.firstMove = null;
309 if (color == 'b') {
310 // A full turn just ended
311 const res = this.resolveSynchroneMove(move);
312 const callback = () => {
313 // start + end don't matter for playOnBoard() and playVisual().
314 // Merging is necessary because moves may overlap.
315 let toPlay = {appear: [], vanish: []};
316 for (let c of ['w', 'b']) {
317 if (res[c + 'm']) {
318 Array.prototype.push.apply(toPlay.vanish, res[c + 'm'].vanish);
319 Array.prototype.push.apply(toPlay.appear, res[c + 'm'].appear);
320 }
321 }
322 this.playOnBoard(toPlay);
323 this.playVisual(toPlay);
324 };
325 if (res.wm)
326 this.animate(res.wm, () => {if (!res.bm) callback();});
327 if (res.bm)
328 this.animate(res.bm, callback);
329 if (!res.wm && !res.bm) {
330 this.displayIllegalInfo("both illegal");
331 ['w', 'b'].forEach(c => this.penalties[c]++);
332 }
333 else if (!res.wm) {
334 this.displayIllegalInfo("white illegal");
335 this.penalties['w']++;
336 }
337 else if (!res.bm) {
338 this.displayIllegalInfo("black illegal");
339 this.penalties['b']++;
340 }
341 this.whiteMove = [];
342 }
343 }
344
345 displayIllegalInfo(msg) {
346 super.displayMessage(null, msg, "illegal-text", 2000);
347 }
348
349 atLeastOneLegalMove(color) {
350 for (let i=0; i<this.size.x; i++) {
351 for (let j=0; j<this.size.y; j++) {
352 if (
353 this.board[i][j] != "" &&
354 this.getColor(i, j) == color &&
355 this.getPotentialMovesFrom([i, j]).some(m => !m.illegal)
356 ) {
357 return true;
358 }
359 }
360 }
361 return false;
362 }
363
364 getCurrentScore() {
365 if (this.turn == 'b') {
366 // Turn (white + black) not over yet.
367 // Could be stalemate if black cannot move (legally):
368 if (!this.atLeastOneLegalMove('b'))
369 return "1/2";
370 return "*";
371 }
372 // Count footmen: if a side has none, it loses
373 let fmCount = { 'w': 0, 'b': 0 };
374 for (let i=0; i<5; i++) {
375 for (let j=0; j<5; j++) {
376 if (this.board[i][j] != "" && this.getPiece(i, j) == 'p')
377 fmCount[this.getColor(i, j)]++;
378 }
379 }
380 if (Object.values(fmCount).some(v => v == 0)) {
381 if (fmCount['w'] == 0 && fmCount['b'] == 0)
382 // Everyone died
383 return "1/2";
384 if (fmCount['w'] == 0) return "0-1";
385 return "1-0"; //fmCount['b'] == 0
386 }
387 // Check penaltyFlags: if a side has 2 or more, it loses
388 if (Object.values(this.penalties).every(v => v == 2)) return "1/2";
389 if (this.penalties['w'] == 2) return "0-1";
390 if (this.penalties['b'] == 2) return "1-0";
391 if (!this.atLeastOneLegalMove('w') || !this.atLeastOneLegalMove('b'))
392 // Stalemate (should be very rare)
393 return "1/2";
394 return "*";
395 }
396
397 };