I'm trying to make this test pass and am not sure how to go about making this pass.
TEST
def test_it_is_thirsty_by_default
vampire = Vampire.new("Count von Count")
assert vampire.thirsty?
end
def test_it_is_not_thirsty_after_drinking
vampire = Vampire.new("Elizabeth Bathory")
vampire.drink
refute vampire.thirsty?
end
CODE
def thirsty?
true
end
def drink
thirsty? === false
end
It is giving a failure message on the last test:
Failed refutation, no message given
What am I missing? My thought is that initially, the vampire is thirsty(true) and then defined a method that would then make the vampire not thirsty(false).
EDIT
Even if I reassign the drink method to:
thirsty? = false
I get a syntax error pointing to the = sign.
You're missing a couple things, most importantly some kind of writer method that allows you to store that fact that #thirsty is getting updated inside of your drink method call
There's a couple different ways to do this but I've shown one below with a few notes:
require 'test/unit'
class Vampire
def initialize(name)
#name = name
#thirsty = true # true by default
end
def drink
#thirsty = false # updates #thirsty for the respective instance
end
def thirsty?
#thirsty
end
end
class VampireTests < Test::Unit::TestCase
def test_it_is_thirsty_by_default
vampire = Vampire.new("Count von Count")
assert vampire.thirsty?
end
def test_it_is_not_thirsty_after_drinking
vampire = Vampire.new("Elizabeth Bathory")
vampire.drink
refute vampire.thirsty?
end
end
Related
class Airport
attr_reader :plane
CAPACITY = 20
def initialize
#plane = []
#capacity = CAPACITY
end
#can land a plane
def land(plane)
fail 'plane is full' if #plane.count >= #capacity
#plane << plane
end
def take_off
fail 'weather is stormy' if #weather
#plane
end
def weather
#weather = weather
end
end
class Weather
def stormy?
[true, false].sample
end
end
I am new to Ruby. I am a little confused in how to use the method of the Weather class in my Airport class.
I want the take_off method in my Airport class to fail when the weather is reported to be stormy. I am unsure where to begin in understanding object interactions. Any help would be greatly appreciated, thank you
If you want to fail if weather is stormy, you have to call its stormy? method:
def take_off
fail 'weather is stormy' if #weather.stormy?
# do something
end
But in order to get the above working, weather needs to be an instance of Weather. I would simply pass it to the constructor. In addition, I would make it a settable attribute, so you can easily update it:
class Airport
attr_accessor :weather
attr_reader :plane
CAPACITY = 20
def initialize(weather)
#weather = weather
#plane = []
#capacity = CAPACITY
end
# ...
end
I would also make the weather a little more deterministic, e.g. by having a wind force attribute:
class Weather
attr_reader :wind_force
def initialize(wind_force)
#wind_force = wind_force
end
def stormy?
wind_force >= 10
end
end
Usage:
weather = Weather.new(3)
airport = Airport.new(weather)
airport.take_off # no exception
airport.weather = Weather.new(12)
airport.take_off # RuntimeError: weather is stormy
I am building a game that is having a problem when calling a method. A monster can appear and will get a randomized weapon, and if that weapon is ranged the monster gets a one-turn setback to give the player a fighting chance. When the method monsterRangedTurnSetback is called I get the error that it is trying to find attributes in nil:NilClass. I ended up tracing it back to a genWeapon function, and that the function isn't being able to be called. Hers some code to
def monsterRangedTurnSetback(weapon)
attribs = weapon.attributes()
attribs.each do |attrib|
if attrib == "Ranged"
return 1
else
return 0
end
end
end
def genWeapon
weaponGen = rand(1..80)
if weaponGen == 1 or weaponGen == 2
weapon = GreatSword.new
hasTwoHandedWeapon = true
elsif weaponGen == (3..23)
weapon = ShortSword.new
hasTwoHandedWeapon = false
elsif weaponGen == (24..34)
weapon = ShortBow.new
hasTwoHandedWeapon = true
elsif weaponGen == (35..48)
weapon = LongBow.new
hasTwoHandedWeapon = true
elsif weaponGen == (49..64)
weapon = Dagger.new
hasTwoHandedWeapon = false
elsif weaponGen == (65..78)
weapon = HandCrossbow.new
hasTwoHandedWeapon = false
elsif weaponGen == 79 or weaponGen == 80
weapon = HeavyCrossbow.new
hasTwoHandedWeapon = true
end
return weapon
puts weapon.name
sleep 2
end
class Orc
attr_accessor :totalDamage, :totalHealth, :armorClass, :attackText, :name,
:turnSetback
def initialize
#wep = genWeapon()
#baseDamage = 7
#weapon = #wep
#turnSetback = monsterRangedTurnSetback(#weapon)
#wep = nil
#health = 5
#hasShield = shield(#weapon)
#armorClass = 6
if #hasShield == true
#armorClass += 2
end
#challengeLevel = 1
#attackText = ["Orc stabs you", "Orc slashes at you", "Orc intimidates you"]
#name = "Orc"
end
end
class ShortSword
attr_reader :attributes, :name, :attackBonus
def initialize
#attributes = ["Melee", "1Hand"]
attackBonus = 3
name = "Short Sword"
end
end
Yes the code goes in that order, and yes I know the monster class is allowing reading of non-existent variables. Any help is appreciated.
The mistake here might be that attr_reader cannot bind to local varaibles like x but only to instance variables like #x. In your code:
attr_reader :attackBonus
def initialize
# This is a local variable that will fall out of scope once the method
# finishes. It is not saved anywhere, simply thrown away.
attackBonus = 3
end
Adding an # prefix to that will make it persist and be readable.
The same thing plays out in genWeapon where local variables are set and discarded. If you need those persisted somehow you need to include them in the return. Those properties should be part of some kind of base Weapon class anyway, where you can call Dagger.new.two_handed? or Dagger.new.hands_required.
As #engineersmnky points out there's a crippling flaw in the genWeapon method where x == (1..2) will never return true for any value of x that isn't literally (1..2). What would work is (1..2).include?(x) or (1..2) === x. Since case uses === internally it makes it easy to write:
case (rand(1..80))
when 1..2
GreatSword.new
when 3..23
ShortSword.new
# ...
end
That's still really tedious. Instead write a lookup-table:
WEAPON_PROBABILITY = {
(1..2) => GreatSword,
(3..23) => ShortSword,
(24..34) => ShortBow,
(35..48) => LongBow,
(49..64) => Dagger,
(65..78) => HandCrossbow,
(79..80) => HeavyCrossbow
}.flat_map do |range, type|
range.to_a.map do |roll|
[ roll, type ]
end
end.to_h
This maps rolls to classes. Then your generator function becomes trivial:
def gen_weapon
WEAPON_PROBABILITY[rand(1..80)].new
end
Making use of Ruby's "everything is an object" principle to make look-up tables of classes dramatically simplifies things and can make the code more immediately understandable. Always try to steer your program towards defining things in terms of data instead of procedures whenever you can.
You might want to revisit how you define some of these classes. Perhaps even include the turn_delay as a method on the Weapon class. Here's how I might refactor this to inherit specialized weapons from a weapon parent class:
class Weapon
attr_reader :attributes, :name, :attack_bonus
def initialize
#attributes = []
end
def turn_delay?
#attributes.include? :ranged
end
def two_handed?
#attributes.include? :two_hand
end
end
class ShortSword < Weapon
def initialize
#attributes = %i(melee one_hand)
#attack_bonus = 3
#name = 'Short Sword'
end
end
class LongBow < Weapon
def initialize
#attributes = %i(ranged)
#attack_bonus = 10
#name = 'Long Bow'
end
end
bow = LongBow.new
puts bow.name
puts bow.turn_delay?
sword = ShortSword.new
puts sword.name
puts sword.turn_delay?
Output:
Long Bow
true
Short Sword
false
I had too much fun with this, a large number of weapons could become cumbersome to write class definitions for. Since you picked Ruby, you can embrace some meta programming and quickly whip up new weapons by using the following (requires you've defined that base Weapon class:
[
{ klass: 'BroadSword', attributes: [:melee, :two_hand], attack_bonus: 20, name: 'Broad Sword' },
{ klass: 'Dagger', attributes: [:melee, :one_hand], attack_bonus: 1, name: 'Dagger' },
{ klass: 'ShortBow', attributes: [:ranged], attack_bonus: 5, name: 'Short Bow' },
].each do |obj|
eval <<WEAPON
class #{obj[:klass]} < Weapon
def initialize
#attributes = #{obj[:attributes]}
#name = '#{obj[:name]}'
#attack_bonus = #{obj[:attack_bonus]}
end
end
WEAPON
end
Then:
bs = BroadSword.new
puts bs.name
puts bs.two_handed?
Broad Sword
true
class Cat
SUPERSTARS = [Cat.new('John'), Cat.new('Alfred')]
attr_accessor :name
def initialize(name)
#name = name
end
end
I get an error
ArgumentError: wrong number of arguments(1 for 0)
because initialize is not again defined.
If I put the definition on the end:
class Cat
attr_accessor :name
def initialize(name)
#name = name
end
SUPERSTARS = [Cat.new('John'), Cat.new('Alfred')]
end
it works.
Is it possible to keep constant declaration on the top of the file?
I think, you can use this trick:
class Cat
def self.const_missing(name)
[Cat.new('John'), Cat.new('Alfred')] if name == :SUPERSTARS
end
attr_accessor :name
def initialize(name)
#name = name
end
end
Avdi Grimm in his free episode of rubytapas called "Barewords" recommends that you don't use constants directly. Instead, wrap them in a method. And when you have a method, you don't even need the constant (which in ruby is not really constant anyway).
So, you can do something like this:
class Cat
def self.superstars
#superstars ||= [Cat.new('John'), Cat.new('Alfred')]
end
attr_accessor :name
def initialize(name)
#name = name
end
end
The only difference is that you now call it Cat.superstars instead of Cat::SUPERSTARS. Your code now works and looks better too! I call it a win-win. :)
The problem is that class definitions are just code that gets executed when the definition is reached. It's like asking if you can access the value of a local variable that you have not yet defined.
A possible workaround is to delay the evaluation of the constant until the class body is executed. Note that it's not really worth it and it shows a working solution that shouldn't be used in practice:
# BasicObject for minimal amount of methods
class Retarded < BasicObject
def initialize(&value)
#value = value
end
# Make sure we remove as many methods as we can to
# make the proxy more transparent.
(instance_methods - %i(__id__ __send__ __binding__)).each do |name|
undef_method name
end
def method_missing(*args, &block)
# Get the actual value
value = #value.call
# Suppress warnings while we traverse constants
warn_level, $VERBOSE = $VERBOSE, nil
traversed_modules = []
constant_finder = -> (root) do
constant_name = root.constants.find do |constant|
# We undefed ==, so we need a different way to compare.
# Given that this class should be used for constant values in place,
# comparing object ids does the job.
root.const_get(constant).__id__ == __id__
end
if constant_name
# Just set the constant to the actual value
root.const_set(constant_name, value)
else
# Recursively search for the containing module of the constant
root.constants.each do |child_name|
child_constant = root.const_get(child_name)
if child_constant.is_a?(::Module) &&
!traversed_modules.include?(child_constant)
# Handle circular references
traversed_modules.push child_constant
constant_finder.(child_constant)
end
end
end
end
# ::Object is the root module where constants are contained
constant_finder.(::Object)
# Bring back warnings
$VERBOSE = warn_level
# We have already found the constant and set its value to whatever was
# passed in the constructor. However, this method_missing was called
# in an attempt to call a method on the value. We now should make that
# invocation.
value.public_send(*args, &block)
end
end
# I know, the example is mononic.
class EdwardKing
DEAD = Retarded.new { [new('I'), new('II'), new('III')] }
def initialize(number)
#number = number
end
def whoami
"Edward #{#number} of England"
end
end
p EdwardKing::DEAD
# [#<EdwardKing:0x007f90fc26fd10 #number="I">, #<EdwardKing:0x007f90fc26fc70 #number="II">, #<EdwardKing:0x007f90fc26fc20 #number="III">
p EdwardKing::DEAD.map(&:whoami)
# ["Edward I of England", "Edward II of England", "Edward III of England"]
Im having a bit of trouble testing some random behaviour in rspec. I have a method on a class that should change one of the classes instance variables if a randomly generated number equals 10. I cant find anyway to correctly test this with rspec.
Here is the code for the class
class Airport
DEFAULT_CAPACITY = 20
attr_reader :landed_planes, :capacity
attr_accessor :weather
def initialize(capacity=DEFAULT_CAPACITY,weather = "clear")
#landed_planes = []
#capacity = capacity
#weather = weather
end
def stormy
if rand(10) == 10 then #weather = "stormy" end
end
end
does anyone know of a way i could write test for the stormy method?
One option is start rspec with rspec --seed 123 this will ensure that your random numbers are always predictable. But this will effect all subsequent calls to rand, shuffle, sample and so on.
Another way is to change your class to inject the randnumber generator:
class Airport
DEFAULT_CAPACITY = 20
attr_reader :landed_planes, :capacity
attr_accessor :weather
def initialize(capacity=DEFAULT_CAPACITY,weather = "clear", randomizer = ->(n) { rand(n)})
#landed_planes = []
#capacity = capacity
#weather = weather
#randomizer = randomizer
end
def stormy
if #randomizer.call(10) == 10 then #weather = "stormy" end
end
end
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
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
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.
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.
Try this:
player = Player.new
armor = Armor.new('Mythril', 100)
player = player.equip(armor) #Initialise the armor object inside Player.
player.hit(10)