Saving Chakart state
[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 // TODO: rethink from here:
179
180 // allow pawns
181 // queen invisible move, king shell: special functions
182
183 // prevent pawns from capturing invisible queen (post)
184 // post-process:
185
186 //events : playPlusVisual after mouse up, playReceived (include animation) on opp move
187 // ==> if move.cont (banana...) self re-call playPlusVisual (rec ?)
188
189 // Moving something. Potential effects resolved after playing
190 getPotentialMovesFrom([x, y], bonus) {
191 let moves = [];
192 if (bonus == "toadette")
193 return this.getDropMovesFrom([x, y]);
194 else if (bonus == "kingboo") {
195 const initPiece = this.getPiece(x, y);
196 const color = this.getColor(x, y);
197 const oppCol = C.GetOppCol(color);
198 // Only allow to swap pieces (TODO: restrict for pawns)
199 for (let i=0; i<this.size.x; i++) {
200 for (let j=0; j<this.size.y; j++) {
201 if ((i != x || j != y) && this.board[i][j] != "") {
202 const pstart = new PiPo({x: x, y: y, p: initPiece, c: color});
203 const pend =
204 let m = this.getBasicMove([x, y], [i, j]);
205 m.appear.push(
206 new PiPo({x: x, y: y, p: this.getPiece(i, j), c: oppCol}));
207 moves.push(m);
208 }
209 }
210 }
211 return moves;
212 }
213 // Normal case (including bonus daisy)
214 switch (this.getPiece(x, y)) {
215 case 'p':
216 moves = this.getPawnMovesFrom([x, y]); //apply promotions
217 // TODO: add mushroom on init square
218 break;
219 case 'q':
220 moves = this.getQueenMovesFrom([x, y]);
221 break;
222 case 'k',
223 moves = this.getKingMovesFrom([x, y]);
224 break;
225 case 'n':
226 moves = super.getPotentialMovesFrom([x, y]);
227 // TODO: add egg on init square
228 break;
229 default:
230 moves = super.getPotentialMovesFrom([x, y]);
231 }
232 return moves;
233 }
234
235 getPawnMovesFrom([x, y]) {
236 const color = this.turn;
237 const oppCol = C.GetOppCol(color);
238 const shiftX = (color == 'w' ? -1 : 1);
239 const firstRank = (color == "w" ? this.size.x - 1 : 0);
240 let moves = [];
241 if (
242 this.board[x + shiftX][y] == "" ||
243 this.getColor(x + shiftX, y) == 'a' ||
244 this.getPiece(x + shiftX, y) == V.INVISIBLE_QUEEN
245 ) {
246
247 // TODO:
248 this.addPawnMoves([x, y], [x + shiftX, y], moves);
249 if (
250 [firstRank, firstRank + shiftX].includes(x) &&
251 (
252 this.board[x + 2 * shiftX][y] == V.EMPTY ||
253 this.getColor(x + 2 * shiftX, y) == 'a' ||
254 this.getPiece(x + 2 * shiftX, y) == V.INVISIBLE_QUEEN
255 )
256 ) {
257 moves.push(this.getBasicMove({ x: x, y: y }, [x + 2 * shiftX, y]));
258 }
259 }
260 for (let shiftY of [-1, 1]) {
261 if (
262 y + shiftY >= 0 &&
263 y + shiftY < sizeY &&
264 this.board[x + shiftX][y + shiftY] != V.EMPTY &&
265 // Pawns cannot capture invisible queen this way!
266 this.getPiece(x + shiftX, y + shiftY) != V.INVISIBLE_QUEEN &&
267 ['a', oppCol].includes(this.getColor(x + shiftX, y + shiftY))
268 ) {
269 this.addPawnMoves([x, y], [x + shiftX, y + shiftY], moves);
270 }
271 }
272 return moves;
273 }
274
275 getQueenMovesFrom(sq) {
276 const normalMoves = super.getPotentialQueenMoves(sq);
277 // If flag allows it, add 'invisible movements'
278 let invisibleMoves = [];
279 if (this.powerFlags[this.turn][V.QUEEN]) {
280 normalMoves.forEach(m => {
281 if (
282 m.appear.length == 1 &&
283 m.vanish.length == 1 &&
284 // Only simple non-capturing moves:
285 m.vanish[0].c != 'a'
286 ) {
287 let im = JSON.parse(JSON.stringify(m));
288 im.appear[0].p = V.INVISIBLE_QUEEN;
289 im.end.noHighlight = true;
290 invisibleMoves.push(im);
291 }
292 });
293 }
294 return normalMoves.concat(invisibleMoves);
295 }
296
297 getKingMovesFrom([x, y]) {
298 let moves = super.getPotentialKingMoves([x, y]);
299 const color = this.turn;
300 // If flag allows it, add 'remote shell captures'
301 if (this.powerFlags[this.turn][V.KING]) {
302 V.steps[V.ROOK].concat(V.steps[V.BISHOP]).forEach(step => {
303 let [i, j] = [x + step[0], y + step[1]];
304 while (
305 V.OnBoard(i, j) &&
306 (
307 this.board[i][j] == V.EMPTY ||
308 this.getPiece(i, j) == V.INVISIBLE_QUEEN ||
309 (
310 this.getColor(i, j) == 'a' &&
311 [V.EGG, V.MUSHROOM].includes(this.getPiece(i, j))
312 )
313 )
314 ) {
315 i += step[0];
316 j += step[1];
317 }
318 if (V.OnBoard(i, j)) {
319 const colIJ = this.getColor(i, j);
320 if (colIJ != color) {
321 // May just destroy a bomb or banana:
322 moves.push(
323 new Move({
324 start: { x: x, y: y},
325 end: { x: i, y: j },
326 appear: [],
327 vanish: [
328 new PiPo({
329 x: i, y: j, c: colIJ, p: this.getPiece(i, j)
330 })
331 ]
332 })
333 );
334 }
335 }
336 });
337 }
338 return moves;
339 }
340
341 // TODO: can merge prePlay into play() ==> no need to distinguish
342 /// if any of my pieces was immobilized, it's not anymore.
343 //if play set a piece immobilized, then mark it
344 prePlay(move) {
345 if (move.effect == "toadette")
346 this.reserve = this.captured;
347 else
348 this.reserve = { w: {}, b: {} };;
349 const color = this.turn;
350 if (
351 move.vanish.length == 2 &&
352 move.vanish[1].c != 'a' &&
353 move.appear.length == 1 //avoid king Boo!
354 ) {
355 // Capture: update this.captured
356 let capturedPiece = move.vanish[1].p;
357 if (capturedPiece == V.INVISIBLE_QUEEN)
358 capturedPiece = V.QUEEN;
359 else if (Object.keys(V.IMMOBILIZE_DECODE).includes(capturedPiece))
360 capturedPiece = V.IMMOBILIZE_DECODE[capturedPiece];
361 this.captured[move.vanish[1].c][capturedPiece]++;
362 }
363 else if (move.vanish.length == 0) {
364 if (move.appear.length == 0 || move.appear[0].c == 'a') return;
365 // A piece is back on board
366 this.captured[move.appear[0].c][move.appear[0].p]--;
367 }
368 if (move.appear.length == 0) {
369 // Three cases: king "shell capture", Chomp or Koopa
370 if (this.getPiece(move.start.x, move.start.y) == V.KING)
371 // King remote capture:
372 this.powerFlags[color][V.KING] = false;
373 else if (move.end.effect == "chomp")
374 this.captured[color][move.vanish[0].p]++;
375 }
376 else if (move.appear[0].p == V.INVISIBLE_QUEEN)
377 this.powerFlags[move.appear[0].c][V.QUEEN] = false;
378 if (this.subTurn == 2) return;
379 if (
380 move.turn[1] == 1 &&
381 move.appear.length == 0 ||
382 !(Object.keys(V.IMMOBILIZE_DECODE).includes(move.appear[0].p))
383 ) {
384 // Look for an immobilized piece of my color: it can now move
385 for (let i=0; i<8; i++) {
386 for (let j=0; j<8; j++) {
387 if (this.board[i][j] != V.EMPTY) {
388 const piece = this.getPiece(i, j);
389 if (
390 this.getColor(i, j) == color &&
391 Object.keys(V.IMMOBILIZE_DECODE).includes(piece)
392 ) {
393 this.board[i][j] = color + V.IMMOBILIZE_DECODE[piece];
394 move.wasImmobilized = [i, j];
395 }
396 }
397 }
398 }
399 }
400 // Also make opponent invisible queen visible again, if any
401 const oppCol = V.GetOppCol(color);
402 for (let i=0; i<8; i++) {
403 for (let j=0; j<8; j++) {
404 if (
405 this.board[i][j] != V.EMPTY &&
406 this.getColor(i, j) == oppCol &&
407 this.getPiece(i, j) == V.INVISIBLE_QUEEN
408 ) {
409 this.board[i][j] = oppCol + V.QUEEN;
410 move.wasInvisible = [i, j];
411 }
412 }
413 }
414 }
415
416 play(move) {
417 this.prePlay(move);
418 this.playOnBoard(move);
419 if (["kingboo", "toadette", "daisy"].includes(move.effect)) {
420 this.effect = move.effect;
421 this.subTurn = 2;
422 }
423 else {
424 this.turn = C.GetOppCol(this.turn);
425 this.movesCount++;
426 this.subTurn = 1;
427 }
428 }
429
430 filterValid(moves) {
431 return moves;
432 }
433
434 // idée : on joue le coup, puis son effet est déterminé, puis la suite (si suite)
435 // est jouée automatiquement ou demande action utilisateur, etc jusqu'à coup terminal.
436 tryMoveFollowup(move, cb) {
437 if (this.getColor(move.end.x, move.end.y) == 'a') {
438 // effect, or bonus/malus
439 const endType = this.getPiece(m.end.x, m.end.y);
440 switch (endType) {
441 case V.EGG:
442 this.applyRandomBonus(move, cb);
443 break;
444 case V.BANANA:
445 case V.BOMB: {
446 const dest =
447 this.getRandomSquare([m.end.x, m.end.y],
448 endType == V.BANANA
449 ? [[1, 1], [1, -1], [-1, 1], [-1, -1]]
450 : [[1, 0], [-1, 0], [0, 1], [0, -1]]);
451 const nextMove = this.getBasicMove([move.end.x, move.end.y], dest);
452 cb(nextMove);
453 break;
454 }
455 case V.MUSHROOM:
456 // aller dans direction, saut par dessus pièce adverse
457 // ou amie (tjours), new step si roi caval pion
458 break;
459 }
460 }
461 }
462
463 applyRandomBonnus(move, cb) {
464 // TODO: determine bonus/malus, and then
465 }
466
467 // Helper to apply banana/bomb effect
468 getRandomSquare([x, y], steps) {
469 const validSteps = steps.filter(s => this.onBoard(x + s[0], y + s[1]));
470 const step = validSteps[Random.randInt(validSteps.length)];
471 return [x + step[0], y + step[1]];
472 }
473 // TODO: turn change indicator ?!
474 playPlusVisual(move, r) {
475 this.moveStack.push(move);
476 this.play(move);
477 this.playVisual(move, r);
478 if (move.bonus)
479 alert(move.bonus); //TODO: nicer display
480 this.tryMoveFollowup(move, (nextMove) => {
481 if (nextMove)
482 this.playPlusVisual(nextMove, r);
483 else
484 this.afterPlay(this.moveStack);
485 });
486 }
487
488 };