Still learning by doing - connecting rooms in a game

Hi,

I am facing another problem with my next "project".

I have classes for different rooms. In each one I defined a function
which basically performs everything in the room. The player moves
through the rooms.

Here are my problems:

1. Connecting the rooms
I can kick everything off in a separate file (the runner) where I create
the first room and then pass the following room as the argument. I also
create other rooms in this separate files. But in the second room
everything crashes, because any of the rooms that could follow are
basically unrecognized as the local variables.

The other option is to create the following room when needed, that is in
the existing room. However, that would pose the problem because the
newly created would again have limited existence to one room.

Of course there is also the issue of returning to a room which was
visited before.

I am doing this as a task in the book learning ruby the hard way by Zed.

Maybe I've made a mistake for trying to put everything in the room
classes and not just the room descriptions.

Anyway, I maintain the code here:
https://code.launchpad.net/~sebastjan-hribar/zombies/trunk

I would appreciate any pointers. I am stuck. I still have a lot of work
to do with various descriptions and particular activities in rooms, but
the main problem is this room connection.

regards
seba

···

--
Posted via http://www.ruby-forum.com/.

I am doing this as a task in the book learning ruby the hard way by Zed.

This book is considered harmful.

Maybe I've made a mistake for trying to put everything in the room
classes and not just the room descriptions.

Anyway, I maintain the code here:
https://code.launchpad.net/~sebastjan-hribar/zombies/trunk

I would appreciate any pointers. I am stuck. I still have a lot of work
to do with various descriptions and particular activities in rooms, but
the main problem is this room connection.

I wouldn't have written the code this way, but I don't know if you are following the book or not.

To start with I would have…

One Room class that takes a name and description and a visited attribute. Not a class for each room as you have done.

Create a Map structure that defines the room layout. Something like

[
  {:room => :kitchen, :next_rooms => {:north => :living_room, :south => :basement}},
  {:room => : living_room, :next_rooms => {:north => :bathroom, :south => :kitchen}},
  ...
]

Henry

···

On 21/09/2012, at 5:11 AM, "Sebastjan H." <lists@ruby-forum.com> wrote:

I have problems with pushing items to player's satchel.

In the room template I have

      item = gets.chomp
      player.satchel.push(item) # or player.satchel << item

and the item doesn't get pushed into the array.

I can puts the item itself or the existing satchel content I defined
manually, but the above doesn't work.

And there is no error message like everything would work.

seba

···

--
Posted via http://www.ruby-forum.com/.

Hi,

the item does get pushed into the array, but when you call
Player#satchel again, the variable @satchel is reset to ['test']. So the
previous array with the new item will be overwritten.

Check it:

player = Player.new 'testname'
player.satchel << 'testitem'
p player.instance_variable_get :@satchel
player.satchel # this resets @satchel to ['test']
p player.instance_variable_get :@satchel

So you have to change those methods. This isn't good style, anyway,
because a getter method is supposed to do nothing but get or calculate a
certain value. It shouldn't change the object (except maybe caching of a
calculated value). When you want to set default values, do it in the
initialize method.

···

--
Posted via http://www.ruby-forum.com/.

Henry Maddocks wrote in post #1076876:

I am doing this as a task in the book learning ruby the hard way by Zed.

This book is considered harmful.

Why exactly?

Maybe I've made a mistake for trying to put everything in the room
classes and not just the room descriptions.

Anyway, I maintain the code here:
https://code.launchpad.net/~sebastjan-hribar/zombies/trunk

I would appreciate any pointers. I am stuck. I still have a lot of work
to do with various descriptions and particular activities in rooms, but
the main problem is this room connection.

I wouldn't have written the code this way, but I don't know if you are
following the book or not.

The book suggests a class per room.

To start with I would have

One Room class that takes a name and description and a visited
attribute. Not a class for each room as you have done.

Create a Map structure that defines the room layout. Something like

[
  {:room => :kitchen, :next_rooms => {:north => :living_room,
:south => :basement}},
  {:room => : living_room, :next_rooms => {:north => :bathroom, :south
=> :kitchen}},
  ...
]

Thank you for the idea of mapping the rooms in this way. I'll try it.
regards,
seba

···

On 21/09/2012, at 5:11 AM, "Sebastjan H." <lists@ruby-forum.com> wrote:

Henry

--
Posted via http://www.ruby-forum.com/\.

To start with I would have…

One Room class that takes a name and description

Ack.

and a visited attribute.

Well, it depends what kind of attribute you are thinking of: if there
are more players then the attribute would at least have to be a
collection type. Conceptually visit is a relationship between room
and user - with potentially more properties (e.g. time of visit,
order).

Not a class for each room as you have done.

Ack.

Create a Map structure that defines the room layout. Something like

[
        {:room => :kitchen, :next_rooms => {:north => :living_room, :south => :basement}},
        {:room => : living_room, :next_rooms => {:north => :bathroom, :south => :kitchen}},
        ...
]

I'd rather make the rooms reference their neighbors, maybe like this:

class Room
  attr_accessor :name, :description

  def neighbors
    @neighbors || = {}
  end

  def connect(direction, other_room)
    neighbors[direction] = other_room
    other_room.neighbors[reverse_direction(direction)] = self
  end

private
  def reverse_direction(d)
    case d
    when :north then :south
    when :south then :north
  #...
    else
      raise ArgumentError, "Illegal direction: %p" % [d]
  end
end

Kind regards

robert

···

On Fri, Sep 21, 2012 at 12:02 AM, Henry Maddocks <hmaddocks@me.com> wrote:

--
remember.guy do |as, often| as.you_can - without end
http://blog.rubybestpractices.com/

Jan E. wrote in post #1078238:

Hi,

the item does get pushed into the array, but when you call
Player#satchel again, the variable @satchel is reset to ['test']. So the
previous array with the new item will be overwritten.

Check it:

player = Player.new 'testname'
player.satchel << 'testitem'
p player.instance_variable_get :@satchel
player.satchel # this resets @satchel to ['test']
p player.instance_variable_get :@satchel

So you have to change those methods. This isn't good style, anyway,
because a getter method is supposed to do nothing but get or calculate a
certain value. It shouldn't change the object (except maybe caching of a
calculated value). When you want to set default values, do it in the
initialize method.

I didn't want to set a default value, the "test" in the satchel was
just my primitive way of testing whether the room and the player
communicate. And yes, I see now that it gets reset.

How should I change these methods? I am not looking for the easy way
out by you just telling me;) but I am puzzled and stuck yet again. Why >> does

it get reset by just calling it?

I was dealing with instance variable modification in my last game where >> the

player hp got modified constantly. I can't see the difference. I

guess it is back to books...

seba

···

--
Posted via http://www.ruby-forum.com/\.

Henry Maddocks wrote in post #1076876:

I am doing this as a task in the book learning ruby the hard way by Zed.

This book is considered harmful.

Why exactly?

It teaches you to write Ruby as if it's Python and this...

The book suggests a class per room.

There isn't any reason to have a class per room because the only difference is the data, not the behaviour.

Henry

···

On 21/09/2012, at 5:08 PM, "Sebastjan H." <lists@ruby-forum.com> wrote:

On 21/09/2012, at 5:11 AM, "Sebastjan H." <lists@ruby-forum.com> wrote:

Thank you all for recommending the reading and all the suggestions and
advices. According to those I'll try to re-factor the game like so:

1. Have one class for Room and then have subclasses for specific rooms.
2. Make a map of the rooms in one of the forms suggested above.

I still have difficulties imagining hot to resolve the interaction
between certain objects.

For example, I wanted to have the kitchen and the dinning room play out
like so:

        a) room description
        b) have the player decide whether or not they take some stuff
        c) second description, prologue to fight
        d) fight with a zombie
        e) exit the room

If I have all this in the room.state() I can't reference the player
object since I get the undefined local variable or method for player.

So I assume I should put any decision making or fighting in the "runner"
file since the runner knows about all the objects and have the
room.state contain only the description?

If there is another way I can reference these objects? If I initialize
an object like player in the room itself it is again limited to the room
in question, right?

regards
seba

···

--
Posted via http://www.ruby-forum.com/.

Sebastjan H. wrote in post #1078242:

How should I change these methods? I am not looking for the easy way
out by you just telling me;) but I am puzzled and stuck yet again. Why
does it get reset by just calling it?

The method assigns a new array to @satchel. So each time you call it,
the instance variable @satchel is overwritten.

A solution (like I said) is to set the values in the initialize method.
This will also allow you to define hp, rifle etc. through attr_reader.

However, there may actually be situations where it makes sense to set
default values on the fly isntead of ahead of time (like when the
calculation is very expensive). In this case you'd do something like
this:

def my_getter
  @myvar ||= 123
end

The assignment only happens when @myvar isn't nil (or false). So you
avoid resetting the variable. Note that this doesn't work if nil or
false can be actual values. In this case you'd need to use the
"defined?" operator:

def my_getter
  return @myvar if defined? @myvar
  @myvar = 123
end

But in your case, simply use the initialize method. That's what it's
for.

···

--
Posted via http://www.ruby-forum.com/\.

Could you be so kind as to suggest another book? I mean there are many
and it's hard to pick the best one for a beginner.

seba

···

--
Posted via http://www.ruby-forum.com/.

Thank you all for recommending the reading and all the suggestions and
advices. According to those I'll try to re-factor the game like so:

1. Have one class for Room and then have subclasses for specific rooms.
2. Make a map of the rooms in one of the forms suggested above.

I still have difficulties imagining hot to resolve the interaction
between certain objects.

For example, I wanted to have the kitchen and the dinning room play out
like so:

        a) room description
        b) have the player decide whether or not they take some stuff
        c) second description, prologue to fight
        d) fight with a zombie
        e) exit the room

It looks that there is a common algorithm for handling all rooms, so
this algorithm should be implemented only once, maybe in a parent
class of all rooms. It can delegate to the actual implementation of a
room the things related to data: does this room have items to pick?,
the descriptions, the monster and the connections to other rooms. Then
you run this algorithm from a specific room against the player that
enters the room. An example:

class Room

def player_enter player
  puts first_description
  if has_items? #maybe you could check the return of items instead of
having this method
    puts "This is what you can find in the room: #{items}"
    # perform the logic for the player to choose or not to pick
  end
  puts second_description
  if has_monster?
    fight player, monster
  end

  puts "Choose a door:"
  connections.each {|c| print the connection }
  ..
end

And then a specific room just implements the required methods:

class DiningRoom < Room
  def first_description
    "a typical dining room"
  end
  def has_items?
    true
  end
  def items
    [:gun, :bullets]
  end
  def second_description
    "you hear a strange sound coming from the dark corner"
  end
  def has_monster?
    true
  end
  def monster
    Zombie.new :hp => 100, :power => 5 #or however you instantiate zombies
  end
end

BTW, this pattern is called Template Method:

If I have all this in the room.state() I can't reference the player
object since I get the undefined local variable or method for player.

I think the method that runs the algorithm could receive the player as
an argument.

So I assume I should put any decision making or fighting in the "runner"
file since the runner knows about all the objects and have the
room.state contain only the description?

Well, the algorithm to go through a room can very well be in other
object other than a parent of all rooms, but still the above applies:
implement in each room the specifics of the room, putting the
algorithm in a single place.

If there is another way I can reference these objects? If I initialize
an object like player in the room itself it is again limited to the room
in question, right?

I don't think the Room is the right place to initialize the player
object. The player object probably belongs to a Game object or such.
What you can do is pass the appropiate objects where they need to be
used.

Jesus.

···

On Mon, Sep 24, 2012 at 8:46 PM, Sebastjan H. <lists@ruby-forum.com> wrote:

Sebastjan H. wrote in post #1076909:

Could you be so kind as to suggest another book? I mean there are many
and it's hard to pick the best one for a beginner.

For a while now the learn the hard way is in the process of rewriting
and maybe it'll be differentiated from python version. I am using the
old version right now.

seba

I've just bought the pickaxe 1.9 and I'll start there.

regards,
seba

···

--
Posted via http://www.ruby-forum.com/\.

In games, and a lot of other types of program, there is usually a 'god' object or controller object that is tasked with 'running' the game.

This object initialises everything (builds the rooms from the map, initialises the player object) and then enters the game loop.

In the inner game loop the GameController receives input and then iterates through the game objects (rooms, player, etc) updating their state.

I true MVC style the rooms and the player don't need to know about each other. All the interaction is done inside the GameController object.

Henry

···

On 25/09/2012, at 8:05 AM, Jesús Gabriel y Galán wrote:

If there is another way I can reference these objects? If I initialize
an object like player in the room itself it is again limited to the room
in question, right?

I don't think the Room is the right place to initialize the player
object. The player object probably belongs to a Game object or such.
What you can do is pass the appropiate objects where they need to be
used.

Ok just a few points on the code that you did write.

Firstly as you have given both name() and state() methods you do not
need the line

attr_accessor :name, :state

It does nothing useful give that name() and state() are methods.
However if you removed the name() method then you could have

attr_reader :name

It's a personal choice. I would personally go for the method but then
is no right way, just don't do both.

Secondly, as I'm sure you will have noticed, each room has:

def initialize(name)
  @name = name
end

def name()
  @name
end

So what you need is

class Room
  def initialize(name)
    @name = name
  end

  def name()
    @name
  end
end

and then the class Bathroom would become

class Bathroom < Room
  def state()
    ... All the stuff for state
  end
end

That should help a little, I hope :slight_smile:

If you are going to learn ruby then you should have a copy of the PickAxe, but the issues I pointed out weren't specifically Ruby issues, more algorithm and object oriented design issues. Unfortunately OOD books tend to be a bit dry. The best way to become better at OOD is to look at good OO code and write lots of code yourself.

Henry

···

On 21/09/2012, at 10:01 PM, Sebastjan H. wrote:

I've just bought the pickaxe 1.9 and I'll start there.

I've try to follow the above guidelines. I've read about the inheritance
and the templates as well. I'm going to use the templates and the
controlling object would be my runner.

So far I made the super class Room and modified the subclass
ChildBedroom.
It plays out ok and the room returns the value of the next room to be
explored. I still haven't figured out how exactly I am going to do that
in the runner, but I haven't played with that much yet.

I haven't learned about the testing yet, so for now to test it, I am
just running the ChildBedroom.rb where I've put the childreen_room
instantiation at the end.

Can anyone have a look if I am on the right track with the relationships
between the two classes?

http://bazaar.launchpad.net/~sebastjan-hribar/zombies/trunk/revision/8?start_revid=8

regards
seba

···

--
Posted via http://www.ruby-forum.com/.

I don't want to disagree with Henry, but there are a couple of resources for OO design that are quite useful. The first is the "Design PAtterns in Ruby" book. It keeps the design patterns pretty short with concrete implementations, so it's a lot better than the classic gang of four book.

Second is the current Clean Coders video series by Bob Martin. Each episode is about 12 bucks, and they're really pretty awesome. They kind of build on each other, though, so you can end up spending a fair bit.

James

···

On Sep 21, 2012, at 6:09 PM 9/21/12, Henry Maddocks <hmaddocks@me.com> wrote:

On 21/09/2012, at 10:01 PM, Sebastjan H. wrote:

I've just bought the pickaxe 1.9 and I'll start there.

If you are going to learn ruby then you should have a copy of the PickAxe, but the issues I pointed out weren't specifically Ruby issues, more algorithm and object oriented design issues. Unfortunately OOD books tend to be a bit dry. The best way to become better at OOD is to look at good OO code and write lots of code yourself.

Henry

Sebastjan H. wrote in post #1077994:

I've try to follow the above guidelines. I've read about the inheritance
and the templates as well. I'm going to use the templates and the
controlling object would be my runner.

So far I made the super class Room and modified the subclass
ChildBedroom.
It plays out ok and the room returns the value of the next room to be
explored. I still haven't figured out how exactly I am going to do that
in the runner, but I haven't played with that much yet.

I haven't learned about the testing yet, so for now to test it, I am
just running the ChildBedroom.rb where I've put the childreen_room
instantiation at the end.

Can anyone have a look if I am on the right track with the relationships
between the two classes?

http://bazaar.launchpad.net/~sebastjan-hribar/zombies/trunk/revision/8?start_revid=8

I've also just added the amended runner.rb. I used the same logic as in
another learning game from the book learn the hard way. Now I get the
conversion error, because what is returned by the room.state can't be
passed into the loop again dut to type error.

···

--
Posted via http://www.ruby-forum.com/\.

Peter Hickman wrote in post #1076973:

Ok just a few points on the code that you did write.

Firstly as you have given both name() and state() methods you do not
need the line

attr_accessor :name, :state

I tried this and I removed the attr_accessor line and had the methods
defined, but then the code doesn't work:

class Zombie

def initialize(name, hp, paces, target)
@name = name
@hp = hp
@paces = paces
@target = target
end

def name()
@name
end

def hp()
@hp
end

def paces()
@paces
end

def target()
@target
end

end

However, the other way around works: having the attr_accessor line and
the initialize method in place w/o specific methods:

class Zombie
attr_accessor :name, :hp, :paces, :target

def initialize(name, hp, paces, target)
@name = name
@hp = hp
@paces = paces
@target = target
end

end

What am I doing wrong now?

···

--
Posted via http://www.ruby-forum.com/\.