NAME

Game::Go::Terminal - a playable game on two filehandles

VERSION

Version 0.04

SYNOPSIS

my $t = Game::Go::Terminal->new(size => 9, level => 2);
my $result = $t->start;       # returns, never exits

DESCRIPTION

A game of Go against the bot, on in and out.

It is a separate module from Game::Go for a reason a sibling distribution paid for: that one put a thousand lines of escape codes in its top-level namespace module, which is why only its board class ever turned out to be reusable.

Why it ships

The confirmation phase is a two-person negotiation and no unit test will tell you it is unusable. This is where somebody finds out that the proposal is the wrong way round, or that the prompt never says which stones are marked, or that "dispute" reads like a refusal rather than a request to play on. Finding that out here costs an afternoon; finding it out after the JavaScript is written costs the JavaScript too.

What the display has to get right

These are rules rather than decoration:

Coordinates on both axes, A to T with I left out and rows numbered from the bottom. A Go player reads them off the edge constantly, which is not true of any other game on this roster.

The last move marked and the captures named. A capture of a large group is the most important thing that can happen in Go and it is completely invisible in an after-picture.

The confirmation phase spelled out. Two passes stop play; they do not end the game. A player told "the game is over" and then shown a board they can still type at will file a bug, and they will be right.

The final arithmetic, line by line. A board full of stones followed by "W+6.5" with no working reads as something the program made up, and komi and prisoners are both invisible on the board.

The board is drawn as a board

Grid lines, not a lattice of dots: ┌─┬─┐ with ╋ on the star points and stones sitting on the intersections. A go board has a line between every two points and drawing it costs nothing, because the space between two cells was already there.

That space is also what the cursor spends. See "pick_point".

A mark has to be a shape

The last move is drawn as a ringed stone, ◉ or ◎, and a point a capture has just emptied as ×. Both are characters, so they survive --ascii, NO_COLOR and a redirected handle.

That is not a preference, it is a bug this had. The last move was marked by wrapping the cell in \e[31m, but the cell was already \e[1;30m●\e[0m from "glyph", so the inner code won and the reset closed it: the mark was emitted on every board and never once visible. A colour laid over a colour is not a mark.

You point at the move you mean

On a terminal with Term::ReadKey installed the arrow keys walk a cursor over the board, the point under it shows the stone you would play, and return plays it. p passes, u takes your last turn back, r plays it again, t goes back to typing, ? lists the keys and q stops. r does not resign: to give up, press t and type resign. Without Term::ReadKey, or off a terminal, every move is typed as before.

Taking a turn back

u in the picker, or undo typed, goes back to the start of your previous turn: your move and the bot's reply are both taken back. r, or redo, plays them again. A new move after an undo forgets what could have been played again. With nothing to take back or play again, a line says so and nothing changes. When the bot plays black its opening move is never taken back.

The two words also work at the mark and accept or dispute prompts of the count. There undo goes back to your previous turn, which on your first turn of the count is the turn you passed on, so play resumes from before that pass and the count is explained again when play next stops.

A list would be absurd here, which is what makes this different from the other terminals on this roster. A 19x19 board offers up to 362 legal moves, so there is nothing to enumerate: the board already shows every one of them, laid out in the shape the player is thinking in. The cursor is two-dimensional for the same reason.

What the point under the cursor says

One line, from the engine rather than from arithmetic here: whether the point can be played and why not if it cannot, what the move would capture, and how many liberties the resulting group would have.

E5: a lone stone with 4 liberties
A8: takes 1, a lone stone with 3 liberties
C9: joins a group of 2 with 6 liberties
A9: that move would leave your own stones with no liberty

It is the engine's own answer because "preview" plays the point on a clone and asks the clone. Nothing is played, which matters more here than in the other games on this roster: a preview that moved the game would be worse than no preview, and the suite asserts the board and the turn are untouched after one.

A clone is 0.009 ms on 19x19, so the line can be recomputed on every keystroke without anybody noticing.

ATTRIBUTES

in, out

The two filehandles. Keeping them as attributes is what lets a test play a whole game in process against tied in-memory handles, with no fork and no subprocess to eat the test output.

size, level, colour, komi, handicap, seed, ascii, quiet

Every one is validated in the constructor. A sibling's terminal refused a bad colour with a tidy sentence and exit 2, and died on a bad variant with a raw Perl message and exit 255, because one option was checked in the constructor and the other several calls deeper.

colour accepts b, w, black, white, dark and light.

game, bot

The Game::Go being played and the Game::Go::Bot playing the other side, built in the constructor from the options above. They are readable so that a test can assert against the position rather than against the drawn board, which is the difference between a test that checks the rules and one that checks the spacing.

history

The Game::Terminal::Kit::History that "Taking a turn back" goes through, built in the constructor. It holds each position as "to_text" in Game::Go wrote it and rebuilds one with "from_text" in Game::Go.

ansi

Whether to colour the output, readable and settable. Worked out at construction if not given: colour is off when NO_COLOR is set to anything but the empty string, which is the whole point of the convention, and otherwise it is used only when out is a terminal.

keyboard, screen

The Game::Terminal::Kit::Keys the keys are read from and the Game::Terminal::Kit::Screen everything is written to, built in the constructor from in, out, keysource, pending, ansi and ascii. Either may be given to the constructor in place of the one it would build.

METHODS

start

Plays the game and returns the Game::Go::Result. It never calls exit: a module that exits cannot be tested in process. Each call starts "history" afresh from the game being played.

person_turn

Tells "history" that the turn has passed to you. "start" calls it before each of your turns, including your turns of the count.

take_back, play_again

Undo and redo: each puts the rebuilt game in place through "swap" and returns true, or returns false and changes nothing when there is no step.

swap

$terminal->swap($game);

Puts $game in place of the one being played. The last move and the captures marked on the board are read from the game, so they follow it; the explanation of the count is shown again the next time it is reached.

options

my $fields = Game::Go::Terminal->options;

The options a person would set before a game, in the order a launcher shows them. Each is a hash reference with a name, a type and a help, and min and max or choices where the type has them. A new list is returned each time.

size

One of 9, 13, 19: the board.

level

A whole number from 1 to 5: how hard the program plays.

colour

One of black, white: your stones.

handicap

A whole number from 2 to 9: stones for black; 2 to 5 on the smaller boards.

komi

Text: a multiple of 0.5; 6.5 by default.

seed

Text: the seed the bot plays from.

run

exit Game::Go::Terminal->run(@ARGV);

The whole command line program as a class method: parses the options bin/go documents, builds the terminal, plays, and returns the exit status rather than calling exit: 0 for a win or a game with no winner, 1 for a loss, 2 for a bad option, a leftover word or a value the terminal refuses, with the reason and the usage on STDERR. --help prints the usage on STDOUT and returns 0. Signal handlers are local to the call and the terminal is left in cooked mode on every way out, so a launcher such as Game::Terminal can call it in its own process and carry on afterwards. Colour is off when NO_COLOR is set and not empty, as it is off a terminal.

board_text

The board as lines, coordinates and all.

glyph

interactive, clear

Whether out is a terminal, and the screen clear the picker does before each frame, which takes what has scrolled off with it. A 19x19 board is twenty-three lines, so a picker that scrolled one of them per keystroke would be unusable.

picking

Whether a point is pointed at rather than typed. Settable, because a failed enter_raw and the t key turn it off for the rest of the game. Unset, it follows "keys_available".

keys_available, enter_raw, leave_raw

Whether there is anything to read keys with, and cbreak on and off. cbreak rather than raw, so an interrupt stays an interrupt. The typed prompts turn it off, so what is typed at them is echoed.

keysource, pending, raw

A coderef returning ONE character in place of Term::ReadKey, characters read and given back, and the cbreak state. keysource is what lets t/23 drive the whole key loop with no terminal and no Term::ReadKey installed; one character a call, because a source handing back "\e[C" whole never becomes a right arrow. keysource and pending are read when the terminal is built, and raw is read only.

read_key

One keystroke: the character itself, or a name from "The names" in Game::Terminal::Kit::Keys for a key that has one. A name is always longer than one character so it never collides with one, and an escape followed by something that is not a sequence leaves that key to be read next.

pick_point, pick_help

The cursor loop and its key list. pick_point returns true if a turn was taken and false to stop, the same contract as the typed turn. An arrow that walks off an edge comes back on at the opposite one, and home and end go to the two ends of the row.

preview

my $peek = $terminal->preview($colour, $point);

What playing a point would do, as { refused => $why } or { move, caps, libs, size }. Played on a clone; the game is not touched.

point_line

"preview" as the one line printed under the board.

One point's character.

SEE ALSO

Game::Go, Game::Go::Bot, bin/go.

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)