Newbie question: Defining a numeric type

On 2009-11-17, Marnen Laibow-Koser [email protected] wrote:

The problem is, I sort of want to be able to do things like:
john.str.add_modifier(“drunk”, -3)

Because really, the fact that str is a stat is part of the intended
published interface.

Then your clients will know that it’s a Stat object, and expect to call
to_i on it.

Except that it’s extremely annoying to have an object where, out of five
hundred uses, four hundred and ninety six need an extra “.to_i”.

I don’t see anything wrong with that; you can overload Stat

  • Fixnum and so on as we discussed earlier in this thread.

Yeah.

But if you really want automatic, you can have it:
class Stat
def to_int
self.to_i
end
end

(According to to_i Vs to_int | Ruby Fleebie , to_int is
called automatically as necessary and will make your class act as an
Integer. to_i, of course, will not do that.)

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. I also end up having to define a bunch of additional
operators;
for instance, if I want to be able to write “john.str + 3”, I have to
define

  • for Stat, even though “3 + john.str” would probably do the right
    thing.

Modifier is a class too, which can handle things like counting down its
duration, etcetera.

And a stat has a handful of them.

OK…these are your value object candidates, perhaps.

Hmm. Well, probably not – modifiers have internal state (duration,
which I
was thinking to indicate as a turn counter) which they want to update.
Maybe.
I could make them into values perhaps if I, say, calculated their
expiration
rather than their duration. Then, if you refresh a modifier, you make a
new
one with a later expiration. Hmm.

Ahh, but I don’t necessarily care whether it’s accurate, just whether I
have a record.

If it’s not accurate, then there’s no point keeping a record.

There can be; see below.

In either case, though, for this sort of functionality you don’t
necessarily need a full-fledged history.

Right. I pretty much meant a mini-history like that.

Right. It’s now clear that your stat object is too complex to be a
simple immutable value object, although some of its components might
well be.

I’m sort of liking the idea of making modifiers into immutable values.
It
would actually make life simpler, I think.

I don’t think delegation will work here – for one thing, you may not
need to store the total value in any actual instance variable. Using
to_int or possibly coerce will work much better.

Delegation empirically does work, although it may not be the most
efficient
or best choice of how to do things. Stashing the total value in an
instance
variable may be useful anyway; these things get looked up pretty often.

-s

Seebs wrote:

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)

On 2009-11-17, Brian C. [email protected] wrote:

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

Seebs wrote:

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.

  1. 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 :slight_smile:

Regards,

Brian.

Brian C. wrote:

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

Brian C. wrote:

Brian C. wrote:

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.

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,

Marnen Laibow-Koser
http://www.marnen.org
[email protected]

Marnen Laibow-Koser wrote:

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.

On 2009-11-17, Brian C. [email protected] wrote:

  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.

  1. 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

Seebs wrote:

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.

On 2009-11-17, Brian C. [email protected] wrote:

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

Seebs wrote:

On 2009-11-17, Brian C. [email protected] wrote:

  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.

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.

On 2009-11-17, Aldric G. [email protected] wrote:

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