Improved PGN parsing, added PGN file loading
This commit is contained in:
		| @@ -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<String, Pattern> patterns = new HashMap<>(); | ||||
| 		patterns.put("pieceMove", | ||||
| 				Pattern.compile( | ||||
| 						"^(?<pieceType>[NBRQK])(?:(?<fromFile>[a-h])|(?<fromRank>[1-8])|(?<fromSquare>[a-h][1-8]))?x?(?<toSquare>[a-h][1-8])$")); | ||||
| 						"^(?<pieceType>[NBRQK])(?:(?<fromFile>[a-h])|(?<fromRank>[1-8])|(?<fromSquare>[a-h][1-8]))?x?(?<toSquare>[a-h][1-8])(?:\\+{0,2}|\\#)$")); | ||||
| 		patterns.put("pawnCapture", | ||||
| 				Pattern | ||||
| 					.compile("^(?<fromFile>[a-h])(?<fromRank>[1-8])?x(?<toSquare>[a-h][1-8])(?<promotedTo>[NBRQK])?$")); | ||||
| 		patterns.put("pawnPush", Pattern.compile("^(?<toSquare>[a-h][1-8])(?<promotedTo>[NBRQK])?$")); | ||||
| 					.compile( | ||||
| 							"^(?<fromFile>[a-h])(?<fromRank>[1-8])?x(?<toSquare>[a-h][1-8])(?<promotedTo>[NBRQK])?(?:\\+{0,2}|\\#)?$")); | ||||
| 		patterns.put("pawnPush", Pattern.compile("^(?<toSquare>[a-h][1-8])(?<promotedTo>[NBRQK])?(?:\\+{0,2}|\\#)$")); | ||||
| 		patterns.put("castling", Pattern.compile("^(?<queenside>O-O-O)|(?<kingside>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; | ||||
| 			} | ||||
| 		}); | ||||
|   | ||||
| @@ -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); | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -18,12 +18,14 @@ public class PGNDatabase { | ||||
|  | ||||
| 	private final List<PGNGame>	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<PGNGame> getGames() { return games; } | ||||
| } | ||||
|   | ||||
| @@ -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; | ||||
| 	} | ||||
|   | ||||
| @@ -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(); | ||||
| 					}); | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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: <strong>Chess</strong><br> | ||||
| @@ -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<String>	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(); | ||||
| 			}); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user