Initial commit

This commit is contained in:
Fokinator
2026-03-09 20:22:06 +03:00
commit b345387164
60 changed files with 3624 additions and 0 deletions
@@ -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();
}
}