Dueling Defenders: Making your own Superhero Battle CLI Tutorial
One of the best ways to practice Ruby when you’re beginning to find your footing in coding, is diving in and creating a CLI application! In this tutorial, I will be guiding the average user through the process of building a CLI application that fetches from an API. The idea behind this application is that a user can play a game where you can pick two heroes, battle them, and the program will judge if one hero would win, lose, or if the two heroes would eventually just tie.
I searched for API’s that would give me not only facts about superheroes, but stats, attributes of their strength and other common characteristics that might matter in a fighting arena. I found what I needed in Superhero API, an API which provides power-stats of superheroes including attributes like strength, combat, intelligence and other stats that might be relevant in a battle. To follow along in this tutorial, you should go to this API and get your own API key before starting. At the time of publishing this article, it was free to get a key so you should be able to attain one without payment.
DEMO
Here is a demo of the completed application you will be building in this tutorial.
SETTING UP YOUR APPLICATION
The first step to creating this application, is going to your command line and writing:
bundle gem Dueling_Defenders
This will stub out a lot of the basic files for a Ruby Gem. You can then cd into that file folder, and we’re in business in creating a Superhero CLI. Let’s start with the gemfile. The basic gems you will need to add to your Gemfile for the functionality that will be used in this application are pry (for debugging as you go), colorize, uri, net/http, json, and dotenv for your API key. Except for colorize which will be used for styling, the other gems will be used for interacting with our external API.
PLANNING THE USER INTERACTIONS
In creating a gem, I typically start with the entry point where the user interacts. When the user first runs the gem, there should be a main menu that shows options for gameplay: Here is a rough outline of what I planned from the beginning.
The main menu should then have six options.
- Search for Information on a hero
- Change your User hero
- Learn about User hero
- Battle a different hero
- How many battles has each hero won/lost?
- Exit
ADDING YOUR API_KEY
By now you should have an API_Key and the Dotenv gem installed. To add your API_KEY that we will be using throughout this project, you should create a .env file in the root directory. Then, add your API_KEY in a pair like this:
API_KEY=whateveryourkeyis
Also be sure to make sure that your gemfile loads Dotenv as Dotenv.load or require ‘dotenv/load’
The best way to make sure your gemfile is loaded every time you start the program is to make sure your setup file is calling bundle install. If you used the bundle gem command, the default file should have this command. Mine is as follows:
#!/usr/bin/env bashset -euo pipefailIFS=$’\n\t’set -vxbundle install
BUILDING THE ENTRY POINT
In the bin folder, create a new file to be your executable. Name it after the gem, “Dueling_Defenders” The file should not be a Ruby file, so use a SheBang line to make sure the file is interpreted correctly.
#!/usr/bin/env rubyrequire “bundler/setup”require “Dueling_Defenders”CLI.new.run
CLI.new.run is the line that hints we are going to build an instance of a CLI class, and we’re going to use a method from the CLI class “run” to start our program.
BUILDING THE PROJECT
In the lib folder, make a folder called “Dueling_Defenders” (required in the last file mentioned, so we have access to these files). In this folder, create a file called CLI.rb
We already know the first method we want is the run method, so let’s stub out that method first.
class CLI def run system(“clear”) @user_input = nil @userhero = nil @enemyhero = nil welcome until @user_input == “6” main_menu endend
This method is first going to clear away the last use of the terminal. We are going to set some instance variables, we’ll initialize the user_input, user_hero, and enemy hero all as nil. The user_input will be what we collect from the user throughout the use of this application. The user_hero will contain the current hero you are playing as, and the enemyhero will hold the hero you will want to battle.
It then calls the welcome method.
def welcome puts “Welcome to the Superhero Battleground”.colorize(:blue) puts “ ________________ //., — — — — — — ,\\\\ // .=^^^^^^^^^^\__|\\\\ \\\\ ` — — — — . .// \\\\ — …….._ `;// \\\\.-,______;.// \\\\ — -.. — // \\\\ // \\\\// “.colorize(:color => :red, :background => :blue)end
This method is primarily aesthetic, it puts out a welcome method that is being styled by the colorize gem, and some ASCII art.
The next line in the run method stipulates that this method will run until user input is 6, which will be assigned to the exit command. It then calls the main_menu method.
def main_menu puts “\n” puts “What would you like to do?” puts “1. Search for information on a Hero” puts “2. Change Your User Hero” puts “3. Learn about User Hero” puts “4. Battle a Different Hero” puts “5. How many battles has each hero won/lost?” puts “6. Exit” @user_input = gets.chomp case @user_input when “1” search_for_hero when “2” make_hero_user when “3” learn_about_user when “4” battle when “5” battlecount when “6” goodbye exit else puts “Invalid input”.colorize(:red) endend
The main_menu method is going to print out the six options we had planned in our main menu. We will use gets.chomp to get our user input and assign it to our instance variable. Each of the numbers leads to the a different method, so we will go over them one by one.
6 is the most basic, if you choose 6, the goodbye method is called.
def goodbye puts “\n” puts “Goodbye, go save the world!”.colorize(:blue) puts “\n”end
It shows an exit method, and because of the until statement the app will stop running because @user_input is now 6.
def search_for_hero print “What hero would you like to search for? “ heroinput = gets.chomp hero = Hero.find_or_create_by_name(heroinput) noheroexists?(hero) || hero.printnicelyend
The search_for_hero method will then prompt the user to enter a hero they want to search for. Then using gets.chomp we will get the user input. This is saved to a local variable, and that local variable is fed into a class method for the Hero class. The Hero class we will add to as we go, but we are going to start with some attributes from the API we are going to know we want to read later. @@all is class variable that will hold each instance of the Hero class. To return this class variable, we will create a class method of self.all.
class Hero attr_reader :name, :alteregos, :aliases, :fullname, :intelligence, :strength, :speed, :durability, :power, :combat, :battleswon, :battleslost, :attributes @@all = [] def self.all @@all end
def self.find_by_name(name) self.all.detect { |hero| hero.name.downcase == name.downcase } end
def self.create(name) APIService.new.get_hero_by_name(name) end
def self.find_or_create_by_name(name) find_by_name(name) || create(name) end
end
The first method called is the find_or_create_by_name method that is fed a term from where it is called in the CLI instance. Find by name is called first, and it checks all Heroes to see if there is a Hero already existing by the same name. If that hero exists, that instance of the Hero class is returned to the CLI class and assigned to hero. If it does not exist, the ternary operator will then create a hero by that name. We then go to the class method create of the hero class. This creates a new instance of the APIService class, and calls get_hero_by_name on that instance.
class APIService key = ENV[‘API_KEY’] BASE_URI = “https://www.superheroapi.com/api.php/#{key}/search/"
def get_hero_by_name(name) @namestring =name.gsub(“ “, “_”) uri = URI(BASE_URI + “#{@namestring}”) heroes = make_request(uri) heroresults = heroes[“results”] if heroresults!= nil lookforexactmatchinresults(heroresults) @hero1 else “No hero like that exists” end end
def make_request(uri) response = Net::HTTP.get_response(uri) JSON.parse(response.body) end
def lookforexactmatchinresults(arrayname) foundhero = arrayname.detect {|result| result[“name”].to_s.downcase == @namestring.downcase} if foundhero!= nil @hero1 = Hero.new(foundhero) else if Hero.find_by_name(arrayname[0][“name”]) !=nil @hero1 = Hero.find_by_name(arrayname[0][“name”]) else @hero1 = Hero.new(arrayname[0]) end end endend
Using the environment variable we created with dotenv, BASE_URI is constructed appended with our search term name and using the URI gem so you can use NET:HTTP to get by URI. That response is then parsed using JSON. It is then returned to the get_hero_by_name method that will either send a message saying nothing matched that search term in the API, or it will lookforexactmatch in results. This method will either select the exact result that matches the search term, or if no exact result exists, it returns the first one returned in results. For example, if you typed in Superman, it would search for exactly the Superman entry, not Quantum Superman or something else. If you typed in Spider, it would return the first result that had a partial match such as Scarlet Spider. This method then checks to see if the partial match exists in the already created Heroes. If it does, that hero is returned. If not or if an exact match is found, the it creates a new Hero with the data found in the API.
class Heroattr_reader :name, :alteregos, :aliases, :fullname, :intelligence, :strength, :speed, :durability, :power, :combat, :battleswon, :battleslost, :attributes@@all = [] def initialize(hero_data) @name = hero_data["name"] @fullname = hero_data["biography"]["full-name"] @alteregos = hero_data["biography"]["alter-egos"] @aliases = hero_data["biography"]["aliases"] @intelligence = hero_data["powerstats"]["intelligence"] @strength = hero_data["powerstats"]["strength"] @speed = hero_data["powerstats"]["speed"] @durability = hero_data["powerstats"]["durability"] @power = hero_data["powerstats"]["power"] @combat = hero_data["powerstats"]["combat"] @battleswon = 0 @battleslost = 0
@attributes.push(@intelligence, @strength, @speed, @durability, @power, @combat) @@all << self end def printnicely puts "\n" puts @name.colorize(:blue) puts "\n" puts "Real Name:" puts @fullname puts "\n" puts "Alter-Egos:" puts @alteregos puts "\n" puts "Aliases:" puts @aliases puts "\n" puts "Stats:" puts "Intelligence: #{@intelligence}" puts "Strength: #{@strength}" puts "Speed: #{@speed}" puts "Durability: #{@durability}" puts "Power: #{@power}" puts "Combat #{@combat}" puts "\n" endend
Using the data from the API, we initialize a new Hero class instance and save some instance variables from the data to use later. We make an array of attributes that are relevant to combat we will use later in battle. We also add the instance to the class variable @@all. Also, we make a method to print out the information using the instance variables in an aesthetically pleasing way that we can use later.
Using the Hero instance just created, we continue in our CLI class.
def make_hero_user print “What hero would you like to become?” herotobecome = gets.chomp hero = Hero.find_or_create_by_name(herotobecome) noheroexists?(hero) || (@userhero = hero puts “\n” puts “Your hero is now #{hero.name}”.colorize(:blue))end
def noheroexists?(hero) if hero == "No hero like that exists" puts "\n" puts hero.colorize(:red) return true else return false endend
If the hero is just the message that it doesn’t exist, we return that message and colorize it. If not, we return the message that the user hero has been changed and assign the instance variable @user_hero to the Hero class instance.
Going back to the main_menu method, if the user inputs 3 — we are going to call the method learn_about_user.
def learn_about_user nouserhero? || @userhero.printnicelyend
def nouserhero? if @userhero == nil puts "\n" puts "You need to pick a hero first, change user hero".colorize(:red) return true else return false endend
Learn about user first checks if the instance variable @userhero is nil or not, and if it is — it goes no further it prints out a message you need to pick a hero. If @userhero exists, then you are going to call the instance method from the Hero class printnicely to output some well formatted information about the userhero.
Choice number “4” brings us to the method battle. Battle first uses the method we made earlier, and makes sure you have a user hero to use to battle. If you do, it makes an enemy user from asking for user input for who they would like to battle. The process for making an enemy user is very similar to how we created a Hero instance for user earlier, only it is assigned to @enemyhero. If the user hero and enemy hero is again, it prints a message to tell you and calls itself again.
def battle nouserhero? || make_enemy_user if @enemyhero!=nil sureaboutbattle endend
def make_enemy_user @enemyhero = nil puts "\n" print "What hero would you like to battle?" enemyinput = gets.chomp hero = Hero.find_or_create_by_name(enemyinput) noheroexists?(hero) || (@enemyhero = hero if @enemyhero.name.downcase == @userhero.name.downcase puts "\n" puts "You can't fight yourself.".colorize(:red) make_enemy_user else puts "\n" puts "You have chosen #{@enemyhero.name} to fight.".colorize(:red) end)end
If we have an enemy hero and user hero, we’re ready to battle. It then calls the sureaboutbattle method.
def sureaboutbattle puts “\n” puts “Are you ready to battle #{@enemyhero.name}?” puts “Yes” puts “No” sureaboutbattleinput = gets.chomp input = sureaboutbattleinput.downcase case input when “yes” testusers when “no” puts “\n” puts “Coward.”.colorize(:red) else puts “I didn’t understand that.”.colorize(:red) sureaboutbattle endend
This method basically is just a fun little method that makes sure the user is sure about fighting, and calls them a coward if not. It’s not totally necessary, but a callback to some vintage video games I thought would be fun. If the user is sure they want to fight, testusers is called.
def testusers puts “\n” puts “The heroes are fighting….” sleep(2) puts “\n” @enemyheropoints = 0 @userheropoints = 0 test_attributes if @enemyheropoints > @userheropoints puts “#{@enemyhero.name} has defeated you!”.colorize(:red) @userhero.loseabattle @enemyhero.winabattle elsif @userheropoints > @enemyheropoints puts “#{@userhero.name} has won the battle! Hooray!”.colorize(:green) @userhero.winabattle @enemyhero.loseabattle elsif @userheropoints == @enemyheropoints puts “Our heroes are evenly matched…maybe they should fight together, not each other…”.colorize(:blue) endend
def test_attributes @userhero.attributes.each_index do |i| if @enemyhero.attributes[i].to_i > @userhero.attributes[i].to_i; @enemyheropoints += 1; elsif @enemyhero.attributes[i].to_i < @userhero.attributes[i].to_i; @userheropoints += 1 end endend
Testusers shows a message and then puts a sleep delay to build a little anticipation. It initializes points for each the user hero and the enemy hero at 0. Test attributes is then called, and test_attributes iterates through the attributes of each and compares them to each other. If Superman has a higher strength than Batman, he gets a point and so on. Then, depending on who has the higher points, a message is put out. Also, the Hero instance methods winabattle and loseabattle are called to record wins and losses in that Hero instance. Those methods are in the Hero class as follows:
def winabattle @battleswon += 1end
def loseabattle @battleslost += 1end
Main menu option 5 is the last option, and it calls the method battlecount.
def battlecount if Hero.all == [] puts “\n” puts “No heroes in battleground yet. Search for heroes to add them to battleground, or become one.”.colorize(:red) else Hero.all.each do |hero| puts “\n” puts “#{hero.name}:”.colorize(:blue) puts “Has won #{hero.battleswon} battle(s).” puts “Has lost #{hero.battleslost} battle(s).” end endend
This method iterates through every Hero instance and prints out how many battles each Hero has won and lost. If no heroes are in the battleground, it prints out a message telling you such.
We have now gone through every method called from our game, and effectively built a Superhero Battle CLI! If you want to see more of the code, check out my repository. Collaboration instructions are also included on the repository page. Also if you enjoyed this tutorial, I will be using this concept as the basis for creating a more fleshed out application using Rails that will use this idea and turn it into a deployable version that has a more styled user interfaced. Keep an eye out for that upcoming tutorial!