Compare commits
14 Commits
v0.1-alpha
...
v0.2-alpha
Author | SHA1 | Date | |
---|---|---|---|
d8f5f3bbf4 | |||
4dcc9f7ca0 | |||
cfd71af142 | |||
fcd8bfb26b | |||
cde7f63996 | |||
8ea0c7a603 | |||
8eda941284 | |||
7a986ab9c4 | |||
c245cdb640 | |||
58340ca6ac | |||
199d2f06c6 | |||
d12b06a1ff | |||
6d98d9a963 | |||
c3a787c3a7 |
@ -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
3
.gitignore
vendored
@ -1,5 +1,6 @@
|
||||
.metadata
|
||||
bin/
|
||||
/bin_test/
|
||||
tmp/
|
||||
*.tmp
|
||||
*.bak
|
||||
@ -20,4 +21,4 @@ local.properties
|
||||
.recommenders/
|
||||
|
||||
# Annotation Processing
|
||||
.apt_generated/
|
||||
.apt_generated/
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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) {
|
||||
|
55
src/dev/kske/chess/board/Log.java
Normal file
55
src/dev/kske/chess/board/Log.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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) {}
|
||||
}
|
||||
|
@ -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; }
|
||||
|
||||
|
@ -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() {}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
82
src/dev/kske/chess/ui/AIConfigDialog.java
Normal file
82
src/dev/kske/chess/ui/AIConfigDialog.java
Normal 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; }
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
|
64
src/dev/kske/chess/ui/MenuBar.java
Normal file
64
src/dev/kske/chess/ui/MenuBar.java
Normal 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();
|
||||
}
|
||||
}
|
@ -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(); }
|
||||
}
|
||||
|
@ -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")));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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]);
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user