Demo: Tic Tac Toe

Let's build the classic game using object-orientation.

Scroll down...

Content

Resources

Comments

In this demo, we'll build a command-line Tic-Tac-Toe game played by two players. It will introduce you to an OOP problem-solving approach which should be useful later on when you're building larger problems.

You can (and should) view the code directly on Github here. If you get a 404, let us know (we may need to enable your access).

The Approach

From your work in previous lessons, you know that we love starting by whiteboarding the major components and logic of the problem. In this case, we'll want to think carefully about what are the objects (ie. "nouns") and what are the "actions" (ie. "verbs") present in the problem. These become apparent if you pseudocode the high level problem.

Seeing these two categories broken down will help us think about which objects need to become full-fledged classes with methods and which ones can be simple data structures like arrays or hashes.

Basically, our approach to OOP challenges is:

  1. Understand the problem.
  2. Pseudocode the approach to expose the "actions" and objects
  3. Figure out which objects should own which "actions" (e.g. which classes should contain which methods) and which objects are really just static data.
  4. Flesh out the pseudocoding to look more Ruby-ish
  5. Code it up in Ruby
  6. Refactor and check privacy

1. Understand the Problem

This is Tic-Tac-Toe with 2 players and will be played from the command line. Players will alternate turns.

2. Pseudocode the High Level Approach

A high level pseudocoding will cover the major actions and might look like:

Set up the game initially
    Create a game board
    Create a couple players
Start the game loop
    Render the board
    Ask for and validate the current player's coordinates
    If the game should end
        Display the proper victory / draw message
        Stop looping
    Else
        Switch to the next player and keep looping

The objects then become more apparent -- which objects are required to make this happen? In this case, we might suggest:

  • The game itself
  • The 2 players
  • The game board
  • Game pieces

3. Assign Actions to Objects

We'll now assign "actions" to objects to help clarify which objects are really full-featured classes and who should "own" what. This is where OOP design principles come in handy -- try to delegate as many actions to an appropriate object as possible instead of keeping them all contained within one giant "god" class.

In this case, it can be tempting to create a single TicTacToe class which runs the game and knows everything about everyone. That's bad OOP! You'll still need a main class to kick things off but keep it as thin and "dumb" as possible.

To help delegate actions, we've broken out the pseudocode into a bit more detail and assigned an owner to each action. It's usually easier to do this on a whiteboard, but we're being explicit here for demonstration purposes.

Set up the game initially [TicTacToe]
    Create a game board [Board]
    Create a couple players [Player]
Start the game loop [TicTacToe]
    Render the board [Board]
    Ask for and validate the current player's coordinates [Player]
    If the game should end [TicTacToe]
        Display the proper victory / draw message
        Stop looping
    Else
        Switch to the next player and keep looping [TicTacToe]

Based on this breakdown, it sounds like we've got three classes -- TicTacToe (which controls the overall game), Player (which models each player), and Board (which preserves the board state and renders it). It doesn't look like we'll actually need a GamePiece class.

4. Make Ruby-ish Pseudocode

Now that we know our classes and roughly their responsibilities, we can set up a basic structure with classes and methods.

In this case, we've made the TicTacToe class manage the high-level game functions like setting up the board, looping through the turns, and asking questions of other objects to determine whether the game is over. Notice that it doesn't directly involve itself in too much; it delegates most of the nitty gritty detail to the other classes.

The Player class handles interactions with the human player, including asking for coordinates and placing that coordinates on the board. The Player validates whether the coordinates are correctly formatted, but not whether they're being placed correctly. We're trusting the Board to handle whether or not a piece is actually being placed in a valid location.

This is the "Tell Don't Ask" principle -- we could have first asked the Board if the attempted placement is valid and then told the Board to place the piece if so, but that's considered an antipattern. Better to leave the full responsibility up to the Board and simply let the Player handle it if it fails. That reduces coupling between the classes.

The Board class handles maintaining the state of the board, returning any necessary data like if the specified piece occurs in a winning triple across verticals, and placing game pieces. The Board will not place a game piece if it is an invalid placement location.

You'll also notice that each method has been broken out into sub-methods again and again until most methods are just a few lines. This makes the code more readable and makes it easier to pinpoint issues, and follows the principle of "Method Decomposition".

# Controls the game play
class TicTacToe
    # initialize
        # set up the board
        # set up the players
        # assign the starting player

    # play
        # loop infinitely
            # call the board rendering method
            # ask for coordinates from the current player
            # break the loop IF the game is over
            # switch players

    # check_game_over
        # check_victory
        # check_draw

    # check_victory
        # IF board says current player's piece has
        # a winning_combination?
            # display a victory message

    # check_draw
        # IF Board says we've filled up
            # display a draw message


    # switch_players
        # PlayerX >> PlayerO or vice versa
end

# Manages all player-related functionality
class Player
    # initialize
        # Set marker type (e.g. X or O)

    # get_coordinates
        # loop infinitely
            # ask_for_coordinates
            # IF validate_coordinates_format is true
                # IF piece can be placed on Board
                    # break the loop


    # ask_for_coordinates
        # Display message asking for coordinates
        # pull coordinates from command line

    # validate_coordinates_format
        # UNLESS coordinates are in the proper format
            # display error message

end

# Maintains game board state
class Board
    # initialize board
        # set up blank data structure

    # render
        # loop through data structure
            # display an existing marker if any, else blank

    # add_piece
        # IF piece_location_valid?
            # place piece
        # ELSE
            # display error message

    # piece_location_valid?
        # Is the placement within_valid_coordinates?
        # Are the piece coordinates_available?

    # within_valid_coordinates?
        # UNLESS piece coords are in the acceptible range
            # display an error message

    # coordinates_available?
        # UNLESS piece coords are not occupied
            # display error message

    # winning_combination?
        # is there a winning_diagonal?
        # or winning_vertical? 
        # or winning_horizontal? for that piece?

    # winning_diagonal?
        # check if specified piece has a triplet across diagonals

    # winning_vertical?
        # check if specified piece has a triplet across verticals

    # winning_horizontal?
        # check if specified piece has a triplet across horizontals

    # diagonals
        # return the diagonal pieces

    # verticals
        # return the vertical pieces

    # horizontals
        # return the horizontal pieces

    # full?
        # does every square contain a piece?
end

5. Convert Pseudocode to Ruby

The following may seem like a lot of code but it's actually just filling in the same pseudocode you just saw with Ruby. We can (and should) remove most of the pseudocode comments but they're left in so you can see the conversion process.

class TicTacToe
    # initialize
    def initialize
        # set up the board
        @board = Board.new

        # set up the players
        @player_x = Player.new("Madame X", :x, @board)
        @player_y = Player.new("Mister Y", :y, @board)

        # assign the starting player
        @current_player = @player_x
    end

    # play
    def play

        # loop infinitely
        loop do
            # call the board rendering method
            @board.render

            # ask for coordinates from the current player
            @current_player.get_coordinates

            # check if game is over
            break if check_game_over

            # switch players
            switch_players
        end
    end

    # check_game_over?
    def check_game_over
        # check for victory
        # check for draw
        check_victory || check_draw
    end

    # check_victory?
    def check_victory
        # IF Board says current player's piece has
        # a winning_combination?
        if @board.winning_combination?(@current_player.piece)
            # then output a victory message
            puts "Congratulations #{@current_player.name}, you win!"
            true
        else
            false
        end
    end

    # check_draw?
    def check_draw
        # If Board says we've filled up 
        if @board.full?
            # display draw message
            puts "Bummer, you've drawn..."
            true
        else
            false
        end
    end

    # switch_players
    def switch_players
        if @current_player == @player_x
            @current_player = @player_y
        else
            @current_player = @player_x
        end
    end

end


# Manages all player-related functionality
class Player
    attr_accessor :name, :piece

    # initialize
    def initialize(name = "Mystery_Player", piece, board)
        # Set marker type (e.g. X or O)
        raise "Piece must be a Symbol!" unless piece.is_a?(Symbol)
        @name = name
        @piece = piece
        @board = board
    end

    # get_coordinates
    def get_coordinates
        # loop infinitely
        loop do
            # ask_for_coordinates
            coords = ask_for_coordinates

            # IF validate_coordinates_format is true
            if validate_coordinates_format(coords)
                # IF piece can be placed on Board
                if @board.add_piece(coords, @piece)
                    # break the loop
                    break
                end
            end
        end
    end


    # ask_for_coordinates
    def ask_for_coordinates
        # Display message asking for coordinates
        puts "#{@name}(#{@piece}), enter your coordinates in the form x,y:"
        # pull coordinates from command line
        gets.strip.split(",").map(&:to_i)
    end

    # validate_coordinates_format
    def validate_coordinates_format(coords)
        # UNLESS coordinates are in the proper format
        if coords.is_a?(Array) && coords.size == 2
            true
        else
            # display error message
            # Note that returning `nil` acts falsy!
            puts "Your coordinates are in the improper format!"
        end
    end

end


# Maintains game board state
class Board
    # initialize board
    def initialize
        # set up blank data structure
        @board = Array.new(3){Array.new(3)}
    end

    # render
    def render
        puts
        # loop through data structure
        @board.each do |row|
            row.each do |cell|
                # display an existing marker if any, else blank
                cell.nil? ? print("-") : print(cell.to_s)
            end
            puts
        end
        puts

    end

    # add_piece
    def add_piece(coords, piece)
        # IF piece_location_valid?
        if piece_location_valid?(coords)
            # place piece
            @board[coords[0]][coords[1]] = piece
            true
        else
            false
        end
    end

    # piece_location_valid?
    def piece_location_valid?(coords)
        # Is the placement within_valid_coordinates?
        if within_valid_coordinates?(coords)
            # Are the piece coordinates_available?
            coordinates_available?(coords)
        end
    end

    # within_valid_coordinates?
    def within_valid_coordinates?(coords)
        # UNLESS piece coords are in the acceptible range
        if (0..2).include?(coords[0]) && (0..2).include?(coords[1])
            true
        else
            # display an error message
            puts "Piece coordinates are out of bounds"
        end
    end

    # coordinates_available?
    def coordinates_available?(coords)
        # UNLESS piece coords are not occupied
        if @board[coords[0]][coords[1]].nil?
            true
        else
            # display error message
            puts "There is already a piece there!"
        end
    end

    # winning_combination?
    def winning_combination?(piece)
        # is there a winning_diagonal?
        # or winning_vertical? 
        # or winning_horizontal? for that piece?
        winning_diagonal?(piece)   || 
        winning_horizontal?(piece) || 
        winning_vertical?(piece)
    end

    # winning_diagonal?
    def winning_diagonal?(piece)
        # check if specified piece has a triplet across diagonals
        diagonals.any? do |diag|
            diag.all?{|cell| cell == piece }
        end
    end

    # winning_vertical?
    def winning_vertical?(piece)
        # check if specified piece has a triplet across verticals
        verticals.any? do |vert|
            vert.all?{|cell| cell == piece }
        end
    end

    # winning_horizontal?
    def winning_horizontal?(piece)
        # check if specified piece has a triplet across horizontals
        horizontals.any? do |horz|
            horz.all?{|cell| cell == piece }
        end
    end

    # diagonals
    def diagonals
        # return the diagonal pieces
        [[ @board[0][0],@board[1][1],@board[2][2] ],[ @board[2][0],@board[1][1],@board[0][2] ]]
    end

    # verticals
    def verticals
        # return the vertical pieces
        @board
    end

    # horizontals
    def horizontals
        # return the horizontal pieces
        horizontals = []
        3.times do |i|
            horizontals << [@board[0][i],@board[1][i],@board[2][i]]
        end
        horizontals
    end

    # full?
    def full?
        # does every square contain a piece?
        @board.all? do |row|
            row.none?(&:nil?)
        end
    end

end

t = TicTacToe.new
t.play
> load 'tictactoe.rb'
---
---
---
Enter your coordinates in the form x,y:

6. Refactor

In this case, the first refactoring step would be to remove the extra comments and to break out each class into its own .rb file because both of those things are universal. There are some other places where we used a verbose approach (for instance when working with coordinates) and some more elegant Ruby might function equally well. What can you find to improve?

Wrapping Up

Read through the code above and make sure you understand what's going on! Try to see how we identified the procedures and objects that make up this problem using high level pseudocode and then how they shook out into classes and methods.

You can view the code above or on Github here.

Pay special attention to the fact that each object tried to stay out of the other objects' business (for instance, the TicTacToe class sets up a Board and the Players, but fully delegates specific tasks like collecting input and validating game ending conditions to each of them).

You'll get practice with this during the assignment and the projects.

Octocat 300


Sign up to track your progress for free

There are ( ) additional resources for this lesson. Check them out!

Sorry, comments aren't active just yet!

Next Lesson: Refactoring Code