i've went through about 15 similar questions and haven't yet found one that answered mine properly, so i thought it might warrant a new thread.
I'm making a ruby terminal app for a game, and I'm trying to give the user the option to delete a sword of his choice based on their text input. This is what i have so far.
class Weapon
attr_reader :weapon_name, :grip, :guard, :blade
attr_accessor :strength, :speed, :defence, :ascii_image
SWORDS = []
def initialize(weapon_name, grip, guard, blade)
#weapon_name = weapon_name
#strength = 0
#speed = 0
#defence = 0
self.grip = grip
self.guard = guard
self.blade = blade
self.ascii_image = ascii_image
SWORDS << self
end
...
Weapon.new("Epic Sword", "straight", "forward", "narrow")
def self.delete
puts "Which weapon would you like to delete?"
delete_target = gets.chomp.downcase.strip
SWORDS.delete_if{#weapon_name == delete_target}
puts SWORDS
end
end
The above method for some reason seems to delete every sword in the array and ONLY IF delete_target is = to the last item in the array. (Otherwise it seems to do nothing) What am i doing wrong here? How can i target a specific sword by its #weapon_name?
You seem confused about what each object is. Let's recap:
epic_sword = Weapon.new("Epic Sword", "straight", "forward", "narrow")
The epic_sword object is an instance of Weapon.
As a side-effect of initialising this, you also updated Weapon::SWORDS to contain this object.
puts "Which weapon would you like to delete?"
delete_target = gets.chomp.downcase.strip
delete_target is a String, e.g "epic sword". It is only the (lower-case) name of the sword, not the weapon object itself.
SWORDS.delete_if{#weapon_name == delete_target}
This line doesn't make sense. For one thing, #weapon_name is undefined; you're inside a class method, not within a Weapon instance. And as stated above, the delete_target is only a String, so won't be equal to the weapon object.
To fix this, you can do:
SWORDS.delete_if{ |sword| sword.weapon_name.downcase == delete_target}
Related
this is my first post and I'm quite new to programming/this site, so I apologise in advance if I'm doing something wrong/annoying.
I wanted to find a way to define objects without having to do so for each object. I came up with this
class Number
def initialize(name)
#name = name
end
def description
puts "I'm #{#name} "
end
end
a = ["zero", "one","two", "three", "four"]
for i in (0..5) do
a[i] = Number.new(a[i])
end
a[3].description
I'm hoping someone can tell me what kind of Frankensteins monster I've created?
It seems to work, a[3].description returns "I'm three" but does that mean three/a[3] exists as its own object and not an element of an array?
Furthermore if I try to do:
puts a[3]
I get:
<Context::Number:0x000000009b7fd0 #name="three">, #
To clarify I just want to know whether I have actually managed to create objects here, and why on earth when I try and access elements of my array I get that weird feedback (kind of seems like its accessing memory or something, but that is a little beyond me)
My thanks in advance for anyone who replies to this.
All objects stand on their own, regardless of whether they are contained by/in other objects such as Array instances.
Regarding this:
<Context::Number:0x000000009b7fd0 #name="three">, #
...did you mean you get that when you puts a[3] and not puts a?
Every instance of Object and its subclasses has a to_s method that returns a string representation of the object. Since you did not override that in your Number class, it used the default implementation defined in class Object. It is showing you:
1) the class name (I presume you defined Number in side a class or module named Context)
2) the object id (a unique id in the Ruby runtime)
3) the string representation of its instance variable(s)
Also, regarding this:
a = ["zero", "one","two", "three", "four"]
This is equivalent and easier to type (I use 2 spaces for better readability):
%w(zero one two three four)
Also, as Ilya pointed out, map will simplify your code. I'll go a little further and recommend this to do the array initialization:
a = %w(zero one two three four).map { |s| Number.new(s) }
Yes, you have created objects. It's just how Ruby represents a class as a string.
class MyClass
attr_accessor :one, :two
def initialize(one, two)
#one, #two = one, two
end
end
my_class = MyClass.new(1, 2)
my_class.to_s # #<MyClass:0x007fcacb8c7c68>
my_class.inspect # #<MyClass:0x007fcacb8c7c68 #one=1, #two=2>
I am trying to write fast and concise code. I'd appreciate your thoughts on which is the best way to write the following code and why:
Option #1
def get_title
title = check_in_place_one
if title.empty?
title = check_in_place_two
if title.empty?
title = check_in_place_three
end
end
return title
end
Option #2
def get_title
title = check_in_place_one
title = check_in_place_two unless !title.empty?
title = check_in_place_three unless !title.empty?
return title
end
I think Option #1 is better since if the title is found by check_in_place_one, we test title.empty? once and then skip the rest of the code in the method and return. But, it looks too long. Option #2 appears better, but processes title.empty? one extra time, and unnecessary time before returning. Also, am I missing a third option?
From performance, there is no difference between the two versions of your code (besides very minor difference that may come from parsing, which should be ignorable). The control structures are the same.
From readability, if you can get away with nesting, doing so is better. Your second option is better.
It is usually better to get rid of any case that does not need further processing. That is done by return.
def get_title
title = check_in_place_one
return title unless title.empty?
title = check_in_place_two
return title unless title.empty?
title = check_in_place_three
return title
end
The last title = and return in the code above are redundant, but I put them there for consistency, which improves readability.
You can further compact the code using tap like this:
def get_title
check_in_place_one.tap{|s| return s unless s.empty?}
check_in_place_two.tap{|s| return s unless s.empty?}
check_in_place_three
end
tap is a pretty much fast method, and unlike instance_eval, its performance penalty is usually ignorable.
The following approach could be used for any number of sequential tests. Moreover, it is completely general. The return condition could be changed, arguments could easily be assigned to the test methods, etc.
tests = %w[check_in_place_one check_in_place_two check_in_place_three]
def do_tests(tests)
title = nil # Define title outside block
tests.each do |t|
title = send(t)
break unless title.empty?
end
title
end
Let's try it:
def check_in_place_one
puts "check 1"
[]
end
def check_in_place_two
puts "check 2"
''
end
def check_in_place_three
puts "check 3"
[3]
end
do_tests(tests) #=> [3]
check 1
check 2
check 3
#=> [3]
Now change one of the tests:
def check_in_place_two
puts "check 2"
'cat'
end
do_tests(tests) #=> 'cat'
check 1
check 2
#=> "cat"
If there were more tests, it might be convenient to put them in a module which would be included into a class. Mixed-in methods behave the same as those that you define for the class. For example, they have access to instance variables. I will demonstrate that with the definition of the first test method. We probably want to make the test methods private. We could do it like this:
module TestMethods
private
def check_in_place_one
puts "#pet => #{#pet}"
puts "check 1"
[]
end
def check_in_place_two
puts "check 2"
''
end
def check_in_place_three
puts "check 3"
[3]
end
end
class MyClass
##tests = TestMethods.private_instance_methods(false)
puts "##tests = #{##tests}"
def initialize
#pet = 'dog'
end
def do_tests
title = nil # Define title outside block
##tests.each do |t|
title = send(t)
break unless title.empty?
end
title
end
include TestMethods
end
The following is displayed when the code is parsed:
##tests = [:check_in_place_one, :check_in_place_two, :check_in_place_three]
Now we perform the tests:
MyClass.new.do_tests #=> [3]
#pet => dog
check 1
check 2
check 3
Confirm the test methods are private:
MyClass.new.check_in_place_one
#=> private method 'check_in_place_one' called for...'
The advantage of using a module is that you can add, delete, rearrange and rename the test methods without making any changes to the class.
Well, here's a few other alternatives.
Option 1: Return first non-empty check.
def get_title
return check_in_place_one unless check_in_place_one.empty?
return check_in_place_two unless check_in_place_two.empty?
return check_in_place_three
end
Option 2: Helper method with short-circuit evaluation.
def get_title
check_place("one") || check_place("two") || check_place("three")
end
private
def check_place(place)
result = send("check_in_place_#{place}")
result.empty? ? nil : result
end
Option 3: Check all places then find the first that's non-empty.
def get_title
[
check_in_place_one,
check_in_place_two,
check_in_place_three,
].find{|x| !x.empty? }
end
Option 2 looks good although you did a 360 degree turn with the unless !title.empty?. You can shorten that to if title.empty? since unless is equivalent to an if ! so doing an unless ! takes you back to just if.
If you're only ever going to have 3 places to look in then option 2 is the best. It's short, concise, and easy to read (easier once you fix the aforementioned whoopsie). If you might add on to the places you look for a title in you can get a bit more dynamic:
def get_title(num_places = 4)
title, cur_place = nil, 0
title = check_in_place(cur_place += 1) while title.nil? && cur_place < num_places
end
def check_in_place(place_num)
# some code to check in the place # given by place_num
end
The tricky line is that one with the while in it. What's happening is that the while will check the expression title.nil? && cur_place < num_places and return true because the title is still nil and 0 is less than 4.
Then we'll call the check_in_place method and pass in a value of 1 because the cur_place += 1 expression will increment its value to 1 and then return it, giving it to the method (assuming we want to start checking in place 1, of course).
This will repeat until either check_in_place returns a non nil value, or we run out of places to check.
Now the get_title method is shorter and will automatically support checking in num_places places given that your check_in_place method can also look in more places.
One more thing, you might like to give https://codereview.stackexchange.com/ a look, this question seems like it'd be a good fit for it.
I don't think there's any reason to get too clever:
def get_title
check_in_place_one || check_in_place_two || check_in_place_three
end
Edit: if the check_in_place_X methods are indeed returning an empty string on failure it would be better (and more idiomatic) to have them instead return nil. Not only does it allow for truthy comparisons like the above code, return "" results in the construction of a new and unnecessary String object.
I've put together two sample classes implemented in a couple of different ways which pretty well mirrors what I want to do in my Rails model. My concern is that I don't know what, if any are the concerns of using either method. And I've only found posts which explain how to implement them or a general warning to avoid/ be careful when using them. What I have not found is a clear explanation of how to accomplish this safely, and what I'm being careful of or why I should avoid this pattern.
class X
attr_accessor :yn_sc, :um_sc
def initialize
#yn_sc = 0
#um_sc = 0
end
types = %w(yn um)
types.each do |t|
define_method("#{t}_add") do |val|
val = ActiveRecord::Base.send(:sanitize_sql_array, ["%s", val])
eval("##{t}_sc += #{val}")
end
end
end
class X
attr_accessor :yn_sc, :um_sc
def initialize
#yn_sc = 0
#um_sc = 0
end
types = %w(yn um)
types.each do |t|
# eval <<-EVAL also works
self.class_eval <<-EVAL
def #{t}_add(val)
##{t}_sc += val
end
EVAL
end
end
x = X.new
x.yn_add(1) #=> x.yn_sc == 1 for both
Well, your code looks realy safe. But imagine a code based on user input. It might be look something like
puts 'Give me an order, sir!'
order = gets.chomp
eval(order)
What will happen if our captain will go wild and order us to 'rm -rf ~/'? Sad things for sure!
So take a little lesson. eval is not safe because it evaluates every string it receives.
But there's another reason not to use eval. Sometimes it evaluates slower than alternatives. Look here if interested.
Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 8 years ago.
Improve this question
What I do need:
I pass a string that has to set an unmutable object inside an array, but I do not know hot how to make the transition from the string that the user inputs to the object name I need.
What I am intending to do:
I am working on a conversational adventure. The key point is to have a function that creates a command prompt so the user can interact with the game. Whenever the user says "go to somewhere", there is another function called "goto" that compares whether the input is included in the exits of the place where the player is; if so, the attribute "place" for the player takes a new place.
What I did:
I made a command prompt that actually works*
loop do
print "\n >>> "
input = gets.chomp
sentence = input.split
case
when sentence[0] == "inspect"
$action.inspect(sentence[1])
when sentence[0] == "go" && sentence[1] == "to"
$action.goto(sentence[2])
when sentence[0] == "quit"
break
else
puts "\nNo le entiendo Senor..."
end
And I initialized the objects as I need them (the third attribute goes for the exits):
room = Place.new("room", "Room", [newroom], "This is a blank room. You can _inspect_ the -clock- or go to [newroom].", ["this this"])
newroom = Place.new("newroom", "New Room", [room], "This is another blank room. You can _inspect_ the -clock-", ["this this"])
Then I made a method inside the action controller that has to compare and set the places properly. (Beware: monster newbie code following. Protect you eyes).
def goto(destiny) #trying to translate the commands into variable names
if (send "#{destiny}").is_in? $player.place.exits
$player.place = send "#{sentence[2]}"
puts destiny.description
else
puts "I cannot go there."
end
end
I think you want to convert a string to constant. Well it is easy. Read an example:
string = 'Hash'
const = Object.const_get(string) #=> Hash
const.new #=> {}; <- it is an empty Hash!
But be careful. If there's no such a constant you will get uninitialized constant error. In this case your adventures will stop.
I hope I understood your question and you will understand my answer.
How to change string to object, there are few options:
Bad(eval family):
eval("name_of_your_variable = #{21+21}")
eval("puts name_of_your_variable") #42
You can see that eval can make everything. So use with caution.
However, as pointed by #user2422869 you need(be in) scope - place where your variables are saved. So above code won't run everywhere
Everytime you run following method you create another scope
def meth1
puts "defined: #{(defined? local_a) ? 'yes' : 'no'}!"
eval 'local_a = 42'
local_a += 100
eval 'puts local_a'
end
meth1
and here is output:
defined: no!
142
If you want to grab local_a from one of scopes of meth1 you need binding.
def meth2
var_a = 222
binding
end
bin = meth2
bin.eval 'var_a'
#output:
#222
About binding you can read in doc. As for scopes, I don't have good site.
Better:
hash_variable = Hash.new # or just {}
hash[your_string_goes_here] = "some value #{42}"
puts hash[your_string_goes_here]
I don't know if good or bad:
As for this: send "#{destiny}". I assume that your destiny doesn't exist, so you can use method_missing:
def method_missing arg, *args, &block
#do some with "destiny"; save into variable/hash, check if "destiny" is in right place etc.
# return something
end
I run an irc bot, written in ruby, running the cinch irc framework. The bot replies with interesting facts and cycles through these facts so you won't get bored of them. I have set a cool down, so they can't be shown for 6 hours. Instead of showing the facts it first showed, it now shows randomly selected ones, which could be the ones that have been shown earlier.
line = IO.readlines("facts.txt")
factnumber = rand(line.length)
if fact_not_next_6_hours[factnumber] == true
factnumber = rand(line.length)
m.reply "fact #{factnumber}: #{line[factnumber]}"
fact_not_next_6_hours[factnumber] = true
fact_not_next_6_hours[factnumber] is the variable for the 6 hour cool down; if it's set to true, cool down is active. I need to do:
factnumber = rand(line.length)
until it gets one that dosen't have the 6 hour cool down set to true, and then do
m.reply "fact #{factnumber}: #{line[factnumber]}"
fact_not_next_6_hours[factnumber] = true
My first idea was to do multiple ifs, but it didn't work and I'm sure there is a better way.
You can do:
factnumber = rand(line.length)
while fact_not_next_6_hours[factnumber] == true
factnumber = rand(line.length)
end
m.reply "fact #{factnumber}: #{line[factnumber]}"
fact_not_next_6_hours[factnumber] = true
Or:
nil while fact_not_next_6_hours[factnumber = rand(line.length)] == true
m.reply "fact #{factnumber}: #{line[factnumber]}"
fact_not_next_6_hours[factnumber] = true
You should really rename line to lines, as it is in fact an array of lines. I've done so in my answer.
This is essentially a "do while" loop:
begin
factnumber = rand(lines.length)
end while fact_not_next_6_hours[factnumber]
But depending on how many facts you have and how many you expect to be "used", filtering out the ones you can't use first may make more sense:
fact = (0...lines.length).zip(lines).reject do |k, v|
fact_not_next_6_hours[k]
end.sample
m.reply "fact #{fact[0]}: #{fact[1]}"
The first bit of that ((0...lines.length).zip(lines)) is just associating each of the lines with a number (e.g. [[0, "fact"], [1, "afact"], ...]). I recommend running each part of the method chain individually so you can fully understand what's happening.
First of all, if you just set a boolean flag, how will you know when to "unfreeze" it? I'd keep the "timestamp" of last accessed time in the object. Also, instead of using primitive types all around, I'd do it in a bit more object oriented fashion.
Here's my solution:
class Fact
attr_reader :text
def initialize(text)
#text = text
#last_accessed = Time.new(0) # a long time ago, not in cooldown
end
def in_cooldown?
Time.now - #last_accessed < 60*60*6
end
def cooldown!
#last_accessed = Time.now
end
end
class Facts
attr_reader :all
def initialize(file_name)
#all = IO.readlines("facts.txt").map{|line| Fact.new(line)}
end
end
class FactRandomizer
def initialize(facts)
#facts = facts
end
def get
fact = not_in_cooldown.sample || all.sample # all can be in cooldown
fact.cooldown!
fact.text
end
private
def not_in_cooldown
#facts.select{|fact| !fact.in_cooldown?}
end
end
Usage:
facts = Facts.new("whatever").all
randomizer = FactRandomizer.new(facts)
randomizer.get
EDIT:
I refactored the code, so that it does not use class methods anymore. Note, how much easier it would be to test this code now and how easy it is to interchange parts of it (like for example replacing the part that reads the facts from file or what does it mean for a fact to be in cooldown).