Tic Tac Toe

The game of kings and coding interviews
Posted by Conor Breen on June 28, 2019 · 21 mins read

I’m back! And in Toronto now. There hasn’t been a post in quite some time. It turns out that moving to a different country is more demanding of your time than I had once thought. So after spending the obligatory month-and-a-bit making sure I wasn’t homeless, getting a credit score and going through the financial turmoil of furnishing a living space I’m back to doing what I love, coding.

I thought I’d start with something relevant to me at the moment, which is a coding test. This particular one was given to me in my last place of work for my coding interview. Tic Tac Toe.

side note: If you’re a perspective employer reading this, hello, I’m great and a super great employee.

OK, lets get started.

The game is Tic Tac Toe, everyone’s played it and it makes for a good coding exercise. Its a 3 x 3 board where you each take a turn marking an X or an O with the hopes of getting three in a row. simple

lets begin with creating the Game class

class Game
  def initialize()

  end
end

Now we have a class with an initialize method in it, easy money. Now lets get the contestants.

class Game
  def initialize()
   setup_players
  end
  
  def setup_players
    puts("Hello and welcome to Tic Tac Toe")
    puts("Please enter the name of player 1:")
    @player1 = gets.chomp()

    puts("Please enter the name of player 2:")
    @player2 = gets.chomp()
    puts("welcome #{@player1} and #{@player2}. #{@player1} goes first.")
  end
end

So here we’ve added a simple method to setup the players, it prompts them to enter their names and grabs the text and stores it in a class variable. We also want this to run as soon as the game is run so we call the method in our initialize method.

Its all well and good having the players but they have nothing to play, lets create the board.

  def setup_board
    @board = {one: 1, two: 2, three: 3, four: 4, five: 5,
              six: 6, seven: 7, eight: 8, nine: 9}
  end

  def get_board
    puts("\n")
    puts("#{@board[:one]} | #{@board[:two]} | #{@board[:three]}")
    puts("- + - + -")
    puts("#{@board[:four]} | #{@board[:five]} | #{@board[:six]}")
    puts("- + - + -")
    puts("#{@board[:seven]} | #{@board[:eight]} | #{@board[:nine]}")
    puts("\n")
  end

Now we’re cooking with gas, we have a method that creates a hash giving each tile on the board a number, the idea being that if player ones presses 1 it will be replaced with an x. The get board method will get the board in its current state and print it to the screen. so when we run the game we’ll be asked for our names and presented with the board

1 | 2 | 3
- + - + -
4 | 5 | 6
- + - + -
7 | 8 | 9

This game is shaping up nicely, but the players can’t interact with the board in any way. This might stifle gameplay so lets address that next. We want to make it possible for players to take turns choosing tiles and having the board update to reflect those changes. So, 1.) we need to prompt the player to pick a tile. 2.) we need to start a counter to govern who’s turn it is and 3.) we need to print the new updated board back so the players can see where they are in the game.

  # the updated initialize method with our new methods in it.
  def initialize()
    @turn_counter = 0
    setup_players
    setup_board
    get_board
    player_move
  end
 

  def player_move
    if @turn_counter < 9 && @turn_counter.even?
      print("#{@player1} please select a tile:")
      key = gets.chomp().to_i
        @board[@board.keys[key - 1]] = 'x'
        @turn_counter += 1
      end
    else
      print("#{@player2} please select a tile:")
      key = gets.chomp().to_i
      @board[@board.keys[key - 1]] = 'o'
      @turn_counter += 1
     end
     get_board
    end

Lets take a look at what we’ve done. The player_move method checks the counter and if its even (0 is even too) its player1’s go, and if its odd its player2’s go. Simple, now it prompts the player to pick a tile, stores the input as a int, checks the value against the hash we created (subtracting 1 from the number because this is programming and everything starts at 0) earlier and updates it. so when we print the board again at the end of this method we can see the updated board with the player’s input on it.

x | o | x
- + - + -
o | 5 | 6
- + - + -
7 | 8 | 9

So far so good, but there’s no winning in this game, just a board you get to update. The game looses its charm after you notice it never ends and you can update tiles that have already been chosen. Its a modern take on the game thus far, but certainly the game-breaking “features” takes away from the experience somewhat. So next on the list are the win/draw conditions and making sure the players can only update a tile once.

First lets bang out the issue of multiple-use tiles:

  def tile_taken(tile)
    tile.kind_of?(String)
  end

  #updated player_move to check for used tiles
  def player_move
    if @turn_counter < 9 && @turn_counter.even?
      print("#{@player1} please select a tile:")
      key = gets.chomp().to_i
      if tile_taken(@board[@board.keys[key - 1]])
        puts("that tile is taken, please select another")
      else
        @board[@board.keys[key - 1]] = 'x'
        @turn_counter += 1
      end
    else
      print("#{@player2} please select a tile:")
      key = gets.chomp().to_i
      if tile_taken(@board[@board.keys[key - 1]])
        puts("that tile is taken, please select another")
      else
        @board[@board.keys[key - 1]] = 'o'
        @turn_counter += 1
      end
    end
    get_board
  end

Now that that’s been ironed out, onto the win/draw conditions. Tic Tac Toe as a game only has 8 possible combinations that result in a player winning, with a number that low we can just hard-code them in and do a quick check to see if a player has won after each turn. And because there’s only nine tiles, after 9 moves the game will end anyway because the board is full and the game is no longer playable.

  def win_condition_met
    if @board.values_at(:one,:two,:three).uniq.length == 1 ||
      @board.values_at(:four,:five,:six).uniq.length == 1 ||
      @board.values_at(:seven,:eight,:nine).uniq.length == 1 ||
      @board.values_at(:one,:four,:seven).uniq.length == 1 ||
      @board.values_at(:two,:five,:eight).uniq.length == 1 ||
      @board.values_at(:three,:six,:nine).uniq.length == 1 ||
      @board.values_at(:one,:five,:nine).uniq.length == 1 ||
      @board.values_at(:three,:five,:seven).uniq.length == 1
      @turn_counter.even? ? puts("Congratulations #{@player2} you are the winner!") : puts("Congratulations #{@player1} you are the winner!")
      exit()
    end
  end

  def move_limit_reached
    @turn_counter >= 9 ? puts("its a draw!") && exit() : player_move
  end

  #added the win/draw methods to check after each turn. 
  def player_move
    if @turn_counter < 9 && @turn_counter.even?
      print("#{@player1} please select a tile:")
      key = gets.chomp().to_i
      if tile_taken(@board[@board.keys[key - 1]])
        puts("that tile is taken, please select another")
      else
        @board[@board.keys[key - 1]] = 'x'
        @turn_counter += 1
      end
    else
      print("#{@player2} please select a tile:")
      key = gets.chomp().to_i
      if tile_taken(@board[@board.keys[key - 1]])
        puts("that tile is taken, please select another")
      else
        @board[@board.keys[key - 1]] = 'o'
        @turn_counter += 1
      end
    end
    get_board
    win_condition_met
    move_limit_reached
  end
end

So, rather than checking twice, once for player1 and once for player2 to see who’s won we can shorten it down a bit and use the .uniq method to grab an array of the values and see if there’s only one value in it. So if it’s either all x or all o it will return true. Then do a quick check with the turn counter to see who’s won. Almost every problem can be solved by putting stuff into arrays, remember: Work smart, not hard.

the next thing we did was to check to see if the game ended in a draw, and that’s easily done by checking the counter. So not much to explain there. We also put in an exit() at the end of each method because the game is over and you don’t want the game to continue after it’s over.

And that’s about it, this took just over an hour to so a perfect interview-amount-of-time for a coding challenge. Here’s the end result:

peter please select a tile:7

x | o | x
- + - + -
o | x | o
- + - + -
x | 8 | 9

Congratulations peter you are the winner!

If you were following along great, you’ve definitely run it by now and have had fun playing Tic Tac Toe. If not, Heres the TLDR, just copy and paste this code into a file and run it.

class Game
  def initialize()
    @turn_counter = 0
    setup_players
    setup_board
    get_board
    player_move
  end

  def setup_board
    @board = {one: 1, two: 2, three: 3, four: 4, five: 5, six: 6, seven: 7, eight: 8, nine: 9}
  end

  def setup_players
    puts("Hello and welcome to Tic Tac Toe")
    puts("Please enter the name of player 1:")
    @player1 = gets.chomp()

    puts("Please enter the name of player 2:")
    @player2 = gets.chomp()

    puts("welcome #{@player1} and #{@player2}. #{@player1} goes first, please select a tile")
  end

  def get_board
    puts("\n")
    puts("#{@board[:one]} | #{@board[:two]} | #{@board[:three]}")
    puts("- + - + -")
    puts("#{@board[:four]} | #{@board[:five]} | #{@board[:six]}")
    puts("- + - + -")
    puts("#{@board[:seven]} | #{@board[:eight]} | #{@board[:nine]}")
    puts("\n")
  end

  def win_condition_met
    if @board.values_at(:one,:two,:three).uniq.length == 1 ||
      @board.values_at(:four,:five,:six).uniq.length == 1 ||
      @board.values_at(:seven,:eight,:nine).uniq.length == 1 ||
      @board.values_at(:one,:four,:seven).uniq.length == 1 ||
      @board.values_at(:two,:five,:eight).uniq.length == 1 ||
      @board.values_at(:three,:six,:nine).uniq.length == 1 ||
      @board.values_at(:one,:five,:nine).uniq.length == 1 ||
      @board.values_at(:three,:five,:seven).uniq.length == 1
      @turn_counter.even? ? puts("Congratulations #{@player2} you are the winner!") : puts("Congratulations #{@player1} you are the winner!")
      exit()
    end
  end

  def move_limit_reached
    @turn_counter >= 9 ? puts("Its a draw!") && exit() : player_move
  end

  def tile_taken(tile)
    tile.kind_of?(String)
  end

  def player_move
    if @turn_counter < 9 && @turn_counter.even?
      print("#{@player1} please select a tile:")
      key = gets.chomp().to_i
      if tile_taken(@board[@board.keys[key - 1]])
        puts("that tile is taken, please select another")
      else
        @board[@board.keys[key - 1]] = 'x'
        @turn_counter += 1
      end
    else
      print("#{@player2} please select a tile:")
      key = gets.chomp().to_i
      if tile_taken(@board[@board.keys[key - 1]])
        puts("that tile is taken, please select another")
      else
        @board[@board.keys[key - 1]] = 'o'
        @turn_counter += 1
      end
    end
    get_board
    win_condition_met
    move_limit_reached
  end
end

Game.new()