patternrubyMinor
Zork-style game engine (Specifically, a Cactus prototype)
Viewed 0 times
enginecactuszorkspecificallystylegameprototype
Problem
So I'm one of those weirdos over here talking about making a game engine for text-based (think Zork) games. There was pretty recently another question by someone else working on the same thing, if you're interested.
Anyway, this is a prototype of the code for that engine, but instead of reading for a file (and being written in Python) it has all the data hardcoded (and is written in Ruby).
Note: Because it's all going to be dynamically generated anyway, I'm not looking for advice on DRYing up either
What I'm specifically looking for here is:
Other suggestions are welcome as well, but the three points above are the focus of the question.
Since this is just a prototype, I'm not concerned with speed or memory efficiency, though any tips that can be implemented without sacrificing understandability are welcome.
```
LOCATIONS = {
# NOTE: These properties _must_ be defined for the game to work properly:
# connections array of Symbol
# allow_entrance lambda
# on_enter lambda
# on_exit lambda
# on_stay lambda
# on_block lambda
# Normally we'd get all this from the game files, but here it's hardcoded because I'm not writing the entire
# gosh-darned engine right now >:(
center:
{
connections: [:left, :right],
allow_entrance: lambda {|_| true},
on_enter: lambda {|_| puts 'Welcome to the center.'},
Anyway, this is a prototype of the code for that engine, but instead of reading for a file (and being written in Python) it has all the data hardcoded (and is written in Ruby).
Note: Because it's all going to be dynamically generated anyway, I'm not looking for advice on DRYing up either
LOCATIONS or GAME_PROPS. What I'm specifically looking for here is:
- Readability tips -- I have a ton of comments, but I dunno if they explain enough.
- Ways to make the code more compact and readable, though not at the expense of readability.
- Features to implement -- nothing major, like "read and parse the data from a file", but small things to make it easier, like "let the user put in a string to be printed instead of requiring boilerplate code".
Other suggestions are welcome as well, but the three points above are the focus of the question.
Since this is just a prototype, I'm not concerned with speed or memory efficiency, though any tips that can be implemented without sacrificing understandability are welcome.
```
LOCATIONS = {
# NOTE: These properties _must_ be defined for the game to work properly:
# connections array of Symbol
# allow_entrance lambda
# on_enter lambda
# on_exit lambda
# on_stay lambda
# on_block lambda
# Normally we'd get all this from the game files, but here it's hardcoded because I'm not writing the entire
# gosh-darned engine right now >:(
center:
{
connections: [:left, :right],
allow_entrance: lambda {|_| true},
on_enter: lambda {|_| puts 'Welcome to the center.'},
Solution
I largely ignored the comments on the first read to see how clear the code is without them, and other than a couple small naming things the code is fairly clear. Having the same hash represent the game data and game state is an interesting approach, but it seems it'll work well enough for this style of game. The only organizational thing I would suggest is breaking up the GAME_PROPS hash into the parts that are meant to be configurable, changed by gameplay, or defined by the game maker, since you're likely to wind up getting them from different things anyway.
For naming concerns, naming the areas 'left', 'right', and 'center' seems odd. Usually in games like Zork you say something like 'go north'. It prevents the user from having to care about the unique names of each room. Giving them ids or arbitrary names illustrates the problem a bit--what we really want is for the rooms to have an id, and in the connections list pair each connection with a keyword to access that connection. For example:
And then this code:
would become
You can also assign the location has to a local variable to reduce access depth, and if you do decide to have at least the locations be proper objects (you can always have them delegate [] to the hash they're constructed with!), you can provide any of the magic methods (meta-programming) to have the room check if it has a room at the requested connection:
Update:
We can make it easier by swapping the key/value pairs in connections, so that a room description looks like this:
Then, when checking a users input for room changing, we get the easier code:
For naming concerns, naming the areas 'left', 'right', and 'center' seems odd. Usually in games like Zork you say something like 'go north'. It prevents the user from having to care about the unique names of each room. Giving them ids or arbitrary names illustrates the problem a bit--what we really want is for the rooms to have an id, and in the connections list pair each connection with a keyword to access that connection. For example:
R1:
{
connections: {R2: :left, R3: :right],
allow_entrance: lambda {|_| true},
on_enter: lambda {|_| puts 'Welcome to the center.'},
on_exit: lambda {|_| puts 'You are leaving the center.'},
on_stay: lambda {|_| puts 'Please enter a command.'},
on_block: lambda {|_| puts 'You are not allowed into this room.'},
},
...And then this code:
if current[:connections].include? new_loc
# If new_loc will let us in
if LOCATIONS[new_loc][:allow_entrance].call(GAME_PROPS)
...would become
room_id = current[:connections].select{|rid, connection| connection == new_loc}.keys.first
if room_id
if LOCATIONS[room_id][:allow_entrance].call(GAME_PROPS)
...You can also assign the location has to a local variable to reduce access depth, and if you do decide to have at least the locations be proper objects (you can always have them delegate [] to the hash they're constructed with!), you can provide any of the magic methods (meta-programming) to have the room check if it has a room at the requested connection:
if room.send(new_loc)
...Update:
We can make it easier by swapping the key/value pairs in connections, so that a room description looks like this:
R1:
{
connections: {left: :R2, right: :R3],
allow_entrance: lambda {|_| true},
...
}Then, when checking a users input for room changing, we get the easier code:
if current[:connections][new_loc.to_sym]
room_id = current[:connections][new_loc.to_sym] # because we shouldn't assign stuff in if checks... :)
if LOCATIONS[room_id][:allow_entrance].call(GAME_PROPS)
...Code Snippets
R1:
{
connections: {R2: :left, R3: :right],
allow_entrance: lambda {|_| true},
on_enter: lambda {|_| puts 'Welcome to the center.'},
on_exit: lambda {|_| puts 'You are leaving the center.'},
on_stay: lambda {|_| puts 'Please enter a command.'},
on_block: lambda {|_| puts 'You are not allowed into this room.'},
},
...if current[:connections].include? new_loc
# If new_loc will let us in
if LOCATIONS[new_loc][:allow_entrance].call(GAME_PROPS)
...room_id = current[:connections].select{|rid, connection| connection == new_loc}.keys.first
if room_id
if LOCATIONS[room_id][:allow_entrance].call(GAME_PROPS)
...if room.send(new_loc)
...R1:
{
connections: {left: :R2, right: :R3],
allow_entrance: lambda {|_| true},
...
}Context
StackExchange Code Review Q#92528, answer score: 6
Revisions (0)
No revisions yet.