NAME

Term::Chess - chess on the terminal against a bot

VERSION

Version 0.03

SYNOPSIS

my $t = Term::Chess->new(level => 10_000);
my $status = $t->start;          # RETURNS the exit status; never exits

Term::Chess->new(ascii => 1)->draw;

DESCRIPTION

Everything that knows about a screen, and nothing that knows a rule of chess. The rules are Chess::Plisco's, behind Term::Chess::Game; the opponent is Term::Chess::Bot. in and out are read-write properties defaulting to STDIN and STDOUT, so a test can hand it a pair of in-memory handles and read back exactly what it drew.

start returns and never calls exit. Its return value is the exit status bin/chess should use: 0 if you won, drew or quit, 1 if you lost.

The board is drawn in fixed cells, with a margin for the marks

A square is size rows tall and twice size columns wide, with one gap column between squares: 2 by 1 at size 1, 4 by 2 at size 2 (the default) and 6 by 3 at size 3. A rank is therefore always the same width whatever stands on it, 27, 43 or 59 columns with the rank label and the two margins, and nothing measures a string to decide how to pad it. The glyph sits on the middle row of its square (the lower of two) and the rank label beside it.

There is one spare column at each side, which is what the picker's marks go in. A mark replaces the gap character beside a square and never a column of the square itself, and files a and h have no gap on their outer side, so without the margins a bracket on either edge would add a column to one row and skew the board.

With colour on, the gaps and the margins are painted too, each in the colour of the square to its left, so the board is one block and not eight stripes with the terminal's background showing between them. A square then appears one column wider than its cell, and the marks sit on that colour.

The board is drawn from White's side for the seat that is White and turned round, files and all, for the seat that is Black, so a player's own king is at the bottom right as it is over a real board.

The shade of a square is a character

Without colour an empty light square is . and an empty dark square :, so the board reads under NO_COLOR, through ascii and in a file. With colour on the background carries the shade and an empty square is blank. a1 is dark: a square whose file and rank are of the same parity is dark, and light is the one place that rule is written.

The glyphs

The chess pieces come in two sets in Unicode, outline (U+2654 to U+2659) and filled (U+265A to U+265F), named white and black. On a dark terminal the filled glyph is the one that looks white, so the names are not trusted: with colour on, both sides use the filled set and the colour says the side; with colour off, White is the outline set and Black the filled set, because then shape is all there is. ascii => 1 is the FEN letters, KQRBNP for White and kqrbnp for Black.

Colour, and NO_COLOR beating everything

If NO_COLOR is set in the environment to anything but the empty string, output is uncoloured whatever else was asked for, including an explicit colour => 1. Its value is not read, so NO_COLOR=0 still means no colour; an empty NO_COLOR leaves colour as it was asked for.

The output handle gets a UTF-8 layer

Unless ascii is set, out is given an :encoding(UTF-8) layer when it is set. Without one, every glyph raises "Wide character in print", which is a warning and not an error: the board still appears, mangled, and the test that goes red is some later one watching for warnings.

The picker

With a terminal and Term::ReadKey, start offers the arrow keys instead of asking for a typed move. It is two phases, and a third for a pawn: the cursor walks the pieces that have a legal move, enter or space picks one up, then the cursor walks that piece's squares and enter or space plays. When the square is on the last rank and the piece is a pawn, the caption asks which piece it becomes: q, r, b or n, enter or space for the queen, escape back to the squares. Escape in the second phase puts the piece back.

u takes back your last move and the bot's reply to it, and r plays again what u took back, until another move is made. With nothing to take back or play again a line says so and nothing changes. The r in the question about a pawn is still the rook.

While the picker is running it is the only thing that draws a board. Its first frame is the position, so start draws one itself only when there is no picker and once more at the end.

The cursor only ever stops where a move is. The sets it walks are Plisco's own: the distinct from-squares of the legal moves and then one piece's to-squares, both in the order the board is drawn, which turns over with the board for the seat that is Black. Left and right are a ring through that order; up and down go to the nearest square on another rank, preferring the closest file, and stay put at the edges.

The marks are characters and never colours: [ ] round the cursor, ( ) round the held piece, + on an empty square it may go to and x on both sides of a square where there is something to take. The pawn taken en passant is marked where it stands, which is not the destination.

The caption under the board is Plisco's vocabulary and not arithmetic done here: what the move takes, whether it castles, promotes, checks or mates (from the SAN), whether a capture loses material (from SEE), and whether the piece under the cursor is attacked (from attacked, which reports the attacks of the side not on move).

Typed play

Without a terminal, without Term::ReadKey, or with picking set false, a move is typed at a prompt: coordinates like e2e4, or standard algebraic notation like Nf3, exd5, O-O or a8=Q. The commands are board, legal, fen, log, hint, undo, redo, resign, help and quit. undo and redo are the picker's u and r. A refused move is answered with the reason and its code.

On a terminal the screen is cleared before each new position is drawn, and the bot's announcement is printed above the board. The announcement is the move in SAN with the coordinates beside it, what it took, and whether it checks or mates, all read from Plisco. A move the bot offers and the game refuses ends the game with the code rather than asking the same deterministic bot the same question for ever.

METHODS

new

Term::Chess->new(in => $fh, out => $fh, game => $g, ascii => 0,
                 colour => undef, size => 2, level => 10_000, seat => 'p1');

in and out default to STDIN and STDOUT. colour left undefined means colour unless NO_COLOR is set; level is a node budget for the opponent, and undef lets the game's seed draw a rung. keysource, pending, picking and rewind are taken too.

keyboard

screen

The Game::Terminal::Kit::Keys the keys are read from and the Game::Terminal::Kit::Screen everything is written to. Both are built by new from in, out, keysource, pending, ascii and colour, and those accessors read and set them.

in

out

Read-write properties. Setting out also gives it a UTF-8 layer unless ascii is on, and does so at most once per handle.

game

Read-write. The Term::Chess::Game being played; start makes one if there is none.

history

The Game::Terminal::Kit::History the turns are kept in, as the game's to_text. start begins it with the game it plays; a turn is kept each time the move passes to you, and once more when the game ends, so that after start returns, an undo takes back the move that ended it. A bot's opening move is never one to take back.

person_turn

$t->person_turn;

Keeps the game as it is now in "history" as a turn. Returns the terminal.

take_back

play_again

$t->take_back;
$t->play_again;

u and undo, r and redo: the game as it was a turn back, or a turn forward again, through "swap". Each returns 1, or says there is nothing to take back or play again and returns 0 with the game unchanged.

swap

$t->swap($game, 'taken back');

Plays on with another game: it becomes "game", the screen is cleared as "clear" does with the lines given, and the board is drawn again unless the picker is running, which draws its own. Returns the terminal.

size

1, 2 or 3: how many rows tall a square is drawn, and half how many columns wide. Read-only; anything else croaks in new.

ascii

colour

Read-write. colour can be turned on and off, but a NO_COLOR that is set and not empty has already won by the time new returns.

level

The node budget handed to the opponent, or undef to let the game's seed draw a rung. Read-only: it is a property of the sitting, not of a turn.

seat

The seat the person at the keyboard plays, 'p1' or 'p2'. Read-only. The board is drawn from that seat's side.

light

$t->light('h1');    # 1

Whether a square is light. The one place the parity rule is written.

glyph

$t->glyph($piece, $colour);

The character for a Plisco piece and colour under the current ascii and colour settings, or '' for no piece.

piece_name

'white knight', or 'nothing'.

board_lines

my @lines = $t->board_lines(position => $pos, flip => 1);
my @lines = $t->board_lines(cursor => 'e2', from => 'e2', spots => ['e3', 'e4']);

The board as a list of lines, without printing anything. White is at the bottom unless flip. Every line is the same number of display columns. cursor and from are single squares and spots a list of them; a spot with a piece on it is framed as something to take and an empty one is drawn as a target. The cursor's bracket wins over the held piece's parenthesis if both are on one square.

draw

The same, printed to out. Returns the terminal.

clear

$t->clear('the bot plays Nf3 (g1f3)');

Clears the screen when out is a terminal (when "rewind" is true) and prints the lines given, so what was just said survives the clear. start calls it before every new position: after the bot's move with its announcement, after a typed move, and once more at the end. Under a pipe it only prints the lines.

describe

$t->describe($game, 'g1f3', 'the bot plays');

One line for a move: the lead, the SAN, the coordinates, what it takes, and whether it checks or mates.

start

Plays a game against the bot on in and out until somebody quits or the game ends. Returns the exit status and never calls exit: 0 if you won, drew or quit, 1 if you lost, 2 if a game could not be built.

pick_move

my $uci = $t->pick_move($game, 'p1');

Runs the picker until a move is chosen and returns it as coordinates, or the string 'quit', 'undo' for u or 'redo' for r. Draws to out and reads from keysource or the terminal; it does not play the move, so a caller still gets the game's refusal if it somehow offers one.

origins, targets

my $squares = $t->origins($pos, $flip);
my $squares = $t->targets($pos, 'e2', $flip);

The squares the side to move may move from, and the squares one piece may move to, each once and in the drawn order for $flip. Both come from Plisco's legal moves; a square whose piece cannot move is not in the first and the four promotions are one square in the second.

moves_between

my @moves = $t->moves_between($pos, 'a7', 'a8');

Plisco's legal moves from one square to another: one, or four for a promotion, or none.

step

my $i = $t->step(\@squares, $at, 'up', $flip);

The index the cursor lands on. Left, right and tab are a ring; up and down go to the nearest square on another drawn rank and then to the closest file, and stay put at the top and the bottom. Home and end are the first and the last.

victims

my $squares = $t->victims($pos, 'e5');

The squares of pawns that piece would take en passant, as a hash of square to true. Empty for everything but a pawn with that capture.

move_line, piece_line

The caption under the board: what the move from one square to another does, and what the piece on a square is and how many moves it has. Both read Plisco rather than working anything out here.

options

my $fields = Term::Chess->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.

level

A whole number from 0 to 3: a rung of the ladder, 0 the weakest.

white

One of p1, p2: whoever is White moves first.

seat

One of p1, p2: which seat you play.

size

A whole number from 1 to 3: squares of 2, 4 or 6 columns.

seed

Text: up to thirty-two bytes, for a reproducible game.

run

exit Term::Chess->run(@ARGV);

The whole command line program as a class method: parses the options bin/chess documents, builds the game and the terminal, plays, and returns the exit status rather than calling exit: 0 if you won, drew or quit, 1 if you lost, 2 for a bad option or a refused value, with the usage on STDERR. --help prints the usage on STDOUT and returns 0. Options are read from the list passed in and never from @ARGV. Signal handlers are local to the call and the terminal is given back on every way out, an interrupt included, so a launcher such as Game::Terminal can call it in its own process and carry on afterwards.

--level takes a rung index into Term::Chess::Bot's ladder or a node budget outright, and tells them apart by size; a number that is neither is refused naming both ranges.

picking

Whether the arrow keys are offered. Set it false to type moves instead; left unset it follows "keys_available", and start turns it off for the rest of the game if enter_raw fails.

rewind

Whether a redrawn board is written over the last one instead of under it. Unset it follows whether out is a terminal.

keys_available

Whether a key can be read at all: a keysource, or in being a terminal with Term::ReadKey installed. Term::ReadKey is a recommendation and not a prerequisite, so this is false on a perfectly good installation and the typed prompt is what that installation gets.

enter_raw, leave_raw

The cbreak mode round the picker. Cbreak and not raw, so an interrupt is still an interrupt. leave_raw is safe to call from a signal handler and more than once; start calls it on the way out.

keysource, pending, raw

The seam a test drives the picker through, the pushback queue for an escape sequence that turned out not to be one, and whether keys are being read one at a time. keysource is a coderef returning one character per call. raw is read-only: enter_raw and leave_raw change it.

read_key

One named key: 'up', 'enter', 'escape' and so on, or the character itself. An escape sequence that is no key the picker knows is 'unknown'.