Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/invvd/mtg-cheaper-deck/llms.txt

Use this file to discover all available pages before exploring further.

The scoring module is responsible for two things: efficiently loading the set of legal, priced cards for a given format (the candidate pool), and ranking those candidates against a target card using a weighted similarity score. The pool is loaded once per deck run — not once per card — so the expensive SQL + JSON work happens a single time regardless of how many cards are in the deck. Scoring combines four independent signals. Each signal is a value between 0 and 1 (or slightly above for EDHREC), then multiplied by its weight before being summed into a final score. Higher is more similar.

Scoring Weights

The four weight constants control how much each signal contributes to the total score.
ConstantValueSignal
KEYWORD_WEIGHT3.0Jaccard overlap between Scryfall evergreen keyword lists
TEXT_OVERLAP_WEIGHT2.5Jaccard overlap between tokenised, stop-word-filtered oracle texts
CMC_WEIGHT1.0Proximity in converted mana cost: `1 / (1 +Δcmc)`
EDHREC_WEIGHT1.0Popularity signal: 1 / (1 + rank / 1000)
KEYWORD_WEIGHT is the highest because shared evergreen keywords (Flying, Trample, Deathtouch, etc.) are the most direct signal of functional overlap. TEXT_OVERLAP_WEIGHT is almost as important because many combo or synergy pieces share no keywords at all — their function lives entirely in the oracle text.

extract_core_types

Parses a Scryfall type_line string and returns the set of recognised core supertypes present.
type_line
str | None
required
A Scryfall type line string such as "Legendary Creature — Elf Druid" or "Instant". The function only considers the portion before . Returns an empty set when None or an empty string is passed.
Returns set[str] — a subset of the recognised core types:
Artifact, Battle, Conspiracy, Creature, Dungeon, Enchantment,
Instant, Kindred, Land, Phenomenon, Plane, Planeswalker,
Scheme, Sorcery, Tribal, Vanguard
This function is used internally by suggest_alternatives() to ensure that candidate cards share at least one core type with the original (e.g. a Sorcery is not suggested as a replacement for a Creature).
from scoring import extract_core_types

extract_core_types("Legendary Creature — Elf Druid")
# {'Creature'}

extract_core_types("Artifact Creature — Construct")
# {'Artifact', 'Creature'}

extract_core_types("Instant")
# {'Instant'}

CandidatePool

CandidatePool holds all legal, priced cards for a format, pre-processed for fast per-card scoring. Construct it via load_candidate_pool() rather than directly.

Constructor

rows
list
required
A list of card row dicts, each sourced from the cards table in the local database. Every row must have at least price_usd, keywords, and oracle_text fields. The constructor pre-computes _keywords_set (a set of keyword strings) and _text_tokens (a stop-word-filtered token set from oracle_text) on each row in place, then sorts the list ascending by price_usd.
Pre-computing keyword sets and token sets here — once per pool load — avoids repeating the same tokenisation work for every card in the deck.

cheaper_than

Returns all cards in the pool whose price_usd is strictly less than the given price.
price
float
required
The price ceiling (exclusive). Only cards with price_usd < price are returned.
Returns list[dict] — a slice of the sorted pool rows, all cheaper than price. Uses bisect.bisect_left on the pre-built price list for O(log n) lookup.
pool = load_candidate_pool("commander")
cheaper = pool.cheaper_than(5.00)
print(f"{len(cheaper)} cards cost less than $5.00")

load_candidate_pool

Queries the local database and returns a CandidatePool containing every card with a USD price that is legal in the given format.
format_key
str
required
A Scryfall format name stored as a key inside the legalities JSON column — for example "commander", "standard", "modern", "pioneer". Only cards where legalities[format_key] == "legal" are included.
Returns CandidatePool — pre-sorted by price_usd ascending, with _keywords_set and _text_tokens pre-computed on every row.
load_candidate_pool currently requires a SQLite database. The legalities filter uses SQLite’s json_extract function. For PostgreSQL you would need to replace that clause with the jsonb ->> operator before switching dialects. The color identity filter (color_identity_mask bitmask AND) is portable to any SQL dialect.
from scoring import load_candidate_pool

pool = load_candidate_pool("commander")
print(f"Pool contains {len(pool.rows)} legal cards with a price")

suggest_alternatives

Scores every candidate in the pool against original and returns the top top_n suggestions.
original
dict
required
A local database row dict for the card being replaced. Must have at minimum oracle_id, price_usd, color_identity_mask, type_line, keywords, oracle_text, and cmc. If price_usd is None, an empty list is returned immediately.
pool
CandidatePool
required
A CandidatePool loaded with load_candidate_pool() for the same format as the deck. Should be created once and reused for every card in the deck.
top_n
int
default:"5"
Maximum number of suggestions to return. Results are sorted by score descending; only the highest-scoring top_n candidates are returned.
exclude_oracle_ids
set | None
default:"None"
A set of oracle_id values to skip — typically the oracle IDs of all cards already in the deck. Without this, a card already present elsewhere in the deck could appear as a suggestion for a different card.
Returns list[dict] — up to top_n suggestion dicts, sorted by score descending. Each dict has:
card
dict
The full candidate card row from the database.
score
float
The combined weighted similarity score (sum of all four weighted signals).
reasons
dict
A human-readable breakdown of the score:
  • shared_keywords — sorted list of keyword strings shared between original and candidate
  • shared_text_terms — sorted list of up to 6 oracle-text tokens shared between original and candidate
  • cmc_delta — absolute CMC difference (float), or None if either card lacks a CMC
  • edhrec_rank — the candidate’s EDHREC rank (int), or None if unavailable
Before scoring, candidates are filtered by three hard rules: they must be strictly cheaper than the original, their color_identity_mask must be a subset of the original’s (no off-colour suggestions), and they must share at least one core type with the original (no cross-type suggestions).
from scoring import load_candidate_pool, suggest_alternatives

pool = load_candidate_pool("commander")

# `original` is a row dict from the local `cards` table
suggestions = suggest_alternatives(original, pool, top_n=5)

for s in suggestions:
    card = s["card"]
    reasons = s["reasons"]
    print(f"{card['name']} ${card['price_usd']:.2f} — score {s['score']:.2f}")
    print(f"  Keywords: {reasons['shared_keywords']}")
    print(f"  Text overlap: {reasons['shared_text_terms']}")
    print(f"  CMC delta: {reasons['cmc_delta']}")

Build docs developers (and LLMs) love