mtg-calculator
Version:
an MtG calculator for computing the probability of being able to draw and play cards from a deck
194 lines (193 loc) • 13.4 kB
JavaScript
const bigNum = require("bignumber.js");
const { postProcess, nCk, factorial, binKeySort } = require("./probabilityCalculatorUtils");
// for copying one-layer-deep arrays or json
function copyShallow(variable) {
const variableCopy = new variable.__proto__.constructor();
for (let key in variable) {
variableCopy[key] = variable[key];
}
return variableCopy;
}
function hashJSON(json) {
let hash = "|";
Object.keys(json).forEach(key => {
hash += key + ":" + json[key] + "|";
});
return hash;
}
function computeProbabilities(deckBins, costBins, tapBins, relevantBinsMap, relevantBinsReverseMap, deckInfo, totalDraws, startingHandSize = 7) {
// lots of variables...
const deckBinKeys = binKeySort(Object.keys(deckBins));
const costBinKeys = binKeySort(Object.keys(costBins));
const relevantBinTotals = Object.keys(relevantBinsMap).reduce((totals, costKey) => {
totals[costKey] = 0;
relevantBinsMap[costKey].forEach(binKeyIndex => {
totals[costKey] += deckBins[deckBinKeys[binKeyIndex]];
});
return totals;
}, {});
const drawsRemaining = totalDraws;
const costKeyIndex = 0;
const relevantBinKeyIndex = 0;
const amountOfCurrentTypeRemaining = costBins[costBinKeys[costKeyIndex]];
const spaceRemainingForCurrent = relevantBinTotals[costBinKeys[costKeyIndex]] - deckBins[deckBinKeys[relevantBinsMap[costBinKeys[costKeyIndex]][relevantBinKeyIndex]]];
const CMC = costBinKeys.reduce((cmc, costKey) => ["O", "T"].includes(costKey) ? cmc : cmc + costBins[costKey], 0); // converted mana cost
const cache = {}; // to prevent a hand from being created two different ways
const nCkCache = {}; // to make binomials faster
const results = new Array(totalDraws - startingHandSize + 1).fill(0).map(n => new bigNum(n)); // our curve
const handBins = {}; // data structure of the hand
const costBinsPlacements = {}; // dict of binary masks
costBinKeys.forEach(costBinKey => costBinsPlacements[costBinKey] = 0);
deckBinKeys.forEach(deckBinKey => handBins[deckBinKey] = 0);
function countHandSymmetries(handBins, deckBinFulfilledMask, cardsDrawn) {
let count = new bigNum(factorial(cardsDrawn));
// basic count of hand symmetries
for (handBinKey in handBins) {
if (handBins[handBinKey] > 0)
count = count.times(nCk(deckBins[handBinKey], handBins[handBinKey], nCkCache));
}
// count of hands which taplands prevent from working
let problematicHandCount = new bigNum(0);
const onCurve = cardsDrawn - 6 === CMC;
// checking if all the lands in our hand could be taplands
let allLandsCouldBeTapLands = Boolean(Object.keys(tapBins).length > 0);
let handBinKeyIndex = 0;
while (allLandsCouldBeTapLands && handBinKeyIndex < Object.keys(handBins).length) {
let handBinKey = Object.keys(handBins)[handBinKeyIndex];
if (handBinKey !== "O" && handBinKey !== "T,O" && handBins[handBinKey] > 0)
allLandsCouldBeTapLands = allLandsCouldBeTapLands && handBins[handBinKey] <= tapBins[handBinKey] && (handBins[handBinKey] + tapBins[handBinKey] > 0);
handBinKeyIndex++;
}
// if all our lands are taplands on curve it doesnt matter how much excess mana we have, we still can"t play the card
if (onCurve && allLandsCouldBeTapLands) {
let problematicHandsCausedByTapLandsOnCurve = new bigNum(1);
let totalTaplands = 0;
for (let i = 0; i < deckBinKeys.length; i++) {
// choose all taplands, and count how many for later
if (handBins[deckBinKeys[i]] > 0 && tapBins[deckBinKeys[i]] > 0) {
problematicHandsCausedByTapLandsOnCurve = problematicHandsCausedByTapLandsOnCurve.times(nCk(tapBins[deckBinKeys[i]], handBins[deckBinKeys[i]], nCkCache));
// dont want to double count when the max number of tap lands of this type are drawn and considered problematic because of their order
// but that only happens if no overflow card have been placed s.t. deckKeyBinPrimeProductIds[deckBinKeys[i]] divides deckCostBinsFulfilledComposite
if ((deckBinFulfilledMask & (1 << i)) === 0)
totalTaplands += handBins[deckBinKeys[i]];
}
// choose whatever
else if (handBins[deckBinKeys[i]] > 0) {
problematicHandsCausedByTapLandsOnCurve = problematicHandsCausedByTapLandsOnCurve.times(nCk(deckBins[deckBinKeys[i]], handBins[deckBinKeys[i]], nCkCache));
}
}
// 9! orders to get such a version of this hand, but some will be counted by the next step of the process where we count the situations where a taplands is drawn last,
// so here we only count the number of ways we do not draw a tapland last
problematicHandCount = problematicHandCount.plus(problematicHandsCausedByTapLandsOnCurve.times(factorial(cardsDrawn - 1).times(cardsDrawn - totalTaplands)));
}
// for each deck bin...
for (let i = 0; i < deckBinKeys.length; i++) {
// if there are taplands in this deck bin AND it can still cause a problem
const deckBinCanCauseProblem = tapBins[deckBinKeys[i]] > 0 && handBins[deckBinKeys[i]] > 0 && ((deckBinFulfilledMask & (1 << i)) === 0);
if (deckBinCanCauseProblem) {
// number of problematic hands begins at 1
let problematicHandCausedByDeckBinCount = new bigNum(1);
// multiply the number of problematic hands caused by this bin by a factor
for (let j = 0; j < deckBinKeys.length; j++) {
let factor;
// if the deck bin we're checking is not the deck bin we"re adding a factor for, count the number of ways to choose the number in the hand bin by the number in the deck bin
if (i !== j)
factor = nCk(deckBins[deckBinKeys[j]], handBins[deckBinKeys[j]]);
// if this is the problematic bin, the factor is the sum of the ways to choose N tap lands and H - N non tap lands
// where H is the number of cards we"re taking from the bin total, and N ranges from the minium to the maximum number we can choose
// each term is multiplied by the number of tap lands being chosen as that is the number of cards which can be last-drawn to create a problem
else {
let minTapLands = Math.max(1, handBins[deckBinKeys[j]] + tapBins[deckBinKeys[j]] - deckBins[deckBinKeys[j]]);
const maxTapLands = Math.min(handBins[deckBinKeys[j]], tapBins[deckBinKeys[j]]);
factor = new bigNum(0);
while (minTapLands <= maxTapLands) {
// (deck_bin_size - tap_bin_size CHOOSE hand_bin_size - tap_lands_chosen) * (tap_bin_size CHOOSE tap_lands_chosen) * tap_lands_chosen
let term = nCk(deckBins[deckBinKeys[j]] - tapBins[deckBinKeys[j]], handBins[deckBinKeys[j]] - minTapLands, nCkCache).times(nCk(tapBins[deckBinKeys[j]], minTapLands, nCkCache)).times(minTapLands);
factor = factor.plus(term);
minTapLands++;
}
}
// if the factor is > 0 we'll append it to our product, otherwise we"ll just leave it alone (representing a situation in which no tap lands could be chosen)
if (factor.isGreaterThan(1))
problematicHandCausedByDeckBinCount = problematicHandCausedByDeckBinCount.times(factor);
}
// now that we've counted the number of hands which could cause problems given deck bin "i", we count the number of orders of said hand
// this means multiplying by *(hand size - 1)!* because we know that a problematic tapland must be drawn last
problematicHandCount = problematicHandCount.plus(problematicHandCausedByDeckBinCount.times(factorial(cardsDrawn - 1)));
}
}
// subtract problematic hands and return count
count = count.minus(problematicHandCount);
return count;
}
function makeAbstractHands(handBinsCopy, costBinsPlacementsCopy, deckBinFulfilledMask, costKeyIndex, relevantBinKeyIndex, drawsRemaining, amountOfCurrentTypeRemaining, spaceRemainingForCurrent) {
// checking if hand is playable and conditionally recording it
const turn = totalDraws - drawsRemaining - startingHandSize; // zero indexed
if ((turn + 1) >= CMC && turn >= 0) {
const handHash = hashJSON(handBinsCopy);
if (!cache[handHash]) {
results[turn] = results[turn].plus(countHandSymmetries(handBinsCopy, deckBinFulfilledMask, totalDraws - drawsRemaining));
cache[handHash] = true;
}
}
// if we've exhausted the current cost bin, move on -or terminate-
if (amountOfCurrentTypeRemaining === 0) {
costKeyIndex++;
if (costKeyIndex === costBinKeys.length)
return;
relevantBinKeyIndex = 0;
amountOfCurrentTypeRemaining = costBins[costBinKeys[costKeyIndex]];
spaceRemainingForCurrent = relevantBinTotals[costBinKeys[costKeyIndex]] - deckBins[deckBinKeys[relevantBinsMap[costBinKeys[costKeyIndex]][relevantBinKeyIndex]]];
}
let relevantCostBinKeyIndices = relevantBinsMap[costBinKeys[costKeyIndex]]; // deck keys which current cost key has edges to
// if our cost key and our relevant bin key are still within bounds we can continue
if (costKeyIndex < costBinKeys.length && relevantBinKeyIndex < relevantCostBinKeyIndices.length) {
const relevantBinKey = deckBinKeys[relevantCostBinKeyIndices[relevantBinKeyIndex]];
const min = 0; //Math.max(amountOfCurrentTypeRemaining - spaceRemainingForCurrent, 0) // TODO make this logic good
const max = Math.min(deckBins[relevantBinKey] - handBinsCopy[relevantBinKey], amountOfCurrentTypeRemaining);
let nextHandBinsCopy;
let nextCostBinsPlacementsCopy;
// add as few to as many cards as possible to this bin and move on to the next one (branch for each choice of how many)
for (let i = min; i <= max; i++) {
// copy of handBins updated for branching
nextCostBinsPlacementsCopy = copyShallow(costBinsPlacementsCopy);
nextHandBinsCopy = copyShallow(handBinsCopy);
nextHandBinsCopy[relevantBinKey] += i;
let nextDeckBinFulfilledMask = deckBinFulfilledMask;
// updating info about which bins are "fulfilled" and which bins payed for which costs
if (costBinKeys[costKeyIndex] === "O" && i > 0) {
/*
to fulfill a bin, or make a bin such that taplands cannot obstruct it, you must:
mark that the bin is fulfilled,
if any unfulfilled deck bins which have balls placed in them from cost bins which this deck bin could fulfill exist, fulfill those as well
*/
function fulfill(deckBinIndex) {
if (nextDeckBinFulfilledMask & (1 << deckBinIndex))
return;
nextDeckBinFulfilledMask = nextDeckBinFulfilledMask | (1 << deckBinIndex);
relevantBinsReverseMap[deckBinKeys[deckBinIndex]].forEach(costBinKeyIndex => {
let j = 0;
while ((1 << j) <= costBinsPlacementsCopy[costBinKeys[costBinKeyIndex]]) {
if (costBinsPlacementsCopy[costBinKeys[costBinKeyIndex]] & (1 << j))
fulfill(j);
j++;
}
});
}
// since we're placing an overflow card in this bin we "fulfill" it
fulfill(relevantCostBinKeyIndices[relevantBinKeyIndex]);
}
else if (i > 0) {
// if we're placing a ball from one of our non-overflow cost bins, we record where to see which bins become fulfilled by overflow cards
nextCostBinsPlacementsCopy[costBinKeys[costKeyIndex]] = nextCostBinsPlacementsCopy[costBinKeys[costKeyIndex]] | (1 << relevantCostBinKeyIndices[relevantBinKeyIndex]);
}
makeAbstractHands(nextHandBinsCopy, nextCostBinsPlacementsCopy, nextDeckBinFulfilledMask, costKeyIndex, relevantBinKeyIndex + 1, drawsRemaining - i, amountOfCurrentTypeRemaining - i, spaceRemainingForCurrent - deckBins[relevantBinKey]);
}
}
}
makeAbstractHands(handBins, costBinsPlacements, 0, costKeyIndex, relevantBinKeyIndex, drawsRemaining, amountOfCurrentTypeRemaining, spaceRemainingForCurrent);
return postProcess(results, deckBins, startingHandSize, totalDraws, deckInfo, CMC, nCkCache);
}
module.exports = {
computeProbabilities
};