Newbie question: Defining a numeric type

B

Brian Candler

Seebs said:
This turns out not to quite be the case, in experiments. If I do that,
it works most of the time, but as an example:
john.str + john.dex + john.con
doesn't work, because it can't figure out that ANY of them should be
integers.

ISTM that you are over-complicating. str, dex and con are individual
attributes of the character and can be just Fixnums.

If you want the whole combined set of attributes to act as an integer
with a single value then that's straightforward to arrange.

class Stats
attr_accessor :str, :dex, :con
def initialize(str, dex, con)
@str, @dex, @con = str, dex, con
end
def to_int
str + dex + con
end
def to_s
to_int.to_s
end
def method_missing(*args)
to_int.send(*args)
end
end

ogre = Stats.new(16,3,2)
elf = Stats.new(5,15,3)
puts ogre < elf # => true
puts elf - ogre # => 2
puts "Argh!" if ogre + rand(6) < elf # => sometimes

This is the solution I posted before - what's the problem with it?

You said you wanted to remember things like the highest str and be able
to restore it. So just include that state too.

class Stats
attr_accessor :str, :dex, :con
def initialize(str, dex, con)
@str, @dex, @con = str, dex, con
@max_str, @max_dex, @max_con = str, dex, con
end

def str=(x)
@str=x
@max_str=x if x > @max_str
end

def restore_strength
@str = @max_str
end

def to_int
str + dex + con
end

def to_s
to_int.to_s
end

def method_missing(*args)
to_int.send(*args)
end
end

ogre = Stats.new(16,3,2)
puts ogre # => 21
ogre.str = 4
puts ogre # => 9
ogre.restore_strength
puts ogre # => 21

Sure, there's some duplication involved if you repeat this for
individual stats. Is that a problem? Use a bit of metaprogramming to
save the typing.

Or, your Stats object could include a Hash with the individual
attributes, which would be extensible.

ogre.get:)str) # or ogre[:str]
ogre.set:)str, 12) # or ogre[:str] = 12
ogre.max:)str)
ogre.restore:)str)
 
S

Seebs

ISTM that you are over-complicating. str, dex and con are individual
attributes of the character and can be just Fixnums.

Except they can't, because a character doesn't just have a current
str, but also a internal base str, a series of possible modifiers,
some of which have their own internal state, a history of the highest
base str (which may not be the same as the current base str), a current
total value (which could be calculated by examining the others)...
This is the solution I posted before - what's the problem with it?

Look at the stuff about modifiers.

john.wisdom.modify("drunk", "-3")

For this to work, "wisdom" needs to know both the unmodified value and
its current set of modifiers, in order to figure out its current total.
class Stats
attr_accessor :str, :dex, :con
def initialize(str, dex, con)
@str, @dex, @con = str, dex, con
@max_str, @max_dex, @max_con = str, dex, con
end

If I'm going to have a bunch of things (9ish, I think) all of which
have the same semantics (a stored maximum value, etcetera), it seems
to me that they are sort of like a set of objects with similar
characteristics... Which is to say, a class.

I'm pretty sure I really do want to have these things have non-trivial
internal state.

-s
 
B

Brian Candler

Seebs said:
I'm pretty sure I really do want to have these things have non-trivial
internal state.

That's fine - make each individual attribute be an object. But you also
want these stats to appear to be readable and writable as if they were
vanilla integers. I think I'd do this by hiding this behavior in the
parent class, the Character which owns the Stats.

class Stat
attr_reader :val
def initialize(val=0, max_val=val)
@val, @max_val = val, max_val
end
def val=(x)
@val = x
@max_val = x if x > @max_val
end
def restore
@val = @max_val
end
def to_i
@val
end
end

class Character
attr_reader :name
def initialize(name)
@name = name
end

def str_stat
@str_stat ||= Stat.new
end

def str
str_stat.val
end

def str=(v)
str_stat.val = v.to_i
end

def dex; 0; end # placeholder
def con; 0; end # placeholder

def level
str + dex + con
end
end

c = Character.new("Ogre")
c.str = 14 # => 14
puts c.level
c.str = 8 # => 8
puts c.level
c.str_stat.restore
puts c.level # => 14

The points I'm trying to make here are:

1. Whilst you want the individual stats to behave as integers, you
probably don't want the entire Character to behave as an integer - the
Character is likely to have many other attributes, such as 'name' in the
above example. So, whenever you deal with individual stats, you are
always going to do c.str or c.str=val. This is a convenient place to
masquerade the Stat.

2. When you set the strength of a character, you don't want to replace
the entire Stat object, you want to update its state so it can retain
history. So when you write

c.str = x

then really you don't want to replace the strength Stat object inside c;
you are sending a message to it to update its state. If you use the code
I've shown above, then it avoids you having to write

c.str.val = x

which is what you'd have to do if c.str returned the Stat object itself.

3. When dealing with the Character, I think you will infrequently want
to deal with the underlying Stat object directly, but I have added an
accessor (str_stat) to allow this if you need it. With sufficient proxy
methods you could hide the underlying Stat objects entirely: e.g.

class Character
def restore_strength
@str_stat.restore
end
end

You can still make the character objects be Comparable, if normally you
want to order them by level.

class Character
include Comparable
def <=>(other)
level <=> other.level
end
end

And if you really want to, you can define to_int and method_missing so
that the entire Character resolves to its level value. But I think this
is more likely to be confusing rather than helpful. If you ever want to
do arithmetic on levels, I think it would be clearer to see "ogre.level
- elf.level" rather than "ogre - elf", because ogres have more
attributes than just their level.

In other words, ogres are like onions :)

Regards,

Brian.
 
B

Brian Candler

Brian said:
You can still make the character objects be Comparable, if normally you
want to order them by level.

... and in this case it probably makes sense to have a to_i method.

class Character
def to_i
level
end

include Comparable
def <=>(other)
to_i <=> other.to_i
end
end

This allows you to say not only:

if ogre > elf

but also:

if ogre > 12

which I imagine would be useful. If you want "if 12 < ogre" as well,
then:

class Character
def coerce(other)
[other, to_i]
end
end
 
M

Marnen Laibow-Koser

Brian said:
... and in this case it probably makes sense to have a to_i method.

class Character
def to_i
level
end

include Comparable
def <=>(other)
to_i <=> other.to_i
end
end

This allows you to say not only:

if ogre > elf

but also:

if ogre > 12

which I imagine would be useful.

I don't know about you, but I'd find this confusing. A character is not
its level, and I think
if ogre < 12
is actually harder to understand than
if ogre.level < 12


Best,
 
A

Aldric Giacomoni

Marnen said:
I don't know about you, but I'd find this confusing. A character is not
its level, and I think
if ogre < 12
is actually harder to understand than
if ogre.level < 12

oh, good - and here I thought I was the only one for whom the former
comparison made no sense.
 
S

Seebs

1. Whilst you want the individual stats to behave as integers, you
probably don't want the entire Character to behave as an integer - the
Character is likely to have many other attributes, such as 'name' in the
above example. So, whenever you deal with individual stats, you are
always going to do c.str or c.str=val. This is a convenient place to
masquerade the Stat.

Actually, yes.
2. When you set the strength of a character, you don't want to replace
the entire Stat object, you want to update its state so it can retain
history. So when you write
c.str = x
then really you don't want to replace the strength Stat object inside c;
you are sending a message to it to update its state. If you use the code
I've shown above, then it avoids you having to write
c.str.val = x
which is what you'd have to do if c.str returned the Stat object itself.

Actually, not exactly.

The thing is, when you write "c.str = x", you aren't sending a message to
c.str -- you're sending "str=" to c.

So I had handled this by wrapping "str=" differently, but having "str" just
yield the stat object. Which meant I had to do extra work to make it just
look like an integer the rest of the time, admittedly.
And if you really want to, you can define to_int and method_missing so
that the entire Character resolves to its level value. But I think this
is more likely to be confusing rather than helpful. If you ever want to
do arithmetic on levels, I think it would be clearer to see "ogre.level
- elf.level" rather than "ogre - elf", because ogres have more
attributes than just their level.

Oh, I would have no intent ever of trying to make characters comparable.
If I needed to sort a list of critters, that'd be time for a sort_by.

-s
 
B

Brian Candler

Seebs said:
The thing is, when you write "c.str = x", you aren't sending a message
to
c.str -- you're sending "str=" to c.

This is true. So for symmetry, you could make str=(v) set the [current]
value in the Stat object, and str return the current value from the Stat
object. Anyway, it's just a thought.
 
A

Aldric Giacomoni

Seebs said:
Actually, yes.

Why not take a step back and have "stat" be a hash of all the stats?

@stat = { :strength => { :base_value => 15, :mods => { :race => 2 },
:current_value => 17 }

This way you "need" Alucard.stat[:strength].current_value, but you can
fix that with custom methods / method_missing to become
Alucard.strength.

This way if you need to compare Alucard.stat[:strength] and
Maria.stat[:strength], you can do a custom comparison for whatever
sub-stat you want -- and you can write a single comparison method for
all your stats.

I'm sort of showing up after the war, here, but I really fail to
understand WHAT we're arguing about if not a design decision.
 
S

Seebs

This is true. So for symmetry, you could make str=(v) set the [current]
value in the Stat object, and str return the current value from the Stat
object. Anyway, it's just a thought.

I've sort of waffled on that.

I could certainly do something like:

john.str (yields the fixnum)
john.str= (sets things such that the fixnum acquires the new value)
john.str_stat (yields the Stat)

But so far I prefer just having Stat delegate stuff to its current value,
because then I can always say "john.str", and it responds to every message
the way I expect it to.

-s
 
S

Seebs

Why not take a step back and have "stat" be a hash of all the stats?
Interesting.

@stat = { :strength => { :base_value => 15, :mods => { :race => 2 },
:current_value => 17 }
This way you "need" Alucard.stat[:strength].current_value, but you can
fix that with custom methods / method_missing to become
Alucard.strength.

Hmm. I sorta like that.
I'm sort of showing up after the war, here, but I really fail to
understand WHAT we're arguing about if not a design decision.

I'd say "discussing". It's not "arguing" until people start accusing each
other of being Fortran programmers.

-s
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,755
Messages
2,569,536
Members
45,020
Latest member
GenesisGai

Latest Threads

Top