Implemented game serialization to the PGN format #16
@ -49,6 +49,11 @@ public class Log implements Iterable<MoveNode> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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
|
@Override
|
||||||
public Iterator<MoveNode> iterator() {
|
public Iterator<MoveNode> iterator() {
|
||||||
return new Iterator<MoveNode>() {
|
return new Iterator<MoveNode>() {
|
||||||
@ -98,7 +103,7 @@ public class Log implements Iterable<MoveNode> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
* move.
|
||||||
*/
|
*/
|
||||||
public void removeLast() {
|
public void removeLast() {
|
||||||
@ -165,6 +170,10 @@ public class Log implements Iterable<MoveNode> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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() {
|
private void update() {
|
||||||
activeColor = current.activeColor;
|
activeColor = current.activeColor;
|
||||||
castlingRights = current.castlingRights.clone();
|
castlingRights = current.castlingRights.clone();
|
||||||
@ -173,6 +182,15 @@ public class Log implements Iterable<MoveNode> {
|
|||||||
halfmoveClock = current.halfmoveClock;
|
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) {
|
private void disableCastlingRights(Piece piece, Position initialPosition) {
|
||||||
// Kingside
|
// Kingside
|
||||||
if (piece instanceof King || piece instanceof Rook && initialPosition.x == 7)
|
if (piece instanceof King || piece instanceof Rook && initialPosition.x == 7)
|
||||||
|
@ -2,6 +2,7 @@ package dev.kske.chess.pgn;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -13,6 +14,9 @@ import dev.kske.chess.exception.ChessException;
|
|||||||
* Project: <strong>Chess</strong><br>
|
* Project: <strong>Chess</strong><br>
|
||||||
* File: <strong>PGNDatabase.java</strong><br>
|
* File: <strong>PGNDatabase.java</strong><br>
|
||||||
* Created: <strong>4 Oct 2019</strong><br>
|
* Created: <strong>4 Oct 2019</strong><br>
|
||||||
|
* <br>
|
||||||
|
* Contains a series of {@link PGNGame} objects that can be stored inside a PGN
|
||||||
|
* file.
|
||||||
*
|
*
|
||||||
* @since Chess v0.5-alpha
|
* @since Chess v0.5-alpha
|
||||||
* @author Kai S. K. Engelbart
|
* @author Kai S. K. Engelbart
|
||||||
@ -21,6 +25,13 @@ public class PGNDatabase {
|
|||||||
|
|
||||||
private final List<PGNGame> games = new ArrayList<>();
|
private final List<PGNGame> 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 {
|
public void load(File pgnFile) throws FileNotFoundException, ChessException {
|
||||||
Scanner sc = new Scanner(pgnFile);
|
Scanner sc = new Scanner(pgnFile);
|
||||||
while (sc.hasNext())
|
while (sc.hasNext())
|
||||||
@ -28,7 +39,14 @@ public class PGNDatabase {
|
|||||||
sc.close();
|
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);
|
PrintWriter pw = new PrintWriter(pgnFile);
|
||||||
games.forEach(g -> g.writePGN(pw));
|
games.forEach(g -> g.writePGN(pw));
|
||||||
pw.close();
|
pw.close();
|
||||||
|
@ -23,7 +23,11 @@ import dev.kske.chess.exception.ChessException;
|
|||||||
public class PGNGame {
|
public class PGNGame {
|
||||||
|
|
||||||
private final Map<String, String> tagPairs = new HashMap<>(7);
|
private final Map<String, String> 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 {
|
public static PGNGame parse(Scanner sc) throws ChessException {
|
||||||
PGNGame game = new PGNGame();
|
PGNGame game = new PGNGame();
|
||||||
@ -65,17 +69,23 @@ public class PGNGame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void writePGN(PrintWriter pw) {
|
public void writePGN(PrintWriter pw) {
|
||||||
|
// Set the unknown result tag if no result tag is specified
|
||||||
|
tagPairs.putIfAbsent("Result", "*");
|
||||||
|
|
||||||
// Write tag pairs
|
// Write tag pairs
|
||||||
tagPairs.forEach((k, v) -> pw.printf("[%s \"%s\"]%n", k, v));
|
tagPairs.forEach((k, v) -> pw.printf("[%s \"%s\"]%n", k, v));
|
||||||
|
|
||||||
|
// Insert newline if tags were printed
|
||||||
|
if (!tagPairs.isEmpty()) pw.println();
|
||||||
|
|
||||||
// Write movetext
|
// Write movetext
|
||||||
board.getLog().forEach(m -> {
|
board.getLog().forEach(m -> {
|
||||||
if (m.activeColor == Color.BLACK) pw.printf("%d. ", m.fullmoveCounter);
|
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
|
// Write game termination marker
|
||||||
// TODO: Check if the game has ended
|
pw.print(tagPairs.get("Result"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getTag(String tagName) { return tagPairs.get(tagName); }
|
public String getTag(String tagName) { return tagPairs.get(tagName); }
|
||||||
|
@ -5,6 +5,7 @@ import java.awt.Font;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
@ -15,7 +16,7 @@ import javax.swing.JFileChooser;
|
|||||||
import javax.swing.JLabel;
|
import javax.swing.JLabel;
|
||||||
import javax.swing.JOptionPane;
|
import javax.swing.JOptionPane;
|
||||||
import javax.swing.JPanel;
|
import javax.swing.JPanel;
|
||||||
import javax.swing.filechooser.FileFilter;
|
import javax.swing.filechooser.FileNameExtensionFilter;
|
||||||
|
|
||||||
import dev.kske.chess.io.EngineUtil;
|
import dev.kske.chess.io.EngineUtil;
|
||||||
|
|
||||||
@ -31,27 +32,24 @@ public class DialogUtil {
|
|||||||
|
|
||||||
private DialogUtil() {}
|
private DialogUtil() {}
|
||||||
|
|
||||||
public static void showFileSelectionDialog(Component parent, Consumer<List<File>> action) {
|
public static void showFileSelectionDialog(Component parent, Consumer<List<File>> action, Collection<FileNameExtensionFilter> filters) {
|
||||||
JFileChooser fileChooser = new JFileChooser();
|
JFileChooser fileChooser = new JFileChooser();
|
||||||
fileChooser.setCurrentDirectory(new File(System.getProperty("user.home")));
|
fileChooser.setCurrentDirectory(new File(System.getProperty("user.home")));
|
||||||
fileChooser.setAcceptAllFileFilterUsed(false);
|
fileChooser.setAcceptAllFileFilterUsed(false);
|
||||||
fileChooser.addChoosableFileFilter(new FileFilter() {
|
filters.forEach(fileChooser::addChoosableFileFilter);
|
||||||
|
|
||||||
@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"; }
|
|
||||||
});
|
|
||||||
if (fileChooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) action.accept(Arrays.asList(fileChooser.getSelectedFile()));
|
if (fileChooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) action.accept(Arrays.asList(fileChooser.getSelectedFile()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void showFileSaveDialog(Component parent, Consumer<File> action, Collection<FileNameExtensionFilter> 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<String, String> action) {
|
public static void showGameConfigurationDialog(Component parent, BiConsumer<String, String> action) {
|
||||||
JPanel dialogPanel = new JPanel();
|
JPanel dialogPanel = new JPanel();
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import java.awt.EventQueue;
|
|||||||
import java.awt.Toolkit;
|
import java.awt.Toolkit;
|
||||||
import java.awt.dnd.DropTarget;
|
import java.awt.dnd.DropTarget;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.util.List;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,13 @@ package dev.kske.chess.ui;
|
|||||||
|
|
||||||
import java.awt.Toolkit;
|
import java.awt.Toolkit;
|
||||||
import java.awt.datatransfer.StringSelection;
|
import java.awt.datatransfer.StringSelection;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
import javax.swing.JMenu;
|
import javax.swing.JMenu;
|
||||||
import javax.swing.JMenuBar;
|
import javax.swing.JMenuBar;
|
||||||
import javax.swing.JMenuItem;
|
import javax.swing.JMenuItem;
|
||||||
import javax.swing.JOptionPane;
|
import javax.swing.JOptionPane;
|
||||||
|
import javax.swing.filechooser.FileNameExtensionFilter;
|
||||||
|
|
||||||
import dev.kske.chess.board.FENString;
|
import dev.kske.chess.board.FENString;
|
||||||
import dev.kske.chess.exception.ChessException;
|
import dev.kske.chess.exception.ChessException;
|
||||||
@ -48,9 +50,18 @@ public class MenuBar extends JMenuBar {
|
|||||||
gameMenu.add(newGameMenuItem);
|
gameMenu.add(newGameMenuItem);
|
||||||
|
|
||||||
JMenuItem loadFileMenu = new JMenuItem("Load game file");
|
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);
|
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);
|
add(gameMenu);
|
||||||
newGameMenuItem.doClick();
|
newGameMenuItem.doClick();
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user