Make Your First Text Adventure in Ruby

Game State

In previous chapters we discussed objects and the messages they send and receive. It would be unfair to continue without highlighting another key feature of objects. Along with methods, an object can have attributes.

Think of yourself. You are not an object, but you do have attributes. You probably have an eye colour, you may own 0 or more pairs of shoes. You might even have a preferred animal. These are your attributes. If you were to describe yourself in a class, you might write the following:

class Me
  attr_reader :eye_colour
  attr_reader :pairs_of_shoes
  attr_reader :preferred_animal
end

To put it another way, an attribute is something an object knows about itself. The sum of an object's attributes is its state. Thus, the state of the game is the sum of everything that every object knows. We saw an example of this in the last chapter. The Game object had the attribute @steps_taken.

It is a common anti-pattern in game development to have much or all of your state and management of game flow in one class. It will often be called something like "GameManager". This is a tempting habit to fall in to because at first, all will be clear to you and your game will be easy to enhance. In the long term, this is the road to ruin. As your game grows, the meaning of the file becomes more obscure, and change more costly and damaging.

Antipattern: A common approach to a problem that appears fitting but generally causes more problems than it solves.

Now, it is probable that you will have a process that knows a lot about a lot. It is still good to avoid this where possible. Let us look at an example.

Our game from before has a new twist. In the updated version, the developers have added the ability for the player to lose hit points.

class Game
  MAX_PLAYER_HITPOINTS = 100

  def initialize
    @player_hit_points     = MAX_PLAYER_HITPOINTS
    # ...
  end

  def hurt_player(amount)
    @player_hit_points -= amount
    kill_player if @player_hit_points < 1
  end

  def kill_player
    puts "Game over!"
    exit
  end

  def start_game
    # ...
  end
end

Game.new

Our wanderer can now be overcome by a wide range of threats. Since our game must know about our player, it seems reasonable that the Game class should be responsible for managing our player. However, our player is quite capable of managing themselves.

class Game
  def initialize
    @player = Player.new
    # ...
  end

  def start_game
    # ...
  end
end

def Player
  MAX_PLAYER_HITPOINTS = 100

  def initialize
    @hit_points = MAX_PLAYER_HITPOINTS
  end

  def hurt(amount)
    @hit_points -= amount
    kill if @hit_points < 1
  end

  def kill
    # ...
  end
end

Game.new

We now have a player class that is largely responsible for managing its own state. The game class knows that the player exists and what it does, but how it does it is not the game class' concern. Trust between objects is a critical part of good object oriented design. By designing objects that can trust eachother to carry out their end of the bargain, we can develop a system that is easy to change, extend and maintain.

At all stages it is vital to carefully consider the structure that you wish to use to store your game's state.