NAME
Game::Schnapsen::Terminal - the whole of the input and output
VERSION
Version 0.04
SYNOPSIS
Game::Schnapsen::Terminal->new(
variant => 'schnapsen',
mode => 'bot',
level => 2,
seat => 'p1',
)->play(seed => $thirty_two_bytes);
# and with no terminal at all, which is how it is tested
my $t = Game::Schnapsen::Terminal->new(
variant => 'sixtysix', mode => 'watch',
in => $read_handle, out => $write_handle, ascii => 1, colour => 0);
DESCRIPTION
Nothing else in this distribution prints, reads a handle, or calls rand. This is the only module that does any of the three.
in and out are attributes rather than STDIN and STDOUT, which is what makes the whole of it testable with no terminal present. A version that reached for the real handles would be exercised only by a person sitting in front of it.
The score line runs the way its game runs
Sixty-Six counts up to seven and Schnapsen counts down to zero, so the line says "Game points" for one and "Game points left" for the other, and names the direction. Showing both as a count up would be the first place the two games started to blur into one, and a player reading a Schnapsen scoreboard expects to watch a number fall.
What a player types
A card name plays it: KH, TH, and 10H for the same card. Then m H for a marriage in hearts, x to exchange for the trump card, c to close the talon, out to claim 66, d to draw. ? lists whatever is legal now.
Only what is legal is accepted, because every option comes from $game->legal rather than from a list kept here. That means a rule added to the engine reaches the prompt without this file being touched, and a rule removed cannot be played from it.
Claiming is typed out in full
out, claim or 66, and never a single letter. A false claim loses the deal, and the prompt shows the player their own card points, so the only way to make one is a slip. It should not be one keystroke away from something else.
You point at the card you mean
On a terminal with Term::ReadKey installed the left and right keys walk your own hand, and the line under it says what the card under the cursor would do to the trick: whether it takes it, what the trick is worth, and what that would put you on. Home and end go to the ends of the hand. Return plays it.
What the other seat did since your last move, and how the last trick or deal came out, is drawn above the table for as long as you are choosing.
The moves that are not cards keep their own keys, x, c, m, o and d. A card is a thing in your hand and the cursor belongs on it; exchanging, closing, declaring and claiming are not cards, and putting them in the same row as five cards would make the row lie about what it holds. Claiming keeps the reasoning in "Claiming is typed out in full" as well: it is o rather than anything next to Return.
Without Term::ReadKey, off a terminal, or under --nopick, every move is typed as before.
What the card under the cursor says
K♥: takes the trick, 6 points, which puts you on 6
A♦: loses the trick, 13 points
Both halves come from Game::Schnapsen::Trick, winner_of and value_of, so they are the engine's own answers rather than arithmetic repeated here. No clone is needed and nothing is played: a trick's winner and value are a function of two cards and the trump.
The losing line is the one that earns its place. Throwing an ace under a trick you are not going to win costs eleven points to the other side, and that is the mistake a player new to the game makes over and over without ever being told the number.
The card you just drew
You draw one at the end of every trick, so "which of these five is new" is a question this screen has to answer every single turn. The new card is drawn in a heavier frame.
Worked out by diffing the hand, not from an event. The draw does not say what it drew, and it must not: everybody sees a card leave the talon, but which card is yours alone, and an event naming it would leak through any spectator view. The terminal may diff because it already holds that hand in order to draw it.
A mark is a frame, not a colour
Settled cards are drawn in light box drawing, the card just drawn in double, and the card under the cursor in heavy. In ascii that is +---, #=== and +=== with ! down the sides.
Three sets and not two, because the drawn card and the cursor are on screen together and one marked style would make them look alike. Frames rather than colours because the point of marking a card is that it is the thing you have not read yet, so it has to survive --ascii, NO_COLOR and a redirected handle.
METHODS
new
Takes variant, mode (bot, hotseat or watch), level, seat, in, out, ascii and colour, and for a test interactive, picking, keysource and pending. Colour left unset is on when out is a terminal and NO_COLOR is unset or empty.
variant has no default. An earlier version of this file fell back to Schnapsen, which is the one thing the distribution is arranged to prevent: the two are different games, and a terminal that guessed would play the wrong one without saying so. play refuses instead.
play
$terminal->play(seed => $seed, variant => ..., dealer => ...);
Runs a match to the end and returns the Game::Schnapsen, or an error if it could not be built. Without a seed it makes one.
options
my $fields = Game::Schnapsen::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.
mode-
One of
bot,hotseat,watch: you against the bot, two people, or two bots. level-
A whole number from 1 to 2: the bot's strength.
seat-
One of
p1,p2: your seat. seed-
Text: deal the same match every time.
Called on a terminal that is playing, with a seat, it is still the kinds of play that seat may make.
run
exit Game::Schnapsen::Terminal->run(@ARGV);
The whole command line program as a class method: parses the options bin/schnapsen documents, builds the terminal, plays a match, and returns the exit status rather than calling exit: 0 if your seat won or nobody at the keyboard was playing a seat, 1 if you lost or stopped early, 2 for a bad option, a leftover word, a missing --variant or a refused value, 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, as it is off a terminal.
game, out, in, mode, level, seat, variant, ascii, colour
What it was built with. game is set by play.
render, show, table_lines, hand_lines, score_line
The screen, as a list of lines and then printed. Split up so a test can look at one part without a terminal.
plain
A card's name with no colour in it. "pretty" is this painted, and the hand's label row pads from this length rather than that one: sprintf '%-7s' over a string that already carries escape codes counts the escapes, so with colour on the labels lost their padding entirely and stopped lining up under the cards they name. Colour is on by default on a terminal, so that was the case every real player saw.
rank_text
The rank of a card as it is printed: 10 for the ten, the letter for the rest.
card_art, card_back, fan, pretty
A card as seven rows, a face-down card, a row of them, and a card as two characters for a sentence.
ask, say_to, paint
One line in, one line out, and colour if the output is a terminal and NO_COLOR is unset or empty.
tell
$terminal->tell('They closed the talon');
Prints a line as "say_to" does and keeps it in "log", so the picker can still show it after the screen is drawn again.
describe, announce, deal_result_line, result_line
What just happened, in words, from the point of view of the seat being played.
human_move, bot_move, is_human, options, card_named
Where a move comes from. card_named accepts 10H as well as TH, because the card itself says 10.
marks, remember
What is new to this seat, as a hashref of card id to mark name, and the setter that works it out by diffing the hand across a move.
quit
Set when the player stopped at the keys. "play" checks it.
interactive, clear
Whether out is a terminal, and the screen clear the picker does before its first frame. Later frames are drawn over the one before.
picking
Whether a card is pointed at rather than typed. Settable, because a failed enter_raw and the t key turn it off for the rest of the match. Unset, it follows "keys_available".
keys_available, enter_raw, leave_raw, raw
Whether there is anything to read keys with, reading a key at a time on and off, and whether it is on now. An interrupt stays an interrupt while it is on. "run" gives the terminal back on every way out, a signal included, because a program that dies reading a key at a time leaves the shell with no echo.
keysource, pending
Given to new: a coderef returning ONE character in place of the keyboard, and an array reference of characters to be read before any others. keysource is what lets t/23 drive the 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.
keyboard, screen, log
The Game::Terminal::Kit::Keys the keys are read through, built from in, keysource and pending; the Game::Terminal::Kit::Screen everything is printed through, built from out, interactive, colour and ascii; and the Game::Terminal::Kit::Log holding what "tell" said since the player's last move and what the picker has to say about a key. Each may be given to new instead.
read_key
One keystroke, as "read_key" in Game::Terminal::Kit::Keys names it: the character itself, or a name such as left, enter or escape.
pick_move, pick_help
The key loop, over a Game::Terminal::Kit::Picker holding the playable cards in the order the hand is drawn, and its key list. pick_move returns a move for "apply" in Game::Schnapsen, or undef to stop, the same contract as "typed_move".
typed_move
The prompt the game used before, and what t and a missing Term::ReadKey fall back to.
card_line
What a card would do to the trick, as the one line printed under the hand.
other
The other seat.
SEE ALSO
Game::Schnapsen, Game::Schnapsen::Bot.
AUTHOR
LNATION, <email@lnation.org>
LICENSE AND COPYRIGHT
This software is Copyright (c) 2026 by LNATION.
This is free software, licensed under the Artistic License 2.0.