UNPKG

mtg-calculator

Version:

an MtG calculator for computing the probability of being able to draw and play cards from a deck

291 lines (290 loc) 12 kB
// the following two functions are from https://www.delftstack.com/howto/javascript/javascript-random-seed-to-generate-random/ // the algorithms they're using (which they are named by) are far more ubiquitous function MurmurHash3(string) { let i = 0; let hash = 1779033703 ^ string.length; for (; i < string.length; i++) { let bitwise_xor_from_character = hash ^ string.charCodeAt(i); hash = Math.imul(bitwise_xor_from_character, 3432918353); hash = hash << 13 | hash >>> 19; } return () => { // Return the hash that you can use as a seed hash = Math.imul(hash ^ (hash >>> 16), 2246822507); hash = Math.imul(hash ^ (hash >>> 13), 3266489909); return (hash ^= hash >>> 16) >>> 0; }; } function SimpleFastCounter32(seed_1, seed_2, seed_3, seed_4) { return () => { seed_1 >>>= 0; seed_2 >>>= 0; seed_3 >>>= 0; seed_4 >>>= 0; let cast32 = (seed_1 + seed_2) | 0; seed_1 = seed_2 ^ seed_2 >>> 9; seed_2 = seed_3 + (seed_3 << 3) | 0; seed_3 = (seed_3 << 21 | seed_3 >>> 11); seed_4 = seed_4 + 1 | 0; cast32 = cast32 + seed_4 | 0; seed_3 = seed_3 + cast32 | 0; return (cast32 >>> 0) / 4294967296; }; } const RANDOM_SEED = "OMG ur so random!! 😜"; const seeder = MurmurHash3(RANDOM_SEED); const rand = SimpleFastCounter32(seeder(), seeder(), seeder(), seeder()); // making a synthetic deck from our bins function synthesizeDeck(deckBins, tapBins, costBins, deckInfo) { let uselessLands = deckInfo.landCount - Object.keys(deckBins) .reduce((sum, typeKey) => sum + (/^(O)|(T\,O)$/.test(typeKey) ? 0 : deckBins[typeKey]), 0); const deck = []; const cardCost = { ...costBins }; delete cardCost.O; delete cardCost.T; let targetCard = null; Object.keys(deckBins).forEach(deckBinKey => { for (let i = 0; i < deckBins[deckBinKey]; i++) { const card = {}; if (i < tapBins[deckBinKey]) { card.isTapLand = true; } if (deckBinKey !== "O" && deckBinKey !== "T,O") { card.producibleManaColors = new Set(deckBinKey.split(",").filter(type => !["O", "T"].includes(type))); card.isLand = true; } else if (deckBinKey === "T,O") { card.isLand = false; card.name = "targetCard"; card.manaCost = cardCost; card.CMC = Object.values(card.manaCost).reduce(sum, 0); targetCard = card; } else if (uselessLands > 0) { card.producibleManaColors = new Set([]); card.isLand = true; uselessLands--; } else { card.isLand = false; card.name = null; } deck.push(card); } }); return { deck, targetCard }; } // simulates drawing cards "iterations" number of times and records -for each non land card in the deck- how many times it was playable on each turn up to turn "turns" function simulateDraws(deckBins, tapBins, costBins, deckInfo, turns, iterations, startingHandSize = 7) { let cardCurve = new Array(turns).fill(0).map(_ => ({ played: 0, targetDrawn: 0, enoughLands: 0 })); const enumerationCache = {}; const { deck, targetCard } = synthesizeDeck(deckBins, tapBins, costBins, deckInfo); for (let i = 0; i < iterations && targetCard; i++) { // preparing for next iteration let deckCopy = deck.slice(); let landList = []; let tapLandList = []; let uselessLandsDrawn = 0; let lastCardDrawnIsTapland = false; let haveTargetCard = false; let targetCardPlayedCurve = new Array(turns).fill(0).map(_ => ({ played: false, targetDrawn: false, enoughLands: false })); // "draws" a card and puts it tallies/saves it for computation later function placeCard(card) { lastCardDrawnIsTapland = !!card.isTapLand; if (card.isLand) { if (card.producibleManaColors.size === 0) uselessLandsDrawn++; else if (card.isTapLand) tapLandList.push(card); else landList.push(card); } else if (card.name === "targetCard") { haveTargetCard = true; } } // preparing deck fisherYates(deckCopy); for (let j = 0; j < startingHandSize; j++) { let nextCard = deckCopy.pop(); placeCard(nextCard); } // for each turn for (let turn = 0; turn < Math.min(turns, deck.length - 6); turn++) { // count all the drawn lands so we can compute conditional stats later if (uselessLandsDrawn + landList.length + tapLandList.length >= targetCard.CMC) targetCardPlayedCurve[turn].enoughLands = true; // check if each nonland card drawn thus far is playable, if it was playable last turn it still is if (haveTargetCard) { // if the last card drawn was a tapland and it without it there aren"t enough lands, the card is not playable if (lastCardDrawnIsTapland && tapLandList.length + landList.length === targetCard.CMC) targetCardPlayedCurve[turn].played = false; // if card was playable last turn, it still is else if (turn > 0 && targetCardPlayedCurve[turn - 1].played) targetCardPlayedCurve[turn].played = true; // else find out if it's playable by simulating paying the mana cost else if (isPlayable(targetCard, landList, turn + 1, enumerationCache)) targetCardPlayedCurve[turn].played = true; targetCardPlayedCurve[turn].targetDrawn = true; } // if this was our last turn, just stop if (turn === turns) break; // otherwise push the taplands drawn this turn into the list with the other land cards and darw/place our next card in anticipation of next turn if (tapLandList.length) landList = landList.concat(...tapLandList); tapLandList = []; let nextCard = deckCopy.pop(); if (!nextCard) break; placeCard(nextCard); } // now that all our cards for this iteration have been registered as playable or not, lets add some 1's to our counts cardCurve = cardCurve.map((countDict, i) => { Object.keys(countDict).forEach(countKey => { countDict[countKey] = targetCardPlayedCurve[i][countKey] ? countDict[countKey] + 1 : countDict[countKey]; }); return countDict; }); } // return our results return postProcessResults(cardCurve, iterations); } // making our results into stats function postProcessResults(cardCurve, iterations) { return cardCurve.map(countDict => ({ independent: (iterations ? countDict.played / iterations : 0).toString(), conditionalTargetDrawn: (countDict.targetDrawn ? countDict.played / countDict.targetDrawn : 0).toString(), conditionalEnoughLand: (countDict.enoughLands ? countDict.played / countDict.enoughLands : 0).toString() })); } // returns bool of if the card is playable given the state of the available lands and the turn function isPlayable(card, landList, turn, enumerationCache = null) { const enoughTurns = turn >= card.CMC; const enoughLands = landList.length >= card.CMC; if (!enoughTurns || !enoughLands) return false; const landCombinations = nCkBitMasks(landList.length, turn, enumerationCache); for (let i = 0; i < landCombinations.length; i++) { const landCombo = landList.filter((_, j) => ((1 << j) & landCombinations[i]) > 0); // this line of logic takes care of taplands const everyLandInSelectionIsTapland = landCombo.every(land => land.isTapLand); if (landCombo.length === card.CMC && turn === card.CMC && everyLandInSelectionIsTapland) continue; const costCopy = copyShallow(card.manaCost); const availableMana = availableUsefulMana(costCopy, landCombo); payMana(costCopy, availableMana); if (Object.keys(costCopy).every(key => costCopy[key] <= 0)) return true; } return false; } // alg for paying mana intelligently based on a cards cost and the possible mana available function payMana(cost, manaBase) { let scarcities = {}; Object.keys(cost).forEach(color => { scarcities[color] = 0; Object.keys(manaBase).forEach(type => { if (type.split(",").includes(color)) scarcities[color]++; }); }); let sortedKeys = Object.keys(scarcities).sort((a, b) => scarcities[a] - scarcities[b]); sortedKeys.forEach(color => { Object.keys(manaBase).forEach(type => { if (type.split(",").includes(color)) { let min = Math.min(manaBase[type], cost[color]); cost[color] -= min; manaBase[type] -= min; } }); }); if (cost.C) { Object.keys(manaBase).forEach(type => { if (type !== "F" && cost.C) { let min = Math.min(manaBase[type], cost.C); cost.C -= min; manaBase[type] -= min; } }); } } function nCkBitMasks(n, k, cache = null) { let results = []; if (n < k) return [(1 << n) - 1]; if (cache && cache[`${n},${k}`]) return cache[`${n},${k}`]; function generate(kRemaining = k, order = 0, b = 0) { if (order > n && kRemaining === 0) results.push(b); else if (kRemaining <= n - order && kRemaining >= 0) { generate(kRemaining - 1, order + 1, b | Math.pow(2, order)); generate(kRemaining, order + 1, b); } } generate(); if (cache) cache[`${n},${k}`] = results; return results; } // finds useful available mana from landlist function availableUsefulMana(cardCost, landList) { const availableMana = {}; // making deck bins which are dependant on the cost of the card for (let i = 0; i < landList.length; i++) { let land = landList[i]; // all possible types the land can produce let usefulProducibleColors = new Set(); let producibleColors = land.producibleManaColors; producibleColors.forEach(producibleColor => { for (necessaryColor in cardCost) { // for each color the land produces, if that land could pay for any of the types of the cost bins, hand onto it const producesThisColor = (necessaryColor === "C" || necessaryColor.includes(producibleColor)); if (producesThisColor) usefulProducibleColors.add(necessaryColor); } }); usefulProducibleColors.forEach(color => { availableMana[color] = availableMana[color] ? availableMana[color] + 1 : 1; }); } return availableMana; } // shuffling in place function fisherYates(array) { for (let i = array.length - 1; i >= 0; i--) { const r = Math.floor(rand() * (i + 1)); array.push(array.splice(r, 1)[0]); } } // reduce callback function sum(a, b) { return a + b; } // for copying one-layer-deep arrays or json function copyShallow(variable) { let variableCopy; if (Array.isArray(variable)) variableCopy = []; else variableCopy = {}; for (let key in variable) { variableCopy[key] = variable[key]; } return variableCopy; } module.exports = { fisherYates, simulateDraws, rand, MurmurHash3 };