There is a wonderful Magic: The Gathering scene here in Durham, NC. Unfortunately, Magic often deserves the trope that it gets of being an expensive hobby. In order to enjoy the game on a budget, myself and a group of locals created a new format: Paper Dreadful. The format is simple: create a 60 card deck with a maximum of 4 of any card (other than basic lands) and the total cost must be $20 or less on that day. The same rule applies with the sideboard, except it must by $5 or less.
The problem here is that Magic cards change in price all the time. What happens if I build a deck and then a card skyrockets in price?
Proposed Solution
What our group essentially needs is a way to cement the price for a deck list in place so that we know that it was $20 at the time it was built. The initial way we did this was by taking a screenshot of the deck and post it in a discord. This was cumbersome though and it did not split the price out into mainboard and sideboard cards.
Our proposed solution was to make a discord bot where you paste a link to your deck and it stamps it with an approval or rejection based on the price that day. Such a bot would need to interact with three services: the deck list provider, a pricing provider, and Discord.
I have been playing around with Rust a lot recently and decided that this would be a good place to try it out on a larger project. If you want to follow along in the code, the source can be found at https://github.com/Carrigan/dreadbot.
Consuming APIs
In order to build a price list for a given deck, we need to work with at least two resources: the deck lists on mtggoldfish.com and pricing info from scryfall.com.
The MTGGoldfish connection is quite simple: all we are doing is downloading a text file formatted with one card per line and the quantity of that card. There are two parts of a deck, the main deck and the sideboard, and they are separated by an empty line. We consume this file by creating two data types: a Deck
object that has many Card
objects.
let mut mainboard: Vec<Card> = Vec::new();
let mut sideboard: Vec<Card> = Vec::new();
for line in block.split("\r\n") {
match Card::from_goldfish_line(line) {
Some(card) => {
if sideboard_flag { sideboard.push(card) } else { mainboard.push(card) }
},
None => sideboard_flag = true
};
}
Now that we have the Deck
parsed, our next goal is to pair this info with pricing info. The Card
object is capable of holding pricing information, but at this point that information is None
. Our Scryfall API takes the Deck
object and builds a query based on the cards in it. It then finds the cheapest version a given card and returns a vector of pricing information. This vector can be passed back to the Deck
object’s update_pricing(&mut self, scryfall_entries: Vec<PricingSource>)
function to fill in pricing. We now have a Deck
object with a list of fully filled Card
objects.
Interacting with Discord
Now that we have a way of fetching populated Deck
objects, we need a way for users to work with them. There are three important things we need to do: verify that a new deck’s main deck is under $20 and that its sideboard is under $5, give pricing info about a deck to guide users to meet these limits, and verify that a list has not changed.
Our group was already using Discord to do these things manually, so it seemed like a natural extension to provide discord bindings to do them. I found the Serenity crate which allows you to do this quite easily by implementing a trait with callbacks for message and connection events. For our handler, I started with a root level regex that defines whether the bot should respond to a message and then if so, attempts to see if the contents of the message inside the package are a valid command. If so, the bot performs the action; otherwise the bot will return a message telling the user what the available commands are.
impl EventHandler for Handler {
fn message(&self, ctx: Context, msg: Message) {
let regex = Regex::new(DREADBOT_PREFIX).unwrap();
if let Some(captures) = regex.captures(&msg.content) {
if let Some(remaining_message) = captures.get(1) {
if dreadbot_verify(&ctx, &msg, remaining_message.as_str()) { return }
if dreadbot_info(&ctx, &msg, remaining_message.as_str()) { return }
if dreadbot_hash(&ctx, &msg, remaining_message.as_str()) { return }
// Fallback to the help message
dreadbot_help(&ctx, &msg);
};
}
}
fn ready(&self, _: Context, ready: Ready) {
println!("{} is connected!", ready.user.name);
}
}
All of the functions take an argument in the form of a MTGGoldfish link. The verify function will retrieve the deck and pricing and give the deck a stamp of approval/disapproval and also give it a hash based on its contents so that people cannot change the cards in the deck list after it received a stamp of approval.
The info function retrieves the cards and pricing, but instead of applying any of our format’s logic to it, it simply returns the deck contents and how it priced the deck. This is useful for when you are at $20.05 and need to shave that remaining 5 cents.
Finally, the hash function retrieves just the deck contents and runs our hash algorithm on them to verify that they are the same as when they were initially submitted.
Conclusion
This bot has been in production for over a month now and our format is currently running its first league using the bot. I run the production version on a cheap server and it has had no outages or problems to date.
This was my first medium sized project in Rust, and I really enjoyed working with it. I worked with C as an embedded engineer for 4 years and it amazes me that a language exists that can output machine code as efficient as that while having all the conveniences of modern languages. I loved being able to perform test-driven design in the same file that I created new functions in. Between this and Elixir, I have also fallen in love with pattern matching. I have been working on several smaller embedded projects in Rust and hope that I get to work with it more in the future!
Project link: https://github.com/Carrigan/dreadbot