68 Commits

Author SHA1 Message Date
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
43 changed files with 2800 additions and 338 deletions

View File

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

4
.gitignore vendored
View File

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

View File

@ -20,14 +20,6 @@ public class Bishop extends Piece {
return move.isDiagonal() && isFreePath(move); return move.isDiagonal() && isFreePath(move);
} }
@Override
protected boolean isFreePath(Move move) {
for (int i = move.pos.x + move.xSign, j = move.pos.y
+ move.ySign; i != move.dest.x; i += move.xSign, j += move.ySign)
if (board.getBoardArr()[i][j] != null) return false;
return checkDestination(move);
}
@Override @Override
protected List<Move> getPseudolegalMoves(Position pos) { protected List<Move> getPseudolegalMoves(Position pos) {
List<Move> moves = new ArrayList<>(); List<Move> moves = new ArrayList<>();

View File

@ -5,6 +5,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import dev.kske.chess.board.Log.MoveNode;
import dev.kske.chess.board.Piece.Color; import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.board.Piece.Type; import dev.kske.chess.board.Piece.Type;
@ -14,15 +15,88 @@ import dev.kske.chess.board.Piece.Type;
* Created: <strong>01.07.2019</strong><br> * Created: <strong>01.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong> * Author: <strong>Kai S. K. Engelbart</strong>
*/ */
public class Board implements Cloneable { public class Board {
private Piece[][] boardArr; private Piece[][] boardArr = new Piece[8][8];
private Map<Color, Position> kingPos; private Map<Color, Position> kingPos = new HashMap<>();
private Map<Color, Map<Type, Boolean>> castlingRights = new HashMap<>();
private Log log = new Log();
private static final Map<Type, int[][]> positionScores;
static {
positionScores = new HashMap<>();
positionScores.put(Type.KING,
new int[][] { new int[] { -3, -4, -4, -5, -5, -4, -4, -3 },
new int[] { -3, -4, -4, -5, -4, -4, -4, -3 }, new int[] { -3, -4, -4, -5, -4, -4, -4, -3 },
new int[] { -3, -4, -4, -5, -4, -4, -4, -3 }, new int[] { -2, -3, -3, -2, -2, -2, -2, -1 },
new int[] { -1, -2, -2, -2, -2, -2, -2, -1 }, new int[] { 2, 2, 0, 0, 0, 0, 2, 2 },
new int[] { 2, 3, 1, 0, 0, 1, 3, 2 } });
positionScores.put(Type.QUEEN,
new int[][] { new int[] { -2, -1, -1, -1, -1, -1, -1, -2 }, new int[] { -1, 0, 0, 0, 0, 0, 0, -1 },
new int[] { -1, 0, 1, 1, 1, 1, 0, -1 }, new int[] { -1, 0, 1, 1, 1, 1, 0, -1 },
new int[] { 0, 0, 1, 1, 1, 1, 0, -1 }, new int[] { -1, 1, 1, 1, 1, 1, 0, -1 },
new int[] { -1, 0, 1, 0, 0, 0, 0, -1 }, new int[] { -2, -1, -1, -1, -1, -1, -1, -2 } });
positionScores.put(Type.ROOK,
new int[][] { new int[] { 0, 0, 0, 0, 0, 0, 0, 0 }, new int[] { 1, 1, 1, 1, 1, 1, 1, 1 },
new int[] { -1, 0, 0, 0, 0, 0, 0, -1 }, new int[] { -1, 0, 0, 0, 0, 0, 0, -1 },
new int[] { -1, 0, 0, 0, 0, 0, 0, -1 }, new int[] { -1, 0, 0, 0, 0, 0, 0, -1 },
new int[] { -1, 0, 0, 0, 0, 0, 0, -1 }, new int[] { 0, 0, 0, 1, 1, 0, 0, 0 } });
positionScores.put(Type.KNIGHT,
new int[][] { new int[] { -5, -4, -3, -3, -3, -3, -4, -5 }, new int[] { -4, -2, 0, 0, 0, 0, -2, -4 },
new int[] { -3, 0, 1, 2, 2, 1, 0, -3 }, new int[] { -3, 1, 2, 2, 2, 2, 1, -3 },
new int[] { -3, 0, 2, 2, 2, 2, 0, -1 }, new int[] { -3, 1, 1, 2, 2, 1, 1, -3 },
new int[] { -4, -2, 0, 1, 1, 0, -2, -4 }, new int[] { -5, -4, -3, -3, -3, -3, -4, -5 } });
positionScores.put(Type.BISHOP,
new int[][] { new int[] { -2, -1, -1, -1, -1, -1, -1, 2 }, new int[] { -1, 0, 0, 0, 0, 0, 0, -1 },
new int[] { -1, 0, 1, 1, 1, 1, 0, -1 }, new int[] { -1, 1, 1, 1, 1, 1, 1, -1 },
new int[] { -1, 0, 1, 1, 1, 1, 0, -1 }, new int[] { -1, 1, 1, 1, 1, 1, 1, -1 },
new int[] { -1, 1, 0, 0, 0, 0, 1, -1 }, new int[] { -2, -1, -1, -1, -1, -1, -1, -2 } });
positionScores.put(Type.PAWN,
new int[][] { new int[] { 0, 0, 0, 0, 0, 0, 0, 0 }, new int[] { 5, 5, 5, 5, 5, 5, 5, 5 },
new int[] { 1, 1, 2, 3, 3, 2, 1, 1 }, new int[] { 0, 0, 1, 3, 3, 1, 0, 0 },
new int[] { 0, 0, 0, 2, 2, 0, 0, 0 }, new int[] { 0, 0, -1, 0, 0, -1, 0, 0 },
new int[] { 0, 1, 1, -2, -2, 1, 1, 0 }, new int[] { 0, 0, 0, 0, 0, 0, 0, 0 } });
}
/**
* Initializes the board with the default chess starting position.
*/
public Board() { public Board() {
boardArr = new Piece[8][8]; initDefaultPositions();
kingPos = new HashMap<>(); }
initializeDefaultPositions();
/**
* Initializes the board with data from a FEN-string.
*
* @param fen The FEN-string to initialize the board from
*/
public Board(String fen) {
initFromFEN(fen);
}
/**
* 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
*/
public Board(Board other) {
boardArr = new Piece[8][8];
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);
Map<Type, Boolean> whiteCastling = new HashMap<>(other.castlingRights.get(Color.WHITE)),
blackCastling = new HashMap<>(other.castlingRights.get(Color.BLACK));
castlingRights.put(Color.WHITE, whiteCastling);
castlingRights.put(Color.BLACK, blackCastling);
log = new Log(other.log, false);
} }
/** /**
@ -35,12 +109,15 @@ public class Board implements Cloneable {
Piece piece = getPos(move); Piece piece = getPos(move);
if (piece == null || !piece.isValidMove(move)) return false; if (piece == null || !piece.isValidMove(move)) return false;
else { else {
// Set type after validation
if (move.type == Move.Type.UNKNOWN) move.type = Move.Type.NORMAL;
// Move piece // Move piece
Piece capturePiece = move(move); move(move);
// Revert move if it caused a check for its team // Revert move if it caused a check for its team
if (checkCheck(piece.getColor())) { if (checkCheck(piece.getColor())) {
revert(move, capturePiece); revert();
return false; return false;
} }
@ -52,33 +129,133 @@ public class Board implements Cloneable {
* Moves a piece across the board without checking if the move is legal. * Moves a piece across the board without checking if the move is legal.
* *
* @param move The move to execute * @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 piece = getPos(move);
Piece capturePiece = getDest(move); Piece capturePiece = getDest(move);
setDest(move, piece);
setPos(move, null);
// Update the king's position if the moved piece is the king switch (move.type) {
case PAWN_PROMOTION:
setPos(move, null);
// TODO: Select promotion
setDest(move, new Queen(piece.getColor(), this));
break;
case EN_PASSANT:
setDest(move, piece);
setPos(move, null);
boardArr[move.dest.x][move.dest.y - move.ySign] = null;
break;
case CASTLING:
// Move the king
setDest(move, piece);
setPos(move, null);
// Move the rook
Move rookMove = move.dest.x == 6 ? new Move(7, move.pos.y, 5, move.pos.y) // Kingside
: new Move(0, move.pos.y, 3, move.pos.y); // Queenside
// Move the rook
setDest(rookMove, getPos(rookMove));
setPos(rookMove, null);
getDest(rookMove).incMoveCounter();
break;
case UNKNOWN:
System.err.printf("Move of unknown type %s found!%n", move);
case NORMAL:
setDest(move, piece);
setPos(move, null);
break;
default:
System.err.printf("Move %s of unimplemented type found!%n", move);
}
// Increment move counter
getDest(move).incMoveCounter();
// Update the king's position if the moved piece is the king and castling
// availability
if (piece.getType() == Type.KING) kingPos.put(piece.getColor(), move.dest); if (piece.getType() == Type.KING) kingPos.put(piece.getColor(), move.dest);
return capturePiece; // Update log
log.add(move, capturePiece, piece.getType() == Type.PAWN);
updateCastlingRights();
} }
/** /**
* Reverts a move. * Reverts the last move.
*
* @param move The move to revert
* @param capturedPiece The piece that has been captured when the move has been
* applied
*/ */
public void revert(Move move, Piece capturedPiece) { public void revert() {
setPos(move, getDest(move)); MoveNode moveNode = log.getLast();
setDest(move, capturedPiece); Move move = moveNode.move;
Piece capturedPiece = moveNode.capturedPiece;
switch (move.type) {
case PAWN_PROMOTION:
setPos(move, new Pawn(getDest(move).getColor(), this));
setDest(move, capturedPiece);
break;
case EN_PASSANT:
setPos(move, getDest(move));
setDest(move, null);
boardArr[move.dest.x][move.dest.y - move.ySign] = new Pawn(getPos(move).getColor().opposite(), this);
break;
case CASTLING:
// Move the king
setPos(move, getDest(move));
setDest(move, null);
// Move the rook
Move rookMove = move.dest.x == 6 ? new Move(5, move.pos.y, 7, move.pos.y) // Kingside
: new Move(3, move.pos.y, 0, move.pos.y); // Queenside
setDest(rookMove, getPos(rookMove));
setPos(rookMove, null);
getDest(rookMove).decMoveCounter();
break;
case UNKNOWN:
System.err.printf("Move of unknown type %s found!%n", move);
case NORMAL:
setPos(move, getDest(move));
setDest(move, capturedPiece);
break;
default:
System.err.printf("Move %s of unimplemented type found!%n", move);
}
// Decrement move counter
getPos(move).decMoveCounter();
// Update the king's position if the moved piece is the king // 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).getType() == Type.KING) kingPos.put(getPos(move).getColor(), move.pos);
// Update log
log.removeLast();
updateCastlingRights();
}
private void updateCastlingRights() {
// White
if (new Position(4, 7).equals(kingPos.get(Color.WHITE))) {
final King king = (King) get(kingPos.get(Color.WHITE));
castlingRights.get(Color.WHITE).put(Type.KING, king.canCastleKingside());
castlingRights.get(Color.WHITE).put(Type.QUEEN, king.canCastleQueenside());
} else {
castlingRights.get(Color.WHITE).put(Type.KING, false);
castlingRights.get(Color.WHITE).put(Type.QUEEN, false);
}
// Black
if (new Position(4, 0).equals(kingPos.get(Color.BLACK))) {
final King king = (King) get(kingPos.get(Color.BLACK));
castlingRights.get(Color.BLACK).put(Type.KING, king.canCastleKingside());
castlingRights.get(Color.BLACK).put(Type.QUEEN, king.canCastleQueenside());
} else {
castlingRights.get(Color.BLACK).put(Type.KING, false);
castlingRights.get(Color.BLACK).put(Type.QUEEN, false);
}
} }
/** /**
@ -129,9 +306,9 @@ public class Board implements Cloneable {
if (!getMoves(kingPos.get(color)).isEmpty()) return false; if (!getMoves(kingPos.get(color)).isEmpty()) return false;
else { else {
for (Move move : getMoves(color)) { for (Move move : getMoves(color)) {
Piece capturePiece = move(move); move(move);
boolean check = checkCheck(color); boolean check = checkCheck(color);
revert(move, capturePiece); revert();
if (!check) return false; if (!check) return false;
} }
return true; return true;
@ -139,9 +316,9 @@ public class Board implements Cloneable {
} }
public GameState getGameEventType(Color color) { public GameState getGameEventType(Color color) {
return checkCheck(color) return checkCheck(color) ? checkCheckmate(color) ? GameState.CHECKMATE : GameState.CHECK
? checkCheckmate(color) ? GameState.CHECKMATE : GameState.CHECK : getMoves(color).isEmpty() || log.getLast().halfmoveClock >= 50 ? GameState.STALEMATE
: getMoves(color).isEmpty() ? GameState.STALEMATE : GameState.NORMAL; : GameState.NORMAL;
} }
/** /**
@ -154,22 +331,26 @@ public class Board implements Cloneable {
int score = 0; int score = 0;
for (int i = 0; i < 8; i++) for (int i = 0; i < 8; i++)
for (int j = 0; j < 8; j++) for (int j = 0; j < 8; j++)
if (boardArr[i][j] != null && boardArr[i][j].getColor() == color) switch (boardArr[i][j].getType()) { if (boardArr[i][j] != null && boardArr[i][j].getColor() == color) {
case QUEEN: switch (boardArr[i][j].getType()) {
score += 8; case QUEEN:
break; score += 90;
case ROOK: break;
score += 5; case ROOK:
break; score += 50;
case KNIGHT: break;
score += 3; case KNIGHT:
break; score += 30;
case BISHOP: break;
score += 3; case BISHOP:
break; score += 30;
case PAWN: break;
score += 1; case PAWN:
break; score += 10;
break;
}
if (positionScores.containsKey(boardArr[i][j].getType()))
score += positionScores.get(boardArr[i][j].getType())[i][color == Color.WHITE ? j : 7 - j];
} }
return score; return score;
} }
@ -177,7 +358,7 @@ public class Board implements Cloneable {
/** /**
* Initialized the board array with the default chess pieces and positions. * Initialized the board array with the default chess pieces and positions.
*/ */
public void initializeDefaultPositions() { public void initDefaultPositions() {
// Initialize pawns // Initialize pawns
for (int i = 0; i < 8; i++) { for (int i = 0; i < 8; i++) {
boardArr[i][1] = new Pawn(Color.BLACK, this); boardArr[i][1] = new Pawn(Color.BLACK, this);
@ -218,54 +399,204 @@ public class Board implements Cloneable {
for (int i = 0; i < 8; i++) for (int i = 0; i < 8; i++)
for (int j = 2; j < 6; j++) for (int j = 2; j < 6; j++)
boardArr[i][j] = null; boardArr[i][j] = null;
// Initialize castling rights
Map<Type, Boolean> whiteCastling = new HashMap<>(), blackCastling = new HashMap<>();
whiteCastling.put(Type.KING, true);
whiteCastling.put(Type.QUEEN, true);
blackCastling.put(Type.KING, true);
blackCastling.put(Type.QUEEN, true);
castlingRights.put(Color.WHITE, whiteCastling);
castlingRights.put(Color.BLACK, blackCastling);
log.reset();
} }
/** /**
* @return A new instance of this class with a shallow copy of both * Initialized the board with a position specified in a FEN-encoded string.
* {@code kingPos} and {code boardArr} *
* @param fen The FEN-encoded string representing target state of the board
*/ */
@Override public void initFromFEN(String fen) {
public Object clone() { String[] parts = fen.split(" ");
Board board = null; log.reset();
try {
board = (Board) super.clone(); // Piece placement (from white's perspective)
} catch (CloneNotSupportedException ex) { String[] rows = parts[0].split("/");
ex.printStackTrace(); for (int i = 0; i < 8; i++) {
} char[] places = rows[i].toCharArray();
board.boardArr = new Piece[8][8]; for (int j = 0, k = 0; k < places.length; j++, k++) {
for (int i = 0; i < 8; i++) if (Character.isDigit(places[k])) {
for (int j = 0; j < 8; j++) { for (int l = j; l < Character.digit(places[k], 10); l++, j++)
if (boardArr[i][j] == null) continue; boardArr[j][i] = null;
board.boardArr[i][j] = (Piece) boardArr[i][j].clone(); --j;
board.boardArr[i][j].board = board; } else {
Color color = Character.isUpperCase(places[k]) ? Color.WHITE : Color.BLACK;
switch (Character.toLowerCase(places[k])) {
case 'k':
boardArr[j][i] = new King(color, this);
kingPos.put(color, new Position(j, i));
break;
case 'q':
boardArr[j][i] = new Queen(color, this);
break;
case 'r':
boardArr[j][i] = new Rook(color, this);
break;
case 'n':
boardArr[j][i] = new Knight(color, this);
break;
case 'b':
boardArr[j][i] = new Bishop(color, this);
break;
case 'p':
boardArr[j][i] = new Pawn(color, this);
break;
default:
System.err.printf("Unknown character '%c' in board declaration of FEN string '%s'%n",
places[k],
fen);
}
}
} }
}
board.kingPos = new HashMap<>(); // Active color
board.kingPos.putAll(kingPos); log.setActiveColor(Color.fromFirstChar(parts[1].charAt(0)));
return board; // Castling rights
Map<Type, Boolean> whiteCastling = new HashMap<>(), blackCastling = new HashMap<>();
for (char c : parts[2].toCharArray())
switch (c) {
case 'K':
whiteCastling.put(Type.KING, true);
case 'Q':
whiteCastling.put(Type.QUEEN, true);
case 'k':
blackCastling.put(Type.KING, true);
case 'q':
blackCastling.put(Type.QUEEN, true);
case '-':
break;
default:
System.err
.printf("Unknown character '%c' in castling rights declaration of FEN string '%s'", c, fen);
}
castlingRights.put(Color.WHITE, whiteCastling);
castlingRights.put(Color.BLACK, blackCastling);
// En passant availability
if (!parts[3].equals("-")) log.setEnPassant(Position.fromSAN(parts[3]));
// Halfmove clock
log.setHalfmoveClock(Integer.parseInt(parts[4]));
// Fullmove counter
log.setFullmoveCounter(Integer.parseInt(parts[5]));
} }
/**
* @return a FEN-encoded string representing the board
*/
public String toFEN() {
StringBuilder sb = new StringBuilder();
// Piece placement (from white's perspective)
for (int i = 0; i < 8; i++) {
int emptyCount = 0;
for (int j = 0; j < 8; j++) {
final Piece piece = boardArr[j][i];
if (piece == null) ++emptyCount;
else {
if (emptyCount != 0) {
sb.append(emptyCount);
emptyCount = 0;
}
char p = boardArr[j][i].getType().firstChar();
sb.append(piece.getColor() == Color.WHITE ? Character.toUpperCase(p) : p);
}
}
if (emptyCount != 0) sb.append(emptyCount);
if (i < 7) sb.append('/');
}
// Active color
sb.append(" " + log.getActiveColor().firstChar());
// Castling Rights
sb.append(' ');
StringBuilder castlingSb = new StringBuilder();
if (castlingRights.get(Color.WHITE).get(Type.KING)) castlingSb.append('K');
if (castlingRights.get(Color.WHITE).get(Type.QUEEN)) castlingSb.append('Q');
if (castlingRights.get(Color.BLACK).get(Type.KING)) castlingSb.append('k');
if (castlingRights.get(Color.BLACK).get(Type.QUEEN)) castlingSb.append('q');
if (castlingSb.length() == 0) sb.append("-");
sb.append(castlingSb);
final MoveNode lastMove = log.getLast();
// En passant availability
sb.append(" " + (lastMove == null || lastMove.enPassant == null ? "-" : lastMove.enPassant.toSAN()));
// Halfmove clock
sb.append(" " + String.valueOf(lastMove == null ? 0 : lastMove.halfmoveClock));
// Fullmove counter
sb.append(" " + String.valueOf(lastMove == null ? 1 : lastMove.fullmoveCounter));
return sb.toString();
}
/**
* @param pos The position from which to return a piece
* @return The piece at the position
*/
public Piece get(Position pos) { public Piece get(Position pos) {
return boardArr[pos.x][pos.y]; return boardArr[pos.x][pos.y];
} }
/**
* 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) { public void set(Position pos, Piece piece) {
boardArr[pos.x][pos.y] = piece; boardArr[pos.x][pos.y] = piece;
} }
/**
* @param move The move from which position to return a piece
* @return The piece at the position of the move
*/
public Piece getPos(Move move) { public Piece getPos(Move move) {
return get(move.pos); return get(move.pos);
} }
/**
* @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) { public Piece getDest(Move move) {
return get(move.dest); return get(move.dest);
} }
/**
* 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) { public void setPos(Move move, Piece piece) {
set(move.pos, piece); set(move.pos, 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) { public void setDest(Move move, Piece piece) {
set(move.dest, piece); set(move.dest, piece);
} }
@ -274,4 +605,9 @@ public class Board implements Cloneable {
* @return The board array * @return The board array
*/ */
public Piece[][] getBoardArr() { return boardArr; } public Piece[][] getBoardArr() { return boardArr; }
/**
* @return The move log
*/
public Log getLog() { return log; }
} }

View File

@ -17,7 +17,39 @@ public class King extends Piece {
@Override @Override
public boolean isValidMove(Move move) { public boolean isValidMove(Move move) {
return move.xDist <= 1 && move.yDist <= 1 && isFreePath(move); // Castling
if (move.xDist == 2 && move.yDist == 0) {
if (canCastleKingside()) {
move.type = Move.Type.CASTLING;
return true;
}
if (canCastleQueenside()) {
move.type = Move.Type.CASTLING;
return true;
}
}
return move.xDist <= 1 && move.yDist <= 1 && checkDestination(move);
}
public boolean canCastleKingside() {
if (getMoveCounter() == 0) {
int y = getColor() == Color.WHITE ? 7 : 0;
Position rookPos = new Position(7, y);
Piece rook = board.get(rookPos);
return rook != null && rook.getType() == Type.ROOK && rook.getMoveCounter() == 0
&& isFreePath(new Move(new Position(4, y), new Position(6, y)));
} else return false;
}
public boolean canCastleQueenside() {
if (getMoveCounter() == 0) {
int y = getColor() == Color.WHITE ? 7 : 0;
Position rookPos = new Position(0, y);
Piece rook = board.get(rookPos);
return rook != null && rook.getType() == Type.ROOK && rook.getMoveCounter() == 0
&& isFreePath(new Move(new Position(4, y), new Position(1, y)));
} else return false;
} }
@Override @Override
@ -29,6 +61,12 @@ public class King extends Piece {
Move move = new Move(pos, new Position(i, j)); Move move = new Move(pos, new Position(i, j));
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) moves.add(move); if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) moves.add(move);
} }
// Castling
// TODO: Condition: cannot castle out of, through or into check
if (canCastleKingside()) moves.add(new Move(pos, new Position(6, pos.y), Move.Type.CASTLING));
if (canCastleQueenside()) moves.add(new Move(pos, new Position(2, pos.y), Move.Type.CASTLING));
return moves; return moves;
} }

View File

@ -17,7 +17,8 @@ public class Knight extends Piece {
@Override @Override
public boolean isValidMove(Move move) { 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.xDist - move.yDist) == 1
&& (move.xDist == 1 && move.yDist == 2 || move.xDist == 2 && move.yDist == 1) && checkDestination(move);
} }
private void checkAndInsertMove(List<Move> moves, Position pos, int offsetX, int offsetY) { private void checkAndInsertMove(List<Move> moves, Position pos, int offsetX, int offsetY) {

View File

@ -0,0 +1,200 @@
package dev.kske.chess.board;
import java.util.ArrayList;
import java.util.List;
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>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class Log {
private MoveNode root, current;
private Position enPassant;
private Color activeColor;
private int fullmoveCounter, 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;
activeColor = other.activeColor;
fullmoveCounter = other.fullmoveCounter;
halfmoveClock = other.halfmoveClock;
// The new root is the current node of the copied instance
if (!other.isEmpty()) {
root = new MoveNode(other.current, copyVariations);
root.parent = null;
current = root;
}
}
/**
* Adds a move to the move history and adjusts the log to the new position.
*
* @param move The move to log
* @param capturedPiece The piece captured with the move
* @param pawnMove {@code true} if the move was made by a pawn
*/
public void add(Move move, Piece capturedPiece, boolean pawnMove) {
enPassant = pawnMove && move.yDist == 2 ? new Position(move.pos.x, move.pos.y + move.ySign) : null;
if (activeColor == Color.BLACK) ++fullmoveCounter;
if (pawnMove || capturedPiece != null) halfmoveClock = 0;
else++halfmoveClock;
activeColor = activeColor.opposite();
final MoveNode leaf = new MoveNode(move, capturedPiece, enPassant, activeColor, fullmoveCounter, halfmoveClock);
if (isEmpty()) {
root = leaf;
current = leaf;
} else {
current.addVariation(leaf);
current = leaf;
}
}
/**
* Removed the last move from the log and adjusts its state to the previous
* move.
*/
public void removeLast() {
if (!isEmpty() && current.parent != null) {
current.parent.variations.remove(current);
current = current.parent;
activeColor = current.activeColor;
enPassant = current.enPassant;
fullmoveCounter = current.fullmoveCounter;
halfmoveClock = current.halfmoveClock;
} else reset();
}
public boolean isEmpty() { return root == null; }
/**
* Reverts the log to its initial state corresponding to the default board
* position.
*/
public void reset() {
root = null;
current = null;
enPassant = null;
activeColor = Color.WHITE;
fullmoveCounter = 1;
halfmoveClock = 0;
}
/**
* @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 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 getFullmoveCounter() { return fullmoveCounter; }
public void setFullmoveCounter(int fullmoveCounter) { this.fullmoveCounter = fullmoveCounter; }
public int getHalfmoveClock() { return halfmoveClock; }
public void setHalfmoveClock(int halfmoveClock) { this.halfmoveClock = halfmoveClock; }
public static class MoveNode {
public final Move move;
public final Piece capturedPiece;
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, Position enPassant, Color activeColor, int fullmoveCounter,
int halfmoveClock) {
this.move = move;
this.capturedPiece = capturedPiece;
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.enPassant, other.activeColor, other.fullmoveCounter,
other.halfmoveClock);
if (copyVariations && other.variations != null) {
if (variations == null) variations = new ArrayList<>();
other.variations.forEach(variation -> {
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; }
}
}

View File

@ -10,20 +10,35 @@ public class Move {
public final Position pos, dest; public final Position pos, dest;
public final int xDist, yDist, xSign, ySign; public final int xDist, yDist, xSign, ySign;
public Type type;
public Move(Position pos, Position dest) { public Move(Position pos, Position dest, Type type) {
this.pos = pos; this.pos = pos;
this.dest = dest; this.dest = dest;
this.type = type;
xDist = Math.abs(dest.x - pos.x); xDist = Math.abs(dest.x - pos.x);
yDist = Math.abs(dest.y - pos.y); yDist = Math.abs(dest.y - pos.y);
xSign = (int) Math.signum(dest.x - pos.x); xSign = (int) Math.signum(dest.x - pos.x);
ySign = (int) Math.signum(dest.y - pos.y); ySign = (int) Math.signum(dest.y - pos.y);
} }
public Move(Position pos, Position dest) {
this(pos, dest, Type.NORMAL);
}
public Move(int xPos, int yPos, int xDest, int yDest) { public Move(int xPos, int yPos, int xDest, int yDest) {
this(new Position(xPos, yPos), new Position(xDest, yDest)); this(new Position(xPos, yPos), new Position(xDest, yDest));
} }
public static Move fromSAN(String move) {
return new Move(Position.fromSAN(move.substring(0, 2)),
Position.fromSAN(move.substring(2)));
}
public String toSAN() {
return pos.toSAN() + dest.toSAN();
}
public boolean isHorizontal() { return yDist == 0; } public boolean isHorizontal() { return yDist == 0; }
public boolean isVertical() { return xDist == 0; } public boolean isVertical() { return xDist == 0; }
@ -34,4 +49,8 @@ public class Move {
public String toString() { public String toString() {
return String.format("%s -> %s", pos, dest); return String.format("%s -> %s", pos, dest);
} }
public static enum Type {
NORMAL, PAWN_PROMOTION, CASTLING, EN_PASSANT, UNKNOWN
}
} }

View File

@ -17,13 +17,25 @@ public class Pawn extends Piece {
@Override @Override
public boolean isValidMove(Move move) { public boolean isValidMove(Move move) {
// TODO: en passant, pawn promotion
boolean step = move.isVertical() && move.yDist == 1; boolean step = move.isVertical() && move.yDist == 1;
boolean doubleStep = move.isVertical() && move.yDist == 2; boolean doubleStep = move.isVertical() && move.yDist == 2;
boolean strafe = move.isDiagonal() && move.xDist == 1; boolean strafe = move.isDiagonal() && move.xDist == 1;
boolean enPassant = false;
if (getColor() == Color.WHITE) doubleStep &= move.pos.y == 6; if (getColor() == Color.WHITE) doubleStep &= move.pos.y == 6;
else doubleStep &= move.pos.y == 1; else doubleStep &= move.pos.y == 1;
return (step ^ doubleStep ^ strafe) && move.ySign == (getColor() == Color.WHITE ? -1 : 1) && isFreePath(move);
// Mark move as pawn promotion if necessary
if (move.ySign == 1 && move.pos.y == 6 || move.ySign == -1 && move.pos.y == 1)
move.type = Move.Type.PAWN_PROMOTION;
// Mark the move as en passant if necessary
if (strafe && move.dest.equals(board.getLog().getEnPassant())) {
enPassant = true;
move.type = Move.Type.EN_PASSANT;
}
return enPassant || (step ^ doubleStep ^ strafe) && move.ySign == (getColor() == Color.WHITE ? -1 : 1)
&& isFreePath(move);
} }
@Override @Override
@ -43,8 +55,6 @@ public class Pawn extends Piece {
int sign = getColor() == Color.WHITE ? -1 : 1; int sign = getColor() == Color.WHITE ? -1 : 1;
if (sign == -1 && pos.y == 1 || sign == 1 && pos.y == 7) return moves;
// Strafe left // Strafe left
if (pos.x > 0) { if (pos.x > 0) {
Move move = new Move(pos, new Position(pos.x - 1, pos.y + sign)); Move move = new Move(pos, new Position(pos.x - 1, pos.y + sign));
@ -68,6 +78,17 @@ public class Pawn extends Piece {
Move move = new Move(pos, new Position(pos.x, pos.y + 2 * sign)); Move move = new Move(pos, new Position(pos.x, pos.y + 2 * sign));
if (isFreePath(move)) moves.add(move); if (isFreePath(move)) moves.add(move);
} }
// Mark moves as pawn promotion if necessary
if (sign == 1 && pos.y == 6 || sign == -1 && pos.y == 1)
moves.parallelStream().forEach(m -> m.type = Move.Type.PAWN_PROMOTION);
// Add en passant move if necessary
if (board.getLog().getEnPassant() != null) {
Move move = new Move(pos, board.getLog().getEnPassant(), Move.Type.EN_PASSANT);
if (move.isDiagonal() && move.xDist == 1) moves.add(move);
}
return moves; return moves;
} }

View File

@ -11,8 +11,9 @@ import java.util.List;
*/ */
public abstract class Piece implements Cloneable { public abstract class Piece implements Cloneable {
protected Color color; private final Color color;
protected Board board; protected Board board;
private int moveCounter;
public Piece(Color color, Board board) { public Piece(Color color, Board board) {
this.color = color; this.color = color;
@ -22,11 +23,10 @@ public abstract class Piece implements Cloneable {
public List<Move> getMoves(Position pos) { public List<Move> getMoves(Position pos) {
List<Move> moves = getPseudolegalMoves(pos); List<Move> moves = getPseudolegalMoves(pos);
for (Iterator<Move> iterator = moves.iterator(); iterator.hasNext();) { for (Iterator<Move> iterator = moves.iterator(); iterator.hasNext();) {
Move move = iterator.next(); Move move = iterator.next();
Piece capturePiece = board.move(move); board.move(move);
if (board.checkCheck(getColor())) if (board.checkCheck(getColor())) iterator.remove();
iterator.remove(); board.revert();
board.revert(move, capturePiece);
} }
return moves; return moves;
} }
@ -35,8 +35,16 @@ public abstract class Piece implements Cloneable {
public abstract boolean isValidMove(Move move); public abstract boolean isValidMove(Move move);
/**
* Checks, if the squares between the position and the destination of a move are
* free.
*
* @param move The move to check
*/
protected boolean isFreePath(Move move) { protected boolean isFreePath(Move move) {
// Only check destination by default for (int i = move.pos.x + move.xSign, j = move.pos.y + move.ySign; i != move.dest.x
|| j != move.dest.y; i += move.xSign, j += move.ySign)
if (board.getBoardArr()[i][j] != null) return false;
return checkDestination(move); return checkDestination(move);
} }
@ -66,13 +74,38 @@ public abstract class Piece implements Cloneable {
public Color getColor() { return color; } public Color getColor() { return color; }
public int getMoveCounter() { return moveCounter; }
public void incMoveCounter() {
++moveCounter;
}
public void decMoveCounter() {
--moveCounter;
}
public static enum Type { public static enum Type {
KING, QUEEN, ROOK, KNIGHT, BISHOP, PAWN; KING, QUEEN, ROOK, KNIGHT, BISHOP, PAWN;
/**
* @return The first character of this {@link Type} in algebraic notation and lower case
*/
public char firstChar() {
return this == KNIGHT ? 'n' : Character.toLowerCase(this.toString().charAt(0));
}
} }
public static enum Color { public static enum Color {
WHITE, BLACK; 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() { public Color opposite() {
return this == WHITE ? BLACK : WHITE; return this == WHITE ? BLACK : WHITE;
} }

View File

@ -15,8 +15,36 @@ public class Position {
this.y = y; this.y = y;
} }
public static Position fromSAN(String pos) {
return new Position(pos.charAt(0) - 97, 8 - Character.getNumericValue(pos.charAt(1)));
}
public String toSAN() {
return String.valueOf((char) (x + 97)) + String.valueOf(8 - y);
}
@Override @Override
public String toString() { public String toString() {
return String.format("[%d, %d]", x, y); 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

@ -20,22 +20,6 @@ public class Queen extends Piece {
return ((move.isHorizontal() || move.isVertical()) || move.isDiagonal()) && isFreePath(move); return ((move.isHorizontal() || move.isVertical()) || move.isDiagonal()) && isFreePath(move);
} }
@Override
protected boolean isFreePath(Move move) {
if (move.isHorizontal()) {
for (int i = move.pos.x + move.xSign; i != move.dest.x; i += move.xSign)
if (board.getBoardArr()[i][move.pos.y] != null) return false;
} else if (move.isVertical()) {
for (int i = move.pos.y + move.ySign; i != move.dest.y; i += move.ySign)
if (board.getBoardArr()[move.pos.x][i] != null) return false;
} else {
for (int i = move.pos.x + move.xSign, j = move.pos.y
+ move.ySign; i != move.dest.x; i += move.xSign, j += move.ySign)
if (board.getBoardArr()[i][j] != null) return false;
}
return checkDestination(move);
}
@Override @Override
protected List<Move> getPseudolegalMoves(Position pos) { protected List<Move> getPseudolegalMoves(Position pos) {
List<Move> moves = new ArrayList<>(); List<Move> moves = new ArrayList<>();

View File

@ -20,18 +20,6 @@ public class Rook extends Piece {
return (move.isHorizontal() || move.isVertical()) && isFreePath(move); return (move.isHorizontal() || move.isVertical()) && isFreePath(move);
} }
@Override
protected boolean isFreePath(Move move) {
if (move.isHorizontal()) {
for (int i = move.pos.x + move.xSign; i != move.dest.x; i += move.xSign)
if (board.getBoardArr()[i][move.pos.y] != null) return false;
} else {
for (int i = move.pos.y + move.ySign; i != move.dest.y; i += move.ySign)
if (board.getBoardArr()[move.pos.x][i] != null) return false;
}
return checkDestination(move);
}
@Override @Override
protected List<Move> getPseudolegalMoves(Position pos) { protected List<Move> getPseudolegalMoves(Position pos) {
List<Move> moves = new ArrayList<>(); List<Move> moves = new ArrayList<>();

View File

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

View File

@ -0,0 +1,36 @@
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>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
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.board.Move;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>MoveEvent.java</strong><br>
* Created: <strong>7 Aug 2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class MoveEvent implements Event<Move> {
private final Move move;
public MoveEvent(Move move) {
this.move = move;
}
@Override
public Move getData() { return move; }
}

View File

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

@ -1,12 +1,22 @@
package dev.kske.chess.game; package dev.kske.chess.game;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import javax.swing.JOptionPane;
import dev.kske.chess.board.Board; import dev.kske.chess.board.Board;
import dev.kske.chess.board.GameState; import dev.kske.chess.board.GameState;
import dev.kske.chess.board.Move; import dev.kske.chess.board.Move;
import dev.kske.chess.board.Piece.Color; import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.event.EventBus;
import dev.kske.chess.event.MoveEvent;
import dev.kske.chess.game.ai.AIPlayer;
import dev.kske.chess.ui.BoardComponent; import dev.kske.chess.ui.BoardComponent;
import dev.kske.chess.ui.BoardPane;
import dev.kske.chess.ui.EngineUtil;
import dev.kske.chess.ui.EngineUtil.EngineInfo;
import dev.kske.chess.ui.OverlayComponent;
/** /**
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
@ -16,39 +26,119 @@ import dev.kske.chess.ui.BoardComponent;
*/ */
public class Game { public class Game {
private Map<Color, Player> players; private Map<Color, Player> players = new HashMap<>();
private Board board; private Board board;
private OverlayComponent overlayComponent;
private BoardComponent boardComponent; private BoardComponent boardComponent;
public Game(Map<Color, Player> players, BoardComponent boardComponent) { public Game(BoardPane boardPane, String whiteName, String blackName) {
this.players = players; board = new Board();
this.boardComponent = boardComponent; init(boardPane, whiteName, blackName);
this.board = boardComponent.getBoard(); }
public Game(BoardPane boardPane, String whiteName, String blackName, String fen) {
board = new Board(fen);
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 // Initialize the game variable in each player
players.values().forEach(player -> player.setGame(this)); players.values().forEach(player -> player.setGame(this));
} }
public void start() { private Player getPlayer(String name, Color color) {
players.get(Color.WHITE).requestMove(); 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) { public void onMove(Player player, Move move) {
if (board.getPos(move).getColor() == player.color && board.attemptMove(move)) { if (board.getPos(move).getColor() == player.color && board.attemptMove(move)) {
// Redraw
boardComponent.repaint();
overlayComponent.displayArrow(move);
// Run garbage collection
System.gc();
System.out.printf("%s: %s%n", player.color, move); System.out.printf("%s: %s%n", player.color, move);
System.out.println("FEN: " + board.toFEN());
EventBus.getInstance().dispatch(new MoveEvent(move));
GameState eventType = board.getGameEventType(board.getDest(move).getColor().opposite()); GameState eventType = board.getGameEventType(board.getDest(move).getColor().opposite());
switch (eventType) { switch (eventType) {
case CHECKMATE: case CHECKMATE:
case STALEMATE: case STALEMATE:
System.out.printf("%s in %s!%n", player.color.opposite(), eventType); String result = String.format("%s in %s!%n", player.color.opposite(), eventType);
System.out.print(result);
JOptionPane.showMessageDialog(boardComponent, result);
break; break;
case CHECK: case CHECK:
System.out.printf("%s in check!%n", player.color.opposite()); System.out.printf("%s in check!%n", player.color.opposite());
default: default:
boardComponent.repaint(); players.get(board.getLog().getActiveColor()).requestMove();
players.get(player.color.opposite()).requestMove();
} }
} else player.requestMove(); } else player.requestMove();
} }
public void start() {
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,13 @@
package dev.kske.chess.game; package dev.kske.chess.game;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent; import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import dev.kske.chess.board.Board; import dev.kske.chess.board.Board;
import dev.kske.chess.board.Move; import dev.kske.chess.board.Move;
import dev.kske.chess.board.Move.Type;
import dev.kske.chess.board.Piece.Color; import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.board.Position; import dev.kske.chess.board.Position;
import dev.kske.chess.ui.OverlayComponent; import dev.kske.chess.ui.OverlayComponent;
@ -17,47 +18,72 @@ import dev.kske.chess.ui.OverlayComponent;
* Created: <strong>06.07.2019</strong><br> * Created: <strong>06.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong> * Author: <strong>Kai S. K. Engelbart</strong>
*/ */
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) { private boolean moveRequested;
super(board, color); private Position pos;
moveRequested = false;
overlayComponent.addMouseListener(new MouseAdapter() {
private Position pos; public NaturalPlayer(Color color, OverlayComponent overlayComponent) {
super(color);
this.overlayComponent = overlayComponent;
name = "Player";
moveRequested = false;
@Override overlayComponent.addMouseListener(this);
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;
}
}
});
} }
@Override @Override
public void requestMove() { public void requestMove() {
moveRequested = true; moveRequested = true;
} }
@Override
public void cancelMove() {
moveRequested = false;
}
@Override
public void disconnect() {
overlayComponent.removeMouseListener(this);
}
@Override
public void mousePressed(MouseEvent evt) {
if (!moveRequested) return;
if (pos == null) {
pos = new Position(evt.getPoint().x / overlayComponent.getTileSize(),
evt.getPoint().y / overlayComponent.getTileSize());
Board board = new Board(this.board);
if (board.get(pos) != null && board.get(pos).getColor() == color) {
List<Position> positions = board.getMoves(pos)
.stream()
.map(move -> move.dest)
.collect(Collectors.toList());
overlayComponent.displayDots(positions);
} else pos = null;
} else {
Position dest = new Position(evt.getPoint().x / overlayComponent.getTileSize(),
evt.getPoint().y / overlayComponent.getTileSize());
overlayComponent.clearDots();
moveRequested = false;
game.onMove(NaturalPlayer.this, new Move(pos, dest, Type.UNKNOWN));
pos = null;
}
}
@Override
public void mouseClicked(MouseEvent e) {}
@Override
public void mouseReleased(MouseEvent e) {}
@Override
public void mouseEntered(MouseEvent e) {}
@Override
public void mouseExited(MouseEvent e) {}
} }

View File

@ -11,20 +11,27 @@ import dev.kske.chess.board.Piece.Color;
*/ */
public abstract class Player { public abstract class Player {
protected Game game; protected Game game;
protected Board board; protected Board board;
protected Color color; protected Color color;
protected String name;
public Player(Board board, Color color) { public Player(Color color) {
this.board = board; this.color = color;
this.color = color;
} }
public abstract void requestMove(); public abstract void requestMove();
public abstract void cancelMove();
public abstract void disconnect();
public Game getGame() { return game; } 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; } public Board getBoard() { return board; }
@ -33,4 +40,8 @@ public abstract class Player {
public Color getColor() { return color; } public Color getColor() { return color; }
public void setColor(Color color) { this.color = 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,92 @@
package dev.kske.chess.game;
import java.io.IOException;
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>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
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(board.toFEN());
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.fromSAN(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.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import javax.swing.SwingUtilities; import javax.swing.SwingUtilities;
@ -24,15 +25,23 @@ public class AIPlayer extends Player {
private int availableProcessors; private int availableProcessors;
private int maxDepth; private int maxDepth;
private int alphaBetaThreshold;
public AIPlayer(Board board, Color color, int maxDepth) { private volatile boolean exitRequested;
super(board, color); private volatile ExecutorService executor;
availableProcessors = Runtime.getRuntime().availableProcessors();
this.maxDepth = maxDepth; 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 @Override
public void requestMove() { public void requestMove() {
exitRequested = false;
/* /*
* Define some processing threads, split the available moves between them and * Define some processing threads, split the available moves between them and
* retrieve the result after their execution. * retrieve the result after their execution.
@ -41,8 +50,8 @@ public class AIPlayer extends Player {
/* /*
* Get a copy of the board and the available moves. * Get a copy of the board and the available moves.
*/ */
Board board = (Board) AIPlayer.this.board.clone(); Board board = new Board(this.board);
List<Move> moves = board.getMoves(color); List<Move> moves = board.getMoves(color);
/* /*
* Define move processors and split the available moves between them. * Define move processors and split the available moves between them.
@ -55,16 +64,16 @@ public class AIPlayer extends Player {
for (int i = 0; i < numThreads; i++) { for (int i = 0; i < numThreads; i++) {
if (rem-- > 0) ++endIndex; if (rem-- > 0) ++endIndex;
endIndex += step; endIndex += step;
processors.add( processors.add(new MoveProcessor(new Board(board), moves.subList(beginIndex, endIndex), color,
new MoveProcessor((Board) board.clone(), moves.subList(beginIndex, endIndex), color, maxDepth)); maxDepth, alphaBetaThreshold));
beginIndex = endIndex; beginIndex = endIndex;
} }
/* /*
* Execute processors, get the best result and pass it back to the Game class * Execute processors, get the best result and pass it back to the Game class
*/ */
ExecutorService executor = Executors.newFixedThreadPool(numThreads); executor = Executors.newFixedThreadPool(numThreads);
List<ProcessingResult> results = new ArrayList<>(numThreads); List<ProcessingResult> results = new ArrayList<>(numThreads);
try { try {
List<Future<ProcessingResult>> futures = executor.invokeAll(processors); List<Future<ProcessingResult>> futures = executor.invokeAll(processors);
for (Future<ProcessingResult> f : futures) for (Future<ProcessingResult> f : futures)
@ -74,7 +83,23 @@ public class AIPlayer extends Player {
ex.printStackTrace(); ex.printStackTrace();
} }
results.sort((r1, r2) -> Integer.compare(r2.score, r1.score)); 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(); }, "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

@ -5,7 +5,6 @@ import java.util.concurrent.Callable;
import dev.kske.chess.board.Board; import dev.kske.chess.board.Board;
import dev.kske.chess.board.Move; import dev.kske.chess.board.Move;
import dev.kske.chess.board.Piece;
import dev.kske.chess.board.Piece.Color; import dev.kske.chess.board.Piece.Color;
/** /**
@ -17,17 +16,19 @@ import dev.kske.chess.board.Piece.Color;
public class MoveProcessor implements Callable<ProcessingResult> { public class MoveProcessor implements Callable<ProcessingResult> {
private final Board board; private final Board board;
private final List<Move> rootMoves;; private final List<Move> rootMoves;
private final Color color; private final Color color;
private final int maxDepth; private final int maxDepth;
private final int alphaBetaThreshold;
private Move bestMove; private Move bestMove;
public MoveProcessor(Board board, List<Move> rootMoves, Color color, int maxDepth) { public MoveProcessor(Board board, List<Move> rootMoves, Color color, int maxDepth, int alphaBetaThreshold) {
this.board = board; this.board = board;
this.rootMoves = rootMoves; this.rootMoves = rootMoves;
this.color = color; this.color = color;
this.maxDepth = maxDepth; this.maxDepth = maxDepth;
this.alphaBetaThreshold = alphaBetaThreshold;
} }
@Override @Override
@ -39,12 +40,12 @@ public class MoveProcessor implements Callable<ProcessingResult> {
private int miniMax(Board board, List<Move> moves, Color color, int depth) { private int miniMax(Board board, List<Move> moves, Color color, int depth) {
int bestValue = Integer.MIN_VALUE; int bestValue = Integer.MIN_VALUE;
for (Move move : moves) { for (Move move : moves) {
Piece capturePiece = board.move(move); board.move(move);
int teamValue = board.evaluate(color); int teamValue = board.evaluate(color);
int enemyValue = board.evaluate(color.opposite()); int enemyValue = board.evaluate(color.opposite());
int valueChange = teamValue - enemyValue; 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); valueChange -= miniMax(board, board.getMoves(color.opposite()), color.opposite(), depth + 1);
if (valueChange > bestValue) { if (valueChange > bestValue) {
@ -52,7 +53,7 @@ public class MoveProcessor implements Callable<ProcessingResult> {
if (depth == 0) bestMove = move; if (depth == 0) bestMove = move;
} }
board.revert(move, capturePiece); board.revert();
} }
return bestValue; return bestValue;
} }

View File

@ -0,0 +1,143 @@
package dev.kske.chess.uci;
import java.io.IOException;
import java.io.PrintWriter;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>UCIHandle.java</strong><br>
* Created: <strong>18.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
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");
}
// TODO: position
/**
* Sets up the position in its initial state.
*/
public void startPosition() {
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);
}
// TODO: go with parameters
public void go() {
out.println("go");
}
/**
* 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,198 @@
package dev.kske.chess.uci;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import dev.kske.chess.board.Move;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>UCIInfo.java</strong><br>
* Created: <strong>28.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class UCIInfo {
private int depth, seldepth, time, nodes, multipv, currmovenumber, hashfull, nps, tbhits, sbhits, cpuload,
cpunr;
private List<Move> pv, refutation, currline;
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) {
pv = new ArrayList<>();
refutation = new ArrayList<>();
currline = new ArrayList<>();
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.fromSAN(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.fromSAN(tokens[i]));
break;
case "refutation":
while (++i < tokens.length && !params.contains(tokens[i]))
refutation.add(Move.fromSAN(tokens[i]));
break;
// TODO: currline
case "currline":
while (++i < tokens.length && !params.contains(tokens[i]))
;
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 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,94 @@
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>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
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,68 @@
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>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
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,143 @@
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>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
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.fromSAN(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,81 @@
package dev.kske.chess.ui;
import java.awt.Dimension;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JSpinner;
import javax.swing.SpinnerNumberModel;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>AIConfigDialog.java</strong><br>
* Created: <strong>16.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class AIConfigDialog extends JDialog {
private static final long serialVersionUID = -8047984368152479992L;
private int maxDepth;
private int alphaBetaThreshold;
private boolean startGame = false;
public AIConfigDialog() {
setSize(new Dimension(337, 212));
setModal(true);
setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
setTitle("AI Configuration");
getContentPane().setLayout(null);
JSpinner spAlphaBetaThreshold = new JSpinner();
spAlphaBetaThreshold.setBounds(222, 68, 95, 28);
getContentPane().add(spAlphaBetaThreshold);
spAlphaBetaThreshold.setModel(new SpinnerNumberModel(-10, -100, 100, 5));
JSpinner spMaxDepth = new JSpinner();
spMaxDepth.setBounds(222, 6, 95, 28);
getContentPane().add(spMaxDepth);
spMaxDepth.setModel(new SpinnerNumberModel(4, 1, 10, 1));
JLabel lblAlphabetaThreshold = new JLabel("Alpha-Beta Threshold:");
lblAlphabetaThreshold.setBounds(16, 68, 194, 28);
getContentPane().add(lblAlphabetaThreshold);
JButton btnOk = new JButton("OK");
btnOk.setBounds(16, 137, 84, 28);
getContentPane().add(btnOk);
btnOk.addActionListener((evt) -> {
maxDepth = ((Integer) spMaxDepth.getValue()).intValue();
alphaBetaThreshold = ((Integer) spAlphaBetaThreshold.getValue()).intValue();
startGame = true;
dispose();
});
btnOk.setToolTipText("Start the game");
JButton btnCancel = new JButton("Cancel");
btnCancel.setBounds(222, 137, 95, 28);
getContentPane().add(btnCancel);
btnCancel.addActionListener((evt) -> dispose());
btnCancel.setToolTipText("Cancel the game start");
JLabel lblMaximalRecursionDepth = new JLabel("Maximal Recursion Depth:");
lblMaximalRecursionDepth.setBounds(16, 12, 194, 16);
getContentPane().add(lblMaximalRecursionDepth);
setLocationRelativeTo(null);
}
public int getMaxDepth() { return maxDepth; }
public void setMaxDepth(int maxDepth) { this.maxDepth = maxDepth; }
public int getAlphaBetaThreshold() { return alphaBetaThreshold; }
public void setAlphaBetaThreshold(int alphaBetaThreshold) { this.alphaBetaThreshold = alphaBetaThreshold; }
public boolean isStartGame() { return startGame; }
public void setStartGame(boolean startGame) { this.startGame = startGame; }
}

View File

@ -1,8 +1,6 @@
package dev.kske.chess.ui; package dev.kske.chess.ui;
import java.awt.Dimension; import java.awt.Dimension;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import javax.swing.JLayeredPane; import javax.swing.JLayeredPane;
@ -24,23 +22,13 @@ public class BoardPane extends JLayeredPane {
public BoardPane() { public BoardPane() {
boardComponent = new BoardComponent(this); boardComponent = new BoardComponent(this);
overlayComponent = new OverlayComponent(this); overlayComponent = new OverlayComponent(this);
setLayer(overlayComponent, 1);
setLayout(null);
add(boardComponent, Integer.valueOf(1)); add(boardComponent);
add(overlayComponent, Integer.valueOf(2)); add(overlayComponent);
/* tileSize = 60;
* Add a component listener for adjusting the tile size on resizing.
* The size of the board is assumed to be 8x8, as well as the both the board and
* the tiles being square.
*/
addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
tileSize = getWidth() / 8;
TextureUtil.scalePieceTextures(tileSize);
}
});
setSize(getPreferredSize()); setSize(getPreferredSize());
} }

View File

@ -0,0 +1,98 @@
package dev.kske.chess.ui;
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>
* Author: <strong>Leon Hofmeister</strong>
*/
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,55 @@
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.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.List;
import dev.kske.chess.game.Game;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>FENDropTarget.java</strong><br>
* Created: <strong>13 Aug 2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class FENDropTarget extends DropTargetAdapter {
private MainWindow mainWindow;
public FENDropTarget(MainWindow mainWindow) {
this.mainWindow = mainWindow;
}
@SuppressWarnings("unchecked")
@Override
public void drop(DropTargetDropEvent evt) {
try {
evt.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
((List<File>) evt.getTransferable().getTransferData(DataFlavor.javaFileListFlavor)).forEach(file -> {
try (BufferedReader br = new BufferedReader(new FileReader(file))) {
final GamePane gamePane = mainWindow.addGamePane();
final String fen = br.readLine();
GameConfigurationDialog.show((whiteName, blackName) -> {
final Game game = new Game(gamePane.getBoardPane(), whiteName, blackName, fen);
gamePane.setGame(game);
game.start();
});
evt.dropComplete(true);
} catch (IOException e) {
e.printStackTrace();
evt.rejectDrop();
}
});
} catch (UnsupportedFlavorException | IOException ex) {
ex.printStackTrace();
evt.rejectDrop();
}
}
}

View File

@ -0,0 +1,76 @@
package dev.kske.chess.ui;
import java.awt.Font;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.BiConsumer;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JLabel;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>GameConfigurationDialog.java</strong><br>
* Created: <strong>24.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class GameConfigurationDialog {
private GameConfigurationDialog() {}
public static void show(BiConsumer<String, String> action) {
new JDialog() {
private static final long serialVersionUID = -5768339760489440385L;
{
setTitle("Game Configuration");
setBounds(100, 100, 281, 142);
setModal(true);
setLocationRelativeTo(null);
getContentPane().setLayout(null);
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);
getContentPane().add(lblWhite);
JComboBox<Object> cbWhite = new JComboBox<>();
cbWhite.setModel(new DefaultComboBoxModel<Object>(options.toArray()));
cbWhite.setBounds(98, 9, 159, 22);
getContentPane().add(cbWhite);
JLabel lblBlack = new JLabel("Black:");
lblBlack.setFont(new Font("Tahoma", Font.PLAIN, 14));
lblBlack.setBounds(10, 38, 49, 14);
getContentPane().add(lblBlack);
JComboBox<Object> cbBlack = new JComboBox<>();
cbBlack.setModel(new DefaultComboBoxModel<Object>(options.toArray()));
cbBlack.setBounds(98, 36, 159, 22);
getContentPane().add(cbBlack);
JButton btnStart = new JButton("Start");
btnStart.addActionListener((evt) -> {
dispose();
action.accept(options.get(cbWhite.getSelectedIndex()), options.get(cbBlack.getSelectedIndex()));
});
btnStart.setBounds(20, 73, 89, 23);
getContentPane().add(btnStart);
JButton btnCancel = new JButton("Cancel");
btnCancel.addActionListener((evt) -> dispose());
btnCancel.setBounds(157, 73, 89, 23);
getContentPane().add(btnCancel);
}
}.setVisible(true);
}
}

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,126 @@
package dev.kske.chess.ui;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import dev.kske.chess.board.Piece.Color;
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>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class GamePane extends JComponent {
private static final long serialVersionUID = 4349772338239617477L;
private JButton btnRestart, btnSwapColors;
private BoardPane boardPane;
private LogPanel logPanel;
private Game game;
private Color activeColor;
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, 0.0, Double.MIN_VALUE };
gridBagLayout.rowWeights = new double[] { 0.0, 0.0, 0.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;
add(toolPanel, gbc_toolPanel);
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);
}
// Initialize LogPanel
logPanel = new LogPanel();
GridBagConstraints gbc_logPanel = new GridBagConstraints();
gbc_logPanel.anchor = GridBagConstraints.EAST;
gbc_logPanel.fill = GridBagConstraints.VERTICAL;
gbc_logPanel.gridx = 2;
gbc_logPanel.gridy = 1;
add(logPanel, gbc_logPanel);
}
/**
* @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;
btnSwapColors.setEnabled(game.getPlayers().get(Color.WHITE) instanceof NaturalPlayer
^ game.getPlayers().get(Color.BLACK) instanceof NaturalPlayer);
logPanel.setLog(game.getBoard().getLog());
}
}

View File

@ -0,0 +1,76 @@
package dev.kske.chess.ui;
import java.awt.BorderLayout;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.border.EmptyBorder;
import javax.swing.table.DefaultTableModel;
import dev.kske.chess.board.Log;
import dev.kske.chess.board.Log.MoveNode;
import dev.kske.chess.event.Event;
import dev.kske.chess.event.EventBus;
import dev.kske.chess.event.MoveEvent;
import dev.kske.chess.event.Subscribable;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>LogPanel.java</strong><br>
* Created: <strong>17.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class LogPanel extends JPanel implements Subscribable {
private static final long serialVersionUID = 1932671698254197119L;
private JTable mtable;
private Log log;
/**
* Create the frame.
*/
public LogPanel() {
setBorder(new EmptyBorder(5, 5, 5, 5));
setLayout(new BorderLayout(0, 0));
mtable = new JTable();
mtable.setEnabled(false);
add(new JScrollPane(mtable), BorderLayout.CENTER);
EventBus.getInstance().register(this);
}
@Override
public Set<Class<?>> supports() {
return new HashSet<>(Arrays.asList(MoveEvent.class));
}
@Override
public void handle(Event<?> event) {
if (log == null) return;
// TODO: Display log with variations
final List<MoveNode> moves = /* log.getLoggedMoves() */ new ArrayList<>();
String[][] data = new String[moves.size() / 2 + moves.size() % 2][2];
for (int i = 0; i < data.length; i++) {
data[i][0] = moves.get(i * 2).move.toSAN();
if (i * 2 + 1 < moves.size()) data[i][1] = moves.get(i * 2 + 1).move.toSAN();
}
mtable.setModel(new DefaultTableModel(data, new String[] { "White", "Black" }));
}
public Log getLog() { return log; }
public void setLog(Log log) {
this.log = log;
handle(null);
}
}

View File

@ -1,14 +1,11 @@
package dev.kske.chess.ui; package dev.kske.chess.ui;
import java.awt.BorderLayout;
import java.awt.EventQueue; import java.awt.EventQueue;
import java.awt.Toolkit;
import java.awt.dnd.DropTarget;
import javax.swing.JButton;
import javax.swing.JFrame; import javax.swing.JFrame;
import javax.swing.JPanel; import javax.swing.JTabbedPane;
import dev.kske.chess.board.Board;
/** /**
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
@ -16,23 +13,21 @@ import dev.kske.chess.board.Board;
* Created: <strong>01.07.2019</strong><br> * Created: <strong>01.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong> * Author: <strong>Kai S. K. Engelbart</strong>
*/ */
public class MainWindow { public class MainWindow extends JFrame {
private JFrame mframe; private static final long serialVersionUID = -3100939302567978977L;
private JTabbedPane tabbedPane;
/** /**
* Launch the application. * Launch the application.
*/ */
public static void main(String[] args) { public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() { EventQueue.invokeLater(() -> {
try {
public void run() { new MainWindow();
try { } catch (Exception ex) {
MainWindow window = new MainWindow(); ex.printStackTrace();
window.mframe.setVisible(true);
} catch (Exception e) {
e.printStackTrace();
}
} }
}); });
} }
@ -41,6 +36,7 @@ public class MainWindow {
* Create the application. * Create the application.
*/ */
public MainWindow() { public MainWindow() {
super("Chess by Kai S. K. Engelbart");
initialize(); initialize();
} }
@ -48,24 +44,48 @@ public class MainWindow {
* Initialize the contents of the frame. * Initialize the contents of the frame.
*/ */
private void initialize() { private void initialize() {
mframe = new JFrame(); // Configure frame
mframe.setResizable(false); setResizable(false);
mframe.setBounds(100, 100, 494, 565); setBounds(100, 100, 494, 565);
mframe.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setIconImage(Toolkit.getDefaultToolkit().getImage(getClass().getResource("/pieces/queen_white.png")));
BoardPane boardPane = new BoardPane(); // Add frame content
boardPane.getBoardComponent().setBoard(new Board()); tabbedPane = new JTabbedPane();
mframe.getContentPane().add(boardPane, BorderLayout.CENTER); getContentPane().add(tabbedPane);
JPanel toolPanel = new JPanel(); setJMenuBar(new MenuBar(this));
mframe.getContentPane().add(toolPanel, BorderLayout.NORTH); new DropTarget(this, new FENDropTarget(this));
JButton btnRestart = new JButton("Restart"); // Update position and dimensions
btnRestart.addActionListener((evt) -> System.err.println("Resetting not implemented!")); pack();
toolPanel.add(btnRestart); setLocationRelativeTo(null);
mframe.pack(); 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.
*
* @return The new {@link GamePane}
*/
public GamePane addGamePane() {
GamePane gamePane = new GamePane();
tabbedPane.add("Game " + (tabbedPane.getComponentCount() + 1), gamePane);
tabbedPane.setSelectedIndex(tabbedPane.getComponentCount() - 1);
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);
} }
} }

View File

@ -0,0 +1,93 @@
package dev.kske.chess.ui;
import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import dev.kske.chess.game.Game;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>MenuBar.java</strong><br>
* Created: <strong>16.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
public class MenuBar extends JMenuBar {
private static final long serialVersionUID = -7221583703531248228L;
private final MainWindow mainWindow;
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) -> {
GameConfigurationDialog.show((whiteName, blackName) -> {
GamePane gamePane = mainWindow.addGamePane();
Game game = new Game(gamePane.getBoardPane(), whiteName, blackName);
gamePane.setGame(game);
game.start();
});
});
gameMenu.add(newGameMenuItem);
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 = mainWindow.getSelectedGamePane().getGame().getBoard().toFEN();
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: ");
GameConfigurationDialog.show((whiteName, blackName) -> {
final Game game = new Game(gamePane.getBoardPane(), whiteName, blackName, fen);
gamePane.setGame(game);
game.start();
});
});
toolsMenu.add(loadFromFENMenuItem);
add(toolsMenu);
}
}

View File

@ -1,12 +1,19 @@
package dev.kske.chess.ui; package dev.kske.chess.ui;
import java.awt.BasicStroke;
import java.awt.Color; import java.awt.Color;
import java.awt.Graphics; 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.ArrayList;
import java.util.List; import java.util.List;
import javax.swing.JComponent; import javax.swing.JComponent;
import dev.kske.chess.board.Move;
import dev.kske.chess.board.Position; import dev.kske.chess.board.Position;
/** /**
@ -22,11 +29,12 @@ public class OverlayComponent extends JComponent {
private final BoardPane boardPane; private final BoardPane boardPane;
private List<Position> dots; private List<Position> dots;
private Move arrow;
public OverlayComponent(BoardPane boardPane) { public OverlayComponent(BoardPane boardPane) {
this.boardPane = boardPane; this.boardPane = boardPane;
dots = new ArrayList<>();
setSize(boardPane.getPreferredSize()); setSize(boardPane.getPreferredSize());
dots = new ArrayList<>();
} }
@Override @Override
@ -34,7 +42,27 @@ public class OverlayComponent extends JComponent {
super.paintComponent(g); super.paintComponent(g);
final int tileSize = getTileSize(); 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.pos.x * tileSize + tileSize / 2, arrow.pos.y * tileSize + tileSize / 2);
Point dest = new Point(arrow.dest.x * tileSize + tileSize / 2, arrow.dest.y * tileSize + tileSize / 2);
Graphics2D g2d = (Graphics2D) g;
g2d.setStroke(new BasicStroke(3));
g2d.setColor(Color.yellow);
g2d.drawRect(arrow.pos.x * tileSize, arrow.pos.y * tileSize, tileSize, tileSize);
g2d.drawRect(arrow.dest.x * tileSize, arrow.dest.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 // Draw possible moves if a piece was selected
if (!dots.isEmpty()) { if (!dots.isEmpty()) {
g.setColor(Color.green); g.setColor(Color.green);
@ -47,6 +75,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) { public void displayDots(List<Position> dots) {
this.dots.clear(); this.dots.clear();
this.dots.addAll(dots); this.dots.addAll(dots);
@ -58,5 +116,15 @@ public class OverlayComponent extends JComponent {
repaint(); repaint();
} }
public void displayArrow(Move arrow) {
this.arrow = arrow;
repaint();
}
public void clearArrow() {
arrow = null;
repaint();
}
public int getTileSize() { return boardPane.getTileSize(); } public int getTileSize() { return boardPane.getTileSize(); }
} }

View File

@ -2,8 +2,8 @@ package dev.kske.chess.ui;
import java.awt.Image; import java.awt.Image;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -19,11 +19,11 @@ import dev.kske.chess.board.Piece;
*/ */
public class TextureUtil { public class TextureUtil {
private static Map<String, Image> textures; private static Map<String, Image> textures = new HashMap<>(), scaledTextures = new HashMap<>();
static { static {
textures = new HashMap<>();
loadPieceTextures(); loadPieceTextures();
scaledTextures.putAll(textures);
} }
private TextureUtil() {} private TextureUtil() {}
@ -36,26 +36,28 @@ public class TextureUtil {
*/ */
public static Image getPieceTexture(Piece piece) { public static Image getPieceTexture(Piece piece) {
String key = piece.getType().toString().toLowerCase() + "_" + piece.getColor().toString().toLowerCase(); String key = piece.getType().toString().toLowerCase() + "_" + piece.getColor().toString().toLowerCase();
return textures.get(key); return scaledTextures.get(key);
} }
/** /**
* Scales all piece textures to fit the current tile size * Scales all piece textures to fit the current tile size
*/ */
public static void scalePieceTextures(int scale) { public static void scalePieceTextures(int scale) {
textures.replaceAll((key, img) -> img.getScaledInstance(scale, scale, Image.SCALE_SMOOTH)); scaledTextures.clear();
textures
.forEach((key, img) -> scaledTextures.put(key, img.getScaledInstance(scale, scale, Image.SCALE_SMOOTH)));
} }
/** /**
* Loads an image from a file. * Loads an image from a file in the resource folder.
* *
* @param file The image file * @param fileName The name of the image resource
* @return The loaded image * @return The loaded image
*/ */
private static Image loadImage(File file) { private static Image loadImage(String fileName) {
BufferedImage in = null; BufferedImage in = null;
try { try {
in = ImageIO.read(file); in = ImageIO.read(TextureUtil.class.getResourceAsStream(fileName));
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
} }
@ -67,10 +69,20 @@ public class TextureUtil {
* The filenames without extensions are used as keys in the map textures. * The filenames without extensions are used as keys in the map textures.
*/ */
private static void loadPieceTextures() { private static void loadPieceTextures() {
File dir = new File("res/pieces"); Arrays
File[] files = dir.listFiles((File parentDir, String name) -> name.toLowerCase().endsWith(".png")); .asList("king_white",
for (File file : files) "king_black",
textures.put(file.getName().replaceFirst("[.][^.]+$", ""), TextureUtil.loadImage(file)); "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

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

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.getFullmoveCounter(), 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.fromSAN("a2a4"), null, true);
log.add(Move.fromSAN("a4a5"), null, true);
other.add(Move.fromSAN("a2a4"), null, true);
other.add(Move.fromSAN("a4a5"), null, true);
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#getFullmoveCounter()}.
*/
@Test
void testGetFullmoveCounter() {
fail("Not yet implemented");
}
/**
* Test method for {@link dev.kske.chess.board.Log#setFullmoveCounter(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#fromSAN(java.lang.String)}.
*/
@Test
void testFromSAN() {
for (int i = 0; i < n; i++)
assertEquals(positions[i], Position.fromSAN(sans[i]));
}
/**
* Test method for {@link dev.kske.chess.board.Position#toSAN()}.
*/
@Test
void testToSAN() {
for (int i = 0; i < n; i++)
assertEquals(sans[i], positions[i].toSAN());
}
/**
* 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());
}
}