363 lines
18 KiB
Java
363 lines
18 KiB
Java
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<Position, Piece> pieces = new HashMap<>();
|
|
|
|
PieceColor currentlyMoving = PieceColor.WHITE;
|
|
transient Function<PieceColor, Piece> pawnPromoter;
|
|
|
|
/// Returns a default starting position, which looks like this:
|
|
/// <p><img src="https://images.chesscomfiles.com/uploads/v1/images_users/tiny_mce/Sammy_Thechessboy/phpa7Oa0h.png" width="300" height="300" />
|
|
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<Piece> getPieces(Position.Column column) {
|
|
List<Piece> 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<Piece> getPieces(Position.Row row) {
|
|
List<Piece> 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<Position, Piece> getPieces() {
|
|
return Map.copyOf(pieces);
|
|
}
|
|
|
|
/// Clears the pieces from this chess board and adds all the given ones.
|
|
public synchronized void setPieces(Map<Position, Piece> 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<? extends Piece> 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<? extends Piece> 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<MoveEntry> getMoves(Position pos, MoveStack moveStack) {
|
|
if (isEmpty(Objects.requireNonNull(pos, "pos"))) {
|
|
return Set.of();
|
|
}
|
|
Collection<MoveEntry> 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`.
|
|
/// <p>
|
|
/// 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<MoveEntry> getMovesNoKingCheck(Position pos, MoveStack moveStack, boolean checkCastlingAndEnPassant) {
|
|
if (isEmpty(Objects.requireNonNull(pos, "pos"))) {
|
|
return new HashSet<>();
|
|
}
|
|
Collection<MoveEntry> result = new HashSet<>();
|
|
Piece piece = getPiece(pos);
|
|
Collection<? extends Collection<ConditionalMovement>> potentialMoves = piece.getPotentialMoves(pos);
|
|
for (Collection<ConditionalMovement> 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();
|
|
}
|
|
}
|