Search code examples
ruby

What causes NameError in Game Object?


I am learning OOP with Ruby. I have 2 classes - Game & Player. Player and Computer are to be created for Tick-Tack-Toe game. My idea is to use Player class to create a player and a computer for the game.

I believe Player class could have interaction with Game class hence Game class is initialised with @player. Game class should be able to print out player_name, however, it doesn't happen as I plan.

class Game
    attr_accessor :player, :computer

    @@board = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

    def initialize(player, computer)
        @player = player
        @computer = computer

        puts "#{player.name} vs #{computer.name}"
    end

    def self.player_name
        puts "#{player.name}" # line 14
    end

    def self.computer_name
        puts "#{computer.name}"
    end

    def self.status
        puts "#{@@board}"
    end
end
class Player
    attr_reader :name

    def initialize(name)
        @name = name

        puts "#{name} is online."
    end
end

I execute codes below:

p1 = Player.new('John')
p2 = Player.new('Computer')
Game.new(p1, p2)
Game.player_name # line 39
Game.computer_name

Got error message below. The error comes from line 39 and 14.

John is online.
Computer is online.
John vs Computer
test.rb:14:in `player_name': undefined local variable or method `player' for Game:Class (NameError)

        puts "#{player.name}"
                ^^^^^^
Did you mean?  player_name
        from test.rb:39:in `<main>'

I am really confused where is my mistake.


Solution

  • I am really confused where is my mistake.

    You're mixing instance variables / methods with class variables / methods. attr_accessor :player defines an attribute backed by an instance variable @player. In initialize you set that instance variable. With def self.player_name however you define a class method and the class doesn't know player (only the instance does).

    To fix this, turn your class methods to instance methods: (remove self.)

    class Game
      attr_accessor :player, :computer
    
      def initialize(player, computer)
        @player = player
        @computer = computer
      end
    
      def player_name
        puts "#{player.name}"
      end
    
      def computer_name
        puts "#{computer.name}"
      end
    end
    

    And assign your game instance to a variable, just like you did with p1 and p2, so you can call these instance methods:

    game = Game.new(p1, p2)
    game.player_name    # prints: John
    game.computer_name  # prints: Computer
    

    I've left out @@board to keep the above example small. In general, you should avoid class variables because of their unusual behavior. (see Why is using a class variable in Ruby considered a 'code smell'?)

    I would just assign it to another instance variable:

    class Game
      attr_accessor :player, :computer
    
      def initialize(player, computer)
        @board = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
    
        # ...
      end
    end