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
+36
View File
@@ -0,0 +1,36 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### IntelliJ IDEA ###
.idea
.gigaide
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store
+1
View File
@@ -0,0 +1 @@
lombok.copyableAnnotations += com.fasterxml.jackson.annotation.JsonProperty
+96
View File
@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ru.fokinatorr</groupId>
<artifactId>chess.api</artifactId>
<version>0.0.2-SNAPSHOT</version>
<name>Ch4ss API</name>
<description>Common stuff used by both client and server</description>
<properties>
<maven.compiler.source>23</maven.compiler.source>
<maven.compiler.target>23</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.38</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.17</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.5.18</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.18</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.18.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.18.3</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>6.2.6</version>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.3.1</version>
<executions>
<execution>
<id>attach-sources</id>
<phase>verify</phase>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.38</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -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();
}
}
@@ -0,0 +1,164 @@
package ru.fokinatorr.chess.impl;
import lombok.experimental.UtilityClass;
import ru.fokinatorr.chess.impl.movestack.*;
import ru.fokinatorr.chess.impl.piece.*;
import ru.fokinatorr.chess.impl.misc.ChessBoardAndMoveStack;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import static java.util.Map.entry;
import static java.util.Map.ofEntries;
/// Some utilities.
@UtilityClass
public class ChessUtil {
/// Add the given element to the given collection, if it is not `null`.
/// @param collection the collection that may or may not be modified
/// @param element the element to add, if not `null`
/// @throws NullPointerException if collection is `null`
public <T> void addIfNotNull(Collection<T> collection, T element) {
addIfNotNull(collection, element, Function.identity());
}
/// Add the given element to the given collection, if it is not `null`.
/// @param collection the collection that may or may not be modified
/// @param element the element to add, if not `null`
/// @throws NullPointerException if collection is `null`
public <T> void addSingletonListIfNotNull(Collection<? super Collection<T>> collection, T element) {
addIfNotNull(collection, element, List::of);
}
/// Add the given element to the given collection, if it is not `null`.
/// @param collection the collection that may or may not be modified
/// @param element the element to add, if not `null`
/// @param finisher Some function that modifies the element, if it's not `null`.
/// @throws NullPointerException if collection or finisher is `null`
public <T, R> void addIfNotNull(Collection<R> collection, T element, Function<T, R> finisher) {
if (element != null) {
collection.add(finisher.apply(element));
}
}
/// Returns whether the given move was a double move of a pawn of the given color to the given pos.
/// @param move the move to check
/// @param toPos the expected new position of the pawn
/// @param ofColor the expected color of the pawn
public boolean wasPawnDoubleMove(Move move, Position toPos, PieceColor ofColor) {
return move instanceof SimpleMove simpleMove
&& simpleMove.getNewPos().equals(toPos)
&& simpleMove.getMovedPiece() instanceof Pawn pawn
&& pawn.getColor() == ofColor
&& simpleMove.getOldPos().equals(toPos.down(2 * pawn.getForwardMovementFactor()))
&& simpleMove.isWasNeverMoved();
}
/// Returns whether the given move was a pawn double move.
/// @param move the move to check
public boolean wasPawnDoubleMove(Move move) {
return move instanceof SimpleMove simpleMove
&& simpleMove.getMovedPiece() instanceof Pawn pawn
&& simpleMove.getOldPos().equals(simpleMove.getNewPos().down(2 * pawn.getForwardMovementFactor()))
&& simpleMove.isWasNeverMoved();
}
/// Returns whether the given move was a capture.
/// @param move the move to check
public boolean wasCaptureMove(Move move) {
if (move instanceof SimpleMove simpleMove) {
return simpleMove.getCapturedPiece() != null;
} else if (move instanceof EnPassantMove) {
return true;
} else if (move instanceof PawnPromotionMove pawnPromotionMove) {
return pawnPromotionMove.getCapturedPiece() != null;
}
return false;
}
/// Returns whether the given move is valid on the given board.
/// @throws IllegalArgumentException if the move was done
public boolean isValidMove(ChessBoardAndMoveStack chessBoardAndMoveStack, Move move) {
if (move == NoOpMove.INSTANCE) {
return chessBoardAndMoveStack.getChessBoard().getCurrentlyMoving() == PieceColor.WHITE && chessBoardAndMoveStack.peekDone() == null && chessBoardAndMoveStack.peekUndone() == null;
}
if (move.isDone()) {
throw new IllegalArgumentException("The given move was done: " + move);
}
for (Position pos : chessBoardAndMoveStack.getPieces().keySet()) {
if (chessBoardAndMoveStack.getMoves(pos).stream().anyMatch(moveEntry -> moveEntry.move().equals(move))) {
return true;
}
}
return false;
}
/// Suffixes the `base` string with `+` if it is check or with `#` if it is checkmate.
public String commonMoveSuffix(PieceColor movedColor, String base, ChessBoard chessBoard, MoveStack moveStack) {
if (chessBoard.isCheckmate(movedColor.reverse(), moveStack)) {
return base + "#";
} else if (chessBoard.isCheck(movedColor.reverse())) {
return base + "+";
}
return base;
}
/// Parses the given UCI-format move to a `Move` object. Only to be used by chess engines.
/// @param s the move to parse
/// @param chessBoardAndMoveStack the chess board on which the move will be performed
public Move parseUCIMove(String s, ChessBoardAndMoveStack chessBoardAndMoveStack) {
if (s.equals("0000") || s.equals("(none)")) {
return NoOpMove.INSTANCE;
}
// Assuming that is a legal move
Position oldPos = Position.valueOf(s.substring(0, 2)), newPos = Position.valueOf(s.substring(2, 4));
boolean isRight = newPos.getColumn().getIndex() - oldPos.getColumn().getIndex() > 0;
if (chessBoardAndMoveStack.isPieceOfType(oldPos, King.class)
&& (newPos.equals(oldPos.left(2)) || newPos.equals(oldPos.right(2)))) {
if (isRight) {
return new CastlingMove(oldPos, Position.valueOf(Position.Column.H, oldPos.getRow()), true);
} else {
return new CastlingMove(oldPos, Position.valueOf(Position.Column.A, oldPos.getRow()), false);
}
}
if (chessBoardAndMoveStack.isPieceOfType(oldPos, Pawn.class)) {
Pawn pawn = (Pawn) chessBoardAndMoveStack.getPiece(oldPos);
if (chessBoardAndMoveStack.isEmpty(newPos)
&& (newPos.equals(oldPos.move(pawn.getForwardMovementFactor(), 1))
|| newPos.equals(oldPos.move(pawn.getForwardMovementFactor(), -1)))) {
return new EnPassantMove(oldPos, isRight);
}
if (newPos.getRow().equals(pawn.getPromotionRow())) {
return new PawnPromotionMove(oldPos, newPos, pieceColor -> switch (s.charAt(4)) {
case 'q' -> new Queen(pieceColor);
case 'r' -> new Rook(pieceColor);
case 'n' -> new Knight(pieceColor);
case 'b' -> new Bishop(pieceColor);
default -> throw new IllegalArgumentException("Unexpected promotion piece: " + s.charAt(4));
});
}
}
return new SimpleMove(oldPos, newPos);
}
// Pre-create functions to avoid reflection
private static final Map<Class<? extends Piece>, Function<PieceColor, Piece>> PIECE_CREATOR_BY_CLASS = ofEntries(
entry(Bishop.class, Bishop::new),
entry(King.class, King::new),
entry(Knight.class, Knight::new),
entry(Pawn.class, Pawn::new),
entry(Queen.class, Queen::new),
entry(Rook.class, Rook::new)
);
/// Returns a `Function` that creates a new instance of the given `Piece` class. This method exists to avoid reflection
/// when creating a piece by its class.
@SuppressWarnings("unchecked")
public <P extends Piece> Function<PieceColor, P> getPieceCreator(Class<? extends P> pieceClass) {
return (Function<PieceColor, P>) PIECE_CREATOR_BY_CLASS.get(pieceClass);
}
}
@@ -0,0 +1,18 @@
package ru.fokinatorr.chess.impl;
import ru.fokinatorr.chess.impl.movestack.Move;
import java.util.Objects;
/// Some properties returned by {@link ChessBoard#getMoves ChessBoard.getMoves()} method.
/// @param destPos the destination pos of a piece
/// @param move the move to put in a move stack
/// @param isAttack whether this move can be a capture
public record MoveEntry(Position destPos, Move move, boolean isAttack) {
/// @throws NullPointerException if any argument is `null`
public MoveEntry {
Objects.requireNonNull(destPos, "dest pos");
Objects.requireNonNull(move, "move");
}
}
@@ -0,0 +1,105 @@
package ru.fokinatorr.chess.impl;
import ru.fokinatorr.chess.impl.movestack.*;
import ru.fokinatorr.chess.impl.piece.*;
import ru.fokinatorr.chess.impl.Position.Column;
import ru.fokinatorr.chess.impl.Position.Row;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MoveParser {
public static Move parseMove(String moveNotation, ChessBoard chessBoard, MoveStack moveStack) {
// Remove check/checkmate suffixes
String notation = moveNotation.replaceAll("[+#]$", "");
// Castling
if (notation.equals("O-O")) {
// Assuming white kingside castling
return createCastlingMove(PieceColor.WHITE, true);
} else if (notation.equals("O-O-O")) {
// Assuming white queenside castling
return createCastlingMove(PieceColor.WHITE, false);
}
// Pawn Promotion
Pattern promotionPattern = Pattern.compile("([a-h][1-8])=([QRBN])");
Matcher promotionMatcher = promotionPattern.matcher(notation);
if (promotionMatcher.matches()) {
String toPosStr = promotionMatcher.group(1);
String pieceTypeStr = promotionMatcher.group(2);
Position toPos = Position.valueOf(toPosStr);
PieceColor color = chessBoard.getPiece(toPos).getColor().reverse();
Function<PieceColor, Piece> promoter = (pieceColor) -> switch (pieceTypeStr) {
case "Q" -> new Queen(pieceColor);
case "R" -> new Rook(pieceColor);
case "B" -> new Bishop(pieceColor);
case "N" -> new Knight(pieceColor);
default -> throw new IllegalArgumentException("Invalid promotion piece: " + pieceTypeStr);
};
Position fromPos = toPos.up(color == PieceColor.WHITE ? -1 : 1);
return new PawnPromotionMove(fromPos, toPos, promoter);
}
// Piece doneMoves and pawn doneMoves
Pattern pieceMovePattern = Pattern.compile("([KQRBN]?)([a-h]?[1-8]?)?x?([a-h][1-8])");
Matcher pieceMoveMatcher = pieceMovePattern.matcher(notation);
if (pieceMoveMatcher.matches()) {
String pieceTypeStr = pieceMoveMatcher.group(1);
String disambiguation = pieceMoveMatcher.group(2);
String toPosStr = pieceMoveMatcher.group(3);
Position toPos = Position.valueOf(toPosStr);
Class<? extends Piece> pieceType = parsePieceType(pieceTypeStr);
// Find the piece on the board that can move to the target position
PieceColor color = null;
Position fromPos = null;
for (Position pos : chessBoard.getPieces().keySet()) {
Piece piece = chessBoard.getPiece(pos);
if (piece != null && piece.getClass() == pieceType) {
if(chessBoard.getMoves(pos, moveStack).stream().anyMatch(move -> move.destPos().equals(toPos))){
color = piece.getColor();
fromPos = pos;
break;
}
}
}
if (fromPos == null) {
throw new IllegalArgumentException("No piece found that can move to " + toPosStr);
}
return new SimpleMove(fromPos, toPos);
}
throw new IllegalArgumentException("Invalid move notation: " + moveNotation);
}
private static CastlingMove createCastlingMove(PieceColor color, boolean isKingside) {
// Assuming standard castling positions. Adjust if needed for your setup.
int row = (color == PieceColor.WHITE) ? 0 : 7;
Position kingPos = Position.valueOf(Column.E, Row.valueOfIndex(row));
Position rookPos = isKingside ? Position.valueOf(Column.H, Row.valueOfIndex(row)) : Position.valueOf(Column.A, Row.valueOfIndex(row));
return new CastlingMove(kingPos, rookPos, isKingside);
}
private static Class<? extends Piece> parsePieceType(String pieceTypeStr) {
switch (pieceTypeStr) {
case "K": return King.class;
case "Q": return Queen.class;
case "R": return Rook.class;
case "B": return Bishop.class;
case "N": return Knight.class;
case "": return Pawn.class; // Pawn move
default: throw new IllegalArgumentException("Invalid piece type: " + pieceTypeStr);
}
}
}
@@ -0,0 +1,14 @@
package ru.fokinatorr.chess.impl;
/// A color of a piece in chess.
public enum PieceColor {
/// White color, at the bottom of the board at the beginning. They begin the game.
WHITE,
/// Black color, at the top of the board at the beginning.
BLACK;
/// Returns the opposite of this color.
public PieceColor reverse() {
return this == WHITE ? BLACK : WHITE;
}
}
@@ -0,0 +1,25 @@
package ru.fokinatorr.chess.impl;
import lombok.AccessLevel;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import java.util.random.RandomGenerator;
/// A special enum to allow "random color" choice in GUIs, etc.
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
public enum PieceColorOrRandom {
BLACK(PieceColor.BLACK),
WHITE(PieceColor.WHITE),
RANDOM(null);
PieceColor pieceColor;
/// Retrieve the backing [PieceColor], or a random one if `this == PieceColorOrRandom.RANDOM`.
@NonNull
public PieceColor getActualPieceColor(RandomGenerator random) {
return pieceColor == null ? PieceColor.values()[random.nextInt(2)] : pieceColor;
}
}
@@ -0,0 +1,275 @@
package ru.fokinatorr.chess.impl;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.*;
import lombok.experimental.FieldDefaults;
import ru.fokinatorr.chess.misc.Pair;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern;
import static java.util.Map.entry;
import static java.util.Map.ofEntries;
/// A position on a chess board, consisting of a letter `a`-`h` that identifies the column and a number `1`-`8` that
/// identifies the row, e.g. "`e4`".
/// <p>
/// In this class there's a {@link Position#POSITION_PATTERN pattern} describing the rules above.
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@Data
public final class Position {
/// A {@link java.util.regex RegExp} [Pattern] matching all possible chess board positions.
public static final Pattern POSITION_PATTERN = Pattern.compile("^[a-h][1-8]$");
/// Cache the positions to save more memory.
private static final Map<Pair<Column, Row>, Position> POSITION_CACHE = HashMap.newHashMap(64);
Column column;
Row row;
/// Creates a new `Position` with the given column and row.
/// Use the {@link #valueOf static factory} to get an instance of `Position`.
/// @param column the column
/// @param row the row
private Position(Column column, Row row) {
// all sanity checks are done in factory methods
this.column = column;
this.row = row;
}
/// Creates (or gets an existing one) and returns an instance of `Position` with the given
/// column and row.
/// @param column the column
/// @param row the row
/// @throws NullPointerException if any of the arguments is `null`
@JsonCreator
public static Position valueOf(@JsonProperty("column") final Column column, @JsonProperty("row") final Row row) {
Objects.requireNonNull(column, "column");
Objects.requireNonNull(row, "row");
return POSITION_CACHE.computeIfAbsent(new Pair<>(column, row), _ -> new Position(column, row));
}
/// Creates (or gets an existing one) and returns an instance of `Position` with the given
/// column and row names.
/// @param column the column name
/// @param row the row name
/// @throws IllegalArgumentException if any argument is out of bounds
public static Position valueOf(final char column, final int row) {
return valueOf(Column.valueOf(column), Row.valueOfNumber(row));
}
/// Creates (or gets an existing one) and returns an instance of `Position` with the given
/// column and row indexes.
/// @param column the column index
/// @param row the row index
/// @throws IllegalArgumentException if any argument is out of bounds
public static Position valueOf(final int column, final int row) {
return valueOf(Column.valueOf(column), Row.valueOfIndex(row));
}
/// Creates (or gets an existing one) and returns an instance of `Position` with the given
/// string representation.
/// @param pos the textual representation of the position
/// @throws NullPointerException if `pos` is `null`
/// @throws IllegalArgumentException if the given string is invalid
public static Position valueOf(final String pos) {
if (!POSITION_PATTERN.matcher(Objects.requireNonNull(pos, "pos")).matches()) {
throw new IllegalArgumentException("Unexpected value: " + pos);
}
return valueOf(Column.valueOf(pos.charAt(0)), Row.valueOfNumber(pos.charAt(1) - '0'));
}
// moving position
/// Move this position up the given amount of times. If the resulting position is out of bounds,
/// `null` is returned.
/// @param times how much times to move up
public Position up(int times) {
int newRowIndex = row.index + times;
return Row.BY_INDEX.containsKey(newRowIndex) ? valueOf(column.index, newRowIndex) : null;
}
/// Move this position down the given amount of times. If the resulting position is out of bounds,
/// `null` is returned.
/// @param times how much times to move down
public Position down(int times) {
return up(-times);
}
/// Move this position right the given amount of times. If the resulting position is out of bounds,
/// `null` is returned.
/// @param times how much times to move right
public Position left(int times) {
return right(-times);
}
/// Move this position right the given amount of times. If the resulting position is out of bounds,
/// `null` is returned.
/// @param times how much times to move right
public Position right(int times) {
int newColumnIndex = column.index + times;
return Column.BY_INDEX.containsKey(newColumnIndex) ? valueOf(newColumnIndex, row.index) : null;
}
// free move
/// Move this position up and right the given amount of times. If the resulting position is out of bounds,
/// `null` is returned.
/// @param up how much times to move up
/// @param right how much times to move right
public Position move(int up, int right) {
Position pos = up(up);
return pos == null ? null : pos.right(right);
}
// I think diagonal moving will be helpful too
/// Move this position diagonally up and right the given amount of times. If the resulting position is out of bounds,
/// `null` is returned.
/// @param times how much times to move up and right
public Position upRight(int times) {
return move(times, times);
}
/// Move this position diagonally up and left the given amount of times. If the resulting position is out of bounds,
/// `null` is returned.
/// @param times how much times to move up and left
public Position upLeft(int times) {
return move(times, -times);
}
/// Move this position diagonally down and right the given amount of times. If the resulting position is out of bounds,
/// `null` is returned.
/// @param times how much times to move down and right
public Position downRight(int times) {
return move(-times, times);
}
/// Move this position diagonally down and left the given amount of times. If the resulting position is out of bounds,
/// `null` is returned.
/// @param times how much times to move down and left
public Position downLeft(int times) {
return move(-times, -times);
}
@Override
public String toString() {
return column.character + String.valueOf(row.getNumber());
}
/// An enumeration class identifying a column on a chess board.
@Getter
@RequiredArgsConstructor
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public enum Column {
A('a', 0),
B('b', 1),
C('c', 2),
D('d', 3),
E('e', 4),
F('f', 5),
G('g', 6),
H('h', 7);
private static Map<Character, Column> BY_CHARACTER;
private static Map<Integer, Column> BY_INDEX;
char character;
int index;
public static Column valueOf(char character) {
if (BY_CHARACTER == null) {
BY_CHARACTER = ofEntries(
entry('a', A),
entry('b', B),
entry('c', C),
entry('d', D),
entry('e', E),
entry('f', F),
entry('g', G),
entry('h', H)
);
}
Column result = BY_CHARACTER.get(character);
if (result == null) {
throw new IllegalArgumentException("Unexpected value: '" + character + "'");
}
return result;
}
public static Column valueOf(int index) {
if (BY_INDEX == null) {
BY_INDEX = ofEntries(
entry(0, A),
entry(1, B),
entry(2, C),
entry(3, D),
entry(4, E),
entry(5, F),
entry(6, G),
entry(7, H)
);
}
Column result = BY_INDEX.get(index);
if (result == null) {
throw new IllegalArgumentException("Unexpected value: " + index);
}
return result;
}
}
/// An enumeration class identifying a row on a chess board.
@Getter
@RequiredArgsConstructor
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public enum Row {
ONE(0),
TWO(1),
THREE(2),
FOUR(3),
FIVE(4),
SIX(5),
SEVEN(6),
EIGHT(7);
private static Map<Integer, Row> BY_INDEX;
int index;
/**
* {@return the number representation of a row}
*/
public int getNumber() {
return index + 1;
}
public static Row valueOfNumber(int number) {
return valueOfIndex(number - 1);
}
public static Row valueOfIndex(int index) {
if (BY_INDEX == null) {
BY_INDEX = ofEntries(
entry(0, ONE),
entry(1, TWO),
entry(2, THREE),
entry(3, FOUR),
entry(4, FIVE),
entry(5, SIX),
entry(6, SEVEN),
entry(7, EIGHT)
);
}
Row result = BY_INDEX.get(index);
if (result == null) {
throw new IllegalArgumentException("Unexpected value: " + index);
}
return result;
}
}
}
@@ -0,0 +1,285 @@
package ru.fokinatorr.chess.impl.misc;
import lombok.*;
import lombok.experimental.FieldDefaults;
import lombok.experimental.UtilityClass;
import ru.fokinatorr.chess.impl.*;
import ru.fokinatorr.chess.impl.movestack.Move;
import ru.fokinatorr.chess.impl.movestack.MoveStack;
import ru.fokinatorr.chess.impl.movestack.SimpleMove;
import ru.fokinatorr.chess.impl.piece.*;
import java.io.Serial;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Function;
/// A pair of [ChessBoard] and [MoveStack].
@Getter
@EqualsAndHashCode
@ToString
@RequiredArgsConstructor
@FieldDefaults(makeFinal = true, level = AccessLevel.PROTECTED)
public class ChessBoardAndMoveStack implements Serializable, Cloneable { // not record to allow subclassing
@Serial
private static final long serialVersionUID = -5529146297013520402L;
@NonNull
ChessBoard chessBoard;
@NonNull
MoveStack moveStack;
/// Constructs an empty `ChessBoardAndMoveStack`.
public ChessBoardAndMoveStack() {
this(new ChessBoard(), new MoveStack());
}
/// Constructs a `ChessBoardAndMoveStack` with the given starting position chess board. It is cloned.
public ChessBoardAndMoveStack(ChessBoard startPos) {
this(startPos.clone(), new MoveStack());
}
/// Constructs a `ChessBoardAndMoveStack`, in which the starting position is {@link ChessBoard#startPosition() default}
/// and then the doneMoves from the given stack are performed on it.
public ChessBoardAndMoveStack(MoveStack moveStack) {
this(ChessBoard.startPosition(), new MoveStack());
for (Move move : moveStack.getDoneMoves()) {
perform(move);
}
}
/// Returns a `ChessBoardAndMoveStack`, in which the starting position is {@link ChessBoard#startPosition() default}.
public static ChessBoardAndMoveStack startPosition() {
return new ChessBoardAndMoveStack(ChessBoard.startPosition());
}
/// @see MoveStack#perform(Move, ChessBoard)
public void perform(Move move) {
moveStack.perform(move, chessBoard);
}
/// @see MoveStack#undo(ChessBoard)
public Move undo() {
return moveStack.undo(chessBoard);
}
/// @see MoveStack#redo(ChessBoard)
public Move redo() {
return moveStack.redo(chessBoard);
}
/// @see MoveStack#peekDone()
public Move peekDone() {
return moveStack.peekDone();
}
/// @see MoveStack#peekUndone()
public Move peekUndone() {
return moveStack.peekUndone();
}
/// Sets the pawn promoter for the chess board.
/// @see ChessBoard#setPawnPromoter(Function)
public void setPawnPromoter(Function<PieceColor, Piece> pawnPromoter) {
chessBoard.setPawnPromoter(pawnPromoter);
}
/// Reverses the currently moving player.
/// @see PieceColor#reverse()
/// @see ChessBoard#reverseCurrentlyMoving()
public void reverseCurrentlyMoving() {
chessBoard.reverseCurrentlyMoving();
}
/// @see ChessBoard#getPiece(Position)
public Piece getPiece(Position pos) {
return chessBoard.getPiece(pos);
}
/// @see ChessBoard#getPieces(Position.Column)
public List<Piece> getPieces(Position.Column column) {
return chessBoard.getPieces(column);
}
/// @see ChessBoard#getPieces(Position.Row)
public List<Piece> getPieces(Position.Row row) {
return chessBoard.getPieces(row);
}
/// @see ChessBoard#getPieces()
public Map<Position, Piece> getPieces() {
return chessBoard.getPieces();
}
/// @see ChessBoard#setPiece(Position, Piece)
public Piece setPiece(Position pos, Piece newPiece) {
return chessBoard.setPiece(pos, newPiece);
}
/// @see ChessBoard#isPieceOfColor(Position, PieceColor)
public boolean isPieceOfColor(Position pos, PieceColor color) {
return chessBoard.isPieceOfColor(pos, color);
}
/// @see ChessBoard#isPieceOfType(Position, Class)
public boolean isPieceOfType(Position pos, Class<? extends Piece> type) {
return chessBoard.isPieceOfType(pos, type);
}
/// @see ChessBoard#isPieceOfColorAndType(Position, PieceColor, Class)
public boolean isPieceOfColorAndType(Position pos, PieceColor color, Class<? extends Piece> type) {
return chessBoard.isPieceOfColorAndType(pos, color, type);
}
/// @see ChessBoard#isPresent(Position)
public boolean isPresent(Position pos) {
return chessBoard.isPresent(pos);
}
/// @see ChessBoard#isEmpty(Position)
public boolean isEmpty(Position pos) {
return chessBoard.isEmpty(pos);
}
/// @see ChessBoard#getMoves(Position, MoveStack)
public Collection<MoveEntry> getMoves(Position pos) {
return chessBoard.getMoves(pos, moveStack);
}
/// @see ChessBoard#isPositionAttacked(Position, PieceColor)
public boolean isPositionAttacked(Position givenPos, PieceColor color) {
return chessBoard.isPositionAttacked(givenPos, color);
}
/// @see ChessBoard#isCheck(PieceColor)
public boolean isCheck(PieceColor kingColor) {
return chessBoard.isCheck(kingColor);
}
/// @see ChessBoard#isCheckmate(PieceColor, MoveStack)
public boolean isCheckmate(PieceColor color) {
return chessBoard.isCheckmate(color, moveStack);
}
/// @see ChessBoard#isStalemate(PieceColor, MoveStack)
public boolean isStalemate(PieceColor color) {
return chessBoard.isStalemate(color, moveStack);
}
/// Converts this `ChessBoardAndMoveStack` to Forsyth–Edwards Notation (FEN) format. Useful for `ChessEngine` implementations.
public String toFEN() {
StringBuilder sb = new StringBuilder();
// write down pieces
for (int i = Position.Row.values().length - 1; i >= 0; i--) { // reverse order of rows
Position.Row row = Position.Row.values()[i];
int emptyCount = 0;
for (Position.Column column : Position.Column.values()) {
Piece piece = getPiece(Position.valueOf(column, row));
if (piece != null) {
if (emptyCount != 0) {
sb.append(emptyCount);
}
emptyCount = 0;
char pieceFEN = 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(pieceFEN) : pieceFEN);
} else {
emptyCount++;
}
}
if (emptyCount != 0) {
sb.append(emptyCount);
}
sb.append('/');
}
sb.deleteCharAt(sb.length() - 1) // remove last '/'
.append(' ')
.append(chessBoard.getCurrentlyMoving().name().toLowerCase(Locale.ENGLISH).charAt(0)) // currently moving color
.append(' ');
boolean noCastling = true;
if (FENHelper.canCastle(chessBoard, PieceColor.WHITE, FENHelper.WHITE_KING_POS, FENHelper.WHITE_ROOK_KSIDE_POS)) {
sb.append('K');
noCastling = false;
}
if (FENHelper.canCastle(chessBoard, PieceColor.WHITE, FENHelper.WHITE_KING_POS, FENHelper.WHITE_ROOK_QSIDE_POS)) {
sb.append('Q');
noCastling = false;
}
if (FENHelper.canCastle(chessBoard, PieceColor.BLACK, FENHelper.BLACK_KING_POS, FENHelper.BLACK_ROOK_KSIDE_POS)) {
sb.append('k');
noCastling = false;
}
if (FENHelper.canCastle(chessBoard, PieceColor.BLACK, FENHelper.BLACK_KING_POS, FENHelper.BLACK_ROOK_QSIDE_POS)) {
sb.append('q');
noCastling = false;
}
if (noCastling) {
sb.append('-');
}
sb.append(' ');
if (ChessUtil.wasPawnDoubleMove(peekDone())) { // implicit null check in Util.wasPawnDoubleMove()
SimpleMove simpleMove = (SimpleMove) peekDone();
sb.append(simpleMove.getNewPos().down(((Pawn) simpleMove.getMovedPiece()).getForwardMovementFactor()));
} else {
sb.append('-');
}
sb.append(' ');
int fiftyMovesClock = 0;
double moveCount = 1;
for (Move move : moveStack.getDoneMoves()) {
if (move instanceof SimpleMove simpleMove && simpleMove.getMovedPiece() instanceof Pawn || ChessUtil.wasCaptureMove(move)) {
fiftyMovesClock = 0;
} else {
fiftyMovesClock++;
}
// Since moveStack.getDoneMoves() returns Collection, we can't find index of any item
moveCount += 0.5;
}
sb.append(fiftyMovesClock)
.append(' ')
.append((int) Math.floor(moveCount)); // cast to int to prevent ".0" suffix
return sb.toString();
}
/// Creates and returns a copy of this `ChessBoardAndMoveStack`.
@Override
@SuppressWarnings("MethodDoesntCallSuperMethod")
public ChessBoardAndMoveStack clone() {
return new ChessBoardAndMoveStack(chessBoard.clone(), moveStack.clone());
}
}
@UtilityClass
class FENHelper {
static final Position WHITE_KING_POS = Position.valueOf(Position.Column.E, Position.Row.ONE),
WHITE_ROOK_QSIDE_POS = Position.valueOf(Position.Column.A, Position.Row.ONE),
WHITE_ROOK_KSIDE_POS = Position.valueOf(Position.Column.H, Position.Row.ONE),
BLACK_KING_POS = Position.valueOf(Position.Column.E, Position.Row.EIGHT),
BLACK_ROOK_QSIDE_POS = Position.valueOf(Position.Column.A, Position.Row.EIGHT),
BLACK_ROOK_KSIDE_POS = Position.valueOf(Position.Column.H, Position.Row.EIGHT);
boolean isPieceOfColorAndTypeAndNeverMoved(ChessBoard chessBoard, Position pos, PieceColor color, Class<? extends Piece> type) {
return chessBoard.isPieceOfColorAndType(pos, color, type) // if piece == null, returns false
&& chessBoard.getPiece(pos).isNeverMoved();
}
boolean canCastle(ChessBoard chessBoard, PieceColor color, Position kingPos, Position rookPos) {
return isPieceOfColorAndTypeAndNeverMoved(chessBoard, kingPos, color, King.class)
&& isPieceOfColorAndTypeAndNeverMoved(chessBoard, rookPos, color, Rook.class);
}
}
@@ -0,0 +1,116 @@
package ru.fokinatorr.chess.impl.movestack;
import lombok.*;
import lombok.experimental.FieldDefaults;
import ru.fokinatorr.chess.impl.ChessBoard;
import ru.fokinatorr.chess.impl.ChessUtil;
import ru.fokinatorr.chess.impl.Position;
import ru.fokinatorr.chess.impl.piece.King;
import ru.fokinatorr.chess.impl.piece.Piece;
import ru.fokinatorr.chess.impl.piece.Rook;
/// A special move, where a king doneMoves two fields horizontally and a rook stands on the position which the king "jumped" over.
/// @see <a href="https://wikipedia.org/wiki/Castling">"Castling" on Wikipedia</a>
@Getter
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@ToString
@FieldDefaults(level = AccessLevel.PRIVATE)
public non-sealed class CastlingMove implements Move {
// explicit constructor because kingNewPos & rookNewPos are final and for some reason
// the compiler doesn't allow to init them in the field definitions D:
/// Creates a new `CastlingMove`.
/// @param kingOldPos The king's initial position (must be e1 or e8)
/// @param rookOldPos The rook's initial position (must be a1, a8, h1 or h8)
/// @param kingside whether to castle to the right
public CastlingMove(Position kingOldPos, Position rookOldPos, boolean kingside) {
this.kingOldPos = kingOldPos;
this.rookOldPos = rookOldPos;
this.kingside = kingside;
kingNewPos = kingOldPos.right(kingside ? 2 : -2);
rookNewPos = kingOldPos.right(kingside ? 1 : -1);
}
final Position kingOldPos;
final Position rookOldPos;
final boolean kingside;
King king;
Rook rook;
final Position kingNewPos;
final Position rookNewPos;
@Getter(AccessLevel.NONE)
String cacheNotation;
boolean done;
/// {@inheritDoc}
/// @throws IllegalStateException {@inheritDoc}
@Override
public void perform(ChessBoard chessBoard, MoveStack moveStack) {
Piece piece1 = chessBoard.setPiece(kingOldPos, null);
if (!(piece1 instanceof King kingPiece)) {
throw new IllegalStateException("The piece on " + kingOldPos + " on " + chessBoard + " is not a King");
} else {
Piece piece2 = chessBoard.setPiece(rookOldPos, null);
if (!(piece2 instanceof Rook rookPiece)) {
throw new IllegalStateException("The piece on " + rookOldPos + " on " + chessBoard + " is not a Rook");
} else if (rookPiece.getColor() != kingPiece.getColor()) {
throw new IllegalStateException(rookPiece + " and " + kingPiece + " are not the same color");
} else {
king = kingPiece;
rook = rookPiece;
chessBoard.setPiece(kingNewPos, kingPiece);
chessBoard.setPiece(rookNewPos, rookPiece);
done = true;
}
}
}
/// {@inheritDoc}
/// @throws IllegalStateException {@inheritDoc}
@Override
public void undo(ChessBoard chessBoard) {
if (chessBoard.setPiece(kingNewPos, null) == null) {
throw new IllegalStateException("No piece to move from " + kingNewPos + " on " + chessBoard);
}
if (chessBoard.setPiece(rookNewPos, null) == null) {
throw new IllegalStateException("No piece to move from " + rookNewPos + " on " + chessBoard);
}
chessBoard.setPiece(kingOldPos, king);
chessBoard.setPiece(rookOldPos, rook);
king = null;
rook = null;
cacheNotation = null;
done = false;
}
/// {@inheritDoc}
/// @throws IllegalStateException {@inheritDoc}
@Override
public String getNotationRepresentation(ChessBoard afterMoveChessBoard, MoveStack afterMoveMoveStack) {
if (!done) {
throw new IllegalStateException("This move wasn't done yet");
}
if (cacheNotation != null) {
return cacheNotation;
}
return cacheNotation = ChessUtil.commonMoveSuffix(king.getColor(), kingside ? "O-O" : "O-O-O", afterMoveChessBoard, afterMoveMoveStack);
}
/// {@inheritDoc}
@Override
public String toUCI() {
return kingOldPos.toString() + kingNewPos.toString();
}
/// {@inheritDoc}
@Override
public CastlingMove deepCopy() {
CastlingMove copy = new CastlingMove(kingOldPos, rookOldPos, kingside);
copy.king = this.king == null ? null : (King) this.king.clone();
copy.rook = this.rook == null ? null : (Rook) this.rook.clone();
copy.cacheNotation = this.cacheNotation;
copy.done = this.done;
return copy;
}
}
@@ -0,0 +1,123 @@
package ru.fokinatorr.chess.impl.movestack;
import lombok.*;
import lombok.experimental.FieldDefaults;
import ru.fokinatorr.chess.impl.ChessBoard;
import ru.fokinatorr.chess.impl.ChessUtil;
import ru.fokinatorr.chess.impl.Position;
import ru.fokinatorr.chess.impl.piece.Pawn;
import ru.fokinatorr.chess.impl.piece.Piece;
import java.util.Locale;
import java.util.Objects;
/// A special move, so-called "en passant capture" where a pawn can capture another pawn, "passing" it.
/// @see <a href="https://wikipedia.org/wiki/En_passant">"En passant" on Wikipedia</a>
@Getter
@RequiredArgsConstructor
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@ToString
@FieldDefaults(level = AccessLevel.PRIVATE)
public non-sealed class EnPassantMove implements Move {
// Yes, it's that one move that requires VERY specific conditions to be legal. It must even consider the previous move! :O
@EqualsAndHashCode.Include
final Position oldPos;
@EqualsAndHashCode.Include
final boolean isRight;
Position newPos;
Pawn movedPiece;
Position capturePos;
Pawn capturedPiece;
@Getter(AccessLevel.NONE)
String cacheNotation;
boolean done;
/// {@inheritDoc}
/// @throws IllegalStateException {@inheritDoc}
/// @throws NullPointerException if a piece is moved out of range
@Override
public void perform(ChessBoard chessBoard, MoveStack moveStack) {
Piece piece = chessBoard.setPiece(oldPos, null);
if (piece == null) {
throw new IllegalStateException("No piece to move from " + oldPos + " on " + chessBoard);
} else if (!(piece instanceof Pawn pawn)) {
throw new IllegalStateException("The piece on " + oldPos + " on " + chessBoard + " is not a Pawn, but a " + piece.getClass().getSimpleName());
} else if (pawn.getEnPassantRow() != oldPos.getRow()) {
throw new IllegalStateException("A " + pawn.getColor().toString().toLowerCase(Locale.ENGLISH) + " Pawn can't perform an en passant from row " + oldPos.getRow().getNumber());
} else {
Position capture = Objects.requireNonNull(oldPos.right(isRight ? 1 : -1), "Can't move the Pawn from " + oldPos + " outside the board");
Position target = Objects.requireNonNull(capture.up(pawn.getForwardMovementFactor()), "Can't move the Pawn from " + oldPos + " outside the board");
Piece targetPiece = chessBoard.getPiece(capture);
if (!(targetPiece instanceof Pawn targetPawn) || targetPawn.getColor() == pawn.getColor()) {
throw new IllegalStateException("En passant can only capture a pawn of different color");
} else {
if (moveStack == null || ChessUtil.wasPawnDoubleMove(moveStack.peekDone(), capture, targetPawn.getColor())) {
movedPiece = pawn;
capturedPiece = targetPawn;
capturePos = capture;
newPos = target;
chessBoard.setPiece(capture, null);
chessBoard.setPiece(target, pawn);
done = true;
} else {
throw new IllegalStateException("The previous move must be a double-move of a pawn of different color");
}
}
}
}
/// {@inheritDoc}
/// @throws IllegalStateException {@inheritDoc}
@Override
public void undo(ChessBoard chessBoard) {
if (chessBoard.setPiece(newPos, null) == null) {
throw new IllegalStateException("No piece to move from " + newPos + " on " + chessBoard);
}
chessBoard.setPiece(oldPos, movedPiece);
chessBoard.setPiece(capturePos, capturedPiece);
// cache the newPos for UCI
movedPiece = null;
capturedPiece = null;
capturePos = null;
done = false;
}
/// {@inheritDoc}
/// @throws IllegalStateException {@inheritDoc}
@Override
public String getNotationRepresentation(ChessBoard afterMoveChessBoard, MoveStack afterMoveMoveStack) {
if (!done) {
throw new IllegalStateException("This move wasn't done yet");
}
if (cacheNotation != null) {
return cacheNotation;
}
return cacheNotation = ChessUtil.commonMoveSuffix(
movedPiece.getColor(),
oldPos.getColumn().getCharacter() + "x" + newPos + " e.p.",
afterMoveChessBoard,
afterMoveMoveStack
);
}
/// {@inheritDoc}
@Override
public String toUCI() {
return oldPos.toString() + newPos.toString();
}
/// {@inheritDoc}
@Override
public EnPassantMove deepCopy() {
EnPassantMove copy = new EnPassantMove(oldPos, isRight);
copy.newPos = this.newPos;
copy.movedPiece = this.movedPiece == null ? null : (Pawn) this.movedPiece.clone();
copy.capturePos = this.capturePos;
copy.capturedPiece = this.capturedPiece == null ? null : (Pawn) this.capturedPiece.clone();
copy.cacheNotation = this.cacheNotation;
copy.done = this.done;
return copy;
}
}
@@ -0,0 +1,37 @@
package ru.fokinatorr.chess.impl.movestack;
import ru.fokinatorr.chess.impl.ChessBoard;
/// Defines a move performed on a chessboard, someway modifying it. It can be undone.
/// <p>
/// It is not recommended to perform and undo this move on different boards.
public sealed interface Move extends java.io.Serializable permits CastlingMove, EnPassantMove, NoOpMove, PawnPromotionMove, SimpleMove {
/// Perform this move on the given board.
/// @param moveStack may be required if the move needs to know the previous move. Only yet required by [EnPassantMove].
/// @throws IllegalStateException if the move couldn't be performed on the given board
void perform(ChessBoard chessBoard, MoveStack moveStack);
/// Undo this move on the given board.
/// @throws IllegalStateException if the move couldn't be undone on the given board
void undo(ChessBoard chessBoard);
/// Determine whether this move was done on some board.
boolean isDone();
/// Returns how this move would be represented in the chess notation,
/// e.g. `Nc3` if a knight moved to `c3`. Only can be executed if this move
/// was {@link #isDone() done}.
/// @throws IllegalStateException if this move wasn't done yet.
String getNotationRepresentation(ChessBoard afterMoveChessBoard, MoveStack afterMoveMoveStack);
/// Returns this move in the Universal Chess Interface (UCI) format.
String toUCI();
/// Creates and returns a deep copy of this move. This method exists instead of clone() because enums do not allow cloning.
Move deepCopy();
/// Returns whether this move is equal to the given object. Two moves are equal if and only if
/// they change the same board the same way.
@Override
boolean equals(Object obj);
}
@@ -0,0 +1,88 @@
package ru.fokinatorr.chess.impl.movestack;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.experimental.FieldDefaults;
import ru.fokinatorr.chess.impl.ChessBoard;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
/// A stack containing the moves that were done on some board.
/// It has two stacks: done moves and undone moves, so redoing moves is possible.
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class MoveStack implements java.io.Serializable, Cloneable {
@java.io.Serial
private static final long serialVersionUID = 1698600303738920117L;
Deque<Move> doneMoves = new ArrayDeque<>();
Deque<Move> undoneMoves = new ArrayDeque<>();
/// Performs the given move on the given board, puts it into the done moves stack and clears the undone moves stack.
/// @throws IllegalStateException if the move couldn't be performed on the given board
public void perform(Move move, ChessBoard chessBoard) {
move.perform(chessBoard, this);
doneMoves.offerLast(move);
undoneMoves.clear();
}
/// Undoes the last move in done moves stack on the given board, puts it into the undone moves stack, removes it from the done moves stack and returns it.
/// If there's no move to undo, `null` is returned.
/// @throws IllegalStateException if the move couldn't be undone on the given board
public Move undo(ChessBoard chessBoard) {
Move undo = doneMoves.pollLast();
if (undo != null) {
undo.undo(chessBoard);
undoneMoves.offerLast(undo);
}
return undo;
}
/// Redoes the last move in undone moves stack on the given board, puts it into the done moves stack, removes it from the undone moves stack and returns it.
/// If there's no move to redo, `null` is returned.
/// @throws IllegalStateException if the move couldn't be performed on the given board
public Move redo(ChessBoard chessBoard) {
Move redo = undoneMoves.pollLast();
if (redo != null) {
redo.perform(chessBoard, this);
doneMoves.offerLast(redo);
}
return redo;
}
/// Retrieves, but does not remove, the last move in the done moves stack,
/// or returns `null` if the stack is empty.
public Move peekDone() {
return doneMoves.peekLast();
}
/// Retrieves, but does not remove, the last move in the undone moves stack,
/// or returns `null` if the stack is empty.
public Move peekUndone() {
return undoneMoves.peekLast();
}
/// Returns an unmodifiable view of the done moves stack. This method exists to allow restoring the chess board by a stack of moves.
public Collection<Move> getDoneMoves() {
return Collections.unmodifiableCollection(doneMoves);
}
/// Returns an unmodifiable view of the undone moves stack. This method exists to allow restoring the chess board by a stack of moves.
public Collection<Move> getUndoneMoves() {
return Collections.unmodifiableCollection(undoneMoves);
}
/// Creates and returns a copy of this `MoveStack`.
@Override
@SuppressWarnings("MethodDoesntCallSuperMethod")
public MoveStack clone() {
MoveStack newStack = new MoveStack();
newStack.doneMoves.addAll(doneMoves.stream().map(Move::deepCopy).toList());
newStack.undoneMoves.addAll(undoneMoves.stream().map(Move::deepCopy).toList());
return newStack;
}
}
@@ -0,0 +1,47 @@
package ru.fokinatorr.chess.impl.movestack;
import ru.fokinatorr.chess.impl.ChessBoard;
/// A move that does nothing. This move is required if black starts in a custom position, then first white's move is a `NoOpMove`.
public enum NoOpMove implements Move {
/// Sole instance.
INSTANCE;
/// {@inheritDoc}
/// As stated in the class's documentation, this move does nothing.
@Override
public void perform(ChessBoard chessBoard, MoveStack moveStack) {
}
/// {@inheritDoc}
/// As stated in the class's documentation, this move does nothing, so there's nothing to undo.
@Override
public void undo(ChessBoard chessBoard) {
}
/// Returns how this move would be represented in chess notation. Unlike other implementations, this method never throws any exceptions.
@Override
public String getNotationRepresentation(ChessBoard afterMoveChessBoard, MoveStack afterMoveMoveStack) {
return "...";
}
///{@inheritDoc}
@Override
public String toUCI() {
return "0000";
}
/// {@inheritDoc}
@Override
public NoOpMove deepCopy() {
return INSTANCE; // enums can't clone
}
/// {@inheritDoc}
/// This method always returns `true`, but that doesn't really matter since this is never checked.
@Override
public boolean isDone() {
return true;
}
}
@@ -0,0 +1,129 @@
package ru.fokinatorr.chess.impl.movestack;
import lombok.*;
import lombok.experimental.FieldDefaults;
import ru.fokinatorr.chess.impl.ChessBoard;
import ru.fokinatorr.chess.impl.ChessUtil;
import ru.fokinatorr.chess.impl.PieceColor;
import ru.fokinatorr.chess.impl.Position;
import ru.fokinatorr.chess.impl.piece.King;
import ru.fokinatorr.chess.impl.piece.Pawn;
import ru.fokinatorr.chess.impl.piece.Piece;
import java.util.Locale;
import java.util.Objects;
import java.util.function.Function;
/// A special move, where a pawn doneMoves to the edge of a board and transforms into some other piece (any except king).
/// @see <a href="https://wikipedia.org/wiki/Promotion_%28chess%29">"Pawn promotion" on Wikipedia</a>
@Getter
@RequiredArgsConstructor
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@ToString
@FieldDefaults(level = AccessLevel.PRIVATE)
public non-sealed class PawnPromotionMove implements Move {
@EqualsAndHashCode.Include
final Position oldPos;
@EqualsAndHashCode.Include
final Position newPos;
@Setter
@NonNull
Function<PieceColor, Piece> pawnPromoter;
// OR
Class<? extends Piece> newPieceType;
Pawn movedPiece;
Piece promotedToPiece;
Piece capturedPiece;
@Getter(AccessLevel.NONE)
String cacheNotation;
boolean done;
///{@inheritDoc}
/// @throws IllegalStateException {@inheritDoc}
/// @throws IllegalArgumentException if the piece to promote to is invalid
/// @throws NullPointerException if the piece to promote to is `null`
@Override
public void perform(ChessBoard chessBoard, MoveStack moveStack) {
Piece piece = chessBoard.setPiece(oldPos, null);
if (piece == null) {
throw new IllegalStateException("No piece to move from " + oldPos + " on " + chessBoard);
} else if (!(piece instanceof Pawn pawn)) {
throw new IllegalStateException("The piece on " + oldPos + " on " + chessBoard + " is not a Pawn, but a " + piece.getClass().getSimpleName());
} else {
movedPiece = pawn;
if (newPieceType != null) { // the move is redone
pawnPromoter = ChessUtil.getPieceCreator(newPieceType);
}
Piece newPiece = Objects.requireNonNull(pawnPromoter.apply(pawn.getColor()), "Cannot promote a Pawn to null");
if (newPiece instanceof Pawn || newPiece instanceof King) {
throw new IllegalArgumentException("Cannot promote a Pawn to a Pawn or King");
}
if (newPiece.getColor() != pawn.getColor()) {
throw new IllegalArgumentException("Cannot promote a Pawn to a piece of different color");
}
newPiece.setNeverMoved(false);
promotedToPiece = newPiece;
if (newPieceType == null) {
newPieceType = newPiece.getClass();
}
capturedPiece = chessBoard.setPiece(newPos, newPiece);
done = true;
}
}
///{@inheritDoc}
/// @throws IllegalStateException {@inheritDoc}
@Override
public void undo(ChessBoard chessBoard) {
if (chessBoard.setPiece(newPos, capturedPiece) == null) {
throw new IllegalStateException("No piece to move from " + newPos + " on " + chessBoard);
}
promotedToPiece = null;
// cache newPieceType for UCI
capturedPiece = null;
chessBoard.setPiece(oldPos, movedPiece);
movedPiece = null;
done = false;
}
/// {@inheritDoc}
/// @throws IllegalStateException {@inheritDoc}
@Override
public String getNotationRepresentation(ChessBoard afterMoveChessBoard, MoveStack afterMoveMoveStack) {
if (!done) {
throw new IllegalStateException("This move wasn't done yet");
}
if (cacheNotation != null) {
return cacheNotation;
}
return cacheNotation = ChessUtil.commonMoveSuffix(
movedPiece.getColor(),
(capturedPiece == null ? "" : oldPos.getColumn().getCharacter() + "x") + newPos + "=" + promotedToPiece.getNotationRepresentation(),
afterMoveChessBoard,
afterMoveMoveStack
);
}
/// {@inheritDoc}
@Override
public String toUCI() {
if (!done) {
throw new IllegalStateException("This move wasn't done yet");
}
return oldPos.toString() + newPos.toString() + newPieceType.getSimpleName().toLowerCase(Locale.ENGLISH).charAt(0);
}
/// {@inheritDoc}
@Override
public PawnPromotionMove deepCopy() {
PawnPromotionMove copy = new PawnPromotionMove(oldPos, newPos, pawnPromoter);
copy.newPieceType = this.newPieceType;
copy.movedPiece = this.movedPiece == null ? null : (Pawn) this.movedPiece.clone();
copy.promotedToPiece = this.promotedToPiece == null ? null : this.promotedToPiece.clone();
copy.capturedPiece = this.capturedPiece == null ? null : this.capturedPiece.clone();
copy.cacheNotation = this.cacheNotation;
copy.done = this.done;
return copy;
}
}
@@ -0,0 +1,119 @@
package ru.fokinatorr.chess.impl.movestack;
import lombok.*;
import lombok.experimental.FieldDefaults;
import ru.fokinatorr.chess.impl.ChessBoard;
import ru.fokinatorr.chess.impl.ChessUtil;
import ru.fokinatorr.chess.impl.Position;
import ru.fokinatorr.chess.impl.piece.Pawn;
import ru.fokinatorr.chess.impl.piece.Piece;
/// A simple move, which is just moving a piece to a different position.
/// <p>
/// Performing this move can end up a piece being captured. You can also retrieve the captured piece.
@Getter
@RequiredArgsConstructor
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@ToString
@FieldDefaults(level = AccessLevel.PRIVATE)
public non-sealed class SimpleMove implements Move {
@EqualsAndHashCode.Include
final Position oldPos;
@EqualsAndHashCode.Include
final Position newPos;
Piece movedPiece;
boolean wasNeverMoved;
Piece capturedPiece;
@Getter(AccessLevel.NONE)
String cacheNotation;
@Getter(AccessLevel.NONE)
ChessBoard beforeMoveChessBoard;
@Getter(AccessLevel.NONE)
MoveStack beforeMoveMoveStack;
boolean done;
/// {@inheritDoc}
/// @throws IllegalStateException {@inheritDoc}
@Override
public void perform(ChessBoard chessBoard, MoveStack moveStack) {
beforeMoveChessBoard = chessBoard.clone();
beforeMoveMoveStack = moveStack.clone();
Piece piece = chessBoard.setPiece(oldPos, null);
if (piece == null) {
throw new IllegalStateException("No piece to move from " + oldPos + " on " + chessBoard);
}
capturedPiece = chessBoard.setPiece(newPos, movedPiece = piece);
wasNeverMoved = piece.isNeverMoved();
piece.setNeverMoved(false);
done = true;
}
/// {@inheritDoc}
/// @throws IllegalStateException {@inheritDoc}
@Override
public void undo(ChessBoard chessBoard) {
Piece piece = chessBoard.setPiece(newPos, capturedPiece);
if (piece == null) {
throw new IllegalStateException("No piece to move from " + newPos + " on " + chessBoard);
}
movedPiece = capturedPiece = null;
chessBoard.setPiece(oldPos, piece);
piece.setNeverMoved(wasNeverMoved);
beforeMoveChessBoard = null;
beforeMoveMoveStack = null;
done = false;
}
///{@inheritDoc}
/// @throws IllegalStateException {@inheritDoc}
@Override
public String getNotationRepresentation(ChessBoard afterMoveChessBoard, MoveStack afterMoveMoveStack) {
if (!done) {
throw new IllegalStateException("This move wasn't done yet");
}
if (cacheNotation != null) {
return cacheNotation;
}
StringBuilder sb = new StringBuilder(movedPiece.getNotationRepresentation());
boolean includeColumnInNotation = false, includeRowInNotation = false;
for (Position pos : beforeMoveChessBoard.getPieces().keySet()) {
if (!pos.equals(oldPos) && beforeMoveChessBoard.isPieceOfColorAndType(pos, movedPiece.getColor(), movedPiece.getClass())
&& beforeMoveChessBoard.getMoves(pos, beforeMoveMoveStack).stream().anyMatch(moveEntry -> moveEntry.destPos().equals(newPos))) {
includeColumnInNotation = pos.getRow() == oldPos.getRow();
includeRowInNotation = pos.getColumn() == oldPos.getColumn();
}
}
if (includeColumnInNotation || movedPiece instanceof Pawn && capturedPiece != null) {
sb.append(oldPos.getColumn().getCharacter());
}
if (includeRowInNotation) {
sb.append(oldPos.getRow().getNumber());
}
if (capturedPiece != null) {
sb.append("x");
}
return cacheNotation = ChessUtil.commonMoveSuffix(movedPiece.getColor(), sb.append(newPos).toString(), afterMoveChessBoard, afterMoveMoveStack);
}
/// {@inheritDoc}
@Override
public String toUCI() {
return oldPos.toString() + newPos.toString();
}
/// {@inheritDoc}
@Override
public SimpleMove deepCopy() {
SimpleMove copy = new SimpleMove(oldPos, newPos);
copy.movedPiece = this.movedPiece == null ? null : this.movedPiece.clone();
copy.wasNeverMoved = this.wasNeverMoved;
copy.capturedPiece = this.capturedPiece == null ? null : this.capturedPiece.clone();
copy.cacheNotation = this.cacheNotation;
copy.beforeMoveChessBoard = this.beforeMoveChessBoard;
copy.beforeMoveMoveStack = this.beforeMoveMoveStack;
copy.done = this.done;
return copy;
}
}
@@ -0,0 +1,2 @@
/// Implementation of the Chess Logic.
package ru.fokinatorr.chess.impl;
@@ -0,0 +1,37 @@
package ru.fokinatorr.chess.impl.piece;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import ru.fokinatorr.chess.impl.ChessUtil;
import ru.fokinatorr.chess.impl.PieceColor;
import ru.fokinatorr.chess.impl.Position;
import java.util.*;
import static ru.fokinatorr.chess.impl.piece.ConditionalMovement.*;
/// @see <a href="https://wikipedia.org/wiki/Bishop_%28chess%29">Bishop on Wikipedia</a>
public non-sealed class Bishop extends Piece {
/// Creates a new bishop.
@JsonCreator
public Bishop(@JsonProperty("color") PieceColor color) {
super(color, "B");
}
@Override
public Collection<? extends Collection<ConditionalMovement>> getPotentialMoves(Position fromPos) {
Collection<ConditionalMovement> upRight = new LinkedList<>(),
upLeft = new LinkedList<>(),
downRight = new LinkedList<>(),
downLeft = new LinkedList<>();
for (int i = 1; i < 8; i++) {
ChessUtil.addIfNotNull(upLeft, upRight(fromPos, i));
ChessUtil.addIfNotNull(upRight, upLeft(fromPos, i));
ChessUtil.addIfNotNull(downRight, downRight(fromPos, i));
ChessUtil.addIfNotNull(downLeft, downLeft(fromPos, i));
}
return List.of(upRight, upLeft, downLeft, downRight);
}
}
@@ -0,0 +1,116 @@
package ru.fokinatorr.chess.impl.piece;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import ru.fokinatorr.chess.impl.Position;
import java.util.Objects;
/// The conditional potentially possible movement of a piece. Can only be applied if the move condition matches.
/// @param pos the position to move to
/// @param moveCondition The condition when the move can only be performed.
public record ConditionalMovement(Position pos, MoveCondition moveCondition) {
/// Creates an instance of `ConditionalMovement`.
/// @throws NullPointerException if any argument is `null`
public ConditionalMovement {
Objects.requireNonNull(pos, "pos");
Objects.requireNonNull(moveCondition, "move condition");
}
/// Creates an instance of `ConditionalMovement` with {@link MoveCondition#ANY ANY} condition.
/// @throws NullPointerException if any argument is `null`
public ConditionalMovement(Position pos) {
this(pos, MoveCondition.ANY);
}
/// Convenience method that modifies the condition of this `ConditionalMovement`.
/// @param condition the new condition
/// @throws NullPointerException if `condition` is `null`
public ConditionalMovement withCondition(MoveCondition condition) {
return new ConditionalMovement(pos, condition);
}
// Relative movement methods
public static ConditionalMovement up(Position fromPos, int times) {
Position pos = fromPos.up(times);
return pos == null ? null : new ConditionalMovement(pos);
}
public static ConditionalMovement down(Position fromPos, int times) {
Position pos = fromPos.down(times);
return pos == null ? null : new ConditionalMovement(pos);
}
public static ConditionalMovement left(Position fromPos, int times) {
Position pos = fromPos.left(times);
return pos == null ? null : new ConditionalMovement(pos);
}
public static ConditionalMovement right(Position fromPos, int times) {
Position pos = fromPos.right(times);
return pos == null ? null : new ConditionalMovement(pos);
}
public static ConditionalMovement upRight(Position fromPos, int times) {
Position pos = fromPos.upRight(times);
return pos == null ? null : new ConditionalMovement(pos);
}
public static ConditionalMovement upLeft(Position fromPos, int times) {
Position pos = fromPos.upLeft(times);
return pos == null ? null : new ConditionalMovement(pos);
}
public static ConditionalMovement downRight(Position fromPos, int times) {
Position pos = fromPos.downRight(times);
return pos == null ? null : new ConditionalMovement(pos);
}
public static ConditionalMovement downLeft(Position fromPos, int times) {
Position pos = fromPos.downLeft(times);
return pos == null ? null : new ConditionalMovement(pos);
}
public static ConditionalMovement relative(Position fromPos, int up, int right) {
Position pos = fromPos.move(up, right);
return pos == null ? null : new ConditionalMovement(pos);
}
/// The condition that must match for a piece to move.
@RequiredArgsConstructor
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public enum MoveCondition {
/// The position is accessible in any case except when there's a piece of the same color.
ANY(true, true),
/// The position is accessible only if it's empty.
MOVE_ONLY(true, false),
/// The position is accessible only if there's a piece of the opposite color.
MOVE_AND_CAPTURE_ONLY(false, true),
/// Exclusive to king.
/// <p>
/// The move can only be done if all the following conditions are met:
/// <ul>
/// <li>the king, or any field he's going past, is not attacked;</li>
/// <li>both the king and a rook which is going to take part in the castling had never moved before;</li>
/// <li>there are no pieces between the king and the rook.</li>
/// </ul>
CASTLING(true, false);
boolean canMove;
boolean canCapture;
/// Determines whether the position is accessible if it is empty.
public boolean canMove() {
return canMove;
}
/// Determines whether the position is accessible if it has a piece of the opposite type on it.
public boolean canCapture() {
return canCapture;
}
}
}
@@ -0,0 +1,39 @@
package ru.fokinatorr.chess.impl.piece;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import ru.fokinatorr.chess.impl.ChessUtil;
import ru.fokinatorr.chess.impl.PieceColor;
import ru.fokinatorr.chess.impl.Position;
import java.util.*;
import static ru.fokinatorr.chess.impl.piece.ConditionalMovement.*;
import static ru.fokinatorr.chess.impl.piece.ConditionalMovement.MoveCondition.CASTLING;
/// @see <a href="https://wikipedia.org/wiki/King_%28chess%29">King on Wikipedia</a>
public non-sealed class King extends Piece {
/// Creates a new king.
@JsonCreator
public King(@JsonProperty("color") PieceColor color) {
super(color, "K");
}
@Override
public Collection<? extends Collection<ConditionalMovement>> getPotentialMoves(Position fromPos) {
Collection<Collection<ConditionalMovement>> result = new LinkedList<>();
ChessUtil.addSingletonListIfNotNull(result, up(fromPos, 1));
ChessUtil.addSingletonListIfNotNull(result, upRight(fromPos, 1));
ChessUtil.addSingletonListIfNotNull(result, right(fromPos, 1));
ChessUtil.addSingletonListIfNotNull(result, downRight(fromPos, 1));
ChessUtil.addSingletonListIfNotNull(result, down(fromPos, 1));
ChessUtil.addSingletonListIfNotNull(result, downLeft(fromPos, 1));
ChessUtil.addSingletonListIfNotNull(result, left(fromPos, 1));
ChessUtil.addSingletonListIfNotNull(result, upLeft(fromPos, 1));
if (isNeverMoved()) {
ChessUtil.addIfNotNull(result, left(fromPos, 2), m -> Set.of(m.withCondition(CASTLING)));
ChessUtil.addIfNotNull(result, right(fromPos, 2), m -> Set.of(m.withCondition(CASTLING)));
}
return List.copyOf(result);
}
}
@@ -0,0 +1,35 @@
package ru.fokinatorr.chess.impl.piece;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import ru.fokinatorr.chess.impl.ChessUtil;
import ru.fokinatorr.chess.impl.PieceColor;
import ru.fokinatorr.chess.impl.Position;
import java.util.*;
import static ru.fokinatorr.chess.impl.piece.ConditionalMovement.relative;
/// @see <a href="https://wikipedia.org/wiki/Knight_%28chess%29">Knight on Wikipedia</a>
public non-sealed class Knight extends Piece {
/// Creates a new knight.
@JsonCreator
public Knight(@JsonProperty("color") PieceColor color) {
super(color, "N");
}
///{@inheritDoc}
@Override
public Collection<? extends Collection<ConditionalMovement>> getPotentialMoves(Position fromPos) {
Collection<Collection<ConditionalMovement>> result = new LinkedList<>();
ChessUtil.addSingletonListIfNotNull(result, relative(fromPos, 2, 1 ));
ChessUtil.addSingletonListIfNotNull(result, relative(fromPos, 2, -1 ));
ChessUtil.addSingletonListIfNotNull(result, relative(fromPos, -2, -1));
ChessUtil.addSingletonListIfNotNull(result, relative(fromPos, -2, 1 ));
ChessUtil.addSingletonListIfNotNull(result, relative(fromPos, 1, 2 ));
ChessUtil.addSingletonListIfNotNull(result, relative(fromPos, 1, -2 ));
ChessUtil.addSingletonListIfNotNull(result, relative(fromPos, -1, -2));
ChessUtil.addSingletonListIfNotNull(result, relative(fromPos, -1, 2 ));
return List.copyOf(result);
}
}
@@ -0,0 +1,58 @@
package ru.fokinatorr.chess.impl.piece;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import ru.fokinatorr.chess.impl.ChessUtil;
import ru.fokinatorr.chess.impl.PieceColor;
import ru.fokinatorr.chess.impl.Position;
import java.util.*;
import static ru.fokinatorr.chess.impl.piece.ConditionalMovement.MoveCondition.MOVE_AND_CAPTURE_ONLY;
import static ru.fokinatorr.chess.impl.piece.ConditionalMovement.MoveCondition.MOVE_ONLY;
import static ru.fokinatorr.chess.impl.piece.ConditionalMovement.relative;
import static ru.fokinatorr.chess.impl.piece.ConditionalMovement.up;
/// @see <a href="https://wikipedia.org/wiki/Pawn_%28chess%29">Pawn on Wikipedia</a>
public non-sealed class Pawn extends Piece {
/// Creates a new pawn.
@JsonCreator
public Pawn(@JsonProperty("color") PieceColor color) {
super(color, "");
}
///{@inheritDoc}
@Override
public Collection<? extends Collection<ConditionalMovement>> getPotentialMoves(Position fromPos) {
Collection<Collection<ConditionalMovement>> result = new LinkedList<>();
Collection<ConditionalMovement> forwardChain = new LinkedList<>();
ChessUtil.addIfNotNull(forwardChain, up(fromPos, getForwardMovementFactor()), m -> m.withCondition(MOVE_ONLY));
ChessUtil.addIfNotNull(result, relative(fromPos, getForwardMovementFactor(),1), m -> List.of(m.withCondition(MOVE_AND_CAPTURE_ONLY)));
ChessUtil.addIfNotNull(result, relative(fromPos, getForwardMovementFactor(), -1), m -> List.of(m.withCondition(MOVE_AND_CAPTURE_ONLY)));
if (isNeverMoved()) {
ChessUtil.addIfNotNull(forwardChain, up(fromPos, 2 * getForwardMovementFactor()), m -> m.withCondition(MOVE_ONLY));
}
result.add(List.copyOf(forwardChain));
return List.copyOf(result);
}
// This is required as a pawn's direction of moving depends on their color
@JsonIgnore
public int getForwardMovementFactor() {
return color == PieceColor.WHITE ? 1 : -1;
}
/// Returns the row on which the `en passant` can be performed by this pawn.
@JsonIgnore
public Position.Row getEnPassantRow() {
return color == PieceColor.WHITE ? Position.Row.FIVE : Position.Row.FOUR;
}
/// Returns the row on which the `promotion` of this pawn can be performed.
@JsonIgnore
public Position.Row getPromotionRow() {
return color == PieceColor.WHITE ? Position.Row.EIGHT : Position.Row.ONE;
}
}
@@ -0,0 +1,60 @@
package ru.fokinatorr.chess.impl.piece;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import lombok.Data;
import lombok.EqualsAndHashCode;
import ru.fokinatorr.chess.impl.PieceColor;
import ru.fokinatorr.chess.impl.Position;
import java.util.Collection;
/// Common stuff for all chess pieces.
@Data
@JsonTypeInfo(
use = JsonTypeInfo.Id.SIMPLE_NAME,
property = "type"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = Bishop.class, name = "Bishop"),
@JsonSubTypes.Type(value = King.class, name = "King"),
@JsonSubTypes.Type(value = Knight.class, name = "Knight"),
@JsonSubTypes.Type(value = Pawn.class, name = "Pawn"),
@JsonSubTypes.Type(value = Queen.class, name = "Queen"),
@JsonSubTypes.Type(value = Rook.class, name = "Rook"),
})
public abstract sealed class Piece implements java.io.Serializable, Cloneable permits Bishop, King, Knight, Pawn, Queen, Rook {
@java.io.Serial
private static final long serialVersionUID = -8808296847757371338L;
/// This piece's color.
protected final PieceColor color;
/// The piece's chess notation representation.
@JsonIgnore
protected final String notationRepresentation;
/// Whether this piece has never moved before. Needed for pawns, kings and rooks.
@EqualsAndHashCode.Exclude
private boolean neverMoved = true;
/// Returns the potential doneMoves of this piece, i.e. where it can move on an empty chess board.
@JsonIgnore
public abstract Collection<? extends Collection<ConditionalMovement>> getPotentialMoves(Position fromPos);
/// {@inheritDoc}
@Override
public String toString() {
return getClass().getSimpleName() + " (" + notationRepresentation + ") (color=" + color + ")";
}
@Override
public Piece clone() {
try {
// all the state of Piece is immutable so leave it like this
return (Piece) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
@@ -0,0 +1,43 @@
package ru.fokinatorr.chess.impl.piece;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import ru.fokinatorr.chess.impl.ChessUtil;
import ru.fokinatorr.chess.impl.PieceColor;
import ru.fokinatorr.chess.impl.Position;
import java.util.*;
import static ru.fokinatorr.chess.impl.piece.ConditionalMovement.*;
/// @see <a href="https://wikipedia.org/wiki/Queen_%28chess%29">Queen on Wikipedia</a>
public non-sealed class Queen extends Piece {
/// Creates a new queen.
@JsonCreator
public Queen(@JsonProperty("color") PieceColor color) {
super(color, "Q");
}
@Override
public Collection<? extends Collection<ConditionalMovement>> getPotentialMoves(Position fromPos) {
Collection<ConditionalMovement> up = new LinkedList<>(),
upRight = new LinkedList<>(),
right = new LinkedList<>(),
downRight = new LinkedList<>(),
down = new LinkedList<>(),
downLeft = new LinkedList<>(),
left = new LinkedList<>(),
upLeft = new LinkedList<>();
for (int i = 1; i < 8; i++) {
ChessUtil.addIfNotNull(up, up(fromPos, i));
ChessUtil.addIfNotNull(upRight, upRight(fromPos, i));
ChessUtil.addIfNotNull(right, right(fromPos, i));
ChessUtil.addIfNotNull(downRight, downRight(fromPos, i));
ChessUtil.addIfNotNull(down, down(fromPos, i));
ChessUtil.addIfNotNull(downLeft, downLeft(fromPos, i));
ChessUtil.addIfNotNull(left, left(fromPos, i));
ChessUtil.addIfNotNull(upLeft, upLeft(fromPos, i));
}
return List.of(up, upRight, right, downRight, down, downLeft, left, upLeft);
}
}
@@ -0,0 +1,35 @@
package ru.fokinatorr.chess.impl.piece;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import ru.fokinatorr.chess.impl.ChessUtil;
import ru.fokinatorr.chess.impl.PieceColor;
import ru.fokinatorr.chess.impl.Position;
import java.util.*;
import static ru.fokinatorr.chess.impl.piece.ConditionalMovement.*;
/// @see <a href="https://wikipedia.org/wiki/Rook_%28chess%29">Rook on Wikipedia</a>
public non-sealed class Rook extends Piece {
/// Creates a new rook.
@JsonCreator
public Rook(@JsonProperty("color") PieceColor color) {
super(color, "R");
}
@Override
public Collection<? extends Collection<ConditionalMovement>> getPotentialMoves(Position fromPos) {
Collection<ConditionalMovement> up = new LinkedList<>(),
down = new LinkedList<>(),
left = new LinkedList<>(),
right = new LinkedList<>();
for (int i = 1; i < 8; i++) {
ChessUtil.addIfNotNull(up, up(fromPos, i));
ChessUtil.addIfNotNull(down, down(fromPos, i));
ChessUtil.addIfNotNull(left, left(fromPos, i));
ChessUtil.addIfNotNull(right, right(fromPos, i));
}
return List.of(up, down, left, right);
}
}
@@ -0,0 +1,8 @@
package ru.fokinatorr.chess.misc;
import lombok.Builder;
/// A pair of two objects.
@Builder(toBuilder = true)
public record Pair<A, B>(A first, B second) {
}
@@ -0,0 +1,9 @@
package ru.fokinatorr.chess.server;
import lombok.Builder;
import lombok.NonNull;
/// Data-transfer object representing a registered `User`.
@Builder(toBuilder = true)
public record UserDTO(long id, @NonNull String username) {
}
@@ -0,0 +1,10 @@
package ru.fokinatorr.chess.server.exception;
import java.util.UUID;
/// Thrown to indicate that a user with the given ID can not accept or refuse draw in game with the given ID. This means that a draw WAS requested by someone in that game.
public class CannotAcceptOrRefuseDrawException extends GameAccessDeniedException {
public CannotAcceptOrRefuseDrawException(long userId, UUID gameId) {
super(userId, gameId);
}
}
@@ -0,0 +1,20 @@
package ru.fokinatorr.chess.server.exception;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
import java.util.UUID;
/// Exception thrown to indicate that draw had already been offered in the game with the given ID.
@RequiredArgsConstructor
@ResponseStatus(HttpStatus.FORBIDDEN)
public class DrawAlreadyOfferedException extends RuntimeException {
private final UUID gameId;
@Override
public String getMessage() {
return "In game with ID: " + gameId;
}
}
@@ -0,0 +1,20 @@
package ru.fokinatorr.chess.server.exception;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
import java.util.UUID;
/// Exception thrown to indicate that draw wasn't offered in the game with the given ID.
@RequiredArgsConstructor
@ResponseStatus(HttpStatus.FORBIDDEN)
public class DrawNotOfferedException extends RuntimeException {
private final UUID gameId;
@Override
public String getMessage() {
return "In game with ID: " + gameId;
}
}
@@ -0,0 +1,22 @@
package ru.fokinatorr.chess.server.exception;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import java.util.UUID;
/// Thrown to indicate that a user with the given ID can not access a game with the given ID for whatever reason.
@Getter
@RequiredArgsConstructor
@InheritedResponseStatus(HttpStatus.FORBIDDEN)
public abstract class GameAccessDeniedException extends RuntimeException {
private final long userId;
private final UUID gameId;
@Override
public String getMessage() {
return "For user with ID: " + userId + " on game with ID: " + gameId;
}
}
@@ -0,0 +1,19 @@
package ru.fokinatorr.chess.server.exception;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.UUID;
/// Special exception emitted by game join event `Mono` when a game gets cancelled.
@Getter
@RequiredArgsConstructor
public class GameCancelledException extends RuntimeException {
private final UUID gameId;
@Override
public String getMessage() {
return "With ID: " + gameId;
}
}
@@ -0,0 +1,22 @@
package ru.fokinatorr.chess.server.exception;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
import java.util.UUID;
/// Exception thrown to indicate that a game with the given ID was not found.
@Getter
@RequiredArgsConstructor
@ResponseStatus(HttpStatus.NOT_FOUND)
public class GameNotFoundException extends RuntimeException {
private final UUID id;
@Override
public String getMessage() {
return "With ID: " + id;
}
}
@@ -0,0 +1,30 @@
package ru.fokinatorr.chess.server.exception;
import org.springframework.core.annotation.AliasFor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
import java.lang.annotation.*;
/// An inherited variant of [ResponseStatus].
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ResponseStatus
@Inherited
public @interface InheritedResponseStatus {
/// Alias for [ResponseStatus#value()].
@AliasFor(annotation = ResponseStatus.class)
HttpStatus value() default HttpStatus.INTERNAL_SERVER_ERROR;
/// Alias for [ResponseStatus#code()].
@AliasFor(annotation = ResponseStatus.class)
HttpStatus code() default HttpStatus.INTERNAL_SERVER_ERROR;
/// Alias for [ResponseStatus#reason()()].
@AliasFor(annotation = ResponseStatus.class)
String reason() default "";
}
@@ -0,0 +1,22 @@
package ru.fokinatorr.chess.server.exception;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
import ru.fokinatorr.chess.server.game.ChessBoardAndMoveStackDTO;
/// Thrown to indicate that some move is invalid.
@Getter
@RequiredArgsConstructor
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class InvalidMoveException extends RuntimeException {
private final String uciMove;
private final ChessBoardAndMoveStackDTO chessBoardAndMoveStack;
@Override
public String getMessage() {
return "Move " + uciMove + " on " + chessBoardAndMoveStack;
}
}
@@ -0,0 +1,16 @@
package ru.fokinatorr.chess.server.exception;
import java.util.UUID;
/// Thrown to indicate that a user with the given ID can not access the game because someone had already done a move.
public class MoveAlreadyDoneException extends GameAccessDeniedException {
public MoveAlreadyDoneException(UUID gameId) {
super(-1, gameId);
}
@Override
public String getMessage() {
return "In game with ID: " + getGameId();
}
}
@@ -0,0 +1,11 @@
package ru.fokinatorr.chess.server.exception;
import java.util.UUID;
/// Thrown to indicate that a user with the given ID can access the game, but it's not their turn to move.
public class MoveOrderException extends GameAccessDeniedException {
public MoveOrderException(long userId, UUID gameId) {
super(userId, gameId);
}
}
@@ -0,0 +1,11 @@
package ru.fokinatorr.chess.server.exception;
import java.util.UUID;
/// Thrown to indicate that a user with the given ID can not access the game because they aren't its participant.
public class NotParticipantException extends GameAccessDeniedException {
public NotParticipantException(long userId, UUID gameId) {
super(userId, gameId);
}
}
@@ -0,0 +1,20 @@
package ru.fokinatorr.chess.server.exception;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
/// Thrown to indicate that a user with the given ID does not exist.
@Getter
@RequiredArgsConstructor
@ResponseStatus(HttpStatus.NOT_FOUND)
public class UserNotFoundException extends RuntimeException {
private final long userId;
@Override
public String getMessage() {
return "With ID: " + userId;
}
}
@@ -0,0 +1,41 @@
package ru.fokinatorr.chess.server.game;
import lombok.Builder;
import lombok.NonNull;
import ru.fokinatorr.chess.impl.ChessBoard;
import ru.fokinatorr.chess.impl.PieceColor;
import java.util.Arrays;
import java.util.Objects;
/// A data-transfer object representing a chess board and move stack.
@Builder(toBuilder = true)
public record ChessBoardAndMoveStackDTO(@NonNull ChessBoard initialBoard,
@NonNull String[] doneMoves,
@NonNull String[] undoneMoves,
@NonNull PieceColor currentlyMoving) {
@Override
@NonNull
public String toString() {
return "ChessBoardAndMoveStackDTO{" +
"initialBoard=" + (initialBoard.equals(ChessBoard.startPosition()) ? "ChessBoard.startPosition()" : initialBoard) +
", doneMoves=" + Arrays.toString(doneMoves) +
", undoneMoves=" + Arrays.toString(undoneMoves) +
", currentlyMoving=" + currentlyMoving +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ChessBoardAndMoveStackDTO that)) return false;
return Objects.deepEquals(doneMoves, that.doneMoves) && Objects.deepEquals(undoneMoves, that.undoneMoves) && Objects.equals(initialBoard, that.initialBoard) && currentlyMoving == that.currentlyMoving;
}
@Override
public int hashCode() {
return Objects.hash(initialBoard, Arrays.hashCode(doneMoves), Arrays.hashCode(undoneMoves), currentlyMoving);
}
}
@@ -0,0 +1,10 @@
package ru.fokinatorr.chess.server.game;
import lombok.Builder;
import lombok.NonNull;
import ru.fokinatorr.chess.impl.PieceColorOrRandom;
/// A list of fields accepted in `POST` to `/game/create`.
@Builder(toBuilder = true)
public record CreateGameOptions(@NonNull PieceColorOrRandom playAs) {
}
@@ -0,0 +1,19 @@
package ru.fokinatorr.chess.server.game;
import lombok.Builder;
import lombok.NonNull;
import ru.fokinatorr.chess.impl.PieceColor;
import ru.fokinatorr.chess.server.UserDTO;
import ru.fokinatorr.chess.server.game.event.GameFinishedEvent;
import java.util.UUID;
/// Data-transfer object representing a finished chess game.
@Builder(toBuilder = true)
public record FinishedGameDTO(@NonNull UUID id,
@NonNull UserDTO white,
@NonNull UserDTO black,
@NonNull ChessBoardAndMoveStackDTO chessBoardAndMoveStack,
@NonNull PieceColor winner,
@NonNull GameFinishedEvent.GameFinishCause finishCause) {
}
@@ -0,0 +1,15 @@
package ru.fokinatorr.chess.server.game;
import lombok.Builder;
import lombok.NonNull;
import ru.fokinatorr.chess.server.UserDTO;
import java.util.UUID;
/// Data-transfer object representing an ongoing chess game.
@Builder(toBuilder = true)
public record OngoingGameDTO(@NonNull UUID id,
@NonNull UserDTO white,
@NonNull UserDTO black,
@NonNull ChessBoardAndMoveStackDTO chessBoardAndMoveStack) {
}
@@ -0,0 +1,15 @@
package ru.fokinatorr.chess.server.game;
import lombok.Builder;
import lombok.NonNull;
import ru.fokinatorr.chess.impl.PieceColorOrRandom;
import ru.fokinatorr.chess.server.UserDTO;
import java.util.UUID;
/// Data-transfer object representing a pending chess game.
@Builder(toBuilder = true)
public record PendingGameDTO(@NonNull UUID id,
@NonNull UserDTO withUser,
@NonNull PieceColorOrRandom ownerPlaysAs) {
}
@@ -0,0 +1,8 @@
package ru.fokinatorr.chess.server.game.event;
import ru.fokinatorr.chess.impl.PieceColor;
/// The user playing as the specified color offered a draw.
/// @param byColor the color that offered a draw
public record DrawOfferedEvent(PieceColor byColor) implements GameEvent {
}
@@ -0,0 +1,8 @@
package ru.fokinatorr.chess.server.game.event;
import ru.fokinatorr.chess.impl.PieceColor;
/// The user playing as the specified color refused draw.
/// @param byColor the color that refused draw
public record DrawRefusedEvent(PieceColor byColor) implements GameEvent {
}
@@ -0,0 +1,29 @@
package ru.fokinatorr.chess.server.game.event;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
/// An event broadcasted by a server to clients so that they could sync their state with server's.
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
property = "type"
)
@JsonSubTypes({
@Type(value = GameFinishedEvent.class, name = "game_finished"),
@Type(value = MovePerformedEvent.class, name = "move_performed"),
@Type(value = DrawOfferedEvent.class, name = "draw_offered"),
@Type(value = DrawRefusedEvent.class, name = "draw_refused"),
@Type(value = GameJoinedEvent.class, name = "game_joined"),
@Type(value = MoveUndoneEvent.class, name = "move_undone"),
@Type(value = MoveRedoneEvent.class, name = "move_redone"),
})
public interface GameEvent {
/// Indicates whether this event is terminal, i.e. if the client should expect the `onComplete` signal after it.
@JsonIgnore
default boolean isTerminal() {
return false;
}
}
@@ -0,0 +1,30 @@
package ru.fokinatorr.chess.server.game.event;
import ru.fokinatorr.chess.impl.PieceColor;
import ru.fokinatorr.chess.server.game.ChessBoardAndMoveStackDTO;
/// An event indicating that the game has finished for some reason. This is the only terminal event yet.
/// @param winner the pieces' color that won, or `null` if none
/// @param cause the cause of the game end
/// @param chessBoardAndMoveStack the fallback chess board
public record GameFinishedEvent(PieceColor winner,
GameFinishCause cause,
ChessBoardAndMoveStackDTO chessBoardAndMoveStack) implements GameEvent {
/// What caused the game to finish.
public enum GameFinishCause {
CANCELLED,
DRAW_ACCEPTED,
GIVE_UP,
TIME_OVERSTAYED, // currently not in use (NOTE: time overstayed means checkmate)
CHECKMATE,
STALEMATE,
OTHER // currently not in use
}
/// {@inheritDoc}
@Override
public boolean isTerminal() {
return true;
}
}
@@ -0,0 +1,15 @@
package ru.fokinatorr.chess.server.game.event;
import lombok.NonNull;
import ru.fokinatorr.chess.impl.ChessBoard;
import ru.fokinatorr.chess.impl.PieceColor;
import ru.fokinatorr.chess.server.UserDTO;
/// An event indicating that someone joined the game.
/// @param startPosition the start game position
/// @param playingAs the color that the owner plays as
/// @param playingWith the user that the owner plays with
public record GameJoinedEvent(@NonNull ChessBoard startPosition,
@NonNull PieceColor playingAs,
@NonNull UserDTO playingWith) implements GameEvent {
}
@@ -0,0 +1,9 @@
package ru.fokinatorr.chess.server.game.event;
import ru.fokinatorr.chess.server.game.ChessBoardAndMoveStackDTO;
/// Some player performed a move.
/// @param move the move, in UCI format
/// @param chessBoardAndMoveStack the fallback chess board, if performing the move on client-side fails
public record MovePerformedEvent(String move, ChessBoardAndMoveStackDTO chessBoardAndMoveStack) implements GameEvent {
}
@@ -0,0 +1,8 @@
package ru.fokinatorr.chess.server.game.event;
import ru.fokinatorr.chess.server.game.ChessBoardAndMoveStackDTO;
/// A move was redone. Only supported in local/offline games.
/// @param chessBoardAndMoveStack the fallback chess board
public record MoveRedoneEvent(ChessBoardAndMoveStackDTO chessBoardAndMoveStack) implements GameEvent {
}
@@ -0,0 +1,8 @@
package ru.fokinatorr.chess.server.game.event;
import ru.fokinatorr.chess.server.game.ChessBoardAndMoveStackDTO;
/// A move was undone. Only supported in local/offline games.
/// @param chessBoardAndMoveStack the fallback chess board
public record MoveUndoneEvent(ChessBoardAndMoveStackDTO chessBoardAndMoveStack) implements GameEvent {
}
@@ -0,0 +1,2 @@
/// Some POJOs returned by the server.
package ru.fokinatorr.chess.server;
@@ -0,0 +1,14 @@
package ru.fokinatorr.chess.server.security;
import lombok.Builder;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import java.util.Set;
/// A list of fields returned from `GET` request to `/login`.
@Builder(toBuilder = true)
public record LoginRequirements(@NonNull Set<String> requiredFields) {
}
@@ -0,0 +1,5 @@
package ru.fokinatorr.chess.server.security;
/// A list of fields accepted in `POST` to `/register`.
public record RegisterForm(String username, String password, String confirmPassword) {
}
@@ -0,0 +1,42 @@
package ru.fokinatorr.chess.server.service;
import ru.fokinatorr.chess.impl.ChessBoard;
import ru.fokinatorr.chess.impl.ChessUtil;
import ru.fokinatorr.chess.impl.misc.ChessBoardAndMoveStack;
import ru.fokinatorr.chess.impl.movestack.Move;
import ru.fokinatorr.chess.server.game.ChessBoardAndMoveStackDTO;
/// Data-transfer-object related service.
public class DTOService {
/// Creates and returns a DTO for [ChessBoardAndMoveStack].
public ChessBoardAndMoveStackDTO toDTO(ChessBoardAndMoveStack chessBoardAndMoveStack, ChessBoard initialBoard) {
String[] doneMoves = new String[chessBoardAndMoveStack.getMoveStack().getDoneMoves().size()];
String[] undoneMoves = new String[chessBoardAndMoveStack.getMoveStack().getUndoneMoves().size()];
int i = 0;
for (Move move : chessBoardAndMoveStack.getMoveStack().getDoneMoves()) {
doneMoves[i++] = move.toUCI();
}
i = 0;
for (Move move : chessBoardAndMoveStack.getMoveStack().getUndoneMoves()) {
undoneMoves[i++] = move.toUCI();
}
return new ChessBoardAndMoveStackDTO(initialBoard.clone(), doneMoves, undoneMoves, chessBoardAndMoveStack.getChessBoard().getCurrentlyMoving());
}
/// Converts the DTO to a [ChessBoardAndMoveStack].
public ChessBoardAndMoveStack fromDTO(ChessBoardAndMoveStackDTO dto) {
ChessBoardAndMoveStack chessBoardAndMoveStack = new ChessBoardAndMoveStack(dto.initialBoard());
for (String move : dto.doneMoves()) {
chessBoardAndMoveStack.perform(ChessUtil.parseUCIMove(move, chessBoardAndMoveStack));
}
for (int i = dto.undoneMoves().length - 1; i >= 0; i--) {
chessBoardAndMoveStack.perform(ChessUtil.parseUCIMove(dto.undoneMoves()[i], chessBoardAndMoveStack));
}
for (int i = 0; i < dto.undoneMoves().length; i++) {
chessBoardAndMoveStack.undo();
}
chessBoardAndMoveStack.getChessBoard().setCurrentlyMoving(dto.currentlyMoving());
return chessBoardAndMoveStack;
}
}
@@ -0,0 +1,41 @@
package ru.fokinatorr.chess.util;
import java.util.concurrent.Callable;
/// A [Runnable] that may throw an exception [T].
/// @param <T> the type of the exception that may be thrown
public interface ThrowingRunnable<T extends Throwable> extends Runnable {
/// Runs this operation.
/// @throws T the exception thrown by this operation
void throwingRun() throws T;
/// Runs this operation without throwing checked exceptions.
/// By default, any checked exceptions are wrapped into a [RuntimeException]; any unchecked exceptions (subclasses
/// of `RuntimeException` or `Error`) are rethrown as is.
@Override
default void run() {
try {
throwingRun();
} catch (Throwable e) {
if (e instanceof RuntimeException e1) {
throw e1;
} else if (e instanceof Error e1) {
throw e1;
} else {
throw new RuntimeException(e);
}
}
}
/// Converts a `ThrowingRunnable<`[Exception]`>` into a [Callable] that delegates to it and
/// returns `null`.
/// @param throwingRun the runnable
/// @return the input runnable as a callable
static <T> Callable<T> toCallable(ThrowingRunnable<? extends Exception> throwingRun) {
return () -> {
throwingRun.run();
return null;
};
}
}
@@ -0,0 +1,600 @@
package ru.fokinatorr.chess;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import ru.fokinatorr.chess.impl.*;
import ru.fokinatorr.chess.impl.movestack.CastlingMove;
import ru.fokinatorr.chess.impl.movestack.EnPassantMove;
import ru.fokinatorr.chess.impl.movestack.PawnPromotionMove;
import ru.fokinatorr.chess.impl.movestack.SimpleMove;
import ru.fokinatorr.chess.impl.piece.*;
import ru.fokinatorr.chess.impl.misc.ChessBoardAndMoveStack;
import java.util.Collection;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;
import static java.util.Map.entry;
import static org.junit.jupiter.api.Assertions.*;
@Slf4j
class ChessLogicTests {
static final Map<Character, Supplier<Piece>> CHAR_TO_PIECE = Map.ofEntries(
entry('R', () -> new Rook(PieceColor.WHITE)),
entry('N', () -> new Knight(PieceColor.WHITE)),
entry('B', () -> new Bishop(PieceColor.WHITE)),
entry('Q', () -> new Queen(PieceColor.WHITE)),
entry('K', () -> new King(PieceColor.WHITE)),
entry('P', () -> new Pawn(PieceColor.WHITE)),
entry('r', () -> new Rook(PieceColor.BLACK)),
entry('n', () -> new Knight(PieceColor.BLACK)),
entry('b', () -> new Bishop(PieceColor.BLACK)),
entry('q', () -> new Queen(PieceColor.BLACK)),
entry('k', () -> new King(PieceColor.BLACK)),
entry('p', () -> new Pawn(PieceColor.BLACK)),
entry(' ', () -> null)
);
ChessBoardAndMoveStack fromString(String s) {
ChessBoard chessBoard = new ChessBoard();
String[] rows = s.split("\\n");
assertEquals(8, rows.length);
for (int i = 0; i < 8; i++) {
assertEquals(8, rows[i].length());
for (int j = 0; j < 8; j++) {
char c = rows[i].charAt(j);
Position pos = Position.valueOf(j, 7 - i);
Supplier<Piece> sup = CHAR_TO_PIECE.get(c);
if (sup != null) {
chessBoard.setPiece(pos, sup.get());
}
}
}
return new ChessBoardAndMoveStack(chessBoard);
}
@Test
void testPieces() {
ChessBoardAndMoveStack chessBoardAndMoveStack = ChessBoardAndMoveStack.startPosition();
assertEquals(new Pawn(PieceColor.WHITE), chessBoardAndMoveStack.getPiece(Position.valueOf("e2")));
assertEquals(new King(PieceColor.WHITE), chessBoardAndMoveStack.getPiece(Position.valueOf("e1")));
assertEquals(new Knight(PieceColor.BLACK), chessBoardAndMoveStack.getPiece(Position.valueOf("b8")));
assertNull(chessBoardAndMoveStack.getPiece(Position.valueOf("b3")));
}
@Test
void testSimpleMoves() {
ChessBoardAndMoveStack chessBoardAndMoveStack = ChessBoardAndMoveStack.startPosition();
Collection<MoveEntry> e2Moves = chessBoardAndMoveStack.getMoves(Position.valueOf("e2"));
assertEquals(2, e2Moves.size());
assertTrue(e2Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("e2")
&& simpleMove.getNewPos().toString().equals("e4")));
assertTrue(e2Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("e2")
&& simpleMove.getNewPos().toString().equals("e3")));
Collection<MoveEntry> e1Moves = chessBoardAndMoveStack.getMoves(Position.valueOf("e1"));
assertEquals(0, e1Moves.size());
Collection<MoveEntry> h1Moves = chessBoardAndMoveStack.getMoves(Position.valueOf("h1"));
assertEquals(0, h1Moves.size());
Collection<MoveEntry> b8Moves = chessBoardAndMoveStack.getMoves(Position.valueOf("b8"));
assertEquals(2, b8Moves.size());
assertTrue(b8Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("b8")
&& simpleMove.getNewPos().toString().equals("a6")));
assertTrue(b8Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("b8")
&& simpleMove.getNewPos().toString().equals("c6")));
}
@Test
void testCastling() {
ChessBoardAndMoveStack chessBoardAndMoveStack = fromString("""
r k r
\s
\s
\s
\s
\s
\s
R K R""");
Collection<MoveEntry> e1Moves = chessBoardAndMoveStack.getMoves(Position.valueOf("e1"));
assertEquals(7, e1Moves.size());
assertTrue(e1Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("e1")
&& simpleMove.getNewPos().toString().equals("d1")));
assertTrue(e1Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("e1")
&& simpleMove.getNewPos().toString().equals("d2")));
assertTrue(e1Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("e1")
&& simpleMove.getNewPos().toString().equals("e2")));
assertTrue(e1Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("e1")
&& simpleMove.getNewPos().toString().equals("f2")));
assertTrue(e1Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("e1")
&& simpleMove.getNewPos().toString().equals("f1")));
assertTrue(e1Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof CastlingMove castlingMove
&& castlingMove.getKingOldPos().toString().equals("e1")
&& castlingMove.getRookOldPos().toString().equals("a1")
&& !castlingMove.isKingside()));
assertTrue(e1Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof CastlingMove castlingMove
&& castlingMove.getKingOldPos().toString().equals("e1")
&& castlingMove.getRookOldPos().toString().equals("h1")
&& castlingMove.isKingside()));
Collection<MoveEntry> e8Moves = chessBoardAndMoveStack.getMoves(Position.valueOf("e8"));
assertEquals(7, e8Moves.size());
assertTrue(e8Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("e8")
&& simpleMove.getNewPos().toString().equals("d8")));
assertTrue(e8Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("e8")
&& simpleMove.getNewPos().toString().equals("d7")));
assertTrue(e8Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("e8")
&& simpleMove.getNewPos().toString().equals("e7")));
assertTrue(e8Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("e8")
&& simpleMove.getNewPos().toString().equals("f7")));
assertTrue(e8Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("e8")
&& simpleMove.getNewPos().toString().equals("f8")));
assertTrue(e8Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof CastlingMove castlingMove
&& castlingMove.getKingOldPos().toString().equals("e8")
&& castlingMove.getRookOldPos().toString().equals("a8")
&& !castlingMove.isKingside()));
assertTrue(e8Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof CastlingMove castlingMove
&& castlingMove.getKingOldPos().toString().equals("e8")
&& castlingMove.getRookOldPos().toString().equals("h8")
&& castlingMove.isKingside()));
}
@Test
void testCastlingFails1() {
ChessBoardAndMoveStack chessBoardAndMoveStack = fromString("""
\s
\s
\s
b \s
\s
\s
\s
K R"""); // white king is in check
assertTrue(chessBoardAndMoveStack.isCheck(PieceColor.WHITE));
Collection<MoveEntry> e1Moves = chessBoardAndMoveStack.getMoves(Position.valueOf("e1"));
assertEquals(4, e1Moves.size());
assertTrue(e1Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("e1")
&& simpleMove.getNewPos().toString().equals("d1")));
assertTrue(e1Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("e1")
&& simpleMove.getNewPos().toString().equals("e2")));
assertTrue(e1Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("e1")
&& simpleMove.getNewPos().toString().equals("f2")));
assertTrue(e1Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("e1")
&& simpleMove.getNewPos().toString().equals("f1")));
}
@Test
void testCastlingFails2() {
ChessBoardAndMoveStack chessBoardAndMoveStack = fromString("""
r k \s
\s
\s
\s
\s
\s
\s
QK \s"""); // the field that black king jumps over is attacked by queen
assertFalse(chessBoardAndMoveStack.isCheck(PieceColor.WHITE));
assertFalse(chessBoardAndMoveStack.isCheck(PieceColor.BLACK));
Collection<MoveEntry> e8Moves = chessBoardAndMoveStack.getMoves(Position.valueOf("e8"));
assertEquals(3, e8Moves.size());
assertTrue(e8Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("e8")
&& simpleMove.getNewPos().toString().equals("e7")));
assertTrue(e8Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("e8")
&& simpleMove.getNewPos().toString().equals("f7")));
assertTrue(e8Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("e8")
&& simpleMove.getNewPos().toString().equals("f8")));
}
@Test
void testCastlingFails3() {
ChessBoardAndMoveStack chessBoardAndMoveStack = fromString("""
r k \s
\s
\s
\s
\s
q\s
\s
R K \s"""); // the field that black king ends up being in is attacked by white rook; black queen attacks white king
assertTrue(chessBoardAndMoveStack.isCheck(PieceColor.WHITE));
assertFalse(chessBoardAndMoveStack.isCheck(PieceColor.BLACK));
Collection<MoveEntry> e8Moves = chessBoardAndMoveStack.getMoves(Position.valueOf("e8"));
assertEquals(5, e8Moves.size());
assertTrue(e8Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("e8")
&& simpleMove.getNewPos().toString().equals("e7")));
assertTrue(e8Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("e8")
&& simpleMove.getNewPos().toString().equals("f7")));
assertTrue(e8Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("e8")
&& simpleMove.getNewPos().toString().equals("f8")));
assertTrue(e8Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("e8")
&& simpleMove.getNewPos().toString().equals("d7")));
assertTrue(e8Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("e8")
&& simpleMove.getNewPos().toString().equals("d8")));
}
@Test
void testPromotion() {
ChessBoardAndMoveStack chessBoardAndMoveStack = fromString("""
n k \s
PP \s
\s
\s
\s
\s
p \s
B K \s"""); // one white pawn can promote either by going forward or capturing a knight; black pawn can't promote
Function<PieceColor, Piece> promoter = Queen::new; // always promote to a queen
chessBoardAndMoveStack.setPawnPromoter(promoter);
Collection<MoveEntry> b7Moves = chessBoardAndMoveStack.getMoves(Position.valueOf("b7"));
assertEquals(0, b7Moves.size());
Collection<MoveEntry> e2Moves = chessBoardAndMoveStack.getMoves(Position.valueOf("e2"));
assertEquals(0, e2Moves.size());
Collection<MoveEntry> c7Moves = chessBoardAndMoveStack.getMoves(Position.valueOf("c7"));
assertEquals(2, c7Moves.size());
assertTrue(c7Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof PawnPromotionMove pawnPromotionMove
&& pawnPromotionMove.getOldPos().toString().equals("c7")
&& pawnPromotionMove.getNewPos().toString().equals("c8")
&& pawnPromotionMove.getPawnPromoter() == promoter));
assertTrue(c7Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof PawnPromotionMove pawnPromotionMove
&& pawnPromotionMove.getOldPos().toString().equals("c7")
&& pawnPromotionMove.getNewPos().toString().equals("b8")
&& pawnPromotionMove.getPawnPromoter() == promoter));
}
@Test
void testEnPassant() {
ChessBoardAndMoveStack chessBoardAndMoveStack = fromString("""
k \s
pppppppp
\s
K P \s
\s
\s
\s
\s"""); // if c7 or e7 pawn double-doneMoves, it can get captured by d5 pawn en passant
chessBoardAndMoveStack.getPiece(Position.valueOf("d5")).setNeverMoved(false);
Collection<MoveEntry> c7Moves = chessBoardAndMoveStack.getMoves(Position.valueOf("c7"));
assertEquals(2, c7Moves.size());
assertTrue(c7Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("c7")
&& simpleMove.getNewPos().toString().equals("c6")));
assertTrue(c7Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("c7")
&& simpleMove.getNewPos().toString().equals("c5")));
@SuppressWarnings("OptionalGetWithoutIsPresent") // we already found it before
MoveEntry pawn_c5 = c7Moves.stream()
.filter(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("c7")
&& simpleMove.getNewPos().toString().equals("c5"))
.findAny().get();
chessBoardAndMoveStack.perform(pawn_c5.move());
assertTrue(ChessUtil.wasPawnDoubleMove(pawn_c5.move(), Position.valueOf("c5"), PieceColor.BLACK));
Collection<MoveEntry> d5Moves = chessBoardAndMoveStack.getMoves(Position.valueOf("d5"));
assertEquals(2, d5Moves.size());
assertTrue(d5Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("d5")
&& simpleMove.getNewPos().toString().equals("d6")));
assertTrue(d5Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof EnPassantMove enPassantMove
&& enPassantMove.getOldPos().toString().equals("d5")
&& !enPassantMove.isRight()));
}
@Test
void testEnPassantFails() {
ChessBoardAndMoveStack chessBoardAndMoveStack = fromString("""
k \s
pp ppppp
\s
K pP \s
\s
\s
\s
\s"""); // the pawn on c5 e.g. moved from c6 or was already like that for whatever reason
chessBoardAndMoveStack.getPiece(Position.valueOf("d5")).setNeverMoved(false);
chessBoardAndMoveStack.getPiece(Position.valueOf("c5")).setNeverMoved(false);
Collection<MoveEntry> d5Moves = chessBoardAndMoveStack.getMoves(Position.valueOf("d5"));
assertEquals(1, d5Moves.size());
assertTrue(d5Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("d5")
&& simpleMove.getNewPos().toString().equals("d6")));
}
@Test
void testNotCheckmate1() {
ChessBoardAndMoveStack chessBoardAndMoveStack = fromString("""
R k \s
\s
K \s
q \s
\s
\s
\s
\s"""); // the queen on a5 can either take the rook on a8 or block its attack by going to d8
assertFalse(chessBoardAndMoveStack.isCheckmate(PieceColor.BLACK));
assertTrue(chessBoardAndMoveStack.isCheck(PieceColor.BLACK));
Collection<MoveEntry> e8Moves = chessBoardAndMoveStack.getMoves(Position.valueOf("e8"));
assertEquals(0, e8Moves.size());
Collection<MoveEntry> a5Moves = chessBoardAndMoveStack.getMoves(Position.valueOf("a5"));
assertEquals(2, a5Moves.size());
assertTrue(a5Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("a5")
&& simpleMove.getNewPos().toString().equals("a8")));
assertTrue(a5Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("a5")
&& simpleMove.getNewPos().toString().equals("d8")));
}
@Test
void testCheckmate() {
ChessBoardAndMoveStack chessBoardAndMoveStack = fromString("""
k \s
Q \s
K \s
n \s
\s
\s
\s
\s""");
assertTrue(chessBoardAndMoveStack.isCheckmate(PieceColor.BLACK));
Collection<MoveEntry> e8Moves = chessBoardAndMoveStack.getMoves(Position.valueOf("e8"));
assertEquals(0, e8Moves.size());
Collection<MoveEntry> a5Moves = chessBoardAndMoveStack.getMoves(Position.valueOf("a5"));
assertEquals(0, a5Moves.size());
}
@Test
void testNotCheckmate2() {
@SuppressWarnings("EscapedSpace") // required for each line to be 8 chars
ChessBoardAndMoveStack chessBoardAndMoveStack = fromString("""
k \s
Q \s
\s
K \s
\s \s
\s
\s
\s""");
assertFalse(chessBoardAndMoveStack.isCheckmate(PieceColor.BLACK));
assertTrue(chessBoardAndMoveStack.isCheck(PieceColor.BLACK));
Collection<MoveEntry> e8Moves = chessBoardAndMoveStack.getMoves(Position.valueOf("e8"));
assertEquals(1, e8Moves.size());
assertTrue(e8Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("e8")
&& simpleMove.getNewPos().toString().equals("e7")));
}
@Test
void testStalemate() {
@SuppressWarnings("EscapedSpace") // required for each line to be 8 chars
ChessBoardAndMoveStack chessBoardAndMoveStack = fromString("""
k \s
P \s
K \s
\s
\s \s
\s
\s
\s"""); // black king can't move but not in check
chessBoardAndMoveStack.setPawnPromoter(Queen::new);
assertFalse(chessBoardAndMoveStack.isCheck(PieceColor.BLACK));
assertTrue(chessBoardAndMoveStack.isStalemate(PieceColor.BLACK));
}
@Test
void testNotStalemate() {
ChessBoardAndMoveStack chessBoardAndMoveStack = fromString("""
k \s
P \s
K \s
\s
p \s
\s
\s
\s"""); // black king can't move but not in check
chessBoardAndMoveStack.getPiece(Position.valueOf("a4")).setNeverMoved(false);
chessBoardAndMoveStack.setPawnPromoter(Queen::new);
assertFalse(chessBoardAndMoveStack.isCheck(PieceColor.BLACK));
assertFalse(chessBoardAndMoveStack.isStalemate(PieceColor.BLACK));
Collection<MoveEntry> e8Moves = chessBoardAndMoveStack.getMoves(Position.valueOf("e8"));
assertEquals(0, e8Moves.size());
Collection<MoveEntry> a4Moves = chessBoardAndMoveStack.getMoves(Position.valueOf("a4"));
assertEquals(1, a4Moves.size());
assertTrue(a4Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("a4")
&& simpleMove.getNewPos().toString().equals("a3")));
}
@Test
void testAmbiguousMovesNotation() {
ChessBoardAndMoveStack chessBoardAndMoveStack = fromString("""
R \s
\s
K \s
\s
k \s
\s
\s
R R""");
// if one of the rooks doneMoves to a field on the same column, e.g. Ra5, it would be ambiguous.
// It should be R1a5 or R8a5
Collection<MoveEntry> a8Moves = chessBoardAndMoveStack.getMoves(Position.valueOf("a8"));
assertTrue(a8Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("a8")
&& simpleMove.getNewPos().toString().equals("a5")));
@SuppressWarnings("OptionalGetWithoutIsPresent") // we already know it's present
MoveEntry rook_8_a5 = a8Moves.stream().filter(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("a8")
&& simpleMove.getNewPos().toString().equals("a5"))
.findAny().get();
chessBoardAndMoveStack.perform(rook_8_a5.move());
assertEquals("R8a5", rook_8_a5.move().getNotationRepresentation(chessBoardAndMoveStack.getChessBoard(), chessBoardAndMoveStack.getMoveStack()));
chessBoardAndMoveStack.undo();
Collection<MoveEntry> a1Moves = chessBoardAndMoveStack.getMoves(Position.valueOf("a1"));
assertTrue(a1Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("a1")
&& simpleMove.getNewPos().toString().equals("a5")));
@SuppressWarnings("OptionalGetWithoutIsPresent") // we already know it's present
MoveEntry rook_1_a5 = a1Moves.stream().filter(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("a1")
&& simpleMove.getNewPos().toString().equals("a5"))
.findAny().get();
chessBoardAndMoveStack.perform(rook_1_a5.move());
assertEquals("R1a5", rook_1_a5.move().getNotationRepresentation(chessBoardAndMoveStack.getChessBoard(), chessBoardAndMoveStack.getMoveStack()));
chessBoardAndMoveStack.undo();
assertTrue(a1Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("a1")
&& simpleMove.getNewPos().toString().equals("e1")));
@SuppressWarnings("OptionalGetWithoutIsPresent") // we already know it's present
MoveEntry rook_a_e1 = a1Moves.stream().filter(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("a1")
&& simpleMove.getNewPos().toString().equals("e1"))
.findAny().get();
chessBoardAndMoveStack.perform(rook_a_e1.move());
assertEquals("Rae1", rook_a_e1.move().getNotationRepresentation(chessBoardAndMoveStack.getChessBoard(), chessBoardAndMoveStack.getMoveStack()));
chessBoardAndMoveStack.undo();
Collection<MoveEntry> h1Moves = chessBoardAndMoveStack.getMoves(Position.valueOf("h1"));
assertTrue(h1Moves.stream().anyMatch(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("h1")
&& simpleMove.getNewPos().toString().equals("e1")));
@SuppressWarnings("OptionalGetWithoutIsPresent") // we already know it's present
MoveEntry rook_h_e1 = h1Moves.stream().filter(moveEntry -> moveEntry.move() instanceof SimpleMove simpleMove
&& simpleMove.getOldPos().toString().equals("h1")
&& simpleMove.getNewPos().toString().equals("e1"))
.findAny().get();
chessBoardAndMoveStack.perform(rook_h_e1.move());
assertEquals("Rhe1", rook_h_e1.move().getNotationRepresentation(chessBoardAndMoveStack.getChessBoard(), chessBoardAndMoveStack.getMoveStack()));
}
@Test
void testFEN1() {
ChessBoardAndMoveStack chessBoardAndMoveStack = ChessBoardAndMoveStack.startPosition();
assertEquals("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", chessBoardAndMoveStack.toFEN());
chessBoardAndMoveStack.reverseCurrentlyMoving();
assertEquals("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b KQkq - 0 1", chessBoardAndMoveStack.toFEN());
}
@Test
void testFEN2() {
ChessBoardAndMoveStack chessBoardAndMoveStack = fromString("""
r k r
\s
\s
\s
\s
\s
\s
R K R""");
assertEquals("r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1", chessBoardAndMoveStack.toFEN());
chessBoardAndMoveStack.reverseCurrentlyMoving();
assertEquals("r3k2r/8/8/8/8/8/8/R3K2R b KQkq - 0 1", chessBoardAndMoveStack.toFEN());
}
@Test
void testUCIMoveParserOnSimpleMove() {
ChessBoardAndMoveStack chessBoardAndMoveStack = ChessBoardAndMoveStack.startPosition();
SimpleMove simpleMove = assertInstanceOf(SimpleMove.class, ChessUtil.parseUCIMove("e2e4", chessBoardAndMoveStack));
assertEquals(Position.valueOf("e2"), simpleMove.getOldPos());
assertEquals(Position.valueOf("e4"), simpleMove.getNewPos());
}
@Test
void testUCIMoveParserOnCastling() {
ChessBoardAndMoveStack chessBoardAndMoveStack = fromString("""
r k r
\s
\s
\s
\s
\s
\s
R K R""");
CastlingMove castlingMove = assertInstanceOf(CastlingMove.class, ChessUtil.parseUCIMove("e1g1", chessBoardAndMoveStack));
assertEquals(Position.valueOf("e1"), castlingMove.getKingOldPos());
assertEquals(Position.valueOf("h1"), castlingMove.getRookOldPos());
}
@Test
void testUCIMoveParserOnPawnPromotion() {
ChessBoardAndMoveStack chessBoardAndMoveStack = fromString("""
n k \s
PP \s
\s
\s
\s
\s
p \s
B K \s""");
PawnPromotionMove pawnPromotionMove = assertInstanceOf(PawnPromotionMove.class, ChessUtil.parseUCIMove("c7c8q", chessBoardAndMoveStack));
assertEquals(Position.valueOf("c7"), pawnPromotionMove.getOldPos());
assertEquals(Position.valueOf("c8"), pawnPromotionMove.getNewPos());
assertInstanceOf(Queen.class, pawnPromotionMove.getPawnPromoter().apply(PieceColor.WHITE));
pawnPromotionMove = assertInstanceOf(PawnPromotionMove.class, ChessUtil.parseUCIMove("c7b8r", chessBoardAndMoveStack));
assertEquals(Position.valueOf("c7"), pawnPromotionMove.getOldPos());
assertEquals(Position.valueOf("b8"), pawnPromotionMove.getNewPos());
assertInstanceOf(Rook.class, pawnPromotionMove.getPawnPromoter().apply(PieceColor.WHITE));
}
@Test
void testUCIMoveParserOnEnPassant() {
ChessBoardAndMoveStack chessBoardAndMoveStack = fromString("""
k \s
pp ppppp
\s
K pP \s
\s
\s
\s
\s""");
EnPassantMove enPassantMove = assertInstanceOf(EnPassantMove.class, ChessUtil.parseUCIMove("d5c6", chessBoardAndMoveStack));
assertEquals(Position.valueOf("d5"), enPassantMove.getOldPos());
assertFalse(enPassantMove.isRight());
}
}