113 Commits

Author SHA1 Message Date
95773c49ba Merge pull request #16 from CyB3RC0nN0R/f/pgn_save
Implemented game serialization to the PGN format
2019-12-11 07:53:46 +01:00
3bff5aa56e Fixed setter parameter naming for fullmove number in Log 2019-12-11 07:53:44 +01:00
0487b47691 Fixed missing end of line chunk in movetext 2019-12-11 07:45:39 +01:00
39b7ce20b1 Fixed fullmove counters in PGN export 2019-12-10 21:27:06 +01:00
91512b7774 Including fullmove counters in PGN export 2019-12-08 22:15:29 +01:00
44f91591b4 Implemented saving to PGN file
+ copyVariations parameter in copy constructors of Board and Log

This procedure still required work in the form of efficiently rewinding
the board to the first position for SAN move extraction and shortening
SAN moves to the smallest possible representation.
2019-12-06 23:54:11 +01:00
6af213ed4f Working on SAN serialization of moves
+ SAN generation for castling moves
+ Appending promotion piece symbol in pawn promotion SAN

+ Asking the user to view a generated PGN file
2019-11-24 21:31:32 +01:00
fbf66e6ec1 Created Move#fromSAN, moved implementation from Board 2019-11-19 06:30:53 +01:00
6360a8a4ba Added a PGN file saving routine
+ showFileSaveDialog in DialogUtil
+ Javadoc in Log and PGNDatabase
+ "Save game file" button in MenuBar
+ saveFile method in MainWindow
+ Formatting and result tag in PGNGame tag pair serialization
2019-11-08 15:22:12 +01:00
78bac2e913 Added empty save methods to PGNDatabase and PGNGame 2019-11-07 05:53:28 +01:00
44f6a4b9b8 Fixed color switching button
* Renamed RestartEvent to GameStartEvent
* Moved event trigger to Game
* Renamed GameState to BoardState
* Added BoardState instance to MoveEvent
* Disabling the color switching button when a checkmate or stalemate is
reached

Fixes #1
2019-11-06 19:52:52 +01:00
6bbef7deee Using reflection in FENString, fixed input field in MainWindow 2019-11-05 05:51:41 +01:00
a98ca5e350 Merge pull request #11 from CyB3RC0nN0R/f/pawn_promotion
Added pawn promotion
2019-11-05 05:44:11 +01:00
15cdf00eb1 Added pawn promotion selection
* Letting a NaturalPlayer select the promotion piece with a combo box
* Optimized reflection use in PawnPromotion
* Changed toString method of Move to use LAN

Closes #9
2019-11-05 05:41:26 +01:00
37c5f2bd23 Added pawn promotion support to LAN 2019-11-04 18:11:23 +01:00
02c5e33f10 Removed Type enumeration from Piece class 2019-11-04 05:51:11 +01:00
28939f0471 Implemented proper pawn promotion
* Moved move execution and reversion to the Move class
* Removed Move.Type enumeration
* Added Move subclasses Castling, EnPassant and PawnPromotion
* Generating all four possible pawn promotions in the Pawn class
* Temporarily removed special move support from NaturalPlayer
2019-11-03 15:46:08 +01:00
b5c30d59af Created getters for all fields in Move 2019-10-31 19:09:43 +01:00
2e672841cd Moved board evaluation logic to MoveProcessor 2019-10-30 17:11:57 +01:00
1e44234f7e Added RestartEvent to clear move list after game restart 2019-10-30 06:12:31 +01:00
792f93fc3f Fully implemented castling conditions
Closes #8
2019-10-29 06:24:44 +01:00
27f37a8cf0 Implemented 'go' command in UCI protocol
Closes #5
2019-10-28 18:54:00 +01:00
4d943c1a19 Implemented 'position moves' command in UCI protocol 2019-10-28 18:31:18 +01:00
1d78b8f071 Implemented 'currline' command in UCI protocol 2019-10-28 18:24:26 +01:00
a41a2819da Changed class Javadoc to use @author and @since tags 2019-10-26 07:55:21 +02:00
40d80fdc12 Merge pull request #7 from CyB3RC0nN0R/feature/io
Refined IO functionality, fixed FEN string serialization and deserialization
2019-10-25 17:01:55 +02:00
cbf6aa2013 Moved loadFile method to MainWindow, removed redundancies 2019-10-25 16:58:18 +02:00
5dbd38d1c0 Removed old FEN string methods, fixed FEN regex 2019-10-25 11:52:48 +02:00
4857b48e4e Moved castling right logging to Log
* Removed move counter from Piece
* Added castling right array to MoveNode and Log
* Removed castling right map from Board
* Added castling right serialization and deserialization to FENString
* Modified LogTest
2019-10-25 11:34:07 +02:00
c438dd00cb Enhanced FENString class, added unit test and Board#equals() 2019-10-24 19:54:59 +02:00
7e8f75a008 Added FENString class 2019-10-24 06:09:16 +02:00
db8fe1c4c0 Renamed FEN string fullmove counter to fullmove number 2019-10-22 21:25:06 +02:00
acb0e63c82 Added PGNDatabaseTest and test file 2019-10-21 21:45:38 +02:00
2f1ae6e9c8 Created io package, moved IO-related classes 2019-10-20 17:27:52 +02:00
1f2cedd455 Replaced game configuration dialog by a JOptionPane 2019-10-20 11:51:56 +02:00
1d19f17c56 Improved PGN parsing, added PGN file loading 2019-10-20 11:22:58 +02:00
86cf2afc8f Implemented game loading from FEN file 2019-10-18 17:45:13 +02:00
8b9793611a Added file chooser dialog, renamed GameConfigurationDialog to DialogUtil 2019-10-14 06:31:03 +02:00
446f895ae1 Replaced LogPanel by a JList inside GamePane with a custom cell renderer 2019-10-13 21:34:22 +02:00
9839d5a23e Fixed texture scaling method in TextureUtil 2019-10-13 14:37:35 +02:00
713c95338e Added PGNDatabase and PGNGame classes with support for PGN parsing 2019-10-09 21:03:39 +02:00
84b3e1503f Renamed SAN-like coordinate notation to LAN, added SAN support to Board 2019-10-09 21:02:22 +02:00
cc0440233b Added ChessException class 2019-10-04 18:18:00 +02:00
286ea93ee3 Moved MoveNode into a separate file 2019-10-04 10:25:03 +02:00
994cb84729 Fixed LogPanel to support new log data model 2019-09-23 17:37:42 +02:00
54e4a0e2e4 Fixed memory leak, improved copy constructors 2019-09-19 21:31:24 +02:00
1ecafa5485 Fixed Log with respect to variations 2019-09-18 15:00:31 +02:00
c987bfcebb Added variations in Log, added LogTest 2019-09-13 18:13:34 +02:00
3941a75c91 Changed unit test package structure
- Changed unit test package structure to match src
- Refactored PositionTest
+ TestToString method in PositionTest
2019-09-10 21:15:31 +02:00
249480724a Refactoring
- Simplified FEN-string generation
- Made GameConfigurationDialog a utility class
2019-09-09 19:05:57 +02:00
216877b76b Added LogPanel to GamePane 2019-09-09 06:14:37 +02:00
19712a2bb7 Refactoring and documentation, improved FEN integration
+ Adding a new game when loading a FEN string
+ Loading multiple FEN files with drag and drop
2019-09-08 21:37:47 +02:00
08ac0ac114 Fixed frozen game after adding a new one 2019-09-06 13:24:58 +02:00
de9cd05602 Opening the new tab after starting a game 2019-09-01 18:57:31 +02:00
3b73cd1efb Added creation of new game tabs
- Switched to GridBagLayout in GamePane
- New games created in MenuBar are appended to the tabbed pane in
MainWindow
+ LogPanel class for displaying the game log inside a GamePane
- Removed LogFrame in favor of LogPanel
2019-09-01 12:45:06 +02:00
964de02e24 Associated DropTarget with MainWindow
When a FEN file is dropped into the main window, the drop target
automatically loads it into the currently visible (=selected) game.
2019-08-24 16:04:09 +02:00
76fa3859ef Added game tab support
+ GamePane component with the game displaying functionality from
MainWindow
- Simplified MainWindow, added JTabbedPane with GamePane elements
- Adjusted FENDropTarget and MenuBar
2019-08-23 22:00:30 +02:00
c1a8589a04 Fixed documentation 2019-08-23 21:10:19 +02:00
358654b1ed Added drag and drop support for FEN files 2019-08-14 20:17:28 +02:00
8e2af63c35 Improved documentation in Board 2019-08-13 06:16:10 +02:00
642a0bf4d1 Fixed Game and Log synchronization on FEN loading 2019-08-13 05:59:47 +02:00
3ea48ef21b Implemented loadFromFen method in Board 2019-08-13 05:43:26 +02:00
d121e85897 Cleaned up and improved Log
+ Current log state properties
- Removed Log delegates from Board
2019-08-12 06:44:55 +02:00
14c7167ce4 Changed event model
+ MoveEvent
2019-08-07 18:54:00 +02:00
90c100e0e1 Working on board loading from FEN-encoded string 2019-08-05 21:02:54 +02:00
e7af9f40c2 Implemented LogFrame updating
+ Export to SAN in move
+ Updating LogFrame after a move
- Turned EventBus into a singleton
2019-08-04 14:40:25 +02:00
3d8877ddbd Implemented LogFrame, added menu item for opening it 2019-08-03 21:48:10 +02:00
83c6e48f03 Added event package with EventBus class 2019-08-02 18:46:00 +02:00
1ce8b8355a Fixed and fully implemented UCI 'info' command parsing
- Except for 'currline' which is a feature requested by the GUI and thus
optional
2019-08-01 21:34:01 +02:00
36597ac6f1 Fixed parsing score and after score 2019-08-01 19:01:17 +02:00
e72297bebf UCI refactoring
+ Multiple listener support in UCIHandle
+ UCIInfo class
- Moved info and option parsing into the respective classes
- Removed unimplemented UCI callback methods from UCIPlayer
2019-07-31 17:47:49 +02:00
545f946aa0 Simplified EngineUtil and MenuBar 2019-07-30 06:18:24 +02:00
984bedfafe Implemented en passant 2019-07-28 13:51:10 +02:00
cac235a0db Fixed letter alignment below the board 2019-07-28 09:43:53 +02:00
1f5242935f Added board coordinates 2019-07-27 09:34:40 +02:00
51558797cc Added border around board, changed display order in OverlayComponent 2019-07-27 08:06:43 +02:00
ae38e67a90 Added naming support in Player and subclasses 2019-07-26 16:14:22 +02:00
5abc51688b Added log resetting, disabled resizing in MainFrame 2019-07-25 21:38:49 +02:00
8bcd89d975 Simplified game creation, added new configuration dialog 2019-07-25 07:21:07 +02:00
4c0432ca30 Fixed engine menu reloading on engine addition 2019-07-24 19:07:22 +02:00
36832733b6 Added en passant availability logging and FEN string export 2019-07-24 17:52:42 +02:00
601104485c Fixed FEN string export when board is in start position 2019-07-24 15:58:23 +02:00
2da185a8fb Added tools menu in MenuBar with FEN export menu item 2019-07-24 07:41:45 +02:00
0ed80228fe Moved activeColor, fullmoveCounter and halfmoveClock to Log 2019-07-24 07:32:59 +02:00
e353aef867 Added engine info serialization and integration into MenuBar 2019-07-23 16:28:53 +02:00
b25acff367 Working on external engine integration, added extra menu
+ EngineUtil for storing engine information
- Changed all UCIListener methods to default
2019-07-23 11:54:43 +02:00
184c96db8c Added checkmate and stalemate notification through dialog, changed icon 2019-07-23 11:02:34 +02:00
309495cfae Improved BoardOverlay, disabled color swap in natural-vs-natural game 2019-07-23 10:38:19 +02:00
91962c01e0 Added dynamic color swap button text 2019-07-23 09:59:22 +02:00
b3710a878f Implemented color swapping
+ swapColor method in Board
+ Button for swapping colors in MainWindow
2019-07-23 09:31:20 +02:00
68d1996bd6 Fixed castling, added castling export to FEN
+ isFreePath implementation in Piece
- Removed isFreePath from Bishop, Rook, Queen and King
+ canCastleKingside and canCastleQueenside methods in King
+ Castling rights record in Board + FEN export

+ equals method in Position
+ UCI 'position startpos' command
- Switched to Java 8 compliance for compatibility reasons
2019-07-22 21:40:25 +02:00
efe7ab2b60 First working UCI implementation
+ bestmove, position and go command implementations
+ Move initialization from algebraic notation
+ FEN string generation
2019-07-22 14:51:24 +02:00
a68a87962c Implemented option setting, added UCIOption class 2019-07-22 08:59:13 +02:00
ab54f88a89 Fixed UCI option parsing 2019-07-22 07:29:58 +02:00
b5b7a749d6 Implemented UCI handshake with engine
+ UCI game start in MenuBar
+ UCI game creation method in Game
- Fixed double game instance bug after starting a new game
+ Name and author parsing in UCIReceiver
2019-07-21 14:35:14 +02:00
709383e758 Fixed UCI combo GUI type to support multiple predefined values 2019-07-20 06:48:42 +02:00
347eb5d531 Added UCIListener, started working on an implementation 2019-07-20 06:36:56 +02:00
062a5c3075 Added UCIReceiver and UCIListner, implemented a part of the UCI protocol 2019-07-19 22:16:02 +02:00
29e17d90a5 Working on UCI support
+ UCIHandle class for communicating with an engine through UCI
+ UCIPlayer class for using an engine within the gui
2019-07-19 08:34:31 +02:00
d8f5f3bbf4 Fixed input listening bug in NaturalPlayer
+ disconnect methods in Game and Player
+ NaturalPlayer removes its MouseListener from OverlayComponent after
the disconnect method is called
2019-07-18 15:01:15 +02:00
4dcc9f7ca0 Set white king as MainWindow icon 2019-07-18 11:19:58 +02:00
cfd71af142 Moved game and board creation to Game 2019-07-17 08:26:51 +02:00
fcd8bfb26b Fixed game state related bugs 2019-07-16 18:24:48 +02:00
cde7f63996 Fixed UI bugs, added move drawing to OverlayComponent 2019-07-16 15:32:02 +02:00
8ea0c7a603 Added alpha-beta pruning threshold to the AI and a configuration dialog 2019-07-16 14:42:10 +02:00
8eda941284 Moved tests in test source folder, replaced GameModeDialog by MenuBar 2019-07-16 11:58:51 +02:00
7a986ab9c4 Added resource folder to class path, implemented proper texture scaling 2019-07-15 18:16:45 +02:00
c245cdb640 Implemented game restarting
+ Restarting method in Game
+ Abstract cancelMove method in Player
+ Stopping calculations in AIPlayer when the game has been restarted
2019-07-14 12:03:45 +02:00
58340ca6ac Added castling, fixed some minor bugs 2019-07-13 11:38:44 +02:00
199d2f06c6 Made application terminate when GameModeDialog is closed 2019-07-12 13:33:34 +02:00
d12b06a1ff Fixed knight move validation, renamed test 2019-07-12 10:07:02 +02:00
6d98d9a963 Added positional board evaluation 2019-07-11 19:57:54 +02:00
c3a787c3a7 Added move history and pawn promotion
+ Log class for move history
+ LoggedMove class with piece captured by the logged move
- Made move reversion easier
+ MoveType for recognizing special moves
+ MoveType determination during move generation and validation
2019-07-10 18:54:53 +02:00
59 changed files with 4153 additions and 538 deletions

View File

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

4
.gitignore vendored
View File

@ -1,5 +1,6 @@
.metadata
bin/
/bin_test/
tmp/
*.tmp
*.bak
@ -20,4 +21,5 @@ local.properties
.recommenders/
# Annotation Processing
.apt_generated/
.apt_generated/
/engine_infos.ser

View File

@ -7,7 +7,9 @@ import java.util.List;
* Project: <strong>Chess</strong><br>
* File: <strong>Bishop.java</strong><br>
* Created: <strong>01.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class Bishop extends Piece {
@ -20,14 +22,6 @@ public class Bishop extends Piece {
return move.isDiagonal() && isFreePath(move);
}
@Override
protected boolean isFreePath(Move move) {
for (int i = move.pos.x + move.xSign, j = move.pos.y
+ move.ySign; i != move.dest.x; i += move.xSign, j += move.ySign)
if (board.getBoardArr()[i][j] != null) return false;
return checkDestination(move);
}
@Override
protected List<Move> getPseudolegalMoves(Position pos) {
List<Move> moves = new ArrayList<>();
@ -71,5 +65,5 @@ public class Bishop extends Piece {
}
@Override
public Type getType() { return Type.BISHOP; }
public int getValue() { return 30; }
}

View File

@ -1,33 +1,60 @@
package dev.kske.chess.board;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.board.Piece.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>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class Board implements Cloneable {
public class Board {
private Piece[][] boardArr;
private Map<Color, Position> kingPos;
private Piece[][] boardArr = new Piece[8][8];
private Map<Color, Position> kingPos = new HashMap<>();
private Log log = new Log();
public Board() {
boardArr = new Piece[8][8];
kingPos = new HashMap<>();
initializeDefaultPositions();
/**
* 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, but does not contain any move history
* apart from the current {@link MoveNode}.
*
* @param other The {@link Board} instance to copy
* @param copyVariations TODO
*/
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
*/
@ -36,11 +63,11 @@ public class Board implements Cloneable {
if (piece == null || !piece.isValidMove(move)) return false;
else {
// Move piece
Piece capturePiece = move(move);
move(move);
// Revert move if it caused a check for its team
if (checkCheck(piece.getColor())) {
revert(move, capturePiece);
revert();
return false;
}
@ -50,40 +77,52 @@ public class Board implements Cloneable {
/**
* 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 Piece move(Move move) {
public void move(Move move) {
Piece piece = getPos(move);
Piece capturePiece = getDest(move);
setDest(move, piece);
setPos(move, null);
// Execute the move
move.execute(this);
// Update the king's position if the moved piece is the king
if (piece.getType() == Type.KING) kingPos.put(piece.getColor(), move.dest);
if (piece instanceof King) kingPos.put(piece.getColor(), move.getDest());
return capturePiece;
// Update log
log.add(move, piece, capturePiece);
}
/**
* Reverts a move.
*
* @param move The move to revert
* @param capturedPiece The piece that has been captured when the move has been
* applied
* 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 revert(Move move, Piece capturedPiece) {
setPos(move, getDest(move));
setDest(move, capturedPiece);
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).getType() == Type.KING) kingPos.put(getPos(move).getColor(), move.pos);
if (getPos(move) instanceof King) kingPos.put(getPos(move).getColor(), move.getPos());
// 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
*/
@ -91,28 +130,32 @@ public class Board implements Cloneable {
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)));
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);
}
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) {
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, kingPos.get(color))))
return true;
if (get(pos) != null && get(pos).getColor() == color && get(pos).isValidMove(new Move(pos, dest))) return true;
}
return false;
}
@ -120,7 +163,7 @@ public class Board implements Cloneable {
/**
* 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
*/
@ -129,55 +172,24 @@ public class Board implements Cloneable {
if (!getMoves(kingPos.get(color)).isEmpty()) return false;
else {
for (Move move : getMoves(color)) {
Piece capturePiece = move(move);
boolean check = checkCheck(color);
revert(move, capturePiece);
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 += 8;
break;
case ROOK:
score += 5;
break;
case KNIGHT:
score += 3;
break;
case BISHOP:
score += 3;
break;
case PAWN:
score += 1;
break;
}
return score;
public BoardState getGameEventType(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 initializeDefaultPositions() {
public void initDefaultPositions() {
// Initialize pawns
for (int i = 0; i < 8; i++) {
boardArr[i][1] = new Pawn(Color.BLACK, this);
@ -218,60 +230,125 @@ public class Board implements Cloneable {
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);
}
/**
* @return A new instance of this class with a shallow copy of both
* {@code kingPos} and {code boardArr}
* @param pos The position from which to return a piece
* @return The piece at the position
*/
@Override
public Object clone() {
Board board = null;
try {
board = (Board) super.clone();
} catch (CloneNotSupportedException ex) {
ex.printStackTrace();
}
board.boardArr = new Piece[8][8];
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++)
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);
return board;
if (boardArr[x][i] != null && boardArr[x][i].getClass() == pieceClass && boardArr[x][i].getColor() == log.getActiveColor()) return 8 - i;
return -1;
}
public Piece get(Position pos) {
return boardArr[pos.x][pos.y];
/**
* 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 '-';
}
public void set(Position pos, Piece piece) {
boardArr[pos.x][pos.y] = piece;
/**
* 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;
}
public Piece getPos(Move move) {
return get(move.pos);
}
/**
* 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; }
public Piece getDest(Move move) {
return get(move.dest);
}
/**
* @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()); }
public void setPos(Move move, Piece piece) {
set(move.pos, piece);
}
/**
* @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()); }
public void setDest(Move move, Piece piece) {
set(move.dest, piece);
}
/**
* Places a piece at the position of a move.
*
* @param move The move at which position to place the piece
* @param piece The piece to place
*/
public void setPos(Move move, Piece piece) { set(move.getPos(), piece); }
/**
* Places a piece at the destination of a move.
*
* @param move The move at which destination to place the piece
* @param piece The piece to place
*/
public void setDest(Move move, Piece piece) { set(move.getDest(), piece); }
/**
* @return The board array
*/
public Piece[][] getBoardArr() { return boardArr; }
/**
* @return The move log
*/
public Log getLog() { return log; }
}

View File

@ -2,10 +2,12 @@ package dev.kske.chess.board;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>GameState.java</strong><br>
* File: <strong>BoardState.java</strong><br>
* Created: <strong>07.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public enum GameState {
public enum BoardState {
CHECK, CHECKMATE, STALEMATE, NORMAL;
}

View File

@ -0,0 +1,44 @@
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;
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);
}
public Castling(int xPos, int yPos, int xDest, int yDest) { this(new Position(xPos, yPos), new Position(xDest, yDest)); }
@Override
public void execute(Board board) {
// Move the king and the rook
super.execute(board);
rookMove.execute(board);
}
@Override
public void revert(Board board, Piece capturedPiece) {
// Move the king and the rook
super.revert(board, capturedPiece);
rookMove.revert(board, null);
}
/**
* @return {@code O-O-O} for a queenside castling or {@code O-O} for a kingside
* castling
*/
@Override
public String toSAN(Board board) {
return rookMove.pos.x == 0 ? "O-O-O" : "O-O";
}
}

View File

@ -0,0 +1,35 @@
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;
public EnPassant(Position pos, Position dest) {
super(pos, dest);
capturePos = new Position(dest.x, dest.y - ySign);
}
public EnPassant(int xPos, int yPos, int xDest, int yDest) { this(new Position(xPos, yPos), new Position(xDest, yDest)); }
@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));
}
public Position getCapturePos() { return capturePos; }
}

View File

@ -0,0 +1,212 @@
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
*/
public FENString(String fen) throws ChessException {
// Check fen string against regex
Pattern fenPattern = Pattern.compile(
"^(?<piecePlacement>(?:[1-8nbrqkpNBRQKP]{1,8}\\/){7}[1-8nbrqkpNBRQKP]{1,8}) (?<activeColor>[wb]) (?<castlingAvailability>-|[KQkq]{1,4}) (?<enPassantTargetSquare>-|[a-h][1-8]) (?<halfmoveClock>\\d+) (?<fullmoveNumber>\\d+)$");
Matcher matcher = fenPattern.matcher(fen);
if (!matcher.find()) throw new ChessException("FEN string does not match pattern " + fenPattern.pattern());
// Initialize data fields
piecePlacement = matcher.group("piecePlacement");
activeColor = Color.fromFirstChar(matcher.group("activeColor").charAt(0));
castlingAvailability = matcher.group("castlingAvailability");
if (!matcher.group("enPassantTargetSquare").equals("-")) enPassantTargetSquare = Position.fromLAN(matcher.group("enPassantTargetSquare"));
halfmoveClock = Integer.parseInt(matcher.group("halfmoveClock"));
fullmoveNumber = Integer.parseInt(matcher.group("fullmoveNumber"));
// Initialize and clean board
board = new Board();
for (int i = 0; i < 8; i++)
for (int j = 0; j < 8; j++)
board.getBoardArr()[i][j] = null;
// Parse individual fields
// Piece placement
final String[] rows = piecePlacement.split("/");
if (rows.length != 8) throw new ChessException("FEN string contains invalid piece placement");
for (int i = 0; i < 8; i++) {
final char[] cols = rows[i].toCharArray();
int j = 0;
for (char c : cols) {
// Empty space
if (Character.isDigit(c)) {
j += Character.getNumericValue(c);
} else {
Color color = Character.isUpperCase(c) ? Color.WHITE : Color.BLACK;
try {
Constructor<? extends Piece> pieceConstructor = Piece.fromFirstChar(c).getDeclaredConstructor(Color.class, Board.class);
pieceConstructor.setAccessible(true);
board.getBoardArr()[j][i] = pieceConstructor.newInstance(color, board);
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException
| NoSuchMethodException | SecurityException e) {
e.printStackTrace();
}
++j;
}
}
}
// Active color
board.getLog().setActiveColor(activeColor);
// Castling availability
boolean castlingRights[] = new boolean[4];
for (char c : castlingAvailability.toCharArray())
switch (c) {
case 'K':
castlingRights[MoveNode.WHITE_KINGSIDE] = true;
break;
case 'Q':
castlingRights[MoveNode.WHITE_QUEENSIDE] = true;
break;
case 'k':
castlingRights[MoveNode.BLACK_KINGSIDE] = true;
break;
case 'q':
castlingRights[MoveNode.BLACK_QUEENSIDE] = true;
break;
}
board.getLog().setCastlingRights(castlingRights);
// En passant square
board.getLog().setEnPassant(enPassantTargetSquare);
// Halfmove clock
board.getLog().setHalfmoveClock(halfmoveClock);
// Fullmove number
board.getLog().setFullmoveNumber(fullmoveNumber);
}
/**
* Constructs a {@link FENString} form a {@link Board} object.
*
* @param board the {@link Board} object to encode in this {@link FENString}
*/
public FENString(Board board) {
this.board = board;
// Serialize individual fields
// Piece placement
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 8; i++) {
int empty = 0;
for (int j = 0; j < 8; j++) {
final Piece piece = board.getBoardArr()[j][i];
if (piece == null) ++empty;
else {
// Write empty field count
if (empty > 0) {
sb.append(empty);
empty = 0;
}
// Write piece character
char p = piece.firstChar();
sb.append(piece.getColor() == Color.WHITE ? Character.toUpperCase(p) : p);
}
}
// Write empty field count
if (empty > 0) {
sb.append(empty);
empty = 0;
}
if (i < 7) sb.append('/');
}
piecePlacement = sb.toString();
// Active color
activeColor = board.getLog().getActiveColor();
// Castling availability
castlingAvailability = "";
final char castlingRightsChars[] = new char[] { 'K', 'Q', 'k', 'q' };
for (int i = 0; i < 4; i++)
if (board.getLog().getCastlingRights()[i]) castlingAvailability += castlingRightsChars[i];
if (castlingAvailability.isEmpty()) castlingAvailability = "-";
// En passant availability
enPassantTargetSquare = board.getLog().getEnPassant();
// Halfmove clock
halfmoveClock = board.getLog().getHalfmoveClock();
// Fullmove counter
fullmoveNumber = board.getLog().getFullmoveNumber();
}
/**
* Exports this {@link FENString} object to a FEN string.
*
* @return a FEN string representing the board
*/
@Override
public String toString() {
return String.format("%s %c %s %s %d %d",
piecePlacement,
activeColor.firstChar(),
castlingAvailability,
enPassantTargetSquare == null ? "-" : enPassantTargetSquare.toLAN(),
halfmoveClock,
fullmoveNumber);
}
/**
* @return a {@link Board} object corresponding to this {@link FENString}
*/
public Board getBoard() { return board; }
}

View File

@ -7,17 +7,19 @@ 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>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class King extends Piece {
public King(Color color, Board board) {
super(color, board);
}
public King(Color color, Board board) { super(color, board); }
@Override
public boolean isValidMove(Move move) {
return move.xDist <= 1 && move.yDist <= 1 && isFreePath(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
@ -29,9 +31,42 @@ public class King extends Piece {
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);
} else 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);
} else 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 Type getType() { return Type.KING; }
}
public int getValue() { return 0; }
}

View File

@ -7,7 +7,9 @@ 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>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class Knight extends Piece {
@ -17,7 +19,8 @@ public class Knight extends Piece {
@Override
public boolean isValidMove(Move move) {
return Math.abs(move.xDist - move.yDist) == 1 && (move.xDist == 1 || move.yDist == 1) && isFreePath(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) {
@ -42,5 +45,8 @@ public class Knight extends Piece {
}
@Override
public Type getType() { return Type.KNIGHT; }
public int getValue() { return 35; }
@Override
public char firstChar() { return 'n'; }
}

View File

@ -0,0 +1,252 @@
package dev.kske.chess.board;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Objects;
import dev.kske.chess.board.Piece.Color;
/**
* 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;
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; }
public boolean[] getCastlingRights() { return castlingRights; }
public void setCastlingRights(boolean[] castlingRights) { this.castlingRights = castlingRights; }
public Position getEnPassant() { return enPassant; }
public void setEnPassant(Position enPassant) { this.enPassant = enPassant; }
public Color getActiveColor() { return activeColor; }
public void setActiveColor(Color activeColor) { this.activeColor = activeColor; }
public int getFullmoveNumber() { return fullmoveNumber; }
public void setFullmoveNumber(int fullmoveNumber) { this.fullmoveNumber = fullmoveNumber; }
public int getHalfmoveClock() { return halfmoveClock; }
public void setHalfmoveClock(int halfmoveClock) { this.halfmoveClock = halfmoveClock; }
}

View File

@ -1,15 +1,25 @@
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>
* Author: <strong>Kai S. K. Engelbart</strong>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class Move {
public final Position pos, dest;
public final int xDist, yDist, xSign, ySign;
protected final Position pos, dest;
protected final int xDist, yDist, xSign, ySign;
public Move(Position pos, Position dest) {
this.pos = pos;
@ -20,18 +30,176 @@ public class Move {
ySign = (int) Math.signum(dest.y - pos.y);
}
public Move(int xPos, int yPos, int xDest, int yDest) {
this(new Position(xPos, yPos), new Position(xDest, yDest));
public Move(int xPos, int yPos, int xDest, int yDest) { this(new Position(xPos, yPos), new Position(xDest, yDest)); }
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);
}
public boolean isHorizontal() { return yDist == 0; }
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);
}
public boolean isVertical() { return xDist == 0; }
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;
}
} else return new Move(pos, dest);
}
public boolean isDiagonal() { return xDist == yDist; }
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;
}
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();
}
public boolean isHorizontal() { return getyDist() == 0; }
public boolean isVertical() { return getxDist() == 0; }
public boolean isDiagonal() { return getxDist() == getyDist(); }
@Override
public String toString() {
return String.format("%s -> %s", pos, dest);
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();
}
public Position getPos() { return pos; }
public Position getDest() { return dest; }
public int getxDist() { return xDist; }
public int getyDist() { return yDist; }
public int getxSign() { return xSign; }
public int getySign() { return ySign; }
}

View File

@ -0,0 +1,126 @@
package dev.kske.chess.board;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
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 {
public static final int WHITE_KINGSIDE = 0, WHITE_QUEENSIDE = 1, BLACK_KINGSIDE = 2, BLACK_QUEENSIDE = 3;
public final Move move;
public final Piece capturedPiece;
public final boolean[] castlingRights;
public final Position enPassant;
public final Color activeColor;
public final int fullmoveCounter, 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 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
* @param halfmoveClock
*/
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; }
public boolean hasVariations() {
return variations != null && variations.size() > 0;
}
public MoveNode getParent() { return parent; }
public void setParent(MoveNode parent) { this.parent = parent; }
public boolean hasParent() {
return parent != null;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + Arrays.hashCode(castlingRights);
result = prime * result
+ Objects.hash(activeColor, capturedPiece, enPassant, fullmoveCounter, halfmoveClock, move);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
MoveNode other = (MoveNode) obj;
return activeColor == other.activeColor && Objects.equals(capturedPiece, other.capturedPiece)
&& Arrays.equals(castlingRights, other.castlingRights) && Objects.equals(enPassant, other.enPassant)
&& fullmoveCounter == other.fullmoveCounter && halfmoveClock == other.halfmoveClock
&& Objects.equals(move, other.move);
}
}

View File

@ -7,70 +7,80 @@ 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>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class Pawn extends Piece {
public Pawn(Color color, Board board) {
super(color, board);
}
public Pawn(Color color, Board board) { super(color, board); }
@Override
public boolean isValidMove(Move move) {
// TODO: en passant, pawn promotion
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;
return (step ^ doubleStep ^ strafe) && move.ySign == (getColor() == Color.WHITE ? -1 : 1) && isFreePath(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.yDist == 2)
return board.getBoardArr()[move.pos.x][move.dest.y - move.ySign] == null && board.getDest(move) == null;
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.xDist == 0) return board.getDest(move) == null;
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;
if (sign == -1 && pos.y == 1 || sign == 1 && pos.y == 7) return moves;
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) {
Move move = new Move(pos, new Position(pos.x - 1, pos.y + sign));
if (isFreePath(move)) moves.add(move);
}
if (pos.x > 0) addMoveIfValid(moves, pos, new Position(pos.x - 1, pos.y + sign), pawnPromotion);
// 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);
}
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) {
Move move = new Move(pos, new Position(pos.x, pos.y + sign));
if (isFreePath(move)) moves.add(move);
}
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) {
Move move = new Move(pos, new Position(pos.x, pos.y + 2 * sign));
if (isFreePath(move)) moves.add(move);
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 Type getType() { return Type.PAWN; }
public int getValue() { return 10; }
}

View File

@ -0,0 +1,80 @@
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;
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));
}
public PawnPromotion(int xPos, int yPos, int xDest, int yDest, Class<? extends Piece> promotionPiece)
throws ReflectiveOperationException, RuntimeException {
this(new Position(xPos, yPos), new Position(xDest, yDest), promotionPiece);
}
@Override
public void execute(Board board) {
try {
board.set(pos, promotionPieceConstructor.newInstance(board.get(pos).getColor(), board));
super.execute(board);
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | SecurityException e) {
e.printStackTrace();
}
}
@Override
public void revert(Board board, Piece capturedPiece) {
board.set(dest, new Pawn(board.get(dest).getColor(), board));
super.revert(board, capturedPiece);
}
@Override
public String toLAN() { return pos.toLAN() + dest.toLAN() + promotionPieceChar; }
@Override
public String toSAN(Board board) {
String san = super.toSAN(board);
return san + Character.toUpperCase(promotionPieceChar);
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + Objects.hash(promotionPieceChar, promotionPieceConstructor);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!super.equals(obj)) return false;
if (!(obj instanceof PawnPromotion)) return false;
PawnPromotion other = (PawnPromotion) obj;
return promotionPieceChar == other.promotionPieceChar && Objects.equals(promotionPieceConstructor, other.promotionPieceConstructor);
}
}

View File

@ -2,17 +2,20 @@ package dev.kske.chess.board;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
/**
* 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>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public abstract class Piece implements Cloneable {
protected Color color;
protected Board board;
private final Color color;
protected Board board;
public Piece(Color color, Board board) {
this.color = color;
@ -22,11 +25,10 @@ public abstract class Piece implements Cloneable {
public List<Move> getMoves(Position pos) {
List<Move> moves = getPseudolegalMoves(pos);
for (Iterator<Move> iterator = moves.iterator(); iterator.hasNext();) {
Move move = iterator.next();
Piece capturePiece = board.move(move);
if (board.checkCheck(getColor()))
iterator.remove();
board.revert(move, capturePiece);
Move move = iterator.next();
board.move(move);
if (board.checkCheck(getColor())) iterator.remove();
board.revert();
}
return moves;
}
@ -35,8 +37,16 @@ public abstract class Piece implements Cloneable {
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
*/
protected boolean isFreePath(Move move) {
// Only check destination by default
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);
}
@ -47,9 +57,7 @@ public abstract class Piece implements Cloneable {
* @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();
}
protected final boolean checkDestination(Move move) { return board.getDest(move) == null || board.getDest(move).getColor() != getColor(); }
@Override
public Object clone() {
@ -62,19 +70,65 @@ public abstract class Piece implements Cloneable {
return piece;
}
public abstract Type getType();
@Override
public String toString() { return getClass().getSimpleName(); }
public Color getColor() { return color; }
@Override
public int hashCode() { return Objects.hash(color); }
public static enum Type {
KING, QUEEN, ROOK, KNIGHT, BISHOP, PAWN;
@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;
}
public static enum Color {
WHITE, BLACK;
/**
* @return the standard value of this {@link Piece} that can be used for board
* evaluation
*/
public abstract int getValue();
public Color opposite() {
return this == WHITE ? BLACK : WHITE;
/**
* @return The first character of this {@link Piece} in algebraic notation and
* lower case
*/
public char firstChar() { return Character.toLowerCase(toString().charAt(0)); }
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; }
public static enum Color {
WHITE, BLACK;
public static Color fromFirstChar(char c) { return Character.toLowerCase(c) == 'w' ? WHITE : BLACK; }
public char firstChar() { return this == WHITE ? 'w' : 'b'; }
public Color opposite() { return this == WHITE ? BLACK : WHITE; }
}
}

View File

@ -4,7 +4,9 @@ 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>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class Position {
@ -15,8 +17,36 @@ public class Position {
this.y = y;
}
public static Position fromLAN(String pos) {
return new Position(pos.charAt(0) - 97, 8 - Character.getNumericValue(pos.charAt(1)));
}
public String toLAN() {
return String.valueOf((char) (x + 97)) + String.valueOf(8 - y);
}
@Override
public String toString() {
return String.format("[%d, %d]", x, y);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + x;
result = prime * result + y;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
Position other = (Position) obj;
if (x != other.x) return false;
if (y != other.y) return false;
return true;
}
}

View File

@ -7,7 +7,9 @@ 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>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class Queen extends Piece {
@ -20,22 +22,6 @@ public class Queen extends Piece {
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<>();
@ -115,5 +101,5 @@ public class Queen extends Piece {
}
@Override
public Type getType() { return Type.QUEEN; }
public int getValue() { return 90; }
}

View File

@ -7,7 +7,9 @@ import java.util.List;
* Project: <strong>Chess</strong><br>
* File: <strong>Rook.java</strong><br>
* Created: <strong>01.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class Rook extends Piece {
@ -20,18 +22,6 @@ public class Rook extends Piece {
return (move.isHorizontal() || move.isVertical()) && isFreePath(move);
}
@Override
protected boolean isFreePath(Move move) {
if (move.isHorizontal()) {
for (int i = move.pos.x + move.xSign; i != move.dest.x; i += move.xSign)
if (board.getBoardArr()[i][move.pos.y] != null) return false;
} else {
for (int i = move.pos.y + move.ySign; i != move.dest.y; i += move.ySign)
if (board.getBoardArr()[move.pos.x][i] != null) return false;
}
return checkDestination(move);
}
@Override
protected List<Move> getPseudolegalMoves(Position pos) {
List<Move> moves = new ArrayList<>();
@ -75,5 +65,5 @@ public class Rook extends Piece {
}
@Override
public Type getType() { return Type.ROOK; }
public int getValue() { return 50; }
}

View File

@ -0,0 +1,17 @@
package dev.kske.chess.event;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>Event.java</strong><br>
* Created: <strong>7 Aug 2019</strong><br>
*
* @since Chess v0.4-alpha
* @author Kai S. K. Engelbart
*/
public interface Event<T> {
/**
* @return The data associated with the event
*/
T getData();
}

View File

@ -0,0 +1,38 @@
package dev.kske.chess.event;
import java.util.ArrayList;
import java.util.List;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>EventBus.java</strong><br>
* Created: <strong>7 Aug 2019</strong><br>
*
* @since Chess v0.4-alpha
* @author Kai S. K. Engelbart
*/
public class EventBus {
private List<Subscribable> subscribers;
private static EventBus instance;
public static EventBus getInstance() {
if (instance == null) instance = new EventBus();
return instance;
}
private EventBus() {
subscribers = new ArrayList<>();
}
public void register(Subscribable subscribable) {
subscribers.add(subscribable);
}
public void dispatch(Event<?> event) {
subscribers.stream().filter(e -> e.supports().contains(event.getClass())).forEach(e -> e.handle(event));
}
public List<Subscribable> getSubscribers() { return subscribers; }
}

View File

@ -0,0 +1,21 @@
package dev.kske.chess.event;
import dev.kske.chess.game.Game;
/**
* 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 Event<Game> {
private final Game game;
public GameStartEvent(Game source) { game = source; }
@Override
public Game getData() { return game; }
}

View File

@ -0,0 +1,28 @@
package dev.kske.chess.event;
import dev.kske.chess.board.BoardState;
import dev.kske.chess.board.Move;
/**
* 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 Event<Move> {
private final Move move;
private final BoardState boardState;
public MoveEvent(Move move, BoardState boardState) {
this.move = move;
this.boardState = boardState;
}
@Override
public Move getData() { return move; }
public BoardState getBoardState() { return boardState; }
}

View File

@ -0,0 +1,26 @@
package dev.kske.chess.event;
import java.util.Set;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>Subscribable.java</strong><br>
* Created: <strong>7 Aug 2019</strong><br>
*
* @since Chess v0.4-alpha
* @author Kai S. K. Engelbart
*/
public interface Subscribable {
/**
* Consumes an event dispatched by an event bus.
*
* @param event The event dispatched by the event bus, only of supported type
*/
void handle(Event<?> event);
/**
* @return A set of classes this class is supposed to handle in events
*/
Set<Class<?>> supports();
}

View File

@ -0,0 +1,26 @@
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;
public ChessException(String message, Throwable cause) {
super(message, cause);
}
public ChessException(String message) {
super(message);
}
public ChessException(Throwable cause) {
super(cause);
}
}

View File

@ -1,54 +1,146 @@
package dev.kske.chess.game;
import java.util.HashMap;
import java.util.Map;
import javax.swing.JOptionPane;
import dev.kske.chess.board.Board;
import dev.kske.chess.board.GameState;
import dev.kske.chess.board.BoardState;
import dev.kske.chess.board.Move;
import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.event.EventBus;
import dev.kske.chess.event.GameStartEvent;
import dev.kske.chess.event.MoveEvent;
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.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>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class Game {
private Map<Color, Player> players;
private Map<Color, Player> players = new HashMap<>();
private Board board;
private OverlayComponent overlayComponent;
private BoardComponent boardComponent;
public Game(Map<Color, Player> players, BoardComponent boardComponent) {
this.players = players;
this.boardComponent = boardComponent;
this.board = boardComponent.getBoard();
public Game(BoardPane boardPane, String whiteName, String blackName) {
board = new Board();
init(boardPane, whiteName, blackName);
}
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));
// Initialize the game variable in each player
players.values().forEach(player -> player.setGame(this));
}
public void start() {
players.get(Color.WHITE).requestMove();
private Player getPlayer(String name, Color color) {
switch (name) {
case "Natural Player":
return new NaturalPlayer(color, overlayComponent);
case "AI Player":
return new AIPlayer(color, 4, -10);
default:
for (EngineInfo info : EngineUtil.getEngineInfos())
if (info.name.equals(name)) return new UCIPlayer(color, info.path);
System.err.println("Invalid player name: " + name);
return null;
}
}
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) {
// Redraw
boardComponent.repaint();
overlayComponent.displayArrow(move);
// Run garbage collection
System.gc();
BoardState boardState = board.getGameEventType(board.getDest(move).getColor().opposite());
EventBus.getInstance().dispatch(new MoveEvent(move, boardState));
switch (boardState) {
case CHECKMATE:
case STALEMATE:
System.out.printf("%s in %s!%n", player.color.opposite(), eventType);
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:
boardComponent.repaint();
players.get(player.color.opposite()).requestMove();
players.get(board.getLog().getActiveColor()).requestMove();
}
} else player.requestMove();
}
public void start() {
EventBus.getInstance().dispatch(new GameStartEvent(this));
players.get(board.getLog().getActiveColor()).requestMove();
}
public void reset() {
players.values().forEach(Player::cancelMove);
board.initDefaultPositions();
boardComponent.repaint();
overlayComponent.clearDots();
overlayComponent.clearArrow();
}
/**
* Stops the game by disconnecting its players form the UI.
*/
public void stop() {
players.values().forEach(Player::disconnect);
}
/**
* Assigns the players their opposite colors.
*/
public void swapColors() {
players.values().forEach(Player::cancelMove);
Player white = players.get(Color.WHITE);
Player black = players.get(Color.BLACK);
white.setColor(Color.BLACK);
black.setColor(Color.WHITE);
players.put(Color.WHITE, black);
players.put(Color.BLACK, white);
players.get(board.getLog().getActiveColor()).requestMove();
}
/**
* @return The board on which this game's moves are made
*/
public Board getBoard() { return board; }
/**
* @return The players participating in this game
*/
public Map<Color, Player> getPlayers() { return players; }
}

View File

@ -1,12 +1,15 @@
package dev.kske.chess.game;
import java.awt.event.MouseAdapter;
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 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;
@ -15,49 +18,97 @@ 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>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class NaturalPlayer extends Player {
public class NaturalPlayer extends Player implements MouseListener {
private boolean moveRequested;
private final OverlayComponent overlayComponent;
public NaturalPlayer(Board board, Color color, OverlayComponent overlayComponent) {
super(board, color);
moveRequested = false;
overlayComponent.addMouseListener(new MouseAdapter() {
private boolean moveRequested;
private Piece selectedPiece;
private List<Move> possibleMoves;
private Position pos;
public NaturalPlayer(Color color, OverlayComponent overlayComponent) {
super(color);
this.overlayComponent = overlayComponent;
name = "Player";
moveRequested = false;
@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));
pos = null;
}
}
});
overlayComponent.addMouseListener(this);
}
@Override
public void requestMove() {
moveRequested = true;
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 -> 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<Move>(selectedMoves.toArray(new Move[0]));
JOptionPane.showMessageDialog(overlayComponent, comboBox, "Select a promotion", JOptionPane.QUESTION_MESSAGE);
move = selectedMoves.get(comboBox.getSelectedIndex());
} else move = selectedMoves.get(0);
// Tell the game to execute the move
moveRequested = false;
game.onMove(NaturalPlayer.this, move);
}
// Discard the selection
overlayComponent.clearDots();
selectedPiece = null;
possibleMoves = null;
}
}
@Override
public void mouseClicked(MouseEvent e) {}
@Override
public void mouseReleased(MouseEvent e) {}
@Override
public void mouseEntered(MouseEvent e) {}
@Override
public void mouseExited(MouseEvent e) {}
}

View File

@ -7,24 +7,33 @@ 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>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public abstract class Player {
protected Game game;
protected Board board;
protected Color color;
protected Game game;
protected Board board;
protected Color color;
protected String name;
public Player(Board board, Color color) {
this.board = board;
this.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; }
public void setGame(Game game) {
this.game = game;
board = game.getBoard();
}
public Board getBoard() { return board; }
@ -33,4 +42,8 @@ public abstract class Player {
public Color getColor() { return color; }
public void setColor(Color color) { this.color = color; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}

View File

@ -0,0 +1,95 @@
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;
/**
* 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;
public UCIPlayer(Color color, String enginePath) {
super(color);
try {
handle = new UCIHandle(enginePath);
handle.setListener(this);
handle.start();
} catch (IOException ex) {
ex.printStackTrace();
}
}
@Override
public void requestMove() {
handle.positionFEN(new FENString(board).toString());
handle.go();
}
@Override
public void cancelMove() {
handle.stop();
}
@Override
public void disconnect() {
handle.quit();
}
@Override
public void onIdName(String name) {
this.name = name;
}
@Override
public void onBestMove(String move) {
Move moveObj = Move.fromLAN(move);
game.onMove(this, moveObj);
}
@Override
public void onBestMove(String move, Move ponderMove) {
onBestMove(move);
}
@Override
public void onCopyProtectionChecking() {
System.out.println("Copy protection checking...");
}
@Override
public void onCopyProtectionOk() {
System.out.println("Copy protection ok");
}
@Override
public void onCopyProtectionError() {
System.err.println("Copy protection error!");
}
@Override
public void onRegistrationChecking() {
System.out.println("Registration checking...");
}
@Override
public void onRegistrationOk() {
System.out.println("Registration ok");
}
@Override
public void onRegistrationError() {
System.err.println("Registration error!");
}
}

View File

@ -6,6 +6,7 @@ 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;
@ -18,21 +19,31 @@ 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>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class AIPlayer extends Player {
private int availableProcessors;
private int maxDepth;
private int alphaBetaThreshold;
public AIPlayer(Board board, Color color, int maxDepth) {
super(board, color);
availableProcessors = Runtime.getRuntime().availableProcessors();
this.maxDepth = maxDepth;
private volatile boolean exitRequested;
private volatile ExecutorService executor;
public AIPlayer(Color color, int maxDepth, int alphaBetaThreshold) {
super(color);
name = "AIPlayer";
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.
@ -41,8 +52,8 @@ public class AIPlayer extends Player {
/*
* Get a copy of the board and the available moves.
*/
Board board = (Board) AIPlayer.this.board.clone();
List<Move> moves = board.getMoves(color);
Board board = new Board(this.board, false);
List<Move> moves = board.getMoves(color);
/*
* Define move processors and split the available moves between them.
@ -55,16 +66,16 @@ public class AIPlayer extends Player {
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));
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
*/
ExecutorService executor = Executors.newFixedThreadPool(numThreads);
List<ProcessingResult> results = new ArrayList<>(numThreads);
executor = Executors.newFixedThreadPool(numThreads);
List<ProcessingResult> results = new ArrayList<>(numThreads);
try {
List<Future<ProcessingResult>> futures = executor.invokeAll(processors);
for (Future<ProcessingResult> f : futures)
@ -74,7 +85,23 @@ public class AIPlayer extends Player {
ex.printStackTrace();
}
results.sort((r1, r2) -> Integer.compare(r2.score, r1.score));
SwingUtilities.invokeLater(() -> game.onMove(this, results.get(0).move));
if (!exitRequested) SwingUtilities.invokeLater(() -> game.onMove(this, results.get(0).move));
}, "AIPlayer calculation setup").start();
}
@Override
public void cancelMove() {
exitRequested = true;
if (executor != null) {
executor.shutdownNow();
try {
executor.awaitTermination(500, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void disconnect() {}
}

View File

@ -1,33 +1,79 @@
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.Bishop;
import dev.kske.chess.board.Board;
import dev.kske.chess.board.King;
import dev.kske.chess.board.Knight;
import dev.kske.chess.board.Move;
import dev.kske.chess.board.Pawn;
import dev.kske.chess.board.Piece;
import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.board.Queen;
import dev.kske.chess.board.Rook;
/**
* 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>
*
* @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 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) {
this.board = board;
this.rootMoves = rootMoves;
this.color = color;
this.maxDepth = maxDepth;
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 } });
}
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
@ -39,12 +85,12 @@ public class MoveProcessor implements Callable<ProcessingResult> {
private int miniMax(Board board, List<Move> moves, Color color, int depth) {
int bestValue = Integer.MIN_VALUE;
for (Move move : moves) {
Piece capturePiece = board.move(move);
int teamValue = board.evaluate(color);
int enemyValue = board.evaluate(color.opposite());
int valueChange = teamValue - enemyValue;
board.move(move);
int teamValue = evaluate(board, color);
int enemyValue = evaluate(board, color.opposite());
int valueChange = teamValue - enemyValue;
if (depth < maxDepth && valueChange >= 0)
if (depth < maxDepth && valueChange >= alphaBetaThreshold)
valueChange -= miniMax(board, board.getMoves(color.opposite()), color.opposite(), depth + 1);
if (valueChange > bestValue) {
@ -52,8 +98,26 @@ public class MoveProcessor implements Callable<ProcessingResult> {
if (depth == 0) bestMove = move;
}
board.revert(move, capturePiece);
board.revert();
}
return bestValue;
}
/**
* Evaluated a board.
*
* @param color The color to evaluate for
* @return An positive number representing how good the position is
*/
private int evaluate(Board board, Color color) {
int score = 0;
for (int i = 0; i < 8; i++)
for (int j = 0; j < 8; j++)
if (board.getBoardArr()[i][j] != null && board.getBoardArr()[i][j].getColor() == color) {
score += board.getBoardArr()[i][j].getValue();
if (positionScores.containsKey(board.getBoardArr()[i][j].getClass()))
score += positionScores.get(board.getBoardArr()[i][j].getClass())[i][color == Color.WHITE ? j : 7 - j];
}
return score;
}
}

View File

@ -6,7 +6,9 @@ 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>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class ProcessingResult {

View File

@ -0,0 +1,101 @@
package dev.kske.chess.io;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import dev.kske.chess.uci.UCIHandle;
import dev.kske.chess.uci.UCIListener;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>MenuBar.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() {}
public static void addEngine(String enginePath) {
try {
EngineInfo info = new EngineInfo(enginePath);
UCIHandle handle = new UCIHandle(enginePath);
handle.setListener(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();
}
}
public static class EngineInfo implements Serializable {
private static final long serialVersionUID = -474177108900833005L;
public String path, name, author;
public EngineInfo(String path) {
this.path = path;
}
@Override
public String toString() {
return name + " by " + author + " at " + path;
}
}
public static List<EngineInfo> getEngineInfos() { return engineInfos; }
}

View File

@ -0,0 +1,90 @@
package dev.kske.chess.io;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import javax.imageio.ImageIO;
import dev.kske.chess.board.Piece;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>TextureUtil.java</strong><br>
* Created: <strong>01.07.2019</strong><br>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class TextureUtil {
private static Map<String, Image> textures = new HashMap<>(), scaledTextures = new HashMap<>();
static {
loadPieceTextures();
scaledTextures.putAll(textures);
}
private TextureUtil() {}
/**
* Loads a piece texture fitting to a piece object.
*
* @param piece The piece from which the texture properties are taken
* @return The fitting texture
*/
public static Image getPieceTexture(Piece piece) {
String key = piece.toString().toLowerCase() + "_" + piece.getColor().toString().toLowerCase();
return scaledTextures.get(key);
}
/**
* 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 tileSize) {
scaledTextures.clear();
textures.forEach((key, img) -> scaledTextures.put(key, img.getScaledInstance(tileSize, tileSize, Image.SCALE_SMOOTH)));
}
/**
* Loads an image from a file in the resource folder.
*
* @param fileName The name of the image resource
* @return The loaded image
*/
private static Image loadImage(String fileName) {
BufferedImage in = null;
try {
in = ImageIO.read(TextureUtil.class.getResourceAsStream(fileName));
} catch (IOException e) {
e.printStackTrace();
}
return in;
}
/**
* Load every PNG file inside the res/pieces directory.
* The filenames without extensions are used as keys in the map textures.
*/
private static void loadPieceTextures() {
Arrays
.asList("king_white",
"king_black",
"queen_white",
"queen_black",
"rook_white",
"rook_black",
"knight_white",
"knight_black",
"bishop_white",
"bishop_black",
"pawn_white",
"pawn_black")
.forEach(name -> textures.put(name, loadImage("/pieces/" + name + ".png")));
}
}

View File

@ -0,0 +1,56 @@
package dev.kske.chess.pgn;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
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 {
Scanner sc = new Scanner(pgnFile);
while (sc.hasNext())
games.add(PGNGame.parse(sc));
sc.close();
}
/**
* 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();
PrintWriter pw = new PrintWriter(pgnFile);
games.forEach(g -> g.writePGN(pw));
pw.close();
}
public List<PGNGame> getGames() { return games; }
}

View File

@ -0,0 +1,121 @@
package dev.kske.chess.pgn;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.regex.MatchResult;
import java.util.regex.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;
import dev.kske.chess.exception.ChessException;
/**
* 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;
public PGNGame() { board = new Board(); }
public PGNGame(Board board) { this.board = board; }
public static PGNGame parse(Scanner sc) throws ChessException {
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;
}
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"));
}
public String getTag(String tagName) { return tagPairs.get(tagName); }
public boolean hasTag(String tagName) { return tagPairs.containsKey(tagName); }
public void setTag(String tagName, String tagValue) { tagPairs.put(tagName, tagValue); }
public Board getBoard() { return board; }
}

View File

@ -0,0 +1,200 @@
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;
public UCIHandle(String enginePath) throws IOException {
process = new ProcessBuilder(enginePath).start();
out = new PrintWriter(process.getOutputStream(), true);
receiver = new UCIReceiver(process.getInputStream());
}
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"); }
public void setListener(UCIListener listener) { receiver.addListener(listener); }
}

View File

@ -0,0 +1,202 @@
package dev.kske.chess.uci;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
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");
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.valueOf(tokens[i++]) : 1;
final ArrayList<Move> moves = new ArrayList<>();
while (i < tokens.length && !params.contains(tokens[i]))
moves.add(Move.fromLAN(tokens[i++]));
currline.put(cpu, moves);
System.err.println("The parameter 'currline' for command 'info' is not yet implemented");
break;
default:
System.err.printf("Unknown parameter '%s' for command 'info' found!%n", tokens[i]);
}
}
public int getDepth() { return depth; }
public int getSeldepth() { return seldepth; }
public int getTime() { return time; }
public int getNodes() { return nodes; }
public int getMultipv() { return multipv; }
public int getCurrmovenumber() { return currmovenumber; }
public int getHashfull() { return hashfull; }
public int getNps() { return nps; }
public int getTbhits() { return tbhits; }
public int getSbhits() { return sbhits; }
public int getCpuload() { return cpuload; }
public int getCpunr() { return cpunr; }
public List<Move> getPv() { return pv; }
public List<Move> getRefutation() { return refutation; }
public Map<Integer, List<Move>> getCurrline() { return currline; }
public Move getCurrmove() { return currmove; }
public Score getScore() { return score; }
public String getDisplayString() { return displayString; }
public static class Score {
private int cp, mate;
private boolean lowerbound, upperbound;
private int length;
public Score(String line) {
String[] tokens = line.split(" ");
int i = 0;
for (; i < tokens.length; i++) {
if (params.contains(tokens[i])) break;
switch (tokens[i]) {
case "cp":
cp = Integer.parseInt(tokens[++i]);
break;
case "mate":
mate = Integer.parseInt(tokens[++i]);
break;
case "lowerbound":
lowerbound = true;
break;
case "upperbound":
upperbound = true;
break;
default:
System.err.printf("Unknown parameter '%s' for command 'score' found!%n", tokens[i]);
}
}
length = i + 1;
}
public int getCp() { return cp; }
public int getMate() { return mate; }
public boolean isLowerbound() { return lowerbound; }
public boolean isUpperbound() { return upperbound; }
/**
* @return The number of tokens this 'score' command contains (including
* itself).
*/
public int getLength() { return length; }
}
}

View File

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

View File

@ -0,0 +1,70 @@
package dev.kske.chess.uci;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.StringJoiner;
/**
* 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 static enum GUIType {
CHECK, SPIN, COMBO, BUTTON, STRING
}
}

View File

@ -0,0 +1,139 @@
package dev.kske.chess.uci;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
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;
public UCIReceiver(InputStream in) {
this.in = new BufferedReader(new InputStreamReader(in));
listeners = new ArrayList<>();
}
@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)))); }
public void addListener(UCIListener listener) { listeners.add(listener); }
}

View File

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

View File

@ -6,16 +6,19 @@ import java.awt.Graphics;
import javax.swing.JComponent;
import dev.kske.chess.board.Board;
import dev.kske.chess.io.TextureUtil;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>BoardComponent.java</strong><br>
* Created: <strong>01.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong><br>
* <br>
* A square panel for rendering the chess board. To work correctly,
* this must be added to a parent component that allows the child to decide the
* size.
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class BoardComponent extends JComponent {
@ -47,8 +50,8 @@ public class BoardComponent extends JComponent {
// Draw the pieces if a board is present
if (board != null) for (int i = 0; i < 8; i++)
for (int j = 0; j < 8; j++)
if (board.getBoardArr()[i][j] != null) g.drawImage(TextureUtil
.getPieceTexture(board.getBoardArr()[i][j]), i * tileSize, j * tileSize, this);
if (board.getBoardArr()[i][j] != null)
g.drawImage(TextureUtil.getPieceTexture(board.getBoardArr()[i][j]), i * tileSize, j * tileSize, this);
}
public int getTileSize() { return boardPane.getTileSize(); }

View File

@ -1,8 +1,6 @@
package dev.kske.chess.ui;
import java.awt.Dimension;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import javax.swing.JLayeredPane;
@ -10,7 +8,9 @@ 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>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class BoardPane extends JLayeredPane {
@ -24,23 +24,13 @@ public class BoardPane extends JLayeredPane {
public BoardPane() {
boardComponent = new BoardComponent(this);
overlayComponent = new OverlayComponent(this);
setLayer(overlayComponent, 1);
setLayout(null);
add(boardComponent, Integer.valueOf(1));
add(overlayComponent, Integer.valueOf(2));
add(boardComponent);
add(overlayComponent);
/*
* 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);
}
});
tileSize = 60;
setSize(getPreferredSize());
}

View File

@ -0,0 +1,82 @@
package dev.kske.chess.ui;
import java.awt.Component;
import java.awt.Font;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JComboBox;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.filechooser.FileNameExtensionFilter;
import 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() {}
public static void showFileSelectionDialog(Component parent, Consumer<List<File>> action, Collection<FileNameExtensionFilter> filters) {
JFileChooser fileChooser = new JFileChooser();
fileChooser.setCurrentDirectory(new File(System.getProperty("user.home")));
fileChooser.setAcceptAllFileFilterUsed(false);
filters.forEach(fileChooser::addChoosableFileFilter);
if (fileChooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) action.accept(Arrays.asList(fileChooser.getSelectedFile()));
}
public static void showFileSaveDialog(Component parent, Consumer<File> action, Collection<FileNameExtensionFilter> filters) {
JFileChooser fileChooser = new JFileChooser();
fileChooser.setCurrentDirectory(new File(System.getProperty("user.home")));
fileChooser.setAcceptAllFileFilterUsed(false);
filters.forEach(fileChooser::addChoosableFileFilter);
if (fileChooser.showSaveDialog(parent) == JFileChooser.APPROVE_OPTION) action.accept(
new File(fileChooser.getSelectedFile().getAbsolutePath() + "."
+ ((FileNameExtensionFilter) fileChooser.getFileFilter()).getExtensions()[0]));
}
public static void showGameConfigurationDialog(Component parent, BiConsumer<String, String> action) {
JPanel dialogPanel = new JPanel();
List<String> options = new ArrayList<>(Arrays.asList("Natural Player", "AI Player"));
EngineUtil.getEngineInfos().forEach(info -> options.add(info.name));
JLabel lblWhite = new JLabel("White:");
lblWhite.setFont(new Font("Tahoma", Font.PLAIN, 14));
lblWhite.setBounds(10, 11, 49, 14);
dialogPanel.add(lblWhite);
JComboBox<Object> cbWhite = new JComboBox<>();
cbWhite.setModel(new DefaultComboBoxModel<>(options.toArray()));
cbWhite.setBounds(98, 9, 159, 22);
dialogPanel.add(cbWhite);
JLabel lblBlack = new JLabel("Black:");
lblBlack.setFont(new Font("Tahoma", Font.PLAIN, 14));
lblBlack.setBounds(10, 38, 49, 14);
dialogPanel.add(lblBlack);
JComboBox<Object> cbBlack = new JComboBox<>();
cbBlack.setModel(new DefaultComboBoxModel<>(options.toArray()));
cbBlack.setBounds(98, 36, 159, 22);
dialogPanel.add(cbBlack);
JOptionPane.showMessageDialog(parent, dialogPanel, "Game configuration", JOptionPane.QUESTION_MESSAGE);
action.accept(options.get(cbWhite.getSelectedIndex()), options.get(cbBlack.getSelectedIndex()));
}
}

View File

@ -0,0 +1,37 @@
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;
/**
* 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;
public GameDropTarget(MainWindow mainWindow) { this.mainWindow = mainWindow; }
@SuppressWarnings("unchecked")
@Override
public void drop(DropTargetDropEvent evt) {
try {
evt.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
mainWindow.loadFiles((List<File>) evt.getTransferable().getTransferData(DataFlavor.javaFileListFlavor));
} catch (UnsupportedFlavorException | IOException ex) {
ex.printStackTrace();
evt.rejectDrop();
}
}
}

View File

@ -1,72 +0,0 @@
package dev.kske.chess.ui;
import java.awt.FlowLayout;
import java.util.HashMap;
import java.util.Map;
import javax.swing.JButton;
import javax.swing.JDialog;
import dev.kske.chess.board.Board;
import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.game.Game;
import dev.kske.chess.game.NaturalPlayer;
import dev.kske.chess.game.Player;
import dev.kske.chess.game.ai.AIPlayer;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>GameModeDialog.java</strong><br>
* Created: <strong>06.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class GameModeDialog extends JDialog {
private static final long serialVersionUID = 5470026233924735607L;
/**
* Create the dialog.
*/
public GameModeDialog(BoardPane boardPane) {
super();
setModal(true);
setTitle("Game Mode Selection");
setBounds(100, 100, 231, 133);
setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
getContentPane().setLayout(new FlowLayout(FlowLayout.CENTER, 5, 5));
final BoardComponent boardComponent = boardPane.getBoardComponent();
final OverlayComponent overlayComponent = boardPane.getOverlayComponent();
final Board board = boardComponent.getBoard();
JButton btnNatural = new JButton("Game against natural opponent");
btnNatural.addActionListener((evt) -> {
Map<Color, Player> players = new HashMap<>();
players.put(Color.WHITE, new NaturalPlayer(board, Color.WHITE, overlayComponent));
players.put(Color.BLACK, new NaturalPlayer(board, Color.BLACK, overlayComponent));
new Game(players, boardComponent).start();
dispose();
});
getContentPane().add(btnNatural);
JButton btnAI = new JButton("Game against AI");
btnAI.addActionListener((evt) -> {
Map<Color, Player> players = new HashMap<>();
players.put(Color.WHITE, new NaturalPlayer(board, Color.WHITE, overlayComponent));
players.put(Color.BLACK, new AIPlayer(board, Color.BLACK, 4));
new Game(players, boardComponent).start();
dispose();
});
getContentPane().add(btnAI);
JButton btnAI2 = new JButton("AI against AI");
btnAI2.addActionListener((evt) -> {
Map<Color, Player> players = new HashMap<>();
players.put(Color.WHITE, new AIPlayer(board, Color.WHITE, 4));
players.put(Color.BLACK, new AIPlayer(board, Color.BLACK, 3));
new Game(players, boardComponent).start();
dispose();
});
getContentPane().add(btnAI2);
}
}

View File

@ -0,0 +1,199 @@
package dev.kske.chess.ui;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ListSelectionModel;
import dev.kske.chess.board.BoardState;
import dev.kske.chess.board.MoveNode;
import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.event.Event;
import dev.kske.chess.event.EventBus;
import dev.kske.chess.event.GameStartEvent;
import dev.kske.chess.event.MoveEvent;
import dev.kske.chess.event.Subscribable;
import dev.kske.chess.game.Game;
import dev.kske.chess.game.NaturalPlayer;
/**
* 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 {
private static final long serialVersionUID = 4349772338239617477L;
private JButton btnRestart, btnSwapColors;
private BoardPane boardPane;
private Game game;
private Color activeColor;
private JPanel moveSelectionPanel;
private JButton btnNext;
private JButton btnFirst;
private JButton btnLast;
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);
JButton btnPreviousMove = new JButton("Previous");
btnPreviousMove.setEnabled(false);
moveSelectionPanel.add(btnPreviousMove);
btnNext = new JButton("Next");
btnNext.setEnabled(false);
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(JLabel.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);
JList<MoveNode> 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().register(new Subscribable() {
@Override
public void handle(Event<?> event) {
if (event instanceof MoveEvent && (((MoveEvent) event).getBoardState() == BoardState.CHECKMATE
|| ((MoveEvent) event).getBoardState() == BoardState.STALEMATE))
btnSwapColors.setEnabled(false);
else if (event instanceof GameStartEvent) btnSwapColors.setEnabled(
game.getPlayers().get(Color.WHITE) instanceof NaturalPlayer ^ game.getPlayers().get(Color.BLACK) instanceof NaturalPlayer);
if (game.getBoard().getLog() == null) return;
DefaultListModel<MoveNode> model = new DefaultListModel<>();
game.getBoard().getLog().forEach(node -> model.addElement(node));
pgnList.setModel(model);
}
@Override
public Set<Class<?>> supports() { return new HashSet<>(Arrays.asList(MoveEvent.class, GameStartEvent.class)); }
});
}
/**
* @return The {@link BoardPane} instance associated with this game pane
*/
public BoardPane getBoardPane() { return boardPane; }
/**
* @return The {@link Game} instance associated with this game pane
*/
public Game getGame() { return game; }
/**
* Assigns a new {@link Game} instance to this game pane. If exactly one of the
* players is natural, color swapping functionality is enabled.
*
* @param game The {@link Game} to assign to this game pane.
*/
public void setGame(Game game) {
if (this.game != null) this.game.stop();
this.game = game;
}
}

View File

@ -1,38 +1,50 @@
package dev.kske.chess.ui;
import java.awt.BorderLayout;
import java.awt.Desktop;
import java.awt.EventQueue;
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.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JOptionPane;
import javax.swing.JTabbedPane;
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>
* Author: <strong>Kai S. K. Engelbart</strong>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class MainWindow {
public class MainWindow extends JFrame {
private JFrame mframe;
private static final long serialVersionUID = -3100939302567978977L;
private JTabbedPane tabbedPane;
/**
* 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();
}
EventQueue.invokeLater(() -> {
try {
new MainWindow();
} catch (Exception ex) {
ex.printStackTrace();
}
});
}
@ -41,6 +53,7 @@ public class MainWindow {
* Create the application.
*/
public MainWindow() {
super("Chess by Kai S. K. Engelbart");
initialize();
}
@ -48,24 +61,135 @@ public class MainWindow {
* 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);
// Configure frame
setResizable(false);
setBounds(100, 100, 494, 565);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setIconImage(Toolkit.getDefaultToolkit().getImage(getClass().getResource("/pieces/queen_white.png")));
BoardPane boardPane = new BoardPane();
boardPane.getBoardComponent().setBoard(new Board());
mframe.getContentPane().add(boardPane, BorderLayout.CENTER);
// Add frame content
tabbedPane = new JTabbedPane();
getContentPane().add(tabbedPane);
JPanel toolPanel = new JPanel();
mframe.getContentPane().add(toolPanel, BorderLayout.NORTH);
setJMenuBar(new MenuBar(this));
new DropTarget(this, new GameDropTarget(this));
JButton btnRestart = new JButton("Restart");
btnRestart.addActionListener((evt) -> System.err.println("Resetting not implemented!"));
toolPanel.add(btnRestart);
mframe.pack();
// Update position and dimensions
pack();
setLocationRelativeTo(null);
setVisible(true);
}
// Display dialog for game mode selection
new GameModeDialog(boardPane).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.getComponentCount() + 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.setSelectedIndex(tabbedPane.getComponentCount() - 1);
return 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);
}
});
}
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.toString(),
"File saving error",
JOptionPane.ERROR_MESSAGE);
}
}
}

View File

@ -0,0 +1,118 @@
package dev.kske.chess.ui;
import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
import java.util.Arrays;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
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;
public MenuBar(MainWindow mainWindow) {
this.mainWindow = mainWindow;
initGameMenu();
initEngineMenu();
initToolsMenu();
}
private void initGameMenu() {
JMenu gameMenu = new JMenu("Game");
JMenuItem newGameMenuItem = new JMenuItem("New Game");
newGameMenuItem.addActionListener((evt) -> DialogUtil.showGameConfigurationDialog(mainWindow, (whiteName, blackName) -> {
GamePane gamePane = mainWindow.addGamePane();
Game game = new Game(gamePane.getBoardPane(), whiteName, blackName);
gamePane.setGame(game);
game.start();
}));
gameMenu.add(newGameMenuItem);
JMenuItem loadFileMenu = new JMenuItem("Load game file");
loadFileMenu.addActionListener((evt) -> DialogUtil
.showFileSelectionDialog(mainWindow,
mainWindow::loadFiles,
Arrays.asList(new FileNameExtensionFilter("FEN and PGN files", "fen", "pgn"))));
gameMenu.add(loadFileMenu);
JMenuItem saveFileMenu = new JMenuItem("Save game file");
saveFileMenu
.addActionListener((evt) -> DialogUtil
.showFileSaveDialog(mainWindow, mainWindow::saveFile, Arrays.asList(new FileNameExtensionFilter("PGN file", "pgn"))));
gameMenu.add(saveFileMenu);
add(gameMenu);
newGameMenuItem.doClick();
}
private void initEngineMenu() {
JMenu engineMenu = new JMenu("Engine");
JMenuItem addEngineMenuItem = new JMenuItem("Add engine");
addEngineMenuItem.addActionListener((evt) -> {
String enginePath = JOptionPane
.showInputDialog(getParent(), "Enter the path to a UCI-compatible chess engine:", "Engine selection", JOptionPane.QUESTION_MESSAGE);
if (enginePath != null) EngineUtil.addEngine(enginePath);
});
JMenuItem showInfoMenuItem = new JMenuItem("Show engine info");
engineMenu.add(addEngineMenuItem);
engineMenu.add(showInfoMenuItem);
add(engineMenu);
}
private void initToolsMenu() {
JMenu toolsMenu = new JMenu("Tools");
JMenuItem exportFENMenuItem = new JMenuItem("Export board to FEN");
exportFENMenuItem.addActionListener((evt) -> {
final String fen = new FENString(mainWindow.getSelectedGamePane().getGame().getBoard()).toString();
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(fen), null);
JOptionPane.showMessageDialog(mainWindow, String.format("FEN-string copied to clipboard!%n%s", fen));
});
toolsMenu.add(exportFENMenuItem);
JMenuItem loadFromFENMenuItem = new JMenuItem("Load board from FEN");
loadFromFENMenuItem.addActionListener((evt) -> {
final GamePane gamePane = mainWindow.addGamePane();
final String fen = JOptionPane.showInputDialog("Enter a FEN string: ");
DialogUtil.showGameConfigurationDialog(mainWindow, (whiteName, blackName) -> {
Game game;
try {
game = new Game(gamePane.getBoardPane(), whiteName, blackName, new FENString(fen).getBoard());
gamePane.setGame(game);
game.start();
} catch (ChessException e) {
e.printStackTrace();
JOptionPane
.showMessageDialog(mainWindow, "Failed to load FEN string: " + e.toString(), "FEN loading error", JOptionPane.ERROR_MESSAGE);
}
});
});
toolsMenu.add(loadFromFENMenuItem);
add(toolsMenu);
}
}

View File

@ -0,0 +1,34 @@
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));
setText(node.move.toLAN());
setBackground(isSelected ? Color.red : Color.white);
setOpaque(true);
return this;
}
}

View File

@ -1,19 +1,28 @@
package dev.kske.chess.ui;
import java.awt.BasicStroke;
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>
*
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
public class OverlayComponent extends JComponent {
@ -22,11 +31,12 @@ public class OverlayComponent extends JComponent {
private final BoardPane boardPane;
private List<Position> dots;
private Move arrow;
public OverlayComponent(BoardPane boardPane) {
this.boardPane = boardPane;
dots = new ArrayList<>();
setSize(boardPane.getPreferredSize());
dots = new ArrayList<>();
}
@Override
@ -34,7 +44,27 @@ public class OverlayComponent extends JComponent {
super.paintComponent(g);
final int tileSize = 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);
@ -47,6 +77,36 @@ public class OverlayComponent extends JComponent {
}
}
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);
@ -58,5 +118,15 @@ public class OverlayComponent extends JComponent {
repaint();
}
public void displayArrow(Move arrow) {
this.arrow = arrow;
repaint();
}
public void clearArrow() {
arrow = null;
repaint();
}
public int getTileSize() { return boardPane.getTileSize(); }
}

View File

@ -1,76 +0,0 @@
package dev.kske.chess.ui;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.imageio.ImageIO;
import dev.kske.chess.board.Piece;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>TextureUtil.java</strong><br>
* Created: <strong>01.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class TextureUtil {
private static Map<String, Image> textures;
static {
textures = new HashMap<>();
loadPieceTextures();
}
private TextureUtil() {}
/**
* Loads a piece texture fitting to a piece object
*
* @param piece The piece from which the texture properties are taken
* @return The fitting texture
*/
public static Image getPieceTexture(Piece piece) {
String key = piece.getType().toString().toLowerCase() + "_" + piece.getColor().toString().toLowerCase();
return textures.get(key);
}
/**
* Scales all piece textures to fit the current tile size
*/
public static void scalePieceTextures(int scale) {
textures.replaceAll((key, img) -> img.getScaledInstance(scale, scale, Image.SCALE_SMOOTH));
}
/**
* Loads an image from a file.
*
* @param file The image file
* @return The loaded image
*/
private static Image loadImage(File file) {
BufferedImage in = null;
try {
in = ImageIO.read(file);
} catch (IOException e) {
e.printStackTrace();
}
return in;
}
/**
* Load every PNG file inside the res/pieces directory.
* The filenames without extensions are used as keys in the map textures.
*/
private static void loadPieceTextures() {
File dir = new File("res/pieces");
File[] files = dir.listFiles((File parentDir, String name) -> name.toLowerCase().endsWith(".png"));
for (File file : files)
textures.put(file.getName().replaceFirst("[.][^.]+$", ""), TextureUtil.loadImage(file));
}
}

View File

@ -1,4 +1,4 @@
package dev.kske.chess.test;
package dev.kske.chess.board;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotSame;
@ -6,17 +6,15 @@ import static org.junit.Assert.assertNotSame;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import dev.kske.chess.board.Board;
import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.board.Queen;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>BoardCloneTest.java</strong><br>
* File: <strong>BoardTest.java</strong><br>
* Created: <strong>08.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
class BoardCloneTest {
class BoardTest {
Board board;
@ -33,12 +31,13 @@ class BoardCloneTest {
*/
@Test
void testClone() {
Board clone = (Board) board.clone();
Board clone = new Board(board, false);
assertNotSame(clone, board);
assertNotSame(clone.getBoardArr(), board.getBoardArr());
clone.getBoardArr()[0][0] = new Queen(Color.BLACK, clone);
clone.move(new Move(1, 1, 1, 2));
assertNotEquals(clone.getBoardArr()[0][0], board.getBoardArr()[0][0]);
assertNotEquals(clone.getLog().getActiveColor(), board.getLog().getActiveColor());
}
}

View File

@ -0,0 +1,72 @@
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 {
List<String> fenStrings = new ArrayList<>();
List<Board> boards = new ArrayList<>();
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 dev.kske.chess.board.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 dev.kske.chess.board.FENString#getBoard()}.
*
* @throws ChessException
*/
@Test
void testGetBoard() throws ChessException {
for (int i = 0; i < boards.size(); i++)
assertEquals(boards.get(i), new FENString(fenStrings.get(i)).getBoard());
}
}

View File

@ -0,0 +1,165 @@
package dev.kske.chess.board;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import org.junit.jupiter.api.Test;
import 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 {
Log log = new Log();
/**
* Test method for {@link dev.kske.chess.board.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 dev.kske.chess.board.Log#clone()}.
*/
@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 dev.kske.chess.board.Log#add(dev.kske.chess.board.Move, dev.kske.chess.board.Piece, boolean)}.
*/
@Test
void testAdd() {
fail("Not yet implemented");
}
/**
* Test method for {@link dev.kske.chess.board.Log#removeLast()}.
*/
@Test
void testRemoveLast() {
fail("Not yet implemented");
}
/**
* Test method for {@link dev.kske.chess.board.Log#isEmpty()}.
*/
@Test
void testIsEmpty() {
fail("Not yet implemented");
}
/**
* Test method for {@link dev.kske.chess.board.Log#reset()}.
*/
@Test
void testReset() {
fail("Not yet implemented");
}
/**
* Test method for {@link dev.kske.chess.board.Log#getRoot()}.
*/
@Test
void testGetRoot() {
fail("Not yet implemented");
}
/**
* Test method for {@link dev.kske.chess.board.Log#getLast()}.
*/
@Test
void testGetLast() {
fail("Not yet implemented");
}
/**
* Test method for {@link dev.kske.chess.board.Log#getEnPassant()}.
*/
@Test
void testGetEnPassant() {
fail("Not yet implemented");
}
/**
* Test method for {@link dev.kske.chess.board.Log#setEnPassant(dev.kske.chess.board.Position)}.
*/
@Test
void testSetEnPassant() {
fail("Not yet implemented");
}
/**
* Test method for {@link dev.kske.chess.board.Log#getActiveColor()}.
*/
@Test
void testGetActiveColor() {
fail("Not yet implemented");
}
/**
* Test method for {@link dev.kske.chess.board.Log#setActiveColor(dev.kske.chess.board.Piece.Color)}.
*/
@Test
void testSetActiveColor() {
fail("Not yet implemented");
}
/**
* Test method for {@link dev.kske.chess.board.Log#getFullmoveNumber()}.
*/
@Test
void testGetFullmoveCounter() {
fail("Not yet implemented");
}
/**
* Test method for {@link dev.kske.chess.board.Log#setFullmoveNumber(int)}.
*/
@Test
void testSetFullmoveCounter() {
fail("Not yet implemented");
}
/**
* Test method for {@link dev.kske.chess.board.Log#getHalfmoveClock()}.
*/
@Test
void testGetHalfmoveClock() {
fail("Not yet implemented");
}
/**
* Test method for {@link dev.kske.chess.board.Log#setHalfmoveClock(int)}.
*/
@Test
void testSetHalfmoveClock() {
fail("Not yet implemented");
}
}

View File

@ -0,0 +1,47 @@
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 {
final int n = 4;
Position[] positions = new Position[] { new Position(0, 0), new Position(7, 7), new Position(0, 7), new Position(7, 0) };
String[] sans = new String[] { "a8", "h1", "a1", "h8" };
String[] strings = new String[] { "[0, 0]", "[7, 7]", "[0, 7]", "[7, 0]" };
/**
* Test method for
* {@link dev.kske.chess.board.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 dev.kske.chess.board.Position#toLAN()}.
*/
@Test
void testToSAN() {
for (int i = 0; i < n; i++)
assertEquals(sans[i], positions[i].toLAN());
}
/**
* Test method for {@link dev.kske.chess.board.Position#toString()}.
*/
@Test
void testToString() {
for (int i = 0; i < n; i++)
assertEquals(strings[i], positions[i].toString());
}
}

View File

@ -0,0 +1,36 @@
package dev.kske.chess.pgn;
import static org.junit.jupiter.api.Assertions.fail;
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
* @throws FileNotFoundException
*/
@Test
void testLoad() {
PGNDatabase db = new PGNDatabase();
try {
db.load(new File(getClass().getClassLoader().getResource("test.pgn").getFile()));
} catch (FileNotFoundException | ChessException e) {
e.printStackTrace();
fail(e);
}
}
}

18
test_res/test.pgn Normal file
View File

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