From cbf110c28dca8d2ff879a1a301977f5c23843065 Mon Sep 17 00:00:00 2001 From: kske Date: Sun, 20 Oct 2019 08:52:51 +0200 Subject: [PATCH] Improved PGN parsing, added PGN file loading --- src/dev/kske/chess/board/Board.java | 23 ++++++++---- src/dev/kske/chess/game/Game.java | 6 +-- src/dev/kske/chess/pgn/PGNDatabase.java | 6 ++- src/dev/kske/chess/pgn/PGNGame.java | 32 ++++++++-------- src/dev/kske/chess/ui/FENDropTarget.java | 3 +- src/dev/kske/chess/ui/GamePane.java | 7 ---- src/dev/kske/chess/ui/MenuBar.java | 47 ++++++++++++++++++++---- 7 files changed, 79 insertions(+), 45 deletions(-) diff --git a/src/dev/kske/chess/board/Board.java b/src/dev/kske/chess/board/Board.java index 1a04150..435b371 100644 --- a/src/dev/kske/chess/board/Board.java +++ b/src/dev/kske/chess/board/Board.java @@ -154,8 +154,6 @@ public class Board { // Move the rook Move rookMove = move.dest.x == 6 ? new Move(7, move.pos.y, 5, move.pos.y) // Kingside : new Move(0, move.pos.y, 3, move.pos.y); // Queenside - - // Move the rook setDest(rookMove, getPos(rookMove)); setPos(rookMove, null); @@ -193,18 +191,22 @@ public class Board { Map patterns = new HashMap<>(); patterns.put("pieceMove", Pattern.compile( - "^(?[NBRQK])(?:(?[a-h])|(?[1-8])|(?[a-h][1-8]))?x?(?[a-h][1-8])$")); + "^(?[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])(?[NBRQK])?$")); - patterns.put("pawnPush", Pattern.compile("^(?[a-h][1-8])(?[NBRQK])?$")); + .compile( + "^(?[a-h])(?[1-8])?x(?[a-h][1-8])(?[NBRQK])?(?:\\+{0,2}|\\#)?$")); + patterns.put("pawnPush", Pattern.compile("^(?[a-h][1-8])(?[NBRQK])?(?:\\+{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 = Position.fromLAN(m.group("toSquare")); + Position pos = null, dest = null; + Move.Type moveType = Move.Type.NORMAL; switch (patternName) { case "pieceMove": + dest = Position.fromLAN(m.group("toSquare")); if (m.group("fromSquare") != null) pos = Position.fromLAN(m.group("fromSquare")); else { Type type = Type.fromFirstChar(m.group("pieceType").charAt(0)); @@ -222,12 +224,14 @@ public class Board { } break; case "pawnCapture": + dest = Position.fromLAN(m.group("toSquare")); char file = m.group("fromFile").charAt(0); int rank = m.group("fromRank") == null ? get(Type.PAWN, file) : Integer.parseInt(m.group("fromRank")); pos = Position.fromLAN(String.format("%c%d", file, rank)); break; case "pawnPush": + dest = Position.fromLAN(m.group("toSquare")); // TODO: Pawn promotion int step = log.getActiveColor() == Color.WHITE ? 1 : -1; @@ -236,8 +240,13 @@ public class Board { // Double step forward else pos = new Position(dest.x, dest.y + 2 * step); break; + case "castling": + pos = new Position(4, log.getActiveColor() == Color.WHITE ? 7 : 0); + dest = new Position(m.group("kingside") != null ? 6 : 2, pos.y); + moveType = Move.Type.CASTLING; + break; } - move(new Move(pos, dest)); + move(new Move(pos, dest, moveType)); return; } }); diff --git a/src/dev/kske/chess/game/Game.java b/src/dev/kske/chess/game/Game.java index 4fe88c3..13cf92b 100644 --- a/src/dev/kske/chess/game/Game.java +++ b/src/dev/kske/chess/game/Game.java @@ -32,12 +32,12 @@ public class Game { private BoardComponent boardComponent; public Game(BoardPane boardPane, String whiteName, String blackName) { - board = new Board(); + board = new Board(); init(boardPane, whiteName, blackName); } - public Game(BoardPane boardPane, String whiteName, String blackName, String fen) { - board = new Board(fen); + public Game(BoardPane boardPane, String whiteName, String blackName, Board board) { + this.board = board; init(boardPane, whiteName, blackName); } diff --git a/src/dev/kske/chess/pgn/PGNDatabase.java b/src/dev/kske/chess/pgn/PGNDatabase.java index 61ccdc6..281e962 100644 --- a/src/dev/kske/chess/pgn/PGNDatabase.java +++ b/src/dev/kske/chess/pgn/PGNDatabase.java @@ -18,12 +18,14 @@ public class PGNDatabase { private final List games = new ArrayList<>(); - public void load(File pgnFile) { + 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) { - e.printStackTrace(); + throw e; } } + + public List getGames() { return games; } } diff --git a/src/dev/kske/chess/pgn/PGNGame.java b/src/dev/kske/chess/pgn/PGNGame.java index e9d3a7a..9b56b8f 100644 --- a/src/dev/kske/chess/pgn/PGNGame.java +++ b/src/dev/kske/chess/pgn/PGNGame.java @@ -25,17 +25,15 @@ public class PGNGame { MatchResult matchResult; Pattern tagPairPattern = Pattern.compile("\\[(\\w+) \"(.*)\"]"), - movePattern = Pattern.compile("\\d+\\. (\\S+)\\s(\\S+)"), + movePattern = Pattern.compile("\\d+\\.\\s+(?:(?:(\\S+)\\s+(\\S+))|(?:O-O-O)|(?:O-O))(?:\\+{0,2}|\\#)"), nagPattern = Pattern.compile("(\\$\\d{1,3})*"), terminationMarkerPattern = Pattern.compile("1-0|0-1|1\\/2-1\\/2|\\*"); // Parse tag pairs - while (true) { - if (sc.findInLine(tagPairPattern) != null) { - matchResult = sc.match(); - if (matchResult.groupCount() == 2) game.setTag(matchResult.group(0), matchResult.group(1)); - else break; - } else break; + while (sc.findInLine(tagPairPattern) != null) { + matchResult = sc.match(); + if (matchResult.groupCount() == 2) game.setTag(matchResult.group(1), matchResult.group(2)); + else break; sc.nextLine(); } @@ -46,19 +44,19 @@ public class PGNGame { // TODO: Parse RAV (Recursive Annotation Variation) - sc.findWithinHorizon(movePattern, 20); - matchResult = sc.match(); - if (matchResult.groupCount() > 0) for (int i = 1; i < matchResult.groupCount() + 1; i++) { - game.board.move(matchResult.group(i)); - System.out.println(game.board.toFEN()); - } - else break; + if (sc.findWithinHorizon(movePattern, 20) != null) { + matchResult = sc.match(); + if (matchResult.groupCount() > 0) for (int i = 1; i < matchResult.groupCount() + 1; i++) { + game.board.move(matchResult.group(i)); + System.out.println(game.getBoard().getLog().getLast().move.toLAN() + ": " + game.board.toFEN()); + } + else break; + } else break; } // Parse game termination marker - if (sc.hasNext(terminationMarkerPattern)) { - sc.next(terminationMarkerPattern); - } else throw new ChessException("Game termination marker expected"); + if (sc.findWithinHorizon(terminationMarkerPattern, 20) == null) + System.err.println("Termination marker expected"); return game; } diff --git a/src/dev/kske/chess/ui/FENDropTarget.java b/src/dev/kske/chess/ui/FENDropTarget.java index 23a5aef..37fcdb2 100644 --- a/src/dev/kske/chess/ui/FENDropTarget.java +++ b/src/dev/kske/chess/ui/FENDropTarget.java @@ -11,6 +11,7 @@ import java.io.FileReader; import java.io.IOException; import java.util.List; +import dev.kske.chess.board.Board; import dev.kske.chess.game.Game; /** @@ -37,7 +38,7 @@ public class FENDropTarget extends DropTargetAdapter { final GamePane gamePane = mainWindow.addGamePane(); final String fen = br.readLine(); DialogUtil.showGameConfigurationDialog((whiteName, blackName) -> { - final Game game = new Game(gamePane.getBoardPane(), whiteName, blackName, fen); + final Game game = new Game(gamePane.getBoardPane(), whiteName, blackName, new Board(fen)); gamePane.setGame(game); game.start(); }); diff --git a/src/dev/kske/chess/ui/GamePane.java b/src/dev/kske/chess/ui/GamePane.java index 96cbefa..c07f59e 100644 --- a/src/dev/kske/chess/ui/GamePane.java +++ b/src/dev/kske/chess/ui/GamePane.java @@ -3,7 +3,6 @@ package dev.kske.chess.ui; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.GridLayout; -import java.awt.Insets; import java.util.Arrays; import java.util.HashSet; import java.util.Set; @@ -70,7 +69,6 @@ public class GamePane extends JComponent { toolPanel.add(btnSwapColors); GridBagConstraints gbc_toolPanel = new GridBagConstraints(); - gbc_toolPanel.insets = new Insets(0, 0, 5, 5); gbc_toolPanel.anchor = GridBagConstraints.NORTH; gbc_toolPanel.fill = GridBagConstraints.HORIZONTAL; gbc_toolPanel.gridx = 0; @@ -80,7 +78,6 @@ public class GamePane extends JComponent { moveSelectionPanel = new JPanel(); GridBagConstraints gbc_moveSelectionPanel = new GridBagConstraints(); - gbc_moveSelectionPanel.insets = new Insets(0, 0, 5, 0); gbc_moveSelectionPanel.fill = GridBagConstraints.BOTH; gbc_moveSelectionPanel.gridx = 2; gbc_moveSelectionPanel.gridy = 0; @@ -103,7 +100,6 @@ public class GamePane extends JComponent { moveSelectionPanel.add(btnLast); boardPane = new BoardPane(); GridBagConstraints gbc_boardPane = new GridBagConstraints(); - gbc_boardPane.insets = new Insets(0, 0, 5, 5); gbc_boardPane.fill = GridBagConstraints.BOTH; gbc_boardPane.gridx = 0; gbc_boardPane.gridy = 1; @@ -111,7 +107,6 @@ public class GamePane extends JComponent { JPanel numberPanel = new JPanel(new GridLayout(8, 1)); GridBagConstraints gbc_numberPanel = new GridBagConstraints(); - gbc_numberPanel.insets = new Insets(0, 0, 5, 5); gbc_numberPanel.anchor = GridBagConstraints.WEST; gbc_numberPanel.fill = GridBagConstraints.VERTICAL; gbc_numberPanel.gridx = 1; @@ -120,7 +115,6 @@ public class GamePane extends JComponent { JPanel letterPanel = new JPanel(new GridLayout(1, 8)); GridBagConstraints gbc_letterPanel = new GridBagConstraints(); - gbc_letterPanel.insets = new Insets(0, 0, 0, 5); gbc_letterPanel.anchor = GridBagConstraints.NORTH; gbc_letterPanel.fill = GridBagConstraints.HORIZONTAL; gbc_letterPanel.gridx = 0; @@ -137,7 +131,6 @@ public class GamePane extends JComponent { JScrollPane scrollPane = new JScrollPane(); GridBagConstraints gbc_scrollPane = new GridBagConstraints(); - gbc_scrollPane.insets = new Insets(0, 0, 5, 0); gbc_scrollPane.fill = GridBagConstraints.BOTH; gbc_scrollPane.gridx = 2; gbc_scrollPane.gridy = 1; diff --git a/src/dev/kske/chess/ui/MenuBar.java b/src/dev/kske/chess/ui/MenuBar.java index 588aa8c..1798ed4 100644 --- a/src/dev/kske/chess/ui/MenuBar.java +++ b/src/dev/kske/chess/ui/MenuBar.java @@ -2,16 +2,19 @@ package dev.kske.chess.ui; import java.awt.Toolkit; import java.awt.datatransfer.StringSelection; -import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import javax.swing.JComboBox; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JOptionPane; +import dev.kske.chess.board.Board; import dev.kske.chess.game.Game; +import dev.kske.chess.pgn.PGNDatabase; +import dev.kske.chess.pgn.PGNGame; /** * Project: Chess
@@ -47,20 +50,20 @@ public class MenuBar extends JMenuBar { JMenuItem loadFileMenu = new JMenuItem("Load game file"); loadFileMenu.addActionListener((evt) -> DialogUtil.showFileSelectionDialog(mainWindow, (file) -> { - final String extension = file.getName().substring(file.getName().lastIndexOf('.')).toLowerCase(); + final String name = file.getName().substring(0, file.getName().lastIndexOf('.')); + final String extension = file.getName().substring(file.getName().lastIndexOf('.')).toLowerCase(); switch (extension) { case ".fen": try { - final GamePane gamePane = mainWindow - .addGamePane(file.getName().substring(0, file.getName().lastIndexOf('.'))); + final GamePane gamePane = mainWindow.addGamePane(name); final String fen = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); DialogUtil.showGameConfigurationDialog((whiteName, blackName) -> { - final Game game = new Game(gamePane.getBoardPane(), whiteName, blackName, fen); + final Game game = new Game(gamePane.getBoardPane(), whiteName, blackName, new Board(fen)); gamePane.setGame(game); game.start(); }); - } catch (IOException e) { + } catch (Exception e) { e.printStackTrace(); JOptionPane.showMessageDialog(mainWindow, "Failed to load the file " + file.getName() + ": " + e.toString(), @@ -69,7 +72,35 @@ public class MenuBar extends JMenuBar { } break; case ".pgn": - // TODO: Load board from PGN + try { + final GamePane gamePane = mainWindow.addGamePane(name); + PGNDatabase pgnDB = new PGNDatabase(); + pgnDB.load(file); + if (pgnDB.getGames().size() > 0) { + String[] gameNames = new String[pgnDB.getGames().size()]; + for (int i = 0; i < gameNames.length; i++) { + final PGNGame game = pgnDB.getGames().get(i); + gameNames[i] = String.format("%s vs %s: %s", + game.getTag("White"), + game.getTag("Black"), + game.getTag("Result")); + } + JComboBox comboBox = new JComboBox<>(gameNames); + JOptionPane + .showMessageDialog(mainWindow, comboBox, "Select a game", JOptionPane.QUESTION_MESSAGE); + DialogUtil.showGameConfigurationDialog((whiteName, blackName) -> { + final Game game = new Game(gamePane.getBoardPane(), whiteName, blackName, + pgnDB.getGames().get(comboBox.getSelectedIndex()).getBoard()); + game.start(); + }); + } + } catch (Exception e) { + e.printStackTrace(); + JOptionPane.showMessageDialog(mainWindow, + "Failed to load the file " + file.getName() + ": " + e.toString(), + "File loading error", + JOptionPane.ERROR_MESSAGE); + } break; default: JOptionPane.showMessageDialog(mainWindow, @@ -119,7 +150,7 @@ public class MenuBar extends JMenuBar { final GamePane gamePane = mainWindow.addGamePane(); final String fen = JOptionPane.showInputDialog("Enter a FEN string: "); DialogUtil.showGameConfigurationDialog((whiteName, blackName) -> { - final Game game = new Game(gamePane.getBoardPane(), whiteName, blackName, fen); + final Game game = new Game(gamePane.getBoardPane(), whiteName, blackName, new Board(fen)); gamePane.setGame(game); game.start(); });