Generating balanced cubes for Magic the Gathering

A huge problem I encounter with Magic: The Gathering (MTG) is that there are thousands of cards in the game (around 33000 unique cards as of the time of writing). Depending on the format you play, you may be required to select a limited subset of these cards to play with.

A common social format is the Cube draft or Cube. You and a group select and refine ~300-700 cards to create a custom set. While many players enjoy the process of selecting and refining every single card, I usually know a few cards I really want to play with ahead of time but don't really have the patience or will power to build out the rest of the cube.

What is a balanced set?

Looking at existing sets, to keep them playable in Draft formats, they aim to balance features like color, mana, rarity, theme. For example, if the mana curve is unbalanced, players might encounter too many expensive cards and end up not being able to do anything for several turns of the game. Or encountering too much of a single color would make gameplay homogenous - everyone playing the same deck is no fun.

Examining the latest 5 sets shows us what this balance looks like

Embedding cards in a set and clustering allows us to visualize themes and strategies that are dominant in the sets.

Depending on the number of clusters you select, you might notice a card or creature type heavily preferring some mechanics, and mechanics that cross card type boundaries. You may also notice color distribution preferences as well.

To build a well balanced cube I wanted to be able to encode these preferences into my model/process.

Cube building as an optimal assignment problem

I initially started by attempting to train a model to predict a card's likelyhood of being part of a set, but quickly ran into the lack of training data. There are roughly ~1000 sets (including promos, commander, etc) so that wasn't a viable strategy.

I reframed my problem was trying to fit my selection to sets already available. Existing sets already encode preferences such as the number of cards, color distribution, mana curve and theme groupings. So fitting my preferences to the shape of an existing set should result in something that feels like a part of the game.

I realized that if I treat every card in a template set list as a slot, I can build a cost matrix against my card selections and encode penalties for mismatched card properties like color, mana, type and rarity mismatches. This would result in a score that would roughly indicate a "best fit" for any given slot. Let's illustrate with an example.

Assuming a hypothetical set has 5 cards in it. Loyal Tutor, Cruel Calculations, Marwyn, the Preserver, and Carnelian Orb of Dragonkind. I want to replace the cards in that set to buld my own custom set. So I pick 3 cards that I really want to play with - Generous Gift, Opt and Kargan Dragonrider.

My cost matrix can help me decide which of my selected cards are best suited to replace cards in the set. I can apply penalties to adjust how important card features are to filling those slots. Let's assume w_color, w_type, w_cmc, and w_rarity are my weight penalties indicating a preference of color, type, cost, and rarity respectively. My goal is to pick cards with the lowest costs to fill slots.

cheap expensive assigned Rows are the example set's cards (slots), columns are cards from my list. Click a cell to see why it costs what it does.

Initially, Generous Gift is the best card to fill the Loyal Tutor slot because they share a lot of the same properties. They are both white spells and instants but have different mana costs. However by lowering the w_color weight, color becomes less important and the blue instant spell Opt becomes a much lower cost to fill that slot because they have the same mana cost despite being different colors.

Adding a max_cost allows us to set a threshold that indicates that there's no suitable card in our list and so a card needs to be selected outside of our selection pool to fill that gap. We do the same if there weren't enough cards selected to fill every slot

Selecting for thematic cohesion

The process so far allows me to select cards based on some card features, however I want to ensure that the cards I select follow a consistent theme. Embeddings help me find similar/relevant cards to any given cards (I'm using my mtg card text embedding model nishtahir/ModernBERT-mtg-commander which focuses on cards that are typically played together in Commander. You could use other models but YMMV) using cosine similarity.

This worked well, however it had a tendency to skew towards a single theme/mechanic. To prevent this, I group my selected list into themes by clustering with K-means. This allows me to select how many clusters appear in the final output. Introducing a theme_alpha allows me to control the distribution of themes to either all have the same share in the output or be proportional to my selections.

Cards per theme

in my list chosen for a slot quota

Evaluating the solution

So far, I've shown that the process is capable of making card selections but it's important to measure get some sense of how good the output is. Since one of my initial approaches was to try to recover official set lists using retrieval I focused on that as my baseline. I decided on a few key metrics

  • Cohesion - Average pairwise cosine simiarity of the finished set.
  • Card color distance
  • Card type distance
  • KL Divergence of the mana curve

I tested using 13 recent expansion sets. For each set, 50 random non land cards would form my selection list and the goal is to build an output that is thematically consistent with the original set.

method cohesion (higher is better) color distance (lower is better) type distance (lower is better) mana curve KL (lower is better)
mtg-cube-builder 0.126 0.026 0.053 0.016
centroid retrieval 0.350 0.334 0.319 0.180
kNN-10 retrieval 0.338 0.328 0.329 0.182
random 0.063 0.105 0.117 0.061

My method does a much better job fiting on the curve distributions than the others but in terms of pairwise similarity the retrieval processes come out ahea.

This was a surprising result, but in hindsight makes a lot of sense because my process is constrained by fitting the constraints of slots in the template while retrieval focuses just on finding similar/relevant cards from the pool of every available card.

I decided to test how well this process recovers partial sets. I generated a set by sampling at different sizes (10%, 25% and 50%), I then scored the similarity each generated card against the missing cards and then keep the best one, the continue until I ran out of cards then take the average to produce a reconstruction score

list size mtg-cube-builder (higher is better) kNN-10 random
10% 0.546 0.523 0.474
25% 0.563 0.555 0.495
50% 0.573 0.568 0.517

While my method might not recover exact cards as often it does about a good of a job as KNN does at staying close to the theme. When considering that it does this while maintaining a good color/mana curve distribution, I think it's a win.

Conclusion

I built this process into a tool to make it repeatable and easy to use. It's avaliable on GitHub, with some sample output here. I selected 50 cards of which 41 cards were selected to build a 300 card set. It identified a food and human theme in list I gave it despite being a hodge podge of random cards I like. It's far from perfect but has produced an interesting enough result that I'm excited to put the cards together to try it out.

Ultimately, the most important metric is whether it's fun to play. That can really only come from play testing so the next step from here is generating faux packs, assembling the cards and bringing them to the table.