Compare commits

..

No commits in common. "v0.2-alpha" and "master" have entirely different histories.

89 changed files with 6855 additions and 1903 deletions

View File

@ -1,17 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="res"/>
<classpathentry kind="src" output="bin_test" path="test">
<attributes>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER">
<attributes>
<attribute name="module" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/5"/>
<classpathentry kind="output" path="bin"/>
</classpath>
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<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-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

23
.gitignore vendored
View File

@ -1,24 +1,3 @@
.metadata
bin/
/bin_test/
tmp/
*.tmp
*.bak
*.swp
*~.nib
local.properties
.settings/
.loadpath
.recommenders
# Locally stored "Eclipse launch configurations"
*.launch
# Java annotation processor (APT)
.factorypath
# Code Recommenders
.recommenders/
# Annotation Processing
.apt_generated/
/target/

View File

@ -10,8 +10,14 @@
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>

67
pom.xml Normal file
View File

@ -0,0 +1,67 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dev.kske</groupId>
<artifactId>chess</artifactId>
<version>0.1-beta</version>
<name>Chess</name>
<description>A chess GUI with UCI support written in Java.</description>
<url>https://git.kske.dev/kske/chess</url>
<licenses>
<license>
<name>MIT License</name>
<url>http://www.opensource.org/licenses/mit-license.php</url>
</license>
</licenses>
<developers>
<developer>
<id>kske</id>
<name>Kai S. K. Engelbart</name>
<email>kai@kske.dev</email>
<url>https://kske.dev</url>
<roles>
<role>architect</role>
<role>developer</role>
</roles>
<timezone>Europe/Berlin</timezone>
</developer>
</developers>
<scm>
<connection>scm:git:https://git.kske.dev/kske/chess.git</connection>
<developerConnection>scm:git:ssh:git@git.kske.dev:kske/chess.git</developerConnection>
</scm>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<repositories>
<repository>
<id>kske-repo</id>
<url>https://kske.dev/maven-repo</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>dev.kske</groupId>
<artifactId>event-bus</artifactId>
<version>0.0.3</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.6.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -1,391 +0,0 @@
package dev.kske.chess.board;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import dev.kske.chess.board.Log.LoggedMove;
import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.board.Piece.Type;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>Board.java</strong><br>
* Created: <strong>01.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class Board implements Cloneable {
private Piece[][] boardArr;
private Map<Color, Position> kingPos;
private Log log;
private static final Map<Type, int[][]> positionScores;
static {
positionScores = new HashMap<>();
positionScores.put(Type.KING,
new int[][] { new int[] { -3, -4, -4, -5, -5, -4, -4, -3 },
new int[] { -3, -4, -4, -5, -4, -4, -4, -3 }, new int[] { -3, -4, -4, -5, -4, -4, -4, -3 },
new int[] { -3, -4, -4, -5, -4, -4, -4, -3 }, new int[] { -2, -3, -3, -2, -2, -2, -2, -1 },
new int[] { -1, -2, -2, -2, -2, -2, -2, -1 }, new int[] { 2, 2, 0, 0, 0, 0, 2, 2 },
new int[] { 2, 3, 1, 0, 0, 1, 3, 2 } });
positionScores.put(Type.QUEEN,
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(Type.ROOK,
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(Type.KNIGHT,
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(Type.BISHOP,
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(Type.PAWN,
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 Board() {
boardArr = new Piece[8][8];
kingPos = new HashMap<>();
log = new Log();
initializeDefaultPositions();
}
/**
* Moves a piece across the board if the move is legal.
*
* @param move The move to execute
* @return {@code true}, if the attempted move was legal and thus executed
*/
public boolean attemptMove(Move move) {
Piece piece = getPos(move);
if (piece == null || !piece.isValidMove(move)) return false;
else {
// Set type after validation
if (move.type == Move.Type.UNKNOWN) move.type = Move.Type.NORMAL;
// Move piece
move(move);
// Revert move if it caused a check for its team
if (checkCheck(piece.getColor())) {
revert();
return false;
}
return true;
}
}
/**
* Moves a piece across the board without checking if the move is legal.
*
* @param move The move to execute
* @return The captures piece, or null if the move's destination was empty
*/
public void move(Move move) {
Piece piece = getPos(move);
Piece capturePiece = getDest(move);
switch (move.type) {
case PAWN_PROMOTION:
setPos(move, null);
// TODO: Select promotion
setDest(move, new Queen(piece.getColor(), this));
break;
case CASTLING:
// Move the king
setDest(move, piece);
setPos(move, null);
// Move the rook
Move rookMove = move.dest.x == 6 ? new Move(7, move.pos.y, 5, move.pos.y) // Kingside
: new Move(0, move.pos.y, 3, move.pos.y); // Queenside
// Move the rook
setDest(rookMove, getPos(rookMove));
setPos(rookMove, null);
getDest(rookMove).incMoveCounter();
break;
case UNKNOWN:
System.err.printf("Move of unknown type %s found!%n", move);
case NORMAL:
setDest(move, piece);
setPos(move, null);
break;
default:
System.err.printf("Move %s of unimplemented type found!%n", move);
}
// Increment move counter
getDest(move).incMoveCounter();
// Update the king's position if the moved piece is the king
if (piece.getType() == Type.KING) kingPos.put(piece.getColor(), move.dest);
// Update log
log.add(move, capturePiece);
}
/**
* Reverts the last move.
*/
public void revert() {
LoggedMove loggedMove = log.getLast();
Move move = loggedMove.move;
Piece capturedPiece = loggedMove.capturedPiece;
switch (move.type) {
case PAWN_PROMOTION:
setPos(move, new Pawn(getDest(move).getColor(), this));
setDest(move, capturedPiece);
break;
case CASTLING:
// Move the king
setPos(move, getDest(move));
setDest(move, null);
// Move the rook
Move rookMove = move.dest.x == 6 ? new Move(5, move.pos.y, 7, move.pos.y) // Kingside
: new Move(3, move.pos.y, 0, move.pos.y); // Queenside
// Move the rook
setDest(rookMove, getPos(rookMove));
setPos(rookMove, null);
getDest(rookMove).decMoveCounter();
break;
case UNKNOWN:
System.err.printf("Move of unknown type %s found!%n", move);
case NORMAL:
setPos(move, getDest(move));
setDest(move, capturedPiece);
break;
default:
System.err.printf("Move %s of unimplemented type found!%n", move);
}
// Decrement move counter
getPos(move).decMoveCounter();
// Update the king's position if the moved piece is the king
if (getPos(move).getType() == Type.KING) kingPos.put(getPos(move).getColor(), move.pos);
// Update log
log.removeLast();
}
/**
* Generated every legal move for one color
*
* @param color The color to generate the moves for
* @return A list of all legal moves
*/
public List<Move> getMoves(Color color) {
List<Move> moves = new ArrayList<>();
for (int i = 0; i < 8; i++)
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)));
return moves;
}
public List<Move> getMoves(Position pos) {
return get(pos).getMoves(pos);
}
/**
* Checks, if the king is in check.
*
* @param color The color of the king to check
* @return {@code true}, if the king is in check
*/
public boolean checkCheck(Color color) {
for (int i = 0; i < 8; i++)
for (int j = 0; j < 8; j++) {
Position pos = new Position(i, j);
if (get(pos) != null && get(pos).getColor() != color
&& get(pos).isValidMove(new Move(pos, kingPos.get(color))))
return true;
}
return false;
}
/**
* Checks, if the king is in checkmate.
* This requires the king to already be in check!
*
* @param color The color of the king to check
* @return {@code true}, if the king is in checkmate
*/
public boolean checkCheckmate(Color color) {
// Return false immediately if the king can move
if (!getMoves(kingPos.get(color)).isEmpty()) return false;
else {
for (Move move : getMoves(color)) {
move(move);
boolean check = checkCheck(color);
revert();
if (!check) return false;
}
return true;
}
}
public GameState getGameEventType(Color color) {
return checkCheck(color) ? checkCheckmate(color) ? GameState.CHECKMATE : GameState.CHECK
: getMoves(color).isEmpty() ? GameState.STALEMATE : GameState.NORMAL;
}
/**
* Evaluated the board.
*
* @param color The color to evaluate for
* @return An positive number representing how good the position is
*/
public int evaluate(Color color) {
int score = 0;
for (int i = 0; i < 8; i++)
for (int j = 0; j < 8; j++)
if (boardArr[i][j] != null && boardArr[i][j].getColor() == color) {
switch (boardArr[i][j].getType()) {
case QUEEN:
score += 90;
break;
case ROOK:
score += 50;
break;
case KNIGHT:
score += 30;
break;
case BISHOP:
score += 30;
break;
case PAWN:
score += 10;
break;
}
if (positionScores.containsKey(boardArr[i][j].getType()))
score += positionScores.get(boardArr[i][j].getType())[i][color == Color.WHITE ? j : 7 - j];
}
return score;
}
/**
* Initialized the board array with the default chess pieces and positions.
*/
public void initializeDefaultPositions() {
// Initialize pawns
for (int i = 0; i < 8; i++) {
boardArr[i][1] = new Pawn(Color.BLACK, this);
boardArr[i][6] = new Pawn(Color.WHITE, this);
}
// Initialize kings
boardArr[4][0] = new King(Color.BLACK, this);
boardArr[4][7] = new King(Color.WHITE, this);
// Initialize king position objects
kingPos.put(Color.BLACK, new Position(4, 0));
kingPos.put(Color.WHITE, new Position(4, 7));
// Initialize queens
boardArr[3][0] = new Queen(Color.BLACK, this);
boardArr[3][7] = new Queen(Color.WHITE, this);
// Initialize rooks
boardArr[0][0] = new Rook(Color.BLACK, this);
boardArr[0][7] = new Rook(Color.WHITE, this);
boardArr[7][0] = new Rook(Color.BLACK, this);
boardArr[7][7] = new Rook(Color.WHITE, this);
// Initialize knights
boardArr[1][0] = new Knight(Color.BLACK, this);
boardArr[1][7] = new Knight(Color.WHITE, this);
boardArr[6][0] = new Knight(Color.BLACK, this);
boardArr[6][7] = new Knight(Color.WHITE, this);
// Initialize bishops
boardArr[2][0] = new Bishop(Color.BLACK, this);
boardArr[2][7] = new Bishop(Color.WHITE, this);
boardArr[5][0] = new Bishop(Color.BLACK, this);
boardArr[5][7] = new Bishop(Color.WHITE, this);
// Clear all other tiles
for (int i = 0; i < 8; i++)
for (int j = 2; j < 6; j++)
boardArr[i][j] = null;
}
/**
* @return A new instance of this class with a shallow copy of both
* {@code kingPos} and {code boardArr}
*/
@Override
public Object clone() {
Board board = null;
try {
board = (Board) super.clone();
} catch (CloneNotSupportedException ex) {
ex.printStackTrace();
}
board.boardArr = new Piece[8][8];
for (int i = 0; i < 8; i++)
for (int j = 0; j < 8; j++) {
if (boardArr[i][j] == null) continue;
board.boardArr[i][j] = (Piece) boardArr[i][j].clone();
board.boardArr[i][j].board = board;
}
board.kingPos = new HashMap<>();
board.kingPos.putAll(kingPos);
board.log = (Log) log.clone();
return board;
}
public Piece get(Position pos) {
return boardArr[pos.x][pos.y];
}
public void set(Position pos, Piece piece) {
boardArr[pos.x][pos.y] = piece;
}
public Piece getPos(Move move) {
return get(move.pos);
}
public Piece getDest(Move move) {
return get(move.dest);
}
public void setPos(Move move, Piece piece) {
set(move.pos, piece);
}
public void setDest(Move move, Piece piece) {
set(move.dest, piece);
}
/**
* @return The board array
*/
public Piece[][] getBoardArr() { return boardArr; }
}

View File

@ -1,11 +0,0 @@
package dev.kske.chess.board;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>GameState.java</strong><br>
* Created: <strong>07.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public enum GameState {
CHECK, CHECKMATE, STALEMATE, NORMAL;
}

View File

@ -1,80 +0,0 @@
package dev.kske.chess.board;
import java.util.ArrayList;
import java.util.List;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>King.java</strong><br>
* Created: <strong>01.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class King extends Piece {
public King(Color color, Board board) {
super(color, board);
}
@Override
public boolean isValidMove(Move move) {
// Castling
if (getMoveCounter() == 0 && move.xDist == 2 && move.yDist == 0) {
// Kingside
if (board.getBoardArr()[7][move.pos.y] != null && board.getBoardArr()[7][move.pos.y].getType() == Type.ROOK
&& isFreePath(new Move(new Position(5, move.pos.y), new Position(7, move.pos.y)))) {
move.type = Move.Type.CASTLING;
return true;
}
// Queenside
if (board.getBoardArr()[0][move.pos.y] != null && board.getBoardArr()[0][move.pos.y].getType() == Type.ROOK
&& isFreePath(new Move(new Position(1, move.pos.y), new Position(4, move.pos.y)))) {
move.type = Move.Type.CASTLING;
return true;
}
}
return move.xDist <= 1 && move.yDist <= 1 && checkDestination(move);
}
@Override
protected List<Move> getPseudolegalMoves(Position pos) {
List<Move> moves = new ArrayList<>();
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++)
if (i != pos.x || j != pos.y) {
Move move = new Move(pos, new Position(i, j));
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) moves.add(move);
}
// Castling
// TODO: Check attacked squares in between
// TODO: Castling out of check?
if (getMoveCounter() == 0) {
// Kingside
if (board.getBoardArr()[7][pos.y] != null && board.getBoardArr()[7][pos.y].getType() == Type.ROOK
&& isFreePath(new Move(new Position(5, pos.y), new Position(7, pos.y))))
moves.add(new Move(pos, new Position(6, pos.y), Move.Type.CASTLING));
// Queenside
if (board.getBoardArr()[0][pos.y] != null && board.getBoardArr()[0][pos.y].getType() == Type.ROOK
&& isFreePath(new Move(new Position(1, pos.y), new Position(4, pos.y))))
moves.add(new Move(pos, new Position(2, pos.y), Move.Type.CASTLING));
}
return moves;
}
@Override
protected boolean isFreePath(Move move) {
for (int i = move.pos.x, j = move.pos.y; i != move.dest.x || j != move.dest.y; i += move.xSign, j += move.ySign)
if (board.getBoardArr()[i][j] != null) return false;
return true;
}
@Override
public Type getType() { return Type.KING; }
}

View File

@ -1,47 +0,0 @@
package dev.kske.chess.board;
import java.util.ArrayList;
import java.util.List;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>Knight.java</strong><br>
* Created: <strong>01.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class Knight extends Piece {
public Knight(Color color, Board board) {
super(color, board);
}
@Override
public boolean isValidMove(Move move) {
return Math.abs(move.xDist - move.yDist) == 1
&& (move.xDist == 1 && move.yDist == 2 || move.xDist == 2 && move.yDist == 1) && checkDestination(move);
}
private void checkAndInsertMove(List<Move> moves, Position pos, int offsetX, int offsetY) {
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);
}
}
@Override
protected List<Move> getPseudolegalMoves(Position pos) {
List<Move> moves = new ArrayList<>();
checkAndInsertMove(moves, pos, -2, 1);
checkAndInsertMove(moves, pos, -1, 2);
checkAndInsertMove(moves, pos, 1, 2);
checkAndInsertMove(moves, pos, 2, 1);
checkAndInsertMove(moves, pos, -2, -1);
checkAndInsertMove(moves, pos, -1, -2);
checkAndInsertMove(moves, pos, 1, -2);
checkAndInsertMove(moves, pos, 2, -1);
return moves;
}
@Override
public Type getType() { return Type.KNIGHT; }
}

View File

@ -1,55 +0,0 @@
package dev.kske.chess.board;
import java.util.ArrayList;
import java.util.List;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>Log.java</strong><br>
* Created: <strong>09.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class Log implements Cloneable {
private List<LoggedMove> moves;
public Log() {
moves = new ArrayList<>();
}
public void add(Move move, Piece capturedPiece) {
moves.add(new LoggedMove(move, capturedPiece));
}
public LoggedMove getLast() {
return moves.get(moves.size() - 1);
}
public void removeLast() {
if (!moves.isEmpty()) moves.remove(moves.size() - 1);
}
@Override
public Object clone() {
Log log = null;
try {
log = (Log) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
log.moves = new ArrayList<>();
log.moves.addAll(this.moves);
return log;
}
public static class LoggedMove {
public final Move move;
public final Piece capturedPiece;
public LoggedMove(Move move, Piece capturedPiece) {
this.move = move;
this.capturedPiece = capturedPiece;
}
}
}

View File

@ -1,47 +0,0 @@
package dev.kske.chess.board;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>Move.java</strong><br>
* Created: <strong>02.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class Move {
public final Position pos, dest;
public final int xDist, yDist, xSign, ySign;
public Type type;
public Move(Position pos, Position dest, Type type) {
this.pos = pos;
this.dest = dest;
this.type = type;
xDist = Math.abs(dest.x - pos.x);
yDist = Math.abs(dest.y - pos.y);
xSign = (int) Math.signum(dest.x - pos.x);
ySign = (int) Math.signum(dest.y - pos.y);
}
public Move(Position pos, Position dest) {
this(pos, dest, Type.NORMAL);
}
public Move(int xPos, int yPos, int xDest, int yDest) {
this(new Position(xPos, yPos), new Position(xDest, yDest));
}
public boolean isHorizontal() { return yDist == 0; }
public boolean isVertical() { return xDist == 0; }
public boolean isDiagonal() { return xDist == yDist; }
@Override
public String toString() {
return String.format("%s -> %s", pos, dest);
}
public static enum Type {
NORMAL, PAWN_PROMOTION, CASTLING, EN_PASSANT, UNKNOWN
}
}

View File

@ -1,84 +0,0 @@
package dev.kske.chess.board;
import java.util.ArrayList;
import java.util.List;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>Pawn.java</strong><br>
* Created: <strong>01.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class Pawn extends Piece {
public Pawn(Color color, Board board) {
super(color, board);
}
@Override
public boolean isValidMove(Move move) {
// TODO: en passant
boolean step = move.isVertical() && move.yDist == 1;
boolean doubleStep = move.isVertical() && move.yDist == 2;
boolean strafe = move.isDiagonal() && move.xDist == 1;
if (getColor() == Color.WHITE) doubleStep &= move.pos.y == 6;
else doubleStep &= move.pos.y == 1;
// Mark move as pawn promotion if necessary
if (move.ySign == 1 && move.pos.y == 6 || move.ySign == -1 && move.pos.y == 1)
move.type = Move.Type.PAWN_PROMOTION;
return (step ^ doubleStep ^ strafe) && move.ySign == (getColor() == Color.WHITE ? -1 : 1) && isFreePath(move);
}
@Override
protected boolean isFreePath(Move move) {
// Two steps forward
if (move.yDist == 2)
return board.getBoardArr()[move.pos.x][move.dest.y - move.ySign] == null && board.getDest(move) == null;
// One step forward
else if (move.xDist == 0) return board.getDest(move) == null;
// Capture move
else return board.getDest(move) != null && board.getDest(move).getColor() != getColor();
}
@Override
protected List<Move> getPseudolegalMoves(Position pos) {
List<Move> moves = new ArrayList<>();
int sign = getColor() == Color.WHITE ? -1 : 1;
// Strafe left
if (pos.x > 0) {
Move move = new Move(pos, new Position(pos.x - 1, pos.y + sign));
if (isFreePath(move)) moves.add(move);
}
// Strafe right
if (pos.x < 7) {
Move move = new Move(pos, new Position(pos.x + 1, pos.y + sign));
if (isFreePath(move)) moves.add(move);
}
// Step forward
if (sign == 1 && pos.y < 7 || sign == -1 && pos.y > 0) {
Move move = new Move(pos, new Position(pos.x, pos.y + sign));
if (isFreePath(move)) moves.add(move);
}
// Double step forward
if (sign == 1 && pos.y == 1 || sign == -1 && pos.y == 6) {
Move move = new Move(pos, new Position(pos.x, pos.y + 2 * sign));
if (isFreePath(move)) moves.add(move);
}
// Mark moves as pawn promotions if necessary
if (sign == 1 && pos.y == 6 || sign == -1 && pos.y == 1)
moves.parallelStream().forEach(m -> m.type = Move.Type.PAWN_PROMOTION);
return moves;
}
@Override
public Type getType() { return Type.PAWN; }
}

View File

@ -1,90 +0,0 @@
package dev.kske.chess.board;
import java.util.Iterator;
import java.util.List;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>Piece.java</strong><br>
* Created: <strong>01.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public abstract class Piece implements Cloneable {
private final Color color;
protected Board board;
private int moveCounter;
public Piece(Color color, Board board) {
this.color = color;
this.board = board;
}
public List<Move> getMoves(Position pos) {
List<Move> moves = getPseudolegalMoves(pos);
for (Iterator<Move> iterator = moves.iterator(); iterator.hasNext();) {
Move move = iterator.next();
board.move(move);
if (board.checkCheck(getColor())) iterator.remove();
board.revert();
}
return moves;
}
protected abstract List<Move> getPseudolegalMoves(Position pos);
public abstract boolean isValidMove(Move move);
protected boolean isFreePath(Move move) {
// Only check destination by default
return checkDestination(move);
}
/**
* Checks if the destination of a move is empty or a piece from the opposing
* team
*
* @param move The move to check
* @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();
}
@Override
public Object clone() {
Piece piece = null;
try {
piece = (Piece) super.clone();
} catch (CloneNotSupportedException ex) {
ex.printStackTrace();
}
return piece;
}
public abstract Type getType();
public Color getColor() { return color; }
public int getMoveCounter() { return moveCounter; }
public void incMoveCounter() {
++moveCounter;
}
public void decMoveCounter() {
--moveCounter;
}
public static enum Type {
KING, QUEEN, ROOK, KNIGHT, BISHOP, PAWN
}
public static enum Color {
WHITE, BLACK;
public Color opposite() {
return this == WHITE ? BLACK : WHITE;
}
}
}

View File

@ -1,22 +0,0 @@
package dev.kske.chess.board;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>Position.java</strong><br>
* Created: <strong>02.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class Position {
public final int x, y;
public Position(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public String toString() {
return String.format("[%d, %d]", x, y);
}
}

View File

@ -1,119 +0,0 @@
package dev.kske.chess.board;
import java.util.ArrayList;
import java.util.List;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>Queen.java</strong><br>
* Created: <strong>01.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class Queen extends Piece {
public Queen(Color color, Board board) {
super(color, board);
}
@Override
public boolean isValidMove(Move move) {
return ((move.isHorizontal() || move.isVertical()) || move.isDiagonal()) && isFreePath(move);
}
@Override
protected boolean isFreePath(Move move) {
if (move.isHorizontal()) {
for (int i = move.pos.x + move.xSign; i != move.dest.x; i += move.xSign)
if (board.getBoardArr()[i][move.pos.y] != null) return false;
} else if (move.isVertical()) {
for (int i = move.pos.y + move.ySign; i != move.dest.y; i += move.ySign)
if (board.getBoardArr()[move.pos.x][i] != null) return false;
} else {
for (int i = move.pos.x + move.xSign, j = move.pos.y
+ move.ySign; i != move.dest.x; i += move.xSign, j += move.ySign)
if (board.getBoardArr()[i][j] != null) return false;
}
return checkDestination(move);
}
@Override
protected List<Move> getPseudolegalMoves(Position pos) {
List<Move> moves = new ArrayList<>();
// Horizontal moves to the right
for (int i = pos.x + 1; i < 8; i++) {
Move move = new Move(pos, new Position(i, pos.y));
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) {
moves.add(move);
if (board.getDest(move) != null) break;
} else break;
}
// Horizontal moves to the left
for (int i = pos.x - 1; i >= 0; i--) {
Move move = new Move(pos, new Position(i, pos.y));
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) {
moves.add(move);
if (board.getDest(move) != null) break;
} else break;
}
// Vertical moves to the top
for (int i = pos.y - 1; i >= 0; i--) {
Move move = new Move(pos, new Position(pos.x, i));
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) {
moves.add(move);
if (board.getDest(move) != null) break;
} else break;
}
// Vertical moves to the bottom
for (int i = pos.y + 1; i < 8; i++) {
Move move = new Move(pos, new Position(pos.x, i));
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) {
moves.add(move);
if (board.getDest(move) != null) break;
} else break;
}
// Diagonal moves to the lower right
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));
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) {
moves.add(move);
if (board.getDest(move) != null) break;
} else break;
}
// Diagonal moves to the lower left
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));
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) {
moves.add(move);
if (board.getDest(move) != null) break;
} else break;
}
// Diagonal moves to the upper right
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));
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) {
moves.add(move);
if (board.getDest(move) != null) break;
} else break;
}
// Diagonal moves to the upper left
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));
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) {
moves.add(move);
if (board.getDest(move) != null) break;
} else break;
}
return moves;
}
@Override
public Type getType() { return Type.QUEEN; }
}

View File

@ -1,104 +0,0 @@
package dev.kske.chess.game;
import java.util.HashMap;
import java.util.Map;
import dev.kske.chess.board.Board;
import dev.kske.chess.board.GameState;
import dev.kske.chess.board.Move;
import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.game.ai.AIPlayer;
import dev.kske.chess.ui.BoardComponent;
import dev.kske.chess.ui.BoardPane;
import dev.kske.chess.ui.OverlayComponent;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>Game.java</strong><br>
* Created: <strong>06.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class Game {
private Map<Color, Player> players;
private Board board;
private OverlayComponent overlayComponent;
private BoardComponent boardComponent;
public Game(Map<Color, Player> players, BoardPane boardPane) {
this.players = players;
this.overlayComponent = boardPane.getOverlayComponent();
this.boardComponent = boardPane.getBoardComponent();
this.board = new Board();
boardComponent.setBoard(board);
// Initialize the game variable in each player
players.values().forEach(player -> player.setGame(this));
}
public static Game createNatural(BoardPane boardPane) {
Map<Color, Player> players = new HashMap<>();
OverlayComponent overlay = boardPane.getOverlayComponent();
players.put(Color.WHITE, new NaturalPlayer(Color.WHITE, overlay));
players.put(Color.BLACK, new NaturalPlayer(Color.BLACK, overlay));
return new Game(players, boardPane);
}
public static Game createNaturalVsAI(BoardPane boardPane, int maxDepth, int alphaBeta) {
Map<Color, Player> players = new HashMap<>();
OverlayComponent overlay = boardPane.getOverlayComponent();
players.put(Color.WHITE, new NaturalPlayer(Color.WHITE, overlay));
players.put(Color.BLACK, new AIPlayer(Color.BLACK, maxDepth, alphaBeta));
return new Game(players, boardPane);
}
public static Game createAIVsAI(BoardPane boardPane, int maxDepthW, int maxDepthB, int alphaBetaW, int alphaBetaB) {
Map<Color, Player> players = new HashMap<>();
players.put(Color.WHITE, new AIPlayer(Color.WHITE, maxDepthW, alphaBetaW));
players.put(Color.BLACK, new AIPlayer(Color.BLACK, maxDepthB, alphaBetaB));
return new Game(players, boardPane);
}
public void onMove(Player player, Move move) {
if (board.getPos(move).getColor() == player.color && board.attemptMove(move)) {
System.out.printf("%s: %s%n", player.color, move);
GameState eventType = board.getGameEventType(board.getDest(move).getColor().opposite());
switch (eventType) {
case CHECKMATE:
case STALEMATE:
System.out.printf("%s in %s!%n", player.color.opposite(), eventType);
break;
case CHECK:
System.out.printf("%s in check!%n", player.color.opposite());
default:
boardComponent.repaint();
players.get(player.color.opposite()).requestMove();
}
overlayComponent.displayArrow(move);
} else player.requestMove();
}
public void start() {
players.get(Color.WHITE).requestMove();
}
public void reset() {
players.forEach((k, v) -> v.cancelMove());
board.initializeDefaultPositions();
boardComponent.repaint();
overlayComponent.clearDots();
overlayComponent.clearArrow();
}
/**
* Removed all connections between the game and the ui.
*/
public void disconnect() {
players.values().forEach(Player::disconnect);
}
public Board getBoard() { return board; }
}

View File

@ -1,88 +0,0 @@
package dev.kske.chess.game;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.List;
import java.util.stream.Collectors;
import dev.kske.chess.board.Board;
import dev.kske.chess.board.Move;
import dev.kske.chess.board.Move.Type;
import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.board.Position;
import dev.kske.chess.ui.OverlayComponent;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>NaturalPlayer.java</strong><br>
* Created: <strong>06.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class NaturalPlayer extends Player implements MouseListener {
private final OverlayComponent overlayComponent;
private boolean moveRequested;
private Position pos;
public NaturalPlayer(Color color, OverlayComponent overlayComponent) {
super(color);
this.overlayComponent = overlayComponent;
moveRequested = false;
overlayComponent.addMouseListener(this);
}
@Override
public void requestMove() {
moveRequested = true;
}
@Override
public void cancelMove() {
moveRequested = false;
}
@Override
public void disconnect() {
overlayComponent.removeMouseListener(this);
}
@Override
public void mousePressed(MouseEvent evt) {
if (!moveRequested) return;
if (pos == null) {
pos = new Position(evt.getPoint().x / overlayComponent.getTileSize(),
evt.getPoint().y / overlayComponent.getTileSize());
Board board = (Board) NaturalPlayer.this.board.clone();
if (board.get(pos) != null && board.get(pos).getColor() == color) {
List<Position> positions = board.getMoves(pos)
.stream()
.map(move -> move.dest)
.collect(Collectors.toList());
overlayComponent.displayDots(positions);
} else pos = null;
} else {
Position dest = new Position(evt.getPoint().x / overlayComponent.getTileSize(),
evt.getPoint().y / overlayComponent.getTileSize());
overlayComponent.clearDots();
moveRequested = false;
game.onMove(NaturalPlayer.this, new Move(pos, dest, Type.UNKNOWN));
pos = null;
}
}
@Override
public void mouseClicked(MouseEvent e) {}
@Override
public void mouseReleased(MouseEvent e) {}
@Override
public void mouseEntered(MouseEvent e) {}
@Override
public void mouseExited(MouseEvent e) {}
}

View File

@ -1,42 +0,0 @@
package dev.kske.chess.game;
import dev.kske.chess.board.Board;
import dev.kske.chess.board.Piece.Color;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>Player.java</strong><br>
* Created: <strong>06.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public abstract class Player {
protected Game game;
protected Board board;
protected Color color;
public Player(Color color) {
this.color = color;
}
public abstract void requestMove();
public abstract void cancelMove();
public abstract void disconnect();
public Game getGame() { return game; }
public void setGame(Game game) {
this.game = game;
board = game.getBoard();
}
public Board getBoard() { return board; }
public void setBoard(Board board) { this.board = board; }
public Color getColor() { return color; }
public void setColor(Color color) { this.color = color; }
}

View File

@ -1,104 +0,0 @@
package dev.kske.chess.game.ai;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
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 dev.kske.chess.board.Board;
import dev.kske.chess.board.Move;
import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.game.Player;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>AIPlayer.java</strong><br>
* Created: <strong>06.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class AIPlayer extends Player {
private int availableProcessors;
private int maxDepth;
private int alphaBetaThreshold;
private volatile boolean exitRequested;
private volatile ExecutorService executor;
public AIPlayer(Color color, int maxDepth, int alphaBetaThreshold) {
super(color);
availableProcessors = Runtime.getRuntime().availableProcessors();
this.maxDepth = maxDepth;
this.alphaBetaThreshold = alphaBetaThreshold;
exitRequested = false;
}
@Override
public void requestMove() {
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 = (Board) AIPlayer.this.board.clone();
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++) {
if (rem-- > 0) ++endIndex;
endIndex += step;
processors.add(new MoveProcessor((Board) board.clone(), moves.subList(beginIndex, endIndex), color,
maxDepth, alphaBetaThreshold));
beginIndex = endIndex;
}
/*
* Execute processors, get the best result and pass it back to the Game class
*/
executor = Executors.newFixedThreadPool(numThreads);
List<ProcessingResult> results = new ArrayList<>(numThreads);
try {
List<Future<ProcessingResult>> futures = executor.invokeAll(processors);
for (Future<ProcessingResult> f : futures)
results.add(f.get());
executor.shutdown();
} catch (InterruptedException | ExecutionException ex) {
ex.printStackTrace();
}
results.sort((r1, r2) -> Integer.compare(r2.score, r1.score));
if (!exitRequested) SwingUtilities.invokeLater(() -> game.onMove(this, results.get(0).move));
}, "AIPlayer calculation setup").start();
}
@Override
public void cancelMove() {
exitRequested = true;
if (executor != null) {
executor.shutdownNow();
try {
executor.awaitTermination(500, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void disconnect() {}
}

View File

@ -1,60 +0,0 @@
package dev.kske.chess.game.ai;
import java.util.List;
import java.util.concurrent.Callable;
import dev.kske.chess.board.Board;
import dev.kske.chess.board.Move;
import dev.kske.chess.board.Piece.Color;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>MoveProcessor.java</strong><br>
* Created: <strong>08.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class MoveProcessor implements Callable<ProcessingResult> {
private final Board board;
private final List<Move> rootMoves;
private final Color color;
private final int maxDepth;
private final int alphaBetaThreshold;
private Move bestMove;
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
public ProcessingResult call() throws Exception {
int score = miniMax(board, rootMoves, color, 0);
return new ProcessingResult(bestMove, score);
}
private int miniMax(Board board, List<Move> moves, Color color, int depth) {
int bestValue = Integer.MIN_VALUE;
for (Move move : moves) {
board.move(move);
int teamValue = board.evaluate(color);
int enemyValue = board.evaluate(color.opposite());
int valueChange = teamValue - enemyValue;
if (depth < maxDepth && valueChange >= alphaBetaThreshold)
valueChange -= miniMax(board, board.getMoves(color.opposite()), color.opposite(), depth + 1);
if (valueChange > bestValue) {
bestValue = valueChange;
if (depth == 0) bestMove = move;
}
board.revert();
}
return bestValue;
}
}

View File

@ -1,25 +0,0 @@
package dev.kske.chess.game.ai;
import dev.kske.chess.board.Move;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>ProcessingResult.java</strong><br>
* Created: <strong>08.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class ProcessingResult {
public final Move move;
public final int score;
public ProcessingResult(Move move, int score) {
this.move = move;
this.score = score;
}
@Override
public String toString() {
return String.format("ProcessingResult[Move = %s, Score = %d]", move, score);
}
}

View File

@ -1,82 +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>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
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

@ -1,55 +0,0 @@
package dev.kske.chess.ui;
import java.awt.Dimension;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import javax.swing.JLayeredPane;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>BoardPane.java</strong><br>
* Created: <strong>08.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class BoardPane extends JLayeredPane {
private static final long serialVersionUID = -5415058382478806092L;
private final BoardComponent boardComponent;
private final OverlayComponent overlayComponent;
private int tileSize;
public BoardPane() {
boardComponent = new BoardComponent(this);
overlayComponent = new OverlayComponent(this);
add(boardComponent, Integer.valueOf(1));
add(overlayComponent, Integer.valueOf(2));
/*
* Add a component listener for adjusting the tile size on resizing.
* The size of the board is assumed to be 8x8, as well as the both the board and
* the tiles being square.
*/
addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
tileSize = getWidth() / 8;
TextureUtil.scalePieceTextures(tileSize);
}
});
setSize(getPreferredSize());
}
@Override
public Dimension getPreferredSize() { return new Dimension(480, 480); }
public BoardComponent getBoardComponent() { return boardComponent; }
public OverlayComponent getOverlayComponent() { return overlayComponent; }
public int getTileSize() { return tileSize; }
}

View File

@ -1,80 +0,0 @@
package dev.kske.chess.ui;
import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.Toolkit;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import dev.kske.chess.game.Game;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>MainWindow.java</strong><br>
* Created: <strong>01.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class MainWindow {
private JFrame mframe;
private BoardPane boardPane;
private Game game;
/**
* Launch the application.
*/
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
try {
MainWindow window = new MainWindow();
window.mframe.setVisible(true);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
/**
* Create the application.
*/
public MainWindow() {
initialize();
}
/**
* Initialize the contents of the frame.
*/
private void initialize() {
mframe = new JFrame();
mframe.setResizable(false);
mframe.setBounds(100, 100, 494, 565);
mframe.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mframe.setIconImage(Toolkit.getDefaultToolkit().getImage(getClass().getResource("/pieces/king_white.png")));
boardPane = new BoardPane();
mframe.getContentPane().add(boardPane, BorderLayout.CENTER);
mframe.setJMenuBar(new MenuBar(this));
JPanel toolPanel = new JPanel();
mframe.getContentPane().add(toolPanel, BorderLayout.NORTH);
JButton btnRestart = new JButton("Restart");
btnRestart.addActionListener((evt) -> { if (game != null) game.reset(); game.start(); });
toolPanel.add(btnRestart);
mframe.pack();
mframe.setLocationRelativeTo(null);
}
public BoardPane getBoardPane() { return boardPane; }
public Game getGame() { return game; }
public void setGame(Game game) { this.game = game; }
}

View File

@ -1,64 +0,0 @@
package dev.kske.chess.ui;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import dev.kske.chess.game.Game;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>MenuBar.java</strong><br>
* Created: <strong>16.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class MenuBar extends JMenuBar {
private static final long serialVersionUID = -7221583703531248228L;
private final MainWindow mainWindow;
private final BoardPane boardPane;
public MenuBar(MainWindow mainWindow) {
this.mainWindow = mainWindow;
boardPane = mainWindow.getBoardPane();
initGameMenu();
}
private void initGameMenu() {
JMenu gameMenu = new JMenu("Game");
JMenuItem naturalMenuItem = new JMenuItem("Game against natural opponent");
JMenuItem aiMenuItem = new JMenuItem("Game against artificial opponent");
JMenuItem aiVsAiMenuItem = new JMenuItem("Watch AI vs. AI");
naturalMenuItem.addActionListener((evt) -> startGame(Game.createNatural(boardPane)));
aiMenuItem.addActionListener((evt) -> {
AIConfigDialog dialog = new AIConfigDialog();
dialog.setVisible(true);
if (dialog.isStartGame())
startGame(Game.createNaturalVsAI(boardPane, dialog.getMaxDepth(), dialog.getAlphaBetaThreshold()));
});
aiVsAiMenuItem.addActionListener((evt) -> startGame(Game.createAIVsAI(boardPane, 4, 3, -10, -10)));
gameMenu.add(naturalMenuItem);
gameMenu.add(aiMenuItem);
gameMenu.add(aiVsAiMenuItem);
add(gameMenu);
// Start a game
naturalMenuItem.doClick();
}
private void startGame(Game game) {
mainWindow.setGame(game);
// Update board and board component
game.reset();
game.start();
}
}

View File

@ -1,116 +0,0 @@
package dev.kske.chess.ui;
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.util.ArrayList;
import java.util.List;
import javax.swing.JComponent;
import dev.kske.chess.board.Move;
import dev.kske.chess.board.Position;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>OverlayComponent.java</strong><br>
* Created: <strong>08.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class OverlayComponent extends JComponent {
private static final long serialVersionUID = -7326936060890082183L;
private final BoardPane boardPane;
private List<Position> dots;
private Move arrow;
public OverlayComponent(BoardPane boardPane) {
this.boardPane = boardPane;
setSize(boardPane.getPreferredSize());
dots = new ArrayList<>();
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
final int tileSize = getTileSize();
// Draw possible moves if a piece was selected
if (!dots.isEmpty()) {
g.setColor(Color.green);
int radius = tileSize / 4;
for (Position dot : dots)
g.fillOval(dot.x * tileSize + tileSize / 2 - radius / 2,
dot.y * tileSize + tileSize / 2 - radius / 2,
radius,
radius);
}
if (arrow != null) {
g.setColor(new Color(255, 0, 0, 127));
Point pos = new Point(arrow.pos.x * tileSize + tileSize / 2, arrow.pos.y * tileSize + tileSize / 2);
Point dest = new Point(arrow.dest.x * tileSize + tileSize / 2, arrow.dest.y * tileSize + tileSize / 2);
((Graphics2D) g).fill(createArrowShape(pos, dest));
}
}
private Shape createArrowShape(Point pos, Point dest) {
Polygon arrowPolygon = new Polygon();
arrowPolygon.addPoint(-6, 1);
arrowPolygon.addPoint(3, 1);
arrowPolygon.addPoint(3, 3);
arrowPolygon.addPoint(6, 0);
arrowPolygon.addPoint(3, -3);
arrowPolygon.addPoint(3, -1);
arrowPolygon.addPoint(-6, -1);
Point midPoint = midpoint(pos, dest);
double rotate = Math.atan2(dest.y - pos.y, dest.x - pos.x);
double ptDistance = pos.distance(dest);
double scale = ptDistance / 12.0; // 12 because it's the length of the arrow
// polygon.
AffineTransform transform = new AffineTransform();
transform.translate(midPoint.x, midPoint.y);
transform.rotate(rotate);
transform.scale(scale, 5);
return transform.createTransformedShape(arrowPolygon);
}
private Point midpoint(Point p1, Point p2) {
return new Point((int) ((p1.x + p2.x) / 2.0), (int) ((p1.y + p2.y) / 2.0));
}
public void displayDots(List<Position> dots) {
this.dots.clear();
this.dots.addAll(dots);
repaint();
}
public void clearDots() {
dots.clear();
repaint();
}
public void displayArrow(Move arrow) {
this.arrow = arrow;
repaint();
}
public void clearArrow() {
arrow = null;
repaint();
}
public int getTileSize() { return boardPane.getTileSize(); }
}

View File

@ -7,10 +7,18 @@ import java.util.List;
* Project: <strong>Chess</strong><br>
* File: <strong>Bishop.java</strong><br>
* Created: <strong>01.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
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) {
super(color, board);
}
@ -20,14 +28,6 @@ public class Bishop extends Piece {
return move.isDiagonal() && isFreePath(move);
}
@Override
protected boolean isFreePath(Move move) {
for (int i = move.pos.x + move.xSign, j = move.pos.y
+ move.ySign; i != move.dest.x; i += move.xSign, j += move.ySign)
if (board.getBoardArr()[i][j] != null) return false;
return checkDestination(move);
}
@Override
protected List<Move> getPseudolegalMoves(Position pos) {
List<Move> moves = new ArrayList<>();
@ -35,41 +35,58 @@ public class Bishop extends Piece {
// Diagonal moves to the lower right
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));
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) {
if (
board.getDest(move) == null
|| board.getDest(move).getColor() != getColor()
) {
moves.add(move);
if (board.getDest(move) != null) break;
} else break;
if (board.getDest(move) != null)
break;
} else
break;
}
// Diagonal moves to the lower left
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));
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) {
if (
board.getDest(move) == null
|| board.getDest(move).getColor() != getColor()
) {
moves.add(move);
if (board.getDest(move) != null) break;
} else break;
if (board.getDest(move) != null)
break;
} else
break;
}
// Diagonal moves to the upper right
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));
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) {
if (
board.getDest(move) == null
|| board.getDest(move).getColor() != getColor()
) {
moves.add(move);
if (board.getDest(move) != null) break;
} else break;
if (board.getDest(move) != null)
break;
} else
break;
}
// Diagonal moves to the upper left
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));
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) {
if (
board.getDest(move) == null
|| board.getDest(move).getColor() != getColor()
) {
moves.add(move);
if (board.getDest(move) != null) break;
} else break;
if (board.getDest(move) != null)
break;
} else
break;
}
return moves;
}
@Override
public Type getType() { return Type.BISHOP; }
public int getValue() { return 30; }
}

View File

@ -0,0 +1,461 @@
package dev.kske.chess.board;
import java.util.*;
import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.event.MoveEvent;
import dev.kske.eventbus.EventBus;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>Board.java</strong><br>
* Created: <strong>01.07.2019</strong><br>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class Board {
private Piece[][] boardArr = new Piece[8][8];
private Map<Color, Position> kingPos = new EnumMap<>(Color.class);
private Log log = new Log();
/**
* Initializes the board with the default chess starting position.
*/
public Board() {
initDefaultPositions();
}
/**
* Creates a copy of another {@link Board} instance.<br>
* The created object is a deep copy, and can optionally contain the move
* history of the Board to copy.
*
* @param other The Board instance to copy
* @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) {
for (int i = 0; i < 8; i++)
for (int j = 0; j < 8; j++) {
if (other.boardArr[i][j] == null)
continue;
boardArr[i][j] = (Piece) other.boardArr[i][j].clone();
boardArr[i][j].board = this;
}
kingPos.putAll(other.kingPos);
log = new Log(other.log, copyVariations);
// Synchronize the current move node with the board
while (log.getLast().hasVariations())
log.selectNextNode(0);
}
/**
* Moves a piece across the board if the move is legal.
*
* @param move The move to execute
* @return {@code true}, if the attempted move was legal and thus executed
*/
public boolean attemptMove(Move move) {
Piece piece = getPos(move);
if (piece == null || !piece.isValidMove(move))
return false;
// Move piece
move(move);
// Revert move if it caused a check for its team
if (checkCheck(piece.getColor())) {
revert();
return false;
}
return true;
}
/**
* Moves a piece across the board without checking if the move is legal.
*
* @param move The move to execute
*/
public void move(Move move) {
Piece piece = getPos(move);
Piece capturePiece = getDest(move);
// Execute the move
move.execute(this);
// Update the king's position if the moved piece is the king
if (piece instanceof King)
kingPos.put(piece.getColor(), move.getDest());
// Update log
log.add(move, piece, capturePiece);
}
/**
* Moves a piece across the board without checking if the move is legal.
*
* @param sanMove The move to execute in SAN (Standard Algebraic Notation)
*/
public void move(String sanMove) {
move(Move.fromSAN(sanMove, this));
}
/**
* Reverts the last move and removes it from the log.
*/
public void revert() {
MoveNode moveNode = log.getLast();
Move move = moveNode.move;
// Revert the move
move.revert(this, moveNode.capturedPiece);
// Update the king's position if the moved piece is the king
if (getPos(move) instanceof King)
kingPos.put(getPos(move).getColor(), move.getPos());
// Update log
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
*
* @param color The color to generate the moves for
* @return A list of all legal moves
*/
public List<Move> getMoves(Color color) {
List<Move> moves = new ArrayList<>();
for (int i = 0; i < 8; i++)
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)));
return moves;
}
/**
* 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.
*
* @param color The color of the king to check
* @return {@code true}, if the king is in check
*/
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.
*
* @param dest the field to check
* @param color the color of a potential attacker piece
* @return {@code true} if a move with the destination {@code dest}
*/
public boolean isAttacked(Position dest, Color color) {
for (int i = 0; i < 8; i++)
for (int j = 0; j < 8; j++) {
Position pos = new Position(i, j);
if (
get(pos) != null && get(pos).getColor() == color
&& get(pos).isValidMove(new Move(pos, dest))
)
return true;
}
return false;
}
/**
* Checks, if the king is in checkmate.
* This requires the king to already be in check!
*
* @param color The color of the king to check
* @return {@code true}, if the king is in checkmate
*/
public boolean checkCheckmate(Color color) {
// Return false immediately if the king can move
if (!getMoves(kingPos.get(color)).isEmpty())
return false;
for (Move move : getMoves(color)) {
move(move);
boolean check = checkCheck(color);
revert();
if (!check)
return false;
}
return true;
}
/**
* Checks whether the a check, checkmate, stalemate of none of the above is
* 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;
}
/**
* Initialized the board array with the default chess pieces and positions.
*/
public void initDefaultPositions() {
// Initialize pawns
for (int i = 0; i < 8; i++) {
boardArr[i][1] = new Pawn(Color.BLACK, this);
boardArr[i][6] = new Pawn(Color.WHITE, this);
}
// Initialize kings
boardArr[4][0] = new King(Color.BLACK, this);
boardArr[4][7] = new King(Color.WHITE, this);
// Initialize king position objects
kingPos.put(Color.BLACK, new Position(4, 0));
kingPos.put(Color.WHITE, new Position(4, 7));
// Initialize queens
boardArr[3][0] = new Queen(Color.BLACK, this);
boardArr[3][7] = new Queen(Color.WHITE, this);
// Initialize rooks
boardArr[0][0] = new Rook(Color.BLACK, this);
boardArr[0][7] = new Rook(Color.WHITE, this);
boardArr[7][0] = new Rook(Color.BLACK, this);
boardArr[7][7] = new Rook(Color.WHITE, this);
// Initialize knights
boardArr[1][0] = new Knight(Color.BLACK, this);
boardArr[1][7] = new Knight(Color.WHITE, this);
boardArr[6][0] = new Knight(Color.BLACK, this);
boardArr[6][7] = new Knight(Color.WHITE, this);
// Initialize bishops
boardArr[2][0] = new Bishop(Color.BLACK, this);
boardArr[2][7] = new Bishop(Color.WHITE, this);
boardArr[5][0] = new Bishop(Color.BLACK, this);
boardArr[5][7] = new Bishop(Color.WHITE, this);
// Clear all other tiles
for (int i = 0; i < 8; i++)
for (int j = 2; j < 6; j++)
boardArr[i][j] = null;
log.reset();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + Arrays.deepHashCode(boardArr);
result = prime * result + Objects.hash(kingPos, log);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Board other = (Board) obj;
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
* @return The piece at the position
*/
public Piece get(Position pos) {
return boardArr[pos.x][pos.y];
}
/**
* Searches for a {@link Piece} inside a file (A - H).
*
* @param pieceClass The class of the piece to search for
* @param file The file in which to search for the piece
* @return The rank (1 - 8) of the first piece with the specified type and
* current color in the file, or {@code -1} if there isn't any
*/
public int get(Class<? extends Piece> pieceClass, char file) {
int x = file - 97;
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;
return -1;
}
/**
* Searches for a {@link Piece} inside a rank (1 - 8).
*
* @param pieceClass The class of the piece to search for
* @param rank The rank in which to search for the piece
* @return The file (A - H) of the first piece with the specified type and
* current color in the file, or {@code -} if there isn't any
*/
public char get(Class<? extends Piece> pieceClass, int rank) {
int y = rank - 1;
for (int i = 0; i < 8; i++)
if (
boardArr[i][y] != null
&& boardArr[i][y].getClass() == pieceClass
&& boardArr[i][y].getColor() == log.getActiveColor()
)
return (char) (i + 97);
return '-';
}
/**
* Searches for a {@link Piece} that can move to a {@link Position}.
*
* @param pieceClass The class of the piece to search for
* @param dest The destination that the piece is required to reach
* @return The position of a piece that can move to the specified
* destination
*/
public Position get(Class<? extends Piece> pieceClass, Position dest) {
for (int i = 0; i < 8; i++)
for (int j = 0; j < 8; j++)
if (
boardArr[i][j] != null
&& boardArr[i][j].getClass() == pieceClass
&& boardArr[i][j].getColor() == log.getActiveColor()
) {
Position pos = new Position(i, j);
if (boardArr[i][j].isValidMove(new Move(pos, dest)))
return pos;
}
return null;
}
/**
* Places a piece at a position.
*
* @param pos The position to place the piece at
* @param piece The piece to place
*/
public void set(Position pos, Piece piece) {
boardArr[pos.x][pos.y] = piece;
}
/**
* @param move The move from which position to return a piece
* @return The piece at the position of the move
*/
public Piece getPos(Move move) {
return get(move.getPos());
}
/**
* @param move The move from which destination to return a piece
* @return The piece at the destination of the move
*/
public Piece getDest(Move move) {
return get(move.getDest());
}
/**
* Places a piece at the position of a move.
*
* @param move The move at which position to place the piece
* @param piece The piece to place
*/
public void setPos(Move move, Piece piece) {
set(move.getPos(), piece);
}
/**
* Places a piece at the destination of a move.
*
* @param move The move at which destination to place the piece
* @param piece The piece to place
*/
public void setDest(Move move, Piece piece) {
set(move.getDest(), piece);
}
/**
* @return The board array
*/
public Piece[][] getBoardArr() { return boardArr; }
/**
* @return The move log
*/
public Log getLog() { return log; }
}

View File

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

View File

@ -0,0 +1,62 @@
package dev.kske.chess.board;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>Castling.java</strong><br>
* Created: <strong>2 Nov 2019</strong><br>
*
* @since Chess v0.5-alpha
* @author Kai S. K. Engelbart
*/
public class Castling extends Move {
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) {
super(pos, dest);
rookMove = dest.x == 6 ? new Move(7, pos.y, 5, pos.y)
: new Move(0, pos.y, 3, pos.y);
}
/**
* 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
public void execute(Board board) {
// Move the king and the rook
super.execute(board);
rookMove.execute(board);
}
@Override
public void revert(Board board, Piece capturedPiece) {
// Move the king and the rook
super.revert(board, capturedPiece);
rookMove.revert(board, null);
}
/**
* @return {@code O-O-O} for a queenside castling or {@code O-O} for a
* kingside
* castling
*/
@Override
public String toSAN(Board board) {
return rookMove.pos.x == 0 ? "O-O-O" : "O-O";
}
}

View File

@ -0,0 +1,57 @@
package dev.kske.chess.board;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>EnPassant.java</strong><br>
* Created: <strong>2 Nov 2019</strong><br>
*
* @since Chess v0.5-alpha
* @author Kai S. K. Engelbart
*/
public class EnPassant extends Move {
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) {
super(pos, dest);
capturePos = new Position(dest.x, dest.y - ySign);
}
/**
* 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
public void execute(Board board) {
super.execute(board);
board.set(capturePos, null);
}
@Override
public void revert(Board board, Piece capturedPiece) {
super.revert(board, capturedPiece);
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; }
}

View File

@ -0,0 +1,238 @@
package dev.kske.chess.board;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.exception.ChessException;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>FENString.java</strong><br>
* Created: <strong>20 Oct 2019</strong><br>
* <br>
* Represents a FEN string and enables parsing an existing FEN string or
* serializing a {@link Board} to one.
*
* @since Chess v0.5-alpha
* @author Kai S. K. Engelbart
*/
public class FENString {
private Board board;
private String piecePlacement, castlingAvailability;
private int halfmoveClock, fullmoveNumber;
private Color activeColor;
private Position enPassantTargetSquare;
/**
* Constructs a {@link FENString} representing the starting position
* {@code rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1}.
*/
public FENString() {
board = new Board();
piecePlacement = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR";
activeColor = Color.WHITE;
castlingAvailability = "KQkq";
halfmoveClock = 0;
fullmoveNumber = 1;
}
/**
* Constructs a {@link FENString} by parsing an existing string.
*
* @param fen the FEN string to parse
* @throws ChessException if the FEN string contains invalid syntax
*/
public FENString(String fen) throws ChessException {
// Check fen string against regex
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+)$"
);
Matcher matcher = fenPattern.matcher(fen);
if (!matcher.find())
throw new ChessException(
"FEN string does not match pattern " + fenPattern.pattern()
);
// Initialize data fields
piecePlacement = matcher.group("piecePlacement");
activeColor
= Color.fromFirstChar(matcher.group("activeColor").charAt(0));
castlingAvailability = matcher.group("castlingAvailability");
if (!matcher.group("enPassantTargetSquare").equals("-"))
enPassantTargetSquare
= Position.fromLAN(matcher.group("enPassantTargetSquare"));
halfmoveClock = Integer.parseInt(matcher.group("halfmoveClock"));
fullmoveNumber = Integer.parseInt(matcher.group("fullmoveNumber"));
// Initialize and clean board
board = new Board();
for (int i = 0; i < 8; i++)
for (int j = 0; j < 8; j++)
board.getBoardArr()[i][j] = null;
// Parse individual fields
// Piece placement
final String[] rows = piecePlacement.split("/");
if (rows.length != 8)
throw new ChessException(
"FEN string contains invalid piece placement"
);
for (int i = 0; i < 8; i++) {
final char[] cols = rows[i].toCharArray();
int j = 0;
for (char c : cols)
// Empty space
if (Character.isDigit(c))
j += Character.getNumericValue(c);
else {
Color color
= Character.isUpperCase(c) ? Color.WHITE : Color.BLACK;
try {
Constructor<? extends Piece> pieceConstructor = Piece
.fromFirstChar(c)
.getDeclaredConstructor(Color.class, Board.class);
pieceConstructor.setAccessible(true);
board.getBoardArr()[j][i]
= pieceConstructor.newInstance(color, board);
} catch (
InstantiationException
| IllegalAccessException
| IllegalArgumentException
| InvocationTargetException
| NoSuchMethodException
| SecurityException e
) {
e.printStackTrace();
}
++j;
}
}
// Active color
board.getLog().setActiveColor(activeColor);
// Castling availability
boolean castlingRights[] = new boolean[4];
for (char c : castlingAvailability.toCharArray())
switch (c) {
case 'K':
castlingRights[MoveNode.WHITE_KINGSIDE] = true;
break;
case 'Q':
castlingRights[MoveNode.WHITE_QUEENSIDE] = true;
break;
case 'k':
castlingRights[MoveNode.BLACK_KINGSIDE] = true;
break;
case 'q':
castlingRights[MoveNode.BLACK_QUEENSIDE] = true;
break;
}
board.getLog().setCastlingRights(castlingRights);
// En passant square
board.getLog().setEnPassant(enPassantTargetSquare);
// Halfmove clock
board.getLog().setHalfmoveClock(halfmoveClock);
// Fullmove number
board.getLog().setFullmoveNumber(fullmoveNumber);
}
/**
* Constructs a {@link FENString} form a {@link Board} object.
*
* @param board the {@link Board} object to encode in this {@link FENString}
*/
public FENString(Board board) {
this.board = board;
// Serialize individual fields
// Piece placement
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 8; i++) {
int empty = 0;
for (int j = 0; j < 8; j++) {
final Piece piece = board.getBoardArr()[j][i];
if (piece == null)
++empty;
else {
// Write empty field count
if (empty > 0) {
sb.append(empty);
empty = 0;
}
// Write piece character
char p = piece.firstChar();
sb.append(
piece.getColor() == Color.WHITE
? Character.toUpperCase(p)
: p
);
}
}
// Write empty field count
if (empty > 0) {
sb.append(empty);
empty = 0;
}
if (i < 7)
sb.append('/');
}
piecePlacement = sb.toString();
// Active color
activeColor = board.getLog().getActiveColor();
// Castling availability
castlingAvailability = "";
final char castlingRightsChars[] = new char[] {
'K', 'Q', 'k', 'q'
};
for (int i = 0; i < 4; i++)
if (board.getLog().getCastlingRights()[i])
castlingAvailability += castlingRightsChars[i];
if (castlingAvailability.isEmpty())
castlingAvailability = "-";
// En passant availability
enPassantTargetSquare = board.getLog().getEnPassant();
// Halfmove clock
halfmoveClock = board.getLog().getHalfmoveClock();
// Fullmove counter
fullmoveNumber = board.getLog().getFullmoveNumber();
}
/**
* Exports this {@link FENString} object to a FEN string.
*
* @return a FEN string representing the board
*/
@Override
public String toString() {
return String.format(
"%s %c %s %s %d %d",
piecePlacement,
activeColor.firstChar(),
castlingAvailability,
enPassantTargetSquare == null ? "-" : enPassantTargetSquare.toLAN(),
halfmoveClock,
fullmoveNumber
);
}
/**
* @return a {@link Board} object corresponding to this {@link FENString}
*/
public Board getBoard() { return board; }
}

View File

@ -0,0 +1,106 @@
package dev.kske.chess.board;
import java.util.ArrayList;
import java.util.List;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>King.java</strong><br>
* Created: <strong>01.07.2019</strong><br>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class King extends Piece {
/**
* 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
public boolean isValidMove(Move move) {
return move.getxDist() == 2 && move.getyDist() == 0
&& (move.getDest().x == 6 && canCastleKingside()
|| move.getDest().x == 2 && canCastleQueenside())
|| move.getxDist() <= 1 && move.getyDist() <= 1
&& checkDestination(move);
}
@Override
protected List<Move> getPseudolegalMoves(Position pos) {
List<Move> moves = new ArrayList<>();
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++
)
if (i != pos.x || j != pos.y) {
Move move = new Move(pos, new Position(i, j));
if (
board.getDest(move) == null
|| board.getDest(move).getColor() != getColor()
)
moves.add(move);
}
// Castling
if (canCastleKingside())
moves.add(new Castling(pos, new Position(6, pos.y)));
if (canCastleQueenside())
moves.add(new Castling(pos, new Position(2, pos.y)));
return moves;
}
private boolean canCastleKingside() {
if (
board.getLog().getCastlingRights()[getColor() == Color.WHITE
? MoveNode.WHITE_KINGSIDE
: MoveNode.BLACK_KINGSIDE]
) {
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 false;
}
private boolean canCastleQueenside() {
if (
board.getLog().getCastlingRights()[getColor() == Color.WHITE
? MoveNode.WHITE_QUEENSIDE
: MoveNode.BLACK_QUEENSIDE]
) {
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 false;
}
private boolean canCastle(
Position kingPos, Position freeDest, Position rookPos, Position jumpPos
) {
Piece rook = board.get(rookPos);
return rook != null && rook instanceof Rook && isFreePath(
new Move(kingPos, freeDest)
) && !board.isAttacked(kingPos, getColor().opposite())
&& !board.isAttacked(jumpPos, getColor().opposite());
}
@Override
public int getValue() { return 0; }
}

View File

@ -0,0 +1,69 @@
package dev.kske.chess.board;
import java.util.ArrayList;
import java.util.List;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>Knight.java</strong><br>
* Created: <strong>01.07.2019</strong><br>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
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) {
super(color, board);
}
@Override
public boolean isValidMove(Move move) {
return Math.abs(move.getxDist() - move.getyDist()) == 1
&& (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
) {
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);
}
}
@Override
protected List<Move> getPseudolegalMoves(Position pos) {
List<Move> moves = new ArrayList<>();
checkAndInsertMove(moves, pos, -2, 1);
checkAndInsertMove(moves, pos, -1, 2);
checkAndInsertMove(moves, pos, 1, 2);
checkAndInsertMove(moves, pos, 2, 1);
checkAndInsertMove(moves, pos, -2, -1);
checkAndInsertMove(moves, pos, -1, -2);
checkAndInsertMove(moves, pos, 1, -2);
checkAndInsertMove(moves, pos, 2, -1);
return moves;
}
@Override
public int getValue() { return 35; }
@Override
public char firstChar() {
return 'n';
}
}

View File

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

View File

@ -0,0 +1,374 @@
package dev.kske.chess.board;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import dev.kske.chess.board.Piece.Color;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>Move.java</strong><br>
* Created: <strong>02.07.2019</strong><br>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class Move {
protected final Position pos, dest;
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) {
this.pos = pos;
this.dest = dest;
xDist = Math.abs(dest.x - pos.x);
yDist = Math.abs(dest.y - pos.y);
xSign = (int) Math.signum(dest.x - pos.x);
ySign = (int) Math.signum(dest.y - pos.y);
}
/**
* 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) {
// Move the piece to the move's destination square and clean the old
// position
board.set(dest, board.get(pos));
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) {
// Move the piece to the move's position square and clean the
// destination
board.set(pos, board.get(dest));
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) {
Position pos = Position.fromLAN(move.substring(0, 2));
Position dest = Position.fromLAN(move.substring(2));
if (move.length() == 5)
try {
return new PawnPromotion(
pos,
dest,
Piece.fromFirstChar(move.charAt(4))
);
} catch (Exception e) {
e.printStackTrace();
return null;
}
return new Move(pos, dest);
}
/**
* 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}
* object.
*
* @param sanMove the move string to convert from
* @param board the board on which the move has to be executed
* @return the converted {@link Move} object
*/
public static Move fromSAN(String sanMove, Board board) {
Map<String, Pattern> patterns = new HashMap<>();
patterns.put(
"pieceMove",
Pattern.compile(
"^(?<pieceType>[NBRQK])(?:(?<fromFile>[a-h])|(?<fromRank>[1-8])|(?<fromSquare>[a-h][1-8]))?x?(?<toSquare>[a-h][1-8])(?:\\+{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()) {
Matcher m = entry.getValue().matcher(sanMove);
if (m.find()) {
Position pos = null, dest = null;
Move move = null;
switch (entry.getKey()) {
case "pieceMove":
dest = Position.fromLAN(m.group("toSquare"));
if (m.group("fromSquare") != null)
pos = Position.fromLAN(m.group("fromSquare"));
else {
Class<? extends Piece> pieceClass = Piece
.fromFirstChar(m.group("pieceType").charAt(0));
char file;
int rank;
if (m.group("fromFile") != null) {
file = m.group("fromFile").charAt(0);
rank = board.get(pieceClass, file);
pos = Position
.fromLAN(String.format("%c%d", file, rank));
} else
if (m.group("fromRank") != null) {
rank = Integer.parseInt(
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);
break;
case "pawnCapture":
char file = m.group("fromFile").charAt(0);
int rank = m.group("fromRank") == null
? board.get(Pawn.class, file)
: Integer.parseInt(m.group("fromRank"));
dest = Position.fromLAN(m.group("toSquare"));
pos = Position
.fromLAN(String.format("%c%d", file, rank));
if (m.group("promotedTo") != null)
try {
move = new PawnPromotion(
pos,
dest,
Piece.fromFirstChar(m.group("promotedTo").charAt(0))
);
} catch (Exception e) {
e.printStackTrace();
}
else
move = new Move(pos, dest);
break;
case "pawnPush":
dest = Position.fromLAN(m.group("toSquare"));
int step
= board.getLog().getActiveColor() == Color.WHITE ? 1
: -1;
// One step forward
if (board.getBoardArr()[dest.x][dest.y + step] != null)
pos = new Position(dest.x, dest.y + step);
// Double step forward
else
pos = new Position(dest.x, dest.y + 2 * step);
if (m.group("promotedTo") != null)
try {
move = new PawnPromotion(
pos,
dest,
Piece.fromFirstChar(m.group("promotedTo").charAt(0))
);
} catch (Exception e) {
e.printStackTrace();
}
else
move = new Move(pos, dest);
break;
case "castling":
pos = new Position(
4,
board.getLog().getActiveColor() == Color.WHITE ? 7
: 0
);
dest = new Position(
m.group("kingside") != null ? 6 : 2,
pos.y
);
move = new Castling(pos, dest);
break;
}
return move;
}
}
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) {
final Piece piece = board.get(pos);
StringBuilder sb = new StringBuilder(8);
// Piece symbol
if (!(piece instanceof Pawn))
sb.append(Character.toUpperCase(piece.firstChar()));
// Position
// TODO: Deconstruct position into optional file or rank
// Omit position if the move is a pawn push
if (!(piece instanceof Pawn && xDist == 0))
sb.append(pos.toLAN());
// Capture indicator
if (board.get(dest) != null)
sb.append('x');
// Destination
sb.append(dest.toLAN());
return sb.toString();
}
/**
* @return {@code true} if the move is purely horizontal
*/
public boolean isHorizontal() { return getyDist() == 0; }
/**
* @return {@code true} if the move is purely vertical
*/
public boolean isVertical() { return getxDist() == 0; }
/**
* @return {@code true} if the move is diagonal
*/
public boolean isDiagonal() { return getxDist() == getyDist(); }
@Override
public String toString() {
return toLAN();
}
@Override
public int hashCode() {
return Objects.hash(
getDest(),
getPos(),
getxDist(),
getxSign(),
getyDist(),
getySign()
);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Move other = (Move) obj;
return Objects.equals(getDest(), other.getDest()) && Objects
.equals(getPos(), other.getPos()) && getxDist() == other.getxDist()
&& getxSign() == other.getxSign() && getyDist() == other.getyDist()
&& getySign() == other.getySign();
}
/**
* @return the position
*/
public Position getPos() { return pos; }
/**
* @return the destination
*/
public Position getDest() { return dest; }
/**
* @return the x distance
*/
public int getxDist() {
return xDist;
}
/**
* @return the y distance
*/
public int getyDist() {
return yDist;
}
/**
* @return the sign of the x distance
*/
public int getxSign() {
return xSign;
}
/**
* @return the sign of the y distance
*/
public int getySign() {
return ySign;
}
}

View File

@ -0,0 +1,227 @@
package dev.kske.chess.board;
import java.util.*;
import dev.kske.chess.board.Piece.Color;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>MoveNode.java</strong><br>
* Created: <strong>02.10.2019</strong><br>
*
* @since Chess v0.5-alpha
* @author Kai S. K. Engelbart
*/
public class MoveNode {
/**
* The index of the white kingside casting in a casting rights array.
*/
public static final int WHITE_KINGSIDE = 0;
/**
* The index of the white queenside castling in a castling rights array.
*/
public static final int WHITE_QUEENSIDE = 1;
/**
* 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}.
*
* @param move the logged {@link Move}
* @param capturedPiece the {@link Piece} captures by 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
* @param activeColor the {@link Color} active after the logged
* {@link Move}
* @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, int fullmoveCounter,
int halfmoveClock
) {
this.move = move;
this.capturedPiece = capturedPiece;
this.castlingRights = castlingRights;
this.enPassant = enPassant;
this.activeColor = activeColor;
this.fullmoveCounter = fullmoveCounter;
this.halfmoveClock = halfmoveClock;
}
/**
* Creates a (deep) copy of another {@link MoveNode}.
*
* @param other The {@link MoveNode} to copy
* @param copyVariations When this is set to {@code true} a deep copy is
* created, which
* considers subsequent variations
*/
public MoveNode(MoveNode other, boolean copyVariations) {
this(
other.move,
other.capturedPiece,
other.castlingRights.clone(),
other.enPassant,
other.activeColor,
other.fullmoveCounter,
other.halfmoveClock
);
if (copyVariations && other.variations != null) {
if (variations == null)
variations = new ArrayList<>();
for (MoveNode variation : other.variations) {
MoveNode copy = new MoveNode(variation, true);
copy.parent = this;
variations.add(copy);
}
}
}
/**
* Adds another {@link MoveNode} as a child node.
*
* @param variation The {@link MoveNode} to append to this {@link MoveNode}
*/
public void addVariation(MoveNode variation) {
if (variations == null)
variations = new ArrayList<>();
if (!variations.contains(variation)) {
variations.add(variation);
variation.parent = this;
}
}
/**
* @return A list of all variations associated with this {@link MoveNode}
*/
public List<MoveNode> getVariations() { return variations; }
/**
* @return {@code true} if this move node has any variations
*/
public boolean hasVariations() {
return variations != null && variations.size() > 0;
}
/**
* @return the parent node of this move node
*/
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; }
/**
* @return {@code true} if this move node has a parent
*/
public boolean hasParent() {
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
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + Arrays.hashCode(castlingRights);
result = prime * result + Objects.hash(
activeColor,
capturedPiece,
enPassant,
fullmoveCounter,
halfmoveClock,
move
);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
MoveNode other = (MoveNode) obj;
return activeColor == other.activeColor
&& Objects.equals(capturedPiece, other.capturedPiece)
&& Arrays.equals(castlingRights, other.castlingRights) && Objects.equals(enPassant, other.enPassant)
&& fullmoveCounter == other.fullmoveCounter
&& halfmoveClock == other.halfmoveClock
&& Objects.equals(move, other.move);
}
}

View File

@ -0,0 +1,131 @@
package dev.kske.chess.board;
import java.util.ArrayList;
import java.util.List;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>Pawn.java</strong><br>
* Created: <strong>01.07.2019</strong><br>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class Pawn extends Piece {
/**
* 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
public boolean isValidMove(Move move) {
boolean step = move.isVertical() && move.getyDist() == 1;
boolean doubleStep = move.isVertical() && move.getyDist() == 2;
boolean strafe = move.isDiagonal() && move.getxDist() == 1;
boolean enPassant
= strafe && move.getDest().equals(board.getLog().getEnPassant());
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);
}
@Override
protected boolean isFreePath(Move move) {
// Two steps forward
if (move.getyDist() == 2)
return board.getBoardArr()[move.getPos().x][move.getDest().y
- move.getySign()] == null && board.getDest(move) == null;
// One step forward
else
if (move.getxDist() == 0)
return board.getDest(move) == null;
// Capture move
else
return board.getDest(move) != null
&& board.getDest(move).getColor() != getColor();
}
@Override
protected List<Move> getPseudolegalMoves(Position pos) {
List<Move> moves = new ArrayList<>();
int sign = getColor() == Color.WHITE ? -1 : 1;
boolean pawnPromotion
= sign == 1 && pos.y == 6 || sign == -1 && pos.y == 1;
// Strafe left
if (pos.x > 0)
addMoveIfValid(
moves,
pos,
new Position(pos.x - 1, pos.y + sign),
pawnPromotion
);
// Strafe right
if (pos.x < 7)
addMoveIfValid(
moves,
pos,
new Position(pos.x + 1, pos.y + sign),
pawnPromotion
);
// Step forward
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
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
if (board.getLog().getEnPassant() != null) {
Move move = new EnPassant(pos, board.getLog().getEnPassant());
if (move.isDiagonal() && move.getxDist() == 1)
moves.add(move);
}
return moves;
}
private void addMoveIfValid(
List<Move> moves, Position pos, Position dest, boolean pawnPromotion
) {
Move move = new Move(pos, dest);
if (isFreePath(move))
if (pawnPromotion)
try {
moves.add(new PawnPromotion(pos, dest, Queen.class));
moves.add(new PawnPromotion(pos, dest, Rook.class));
moves.add(new PawnPromotion(pos, dest, Knight.class));
moves.add(new PawnPromotion(pos, dest, Bishop.class));
} catch (Exception e) {
e.printStackTrace();
}
else
moves.add(move);
}
@Override
public int getValue() { return 10; }
}

View File

@ -0,0 +1,133 @@
package dev.kske.chess.board;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Objects;
import dev.kske.chess.board.Piece.Color;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>PawnPromotion.java</strong><br>
* Created: <strong>2 Nov 2019</strong><br>
*
* @since Chess v0.5-alpha
* @author Kai S. K. Engelbart
*/
public class PawnPromotion extends Move {
private final Constructor<? extends Piece> promotionPieceConstructor;
private final char promotionPieceChar;
/**
* 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);
// Cache piece constructor
promotionPieceConstructor = promotionPieceClass
.getDeclaredConstructor(Color.class, Board.class);
promotionPieceConstructor.setAccessible(true);
// Get piece char
promotionPieceChar = (char) promotionPieceClass.getMethod("firstChar")
.invoke(promotionPieceConstructor.newInstance(null, null));
}
/**
* Creates an instance of {@link PawnPromotion}.
*
* @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
public void execute(Board board) {
try {
board.set(
pos,
promotionPieceConstructor.newInstance(board.get(pos).getColor(), board)
);
super.execute(board);
} catch (
InstantiationException
| IllegalAccessException
| IllegalArgumentException
| InvocationTargetException
| SecurityException e
) {
e.printStackTrace();
}
}
@Override
public void revert(Board board, Piece capturedPiece) {
board.set(dest, new Pawn(board.get(dest).getColor(), board));
super.revert(board, capturedPiece);
}
@Override
public String toLAN() {
return pos.toLAN() + dest.toLAN() + promotionPieceChar;
}
@Override
public String toSAN(Board board) {
String san = super.toSAN(board);
return san + Character.toUpperCase(promotionPieceChar);
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result
+ Objects.hash(promotionPieceChar, promotionPieceConstructor);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (!super.equals(obj))
return false;
if (!(obj instanceof PawnPromotion))
return false;
PawnPromotion other = (PawnPromotion) obj;
return promotionPieceChar == other.promotionPieceChar && Objects
.equals(promotionPieceConstructor, other.promotionPieceConstructor);
}
}

View File

@ -0,0 +1,223 @@
package dev.kske.chess.board;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
/**
* Represents a piece on a board with a color.<br>
* <br>
* Project: <strong>Chess</strong><br>
* File: <strong>Piece.java</strong><br>
* Created: <strong>01.07.2019</strong><br>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public abstract class Piece implements Cloneable {
private final Color color;
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) {
this.color = color;
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) {
List<Move> moves = getPseudolegalMoves(pos);
for (Iterator<Move> iterator = moves.iterator(); iterator.hasNext();) {
Move move = iterator.next();
board.move(move);
if (board.checkCheck(getColor()))
iterator.remove();
board.revert();
}
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);
/**
* 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);
/**
* Checks, if the squares between the position and the destination of a move
* are
* free.
*
* @param move The move to check
* @return {@true} if the path is free
*/
protected boolean isFreePath(Move move) {
for (
int i = move.getPos().x + move.getxSign(), j
= 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);
}
/**
* Checks if the destination of a move is empty or a piece from the opposing
* team
*
* @param move The move to check
* @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();
}
@Override
public Object clone() {
Piece piece = null;
try {
piece = (Piece) super.clone();
} catch (CloneNotSupportedException ex) {
ex.printStackTrace();
}
return piece;
}
@Override
public String toString() {
return String.format("%s[color=%s]", getClass().getSimpleName(), color);
}
@Override
public int hashCode() {
return Objects.hash(color);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Piece other = (Piece) obj;
return color == other.color;
}
/**
* @return the standard value of this {@link Piece} that can be used for
* board
* evaluation
*/
public abstract int getValue();
/**
* @return The first character of this {@link Piece} in algebraic notation
* and
* lower case
*/
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) {
switch (Character.toLowerCase(firstChar)) {
case 'k':
return King.class;
case 'q':
return Queen.class;
case 'r':
return Rook.class;
case 'n':
return Knight.class;
case 'b':
return Bishop.class;
case 'p':
return Pawn.class;
default:
return null;
}
}
/**
* @return the {@link Color} of this {@link Piece}
*/
public Color getColor() { return 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 {
/**
* Represents the color of the white pieces on a board.
*/
WHITE,
/**
* Represents the color of the black pieces on a board.
*/
BLACK;
/**
* @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;
}
/**
* @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

@ -0,0 +1,85 @@
package dev.kske.chess.board;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>Position.java</strong><br>
* Created: <strong>02.07.2019</strong><br>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class Position {
/**
* 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) {
this.x = x;
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) {
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() {
return String.valueOf((char) (x + 97)) + String.valueOf(8 - y);
}
@Override
public String toString() {
return String.format("[%d, %d]", x, y);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + x;
result = prime * result + y;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Position other = (Position) obj;
if (x != other.x)
return false;
if (y != other.y)
return false;
return true;
}
}

View File

@ -0,0 +1,145 @@
package dev.kske.chess.board;
import java.util.ArrayList;
import java.util.List;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>Queen.java</strong><br>
* Created: <strong>01.07.2019</strong><br>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
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) {
super(color, board);
}
@Override
public boolean isValidMove(Move move) {
return (move.isHorizontal() || move.isVertical() || move.isDiagonal())
&& isFreePath(move);
}
@Override
protected List<Move> getPseudolegalMoves(Position pos) {
List<Move> moves = new ArrayList<>();
// Horizontal moves to the right
for (int i = pos.x + 1; i < 8; i++) {
Move move = new Move(pos, new Position(i, pos.y));
if (
board.getDest(move) == null
|| board.getDest(move).getColor() != getColor()
) {
moves.add(move);
if (board.getDest(move) != null)
break;
} else
break;
}
// Horizontal moves to the left
for (int i = pos.x - 1; i >= 0; i--) {
Move move = new Move(pos, new Position(i, pos.y));
if (
board.getDest(move) == null
|| board.getDest(move).getColor() != getColor()
) {
moves.add(move);
if (board.getDest(move) != null)
break;
} else
break;
}
// Vertical moves to the top
for (int i = pos.y - 1; i >= 0; i--) {
Move move = new Move(pos, new Position(pos.x, i));
if (
board.getDest(move) == null
|| board.getDest(move).getColor() != getColor()
) {
moves.add(move);
if (board.getDest(move) != null)
break;
} else
break;
}
// Vertical moves to the bottom
for (int i = pos.y + 1; i < 8; i++) {
Move move = new Move(pos, new Position(pos.x, i));
if (
board.getDest(move) == null
|| board.getDest(move).getColor() != getColor()
) {
moves.add(move);
if (board.getDest(move) != null)
break;
} else
break;
}
// Diagonal moves to the lower right
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));
if (
board.getDest(move) == null
|| board.getDest(move).getColor() != getColor()
) {
moves.add(move);
if (board.getDest(move) != null)
break;
} else
break;
}
// Diagonal moves to the lower left
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));
if (
board.getDest(move) == null
|| board.getDest(move).getColor() != getColor()
) {
moves.add(move);
if (board.getDest(move) != null)
break;
} else
break;
}
// Diagonal moves to the upper right
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));
if (
board.getDest(move) == null
|| board.getDest(move).getColor() != getColor()
) {
moves.add(move);
if (board.getDest(move) != null)
break;
} else
break;
}
// Diagonal moves to the upper left
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));
if (
board.getDest(move) == null
|| board.getDest(move).getColor() != getColor()
) {
moves.add(move);
if (board.getDest(move) != null)
break;
} else
break;
}
return moves;
}
@Override
public int getValue() { return 90; }
}

View File

@ -7,10 +7,18 @@ import java.util.List;
* Project: <strong>Chess</strong><br>
* File: <strong>Rook.java</strong><br>
* Created: <strong>01.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
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) {
super(color, board);
}
@ -20,18 +28,6 @@ public class Rook extends Piece {
return (move.isHorizontal() || move.isVertical()) && isFreePath(move);
}
@Override
protected boolean isFreePath(Move move) {
if (move.isHorizontal()) {
for (int i = move.pos.x + move.xSign; i != move.dest.x; i += move.xSign)
if (board.getBoardArr()[i][move.pos.y] != null) return false;
} else {
for (int i = move.pos.y + move.ySign; i != move.dest.y; i += move.ySign)
if (board.getBoardArr()[move.pos.x][i] != null) return false;
}
return checkDestination(move);
}
@Override
protected List<Move> getPseudolegalMoves(Position pos) {
List<Move> moves = new ArrayList<>();
@ -39,41 +35,58 @@ public class Rook extends Piece {
// Horizontal moves to the right
for (int i = pos.x + 1; i < 8; i++) {
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);
if (board.getDest(move) != null) break;
} else break;
if (board.getDest(move) != null)
break;
} else
break;
}
// Horizontal moves to the left
for (int i = pos.x - 1; i >= 0; i--) {
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);
if (board.getDest(move) != null) break;
} else break;
if (board.getDest(move) != null)
break;
} else
break;
}
// Vertical moves to the top
for (int i = pos.y - 1; i >= 0; 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);
if (board.getDest(move) != null) break;
} else break;
if (board.getDest(move) != null)
break;
} else
break;
}
// Vertical moves to the bottom
for (int i = pos.y + 1; i < 8; 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);
if (board.getDest(move) != null) break;
} else break;
if (board.getDest(move) != null)
break;
} else
break;
}
return moves;
}
@Override
public Type getType() { return Type.ROOK; }
public int getValue() { return 50; }
}

View File

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

View File

@ -0,0 +1,39 @@
package dev.kske.chess.event;
import dev.kske.chess.board.*;
import dev.kske.eventbus.IEvent;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>MoveEvent.java</strong><br>
* Created: <strong>7 Aug 2019</strong><br>
*
* @since Chess v0.4-alpha
* @author Kai S. K. Engelbart
*/
public class MoveEvent implements IEvent {
private final Move move;
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) {
this.move = move;
this.boardState = boardState;
}
/**
* @return the move
*/
public Move getMove() { return move; }
/**
* @return the state of the board after the move
*/
public BoardState getBoardState() { return boardState; }
}

View File

@ -0,0 +1,42 @@
package dev.kske.chess.exception;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>ChessException.java</strong><br>
* Created: <strong>22 Sep 2019</strong><br>
*
* @since Chess v0.5-alpha
* @author Kai S. K. Engelbart
*/
public class ChessException extends Exception {
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) {
super(message, cause);
}
/**
* Initializes chess exception.
*
* @param message the message associated with this exception
*/
public ChessException(String message) {
super(message);
}
/**
* Initializes chess exception.
*
* @param cause the cause of this exception
*/
public ChessException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,199 @@
package dev.kske.chess.game;
import java.util.*;
import javax.swing.JOptionPane;
import dev.kske.chess.board.*;
import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.event.*;
import dev.kske.chess.game.ai.AIPlayer;
import dev.kske.chess.io.EngineUtil;
import dev.kske.chess.io.EngineUtil.EngineInfo;
import dev.kske.chess.ui.*;
import dev.kske.eventbus.EventBus;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>Game.java</strong><br>
* Created: <strong>06.07.2019</strong><br>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class Game {
private Map<Color, Player> players = new EnumMap<>(Color.class);
private Board board;
private OverlayComponent overlayComponent;
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) {
board = new Board();
init(boardPane, whiteName, blackName);
}
/**
* 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;
init(boardPane, whiteName, blackName);
}
private void init(BoardPane boardPane, String whiteName, String blackName) {
// Initialize / synchronize UI
overlayComponent = boardPane.getOverlayComponent();
boardComponent = boardPane.getBoardComponent();
boardComponent.setBoard(board);
// Initialize players
players.put(Color.WHITE, getPlayer(whiteName, Color.WHITE));
players.put(Color.BLACK, getPlayer(blackName, Color.BLACK));
}
/**
* 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) {
switch (name) {
case "Natural Player":
return new NaturalPlayer(this, color, overlayComponent);
case "AI Player":
return new AIPlayer(this, color, 4, -10);
default:
for (EngineInfo info : EngineUtil.getEngineInfos())
if (info.name.equals(name))
return new UCIPlayer(this, color, info.path);
System.err.println("Invalid player name: " + name);
return null;
}
}
/**
* 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
boardComponent.repaint();
overlayComponent.displayArrow(move);
// Run garbage collection
System.gc();
BoardState boardState
= board.getState(board.getDest(move).getColor().opposite());
EventBus.getInstance().dispatch(new MoveEvent(move, boardState));
switch (boardState) {
case CHECKMATE:
case STALEMATE:
String result = String.format(
"%s in %s!%n",
player.color.opposite(),
boardState
);
System.out.print(result);
JOptionPane.showMessageDialog(boardComponent, result);
break;
case CHECK:
System.out
.printf("%s in check!%n", player.color.opposite());
default:
players.get(board.getLog().getActiveColor()).requestMove();
}
} else
player.requestMove();
}
/**
* 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));
players.get(board.getLog().getActiveColor()).requestMove();
}
/**
* Cancels move calculations, initializes the default position and clears
* the
* {@link OverlayComponent}.
*/
public synchronized void reset() {
players.values().forEach(Player::cancelMove);
board.initDefaultPositions();
boardComponent.repaint();
overlayComponent.clearDots();
overlayComponent.clearArrow();
}
/**
* Stops the game by disconnecting its players from the UI.
*/
public synchronized void stop() {
players.values().forEach(Player::disconnect);
}
/**
* Assigns the players their opposite colors.
*/
public synchronized void swapColors() {
players.values().forEach(Player::cancelMove);
Player white = players.get(Color.WHITE);
Player black = players.get(Color.BLACK);
white.setColor(Color.BLACK);
black.setColor(Color.WHITE);
players.put(Color.WHITE, black);
players.put(Color.BLACK, white);
players.get(board.getLog().getActiveColor()).requestMove();
}
/**
* @return The board on which this game's moves are made
*/
public Board getBoard() { return board; }
/**
* @return The players participating in this game
*/
public Map<Color, Player> getPlayers() { return players; }
}

View File

@ -0,0 +1,151 @@
package dev.kske.chess.game;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.List;
import java.util.stream.Collectors;
import javax.swing.JComboBox;
import javax.swing.JOptionPane;
import dev.kske.chess.board.Move;
import dev.kske.chess.board.Piece;
import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.board.Position;
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>
* File: <strong>NaturalPlayer.java</strong><br>
* Created: <strong>06.07.2019</strong><br>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class NaturalPlayer extends Player implements MouseListener {
private final OverlayComponent overlayComponent;
private boolean moveRequested;
private Piece selectedPiece;
private List<Move> possibleMoves;
/**
* Creates an instance of {@link NaturalPlayer}.
*
* @param game the game in which this player will be used
* @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);
}
@Override
public void requestMove() {
moveRequested = true;
}
@Override
public void cancelMove() {
moveRequested = false;
}
@Override
public void disconnect() {
overlayComponent.removeMouseListener(this);
}
@Override
public void mousePressed(MouseEvent evt) {
if (!moveRequested)
return;
if (selectedPiece == null) {
// Get selected Piece
final Position pos = new Position(
evt.getPoint().x / overlayComponent.getTileSize(),
evt.getPoint().y / overlayComponent.getTileSize()
);
selectedPiece = board.get(pos);
// Check if a piece was selected
if (selectedPiece != null)
// Discard selection if the piece has the wrong color
if (selectedPiece.getColor() == color.opposite())
selectedPiece = null;
else {
// Generate all moves possible with the selected piece and
// display their
// destinations
possibleMoves = selectedPiece.getMoves(pos);
overlayComponent.displayDots(
possibleMoves.stream().map(Move::getDest).collect(Collectors.toList())
);
}
} else {
Position dest = new Position(
evt.getPoint().x / overlayComponent.getTileSize(),
evt.getPoint().y / overlayComponent.getTileSize()
);
// Get all moves leading to the specified destination
List<Move> selectedMoves = possibleMoves.stream()
.filter(m -> m.getDest().equals(dest))
.collect(Collectors.toList());
if (!selectedMoves.isEmpty()) {
Move move;
// Process pawn promotion if necessary
if (selectedMoves.size() > 1) {
// Let the user select a promotion piece
JComboBox<Move> comboBox
= new JComboBox<>(selectedMoves.toArray(new Move[0]));
JOptionPane.showMessageDialog(
overlayComponent,
comboBox,
"Select a promotion",
JOptionPane.QUESTION_MESSAGE
);
move = selectedMoves.get(comboBox.getSelectedIndex());
} else
move = selectedMoves.get(0);
// Tell the game to execute the move
moveRequested = false;
game.onMove(NaturalPlayer.this, move);
}
// Discard the selection
overlayComponent.clearDots();
selectedPiece = null;
possibleMoves = null;
}
}
@Override
public void mouseClicked(MouseEvent e) {}
@Override
public void mouseReleased(MouseEvent e) {}
@Override
public void mouseEntered(MouseEvent e) {}
@Override
public void mouseExited(MouseEvent e) {}
}

View File

@ -0,0 +1,87 @@
package dev.kske.chess.game;
import dev.kske.chess.board.Board;
import dev.kske.chess.board.Move;
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>
* File: <strong>Player.java</strong><br>
* Created: <strong>06.07.2019</strong><br>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public abstract class Player {
protected final Game game;
protected final Board board;
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;
}
/**
* Initiates a move generation and reports the result to the game by calling
* {@link Game#onMove(Player, Move)}.
*/
public abstract void requestMove();
/**
* Cancels the move generation process.
*/
public abstract void cancelMove();
/**
* Closes all resources required for move generation.
*/
public abstract void disconnect();
/**
* @return the game in which this player is used
*/
public Game getGame() { return game; }
/**
* @return the board on which this player is used
*/
public Board getBoard() { return board; }
/**
* @return the color of pieces controlled by this player
*/
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; }
/**
* @return the name of this player
*/
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; }
}

View File

@ -0,0 +1,106 @@
package dev.kske.chess.game;
import java.io.IOException;
import dev.kske.chess.board.FENString;
import dev.kske.chess.board.Move;
import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.uci.UCIHandle;
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>
* File: <strong>UCIPlayer.java</strong><br>
* Created: <strong>18.07.2019</strong><br>
*
* @since Chess v0.3-alpha
* @author Kai S. K. Engelbart
*/
public class UCIPlayer extends Player implements UCIListener {
private UCIHandle handle;
/**
* 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 {
handle = new UCIHandle(enginePath);
handle.registerListener(this);
handle.start();
} catch (IOException ex) {
ex.printStackTrace();
}
}
@Override
public void requestMove() {
handle.positionFEN(new FENString(board).toString());
handle.go();
}
@Override
public void cancelMove() {
handle.stop();
}
@Override
public void disconnect() {
handle.quit();
}
@Override
public void onIdName(String name) {
this.name = name;
}
@Override
public void onBestMove(String move) {
Move moveObj = Move.fromLAN(move);
game.onMove(this, moveObj);
}
@Override
public void onBestMove(String move, Move ponderMove) {
onBestMove(move);
}
@Override
public void onCopyProtectionChecking() {
System.out.println("Copy protection checking...");
}
@Override
public void onCopyProtectionOk() {
System.out.println("Copy protection ok");
}
@Override
public void onCopyProtectionError() {
System.err.println("Copy protection error!");
}
@Override
public void onRegistrationChecking() {
System.out.println("Registration checking...");
}
@Override
public void onRegistrationOk() {
System.out.println("Registration ok");
}
@Override
public void onRegistrationError() {
System.err.println("Registration error!");
}
}

View File

@ -0,0 +1,116 @@
package dev.kske.chess.game.ai;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
import javax.swing.SwingUtilities;
import dev.kske.chess.board.Board;
import dev.kske.chess.board.Move;
import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.game.Game;
import dev.kske.chess.game.Player;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>AIPlayer.java</strong><br>
* Created: <strong>06.07.2019</strong><br>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class AIPlayer extends Player {
private int availableProcessors;
private int maxDepth;
private int alphaBetaThreshold;
private volatile boolean exitRequested;
private volatile ExecutorService executor;
/**
* Creates an instance of {@link AIPlayer}.
*
* @param game the game in which this player will be used
* @param color the piece color this player will control
* @param maxDepth the maximum search depth
* @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
public void requestMove() {
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 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++) {
if (rem-- > 0)
++endIndex;
endIndex += step;
processors
.add(new MoveProcessor(new Board(board, false), moves.subList(beginIndex, endIndex), color, maxDepth, alphaBetaThreshold));
beginIndex = endIndex;
}
// Execute processors, get the best result and pass it back to the
// Game class
executor = Executors.newFixedThreadPool(numThreads);
List<ProcessingResult> results = new ArrayList<>(numThreads);
try {
List<Future<ProcessingResult>> futures
= executor.invokeAll(processors);
for (Future<ProcessingResult> f : futures)
results.add(f.get());
} catch (InterruptedException | ExecutionException ex) {
ex.printStackTrace();
} finally {
executor.shutdown();
}
results.sort((r1, r2) -> Integer.compare(r2.score, r1.score));
if (!exitRequested)
SwingUtilities
.invokeLater(() -> game.onMove(this, results.get(0).move));
}, "AIPlayer calculation setup").start();
}
@Override
public void cancelMove() {
exitRequested = true;
if (executor != null) {
executor.shutdownNow();
try {
executor.awaitTermination(500, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void disconnect() {}
}

View File

@ -0,0 +1,283 @@
package dev.kske.chess.game.ai;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import dev.kske.chess.board.*;
import dev.kske.chess.board.Piece.Color;
/**
* Implements a basic minimax move search algorithm for testing purposes.<br>
* <br>
* Project: <strong>Chess</strong><br>
* File: <strong>MoveProcessor.java</strong><br>
* Created: <strong>08.07.2019</strong><br>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class MoveProcessor implements Callable<ProcessingResult> {
private final Board board;
private final List<Move> rootMoves;
private final Color color;
private final int maxDepth;
private final int alphaBetaThreshold;
private Move bestMove;
private static final Map<Class<? extends Piece>, int[][]> positionScores;
static {
positionScores = new HashMap<>();
positionScores.put(
King.class,
new int[][] {
new int[] {
-3, -4, -4, -5, -5, -4, -4, -3
}, new int[] {
-3, -4, -4, -5, -4, -4, -4, -3
},
new int[] {
-3, -4, -4, -5, -4, -4, -4, -3
},
new int[] {
-3, -4, -4, -5, -4, -4, -4, -3
},
new int[] {
-2, -3, -3, -2, -2, -2, -2, -1
},
new int[] {
-1, -2, -2, -2, -2, -2, -2, -1
},
new int[] {
2, 2, 0, 0, 0, 0, 2, 2
},
new int[] {
2, 3, 1, 0, 0, 1, 3, 2
}
}
);
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
}
}
);
}
/**
* Creates an instance of {@link MoveProcessor}.
*
* @param board the board to search
* @param rootMoves the moves on which the search is based
* @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
public ProcessingResult call() throws Exception {
int score = miniMax(board, rootMoves, color, 0);
return new ProcessingResult(bestMove, score);
}
private int miniMax(Board board, List<Move> moves, Color color, int depth) {
int bestValue = Integer.MIN_VALUE;
for (Move move : moves) {
board.move(move);
int teamValue = evaluate(board, color);
int enemyValue = evaluate(board, color.opposite());
int valueChange = teamValue - enemyValue;
if (depth < maxDepth && valueChange >= alphaBetaThreshold)
valueChange -= miniMax(
board,
board.getMoves(color.opposite()),
color.opposite(),
depth + 1
);
if (valueChange > bestValue) {
bestValue = valueChange;
if (depth == 0)
bestMove = move;
}
board.revert();
}
return bestValue;
}
/**
* Evaluated a board.
*
* @param board the board to evaluate
* @param color The color to evaluate for
* @return a positive number representing how good the position is
*/
private int evaluate(Board board, Color color) {
int score = 0;
for (int i = 0; i < 8; i++)
for (int j = 0; j < 8; j++)
if (
board.getBoardArr()[i][j] != null
&& board.getBoardArr()[i][j].getColor() == color
) {
score += board.getBoardArr()[i][j].getValue();
if (
positionScores
.containsKey(board.getBoardArr()[i][j].getClass())
)
score += positionScores.get(
board.getBoardArr()[i][j].getClass()
)[i][color == Color.WHITE ? j : 7 - j];
}
return score;
}
}

View File

@ -0,0 +1,43 @@
package dev.kske.chess.game.ai;
import dev.kske.chess.board.Move;
/**
* Contains information about a move search performed by a chess engine.
* <br>
* Project: <strong>Chess</strong><br>
* File: <strong>ProcessingResult.java</strong><br>
* Created: <strong>08.07.2019</strong><br>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class ProcessingResult {
/**
* 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) {
this.move = move;
this.score = score;
}
@Override
public String toString() {
return String
.format("ProcessingResult[Move = %s,Score = %d]", move, score);
}
}

View File

@ -0,0 +1,141 @@
package dev.kske.chess.io;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import dev.kske.chess.uci.UCIHandle;
import dev.kske.chess.uci.UCIListener;
/**
* 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
* @author Leon Hofmeister
*/
public class EngineUtil {
private static volatile List<EngineInfo> engineInfos;
private static final String engineInfoFile = "engine_infos.ser";
static {
loadEngineInfos();
}
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) {
try {
EngineInfo info = new EngineInfo(enginePath);
UCIHandle handle = new UCIHandle(enginePath);
handle.registerListener(new UCIListener() {
@Override
public void onIdName(String name) {
info.name = name;
}
@Override
public void onIdAuthor(String author) {
info.author = author;
}
@Override
public void onUCIOk() {
engineInfos.add(info);
handle.quit();
saveEngineInfos();
}
});
handle.start();
} catch (IOException ex) {
ex.printStackTrace();
}
}
@SuppressWarnings("unchecked")
private static void loadEngineInfos() {
try (
ObjectInputStream in
= new ObjectInputStream(new FileInputStream(engineInfoFile))
) {
Object obj = in.readObject();
if (obj instanceof ArrayList<?>)
engineInfos = (ArrayList<EngineInfo>) obj;
else
throw new IOException("Serialized object has the wrong class.");
} catch (ClassNotFoundException | IOException ex) {
engineInfos = new ArrayList<>();
}
}
private static void saveEngineInfos() {
try (
ObjectOutputStream out
= new ObjectOutputStream(new FileOutputStream(engineInfoFile))
) {
out.writeObject(engineInfos);
} catch (IOException ex) {
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 {
private static final long serialVersionUID = -474177108900833005L;
/**
* 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) {
this.path = path;
}
@Override
public String toString() {
return name + " by " + author + " at " + path;
}
}
/**
* @return a list of all stored engine infos
*/
public static List<EngineInfo> getEngineInfos() { return engineInfos; }
}

View File

@ -1,4 +1,4 @@
package dev.kske.chess.ui;
package dev.kske.chess.io;
import java.awt.Image;
import java.awt.image.BufferedImage;
@ -15,15 +15,16 @@ import dev.kske.chess.board.Piece;
* Project: <strong>Chess</strong><br>
* File: <strong>TextureUtil.java</strong><br>
* Created: <strong>01.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class TextureUtil {
private static Map<String, Image> textures, scaledTextures;
private static Map<String, Image> textures = new HashMap<>(),
scaledTextures = new HashMap<>();
static {
textures = new HashMap<>();
scaledTextures = new HashMap<>();
loadPieceTextures();
scaledTextures.putAll(textures);
}
@ -31,28 +32,32 @@ public class TextureUtil {
private 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
* @return The fitting texture
*/
public static Image getPieceTexture(Piece piece) {
String key = piece.getType().toString().toLowerCase() + "_" + piece.getColor().toString().toLowerCase();
String key = piece.getClass().getSimpleName().toLowerCase() + "_"
+ piece.getColor().toString().toLowerCase();
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
*/
public static void scalePieceTextures(int scale) {
public static void scalePieceTextures(int tileSize) {
scaledTextures.clear();
textures
.forEach((key, img) -> scaledTextures.put(key, img.getScaledInstance(scale, scale, 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.
*
*
* @param fileName The name of the image resource
* @return The loaded image
*/
@ -72,19 +77,22 @@ public class TextureUtil {
*/
private static void loadPieceTextures() {
Arrays
.asList("king_white",
"king_black",
"queen_white",
"queen_black",
"rook_white",
"rook_black",
"knight_white",
"knight_black",
"bishop_white",
"bishop_black",
"pawn_white",
"pawn_black")
.forEach(name -> textures.put(name, loadImage("/pieces/" + name + ".png")));
.asList(
"king_white",
"king_black",
"queen_white",
"queen_black",
"rook_white",
"rook_black",
"knight_white",
"knight_black",
"bishop_white",
"bishop_black",
"pawn_white",
"pawn_black"
)
.forEach(
name -> textures.put(name, loadImage("/pieces/" + name + ".png"))
);
}
}

View File

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

View File

@ -0,0 +1,178 @@
package dev.kske.chess.pgn;
import java.io.PrintWriter;
import java.util.*;
import java.util.regex.MatchResult;
import java.util.regex.Pattern;
import dev.kske.chess.board.Board;
import dev.kske.chess.board.FENString;
import dev.kske.chess.board.Move;
import dev.kske.chess.board.Piece.Color;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>PGNGame.java</strong><br>
* Created: <strong>22 Sep 2019</strong><br>
*
* @since Chess v0.5-alpha
* @author Kai S. K. Engelbart
*/
public class PGNGame {
private final Map<String, String> tagPairs = new HashMap<>(7);
private final Board board;
/**
* Creates an instance of {@link PGNGame}. A new default {@link Board} will
* be
* created.
*/
public PGNGame() {
board = new Board();
}
/**
* Creates an instance of {@link PGNGame}.
*
* @param board the board associated with the game
*/
public PGNGame(Board board) {
this.board = board;
}
/**
* 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();
MatchResult matchResult;
Pattern tagPairPattern = Pattern.compile("\\[(\\w+) \"(.*)\"]"),
movePattern = Pattern.compile(
"\\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
while (sc.findInLine(tagPairPattern) != null) {
matchResult = sc.match();
if (matchResult.groupCount() == 2)
game.setTag(matchResult.group(1), matchResult.group(2));
else
break;
sc.nextLine();
}
// Parse movetext
while (true) {
// Skip NAG (Numeric Annotation Glyph)
sc.skip(nagPattern);
// TODO: Parse RAV (Recursive Annotation Variation)
if (sc.findWithinHorizon(movePattern, 20) != null) {
matchResult = sc.match();
if (matchResult.groupCount() > 0)
for (int i = 1; i < matchResult.groupCount() + 1; i++) {
game.board.move(matchResult.group(i));
System.out.println(
game.getBoard().getLog().getLast().move.toLAN()
+ ": " + new FENString(game.board).toString()
);
}
else
break;
} else
break;
}
// Parse game termination marker
if (sc.findWithinHorizon(terminationMarkerPattern, 20) == null)
System.err.println("Termination marker expected");
return game;
}
/**
* Serializes this game to {@code PGN} format.
*
* @param pw the writer to write the game to
*/
public void writePGN(PrintWriter pw) {
// Set the unknown result tag if no result tag is specified
tagPairs.putIfAbsent("Result", "*");
// Write tag pairs
tagPairs.forEach((k, v) -> pw.printf("[%s \"%s\"]%n", k, v));
// Insert newline if tags were printed
if (!tagPairs.isEmpty())
pw.println();
if (!board.getLog().isEmpty()) {
// Collect SAN moves
Board clone = new Board(board, true);
List<String> chunks = new ArrayList<>();
boolean flag = true;
while (flag) {
Move move = clone.getLog().getLast().move;
flag = clone.getLog().hasParent();
clone.revert();
String chunk = clone.getLog().getActiveColor() == Color.WHITE
? String.format(" %d. ", clone.getLog().getFullmoveNumber())
: " ";
chunk += move.toSAN(clone);
chunks.add(chunk);
}
Collections.reverse(chunks);
// Write movetext
String line = "";
for (String chunk : chunks)
if (line.length() + chunk.length() <= 80)
line += chunk;
else {
pw.println(line);
line = chunk;
}
if (!line.isEmpty())
pw.println(line);
}
// Write game termination marker
pw.print(tagPairs.get("Result"));
}
/**
* @param tagName the name of a game tag
* @return the value of the game tag
*/
public String getTag(String tagName) {
return tagPairs.get(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);
}
/**
* 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; }
}

View File

@ -0,0 +1,254 @@
package dev.kske.chess.uci;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import java.util.StringJoiner;
import dev.kske.chess.board.Move;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>UCIHandle.java</strong><br>
* Created: <strong>18.07.2019</strong><br>
*
* @since Chess v0.3-alpha
* @author Kai S. K. Engelbart
*/
public class UCIHandle {
private final Process process;
private final PrintWriter out;
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 {
process = new ProcessBuilder(enginePath).start();
out = new PrintWriter(process.getOutputStream(), true);
receiver = new UCIReceiver(process.getInputStream());
}
/**
* Starts the {@link UCIReceiver} used to gather engine output.
*/
public void start() {
new Thread(receiver, "UCI Receiver").start();
uci();
}
/**
* Tells the engine to use UCI.
*/
public void uci() {
out.println("uci");
}
/**
* Switches the debug mode of the engine on or off.
*
* @param debug Enables debugging if set to {@code true}, disables it
* otherwise
*/
public void debug(boolean debug) {
out.println("debug " + (debug ? "on" : "off"));
}
/**
* Synchronized the engine with the GUI
*/
public void isready() {
out.println("isready");
}
/**
* Signifies a button press to the engine.
*
* @param name The name of the button
*/
public void setOption(String name) {
out.println("setoption name " + name);
}
/**
* Changes an internal parameter of the engine.
*
* @param name The name 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);
}
/**
* Registers the engine
*
* @param name The name 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);
}
/**
* Tells the engine to postpone the registration.
*/
public void registerLater() {
out.println("register later");
}
/**
* Tells the engine that the next search will be from a different game.
*/
public void uciNewGame() {
out.println("ucinewgame");
}
/**
* Sets up the position in its initial state.
*/
public void positionStartpos() {
out.println("position startpos");
}
/**
* Sets up the position described in the FEN string.
*
* @param fen FEN representation of the current board
*/
public void positionFEN(String fen) {
out.println("position fen " + fen);
}
/**
* Sets up the position described by a list of moves.
*
* @param moves the moves to execute from the starting position to reach the
* desired position
*/
public void positionMoves(List<Move> moves) {
StringJoiner joiner = new StringJoiner(" ");
moves.forEach(m -> joiner.add(m.toLAN()));
out.println("position moves " + joiner);
}
/**
* Starts calculating on the current position.
*/
public void go() {
out.println("go");
}
/**
* Starts calculating on the current position.
* This command has multiple optional parameters which will only be included
* 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
* respectively.
*
* @param searchMoves restrict the search to these moves only
* @param ponder start the search in ponder mode
* @param wTime the amount of milliseconds left on white's clock
* @param bTime the amount of milliseconds left on black's clocks
* @param wInc white's increment per move in milliseconds
* @param bInc black's increment per move in milliseconds
* @param movesToGo the number of moves left until the next time control
* @param depth the maximal amount of plies to search
* @param nodes the maximal amount of nodes to search
* @param mate the amount of moves in which to search for a mate
* @param moveTime the exact search time
* @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,
int moveTime, boolean infinite
) {
StringJoiner joiner = new StringJoiner(" ");
joiner.add("go");
if (searchMoves != null && !searchMoves.isEmpty()) {
joiner.add("searchmoves");
searchMoves.forEach(m -> joiner.add(m.toLAN()));
}
if (ponder)
joiner.add("ponder");
if (wTime > 0) {
joiner.add("wtime");
joiner.add(String.valueOf(wTime));
}
if (bTime > 0) {
joiner.add("btime");
joiner.add(String.valueOf(bTime));
}
if (wInc > 0) {
joiner.add("winc");
joiner.add(String.valueOf(wInc));
}
if (bInc > 0) {
joiner.add("bind");
joiner.add(String.valueOf(bInc));
}
if (movesToGo > 0) {
joiner.add("movestogo");
joiner.add(String.valueOf(movesToGo));
}
if (depth > 0) {
joiner.add("depth");
joiner.add(String.valueOf(depth));
}
if (nodes > 0) {
joiner.add("nodes");
joiner.add(String.valueOf(nodes));
}
if (mate > 0) {
joiner.add("mate");
joiner.add(String.valueOf(mate));
}
if (moveTime > 0) {
joiner.add("movetime");
joiner.add(String.valueOf(moveTime));
}
if (infinite)
joiner.add("infinite");
out.println(joiner);
}
/**
* Stops calculation as soon as possible.
*/
public void stop() {
out.println("stop");
}
/**
* Tells the engine that the user has played the expected move.
*/
public void ponderHit() {
out.println("ponderhit");
}
/**
* Quits the engine process as soon as possible.
*/
public void quit() {
out.println("quit");
}
/**
* Registers a UCI listener.
*
* @param listener the UCI listener to register
*/
public void registerListener(UCIListener listener) {
receiver.registerListener(listener);
}
}

View File

@ -0,0 +1,220 @@
package dev.kske.chess.uci;
import java.util.*;
import dev.kske.chess.board.Move;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>UCIInfo.java</strong><br>
* Created: <strong>28.07.2019</strong><br>
*
* @since Chess v0.3-alpha
* @author Kai S. K. Engelbart
*/
public class UCIInfo {
private int depth, seldepth, time, nodes, multipv, currmovenumber, hashfull,
nps, tbhits, sbhits, cpuload, cpunr;
private List<Move> pv = new ArrayList<>(), refutation = new ArrayList<>();
private Map<Integer, List<Move>> currline = new HashMap<>();
private Move currmove;
private Score score;
private String displayString;
/**
* Contains every parameter for the UCI info command. Helpful for parsing
* multi-value parameters.
*/
private static final List<String> params = Arrays.asList(
"depth",
"seldepth",
"time",
"nodes",
"multipv",
"currmove",
"currmovenumber",
"hashfull",
"nps",
"tbhits",
"sbhits",
"cpuload",
"string",
"score",
"pv",
"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) {
String[] tokens = line.split(" ");
for (int i = 0; i < tokens.length; i++)
switch (tokens[i]) {
// Single parameter info
case "depth":
depth = Integer.parseInt(tokens[++i]);
break;
case "seldepth":
seldepth = Integer.parseInt(tokens[++i]);
break;
case "time":
time = Integer.parseInt(tokens[++i]);
break;
case "nodes":
nodes = Integer.parseInt(tokens[++i]);
break;
case "multipv":
multipv = Integer.parseInt(tokens[++i]);
break;
case "currmove":
currmove = Move.fromLAN(tokens[++i]);
break;
case "currmovenumber":
currmovenumber = Integer.parseInt(tokens[++i]);
break;
case "hashfull":
hashfull = Integer.parseInt(tokens[++i]);
break;
case "nps":
nps = Integer.parseInt(tokens[++i]);
break;
case "tbhits":
tbhits = Integer.parseInt(tokens[++i]);
break;
case "sbhits":
sbhits = Integer.parseInt(tokens[++i]);
break;
case "cpuload":
cpuload = Integer.parseInt(tokens[++i]);
break;
case "string":
displayString = tokens[++i];
break;
case "score":
score
= new Score(line.substring(line.indexOf("score") + tokens[i].length() + 1));
i += score.getLength() + 1;
break;
case "pv":
while (++i < tokens.length && !params.contains(tokens[i]))
pv.add(Move.fromLAN(tokens[i]));
break;
case "refutation":
while (++i < tokens.length && !params.contains(tokens[i]))
refutation.add(Move.fromLAN(tokens[i]));
break;
case "currline":
// A CPU number of 1 can be omitted
final Integer cpu = tokens[i].matches("\\d+")
? Integer.parseInt(tokens[i++])
: 1;
final ArrayList<Move> moves = new ArrayList<>();
while (i < tokens.length && !params.contains(tokens[i]))
moves.add(Move.fromLAN(tokens[i++]));
currline.put(cpu, moves);
System.err.println(
"The parameter 'currline' for command 'info' is not yet implemented"
);
break;
default:
System.err.printf(
"Unknown parameter '%s' for command 'info' found!%n",
tokens[i]
);
}
}
public int getDepth() { return depth; }
public int getSeldepth() { return seldepth; }
public int getTime() { return time; }
public int getNodes() { return nodes; }
public int getMultipv() { return multipv; }
public int getCurrmovenumber() { return currmovenumber; }
public int getHashfull() { return hashfull; }
public int getNps() { return nps; }
public int getTbhits() { return tbhits; }
public int getSbhits() { return sbhits; }
public int getCpuload() { return cpuload; }
public int getCpunr() { return cpunr; }
public List<Move> getPv() { return pv; }
public List<Move> getRefutation() { return refutation; }
public Map<Integer, List<Move>> getCurrline() { return currline; }
public Move getCurrmove() { return currmove; }
public Score getScore() { return score; }
public String getDisplayString() { return displayString; }
public static class Score {
private int cp, mate;
private boolean lowerbound, upperbound;
private int length;
public Score(String line) {
String[] tokens = line.split(" ");
int i = 0;
for (; i < tokens.length; i++) {
if (params.contains(tokens[i]))
break;
switch (tokens[i]) {
case "cp":
cp = Integer.parseInt(tokens[++i]);
break;
case "mate":
mate = Integer.parseInt(tokens[++i]);
break;
case "lowerbound":
lowerbound = true;
break;
case "upperbound":
upperbound = true;
break;
default:
System.err.printf(
"Unknown parameter '%s' for command 'score' found!%n",
tokens[i]
);
}
}
length = i + 1;
}
public int getCp() { return cp; }
public int getMate() { return mate; }
public boolean isLowerbound() { return lowerbound; }
public boolean isUpperbound() { return upperbound; }
/**
* @return The number of tokens this 'score' command contains (including
* itself).
*/
public int getLength() { return length; }
}
}

View File

@ -0,0 +1,97 @@
package dev.kske.chess.uci;
import dev.kske.chess.board.Move;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>UCIListener.java</strong><br>
* Created: <strong>19.07.2019</strong><br>
*
* @since Chess v0.3-alpha
* @author Kai S. K. Engelbart
*/
public interface UCIListener {
/**
* Identifies the name of the engine.
*
* @param name The name of the engine
*/
default void onIdName(String name) {}
/**
* Identifies the author of the engine.
*
* @param author The name of the engine's author
*/
default void onIdAuthor(String author) {}
/**
* The engine is ready in UCI mode.
*/
default void onUCIOk() {}
/**
* The engine has processed all inputs and is ready for new commands.
*/
default void onReadyOk() {}
/**
* The engine has stopped searching and has found the best move.
*
* @param move The best moves the engine has found
*/
default void onBestMove(String move) {}
/**
* The engine has stopped searching and has found the best move.
*
* @param move The best move the engine has found
* @param ponderMove The move the engine likes to ponder on
*/
default void onBestMove(String move, Move ponderMove) {}
/**
* The engine will check the copy protection now.
*/
default void onCopyProtectionChecking() {}
/**
* The engine has successfully checked the copy protection.
*/
default void onCopyProtectionOk() {}
/**
* The engine has encountered an error during copy protection checking.
*/
default void onCopyProtectionError() {}
/**
* The engine will check the registration now.
*/
default void onRegistrationChecking() {}
/**
* The engine has successfully checked the registration.
*/
default void onRegistrationOk() {}
/**
* The engine has encountered an error during registration checking.
*/
default void onRegistrationError() {}
/**
* The engine sends information to the GUI.
*
* @param info Contains all pieces of information to be sent
*/
default void onInfo(UCIInfo info) {}
/**
* Tells the GUI which parameters can be changed in the engine.
*
* @param option Option object describing the parameter
*/
default void onOption(UCIOption option) {}
}

View File

@ -0,0 +1,73 @@
package dev.kske.chess.uci;
import java.util.*;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>UCIOption.java</strong><br>
* Created: <strong>22.07.2019</strong><br>
*
* @since Chess v0.3-alpha
* @author Kai S. K. Engelbart
*/
public class UCIOption {
private String name, defaultVal, minVal, maxVal;
private GUIType type;
private List<String> varList;
public UCIOption(String line) {
varList = new ArrayList<>();
String[] tokens = line.split(" ");
for (int i = 0; i < tokens.length; i++)
switch (tokens[i]) {
case "name":
StringJoiner nameJoiner = new StringJoiner(" ");
while (
!Arrays.asList("type", "default", "min", "max", "var")
.contains(tokens[i + 1])
)
nameJoiner.add(tokens[++i]);
name = nameJoiner.toString();
break;
case "type":
type = GUIType.valueOf(tokens[++i].toUpperCase());
break;
case "default":
// Default string may be empty
defaultVal = i == tokens.length - 1 ? "" : tokens[++i];
break;
case "min":
minVal = tokens[++i];
break;
case "max":
maxVal = tokens[++i];
break;
case "var":
varList.add(tokens[++i]);
break;
default:
System.err.printf(
"Unknown parameter '%s' for command 'option' found!%n",
tokens[i]
);
}
}
public String getName() { return name; }
public String getDefaultVal() { return defaultVal; }
public String getMinVal() { return minVal; }
public String getMaxVal() { return maxVal; }
public GUIType getType() { return type; }
public List<String> getVarList() { return varList; }
public enum GUIType {
CHECK, SPIN, COMBO, BUTTON, STRING
}
}

View File

@ -0,0 +1,168 @@
package dev.kske.chess.uci;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import dev.kske.chess.board.Move;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>UCIReceiver.java</strong><br>
* Created: <strong>19.07.2019</strong><br>
*
* @since Chess v0.3-alpha
* @author Kai S. K. Engelbart
*/
public class UCIReceiver implements Runnable {
private final BufferedReader in;
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) {
this.in = new BufferedReader(new InputStreamReader(in));
listeners = new ArrayList<>();
}
/**
* Starts listening for UCI commands passed through the input stream.
*/
@Override
public void run() {
String line;
while (!Thread.currentThread().isInterrupted())
try {
if ((line = in.readLine()) != null && !line.isEmpty())
parse(line);
} catch (IndexOutOfBoundsException ex) {
System.err.println("Too few arguments were provided!");
ex.printStackTrace();
} catch (IOException ex) {
ex.printStackTrace();
}
}
private void parse(String line) {
int spaceIndex = line.indexOf(' ');
String command
= spaceIndex == -1 ? line : line.substring(0, spaceIndex);
switch (command) {
case "id":
parseId(line.substring(command.length() + 1));
break;
case "uciok":
listeners.forEach(UCIListener::onUCIOk);
break;
case "readyok":
listeners.forEach(UCIListener::onReadyOk);
break;
case "bestmove":
parseBestMove(line.substring(command.length() + 1));
break;
case "copyprotection":
parseCopyProtection(line.substring(command.length() + 1));
break;
case "registration":
parseRegistration(line.substring(command.length() + 1));
break;
case "info":
parseInfo(line.substring(command.length() + 1));
break;
case "option":
parseOption(line.substring(command.length() + 1));
break;
default:
System.err.printf("Unknown command '%s' found!%n", command);
}
}
private void parseId(String line) {
String param = line.substring(0, line.indexOf(' '));
String arg = line.substring(param.length() + 1);
switch (param) {
case "name":
listeners.forEach(l -> l.onIdName(arg));
break;
case "author":
listeners.forEach(l -> l.onIdAuthor(arg));
break;
default:
System.err.printf(
"Unknown parameter '%s' for command 'id' found!%n",
param
);
}
}
private void parseBestMove(String line) {
String[] tokens = line.split(" ");
String move = tokens[0];
// Ponder move
if (tokens.length == 3)
listeners.forEach(l -> l.onBestMove(move, Move.fromLAN(tokens[2])));
else
listeners.forEach(l -> l.onBestMove(move));
}
private void parseCopyProtection(String line) {
switch (line) {
case "checking":
listeners.forEach(UCIListener::onCopyProtectionChecking);
break;
case "ok":
listeners.forEach(UCIListener::onCopyProtectionOk);
break;
case "error":
listeners.forEach(UCIListener::onCopyProtectionError);
break;
default:
System.err.printf(
"Unknown parameter '%s' for command 'copyprotection' found!%n",
line
);
}
}
private void parseRegistration(String line) {
switch (line) {
case "checking":
listeners.forEach(UCIListener::onRegistrationChecking);
break;
case "ok":
listeners.forEach(UCIListener::onRegistrationOk);
break;
case "error":
listeners.forEach(UCIListener::onRegistrationError);
break;
default:
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 parseOption(String line) {
listeners.forEach(l -> l.onOption(new UCIOption(line)));
}
/**
* Registers a UCI listener
*
* @param listener the UCI listener to register
*/
public void registerListener(UCIListener listener) {
listeners.add(listener);
}
}

View File

@ -6,16 +6,19 @@ import java.awt.Graphics;
import javax.swing.JComponent;
import dev.kske.chess.board.Board;
import dev.kske.chess.io.TextureUtil;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>BoardComponent.java</strong><br>
* Created: <strong>01.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong><br>
* <br>
* 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
* size.
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class BoardComponent extends JComponent {
@ -25,6 +28,12 @@ public class BoardComponent extends JComponent {
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) {
this.boardPane = boardPane;
setSize(boardPane.getPreferredSize());
@ -34,27 +43,44 @@ public class BoardComponent extends JComponent {
protected void paintComponent(Graphics g) {
super.paintComponent(g);
final int tileSize = getTileSize();
final int tileSize = boardPane.getTileSize();
// Draw the board
g.setColor(Color.white);
for (int i = 0; i < 8; i++)
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);
}
// Draw the pieces if a board is present
if (board != null) for (int i = 0; i < 8; i++)
for (int j = 0; j < 8; j++)
if (board.getBoardArr()[i][j] != null) g.drawImage(TextureUtil
.getPieceTexture(board.getBoardArr()[i][j]), i * tileSize, j * tileSize, this);
if (board != null)
for (int i = 0; i < 8; i++)
for (int j = 0; j < 8; j++)
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; }
/**
* 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) {
this.board = board;
repaint();

View File

@ -0,0 +1,62 @@
package dev.kske.chess.ui;
import java.awt.Dimension;
import javax.swing.JLayeredPane;
/**
* Combines a {@link BoardComponent} and an {@link OverlayComponent} into a
* layered pane.
* <br>
* Project: <strong>Chess</strong><br>
* File: <strong>BoardPane.java</strong><br>
* Created: <strong>08.07.2019</strong><br>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class BoardPane extends JLayeredPane {
private static final long serialVersionUID = -5415058382478806092L;
private final BoardComponent boardComponent;
private final OverlayComponent overlayComponent;
private int tileSize;
/**
* Creates an instance of {@link BoardPane}.
*/
public BoardPane() {
boardComponent = new BoardComponent(this);
overlayComponent = new OverlayComponent(this);
setLayer(overlayComponent, 1);
setLayout(null);
add(boardComponent);
add(overlayComponent);
tileSize = 60;
setSize(getPreferredSize());
}
@Override
public Dimension getPreferredSize() { return new Dimension(480, 480); }
/**
* @return the board component contained inside this board pane
*/
public BoardComponent getBoardComponent() { return boardComponent; }
/**
* @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; }
}

View File

@ -0,0 +1,139 @@
package dev.kske.chess.ui;
import java.awt.Component;
import java.awt.Font;
import java.io.File;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.prefs.Preferences;
import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;
import dev.kske.chess.io.EngineUtil;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>DialogUtil.java</strong><br>
* Created: <strong>24.07.2019</strong><br>
*
* @since Chess v0.3-alpha
* @author Kai S. K. Engelbart
*/
public class DialogUtil {
private DialogUtil() {}
/**
* Saves the last accessed folder for loading and saving game files.
*/
private static Preferences preferences
= Preferences.userNodeForPackage(DialogUtil.class);
/**
* 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);
if (fileChooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) {
action.accept(Arrays.asList(fileChooser.getSelectedFile()));
preferences.put("path", fileChooser.getSelectedFile().getParent());
}
}
/**
* 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);
if (fileChooser.showSaveDialog(parent) == JFileChooser.APPROVE_OPTION) {
action.accept(
new File(
fileChooser.getSelectedFile().getAbsolutePath() + "."
+ ((FileNameExtensionFilter) fileChooser
.getFileFilter()).getExtensions()[0]
)
);
preferences.put("path", fileChooser.getSelectedFile().getParent());
}
}
private static JFileChooser
createFileChooser(Collection<FileNameExtensionFilter> filters) {
JFileChooser fileChooser = new JFileChooser();
fileChooser.setCurrentDirectory(
new File(preferences.get("path", System.getProperty("user.home")))
);
fileChooser.setAcceptAllFileFilterUsed(false);
filters.forEach(fileChooser::addChoosableFileFilter);
return fileChooser;
}
/**
* 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();
List<String> options
= new ArrayList<>(Arrays.asList("Natural Player", "AI Player"));
EngineUtil.getEngineInfos().forEach(info -> options.add(info.name));
JLabel lblWhite = new JLabel("White:");
lblWhite.setFont(new Font("Tahoma", Font.PLAIN, 14));
lblWhite.setBounds(10, 11, 49, 14);
dialogPanel.add(lblWhite);
JComboBox<Object> cbWhite = new JComboBox<>();
cbWhite.setModel(new DefaultComboBoxModel<>(options.toArray()));
cbWhite.setBounds(98, 9, 159, 22);
dialogPanel.add(cbWhite);
JLabel lblBlack = new JLabel("Black:");
lblBlack.setFont(new Font("Tahoma", Font.PLAIN, 14));
lblBlack.setBounds(10, 38, 49, 14);
dialogPanel.add(lblBlack);
JComboBox<Object> cbBlack = new JComboBox<>();
cbBlack.setModel(new DefaultComboBoxModel<>(options.toArray()));
cbBlack.setBounds(98, 36, 159, 22);
dialogPanel.add(cbBlack);
JOptionPane.showMessageDialog(
parent,
dialogPanel,
"Game configuration",
JOptionPane.QUESTION_MESSAGE
);
action.accept(
options.get(cbWhite.getSelectedIndex()),
options.get(cbBlack.getSelectedIndex())
);
}
}

View File

@ -0,0 +1,51 @@
package dev.kske.chess.ui;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTargetAdapter;
import java.awt.dnd.DropTargetDropEvent;
import java.io.File;
import java.io.IOException;
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>
* File: <strong>GameDropTarget.java</strong><br>
* Created: <strong>13 Aug 2019</strong><br>
*
* @since Chess v0.3-alpha
* @author Kai S. K. Engelbart
*/
public class GameDropTarget extends DropTargetAdapter {
private 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")
@Override
public void drop(DropTargetDropEvent evt) {
try {
evt.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
mainWindow.loadFiles(
(List<File>) evt.getTransferable()
.getTransferData(DataFlavor.javaFileListFlavor)
);
} catch (UnsupportedFlavorException | IOException ex) {
ex.printStackTrace();
evt.rejectDrop();
}
}
}

View File

@ -0,0 +1,230 @@
package dev.kske.chess.ui;
import java.awt.*;
import javax.swing.*;
import dev.kske.chess.board.*;
import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.event.*;
import dev.kske.chess.game.*;
import dev.kske.eventbus.*;
import dev.kske.eventbus.Event;
/**
* 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>
* File: <strong>GamePane.java</strong><br>
* Created: <strong>23.08.2019</strong><br>
*
* @since Chess v0.4-alpha
* @author Kai S. K. Engelbart
*/
public class GamePane extends JComponent implements EventListener {
private static final long serialVersionUID = 4349772338239617477L;
private JButton btnRestart, btnSwapColors;
private BoardPane boardPane;
private Game game;
private Color activeColor;
private JPanel moveSelectionPanel;
private JButton btnFirst, btnPrevious, btnNext, btnLast;
private JList<MoveNode> pgnList;
/**
* Creates an instance of {@link GamePane}.
*/
public GamePane() {
activeColor = Color.WHITE;
GridBagLayout gridBagLayout = new GridBagLayout();
gridBagLayout.columnWidths = new int[] {
450, 1, 0
};
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);
JPanel toolPanel = new JPanel();
btnRestart = new JButton("Restart");
btnRestart.addActionListener(evt -> {
if (game != null) {
game.reset();
game.start();
}
});
btnSwapColors = new JButton("Play as black");
btnSwapColors.addActionListener(evt -> {
game.swapColors();
btnSwapColors
.setText("Play as " + activeColor.toString().toLowerCase());
activeColor = activeColor.opposite();
});
toolPanel.add(btnRestart);
toolPanel.add(btnSwapColors);
GridBagConstraints gbc_toolPanel = new GridBagConstraints();
gbc_toolPanel.anchor = GridBagConstraints.NORTH;
gbc_toolPanel.fill = GridBagConstraints.HORIZONTAL;
gbc_toolPanel.gridx = 0;
gbc_toolPanel.gridy = 0;
gbc_toolPanel.gridwidth = 2;
add(toolPanel, gbc_toolPanel);
moveSelectionPanel = new JPanel();
GridBagConstraints gbc_moveSelectionPanel = new GridBagConstraints();
gbc_moveSelectionPanel.fill = GridBagConstraints.BOTH;
gbc_moveSelectionPanel.gridx = 2;
gbc_moveSelectionPanel.gridy = 0;
add(moveSelectionPanel, gbc_moveSelectionPanel);
btnFirst = new JButton("First");
btnFirst.setEnabled(false);
moveSelectionPanel.add(btnFirst);
btnPrevious = new JButton("Previous");
btnPrevious.addActionListener(evt -> {
if (game != null) {
game.getBoard().selectPreviousNode();
getBoardPane().getOverlayComponent().clearArrow();
repaint();
}
});
moveSelectionPanel.add(btnPrevious);
btnNext = new JButton("Next");
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);
btnLast = new JButton("Last");
btnLast.setEnabled(false);
moveSelectionPanel.add(btnLast);
boardPane = new BoardPane();
GridBagConstraints gbc_boardPane = new GridBagConstraints();
gbc_boardPane.fill = GridBagConstraints.BOTH;
gbc_boardPane.gridx = 0;
gbc_boardPane.gridy = 1;
add(boardPane, gbc_boardPane);
JPanel numberPanel = new JPanel(new GridLayout(8, 1));
GridBagConstraints gbc_numberPanel = new GridBagConstraints();
gbc_numberPanel.anchor = GridBagConstraints.WEST;
gbc_numberPanel.fill = GridBagConstraints.VERTICAL;
gbc_numberPanel.gridx = 1;
gbc_numberPanel.gridy = 1;
add(numberPanel, gbc_numberPanel);
JPanel letterPanel = new JPanel(new GridLayout(1, 8));
GridBagConstraints gbc_letterPanel = new GridBagConstraints();
gbc_letterPanel.anchor = GridBagConstraints.NORTH;
gbc_letterPanel.fill = GridBagConstraints.HORIZONTAL;
gbc_letterPanel.gridx = 0;
gbc_letterPanel.gridy = 2;
add(letterPanel, gbc_letterPanel);
// Initialize board coordinates
for (int i = 0; i < 8; i++) {
numberPanel.add(new JLabel(String.valueOf(8 - i)));
JLabel letterLabel = new JLabel(String.valueOf((char) (65 + i)));
letterLabel.setHorizontalAlignment(SwingConstants.CENTER);
letterPanel.add(letterLabel);
}
JScrollPane scrollPane = new JScrollPane();
GridBagConstraints gbc_scrollPane = new GridBagConstraints();
gbc_scrollPane.fill = GridBagConstraints.BOTH;
gbc_scrollPane.gridx = 2;
gbc_scrollPane.gridy = 1;
add(scrollPane, gbc_scrollPane);
pgnList = new JList<>();
pgnList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
pgnList.setLayoutOrientation(JList.HORIZONTAL_WRAP);
pgnList.setVisibleRowCount(0);
pgnList.setCellRenderer(new MoveNodeRenderer());
scrollPane.setViewportView(pgnList);
// Listen to moves and game (re-)starts and update the move list or
// disable the
// color switching buttons if necessary
EventBus.getInstance().registerListener(this);
}
@Event
public void onMove(MoveEvent evt) {
btnSwapColors.setEnabled(
evt.getBoardState() != BoardState.CHECKMATE
&& evt.getBoardState() != BoardState.STALEMATE
);
updateLog();
}
@Event
public void onGameStart(GameStartEvent evt) {
btnSwapColors.setEnabled(
game.getPlayers().get(Color.WHITE) instanceof NaturalPlayer ^ game.getPlayers().get(Color.BLACK) instanceof NaturalPlayer
);
updateLog();
}
private void updateLog() {
if (game.getBoard().getLog() == null)
return;
DefaultListModel<MoveNode> model = new DefaultListModel<>();
game.getBoard().getLog().forEach(model::addElement);
pgnList.setModel(model);
}
/**
* @return The {@link BoardPane} instance associated with this game pane
*/
public BoardPane getBoardPane() { return boardPane; }
/**
* @return The {@link Game} instance associated with this game pane
*/
public Game getGame() { return game; }
/**
* Assigns a new {@link Game} instance to this game pane. If exactly one of
* the
* players is natural, color swapping functionality is enabled.
*
* @param game The {@link Game} to assign to this game pane.
*/
public void setGame(Game game) {
if (this.game != null)
this.game.stop();
this.game = game;
}
}

View File

@ -0,0 +1,119 @@
package dev.kske.chess.ui;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.*;
import javax.swing.plaf.basic.BasicButtonUI;
/**
* Renders the title and the closing button of a {@link JTabbedPane}.<br>
* <br>
* Project: <strong>Chess</strong><br>
* File: <strong>GameTabComponent.java</strong><br>
* Created: <strong>11 Dec 2019</strong><br>
*
* @author Kai S. K. Engelbart
*/
public class GameTabComponent extends JPanel {
private final JTabbedPane tabbedPane;
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) {
super(new FlowLayout(FlowLayout.LEFT, 0, 0));
if (tabbedPane == null)
throw new NullPointerException("TabbedPane is null");
this.tabbedPane = tabbedPane;
// Create title JLabel
JLabel label = new JLabel() {
private static final long serialVersionUID = 7902391411509551586L;
@Override
public String getText() {
int i = tabbedPane.indexOfTabComponent(GameTabComponent.this);
return i != -1 ? tabbedPane.getTitleAt(i) : "";
}
};
label.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 5));
add(label);
// Create close JButton
add(new TabButton());
setBorder(BorderFactory.createEmptyBorder(2, 0, 0, 0));
}
private class TabButton extends JButton {
private static final long serialVersionUID = -2757857832337636731L;
public TabButton() {
final int size = 17;
setPreferredSize(new Dimension(size, size));
setToolTipText("Close this tab");
setUI(new BasicButtonUI());
setContentAreaFilled(false);
setFocusable(false);
setBorder(BorderFactory.createEtchedBorder());
setBorderPainted(false);
addMouseListener(new MouseAdapter() {
@Override
public void mouseEntered(MouseEvent evt) {
setBorderPainted(true);
}
@Override
public void mouseExited(MouseEvent evt) {
setBorderPainted(false);
}
});
setRolloverEnabled(true);
addActionListener(evt -> {
int i = tabbedPane.indexOfTabComponent(GameTabComponent.this);
if (i != -1)
tabbedPane.remove(i);
});
}
@Override
public void updateUI() {}
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create();
// shift the image for pressed buttons
if (getModel().isPressed())
g2.translate(1, 1);
g2.setStroke(new BasicStroke(2));
g2.setColor(Color.BLACK);
if (getModel().isRollover())
g2.setColor(Color.MAGENTA);
final int delta = 6;
g2.drawLine(
delta,
delta,
getWidth() - delta - 1,
getHeight() - delta - 1
);
g2.drawLine(
getWidth() - delta - 1,
delta,
delta,
getHeight() - delta - 1
);
g2.dispose();
}
}
}

View File

@ -0,0 +1,255 @@
package dev.kske.chess.ui;
import java.awt.Desktop;
import java.awt.Toolkit;
import java.awt.dnd.DropTarget;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.List;
import javax.swing.*;
import dev.kske.chess.board.Board;
import dev.kske.chess.board.FENString;
import dev.kske.chess.exception.ChessException;
import dev.kske.chess.game.Game;
import dev.kske.chess.pgn.PGNDatabase;
import dev.kske.chess.pgn.PGNGame;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>MainWindow.java</strong><br>
* Created: <strong>01.07.2019</strong><br>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class MainWindow extends JFrame {
private JTabbedPane tabbedPane = new JTabbedPane();
private static final long serialVersionUID = -3100939302567978977L;
/**
* Launch the application.
*
* @param args command line arguments are ignored
*/
public static void main(String[] args) {
SwingUtilities.invokeLater(MainWindow::new);
}
/**
* Create the application.
*/
private MainWindow() {
super("Chess by Kai S. K. Engelbart");
// Configure frame
setResizable(false);
setBounds(100, 100, 494, 565);
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
setIconImage(
Toolkit.getDefaultToolkit()
.getImage(getClass().getResource("/pieces/queen_white.png"))
);
// Add tabbed pane, menu bar and drop target
getContentPane().add(tabbedPane);
setJMenuBar(new MenuBar(this));
new DropTarget(this, new GameDropTarget(this));
// Update position and dimensions
pack();
setLocationRelativeTo(null);
setVisible(true);
}
/**
* @return the currently selected {@link GamePane} component
*/
public GamePane getSelectedGamePane() {
return (GamePane) tabbedPane.getSelectedComponent();
}
/**
* 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.
*
* @return the new {@link GamePane}
*/
public GamePane addGamePane() {
return addGamePane("Game " + (tabbedPane.getTabCount() + 1));
}
/**
* Creates a new {@link GamePane}, adds it to the tabbed pane and opens it.
*
* @param title The title of the {@link GamePane}
* @return the new {@link GamePane}
*/
public GamePane addGamePane(String title) {
GamePane gamePane = new GamePane();
tabbedPane.add(title, gamePane);
tabbedPane.setTabComponentAt(
tabbedPane.getTabCount() - 1,
new GameTabComponent(tabbedPane)
);
tabbedPane.setSelectedIndex(tabbedPane.getTabCount() - 1);
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) {
GamePane gamePane = addGamePane(title);
DialogUtil.showGameConfigurationDialog(
this,
(whiteName, blackName) -> {
Game game = new Game(
gamePane.getBoardPane(),
whiteName,
blackName,
board
);
gamePane.setGame(game);
game.start();
}
);
return gamePane;
}
/**
* Removes a {@link GamePane} form the tabbed pane.
*
* @param index The index of the {@link GamePane} to remove
*/
public void removeGamePane(int index) {
tabbedPane.remove(index);
}
/**
* Loads a game file (FEN or PGN) and adds it to a new {@link GamePane}.
*
* @param files the files to load the game from
*/
public void loadFiles(List<File> files) {
files.forEach(file -> {
final String name
= file.getName().substring(0, file.getName().lastIndexOf('.'));
final String extension = file.getName()
.substring(file.getName().lastIndexOf('.'))
.toLowerCase();
try {
Board board;
switch (extension) {
case ".fen":
board = new FENString(
new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8)
).getBoard();
break;
case ".pgn":
PGNDatabase pgnDB = new PGNDatabase();
pgnDB.load(file);
if (pgnDB.getGames().size() > 0) {
String[] gameNames
= new String[pgnDB.getGames().size()];
for (int i = 0; i < gameNames.length; 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")
);
}
JComboBox<String> comboBox
= new JComboBox<>(gameNames);
JOptionPane.showMessageDialog(
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;
default:
throw new ChessException(
"The file extension '" + extension
+ "' is not supported!"
);
}
addGamePane(name, board);
} catch (Exception e) {
e.printStackTrace();
JOptionPane.showMessageDialog(
this,
"Failed to load the file " + file.getName() + ": "
+ 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) {
final int dotIndex = file.getName().lastIndexOf('.');
final String extension
= file.getName().substring(dotIndex).toLowerCase();
if (extension.equals(".pgn"))
try {
PGNGame pgnGame
= new PGNGame(getSelectedGamePane().getGame().getBoard());
pgnGame.setTag(
"Event",
tabbedPane.getTitleAt(tabbedPane.getSelectedIndex())
);
pgnGame.setTag("Result", "*");
PGNDatabase pgnDB = new PGNDatabase();
pgnDB.getGames().add(pgnGame);
pgnDB.save(file);
if (
JOptionPane.showConfirmDialog(
this,
"Game export finished. Do you want to view the created file?",
"Game export finished",
JOptionPane.YES_NO_OPTION
) == JOptionPane.YES_OPTION
)
Desktop.getDesktop().open(file);
} catch (IOException e) {
e.printStackTrace();
JOptionPane.showMessageDialog(
this,
"Failed to save the file " + file.getName() + ": " + e,
"File saving error",
JOptionPane.ERROR_MESSAGE
);
}
}
}

View File

@ -0,0 +1,174 @@
package dev.kske.chess.ui;
import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
import java.util.Arrays;
import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;
import dev.kske.chess.board.FENString;
import dev.kske.chess.exception.ChessException;
import dev.kske.chess.game.Game;
import dev.kske.chess.io.EngineUtil;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>MenuBar.java</strong><br>
* Created: <strong>16.07.2019</strong><br>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class MenuBar extends JMenuBar {
private static final long serialVersionUID = -7221583703531248228L;
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) {
this.mainWindow = mainWindow;
initGameMenu();
initEngineMenu();
initToolsMenu();
}
private void initGameMenu() {
JMenu gameMenu = new JMenu("Game");
JMenuItem newGameMenuItem = new JMenuItem("New Game");
newGameMenuItem.addActionListener(
evt -> DialogUtil.showGameConfigurationDialog(
mainWindow,
(whiteName, blackName) -> {
GamePane gamePane = mainWindow.addGamePane();
Game game = new Game(
gamePane.getBoardPane(),
whiteName,
blackName
);
gamePane.setGame(game);
game.start();
}
)
);
gameMenu.add(newGameMenuItem);
JMenuItem loadFileMenu = new JMenuItem("Load game file");
loadFileMenu.addActionListener(
evt -> DialogUtil
.showFileSelectionDialog(
mainWindow,
mainWindow::loadFiles,
Arrays.asList(
new FileNameExtensionFilter(
"FEN and PGN files",
"fen",
"pgn"
)
)
)
);
gameMenu.add(loadFileMenu);
JMenuItem saveFileMenu = new JMenuItem("Save game file");
saveFileMenu
.addActionListener(
evt -> DialogUtil
.showFileSaveDialog(
mainWindow,
mainWindow::saveFile,
Arrays.asList(
new FileNameExtensionFilter("PGN file", "pgn")
)
)
);
gameMenu.add(saveFileMenu);
add(gameMenu);
newGameMenuItem.doClick();
}
private void initEngineMenu() {
JMenu engineMenu = new JMenu("Engine");
JMenuItem addEngineMenuItem = new JMenuItem("Add engine");
addEngineMenuItem.addActionListener(evt -> {
String enginePath = JOptionPane
.showInputDialog(
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");
engineMenu.add(addEngineMenuItem);
engineMenu.add(showInfoMenuItem);
add(engineMenu);
}
private void initToolsMenu() {
JMenu toolsMenu = new JMenu("Tools");
JMenuItem exportFENMenuItem = new JMenuItem("Export board to FEN");
exportFENMenuItem.addActionListener(evt -> {
final String fen = new FENString(
mainWindow.getSelectedGamePane().getGame().getBoard()
).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);
JMenuItem loadFromFENMenuItem = new JMenuItem("Load board from FEN");
loadFromFENMenuItem.addActionListener(evt -> {
final GamePane gamePane = mainWindow.addGamePane();
final String fen
= JOptionPane.showInputDialog("Enter a FEN string: ");
DialogUtil.showGameConfigurationDialog(
mainWindow,
(whiteName, blackName) -> {
Game game;
try {
game = new Game(
gamePane.getBoardPane(),
whiteName,
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);
add(toolsMenu);
}
}

View File

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

View File

@ -0,0 +1,173 @@
package dev.kske.chess.ui;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JComponent;
import dev.kske.chess.board.Move;
import dev.kske.chess.board.Position;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>OverlayComponent.java</strong><br>
* Created: <strong>08.07.2019</strong><br>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class OverlayComponent extends JComponent {
private static final long serialVersionUID = -7326936060890082183L;
private final BoardPane boardPane;
private List<Position> dots;
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) {
this.boardPane = boardPane;
dots = new ArrayList<>();
setSize(boardPane.getPreferredSize());
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
final int tileSize = boardPane.getTileSize();
// Draw an arrow representing the last move and mark its position and
// destination
if (arrow != null) {
Point pos = new Point(
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;
g2d.setStroke(new BasicStroke(3));
g2d.setColor(Color.yellow);
g2d.drawRect(
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);
g.setColor(new Color(255, 0, 0, 127));
g2d.fill(arrowShape);
g2d.setColor(Color.black);
g2d.draw(arrowShape);
}
// Draw possible moves if a piece was selected
if (!dots.isEmpty()) {
g.setColor(Color.green);
int radius = tileSize / 4;
for (Position dot : dots)
g.fillOval(
dot.x * tileSize + tileSize / 2 - radius / 2,
dot.y * tileSize + tileSize / 2 - radius / 2,
radius,
radius
);
}
}
private Shape createArrowShape(Point pos, Point dest) {
Polygon arrowPolygon = new Polygon();
arrowPolygon.addPoint(-6, 1);
arrowPolygon.addPoint(3, 1);
arrowPolygon.addPoint(3, 3);
arrowPolygon.addPoint(6, 0);
arrowPolygon.addPoint(3, -3);
arrowPolygon.addPoint(3, -1);
arrowPolygon.addPoint(-6, -1);
Point midPoint = midpoint(pos, dest);
double rotate = Math.atan2(dest.y - pos.y, dest.x - pos.x);
double ptDistance = pos.distance(dest);
double scale = ptDistance / 12.0; // 12 because it's the length of the
// arrow
// polygon.
AffineTransform transform = new AffineTransform();
transform.translate(midPoint.x, midPoint.y);
transform.rotate(rotate);
transform.scale(scale, 5);
return transform.createTransformedShape(arrowPolygon);
}
private Point midpoint(Point p1, Point p2) {
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) {
this.dots.clear();
this.dots.addAll(dots);
repaint();
}
/**
* Clears all dots displayed at some positions.
*/
public void clearDots() {
dots.clear();
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) {
this.arrow = arrow;
repaint();
}
/**
* Clears the arrow displayed to indicate a move.
*/
public void clearArrow() {
arrow = null;
repaint();
}
/**
* @return the size of one board tile in pixels.
* @see dev.kske.chess.ui.BoardPane#getTileSize()
*/
public int getTileSize() { return boardPane.getTileSize(); }
}

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 797 B

After

Width:  |  Height:  |  Size: 797 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 725 B

After

Width:  |  Height:  |  Size: 725 B

View File

Before

Width:  |  Height:  |  Size: 933 B

After

Width:  |  Height:  |  Size: 933 B

View File

@ -1,14 +1,11 @@
package dev.kske.chess.test;
package dev.kske.chess.board;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import dev.kske.chess.board.Board;
import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.board.Queen;
/**
* Project: <strong>Chess</strong><br>
@ -18,7 +15,7 @@ import dev.kske.chess.board.Queen;
*/
class BoardTest {
Board board;
private Board board;
/**
* @throws java.lang.Exception
@ -29,15 +26,20 @@ class BoardTest {
}
/**
* Test method for {@link dev.kske.chess.board.Board#clone()}.
* Test method for {@link Board#Board(Board, boolean)}.
*/
@Test
void testClone() {
Board clone = (Board) board.clone();
Board clone = new Board(board, false);
assertNotSame(clone, board);
assertNotSame(clone.getBoardArr(), board.getBoardArr());
clone.getBoardArr()[0][0] = new Queen(Color.BLACK, clone);
clone.move(new Move(1, 1, 1, 2));
assertNotEquals(clone.getBoardArr()[0][0], board.getBoardArr()[0][0]);
assertNotEquals(
clone.getLog().getActiveColor(),
board.getLog().getActiveColor()
);
}
}

View File

@ -0,0 +1,87 @@
package dev.kske.chess.board;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.exception.ChessException;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>FENStringTest.java</strong><br>
* Created: <strong>24 Oct 2019</strong><br>
*
* @author Kai S. K. Engelbart
*/
class FENStringTest {
private List<String> fenStrings = new ArrayList<>();
private List<Board> boards = new ArrayList<>();
/**
* Removes all pieces from a board
*
* @param board the board to clean
*/
void cleanBoard(Board board) {
for (int i = 0; i < 8; i++)
for (int j = 0; j < 8; j++)
board.getBoardArr()[i][j] = null;
}
/**
* @throws java.lang.Exception
*/
@BeforeEach
void setUp() throws Exception {
fenStrings.addAll(
Arrays.asList(
"rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2"
)
);
Board board = new Board();
board.set(Position.fromLAN("c7"), null);
board.set(Position.fromLAN("c5"), new Pawn(Color.BLACK, board));
board.set(Position.fromLAN("e4"), new Pawn(Color.WHITE, board));
board.set(Position.fromLAN("f3"), new Knight(Color.WHITE, board));
board.set(Position.fromLAN("e2"), null);
board.set(Position.fromLAN("g1"), null);
board.getLog().setActiveColor(Color.BLACK);
board.getLog().setHalfmoveClock(1);
board.getLog().setFullmoveNumber(2);
boards.add(board);
}
/**
* Test method for {@link FENString#toString()}.
*/
@Test
void testToString() {
for (int i = 0; i < fenStrings.size(); i++)
assertEquals(
fenStrings.get(i),
new FENString(boards.get(i)).toString()
);
}
/**
* Test method for {@link FENString#getBoard()}.
*
* @throws ChessException
*/
@Test
void testGetBoard() throws ChessException {
for (int i = 0; i < boards.size(); i++)
assertEquals(
boards.get(i),
new FENString(fenStrings.get(i)).getBoard()
);
}
}

View File

@ -0,0 +1,165 @@
package dev.kske.chess.board;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import dev.kske.chess.board.Piece.Color;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>LogTest.java</strong><br>
* Created: <strong>13 Sep 2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
class LogTest {
private Log log = new Log();
/**
* Test method for {@link Log#Log()}.
*/
@Test
void testLog() {
assertTrue(log.isEmpty());
assertNull(log.getLast());
assertNull(log.getRoot());
assertEquals(log.getActiveColor(), Color.WHITE);
assertNull(log.getEnPassant());
assertEquals(log.getFullmoveNumber(), 1);
assertEquals(log.getHalfmoveClock(), 0);
}
/**
* Test method for {@link Log#Log(Log, boolean)}.
*/
@Test
void testClone() {
Log other = new Log(log, false);
log.setActiveColor(Color.WHITE);
other.setActiveColor(Color.BLACK);
assertNotEquals(log.getActiveColor(), other.getActiveColor());
log.add(Move.fromLAN("a2a4"), new Pawn(Color.WHITE, null), null);
log.add(Move.fromLAN("a4a5"), 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);
assertNotEquals(log.getRoot(), other.getRoot());
assertNotEquals(
log.getRoot().getVariations(),
other.getRoot().getVariations()
);
}
/**
* Test method for {@link Log#add(Move, Piece, Piece)}.
*/
@Test
void testAdd() {
fail("Not yet implemented");
}
/**
* Test method for {@link Log#removeLast()}.
*/
@Test
void testRemoveLast() {
fail("Not yet implemented");
}
/**
* Test method for {@link Log#isEmpty()}.
*/
@Test
void testIsEmpty() {
fail("Not yet implemented");
}
/**
* Test method for {@link Log#reset()}.
*/
@Test
void testReset() {
fail("Not yet implemented");
}
/**
* Test method for {@link Log#getRoot()}.
*/
@Test
void testGetRoot() {
fail("Not yet implemented");
}
/**
* Test method for {@link Log#getLast()}.
*/
@Test
void testGetLast() {
fail("Not yet implemented");
}
/**
* Test method for {@link Log#getEnPassant()}.
*/
@Test
void testGetEnPassant() {
fail("Not yet implemented");
}
/**
* Test method for {@link Log#setEnPassant(dev.kske.chess.board.Position)}.
*/
@Test
void testSetEnPassant() {
fail("Not yet implemented");
}
/**
* Test method for {@link Log#getActiveColor()}.
*/
@Test
void testGetActiveColor() {
fail("Not yet implemented");
}
/**
* Test method for
* {@link Log#setActiveColor(dev.kske.chess.board.Piece.Color)}.
*/
@Test
void testSetActiveColor() {
fail("Not yet implemented");
}
/**
* Test method for {@link Log#getFullmoveNumber()}.
*/
@Test
void testGetFullmoveCounter() {
fail("Not yet implemented");
}
/**
* Test method for {@link Log#setFullmoveNumber(int)}.
*/
@Test
void testSetFullmoveCounter() {
fail("Not yet implemented");
}
/**
* Test method for {@link Log#getHalfmoveClock()}.
*/
@Test
void testGetHalfmoveClock() {
fail("Not yet implemented");
}
/**
* Test method for {@link Log#setHalfmoveClock(int)}.
*/
@Test
void testSetHalfmoveClock() {
fail("Not yet implemented");
}
}

View File

@ -0,0 +1,56 @@
package dev.kske.chess.board;
import static org.junit.Assert.assertEquals;
import org.junit.jupiter.api.Test;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>PositionTest.java</strong><br>
* Created: <strong>24.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
class PositionTest {
private final int n = 4;
private Position[] positions = new Position[] {
new Position(0, 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
* {@link Position#fromLAN(java.lang.String)}.
*/
@Test
void testFromSAN() {
for (int i = 0; i < n; i++)
assertEquals(positions[i], Position.fromLAN(sans[i]));
}
/**
* Test method for {@link Position#toLAN()}.
*/
@Test
void testToSAN() {
for (int i = 0; i < n; i++)
assertEquals(sans[i], positions[i].toLAN());
}
/**
* Test method for {@link Position#toString()}.
*/
@Test
void testToString() {
for (int i = 0; i < n; i++)
assertEquals(strings[i], positions[i].toString());
}
}

View File

@ -0,0 +1,33 @@
package dev.kske.chess.pgn;
import java.io.File;
import java.io.FileNotFoundException;
import org.junit.jupiter.api.Test;
import dev.kske.chess.exception.ChessException;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>PGNDatabaseTest.java</strong><br>
* Created: <strong>4 Oct 2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
class PGNDatabaseTest {
/**
* Test method for
* {@link dev.kske.chess.pgn.PGNDatabase#load(java.io.File)}.
*
* @throws ChessException if an error occurs while parsing the file
* @throws FileNotFoundException if the test file {@code test.pgn} is not
* present
*/
@Test
void testLoad() throws FileNotFoundException, ChessException {
PGNDatabase db = new PGNDatabase();
db.load(
new File(getClass().getClassLoader().getResource("test.pgn").getFile())
);
}
}

View File

@ -0,0 +1,18 @@
[Event "Test Game"]
[Site "Test Environment"]
[Date "2019.10.04"]
[Round "1"]
[Result "1-0"]
[White "Kai Engelbart"]
[Black "Kai Engelbart 2"]
1. e4 c5 2. Nf3 e6 3. d4 cxd4 4. Nxd4 a6 5. Bd3 Nf6 6. O-O d6
7. c4 Bd7 8. Nc3 Nc6 9. Be3 Be7 10. h3 Ne5 11. Be2 Rc8 12. Qb3
Qc7 13. Rac1 O-O 14. f4 Nc6 15. Nf3 Qb8 16. Qd1 Be8 17. Qd2
Na5 18. b3 b6 19. Bd3 Nc6 20. Qf2 b5 21. Rfd1 Nb4 22. Bf1 bxc4
23. bxc4 a5 24. Nd4 Qa8 25. Qf3 Na6 26. Ndb5 Nc5 27. e5 dxe5
28. Qxa8 Rxa8 29. fxe5 Nfe4 30. Nd6 Bc6 31. Ncxe4 Nxe4 32. c5
Ng3 33. Bc4 h5 34. Bf2 h4 35. Bxg3 hxg3 36. Bb5 Bxb5 37. Nxb5
f6 38. Rd7 Bd8 39. Rc3 fxe5 40. Rxg3 Rf7 41. Rxf7 Kxf7 42. c6
Bb6+ 43. Kf1 Kf8 44. c7 Rc8 45. a4 e4 46. Ke2 e5 47. Rg6 Bd4
48. h4 Bb2 1-0