Implemented navigation of the move log, added Javadoc #19

Merged
CyB3RC0nN0R merged 6 commits from f/log_navigation into master 2020-07-01 09:51:48 +02:00
56 changed files with 3128 additions and 1351 deletions

View File

@ -1,18 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<classpath> <classpath>
<classpathentry kind="src" path="src"/> <classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="res"/> <classpathentry kind="src" path="res"/>
<classpathentry kind="src" output="bin_test" path="test"> <classpathentry kind="src" output="bin_test" path="test">
<attributes> <attributes>
<attribute name="test" value="true"/> <attribute name="test" value="true"/>
</attributes> </attributes>
</classpathentry> </classpathentry>
<classpathentry kind="src" output="bin_test" path="test_res"> <classpathentry kind="src" output="bin_test" path="test_res">
<attributes> <attributes>
<attribute name="test" value="true"/> <attribute name="test" value="true"/>
</attributes> </attributes>
</classpathentry> </classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/5"/> <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/5"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/> <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
<classpathentry kind="output" path="bin"/> <attributes>
</classpath> <attribute name="module" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="bin"/>
</classpath>

View File

@ -7,12 +7,18 @@ import java.util.List;
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>Bishop.java</strong><br> * File: <strong>Bishop.java</strong><br>
* Created: <strong>01.07.2019</strong><br> * Created: <strong>01.07.2019</strong><br>
* *
* @since Chess v0.1-alpha * @since Chess v0.1-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
public class Bishop extends Piece { public class Bishop extends Piece {
/**
* Creates bishop {@link Piece}.
*
* @param color the color of this bishop
* @param board the board on which this bishop will be placed
*/
public Bishop(Color color, Board board) { public Bishop(Color color, Board board) {
super(color, board); super(color, board);
} }
@ -29,37 +35,54 @@ public class Bishop extends Piece {
// Diagonal moves to the lower right // Diagonal moves to the lower right
for (int i = pos.x + 1, j = pos.y + 1; i < 8 && j < 8; i++, j++) { for (int i = pos.x + 1, j = pos.y + 1; i < 8 && j < 8; i++, j++) {
Move move = new Move(pos, new Position(i, j)); Move move = new Move(pos, new Position(i, j));
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) { if (
board.getDest(move) == null
|| board.getDest(move).getColor() != getColor()
) {
moves.add(move); moves.add(move);
if (board.getDest(move) != null) break; if (board.getDest(move) != null)
} else break; break;
} else
break;
} }
// Diagonal moves to the lower left // Diagonal moves to the lower left
for (int i = pos.x - 1, j = pos.y + 1; i >= 0 && j < 8; i--, j++) { for (int i = pos.x - 1, j = pos.y + 1; i >= 0 && j < 8; i--, j++) {
Move move = new Move(pos, new Position(i, j)); Move move = new Move(pos, new Position(i, j));
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) { if (
board.getDest(move) == null
|| board.getDest(move).getColor() != getColor()
) {
moves.add(move); moves.add(move);
if (board.getDest(move) != null) break; if (board.getDest(move) != null)
} else break; break;
} else
break;
} }
// Diagonal moves to the upper right // Diagonal moves to the upper right
for (int i = pos.x + 1, j = pos.y - 1; i < 8 && j >= 0; i++, j--) { for (int i = pos.x + 1, j = pos.y - 1; i < 8 && j >= 0; i++, j--) {
Move move = new Move(pos, new Position(i, j)); Move move = new Move(pos, new Position(i, j));
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) { if (
board.getDest(move) == null
|| board.getDest(move).getColor() != getColor()
) {
moves.add(move); moves.add(move);
if (board.getDest(move) != null) break; if (board.getDest(move) != null)
} else break; break;
} else
break;
} }
// Diagonal moves to the upper left // Diagonal moves to the upper left
for (int i = pos.x - 1, j = pos.y - 1; i >= 0 && j >= 0; i--, j--) { for (int i = pos.x - 1, j = pos.y - 1; i >= 0 && j >= 0; i--, j--) {
Move move = new Move(pos, new Position(i, j)); Move move = new Move(pos, new Position(i, j));
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) { if (
board.getDest(move) == null
|| board.getDest(move).getColor() != getColor()
) {
moves.add(move); moves.add(move);
if (board.getDest(move) != null) break; if (board.getDest(move) != null)
} else break; break;
} else
break;
} }
return moves; return moves;
} }

View File

@ -1,13 +1,10 @@
package dev.kske.chess.board; package dev.kske.chess.board;
import java.util.ArrayList; import java.util.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import dev.kske.chess.board.Piece.Color; import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.event.EventBus;
import dev.kske.chess.event.MoveEvent;
/** /**
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
@ -19,29 +16,35 @@ import dev.kske.chess.board.Piece.Color;
*/ */
public class Board { public class Board {
private Piece[][] boardArr = new Piece[8][8]; private Piece[][] boardArr = new Piece[8][8];
private Map<Color, Position> kingPos = new HashMap<>(); private Map<Color, Position> kingPos = new EnumMap<>(Color.class);
private Log log = new Log(); private Log log = new Log();
/** /**
* Initializes the board with the default chess starting position. * Initializes the board with the default chess starting position.
*/ */
public Board() { initDefaultPositions(); } public Board() {
initDefaultPositions();
}
/** /**
* Creates a copy of another {@link Board} instance.<br> * Creates a copy of another {@link Board} instance.<br>
* The created object is a deep copy, but does not contain any move history * The created object is a deep copy, and can optionally contain the move
* apart from the current {@link MoveNode}. * history of the Board to copy.
* *
* @param other The {@link Board} instance to copy * @param other The Board instance to copy
* @param copyVariations TODO * @param copyVariations if set to {@code true}, the {@link Log} object of
* the
* other Board instance is copied with its entire move
* history
*/ */
public Board(Board other, boolean copyVariations) { public Board(Board other, boolean copyVariations) {
for (int i = 0; i < 8; i++) for (int i = 0; i < 8; i++)
for (int j = 0; j < 8; j++) { for (int j = 0; j < 8; j++) {
if (other.boardArr[i][j] == null) continue; if (other.boardArr[i][j] == null)
boardArr[i][j] = (Piece) other.boardArr[i][j].clone(); continue;
boardArr[i][j].board = this; boardArr[i][j] = (Piece) other.boardArr[i][j].clone();
boardArr[i][j].board = this;
} }
kingPos.putAll(other.kingPos); kingPos.putAll(other.kingPos);
@ -60,19 +63,18 @@ public class Board {
*/ */
public boolean attemptMove(Move move) { public boolean attemptMove(Move move) {
Piece piece = getPos(move); Piece piece = getPos(move);
if (piece == null || !piece.isValidMove(move)) return false; if (piece == null || !piece.isValidMove(move))
else { return false;
// Move piece
move(move);
// Revert move if it caused a check for its team // Move piece
if (checkCheck(piece.getColor())) { move(move);
revert();
return false;
}
return true; // Revert move if it caused a check for its team
if (checkCheck(piece.getColor())) {
revert();
return false;
} }
return true;
} }
/** /**
@ -81,14 +83,15 @@ public class Board {
* @param move The move to execute * @param move The move to execute
*/ */
public void move(Move move) { public void move(Move move) {
Piece piece = getPos(move); Piece piece = getPos(move);
Piece capturePiece = getDest(move); Piece capturePiece = getDest(move);
// Execute the move // Execute the move
move.execute(this); move.execute(this);
// Update the king's position if the moved piece is the king // Update the king's position if the moved piece is the king
if (piece instanceof King) kingPos.put(piece.getColor(), move.getDest()); if (piece instanceof King)
kingPos.put(piece.getColor(), move.getDest());
// Update log // Update log
log.add(move, piece, capturePiece); log.add(move, piece, capturePiece);
@ -107,19 +110,67 @@ public class Board {
* Reverts the last move and removes it from the log. * Reverts the last move and removes it from the log.
*/ */
public void revert() { public void revert() {
MoveNode moveNode = log.getLast(); MoveNode moveNode = log.getLast();
Move move = moveNode.move; Move move = moveNode.move;
// Revert the move // Revert the move
move.revert(this, moveNode.capturedPiece); move.revert(this, moveNode.capturedPiece);
// Update the king's position if the moved piece is the king // Update the king's position if the moved piece is the king
if (getPos(move) instanceof King) kingPos.put(getPos(move).getColor(), move.getPos()); if (getPos(move) instanceof King)
kingPos.put(getPos(move).getColor(), move.getPos());
// Update log // Update log
log.removeLast(); log.removeLast();
} }
/**
* Reverts the last move without removing it from the log. After that, a
* {@link MoveEvent} is dispatched containing the inverse of the reverted
* move.
*/
public void selectPreviousNode() {
MoveNode moveNode = log.getLast();
Move move = moveNode.move;
// Revert the move
move.revert(this, moveNode.capturedPiece);
// Select previous move node
log.selectPreviousNode();
// Dispatch move event
EventBus.getInstance()
.dispatch(
new MoveEvent(
move.invert(),
getState(log.getActiveColor().opposite())
)
);
}
/**
* Applies the next move stored in the log. After that, a {@link MoveEvent}
* is
* dispatched.
*
* @param index the variation index of the move to select
*/
public void selectNextNode(int index) {
log.selectNextNode(index);
MoveNode moveNode = log.getLast();
Move move = moveNode.move;
// Execute the next move
move.execute(this);
// Dispatch move event
EventBus.getInstance()
.dispatch(
new MoveEvent(move, getState(log.getActiveColor().opposite()))
);
}
/** /**
* Generated every legal move for one color * Generated every legal move for one color
* *
@ -130,11 +181,22 @@ public class Board {
List<Move> moves = new ArrayList<>(); List<Move> moves = new ArrayList<>();
for (int i = 0; i < 8; i++) for (int i = 0; i < 8; i++)
for (int j = 0; j < 8; j++) for (int j = 0; j < 8; j++)
if (boardArr[i][j] != null && boardArr[i][j].getColor() == color) moves.addAll(boardArr[i][j].getMoves(new Position(i, j))); if (
boardArr[i][j] != null && boardArr[i][j].getColor() == color
)
moves.addAll(boardArr[i][j].getMoves(new Position(i, j)));
return moves; return moves;
} }
public List<Move> getMoves(Position pos) { return get(pos).getMoves(pos); } /**
* Delegate method for {@link Piece#getMoves(Position)}.
*
* @param pos the position of the piece to invoke the method on
* @return a list of legal moves generated for the piece
*/
public List<Move> getMoves(Position pos) {
return get(pos).getMoves(pos);
}
/** /**
* Checks, if the king is in check. * Checks, if the king is in check.
@ -142,7 +204,9 @@ public class Board {
* @param color The color of the king to check * @param color The color of the king to check
* @return {@code true}, if the king is in check * @return {@code true}, if the king is in check
*/ */
public boolean checkCheck(Color color) { return isAttacked(kingPos.get(color), color.opposite()); } public boolean checkCheck(Color color) {
return isAttacked(kingPos.get(color), color.opposite());
}
/** /**
* Checks, if a field can be attacked by pieces of a certain color. * Checks, if a field can be attacked by pieces of a certain color.
@ -155,7 +219,11 @@ public class Board {
for (int i = 0; i < 8; i++) for (int i = 0; i < 8; i++)
for (int j = 0; j < 8; j++) { for (int j = 0; j < 8; j++) {
Position pos = new Position(i, j); Position pos = new Position(i, j);
if (get(pos) != null && get(pos).getColor() == color && get(pos).isValidMove(new Move(pos, dest))) return true; if (
get(pos) != null && get(pos).getColor() == color
&& get(pos).isValidMove(new Move(pos, dest))
)
return true;
} }
return false; return false;
} }
@ -169,21 +237,30 @@ public class Board {
*/ */
public boolean checkCheckmate(Color color) { public boolean checkCheckmate(Color color) {
// Return false immediately if the king can move // Return false immediately if the king can move
if (!getMoves(kingPos.get(color)).isEmpty()) return false; if (!getMoves(kingPos.get(color)).isEmpty())
else { return false;
for (Move move : getMoves(color)) {
move(move); for (Move move : getMoves(color)) {
boolean check = checkCheck(color); move(move);
revert(); boolean check = checkCheck(color);
if (!check) return false; revert();
} if (!check)
return true; return false;
} }
return true;
} }
public BoardState getGameEventType(Color color) { /**
return checkCheck(color) ? checkCheckmate(color) ? BoardState.CHECKMATE : BoardState.CHECK * Checks whether the a check, checkmate, stalemate of none of the above is
: getMoves(color).isEmpty() || log.getLast().halfmoveClock >= 50 ? BoardState.STALEMATE : BoardState.NORMAL; * currently present.
*
* @param color the color to evaluate the board for
* @return the current {@link BoardState}
*/
public BoardState getState(Color color) {
return checkCheck(color) ? checkCheckmate(color) ? BoardState.CHECKMATE
: BoardState.CHECK
: getMoves(color).isEmpty() || log.getLast().halfmoveClock >= 50 ? BoardState.STALEMATE : BoardState.NORMAL;
} }
/** /**
@ -192,39 +269,38 @@ public class Board {
public void initDefaultPositions() { public void initDefaultPositions() {
// Initialize pawns // Initialize pawns
for (int i = 0; i < 8; i++) { for (int i = 0; i < 8; i++) {
boardArr[i][1] = new Pawn(Color.BLACK, this); boardArr[i][1] = new Pawn(Color.BLACK, this);
boardArr[i][6] = new Pawn(Color.WHITE, this); boardArr[i][6] = new Pawn(Color.WHITE, this);
} }
// Initialize kings // Initialize kings
boardArr[4][0] = new King(Color.BLACK, this); boardArr[4][0] = new King(Color.BLACK, this);
boardArr[4][7] = new King(Color.WHITE, this); boardArr[4][7] = new King(Color.WHITE, this);
// Initialize king position objects // Initialize king position objects
kingPos.put(Color.BLACK, new Position(4, 0)); kingPos.put(Color.BLACK, new Position(4, 0));
kingPos.put(Color.WHITE, new Position(4, 7)); kingPos.put(Color.WHITE, new Position(4, 7));
// Initialize queens // Initialize queens
boardArr[3][0] = new Queen(Color.BLACK, this); boardArr[3][0] = new Queen(Color.BLACK, this);
boardArr[3][7] = new Queen(Color.WHITE, this); boardArr[3][7] = new Queen(Color.WHITE, this);
// Initialize rooks // Initialize rooks
boardArr[0][0] = new Rook(Color.BLACK, this); boardArr[0][0] = new Rook(Color.BLACK, this);
boardArr[0][7] = new Rook(Color.WHITE, this); boardArr[0][7] = new Rook(Color.WHITE, this);
boardArr[7][0] = new Rook(Color.BLACK, this); boardArr[7][0] = new Rook(Color.BLACK, this);
boardArr[7][7] = new Rook(Color.WHITE, this); boardArr[7][7] = new Rook(Color.WHITE, this);
// Initialize knights // Initialize knights
boardArr[1][0] = new Knight(Color.BLACK, this); boardArr[1][0] = new Knight(Color.BLACK, this);
boardArr[1][7] = new Knight(Color.WHITE, this); boardArr[1][7] = new Knight(Color.WHITE, this);
boardArr[6][0] = new Knight(Color.BLACK, this); boardArr[6][0] = new Knight(Color.BLACK, this);
boardArr[6][7] = new Knight(Color.WHITE, this); boardArr[6][7] = new Knight(Color.WHITE, this);
// Initialize bishops // Initialize bishops
boardArr[2][0] = new Bishop(Color.BLACK, this); boardArr[2][0] = new Bishop(Color.BLACK, this);
boardArr[2][7] = new Bishop(Color.WHITE, this); boardArr[2][7] = new Bishop(Color.WHITE, this);
boardArr[5][0] = new Bishop(Color.BLACK, this); boardArr[5][0] = new Bishop(Color.BLACK, this);
boardArr[5][7] = new Bishop(Color.WHITE, this); boardArr[5][7] = new Bishop(Color.WHITE, this);
// Clear all other tiles // Clear all other tiles
for (int i = 0; i < 8; i++) for (int i = 0; i < 8; i++)
@ -236,27 +312,33 @@ public class Board {
@Override @Override
public int hashCode() { public int hashCode() {
final int prime = 31; final int prime = 31;
int result = 1; int result = 1;
result = prime * result + Arrays.deepHashCode(boardArr); result = prime * result + Arrays.deepHashCode(boardArr);
result = prime * result + Objects.hash(kingPos, log); result = prime * result + Objects.hash(kingPos, log);
return result; return result;
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (this == obj) return true; if (this == obj)
if (obj == null) return false; return true;
if (getClass() != obj.getClass()) return false; if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Board other = (Board) obj; Board other = (Board) obj;
return Arrays.deepEquals(boardArr, other.boardArr) && Objects.equals(kingPos, other.kingPos) && Objects.equals(log, other.log); return Arrays.deepEquals(boardArr, other.boardArr) && Objects
.equals(kingPos, other.kingPos) && Objects.equals(log, other.log);
} }
/** /**
* @param pos The position from which to return a piece * @param pos The position from which to return a piece
* @return The piece at the position * @return The piece at the position
*/ */
public Piece get(Position pos) { return boardArr[pos.x][pos.y]; } public Piece get(Position pos) {
return boardArr[pos.x][pos.y];
}
/** /**
* Searches for a {@link Piece} inside a file (A - H). * Searches for a {@link Piece} inside a file (A - H).
@ -269,7 +351,12 @@ public class Board {
public int get(Class<? extends Piece> pieceClass, char file) { public int get(Class<? extends Piece> pieceClass, char file) {
int x = file - 97; int x = file - 97;
for (int i = 0; i < 8; i++) for (int i = 0; i < 8; i++)
if (boardArr[x][i] != null && boardArr[x][i].getClass() == pieceClass && boardArr[x][i].getColor() == log.getActiveColor()) return 8 - i; if (
boardArr[x][i] != null
&& boardArr[x][i].getClass() == pieceClass
&& boardArr[x][i].getColor() == log.getActiveColor()
)
return 8 - i;
return -1; return -1;
} }
@ -284,7 +371,11 @@ public class Board {
public char get(Class<? extends Piece> pieceClass, int rank) { public char get(Class<? extends Piece> pieceClass, int rank) {
int y = rank - 1; int y = rank - 1;
for (int i = 0; i < 8; i++) for (int i = 0; i < 8; i++)
if (boardArr[i][y] != null && boardArr[i][y].getClass() == pieceClass && boardArr[i][y].getColor() == log.getActiveColor()) if (
boardArr[i][y] != null
&& boardArr[i][y].getClass() == pieceClass
&& boardArr[i][y].getColor() == log.getActiveColor()
)
return (char) (i + 97); return (char) (i + 97);
return '-'; return '-';
} }
@ -294,14 +385,20 @@ public class Board {
* *
* @param pieceClass The class of the piece to search for * @param pieceClass The class of the piece to search for
* @param dest The destination that the piece is required to reach * @param dest The destination that the piece is required to reach
* @return The position of a piece that can move to the specified destination * @return The position of a piece that can move to the specified
* destination
*/ */
public Position get(Class<? extends Piece> pieceClass, Position dest) { public Position get(Class<? extends Piece> pieceClass, Position dest) {
for (int i = 0; i < 8; i++) for (int i = 0; i < 8; i++)
for (int j = 0; j < 8; j++) for (int j = 0; j < 8; j++)
if (boardArr[i][j] != null && boardArr[i][j].getClass() == pieceClass && boardArr[i][j].getColor() == log.getActiveColor()) { if (
boardArr[i][j] != null
&& boardArr[i][j].getClass() == pieceClass
&& boardArr[i][j].getColor() == log.getActiveColor()
) {
Position pos = new Position(i, j); Position pos = new Position(i, j);
if (boardArr[i][j].isValidMove(new Move(pos, dest))) return pos; if (boardArr[i][j].isValidMove(new Move(pos, dest)))
return pos;
} }
return null; return null;
} }
@ -312,19 +409,25 @@ public class Board {
* @param pos The position to place the piece at * @param pos The position to place the piece at
* @param piece The piece to place * @param piece The piece to place
*/ */
public void set(Position pos, Piece piece) { boardArr[pos.x][pos.y] = piece; } public void set(Position pos, Piece piece) {
boardArr[pos.x][pos.y] = piece;
}
/** /**
* @param move The move from which position to return a piece * @param move The move from which position to return a piece
* @return The piece at the position of the move * @return The piece at the position of the move
*/ */
public Piece getPos(Move move) { return get(move.getPos()); } public Piece getPos(Move move) {
return get(move.getPos());
}
/** /**
* @param move The move from which destination to return a piece * @param move The move from which destination to return a piece
* @return The piece at the destination of the move * @return The piece at the destination of the move
*/ */
public Piece getDest(Move move) { return get(move.getDest()); } public Piece getDest(Move move) {
return get(move.getDest());
}
/** /**
* Places a piece at the position of a move. * Places a piece at the position of a move.
@ -332,7 +435,9 @@ public class Board {
* @param move The move at which position to place the piece * @param move The move at which position to place the piece
* @param piece The piece to place * @param piece The piece to place
*/ */
public void setPos(Move move, Piece piece) { set(move.getPos(), piece); } public void setPos(Move move, Piece piece) {
set(move.getPos(), piece);
}
/** /**
* Places a piece at the destination of a move. * Places a piece at the destination of a move.
@ -340,7 +445,9 @@ public class Board {
* @param move The move at which destination to place the piece * @param move The move at which destination to place the piece
* @param piece The piece to place * @param piece The piece to place
*/ */
public void setDest(Move move, Piece piece) { set(move.getDest(), piece); } public void setDest(Move move, Piece piece) {
set(move.getDest(), piece);
}
/** /**
* @return The board array * @return The board array

View File

@ -4,10 +4,11 @@ package dev.kske.chess.board;
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>BoardState.java</strong><br> * File: <strong>BoardState.java</strong><br>
* Created: <strong>07.07.2019</strong><br> * Created: <strong>07.07.2019</strong><br>
* *
* @since Chess v0.1-alpha * @since Chess v0.1-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
@SuppressWarnings("javadoc")
public enum BoardState { public enum BoardState {
CHECK, CHECKMATE, STALEMATE, NORMAL; CHECK, CHECKMATE, STALEMATE, NORMAL;
} }

View File

@ -12,12 +12,29 @@ public class Castling extends Move {
private final Move rookMove; private final Move rookMove;
/**
* Creates a castling move.
*
* @param pos the position of this castling move
* @param dest the destination of this castling move
*/
public Castling(Position pos, Position dest) { public Castling(Position pos, Position dest) {
super(pos, dest); super(pos, dest);
rookMove = dest.x == 6 ? new Move(7, pos.y, 5, pos.y) : new Move(0, pos.y, 3, pos.y); rookMove = dest.x == 6 ? new Move(7, pos.y, 5, pos.y)
: new Move(0, pos.y, 3, pos.y);
} }
public Castling(int xPos, int yPos, int xDest, int yDest) { this(new Position(xPos, yPos), new Position(xDest, yDest)); } /**
* Creates a castling move.
*
* @param xPos the horizontal position of this castling move
* @param yPos the vertical position of this castling move
* @param xDest the horizontal destination of this castling move
* @param yDest the vertical destination of this castling move
*/
public Castling(int xPos, int yPos, int xDest, int yDest) {
this(new Position(xPos, yPos), new Position(xDest, yDest));
}
@Override @Override
public void execute(Board board) { public void execute(Board board) {
@ -34,7 +51,8 @@ public class Castling extends Move {
} }
/** /**
* @return {@code O-O-O} for a queenside castling or {@code O-O} for a kingside * @return {@code O-O-O} for a queenside castling or {@code O-O} for a
* kingside
* castling * castling
*/ */
@Override @Override

View File

@ -4,7 +4,7 @@ package dev.kske.chess.board;
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>EnPassant.java</strong><br> * File: <strong>EnPassant.java</strong><br>
* Created: <strong>2 Nov 2019</strong><br> * Created: <strong>2 Nov 2019</strong><br>
* *
* @since Chess v0.5-alpha * @since Chess v0.5-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
@ -12,12 +12,28 @@ public class EnPassant extends Move {
private final Position capturePos; private final Position capturePos;
/**
* Initializes an en passant move.
*
* @param pos the position of this move
* @param dest the destination of this move
*/
public EnPassant(Position pos, Position dest) { public EnPassant(Position pos, Position dest) {
super(pos, dest); super(pos, dest);
capturePos = new Position(dest.x, dest.y - ySign); capturePos = new Position(dest.x, dest.y - ySign);
} }
public EnPassant(int xPos, int yPos, int xDest, int yDest) { this(new Position(xPos, yPos), new Position(xDest, yDest)); } /**
* Initializes an en passant move.
*
* @param xPos the horizontal position of this move
* @param yPos the vertical position of this move
* @param xDest the horizontal destination of this move
* @param yDest the vertical destination of this move
*/
public EnPassant(int xPos, int yPos, int xDest, int yDest) {
this(new Position(xPos, yPos), new Position(xDest, yDest));
}
@Override @Override
public void execute(Board board) { public void execute(Board board) {
@ -28,8 +44,14 @@ public class EnPassant extends Move {
@Override @Override
public void revert(Board board, Piece capturedPiece) { public void revert(Board board, Piece capturedPiece) {
super.revert(board, capturedPiece); super.revert(board, capturedPiece);
board.set(capturePos, new Pawn(board.get(pos).getColor().opposite(), board)); board.set(
capturePos,
new Pawn(board.get(pos).getColor().opposite(), board)
);
} }
/**
* @return the position of the piece captures by this move
*/
public Position getCapturePos() { return capturePos; } public Position getCapturePos() { return capturePos; }
} }

View File

@ -15,51 +15,58 @@ import dev.kske.chess.exception.ChessException;
* <br> * <br>
* Represents a FEN string and enables parsing an existing FEN string or * Represents a FEN string and enables parsing an existing FEN string or
* serializing a {@link Board} to one. * serializing a {@link Board} to one.
* *
* @since Chess v0.5-alpha * @since Chess v0.5-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
public class FENString { public class FENString {
private Board board; private Board board;
private String piecePlacement, castlingAvailability; private String piecePlacement, castlingAvailability;
private int halfmoveClock, fullmoveNumber; private int halfmoveClock, fullmoveNumber;
private Color activeColor; private Color activeColor;
private Position enPassantTargetSquare; private Position enPassantTargetSquare;
/** /**
* Constructs a {@link FENString} representing the starting position * Constructs a {@link FENString} representing the starting position
* {@code rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1}. * {@code rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1}.
*/ */
public FENString() { public FENString() {
board = new Board(); board = new Board();
piecePlacement = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"; piecePlacement = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR";
activeColor = Color.WHITE; activeColor = Color.WHITE;
castlingAvailability = "KQkq"; castlingAvailability = "KQkq";
halfmoveClock = 0; halfmoveClock = 0;
fullmoveNumber = 1; fullmoveNumber = 1;
} }
/** /**
* Constructs a {@link FENString} by parsing an existing string. * Constructs a {@link FENString} by parsing an existing string.
* *
* @param fen the FEN string to parse * @param fen the FEN string to parse
* @throws ChessException * @throws ChessException if the FEN string contains invalid syntax
*/ */
public FENString(String fen) throws ChessException { public FENString(String fen) throws ChessException {
// Check fen string against regex // Check fen string against regex
Pattern fenPattern = Pattern.compile( Pattern fenPattern = Pattern.compile(
"^(?<piecePlacement>(?:[1-8nbrqkpNBRQKP]{1,8}\\/){7}[1-8nbrqkpNBRQKP]{1,8}) (?<activeColor>[wb]) (?<castlingAvailability>-|[KQkq]{1,4}) (?<enPassantTargetSquare>-|[a-h][1-8]) (?<halfmoveClock>\\d+) (?<fullmoveNumber>\\d+)$"); "^(?<piecePlacement>(?:[1-8nbrqkpNBRQKP]{1,8}\\/){7}[1-8nbrqkpNBRQKP]{1,8}) (?<activeColor>[wb]) (?<castlingAvailability>-|[KQkq]{1,4}) (?<enPassantTargetSquare>-|[a-h][1-8]) (?<halfmoveClock>\\d+) (?<fullmoveNumber>\\d+)$"
Matcher matcher = fenPattern.matcher(fen); );
if (!matcher.find()) throw new ChessException("FEN string does not match pattern " + fenPattern.pattern()); Matcher matcher = fenPattern.matcher(fen);
if (!matcher.find())
throw new ChessException(
"FEN string does not match pattern " + fenPattern.pattern()
);
// Initialize data fields // Initialize data fields
piecePlacement = matcher.group("piecePlacement"); piecePlacement = matcher.group("piecePlacement");
activeColor = Color.fromFirstChar(matcher.group("activeColor").charAt(0)); activeColor
castlingAvailability = matcher.group("castlingAvailability"); = Color.fromFirstChar(matcher.group("activeColor").charAt(0));
if (!matcher.group("enPassantTargetSquare").equals("-")) enPassantTargetSquare = Position.fromLAN(matcher.group("enPassantTargetSquare")); castlingAvailability = matcher.group("castlingAvailability");
halfmoveClock = Integer.parseInt(matcher.group("halfmoveClock")); if (!matcher.group("enPassantTargetSquare").equals("-"))
fullmoveNumber = Integer.parseInt(matcher.group("fullmoveNumber")); enPassantTargetSquare
= Position.fromLAN(matcher.group("enPassantTargetSquare"));
halfmoveClock = Integer.parseInt(matcher.group("halfmoveClock"));
fullmoveNumber = Integer.parseInt(matcher.group("fullmoveNumber"));
// Initialize and clean board // Initialize and clean board
board = new Board(); board = new Board();
@ -71,30 +78,40 @@ public class FENString {
// Piece placement // Piece placement
final String[] rows = piecePlacement.split("/"); final String[] rows = piecePlacement.split("/");
if (rows.length != 8) throw new ChessException("FEN string contains invalid piece placement"); if (rows.length != 8)
throw new ChessException(
"FEN string contains invalid piece placement"
);
for (int i = 0; i < 8; i++) { for (int i = 0; i < 8; i++) {
final char[] cols = rows[i].toCharArray(); final char[] cols = rows[i].toCharArray();
int j = 0; int j = 0;
for (char c : cols) { for (char c : cols)
// Empty space // Empty space
if (Character.isDigit(c)) { if (Character.isDigit(c))
j += Character.getNumericValue(c); j += Character.getNumericValue(c);
} else { else {
Color color = Character.isUpperCase(c) ? Color.WHITE : Color.BLACK; Color color
= Character.isUpperCase(c) ? Color.WHITE : Color.BLACK;
try { try {
Constructor<? extends Piece> pieceConstructor = Piece.fromFirstChar(c).getDeclaredConstructor(Color.class, Board.class); Constructor<? extends Piece> pieceConstructor = Piece
.fromFirstChar(c)
.getDeclaredConstructor(Color.class, Board.class);
pieceConstructor.setAccessible(true); pieceConstructor.setAccessible(true);
board.getBoardArr()[j][i] = pieceConstructor.newInstance(color, board); board.getBoardArr()[j][i]
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException = pieceConstructor.newInstance(color, board);
| NoSuchMethodException | SecurityException e) { } catch (
InstantiationException
| IllegalAccessException
| IllegalArgumentException
| InvocationTargetException
| NoSuchMethodException
| SecurityException e
) {
e.printStackTrace(); e.printStackTrace();
} }
++j; ++j;
} }
}
} }
// Active color // Active color
board.getLog().setActiveColor(activeColor); board.getLog().setActiveColor(activeColor);
@ -129,7 +146,7 @@ public class FENString {
/** /**
* Constructs a {@link FENString} form a {@link Board} object. * Constructs a {@link FENString} form a {@link Board} object.
* *
* @param board the {@link Board} object to encode in this {@link FENString} * @param board the {@link Board} object to encode in this {@link FENString}
*/ */
public FENString(Board board) { public FENString(Board board) {
@ -144,7 +161,8 @@ public class FENString {
for (int j = 0; j < 8; j++) { for (int j = 0; j < 8; j++) {
final Piece piece = board.getBoardArr()[j][i]; final Piece piece = board.getBoardArr()[j][i];
if (piece == null) ++empty; if (piece == null)
++empty;
else { else {
// Write empty field count // Write empty field count
@ -152,20 +170,22 @@ public class FENString {
sb.append(empty); sb.append(empty);
empty = 0; empty = 0;
} }
// Write piece character // Write piece character
char p = piece.firstChar(); char p = piece.firstChar();
sb.append(piece.getColor() == Color.WHITE ? Character.toUpperCase(p) : p); sb.append(
piece.getColor() == Color.WHITE
? Character.toUpperCase(p)
: p
);
} }
} }
// Write empty field count // Write empty field count
if (empty > 0) { if (empty > 0) {
sb.append(empty); sb.append(empty);
empty = 0; empty = 0;
} }
if (i < 7)
if (i < 7) sb.append('/'); sb.append('/');
} }
piecePlacement = sb.toString(); piecePlacement = sb.toString();
@ -174,10 +194,14 @@ public class FENString {
// Castling availability // Castling availability
castlingAvailability = ""; castlingAvailability = "";
final char castlingRightsChars[] = new char[] { 'K', 'Q', 'k', 'q' }; final char castlingRightsChars[] = new char[] {
'K', 'Q', 'k', 'q'
};
for (int i = 0; i < 4; i++) for (int i = 0; i < 4; i++)
if (board.getLog().getCastlingRights()[i]) castlingAvailability += castlingRightsChars[i]; if (board.getLog().getCastlingRights()[i])
if (castlingAvailability.isEmpty()) castlingAvailability = "-"; castlingAvailability += castlingRightsChars[i];
if (castlingAvailability.isEmpty())
castlingAvailability = "-";
// En passant availability // En passant availability
enPassantTargetSquare = board.getLog().getEnPassant(); enPassantTargetSquare = board.getLog().getEnPassant();
@ -191,18 +215,20 @@ public class FENString {
/** /**
* Exports this {@link FENString} object to a FEN string. * Exports this {@link FENString} object to a FEN string.
* *
* @return a FEN string representing the board * @return a FEN string representing the board
*/ */
@Override @Override
public String toString() { public String toString() {
return String.format("%s %c %s %s %d %d", return String.format(
piecePlacement, "%s %c %s %s %d %d",
activeColor.firstChar(), piecePlacement,
castlingAvailability, activeColor.firstChar(),
enPassantTargetSquare == null ? "-" : enPassantTargetSquare.toLAN(), castlingAvailability,
halfmoveClock, enPassantTargetSquare == null ? "-" : enPassantTargetSquare.toLAN(),
fullmoveNumber); halfmoveClock,
fullmoveNumber
);
} }
/** /**

View File

@ -7,66 +7,100 @@ import java.util.List;
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>King.java</strong><br> * File: <strong>King.java</strong><br>
* Created: <strong>01.07.2019</strong><br> * Created: <strong>01.07.2019</strong><br>
* *
* @since Chess v0.1-alpha * @since Chess v0.1-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
public class King extends Piece { public class King extends Piece {
public King(Color color, Board board) { super(color, board); } /**
* Creates king {@link Piece}.
*
* @param color the color of this king
* @param board the board on which this king will be placed
*/
public King(Color color, Board board) {
super(color, board);
}
@Override @Override
public boolean isValidMove(Move move) { public boolean isValidMove(Move move) {
return (move.getxDist() == 2 && move.getyDist() == 0 return move.getxDist() == 2 && move.getyDist() == 0
&& (move.getDest().x == 6 && canCastleKingside() || move.getDest().x == 2 && canCastleQueenside())) && (move.getDest().x == 6 && canCastleKingside()
|| move.getxDist() <= 1 && move.getyDist() <= 1 && checkDestination(move); || move.getDest().x == 2 && canCastleQueenside())
|| move.getxDist() <= 1 && move.getyDist() <= 1
&& checkDestination(move);
} }
@Override @Override
protected List<Move> getPseudolegalMoves(Position pos) { protected List<Move> getPseudolegalMoves(Position pos) {
List<Move> moves = new ArrayList<>(); List<Move> moves = new ArrayList<>();
for (int i = Math.max(0, pos.x - 1); i < Math.min(8, pos.x + 2); i++) for (int i = Math.max(0, pos.x - 1); i < Math.min(8, pos.x + 2); i++)
for (int j = Math.max(0, pos.y - 1); j < Math.min(8, pos.y + 2); j++) for (
int j = Math.max(0, pos.y - 1);
j < Math.min(8, pos.y + 2);
j++
)
if (i != pos.x || j != pos.y) { if (i != pos.x || j != pos.y) {
Move move = new Move(pos, new Position(i, j)); Move move = new Move(pos, new Position(i, j));
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) moves.add(move); if (
board.getDest(move) == null
|| board.getDest(move).getColor() != getColor()
)
moves.add(move);
} }
// Castling // Castling
if (canCastleKingside()) moves.add(new Castling(pos, new Position(6, pos.y))); if (canCastleKingside())
if (canCastleQueenside()) moves.add(new Castling(pos, new Position(2, pos.y))); moves.add(new Castling(pos, new Position(6, pos.y)));
if (canCastleQueenside())
moves.add(new Castling(pos, new Position(2, pos.y)));
return moves; return moves;
} }
private boolean canCastleKingside() { private boolean canCastleKingside() {
if (board.getLog().getCastlingRights()[getColor() == Color.WHITE ? MoveNode.WHITE_KINGSIDE : MoveNode.BLACK_KINGSIDE]) { if (
int y = getColor() == Color.WHITE ? 7 : 0; board.getLog().getCastlingRights()[getColor() == Color.WHITE
Position kingPos = new Position(4, y); ? MoveNode.WHITE_KINGSIDE
Position jumpPos = new Position(5, y); : MoveNode.BLACK_KINGSIDE]
Position kingDest = new Position(6, y); ) {
Position rookPos = new Position(7, y); int y = getColor() == Color.WHITE ? 7 : 0;
Position kingPos = new Position(4, y);
Position jumpPos = new Position(5, y);
Position kingDest = new Position(6, y);
Position rookPos = new Position(7, y);
return canCastle(kingPos, kingDest, rookPos, jumpPos); return canCastle(kingPos, kingDest, rookPos, jumpPos);
} else return false; }
return false;
} }
private boolean canCastleQueenside() { private boolean canCastleQueenside() {
if (board.getLog().getCastlingRights()[getColor() == Color.WHITE ? MoveNode.WHITE_QUEENSIDE : MoveNode.BLACK_QUEENSIDE]) { if (
int y = getColor() == Color.WHITE ? 7 : 0; board.getLog().getCastlingRights()[getColor() == Color.WHITE
Position kingPos = new Position(4, y); ? MoveNode.WHITE_QUEENSIDE
Position jumpPos = new Position(3, y); : MoveNode.BLACK_QUEENSIDE]
Position freeDest = new Position(1, y); ) {
Position rookPos = new Position(0, y); int y = getColor() == Color.WHITE ? 7 : 0;
Position kingPos = new Position(4, y);
Position jumpPos = new Position(3, y);
Position freeDest = new Position(1, y);
Position rookPos = new Position(0, y);
return canCastle(kingPos, freeDest, rookPos, jumpPos); return canCastle(kingPos, freeDest, rookPos, jumpPos);
} else return false; }
return false;
} }
private boolean canCastle(Position kingPos, Position freeDest, Position rookPos, Position jumpPos) { private boolean canCastle(
Position kingPos, Position freeDest, Position rookPos, Position jumpPos
) {
Piece rook = board.get(rookPos); Piece rook = board.get(rookPos);
return rook != null && rook instanceof Rook && isFreePath(new Move(kingPos, freeDest)) && !board.isAttacked(kingPos, getColor().opposite()) return rook != null && rook instanceof Rook && isFreePath(
&& !board.isAttacked(jumpPos, getColor().opposite()); new Move(kingPos, freeDest)
) && !board.isAttacked(kingPos, getColor().opposite())
&& !board.isAttacked(jumpPos, getColor().opposite());
} }
@Override @Override
public int getValue() { return 0; } public int getValue() { return 0; }
} }

View File

@ -7,12 +7,18 @@ import java.util.List;
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>Knight.java</strong><br> * File: <strong>Knight.java</strong><br>
* Created: <strong>01.07.2019</strong><br> * Created: <strong>01.07.2019</strong><br>
* *
* @since Chess v0.1-alpha * @since Chess v0.1-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
public class Knight extends Piece { public class Knight extends Piece {
/**
* Creates knight {@link Piece}.
*
* @param color the color of this knight
* @param board the board on which this knight will be placed
*/
public Knight(Color color, Board board) { public Knight(Color color, Board board) {
super(color, board); super(color, board);
} }
@ -20,13 +26,22 @@ public class Knight extends Piece {
@Override @Override
public boolean isValidMove(Move move) { public boolean isValidMove(Move move) {
return Math.abs(move.getxDist() - move.getyDist()) == 1 return Math.abs(move.getxDist() - move.getyDist()) == 1
&& (move.getxDist() == 1 && move.getyDist() == 2 || move.getxDist() == 2 && move.getyDist() == 1) && checkDestination(move); && (move.getxDist() == 1 && move.getyDist() == 2
|| move.getxDist() == 2 && move.getyDist() == 1)
&& checkDestination(move);
} }
private void checkAndInsertMove(List<Move> moves, Position pos, int offsetX, int offsetY) { private void checkAndInsertMove(
if (pos.x + offsetX >= 0 && pos.x + offsetX < 8 && pos.y + offsetY >= 0 && pos.y + offsetY < 8) { List<Move> moves, Position pos, int offsetX, int offsetY
Move move = new Move(pos, new Position(pos.x + offsetX, pos.y + offsetY)); ) {
if (checkDestination(move)) moves.add(move); if (
pos.x + offsetX >= 0 && pos.x + offsetX < 8 && pos.y + offsetY >= 0
&& pos.y + offsetY < 8
) {
Move move
= new Move(pos, new Position(pos.x + offsetX, pos.y + offsetY));
if (checkDestination(move))
moves.add(move);
} }
} }
@ -48,5 +63,7 @@ public class Knight extends Piece {
public int getValue() { return 35; } public int getValue() { return 35; }
@Override @Override
public char firstChar() { return 'n'; } public char firstChar() {
return 'n';
}
} }

View File

@ -5,8 +5,11 @@ import java.util.Iterator;
import java.util.Objects; import java.util.Objects;
import dev.kske.chess.board.Piece.Color; 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> * Project: <strong>Chess</strong><br>
* File: <strong>Log.java</strong><br> * File: <strong>Log.java</strong><br>
* Created: <strong>09.07.2019</strong><br> * Created: <strong>09.07.2019</strong><br>
@ -18,28 +21,35 @@ public class Log implements Iterable<MoveNode> {
private MoveNode root, current; private MoveNode root, current;
private Color activeColor; private Color activeColor;
private boolean[] castlingRights; private boolean[] castlingRights;
private Position enPassant; private Position enPassant;
private int fullmoveNumber, halfmoveClock; private int fullmoveNumber, halfmoveClock;
public Log() { reset(); }
/** /**
* Creates a (partially deep) copy of another {@link Log} instance which begins * 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}. * with the current {@link MoveNode}.
* *
* @param other The {@link Log} instance to copy * @param other The {@link Log} instance to copy
* @param copyVariations If set to {@code true}, subsequent variations of the * @param copyVariations If set to {@code true}, subsequent variations of
* the
* current {@link MoveNode} are copied with the * current {@link MoveNode} are copied with the
* {@link Log} * {@link Log}
*/ */
public Log(Log other, boolean copyVariations) { public Log(Log other, boolean copyVariations) {
enPassant = other.enPassant; enPassant = other.enPassant;
castlingRights = other.castlingRights.clone(); castlingRights = other.castlingRights.clone();
activeColor = other.activeColor; activeColor = other.activeColor;
fullmoveNumber = other.fullmoveNumber; fullmoveNumber = other.fullmoveNumber;
halfmoveClock = other.halfmoveClock; halfmoveClock = other.halfmoveClock;
// The new root is the current node of the copied instance // The new root is the current node of the copied instance
if (!other.isEmpty()) { if (!other.isEmpty()) {
@ -58,17 +68,21 @@ public class Log implements Iterable<MoveNode> {
public Iterator<MoveNode> iterator() { public Iterator<MoveNode> iterator() {
return new Iterator<MoveNode>() { return new Iterator<MoveNode>() {
private MoveNode current = root; private MoveNode current = root;
private boolean hasNext = !isEmpty(); private boolean hasNext = !isEmpty();
@Override @Override
public boolean hasNext() { return hasNext; } public boolean hasNext() {
return hasNext;
}
@Override @Override
public MoveNode next() { public MoveNode next() {
MoveNode result = current; MoveNode result = current;
if (current.hasVariations()) current = current.getVariations().get(0); if (current.hasVariations())
else hasNext = false; current = current.getVariations().get(0);
else
hasNext = false;
return result; return result;
} }
}; };
@ -82,20 +96,34 @@ public class Log implements Iterable<MoveNode> {
* @param capturedPiece The piece captured with the move * @param capturedPiece The piece captured with the move
*/ */
public void add(Move move, Piece piece, Piece capturedPiece) { 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; enPassant = piece instanceof Pawn && move.getyDist() == 2
if (activeColor == Color.BLACK) ++fullmoveNumber; ? new Position(move.getPos().x, move.getPos().y + move.getySign())
if (piece instanceof Pawn || capturedPiece != null) halfmoveClock = 0; : null;
else++halfmoveClock; if (activeColor == Color.BLACK)
++fullmoveNumber;
if (piece instanceof Pawn || capturedPiece != null)
halfmoveClock = 0;
else
++halfmoveClock;
activeColor = activeColor.opposite(); activeColor = activeColor.opposite();
// Disable castling rights if a king or a rook has been moved // Disable castling rights if a king or a rook has been moved
if (piece instanceof King || piece instanceof Rook) disableCastlingRights(piece, move.getPos()); if (piece instanceof King || piece instanceof Rook)
disableCastlingRights(piece, move.getPos());
final MoveNode leaf = new MoveNode(move, capturedPiece, castlingRights.clone(), enPassant, activeColor, fullmoveNumber, halfmoveClock); final MoveNode leaf = new MoveNode(
move,
capturedPiece,
castlingRights.clone(),
enPassant,
activeColor,
fullmoveNumber,
halfmoveClock
);
if (isEmpty()) { if (isEmpty()) {
root = leaf; root = leaf;
current = leaf; current = leaf;
} else { } else {
current.addVariation(leaf); current.addVariation(leaf);
current = leaf; current = leaf;
@ -111,7 +139,8 @@ public class Log implements Iterable<MoveNode> {
current.getParent().getVariations().remove(current); current.getParent().getVariations().remove(current);
current = current.getParent(); current = current.getParent();
update(); update();
} else reset(); } else
reset();
} }
/** /**
@ -122,20 +151,24 @@ public class Log implements Iterable<MoveNode> {
/** /**
* @return {@code true} if the current node has a parent node * @return {@code true} if the current node has a parent node
*/ */
public boolean hasParent() { return !isEmpty() && current.hasParent(); } public boolean hasParent() {
return !isEmpty() && current.hasParent();
}
/** /**
* Reverts the log to its initial state corresponding to the default board * Reverts the log to its initial state corresponding to the default board
* position. * position.
*/ */
public void reset() { public void reset() {
root = null; root = null;
current = null; current = null;
castlingRights = new boolean[] { true, true, true, true }; castlingRights = new boolean[] {
enPassant = null; true, true, true, true
activeColor = Color.WHITE; };
fullmoveNumber = 1; enPassant = null;
halfmoveClock = 0; activeColor = Color.WHITE;
fullmoveNumber = 1;
halfmoveClock = 0;
} }
/** /**
@ -144,7 +177,10 @@ public class Log implements Iterable<MoveNode> {
* @param index the index of the variation to select * @param index the index of the variation to select
*/ */
public void selectNextNode(int index) { public void selectNextNode(int index) {
if (!isEmpty() && current.hasVariations() && index < current.getVariations().size()) { if (
!isEmpty() && current.hasVariations()
&& index < current.getVariations().size()
) {
current = current.getVariations().get(index); current = current.getVariations().get(index);
update(); update();
} }
@ -171,53 +207,78 @@ public class Log implements Iterable<MoveNode> {
} }
/** /**
* Sets the active color, castling rights, en passant target square, fullmove * Sets the active color, castling rights, en passant target square,
* fullmove
* number and halfmove clock to those of the current {@link MoveNode}. * number and halfmove clock to those of the current {@link MoveNode}.
*/ */
private void update() { private void update() {
activeColor = current.activeColor; activeColor = current.activeColor;
castlingRights = current.castlingRights.clone(); castlingRights = current.castlingRights.clone();
enPassant = current.enPassant; enPassant = current.enPassant;
fullmoveNumber = current.fullmoveCounter; fullmoveNumber = current.fullmoveCounter;
halfmoveClock = current.halfmoveClock; halfmoveClock = current.halfmoveClock;
} }
/** /**
* Removed the castling rights bound to a rook or king for the rest of the game. * 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 * This method should be called once the piece has been moved, as a castling
* move involving this piece is forbidden afterwards. * move involving this piece is forbidden afterwards.
* *
* @param piece the rook or king to disable the castling rights for * @param piece the rook or king to disable the castling rights
* @param initialPosition the initial position of the piece during the start of * for
* @param initialPosition the initial position of the piece during the start
* of
* the game * the game
*/ */
private void disableCastlingRights(Piece piece, Position initialPosition) { private void disableCastlingRights(Piece piece, Position initialPosition) {
// Kingside // Kingside
if (piece instanceof King || piece instanceof Rook && initialPosition.x == 7) if (
castlingRights[piece.getColor() == Color.WHITE ? MoveNode.WHITE_KINGSIDE : MoveNode.BLACK_KINGSIDE] = false; piece instanceof King
|| piece instanceof Rook && initialPosition.x == 7
)
castlingRights[piece.getColor() == Color.WHITE
? MoveNode.WHITE_KINGSIDE
: MoveNode.BLACK_KINGSIDE] = false;
// Queenside // Queenside
if (piece instanceof King || piece instanceof Rook && initialPosition.x == 0) if (
castlingRights[piece.getColor() == Color.WHITE ? MoveNode.WHITE_QUEENSIDE : MoveNode.BLACK_QUEENSIDE] = false; piece instanceof King
|| piece instanceof Rook && initialPosition.x == 0
)
castlingRights[piece.getColor() == Color.WHITE
? MoveNode.WHITE_QUEENSIDE
: MoveNode.BLACK_QUEENSIDE] = false;
} }
@Override @Override
public int hashCode() { public int hashCode() {
final int prime = 31; final int prime = 31;
int result = 1; int result = 1;
result = prime * result + Arrays.hashCode(castlingRights); result = prime * result + Arrays.hashCode(castlingRights);
result = prime * result + Objects.hash(activeColor, current, enPassant, fullmoveNumber, halfmoveClock); result = prime * result + Objects.hash(
activeColor,
current,
enPassant,
fullmoveNumber,
halfmoveClock
);
return result; return result;
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (this == obj) return true; if (this == obj)
if (obj == null) return false; return true;
if (getClass() != obj.getClass()) return false; if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Log other = (Log) obj; Log other = (Log) obj;
return activeColor == other.activeColor && Arrays.equals(castlingRights, other.castlingRights) && Objects.equals(current, other.current) return activeColor == other.activeColor && Arrays
&& Objects.equals(enPassant, other.enPassant) && fullmoveNumber == other.fullmoveNumber && halfmoveClock == other.halfmoveClock; .equals(castlingRights, other.castlingRights)
&& Objects.equals(current, other.current)
&& Objects.equals(enPassant, other.enPassant) && fullmoveNumber == other.fullmoveNumber && halfmoveClock == other.halfmoveClock;
} }
/** /**
@ -230,23 +291,75 @@ public class Log implements Iterable<MoveNode> {
*/ */
public MoveNode getLast() { return current; } public MoveNode getLast() { return current; }
/**
* @return the castling rights present during the current move
*/
public boolean[] getCastlingRights() { return castlingRights; } public boolean[] getCastlingRights() { return castlingRights; }
public void setCastlingRights(boolean[] castlingRights) { this.castlingRights = 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; } public Position getEnPassant() { return enPassant; }
public void setEnPassant(Position enPassant) { this.enPassant = 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; } public Color getActiveColor() { return activeColor; }
public void setActiveColor(Color activeColor) { this.activeColor = 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; } public int getFullmoveNumber() { return fullmoveNumber; }
public void setFullmoveNumber(int fullmoveNumber) { this.fullmoveNumber = 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; } public int getHalfmoveClock() { return halfmoveClock; }
public void setHalfmoveClock(int halfmoveClock) { this.halfmoveClock = 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;
}
}

View File

@ -18,46 +18,104 @@ import dev.kske.chess.board.Piece.Color;
*/ */
public class Move { public class Move {
protected final Position pos, dest; protected final Position pos, dest;
protected final int xDist, yDist, xSign, ySign; protected final int xDist, yDist, xSign, ySign;
/**
* Creates an instance of {@link Move}.
*
* @param pos the position of this move
* @param dest the destination of this move
*/
public Move(Position pos, Position dest) { public Move(Position pos, Position dest) {
this.pos = pos; this.pos = pos;
this.dest = dest; this.dest = dest;
xDist = Math.abs(dest.x - pos.x); xDist = Math.abs(dest.x - pos.x);
yDist = Math.abs(dest.y - pos.y); yDist = Math.abs(dest.y - pos.y);
xSign = (int) Math.signum(dest.x - pos.x); xSign = (int) Math.signum(dest.x - pos.x);
ySign = (int) Math.signum(dest.y - pos.y); ySign = (int) Math.signum(dest.y - pos.y);
} }
public Move(int xPos, int yPos, int xDest, int yDest) { this(new Position(xPos, yPos), new Position(xDest, yDest)); } /**
* Creates an instance of {@link Move}.
*
* @param xPos the horizontal position of this move
* @param yPos the vertical position of this move
* @param xDest the horizontal destination of this move
* @param yDest the vertical destination of this move
*/
public Move(int xPos, int yPos, int xDest, int yDest) {
this(new Position(xPos, yPos), new Position(xDest, yDest));
}
/**
* Executed this move on a board.
*
* @param board the board to execute this move on.
*/
public void execute(Board board) { public void execute(Board board) {
// Move the piece to the move's destination square and clean the old position // Move the piece to the move's destination square and clean the old
// position
board.set(dest, board.get(pos)); board.set(dest, board.get(pos));
board.set(pos, null); board.set(pos, null);
} }
/**
* Reverts this move on a board.
*
* @param board the board to revert this move on
* @param capturedPiece the piece to place at the destination of this move
* (used
* for reinstating captured pieces)
*/
public void revert(Board board, Piece capturedPiece) { public void revert(Board board, Piece capturedPiece) {
// Move the piece to the move's position square and clean the destination // Move the piece to the move's position square and clean the
// destination
board.set(pos, board.get(dest)); board.set(pos, board.get(dest));
board.set(dest, capturedPiece); board.set(dest, capturedPiece);
} }
/**
* @return a new move containing this move's destination as its position and
* this move's position as its destination
*/
public Move invert() {
return new Move(dest, pos);
}
/**
* Constructs a move from a string representation in Long Algebraic Notation
* (LAN).
*
* @param move the LAN string to construct the move from
* @return the constructed move
*/
public static Move fromLAN(String move) { public static Move fromLAN(String move) {
Position pos = Position.fromLAN(move.substring(0, 2)); Position pos = Position.fromLAN(move.substring(0, 2));
Position dest = Position.fromLAN(move.substring(2)); Position dest = Position.fromLAN(move.substring(2));
if (move.length() == 5) { if (move.length() == 5)
try { try {
return new PawnPromotion(pos, dest, Piece.fromFirstChar(move.charAt(4))); return new PawnPromotion(
pos,
dest,
Piece.fromFirstChar(move.charAt(4))
);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
return null; return null;
} }
} else return new Move(pos, dest); return new Move(pos, dest);
} }
public String toLAN() { return getPos().toLAN() + getDest().toLAN(); } /**
* Generates a string representation of this move in Long Algebraic Notation
* (LAN).
*
* @return the LAN string
*/
public String toLAN() {
return getPos().toLAN() + getDest().toLAN();
}
/** /**
* Converts a move string from standard algebraic notation to a {@link Move} * Converts a move string from standard algebraic notation to a {@link Move}
@ -69,75 +127,125 @@ public class Move {
*/ */
public static Move fromSAN(String sanMove, Board board) { public static Move fromSAN(String sanMove, Board board) {
Map<String, Pattern> patterns = new HashMap<>(); Map<String, Pattern> patterns = new HashMap<>();
patterns.put("pieceMove", patterns.put(
Pattern.compile( "pieceMove",
"^(?<pieceType>[NBRQK])(?:(?<fromFile>[a-h])|(?<fromRank>[1-8])|(?<fromSquare>[a-h][1-8]))?x?(?<toSquare>[a-h][1-8])(?:\\+{0,2}|\\#)$")); Pattern.compile(
patterns.put("pawnCapture", "^(?<pieceType>[NBRQK])(?:(?<fromFile>[a-h])|(?<fromRank>[1-8])|(?<fromSquare>[a-h][1-8]))?x?(?<toSquare>[a-h][1-8])(?:\\+{0,2}|\\#)$"
Pattern.compile("^(?<fromFile>[a-h])(?<fromRank>[1-8])?x(?<toSquare>[a-h][1-8])(?<promotedTo>[NBRQ])?(?:\\+{0,2}|\\#)?$")); )
patterns.put("pawnPush", Pattern.compile("^(?<toSquare>[a-h][1-8])(?<promotedTo>[NBRQ])?(?:\\+{0,2}|\\#)$")); );
patterns.put("castling", Pattern.compile("^(?<queenside>O-O-O)|(?<kingside>O-O)(?:\\+{0,2}|\\#)?$")); patterns.put(
"pawnCapture",
Pattern.compile(
"^(?<fromFile>[a-h])(?<fromRank>[1-8])?x(?<toSquare>[a-h][1-8])(?<promotedTo>[NBRQ])?(?:\\+{0,2}|\\#)?$"
)
);
patterns.put(
"pawnPush",
Pattern.compile(
"^(?<toSquare>[a-h][1-8])(?<promotedTo>[NBRQ])?(?:\\+{0,2}|\\#)$"
)
);
patterns.put(
"castling",
Pattern.compile(
"^(?<queenside>O-O-O)|(?<kingside>O-O)(?:\\+{0,2}|\\#)?$"
)
);
for (Map.Entry<String, Pattern> entry : patterns.entrySet()) { for (Map.Entry<String, Pattern> entry : patterns.entrySet()) {
Matcher m = entry.getValue().matcher(sanMove); Matcher m = entry.getValue().matcher(sanMove);
if (m.find()) { if (m.find()) {
Position pos = null, dest = null; Position pos = null, dest = null;
Move move = null; Move move = null;
switch (entry.getKey()) { switch (entry.getKey()) {
case "pieceMove": case "pieceMove":
dest = Position.fromLAN(m.group("toSquare")); dest = Position.fromLAN(m.group("toSquare"));
if (m.group("fromSquare") != null) pos = Position.fromLAN(m.group("fromSquare")); if (m.group("fromSquare") != null)
pos = Position.fromLAN(m.group("fromSquare"));
else { else {
Class<? extends Piece> pieceClass = Piece.fromFirstChar(m.group("pieceType").charAt(0)); Class<? extends Piece> pieceClass = Piece
char file; .fromFirstChar(m.group("pieceType").charAt(0));
int rank; char file;
int rank;
if (m.group("fromFile") != null) { if (m.group("fromFile") != null) {
file = m.group("fromFile").charAt(0); file = m.group("fromFile").charAt(0);
rank = board.get(pieceClass, file); rank = board.get(pieceClass, file);
pos = Position.fromLAN(String.format("%c%d", file, rank)); pos = Position
} else if (m.group("fromRank") != null) { .fromLAN(String.format("%c%d", file, rank));
rank = Integer.parseInt(m.group("fromRank").substring(0, 1)); } else
file = board.get(pieceClass, rank); if (m.group("fromRank") != null) {
pos = Position.fromLAN(String.format("%c%d", file, rank)); rank = Integer.parseInt(
} else pos = board.get(pieceClass, dest); m.group("fromRank").substring(0, 1)
);
file = board.get(pieceClass, rank);
pos = Position.fromLAN(
String.format("%c%d", file, rank)
);
} else
pos = board.get(pieceClass, dest);
} }
move = new Move(pos, dest); move = new Move(pos, dest);
break; break;
case "pawnCapture": case "pawnCapture":
char file = m.group("fromFile").charAt(0); char file = m.group("fromFile").charAt(0);
int rank = m.group("fromRank") == null ? board.get(Pawn.class, file) : Integer.parseInt(m.group("fromRank")); int rank = m.group("fromRank") == null
? board.get(Pawn.class, file)
: Integer.parseInt(m.group("fromRank"));
dest = Position.fromLAN(m.group("toSquare")); dest = Position.fromLAN(m.group("toSquare"));
pos = Position.fromLAN(String.format("%c%d", file, rank)); pos = Position
.fromLAN(String.format("%c%d", file, rank));
if (m.group("promotedTo") != null) { if (m.group("promotedTo") != null)
try { try {
move = new PawnPromotion(pos, dest, Piece.fromFirstChar(m.group("promotedTo").charAt(0))); move = new PawnPromotion(
pos,
dest,
Piece.fromFirstChar(m.group("promotedTo").charAt(0))
);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
} else move = new Move(pos, dest); else
move = new Move(pos, dest);
break; break;
case "pawnPush": case "pawnPush":
dest = Position.fromLAN(m.group("toSquare")); dest = Position.fromLAN(m.group("toSquare"));
int step = board.getLog().getActiveColor() == Color.WHITE ? 1 : -1; int step
= board.getLog().getActiveColor() == Color.WHITE ? 1
: -1;
// One step forward // One step forward
if (board.getBoardArr()[dest.x][dest.y + step] != null) pos = new Position(dest.x, dest.y + step); if (board.getBoardArr()[dest.x][dest.y + step] != null)
pos = new Position(dest.x, dest.y + step);
// Double step forward // Double step forward
else pos = new Position(dest.x, dest.y + 2 * step); else
pos = new Position(dest.x, dest.y + 2 * step);
if (m.group("promotedTo") != null) { if (m.group("promotedTo") != null)
try { try {
move = new PawnPromotion(pos, dest, Piece.fromFirstChar(m.group("promotedTo").charAt(0))); move = new PawnPromotion(
pos,
dest,
Piece.fromFirstChar(m.group("promotedTo").charAt(0))
);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
} else move = new Move(pos, dest); else
move = new Move(pos, dest);
break; break;
case "castling": case "castling":
pos = new Position(4, board.getLog().getActiveColor() == Color.WHITE ? 7 : 0); pos = new Position(
dest = new Position(m.group("kingside") != null ? 6 : 2, pos.y); 4,
board.getLog().getActiveColor() == Color.WHITE ? 7
: 0
);
dest = new Position(
m.group("kingside") != null ? 6 : 2,
pos.y
);
move = new Castling(pos, dest); move = new Castling(pos, dest);
break; break;
} }
@ -147,21 +255,31 @@ public class Move {
return null; return null;
} }
/**
* Generates a string representation of this move in Standard Algebraic
* Notation
* (SAN).
*
* @param board the {@link Board} providing the context of this move
* @return the SAN string
*/
public String toSAN(Board board) { public String toSAN(Board board) {
final Piece piece = board.get(pos); final Piece piece = board.get(pos);
StringBuilder sb = new StringBuilder(8); StringBuilder sb = new StringBuilder(8);
// Piece symbol // Piece symbol
if(!(piece instanceof Pawn)) if (!(piece instanceof Pawn))
sb.append(Character.toUpperCase(piece.firstChar())); sb.append(Character.toUpperCase(piece.firstChar()));
// Position // Position
// TODO: Deconstruct position into optional file or rank // TODO: Deconstruct position into optional file or rank
// Omit position if the move is a pawn push // Omit position if the move is a pawn push
if (!(piece instanceof Pawn && xDist == 0)) sb.append(pos.toLAN()); if (!(piece instanceof Pawn && xDist == 0))
sb.append(pos.toLAN());
// Capture indicator // Capture indicator
if (board.get(dest) != null) sb.append('x'); if (board.get(dest) != null)
sb.append('x');
// Destination // Destination
sb.append(dest.toLAN()); sb.append(dest.toLAN());
@ -169,37 +287,88 @@ public class Move {
return sb.toString(); return sb.toString();
} }
/**
* @return {@code true} if the move is purely horizontal
*/
public boolean isHorizontal() { return getyDist() == 0; } public boolean isHorizontal() { return getyDist() == 0; }
/**
* @return {@code true} if the move is purely vertical
*/
public boolean isVertical() { return getxDist() == 0; } public boolean isVertical() { return getxDist() == 0; }
/**
* @return {@code true} if the move is diagonal
*/
public boolean isDiagonal() { return getxDist() == getyDist(); } public boolean isDiagonal() { return getxDist() == getyDist(); }
@Override @Override
public String toString() { return toLAN(); } public String toString() {
return toLAN();
}
@Override @Override
public int hashCode() { return Objects.hash(getDest(), getPos(), getxDist(), getxSign(), getyDist(), getySign()); } public int hashCode() {
return Objects.hash(
getDest(),
getPos(),
getxDist(),
getxSign(),
getyDist(),
getySign()
);
}
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (this == obj) return true; if (this == obj)
if (obj == null) return false; return true;
if (getClass() != obj.getClass()) return false; if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Move other = (Move) obj; Move other = (Move) obj;
return Objects.equals(getDest(), other.getDest()) && Objects.equals(getPos(), other.getPos()) && getxDist() == other.getxDist() return Objects.equals(getDest(), other.getDest()) && Objects
&& getxSign() == other.getxSign() && getyDist() == other.getyDist() && getySign() == other.getySign(); .equals(getPos(), other.getPos()) && getxDist() == other.getxDist()
&& getxSign() == other.getxSign() && getyDist() == other.getyDist()
&& getySign() == other.getySign();
} }
/**
* @return the position
*/
public Position getPos() { return pos; } public Position getPos() { return pos; }
/**
* @return the destination
*/
public Position getDest() { return dest; } public Position getDest() { return dest; }
public int getxDist() { return xDist; } /**
* @return the x distance
*/
public int getxDist() {
return xDist;
}
public int getyDist() { return yDist; } /**
* @return the y distance
*/
public int getyDist() {
return yDist;
}
public int getxSign() { return xSign; } /**
* @return the sign of the x distance
*/
public int getxSign() {
return xSign;
}
public int getySign() { return ySign; } /**
* @return the sign of the y distance
*/
public int getySign() {
return ySign;
}
} }

View File

@ -1,9 +1,6 @@
package dev.kske.chess.board; package dev.kske.chess.board;
import java.util.ArrayList; import java.util.*;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import dev.kske.chess.board.Piece.Color; import dev.kske.chess.board.Piece.Color;
@ -17,38 +14,94 @@ import dev.kske.chess.board.Piece.Color;
*/ */
public class MoveNode { public class MoveNode {
public static final int WHITE_KINGSIDE = 0, WHITE_QUEENSIDE = 1, BLACK_KINGSIDE = 2, BLACK_QUEENSIDE = 3; /**
* The index of the white kingside casting in a casting rights array.
*/
public static final int WHITE_KINGSIDE = 0;
public final Move move; /**
public final Piece capturedPiece; * The index of the white queenside castling in a castling rights array.
public final boolean[] castlingRights; */
public final Position enPassant; public static final int WHITE_QUEENSIDE = 1;
public final Color activeColor;
public final int fullmoveCounter, halfmoveClock;
private MoveNode parent; /**
private List<MoveNode> variations; * The index of the white kingside casting in a casting rights array.
*/
public static final int BLACK_KINGSIDE = 2;
/**
* The index of the white queenside castling in a castling rights array.
*/
public static final int BLACK_QUEENSIDE = 3;
/**
* The move on the board associated with this move node.
*/
public final Move move;
/**
* The piece captured by the move.
*/
public final Piece capturedPiece;
/**
* The castling rights present during the move.
*/
public final boolean[] castlingRights;
/**
* The en passant target position or {@code null} if the move is not an en
* passant move.
*/
public final Position enPassant;
/**
* The color active during the move.
*/
public final Color activeColor;
/**
* The number of moves performed since the beginning of the game.
*/
public final int fullmoveCounter;
/**
* The halfmoves performed since the last capture move or pawn move.
*/
public final int halfmoveClock;
private MoveNode parent;
private List<MoveNode> variations;
/** /**
* Creates a new {@link MoveNode}. * Creates a new {@link MoveNode}.
* *
* @param move The logged {@link Move} * @param move the logged {@link Move}
* @param capturedPiece The {@link Piece} captures by the logged {@link Move} * @param capturedPiece the {@link Piece} captures by the logged
* @param enPassant The en passant {@link Position} valid after the logged * {@link Move}
* @param castlingRights the castling rights present during the move
* @param enPassant the en passant {@link Position} valid after the
* logged
* {@link Move}, or {@code null} if there is none * {@link Move}, or {@code null} if there is none
* @param activeColor The {@link Color} active after the logged {@link Move} * @param activeColor the {@link Color} active after the logged
* @param fullmoveCounter * {@link Move}
* @param halfmoveClock * @param fullmoveCounter the number of moves made until the current move
* @param halfmoveClock the number of halfmoves since the last capture
* move or
* pawn move
*/ */
public MoveNode(Move move, Piece capturedPiece, boolean castlingRights[], Position enPassant, Color activeColor, public MoveNode(
int fullmoveCounter, int halfmoveClock) { Move move, Piece capturedPiece, boolean castlingRights[],
this.move = move; Position enPassant, Color activeColor, int fullmoveCounter,
this.capturedPiece = capturedPiece; int halfmoveClock
this.castlingRights = castlingRights; ) {
this.enPassant = enPassant; this.move = move;
this.activeColor = activeColor; this.capturedPiece = capturedPiece;
this.fullmoveCounter = fullmoveCounter; this.castlingRights = castlingRights;
this.halfmoveClock = halfmoveClock; this.enPassant = enPassant;
this.activeColor = activeColor;
this.fullmoveCounter = fullmoveCounter;
this.halfmoveClock = halfmoveClock;
} }
/** /**
@ -60,10 +113,18 @@ public class MoveNode {
* considers subsequent variations * considers subsequent variations
*/ */
public MoveNode(MoveNode other, boolean copyVariations) { public MoveNode(MoveNode other, boolean copyVariations) {
this(other.move, other.capturedPiece, other.castlingRights.clone(), other.enPassant, other.activeColor, this(
other.fullmoveCounter, other.halfmoveClock); other.move,
other.capturedPiece,
other.castlingRights.clone(),
other.enPassant,
other.activeColor,
other.fullmoveCounter,
other.halfmoveClock
);
if (copyVariations && other.variations != null) { if (copyVariations && other.variations != null) {
if (variations == null) variations = new ArrayList<>(); if (variations == null)
variations = new ArrayList<>();
for (MoveNode variation : other.variations) { for (MoveNode variation : other.variations) {
MoveNode copy = new MoveNode(variation, true); MoveNode copy = new MoveNode(variation, true);
copy.parent = this; copy.parent = this;
@ -78,7 +139,8 @@ public class MoveNode {
* @param variation The {@link MoveNode} to append to this {@link MoveNode} * @param variation The {@link MoveNode} to append to this {@link MoveNode}
*/ */
public void addVariation(MoveNode variation) { public void addVariation(MoveNode variation) {
if (variations == null) variations = new ArrayList<>(); if (variations == null)
variations = new ArrayList<>();
if (!variations.contains(variation)) { if (!variations.contains(variation)) {
variations.add(variation); variations.add(variation);
variation.parent = this; variation.parent = this;
@ -90,37 +152,76 @@ public class MoveNode {
*/ */
public List<MoveNode> getVariations() { return variations; } public List<MoveNode> getVariations() { return variations; }
/**
* @return {@code true} if this move node has any variations
*/
public boolean hasVariations() { public boolean hasVariations() {
return variations != null && variations.size() > 0; return variations != null && variations.size() > 0;
} }
/**
* @return the parent node of this move node
*/
public MoveNode getParent() { return parent; } public MoveNode getParent() { return parent; }
/**
* Sets the parent node of this move node
*
* @param parent the parent node to set
*/
public void setParent(MoveNode parent) { this.parent = parent; } public void setParent(MoveNode parent) { this.parent = parent; }
/**
* @return {@code true} if this move node has a parent
*/
public boolean hasParent() { public boolean hasParent() {
return parent != null; return parent != null;
} }
@Override
public String toString() {
return String.format(
"MoveNode[move=%s,capturedPiece=%s,castlingRights=%s,enPassant=%s,activeColor=%s,fullmoveCounter=%d,halfmoveClock=%d]",
move,
capturedPiece,
Arrays.toString(castlingRights),
enPassant,
activeColor,
fullmoveCounter,
halfmoveClock
);
}
@Override @Override
public int hashCode() { public int hashCode() {
final int prime = 31; final int prime = 31;
int result = 1; int result = 1;
result = prime * result + Arrays.hashCode(castlingRights); result = prime * result + Arrays.hashCode(castlingRights);
result = prime * result result = prime * result + Objects.hash(
+ Objects.hash(activeColor, capturedPiece, enPassant, fullmoveCounter, halfmoveClock, move); activeColor,
capturedPiece,
enPassant,
fullmoveCounter,
halfmoveClock,
move
);
return result; return result;
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (this == obj) return true; if (this == obj)
if (obj == null) return false; return true;
if (getClass() != obj.getClass()) return false; if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
MoveNode other = (MoveNode) obj; MoveNode other = (MoveNode) obj;
return activeColor == other.activeColor && Objects.equals(capturedPiece, other.capturedPiece) return activeColor == other.activeColor
&& Arrays.equals(castlingRights, other.castlingRights) && Objects.equals(enPassant, other.enPassant) && Objects.equals(capturedPiece, other.capturedPiece)
&& fullmoveCounter == other.fullmoveCounter && halfmoveClock == other.halfmoveClock && Arrays.equals(castlingRights, other.castlingRights) && Objects.equals(enPassant, other.enPassant)
&& Objects.equals(move, other.move); && fullmoveCounter == other.fullmoveCounter
&& halfmoveClock == other.halfmoveClock
&& Objects.equals(move, other.move);
} }
} }

View File

@ -13,62 +13,107 @@ import java.util.List;
*/ */
public class Pawn extends Piece { public class Pawn extends Piece {
public Pawn(Color color, Board board) { super(color, board); } /**
* Creates pawn {@link Piece}.
*
* @param color the color of this pawn
* @param board the board on which this pawn will be placed
*/
public Pawn(Color color, Board board) {
super(color, board);
}
@Override @Override
public boolean isValidMove(Move move) { public boolean isValidMove(Move move) {
boolean step = move.isVertical() && move.getyDist() == 1; boolean step = move.isVertical() && move.getyDist() == 1;
boolean doubleStep = move.isVertical() && move.getyDist() == 2; boolean doubleStep = move.isVertical() && move.getyDist() == 2;
boolean strafe = move.isDiagonal() && move.getxDist() == 1; boolean strafe = move.isDiagonal() && move.getxDist() == 1;
boolean enPassant = strafe && move.getDest().equals(board.getLog().getEnPassant()); boolean enPassant
if (getColor() == Color.WHITE) doubleStep &= move.getPos().y == 6; = strafe && move.getDest().equals(board.getLog().getEnPassant());
else doubleStep &= move.getPos().y == 1; if (getColor() == Color.WHITE)
doubleStep &= move.getPos().y == 6;
else
doubleStep &= move.getPos().y == 1;
return enPassant || (step ^ doubleStep ^ strafe) && move.getySign() == (getColor() == Color.WHITE ? -1 : 1) && isFreePath(move); return enPassant || step ^ doubleStep ^ strafe
&& move.getySign() == (getColor() == Color.WHITE ? -1 : 1)
&& isFreePath(move);
} }
@Override @Override
protected boolean isFreePath(Move move) { protected boolean isFreePath(Move move) {
// Two steps forward // Two steps forward
if (move.getyDist() == 2) if (move.getyDist() == 2)
return board.getBoardArr()[move.getPos().x][move.getDest().y - move.getySign()] == null && board.getDest(move) == null; return board.getBoardArr()[move.getPos().x][move.getDest().y
- move.getySign()] == null && board.getDest(move) == null;
// One step forward // One step forward
else if (move.getxDist() == 0) return board.getDest(move) == null; else
// Capture move if (move.getxDist() == 0)
else return board.getDest(move) != null && board.getDest(move).getColor() != getColor(); return board.getDest(move) == null;
// Capture move
else
return board.getDest(move) != null
&& board.getDest(move).getColor() != getColor();
} }
@Override @Override
protected List<Move> getPseudolegalMoves(Position pos) { protected List<Move> getPseudolegalMoves(Position pos) {
List<Move> moves = new ArrayList<>(); List<Move> moves = new ArrayList<>();
int sign = getColor() == Color.WHITE ? -1 : 1; int sign = getColor() == Color.WHITE ? -1 : 1;
boolean pawnPromotion = sign == 1 && pos.y == 6 || sign == -1 && pos.y == 1; boolean pawnPromotion
= sign == 1 && pos.y == 6 || sign == -1 && pos.y == 1;
// Strafe left // Strafe left
if (pos.x > 0) addMoveIfValid(moves, pos, new Position(pos.x - 1, pos.y + sign), pawnPromotion); if (pos.x > 0)
addMoveIfValid(
moves,
pos,
new Position(pos.x - 1, pos.y + sign),
pawnPromotion
);
// Strafe right // Strafe right
if (pos.x < 7) addMoveIfValid(moves, pos, new Position(pos.x + 1, pos.y + sign), pawnPromotion); if (pos.x < 7)
addMoveIfValid(
moves,
pos,
new Position(pos.x + 1, pos.y + sign),
pawnPromotion
);
// Step forward // Step forward
if (sign == 1 && pos.y < 7 || sign == -1 && pos.y > 0) addMoveIfValid(moves, pos, new Position(pos.x, pos.y + sign), pawnPromotion); if (sign == 1 && pos.y < 7 || sign == -1 && pos.y > 0)
addMoveIfValid(
moves,
pos,
new Position(pos.x, pos.y + sign),
pawnPromotion
);
// Double step forward // Double step forward
if (sign == 1 && pos.y == 1 || sign == -1 && pos.y == 6) addMoveIfValid(moves, pos, new Position(pos.x, pos.y + 2 * sign), pawnPromotion); if (sign == 1 && pos.y == 1 || sign == -1 && pos.y == 6)
addMoveIfValid(
moves,
pos,
new Position(pos.x, pos.y + 2 * sign),
pawnPromotion
);
// Add en passant move if necessary // Add en passant move if necessary
if (board.getLog().getEnPassant() != null) { if (board.getLog().getEnPassant() != null) {
Move move = new EnPassant(pos, board.getLog().getEnPassant()); Move move = new EnPassant(pos, board.getLog().getEnPassant());
if (move.isDiagonal() && move.getxDist() == 1) moves.add(move); if (move.isDiagonal() && move.getxDist() == 1)
moves.add(move);
} }
return moves; return moves;
} }
private void addMoveIfValid(List<Move> moves, Position pos, Position dest, boolean pawnPromotion) { private void addMoveIfValid(
List<Move> moves, Position pos, Position dest, boolean pawnPromotion
) {
Move move = new Move(pos, dest); Move move = new Move(pos, dest);
if (isFreePath(move)) { if (isFreePath(move))
if (pawnPromotion) { if (pawnPromotion)
try { try {
moves.add(new PawnPromotion(pos, dest, Queen.class)); moves.add(new PawnPromotion(pos, dest, Queen.class));
moves.add(new PawnPromotion(pos, dest, Rook.class)); moves.add(new PawnPromotion(pos, dest, Rook.class));
@ -77,8 +122,8 @@ public class Pawn extends Piece {
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
} else moves.add(move); else
} moves.add(move);
} }
@Override @Override

View File

@ -16,32 +16,78 @@ import dev.kske.chess.board.Piece.Color;
*/ */
public class PawnPromotion extends Move { public class PawnPromotion extends Move {
private final Constructor<? extends Piece> promotionPieceConstructor; private final Constructor<? extends Piece> promotionPieceConstructor;
private final char promotionPieceChar; private final char promotionPieceChar;
public PawnPromotion(Position pos, Position dest, Class<? extends Piece> promotionPieceClass) /**
throws ReflectiveOperationException, RuntimeException { * Initializes a pawn promotion move.
*
* @param pos the position of this move
* @param dest the destination of this move
* @param promotionPieceClass the class of the piece to which the pawn is
* promoted
* @throws ReflectiveOperationException if the promotion piece could not be
* instantiated
* @throws RuntimeException if the promotion piece could not be
* instantiated
*/
public PawnPromotion(
Position pos, Position dest, Class<? extends Piece> promotionPieceClass
)
throws ReflectiveOperationException, RuntimeException {
super(pos, dest); super(pos, dest);
// Cache piece constructor // Cache piece constructor
promotionPieceConstructor = promotionPieceClass.getDeclaredConstructor(Color.class, Board.class); promotionPieceConstructor = promotionPieceClass
.getDeclaredConstructor(Color.class, Board.class);
promotionPieceConstructor.setAccessible(true); promotionPieceConstructor.setAccessible(true);
// Get piece char // Get piece char
promotionPieceChar = (char) promotionPieceClass.getMethod("firstChar").invoke(promotionPieceConstructor.newInstance(null, null)); promotionPieceChar = (char) promotionPieceClass.getMethod("firstChar")
.invoke(promotionPieceConstructor.newInstance(null, null));
} }
public PawnPromotion(int xPos, int yPos, int xDest, int yDest, Class<? extends Piece> promotionPiece) /**
throws ReflectiveOperationException, RuntimeException { * Creates an instance of {@link PawnPromotion}.
this(new Position(xPos, yPos), new Position(xDest, yDest), promotionPiece); *
* @param xPos the horizontal position of this move
* @param yPos the vertical position of this move
* @param xDest the horizontal destination of this move
* @param yDest the vertical destination of this move
* @param promotionPieceClass the class of the piece to which the pawn is
* promoted
* @throws ReflectiveOperationException if the promotion piece could not be
* instantiated
* @throws RuntimeException if the promotion piece could not be
* instantiated
*/
public PawnPromotion(
int xPos, int yPos, int xDest, int yDest,
Class<? extends Piece> promotionPieceClass
)
throws ReflectiveOperationException, RuntimeException {
this(
new Position(xPos, yPos),
new Position(xDest, yDest),
promotionPieceClass
);
} }
@Override @Override
public void execute(Board board) { public void execute(Board board) {
try { try {
board.set(pos, promotionPieceConstructor.newInstance(board.get(pos).getColor(), board)); board.set(
pos,
promotionPieceConstructor.newInstance(board.get(pos).getColor(), board)
);
super.execute(board); super.execute(board);
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | SecurityException e) { } catch (
InstantiationException
| IllegalAccessException
| IllegalArgumentException
| InvocationTargetException
| SecurityException e
) {
e.printStackTrace(); e.printStackTrace();
} }
} }
@ -53,7 +99,9 @@ public class PawnPromotion extends Move {
} }
@Override @Override
public String toLAN() { return pos.toLAN() + dest.toLAN() + promotionPieceChar; } public String toLAN() {
return pos.toLAN() + dest.toLAN() + promotionPieceChar;
}
@Override @Override
public String toSAN(Board board) { public String toSAN(Board board) {
@ -63,18 +111,23 @@ public class PawnPromotion extends Move {
@Override @Override
public int hashCode() { public int hashCode() {
final int prime = 31; final int prime = 31;
int result = super.hashCode(); int result = super.hashCode();
result = prime * result + Objects.hash(promotionPieceChar, promotionPieceConstructor); result = prime * result
+ Objects.hash(promotionPieceChar, promotionPieceConstructor);
return result; return result;
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (this == obj) return true; if (this == obj)
if (!super.equals(obj)) return false; return true;
if (!(obj instanceof PawnPromotion)) return false; if (!super.equals(obj))
return false;
if (!(obj instanceof PawnPromotion))
return false;
PawnPromotion other = (PawnPromotion) obj; PawnPromotion other = (PawnPromotion) obj;
return promotionPieceChar == other.promotionPieceChar && Objects.equals(promotionPieceConstructor, other.promotionPieceConstructor); return promotionPieceChar == other.promotionPieceChar && Objects
.equals(promotionPieceConstructor, other.promotionPieceConstructor);
} }
} }

View File

@ -5,59 +5,97 @@ import java.util.List;
import java.util.Objects; import java.util.Objects;
/** /**
* Represents a piece on a board with a color.<br>
* <br>
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>Piece.java</strong><br> * File: <strong>Piece.java</strong><br>
* Created: <strong>01.07.2019</strong><br> * Created: <strong>01.07.2019</strong><br>
* *
* @since Chess v0.1-alpha * @since Chess v0.1-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
public abstract class Piece implements Cloneable { public abstract class Piece implements Cloneable {
private final Color color; private final Color color;
protected Board board; protected Board board;
/**
* Initializes a piece.
*
* @param color the color of this piece
* @param board the board on which this piece is placed
*/
public Piece(Color color, Board board) { public Piece(Color color, Board board) {
this.color = color; this.color = color;
this.board = board; this.board = board;
} }
/**
* Generated a list of legal moves this piece can make.
*
* @param pos the position of this piece
* @return a list of legal moves this piece can make
*/
public List<Move> getMoves(Position pos) { public List<Move> getMoves(Position pos) {
List<Move> moves = getPseudolegalMoves(pos); List<Move> moves = getPseudolegalMoves(pos);
for (Iterator<Move> iterator = moves.iterator(); iterator.hasNext();) { for (Iterator<Move> iterator = moves.iterator(); iterator.hasNext();) {
Move move = iterator.next(); Move move = iterator.next();
board.move(move); board.move(move);
if (board.checkCheck(getColor())) iterator.remove(); if (board.checkCheck(getColor()))
iterator.remove();
board.revert(); board.revert();
} }
return moves; return moves;
} }
/**
* Generates a list of pseudo legal moves this piece can make.
*
* @param pos the position of this piece
* @return a list of pseudo legal moves this piece can make
*/
protected abstract List<Move> getPseudolegalMoves(Position pos); protected abstract List<Move> getPseudolegalMoves(Position pos);
/**
* Checks, if a given move is valid.
*
* @param move the move to check
* @return {@code true} if the move is valid
*/
public abstract boolean isValidMove(Move move); public abstract boolean isValidMove(Move move);
/** /**
* Checks, if the squares between the position and the destination of a move are * Checks, if the squares between the position and the destination of a move
* are
* free. * free.
* *
* @param move The move to check * @param move The move to check
* @return {@true} if the path is free
*/ */
protected boolean isFreePath(Move move) { protected boolean isFreePath(Move move) {
for (int i = move.getPos().x + move.getxSign(), j = move.getPos().y + move.getySign(); i != move.getDest().x for (
|| j != move.getDest().y; i += move.getxSign(), j += move.getySign()) int i = move.getPos().x + move.getxSign(), j
if (board.getBoardArr()[i][j] != null) return false; = move.getPos().y + move.getySign();
i != move.getDest().x
|| j != move.getDest().y;
i += move.getxSign(), j += move.getySign()
)
if (board.getBoardArr()[i][j] != null)
return false;
return checkDestination(move); return checkDestination(move);
} }
/** /**
* Checks if the destination of a move is empty or a piece from the opposing * Checks if the destination of a move is empty or a piece from the opposing
* team * team
* *
* @param move The move to check * @param move The move to check
* @return {@code false} if the move's destination is from the same team * @return {@code false} if the move's destination is from the same team
*/ */
protected final boolean checkDestination(Move move) { return board.getDest(move) == null || board.getDest(move).getColor() != getColor(); } protected final boolean checkDestination(Move move) {
return board.getDest(move) == null
|| board.getDest(move).getColor() != getColor();
}
@Override @Override
public Object clone() { public Object clone() {
@ -71,32 +109,49 @@ public abstract class Piece implements Cloneable {
} }
@Override @Override
public String toString() { return getClass().getSimpleName(); } public String toString() {
return String.format("%s[color=%s]", getClass().getSimpleName(), color);
}
@Override @Override
public int hashCode() { return Objects.hash(color); } public int hashCode() {
return Objects.hash(color);
}
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (this == obj) return true; if (this == obj)
if (obj == null) return false; return true;
if (getClass() != obj.getClass()) return false; if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Piece other = (Piece) obj; Piece other = (Piece) obj;
return color == other.color; return color == other.color;
} }
/** /**
* @return the standard value of this {@link Piece} that can be used for board * @return the standard value of this {@link Piece} that can be used for
* board
* evaluation * evaluation
*/ */
public abstract int getValue(); public abstract int getValue();
/** /**
* @return The first character of this {@link Piece} in algebraic notation and * @return The first character of this {@link Piece} in algebraic notation
* and
* lower case * lower case
*/ */
public char firstChar() { return Character.toLowerCase(toString().charAt(0)); } public char firstChar() {
return Character.toLowerCase(getClass().getSimpleName().charAt(0));
}
/**
* @param firstChar the first character of a piece's name
* @return the class of the piece associated with that character or
* {@code null}
* if no piece is associated with the given character
*/
public static Class<? extends Piece> fromFirstChar(char firstChar) { public static Class<? extends Piece> fromFirstChar(char firstChar) {
switch (Character.toLowerCase(firstChar)) { switch (Character.toLowerCase(firstChar)) {
case 'k': case 'k':
@ -121,14 +176,48 @@ public abstract class Piece implements Cloneable {
*/ */
public Color getColor() { return color; } public Color getColor() { return color; }
public static enum Color { /**
* Project: <strong>Chess</strong><br>
* File: <strong>Piece.java</strong><br>
* Created: <strong>01.07.2019</strong><br>
*
* @author Kai S. K. Engelbart
* @since Chess v0.1-alpha
*/
public enum Color {
WHITE, BLACK; /**
* Represents the color of the white pieces on a board.
*/
WHITE,
public static Color fromFirstChar(char c) { return Character.toLowerCase(c) == 'w' ? WHITE : BLACK; } /**
* Represents the color of the black pieces on a board.
*/
BLACK;
public char firstChar() { return this == WHITE ? 'w' : 'b'; } /**
* @param c the first character of a color's name
* @return {@code WHITE} if the character is {@code w} or {@code W},
* else
* {@code BLACK}
*/
public static Color fromFirstChar(char c) {
return Character.toLowerCase(c) == 'w' ? WHITE : BLACK;
}
public Color opposite() { return this == WHITE ? BLACK : WHITE; } /**
* @return the first character (lower case) of this color
*/
public char firstChar() {
return this == WHITE ? 'w' : 'b';
}
/**
* @return the opposite of this color
*/
public Color opposite() {
return this == WHITE ? BLACK : WHITE;
}
} }
} }

View File

@ -4,23 +4,51 @@ package dev.kske.chess.board;
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>Position.java</strong><br> * File: <strong>Position.java</strong><br>
* Created: <strong>02.07.2019</strong><br> * Created: <strong>02.07.2019</strong><br>
* *
* @since Chess v0.1-alpha * @since Chess v0.1-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
public class Position { public class Position {
public final int x, y; /**
* The horizontal component of this position.
*/
public final int x;
/**
* The vertical component of this position.
*/
public final int y;
/**
* Initializes a position.
*
* @param x the horizontal component of this position
* @param y the vertical component of this position
*/
public Position(int x, int y) { public Position(int x, int y) {
this.x = x; this.x = x;
this.y = y; this.y = y;
} }
/**
* Constructs a position from Long Algebraic Notation (LAN)
*
* @param pos the LAN string to construct a position from
* @return the position constructed from LAN
*/
public static Position fromLAN(String pos) { public static Position fromLAN(String pos) {
return new Position(pos.charAt(0) - 97, 8 - Character.getNumericValue(pos.charAt(1))); return new Position(
pos.charAt(0) - 97,
8 - Character.getNumericValue(pos.charAt(1))
);
} }
/**
* Converts this position to Long Algebraic Notation (LAN)
*
* @return a LAN string representing this position
*/
public String toLAN() { public String toLAN() {
return String.valueOf((char) (x + 97)) + String.valueOf(8 - y); return String.valueOf((char) (x + 97)) + String.valueOf(8 - y);
} }
@ -32,21 +60,26 @@ public class Position {
@Override @Override
public int hashCode() { public int hashCode() {
final int prime = 31; final int prime = 31;
int result = 1; int result = 1;
result = prime * result + x; result = prime * result + x;
result = prime * result + y; result = prime * result + y;
return result; return result;
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (this == obj) return true; if (this == obj)
if (obj == null) return false; return true;
if (getClass() != obj.getClass()) return false; if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Position other = (Position) obj; Position other = (Position) obj;
if (x != other.x) return false; if (x != other.x)
if (y != other.y) return false; return false;
if (y != other.y)
return false;
return true; return true;
} }
} }

View File

@ -7,19 +7,26 @@ import java.util.List;
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>Queen.java</strong><br> * File: <strong>Queen.java</strong><br>
* Created: <strong>01.07.2019</strong><br> * Created: <strong>01.07.2019</strong><br>
* *
* @since Chess v0.1-alpha * @since Chess v0.1-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
public class Queen extends Piece { public class Queen extends Piece {
/**
* Creates queen {@link Piece}.
*
* @param color the color of this queen
* @param board the board on which this queen will be placed
*/
public Queen(Color color, Board board) { public Queen(Color color, Board board) {
super(color, board); super(color, board);
} }
@Override @Override
public boolean isValidMove(Move move) { public boolean isValidMove(Move move) {
return ((move.isHorizontal() || move.isVertical()) || move.isDiagonal()) && isFreePath(move); return (move.isHorizontal() || move.isVertical() || move.isDiagonal())
&& isFreePath(move);
} }
@Override @Override
@ -29,73 +36,106 @@ public class Queen extends Piece {
// Horizontal moves to the right // Horizontal moves to the right
for (int i = pos.x + 1; i < 8; i++) { for (int i = pos.x + 1; i < 8; i++) {
Move move = new Move(pos, new Position(i, pos.y)); Move move = new Move(pos, new Position(i, pos.y));
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) { if (
board.getDest(move) == null
|| board.getDest(move).getColor() != getColor()
) {
moves.add(move); moves.add(move);
if (board.getDest(move) != null) break; if (board.getDest(move) != null)
} else break; break;
} else
break;
} }
// Horizontal moves to the left // Horizontal moves to the left
for (int i = pos.x - 1; i >= 0; i--) { for (int i = pos.x - 1; i >= 0; i--) {
Move move = new Move(pos, new Position(i, pos.y)); Move move = new Move(pos, new Position(i, pos.y));
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) { if (
board.getDest(move) == null
|| board.getDest(move).getColor() != getColor()
) {
moves.add(move); moves.add(move);
if (board.getDest(move) != null) break; if (board.getDest(move) != null)
} else break; break;
} else
break;
} }
// Vertical moves to the top // Vertical moves to the top
for (int i = pos.y - 1; i >= 0; i--) { for (int i = pos.y - 1; i >= 0; i--) {
Move move = new Move(pos, new Position(pos.x, i)); Move move = new Move(pos, new Position(pos.x, i));
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) { if (
board.getDest(move) == null
|| board.getDest(move).getColor() != getColor()
) {
moves.add(move); moves.add(move);
if (board.getDest(move) != null) break; if (board.getDest(move) != null)
} else break; break;
} else
break;
} }
// Vertical moves to the bottom // Vertical moves to the bottom
for (int i = pos.y + 1; i < 8; i++) { for (int i = pos.y + 1; i < 8; i++) {
Move move = new Move(pos, new Position(pos.x, i)); Move move = new Move(pos, new Position(pos.x, i));
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) { if (
board.getDest(move) == null
|| board.getDest(move).getColor() != getColor()
) {
moves.add(move); moves.add(move);
if (board.getDest(move) != null) break; if (board.getDest(move) != null)
} else break; break;
} else
break;
} }
// Diagonal moves to the lower right // Diagonal moves to the lower right
for (int i = pos.x + 1, j = pos.y + 1; i < 8 && j < 8; i++, j++) { for (int i = pos.x + 1, j = pos.y + 1; i < 8 && j < 8; i++, j++) {
Move move = new Move(pos, new Position(i, j)); Move move = new Move(pos, new Position(i, j));
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) { if (
board.getDest(move) == null
|| board.getDest(move).getColor() != getColor()
) {
moves.add(move); moves.add(move);
if (board.getDest(move) != null) break; if (board.getDest(move) != null)
} else break; break;
} else
break;
} }
// Diagonal moves to the lower left // Diagonal moves to the lower left
for (int i = pos.x - 1, j = pos.y + 1; i >= 0 && j < 8; i--, j++) { for (int i = pos.x - 1, j = pos.y + 1; i >= 0 && j < 8; i--, j++) {
Move move = new Move(pos, new Position(i, j)); Move move = new Move(pos, new Position(i, j));
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) { if (
board.getDest(move) == null
|| board.getDest(move).getColor() != getColor()
) {
moves.add(move); moves.add(move);
if (board.getDest(move) != null) break; if (board.getDest(move) != null)
} else break; break;
} else
break;
} }
// Diagonal moves to the upper right // Diagonal moves to the upper right
for (int i = pos.x + 1, j = pos.y - 1; i < 8 && j >= 0; i++, j--) { for (int i = pos.x + 1, j = pos.y - 1; i < 8 && j >= 0; i++, j--) {
Move move = new Move(pos, new Position(i, j)); Move move = new Move(pos, new Position(i, j));
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) { if (
board.getDest(move) == null
|| board.getDest(move).getColor() != getColor()
) {
moves.add(move); moves.add(move);
if (board.getDest(move) != null) break; if (board.getDest(move) != null)
} else break; break;
} else
break;
} }
// Diagonal moves to the upper left // Diagonal moves to the upper left
for (int i = pos.x - 1, j = pos.y - 1; i >= 0 && j >= 0; i--, j--) { for (int i = pos.x - 1, j = pos.y - 1; i >= 0 && j >= 0; i--, j--) {
Move move = new Move(pos, new Position(i, j)); Move move = new Move(pos, new Position(i, j));
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) { if (
board.getDest(move) == null
|| board.getDest(move).getColor() != getColor()
) {
moves.add(move); moves.add(move);
if (board.getDest(move) != null) break; if (board.getDest(move) != null)
} else break; break;
} else
break;
} }
return moves; return moves;
} }

View File

@ -7,12 +7,18 @@ import java.util.List;
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>Rook.java</strong><br> * File: <strong>Rook.java</strong><br>
* Created: <strong>01.07.2019</strong><br> * Created: <strong>01.07.2019</strong><br>
* *
* @since Chess v0.1-alpha * @since Chess v0.1-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
public class Rook extends Piece { public class Rook extends Piece {
/**
* Creates rook {@link Piece}.
*
* @param color the color of this rook
* @param board the board on which this rook will be placed
*/
public Rook(Color color, Board board) { public Rook(Color color, Board board) {
super(color, board); super(color, board);
} }
@ -29,37 +35,54 @@ public class Rook extends Piece {
// Horizontal moves to the right // Horizontal moves to the right
for (int i = pos.x + 1; i < 8; i++) { for (int i = pos.x + 1; i < 8; i++) {
Move move = new Move(pos, new Position(i, pos.y)); Move move = new Move(pos, new Position(i, pos.y));
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) { if (
board.getDest(move) == null
|| board.getDest(move).getColor() != getColor()
) {
moves.add(move); moves.add(move);
if (board.getDest(move) != null) break; if (board.getDest(move) != null)
} else break; break;
} else
break;
} }
// Horizontal moves to the left // Horizontal moves to the left
for (int i = pos.x - 1; i >= 0; i--) { for (int i = pos.x - 1; i >= 0; i--) {
Move move = new Move(pos, new Position(i, pos.y)); Move move = new Move(pos, new Position(i, pos.y));
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) { if (
board.getDest(move) == null
|| board.getDest(move).getColor() != getColor()
) {
moves.add(move); moves.add(move);
if (board.getDest(move) != null) break; if (board.getDest(move) != null)
} else break; break;
} else
break;
} }
// Vertical moves to the top // Vertical moves to the top
for (int i = pos.y - 1; i >= 0; i--) { for (int i = pos.y - 1; i >= 0; i--) {
Move move = new Move(pos, new Position(pos.x, i)); Move move = new Move(pos, new Position(pos.x, i));
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) { if (
board.getDest(move) == null
|| board.getDest(move).getColor() != getColor()
) {
moves.add(move); moves.add(move);
if (board.getDest(move) != null) break; if (board.getDest(move) != null)
} else break; break;
} else
break;
} }
// Vertical moves to the bottom // Vertical moves to the bottom
for (int i = pos.y + 1; i < 8; i++) { for (int i = pos.y + 1; i < 8; i++) {
Move move = new Move(pos, new Position(pos.x, i)); Move move = new Move(pos, new Position(pos.x, i));
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) { if (
board.getDest(move) == null
|| board.getDest(move).getColor() != getColor()
) {
moves.add(move); moves.add(move);
if (board.getDest(move) != null) break; if (board.getDest(move) != null)
} else break; break;
} else
break;
} }
return moves; return moves;
} }

View File

@ -4,9 +4,10 @@ package dev.kske.chess.event;
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>Event.java</strong><br> * File: <strong>Event.java</strong><br>
* Created: <strong>7 Aug 2019</strong><br> * Created: <strong>7 Aug 2019</strong><br>
* *
* @since Chess v0.4-alpha * @since Chess v0.4-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @param <T> the type of the event's value
*/ */
public interface Event<T> { public interface Event<T> {

View File

@ -4,21 +4,27 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
* Dispatches {@link Event}s to various {@link Subscriber}s.<br>
* <br>
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>EventBus.java</strong><br> * File: <strong>EventBus.java</strong><br>
* Created: <strong>7 Aug 2019</strong><br> * Created: <strong>7 Aug 2019</strong><br>
* *
* @since Chess v0.4-alpha * @since Chess v0.4-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
public class EventBus { public class EventBus {
private List<Subscribable> subscribers; private List<Subscriber> subscribers;
private static EventBus instance; private static EventBus instance;
/**
* @return a singleton instance of {@link EventBus}
*/
public static EventBus getInstance() { public static EventBus getInstance() {
if (instance == null) instance = new EventBus(); if (instance == null)
instance = new EventBus();
return instance; return instance;
} }
@ -26,13 +32,28 @@ public class EventBus {
subscribers = new ArrayList<>(); subscribers = new ArrayList<>();
} }
public void register(Subscribable subscribable) { /**
* Registers a subscriber to which future events will be dispatched.
*
* @param subscribable the subscriber to register
*/
public void register(Subscriber subscribable) {
subscribers.add(subscribable); subscribers.add(subscribable);
} }
/**
* Dispatches an event to all {@Subscriber}s registered at this event bus.
*
* @param event the event to dispatch
*/
public void dispatch(Event<?> event) { public void dispatch(Event<?> event) {
subscribers.stream().filter(e -> e.supports().contains(event.getClass())).forEach(e -> e.handle(event)); subscribers.stream()
.filter(e -> e.supports().contains(event.getClass()))
.forEach(e -> e.handle(event));
} }
public List<Subscribable> getSubscribers() { return subscribers; } /**
* @return a list of all registered subscribers
*/
public List<Subscriber> getSubscribers() { return subscribers; }
} }

View File

@ -6,7 +6,7 @@ import dev.kske.chess.game.Game;
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>GameStartEvent.java</strong><br> * File: <strong>GameStartEvent.java</strong><br>
* Created: <strong>30 Oct 2019</strong><br> * Created: <strong>30 Oct 2019</strong><br>
* *
* @since Chess v0.5-alpha * @since Chess v0.5-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
@ -14,7 +14,14 @@ public class GameStartEvent implements Event<Game> {
private final Game game; private final Game game;
public GameStartEvent(Game source) { game = source; } /**
* Creates an instance of {@link GameStartEvent}.
*
* @param source the game started
*/
public GameStartEvent(Game source) {
game = source;
}
@Override @Override
public Game getData() { return game; } public Game getData() { return game; }

View File

@ -7,22 +7,31 @@ import dev.kske.chess.board.Move;
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>MoveEvent.java</strong><br> * File: <strong>MoveEvent.java</strong><br>
* Created: <strong>7 Aug 2019</strong><br> * Created: <strong>7 Aug 2019</strong><br>
* *
* @since Chess v0.4-alpha * @since Chess v0.4-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
public class MoveEvent implements Event<Move> { public class MoveEvent implements Event<Move> {
private final Move move; private final Move move;
private final BoardState boardState; private final BoardState boardState;
/**
* Creates an instance of {@link MoveEvent}.
*
* @param move the move by which the event was triggered
* @param boardState the state of the board after the move
*/
public MoveEvent(Move move, BoardState boardState) { public MoveEvent(Move move, BoardState boardState) {
this.move = move; this.move = move;
this.boardState = boardState; this.boardState = boardState;
} }
@Override @Override
public Move getData() { return move; } public Move getData() { return move; }
/**
* @return the state of the board after the move
*/
public BoardState getBoardState() { return boardState; } public BoardState getBoardState() { return boardState; }
} }

View File

@ -3,19 +3,24 @@ package dev.kske.chess.event;
import java.util.Set; import java.util.Set;
/** /**
* Implementations of this interface can register themselves at the
* {@link EventBus} and will be triggered every time an {@link Event} of a
* supported type.<br>
* <br>
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>Subscribable.java</strong><br> * File: <strong>Subscribable.java</strong><br>
* Created: <strong>7 Aug 2019</strong><br> * Created: <strong>7 Aug 2019</strong><br>
* *
* @since Chess v0.4-alpha * @since Chess v0.4-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
public interface Subscribable { public interface Subscriber {
/** /**
* Consumes an event dispatched by an event bus. * Consumes an event dispatched by an event bus.
* *
* @param event The event dispatched by the event bus, only of supported type * @param event The event dispatched by the event bus, only of supported
* type
*/ */
void handle(Event<?> event); void handle(Event<?> event);

View File

@ -4,7 +4,7 @@ package dev.kske.chess.exception;
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>ChessException.java</strong><br> * File: <strong>ChessException.java</strong><br>
* Created: <strong>22 Sep 2019</strong><br> * Created: <strong>22 Sep 2019</strong><br>
* *
* @since Chess v0.5-alpha * @since Chess v0.5-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
@ -12,14 +12,30 @@ public class ChessException extends Exception {
private static final long serialVersionUID = -2208596063548245189L; private static final long serialVersionUID = -2208596063548245189L;
/**
* Initializes chess exception.
*
* @param message the message associated with this exception
* @param cause the cause of this exception
*/
public ChessException(String message, Throwable cause) { public ChessException(String message, Throwable cause) {
super(message, cause); super(message, cause);
} }
/**
* Initializes chess exception.
*
* @param message the message associated with this exception
*/
public ChessException(String message) { public ChessException(String message) {
super(message); super(message);
} }
/**
* Initializes chess exception.
*
* @param cause the cause of this exception
*/
public ChessException(Throwable cause) { public ChessException(Throwable cause) {
super(cause); super(cause);
} }

View File

@ -1,6 +1,6 @@
package dev.kske.chess.game; package dev.kske.chess.game;
import java.util.HashMap; import java.util.EnumMap;
import java.util.Map; import java.util.Map;
import javax.swing.JOptionPane; import javax.swing.JOptionPane;
@ -23,23 +23,42 @@ import dev.kske.chess.ui.OverlayComponent;
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>Game.java</strong><br> * File: <strong>Game.java</strong><br>
* Created: <strong>06.07.2019</strong><br> * Created: <strong>06.07.2019</strong><br>
* *
* @since Chess v0.1-alpha * @since Chess v0.1-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
public class Game { public class Game {
private Map<Color, Player> players = new HashMap<>(); private Map<Color, Player> players = new EnumMap<>(Color.class);
private Board board; private Board board;
private OverlayComponent overlayComponent; private OverlayComponent overlayComponent;
private BoardComponent boardComponent; private BoardComponent boardComponent;
/**
* Initializes game with a new {@link Board}.
*
* @param boardPane the board pane which will display the newly created
* board
* @param whiteName the name of the player controlling the white pieces
* @param blackName the name of the player controlling the black pieces
*/
public Game(BoardPane boardPane, String whiteName, String blackName) { public Game(BoardPane boardPane, String whiteName, String blackName) {
board = new Board(); board = new Board();
init(boardPane, whiteName, blackName); init(boardPane, whiteName, blackName);
} }
public Game(BoardPane boardPane, String whiteName, String blackName, Board board) { /**
* Initializes game with an existing {@link Board}.
*
* @param boardPane the board pane which will display the newly created
* board
* @param whiteName the name of the player controlling the white pieces
* @param blackName the name of the player controlling the black pieces
* @param board the board on which the game will be played
*/
public Game(
BoardPane boardPane, String whiteName, String blackName, Board board
) {
this.board = board; this.board = board;
init(boardPane, whiteName, blackName); init(boardPane, whiteName, blackName);
} }
@ -47,34 +66,57 @@ public class Game {
private void init(BoardPane boardPane, String whiteName, String blackName) { private void init(BoardPane boardPane, String whiteName, String blackName) {
// Initialize / synchronize UI // Initialize / synchronize UI
overlayComponent = boardPane.getOverlayComponent(); overlayComponent = boardPane.getOverlayComponent();
boardComponent = boardPane.getBoardComponent(); boardComponent = boardPane.getBoardComponent();
boardComponent.setBoard(board); boardComponent.setBoard(board);
// Initialize players // Initialize players
players.put(Color.WHITE, getPlayer(whiteName, Color.WHITE)); players.put(Color.WHITE, getPlayer(whiteName, Color.WHITE));
players.put(Color.BLACK, getPlayer(blackName, Color.BLACK)); players.put(Color.BLACK, getPlayer(blackName, Color.BLACK));
// Initialize the game variable in each player
players.values().forEach(player -> player.setGame(this));
} }
/**
* Initializes player subclass.
*
* @param name the name of the player. {@code Natural Player} will
* initialize a
* {@link NaturalPlayer}, {@code AI Player} will initialize an
* {@link AIPlayer}. Everything else will attempt to load an
* engine
* with that name
* @param color the color of the player
* @return the instantiated player or {@code null} if the name could not be
* recognized
*/
private Player getPlayer(String name, Color color) { private Player getPlayer(String name, Color color) {
switch (name) { switch (name) {
case "Natural Player": case "Natural Player":
return new NaturalPlayer(color, overlayComponent); return new NaturalPlayer(this, color, overlayComponent);
case "AI Player": case "AI Player":
return new AIPlayer(color, 4, -10); return new AIPlayer(this, color, 4, -10);
default: default:
for (EngineInfo info : EngineUtil.getEngineInfos()) for (EngineInfo info : EngineUtil.getEngineInfos())
if (info.name.equals(name)) return new UCIPlayer(color, info.path); if (info.name.equals(name))
return new UCIPlayer(this, color, info.path);
System.err.println("Invalid player name: " + name); System.err.println("Invalid player name: " + name);
return null; return null;
} }
} }
public void onMove(Player player, Move move) { /**
if (board.getPos(move).getColor() == player.color && board.attemptMove(move)) { * Should be called once a player makes a move. Depending on the legality of
* that move and the state of the game another move might be requested from
* one
* of the players.
*
* @param player the player who generated the move
* @param move the generated move
*/
public synchronized void onMove(Player player, Move move) {
if (
board.getPos(move).getColor() == player.color
&& board.attemptMove(move)
) {
// Redraw // Redraw
boardComponent.repaint(); boardComponent.repaint();
@ -83,29 +125,46 @@ public class Game {
// Run garbage collection // Run garbage collection
System.gc(); System.gc();
BoardState boardState = board.getGameEventType(board.getDest(move).getColor().opposite()); BoardState boardState
= board.getState(board.getDest(move).getColor().opposite());
EventBus.getInstance().dispatch(new MoveEvent(move, boardState)); EventBus.getInstance().dispatch(new MoveEvent(move, boardState));
switch (boardState) { switch (boardState) {
case CHECKMATE: case CHECKMATE:
case STALEMATE: case STALEMATE:
String result = String.format("%s in %s!%n", player.color.opposite(), boardState); String result = String.format(
"%s in %s!%n",
player.color.opposite(),
boardState
);
System.out.print(result); System.out.print(result);
JOptionPane.showMessageDialog(boardComponent, result); JOptionPane.showMessageDialog(boardComponent, result);
break; break;
case CHECK: case CHECK:
System.out.printf("%s in check!%n", player.color.opposite()); System.out
.printf("%s in check!%n", player.color.opposite());
default: default:
players.get(board.getLog().getActiveColor()).requestMove(); players.get(board.getLog().getActiveColor()).requestMove();
} }
} else player.requestMove(); } else
player.requestMove();
} }
public void start() { /**
* Starts the game by requesting a move from the player of the currently
* active
* color.
*/
public synchronized void start() {
EventBus.getInstance().dispatch(new GameStartEvent(this)); EventBus.getInstance().dispatch(new GameStartEvent(this));
players.get(board.getLog().getActiveColor()).requestMove(); players.get(board.getLog().getActiveColor()).requestMove();
} }
public void reset() { /**
* Cancels move calculations, initializes the default position and clears
* the
* {@link OverlayComponent}.
*/
public synchronized void reset() {
players.values().forEach(Player::cancelMove); players.values().forEach(Player::cancelMove);
board.initDefaultPositions(); board.initDefaultPositions();
boardComponent.repaint(); boardComponent.repaint();
@ -114,19 +173,19 @@ public class Game {
} }
/** /**
* Stops the game by disconnecting its players form the UI. * Stops the game by disconnecting its players from the UI.
*/ */
public void stop() { public synchronized void stop() {
players.values().forEach(Player::disconnect); players.values().forEach(Player::disconnect);
} }
/** /**
* Assigns the players their opposite colors. * Assigns the players their opposite colors.
*/ */
public void swapColors() { public synchronized void swapColors() {
players.values().forEach(Player::cancelMove); players.values().forEach(Player::cancelMove);
Player white = players.get(Color.WHITE); Player white = players.get(Color.WHITE);
Player black = players.get(Color.BLACK); Player black = players.get(Color.BLACK);
white.setColor(Color.BLACK); white.setColor(Color.BLACK);
black.setColor(Color.WHITE); black.setColor(Color.WHITE);
players.put(Color.WHITE, black); players.put(Color.WHITE, black);

View File

@ -15,10 +15,14 @@ import dev.kske.chess.board.Position;
import dev.kske.chess.ui.OverlayComponent; import dev.kske.chess.ui.OverlayComponent;
/** /**
* Enables the user to make moves in a {@link Game} by clicking on a
* {@link Piece} and then selecting one of the highlighted positions as the move
* destination.<br>
* <br>
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>NaturalPlayer.java</strong><br> * File: <strong>NaturalPlayer.java</strong><br>
* Created: <strong>06.07.2019</strong><br> * Created: <strong>06.07.2019</strong><br>
* *
* @since Chess v0.1-alpha * @since Chess v0.1-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
@ -26,55 +30,83 @@ public class NaturalPlayer extends Player implements MouseListener {
private final OverlayComponent overlayComponent; private final OverlayComponent overlayComponent;
private boolean moveRequested; private boolean moveRequested;
private Piece selectedPiece; private Piece selectedPiece;
private List<Move> possibleMoves; private List<Move> possibleMoves;
public NaturalPlayer(Color color, OverlayComponent overlayComponent) { /**
super(color); * Creates an instance of {@link NaturalPlayer}.
this.overlayComponent = overlayComponent; *
name = "Player"; * @param game the game in which this player will be used
moveRequested = false; * @param color the piece color this player will control
* @param overlayComponent the overlay component that will be used to
* display
* possible moves to the user
*/
public NaturalPlayer(
Game game, Color color, OverlayComponent overlayComponent
) {
super(game, color);
this.overlayComponent = overlayComponent;
name = "Player";
moveRequested = false;
overlayComponent.addMouseListener(this); overlayComponent.addMouseListener(this);
} }
@Override @Override
public void requestMove() { moveRequested = true; } public void requestMove() {
moveRequested = true;
}
@Override @Override
public void cancelMove() { moveRequested = false; } public void cancelMove() {
moveRequested = false;
}
@Override @Override
public void disconnect() { overlayComponent.removeMouseListener(this); } public void disconnect() {
overlayComponent.removeMouseListener(this);
}
@Override @Override
public void mousePressed(MouseEvent evt) { public void mousePressed(MouseEvent evt) {
if (!moveRequested) return; if (!moveRequested)
return;
if (selectedPiece == null) { if (selectedPiece == null) {
// Get selected Piece // Get selected Piece
final Position pos = new Position(evt.getPoint().x / overlayComponent.getTileSize(), evt.getPoint().y / overlayComponent.getTileSize()); final Position pos = new Position(
evt.getPoint().x / overlayComponent.getTileSize(),
evt.getPoint().y / overlayComponent.getTileSize()
);
selectedPiece = board.get(pos); selectedPiece = board.get(pos);
// Check if a piece was selected // Check if a piece was selected
if (selectedPiece != null) { if (selectedPiece != null)
// Discard selection if the piece has the wrong color // Discard selection if the piece has the wrong color
if (selectedPiece.getColor() == color.opposite()) selectedPiece = null; if (selectedPiece.getColor() == color.opposite())
selectedPiece = null;
else { else {
// Generate all moves possible with the selected piece and display their // Generate all moves possible with the selected piece and
// display their
// destinations // destinations
possibleMoves = selectedPiece.getMoves(pos); possibleMoves = selectedPiece.getMoves(pos);
overlayComponent.displayDots(possibleMoves.stream().map(move -> move.getDest()).collect(Collectors.toList())); overlayComponent.displayDots(
possibleMoves.stream().map(Move::getDest).collect(Collectors.toList())
);
} }
}
} else { } else {
Position dest = new Position(evt.getPoint().x / overlayComponent.getTileSize(), evt.getPoint().y / overlayComponent.getTileSize()); Position dest = new Position(
evt.getPoint().x / overlayComponent.getTileSize(),
evt.getPoint().y / overlayComponent.getTileSize()
);
// Get all moves leading to the specified destination // Get all moves leading to the specified destination
List<Move> selectedMoves = possibleMoves.stream().filter(m -> m.getDest().equals(dest)).collect(Collectors.toList()); List<Move> selectedMoves = possibleMoves.stream()
.filter(m -> m.getDest().equals(dest))
.collect(Collectors.toList());
if (!selectedMoves.isEmpty()) { if (!selectedMoves.isEmpty()) {
Move move; Move move;
@ -82,21 +114,26 @@ public class NaturalPlayer extends Player implements MouseListener {
if (selectedMoves.size() > 1) { if (selectedMoves.size() > 1) {
// Let the user select a promotion piece // Let the user select a promotion piece
JComboBox<Move> comboBox = new JComboBox<Move>(selectedMoves.toArray(new Move[0])); JComboBox<Move> comboBox
JOptionPane.showMessageDialog(overlayComponent, comboBox, "Select a promotion", JOptionPane.QUESTION_MESSAGE); = new JComboBox<>(selectedMoves.toArray(new Move[0]));
JOptionPane.showMessageDialog(
overlayComponent,
comboBox,
"Select a promotion",
JOptionPane.QUESTION_MESSAGE
);
move = selectedMoves.get(comboBox.getSelectedIndex()); move = selectedMoves.get(comboBox.getSelectedIndex());
} else move = selectedMoves.get(0); } else
move = selectedMoves.get(0);
// Tell the game to execute the move // Tell the game to execute the move
moveRequested = false; moveRequested = false;
game.onMove(NaturalPlayer.this, move); game.onMove(NaturalPlayer.this, move);
} }
// Discard the selection // Discard the selection
overlayComponent.clearDots(); overlayComponent.clearDots();
selectedPiece = null; selectedPiece = null;
possibleMoves = null; possibleMoves = null;
} }
} }

View File

@ -1,49 +1,87 @@
package dev.kske.chess.game; package dev.kske.chess.game;
import dev.kske.chess.board.Board; import dev.kske.chess.board.Board;
import dev.kske.chess.board.Move;
import dev.kske.chess.board.Piece.Color; import dev.kske.chess.board.Piece.Color;
/** /**
* Acts as the interface between the {@link Game} class and some kind of move
* generation backend implemented as a subclass.<br>
* <br>
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>Player.java</strong><br> * File: <strong>Player.java</strong><br>
* Created: <strong>06.07.2019</strong><br> * Created: <strong>06.07.2019</strong><br>
* *
* @since Chess v0.1-alpha * @since Chess v0.1-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
public abstract class Player { public abstract class Player {
protected Game game; protected final Game game;
protected Board board; protected final Board board;
protected Color color;
protected String name;
public Player(Color color) { protected String name;
protected Color color;
/**
* Initializes the color of this player.
*
* @param game the game in which this player will be used
* @param color the piece color that this player will control
*/
public Player(Game game, Color color) {
this.game = game;
board = game.getBoard();
this.color = color; this.color = color;
} }
/**
* Initiates a move generation and reports the result to the game by calling
* {@link Game#onMove(Player, Move)}.
*/
public abstract void requestMove(); public abstract void requestMove();
/**
* Cancels the move generation process.
*/
public abstract void cancelMove(); public abstract void cancelMove();
/**
* Closes all resources required for move generation.
*/
public abstract void disconnect(); public abstract void disconnect();
/**
* @return the game in which this player is used
*/
public Game getGame() { return game; } public Game getGame() { return game; }
public void setGame(Game game) { /**
this.game = game; * @return the board on which this player is used
board = game.getBoard(); */
}
public Board getBoard() { return board; } public Board getBoard() { return board; }
public void setBoard(Board board) { this.board = board; } /**
* @return the color of pieces controlled by this player
*/
public Color getColor() { return color; } public Color getColor() { return color; }
/**
* Sets the color of pieces controlled by this player.
*
* @param color the color to set
*/
public void setColor(Color color) { this.color = color; } public void setColor(Color color) { this.color = color; }
/**
* @return the name of this player
*/
public String getName() { return name; } public String getName() { return name; }
/**
* Sets the name of this player
*
* @param name the name to set
*/
public void setName(String name) { this.name = name; } public void setName(String name) { this.name = name; }
} }

View File

@ -9,22 +9,33 @@ import dev.kske.chess.uci.UCIHandle;
import dev.kske.chess.uci.UCIListener; import dev.kske.chess.uci.UCIListener;
/** /**
* Acts as the interface between the {@link Game} class and the
* {@link dev.kske.chess.uci} package enabling an engine to make moves in a
* game.<br>
* <br>
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>UCIPlayer.java</strong><br> * File: <strong>UCIPlayer.java</strong><br>
* Created: <strong>18.07.2019</strong><br> * Created: <strong>18.07.2019</strong><br>
* *
* @since Chess v0.3-alpha * @since Chess v0.3-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
public class UCIPlayer extends Player implements UCIListener { public class UCIPlayer extends Player implements UCIListener {
private UCIHandle handle; private UCIHandle handle;
public UCIPlayer(Color color, String enginePath) { /**
super(color); * Creates an instance of {@link UCIPlayer}.
*
* @param game the game in which this player will be used
* @param color the piece color that this player will control
* @param enginePath the path to the engine executable
*/
public UCIPlayer(Game game, Color color, String enginePath) {
super(game, color);
try { try {
handle = new UCIHandle(enginePath); handle = new UCIHandle(enginePath);
handle.setListener(this); handle.registerListener(this);
handle.start(); handle.start();
} catch (IOException ex) { } catch (IOException ex) {
ex.printStackTrace(); ex.printStackTrace();

View File

@ -2,90 +2,99 @@ package dev.kske.chess.game.ai;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutionException; import java.util.concurrent.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import javax.swing.SwingUtilities; import javax.swing.SwingUtilities;
import dev.kske.chess.board.Board; import dev.kske.chess.board.Board;
import dev.kske.chess.board.Move; import dev.kske.chess.board.Move;
import dev.kske.chess.board.Piece.Color; import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.game.Game;
import dev.kske.chess.game.Player; import dev.kske.chess.game.Player;
/** /**
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>AIPlayer.java</strong><br> * File: <strong>AIPlayer.java</strong><br>
* Created: <strong>06.07.2019</strong><br> * Created: <strong>06.07.2019</strong><br>
* *
* @since Chess v0.1-alpha * @since Chess v0.1-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
public class AIPlayer extends Player { public class AIPlayer extends Player {
private int availableProcessors; private int availableProcessors;
private int maxDepth; private int maxDepth;
private int alphaBetaThreshold; private int alphaBetaThreshold;
private volatile boolean exitRequested; private volatile boolean exitRequested;
private volatile ExecutorService executor; private volatile ExecutorService executor;
public AIPlayer(Color color, int maxDepth, int alphaBetaThreshold) { /**
super(color); * Creates an instance of {@link AIPlayer}.
name = "AIPlayer"; *
availableProcessors = Runtime.getRuntime().availableProcessors(); * @param game the game in which this player will be used
this.maxDepth = maxDepth; * @param color the piece color this player will control
this.alphaBetaThreshold = alphaBetaThreshold; * @param maxDepth the maximum search depth
exitRequested = false; * @param alphaBetaThreshold the board evaluation threshold that has to be
* reached to continue searching the children of a
* move
*/
public AIPlayer(
Game game, Color color, int maxDepth, int alphaBetaThreshold
) {
super(game, color);
name = "AIPlayer";
availableProcessors = Runtime.getRuntime().availableProcessors();
this.maxDepth = maxDepth;
this.alphaBetaThreshold = alphaBetaThreshold;
} }
@Override @Override
public void requestMove() { public void requestMove() {
exitRequested = false; exitRequested = false;
/*
* Define some processing threads, split the available moves between them and
* retrieve the result after their execution.
*/
new Thread(() -> {
/*
* Get a copy of the board and the available moves.
*/
Board board = new Board(this.board, false);
List<Move> moves = board.getMoves(color);
/* // Define some processing threads, split the available moves between
* Define move processors and split the available moves between them. // them and
*/ // retrieve the result after their execution.
int numThreads = Math.min(moves.size(), availableProcessors); new Thread(() -> {
List<MoveProcessor> processors = new ArrayList<>(numThreads);
final int step = moves.size() / numThreads; // Get a copy of the board and the available moves.
int rem = moves.size() % numThreads; Board board = new Board(this.board, false);
int beginIndex = 0, endIndex = 0; List<Move> moves = board.getMoves(color);
// Define move processors and split the available moves between
// them.
int numThreads = Math.min(moves.size(), availableProcessors);
List<MoveProcessor> processors = new ArrayList<>(numThreads);
final int step = moves.size() / numThreads;
int rem = moves.size() % numThreads;
int beginIndex = 0, endIndex = 0;
for (int i = 0; i < numThreads; i++) { for (int i = 0; i < numThreads; i++) {
if (rem-- > 0) ++endIndex; if (rem-- > 0)
++endIndex;
endIndex += step; endIndex += step;
processors.add(new MoveProcessor(new Board(board, false), moves.subList(beginIndex, endIndex), color, processors
maxDepth, alphaBetaThreshold)); .add(new MoveProcessor(new Board(board, false), moves.subList(beginIndex, endIndex), color, maxDepth, alphaBetaThreshold));
beginIndex = endIndex; beginIndex = endIndex;
} }
// Execute processors, get the best result and pass it back to the
/* // Game class
* Execute processors, get the best result and pass it back to the Game class
*/
executor = Executors.newFixedThreadPool(numThreads); executor = Executors.newFixedThreadPool(numThreads);
List<ProcessingResult> results = new ArrayList<>(numThreads); List<ProcessingResult> results = new ArrayList<>(numThreads);
try { try {
List<Future<ProcessingResult>> futures = executor.invokeAll(processors); List<Future<ProcessingResult>> futures
= executor.invokeAll(processors);
for (Future<ProcessingResult> f : futures) for (Future<ProcessingResult> f : futures)
results.add(f.get()); results.add(f.get());
executor.shutdown();
} catch (InterruptedException | ExecutionException ex) { } catch (InterruptedException | ExecutionException ex) {
ex.printStackTrace(); ex.printStackTrace();
} finally {
executor.shutdown();
} }
results.sort((r1, r2) -> Integer.compare(r2.score, r1.score)); results.sort((r1, r2) -> Integer.compare(r2.score, r1.score));
if (!exitRequested) SwingUtilities.invokeLater(() -> game.onMove(this, results.get(0).move)); if (!exitRequested)
SwingUtilities
.invokeLater(() -> game.onMove(this, results.get(0).move));
}, "AIPlayer calculation setup").start(); }, "AIPlayer calculation setup").start();
} }

View File

@ -5,32 +5,26 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import dev.kske.chess.board.Bishop; import dev.kske.chess.board.*;
import dev.kske.chess.board.Board;
import dev.kske.chess.board.King;
import dev.kske.chess.board.Knight;
import dev.kske.chess.board.Move;
import dev.kske.chess.board.Pawn;
import dev.kske.chess.board.Piece;
import dev.kske.chess.board.Piece.Color; import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.board.Queen;
import dev.kske.chess.board.Rook;
/** /**
* Implements a basic minimax move search algorithm for testing purposes.<br>
* <br>
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>MoveProcessor.java</strong><br> * File: <strong>MoveProcessor.java</strong><br>
* Created: <strong>08.07.2019</strong><br> * Created: <strong>08.07.2019</strong><br>
* *
* @since Chess v0.1-alpha * @since Chess v0.1-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
public class MoveProcessor implements Callable<ProcessingResult> { public class MoveProcessor implements Callable<ProcessingResult> {
private final Board board; private final Board board;
private final List<Move> rootMoves; private final List<Move> rootMoves;
private final Color color; private final Color color;
private final int maxDepth; private final int maxDepth;
private final int alphaBetaThreshold; private final int alphaBetaThreshold;
private Move bestMove; private Move bestMove;
@ -38,42 +32,194 @@ public class MoveProcessor implements Callable<ProcessingResult> {
static { static {
positionScores = new HashMap<>(); positionScores = new HashMap<>();
positionScores.put(King.class, positionScores.put(
new int[][] { new int[] { -3, -4, -4, -5, -5, -4, -4, -3 }, new int[] { -3, -4, -4, -5, -4, -4, -4, -3 }, King.class,
new int[] { -3, -4, -4, -5, -4, -4, -4, -3 }, new int[] { -3, -4, -4, -5, -4, -4, -4, -3 }, new int[][] {
new int[] { -2, -3, -3, -2, -2, -2, -2, -1 }, new int[] { -1, -2, -2, -2, -2, -2, -2, -1 }, new int[] {
new int[] { 2, 2, 0, 0, 0, 0, 2, 2 }, new int[] { 2, 3, 1, 0, 0, 1, 3, 2 } }); -3, -4, -4, -5, -5, -4, -4, -3
positionScores.put(Queen.class, }, new int[] {
new int[][] { new int[] { -2, -1, -1, -1, -1, -1, -1, -2 }, new int[] { -1, 0, 0, 0, 0, 0, 0, -1 }, -3, -4, -4, -5, -4, -4, -4, -3
new int[] { -1, 0, 1, 1, 1, 1, 0, -1 }, new int[] { -1, 0, 1, 1, 1, 1, 0, -1 }, new int[] { 0, 0, 1, 1, 1, 1, 0, -1 }, },
new int[] { -1, 1, 1, 1, 1, 1, 0, -1 }, new int[] { -1, 0, 1, 0, 0, 0, 0, -1 }, new int[] {
new int[] { -2, -1, -1, -1, -1, -1, -1, -2 } }); -3, -4, -4, -5, -4, -4, -4, -3
positionScores.put(Rook.class, },
new int[][] { new int[] { 0, 0, 0, 0, 0, 0, 0, 0 }, new int[] { 1, 1, 1, 1, 1, 1, 1, 1 }, new int[] { -1, 0, 0, 0, 0, 0, 0, -1 }, new int[] {
new int[] { -1, 0, 0, 0, 0, 0, 0, -1 }, new int[] { -1, 0, 0, 0, 0, 0, 0, -1 }, new int[] { -1, 0, 0, 0, 0, 0, 0, -1 }, -3, -4, -4, -5, -4, -4, -4, -3
new int[] { -1, 0, 0, 0, 0, 0, 0, -1 }, new int[] { 0, 0, 0, 1, 1, 0, 0, 0 } }); },
positionScores.put(Knight.class, new int[] {
new int[][] { new int[] { -5, -4, -3, -3, -3, -3, -4, -5 }, new int[] { -4, -2, 0, 0, 0, 0, -2, -4 }, -2, -3, -3, -2, -2, -2, -2, -1
new int[] { -3, 0, 1, 2, 2, 1, 0, -3 }, new int[] { -3, 1, 2, 2, 2, 2, 1, -3 }, new int[] { -3, 0, 2, 2, 2, 2, 0, -1 }, },
new int[] { -3, 1, 1, 2, 2, 1, 1, -3 }, new int[] { -4, -2, 0, 1, 1, 0, -2, -4 }, new int[] {
new int[] { -5, -4, -3, -3, -3, -3, -4, -5 } }); -1, -2, -2, -2, -2, -2, -2, -1
positionScores.put(Bishop.class, },
new int[][] { new int[] { -2, -1, -1, -1, -1, -1, -1, 2 }, new int[] { -1, 0, 0, 0, 0, 0, 0, -1 }, new int[] {
new int[] { -1, 0, 1, 1, 1, 1, 0, -1 }, new int[] { -1, 1, 1, 1, 1, 1, 1, -1 }, new int[] { -1, 0, 1, 1, 1, 1, 0, -1 }, 2, 2, 0, 0, 0, 0, 2, 2
new int[] { -1, 1, 1, 1, 1, 1, 1, -1 }, new int[] { -1, 1, 0, 0, 0, 0, 1, -1 }, },
new int[] { -2, -1, -1, -1, -1, -1, -1, -2 } }); new int[] {
positionScores.put(Pawn.class, 2, 3, 1, 0, 0, 1, 3, 2
new int[][] { new int[] { 0, 0, 0, 0, 0, 0, 0, 0 }, new int[] { 5, 5, 5, 5, 5, 5, 5, 5 }, new int[] { 1, 1, 2, 3, 3, 2, 1, 1 }, }
new int[] { 0, 0, 1, 3, 3, 1, 0, 0 }, new int[] { 0, 0, 0, 2, 2, 0, 0, 0 }, new int[] { 0, 0, -1, 0, 0, -1, 0, 0 }, }
new int[] { 0, 1, 1, -2, -2, 1, 1, 0 }, new int[] { 0, 0, 0, 0, 0, 0, 0, 0 } }); );
positionScores.put(
Queen.class,
new int[][] {
new int[] {
-2, -1, -1, -1, -1, -1, -1, -2
}, new int[] {
-1, 0, 0, 0, 0, 0, 0, -1
},
new int[] {
-1, 0, 1, 1, 1, 1, 0, -1
},
new int[] {
-1, 0, 1, 1, 1, 1, 0, -1
},
new int[] {
0, 0, 1, 1, 1, 1, 0, -1
},
new int[] {
-1, 1, 1, 1, 1, 1, 0, -1
},
new int[] {
-1, 0, 1, 0, 0, 0, 0, -1
},
new int[] {
-2, -1, -1, -1, -1, -1, -1, -2
}
}
);
positionScores.put(
Rook.class,
new int[][] {
new int[] {
0, 0, 0, 0, 0, 0, 0, 0
}, new int[] {
1, 1, 1, 1, 1, 1, 1, 1
}, new int[] {
-1, 0, 0, 0, 0, 0, 0, -1
},
new int[] {
-1, 0, 0, 0, 0, 0, 0, -1
},
new int[] {
-1, 0, 0, 0, 0, 0, 0, -1
},
new int[] {
-1, 0, 0, 0, 0, 0, 0, -1
},
new int[] {
-1, 0, 0, 0, 0, 0, 0, -1
},
new int[] {
0, 0, 0, 1, 1, 0, 0, 0
}
}
);
positionScores.put(
Knight.class,
new int[][] {
new int[] {
-5, -4, -3, -3, -3, -3, -4, -5
}, new int[] {
-4, -2, 0, 0, 0, 0, -2, -4
},
new int[] {
-3, 0, 1, 2, 2, 1, 0, -3
},
new int[] {
-3, 1, 2, 2, 2, 2, 1, -3
},
new int[] {
-3, 0, 2, 2, 2, 2, 0, -1
},
new int[] {
-3, 1, 1, 2, 2, 1, 1, -3
},
new int[] {
-4, -2, 0, 1, 1, 0, -2, -4
},
new int[] {
-5, -4, -3, -3, -3, -3, -4, -5
}
}
);
positionScores.put(
Bishop.class,
new int[][] {
new int[] {
-2, -1, -1, -1, -1, -1, -1, 2
}, new int[] {
-1, 0, 0, 0, 0, 0, 0, -1
},
new int[] {
-1, 0, 1, 1, 1, 1, 0, -1
},
new int[] {
-1, 1, 1, 1, 1, 1, 1, -1
},
new int[] {
-1, 0, 1, 1, 1, 1, 0, -1
},
new int[] {
-1, 1, 1, 1, 1, 1, 1, -1
},
new int[] {
-1, 1, 0, 0, 0, 0, 1, -1
},
new int[] {
-2, -1, -1, -1, -1, -1, -1, -2
}
}
);
positionScores.put(
Pawn.class,
new int[][] {
new int[] {
0, 0, 0, 0, 0, 0, 0, 0
}, new int[] {
5, 5, 5, 5, 5, 5, 5, 5
}, new int[] {
1, 1, 2, 3, 3, 2, 1, 1
},
new int[] {
0, 0, 1, 3, 3, 1, 0, 0
},
new int[] {
0, 0, 0, 2, 2, 0, 0, 0
},
new int[] {
0, 0, -1, 0, 0, -1, 0, 0
},
new int[] {
0, 1, 1, -2, -2, 1, 1, 0
},
new int[] {
0, 0, 0, 0, 0, 0, 0, 0
}
}
);
} }
public MoveProcessor(Board board, List<Move> rootMoves, Color color, int maxDepth, int alphaBetaThreshold) { /**
this.board = board; * Creates an instance of {@link MoveProcessor}.
this.rootMoves = rootMoves; *
this.color = color; * @param board the board to search
this.maxDepth = maxDepth; * @param rootMoves the moves on which the search is based
this.alphaBetaThreshold = alphaBetaThreshold; * @param color the color for which to search
* @param maxDepth the maximal recursion depth to search to
* @param alphaBetaThreshold the threshold necessary to continue a search
* for a
* specific move
*/
public MoveProcessor(
Board board, List<Move> rootMoves, Color color, int maxDepth,
int alphaBetaThreshold
) {
this.board = board;
this.rootMoves = rootMoves;
this.color = color;
this.maxDepth = maxDepth;
this.alphaBetaThreshold = alphaBetaThreshold;
} }
@Override @Override
@ -86,18 +232,23 @@ public class MoveProcessor implements Callable<ProcessingResult> {
int bestValue = Integer.MIN_VALUE; int bestValue = Integer.MIN_VALUE;
for (Move move : moves) { for (Move move : moves) {
board.move(move); board.move(move);
int teamValue = evaluate(board, color); int teamValue = evaluate(board, color);
int enemyValue = evaluate(board, color.opposite()); int enemyValue = evaluate(board, color.opposite());
int valueChange = teamValue - enemyValue; int valueChange = teamValue - enemyValue;
if (depth < maxDepth && valueChange >= alphaBetaThreshold) if (depth < maxDepth && valueChange >= alphaBetaThreshold)
valueChange -= miniMax(board, board.getMoves(color.opposite()), color.opposite(), depth + 1); valueChange -= miniMax(
board,
board.getMoves(color.opposite()),
color.opposite(),
depth + 1
);
if (valueChange > bestValue) { if (valueChange > bestValue) {
bestValue = valueChange; bestValue = valueChange;
if (depth == 0) bestMove = move; if (depth == 0)
bestMove = move;
} }
board.revert(); board.revert();
} }
return bestValue; return bestValue;
@ -105,18 +256,27 @@ public class MoveProcessor implements Callable<ProcessingResult> {
/** /**
* Evaluated a board. * Evaluated a board.
* *
* @param board the board to evaluate
* @param color The color to evaluate for * @param color The color to evaluate for
* @return An positive number representing how good the position is * @return a positive number representing how good the position is
*/ */
private int evaluate(Board board, Color color) { private int evaluate(Board board, Color color) {
int score = 0; int score = 0;
for (int i = 0; i < 8; i++) for (int i = 0; i < 8; i++)
for (int j = 0; j < 8; j++) for (int j = 0; j < 8; j++)
if (board.getBoardArr()[i][j] != null && board.getBoardArr()[i][j].getColor() == color) { if (
board.getBoardArr()[i][j] != null
&& board.getBoardArr()[i][j].getColor() == color
) {
score += board.getBoardArr()[i][j].getValue(); score += board.getBoardArr()[i][j].getValue();
if (positionScores.containsKey(board.getBoardArr()[i][j].getClass())) if (
score += positionScores.get(board.getBoardArr()[i][j].getClass())[i][color == Color.WHITE ? j : 7 - j]; positionScores
.containsKey(board.getBoardArr()[i][j].getClass())
)
score += positionScores.get(
board.getBoardArr()[i][j].getClass()
)[i][color == Color.WHITE ? j : 7 - j];
} }
return score; return score;
} }

View File

@ -3,25 +3,41 @@ package dev.kske.chess.game.ai;
import dev.kske.chess.board.Move; import dev.kske.chess.board.Move;
/** /**
* Contains information about a move search performed by a chess engine.
* <br>
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>ProcessingResult.java</strong><br> * File: <strong>ProcessingResult.java</strong><br>
* Created: <strong>08.07.2019</strong><br> * Created: <strong>08.07.2019</strong><br>
* *
* @since Chess v0.1-alpha * @since Chess v0.1-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
public class ProcessingResult { public class ProcessingResult {
public final Move move; /**
public final int score; * The best move found by the search
*/
public final Move move;
/**
* The score associated with the best move
*/
public final int score;
/**
* Creates an instance of {@link ProcessingResult}.
*
* @param move the best move found by the search
* @param score the score associated with the best move
*/
public ProcessingResult(Move move, int score) { public ProcessingResult(Move move, int score) {
this.move = move; this.move = move;
this.score = score; this.score = score;
} }
@Override @Override
public String toString() { public String toString() {
return String.format("ProcessingResult[Move = %s, Score = %d]", move, score); return String
.format("ProcessingResult[Move = %s,Score = %d]", move, score);
} }
} }

View File

@ -1,11 +1,6 @@
package dev.kske.chess.io; package dev.kske.chess.io;
import java.io.FileInputStream; import java.io.*;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -14,9 +9,9 @@ import dev.kske.chess.uci.UCIListener;
/** /**
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>MenuBar.java</strong><br> * File: <strong>EngineUtil.java</strong><br>
* Created: <strong>23.07.2019</strong><br> * Created: <strong>23.07.2019</strong><br>
* *
* @since Chess v0.2-alpha * @since Chess v0.2-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @author Leon Hofmeister * @author Leon Hofmeister
@ -33,11 +28,16 @@ public class EngineUtil {
private EngineUtil() {} private EngineUtil() {}
/**
* Stores information about an engine while checking its availability.
*
* @param enginePath the path to the executable of the engine
*/
public static void addEngine(String enginePath) { public static void addEngine(String enginePath) {
try { try {
EngineInfo info = new EngineInfo(enginePath); EngineInfo info = new EngineInfo(enginePath);
UCIHandle handle = new UCIHandle(enginePath); UCIHandle handle = new UCIHandle(enginePath);
handle.setListener(new UCIListener() { handle.registerListener(new UCIListener() {
@Override @Override
public void onIdName(String name) { public void onIdName(String name) {
@ -64,29 +64,66 @@ public class EngineUtil {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private static void loadEngineInfos() { private static void loadEngineInfos() {
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(engineInfoFile))) { try (
ObjectInputStream in
= new ObjectInputStream(new FileInputStream(engineInfoFile))
) {
Object obj = in.readObject(); Object obj = in.readObject();
if (obj instanceof ArrayList<?>) engineInfos = (ArrayList<EngineInfo>) obj; if (obj instanceof ArrayList<?>)
else throw new IOException("Serialized object has the wrong class."); engineInfos = (ArrayList<EngineInfo>) obj;
else
throw new IOException("Serialized object has the wrong class.");
} catch (ClassNotFoundException | IOException ex) { } catch (ClassNotFoundException | IOException ex) {
engineInfos = new ArrayList<>(); engineInfos = new ArrayList<>();
} }
} }
private static void saveEngineInfos() { private static void saveEngineInfos() {
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(engineInfoFile))) { try (
ObjectOutputStream out
= new ObjectOutputStream(new FileOutputStream(engineInfoFile))
) {
out.writeObject(engineInfos); out.writeObject(engineInfos);
} catch (IOException ex) { } catch (IOException ex) {
ex.printStackTrace(); ex.printStackTrace();
} }
} }
/**
* Stores the name and author of an engine, as well as a path to its
* executable.<br>
* <br>
* Project: <strong>Chess</strong><br>
* File: <strong>EngineUtil.java</strong><br>
* Created: <strong>23.07.2019</strong><br>
*
* @since Chess v0.2-alpha
* @author Kai S. K. Engelbart
*/
public static class EngineInfo implements Serializable { public static class EngineInfo implements Serializable {
private static final long serialVersionUID = -474177108900833005L; private static final long serialVersionUID = -474177108900833005L;
public String path, name, author; /**
* The path to the executable of the engine
*/
public String path;
/**
* The name of the engine
*/
public String name;
/**
* The author of the engine
*/
public String author;
/**
* Creates an instance of {@link EngineInfo}.
*
* @param path the path of the engine executable
*/
public EngineInfo(String path) { public EngineInfo(String path) {
this.path = path; this.path = path;
} }
@ -97,5 +134,8 @@ public class EngineUtil {
} }
} }
/**
* @return a list of all stored engine infos
*/
public static List<EngineInfo> getEngineInfos() { return engineInfos; } public static List<EngineInfo> getEngineInfos() { return engineInfos; }
} }

View File

@ -15,13 +15,14 @@ import dev.kske.chess.board.Piece;
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>TextureUtil.java</strong><br> * File: <strong>TextureUtil.java</strong><br>
* Created: <strong>01.07.2019</strong><br> * Created: <strong>01.07.2019</strong><br>
* *
* @since Chess v0.1-alpha * @since Chess v0.1-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
public class TextureUtil { public class TextureUtil {
private static Map<String, Image> textures = new HashMap<>(), scaledTextures = new HashMap<>(); private static Map<String, Image> textures = new HashMap<>(),
scaledTextures = new HashMap<>();
static { static {
loadPieceTextures(); loadPieceTextures();
@ -32,28 +33,31 @@ public class TextureUtil {
/** /**
* Loads a piece texture fitting to a piece object. * Loads a piece texture fitting to a piece object.
* *
* @param piece The piece from which the texture properties are taken * @param piece The piece from which the texture properties are taken
* @return The fitting texture * @return The fitting texture
*/ */
public static Image getPieceTexture(Piece piece) { public static Image getPieceTexture(Piece piece) {
String key = piece.toString().toLowerCase() + "_" + piece.getColor().toString().toLowerCase(); String key = piece.getClass().getSimpleName().toLowerCase() + "_"
+ piece.getColor().toString().toLowerCase();
return scaledTextures.get(key); return scaledTextures.get(key);
} }
/** /**
* Scales all piece textures to fit the current tile size. * Scales all piece textures to fit the current tile size.
* *
* @param tileSize the new width and height of the piece textures * @param tileSize the new width and height of the piece textures
*/ */
public static void scalePieceTextures(int tileSize) { public static void scalePieceTextures(int tileSize) {
scaledTextures.clear(); scaledTextures.clear();
textures.forEach((key, img) -> scaledTextures.put(key, img.getScaledInstance(tileSize, tileSize, Image.SCALE_SMOOTH))); textures.forEach(
(key, img) -> scaledTextures.put(key, img.getScaledInstance(tileSize, tileSize, Image.SCALE_SMOOTH))
);
} }
/** /**
* Loads an image from a file in the resource folder. * Loads an image from a file in the resource folder.
* *
* @param fileName The name of the image resource * @param fileName The name of the image resource
* @return The loaded image * @return The loaded image
*/ */
@ -73,18 +77,22 @@ public class TextureUtil {
*/ */
private static void loadPieceTextures() { private static void loadPieceTextures() {
Arrays Arrays
.asList("king_white", .asList(
"king_black", "king_white",
"queen_white", "king_black",
"queen_black", "queen_white",
"rook_white", "queen_black",
"rook_black", "rook_white",
"knight_white", "rook_black",
"knight_black", "knight_white",
"bishop_white", "knight_black",
"bishop_black", "bishop_white",
"pawn_white", "bishop_black",
"pawn_black") "pawn_white",
.forEach(name -> textures.put(name, loadImage("/pieces/" + name + ".png"))); "pawn_black"
)
.forEach(
name -> textures.put(name, loadImage("/pieces/" + name + ".png"))
);
} }
} }

View File

@ -1,9 +1,6 @@
package dev.kske.chess.pgn; package dev.kske.chess.pgn;
import java.io.File; import java.io.*;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Scanner; import java.util.Scanner;
@ -17,7 +14,7 @@ import dev.kske.chess.exception.ChessException;
* <br> * <br>
* Contains a series of {@link PGNGame} objects that can be stored inside a PGN * Contains a series of {@link PGNGame} objects that can be stored inside a PGN
* file. * file.
* *
* @since Chess v0.5-alpha * @since Chess v0.5-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
@ -27,30 +24,34 @@ public class PGNDatabase {
/** /**
* Loads PGN games from a file. * Loads PGN games from a file.
* *
* @param pgnFile the file to load the games from * @param pgnFile the file to load the games from
* @throws FileNotFoundException if the specified file is not found * @throws FileNotFoundException if the specified file is not found
* @throws ChessException if an error occurs while parsing the file * @throws ChessException if an error occurs while parsing the file
*/ */
public void load(File pgnFile) throws FileNotFoundException, ChessException { public void load(File pgnFile)
Scanner sc = new Scanner(pgnFile); throws FileNotFoundException, ChessException {
while (sc.hasNext()) try (Scanner sc = new Scanner(pgnFile)) {
games.add(PGNGame.parse(sc)); while (sc.hasNext())
sc.close(); games.add(PGNGame.parse(sc));
}
} }
/** /**
* Saves PGN games to a file. * Saves PGN games to a file.
* *
* @param pgnFile the file to save the games to. * @param pgnFile the file to save the games to.
* @throws IOException if the file could not be created * @throws IOException if the file could not be created
*/ */
public void save(File pgnFile) throws IOException { public void save(File pgnFile) throws IOException {
pgnFile.getParentFile().mkdirs(); pgnFile.getParentFile().mkdirs();
PrintWriter pw = new PrintWriter(pgnFile); try (PrintWriter pw = new PrintWriter(pgnFile)) {
games.forEach(g -> g.writePGN(pw)); games.forEach(g -> g.writePGN(pw));
pw.close(); }
} }
/**
* @return all games contained inside this database
*/
public List<PGNGame> getGames() { return games; } public List<PGNGame> getGames() { return games; }
} }

View File

@ -1,12 +1,7 @@
package dev.kske.chess.pgn; package dev.kske.chess.pgn;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.util.ArrayList; import java.util.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.regex.MatchResult; import java.util.regex.MatchResult;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -14,7 +9,6 @@ import dev.kske.chess.board.Board;
import dev.kske.chess.board.FENString; import dev.kske.chess.board.FENString;
import dev.kske.chess.board.Move; import dev.kske.chess.board.Move;
import dev.kske.chess.board.Piece.Color; import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.exception.ChessException;
/** /**
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
@ -26,29 +20,53 @@ import dev.kske.chess.exception.ChessException;
*/ */
public class PGNGame { public class PGNGame {
private final Map<String, String> tagPairs = new HashMap<>(7); private final Map<String, String> tagPairs = new HashMap<>(7);
private final Board board; private final Board board;
public PGNGame() { board = new Board(); } /**
* Creates an instance of {@link PGNGame}. A new default {@link Board} will
* be
* created.
*/
public PGNGame() {
board = new Board();
}
public PGNGame(Board board) { this.board = board; } /**
* Creates an instance of {@link PGNGame}.
*
* @param board the board associated with the game
*/
public PGNGame(Board board) {
this.board = board;
}
public static PGNGame parse(Scanner sc) throws ChessException { /**
* Parses a game in {@code PGN} format from a {@link Scanner} instance
*
* @param sc the {@link Scanner} to parse the game from, which is not closed
* after this process
* @return the parsed {@link PGNGame}
*/
public static PGNGame parse(Scanner sc) {
PGNGame game = new PGNGame(); PGNGame game = new PGNGame();
MatchResult matchResult; MatchResult matchResult;
Pattern tagPairPattern = Pattern.compile("\\[(\\w+) \"(.*)\"]"), Pattern tagPairPattern = Pattern.compile("\\[(\\w+) \"(.*)\"]"),
movePattern = Pattern.compile("\\d+\\.\\s+(?:(?:(\\S+)\\s+(\\S+))|(?:O-O-O)|(?:O-O))(?:\\+{0,2}|\\#)"), movePattern = Pattern.compile(
nagPattern = Pattern.compile("(\\$\\d{1,3})*"), terminationMarkerPattern = Pattern.compile("1-0|0-1|1\\/2-1\\/2|\\*"); "\\d+\\.\\s+(?:(?:(\\S+)\\s+(\\S+))|(?:O-O-O)|(?:O-O))(?:\\+{0,2}|\\#)"
),
nagPattern = Pattern.compile("(\\$\\d{1,3})*"), terminationMarkerPattern = Pattern.compile("1-0|0-1|1\\/2-1\\/2|\\*");
// Parse tag pairs // Parse tag pairs
while (sc.findInLine(tagPairPattern) != null) { while (sc.findInLine(tagPairPattern) != null) {
matchResult = sc.match(); matchResult = sc.match();
if (matchResult.groupCount() == 2) game.setTag(matchResult.group(1), matchResult.group(2)); if (matchResult.groupCount() == 2)
else break; game.setTag(matchResult.group(1), matchResult.group(2));
else
break;
sc.nextLine(); sc.nextLine();
} }
// Parse movetext // Parse movetext
while (true) { while (true) {
// Skip NAG (Numeric Annotation Glyph) // Skip NAG (Numeric Annotation Glyph)
@ -58,20 +76,31 @@ public class PGNGame {
if (sc.findWithinHorizon(movePattern, 20) != null) { if (sc.findWithinHorizon(movePattern, 20) != null) {
matchResult = sc.match(); matchResult = sc.match();
if (matchResult.groupCount() > 0) for (int i = 1; i < matchResult.groupCount() + 1; i++) { if (matchResult.groupCount() > 0)
game.board.move(matchResult.group(i)); for (int i = 1; i < matchResult.groupCount() + 1; i++) {
System.out.println(game.getBoard().getLog().getLast().move.toLAN() + ": " + new FENString(game.board).toString()); game.board.move(matchResult.group(i));
} System.out.println(
else break; game.getBoard().getLog().getLast().move.toLAN()
} else break; + ": " + new FENString(game.board).toString()
);
}
else
break;
} else
break;
} }
// Parse game termination marker // Parse game termination marker
if (sc.findWithinHorizon(terminationMarkerPattern, 20) == null) System.err.println("Termination marker expected"); if (sc.findWithinHorizon(terminationMarkerPattern, 20) == null)
System.err.println("Termination marker expected");
return game; return game;
} }
/**
* Serializes this game to {@code PGN} format.
*
* @param pw the writer to write the game to
*/
public void writePGN(PrintWriter pw) { public void writePGN(PrintWriter pw) {
// Set the unknown result tag if no result tag is specified // Set the unknown result tag if no result tag is specified
tagPairs.putIfAbsent("Result", "*"); tagPairs.putIfAbsent("Result", "*");
@ -80,18 +109,21 @@ public class PGNGame {
tagPairs.forEach((k, v) -> pw.printf("[%s \"%s\"]%n", k, v)); tagPairs.forEach((k, v) -> pw.printf("[%s \"%s\"]%n", k, v));
// Insert newline if tags were printed // Insert newline if tags were printed
if (!tagPairs.isEmpty()) pw.println(); if (!tagPairs.isEmpty())
pw.println();
if (!board.getLog().isEmpty()) { if (!board.getLog().isEmpty()) {
// Collect SAN moves // Collect SAN moves
Board clone = new Board(board, true); Board clone = new Board(board, true);
List<String> chunks = new ArrayList<>(); List<String> chunks = new ArrayList<>();
boolean flag = true; boolean flag = true;
while (flag) { while (flag) {
Move move = clone.getLog().getLast().move; Move move = clone.getLog().getLast().move;
flag = clone.getLog().hasParent(); flag = clone.getLog().hasParent();
clone.revert(); clone.revert();
String chunk = clone.getLog().getActiveColor() == Color.WHITE ? String.format(" %d. ", clone.getLog().getFullmoveNumber()) : " "; String chunk = clone.getLog().getActiveColor() == Color.WHITE
? String.format(" %d. ", clone.getLog().getFullmoveNumber())
: " ";
chunk += move.toSAN(clone); chunk += move.toSAN(clone);
chunks.add(chunk); chunks.add(chunk);
} }
@ -100,22 +132,47 @@ public class PGNGame {
// Write movetext // Write movetext
String line = ""; String line = "";
for (String chunk : chunks) for (String chunk : chunks)
if (line.length() + chunk.length() <= 80) line += chunk; if (line.length() + chunk.length() <= 80)
line += chunk;
else { else {
pw.println(line); pw.println(line);
line = chunk; line = chunk;
} }
if (!line.isEmpty()) pw.println(line); if (!line.isEmpty())
pw.println(line);
} }
// Write game termination marker // Write game termination marker
pw.print(tagPairs.get("Result")); pw.print(tagPairs.get("Result"));
} }
public String getTag(String tagName) { return tagPairs.get(tagName); } /**
* @param tagName the name of a game tag
* @return the value of the game tag
*/
public String getTag(String tagName) {
return tagPairs.get(tagName);
}
public boolean hasTag(String tagName) { return tagPairs.containsKey(tagName); } /**
* @param tagName the name of a game tag
* @return {@code true} if the tag is present
*/
public boolean hasTag(String tagName) {
return tagPairs.containsKey(tagName);
}
public void setTag(String tagName, String tagValue) { tagPairs.put(tagName, tagValue); } /**
* Sets a game tag.
*
* @param tagName the name of the tag
* @param tagValue the value of the tag
*/
public void setTag(String tagName, String tagValue) {
tagPairs.put(tagName, tagValue);
}
/**
* @return the board associated with this game
*/
public Board getBoard() { return board; } public Board getBoard() { return board; }
} }

View File

@ -11,22 +11,33 @@ import dev.kske.chess.board.Move;
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>UCIHandle.java</strong><br> * File: <strong>UCIHandle.java</strong><br>
* Created: <strong>18.07.2019</strong><br> * Created: <strong>18.07.2019</strong><br>
* *
* @since Chess v0.3-alpha * @since Chess v0.3-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
public class UCIHandle { public class UCIHandle {
private final Process process; private final Process process;
private final PrintWriter out; private final PrintWriter out;
private final UCIReceiver receiver; private final UCIReceiver receiver;
/**
* Creates an instance of {@link UCIHandle}. The engine process is started
* and
* passed to a new {@link UCIReceiver}.
*
* @param enginePath the path to the engine executable
* @throws IOException if the engine process could not be started
*/
public UCIHandle(String enginePath) throws IOException { public UCIHandle(String enginePath) throws IOException {
process = new ProcessBuilder(enginePath).start(); process = new ProcessBuilder(enginePath).start();
out = new PrintWriter(process.getOutputStream(), true); out = new PrintWriter(process.getOutputStream(), true);
receiver = new UCIReceiver(process.getInputStream()); receiver = new UCIReceiver(process.getInputStream());
} }
/**
* Starts the {@link UCIReceiver} used to gather engine output.
*/
public void start() { public void start() {
new Thread(receiver, "UCI Receiver").start(); new Thread(receiver, "UCI Receiver").start();
uci(); uci();
@ -35,68 +46,89 @@ public class UCIHandle {
/** /**
* Tells the engine to use UCI. * Tells the engine to use UCI.
*/ */
public void uci() { out.println("uci"); } public void uci() {
out.println("uci");
}
/** /**
* Switches the debug mode of the engine on or off. * Switches the debug mode of the engine on or off.
* *
* @param debug Enables debugging if set to {@code true}, disables it otherwise * @param debug Enables debugging if set to {@code true}, disables it
* otherwise
*/ */
public void debug(boolean debug) { out.println("debug " + (debug ? "on" : "off")); } public void debug(boolean debug) {
out.println("debug " + (debug ? "on" : "off"));
}
/** /**
* Synchronized the engine with the GUI * Synchronized the engine with the GUI
*/ */
public void isready() { out.println("isready"); } public void isready() {
out.println("isready");
}
/** /**
* Signifies a button press to the engine. * Signifies a button press to the engine.
* *
* @param name The name of the button * @param name The name of the button
*/ */
public void setOption(String name) { out.println("setoption name " + name); } public void setOption(String name) {
out.println("setoption name " + name);
}
/** /**
* Changes an internal parameter of the engine. * Changes an internal parameter of the engine.
* *
* @param name The name of the parameter * @param name The name of the parameter
* @param value The value of the parameter * @param value The value of the parameter
*/ */
public void setOption(String name, String value) { out.printf("setoption name %s value %s%n", name, value); } public void setOption(String name, String value) {
out.printf("setoption name %s value %s%n", name, value);
}
/** /**
* Registers the engine * Registers the engine
* *
* @param name The name the engine should be registered with * @param name The name the engine should be registered with
* @param code The code the engine should be registered with * @param code The code the engine should be registered with
*/ */
public void register(String name, String code) { out.printf("register %s %s%n", name, code); } public void register(String name, String code) {
out.printf("register %s %s%n", name, code);
}
/** /**
* Tells the engine to postpone the registration. * Tells the engine to postpone the registration.
*/ */
public void registerLater() { out.println("register later"); } public void registerLater() {
out.println("register later");
}
/** /**
* Tells the engine that the next search will be from a different game. * Tells the engine that the next search will be from a different game.
*/ */
public void uciNewGame() { out.println("ucinewgame"); } public void uciNewGame() {
out.println("ucinewgame");
}
/** /**
* Sets up the position in its initial state. * Sets up the position in its initial state.
*/ */
public void positionStartpos() { out.println("position startpos"); } public void positionStartpos() {
out.println("position startpos");
}
/** /**
* Sets up the position described in the FEN string. * Sets up the position described in the FEN string.
* *
* @param fen FEN representation of the current board * @param fen FEN representation of the current board
*/ */
public void positionFEN(String fen) { out.println("position fen " + fen); } public void positionFEN(String fen) {
out.println("position fen " + fen);
}
/** /**
* Sets up the position described by a list of moves. * Sets up the position described by a list of moves.
* *
* @param moves the moves to execute from the starting position to reach the * @param moves the moves to execute from the starting position to reach the
* desired position * desired position
*/ */
@ -109,15 +141,19 @@ public class UCIHandle {
/** /**
* Starts calculating on the current position. * Starts calculating on the current position.
*/ */
public void go() { out.println("go"); } public void go() {
out.println("go");
}
/** /**
* Starts calculating on the current position. * Starts calculating on the current position.
* This command has multiple optional parameters which will only be included in * This command has multiple optional parameters which will only be included
* the call if they are not {@code null}, greater than zero or {@code true} for * in
* the call if they are not {@code null}, greater than zero or {@code true}
* for
* {@code searchMoves}, all integer parameters and all boolean parameters * {@code searchMoves}, all integer parameters and all boolean parameters
* respectively. * respectively.
* *
* @param searchMoves restrict the search to these moves only * @param searchMoves restrict the search to these moves only
* @param ponder start the search in ponder mode * @param ponder start the search in ponder mode
* @param wTime the amount of milliseconds left on white's clock * @param wTime the amount of milliseconds left on white's clock
@ -131,8 +167,11 @@ public class UCIHandle {
* @param moveTime the exact search time * @param moveTime the exact search time
* @param infinite search until the {@code stop} command * @param infinite search until the {@code stop} command
*/ */
public void go(List<Move> searchMoves, boolean ponder, int wTime, int bTime, int wInc, int bInc, int movesToGo, int depth, int nodes, int mate, public void go(
int moveTime, boolean infinite) { List<Move> searchMoves, boolean ponder, int wTime, int bTime, int wInc,
int bInc, int movesToGo, int depth, int nodes, int mate,
int moveTime, boolean infinite
) {
StringJoiner joiner = new StringJoiner(" "); StringJoiner joiner = new StringJoiner(" ");
joiner.add("go"); joiner.add("go");
@ -140,7 +179,8 @@ public class UCIHandle {
joiner.add("searchmoves"); joiner.add("searchmoves");
searchMoves.forEach(m -> joiner.add(m.toLAN())); searchMoves.forEach(m -> joiner.add(m.toLAN()));
} }
if (ponder) joiner.add("ponder"); if (ponder)
joiner.add("ponder");
if (wTime > 0) { if (wTime > 0) {
joiner.add("wtime"); joiner.add("wtime");
joiner.add(String.valueOf(wTime)); joiner.add(String.valueOf(wTime));
@ -177,24 +217,38 @@ public class UCIHandle {
joiner.add("movetime"); joiner.add("movetime");
joiner.add(String.valueOf(moveTime)); joiner.add(String.valueOf(moveTime));
} }
if (infinite) joiner.add("infinite"); if (infinite)
joiner.add("infinite");
out.println(joiner); out.println(joiner);
} }
/** /**
* Stops calculation as soon as possible. * Stops calculation as soon as possible.
*/ */
public void stop() { out.println("stop"); } public void stop() {
out.println("stop");
}
/** /**
* Tells the engine that the user has played the expected move. * Tells the engine that the user has played the expected move.
*/ */
public void ponderHit() { out.println("ponderhit"); } public void ponderHit() {
out.println("ponderhit");
}
/** /**
* Quits the engine process as soon as possible. * Quits the engine process as soon as possible.
*/ */
public void quit() { out.println("quit"); } public void quit() {
out.println("quit");
}
public void setListener(UCIListener listener) { receiver.addListener(listener); } /**
* Registers a UCI listener.
*
* @param listener the UCI listener to register
*/
public void registerListener(UCIListener listener) {
receiver.registerListener(listener);
}
} }

View File

@ -1,10 +1,6 @@
package dev.kske.chess.uci; package dev.kske.chess.uci;
import java.util.ArrayList; import java.util.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import dev.kske.chess.board.Move; import dev.kske.chess.board.Move;
@ -12,41 +8,51 @@ import dev.kske.chess.board.Move;
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>UCIInfo.java</strong><br> * File: <strong>UCIInfo.java</strong><br>
* Created: <strong>28.07.2019</strong><br> * Created: <strong>28.07.2019</strong><br>
* *
* @since Chess v0.3-alpha * @since Chess v0.3-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
public class UCIInfo { public class UCIInfo {
private int depth, seldepth, time, nodes, multipv, currmovenumber, hashfull, nps, tbhits, sbhits, cpuload, cpunr; private int depth, seldepth, time, nodes, multipv, currmovenumber, hashfull,
private List<Move> pv = new ArrayList<>(), refutation = new ArrayList<>(); nps, tbhits, sbhits, cpuload, cpunr;
private Map<Integer, List<Move>> currline = new HashMap<>(); private List<Move> pv = new ArrayList<>(), refutation = new ArrayList<>();
private Move currmove; private Map<Integer, List<Move>> currline = new HashMap<>();
private Score score; private Move currmove;
private String displayString; private Score score;
private String displayString;
/** /**
* Contains every parameter for the UCI info command. Helpful for parsing * Contains every parameter for the UCI info command. Helpful for parsing
* multi-value parameters. * multi-value parameters.
*/ */
private static final List<String> params = Arrays.asList("depth", private static final List<String> params = Arrays.asList(
"seldepth", "depth",
"time", "seldepth",
"nodes", "time",
"multipv", "nodes",
"currmove", "multipv",
"currmovenumber", "currmove",
"hashfull", "currmovenumber",
"nps", "hashfull",
"tbhits", "nps",
"sbhits", "tbhits",
"cpuload", "sbhits",
"string", "cpuload",
"score", "string",
"pv", "score",
"refutation", "pv",
"currline"); "refutation",
"currline"
);
/**
* Creates an instance of {@link UCIInfo} by parsing the argument list of a
* UCI
* info command generated from an engine.
*
* @param line the UCI info argument list to parse
*/
public UCIInfo(String line) { public UCIInfo(String line) {
String[] tokens = line.split(" "); String[] tokens = line.split(" ");
@ -93,7 +99,8 @@ public class UCIInfo {
displayString = tokens[++i]; displayString = tokens[++i];
break; break;
case "score": case "score":
score = new Score(line.substring(line.indexOf("score") + tokens[i].length() + 1)); score
= new Score(line.substring(line.indexOf("score") + tokens[i].length() + 1));
i += score.getLength() + 1; i += score.getLength() + 1;
break; break;
case "pv": case "pv":
@ -106,15 +113,22 @@ public class UCIInfo {
break; break;
case "currline": case "currline":
// A CPU number of 1 can be omitted // A CPU number of 1 can be omitted
final Integer cpu = tokens[i].matches("\\d+") ? Integer.valueOf(tokens[i++]) : 1; final Integer cpu = tokens[i].matches("\\d+")
? Integer.parseInt(tokens[i++])
: 1;
final ArrayList<Move> moves = new ArrayList<>(); final ArrayList<Move> moves = new ArrayList<>();
while (i < tokens.length && !params.contains(tokens[i])) while (i < tokens.length && !params.contains(tokens[i]))
moves.add(Move.fromLAN(tokens[i++])); moves.add(Move.fromLAN(tokens[i++]));
currline.put(cpu, moves); currline.put(cpu, moves);
System.err.println("The parameter 'currline' for command 'info' is not yet implemented"); System.err.println(
"The parameter 'currline' for command 'info' is not yet implemented"
);
break; break;
default: default:
System.err.printf("Unknown parameter '%s' for command 'info' found!%n", tokens[i]); System.err.printf(
"Unknown parameter '%s' for command 'info' found!%n",
tokens[i]
);
} }
} }
@ -156,15 +170,16 @@ public class UCIInfo {
public static class Score { public static class Score {
private int cp, mate; private int cp, mate;
private boolean lowerbound, upperbound; private boolean lowerbound, upperbound;
private int length; private int length;
public Score(String line) { public Score(String line) {
String[] tokens = line.split(" "); String[] tokens = line.split(" ");
int i = 0; int i = 0;
for (; i < tokens.length; i++) { for (; i < tokens.length; i++) {
if (params.contains(tokens[i])) break; if (params.contains(tokens[i]))
break;
switch (tokens[i]) { switch (tokens[i]) {
case "cp": case "cp":
cp = Integer.parseInt(tokens[++i]); cp = Integer.parseInt(tokens[++i]);
@ -179,7 +194,10 @@ public class UCIInfo {
upperbound = true; upperbound = true;
break; break;
default: default:
System.err.printf("Unknown parameter '%s' for command 'score' found!%n", tokens[i]); System.err.printf(
"Unknown parameter '%s' for command 'score' found!%n",
tokens[i]
);
} }
} }
length = i + 1; length = i + 1;

View File

@ -6,7 +6,7 @@ import dev.kske.chess.board.Move;
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>UCIListener.java</strong><br> * File: <strong>UCIListener.java</strong><br>
* Created: <strong>19.07.2019</strong><br> * Created: <strong>19.07.2019</strong><br>
* *
* @since Chess v0.3-alpha * @since Chess v0.3-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
@ -14,14 +14,14 @@ public interface UCIListener {
/** /**
* Identifies the name of the engine. * Identifies the name of the engine.
* *
* @param name The name of the engine * @param name The name of the engine
*/ */
default void onIdName(String name) {} default void onIdName(String name) {}
/** /**
* Identifies the author of the engine. * Identifies the author of the engine.
* *
* @param author The name of the engine's author * @param author The name of the engine's author
*/ */
default void onIdAuthor(String author) {} default void onIdAuthor(String author) {}
@ -38,14 +38,14 @@ public interface UCIListener {
/** /**
* The engine has stopped searching and has found the best move. * The engine has stopped searching and has found the best move.
* *
* @param move The best moves the engine has found * @param move The best moves the engine has found
*/ */
default void onBestMove(String move) {} default void onBestMove(String move) {}
/** /**
* The engine has stopped searching and has found the best move. * The engine has stopped searching and has found the best move.
* *
* @param move The best move the engine has found * @param move The best move the engine has found
* @param ponderMove The move the engine likes to ponder on * @param ponderMove The move the engine likes to ponder on
*/ */
@ -83,14 +83,14 @@ public interface UCIListener {
/** /**
* The engine sends information to the GUI. * The engine sends information to the GUI.
* *
* @param info Contains all pieces of information to be sent * @param info Contains all pieces of information to be sent
*/ */
default void onInfo(UCIInfo info) {} default void onInfo(UCIInfo info) {}
/** /**
* Tells the GUI which parameters can be changed in the engine. * Tells the GUI which parameters can be changed in the engine.
* *
* @param option Option object describing the parameter * @param option Option object describing the parameter
*/ */
default void onOption(UCIOption option) {} default void onOption(UCIOption option) {}

View File

@ -1,23 +1,20 @@
package dev.kske.chess.uci; package dev.kske.chess.uci;
import java.util.ArrayList; import java.util.*;
import java.util.Arrays;
import java.util.List;
import java.util.StringJoiner;
/** /**
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>UCIOption.java</strong><br> * File: <strong>UCIOption.java</strong><br>
* Created: <strong>22.07.2019</strong><br> * Created: <strong>22.07.2019</strong><br>
* *
* @since Chess v0.3-alpha * @since Chess v0.3-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
public class UCIOption { public class UCIOption {
private String name, defaultVal, minVal, maxVal; private String name, defaultVal, minVal, maxVal;
private GUIType type; private GUIType type;
private List<String> varList; private List<String> varList;
public UCIOption(String line) { public UCIOption(String line) {
varList = new ArrayList<>(); varList = new ArrayList<>();
@ -27,7 +24,10 @@ public class UCIOption {
switch (tokens[i]) { switch (tokens[i]) {
case "name": case "name":
StringJoiner nameJoiner = new StringJoiner(" "); StringJoiner nameJoiner = new StringJoiner(" ");
while (!Arrays.asList("type", "default", "min", "max", "var").contains(tokens[i + 1])) while (
!Arrays.asList("type", "default", "min", "max", "var")
.contains(tokens[i + 1])
)
nameJoiner.add(tokens[++i]); nameJoiner.add(tokens[++i]);
name = nameJoiner.toString(); name = nameJoiner.toString();
break; break;
@ -48,7 +48,10 @@ public class UCIOption {
varList.add(tokens[++i]); varList.add(tokens[++i]);
break; break;
default: default:
System.err.printf("Unknown parameter '%s' for command 'option' found!%n", tokens[i]); System.err.printf(
"Unknown parameter '%s' for command 'option' found!%n",
tokens[i]
);
} }
} }
@ -64,7 +67,7 @@ public class UCIOption {
public List<String> getVarList() { return varList; } public List<String> getVarList() { return varList; }
public static enum GUIType { public enum GUIType {
CHECK, SPIN, COMBO, BUTTON, STRING CHECK, SPIN, COMBO, BUTTON, STRING
} }
} }

View File

@ -1,9 +1,6 @@
package dev.kske.chess.uci; package dev.kske.chess.uci;
import java.io.BufferedReader; import java.io.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -13,7 +10,7 @@ import dev.kske.chess.board.Move;
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>UCIReceiver.java</strong><br> * File: <strong>UCIReceiver.java</strong><br>
* Created: <strong>19.07.2019</strong><br> * Created: <strong>19.07.2019</strong><br>
* *
* @since Chess v0.3-alpha * @since Chess v0.3-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
@ -23,17 +20,26 @@ public class UCIReceiver implements Runnable {
private List<UCIListener> listeners; private List<UCIListener> listeners;
/**
* Creates an instance of {@link UCIReceiver}.
*
* @param in the input stream to parse for commands generated by the engine
*/
public UCIReceiver(InputStream in) { public UCIReceiver(InputStream in) {
this.in = new BufferedReader(new InputStreamReader(in)); this.in = new BufferedReader(new InputStreamReader(in));
listeners = new ArrayList<>(); listeners = new ArrayList<>();
} }
/**
* Starts listening for UCI commands passed through the input stream.
*/
@Override @Override
public void run() { public void run() {
String line; String line;
while (!Thread.currentThread().isInterrupted()) while (!Thread.currentThread().isInterrupted())
try { try {
if ((line = in.readLine()) != null && !line.isEmpty()) parse(line); if ((line = in.readLine()) != null && !line.isEmpty())
parse(line);
} catch (IndexOutOfBoundsException ex) { } catch (IndexOutOfBoundsException ex) {
System.err.println("Too few arguments were provided!"); System.err.println("Too few arguments were provided!");
ex.printStackTrace(); ex.printStackTrace();
@ -43,8 +49,9 @@ public class UCIReceiver implements Runnable {
} }
private void parse(String line) { private void parse(String line) {
int spaceIndex = line.indexOf(' '); int spaceIndex = line.indexOf(' ');
String command = spaceIndex == -1 ? line : line.substring(0, spaceIndex); String command
= spaceIndex == -1 ? line : line.substring(0, spaceIndex);
switch (command) { switch (command) {
case "id": case "id":
parseId(line.substring(command.length() + 1)); parseId(line.substring(command.length() + 1));
@ -76,8 +83,8 @@ public class UCIReceiver implements Runnable {
} }
private void parseId(String line) { private void parseId(String line) {
String param = line.substring(0, line.indexOf(' ')); String param = line.substring(0, line.indexOf(' '));
String arg = line.substring(param.length() + 1); String arg = line.substring(param.length() + 1);
switch (param) { switch (param) {
case "name": case "name":
listeners.forEach(l -> l.onIdName(arg)); listeners.forEach(l -> l.onIdName(arg));
@ -86,17 +93,22 @@ public class UCIReceiver implements Runnable {
listeners.forEach(l -> l.onIdAuthor(arg)); listeners.forEach(l -> l.onIdAuthor(arg));
break; break;
default: default:
System.err.printf("Unknown parameter '%s' for command 'id' found!%n", param); System.err.printf(
"Unknown parameter '%s' for command 'id' found!%n",
param
);
} }
} }
private void parseBestMove(String line) { private void parseBestMove(String line) {
String[] tokens = line.split(" "); String[] tokens = line.split(" ");
String move = tokens[0]; String move = tokens[0];
// Ponder move // Ponder move
if (tokens.length == 3) listeners.forEach(l -> l.onBestMove(move, Move.fromLAN(tokens[2]))); if (tokens.length == 3)
else listeners.forEach(l -> l.onBestMove(move)); listeners.forEach(l -> l.onBestMove(move, Move.fromLAN(tokens[2])));
else
listeners.forEach(l -> l.onBestMove(move));
} }
private void parseCopyProtection(String line) { private void parseCopyProtection(String line) {
@ -111,7 +123,10 @@ public class UCIReceiver implements Runnable {
listeners.forEach(UCIListener::onCopyProtectionError); listeners.forEach(UCIListener::onCopyProtectionError);
break; break;
default: default:
System.err.printf("Unknown parameter '%s' for command 'copyprotection' found!%n", line); System.err.printf(
"Unknown parameter '%s' for command 'copyprotection' found!%n",
line
);
} }
} }
@ -127,13 +142,27 @@ public class UCIReceiver implements Runnable {
listeners.forEach(UCIListener::onRegistrationError); listeners.forEach(UCIListener::onRegistrationError);
break; break;
default: default:
System.err.printf("Unknown parameter '%s' for command 'registration' found!%n", line); System.err.printf(
"Unknown parameter '%s' for command 'registration' found!%n",
line
);
} }
} }
private void parseInfo(String line) { listeners.forEach(l -> l.onInfo(new UCIInfo(line))); } private void parseInfo(String line) {
listeners.forEach(l -> l.onInfo(new UCIInfo(line)));
}
private void parseOption(String line) { listeners.forEach(l -> l.onOption(new UCIOption((line)))); } private void parseOption(String line) {
listeners.forEach(l -> l.onOption(new UCIOption(line)));
}
public void addListener(UCIListener listener) { listeners.add(listener); } /**
* Registers a UCI listener
*
* @param listener the UCI listener to register
*/
public void registerListener(UCIListener listener) {
listeners.add(listener);
}
} }

View File

@ -1,84 +0,0 @@
package dev.kske.chess.ui;
import java.awt.Dimension;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JSpinner;
import javax.swing.SpinnerNumberModel;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>AIConfigDialog.java</strong><br>
* Created: <strong>16.07.2019</strong><br>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
@Deprecated
public class AIConfigDialog extends JDialog {
private static final long serialVersionUID = -8047984368152479992L;
private int maxDepth;
private int alphaBetaThreshold;
private boolean startGame = false;
public AIConfigDialog() {
setSize(new Dimension(337, 212));
setModal(true);
setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
setTitle("AI Configuration");
getContentPane().setLayout(null);
JSpinner spAlphaBetaThreshold = new JSpinner();
spAlphaBetaThreshold.setBounds(222, 68, 95, 28);
getContentPane().add(spAlphaBetaThreshold);
spAlphaBetaThreshold.setModel(new SpinnerNumberModel(-10, -100, 100, 5));
JSpinner spMaxDepth = new JSpinner();
spMaxDepth.setBounds(222, 6, 95, 28);
getContentPane().add(spMaxDepth);
spMaxDepth.setModel(new SpinnerNumberModel(4, 1, 10, 1));
JLabel lblAlphabetaThreshold = new JLabel("Alpha-Beta Threshold:");
lblAlphabetaThreshold.setBounds(16, 68, 194, 28);
getContentPane().add(lblAlphabetaThreshold);
JButton btnOk = new JButton("OK");
btnOk.setBounds(16, 137, 84, 28);
getContentPane().add(btnOk);
btnOk.addActionListener((evt) -> {
maxDepth = ((Integer) spMaxDepth.getValue()).intValue();
alphaBetaThreshold = ((Integer) spAlphaBetaThreshold.getValue()).intValue();
startGame = true;
dispose();
});
btnOk.setToolTipText("Start the game");
JButton btnCancel = new JButton("Cancel");
btnCancel.setBounds(222, 137, 95, 28);
getContentPane().add(btnCancel);
btnCancel.addActionListener((evt) -> dispose());
btnCancel.setToolTipText("Cancel the game start");
JLabel lblMaximalRecursionDepth = new JLabel("Maximal Recursion Depth:");
lblMaximalRecursionDepth.setBounds(16, 12, 194, 16);
getContentPane().add(lblMaximalRecursionDepth);
setLocationRelativeTo(null);
}
public int getMaxDepth() { return maxDepth; }
public void setMaxDepth(int maxDepth) { this.maxDepth = maxDepth; }
public int getAlphaBetaThreshold() { return alphaBetaThreshold; }
public void setAlphaBetaThreshold(int alphaBetaThreshold) { this.alphaBetaThreshold = alphaBetaThreshold; }
public boolean isStartGame() { return startGame; }
public void setStartGame(boolean startGame) { this.startGame = startGame; }
}

View File

@ -16,7 +16,7 @@ import dev.kske.chess.io.TextureUtil;
* A square panel for rendering the chess board. To work correctly, * A square panel for rendering the chess board. To work correctly,
* this must be added to a parent component that allows the child to decide the * this must be added to a parent component that allows the child to decide the
* size. * size.
* *
* @since Chess v0.1-alpha * @since Chess v0.1-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
@ -28,6 +28,12 @@ public class BoardComponent extends JComponent {
private Board board; private Board board;
/**
* Creates an instance of {@link BoardComponent}.
*
* @param boardPane the board pane inside which this board component is
* contained
*/
public BoardComponent(BoardPane boardPane) { public BoardComponent(BoardPane boardPane) {
this.boardPane = boardPane; this.boardPane = boardPane;
setSize(boardPane.getPreferredSize()); setSize(boardPane.getPreferredSize());
@ -37,27 +43,44 @@ public class BoardComponent extends JComponent {
protected void paintComponent(Graphics g) { protected void paintComponent(Graphics g) {
super.paintComponent(g); super.paintComponent(g);
final int tileSize = getTileSize(); final int tileSize = boardPane.getTileSize();
// Draw the board // Draw the board
g.setColor(Color.white); g.setColor(Color.white);
for (int i = 0; i < 8; i++) for (int i = 0; i < 8; i++)
for (int j = 0; j < 8; j++) { for (int j = 0; j < 8; j++) {
if (j > 0) g.setColor(g.getColor().equals(Color.white) ? Color.lightGray : Color.white); if (j > 0)
g.setColor(
g.getColor().equals(Color.white) ? Color.lightGray : Color.white
);
g.fillRect(tileSize * i, tileSize * j, tileSize, tileSize); g.fillRect(tileSize * i, tileSize * j, tileSize, tileSize);
} }
// Draw the pieces if a board is present // Draw the pieces if a board is present
if (board != null) for (int i = 0; i < 8; i++) if (board != null)
for (int j = 0; j < 8; j++) for (int i = 0; i < 8; i++)
if (board.getBoardArr()[i][j] != null) for (int j = 0; j < 8; j++)
g.drawImage(TextureUtil.getPieceTexture(board.getBoardArr()[i][j]), i * tileSize, j * tileSize, this); if (board.getBoardArr()[i][j] != null)
g.drawImage(
TextureUtil
.getPieceTexture(board.getBoardArr()[i][j]),
i * tileSize,
j * tileSize,
this
);
} }
public int getTileSize() { return boardPane.getTileSize(); } /**
* @return the board rendered by this board component
*/
public Board getBoard() { return board; } public Board getBoard() { return board; }
/**
* Sets the board rendered by this board component and repaints the
* component
*
* @param board the board rendered by this board component
*/
public void setBoard(Board board) { public void setBoard(Board board) {
this.board = board; this.board = board;
repaint(); repaint();

View File

@ -5,10 +5,13 @@ import java.awt.Dimension;
import javax.swing.JLayeredPane; import javax.swing.JLayeredPane;
/** /**
* Combines a {@link BoardComponent} and an {@link OverlayComponent} into a
* layered pane.
* <br>
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>BoardPane.java</strong><br> * File: <strong>BoardPane.java</strong><br>
* Created: <strong>08.07.2019</strong><br> * Created: <strong>08.07.2019</strong><br>
* *
* @since Chess v0.1-alpha * @since Chess v0.1-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
@ -16,14 +19,17 @@ public class BoardPane extends JLayeredPane {
private static final long serialVersionUID = -5415058382478806092L; private static final long serialVersionUID = -5415058382478806092L;
private final BoardComponent boardComponent; private final BoardComponent boardComponent;
private final OverlayComponent overlayComponent; private final OverlayComponent overlayComponent;
private int tileSize; private int tileSize;
/**
* Creates an instance of {@link BoardPane}.
*/
public BoardPane() { public BoardPane() {
boardComponent = new BoardComponent(this); boardComponent = new BoardComponent(this);
overlayComponent = new OverlayComponent(this); overlayComponent = new OverlayComponent(this);
setLayer(overlayComponent, 1); setLayer(overlayComponent, 1);
setLayout(null); setLayout(null);
@ -37,9 +43,20 @@ public class BoardPane extends JLayeredPane {
@Override @Override
public Dimension getPreferredSize() { return new Dimension(480, 480); } public Dimension getPreferredSize() { return new Dimension(480, 480); }
/**
* @return the board component contained inside this board pane
*/
public BoardComponent getBoardComponent() { return boardComponent; } public BoardComponent getBoardComponent() { return boardComponent; }
public OverlayComponent getOverlayComponent() { return overlayComponent; } /**
* @return overlay component contained inside this board pane
*/
public OverlayComponent getOverlayComponent() {
return overlayComponent;
}
/**
* @return the size of an individual board tile in pixels
*/
public int getTileSize() { return tileSize; } public int getTileSize() { return tileSize; }
} }

View File

@ -3,20 +3,12 @@ package dev.kske.chess.ui;
import java.awt.Component; import java.awt.Component;
import java.awt.Font; import java.awt.Font;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.util.*;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.prefs.Preferences; import java.util.prefs.Preferences;
import javax.swing.DefaultComboBoxModel; import javax.swing.*;
import javax.swing.JComboBox;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.filechooser.FileNameExtensionFilter; import javax.swing.filechooser.FileNameExtensionFilter;
import dev.kske.chess.io.EngineUtil; import dev.kske.chess.io.EngineUtil;
@ -36,9 +28,20 @@ public class DialogUtil {
/** /**
* Saves the last accessed folder for loading and saving game files. * Saves the last accessed folder for loading and saving game files.
*/ */
private static Preferences preferences = Preferences.userNodeForPackage(DialogUtil.class); private static Preferences preferences
= Preferences.userNodeForPackage(DialogUtil.class);
public static void showFileSelectionDialog(Component parent, Consumer<List<File>> action, Collection<FileNameExtensionFilter> filters) { /**
* Displays a parameterized file opening dialog.
*
* @param parent the parent component of the dialog
* @param action the action executed with the selected files a its argument
* @param filters the file extension filters passed to the dialog
*/
public static void showFileSelectionDialog(
Component parent, Consumer<List<File>> action,
Collection<FileNameExtensionFilter> filters
) {
JFileChooser fileChooser = createFileChooser(filters); JFileChooser fileChooser = createFileChooser(filters);
if (fileChooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) { if (fileChooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) {
action.accept(Arrays.asList(fileChooser.getSelectedFile())); action.accept(Arrays.asList(fileChooser.getSelectedFile()));
@ -46,27 +49,60 @@ public class DialogUtil {
} }
} }
public static void showFileSaveDialog(Component parent, Consumer<File> action, Collection<FileNameExtensionFilter> filters) { /**
* Displays a parameterized file saving dialog.
*
* @param parent the parent component of the dialog
* @param action the action executed with the selected file a its argument
* @param filters the file extension filters passed to the dialog
*/
public static void showFileSaveDialog(
Component parent, Consumer<File> action,
Collection<FileNameExtensionFilter> filters
) {
JFileChooser fileChooser = createFileChooser(filters); JFileChooser fileChooser = createFileChooser(filters);
if (fileChooser.showSaveDialog(parent) == JFileChooser.APPROVE_OPTION) { if (fileChooser.showSaveDialog(parent) == JFileChooser.APPROVE_OPTION) {
action.accept(new File(fileChooser.getSelectedFile().getAbsolutePath() + "." action.accept(
+ ((FileNameExtensionFilter) fileChooser.getFileFilter()).getExtensions()[0])); new File(
fileChooser.getSelectedFile().getAbsolutePath() + "."
+ ((FileNameExtensionFilter) fileChooser
.getFileFilter()).getExtensions()[0]
)
);
preferences.put("path", fileChooser.getSelectedFile().getParent()); preferences.put("path", fileChooser.getSelectedFile().getParent());
} }
} }
private static JFileChooser createFileChooser(Collection<FileNameExtensionFilter> filters) { private static JFileChooser
createFileChooser(Collection<FileNameExtensionFilter> filters) {
JFileChooser fileChooser = new JFileChooser(); JFileChooser fileChooser = new JFileChooser();
fileChooser.setCurrentDirectory(new File(preferences.get("path", System.getProperty("user.home")))); fileChooser.setCurrentDirectory(
new File(preferences.get("path", System.getProperty("user.home")))
);
fileChooser.setAcceptAllFileFilterUsed(false); fileChooser.setAcceptAllFileFilterUsed(false);
filters.forEach(fileChooser::addChoosableFileFilter); filters.forEach(fileChooser::addChoosableFileFilter);
return fileChooser; return fileChooser;
} }
public static void showGameConfigurationDialog(Component parent, BiConsumer<String, String> action) { /**
* Displays a dialog in which the user can select the player types for a
* game.<br>
* <br>
* The dialog will always display {@code Natural Player} and
* {@code AIPlayer},
* as well as all engine names stored by {@link EngineUtil}.
*
* @param parent the parent component of the dialog
* @param action the action executed with the two selected names as
* arguments
*/
public static void showGameConfigurationDialog(
Component parent, BiConsumer<String, String> action
) {
JPanel dialogPanel = new JPanel(); JPanel dialogPanel = new JPanel();
List<String> options = new ArrayList<>(Arrays.asList("Natural Player", "AI Player")); List<String> options
= new ArrayList<>(Arrays.asList("Natural Player", "AI Player"));
EngineUtil.getEngineInfos().forEach(info -> options.add(info.name)); EngineUtil.getEngineInfos().forEach(info -> options.add(info.name));
JLabel lblWhite = new JLabel("White:"); JLabel lblWhite = new JLabel("White:");
@ -89,7 +125,15 @@ public class DialogUtil {
cbBlack.setBounds(98, 36, 159, 22); cbBlack.setBounds(98, 36, 159, 22);
dialogPanel.add(cbBlack); dialogPanel.add(cbBlack);
JOptionPane.showMessageDialog(parent, dialogPanel, "Game configuration", JOptionPane.QUESTION_MESSAGE); JOptionPane.showMessageDialog(
action.accept(options.get(cbWhite.getSelectedIndex()), options.get(cbBlack.getSelectedIndex())); parent,
dialogPanel,
"Game configuration",
JOptionPane.QUESTION_MESSAGE
);
action.accept(
options.get(cbWhite.getSelectedIndex()),
options.get(cbBlack.getSelectedIndex())
);
} }
} }

View File

@ -10,10 +10,13 @@ import java.io.IOException;
import java.util.List; import java.util.List;
/** /**
* Enables drag and drop support of {@code FEN} and {@code PGN} files for the
* {@link MainWindow}.<br>
* <br>
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>GameDropTarget.java</strong><br> * File: <strong>GameDropTarget.java</strong><br>
* Created: <strong>13 Aug 2019</strong><br> * Created: <strong>13 Aug 2019</strong><br>
* *
* @since Chess v0.3-alpha * @since Chess v0.3-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
@ -21,14 +24,25 @@ public class GameDropTarget extends DropTargetAdapter {
private MainWindow mainWindow; private MainWindow mainWindow;
public GameDropTarget(MainWindow mainWindow) { this.mainWindow = mainWindow; } /**
* Creates an instance of {@link GameDropTarget}.
*
* @param mainWindow the {@link MainWindow} onto which {@code FEN} and
* {@code PGN} files can be dropped
*/
public GameDropTarget(MainWindow mainWindow) {
this.mainWindow = mainWindow;
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Override @Override
public void drop(DropTargetDropEvent evt) { public void drop(DropTargetDropEvent evt) {
try { try {
evt.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE); evt.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
mainWindow.loadFiles((List<File>) evt.getTransferable().getTransferData(DataFlavor.javaFileListFlavor)); mainWindow.loadFiles(
(List<File>) evt.getTransferable()
.getTransferData(DataFlavor.javaFileListFlavor)
);
} catch (UnsupportedFlavorException | IOException ex) { } catch (UnsupportedFlavorException | IOException ex) {
ex.printStackTrace(); ex.printStackTrace();
evt.rejectDrop(); evt.rejectDrop();

View File

@ -7,31 +7,23 @@ import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import javax.swing.DefaultListModel; import javax.swing.*;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ListSelectionModel;
import dev.kske.chess.board.BoardState; import dev.kske.chess.board.BoardState;
import dev.kske.chess.board.MoveNode; import dev.kske.chess.board.MoveNode;
import dev.kske.chess.board.Piece.Color; import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.event.Event; import dev.kske.chess.event.*;
import dev.kske.chess.event.EventBus;
import dev.kske.chess.event.GameStartEvent;
import dev.kske.chess.event.MoveEvent;
import dev.kske.chess.event.Subscribable;
import dev.kske.chess.game.Game; import dev.kske.chess.game.Game;
import dev.kske.chess.game.NaturalPlayer; import dev.kske.chess.game.NaturalPlayer;
/** /**
* The part of this application's {@link MainWindow} that displays {@link Game}s
* and other components allowing to manipulate them.<br>
* <br>
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>GamePane.java</strong><br> * File: <strong>GamePane.java</strong><br>
* Created: <strong>23.08.2019</strong><br> * Created: <strong>23.08.2019</strong><br>
* *
* @since Chess v0.4-alpha * @since Chess v0.4-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
@ -39,28 +31,37 @@ public class GamePane extends JComponent {
private static final long serialVersionUID = 4349772338239617477L; private static final long serialVersionUID = 4349772338239617477L;
private JButton btnRestart, btnSwapColors; private JButton btnRestart, btnSwapColors;
private BoardPane boardPane; private BoardPane boardPane;
private Game game; private Game game;
private Color activeColor; private Color activeColor;
private JPanel moveSelectionPanel; private JPanel moveSelectionPanel;
private JButton btnNext; private JButton btnFirst, btnPrevious, btnNext, btnLast;
private JButton btnFirst;
private JButton btnLast;
/**
* Creates an instance of {@link GamePane}.
*/
public GamePane() { public GamePane() {
activeColor = Color.WHITE; activeColor = Color.WHITE;
GridBagLayout gridBagLayout = new GridBagLayout(); GridBagLayout gridBagLayout = new GridBagLayout();
gridBagLayout.columnWidths = new int[] { 450, 1, 0 }; gridBagLayout.columnWidths = new int[] {
gridBagLayout.rowHeights = new int[] { 33, 267, 1, 0 }; 450, 1, 0
gridBagLayout.columnWeights = new double[] { 0.0, 1.0, 1.0 }; };
gridBagLayout.rowWeights = new double[] { 1.0, 1.0, 1.0, Double.MIN_VALUE }; gridBagLayout.rowHeights = new int[] {
33, 267, 1, 0
};
gridBagLayout.columnWeights = new double[] {
0.0, 1.0, 1.0
};
gridBagLayout.rowWeights = new double[] {
1.0, 1.0, 1.0, Double.MIN_VALUE
};
setLayout(gridBagLayout); setLayout(gridBagLayout);
JPanel toolPanel = new JPanel(); JPanel toolPanel = new JPanel();
btnRestart = new JButton("Restart"); btnRestart = new JButton("Restart");
btnRestart.addActionListener((evt) -> { btnRestart.addActionListener(evt -> {
if (game != null) { if (game != null) {
game.reset(); game.reset();
game.start(); game.start();
@ -68,9 +69,10 @@ public class GamePane extends JComponent {
}); });
btnSwapColors = new JButton("Play as black"); btnSwapColors = new JButton("Play as black");
btnSwapColors.addActionListener((evt) -> { btnSwapColors.addActionListener(evt -> {
game.swapColors(); game.swapColors();
btnSwapColors.setText("Play as " + activeColor.toString().toLowerCase()); btnSwapColors
.setText("Play as " + activeColor.toString().toLowerCase());
activeColor = activeColor.opposite(); activeColor = activeColor.opposite();
}); });
@ -78,71 +80,94 @@ public class GamePane extends JComponent {
toolPanel.add(btnSwapColors); toolPanel.add(btnSwapColors);
GridBagConstraints gbc_toolPanel = new GridBagConstraints(); GridBagConstraints gbc_toolPanel = new GridBagConstraints();
gbc_toolPanel.anchor = GridBagConstraints.NORTH; gbc_toolPanel.anchor = GridBagConstraints.NORTH;
gbc_toolPanel.fill = GridBagConstraints.HORIZONTAL; gbc_toolPanel.fill = GridBagConstraints.HORIZONTAL;
gbc_toolPanel.gridx = 0; gbc_toolPanel.gridx = 0;
gbc_toolPanel.gridy = 0; gbc_toolPanel.gridy = 0;
gbc_toolPanel.gridwidth = 2; gbc_toolPanel.gridwidth = 2;
add(toolPanel, gbc_toolPanel); add(toolPanel, gbc_toolPanel);
moveSelectionPanel = new JPanel(); moveSelectionPanel = new JPanel();
GridBagConstraints gbc_moveSelectionPanel = new GridBagConstraints(); GridBagConstraints gbc_moveSelectionPanel = new GridBagConstraints();
gbc_moveSelectionPanel.fill = GridBagConstraints.BOTH; gbc_moveSelectionPanel.fill = GridBagConstraints.BOTH;
gbc_moveSelectionPanel.gridx = 2; gbc_moveSelectionPanel.gridx = 2;
gbc_moveSelectionPanel.gridy = 0; gbc_moveSelectionPanel.gridy = 0;
add(moveSelectionPanel, gbc_moveSelectionPanel); add(moveSelectionPanel, gbc_moveSelectionPanel);
btnFirst = new JButton("First"); btnFirst = new JButton("First");
btnFirst.setEnabled(false); btnFirst.setEnabled(false);
moveSelectionPanel.add(btnFirst); moveSelectionPanel.add(btnFirst);
JButton btnPreviousMove = new JButton("Previous"); btnPrevious = new JButton("Previous");
btnPreviousMove.setEnabled(false); btnPrevious.addActionListener(evt -> {
moveSelectionPanel.add(btnPreviousMove); if (game != null) {
game.getBoard().selectPreviousNode();
getBoardPane().getOverlayComponent().clearArrow();
repaint();
}
});
moveSelectionPanel.add(btnPrevious);
btnNext = new JButton("Next"); btnNext = new JButton("Next");
btnNext.setEnabled(false); btnNext.addActionListener(evt -> {
if (game != null) {
int numVariations
= game.getBoard().getLog().getLast().getVariations().size();
int index;
if (numVariations == 1)
index = 1;
else
index
= Integer.parseInt(
JOptionPane
.showInputDialog("Enter the variation index.")
);
game.getBoard().selectNextNode(index);
getBoardPane().getOverlayComponent().clearArrow();
repaint();
}
});
moveSelectionPanel.add(btnNext); moveSelectionPanel.add(btnNext);
btnLast = new JButton("Last"); btnLast = new JButton("Last");
btnLast.setEnabled(false); btnLast.setEnabled(false);
moveSelectionPanel.add(btnLast); moveSelectionPanel.add(btnLast);
boardPane = new BoardPane(); boardPane = new BoardPane();
GridBagConstraints gbc_boardPane = new GridBagConstraints(); GridBagConstraints gbc_boardPane = new GridBagConstraints();
gbc_boardPane.fill = GridBagConstraints.BOTH; gbc_boardPane.fill = GridBagConstraints.BOTH;
gbc_boardPane.gridx = 0; gbc_boardPane.gridx = 0;
gbc_boardPane.gridy = 1; gbc_boardPane.gridy = 1;
add(boardPane, gbc_boardPane); add(boardPane, gbc_boardPane);
JPanel numberPanel = new JPanel(new GridLayout(8, 1)); JPanel numberPanel = new JPanel(new GridLayout(8, 1));
GridBagConstraints gbc_numberPanel = new GridBagConstraints(); GridBagConstraints gbc_numberPanel = new GridBagConstraints();
gbc_numberPanel.anchor = GridBagConstraints.WEST; gbc_numberPanel.anchor = GridBagConstraints.WEST;
gbc_numberPanel.fill = GridBagConstraints.VERTICAL; gbc_numberPanel.fill = GridBagConstraints.VERTICAL;
gbc_numberPanel.gridx = 1; gbc_numberPanel.gridx = 1;
gbc_numberPanel.gridy = 1; gbc_numberPanel.gridy = 1;
add(numberPanel, gbc_numberPanel); add(numberPanel, gbc_numberPanel);
JPanel letterPanel = new JPanel(new GridLayout(1, 8)); JPanel letterPanel = new JPanel(new GridLayout(1, 8));
GridBagConstraints gbc_letterPanel = new GridBagConstraints(); GridBagConstraints gbc_letterPanel = new GridBagConstraints();
gbc_letterPanel.anchor = GridBagConstraints.NORTH; gbc_letterPanel.anchor = GridBagConstraints.NORTH;
gbc_letterPanel.fill = GridBagConstraints.HORIZONTAL; gbc_letterPanel.fill = GridBagConstraints.HORIZONTAL;
gbc_letterPanel.gridx = 0; gbc_letterPanel.gridx = 0;
gbc_letterPanel.gridy = 2; gbc_letterPanel.gridy = 2;
add(letterPanel, gbc_letterPanel); add(letterPanel, gbc_letterPanel);
// Initialize board coordinates // Initialize board coordinates
for (int i = 0; i < 8; i++) { for (int i = 0; i < 8; i++) {
numberPanel.add(new JLabel(String.valueOf(8 - i))); numberPanel.add(new JLabel(String.valueOf(8 - i)));
JLabel letterLabel = new JLabel(String.valueOf((char) (65 + i))); JLabel letterLabel = new JLabel(String.valueOf((char) (65 + i)));
letterLabel.setHorizontalAlignment(JLabel.CENTER); letterLabel.setHorizontalAlignment(SwingConstants.CENTER);
letterPanel.add(letterLabel); letterPanel.add(letterLabel);
} }
JScrollPane scrollPane = new JScrollPane();
JScrollPane scrollPane = new JScrollPane(); GridBagConstraints gbc_scrollPane = new GridBagConstraints();
GridBagConstraints gbc_scrollPane = new GridBagConstraints(); gbc_scrollPane.fill = GridBagConstraints.BOTH;
gbc_scrollPane.fill = GridBagConstraints.BOTH; gbc_scrollPane.gridx = 2;
gbc_scrollPane.gridx = 2; gbc_scrollPane.gridy = 1;
gbc_scrollPane.gridy = 1;
add(scrollPane, gbc_scrollPane); add(scrollPane, gbc_scrollPane);
JList<MoveNode> pgnList = new JList<>(); JList<MoveNode> pgnList = new JList<>();
@ -152,27 +177,40 @@ public class GamePane extends JComponent {
pgnList.setCellRenderer(new MoveNodeRenderer()); pgnList.setCellRenderer(new MoveNodeRenderer());
scrollPane.setViewportView(pgnList); scrollPane.setViewportView(pgnList);
// Listen to moves and game (re-)starts and update the move list or disable the // Listen to moves and game (re-)starts and update the move list or
// disable the
// color switching buttons if necessary // color switching buttons if necessary
EventBus.getInstance().register(new Subscribable() { EventBus.getInstance().register(new Subscriber() {
@Override @Override
public void handle(Event<?> event) { public void handle(Event<?> event) {
if (event instanceof MoveEvent && (((MoveEvent) event).getBoardState() == BoardState.CHECKMATE if (
|| ((MoveEvent) event).getBoardState() == BoardState.STALEMATE)) event instanceof MoveEvent && (((MoveEvent) event)
.getBoardState() == BoardState.CHECKMATE
|| ((MoveEvent) event)
.getBoardState() == BoardState.STALEMATE)
)
btnSwapColors.setEnabled(false); btnSwapColors.setEnabled(false);
else if (event instanceof GameStartEvent) btnSwapColors.setEnabled( else
game.getPlayers().get(Color.WHITE) instanceof NaturalPlayer ^ game.getPlayers().get(Color.BLACK) instanceof NaturalPlayer); if (event instanceof GameStartEvent)
btnSwapColors.setEnabled(
game.getPlayers().get(Color.WHITE) instanceof NaturalPlayer ^ game.getPlayers().get(Color.BLACK) instanceof NaturalPlayer
);
if (game.getBoard().getLog() == null) return; if (game.getBoard().getLog() == null)
return;
DefaultListModel<MoveNode> model = new DefaultListModel<>(); DefaultListModel<MoveNode> model = new DefaultListModel<>();
game.getBoard().getLog().forEach(node -> model.addElement(node)); game.getBoard().getLog().forEach(model::addElement);
pgnList.setModel(model); pgnList.setModel(model);
} }
@Override @Override
public Set<Class<?>> supports() { return new HashSet<>(Arrays.asList(MoveEvent.class, GameStartEvent.class)); } public Set<Class<?>> supports() {
return new HashSet<>(
Arrays.asList(MoveEvent.class, GameStartEvent.class)
);
}
}); });
} }
@ -187,13 +225,15 @@ public class GamePane extends JComponent {
public Game getGame() { return game; } public Game getGame() { return game; }
/** /**
* Assigns a new {@link Game} instance to this game pane. If exactly one of the * Assigns a new {@link Game} instance to this game pane. If exactly one of
* the
* players is natural, color swapping functionality is enabled. * players is natural, color swapping functionality is enabled.
* *
* @param game The {@link Game} to assign to this game pane. * @param game The {@link Game} to assign to this game pane.
*/ */
public void setGame(Game game) { public void setGame(Game game) {
if (this.game != null) this.game.stop(); if (this.game != null)
this.game.stop();
this.game = game; this.game = game;
} }
} }

View File

@ -1,22 +1,15 @@
package dev.kske.chess.ui; package dev.kske.chess.ui;
import java.awt.BasicStroke; import java.awt.*;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.MouseAdapter; import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent; import java.awt.event.MouseEvent;
import javax.swing.BorderFactory; import javax.swing.*;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
import javax.swing.plaf.basic.BasicButtonUI; import javax.swing.plaf.basic.BasicButtonUI;
/** /**
* Renders the title and the closing button of a {@link JTabbedPane}.<br>
* <br>
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>GameTabComponent.java</strong><br> * File: <strong>GameTabComponent.java</strong><br>
* Created: <strong>11 Dec 2019</strong><br> * Created: <strong>11 Dec 2019</strong><br>
@ -29,9 +22,16 @@ public class GameTabComponent extends JPanel {
private static final long serialVersionUID = 9022979950018125935L; private static final long serialVersionUID = 9022979950018125935L;
/**
* Creates an instance of {@link GameTabComponent}.
*
* @param tabbedPane the tabbed pane which contains this
* {@link GameTabComponent}
*/
public GameTabComponent(JTabbedPane tabbedPane) { public GameTabComponent(JTabbedPane tabbedPane) {
super(new FlowLayout(FlowLayout.LEFT, 0, 0)); super(new FlowLayout(FlowLayout.LEFT, 0, 0));
if (tabbedPane == null) throw new NullPointerException("TabbedPane is null"); if (tabbedPane == null)
throw new NullPointerException("TabbedPane is null");
this.tabbedPane = tabbedPane; this.tabbedPane = tabbedPane;
// Create title JLabel // Create title JLabel
@ -69,13 +69,21 @@ public class GameTabComponent extends JPanel {
addMouseListener(new MouseAdapter() { addMouseListener(new MouseAdapter() {
@Override @Override
public void mouseEntered(MouseEvent evt) { setBorderPainted(true); } public void mouseEntered(MouseEvent evt) {
setBorderPainted(true);
}
@Override @Override
public void mouseExited(MouseEvent evt) { setBorderPainted(false); } public void mouseExited(MouseEvent evt) {
setBorderPainted(false);
}
}); });
setRolloverEnabled(true); setRolloverEnabled(true);
addActionListener((evt) -> { int i = tabbedPane.indexOfTabComponent(GameTabComponent.this); if (i != -1) tabbedPane.remove(i); }); addActionListener(evt -> {
int i = tabbedPane.indexOfTabComponent(GameTabComponent.this);
if (i != -1)
tabbedPane.remove(i);
});
} }
@Override @Override
@ -86,13 +94,25 @@ public class GameTabComponent extends JPanel {
super.paintComponent(g); super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create(); Graphics2D g2 = (Graphics2D) g.create();
// shift the image for pressed buttons // shift the image for pressed buttons
if (getModel().isPressed()) { g2.translate(1, 1); } if (getModel().isPressed())
g2.translate(1, 1);
g2.setStroke(new BasicStroke(2)); g2.setStroke(new BasicStroke(2));
g2.setColor(Color.BLACK); g2.setColor(Color.BLACK);
if (getModel().isRollover()) { g2.setColor(Color.MAGENTA); } if (getModel().isRollover())
g2.setColor(Color.MAGENTA);
final int delta = 6; final int delta = 6;
g2.drawLine(delta, delta, getWidth() - delta - 1, getHeight() - delta - 1); g2.drawLine(
g2.drawLine(getWidth() - delta - 1, delta, delta, getHeight() - delta - 1); delta,
delta,
getWidth() - delta - 1,
getHeight() - delta - 1
);
g2.drawLine(
getWidth() - delta - 1,
delta,
delta,
getHeight() - delta - 1
);
g2.dispose(); g2.dispose();
} }
} }

View File

@ -1,7 +1,6 @@
package dev.kske.chess.ui; package dev.kske.chess.ui;
import java.awt.Desktop; import java.awt.Desktop;
import java.awt.EventQueue;
import java.awt.Toolkit; import java.awt.Toolkit;
import java.awt.dnd.DropTarget; import java.awt.dnd.DropTarget;
import java.io.File; import java.io.File;
@ -10,10 +9,7 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.List; import java.util.List;
import javax.swing.JComboBox; import javax.swing.*;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JTabbedPane;
import dev.kske.chess.board.Board; import dev.kske.chess.board.Board;
import dev.kske.chess.board.FENString; import dev.kske.chess.board.FENString;
@ -32,45 +28,36 @@ import dev.kske.chess.pgn.PGNGame;
*/ */
public class MainWindow extends JFrame { public class MainWindow extends JFrame {
private static final long serialVersionUID = -3100939302567978977L; private JTabbedPane tabbedPane = new JTabbedPane();
private JTabbedPane tabbedPane; private static final long serialVersionUID = -3100939302567978977L;
/** /**
* Launch the application. * Launch the application.
*
* @param args command line arguments are ignored
*/ */
public static void main(String[] args) { public static void main(String[] args) {
EventQueue.invokeLater(() -> { SwingUtilities.invokeLater(MainWindow::new);
try {
new MainWindow();
} catch (Exception ex) {
ex.printStackTrace();
}
});
} }
/** /**
* Create the application. * Create the application.
*/ */
public MainWindow() { private MainWindow() {
super("Chess by Kai S. K. Engelbart"); super("Chess by Kai S. K. Engelbart");
initialize();
}
/**
* Initialize the contents of the frame.
*/
private void initialize() {
// Configure frame // Configure frame
setResizable(false); setResizable(false);
setBounds(100, 100, 494, 565); setBounds(100, 100, 494, 565);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
setIconImage(Toolkit.getDefaultToolkit().getImage(getClass().getResource("/pieces/queen_white.png"))); setIconImage(
Toolkit.getDefaultToolkit()
.getImage(getClass().getResource("/pieces/queen_white.png"))
);
// Add frame content // Add tabbed pane, menu bar and drop target
tabbedPane = new JTabbedPane();
getContentPane().add(tabbedPane); getContentPane().add(tabbedPane);
setJMenuBar(new MenuBar(this)); setJMenuBar(new MenuBar(this));
new DropTarget(this, new GameDropTarget(this)); new DropTarget(this, new GameDropTarget(this));
@ -81,40 +68,64 @@ public class MainWindow extends JFrame {
} }
/** /**
* @return The currently selected {@link GamePane} component * @return the currently selected {@link GamePane} component
*/ */
public GamePane getSelectedGamePane() { return (GamePane) tabbedPane.getSelectedComponent(); } public GamePane getSelectedGamePane() {
return (GamePane) tabbedPane.getSelectedComponent();
}
/** /**
* Creates a new {@link GamePane}, adds it to the tabbed pane and opens it. * Creates a new {@link GamePane}, adds it to the tabbed pane and opens it.
* The new tab has the title {@code Game n} where {@code n} is its number. * The new tab has the title {@code Game n} where {@code n} is its number.
* *
* @return The new {@link GamePane} * @return the new {@link GamePane}
*/ */
public GamePane addGamePane() { return addGamePane("Game " + (tabbedPane.getTabCount() + 1)); } public GamePane addGamePane() {
return addGamePane("Game " + (tabbedPane.getTabCount() + 1));
}
/** /**
* Creates a new {@link GamePane}, adds it to the tabbed pane and opens it. * Creates a new {@link GamePane}, adds it to the tabbed pane and opens it.
* *
* @param title The title of the {@link GamePane} * @param title The title of the {@link GamePane}
* @return The new {@link GamePane} * @return the new {@link GamePane}
*/ */
public GamePane addGamePane(String title) { public GamePane addGamePane(String title) {
GamePane gamePane = new GamePane(); GamePane gamePane = new GamePane();
tabbedPane.add(title, gamePane); tabbedPane.add(title, gamePane);
tabbedPane.setTabComponentAt(tabbedPane.getTabCount() - 1, new GameTabComponent(tabbedPane)); tabbedPane.setTabComponentAt(
tabbedPane.getTabCount() - 1,
new GameTabComponent(tabbedPane)
);
tabbedPane.setSelectedIndex(tabbedPane.getTabCount() - 1); tabbedPane.setSelectedIndex(tabbedPane.getTabCount() - 1);
return gamePane; return gamePane;
} }
/**
* Creates a new {@link GamePane}, adds it to the tabbed pane and
* immediately
* displays a game configuration dialog for a new game on an existing
* {@link Board}.
*
* @param title the title of the {@link GamePane}
* @param board the {@link Board} with which the new {@link Game} is started
* @return the new {@link GamePane}
*/
public GamePane addGamePane(String title, Board board) { public GamePane addGamePane(String title, Board board) {
GamePane gamePane = addGamePane(title); GamePane gamePane = addGamePane(title);
DialogUtil.showGameConfigurationDialog(this, DialogUtil.showGameConfigurationDialog(
(whiteName, blackName) -> { this,
Game game = new Game(gamePane.getBoardPane(), whiteName, blackName, board); (whiteName, blackName) -> {
gamePane.setGame(game); Game game = new Game(
game.start(); gamePane.getBoardPane(),
}); whiteName,
blackName,
board
);
gamePane.setGame(game);
game.start();
}
);
return gamePane; return gamePane;
} }
@ -123,7 +134,9 @@ public class MainWindow extends JFrame {
* *
* @param index The index of the {@link GamePane} to remove * @param index The index of the {@link GamePane} to remove
*/ */
public void removeGamePane(int index) { tabbedPane.remove(index); } public void removeGamePane(int index) {
tabbedPane.remove(index);
}
/** /**
* Loads a game file (FEN or PGN) and adds it to a new {@link GamePane}. * Loads a game file (FEN or PGN) and adds it to a new {@link GamePane}.
@ -132,65 +145,111 @@ public class MainWindow extends JFrame {
*/ */
public void loadFiles(List<File> files) { public void loadFiles(List<File> files) {
files.forEach(file -> { files.forEach(file -> {
final String name = file.getName().substring(0, file.getName().lastIndexOf('.')); final String name
final String extension = file.getName().substring(file.getName().lastIndexOf('.')).toLowerCase(); = file.getName().substring(0, file.getName().lastIndexOf('.'));
final String extension = file.getName()
.substring(file.getName().lastIndexOf('.'))
.toLowerCase();
try { try {
Board board; Board board;
switch (extension) { switch (extension) {
case ".fen": case ".fen":
board = new FENString(new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8)).getBoard(); board = new FENString(
new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8)
).getBoard();
break; break;
case ".pgn": case ".pgn":
PGNDatabase pgnDB = new PGNDatabase(); PGNDatabase pgnDB = new PGNDatabase();
pgnDB.load(file); pgnDB.load(file);
if (pgnDB.getGames().size() > 0) { if (pgnDB.getGames().size() > 0) {
String[] gameNames = new String[pgnDB.getGames().size()]; String[] gameNames
= new String[pgnDB.getGames().size()];
for (int i = 0; i < gameNames.length; i++) { for (int i = 0; i < gameNames.length; i++) {
final PGNGame game = pgnDB.getGames().get(i); final PGNGame game = pgnDB.getGames().get(i);
gameNames[i] = String.format("%s vs %s: %s", game.getTag("White"), game.getTag("Black"), game.getTag("Result")); gameNames[i] = String.format(
"%s vs %s: %s",
game.getTag("White"),
game.getTag("Black"),
game.getTag("Result")
);
} }
JComboBox<String> comboBox = new JComboBox<>(gameNames); JComboBox<String> comboBox
JOptionPane.showMessageDialog(this, comboBox, "Select a game", JOptionPane.QUESTION_MESSAGE); = new JComboBox<>(gameNames);
board = pgnDB.getGames().get(comboBox.getSelectedIndex()).getBoard(); JOptionPane.showMessageDialog(
} else throw new ChessException("The PGN database '" + name + "' is empty!"); this,
comboBox,
"Select a game",
JOptionPane.QUESTION_MESSAGE
);
board = pgnDB.getGames()
.get(comboBox.getSelectedIndex())
.getBoard();
} else
throw new ChessException(
"The PGN database '" + name + "' is empty!"
);
break; break;
default: default:
throw new ChessException("The file extension '" + extension + "' is not supported!"); throw new ChessException(
"The file extension '" + extension
+ "' is not supported!"
);
} }
addGamePane(name, board); addGamePane(name, board);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
JOptionPane.showMessageDialog(this, JOptionPane.showMessageDialog(
"Failed to load the file " + file.getName() + ": " + e.toString(), this,
"File loading error", "Failed to load the file " + file.getName() + ": "
JOptionPane.ERROR_MESSAGE); + e.toString(),
"File loading error",
JOptionPane.ERROR_MESSAGE
);
} }
}); });
} }
/**
* Saves the current {@link Game} as a file in {@code PGN} or {@code FEN}
* format.
*
* @param file the file in which to save the current {@link Game}
*/
public void saveFile(File file) { public void saveFile(File file) {
final int dotIndex = file.getName().lastIndexOf('.'); final int dotIndex = file.getName().lastIndexOf('.');
final String extension = file.getName().substring(dotIndex).toLowerCase(); final String extension
= file.getName().substring(dotIndex).toLowerCase();
if (extension.equals(".pgn")) try { if (extension.equals(".pgn"))
PGNGame pgnGame = new PGNGame(getSelectedGamePane().getGame().getBoard()); try {
pgnGame.setTag("Event", tabbedPane.getTitleAt(tabbedPane.getSelectedIndex())); PGNGame pgnGame
pgnGame.setTag("Result", "*"); = new PGNGame(getSelectedGamePane().getGame().getBoard());
PGNDatabase pgnDB = new PGNDatabase(); pgnGame.setTag(
pgnDB.getGames().add(pgnGame); "Event",
pgnDB.save(file); tabbedPane.getTitleAt(tabbedPane.getSelectedIndex())
);
pgnGame.setTag("Result", "*");
PGNDatabase pgnDB = new PGNDatabase();
pgnDB.getGames().add(pgnGame);
pgnDB.save(file);
if (JOptionPane.showConfirmDialog(this, if (
"Game export finished. Do you want to view the created file?", JOptionPane.showConfirmDialog(
"Game export finished", this,
JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) "Game export finished. Do you want to view the created file?",
Desktop.getDesktop().open(file); "Game export finished",
} catch (IOException e) { JOptionPane.YES_NO_OPTION
e.printStackTrace(); ) == JOptionPane.YES_OPTION
JOptionPane.showMessageDialog(this, )
"Failed to save the file " + file.getName() + ": " + e.toString(), Desktop.getDesktop().open(file);
} catch (IOException e) {
e.printStackTrace();
JOptionPane.showMessageDialog(
this,
"Failed to save the file " + file.getName() + ": " + e,
"File saving error", "File saving error",
JOptionPane.ERROR_MESSAGE); JOptionPane.ERROR_MESSAGE
} );
}
} }
} }

View File

@ -4,10 +4,7 @@ import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.StringSelection;
import java.util.Arrays; import java.util.Arrays;
import javax.swing.JMenu; import javax.swing.*;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.filechooser.FileNameExtensionFilter; import javax.swing.filechooser.FileNameExtensionFilter;
import dev.kske.chess.board.FENString; import dev.kske.chess.board.FENString;
@ -19,7 +16,7 @@ import dev.kske.chess.io.EngineUtil;
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>MenuBar.java</strong><br> * File: <strong>MenuBar.java</strong><br>
* Created: <strong>16.07.2019</strong><br> * Created: <strong>16.07.2019</strong><br>
* *
* @since Chess v0.1-alpha * @since Chess v0.1-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
@ -29,6 +26,11 @@ public class MenuBar extends JMenuBar {
private final MainWindow mainWindow; private final MainWindow mainWindow;
/**
* Creates an instance of {@link MenuBar}.
*
* @param mainWindow the main window inside which this menu bar is contained
*/
public MenuBar(MainWindow mainWindow) { public MenuBar(MainWindow mainWindow) {
this.mainWindow = mainWindow; this.mainWindow = mainWindow;
@ -41,25 +43,52 @@ public class MenuBar extends JMenuBar {
JMenu gameMenu = new JMenu("Game"); JMenu gameMenu = new JMenu("Game");
JMenuItem newGameMenuItem = new JMenuItem("New Game"); JMenuItem newGameMenuItem = new JMenuItem("New Game");
newGameMenuItem.addActionListener((evt) -> DialogUtil.showGameConfigurationDialog(mainWindow, (whiteName, blackName) -> { newGameMenuItem.addActionListener(
GamePane gamePane = mainWindow.addGamePane(); evt -> DialogUtil.showGameConfigurationDialog(
Game game = new Game(gamePane.getBoardPane(), whiteName, blackName); mainWindow,
gamePane.setGame(game); (whiteName, blackName) -> {
game.start(); GamePane gamePane = mainWindow.addGamePane();
})); Game game = new Game(
gamePane.getBoardPane(),
whiteName,
blackName
);
gamePane.setGame(game);
game.start();
}
)
);
gameMenu.add(newGameMenuItem); gameMenu.add(newGameMenuItem);
JMenuItem loadFileMenu = new JMenuItem("Load game file"); JMenuItem loadFileMenu = new JMenuItem("Load game file");
loadFileMenu.addActionListener((evt) -> DialogUtil loadFileMenu.addActionListener(
.showFileSelectionDialog(mainWindow, evt -> DialogUtil
.showFileSelectionDialog(
mainWindow,
mainWindow::loadFiles, mainWindow::loadFiles,
Arrays.asList(new FileNameExtensionFilter("FEN and PGN files", "fen", "pgn")))); Arrays.asList(
new FileNameExtensionFilter(
"FEN and PGN files",
"fen",
"pgn"
)
)
)
);
gameMenu.add(loadFileMenu); gameMenu.add(loadFileMenu);
JMenuItem saveFileMenu = new JMenuItem("Save game file"); JMenuItem saveFileMenu = new JMenuItem("Save game file");
saveFileMenu saveFileMenu
.addActionListener((evt) -> DialogUtil .addActionListener(
.showFileSaveDialog(mainWindow, mainWindow::saveFile, Arrays.asList(new FileNameExtensionFilter("PGN file", "pgn")))); evt -> DialogUtil
.showFileSaveDialog(
mainWindow,
mainWindow::saveFile,
Arrays.asList(
new FileNameExtensionFilter("PGN file", "pgn")
)
)
);
gameMenu.add(saveFileMenu); gameMenu.add(saveFileMenu);
add(gameMenu); add(gameMenu);
@ -70,10 +99,16 @@ public class MenuBar extends JMenuBar {
JMenu engineMenu = new JMenu("Engine"); JMenu engineMenu = new JMenu("Engine");
JMenuItem addEngineMenuItem = new JMenuItem("Add engine"); JMenuItem addEngineMenuItem = new JMenuItem("Add engine");
addEngineMenuItem.addActionListener((evt) -> { addEngineMenuItem.addActionListener(evt -> {
String enginePath = JOptionPane String enginePath = JOptionPane
.showInputDialog(getParent(), "Enter the path to a UCI-compatible chess engine:", "Engine selection", JOptionPane.QUESTION_MESSAGE); .showInputDialog(
if (enginePath != null) EngineUtil.addEngine(enginePath); getParent(),
"Enter the path to a UCI-compatible chess engine:",
"Engine selection",
JOptionPane.QUESTION_MESSAGE
);
if (enginePath != null)
EngineUtil.addEngine(enginePath);
}); });
JMenuItem showInfoMenuItem = new JMenuItem("Show engine info"); JMenuItem showInfoMenuItem = new JMenuItem("Show engine info");
@ -87,29 +122,50 @@ public class MenuBar extends JMenuBar {
JMenu toolsMenu = new JMenu("Tools"); JMenu toolsMenu = new JMenu("Tools");
JMenuItem exportFENMenuItem = new JMenuItem("Export board to FEN"); JMenuItem exportFENMenuItem = new JMenuItem("Export board to FEN");
exportFENMenuItem.addActionListener((evt) -> { exportFENMenuItem.addActionListener(evt -> {
final String fen = new FENString(mainWindow.getSelectedGamePane().getGame().getBoard()).toString(); final String fen = new FENString(
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(fen), null); mainWindow.getSelectedGamePane().getGame().getBoard()
JOptionPane.showMessageDialog(mainWindow, String.format("FEN-string copied to clipboard!%n%s", fen)); ).toString();
Toolkit.getDefaultToolkit()
.getSystemClipboard()
.setContents(new StringSelection(fen), null);
JOptionPane.showMessageDialog(
mainWindow,
String.format("FEN-string copied to clipboard!%n%s", fen)
);
}); });
toolsMenu.add(exportFENMenuItem); toolsMenu.add(exportFENMenuItem);
JMenuItem loadFromFENMenuItem = new JMenuItem("Load board from FEN"); JMenuItem loadFromFENMenuItem = new JMenuItem("Load board from FEN");
loadFromFENMenuItem.addActionListener((evt) -> { loadFromFENMenuItem.addActionListener(evt -> {
final GamePane gamePane = mainWindow.addGamePane(); final GamePane gamePane = mainWindow.addGamePane();
final String fen = JOptionPane.showInputDialog("Enter a FEN string: "); final String fen
DialogUtil.showGameConfigurationDialog(mainWindow, (whiteName, blackName) -> { = JOptionPane.showInputDialog("Enter a FEN string: ");
Game game; DialogUtil.showGameConfigurationDialog(
try { mainWindow,
game = new Game(gamePane.getBoardPane(), whiteName, blackName, new FENString(fen).getBoard()); (whiteName, blackName) -> {
gamePane.setGame(game); Game game;
game.start(); try {
} catch (ChessException e) { game = new Game(
e.printStackTrace(); gamePane.getBoardPane(),
JOptionPane whiteName,
.showMessageDialog(mainWindow, "Failed to load FEN string: " + e.toString(), "FEN loading error", JOptionPane.ERROR_MESSAGE); blackName,
new FENString(fen).getBoard()
);
gamePane.setGame(game);
game.start();
} catch (ChessException e) {
e.printStackTrace();
JOptionPane
.showMessageDialog(
mainWindow,
"Failed to load FEN string: " + e.toString(),
"FEN loading error",
JOptionPane.ERROR_MESSAGE
);
}
} }
}); );
}); });
toolsMenu.add(loadFromFENMenuItem); toolsMenu.add(loadFromFENMenuItem);

View File

@ -14,19 +14,26 @@ import dev.kske.chess.board.MoveNode;
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>MoveNodeRenderer.java</strong><br> * File: <strong>MoveNodeRenderer.java</strong><br>
* Created: <strong>9 Oct 2019</strong><br> * Created: <strong>9 Oct 2019</strong><br>
* *
* @since Chess v0.5-alpha * @since Chess v0.5-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
public class MoveNodeRenderer extends JLabel implements ListCellRenderer<MoveNode> { public class MoveNodeRenderer extends JLabel
implements ListCellRenderer<MoveNode> {
private static final long serialVersionUID = 5242015788752442446L; private static final long serialVersionUID = 5242015788752442446L;
@Override @Override
public Component getListCellRendererComponent(JList<? extends MoveNode> list, MoveNode node, int index, public Component getListCellRendererComponent(
boolean isSelected, boolean cellHasFocus) { JList<? extends MoveNode> list, MoveNode node, int index,
boolean isSelected, boolean cellHasFocus
) {
setBorder(new EmptyBorder(5, 5, 5, 5)); setBorder(new EmptyBorder(5, 5, 5, 5));
setText(node.move.toLAN());
int numVariations
= node.hasVariations() ? node.getVariations().size() : 0;
setText(String.format("%s (%d)", node.move.toLAN(), numVariations));
setBackground(isSelected ? Color.red : Color.white); setBackground(isSelected ? Color.red : Color.white);
setOpaque(true); setOpaque(true);
return this; return this;

View File

@ -1,12 +1,6 @@
package dev.kske.chess.ui; package dev.kske.chess.ui;
import java.awt.BasicStroke; import java.awt.*;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.Shape;
import java.awt.geom.AffineTransform; import java.awt.geom.AffineTransform;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -20,7 +14,7 @@ import dev.kske.chess.board.Position;
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>OverlayComponent.java</strong><br> * File: <strong>OverlayComponent.java</strong><br>
* Created: <strong>08.07.2019</strong><br> * Created: <strong>08.07.2019</strong><br>
* *
* @since Chess v0.1-alpha * @since Chess v0.1-alpha
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
@ -30,9 +24,15 @@ public class OverlayComponent extends JComponent {
private final BoardPane boardPane; private final BoardPane boardPane;
private List<Position> dots; private List<Position> dots;
private Move arrow; private Move arrow;
/**
* Creates an instance of {@link OverlayComponent}.
*
* @param boardPane the board pane inside which this overlay component is
* contained
*/
public OverlayComponent(BoardPane boardPane) { public OverlayComponent(BoardPane boardPane) {
this.boardPane = boardPane; this.boardPane = boardPane;
dots = new ArrayList<>(); dots = new ArrayList<>();
@ -43,20 +43,36 @@ public class OverlayComponent extends JComponent {
protected void paintComponent(Graphics g) { protected void paintComponent(Graphics g) {
super.paintComponent(g); super.paintComponent(g);
final int tileSize = getTileSize(); final int tileSize = boardPane.getTileSize();
// Draw an arrow representing the last move and mark its position and // Draw an arrow representing the last move and mark its position and
// destination // destination
if (arrow != null) { if (arrow != null) {
Point pos = new Point(arrow.getPos().x * tileSize + tileSize / 2, arrow.getPos().y * tileSize + tileSize / 2); Point pos = new Point(
Point dest = new Point(arrow.getDest().x * tileSize + tileSize / 2, arrow.getDest().y * tileSize + tileSize / 2); arrow.getPos().x * tileSize + tileSize / 2,
arrow.getPos().y * tileSize + tileSize / 2
);
Point dest = new Point(
arrow.getDest().x * tileSize + tileSize / 2,
arrow.getDest().y * tileSize + tileSize / 2
);
Graphics2D g2d = (Graphics2D) g; Graphics2D g2d = (Graphics2D) g;
g2d.setStroke(new BasicStroke(3)); g2d.setStroke(new BasicStroke(3));
g2d.setColor(Color.yellow); g2d.setColor(Color.yellow);
g2d.drawRect(arrow.getPos().x * tileSize, arrow.getPos().y * tileSize, tileSize, tileSize); g2d.drawRect(
g2d.drawRect(arrow.getDest().x * tileSize, arrow.getDest().y * tileSize, tileSize, tileSize); arrow.getPos().x * tileSize,
arrow.getPos().y * tileSize,
tileSize,
tileSize
);
g2d.drawRect(
arrow.getDest().x * tileSize,
arrow.getDest().y * tileSize,
tileSize,
tileSize
);
Shape arrowShape = createArrowShape(pos, dest); Shape arrowShape = createArrowShape(pos, dest);
g.setColor(new Color(255, 0, 0, 127)); g.setColor(new Color(255, 0, 0, 127));
@ -64,16 +80,17 @@ public class OverlayComponent extends JComponent {
g2d.setColor(Color.black); g2d.setColor(Color.black);
g2d.draw(arrowShape); g2d.draw(arrowShape);
} }
// Draw possible moves if a piece was selected // Draw possible moves if a piece was selected
if (!dots.isEmpty()) { if (!dots.isEmpty()) {
g.setColor(Color.green); g.setColor(Color.green);
int radius = tileSize / 4; int radius = tileSize / 4;
for (Position dot : dots) for (Position dot : dots)
g.fillOval(dot.x * tileSize + tileSize / 2 - radius / 2, g.fillOval(
dot.y * tileSize + tileSize / 2 - radius / 2, dot.x * tileSize + tileSize / 2 - radius / 2,
radius, dot.y * tileSize + tileSize / 2 - radius / 2,
radius); radius,
radius
);
} }
} }
@ -89,10 +106,11 @@ public class OverlayComponent extends JComponent {
Point midPoint = midpoint(pos, dest); Point midPoint = midpoint(pos, dest);
double rotate = Math.atan2(dest.y - pos.y, dest.x - pos.x); double rotate = Math.atan2(dest.y - pos.y, dest.x - pos.x);
double ptDistance = pos.distance(dest); double ptDistance = pos.distance(dest);
double scale = ptDistance / 12.0; // 12 because it's the length of the arrow double scale = ptDistance / 12.0; // 12 because it's the length of the
// polygon. // arrow
// polygon.
AffineTransform transform = new AffineTransform(); AffineTransform transform = new AffineTransform();
@ -104,29 +122,52 @@ public class OverlayComponent extends JComponent {
} }
private Point midpoint(Point p1, Point p2) { private Point midpoint(Point p1, Point p2) {
return new Point((int) ((p1.x + p2.x) / 2.0), (int) ((p1.y + p2.y) / 2.0)); return new Point(
(int) ((p1.x + p2.x) / 2.0),
(int) ((p1.y + p2.y) / 2.0)
);
} }
/**
* Displays green dots at a list of positions.
*
* @param dots the positions at which the dots should be displayed
*/
public void displayDots(List<Position> dots) { public void displayDots(List<Position> dots) {
this.dots.clear(); this.dots.clear();
this.dots.addAll(dots); this.dots.addAll(dots);
repaint(); repaint();
} }
/**
* Clears all dots displayed at some positions.
*/
public void clearDots() { public void clearDots() {
dots.clear(); dots.clear();
repaint(); repaint();
} }
/**
* Displays an arrow from the position to the destination of a move.
*
* @param arrow the move indicating the arrows position and destination
*/
public void displayArrow(Move arrow) { public void displayArrow(Move arrow) {
this.arrow = arrow; this.arrow = arrow;
repaint(); repaint();
} }
/**
* Clears the arrow displayed to indicate a move.
*/
public void clearArrow() { public void clearArrow() {
arrow = null; arrow = null;
repaint(); repaint();
} }
/**
* @return the size of one board tile in pixels.
* @see dev.kske.chess.ui.BoardPane#getTileSize()
*/
public int getTileSize() { return boardPane.getTileSize(); } public int getTileSize() { return boardPane.getTileSize(); }
} }

View File

@ -1,7 +1,6 @@
package dev.kske.chess.board; package dev.kske.chess.board;
import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.*;
import static org.junit.Assert.assertNotSame;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -16,7 +15,7 @@ import dev.kske.chess.board.Piece.Color;
*/ */
class BoardTest { class BoardTest {
Board board; private Board board;
/** /**
* @throws java.lang.Exception * @throws java.lang.Exception
@ -27,7 +26,7 @@ class BoardTest {
} }
/** /**
* Test method for {@link dev.kske.chess.board.Board#clone()}. * Test method for {@link Board#Board(Board, boolean)}.
*/ */
@Test @Test
void testClone() { void testClone() {
@ -38,6 +37,9 @@ class BoardTest {
clone.getBoardArr()[0][0] = new Queen(Color.BLACK, clone); clone.getBoardArr()[0][0] = new Queen(Color.BLACK, clone);
clone.move(new Move(1, 1, 1, 2)); clone.move(new Move(1, 1, 1, 2));
assertNotEquals(clone.getBoardArr()[0][0], board.getBoardArr()[0][0]); assertNotEquals(clone.getBoardArr()[0][0], board.getBoardArr()[0][0]);
assertNotEquals(clone.getLog().getActiveColor(), board.getLog().getActiveColor()); assertNotEquals(
clone.getLog().getActiveColor(),
board.getLog().getActiveColor()
);
} }
} }

View File

@ -16,14 +16,19 @@ import dev.kske.chess.exception.ChessException;
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>FENStringTest.java</strong><br> * File: <strong>FENStringTest.java</strong><br>
* Created: <strong>24 Oct 2019</strong><br> * Created: <strong>24 Oct 2019</strong><br>
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
*/ */
class FENStringTest { class FENStringTest {
List<String> fenStrings = new ArrayList<>(); private List<String> fenStrings = new ArrayList<>();
List<Board> boards = new ArrayList<>(); private List<Board> boards = new ArrayList<>();
/**
* Removes all pieces from a board
*
* @param board the board to clean
*/
void cleanBoard(Board board) { void cleanBoard(Board board) {
for (int i = 0; i < 8; i++) for (int i = 0; i < 8; i++)
for (int j = 0; j < 8; j++) for (int j = 0; j < 8; j++)
@ -35,7 +40,11 @@ class FENStringTest {
*/ */
@BeforeEach @BeforeEach
void setUp() throws Exception { void setUp() throws Exception {
fenStrings.addAll(Arrays.asList("rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2")); fenStrings.addAll(
Arrays.asList(
"rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2"
)
);
Board board = new Board(); Board board = new Board();
board.set(Position.fromLAN("c7"), null); board.set(Position.fromLAN("c7"), null);
board.set(Position.fromLAN("c5"), new Pawn(Color.BLACK, board)); board.set(Position.fromLAN("c5"), new Pawn(Color.BLACK, board));
@ -51,22 +60,28 @@ class FENStringTest {
} }
/** /**
* Test method for {@link dev.kske.chess.board.FENString#toString()}. * Test method for {@link FENString#toString()}.
*/ */
@Test @Test
void testToString() { void testToString() {
for (int i = 0; i < fenStrings.size(); i++) for (int i = 0; i < fenStrings.size(); i++)
assertEquals(fenStrings.get(i), new FENString(boards.get(i)).toString()); assertEquals(
fenStrings.get(i),
new FENString(boards.get(i)).toString()
);
} }
/** /**
* Test method for {@link dev.kske.chess.board.FENString#getBoard()}. * Test method for {@link FENString#getBoard()}.
* *
* @throws ChessException * @throws ChessException
*/ */
@Test @Test
void testGetBoard() throws ChessException { void testGetBoard() throws ChessException {
for (int i = 0; i < boards.size(); i++) for (int i = 0; i < boards.size(); i++)
assertEquals(boards.get(i), new FENString(fenStrings.get(i)).getBoard()); assertEquals(
boards.get(i),
new FENString(fenStrings.get(i)).getBoard()
);
} }
} }

View File

@ -1,10 +1,6 @@
package dev.kske.chess.board; package dev.kske.chess.board;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -18,10 +14,10 @@ import dev.kske.chess.board.Piece.Color;
*/ */
class LogTest { class LogTest {
Log log = new Log(); private Log log = new Log();
/** /**
* Test method for {@link dev.kske.chess.board.Log#Log()}. * Test method for {@link Log#Log()}.
*/ */
@Test @Test
void testLog() { void testLog() {
@ -35,7 +31,7 @@ class LogTest {
} }
/** /**
* Test method for {@link dev.kske.chess.board.Log#clone()}. * Test method for {@link Log#Log(Log, boolean)}.
*/ */
@Test @Test
void testClone() { void testClone() {
@ -48,11 +44,14 @@ class LogTest {
other.add(Move.fromLAN("a2a4"), new Pawn(Color.WHITE, null), null); other.add(Move.fromLAN("a2a4"), new Pawn(Color.WHITE, null), null);
other.add(Move.fromLAN("a4a5"), new Pawn(Color.WHITE, null), null); other.add(Move.fromLAN("a4a5"), new Pawn(Color.WHITE, null), null);
assertNotEquals(log.getRoot(), other.getRoot()); assertNotEquals(log.getRoot(), other.getRoot());
assertNotEquals(log.getRoot().getVariations(), other.getRoot().getVariations()); assertNotEquals(
log.getRoot().getVariations(),
other.getRoot().getVariations()
);
} }
/** /**
* Test method for {@link dev.kske.chess.board.Log#add(dev.kske.chess.board.Move, dev.kske.chess.board.Piece, boolean)}. * Test method for {@link Log#add(Move, Piece, Piece)}.
*/ */
@Test @Test
void testAdd() { void testAdd() {
@ -60,7 +59,7 @@ class LogTest {
} }
/** /**
* Test method for {@link dev.kske.chess.board.Log#removeLast()}. * Test method for {@link Log#removeLast()}.
*/ */
@Test @Test
void testRemoveLast() { void testRemoveLast() {
@ -68,7 +67,7 @@ class LogTest {
} }
/** /**
* Test method for {@link dev.kske.chess.board.Log#isEmpty()}. * Test method for {@link Log#isEmpty()}.
*/ */
@Test @Test
void testIsEmpty() { void testIsEmpty() {
@ -76,7 +75,7 @@ class LogTest {
} }
/** /**
* Test method for {@link dev.kske.chess.board.Log#reset()}. * Test method for {@link Log#reset()}.
*/ */
@Test @Test
void testReset() { void testReset() {
@ -84,7 +83,7 @@ class LogTest {
} }
/** /**
* Test method for {@link dev.kske.chess.board.Log#getRoot()}. * Test method for {@link Log#getRoot()}.
*/ */
@Test @Test
void testGetRoot() { void testGetRoot() {
@ -92,7 +91,7 @@ class LogTest {
} }
/** /**
* Test method for {@link dev.kske.chess.board.Log#getLast()}. * Test method for {@link Log#getLast()}.
*/ */
@Test @Test
void testGetLast() { void testGetLast() {
@ -100,7 +99,7 @@ class LogTest {
} }
/** /**
* Test method for {@link dev.kske.chess.board.Log#getEnPassant()}. * Test method for {@link Log#getEnPassant()}.
*/ */
@Test @Test
void testGetEnPassant() { void testGetEnPassant() {
@ -108,7 +107,7 @@ class LogTest {
} }
/** /**
* Test method for {@link dev.kske.chess.board.Log#setEnPassant(dev.kske.chess.board.Position)}. * Test method for {@link Log#setEnPassant(dev.kske.chess.board.Position)}.
*/ */
@Test @Test
void testSetEnPassant() { void testSetEnPassant() {
@ -116,7 +115,7 @@ class LogTest {
} }
/** /**
* Test method for {@link dev.kske.chess.board.Log#getActiveColor()}. * Test method for {@link Log#getActiveColor()}.
*/ */
@Test @Test
void testGetActiveColor() { void testGetActiveColor() {
@ -124,7 +123,8 @@ class LogTest {
} }
/** /**
* Test method for {@link dev.kske.chess.board.Log#setActiveColor(dev.kske.chess.board.Piece.Color)}. * Test method for
* {@link Log#setActiveColor(dev.kske.chess.board.Piece.Color)}.
*/ */
@Test @Test
void testSetActiveColor() { void testSetActiveColor() {
@ -132,7 +132,7 @@ class LogTest {
} }
/** /**
* Test method for {@link dev.kske.chess.board.Log#getFullmoveNumber()}. * Test method for {@link Log#getFullmoveNumber()}.
*/ */
@Test @Test
void testGetFullmoveCounter() { void testGetFullmoveCounter() {
@ -140,7 +140,7 @@ class LogTest {
} }
/** /**
* Test method for {@link dev.kske.chess.board.Log#setFullmoveNumber(int)}. * Test method for {@link Log#setFullmoveNumber(int)}.
*/ */
@Test @Test
void testSetFullmoveCounter() { void testSetFullmoveCounter() {
@ -148,7 +148,7 @@ class LogTest {
} }
/** /**
* Test method for {@link dev.kske.chess.board.Log#getHalfmoveClock()}. * Test method for {@link Log#getHalfmoveClock()}.
*/ */
@Test @Test
void testGetHalfmoveClock() { void testGetHalfmoveClock() {
@ -156,10 +156,10 @@ class LogTest {
} }
/** /**
* Test method for {@link dev.kske.chess.board.Log#setHalfmoveClock(int)}. * Test method for {@link Log#setHalfmoveClock(int)}.
*/ */
@Test @Test
void testSetHalfmoveClock() { void testSetHalfmoveClock() {
fail("Not yet implemented"); fail("Not yet implemented");
} }
} }

View File

@ -12,14 +12,23 @@ import org.junit.jupiter.api.Test;
*/ */
class PositionTest { class PositionTest {
final int n = 4; private final int n = 4;
Position[] positions = new Position[] { new Position(0, 0), new Position(7, 7), new Position(0, 7), new Position(7, 0) }; private Position[] positions = new Position[] {
String[] sans = new String[] { "a8", "h1", "a1", "h8" }; new Position(0, 0),
String[] strings = new String[] { "[0, 0]", "[7, 7]", "[0, 7]", "[7, 0]" }; new Position(7, 7),
new Position(0, 7),
new Position(7, 0)
};
private String[] sans = new String[] {
"a8", "h1", "a1", "h8"
};
private String[] strings = new String[] {
"[0, 0]", "[7, 7]", "[0, 7]", "[7, 0]"
};
/** /**
* Test method for * Test method for
* {@link dev.kske.chess.board.Position#fromLAN(java.lang.String)}. * {@link Position#fromLAN(java.lang.String)}.
*/ */
@Test @Test
void testFromSAN() { void testFromSAN() {
@ -28,7 +37,7 @@ class PositionTest {
} }
/** /**
* Test method for {@link dev.kske.chess.board.Position#toLAN()}. * Test method for {@link Position#toLAN()}.
*/ */
@Test @Test
void testToSAN() { void testToSAN() {
@ -37,7 +46,7 @@ class PositionTest {
} }
/** /**
* Test method for {@link dev.kske.chess.board.Position#toString()}. * Test method for {@link Position#toString()}.
*/ */
@Test @Test
void testToString() { void testToString() {

View File

@ -1,7 +1,5 @@
package dev.kske.chess.pgn; package dev.kske.chess.pgn;
import static org.junit.jupiter.api.Assertions.fail;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
@ -18,19 +16,18 @@ import dev.kske.chess.exception.ChessException;
class PGNDatabaseTest { class PGNDatabaseTest {
/** /**
* Test method for {@link dev.kske.chess.pgn.PGNDatabase#load(java.io.File)}. * Test method for
* * {@link dev.kske.chess.pgn.PGNDatabase#load(java.io.File)}.
* @throws ChessException *
* @throws FileNotFoundException * @throws ChessException if an error occurs while parsing the file
* @throws FileNotFoundException if the test file {@code test.pgn} is not
* present
*/ */
@Test @Test
void testLoad() { void testLoad() throws FileNotFoundException, ChessException {
PGNDatabase db = new PGNDatabase(); PGNDatabase db = new PGNDatabase();
try { db.load(
db.load(new File(getClass().getClassLoader().getResource("test.pgn").getFile())); new File(getClass().getClassLoader().getResource("test.pgn").getFile())
} catch (FileNotFoundException | ChessException e) { );
e.printStackTrace();
fail(e);
}
} }
} }