keskiviikko
keskiviikko

Reputation: 9

Ruby: How to replace elements in an Array?

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.)

Upvotes: 0

Views: 375

Answers (1)

Stefan
Stefan

Reputation: 114158

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.

Upvotes: 1

Related Questions