NAME

Game::Mahjong::Terminal - a game against three bots at the keyboard

VERSION

Version 0.04

SYNOPSIS

my $t = Game::Mahjong::Terminal->new(seed => 'evening', level => 2, seat => 0,
    in => \*STDIN, out => \*STDOUT, colour => -t STDOUT);
$t->start;

mahjong --level 3 --glyphs        # from the shell

DESCRIPTION

The table drawn every time it is your move: the hand and round, your wind, the wall, the totals, each seat's melds, flowers and discard pool in rows of six, and your hand numbered so a discard is a number. A window says who discarded what and lists what you may do; a win names every fan with its points and the settlement; the end of the game is the standings. The bots play in between and are narrated a line at a time.

Commands

<n>             discard the tile numbered n
d 5m            discard by name (E S W N R G B for the honours)
kong [5m]       declare a kong
win             declare your hand complete
pass, pung, kong, chow [n]     answer a discard
hint            what the strongest bot would do now
table, fans, help, quit

The tiles

A tile is drawn as a tile: a box three lines tall with a face two cells wide. Two cells is what makes a rack of fourteen lay out without measuring anything, because all three ways of writing a face are two cells: the notation's codes (5m, F1), an honour as its letter and a space, and a character from the Unicode Mahjong Tiles block, which is double width. A rack of fourteen with a space between the tiles is sixty-nine columns and fits an eighty column terminal.

Your own rack, your own sets and the tile somebody has just thrown are drawn as tiles, because those are what a decision is about. The pools and the other seats' sets are drawn as chips, [5m], because a pool of twenty as boxes is nine lines a seat and thirty-six for the table, and it is the record rather than the thing anybody is about to touch.

Glyphs, colour and the frame

glyphs puts the Unicode Mahjong Tiles block on the faces for a terminal that has the font. colour paints the face ivory with the suit's own ink, the character suit red, dots blue, bamboo green, and the three dragons their own names. ascii draws the frame with +, - and | for a terminal that cannot manage box drawing. clear clears the screen before each table, so the table is in one place and is read rather than scrolled to; it is on when standard output is a terminal.

You point at the tile you mean

On a terminal with Term::ReadKey installed the left and right keys walk your own rack, the tile under the cursor is the one you would throw, and return throws it. The numbers still work and still mean what is printed under them, h is the hint and moves the cursor to what it suggests, t goes back to typing, ? lists the keys and q stops. Without Term::ReadKey, off a terminal, or under --no-pick, every move is typed as before.

The cursor starts on the tile you just drew, which is the one most often thrown.

The claim phase stays typed. pass, chow 1, pung and win are four words and a cursor would be four words with a box round one of them. It is the discard that wants pointing at, because the discard list IS the rack: eight to fourteen distinct kinds, twelve or thirteen typically, so a list of them is a worse drawing of the thing already on the screen.

What the throw would leave you

Under the rack, one line: throw 2m and you are 5 away, 32 kinds would help, a step backwards. The distance is "after_discard" in Game::Mahjong::Shanten and the count is "ukeire" in Game::Mahjong::Shanten over the hand without that tile, so both are the engine's own answers rather than the terminal's arithmetic.

It is affordable because after_discard is XS and costs two microseconds, so a whole rack is a fiftieth of a millisecond. ukeire needs a hand to ask and so costs a clone, a tenth of a millisecond, and is therefore asked only for the tile under the cursor rather than for all fourteen on every keypress.

"A step backwards" is the line's reason for existing. A hand two away from a win can be thrown three away, and which tiles do that is exactly what a player new to the game cannot see and an experienced one counts in their head.

The number under a tile is the tile above it

The rack moves the tile you just drew out of its sorted place, gaps it and puts it at the end, and numbers what it drew. For a long time the parser indexed $hand->tiles instead, which is not reordered, so the number under a tile threw a different tile: wrong somewhere in 92% of racks, an average of 6.4 of the fourteen slots pointing elsewhere, and slot 14, the tile just drawn and the one most often thrown, wrong every time that tile did not already sort last.

"rack_order" is now the only place that order is decided, and the drawing, the numbering and both pickers all ask it. One row of tiles may have only one order. The test for it compares the tile the frame NAMES against the tile the move throws, because comparing the move against a second copy of the same arithmetic passes while the screen lies.

ATTRIBUTES

seed

The game's seed; the same seed deals the same tiles.

level

The bots' level, 1 to 3.

seat

The person's seat, 0 to 3; seat 0 deals the first hand.

in

The filehandle commands are read from.

out

The filehandle the table is written to.

colour

ANSI colour when true.

glyphs

The Unicode Mahjong Tiles block instead of the notation when true.

ascii

Draw the tile frames with +, - and | rather than box drawing, for a terminal that cannot manage them.

clear

Clear the screen before each table. On when standard output is a terminal.

auto

Every seat played by a bot, no prompt: a game to watch. The table is drawn once a round rather than once a move, so a watched game reads as a game rather than as a log.

names

The four seats' names, the person's first.

rules

The game.

bots

The four bots.

hinter

The level-three bot the hint asks.

quit

Whether the person left.

METHODS

start

Plays until the game ends or the person quits; returns the rules object.

options

my $fields = Game::Mahjong::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 1 to 3: the bots' strength.

seat

A whole number from 0 to 3: your seat; seat 0 deals the first hand.

glyphs

A switch: the Unicode Mahjong Tiles block on the tile faces.

auto

A switch: four bots, no prompt: watch a game.

seed

Text: the same seed deals the same tiles.

run

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

The whole command line program as a class method: parses the options bin/mahjong documents, builds the terminal on STDIN and STDOUT, plays, and returns the exit status rather than calling exit: 0 if you won or the game was drawn, 1 if you lost or stopped early, 2 for a bad option or a refused value, 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 given back 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 to anything but the empty string, and off a terminal, and out is UTF-8 unless --ascii was given.

show_table, show_hand_end, show_game_end, narrate, help, show_fans

The pieces of the display, each writing to out.

read_move

Reads commands from in until one is a legal move.

face

One tile's face as exactly two cells: its notation code, an honour's letter and a space, or one double width character from the Unicode Mahjong Tiles block when glyphs is on.

tile_art

The three lines of one tile, framed. back draws it face down, which is what another seat's concealed kong is; mark emphasises it.

tile_row

A run of tiles side by side, as three lines. gaps names the positions that take an extra space before them, which is how the tile just drawn is set apart from the sorted hand.

tile_chip, tile_chips

The compact form, [5m], for the pools and for another seat's sets.

finished, winner

Whether the sixteen hands were played out, and who won, recorded as the game ends so bin/mahjong can set an exit status. Undefined winner is a shared first place.

cursor

The rack position the picker is pointing at, or undef.

It is drawn differently from the tile you just drew, not the same way. The two are on screen together, so one marked style makes them look alike: the cursor takes the heavy frame and reverse video, the drawn tile the double frame in green, and everything else the light frame. Three styles for three things, the frames carrying it without colour and the colour carrying it again for anyone who has it.

Reverse rather than a colour of its own, because a tile face is already painted in its suit's ink on ivory and any fixed background picked for the cursor reads badly against one suit or another: navy dots on light blue were the case that decided it. Reversing swaps whatever the tile already is, so the contrast is as good as the tile's own and no suit is the loser.

keyboard

The Game::Terminal::Kit::Keys the keys and the typed lines are read through, built from "in", keysource and pending.

screen

The Game::Terminal::Kit::Screen everything is printed through, built from "out", "colour", "ascii" and "clear".

log

The Game::Terminal::Kit::Log holding what the picker has to say under the rack: the hint, the key list, a key that does nothing. Each is drawn in one frame.

keysource, pending

Constructor arguments: a coderef returning ONE character in place of Term::ReadKey, and characters to be read before any other. keysource is what lets t/32 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.

raw

True while keys are read one at a time.

picking

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

keys_available, enter_raw, leave_raw

Whether there is anything to read keys with, and reading a key at a time on and off. An interrupt stays an interrupt throughout. "run" turns it off on every way out, a signal included, because a program that stops while reading a key at a time leaves the shell with no echo.

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. A name is always longer than one character, so it never collides with one.

rack_order

my ($kinds, $drawn_at) = $terminal->rack_order;

The rack in the order it is drawn, and where in it the tile just drawn sits. The single authority on that order: see "The number under a tile is the tile above it".

picker

my $picker = $terminal->picker(@legal);

The Game::Terminal::Kit::Picker for a discard: one item for every tile of the rack in the order "rack_order" gives, a tile that may not be thrown stepped over, and the cursor on the last tile that may be, which is the tile just drawn. Undef when nothing can be thrown.

pick_discard, show_pick, pick_help

The key loop for a discard, one frame of it, and its key list. pick_discard returns a move for "apply" in Game::Mahjong::Rules, or undef to stop. show_pick takes the picker and an array reference of the legal moves.

throw_line

The line under the rack saying what throwing a kind would leave.

tile, tiles, name_of, say

Small helpers: a kind as text, kinds as text, a seat's name, a line out.

SEE ALSO

Game::Mahjong::Rules, Game::Mahjong::Bot

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.