NAME

Game::Xiangqi::Terminal - the board on a terminal, and UCCI

SYNOPSIS

my $t = Game::Xiangqi::Terminal->new(level => 8000);
my $status = $t->start;          # RETURNS the exit status; never exits

Game::Xiangqi::Terminal->new(ascii => 1)->draw;

Game::Xiangqi::Terminal->new->ucci;    # speak UCCI, draw nothing

DESCRIPTION

Everything that knows about a screen, and nothing that knows about the rules. 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/xiangqi should use: 0 if you won or the game drew, 1 if you lost.

The pieces are the characters, and the two sides do not share them

A xiangqi set uses different characters for the two sides for five of the seven pieces, which is the opposite of chess. Red's general is 帥 and Black's is 將.

The table is pinned by codepoint and never by a pasted glyph, and t/19 asserts all fourteen numbers against it, because a glyph read in a diff is exactly the check that does not work: the red general's simplified form U+5E05 is not U+5E25, U+4EF5 is not the advisor U+4ED5 and U+50CC is not the horse U+508C, and a board drawn with any of them is perfectly legible and wrong.

ascii => 1 falls back to the seven Latin letters, uppercase for Red, and this is the only place in the distribution that uses them: elsewhere a chariot is a chariot and never a rook, because four of the seven pieces move differently from the chess piece whose name they would borrow.

Colour, and NO_COLOR beating everything

If NO_COLOR is set to anything but the empty string, output is uncoloured whatever else was asked for, including an explicit colour => 1. Its value does not count, so NO_COLOR=0 still means no colour.

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 board is drawn in fixed cells, with a margin for the marks

Every point occupies two display columns and every gap between points one, so a rank is always 26 columns wide whatever is standing on it. Nothing measures a string to decide how to pad it.

A rank label takes two more columns and 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 point and never a column of the point itself, and files a and i have no gap on their outer side, so without the margins a bracket on either edge would add a column to one row out of twenty and skew the board.

The marks are characters and never colours

The cursor is [ ] round the point it is on, the piece already chosen is ( ), a point that piece can move to is + if it is empty and x on both sides if there is something to take there. All four survive --ascii, NO_COLOR and the board being piped into a file, which a colour does not.

The picker

With a terminal and Term::ReadKey, start offers the arrow keys instead of asking for a typed move. It is two phases: the cursor walks the pieces that have a legal move, enter or space picks one up, and then the cursor walks that piece's points and enter or space plays. Escape puts the piece back.

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; a draw beside a picker frame puts two boards on the screen and the upper one is already stale.

The cursor only ever stops where a move is. The set it walks is the engine's own: the distinct from-points of legal for the first phase and the to-points of one piece for the second. A cursor free to roam all ninety points would spend most of its travel on points that cannot be chosen: the opening has sixteen pieces a side and seventy-four empty points, and a chariot boxed in behind its own soldier is not a stop either.

Left and right are a ring through that set in the drawn order, and tab and shift-tab do the same; up and down go to the nearest point on another rank, preferring the closest file. All of them are measured in the drawn direction, so they turn over for the second seat along with the board.

The caption under the board is the engine's vocabulary and not arithmetic done here: Game::Xiangqi::Engine's is_mate, is_ttc, is_check, is_sacrifice and is_exchange are what say a move mates, threatens mate, checks, loses the piece or trades evenly, and attacked is what says the piece under the cursor is itself in danger.

Without a terminal, without Term::ReadKey, or with picking set false, the typed prompt is offered instead and every command it ever took still works.

Undo and redo

u in the picker, or undo typed, takes back your last move and the bot's reply to it, and puts the board as it was when that turn began. r, or redo, plays it again. Each turn of yours is one step, kept from your first turn on, so a move the bot made before you had a turn is never taken back. A move played after an undo forgets what could have been played again. With nothing to take back or play again a line says so and the game is unchanged.

METHODS

new

Game::Xiangqi::Terminal->new(in => $fh, out => $fh, game => $g,
                             ascii => 0, wxf => 0, colour => undef,
                             level => 6000, 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.

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 Game::Xiangqi being played; start makes one if there is none.

ascii

colour

Read-write. colour can be turned on and off, but NO_COLOR in the environment has already won by the time new returns.

wxf

Read-write. Whether a move is shown in WXF beside its coordinates.

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, and it is also which way up the board is drawn.

board_lines

my @lines = $t->board_lines(position => $pos, flip => 1);

The board as a list of lines, without printing anything. Red is at the bottom unless flip. Every line is the same number of display columns.

draw

The same, printed to out. Returns the terminal.

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.

options

my $fields = Game::Xiangqi::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.

level

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

red

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

seat

One of p1, p2: which seat you play.

wxf

A switch: show WXF notation beside the coordinates.

seed

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

run

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

The whole command line program as a class method: parses the options bin/xiangqi 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, with 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.

board_lines, with the marks

$t->board_lines(cursor => $point, from => $point, spots => \@points);

cursor and from are single points 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 point, because the cursor is the thing that moves.

pick_move

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

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

origins, targets

my $from = $t->origins($position, $flip);
my $to   = $t->targets($position, $point, $flip);

The points a seat may move from, and the points one piece may move to, both in the order the board is drawn: the top rank first and then left to right. With $flip true the top rank is rank 0, as it is on the second seat's board. These are the two sets the cursor walks, and both come from legal.

step

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

The index the cursor lands on. Left and right are a ring, and so are tab and back_tab; up and down go to the nearest point on another drawn rank and then to the closest file, and stay put at the top and the bottom rather than wrapping. $flip turns up and down over.

move_line, piece_line

The caption under the board: what the move under the cursor does, and what the piece under it is. Both read Game::Xiangqi::Engine's vocabulary rather than working anything out here.

point_name, piece_name

'e4' and 'red chariot'. The two sides do not share a piece name any more than they share a glyph.

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, so a recorded handle gets plain frames and a screen gets one board that changes.

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. enter_raw returns the terminal, or undef when keys cannot be read. leave_raw is safe to call from a signal handler and more than once; start calls it on the way out, and run calls it again on INT, TERM, HUP and after a die, because a program that leaves a terminal in cbreak leaves the shell unusable.

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 the terminal is in cbreak mode. keysource is a coderef returning one character per call, which is what makes t/19 able to walk the cursor with no terminal anywhere. raw is read-only.

read_key

One named key: 'up', 'enter', 'escape' and so on, or the character itself. Escape on its own is 'escape'; an escape sequence that names no key is 'unknown', and the picker ignores it.

keyboard

The Game::Terminal::Kit::Keys every key and every typed line is read through. One is built from in, keysource and pending unless one is passed to new.

screen

The Game::Terminal::Kit::Screen everything is written through. One is built from out, colour, ascii and rewind unless one is passed to new.

history

The Game::Terminal::Kit::History your turns are kept in, as the game's to_text. start begins it again with the game it plays.

person_turn

Keeps the game in "history" as a turn of yours. start calls it each time the move is yours.

take_back

play_again

The undo and redo commands and the u and r keys: each puts the game it rebuilds in place of the one being played and returns 1, or says there is nothing to take back or play again and returns 0.

swap

$t->swap($game);

Plays on with $game in place of the game being played. Returns the terminal.

ucci

Speaks UCCI on in and out and draws nothing. Returns 0.

UCCI

ucci reads UCCI on in and writes it to out. Moves are ICCS coordinates, which is what this distribution stores anyway, so a log feeds an external engine without translation.

Implemented: ucci, isready, position startpos|fen ... [moves ...], go nodes N, go depth N, stop, quit, and bestmove in reply. An unknown command is ignored rather than fatal.

Not implemented, listed so nobody has to find out by trying: ponder, time controls (go time, go movetime), banmoves, setoption, and info lines during a search.