From 12f6f7f0ef87bff67ac859214772b15912012700 Mon Sep 17 00:00:00 2001 From: CyB3RC0nN0R Date: Thu, 7 Nov 2019 05:53:28 +0100 Subject: [PATCH 1/9] Added empty save methods to PGNDatabase and PGNGame --- src/dev/kske/chess/board/Log.java | 6 ++++++ src/dev/kske/chess/pgn/PGNDatabase.java | 19 ++++++++++++------- src/dev/kske/chess/pgn/PGNGame.java | 16 ++++++++++++++++ 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/dev/kske/chess/board/Log.java b/src/dev/kske/chess/board/Log.java index 9d60002..d31f5de 100644 --- a/src/dev/kske/chess/board/Log.java +++ b/src/dev/kske/chess/board/Log.java @@ -109,8 +109,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(); } /** diff --git a/src/dev/kske/chess/pgn/PGNDatabase.java b/src/dev/kske/chess/pgn/PGNDatabase.java index c29598e..e00a4ad 100644 --- a/src/dev/kske/chess/pgn/PGNDatabase.java +++ b/src/dev/kske/chess/pgn/PGNDatabase.java @@ -2,6 +2,7 @@ package dev.kske.chess.pgn; import java.io.File; import java.io.FileNotFoundException; +import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; import java.util.Scanner; @@ -18,15 +19,19 @@ import dev.kske.chess.exception.ChessException; */ public class PGNDatabase { - private final List games = new ArrayList<>(); + private final List games = new ArrayList<>(); 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(); + } + + public void save(File pgnFile) throws FileNotFoundException { + 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..e5c35f0 100644 --- a/src/dev/kske/chess/pgn/PGNGame.java +++ b/src/dev/kske/chess/pgn/PGNGame.java @@ -1,5 +1,6 @@ package dev.kske.chess.pgn; +import java.io.PrintWriter; import java.util.HashMap; import java.util.Map; import java.util.Scanner; @@ -8,6 +9,7 @@ import java.util.regex.Pattern; import dev.kske.chess.board.Board; import dev.kske.chess.board.FENString; +import dev.kske.chess.board.Piece.Color; import dev.kske.chess.exception.ChessException; /** @@ -62,6 +64,20 @@ public class PGNGame { return game; } + public void writePGN(PrintWriter pw) { + // Write tag pairs + tagPairs.forEach((k, v) -> pw.printf("[%s \"%s\"]%n", k, v)); + + // Write movetext + board.getLog().forEach(m -> { + if (m.activeColor == Color.BLACK) pw.printf("%d. ", m.fullmoveCounter); + pw.print(m.move); // TODO: Convert to SAN + }); + + // TODO: Write game termination marker + // TODO: Check if the game has ended + } + public String getTag(String tagName) { return tagPairs.get(tagName); } public boolean hasTag(String tagName) { return tagPairs.containsKey(tagName); } From cf5b507f00bfeb5eee332ee3871261dc64a6b2bf Mon Sep 17 00:00:00 2001 From: CyB3RC0nN0R Date: Fri, 8 Nov 2019 15:22:12 +0100 Subject: [PATCH 2/9] Added a PGN file saving routine + showFileSaveDialog in DialogUtil + Javadoc in Log and PGNDatabase + "Save game file" button in MenuBar + saveFile method in MainWindow + Formatting and result tag in PGNGame tag pair serialization --- src/dev/kske/chess/board/Log.java | 20 ++++++++++++++++- src/dev/kske/chess/pgn/PGNDatabase.java | 20 ++++++++++++++++- src/dev/kske/chess/pgn/PGNGame.java | 18 +++++++++++---- src/dev/kske/chess/ui/DialogUtil.java | 30 ++++++++++++------------- src/dev/kske/chess/ui/MainWindow.java | 22 ++++++++++++++++++ src/dev/kske/chess/ui/MenuBar.java | 13 ++++++++++- 6 files changed, 100 insertions(+), 23 deletions(-) diff --git a/src/dev/kske/chess/board/Log.java b/src/dev/kske/chess/board/Log.java index d31f5de..95b1433 100644 --- a/src/dev/kske/chess/board/Log.java +++ b/src/dev/kske/chess/board/Log.java @@ -49,6 +49,11 @@ public class Log implements Iterable { } } + /** + * @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() { @@ -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() { @@ -165,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(); @@ -173,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) diff --git a/src/dev/kske/chess/pgn/PGNDatabase.java b/src/dev/kske/chess/pgn/PGNDatabase.java index e00a4ad..127f9a6 100644 --- a/src/dev/kske/chess/pgn/PGNDatabase.java +++ b/src/dev/kske/chess/pgn/PGNDatabase.java @@ -2,6 +2,7 @@ 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; @@ -13,6 +14,9 @@ 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 @@ -21,6 +25,13 @@ public class PGNDatabase { 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 { Scanner sc = new Scanner(pgnFile); while (sc.hasNext()) @@ -28,7 +39,14 @@ public class PGNDatabase { sc.close(); } - public void save(File pgnFile) throws FileNotFoundException { + /** + * 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(); diff --git a/src/dev/kske/chess/pgn/PGNGame.java b/src/dev/kske/chess/pgn/PGNGame.java index e5c35f0..5d443c8 100644 --- a/src/dev/kske/chess/pgn/PGNGame.java +++ b/src/dev/kske/chess/pgn/PGNGame.java @@ -23,7 +23,11 @@ import dev.kske.chess.exception.ChessException; 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(); @@ -65,17 +69,23 @@ public class PGNGame { } 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(); + // Write movetext board.getLog().forEach(m -> { if (m.activeColor == Color.BLACK) pw.printf("%d. ", m.fullmoveCounter); - pw.print(m.move); // TODO: Convert to SAN + pw.printf("%s ", m.move); // TODO: Convert to SAN }); - // TODO: Write game termination marker - // TODO: Check if the game has ended + // Write game termination marker + pw.print(tagPairs.get("Result")); } public String getTag(String tagName) { return tagPairs.get(tagName); } diff --git a/src/dev/kske/chess/ui/DialogUtil.java b/src/dev/kske/chess/ui/DialogUtil.java index 9a70d7c..5c884b5 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; @@ -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(); diff --git a/src/dev/kske/chess/ui/MainWindow.java b/src/dev/kske/chess/ui/MainWindow.java index b4c8b5e..1fe3960 100644 --- a/src/dev/kske/chess/ui/MainWindow.java +++ b/src/dev/kske/chess/ui/MainWindow.java @@ -4,6 +4,7 @@ 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 +165,25 @@ public class MainWindow extends JFrame { } }); } + + public void saveFile(File file) { + final int dotIndex = file.getName().lastIndexOf('.'); + final String name = file.getName().substring(0, dotIndex); + 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); + } 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(); } From 21199dede549c586f1521d8507d0eaab939d1804 Mon Sep 17 00:00:00 2001 From: CyB3RC0nN0R Date: Tue, 19 Nov 2019 06:30:53 +0100 Subject: [PATCH 3/9] Created Move#fromSAN, moved implementation from Board --- src/dev/kske/chess/board/Board.java | 80 +----------------------- src/dev/kske/chess/board/Move.java | 96 +++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 79 deletions(-) diff --git a/src/dev/kske/chess/board/Board.java b/src/dev/kske/chess/board/Board.java index 327e7e3..3288e2b 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; @@ -98,83 +96,7 @@ public class Board { * @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)); } /** diff --git a/src/dev/kske/chess/board/Move.java b/src/dev/kske/chess/board/Move.java index a03a78c..6196cdf 100644 --- a/src/dev/kske/chess/board/Move.java +++ b/src/dev/kske/chess/board/Move.java @@ -1,6 +1,12 @@ 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
@@ -53,6 +59,96 @@ 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 (NoSuchMethodException | SecurityException | IllegalArgumentException 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 (NoSuchMethodException | SecurityException | IllegalArgumentException 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) { return null; } + public boolean isHorizontal() { return getyDist() == 0; } public boolean isVertical() { return getxDist() == 0; } From 927d5ed254053b1a38c4be02faae00ef0d2d452b Mon Sep 17 00:00:00 2001 From: CyB3RC0nN0R Date: Sun, 24 Nov 2019 21:31:32 +0100 Subject: [PATCH 4/9] Working on SAN serialization of moves + SAN generation for castling moves + Appending promotion piece symbol in pawn promotion SAN + Asking the user to view a generated PGN file --- src/dev/kske/chess/board/Castling.java | 11 ++++++- src/dev/kske/chess/board/Move.java | 32 +++++++++++++++---- src/dev/kske/chess/board/Pawn.java | 4 +-- src/dev/kske/chess/board/PawnPromotion.java | 35 ++++++++++----------- src/dev/kske/chess/ui/MainWindow.java | 8 ++++- 5 files changed, 62 insertions(+), 28 deletions(-) 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/Move.java b/src/dev/kske/chess/board/Move.java index 6196cdf..d3841c8 100644 --- a/src/dev/kske/chess/board/Move.java +++ b/src/dev/kske/chess/board/Move.java @@ -12,7 +12,7 @@ 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 */ @@ -50,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; } @@ -62,7 +62,7 @@ public class Move { /** * 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 @@ -112,7 +112,7 @@ public class Move { if (m.group("promotedTo") != null) { try { move = new PawnPromotion(pos, dest, Piece.fromFirstChar(m.group("promotedTo").charAt(0))); - } catch (NoSuchMethodException | SecurityException | IllegalArgumentException e) { + } catch (Exception e) { e.printStackTrace(); } } else move = new Move(pos, dest); @@ -130,7 +130,7 @@ public class Move { if (m.group("promotedTo") != null) { try { move = new PawnPromotion(pos, dest, Piece.fromFirstChar(m.group("promotedTo").charAt(0))); - } catch (NoSuchMethodException | SecurityException | IllegalArgumentException e) { + } catch (Exception e) { e.printStackTrace(); } } else move = new Move(pos, dest); @@ -147,7 +147,27 @@ public class Move { return null; } - public String toSAN(Board board) { 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 + // TODO: Omit if the move is a pawn push + 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; } 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/ui/MainWindow.java b/src/dev/kske/chess/ui/MainWindow.java index 1fe3960..3dc54b5 100644 --- a/src/dev/kske/chess/ui/MainWindow.java +++ b/src/dev/kske/chess/ui/MainWindow.java @@ -1,5 +1,6 @@ package dev.kske.chess.ui; +import java.awt.Desktop; import java.awt.EventQueue; import java.awt.Toolkit; import java.awt.dnd.DropTarget; @@ -168,7 +169,6 @@ public class MainWindow extends JFrame { public void saveFile(File file) { final int dotIndex = file.getName().lastIndexOf('.'); - final String name = file.getName().substring(0, dotIndex); final String extension = file.getName().substring(dotIndex).toLowerCase(); if (extension.equals(".pgn")) try { @@ -178,6 +178,12 @@ public class MainWindow extends JFrame { 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, From 719e4f99efc19b20835d14517e7076e8352472c9 Mon Sep 17 00:00:00 2001 From: CyB3RC0nN0R Date: Fri, 6 Dec 2019 23:54:11 +0100 Subject: [PATCH 5/9] Implemented saving to PGN file + copyVariations parameter in copy constructors of Board and Log This procedure still required work in the form of efficiently rewinding the board to the first position for SAN move extraction and shortening SAN moves to the smallest possible representation. --- src/dev/kske/chess/board/Board.java | 40 +++++++++++++----------- src/dev/kske/chess/board/Log.java | 14 ++++----- src/dev/kske/chess/board/Move.java | 4 +-- src/dev/kske/chess/board/MoveNode.java | 12 +++---- src/dev/kske/chess/game/ai/AIPlayer.java | 4 +-- src/dev/kske/chess/pgn/PGNGame.java | 28 +++++++++++++---- src/dev/kske/chess/ui/DialogUtil.java | 6 ++-- test/dev/kske/chess/board/BoardTest.java | 2 +- 8 files changed, 65 insertions(+), 45 deletions(-) diff --git a/src/dev/kske/chess/board/Board.java b/src/dev/kske/chess/board/Board.java index 3288e2b..114f32e 100644 --- a/src/dev/kske/chess/board/Board.java +++ b/src/dev/kske/chess/board/Board.java @@ -13,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 */ @@ -32,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; @@ -45,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 */ @@ -73,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) { @@ -92,7 +96,7 @@ 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) { @@ -118,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 */ @@ -134,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 */ @@ -142,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} @@ -159,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 */ @@ -256,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 @@ -271,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 @@ -287,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 @@ -304,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 */ @@ -324,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 */ @@ -332,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/Log.java b/src/dev/kske/chess/board/Log.java index 95b1433..d8421e4 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,7 +43,7 @@ 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; } @@ -76,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 @@ -140,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) { @@ -186,7 +186,7 @@ public class Log implements Iterable { * 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 @@ -244,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 fullmoveCounter) { fullmoveNumber = fullmoveCounter; } public int getHalfmoveClock() { return halfmoveClock; } diff --git a/src/dev/kske/chess/board/Move.java b/src/dev/kske/chess/board/Move.java index d3841c8..2ff63e2 100644 --- a/src/dev/kske/chess/board/Move.java +++ b/src/dev/kske/chess/board/Move.java @@ -157,8 +157,8 @@ public class Move { // Position // TODO: Deconstruct position into optional file or rank - // TODO: Omit if the move is a pawn push - sb.append(pos.toLAN()); + // 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'); 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/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/PGNGame.java b/src/dev/kske/chess/pgn/PGNGame.java index 5d443c8..9308b47 100644 --- a/src/dev/kske/chess/pgn/PGNGame.java +++ b/src/dev/kske/chess/pgn/PGNGame.java @@ -1,7 +1,9 @@ package dev.kske.chess.pgn; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Scanner; import java.util.regex.MatchResult; @@ -9,14 +11,14 @@ import java.util.regex.Pattern; import dev.kske.chess.board.Board; import dev.kske.chess.board.FENString; -import dev.kske.chess.board.Piece.Color; +import dev.kske.chess.board.Move; 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 */ @@ -78,11 +80,25 @@ public class PGNGame { // Insert newline if tags were printed if (!tagPairs.isEmpty()) pw.println(); + // Collect SAN moves + Board clone = new Board(board, true); + List sanMoves = new ArrayList<>(); + + while (clone.getLog().hasParent()) { + Move move = clone.getLog().getLast().move; + clone.revert(); + sanMoves.add(move.toSAN(clone)); + } + // Write movetext - board.getLog().forEach(m -> { - if (m.activeColor == Color.BLACK) pw.printf("%d. ", m.fullmoveCounter); - pw.printf("%s ", m.move); // TODO: Convert to SAN - }); + for (int i = sanMoves.size() - 1; i >= 0; i--) + pw.printf("%s ", sanMoves.get(i)); + + // Write movetext + // board.getLog().forEach(m -> { + // if (m.activeColor == Color.BLACK) pw.printf("%d. ", m.fullmoveCounter); + // pw.printf("%s ", m.move.toSAN(board)); + // }); // Write game termination marker pw.print(tagPairs.get("Result")); diff --git a/src/dev/kske/chess/ui/DialogUtil.java b/src/dev/kske/chess/ui/DialogUtil.java index 5c884b5..0621666 100644 --- a/src/dev/kske/chess/ui/DialogUtil.java +++ b/src/dev/kske/chess/ui/DialogUtil.java @@ -24,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 */ @@ -62,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); @@ -72,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/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()); From 546b81834521af6c1c18f3b0b254f8df16cb0d8d Mon Sep 17 00:00:00 2001 From: CyB3RC0nN0R Date: Sun, 8 Dec 2019 22:15:29 +0100 Subject: [PATCH 6/9] Including fullmove counters in PGN export --- src/dev/kske/chess/pgn/PGNGame.java | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/dev/kske/chess/pgn/PGNGame.java b/src/dev/kske/chess/pgn/PGNGame.java index 9308b47..8a86f04 100644 --- a/src/dev/kske/chess/pgn/PGNGame.java +++ b/src/dev/kske/chess/pgn/PGNGame.java @@ -2,6 +2,7 @@ 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; @@ -12,6 +13,7 @@ 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; /** @@ -81,24 +83,27 @@ public class PGNGame { if (!tagPairs.isEmpty()) pw.println(); // Collect SAN moves - Board clone = new Board(board, true); - List sanMoves = new ArrayList<>(); - + Board clone = new Board(board, true); + List chunks = new ArrayList<>(); while (clone.getLog().hasParent()) { Move move = clone.getLog().getLast().move; clone.revert(); - sanMoves.add(move.toSAN(clone)); + String chunk = clone.getLog().getLast().activeColor == Color.WHITE ? String.format(" %d. ", clone.getLog().getLast().fullmoveCounter) + : " "; + chunk += move.toSAN(clone); + chunks.add(chunk); } + Collections.reverse(chunks); // Write movetext - for (int i = sanMoves.size() - 1; i >= 0; i--) - pw.printf("%s ", sanMoves.get(i)); - - // Write movetext - // board.getLog().forEach(m -> { - // if (m.activeColor == Color.BLACK) pw.printf("%d. ", m.fullmoveCounter); - // pw.printf("%s ", m.move.toSAN(board)); - // }); + String line = ""; + for (String chunk : chunks) + if (line.length() + chunk.length() <= 80) line += chunk; + else { + // Flush line + pw.println(line); + line = ""; + } // Write game termination marker pw.print(tagPairs.get("Result")); From 1fd1540d6005b47e51fa9479e7bfb84ad4fd5730 Mon Sep 17 00:00:00 2001 From: CyB3RC0nN0R Date: Tue, 10 Dec 2019 21:27:06 +0100 Subject: [PATCH 7/9] Fixed fullmove counters in PGN export --- src/dev/kske/chess/pgn/PGNGame.java | 44 +++++++++++++++-------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/src/dev/kske/chess/pgn/PGNGame.java b/src/dev/kske/chess/pgn/PGNGame.java index 8a86f04..2d91930 100644 --- a/src/dev/kske/chess/pgn/PGNGame.java +++ b/src/dev/kske/chess/pgn/PGNGame.java @@ -82,29 +82,31 @@ public class PGNGame { // Insert newline if tags were printed if (!tagPairs.isEmpty()) pw.println(); - // Collect SAN moves - Board clone = new Board(board, true); - List chunks = new ArrayList<>(); - while (clone.getLog().hasParent()) { - Move move = clone.getLog().getLast().move; - clone.revert(); - String chunk = clone.getLog().getLast().activeColor == Color.WHITE ? String.format(" %d. ", clone.getLog().getLast().fullmoveCounter) - : " "; - 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 { - // Flush line - pw.println(line); - line = ""; + 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 = ""; + } + if (!line.isEmpty()) pw.println(line); + } // Write game termination marker pw.print(tagPairs.get("Result")); } From 3e5d584e6f14cfc0fb82913c79844c3847faf250 Mon Sep 17 00:00:00 2001 From: CyB3RC0nN0R Date: Wed, 11 Dec 2019 07:45:39 +0100 Subject: [PATCH 8/9] Fixed missing end of line chunk in movetext --- src/dev/kske/chess/pgn/PGNGame.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dev/kske/chess/pgn/PGNGame.java b/src/dev/kske/chess/pgn/PGNGame.java index 2d91930..609a8f9 100644 --- a/src/dev/kske/chess/pgn/PGNGame.java +++ b/src/dev/kske/chess/pgn/PGNGame.java @@ -103,7 +103,7 @@ public class PGNGame { if (line.length() + chunk.length() <= 80) line += chunk; else { pw.println(line); - line = ""; + line = chunk; } if (!line.isEmpty()) pw.println(line); } From d0cfa4c1ffb400941ce89d851f09b608c76a835d Mon Sep 17 00:00:00 2001 From: CyB3RC0nN0R Date: Wed, 11 Dec 2019 07:53:44 +0100 Subject: [PATCH 9/9] Fixed setter parameter naming for fullmove number in Log --- src/dev/kske/chess/board/Log.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dev/kske/chess/board/Log.java b/src/dev/kske/chess/board/Log.java index d8421e4..ba4c509 100644 --- a/src/dev/kske/chess/board/Log.java +++ b/src/dev/kske/chess/board/Log.java @@ -244,7 +244,7 @@ public class Log implements Iterable { public int getFullmoveNumber() { return fullmoveNumber; } - public void setFullmoveNumber(int fullmoveCounter) { fullmoveNumber = fullmoveCounter; } + public void setFullmoveNumber(int fullmoveNumber) { this.fullmoveNumber = fullmoveNumber; } public int getHalfmoveClock() { return halfmoveClock; }