The MTG Deck Builder algorithm calculates the probability that you can cast a specific card by a certain turn, given your deck composition. It does this by:
Determining if the deck can theoretically cast the card
Parsing all possible “good hands” that can cast the card
Calculating the probability of drawing each hand using hypergeometric distribution
Summing probabilities across all viable hand combinations
const probabilityOfPlayingCard = function ( cardsDrawn, // Total cards drawn (e.g., 7 for opening hand) card, // The card object with manaCost and type deck, // Array of all cards in the deck startingHandSize // Default 7, can be 6 after mulligan)
This boolean function determines if a deck has the resources to cast a card:
ArithmaticHelpers.js:347-375
function cardPlayable(draws, card, deck, startingHandSize = 7) { deck = deck.copy(); let cost = cardCost(card); let convertedManaCost = Object.keys(cost).reduce((a, b) => a + cost[b], 0); let manaBase = JSONmanaBase(deck); // Check 1: Have you had enough turns to reach that mana? let turnCondition = draws - startingHandSize >= convertedManaCost - 1; // Check 2: Do you have enough lands total? let manaCondition = Object.keys(manaBase).reduce((a, b) => a + manaBase[b], 0) >= convertedManaCost; // Check 3: Do you have the right colors? delete cost.C // Ignore colorless mana let colorCondition = Object.keys(cost).reduce((a, b) => { Object.keys(manaBase).forEach(v => { if (v.split(',').includes(b)) { let min = Math.min(manaBase[v], cost[b]); cost[b] -= min; manaBase[v] -= min; } }); return a && cost[b] <= 0; }, true); // Check 4: Is the card in the deck? let includesCondition = deck.map(v => v.name).includes(card.name); return colorCondition && manaCondition && turnCondition && includesCondition;}
A hand is represented as an array of arrays: [[drawn, available, type], ...]Example hand that can cast {2}{U}{U}:
[ [1, 4, 'T'], // 1 copy of target card drawn (out of 4 in deck) [0, 32, 'O'], // 0 other cards drawn (32 available) [2, 10, 'U'], // 2 Islands drawn (10 in deck) [4, 14, 'U,R'] // 4 dual lands drawn (14 in deck)]
The algorithm must account for dual lands being used for multiple color requirements, which significantly complicates the parsing logic (ArithmaticHelpers.js:216-239).
The numerator represents the number of ways to draw a specific hand combination. The denominator C(deckSize, cardsDrawn) is constant for all hands, so it’s calculated once in the parent function (ArithmaticHelpers.js:59) and used to divide the sum of all numerators.
This recursive function generates all combinations of placing numBalls balls into bins without exceeding bin capacity:
ArithmaticHelpers.js:379-400
function multichoose(numBalls, bins, combinations, com = bins.copy()) { // Base case: no balls left to place if (numBalls === 0) { combinations.push(com.copy()); } else if (bins.length) { // Try placing 0 to min(capacity, numBalls) balls in current bin for (var i = 0; i <= Math.min(bins[0][1] - bins[0][0], numBalls); i++) { // Update the current bin if (i) com[com.length - bins.length][0] = com[com.length - bins.length][0] + 1; // Recurse with remaining balls and bins multichoose( numBalls - i, bins.copy().slice(1), combinations, com.copy().slice(0) ); } }}
The algorithm uses Vandermonde’s identity to partition probability across all possible fetch land draws:
ArithmaticHelpers.js:60-141
let fetches = deck.filter(v => { if (v.ProducibleManaColors) { if (v.ProducibleManaColors.includes('F')) { return manaToFetch(deck, v.fetchOptions).ProducibleManaColors .split(',').reduce((a, b) => { return a || C[b] > 0 }, false) } } else return false})let others = deck.filter(v => !fetches.includes(v))for (var i = 0; i <= fetches.length; i++) { // Calculate probability for drawing exactly i fetch lands muliplier = nCk((fetches.length).toString(), (i).toString()) // Adjust the card's cost for fetched mana if (i > 0 && sudoCard.manaCost !== '') { // ... fetch land logic } // Parse hands without the fetches let goodHands = parseHands(cardsDrawn - i, sudoCard, others); // Sum probabilities let P = '0'; goodHands.forEach(hand => { h = hypergeometric(cardsDrawn - i, hand, memo) P = additionString(h, P); }); PP += parseFloat(multiplyString(P, muliplier))}
function nCk(n, k) { let result = '1'; let d = 1; for (var i = n; i > Math.max(n - k, k); i--) { result = multiplyString(result, i.toString()); result = divideString(result, d.toString()); d++; } return result;}
Example: C(60, 7) = 386,206,920 - This fits in a JS number, but C(99, 50) for Commander decks does not.