UNPKG

chessops

Version:

Chess and chess variant rules and operations

902 lines (807 loc) 30.9 kB
import { Result } from '@badrap/result'; import { between, kingAttacks, pawnAttacks } from './attacks.js'; import { Board } from './board.js'; import { Castles, castlingSide, Chess, Context, equalsIgnoreMoves, IllegalSetup, isImpossibleCheck, isStandardMaterialSide, normalizeMove, Position, PositionError, pseudoDests, } from './chess.js'; import { Material, MaterialSide, RemainingChecks, Setup } from './setup.js'; import { SquareSet } from './squareSet.js'; import { Color, COLORS, Outcome, Piece, Rules, Square } from './types.js'; import { defined, opposite } from './util.js'; export { Castles, castlingSide, Chess, Context, equalsIgnoreMoves, IllegalSetup, isImpossibleCheck, normalizeMove, Position, PositionError, }; export class Crazyhouse extends Position { private constructor() { super('crazyhouse'); } reset() { super.reset(); this.pockets = Material.empty(); } protected setupUnchecked(setup: Setup) { super.setupUnchecked(setup); this.board.promoted = setup.board.promoted .intersect(setup.board.occupied) .diff(setup.board.king) .diff(setup.board.pawn); this.pockets = setup.pockets ? setup.pockets.clone() : Material.empty(); } static default(): Crazyhouse { const pos = new this(); pos.reset(); return pos; } static fromSetup(setup: Setup): Result<Crazyhouse, PositionError> { const pos = new this(); pos.setupUnchecked(setup); return pos.validate().map(_ => pos); } clone(): Crazyhouse { return super.clone() as Crazyhouse; } protected validate(): Result<undefined, PositionError> { return super.validate().chain(_ => { if (this.pockets?.count('king')) { return Result.err(new PositionError(IllegalSetup.Kings)); } if ((this.pockets?.size() || 0) + this.board.occupied.size() > 64) { return Result.err(new PositionError(IllegalSetup.Variant)); } return Result.ok(undefined); }); } hasInsufficientMaterial(color: Color): boolean { // No material can leave the game, but we can easily check this for // custom positions. if (!this.pockets) return super.hasInsufficientMaterial(color); return ( this.board.occupied.size() + this.pockets.size() <= 3 && this.board.pawn.isEmpty() && this.board.promoted.isEmpty() && this.board.rooksAndQueens().isEmpty() && this.pockets.count('pawn') <= 0 && this.pockets.count('rook') <= 0 && this.pockets.count('queen') <= 0 ); } dropDests(ctx?: Context): SquareSet { const mask = this.board.occupied .complement() .intersect( this.pockets?.[this.turn].hasNonPawns() ? SquareSet.full() : this.pockets?.[this.turn].hasPawns() ? SquareSet.backranks().complement() : SquareSet.empty(), ); ctx = ctx || this.ctx(); if (defined(ctx.king) && ctx.checkers.nonEmpty()) { const checker = ctx.checkers.singleSquare(); if (!defined(checker)) return SquareSet.empty(); return mask.intersect(between(checker, ctx.king)); } else return mask; } } export class Atomic extends Position { private constructor() { super('atomic'); } static default(): Atomic { const pos = new this(); pos.reset(); return pos; } static fromSetup(setup: Setup): Result<Atomic, PositionError> { const pos = new this(); pos.setupUnchecked(setup); return pos.validate().map(_ => pos); } clone(): Atomic { return super.clone() as Atomic; } protected validate(): Result<undefined, PositionError> { // Like chess, but allow our king to be missing. if (this.board.occupied.isEmpty()) return Result.err(new PositionError(IllegalSetup.Empty)); if (this.board.king.size() > 2) return Result.err(new PositionError(IllegalSetup.Kings)); const otherKing = this.board.kingOf(opposite(this.turn)); if (!defined(otherKing)) return Result.err(new PositionError(IllegalSetup.Kings)); if (this.kingAttackers(otherKing, this.turn, this.board.occupied).nonEmpty()) { return Result.err(new PositionError(IllegalSetup.OppositeCheck)); } if (SquareSet.backranks().intersects(this.board.pawn)) { return Result.err(new PositionError(IllegalSetup.PawnsOnBackrank)); } return Result.ok(undefined); } kingAttackers(square: Square, attacker: Color, occupied: SquareSet): SquareSet { const attackerKings = this.board.pieces(attacker, 'king'); if (attackerKings.isEmpty() || kingAttacks(square).intersects(attackerKings)) { return SquareSet.empty(); } return super.kingAttackers(square, attacker, occupied); } protected playCaptureAt(square: Square, captured: Piece): void { super.playCaptureAt(square, captured); this.board.take(square); for (const explode of kingAttacks(square).intersect(this.board.occupied).diff(this.board.pawn)) { const piece = this.board.take(explode); if (piece?.role === 'rook') this.castles.discardRook(explode); if (piece?.role === 'king') this.castles.discardColor(piece.color); } } hasInsufficientMaterial(color: Color): boolean { // Remaining material does not matter if the enemy king is already // exploded. if (this.board.pieces(opposite(color), 'king').isEmpty()) return false; // Bare king cannot mate. if (this.board[color].diff(this.board.king).isEmpty()) return true; // As long as the enemy king is not alone, there is always a chance their // own pieces explode next to it. if (this.board[opposite(color)].diff(this.board.king).nonEmpty()) { // Unless there are only bishops that cannot explode each other. if (this.board.occupied.equals(this.board.bishop.union(this.board.king))) { if (!this.board.bishop.intersect(this.board.white).intersects(SquareSet.darkSquares())) { return !this.board.bishop.intersect(this.board.black).intersects(SquareSet.lightSquares()); } if (!this.board.bishop.intersect(this.board.white).intersects(SquareSet.lightSquares())) { return !this.board.bishop.intersect(this.board.black).intersects(SquareSet.darkSquares()); } } return false; } // Queen or pawn (future queen) can give mate against bare king. if (this.board.queen.nonEmpty() || this.board.pawn.nonEmpty()) return false; // Single knight, bishop or rook cannot mate against bare king. if (this.board.knight.union(this.board.bishop).union(this.board.rook).size() === 1) return true; // If only knights, more than two are required to mate bare king. if (this.board.occupied.equals(this.board.knight.union(this.board.king))) { return this.board.knight.size() <= 2; } return false; } dests(square: Square, ctx?: Context): SquareSet { ctx = ctx || this.ctx(); let dests = SquareSet.empty(); for (const to of pseudoDests(this, square, ctx)) { const after = this.clone(); after.play({ from: square, to }); const ourKing = after.board.kingOf(this.turn); if ( defined(ourKing) && (!defined(after.board.kingOf(after.turn)) || after.kingAttackers(ourKing, after.turn, after.board.occupied).isEmpty()) ) { dests = dests.with(to); } } return dests; } isVariantEnd(): boolean { return !!this.variantOutcome(); } variantOutcome(_ctx?: Context): Outcome | undefined { for (const color of COLORS) { if (this.board.pieces(color, 'king').isEmpty()) return { winner: opposite(color) }; } return; } } export class Antichess extends Position { private constructor() { super('antichess'); } reset() { super.reset(); this.castles = Castles.empty(); } protected setupUnchecked(setup: Setup) { super.setupUnchecked(setup); this.castles = Castles.empty(); } static default(): Antichess { const pos = new this(); pos.reset(); return pos; } static fromSetup(setup: Setup): Result<Antichess, PositionError> { const pos = new this(); pos.setupUnchecked(setup); return pos.validate().map(_ => pos); } clone(): Antichess { return super.clone() as Antichess; } protected validate(): Result<undefined, PositionError> { if (this.board.occupied.isEmpty()) return Result.err(new PositionError(IllegalSetup.Empty)); if (SquareSet.backranks().intersects(this.board.pawn)) { return Result.err(new PositionError(IllegalSetup.PawnsOnBackrank)); } return Result.ok(undefined); } kingAttackers(_square: Square, _attacker: Color, _occupied: SquareSet): SquareSet { return SquareSet.empty(); } ctx(): Context { const ctx = super.ctx(); if ( defined(this.epSquare) && pawnAttacks(opposite(this.turn), this.epSquare).intersects(this.board.pieces(this.turn, 'pawn')) ) { ctx.mustCapture = true; return ctx; } const enemy = this.board[opposite(this.turn)]; for (const from of this.board[this.turn]) { if (pseudoDests(this, from, ctx).intersects(enemy)) { ctx.mustCapture = true; return ctx; } } return ctx; } dests(square: Square, ctx?: Context): SquareSet { ctx = ctx || this.ctx(); const dests = pseudoDests(this, square, ctx); const enemy = this.board[opposite(this.turn)]; return dests.intersect( ctx.mustCapture ? defined(this.epSquare) && this.board.getRole(square) === 'pawn' ? enemy.with(this.epSquare) : enemy : SquareSet.full(), ); } hasInsufficientMaterial(color: Color): boolean { if (this.board[color].isEmpty()) return false; if (this.board[opposite(color)].isEmpty()) return true; if (this.board.occupied.equals(this.board.bishop)) { const weSomeOnLight = this.board[color].intersects(SquareSet.lightSquares()); const weSomeOnDark = this.board[color].intersects(SquareSet.darkSquares()); const theyAllOnDark = this.board[opposite(color)].isDisjoint(SquareSet.lightSquares()); const theyAllOnLight = this.board[opposite(color)].isDisjoint(SquareSet.darkSquares()); return (weSomeOnLight && theyAllOnDark) || (weSomeOnDark && theyAllOnLight); } if (this.board.occupied.equals(this.board.knight) && this.board.occupied.size() === 2) { return ( (this.board.white.intersects(SquareSet.lightSquares()) !== this.board.black.intersects(SquareSet.darkSquares())) !== (this.turn === color) ); } return false; } isVariantEnd(): boolean { return this.board[this.turn].isEmpty(); } variantOutcome(ctx?: Context): Outcome | undefined { ctx = ctx || this.ctx(); if (ctx.variantEnd || this.isStalemate(ctx)) { return { winner: this.turn }; } return; } } export class KingOfTheHill extends Position { private constructor() { super('kingofthehill'); } static default(): KingOfTheHill { const pos = new this(); pos.reset(); return pos; } static fromSetup(setup: Setup): Result<KingOfTheHill, PositionError> { const pos = new this(); pos.setupUnchecked(setup); return pos.validate().map(_ => pos); } clone(): KingOfTheHill { return super.clone() as KingOfTheHill; } hasInsufficientMaterial(_color: Color): boolean { return false; } isVariantEnd(): boolean { return this.board.king.intersects(SquareSet.center()); } variantOutcome(_ctx?: Context): Outcome | undefined { for (const color of COLORS) { if (this.board.pieces(color, 'king').intersects(SquareSet.center())) return { winner: color }; } return; } } export class ThreeCheck extends Position { private constructor() { super('3check'); } reset() { super.reset(); this.remainingChecks = RemainingChecks.default(); } protected setupUnchecked(setup: Setup) { super.setupUnchecked(setup); this.remainingChecks = setup.remainingChecks?.clone() || RemainingChecks.default(); } static default(): ThreeCheck { const pos = new this(); pos.reset(); return pos; } static fromSetup(setup: Setup): Result<ThreeCheck, PositionError> { const pos = new this(); pos.setupUnchecked(setup); return pos.validate().map(_ => pos); } clone(): ThreeCheck { return super.clone() as ThreeCheck; } hasInsufficientMaterial(color: Color): boolean { return this.board.pieces(color, 'king').equals(this.board[color]); } isVariantEnd(): boolean { return !!this.remainingChecks && (this.remainingChecks.white <= 0 || this.remainingChecks.black <= 0); } variantOutcome(_ctx?: Context): Outcome | undefined { if (this.remainingChecks) { for (const color of COLORS) { if (this.remainingChecks[color] <= 0) return { winner: color }; } } return; } } const racingKingsBoard = (): Board => { const board = Board.empty(); board.occupied = new SquareSet(0xffff, 0); board.promoted = SquareSet.empty(); board.white = new SquareSet(0xf0f0, 0); board.black = new SquareSet(0x0f0f, 0); board.pawn = SquareSet.empty(); board.knight = new SquareSet(0x1818, 0); board.bishop = new SquareSet(0x2424, 0); board.rook = new SquareSet(0x4242, 0); board.queen = new SquareSet(0x0081, 0); board.king = new SquareSet(0x8100, 0); return board; }; export class RacingKings extends Position { private constructor() { super('racingkings'); } reset() { this.board = racingKingsBoard(); this.pockets = undefined; this.turn = 'white'; this.castles = Castles.empty(); this.epSquare = undefined; this.remainingChecks = undefined; this.halfmoves = 0; this.fullmoves = 1; } setupUnchecked(setup: Setup) { super.setupUnchecked(setup); this.castles = Castles.empty(); } static default(): RacingKings { const pos = new this(); pos.reset(); return pos; } static fromSetup(setup: Setup): Result<RacingKings, PositionError> { const pos = new this(); pos.setupUnchecked(setup); return pos.validate().map(_ => pos); } clone(): RacingKings { return super.clone() as RacingKings; } protected validate(): Result<undefined, PositionError> { if (this.isCheck() || this.board.pawn.nonEmpty()) return Result.err(new PositionError(IllegalSetup.Variant)); return super.validate(); } dests(square: Square, ctx?: Context): SquareSet { ctx = ctx || this.ctx(); // Kings cannot give check. if (square === ctx.king) return super.dests(square, ctx); // Do not allow giving check. let dests = SquareSet.empty(); for (const to of super.dests(square, ctx)) { // Valid, because there are no promotions (or even pawns). const move = { from: square, to }; const after = this.clone(); after.play(move); if (!after.isCheck()) dests = dests.with(to); } return dests; } hasInsufficientMaterial(_color: Color): boolean { return false; } isVariantEnd(): boolean { const goal = SquareSet.fromRank(7); const inGoal = this.board.king.intersect(goal); if (inGoal.isEmpty()) return false; if (this.turn === 'white' || inGoal.intersects(this.board.black)) return true; // White has reached the backrank. Check if black can catch up. const blackKing = this.board.kingOf('black'); if (defined(blackKing)) { const occ = this.board.occupied.without(blackKing); for (const target of kingAttacks(blackKing).intersect(goal).diff(this.board.black)) { if (this.kingAttackers(target, 'white', occ).isEmpty()) return false; } } return true; } variantOutcome(ctx?: Context): Outcome | undefined { if (ctx ? !ctx.variantEnd : !this.isVariantEnd()) return; const goal = SquareSet.fromRank(7); const blackInGoal = this.board.pieces('black', 'king').intersects(goal); const whiteInGoal = this.board.pieces('white', 'king').intersects(goal); if (blackInGoal && !whiteInGoal) return { winner: 'black' }; if (whiteInGoal && !blackInGoal) return { winner: 'white' }; return { winner: undefined }; } } const hordeBoard = (): Board => { const board = Board.empty(); board.occupied = new SquareSet(0xffff_ffff, 0xffff_0066); board.promoted = SquareSet.empty(); board.white = new SquareSet(0xffff_ffff, 0x0000_0066); board.black = new SquareSet(0, 0xffff_0000); board.pawn = new SquareSet(0xffff_ffff, 0x00ff_0066); board.knight = new SquareSet(0, 0x4200_0000); board.bishop = new SquareSet(0, 0x2400_0000); board.rook = new SquareSet(0, 0x8100_0000); board.queen = new SquareSet(0, 0x0800_0000); board.king = new SquareSet(0, 0x1000_0000); return board; }; export class Horde extends Position { private constructor() { super('horde'); } reset() { this.board = hordeBoard(); this.pockets = undefined; this.turn = 'white'; this.castles = Castles.default(); this.castles.discardColor('white'); this.epSquare = undefined; this.remainingChecks = undefined; this.halfmoves = 0; this.fullmoves = 1; } static default(): Horde { const pos = new this(); pos.reset(); return pos; } static fromSetup(setup: Setup): Result<Horde, PositionError> { const pos = new this(); pos.setupUnchecked(setup); return pos.validate().map(_ => pos); } clone(): Horde { return super.clone() as Horde; } protected validate(): Result<undefined, PositionError> { if (this.board.occupied.isEmpty()) return Result.err(new PositionError(IllegalSetup.Empty)); if (this.board.king.size() !== 1) return Result.err(new PositionError(IllegalSetup.Kings)); const otherKing = this.board.kingOf(opposite(this.turn)); if (defined(otherKing) && this.kingAttackers(otherKing, this.turn, this.board.occupied).nonEmpty()) { return Result.err(new PositionError(IllegalSetup.OppositeCheck)); } for (const color of COLORS) { const backranks = this.board.pieces(color, 'king').isEmpty() ? SquareSet.backrank(opposite(color)) : SquareSet.backranks(); if (this.board.pieces(color, 'pawn').intersects(backranks)) { return Result.err(new PositionError(IllegalSetup.PawnsOnBackrank)); } } return Result.ok(undefined); } hasInsufficientMaterial(color: Color): boolean { // The side with the king can always win by capturing the horde. if (this.board.pieces(color, 'king').nonEmpty()) return false; type SquareColor = 'light' | 'dark'; const oppositeSquareColor = (squareColor: SquareColor): SquareColor => (squareColor === 'light' ? 'dark' : 'light'); const coloredSquares = (squareColor: SquareColor): SquareSet => squareColor === 'light' ? SquareSet.lightSquares() : SquareSet.darkSquares(); const hasBishopPair = (side: Color) => { const bishops = this.board.pieces(side, 'bishop'); return bishops.intersects(SquareSet.darkSquares()) && bishops.intersects(SquareSet.lightSquares()); }; // By this point: color is the horde. // Based on // https://github.com/stevepapazis/horde-insufficient-material-tests. const horde = MaterialSide.fromBoard(this.board, color); const hordeBishops = (squareColor: SquareColor) => coloredSquares(squareColor).intersect(this.board.pieces(color, 'bishop')).size(); const hordeBishopColor: SquareColor = hordeBishops('light') >= 1 ? 'light' : 'dark'; const hordeNum = horde.pawn + horde.knight + horde.rook + horde.queen + Math.min(hordeBishops('dark'), 2) + Math.min(hordeBishops('light'), 2); const pieces = MaterialSide.fromBoard(this.board, opposite(color)); const piecesBishops = (squareColor: SquareColor) => coloredSquares(squareColor) .intersect(this.board.pieces(opposite(color), 'bishop')) .size(); const piecesNum = pieces.size(); const piecesOfRoleNot = (piece: number) => piecesNum - piece; if (hordeNum === 0) return true; if (hordeNum >= 4) { // Four or more pieces can always deliver mate. return false; } if ((horde.pawn >= 1 || horde.queen >= 1) && hordeNum >= 2) { // Pawns/queens are never insufficient material when paired with any other // piece (a pawn promotes to a queen and delivers mate). return false; } if (horde.rook >= 1 && hordeNum >= 2) { // A rook is insufficient material only when it is paired with a bishop // against a lone king. The horde can mate in any other case. // A rook on A1 and a bishop on C3 mate a king on B1 when there is a // friendly pawn/opposite-color-bishop/rook/queen on C2. // A rook on B8 and a bishop C3 mate a king on A1 when there is a friendly // knight on A2. if ( !( hordeNum === 2 && horde.rook === 1 && horde.bishop === 1 && piecesOfRoleNot(piecesBishops(hordeBishopColor)) === 1 ) ) { return false; } } if (hordeNum === 1) { if (piecesNum === 1) { // A lone piece cannot mate a lone king. return true; } else if (horde.queen === 1) { // The horde has a lone queen. // A lone queen mates a king on A1 bounded by: // -- a pawn/rook on A2 // -- two same color bishops on A2, B1 // We ignore every other mating case, since it can be reduced to // the two previous cases (e.g. a black pawn on A2 and a black // bishop on B1). return !(pieces.pawn >= 1 || pieces.rook >= 1 || piecesBishops('light') >= 2 || piecesBishops('dark') >= 2); } else if (horde.pawn === 1) { // Promote the pawn to a queen or a knight and check whether white // can mate. const pawnSquare = this.board.pieces(color, 'pawn').last()!; const promoteToQueen = this.clone(); promoteToQueen.board.set(pawnSquare, { color, role: 'queen' }); const promoteToKnight = this.clone(); promoteToKnight.board.set(pawnSquare, { color, role: 'knight' }); return promoteToQueen.hasInsufficientMaterial(color) && promoteToKnight.hasInsufficientMaterial(color); } else if (horde.rook === 1) { // A lone rook mates a king on A8 bounded by a pawn/rook on A7 and a // pawn/knight on B7. We ignore every other case, since it can be // reduced to the two previous cases. // (e.g. three pawns on A7, B7, C7) return !( pieces.pawn >= 2 || (pieces.rook >= 1 && pieces.pawn >= 1) || (pieces.rook >= 1 && pieces.knight >= 1) || (pieces.pawn >= 1 && pieces.knight >= 1) ); } else if (horde.bishop === 1) { // The horde has a lone bishop. return !( // The king can be mated on A1 if there is a pawn/opposite-color-bishop // on A2 and an opposite-color-bishop on B1. // If black has two or more pawns, white gets the benefit of the doubt; // there is an outside chance that white promotes its pawns to // opposite-color-bishops and selfmates theirself. // Every other case that the king is mated by the bishop requires that // black has two pawns or two opposite-color-bishop or a pawn and an // opposite-color-bishop. // For example a king on A3 can be mated if there is // a pawn/opposite-color-bishop on A4, a pawn/opposite-color-bishop on // B3, a pawn/bishop/rook/queen on A2 and any other piece on B2. piecesBishops(oppositeSquareColor(hordeBishopColor)) >= 2 || (piecesBishops(oppositeSquareColor(hordeBishopColor)) >= 1 && pieces.pawn >= 1) || pieces.pawn >= 2 ); } else if (horde.knight === 1) { // The horde has a lone knight. return !( // The king on A1 can be smother mated by a knight on C2 if there is // a pawn/knight/bishop on B2, a knight/rook on B1 and any other piece // on A2. // Moreover, when black has four or more pieces and two of them are // pawns, black can promote their pawns and selfmate theirself. piecesNum >= 4 && (pieces.knight >= 2 || pieces.pawn >= 2 || (pieces.rook >= 1 && pieces.knight >= 1) || (pieces.rook >= 1 && pieces.bishop >= 1) || (pieces.knight >= 1 && pieces.bishop >= 1) || (pieces.rook >= 1 && pieces.pawn >= 1) || (pieces.knight >= 1 && pieces.pawn >= 1) || (pieces.bishop >= 1 && pieces.pawn >= 1) || (hasBishopPair(opposite(color)) && pieces.pawn >= 1)) && (piecesBishops('dark') < 2 || piecesOfRoleNot(piecesBishops('dark')) >= 3) && (piecesBishops('light') < 2 || piecesOfRoleNot(piecesBishops('light')) >= 3) ); } // By this point, we only need to deal with white's minor pieces. } else if (hordeNum === 2) { if (piecesNum === 1) { // Two minor pieces cannot mate a lone king. return true; } else if (horde.knight === 2) { // A king on A1 is mated by two knights, if it is obstructed by a // pawn/bishop/knight on B2. On the other hand, if black only has // major pieces it is a draw. return pieces.pawn + pieces.bishop + pieces.knight < 1; } else if (hasBishopPair(color)) { return !( // A king on A1 obstructed by a pawn/bishop on A2 is mated // by the bishop pair. pieces.pawn >= 1 || pieces.bishop >= 1 // A pawn/bishop/knight on B4, a pawn/bishop/rook/queen on // A4 and the king on A3 enable Boden's mate by the bishop // pair. In every other case white cannot win. || (pieces.knight >= 1 && pieces.rook + pieces.queen >= 1) ); } else if (horde.bishop >= 1 && horde.knight >= 1) { // The horde has a bishop and a knight. return !( // A king on A1 obstructed by a pawn/opposite-color-bishop on // A2 is mated by a knight on D2 and a bishop on C3. pieces.pawn >= 1 || piecesBishops(oppositeSquareColor(hordeBishopColor)) >= 1 // A king on A1 bounded by two friendly pieces on A2 and B1 is // mated when the knight moves from D4 to C2 so that both the // knight and the bishop deliver check. || piecesOfRoleNot(piecesBishops(hordeBishopColor)) >= 3 ); } else { // The horde has two or more bishops on the same color. // White can only win if black has enough material to obstruct // the squares of the opposite color around the king. return !( // A king on A1 obstructed by a pawn/opposite-bishop/knight // on A2 and a opposite-bishop/knight on B1 is mated by two // bishops on B2 and C3. This position is theoretically // achievable even when black has two pawns or when they // have a pawn and an opposite color bishop. (pieces.pawn >= 1 && piecesBishops(oppositeSquareColor(hordeBishopColor)) >= 1) || (pieces.pawn >= 1 && pieces.knight >= 1) || (piecesBishops(oppositeSquareColor(hordeBishopColor)) >= 1 && pieces.knight >= 1) || piecesBishops(oppositeSquareColor(hordeBishopColor)) >= 2 || pieces.knight >= 2 || pieces.pawn >= 2 // In every other case, white can only draw. ); } } else if (hordeNum === 3) { // A king in the corner is mated by two knights and a bishop or three // knights or the bishop pair and a knight/bishop. if ((horde.knight === 2 && horde.bishop === 1) || horde.knight === 3 || hasBishopPair(color)) { return false; } else { // White has two same color bishops and a knight. // A king on A1 is mated by a bishop on B2, a bishop on C1 and a // knight on C3, as long as there is another black piece to waste // a tempo. return piecesNum === 1; } } return true; } isVariantEnd(): boolean { return this.board.white.isEmpty() || this.board.black.isEmpty(); } variantOutcome(_ctx?: Context): Outcome | undefined { if (this.board.white.isEmpty()) return { winner: 'black' }; if (this.board.black.isEmpty()) return { winner: 'white' }; return; } } export const defaultPosition = (rules: Rules): Position => { switch (rules) { case 'chess': return Chess.default(); case 'antichess': return Antichess.default(); case 'atomic': return Atomic.default(); case 'horde': return Horde.default(); case 'racingkings': return RacingKings.default(); case 'kingofthehill': return KingOfTheHill.default(); case '3check': return ThreeCheck.default(); case 'crazyhouse': return Crazyhouse.default(); } }; export const setupPosition = (rules: Rules, setup: Setup): Result<Position, PositionError> => { switch (rules) { case 'chess': return Chess.fromSetup(setup); case 'antichess': return Antichess.fromSetup(setup); case 'atomic': return Atomic.fromSetup(setup); case 'horde': return Horde.fromSetup(setup); case 'racingkings': return RacingKings.fromSetup(setup); case 'kingofthehill': return KingOfTheHill.fromSetup(setup); case '3check': return ThreeCheck.fromSetup(setup); case 'crazyhouse': return Crazyhouse.fromSetup(setup); } }; export const isStandardMaterial = (pos: Position): boolean => { switch (pos.rules) { case 'chess': case 'antichess': case 'atomic': case 'kingofthehill': case '3check': return COLORS.every(color => isStandardMaterialSide(pos.board, color)); case 'crazyhouse': { const promoted = pos.board.promoted; return ( promoted.size() + pos.board.pawn.size() + (pos.pockets?.count('pawn') || 0) <= 16 && pos.board.knight.diff(promoted).size() + (pos.pockets?.count('knight') || 0) <= 4 && pos.board.bishop.diff(promoted).size() + (pos.pockets?.count('bishop') || 0) <= 4 && pos.board.rook.diff(promoted).size() + (pos.pockets?.count('rook') || 0) <= 4 && pos.board.queen.diff(promoted).size() + (pos.pockets?.count('queen') || 0) <= 2 ); } case 'horde': return COLORS.every(color => pos.board.pieces(color, 'king').nonEmpty() ? isStandardMaterialSide(pos.board, color) : pos.board[color].size() <= 36 ); case 'racingkings': return COLORS.every( color => pos.board.pieces(color, 'knight').size() <= 2 && pos.board.pieces(color, 'bishop').size() <= 2 && pos.board.pieces(color, 'rook').size() <= 2 && pos.board.pieces(color, 'queen').size() <= 1, ); } };