14 Commits

Author SHA1 Message Date
d8f5f3bbf4 Fixed input listening bug in NaturalPlayer
+ disconnect methods in Game and Player
+ NaturalPlayer removes its MouseListener from OverlayComponent after
the disconnect method is called
2019-07-18 15:01:15 +02:00
4dcc9f7ca0 Set white king as MainWindow icon 2019-07-18 11:19:58 +02:00
cfd71af142 Moved game and board creation to Game 2019-07-17 08:26:51 +02:00
fcd8bfb26b Fixed game state related bugs 2019-07-16 18:24:48 +02:00
cde7f63996 Fixed UI bugs, added move drawing to OverlayComponent 2019-07-16 15:32:02 +02:00
8ea0c7a603 Added alpha-beta pruning threshold to the AI and a configuration dialog 2019-07-16 14:42:10 +02:00
8eda941284 Moved tests in test source folder, replaced GameModeDialog by MenuBar 2019-07-16 11:58:51 +02:00
7a986ab9c4 Added resource folder to class path, implemented proper texture scaling 2019-07-15 18:16:45 +02:00
c245cdb640 Implemented game restarting
+ Restarting method in Game
+ Abstract cancelMove method in Player
+ Stopping calculations in AIPlayer when the game has been restarted
2019-07-14 12:03:45 +02:00
58340ca6ac Added castling, fixed some minor bugs 2019-07-13 11:38:44 +02:00
199d2f06c6 Made application terminate when GameModeDialog is closed 2019-07-12 13:33:34 +02:00
d12b06a1ff Fixed knight move validation, renamed test 2019-07-12 10:07:02 +02:00
6d98d9a963 Added positional board evaluation 2019-07-11 19:57:54 +02:00
c3a787c3a7 Added move history and pawn promotion
+ Log class for move history
+ LoggedMove class with piece captured by the logged move
- Made move reversion easier
+ MoveType for recognizing special moves
+ MoveType determination during move generation and validation
2019-07-10 18:54:53 +02:00
21 changed files with 717 additions and 214 deletions

View File

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

3
.gitignore vendored
View File

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

View File

@ -5,6 +5,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import dev.kske.chess.board.Log.LoggedMove;
import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.board.Piece.Type;
@ -18,10 +19,49 @@ public class Board implements Cloneable {
private Piece[][] boardArr;
private Map<Color, Position> kingPos;
private Log log;
private static final Map<Type, int[][]> positionScores;
static {
positionScores = new HashMap<>();
positionScores.put(Type.KING,
new int[][] { new int[] { -3, -4, -4, -5, -5, -4, -4, -3 },
new int[] { -3, -4, -4, -5, -4, -4, -4, -3 }, new int[] { -3, -4, -4, -5, -4, -4, -4, -3 },
new int[] { -3, -4, -4, -5, -4, -4, -4, -3 }, new int[] { -2, -3, -3, -2, -2, -2, -2, -1 },
new int[] { -1, -2, -2, -2, -2, -2, -2, -1 }, new int[] { 2, 2, 0, 0, 0, 0, 2, 2 },
new int[] { 2, 3, 1, 0, 0, 1, 3, 2 } });
positionScores.put(Type.QUEEN,
new int[][] { new int[] { -2, -1, -1, -1, -1, -1, -1, -2 }, new int[] { -1, 0, 0, 0, 0, 0, 0, -1 },
new int[] { -1, 0, 1, 1, 1, 1, 0, -1 }, new int[] { -1, 0, 1, 1, 1, 1, 0, -1 },
new int[] { 0, 0, 1, 1, 1, 1, 0, -1 }, new int[] { -1, 1, 1, 1, 1, 1, 0, -1 },
new int[] { -1, 0, 1, 0, 0, 0, 0, -1 }, new int[] { -2, -1, -1, -1, -1, -1, -1, -2 } });
positionScores.put(Type.ROOK,
new int[][] { new int[] { 0, 0, 0, 0, 0, 0, 0, 0 }, new int[] { 1, 1, 1, 1, 1, 1, 1, 1 },
new int[] { -1, 0, 0, 0, 0, 0, 0, -1 }, new int[] { -1, 0, 0, 0, 0, 0, 0, -1 },
new int[] { -1, 0, 0, 0, 0, 0, 0, -1 }, new int[] { -1, 0, 0, 0, 0, 0, 0, -1 },
new int[] { -1, 0, 0, 0, 0, 0, 0, -1 }, new int[] { 0, 0, 0, 1, 1, 0, 0, 0 } });
positionScores.put(Type.KNIGHT,
new int[][] { new int[] { -5, -4, -3, -3, -3, -3, -4, -5 }, new int[] { -4, -2, 0, 0, 0, 0, -2, -4 },
new int[] { -3, 0, 1, 2, 2, 1, 0, -3 }, new int[] { -3, 1, 2, 2, 2, 2, 1, -3 },
new int[] { -3, 0, 2, 2, 2, 2, 0, -1 }, new int[] { -3, 1, 1, 2, 2, 1, 1, -3 },
new int[] { -4, -2, 0, 1, 1, 0, -2, -4 }, new int[] { -5, -4, -3, -3, -3, -3, -4, -5 } });
positionScores.put(Type.BISHOP,
new int[][] { new int[] { -2, -1, -1, -1, -1, -1, -1, 2 }, new int[] { -1, 0, 0, 0, 0, 0, 0, -1 },
new int[] { -1, 0, 1, 1, 1, 1, 0, -1 }, new int[] { -1, 1, 1, 1, 1, 1, 1, -1 },
new int[] { -1, 0, 1, 1, 1, 1, 0, -1 }, new int[] { -1, 1, 1, 1, 1, 1, 1, -1 },
new int[] { -1, 1, 0, 0, 0, 0, 1, -1 }, new int[] { -2, -1, -1, -1, -1, -1, -1, -2 } });
positionScores.put(Type.PAWN,
new int[][] { new int[] { 0, 0, 0, 0, 0, 0, 0, 0 }, new int[] { 5, 5, 5, 5, 5, 5, 5, 5 },
new int[] { 1, 1, 2, 3, 3, 2, 1, 1 }, new int[] { 0, 0, 1, 3, 3, 1, 0, 0 },
new int[] { 0, 0, 0, 2, 2, 0, 0, 0 }, new int[] { 0, 0, -1, 0, 0, -1, 0, 0 },
new int[] { 0, 1, 1, -2, -2, 1, 1, 0 }, new int[] { 0, 0, 0, 0, 0, 0, 0, 0 } });
}
public Board() {
boardArr = new Piece[8][8];
kingPos = new HashMap<>();
boardArr = new Piece[8][8];
kingPos = new HashMap<>();
log = new Log();
initializeDefaultPositions();
}
@ -35,12 +75,15 @@ public class Board implements Cloneable {
Piece piece = getPos(move);
if (piece == null || !piece.isValidMove(move)) return false;
else {
// Set type after validation
if (move.type == Move.Type.UNKNOWN) move.type = Move.Type.NORMAL;
// Move piece
Piece capturePiece = move(move);
move(move);
// Revert move if it caused a check for its team
if (checkCheck(piece.getColor())) {
revert(move, capturePiece);
revert();
return false;
}
@ -54,31 +97,97 @@ public class Board implements Cloneable {
* @param move The move to execute
* @return The captures piece, or null if the move's destination was empty
*/
public Piece move(Move move) {
public void move(Move move) {
Piece piece = getPos(move);
Piece capturePiece = getDest(move);
setDest(move, piece);
setPos(move, null);
switch (move.type) {
case PAWN_PROMOTION:
setPos(move, null);
// TODO: Select promotion
setDest(move, new Queen(piece.getColor(), this));
break;
case CASTLING:
// Move the king
setDest(move, piece);
setPos(move, null);
// Move the rook
Move rookMove = move.dest.x == 6 ? new Move(7, move.pos.y, 5, move.pos.y) // Kingside
: new Move(0, move.pos.y, 3, move.pos.y); // Queenside
// Move the rook
setDest(rookMove, getPos(rookMove));
setPos(rookMove, null);
getDest(rookMove).incMoveCounter();
break;
case UNKNOWN:
System.err.printf("Move of unknown type %s found!%n", move);
case NORMAL:
setDest(move, piece);
setPos(move, null);
break;
default:
System.err.printf("Move %s of unimplemented type found!%n", move);
}
// Increment move counter
getDest(move).incMoveCounter();
// Update the king's position if the moved piece is the king
if (piece.getType() == Type.KING) kingPos.put(piece.getColor(), move.dest);
return capturePiece;
// Update log
log.add(move, capturePiece);
}
/**
* Reverts a move.
*
* @param move The move to revert
* @param capturedPiece The piece that has been captured when the move has been
* applied
* Reverts the last move.
*/
public void revert(Move move, Piece capturedPiece) {
setPos(move, getDest(move));
setDest(move, capturedPiece);
public void revert() {
LoggedMove loggedMove = log.getLast();
Move move = loggedMove.move;
Piece capturedPiece = loggedMove.capturedPiece;
switch (move.type) {
case PAWN_PROMOTION:
setPos(move, new Pawn(getDest(move).getColor(), this));
setDest(move, capturedPiece);
break;
case CASTLING:
// Move the king
setPos(move, getDest(move));
setDest(move, null);
// Move the rook
Move rookMove = move.dest.x == 6 ? new Move(5, move.pos.y, 7, move.pos.y) // Kingside
: new Move(3, move.pos.y, 0, move.pos.y); // Queenside
// Move the rook
setDest(rookMove, getPos(rookMove));
setPos(rookMove, null);
getDest(rookMove).decMoveCounter();
break;
case UNKNOWN:
System.err.printf("Move of unknown type %s found!%n", move);
case NORMAL:
setPos(move, getDest(move));
setDest(move, capturedPiece);
break;
default:
System.err.printf("Move %s of unimplemented type found!%n", move);
}
// Decrement move counter
getPos(move).decMoveCounter();
// Update the king's position if the moved piece is the king
if (getPos(move).getType() == Type.KING) kingPos.put(getPos(move).getColor(), move.pos);
// Update log
log.removeLast();
}
/**
@ -129,9 +238,9 @@ public class Board implements Cloneable {
if (!getMoves(kingPos.get(color)).isEmpty()) return false;
else {
for (Move move : getMoves(color)) {
Piece capturePiece = move(move);
boolean check = checkCheck(color);
revert(move, capturePiece);
move(move);
boolean check = checkCheck(color);
revert();
if (!check) return false;
}
return true;
@ -139,8 +248,7 @@ public class Board implements Cloneable {
}
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;
}
@ -154,22 +262,26 @@ public class Board implements Cloneable {
int score = 0;
for (int i = 0; i < 8; i++)
for (int j = 0; j < 8; j++)
if (boardArr[i][j] != null && boardArr[i][j].getColor() == color) switch (boardArr[i][j].getType()) {
case QUEEN:
score += 8;
break;
case ROOK:
score += 5;
break;
case KNIGHT:
score += 3;
break;
case BISHOP:
score += 3;
break;
case PAWN:
score += 1;
break;
if (boardArr[i][j] != null && boardArr[i][j].getColor() == color) {
switch (boardArr[i][j].getType()) {
case QUEEN:
score += 90;
break;
case ROOK:
score += 50;
break;
case KNIGHT:
score += 30;
break;
case BISHOP:
score += 30;
break;
case PAWN:
score += 10;
break;
}
if (positionScores.containsKey(boardArr[i][j].getType()))
score += positionScores.get(boardArr[i][j].getType())[i][color == Color.WHITE ? j : 7 - j];
}
return score;
}
@ -240,9 +352,11 @@ public class Board implements Cloneable {
board.boardArr[i][j].board = board;
}
board.kingPos = new HashMap<>();
board.kingPos = new HashMap<>();
board.kingPos.putAll(kingPos);
board.log = (Log) log.clone();
return board;
}

View File

@ -17,7 +17,26 @@ public class King extends Piece {
@Override
public boolean isValidMove(Move move) {
return move.xDist <= 1 && move.yDist <= 1 && isFreePath(move);
// Castling
if (getMoveCounter() == 0 && move.xDist == 2 && move.yDist == 0) {
// Kingside
if (board.getBoardArr()[7][move.pos.y] != null && board.getBoardArr()[7][move.pos.y].getType() == Type.ROOK
&& isFreePath(new Move(new Position(5, move.pos.y), new Position(7, move.pos.y)))) {
move.type = Move.Type.CASTLING;
return true;
}
// Queenside
if (board.getBoardArr()[0][move.pos.y] != null && board.getBoardArr()[0][move.pos.y].getType() == Type.ROOK
&& isFreePath(new Move(new Position(1, move.pos.y), new Position(4, move.pos.y)))) {
move.type = Move.Type.CASTLING;
return true;
}
}
return move.xDist <= 1 && move.yDist <= 1 && checkDestination(move);
}
@Override
@ -29,9 +48,33 @@ public class King extends Piece {
Move move = new Move(pos, new Position(i, j));
if (board.getDest(move) == null || board.getDest(move).getColor() != getColor()) moves.add(move);
}
// Castling
// TODO: Check attacked squares in between
// TODO: Castling out of check?
if (getMoveCounter() == 0) {
// Kingside
if (board.getBoardArr()[7][pos.y] != null && board.getBoardArr()[7][pos.y].getType() == Type.ROOK
&& isFreePath(new Move(new Position(5, pos.y), new Position(7, pos.y))))
moves.add(new Move(pos, new Position(6, pos.y), Move.Type.CASTLING));
// Queenside
if (board.getBoardArr()[0][pos.y] != null && board.getBoardArr()[0][pos.y].getType() == Type.ROOK
&& isFreePath(new Move(new Position(1, pos.y), new Position(4, pos.y))))
moves.add(new Move(pos, new Position(2, pos.y), Move.Type.CASTLING));
}
return moves;
}
@Override
protected boolean isFreePath(Move move) {
for (int i = move.pos.x, j = move.pos.y; i != move.dest.x || j != move.dest.y; i += move.xSign, j += move.ySign)
if (board.getBoardArr()[i][j] != null) return false;
return true;
}
@Override
public Type getType() { return Type.KING; }
}

View File

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

View File

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

View File

@ -10,16 +10,22 @@ public class Move {
public final Position pos, dest;
public final int xDist, yDist, xSign, ySign;
public Type type;
public Move(Position pos, Position dest) {
public Move(Position pos, Position dest, Type type) {
this.pos = pos;
this.dest = dest;
this.type = type;
xDist = Math.abs(dest.x - pos.x);
yDist = Math.abs(dest.y - pos.y);
xSign = (int) Math.signum(dest.x - pos.x);
ySign = (int) Math.signum(dest.y - pos.y);
}
public Move(Position pos, Position dest) {
this(pos, dest, Type.NORMAL);
}
public Move(int xPos, int yPos, int xDest, int yDest) {
this(new Position(xPos, yPos), new Position(xDest, yDest));
}
@ -34,4 +40,8 @@ public class Move {
public String toString() {
return String.format("%s -> %s", pos, dest);
}
public static enum Type {
NORMAL, PAWN_PROMOTION, CASTLING, EN_PASSANT, UNKNOWN
}
}

View File

@ -17,12 +17,17 @@ public class Pawn extends Piece {
@Override
public boolean isValidMove(Move move) {
// TODO: en passant, pawn promotion
// TODO: en passant
boolean step = move.isVertical() && move.yDist == 1;
boolean doubleStep = move.isVertical() && move.yDist == 2;
boolean strafe = move.isDiagonal() && move.xDist == 1;
if (getColor() == Color.WHITE) doubleStep &= move.pos.y == 6;
else doubleStep &= move.pos.y == 1;
// Mark move as pawn promotion if necessary
if (move.ySign == 1 && move.pos.y == 6 || move.ySign == -1 && move.pos.y == 1)
move.type = Move.Type.PAWN_PROMOTION;
return (step ^ doubleStep ^ strafe) && move.ySign == (getColor() == Color.WHITE ? -1 : 1) && isFreePath(move);
}
@ -43,8 +48,6 @@ public class Pawn extends Piece {
int sign = getColor() == Color.WHITE ? -1 : 1;
if (sign == -1 && pos.y == 1 || sign == 1 && pos.y == 7) return moves;
// Strafe left
if (pos.x > 0) {
Move move = new Move(pos, new Position(pos.x - 1, pos.y + sign));
@ -68,6 +71,11 @@ public class Pawn extends Piece {
Move move = new Move(pos, new Position(pos.x, pos.y + 2 * sign));
if (isFreePath(move)) moves.add(move);
}
// Mark moves as pawn promotions if necessary
if (sign == 1 && pos.y == 6 || sign == -1 && pos.y == 1)
moves.parallelStream().forEach(m -> m.type = Move.Type.PAWN_PROMOTION);
return moves;
}

View File

@ -11,8 +11,9 @@ import java.util.List;
*/
public abstract class Piece implements Cloneable {
protected Color color;
protected Board board;
private final Color color;
protected Board board;
private int moveCounter;
public Piece(Color color, Board board) {
this.color = color;
@ -22,11 +23,10 @@ public abstract class Piece implements Cloneable {
public List<Move> getMoves(Position pos) {
List<Move> moves = getPseudolegalMoves(pos);
for (Iterator<Move> iterator = moves.iterator(); iterator.hasNext();) {
Move move = iterator.next();
Piece capturePiece = board.move(move);
if (board.checkCheck(getColor()))
iterator.remove();
board.revert(move, capturePiece);
Move move = iterator.next();
board.move(move);
if (board.checkCheck(getColor())) iterator.remove();
board.revert();
}
return moves;
}
@ -66,8 +66,18 @@ public abstract class Piece implements Cloneable {
public Color getColor() { return color; }
public int getMoveCounter() { return moveCounter; }
public void incMoveCounter() {
++moveCounter;
}
public void decMoveCounter() {
--moveCounter;
}
public static enum Type {
KING, QUEEN, ROOK, KNIGHT, BISHOP, PAWN;
KING, QUEEN, ROOK, KNIGHT, BISHOP, PAWN
}
public static enum Color {

View File

@ -1,12 +1,16 @@
package dev.kske.chess.game;
import java.util.HashMap;
import java.util.Map;
import dev.kske.chess.board.Board;
import dev.kske.chess.board.GameState;
import dev.kske.chess.board.Move;
import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.game.ai.AIPlayer;
import dev.kske.chess.ui.BoardComponent;
import dev.kske.chess.ui.BoardPane;
import dev.kske.chess.ui.OverlayComponent;
/**
* Project: <strong>Chess</strong><br>
@ -18,19 +22,44 @@ public class Game {
private Map<Color, Player> players;
private Board board;
private OverlayComponent overlayComponent;
private BoardComponent boardComponent;
public Game(Map<Color, Player> players, BoardComponent boardComponent) {
this.players = players;
this.boardComponent = boardComponent;
this.board = boardComponent.getBoard();
public Game(Map<Color, Player> players, BoardPane boardPane) {
this.players = players;
this.overlayComponent = boardPane.getOverlayComponent();
this.boardComponent = boardPane.getBoardComponent();
this.board = new Board();
boardComponent.setBoard(board);
// Initialize the game variable in each player
players.values().forEach(player -> player.setGame(this));
}
public void start() {
players.get(Color.WHITE).requestMove();
public static Game createNatural(BoardPane boardPane) {
Map<Color, Player> players = new HashMap<>();
OverlayComponent overlay = boardPane.getOverlayComponent();
players.put(Color.WHITE, new NaturalPlayer(Color.WHITE, overlay));
players.put(Color.BLACK, new NaturalPlayer(Color.BLACK, overlay));
return new Game(players, boardPane);
}
public static Game createNaturalVsAI(BoardPane boardPane, int maxDepth, int alphaBeta) {
Map<Color, Player> players = new HashMap<>();
OverlayComponent overlay = boardPane.getOverlayComponent();
players.put(Color.WHITE, new NaturalPlayer(Color.WHITE, overlay));
players.put(Color.BLACK, new AIPlayer(Color.BLACK, maxDepth, alphaBeta));
return new Game(players, boardPane);
}
public static Game createAIVsAI(BoardPane boardPane, int maxDepthW, int maxDepthB, int alphaBetaW, int alphaBetaB) {
Map<Color, Player> players = new HashMap<>();
players.put(Color.WHITE, new AIPlayer(Color.WHITE, maxDepthW, alphaBetaW));
players.put(Color.BLACK, new AIPlayer(Color.BLACK, maxDepthB, alphaBetaB));
return new Game(players, boardPane);
}
public void onMove(Player player, Move move) {
@ -47,8 +76,29 @@ public class Game {
default:
boardComponent.repaint();
players.get(player.color.opposite()).requestMove();
}
overlayComponent.displayArrow(move);
} else player.requestMove();
}
public void start() {
players.get(Color.WHITE).requestMove();
}
public void reset() {
players.forEach((k, v) -> v.cancelMove());
board.initializeDefaultPositions();
boardComponent.repaint();
overlayComponent.clearDots();
overlayComponent.clearArrow();
}
/**
* Removed all connections between the game and the ui.
*/
public void disconnect() {
players.values().forEach(Player::disconnect);
}
public Board getBoard() { return board; }
}

View File

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

View File

@ -15,16 +15,22 @@ public abstract class Player {
protected Board board;
protected Color color;
public Player(Board board, Color color) {
this.board = board;
this.color = color;
public Player(Color color) {
this.color = color;
}
public abstract void requestMove();
public abstract void cancelMove();
public abstract void disconnect();
public Game getGame() { return game; }
public void setGame(Game game) { this.game = game; }
public void setGame(Game game) {
this.game = game;
board = game.getBoard();
}
public Board getBoard() { return board; }

View File

@ -6,6 +6,7 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import javax.swing.SwingUtilities;
@ -24,15 +25,22 @@ public class AIPlayer extends Player {
private int availableProcessors;
private int maxDepth;
private int alphaBetaThreshold;
public AIPlayer(Board board, Color color, int maxDepth) {
super(board, color);
availableProcessors = Runtime.getRuntime().availableProcessors();
this.maxDepth = maxDepth;
private volatile boolean exitRequested;
private volatile ExecutorService executor;
public AIPlayer(Color color, int maxDepth, int alphaBetaThreshold) {
super(color);
availableProcessors = Runtime.getRuntime().availableProcessors();
this.maxDepth = maxDepth;
this.alphaBetaThreshold = alphaBetaThreshold;
exitRequested = false;
}
@Override
public void requestMove() {
exitRequested = false;
/*
* Define some processing threads, split the available moves between them and
* retrieve the result after their execution.
@ -41,8 +49,8 @@ public class AIPlayer extends Player {
/*
* Get a copy of the board and the available moves.
*/
Board board = (Board) AIPlayer.this.board.clone();
List<Move> moves = board.getMoves(color);
Board board = (Board) AIPlayer.this.board.clone();
List<Move> moves = board.getMoves(color);
/*
* Define move processors and split the available moves between them.
@ -55,16 +63,16 @@ public class AIPlayer extends Player {
for (int i = 0; i < numThreads; i++) {
if (rem-- > 0) ++endIndex;
endIndex += step;
processors.add(
new MoveProcessor((Board) board.clone(), moves.subList(beginIndex, endIndex), color, maxDepth));
processors.add(new MoveProcessor((Board) board.clone(), moves.subList(beginIndex, endIndex), color,
maxDepth, alphaBetaThreshold));
beginIndex = endIndex;
}
/*
* Execute processors, get the best result and pass it back to the Game class
*/
ExecutorService executor = Executors.newFixedThreadPool(numThreads);
List<ProcessingResult> results = new ArrayList<>(numThreads);
executor = Executors.newFixedThreadPool(numThreads);
List<ProcessingResult> results = new ArrayList<>(numThreads);
try {
List<Future<ProcessingResult>> futures = executor.invokeAll(processors);
for (Future<ProcessingResult> f : futures)
@ -74,7 +82,23 @@ public class AIPlayer extends Player {
ex.printStackTrace();
}
results.sort((r1, r2) -> Integer.compare(r2.score, r1.score));
SwingUtilities.invokeLater(() -> game.onMove(this, results.get(0).move));
if (!exitRequested) SwingUtilities.invokeLater(() -> game.onMove(this, results.get(0).move));
}, "AIPlayer calculation setup").start();
}
@Override
public void cancelMove() {
exitRequested = true;
if (executor != null) {
executor.shutdownNow();
try {
executor.awaitTermination(500, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void disconnect() {}
}

View File

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

View File

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

View File

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

View File

@ -2,13 +2,13 @@ package dev.kske.chess.ui;
import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.Toolkit;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import dev.kske.chess.board.Board;
import dev.kske.chess.game.Game;
/**
* Project: <strong>Chess</strong><br>
@ -18,7 +18,9 @@ import dev.kske.chess.board.Board;
*/
public class MainWindow {
private JFrame mframe;
private JFrame mframe;
private BoardPane boardPane;
private Game game;
/**
* Launch the application.
@ -53,19 +55,26 @@ public class MainWindow {
mframe.setBounds(100, 100, 494, 565);
mframe.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
BoardPane boardPane = new BoardPane();
boardPane.getBoardComponent().setBoard(new Board());
mframe.setIconImage(Toolkit.getDefaultToolkit().getImage(getClass().getResource("/pieces/king_white.png")));
boardPane = new BoardPane();
mframe.getContentPane().add(boardPane, BorderLayout.CENTER);
mframe.setJMenuBar(new MenuBar(this));
JPanel toolPanel = new JPanel();
mframe.getContentPane().add(toolPanel, BorderLayout.NORTH);
JButton btnRestart = new JButton("Restart");
btnRestart.addActionListener((evt) -> System.err.println("Resetting not implemented!"));
btnRestart.addActionListener((evt) -> { if (game != null) game.reset(); game.start(); });
toolPanel.add(btnRestart);
mframe.pack();
// Display dialog for game mode selection
new GameModeDialog(boardPane).setVisible(true);
mframe.setLocationRelativeTo(null);
}
public BoardPane getBoardPane() { return boardPane; }
public Game getGame() { return game; }
public void setGame(Game game) { this.game = game; }
}

View File

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

View File

@ -2,11 +2,17 @@ package dev.kske.chess.ui;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JComponent;
import dev.kske.chess.board.Move;
import dev.kske.chess.board.Position;
/**
@ -22,11 +28,12 @@ public class OverlayComponent extends JComponent {
private final BoardPane boardPane;
private List<Position> dots;
private Move arrow;
public OverlayComponent(BoardPane boardPane) {
this.boardPane = boardPane;
setSize(boardPane.getPreferredSize());
dots = new ArrayList<>();
dots = new ArrayList<>();
}
@Override
@ -34,7 +41,7 @@ public class OverlayComponent extends JComponent {
super.paintComponent(g);
final int tileSize = getTileSize();
// Draw possible moves if a piece was selected
if (!dots.isEmpty()) {
g.setColor(Color.green);
@ -45,6 +52,43 @@ public class OverlayComponent extends JComponent {
radius,
radius);
}
if (arrow != null) {
g.setColor(new Color(255, 0, 0, 127));
Point pos = new Point(arrow.pos.x * tileSize + tileSize / 2, arrow.pos.y * tileSize + tileSize / 2);
Point dest = new Point(arrow.dest.x * tileSize + tileSize / 2, arrow.dest.y * tileSize + tileSize / 2);
((Graphics2D) g).fill(createArrowShape(pos, dest));
}
}
private Shape createArrowShape(Point pos, Point dest) {
Polygon arrowPolygon = new Polygon();
arrowPolygon.addPoint(-6, 1);
arrowPolygon.addPoint(3, 1);
arrowPolygon.addPoint(3, 3);
arrowPolygon.addPoint(6, 0);
arrowPolygon.addPoint(3, -3);
arrowPolygon.addPoint(3, -1);
arrowPolygon.addPoint(-6, -1);
Point midPoint = midpoint(pos, dest);
double rotate = Math.atan2(dest.y - pos.y, dest.x - pos.x);
double ptDistance = pos.distance(dest);
double scale = ptDistance / 12.0; // 12 because it's the length of the arrow
// polygon.
AffineTransform transform = new AffineTransform();
transform.translate(midPoint.x, midPoint.y);
transform.rotate(rotate);
transform.scale(scale, 5);
return transform.createTransformedShape(arrowPolygon);
}
private Point midpoint(Point p1, Point p2) {
return new Point((int) ((p1.x + p2.x) / 2.0), (int) ((p1.y + p2.y) / 2.0));
}
public void displayDots(List<Position> dots) {
@ -58,5 +102,15 @@ public class OverlayComponent extends JComponent {
repaint();
}
public void displayArrow(Move arrow) {
this.arrow = arrow;
repaint();
}
public void clearArrow() {
arrow = null;
repaint();
}
public int getTileSize() { return boardPane.getTileSize(); }
}

View File

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

View File

@ -12,11 +12,11 @@ import dev.kske.chess.board.Queen;
/**
* Project: <strong>Chess</strong><br>
* File: <strong>BoardCloneTest.java</strong><br>
* File: <strong>BoardTest.java</strong><br>
* Created: <strong>08.07.2019</strong><br>
* Author: <strong>Kai S. K. Engelbart</strong>
*/
class BoardCloneTest {
class BoardTest {
Board board;
@ -40,5 +40,4 @@ class BoardCloneTest {
clone.getBoardArr()[0][0] = new Queen(Color.BLACK, clone);
assertNotEquals(clone.getBoardArr()[0][0], board.getBoardArr()[0][0]);
}
}