Leslie V. wrote:
parser = Parser(Player.new(World.new(Map.new, Things.new)))
What I want to know is: what’s the best way? I had globals $player,
$world, $map
earlier and that makes things a bit easier but it also makes me nervous
(perhaps just because I have been anti-global-indoctrinated for 20 years
now)
I’m still a bit vague on Modules. Any ideas?
Les
Modules are typically used not for organizing instances, but for
grouping methods and constants, so that chuncks of code can be reused in
different situations.
Why not simply replace the globals with a Game class and an instance
that has attributes for player, world, map, etc.?
If your program is complex enough, you may find a technique called
“dependency injection” useful. It’s kind of like the Game class
suggestion, with a little extra syntactic sugar. DI is a way of
extracting the connection logic (which you are now doing by “passing
objets around when initializing”) from the rest of the object creation
code. With DI, you define containers for your instances, and you can
decide whether a particular kind of instance should be unique in the
container, or there should be one instance for each key (such as a
location name).
There are several DI frameworks for ruby (see RAA). Here’s an example
using my own MinDI library:
require ‘mindi’
Parser = Struct.new :game # add parser state vars here
Player = Struct.new :game, :location, :contents
World = Struct.new :game # add state vars (:time, maybe)
Map = Struct.new :game
The :game attr is useful if you want to write methods of
these classes (Player for example) that refer to the other
game objects. But it’s not strictly necessary.
Room = Struct.new :name, :contents
Sometimes, a struct is not enough…
class Thing
attr_accessor :name
attr_reader :location
def initialize name, location = nil
@name = name
@location = location
end
Moving a Thing updates the Room it’s in or Player who has it
def location=(loc)
@location.contents.delete self if @location
@location = loc
@location.contents << self if @location
end
end
class GameContainer
extend MinDI::Container
These are some singletons–one instance each.
parser { Parser.new self }
player { Player.new self, start_room, [] }
world { World.new self }
map { Map.new self }
A multiton.
The |name| means there can be many things–one for each name.
internally, there is a hash stored in @thing that maps
each name strign to a Thing.
thing { |name| Thing.new name }
The shovel (unique with that name).
shovel { thing “Shovel” }
room { |name| Room.new name, [] }
start_room { room “garden” }
Set up some initial conditions
def initialize
# create and locate the shovel
shovel.location = start_room
end
end
game = GameContainer.new
The shovel is already defined:
p game.shovel
puts
Create a new thing:
ball = game.thing(“ball”)
ball.location = game.room “basement”
p ball
puts
player = game.player
pick up the shovel and anything else in the start room
This could be made into a #pick_up method of Game or of Player
player.location.contents.each { |thing| thing.location = player }
p player.contents.map {|thing| thing.name}
puts
move the player
p player.location.name
player.location = game.room “basement”
p player.location.name
puts
get the ball
player.location.contents.each { |thing| thing.location = player }
p player.contents.map {|thing| thing.name}
puts
You can define new methods on the GameContainer that access the
internal
storage used by the “service” methods.
class GameContainer
def rooms
@room.values
# because internally the “room” service uses a hash stored in
# the @room instance var
end
def things
@thing.values
end
end
Show all the rooms
p game.rooms
Show all the things
p game.things.map {|thing| thing.name}
END
Output:
#<Thing:0xb7d862fc @name=“Shovel”, @location=#<struct Room
name=“garden”, contents=[#<Thing:0xb7d862fc …>]>>
#<Thing:0xb7d85078 @name=“ball”, @location=#<struct Room
name=“basement”, contents=[#<Thing:0xb7d85078 …>]>>
[“Shovel”]
“garden”
“basement”
[“Shovel”, “ball”]
[#<struct Room name=“garden”, contents=[]>, #<struct Room
name=“basement”, contents=[]>]
[“ball”, “Shovel”]