Skip to content

Domain Model

Everything in Commander Spellbook is built from a small vocabulary. Learn these six concepts and the rest of the codebase reads easily. The models live in backend/spellbook/models/.

The core concepts

Card

A Magic card, mirrored from Scryfall and keyed by oracle_id (synced by the update_cards task). A Card entity mirrors an oracle card — the game-rules identity shared by every printing — not a specific reprint: there is one Card per oracle card, regardless of how many times it has been printed. The table holds every oracle card Scryfall knows, so that a decklist resolves whole; the ones an editor has curated carry a compact number, and that number — not the primary key — is what every other model references, what a variant id is built from, and what the API calls the card's id. An editor curates a card with the action on the card list, and can enter one Scryfall has not published yet: it simply has no oracle_id until a later sync adopts it by name. Beyond its name it carries the Magic characteristics used for filtering and validation — color identity, mana value, type line, oracle text, keywords — inherited from the abstract Playable base. A card can produce features directly (e.g. a card that by itself is "an extra turn").

Feature

A named effect or result — the abstraction that lets the engine chain things together. Examples: Infinite mana, Untap target permanent, Win the game. Cards and combos produce features; combos need features. A feature's status decides how it is treated and shown:

Status Meaning
Hidden / Public utility Intermediate building block used only by the engine (public ones are visible to combo submitters)
Helper A reusable effect meant to be exploited by other combos
Contextual Situational effect
Standalone A meaningful, usually game-impacting result

Features can be marked uncountable (only ever one copy — this also speeds up generation).

Template

A placeholder for "any card matching a query", e.g. "a creature with power 4 or greater". A template is defined either by a Scryfall-style search query or by an explicit list of concrete replacements (cards known to satisfy it) — not both. Either way the replacements are ordinary Cards. Templates let a combo be written generically; the engine expands them into real cards.

Both kinds resolve to the same relation, matches, recomputed by the card sync and whenever a template is edited: a query is evaluated locally against the card table, a replacement list is taken as written. That is what lets a deck be told how many copies of a template it actually holds, rather than assuming a query template is always available.

Combo

The editor-authored interaction — the input to the engine. A combo is a recipe that declares:

  • uses — cards it needs (CardInCombo)
  • requires — templates it needs (TemplateInCombo)
  • needs — features it consumes as prerequisites (FeatureNeededInCombo)
  • produces — features it results in (FeatureProducedInCombo)
  • removes — features it invalidates (FeatureRemovedInCombo)

plus editorial text (mana needed, prerequisites, step-by-step description, notes). A combo's status controls its role in generation — most importantly GENERATOR (a combo that variants are generated from) versus UTILITY (a building block that only exists to be chained into others).

Variant

A concrete, fully-resolved card combination produced by the engine — the primary object the API serves. Where a combo may say "a mana dork + Isochron Scepter", a variant names the exact cards. Each variant records the generator combos it is of, the combos it includes, the cards it uses, the templates it requires, and the features it produces, and adds derived data: color identity, mana cost, popularity, a power bracket estimate, and a review status (only OK and EXAMPLE variants are public). Variant output is pre-serialized/denormalized on save so reads are fast.

Suggestions

Community-submitted content that waits for editor review before it becomes canonical:

  • VariantSuggestion — a combo submitted by a user. Editors review it and, if accepted, turn it into a real Combo.
  • VariantUpdateSuggestion — proposed edits to existing variants.
  • VariantAlias — a redirect from an old or alternative id to a canonical variant, so links never break.

How they relate

UML diagram image

The feature is the pivot: a card or combo produces a feature, and another combo needs it. Chaining "produces → needs" across the graph is exactly what the variant generation engine walks.

Shared building blocks

A few abstract models and join tables recur throughout:

  • Recipe — the abstract uses / requires / produces structure and automatic name generation, shared by Combo, Variant, and VariantSuggestion.
  • Playable — Magic characteristics (color identity, mana value, type line, …) shared by Card and Variant.
  • Ingredient / OrderedIngredient — the through-model base that carries per-item data such as quantity and starting zone locations (hand, battlefield, graveyard, …).

The [[name]] and {{name}} reference syntaxes

Text fields (descriptions, prerequisites) can reference a feature by name with [[name]]. Two modifiers exist: [[name$selector]] selects one of several copies, and [[name|alias]], always written last, names the reference so that the same text can repeat it as [[!alias]], or write its producers' text as {{!alias}}, before or after the definition. This lets editorial prose refer to generated pieces without hardcoding card names. Every character of the syntax is defined once, at the top of spellbook/models/references.py, which builds the patterns, the validators and the editors' help text from them.

A combo may only reference the features it needs, and its texts only see the copies its needed feature rows accept: a card the variant holds for another reason, producing the feature with attributes the combo rules out, is never named by them. A card feature needs no feature, so its texts cannot reference any. The admin refuses a save breaking either rule.

The selector is either a number or an attribute name. A number counts through the copies the combo owning the text needs of that feature, row after row, so when each row needs one copy [[name$2]] always means "whatever satisfies the second row", in every variant of that combo — the rows are drag-sortable in the admin, and a number past the copies the combo needs is refused. An attribute name, as in [[name$Landfall]], picks the copy produced with that attribute regardless of any ordering. The starting location override of a needed feature row reaches the same copies, so what [[name$2]] names starts where the second row asks, and a copy left over starts where every row accepting it asks. Renaming a feature or a feature attribute in the admin rewrites the references to it across every text field, so neither kind of name may contain the $ and | characters the syntax reserves.

{{name}} references the same features, but replaces itself with the text box it is written in — the description with descriptions, the notes with notes — taken from every combo or card feature producing that feature that the combo's rows accept. Those texts are then not appended to the variant's own, because they have already been written where they were mentioned; a field holding no {{name}} at all is assembled by appending, as it always was. Two identical {{name}} write the text twice, an inclusion inside an included text expands in turn, and a text including itself stops there. {{name$selector}} takes the same numbers and attribute names as [[name$selector]] and points at the same thing, filtering which producers are written — the ones it leaves out are still appended. An inclusion left with nothing to write, because nobody produces the feature or the selector picks nobody, is deleted from the variant without touching the text around it, so a clause only some producers write is best glued to the word before it, its own punctuation leading its text: x{{name}}. reads x, y. or x.. A line the deletion leaves empty goes away with it; an unresolvable [[name]], instead, still stays in the text verbatim. It is available in easy_prerequisites, notable_prerequisites, description, notes and comment, and in the starting card states, which are assembled the same way, out of the rows each ingredient inherits its state from; only mana_needed is left out of it, because it takes mana symbols alone.

{{#Z}}, {{#+X}} and {{#-Y}} point at the Zth line of their own text, the line X lines below and the one Y lines above, and each variant replaces them with the number that line ends up at, once inclusions have moved it. Aliases and line references have prefixes of their own so that they never collide with feature names: a feature name may start with a digit, but never with a prefix.

Where to go next

The Variant Generation page explains how the engine turns these combos into variants.

Authors: ldeluigi