NAME
Game::Go::SGF - read and write the SGF subset a Go record needs
VERSION
Version 0.01
SYNOPSIS
my $in = Game::Go::SGF::read($text, lenient => 1);
my $game = $in->{game};
print Game::Go::SGF::write($game, black => 'Shusaku', white => 'Gennan');
DESCRIPTION
Smart Game Format, enough of it to consume a public record and to emit one.
Only the main line is read: the first child at every branch. A record's variations are somebody's analysis, and a game is what was played.
The reader is lenient on request, and strict by default
The shipped ruleset forbids suicide and enforces positional superko. Real records contain moves that neither permits, and a reader that could not be told to relax would refuse the corpus that is meant to be testing us.
The SGF specification's own execution model permits suicide outright:
When a B (resp. W) property is encountered, a stone of that color is placed
on the given position (no matter what was there before). Then the
application should check any W (resp. B) groups that are adjacent to the
stone just placed. If they have no liberties they should be removed and the
prisoner count increased accordingly. Lastly, the B (resp. W) group that the
newest stone belongs to should be checked for liberties, and if it has no
liberties, it should be removed (suicide) and the prisoner count increased
accordingly.
Note also "no matter what was there before", which is the spec saying a record may place a stone on an occupied point.
So:
lenient => 0 (default) the shipped rules. A record that breaks them is
refused, naming the move number and the reason
lenient => 1 simple ko only, so a repetition a real game
contained is not refused
Making the default lenient would have been the easy mistake: the site would then accept an imported game that its own rules refuse. A leniently read game is a record of what somebody played, not a game this engine would have allowed.
HA places nothing
The spec:
Defines the number of handicap stones (>=2). If there is a handicap, the
position should be set up with AB within the same node. HA itself doesn't
add any stones to the board, nor does it imply any particular way of placing
the handicap stones.
So the stones come from AB and HA is a cross-check: a count that disagrees with AB is refused. A reader that placed stones from HA would put them on this distribution's star points rather than the ones the game was played with, which for a free-placement record is a different game.
A pass has two spellings
A pass move is shown as '[]' or alternatively as '[tt]' (only for boards
<= 19x19), i.e. applications should be able to deal with both
representations. '[tt]' is kept for compatibility with FF[3].
Both are read. Only [] is written. tt works as a sentinel precisely because it is column 20, off any board this distribution offers, which is also why the spec limits it to boards of 19 and under.
FUNCTIONS
read
Game::Go::SGF::read($text, lenient => 0)
A hashref: the game, and the record's size, komi, handicap, moves, first, result, players, ranks, date, rules, territory and setup.
territory is the TB and TW properties, which is the agreed territory a server wrote when the game was counted. It is the only external oracle this distribution's territory scorer will ever have.
write
Game::Go::SGF::write($game, black => $name, white => $name, date => $when)
The game as SGF. TB and TW are emitted only for a game that was actually counted, because until the players agreed there is no agreed territory.
parse_result, format_result
RE in its five forms and back again:
B+3.5 a score B+R a resignation
0 a jigo B+T a timeout
Void no result
parse_result returns a hashref with a kind, and unknown rather than undef for anything it does not recognise, so a caller can tell "no result recorded" from "a result I could not read".
SEE ALSO
Game::Go::Notation, which owns the two coordinate alphabets and the gap between them.
AUTHOR
LNATION <email@lnation.org>
LICENSE AND COPYRIGHT
This software is Copyright (c) 2026 by LNATION <email@lnation.org>.
This is free software, licensed under:
The Artistic License 2.0 (GPL Compatible)