/class_wizard

a fantasy tale about simple ruby class interaction

Primary LanguageRuby

This is a article about how ruby classes and the methods within can interact to create something beautiful: a simple program. For greatest ease, I'd recommend using pry or irb to follow along.

The Wizard, the Cat, and the Dragon

a Strange Tale of Ruby Class Interaction

So you've coded up some clever ruby methods and you're feeling pretty slick. Well, it's time to incorporate those methods into something grander: a classic good vs. evil tale with a medieval fantasy backdrop.

The Human Class

Our tale begins with its protagonist: a hapless wandering human. So, let's give them a class:

class Human
  attr_reader :name, :intellect, :location
  attr_accessor :health

  def initialize(stats)
    @name = stats[:name]
    @health = stats[:health]
    @intellect = stats[:intellect]
    @location = 0
  end

  def run
    @location += 5
    puts "#{@name} wanders through the forest"
  end

  def speak(words)
    puts words
  end
end

Let's give this a good look-over. First, let's look at our initialize method. This method, called upon each new instantiation of this class, requires that a 'stats' hash be passed in. This hash will include key value pairs for our character's name, health, and intellect, which are then created as instance variables.

The attr_reader up top acts as a getter method for the instance variables that follow. attr_writer would act as a setter method, and attr_accessor would provide both getter and setter. Let's see this in action.

First, we create a hash with our character's stats. Every story begins with a hero. This one begins with Pixel, our lonely wandering wizardess.

  pixels_stats = {
    name: "Pixel",
    health: 10,
    intellect: 7
  }

  pixel = Human.new(pixels_stats)

If you've loaded your human.rb file correctly (type load 'human.rb' in pry; it should return true), the previous block of code should return something like this:

#<Human:0x007ffe48be8518 @health=10, @intellect=7, @location=0, @name="Pixel">

The Wizard Class

But wait! Pixel isn't just a human. Pixel has magic in her bones. Pixel is a wizard. Still, we want her to maintain the basic human abilities (methods), such as run and speak.

Within a new file, wizard.rb, we're setting up the Wizard class as a subclass of the Human superclass. The require_relative 'human' makes sure to reference the human.rb file, presumably in the same directory. The < operator in the class name denotes that wizard will be a subclass of human, thus containing its methods.

require_relative 'human'

class Wizard < Human
  def speak(words)
    super + " wisely"
  end
end

Pixel, being a human, can still do human things, such as 'run' and 'speak'. The Wizard class would "inherit" these methods. Some methods, however, despite maintaining the same name, could change. In this case, we're making Pixel sound smarter - she is a wizard, after all. I've made an adjustment to the Human class' speak method. First, via super, we call the method in the super class, printing the input words in the console. Our addition adds the " wisely" string.

[5] pry(main)> pixel.speak("is there an echo in here?")
=> "is there an echo in here?, says Pixel wisely"

Now, let's give Pixel some powers.

def read(book)
  "#{@name} reads #{book}"
end

def learn
  @intellect += 1
end

def cast_spell(enemy)
  puts "Pixel casts a spell!"
  enemy.health -= 1
end

Ah yeah. Now Pixel can read books; learn, increasing her intellect by 1 (remember, without a attr_writer or attr_accessor, attributes like this wouldn't be changeable without specific methods that do so, like this one); and cast a spell at an enemy. Note that we can now instantiate Pixel with the Wizard class, i.e. Wizard.new(pixels_stats).

The Prologue Class

Now that we've set up what's necessary for characters, it's time to begin our journey. We'll need to create a backdrop. What could be more fitting (and uninspired) than a Prologue Class?

class Prologue
  def initialize
    @characters = []
    puts "The adventure has begun.."
  end

  def introduce_character(character)
    @characters << character
    name = character.name

    puts "#{name} has entered the story!"
  end

  def journey_forth
    sentence_start = @characters.map { |character| character.name }.join(" and ")
    sentence_end = " begin their journey.."
    sentence_start + sentence_end
  end
end

So, what have we here? We initialize our Prologue class object with an empty array for our @characters instance variable. We have a method, introduce_character explicitly named for introducing a character, and another, journey_forth, for setting said characters on their journey. This is still a tale, after all. Code just makes it possible (and fun!). Combining the aforementioned methods, we have a perfect prologue.

The Cat Class

Also!, we already know our beloved Pixel, but we can't forget Pixel's beloved cat, Palindrome. Let's quickly code up a Cat class.

class Cat
  attr_reader :name

  def initialize(name)
    @name = name
    speak
  end

  def distract(enemy)
    powers = [
      "backflips",
      "sand-paper licks",
      "mind control lol wut"
    ]

    enemy.distracted = true
    puts "#{@name} distracts #{enemy.name} with #{powers.sample}"
  end

  def run
    @location += 5
    "#{@name} wanders through the forest"
  end

  def speak
    sounds = ["meow", "purr", "hiss"]
    puts sounds.sample
  end
end

After making sure to require_relative our wizard and cat files in our prologue file, we can truly begin. Let's use the introduce_character method to add our characters to the Prologue, and then let's journey_forth.

[6] pry(main)> Palindrome = Cat.new("Palindrome")
meow
=> #<Cat:0x007f9ef92d9b88 @name="Palindrome">
[7] pry(main)> Story = Prologue.new
Our adventure has begun..
=> #<Prologue:0x007f9ef92966d0 @characters=[]>
[8] pry(main)> Story.introduce_character(Pixel)
=> "Pixel has entered the story!"
[9] pry(main)> Story.introduce_character(Palindrome)
=> "Palindrome has entered the story!"
[10] pry(main)> Story.journey_forth
=> "Pixel and Palindrome begin their journey.."

Great! Awesome! Drama! Well, not really. Pixel and Palindrome are stuck in the prologue, walking and talking. But that's all they really can do. It's up to us, the programmer, the scripter, the creative-intellectual, to pen this fable.

[11] pry(main)> Pixel.speak("I could really go for some tacos")
=> "I could really go for some tacos, says Pixel wisely"
[12] pry(main)> Palindrome.speak
=> "meow"

The Dragon Class

I did say this was going to be a classic story of good vs. evil, of heroes versus villains. So, let's code up a villain: dragon.rb.

class Dragon
  attr_reader :name
  attr_accessor :distracted, :health

  def initialize(stats)
    @name = stats[:name]
    @health = stats[:health]
    @distracted = true

    "#{@name} lets out a smokey belch"
  end

  def breath_fire(enemy)
    unless @distracted
      puts "#{@name} breathes fire at #{enemy.name}!"
      enemy.health -= 3
    else
      puts "#{@name} is distracted!"
    end
  end
end

Thus have we have our dragon class. Not very refined; it breathes fire and it has name, health, and distracted instance variables. Note the attr_accessor :distracted, :health. This will allow us to check or change these attributes from outside of the class without getter or setter methods.

I've named the dragon Primus and given him 100 health. Chilling.

[1] pry(main)> load 'dragon.rb'
=> true
[2] pry(main)> Dragon.new({ name: "Primus", health: 100 })
Primus lets out a smokey belch
=> #<Dragon:0x007f856aab0a30 @distracted=true, @health=100, @name="Primus">

The Climactic_Battle_Scene Class

This story is in desperate need of some drama. The prologue has come to a stirring conclusion. The scene is set. Let the deadly encounter begin. Let's create a new class and file to hold it: climactic_battle_scene.

class Climactic_Battle_Scene
  def initialize(protagonist, protagonists_sidekick, antagonist)
    @protagonist = protagonist
    @cat = protagonists_sidekick
    @antagonist = antagonist
  end

  def first_encounter
    puts "#{@antagonist.name} flaps his mighty wings and descends before
          the frightened wayfarers.  '7, 11, 17, 3!', he putters, fluent
          only in primes.  But #{@protagonist.name} understands every syllable.
          #{@antagonist.name} is hungry.  And hungry dragons must eat."
  end

  def end_of_deadly_encounter
    @antagonist.health <= 0 or @protagonist.health <= 0
  end

  def satisfying_conclusion
    winner = @protagonist.health > @antagonist.health ? @protagonist : @antagonist
    puts "#{winner.name} emerges victorious! Sweet relief!"
  end


  def battle_sequence
    first_encounter

    until end_of_deadly_encounter
      # how will the battle unfold?
    end

    satisfying_conclusion
  end
end

Let's break this down. First - the not shown: we relative_require the necessary files. Then, we initialize our Climactic_Battle_Scene with the same stats hashes used previously for Pixel, Palindrome, and Primus:

[1] pry(main)> Palindrome = Cat.new("Palindrome")
meow
=> #<Cat:0x007f9ef92d9b88 @name="Palindrome">
[2] pry(main)> pixels_stats = {
[2] pry(main)*   name: "Pixel",
[2] pry(main)*   health: 10,
[2] pry(main)*   intellect: 7
[2] pry(main)* }
=> {:name=>"Pixel", :health=>10, :intellect=>7}
[3] pry(main)> primus_stats = {
[3] pry(main)*   name: "Primus",
[3] pry(main)*   health: 100
[3] pry(main)* }
=> {:name=>"Primus", :health=>100}
[4] pry(main)> load 'climactic_battle_scene.rb'
=> true
[5] pry(main)> Climactic_Battle_Scene.new(pixels_states, Palindrome, primus_stats)
=> #<Climactic_Battle_Scene:0x007fea032755e8
 @antagonist={:name=>"Primus", :health=>100},
 @cat=#<Cat:0x007f9f21086070 @name="Palindrome">,
 @protagonist={:name=>"Pixel", :health=>3, :intellect=>2}>

We have methods to depict a first_encounter, to determine an end_of_deadly_encounter, and to output a satisfying_conclusion. But our battle_sequence method is woefully lacking.

But, first, we have a more serious problem. Pixel, our wizard, has 10 health and a cast_spell ability that lowers the enemy's health by 1. Primus, the dragon, has 100 health, and can breathe_fire, removing 3 of his enemy's health. This does not bode well for Pixel or her beloved cat. But we're the authors of this tale, so let's thicken the plot.

The Ancient_Tome Module

There's a rhetoric device called 'deus ex machina'. It's when a desperately hopeless protagonist is saved by seeming divine intervention. It's happening right now.

"Pixel stumbles back, stunned. A dragon! A real, live, fire-breathing dragon! How could she ever defend herself? Palindrome, unmoved, sees something hidden beneath the scattered leaves on the forest floor. He pounces on it and paws away at the rubble. It's a book! A book of ancient spells and esoteric oddities! Pixel hears Palindrome's purr, picks up the tome, and reads the first page."

module Ancient_Tome
  def decrypt(enemy)
    puts "#{enemy.name} is decrypted! What!!"
    enemy.health -= 50
  end

  def cook_tree_bark_soup
    puts "yum"
    @health += 1
  end
end

Woah. A module? Awesome. Now that Pixel has read this Ancient_Tome, let's go back and include it in the Wizard class. By doing this, we 'include' the methods in the given module, making them callable within the containing Class.

require_relative 'ancient_tome'

class Wizard < Human
  include Ancient_Tome
  ...

And, by my pen (keyboard), Pixel can now decrypt her enemies, and cook a mean tree bark soup. With our new found skills, there is hope against the dragon Primus. Let's return to the battle_sequence method in the Climactic_Battle_Scene class.

def battle_sequence
  first_encounter

  until end_of_deadly_encounter
    @antagonist.breathe_fire(@protagonist)
    # oh no!
    @cat.distract(@antagonist)
    @antagonist.breathe_fire(@protagonist)
    # "Primus is distracted!"
    @protagonist.decrypt(@antagonist)
  end

  satisfying_conclusion
end

Before we step into battle, I've made a design decision to place all my require_relative statements into a single file, story_elements.rb, as to load them all at once in pry, rather than typing them in one by one.

require_relative 'human'
require_relative 'wizard'
require_relative 'cat'
require_relative 'dragon'
require_relative 'ancient_tome'
require_relative 'prologue'
require_relative 'climactic_battle_scene'
[1] pry(main)> load 'story_elements.rb'
=> true

Okay, enough of that. Let the climax begin!! In terminal, from start to finish, our story plays out:

My-MacBook-Pro:class_wizard jw$ pry
[1] pry(main)> load 'story_elements.rb'
=> true
[2] pry(main)> pixels_stats = {
[2] pry(main)*   name: "Pixel",
[2] pry(main)*   health: 10,
[2] pry(main)*   intellect: 7
[2] pry(main)* }
=> {:name=>"Pixel", :health=>10, :intellect=>7}
[3] pry(main)>
[4] pry(main)> primus_stats = {
[4] pry(main)*   name: "Primus",
[4] pry(main)*   health: 100
[4] pry(main)* }
=> {:name=>"Primus", :health=>100}
[5] pry(main)> Pixel = Wizard.new(pixels_stats)
=> #<Wizard:0x007f95e1850c68 @health=10, @intellect=7, @location=0, @name="Pixel">
[6] pry(main)> Palindrome = Cat.new("Palindrome")
hiss
=> #<Cat:0x007f95e2256758 @name="Palindrome">
[7] pry(main)> Primus = Dragon.new(primus_stats)
Primus lets out a smokey belch
=> #<Dragon:0x007f95e2299620 @distracted=true, @health=100, @name="Primus">
[8] pry(main)> Story = Climactic_Battle_Scene.new(Pixel, Palindrome, Primus)
=> #<Climactic_Battle_Scene:0x007f95e2229640
 @antagonist=#<Dragon:0x007f95e2299620 @distracted=true, @health=100, @name="Primus">,
 @cat=#<Cat:0x007f95e2256758 @name="Palindrome">,
 @protagonist=#<Wizard:0x007f95e1850c68 @health=10, @intellect=7, @location=0, @name="Pixel">>
[9] pry(main)> Story.battle_sequence
Primus flaps his mighty wings and descends before
    the frightened wayfarers.  '7, 11, 17, 3!', he putters, fluent
    only in primes.  But Pixel understands every syllable.
    Primus is hungry.  And hungry dragons must eat.
Primus is distracted!
Palindrome distracts Primus with sand-paper licks
Primus is distracted!
Primus is decrypted! What!!
Primus is distracted!
Palindrome distracts Primus with mind control lol wut
Primus is distracted!
Primus is decrypted! What!!
Pixel emerges victorious! Sweet relief!

Pixel has defeated Primus! The until loop in the battle_sequence method continued until the conditional statement from the end_of_deadly_encounter was met. Primus' health is 0. With the conclusion of this loop, the satisfying_conclusion method is called, Pixel is determined the winner, and Palindrome, finder of ancient tome, deserves at least some of the credit. The two will continue their journey, feasting on tree bark soup for many a moon.

This completes our long, strange coding journey into ruby class interactions. Please note that everything you've just read is not necessarily best practice, but rather was written for the purpose of demonstration and experimentation. Ruby is truly an elegant and even, at times, eloquent language that allows for clean, straightforward, english-like object-oriented programming. As the old trite maxim goes: the possibilities are truly limitless.

Anywho, I hope you've enjoyed the tale. Now go forth and code!