package ru.fokinatorr.chess.impl; import lombok.AccessLevel; import lombok.Data; import lombok.experimental.FieldDefaults; import lombok.extern.slf4j.Slf4j; import ru.fokinatorr.chess.impl.movestack.*; import ru.fokinatorr.chess.impl.piece.*; import java.util.*; import java.util.function.Function; /// A chess board containing pieces. @Slf4j @Data @FieldDefaults(level = AccessLevel.PRIVATE) public class ChessBoard implements java.io.Serializable, Cloneable { @java.io.Serial private static final long serialVersionUID = 2600172883193074936L; final Map pieces = new HashMap<>(); PieceColor currentlyMoving = PieceColor.WHITE; transient Function pawnPromoter; /// Returns a default starting position, which looks like this: ///

public static ChessBoard startPosition() { ChessBoard chessBoard = new ChessBoard(); chessBoard.setPiece(Position.valueOf("a1"), new Rook(PieceColor.WHITE)); chessBoard.setPiece(Position.valueOf("b1"), new Knight(PieceColor.WHITE)); chessBoard.setPiece(Position.valueOf("c1"), new Bishop(PieceColor.WHITE)); chessBoard.setPiece(Position.valueOf("d1"), new Queen(PieceColor.WHITE)); chessBoard.setPiece(Position.valueOf("e1"), new King(PieceColor.WHITE)); chessBoard.setPiece(Position.valueOf("f1"), new Bishop(PieceColor.WHITE)); chessBoard.setPiece(Position.valueOf("g1"), new Knight(PieceColor.WHITE)); chessBoard.setPiece(Position.valueOf("h1"), new Rook(PieceColor.WHITE)); chessBoard.setPiece(Position.valueOf("a2"), new Pawn(PieceColor.WHITE)); chessBoard.setPiece(Position.valueOf("b2"), new Pawn(PieceColor.WHITE)); chessBoard.setPiece(Position.valueOf("c2"), new Pawn(PieceColor.WHITE)); chessBoard.setPiece(Position.valueOf("d2"), new Pawn(PieceColor.WHITE)); chessBoard.setPiece(Position.valueOf("e2"), new Pawn(PieceColor.WHITE)); chessBoard.setPiece(Position.valueOf("f2"), new Pawn(PieceColor.WHITE)); chessBoard.setPiece(Position.valueOf("g2"), new Pawn(PieceColor.WHITE)); chessBoard.setPiece(Position.valueOf("h2"), new Pawn(PieceColor.WHITE)); chessBoard.setPiece(Position.valueOf("a8"), new Rook(PieceColor.BLACK)); chessBoard.setPiece(Position.valueOf("b8"), new Knight(PieceColor.BLACK)); chessBoard.setPiece(Position.valueOf("c8"), new Bishop(PieceColor.BLACK)); chessBoard.setPiece(Position.valueOf("d8"), new Queen(PieceColor.BLACK)); chessBoard.setPiece(Position.valueOf("e8"), new King(PieceColor.BLACK)); chessBoard.setPiece(Position.valueOf("f8"), new Bishop(PieceColor.BLACK)); chessBoard.setPiece(Position.valueOf("g8"), new Knight(PieceColor.BLACK)); chessBoard.setPiece(Position.valueOf("h8"), new Rook(PieceColor.BLACK)); chessBoard.setPiece(Position.valueOf("a7"), new Pawn(PieceColor.BLACK)); chessBoard.setPiece(Position.valueOf("b7"), new Pawn(PieceColor.BLACK)); chessBoard.setPiece(Position.valueOf("c7"), new Pawn(PieceColor.BLACK)); chessBoard.setPiece(Position.valueOf("d7"), new Pawn(PieceColor.BLACK)); chessBoard.setPiece(Position.valueOf("e7"), new Pawn(PieceColor.BLACK)); chessBoard.setPiece(Position.valueOf("f7"), new Pawn(PieceColor.BLACK)); chessBoard.setPiece(Position.valueOf("g7"), new Pawn(PieceColor.BLACK)); chessBoard.setPiece(Position.valueOf("h7"), new Pawn(PieceColor.BLACK)); return chessBoard; } /// Get a piece on the given position. public Piece getPiece(Position pos) { return pieces.get(pos); } /// Get all pieces in the given column. public synchronized List getPieces(Position.Column column) { List result = new ArrayList<>(); for (Position.Row row : Position.Row.values()) { Piece piece = getPiece(Position.valueOf(column, row)); if (piece != null) result.add(piece); } return Collections.unmodifiableList(result); } /// Get all pieces in the given row. public synchronized List getPieces(Position.Row row) { List result = new ArrayList<>(); for (Position.Column column : Position.Column.values()) { Piece piece = getPiece(Position.valueOf(column, row)); if (piece != null) result.add(piece); } return Collections.unmodifiableList(result); } /// Returns the pieces of this chess board as position mapped to piece. public synchronized Map getPieces() { return Map.copyOf(pieces); } /// Clears the pieces from this chess board and adds all the given ones. public synchronized void setPieces(Map newPieces) { pieces.clear(); pieces.putAll(newPieces); } /// Place the given piece on the given position and return the old one. /// Removes the piece at the given position if `newPiece` is `null`. /// @throws NullPointerException if `pos` is `null` public synchronized Piece setPiece(Position pos, Piece newPiece) { Objects.requireNonNull(pos, "Position must not be null"); log.info("Set piece {} on position {}", newPiece, pos); return newPiece == null ? pieces.remove(pos) : pieces.put(pos, newPiece); } /// Determines if a piece on the given position, if it exists, is of given color. public boolean isPieceOfColor(Position pos, PieceColor color) { Piece piece = getPiece(pos); return piece != null && piece.getColor() == color; } /// Determines if a piece on the given position, if it exists, is of given type. public boolean isPieceOfType(Position pos, Class type) { Piece piece = getPiece(pos); return piece != null && type.isAssignableFrom(piece.getClass()); } /// Determines if a piece on the given position, if it exists, is of given color. public boolean isPieceOfColorAndType(Position pos, PieceColor color, Class type) { Piece piece = getPiece(pos); return piece != null && piece.getColor() == color && type.isAssignableFrom(piece.getClass()); } /// Determines if a piece on the given position exists. public boolean isPresent(Position pos) { return getPiece(pos) != null; } /// Determines if a piece on the given position does not exist. public boolean isEmpty(Position pos) { return getPiece(pos) == null; } /// Reverse the currently moving player. public void reverseCurrentlyMoving() { setCurrentlyMoving(currentlyMoving.reverse()); } /// Find the actual positions that a piece on position `pos`, if it's present, can reach, and return them /// as an immutable [Collection]. If no piece is present at the given position, returns an empty `Collection`. /// @throws NullPointerException if any argument is `null` public Collection getMoves(Position pos, MoveStack moveStack) { if (isEmpty(Objects.requireNonNull(pos, "pos"))) { return Set.of(); } Collection toFilter = getMovesNoKingCheck(pos, moveStack, true); PieceColor pieceColor = getPiece(pos).getColor(); var iterator = toFilter.iterator(); while (iterator.hasNext()) { MoveEntry moveEntry = iterator.next(); Move move = moveEntry.move(); if (move instanceof PawnPromotionMove pawnPromotionMove) { pawnPromotionMove.setPawnPromoter(Queen::new); } move.perform(this, moveStack); if (isCheck(pieceColor)) { iterator.remove(); } move.undo(this); if (move instanceof PawnPromotionMove pawnPromotionMove) { pawnPromotionMove.setPawnPromoter(pawnPromoter); } } return Set.copyOf(toFilter); } /// Find the actual positions that a piece on position `pos`, if it's present, can reach, and return them /// as a mutable [Collection]. If no piece is present at the given position, returns an empty `Collection`. ///

/// Note that this method does not check if the king is going to be attacked after one of these doneMoves. /// @throws NullPointerException if `pos` argument is `null`, or if `moveStack` is `null` and `checkCastlingAndEnPassant` is `true` private Collection getMovesNoKingCheck(Position pos, MoveStack moveStack, boolean checkCastlingAndEnPassant) { if (isEmpty(Objects.requireNonNull(pos, "pos"))) { return new HashSet<>(); } Collection result = new HashSet<>(); Piece piece = getPiece(pos); Collection> potentialMoves = piece.getPotentialMoves(pos); for (Collection movementChain : potentialMoves) { for (ConditionalMovement conditionalMovement : movementChain) { if (conditionalMovement.moveCondition() == ConditionalMovement.MoveCondition.CASTLING) { // at this point it's known that the piece is king if (checkCastlingAndEnPassant && piece instanceof King king && king.isNeverMoved()) { Piece maybeRook; // the rook that's going to take part in castling Position kingJumpPos; // the position that king jumps over. should not be attacked Position.Column rookColumn; boolean right = conditionalMovement.pos().getColumn().getIndex() - pos.getColumn().getIndex() > 0; if (right) { kingJumpPos = pos.right(1); maybeRook = getPiece(Position.valueOf(rookColumn = Position.Column.H, pos.getRow())); } else { kingJumpPos = pos.left(1); maybeRook = getPiece(Position.valueOf(rookColumn = Position.Column.A, pos.getRow())); } // the color of the rook doesn't matter because: // 1. if it's the opposite color, the king is in check => castling can't be done; // 2. if it's the opposite color and the king isn't in check, there's a piece between them => castling can't be done. if (maybeRook instanceof Rook rook && rook.isNeverMoved() // there's a piece, it's a rook and it never moved before && !isPositionAttacked(pos, king.getColor().reverse()) // it's not check currently && !isPositionAttacked(kingJumpPos, king.getColor().reverse())) { // the position that king "jumps" over is not attacked too assert kingJumpPos != null; // check all the pieces between the king and the rook boolean noPiecesBetween = true; for (Position.Column column = kingJumpPos.getColumn(); Math.min(column.getIndex(), rookColumn.getIndex()) < Math.max(column.getIndex(), rookColumn.getIndex()); column = Position.Column.valueOf(column.getIndex() + (right ? 1 : -1))) { noPiecesBetween &= isEmpty(Position.valueOf(column, conditionalMovement.pos().getRow())); } if (noPiecesBetween) { result.add(new MoveEntry(conditionalMovement.pos(), new CastlingMove(pos, Position.valueOf(rookColumn, pos.getRow()), right), false)); } } } break; } else if (piece instanceof Pawn pawn && pos.getRow() == pawn.getEnPassantRow() && conditionalMovement.moveCondition().canCapture() && isEmpty(conditionalMovement.pos()) && checkCastlingAndEnPassant) { Position capturePos = conditionalMovement.pos().down(pawn.getForwardMovementFactor()); if (isPieceOfColorAndType(capturePos, pawn.getColor().reverse(), Pawn.class) && ChessUtil.wasPawnDoubleMove(moveStack.peekDone(), capturePos, pawn.getColor().reverse())) { result.add(new MoveEntry( conditionalMovement.pos(), new EnPassantMove(pos, conditionalMovement.pos().getColumn().getIndex() > pos.getColumn().getIndex()), // can't be attack since conditionalMovement.pos() is empty false )); break; } } else if (isPieceOfColor(conditionalMovement.pos(), piece.getColor()) || !conditionalMovement.moveCondition().canMove() && isEmpty(conditionalMovement.pos()) || !conditionalMovement.moveCondition().canCapture() && isPresent(conditionalMovement.pos())) { break; } else { if (piece instanceof Pawn pawn) { if (conditionalMovement.pos().getRow() == pawn.getPromotionRow()) { result.add(new MoveEntry( conditionalMovement.pos(), new PawnPromotionMove(pos, conditionalMovement.pos(), pawnPromoter), conditionalMovement.moveCondition().canCapture() )); break; } } result.add(new MoveEntry( conditionalMovement.pos(), new SimpleMove(pos, conditionalMovement.pos()), conditionalMovement.moveCondition().canCapture() )); if (isPieceOfColor(conditionalMovement.pos(), piece.getColor().reverse())) { break; } } } } return result; } /// Determines whether the given position is attacked by any piece of the given color. /// @throws NullPointerException if any argument is `null` public boolean isPositionAttacked(Position givenPos, PieceColor color) { Objects.requireNonNull(givenPos, "pos"); Objects.requireNonNull(color, "color"); for (Position pos : pieces.keySet()) { if (isPieceOfColor(pos, color)) { for (MoveEntry move : getMovesNoKingCheck(pos, null, false)) { if (move.isAttack() && move.destPos().equals(givenPos)) { return true; } } } } return false; } /// Determines whether a king of the given color is attacked. /// @throws InternalError if a king of the given color is not present on this board public boolean isCheck(PieceColor kingColor) { // find the king Position kingPos = null; for (var posPieceEntry : pieces.entrySet()) { if (posPieceEntry.getValue() instanceof King king && king.getColor() == kingColor) { kingPos = posPieceEntry.getKey(); break; } } if (kingPos == null) { throw new InternalError("No " + kingColor.name().toLowerCase(Locale.ENGLISH) + " King found on " + this); } return isPositionAttacked(kingPos, kingColor.reverse()); } /// Determines whether no piece of the given color has a legal move. Common check for checkmate and stalemate. private boolean noPieceOfColorCanMove(PieceColor color, MoveStack moveStack) { for (Position pos : pieces.keySet().toArray(new Position[0])) { if (isPieceOfColor(pos, color) && !getMoves(pos, moveStack).isEmpty()) { return false; } } return true; } /// Determines whether a king of the given color is in check and no piece of the given color has a legal move. public boolean isCheckmate(PieceColor color, MoveStack moveStack) { return isCheck(color) && noPieceOfColorCanMove(color, moveStack); } /// Determines whether no piece of the given color has a legal move, but a king of the given color is not in check. public boolean isStalemate(PieceColor color, MoveStack moveStack) { return !isCheck(color) && noPieceOfColorCanMove(color, moveStack); } /// Creates and returns a deep copy of this chess board. @SuppressWarnings("MethodDoesntCallSuperMethod") @Override public ChessBoard clone() { log.info("Cloning board"); ChessBoard clone = new ChessBoard(); clone.pawnPromoter = this.pawnPromoter; clone.currentlyMoving = this.currentlyMoving; for (Position pos : pieces.keySet()) { clone.setPiece(pos, getPiece(pos).clone()); } return clone; } @Override public String toString() { StringBuilder sb = new StringBuilder(" ABCDEFGH"); for (int i = 8; i >= 1; i--) { sb.append('\n').append(i); for (Piece piece : getPieces(Position.Row.valueOfNumber(i))) { char pieceNotation = switch (piece) { case Bishop _ -> 'B'; case King _ -> 'K'; case Knight _ -> 'N'; case Pawn _ -> 'P'; case Queen _ -> 'Q'; case Rook _ -> 'R'; }; sb.append(piece.getColor() == PieceColor.BLACK ? Character.toLowerCase(pieceNotation) : pieceNotation); } } return sb.toString(); } }