diff --git a/src/dev/kske/chess/board/Board.java b/src/dev/kske/chess/board/Board.java
index 327e7e3..114f32e 100644
--- a/src/dev/kske/chess/board/Board.java
+++ b/src/dev/kske/chess/board/Board.java
@@ -6,8 +6,6 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
import dev.kske.chess.board.Piece.Color;
@@ -15,7 +13,7 @@ import dev.kske.chess.board.Piece.Color;
* Project: Chess
* File: Board.java
* Created: 01.07.2019
- *
+ *
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
@@ -34,11 +32,11 @@ public class Board {
* Creates a copy of another {@link Board} instance.
* The created object is a deep copy, but does not contain any move history
* apart from the current {@link MoveNode}.
- *
+ *
* @param other The {@link Board} instance to copy
+ * @param copyVariations TODO
*/
- public Board(Board other) {
- boardArr = new Piece[8][8];
+ public Board(Board other, boolean copyVariations) {
for (int i = 0; i < 8; i++)
for (int j = 0; j < 8; j++) {
if (other.boardArr[i][j] == null) continue;
@@ -47,12 +45,16 @@ public class Board {
}
kingPos.putAll(other.kingPos);
- log = new Log(other.log, false);
+ log = new Log(other.log, copyVariations);
+
+ // Synchronize the current move node with the board
+ while (log.getLast().hasVariations())
+ log.selectNextNode(0);
}
/**
* Moves a piece across the board if the move is legal.
- *
+ *
* @param move The move to execute
* @return {@code true}, if the attempted move was legal and thus executed
*/
@@ -75,7 +77,7 @@ public class Board {
/**
* Moves a piece across the board without checking if the move is legal.
- *
+ *
* @param move The move to execute
*/
public void move(Move move) {
@@ -94,87 +96,11 @@ public class Board {
/**
* Moves a piece across the board without checking if the move is legal.
- *
+ *
* @param sanMove The move to execute in SAN (Standard Algebraic Notation)
*/
public void move(String sanMove) {
- Map patterns = new HashMap<>();
- patterns.put("pieceMove",
- Pattern.compile(
- "^(?[NBRQK])(?:(?[a-h])|(?[1-8])|(?[a-h][1-8]))?x?(?[a-h][1-8])(?:\\+{0,2}|\\#)$"));
- patterns.put("pawnCapture",
- Pattern.compile("^(?[a-h])(?[1-8])?x(?[a-h][1-8])(?[NBRQ])?(?:\\+{0,2}|\\#)?$"));
- patterns.put("pawnPush", Pattern.compile("^(?[a-h][1-8])(?[NBRQ])?(?:\\+{0,2}|\\#)$"));
- patterns.put("castling", Pattern.compile("^(?O-O-O)|(?O-O)(?:\\+{0,2}|\\#)?$"));
-
- patterns.forEach((patternName, pattern) -> {
- Matcher m = pattern.matcher(sanMove);
- if (m.find()) {
- Position pos = null, dest = null;
- Move move = null;
- switch (patternName) {
- case "pieceMove":
- dest = Position.fromLAN(m.group("toSquare"));
- if (m.group("fromSquare") != null) pos = Position.fromLAN(m.group("fromSquare"));
- else {
- Class extends Piece> pieceClass = Piece.fromFirstChar(m.group("pieceType").charAt(0));
- char file;
- int rank;
- if (m.group("fromFile") != null) {
- file = m.group("fromFile").charAt(0);
- rank = get(pieceClass, file);
- pos = Position.fromLAN(String.format("%c%d", file, rank));
- } else if (m.group("fromRank") != null) {
- rank = Integer.parseInt(m.group("fromRank").substring(0, 1));
- file = get(pieceClass, rank);
- pos = Position.fromLAN(String.format("%c%d", file, rank));
- } else pos = get(pieceClass, dest);
- }
- move = new Move(pos, dest);
- break;
- case "pawnCapture":
- char file = m.group("fromFile").charAt(0);
- int rank = m.group("fromRank") == null ? get(Pawn.class, file) : Integer.parseInt(m.group("fromRank"));
-
- dest = Position.fromLAN(m.group("toSquare"));
- pos = Position.fromLAN(String.format("%c%d", file, rank));
-
- if (m.group("promotedTo") != null) {
- try {
- move = new PawnPromotion(pos, dest, Piece.fromFirstChar(m.group("promotedTo").charAt(0)));
- } catch (NoSuchMethodException | SecurityException | IllegalArgumentException e) {
- e.printStackTrace();
- }
- } else move = new Move(pos, dest);
- break;
- case "pawnPush":
- dest = Position.fromLAN(m.group("toSquare"));
- int step = log.getActiveColor() == Color.WHITE ? 1 : -1;
-
- // One step forward
- if (boardArr[dest.x][dest.y + step] != null) pos = new Position(dest.x, dest.y + step);
-
- // Double step forward
- else pos = new Position(dest.x, dest.y + 2 * step);
-
- if (m.group("promotedTo") != null) {
- try {
- move = new PawnPromotion(pos, dest, Piece.fromFirstChar(m.group("promotedTo").charAt(0)));
- } catch (NoSuchMethodException | SecurityException | IllegalArgumentException e) {
- e.printStackTrace();
- }
- } else move = new Move(pos, dest);
- break;
- case "castling":
- pos = new Position(4, log.getActiveColor() == Color.WHITE ? 7 : 0);
- dest = new Position(m.group("kingside") != null ? 6 : 2, pos.y);
- move = new Castling(pos, dest);
- break;
- }
- move(move);
- return;
- }
- });
+ move(Move.fromSAN(sanMove, this));
}
/**
@@ -196,7 +122,7 @@ public class Board {
/**
* Generated every legal move for one color
- *
+ *
* @param color The color to generate the moves for
* @return A list of all legal moves
*/
@@ -212,7 +138,7 @@ public class Board {
/**
* Checks, if the king is in check.
- *
+ *
* @param color The color of the king to check
* @return {@code true}, if the king is in check
*/
@@ -220,7 +146,7 @@ public class Board {
/**
* Checks, if a field can be attacked by pieces of a certain color.
- *
+ *
* @param dest the field to check
* @param color the color of a potential attacker piece
* @return {@code true} if a move with the destination {@code dest}
@@ -237,7 +163,7 @@ public class Board {
/**
* Checks, if the king is in checkmate.
* This requires the king to already be in check!
- *
+ *
* @param color The color of the king to check
* @return {@code true}, if the king is in checkmate
*/
@@ -334,7 +260,7 @@ public class Board {
/**
* Searches for a {@link Piece} inside a file (A - H).
- *
+ *
* @param pieceClass The class of the piece to search for
* @param file The file in which to search for the piece
* @return The rank (1 - 8) of the first piece with the specified type and
@@ -349,7 +275,7 @@ public class Board {
/**
* Searches for a {@link Piece} inside a rank (1 - 8).
- *
+ *
* @param pieceClass The class of the piece to search for
* @param rank The rank in which to search for the piece
* @return The file (A - H) of the first piece with the specified type and
@@ -365,7 +291,7 @@ public class Board {
/**
* Searches for a {@link Piece} that can move to a {@link Position}.
- *
+ *
* @param pieceClass The class of the piece to search for
* @param dest The destination that the piece is required to reach
* @return The position of a piece that can move to the specified destination
@@ -382,7 +308,7 @@ public class Board {
/**
* Places a piece at a position.
- *
+ *
* @param pos The position to place the piece at
* @param piece The piece to place
*/
@@ -402,7 +328,7 @@ public class Board {
/**
* Places a piece at the position of a move.
- *
+ *
* @param move The move at which position to place the piece
* @param piece The piece to place
*/
@@ -410,7 +336,7 @@ public class Board {
/**
* Places a piece at the destination of a move.
- *
+ *
* @param move The move at which destination to place the piece
* @param piece The piece to place
*/
diff --git a/src/dev/kske/chess/board/Castling.java b/src/dev/kske/chess/board/Castling.java
index e631a24..9f973ad 100644
--- a/src/dev/kske/chess/board/Castling.java
+++ b/src/dev/kske/chess/board/Castling.java
@@ -4,7 +4,7 @@ package dev.kske.chess.board;
* Project: Chess
* File: Castling.java
* Created: 2 Nov 2019
- *
+ *
* @since Chess v0.5-alpha
* @author Kai S. K. Engelbart
*/
@@ -32,4 +32,13 @@ public class Castling extends Move {
super.revert(board, capturedPiece);
rookMove.revert(board, null);
}
+
+ /**
+ * @return {@code O-O-O} for a queenside castling or {@code O-O} for a kingside
+ * castling
+ */
+ @Override
+ public String toSAN(Board board) {
+ return rookMove.pos.x == 0 ? "O-O-O" : "O-O";
+ }
}
diff --git a/src/dev/kske/chess/board/Log.java b/src/dev/kske/chess/board/Log.java
index 9d60002..ba4c509 100644
--- a/src/dev/kske/chess/board/Log.java
+++ b/src/dev/kske/chess/board/Log.java
@@ -10,7 +10,7 @@ import dev.kske.chess.board.Piece.Color;
* Project: Chess
* File: Log.java
* Created: 09.07.2019
- *
+ *
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
@@ -28,7 +28,7 @@ public class Log implements Iterable {
/**
* Creates a (partially deep) copy of another {@link Log} instance which begins
* with the current {@link MoveNode}.
- *
+ *
* @param other The {@link Log} instance to copy
* @param copyVariations If set to {@code true}, subsequent variations of the
* current {@link MoveNode} are copied with the
@@ -43,12 +43,17 @@ public class Log implements Iterable {
// The new root is the current node of the copied instance
if (!other.isEmpty()) {
- root = new MoveNode(other.current, copyVariations);
+ root = new MoveNode(other.root, copyVariations);
root.setParent(null);
current = root;
}
}
+ /**
+ * @return an iterator over all {@link MoveNode} objects that are either the
+ * root node or a first variation of another node, starting from the
+ * root node
+ */
@Override
public Iterator iterator() {
return new Iterator() {
@@ -71,7 +76,7 @@ public class Log implements Iterable {
/**
* Adds a move to the move history and adjusts the log to the new position.
- *
+ *
* @param move The move to log
* @param piece The piece that performed the move
* @param capturedPiece The piece captured with the move
@@ -98,7 +103,7 @@ public class Log implements Iterable {
}
/**
- * Removed the last move from the log and adjusts its state to the previous
+ * Removes the last move from the log and adjusts its state to the previous
* move.
*/
public void removeLast() {
@@ -109,8 +114,14 @@ public class Log implements Iterable {
} else reset();
}
+ /**
+ * @return {@code true} if the root node exists
+ */
public boolean isEmpty() { return root == null; }
+ /**
+ * @return {@code true} if the current node has a parent node
+ */
public boolean hasParent() { return !isEmpty() && current.hasParent(); }
/**
@@ -129,7 +140,7 @@ public class Log implements Iterable {
/**
* Changes the current node to one of its children (variations).
- *
+ *
* @param index the index of the variation to select
*/
public void selectNextNode(int index) {
@@ -159,6 +170,10 @@ public class Log implements Iterable {
}
}
+ /**
+ * Sets the active color, castling rights, en passant target square, fullmove
+ * number and halfmove clock to those of the current {@link MoveNode}.
+ */
private void update() {
activeColor = current.activeColor;
castlingRights = current.castlingRights.clone();
@@ -167,6 +182,15 @@ public class Log implements Iterable {
halfmoveClock = current.halfmoveClock;
}
+ /**
+ * Removed the castling rights bound to a rook or king for the rest of the game.
+ * This method should be called once the piece has been moved, as a castling
+ * move involving this piece is forbidden afterwards.
+ *
+ * @param piece the rook or king to disable the castling rights for
+ * @param initialPosition the initial position of the piece during the start of
+ * the game
+ */
private void disableCastlingRights(Piece piece, Position initialPosition) {
// Kingside
if (piece instanceof King || piece instanceof Rook && initialPosition.x == 7)
@@ -220,7 +244,7 @@ public class Log implements Iterable {
public int getFullmoveNumber() { return fullmoveNumber; }
- public void setFullmoveNumber(int fullmoveCounter) { this.fullmoveNumber = fullmoveCounter; }
+ public void setFullmoveNumber(int fullmoveNumber) { this.fullmoveNumber = fullmoveNumber; }
public int getHalfmoveClock() { return halfmoveClock; }
diff --git a/src/dev/kske/chess/board/Move.java b/src/dev/kske/chess/board/Move.java
index a03a78c..2ff63e2 100644
--- a/src/dev/kske/chess/board/Move.java
+++ b/src/dev/kske/chess/board/Move.java
@@ -1,12 +1,18 @@
package dev.kske.chess.board;
+import java.util.HashMap;
+import java.util.Map;
import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import dev.kske.chess.board.Piece.Color;
/**
* Project: Chess
* File: Move.java
* Created: 02.07.2019
- *
+ *
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
@@ -44,7 +50,7 @@ public class Move {
if (move.length() == 5) {
try {
return new PawnPromotion(pos, dest, Piece.fromFirstChar(move.charAt(4)));
- } catch (NoSuchMethodException | SecurityException | IllegalArgumentException e) {
+ } catch (Exception e) {
e.printStackTrace();
return null;
}
@@ -53,6 +59,116 @@ public class Move {
public String toLAN() { return getPos().toLAN() + getDest().toLAN(); }
+ /**
+ * Converts a move string from standard algebraic notation to a {@link Move}
+ * object.
+ *
+ * @param sanMove the move string to convert from
+ * @param board the board on which the move has to be executed
+ * @return the converted {@link Move} object
+ */
+ public static Move fromSAN(String sanMove, Board board) {
+ Map patterns = new HashMap<>();
+ patterns.put("pieceMove",
+ Pattern.compile(
+ "^(?[NBRQK])(?:(?[a-h])|(?[1-8])|(?[a-h][1-8]))?x?(?[a-h][1-8])(?:\\+{0,2}|\\#)$"));
+ patterns.put("pawnCapture",
+ Pattern.compile("^(?[a-h])(?[1-8])?x(?[a-h][1-8])(?[NBRQ])?(?:\\+{0,2}|\\#)?$"));
+ patterns.put("pawnPush", Pattern.compile("^(?[a-h][1-8])(?[NBRQ])?(?:\\+{0,2}|\\#)$"));
+ patterns.put("castling", Pattern.compile("^(?O-O-O)|(?O-O)(?:\\+{0,2}|\\#)?$"));
+
+ for (Map.Entry entry : patterns.entrySet()) {
+ Matcher m = entry.getValue().matcher(sanMove);
+ if (m.find()) {
+ Position pos = null, dest = null;
+ Move move = null;
+ switch (entry.getKey()) {
+ case "pieceMove":
+ dest = Position.fromLAN(m.group("toSquare"));
+ if (m.group("fromSquare") != null) pos = Position.fromLAN(m.group("fromSquare"));
+ else {
+ Class extends Piece> pieceClass = Piece.fromFirstChar(m.group("pieceType").charAt(0));
+ char file;
+ int rank;
+ if (m.group("fromFile") != null) {
+ file = m.group("fromFile").charAt(0);
+ rank = board.get(pieceClass, file);
+ pos = Position.fromLAN(String.format("%c%d", file, rank));
+ } else if (m.group("fromRank") != null) {
+ rank = Integer.parseInt(m.group("fromRank").substring(0, 1));
+ file = board.get(pieceClass, rank);
+ pos = Position.fromLAN(String.format("%c%d", file, rank));
+ } else pos = board.get(pieceClass, dest);
+ }
+ move = new Move(pos, dest);
+ break;
+ case "pawnCapture":
+ char file = m.group("fromFile").charAt(0);
+ int rank = m.group("fromRank") == null ? board.get(Pawn.class, file) : Integer.parseInt(m.group("fromRank"));
+
+ dest = Position.fromLAN(m.group("toSquare"));
+ pos = Position.fromLAN(String.format("%c%d", file, rank));
+
+ if (m.group("promotedTo") != null) {
+ try {
+ move = new PawnPromotion(pos, dest, Piece.fromFirstChar(m.group("promotedTo").charAt(0)));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ } else move = new Move(pos, dest);
+ break;
+ case "pawnPush":
+ dest = Position.fromLAN(m.group("toSquare"));
+ int step = board.getLog().getActiveColor() == Color.WHITE ? 1 : -1;
+
+ // One step forward
+ if (board.getBoardArr()[dest.x][dest.y + step] != null) pos = new Position(dest.x, dest.y + step);
+
+ // Double step forward
+ else pos = new Position(dest.x, dest.y + 2 * step);
+
+ if (m.group("promotedTo") != null) {
+ try {
+ move = new PawnPromotion(pos, dest, Piece.fromFirstChar(m.group("promotedTo").charAt(0)));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ } else move = new Move(pos, dest);
+ break;
+ case "castling":
+ pos = new Position(4, board.getLog().getActiveColor() == Color.WHITE ? 7 : 0);
+ dest = new Position(m.group("kingside") != null ? 6 : 2, pos.y);
+ move = new Castling(pos, dest);
+ break;
+ }
+ return move;
+ }
+ }
+ return null;
+ }
+
+ public String toSAN(Board board) {
+ final Piece piece = board.get(pos);
+ StringBuilder sb = new StringBuilder(8);
+
+ // Piece symbol
+ if(!(piece instanceof Pawn))
+ sb.append(Character.toUpperCase(piece.firstChar()));
+
+ // Position
+ // TODO: Deconstruct position into optional file or rank
+ // Omit position if the move is a pawn push
+ if (!(piece instanceof Pawn && xDist == 0)) sb.append(pos.toLAN());
+
+ // Capture indicator
+ if (board.get(dest) != null) sb.append('x');
+
+ // Destination
+ sb.append(dest.toLAN());
+
+ return sb.toString();
+ }
+
public boolean isHorizontal() { return getyDist() == 0; }
public boolean isVertical() { return getxDist() == 0; }
diff --git a/src/dev/kske/chess/board/MoveNode.java b/src/dev/kske/chess/board/MoveNode.java
index a8bf2b8..e618716 100644
--- a/src/dev/kske/chess/board/MoveNode.java
+++ b/src/dev/kske/chess/board/MoveNode.java
@@ -11,7 +11,7 @@ import dev.kske.chess.board.Piece.Color;
* Project: Chess
* File: MoveNode.java
* Created: 02.10.2019
- *
+ *
* @since Chess v0.5-alpha
* @author Kai S. K. Engelbart
*/
@@ -31,7 +31,7 @@ public class MoveNode {
/**
* Creates a new {@link MoveNode}.
- *
+ *
* @param move The logged {@link Move}
* @param capturedPiece The {@link Piece} captures by the logged {@link Move}
* @param enPassant The en passant {@link Position} valid after the logged
@@ -53,7 +53,7 @@ public class MoveNode {
/**
* Creates a (deep) copy of another {@link MoveNode}.
- *
+ *
* @param other The {@link MoveNode} to copy
* @param copyVariations When this is set to {@code true} a deep copy is
* created, which
@@ -64,17 +64,17 @@ public class MoveNode {
other.fullmoveCounter, other.halfmoveClock);
if (copyVariations && other.variations != null) {
if (variations == null) variations = new ArrayList<>();
- other.variations.forEach(variation -> {
+ for (MoveNode variation : other.variations) {
MoveNode copy = new MoveNode(variation, true);
copy.parent = this;
variations.add(copy);
- });
+ }
}
}
/**
* Adds another {@link MoveNode} as a child node.
- *
+ *
* @param variation The {@link MoveNode} to append to this {@link MoveNode}
*/
public void addVariation(MoveNode variation) {
diff --git a/src/dev/kske/chess/board/Pawn.java b/src/dev/kske/chess/board/Pawn.java
index 18e275e..7b44b0a 100644
--- a/src/dev/kske/chess/board/Pawn.java
+++ b/src/dev/kske/chess/board/Pawn.java
@@ -7,7 +7,7 @@ import java.util.List;
* Project: Chess
* File: Pawn.java
* Created: 01.07.2019
- *
+ *
* @since Chess v0.1-alpha
* @author Kai S. K. Engelbart
*/
@@ -74,7 +74,7 @@ public class Pawn extends Piece {
moves.add(new PawnPromotion(pos, dest, Rook.class));
moves.add(new PawnPromotion(pos, dest, Knight.class));
moves.add(new PawnPromotion(pos, dest, Bishop.class));
- } catch (NoSuchMethodException | SecurityException | IllegalArgumentException e) {
+ } catch (Exception e) {
e.printStackTrace();
}
} else moves.add(move);
diff --git a/src/dev/kske/chess/board/PawnPromotion.java b/src/dev/kske/chess/board/PawnPromotion.java
index 91ddaac..2c78750 100644
--- a/src/dev/kske/chess/board/PawnPromotion.java
+++ b/src/dev/kske/chess/board/PawnPromotion.java
@@ -10,27 +10,29 @@ import dev.kske.chess.board.Piece.Color;
* Project: Chess
* File: PawnPromotion.java
* Created: 2 Nov 2019
- *
+ *
* @since Chess v0.5-alpha
* @author Kai S. K. Engelbart
*/
public class PawnPromotion extends Move {
- private final Class extends Piece> promotionPieceClass;
private final Constructor extends Piece> promotionPieceConstructor;
+ private final char promotionPieceChar;
- public PawnPromotion(Position pos, Position dest, Class extends Piece> promotionPieceClass) throws NoSuchMethodException, SecurityException {
+ public PawnPromotion(Position pos, Position dest, Class extends Piece> promotionPieceClass)
+ throws ReflectiveOperationException, RuntimeException {
super(pos, dest);
- this.promotionPieceClass = promotionPieceClass;
// Cache piece constructor
promotionPieceConstructor = promotionPieceClass.getDeclaredConstructor(Color.class, Board.class);
promotionPieceConstructor.setAccessible(true);
+
+ // Get piece char
+ promotionPieceChar = (char) promotionPieceClass.getMethod("firstChar").invoke(promotionPieceConstructor.newInstance(null, null));
}
public PawnPromotion(int xPos, int yPos, int xDest, int yDest, Class extends Piece> promotionPiece)
- throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException,
- InstantiationException {
+ throws ReflectiveOperationException, RuntimeException {
this(new Position(xPos, yPos), new Position(xDest, yDest), promotionPiece);
}
@@ -51,22 +53,19 @@ public class PawnPromotion extends Move {
}
@Override
- public String toLAN() {
- char promotionPieceChar = '-';
- try {
- promotionPieceChar = (char) promotionPieceClass.getMethod("firstChar").invoke(promotionPieceConstructor.newInstance(null, null));
- } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException
- | InstantiationException e) {
- e.printStackTrace();
- }
- return pos.toLAN() + dest.toLAN() + promotionPieceChar;
+ public String toLAN() { return pos.toLAN() + dest.toLAN() + promotionPieceChar; }
+
+ @Override
+ public String toSAN(Board board) {
+ String san = super.toSAN(board);
+ return san + Character.toUpperCase(promotionPieceChar);
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
- result = prime * result + Objects.hash(promotionPieceClass);
+ result = prime * result + Objects.hash(promotionPieceChar, promotionPieceConstructor);
return result;
}
@@ -74,8 +73,8 @@ public class PawnPromotion extends Move {
public boolean equals(Object obj) {
if (this == obj) return true;
if (!super.equals(obj)) return false;
- if (getClass() != obj.getClass()) return false;
+ if (!(obj instanceof PawnPromotion)) return false;
PawnPromotion other = (PawnPromotion) obj;
- return Objects.equals(promotionPieceClass, other.promotionPieceClass);
+ return promotionPieceChar == other.promotionPieceChar && Objects.equals(promotionPieceConstructor, other.promotionPieceConstructor);
}
}
diff --git a/src/dev/kske/chess/game/ai/AIPlayer.java b/src/dev/kske/chess/game/ai/AIPlayer.java
index 7e8c015..9e53326 100644
--- a/src/dev/kske/chess/game/ai/AIPlayer.java
+++ b/src/dev/kske/chess/game/ai/AIPlayer.java
@@ -52,7 +52,7 @@ public class AIPlayer extends Player {
/*
* Get a copy of the board and the available moves.
*/
- Board board = new Board(this.board);
+ Board board = new Board(this.board, false);
List moves = board.getMoves(color);
/*
@@ -66,7 +66,7 @@ public class AIPlayer extends Player {
for (int i = 0; i < numThreads; i++) {
if (rem-- > 0) ++endIndex;
endIndex += step;
- processors.add(new MoveProcessor(new Board(board), moves.subList(beginIndex, endIndex), color,
+ processors.add(new MoveProcessor(new Board(board, false), moves.subList(beginIndex, endIndex), color,
maxDepth, alphaBetaThreshold));
beginIndex = endIndex;
}
diff --git a/src/dev/kske/chess/pgn/PGNDatabase.java b/src/dev/kske/chess/pgn/PGNDatabase.java
index c29598e..127f9a6 100644
--- a/src/dev/kske/chess/pgn/PGNDatabase.java
+++ b/src/dev/kske/chess/pgn/PGNDatabase.java
@@ -2,6 +2,8 @@ package dev.kske.chess.pgn;
import java.io.File;
import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
@@ -12,21 +14,42 @@ import dev.kske.chess.exception.ChessException;
* Project: Chess
* File: PGNDatabase.java
* Created: 4 Oct 2019
+ *
+ * Contains a series of {@link PGNGame} objects that can be stored inside a PGN
+ * file.
*
* @since Chess v0.5-alpha
* @author Kai S. K. Engelbart
*/
public class PGNDatabase {
- private final List games = new ArrayList<>();
+ private final List games = new ArrayList<>();
+ /**
+ * Loads PGN games from a file.
+ *
+ * @param pgnFile the file to load the games from
+ * @throws FileNotFoundException if the specified file is not found
+ * @throws ChessException if an error occurs while parsing the file
+ */
public void load(File pgnFile) throws FileNotFoundException, ChessException {
- try (Scanner sc = new Scanner(pgnFile)) {
- while (sc.hasNext())
- games.add(PGNGame.parse(sc));
- } catch (FileNotFoundException | ChessException e) {
- throw e;
- }
+ Scanner sc = new Scanner(pgnFile);
+ while (sc.hasNext())
+ games.add(PGNGame.parse(sc));
+ sc.close();
+ }
+
+ /**
+ * Saves PGN games to a file.
+ *
+ * @param pgnFile the file to save the games to.
+ * @throws IOException if the file could not be created
+ */
+ public void save(File pgnFile) throws IOException {
+ pgnFile.getParentFile().mkdirs();
+ PrintWriter pw = new PrintWriter(pgnFile);
+ games.forEach(g -> g.writePGN(pw));
+ pw.close();
}
public List getGames() { return games; }
diff --git a/src/dev/kske/chess/pgn/PGNGame.java b/src/dev/kske/chess/pgn/PGNGame.java
index 575fb75..609a8f9 100644
--- a/src/dev/kske/chess/pgn/PGNGame.java
+++ b/src/dev/kske/chess/pgn/PGNGame.java
@@ -1,6 +1,10 @@
package dev.kske.chess.pgn;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.regex.MatchResult;
@@ -8,20 +12,26 @@ import java.util.regex.Pattern;
import dev.kske.chess.board.Board;
import dev.kske.chess.board.FENString;
+import dev.kske.chess.board.Move;
+import dev.kske.chess.board.Piece.Color;
import dev.kske.chess.exception.ChessException;
/**
* Project: Chess
* File: PGNGame.java
* Created: 22 Sep 2019
- *
+ *
* @since Chess v0.5-alpha
* @author Kai S. K. Engelbart
*/
public class PGNGame {
private final Map tagPairs = new HashMap<>(7);
- private final Board board = new Board();
+ private final Board board;
+
+ public PGNGame() { board = new Board(); }
+
+ public PGNGame(Board board) { this.board = board; }
public static PGNGame parse(Scanner sc) throws ChessException {
PGNGame game = new PGNGame();
@@ -62,6 +72,45 @@ public class PGNGame {
return game;
}
+ public void writePGN(PrintWriter pw) {
+ // Set the unknown result tag if no result tag is specified
+ tagPairs.putIfAbsent("Result", "*");
+
+ // Write tag pairs
+ tagPairs.forEach((k, v) -> pw.printf("[%s \"%s\"]%n", k, v));
+
+ // Insert newline if tags were printed
+ if (!tagPairs.isEmpty()) pw.println();
+
+ if (!board.getLog().isEmpty()) {
+ // Collect SAN moves
+ Board clone = new Board(board, true);
+ List chunks = new ArrayList<>();
+ boolean flag = true;
+ while (flag) {
+ Move move = clone.getLog().getLast().move;
+ flag = clone.getLog().hasParent();
+ clone.revert();
+ String chunk = clone.getLog().getActiveColor() == Color.WHITE ? String.format(" %d. ", clone.getLog().getFullmoveNumber()) : " ";
+ chunk += move.toSAN(clone);
+ chunks.add(chunk);
+ }
+ Collections.reverse(chunks);
+
+ // Write movetext
+ String line = "";
+ for (String chunk : chunks)
+ if (line.length() + chunk.length() <= 80) line += chunk;
+ else {
+ pw.println(line);
+ line = chunk;
+ }
+ if (!line.isEmpty()) pw.println(line);
+ }
+ // Write game termination marker
+ pw.print(tagPairs.get("Result"));
+ }
+
public String getTag(String tagName) { return tagPairs.get(tagName); }
public boolean hasTag(String tagName) { return tagPairs.containsKey(tagName); }
diff --git a/src/dev/kske/chess/ui/DialogUtil.java b/src/dev/kske/chess/ui/DialogUtil.java
index 9a70d7c..0621666 100644
--- a/src/dev/kske/chess/ui/DialogUtil.java
+++ b/src/dev/kske/chess/ui/DialogUtil.java
@@ -5,6 +5,7 @@ import java.awt.Font;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
@@ -15,7 +16,7 @@ import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
-import javax.swing.filechooser.FileFilter;
+import javax.swing.filechooser.FileNameExtensionFilter;
import dev.kske.chess.io.EngineUtil;
@@ -23,7 +24,7 @@ import dev.kske.chess.io.EngineUtil;
* Project: Chess
* File: DialogUtil.java
* Created: 24.07.2019
- *
+ *
* @since Chess v0.3-alpha
* @author Kai S. K. Engelbart
*/
@@ -31,27 +32,24 @@ public class DialogUtil {
private DialogUtil() {}
- public static void showFileSelectionDialog(Component parent, Consumer> action) {
+ public static void showFileSelectionDialog(Component parent, Consumer> action, Collection filters) {
JFileChooser fileChooser = new JFileChooser();
fileChooser.setCurrentDirectory(new File(System.getProperty("user.home")));
fileChooser.setAcceptAllFileFilterUsed(false);
- fileChooser.addChoosableFileFilter(new FileFilter() {
-
- @Override
- public boolean accept(File f) {
- int dotIndex = f.getName().lastIndexOf('.');
- if (dotIndex >= 0) {
- String extension = f.getName().substring(dotIndex).toLowerCase();
- return extension.equals(".fen") || extension.equals(".pgn");
- } else return f.isDirectory();
- }
-
- @Override
- public String getDescription() { return "FEN and PGN files"; }
- });
+ filters.forEach(fileChooser::addChoosableFileFilter);
if (fileChooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) action.accept(Arrays.asList(fileChooser.getSelectedFile()));
}
+ public static void showFileSaveDialog(Component parent, Consumer action, Collection filters) {
+ JFileChooser fileChooser = new JFileChooser();
+ fileChooser.setCurrentDirectory(new File(System.getProperty("user.home")));
+ fileChooser.setAcceptAllFileFilterUsed(false);
+ filters.forEach(fileChooser::addChoosableFileFilter);
+ if (fileChooser.showSaveDialog(parent) == JFileChooser.APPROVE_OPTION) action.accept(
+ new File(fileChooser.getSelectedFile().getAbsolutePath() + "."
+ + ((FileNameExtensionFilter) fileChooser.getFileFilter()).getExtensions()[0]));
+ }
+
public static void showGameConfigurationDialog(Component parent, BiConsumer action) {
JPanel dialogPanel = new JPanel();
@@ -64,7 +62,7 @@ public class DialogUtil {
dialogPanel.add(lblWhite);
JComboBox