"Your message": "Your message",
// Variants boxes:
+ "Ancient rules": "Ancient rules",
"Attract opposite king": "Attract opposite king",
"Balanced sliders & leapers": "Balanced sliders & leapers",
"Big board": "Big board",
"Captures reborn": "Captures reborn",
"Change colors": "Change colors",
"Dangerous collisions": "Dangerous collisions",
- "Exchange pieces positions": "Exchange pieces positions",
"Exotic captures": "Exotic captures",
"Explosive captures": "Explosive captures",
"In the shadow": "In the shadow",
+ "Give three checks": "Give three checks",
"Keep antiking in check": "Keep antiking in check",
"King crosses the board": "King crosses the board",
"Laws of attraction": "Laws of attraction",
"Lose all pieces": "Lose all pieces",
"Mate any piece": "Mate any piece",
+ "Middle battle": "Middle battle",
+ "Move like a knight": "Move like a knight",
"Move twice": "Move twice",
"Neverending rows": "Neverending rows",
"Pawns move diagonally": "Pawns move diagonally",
"Reverse captures": "Reverse captures",
"Run forward": "Run forward",
"Shared pieces": "Shared pieces",
+ "Shoot pieces": "Shoot pieces",
+ "Squares disappear": "Squares disappear",
"Standard rules": "Standard rules",
"Unidentified pieces": "Unidentified pieces"
};
"Your message": "Tu mensaje",
// Variants boxes:
+ "Ancient rules": "Viejas reglas",
"Attract opposite king": "Atraer al rey contrario",
"Balanced sliders & leapers": "Modos de desplazamiento equilibrados",
"Big board": "Gran tablero",
"Captures reborn": "Las capturas renacen",
"Change colors": "Cambiar colores",
"Dangerous collisions": "Colisiones peligrosas",
- "Exchange pieces positions": "Intercambiar las posiciones de las piezas",
"Exotic captures": "Capturas exóticas",
"Explosive captures": "Capturas explosivas",
"In the shadow": "En la sombra",
+ "Give three checks": "Dar tres jaques",
"Keep antiking in check": "Mantener el antirey en jaque",
"King crosses the board": "El rey cruza el tablero",
"Laws of attraction": "Las leyes de las atracciones",
"Lose all pieces": "Perder todas las piezas",
"Mate any piece": "Matar cualquier pieza",
+ "Middle battle": "Batalla media",
+ "Move like a knight": "Moverse como un caballo",
"Move twice": "Mover dos veces",
"Neverending rows": "Filas interminables",
"Pawns move diagonally": "Peones se mueven en diagonal",
"Reverse captures": "Capturas invertidas",
"Run forward": "Correr hacia adelante",
"Shared pieces": "Piezas compartidas",
+ "Shoot pieces": "Tirar de las piezas",
+ "Squares disappear": "Las casillas desaparecen",
"Standard rules": "Reglas estandar",
"Unidentified pieces": "Piezas no identificadas"
};
"Your message": "Votre message",
// Variants boxes:
+ "Ancient rules": "Règles anciennes",
"Attract opposite king": "Attirer le roi adverse",
"Balanced sliders & leapers": "Modes de déplacement équilibrés",
"Big board": "Grand échiquier",
"Captures reborn": "Les captures renaissent",
"Change colors": "Changer les couleurs",
"Dangerous collisions": "Collisions dangeureuses",
- "Exchange pieces positions": "Échangez les positions des pièces",
"Exotic captures": "Captures exotiques",
"Explosive captures": "Captures explosives",
"In the shadow": "Dans l'ombre",
+ "Give three checks": "Donnez trois échecs",
"Keep antiking in check": "Gardez l'antiroi en échec",
"King crosses the board": "Le roi traverse l'échiquier",
"Laws of attraction": "Les lois de l'attraction",
"Lose all pieces": "Perdez toutes les pièces",
"Mate any piece": "Mater n'importe quelle pièce",
+ "Middle battle": "Bataille du milieu",
+ "Move like a knight": "Bougez comme un cavalier",
"Move twice": "Jouer deux coups",
"Neverending rows": "Rangées sans fin",
"Pawns move diagonally": "Les pions vont en diagonale",
"Reverse captures": "Captures inversées",
"Run forward": "Courir vers l'avant",
"Shared pieces": "Pièces partagées",
+ "Shoot pieces": "Tirez sur les pièces",
+ "Squares disappear": "Les cases disparaissent",
"Standard rules": "Règles usuelles",
"Unidentified pieces": "Pièces non identifiées"
};
--- /dev/null
+import { ChessRules } from "@/base_rules";
+
+export const VariantRules = class ArenaRules extends ChessRules {
+ static get hasFlags() {
+ return false;
+ }
+
+ static GenRandInitFen() {
+ return ChessRules.GenRandInitFen().replace("w 1111 -", "w -");
+ }
+
+ static InArena(x) {
+ return Math.abs(3.5 - x) <= 1.5;
+ }
+
+ getPotentialMovesFrom([x, y]) {
+ const moves = super.getPotentialMovesFrom([x, y]);
+ // Eliminate moves which neither enter the arena or capture something
+ return moves.filter(m => {
+ const startInArena = V.InArena(m.start.x);
+ const endInArena = V.InArena(m.end.x);
+ return (
+ (startInArena && endInArena && m.vanish.length == 2) ||
+ (!startInArena && endInArena)
+ );
+ });
+
+ return moves;
+ }
+
+ getPotentialPawnMoves([x, y]) {
+ const color = this.turn;
+ let moves = [];
+ const [sizeX, sizeY] = [V.size.x, V.size.y];
+ const shiftX = color == "w" ? -1 : 1;
+ const startRank = color == "w" ? sizeX - 2 : 1;
+
+ if (this.board[x + shiftX][y] == V.EMPTY) {
+ // One square forward
+ moves.push(this.getBasicMove([x, y], [x + shiftX, y]));
+ // Next condition because pawns on 1st rank can generally jump
+ if (
+ x == startRank &&
+ this.board[x + 2 * shiftX][y] == V.EMPTY
+ ) {
+ // Two squares jump
+ moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y]));
+ }
+ }
+ // Captures
+ for (let shiftY of [-1, 1]) {
+ if (
+ y + shiftY >= 0 &&
+ y + shiftY < sizeY &&
+ this.board[x + shiftX][y + shiftY] != V.EMPTY &&
+ this.canTake([x, y], [x + shiftX, y + shiftY])
+ ) {
+ moves.push(this.getBasicMove([x, y], [x + shiftX, y + shiftY]));
+ }
+ }
+
+ // En passant
+ const Lep = this.epSquares.length;
+ const epSquare = this.epSquares[Lep - 1]; //always at least one element
+ if (
+ !!epSquare &&
+ epSquare.x == x + shiftX &&
+ Math.abs(epSquare.y - y) == 1
+ ) {
+ let enpassantMove = this.getBasicMove([x, y], [epSquare.x, epSquare.y]);
+ enpassantMove.vanish.push({
+ x: x,
+ y: epSquare.y,
+ p: "p",
+ c: this.getColor(x, epSquare.y)
+ });
+ moves.push(enpassantMove);
+ }
+
+ return moves;
+ }
+
+ getPotentialQueenMoves(sq) {
+ return this.getSlideNJumpMoves(
+ sq,
+ V.steps[V.ROOK].concat(V.steps[V.BISHOP])
+ ).filter(m => {
+ // Filter out moves longer than 3 squares
+ return Math.max(
+ Math.abs(m.end.x - m.start.x),
+ Math.abs(m.end.y - m.start.y)) <= 3;
+ });
+ }
+
+ getPotentialKingMoves(sq) {
+ return this.getSlideNJumpMoves(
+ sq,
+ V.steps[V.ROOK].concat(V.steps[V.BISHOP])
+ ).filter(m => {
+ // Filter out moves longer than 3 squares
+ return Math.max(
+ Math.abs(m.end.x - m.start.x),
+ Math.abs(m.end.y - m.start.y)) <= 3;
+ });
+ }
+
+ getCheckSquares() {
+ return [];
+ }
+
+ filterValid(moves) {
+ // No check conditions
+ return moves;
+ }
+
+ getCurrentScore() {
+ const color = this.turn;
+ if (!this.atLeastOneMove())
+ // I cannot move anymore
+ return color == "w" ? "0-1" : "1-0";
+ // Win if the opponent has no more pieces left (in the Arena),
+ // (and/)or if he lost both his dukes.
+ let someUnitRemain = false;
+ let atLeastOneDuke = false;
+ let somethingInArena = false;
+ outerLoop: for (let i=0; i<V.size.x; i++) {
+ for (let j=0; j<V.size.y; j++) {
+ if (this.getColor(i,j) == color) {
+ someUnitRemain = true;
+ if (this.movesCount >= 2 && V.InArena(i)) {
+ somethingInArena = true;
+ if (atLeastOneDuke)
+ break outerLoop;
+ }
+ if ([V.QUEEN,V.KING].includes(this.getPiece(i,j))) {
+ atLeastOneDuke = true;
+ if (this.movesCount < 2 || somethingInArena)
+ break outerLoop;
+ }
+ }
+ }
+ }
+ if (
+ !someUnitRemain ||
+ !atLeastOneDuke ||
+ (this.movesCount >= 2 && !somethingInArena)
+ ) {
+ return color == "w" ? "0-1" : "1-0";
+ }
+ return "*";
+ }
+};
--- /dev/null
+import { ChessRules } from "@/base_rules";
+
+export const VariantRules = class ChaturangaRules extends ChessRules {
+ static get HasFlags() {
+ return false;
+ }
+
+ static get HasEnpassant() {
+ return false;
+ }
+
+ static get ElephantSteps() {
+ return [
+ [-2, -2],
+ [-2, 2],
+ [2, -2],
+ [2, 2]
+ ];
+ }
+
+ static GenRandInitFen() {
+ return ChessRules.GenRandInitFen().replace("w 1111 -", "w");
+ }
+
+ getPotentialPawnMoves([x, y]) {
+ const color = this.turn;
+ let moves = [];
+ const [sizeX, sizeY] = [V.size.x, V.size.y];
+ const shiftX = color == "w" ? -1 : 1;
+ const startRank = color == "w" ? sizeX - 2 : 1;
+ const lastRank = color == "w" ? 0 : sizeX - 1;
+ // Promotion in minister (queen) only:
+ const finalPiece = x + shiftX == lastRank ? V.QUEEN : V.PAWN;
+
+ if (this.board[x + shiftX][y] == V.EMPTY) {
+ // One square forward
+ moves.push(
+ this.getBasicMove([x, y], [x + shiftX, y], {
+ c: color,
+ p: finalPiece
+ })
+ );
+ }
+ // Captures
+ for (let shiftY of [-1, 1]) {
+ if (
+ y + shiftY >= 0 &&
+ y + shiftY < sizeY &&
+ this.board[x + shiftX][y + shiftY] != V.EMPTY &&
+ this.canTake([x, y], [x + shiftX, y + shiftY])
+ ) {
+ moves.push(
+ this.getBasicMove([x, y], [x + shiftX, y + shiftY], {
+ c: color,
+ p: finalPiece
+ })
+ );
+ }
+ }
+
+ return moves;
+ }
+
+ getPotentialBishopMoves(sq) {
+ let moves = this.getSlideNJumpMoves(sq, V.ElephantSteps, "oneStep");
+ // Complete with "repositioning moves": like a queen, without capture
+ let repositioningMoves = this.getSlideNJumpMoves(
+ sq,
+ V.steps[V.BISHOP],
+ "oneStep"
+ ).filter(m => m.vanish.length == 1);
+ return moves.concat(repositioningMoves);
+ }
+
+ getPotentialQueenMoves(sq) {
+ return this.getSlideNJumpMoves(
+ sq,
+ V.steps[V.BISHOP],
+ "oneStep"
+ );
+ }
+
+ getPotentialKingMoves(sq) {
+ return this.getSlideNJumpMoves(
+ sq,
+ V.steps[V.ROOK].concat(V.steps[V.BISHOP]),
+ "oneStep"
+ );
+ }
+
+ isAttackedByBishop(sq, colors) {
+ return this.isAttackedBySlideNJump(
+ sq,
+ colors,
+ V.BISHOP,
+ V.ElephantSteps,
+ "oneStep"
+ );
+ }
+
+ isAttackedByQueen(sq, colors) {
+ return this.isAttackedBySlideNJump(
+ sq,
+ colors,
+ V.QUEEN,
+ V.steps[V.BISHOP],
+ "oneStep"
+ );
+ }
+
+ static get VALUES() {
+ return {
+ p: 1,
+ r: 5,
+ n: 3,
+ b: 2.5,
+ q: 2,
+ k: 1000
+ };
+ }
+};
if (this.kingPos[color][0] == 0)
// The opposing edge is reached!
return color == "w" ? "1-0" : "0-1";
- return "*";
+ if (this.atLeastOneMove())
+ return "*";
+ // Stalemate (will probably never happen)
+ return "1/2";
}
static get SEARCH_DEPTH() {
--- /dev/null
+import { ChessRules, PiPo, Move } from "@/base_rules";
+import { ArrayFun } from "@/utils/array";
+import { randInt } from "@/utils/alea";
+
+// TODO:
+
+export const VariantRules = class HiddenRules extends ChessRules {
+ static get HasFlags() {
+ return false;
+ }
+
+ static get HasEnpassant() {
+ return false;
+ }
+
+ // Analyse in Hidden mode makes no sense
+ static get CanAnalyze() {
+ return false;
+ }
+
+ // Moves are revealed only when game ends
+ static get ShowMoves() {
+ return "none";
+ }
+
+ static get HIDDEN_DECODE() {
+ return {
+ s: "p",
+ t: "q",
+ u: "r",
+ c: "b",
+ o: "n",
+ l: "k"
+ };
+ }
+ static get HIDDEN_CODE() {
+ return {
+ p: "s",
+ q: "t",
+ r: "u",
+ b: "c",
+ n: "o",
+ k: "l"
+ };
+ }
+
+ // Turn a hidden piece or revealed piece into revealed piece:
+ static Decode(p) {
+ if (Object.keys(V.HIDDEN_DECODE).includes(p))
+ return V.HIDDEN_DECODE[p];
+ return p;
+ }
+
+ static get PIECES() {
+ return ChessRules.PIECES.concat(Object.values(V.HIDDEN_CODE));
+ }
+
+ // Pieces can be hidden :)
+ getPiece(i, j) {
+ const piece = this.board[i][j].charAt(1);
+ if (Object.keys(V.HIDDEN_DECODE).includes(piece))
+ return V.HIDDEN_DECODE[piece];
+ return piece;
+ }
+
+ // Scan board for kings positions (no castling)
+ scanKingsRooks(fen) {
+ this.kingPos = { w: [-1, -1], b: [-1, -1] };
+ const fenRows = V.ParseFen(fen).position.split("/");
+ for (let i = 0; i < fenRows.length; i++) {
+ let k = 0; //column index on board
+ for (let j = 0; j < fenRows[i].length; j++) {
+ switch (fenRows[i].charAt(j)) {
+ case "k":
+ case "l":
+ this.kingPos["b"] = [i, k];
+ break;
+ case "K":
+ case "L":
+ this.kingPos["w"] = [i, k];
+ break;
+ default: {
+ const num = parseInt(fenRows[i].charAt(j));
+ if (!isNaN(num)) k += num - 1;
+ }
+ }
+ k++;
+ }
+ }
+ }
+
+ getPpath(b, color, score) {
+ if (Object.keys(V.HIDDEN_DECODE).includes(b[1])) {
+ // Supposed to be hidden.
+ if (score == "*" && (!color || color != b[0]))
+ return "Hidden/" + b[0] + "p";
+ // Else: condition OK to show the piece
+ return b[0] + V.HIDDEN_DECODE[b[1]];
+ }
+ // The piece is already not supposed to be hidden:
+ return b;
+ }
+
+ getBasicMove([sx, sy], [ex, ey], tr) {
+ if (
+ tr &&
+ Object.keys(V.HIDDEN_DECODE).includes(this.board[sx][sy].charAt(1))
+ ) {
+ // The transformed piece is a priori hidden
+ tr.p = V.HIDDEN_CODE[tr.p];
+ }
+ let mv = new Move({
+ appear: [
+ new PiPo({
+ x: ex,
+ y: ey,
+ c: tr ? tr.c : this.getColor(sx, sy),
+ p: tr ? tr.p : this.board[sx][sy].charAt(1)
+ })
+ ],
+ vanish: [
+ new PiPo({
+ x: sx,
+ y: sy,
+ c: this.getColor(sx, sy),
+ p: this.board[sx][sy].charAt(1)
+ })
+ ]
+ });
+
+ // The opponent piece disappears if we take it
+ if (this.board[ex][ey] != V.EMPTY) {
+ mv.vanish.push(
+ new PiPo({
+ x: ex,
+ y: ey,
+ c: this.getColor(ex, ey),
+ p: this.board[ex][ey].charAt(1)
+ })
+ );
+ // Pieces are revealed when they capture
+ mv.appear[0].p = V.Decode(mv.appear[0].p);
+ }
+
+ return mv;
+ }
+
+ // What are the king moves from square x,y ?
+ getPotentialKingMoves(sq) {
+ // No castling:
+ return this.getSlideNJumpMoves(
+ sq,
+ V.steps[V.ROOK].concat(V.steps[V.BISHOP]),
+ "oneStep"
+ );
+ }
+
+ filterValid(moves) {
+ return moves;
+ }
+
+ static GenRandInitFen() {
+ let pieces = { w: new Array(8), b: new Array(8) };
+ // Shuffle pieces + pawns on two first ranks
+ for (let c of ["w", "b"]) {
+ let positions = ArrayFun.range(16);
+
+ // Get random squares for bishops
+ let randIndex = 2 * randInt(8);
+ const bishop1Pos = positions[randIndex];
+ // The second bishop must be on a square of different color
+ let randIndex_tmp = 2 * randInt(8) + 1;
+ const bishop2Pos = positions[randIndex_tmp];
+ // Remove chosen squares
+ positions.splice(Math.max(randIndex, randIndex_tmp), 1);
+ positions.splice(Math.min(randIndex, randIndex_tmp), 1);
+
+ // Get random squares for knights
+ randIndex = randInt(14);
+ const knight1Pos = positions[randIndex];
+ positions.splice(randIndex, 1);
+ randIndex = randInt(13);
+ const knight2Pos = positions[randIndex];
+ positions.splice(randIndex, 1);
+
+ // Get random squares for rooks
+ randIndex = randInt(12);
+ const rook1Pos = positions[randIndex];
+ positions.splice(randIndex, 1);
+ randIndex = randInt(11);
+ const rook2Pos = positions[randIndex];
+ positions.splice(randIndex, 1);
+
+ // Get random square for queen
+ randIndex = randInt(10);
+ const queenPos = positions[randIndex];
+ positions.splice(randIndex, 1);
+
+ // Get random square for king
+ randIndex = randInt(9);
+ const kingPos = positions[randIndex];
+ positions.splice(randIndex, 1);
+
+ // Pawns position are all remaining slots:
+ for (let p of positions)
+ pieces[c][p] = "s";
+
+ // Finally put the shuffled pieces in the board array
+ pieces[c][rook1Pos] = "u";
+ pieces[c][knight1Pos] = "o";
+ pieces[c][bishop1Pos] = "c";
+ pieces[c][queenPos] = "t";
+ pieces[c][kingPos] = "l";
+ pieces[c][bishop2Pos] = "c";
+ pieces[c][knight2Pos] = "o";
+ pieces[c][rook2Pos] = "u";
+ }
+ let upFen = pieces["b"].join("");
+ upFen = upFen.substr(0,8) + "/" + upFen.substr(8).split("").reverse().join("");
+ let downFen = pieces["b"].join("").toUpperCase();
+ downFen = downFen.substr(0,8) + "/" + downFen.substr(8).split("").reverse().join("");
+ return upFen + "/8/8/8/8/" + downFen + " w 0";
+ }
+
+ getCheckSquares() {
+ return [];
+ }
+
+ updateVariables(move) {
+ super.updateVariables(move);
+ if (
+ move.vanish.length >= 2 &&
+ [V.KING,V.HIDDEN_CODE[V.KING]].includes(move.vanish[1].p)
+ ) {
+ // We took opponent king
+ this.kingPos[this.turn] = [-1, -1];
+ }
+ }
+
+ unupdateVariables(move) {
+ super.unupdateVariables(move);
+ const c = move.vanish[0].c;
+ const oppCol = V.GetOppCol(c);
+ if (this.kingPos[oppCol][0] < 0)
+ // Last move took opponent's king:
+ this.kingPos[oppCol] = [move.vanish[1].x, move.vanish[1].y];
+ }
+
+ getCurrentScore() {
+ const color = this.turn;
+ const kp = this.kingPos[color];
+ if (kp[0] < 0)
+ // King disappeared
+ return color == "w" ? "0-1" : "1-0";
+ // Assume that stalemate is impossible:
+ return "*";
+ }
+
+ getComputerMove() {
+ const color = this.turn;
+ let moves = this.getAllValidMoves();
+ for (let move of moves) {
+ move.eval = 0; //a priori...
+
+ // Can I take something ? If yes, do it with some probability
+ if (move.vanish.length == 2 && move.vanish[1].c != color) {
+ // OK this isn't a castling move
+ const myPieceVal = V.VALUES[move.appear[0].p];
+ const hisPieceVal = Object.keys(V.HIDDEN_DECODE).includes(move.vanish[1].p)
+ ? undefined
+ : V.VALUES[move.vanish[1].p];
+ if (!hisPieceVal) {
+ // Opponent's piece is unknown: do not take too much risk
+ move.eval = -myPieceVal + 1.5; //so that pawns always take
+ }
+ // Favor captures
+ else if (myPieceVal <= hisPieceVal)
+ move.eval = hisPieceVal - myPieceVal + 1;
+ else {
+ // Taking a pawn with minor piece,
+ // or minor piece or pawn with a rook,
+ // or anything but a queen with a queen,
+ // or anything with a king.
+ move.eval = hisPieceVal - myPieceVal;
+ }
+ } else {
+ // If no capture, favor small step moves,
+ // but sometimes move the knight anyway
+ const penalty = V.Decode(move.vanish[0].p) != V.KNIGHT
+ ? Math.abs(move.end.x - move.start.x) + Math.abs(move.end.y - move.start.y)
+ : (Math.random() < 0.5 ? 3 : 1);
+ move.eval -= penalty / (V.size.x + V.size.y - 1);
+ }
+
+ // TODO: also favor movements toward the center?
+ }
+
+ moves.sort((a, b) => b.eval - a.eval);
+ let candidates = [0];
+ for (let j = 1; j < moves.length && moves[j].eval == moves[0].eval; j++)
+ candidates.push(j);
+ return moves[candidates[randInt(candidates.length)]];
+ }
+
+ getNotation(move) {
+ // Translate final square
+ const finalSquare = V.CoordsToSquare(move.end);
+
+ const piece = this.getPiece(move.start.x, move.start.y);
+ if (piece == V.PAWN) {
+ // Pawn move
+ let notation = "";
+ if (move.vanish.length > move.appear.length) {
+ // Capture
+ const startColumn = V.CoordToColumn(move.start.y);
+ notation = startColumn + "x" + finalSquare;
+ }
+ else notation = finalSquare;
+ if (move.appear.length > 0 && !["p","s"].includes(move.appear[0].p)) {
+ // Promotion
+ const appearPiece = V.Decode(move.appear[0].p);
+ notation += "=" + appearPiece.toUpperCase();
+ }
+ return notation;
+ }
+ // Piece movement
+ return (
+ piece.toUpperCase() +
+ (move.vanish.length > move.appear.length ? "x" : "") +
+ finalSquare
+ );
+ }
+};
('Allmate', 'Mate any piece'),
('Antiking', 'Keep antiking in check'),
('Antimatter', 'Dangerous collisions'),
+ ('Arena', 'Middle battle'),
('Atomic', 'Explosive captures'),
('Baroque', 'Exotic captures'),
('Benedict', 'Change colors'),
('Berolina', 'Pawns move diagonally'),
+ ('Chaturanga', 'Ancient rules'),
('Checkered', 'Shared pieces'),
+ ('Check3', 'Give three checks'),
('Chess960', 'Standard rules'),
('Circular', 'Run forward'),
('Crazyhouse', 'Captures reborn'),
('Extinction', 'Capture all of a kind'),
('Grand', 'Big board'),
('Hidden', 'Unidentified pieces'),
+ ('Knightrelay', 'Move like a knight'),
('Losers', 'Lose all pieces'),
('Magnetic', 'Laws of attraction'),
('Marseille', 'Move twice'),
+ ('Rifle', 'Shoot pieces'),
('Royalrace', 'King crosses the board'),
('Recycle', 'Reuse pieces'),
('Suction', 'Attract opposite king'),
('Upsidedown', 'Board upside down'),
('Wildebeest', 'Balanced sliders & leapers'),
+ ('Wormhole', 'Squares disappear'),
('Zen', 'Reverse captures');