8
\$\begingroup\$

Here's my first shot at implementing Tic Tac Toe in Ruby. After watching Gary Bernhardt's Functional Core, Imperative Shell, I thought this would be a great exercise in trying out the ideology presented in that screencast. Tear my code apart.

Link to GitHub repo (includes video of usage)

tic_tac_toe.rb -- the procedural shell

require_relative "board"
require_relative "cell"
require "pry"

def initialize_board
  @board = Board.new
end

def choose_characters
  puts "X or O?"
  @player_character = gets.chomp
  @computer_character = case @player_character
                        when "X" then "O"
                        when "O" then "X"
                        end
end

def start_game
  initialize_board
  choose_characters
  play
end

def play
  loop do
    puts "Make a move: "
    move = gets.chomp
    board_index = Board::POSITIONS[move]
    winner = @player_character

    @board.board[board_index].value = @player_character

    if [email protected]?
      @board.available_cells.sample.value = @computer_character
      winner = @computer_character if @board.winner?
    end

    if @board.winner?
      p "#{winner.upcase} WINS!!!"
      @board.display
      puts "Do you want to play again?"

      start_game if gets.chomp == "Yes"
    else
      @board.display
      play
    end

    break if @board.winner?
  end
end

start_game

board.rb

class Board
  attr_accessor :board

  WINNING_COMBINATIONS = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6]
  ]

  POSITIONS = {
    "top left" => 0,
    "top middle" => 1,
    "top right" => 2,
    "middle left" => 3,
    "center" => 4,
    "middle right" => 5,
    "bottom left" => 6,
    "bottom middle" => 7,
    "bottom right" => 8
  }

  def initialize
    @board = Array.new(9) { Cell.new }
  end

  def winner?
    winner = false
    WINNING_COMBINATIONS.each do |combination|
      first_cell = board[combination[0]].value
      second_cell = board[combination[1]].value
      third_cell = board[combination[2]].value
      consideration = [first_cell, second_cell, third_cell]

      if consideration.uniq.length == 1 && first_cell.is_a?(String)
        winner = true
      end
    end
    winner
  end

  def available_cells
    board.select { |cell| cell.value == :blank }
  end

  def display
    rows = board.each_slice(3).to_a

    rows.each do |row|
      p row.map(&:value)
    end
  end
end

cell.rb

class Cell
  attr_accessor :value

  def initialize(options = {})
    @value = options.fetch(:value, :blank)
  end
end
\$\endgroup\$
5
  • \$\begingroup\$ Does this code live in a repository somewhere that is easy to grab and work with? \$\endgroup\$
    – vgoff
    Apr 30, 2015 at 4:43
  • \$\begingroup\$ Yes! It's here: github.com/dylanerichards/tic-tac-toe. There's even a video of me demonstrating use of the application. I'll update the original post. \$\endgroup\$ Apr 30, 2015 at 5:14
  • \$\begingroup\$ Now that is some very useful information to have right there. :) You even have specs... Was having some difficulties running it without referring to the code to know what the expected inputs were. \$\endgroup\$
    – vgoff
    Apr 30, 2015 at 5:20
  • \$\begingroup\$ The fact that you had to explain the moves in your video by stating verbally what the list of valid locations are, means that there is not enough of a prompt, there should be something indicating move options, just like (and probably less reason to be) your prompt showing the very well know and expected "X" and "O" options for player. \$\endgroup\$
    – vgoff
    Apr 30, 2015 at 6:15
  • \$\begingroup\$ "9 openings (8 indexes)" 0 is an index as well. You have 9 indices, (or the other valid English plural, 'indexes') to be sure. Otherwise you are missing a valid placement on a tic-tac-toe board. \$\endgroup\$
    – vgoff
    Apr 30, 2015 at 6:54

1 Answer 1

2
\$\begingroup\$

You require pry, and you don't seem to use it. Perhaps it is being used for troubleshooting, and the binding.pry or other code, has been removed. In that case it may be more appropriate to simply use the -rpry option as in ruby -rpry file.rb. Then you don't have to remember to remove that line, just the troubleshooting line. (Or even better, set up a system that lets you leave things in but turn them off and on from an option in the system call. Perhaps -w or -v flags. Perhaps an environment variable. A line in with this in mind may look like this: binding.pry if $VERBOSE And a require 'pry' if $VERBOSE)

Looking at your loop do tells me that you have an infinite loop. But you have a well defined exit strategy, there is no need for an infinite loop. Using something like until @board.winner? will run the block at least once, and will stop when your method returns true/truthy value. You also call 'play' from inside the loop inside of play, but it is already going to start from the beginning of the loop.

If you had folders bin/, lib/, I would expect some of the code in your tic-tac-toe.rb file to be in maybe bin/tic-tac-toe. The start equivalent for sure. This will allow you to use the lib/tic-tac-toe.rb file in a way that someone that requires that library would expect. Right now, you are unable to require that library file without starting the game.

The interface of using words like top left seems clumsy and error prone, even if it is nice. Creating a numbered or lettered menu or presenting the grid with numbers or letters could make play easier, less typing to do, less error prone. Perhaps a lettered 'x, y' grid. Presenting that grid would also document the game play, and make initial play more straight forward.

Your "choose_character" method does two things, it prompts and processes the choice. The @player_character and @computer_character is repetitive, confusing in a way. Simply having @player and @computer is probably clear enough. @player.character might be better, though I think we consider that letter a 'marker', as the player mark may be 'X' and the AI mark may be 'O'.

Looking at cell.rb you could be using the new named arguments in Ruby 2.0.

In the Board file you can likely use the all? Enumerable method, if an array has all? the markers that match the current players marker, then that player is the winner.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.