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(); }