Tic-tac-toe in Erlang — user input

This is part of an Erlang tutorial built around a tic-tac-toe program. The program is stuffed into one file, called tic.erl and available for download. The source code and this tutorial are organized as follows:

User input

This section provides a function that asks the user for instructions. The user types these single-letter commands from the Erlang shell (standard-IO).

q Quit the game.
h Help — briefly describe all the commands available to the user.
g Game — start a new game.
b Board — display a simple board without predictions or position numbers.
n Number — display the tic-tac-toe board with empty positions numbered.
p Predict — display the tic-tac-toe board with predictions for each next move.
1-9 Make the next move at the numbered position. The user should enter the number of an empty board position.
a Automatic — let the computer choose the next move.
s Skip the next move. So if it is X‘s turn and you skip it, it becomes O‘s turn. Predicted outcomes will be recalculated.
e1-e9 Erase — erase the mark at position 1-9. This is the only two-letter command. You can do this even after a game is won. Predicted outcomes will be recalculated.

Interface

The user input section implements a single function that is used elsewhere.

get_next_move( Game )
Asks the user what to do next. Returns a command. Provides help.

Source code

Here’s the Erlang source code for the user input section of the tic.erl file.

% --------------------------------------------------------------
% User Input - get_next_move(..)
% --------------------------------------------------------------
%
%   Asks the user for the next move. Also prints help.
%
%   The user choices are:
%     quit      - quit playing
%     help      - describe the user options
%
%     new_game  - start a new game,
%                   even if the current one is not finished.
%
%     simple    - show the board again with a simple display
%                   that shows only X's and O's. Change the
%                   print option.
%     number    - change print option and show board with
%                   numbers in blank spots.
%     predict   - change print option and show board with
%                   predicted outcomes described in the
%                   blank spots.
%                   Also shows numbers in the blank spots.
%
%     automatic - have the computer select the next move.
%     skip      - skip the next move, so if it was X's turn
%                   skip that and make it O's turn instead.
%
%     {mark, Position}  - select Position (1-9) for the next
%                           X or O move, whoever's turn it is.
%     {erase, Position} - erase the X or O at Position. If an
%                           X is erased it becomes X's turn,
%                           and the same for O.
%
%   This section "exports" one function: get_next_move( Game ).
%   It returns one of the above commands, except it handles
%   help itself and never returns 'help'.
%
%   All returns are validated and guaranteed to be OK.
%   The atoms 'quit' 'new_game' 'simple' 'number' and 'predict'
%   are always OK and can be returned at any time.
%   'automatic' and 'skip' are only returned if the board is
%   in play, which means it is neither won nor cat.
%   {mark,Pos} is only returned if the board is in play and
%   the Pos (1-9) is not marked X or O.
%   {empty,Pos} is only returned if the board is marked X or
%   O at Pos (1-9), so it is never returned for an empty board.
%   {empty,Pos} can be returned if the board is won or cat.

% --------------------------------------------------------------
% get_next_move( Game )
% get_next_move( State, Board )
%
%   Returns the next user command from default standard input.
%   Only returns valid commands that are consistent with Board
%   and State.
%
%   Returns one of the following 8 commands. If Position (1-9)
%   is part of the command, it will have been validated.
%     quit  new_game
%     simple  number  predict
%     automatic  skip
%     {mark, Position}
%     {erase, Position}

get_next_move( Game ) ->
  get_next_move( get_game_state( Game), get_game_board( Game)).

get_next_move( cat_game, Board ) ->
  write_line( "Cat game."),
  ask_for_command( cat_game, Board);

get_next_move( x_winner, Board ) ->
  write_line( "X is the winner."),
  ask_for_command( x_winner, Board);

get_next_move( o_winner, Board ) ->
  write_line( "O is the winner."),
  ask_for_command( o_winner, Board);

get_next_move( x_next, Board ) ->
  write_line( "It is X's turn to move."),
  ask_for_command( x_next, Board);

get_next_move( o_next, Board ) ->
  write_line( "It is O's turn to move."),
  ask_for_command( o_next, Board).

% --------------------------------------------------------------
% ask_for_command( State, Board )

ask_for_command( State, Board ) ->
  Command =
    translate_to_command(
      get_user_input(
        "What do you want to do (h=help, q=quit)? ")),

  % Useful letter in some of the messages below.
  Xo_letter =
    case State of
      x_next -> $X;
      o_next -> $O;
      _ -> $#
    end,

  case Command of
    % Quit and new_game are always valid commands.
    quit      -> quit;
    new_game  -> new_game;

    % Change the print option, which is always OK.
    simple    ->
      write_line(
        "Setting print option to show a simple board."),
      simple;
    number    ->
      write_line(
        "Setting print option to show position numbers."),
      number;
    predict   ->
      write_line(
        "Setting print option to show predicted outcomes."),
      predict;

    % Move commands are invalid on won and cat games.
    automatic ->
      case is_state_consistent_with_move( State ) of
        true  ->
          write_line(
            "Automatically selecting ~c's next move.",
            Xo_letter),
          automatic;
        false ->
          ask_for_command( State, Board)
      end;

    % Skip is like a move command.
    skip      ->
      case is_state_consistent_with_move( State ) of
        true  ->
          write_line( "Skipping ~c's next turn.", Xo_letter),
          skip;
        false ->
          ask_for_command( State, Board)
      end;

    % Move command can only be issued if game not yet won or
    % cat, and if move position isn't already marked X or O.
    {mark, Pos} ->
      case is_state_consistent_with_move( State, Board, Pos) of
        true  ->
          write_line( "Marking ~c at position ~w.",
            Xo_letter, Pos),
          Command;
        false ->
          ask_for_command( State, Board)
      end;

    % Erase command that is not valid on an empty board.
    % It is also invalid is Pos is not marked X or O.
    {erase, Pos} ->
      case is_board_consistent_with_erase( Board, Pos) of
        true  ->
          Xo_erase =
            case get_board_mark( Board, Pos) of
              x_mark -> $X;
              o_mark -> $O
            end,
          write_line( "Erasing the ~c at position ~w.",
            Xo_erase, Pos),
          Command;
        false ->
          ask_for_command( State, Board)
      end;

    % Help is handled here. Print help and ask again.
    help ->
      print_help( State, Board),
      ask_for_command( State, Board);

    % Fall through that prints help and asks again.
    _ ->
      write_line( "Unrecognized command."),
      print_help( State, Board),
      ask_for_command( State, Board)
  end.

% --------------------------------------------------------------
% get_user_input( Prompt )
%
%   Asks the user for a command. After the user types something
%   and hits return, we strip the line-feed from the end of the
%   string, and any spaces from the front and back.

get_user_input( Prompt ) ->
  string:strip(   % remove spaces from front and back
    string:strip( % remove line-feed from the end
      io:get_line( Prompt), right, $\n)).

% --------------------------------------------------------------
% is_state_consistent_with_move( State, Board, Position )
% is_state_consistent_with_move( State )

is_state_consistent_with_move( State, Board, Pos ) ->
  case is_state_consistent_with_move( State) of
    false -> false;
    _ ->
      Mark = get_board_mark( Board, Pos),
      if
        Mark /= x_mark, Mark /= o_mark -> true;
        true ->
          write_string( "Invalid request - spot "),
          write_arg( Pos),
          write_string( " is already marked "),
          write_line(
            case Mark of
              x_mark -> "X.";
              o_mark -> "O."
            end),
          case {State, Mark} of
            {x_next, x_mark} -> ok;
            {o_next, o_mark} -> ok;
            _                ->
              write_string( "To change it to "),
              write_string(
                case State of
                  x_next -> "X";
                  o_next -> "O"
                end),
              write_string( " erase it first with the 'e"),
              write_arg( Pos),
              write_line( "' command.")
          end,
          false
      end
  end.

is_state_consistent_with_move( State ) ->
  case State of
    % You can only make move if the game is not won or cat.
    x_next -> true;
    o_next -> true;
    _ ->
      write_string( "Invalid request - "),
      write_line(
        case State of
          x_winner -> "X has already won.";
          o_winner -> "O has already won.";
          cat_game -> "The game over in a tie."
        end),
      write_line( "You cannot add more marks to the board."),
      false
  end.

% --------------------------------------------------------------
% is_board_consistent_with_erase( Board, Position )

is_board_consistent_with_erase( Board, Pos ) ->
  Mark = get_board_mark( Board, Pos),
  case Mark of
    x_mark -> true;
    o_mark -> true;
    _ ->
      write_string( "Invalid request - spot "),
      write_arg( Pos),
      write_string( " is not marked X or O"),
      write_line( " and so cannot be erased."),
      false
  end.

% --------------------------------------------------------------
% translate_to_command( String )
%
%   Return values:
%     quit; new_game;
%     simple; number; predict;
%     automatic; skip;
%     {mark, Pos}; {erase, Pos}
%     false

% Quit the game.
translate_to_command( [Q|_] )
  when Q == $q; Q == $Q
  ->
  quit;

% Print help.
translate_to_command( [H|_] )
  when H == $h; H == $H
  ->
  help;

% Start a new game.
translate_to_command( [G|_] )
  when G == $g; G == $G
  ->
  new_game;

% Print a simple board and set print option to 'simple'.
translate_to_command( [B|_] )
  when B == $b; B == $B
  ->
  simple;

% Set print options to show the board with numbers in
% the empty spots, and then print the board again.
translate_to_command( [N|_] )
  when N == $n; N == $N
  ->
  number;

% Print the big board with predictions.
% Set print options to show the board with predicted outcomes
% and numbers in the empty spots, and print the board again.
translate_to_command( [P|_] )
  when P == $p; P == $P
  ->
  predict;

% Let the computer select the next move automatically.
translate_to_command( [A|_] )
  when A == $a; A == $A
  ->
  automatic;

% Skip this turn for the current xo mark.
% This is the same as saying if it's X's turn, make it O's
% turn instead, and vice versa.
translate_to_command( [S|_] )
  when S == $s; S == $S
  ->
  skip;

% Let the computer select the next move automatically.
translate_to_command( [Digit] )
  when Digit >= $1, Digit =< $9
  ->
  Pos = Digit - $0,
  ?m_assert( (1 =< Pos) and (Pos =< 9)),
  {mark, Pos};

% Erase an X or O now on the board.
% Returns either {erase, Pos} or false the entry was not valid.
translate_to_command( [E | Digit_str] )
  when E == $e; E == $E
  ->
  case string:strip( Digit_str) of
    [Pos_char]
        when is_integer( Pos_char),
        $1 =< Pos_char, Pos_char =< $9
      ->
        Pos = Pos_char - $0,
        ?m_assert( (1 =< Pos) and (Pos =< 9)),
        {erase, Pos};
    _ ->
        false
  end;

% Catch all case. Returns false whenever the user enters
% h, help, or anything that was not understood.
translate_to_command( _ )
  ->
  false.

% --------------------------------------------------------------
% print_help( State, Board )

print_help( State, Board ) ->
  write_line( ),
  write_line( "Please enter one of the following:"),
  write_line( "  q - quit"),
  write_line( "  h - help, show this message"),
  write_line( "  g - start a new game"),
  write_line( "  b - show a simple board"),
  write_line( "  n - show the board with open spots numbered"),
  write_line( "  p - show a board with predicted outcomes"),

  fun
    ( ok     ) -> ok;
    ( Xo_str ) ->
      Print_helper =
        fun( Prefix ) ->
          write_string( Prefix),
          write_string( Xo_str),
          write_line( " next move")
        end,
      write_line(   "  1-9 (a single digit)"     ),
      Print_helper( "    - choose "              ),
      Print_helper( "  a - automatically choose "),
      Print_helper( "  s - skip "                )
  end( case State of
         x_next -> "X's";
         o_next -> "O's";
         _      -> ok
       end),

  case is_board_empty( Board) of
    true  -> ok;
    false ->
      write_line( "  e1-e9 ('e' followed by a single digit)"),
      write_line( "    - erase an X or O already on the board")
  end,

  write_line( ).

Comments

Leave a Reply