0

I'm building a simple tic-tac-toe game and I've totally hit a wall with my grid.

I'd like to "puts @grid" including player input every time players have made their choice. How can I achieve this? I'm almost a total beginner with Ruby and have never build any games before. Any help is greatly appreciated, thanks in advance!

I tried making two different grids (grid and grid_with_markers) but couldn't figure out where to go from there. Looking back, having two grids also just seemed like a bad idea. I also tried having a Hash (marker_positions), but that seemed overly complicated compared to an Array.

Here is the grid

def initialize
    @possible_choice = [1,2,3,4,5,6,7,8,9]
    @marker_positions = [1,2,3,4,5,6,7,8,9]
    @grid = "
                |----|----|----|   
                |  #{@marker_positions[0]} |  #{@marker_positions[1]} |  #{@marker_positions[2]} |
                |----|----|----|
                |  #{@marker_positions[3]} |  #{@marker_positions[4]} |  #{@marker_positions[5]} |
                |----|----|----|
                |  #{@marker_positions[6]} |  #{@marker_positions[7]} |  #{@marker_positions[8]} |
                |----|----|----|
                "
end

I'd like to use a method add_markers to show the grid with user input. So when a player selects number 1 @marker_positions[0] would be replaced as "X" (or "O"). Number 4 would replace @marker_positions[3] etc.

Edit: I realised the title of this post was misleading since I actually want to replace elements of @marker_positions Array with Strings ("X" or "O"). But the replaced element is selected based on user input, in other words, player_one and player_two Arrays.

def add_markers
  puts @grid
end

Here's the player_one_turn method

def player_one_turn()
    puts "Player One, make your choice:"
    p @possible_choice
    add_markers
    @@player_one << @possible_choice.delete(gets.chomp.to_i)
    p "Player One has chosen: #{@@player_one}"
end

And here's my entire tictactoe.rb file.


class Grid
    WINNING_COMBOS = [
        [1,2,3],[4,5,6],[7,8,9],
        [1,4,7],[2,5,8],[3,6,9],
        [1,5,9],[3,5,7]
    ]
    attr_accessor :possible_choice
    attr_accessor :marker_positions
    attr_accessor :grid

    def initialize
        @possible_choice = [1,2,3,4,5,6,7,8,9]
        @marker_positions = [1,2,3,4,5,6,7,8,9]
        @grid = "
                |----|----|----|   
                |  #{@marker_positions[0]} |  #{@marker_positions[1]} |  #{@marker_positions[2]} |
                |----|----|----|
                |  #{@marker_positions[3]} |  #{@marker_positions[4]} |  #{@marker_positions[5]} |
                |----|----|----|
                |  #{@marker_positions[6]} |  #{@marker_positions[7]} |  #{@marker_positions[8]} |
                |----|----|----|
                "
    end

    def add_markers

        puts @grid
    end
end

class Game < Grid
    @@player_one = Array.new
    @@player_two = Array.new

    def game
        puts
        puts "*** This is a tic-tac-toe game for two human players. ***"
        puts
        loop do
            player_one_turn()
            puts
                if has_won?(@@player_one)
                    puts "The game has ended. Player One has won!"
                    puts
                    return
                end
            break if @@player_one.length == 5 || @@player_one.include?(nil)
            player_two_turn()
            puts
                if has_won?(@@player_two)
                    puts "The game has ended. Player Two has won!"
                    puts
                    return
                end
        end
    end

    def player_one_turn()
        puts "Player One, make your choice:"
        p @possible_choice
        add_markers
        @@player_one << @possible_choice.delete(gets.chomp.to_i)
        p "Player One has chosen: #{@@player_one}"
    end

    def player_two_turn()
        puts "Player Two, make your choice:"
        p @possible_choice
        add_markers
        @@player_two << @possible_choice.delete(gets.chomp.to_i)
        p "Player Two has chosen: #{@@player_two}"
    end

    def has_won?(player)
        WINNING_COMBOS.any? { |combo| (player & combo).size == combo.size}
    end
end

new_game = Game.new
new_game.game

(I know it isn't very clean. Thank you for taking time to read all the way down here.)

3
  • Why don't you make @marker_positions a 3x3 matrix, so that whenever a user inputs a position, you find the corresponding row and column, and update the value stored in that position? Commented May 29, 2019 at 8:53
  • @StefanRendevski in a 3×3 grid choosing a slot from 1 to 9 seems easier than asking the user for row and column. Commented May 29, 2019 at 9:59
  • I should try that too. Thanks for the reply! Commented May 29, 2019 at 10:25

1 Answer 1

1

So when a player selects number 1 @marker_positions[0] would be replaced as "X"

You just have to read the user input: (example shows result for player entering 1)

input = gets.to_i
#=> 1

and replace the corresponding array element: (we have to subtract 1 because array is zero-based)

@marker_positions[input - 1] = 'X'

Using the array values within the grid string is another issue. The interpolations via #{...} happens when creating the string, just before it is assigned to @grid in initialize. This means the string is not being updated when the interpolated value changes afterwards – it just stays the same unless you interpolate it again:

x = 'foo'
@grid = "hello #{x}"
#=> "Hello foo"

x = 'bar'
@grid
#=> "Hello foo"  <- doesn't change

@grid = "hello #{x}"
#=> "Hello bar"

To get a "fresh" grid every time you call add_markers you could simply move the string interpolation code from initialize into that method: (instead of "...", I'm using <<~EOD ... EOD, a "squiggly" heredoc which ignores leading whitespace)

class Grid
  attr_accessor :marker_positions

  def initialize
    @marker_positions = [1, 2, 3, 4, 5, 6, 7, 8, 9]
  end

  def add_markers
    puts <<~EOD
      |----|----|----|
      |  #{@marker_positions[0]} |  #{@marker_positions[1]} |  #{@marker_positions[2]} |
      |----|----|----|
      |  #{@marker_positions[3]} |  #{@marker_positions[4]} |  #{@marker_positions[5]} |
      |----|----|----|
      |  #{@marker_positions[6]} |  #{@marker_positions[7]} |  #{@marker_positions[8]} |
      |----|----|----|
    EOD
  end
end

This would result in: (the # are not part of the actual output)

grid = Grid.new
grid.add_markers
#|----|----|----|
#|  1 |  2 |  3 |
#|----|----|----|
#|  4 |  5 |  6 |
#|----|----|----|
#|  7 |  8 |  9 |
#|----|----|----|

grid.marker_positions[0] = 'X'
grid.marker_positions[4] = 'X'
grid.marker_positions[8] = 'X'
grid.add_markers
#|----|----|----|
#|  X |  2 |  3 |
#|----|----|----|
#|  4 |  X |  6 |
#|----|----|----|
#|  7 |  8 |  X |
#|----|----|----|

Another – maybe cleaner – option is to define a template with placeholders that are replaced by their actual (or current) values when rendering the template:

class Grid
  TEMPLATE = <<~EOD.freeze
    ┌───┬───┬───┐
    │ 1 │ 2 │ 3 │
    ├───┼───┼───┤
    │ 4 │ 5 │ 6 │
    ├───┼───┼───┤
    │ 7 │ 8 │ 9 │
    └───┴───┴───┘
  EOD

  attr_accessor :marker_positions

  def initialize
    @marker_positions = [1, 2, 3, 4, 5, 6, 7, 8, 9]
  end

  def render
    puts TEMPLATE.gsub(/[1-9]/) { |d| @marker_positions[d.to_i - 1] }
  end
end

gsub scans the template string for digits 1 to 9 and replaces each occurrence d with the block's result, which simply picks the corresponding value from our array.

Usage:

grid = Grid.new
grid.render
#┌───┬───┬───┐
#│ 1 │ 2 │ 3 │
#├───┼───┼───┤
#│ 4 │ 5 │ 6 │
#├───┼───┼───┤
#│ 7 │ 8 │ 9 │
#└───┴───┴───┘

grid.marker_positions[0] = 'X'
grid.marker_positions[4] = 'X'
grid.marker_positions[8] = 'X'
grid.render
#┌───┬───┬───┐
#│ X │ 2 │ 3 │
#├───┼───┼───┤
#│ 4 │ X │ 6 │
#├───┼───┼───┤
#│ 7 │ 8 │ X │
#└───┴───┴───┘

The Unicode box-drawing characters are just for demonstration purposes, it will work just fine with ASCII characters.

Based on the above, a very basic game loop could look like this:

grid = Grid.new

%w[X O].cycle do |marker|
  grid.render
  print "Player #{marker}: "
  input = gets.to_i
  grid.marker_positions[input - 1] = marker
  # TODO: break if player wins
end

The code cycles between "X" and "O" and for each iteration:

  • renders the grid
  • ask the player with that marker for their input
  • updates the marker_positions

What's missing is an additional step to break the loop if a player wins. And probably some logic to validate the input (prevent a player from overwriting a spot already taken, check that input is within 1-9 etc).

Hope this helps.

A last note: assuming that users enter their input via a computer keyboard, you might want to flip the grid vertically to match the layout of a numeric keypad.

Sign up to request clarification or add additional context in comments.

3 Comments

Thanks! That's very helpful (if a little confusing). I uploaded my working game to github link. I know, it isn't very DRY. Need to improve it though.
Oh, didn't notice you edited your answer. That's even more helpful! Thank you so much.
@keskiviikko you're welcome. BTW, if having two separate arrays seems like a bad idea in hindsight, you should maybe just ditch them. You're writing software after all, it's easy to change :-)

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.