1

I've created this game thing in order to learn OOP and I'm having trouble with part of it. Here's what's causing me problems:

I have two classes. On line 3 of class Player, I have some code which is probably way wrong, but basically, what I'm trying to do is use armor to modify how much damage a player receives. I'm getting an error, though: "undefined method 'protection' for nil:NilClass (NoMethodError)

I have Armor as another class. I think the problem might relate to the fact that I am calling @armor.protection when protection is mentioned in Armor and @armor is mentioned in Player, but I am unsure how to fix this. I have added all the code I believe is relevant to my question below. Like I said, I'm really new at this, so please use terminology a noob could understand.

class Player
  def equip(armor)
    @armor = armor
  end

  def hit(damage)
    #damage = damage - @armor.protection
    @health -= damage
  end
end  

class Armor
  def initialize(name, protection)
    @protection = protection
  end
end

EDIT: added additional code to show all of what I've got going on for clarification. I don't expect anyone to read all of what I've got, though. :S It's probabably scary and snarled up. :P

class Player 

  def initialize(name, health) 
    @name = name 
    @health = health 
  end 

  def equip(armor) 
    @armor = armor 
  end 

  def health 
    @health 
  end 

  def health=(value) 
    @health = value 
  end 

  def hit(damage) 
damage = damage - @armor.protection 
    @health -= damage 
  end 

  def dead? 
if @health <= 0 
return true 
elsif @health > 0 
return false 
end 
  end 

  def name 
    @name 
  end 

  def attack(target) 
    damage = rand(30) 
    puts "#{@name} attacks #{target.name}" 
target.hit(damage) 
puts "#{@name} hits #{target.name} for #{damage} damage." 
  end 
end 

class Armor 
  def initialize(name, protection) 
  @protection = protection 
  end 
end 


player1 = Player.new("Melanie", 100) 
player2 = Player.new("a Monster", 200) 
shirt = Armor.new('shirt', 4) 
player1.equip(shirt) 

while player1.dead? == false && player2.dead? == false 
  player1.attack(player2) 
    if player2.health > 0 
      puts "#{player2.name}'s health is at #{player2.health}." 
    elsif player2.health <= 0 
puts "#{player2.name} has no health." 
end 
  player2.attack(player1) 
    if player1.health > 0 
      puts "#{player1.name}'s health is at #{player1.health}." 
    elsif player1.health <= 0 
puts "#{player1.name} has no health." 
end 
end 

if player1.health > player2.health 
  puts "#{player2.name} is dead." 
  puts "#{player1.name} wins." 
elsif player2.health > player1.health 
  puts "#{player1.name} is dead." 
  puts "#{player2.name} wins." 
elsif player2.health == player1.health 
  puts "#{player1.name} and #{player2.name} killed each other." 
end 

4 Answers 4

3

If your Armor class has a protection method, it would work fine. However it doesn't, so even if you were to call it from inside the Armor class, you'd get the same error. To define it you can either use attr_reader or attr_accessor or define it by hand.

class Armor
  attr_accessor :protection

  def initialize(name, protection)
    @protection = protection
  end
end

or

class Armor
  def initialize(name, protection)
    @protection = protection
  end

  def protection
    @protection
  end
end
Sign up to request clarification or add additional context in comments.

4 Comments

As of right now, the program doesn't know that by saying armor, I mean, in this player's case, a shirt. The only mention I have of armor, lowercase a, besides in damage = damage - @armor.protection is with the definition for equip in the class Player. I tried the second example, hoping it would take care of this, but in either case I'm still having the same error. I know I'm a bit over my head, but I do understand a lot of it and it's definitely a learning experience. :S
@Melanie: First of all my code was wrong as I accidentally typed armor when I mentioned protection, so it couldn't work. Now I fixed it and as long as you call equip before calling any method which uses @armor, it will work.
A better solution to requiring the calling of equip first might be to initialize @armor to a zero'd out Armor class instance in the Player class initialize method. That way you don't have to worry about it if the player starts out with no armor.
And just for reference and Google fodder: @Mike Bethany's suggestion is the Null Object Refactoring.
2

I just ran your 2nd (full) example.

Besides the accessor problem explained in the other answers (just add attr_reader :protection to class Armor), you overlooked something in the test scenario :)

The error message gives a hint: undefined method 'protection' for nil:NilClass (NoMethodError). Given that this is caused in the 1st line of the hit method, this means @armor was nil, and of course nil is not an instance of Armor, so it has no protection method. Why was it nil? Well, look at how your fight begins:

player1 = Player.new("Melanie", 100) 
player2 = Player.new("a Monster", 200) 
shirt = Armor.new('shirt', 4)
player1.equip(shirt) 

Only Melanie has a shirt, and you gave the monster no armor at all! Not very fair, is it :)

To fix that, you either need to give him some armor, or change your hit method so that it still works when @armor was not initialized. A nice OO way of doing that is to initialize all players with a default dummy armor that provides no protection:

class Player
  def initialize(name, health)
    @armor = Armor.new('nothing', 0)
    # ...

Done!


Now, since that dummy armor will be useful whatever the specific rules of your game are, I'd abstract it from the point of view of class Player, by making class Armor responsible for creating it instead:

class Armor
  class << self # define a class method, like new
    def none
      self.new('nothing', 0)
    end
  end
  # ...

Then you can just say Armor.none instead of Armor.new('nothing', 0) in Player.initialize. This way, if you need to change how Armor works internally, you can update the dummy armor at the same time, and without touching other classes.

3 Comments

Wow! That's amazing! Thank you so much! I wouldn't have thought that was a problem in a million years. I'll have to learn to better read error messages. At least I'm a couple steps up from "OMG! An error!"
You're welcome. BTW, if I can nitpick some more, I see while player1.dead? == false && player2.dead? == false... clumsy style! At the minimum, replace some_boolean == false by not some_boolean; personally I'd replace the dead? predicate by an alive? one that you can use directly without a negation :)
Yeah, that's what my friend said! Thank you!
1

The problem here is that you have a @protection instance variable, but no "accessor" to get to it. Instance variables are private to the instance of the class they're owned by, so if you want to expose them to the outside world, you have to set up accessors to do so. In this case, you want:

class Armor
  attr_reader :protection
  ...
end

This will let you call @armor.protection, and will return the value of your armor instance's @protection variable.

1 Comment

This definitely has helped me understand the attr_reader, attr_accessor thing because I never "got it" before, but I think part of the problem for me is that I haven't "tied" armor to Armor. I'm not sure how to do this. As of right now, the program doesn't know that by saying armor, I mean, in this player's case, a shirt. The only mention I have of armor, lowercase a, besides in damage = damage - @armor.protection is with the definition for equip in the class Player. :S
1

Try this:

player = Player.new 
armor = Armor.new('Mythril', 100)
player = player.equip(armor)  #Initialise the armor object inside Player.

player.hit(10)

2 Comments

should be player = Player.new and player.equip(armbor) and player.hit(10) , these are all instance methods.
I think I have something kind of like that already. I have added all of the code I have to clarify. Hopefully it makes sense.

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.