NAME
Game::Go - the rules of Go, with the board in C
VERSION
Version 0.01
SYNOPSIS
use Game::Go;
my $game = Game::Go->new(size => 9, seed => $bytes);
my $moves = $game->legal($game->turn); # plays, and a pass
my $move = $game->play(Game::Go::BLACK, $pt);
if (ref $move eq 'Game::Go::Error') {
say $move->message;
}
print $game->board->to_text;
DESCRIPTION
Go on a 9x9, 13x13 or 19x19 board, under the Japanese rules of 1989 with one declared amendment. The board, the chains and the liberties are in C behind a published ABI; this is the game over them.
It is the engine behind the go at https://peer2peergames.com.
What is here in this version
Turn order, plays, passes, the log, and replay. Resignation, timeout and abandonment.
Not here yet: the confirmation phase that Article 9 requires, and therefore scoring. Two consecutive passes stop play and put the game into the marking phase, which is as far as it goes: agreeing the dead stones and counting the territory are the next two phases of work.
A point is opaque
A point is a padded index into a board that carries a sentinel ring, so that the engine's four-neighbour walk needs no bounds test. It is not row * size + col. Build one with Game::Go::Board, or with $game->board, which speaks in columns and rows and never hands one out.
The log is the game
The event log is the canonical serialisation, not the position. For Go that is not even arguable: a position cannot say what the ko point is, so a snapshot loses the rule that makes a ko fight a ko fight, and it cannot say how many prisoners each side holds, which under Article 10.2 is half the score.
So a log carries the engine's point index and nothing prettier. A log written in coordinates reads well and replays wrongly, because replay hands each player event straight back to the method that made it.
CONSTANTS
EMPTY, BLACK, WHITE, BORDER
OK, ILL_OFF, ILL_TAKEN, ILL_KO, ILL_SUICIDE, ILL_REPEAT, ILL_COLOUR
Aliases for the values in Game::Go::Rules, where they are defined.
The ILL_ codes are what Game::Go::Engine returns. This facade turns them into Game::Go::Error objects, so a caller of Game::Go normally sees flags and sentences rather than numbers.
METHODS
new
Game::Go->new(size => 9, komi => 6.5, handicap => 0, seed => $bytes)
size must be 9, 13 or 19. komi must be a multiple of 0.5. handicap is accepted and not yet implemented: a non-zero one croaks, because the traditional placements are a convention that has to be cited before it is typed, and guessing at them would be worse than refusing.
A bad argument croaks. That is programmer error, not a refused move.
size, komi, handicap, status, phase, turn, winner, result, log
status is active or finished. phase is play or marking, and it is a separate thing from status because Article 9 stops play and ends the game in two different sentences: a game being counted has not ended.
ko
positional by default, which is the shipped rule, or simple to leave Article 6's ko rule alone and turn the superko backstop off.
simple exists for one reason: real records contain moves the shipped ruleset refuses, and a reader that could not be told to relax would refuse the corpus that is meant to be testing us. See Game::Go::SGF's lenient.
setup, first
Constructor arguments, not for general use: stones placed before anybody moves, and which colour then plays. They are what SGF's AB, AW and PL properties need, so that a foreign record's handicap stones go where that record put them rather than where this distribution's own table would.
setup is a hashref of colour letter to [$col, $row] pairs, logged as one sys setup event so a replay reproduces the position. first is b or w.
territory_map
Who owns each empty point, as a hashref of point to colour, holding only the points that belong to somebody.
It comes from the engine rather than being worked out here, and that is not laziness. Territory is a property of a region: an empty point beside one black stone on an otherwise open board is part of one big region that reaches both colours, and it belongs to nobody. A per-point neighbour check says it is black's, and an early version of the SGF writer did exactly that and gave a nearly empty board eight points of territory the scorer had given it none of.
For a page that shades the territory, and for SGF's TB and TW.
search_from, dead_guess_from, is_alive, chain_id
What a searcher needs, and the whole of it.
Object::Proto::Sugar makes the engine private, so a bot in another module cannot reach past this class to the board, and it should not: a searcher that did would be reaching past the rules.
search_from takes the root moves this class has already filtered, which is how the C never has to see the superko history: the history lives here. chain_id is the id the confirmation phase names a chain by, its lowest point, which survives a replay where the engine's own chain root does not.
seats
BLACK and WHITE.
board
A Game::Go::Board: a read-only view in columns and rows.
point, col_row
my $pt = $game->point($col, $row); # -1 off the board
my ($col, $row) = $game->col_row($pt);
The two ways across the boundary between what a person names and what play takes. They exist because a point is opaque and the engine that can build one is private: without them the only route to a point would be through legal, which suits a bot and is useless to somebody who wants to play a particular square.
events
A copy of the log.
prisoners
How many stones each colour has captured, as a hashref keyed by colour. Derived from the log rather than kept, because the log is the game.
waiting_on
The colours whose move is awaited: one while the game runs, none once it is over.
seed
The seed, and only once the game is finished. While it is running this returns undef, which is what lets a site publish the seed at the end so anyone can re-verify a bot's play without being able to read it in advance.
legal
$game->legal($colour)
The moves this colour may make now, as Game::Go::Move objects. An arrayref, empty off turn, after the end, and during confirmation.
A pass is always in the list. This is the opposite of the previous game the house built, where the pass is forced, automatic, never offered and refused if posted. In Go a pass is a move a player makes on purpose, at any time, and it is the only way a game ever reaches a score, so a list without one would describe a game that cannot end.
play
$game->play($colour, $point)
A Game::Go::Move, or a Game::Go::Error. Never dies for a refused move.
pass
A Game::Go::Move, or an error. Two consecutive passes stop play and move the game into the marking phase, per Article 9.1.
THE CONFIRMATION PHASE
Two consecutive passes stop play. They do not end the game. Article 9.2 ends it, "through confirmation and agreement by the two players about the life and death of stones and territory", and that is a negotiation rather than a computation.
It is sequential, and the site is the reason. The obvious design has both players confirming, so both seats wait; the site voids a game whose deadline passes with two seats waiting, and a game that has reached this phase has been played to the end. So the proposer marks and says done, then the answerer accepts or disputes, and exactly one seat is waiting in every reachable state.
marking
The Game::Go::Marking while there is one, and undef otherwise.
scored_by
How a scored game was scored: territory when the players agreed, area when they could not and the disputes ran out. Undef until the game is scored, and undef forever on a resignation, timeout or abandonment.
outcome
The Game::Go::Result once the game has been counted, and undef otherwise (including after a resignation, a timeout or an abandonment, none of which is counted).
It is not called result, and the name is the site's doing. result is one of four strings and goes straight into a column CHECK-constrained to exactly those; the workings, the territory counts and the prisoner fill are a different thing with a different lifetime. Naming both result would have meant the adapter reaching for a string and getting an object on the one code path where it matters most.
raw_score, raw_area
The two scorers' own numbers, as hashrefs, before they are dressed as a result. Scores in them are in tenths, because komi is fractional and the C engine has no floats.
These are public because the scorer is a separate module and the engine is private: Object::Proto::Sugar enforces that, so Game::Go::Scoring cannot reach past this class to the board, and it should not. A scorer that is a pure function of numbers is a scorer a test can feed by hand, and a page that wants to show the workings rather than print a bare margin wants these too.
star_points
The board's star points, as engine points. Nine on 19x19 and five on the other two, which is why a nine-stone handicap is a 19x19 thing.
ko_point, ko_colour
The point a ko forbids, or -1, and the one colour it forbids it to.
Public because a client has to be able to draw it. A point that is empty and refused needs a reason visible on the board, not only in the error a player gets after clicking it.
A ko restricts one player, per Article 6: "A player whose stone has been captured in a ko cannot recapture in that ko on the next move." The capturer may play the point, and on a filled board sometimes wants to.
marked_dead, marked_seki
What the confirmation phase has agreed, as arrayrefs of points, or empty when there is no confirmation phase.
disputes
How many times the answerer has sent the game back to the board.
mark
$game->mark($colour, $point)
Toggles the chain at a point between dead and not. The proposer's move.
Benson's algorithm is a veto here, not a proposal. Nothing is marked dead by default, because a dead-stone proposal is a life-and-death solver and this distribution does not have one. What it does have is the set of chains that are unconditionally alive, and a chain in that set cannot be agreed dead: the attempt is refused with alive_chain.
Without that, a player who has lost could mark the opponent's living wall dead, refuse to accept, and force the dispute path every game. With it, the worst they can mark is something merely alive, which is exactly the case Article 9.3 exists to settle by playing it out.
mark_seki
Toggles an empty point as seki. Article 8 gives seki no territory, not even its eye points, and no flood fill can see that on its own, so seki is an agreed fact rather than a detected one.
done
Ends the proposal and passes the turn to the answerer.
accept
Ends the game, scored by territory. The proposer's marks stand.
dispute
Sends the game back to the board, and the proposer gets the move.
Article 9.3: "If a player requests resumption of a stopped game, his opponent must oblige and has the right to play first." The answerer asked, so the proposer moves. This reads backwards until you see what it is for: asking to resume costs you the initiative, and that is the only thing stopping a player who is losing from asking forever.
A game may be sent back three times. After that the next stoppage has no confirmation phase at all and the game is scored by area, which needs nobody's agreement. Skipping the phase rather than forcing an acceptance is deliberate: forcing the answerer to accept would let the proposer put up an absurd dead set on the last round and win with it.
resign, timeout, abandon
The three ends that are not a score. Each finishes the game and returns the result string.
timeout and abandon are the site talking, not the engine: nothing in the rules of Go knows about a clock or a player walking away. They are here because the log has to carry them, and they are the only two kinds replay applies rather than regenerates.
replay
$game->replay(\@events)
Applies a log to a fresh game and returns the number of events seen.
Player events are handed back to the method that made them and must be accepted. sys timeout and sys abandon are applied. Every other sys event is regenerated and compared, and a disagreement croaks: a log the engine does not reproduce is a log that cannot be trusted, and that is what makes a forged one detectable.
clone
An independent game at the same position, by replaying this one's log.
sizes, other, refusal, refusals
Delegated to Game::Go::Rules.
abi_version
The version of the C ABI this build carries. A consumer requires >= the version it was written against and never ==: the table only ever grows at the end.
HOW THIS IS TESTED, AND WHAT IT IS TESTED AGAINST
There is no perft ladder for Go, and that absence is a decision rather than an omission.
A sibling distribution can check a move generator against an independently published count of positions by ply, which is the strongest kind of oracle there is: somebody else's arithmetic, arrived at by somebody else's code. Go has no equivalent. The branching factor makes a leaf count useless as an oracle even where one exists, the numbers are astronomical by depth five, nobody has published them per ruleset, and a ruleset-dependent count would compare our rules against somebody else's rather than test either. Generating a ladder from this engine and calling it one would be the engine agreeing with itself with extra steps.
What stands in its place, in the order of what each is worth:
The territory and area scorers, over the same position. Two scorers, one board, and a published condition under which they must agree exactly. This is the only differential test a distribution with one implementation of everything can have, and it runs over every game the suite plays.
The maintained structures, recomputed from scratch in Perl. The chains, the liberty counts and the zobrist key are derived quantities, so they can be worked out again from the colours alone by code that shares nothing with the C. See t/12-invariants.t, and xt/invariants-paranoid.t which does it after every move of every game.
Real records, replayed. t/05-sgf-replay.t and t/26-score-sgf.t read t/sgf/, which ships empty of records and explains why in its README.
Another engine, over GTP. Game::Go::GTP exists so that something outside this distribution can answer
final_status_list deadand play a match.Benson's published positions, which are exact, and hand-derived vectors, one per pinned rule, each with its derivation written beside it.
SEE ALSO
Game::Go::Board, Game::Go::Engine, Game::Go::Move, Game::Go::Error, Game::Go::Rules.
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)