package dev.kske.chess.pgn; import java.io.PrintWriter; import java.util.*; import java.util.regex.MatchResult; 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; /** * Project: Chess
* File: PGNGame.java
* Created: 22 Sep 2019
* * @since Chess v0.5-alpha * @author Kai S. K. Engelbart */ public class PGNGame { private final Map tagPairs = new HashMap<>(7); private final Board board; /** * Creates an instance of {@link PGNGame}. A new default {@link Board} will * be * created. */ public PGNGame() { board = new Board(); } /** * Creates an instance of {@link PGNGame}. * * @param board the board associated with the game */ public PGNGame(Board board) { this.board = board; } /** * Parses a game in {@code PGN} format from a {@link Scanner} instance * * @param sc the {@link Scanner} to parse the game from, which is not closed * after this process * @return the parsed {@link PGNGame} */ public static PGNGame parse(Scanner sc) { PGNGame game = new PGNGame(); MatchResult matchResult; Pattern tagPairPattern = Pattern.compile("\\[(\\w+) \"(.*)\"]"), 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 (sc.findInLine(tagPairPattern) != null) { matchResult = sc.match(); if (matchResult.groupCount() == 2) game.setTag(matchResult.group(1), matchResult.group(2)); else break; sc.nextLine(); } // Parse movetext while (true) { // Skip NAG (Numeric Annotation Glyph) sc.skip(nagPattern); // TODO: Parse RAV (Recursive Annotation Variation) 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() + ": " + new FENString(game.board).toString() ); } else break; } else break; } // Parse game termination marker if (sc.findWithinHorizon(terminationMarkerPattern, 20) == null) System.err.println("Termination marker expected"); return game; } /** * Serializes this game to {@code PGN} format. * * @param pw the writer to write the game to */ 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(); 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 = chunk; } if (!line.isEmpty()) pw.println(line); } // Write game termination marker pw.print(tagPairs.get("Result")); } /** * @param tagName the name of a game tag * @return the value of the game tag */ public String getTag(String tagName) { return tagPairs.get(tagName); } /** * @param tagName the name of a game tag * @return {@code true} if the tag is present */ public boolean hasTag(String tagName) { return tagPairs.containsKey(tagName); } /** * Sets a game tag. * * @param tagName the name of the tag * @param tagValue the value of the tag */ public void setTag(String tagName, String tagValue) { tagPairs.put(tagName, tagValue); } /** * @return the board associated with this game */ public Board getBoard() { return board; } }