Tic-tac-toe in Erlang — game abstraction
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 tutorial are organized into these sections:
- Introduction
- Top-level loop
- User input
- Board display
- Board abstraction
- Game abstraction (this page)
- Next move calculations and predictions
- Predicted outcome abstraction
- Utilities to introduce randomness
- Macros for testing and debugging
Game abstraction
The game record (or #game{} record if you prefer) keeps track of the tic-tac-toe game as it is being played. It consists of these properties:
| state | Whose turn it is (x_next or o_next). Or who has won the game (x_winner or o_winner). Or cat_game. |
| board | Structure that holds all the X and O marks on the board. Also holds predicted outcomes for spots on the board not marked X or O (unless the game is over). |
| print_option | How to display the board. One of simple, number, or predict. |
The game “object” is fully abstracted by the wrapping functions below. No code outside this section assumes anything about its internal structure, or even assumes it’s a record.
I hesitate using the word object above. An object is often thought of as an instance of a class, and Erlang records are not classes. They’re more like structs in C, without inheritance or polymorphism (virtual methods).
Interface
The functional interface that wraps around the #game{} record is described below.
Source code
The Erlang #game{} record definition looks like this:
% --------------------------------------------------------------
% Record type - #game
% --------------------------------------------------------------
%
% The #game{} record type holds the following values:
%
% ----------------------------------------------------------
% state - state of the game
% One of these atoms:
% x_next - it's X's turn to play next
% o_next - it's O's turn to play
% x_winner - X is the winner of the game
% o_winner - O is the winner
% cat_game - the board is full with no winner
%
% ----------------------------------------------------------
% board - 3x3 tic-tac-toe board
% Tuple of 9 elements (3 rows times 3 columns).
% See the Board section below for details.
%
% ----------------------------------------------------------
% print_option - how to print board
% One of these atoms:
% simple - just show X's and O's
% number - print number in empty spots
% predict - print number and prediction in empty slots
%
% Suggestions:
% Record the move history to enable undo.
% Allow a bigger board instead of just a 3x3.
% Record the name(s) of the players.
-record(
game,
{ state
, board
, print_option
}).
The functions that wrap around the #game{} record and provide a layer of abstraction are listed here. This is the only section of code in tic.erl that uses the #game{} record’s internal structure.
% --------------------------------------------------------------
% Game functions
% --------------------------------------------------------------
% --------------------------------------------------------------
% get_game_state( Game )
% get_game_board( Game )
% get_game_print_option( Game )
%
% Simple accessors.
% One way to get a value out of a #game object.
get_game_state( _ = #game{ state=State } ) -> State.
get_game_board( _ = #game{ board=Board } ) -> Board.
% Another way to get a value out of #game.
get_game_print_option( G = #game{} ) -> G#game.print_option.
% --------------------------------------------------------------
% init_game( )
% init_game( Game = #game{} )
% init_game( Print_option )
%
% Use this to start a game. You can carry over
init_game( ) ->
init_game( predict).
init_game( _ = #game{ print_option=Option } ) ->
init_game( Option);
init_game( Print_option ) ->
?m_assert(
(Print_option == simple) or
(Print_option == number) or
(Print_option == predict)),
#game
{ state = x_next
, board = init_board( )
, print_option = Print_option
}.
% --------------------------------------------------------------
% set_game_print_option( Game, Print_option )
%
% Returns a copy of Game with a new print option.
set_game_print_option( Game = #game{}, Option ) ->
?m_assert(
(Option == simple) or
(Option == number) or
(Option == predict)),
Game#game{ print_option = Option }.
% --------------------------------------------------------------
% set_game_state_board( Game, State, Board )
%
% Returns a copy of Game with a new state and board.
% Confirms that State and Board are consistent with each
% other. Does not confirm that the new board is one move
% different from the old board.
set_game_state_board( Game = #game{}, State, Board ) ->
% State must be legal.
?m_assert(
(State == x_next ) or
(State == o_next ) or
(State == x_winner) or
(State == o_winner) or
(State == cat_game)),
% Winning states must agree with the board.
% There can only be one winner.
?m_assert(
(State == x_winner) == is_board_won( Board, x_mark)),
?m_assert(
(State == o_winner) == is_board_won( Board, o_mark)),
% State cat_game implies the board is full.
?m_assert(
(State /= cat_game) or is_board_full( Board)),
% Playable game implies the board is not full.
% Remember:
% (A implies B) is the same as ((not A) or B)
?m_assert(
((State /= x_next) and (State /= o_next)) or
not is_board_full( Board)),
% If appropriate, fill the board with predicted outcomes.
% This will fill the board with predictions even if the
% board is empty (all spaces will predict cat games).
% Maybe we should change that to just fill an empty board
% with 'empty' elements.
Board_with_predictions =
case State of
x_next -> fix_board_predicted_outcomes( Board, x_mark);
o_next -> fix_board_predicted_outcomes( Board, o_mark);
_ -> Board
end,
% Make a new game record.
Game#game
{ state = State
, board = Board_with_predictions
}.
Comments
Leave a Reply