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:
- Introduction
- Top-level loop
- User input (this page)
- Board display
- Board abstraction
- Game abstraction
- Next move calculations and predictions
- Predicted outcome abstraction
- Utilities to introduce randomness
- Macros for testing and debugging
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