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 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 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 promotionPieceClass; private final Constructor promotionPieceConstructor; + private final char promotionPieceChar; - public PawnPromotion(Position pos, Position dest, Class promotionPieceClass) throws NoSuchMethodException, SecurityException { + public PawnPromotion(Position pos, Position dest, Class 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 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 cbWhite = new JComboBox<>(); - cbWhite.setModel(new DefaultComboBoxModel(options.toArray())); + cbWhite.setModel(new DefaultComboBoxModel<>(options.toArray())); cbWhite.setBounds(98, 9, 159, 22); dialogPanel.add(cbWhite); @@ -74,7 +72,7 @@ public class DialogUtil { dialogPanel.add(lblBlack); JComboBox cbBlack = new JComboBox<>(); - cbBlack.setModel(new DefaultComboBoxModel(options.toArray())); + cbBlack.setModel(new DefaultComboBoxModel<>(options.toArray())); cbBlack.setBounds(98, 36, 159, 22); dialogPanel.add(cbBlack); diff --git a/src/dev/kske/chess/ui/MainWindow.java b/src/dev/kske/chess/ui/MainWindow.java index b4c8b5e..3dc54b5 100644 --- a/src/dev/kske/chess/ui/MainWindow.java +++ b/src/dev/kske/chess/ui/MainWindow.java @@ -1,9 +1,11 @@ package dev.kske.chess.ui; +import java.awt.Desktop; import java.awt.EventQueue; import java.awt.Toolkit; import java.awt.dnd.DropTarget; import java.io.File; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.List; @@ -164,4 +166,30 @@ public class MainWindow extends JFrame { } }); } + + public void saveFile(File file) { + final int dotIndex = file.getName().lastIndexOf('.'); + final String extension = file.getName().substring(dotIndex).toLowerCase(); + + if (extension.equals(".pgn")) try { + PGNGame pgnGame = new PGNGame(getSelectedGamePane().getGame().getBoard()); + pgnGame.setTag("Event", tabbedPane.getTitleAt(tabbedPane.getSelectedIndex())); + pgnGame.setTag("Result", "*"); + PGNDatabase pgnDB = new PGNDatabase(); + pgnDB.getGames().add(pgnGame); + pgnDB.save(file); + + if (JOptionPane.showConfirmDialog(this, + "Game export finished. Do you want to view the created file?", + "Game export finished", + JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) + Desktop.getDesktop().open(file); + } catch (IOException e) { + e.printStackTrace(); + JOptionPane.showMessageDialog(this, + "Failed to save the file " + file.getName() + ": " + e.toString(), + "File saving error", + JOptionPane.ERROR_MESSAGE); + } + } } diff --git a/src/dev/kske/chess/ui/MenuBar.java b/src/dev/kske/chess/ui/MenuBar.java index 84c3c15..3ea86cc 100644 --- a/src/dev/kske/chess/ui/MenuBar.java +++ b/src/dev/kske/chess/ui/MenuBar.java @@ -2,11 +2,13 @@ package dev.kske.chess.ui; import java.awt.Toolkit; import java.awt.datatransfer.StringSelection; +import java.util.Arrays; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JOptionPane; +import javax.swing.filechooser.FileNameExtensionFilter; import dev.kske.chess.board.FENString; import dev.kske.chess.exception.ChessException; @@ -48,9 +50,18 @@ public class MenuBar extends JMenuBar { gameMenu.add(newGameMenuItem); JMenuItem loadFileMenu = new JMenuItem("Load game file"); - loadFileMenu.addActionListener((evt) -> DialogUtil.showFileSelectionDialog(mainWindow, mainWindow::loadFiles)); + loadFileMenu.addActionListener((evt) -> DialogUtil + .showFileSelectionDialog(mainWindow, + mainWindow::loadFiles, + Arrays.asList(new FileNameExtensionFilter("FEN and PGN files", "fen", "pgn")))); gameMenu.add(loadFileMenu); + JMenuItem saveFileMenu = new JMenuItem("Save game file"); + saveFileMenu + .addActionListener((evt) -> DialogUtil + .showFileSaveDialog(mainWindow, mainWindow::saveFile, Arrays.asList(new FileNameExtensionFilter("PGN file", "pgn")))); + gameMenu.add(saveFileMenu); + add(gameMenu); newGameMenuItem.doClick(); } diff --git a/test/dev/kske/chess/board/BoardTest.java b/test/dev/kske/chess/board/BoardTest.java index b647632..776d221 100644 --- a/test/dev/kske/chess/board/BoardTest.java +++ b/test/dev/kske/chess/board/BoardTest.java @@ -31,7 +31,7 @@ class BoardTest { */ @Test void testClone() { - Board clone = new Board(board); + Board clone = new Board(board, false); assertNotSame(clone, board); assertNotSame(clone.getBoardArr(), board.getBoardArr());