Initial commit
This commit is contained in:
@@ -0,0 +1,362 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user