NAME
Game::Terminal - one game command to rule all terminal games
VERSION
Version 0.02
SYNOPSIS
game # the menu
game checkers --level 3 # the same as: checkers --level 3
game sixtysix # Schnapsen, in the Sixty-Six variant
game list
game help oware
use Game::Terminal;
my $row = Game::Terminal::find('draughts'); # the checkers row
my ($status, $error) = Game::Terminal::launch($row, ['--level', 1]);
DESCRIPTION
A roster of the terminal games in the Game namespace, Term::Maze and Term::Chess, with one command, game, that lists them and starts any of them. A game is started inside the same process: its Terminal class's run method takes the command line and returns the exit status, so game checkers --level 3 is checkers --level 3 with nothing between them, and the menu is back when the game ends.
This module is the roster and the functions over it, and does no terminal input or output of its own; the menu is Game::Terminal::Menu and the command is bin/game.
What a game promises
Every class in the roster has a class method run(@argv) that returns 0 when you won, drew or quit, 1 when you lost and 2 for a bad option, and never calls exit. --help prints to STDOUT and returns 0; a bad option prints the usage to STDERR and returns 2. Colour is off when NO_COLOR is set. launch relies on nothing else, and guards against a game that forgets the rest (see below).
The roster
Twenty-one rows for twenty games: Schnapsen is two rows, schnapsen and sixtysix, because its program needs to be told the variant. Each row has a key (the word you type), the module to install and the class to call, a one-line blurb, a family (board, card, tile, puzzle or arcade), the number of players, the args the launcher puts before yours, the aliases that also find it, and the flags that spell the launcher's own --ascii and --no-pick in that game's words.
The aliases: draughts is checkers, othello is reversi, ur is royalur, morris and mills are merrills, tafl is brandubh, weiqi and baduk are go, chinesechess is xiangqi, awale and awari are oware, rummy is gin, crib is cribbage, flappy is floppy, and 66 is sixtysix.
Where the options come from
The settings a person chooses before a game (the level, a side or seat, a mode, a variant, a board size, a seed, the switches that turn a game into hotseat play or a bot watching itself) are the game's own to list. Each class in the roster has a class method options that returns them as fields, and "options_for" asks it. A field is a hash of name, type and help, with min and max for an int and choices for an enum; Game::Terminal::Kit::Options describes them in full. File paths, replay records and layout widths are not among them; they stay on the command line, and the menu's extra line takes them.
The launcher never repeats a game's defaults: a field nobody set sends nothing, so the game decides.
FUNCTIONS
games
The rows, in order.
names
The keys, in order.
normalise
Lowercases a name, drops a leading Game:: or game- and every character that is not a letter or a digit, so Game::Checkers, game-checkers and CHECKERS all become checkers.
find
my $row = Game::Terminal::find('Draughts');
The row for a key or an alias after "normalise", or undef.
nearest
my @keys = Game::Terminal::nearest('chekers');
Keys that start with the name, or failing that the keys within three edits of it, nearest first; empty when nothing is close. For "did you mean".
distance
The Levenshtein distance between two strings.
module_file
The path under @INC of a row's class, Game/Checkers/Terminal.pm.
installed
my $path = Game::Terminal::installed($row);
my $path = Game::Terminal::installed($row, @dirs);
The file a row's class would load from, searching @INC or the directories given, or undef when it is nowhere.
version_of
The $VERSION written in that file, read without loading it, or undef.
load
Requires the row's class and returns its name.
options_for
my $fields = Game::Terminal::options_for($row);
The fields of a row's game, as an array reference: what its class's options method returns. The class is loaded to ask it, so this is called when a game's options are about to be shown and not while the games are listed. A game that is not installed, or whose class has no such method, has no fields. A row that carries its own options list is believed without asking. loader may be given as for "launch".
flags_for
my @flags = Game::Terminal::flags_for($row, { ascii => 1, pick => 0 });
The launcher's common options spelt in the row's words: --ascii becomes --no-unicode for brandubh and fanorona and nothing for maze, --no-pick is --no-pick everywhere but maze, snake and floppy, which are played in real time. Colour is not a flag: see no_colour under "launch".
step_value
my $next = Game::Terminal::step_value($field, $current, +1);
"step_value" in Game::Terminal::Kit::Options, under the name it has always had here.
The value one step to the right (+1) or left (-1) of $current for a field: an int moves by one and stops at its ends, and from unset goes to min or max; an enum cycles through its choices; a flag toggles; a bool cycles through unset, on and off. str fields do not step.
check_value
my ($value, $error) = Game::Terminal::check_value($field, $typed);
"check_value" in Game::Terminal::Kit::Options, likewise.
A typed value for a field: the number, the matching choice regardless of case, or the text, with $error set and $value undef when an int is out of range or an enum is not a choice. Blank is unset.
argv_for
my @argv = Game::Terminal::argv_for($row, \%values);
The command line the values make for the row's fields, in the order "options_for" gives them, with the extra text split like a shell and appended last.
launch
my ($status, $error) = Game::Terminal::launch($row, \@argv, %seam);
Loads the row's class and calls run with the row's args followed by @argv, and returns the status it gave. If run died, $status is undef and $error is the message without the at FILE line N tail.
Around the call, so that a game leaves the process as it found it for the next one: the Getopt::Long configuration is saved and put back; the INT, TERM and HUP handlers are local; every layer the game pushes onto the output handle is popped again, since a second game pushing its own encoding layer on top of the first would double-encode; the handle's autoflush and the selected handle are restored; and if Term::ReadKey is loaded the terminal is put back in cooked mode, for a game that died before its own leave_raw.
%seam takes loader (a code ref returning the class, in place of "load"), out (the handle whose layers are guarded, STDOUT by default) and no_colour (true sets NO_COLOR for the duration of the call).
SEE ALSO
Game::Terminal::Menu, and the games: Game::Backgammon, Game::Brandubh, Game::Checkers, Game::Cribbage, Game::Dominoes, Game::Durak, Game::Fanorona, Game::Floppy, Game::Gin, Game::Go, Game::Mahjong, Game::Merrills, Game::Oware, Game::Reversi, Game::RoyalUr, Game::Schnapsen, Game::Snake, Game::Xiangqi, Term::Maze.
AUTHOR
LNATION, <email at lnation.org>
LICENSE AND COPYRIGHT
This software is Copyright (c) 2026 by LNATION.
This is free software, licensed under:
The Artistic License 2.0 (GPL Compatible)