Building Quartermaster: My First CLI Application

As I made my way through the Object Oriented Ruby module in Learn, I knew that my first major project for the Flatiron School would be creeping up soon and I began to brainstorm ideas for applications that I could use in my every day life. I knew I wanted to use an API, as I found scraping a bit unwieldy and time consuming and I had worked with APIs in small amounts at my previous place of employment.

I was particularly excited when I discovered the Dungeons and Dragons 5E API, as I had been spending much of my free time during the pandemic playing the classic roleplaying game online, and I knew that it could help me build something applicable to that hobby and to the group I play with.

One of the biggest issues I have running a game of Dungeons and Dragons is finding treasure and magical equipment with which to reward my players. The book that contains these resources is large and difficult to quickly reference, which can be frustrating when a fast decision needs to be made in the heat of the story. As such, my goal was to build a CLI application that would allow me to easily pick from a list of magic items and pull up their details and description.

My first task was to ensure that I could properly retrieved the data from the API. After reviewing a few helpful tutorials, I found the initial data call to be rather easy, using the RestClient gem in conjunction with JSON to create a method called #get_items in my Quartermaster::API class. This allowed me to retrieve data from a hash that included the name, index and url reference of specific magic items.

With the API functioning correctly, I set about building a class called MagicItem that would store the data for each object I wanted to create. I initialized each object with a name and index, setting them to a default of nil in the off the chance that the information wasn’t available for some of the items. The index was especially important, as I knew it would later allow me to access deeper into the API to fill out additional information. I also created an extra attribute as an attr_accessor, desc (or description), because I knew that I would be adding that data to objects later on.

class MagicItemattr_accessor :name, :index, :equip_category, :desc@@all = []
def initialize(name = nil, index = nil) @name = name
@index = index
save
end
def save @@all << selfenddef self.all @@allenddef self.clear @@all.clearend

Lastly, I created a class variable called @@all to store an empty array that would contain my initialized objects, as well as a method to save each object on initialization, a class method to return the array and a class method to clear it. I may have done this a bit early, but I knew that I would eventually need those methods to reference in my CLI later.

With my MagicItem class built, I went back to work on the Quartermaster::API class to finish out #get_items, which would allow me to create MagicItem objects using the DND API. To accomplish this, I iterated over the data, and assigned each instance of that data to a new MagicItem object on initialization.

def self.get_items   get_items = RestClient.get("https://www.dnd5eapi.co/api/magic-       
items")
@items = JSON.parse(get_items)["results"] @items.each do |magic_item|
MagicItem.new(magic_item["name"], magic_item["index"])
end
end

This was the first moment of true success, as I was able to get the appropriate data from the API and create instances of the MagicItem class using that data. My classes were officially interacting.

But with this first success, came the first real struggle as well. The DND API is structured in such a way, that when you make the initial data call, the only information you retrieve for each item is a name, index and URL. To get to the more pertinent data, such as the description of the item, I had to access one level deeper by calling the individual item’s URLs.

To give myself time to work through this problem, I started dedicating more effort to my Quartermaster::CLI class, which would present the user with the data and allow for interaction — ultimately giving me the opportunity to test and visualize the objects myself.

I had already been working on a rudimentary CLI. My initial goal was to create a #call method that was easy to read and that anyone could look at and understand what it was trying to accomplish. A lot of the CLIs I had looked at in example projects were fairly overwhelming to me and my background as a hobbyist game developer had already given me a tendency of breaking up individual behaviors into separate methods. As such, I started by building out a simple #greeting method that put out a thematically appropriate welcome to the program. I also stubbed out a #display_items method that called #get_items from Quartermaster::API, then iterated over the newly imported objects in MagicItems.all, finally printing out a numerical list of magic items that the user could reference.

def display_items   puts "Let's see what we have here..."   puts " "   Quartermaster::API.get_items   MagicItem.all.each.with_index(1) do |magic_item, index|      puts "#{index}. #{magic_item.name}"

end
end

My next goal was to allow the user to enter the index number of the magic item that they’d like to view and pull up the specific details of that item. However, that would require me to solve my initial problem.

To accomplish this, I started by creating a new class method in Quartermaster::API called #self.item_details that took in an argument of a specific magic_item. Then, when using RestClient, I interpolated the magic_item’s index into the API’s URL, calling specific descriptions for specific items and assigning them post-initialization.

def self.item_details(magic_item)   get_item = RestClient.get("https://www.dnd5eapi.co/api/magic-     
items/#{magic_item.index}")
magic_item_info = JSON.parse(get_item) magic_item.desc = magic_item_info["desc"]end

But the method alone was not enough to test if this would work. I needed it to be able to interact with a full list of MagicItems.all in order to retrieve the appropriate indices and add the description data.

I returned to Quartermaster::CLI to construct a new method called #display_magic_item that would pass in a numerical input from the user, referencing the provided list of magic items. After some noodling around with the code, I settled on a conditional statement that would call Quartermaster::API.item_details if the input was between 1 and the length of the MagicItem.all array. If this were true, I would create a magic_item variable equivalent to subtracting 1 from the user’s input and using that to reference an index from the array. That magic_item would get passed in to Quartermaster::API.item_details and print out the name, type and description of the selection.

def display_magic_item(input = nil)   puts "An interesting choice...".colorize(:light_blue)   if input.to_i.between?(1, MagicItem.all.length)      magic_item = MagicItem.all[input - 1]                                  Quartermaster::API.item_details(magic_item)
puts "Name: #{magic_item.name}".colorize(:light_yellow)
puts "Type: #{magic_item.desc[0]}".colorize(:light_green) puts "Desc:"

magic_item.desc[1..100].each do |l|

puts l

end
endend

Finally, my basic functionality was complete. My program would welcome the user, provide them a list of magic items to review, allow the user to pick an item, and then to show them the details of that item. I was excited to see my program come to life, but there was still one additional feature I wanted to add, as well as few quality of life options to sort out.

Beyond the simple option of referencing magic items from a list, I liked the idea of a user being able to randomly choose an item, an option that they could use to get quick, unique treasure for their dungeon delves.

This option would be presented early in the CLI, during the greeting, with the user being provided the options to browse the catalog or receive a random gift. If they typed “browse” into their terminal, the program would run normally. But if instead they typed “random”, an item would be automatically generated without any additional steps.

It took some time to research the best way to implement this feature, but I settled on using the #sample method, which can be called on an array and return a random element from that array. I tested it in pry, and got the results I was looking for, so I implemented it.

My #random method started by calling Quartermaster::API.get_items to fill @@all with the necessary objects. I then proceeded to set a variable called randomizer to MagicItem.all.sample, which created the random object. Finally, I passed randomizer into Quartermaster::API.item_details and put out the data using the same pattern I did in my previous methods. Initially, I made this a class method in MagicItem, but determined that since it printed out data, it would be a better fit in Quartermaster::CLI.

def random   Quartermaster::API.get_items   randomizer = MagicItem.all.sample   Quartermaster::API.item_details(randomizer)     puts "Name: #{randomizer.name}".colorize(:light_yellow)     puts "Type: #{randomizer.desc[0]}".colorize(:light_green)     puts "Desc:"     randomizer.desc[1..100].each do |l|        puts l     endend

I proceeded to create a conditional statement, just after my initial greeting in the CLI, that would allow the user to select between a list of magic items or a random treasure, wrapping up my work on the feature.

Most of the rest of my work was either cosmetic, attempting to make the CLI more readable for the user, or fixing potential bugs, such as what would happen if a user mis-typed “random” or “browse” during the greeting stage. I used the colorize gem to make certain lines of text stand out from others and iterated over the incoming description so that the lines would break in a more digestible fashion.

My last big requirement was that users be prompted to start over if they wanted to browse again or receive another random item. Otherwise, the program would exit.

I had an initial version of this idea running from the beginning of my project, but it cluttered my call method and restarted the program at the greeting, which felt a unwieldy. As such, I determined it would be a smarter move to create an #exit_prompt method that would have the functionality I was looking for while being simple to understand in #call.

This method would ask the user if they would like to try again, then start them on a new a cycle of the program. Ultimately, it looks very similar to #call, but without the greeting. It still doesn’t feel like the best solution, as there is a lot of repetitive code, but I’ve decided to settle on it for now as I continue to improve the program into the future.

While the CLI project was challenging, often causing me to wrack my brain for hours on end to solve simple problems, it was also incredibly rewarding. I knew I was grasping a lot of the concepts I had learned in the course up until now, but felt like I was struggling to put them all together in my head in a cohesive way. This was the first time that I felt as though the right neurons were connecting and I was actually beginning to truly understand the work of coding. It helped me draw a clear line from the beginning of my time at Flatiron to where I am now and to visualize my progress. I’m very proud of the work I’ve done and am very excited to see what comes next.