Initial commit
This commit is contained in:
+36
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
lombok.copyableAnnotations += com.fasterxml.jackson.annotation.JsonProperty
|
||||||
@@ -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) {
|
||||||
|
}
|
||||||
+10
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user