From: Benjamin Auder Date: Thu, 30 Jan 2025 11:00:00 +0000 (+0100) Subject: Some further transformation on Dynamo, prepare next variants too X-Git-Url: https://git.auder.net/game/doc/html/scripts/%7B%7B%20path('mixstore_static_about')%20%7D%7D?a=commitdiff_plain;p=xogo.git Some further transformation on Dynamo, prepare next variants too --- diff --git a/variants/Dynamo/class.js b/variants/Dynamo/class.js index ed0ea27..7b835e0 100644 --- a/variants/Dynamo/class.js +++ b/variants/Dynamo/class.js @@ -307,7 +307,7 @@ export default class DynamoRules extends ChessRules { const dir = this.getNormalizedDirection( [fm.start.x - x, fm.start.y - y]); const nbSteps = - [V.PAWN, V.KING, V.KNIGHT].includes(piece) + ['p', 'k', 'n'].includes(piece) ? 1 : null; return this.getMovesInDirection([x, y], dir, nbSteps); @@ -327,7 +327,7 @@ export default class DynamoRules extends ChessRules { const deltaX = Math.abs(fm.start.x - x); const deltaY = Math.abs(fm.start.y - y); switch (piece) { - case V.PAWN: + case 'p': if (x == pawnStartRank) { if ( (fm.start.x - x) * pawnShift < 0 || @@ -354,7 +354,7 @@ export default class DynamoRules extends ChessRules { } } break; - case V.KNIGHT: + case 'n': if ( (deltaX + deltaY != 3 || (deltaX == 0 && deltaY == 0)) || (fm.end.x - fm.start.x != fm.start.x - x) || @@ -363,7 +363,7 @@ export default class DynamoRules extends ChessRules { return []; } break; - case V.KING: + case 'k': if ( (deltaX >= 2 || deltaY >= 2) || (fm.end.x - fm.start.x != fm.start.x - x) || @@ -372,15 +372,15 @@ export default class DynamoRules extends ChessRules { return []; } break; - case V.BISHOP: + case 'b': if (deltaX != deltaY) return []; break; - case V.ROOK: + case 'r': if (deltaX != 0 && deltaY != 0) return []; break; - case V.QUEEN: + case 'q': if (deltaX != deltaY && deltaX != 0 && deltaY != 0) return []; break; @@ -404,7 +404,7 @@ export default class DynamoRules extends ChessRules { // Note: kings cannot suicide, so fm.vanish[0].p is not KING. // Could be PAWN though, if a pawn was pushed out of board. if ( - fm.vanish[0].p != V.PAWN && //pawns cannot pull + fm.vanish[0].p != 'p' && //pawns cannot pull this.isAprioriValidExit( [x, y], [fm.start.x, fm.start.y], @@ -415,13 +415,13 @@ export default class DynamoRules extends ChessRules { // Seems so: const dir = this.getNormalizedDirection( [fm.start.x - x, fm.start.y - y]); - const nbSteps = (fm.vanish[0].p == V.KNIGHT ? 1 : null); + const nbSteps = (fm.vanish[0].p == 'n' ? 1 : null); return this.getMovesInDirection([x, y], dir, nbSteps); } return []; }; const getPullMoves = () => { - if (fm.vanish[0].p == V.PAWN) + if (fm.vanish[0].p == 'p') // pawns cannot pull return []; const dirM = this.getNormalizedDirection( @@ -434,8 +434,8 @@ export default class DynamoRules extends ChessRules { const deltaX = Math.abs(x - fm.start.x); const deltaY = Math.abs(y - fm.start.y); if ( - (fm.vanish[0].p == V.KING && (deltaX > 1 || deltaY > 1)) || - (fm.vanish[0].p == V.KNIGHT && + (fm.vanish[0].p == 'k' && (deltaX > 1 || deltaY > 1)) || + (fm.vanish[0].p == 'n' && (deltaX + deltaY != 3 || deltaX == 0 || deltaY == 0)) ) { return []; @@ -444,7 +444,7 @@ export default class DynamoRules extends ChessRules { let [i, j] = [x + dir[0], y + dir[1]]; while ( (i != fm.start.x || j != fm.start.y) && - this.board[i][j] == V.EMPTY + this.board[i][j] == "" ) { i += dir[0]; j += dir[1]; @@ -493,20 +493,20 @@ export default class DynamoRules extends ChessRules { outerLoop: for (let step of steps) { let i = x + step[0]; let j = y + step[1]; - while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { + while (this.onBoard(i, j) && this.board[i][j] == "") { moves.push(this.getBasicMove([x, y], [i, j])); if (oneStep) continue outerLoop; i += step[0]; j += step[1]; } - if (V.OnBoard(i, j)) { + if (this.onBoard(i, j)) { if (this.canTake([x, y], [i, j])) moves.push(this.getBasicMove([x, y], [i, j])); } else { // Add potential board exit (suicide), except for the king - if (piece != V.KING) { + if (piece != 'k') { moves.push({ start: { x: x, y: y}, end: { x: this.kingPos[c][0], y: this.kingPos[c][1] }, @@ -563,7 +563,8 @@ export default class DynamoRules extends ChessRules { } } - // TODO :: +// TODO: re-write just for here getAllPotentialMoves() ? + filterValid(moves) { const color = this.turn; const La = this.amoves.length; @@ -572,7 +573,7 @@ export default class DynamoRules extends ChessRules { // A move is valid either if it doesn't result in a check, // or if a second move is possible to counter the check // (not undoing a potential move + action of the opponent) - this.play(m); + this.playOnBoard(m); let res = this.underCheck(color); if (this.subTurn == 2) { let isOpposite = La > 0 && this.oppositeMoves(this.amoves[La-1], m); @@ -580,7 +581,7 @@ export default class DynamoRules extends ChessRules { const moves2 = this.getAllPotentialMoves(); for (let m2 of moves2) { this.play(m2); - const res2 = this.underCheck(color); + const res2 = this.underCheck(color); //TODO: + square const amove = this.getAmove(m, m2); isOpposite = La > 0 && this.oppositeMoves(this.amoves[La-1], amove); @@ -592,7 +593,7 @@ export default class DynamoRules extends ChessRules { } } } - this.undo(m); + this.undoOnBoard(m); return !res; }); } diff --git a/variants/Eightpieces/class.js b/variants/Eightpieces/class.js new file mode 100644 index 0000000..3808a16 --- /dev/null +++ b/variants/Eightpieces/class.js @@ -0,0 +1,1136 @@ +import { randInt, sample } from "@/utils/alea"; +import { ChessRules, PiPo, Move } from "@/base_rules"; + +export class EightpiecesRules extends ChessRules { + + static get JAILER() { + return "j"; + } + static get SENTRY() { + return "s"; + } + static get LANCER() { + return "l"; + } + + static get IMAGE_EXTENSION() { + // Temporarily, for the time SVG pieces are being designed: + return ".png"; + } + + // Lancer directions *from white perspective* + static get LANCER_DIRS() { + return { + 'c': [-1, 0], //north + 'd': [-1, 1], //N-E + 'e': [0, 1], //east + 'f': [1, 1], //S-E + 'g': [1, 0], //south + 'h': [1, -1], //S-W + 'm': [0, -1], //west + 'o': [-1, -1] //N-W + }; + } + + static get PIECES() { + return ChessRules.PIECES + .concat([V.JAILER, V.SENTRY]) + .concat(Object.keys(V.LANCER_DIRS)); + } + + getPiece(i, j) { + const piece = this.board[i][j].charAt(1); + // Special lancer case: 8 possible orientations + if (Object.keys(V.LANCER_DIRS).includes(piece)) return V.LANCER; + return piece; + } + + getPpath(b, color, score, orientation) { + if ([V.JAILER, V.SENTRY].includes(b[1])) return "Eightpieces/tmp_png/" + b; + if (Object.keys(V.LANCER_DIRS).includes(b[1])) { + if (orientation == 'w') return "Eightpieces/tmp_png/" + b; + // Find opposite direction for adequate display: + let oppDir = ''; + switch (b[1]) { + case 'c': + oppDir = 'g'; + break; + case 'g': + oppDir = 'c'; + break; + case 'd': + oppDir = 'h'; + break; + case 'h': + oppDir = 'd'; + break; + case 'e': + oppDir = 'm'; + break; + case 'm': + oppDir = 'e'; + break; + case 'f': + oppDir = 'o'; + break; + case 'o': + oppDir = 'f'; + break; + } + return "Eightpieces/tmp_png/" + b[0] + oppDir; + } + // TODO: after we have SVG pieces, remove the folder and next prefix: + return "Eightpieces/tmp_png/" + b; + } + + getPPpath(m, orientation) { + return ( + this.getPpath( + m.appear[0].c + m.appear[0].p, + null, + null, + orientation + ) + ); + } + + static ParseFen(fen) { + const fenParts = fen.split(" "); + return Object.assign( + ChessRules.ParseFen(fen), + { sentrypush: fenParts[5] } + ); + } + + static IsGoodFen(fen) { + if (!ChessRules.IsGoodFen(fen)) return false; + const fenParsed = V.ParseFen(fen); + // 5) Check sentry push (if any) + if ( + fenParsed.sentrypush != "-" && + !fenParsed.sentrypush.match(/^([a-h][1-8]){2,2}$/) + ) { + return false; + } + return true; + } + + getFen() { + return super.getFen() + " " + this.getSentrypushFen(); + } + + getFenForRepeat() { + return super.getFenForRepeat() + "_" + this.getSentrypushFen(); + } + + getSentrypushFen() { + const L = this.sentryPush.length; + if (!this.sentryPush[L-1]) return "-"; + let res = ""; + const spL = this.sentryPush[L-1].length; + // Condensate path: just need initial and final squares: + return [0, spL - 1] + .map(i => V.CoordsToSquare(this.sentryPush[L-1][i])) + .join(""); + } + + setOtherVariables(fen) { + super.setOtherVariables(fen); + // subTurn == 2 only when a sentry moved, and is about to push something + this.subTurn = 1; + // Sentry position just after a "capture" (subTurn from 1 to 2) + this.sentryPos = null; + // Stack pieces' forbidden squares after a sentry move at each turn + const parsedFen = V.ParseFen(fen); + if (parsedFen.sentrypush == "-") this.sentryPush = [null]; + else { + // Expand init + dest squares into a full path: + const init = V.SquareToCoords(parsedFen.sentrypush.substr(0, 2)), + dest = V.SquareToCoords(parsedFen.sentrypush.substr(2)); + let newPath = [init]; + const delta = ['x', 'y'].map(i => Math.abs(dest[i] - init[i])); + // Check that it's not a knight movement: + if (delta[0] == 0 || delta[1] == 0 || delta[0] == delta[1]) { + const step = ['x', 'y'].map((i, idx) => { + return (dest[i] - init[i]) / delta[idx] || 0 + }); + let x = init.x + step[0], + y = init.y + step[1]; + while (x != dest.x || y != dest.y) { + newPath.push({ x: x, y: y }); + x += step[0]; + y += step[1]; + } + } + newPath.push(dest); + this.sentryPush = [newPath]; + } + } + + static GenRandInitFen(options) { + if (options.randomness == 0) + return "jfsqkbnr/pppppppp/8/8/8/8/PPPPPPPP/JDSQKBNR w 0 ahah - -"; + + const baseFen = ChessRules.GenRandInitFen(options); + const fenParts = baseFen.split(' '); + const posParts = fenParts[0].split('/'); + + // Replace one bishop by sentry, so that sentries on different colors + // Also replace one random rook by jailer, + // and one random knight by lancer (facing north/south) + let pieceLine = { b: posParts[0], w: posParts[7].toLowerCase() }; + let posBlack = { r: -1, n: -1, b: -1 }; + const mapP = { r: 'j', n: 'l', b: 's' }; + ['b', 'w'].forEach(c => { + ['r', 'n', 'b'].forEach(p => { + let pl = pieceLine[c]; + let pos = -1; + if (options.randomness == 2 || c == 'b') + pos = (randInt(2) == 0 ? pl.indexOf(p) : pl.lastIndexOf(p)); + else pos = posBlack[p]; + pieceLine[c] = + pieceLine[c].substr(0, pos) + mapP[p] + pieceLine[c].substr(pos+1); + if (options.randomness == 1 && c == 'b') posBlack[p] = pos; + }); + }); + // Rename 'l' into 'g' (black) or 'c' (white) + pieceLine['w'] = pieceLine['w'].replace('l', 'c'); + pieceLine['b'] = pieceLine['b'].replace('l', 'g'); + if (options.randomness == 2) { + const ws = pieceLine['w'].indexOf('s'); + const bs = pieceLine['b'].indexOf('s'); + if (ws % 2 != bs % 2) { + // Fix sentry: should be on different colors. + // => move sentry on other bishop for random color + const c = sample(['w', 'b'], 1); + pieceLine[c] = pieceLine[c] + .replace('b', 't') //tmp + .replace('s', 'b') + .replace('t', 's'); + } + } + + return ( + pieceLine['b'] + "/" + + posParts.slice(1, 7).join('/') + "/" + + pieceLine['w'].toUpperCase() + " " + + fenParts.slice(1, 5).join(' ') + " -" + ); + } + + canTake([x1, y1], [x2, y2]) { + if (this.subTurn == 2) + // Only self captures on this subturn: + return this.getColor(x1, y1) == this.getColor(x2, y2); + return super.canTake([x1, y1], [x2, y2]); + } + + // Is piece on square (x,y) immobilized? + isImmobilized([x, y]) { + const color = this.getColor(x, y); + const oppCol = V.GetOppCol(color); + for (let step of V.steps[V.ROOK]) { + const [i, j] = [x + step[0], y + step[1]]; + if ( + V.OnBoard(i, j) && + this.board[i][j] != V.EMPTY && + this.getColor(i, j) == oppCol + ) { + if (this.getPiece(i, j) == V.JAILER) return [i, j]; + } + } + return null; + } + + canIplay(side, [x, y]) { + return ( + (this.subTurn == 1 && this.turn == side && this.getColor(x, y) == side) + || + (this.subTurn == 2 && x == this.sentryPos.x && y == this.sentryPos.y) + ); + } + + getPotentialMovesFrom([x, y]) { + const piece = this.getPiece(x, y); + const L = this.sentryPush.length; + // At subTurn == 2, jailers aren't effective (Jeff K) + if (this.subTurn == 1) { + const jsq = this.isImmobilized([x, y]); + if (!!jsq) { + let moves = []; + // Special pass move if king: + if (piece == V.KING) { + moves.push( + new Move({ + appear: [], + vanish: [], + start: { x: x, y: y }, + end: { x: jsq[0], y: jsq[1] } + }) + ); + } + else if (piece == V.LANCER && !!this.sentryPush[L-1]) { + // A pushed lancer next to the jailer: reorient + const color = this.getColor(x, y); + const curDir = this.board[x][y].charAt(1); + Object.keys(V.LANCER_DIRS).forEach(k => { + moves.push( + new Move({ + appear: [{ x: x, y: y, c: color, p: k }], + vanish: [{ x: x, y: y, c: color, p: curDir }], + start: { x: x, y: y }, + end: { x: jsq[0], y: jsq[1] } + }) + ); + }); + } + return moves; + } + } + let moves = []; + switch (piece) { + case V.JAILER: + moves = this.getPotentialJailerMoves([x, y]); + break; + case V.SENTRY: + moves = this.getPotentialSentryMoves([x, y]); + break; + case V.LANCER: + moves = this.getPotentialLancerMoves([x, y]); + break; + default: + moves = super.getPotentialMovesFrom([x, y]); + break; + } + if (!!this.sentryPush[L-1]) { + // Delete moves walking back on sentry push path, + // only if not a pawn, and the piece is the pushed one. + const pl = this.sentryPush[L-1].length; + const finalPushedSq = this.sentryPush[L-1][pl-1]; + moves = moves.filter(m => { + if ( + m.vanish[0].p != V.PAWN && + m.start.x == finalPushedSq.x && m.start.y == finalPushedSq.y && + this.sentryPush[L-1].some(sq => sq.x == m.end.x && sq.y == m.end.y) + ) { + return false; + } + return true; + }); + } + else if (this.subTurn == 2) { + // Put back the sentinel on board: + const color = this.turn; + moves.forEach(m => { + m.appear.push({x: x, y: y, p: V.SENTRY, c: color}); + }); + } + return moves; + } + + getPotentialPawnMoves([x, y]) { + const color = this.getColor(x, y); + let moves = []; + const [sizeX, sizeY] = [V.size.x, V.size.y]; + let shiftX = (color == "w" ? -1 : 1); + if (this.subTurn == 2) shiftX *= -1; + const firstRank = color == "w" ? sizeX - 1 : 0; + const startRank = color == "w" ? sizeX - 2 : 1; + const lastRank = color == "w" ? 0 : sizeX - 1; + + // Pawns might be pushed on 1st rank and attempt to move again: + if (!V.OnBoard(x + shiftX, y)) return []; + + // A push cannot put a pawn on last rank (it goes backward) + let finalPieces = [V.PAWN]; + if (x + shiftX == lastRank) { + // Only allow direction facing inside board: + const allowedLancerDirs = + lastRank == 0 + ? ['e', 'f', 'g', 'h', 'm'] + : ['c', 'd', 'e', 'm', 'o']; + finalPieces = + allowedLancerDirs + .concat([V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN, V.SENTRY, V.JAILER]); + } + if (this.board[x + shiftX][y] == V.EMPTY) { + // One square forward + for (let piece of finalPieces) { + moves.push( + this.getBasicMove([x, y], [x + shiftX, y], { + c: color, + p: piece + }) + ); + } + if ( + // 2-squares jumps forbidden if pawn push + this.subTurn == 1 && + [startRank, firstRank].includes(x) && + 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]) + ) { + for (let piece of finalPieces) { + moves.push( + this.getBasicMove([x, y], [x + shiftX, y + shiftY], { + c: color, + p: piece + }) + ); + } + } + } + + // En passant: only on subTurn == 1 + const Lep = this.epSquares.length; + const epSquare = this.epSquares[Lep - 1]; + if ( + this.subTurn == 1 && + !!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; + } + + doClick(square) { + if (isNaN(square[0])) return null; + const L = this.sentryPush.length; + const [x, y] = [square[0], square[1]]; + const color = this.turn; + if ( + this.subTurn == 2 || + this.board[x][y] == V.EMPTY || + this.getPiece(x, y) != V.LANCER || + this.getColor(x, y) != color || + !!this.sentryPush[L-1] + ) { + return null; + } + // Stuck lancer? + const orientation = this.board[x][y][1]; + const step = V.LANCER_DIRS[orientation]; + if (!V.OnBoard(x + step[0], y + step[1])) { + let choices = []; + Object.keys(V.LANCER_DIRS).forEach(k => { + const dir = V.LANCER_DIRS[k]; + if ( + (dir[0] != step[0] || dir[1] != step[1]) && + V.OnBoard(x + dir[0], y + dir[1]) + ) { + choices.push( + new Move({ + vanish: [ + new PiPo({ + x: x, + y: y, + c: color, + p: orientation + }) + ], + appear: [ + new PiPo({ + x: x, + y: y, + c: color, + p: k + }) + ], + start: { x: x, y : y }, + end: { x: -1, y: -1 } + }) + ); + } + }); + return choices; + } + return null; + } + + // Obtain all lancer moves in "step" direction + getPotentialLancerMoves_aux([x, y], step, tr) { + let moves = []; + // Add all moves to vacant squares until opponent is met: + const color = this.getColor(x, y); + const oppCol = + this.subTurn == 1 + ? V.GetOppCol(color) + // at subTurn == 2, consider own pieces as opponent + : color; + let sq = [x + step[0], y + step[1]]; + while (V.OnBoard(sq[0], sq[1]) && this.getColor(sq[0], sq[1]) != oppCol) { + if (this.board[sq[0]][sq[1]] == V.EMPTY) + moves.push(this.getBasicMove([x, y], sq, tr)); + sq[0] += step[0]; + sq[1] += step[1]; + } + if (V.OnBoard(sq[0], sq[1])) + // Add capturing move + moves.push(this.getBasicMove([x, y], sq, tr)); + return moves; + } + + getPotentialLancerMoves([x, y]) { + let moves = []; + // Add all lancer possible orientations, similar to pawn promotions. + // Except if just after a push: allow all movements from init square then + const L = this.sentryPush.length; + const color = this.getColor(x, y); + const dirCode = this.board[x][y][1]; + const curDir = V.LANCER_DIRS[dirCode]; + if (!!this.sentryPush[L-1]) { + // Maybe I was pushed + const pl = this.sentryPush[L-1].length; + if ( + this.sentryPush[L-1][pl-1].x == x && + this.sentryPush[L-1][pl-1].y == y + ) { + // I was pushed: allow all directions (for this move only), but + // do not change direction after moving, *except* if I keep the + // same orientation in which I was pushed. + // Also allow simple reorientation ("capturing king"): + if (!V.OnBoard(x + curDir[0], y + curDir[1])) { + const kp = this.kingPos[color]; + let reorientMoves = []; + Object.keys(V.LANCER_DIRS).forEach(k => { + const dir = V.LANCER_DIRS[k]; + if ( + (dir[0] != curDir[0] || dir[1] != curDir[1]) && + V.OnBoard(x + dir[0], y + dir[1]) + ) { + reorientMoves.push( + new Move({ + vanish: [ + new PiPo({ + x: x, + y: y, + c: color, + p: dirCode + }) + ], + appear: [ + new PiPo({ + x: x, + y: y, + c: color, + p: k + }) + ], + start: { x: x, y : y }, + end: { x: kp[0], y: kp[1] } + }) + ); + } + }); + Array.prototype.push.apply(moves, reorientMoves); + } + Object.values(V.LANCER_DIRS).forEach(step => { + const dirCode = Object.keys(V.LANCER_DIRS).find(k => { + return ( + V.LANCER_DIRS[k][0] == step[0] && + V.LANCER_DIRS[k][1] == step[1] + ); + }); + const dirMoves = + this.getPotentialLancerMoves_aux( + [x, y], + step, + { p: dirCode, c: color } + ); + if (curDir[0] == step[0] && curDir[1] == step[1]) { + // Keeping same orientation: can choose after + let chooseMoves = []; + dirMoves.forEach(m => { + Object.keys(V.LANCER_DIRS).forEach(k => { + const newDir = V.LANCER_DIRS[k]; + // Prevent orientations toward outer board: + if (V.OnBoard(m.end.x + newDir[0], m.end.y + newDir[1])) { + let mk = JSON.parse(JSON.stringify(m)); + mk.appear[0].p = k; + chooseMoves.push(mk); + } + }); + }); + Array.prototype.push.apply(moves, chooseMoves); + } + else Array.prototype.push.apply(moves, dirMoves); + }); + return moves; + } + } + // I wasn't pushed: standard lancer move + const monodirMoves = + this.getPotentialLancerMoves_aux([x, y], V.LANCER_DIRS[dirCode]); + // Add all possible orientations aftermove except if I'm being pushed + if (this.subTurn == 1) { + monodirMoves.forEach(m => { + Object.keys(V.LANCER_DIRS).forEach(k => { + const newDir = V.LANCER_DIRS[k]; + // Prevent orientations toward outer board: + if (V.OnBoard(m.end.x + newDir[0], m.end.y + newDir[1])) { + let mk = JSON.parse(JSON.stringify(m)); + mk.appear[0].p = k; + moves.push(mk); + } + }); + }); + return moves; + } + else { + // I'm pushed: add potential nudges, except for current orientation + let potentialNudges = []; + for (let step of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) { + if ( + (step[0] != curDir[0] || step[1] != curDir[1]) && + V.OnBoard(x + step[0], y + step[1]) && + this.board[x + step[0]][y + step[1]] == V.EMPTY + ) { + const newDirCode = Object.keys(V.LANCER_DIRS).find(k => { + const codeStep = V.LANCER_DIRS[k]; + return (codeStep[0] == step[0] && codeStep[1] == step[1]); + }); + potentialNudges.push( + this.getBasicMove( + [x, y], + [x + step[0], y + step[1]], + { c: color, p: newDirCode } + ) + ); + } + } + return monodirMoves.concat(potentialNudges); + } + } + + getPotentialSentryMoves([x, y]) { + // The sentry moves a priori like a bishop: + let moves = super.getPotentialBishopMoves([x, y]); + // ...but captures are replaced by special move, if and only if + // "captured" piece can move now, considered as the capturer unit. + // --> except is subTurn == 2, in this case I don't push anything. + if (this.subTurn == 2) return moves.filter(m => m.vanish.length == 1); + moves.forEach(m => { + if (m.vanish.length == 2) { + // Temporarily cancel the sentry capture: + m.appear.pop(); + m.vanish.pop(); + } + }); + const color = this.getColor(x, y); + const fMoves = moves.filter(m => { + // Can the pushed unit make any move? ...resulting in a non-self-check? + if (m.appear.length == 0) { + let res = false; + this.play(m); + let moves2 = this.getPotentialMovesFrom([m.end.x, m.end.y]); + for (let m2 of moves2) { + this.play(m2); + res = !this.underCheck(color); + this.undo(m2); + if (res) break; + } + this.undo(m); + return res; + } + return true; + }); + return fMoves; + } + + getPotentialJailerMoves([x, y]) { + return super.getPotentialRookMoves([x, y]).filter(m => { + // Remove jailer captures + return m.vanish[0].p != V.JAILER || m.vanish.length == 1; + }); + } + + getPotentialKingMoves(sq) { + const moves = this.getSlideNJumpMoves( + sq, V.steps[V.ROOK].concat(V.steps[V.BISHOP]), 1); + return ( + this.subTurn == 1 + ? moves.concat(this.getCastleMoves(sq)) + : moves + ); + } + + atLeastOneMove() { + // If in second-half of a move, we already know that a move is possible + if (this.subTurn == 2) return true; + return super.atLeastOneMove(); + } + + filterValid(moves) { + if (moves.length == 0) return []; + const basicFilter = (m, c) => { + this.play(m); + const res = !this.underCheck(c); + this.undo(m); + return res; + }; + // Disable check tests for sentry pushes, + // because in this case the move isn't finished + let movesWithoutSentryPushes = []; + let movesWithSentryPushes = []; + moves.forEach(m => { + // Second condition below for special king "pass" moves + if (m.appear.length > 0 || m.vanish.length == 0) + movesWithoutSentryPushes.push(m); + else movesWithSentryPushes.push(m); + }); + const color = this.turn; + const oppCol = V.GetOppCol(color); + const filteredMoves = + movesWithoutSentryPushes.filter(m => basicFilter(m, color)); + // If at least one full move made, everything is allowed. + // Else: forbid checks and captures. + return ( + this.movesCount >= 2 + ? filteredMoves + : filteredMoves.filter(m => { + return (m.vanish.length <= 1 && basicFilter(m, oppCol)); + }) + ).concat(movesWithSentryPushes); + } + + getAllValidMoves() { + if (this.subTurn == 1) return super.getAllValidMoves(); + // Sentry push: + const sentrySq = [this.sentryPos.x, this.sentryPos.y]; + return this.filterValid(this.getPotentialMovesFrom(sentrySq)); + } + + isAttacked(sq, color) { + return ( + super.isAttacked(sq, color) || + this.isAttackedByLancer(sq, color) || + this.isAttackedBySentry(sq, color) + // The jailer doesn't capture. + ); + } + + isAttackedBySlideNJump([x, y], color, piece, steps, oneStep) { + for (let step of steps) { + let rx = x + step[0], + ry = y + step[1]; + while (V.OnBoard(rx, ry) && this.board[rx][ry] == V.EMPTY && !oneStep) { + rx += step[0]; + ry += step[1]; + } + if ( + V.OnBoard(rx, ry) && + this.getPiece(rx, ry) == piece && + this.getColor(rx, ry) == color && + !this.isImmobilized([rx, ry]) + ) { + return true; + } + } + return false; + } + + isAttackedByPawn([x, y], color) { + const pawnShift = (color == "w" ? 1 : -1); + if (x + pawnShift >= 0 && x + pawnShift < V.size.x) { + for (let i of [-1, 1]) { + if ( + y + i >= 0 && + y + i < V.size.y && + this.getPiece(x + pawnShift, y + i) == V.PAWN && + this.getColor(x + pawnShift, y + i) == color && + !this.isImmobilized([x + pawnShift, y + i]) + ) { + return true; + } + } + } + return false; + } + + isAttackedByLancer([x, y], color) { + for (let step of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) { + // If in this direction there are only enemy pieces and empty squares, + // and we meet a lancer: can he reach us? + // NOTE: do not stop at first lancer, there might be several! + let coord = { x: x + step[0], y: y + step[1] }; + let lancerPos = []; + while ( + V.OnBoard(coord.x, coord.y) && + ( + this.board[coord.x][coord.y] == V.EMPTY || + this.getColor(coord.x, coord.y) == color + ) + ) { + if ( + this.getPiece(coord.x, coord.y) == V.LANCER && + !this.isImmobilized([coord.x, coord.y]) + ) { + lancerPos.push({x: coord.x, y: coord.y}); + } + coord.x += step[0]; + coord.y += step[1]; + } + const L = this.sentryPush.length; + const pl = (!!this.sentryPush[L-1] ? this.sentryPush[L-1].length : 0); + for (let xy of lancerPos) { + const dir = V.LANCER_DIRS[this.board[xy.x][xy.y].charAt(1)]; + if ( + (dir[0] == -step[0] && dir[1] == -step[1]) || + // If the lancer was just pushed, this is an attack too: + ( + !!this.sentryPush[L-1] && + this.sentryPush[L-1][pl-1].x == xy.x && + this.sentryPush[L-1][pl-1].y == xy.y + ) + ) { + return true; + } + } + } + return false; + } + + // Helper to check sentries attacks: + selfAttack([x1, y1], [x2, y2]) { + const color = this.getColor(x1, y1); + const oppCol = V.GetOppCol(color); + const sliderAttack = (allowedSteps, lancer) => { + const deltaX = x2 - x1, + deltaY = y2 - y1; + const absDeltaX = Math.abs(deltaX), + absDeltaY = Math.abs(deltaY); + const step = [ deltaX / absDeltaX || 0, deltaY / absDeltaY || 0 ]; + if ( + // Check that the step is a priori valid: + (absDeltaX != absDeltaY && deltaX != 0 && deltaY != 0) || + allowedSteps.every(st => st[0] != step[0] || st[1] != step[1]) + ) { + return false; + } + let sq = [ x1 + step[0], y1 + step[1] ]; + while (sq[0] != x2 || sq[1] != y2) { + // NOTE: no need to check OnBoard in this special case + if (this.board[sq[0]][sq[1]] != V.EMPTY) { + const p = this.getPiece(sq[0], sq[1]); + const pc = this.getColor(sq[0], sq[1]); + if ( + // Enemy sentry on the way will be gone: + (p != V.SENTRY || pc != oppCol) && + // Lancer temporarily "changed color": + (!lancer || pc == color) + ) { + return false; + } + } + sq[0] += step[0]; + sq[1] += step[1]; + } + return true; + }; + switch (this.getPiece(x1, y1)) { + case V.PAWN: { + // Pushed pawns move as enemy pawns + const shift = (color == 'w' ? 1 : -1); + return (x1 + shift == x2 && Math.abs(y1 - y2) == 1); + } + case V.KNIGHT: { + const deltaX = Math.abs(x1 - x2); + const deltaY = Math.abs(y1 - y2); + return ( + deltaX + deltaY == 3 && + [1, 2].includes(deltaX) && + [1, 2].includes(deltaY) + ); + } + case V.ROOK: + return sliderAttack(V.steps[V.ROOK]); + case V.BISHOP: + return sliderAttack(V.steps[V.BISHOP]); + case V.QUEEN: + return sliderAttack(V.steps[V.ROOK].concat(V.steps[V.BISHOP])); + case V.LANCER: { + // Special case: as long as no enemy units stands in-between, + // it attacks (if it points toward the king). + const allowedStep = V.LANCER_DIRS[this.board[x1][y1].charAt(1)]; + return sliderAttack([allowedStep], "lancer"); + } + // No sentries or jailer tests: they cannot self-capture + } + return false; + } + + isAttackedBySentry([x, y], color) { + // Attacked by sentry means it can self-take our king. + // Just check diagonals of enemy sentry(ies), and if it reaches + // one of our pieces: can I self-take? + const myColor = V.GetOppCol(color); + let candidates = []; + for (let i=0; i { + const score = this.getCurrentScore(); + const curEval = move.eval; + if (score != "*") { + move.eval = + score == "1/2" + ? 0 + : (score == "1-0" ? 1 : -1) * maxeval; + } else move.eval = this.evalPosition(); + if ( + // "next" is defined after sentry pushes + !!next && ( + !curEval || + color == 'w' && move.eval > curEval || + color == 'b' && move.eval < curEval + ) + ) { + move.second = next; + } + }; + + // Just search_depth == 1 (because of sentries. TODO: can do better...) + moves1.forEach(m1 => { + this.play(m1); + if (this.subTurn == 1) setEval(m1); + else { + // Need to play every pushes and count: + const moves2 = this.getAllValidMoves(); + moves2.forEach(m2 => { + this.play(m2); + setEval(m1, m2); + this.undo(m2); + }); + } + this.undo(m1); + }); + + moves1.sort((a, b) => { + return (color == "w" ? 1 : -1) * (b.eval - a.eval); + }); + let candidates = [0]; + for (let j = 1; j < moves1.length && moves1[j].eval == moves1[0].eval; j++) + candidates.push(j); + const choice = moves1[candidates[randInt(candidates.length)]]; + return (!choice.second ? choice : [choice, choice.second]); + } + + // For moves notation: + static get LANCER_DIRNAMES() { + return { + 'c': "N", + 'd': "NE", + 'e': "E", + 'f': "SE", + 'g': "S", + 'h': "SW", + 'm': "W", + 'o': "NW" + }; + } + + getNotation(move) { + // Special case "king takes jailer" is a pass move + if (move.appear.length == 0 && move.vanish.length == 0) return "pass"; + let notation = undefined; + if (this.subTurn == 2) { + // Do not consider appear[1] (sentry) for sentry pushes + const simpleMove = { + appear: [move.appear[0]], + vanish: move.vanish, + start: move.start, + end: move.end + }; + notation = super.getNotation(simpleMove); + } + else if ( + move.appear.length > 0 && + move.vanish[0].x == move.appear[0].x && + move.vanish[0].y == move.appear[0].y + ) { + // Lancer in-place reorientation: + notation = "L" + V.CoordsToSquare(move.start) + ":R"; + } + else notation = super.getNotation(move); + if (Object.keys(V.LANCER_DIRNAMES).includes(move.vanish[0].p)) + // Lancer: add direction info + notation += "=" + V.LANCER_DIRNAMES[move.appear[0].p]; + else if ( + move.vanish[0].p == V.PAWN && + Object.keys(V.LANCER_DIRNAMES).includes(move.appear[0].p) + ) { + // Fix promotions in lancer: + notation = notation.slice(0, -1) + + "L:" + V.LANCER_DIRNAMES[move.appear[0].p]; + } + return notation; + } + +}; diff --git a/variants/Eightpieces/complete_rules.html b/variants/Eightpieces/complete_rules.html new file mode 100644 index 0000000..c1d12ab --- /dev/null +++ b/variants/Eightpieces/complete_rules.html @@ -0,0 +1,48 @@ +p.boxed + | Three new pieces appear. All pieces are unique. + +p. + There are only one rook, one bishop and one knight per side in this variant. + That explains the name. The king and queen are still there, + and the three remaining slots are taken by new pieces: + +ul + li. + The lancer 'L' is oriented and can only move in the direction it points, + by any number of squares as long as an enemy isn't met + (it can jump over friendly pieces). If an opponent' piece is found, + it can be captured. After moving you can reorient the lancer. + li. + The sentry 'S' moves like a bishop but doesn't capture directly. + It "pushes" enemy pieces instead, either on an empty square or on other + enemy pieces which are thus (self-)captured. + li. + The jailer 'J' moves like a rook but also doesn't capture. + It immobilizes enemy pieces which are vertically or horizontally adjacent. + +p. + On the following diagram the white sentry can push the black lancer to + capture the black pawn on b4. The lancer is then immobilized + by the white jailer at a4. + +figure.diagram-container + .diagram.diag12 + | fen:7k/8/8/8/Jp3m2/8/3S4/K7: + .diagram.diag22 + | fen:7k/8/8/8/Jm3S2/8/8/K7: + figcaption Left: before white move S"push"f4. Right: after this move. + +p To reorient a stuck lancer, +ul + li Just after being pushed: play a move which 'capture your king". + li Later in the game: click on the lancer. + +h3 Complete rules + +p + | The rules were invented by Jeff Kubach (2020), who described them much + | more precisely on the + a(href="https://www.chessvariants.com/rules/8-piece-chess") + | chessvariants page + | . While the summary given above may suffice to start playing, + | you should read the complete rules to fully understand this variant. diff --git a/variants/Eightpieces/rules.html b/variants/Eightpieces/rules.html new file mode 100644 index 0000000..41e9d42 --- /dev/null +++ b/variants/Eightpieces/rules.html @@ -0,0 +1,3 @@ +Three new pieces appear: the lancer is a rook with a constrained direction, the sentry moves like a bishop and pushes pieces, and the jailer immobilizes pieces orthogonally adjacent. + +The goal is still to checkmate. diff --git a/variants/Eightpieces/style.css b/variants/Eightpieces/style.css new file mode 100644 index 0000000..a3550bc --- /dev/null +++ b/variants/Eightpieces/style.css @@ -0,0 +1 @@ +@import url("/base_pieces.css"); diff --git a/variants/Emergo/class.js b/variants/Emergo/class.js new file mode 100644 index 0000000..13069de --- /dev/null +++ b/variants/Emergo/class.js @@ -0,0 +1,576 @@ +import { ChessRules, Move, PiPo } from "@/base_rules"; +import { randInt } from "@/utils/alea"; +import { ArrayFun } from "@/utils/array"; + +export class EmergoRules extends ChessRules { + + // Simple encoding: A to L = 1 to 12, from left to right, if white controls. + // Lowercase if black controls. + // Single piece (no prisoners): A@ to L@ (+ lowercase) + + static get Options() { + return null; + } + + static get HasFlags() { + return false; + } + + static get HasEnpassant() { + return false; + } + + static get DarkBottomRight() { + return true; + } + + // board element == file name: + static board2fen(b) { + return b; + } + static fen2board(f) { + return f; + } + + static IsGoodPosition(position) { + if (position.length == 0) return false; + const rows = position.split("/"); + if (rows.length != V.size.x) return false; + for (let row of rows) { + let sumElts = 0; + for (let i = 0; i < row.length; i++) { + // Add only 0.5 per symbol because 2 per piece + if (row[i].toLowerCase().match(/^[a-lA-L@]$/)) sumElts += 0.5; + else { + const num = parseInt(row[i], 10); + if (isNaN(num) || num <= 0) return false; + sumElts += num; + } + } + if (sumElts != V.size.y) return false; + } + return true; + } + + static GetBoard(position) { + const rows = position.split("/"); + let board = ArrayFun.init(V.size.x, V.size.y, ""); + for (let i = 0; i < rows.length; i++) { + let j = 0; + for (let indexInRow = 0; indexInRow < rows[i].length; indexInRow++) { + const character = rows[i][indexInRow]; + const num = parseInt(character, 10); + // If num is a number, just shift j: + if (!isNaN(num)) j += num; + else + // Something at position i,j + board[i][j++] = V.fen2board(character + rows[i][++indexInRow]); + } + } + return board; + } + + getPpath(b) { + return "Emergo/" + b; + } + + getColor(x, y) { + if (x >= V.size.x) return x == V.size.x ? "w" : "b"; + if (this.board[x][y].charCodeAt(0) < 97) return 'w'; + return 'b'; + } + + getPiece() { + return V.PAWN; //unused + } + + static IsGoodFen(fen) { + if (!ChessRules.IsGoodFen(fen)) return false; + const fenParsed = V.ParseFen(fen); + // 3) Check reserves + if ( + !fenParsed.reserve || + !fenParsed.reserve.match(/^([0-9]{1,2},?){2,2}$/) + ) { + return false; + } + return true; + } + + static ParseFen(fen) { + const fenParts = fen.split(" "); + return Object.assign( + ChessRules.ParseFen(fen), + { reserve: fenParts[3] } + ); + } + + static get size() { + return { x: 9, y: 9 }; + } + + static GenRandInitFen() { + return "9/9/9/9/9/9/9/9/9 w 0 12,12"; + } + + getFen() { + return super.getFen() + " " + this.getReserveFen(); + } + + getFenForRepeat() { + return super.getFenForRepeat() + "_" + this.getReserveFen(); + } + + getReserveFen() { + return ( + (!this.reserve["w"] ? 0 : this.reserve["w"][V.PAWN]) + "," + + (!this.reserve["b"] ? 0 : this.reserve["b"][V.PAWN]) + ); + } + + getReservePpath(index, color) { + return "Emergo/" + (color == 'w' ? 'A' : 'a') + '@'; + } + + static get RESERVE_PIECES() { + return [V.PAWN]; //only array length matters + } + + setOtherVariables(fen) { + const reserve = + V.ParseFen(fen).reserve.split(",").map(x => parseInt(x, 10)); + this.reserve = { w: null, b: null }; + if (reserve[0] > 0) this.reserve['w'] = { [V.PAWN]: reserve[0] }; + if (reserve[1] > 0) this.reserve['b'] = { [V.PAWN]: reserve[1] }; + // Local stack of captures during a turn (squares + directions) + this.captures = [ [] ]; + } + + atLeastOneCaptureFrom([x, y], color, forbiddenStep) { + for (let s of V.steps[V.BISHOP]) { + if ( + !forbiddenStep || + (s[0] != -forbiddenStep[0] || s[1] != -forbiddenStep[1]) + ) { + const [i, j] = [x + s[0], y + s[1]]; + if ( + V.OnBoard(i + s[0], j + s[1]) && + this.board[i][j] != V.EMPTY && + this.getColor(i, j) != color && + this.board[i + s[0]][j + s[1]] == V.EMPTY + ) { + return true; + } + } + } + return false; + } + + atLeastOneCapture(color) { + const L0 = this.captures.length; + const captures = this.captures[L0 - 1]; + const L = captures.length; + if (L > 0) { + return ( + this.atLeastOneCaptureFrom( + captures[L-1].square, color, captures[L-1].step) + ); + } + for (let i = 0; i < V.size.x; i++) { + for (let j=0; j< V.size.y; j++) { + if ( + this.board[i][j] != V.EMPTY && + this.getColor(i, j) == color && + this.atLeastOneCaptureFrom([i, j], color) + ) { + return true; + } + } + } + return false; + } + + maxLengthIndices(caps) { + let maxLength = 0; + let res = []; + for (let i = 0; i < caps.length; i++) { + if (caps[i].length > maxLength) { + res = [i]; + maxLength = caps[i].length; + } + else if (caps[i].length == maxLength) res.push(i); + } + return res; + }; + + getLongestCaptures_aux([x, y], color, locSteps) { + let res = []; + const L = locSteps.length; + const lastStep = (L > 0 ? locSteps[L-1] : null); + for (let s of V.steps[V.BISHOP]) { + if (!!lastStep && s[0] == -lastStep[0] && s[1] == -lastStep[1]) continue; + const [i, j] = [x + s[0], y + s[1]]; + if ( + V.OnBoard(i + s[0], j + s[1]) && + this.board[i + s[0]][j + s[1]] == V.EMPTY && + this.board[i][j] != V.EMPTY && + this.getColor(i, j) != color + ) { + const move = this.getBasicMove([x, y], [i + s[0], j + s[1]], [i, j]); + locSteps.push(s); + V.PlayOnBoard(this.board, move); + const nextRes = + this.getLongestCaptures_aux([i + s[0], j + s[1]], color, locSteps); + res.push(1 + nextRes); + locSteps.pop(); + V.UndoOnBoard(this.board, move); + } + } + if (res.length == 0) return 0; + return Math.max(...res); + } + + getLongestCapturesFrom([x, y], color, locSteps) { + let res = []; + const L = locSteps.length; + const lastStep = (L > 0 ? locSteps[L-1] : null); + for (let s of V.steps[V.BISHOP]) { + if (!!lastStep && s[0] == -lastStep[0] && s[1] == -lastStep[1]) continue; + const [i, j] = [x + s[0], y + s[1]]; + if ( + V.OnBoard(i + s[0], j + s[1]) && + this.board[i + s[0]][j + s[1]] == V.EMPTY && + this.board[i][j] != V.EMPTY && + this.getColor(i, j) != color + ) { + const move = this.getBasicMove([x, y], [i + s[0], j + s[1]], [i, j]); + locSteps.push(s); + V.PlayOnBoard(this.board, move); + const stepRes = + this.getLongestCaptures_aux([i + s[0], j + s[1]], color, locSteps); + res.push({ step: s, length: 1 + stepRes }); + locSteps.pop(); + V.UndoOnBoard(this.board, move); + } + } + return this.maxLengthIndices(res).map(i => res[i]);; + } + + getAllLongestCaptures(color) { + const L0 = this.captures.length; + const captures = this.captures[L0 - 1]; + const L = captures.length; + let caps = []; + if (L > 0) { + let locSteps = [ captures[L-1].step ]; + let res = + this.getLongestCapturesFrom(captures[L-1].square, color, locSteps); + Array.prototype.push.apply( + caps, + res.map(r => Object.assign({ square: captures[L-1].square }, r)) + ); + } + else { + for (let i = 0; i < V.size.x; i++) { + for (let j=0; j < V.size.y; j++) { + if ( + this.board[i][j] != V.EMPTY && + this.getColor(i, j) == color + ) { + let locSteps = []; + let res = this.getLongestCapturesFrom([i, j], color, locSteps); + Array.prototype.push.apply( + caps, + res.map(r => Object.assign({ square: [i, j] }, r)) + ); + } + } + } + } + return this.maxLengthIndices(caps).map(i => caps[i]); + } + + getBasicMove([x1, y1], [x2, y2], capt) { + const cp1 = this.board[x1][y1]; + if (!capt) { + return new Move({ + appear: [ new PiPo({ x: x2, y: y2, c: cp1[0], p: cp1[1] }) ], + vanish: [ new PiPo({ x: x1, y: y1, c: cp1[0], p: cp1[1] }) ] + }); + } + // Compute resulting types based on jumped + jumping pieces + const color = this.getColor(x1, y1); + const firstCodes = (color == 'w' ? [65, 97] : [97, 65]); + const cpCapt = this.board[capt[0]][capt[1]]; + let count1 = [cp1.charCodeAt(0) - firstCodes[0], -1]; + if (cp1[1] != '@') count1[1] = cp1.charCodeAt(1) - firstCodes[0]; + let countC = [cpCapt.charCodeAt(0) - firstCodes[1], -1]; + if (cpCapt[1] != '@') countC[1] = cpCapt.charCodeAt(1) - firstCodes[1]; + count1[1]++; + countC[0]--; + let colorChange = false, + captVanish = false; + if (countC[0] < 0) { + if (countC[1] >= 0) { + colorChange = true; + countC = [countC[1], -1]; + } + else captVanish = true; + } + const incPrisoners = String.fromCharCode(firstCodes[0] + count1[1]); + let mv = new Move({ + appear: [ + new PiPo({ + x: x2, + y: y2, + c: cp1[0], + p: incPrisoners + }) + ], + vanish: [ + new PiPo({ x: x1, y: y1, c: cp1[0], p: cp1[1] }), + new PiPo({ x: capt[0], y: capt[1], c: cpCapt[0], p: cpCapt[1] }) + ] + }); + if (!captVanish) { + mv.appear.push( + new PiPo({ + x: capt[0], + y: capt[1], + c: String.fromCharCode( + firstCodes[(colorChange ? 0 : 1)] + countC[0]), + p: (colorChange ? '@' : cpCapt[1]), + }) + ); + } + return mv; + } + + getReserveMoves(x) { + const color = this.turn; + if (!this.reserve[color] || this.atLeastOneCapture(color)) return []; + let moves = []; + const shadowPiece = + this.reserve[V.GetOppCol(color)] == null + ? this.reserve[color][V.PAWN] - 1 + : 0; + const appearColor = String.fromCharCode( + (color == 'w' ? 'A' : 'a').charCodeAt(0) + shadowPiece); + const addMove = ([i, j]) => { + moves.push( + new Move({ + appear: [ new PiPo({ x: i, y: j, c: appearColor, p: '@' }) ], + vanish: [], + start: { x: V.size.x + (color == 'w' ? 0 : 1), y: 0 } + }) + ); + }; + const oppCol = V.GetOppCol(color); + const opponentCanCapture = this.atLeastOneCapture(oppCol); + for (let i = 0; i < V.size.x; i++) { + for (let j = i % 2; j < V.size.y; j += 2) { + if ( + this.board[i][j] == V.EMPTY && + // prevent playing on central square at move 1: + (this.movesCount >= 1 || i != 4 || j != 4) + ) { + if (opponentCanCapture) addMove([i, j]); + else { + let canAddMove = true; + for (let s of V.steps[V.BISHOP]) { + if ( + V.OnBoard(i + s[0], j + s[1]) && + V.OnBoard(i - s[0], j - s[1]) && + this.board[i + s[0]][j + s[1]] != V.EMPTY && + this.board[i - s[0]][j - s[1]] == V.EMPTY && + this.getColor(i + s[0], j + s[1]) == oppCol + ) { + canAddMove = false; + break; + } + } + if (canAddMove) addMove([i, j]); + } + } + } + } + return moves; + } + + getPotentialMovesFrom([x, y], longestCaptures) { + if (x >= V.size.x) { + if (longestCaptures.length == 0) return this.getReserveMoves(x); + return []; + } + const color = this.turn; + if (!!this.reserve[color] && !this.atLeastOneCapture(color)) return []; + const L0 = this.captures.length; + const captures = this.captures[L0 - 1]; + const L = captures.length; + let moves = []; + if (longestCaptures.length > 0) { + if ( + L > 0 && + (x != captures[L-1].square[0] || y != captures[L-1].square[1]) + ) { + return []; + } + longestCaptures.forEach(lc => { + if (lc.square[0] == x && lc.square[1] == y) { + const s = lc.step; + const [i, j] = [x + s[0], y + s[1]]; + moves.push(this.getBasicMove([x, y], [i + s[0], j + s[1]], [i, j])); + } + }); + return moves; + } + // Just search simple moves: + for (let s of V.steps[V.BISHOP]) { + const [i, j] = [x + s[0], y + s[1]]; + if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) + moves.push(this.getBasicMove([x, y], [i, j])); + } + return moves; + } + + getAllValidMoves() { + const color = this.turn; + const longestCaptures = this.getAllLongestCaptures(color); + let potentialMoves = []; + for (let i = 0; i < V.size.x; i++) { + for (let j = 0; j < V.size.y; j++) { + if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) { + Array.prototype.push.apply( + potentialMoves, + this.getPotentialMovesFrom([i, j], longestCaptures) + ); + } + } + } + // Add reserve moves + potentialMoves = potentialMoves.concat( + this.getReserveMoves(V.size.x + (color == "w" ? 0 : 1)) + ); + return potentialMoves; + } + + getPossibleMovesFrom([x, y]) { + const longestCaptures = this.getAllLongestCaptures(this.getColor(x, y)); + return this.getPotentialMovesFrom([x, y], longestCaptures); + } + + filterValid(moves) { + return moves; + } + + getCheckSquares() { + return []; + } + + play(move) { + const color = this.turn; + move.turn = color; //for undo + V.PlayOnBoard(this.board, move); + if (move.vanish.length == 2) { + const L0 = this.captures.length; + let captures = this.captures[L0 - 1]; + captures.push({ + square: [move.end.x, move.end.y], + step: [(move.end.x - move.start.x)/2, (move.end.y - move.start.y)/2] + }); + if (this.atLeastOneCapture(color)) + // There could be other captures (mandatory) + move.notTheEnd = true; + } + else if (move.vanish == 0) { + const firstCode = (color == 'w' ? 65 : 97); + // Generally, reserveCount == 1 (except for shadow piece) + const reserveCount = move.appear[0].c.charCodeAt() - firstCode + 1; + this.reserve[color][V.PAWN] -= reserveCount; + if (this.reserve[color][V.PAWN] == 0) this.reserve[color] = null; + } + if (!move.notTheEnd) { + this.turn = V.GetOppCol(color); + this.movesCount++; + this.captures.push([]); + } + } + + undo(move) { + V.UndoOnBoard(this.board, move); + if (!move.notTheEnd) { + this.turn = move.turn; + this.movesCount--; + this.captures.pop(); + } + if (move.vanish.length == 0) { + const color = (move.appear[0].c == 'A' ? 'w' : 'b'); + const firstCode = (color == 'w' ? 65 : 97); + const reserveCount = move.appear[0].c.charCodeAt() - firstCode + 1; + if (!this.reserve[color]) this.reserve[color] = { [V.PAWN]: 0 }; + this.reserve[color][V.PAWN] += reserveCount; + } + else if (move.vanish.length == 2) { + const L0 = this.captures.length; + let captures = this.captures[L0 - 1]; + captures.pop(); + } + } + + atLeastOneMove() { + const color = this.turn; + if (this.atLeastOneCapture(color)) return true; + for (let i = 0; i < V.size.x; i++) { + for (let j = 0; j < V.size.y; j++) { + if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) { + const moves = this.getPotentialMovesFrom([i, j], []); + if (moves.length > 0) return true; + } + } + } + const reserveMoves = + this.getReserveMoves(V.size.x + (this.turn == "w" ? 0 : 1)); + return (reserveMoves.length > 0); + } + + getCurrentScore() { + const color = this.turn; + // If no pieces on board + reserve, I lose + if (!!this.reserve[color]) return "*"; + let atLeastOnePiece = false; + outerLoop: for (let i=0; i < V.size.x; i++) { + for (let j=0; j < V.size.y; j++) { + if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) { + atLeastOnePiece = true; + break outerLoop; + } + } + } + if (!atLeastOnePiece) return (color == 'w' ? "0-1" : "1-0"); + if (!this.atLeastOneMove()) return "1/2"; + return "*"; + } + + getComputerMove() { + // Random mover for now (TODO) + const color = this.turn; + let mvArray = []; + let mv = null; + while (this.turn == color) { + const moves = this.getAllValidMoves(); + mv = moves[randInt(moves.length)]; + mvArray.push(mv); + this.play(mv); + } + for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]); + return (mvArray.length > 1 ? mvArray : mvArray[0]); + } + + getNotation(move) { + if (move.vanish.length == 0) return "@" + V.CoordsToSquare(move.end); + const L0 = this.captures.length; + if (this.captures[L0 - 1].length > 0) return V.CoordsToSquare(move.end); + return V.CoordsToSquare(move.start) + V.CoordsToSquare(move.end); + } + +}; diff --git a/variants/Empire/class.js b/variants/Empire/class.js new file mode 100644 index 0000000..d04e4a8 --- /dev/null +++ b/variants/Empire/class.js @@ -0,0 +1,432 @@ +import { ChessRules } from "@/base_rules"; + +export class EmpireRules extends ChessRules { + + static get PawnSpecs() { + return Object.assign( + {}, + ChessRules.PawnSpecs, + { promotions: [V.QUEEN] } + ); + } + + static get LoseOnRepetition() { + return true; + } + + static IsGoodFlags(flags) { + // Only black can castle + return !!flags.match(/^[a-z]{2,2}$/); + } + + getPpath(b) { + return (b[0] == 'w' ? "Empire/" : "") + b; + } + + static GenRandInitFen(options) { + if (options.randomness == 0) + return "rnbqkbnr/pppppppp/8/8/8/PPPSSPPP/8/TECDKCET w 0 ah -"; + + // Mapping kingdom --> empire: + const piecesMap = { + 'R': 'T', + 'N': 'E', + 'B': 'C', + 'Q': 'D', + 'K': 'K' + }; + + const baseFen = ChessRules.GenRandInitFen(options); + return ( + baseFen.substr(0, 24) + "PPPSSPPP/8/" + + baseFen.substr(35, 8).split('').map(p => piecesMap[p]).join('') + + baseFen.substr(43, 5) + baseFen.substr(50) + ); + } + + getFlagsFen() { + return this.castleFlags['b'].map(V.CoordToColumn).join(""); + } + + setFlags(fenflags) { + this.castleFlags = { 'b': [-1, -1] }; + for (let i = 0; i < 2; i++) + this.castleFlags['b'][i] = V.ColumnToCoord(fenflags.charAt(i)); + } + + static get TOWER() { + return 't'; + } + static get EAGLE() { + return 'e'; + } + static get CARDINAL() { + return 'c'; + } + static get DUKE() { + return 'd'; + } + static get SOLDIER() { + return 's'; + } + // Kaiser is technically a King, so let's keep things simple. + + static get PIECES() { + return ChessRules.PIECES.concat( + [V.TOWER, V.EAGLE, V.CARDINAL, V.DUKE, V.SOLDIER]); + } + + getPotentialMovesFrom(sq) { + let moves = []; + const piece = this.getPiece(sq[0], sq[1]); + switch (piece) { + case V.TOWER: + moves = this.getPotentialTowerMoves(sq); + break; + case V.EAGLE: + moves = this.getPotentialEagleMoves(sq); + break; + case V.CARDINAL: + moves = this.getPotentialCardinalMoves(sq); + break; + case V.DUKE: + moves = this.getPotentialDukeMoves(sq); + break; + case V.SOLDIER: + moves = this.getPotentialSoldierMoves(sq); + break; + default: + moves = super.getPotentialMovesFrom(sq); + } + if ( + piece != V.KING && + this.kingPos['w'][0] != this.kingPos['b'][0] && + this.kingPos['w'][1] != this.kingPos['b'][1] + ) { + return moves; + } + // TODO: factor two next "if" into one (rank/column...) + if (this.kingPos['w'][1] == this.kingPos['b'][1]) { + const colKing = this.kingPos['w'][1]; + let intercept = 0; //count intercepting pieces + let [kingPos1, kingPos2] = [this.kingPos['w'][0], this.kingPos['b'][0]]; + if (kingPos1 > kingPos2) [kingPos1, kingPos2] = [kingPos2, kingPos1]; + for (let i = kingPos1 + 1; i < kingPos2; i++) { + if (this.board[i][colKing] != V.EMPTY) intercept++; + } + if (intercept >= 2) return moves; + // intercept == 1 (0 is impossible): + // Any move not removing intercept is OK + return moves.filter(m => { + return ( + // From another column? + m.start.y != colKing || + // From behind a king? (including kings themselves!) + m.start.x <= kingPos1 || + m.start.x >= kingPos2 || + // Intercept piece moving: must remain in-between + ( + m.end.y == colKing && + m.end.x > kingPos1 && + m.end.x < kingPos2 + ) + ); + }); + } + if (this.kingPos['w'][0] == this.kingPos['b'][0]) { + const rowKing = this.kingPos['w'][0]; + let intercept = 0; //count intercepting pieces + let [kingPos1, kingPos2] = [this.kingPos['w'][1], this.kingPos['b'][1]]; + if (kingPos1 > kingPos2) [kingPos1, kingPos2] = [kingPos2, kingPos1]; + for (let i = kingPos1 + 1; i < kingPos2; i++) { + if (this.board[rowKing][i] != V.EMPTY) intercept++; + } + if (intercept >= 2) return moves; + // intercept == 1 (0 is impossible): + // Any move not removing intercept is OK + return moves.filter(m => { + return ( + // From another row? + m.start.x != rowKing || + // From "behind" a king? (including kings themselves!) + m.start.y <= kingPos1 || + m.start.y >= kingPos2 || + // Intercept piece moving: must remain in-between + ( + m.end.x == rowKing && + m.end.y > kingPos1 && + m.end.y < kingPos2 + ) + ); + }); + } + // piece == king: check only if move.end.y == enemy king column, + // or if move.end.x == enemy king rank. + const color = this.getColor(sq[0], sq[1]); + const oppCol = V.GetOppCol(color); + return moves.filter(m => { + if ( + m.end.y != this.kingPos[oppCol][1] && + m.end.x != this.kingPos[oppCol][0] + ) { + return true; + } + // check == -1 if (row, or col) unchecked, 1 if checked and occupied, + // 0 if checked and clear + let check = [-1, -1]; + // TODO: factor two next "if"... + if (m.end.x == this.kingPos[oppCol][0]) { + if (check[0] < 0) { + // Do the check: + check[0] = 0; + let [kingPos1, kingPos2] = [m.end.y, this.kingPos[oppCol][1]]; + if (kingPos1 > kingPos2) [kingPos1, kingPos2] = [kingPos2, kingPos1]; + for (let i = kingPos1 + 1; i < kingPos2; i++) { + if (this.board[m.end.x][i] != V.EMPTY) { + check[0]++; + break; + } + } + return check[0] == 1; + } + // Check already done: + return check[0] == 1; + } + //if (m.end.y == this.kingPos[oppCol][1]) //true... + if (check[1] < 0) { + // Do the check: + check[1] = 0; + let [kingPos1, kingPos2] = [m.end.x, this.kingPos[oppCol][0]]; + if (kingPos1 > kingPos2) [kingPos1, kingPos2] = [kingPos2, kingPos1]; + for (let i = kingPos1 + 1; i < kingPos2; i++) { + if (this.board[i][m.end.y] != V.EMPTY) { + check[1]++; + break; + } + } + return check[1] == 1; + } + // Check already done: + return check[1] == 1; + }); + } + + // TODO: some merging to do with Orda method (and into base_rules.js) + getSlideNJumpMoves_([x, y], steps, oneStep) { + let moves = []; + outerLoop: for (let step of steps) { + const s = step.s; + let i = x + s[0]; + let j = y + s[1]; + while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { + if (!step.onlyTake) moves.push(this.getBasicMove([x, y], [i, j])); + // NOTE: (bad) HACK here, since onlyTake is true only for Eagle + // capturing moves, which are oneStep... + if (oneStep || step.onlyTake) continue outerLoop; + i += s[0]; + j += s[1]; + } + if (V.OnBoard(i, j) && this.canTake([x, y], [i, j]) && !step.onlyMove) + moves.push(this.getBasicMove([x, y], [i, j])); + } + return moves; + } + + static get steps() { + return ( + Object.assign( + { + t: [ + { s: [-1, 0] }, + { s: [1, 0] }, + { s: [0, -1] }, + { s: [0, 1] }, + { s: [-1, -1], onlyMove: true }, + { s: [-1, 1], onlyMove: true }, + { s: [1, -1], onlyMove: true }, + { s: [1, 1], onlyMove: true } + ], + c: [ + { s: [-1, 0], onlyMove: true }, + { s: [1, 0], onlyMove: true }, + { s: [0, -1], onlyMove: true }, + { s: [0, 1], onlyMove: true }, + { s: [-1, -1] }, + { s: [-1, 1] }, + { s: [1, -1] }, + { s: [1, 1] } + ], + e: [ + { s: [-1, 0], onlyMove: true }, + { s: [1, 0], onlyMove: true }, + { s: [0, -1], onlyMove: true }, + { s: [0, 1], onlyMove: true }, + { s: [-1, -1], onlyMove: true }, + { s: [-1, 1], onlyMove: true }, + { s: [1, -1], onlyMove: true }, + { s: [1, 1], onlyMove: true }, + { s: [-2, -1], onlyTake: true }, + { s: [-2, 1], onlyTake: true }, + { s: [-1, -2], onlyTake: true }, + { s: [-1, 2], onlyTake: true }, + { s: [1, -2], onlyTake: true }, + { s: [1, 2], onlyTake: true }, + { s: [2, -1], onlyTake: true }, + { s: [2, 1], onlyTake: true } + ] + }, + ChessRules.steps + ) + ); + } + + getPotentialTowerMoves(sq) { + return this.getSlideNJumpMoves_(sq, V.steps[V.TOWER]); + } + + getPotentialCardinalMoves(sq) { + return this.getSlideNJumpMoves_(sq, V.steps[V.CARDINAL]); + } + + getPotentialEagleMoves(sq) { + return this.getSlideNJumpMoves_(sq, V.steps[V.EAGLE]); + } + + getPotentialDukeMoves([x, y]) { + // Anything to capture around? mark other steps to explore after + let steps = []; + const oppCol = V.GetOppCol(this.getColor(x, y)); + let moves = []; + for (let s of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) { + const [i, j] = [x + s[0], y + s[1]]; + if ( + V.OnBoard(i, j) && + this.board[i][j] != V.EMPTY && + this.getColor(i, j) == oppCol + ) { + moves.push(super.getBasicMove([x, y], [i, j])); + } + else steps.push({ s: s, onlyMove: true }); + } + if (steps.length > 0) { + const noncapturingMoves = this.getSlideNJumpMoves_([x, y], steps); + Array.prototype.push.apply(moves, noncapturingMoves); + } + return moves; + } + + getPotentialKingMoves([x, y]) { + if (this.getColor(x, y) == 'b') return super.getPotentialKingMoves([x, y]); + // Empire doesn't castle: + return super.getSlideNJumpMoves( + [x, y], V.steps[V.ROOK].concat(V.steps[V.BISHOP]), 1); + } + + getPotentialSoldierMoves([x, y]) { + const c = this.getColor(x, y); + const shiftX = (c == 'w' ? -1 : 1); + const lastRank = (c == 'w' && x == 0 || c == 'b' && x == 9); + let steps = []; + if (!lastRank) steps.push([shiftX, 0]); + if (y > 0) steps.push([0, -1]); + if (y < 9) steps.push([0, 1]); + return super.getSlideNJumpMoves([x, y], steps, 1); + } + + isAttacked(sq, color) { + if (color == 'b') return super.isAttacked(sq, color); + // Empire: only pawn and king (+ queen if promotion) in common: + return ( + super.isAttackedByPawn(sq, color) || + this.isAttackedByTower(sq, color) || + this.isAttackedByEagle(sq, color) || + this.isAttackedByCardinal(sq, color) || + this.isAttackedByDuke(sq, color) || + this.isAttackedBySoldier(sq, color) || + super.isAttackedByKing(sq, color) || + super.isAttackedByQueen(sq, color) + ); + } + + isAttackedByTower(sq, color) { + return super.isAttackedBySlideNJump(sq, color, V.TOWER, V.steps[V.ROOK]); + } + + isAttackedByEagle(sq, color) { + return super.isAttackedBySlideNJump( + sq, color, V.EAGLE, V.steps[V.KNIGHT], 1); + } + + isAttackedByCardinal(sq, color) { + return super.isAttackedBySlideNJump( + sq, color, V.CARDINAL, V.steps[V.BISHOP]); + } + + isAttackedByDuke(sq, color) { + return ( + super.isAttackedBySlideNJump( + sq, color, V.DUKE, + V.steps[V.ROOK].concat(V.steps[V.BISHOP]), 1 + ) + ); + } + + isAttackedBySoldier([x, y], color) { + const shiftX = (color == 'w' ? 1 : -1); //shift from king + return super.isAttackedBySlideNJump( + [x, y], color, V.SOLDIER, [[shiftX, 0], [0, 1], [0, -1]], 1); + } + + updateCastleFlags(move, piece) { + // Only black can castle: + const firstRank = 0; + if (piece == V.KING && move.appear[0].c == 'b') + this.castleFlags['b'] = [8, 8]; + else if ( + move.start.x == firstRank && + this.castleFlags['b'].includes(move.start.y) + ) { + const flagIdx = (move.start.y == this.castleFlags['b'][0] ? 0 : 1); + this.castleFlags['b'][flagIdx] = 8; + } + else if ( + move.end.x == firstRank && + this.castleFlags['b'].includes(move.end.y) + ) { + const flagIdx = (move.end.y == this.castleFlags['b'][0] ? 0 : 1); + this.castleFlags['b'][flagIdx] = 8; + } + } + + getCurrentScore() { + // Turn has changed: + const color = V.GetOppCol(this.turn); + const lastRank = (color == 'w' ? 0 : 7); + if (this.kingPos[color][0] == lastRank) + // The opposing edge is reached! + return color == "w" ? "1-0" : "0-1"; + if (this.atLeastOneMove()) return "*"; + // Game over + const oppCol = this.turn; + return (oppCol == "w" ? "0-1" : "1-0"); + } + + static get VALUES() { + return Object.assign( + {}, + ChessRules.VALUES, + { + t: 7, + e: 7, + c: 4, + d: 4, + s: 2 + } + ); + } + + static get SEARCH_DEPTH() { + return 2; + } + +}; diff --git a/variants/Enpassant/class.js b/variants/Enpassant/class.js new file mode 100644 index 0000000..cb21830 --- /dev/null +++ b/variants/Enpassant/class.js @@ -0,0 +1,209 @@ +import { ChessRules, PiPo, Move } from "@/base_rules"; + +export class EnpassantRules extends ChessRules { + + static IsGoodEnpassant(enpassant) { + if (enpassant != "-") { + const squares = enpassant.split(","); + if (squares.length > 2) return false; + for (let sq of squares) { + const ep = V.SquareToCoords(sq); + if (isNaN(ep.x) || !V.OnBoard(ep)) return false; + } + } + return true; + } + + getPpath(b) { + return (b[1] == V.KNIGHT ? "Enpassant/" : "") + b; + } + + getEpSquare(moveOrSquare) { + if (!moveOrSquare) return undefined; + if (typeof moveOrSquare === "string") { + const square = moveOrSquare; + if (square == "-") return undefined; + // Expand init + dest squares into a full path: + const init = V.SquareToCoords(square.substr(0, 2)); + let newPath = [init]; + if (square.length == 2) return newPath; + const dest = V.SquareToCoords(square.substr(2)); + const delta = ['x', 'y'].map(i => Math.abs(dest[i] - init[i])); + // Check if it's a knight(rider) movement: + let step = [0, 0]; + if (delta[0] > 0 && delta[1] > 0 && delta[0] != delta[1]) { + // Knightrider + const minShift = Math.min(delta[0], delta[1]); + step[0] = (dest.x - init.x) / minShift; + step[1] = (dest.y - init.y) / minShift; + } else { + // "Sliders" + step = ['x', 'y'].map((i, idx) => { + return (dest[i] - init[i]) / delta[idx] || 0 + }); + } + let x = init.x + step[0], + y = init.y + step[1]; + while (x != dest.x || y != dest.y) { + newPath.push({ x: x, y: y }); + x += step[0]; + y += step[1]; + } + newPath.push(dest); + return newPath; + } + // Argument is a move: all intermediate squares are en-passant candidates, + // except if the moving piece is a king. + const move = moveOrSquare; + const piece = move.appear[0].p; + if (piece == V.KING || + ( + Math.abs(move.end.x-move.start.x) <= 1 && + Math.abs(move.end.y-move.start.y) <= 1 + ) + ) { + return undefined; + } + const delta = [move.end.x-move.start.x, move.end.y-move.start.y]; + let step = undefined; + if (piece == V.KNIGHT) { + const divisor = Math.min(Math.abs(delta[0]), Math.abs(delta[1])); + step = [delta[0]/divisor || 0, delta[1]/divisor || 0]; + } else { + step = [ + delta[0]/Math.abs(delta[0]) || 0, + delta[1]/Math.abs(delta[1]) || 0 + ]; + } + let res = []; + for ( + let [x,y] = [move.start.x+step[0],move.start.y+step[1]]; + x != move.end.x || y != move.end.y; + x += step[0], y += step[1] + ) { + res.push({ x: x, y: y }); + } + // Add final square to know which piece is taken en passant: + res.push(move.end); + return res; + } + + getEnpassantFen() { + const L = this.epSquares.length; + if (!this.epSquares[L - 1]) return "-"; //no en-passant + const epsq = this.epSquares[L - 1]; + if (epsq.length <= 2) return epsq.map(V.CoordsToSquare).join(""); + // Condensate path: just need initial and final squares: + return V.CoordsToSquare(epsq[0]) + V.CoordsToSquare(epsq[epsq.length - 1]); + } + + getPotentialMovesFrom([x, y]) { + let moves = super.getPotentialMovesFrom([x,y]); + // Add en-passant captures from this square: + const L = this.epSquares.length; + if (!this.epSquares[L - 1]) return moves; + const squares = this.epSquares[L - 1]; + const S = squares.length; + // Object describing the removed opponent's piece: + const pipoV = new PiPo({ + x: squares[S-1].x, + y: squares[S-1].y, + c: V.GetOppCol(this.turn), + p: this.getPiece(squares[S-1].x, squares[S-1].y) + }); + // Check if existing non-capturing moves could also capture en passant + moves.forEach(m => { + if ( + m.appear[0].p != V.PAWN && //special pawn case is handled elsewhere + m.vanish.length <= 1 && + [...Array(S-1).keys()].some(i => { + return m.end.x == squares[i].x && m.end.y == squares[i].y; + }) + ) { + m.vanish.push(pipoV); + } + }); + // Special case of the king knight's movement: + if (this.getPiece(x, y) == V.KING) { + V.steps[V.KNIGHT].forEach(step => { + const endX = x + step[0]; + const endY = y + step[1]; + if ( + V.OnBoard(endX, endY) && + [...Array(S-1).keys()].some(i => { + return endX == squares[i].x && endY == squares[i].y; + }) + ) { + let enpassantMove = this.getBasicMove([x, y], [endX, endY]); + enpassantMove.vanish.push(pipoV); + moves.push(enpassantMove); + } + }); + } + return moves; + } + + getEnpassantCaptures([x, y], shiftX) { + const Lep = this.epSquares.length; + const squares = this.epSquares[Lep - 1]; + let moves = []; + if (!!squares) { + const S = squares.length; + const taken = squares[S-1]; + const pipoV = new PiPo({ + x: taken.x, + y: taken.y, + p: this.getPiece(taken.x, taken.y), + c: this.getColor(taken.x, taken.y) + }); + [...Array(S-1).keys()].forEach(i => { + const sq = squares[i]; + if (sq.x == x + shiftX && Math.abs(sq.y - y) == 1) { + let enpassantMove = this.getBasicMove([x, y], [sq.x, sq.y]); + enpassantMove.vanish.push(pipoV); + moves.push(enpassantMove); + } + }); + } + return moves; + } + + // Remove the "onestep" condition: knight promote to knightrider: + getPotentialKnightMoves(sq) { + return this.getSlideNJumpMoves(sq, V.steps[V.KNIGHT]); + } + + filterValid(moves) { + const filteredMoves = super.filterValid(moves); + // If at least one full move made, everything is allowed: + if (this.movesCount >= 2) + return filteredMoves; + // Else, forbid captures: + return filteredMoves.filter(m => m.vanish.length == 1); + } + + isAttackedByKnight(sq, color) { + return this.isAttackedBySlideNJump( + sq, + color, + V.KNIGHT, + V.steps[V.KNIGHT] + ); + } + + static get SEARCH_DEPTH() { + return 2; + } + + static get VALUES() { + return { + p: 1, + r: 5, + n: 4, + b: 3, + q: 9, + k: 1000 + }; + } + +}; diff --git a/variants/Evolution/class.js b/variants/Evolution/class.js new file mode 100644 index 0000000..5250089 --- /dev/null +++ b/variants/Evolution/class.js @@ -0,0 +1,34 @@ +import { ChessRules } from "@/base_rules"; + +export class EvolutionRules extends ChessRules { + + getPotentialMovesFrom([x, y]) { + let moves = super.getPotentialMovesFrom([x, y]); + const c = this.getColor(x, y); + const piece = this.getPiece(x, y); + if ( + [V.BISHOP, V.ROOK, V.QUEEN].includes(piece) && + (c == 'w' && x == 7) || (c == 'b' && x == 0) + ) { + // Move from first rank + const forward = (c == 'w' ? -1 : 1); + for (let shift of [-2, 0, 2]) { + if ( + (piece == V.ROOK && shift != 0) || + (piece == V.BISHOP && shift == 0) + ) { + continue; + } + if ( + V.OnBoard(x+2*forward, y+shift) && + this.board[x+forward][y+shift/2] != V.EMPTY && + this.getColor(x+2*forward, y+shift) != c + ) { + moves.push(this.getBasicMove([x,y], [x+2*forward,y+shift])); + } + } + } + return moves; + } + +}; diff --git a/variants/Extinction/class.js b/variants/Extinction/class.js new file mode 100644 index 0000000..e42e294 --- /dev/null +++ b/variants/Extinction/class.js @@ -0,0 +1,118 @@ +import { ChessRules } from "@/base_rules"; + +export class ExtinctionRules extends ChessRules { + + static get PawnSpecs() { + return Object.assign( + {}, + ChessRules.PawnSpecs, + { promotions: ChessRules.PawnSpecs.promotions.concat([V.KING]) } + ); + } + + static IsGoodPosition(position) { + if (!ChessRules.IsGoodPosition(position)) return false; + // Also check that each piece type is present + const rows = position.split("/"); + let pieces = {}; + for (let row of rows) { + for (let i = 0; i < row.length; i++) { + if (isNaN(parseInt(row[i], 10)) && !pieces[row[i]]) + pieces[row[i]] = true; + } + } + if (Object.keys(pieces).length != 12) return false; + return true; + } + + setOtherVariables(fen) { + super.setOtherVariables(fen); + const pos = V.ParseFen(fen).position; + // NOTE: no need for safety "|| []", because each piece type is present + // (otherwise game is already over!) + this.material = { + w: { + [V.KING]: pos.match(/K/g).length, + [V.QUEEN]: pos.match(/Q/g).length, + [V.ROOK]: pos.match(/R/g).length, + [V.KNIGHT]: pos.match(/N/g).length, + [V.BISHOP]: pos.match(/B/g).length, + [V.PAWN]: pos.match(/P/g).length + }, + b: { + [V.KING]: pos.match(/k/g).length, + [V.QUEEN]: pos.match(/q/g).length, + [V.ROOK]: pos.match(/r/g).length, + [V.KNIGHT]: pos.match(/n/g).length, + [V.BISHOP]: pos.match(/b/g).length, + [V.PAWN]: pos.match(/p/g).length + } + }; + } + + // TODO: verify this assertion + atLeastOneMove() { + return true; //always at least one possible move + } + + filterValid(moves) { + return moves; //there is no check + } + + getCheckSquares() { + return []; + } + + postPlay(move) { + super.postPlay(move); + // Treat the promotion case: (not the capture part) + if (move.appear[0].p != move.vanish[0].p) { + this.material[move.appear[0].c][move.appear[0].p]++; + this.material[move.appear[0].c][V.PAWN]--; + } + if (move.vanish.length == 2 && move.appear.length == 1) + //capture + this.material[move.vanish[1].c][move.vanish[1].p]--; + } + + postUndo(move) { + super.postUndo(move); + if (move.appear[0].p != move.vanish[0].p) { + this.material[move.appear[0].c][move.appear[0].p]--; + this.material[move.appear[0].c][V.PAWN]++; + } + if (move.vanish.length == 2 && move.appear.length == 1) + this.material[move.vanish[1].c][move.vanish[1].p]++; + } + + getCurrentScore() { + if (this.atLeastOneMove()) { + // Game not over? + const color = this.turn; + if ( + Object.keys(this.material[color]).some(p => { + return this.material[color][p] == 0; + }) + ) { + return this.turn == "w" ? "0-1" : "1-0"; + } + return "*"; + } + return this.turn == "w" ? "0-1" : "1-0"; //NOTE: currently unreachable... + } + + evalPosition() { + const color = this.turn; + if ( + Object.keys(this.material[color]).some(p => { + return this.material[color][p] == 0; + }) + ) { + // Very negative (resp. positive) + // if white (reps. black) pieces set is incomplete + return (color == "w" ? -1 : 1) * V.INFINITY; + } + return super.evalPosition(); + } + +}; diff --git a/variants/Fanorona/class.js b/variants/Fanorona/class.js new file mode 100644 index 0000000..e1e653a --- /dev/null +++ b/variants/Fanorona/class.js @@ -0,0 +1,342 @@ +import { ChessRules, Move, PiPo } from "@/base_rules"; +import { randInt } from "@/utils/alea"; + +export class FanoronaRules extends ChessRules { + + static get Options() { + return null; + } + + static get HasFlags() { + return false; + } + + static get HasEnpassant() { + return false; + } + + static get Monochrome() { + return true; + } + + static get Lines() { + let lines = []; + // Draw all inter-squares lines, shifted: + for (let i = 0; i < V.size.x; i++) + lines.push([[i+0.5, 0.5], [i+0.5, V.size.y-0.5]]); + for (let j = 0; j < V.size.y; j++) + lines.push([[0.5, j+0.5], [V.size.x-0.5, j+0.5]]); + const columnDiags = [ + [[0.5, 0.5], [2.5, 2.5]], + [[0.5, 2.5], [2.5, 0.5]], + [[2.5, 0.5], [4.5, 2.5]], + [[4.5, 0.5], [2.5, 2.5]] + ]; + for (let j of [0, 2, 4, 6]) { + lines = lines.concat( + columnDiags.map(L => [[L[0][0], L[0][1] + j], [L[1][0], L[1][1] + j]]) + ); + } + return lines; + } + + static get Notoodark() { + return true; + } + + static GenRandInitFen() { + return "ppppppppp/ppppppppp/pPpP1pPpP/PPPPPPPPP/PPPPPPPPP w 0"; + } + + setOtherVariables(fen) { + // Local stack of captures during a turn (squares + directions) + this.captures = [ [] ]; + } + + static get size() { + return { x: 5, y: 9 }; + } + + getPiece() { + return V.PAWN; + } + + static IsGoodPosition(position) { + if (position.length == 0) return false; + const rows = position.split("/"); + if (rows.length != V.size.x) return false; + for (let row of rows) { + let sumElts = 0; + for (let i = 0; i < row.length; i++) { + if (row[i].toLowerCase() == V.PAWN) sumElts++; + else { + const num = parseInt(row[i], 10); + if (isNaN(num) || num <= 0) return false; + sumElts += num; + } + } + if (sumElts != V.size.y) return false; + } + return true; + } + + getPpath(b) { + return "Fanorona/" + b; + } + + getPPpath(m, orientation) { + // m.vanish.length >= 2, first capture gives direction + const ref = (Math.abs(m.vanish[1].x - m.start.x) == 1 ? m.start : m.end); + const step = [m.vanish[1].x - ref.x, m.vanish[1].y - ref.y]; + const multStep = (orientation == 'w' ? 1 : -1); + const normalizedStep = [ + multStep * step[0] / Math.abs(step[0]), + multStep * step[1] / Math.abs(step[1]) + ]; + return ( + "Fanorona/arrow_" + + (normalizedStep[0] || 0) + "_" + (normalizedStep[1] || 0) + ); + } + + // After moving, add stones captured in "step" direction from new location + // [x, y] to mv.vanish (if any captured stone!) + addCapture([x, y], step, move) { + let [i, j] = [x + step[0], y + step[1]]; + const oppCol = V.GetOppCol(move.vanish[0].c); + while ( + V.OnBoard(i, j) && + this.board[i][j] != V.EMPTY && + this.getColor(i, j) == oppCol + ) { + move.vanish.push(new PiPo({ x: i, y: j, c: oppCol, p: V.PAWN })); + i += step[0]; + j += step[1]; + } + return (move.vanish.length >= 2); + } + + getPotentialMovesFrom([x, y]) { + const L0 = this.captures.length; + const captures = this.captures[L0 - 1]; + const L = captures.length; + if (L > 0) { + var c = captures[L-1]; + if (x != c.square.x + c.step[0] || y != c.square.y + c.step[1]) + return []; + } + const oppCol = V.GetOppCol(this.turn); + let steps = V.steps[V.ROOK]; + if ((x + y) % 2 == 0) steps = steps.concat(V.steps[V.BISHOP]); + let moves = []; + for (let s of steps) { + if (L > 0 && c.step[0] == s[0] && c.step[1] == s[1]) { + // Add a move to say "I'm done capturing" + moves.push( + new Move({ + appear: [], + vanish: [], + start: { x: x, y: y }, + end: { x: x - s[0], y: y - s[1] } + }) + ); + continue; + } + let [i, j] = [x + s[0], y + s[1]]; + if (captures.some(c => c.square.x == i && c.square.y == j)) continue; + if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { + // The move is potentially allowed. Might lead to 2 different captures + let mv = super.getBasicMove([x, y], [i, j]); + const capt = this.addCapture([i, j], s, mv); + if (capt) { + moves.push(mv); + mv = super.getBasicMove([x, y], [i, j]); + } + const capt_bw = this.addCapture([x, y], [-s[0], -s[1]], mv); + if (capt_bw) moves.push(mv); + // Captures take priority (if available) + if (!capt && !capt_bw && L == 0) moves.push(mv); + } + } + return moves; + } + + atLeastOneCapture() { + const color = this.turn; + const oppCol = V.GetOppCol(color); + const L0 = this.captures.length; + const captures = this.captures[L0 - 1]; + const L = captures.length; + if (L > 0) { + // If some adjacent enemy stone, with free space to capture it, + // toward a square not already visited, through a different step + // from last one: then yes. + const c = captures[L-1]; + const [x, y] = [c.square.x + c.step[0], c.square.y + c.step[1]]; + let steps = V.steps[V.ROOK]; + if ((x + y) % 2 == 0) steps = steps.concat(V.steps[V.BISHOP]); + // TODO: half of the steps explored are redundant + for (let s of steps) { + if (s[0] == c.step[0] && s[1] == c.step[1]) continue; + const [i, j] = [x + s[0], y + s[1]]; + if ( + !V.OnBoard(i, j) || + this.board[i][j] != V.EMPTY || + captures.some(c => c.square.x == i && c.square.y == j) + ) { + continue; + } + if ( + V.OnBoard(i + s[0], j + s[1]) && + this.board[i + s[0]][j + s[1]] != V.EMPTY && + this.getColor(i + s[0], j + s[1]) == oppCol + ) { + return true; + } + if ( + V.OnBoard(x - s[0], y - s[1]) && + this.board[x - s[0]][y - s[1]] != V.EMPTY && + this.getColor(x - s[0], y - s[1]) == oppCol + ) { + return true; + } + } + return false; + } + for (let i = 0; i < V.size.x; i++) { + for (let j = 0; j < V.size.y; j++) { + if ( + this.board[i][j] != V.EMPTY && + this.getColor(i, j) == color && + // TODO: this could be more efficient + this.getPotentialMovesFrom([i, j]).some(m => m.vanish.length >= 2) + ) { + return true; + } + } + } + return false; + } + + static KeepCaptures(moves) { + return moves.filter(m => m.vanish.length >= 2); + } + + getPossibleMovesFrom(sq) { + let moves = this.getPotentialMovesFrom(sq); + const L0 = this.captures.length; + const captures = this.captures[L0 - 1]; + if (captures.length > 0) return this.getPotentialMovesFrom(sq); + const captureMoves = V.KeepCaptures(moves); + if (captureMoves.length > 0) return captureMoves; + if (this.atLeastOneCapture()) return []; + return moves; + } + + getAllValidMoves() { + const moves = super.getAllValidMoves(); + if (moves.some(m => m.vanish.length >= 2)) return V.KeepCaptures(moves); + return moves; + } + + filterValid(moves) { + return moves; + } + + getCheckSquares() { + return []; + } + + play(move) { + const color = this.turn; + move.turn = color; //for undo + V.PlayOnBoard(this.board, move); + if (move.vanish.length >= 2) { + const L0 = this.captures.length; + let captures = this.captures[L0 - 1]; + captures.push({ + square: move.start, + step: [move.end.x - move.start.x, move.end.y - move.start.y] + }); + if (this.atLeastOneCapture()) + // There could be other captures (optional) + move.notTheEnd = true; + } + if (!move.notTheEnd) { + this.turn = V.GetOppCol(color); + this.movesCount++; + this.captures.push([]); + } + } + + undo(move) { + V.UndoOnBoard(this.board, move); + if (!move.notTheEnd) { + this.turn = move.turn; + this.movesCount--; + this.captures.pop(); + } + if (move.vanish.length >= 2) { + const L0 = this.captures.length; + let captures = this.captures[L0 - 1]; + captures.pop(); + } + } + + getCurrentScore() { + const color = this.turn; + // If no stones on board, I lose + if ( + this.board.every(b => { + return b.every(cell => { + return (cell == "" || cell[0] != color); + }); + }) + ) { + return (color == 'w' ? "0-1" : "1-0"); + } + return "*"; + } + + getComputerMove() { + const moves = this.getAllValidMoves(); + if (moves.length == 0) return null; + const color = this.turn; + // Capture available? If yes, play it + let captures = moves.filter(m => m.vanish.length >= 2); + let mvArray = []; + while (captures.length >= 1) { + // Then just pick random captures (trying to maximize) + let candidates = captures.filter(c => !!c.notTheEnd); + let mv = null; + if (candidates.length >= 1) mv = candidates[randInt(candidates.length)]; + else mv = captures[randInt(captures.length)]; + this.play(mv); + mvArray.push(mv); + captures = (this.turn == color ? this.getAllValidMoves() : []); + } + if (mvArray.length >= 1) { + for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]); + return mvArray; + } + // Just play a random move, which if possible does not let a capture + let candidates = []; + for (let m of moves) { + this.play(m); + if (!this.atLeastOneCapture()) candidates.push(m); + this.undo(m); + } + if (candidates.length >= 1) return candidates[randInt(candidates.length)]; + return moves[randInt(moves.length)]; + } + + getNotation(move) { + if (move.appear.length == 0) return "stop"; + return ( + V.CoordsToSquare(move.start) + + V.CoordsToSquare(move.end) + + (move.vanish.length >= 2 ? "X" : "") + ); + } + +}; diff --git a/variants/Sleepy/class.js b/variants/Sleepy/class.js new file mode 100644 index 0000000..e58ceae --- /dev/null +++ b/variants/Sleepy/class.js @@ -0,0 +1,110 @@ +import ChessRules from "/base_rules.js"; +import PiPo from "/utils/PiPo.js"; +import Move from "/utils/Move.js"; + +export default class SleepyRules extends ChessRules { + + static get Options() { + return { + select: C.Options.select, + input: {}, + styles: ["cylinder"] //TODO + }; + } + + setOtherVariables(fenParsed) { + super.setOtherVariables(fenParsed); + // Stack of "last move" only for intermediate chaining + this.lastMoveEnd = []; + } + + getBasicMove([sx, sy], [ex, ey], tr) { + const L = this.lastMoveEnd.length; + const piece = (L >= 1 ? this.lastMoveEnd[L-1].p : null); + if ( + this.board[ex][ey] == "" || + this.getColor(ex, ey) == C.GetOppTurn(this.turn) + ) { + if (piece && !tr) + tr = {c: this.turn, p: piece}; + let mv = super.getBasicMove([sx, sy], [ex, ey], tr); + if (piece) + mv.vanish.pop(); //end of a chain: initial piece remains + return mv; + } + // (Self)Capture: initial, or inside a chain + const initPiece = (piece || this.getPiece(sx, sy)), + destPiece = this.getPiece(ex, ey); + let mv = new Move({ + start: {x: sx, y: sy}, + end: {x: ex, y: ey}, + appear: [ + new PiPo({ + x: ex, + y: ey, + c: this.turn, + p: (!!tr ? tr.p : initPiece) + }) + ], + vanish: [ + new PiPo({ + x: ex, + y: ey, + c: this.turn, + p: destPiece + }) + ] + }); + if (!piece) { + // Initial capture + mv.vanish.unshift( + new PiPo({ + x: sx, + y: sy, + c: this.turn, + p: initPiece + }) + ); + } + mv.chained = destPiece; //easier (no need to detect it) +// mv.drag = {c: this.turn, p: initPiece}; //TODO: doesn't work + return mv; + } + + getPiece(x, y) { + const L = this.lastMoveEnd.length; + if (L >= 1 && this.lastMoveEnd[L-1].x == x && this.lastMoveEnd[L-1].y == y) + return this.lastMoveEnd[L-1].p; + return super.getPiece(x, y); + } + + getPotentialMovesFrom([x, y], color) { + const L = this.lastMoveEnd.length; + if ( + L >= 1 && + (x != this.lastMoveEnd[L-1].x || y != this.lastMoveEnd[L-1].y) + ) { + // A self-capture was played: wrong square + return []; + } + return super.getPotentialMovesFrom([x, y], color); + } + + isLastMove(move) { + return !move.chained; + } + + postPlay(move) { + super.postPlay(move); + if (!!move.chained) { + this.lastMoveEnd.push({ + x: move.end.x, + y: move.end.y, + p: move.chained + }); + } + else + this.lastMoveEnd = []; + } + +}; diff --git a/variants/Sleepy/rules.html b/variants/Sleepy/rules.html new file mode 100644 index 0000000..f27cd96 --- /dev/null +++ b/variants/Sleepy/rules.html @@ -0,0 +1,7 @@ +

+ You can "capture" your own pieces, and then move them from the capturing + square in the same turn, with potential chaining if the captured unit + makes a self-capture too. +

+ +

Benjamin Auder (2021).

diff --git a/variants/Sleepy/style.css b/variants/Sleepy/style.css new file mode 100644 index 0000000..a3550bc --- /dev/null +++ b/variants/Sleepy/style.css @@ -0,0 +1 @@ +@import url("/base_pieces.css");