429 lines
14 KiB
Java
429 lines
14 KiB
Java
package dev.kske.chess.board;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Objects;
|
|
import java.util.regex.Matcher;
|
|
import java.util.regex.Pattern;
|
|
|
|
import dev.kske.chess.board.Piece.Color;
|
|
|
|
/**
|
|
* Project: <strong>Chess</strong><br>
|
|
* File: <strong>Board.java</strong><br>
|
|
* Created: <strong>01.07.2019</strong><br>
|
|
*
|
|
* @since Chess v0.1-alpha
|
|
* @author Kai S. K. Engelbart
|
|
*/
|
|
public class Board {
|
|
|
|
private Piece[][] boardArr = new Piece[8][8];
|
|
private Map<Color, Position> kingPos = new HashMap<>();
|
|
private Log log = new Log();
|
|
|
|
/**
|
|
* Initializes the board with the default chess starting position.
|
|
*/
|
|
public Board() { initDefaultPositions(); }
|
|
|
|
/**
|
|
* Creates a copy of another {@link Board} instance.<br>
|
|
* The created object is a deep copy, but does not contain any move history
|
|
* apart from the current {@link MoveNode}.
|
|
*
|
|
* @param other The {@link Board} instance to copy
|
|
*/
|
|
public Board(Board other) {
|
|
boardArr = new Piece[8][8];
|
|
for (int i = 0; i < 8; i++)
|
|
for (int j = 0; j < 8; j++) {
|
|
if (other.boardArr[i][j] == null) continue;
|
|
boardArr[i][j] = (Piece) other.boardArr[i][j].clone();
|
|
boardArr[i][j].board = this;
|
|
}
|
|
|
|
kingPos.putAll(other.kingPos);
|
|
log = new Log(other.log, false);
|
|
}
|
|
|
|
/**
|
|
* Moves a piece across the board if the move is legal.
|
|
*
|
|
* @param move The move to execute
|
|
* @return {@code true}, if the attempted move was legal and thus executed
|
|
*/
|
|
public boolean attemptMove(Move move) {
|
|
Piece piece = getPos(move);
|
|
if (piece == null || !piece.isValidMove(move)) return false;
|
|
else {
|
|
// Move piece
|
|
move(move);
|
|
|
|
// Revert move if it caused a check for its team
|
|
if (checkCheck(piece.getColor())) {
|
|
revert();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Moves a piece across the board without checking if the move is legal.
|
|
*
|
|
* @param move The move to execute
|
|
*/
|
|
public void move(Move move) {
|
|
Piece piece = getPos(move);
|
|
Piece capturePiece = getDest(move);
|
|
|
|
// Execute the move
|
|
move.execute(this);
|
|
|
|
// Update the king's position if the moved piece is the king
|
|
if (piece instanceof King) kingPos.put(piece.getColor(), move.getDest());
|
|
|
|
// Update log
|
|
log.add(move, piece, capturePiece);
|
|
}
|
|
|
|
/**
|
|
* Moves a piece across the board without checking if the move is legal.
|
|
*
|
|
* @param sanMove The move to execute in SAN (Standard Algebraic Notation)
|
|
*/
|
|
public void move(String sanMove) {
|
|
Map<String, Pattern> patterns = new HashMap<>();
|
|
patterns.put("pieceMove",
|
|
Pattern.compile(
|
|
"^(?<pieceType>[NBRQK])(?:(?<fromFile>[a-h])|(?<fromRank>[1-8])|(?<fromSquare>[a-h][1-8]))?x?(?<toSquare>[a-h][1-8])(?:\\+{0,2}|\\#)$"));
|
|
patterns.put("pawnCapture",
|
|
Pattern.compile("^(?<fromFile>[a-h])(?<fromRank>[1-8])?x(?<toSquare>[a-h][1-8])(?<promotedTo>[NBRQ])?(?:\\+{0,2}|\\#)?$"));
|
|
patterns.put("pawnPush", Pattern.compile("^(?<toSquare>[a-h][1-8])(?<promotedTo>[NBRQ])?(?:\\+{0,2}|\\#)$"));
|
|
patterns.put("castling", Pattern.compile("^(?<queenside>O-O-O)|(?<kingside>O-O)(?:\\+{0,2}|\\#)?$"));
|
|
|
|
patterns.forEach((patternName, pattern) -> {
|
|
Matcher m = pattern.matcher(sanMove);
|
|
if (m.find()) {
|
|
Position pos = null, dest = null;
|
|
Move move = null;
|
|
switch (patternName) {
|
|
case "pieceMove":
|
|
dest = Position.fromLAN(m.group("toSquare"));
|
|
if (m.group("fromSquare") != null) pos = Position.fromLAN(m.group("fromSquare"));
|
|
else {
|
|
Class<? extends Piece> pieceClass = Piece.fromFirstChar(m.group("pieceType").charAt(0));
|
|
char file;
|
|
int rank;
|
|
if (m.group("fromFile") != null) {
|
|
file = m.group("fromFile").charAt(0);
|
|
rank = get(pieceClass, file);
|
|
pos = Position.fromLAN(String.format("%c%d", file, rank));
|
|
} else if (m.group("fromRank") != null) {
|
|
rank = Integer.parseInt(m.group("fromRank").substring(0, 1));
|
|
file = get(pieceClass, rank);
|
|
pos = Position.fromLAN(String.format("%c%d", file, rank));
|
|
} else pos = get(pieceClass, dest);
|
|
}
|
|
move = new Move(pos, dest);
|
|
break;
|
|
case "pawnCapture":
|
|
char file = m.group("fromFile").charAt(0);
|
|
int rank = m.group("fromRank") == null ? get(Pawn.class, file) : Integer.parseInt(m.group("fromRank"));
|
|
|
|
dest = Position.fromLAN(m.group("toSquare"));
|
|
pos = Position.fromLAN(String.format("%c%d", file, rank));
|
|
|
|
if (m.group("promotedTo") != null) {
|
|
try {
|
|
move = new PawnPromotion(pos, dest, Piece.fromFirstChar(m.group("promotedTo").charAt(0)));
|
|
} catch (NoSuchMethodException | SecurityException | IllegalArgumentException e) {
|
|
e.printStackTrace();
|
|
}
|
|
} else move = new Move(pos, dest);
|
|
break;
|
|
case "pawnPush":
|
|
dest = Position.fromLAN(m.group("toSquare"));
|
|
int step = log.getActiveColor() == Color.WHITE ? 1 : -1;
|
|
|
|
// One step forward
|
|
if (boardArr[dest.x][dest.y + step] != null) pos = new Position(dest.x, dest.y + step);
|
|
|
|
// Double step forward
|
|
else pos = new Position(dest.x, dest.y + 2 * step);
|
|
|
|
if (m.group("promotedTo") != null) {
|
|
try {
|
|
move = new PawnPromotion(pos, dest, Piece.fromFirstChar(m.group("promotedTo").charAt(0)));
|
|
} catch (NoSuchMethodException | SecurityException | IllegalArgumentException e) {
|
|
e.printStackTrace();
|
|
}
|
|
} else move = new Move(pos, dest);
|
|
break;
|
|
case "castling":
|
|
pos = new Position(4, log.getActiveColor() == Color.WHITE ? 7 : 0);
|
|
dest = new Position(m.group("kingside") != null ? 6 : 2, pos.y);
|
|
move = new Castling(pos, dest);
|
|
break;
|
|
}
|
|
move(move);
|
|
return;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Reverts the last move and removes it from the log.
|
|
*/
|
|
public void revert() {
|
|
MoveNode moveNode = log.getLast();
|
|
Move move = moveNode.move;
|
|
|
|
// Revert the move
|
|
move.revert(this, moveNode.capturedPiece);
|
|
|
|
// Update the king's position if the moved piece is the king
|
|
if (getPos(move) instanceof King) kingPos.put(getPos(move).getColor(), move.getPos());
|
|
|
|
// Update log
|
|
log.removeLast();
|
|
}
|
|
|
|
/**
|
|
* Generated every legal move for one color
|
|
*
|
|
* @param color The color to generate the moves for
|
|
* @return A list of all legal moves
|
|
*/
|
|
public List<Move> getMoves(Color color) {
|
|
List<Move> moves = new ArrayList<>();
|
|
for (int i = 0; i < 8; i++)
|
|
for (int j = 0; j < 8; j++)
|
|
if (boardArr[i][j] != null && boardArr[i][j].getColor() == color) moves.addAll(boardArr[i][j].getMoves(new Position(i, j)));
|
|
return moves;
|
|
}
|
|
|
|
public List<Move> getMoves(Position pos) { return get(pos).getMoves(pos); }
|
|
|
|
/**
|
|
* Checks, if the king is in check.
|
|
*
|
|
* @param color The color of the king to check
|
|
* @return {@code true}, if the king is in check
|
|
*/
|
|
public boolean checkCheck(Color color) { return isAttacked(kingPos.get(color), color.opposite()); }
|
|
|
|
/**
|
|
* Checks, if a field can be attacked by pieces of a certain color.
|
|
*
|
|
* @param dest the field to check
|
|
* @param color the color of a potential attacker piece
|
|
* @return {@code true} if a move with the destination {@code dest}
|
|
*/
|
|
public boolean isAttacked(Position dest, Color color) {
|
|
for (int i = 0; i < 8; i++)
|
|
for (int j = 0; j < 8; j++) {
|
|
Position pos = new Position(i, j);
|
|
if (get(pos) != null && get(pos).getColor() == color && get(pos).isValidMove(new Move(pos, dest))) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Checks, if the king is in checkmate.
|
|
* This requires the king to already be in check!
|
|
*
|
|
* @param color The color of the king to check
|
|
* @return {@code true}, if the king is in checkmate
|
|
*/
|
|
public boolean checkCheckmate(Color color) {
|
|
// Return false immediately if the king can move
|
|
if (!getMoves(kingPos.get(color)).isEmpty()) return false;
|
|
else {
|
|
for (Move move : getMoves(color)) {
|
|
move(move);
|
|
boolean check = checkCheck(color);
|
|
revert();
|
|
if (!check) return false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public BoardState getGameEventType(Color color) {
|
|
return checkCheck(color) ? checkCheckmate(color) ? BoardState.CHECKMATE : BoardState.CHECK
|
|
: getMoves(color).isEmpty() || log.getLast().halfmoveClock >= 50 ? BoardState.STALEMATE : BoardState.NORMAL;
|
|
}
|
|
|
|
/**
|
|
* Initialized the board array with the default chess pieces and positions.
|
|
*/
|
|
public void initDefaultPositions() {
|
|
// Initialize pawns
|
|
for (int i = 0; i < 8; i++) {
|
|
boardArr[i][1] = new Pawn(Color.BLACK, this);
|
|
boardArr[i][6] = new Pawn(Color.WHITE, this);
|
|
}
|
|
|
|
// Initialize kings
|
|
boardArr[4][0] = new King(Color.BLACK, this);
|
|
boardArr[4][7] = new King(Color.WHITE, this);
|
|
|
|
// Initialize king position objects
|
|
kingPos.put(Color.BLACK, new Position(4, 0));
|
|
kingPos.put(Color.WHITE, new Position(4, 7));
|
|
|
|
// Initialize queens
|
|
boardArr[3][0] = new Queen(Color.BLACK, this);
|
|
boardArr[3][7] = new Queen(Color.WHITE, this);
|
|
|
|
// Initialize rooks
|
|
boardArr[0][0] = new Rook(Color.BLACK, this);
|
|
boardArr[0][7] = new Rook(Color.WHITE, this);
|
|
boardArr[7][0] = new Rook(Color.BLACK, this);
|
|
boardArr[7][7] = new Rook(Color.WHITE, this);
|
|
|
|
// Initialize knights
|
|
boardArr[1][0] = new Knight(Color.BLACK, this);
|
|
boardArr[1][7] = new Knight(Color.WHITE, this);
|
|
boardArr[6][0] = new Knight(Color.BLACK, this);
|
|
boardArr[6][7] = new Knight(Color.WHITE, this);
|
|
|
|
// Initialize bishops
|
|
boardArr[2][0] = new Bishop(Color.BLACK, this);
|
|
boardArr[2][7] = new Bishop(Color.WHITE, this);
|
|
boardArr[5][0] = new Bishop(Color.BLACK, this);
|
|
boardArr[5][7] = new Bishop(Color.WHITE, this);
|
|
|
|
// Clear all other tiles
|
|
for (int i = 0; i < 8; i++)
|
|
for (int j = 2; j < 6; j++)
|
|
boardArr[i][j] = null;
|
|
|
|
log.reset();
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
final int prime = 31;
|
|
int result = 1;
|
|
result = prime * result + Arrays.deepHashCode(boardArr);
|
|
result = prime * result + Objects.hash(kingPos, log);
|
|
return result;
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object obj) {
|
|
if (this == obj) return true;
|
|
if (obj == null) return false;
|
|
if (getClass() != obj.getClass()) return false;
|
|
Board other = (Board) obj;
|
|
return Arrays.deepEquals(boardArr, other.boardArr) && Objects.equals(kingPos, other.kingPos) && Objects.equals(log, other.log);
|
|
}
|
|
|
|
/**
|
|
* @param pos The position from which to return a piece
|
|
* @return The piece at the position
|
|
*/
|
|
public Piece get(Position pos) { return boardArr[pos.x][pos.y]; }
|
|
|
|
/**
|
|
* Searches for a {@link Piece} inside a file (A - H).
|
|
*
|
|
* @param pieceClass The class of the piece to search for
|
|
* @param file The file in which to search for the piece
|
|
* @return The rank (1 - 8) of the first piece with the specified type and
|
|
* current color in the file, or {@code -1} if there isn't any
|
|
*/
|
|
public int get(Class<? extends Piece> pieceClass, char file) {
|
|
int x = file - 97;
|
|
for (int i = 0; i < 8; i++)
|
|
if (boardArr[x][i] != null && boardArr[x][i].getClass() == pieceClass && boardArr[x][i].getColor() == log.getActiveColor()) return 8 - i;
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Searches for a {@link Piece} inside a rank (1 - 8).
|
|
*
|
|
* @param pieceClass The class of the piece to search for
|
|
* @param rank The rank in which to search for the piece
|
|
* @return The file (A - H) of the first piece with the specified type and
|
|
* current color in the file, or {@code -} if there isn't any
|
|
*/
|
|
public char get(Class<? extends Piece> pieceClass, int rank) {
|
|
int y = rank - 1;
|
|
for (int i = 0; i < 8; i++)
|
|
if (boardArr[i][y] != null && boardArr[i][y].getClass() == pieceClass && boardArr[i][y].getColor() == log.getActiveColor())
|
|
return (char) (i + 97);
|
|
return '-';
|
|
}
|
|
|
|
/**
|
|
* Searches for a {@link Piece} that can move to a {@link Position}.
|
|
*
|
|
* @param pieceClass The class of the piece to search for
|
|
* @param dest The destination that the piece is required to reach
|
|
* @return The position of a piece that can move to the specified destination
|
|
*/
|
|
public Position get(Class<? extends Piece> pieceClass, Position dest) {
|
|
for (int i = 0; i < 8; i++)
|
|
for (int j = 0; j < 8; j++)
|
|
if (boardArr[i][j] != null && boardArr[i][j].getClass() == pieceClass && boardArr[i][j].getColor() == log.getActiveColor()) {
|
|
Position pos = new Position(i, j);
|
|
if (boardArr[i][j].isValidMove(new Move(pos, dest))) return pos;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Places a piece at a position.
|
|
*
|
|
* @param pos The position to place the piece at
|
|
* @param piece The piece to place
|
|
*/
|
|
public void set(Position pos, Piece piece) { boardArr[pos.x][pos.y] = piece; }
|
|
|
|
/**
|
|
* @param move The move from which position to return a piece
|
|
* @return The piece at the position of the move
|
|
*/
|
|
public Piece getPos(Move move) { return get(move.getPos()); }
|
|
|
|
/**
|
|
* @param move The move from which destination to return a piece
|
|
* @return The piece at the destination of the move
|
|
*/
|
|
public Piece getDest(Move move) { return get(move.getDest()); }
|
|
|
|
/**
|
|
* Places a piece at the position of a move.
|
|
*
|
|
* @param move The move at which position to place the piece
|
|
* @param piece The piece to place
|
|
*/
|
|
public void setPos(Move move, Piece piece) { set(move.getPos(), piece); }
|
|
|
|
/**
|
|
* Places a piece at the destination of a move.
|
|
*
|
|
* @param move The move at which destination to place the piece
|
|
* @param piece The piece to place
|
|
*/
|
|
public void setDest(Move move, Piece piece) { set(move.getDest(), piece); }
|
|
|
|
/**
|
|
* @return The board array
|
|
*/
|
|
public Piece[][] getBoardArr() { return boardArr; }
|
|
|
|
/**
|
|
* @return The move log
|
|
*/
|
|
public Log getLog() { return log; }
|
|
}
|