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:

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.

get_game_state( Game )
get_game_board( Game )
get_game_print_option( Game )
Simple functions to access the properties of the #game{} record. I provide the get_.. functions because, for example, get_game_state(Game) is more abstracted than Game#game.state, and it looks better too.
init_game( )
init_game( Game )
Makes a new #game{} record. It gives you a blank board ready for the first move, and it let’s you carry over the print option from the previous game.
set_game_print_option( Game, Print_option )
Returns a new #game{} record, the same as the Game passed in except with a new print option.
set_game_state_board( Game, State, Board )
Returns a new #game{} record, copied from Game but with new values for the state and game properties. Checks carefully to make sure the new state and board are consistent with each other. Calculates predicted outcomes if the board is not a won/lost or cat game.

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

3 Responses to “Tic-tac-toe in Erlang — game abstraction”

  1. Tic-tac-toe in Erlang — game space symmetries : Code Obscurata on August 24th, 2012 6:22 pm

    […] Game abstraction […]

  2. Tic-tac-toe in Erlang — macros for testing and debugging : Code Obscurata on August 24th, 2012 6:24 pm

    […] Game abstraction […]

  3. Tic-tac-toe in Erlang — introduction : Code Obscurata on August 24th, 2012 6:26 pm

    […] Game abstraction […]