I've been tooling around with Ruby by converting a pen and paper RPG to a script.
Right now I have a character's stats kept in a hash, which I would like to be able to set via public method. I got that working with:
class Character
attr_reader :str, :con, :dex, :wis, :int, :level, :mods, :stats
def initialize str, con, dex, wis, int, cha, level = 1
@stats = { :str => str, :con => con, :dex => dex, :wis => wis, :int => int, :cha => cha }
@mods = {}
@level = level
@stats.each_pair do |key, value|
@mods[key] = ((value / 2 ) -5).floor
end
end
def []=(index, value)
@stats[index] = value
end
end
This allows me to instantiate a new character, then update @stats
by running newChar.stats[:str] = 12
However, I also seem to be able to modify the @mods
using this method as well, which is undesirable. newChar.mods[:str] = 15
will successfully alter the @mods
hash, which from my understanding should not be possible with the current setter method.
On a slightly separate note, the iterator I'm using to create my @mods
hash seems clunky but I haven't found anything better to accomplish the task.
You did not even call your []=
method in your example. This would be done like so:
newChar[:str] = 123
instead of
newChar.stats[:str] = 123
so to call newChar.stats[:str] = 123
you do not even need you method definition. The reason is that newChar.stats
as well as newChar.mods
will both return the actual hash which can then be altered.
One possible workaround is to freeze the @mods
variable so it can't be altered any more:
def initialize str, con, dex, wis, int, cha, level = 1
# omitted ...
@stats.each_pair do |key, value|
@mods[key] = ((value / 2 ) -5).floor
end
@mods.freeze
end
This is a good solution if you never want to be able to change @mods
again. Trying to set a value will result in an error:
newChar.mods[:con] = 123
# RuntimeError: can't modify frozen Hash
Inside your class, you can, however, overwrite @mods
entirely.
To summarize, the full class would be:
class Character
attr_reader :str, :con, :dex, :wis, :int, :level, :mods, :stats
def initialize str, con, dex, wis, int, cha, level = 1
@stats = { :str => str, :con => con, :dex => dex, :wis => wis, :int => int, :cha => cha }
@mods = {}
@level = level
@stats.each_pair do |key, value|
@mods[key] = ((value / 2 ) -5).floor
end
@mods.freeze
end
end
If you need a public getter for a hash but you don't want the user to modify the hash –the instance-variable of your class–, you can do it with dup.
class MyClass
....
def my_hash
@my_hash.dup
end
end
Where the solution with freeze
, as mentioned above, will freeze the hash even for your class, the .dup-solution will let you modify the hash from within your class but not from outside.