299 lines
9.0 KiB
Java
299 lines
9.0 KiB
Java
package dev.kske.chess.board;
|
|
|
|
import java.util.Arrays;
|
|
import java.util.Iterator;
|
|
import java.util.Objects;
|
|
|
|
import dev.kske.chess.board.Piece.Color;
|
|
import dev.kske.chess.game.Game;
|
|
|
|
/**
|
|
* Manages the move history of a {@link Game}.<br>
|
|
* <br>
|
|
* Project: <strong>Chess</strong><br>
|
|
* File: <strong>Log.java</strong><br>
|
|
* Created: <strong>09.07.2019</strong><br>
|
|
*
|
|
* @since Chess v0.1-alpha
|
|
* @author Kai S. K. Engelbart
|
|
*/
|
|
public class Log implements Iterable<MoveNode> {
|
|
|
|
private MoveNode root, current;
|
|
|
|
private Color activeColor;
|
|
private boolean[] castlingRights;
|
|
private Position enPassant;
|
|
private int fullmoveNumber, halfmoveClock;
|
|
|
|
/**
|
|
* Creates an instance of {@link Log} in the default state.
|
|
*/
|
|
public Log() { reset(); }
|
|
|
|
/**
|
|
* Creates a (partially deep) copy of another {@link Log} instance which begins
|
|
* with the current {@link MoveNode}.
|
|
*
|
|
* @param other The {@link Log} instance to copy
|
|
* @param copyVariations If set to {@code true}, subsequent variations of the
|
|
* current {@link MoveNode} are copied with the
|
|
* {@link Log}
|
|
*/
|
|
public Log(Log other, boolean copyVariations) {
|
|
enPassant = other.enPassant;
|
|
castlingRights = other.castlingRights.clone();
|
|
activeColor = other.activeColor;
|
|
fullmoveNumber = other.fullmoveNumber;
|
|
halfmoveClock = other.halfmoveClock;
|
|
|
|
// The new root is the current node of the copied instance
|
|
if (!other.isEmpty()) {
|
|
root = new MoveNode(other.root, copyVariations);
|
|
root.setParent(null);
|
|
current = root;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return an iterator over all {@link MoveNode} objects that are either the
|
|
* root node or a first variation of another node, starting from the
|
|
* root node
|
|
*/
|
|
@Override
|
|
public Iterator<MoveNode> iterator() {
|
|
return new Iterator<MoveNode>() {
|
|
|
|
private MoveNode current = root;
|
|
private boolean hasNext = !isEmpty();
|
|
|
|
@Override
|
|
public boolean hasNext() { return hasNext; }
|
|
|
|
@Override
|
|
public MoveNode next() {
|
|
MoveNode result = current;
|
|
if (current.hasVariations()) current = current.getVariations().get(0);
|
|
else hasNext = false;
|
|
return result;
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Adds a move to the move history and adjusts the log to the new position.
|
|
*
|
|
* @param move The move to log
|
|
* @param piece The piece that performed the move
|
|
* @param capturedPiece The piece captured with the move
|
|
*/
|
|
public void add(Move move, Piece piece, Piece capturedPiece) {
|
|
enPassant = piece instanceof Pawn && move.getyDist() == 2 ? new Position(move.getPos().x, move.getPos().y + move.getySign()) : null;
|
|
if (activeColor == Color.BLACK) ++fullmoveNumber;
|
|
if (piece instanceof Pawn || capturedPiece != null) halfmoveClock = 0;
|
|
else++halfmoveClock;
|
|
activeColor = activeColor.opposite();
|
|
|
|
// Disable castling rights if a king or a rook has been moved
|
|
if (piece instanceof King || piece instanceof Rook) disableCastlingRights(piece, move.getPos());
|
|
|
|
final MoveNode leaf = new MoveNode(move, capturedPiece, castlingRights.clone(), enPassant, activeColor, fullmoveNumber, halfmoveClock);
|
|
|
|
if (isEmpty()) {
|
|
root = leaf;
|
|
current = leaf;
|
|
} else {
|
|
current.addVariation(leaf);
|
|
current = leaf;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes the last move from the log and adjusts its state to the previous
|
|
* move.
|
|
*/
|
|
public void removeLast() {
|
|
if (hasParent()) {
|
|
current.getParent().getVariations().remove(current);
|
|
current = current.getParent();
|
|
update();
|
|
} else reset();
|
|
}
|
|
|
|
/**
|
|
* @return {@code true} if the root node exists
|
|
*/
|
|
public boolean isEmpty() { return root == null; }
|
|
|
|
/**
|
|
* @return {@code true} if the current node has a parent node
|
|
*/
|
|
public boolean hasParent() { return !isEmpty() && current.hasParent(); }
|
|
|
|
/**
|
|
* Reverts the log to its initial state corresponding to the default board
|
|
* position.
|
|
*/
|
|
public void reset() {
|
|
root = null;
|
|
current = null;
|
|
castlingRights = new boolean[] { true, true, true, true };
|
|
enPassant = null;
|
|
activeColor = Color.WHITE;
|
|
fullmoveNumber = 1;
|
|
halfmoveClock = 0;
|
|
}
|
|
|
|
/**
|
|
* Changes the current node to one of its children (variations).
|
|
*
|
|
* @param index the index of the variation to select
|
|
*/
|
|
public void selectNextNode(int index) {
|
|
if (!isEmpty() && current.hasVariations() && index < current.getVariations().size()) {
|
|
current = current.getVariations().get(index);
|
|
update();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Selects the parent of the current {@link MoveNode} as the current node.
|
|
*/
|
|
public void selectPreviousNode() {
|
|
if (hasParent()) {
|
|
current = current.getParent();
|
|
update();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Selects the root {@link MoveNode} as the current node.
|
|
*/
|
|
public void selectRootNode() {
|
|
if (!isEmpty()) {
|
|
current = root;
|
|
update();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the active color, castling rights, en passant target square, fullmove
|
|
* number and halfmove clock to those of the current {@link MoveNode}.
|
|
*/
|
|
private void update() {
|
|
activeColor = current.activeColor;
|
|
castlingRights = current.castlingRights.clone();
|
|
enPassant = current.enPassant;
|
|
fullmoveNumber = current.fullmoveCounter;
|
|
halfmoveClock = current.halfmoveClock;
|
|
}
|
|
|
|
/**
|
|
* Removed the castling rights bound to a rook or king for the rest of the game.
|
|
* This method should be called once the piece has been moved, as a castling
|
|
* move involving this piece is forbidden afterwards.
|
|
*
|
|
* @param piece the rook or king to disable the castling rights for
|
|
* @param initialPosition the initial position of the piece during the start of
|
|
* the game
|
|
*/
|
|
private void disableCastlingRights(Piece piece, Position initialPosition) {
|
|
// Kingside
|
|
if (piece instanceof King || piece instanceof Rook && initialPosition.x == 7)
|
|
castlingRights[piece.getColor() == Color.WHITE ? MoveNode.WHITE_KINGSIDE : MoveNode.BLACK_KINGSIDE] = false;
|
|
|
|
// Queenside
|
|
if (piece instanceof King || piece instanceof Rook && initialPosition.x == 0)
|
|
castlingRights[piece.getColor() == Color.WHITE ? MoveNode.WHITE_QUEENSIDE : MoveNode.BLACK_QUEENSIDE] = false;
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
final int prime = 31;
|
|
int result = 1;
|
|
result = prime * result + Arrays.hashCode(castlingRights);
|
|
result = prime * result + Objects.hash(activeColor, current, enPassant, fullmoveNumber, halfmoveClock);
|
|
return result;
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object obj) {
|
|
if (this == obj) return true;
|
|
if (obj == null) return false;
|
|
if (getClass() != obj.getClass()) return false;
|
|
Log other = (Log) obj;
|
|
return activeColor == other.activeColor && Arrays.equals(castlingRights, other.castlingRights) && Objects.equals(current, other.current)
|
|
&& Objects.equals(enPassant, other.enPassant) && fullmoveNumber == other.fullmoveNumber && halfmoveClock == other.halfmoveClock;
|
|
}
|
|
|
|
/**
|
|
* @return The first logged move, or {@code null} if there is none
|
|
*/
|
|
public MoveNode getRoot() { return root; }
|
|
|
|
/**
|
|
* @return the last logged move, or {@code null} if there is none
|
|
*/
|
|
public MoveNode getLast() { return current; }
|
|
|
|
/**
|
|
* @return the castling rights present during the current move
|
|
*/
|
|
public boolean[] getCastlingRights() { return castlingRights; }
|
|
|
|
/**
|
|
* Sets the castling rights present during the current move.
|
|
*
|
|
* @param castlingRights the castling rights to set
|
|
*/
|
|
public void setCastlingRights(boolean[] castlingRights) { this.castlingRights = castlingRights; }
|
|
|
|
/**
|
|
* @return the en passant target position of the current move or {@code null} if
|
|
* the current move is not an en passant move.
|
|
*/
|
|
public Position getEnPassant() { return enPassant; }
|
|
|
|
/**
|
|
* Sets the en passant target position.
|
|
*
|
|
* @param enPassant the en passant target position to set
|
|
*/
|
|
public void setEnPassant(Position enPassant) { this.enPassant = enPassant; }
|
|
|
|
/**
|
|
* @return the color active during the current move
|
|
*/
|
|
public Color getActiveColor() { return activeColor; }
|
|
|
|
/**
|
|
* Sets the color active during the current move.
|
|
*
|
|
* @param activeColor the active color to set
|
|
*/
|
|
public void setActiveColor(Color activeColor) { this.activeColor = activeColor; }
|
|
|
|
/**
|
|
* @return the number of moves made until the current move
|
|
*/
|
|
public int getFullmoveNumber() { return fullmoveNumber; }
|
|
|
|
/**
|
|
* Sets the number of moves made until the current move.
|
|
*
|
|
* @param fullmoveNumber the fullmove number to set
|
|
*/
|
|
public void setFullmoveNumber(int fullmoveNumber) { this.fullmoveNumber = fullmoveNumber; }
|
|
|
|
/**
|
|
* @return the number of halfmoves since the last capture move or pawn move
|
|
*/
|
|
public int getHalfmoveClock() { return halfmoveClock; }
|
|
|
|
/**
|
|
* Sets then number of halfmoves since the last capture move or pawn move
|
|
*
|
|
* @param halfmoveClock the halfmove clock to set
|
|
*/
|
|
public void setHalfmoveClock(int halfmoveClock) { this.halfmoveClock = halfmoveClock; }
|
|
} |