Task 539: .PGN File Format

Task 539: .PGN File Format

PGN File Format Specifications

The .PGN (Portable Game Notation) file format is a plain text standard for recording chess games, including metadata (tags) and move sequences. It uses ASCII text (subset of ISO 8859/1 Latin-1) for portability, with lines typically limited to 255 characters. PGN supports both import (flexible parsing) and export (strict formatting) modes. A PGN file consists of one or more games, each with a tag pair section (metadata in [Tag "Value"] format) followed by a movetext section (moves in Standard Algebraic Notation, annotations, variations, and a termination marker). Games are separated by blank lines. Character encoding is 7-bit ASCII for core elements, with optional 8-bit Latin-1 for diacritics in names/comments. No binary headers or magic numbers; it's human-readable text. File extension is .pgn; MIME type is often application/x-chess-pgn or text/plain.

List of all properties (standard tags) intrinsic to the format:

  • Event: Name of the tournament or match.
  • Site: Location of the event (city, region, country code).
  • Date: Starting date of the game (YYYY.MM.DD format).
  • Round: Playing round ordinal.
  • White: Player(s) of the white pieces.
  • Black: Player(s) of the black pieces.
  • Result: Game outcome (1-0, 0-1, 1/2-1/2, or *).
  • Annotator: Name of the annotator(s).
  • PlyCount: Total number of half-moves.
  • TimeControl: Time control details (e.g., moves/seconds or seconds+increment).
  • Mode: Playing mode (e.g., OTB, ICS).
  • Termination: Reason for game end (e.g., normal, time forfeit).
  • FEN: Forsyth-Edwards Notation for starting position.
  • SetUp: Indicates if FEN is used for a custom starting position (0 or 1).
  • WhiteTitle: FIDE title of white player (e.g., GM).
  • BlackTitle: FIDE title of black player.
  • WhiteElo: Elo rating of white player.
  • BlackElo: Elo rating of black player.
  • WhiteUSCF: USCF rating of white player.
  • BlackUSCF: USCF rating of black player.
  • WhiteNA: Network address of white player.
  • BlackNA: Network address of black player.
  • WhiteType: Type of white player (human or program).
  • BlackType: Type of black player.
  • EventDate: Start date of the event.
  • EventSponsor: Sponsor name.
  • Section: Tournament section.
  • Stage: Event stage.
  • Board: Board number.
  • Opening: Traditional opening name.
  • Variation: Opening variation.
  • SubVariation: Further refinement of variation.
  • ECO: Encyclopedia of Chess Openings code.
  • NIC: New in Chess code.
  • Time: Local start time (HH:MM:SS).
  • UTCTime: UTC start time.
  • UTCDate: UTC start date.
  • EventType: Type of event (e.g., Swiss).
  • Variant: Chess variant (e.g., standard).

These are the standard tags; custom tags are allowed but not listed here. Properties are stored as key-value pairs in the tag section.

Two direct download links for .PGN files:

HTML/JavaScript for drag-and-drop .PGN file dumper (embeddable in a blog post):

Drag and drop .PGN file here

Python class for .PGN handling:

import re
import os

class PGNHandler:
    def __init__(self):
        self.properties = {}
        self.movetext = ''

    def read(self, filepath):
        if not os.path.exists(filepath) or not filepath.endswith('.pgn'):
            raise ValueError("Invalid .PGN file path.")
        with open(filepath, 'r', encoding='latin-1') as f:
            content = f.read()
        lines = content.split('\n')
        in_tags = True
        self.properties = {}
        self.movetext = []
        for line in lines:
            line = line.strip()
            if in_tags:
                match = re.match(r'^\[(\w+)\s+"(.*)"\]$', line)
                if match:
                    self.properties[match.group(1)] = match.group(2)
                elif line == '' or re.match(r'^\d', line):
                    in_tags = False
            if not in_tags and line:
                self.movetext.append(line)
        self.movetext = ' '.join(self.movetext)

    def print_properties(self):
        for key, value in self.properties.items():
            print(f"{key}: {value}")

    def write(self, filepath, properties=None, movetext='*'):
        if properties:
            self.properties = properties
        with open(filepath, 'w', encoding='latin-1') as f:
            for key, value in sorted(self.properties.items(), key=lambda x: x[0]):
                f.write(f'[{key} "{value}"]\n')
            f.write('\n')
            f.write(movetext + '\n')

# Example usage:
# handler = PGNHandler()
# handler.read('example.pgn')
# handler.print_properties()
# handler.write('new.pgn', {'Event': 'Test', 'Result': '*'})

Java class for .PGN handling:

import java.io.*;
import java.util.*;
import java.util.regex.*;

public class PGNHandler {
    private Map<String, String> properties = new HashMap<>();
    private String movetext = "";

    public void read(String filepath) throws IOException {
        if (!filepath.endsWith(".pgn")) {
            throw new IllegalArgumentException("Invalid .PGN file path.");
        }
        properties.clear();
        movetext = "";
        boolean inTags = true;
        List<String> moves = new ArrayList<>();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(filepath), "ISO-8859-1"))) {
            String line;
            while ((line = reader.readLine()) != null) {
                line = line.trim();
                if (inTags) {
                    Pattern pattern = Pattern.compile("^\\[(\\w+)\\s+\"(.*)\"\\]$");
                    Matcher matcher = pattern.matcher(line);
                    if (matcher.matches()) {
                        properties.put(matcher.group(1), matcher.group(2));
                    } else if (line.isEmpty() || line.matches("^\\d")) {
                        inTags = false;
                    }
                }
                if (!inTags && !line.isEmpty()) {
                    moves.add(line);
                }
            }
        }
        movetext = String.join(" ", moves);
    }

    public void printProperties() {
        for (Map.Entry<String, String> entry : properties.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }

    public void write(String filepath, Map<String, String> newProperties, String newMovetext) throws IOException {
        if (newProperties != null) {
            properties = newProperties;
        }
        if (newMovetext != null) {
            movetext = newMovetext;
        }
        try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(filepath), "ISO-8859-1"))) {
            List<String> keys = new ArrayList<>(properties.keySet());
            Collections.sort(keys);
            for (String key : keys) {
                writer.write("[" + key + " \"" + properties.get(key) + "\"]\n");
            }
            writer.write("\n");
            writer.write(movetext + "\n");
        }
    }

    // Example usage:
    // public static void main(String[] args) throws IOException {
    //     PGNHandler handler = new PGNHandler();
    //     handler.read("example.pgn");
    //     handler.printProperties();
    //     Map<String, String> props = new HashMap<>();
    //     props.put("Event", "Test");
    //     props.put("Result", "*");
    //     handler.write("new.pgn", props, "*");
    // }
}

JavaScript class for .PGN handling (browser-compatible, uses async for read):

class PGNHandler {
    constructor() {
        this.properties = {};
        this.movetext = '';
    }

    async read(file) {
        if (!file.name.endsWith('.pgn')) {
            throw new Error('Invalid .PGN file.');
        }
        const content = await file.text();
        const lines = content.split('\n');
        let inTags = true;
        this.properties = {};
        const moves = [];
        for (let line of lines) {
            line = line.trim();
            if (inTags) {
                const match = line.match(/^\[(\w+)\s+"(.*)"\]$/);
                if (match) {
                    this.properties[match[1]] = match[2];
                } else if (line === '' || /^\d/.test(line)) {
                    inTags = false;
                }
            }
            if (!inTags && line) {
                moves.push(line);
            }
        }
        this.movetext = moves.join(' ');
    }

    printProperties() {
        for (let key in this.properties) {
            console.log(`${key}: ${this.properties[key]}`);
        }
    }

    write(properties = null, movetext = '*') {
        if (properties) {
            this.properties = properties;
        }
        let output = '';
        const keys = Object.keys(this.properties).sort();
        for (let key of keys) {
            output += `[${key} "${this.properties[key]}"]\n`;
        }
        output += '\n' + movetext + '\n';
        // For browser, could use Blob to download
        const blob = new Blob([output], { type: 'text/plain' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = 'new.pgn';
        a.click();
        URL.revokeObjectURL(url);
    }
}

// Example usage:
// const handler = new PGNHandler();
// // Assume file is a File object from input or drop
// handler.read(file).then(() => handler.printProperties());
// handler.write({ Event: 'Test', Result: '*' });

C "class" (struct with functions) for .PGN handling:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <regex.h>  // For regex; may need -lregex on compile

#define MAX_TAGS 50
#define MAX_KEY_LEN 50
#define MAX_VAL_LEN 255
#define MAX_MOVETEXT 4096

typedef struct {
    char keys[MAX_TAGS][MAX_KEY_LEN];
    char values[MAX_TAGS][MAX_VAL_LEN];
    int tag_count;
    char movetext[MAX_MOVETEXT];
} PGNHandler;

void init_PGNHandler(PGNHandler *handler) {
    handler->tag_count = 0;
    memset(handler->movetext, 0, MAX_MOVETEXT);
}

int read_PGN(PGNHandler *handler, const char *filepath) {
    if (strstr(filepath, ".pgn") != filepath + strlen(filepath) - 4) {
        fprintf(stderr, "Invalid .PGN file path.\n");
        return 1;
    }
    FILE *file = fopen(filepath, "r");
    if (!file) {
        fprintf(stderr, "Failed to open file.\n");
        return 1;
    }
    char line[512];
    int in_tags = 1;
    char temp_movetext[MAX_MOVETEXT] = {0};
    regex_t regex;
    regcomp(&regex, "^\\[([a-zA-Z0-9_]+)\\s+\"(.*)\"\\]$", REG_EXTENDED);
    while (fgets(line, sizeof(line), file)) {
        char *trimmed = strtok(line, "\r\n");  // Trim newline
        if (!trimmed) continue;
        if (in_tags) {
            regmatch_t matches[3];
            if (regexec(&regex, trimmed, 3, matches, 0) == 0) {
                if (handler->tag_count < MAX_TAGS) {
                    int key_len = matches[1].rm_eo - matches[1].rm_so;
                    int val_len = matches[2].rm_eo - matches[2].rm_so;
                    strncpy(handler->keys[handler->tag_count], trimmed + matches[1].rm_so, key_len);
                    handler->keys[handler->tag_count][key_len] = '\0';
                    strncpy(handler->values[handler->tag_count], trimmed + matches[2].rm_so, val_len);
                    handler->values[handler->tag_count][val_len] = '\0';
                    handler->tag_count++;
                }
            } else if (strlen(trimmed) == 0 || (trimmed[0] >= '0' && trimmed[0] <= '9')) {
                in_tags = 0;
            }
        }
        if (!in_tags && strlen(trimmed) > 0) {
            strcat(temp_movetext, trimmed);
            strcat(temp_movetext, " ");
        }
    }
    strcpy(handler->movetext, temp_movetext);
    regfree(&regex);
    fclose(file);
    return 0;
}

void print_properties(PGNHandler *handler) {
    for (int i = 0; i < handler->tag_count; i++) {
        printf("%s: %s\n", handler->keys[i], handler->values[i]);
    }
}

int write_PGN(PGNHandler *handler, const char *filepath, const char (*new_keys)[MAX_KEY_LEN], const char (*new_values)[MAX_VAL_LEN], int new_count, const char *new_movetext) {
    FILE *file = fopen(filepath, "w");
    if (!file) {
        fprintf(stderr, "Failed to write file.\n");
        return 1;
    }
    // If new properties provided, use them
    int count = new_count > 0 ? new_count : handler->tag_count;
    const char (*keys)[MAX_KEY_LEN] = new_count > 0 ? new_keys : handler->keys;
    const char (*values)[MAX_VAL_LEN] = new_count > 0 ? new_values : handler->values;
    // Sort keys (simple bubble sort for demo)
    char sorted_keys[MAX_TAGS][MAX_KEY_LEN];
    char sorted_values[MAX_TAGS][MAX_VAL_LEN];
    memcpy(sorted_keys, keys, sizeof(sorted_keys));
    memcpy(sorted_values, values, sizeof(sorted_values));
    for (int i = 0; i < count - 1; i++) {
        for (int j = 0; j < count - i - 1; j++) {
            if (strcmp(sorted_keys[j], sorted_keys[j + 1]) > 0) {
                char temp_key[MAX_KEY_LEN], temp_val[MAX_VAL_LEN];
                strcpy(temp_key, sorted_keys[j]);
                strcpy(temp_val, sorted_values[j]);
                strcpy(sorted_keys[j], sorted_keys[j + 1]);
                strcpy(sorted_values[j], sorted_values[j + 1]);
                strcpy(sorted_keys[j + 1], temp_key);
                strcpy(sorted_values[j + 1], temp_val);
            }
        }
    }
    for (int i = 0; i < count; i++) {
        fprintf(file, "[%s \"%s\"]\n", sorted_keys[i], sorted_values[i]);
    }
    fprintf(file, "\n");
    fprintf(file, "%s\n", new_movetext ? new_movetext : handler->movetext);
    fclose(file);
    return 0;
}

// Example usage:
// int main() {
//     PGNHandler handler;
//     init_PGNHandler(&handler);
//     read_PGN(&handler, "example.pgn");
//     print_properties(&handler);
//     // For write: define new_keys, new_values arrays
//     return 0;
// }