Compare commits
No commits in common. "v0.2-alpha" and "master" have entirely different histories.
v0.2-alpha
...
master
36
.classpath
@ -1,17 +1,39 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<classpath>
|
<classpath>
|
||||||
<classpathentry kind="src" path="src"/>
|
<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/5"/>
|
||||||
<classpathentry kind="src" path="res"/>
|
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
|
||||||
<classpathentry kind="src" output="bin_test" path="test">
|
|
||||||
<attributes>
|
<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"/>
|
<attribute name="test" value="true"/>
|
||||||
</attributes>
|
</attributes>
|
||||||
</classpathentry>
|
</classpathentry>
|
||||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER">
|
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
|
||||||
<attributes>
|
<attributes>
|
||||||
<attribute name="module" value="true"/>
|
<attribute name="maven.pomderived" value="true"/>
|
||||||
|
<attribute name="test" value="true"/>
|
||||||
</attributes>
|
</attributes>
|
||||||
</classpathentry>
|
</classpathentry>
|
||||||
<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/5"/>
|
<classpathentry kind="output" path="target/classes"/>
|
||||||
<classpathentry kind="output" path="bin"/>
|
|
||||||
</classpath>
|
</classpath>
|
||||||
|
23
.gitignore
vendored
@ -1,24 +1,3 @@
|
|||||||
.metadata
|
.metadata
|
||||||
bin/
|
|
||||||
/bin_test/
|
|
||||||
tmp/
|
|
||||||
*.tmp
|
|
||||||
*.bak
|
|
||||||
*.swp
|
|
||||||
*~.nib
|
|
||||||
local.properties
|
|
||||||
.settings/
|
.settings/
|
||||||
.loadpath
|
/target/
|
||||||
.recommenders
|
|
||||||
|
|
||||||
# Locally stored "Eclipse launch configurations"
|
|
||||||
*.launch
|
|
||||||
|
|
||||||
# Java annotation processor (APT)
|
|
||||||
.factorypath
|
|
||||||
|
|
||||||
# Code Recommenders
|
|
||||||
.recommenders/
|
|
||||||
|
|
||||||
# Annotation Processing
|
|
||||||
.apt_generated/
|
|
||||||
|
6
.project
@ -10,8 +10,14 @@
|
|||||||
<arguments>
|
<arguments>
|
||||||
</arguments>
|
</arguments>
|
||||||
</buildCommand>
|
</buildCommand>
|
||||||
|
<buildCommand>
|
||||||
|
<name>org.eclipse.m2e.core.maven2Builder</name>
|
||||||
|
<arguments>
|
||||||
|
</arguments>
|
||||||
|
</buildCommand>
|
||||||
</buildSpec>
|
</buildSpec>
|
||||||
<natures>
|
<natures>
|
||||||
|
<nature>org.eclipse.m2e.core.maven2Nature</nature>
|
||||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||||
</natures>
|
</natures>
|
||||||
</projectDescription>
|
</projectDescription>
|
||||||
|
67
pom.xml
Normal 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>
|
@ -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; }
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
@ -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; }
|
|
||||||
}
|
|
@ -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; }
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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; }
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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; }
|
|
||||||
}
|
|
@ -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; }
|
|
||||||
}
|
|
@ -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) {}
|
|
||||||
}
|
|
@ -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; }
|
|
||||||
}
|
|
@ -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() {}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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; }
|
|
||||||
|
|
||||||
}
|
|
@ -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; }
|
|
||||||
}
|
|
@ -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; }
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -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(); }
|
|
||||||
}
|
|
@ -7,10 +7,18 @@ import java.util.List;
|
|||||||
* Project: <strong>Chess</strong><br>
|
* Project: <strong>Chess</strong><br>
|
||||||
* File: <strong>Bishop.java</strong><br>
|
* File: <strong>Bishop.java</strong><br>
|
||||||
* Created: <strong>01.07.2019</strong><br>
|
* Created: <strong>01.07.2019</strong><br>
|
||||||
* Author: <strong>Kai S. K. Engelbart</strong>
|
*
|
||||||
|
* @since Chess v0.1-alpha
|
||||||
|
* @author Kai S. K. Engelbart
|
||||||
*/
|
*/
|
||||||
public class Bishop extends Piece {
|
public class Bishop extends Piece {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates bishop {@link Piece}.
|
||||||
|
*
|
||||||
|
* @param color the color of this bishop
|
||||||
|
* @param board the board on which this bishop will be placed
|
||||||
|
*/
|
||||||
public Bishop(Color color, Board board) {
|
public Bishop(Color color, Board board) {
|
||||||
super(color, board);
|
super(color, board);
|
||||||
}
|
}
|
||||||
@ -20,14 +28,6 @@ public class Bishop extends Piece {
|
|||||||
return move.isDiagonal() && isFreePath(move);
|
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
|
@Override
|
||||||
protected List<Move> getPseudolegalMoves(Position pos) {
|
protected List<Move> getPseudolegalMoves(Position pos) {
|
||||||
List<Move> moves = new ArrayList<>();
|
List<Move> moves = new ArrayList<>();
|
||||||
@ -35,41 +35,58 @@ public class Bishop extends Piece {
|
|||||||
// Diagonal moves to the lower right
|
// Diagonal moves to the lower right
|
||||||
for (int i = pos.x + 1, j = pos.y + 1; i < 8 && j < 8; i++, j++) {
|
for (int i = pos.x + 1, j = pos.y + 1; i < 8 && j < 8; i++, j++) {
|
||||||
Move move = new Move(pos, new Position(i, j));
|
Move move = new Move(pos, new Position(i, j));
|
||||||
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) {
|
if (
|
||||||
|
board.getDest(move) == null
|
||||||
|
|| board.getDest(move).getColor() != getColor()
|
||||||
|
) {
|
||||||
moves.add(move);
|
moves.add(move);
|
||||||
if (board.getDest(move) != null) break;
|
if (board.getDest(move) != null)
|
||||||
} else break;
|
break;
|
||||||
|
} else
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Diagonal moves to the lower left
|
// Diagonal moves to the lower left
|
||||||
for (int i = pos.x - 1, j = pos.y + 1; i >= 0 && j < 8; i--, j++) {
|
for (int i = pos.x - 1, j = pos.y + 1; i >= 0 && j < 8; i--, j++) {
|
||||||
Move move = new Move(pos, new Position(i, j));
|
Move move = new Move(pos, new Position(i, j));
|
||||||
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) {
|
if (
|
||||||
|
board.getDest(move) == null
|
||||||
|
|| board.getDest(move).getColor() != getColor()
|
||||||
|
) {
|
||||||
moves.add(move);
|
moves.add(move);
|
||||||
if (board.getDest(move) != null) break;
|
if (board.getDest(move) != null)
|
||||||
} else break;
|
break;
|
||||||
|
} else
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Diagonal moves to the upper right
|
// Diagonal moves to the upper right
|
||||||
for (int i = pos.x + 1, j = pos.y - 1; i < 8 && j >= 0; i++, j--) {
|
for (int i = pos.x + 1, j = pos.y - 1; i < 8 && j >= 0; i++, j--) {
|
||||||
Move move = new Move(pos, new Position(i, j));
|
Move move = new Move(pos, new Position(i, j));
|
||||||
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) {
|
if (
|
||||||
|
board.getDest(move) == null
|
||||||
|
|| board.getDest(move).getColor() != getColor()
|
||||||
|
) {
|
||||||
moves.add(move);
|
moves.add(move);
|
||||||
if (board.getDest(move) != null) break;
|
if (board.getDest(move) != null)
|
||||||
} else break;
|
break;
|
||||||
|
} else
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Diagonal moves to the upper left
|
// Diagonal moves to the upper left
|
||||||
for (int i = pos.x - 1, j = pos.y - 1; i >= 0 && j >= 0; i--, j--) {
|
for (int i = pos.x - 1, j = pos.y - 1; i >= 0 && j >= 0; i--, j--) {
|
||||||
Move move = new Move(pos, new Position(i, j));
|
Move move = new Move(pos, new Position(i, j));
|
||||||
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) {
|
if (
|
||||||
|
board.getDest(move) == null
|
||||||
|
|| board.getDest(move).getColor() != getColor()
|
||||||
|
) {
|
||||||
moves.add(move);
|
moves.add(move);
|
||||||
if (board.getDest(move) != null) break;
|
if (board.getDest(move) != null)
|
||||||
} else break;
|
break;
|
||||||
|
} else
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return moves;
|
return moves;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Type getType() { return Type.BISHOP; }
|
public int getValue() { return 30; }
|
||||||
}
|
}
|
461
src/main/java/dev/kske/chess/board/Board.java
Normal 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; }
|
||||||
|
}
|
14
src/main/java/dev/kske/chess/board/BoardState.java
Normal 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;
|
||||||
|
}
|
62
src/main/java/dev/kske/chess/board/Castling.java
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
57
src/main/java/dev/kske/chess/board/EnPassant.java
Normal 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; }
|
||||||
|
}
|
238
src/main/java/dev/kske/chess/board/FENString.java
Normal 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; }
|
||||||
|
}
|
106
src/main/java/dev/kske/chess/board/King.java
Normal 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; }
|
||||||
|
}
|
69
src/main/java/dev/kske/chess/board/Knight.java
Normal 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';
|
||||||
|
}
|
||||||
|
}
|
365
src/main/java/dev/kske/chess/board/Log.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
374
src/main/java/dev/kske/chess/board/Move.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
227
src/main/java/dev/kske/chess/board/MoveNode.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
131
src/main/java/dev/kske/chess/board/Pawn.java
Normal 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; }
|
||||||
|
}
|
133
src/main/java/dev/kske/chess/board/PawnPromotion.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
223
src/main/java/dev/kske/chess/board/Piece.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
85
src/main/java/dev/kske/chess/board/Position.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
145
src/main/java/dev/kske/chess/board/Queen.java
Normal 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; }
|
||||||
|
}
|
@ -7,10 +7,18 @@ import java.util.List;
|
|||||||
* Project: <strong>Chess</strong><br>
|
* Project: <strong>Chess</strong><br>
|
||||||
* File: <strong>Rook.java</strong><br>
|
* File: <strong>Rook.java</strong><br>
|
||||||
* Created: <strong>01.07.2019</strong><br>
|
* Created: <strong>01.07.2019</strong><br>
|
||||||
* Author: <strong>Kai S. K. Engelbart</strong>
|
*
|
||||||
|
* @since Chess v0.1-alpha
|
||||||
|
* @author Kai S. K. Engelbart
|
||||||
*/
|
*/
|
||||||
public class Rook extends Piece {
|
public class Rook extends Piece {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates rook {@link Piece}.
|
||||||
|
*
|
||||||
|
* @param color the color of this rook
|
||||||
|
* @param board the board on which this rook will be placed
|
||||||
|
*/
|
||||||
public Rook(Color color, Board board) {
|
public Rook(Color color, Board board) {
|
||||||
super(color, board);
|
super(color, board);
|
||||||
}
|
}
|
||||||
@ -20,18 +28,6 @@ public class Rook extends Piece {
|
|||||||
return (move.isHorizontal() || move.isVertical()) && isFreePath(move);
|
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
|
@Override
|
||||||
protected List<Move> getPseudolegalMoves(Position pos) {
|
protected List<Move> getPseudolegalMoves(Position pos) {
|
||||||
List<Move> moves = new ArrayList<>();
|
List<Move> moves = new ArrayList<>();
|
||||||
@ -39,41 +35,58 @@ public class Rook extends Piece {
|
|||||||
// Horizontal moves to the right
|
// Horizontal moves to the right
|
||||||
for (int i = pos.x + 1; i < 8; i++) {
|
for (int i = pos.x + 1; i < 8; i++) {
|
||||||
Move move = new Move(pos, new Position(i, pos.y));
|
Move move = new Move(pos, new Position(i, pos.y));
|
||||||
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) {
|
if (
|
||||||
|
board.getDest(move) == null
|
||||||
|
|| board.getDest(move).getColor() != getColor()
|
||||||
|
) {
|
||||||
moves.add(move);
|
moves.add(move);
|
||||||
if (board.getDest(move) != null) break;
|
if (board.getDest(move) != null)
|
||||||
} else break;
|
break;
|
||||||
|
} else
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Horizontal moves to the left
|
// Horizontal moves to the left
|
||||||
for (int i = pos.x - 1; i >= 0; i--) {
|
for (int i = pos.x - 1; i >= 0; i--) {
|
||||||
Move move = new Move(pos, new Position(i, pos.y));
|
Move move = new Move(pos, new Position(i, pos.y));
|
||||||
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) {
|
if (
|
||||||
|
board.getDest(move) == null
|
||||||
|
|| board.getDest(move).getColor() != getColor()
|
||||||
|
) {
|
||||||
moves.add(move);
|
moves.add(move);
|
||||||
if (board.getDest(move) != null) break;
|
if (board.getDest(move) != null)
|
||||||
} else break;
|
break;
|
||||||
|
} else
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vertical moves to the top
|
// Vertical moves to the top
|
||||||
for (int i = pos.y - 1; i >= 0; i--) {
|
for (int i = pos.y - 1; i >= 0; i--) {
|
||||||
Move move = new Move(pos, new Position(pos.x, i));
|
Move move = new Move(pos, new Position(pos.x, i));
|
||||||
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) {
|
if (
|
||||||
|
board.getDest(move) == null
|
||||||
|
|| board.getDest(move).getColor() != getColor()
|
||||||
|
) {
|
||||||
moves.add(move);
|
moves.add(move);
|
||||||
if (board.getDest(move) != null) break;
|
if (board.getDest(move) != null)
|
||||||
} else break;
|
break;
|
||||||
|
} else
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vertical moves to the bottom
|
// Vertical moves to the bottom
|
||||||
for (int i = pos.y + 1; i < 8; i++) {
|
for (int i = pos.y + 1; i < 8; i++) {
|
||||||
Move move = new Move(pos, new Position(pos.x, i));
|
Move move = new Move(pos, new Position(pos.x, i));
|
||||||
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) {
|
if (
|
||||||
|
board.getDest(move) == null
|
||||||
|
|| board.getDest(move).getColor() != getColor()
|
||||||
|
) {
|
||||||
moves.add(move);
|
moves.add(move);
|
||||||
if (board.getDest(move) != null) break;
|
if (board.getDest(move) != null)
|
||||||
} else break;
|
break;
|
||||||
|
} else
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return moves;
|
return moves;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Type getType() { return Type.ROOK; }
|
public int getValue() { return 50; }
|
||||||
}
|
}
|
31
src/main/java/dev/kske/chess/event/GameStartEvent.java
Normal 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; }
|
||||||
|
}
|
39
src/main/java/dev/kske/chess/event/MoveEvent.java
Normal 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; }
|
||||||
|
}
|
42
src/main/java/dev/kske/chess/exception/ChessException.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
199
src/main/java/dev/kske/chess/game/Game.java
Normal 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; }
|
||||||
|
}
|
151
src/main/java/dev/kske/chess/game/NaturalPlayer.java
Normal 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) {}
|
||||||
|
}
|
87
src/main/java/dev/kske/chess/game/Player.java
Normal 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; }
|
||||||
|
}
|
106
src/main/java/dev/kske/chess/game/UCIPlayer.java
Normal 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!");
|
||||||
|
}
|
||||||
|
}
|
116
src/main/java/dev/kske/chess/game/ai/AIPlayer.java
Normal 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() {}
|
||||||
|
}
|
283
src/main/java/dev/kske/chess/game/ai/MoveProcessor.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
43
src/main/java/dev/kske/chess/game/ai/ProcessingResult.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
141
src/main/java/dev/kske/chess/io/EngineUtil.java
Normal 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; }
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package dev.kske.chess.ui;
|
package dev.kske.chess.io;
|
||||||
|
|
||||||
import java.awt.Image;
|
import java.awt.Image;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
@ -15,15 +15,16 @@ import dev.kske.chess.board.Piece;
|
|||||||
* Project: <strong>Chess</strong><br>
|
* Project: <strong>Chess</strong><br>
|
||||||
* File: <strong>TextureUtil.java</strong><br>
|
* File: <strong>TextureUtil.java</strong><br>
|
||||||
* Created: <strong>01.07.2019</strong><br>
|
* Created: <strong>01.07.2019</strong><br>
|
||||||
* Author: <strong>Kai S. K. Engelbart</strong>
|
*
|
||||||
|
* @since Chess v0.1-alpha
|
||||||
|
* @author Kai S. K. Engelbart
|
||||||
*/
|
*/
|
||||||
public class TextureUtil {
|
public class TextureUtil {
|
||||||
|
|
||||||
private static Map<String, Image> textures, scaledTextures;
|
private static Map<String, Image> textures = new HashMap<>(),
|
||||||
|
scaledTextures = new HashMap<>();
|
||||||
|
|
||||||
static {
|
static {
|
||||||
textures = new HashMap<>();
|
|
||||||
scaledTextures = new HashMap<>();
|
|
||||||
loadPieceTextures();
|
loadPieceTextures();
|
||||||
scaledTextures.putAll(textures);
|
scaledTextures.putAll(textures);
|
||||||
}
|
}
|
||||||
@ -31,23 +32,27 @@ public class TextureUtil {
|
|||||||
private 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
|
* @param piece The piece from which the texture properties are taken
|
||||||
* @return The fitting texture
|
* @return The fitting texture
|
||||||
*/
|
*/
|
||||||
public static Image getPieceTexture(Piece piece) {
|
public static Image getPieceTexture(Piece piece) {
|
||||||
String key = piece.getType().toString().toLowerCase() + "_" + piece.getColor().toString().toLowerCase();
|
String key = piece.getClass().getSimpleName().toLowerCase() + "_"
|
||||||
|
+ piece.getColor().toString().toLowerCase();
|
||||||
return scaledTextures.get(key);
|
return scaledTextures.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scales all piece textures to fit the current tile size
|
* Scales all piece textures to fit the current tile size.
|
||||||
|
*
|
||||||
|
* @param tileSize the new width and height of the piece textures
|
||||||
*/
|
*/
|
||||||
public static void scalePieceTextures(int scale) {
|
public static void scalePieceTextures(int tileSize) {
|
||||||
scaledTextures.clear();
|
scaledTextures.clear();
|
||||||
textures
|
textures.forEach(
|
||||||
.forEach((key, img) -> scaledTextures.put(key, img.getScaledInstance(scale, scale, Image.SCALE_SMOOTH)));
|
(key, img) -> scaledTextures.put(key, img.getScaledInstance(tileSize, tileSize, Image.SCALE_SMOOTH))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -72,7 +77,8 @@ public class TextureUtil {
|
|||||||
*/
|
*/
|
||||||
private static void loadPieceTextures() {
|
private static void loadPieceTextures() {
|
||||||
Arrays
|
Arrays
|
||||||
.asList("king_white",
|
.asList(
|
||||||
|
"king_white",
|
||||||
"king_black",
|
"king_black",
|
||||||
"queen_white",
|
"queen_white",
|
||||||
"queen_black",
|
"queen_black",
|
||||||
@ -83,8 +89,10 @@ public class TextureUtil {
|
|||||||
"bishop_white",
|
"bishop_white",
|
||||||
"bishop_black",
|
"bishop_black",
|
||||||
"pawn_white",
|
"pawn_white",
|
||||||
"pawn_black")
|
"pawn_black"
|
||||||
.forEach(name -> textures.put(name, loadImage("/pieces/" + name + ".png")));
|
)
|
||||||
|
.forEach(
|
||||||
|
name -> textures.put(name, loadImage("/pieces/" + name + ".png"))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
57
src/main/java/dev/kske/chess/pgn/PGNDatabase.java
Normal 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; }
|
||||||
|
}
|
178
src/main/java/dev/kske/chess/pgn/PGNGame.java
Normal 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; }
|
||||||
|
}
|
254
src/main/java/dev/kske/chess/uci/UCIHandle.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
220
src/main/java/dev/kske/chess/uci/UCIInfo.java
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
97
src/main/java/dev/kske/chess/uci/UCIListener.java
Normal 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) {}
|
||||||
|
}
|
73
src/main/java/dev/kske/chess/uci/UCIOption.java
Normal 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
|
||||||
|
}
|
||||||
|
}
|
168
src/main/java/dev/kske/chess/uci/UCIReceiver.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -6,16 +6,19 @@ import java.awt.Graphics;
|
|||||||
import javax.swing.JComponent;
|
import javax.swing.JComponent;
|
||||||
|
|
||||||
import dev.kske.chess.board.Board;
|
import dev.kske.chess.board.Board;
|
||||||
|
import dev.kske.chess.io.TextureUtil;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Project: <strong>Chess</strong><br>
|
* Project: <strong>Chess</strong><br>
|
||||||
* File: <strong>BoardComponent.java</strong><br>
|
* File: <strong>BoardComponent.java</strong><br>
|
||||||
* Created: <strong>01.07.2019</strong><br>
|
* Created: <strong>01.07.2019</strong><br>
|
||||||
* Author: <strong>Kai S. K. Engelbart</strong><br>
|
|
||||||
* <br>
|
* <br>
|
||||||
* A square panel for rendering the chess board. To work correctly,
|
* A square panel for rendering the chess board. To work correctly,
|
||||||
* this must be added to a parent component that allows the child to decide the
|
* this must be added to a parent component that allows the child to decide the
|
||||||
* size.
|
* size.
|
||||||
|
*
|
||||||
|
* @since Chess v0.1-alpha
|
||||||
|
* @author Kai S. K. Engelbart
|
||||||
*/
|
*/
|
||||||
public class BoardComponent extends JComponent {
|
public class BoardComponent extends JComponent {
|
||||||
|
|
||||||
@ -25,6 +28,12 @@ public class BoardComponent extends JComponent {
|
|||||||
|
|
||||||
private Board board;
|
private Board board;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance of {@link BoardComponent}.
|
||||||
|
*
|
||||||
|
* @param boardPane the board pane inside which this board component is
|
||||||
|
* contained
|
||||||
|
*/
|
||||||
public BoardComponent(BoardPane boardPane) {
|
public BoardComponent(BoardPane boardPane) {
|
||||||
this.boardPane = boardPane;
|
this.boardPane = boardPane;
|
||||||
setSize(boardPane.getPreferredSize());
|
setSize(boardPane.getPreferredSize());
|
||||||
@ -34,27 +43,44 @@ public class BoardComponent extends JComponent {
|
|||||||
protected void paintComponent(Graphics g) {
|
protected void paintComponent(Graphics g) {
|
||||||
super.paintComponent(g);
|
super.paintComponent(g);
|
||||||
|
|
||||||
final int tileSize = getTileSize();
|
final int tileSize = boardPane.getTileSize();
|
||||||
|
|
||||||
// Draw the board
|
// Draw the board
|
||||||
g.setColor(Color.white);
|
g.setColor(Color.white);
|
||||||
for (int i = 0; i < 8; i++)
|
for (int i = 0; i < 8; i++)
|
||||||
for (int j = 0; j < 8; j++) {
|
for (int j = 0; j < 8; j++) {
|
||||||
if (j > 0) g.setColor(g.getColor().equals(Color.white) ? Color.lightGray : Color.white);
|
if (j > 0)
|
||||||
|
g.setColor(
|
||||||
|
g.getColor().equals(Color.white) ? Color.lightGray : Color.white
|
||||||
|
);
|
||||||
g.fillRect(tileSize * i, tileSize * j, tileSize, tileSize);
|
g.fillRect(tileSize * i, tileSize * j, tileSize, tileSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw the pieces if a board is present
|
// Draw the pieces if a board is present
|
||||||
if (board != null) for (int i = 0; i < 8; i++)
|
if (board != null)
|
||||||
|
for (int i = 0; i < 8; i++)
|
||||||
for (int j = 0; j < 8; j++)
|
for (int j = 0; j < 8; j++)
|
||||||
if (board.getBoardArr()[i][j] != null) g.drawImage(TextureUtil
|
if (board.getBoardArr()[i][j] != null)
|
||||||
.getPieceTexture(board.getBoardArr()[i][j]), i * tileSize, j * tileSize, this);
|
g.drawImage(
|
||||||
|
TextureUtil
|
||||||
|
.getPieceTexture(board.getBoardArr()[i][j]),
|
||||||
|
i * tileSize,
|
||||||
|
j * tileSize,
|
||||||
|
this
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getTileSize() { return boardPane.getTileSize(); }
|
/**
|
||||||
|
* @return the board rendered by this board component
|
||||||
|
*/
|
||||||
public Board getBoard() { return board; }
|
public Board getBoard() { return board; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the board rendered by this board component and repaints the
|
||||||
|
* component
|
||||||
|
*
|
||||||
|
* @param board the board rendered by this board component
|
||||||
|
*/
|
||||||
public void setBoard(Board board) {
|
public void setBoard(Board board) {
|
||||||
this.board = board;
|
this.board = board;
|
||||||
repaint();
|
repaint();
|
62
src/main/java/dev/kske/chess/ui/BoardPane.java
Normal 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; }
|
||||||
|
}
|
139
src/main/java/dev/kske/chess/ui/DialogUtil.java
Normal 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())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
51
src/main/java/dev/kske/chess/ui/GameDropTarget.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
230
src/main/java/dev/kske/chess/ui/GamePane.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
119
src/main/java/dev/kske/chess/ui/GameTabComponent.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
255
src/main/java/dev/kske/chess/ui/MainWindow.java
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
174
src/main/java/dev/kske/chess/ui/MenuBar.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
41
src/main/java/dev/kske/chess/ui/MoveNodeRenderer.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
173
src/main/java/dev/kske/chess/ui/OverlayComponent.java
Normal 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(); }
|
||||||
|
}
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 797 B After Width: | Height: | Size: 797 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 725 B After Width: | Height: | Size: 725 B |
Before Width: | Height: | Size: 933 B After Width: | Height: | Size: 933 B |
@ -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.*;
|
||||||
import static org.junit.Assert.assertNotSame;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import dev.kske.chess.board.Board;
|
|
||||||
import dev.kske.chess.board.Piece.Color;
|
import dev.kske.chess.board.Piece.Color;
|
||||||
import dev.kske.chess.board.Queen;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Project: <strong>Chess</strong><br>
|
* Project: <strong>Chess</strong><br>
|
||||||
@ -18,7 +15,7 @@ import dev.kske.chess.board.Queen;
|
|||||||
*/
|
*/
|
||||||
class BoardTest {
|
class BoardTest {
|
||||||
|
|
||||||
Board board;
|
private Board board;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws java.lang.Exception
|
* @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
|
@Test
|
||||||
void testClone() {
|
void testClone() {
|
||||||
Board clone = (Board) board.clone();
|
Board clone = new Board(board, false);
|
||||||
assertNotSame(clone, board);
|
assertNotSame(clone, board);
|
||||||
assertNotSame(clone.getBoardArr(), board.getBoardArr());
|
assertNotSame(clone.getBoardArr(), board.getBoardArr());
|
||||||
|
|
||||||
clone.getBoardArr()[0][0] = new Queen(Color.BLACK, clone);
|
clone.getBoardArr()[0][0] = new Queen(Color.BLACK, clone);
|
||||||
|
clone.move(new Move(1, 1, 1, 2));
|
||||||
assertNotEquals(clone.getBoardArr()[0][0], board.getBoardArr()[0][0]);
|
assertNotEquals(clone.getBoardArr()[0][0], board.getBoardArr()[0][0]);
|
||||||
|
assertNotEquals(
|
||||||
|
clone.getLog().getActiveColor(),
|
||||||
|
board.getLog().getActiveColor()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
87
src/test/java/dev/kske/chess/board/FENStringTest.java
Normal 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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
165
src/test/java/dev/kske/chess/board/LogTest.java
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
56
src/test/java/dev/kske/chess/board/PositionTest.java
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
33
src/test/java/dev/kske/chess/pgn/PGNDatabaseTest.java
Normal 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())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
18
src/test/resources/test.pgn
Normal 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
|