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):
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(®ex, "^\\[([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(®ex, 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(®ex);
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;
// }