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}.
*
* Project: Chess
* File: Log.java
* Created: 09.07.2019
* * @since Chess v0.1-alpha * @author Kai S. K. Engelbart */ public class Log implements Iterable { 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 iterator() { return new Iterator() { 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; } }