54 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
41 changed files with 2257 additions and 298 deletions

View File

@ -7,11 +7,7 @@
<attribute name="test" value="true"/> <attribute name="test" value="true"/>
</attributes> </attributes>
</classpathentry> </classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER">
<attributes>
<attribute name="module" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/5"/> <classpathentry kind="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>

1
.gitignore vendored
View File

@ -22,3 +22,4 @@ local.properties
# 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,7 +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.LoggedMove; 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;
@ -15,11 +15,12 @@ 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 Log log; private Map<Color, Map<Type, Boolean>> castlingRights = new HashMap<>();
private Log log = new Log();
private static final Map<Type, int[][]> positionScores; private static final Map<Type, int[][]> positionScores;
@ -58,11 +59,44 @@ public class Board implements Cloneable {
new int[] { 0, 1, 1, -2, -2, 1, 1, 0 }, new int[] { 0, 0, 0, 0, 0, 0, 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<>(); }
log = new Log();
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);
} }
/** /**
@ -95,7 +129,6 @@ 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 void move(Move move) { public void move(Move move) {
Piece piece = getPos(move); Piece piece = getPos(move);
@ -107,6 +140,11 @@ public class Board implements Cloneable {
// TODO: Select promotion // TODO: Select promotion
setDest(move, new Queen(piece.getColor(), this)); setDest(move, new Queen(piece.getColor(), this));
break; break;
case EN_PASSANT:
setDest(move, piece);
setPos(move, null);
boardArr[move.dest.x][move.dest.y - move.ySign] = null;
break;
case CASTLING: case CASTLING:
// Move the king // Move the king
setDest(move, piece); setDest(move, piece);
@ -135,26 +173,34 @@ public class Board implements Cloneable {
// Increment move counter // Increment move counter
getDest(move).incMoveCounter(); getDest(move).incMoveCounter();
// Update the king's position if the moved piece is the king // 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);
// Update log // Update log
log.add(move, capturePiece); log.add(move, capturePiece, piece.getType() == Type.PAWN);
updateCastlingRights();
} }
/** /**
* Reverts the last move. * Reverts the last move.
*/ */
public void revert() { public void revert() {
LoggedMove loggedMove = log.getLast(); MoveNode moveNode = log.getLast();
Move move = loggedMove.move; Move move = moveNode.move;
Piece capturedPiece = loggedMove.capturedPiece; Piece capturedPiece = moveNode.capturedPiece;
switch (move.type) { switch (move.type) {
case PAWN_PROMOTION: case PAWN_PROMOTION:
setPos(move, new Pawn(getDest(move).getColor(), this)); setPos(move, new Pawn(getDest(move).getColor(), this));
setDest(move, capturedPiece); setDest(move, capturedPiece);
break; 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: case CASTLING:
// Move the king // Move the king
setPos(move, getDest(move)); setPos(move, getDest(move));
@ -163,8 +209,6 @@ public class Board implements Cloneable {
// Move the rook // Move the rook
Move rookMove = move.dest.x == 6 ? new Move(5, move.pos.y, 7, move.pos.y) // Kingside 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 : new Move(3, move.pos.y, 0, move.pos.y); // Queenside
// Move the rook
setDest(rookMove, getPos(rookMove)); setDest(rookMove, getPos(rookMove));
setPos(rookMove, null); setPos(rookMove, null);
@ -188,6 +232,30 @@ public class Board implements Cloneable {
// Update log // Update log
log.removeLast(); 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);
}
} }
/** /**
@ -249,7 +317,8 @@ public class Board implements Cloneable {
public GameState getGameEventType(Color color) { public GameState getGameEventType(Color color) {
return checkCheck(color) ? checkCheckmate(color) ? GameState.CHECKMATE : GameState.CHECK return checkCheck(color) ? checkCheckmate(color) ? GameState.CHECKMATE : GameState.CHECK
: getMoves(color).isEmpty() ? GameState.STALEMATE : GameState.NORMAL; : getMoves(color).isEmpty() || log.getLast().halfmoveClock >= 50 ? GameState.STALEMATE
: GameState.NORMAL;
} }
/** /**
@ -289,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);
@ -330,56 +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)));
board.log = (Log) log.clone(); // 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);
return board; // 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);
} }
@ -388,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

@ -18,27 +18,40 @@ public class King extends Piece {
@Override @Override
public boolean isValidMove(Move move) { public boolean isValidMove(Move move) {
// Castling // Castling
if (getMoveCounter() == 0 && move.xDist == 2 && move.yDist == 0) { if (move.xDist == 2 && move.yDist == 0) {
if (canCastleKingside()) {
// Kingside
if (board.getBoardArr()[7][move.pos.y] != null && board.getBoardArr()[7][move.pos.y].getType() == Type.ROOK
&& isFreePath(new Move(new Position(5, move.pos.y), new Position(7, move.pos.y)))) {
move.type = Move.Type.CASTLING; move.type = Move.Type.CASTLING;
return true; return true;
} }
if (canCastleQueenside()) {
// Queenside
if (board.getBoardArr()[0][move.pos.y] != null && board.getBoardArr()[0][move.pos.y].getType() == Type.ROOK
&& isFreePath(new Move(new Position(1, move.pos.y), new Position(4, move.pos.y)))) {
move.type = Move.Type.CASTLING; move.type = Move.Type.CASTLING;
return true; return true;
} }
} }
return move.xDist <= 1 && move.yDist <= 1 && checkDestination(move); 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
protected List<Move> getPseudolegalMoves(Position pos) { protected List<Move> getPseudolegalMoves(Position pos) {
List<Move> moves = new ArrayList<>(); List<Move> moves = new ArrayList<>();
@ -50,31 +63,13 @@ public class King extends Piece {
} }
// Castling // Castling
// TODO: Check attacked squares in between // TODO: Condition: cannot castle out of, through or into check
// TODO: Castling out of check? if (canCastleKingside()) moves.add(new Move(pos, new Position(6, pos.y), Move.Type.CASTLING));
if (getMoveCounter() == 0) { if (canCastleQueenside()) moves.add(new Move(pos, new Position(2, pos.y), Move.Type.CASTLING));
// Kingside
if (board.getBoardArr()[7][pos.y] != null && board.getBoardArr()[7][pos.y].getType() == Type.ROOK
&& isFreePath(new Move(new Position(5, pos.y), new Position(7, pos.y))))
moves.add(new Move(pos, new Position(6, pos.y), Move.Type.CASTLING));
// Queenside
if (board.getBoardArr()[0][pos.y] != null && board.getBoardArr()[0][pos.y].getType() == Type.ROOK
&& isFreePath(new Move(new Position(1, pos.y), new Position(4, pos.y))))
moves.add(new Move(pos, new Position(2, pos.y), Move.Type.CASTLING));
}
return moves; return moves;
} }
@Override
protected boolean isFreePath(Move move) {
for (int i = move.pos.x, j = move.pos.y; i != move.dest.x || j != move.dest.y; i += move.xSign, j += move.ySign)
if (board.getBoardArr()[i][j] != null) return false;
return true;
}
@Override @Override
public Type getType() { return Type.KING; } public Type getType() { return Type.KING; }
} }

View File

@ -3,53 +3,198 @@ package dev.kske.chess.board;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import dev.kske.chess.board.Piece.Color;
/** /**
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
* File: <strong>Log.java</strong><br> * File: <strong>Log.java</strong><br>
* Created: <strong>09.07.2019</strong><br> * Created: <strong>09.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong> * Author: <strong>Kai S. K. Engelbart</strong>
*/ */
public class Log implements Cloneable { public class Log {
private List<LoggedMove> moves; private MoveNode root, current;
private Position enPassant;
private Color activeColor;
private int fullmoveCounter, halfmoveClock;
public Log() { public Log() {
moves = new ArrayList<>(); reset();
} }
public void add(Move move, Piece capturedPiece) { /**
moves.add(new LoggedMove(move, capturedPiece)); * 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;
public LoggedMove getLast() { // The new root is the current node of the copied instance
return moves.get(moves.size() - 1); if (!other.isEmpty()) {
} root = new MoveNode(other.current, copyVariations);
root.parent = null;
public void removeLast() { current = root;
if (!moves.isEmpty()) moves.remove(moves.size() - 1);
}
@Override
public Object clone() {
Log log = null;
try {
log = (Log) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
} }
log.moves = new ArrayList<>();
log.moves.addAll(this.moves);
return log;
} }
public static class LoggedMove { /**
* 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 Move move;
public final Piece capturedPiece; public final Piece capturedPiece;
public final Position enPassant;
public final Color activeColor;
public final int fullmoveCounter, halfmoveClock;
public LoggedMove(Move move, Piece capturedPiece) { private MoveNode parent;
this.move = move; private List<MoveNode> variations;
this.capturedPiece = capturedPiece;
/**
* 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

@ -30,6 +30,15 @@ public class Move {
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; }

View File

@ -17,10 +17,10 @@ public class Pawn extends Piece {
@Override @Override
public boolean isValidMove(Move move) { public boolean isValidMove(Move move) {
// TODO: en passant
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;
@ -28,7 +28,14 @@ public class Pawn extends Piece {
if (move.ySign == 1 && move.pos.y == 6 || move.ySign == -1 && move.pos.y == 1) if (move.ySign == 1 && move.pos.y == 6 || move.ySign == -1 && move.pos.y == 1)
move.type = Move.Type.PAWN_PROMOTION; move.type = Move.Type.PAWN_PROMOTION;
return (step ^ doubleStep ^ strafe) && move.ySign == (getColor() == Color.WHITE ? -1 : 1) && isFreePath(move); // 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
@ -72,10 +79,16 @@ public class Pawn extends Piece {
if (isFreePath(move)) moves.add(move); if (isFreePath(move)) moves.add(move);
} }
// Mark moves as pawn promotions if necessary // Mark moves as pawn promotion if necessary
if (sign == 1 && pos.y == 6 || sign == -1 && pos.y == 1) if (sign == 1 && pos.y == 6 || sign == -1 && pos.y == 1)
moves.parallelStream().forEach(m -> m.type = Move.Type.PAWN_PROMOTION); 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

@ -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);
} }
@ -77,12 +85,27 @@ public abstract class Piece implements Cloneable {
} }
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

@ -3,13 +3,19 @@ package dev.kske.chess.game;
import java.util.HashMap; 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.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.BoardPane;
import dev.kske.chess.ui.EngineUtil;
import dev.kske.chess.ui.EngineUtil.EngineInfo;
import dev.kske.chess.ui.OverlayComponent; import dev.kske.chess.ui.OverlayComponent;
/** /**
@ -20,85 +26,119 @@ import dev.kske.chess.ui.OverlayComponent;
*/ */
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 OverlayComponent overlayComponent;
private BoardComponent boardComponent; private BoardComponent boardComponent;
public Game(Map<Color, Player> players, BoardPane boardPane) { public Game(BoardPane boardPane, String whiteName, String blackName) {
this.players = players; board = new Board();
this.overlayComponent = boardPane.getOverlayComponent(); init(boardPane, whiteName, blackName);
this.boardComponent = boardPane.getBoardComponent(); }
this.board = new Board();
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); 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 static Game createNatural(BoardPane boardPane) { private Player getPlayer(String name, Color color) {
Map<Color, Player> players = new HashMap<>(); switch (name) {
OverlayComponent overlay = boardPane.getOverlayComponent(); case "Natural Player":
return new NaturalPlayer(color, overlayComponent);
players.put(Color.WHITE, new NaturalPlayer(Color.WHITE, overlay)); case "AI Player":
players.put(Color.BLACK, new NaturalPlayer(Color.BLACK, overlay)); return new AIPlayer(color, 4, -10);
return new Game(players, boardPane); default:
} for (EngineInfo info : EngineUtil.getEngineInfos())
if (info.name.equals(name)) return new UCIPlayer(color, info.path);
public static Game createNaturalVsAI(BoardPane boardPane, int maxDepth, int alphaBeta) { System.err.println("Invalid player name: " + name);
Map<Color, Player> players = new HashMap<>(); return null;
OverlayComponent overlay = boardPane.getOverlayComponent(); }
players.put(Color.WHITE, new NaturalPlayer(Color.WHITE, overlay));
players.put(Color.BLACK, new AIPlayer(Color.BLACK, maxDepth, alphaBeta));
return new Game(players, boardPane);
}
public static Game createAIVsAI(BoardPane boardPane, int maxDepthW, int maxDepthB, int alphaBetaW, int alphaBetaB) {
Map<Color, Player> players = new HashMap<>();
players.put(Color.WHITE, new AIPlayer(Color.WHITE, maxDepthW, alphaBetaW));
players.put(Color.BLACK, new AIPlayer(Color.BLACK, maxDepthB, alphaBetaB));
return new Game(players, boardPane);
} }
public void onMove(Player player, Move move) { 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();
} }
overlayComponent.displayArrow(move);
} else player.requestMove(); } else player.requestMove();
} }
public void start() { public void start() {
players.get(Color.WHITE).requestMove(); players.get(board.getLog().getActiveColor()).requestMove();
} }
public void reset() { public void reset() {
players.forEach((k, v) -> v.cancelMove()); players.values().forEach(Player::cancelMove);
board.initializeDefaultPositions(); board.initDefaultPositions();
boardComponent.repaint(); boardComponent.repaint();
overlayComponent.clearDots(); overlayComponent.clearDots();
overlayComponent.clearArrow(); overlayComponent.clearArrow();
} }
/** /**
* Removed all connections between the game and the ui. * Stops the game by disconnecting its players form the UI.
*/ */
public void disconnect() { public void stop() {
players.values().forEach(Player::disconnect); 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; } public Board getBoard() { return board; }
/**
* @return The players participating in this game
*/
public Map<Color, Player> getPlayers() { return players; }
} }

View File

@ -28,6 +28,7 @@ public class NaturalPlayer extends Player implements MouseListener {
public NaturalPlayer(Color color, OverlayComponent overlayComponent) { public NaturalPlayer(Color color, OverlayComponent overlayComponent) {
super(color); super(color);
this.overlayComponent = overlayComponent; this.overlayComponent = overlayComponent;
name = "Player";
moveRequested = false; moveRequested = false;
overlayComponent.addMouseListener(this); overlayComponent.addMouseListener(this);
@ -55,7 +56,7 @@ public class NaturalPlayer extends Player implements MouseListener {
pos = new Position(evt.getPoint().x / overlayComponent.getTileSize(), pos = new Position(evt.getPoint().x / overlayComponent.getTileSize(),
evt.getPoint().y / overlayComponent.getTileSize()); evt.getPoint().y / overlayComponent.getTileSize());
Board board = (Board) NaturalPlayer.this.board.clone(); Board board = new Board(this.board);
if (board.get(pos) != null && board.get(pos).getColor() == color) { if (board.get(pos) != null && board.get(pos).getColor() == color) {
List<Position> positions = board.getMoves(pos) List<Position> positions = board.getMoves(pos)
.stream() .stream()

View File

@ -11,9 +11,10 @@ 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(Color color) { public Player(Color color) {
this.color = color; this.color = color;
@ -39,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

@ -32,6 +32,7 @@ public class AIPlayer extends Player {
public AIPlayer(Color color, int maxDepth, int alphaBetaThreshold) { public AIPlayer(Color color, int maxDepth, int alphaBetaThreshold) {
super(color); super(color);
name = "AIPlayer";
availableProcessors = Runtime.getRuntime().availableProcessors(); availableProcessors = Runtime.getRuntime().availableProcessors();
this.maxDepth = maxDepth; this.maxDepth = maxDepth;
this.alphaBetaThreshold = alphaBetaThreshold; this.alphaBetaThreshold = alphaBetaThreshold;
@ -49,7 +50,7 @@ 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);
/* /*
@ -63,7 +64,7 @@ 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(new MoveProcessor((Board) board.clone(), moves.subList(beginIndex, endIndex), color, processors.add(new MoveProcessor(new Board(board), moves.subList(beginIndex, endIndex), color,
maxDepth, alphaBetaThreshold)); maxDepth, alphaBetaThreshold));
beginIndex = endIndex; beginIndex = endIndex;
} }

View File

@ -24,10 +24,10 @@ public class MoveProcessor implements Callable<ProcessingResult> {
private Move bestMove; private Move bestMove;
public MoveProcessor(Board board, List<Move> rootMoves, Color color, int maxDepth, int alphaBetaThreshold) { 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; this.alphaBetaThreshold = alphaBetaThreshold;
} }
@ -41,9 +41,9 @@ public class MoveProcessor implements Callable<ProcessingResult> {
int bestValue = Integer.MIN_VALUE; int bestValue = Integer.MIN_VALUE;
for (Move move : moves) { for (Move move : moves) {
board.move(move); board.move(move);
int teamValue = 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 >= alphaBetaThreshold) if (depth < maxDepth && valueChange >= alphaBetaThreshold)
valueChange -= miniMax(board, board.getMoves(color.opposite()), color.opposite(), depth + 1); valueChange -= miniMax(board, board.getMoves(color.opposite()), color.opposite(), depth + 1);

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

@ -78,5 +78,4 @@ public class AIConfigDialog extends JDialog {
public boolean isStartGame() { return startGame; } public boolean isStartGame() { return startGame; }
public void setStartGame(boolean startGame) { this.startGame = 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

@ -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.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.game.Game;
/** /**
* Project: <strong>Chess</strong><br> * Project: <strong>Chess</strong><br>
@ -16,25 +13,21 @@ import dev.kske.chess.game.Game;
* 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 BoardPane boardPane;
private Game game; 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();
}
} }
}); });
} }
@ -43,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();
} }
@ -50,31 +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")));
mframe.setIconImage(Toolkit.getDefaultToolkit().getImage(getClass().getResource("/pieces/king_white.png"))); // Add frame content
tabbedPane = new JTabbedPane();
getContentPane().add(tabbedPane);
boardPane = new BoardPane(); setJMenuBar(new MenuBar(this));
mframe.getContentPane().add(boardPane, BorderLayout.CENTER); new DropTarget(this, new FENDropTarget(this));
mframe.setJMenuBar(new MenuBar(this)); // Update position and dimensions
pack();
JPanel toolPanel = new JPanel(); setLocationRelativeTo(null);
mframe.getContentPane().add(toolPanel, BorderLayout.NORTH); setVisible(true);
JButton btnRestart = new JButton("Restart");
btnRestart.addActionListener((evt) -> { if (game != null) game.reset(); game.start(); });
toolPanel.add(btnRestart);
mframe.pack();
mframe.setLocationRelativeTo(null);
} }
public BoardPane getBoardPane() { return boardPane; } /**
* @return The currently selected {@link GamePane} component
*/
public GamePane getSelectedGamePane() { return (GamePane) tabbedPane.getSelectedComponent(); }
public Game getGame() { return game; } /**
* 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;
}
public void setGame(Game game) { this.game = game; } /**
* 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

@ -1,8 +1,12 @@
package dev.kske.chess.ui; package dev.kske.chess.ui;
import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
import javax.swing.JMenu; import javax.swing.JMenu;
import javax.swing.JMenuBar; import javax.swing.JMenuBar;
import javax.swing.JMenuItem; import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import dev.kske.chess.game.Game; import dev.kske.chess.game.Game;
@ -16,49 +20,74 @@ public class MenuBar extends JMenuBar {
private static final long serialVersionUID = -7221583703531248228L; private static final long serialVersionUID = -7221583703531248228L;
private final MainWindow mainWindow; private final MainWindow mainWindow;
private final BoardPane boardPane;
public MenuBar(MainWindow mainWindow) { public MenuBar(MainWindow mainWindow) {
this.mainWindow = mainWindow; this.mainWindow = mainWindow;
boardPane = mainWindow.getBoardPane();
initGameMenu(); initGameMenu();
initEngineMenu();
initToolsMenu();
} }
private void initGameMenu() { private void initGameMenu() {
JMenu gameMenu = new JMenu("Game"); 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();
}
JMenuItem naturalMenuItem = new JMenuItem("Game against natural opponent"); private void initEngineMenu() {
JMenuItem aiMenuItem = new JMenuItem("Game against artificial opponent"); JMenu engineMenu = new JMenu("Engine");
JMenuItem aiVsAiMenuItem = new JMenuItem("Watch AI vs. AI");
naturalMenuItem.addActionListener((evt) -> startGame(Game.createNatural(boardPane))); JMenuItem addEngineMenuItem = new JMenuItem("Add engine");
addEngineMenuItem.addActionListener((evt) -> {
aiMenuItem.addActionListener((evt) -> { String enginePath = JOptionPane.showInputDialog(getParent(),
AIConfigDialog dialog = new AIConfigDialog(); "Enter the path to a UCI-compatible chess engine:",
dialog.setVisible(true); "Engine selection",
if (dialog.isStartGame()) JOptionPane.QUESTION_MESSAGE);
startGame(Game.createNaturalVsAI(boardPane, dialog.getMaxDepth(), dialog.getAlphaBetaThreshold())); if (enginePath != null) EngineUtil.addEngine(enginePath);
}); });
aiVsAiMenuItem.addActionListener((evt) -> startGame(Game.createAIVsAI(boardPane, 4, 3, -10, -10))); JMenuItem showInfoMenuItem = new JMenuItem("Show engine info");
gameMenu.add(naturalMenuItem); engineMenu.add(addEngineMenuItem);
gameMenu.add(aiMenuItem); engineMenu.add(showInfoMenuItem);
gameMenu.add(aiVsAiMenuItem); add(engineMenu);
add(gameMenu);
// Start a game
naturalMenuItem.doClick();
} }
private void startGame(Game game) { private void initToolsMenu() {
mainWindow.setGame(game); JMenu toolsMenu = new JMenu("Tools");
// Update board and board component JMenuItem exportFENMenuItem = new JMenuItem("Export board to FEN");
game.reset(); exportFENMenuItem.addActionListener((evt) -> {
game.start(); 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,5 +1,6 @@
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.Graphics2D;
@ -32,8 +33,8 @@ public class OverlayComponent extends JComponent {
public OverlayComponent(BoardPane boardPane) { public OverlayComponent(BoardPane boardPane) {
this.boardPane = boardPane; this.boardPane = boardPane;
setSize(boardPane.getPreferredSize());
dots = new ArrayList<>(); dots = new ArrayList<>();
setSize(boardPane.getPreferredSize());
} }
@Override @Override
@ -42,6 +43,26 @@ public class OverlayComponent extends JComponent {
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);
@ -52,13 +73,6 @@ public class OverlayComponent extends JComponent {
radius, radius,
radius); radius);
} }
if (arrow != null) {
g.setColor(new Color(255, 0, 0, 127));
Point pos = new Point(arrow.pos.x * tileSize + tileSize / 2, arrow.pos.y * tileSize + tileSize / 2);
Point dest = new Point(arrow.dest.x * tileSize + tileSize / 2, arrow.dest.y * tileSize + tileSize / 2);
((Graphics2D) g).fill(createArrowShape(pos, dest));
}
} }
private Shape createArrowShape(Point pos, Point dest) { private Shape createArrowShape(Point pos, Point dest) {

View File

@ -19,11 +19,9 @@ import dev.kske.chess.board.Piece;
*/ */
public class TextureUtil { public class TextureUtil {
private static Map<String, Image> textures, scaledTextures; private static Map<String, Image> textures = new HashMap<>(), scaledTextures = new HashMap<>();
static { static {
textures = new HashMap<>();
scaledTextures = new HashMap<>();
loadPieceTextures(); loadPieceTextures();
scaledTextures.putAll(textures); scaledTextures.putAll(textures);
} }

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,9 +6,7 @@ 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>
@ -33,11 +31,13 @@ class BoardTest {
*/ */
@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());
}
}