Some progress on Chakart
[xogo.git] / variants / Chakart / class.js
1 import ChessRules from "/base_rules";
2 import GiveawayRules from "/variants/Giveaway";
3 import { ArrayFun } from "/utils/array.js";
4 import { Random } from "/utils/alea.js";
5 import PiPo from "/utils/PiPo.js";
6 import Move from "/utils/Move.js";
7
8 export class ChakartRules extends ChessRules {
9
10 static get Options() {
11 return {
12 select: [
13 {
14 label: "Randomness",
15 variable: "randomness",
16 defaut: 2,
17 options: [
18 {label: "Deterministic", value: 0},
19 {label: "Symmetric random", value: 1},
20 {label: "Asymmetric random", value: 2}
21 ]
22 }
23 ]
24 };
25 }
26
27 get pawnPromotions() {
28 return ['q', 'r', 'n', 'b', 'k'];
29 }
30
31 get hasCastle() {
32 return false;
33 }
34 get hasEnpassant() {
35 return false;
36 }
37
38 static get IMMOBILIZE_CODE() {
39 return {
40 'p': 's',
41 'r': 'u',
42 'n': 'o',
43 'b': 'c',
44 'q': 't',
45 'k': 'l'
46 };
47 }
48
49 static get IMMOBILIZE_DECODE() {
50 return {
51 's': 'p',
52 'u': 'r',
53 'o': 'n',
54 'c': 'b',
55 't': 'q',
56 'l': 'k'
57 };
58 }
59
60 static get INVISIBLE_QUEEN() {
61 return 'i';
62 }
63
64 // Fictive color 'a', bomb banana mushroom egg
65 static get BOMB() {
66 return 'w'; //"Wario"
67 }
68 static get BANANA() {
69 return 'd'; //"Donkey"
70 }
71 static get EGG() {
72 return 'e';
73 }
74 static get MUSHROOM() {
75 return 'm';
76 }
77
78 genRandInitFen(seed) {
79 const gr = new GiveawayRules({mode: "suicide"}, true);
80 return (
81 gr.genRandInitFen(seed).slice(0, -1) +
82 // Add Peach + Mario flags + capture counts
83 '{"flags":"1111","ccount":"000000000000"}'
84 );
85 }
86
87 fen2board(f) {
88 return (
89 f.charCodeAt() <= 90
90 ? "w" + f.toLowerCase()
91 : (['w', 'd', 'e', 'm'].includes(f) ? "a" : "b") + f
92 );
93 }
94
95 setFlags(fenflags) {
96 // King can send shell? Queen can be invisible?
97 this.powerFlags = {
98 w: {k: false, q: false},
99 b: {k: false, q: false}
100 };
101 for (let c of ['w', 'b']) {
102 for (let p of ['k', 'q']) {
103 this.powerFlags[c][p] =
104 fenflags.charAt((c == "w" ? 0 : 2) + (p == 'k' ? 0 : 1)) == "1";
105 }
106 }
107 }
108
109 aggregateFlags() {
110 return this.powerFlags;
111 }
112
113 disaggregateFlags(flags) {
114 this.powerFlags = flags;
115 }
116
117 getFen() {
118 return super.getFen() + " " + this.getCapturedFen();
119 }
120
121 getFlagsFen() {
122 return ['w', 'b'].map(c => {
123 return ['k', 'q'].map(p => this.powerFlags[c][p] ? "1" : "0").join("");
124 }).join("");
125 }
126
127 getCapturedFen() {
128 const res = ['w', 'b'].map(c => {
129 Object.values(this.captured[c])
130 });
131 return res[0].concat(res[1]).join("");
132 }
133
134 setOtherVariables(fenParsed) {
135 super.setOtherVariables(fenParsed);
136 // Initialize captured pieces' counts from FEN
137 const allCapts = fenParsed.captured.split("").map(x => parseInt(x, 10));
138 const pieces = ['p', 'r', 'n', 'b', 'q', 'k'];
139 this.captured = {
140 w: Array.toObject(pieces, allCapts.slice(0, 6)),
141 b: Array.toObject(pieces, allCapts.slice(6, 12))
142 };
143 this.reserve = { w: {}, b: {} }; //to be replaced by this.captured
144 this.moveStack = [];
145 }
146
147 // For Toadette bonus
148 getDropMovesFrom([c, p]) {
149 if (typeof c != "string" || this.reserve[c][p] == 0)
150 return [];
151 let moves = [];
152 const start = (c == 'w' && p == 'p' ? 1 : 0);
153 const end = (color == 'b' && p == 'p' ? 7 : 8);
154 for (let i = start; i < end; i++) {
155 for (let j = 0; j < this.size.y; j++) {
156 const pieceIJ = this.getPiece(i, j);
157 if (
158 this.board[i][j] == "" ||
159 this.getColor(i, j) == 'a' ||
160 pieceIJ == V.INVISIBLE_QUEEN
161 ) {
162 let m = new Move({
163 start: {x: c, y: p},
164 end: {x: i, y: j},
165 appear: [new PiPo({x: i, y: j, c: c, p: p})],
166 vanish: []
167 });
168 // A drop move may remove a bonus (or hidden queen!)
169 if (this.board[i][j] != "")
170 m.vanish.push(new PiPo({x: i, y: j, c: 'a', p: pieceIJ}));
171 moves.push(m);
172 }
173 }
174 }
175 return moves;
176 }
177
178 // Moving something. Potential effects resolved after playing
179 getPotentialMovesFrom([x, y], bonus) {
180 let moves = [];
181 if (bonus == "toadette")
182 return this.getDropMovesFrom([x, y]);
183 if (bonus == "kingboo") {
184 const initPiece = this.getPiece(x, y);
185 const color = this.getColor(x, y);
186 const oppCol = C.GetOppCol(color);
187 // Only allow to swap pieces (TODO: restrict for pawns)
188 for (let i=0; i<this.size.x; i++) {
189 for (let j=0; j<this.size.y; j++) {
190 if ((i != x || j != y) && this.board[i][j] != "") {
191 let m = this.getBasicMove([x, y], [i, j]);
192 m.appear.push(
193 new PiPo({x: x, y: y, p: this.getPiece(i, j), c: oppCol}));
194 moves.push(m);
195 }
196 }
197 }
198 return moves;
199 }
200 // Normal case (including bonus daisy)
201 switch (this.getPiece(x, y)) {
202 case 'p':
203 moves = this.getPawnMovesFrom([x, y]); //apply promotions
204 break;
205 case 'q':
206 moves = this.getQueenMovesFrom([x, y]);
207 break;
208 case 'k',
209 moves = this.getKingMovesFrom([x, y]);
210 break;
211 case 'n':
212 moves = this.getKnightMovesFrom([x, y]);
213 break;
214 case 'b':
215 case 'r':
216 // explicitely listing types to avoid moving immobilized piece
217 moves = super.getPotentialMovesFrom([x, y]);
218 }
219 return moves;
220 }
221
222 getPawnMovesFrom([x, y]) {
223 const color = this.turn;
224 const oppCol = C.GetOppCol(color);
225 const shiftX = (color == 'w' ? -1 : 1);
226 const firstRank = (color == "w" ? this.size.x - 1 : 0);
227 let moves = [];
228 if (
229 this.board[x + shiftX][y] == "" ||
230 this.getColor(x + shiftX, y) == 'a' ||
231 this.getPiece(x + shiftX, y) == V.INVISIBLE_QUEEN
232 ) {
233 moves.push(this.getBasicMove([x, y], [x + shiftX, y]));
234 if (
235 [firstRank, firstRank + shiftX].includes(x) &&
236 (
237 this.board[x + 2 * shiftX][y] == "" ||
238 this.getColor(x + 2 * shiftX, y) == 'a' ||
239 this.getPiece(x + 2 * shiftX, y) == V.INVISIBLE_QUEEN
240 )
241 ) {
242 moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y]));
243 }
244 }
245 for (let shiftY of [-1, 1]) {
246 if (
247 y + shiftY >= 0 &&
248 y + shiftY < sizeY &&
249 this.board[x + shiftX][y + shiftY] != "" &&
250 // Pawns cannot capture invisible queen this way!
251 this.getPiece(x + shiftX, y + shiftY) != V.INVISIBLE_QUEEN &&
252 ['a', oppCol].includes(this.getColor(x + shiftX, y + shiftY))
253 ) {
254 moves.push(this.getBasicMove([x, y], [x + shiftX, y + shiftY]));
255 }
256 }
257 super.pawnPostProcess(moves, color, oppCol);
258 return moves;
259 }
260
261 getQueenMovesFrom(sq) {
262 const normalMoves = super.getPotentialMovesOf('q', sq);
263 // If flag allows it, add 'invisible movements'
264 let invisibleMoves = [];
265 if (this.powerFlags[this.turn]['q']) {
266 normalMoves.forEach(m => {
267 if (
268 m.appear.length == 1 &&
269 m.vanish.length == 1 &&
270 // Only simple non-capturing moves:
271 m.vanish[0].c != 'a'
272 ) {
273 let im = JSON.parse(JSON.stringify(m));
274 im.appear[0].p = V.INVISIBLE_QUEEN;
275 im.end.noHighlight = true;
276 invisibleMoves.push(im);
277 }
278 });
279 }
280 return normalMoves.concat(invisibleMoves);
281 }
282
283 getKingMovesFrom([x, y]) {
284 let moves = super.getPotentialMovesOf('k', [x, y]);
285 // If flag allows it, add 'remote shell captures'
286 if (this.powerFlags[this.turn]['k']) {
287 super.pieces()['k'].moves[0].steps.forEach(step => {
288 let [i, j] = [x + step[0], y + step[1]];
289 while (
290 this.onBoard(i, j) &&
291 (
292 this.board[i][j] == "" ||
293 this.getPiece(i, j) == V.INVISIBLE_QUEEN ||
294 (
295 this.getColor(i, j) == 'a' &&
296 [V.EGG, V.MUSHROOM].includes(this.getPiece(i, j))
297 )
298 )
299 ) {
300 i += step[0];
301 j += step[1];
302 }
303 if (this.onBoard(i, j)) {
304 const colIJ = this.getColor(i, j);
305 if (colIJ != this.turn) {
306 // May just destroy a bomb or banana:
307 moves.push(
308 new Move({
309 start: {x: x, y: y},
310 end: {x: i, y: j},
311 appear: [],
312 vanish: [
313 new PiPo({x: i, y: j, c: colIJ, p: this.getPiece(i, j)})
314 ]
315 })
316 );
317 }
318 }
319 });
320 }
321 return moves;
322 }
323
324 getKnightMovesFrom([x, y]) {
325 // Add egg on initial square:
326 return super.getPotentialMovesOf('n', [x, y]).map(m => {
327 m.appear.push(new PiPo({p: "e", c: "a", x: x, y: y}));
328 return m;
329 });
330 }
331
332 /// if any of my pieces was immobilized, it's not anymore.
333 //if play set a piece immobilized, then mark it
334 play(move) {
335 if (move.effect == "toadette") {
336 this.reserve = this.captured;
337 this.re_drawReserve([this.turn]);
338 }
339 else if (this.reserve) {
340 this.reserve = { w: {}, b: {} };
341 this.re_drawReserve([this.turn]);
342 }
343 const color = this.turn;
344 if (
345 move.vanish.length == 2 &&
346 move.vanish[1].c != 'a' &&
347 move.appear.length == 1 //avoid king Boo!
348 ) {
349 // Capture: update this.captured
350 let capturedPiece = move.vanish[1].p;
351 if (capturedPiece == V.INVISIBLE_QUEEN)
352 capturedPiece = V.QUEEN;
353 else if (Object.keys(V.IMMOBILIZE_DECODE).includes(capturedPiece))
354 capturedPiece = V.IMMOBILIZE_DECODE[capturedPiece];
355 this.captured[move.vanish[1].c][capturedPiece]++;
356 }
357 else if (move.vanish.length == 0) {
358 if (move.appear.length == 0 || move.appear[0].c == 'a')
359 return;
360 // A piece is back on board
361 this.captured[move.appear[0].c][move.appear[0].p]--;
362 }
363 if (move.appear.length == 0) {
364 // Three cases: king "shell capture", Chomp or Koopa
365 if (this.getPiece(move.start.x, move.start.y) == V.KING)
366 // King remote capture:
367 this.powerFlags[color][V.KING] = false;
368 else if (move.end.effect == "chomp")
369 this.captured[color][move.vanish[0].p]++;
370 }
371 else if (move.appear[0].p == V.INVISIBLE_QUEEN)
372 this.powerFlags[move.appear[0].c][V.QUEEN] = false;
373 if (this.subTurn == 2) return;
374 if (
375 move.turn[1] == 1 &&
376 move.appear.length == 0 ||
377 !(Object.keys(V.IMMOBILIZE_DECODE).includes(move.appear[0].p))
378 ) {
379 // Look for an immobilized piece of my color: it can now move
380 for (let i=0; i<8; i++) {
381 for (let j=0; j<8; j++) {
382 if (this.board[i][j] != V.EMPTY) {
383 const piece = this.getPiece(i, j);
384 if (
385 this.getColor(i, j) == color &&
386 Object.keys(V.IMMOBILIZE_DECODE).includes(piece)
387 ) {
388 this.board[i][j] = color + V.IMMOBILIZE_DECODE[piece];
389 move.wasImmobilized = [i, j];
390 }
391 }
392 }
393 }
394 }
395 // Also make opponent invisible queen visible again, if any
396 const oppCol = V.GetOppCol(color);
397 for (let i=0; i<8; i++) {
398 for (let j=0; j<8; j++) {
399 if (
400 this.board[i][j] != V.EMPTY &&
401 this.getColor(i, j) == oppCol &&
402 this.getPiece(i, j) == V.INVISIBLE_QUEEN
403 ) {
404 this.board[i][j] = oppCol + V.QUEEN;
405 move.wasInvisible = [i, j];
406 }
407 }
408 }
409 this.playOnBoard(move);
410 if (["kingboo", "toadette", "daisy"].includes(move.effect)) {
411 this.effect = move.effect;
412 this.subTurn = 2;
413 }
414 else {
415 this.turn = C.GetOppCol(this.turn);
416 this.movesCount++;
417 this.subTurn = 1;
418 }
419 }
420
421 filterValid(moves) {
422 return moves;
423 }
424
425 // idée : on joue le coup, puis son effet est déterminé, puis la suite (si suite)
426 // est jouée automatiquement ou demande action utilisateur, etc jusqu'à coup terminal.
427 tryMoveFollowup(move, cb) {
428 if (this.getColor(move.end.x, move.end.y) == 'a') {
429 // effect, or bonus/malus
430 const endType = this.getPiece(m.end.x, m.end.y);
431 switch (endType) {
432 case V.EGG:
433 this.applyRandomBonus(move, cb);
434 break;
435 case V.BANANA:
436 case V.BOMB: {
437 const dest =
438 this.getRandomSquare([m.end.x, m.end.y],
439 endType == V.BANANA
440 ? [[1, 1], [1, -1], [-1, 1], [-1, -1]]
441 : [[1, 0], [-1, 0], [0, 1], [0, -1]]);
442 const nextMove = this.getBasicMove([move.end.x, move.end.y], dest);
443 cb(nextMove);
444 break;
445 }
446 case V.MUSHROOM:
447 // aller dans direction, saut par dessus pièce adverse
448 // ou amie (tjours), new step si roi caval pion
449 break;
450 }
451 }
452 }
453
454 playVisual(move, r) {
455 super.playVisual(move, r);
456 if (move.bonus)
457 alert(move.bonus); //TODO: nicer display
458 }
459
460 applyRandomBonus(move, cb) {
461 // TODO: determine bonus/malus, and then
462 }
463
464 // Helper to apply banana/bomb effect
465 getRandomSquare([x, y], steps) {
466 const validSteps = steps.filter(s => this.onBoard(x + s[0], y + s[1]));
467 const step = validSteps[Random.randInt(validSteps.length)];
468 return [x + step[0], y + step[1]];
469 }
470
471 // Warning: if play() is called, then move.end changed.
472 playPlusVisual(move, r) {
473 this.moveStack.push(move);
474 this.play(move);
475 this.playVisual(move, r);
476 this.tryMoveFollowup(move, (nextMove) => {
477 if (nextMove)
478 this.playPlusVisual(nextMove, r);
479 else
480 this.afterPlay(this.moveStack);
481 });
482 }
483
484 };