Commit | Line | Data |
---|---|---|
f31de5e4 | 1 | import ChessRules from "/base_rules.js"; |
462947f0 | 2 | import {ArrayFun} from "/utils/array.js"; |
f31de5e4 | 3 | |
98d14451 | 4 | export default class ApocalypseRules extends ChessRules { |
f31de5e4 BA |
5 | |
6 | static get Options() { | |
7 | return {}; | |
8 | } | |
9 | ||
98d14451 BA |
10 | get hasFlags() { |
11 | return false; | |
12 | } | |
13 | get hasEnpassant() { | |
14 | return false; | |
15 | } | |
16 | get hideMoves() { | |
17 | return true; | |
18 | } | |
19 | ||
f31de5e4 BA |
20 | get pawnPromotions() { |
21 | return ['n', 'p']; | |
22 | } | |
23 | ||
24 | get size() { | |
25 | return {x: 5, y: 5}; | |
26 | } | |
27 | ||
462947f0 BA |
28 | setOtherVariables(fenParsed) { |
29 | super.setOtherVariables(fenParsed); | |
98d14451 | 30 | // Often a simple move, but sometimes an array (pawn relocation) |
462947f0 BA |
31 | this.whiteMove = fenParsed.whiteMove != "-" |
32 | ? JSON.parse(fenParsed.whiteMove) | |
98d14451 BA |
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 | ); | |
462947f0 BA |
39 | } |
40 | ||
f31de5e4 BA |
41 | genRandInitBaseFen() { |
42 | return { | |
dc10e429 | 43 | fen: "npppn/p3p/5/P3P/NPPPN", |
98d14451 | 44 | o: {} |
f31de5e4 BA |
45 | }; |
46 | } | |
47 | ||
48 | getPartFen(o) { | |
98d14451 BA |
49 | return { |
50 | whiteMove: (o.init || !this.whiteMove) ? "-" : this.whiteMove, | |
51 | penalties: o.init ? "00" : Object.values(this.penalties).join("") | |
52 | }; | |
f31de5e4 BA |
53 | } |
54 | ||
f31de5e4 | 55 | getWhitemoveFen() { |
98d14451 BA |
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 | |
f31de5e4 BA |
61 | } |
62 | ||
462947f0 BA |
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. | |
f31de5e4 BA |
66 | pieces(color, x, y) { |
67 | const pawnShift = (color == "w" ? -1 : 1); | |
68 | return { | |
69 | 'p': { | |
70 | "class": "pawn", | |
9aebe2aa | 71 | both: [ |
f31de5e4 BA |
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 | ||
462947f0 BA |
82 | // Allow self-captures, because they might be valid |
83 | // if opponent takes on the same square (luck...) | |
f31de5e4 BA |
84 | canTake() { |
85 | return true; | |
86 | } | |
87 | ||
88 | getPotentialMovesFrom([x, y]) { | |
89 | let moves = []; | |
90 | if (this.subTurn == 2) { | |
98d14451 | 91 | const start = this.firstMove.end; |
f31de5e4 BA |
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 { | |
98d14451 BA |
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 | }); | |
f31de5e4 BA |
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 | ||
98d14451 BA |
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 | ||
f31de5e4 BA |
167 | filterValid(moves) { |
168 | // No checks: | |
169 | return moves; | |
170 | } | |
171 | ||
172 | // White and black (partial) moves were played: merge | |
f31de5e4 | 173 | resolveSynchroneMove(move) { |
98d14451 BA |
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 ( | |
b9877ed2 BA |
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 | |
98d14451 BA |
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) { | |
b9877ed2 BA |
241 | const vanishColor = (res.wm.vanish[0].p == 'n' ? 'b' : 'w'); |
242 | res[vanishColor + 'm'].appear.shift(); | |
98d14451 BA |
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; | |
f31de5e4 BA |
275 | } |
276 | ||
b9877ed2 | 277 | play(move, callback) { |
98d14451 BA |
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 | } | |
b9877ed2 | 295 | callback(); |
98d14451 | 296 | return; |
f31de5e4 | 297 | } |
98d14451 BA |
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); | |
f31de5e4 | 308 | } |
98d14451 BA |
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); | |
b9877ed2 | 316 | const afterAnimate = () => { |
98d14451 BA |
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); | |
b9877ed2 | 328 | callback(); |
98d14451 BA |
329 | }; |
330 | if (res.wm) | |
b9877ed2 | 331 | this.animate(res.wm, () => {if (!res.bm) afterAnimate();}); |
98d14451 | 332 | if (res.bm) |
b9877ed2 | 333 | this.animate(res.bm, afterAnimate); |
98d14451 BA |
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 = []; | |
f31de5e4 | 347 | } |
b9877ed2 BA |
348 | else |
349 | callback(); | |
f31de5e4 BA |
350 | } |
351 | ||
98d14451 BA |
352 | displayIllegalInfo(msg) { |
353 | super.displayMessage(null, msg, "illegal-text", 2000); | |
354 | } | |
462947f0 BA |
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 && | |
98d14451 | 362 | this.getPotentialMovesFrom([i, j]).some(m => !m.illegal) |
462947f0 BA |
363 | ) { |
364 | return true; | |
365 | } | |
366 | } | |
367 | } | |
368 | return false; | |
f31de5e4 BA |
369 | } |
370 | ||
371 | getCurrentScore() { | |
462947f0 BA |
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"; | |
f31de5e4 | 377 | return "*"; |
462947f0 | 378 | } |
f31de5e4 | 379 | // Count footmen: if a side has none, it loses |
b9877ed2 BA |
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++) { | |
98d14451 | 383 | if (this.board[i][j] != "" && this.getPiece(i, j) == 'p') |
f31de5e4 BA |
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 | |
98d14451 BA |
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"; | |
f31de5e4 BA |
398 | if (!this.atLeastOneLegalMove('w') || !this.atLeastOneLegalMove('b')) |
399 | // Stalemate (should be very rare) | |
400 | return "1/2"; | |
401 | return "*"; | |
402 | } | |
403 | ||
404 | }; |